mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-09-26 16:27:57 +00:00
Merge upstream/dev into observer-firmware-dev
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"build": {
|
||||
"arduino": {
|
||||
"ldscript": "nrf52840_s140_v6.ld"
|
||||
},
|
||||
"core": "nRF5",
|
||||
"cpu": "cortex-m4",
|
||||
"extra_flags": "-DARDUINO_NRF52840_ThinkNode_M8 -DNRF52840_XXAA",
|
||||
"f_cpu": "64000000L",
|
||||
"hwids": [
|
||||
["0x239A", "0x4405"],
|
||||
["0x239A", "0x0029"],
|
||||
["0x239A", "0x002A"]
|
||||
],
|
||||
"usb_product": "elecrow_thinknode_m8",
|
||||
"mcu": "nrf52840",
|
||||
"variant": "ELECROW-ThinkNode-M8",
|
||||
"variants_dir": "variants",
|
||||
"bsp": {
|
||||
"name": "adafruit"
|
||||
},
|
||||
"softdevice": {
|
||||
"sd_flags": "-DS140",
|
||||
"sd_name": "s140",
|
||||
"sd_version": "6.1.1",
|
||||
"sd_fwid": "0x00B6"
|
||||
},
|
||||
"bootloader": {
|
||||
"settings_addr": "0xFF000"
|
||||
}
|
||||
},
|
||||
"connectivity": ["bluetooth"],
|
||||
"debug": {
|
||||
"jlink_device": "nRF52840_xxAA",
|
||||
"onboard_tools": ["jlink"],
|
||||
"svd_path": "nrf52840.svd",
|
||||
"openocd_target": "nrf52840-mdk-rs"
|
||||
},
|
||||
"frameworks": ["arduino"],
|
||||
"name": "elecrow thinknode m8",
|
||||
"upload": {
|
||||
"maximum_ram_size": 248832,
|
||||
"maximum_size": 815104,
|
||||
"speed": 115200,
|
||||
"protocol": "nrfutil",
|
||||
"protocols": ["jlink", "nrfjprog", "nrfutil", "stlink"],
|
||||
"use_1200bps_touch": true,
|
||||
"require_upload_port": true,
|
||||
"wait_for_upload_port": true
|
||||
},
|
||||
"url": "",
|
||||
"vendor": "ELECROW"
|
||||
}
|
||||
+2
-1
@@ -171,8 +171,9 @@ txt_type
|
||||
| Value | Description | Message content |
|
||||
|--------|---------------------------|--------------------------------------------------------------------------|
|
||||
| `0x00` | plain text message | the plain text of the message |
|
||||
| `0x01` | CLI command | the command text of the message |
|
||||
| `0x01` | CLI data | CLI command OR reply text |
|
||||
| `0x02` | signed plain text message | first four bytes is sender pubkey prefix, followed by plain text message |
|
||||
| `0x03` | CLI command | (since v1.18+) CLI command text (explicit) |
|
||||
|
||||
## Anonymous request
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
#define CMD_SET_DEFAULT_FLOOD_SCOPE 63
|
||||
#define CMD_GET_DEFAULT_FLOOD_SCOPE 64
|
||||
#define CMD_SEND_RAW_PACKET 65
|
||||
#define CMD_RUN_CLI_COMMAND 66 // v14+
|
||||
|
||||
// Stats sub-types for CMD_GET_STATS
|
||||
#define STATS_TYPE_CORE 0
|
||||
@@ -97,6 +98,7 @@
|
||||
#define RESP_ALLOWED_REPEAT_FREQ 26
|
||||
#define RESP_CODE_CHANNEL_DATA_RECV 27
|
||||
#define RESP_CODE_DEFAULT_FLOOD_SCOPE 28
|
||||
#define RESP_CODE_CLI_REPLY 29 // v14+, a reply to CMD_RUN_CLI_COMMAND
|
||||
|
||||
#define MAX_CHANNEL_DATA_LENGTH (MAX_FRAME_SIZE - 9)
|
||||
|
||||
@@ -259,7 +261,7 @@ float MyMesh::getAirtimeBudgetFactor() const {
|
||||
}
|
||||
|
||||
bool MyMesh::getCADEnabled() const {
|
||||
return false; // hardware CAD before TX (disabled by default, until configurable)
|
||||
return _prefs.cad_enabled;
|
||||
}
|
||||
|
||||
int MyMesh::getInterferenceThreshold() const {
|
||||
@@ -529,12 +531,23 @@ void MyMesh::onMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t
|
||||
queueMessage(from, TXT_TYPE_PLAIN, pkt, sender_timestamp, NULL, 0, text);
|
||||
}
|
||||
|
||||
void MyMesh::onCommandDataRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp,
|
||||
const char *text) {
|
||||
void MyMesh::onCommandDataRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp, const char *text) {
|
||||
markConnectionActive(from); // in case this is from a server, and we have a connection
|
||||
queueMessage(from, TXT_TYPE_CLI_DATA, pkt, sender_timestamp, NULL, 0, text);
|
||||
}
|
||||
|
||||
void MyMesh::onCLICommandRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp,
|
||||
const char *text, char* reply) {
|
||||
markConnectionActive(from); // in case this is from a server, and we have a connection
|
||||
if (from.isRemoteCLIAllowed()) {
|
||||
if (!handleCommand(text, sender_timestamp, reply)) {
|
||||
strcat(reply, "Unknown command"); // reply may have cmd prefix from 'text'
|
||||
}
|
||||
} else {
|
||||
queueMessage(from, TXT_TYPE_CLI_COMMAND, pkt, sender_timestamp, NULL, 0, text);
|
||||
}
|
||||
}
|
||||
|
||||
void MyMesh::onSignedMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp,
|
||||
const uint8_t *sender_prefix, const char *text) {
|
||||
markConnectionActive(from);
|
||||
@@ -977,8 +990,9 @@ void MyMesh::begin(bool has_display) {
|
||||
radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
|
||||
radio_driver.setTxPower(_prefs.tx_power_dbm);
|
||||
radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain);
|
||||
board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain);
|
||||
board.setLoRaFemPaGainEnabled(_prefs.radio_fem_txgain);
|
||||
|
||||
board.attachDynamicPrefs(_prefs.getCustom());
|
||||
|
||||
MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s",
|
||||
radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled");
|
||||
// NOTE: no FEM LNA wiring here — companion has its own NodePrefs without
|
||||
@@ -1085,6 +1099,20 @@ void MyMesh::handleCmdFrame(size_t len) {
|
||||
memcpy(&out_frame[i], _prefs.node_name, tlen);
|
||||
i += tlen;
|
||||
_serial->writeFrame(out_frame, i);
|
||||
} else if (cmd_frame[0] == CMD_RUN_CLI_COMMAND && len >= 3) { // V14+
|
||||
int i = 1;
|
||||
char *text = (char *)&cmd_frame[i];
|
||||
int tlen = len - i;
|
||||
text[tlen] = 0; // ensure null
|
||||
|
||||
reply_buf[0] = 0;
|
||||
if (!handleCommand(text, 0, reply_buf)) {
|
||||
strcat(reply_buf, "Unknown command"); // reply_buf may have cmd prefix from 'text'
|
||||
}
|
||||
out_frame[0] = RESP_CODE_CLI_REPLY;
|
||||
int rlen = strlen(reply_buf);
|
||||
memcpy(&out_frame[1], reply_buf, rlen);
|
||||
_serial->writeFrame(out_frame, 1 + rlen);
|
||||
} else if (cmd_frame[0] == CMD_SEND_TXT_MSG && len >= 14) {
|
||||
int i = 1;
|
||||
uint8_t txt_type = cmd_frame[i++];
|
||||
@@ -1095,16 +1123,16 @@ void MyMesh::handleCmdFrame(size_t len) {
|
||||
uint8_t *pub_key_prefix = &cmd_frame[i];
|
||||
i += 6;
|
||||
ContactInfo *recipient = lookupContactByPubKey(pub_key_prefix, 6);
|
||||
if (recipient && (txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_CLI_DATA)) {
|
||||
if (recipient && (txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_CLI_DATA || txt_type == TXT_TYPE_CLI_COMMAND)) {
|
||||
char *text = (char *)&cmd_frame[i];
|
||||
int tlen = len - i;
|
||||
uint32_t est_timeout;
|
||||
text[tlen] = 0; // ensure null
|
||||
int result;
|
||||
uint32_t expected_ack;
|
||||
if (txt_type == TXT_TYPE_CLI_DATA) {
|
||||
if (txt_type == TXT_TYPE_CLI_DATA || txt_type == TXT_TYPE_CLI_COMMAND) {
|
||||
msg_timestamp = getRTCClock()->getCurrentTimeUnique(); // Use node's RTC instead of app timestamp to avoid tripping replay protection
|
||||
result = sendCommandData(*recipient, msg_timestamp, attempt, text, est_timeout);
|
||||
result = sendCommandData(*recipient, msg_timestamp, attempt, txt_type, text, est_timeout);
|
||||
expected_ack = 0; // no Ack expected
|
||||
} else {
|
||||
result = sendMessage(*recipient, msg_timestamp, attempt, text, expected_ack, est_timeout);
|
||||
@@ -1514,16 +1542,20 @@ void MyMesh::handleCmdFrame(size_t len) {
|
||||
#endif
|
||||
} else if (cmd_frame[0] == CMD_SEND_RAW_DATA && len >= 6) {
|
||||
int i = 1;
|
||||
int8_t path_len = cmd_frame[i++];
|
||||
if (path_len >= 0 && i + path_len + 4 <= len) { // minimum 4 byte payload
|
||||
uint8_t *path = &cmd_frame[i];
|
||||
i += path_len;
|
||||
auto pkt = createRawData(&cmd_frame[i], len - i);
|
||||
if (pkt) {
|
||||
sendDirect(pkt, path, path_len);
|
||||
writeOKFrame();
|
||||
uint8_t path_len = cmd_frame[i++];
|
||||
if (path_len >= 0 && mesh::Packet::isValidPathLen(path_len)) {
|
||||
uint8_t path[MAX_PATH_SIZE];
|
||||
i += mesh::Packet::writePath(path, &cmd_frame[i], path_len);
|
||||
if (i + 4 > len) { // min payload 4 bytes
|
||||
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
|
||||
} else {
|
||||
writeErrFrame(ERR_CODE_TABLE_FULL);
|
||||
auto pkt = createRawData(&cmd_frame[i], len - i);
|
||||
if (pkt) {
|
||||
sendDirect(pkt, path, path_len);
|
||||
writeOKFrame();
|
||||
} else {
|
||||
writeErrFrame(ERR_CODE_TABLE_FULL);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); // flood, not supported (yet)
|
||||
@@ -2030,6 +2062,62 @@ void MyMesh::enterCLIRescue() {
|
||||
Serial.println("========= CLI Rescue =========");
|
||||
}
|
||||
|
||||
bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* reply) {
|
||||
while (*command == ' ') command++; // skip leading spaces
|
||||
|
||||
if (strlen(command) > 4 && command[2] == '|') { // optional prefix (for companion radio CLI)
|
||||
memcpy(reply, command, 3); // reflect the prefix back
|
||||
reply += 3;
|
||||
*reply = 0;
|
||||
command += 3;
|
||||
}
|
||||
|
||||
if (_prefs.getRadioPrefs()->handleCommand(command, sender_timestamp, reply)) { // is radio CLI command?
|
||||
if (_prefs.getRadioPrefs()->isDirty()) { savePrefs(); }
|
||||
return true;
|
||||
}
|
||||
|
||||
// hook for variant-specific CLI processing
|
||||
if (board.handleCommand(command, sender_timestamp, reply)) {
|
||||
if (_prefs.isDirty()) { savePrefs(); }
|
||||
return true;
|
||||
}
|
||||
|
||||
if (memcmp(command, "set name ", 9) == 0) {
|
||||
if (AdvertDataParser::isValidName(&command[9])) {
|
||||
StrHelper::strncpy(_prefs.node_name, &command[9], sizeof(_prefs.node_name));
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
} else {
|
||||
strcpy(reply, "Error, bad chars");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (strcmp(command, "get name") == 0) {
|
||||
sprintf(reply, "> %s", _prefs.node_name);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (memcmp(command, "set pin ", 8) == 0) {
|
||||
_prefs.ble_pin = atoi(&command[8]);
|
||||
savePrefs();
|
||||
sprintf(reply, "> pin is now %06d", _prefs.ble_pin);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (strcmp(command, "board") == 0) {
|
||||
strcpy(reply, board.getManufacturerName());
|
||||
return true;
|
||||
}
|
||||
|
||||
if (strcmp(command, "ver") == 0) {
|
||||
sprintf(reply, "%s (Build: %s)", FIRMWARE_VERSION, FIRMWARE_BUILD_DATE);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false; // not handled
|
||||
}
|
||||
|
||||
void MyMesh::checkCLIRescueCmd() {
|
||||
int len = strlen(cli_command);
|
||||
while (Serial.available() && len < sizeof(cli_command)-1) {
|
||||
@@ -2047,15 +2135,10 @@ void MyMesh::checkCLIRescueCmd() {
|
||||
if (len > 0 && cli_command[len - 1] == '\r') { // received complete line
|
||||
cli_command[len - 1] = 0; // replace newline with C string null terminator
|
||||
|
||||
if (memcmp(cli_command, "set ", 4) == 0) {
|
||||
const char* config = &cli_command[4];
|
||||
if (memcmp(config, "pin ", 4) == 0) {
|
||||
_prefs.ble_pin = atoi(&config[4]);
|
||||
savePrefs();
|
||||
Serial.printf(" > pin is now %06d\n", _prefs.ble_pin);
|
||||
} else {
|
||||
Serial.printf(" Error: unknown config: %s\n", config);
|
||||
}
|
||||
reply_buf[0] = 0;
|
||||
if (handleCommand(cli_command, 0, reply_buf)) {
|
||||
// command was handled, print reply output
|
||||
Serial.print(" "); Serial.print(reply_buf); Serial.println();
|
||||
} else if (strcmp(cli_command, "rebuild") == 0) {
|
||||
bool success = _store->formatFileSystem();
|
||||
if (success) {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
#include "AbstractUITask.h"
|
||||
|
||||
/*------------ Frame Protocol --------------*/
|
||||
#define FIRMWARE_VER_CODE 13
|
||||
#define FIRMWARE_VER_CODE 14
|
||||
|
||||
#ifndef FIRMWARE_BUILD_DATE
|
||||
#define FIRMWARE_BUILD_DATE "14 Aug 2026"
|
||||
@@ -135,6 +135,8 @@ protected:
|
||||
const char *text) override;
|
||||
void onCommandDataRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp,
|
||||
const char *text) override;
|
||||
void onCLICommandRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp,
|
||||
const char *text, char* reply) override;
|
||||
void onSignedMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp,
|
||||
const uint8_t *sender_prefix, const char *text) override;
|
||||
void onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packet *pkt, uint32_t timestamp,
|
||||
@@ -169,6 +171,7 @@ public:
|
||||
_prefs.node_lat = sensors.node_lat;
|
||||
_prefs.node_lon = sensors.node_lon;
|
||||
_store->savePrefs(_prefs);
|
||||
_prefs.clearDirty();
|
||||
}
|
||||
|
||||
#if ENV_INCLUDE_GPS == 1
|
||||
@@ -201,6 +204,7 @@ private:
|
||||
}
|
||||
|
||||
void checkCLIRescueCmd();
|
||||
bool handleCommand(const char* text, uint32_t sender_timestamp, char* reply);
|
||||
void checkSerialInterface();
|
||||
bool isValidClientRepeatFreq(uint32_t f) const;
|
||||
|
||||
@@ -225,6 +229,7 @@ private:
|
||||
bool _cli_rescue;
|
||||
bool send_unscoped; // force un-scoped flood (instead of using send_scope)
|
||||
char cli_command[80];
|
||||
char reply_buf[166];
|
||||
uint8_t app_target_ver;
|
||||
uint8_t *sign_data;
|
||||
uint32_t sign_data_len;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#pragma once
|
||||
#include <cstdint> // For uint8_t, uint32_t
|
||||
#include <helpers/ConfigSerializer.h>
|
||||
#include <helpers/CommonRadioPrefs.h>
|
||||
#include <helpers/DynamicConfigSerializer.h>
|
||||
|
||||
#define TELEM_MODE_DENY 0
|
||||
#define TELEM_MODE_ALLOW_FLAGS 1 // use contact.flags
|
||||
@@ -38,11 +40,12 @@ public:
|
||||
uint8_t _client_repeat = 0; // DEPRECATED -> use repeat.disable_fwd
|
||||
uint8_t path_hash_mode = 0; // which path mode to use when sending
|
||||
uint8_t autoadd_max_hops = 0; // 0 = no limit, 1 = direct (0 hops), N = up to N-1 hops (max 64)
|
||||
uint8_t cad_enabled = 0;
|
||||
char default_scope_name[31];
|
||||
uint8_t default_scope_key[16];
|
||||
|
||||
private:
|
||||
class RadioPrefs : public ConfigSerializer { // COPIED from CommonCLI (for now)
|
||||
class RadioPrefs : public CommonRadioPrefs {
|
||||
NodePrefs* _parent;
|
||||
protected:
|
||||
void structure() override {
|
||||
@@ -50,15 +53,11 @@ private:
|
||||
def("bw", _parent->bw);
|
||||
def("sf", _parent->sf);
|
||||
def("cr", _parent->cr);
|
||||
//def("cad", _parent->cad_enabled);
|
||||
def("cad", _parent->cad_enabled);
|
||||
//def("int_thr", _parent->interference_threshold);
|
||||
def("rxgain", _parent->rx_boosted_gain);
|
||||
#if 0
|
||||
// NOTE: these cannot be set (yet) so don't load/save until we can.
|
||||
// also, fem_rxgain WAS mapped to wrong JSON property previously
|
||||
def("fem_rxgain", _parent->radio_fem_rxgain);
|
||||
def("fem_rxgain", _parent->radio_fem_rxgain); // fem_rxgain WAS mapped to wrong JSON property previously
|
||||
def("fem_txgain", _parent->radio_fem_txgain);
|
||||
#endif
|
||||
def("tx", _parent->tx_power_dbm);
|
||||
def("af", _parent->airtime_factor);
|
||||
def("rxdelay", _parent->rx_delay_base);
|
||||
@@ -70,6 +69,42 @@ 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 0; }
|
||||
void setIntThresh(uint8_t t) override { /* no-op */ }
|
||||
uint8_t getRxGain() const override { return _parent->rx_boosted_gain; }
|
||||
void setRxGain(uint8_t g) override { _parent->rx_boosted_gain = g; markDirty(); }
|
||||
int8_t getTxPower() const override { return _parent->tx_power_dbm; }
|
||||
void setTxPower(int8_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(); }
|
||||
uint8_t getAgcResetInt() const override { return 0; }
|
||||
void setAgcResetInt(uint8_t secs) override { /* no-op */ }
|
||||
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 0.5f; } // currently hard-coded
|
||||
void setFloodTxDelay(float d) override { /* no-op */ }
|
||||
float getDirectTxDelay() const override { return 0.2f; } // currently hard-coded
|
||||
void setDirectTxDelay(float d) override { /* no-op */ }
|
||||
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;
|
||||
|
||||
@@ -121,6 +156,8 @@ private:
|
||||
};
|
||||
CompanionPrefs companion;
|
||||
|
||||
DynamicConfigSerializer custom;
|
||||
|
||||
protected:
|
||||
void structure() override {
|
||||
def("name", node_name, sizeof(node_name));
|
||||
@@ -132,9 +169,10 @@ protected:
|
||||
def("gps", gps);
|
||||
def("repeat", repeat);
|
||||
def("comp", companion);
|
||||
def("custom", custom);
|
||||
}
|
||||
public:
|
||||
NodePrefs() : radio(this), gps(this), companion(this) {
|
||||
NodePrefs() : radio(this), gps(this), companion(this), custom(&radio) {
|
||||
node_name[0] = 0;
|
||||
default_scope_name[0] = 0;
|
||||
memset(default_scope_key, 0, sizeof(default_scope_key));
|
||||
@@ -142,4 +180,10 @@ public:
|
||||
// new accessor methods
|
||||
bool isRepeatEn() const { return repeat.disable_fwd == 0; }
|
||||
void setRepeatEn(bool en) { repeat.disable_fwd = en ? 0 : 1; }
|
||||
|
||||
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(); }
|
||||
};
|
||||
|
||||
@@ -2,10 +2,15 @@
|
||||
#include <helpers/TxtDataHelpers.h>
|
||||
#include "../MyMesh.h"
|
||||
#include "target.h"
|
||||
#include <time.h>
|
||||
#ifdef WIFI_SSID
|
||||
#include <WiFi.h>
|
||||
#endif
|
||||
|
||||
#ifndef UI_TZ_OFFSET
|
||||
#define UI_TZ_OFFSET 0
|
||||
#endif
|
||||
|
||||
#ifndef AUTO_OFF_MILLIS
|
||||
#define AUTO_OFF_MILLIS 15000 // 15 seconds
|
||||
#endif
|
||||
@@ -23,7 +28,7 @@
|
||||
#define UI_RECENT_LIST_SIZE 4
|
||||
#endif
|
||||
|
||||
#if UI_HAS_JOYSTICK
|
||||
#if UI_HAS_JOYSTICK || UI_HAS_ROTARY_INPUT
|
||||
#define PRESS_LABEL "press Enter"
|
||||
#else
|
||||
#define PRESS_LABEL "long press"
|
||||
@@ -97,7 +102,9 @@ class HomeScreen : public UIScreen {
|
||||
#if UI_SENSORS_PAGE == 1
|
||||
SENSORS,
|
||||
#endif
|
||||
#ifndef UI_NO_HIBERNATE
|
||||
SHUTDOWN,
|
||||
#endif
|
||||
Count // keep as last
|
||||
};
|
||||
|
||||
@@ -141,11 +148,24 @@ class HomeScreen : public UIScreen {
|
||||
int fillWidth = (batteryPercentage * (iconWidth - 4)) / 100;
|
||||
display.fillRect(iconX + 2, iconY + 2, fillWidth, iconHeight - 4);
|
||||
|
||||
// show muted icon if buzzer is muted
|
||||
// while charging, show a bolt (or a plug once full) just left of the battery,
|
||||
// keeping the fill bar itself clean and uninterrupted
|
||||
bool charging = board.isExternalPowered();
|
||||
if (charging) {
|
||||
// There's no charge-complete signal on most boards, so "full" is a high
|
||||
// voltage band rather than an exact 100% (a real pack rarely reads 4.2V).
|
||||
const int BATT_FULL_PCT = 95;
|
||||
const uint8_t* symbol = (batteryPercentage >= BATT_FULL_PCT) ? plug_icon : charging_icon;
|
||||
display.setColor(UIColor::title_txt);
|
||||
display.drawXbm(iconX - 9, iconY + 1, symbol, 8, 8);
|
||||
}
|
||||
|
||||
// show muted icon if buzzer is muted (shifted further left when the charging
|
||||
// icon already occupies the slot immediately left of the battery)
|
||||
#ifdef PIN_BUZZER
|
||||
if (_task->isBuzzerQuiet()) {
|
||||
display.setColor(UIColor::warning_txt);
|
||||
display.drawXbm(iconX - 9, iconY + 1, muted_icon, 8, 8);
|
||||
display.drawXbm(iconX - (charging ? 18 : 9), iconY + 1, muted_icon, 8, 8);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -225,6 +245,18 @@ public:
|
||||
sprintf(tmp, "MSG: %d", _task->getMsgCount());
|
||||
display.drawTextCentered(display.width() / 2, 22, tmp);
|
||||
|
||||
#ifdef UI_SHOW_CLOCK
|
||||
display.setTextSize(3);
|
||||
uint32_t now = _rtc->getCurrentTime();
|
||||
int8_t tz = UI_TZ_OFFSET; // for now draw time from Santo Domingo ...
|
||||
now += (int32_t)tz * 3600;
|
||||
DateTime dt (now);
|
||||
sprintf(tmp, "%02d:%02d", dt.hour(), dt.minute());
|
||||
display.drawTextCentered(display.width() / 2, 60, tmp);
|
||||
display.setTextSize(1);
|
||||
sprintf(tmp, "%02d/%02d/%d", dt.day(), dt.month(), dt.year());
|
||||
display.drawTextCentered(display.width() / 2, 80, tmp);
|
||||
#endif
|
||||
#ifdef WIFI_SSID
|
||||
IPAddress ip = WiFi.localIP();
|
||||
snprintf(tmp, sizeof(tmp), "IP: %d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]);
|
||||
@@ -234,13 +266,21 @@ public:
|
||||
if (_task->hasConnection()) {
|
||||
display.setColor(UIColor::warning_txt);
|
||||
display.setTextSize(1);
|
||||
#ifdef UI_SHOW_CLOCK
|
||||
display.drawTextCentered(display.width() / 2, 110, "< Connected >");
|
||||
#else
|
||||
display.drawTextCentered(display.width() / 2, 43, "< Connected >");
|
||||
|
||||
#endif
|
||||
} else if (the_mesh.getBLEPin() != 0) { // BT pin
|
||||
display.setColor(UIColor::warning_txt);
|
||||
display.setTextSize(2);
|
||||
sprintf(tmp, "Pin:%d", the_mesh.getBLEPin());
|
||||
#ifdef UI_SHOW_CLOCK
|
||||
display.setTextSize(1);
|
||||
display.drawTextCentered(display.width() / 2, 110, tmp);
|
||||
#else
|
||||
display.setTextSize(2);
|
||||
display.drawTextCentered(display.width() / 2, 43, tmp);
|
||||
#endif
|
||||
}
|
||||
} else if (_page == HomePage::RECENT) {
|
||||
the_mesh.getRecentlyHeard(recent, UI_RECENT_LIST_SIZE);
|
||||
@@ -421,6 +461,7 @@ public:
|
||||
if (sensors_scroll) sensors_scroll_offset = (sensors_scroll_offset+1)%sensors_nb;
|
||||
else sensors_scroll_offset = 0;
|
||||
#endif
|
||||
#ifndef UI_NO_HIBERNATE
|
||||
} else if (_page == HomePage::SHUTDOWN) {
|
||||
display.setColor(UIColor::corp_blue);
|
||||
display.setTextSize(1);
|
||||
@@ -432,6 +473,7 @@ public:
|
||||
display.drawXbm((display.width() - 32) / 2, 18, power_icon, 32, 32);
|
||||
display.drawTextCentered(display.width() / 2, 64 - 11, "hibernate:" PRESS_LABEL);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
return 5000; // next render after 5000 ms
|
||||
}
|
||||
@@ -478,14 +520,20 @@ public:
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
#ifndef UI_NO_HIBERNATE
|
||||
if (c == KEY_ENTER && _page == HomePage::SHUTDOWN) {
|
||||
_shutdown_init = true; // need to wait for button to be released
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
#ifndef UI_MSG_PREVIEW_SIZE
|
||||
#define UI_MSG_PREVIEW_SIZE 78
|
||||
#endif
|
||||
|
||||
class MsgPreviewScreen : public UIScreen {
|
||||
UITask* _task;
|
||||
mesh::RTCClock* _rtc;
|
||||
@@ -493,7 +541,7 @@ class MsgPreviewScreen : public UIScreen {
|
||||
struct MsgEntry {
|
||||
uint32_t timestamp;
|
||||
char origin[62];
|
||||
char msg[78];
|
||||
char msg[UI_MSG_PREVIEW_SIZE];
|
||||
};
|
||||
#define MAX_UNREAD_MSGS 32
|
||||
int num_unread;
|
||||
@@ -720,6 +768,9 @@ void UITask::shutdown(bool restart){
|
||||
if (restart) {
|
||||
_board->reboot();
|
||||
} else {
|
||||
display.forceFullRefresh();
|
||||
display.clear();
|
||||
display.endFrame();
|
||||
// Power off board including radio, display, GPS and components
|
||||
_board->powerOff();
|
||||
}
|
||||
@@ -760,6 +811,17 @@ void UITask::loop() {
|
||||
}
|
||||
#elif defined(PIN_USER_BTN)
|
||||
int ev = user_btn.check();
|
||||
#ifdef UI_HAS_NAV_INPUT
|
||||
if (ev == BUTTON_EVENT_CLICK) {
|
||||
c = checkDisplayOn(KEY_ENTER);
|
||||
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
|
||||
display.turnOff();
|
||||
} else if (ev == BUTTON_EVENT_DOUBLE_CLICK) {
|
||||
c = handleDoubleClick(KEY_SELECT);
|
||||
} else if (ev == BUTTON_EVENT_TRIPLE_CLICK) {
|
||||
c = handleTripleClick(KEY_SELECT);
|
||||
}
|
||||
#else
|
||||
if (ev == BUTTON_EVENT_CLICK) {
|
||||
c = checkDisplayOn(KEY_NEXT);
|
||||
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
|
||||
@@ -769,6 +831,7 @@ void UITask::loop() {
|
||||
} else if (ev == BUTTON_EVENT_TRIPLE_CLICK) {
|
||||
c = handleTripleClick(KEY_SELECT);
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
#if defined(UI_HAS_ROTARY_INPUT)
|
||||
RotaryInputEvent rotaryEv = rotary_input.poll();
|
||||
|
||||
@@ -119,4 +119,14 @@ static const uint8_t advert_icon[] = {
|
||||
|
||||
static const uint8_t muted_icon[] = {
|
||||
0x20, 0x6a, 0xea, 0xe4, 0xe4, 0xea, 0x6a, 0x20
|
||||
};
|
||||
|
||||
// small lightning bolt, 8x8px, shown next to the battery icon while charging
|
||||
static const uint8_t charging_icon[] = {
|
||||
0x18, 0x30, 0x60, 0xFC, 0x18, 0x30, 0x60, 0xC0
|
||||
};
|
||||
|
||||
// small power plug, 8x8px, shown next to the battery icon once fully charged
|
||||
static const uint8_t plug_icon[] = {
|
||||
0x24, 0x24, 0x7E, 0x7E, 0x7E, 0x3C, 0x18, 0x18
|
||||
};
|
||||
@@ -395,7 +395,7 @@ int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t
|
||||
int results_offset = 0;
|
||||
uint8_t results_buffer[130];
|
||||
for(int index = 0; index < count && index + offset < neighbours_count; index++){
|
||||
|
||||
|
||||
// stop if we can't fit another entry in results
|
||||
int entry_size = pubkey_prefix_length + 4 + 1;
|
||||
if(results_offset + entry_size > sizeof(results_buffer)){
|
||||
@@ -853,7 +853,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
|
||||
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
|
||||
|
||||
if (!(flags == TXT_TYPE_PLAIN || flags == TXT_TYPE_CLI_DATA)) {
|
||||
if (!(flags == TXT_TYPE_PLAIN || flags == TXT_TYPE_CLI_DATA || flags == TXT_TYPE_CLI_COMMAND)) {
|
||||
MESH_DEBUG_PRINTLN("onPeerDataRecv: unsupported text type received: flags=%02x", (uint32_t)flags);
|
||||
} else if (sender_timestamp >= client->last_timestamp) { // prevent replay attacks
|
||||
bool is_retry = (sender_timestamp == client->last_timestamp);
|
||||
@@ -1061,6 +1061,8 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
|
||||
#endif
|
||||
// Observer defaults (radio_watchdog, alert.*, snmp.*) moved to applyMQTTDefaults()
|
||||
// in MQTTDefaults.h — they live in /mqtt.json now, not NodePrefs.
|
||||
_prefs.cad_enabled = 0; // hardware CAD before TX (off by default; 'set cad on')
|
||||
_prefs.loop_detect = LOOP_DETECT_MINIMAL;
|
||||
|
||||
// bridge defaults
|
||||
_prefs.bridge_enabled = 1; // enabled
|
||||
@@ -1229,8 +1231,7 @@ void MyMesh::begin(FILESYSTEM *fs) {
|
||||
radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain);
|
||||
MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s",
|
||||
radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled");
|
||||
board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain); // LoRa FEM LNA (FEM boards only)
|
||||
board.setLoRaFemPaGainEnabled(_prefs.radio_fem_txgain);
|
||||
board.attachDynamicPrefs(_prefs.getCustom());
|
||||
|
||||
updateAdvertTimer();
|
||||
updateFloodAdvertTimer();
|
||||
@@ -1441,7 +1442,7 @@ void MyMesh::formatRadioDiagReply(char *reply) {
|
||||
}
|
||||
|
||||
void MyMesh::formatPacketStatsReply(char *reply) {
|
||||
StatsFormatHelper::formatPacketStats(reply, radio_driver, getNumSentFlood(), getNumSentDirect(),
|
||||
StatsFormatHelper::formatPacketStats(reply, radio_driver, getNumSentFlood(), getNumSentDirect(),
|
||||
getNumRecvFlood(), getNumRecvDirect());
|
||||
}
|
||||
|
||||
|
||||
@@ -72,10 +72,6 @@ struct RepeaterStats {
|
||||
uint32_t n_recv_errors;
|
||||
};
|
||||
|
||||
#ifndef MAX_CLIENTS
|
||||
#define MAX_CLIENTS 32
|
||||
#endif
|
||||
|
||||
struct NeighbourInfo {
|
||||
mesh::Identity id;
|
||||
uint32_t advert_timestamp;
|
||||
|
||||
@@ -549,7 +549,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
|
||||
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
|
||||
|
||||
if (!(flags == TXT_TYPE_PLAIN || flags == TXT_TYPE_CLI_DATA)) {
|
||||
if (!(flags == TXT_TYPE_PLAIN || flags == TXT_TYPE_CLI_DATA || flags == TXT_TYPE_CLI_COMMAND)) {
|
||||
MESH_DEBUG_PRINTLN("onPeerDataRecv: unsupported command flags received: flags=%02x", (uint32_t)flags);
|
||||
} else if (sender_timestamp >= client->last_timestamp) { // prevent replay attacks, but send Acks for retries
|
||||
bool is_retry = (sender_timestamp == client->last_timestamp);
|
||||
@@ -569,7 +569,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
|
||||
|
||||
uint8_t temp[166];
|
||||
bool send_ack;
|
||||
if (flags == TXT_TYPE_CLI_DATA) {
|
||||
if (flags == TXT_TYPE_CLI_DATA || flags == TXT_TYPE_CLI_COMMAND) {
|
||||
if (client->isAdmin()) {
|
||||
if (is_retry) {
|
||||
temp[5] = 0; // no reply
|
||||
@@ -969,8 +969,7 @@ void MyMesh::begin(FILESYSTEM *fs) {
|
||||
radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
|
||||
radio_driver.setTxPower(_prefs.tx_power_dbm);
|
||||
radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain);
|
||||
board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain); // LoRa FEM LNA (FEM boards only)
|
||||
board.setLoRaFemPaGainEnabled(_prefs.radio_fem_txgain);
|
||||
board.attachDynamicPrefs(_prefs.getCustom());
|
||||
|
||||
updateAdvertTimer();
|
||||
updateFloodAdvertTimer();
|
||||
|
||||
@@ -242,6 +242,10 @@ protected:
|
||||
|
||||
void onCommandDataRecv(const ContactInfo& from, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text) override {
|
||||
}
|
||||
|
||||
void onCLICommandRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text, char* reply) override {
|
||||
}
|
||||
|
||||
void onSignedMessageRecv(const ContactInfo& from, mesh::Packet* pkt, uint32_t sender_timestamp, const uint8_t *sender_prefix, const char *text) override {
|
||||
}
|
||||
|
||||
|
||||
@@ -577,7 +577,7 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i
|
||||
sendAckTo(*from, ack_hash, packet->getPathHashSize());
|
||||
}
|
||||
}
|
||||
} else if (flags == TXT_TYPE_CLI_DATA) {
|
||||
} else if (flags == TXT_TYPE_CLI_DATA || flags == TXT_TYPE_CLI_COMMAND) {
|
||||
from->last_timestamp = sender_timestamp;
|
||||
from->last_activity = getRTCClock()->getCurrentTime();
|
||||
|
||||
@@ -773,8 +773,7 @@ void SensorMesh::begin(FILESYSTEM* fs) {
|
||||
|
||||
radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
|
||||
radio_driver.setTxPower(_prefs.tx_power_dbm);
|
||||
board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain); // LoRa FEM LNA (FEM boards only)
|
||||
board.setLoRaFemPaGainEnabled(_prefs.radio_fem_txgain);
|
||||
board.attachDynamicPrefs(_prefs.getCustom());
|
||||
|
||||
updateAdvertTimer();
|
||||
updateFloodAdvertTimer();
|
||||
|
||||
@@ -97,6 +97,7 @@ platform_packages =
|
||||
; https://github.com/meshcore-dev/MeshCore/pull/1177
|
||||
; https://github.com/meshcore-dev/MeshCore/pull/1295
|
||||
framework-arduinoadafruitnrf52 @ https://github.com/meshcore-dev/Adafruit_nRF52_Arduino#d541301
|
||||
platformio/toolchain-gccarmnoneeabi@^1.140201.0
|
||||
extra_scripts = create-uf2.py
|
||||
build_flags = ${arduino_base.build_flags}
|
||||
-D NRF52_PLATFORM
|
||||
@@ -186,6 +187,8 @@ build_src_filter =
|
||||
+<../src/helpers/MQTTPayloadBuilder.cpp>
|
||||
+<../src/Packet.cpp>
|
||||
+<../src/helpers/ConfigSerializer.cpp>
|
||||
+<../src/helpers/DynamicConfigSerializer.cpp>
|
||||
+<../test/mocks/CommonRadioPrefs.cpp>
|
||||
lib_deps =
|
||||
google/googletest @ 1.17.0
|
||||
bblanchon/ArduinoJson @ 7.4.3
|
||||
|
||||
+8
-11
@@ -63,22 +63,17 @@ public:
|
||||
virtual void setGpio(uint32_t values) {}
|
||||
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[], bool force_ap = false) { return false; } // not supported
|
||||
// Retain the legacy two-argument hook for RP2040 boards while allowing
|
||||
// observer-capable boards to request a forced access point.
|
||||
virtual bool startOTAUpdate(const char* id, char reply[]) { return false; } // not supported
|
||||
virtual bool startOTAUpdate(const char* id, char reply[], bool force_ap) {
|
||||
return startOTAUpdate(id, reply);
|
||||
}
|
||||
// Pull-based OTA: fetch the firmware build for this variant from a baked-in manifest and flash it.
|
||||
// current_ver is the running firmware version string (used to skip if already up to date); when
|
||||
// dry_run is true the build is only reported, not flashed. Observer (ESP32+WiFi) builds only.
|
||||
virtual bool otaFromManifest(const char* current_ver, bool dry_run, char reply[]) { return false; }
|
||||
|
||||
// LoRa front-end-module LNA (RX gain) control. Only FEM-equipped boards override
|
||||
// these; others report they can't control it. Driven by NodePrefs.radio_fem_rxgain.
|
||||
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 isExternalPowered() { return false; }
|
||||
virtual uint16_t getBootVoltage() { return 0; }
|
||||
@@ -86,6 +81,8 @@ public:
|
||||
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; }
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
@@ -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;
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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(×tamp, 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, ×tamp, 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, ×tamp, 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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -34,7 +34,7 @@ struct ClientInfo {
|
||||
};
|
||||
|
||||
#ifndef MAX_CLIENTS
|
||||
#define MAX_CLIENTS 20
|
||||
#define MAX_CLIENTS 32
|
||||
#endif
|
||||
|
||||
class ClientACL {
|
||||
|
||||
+12
-221
@@ -1202,6 +1202,7 @@ void CommonCLI::savePrefs() {
|
||||
_callbacks->updateAdvertTimer();
|
||||
}
|
||||
_callbacks->savePrefs();
|
||||
_prefs->clearDirty();
|
||||
}
|
||||
|
||||
uint8_t CommonCLI::buildAdvertData(uint8_t node_type, uint8_t* app_data) {
|
||||
@@ -1221,6 +1222,15 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
|
||||
// Observer-only top-level commands (ota check/update, tls.bundletest, alert test)
|
||||
// live in CommonCLI_Observer.cpp.
|
||||
if (handleObserverCommand(sender_timestamp, command, reply)) return;
|
||||
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) {
|
||||
@@ -1517,61 +1527,7 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
const char* config = &command[4];
|
||||
// Observer/MQTT/WiFi/timezone/alert/SNMP commands live in CommonCLI_Observer.cpp.
|
||||
if (handleObserverSetCmd(sender_timestamp, config, reply)) return;
|
||||
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, "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, "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");
|
||||
@@ -1624,77 +1580,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();
|
||||
@@ -1703,24 +1588,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) {
|
||||
@@ -1748,15 +1615,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;
|
||||
@@ -1767,16 +1625,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;
|
||||
@@ -1797,11 +1645,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();
|
||||
@@ -1926,28 +1769,7 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
const char* config = &command[4];
|
||||
// Observer/MQTT/WiFi/timezone/alert/SNMP commands live in CommonCLI_Observer.cpp.
|
||||
if (handleObserverGetCmd(sender_timestamp, config, reply)) return;
|
||||
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, "radio.fem.rxgain", 16) == 0) {
|
||||
if (!_board->canControlLoRaFemLna()) {
|
||||
strcpy(reply, "Error: unsupported");
|
||||
} else {
|
||||
sprintf(reply, "> %s", _board->isLoRaFemLnaEnabled() ? "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));
|
||||
@@ -1968,37 +1790,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) {
|
||||
*reply++ = '>';
|
||||
*reply++ = ' ';
|
||||
@@ -2008,8 +1805,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");
|
||||
@@ -2020,10 +1815,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
@@ -7,6 +7,8 @@
|
||||
#include <helpers/MQTTPresets.h> // For MAX_MQTT_SLOTS (used in NodePrefs struct layout)
|
||||
#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) || defined(WITH_MQTT_BRIDGE)
|
||||
#define WITH_BRIDGE
|
||||
@@ -77,7 +79,7 @@ public:
|
||||
// stays aligned with upstream. See struct MQTTPrefs below.
|
||||
|
||||
private:
|
||||
class RadioPrefs : public ConfigSerializer {
|
||||
class RadioPrefs : public CommonRadioPrefs {
|
||||
NodePrefs* _parent;
|
||||
protected:
|
||||
void structure() override {
|
||||
@@ -101,6 +103,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(); }
|
||||
int8_t getTxPower() const override { return _parent->tx_power_dbm; }
|
||||
void setTxPower(int8_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(); }
|
||||
uint8_t getAgcResetInt() const override { return _parent->agc_reset_interval * 4; }
|
||||
void setAgcResetInt(uint8_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;
|
||||
|
||||
@@ -171,6 +208,8 @@ private:
|
||||
};
|
||||
RoomPrefs room;
|
||||
|
||||
DynamicConfigSerializer custom;
|
||||
|
||||
protected:
|
||||
void structure() override {
|
||||
def("name", node_name, sizeof(node_name));
|
||||
@@ -188,16 +227,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(); }
|
||||
};
|
||||
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
#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) {
|
||||
setAirtimeFactor(atof(&command[7]));
|
||||
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) {
|
||||
setAgcResetInt(atoi(&command[23]));
|
||||
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
|
||||
}
|
||||
@@ -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() override { _is_dirty = false; }
|
||||
bool isDirty() const override { 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 int8_t getTxPower() const = 0;
|
||||
virtual void setTxPower(int8_t dbm) = 0;
|
||||
|
||||
virtual float getRxDelay() const = 0;
|
||||
virtual void setRxDelay(float d) = 0;
|
||||
|
||||
virtual uint8_t getAgcResetInt() const = 0;
|
||||
virtual void setAgcResetInt(uint8_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;
|
||||
};
|
||||
@@ -44,6 +44,7 @@ static bool is_value_char(char c) {
|
||||
#define EXPECT_COMMA_OR_CLOSE 5
|
||||
#define EXPECT_COMMA_OR_KEY 6
|
||||
#define EXPECT_COMMA_OR_KEY_OR_CLOSE 7
|
||||
#define EXPECT_KEY_OR_CLOSE 8
|
||||
|
||||
int ConfigSerializer::Context::readNext() {
|
||||
char c;
|
||||
@@ -60,15 +61,21 @@ int ConfigSerializer::Context::readNext() {
|
||||
|
||||
switch (rd_mode) {
|
||||
case EXPECT_OPEN_BRACE:
|
||||
if (c == '{') { rd_mode = EXPECT_KEY; return TOK_START_OBJ; }
|
||||
if (c == '{') { rd_mode = EXPECT_KEY_OR_CLOSE; return TOK_START_OBJ; }
|
||||
if (is_whitespace(c)) return TOK_WHITESPACE;
|
||||
return TOK_ERROR;
|
||||
|
||||
case EXPECT_KEY_OR_CLOSE:
|
||||
if (c == '}') { rd_mode = EXPECT_COMMA_OR_KEY_OR_CLOSE; return TOK_END_OBJ; }
|
||||
rd_mode = EXPECT_KEY;
|
||||
goto read_key; // a non-empty object must start with a key
|
||||
|
||||
case EXPECT_COMMA_OR_KEY_OR_CLOSE:
|
||||
if (c == '}') { rd_mode = EXPECT_COMMA_OR_KEY_OR_CLOSE; return TOK_END_OBJ; }
|
||||
case EXPECT_COMMA_OR_KEY:
|
||||
if (c == ',') { rd_mode = EXPECT_KEY; return TOK_WHITESPACE; }
|
||||
case EXPECT_KEY:
|
||||
read_key:
|
||||
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 &&
|
||||
@@ -85,7 +92,7 @@ int ConfigSerializer::Context::readNext() {
|
||||
rd_mode = EXPECT_STRING_VAL;
|
||||
return TOK_WHITESPACE;
|
||||
}
|
||||
if (rd_len == 0 && c == '{') { rd_mode = EXPECT_KEY; return TOK_START_OBJ; }
|
||||
if (rd_len == 0 && c == '{') { rd_mode = EXPECT_KEY_OR_CLOSE; return TOK_START_OBJ; }
|
||||
if (is_value_char(c) && rd_len < CONFIG_MAX_TOKEN_LEN-1) {
|
||||
if (rd_len == 0) rd_token_quoted = false;
|
||||
rd_buf[rd_len++] = c;
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
class ConfigSerializer {
|
||||
bool _first;
|
||||
int8_t _depth;
|
||||
bool _dirty = false;
|
||||
|
||||
protected:
|
||||
enum OP { READ, WRITE };
|
||||
|
||||
class Context {
|
||||
@@ -56,13 +58,14 @@ class ConfigSerializer {
|
||||
}
|
||||
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
|
||||
@@ -141,7 +144,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; }
|
||||
};
|
||||
|
||||
@@ -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];
|
||||
};
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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; }
|
||||
};
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <MeshCore.h>
|
||||
#include <helpers/KeyValueStore.h>
|
||||
|
||||
#if defined(NRF52_PLATFORM)
|
||||
|
||||
@@ -57,6 +58,8 @@ 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; }
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -48,6 +48,9 @@ uint32_t RadioLibWrapper::getRngSeed() {
|
||||
}
|
||||
|
||||
void RadioLibWrapper::setTxPower(int8_t dbm) {
|
||||
#if defined(USE_LR2021)
|
||||
idle();
|
||||
#endif
|
||||
_radio->setOutputPower(dbm);
|
||||
}
|
||||
|
||||
@@ -260,4 +263,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};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -831,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
|
||||
@@ -871,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;
|
||||
}
|
||||
@@ -880,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
|
||||
|
||||
@@ -896,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
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;}
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
#include "helpers/CommonRadioPrefs.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
// Native serializer tests instantiate NodePrefs, but the full radio CLI source
|
||||
// depends on target hardware. These two methods are the only out-of-line vtable
|
||||
// entries the host-side NodePrefs tests require.
|
||||
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;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include "helpers/ConfigSerializer.h"
|
||||
#include "helpers/DynamicConfigSerializer.h"
|
||||
|
||||
class NativeFileSystem {
|
||||
public:
|
||||
@@ -192,6 +193,23 @@ TEST(ConfigSerializer, LoadSerial_IgnoreUnknowns) {
|
||||
EXPECT_TRUE(match);
|
||||
}
|
||||
|
||||
TEST(DynamicConfigSerializer, GetSet_Basic) {
|
||||
DynamicConfigSerializer data;
|
||||
|
||||
bool s1 = data.setByKey("age", "11");
|
||||
bool s2 = data.setByKey("name", "Scott");
|
||||
EXPECT_TRUE(s1 && s2);
|
||||
|
||||
char tmp[32];
|
||||
bool g1 = data.getByKey("age", tmp, 31);
|
||||
EXPECT_TRUE(g1);
|
||||
EXPECT_STREQ("11", tmp);
|
||||
|
||||
bool g2 = data.getByKey("name", tmp, 31);
|
||||
EXPECT_TRUE(g2);
|
||||
EXPECT_STREQ("Scott", tmp);
|
||||
}
|
||||
|
||||
TEST(NodePrefs, FemGainSettingsRoundTrip) {
|
||||
NodePrefs saved;
|
||||
saved.radio_fem_rxgain = 0;
|
||||
@@ -209,11 +227,20 @@ TEST(NodePrefs, FemGainSettingsRoundTrip) {
|
||||
loaded.radio_fem_rxgain = 1;
|
||||
loaded.radio_fem_txgain = 0;
|
||||
|
||||
ASSERT_TRUE(loaded.loadSerial(input));
|
||||
ASSERT_TRUE(loaded.loadSerial(input)) << serialised;
|
||||
EXPECT_EQ(0, loaded.radio_fem_rxgain);
|
||||
EXPECT_EQ(1, loaded.radio_fem_txgain);
|
||||
}
|
||||
|
||||
TEST(NodePrefs, TxPowerRemainsSignedThroughRadioPrefs) {
|
||||
NodePrefs prefs;
|
||||
prefs.tx_power_dbm = -9;
|
||||
|
||||
EXPECT_EQ(-9, prefs.getRadioPrefs()->getTxPower());
|
||||
prefs.getRadioPrefs()->setTxPower(-8);
|
||||
EXPECT_EQ(-8, prefs.tx_power_dbm);
|
||||
}
|
||||
|
||||
TEST(ConfigSerializer, LoadSerial_KeyDigitsAfterFirstCharacter) {
|
||||
MockInputStream s("{age:1,flags:2,name:\"ok\",slot1:7}");
|
||||
TestStruct data;
|
||||
@@ -231,6 +258,12 @@ TEST(ConfigSerializer, LoadSerial_RejectsLeadingDigitKey) {
|
||||
EXPECT_FALSE(data.loadSerial(s));
|
||||
}
|
||||
|
||||
TEST(ConfigSerializer, LoadSerial_AcceptsEmptyObject) {
|
||||
MockInputStream s("{}");
|
||||
TestStruct data;
|
||||
EXPECT_TRUE(data.loadSerial(s));
|
||||
}
|
||||
|
||||
// ── /prefs.json compatibility under the strict shape checks ─────────────────
|
||||
//
|
||||
// The scalar-vs-object rejection added for /mqtt.json also runs for the plain
|
||||
@@ -326,6 +359,73 @@ TEST(NodePrefs, ShapeMismatchIsRejectedAndStopsFurtherApplication) {
|
||||
EXPECT_EQ(9, nested.sf);
|
||||
}
|
||||
|
||||
TEST(DynamicConfigSerializer, Set_Replaces) {
|
||||
DynamicConfigSerializer data;
|
||||
|
||||
bool s1 = data.setByKey("age", "11");
|
||||
bool s2 = data.setByKey("name", "Scott");
|
||||
EXPECT_TRUE(s1 && s2);
|
||||
|
||||
bool s3 = data.setByKey("age", "333");
|
||||
EXPECT_TRUE(s3);
|
||||
|
||||
char tmp[32];
|
||||
bool g1 = data.getByKey("age", tmp, 31);
|
||||
EXPECT_TRUE(g1);
|
||||
EXPECT_STREQ("333", tmp);
|
||||
|
||||
bool g2 = data.getByKey("name", tmp, 31);
|
||||
EXPECT_TRUE(g2);
|
||||
EXPECT_STREQ("Scott", tmp);
|
||||
}
|
||||
|
||||
TEST(DynamicConfigSerializer, GetUnknown_Fail) {
|
||||
DynamicConfigSerializer data;
|
||||
|
||||
bool s1 = data.setByKey("age", "11");
|
||||
EXPECT_TRUE(s1);
|
||||
|
||||
char tmp[32];
|
||||
bool g2 = data.getByKey("name", tmp, 31);
|
||||
EXPECT_FALSE(g2);
|
||||
}
|
||||
|
||||
TEST(DynamicConfigSerializer, SaveCustom_Basic) {
|
||||
MockPrintStream s;
|
||||
DynamicConfigSerializer data;
|
||||
|
||||
bool s1 = data.setByKey("age", "11");
|
||||
bool s2 = data.setByKey("name", "Scott");
|
||||
EXPECT_TRUE(s1 && s2);
|
||||
|
||||
bool success = data.saveSerial(s);
|
||||
EXPECT_TRUE(success);
|
||||
|
||||
auto l = s.getLength();
|
||||
char tmp[128];
|
||||
memcpy(tmp, s.getBytes(), l);
|
||||
tmp[l] = 0;
|
||||
|
||||
const char* expect = "{age:\"11\",name:\"Scott\"}";
|
||||
EXPECT_STREQ(expect, tmp);
|
||||
}
|
||||
|
||||
TEST(DynamicConfigSerializer, LoadCustom_Basic) {
|
||||
MockInputStream s("{age:\"" TEST_INT_S "\",name:\"Scott\"}");
|
||||
DynamicConfigSerializer data;
|
||||
|
||||
bool success = data.loadSerial(s);
|
||||
EXPECT_TRUE(success);
|
||||
|
||||
char tmp[32];
|
||||
bool g1 = data.getByKey("age", tmp, 31);
|
||||
EXPECT_TRUE(g1);
|
||||
EXPECT_STREQ(TEST_INT_S, tmp);
|
||||
|
||||
bool g2 = data.getByKey("name", tmp, 31);
|
||||
EXPECT_TRUE(g2);
|
||||
EXPECT_STREQ("Scott", tmp);
|
||||
}
|
||||
|
||||
// ── main ───────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <string.h>
|
||||
#include "MeshCore.h"
|
||||
|
||||
class LegacyOTABoard : public mesh::MainBoard {
|
||||
public:
|
||||
bool called = false;
|
||||
|
||||
uint16_t getBattMilliVolts() override { return 0; }
|
||||
const char* getManufacturerName() const override { return "test"; }
|
||||
void reboot() override { }
|
||||
uint8_t getStartupReason() const override { return BD_STARTUP_NORMAL; }
|
||||
|
||||
bool startOTAUpdate(const char* id, char reply[]) override {
|
||||
called = true;
|
||||
strcpy(reply, id);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
TEST(MainBoardAPI, ThreeArgumentOtaFallsBackToLegacyOverride) {
|
||||
LegacyOTABoard board;
|
||||
mesh::MainBoard* base = &board;
|
||||
char reply[16] = { 0 };
|
||||
|
||||
EXPECT_TRUE(base->startOTAUpdate("legacy", reply, true));
|
||||
EXPECT_TRUE(board.called);
|
||||
EXPECT_STREQ("legacy", reply);
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
@@ -131,10 +131,50 @@ bool T096Board::setLoRaFemLnaEnabled(bool enable) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool T096Board::canControlLoRaFemLna() const {
|
||||
return loRaFEMControl.isLnaCanControl();
|
||||
}
|
||||
|
||||
bool T096Board::isLoRaFemLnaEnabled() const {
|
||||
return loRaFEMControl.isLNAEnabled();
|
||||
}
|
||||
|
||||
void T096Board::attachDynamicPrefs(KeyValueStore* prefs) {
|
||||
_prefs = prefs;
|
||||
|
||||
char radio_fem_rxgain[8] = { 0 };
|
||||
_prefs->getByKey("fem_rxgain", radio_fem_rxgain, 7); // get initial values
|
||||
|
||||
setLoRaFemLnaEnabled(strcmp(radio_fem_rxgain, "1") == 0);
|
||||
}
|
||||
|
||||
bool T096Board::handleCommand(const char* command, uint32_t sender_timestamp, char* reply) {
|
||||
if (strcmp(command, "get radio.fem.rxgain") == 0) {
|
||||
if (!loRaFEMControl.isLnaCanControl()) {
|
||||
strcpy(reply, "Error: unsupported");
|
||||
} else {
|
||||
sprintf(reply, "> %s", isLoRaFemLnaEnabled() ? "on" : "off");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (memcmp(command, "set radio.fem.rxgain ", 21) == 0) {
|
||||
if (!loRaFEMControl.isLnaCanControl()) {
|
||||
strcpy(reply, "Error: unsupported");
|
||||
} else if (memcmp(&command[21], "on", 2) == 0) {
|
||||
if (setLoRaFemLnaEnabled(true)) {
|
||||
_prefs->setByKey("fem_rxgain", "1");
|
||||
strcpy(reply, "OK - LoRa FEM RX gain on");
|
||||
} else {
|
||||
strcpy(reply, "Error: failed to apply LoRa FEM RX gain");
|
||||
}
|
||||
} else if (memcmp(&command[21], "off", 3) == 0) {
|
||||
if (setLoRaFemLnaEnabled(false)) {
|
||||
_prefs->setByKey("fem_rxgain", "0");
|
||||
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");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false; // not handled
|
||||
}
|
||||
|
||||
@@ -4,28 +4,33 @@
|
||||
#include <Arduino.h>
|
||||
#include <helpers/NRF52Board.h>
|
||||
#include <helpers/RefCountedDigitalPin.h>
|
||||
#include <helpers/KeyValueStore.h>
|
||||
#include "LoRaFEMControl.h"
|
||||
|
||||
class T096Board : public NRF52BoardDCDC {
|
||||
KeyValueStore* _prefs = NULL;
|
||||
|
||||
protected:
|
||||
#ifdef NRF52_POWER_MANAGEMENT
|
||||
void initiateShutdown(uint8_t reason) override;
|
||||
#endif
|
||||
void variant_shutdown();
|
||||
|
||||
bool setLoRaFemLnaEnabled(bool enable);
|
||||
bool isLoRaFemLnaEnabled() const;
|
||||
|
||||
public:
|
||||
RefCountedDigitalPin periph_power;
|
||||
LoRaFEMControl loRaFEMControl;
|
||||
|
||||
T096Board() :periph_power(PIN_VEXT_EN,PIN_VEXT_EN_ACTIVE), NRF52Board("T096_OTA") {}
|
||||
void begin();
|
||||
void attachDynamicPrefs(KeyValueStore* prefs);
|
||||
bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply) override;
|
||||
|
||||
void onBeforeTransmit(void) override;
|
||||
void onAfterTransmit(void) override;
|
||||
uint16_t getBattMilliVolts() override;
|
||||
const char* getManufacturerName() const override ;
|
||||
void powerOff() override;
|
||||
bool setLoRaFemLnaEnabled(bool enable) override;
|
||||
bool canControlLoRaFemLna() const override;
|
||||
bool isLoRaFemLnaEnabled() const override;
|
||||
};
|
||||
|
||||
@@ -84,10 +84,50 @@ void HeltecTrackerV2Board::begin() {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HeltecTrackerV2Board::canControlLoRaFemLna() const {
|
||||
return loRaFEMControl.isLnaCanControl();
|
||||
}
|
||||
|
||||
bool HeltecTrackerV2Board::isLoRaFemLnaEnabled() const {
|
||||
return loRaFEMControl.isLNAEnabled();
|
||||
}
|
||||
|
||||
void HeltecTrackerV2Board::attachDynamicPrefs(KeyValueStore* prefs) {
|
||||
_prefs = prefs;
|
||||
|
||||
char radio_fem_rxgain[8] = { 0 };
|
||||
_prefs->getByKey("fem_rxgain", radio_fem_rxgain, 7); // get initial values
|
||||
|
||||
setLoRaFemLnaEnabled(strcmp(radio_fem_rxgain, "1") == 0);
|
||||
}
|
||||
|
||||
bool HeltecTrackerV2Board::handleCommand(const char* command, uint32_t sender_timestamp, char* reply) {
|
||||
if (strcmp(command, "get radio.fem.rxgain") == 0) {
|
||||
if (!loRaFEMControl.isLnaCanControl()) {
|
||||
strcpy(reply, "Error: unsupported");
|
||||
} else {
|
||||
sprintf(reply, "> %s", isLoRaFemLnaEnabled() ? "on" : "off");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (memcmp(command, "set radio.fem.rxgain ", 21) == 0) {
|
||||
if (!loRaFEMControl.isLnaCanControl()) {
|
||||
strcpy(reply, "Error: unsupported");
|
||||
} else if (memcmp(&command[21], "on", 2) == 0) {
|
||||
if (setLoRaFemLnaEnabled(true)) {
|
||||
_prefs->setByKey("fem_rxgain", "1");
|
||||
strcpy(reply, "OK - LoRa FEM RX gain on");
|
||||
} else {
|
||||
strcpy(reply, "Error: failed to apply LoRa FEM RX gain");
|
||||
}
|
||||
} else if (memcmp(&command[21], "off", 3) == 0) {
|
||||
if (setLoRaFemLnaEnabled(false)) {
|
||||
_prefs->setByKey("fem_rxgain", "0");
|
||||
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");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false; // not handled
|
||||
}
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
#include "LoRaFEMControl.h"
|
||||
|
||||
class HeltecTrackerV2Board : public ESP32Board {
|
||||
KeyValueStore* _prefs = NULL;
|
||||
|
||||
bool setLoRaFemLnaEnabled(bool enable);
|
||||
bool isLoRaFemLnaEnabled() const;
|
||||
|
||||
public:
|
||||
RefCountedDigitalPin periph_power;
|
||||
@@ -14,13 +18,13 @@ public:
|
||||
HeltecTrackerV2Board() : periph_power(PIN_VEXT_EN,PIN_VEXT_EN_ACTIVE) { }
|
||||
|
||||
void begin();
|
||||
void attachDynamicPrefs(KeyValueStore* prefs);
|
||||
bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply) override;
|
||||
|
||||
void onBeforeTransmit(void) override;
|
||||
void onAfterTransmit(void) override;
|
||||
void powerOff() override;
|
||||
uint16_t getBattMilliVolts() override;
|
||||
const char* getManufacturerName() const override ;
|
||||
bool setLoRaFemLnaEnabled(bool enable) override;
|
||||
bool canControlLoRaFemLna() const override;
|
||||
bool isLoRaFemLnaEnabled() const override;
|
||||
|
||||
};
|
||||
|
||||
@@ -73,10 +73,50 @@ void HeltecV4Board::begin() {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HeltecV4Board::canControlLoRaFemLna() const {
|
||||
return loRaFEMControl.isLnaCanControl();
|
||||
}
|
||||
|
||||
bool HeltecV4Board::isLoRaFemLnaEnabled() const {
|
||||
return loRaFEMControl.isLNAEnabled();
|
||||
}
|
||||
|
||||
void HeltecV4Board::attachDynamicPrefs(KeyValueStore* prefs) {
|
||||
_prefs = prefs;
|
||||
|
||||
char radio_fem_rxgain[8] = { 0 };
|
||||
_prefs->getByKey("fem_rxgain", radio_fem_rxgain, 7); // get initial values
|
||||
|
||||
setLoRaFemLnaEnabled(strcmp(radio_fem_rxgain, "1") == 0);
|
||||
}
|
||||
|
||||
bool HeltecV4Board::handleCommand(const char* command, uint32_t sender_timestamp, char* reply) {
|
||||
if (strcmp(command, "get radio.fem.rxgain") == 0) {
|
||||
if (!loRaFEMControl.isLnaCanControl()) {
|
||||
strcpy(reply, "Error: unsupported");
|
||||
} else {
|
||||
sprintf(reply, "> %s", isLoRaFemLnaEnabled() ? "on" : "off");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (memcmp(command, "set radio.fem.rxgain ", 21) == 0) {
|
||||
if (!loRaFEMControl.isLnaCanControl()) {
|
||||
strcpy(reply, "Error: unsupported");
|
||||
} else if (memcmp(&command[21], "on", 2) == 0) {
|
||||
if (setLoRaFemLnaEnabled(true)) {
|
||||
_prefs->setByKey("fem_rxgain", "1");
|
||||
strcpy(reply, "OK - LoRa FEM RX gain on");
|
||||
} else {
|
||||
strcpy(reply, "Error: failed to apply LoRa FEM RX gain");
|
||||
}
|
||||
} else if (memcmp(&command[21], "off", 3) == 0) {
|
||||
if (setLoRaFemLnaEnabled(false)) {
|
||||
_prefs->setByKey("fem_rxgain", "0");
|
||||
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");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false; // not handled
|
||||
}
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
#endif
|
||||
|
||||
class HeltecV4Board : public ESP32Board {
|
||||
KeyValueStore* _prefs = NULL;
|
||||
|
||||
bool setLoRaFemLnaEnabled(bool enable);
|
||||
bool isLoRaFemLnaEnabled() const;
|
||||
|
||||
protected:
|
||||
float adc_mult = ADC_MULTIPLIER;
|
||||
@@ -20,12 +24,12 @@ public:
|
||||
HeltecV4Board() : periph_power(PIN_VEXT_EN,PIN_VEXT_EN_ACTIVE) { }
|
||||
|
||||
void begin();
|
||||
void attachDynamicPrefs(KeyValueStore* prefs);
|
||||
bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply) override;
|
||||
|
||||
void onBeforeTransmit(void) override;
|
||||
void onAfterTransmit(void) override;
|
||||
void powerOff() override;
|
||||
bool setLoRaFemLnaEnabled(bool enable) override;
|
||||
bool canControlLoRaFemLna() const override;
|
||||
bool isLoRaFemLnaEnabled() const override;
|
||||
uint16_t getBattMilliVolts() override;
|
||||
bool setAdcMultiplier(float multiplier) override {
|
||||
if (multiplier == 0.0f) {
|
||||
|
||||
@@ -116,10 +116,50 @@ bool HeltecV4R8Board::setLoRaFemLnaEnabled(bool enable) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HeltecV4R8Board::canControlLoRaFemLna() const {
|
||||
return loRaFEMControl.isLnaCanControl();
|
||||
}
|
||||
|
||||
bool HeltecV4R8Board::isLoRaFemLnaEnabled() const {
|
||||
return loRaFEMControl.isLNAEnabled();
|
||||
}
|
||||
|
||||
void HeltecV4R8Board::attachDynamicPrefs(KeyValueStore* prefs) {
|
||||
_prefs = prefs;
|
||||
|
||||
char gain[8] = { 0 };
|
||||
_prefs->getByKey("fem_rxgain", gain, sizeof(gain));
|
||||
setLoRaFemLnaEnabled(strcmp(gain, "1") == 0);
|
||||
}
|
||||
|
||||
bool HeltecV4R8Board::handleCommand(const char* command, uint32_t sender_timestamp, char* reply) {
|
||||
if (strcmp(command, "get radio.fem.rxgain") == 0) {
|
||||
if (!loRaFEMControl.isLnaCanControl()) {
|
||||
strcpy(reply, "Error: unsupported");
|
||||
} else {
|
||||
sprintf(reply, "> %s", isLoRaFemLnaEnabled() ? "on" : "off");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (memcmp(command, "set radio.fem.rxgain ", 21) == 0) {
|
||||
if (!loRaFEMControl.isLnaCanControl()) {
|
||||
strcpy(reply, "Error: unsupported");
|
||||
} else if (memcmp(&command[21], "on", 2) == 0) {
|
||||
if (setLoRaFemLnaEnabled(true)) {
|
||||
_prefs->setByKey("fem_rxgain", "1");
|
||||
strcpy(reply, "OK - LoRa FEM RX gain on");
|
||||
} else {
|
||||
strcpy(reply, "Error: failed to apply LoRa FEM RX gain");
|
||||
}
|
||||
} else if (memcmp(&command[21], "off", 3) == 0) {
|
||||
if (setLoRaFemLnaEnabled(false)) {
|
||||
_prefs->setByKey("fem_rxgain", "0");
|
||||
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");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,11 @@
|
||||
#endif
|
||||
|
||||
class HeltecV4R8Board : public ESP32Board {
|
||||
KeyValueStore* _prefs = NULL;
|
||||
|
||||
bool setLoRaFemLnaEnabled(bool enable);
|
||||
bool isLoRaFemLnaEnabled() const;
|
||||
|
||||
protected:
|
||||
float adc_mult = ADC_MULTIPLIER;
|
||||
|
||||
@@ -21,13 +26,12 @@ public:
|
||||
HeltecV4R8Board() : periph_power(PIN_VEXT_EN, PIN_VEXT_EN_ACTIVE) { }
|
||||
|
||||
void begin();
|
||||
void attachDynamicPrefs(KeyValueStore* prefs);
|
||||
bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply) override;
|
||||
void onBeforeTransmit(void) override;
|
||||
void onAfterTransmit(void) override;
|
||||
void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1);
|
||||
void powerOff() override;
|
||||
bool setLoRaFemLnaEnabled(bool enable) override;
|
||||
bool canControlLoRaFemLna() const override;
|
||||
bool isLoRaFemLnaEnabled() const override;
|
||||
uint16_t getBattMilliVolts() override;
|
||||
bool setAdcMultiplier(float multiplier) override {
|
||||
if (multiplier == 0.0f) {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
#define PIN_VBAT_READ 35
|
||||
|
||||
#include "ESP32Board.h"
|
||||
#include "helpers/ESP32Board.h"
|
||||
|
||||
class MeshadventurerBoard : public ESP32Board {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#define RADIOLIB_STATIC_ONLY 1
|
||||
#include <RadioLib.h>
|
||||
#include <helpers/radiolib/RadioLibWrappers.h>
|
||||
#include <helpers/MeshadventurerBoard.h>
|
||||
#include <MeshadventurerBoard.h>
|
||||
#include <helpers/radiolib/CustomSX1262Wrapper.h>
|
||||
#include <helpers/radiolib/CustomSX1268Wrapper.h>
|
||||
#include <helpers/AutoDiscoverRTCClock.h>
|
||||
@@ -44,4 +44,3 @@ extern MASensorManager sensors;
|
||||
|
||||
bool radio_init();
|
||||
mesh::LocalIdentity radio_new_identity();
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ build_flags = ${nrf52_base.build_flags}
|
||||
${sensor_base.build_flags}
|
||||
-I variants/rak3401
|
||||
-D RAK_3401
|
||||
-D RAK_BOARD
|
||||
-D NRF52_POWER_MANAGEMENT
|
||||
-D RADIO_CLASS=CustomSX1262
|
||||
-D WRAPPER_CLASS=CustomSX1262Wrapper
|
||||
|
||||
@@ -188,8 +188,8 @@ static const uint8_t AREF = PIN_AREF;
|
||||
// Power is on the controllable 3V3_S rail
|
||||
#define PIN_GPS_PPS (17) // Pulse per second input from the GPS
|
||||
|
||||
#define PIN_GPS_RX PIN_SERIAL1_RX
|
||||
#define PIN_GPS_TX PIN_SERIAL1_TX
|
||||
#define PIN_GPS_TX PIN_SERIAL1_RX
|
||||
#define PIN_GPS_RX PIN_SERIAL1_TX
|
||||
|
||||
#define PIN_GPS_1PPS PIN_GPS_PPS
|
||||
#define GPS_BAUD_RATE 9600
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <MeshCore.h>
|
||||
#include <Arduino.h>
|
||||
#include <helpers/KeyValueStore.h>
|
||||
|
||||
// built-ins
|
||||
#define PIN_VBAT_READ 26
|
||||
@@ -16,6 +17,8 @@ public:
|
||||
void begin();
|
||||
uint8_t getStartupReason() const override { return startup_reason; }
|
||||
|
||||
void attachDynamicPrefs(KeyValueStore* prefs) { } // no-op
|
||||
|
||||
void onBeforeTransmit() override {
|
||||
digitalWrite(LED_BUILTIN, HIGH); // turn TX LED on
|
||||
}
|
||||
|
||||
@@ -21,10 +21,6 @@ bool StationG3Board::setLoRaFemLnaEnabled(bool enable) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool StationG3Board::canControlLoRaFemLna() const {
|
||||
return loRaFEMControl.canControlLNA();
|
||||
}
|
||||
|
||||
bool StationG3Board::isLoRaFemLnaEnabled() const {
|
||||
return loRaFEMControl.isLNAEnabled();
|
||||
}
|
||||
@@ -37,10 +33,86 @@ bool StationG3Board::setLoRaFemPaGainEnabled(bool enable) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool StationG3Board::canControlLoRaFemPaGain() const {
|
||||
return loRaFEMControl.canControlPAGain();
|
||||
}
|
||||
|
||||
bool StationG3Board::isLoRaFemPaGainEnabled() const {
|
||||
return loRaFEMControl.isPAGainEnabled();
|
||||
}
|
||||
|
||||
void StationG3Board::attachDynamicPrefs(KeyValueStore* prefs) {
|
||||
_prefs = prefs;
|
||||
|
||||
char gain[8];
|
||||
|
||||
gain[0] = 0;
|
||||
_prefs->getByKey("fem_rxgain", gain, 7); // get initial values
|
||||
setLoRaFemLnaEnabled(strcmp(gain, "1") == 0);
|
||||
|
||||
gain[0] = 0;
|
||||
_prefs->getByKey("fem_txgain", gain, 7); // get initial values
|
||||
setLoRaFemPaGainEnabled(strcmp(gain, "1") == 0);
|
||||
}
|
||||
|
||||
bool StationG3Board::handleCommand(const char* command, uint32_t sender_timestamp, char* reply) {
|
||||
if (strcmp(command, "get radio.fem.rxgain") == 0) {
|
||||
if (!loRaFEMControl.canControlLNA()) {
|
||||
strcpy(reply, "Error: unsupported");
|
||||
} else {
|
||||
sprintf(reply, "> %s", isLoRaFemLnaEnabled() ? "on" : "off");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (memcmp(command, "set radio.fem.rxgain ", 21) == 0) {
|
||||
if (!loRaFEMControl.canControlLNA()) {
|
||||
strcpy(reply, "Error: unsupported");
|
||||
} else if (memcmp(&command[21], "on", 2) == 0) {
|
||||
if (setLoRaFemLnaEnabled(true)) {
|
||||
_prefs->setByKey("fem_rxgain", "1");
|
||||
strcpy(reply, "OK - LoRa FEM RX gain on");
|
||||
} else {
|
||||
strcpy(reply, "Error: failed to apply LoRa FEM RX gain");
|
||||
}
|
||||
} else if (memcmp(&command[21], "off", 3) == 0) {
|
||||
if (setLoRaFemLnaEnabled(false)) {
|
||||
_prefs->setByKey("fem_rxgain", "0");
|
||||
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");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (strcmp(command, "get radio.fem.txgain") == 0) {
|
||||
if (!loRaFEMControl.canControlPAGain()) {
|
||||
strcpy(reply, "Error: unsupported");
|
||||
} else {
|
||||
sprintf(reply, "> %s", isLoRaFemPaGainEnabled() ? "on" : "off");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (memcmp(command, "set radio.fem.txgain ", 21) == 0) {
|
||||
if (!loRaFEMControl.canControlPAGain()) {
|
||||
strcpy(reply, "Error: unsupported");
|
||||
} else if (memcmp(&command[21], "on", 2) == 0) {
|
||||
if (setLoRaFemPaGainEnabled(true)) {
|
||||
_prefs->setByKey("fem_txgain", "1");
|
||||
strcpy(reply, "OK - LoRa FEM TX gain on");
|
||||
} else {
|
||||
strcpy(reply, "Error: failed to apply LoRa FEM TX gain");
|
||||
}
|
||||
} else if (memcmp(&command[21], "off", 3) == 0) {
|
||||
if (setLoRaFemPaGainEnabled(false)) {
|
||||
_prefs->setByKey("fem_txgain", "0");
|
||||
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");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false; // not handled
|
||||
}
|
||||
|
||||
@@ -6,6 +6,12 @@
|
||||
#include "LoRaFEMControl.h"
|
||||
|
||||
class StationG3Board : public ESP32Board {
|
||||
KeyValueStore* _prefs = NULL;
|
||||
|
||||
bool setLoRaFemLnaEnabled(bool enable);
|
||||
bool isLoRaFemLnaEnabled() const;
|
||||
bool setLoRaFemPaGainEnabled(bool enable);
|
||||
bool isLoRaFemPaGainEnabled() const;
|
||||
public:
|
||||
LoRaFEMControl loRaFEMControl;
|
||||
|
||||
@@ -25,6 +31,10 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
void attachDynamicPrefs(KeyValueStore* prefs);
|
||||
|
||||
bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply) override;
|
||||
|
||||
void setPrimaryLNAEnable(bool enabled) {
|
||||
loRaFEMControl.setLNAEnable(enabled);
|
||||
}
|
||||
@@ -43,13 +53,6 @@ public:
|
||||
loRaFEMControl.setRxModeEnable();
|
||||
}
|
||||
|
||||
bool setLoRaFemLnaEnabled(bool enable) override;
|
||||
bool canControlLoRaFemLna() const override;
|
||||
bool isLoRaFemLnaEnabled() const override;
|
||||
bool setLoRaFemPaGainEnabled(bool enable) override;
|
||||
bool canControlLoRaFemPaGain() const override;
|
||||
bool isLoRaFemPaGainEnabled() const override;
|
||||
|
||||
void powerOff() override;
|
||||
|
||||
uint16_t getBattMilliVolts() override {
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
#include <Arduino.h>
|
||||
#include <Wire.h>
|
||||
|
||||
#include "ThinkNodeM8Board.h"
|
||||
|
||||
#ifdef THINKNODE_M8
|
||||
|
||||
void ThinkNodeM8Board::begin() {
|
||||
NRF52Board::begin();
|
||||
|
||||
Wire.begin();
|
||||
|
||||
#ifdef P_LORA_TX_LED
|
||||
pinMode(P_LORA_TX_LED, OUTPUT);
|
||||
digitalWrite(P_LORA_TX_LED, LOW);
|
||||
#endif
|
||||
|
||||
pinMode(SX126X_POWER_EN, OUTPUT);
|
||||
digitalWrite(SX126X_POWER_EN, HIGH);
|
||||
delay(10); // give sx1262 some time to power up
|
||||
}
|
||||
|
||||
uint16_t ThinkNodeM8Board::getBattMilliVolts() {
|
||||
int adcvalue = 0;
|
||||
|
||||
digitalWrite(ADC_EN, HIGH);
|
||||
analogReference(AR_INTERNAL_2_4);
|
||||
analogReadResolution(12);
|
||||
delay(10);
|
||||
|
||||
// ADC range is 0..3000mV and resolution is 12-bit (0..4095)
|
||||
adcvalue = analogRead(PIN_VBAT_READ);
|
||||
// Convert the raw value to compensated mv, taking the resistor-
|
||||
// divider into account (providing the actual LIPO voltage)
|
||||
digitalWrite(ADC_EN, LOW);
|
||||
return (uint16_t)((float)adcvalue * REAL_VBAT_MV_PER_LSB);
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,50 @@
|
||||
#pragma once
|
||||
|
||||
#include <MeshCore.h>
|
||||
#include <Arduino.h>
|
||||
#include <helpers/NRF52Board.h>
|
||||
|
||||
// built-ins
|
||||
#define VBAT_MV_PER_LSB (0.5859375F) // 2.4V ADC range and 12-bit ADC resolution = 2400mV/4096
|
||||
|
||||
#define VBAT_DIVIDER (0.57F) // 150K + 150K voltage divider on VBAT
|
||||
#define VBAT_DIVIDER_COMP (1.75F) // Compensation factor for the VBAT divider
|
||||
|
||||
#define PIN_VBAT_READ (4)
|
||||
#define REAL_VBAT_MV_PER_LSB (VBAT_DIVIDER_COMP * VBAT_MV_PER_LSB)
|
||||
|
||||
class ThinkNodeM8Board : public NRF52Board {
|
||||
public:
|
||||
ThinkNodeM8Board() : NRF52Board("THINKNODE_M8_OTA") {}
|
||||
void begin();
|
||||
uint16_t getBattMilliVolts() override;
|
||||
|
||||
#if defined(P_LORA_TX_LED)
|
||||
void onBeforeTransmit() override {
|
||||
digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED on
|
||||
}
|
||||
void onAfterTransmit() override {
|
||||
digitalWrite(P_LORA_TX_LED, LOW); // turn TX LED off
|
||||
}
|
||||
#endif
|
||||
|
||||
const char* getManufacturerName() const override {
|
||||
return "Elecrow ThinkNode-M8";
|
||||
}
|
||||
|
||||
void shutdownPeripherals() override {
|
||||
// power off board
|
||||
NRF52Board::shutdownPeripherals();
|
||||
|
||||
// make sure every gate is closed
|
||||
digitalWrite(DISP_EN, LOW);
|
||||
digitalWrite(PIN_PWR_EN, LOW);
|
||||
digitalWrite(PIN_GPS_EN, LOW);
|
||||
digitalWrite(SX126X_ANT_SW, LOW);
|
||||
digitalWrite(ADC_EN, LOW);
|
||||
|
||||
#ifdef PIN_BUTTON1 // Use BTN to go out of sleep
|
||||
nrf_gpio_cfg_sense_input(PIN_BUTTON1, NRF_GPIO_PIN_PULLUP, NRF_GPIO_PIN_SENSE_LOW);
|
||||
#endif
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,161 @@
|
||||
[ThinkNode_M8]
|
||||
extends = nrf52_base
|
||||
board = thinknode_m8
|
||||
board_build.ldscript = boards/nrf52840_s140_v6.ld
|
||||
build_flags = ${nrf52_base.build_flags}
|
||||
-I src/helpers/nrf52
|
||||
-I lib/nrf52/s140_nrf52_6.1.1_API/include
|
||||
-I lib/nrf52/s140_nrf52_6.1.1_API/include/nrf52
|
||||
-I variants/thinknode_m8
|
||||
-D THINKNODE_M8=1
|
||||
-D RADIO_CLASS=CustomSX1262
|
||||
-D WRAPPER_CLASS=CustomSX1262Wrapper
|
||||
-D P_LORA_DIO_1=25
|
||||
-D P_LORA_DIO_2=32
|
||||
-D P_LORA_NSS=21
|
||||
-D P_LORA_RESET=24
|
||||
-D P_LORA_BUSY=32 # DIO2
|
||||
-D P_LORA_SCLK=19
|
||||
-D P_LORA_MISO=22
|
||||
-D P_LORA_MOSI=20
|
||||
-D UI_SHOW_CLOCK=1
|
||||
-D UI_HAS_ROTARY_INPUT=1
|
||||
-D UI_HAS_NAV_INPUT=1
|
||||
-D UI_RECENT_LIST_SIZE=9
|
||||
-D UI_TZ_OFFSET=-4 # GMT-4
|
||||
-D UI_MSG_PREVIEW_SIZE=165
|
||||
-D EINK_MAX_PARTIAL_REFRESH=60
|
||||
-D EINK_DISPLAY_MODEL=GxEPD2_154_D67
|
||||
; -D EINK_DISPLAY_MODEL=GxEPD2_154_GDEY0154D67
|
||||
; -D EINK_DISPLAY_MODEL=GxEPD2_150_BN
|
||||
-D EINK_SCALE_X=1.5625f
|
||||
-D EINK_SCALE_Y=1.5625f
|
||||
-D EINK_X_OFFSET=0
|
||||
-D EINK_Y_OFFSET=10
|
||||
-D DISABLE_DIAGNOSTIC_OUTPUT
|
||||
-D SX126X_POWER_EN=37
|
||||
-D SX126X_DIO2_AS_RF_SWITCH=true
|
||||
-D SX126X_DIO3_TCXO_VOLTAGE=3.3
|
||||
-D SX126X_CURRENT_LIMIT=140
|
||||
-D SX126X_RX_BOOSTED_GAIN=1
|
||||
-D LORA_TX_POWER=22
|
||||
build_src_filter = ${nrf52_base.build_src_filter}
|
||||
+<helpers/*.cpp>
|
||||
+<ThinkNodeM8Board.cpp>
|
||||
+<../variants/thinknode_m8>
|
||||
lib_deps =
|
||||
${nrf52_base.lib_deps}
|
||||
stevemarple/MicroNMEA @ ^2.0.6
|
||||
debug_tool = jlink
|
||||
upload_protocol = nrfutil
|
||||
|
||||
[env:ThinkNode_M8_repeater]
|
||||
extends = ThinkNode_M8
|
||||
build_flags =
|
||||
${ThinkNode_M8.build_flags}
|
||||
-D ADVERT_NAME='"ThinkNode Repeater"'
|
||||
-D ADVERT_LAT=0.0
|
||||
-D ADVERT_LON=0.0
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
-D MAX_NEIGHBOURS=50
|
||||
; -D MESH_PACKET_LOGGING=1
|
||||
; -D MESH_DEBUG=1
|
||||
build_src_filter = ${ThinkNode_M8.build_src_filter}
|
||||
+<../examples/simple_repeater/*.cpp>
|
||||
lib_deps =
|
||||
${ThinkNode_M8.lib_deps}
|
||||
|
||||
[env:ThinkNode_M8_room_server]
|
||||
extends = ThinkNode_M8
|
||||
build_flags =
|
||||
${ThinkNode_M8.build_flags}
|
||||
-D ADVERT_NAME='"ThinkNode Room"'
|
||||
-D ADVERT_LAT=0.0
|
||||
-D ADVERT_LON=0.0
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
-D ROOM_PASSWORD='"hello"'
|
||||
; -D MESH_PACKET_LOGGING=1
|
||||
; -D MESH_DEBUG=1
|
||||
build_src_filter = ${ThinkNode_M8.build_src_filter}
|
||||
+<../examples/simple_room_server/*.cpp>
|
||||
lib_deps =
|
||||
${ThinkNode_M8.lib_deps}
|
||||
|
||||
[env:ThinkNode_M8_companion_radio_ble]
|
||||
extends = ThinkNode_M8
|
||||
board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
|
||||
board_upload.maximum_size = 712704
|
||||
build_flags =
|
||||
${ThinkNode_M8.build_flags}
|
||||
-I src/helpers/ui
|
||||
-I examples/companion_radio/ui-new
|
||||
-D MAX_CONTACTS=350
|
||||
-D MAX_GROUP_CHANNELS=40
|
||||
-D BLE_PIN_CODE=123456
|
||||
# -D BLE_DEBUG_LOGGING=1
|
||||
-D DISPLAY_ROTATION=4
|
||||
-D DISPLAY_CLASS=GxEPDDisplay
|
||||
-D BACKLIGHT_BTN=PIN_BUTTON2
|
||||
-D AUTO_OFF_MILLIS=0
|
||||
-D OFFLINE_QUEUE_SIZE=256
|
||||
-D PIN_BUZZER=33
|
||||
-D AUTO_SHUTDOWN_MILLIVOLTS=3300
|
||||
-D QSPIFLASH=1
|
||||
-D ENV_INCLUDE_GPS=1
|
||||
; -D GPS_NMEA_DEBUG=1
|
||||
; -D MESH_PACKET_LOGGING=1
|
||||
-D MESH_DEBUG=1
|
||||
build_src_filter = ${ThinkNode_M8.build_src_filter}
|
||||
+<helpers/nrf52/SerialBLEInterface.cpp>
|
||||
+<helpers/ui/GxEPDDisplay.cpp>
|
||||
+<helpers/ui/buzzer.cpp>
|
||||
+<helpers/ui/MomentaryButton.cpp>
|
||||
+<helpers/ui/RotaryInputGPIO.cpp>
|
||||
+<helpers/sensors/EnvironmentSensorManager.cpp>
|
||||
+<../examples/companion_radio/*.cpp>
|
||||
+<../examples/companion_radio/ui-new/*.cpp>
|
||||
lib_deps =
|
||||
${ThinkNode_M8.lib_deps}
|
||||
densaugeo/base64 @ ~1.4.0
|
||||
zinggjm/GxEPD2 @ 1.6.9
|
||||
bakercp/CRC32 @ ^2.0.0
|
||||
end2endzone/NonBlockingRTTTL@^1.3.0
|
||||
|
||||
[env:ThinkNode_M8_companion_radio_usb]
|
||||
extends = ThinkNode_M8
|
||||
board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
|
||||
board_upload.maximum_size = 712704
|
||||
build_flags =
|
||||
${ThinkNode_M8.build_flags}
|
||||
-I src/helpers/ui
|
||||
-I examples/companion_radio/ui-new
|
||||
-D MAX_CONTACTS=350
|
||||
-D MAX_GROUP_CHANNELS=40
|
||||
-D DISPLAY_ROTATION=4
|
||||
-D QSPIFLASH=1
|
||||
-D DISPLAY_CLASS=GxEPDDisplay
|
||||
-D BACKLIGHT_BTN=PIN_BUTTON2
|
||||
-D AUTO_OFF_MILLIS=0
|
||||
-D OFFLINE_QUEUE_SIZE=256
|
||||
-D PIN_BUZZER=6
|
||||
-D AUTO_SHUTDOWN_MILLIVOLTS=3300
|
||||
-D ENABLE_USB_INTERFACE
|
||||
build_src_filter = ${ThinkNode_M8.build_src_filter}
|
||||
+<helpers/ui/GxEPDDisplay.cpp>
|
||||
+<helpers/ui/buzzer.cpp>
|
||||
+<helpers/ui/MomentaryButton.cpp>
|
||||
+<helpers/ui/RotaryInputGPIO.cpp>
|
||||
+<helpers/sensors/EnvironmentSensorManager.cpp>
|
||||
+<../examples/companion_radio/*.cpp>
|
||||
+<../examples/companion_radio/ui-new/*.cpp>
|
||||
lib_deps =
|
||||
${ThinkNode_M8.lib_deps}
|
||||
densaugeo/base64 @ ~1.4.0
|
||||
zinggjm/GxEPD2 @ 1.6.2
|
||||
bakercp/CRC32 @ ^2.0.0
|
||||
end2endzone/NonBlockingRTTTL@^1.3.0
|
||||
|
||||
[env:ThinkNode_M8_kiss_modem]
|
||||
extends = ThinkNode_M8
|
||||
build_src_filter = ${ThinkNode_M8.build_src_filter}
|
||||
+<../examples/kiss_modem/>
|
||||
@@ -0,0 +1,36 @@
|
||||
#include <Arduino.h>
|
||||
#include "target.h"
|
||||
#include <helpers/ArduinoHelpers.h>
|
||||
#include <helpers/sensors/MicroNMEALocationProvider.h>
|
||||
#include <Wire.h>
|
||||
|
||||
ThinkNodeM8Board board;
|
||||
|
||||
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI);
|
||||
|
||||
WRAPPER_CLASS radio_driver(radio, board);
|
||||
|
||||
VolatileRTCClock fallback_clock;
|
||||
AutoDiscoverRTCClock rtc_clock(fallback_clock);
|
||||
#ifdef ENV_INCLUDE_GPS
|
||||
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock);
|
||||
EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea);
|
||||
#else
|
||||
EnvironmentSensorManager sensors = EnvironmentSensorManager();
|
||||
#endif
|
||||
|
||||
#ifdef DISPLAY_CLASS
|
||||
DISPLAY_CLASS display;
|
||||
MomentaryButton user_btn(PIN_USER_BTN, 1000, true);
|
||||
RotaryInputGPIO rotary_input(PIN_BUTTON1_A, PIN_BUTTON1_B);
|
||||
#endif
|
||||
|
||||
bool radio_init() {
|
||||
rtc_clock.begin(Wire);
|
||||
return radio.std_init(&SPI);
|
||||
}
|
||||
|
||||
mesh::LocalIdentity radio_new_identity() {
|
||||
RadioNoiseListener rng(radio);
|
||||
return mesh::LocalIdentity(&rng); // create new random identity
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
|
||||
#define RADIOLIB_STATIC_ONLY 1
|
||||
#include <RadioLib.h>
|
||||
#include <helpers/radiolib/RadioLibWrappers.h>
|
||||
#include <ThinkNodeM8Board.h>
|
||||
#include <helpers/radiolib/CustomSX1262Wrapper.h>
|
||||
#include <helpers/AutoDiscoverRTCClock.h>
|
||||
#include <helpers/SensorManager.h>
|
||||
#include <helpers/sensors/LocationProvider.h>
|
||||
#include <helpers/sensors/EnvironmentSensorManager.h>
|
||||
#ifdef DISPLAY_CLASS
|
||||
#include <helpers/ui/GxEPDDisplay.h>
|
||||
#include <helpers/ui/RotaryInputGPIO.h>
|
||||
#include <helpers/ui/MomentaryButton.h>
|
||||
#endif
|
||||
|
||||
extern ThinkNodeM8Board board;
|
||||
extern WRAPPER_CLASS radio_driver;
|
||||
extern AutoDiscoverRTCClock rtc_clock;
|
||||
extern EnvironmentSensorManager sensors;
|
||||
|
||||
#ifdef DISPLAY_CLASS
|
||||
extern DISPLAY_CLASS display;
|
||||
extern MomentaryButton user_btn;
|
||||
extern RotaryInputGPIO rotary_input;
|
||||
#endif
|
||||
|
||||
bool radio_init();
|
||||
mesh::LocalIdentity radio_new_identity();
|
||||
@@ -0,0 +1,39 @@
|
||||
#include "variant.h"
|
||||
#include "wiring_constants.h"
|
||||
#include "wiring_digital.h"
|
||||
|
||||
const int MISO = PIN_SPI1_MISO;
|
||||
const int MOSI = PIN_SPI1_MOSI;
|
||||
const int SCK = PIN_SPI1_SCK;
|
||||
|
||||
const uint32_t g_ADigitalPinMap[] = {
|
||||
0xff, 0xff, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,
|
||||
14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26,
|
||||
27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39,
|
||||
40, 41, 42, 43, 44, 45, 46, 47
|
||||
};
|
||||
|
||||
void initVariant() {
|
||||
pinMode(PIN_PWR_EN, OUTPUT);
|
||||
digitalWrite(PIN_PWR_EN, HIGH);
|
||||
pinMode(DISP_EN, OUTPUT);
|
||||
digitalWrite(DISP_EN, HIGH);
|
||||
|
||||
pinMode(PIN_BUTTON1, INPUT_PULLDOWN);
|
||||
pinMode(PIN_BUTTON1_A, INPUT_PULLDOWN);
|
||||
pinMode(PIN_BUTTON1_B, INPUT_PULLDOWN);
|
||||
pinMode(PIN_BUTTON2, INPUT_PULLUP);
|
||||
|
||||
// shutdown gps
|
||||
pinMode(PIN_GPS_STANDBY, OUTPUT);
|
||||
digitalWrite(PIN_GPS_STANDBY, HIGH);
|
||||
pinMode(PIN_GPS_EN, OUTPUT);
|
||||
digitalWrite(PIN_GPS_EN, LOW); // disable at startup
|
||||
|
||||
pinMode(SX126X_ANT_SW, OUTPUT); // ANT_SW
|
||||
digitalWrite(SX126X_ANT_SW, HIGH);
|
||||
|
||||
pinMode(ADC_EN, OUTPUT);
|
||||
digitalWrite(ADC_EN, LOW);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* variant.h
|
||||
* Copyright (C) 2023 Seeed K.K.
|
||||
* MIT License
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "WVariant.h"
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Low frequency clock source
|
||||
|
||||
#define USE_LFXO // 32.768 kHz crystal oscillator
|
||||
#define VARIANT_MCK (64000000ul)
|
||||
|
||||
#define WIRE_INTERFACES_COUNT (1)
|
||||
#define PIN_TXCO (21)
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Power
|
||||
|
||||
#define PIN_PWR_EN (13) // I2C
|
||||
|
||||
#define BATTERY_PIN (4)
|
||||
#define ADC_MULTIPLIER (1.75F)
|
||||
|
||||
#define ADC_RESOLUTION (14)
|
||||
#define BATTERY_SENSE_RES (12)
|
||||
|
||||
#define AREF_VOLTAGE (2.4)
|
||||
|
||||
#define ADC_EN (40)
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Number of pins
|
||||
|
||||
#define PINS_COUNT (48)
|
||||
#define NUM_DIGITAL_PINS (48)
|
||||
#define NUM_ANALOG_INPUTS (1)
|
||||
#define NUM_ANALOG_OUTPUTS (0)
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// UART pin definition
|
||||
|
||||
#define PIN_SERIAL1_RX PIN_GPS_TX
|
||||
#define PIN_SERIAL1_TX PIN_GPS_RX
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// I2C pin definition
|
||||
|
||||
#define PIN_WIRE_SDA (26) // P0.26
|
||||
#define PIN_WIRE_SCL (27) // P0.27
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// SPI pin definition
|
||||
|
||||
#define SPI_INTERFACES_COUNT (2)
|
||||
|
||||
#define PIN_SPI_MISO (22)
|
||||
#define PIN_SPI_MOSI (20)
|
||||
#define PIN_SPI_SCK (19)
|
||||
#define PIN_SPI_NSS (21)
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Builtin LEDs
|
||||
#define LED_BLUE (-1)
|
||||
|
||||
#define LED_BUILTIN LED_BLUE
|
||||
#define LED_PIN LED_BUILTIN
|
||||
#define LED_STATE_ON HIGH
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Builtin buttons
|
||||
|
||||
#define PIN_BUTTON1 (6)
|
||||
#define PIN_BUTTON1_A (8) // Rotary button/Encoder
|
||||
#define PIN_BUTTON1_B (41)
|
||||
#define BUTTON_PIN PIN_BUTTON1
|
||||
|
||||
#define PIN_BUTTON2 (12)
|
||||
#define BUTTON_PIN2 PIN_BUTTON2
|
||||
|
||||
#define PIN_USER_BTN PIN_BUTTON1
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Lora
|
||||
|
||||
#define USE_SX1262
|
||||
#define LORA_CS (24)
|
||||
#define SX126X_DIO1 (20)
|
||||
#define SX126X_BUSY (17)
|
||||
#define SX126X_RESET (25)
|
||||
#define SX126X_ANT_SW (23)
|
||||
#define SX126X_DIO2_AS_RF_SWITCH
|
||||
#define SX126X_DIO3_TCXO_VOLTAGE 1.8
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// SPI1
|
||||
#define PIN_SPI1_NSS (30)
|
||||
#define PIN_SPI1_SCK (31)
|
||||
#define PIN_SPI1_MOSI (29)
|
||||
#define PIN_SPI1_MISO (-1)
|
||||
|
||||
// GxEPD2 needs that for a panel that is not even used !
|
||||
extern const int MISO;
|
||||
extern const int MOSI;
|
||||
extern const int SCK;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// QSPI
|
||||
#define EXTERNAL_FLASH_DEVICES MX25R1635F
|
||||
#define EXTERNAL_FLASH_USE_QSPI
|
||||
#define PIN_QSPI_SCK (46)
|
||||
#define PIN_QSPI_CS (47)
|
||||
#define PIN_QSPI_IO0 (44) // MOSI if using two bit interface
|
||||
#define PIN_QSPI_IO1 (45) // MISO if using two bit interface
|
||||
#define PIN_QSPI_IO2 (7) // WP if using two bit interface (i.e. not used)
|
||||
#define PIN_QSPI_IO3 (5) // HOLD if using two bit interface (i.e. not used)
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Display
|
||||
|
||||
#define DISP_MISO PIN_SPI1_MISO
|
||||
#define DISP_MOSI PIN_SPI1_MOSI
|
||||
#define DISP_SCLK PIN_SPI1_SCK
|
||||
#define PIN_DISPLAY_CS PIN_SPI1_NSS
|
||||
#define PIN_DISPLAY_DC (28)
|
||||
#define PIN_DISPLAY_RST (2)
|
||||
#define PIN_DISPLAY_BUSY (3)
|
||||
#define DISP_BACKLIGHT (43)
|
||||
#define DISP_EN (42)
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// GPS
|
||||
|
||||
#define PIN_GPS_RX (36)
|
||||
#define PIN_GPS_TX (34)
|
||||
#define PIN_GPS_EN (16)
|
||||
#define PIN_GPS_RESET (-1)
|
||||
#define PIN_GPS_PPS (14)
|
||||
#define PIN_GPS_STANDBY (15) // Low = sleep
|
||||
#define GPS_BAUD_RATE 115200
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <MeshCore.h>
|
||||
#include <helpers/KeyValueStore.h>
|
||||
|
||||
// LoRa radio module pins for Waveshare RP2040-LoRa-HF/LF
|
||||
// https://files.waveshare.com/wiki/RP2040-LoRa/Rp2040-lora-sch.pdf
|
||||
@@ -32,6 +33,8 @@ public:
|
||||
void begin();
|
||||
uint8_t getStartupReason() const override { return startup_reason; }
|
||||
|
||||
void attachDynamicPrefs(KeyValueStore* prefs) { } // no-op
|
||||
|
||||
#ifdef P_LORA_TX_LED
|
||||
void onBeforeTransmit() override { digitalWrite(P_LORA_TX_LED, HIGH); }
|
||||
void onAfterTransmit() override { digitalWrite(P_LORA_TX_LED, LOW); }
|
||||
|
||||
@@ -65,6 +65,7 @@ build_flags = ${WioTrackerL1.build_flags}
|
||||
-D MAX_GROUP_CHANNELS=40
|
||||
-D DISPLAY_CLASS=SH1106Display
|
||||
-D UI_HAS_JOYSTICK=1
|
||||
-D UI_NO_HIBERNATE
|
||||
-D OFFLINE_QUEUE_SIZE=256
|
||||
-D PIN_BUZZER=12
|
||||
-D QSPIFLASH=1
|
||||
@@ -95,6 +96,7 @@ build_flags = ${WioTrackerL1.build_flags}
|
||||
-D OFFLINE_QUEUE_SIZE=256
|
||||
-D DISPLAY_CLASS=SH1106Display
|
||||
-D UI_HAS_JOYSTICK=1
|
||||
-D UI_NO_HIBERNATE
|
||||
-D PIN_BUZZER=12
|
||||
-D QSPIFLASH=1
|
||||
-D ADVERT_NAME='"@@MAC"'
|
||||
|
||||
@@ -73,6 +73,7 @@ lib_deps =
|
||||
|
||||
[env:Xiao_C3_companion_radio_ble]
|
||||
extends = Xiao_esp32_C3
|
||||
board_build.partitions = min_spiffs.csv ; get around 4mb flash limit
|
||||
build_src_filter = ${Xiao_esp32_C3.build_src_filter}
|
||||
+<../examples/companion_radio/*.cpp>
|
||||
+<helpers/esp32/*.cpp>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <MeshCore.h>
|
||||
#include <helpers/KeyValueStore.h>
|
||||
|
||||
/*
|
||||
* This board has no built-in way to read battery voltage.
|
||||
@@ -30,6 +31,8 @@ public:
|
||||
void begin();
|
||||
uint8_t getStartupReason() const override { return startup_reason; }
|
||||
|
||||
void attachDynamicPrefs(KeyValueStore* prefs) { } // no-op
|
||||
|
||||
#ifdef P_LORA_TX_LED
|
||||
void onBeforeTransmit() override { digitalWrite(P_LORA_TX_LED, HIGH); }
|
||||
void onAfterTransmit() override { digitalWrite(P_LORA_TX_LED, LOW); }
|
||||
|
||||
Reference in New Issue
Block a user