Merge branch 'dev' into gps-l76k-constellation-init-m6

This commit is contained in:
fdlamotte authored and GitHub committed 2026-09-26 13:38:40 -04:00
commit 6c7acd2bb1
162 files changed
+6589 -838

No files matched your search

+8 -7
View File
@@ -64,21 +64,22 @@ public:
virtual uint8_t getStartupReason() const = 0;
virtual bool getBootloaderVersion(char* version, size_t max_len) { return false; }
virtual bool startOTAUpdate(const char* id, char reply[]) { return false; } // not supported
virtual bool setLoRaFemLnaEnabled(bool enable) { return false; }
virtual bool canControlLoRaFemLna() const { return false; }
virtual bool isLoRaFemLnaEnabled() const { return false; }
// Software-selectable external FEM transmit gain. This is not a PA power switch.
virtual bool setLoRaFemPaGainEnabled(bool enable) { return false; }
virtual bool canControlLoRaFemPaGain() const { return false; }
virtual bool isLoRaFemPaGainEnabled() const { return false; }
// Power management interface (boards with power management override these)
virtual bool isPwrMgtInitialised() const { return false; }
virtual bool isExternalPowered() { return false; }
virtual uint16_t getBootVoltage() { return 0; }
virtual bool getWakeLpcompSupported() const { return false; }
virtual uint32_t getResetReason() const { return 0; }
virtual const char* getResetReasonString(uint32_t reason) { return "Not available"; }
virtual uint8_t getShutdownReason() const { return 0; }
virtual const char* getShutdownReasonString(uint8_t reason) { return "Not available"; }
virtual bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply) { return false; }
// Called from the example main loops. Lets a board feed its watchdog and
// run periodic housekeeping. Default no-op.
virtual void loop() { /* no op */ }
};
/**
+9
View File
@@ -203,6 +203,15 @@ bool Utils::isHexChar(char c) {
return c == '0' || hexVal(c) > 0;
}
bool Utils::isZeroes(const uint8_t* buf, size_t len) {
while (len > 0) {
if (*buf != 0) return false;
buf++;
len--;
}
return true;
}
bool Utils::fromHex(uint8_t* dest, int dest_size, const char *src_hex) {
int len = strlen(src_hex);
if (len != dest_size*2) return false; // incorrect length
+2
View File
@@ -82,6 +82,8 @@ public:
static int parseTextParts(char* text, const char* parts[], int max_num, char separator=',');
static bool isHexChar(char c);
static bool isZeroes(const uint8_t* buf, size_t len);
};
}
+9
View File
@@ -0,0 +1,9 @@
// https://github.com/meshcore-dev/MeshCore/issues/2469
// armstubs.cpp exists to silence warnings introduced after upgrading platformio/toolchain-gccarmnoneeabi
extern "C" __attribute__((weak)) int _close(int fd) { return -1; }
extern "C" __attribute__((weak)) int _lseek(int fd, int offset, int whence) { return 0; }
extern "C" __attribute__((weak)) int _read(int fd, char *buf, int len) { return 0; }
extern "C" __attribute__((weak)) int _fstat(int fd, struct stat *st) { return 0; }
extern "C" __attribute__((weak)) int _isatty(int fd) { return 1; }
extern "C" __attribute__((weak)) int _getpid(void) { return 1; }
extern "C" __attribute__((weak)) int _kill(int pid, int sig) { return -1; }
+8
View File
@@ -28,6 +28,14 @@
return i;
}
bool AdvertDataParser::isValidName(const char *n) {
while (*n) {
if (*n == '[' || *n == ']' || *n == '\\' || *n == ':' || *n == ',' || *n == '?' || *n == '*') return false;
n++;
}
return true;
}
AdvertDataParser::AdvertDataParser(const uint8_t app_data[], uint8_t app_data_len) {
_name[0] = 0;
_lat = _lon = 0;
+2
View File
@@ -50,6 +50,8 @@ class AdvertDataParser {
public:
AdvertDataParser(const uint8_t app_data[], uint8_t app_data_len);
static bool isValidName(const char* name);
bool isValid() const { return _valid; }
uint8_t getType() const { return _flags & 0x0F; }
uint16_t getFeat1() const { return _extra1; }
+44 -13
View File
@@ -9,6 +9,8 @@
#define TXT_ACK_DELAY 200
#endif
#define CLI_REPLY_DELAY_MILLIS 600
void BaseChatMesh::sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis) {
sendFlood(pkt, delay_millis);
}
@@ -227,8 +229,8 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender
ContactInfo& from = contacts[i];
if (type == PAYLOAD_TYPE_TXT_MSG && len > 5) {
uint32_t timestamp;
memcpy(&timestamp, data, 4); // timestamp (by sender's RTC clock - which could be wrong)
uint32_t sender_timestamp;
memcpy(&sender_timestamp, data, 4); // timestamp (by sender's RTC clock - which could be wrong)
uint8_t flags = data[4] >> 2; // message attempt number, and other flags
// len can be > original length, but 'text' will be padded with zeroes
@@ -236,7 +238,7 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender
if (flags == TXT_TYPE_PLAIN) {
from.lastmod = getRTCClock()->getCurrentTime(); // update last heard time
onMessageRecv(from, packet, timestamp, (const char *) &data[5]); // let UI know
onMessageRecv(from, packet, sender_timestamp, (const char *) &data[5]); // let UI know
int text_len = strlen((char *)&data[5]);
uint8_t ack_hash[6]; // calc truncated hash of the message timestamp + text + sender pub_key, to prove to sender that we got it
@@ -254,20 +256,43 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender
sendAckTo(from, ack_hash, 6);
}
} else if (flags == TXT_TYPE_CLI_DATA) {
onCommandDataRecv(from, packet, timestamp, (const char *) &data[5]); // let UI know
char *text = (char *)&data[5];
onCommandDataRecv(from, packet, sender_timestamp, text); // let UI know
// NOTE: no ack expected for CLI_DATA replies
} else if (flags == TXT_TYPE_CLI_COMMAND) {
uint8_t temp[166];
char *command = (char *)&data[5];
char *reply = (char *)&temp[5];
*reply = 0;
if (packet->isRouteFlood()) {
// let this sender know path TO here, so they can use sendDirect() (NOTE: no ACK as extra)
mesh::Packet* path = createPathReturn(from.id, secret, packet->path, packet->path_len, 0, NULL, 0);
if (path) sendFloodScoped(from, path);
onCLICommandRecv(from, packet, sender_timestamp, command, reply); // let UI know
// NOTE: no ack expected for CLI_COMMAND replies
int text_len = strlen(reply);
if (text_len > 0) {
uint32_t timestamp = getRTCClock()->getCurrentTimeUnique();
if (timestamp == sender_timestamp) {
// WORKAROUND: the two timestamps need to be different, in the CLI view
timestamp++;
}
memcpy(temp, &timestamp, 4);
temp[4] = (TXT_TYPE_CLI_DATA << 2);
auto reply_pkt = createDatagram(PAYLOAD_TYPE_TXT_MSG, from.id, secret, temp, 5 + text_len);
if (reply_pkt) {
if (from.out_path_len == OUT_PATH_UNKNOWN) {
sendFloodScoped(from, reply_pkt, CLI_REPLY_DELAY_MILLIS);
} else {
sendDirect(reply_pkt, from.out_path, from.out_path_len, CLI_REPLY_DELAY_MILLIS);
}
}
}
} else if (flags == TXT_TYPE_SIGNED_PLAIN) {
if (timestamp > from.sync_since) { // make sure 'sync_since' is up-to-date
from.sync_since = timestamp;
if (sender_timestamp > from.sync_since) { // make sure 'sync_since' is up-to-date
from.sync_since = sender_timestamp;
}
from.lastmod = getRTCClock()->getCurrentTime(); // update last heard time
onSignedMessageRecv(from, packet, timestamp, &data[5], (const char *) &data[9]); // let UI know
onSignedMessageRecv(from, packet, sender_timestamp, &data[5], (const char *) &data[9]); // let UI know
uint32_t ack_hash; // calc truncated hash of the message timestamp + text + OUR pub_key, to prove to sender that we got it
mesh::Utils::sha256((uint8_t *) &ack_hash, 4, data, 9 + strlen((char *)&data[9]), self_id.pub_key, PUB_KEY_SIZE);
@@ -368,6 +393,12 @@ void BaseChatMesh::handleReturnPathRetry(const ContactInfo& contact, const uint8
int BaseChatMesh::searchChannelsByHash(const uint8_t* hash, mesh::GroupChannel dest[], int max_matches) {
int n = 0;
for (int i = 0; i < MAX_GROUP_CHANNELS && n < max_matches; i++) {
// Skip empty/unconfigured slots. An empty slot has an all-zero secret and
// therefore matches null-key group traffic (a node transmitting with an
// unset PSK): the zero-key MAC validates against the empty slot and the
// foreign message is delivered as if it belonged to that channel. Any node
// with a free channel slot would otherwise act as a null-key sink.
if (channels[i].name[0] == 0) continue;
if (channels[i].channel.hash[0] == hash[0]) {
dest[n++] = channels[i].channel;
}
@@ -458,13 +489,13 @@ int BaseChatMesh::sendMessage(const ContactInfo& recipient, uint32_t timestamp,
return rc;
}
int BaseChatMesh::sendCommandData(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, const char* text, uint32_t& est_timeout) {
int BaseChatMesh::sendCommandData(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, uint8_t txt_type, const char* text, uint32_t& est_timeout) {
int text_len = strlen(text);
if (text_len > MAX_TEXT_LEN) return MSG_SEND_FAILED;
uint8_t temp[5+MAX_TEXT_LEN+1];
memcpy(temp, &timestamp, 4); // mostly an extra blob to help make packet_hash unique
temp[4] = (attempt & 3) | (TXT_TYPE_CLI_DATA << 2);
temp[4] = (attempt & 3) | (txt_type << 2);
memcpy(&temp[5], text, text_len + 1);
auto pkt = createDatagram(PAYLOAD_TYPE_TXT_MSG, recipient.id, recipient.getSharedSecret(self_id), temp, 5 + text_len);
+2 -1
View File
@@ -114,6 +114,7 @@ protected:
virtual bool onContactPathRecv(ContactInfo& from, uint8_t* in_path, uint8_t in_path_len, uint8_t* out_path, uint8_t out_path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len);
virtual void onMessageRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text) = 0;
virtual void onCommandDataRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text) = 0;
virtual void onCLICommandRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text, char* reply) = 0;
virtual void onSignedMessageRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const uint8_t *sender_prefix, const char *text) = 0;
virtual uint32_t calcFloodTimeoutMillisFor(uint32_t pkt_airtime_millis) const = 0;
virtual uint32_t calcDirectTimeoutMillisFor(uint32_t pkt_airtime_millis, uint8_t path_len) const = 0;
@@ -156,7 +157,7 @@ public:
mesh::Packet* createSelfAdvert(const char* name);
mesh::Packet* createSelfAdvert(const char* name, double lat, double lon);
int sendMessage(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, const char* text, uint32_t& expected_ack, uint32_t& est_timeout);
int sendCommandData(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, const char* text, uint32_t& est_timeout);
int sendCommandData(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, uint8_t txt_type, const char* text, uint32_t& est_timeout);
bool sendGroupMessage(uint32_t timestamp, mesh::GroupChannel& channel, const char* sender_name, const char* text, int text_len);
bool sendGroupData(mesh::GroupChannel& channel, uint8_t* path, uint8_t path_len, uint16_t data_type, const uint8_t* data, int data_len);
int sendLogin(const ContactInfo& recipient, const char* password, uint32_t& est_timeout);
+9 -1
View File
@@ -28,13 +28,21 @@ struct ClientInfo {
unsigned long ack_timeout;
uint8_t push_failures;
} room;
struct {
uint32_t expiry_timestamp; // epoch seconds
uint32_t push_tag;
uint16_t scope_region_id; // scope to use when sending telemetry to this client/subscriber
uint8_t min_deltas_len;
uint8_t min_deltas[14]; // LPP encoded
uint8_t prev_telem[14]; // LPP encoded
} sensor;
} extra;
bool isAdmin() const { return (permissions & PERM_ACL_ROLE_MASK) == PERM_ACL_ADMIN; }
};
#ifndef MAX_CLIENTS
#define MAX_CLIENTS 20
#define MAX_CLIENTS 32
#endif
class ClientACL {
+28 -193
View File
@@ -164,6 +164,7 @@ void CommonCLI::savePrefs() {
_prefs->advert_interval = 0; // turn it off, now that device has been manually configured
}
_callbacks->savePrefs();
_prefs->clearDirty();
}
uint8_t CommonCLI::buildAdvertData(uint8_t node_type, uint8_t* app_data) {
@@ -180,6 +181,16 @@ uint8_t CommonCLI::buildAdvertData(uint8_t node_type, uint8_t* app_data) {
}
void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* reply) {
if (_prefs->getRadioPrefs()->handleCommand(command, sender_timestamp, reply)) { // is a radio CLI command?
if (_prefs->getRadioPrefs()->isDirty()) { savePrefs(); }
return;
}
// hook for variant-specific CLI processing
if (_board->handleCommand(command, sender_timestamp, reply)) {
if (_prefs->isDirty()) { savePrefs(); }
return;
}
if (memcmp(command, "poweroff", 8) == 0 || memcmp(command, "shutdown", 8) == 0) {
_board->powerOff(); // doesn't return
} else if (memcmp(command, "reboot", 6) == 0) {
@@ -344,6 +355,20 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
} else {
strcpy(reply, "gps provider not found");
}
} else if (strcmp(command, "gps diag") == 0) {
LocationProvider * l = _sensors->getLocationProvider();
if (l != NULL) {
const char* setting = _sensors->getSettingByKey("gps");
bool requested = setting != NULL && strcmp(setting, "1") == 0;
int prefix_len = snprintf(reply, 160, "req:%u ", requested ? 1U : 0U);
if (prefix_len < 0 || prefix_len >= 160) {
reply[159] = 0;
} else {
l->formatDiagnostics(reply + prefix_len, 160 - prefix_len);
}
} else {
strcpy(reply, "gps provider not found");
}
} else if (memcmp(command, "gps setloc", 10) == 0) {
_prefs->node_lat = _sensors->node_lat;
_prefs->node_lon = _sensors->node_lon;
@@ -447,39 +472,8 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* reply) {
const char* config = &command[4];
if (memcmp(config, "dutycycle ", 10) == 0) {
float dc = atof(&config[10]);
if (dc < 1 || dc > 100) {
strcpy(reply, "ERROR: dutycycle must be 1-100");
} else {
_prefs->airtime_factor = (100.0f / dc) - 1.0f;
savePrefs();
float actual = 100.0f / (_prefs->airtime_factor + 1.0f);
int a_int = (int)actual;
int a_frac = (int)((actual - a_int) * 10.0f + 0.5f);
sprintf(reply, "OK - %d.%d%%", a_int, a_frac);
}
} else if (memcmp(config, "af ", 3) == 0) {
_prefs->airtime_factor = atof(&config[3]);
savePrefs();
strcpy(reply, "OK");
} else if (memcmp(config, "int.thresh ", 11) == 0) {
_prefs->interference_threshold = atoi(&config[11]);
savePrefs();
strcpy(reply, "OK");
} else if (memcmp(config, "cad ", 4) == 0) {
_prefs->cad_enabled = memcmp(&config[4], "on", 2) == 0;
savePrefs();
strcpy(reply, "OK");
} else if (memcmp(config, "agc.reset.interval ", 19) == 0) {
_prefs->agc_reset_interval = atoi(&config[19]) / 4;
savePrefs();
sprintf(reply, "OK - interval rounded to %d", ((uint32_t) _prefs->agc_reset_interval) * 4);
} else if (memcmp(config, "multi.acks ", 11) == 0) {
_prefs->multi_acks = atoi(&config[11]);
savePrefs();
strcpy(reply, "OK");
} else if (memcmp(config, "allow.read.only ", 16) == 0) {
if (memcmp(config, "allow.read.only ", 16) == 0) {
_prefs->allow_read_only = memcmp(&config[16], "on", 2) == 0;
savePrefs();
strcpy(reply, "OK");
@@ -532,77 +526,6 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
_prefs->disable_fwd = memcmp(&config[7], "off", 3) == 0;
savePrefs();
strcpy(reply, _prefs->disable_fwd ? "OK - repeat is now OFF" : "OK - repeat is now ON");
} else if (memcmp(config, "radio.rxgain ", 13) == 0) {
bool enabled = memcmp(&config[13], "on", 2) == 0;
_prefs->rx_boosted_gain = enabled;
savePrefs();
if (_callbacks->setRxBoostedGain(enabled)) {
strcpy(reply, "OK");
} else {
strcpy(reply, "Error: unsupported");
}
} else if (memcmp(config, "radio.fem.rxgain ", 17) == 0) {
if (!_board->canControlLoRaFemLna()) {
strcpy(reply, "Error: unsupported");
} else if (memcmp(&config[17], "on", 2) == 0) {
if (_board->setLoRaFemLnaEnabled(true)) {
_prefs->radio_fem_rxgain = 1;
savePrefs();
strcpy(reply, "OK - LoRa FEM RX gain on");
} else {
strcpy(reply, "Error: failed to apply LoRa FEM RX gain");
}
} else if (memcmp(&config[17], "off", 3) == 0) {
if (_board->setLoRaFemLnaEnabled(false)) {
_prefs->radio_fem_rxgain = 0;
savePrefs();
strcpy(reply, "OK - LoRa FEM RX gain off");
} else {
strcpy(reply, "Error: failed to apply LoRa FEM RX gain");
}
} else {
strcpy(reply, "Error: state must be on or off");
}
} else if (memcmp(config, "radio.fem.txgain ", 17) == 0) {
if (!_board->canControlLoRaFemPaGain()) {
strcpy(reply, "Error: unsupported");
} else if (memcmp(&config[17], "on", 2) == 0) {
if (_board->setLoRaFemPaGainEnabled(true)) {
_prefs->radio_fem_txgain = 1;
savePrefs();
strcpy(reply, "OK - LoRa FEM TX gain on");
} else {
strcpy(reply, "Error: failed to apply LoRa FEM TX gain");
}
} else if (memcmp(&config[17], "off", 3) == 0) {
if (_board->setLoRaFemPaGainEnabled(false)) {
_prefs->radio_fem_txgain = 0;
savePrefs();
strcpy(reply, "OK - LoRa FEM TX gain off");
} else {
strcpy(reply, "Error: failed to apply LoRa FEM TX gain");
}
} else {
strcpy(reply, "Error: state must be on or off");
}
} else if (memcmp(config, "radio ", 6) == 0) {
strcpy(tmp, &config[6]);
const char *parts[4];
int num = mesh::Utils::parseTextParts(tmp, parts, 4);
float freq = num > 0 ? strtof(parts[0], nullptr) : 0.0f;
float bw = num > 1 ? strtof(parts[1], nullptr) : 0.0f;
uint8_t sf = num > 2 ? atoi(parts[2]) : 0;
uint8_t cr = num > 3 ? atoi(parts[3]) : 0;
if (freq >= 150.0f && freq <= 2500.0f && sf >= 5 && sf <= 12 && cr >= 5 && cr <= 8 && bw >= 7.0f && bw <= 500.0f) {
_prefs->sf = sf;
_prefs->cr = cr;
_prefs->freq = freq;
_prefs->bw = bw;
_callbacks->savePrefs();
strcpy(reply, "OK - reboot to apply");
} else {
strcpy(reply, "Error, invalid radio params");
}
} else if (memcmp(config, "lat ", 4) == 0) {
_prefs->node_lat = atof(&config[4]);
savePrefs();
@@ -611,24 +534,6 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
_prefs->node_lon = atof(&config[4]);
savePrefs();
strcpy(reply, "OK");
} else if (memcmp(config, "rxdelay ", 8) == 0) {
float db = atof(&config[8]);
if (db >= 0 && db <= 20.0f) {
_prefs->rx_delay_base = db;
savePrefs();
strcpy(reply, "OK");
} else {
strcpy(reply, "Error, must be 0-20");
}
} else if (memcmp(config, "txdelay ", 8) == 0) {
float f = atof(&config[8]);
if (f >= 0 && f <= 2.0f) {
_prefs->tx_delay_factor = f;
savePrefs();
strcpy(reply, "OK");
} else {
strcpy(reply, "Error, must be 0-2");
}
} else if (memcmp(config, "flood.max.unscoped ", 19) == 0) {
uint8_t m = atoi(&config[19]);
if (m <= 64) {
@@ -656,15 +561,6 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
} else {
strcpy(reply, "Error, max 64");
}
} else if (memcmp(config, "direct.txdelay ", 15) == 0) {
float f = atof(&config[15]);
if (f >= 0 && f <= 2.0f) {
_prefs->direct_tx_delay_factor = f;
savePrefs();
strcpy(reply, "OK");
} else {
strcpy(reply, "Error, must be 0-2");
}
} else if (memcmp(config, "owner.info ", 11) == 0) {
config += 11;
char *dp = _prefs->owner_info;
@@ -675,16 +571,6 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
*dp = 0;
savePrefs();
strcpy(reply, "OK");
} else if (memcmp(config, "path.hash.mode ", 15) == 0) {
config += 15;
uint8_t mode = atoi(config);
if (mode < 3) {
_prefs->path_hash_mode = mode;
savePrefs();
strcpy(reply, "OK");
} else {
strcpy(reply, "Error, must be 0,1, or 2");
}
} else if (memcmp(config, "loop.detect ", 12) == 0) {
config += 12;
uint8_t mode;
@@ -705,11 +591,6 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
savePrefs();
strcpy(reply, "OK");
}
} else if (memcmp(config, "tx ", 3) == 0) {
_prefs->tx_power_dbm = atoi(&config[3]);
savePrefs();
_callbacks->setTxPower(_prefs->tx_power_dbm);
strcpy(reply, "OK");
} else if (sender_timestamp == 0 && memcmp(config, "freq ", 5) == 0) {
_prefs->freq = atof(&config[5]);
savePrefs();
@@ -806,22 +687,7 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* reply) {
const char* config = &command[4];
if (memcmp(config, "dutycycle", 9) == 0) {
float dc = 100.0f / (_prefs->airtime_factor + 1.0f);
int dc_int = (int)dc;
int dc_frac = (int)((dc - dc_int) * 10.0f + 0.5f);
sprintf(reply, "> %d.%d%%", dc_int, dc_frac);
} else if (memcmp(config, "af", 2) == 0) {
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->airtime_factor));
} else if (memcmp(config, "int.thresh", 10) == 0) {
sprintf(reply, "> %d", (uint32_t) _prefs->interference_threshold);
} else if (memcmp(config, "cad", 3) == 0) {
sprintf(reply, "> %s", _prefs->cad_enabled ? "on" : "off");
} else if (memcmp(config, "agc.reset.interval", 18) == 0) {
sprintf(reply, "> %d", ((uint32_t) _prefs->agc_reset_interval) * 4);
} else if (memcmp(config, "multi.acks", 10) == 0) {
sprintf(reply, "> %d", (uint32_t) _prefs->multi_acks);
} else if (memcmp(config, "allow.read.only", 15) == 0) {
if (memcmp(config, "allow.read.only", 15) == 0) {
sprintf(reply, "> %s", _prefs->allow_read_only ? "on" : "off");
} else if (memcmp(config, "flood.advert.interval", 21) == 0) {
sprintf(reply, "> %d", ((uint32_t) _prefs->flood_advert_interval));
@@ -842,37 +708,12 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->node_lat));
} else if (memcmp(config, "lon", 3) == 0) {
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->node_lon));
} else if (memcmp(config, "radio.rxgain", 12) == 0) {
sprintf(reply, "> %s", _prefs->rx_boosted_gain ? "on" : "off");
} else if (memcmp(config, "radio.fem.rxgain", 16) == 0) {
if (!_board->canControlLoRaFemLna()) {
strcpy(reply, "Error: unsupported");
} else {
sprintf(reply, "> %s", _board->isLoRaFemLnaEnabled() ? "on" : "off");
}
} else if (memcmp(config, "radio.fem.txgain", 16) == 0) {
if (!_board->canControlLoRaFemPaGain()) {
strcpy(reply, "Error: unsupported");
} else {
sprintf(reply, "> %s", _board->isLoRaFemPaGainEnabled() ? "on" : "off");
}
} else if (memcmp(config, "radio", 5) == 0) {
char freq[16], bw[16];
strcpy(freq, StrHelper::ftoa(_prefs->freq));
strcpy(bw, StrHelper::ftoa3(_prefs->bw));
sprintf(reply, "> %s,%s,%d,%d", freq, bw, (uint32_t)_prefs->sf, (uint32_t)_prefs->cr);
} else if (memcmp(config, "rxdelay", 7) == 0) {
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->rx_delay_base));
} else if (memcmp(config, "txdelay", 7) == 0) {
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->tx_delay_factor));
} else if (memcmp(config, "flood.max.advert", 16) == 0) {
sprintf(reply, "> %d", (uint32_t)_prefs->flood_max_advert);
} else if (memcmp(config, "flood.max.unscoped", 18) == 0) {
sprintf(reply, "> %d", (uint32_t)_prefs->flood_max_unscoped);
} else if (memcmp(config, "flood.max", 9) == 0) {
sprintf(reply, "> %d", (uint32_t)_prefs->flood_max);
} else if (memcmp(config, "direct.txdelay", 14) == 0) {
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->direct_tx_delay_factor));
} else if (memcmp(config, "owner.info", 10) == 0) {
auto start = reply;
*reply++ = '>';
@@ -883,8 +724,6 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
sp++;
}
*reply = 0; // set null terminator
} else if (memcmp(config, "path.hash.mode", 14) == 0) {
sprintf(reply, "> %d", (uint32_t)_prefs->path_hash_mode);
} else if (memcmp(config, "loop.detect", 11) == 0) {
if (_prefs->loop_detect == LOOP_DETECT_OFF) {
strcpy(reply, "> off");
@@ -895,10 +734,6 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
} else {
strcpy(reply, "> strict");
}
} else if (memcmp(config, "tx", 2) == 0 && (config[2] == 0 || config[2] == ' ')) {
sprintf(reply, "> %d", (int32_t) _prefs->tx_power_dbm);
} else if (memcmp(config, "freq", 4) == 0) {
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->freq));
} else if (memcmp(config, "public.key", 10) == 0) {
strcpy(reply, "> ");
mesh::Utils::toHex(&reply[2], _callbacks->getSelfId().pub_key, PUB_KEY_SIZE);
+48 -2
View File
@@ -6,6 +6,8 @@
#include <helpers/ClientACL.h>
#include <helpers/RegionMap.h>
#include <helpers/ConfigSerializer.h>
#include <helpers/CommonRadioPrefs.h>
#include <helpers/DynamicConfigSerializer.h>
#if defined(WITH_RS232_BRIDGE) || defined(WITH_ESPNOW_BRIDGE)
#define WITH_BRIDGE
@@ -72,7 +74,7 @@ public:
uint8_t extra_sf[4];
private:
class RadioPrefs : public ConfigSerializer {
class RadioPrefs : public CommonRadioPrefs {
NodePrefs* _parent;
protected:
void structure() override {
@@ -96,6 +98,41 @@ private:
}
public:
RadioPrefs(NodePrefs* parent) : _parent(parent) { }
// CommonRadioPrefs interface
float getFreq() const override { return _parent->freq; }
void setFreq(float f) override { _parent->freq = f; markDirty(); }
float getBandwidth() const override { return _parent->bw; }
void setBandwidth(float bw) override { _parent->bw = bw; markDirty(); }
uint8_t getSpreadFactor() const override { return _parent->sf; }
void setSpreadFactor(uint8_t sf) override { _parent->sf = sf; markDirty(); }
uint8_t getCodingRate() const override { return _parent->cr; }
void setCodingRate(uint8_t cr) override { _parent->cr = cr; markDirty(); }
float getAirtimeFactor() const override { return _parent->airtime_factor; }
void setAirtimeFactor(float af) override { _parent->airtime_factor = af; markDirty(); }
bool isCadEnabled() const override { return _parent->cad_enabled; }
void setCadEnabled(bool en) override { _parent->cad_enabled = en; markDirty(); }
uint8_t getIntThresh() const override { return _parent->interference_threshold; }
void setIntThresh(uint8_t t) override { _parent->interference_threshold = t; markDirty(); }
uint8_t getRxGain() const override { return _parent->rx_boosted_gain; }
void setRxGain(uint8_t g) override { _parent->rx_boosted_gain = g; markDirty(); }
uint8_t getTxPower() const override { return _parent->tx_power_dbm; }
void setTxPower(uint8_t dbm) override { _parent->tx_power_dbm = dbm; markDirty(); }
float getRxDelay() const override { return _parent->rx_delay_base; }
void setRxDelay(float d) override { _parent->rx_delay_base = d; markDirty(); }
uint16_t getAgcResetInt() const override { return _parent->agc_reset_interval * 4; }
void setAgcResetInt(uint16_t secs) override { _parent->agc_reset_interval = secs / 4; markDirty(); }
uint8_t getHashMode() const override { return _parent->path_hash_mode; }
void setHashMode(uint8_t m) override { _parent->path_hash_mode = m; markDirty(); }
uint8_t getMultiAcks() const override { return _parent->multi_acks; }
void setMultiAcks(uint8_t m) override { _parent->multi_acks = m; markDirty(); }
float getFloodTxDelay() const override { return _parent->tx_delay_factor; }
void setFloodTxDelay(float d) override { _parent->tx_delay_factor = d; markDirty(); }
float getDirectTxDelay() const override { return _parent->direct_tx_delay_factor; }
void setDirectTxDelay(float d) override { _parent->direct_tx_delay_factor = d; markDirty(); }
uint8_t getFEMRxGain() const override { return _parent->radio_fem_rxgain; }
void setFEMRxGain(uint8_t g) override { _parent->radio_fem_rxgain = g; markDirty(); }
uint8_t getFEMTxGain() const override { return _parent->radio_fem_txgain; }
void setFEMTxGain(uint8_t g) override { _parent->radio_fem_txgain = g; markDirty(); }
};
RadioPrefs radio;
@@ -166,6 +203,8 @@ private:
};
RoomPrefs room;
DynamicConfigSerializer custom;
protected:
void structure() override {
def("name", node_name, sizeof(node_name));
@@ -182,16 +221,23 @@ protected:
def("repeat", repeat);
def("room", room);
def("power", power);
def("custom", custom);
}
public:
NodePrefs() : ConfigSerializer(), bridge(this), gps(this), radio(this), power(this), repeat(this), room(this) {
NodePrefs() : ConfigSerializer(), bridge(this), gps(this), radio(this), power(this), repeat(this), room(this), custom(&radio) {
node_name[0] = 0;
password[0] = 0;
guest_password[0] = 0;
bridge_secret[0] = 0;
owner_info[0] = 0;
}
CommonRadioPrefs* getRadioPrefs() { return &radio; }
KeyValueStore* getCustom() { return &custom; }
bool isDirty() const override { return ConfigSerializer::isDirty() || radio.isDirty() || custom.isDirty(); }
void clearDirty() override { ConfigSerializer::clearDirty(); radio.clearDirty(); custom.clearDirty(); }
};
class CommonCLICallbacks {
+232
View File
@@ -0,0 +1,232 @@
#include "CommonRadioPrefs.h"
#include "TxtDataHelpers.h"
#include "Utils.h"
#include "target.h"
bool CommonRadioPrefs::getByKey(const char* key, char* value, size_t max_len) {
if (strcmp(key, "fem_rxgain") == 0) {
snprintf(value, max_len, "%d", (uint32_t)getFEMRxGain());
return true;
}
if (strcmp(key, "fem_txgain") == 0) {
snprintf(value, max_len, "%d", (uint32_t)getFEMTxGain());
return true;
}
return false;
}
bool CommonRadioPrefs::setByKey(const char* key, const char* value) {
if (strcmp(key, "fem_rxgain") == 0) {
setFEMRxGain(atoi(value));
markDirty();
return true;
}
if (strcmp(key, "fem_txgain") == 0) {
setFEMTxGain(atoi(value));
markDirty();
return true;
}
return false;
}
bool CommonRadioPrefs::handleCommand(const char* command, uint32_t sender_timestamp, char* reply) {
if (strcmp(command, "get radio") == 0) {
char freq[16], bw[16];
strcpy(freq, StrHelper::ftoa(getFreq()));
strcpy(bw, StrHelper::ftoa3(getBandwidth()));
sprintf(reply, "> %s,%s,%d,%d", freq, bw, (uint32_t)getSpreadFactor(), (uint32_t)getCodingRate());
return true;
}
if (memcmp(command, "set radio ", 10) == 0) {
char tmp[132];
strcpy(tmp, &command[10]);
const char *parts[4];
int num = mesh::Utils::parseTextParts(tmp, parts, 4);
float freq = num > 0 ? strtof(parts[0], nullptr) : 0.0f;
float bw = num > 1 ? strtof(parts[1], nullptr) : 0.0f;
uint8_t sf = num > 2 ? atoi(parts[2]) : 0;
uint8_t cr = num > 3 ? atoi(parts[3]) : 0;
if (freq >= 150.0f && freq <= 2500.0f && sf >= 5 && sf <= 12 && cr >= 5 && cr <= 8 && bw >= 7.0f && bw <= 500.0f) {
setSpreadFactor(sf);
setCodingRate(cr);
setFreq(freq);
setBandwidth(bw);
strcpy(reply, "OK - reboot to apply");
} else {
strcpy(reply, "Error, invalid radio params");
}
return true;
}
if (strcmp(command, "get freq") == 0) {
sprintf(reply, "> %s", StrHelper::ftoa(getFreq()));
return true;
}
if (strcmp(command, "get af") == 0) {
sprintf(reply, "> %s", StrHelper::ftoa(getAirtimeFactor()));
return true;
}
if (memcmp(command, "set af ", 7) == 0) {
char* end;
float af = strtof(&command[7], &end);
if (end == &command[7] || af < 0 || af > 9) {
strcpy(reply, "ERROR: af must be 0-9");
} else {
setAirtimeFactor(af);
strcpy(reply, "OK");
}
return true;
}
if (strcmp(command, "get dutycycle") == 0) {
float dc = 100.0f / (getAirtimeFactor() + 1.0f);
int dc_int = (int)dc;
int dc_frac = (int)((dc - dc_int) * 10.0f + 0.5f);
sprintf(reply, "> %d.%d%%", dc_int, dc_frac);
return true;
}
if (memcmp(command, "set dutycycle ", 14) == 0) {
float dc = atof(&command[14]);
if (dc < 1 || dc > 100) {
strcpy(reply, "ERROR: dutycycle must be 1-100");
} else {
setAirtimeFactor((100.0f / dc) - 1.0f);
float actual = 100.0f / (getAirtimeFactor() + 1.0f);
int a_int = (int)actual;
int a_frac = (int)((actual - a_int) * 10.0f + 0.5f);
sprintf(reply, "OK - %d.%d%%", a_int, a_frac);
}
return true;
}
if (strcmp(command, "get int.thresh") == 0) {
sprintf(reply, "> %d", (uint32_t) getIntThresh());
return true;
}
if (memcmp(command, "set int.thresh ", 15) == 0) {
setIntThresh(atoi(&command[15]));
strcpy(reply, "OK");
return true;
}
if (strcmp(command, "get cad") == 0) {
sprintf(reply, "> %s", isCadEnabled() ? "on" : "off");
return true;
}
if (memcmp(command, "set cad ", 8) == 0) {
setCadEnabled(memcmp(&command[8], "on", 2) == 0);
strcpy(reply, "OK");
return true;
}
if (strcmp(command, "get radio.rxgain") == 0) {
sprintf(reply, "> %s", getRxGain() != 0 ? "on" : "off");
return true;
}
if (memcmp(command, "set radio.rxgain ", 17) == 0) {
bool enabled = memcmp(&command[17], "on", 2) == 0;
setRxGain(enabled);
if (radio_driver.setRxBoostedGainMode(enabled)) {
strcpy(reply, "OK");
} else {
strcpy(reply, "Error: unsupported");
}
return true;
}
if (memcmp(command, "get tx", 6) == 0 && (command[6] == 0 || command[6] == ' ')) {
sprintf(reply, "> %d", (int32_t) getTxPower());
return true;
}
if (memcmp(command, "set tx ", 7) == 0) {
setTxPower(atoi(&command[7]));
radio_driver.setTxPower(getTxPower());
strcpy(reply, "OK");
return true;
}
if (strcmp(command, "get rxdelay") == 0) {
sprintf(reply, "> %s", StrHelper::ftoa(getRxDelay()));
return true;
}
if (memcmp(command, "set rxdelay ", 12) == 0) {
float db = atof(&command[12]);
if (db >= 0 && db <= 20.0f) {
setRxDelay(db);
strcpy(reply, "OK");
} else {
strcpy(reply, "Error, must be 0-20");
}
return true;
}
if (strcmp(command, "get agc.reset.interval") == 0) {
sprintf(reply, "> %d", (uint32_t) getAgcResetInt());
return true;
}
if (memcmp(command, "set agc.reset.interval ", 23) == 0) {
int secs = atoi(&command[23]);
secs = constrain(secs, 0, 255 * 4);
setAgcResetInt((uint16_t) secs);
sprintf(reply, "OK - interval rounded to %d", (uint32_t) getAgcResetInt());
return true;
}
if (strcmp(command, "get path.hash.mode") == 0) {
sprintf(reply, "> %d", (uint32_t)getHashMode());
return true;
}
if (memcmp(command, "set path.hash.mode ", 19) == 0) {
const char* config = command + 19;
uint8_t mode = atoi(config);
if (mode < 3) {
setHashMode(mode);
strcpy(reply, "OK");
} else {
strcpy(reply, "Error, must be 0,1, or 2");
}
return true;
}
if (strcmp(command, "get multi.acks") == 0) {
sprintf(reply, "> %d", (uint32_t) getMultiAcks());
return true;
}
if (memcmp(command, "set multi.acks ", 15) == 0) {
setMultiAcks(atoi(&command[15]));
strcpy(reply, "OK");
return true;
}
if (strcmp(command, "get txdelay") == 0) {
sprintf(reply, "> %s", StrHelper::ftoa(getFloodTxDelay()));
return true;
}
if (memcmp(command, "set txdelay ", 12) == 0) {
float f = atof(&command[12]);
if (f >= 0 && f <= 2.0f) {
setFloodTxDelay(f);
strcpy(reply, "OK");
} else {
strcpy(reply, "Error, must be 0-2");
}
return true;
}
if (strcmp(command, "get direct.txdelay") == 0) {
sprintf(reply, "> %s", StrHelper::ftoa(getDirectTxDelay()));
return true;
}
if (memcmp(command, "set direct.txdelay ", 19) == 0) {
float f = atof(&command[19]);
if (f >= 0 && f <= 2.0f) {
setDirectTxDelay(f);
strcpy(reply, "OK");
} else {
strcpy(reply, "Error, must be 0-2");
}
return true;
}
return false; // not handled
}
+69
View File
@@ -0,0 +1,69 @@
#pragma once
#include "ConfigSerializer.h"
#include "KeyValueStore.h"
class CommonRadioPrefs : public ConfigSerializer, public KeyValueStore {
bool _is_dirty = false;
protected:
CommonRadioPrefs() { }
public:
void markDirty() { _is_dirty = true; }
void clearDirty() { _is_dirty = false; }
bool isDirty() const { return _is_dirty; }
virtual float getFreq() const = 0;
virtual void setFreq(float f) = 0;
virtual float getBandwidth() const = 0;
virtual void setBandwidth(float bw) = 0;
virtual uint8_t getSpreadFactor() const = 0;
virtual void setSpreadFactor(uint8_t sf) = 0;
virtual uint8_t getCodingRate() const = 0;
virtual void setCodingRate(uint8_t cr) = 0;
virtual float getAirtimeFactor() const = 0;
virtual void setAirtimeFactor(float af) = 0;
virtual bool isCadEnabled() const = 0;
virtual void setCadEnabled(bool en) = 0;
virtual uint8_t getIntThresh() const = 0;
virtual void setIntThresh(uint8_t t) = 0;
virtual uint8_t getRxGain() const = 0;
virtual void setRxGain(uint8_t g) = 0;
virtual uint8_t getTxPower() const = 0;
virtual void setTxPower(uint8_t dbm) = 0;
virtual float getRxDelay() const = 0;
virtual void setRxDelay(float d) = 0;
virtual uint16_t getAgcResetInt() const = 0;
virtual void setAgcResetInt(uint16_t secs) = 0;
virtual uint8_t getHashMode() const = 0;
virtual void setHashMode(uint8_t m) = 0;
virtual uint8_t getMultiAcks() const = 0;
virtual void setMultiAcks(uint8_t m) = 0;
virtual float getFloodTxDelay() const = 0;
virtual void setFloodTxDelay(float d) = 0;
virtual float getDirectTxDelay() const = 0;
virtual void setDirectTxDelay(float d) = 0;
virtual uint8_t getFEMRxGain() const = 0;
virtual void setFEMRxGain(uint8_t g) = 0;
virtual uint8_t getFEMTxGain() const = 0;
virtual void setFEMTxGain(uint8_t g) = 0;
bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply);
bool setByKey(const char* key, const char* value) override; // for dynamic key/value access
bool getByKey(const char* key, char* value, size_t max_len) override;
};
+5 -1
View File
@@ -1,4 +1,5 @@
#include "ConfigSerializer.h"
#include <stdlib.h> // atoi/atol/atof (Arduino.h pulls this in on-device, native builds do not)
bool ConfigSerializer::saveSerial(Stream& s) {
Context context(&s, OP::WRITE);
@@ -23,7 +24,9 @@ static bool is_whitespace(char c) {
return c == ' ' || c == '\t' || c == '\r' || c == '\n';
}
static bool is_key_char(char c) {
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_';
// digits are allowed: keys like gr_1hop contain one, otherwise loadSerial
// fails at that key and prefs stop reloading after a reboot
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_';
}
static bool is_value_char(char c) {
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || c == '-' || c == '.';
@@ -62,6 +65,7 @@ int ConfigSerializer::Context::readNext() {
case EXPECT_COMMA_OR_KEY:
if (c == ',') { rd_mode = EXPECT_KEY; return TOK_WHITESPACE; }
case EXPECT_KEY:
if (rd_len == 0 && c == '}') { rd_mode = EXPECT_COMMA_OR_KEY_OR_CLOSE; return TOK_END_OBJ; } // empty object, eg. 'custom:{}'
if (rd_len > 0 && c == ':') { rd_buf[rd_len] = 0; rd_len = 0; rd_mode = EXPECT_VAL_OR_OBJ; return TOK_KEY; }
if (rd_len == 0 && is_whitespace(c)) return TOK_WHITESPACE;
if (rd_len < CONFIG_MAX_KEYLEN-1 && is_key_char(c)) { rd_buf[rd_len++] = c; return TOK_WHITESPACE; }
+9 -1
View File
@@ -17,7 +17,9 @@
class ConfigSerializer {
bool _first;
int8_t _depth;
bool _dirty = false;
protected:
enum OP { READ, WRITE };
class Context {
@@ -38,13 +40,14 @@ class ConfigSerializer {
const char* getToken() const { return rd_buf; }
bool keyMatch(int8_t depth, const char* key) { return strcmp(key, _keys[depth]) == 0; }
void setKey(uint8_t depth, const char* key) { strcpy(_keys[depth], key); }
const char* getKey(uint8_t depth) { return _keys[depth]; }
};
Context* _context = NULL;
int8_t getDepth() const { return _depth; }
void writeComma();
protected:
ConfigSerializer() { }
void def(const char* key, char* value, size_t max_len); // max_len inclusive of null
@@ -62,7 +65,12 @@ protected:
virtual void structure() = 0;
void markDirty() { _dirty = true; }
public:
bool loadSerial(Stream& s);
bool saveSerial(Stream& s);
virtual bool isDirty() const { return _dirty; }
virtual void clearDirty() { _dirty = false; }
};
+6
View File
@@ -26,6 +26,12 @@ struct ContactInfo {
return shared_secret;
}
bool isFav() const { return flags & 0x01; }
bool isTelemBaseAllowed() const { return flags & 0x02; }
bool isTelemLocAllowed() const { return flags & 0x04; }
bool isTelemEnvAllowed() const { return flags & 0x08; }
bool isRemoteCLIAllowed() const { return flags & 0x10; }
private:
mutable uint8_t shared_secret[PUB_KEY_SIZE];
};
+97
View File
@@ -0,0 +1,97 @@
#include "DynamicConfigSerializer.h"
#include <Utils.h>
#define PROP_SEP_CHAR '|'
#define PROP_SEP_STR "|"
#define KEY_SEP_CHAR ':'
#define KEY_SEP_STR ":"
bool DynamicConfigSerializer::setByKeyPrv(const char* key, const char* value) {
if (_fallback && _fallback->setByKey(key, value)) return true;
// TODO: guard for bad chars (':' or '|')
char tmp[MAX_DYNAMIC_CONFG];
strcpy(tmp, _config); // make a (modifiable) copy
const char* parts[8];
int n = mesh::Utils::parseTextParts(tmp, parts, 8, PROP_SEP_CHAR);
// add/replace in _config[]
char new_config[MAX_DYNAMIC_CONFG];
new_config[0] = 0;
int keylen = strlen(key);
for (int i = 0; i < n; i++) {
const char* item = parts[i];
if (item[keylen] == KEY_SEP_CHAR && memcmp(item, key, keylen) == 0) {
// key exists, so omit old value from this pass (will append new value at end)
} else {
if (new_config[0]) {
strcat(new_config, PROP_SEP_STR);
}
strcat(new_config, item);
}
}
// now append new key/value (if it fits)
if (strlen(new_config) + strlen(key) + strlen(value) + 2 < sizeof(_config)-1) {
if (new_config[0]) {
strcat(new_config, PROP_SEP_STR);
}
strcat(new_config, key);
strcat(new_config, KEY_SEP_STR);
strcat(new_config, value);
strcpy(_config, new_config); // commit new serialized string
return true;
}
return false; // didn't fit in _config[]
}
bool DynamicConfigSerializer::setByKey(const char* key, const char* value) {
if (setByKeyPrv(key, value)) {
markDirty();
return true;
}
return false;
}
bool DynamicConfigSerializer::getByKey(const char* key, char* value, size_t max_len) {
if (_fallback && _fallback->getByKey(key, value, max_len)) return true;
char tmp[MAX_DYNAMIC_CONFG];
strcpy(tmp, _config); // make a (modifiable) copy
const char* parts[8];
int n = mesh::Utils::parseTextParts(tmp, parts, 8, PROP_SEP_CHAR);
int keylen = strlen(key);
for (int i = 0; i < n; i++) {
const char* item = parts[i];
if (item[keylen] == KEY_SEP_CHAR && memcmp(item, key, keylen) == 0) {
strncpy(value, &item[keylen+1], max_len);
value[max_len] = 0;
return true;
}
}
return false;
}
void DynamicConfigSerializer::structure() {
if (_context->op() == OP::WRITE) {
char tmp[MAX_DYNAMIC_CONFG];
strcpy(tmp, _config); // make a (modifiable) copy
const char* parts[8];
int n = mesh::Utils::parseTextParts(tmp, parts, 8, PROP_SEP_CHAR);
for (int i = 0; i < n; i++) {
char* item = (char *) parts[i];
char* eq = strchr(item, KEY_SEP_CHAR);
if (eq) {
*eq = 0; // replace separator with null terminator
def(item, eq + 1, MAX_DYNAMIC_CONFG/2);
}
}
} else {
setByKeyPrv(_context->getKey(getDepth()), _context->getToken());
}
}
+23
View File
@@ -0,0 +1,23 @@
#pragma once
#include "ConfigSerializer.h"
#include "KeyValueStore.h"
#ifndef MAX_DYNAMIC_CONFG
#define MAX_DYNAMIC_CONFG 128
#endif
class DynamicConfigSerializer : public ConfigSerializer, public KeyValueStore {
char _config[MAX_DYNAMIC_CONFG];
KeyValueStore* _fallback;
bool setByKeyPrv(const char* key, const char* value);
protected:
void structure() override;
public:
DynamicConfigSerializer(KeyValueStore* fallback = NULL) : _fallback(fallback) { _config[0] = 0; }
bool setByKey(const char* key, const char* value) override;
bool getByKey(const char* key, char* value, size_t max_len) override;
};
+3
View File
@@ -15,6 +15,7 @@
#include "soc/rtc.h"
#include "esp_system.h"
#include <driver/rtc_io.h>
#include <helpers/KeyValueStore.h>
class ESP32Board : public mesh::MainBoard {
protected:
@@ -51,6 +52,8 @@ public:
#endif
}
void attachDynamicPrefs(KeyValueStore* prefs) { } // no-op
// Temperature from ESP32 MCU
float getMCUTemperature() override {
uint32_t raw = 0;
+11
View File
@@ -0,0 +1,11 @@
#pragma once
#include <stdint.h>
#include <string.h>
class KeyValueStore {
protected:
KeyValueStore() { }
public:
virtual bool setByKey(const char* key, const char* value) { return false; }
virtual bool getByKey(const char* key, char* value, size_t max_len) { return false; }
};
-51
View File
@@ -1,51 +0,0 @@
#pragma once
#include <Arduino.h>
// LoRa radio module pins for Meshadventurer
#define P_LORA_DIO_1 33
#define P_LORA_NSS 18
#define P_LORA_RESET 23
#define P_LORA_BUSY 32
#define P_LORA_SCLK 5
#define P_LORA_MISO 19
#define P_LORA_MOSI 27
#define PIN_VBAT_READ 35
#include "ESP32Board.h"
class MeshadventurerBoard : public ESP32Board {
public:
void begin() {
ESP32Board::begin();
esp_reset_reason_t reason = esp_reset_reason();
if (reason == ESP_RST_DEEPSLEEP) {
long wakeup_source = esp_sleep_get_ext1_wakeup_status();
if (wakeup_source & (1 << P_LORA_DIO_1)) { // received a LoRa packet (while in deep sleep)
startup_reason = BD_STARTUP_RX_PACKET;
}
rtc_gpio_hold_dis((gpio_num_t)P_LORA_NSS);
rtc_gpio_deinit((gpio_num_t)P_LORA_DIO_1);
}
}
uint16_t getBattMilliVolts() override {
analogReadResolution(12);
uint32_t raw = 0;
for (int i = 0; i < 4; i++) {
raw += analogReadMilliVolts(PIN_VBAT_READ);
}
raw = raw / 4;
return (2 * raw);
}
const char* getManufacturerName() const override {
return "Meshadventurer";
}
};
+27 -20
View File
@@ -47,7 +47,8 @@ static void __attribute__((constructor(101))) nrf52_early_reset_capture() {
g_nrf52_shutdown_reason = NRF_POWER->GPREGRET2;
}
void NRF52Board::initPowerMgr() {
void NRF52Board::pwrmgtInit() {
if (pwrmgt_initialised) return;
// Copy early-captured register values
reset_reason = g_nrf52_reset_reason;
shutdown_reason = g_nrf52_shutdown_reason;
@@ -74,6 +75,7 @@ void NRF52Board::initPowerMgr() {
MESH_DEBUG_PRINTLN("PWRMGT: Reset = %s (0x%lX)",
getResetReasonString(reset_reason), (unsigned long)reset_reason);
}
pwrmgt_initialised = true;
}
const char* NRF52Board::getResetReasonString(uint32_t reason) {
@@ -98,6 +100,7 @@ const char* NRF52Board::getResetReasonString(uint32_t reason) {
const char* NRF52Board::getShutdownReasonString(uint8_t reason) {
switch (reason) {
case SHUTDOWN_REASON_NONE: return "None";
case SHUTDOWN_REASON_LOW_VOLTAGE: return "Low Voltage";
case SHUTDOWN_REASON_USER: return "User Request";
case SHUTDOWN_REASON_BOOT_PROTECT: return "Boot Protection";
@@ -106,7 +109,7 @@ const char* NRF52Board::getShutdownReasonString(uint8_t reason) {
}
bool NRF52Board::checkBootVoltage(const PowerMgtConfig* config) {
initPowerMgr();
pwrmgtInit();
// Read boot voltage
boot_voltage_mv = getBattMilliVolts();
@@ -173,24 +176,30 @@ void NRF52Board::enterSystemOff(uint8_t reason) {
NVIC_SystemReset();
}
void NRF52Board::configureVoltageWake(uint8_t ain_channel, uint8_t refsel) {
void NRF52Board::configureVoltageWake(uint8_t ain_channel, uint8_t lpcomp_refsel) {
pwrmgtWakeArmVbus();
if (!isPwrMgtInitialised()) {
MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake not armed. Reason: Power Management not initialised");
return;
}
if (!getWakeLpcompSupported()) {
MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake not armed. Reason: LPCOMP unsupported on variant");
return;
}
// LPCOMP is not managed by SoftDevice - direct register access required
// Halt and disable before reconfiguration
NRF_LPCOMP->TASKS_STOP = 1;
NRF_LPCOMP->ENABLE = LPCOMP_ENABLE_ENABLE_Disabled;
// Select analog input (AIN0-7 maps to PSEL 0-7)
NRF_LPCOMP->PSEL = ((uint32_t)ain_channel << LPCOMP_PSEL_PSEL_Pos) & LPCOMP_PSEL_PSEL_Msk;
NRF_LPCOMP->PSEL = ((uint32_t)PWRMGT_LPCOMP_AIN << LPCOMP_PSEL_PSEL_Pos) & LPCOMP_PSEL_PSEL_Msk;
// Reference: REFSEL (0-6=1/8..7/8, 7=ARef, 8-15=1/16..15/16)
NRF_LPCOMP->REFSEL = ((uint32_t)refsel << LPCOMP_REFSEL_REFSEL_Pos) & LPCOMP_REFSEL_REFSEL_Msk;
NRF_LPCOMP->REFSEL = ((uint32_t)lpcomp_refsel << LPCOMP_REFSEL_REFSEL_Pos) & LPCOMP_REFSEL_REFSEL_Msk;
// Detect UP events (voltage rises above threshold for battery recovery)
NRF_LPCOMP->ANADETECT = LPCOMP_ANADETECT_ANADETECT_Up;
// Enable 50mV hysteresis for noise immunity
NRF_LPCOMP->HYST = LPCOMP_HYST_HYST_Hyst50mV;
// Clear stale events/interrupts before enabling wake
NRF_LPCOMP->EVENTS_READY = 0;
NRF_LPCOMP->EVENTS_DOWN = 0;
@@ -204,23 +213,22 @@ void NRF52Board::configureVoltageWake(uint8_t ain_channel, uint8_t refsel) {
NRF_LPCOMP->ENABLE = LPCOMP_ENABLE_ENABLE_Enabled;
NRF_LPCOMP->TASKS_START = 1;
// Wait for comparator to settle before entering SYSTEMOFF
// Wait for comparator to settle
for (uint8_t i = 0; i < 20 && !NRF_LPCOMP->EVENTS_READY; i++) {
delayMicroseconds(50);
}
if (refsel == 7) {
MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake configured (AIN%d, ref=ARef)", ain_channel);
} else if (refsel <= 6) {
MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake configured (AIN%d, ref=%d/8 VDD)",
ain_channel, refsel + 1);
if (lpcomp_refsel == 7) {
MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake armed (AIN%d, ref=ARef)", PWRMGT_LPCOMP_AIN);
} else if (lpcomp_refsel <= 6) {
MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake armed (AIN%d, ref=%d/8 VDD)", PWRMGT_LPCOMP_AIN, lpcomp_refsel + 1);
} else {
uint8_t ref_num = (uint8_t)((refsel - 8) * 2 + 1);
MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake configured (AIN%d, ref=%d/16 VDD)",
ain_channel, ref_num);
uint8_t ref_num = (uint8_t)((lpcomp_refsel - 8) * 2 + 1);
MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake armed (AIN%d, ref=%d/16 VDD)", PWRMGT_LPCOMP_AIN, ref_num);
}
}
// Configure VBUS (USB power) wake alongside LPCOMP
void NRF52Board::pwrmgtWakeArmVbus() {
uint8_t sd_enabled = 0;
sd_softdevice_is_enabled(&sd_enabled);
if (sd_enabled) {
@@ -229,8 +237,7 @@ void NRF52Board::configureVoltageWake(uint8_t ain_channel, uint8_t refsel) {
NRF_POWER->EVENTS_USBDETECTED = 0;
NRF_POWER->INTENSET = POWER_INTENSET_USBDETECTED_Msk;
}
MESH_DEBUG_PRINTLN("PWRMGT: VBUS wake configured");
MESH_DEBUG_PRINTLN("PWRMGT: VBUS wake armed");
}
#endif
+11 -3
View File
@@ -2,6 +2,7 @@
#include <Arduino.h>
#include <MeshCore.h>
#include <helpers/KeyValueStore.h>
#if defined(NRF52_PLATFORM)
@@ -25,15 +26,15 @@ struct PowerMgtConfig {
#endif
class NRF52Board : public mesh::MainBoard {
#ifdef NRF52_POWER_MANAGEMENT
void initPowerMgr();
#endif
private:
bool pwrmgt_initialised = false;
protected:
uint8_t startup_reason;
char *ota_name;
#ifdef NRF52_POWER_MANAGEMENT
void pwrmgtInit();
uint32_t reset_reason; // RESETREAS register value
uint8_t shutdown_reason; // GPREGRET value (why we entered last SYSTEMOFF)
uint16_t boot_voltage_mv; // Battery voltage at boot (millivolts)
@@ -41,6 +42,7 @@ protected:
bool checkBootVoltage(const PowerMgtConfig* config);
void enterSystemOff(uint8_t reason);
void configureVoltageWake(uint8_t ain_channel, uint8_t refsel);
void pwrmgtWakeArmVbus();
virtual void initiateShutdown(uint8_t reason);
#endif
@@ -57,12 +59,18 @@ public:
virtual void sleep(uint32_t secs) override;
bool isExternalPowered() override;
void attachDynamicPrefs(KeyValueStore* prefs) { } // no-op
#ifdef NRF52_POWER_MANAGEMENT
uint16_t getBootVoltage() override { return boot_voltage_mv; }
virtual uint32_t getResetReason() const override { return reset_reason; }
uint8_t getShutdownReason() const override { return shutdown_reason; }
const char* getResetReasonString(uint32_t reason) override;
const char* getShutdownReasonString(uint8_t reason) override;
bool isPwrMgtInitialised() const override { return pwrmgt_initialised; }
#ifdef PWRMGT_LPCOMP_AIN
bool getWakeLpcompSupported() const override { return true; }
#endif
#endif
};
+1
View File
@@ -23,6 +23,7 @@ public:
virtual const char* getSettingValue(int i) const { return NULL; }
virtual bool setSettingValue(const char* name, const char* value) { return false; }
virtual LocationProvider* getLocationProvider() { return NULL; }
virtual bool isGPSDetected() const { return false; }
// Helper functions to manage setting by keys (useful in many places ...)
const char* getSettingByKey(const char* key) {
+7 -2
View File
@@ -44,7 +44,7 @@ mesh::Packet* PacketQueue::get(uint32_t now) {
}
mesh::Packet* PacketQueue::removeByIdx(int i) {
if (i >= _num) return NULL; // invalid index
if (i < 0 || i >= _num) return NULL; // invalid index
mesh::Packet* item = _table[i];
_num--;
@@ -80,10 +80,14 @@ mesh::Packet* StaticPoolPacketManager::allocNew() {
}
void StaticPoolPacketManager::free(mesh::Packet* packet) {
unused.add(packet, 0, 0);
if (packet == NULL) return;
if (!unused.add(packet, 0, 0)) {
MESH_DEBUG_PRINTLN("free: unused queue full, possible double-free detected");
}
}
void StaticPoolPacketManager::queueOutbound(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for) {
if (packet == NULL) return;
if (!send_queue.add(packet, priority, scheduled_for)) {
MESH_DEBUG_PRINTLN("queueOutbound: send queue full, dropping packet");
free(packet);
@@ -115,6 +119,7 @@ mesh::Packet* StaticPoolPacketManager::removeOutboundByIdx(int i) {
}
void StaticPoolPacketManager::queueInbound(mesh::Packet* packet, uint32_t scheduled_for) {
if (packet == NULL) return;
if (!rx_queue.add(packet, 0, scheduled_for)) {
MESH_DEBUG_PRINTLN("queueInbound: rx queue full, dropping packet");
free(packet);
+4 -1
View File
@@ -14,7 +14,10 @@ public:
bool add(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for);
int count() const { return _num; }
int countBefore(uint32_t now) const;
mesh::Packet* itemAt(int i) const { return _table[i]; }
mesh::Packet* itemAt(int i) const {
if (i < 0 || i >= _num) return NULL;
return _table[i];
}
mesh::Packet* removeByIdx(int i);
};
+7 -2
View File
@@ -27,8 +27,13 @@ bool StrHelper::isBlank(const char* str) {
}
#include <Arduino.h>
#include <stdio.h>
#if __has_include(<api/itoa.h>)
// https://github.com/stm32duino/Arduino_Core_STM32/issues/3084
#include <api/itoa.h>
#endif
union int32_Float_t
union int32_Float_t
{
int32_t Long;
float Float;
@@ -100,7 +105,7 @@ static void _ftoa(float f, char *p, int *status)
*p++ = '-';
if (int_part == 0)
*p++ = '0';
else
else
{
ltoa(int_part, p, 10);
while (*p)
+3 -1
View File
@@ -4,8 +4,10 @@
#include <stdint.h>
#define TXT_TYPE_PLAIN 0 // a plain text message
#define TXT_TYPE_CLI_DATA 1 // a CLI command
#define TXT_TYPE_CLI_DATA 1 // a CLI command -or- reply
#define TXT_TYPE_SIGNED_PLAIN 2 // plain text, signed by sender
#define TXT_TYPE_CLI_COMMAND 3 // a CLI command (explictly)
#define DATA_TYPE_RESERVED 0x0000 // reserved for future use
#define DATA_TYPE_DEV 0xFFFF // developer namespace for experimenting with group/channel datagrams and building apps
+10
View File
@@ -13,6 +13,16 @@ class CustomLR1110 : public LR1110 {
public:
CustomLR1110(Module *mod) : LR1110(mod) { }
int16_t begin(float freq = 434.0, float bw = 125.0, uint8_t sf = 9, uint8_t cr = 7,
uint8_t syncWord = RADIOLIB_LR11X0_LORA_SYNC_WORD_PRIVATE, int8_t power = 10,
uint16_t preambleLength = 8, float tcxoVoltage = 1.6) {
int16_t state = LR1110::begin(freq, bw, sf, cr, syncWord, power, preambleLength,
tcxoVoltage);
// RadioLib begin() defaults to LDO; use the LR1110 DC/DC regulator.
if (state == RADIOLIB_ERR_NONE) state = setRegulatorDCDC();
return state;
}
size_t getPacketLength(bool update) override {
size_t len = LR1110::getPacketLength(update);
if (len == 0 && getIrqStatus() & RADIOLIB_LR11X0_IRQ_HEADER_ERR) {
+105
View File
@@ -0,0 +1,105 @@
#pragma once
#include <RadioLib.h>
#include "MeshCore.h"
class CustomLR1121 : public LR1121 {
uint32_t _preambleMillis = 66;
uint32_t _maxPayloadMillis = 3934;
uint32_t _activityAt = 0;
bool _headerSeen = false;
bool _rx_boosted = false;
public:
CustomLR1121(Module *mod) : LR1121(mod) { }
int16_t begin(float freq = 434.0, float bw = 125.0, uint8_t sf = 9, uint8_t cr = 7,
uint8_t syncWord = RADIOLIB_LR11X0_LORA_SYNC_WORD_PRIVATE, int8_t power = 10,
uint16_t preambleLength = 8, float tcxoVoltage = 1.6) {
int16_t state = LR1121::begin(freq, bw, sf, cr, syncWord, power, preambleLength,
tcxoVoltage);
// RadioLib begin() defaults to LDO; use the LR1121 DC/DC regulator.
if (state == RADIOLIB_ERR_NONE) state = setRegulatorDCDC();
return state;
}
size_t getPacketLength(bool update) override {
size_t len = LR1121::getPacketLength(update);
if (len == 0 && getIrqStatus() & RADIOLIB_LR11X0_IRQ_HEADER_ERR) {
// we've just received a corrupted packet
// this may have triggered a bug causing subsequent packets to be shifted
// call standby() to return radio to known-good state
// recvRaw will call startReceive() to restart rx
MESH_DEBUG_PRINTLN("LR1121: got header err, calling standby()");
standby();
}
return len;
}
float getFreqMHz() const { return freqMHz; }
int16_t setRxBoostedGainMode(bool en) {
_rx_boosted = en;
return LR1121::setRxBoostedGainMode(en);
}
bool getRxBoostedGainMode() const { return _rx_boosted; }
int16_t startReceive() override {
// include the PREAMBLE_DETECTED irq bit in reported flags.
return LR1121::startReceive(RADIOLIB_LR11X0_RX_TIMEOUT_INF, RADIOLIB_IRQ_RX_DEFAULT_FLAGS | (1UL << RADIOLIB_IRQ_PREAMBLE_DETECTED), RADIOLIB_IRQ_RX_DEFAULT_MASK, 0);
}
bool isReceiving() {
uint32_t irq = getIrqStatus();
bool preamble = irq & RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED; // bit 4
bool header = irq & RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID; // bit 5
bool hdrErr = irq & RADIOLIB_LR11X0_IRQ_HEADER_ERR; // bit 6
uint32_t now = millis();
if (hdrErr) {
clearIrqState(RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED | RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID | RADIOLIB_LR11X0_IRQ_HEADER_ERR);
_activityAt = 0;
_headerSeen = false;
return false;
}
if (!header && _headerSeen) {
// something cleared the header flag, reset our state.
_activityAt = 0; _headerSeen = false;
return false;
}
if (header) {
if (!_headerSeen) { _headerSeen = true; _activityAt = now; };
if (now - _activityAt > _maxPayloadMillis) {
MESH_DEBUG_PRINTLN("Clearing header IRQ after %ums", _maxPayloadMillis);
clearIrqState(RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED | RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID | RADIOLIB_LR11X0_IRQ_HEADER_ERR);
_activityAt = 0; _headerSeen = false;
return false;
}
return true;
}
if (preamble) {
if (_activityAt == 0) _activityAt = now;
if (now - _activityAt > _preambleMillis) {
clearIrqState(RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED);
_activityAt = 0;
MESH_DEBUG_PRINTLN("Clearing preamble IRQ after %ums", _preambleMillis);
return false;
}
return true;
}
_activityAt = 0; _headerSeen = false;
return false;
}
void setPreambleMillis(uint32_t preambleMillis) {
_preambleMillis = preambleMillis;
MESH_DEBUG_PRINTLN("Set _preambleMillis=%u", _preambleMillis);
}
void setMaxPayloadMillis(uint32_t payloadMillis) {
_maxPayloadMillis = payloadMillis;
MESH_DEBUG_PRINTLN("Set _maxPayloadMillis=%u", _maxPayloadMillis);
}
uint8_t getSpreadingFactor() const { return spreadingFactor; }
};
@@ -0,0 +1,53 @@
#pragma once
#include "CustomLR1121.h"
#include "RadioLibWrappers.h"
#include "LR11x0Reset.h"
class CustomLR1121Wrapper : public RadioLibWrapper {
public:
CustomLR1121Wrapper(CustomLR1121& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
void setParams(float freq, float bw, uint8_t sf, uint8_t cr) override {
((CustomLR1121 *)_radio)->setFrequency(freq);
((CustomLR1121 *)_radio)->setSpreadingFactor(sf);
((CustomLR1121 *)_radio)->setBandwidth(bw);
((CustomLR1121 *)_radio)->setCodingRate(cr);
updatePreamble(sf);
PacketMillis pm = calcMaxPacketMillis(sf, bw, cr, preambleLengthForSF(sf));
((CustomLR1121 *)_radio)->setPreambleMillis(pm.preambleMillis);
((CustomLR1121 *)_radio)->setMaxPayloadMillis(pm.payloadMillis);
}
void doResetAGC() override { lr11x0ResetAGC((LR11x0 *)_radio, ((CustomLR1121 *)_radio)->getFreqMHz(), getRxBoostedGainMode()); }
bool isReceivingPacket() override {
return ((CustomLR1121 *)_radio)->isReceiving();
}
float getCurrentRSSI() override {
float rssi = -110;
((CustomLR1121 *)_radio)->getRssiInst(&rssi);
return rssi;
}
void onSendFinished() override {
RadioLibWrapper::onSendFinished();
_radio->setPreambleLength(preambleLengthForSF(getSpreadingFactor())); // overcomes weird issues with small and big pkts
}
uint32_t getEstAirtimeFor(int len_bytes) override {
auto airtime = RadioLibWrapper::getEstAirtimeFor(len_bytes);
return airtime < 200 ? 200 : airtime; // at least 200 millis
}
float getLastRSSI() const override { return ((CustomLR1121 *)_radio)->getRSSI(); }
float getLastSNR() const override { return ((CustomLR1121 *)_radio)->getSNR(); }
uint8_t getSpreadingFactor() const override { return ((CustomLR1121 *)_radio)->getSpreadingFactor(); }
bool setRxBoostedGainMode(bool en) override {
return ((CustomLR1121 *)_radio)->setRxBoostedGainMode(en) == RADIOLIB_ERR_NONE;
}
bool getRxBoostedGainMode() const override {
return ((CustomLR1121 *)_radio)->getRxBoostedGainMode();
}
};
+1 -1
View File
@@ -72,7 +72,7 @@ class CustomLR2021 : public LR2021 {
int16_t startReceive() override {
// include the PREAMBLE_DETECTED irq bit in reported flags
return LR2021::startReceive(RADIOLIB_LR2021_RX_TIMEOUT_INF, RADIOLIB_IRQ_RX_DEFAULT_FLAGS | (1UL << RADIOLIB_LR2021_IRQ_PREAMBLE_DETECTED), RADIOLIB_IRQ_RX_DEFAULT_MASK, 0);
return LR2021::startReceive(RADIOLIB_LR2021_RX_TIMEOUT_INF, RADIOLIB_IRQ_RX_DEFAULT_FLAGS | (1UL << RADIOLIB_IRQ_PREAMBLE_DETECTED), RADIOLIB_IRQ_RX_DEFAULT_MASK, 0);
}
bool isReceiving() {
+4 -1
View File
@@ -48,6 +48,9 @@ uint32_t RadioLibWrapper::getRngSeed() {
}
void RadioLibWrapper::setTxPower(int8_t dbm) {
#if defined(USE_LR2021)
idle();
#endif
_radio->setOutputPower(dbm);
}
@@ -254,4 +257,4 @@ PacketMillis RadioLibWrapper::calcMaxPacketMillis(uint8_t sf, float bw, uint8_t
if (cr >= 5 && cr < 8) { payload_us = (payload_us * 8) / cr; }
return PacketMillis {(preamble_us + 999) / 1000, (payload_us + 999) / 1000};
}
}
+3 -1
View File
@@ -92,8 +92,10 @@ public:
void random(uint8_t* dest, size_t sz) override {
#ifdef USE_CC310_HW_CRYPTO
// CC310 TRNG is higher quality and environment-independent vs radio RSSI noise.
nRFCrypto.Random.generate(dest, (uint16_t)sz);
for (int i = 0; i < sz; i++) {
dest[i] ^= _radio->randomByte() ^ (::random(0, 256) & 0xFF); // combine with Radio's entropy
}
#else
for (int i = 0; i < sz; i++) {
dest[i] = _radio->randomByte() ^ (::random(0, 256) & 0xFF);
+201
View File
@@ -0,0 +1,201 @@
// only built when the env enables the core BLE stack (build_as_lib.py globs this dir)
#ifdef PIO_FRAMEWORK_ARDUINO_ENABLE_BLUETOOTH
#include "SerialBLEInterface.h"
#include <BluetoothLock.h>
#include <stdio.h>
#include <string.h>
// Nordic UART 6E4000xx-B5A3-F393-E0A9-E50E24DCCA9E, as raw bytes: the core lib's string
// parser uses sscanf("%llx"), which newlib-nano on the RP2040 doesn't support (yields all zeros)
#define NUS_UUID(n) { 0x6E, 0x40, 0x00, n, 0xB5, 0xA3, 0xF3, 0x93, 0xE0, 0xA9, 0xE5, 0x0E, 0x24, 0xDC, 0xCA, 0x9E }
static const uint8_t SERVICE_UUID[16] = NUS_UUID(0x01);
static const uint8_t CHARACTERISTIC_UUID_RX[16] = NUS_UUID(0x02);
static const uint8_t CHARACTERISTIC_UUID_TX[16] = NUS_UUID(0x03);
// The BLE core lib's own setValue()/notify path reallocs the value buffer on every call,
// so a second frame queued before the radio drained the first would corrupt it. We keep our
// own frame queue and drive att_server_notify() from the can-send-now callback instead.
SerialBLEInterface::SerialBLEInterface()
: BLEService(BLEUUID(SERVICE_UUID)),
_rx(BLEUUID(CHARACTERISTIC_UUID_RX), BLEWrite, nullptr, ATT_SECURITY_AUTHENTICATED, ATT_SECURITY_AUTHENTICATED),
_tx(BLEUUID(CHARACTERISTIC_UUID_TX), BLERead | BLENotify, nullptr, ATT_SECURITY_AUTHENTICATED, ATT_SECURITY_AUTHENTICATED)
{
_isEnabled = false;
_tx_pending = false;
send_queue_len = 0;
recv_queue_len = 0;
memset(&_can_send, 0, sizeof(_can_send));
_rx.setCallbacks(this);
addCharacteristic(&_rx);
addCharacteristic(&_tx);
}
void SerialBLEInterface::begin(const char* prefix, char* name, uint32_t pin_code) {
// JustWorks here only makes the server request pairing on connect; caps are overridden below
BLE.setSecurity(BLESecurityJustWorks);
// adv data carries the 128-bit service UUID, leaving room for only 8 name chars;
// the full name goes in the scan response
BLE.begin(prefix);
if (strcmp(name, "@@MAC") == 0) {
bd_addr_t a;
gap_local_bd_addr(a);
sprintf(name, "%02X%02X%02X%02X%02X%02X", a[0], a[1], a[2], a[3], a[4], a[5]); // modify (IN-OUT param)
}
char dev_name[32+16];
snprintf(dev_name, sizeof(dev_name), "%s%s", prefix, name);
BLE.server()->setName(dev_name); // GAP device name characteristic
BLE.server()->addService(this);
BLE.server()->setCallbacks(this);
// static passkey pairing with MITM protection, matching the esp32/nrf52 interfaces
sm_set_io_capabilities(IO_CAPABILITY_DISPLAY_ONLY);
sm_set_authentication_requirements(SM_AUTHREQ_MITM_PROTECTION | SM_AUTHREQ_BONDING);
sm_use_fixed_passkey_in_display_role(pin_code);
size_t n = strlen(dev_name);
if (n > sizeof(_scan_rsp) - 2) n = sizeof(_scan_rsp) - 2;
_scan_rsp[0] = n + 1;
_scan_rsp[1] = BLUETOOTH_DATA_TYPE_COMPLETE_LOCAL_NAME;
memcpy(&_scan_rsp[2], dev_name, n);
gap_scan_response_set_data(n + 2, _scan_rsp);
BLE_DEBUG_PRINTLN("begin: name=%s", dev_name);
}
void SerialBLEInterface::clearBuffers() {
send_queue_len = 0;
recv_queue_len = 0;
_tx_pending = false; // a stale registration just fires into an empty queue; BTstack ignores double-adds
}
void SerialBLEInterface::onConnect(BLEServer* s) {
BLE_DEBUG_PRINTLN("connected handle=0x%04X", _tx.conHandle());
clearBuffers();
}
void SerialBLEInterface::onDisconnect(BLEServer* s) {
BLE_DEBUG_PRINTLN("disconnected");
clearBuffers(); // BTstack re-enables advertising on its own
}
void SerialBLEInterface::onWrite(BLECharacteristic* c) {
if (c != &_rx) return;
size_t len = _rx.valueLen();
if (len == 0 || len > MAX_FRAME_SIZE) {
BLE_DEBUG_PRINTLN("onWrite: bad frame len=%u", (unsigned)len);
return;
}
if (recv_queue_len >= FRAME_QUEUE_SIZE) {
BLE_DEBUG_PRINTLN("onWrite: recv queue full, dropping frame");
return;
}
recv_queue[recv_queue_len].len = len;
memcpy(recv_queue[recv_queue_len].buf, _rx.valueData(), len);
recv_queue_len++;
}
// caller holds the BT lock (or is in the BT context)
void SerialBLEInterface::kickSend() {
if (_tx_pending) return;
_tx_pending = true;
_can_send.callback = onCanSend;
_can_send.context = this;
if (att_server_register_can_send_now_callback(&_can_send, _tx.conHandle()) != 0) {
_tx_pending = false;
}
}
// BT context: one notification per can-send-now, re-arm while frames remain
void SerialBLEInterface::sendNext() {
_tx_pending = false;
if (send_queue_len == 0) return;
if (!isConnected()) {
BLE_DEBUG_PRINTLN("sendNext: not connected, clearing send queue");
send_queue_len = 0;
return;
}
uint16_t h = _tx.conHandle();
Frame& f = send_queue[0];
uint16_t mtu = att_server_get_mtu(h);
if (f.len + 3 > mtu) {
// att would silently truncate; drop instead (client must negotiate MTU >= MAX_FRAME_SIZE+3)
BLE_DEBUG_PRINTLN("sendNext: frame len=%u exceeds mtu=%u, dropping", f.len, mtu);
} else {
uint8_t err = att_server_notify(h, _tx.valueHandle(), f.buf, f.len);
if (err == BTSTACK_ACL_BUFFERS_FULL) {
kickSend();
return;
}
if (err) {
BLE_DEBUG_PRINTLN("sendNext: notify failed err=%u, dropping", err);
} else {
BLE_DEBUG_PRINTLN("writeBytes: sz=%u, hdr=%u", f.len, f.buf[0]);
}
}
send_queue_len--;
memmove(&send_queue[0], &send_queue[1], send_queue_len * sizeof(Frame));
if (send_queue_len > 0) kickSend();
}
void SerialBLEInterface::enable() {
if (_isEnabled) return;
_isEnabled = true;
clearBuffers();
BLE.startAdvertising(true);
}
void SerialBLEInterface::disconnect() {
uint16_t h = _tx.conHandle();
if (h) gap_disconnect(h);
}
void SerialBLEInterface::disable() {
_isEnabled = false;
BLE_DEBUG_PRINTLN("disable");
disconnect();
BLE.stopAdvertising();
}
bool SerialBLEInterface::isConnected() const {
// notifications can only be enabled once the link is authenticated (CCCD inherits the perms)
return _isEnabled && _tx.conHandle() != 0 && _tx.notifyEnabled();
}
bool SerialBLEInterface::isWriteBusy() const {
return send_queue_len >= (FRAME_QUEUE_SIZE * 2 / 3);
}
size_t SerialBLEInterface::writeFrame(const uint8_t src[], size_t len) {
if (len == 0 || len > MAX_FRAME_SIZE) {
BLE_DEBUG_PRINTLN("writeFrame(), frame too big, len=%u", (unsigned)len);
return 0;
}
if (!isConnected()) return 0;
BluetoothLock lock;
if (send_queue_len >= FRAME_QUEUE_SIZE) {
BLE_DEBUG_PRINTLN("writeFrame(), send_queue is full!");
return 0;
}
send_queue[send_queue_len].len = len;
memcpy(send_queue[send_queue_len].buf, src, len);
send_queue_len++;
kickSend();
return len;
}
size_t SerialBLEInterface::checkRecvFrame(uint8_t dest[]) {
BluetoothLock lock;
if (recv_queue_len == 0) return 0;
size_t len = recv_queue[0].len;
memcpy(dest, recv_queue[0].buf, len);
recv_queue_len--;
memmove(&recv_queue[0], &recv_queue[1], recv_queue_len * sizeof(Frame));
BLE_DEBUG_PRINTLN("readBytes: sz=%u, hdr=%u", (unsigned)len, dest[0]);
return len;
}
#endif
+75
View File
@@ -0,0 +1,75 @@
#pragma once
#include "../BaseSerialInterface.h"
#include <BLE.h>
#include <btstack.h>
// Nordic UART service over the arduino-pico BLE library (BTstack on the CYW43).
// Build with -D PIO_FRAMEWORK_ARDUINO_ENABLE_BLUETOOTH so the core links liblwip-bt.
class SerialBLEInterface : public BaseSerialInterface, BLEService, BLEServerCallbacks, BLECharacteristicCallbacks {
// subclass only to reach the protected connection/notify state
struct Characteristic : public BLECharacteristic {
using BLECharacteristic::BLECharacteristic;
uint16_t valueHandle() const { return _valueHandle; }
uint16_t conHandle() const { return con_handle; }
bool notifyEnabled() const { return _notificationEnabled; }
};
struct Frame {
uint8_t len;
uint8_t buf[MAX_FRAME_SIZE];
};
#define FRAME_QUEUE_SIZE 8
Characteristic _rx;
Characteristic _tx;
bool _isEnabled;
bool _tx_pending; // a can-send-now callback is registered
btstack_context_callback_registration_t _can_send;
uint8_t _scan_rsp[31]; // complete local name; BTstack keeps the pointer
uint8_t send_queue_len;
Frame send_queue[FRAME_QUEUE_SIZE];
uint8_t recv_queue_len;
Frame recv_queue[FRAME_QUEUE_SIZE];
void clearBuffers();
void kickSend();
void sendNext();
static void onCanSend(void* ctx) { ((SerialBLEInterface*)ctx)->sendNext(); }
// BLE library callbacks (run in the BT context)
void onWrite(BLECharacteristic* c) override;
void onConnect(BLEServer* s) override;
void onDisconnect(BLEServer* s) override;
public:
SerialBLEInterface();
/**
* init the BLE interface.
* @param prefix a prefix for the device name
* @param name IN/OUT - a name for the device (combined with prefix). If "@@MAC", is modified and returned
* @param pin_code the BLE security pin
*/
void begin(const char* prefix, char* name, uint32_t pin_code);
void disconnect();
void enable() override;
void disable() override;
bool isEnabled() const override { return _isEnabled; }
bool isConnected() const override;
bool isWriteBusy() const override;
size_t writeFrame(const uint8_t src[], size_t len) override;
size_t checkRecvFrame(uint8_t dest[]) override;
};
#if BLE_DEBUG_LOGGING && ARDUINO
#include <Arduino.h>
#define BLE_DEBUG_PRINT(F, ...) Serial.printf("BLE: " F, ##__VA_ARGS__)
#define BLE_DEBUG_PRINTLN(F, ...) Serial.printf("BLE: " F "\n", ##__VA_ARGS__)
#else
#define BLE_DEBUG_PRINT(...) {}
#define BLE_DEBUG_PRINTLN(...) {}
#endif
@@ -539,6 +539,8 @@ static void query_bme680_bsec(uint8_t ch, uint8_t, CayenneLPP& lpp) {
// are compiled in. The sentinel at the end keeps the array
// non-empty regardless of which sensors are enabled.
//
// Bosch BMP/BME SDO selects 0x76 or 0x77; probe both.
//
// Ordering here determines channel assignment at runtime:
// the first detected+initialized sensor gets channel 2, the
// next gets channel 3, and so on.
@@ -551,21 +553,27 @@ struct SensorDef {
void (*query)(uint8_t channel, uint8_t sub_channel, CayenneLPP& telemetry);
};
#define TELEM_BOSCH_ALT_ADDR(addr) ((uint8_t)((addr) == 0x76 ? 0x77 : 0x76))
static const SensorDef SENSOR_TABLE[] = {
#if ENV_INCLUDE_AHTX0
{ TELEM_AHTX_ADDRESS, "AHT10/AHT20", init_ahtx0, query_ahtx0 },
#endif
#ifdef ENV_INCLUDE_BME680
{ TELEM_BME680_ADDRESS, "BME680", init_bme680, query_bme680 },
{ TELEM_BME680_ADDRESS, "BME680", init_bme680, query_bme680 },
{ TELEM_BOSCH_ALT_ADDR(TELEM_BME680_ADDRESS), "BME680", init_bme680, query_bme680 },
#endif
#if ENV_INCLUDE_BME680_BSEC
{ TELEM_BME680_ADDRESS, "BME680+BSEC", init_bme680_bsec, query_bme680_bsec },
{ TELEM_BME680_ADDRESS, "BME680+BSEC", init_bme680_bsec, query_bme680_bsec },
{ TELEM_BOSCH_ALT_ADDR(TELEM_BME680_ADDRESS), "BME680+BSEC", init_bme680_bsec, query_bme680_bsec },
#endif
#if ENV_INCLUDE_BME280
{ TELEM_BME280_ADDRESS, "BME280", init_bme280, query_bme280 },
{ TELEM_BME280_ADDRESS, "BME280", init_bme280, query_bme280 },
{ TELEM_BOSCH_ALT_ADDR(TELEM_BME280_ADDRESS), "BME280", init_bme280, query_bme280 },
#endif
#if ENV_INCLUDE_BMP280
{ TELEM_BMP280_ADDRESS, "BMP280", init_bmp280, query_bmp280 },
{ TELEM_BMP280_ADDRESS, "BMP280", init_bmp280, query_bmp280 },
{ TELEM_BOSCH_ALT_ADDR(TELEM_BMP280_ADDRESS), "BMP280", init_bmp280, query_bmp280 },
#endif
#if ENV_INCLUDE_SHTC3
{ 0x70, "SHTC3", init_shtc3, query_shtc3 },
@@ -603,6 +611,8 @@ static const SensorDef SENSOR_TABLE[] = {
{ 0, nullptr, nullptr, nullptr } // sentinel — keeps the array non-empty
};
#undef TELEM_BOSCH_ALT_ADDR
static const size_t SENSOR_TABLE_SIZE = (sizeof(SENSOR_TABLE) / sizeof(SENSOR_TABLE[0])) - 1;
// ============================================================
@@ -640,6 +650,12 @@ bool EnvironmentSensorManager::begin() {
_active_sensor_count = 0;
for (size_t i = 0; i < SENSOR_TABLE_SIZE && _active_sensor_count < MAX_ACTIVE_SENSORS; i++) {
const SensorDef& def = SENSOR_TABLE[i];
// One static driver instance per type: an alternate address is a fallback, not a second device.
bool already_active = false;
for (int j = 0; j < _active_sensor_count; j++) {
if (_active_sensors[j].query == def.query) { already_active = true; break; }
}
if (already_active) continue;
if (!detected[def.address]) {
MESH_DEBUG_PRINTLN("%s not detected at I2C address %02X", def.name, def.address);
continue;
@@ -650,6 +666,7 @@ bool EnvironmentSensorManager::begin() {
continue;
}
MESH_DEBUG_PRINTLN("Found %s at address: %02X", def.name, def.address);
detected[def.address] = false; // consumed; later entries must not re-claim this device
for (uint8_t sub = 0; sub < n && _active_sensor_count < MAX_ACTIVE_SENSORS; sub++) {
_active_sensors[_active_sensor_count++] = { def.query, sub };
}
@@ -814,11 +831,15 @@ bool EnvironmentSensorManager::gpsIsAwake(uint8_t ioPin){
}
#endif
#ifndef RAK_3401
//set initial waking state
pinMode(ioPin,OUTPUT);
digitalWrite(ioPin,LOW);
delay(500);
digitalWrite(ioPin,HIGH);
#endif
// give gps time to power up
delay(500);
//Try to init RAK12500 on I2C
@@ -854,7 +875,9 @@ bool EnvironmentSensorManager::gpsIsAwake(uint8_t ioPin){
return true;
}
#ifndef RAK_3401
pinMode(ioPin, INPUT);
#endif
MESH_DEBUG_PRINTLN("GPS did not init with this IO pin... try the next");
return false;
}
@@ -863,8 +886,10 @@ bool EnvironmentSensorManager::gpsIsAwake(uint8_t ioPin){
void EnvironmentSensorManager::start_gps() {
gps_active = true;
#ifdef RAK_WISBLOCK_GPS
#ifndef RAK_3401
pinMode(gpsResetPin, OUTPUT);
digitalWrite(gpsResetPin, HIGH);
#endif
return;
#endif
@@ -879,8 +904,10 @@ void EnvironmentSensorManager::start_gps() {
void EnvironmentSensorManager::stop_gps() {
gps_active = false;
#ifdef RAK_WISBLOCK_GPS
#ifndef RAK_3401 // rak3401 shouldn't turn off WB_IO2 as it powers the PA
pinMode(gpsResetPin, OUTPUT);
digitalWrite(gpsResetPin, LOW);
#endif
return;
#endif
@@ -38,6 +38,7 @@ public:
#if ENV_INCLUDE_GPS
EnvironmentSensorManager(LocationProvider &location): _location(&location){};
LocationProvider* getLocationProvider() { return _location; }
bool isGPSDetected() const override { return gps_detected; }
#else
EnvironmentSensorManager(){};
#endif
+113 -46
View File
@@ -63,12 +63,66 @@
#define LPP_ERROR_OVERFLOW 1
#define LPP_ERROR_UNKOWN_TYPE 2
class LPPReader {
const uint8_t* _buf;
uint8_t _len;
uint8_t _pos;
class LPPData {
public:
static uint8_t getDataSize(uint8_t type) {
switch (type) {
case LPP_GPS:
return 9;
case LPP_POLYLINE:
return 8; // TODO: this is MINIMIUM
case LPP_GYROMETER:
case LPP_ACCELEROMETER:
return 6;
case LPP_GENERIC_SENSOR:
case LPP_FREQUENCY:
case LPP_DISTANCE:
case LPP_ENERGY:
case LPP_UNIXTIME:
return 4;
case LPP_COLOUR:
return 3;
case LPP_ANALOG_INPUT:
case LPP_ANALOG_OUTPUT:
case LPP_LUMINOSITY:
case LPP_TEMPERATURE:
case LPP_CONCENTRATION:
case LPP_BAROMETRIC_PRESSURE:
case LPP_RELATIVE_HUMIDITY:
case LPP_ALTITUDE:
case LPP_VOLTAGE:
case LPP_CURRENT:
case LPP_DIRECTION:
case LPP_POWER:
return 2;
}
return 1;
}
float getFloat(const uint8_t * buffer, uint8_t size, uint32_t multiplier, bool is_signed) {
static uint32_t getMultiplier(uint8_t type) {
switch (type) {
case LPP_CURRENT:
case LPP_DISTANCE:
case LPP_ENERGY:
return 1000;
case LPP_VOLTAGE:
case LPP_ANALOG_INPUT:
case LPP_ANALOG_OUTPUT:
return 100;
case LPP_TEMPERATURE:
case LPP_BAROMETRIC_PRESSURE:
case LPP_RELATIVE_HUMIDITY:
return 10;
}
return 1;
}
static bool isSigned(uint8_t type) {
return type == LPP_ALTITUDE || type == LPP_TEMPERATURE || type == LPP_GYROMETER ||
type == LPP_ANALOG_INPUT || type == LPP_ANALOG_OUTPUT || type == LPP_GPS || type == LPP_ACCELEROMETER;
}
static float getFloat(const uint8_t * buffer, uint8_t size, uint32_t multiplier, bool is_signed) {
uint32_t value = 0;
for (uint8_t i = 0; i < size; i++) {
value = (value << 8) + buffer[i];
@@ -85,6 +139,36 @@ class LPPReader {
return sign * ((float) value / multiplier);
}
static uint8_t putFloat(uint8_t * dest, float value, uint8_t size, uint32_t multiplier, bool is_signed) {
// check sign
bool sign = value < 0;
if (sign) value = -value;
// get value to store
uint32_t v = value * multiplier;
// format an uint32_t as if it was an int32_t
if (is_signed & sign) {
uint32_t mask = (1 << (size * 8)) - 1;
v = v & mask;
if (sign) v = mask - v + 1;
}
// add bytes (MSB first)
for (uint8_t i=1; i<=size; i++) {
dest[size - i] = (v & 0xFF);
v >>= 8;
}
return size;
}
};
class LPPReader {
const uint8_t* _buf;
uint8_t _len;
uint8_t _pos;
public:
LPPReader(const uint8_t buf[], uint8_t len) : _buf(buf), _len(len), _pos(0) { }
@@ -103,72 +187,42 @@ public:
}
bool readGPS(float& lat, float& lon, float& alt) {
lat = getFloat(&_buf[_pos], 3, 10000, true); _pos += 3;
lon = getFloat(&_buf[_pos], 3, 10000, true); _pos += 3;
alt = getFloat(&_buf[_pos], 3, 100, true); _pos += 3;
lat = LPPData::getFloat(&_buf[_pos], 3, 10000, true); _pos += 3;
lon = LPPData::getFloat(&_buf[_pos], 3, 10000, true); _pos += 3;
alt = LPPData::getFloat(&_buf[_pos], 3, 100, true); _pos += 3;
return _pos <= _len;
}
bool readVoltage(float& voltage) {
voltage = getFloat(&_buf[_pos], 2, 100, false); _pos += 2;
voltage = LPPData::getFloat(&_buf[_pos], 2, 100, false); _pos += 2;
return _pos <= _len;
}
bool readCurrent(float& amps) {
amps = getFloat(&_buf[_pos], 2, 1000, true); _pos += 2;
amps = LPPData::getFloat(&_buf[_pos], 2, 1000, true); _pos += 2;
return _pos <= _len;
}
bool readPower(float& watts) {
watts = getFloat(&_buf[_pos], 2, 1, false); _pos += 2;
watts = LPPData::getFloat(&_buf[_pos], 2, 1, false); _pos += 2;
return _pos <= _len;
}
bool readTemperature(float& degrees_c) {
degrees_c = getFloat(&_buf[_pos], 2, 10, true); _pos += 2;
degrees_c = LPPData::getFloat(&_buf[_pos], 2, 10, true); _pos += 2;
return _pos <= _len;
}
bool readPressure(float& pa) {
pa = getFloat(&_buf[_pos], 2, 10, false); _pos += 2;
pa = LPPData::getFloat(&_buf[_pos], 2, 10, false); _pos += 2;
return _pos <= _len;
}
bool readRelativeHumidity(float& pct) {
pct = getFloat(&_buf[_pos], 1, 2, false); _pos += 1;
pct = LPPData::getFloat(&_buf[_pos], 1, 2, false); _pos += 1;
return _pos <= _len;
}
bool readAltitude(float& m) {
m = getFloat(&_buf[_pos], 2, 1, true); _pos += 2;
m = LPPData::getFloat(&_buf[_pos], 2, 1, true); _pos += 2;
return _pos <= _len;
}
void skipData(uint8_t type) {
switch (type) {
case LPP_GPS:
_pos += 9; break;
case LPP_POLYLINE:
_pos += 8; break; // TODO: this is MINIMUM
case LPP_GYROMETER:
case LPP_ACCELEROMETER:
_pos += 6; break;
case LPP_GENERIC_SENSOR:
case LPP_FREQUENCY:
case LPP_DISTANCE:
case LPP_ENERGY:
case LPP_UNIXTIME:
_pos += 4; break;
case LPP_COLOUR:
_pos += 3; break;
case LPP_ANALOG_INPUT:
case LPP_ANALOG_OUTPUT:
case LPP_LUMINOSITY:
case LPP_TEMPERATURE:
case LPP_CONCENTRATION:
case LPP_BAROMETRIC_PRESSURE:
case LPP_ALTITUDE:
case LPP_VOLTAGE:
case LPP_CURRENT:
case LPP_DIRECTION:
case LPP_POWER:
_pos += 2; break;
default:
_pos++;
}
_pos += LPPData::getDataSize(type);
}
};
@@ -185,6 +239,19 @@ class LPPWriter {
public:
LPPWriter(uint8_t buf[], uint8_t max_len): _buf(buf), _max_len(max_len), _len(0) { }
bool writeData(uint8_t channel, uint8_t type, float v) {
uint8_t sz = LPPData::getDataSize(type);
bool s = LPPData::isSigned(type);
uint32_t mul = LPPData::getMultiplier(type);
if (_len + 2 + sz <= _max_len) {
_buf[_len++] = channel;
_buf[_len++] = type;
_len += LPPData::putFloat(&_buf[_len], v, sz, mul, s);
return true;
}
return false;
}
bool writeVoltage(uint8_t channel, float voltage) {
if (_len + 4 <= _max_len) {
_buf[_len++] = channel;
+12
View File
@@ -1,6 +1,8 @@
#pragma once
#include "Mesh.h"
#include <stddef.h>
#include <stdio.h>
class LocationProvider {
@@ -22,4 +24,14 @@ public:
virtual void stop() = 0;
virtual void loop() = 0;
virtual bool isEnabled() = 0;
// Format compact diagnostics in the caller-provided buffer. Providers that
// have more information (for example UART counters) can override this.
virtual void formatDiagnostics(char* out, size_t out_size) {
if (out_size == 0) return;
snprintf(out, out_size, "en:%u sat:%ld fix:%u",
isEnabled() ? 1U : 0U,
satellitesCount(),
isValid() ? 1U : 0U);
}
};
@@ -4,6 +4,7 @@
#include <MicroNMEA.h>
#include <RTClib.h>
#include <helpers/RefCountedDigitalPin.h>
#include <string.h>
#ifndef GPS_EN
#ifdef PIN_GPS_EN
@@ -61,6 +62,31 @@ class MicroNMEALocationProvider : public LocationProvider {
delay(250);
}
#endif
// Observation-only diagnostics. These fields do not change GPS power,
// timing, reset behaviour, or parser input.
uint32_t _uart_bytes = 0;
uint32_t _nmea_checksum_ok = 0;
uint32_t _nmea_checksum_bad = 0;
uint32_t _begin_calls = 0;
uint32_t _stop_calls = 0;
unsigned long _last_uart_ms = 0;
unsigned long _last_nmea_ms = 0;
unsigned long _last_fix_ms = 0;
bool _uart_seen = false;
bool _nmea_seen = false;
bool _fix_seen = false;
static uint32_t ageMs(unsigned long timestamp) {
return (uint32_t)(millis() - timestamp);
}
static void formatAge(char* out, size_t out_size, bool seen, unsigned long timestamp) {
if (!seen) {
snprintf(out, out_size, "never");
} else {
snprintf(out, out_size, "%lu", (unsigned long)ageMs(timestamp));
}
}
public :
MicroNMEALocationProvider(Stream& ser, mesh::RTCClock* clock = NULL, int pin_reset = GPS_RESET, int pin_en = GPS_EN,RefCountedDigitalPin* peripher_power=NULL) :
@@ -87,6 +113,7 @@ public :
}
void begin() override {
_begin_calls++;
claim();
if (_pin_en != -1) {
digitalWrite(_pin_en, GPS_EN_ACTIVE);
@@ -110,6 +137,7 @@ public :
}
void stop() override {
_stop_calls++;
if (_pin_en != -1) {
digitalWrite(_pin_en, !GPS_EN_ACTIVE);
}
@@ -153,10 +181,36 @@ public :
while (_gps_serial->available()) {
char c = _gps_serial->read();
_uart_bytes++;
_last_uart_ms = millis();
_uart_seen = true;
#ifdef GPS_NMEA_DEBUG
Serial.print(c);
#endif
nmea.process(c);
bool parsed = nmea.process(c);
// MicroNMEA leaves the completed sentence in its buffer after the
// first line terminator. CRLF therefore counts once: CR observes the
// sentence, then LF clears the now-empty buffer.
if ((c == '\0' || c == '\r' || c == '\n') && nmea.getSentence()[0] != '\0') {
if (MicroNMEA::testChecksum(nmea.getSentence())) {
_nmea_checksum_ok++;
_last_nmea_ms = millis();
_nmea_seen = true;
// Refresh fix age only when a newly parsed GGA/RMC sentence
// carries a valid fix. Other valid NMEA sentences must not
// make an old position look fresh.
const char* message_id = nmea.getMessageID();
if (parsed && isValid() &&
(strcmp(message_id, "GGA") == 0 || strcmp(message_id, "RMC") == 0)) {
_last_fix_ms = millis();
_fix_seen = true;
}
} else {
_nmea_checksum_bad++;
}
}
}
if (!isValid()) time_valid = 0;
@@ -179,4 +233,31 @@ public :
}
}
}
void formatDiagnostics(char* out, size_t out_size) override {
if (out_size == 0) return;
char uart_age[11];
char nmea_age[11];
char fix_age[11];
formatAge(uart_age, sizeof(uart_age), _uart_seen, _last_uart_ms);
formatAge(nmea_age, sizeof(nmea_age), _nmea_seen, _last_nmea_ms);
formatAge(fix_age, sizeof(fix_age), _fix_seen, _last_fix_ms);
// Compact enough for CommonCLI's 160-byte reply buffer, including the
// "req:" prefix added by CommonCLI.
snprintf(out, out_size,
"en:%u ub:%lu ua:%s ok:%lu bad:%lu na:%s sat:%ld fix:%u fa:%s bc:%lu sc:%lu",
isEnabled() ? 1U : 0U,
(unsigned long)_uart_bytes,
uart_age,
(unsigned long)_nmea_checksum_ok,
(unsigned long)_nmea_checksum_bad,
nmea_age,
satellitesCount(),
isValid() ? 1U : 0U,
fix_age,
(unsigned long)_begin_calls,
(unsigned long)_stop_calls);
}
};
+3
View File
@@ -2,6 +2,7 @@
#include <MeshCore.h>
#include <Arduino.h>
#include <helpers/KeyValueStore.h>
class STM32Board : public mesh::MainBoard {
protected:
@@ -12,6 +13,8 @@ public:
startup_reason = BD_STARTUP_NORMAL;
}
void attachDynamicPrefs(KeyValueStore* prefs) { } // no-op
uint8_t getStartupReason() const override { return startup_reason; }
uint16_t getBattMilliVolts() override {
+1
View File
@@ -23,6 +23,7 @@ public:
virtual bool isOn() = 0;
virtual bool isEink() { return false; } // default to non-eink, override in eink drivers
virtual void forceFullRefresh() {} // next refresh will be full for eink
virtual void turnOn() = 0;
virtual void turnOff() = 0;
virtual void clear() = 0;
+44 -6
View File
@@ -14,6 +14,10 @@
SPIClass SPI1 = SPIClass(FSPI);
#endif
#ifndef EPD_WASHING_MACHINE_CYCLES
#define EPD_WASHING_MACHINE_CYCLES 0
#endif
// Color scheme
ColorVal UIColor::window_bkg = GxEPD_WHITE;
ColorVal UIColor::title_bkg = GxEPD_WHITE;
@@ -25,7 +29,6 @@ ColorVal UIColor::popup_bkg = GxEPD_WHITE;
ColorVal UIColor::popup_txt = GxEPD_BLACK;
ColorVal UIColor::corp_blue = GxEPD_BLACK;
bool GxEPDDisplay::begin() {
display.epd2.selectSPI(SPI1, SPISettings(4000000, MSBFIRST, SPI_MODE0));
#ifdef ESP32
@@ -36,15 +39,27 @@ bool GxEPDDisplay::begin() {
display.init(115200, true, 2, false);
display.setRotation(DISPLAY_ROTATION);
setTextSize(1); // Default to size 1
display.setPartialWindow(0, 0, display.width(), display.height());
display.fillScreen(GxEPD_WHITE);
display.display(true);
display.setFullWindow();
for (int i = 0; i < EPD_WASHING_MACHINE_CYCLES; i++) {
display.fillScreen(GxEPD_BLACK);
display.display(false);
delay(2000);
display.fillScreen(GxEPD_WHITE);
display.display(false);
delay(2000);
}
display.setPartialWindow(0, 0, display.width(), display.height());
resetPartialRefreshCounter();
#if DISP_BACKLIGHT
digitalWrite(DISP_BACKLIGHT, LOW);
pinMode(DISP_BACKLIGHT, OUTPUT);
#endif
_init = true;
_isOn = true;
return true;
}
@@ -55,7 +70,9 @@ void GxEPDDisplay::turnOn() {
#elif defined(EXP_PIN_BACKLIGHT) && !defined(BACKLIGHT_BTN)
expander.digitalWrite(EXP_PIN_BACKLIGHT, HIGH);
#endif
_isOn = true;
if (!_isOn) {
_isOn = true;
}
}
void GxEPDDisplay::turnOff() {
@@ -65,6 +82,13 @@ void GxEPDDisplay::turnOff() {
expander.digitalWrite(EXP_PIN_BACKLIGHT, LOW);
#endif
_isOn = false;
// do full refresh before powering off to clear screen
// no full refresh needed at wakeup
display.clearScreen(0xFF); // Clears microcontroller side RAM
display.writeScreenBuffer(0xFF); // Forces 0xFF (White) into the display controller's history registers
resetPartialRefreshCounter();
last_display_crc_value=0;
display.hibernate();
}
void GxEPDDisplay::clear() {
@@ -77,6 +101,16 @@ void GxEPDDisplay::startFrame(ColorVal bkg) {
display.fillScreen(bkg);
display.setTextColor(_curr_color = UIColor::primary_txt);
display_crc.reset();
if (_cycles_before_full_refresh != 0) {
display.setPartialWindow(0, 0, display.width(), display.height());
} else {
// forces a full wipe of the screen ...
display.clearScreen(0xFF); // Clears microcontroller side RAM
display.writeScreenBuffer(0xFF); // Forces 0xFF (White) into the display controller's history registers
// we'll need a partial refresh after that (whatever crc value is)
last_display_crc_value = 0;
resetPartialRefreshCounter();
}
}
void GxEPDDisplay::setTextSize(int sz) {
@@ -178,9 +212,13 @@ uint16_t GxEPDDisplay::getTextWidth(const char* str) {
}
void GxEPDDisplay::endFrame() {
if (_isOn == false) return;
uint32_t crc = display_crc.finalize();
if (crc != last_display_crc_value) {
display.display(true);
last_display_crc_value = crc;
if (_cycles_before_full_refresh > 0) {
_cycles_before_full_refresh--;
}
}
last_display_crc_value = crc;
}
+10
View File
@@ -16,6 +16,12 @@
#include "DisplayDriver.h"
#ifndef EINK_MAX_PARTIAL_REFRESH
// 60 to prevent ghosting when refreshing every minute ...
// set to -1 to disable the counter
#define EINK_MAX_PARTIAL_REFRESH -1
#endif
class GxEPDDisplay : public DisplayDriver {
#if defined(EINK_DISPLAY_MODEL)
@@ -36,6 +42,8 @@ class GxEPDDisplay : public DisplayDriver {
uint16_t _curr_color;
CRC32 display_crc;
int last_display_crc_value = 0;
int _cycles_before_full_refresh;
int max_partial_refresh = EINK_MAX_PARTIAL_REFRESH; // so this can be changed by an option after
public:
#if defined(EINK_DISPLAY_MODEL)
@@ -48,6 +56,8 @@ public:
bool isOn() override { return _isOn; }
bool isEink() override { return true; }
void forceFullRefresh() override { _cycles_before_full_refresh = 0; };
void resetPartialRefreshCounter() { _cycles_before_full_refresh = max_partial_refresh; };
void turnOn() override;
void turnOff() override;
void clear() override;
+2 -2
View File
@@ -25,8 +25,8 @@ class MomentaryButton {
public:
MomentaryButton(int8_t pin, int long_press_mills=0, bool reverse=false, bool pulldownup=false, bool multiclick=true);
MomentaryButton(int8_t pin, int long_press_mills, int analog_threshold);
void begin();
int check(bool repeat_click=false); // returns one of BUTTON_EVENT_*
virtual void begin();
virtual int check(bool repeat_click=false); // returns one of BUTTON_EVENT_*
void cancelClick(); // suppress next BUTTON_EVENT_CLICK (if already in DOWN state)
uint8_t getPin() { return _pin; }
bool isPressed() const;
+34
View File
@@ -0,0 +1,34 @@
#include "RotaryInputGPIO.h"
bool RotaryInputGPIO::begin() {
if (_pin_a >= 0) {
pinMode(_pin_a, _pull_a ? INPUT_PULLUP : INPUT);
}
if (_pin_b >= 0) {
pinMode(_pin_b, _pull_b ? INPUT_PULLUP : INPUT);
}
b_prec = digitalRead(_pin_b);
return true;
}
RotaryInputEvent RotaryInputGPIO::poll() {
RotaryInputEvent ev = RotaryInputEvent::None;
bool a = digitalRead(_pin_a);
bool b = digitalRead(_pin_b);
// this is the simplest scheme and it works well
// for thinknode M8, just read A when B rises ;)
if (!b_prec && b) { // rising edge of A
if (a) {
ev = RotaryInputEvent::Next;
} else {
ev = RotaryInputEvent::Prev;
}
}
b_prec = b;
return ev;
}
+21
View File
@@ -0,0 +1,21 @@
#pragma once
#include <Arduino.h>
#include "RotaryInput.h"
#define BUTTON_EVENT_UP 5
#define BUTTON_EVENT_DOWN 6
class RotaryInputGPIO: public RotaryInput {
int8_t _pin_a;
bool _pull_a;
int8_t _pin_b;
bool _pull_b;
bool b_prec;
public:
RotaryInputGPIO(int8_t pin_a, int8_t pin_b, bool pull_a=false, bool pull_b=false): _pin_a(pin_a), _pin_b(pin_b), _pull_a(pull_a), _pull_b(pull_b) {}
bool begin() override;
RotaryInputEvent poll() override;
bool isReady() const override { return true;}
};
+113
View File
@@ -0,0 +1,113 @@
#include "SH1107Display.h"
#include <Adafruit_GrayOLED.h>
#include "Adafruit_SH110X.h"
#ifndef DISPLAY_ROTATION
#define DISPLAY_ROTATION 0
#endif
ColorVal UIColor::window_bkg = SH110X_BLACK;
ColorVal UIColor::title_bkg = SH110X_BLACK;
ColorVal UIColor::title_txt = SH110X_WHITE;
ColorVal UIColor::primary_txt = SH110X_WHITE;
ColorVal UIColor::secondary_txt = SH110X_WHITE;
ColorVal UIColor::warning_txt = SH110X_WHITE;
ColorVal UIColor::popup_bkg = SH110X_BLACK;
ColorVal UIColor::popup_txt = SH110X_WHITE;
ColorVal UIColor::corp_blue = SH110X_WHITE;
bool SH1107Display::i2c_probe(TwoWire &wire, uint8_t addr)
{
wire.beginTransmission(addr);
uint8_t error = wire.endTransmission();
return (error == 0);
}
bool SH1107Display::begin()
{
bool result = display.begin(DISPLAY_ADDRESS, true) && i2c_probe(Wire, DISPLAY_ADDRESS);
if (result) {
display.setRotation(DISPLAY_ROTATION);
}
return result;
}
void SH1107Display::turnOn()
{
display.oled_command(SH110X_DISPLAYON);
uint8_t cmd[] = {0xD5, 0xF0};
display.oled_commandList(cmd, 2);
_isOn = true;
}
void SH1107Display::turnOff()
{
display.oled_command(SH110X_DISPLAYOFF);
_isOn = false;
}
void SH1107Display::clear()
{
display.clearDisplay();
display.display();
}
void SH1107Display::startFrame(ColorVal bkg)
{
display.clearDisplay(); // TODO: apply 'bkg'
display.setContrast(120); // 0-127. default setting was causing some flickering.
// display.SH110X_SETPRECHARGE(255);
_color = SH110X_WHITE;
display.setTextColor(_color);
display.setTextSize(1);
display.cp437(true); // Use full 256 char 'Code Page 437' font
}
void SH1107Display::setTextSize(int sz)
{
display.setTextSize(sz);
}
void SH1107Display::setColor(ColorVal c)
{
_color = (c != 0) ? SH110X_WHITE : SH110X_BLACK;
display.setTextColor(_color);
}
void SH1107Display::setCursor(int x, int y)
{
display.setCursor(x, y);
}
void SH1107Display::print(const char *str)
{
display.print(str);
}
void SH1107Display::fillRect(int x, int y, int w, int h)
{
display.fillRect(x, y, w, h, _color);
}
void SH1107Display::drawRect(int x, int y, int w, int h)
{
display.drawRect(x, y, w, h, _color);
}
void SH1107Display::drawXbm(int x, int y, const uint8_t *bits, int w, int h)
{
display.drawBitmap(x, y, bits, w, h, SH110X_WHITE);
}
uint16_t SH1107Display::getTextWidth(const char *str)
{
int16_t x1, y1;
uint16_t w, h;
display.getTextBounds(str, 0, 0, &x1, &y1, &w, &h);
return w;
}
void SH1107Display::endFrame()
{
display.display();
}
+43
View File
@@ -0,0 +1,43 @@
#pragma once
#include "DisplayDriver.h"
#include <Wire.h>
#include <Adafruit_GFX.h>
#define SH110X_NO_SPLASH
#include <Adafruit_SH110X.h>
#ifndef PIN_OLED_RESET
#define PIN_OLED_RESET -1
#endif
#ifndef DISPLAY_ADDRESS
#define DISPLAY_ADDRESS 0x3c
#endif
class SH1107Display : public DisplayDriver
{
Adafruit_SH1107 display;
bool _isOn;
uint8_t _color;
bool i2c_probe(TwoWire &wire, uint8_t addr);
public:
SH1107Display() : DisplayDriver(128, 128), display(128, 128, &Wire, PIN_OLED_RESET) { _isOn = false; }
bool begin();
bool isOn() override { return _isOn; }
void turnOn() override;
void turnOff() override;
void clear() override;
void startFrame(ColorVal bkg = UIColor::window_bkg) override;
void setTextSize(int sz) override;
void setColor(ColorVal c) override;
void setCursor(int x, int y) override;
void print(const char *str) override;
void fillRect(int x, int y, int w, int h) override;
void drawRect(int x, int y, int w, int h) override;
void drawXbm(int x, int y, const uint8_t *bits, int w, int h) override;
uint16_t getTextWidth(const char *str) override;
void endFrame() override;
};