diff --git a/boards/thinknode_m8.json b/boards/thinknode_m8.json new file mode 100644 index 00000000..acd7f89c --- /dev/null +++ b/boards/thinknode_m8.json @@ -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" +} diff --git a/docs/payloads.md b/docs/payloads.md index a2945e27..fb9cbaf9 100644 --- a/docs/payloads.md +++ b/docs/payloads.md @@ -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 diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 0a14cbfb..536b7803 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -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) { diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 238adada..780de35d 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -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; diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 21766de8..abd701a5 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -1,6 +1,8 @@ #pragma once #include // For uint8_t, uint32_t #include +#include +#include #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(); } }; diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 051f3b31..57ecf935 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -2,10 +2,15 @@ #include #include "../MyMesh.h" #include "target.h" +#include #ifdef WIFI_SSID #include #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(); diff --git a/examples/companion_radio/ui-new/icons.h b/examples/companion_radio/ui-new/icons.h index cbe23790..8d9ab175 100644 --- a/examples/companion_radio/ui-new/icons.h +++ b/examples/companion_radio/ui-new/icons.h @@ -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 }; \ No newline at end of file diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 6c96112a..75d9fb4e 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -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()); } diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index aabd605a..539a1715 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -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; diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index a89dca23..e6bed7d6 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -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(); diff --git a/examples/simple_secure_chat/main.cpp b/examples/simple_secure_chat/main.cpp index 02a60b25..3566ef14 100644 --- a/examples/simple_secure_chat/main.cpp +++ b/examples/simple_secure_chat/main.cpp @@ -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 { } diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index b497fc85..4480753d 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -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(); diff --git a/platformio.ini b/platformio.ini index 73ce373b..d8eff5b5 100644 --- a/platformio.ini +++ b/platformio.ini @@ -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 diff --git a/src/MeshCore.h b/src/MeshCore.h index 15ccdc15..adfc1c9e 100644 --- a/src/MeshCore.h +++ b/src/MeshCore.h @@ -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; } }; /** diff --git a/src/Utils.cpp b/src/Utils.cpp index 5ae7f0e2..7a3fb78b 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -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 diff --git a/src/Utils.h b/src/Utils.h index 5736b874..7a0f7b6e 100644 --- a/src/Utils.h +++ b/src/Utils.h @@ -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); }; } diff --git a/src/armstubs.cpp b/src/armstubs.cpp new file mode 100644 index 00000000..30da40ec --- /dev/null +++ b/src/armstubs.cpp @@ -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; } diff --git a/src/helpers/AdvertDataHelpers.cpp b/src/helpers/AdvertDataHelpers.cpp index 998733ae..25e5cd3f 100644 --- a/src/helpers/AdvertDataHelpers.cpp +++ b/src/helpers/AdvertDataHelpers.cpp @@ -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; diff --git a/src/helpers/AdvertDataHelpers.h b/src/helpers/AdvertDataHelpers.h index abe14cbd..f4e109e9 100644 --- a/src/helpers/AdvertDataHelpers.h +++ b/src/helpers/AdvertDataHelpers.h @@ -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; } diff --git a/src/helpers/BaseChatMesh.cpp b/src/helpers/BaseChatMesh.cpp index 972a97e9..28592415 100644 --- a/src/helpers/BaseChatMesh.cpp +++ b/src/helpers/BaseChatMesh.cpp @@ -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); diff --git a/src/helpers/BaseChatMesh.h b/src/helpers/BaseChatMesh.h index d9878547..0a8fdef4 100644 --- a/src/helpers/BaseChatMesh.h +++ b/src/helpers/BaseChatMesh.h @@ -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); diff --git a/src/helpers/ClientACL.h b/src/helpers/ClientACL.h index b758f706..e0654464 100644 --- a/src/helpers/ClientACL.h +++ b/src/helpers/ClientACL.h @@ -34,7 +34,7 @@ struct ClientInfo { }; #ifndef MAX_CLIENTS - #define MAX_CLIENTS 20 + #define MAX_CLIENTS 32 #endif class ClientACL { diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index bf015527..35d1822f 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -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); diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 42287ffb..040e845f 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -7,6 +7,8 @@ #include // For MAX_MQTT_SLOTS (used in NodePrefs struct layout) #include #include +#include +#include #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 diff --git a/src/helpers/CommonRadioPrefs.cpp b/src/helpers/CommonRadioPrefs.cpp new file mode 100644 index 00000000..a25df880 --- /dev/null +++ b/src/helpers/CommonRadioPrefs.cpp @@ -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 +} diff --git a/src/helpers/CommonRadioPrefs.h b/src/helpers/CommonRadioPrefs.h new file mode 100644 index 00000000..85e6da55 --- /dev/null +++ b/src/helpers/CommonRadioPrefs.h @@ -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; +}; diff --git a/src/helpers/ConfigSerializer.cpp b/src/helpers/ConfigSerializer.cpp index b8788bf5..54d999ec 100644 --- a/src/helpers/ConfigSerializer.cpp +++ b/src/helpers/ConfigSerializer.cpp @@ -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; diff --git a/src/helpers/ConfigSerializer.h b/src/helpers/ConfigSerializer.h index e889e637..6f74a9d8 100644 --- a/src/helpers/ConfigSerializer.h +++ b/src/helpers/ConfigSerializer.h @@ -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; } }; diff --git a/src/helpers/ContactInfo.h b/src/helpers/ContactInfo.h index ede977ca..5a156c82 100644 --- a/src/helpers/ContactInfo.h +++ b/src/helpers/ContactInfo.h @@ -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]; }; diff --git a/src/helpers/DynamicConfigSerializer.cpp b/src/helpers/DynamicConfigSerializer.cpp new file mode 100644 index 00000000..b9c6c267 --- /dev/null +++ b/src/helpers/DynamicConfigSerializer.cpp @@ -0,0 +1,97 @@ +#include "DynamicConfigSerializer.h" +#include + +#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()); + } +} diff --git a/src/helpers/DynamicConfigSerializer.h b/src/helpers/DynamicConfigSerializer.h new file mode 100644 index 00000000..e5dc4c7c --- /dev/null +++ b/src/helpers/DynamicConfigSerializer.h @@ -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; +}; diff --git a/src/helpers/ESP32Board.h b/src/helpers/ESP32Board.h index 3ecf6735..08e264bc 100644 --- a/src/helpers/ESP32Board.h +++ b/src/helpers/ESP32Board.h @@ -15,6 +15,7 @@ #include "soc/rtc.h" #include "esp_system.h" #include +#include 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; diff --git a/src/helpers/KeyValueStore.h b/src/helpers/KeyValueStore.h new file mode 100644 index 00000000..cc4ac455 --- /dev/null +++ b/src/helpers/KeyValueStore.h @@ -0,0 +1,11 @@ +#pragma once +#include +#include + +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; } +}; diff --git a/src/helpers/NRF52Board.h b/src/helpers/NRF52Board.h index de716855..45d1fea7 100644 --- a/src/helpers/NRF52Board.h +++ b/src/helpers/NRF52Board.h @@ -2,6 +2,7 @@ #include #include +#include #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; } diff --git a/src/helpers/TxtDataHelpers.h b/src/helpers/TxtDataHelpers.h index d9e82b23..8b11890a 100644 --- a/src/helpers/TxtDataHelpers.h +++ b/src/helpers/TxtDataHelpers.h @@ -4,8 +4,10 @@ #include #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 diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index 1cf30486..13654d2a 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -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}; -} \ No newline at end of file +} diff --git a/src/helpers/sensors/EnvironmentSensorManager.cpp b/src/helpers/sensors/EnvironmentSensorManager.cpp index 54305692..6f460775 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.cpp +++ b/src/helpers/sensors/EnvironmentSensorManager.cpp @@ -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 diff --git a/src/helpers/stm32/STM32Board.h b/src/helpers/stm32/STM32Board.h index cc33dfe5..ef8e0104 100644 --- a/src/helpers/stm32/STM32Board.h +++ b/src/helpers/stm32/STM32Board.h @@ -2,6 +2,7 @@ #include #include +#include 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 { diff --git a/src/helpers/ui/DisplayDriver.h b/src/helpers/ui/DisplayDriver.h index 74d3f2f6..4e912c94 100644 --- a/src/helpers/ui/DisplayDriver.h +++ b/src/helpers/ui/DisplayDriver.h @@ -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; diff --git a/src/helpers/ui/GxEPDDisplay.cpp b/src/helpers/ui/GxEPDDisplay.cpp index 13dafa34..332157eb 100644 --- a/src/helpers/ui/GxEPDDisplay.cpp +++ b/src/helpers/ui/GxEPDDisplay.cpp @@ -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; } diff --git a/src/helpers/ui/GxEPDDisplay.h b/src/helpers/ui/GxEPDDisplay.h index c653eac4..8a37b72c 100644 --- a/src/helpers/ui/GxEPDDisplay.h +++ b/src/helpers/ui/GxEPDDisplay.h @@ -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; diff --git a/src/helpers/ui/MomentaryButton.h b/src/helpers/ui/MomentaryButton.h index 358a343b..351545ce 100644 --- a/src/helpers/ui/MomentaryButton.h +++ b/src/helpers/ui/MomentaryButton.h @@ -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; diff --git a/src/helpers/ui/RotaryInputGPIO.cpp b/src/helpers/ui/RotaryInputGPIO.cpp new file mode 100644 index 00000000..4ed04c98 --- /dev/null +++ b/src/helpers/ui/RotaryInputGPIO.cpp @@ -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; +} diff --git a/src/helpers/ui/RotaryInputGPIO.h b/src/helpers/ui/RotaryInputGPIO.h new file mode 100644 index 00000000..a68e01d3 --- /dev/null +++ b/src/helpers/ui/RotaryInputGPIO.h @@ -0,0 +1,21 @@ +#pragma once + +#include +#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;} +}; diff --git a/test/mocks/CommonRadioPrefs.cpp b/test/mocks/CommonRadioPrefs.cpp new file mode 100644 index 00000000..d749cc1e --- /dev/null +++ b/test/mocks/CommonRadioPrefs.cpp @@ -0,0 +1,32 @@ +#include "helpers/CommonRadioPrefs.h" +#include +#include + +// 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; +} diff --git a/test/test_config_serializer/test_config_serializer.cpp b/test/test_config_serializer/test_config_serializer.cpp index ae63f682..60774063 100644 --- a/test/test_config_serializer/test_config_serializer.cpp +++ b/test/test_config_serializer/test_config_serializer.cpp @@ -1,5 +1,6 @@ #include #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 ─────────────────────────────────────────────────────── diff --git a/test/test_main_board_api/test_main_board_api.cpp b/test/test_main_board_api/test_main_board_api.cpp new file mode 100644 index 00000000..59ec11ba --- /dev/null +++ b/test/test_main_board_api/test_main_board_api.cpp @@ -0,0 +1,34 @@ +#include +#include +#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(); +} diff --git a/variants/heltec_t096/T096Board.cpp b/variants/heltec_t096/T096Board.cpp index 78af529d..ea26d2cc 100644 --- a/variants/heltec_t096/T096Board.cpp +++ b/variants/heltec_t096/T096Board.cpp @@ -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 +} diff --git a/variants/heltec_t096/T096Board.h b/variants/heltec_t096/T096Board.h index 15c7e68b..e926c8a6 100644 --- a/variants/heltec_t096/T096Board.h +++ b/variants/heltec_t096/T096Board.h @@ -4,28 +4,33 @@ #include #include #include +#include #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; }; diff --git a/variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp b/variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp index 1b7396dd..20fae5b6 100644 --- a/variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp +++ b/variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp @@ -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 +} diff --git a/variants/heltec_tracker_v2/HeltecTrackerV2Board.h b/variants/heltec_tracker_v2/HeltecTrackerV2Board.h index 2bd6a025..5fed2b65 100644 --- a/variants/heltec_tracker_v2/HeltecTrackerV2Board.h +++ b/variants/heltec_tracker_v2/HeltecTrackerV2Board.h @@ -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; }; diff --git a/variants/heltec_v4/HeltecV4Board.cpp b/variants/heltec_v4/HeltecV4Board.cpp index 3f13f41f..794257d7 100644 --- a/variants/heltec_v4/HeltecV4Board.cpp +++ b/variants/heltec_v4/HeltecV4Board.cpp @@ -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 +} diff --git a/variants/heltec_v4/HeltecV4Board.h b/variants/heltec_v4/HeltecV4Board.h index 55166bb3..79a690e8 100644 --- a/variants/heltec_v4/HeltecV4Board.h +++ b/variants/heltec_v4/HeltecV4Board.h @@ -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) { diff --git a/variants/heltec_v4_r8/HeltecV4R8Board.cpp b/variants/heltec_v4_r8/HeltecV4R8Board.cpp index 434a83ca..0aa9d7d3 100644 --- a/variants/heltec_v4_r8/HeltecV4R8Board.cpp +++ b/variants/heltec_v4_r8/HeltecV4R8Board.cpp @@ -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; +} diff --git a/variants/heltec_v4_r8/HeltecV4R8Board.h b/variants/heltec_v4_r8/HeltecV4R8Board.h index d8617b5b..6bfefa6b 100644 --- a/variants/heltec_v4_r8/HeltecV4R8Board.h +++ b/variants/heltec_v4_r8/HeltecV4R8Board.h @@ -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) { diff --git a/src/helpers/MeshadventurerBoard.h b/variants/meshadventurer/MeshadventurerBoard.h similarity index 97% rename from src/helpers/MeshadventurerBoard.h rename to variants/meshadventurer/MeshadventurerBoard.h index 0325161d..d9e88e1b 100644 --- a/src/helpers/MeshadventurerBoard.h +++ b/variants/meshadventurer/MeshadventurerBoard.h @@ -13,7 +13,7 @@ #define PIN_VBAT_READ 35 -#include "ESP32Board.h" +#include "helpers/ESP32Board.h" class MeshadventurerBoard : public ESP32Board { diff --git a/variants/meshadventurer/target.h b/variants/meshadventurer/target.h index d59b3aee..0749565d 100644 --- a/variants/meshadventurer/target.h +++ b/variants/meshadventurer/target.h @@ -3,7 +3,7 @@ #define RADIOLIB_STATIC_ONLY 1 #include #include -#include +#include #include #include #include @@ -44,4 +44,3 @@ extern MASensorManager sensors; bool radio_init(); mesh::LocalIdentity radio_new_identity(); - diff --git a/variants/rak3401/platformio.ini b/variants/rak3401/platformio.ini index 48e71922..c285e3ec 100644 --- a/variants/rak3401/platformio.ini +++ b/variants/rak3401/platformio.ini @@ -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 diff --git a/variants/rak3401/variant.h b/variants/rak3401/variant.h index 98827886..e0c24759 100644 --- a/variants/rak3401/variant.h +++ b/variants/rak3401/variant.h @@ -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 diff --git a/variants/rpi_picow/PicoWBoard.h b/variants/rpi_picow/PicoWBoard.h index 708e9655..e64590f2 100644 --- a/variants/rpi_picow/PicoWBoard.h +++ b/variants/rpi_picow/PicoWBoard.h @@ -2,6 +2,7 @@ #include #include +#include // 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 } diff --git a/variants/station_g3_esp32/StationG3Board.cpp b/variants/station_g3_esp32/StationG3Board.cpp index dd863aca..277c2b8b 100644 --- a/variants/station_g3_esp32/StationG3Board.cpp +++ b/variants/station_g3_esp32/StationG3Board.cpp @@ -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 +} diff --git a/variants/station_g3_esp32/StationG3Board.h b/variants/station_g3_esp32/StationG3Board.h index 52628eb6..b4c5838c 100644 --- a/variants/station_g3_esp32/StationG3Board.h +++ b/variants/station_g3_esp32/StationG3Board.h @@ -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 { diff --git a/variants/thinknode_m8/ThinkNodeM8Board.cpp b/variants/thinknode_m8/ThinkNodeM8Board.cpp new file mode 100644 index 00000000..2ca393d9 --- /dev/null +++ b/variants/thinknode_m8/ThinkNodeM8Board.cpp @@ -0,0 +1,38 @@ +#include +#include + +#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 diff --git a/variants/thinknode_m8/ThinkNodeM8Board.h b/variants/thinknode_m8/ThinkNodeM8Board.h new file mode 100644 index 00000000..73aeb1f5 --- /dev/null +++ b/variants/thinknode_m8/ThinkNodeM8Board.h @@ -0,0 +1,50 @@ +#pragma once + +#include +#include +#include + +// 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 + } +}; diff --git a/variants/thinknode_m8/platformio.ini b/variants/thinknode_m8/platformio.ini new file mode 100644 index 00000000..ad1b90f0 --- /dev/null +++ b/variants/thinknode_m8/platformio.ini @@ -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} + + + + + +<../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} + + + + + + + + + + + + + +<../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} + + + + + + + + + + + +<../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/> diff --git a/variants/thinknode_m8/target.cpp b/variants/thinknode_m8/target.cpp new file mode 100644 index 00000000..ac4ac100 --- /dev/null +++ b/variants/thinknode_m8/target.cpp @@ -0,0 +1,36 @@ +#include +#include "target.h" +#include +#include +#include + +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 +} diff --git a/variants/thinknode_m8/target.h b/variants/thinknode_m8/target.h new file mode 100644 index 00000000..c3402a01 --- /dev/null +++ b/variants/thinknode_m8/target.h @@ -0,0 +1,30 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef DISPLAY_CLASS + #include + #include + #include +#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(); diff --git a/variants/thinknode_m8/variant.cpp b/variants/thinknode_m8/variant.cpp new file mode 100644 index 00000000..e15c0842 --- /dev/null +++ b/variants/thinknode_m8/variant.cpp @@ -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); + +} diff --git a/variants/thinknode_m8/variant.h b/variants/thinknode_m8/variant.h new file mode 100644 index 00000000..9c36c772 --- /dev/null +++ b/variants/thinknode_m8/variant.h @@ -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 diff --git a/variants/waveshare_rp2040_lora/WaveshareBoard.h b/variants/waveshare_rp2040_lora/WaveshareBoard.h index 694b8bd1..e7c70a5d 100644 --- a/variants/waveshare_rp2040_lora/WaveshareBoard.h +++ b/variants/waveshare_rp2040_lora/WaveshareBoard.h @@ -2,6 +2,7 @@ #include #include +#include // 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); } diff --git a/variants/wio-tracker-l1/platformio.ini b/variants/wio-tracker-l1/platformio.ini index fc958ea2..19139c28 100644 --- a/variants/wio-tracker-l1/platformio.ini +++ b/variants/wio-tracker-l1/platformio.ini @@ -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"' diff --git a/variants/xiao_c3/platformio.ini b/variants/xiao_c3/platformio.ini index 587c5c27..c9c107c6 100644 --- a/variants/xiao_c3/platformio.ini +++ b/variants/xiao_c3/platformio.ini @@ -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> + diff --git a/variants/xiao_rp2040/XiaoRP2040Board.h b/variants/xiao_rp2040/XiaoRP2040Board.h index d2951c75..cad399de 100644 --- a/variants/xiao_rp2040/XiaoRP2040Board.h +++ b/variants/xiao_rp2040/XiaoRP2040Board.h @@ -2,6 +2,7 @@ #include #include +#include /* * 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); }