mirror of
https://github.com/airgradienthq/arduino.git
synced 2025-12-30 02:38:05 +01:00
Compare commits
6 Commits
develop
...
feat/satel
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
805b42dbb1 | ||
|
|
25669d4846 | ||
|
|
dbc0b2070f | ||
|
|
2e813f72b9 | ||
|
|
86e2ddcb61 | ||
|
|
d87a354b1c |
@@ -27,6 +27,7 @@ CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License
|
||||
|
||||
*/
|
||||
#include "AgConfigure.h"
|
||||
#include "AgSatellites.h"
|
||||
#include "AgSchedule.h"
|
||||
#include "AgStateMachine.h"
|
||||
#include "AgValue.h"
|
||||
@@ -99,6 +100,7 @@ static TaskHandle_t mqttTask = NULL;
|
||||
static Configuration configuration(Serial);
|
||||
static Measurements measurements(configuration);
|
||||
static AirGradient *ag;
|
||||
static AgSatellites *satellites = nullptr;
|
||||
static OledDisplay oledDisplay(configuration, measurements, Serial);
|
||||
static StateMachine stateMachine(oledDisplay, Serial, measurements, configuration);
|
||||
static WifiConnector wifiConnector(oledDisplay, Serial, stateMachine, configuration);
|
||||
@@ -210,6 +212,12 @@ void setup() {
|
||||
localServer.setAirGraident(ag);
|
||||
measurements.setAirGradient(ag);
|
||||
|
||||
if (configuration.isSatellitesEnabled()) {
|
||||
satellites = new AgSatellites(measurements, configuration);
|
||||
measurements.setSatellites(satellites);
|
||||
Serial.println("Satellites enabled on boot");
|
||||
}
|
||||
|
||||
/** Init sensor */
|
||||
boardInit();
|
||||
setMeasurementMaxPeriod();
|
||||
@@ -348,6 +356,11 @@ void loop() {
|
||||
}
|
||||
}
|
||||
|
||||
/* Run satellite BLE scanning */
|
||||
if (satellites != nullptr) {
|
||||
satellites->run();
|
||||
}
|
||||
|
||||
/* Run measurement schedule */
|
||||
printMeasurementsSchedule.run();
|
||||
|
||||
@@ -1142,6 +1155,15 @@ static void configUpdateHandle() {
|
||||
}
|
||||
}
|
||||
|
||||
if (configuration.isSatellitesChanged() && configuration.isSatellitesEnabled()) {
|
||||
if (satellites == nullptr) {
|
||||
// Initialized if satellites enabled on run time
|
||||
satellites = new AgSatellites(measurements, configuration);
|
||||
measurements.setSatellites(satellites);
|
||||
Serial.println("Satellites enabled on runtime");
|
||||
}
|
||||
}
|
||||
|
||||
// Update display and led bar notification based on updated configuration
|
||||
updateDisplayAndLedBar();
|
||||
}
|
||||
|
||||
@@ -61,6 +61,7 @@ JSON_PROP_DEF(corrections);
|
||||
JSON_PROP_DEF(atmp);
|
||||
JSON_PROP_DEF(rhum);
|
||||
JSON_PROP_DEF(extendedPmMeasures);
|
||||
JSON_PROP_DEF(satellites);
|
||||
|
||||
#define jprop_model_default ""
|
||||
#define jprop_country_default "TH"
|
||||
@@ -317,6 +318,95 @@ bool Configuration::updateTempHumCorrection(JSONVar &json, TempHumCorrection &ta
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Configuration::updateSatellites(JSONVar &json) {
|
||||
if (!json.hasOwnProperty(jprop_satellites)) {
|
||||
emptySatellites();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (JSON.typeof_(json[jprop_satellites]) != "array") {
|
||||
emptySatellites();
|
||||
return false;
|
||||
}
|
||||
|
||||
JSONVar satellites = json[jprop_satellites];
|
||||
|
||||
if (satellites.length() == 0) {
|
||||
if (_satellitesEnabled) {
|
||||
// No satellites available and previously its enabled
|
||||
emptySatellites();
|
||||
return true; // then config is changed
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool changed = false;
|
||||
|
||||
// Check JSON → stored (new satellites)
|
||||
for (int i = 0; i < satellites.length(); i++) {
|
||||
bool found = false;
|
||||
|
||||
for (int j = 0; j < MAX_SATELLITES; j++) {
|
||||
if (_satellites[j] == (const char *)satellites[i]) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
changed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check stored → JSON (removed satellites) only if no new satellites
|
||||
if (!changed) {
|
||||
for (int j = 0; j < MAX_SATELLITES; j++) {
|
||||
if (_satellites[j].length() == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
bool found = false;
|
||||
for (int i = 0; i < satellites.length(); i++) {
|
||||
if (_satellites[j] == (const char *)satellites[i]) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
changed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sync stored list with JSON
|
||||
if (changed) {
|
||||
emptySatellites();
|
||||
int count = min((int)satellites.length(), MAX_SATELLITES);
|
||||
for (int i = 0; i < count; i++) {
|
||||
_satellites[i] = (const char *)satellites[i];
|
||||
}
|
||||
|
||||
_satellitesChanged = true;
|
||||
// Deep copy satellites from root to jconfig, so it will be saved later
|
||||
jconfig[jprop_satellites] = satellites;
|
||||
}
|
||||
|
||||
// Ensure flag is set
|
||||
_satellitesEnabled = true;
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
void Configuration::emptySatellites() {
|
||||
for (int j = 0; j < MAX_SATELLITES; j++) {
|
||||
_satellites[j] = "";
|
||||
}
|
||||
_satellitesEnabled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Save configure to device storage (EEPROM)
|
||||
*
|
||||
@@ -852,6 +942,11 @@ bool Configuration::parse(String data, bool isLocal) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for satellites
|
||||
if (updateSatellites(root)) {
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
ledBarBrightnessChanged = false;
|
||||
@@ -1656,3 +1751,13 @@ Configuration::PMCorrection Configuration::getPMCorrection(void) { return pmCorr
|
||||
Configuration::TempHumCorrection Configuration::getTempCorrection(void) { return tempCorrection; }
|
||||
|
||||
Configuration::TempHumCorrection Configuration::getHumCorrection(void) { return rhumCorrection; }
|
||||
|
||||
bool Configuration::isSatellitesChanged(void) {
|
||||
bool changed = _satellitesChanged;
|
||||
_satellitesChanged = false;
|
||||
return changed;
|
||||
}
|
||||
|
||||
bool Configuration::isSatellitesEnabled(void) { return _satellitesEnabled; }
|
||||
|
||||
const String *Configuration::getSatellites() const { return _satellites; }
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
#include <Arduino.h>
|
||||
#include "Libraries/Arduino_JSON/src/Arduino_JSON.h"
|
||||
|
||||
#define MAX_SATELLITES 10
|
||||
|
||||
class Configuration : public PrintLog {
|
||||
public:
|
||||
struct PMCorrection {
|
||||
@@ -40,6 +42,9 @@ private:
|
||||
PMCorrection pmCorrection;
|
||||
TempHumCorrection tempCorrection;
|
||||
TempHumCorrection rhumCorrection;
|
||||
bool _satellitesEnabled = false;
|
||||
String _satellites[MAX_SATELLITES];
|
||||
bool _satellitesChanged = false;
|
||||
|
||||
AirGradient* ag;
|
||||
|
||||
@@ -49,6 +54,8 @@ private:
|
||||
bool updatePmCorrection(JSONVar &json);
|
||||
bool updateTempHumCorrection(JSONVar &json, TempHumCorrection &target,
|
||||
const char *correctionName);
|
||||
bool updateSatellites(JSONVar &json);
|
||||
void emptySatellites();
|
||||
void saveConfig(void);
|
||||
void loadConfig(void);
|
||||
void defaultConfig(void);
|
||||
@@ -122,6 +129,10 @@ public:
|
||||
PMCorrection getPMCorrection(void);
|
||||
TempHumCorrection getTempCorrection(void);
|
||||
TempHumCorrection getHumCorrection(void);
|
||||
bool isSatellitesChanged(void);
|
||||
bool isSatellitesEnabled(void);
|
||||
const String* getSatellites() const;
|
||||
|
||||
private:
|
||||
ConfigurationUpdatedCallback_t _callback;
|
||||
};
|
||||
|
||||
246
src/AgSatellites.cpp
Normal file
246
src/AgSatellites.cpp
Normal file
@@ -0,0 +1,246 @@
|
||||
#include "AgSatellites.h"
|
||||
|
||||
AgSatellites::AgSatellites(Measurements &measurement, Configuration &config)
|
||||
: _measurements(measurement), _config(config), _pScan(nullptr), _initialized(false),
|
||||
_scanCallbacks(nullptr) {
|
||||
// Initialize satellite array
|
||||
for (int i = 0; i < MAX_SATELLITES; i++) {
|
||||
_satellites[i].id = "";
|
||||
_satellites[i].data.temp = -1000.0f;
|
||||
_satellites[i].data.rhum = -1.0f;
|
||||
_satellites[i].data.useCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
AgSatellites::~AgSatellites() {
|
||||
// Stop scan if running
|
||||
if (_pScan && _initialized) {
|
||||
_pScan->stop();
|
||||
}
|
||||
|
||||
// Cleanup callbacks
|
||||
if (_scanCallbacks) {
|
||||
delete _scanCallbacks;
|
||||
_scanCallbacks = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool AgSatellites::run() {
|
||||
// Check if satellites are enabled in config
|
||||
if (!_config.isSatellitesEnabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Lazy initialization on first run
|
||||
if (!_initialized) {
|
||||
// Initialize NimBLE
|
||||
NimBLEDevice::init("AgSatelliteScanner");
|
||||
|
||||
// Get scan instance
|
||||
_pScan = NimBLEDevice::getScan();
|
||||
if (!_pScan) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create and set scan callbacks
|
||||
_scanCallbacks = new ScanCallbacks(this);
|
||||
_pScan->setScanCallbacks(_scanCallbacks, true);
|
||||
|
||||
// Configure scan parameters
|
||||
// TODO: Might need to adjust for reliablity coex wifi
|
||||
_pScan->setInterval(100); // Scan interval in ms
|
||||
_pScan->setWindow(99); // Scan window in ms
|
||||
_pScan->setActiveScan(true); // Active scan for scan response data
|
||||
|
||||
// Start continuous scanning (0 = scan forever)
|
||||
_pScan->start(0, false);
|
||||
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
// Check if scan is still running, restart if needed
|
||||
if (_pScan && !_pScan->isScanning()) {
|
||||
_pScan->start(0, false);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AgSatellites::isSatelliteInList(String macAddress) {
|
||||
const String *satellites = _config.getSatellites();
|
||||
if (!satellites) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if MAC address is in the configured satellite list
|
||||
for (int i = 0; i < MAX_SATELLITES; i++) {
|
||||
if (satellites[i].length() > 0 && satellites[i].equalsIgnoreCase(macAddress)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void AgSatellites::processAdvertisedDevice(const NimBLEAdvertisedDevice *device) {
|
||||
if (!device) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get MAC address
|
||||
String macAddress = device->getAddress().toString().c_str();
|
||||
|
||||
// Check if this device is in our satellite list
|
||||
if (!isSatelliteInList(macAddress)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Serial.printf("Found satellites: %s\n", macAddress.c_str());
|
||||
|
||||
// Get advertising payload
|
||||
const std::vector<uint8_t> &payload = device->getPayload();
|
||||
if (payload.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find or create entry in satellites array
|
||||
int index = -1;
|
||||
bool isNewEntry = false;
|
||||
|
||||
for (int i = 0; i < MAX_SATELLITES; i++) {
|
||||
if (_satellites[i].id == macAddress) {
|
||||
index = i;
|
||||
break;
|
||||
} else if (_satellites[i].id.length() == 0 && index == -1) {
|
||||
// Found empty slot
|
||||
index = i;
|
||||
isNewEntry = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (index >= 0) {
|
||||
// TODO: What happen if old satellites is removed?
|
||||
// Only set ID if it's a new entry
|
||||
if (isNewEntry) {
|
||||
_satellites[index].id = macAddress;
|
||||
}
|
||||
|
||||
// Parse BTHome advertising data
|
||||
SatelliteData newData;
|
||||
if (decodeBTHome(payload.data(), payload.size(), newData)) {
|
||||
// Successfully parsed - reset use count when new data is received
|
||||
_satellites[index].data.temp = newData.temp;
|
||||
_satellites[index].data.rhum = newData.rhum;
|
||||
_satellites[index].data.useCount = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool AgSatellites::decodeBTHome(const uint8_t *payload, size_t size, SatelliteData &data) {
|
||||
// Initialize with invalid values
|
||||
data.temp = -1000.0f;
|
||||
data.rhum = -1.0f;
|
||||
|
||||
// Walk through BLE AD structures: [len][type][data...]
|
||||
for (size_t i = 0; i + 1 < size;) {
|
||||
uint8_t len = payload[i];
|
||||
if (len == 0) {
|
||||
break; // No more AD structures
|
||||
}
|
||||
|
||||
// Check if we have enough data
|
||||
if (i + 1 + len > size) {
|
||||
break; // Malformed
|
||||
}
|
||||
|
||||
uint8_t type = payload[i + 1];
|
||||
|
||||
// Service Data - 16-bit UUID (type 0x16)
|
||||
if (type == 0x16 && len >= 3) {
|
||||
// UUID is at i+2 (little-endian)
|
||||
uint16_t uuid = payload[i + 2] | (payload[i + 3] << 8);
|
||||
|
||||
// Check for BTHome UUID (0xFCD2)
|
||||
if (uuid == 0xFCD2) {
|
||||
// BTHome payload begins after UUID
|
||||
size_t p = i + 4;
|
||||
size_t end = i + 1 + len; // End of this AD structure
|
||||
|
||||
if (p >= end) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t device_info = payload[p++];
|
||||
|
||||
// Check if encrypted
|
||||
bool encrypted = (device_info & 0x01);
|
||||
if (encrypted) {
|
||||
// Not handling encryption
|
||||
return false;
|
||||
}
|
||||
|
||||
bool found_data = false;
|
||||
|
||||
// Parse BTHome v2 objects
|
||||
while (p < end) {
|
||||
uint8_t obj_id = payload[p++];
|
||||
|
||||
switch (obj_id) {
|
||||
case 0x02: // Temperature (sint16, ×0.01 °C)
|
||||
if (p + 2 > end)
|
||||
return false;
|
||||
{
|
||||
int16_t raw = (int16_t)(payload[p] | (payload[p + 1] << 8));
|
||||
data.temp = raw * 0.01f;
|
||||
p += 2;
|
||||
found_data = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case 0x03: // Humidity (uint16, ×0.01 %)
|
||||
if (p + 2 > end)
|
||||
return false;
|
||||
{
|
||||
uint16_t raw = (uint16_t)(payload[p] | (payload[p + 1] << 8));
|
||||
data.rhum = raw * 0.01f;
|
||||
p += 2;
|
||||
found_data = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case 0x00: // Packet ID (uint8) - skip
|
||||
if (p + 1 > end)
|
||||
return false;
|
||||
p += 1;
|
||||
break;
|
||||
|
||||
case 0x01: // Battery (uint8) - skip
|
||||
if (p + 1 > end)
|
||||
return false;
|
||||
p += 1;
|
||||
break;
|
||||
|
||||
case 0x0C: // Voltage (uint16) - skip
|
||||
if (p + 2 > end)
|
||||
return false;
|
||||
p += 2;
|
||||
break;
|
||||
|
||||
default:
|
||||
// Unknown object ID - stop parsing but return what we have
|
||||
return found_data;
|
||||
}
|
||||
}
|
||||
|
||||
return found_data;
|
||||
}
|
||||
}
|
||||
|
||||
// Go to next AD structure
|
||||
i += 1 + len;
|
||||
}
|
||||
|
||||
return false; // No BTHome data found
|
||||
}
|
||||
|
||||
AgSatellites::Satellite *AgSatellites::getSatellites() { return _satellites; }
|
||||
59
src/AgSatellites.h
Normal file
59
src/AgSatellites.h
Normal file
@@ -0,0 +1,59 @@
|
||||
#ifndef _AG_SATELLITES_H_
|
||||
#define _AG_SATELLITES_H_
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <NimBLEDevice.h>
|
||||
#include "AgConfigure.h"
|
||||
#include "AgValue.h"
|
||||
|
||||
class AgSatellites {
|
||||
public:
|
||||
struct SatelliteData {
|
||||
float temp;
|
||||
float rhum;
|
||||
uint8_t useCount;
|
||||
};
|
||||
|
||||
struct Satellite {
|
||||
String id;
|
||||
SatelliteData data;
|
||||
};
|
||||
|
||||
private:
|
||||
Satellite _satellites[MAX_SATELLITES];
|
||||
Measurements &_measurements;
|
||||
Configuration &_config;
|
||||
NimBLEScan *_pScan;
|
||||
bool _initialized;
|
||||
|
||||
// NimBLE scan callback class
|
||||
class ScanCallbacks : public NimBLEScanCallbacks {
|
||||
private:
|
||||
AgSatellites *_parent;
|
||||
|
||||
public:
|
||||
ScanCallbacks(AgSatellites *parent) : _parent(parent) {}
|
||||
|
||||
void onResult(const NimBLEAdvertisedDevice *advertisedDevice) override {
|
||||
if (_parent) {
|
||||
_parent->processAdvertisedDevice(advertisedDevice);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ScanCallbacks *_scanCallbacks;
|
||||
|
||||
// Helper methods
|
||||
bool isSatelliteInList(String macAddress);
|
||||
void processAdvertisedDevice(const NimBLEAdvertisedDevice *device);
|
||||
bool decodeBTHome(const uint8_t* payload, size_t size, SatelliteData &data);
|
||||
|
||||
public:
|
||||
AgSatellites(Measurements &measurement, Configuration &config);
|
||||
~AgSatellites();
|
||||
|
||||
bool run();
|
||||
Satellite* getSatellites();
|
||||
};
|
||||
|
||||
#endif // _AG_SATELLITES_H_
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "AgValue.h"
|
||||
#include "AgConfigure.h"
|
||||
#include "AgSatellites.h"
|
||||
#include "AirGradient.h"
|
||||
#include "App/AppDef.h"
|
||||
#include <cmath>
|
||||
@@ -76,6 +77,8 @@ Measurements::Measurements(Configuration &config) : config(config) {
|
||||
|
||||
void Measurements::setAirGradient(AirGradient *ag) { this->ag = ag; }
|
||||
|
||||
void Measurements::setSatellites(AgSatellites* satellites) { this->satellites_ = satellites; }
|
||||
|
||||
void Measurements::printCurrentAverage() {
|
||||
Serial.println();
|
||||
if (config.hasSensorS8) {
|
||||
@@ -1106,6 +1109,31 @@ String Measurements::toString(bool localServer, AgFirmwareMode fwMode, int rssi)
|
||||
#endif
|
||||
}
|
||||
|
||||
// Add satellites data
|
||||
if (satellites_ && config.isSatellitesEnabled()) {
|
||||
AgSatellites::Satellite* satellites = satellites_->getSatellites();
|
||||
JSONVar satellitesObj;
|
||||
int count = 0;
|
||||
|
||||
for (int i = 0; i < MAX_SATELLITES; i++) {
|
||||
if (satellites[i].id.length() > 0 &&
|
||||
satellites[i].data.useCount < 2 &&
|
||||
utils::isValidTemperature(satellites[i].data.temp) &&
|
||||
utils::isValidHumidity(satellites[i].data.rhum)) {
|
||||
|
||||
String macKey = satellites[i].id;
|
||||
satellitesObj[macKey.c_str()]["atmp"] = ag->round2(satellites[i].data.temp);
|
||||
satellitesObj[macKey.c_str()]["rhum"] = ag->round2(satellites[i].data.rhum);
|
||||
satellites[i].data.useCount++;
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
if (count > 0) {
|
||||
root["satellites"] = satellitesObj;
|
||||
}
|
||||
}
|
||||
|
||||
String result = JSON.stringify(root);
|
||||
Serial.printf("\n---- PAYLOAD\n %s \n-----\n", result.c_str());
|
||||
return result;
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
// Forward declaration
|
||||
class AgSatellites;
|
||||
|
||||
class Measurements {
|
||||
private:
|
||||
// Generic struct for update indication for respective value
|
||||
@@ -65,6 +68,7 @@ public:
|
||||
};
|
||||
|
||||
void setAirGradient(AirGradient *ag);
|
||||
void setSatellites(AgSatellites* satellites);
|
||||
|
||||
// Enumeration for every AG measurements
|
||||
enum MeasurementType {
|
||||
@@ -202,6 +206,7 @@ public:
|
||||
private:
|
||||
Configuration &config;
|
||||
AirGradient *ag;
|
||||
AgSatellites* satellites_ = nullptr;
|
||||
|
||||
// Some declared as an array (channel), because FW_MODE_O_1PPx has two PMS5003T
|
||||
FloatValue _temperature[2];
|
||||
|
||||
Reference in New Issue
Block a user