diff --git a/.clang-format b/.clang-format new file mode 100644 index 00000000..66ecd43a --- /dev/null +++ b/.clang-format @@ -0,0 +1,84 @@ +# .clang-format +Language: Cpp +AccessModifierOffset: -2 +AlignAfterOpenBracket: Align +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignConsecutiveMacros: + Enabled: true + AcrossEmptyLines: true + AcrossComments: true +AlignOperands: true +AlignTrailingComments: true +AllowAllParametersOfDeclarationOnNextLine: false +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: Inline +AllowShortIfStatementsOnASingleLine: true +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: No +BinPackArguments: true +BinPackParameters: true +BraceWrapping: + AfterClass: false + AfterControlStatement: false + AfterEnum: false + AfterFunction: false + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + BeforeCatch: true + BeforeElse: true + IndentBraces: false +BreakBeforeBinaryOperators: None +BreakBeforeBraces: Attach +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: false +ColumnLimit: 110 +CommentPragmas: '^ IWYU pragma:' +CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: false +Cpp11BracedListStyle: false +DerivePointerAlignment: false +DisableFormat: false +IncludeBlocks: Regroup +IndentCaseLabels: false +IndentPPDirectives: None +IndentWidth: 2 +IndentWrappedFunctionNames: false +KeepEmptyLinesAtTheStartOfBlocks: true +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +ObjCBinPackProtocolList: Auto +PenaltyBreakBeforeFirstCallParameter: 19 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyExcessCharacter: 100000 +PenaltyReturnTypeOnItsOwnLine: 60 +PointerAlignment: Right +ReflowComments: true +SortIncludes: true +SpaceAfterCStyleCast: false +SpaceAfterTemplateKeyword: true +SpaceBeforeAssignmentOperators: true +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: false +SpacesInContainerLiterals: false +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +Standard: Auto +TabWidth: 2 +UseTab: Never +AlignEscapedNewlines: LeftWithLastLine \ No newline at end of file diff --git a/README.md b/README.md index b4943dea..708db2c4 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,11 @@ MeshCore is open-source software released under the MIT License. You are free to Please submit PR's using 'dev' as the base branch! For minor changes just submit your PR and I'll try to review it, but for anything more 'impactful' please open an Issue first and start a discussion. Is better to sound out what it is you want to achieve first, and try to come to a consensus on what the best approach is, especially when it impacts the structure or architecture of this codebase. +Here are some general principals you should try to adhere to: +* Keep it simple. Please, don't think like a high-level lang programmer. Think embedded, and keep code concise, without any unecessary layers. +* No dynamic memory allocation, except during setup/begin functions. +* Use the same brace and indenting style that's in the core source modules. (A .clang-format is prob going to be added soon, but please do NOT retroactively re-format existing code. This just creates unnecessary diffs that make finding problems harder) + ## 📞 Get Support - Report bugs and request features on the [GitHub Issues](https://github.com/ripplebiz/MeshCore/issues) page. diff --git a/boards/nano-g2-ultra.json b/boards/nano-g2-ultra.json index 11e7ebaa..9fa22d7b 100644 --- a/boards/nano-g2-ultra.json +++ b/boards/nano-g2-ultra.json @@ -46,7 +46,8 @@ ], "debug": { "jlink_device": "nRF52840_xxAA", - "svd_path": "nrf52840.svd" + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52.cfg" }, "frameworks": [ "arduino" @@ -69,4 +70,4 @@ }, "url": "https://wiki.uniteng.com/en/meshtastic/nano-g2-ultra", "vendor": "BQ Consulting" -} \ No newline at end of file +} diff --git a/docs/faq.md b/docs/faq.md index 030c5531..c61c8d2b 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -65,7 +65,8 @@ author: https://github.com/LitBomb - [6.4. Q: I can't connect via Bluetooth, what is the Bluetooth pairing code?](#64-q-i-cant-connect-via-bluetooth-what-is-the-bluetooth-pairing-code) - [6.5. Q: My Heltec V3 keeps disconnecting from my smartphone. It can't hold a solid Bluetooth connection.](#65-q-my-heltec-v3-keeps-disconnecting-from-my-smartphone--it-cant-hold-a-solid-bluetooth-connection) - [7. Other Questions:](#7-other-questions) - - [7.1. Q: How to Update repeater and room server firmware over the air?](#71-q-how-to--update-repeater-and-room-server-firmware-over-the-air) + - [7.2 Q: How to update ESP32-based devices over the air?](#72-q-how-to-update-esp32-based-devices-over-the-air) + - [7.1 Q: How to update nRF (RAK, T114, Seed XIAO) repeater and room server firmware over the air using the new simpler DFU app?](#71-q-how-to-update-nrf-rak-t114-seed-xiao-repeater-and-room-server-firmware-over-the-air-using-the-new-simpler-dfu-app) ## 1. Introduction @@ -534,74 +535,35 @@ You can get the epoch time on and use it to se --- ## 7. Other Questions: -### 7.1. Q: How to Update repeater and room server firmware over the air? -**A:** Only nRF-based RAK4631 and Heltec T114 OTA firmware update are verified using nRF smartphone app. Lilygo T-Echo doesn't work currently. -You can update repeater and room server firmware with a Bluetooth connection between your smartphone and your LoRa radio using the nRF app. +### 7.2 Q: How to update ESP32-based devices over the air? -1. Download the ZIP file for the specific node from the web flasher to your smartphone -2. On the phone client, log on to the repeater as administrator (default password is `password`) to issue the `start ota`command to the repeater or room server to get the device into OTA DFU mode - -![image](https://github.com/user-attachments/assets/889bb81b-7214-4a1c-955a-396b5a05d8ad) - -1. `start ota` can be initiated from USB serial console on the web flasher page or a T-Deck -4. On the smartphone, download and run the nRF app and scan for Bluetooth devices -5. Connect to the repeater/room server node you want to update - 1. nRF app is available on both Android and iOS - -**Android continues after the iOS section:** - -**iOS continues here:** -5. Once connected successfully, a `DFU` icon ![Pasted image 20250309173039](https://github.com/user-attachments/assets/af7a9f78-8739-4946-b734-02bade9c8e71) - appears in the top right corner of the app - ![Pasted image 20250309171919](https://github.com/user-attachments/assets/08007ec8-4924-49c1-989f-ca2611e78793) - -6. Scroll down to change the `PRN(s)` number: - -![Pasted image 20250309190158](https://github.com/user-attachments/assets/11f69cdd-12f3-4696-a6fc-14a78c85fe32) - -- For the T114, change the number of packets `(PRN(s)` to 8 -- For RAK, it can be 10, but it also works on 8. - -7. Click the `DFU` icon ![Pasted image 20250309173039](https://github.com/user-attachments/assets/af7a9f78-8739-4946-b734-02bade9c8e71), select the type of file to upload (choose ZIP), then select the ZIP file that was downloaded earlier from the web flasher -8. The upload process will start now. If everything goes well, the node resets and is flashed successfully. -![Pasted image 20250309190342](https://github.com/user-attachments/assets/a60e25d0-33b8-46cf-af90-20a7d8ac2adb) +**A:** For ESP32-based devices (e.g. Heltec V3): +1. On flasher.meshcore.co.uk, download the **non-merged** version of the firmware for your ESP32 device (e.g. `Heltec_v3_repeater-v1.6.2-4449fd3.bin`, no `"merged"` in the file name) +2. From the MeshCore app, login remotely to the repeater you want to update with admin priviledge +4. Go to the Command Line tab, type `start ota` and hit enter. +5. you should see `OK` to confirm the repeater device is now in OTA mode +6. The command `start ota` on an ESP32-based device starts a wifi hotspot named `MeshCore OTA` +7. From your phone or computer connect to the 'MeshCore OTA' hotspot +8. From a browser, go to http://192.168.4.1/update and upload the non-merged bin from the flasher +### 7.1 Q: How to update nRF (RAK, T114, Seed XIAO) repeater and room server firmware over the air using the new simpler DFU app? -**Android steps continues below:** -1. on the top left corner of the nRF Connect app on Android, tap the 3-bar hamburger menu, then `Settings`, then `nRF5 DFU Options` +**A:** The steps below work on both Android and iOS as nRF has made both apps' user interface the same on both platforms: -![Android nRF Hamberger](https://github.com/user-attachments/assets/ea6dfeef-9367-4830-bd70-1441d517c706) - -![Android nRF Settings](https://github.com/user-attachments/assets/c63726bf-cecd-4987-be68-afb6358c7190) - -![Android nRF DFU Options](https://github.com/user-attachments/assets/b20e872f-5122-41d9-90df-0215cff5fbc9) - -2. Change `Number of packets` to `10` for RAK, `8` for Heltec T114 - -![Android nRF Number of Packets](https://github.com/user-attachments/assets/c092adaf-4cb3-460b-b7ef-8d7f450d602b) - -3. Go back to the main screen -4. Your LoRa device should already ben in DFU mode from previous steps -5. tap `SCANNER` and then `SCAN` to find the device you want to update, tap `CONNECT` - -![Android nRF Scanner Scan Connect](https://github.com/user-attachments/assets/37218717-f167-48b6-a6ca-93d132ef77ca) - -6. On the top left corner of the nRF Connect app, tap the `DFU` icon next to the three dots - -![Android nRF DFU](https://github.com/user-attachments/assets/1ec3b818-bf0c-461f-8fdf-37c41a63cafa) - -7. Choose `Distribution packet (ZIP)` and then `OK` - -![Android nRF Distribution Packet (ZIP)](https://github.com/user-attachments/assets/e65f5616-9793-44f5-95c0-a3eb15aa7152) - -8. Choose the firmware file in ZIP formate that you downloaded earlier from the MeshCore web flasher, update will start as soon as you tap the file - -![Android nRF FW Updating](https://github.com/user-attachments/assets/0814d123-85ce-4c87-90a7-e1a25dc71900) - -9. When the update process is done, the device will disconnect from nRF app and the LoRa device is updated +1. Download nRF's DFU app from iOS App Store or Android's Play Store, you can find the app by searching for `nrf dfu`, the app's full name is `nRF Device Firmware Update` +2. On flasher.meshcore.co.uk, download the **ZIP** version of the firmware for your nRF device (e.g. RAK or Heltec T114 or Seeed Studio's Xiao) +3. From the MeshCore app, login remotely to the repeater you want to update with admin priviledge +4. Go to the Command Line tab, type `start ota` and hit enter. +5. you should see `OK` to confirm the repeater device is now in OTA mode +6. Run the DFU app,tab `Settings` on the top right corner +7. Enable `Packets receipt notifications` and change `Number of Packets` to 10 for RAK, 8 for T114. 8 also works for RAK. +8. Select the firmware zip file you downloaded +9. Select the device you want to update. If the device you want to updat is not on the list, try enabling`OTA` on the device again +10. Tab the `Upload` to begin OTA update +11. If it fails, try turning off and on Bluetooth on your phone. If that doesn't work, try rebooting your phone. +12. Wait for the update to complete. It can take a few minutes. --- - diff --git a/docs/payloads.md b/docs/payloads.md new file mode 100644 index 00000000..2c402102 --- /dev/null +++ b/docs/payloads.md @@ -0,0 +1,176 @@ +# Meshcore payloads +Inside of each [meshcore packet](./packet_structure.md) is a payload, identified by the payload type in the packet header. The types of payloads are: + +* Request (destination/source hashes + MAC). +* Response to REQ or ANON_REQ. +* Plain text message. +* Acknowledgment. +* Node advertisement. +* Group text message (unverified). +* Group datagram (unverified). +* Anonymous request. +* Returned path. +* Custom packet (raw bytes, custom encryption). + +This document defines the structure of each of these payload types + +# Node advertisement +This kind of payload notifies receivers that a node exists, and gives information about the node + +| Field | Size (bytes) | Description | +|---------------|-----------------|----------------------------------------------------------| +| public key | 32 | Ed25519 public key | +| timestamp | 4 | unix timestamp of advertisement | +| signature | 64 | Ed25519 signature of public key, timestamp, and app data | +| appdata | rest of payload | optional, see below | + +Appdata + +| Field | Size (bytes) | Description | +|---------------|-----------------|-------------------------------------------------------| +| flags | 1 | specifies which of the fields are present, see below | +| latitude | 4 | decimal latitude multiplied by 1000000, integer | +| longitude | 4 | decimal longitude multiplied by 1000000, integer | +| feature 1 | 2 | reserved for future use | +| feature 2 | 2 | reserved for future use | +| name | rest of appdata | name of the node | + +Appdata Flags + +| Value | Name | Description | +|--------|-----------|---------------------------------------| +| `0x10` | location | appdata contains lat/long information | +| `0x20` | feature 1 | Reserved for future use. | +| `0x40` | feature 2 | Reserved for future use. | +| `0x80` | name | appdata contains a node name | + +# Acknowledgement +| Field | Size (bytes) | Description | +|----------|--------------|------------------------------------------------------------| +| checksum | 4 | CRC checksum of message timestamp, text, and sender pubkey | + + +# Returned path, request, response, and plain text message +| Field | Size (bytes) | Description | +|------------------|-----------------|------------------------------------------------------| +| destination hash | 1 | first byte of destination node public key | +| source hash | 1 | first byte of source node public key | +| cipher MAC | 2 | MAC for encrypted data in next field | +| ciphertext | rest of payload | encrypted message, see subsections below for details | + +## Returned path + +| Field | Size (bytes) | Description | +|-------------|--------------|----------------------------------------------------------------------------------------------| +| path length | 1 | length of next field | +| path | see above | a list of node hashes (one byte each) describing the route from us to the packet author | +| extra type | 1 | extra, bundled payload type, eg., acknowledgement or response. See packet structure spec | +| extra | rest of data | extra, bundled payload content, follows same format as main content defined by this document | + +## Request + +| Field | Size (bytes) | Description | +|--------------|-----------------|----------------------------| +| timestamp | 4 | send time (unix timestamp) | +| request type | 1 | see below | +| request data | rest of payload | depends on request type | + +Request type + +| Value | Name | Description | +|--------|--------------------|---------------------------------------| +| `0x01` | get status | get status of repeater or room server | +| `0x02` | keepalive | TODO | +| `0x03` | get telemetry data | TODO | + +### Get status + +Gets information about the node, possibly including the following: + +* Battery level (millivolts) +* Current transmit queue length +* Current free queue length +* Last RSSI value +* Number of received packets +* Number of sent packets +* Total airtime (seconds) +* Total uptime (seconds) +* Number of packets sent as flood +* Number of packets sent directly +* Number of packets received as flood +* Number of packets received directly +* Error flags +* Last SNR value +* Number of direct route duplicates +* Number of flood route duplicates +* Number posted (?) +* Number of post pushes (?) + +### Keepalive + +No-op request. + +### Get telemetry data + +Request data about sensors on the node, including battery level. + +## Response + +| Field | Size (bytes) | Description | +|---------|-----------------|-------------| +| tag | 4 | TODO | +| content | rest of payload | TODO | + +## Plain text message + +| Field | Size (bytes) | Description | +|--------------|-----------------|--------------------------------------------------------------| +| timestamp | 4 | send time (unix timestamp) | +| flags + TODO | 1 | first six bits are flags (see below), last two bits are TODO | +| message | rest of payload | the message content, see next table | + +Flags + +| Value | Description | Message content | +|--------|---------------------------|------------------------------------------------------------| +| `0x00` | plain text message | the plain text of the message | +| `0x01` | CLI command | the command text of the message | +| `0x02` | signed plain text message | two bytes of sender prefix, followed by plain text message | + +# Anonymous request + +| Field | Size (bytes) | Description | +|------------------|-----------------|-------------------------------------------| +| destination hash | 1 | first byte of destination node public key | +| public key | 32 | sender's Ed25519 public key | +| cipher MAC | 2 | MAC for encrypted data in next field | +| ciphertext | rest of payload | encrypted message, see below for details | + +Plaintext message + +| Field | Size (bytes) | Description | +|----------------|-----------------|-------------------------------------------------------------------------------| +| timestamp | 4 | send time (unix timestamp) | +| sync timestamp | 4 | for room server, otherwise absent: sender's "sync messages SINCE x" timestamp | +| password | rest of message | password for repeater/room | + +# Group text message / datagram + +| Field | Size (bytes) | Description | +|--------------|-----------------|------------------------------------------| +| channel hash | 1 | TODO | +| cipher MAC | 2 | MAC for encrypted data in next field | +| ciphertext | rest of payload | encrypted message, see below for details | + +Plaintext for text message + +| Field | Size (bytes) | Description | +|-----------|-----------------|----------------------------------| +| timestamp | 4 | send time (unix timestamp) | +| content | rest of message | plain group text message content | + +TODO: describe what datagram looks like + +# Custom packet + +Custom packets have no defined format. \ No newline at end of file diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp new file mode 100644 index 00000000..2ba5ccfb --- /dev/null +++ b/examples/companion_radio/DataStore.cpp @@ -0,0 +1,391 @@ +#include +#include "DataStore.h" + +DataStore::DataStore(FILESYSTEM& fs, mesh::RTCClock& clock) : _fs(&fs), _clock(&clock), +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + identity_store(fs, "") +#elif defined(RP2040_PLATFORM) + identity_store(fs, "/identity") +#else + identity_store(fs, "/identity") +#endif +{ +} + +static File openWrite(FILESYSTEM* _fs, const char* filename) { +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + _fs->remove(filename); + return _fs->open(filename, FILE_O_WRITE); +#elif defined(RP2040_PLATFORM) + return _fs->open(filename, "w"); +#else + return _fs->open(filename, "w", true); +#endif +} + +void DataStore::begin() { +#if defined(RP2040_PLATFORM) + identity_store.begin(); +#endif + +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + checkAdvBlobFile(); +#else + // init 'blob store' support + _fs->mkdir("/bl"); +#endif +} + +#if defined(ESP32) + #include +#elif defined(RP2040_PLATFORM) + #include +#endif + +File DataStore::openRead(const char* filename) { +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + return _fs->open(filename, FILE_O_READ); +#elif defined(RP2040_PLATFORM) + return _fs->open(filename, "r"); +#else + return _fs->open(filename, "r", false); +#endif +} + +bool DataStore::removeFile(const char* filename) { + return _fs->remove(filename); +} + +bool DataStore::formatFileSystem() { +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + return _fs->format(); +#elif defined(RP2040_PLATFORM) + return LittleFS.format(); +#elif defined(ESP32) + return ((fs::SPIFFSFS *)_fs)->format(); +#else + #error "need to implement format()" +#endif +} + +bool DataStore::loadMainIdentity(mesh::LocalIdentity &identity) { + return identity_store.load("_main", identity); +} + +bool DataStore::saveMainIdentity(const mesh::LocalIdentity &identity) { + return identity_store.save("_main", identity); +} + +void DataStore::loadPrefs(NodePrefs& prefs, double& node_lat, double& node_lon) { + if (_fs->exists("/new_prefs")) { + loadPrefsInt("/new_prefs", prefs, node_lat, node_lon); // new filename + } else if (_fs->exists("/node_prefs")) { + loadPrefsInt("/node_prefs", prefs, node_lat, node_lon); + savePrefs(prefs, node_lat, node_lon); // save to new filename + _fs->remove("/node_prefs"); // remove old + } +} + +void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& node_lat, double& node_lon) { +#if defined(RP2040_PLATFORM) + File file = _fs->open(filename, "r"); +#else + File file = _fs->open(filename); +#endif + if (file) { + uint8_t pad[8]; + + file.read((uint8_t *)&_prefs.airtime_factor, sizeof(float)); // 0 + file.read((uint8_t *)_prefs.node_name, sizeof(_prefs.node_name)); // 4 + file.read(pad, 4); // 36 + file.read((uint8_t *)&node_lat, sizeof(node_lat)); // 40 + file.read((uint8_t *)&node_lon, sizeof(node_lon)); // 48 + file.read((uint8_t *)&_prefs.freq, sizeof(_prefs.freq)); // 56 + file.read((uint8_t *)&_prefs.sf, sizeof(_prefs.sf)); // 60 + file.read((uint8_t *)&_prefs.cr, sizeof(_prefs.cr)); // 61 + file.read((uint8_t *)&_prefs.reserved1, sizeof(_prefs.reserved1)); // 62 + file.read((uint8_t *)&_prefs.manual_add_contacts, sizeof(_prefs.manual_add_contacts)); // 63 + file.read((uint8_t *)&_prefs.bw, sizeof(_prefs.bw)); // 64 + file.read((uint8_t *)&_prefs.tx_power_dbm, sizeof(_prefs.tx_power_dbm)); // 68 + file.read((uint8_t *)&_prefs.telemetry_mode_base, sizeof(_prefs.telemetry_mode_base)); // 69 + file.read((uint8_t *)&_prefs.telemetry_mode_loc, sizeof(_prefs.telemetry_mode_loc)); // 70 + file.read((uint8_t *)&_prefs.telemetry_mode_env, sizeof(_prefs.telemetry_mode_env)); // 71 + file.read((uint8_t *)&_prefs.rx_delay_base, sizeof(_prefs.rx_delay_base)); // 72 + file.read(pad, 4); // 76 + file.read((uint8_t *)&_prefs.ble_pin, sizeof(_prefs.ble_pin)); // 80 + + file.close(); + } +} + +void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_lon) { + File file = openWrite(_fs, "/new_prefs"); + if (file) { + uint8_t pad[8]; + memset(pad, 0, sizeof(pad)); + + file.write((uint8_t *)&_prefs.airtime_factor, sizeof(float)); // 0 + file.write((uint8_t *)_prefs.node_name, sizeof(_prefs.node_name)); // 4 + file.write(pad, 4); // 36 + file.write((uint8_t *)&node_lat, sizeof(node_lat)); // 40 + file.write((uint8_t *)&node_lon, sizeof(node_lon)); // 48 + file.write((uint8_t *)&_prefs.freq, sizeof(_prefs.freq)); // 56 + file.write((uint8_t *)&_prefs.sf, sizeof(_prefs.sf)); // 60 + file.write((uint8_t *)&_prefs.cr, sizeof(_prefs.cr)); // 61 + file.write((uint8_t *)&_prefs.reserved1, sizeof(_prefs.reserved1)); // 62 + file.write((uint8_t *)&_prefs.manual_add_contacts, sizeof(_prefs.manual_add_contacts)); // 63 + file.write((uint8_t *)&_prefs.bw, sizeof(_prefs.bw)); // 64 + file.write((uint8_t *)&_prefs.tx_power_dbm, sizeof(_prefs.tx_power_dbm)); // 68 + file.write((uint8_t *)&_prefs.telemetry_mode_base, sizeof(_prefs.telemetry_mode_base)); // 69 + file.write((uint8_t *)&_prefs.telemetry_mode_loc, sizeof(_prefs.telemetry_mode_loc)); // 70 + file.write((uint8_t *)&_prefs.telemetry_mode_env, sizeof(_prefs.telemetry_mode_env)); // 71 + file.write((uint8_t *)&_prefs.rx_delay_base, sizeof(_prefs.rx_delay_base)); // 72 + file.write(pad, 4); // 76 + file.write((uint8_t *)&_prefs.ble_pin, sizeof(_prefs.ble_pin)); // 80 + + file.close(); + } +} + +void DataStore::loadContacts(DataStoreHost* host) { + if (_fs->exists("/contacts3")) { +#if defined(RP2040_PLATFORM) + File file = _fs->open("/contacts3", "r"); +#else + File file = _fs->open("/contacts3"); +#endif + if (file) { + bool full = false; + while (!full) { + ContactInfo c; + uint8_t pub_key[32]; + uint8_t unused; + + bool success = (file.read(pub_key, 32) == 32); + success = success && (file.read((uint8_t *)&c.name, 32) == 32); + success = success && (file.read(&c.type, 1) == 1); + success = success && (file.read(&c.flags, 1) == 1); + success = success && (file.read(&unused, 1) == 1); + success = success && (file.read((uint8_t *)&c.sync_since, 4) == 4); // was 'reserved' + success = success && (file.read((uint8_t *)&c.out_path_len, 1) == 1); + success = success && (file.read((uint8_t *)&c.last_advert_timestamp, 4) == 4); + success = success && (file.read(c.out_path, 64) == 64); + success = success && (file.read((uint8_t *)&c.lastmod, 4) == 4); + success = success && (file.read((uint8_t *)&c.gps_lat, 4) == 4); + success = success && (file.read((uint8_t *)&c.gps_lon, 4) == 4); + + if (!success) break; // EOF + + c.id = mesh::Identity(pub_key); + if (!host->onContactLoaded(c)) full = true; + } + file.close(); + } + } +} + +void DataStore::saveContacts(DataStoreHost* host) { + File file = openWrite(_fs, "/contacts3"); + if (file) { + uint32_t idx = 0; + ContactInfo c; + uint8_t unused = 0; + + while (host->getContactForSave(idx, c)) { + bool success = (file.write(c.id.pub_key, 32) == 32); + success = success && (file.write((uint8_t *)&c.name, 32) == 32); + success = success && (file.write(&c.type, 1) == 1); + success = success && (file.write(&c.flags, 1) == 1); + success = success && (file.write(&unused, 1) == 1); + success = success && (file.write((uint8_t *)&c.sync_since, 4) == 4); + success = success && (file.write((uint8_t *)&c.out_path_len, 1) == 1); + success = success && (file.write((uint8_t *)&c.last_advert_timestamp, 4) == 4); + success = success && (file.write(c.out_path, 64) == 64); + success = success && (file.write((uint8_t *)&c.lastmod, 4) == 4); + success = success && (file.write((uint8_t *)&c.gps_lat, 4) == 4); + success = success && (file.write((uint8_t *)&c.gps_lon, 4) == 4); + + if (!success) break; // write failed + + idx++; // advance to next contact + } + file.close(); + } +} + +void DataStore::loadChannels(DataStoreHost* host) { + if (_fs->exists("/channels2")) { +#if defined(RP2040_PLATFORM) + File file = _fs->open("/channels2", "r"); +#else + File file = _fs->open("/channels2"); +#endif + if (file) { + bool full = false; + uint8_t channel_idx = 0; + while (!full) { + ChannelDetails ch; + uint8_t unused[4]; + + bool success = (file.read(unused, 4) == 4); + success = success && (file.read((uint8_t *)ch.name, 32) == 32); + success = success && (file.read((uint8_t *)ch.channel.secret, 32) == 32); + + if (!success) break; // EOF + + if (host->onChannelLoaded(channel_idx, ch)) { + channel_idx++; + } else { + full = true; + } + } + file.close(); + } + } +} + +void DataStore::saveChannels(DataStoreHost* host) { + File file = openWrite(_fs, "/channels2"); + if (file) { + uint8_t channel_idx = 0; + ChannelDetails ch; + uint8_t unused[4]; + memset(unused, 0, 4); + + while (host->getChannelForSave(channel_idx, ch)) { + bool success = (file.write(unused, 4) == 4); + success = success && (file.write((uint8_t *)ch.name, 32) == 32); + success = success && (file.write((uint8_t *)ch.channel.secret, 32) == 32); + + if (!success) break; // write failed + channel_idx++; + } + file.close(); + } +} + +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + +#define MAX_ADVERT_PKT_LEN (2 + 32 + PUB_KEY_SIZE + 4 + SIGNATURE_SIZE + MAX_ADVERT_DATA_SIZE) + +struct BlobRec { + uint32_t timestamp; + uint8_t key[7]; + uint8_t len; + uint8_t data[MAX_ADVERT_PKT_LEN]; +}; + +void DataStore::checkAdvBlobFile() { + if (!_fs->exists("/adv_blobs")) { + File file = openWrite(_fs, "/adv_blobs"); + if (file) { + BlobRec zeroes; + memset(&zeroes, 0, sizeof(zeroes)); + for (int i = 0; i < 20; i++) { // pre-allocate to fixed size + file.write((uint8_t *) &zeroes, sizeof(zeroes)); + } + file.close(); + } + } +} + +uint8_t DataStore::getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]) { + File file = _fs->open("/adv_blobs"); + uint8_t len = 0; // 0 = not found + + if (file) { + BlobRec tmp; + while (file.read((uint8_t *) &tmp, sizeof(tmp)) == sizeof(tmp)) { + if (memcmp(key, tmp.key, sizeof(tmp.key)) == 0) { // only match by 7 byte prefix + len = tmp.len; + memcpy(dest_buf, tmp.data, len); + break; + } + } + file.close(); + } + return len; +} + +bool DataStore::putBlobByKey(const uint8_t key[], int key_len, const uint8_t src_buf[], uint8_t len) { + if (len < PUB_KEY_SIZE+4+SIGNATURE_SIZE || len > MAX_ADVERT_PKT_LEN) return false; + + checkAdvBlobFile(); + + File file = _fs->open("/adv_blobs", FILE_O_WRITE); + if (file) { + uint32_t pos = 0, found_pos = 0; + uint32_t min_timestamp = 0xFFFFFFFF; + + // search for matching key OR evict by oldest timestmap + BlobRec tmp; + file.seek(0); + while (file.read((uint8_t *) &tmp, sizeof(tmp)) == sizeof(tmp)) { + if (memcmp(key, tmp.key, sizeof(tmp.key)) == 0) { // only match by 7 byte prefix + found_pos = pos; + break; + } + if (tmp.timestamp < min_timestamp) { + min_timestamp = tmp.timestamp; + found_pos = pos; + } + + pos += sizeof(tmp); + } + + memcpy(tmp.key, key, sizeof(tmp.key)); // just record 7 byte prefix of key + memcpy(tmp.data, src_buf, len); + tmp.len = len; + tmp.timestamp = _clock->getCurrentTime(); + + file.seek(found_pos); + file.write((uint8_t *) &tmp, sizeof(tmp)); + + file.close(); + return true; + } + return false; // error +} +#else +uint8_t DataStore::getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]) { + char path[64]; + char fname[18]; + + if (key_len > 8) key_len = 8; // just use first 8 bytes (prefix) + mesh::Utils::toHex(fname, key, key_len); + sprintf(path, "/bl/%s", fname); + + if (_fs->exists(path)) { +#if defined(RP2040_PLATFORM) + File f = _fs->open(path, "r"); +#else + File f = _fs->open(path); +#endif + if (f) { + int len = f.read(dest_buf, 255); // currently MAX 255 byte blob len supported!! + f.close(); + return len; + } + } + return 0; // not found +} + +bool DataStore::putBlobByKey(const uint8_t key[], int key_len, const uint8_t src_buf[], uint8_t len) { + char path[64]; + char fname[18]; + + if (key_len > 8) key_len = 8; // just use first 8 bytes (prefix) + mesh::Utils::toHex(fname, key, key_len); + sprintf(path, "/bl/%s", fname); + + File f = openWrite(_fs, path); + if (f) { + int n = f.write(src_buf, len); + f.close(); + if (n == len) return true; // success! + + _fs->remove(path); // blob was only partially written! + } + return false; // error +} +#endif diff --git a/examples/companion_radio/DataStore.h b/examples/companion_radio/DataStore.h new file mode 100644 index 00000000..32ccd196 --- /dev/null +++ b/examples/companion_radio/DataStore.h @@ -0,0 +1,42 @@ +#pragma once + +#include +#include +#include +#include "NodePrefs.h" + +class DataStoreHost { +public: + virtual bool onContactLoaded(const ContactInfo& contact) =0; + virtual bool getContactForSave(uint32_t idx, ContactInfo& contact) =0; + virtual bool onChannelLoaded(uint8_t channel_idx, const ChannelDetails& ch) =0; + virtual bool getChannelForSave(uint8_t channel_idx, ChannelDetails& ch) =0; +}; + +class DataStore { + FILESYSTEM* _fs; + mesh::RTCClock* _clock; + IdentityStore identity_store; + + void loadPrefsInt(const char *filename, NodePrefs& prefs, double& node_lat, double& node_lon); +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + void checkAdvBlobFile(); +#endif + +public: + DataStore(FILESYSTEM& fs, mesh::RTCClock& clock); + void begin(); + bool formatFileSystem(); + bool loadMainIdentity(mesh::LocalIdentity &identity); + bool saveMainIdentity(const mesh::LocalIdentity &identity); + void loadPrefs(NodePrefs& prefs, double& node_lat, double& node_lon); + void savePrefs(const NodePrefs& prefs, double node_lat, double node_lon); + void loadContacts(DataStoreHost* host); + void saveContacts(DataStoreHost* host); + void loadChannels(DataStoreHost* host); + void saveChannels(DataStoreHost* host); + uint8_t getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]); + bool putBlobByKey(const uint8_t key[], int key_len, const uint8_t src_buf[], uint8_t len); + File openRead(const char* filename); + bool removeFile(const char* filename); +}; diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp new file mode 100644 index 00000000..97781a08 --- /dev/null +++ b/examples/companion_radio/MyMesh.cpp @@ -0,0 +1,1484 @@ +#include "MyMesh.h" + +#include // needed for PlatformIO +#include + +#define CMD_APP_START 1 +#define CMD_SEND_TXT_MSG 2 +#define CMD_SEND_CHANNEL_TXT_MSG 3 +#define CMD_GET_CONTACTS 4 // with optional 'since' (for efficient sync) +#define CMD_GET_DEVICE_TIME 5 +#define CMD_SET_DEVICE_TIME 6 +#define CMD_SEND_SELF_ADVERT 7 +#define CMD_SET_ADVERT_NAME 8 +#define CMD_ADD_UPDATE_CONTACT 9 +#define CMD_SYNC_NEXT_MESSAGE 10 +#define CMD_SET_RADIO_PARAMS 11 +#define CMD_SET_RADIO_TX_POWER 12 +#define CMD_RESET_PATH 13 +#define CMD_SET_ADVERT_LATLON 14 +#define CMD_REMOVE_CONTACT 15 +#define CMD_SHARE_CONTACT 16 +#define CMD_EXPORT_CONTACT 17 +#define CMD_IMPORT_CONTACT 18 +#define CMD_REBOOT 19 +#define CMD_GET_BATTERY_VOLTAGE 20 +#define CMD_SET_TUNING_PARAMS 21 +#define CMD_DEVICE_QEURY 22 +#define CMD_EXPORT_PRIVATE_KEY 23 +#define CMD_IMPORT_PRIVATE_KEY 24 +#define CMD_SEND_RAW_DATA 25 +#define CMD_SEND_LOGIN 26 +#define CMD_SEND_STATUS_REQ 27 +#define CMD_HAS_CONNECTION 28 +#define CMD_LOGOUT 29 // 'Disconnect' +#define CMD_GET_CONTACT_BY_KEY 30 +#define CMD_GET_CHANNEL 31 +#define CMD_SET_CHANNEL 32 +#define CMD_SIGN_START 33 +#define CMD_SIGN_DATA 34 +#define CMD_SIGN_FINISH 35 +#define CMD_SEND_TRACE_PATH 36 +#define CMD_SET_DEVICE_PIN 37 +#define CMD_SET_OTHER_PARAMS 38 +#define CMD_SEND_TELEMETRY_REQ 39 +#define CMD_GET_CUSTOM_VARS 40 +#define CMD_SET_CUSTOM_VAR 41 +#define CMD_GET_ADVERT_PATH 42 + +#define RESP_CODE_OK 0 +#define RESP_CODE_ERR 1 +#define RESP_CODE_CONTACTS_START 2 // first reply to CMD_GET_CONTACTS +#define RESP_CODE_CONTACT 3 // multiple of these (after CMD_GET_CONTACTS) +#define RESP_CODE_END_OF_CONTACTS 4 // last reply to CMD_GET_CONTACTS +#define RESP_CODE_SELF_INFO 5 // reply to CMD_APP_START +#define RESP_CODE_SENT 6 // reply to CMD_SEND_TXT_MSG +#define RESP_CODE_CONTACT_MSG_RECV 7 // a reply to CMD_SYNC_NEXT_MESSAGE (ver < 3) +#define RESP_CODE_CHANNEL_MSG_RECV 8 // a reply to CMD_SYNC_NEXT_MESSAGE (ver < 3) +#define RESP_CODE_CURR_TIME 9 // a reply to CMD_GET_DEVICE_TIME +#define RESP_CODE_NO_MORE_MESSAGES 10 // a reply to CMD_SYNC_NEXT_MESSAGE +#define RESP_CODE_EXPORT_CONTACT 11 +#define RESP_CODE_BATTERY_VOLTAGE 12 // a reply to a CMD_GET_BATTERY_VOLTAGE +#define RESP_CODE_DEVICE_INFO 13 // a reply to CMD_DEVICE_QEURY +#define RESP_CODE_PRIVATE_KEY 14 // a reply to CMD_EXPORT_PRIVATE_KEY +#define RESP_CODE_DISABLED 15 +#define RESP_CODE_CONTACT_MSG_RECV_V3 16 // a reply to CMD_SYNC_NEXT_MESSAGE (ver >= 3) +#define RESP_CODE_CHANNEL_MSG_RECV_V3 17 // a reply to CMD_SYNC_NEXT_MESSAGE (ver >= 3) +#define RESP_CODE_CHANNEL_INFO 18 // a reply to CMD_GET_CHANNEL +#define RESP_CODE_SIGN_START 19 +#define RESP_CODE_SIGNATURE 20 +#define RESP_CODE_CUSTOM_VARS 21 +#define RESP_CODE_ADVERT_PATH 22 + +#define SEND_TIMEOUT_BASE_MILLIS 500 +#define FLOOD_SEND_TIMEOUT_FACTOR 16.0f +#define DIRECT_SEND_PERHOP_FACTOR 6.0f +#define DIRECT_SEND_PERHOP_EXTRA_MILLIS 250 +#define LAZY_CONTACTS_WRITE_DELAY 5000 + +#define PUBLIC_GROUP_PSK "izOH6cXN6mrJ5e26oRXNcg==" + +// these are _pushed_ to client app at any time +#define PUSH_CODE_ADVERT 0x80 +#define PUSH_CODE_PATH_UPDATED 0x81 +#define PUSH_CODE_SEND_CONFIRMED 0x82 +#define PUSH_CODE_MSG_WAITING 0x83 +#define PUSH_CODE_RAW_DATA 0x84 +#define PUSH_CODE_LOGIN_SUCCESS 0x85 +#define PUSH_CODE_LOGIN_FAIL 0x86 +#define PUSH_CODE_STATUS_RESPONSE 0x87 +#define PUSH_CODE_LOG_RX_DATA 0x88 +#define PUSH_CODE_TRACE_DATA 0x89 +#define PUSH_CODE_NEW_ADVERT 0x8A +#define PUSH_CODE_TELEMETRY_RESPONSE 0x8B + +#define ERR_CODE_UNSUPPORTED_CMD 1 +#define ERR_CODE_NOT_FOUND 2 +#define ERR_CODE_TABLE_FULL 3 +#define ERR_CODE_BAD_STATE 4 +#define ERR_CODE_FILE_IO_ERROR 5 +#define ERR_CODE_ILLEGAL_ARG 6 + +#define MAX_SIGN_DATA_LEN (8 * 1024) // 8K + +#ifdef DISPLAY_CLASS +#include "UITask.h" +#endif + +void MyMesh::writeOKFrame() { + uint8_t buf[1]; + buf[0] = RESP_CODE_OK; + _serial->writeFrame(buf, 1); +} +void MyMesh::writeErrFrame(uint8_t err_code) { + uint8_t buf[2]; + buf[0] = RESP_CODE_ERR; + buf[1] = err_code; + _serial->writeFrame(buf, 2); +} + +void MyMesh::writeDisabledFrame() { + uint8_t buf[1]; + buf[0] = RESP_CODE_DISABLED; + _serial->writeFrame(buf, 1); +} + +void MyMesh::writeContactRespFrame(uint8_t code, const ContactInfo &contact) { + int i = 0; + out_frame[i++] = code; + memcpy(&out_frame[i], contact.id.pub_key, PUB_KEY_SIZE); + i += PUB_KEY_SIZE; + out_frame[i++] = contact.type; + out_frame[i++] = contact.flags; + out_frame[i++] = contact.out_path_len; + memcpy(&out_frame[i], contact.out_path, MAX_PATH_SIZE); + i += MAX_PATH_SIZE; + StrHelper::strzcpy((char *)&out_frame[i], contact.name, 32); + i += 32; + memcpy(&out_frame[i], &contact.last_advert_timestamp, 4); + i += 4; + memcpy(&out_frame[i], &contact.gps_lat, 4); + i += 4; + memcpy(&out_frame[i], &contact.gps_lon, 4); + i += 4; + memcpy(&out_frame[i], &contact.lastmod, 4); + i += 4; + _serial->writeFrame(out_frame, i); +} + +void MyMesh::updateContactFromFrame(ContactInfo &contact, const uint8_t *frame, int len) { + int i = 0; + uint8_t code = frame[i++]; // eg. CMD_ADD_UPDATE_CONTACT + memcpy(contact.id.pub_key, &frame[i], PUB_KEY_SIZE); + i += PUB_KEY_SIZE; + contact.type = frame[i++]; + contact.flags = frame[i++]; + contact.out_path_len = frame[i++]; + memcpy(contact.out_path, &frame[i], MAX_PATH_SIZE); + i += MAX_PATH_SIZE; + memcpy(contact.name, &frame[i], 32); + i += 32; + memcpy(&contact.last_advert_timestamp, &frame[i], 4); + i += 4; + if (i + 8 >= len) { // optional fields + memcpy(&contact.gps_lat, &frame[i], 4); + i += 4; + memcpy(&contact.gps_lon, &frame[i], 4); + i += 4; + } +} + +void MyMesh::addToOfflineQueue(const uint8_t frame[], int len) { + if (offline_queue_len >= OFFLINE_QUEUE_SIZE) { + MESH_DEBUG_PRINTLN("ERROR: offline_queue is full!"); + } else { + offline_queue[offline_queue_len].len = len; + memcpy(offline_queue[offline_queue_len].buf, frame, len); + offline_queue_len++; + } +} +int MyMesh::getFromOfflineQueue(uint8_t frame[]) { + if (offline_queue_len > 0) { // check offline queue + size_t len = offline_queue[0].len; // take from top of queue + memcpy(frame, offline_queue[0].buf, len); + + offline_queue_len--; + for (int i = 0; i < offline_queue_len; i++) { // delete top item from queue + offline_queue[i] = offline_queue[i + 1]; + } + return len; + } + return 0; // queue is empty +} + +float MyMesh::getAirtimeBudgetFactor() const { + return _prefs.airtime_factor; +} + +int MyMesh::getInterferenceThreshold() const { + return 0; // disabled for now, until currentRSSI() problem is resolved +} + +int MyMesh::calcRxDelay(float score, uint32_t air_time) const { + if (_prefs.rx_delay_base <= 0.0f) return 0; + return (int)((pow(_prefs.rx_delay_base, 0.85f - score) - 1.0) * air_time); +} + +void MyMesh::logRxRaw(float snr, float rssi, const uint8_t raw[], int len) { + if (_serial->isConnected() && len + 3 <= MAX_FRAME_SIZE) { + int i = 0; + out_frame[i++] = PUSH_CODE_LOG_RX_DATA; + out_frame[i++] = (int8_t)(snr * 4); + out_frame[i++] = (int8_t)(rssi); + memcpy(&out_frame[i], raw, len); + i += len; + + _serial->writeFrame(out_frame, i); + } +} + +bool MyMesh::isAutoAddEnabled() const { + return (_prefs.manual_add_contacts & 1) == 0; +} + +void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) { + if (_serial->isConnected()) { + if (!isAutoAddEnabled() && is_new) { + writeContactRespFrame(PUSH_CODE_NEW_ADVERT, contact); + } else { + out_frame[0] = PUSH_CODE_ADVERT; + memcpy(&out_frame[1], contact.id.pub_key, PUB_KEY_SIZE); + _serial->writeFrame(out_frame, 1 + PUB_KEY_SIZE); + } + } else { +#ifdef DISPLAY_CLASS + ui_task.soundBuzzer(UIEventType::newContactMessage); +#endif + } + + // add inbound-path to mem cache + if (path && path_len <= sizeof(AdvertPath::path)) { // check path is valid + AdvertPath* p = advert_paths; + uint32_t oldest = 0xFFFFFFFF; + for (int i = 0; i < ADVERT_PATH_TABLE_SIZE; i++) { // check if already in table, otherwise evict oldest + if (memcmp(advert_paths[i].pubkey_prefix, contact.id.pub_key, sizeof(AdvertPath::pubkey_prefix)) == 0) { + p = &advert_paths[i]; // found + break; + } + if (advert_paths[i].recv_timestamp < oldest) { + oldest = advert_paths[i].recv_timestamp; + p = &advert_paths[i]; + } + } + + memcpy(p->pubkey_prefix, contact.id.pub_key, sizeof(p->pubkey_prefix)); + p->recv_timestamp = getRTCClock()->getCurrentTime(); + p->path_len = path_len; + memcpy(p->path, path, p->path_len); + } + + dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); +} + +void MyMesh::onContactPathUpdated(const ContactInfo &contact) { + out_frame[0] = PUSH_CODE_PATH_UPDATED; + memcpy(&out_frame[1], contact.id.pub_key, PUB_KEY_SIZE); + _serial->writeFrame(out_frame, 1 + PUB_KEY_SIZE); // NOTE: app may not be connected + + dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); +} + +bool MyMesh::processAck(const uint8_t *data) { + // see if matches any in a table + for (int i = 0; i < EXPECTED_ACK_TABLE_SIZE; i++) { + if (memcmp(data, &expected_ack_table[i].ack, 4) == 0) { // got an ACK from recipient + out_frame[0] = PUSH_CODE_SEND_CONFIRMED; + memcpy(&out_frame[1], data, 4); + uint32_t trip_time = _ms->getMillis() - expected_ack_table[i].msg_sent; + memcpy(&out_frame[5], &trip_time, 4); + _serial->writeFrame(out_frame, 9); + + // NOTE: the same ACK can be received multiple times! + expected_ack_table[i].ack = 0; // clear expected hash, now that we have received ACK + return true; + } + } + return checkConnectionsAck(data); +} + +void MyMesh::queueMessage(const ContactInfo &from, uint8_t txt_type, mesh::Packet *pkt, + uint32_t sender_timestamp, const uint8_t *extra, int extra_len, const char *text) { + int i = 0; + if (app_target_ver >= 3) { + out_frame[i++] = RESP_CODE_CONTACT_MSG_RECV_V3; + out_frame[i++] = (int8_t)(pkt->getSNR() * 4); + out_frame[i++] = 0; // reserved1 + out_frame[i++] = 0; // reserved2 + } else { + out_frame[i++] = RESP_CODE_CONTACT_MSG_RECV; + } + memcpy(&out_frame[i], from.id.pub_key, 6); + i += 6; // just 6-byte prefix + uint8_t path_len = out_frame[i++] = pkt->isRouteFlood() ? pkt->path_len : 0xFF; + out_frame[i++] = txt_type; + memcpy(&out_frame[i], &sender_timestamp, 4); + i += 4; + if (extra_len > 0) { + memcpy(&out_frame[i], extra, extra_len); + i += extra_len; + } + int tlen = strlen(text); // TODO: UTF-8 ?? + if (i + tlen > MAX_FRAME_SIZE) { + tlen = MAX_FRAME_SIZE - i; + } + memcpy(&out_frame[i], text, tlen); + i += tlen; + addToOfflineQueue(out_frame, i); + + if (_serial->isConnected()) { + uint8_t frame[1]; + frame[0] = PUSH_CODE_MSG_WAITING; // send push 'tickle' + _serial->writeFrame(frame, 1); + } else { +#ifdef DISPLAY_CLASS + ui_task.soundBuzzer(UIEventType::contactMessage); +#endif + } +#ifdef DISPLAY_CLASS + ui_task.newMsg(path_len, from.name, text, offline_queue_len); +#endif +} + +void MyMesh::onMessageRecv(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_PLAIN, pkt, sender_timestamp, NULL, 0, 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::onSignedMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp, + const uint8_t *sender_prefix, const char *text) { + markConnectionActive(from); + // from.sync_since change needs to be persisted + dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); + queueMessage(from, TXT_TYPE_SIGNED_PLAIN, pkt, sender_timestamp, sender_prefix, 4, text); +} + +void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packet *pkt, uint32_t timestamp, + const char *text) { + int i = 0; + if (app_target_ver >= 3) { + out_frame[i++] = RESP_CODE_CHANNEL_MSG_RECV_V3; + out_frame[i++] = (int8_t)(pkt->getSNR() * 4); + out_frame[i++] = 0; // reserved1 + out_frame[i++] = 0; // reserved2 + } else { + out_frame[i++] = RESP_CODE_CHANNEL_MSG_RECV; + } + + uint8_t channel_idx = findChannelIdx(channel); + out_frame[i++] = channel_idx; + uint8_t path_len = out_frame[i++] = pkt->isRouteFlood() ? pkt->path_len : 0xFF; + + out_frame[i++] = TXT_TYPE_PLAIN; + memcpy(&out_frame[i], ×tamp, 4); + i += 4; + int tlen = strlen(text); // TODO: UTF-8 ?? + if (i + tlen > MAX_FRAME_SIZE) { + tlen = MAX_FRAME_SIZE - i; + } + memcpy(&out_frame[i], text, tlen); + i += tlen; + addToOfflineQueue(out_frame, i); + + if (_serial->isConnected()) { + uint8_t frame[1]; + frame[0] = PUSH_CODE_MSG_WAITING; // send push 'tickle' + _serial->writeFrame(frame, 1); + } else { +#ifdef DISPLAY_CLASS + ui_task.soundBuzzer(UIEventType::channelMessage); +#endif + } +#ifdef DISPLAY_CLASS + // Get the channel name from the channel index + const char *channel_name = "Unknown"; + ChannelDetails channel_details; + if (getChannel(channel_idx, channel_details)) { + channel_name = channel_details.name; + } + ui_task.newMsg(path_len, channel_name, text, offline_queue_len); +#endif +} + +uint8_t MyMesh::onContactRequest(const ContactInfo &contact, uint32_t sender_timestamp, const uint8_t *data, + uint8_t len, uint8_t *reply) { + if (data[0] == REQ_TYPE_GET_TELEMETRY_DATA) { + uint8_t permissions = 0; + uint8_t cp = contact.flags >> 1; // LSB used as 'favourite' bit (so only use upper bits) + + if (_prefs.telemetry_mode_base == TELEM_MODE_ALLOW_ALL) { + permissions = TELEM_PERM_BASE; + } else if (_prefs.telemetry_mode_base == TELEM_MODE_ALLOW_FLAGS) { + permissions = cp & TELEM_PERM_BASE; + } + + if (_prefs.telemetry_mode_loc == TELEM_MODE_ALLOW_ALL) { + permissions |= TELEM_PERM_LOCATION; + } else if (_prefs.telemetry_mode_loc == TELEM_MODE_ALLOW_FLAGS) { + permissions |= cp & TELEM_PERM_LOCATION; + } + + if (_prefs.telemetry_mode_env == TELEM_MODE_ALLOW_ALL) { + permissions |= TELEM_PERM_ENVIRONMENT; + } else if (_prefs.telemetry_mode_env == TELEM_MODE_ALLOW_FLAGS) { + permissions |= cp & TELEM_PERM_ENVIRONMENT; + } + + if (permissions & TELEM_PERM_BASE) { // only respond if base permission bit is set + telemetry.reset(); + telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f); + // query other sensors -- target specific + sensors.querySensors(permissions, telemetry); + + memcpy(reply, &sender_timestamp, + 4); // reflect sender_timestamp back in response packet (kind of like a 'tag') + + uint8_t tlen = telemetry.getSize(); + memcpy(&reply[4], telemetry.getBuffer(), tlen); + return 4 + tlen; + } + } + return 0; // unknown +} + +void MyMesh::onContactResponse(const ContactInfo &contact, const uint8_t *data, uint8_t len) { + uint32_t tag; + memcpy(&tag, data, 4); + + if (pending_login && memcmp(&pending_login, contact.id.pub_key, 4) == 0) { // check for login response + // yes, is response to pending sendLogin() + pending_login = 0; + + int i = 0; + if (memcmp(&data[4], "OK", 2) == 0) { // legacy Repeater login OK response + out_frame[i++] = PUSH_CODE_LOGIN_SUCCESS; + out_frame[i++] = 0; // legacy: is_admin = false + memcpy(&out_frame[i], contact.id.pub_key, 6); + i += 6; // pub_key_prefix + } else if (data[4] == RESP_SERVER_LOGIN_OK) { // new login response + uint16_t keep_alive_secs = ((uint16_t)data[5]) * 16; + if (keep_alive_secs > 0) { + startConnection(contact, keep_alive_secs); + } + out_frame[i++] = PUSH_CODE_LOGIN_SUCCESS; + out_frame[i++] = data[6]; // permissions (eg. is_admin) + memcpy(&out_frame[i], contact.id.pub_key, 6); + i += 6; // pub_key_prefix + memcpy(&out_frame[i], &tag, 4); + i += 4; // NEW: include server timestamp + } else { + out_frame[i++] = PUSH_CODE_LOGIN_FAIL; + out_frame[i++] = 0; // reserved + memcpy(&out_frame[i], contact.id.pub_key, 6); + i += 6; // pub_key_prefix + } + _serial->writeFrame(out_frame, i); + } else if (len > 4 && // check for status response + pending_status && + memcmp(&pending_status, contact.id.pub_key, 4) == 0 // legacy matching scheme + // FUTURE: tag == pending_status + ) { + pending_status = 0; + + int i = 0; + out_frame[i++] = PUSH_CODE_STATUS_RESPONSE; + out_frame[i++] = 0; // reserved + memcpy(&out_frame[i], contact.id.pub_key, 6); + i += 6; // pub_key_prefix + memcpy(&out_frame[i], &data[4], len - 4); + i += (len - 4); + _serial->writeFrame(out_frame, i); + } else if (len > 4 && tag == pending_telemetry) { // check for telemetry response + pending_telemetry = 0; + + int i = 0; + out_frame[i++] = PUSH_CODE_TELEMETRY_RESPONSE; + out_frame[i++] = 0; // reserved + memcpy(&out_frame[i], contact.id.pub_key, 6); + i += 6; // pub_key_prefix + memcpy(&out_frame[i], &data[4], len - 4); + i += (len - 4); + _serial->writeFrame(out_frame, i); + } +} + +void MyMesh::onRawDataRecv(mesh::Packet *packet) { + if (packet->payload_len + 4 > sizeof(out_frame)) { + MESH_DEBUG_PRINTLN("onRawDataRecv(), payload_len too long: %d", packet->payload_len); + return; + } + int i = 0; + out_frame[i++] = PUSH_CODE_RAW_DATA; + out_frame[i++] = (int8_t)(_radio->getLastSNR() * 4); + out_frame[i++] = (int8_t)(_radio->getLastRSSI()); + out_frame[i++] = 0xFF; // reserved (possibly path_len in future) + memcpy(&out_frame[i], packet->payload, packet->payload_len); + i += packet->payload_len; + + if (_serial->isConnected()) { + _serial->writeFrame(out_frame, i); + } else { + MESH_DEBUG_PRINTLN("onRawDataRecv(), data received while app offline"); + } +} + +void MyMesh::onTraceRecv(mesh::Packet *packet, uint32_t tag, uint32_t auth_code, uint8_t flags, + const uint8_t *path_snrs, const uint8_t *path_hashes, uint8_t path_len) { + int i = 0; + out_frame[i++] = PUSH_CODE_TRACE_DATA; + out_frame[i++] = 0; // reserved + out_frame[i++] = path_len; + out_frame[i++] = flags; + memcpy(&out_frame[i], &tag, 4); + i += 4; + memcpy(&out_frame[i], &auth_code, 4); + i += 4; + memcpy(&out_frame[i], path_hashes, path_len); + i += path_len; + memcpy(&out_frame[i], path_snrs, path_len); + i += path_len; + out_frame[i++] = (int8_t)(packet->getSNR() * 4); // extra/final SNR (to this node) + + if (_serial->isConnected()) { + _serial->writeFrame(out_frame, i); + } else { + MESH_DEBUG_PRINTLN("onTraceRecv(), data received while app offline"); + } +} + +uint32_t MyMesh::calcFloodTimeoutMillisFor(uint32_t pkt_airtime_millis) const { + return SEND_TIMEOUT_BASE_MILLIS + (FLOOD_SEND_TIMEOUT_FACTOR * pkt_airtime_millis); +} +uint32_t MyMesh::calcDirectTimeoutMillisFor(uint32_t pkt_airtime_millis, uint8_t path_len) const { + return SEND_TIMEOUT_BASE_MILLIS + + ((pkt_airtime_millis * DIRECT_SEND_PERHOP_FACTOR + DIRECT_SEND_PERHOP_EXTRA_MILLIS) * + (path_len + 1)); +} + +void MyMesh::onSendTimeout() {} + +MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store) + : BaseChatMesh(radio, *new ArduinoMillis(), rng, rtc, *new StaticPoolPacketManager(16), tables), + _serial(NULL), telemetry(MAX_PACKET_PAYLOAD - 4), _store(&store) { + _iter_started = false; + _cli_rescue = false; + offline_queue_len = 0; + app_target_ver = 0; + pending_login = pending_status = pending_telemetry = 0; + next_ack_idx = 0; + sign_data = NULL; + dirty_contacts_expiry = 0; + memset(advert_paths, 0, sizeof(advert_paths)); + + // defaults + memset(&_prefs, 0, sizeof(_prefs)); + _prefs.airtime_factor = 1.0; // one half + strcpy(_prefs.node_name, "NONAME"); + _prefs.freq = LORA_FREQ; + _prefs.sf = LORA_SF; + _prefs.bw = LORA_BW; + _prefs.cr = LORA_CR; + _prefs.tx_power_dbm = LORA_TX_POWER; + //_prefs.rx_delay_base = 10.0f; enable once new algo fixed +} + +void MyMesh::begin(bool has_display) { + BaseChatMesh::begin(); + + if (!_store->loadMainIdentity(self_id)) { + self_id = radio_new_identity(); // create new random identity + int count = 0; + while (count < 10 && (self_id.pub_key[0] == 0x00 || self_id.pub_key[0] == 0xFF)) { // reserved id hashes + self_id = radio_new_identity(); + count++; + } + _store->saveMainIdentity(self_id); + } + + // use hex of first 4 bytes of identity public key as default node name + char pub_key_hex[10]; + mesh::Utils::toHex(pub_key_hex, self_id.pub_key, 4); + strcpy(_prefs.node_name, pub_key_hex); + +// if name is provided as a build flag, use that as default node name instead +#ifdef ADVERT_NAME + strcpy(_prefs.node_name, ADVERT_NAME); +#endif + + // load persisted prefs + _store->loadPrefs(_prefs, sensors.node_lat, sensors.node_lon); + + // sanitise bad pref values + _prefs.rx_delay_base = constrain(_prefs.rx_delay_base, 0, 20.0f); + _prefs.airtime_factor = constrain(_prefs.airtime_factor, 0, 9.0f); + _prefs.freq = constrain(_prefs.freq, 400.0f, 2500.0f); + _prefs.bw = constrain(_prefs.bw, 62.5f, 500.0f); + _prefs.sf = constrain(_prefs.sf, 7, 12); + _prefs.cr = constrain(_prefs.cr, 5, 8); + _prefs.tx_power_dbm = constrain(_prefs.tx_power_dbm, 1, MAX_LORA_TX_POWER); + +#ifdef BLE_PIN_CODE + if (_prefs.ble_pin == 0) { +#ifdef DISPLAY_CLASS + if (has_display) { + StdRNG rng; + _active_ble_pin = rng.nextInt(100000, 999999); // random pin each session + } else { + _active_ble_pin = BLE_PIN_CODE; // otherwise static pin + } +#else + _active_ble_pin = BLE_PIN_CODE; // otherwise static pin +#endif + } else { + _active_ble_pin = _prefs.ble_pin; + } +#else + _active_ble_pin = 0; +#endif + + _store->loadContacts(this); + addChannel("Public", PUBLIC_GROUP_PSK); // pre-configure Andy's public channel + _store->loadChannels(this); + + radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); + radio_set_tx_power(_prefs.tx_power_dbm); +} + +const char *MyMesh::getNodeName() { + return _prefs.node_name; +} +NodePrefs *MyMesh::getNodePrefs() { + return &_prefs; +} +uint32_t MyMesh::getBLEPin() { + return _active_ble_pin; +} + +void MyMesh::startInterface(BaseSerialInterface &serial) { + _serial = &serial; + serial.enable(); +} + +void MyMesh::handleCmdFrame(size_t len) { + if (cmd_frame[0] == CMD_DEVICE_QEURY && len >= 2) { // sent when app establishes connection + app_target_ver = cmd_frame[1]; // which version of protocol does app understand + + int i = 0; + out_frame[i++] = RESP_CODE_DEVICE_INFO; + out_frame[i++] = FIRMWARE_VER_CODE; + out_frame[i++] = MAX_CONTACTS / 2; // v3+ + out_frame[i++] = MAX_GROUP_CHANNELS; // v3+ + memcpy(&out_frame[i], &_prefs.ble_pin, 4); + i += 4; + memset(&out_frame[i], 0, 12); + strcpy((char *)&out_frame[i], FIRMWARE_BUILD_DATE); + i += 12; + StrHelper::strzcpy((char *)&out_frame[i], board.getManufacturerName(), 40); + i += 40; + StrHelper::strzcpy((char *)&out_frame[i], FIRMWARE_VERSION, 20); + i += 20; + _serial->writeFrame(out_frame, i); + } else if (cmd_frame[0] == CMD_APP_START && + len >= 8) { // sent when app establishes connection, respond with node ID + // cmd_frame[1..7] reserved future + char *app_name = (char *)&cmd_frame[8]; + cmd_frame[len] = 0; // make app_name null terminated + MESH_DEBUG_PRINTLN("App %s connected", app_name); + + _iter_started = false; // stop any left-over ContactsIterator + int i = 0; + out_frame[i++] = RESP_CODE_SELF_INFO; + out_frame[i++] = ADV_TYPE_CHAT; // what this node Advert identifies as (maybe node's pronouns too?? :-) + out_frame[i++] = _prefs.tx_power_dbm; + out_frame[i++] = MAX_LORA_TX_POWER; + memcpy(&out_frame[i], self_id.pub_key, PUB_KEY_SIZE); + i += PUB_KEY_SIZE; + + int32_t lat, lon; + lat = (sensors.node_lat * 1000000.0); + lon = (sensors.node_lon * 1000000.0); + memcpy(&out_frame[i], &lat, 4); + i += 4; + memcpy(&out_frame[i], &lon, 4); + i += 4; + out_frame[i++] = 0; // reserved + out_frame[i++] = 0; // reserved + out_frame[i++] = (_prefs.telemetry_mode_env << 4) | (_prefs.telemetry_mode_loc << 2) | + (_prefs.telemetry_mode_base); // v5+ + out_frame[i++] = _prefs.manual_add_contacts; + + uint32_t freq = _prefs.freq * 1000; + memcpy(&out_frame[i], &freq, 4); + i += 4; + uint32_t bw = _prefs.bw * 1000; + memcpy(&out_frame[i], &bw, 4); + i += 4; + out_frame[i++] = _prefs.sf; + out_frame[i++] = _prefs.cr; + + int tlen = strlen(_prefs.node_name); // revisit: UTF_8 ?? + memcpy(&out_frame[i], _prefs.node_name, tlen); + i += tlen; + _serial->writeFrame(out_frame, i); + } else if (cmd_frame[0] == CMD_SEND_TXT_MSG && len >= 14) { + int i = 1; + uint8_t txt_type = cmd_frame[i++]; + uint8_t attempt = cmd_frame[i++]; + uint32_t msg_timestamp; + memcpy(&msg_timestamp, &cmd_frame[i], 4); + i += 4; + 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)) { + 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) { + result = sendCommandData(*recipient, msg_timestamp, attempt, text, est_timeout); + expected_ack = 0; // no Ack expected + } else { + result = sendMessage(*recipient, msg_timestamp, attempt, text, expected_ack, est_timeout); + } + // TODO: add expected ACK to table + if (result == MSG_SEND_FAILED) { + writeErrFrame(ERR_CODE_TABLE_FULL); + } else { + if (expected_ack) { + expected_ack_table[next_ack_idx].msg_sent = _ms->getMillis(); // add to circular table + expected_ack_table[next_ack_idx].ack = expected_ack; + next_ack_idx = (next_ack_idx + 1) % EXPECTED_ACK_TABLE_SIZE; + } + + out_frame[0] = RESP_CODE_SENT; + out_frame[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0; + memcpy(&out_frame[2], &expected_ack, 4); + memcpy(&out_frame[6], &est_timeout, 4); + _serial->writeFrame(out_frame, 10); + } + } else { + writeErrFrame(recipient == NULL + ? ERR_CODE_NOT_FOUND + : ERR_CODE_UNSUPPORTED_CMD); // unknown recipient, or unsuported TXT_TYPE_* + } + } else if (cmd_frame[0] == CMD_SEND_CHANNEL_TXT_MSG) { // send GroupChannel msg + int i = 1; + uint8_t txt_type = cmd_frame[i++]; // should be TXT_TYPE_PLAIN + uint8_t channel_idx = cmd_frame[i++]; + uint32_t msg_timestamp; + memcpy(&msg_timestamp, &cmd_frame[i], 4); + i += 4; + const char *text = (char *)&cmd_frame[i]; + + if (txt_type != TXT_TYPE_PLAIN) { + writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); + } else { + ChannelDetails channel; + bool success = getChannel(channel_idx, channel); + if (success && sendGroupMessage(msg_timestamp, channel.channel, _prefs.node_name, text, len - i)) { + writeOKFrame(); + } else { + writeErrFrame(ERR_CODE_NOT_FOUND); // bad channel_idx + } + } + } else if (cmd_frame[0] == CMD_GET_CONTACTS) { // get Contact list + if (_iter_started) { + writeErrFrame(ERR_CODE_BAD_STATE); // iterator is currently busy + } else { + if (len >= 5) { // has optional 'since' param + memcpy(&_iter_filter_since, &cmd_frame[1], 4); + } else { + _iter_filter_since = 0; + } + + uint8_t reply[5]; + reply[0] = RESP_CODE_CONTACTS_START; + uint32_t count = getNumContacts(); // total, NOT filtered count + memcpy(&reply[1], &count, 4); + _serial->writeFrame(reply, 5); + + // start iterator + _iter = startContactsIterator(); + _iter_started = true; + _most_recent_lastmod = 0; + } + } else if (cmd_frame[0] == CMD_SET_ADVERT_NAME && len >= 2) { + int nlen = len - 1; + if (nlen > sizeof(_prefs.node_name) - 1) nlen = sizeof(_prefs.node_name) - 1; // max len + memcpy(_prefs.node_name, &cmd_frame[1], nlen); + _prefs.node_name[nlen] = 0; // null terminator + savePrefs(); + writeOKFrame(); + } else if (cmd_frame[0] == CMD_SET_ADVERT_LATLON && len >= 9) { + int32_t lat, lon, alt = 0; + memcpy(&lat, &cmd_frame[1], 4); + memcpy(&lon, &cmd_frame[5], 4); + if (len >= 13) { + memcpy(&alt, &cmd_frame[9], 4); // for FUTURE support + } + if (lat <= 90 * 1E6 && lat >= -90 * 1E6 && lon <= 180 * 1E6 && lon >= -180 * 1E6) { + sensors.node_lat = ((double)lat) / 1000000.0; + sensors.node_lon = ((double)lon) / 1000000.0; + savePrefs(); + writeOKFrame(); + } else { + writeErrFrame(ERR_CODE_ILLEGAL_ARG); // invalid geo coordinate + } + } else if (cmd_frame[0] == CMD_GET_DEVICE_TIME) { + uint8_t reply[5]; + reply[0] = RESP_CODE_CURR_TIME; + uint32_t now = getRTCClock()->getCurrentTime(); + memcpy(&reply[1], &now, 4); + _serial->writeFrame(reply, 5); + } else if (cmd_frame[0] == CMD_SET_DEVICE_TIME && len >= 5) { + uint32_t secs; + memcpy(&secs, &cmd_frame[1], 4); + uint32_t curr = getRTCClock()->getCurrentTime(); + if (secs >= curr) { + getRTCClock()->setCurrentTime(secs); + writeOKFrame(); + } else { + writeErrFrame(ERR_CODE_ILLEGAL_ARG); + } + } else if (cmd_frame[0] == CMD_SEND_SELF_ADVERT) { + auto pkt = createSelfAdvert(_prefs.node_name, sensors.node_lat, sensors.node_lon); + if (pkt) { + if (len >= 2 && cmd_frame[1] == 1) { // optional param (1 = flood, 0 = zero hop) + sendFlood(pkt); + } else { + sendZeroHop(pkt); + } + writeOKFrame(); + } else { + writeErrFrame(ERR_CODE_TABLE_FULL); + } + } else if (cmd_frame[0] == CMD_RESET_PATH && len >= 1 + 32) { + uint8_t *pub_key = &cmd_frame[1]; + ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); + if (recipient) { + recipient->out_path_len = -1; + // recipient->lastmod = ?? shouldn't be needed, app already has this version of contact + dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); + writeOKFrame(); + } else { + writeErrFrame(ERR_CODE_NOT_FOUND); // unknown contact + } + } else if (cmd_frame[0] == CMD_ADD_UPDATE_CONTACT && len >= 1 + 32 + 2 + 1) { + uint8_t *pub_key = &cmd_frame[1]; + ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); + if (recipient) { + updateContactFromFrame(*recipient, cmd_frame, len); + // recipient->lastmod = ?? shouldn't be needed, app already has this version of contact + dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); + writeOKFrame(); + } else { + ContactInfo contact; + updateContactFromFrame(contact, cmd_frame, len); + contact.lastmod = getRTCClock()->getCurrentTime(); + contact.sync_since = 0; + if (addContact(contact)) { + dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); + writeOKFrame(); + } else { + writeErrFrame(ERR_CODE_TABLE_FULL); + } + } + } else if (cmd_frame[0] == CMD_REMOVE_CONTACT) { + uint8_t *pub_key = &cmd_frame[1]; + ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); + if (recipient && removeContact(*recipient)) { + dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); + writeOKFrame(); + } else { + writeErrFrame(ERR_CODE_NOT_FOUND); // not found, or unable to remove + } + } else if (cmd_frame[0] == CMD_SHARE_CONTACT) { + uint8_t *pub_key = &cmd_frame[1]; + ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); + if (recipient) { + if (shareContactZeroHop(*recipient)) { + writeOKFrame(); + } else { + writeErrFrame(ERR_CODE_TABLE_FULL); // unable to send + } + } else { + writeErrFrame(ERR_CODE_NOT_FOUND); + } + } else if (cmd_frame[0] == CMD_GET_CONTACT_BY_KEY) { + uint8_t *pub_key = &cmd_frame[1]; + ContactInfo *contact = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); + if (contact) { + writeContactRespFrame(RESP_CODE_CONTACT, *contact); + } else { + writeErrFrame(ERR_CODE_NOT_FOUND); // not found + } + } else if (cmd_frame[0] == CMD_EXPORT_CONTACT) { + if (len < 1 + PUB_KEY_SIZE) { + // export SELF + auto pkt = createSelfAdvert(_prefs.node_name, sensors.node_lat, sensors.node_lon); + if (pkt) { + pkt->header |= ROUTE_TYPE_FLOOD; // would normally be sent in this mode + + out_frame[0] = RESP_CODE_EXPORT_CONTACT; + uint8_t out_len = pkt->writeTo(&out_frame[1]); + releasePacket(pkt); // undo the obtainNewPacket() + _serial->writeFrame(out_frame, out_len + 1); + } else { + writeErrFrame(ERR_CODE_TABLE_FULL); // Error + } + } else { + uint8_t *pub_key = &cmd_frame[1]; + ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); + uint8_t out_len; + if (recipient && (out_len = exportContact(*recipient, &out_frame[1])) > 0) { + out_frame[0] = RESP_CODE_EXPORT_CONTACT; + _serial->writeFrame(out_frame, out_len + 1); + } else { + writeErrFrame(ERR_CODE_NOT_FOUND); // not found + } + } + } else if (cmd_frame[0] == CMD_IMPORT_CONTACT && len > 2 + 32 + 64) { + if (importContact(&cmd_frame[1], len - 1)) { + writeOKFrame(); + } else { + writeErrFrame(ERR_CODE_ILLEGAL_ARG); + } + } else if (cmd_frame[0] == CMD_SYNC_NEXT_MESSAGE) { + int out_len; + if ((out_len = getFromOfflineQueue(out_frame)) > 0) { + _serial->writeFrame(out_frame, out_len); +#ifdef DISPLAY_CLASS + ui_task.msgRead(offline_queue_len); +#endif + } else { + out_frame[0] = RESP_CODE_NO_MORE_MESSAGES; + _serial->writeFrame(out_frame, 1); + } + } else if (cmd_frame[0] == CMD_SET_RADIO_PARAMS) { + int i = 1; + uint32_t freq; + memcpy(&freq, &cmd_frame[i], 4); + i += 4; + uint32_t bw; + memcpy(&bw, &cmd_frame[i], 4); + i += 4; + uint8_t sf = cmd_frame[i++]; + uint8_t cr = cmd_frame[i++]; + + if (freq >= 300000 && freq <= 2500000 && sf >= 7 && sf <= 12 && cr >= 5 && cr <= 8 && bw >= 7000 && + bw <= 500000) { + _prefs.sf = sf; + _prefs.cr = cr; + _prefs.freq = (float)freq / 1000.0; + _prefs.bw = (float)bw / 1000.0; + savePrefs(); + + radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); + MESH_DEBUG_PRINTLN("OK: CMD_SET_RADIO_PARAMS: f=%d, bw=%d, sf=%d, cr=%d", freq, bw, (uint32_t)sf, + (uint32_t)cr); + + writeOKFrame(); + } else { + MESH_DEBUG_PRINTLN("Error: CMD_SET_RADIO_PARAMS: f=%d, bw=%d, sf=%d, cr=%d", freq, bw, (uint32_t)sf, + (uint32_t)cr); + writeErrFrame(ERR_CODE_ILLEGAL_ARG); + } + } else if (cmd_frame[0] == CMD_SET_RADIO_TX_POWER) { + if (cmd_frame[1] > MAX_LORA_TX_POWER) { + writeErrFrame(ERR_CODE_ILLEGAL_ARG); + } else { + _prefs.tx_power_dbm = cmd_frame[1]; + savePrefs(); + radio_set_tx_power(_prefs.tx_power_dbm); + writeOKFrame(); + } + } else if (cmd_frame[0] == CMD_SET_TUNING_PARAMS) { + int i = 1; + uint32_t rx, af; + memcpy(&rx, &cmd_frame[i], 4); + i += 4; + memcpy(&af, &cmd_frame[i], 4); + i += 4; + _prefs.rx_delay_base = ((float)rx) / 1000.0f; + _prefs.airtime_factor = ((float)af) / 1000.0f; + savePrefs(); + writeOKFrame(); + } else if (cmd_frame[0] == CMD_SET_OTHER_PARAMS) { + _prefs.manual_add_contacts = cmd_frame[1]; + if (len >= 3) { + _prefs.telemetry_mode_base = cmd_frame[2] & 0x03; // v5+ + _prefs.telemetry_mode_loc = (cmd_frame[2] >> 2) & 0x03; + _prefs.telemetry_mode_env = (cmd_frame[2] >> 4) & 0x03; + } + savePrefs(); + writeOKFrame(); + } else if (cmd_frame[0] == CMD_REBOOT && memcmp(&cmd_frame[1], "reboot", 6) == 0) { + if (dirty_contacts_expiry) { // is there are pending dirty contacts write needed? + saveContacts(); + } + board.reboot(); + } else if (cmd_frame[0] == CMD_GET_BATTERY_VOLTAGE) { + uint8_t reply[3]; + reply[0] = RESP_CODE_BATTERY_VOLTAGE; + uint16_t battery_millivolts = board.getBattMilliVolts(); + memcpy(&reply[1], &battery_millivolts, 2); + _serial->writeFrame(reply, 3); + } else if (cmd_frame[0] == CMD_EXPORT_PRIVATE_KEY) { +#if ENABLE_PRIVATE_KEY_EXPORT + uint8_t reply[65]; + reply[0] = RESP_CODE_PRIVATE_KEY; + self_id.writeTo(&reply[1], 64); + _serial->writeFrame(reply, 65); +#else + writeDisabledFrame(); +#endif + } else if (cmd_frame[0] == CMD_IMPORT_PRIVATE_KEY && len >= 65) { +#if ENABLE_PRIVATE_KEY_IMPORT + mesh::LocalIdentity identity; + identity.readFrom(&cmd_frame[1], 64); + if (_store->saveMainIdentity(identity)) { + self_id = identity; + writeOKFrame(); + } else { + writeErrFrame(ERR_CODE_FILE_IO_ERROR); + } +#else + writeDisabledFrame(); +#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(); + } else { + writeErrFrame(ERR_CODE_TABLE_FULL); + } + } else { + writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); // flood, not supported (yet) + } + } else if (cmd_frame[0] == CMD_SEND_LOGIN && len >= 1 + PUB_KEY_SIZE) { + uint8_t *pub_key = &cmd_frame[1]; + ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); + char *password = (char *)&cmd_frame[1 + PUB_KEY_SIZE]; + cmd_frame[len] = 0; // ensure null terminator in password + if (recipient) { + uint32_t est_timeout; + int result = sendLogin(*recipient, password, est_timeout); + if (result == MSG_SEND_FAILED) { + writeErrFrame(ERR_CODE_TABLE_FULL); + } else { + pending_telemetry = pending_status = 0; + memcpy(&pending_login, recipient->id.pub_key, 4); // match this to onContactResponse() + out_frame[0] = RESP_CODE_SENT; + out_frame[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0; + memcpy(&out_frame[2], &pending_login, 4); + memcpy(&out_frame[6], &est_timeout, 4); + _serial->writeFrame(out_frame, 10); + } + } else { + writeErrFrame(ERR_CODE_NOT_FOUND); // contact not found + } + } else if (cmd_frame[0] == CMD_SEND_STATUS_REQ && len >= 1 + PUB_KEY_SIZE) { + uint8_t *pub_key = &cmd_frame[1]; + ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); + if (recipient) { + uint32_t tag, est_timeout; + int result = sendRequest(*recipient, REQ_TYPE_GET_STATUS, tag, est_timeout); + if (result == MSG_SEND_FAILED) { + writeErrFrame(ERR_CODE_TABLE_FULL); + } else { + pending_telemetry = pending_login = 0; + // FUTURE: pending_status = tag; // match this in onContactResponse() + memcpy(&pending_status, recipient->id.pub_key, 4); // legacy matching scheme + out_frame[0] = RESP_CODE_SENT; + out_frame[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0; + memcpy(&out_frame[2], &tag, 4); + memcpy(&out_frame[6], &est_timeout, 4); + _serial->writeFrame(out_frame, 10); + } + } else { + writeErrFrame(ERR_CODE_NOT_FOUND); // contact not found + } + } else if (cmd_frame[0] == CMD_SEND_TELEMETRY_REQ && len >= 4 + PUB_KEY_SIZE) { + uint8_t *pub_key = &cmd_frame[4]; + ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); + if (recipient) { + uint32_t tag, est_timeout; + int result = sendRequest(*recipient, REQ_TYPE_GET_TELEMETRY_DATA, tag, est_timeout); + if (result == MSG_SEND_FAILED) { + writeErrFrame(ERR_CODE_TABLE_FULL); + } else { + pending_status = pending_login = 0; + pending_telemetry = tag; // match this in onContactResponse() + out_frame[0] = RESP_CODE_SENT; + out_frame[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0; + memcpy(&out_frame[2], &tag, 4); + memcpy(&out_frame[6], &est_timeout, 4); + _serial->writeFrame(out_frame, 10); + } + } else { + writeErrFrame(ERR_CODE_NOT_FOUND); // contact not found + } + } else if (cmd_frame[0] == CMD_SEND_TELEMETRY_REQ && len == 4) { // 'self' telemetry request + telemetry.reset(); + telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f); + // query other sensors -- target specific + sensors.querySensors(0xFF, telemetry); + + int i = 0; + out_frame[i++] = PUSH_CODE_TELEMETRY_RESPONSE; + out_frame[i++] = 0; // reserved + memcpy(&out_frame[i], self_id.pub_key, 6); + i += 6; // pub_key_prefix + uint8_t tlen = telemetry.getSize(); + memcpy(&out_frame[i], telemetry.getBuffer(), tlen); + i += tlen; + _serial->writeFrame(out_frame, i); + } else if (cmd_frame[0] == CMD_HAS_CONNECTION && len >= 1 + PUB_KEY_SIZE) { + uint8_t *pub_key = &cmd_frame[1]; + if (hasConnectionTo(pub_key)) { + writeOKFrame(); + } else { + writeErrFrame(ERR_CODE_NOT_FOUND); + } + } else if (cmd_frame[0] == CMD_LOGOUT && len >= 1 + PUB_KEY_SIZE) { + uint8_t *pub_key = &cmd_frame[1]; + stopConnection(pub_key); + writeOKFrame(); + } else if (cmd_frame[0] == CMD_GET_CHANNEL && len >= 2) { + uint8_t channel_idx = cmd_frame[1]; + ChannelDetails channel; + if (getChannel(channel_idx, channel)) { + int i = 0; + out_frame[i++] = RESP_CODE_CHANNEL_INFO; + out_frame[i++] = channel_idx; + strcpy((char *)&out_frame[i], channel.name); + i += 32; + memcpy(&out_frame[i], channel.channel.secret, 16); + i += 16; // NOTE: only 128-bit supported + _serial->writeFrame(out_frame, i); + } else { + writeErrFrame(ERR_CODE_NOT_FOUND); + } + } else if (cmd_frame[0] == CMD_SET_CHANNEL && len >= 2 + 32 + 32) { + writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); // not supported (yet) + } else if (cmd_frame[0] == CMD_SET_CHANNEL && len >= 2 + 32 + 16) { + uint8_t channel_idx = cmd_frame[1]; + ChannelDetails channel; + StrHelper::strncpy(channel.name, (char *)&cmd_frame[2], 32); + memset(channel.channel.secret, 0, sizeof(channel.channel.secret)); + memcpy(channel.channel.secret, &cmd_frame[2 + 32], 16); // NOTE: only 128-bit supported + if (setChannel(channel_idx, channel)) { + saveChannels(); + writeOKFrame(); + } else { + writeErrFrame(ERR_CODE_NOT_FOUND); // bad channel_idx + } + } else if (cmd_frame[0] == CMD_SIGN_START) { + out_frame[0] = RESP_CODE_SIGN_START; + out_frame[1] = 0; // reserved + uint32_t len = MAX_SIGN_DATA_LEN; + memcpy(&out_frame[2], &len, 4); + _serial->writeFrame(out_frame, 6); + + if (sign_data) { + free(sign_data); + } + sign_data = (uint8_t *)malloc(MAX_SIGN_DATA_LEN); + sign_data_len = 0; + } else if (cmd_frame[0] == CMD_SIGN_DATA && len > 1) { + if (sign_data == NULL || sign_data_len + (len - 1) > MAX_SIGN_DATA_LEN) { + writeErrFrame(sign_data == NULL ? ERR_CODE_BAD_STATE : ERR_CODE_TABLE_FULL); // error: too long + } else { + memcpy(&sign_data[sign_data_len], &cmd_frame[1], len - 1); + sign_data_len += (len - 1); + writeOKFrame(); + } + } else if (cmd_frame[0] == CMD_SIGN_FINISH) { + if (sign_data) { + self_id.sign(&out_frame[1], sign_data, sign_data_len); + + free(sign_data); // don't need sign_data now + sign_data = NULL; + + out_frame[0] = RESP_CODE_SIGNATURE; + _serial->writeFrame(out_frame, 1 + SIGNATURE_SIZE); + } else { + writeErrFrame(ERR_CODE_BAD_STATE); + } + } else if (cmd_frame[0] == CMD_SEND_TRACE_PATH && len > 10 && len - 10 < MAX_PATH_SIZE) { + uint32_t tag, auth; + memcpy(&tag, &cmd_frame[1], 4); + memcpy(&auth, &cmd_frame[5], 4); + auto pkt = createTrace(tag, auth, cmd_frame[9]); + if (pkt) { + uint8_t path_len = len - 10; + sendDirect(pkt, &cmd_frame[10], path_len); + + uint32_t t = _radio->getEstAirtimeFor(pkt->payload_len + pkt->path_len + 2); + uint32_t est_timeout = calcDirectTimeoutMillisFor(t, path_len); + + out_frame[0] = RESP_CODE_SENT; + out_frame[1] = 0; + memcpy(&out_frame[2], &tag, 4); + memcpy(&out_frame[6], &est_timeout, 4); + _serial->writeFrame(out_frame, 10); + } else { + writeErrFrame(ERR_CODE_TABLE_FULL); + } + } else if (cmd_frame[0] == CMD_SET_DEVICE_PIN && len >= 5) { + + // get pin from command frame + uint32_t pin; + memcpy(&pin, &cmd_frame[1], 4); + + // ensure pin is zero, or a valid 6 digit pin + if (pin == 0 || (pin >= 100000 && pin <= 999999)) { + _prefs.ble_pin = pin; + savePrefs(); + writeOKFrame(); + } else { + writeErrFrame(ERR_CODE_ILLEGAL_ARG); + } + } else if (cmd_frame[0] == CMD_GET_CUSTOM_VARS) { + out_frame[0] = RESP_CODE_CUSTOM_VARS; + char *dp = (char *)&out_frame[1]; + for (int i = 0; i < sensors.getNumSettings() && dp - (char *)&out_frame[1] < 140; i++) { + if (i > 0) { + *dp++ = ','; + } + strcpy(dp, sensors.getSettingName(i)); + dp = strchr(dp, 0); + *dp++ = ':'; + strcpy(dp, sensors.getSettingValue(i)); + dp = strchr(dp, 0); + } + _serial->writeFrame(out_frame, dp - (char *)out_frame); + } else if (cmd_frame[0] == CMD_SET_CUSTOM_VAR && len >= 4) { + cmd_frame[len] = 0; + char *sp = (char *)&cmd_frame[1]; + char *np = strchr(sp, ':'); // look for separator char + if (np) { + *np++ = 0; // modify 'cmd_frame', replace ':' with null + bool success = sensors.setSettingValue(sp, np); + if (success) { + writeOKFrame(); + } else { + writeErrFrame(ERR_CODE_ILLEGAL_ARG); + } + } else { + writeErrFrame(ERR_CODE_ILLEGAL_ARG); + } + } else if (cmd_frame[0] == CMD_GET_ADVERT_PATH && len >= PUB_KEY_SIZE+2) { + // FUTURE use: uint8_t reserved = cmd_frame[1]; + uint8_t *pub_key = &cmd_frame[2]; + AdvertPath* found = NULL; + for (int i = 0; i < ADVERT_PATH_TABLE_SIZE; i++) { + auto p = &advert_paths[i]; + if (memcmp(p->pubkey_prefix, pub_key, sizeof(p->pubkey_prefix)) == 0) { + found = p; + break; + } + } + if (found) { + out_frame[0] = RESP_CODE_ADVERT_PATH; + memcpy(&out_frame[1], &found->recv_timestamp, 4); + out_frame[5] = found->path_len; + memcpy(&out_frame[6], found->path, found->path_len); + _serial->writeFrame(out_frame, 6 + found->path_len); + } else { + writeErrFrame(ERR_CODE_NOT_FOUND); + } + } else { + writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); + MESH_DEBUG_PRINTLN("ERROR: unknown command: %02X", cmd_frame[0]); + } +} + +void MyMesh::enterCLIRescue() { + _cli_rescue = true; + cli_command[0] = 0; + Serial.println("========= CLI Rescue ========="); +} + +void MyMesh::checkCLIRescueCmd() { + int len = strlen(cli_command); + while (Serial.available() && len < sizeof(cli_command)-1) { + char c = Serial.read(); + if (c != '\n') { + cli_command[len++] = c; + cli_command[len] = 0; + } + Serial.print(c); // echo + } + if (len == sizeof(cli_command)-1) { // command buffer full + cli_command[sizeof(cli_command)-1] = '\r'; + } + + 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); + } + } else if (strcmp(cli_command, "rebuild") == 0) { + bool success = _store->formatFileSystem(); + if (success) { + _store->saveMainIdentity(self_id); + savePrefs(); + saveContacts(); + saveChannels(); + Serial.println(" > erase and rebuild done"); + } else { + Serial.println(" Error: erase failed"); + } + } else if (strcmp(cli_command, "erase") == 0) { + bool success = _store->formatFileSystem(); + if (success) { + Serial.println(" > erase done"); + } else { + Serial.println(" Error: erase failed"); + } + } else if (memcmp(cli_command, "ls", 2) == 0) { + + // get path from command e.g: "ls /adafruit" + const char *path = &cli_command[3]; + + // log each file and directory + File root = _store->openRead(path); + if(root){ + File file = root.openNextFile(); + while (file) { + + if (file.isDirectory()) { + Serial.printf("[dir] %s\n", file.name()); + } else { + Serial.printf("[file] %s (%d bytes)\n", file.name(), file.size()); + } + + // move to next file + file = root.openNextFile(); + + } + root.close(); + } + + } else if (memcmp(cli_command, "cat", 3) == 0) { + + // get path from command e.g: "cat /contacts3" + const char *path = &cli_command[4]; + + // log file content as hex + File file = _store->openRead(path); + if(file){ + + // get file content + int file_size = file.available(); + uint8_t buffer[file_size]; + file.read(buffer, file_size); + + // print hex + mesh::Utils::printHex(Serial, buffer, file_size); + Serial.print("\n"); + + file.close(); + + } + + } else if (memcmp(cli_command, "rm ", 3) == 0) { + + // get path from command e.g: "rm /adv_blobs" + const char *path = &cli_command[4]; + + // ensure path is not empty, or root dir + if(!path || strlen(path) == 0 || strcmp(path, "/") == 0){ + Serial.println("Invalid path provided"); + } else { + + // remove file + bool removed = _store->removeFile(path); + if(removed){ + Serial.println("File removed"); + } else { + Serial.println("Failed to remove file"); + } + + } + + } else if (strcmp(cli_command, "reboot") == 0) { + board.reboot(); // doesn't return + } else { + Serial.println(" Error: unknown command"); + } + + cli_command[0] = 0; // reset command buffer + } +} + +void MyMesh::checkSerialInterface() { + size_t len = _serial->checkRecvFrame(cmd_frame); + if (len > 0) { + handleCmdFrame(len); + } else if (_iter_started // check if our ContactsIterator is 'running' + && !_serial->isWriteBusy() // don't spam the Serial Interface too quickly! + ) { + ContactInfo contact; + if (_iter.hasNext(this, contact)) { + if (contact.lastmod > _iter_filter_since) { // apply the 'since' filter + writeContactRespFrame(RESP_CODE_CONTACT, contact); + if (contact.lastmod > _most_recent_lastmod) { + _most_recent_lastmod = contact.lastmod; // save for the RESP_CODE_END_OF_CONTACTS frame + } + } + } else { // EOF + out_frame[0] = RESP_CODE_END_OF_CONTACTS; + memcpy(&out_frame[1], &_most_recent_lastmod, + 4); // include the most recent lastmod, so app can update their 'since' + _serial->writeFrame(out_frame, 5); + _iter_started = false; + } + } else if (!_serial->isWriteBusy()) { + checkConnections(); + } +} + +void MyMesh::loop() { + BaseChatMesh::loop(); + + if (_cli_rescue) { + checkCLIRescueCmd(); + } else { + checkSerialInterface(); + } + + // is there are pending dirty contacts write needed? + if (dirty_contacts_expiry && millisHasNowPassed(dirty_contacts_expiry)) { + saveContacts(); + dirty_contacts_expiry = 0; + } + +#ifdef DISPLAY_CLASS + ui_task.setHasConnection(_serial->isConnected()); + ui_task.loop(); +#endif +} + +bool MyMesh::advert() { + auto pkt = createSelfAdvert(_prefs.node_name, sensors.node_lat, sensors.node_lon); + if (pkt) { + sendZeroHop(pkt); + return true; + } else { + return false; + } +} \ No newline at end of file diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h new file mode 100644 index 00000000..407b11d5 --- /dev/null +++ b/examples/companion_radio/MyMesh.h @@ -0,0 +1,210 @@ +#pragma once + +#include +#include +#ifdef DISPLAY_CLASS +#include "UITask.h" +#endif + +/*------------ Frame Protocol --------------*/ +#define FIRMWARE_VER_CODE 5 + +#ifndef FIRMWARE_BUILD_DATE +#define FIRMWARE_BUILD_DATE "7 Jun 2025" +#endif + +#ifndef FIRMWARE_VERSION +#define FIRMWARE_VERSION "v1.7.0" +#endif + +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) +#include +#elif defined(RP2040_PLATFORM) +#include +#elif defined(ESP32) +#include +#endif + +#include "DataStore.h" +#include "NodePrefs.h" + +#include +#include +#include +#include +#include +#include +#include + +/* ---------------------------------- CONFIGURATION ------------------------------------- */ + +#ifndef LORA_FREQ +#define LORA_FREQ 915.0 +#endif +#ifndef LORA_BW +#define LORA_BW 250 +#endif +#ifndef LORA_SF +#define LORA_SF 10 +#endif +#ifndef LORA_CR +#define LORA_CR 5 +#endif +#ifndef LORA_TX_POWER +#define LORA_TX_POWER 20 +#endif +#ifndef MAX_LORA_TX_POWER +#define MAX_LORA_TX_POWER LORA_TX_POWER +#endif + +#ifndef MAX_CONTACTS +#define MAX_CONTACTS 100 +#endif + +#ifndef OFFLINE_QUEUE_SIZE +#define OFFLINE_QUEUE_SIZE 16 +#endif + +#ifndef BLE_NAME_PREFIX +#define BLE_NAME_PREFIX "MeshCore-" +#endif + +#include + +/* -------------------------------------------------------------------------------------- */ + +#define REQ_TYPE_GET_STATUS 0x01 // same as _GET_STATS +#define REQ_TYPE_KEEP_ALIVE 0x02 +#define REQ_TYPE_GET_TELEMETRY_DATA 0x03 + +class MyMesh : public BaseChatMesh, public DataStoreHost { +public: + MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store); + + void begin(bool has_display); + void startInterface(BaseSerialInterface &serial); + + const char *getNodeName(); + NodePrefs *getNodePrefs(); + uint32_t getBLEPin(); + + void loop(); + void handleCmdFrame(size_t len); + bool advert(); + void enterCLIRescue(); + +protected: + float getAirtimeBudgetFactor() const override; + int getInterferenceThreshold() const override; + int calcRxDelay(float score, uint32_t air_time) const override; + + void logRxRaw(float snr, float rssi, const uint8_t raw[], int len) override; + bool isAutoAddEnabled() const override; + void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) override; + void onContactPathUpdated(const ContactInfo &contact) override; + bool processAck(const uint8_t *data) override; + void queueMessage(const ContactInfo &from, uint8_t txt_type, mesh::Packet *pkt, uint32_t sender_timestamp, + const uint8_t *extra, int extra_len, const char *text); + + void onMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp, + const char *text) override; + void onCommandDataRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp, + const char *text) 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, + const char *text) override; + + uint8_t onContactRequest(const ContactInfo &contact, uint32_t sender_timestamp, const uint8_t *data, + uint8_t len, uint8_t *reply) override; + void onContactResponse(const ContactInfo &contact, const uint8_t *data, uint8_t len) override; + void onRawDataRecv(mesh::Packet *packet) override; + void onTraceRecv(mesh::Packet *packet, uint32_t tag, uint32_t auth_code, uint8_t flags, + const uint8_t *path_snrs, const uint8_t *path_hashes, uint8_t path_len) override; + + uint32_t calcFloodTimeoutMillisFor(uint32_t pkt_airtime_millis) const override; + uint32_t calcDirectTimeoutMillisFor(uint32_t pkt_airtime_millis, uint8_t path_len) const override; + void onSendTimeout() override; + + // DataStoreHost methods + bool onContactLoaded(const ContactInfo& contact) override { return addContact(contact); } + bool getContactForSave(uint32_t idx, ContactInfo& contact) override { return getContactByIdx(idx, contact); } + bool onChannelLoaded(uint8_t channel_idx, const ChannelDetails& ch) override { return setChannel(channel_idx, ch); } + bool getChannelForSave(uint8_t channel_idx, ChannelDetails& ch) override { return getChannel(channel_idx, ch); } + +private: + void writeOKFrame(); + void writeErrFrame(uint8_t err_code); + void writeDisabledFrame(); + void writeContactRespFrame(uint8_t code, const ContactInfo &contact); + void updateContactFromFrame(ContactInfo &contact, const uint8_t *frame, int len); + void addToOfflineQueue(const uint8_t frame[], int len); + int getFromOfflineQueue(uint8_t frame[]); + int getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]) override { + return _store->getBlobByKey(key, key_len, dest_buf); + } + bool putBlobByKey(const uint8_t key[], int key_len, const uint8_t src_buf[], int len) override { + return _store->putBlobByKey(key, key_len, src_buf, len); + } + + void checkCLIRescueCmd(); + void checkSerialInterface(); + + // helpers, short-cuts + void savePrefs() { _store->savePrefs(_prefs, sensors.node_lat, sensors.node_lon); } + void saveChannels() { _store->saveChannels(this); } + void saveContacts() { _store->saveContacts(this); } + +private: + DataStore* _store; + NodePrefs _prefs; + uint32_t pending_login; + uint32_t pending_status; + uint32_t pending_telemetry; + BaseSerialInterface *_serial; + + ContactsIterator _iter; + uint32_t _iter_filter_since; + uint32_t _most_recent_lastmod; + uint32_t _active_ble_pin; + bool _iter_started; + bool _cli_rescue; + char cli_command[80]; + uint8_t app_target_ver; + uint8_t *sign_data; + uint32_t sign_data_len; + unsigned long dirty_contacts_expiry; + + uint8_t cmd_frame[MAX_FRAME_SIZE + 1]; + uint8_t out_frame[MAX_FRAME_SIZE + 1]; + CayenneLPP telemetry; + + struct Frame { + uint8_t len; + uint8_t buf[MAX_FRAME_SIZE]; + }; + int offline_queue_len; + Frame offline_queue[OFFLINE_QUEUE_SIZE]; + + struct AckTableEntry { + unsigned long msg_sent; + uint32_t ack; + }; + #define EXPECTED_ACK_TABLE_SIZE 8 + AckTableEntry expected_ack_table[EXPECTED_ACK_TABLE_SIZE]; // circular table + int next_ack_idx; + + struct AdvertPath { + uint8_t pubkey_prefix[7]; + uint8_t path_len; + uint32_t recv_timestamp; + uint8_t path[MAX_PATH_SIZE]; + }; + #define ADVERT_PATH_TABLE_SIZE 16 + AdvertPath advert_paths[ADVERT_PATH_TABLE_SIZE]; // circular table +}; + +extern MyMesh the_mesh; +#ifdef DISPLAY_CLASS +extern UITask ui_task; +#endif \ No newline at end of file diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 09d04266..4fd0fd3b 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -1,6 +1,4 @@ -#ifndef NODE_PREFS_H -#define NODE_PREFS_H - +#pragma once #include // For uint8_t, uint32_t #define TELEM_MODE_DENY 0 @@ -22,6 +20,4 @@ struct NodePrefs { // persisted to file uint8_t telemetry_mode_env; float rx_delay_base; uint32_t ble_pin; -}; - -#endif // NODE_PREFS_H \ No newline at end of file +}; \ No newline at end of file diff --git a/examples/companion_radio/UITask.cpp b/examples/companion_radio/UITask.cpp index 5ff5f140..01906f90 100644 --- a/examples/companion_radio/UITask.cpp +++ b/examples/companion_radio/UITask.cpp @@ -2,9 +2,10 @@ #include #include #include "NodePrefs.h" +#include "MyMesh.h" #define AUTO_OFF_MILLIS 15000 // 15 seconds -#define BOOT_SCREEN_MILLIS 4000 // 4 seconds +#define BOOT_SCREEN_MILLIS 3000 // 3 seconds #ifdef PIN_STATUS_LED #define LED_ON_MILLIS 20 @@ -33,26 +34,25 @@ static const uint8_t meshcore_logo [] PROGMEM = { 0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfc, 0x3c, 0x0e, 0x1f, 0xf8, 0xff, 0xf8, 0x70, 0x3c, 0x7f, 0xf8, }; -void UITask::begin(DisplayDriver* display, NodePrefs* node_prefs, const char* build_date, const char* firmware_version, uint32_t pin_code) { +void UITask::begin(DisplayDriver* display, NodePrefs* node_prefs) { _display = display; _auto_off = millis() + AUTO_OFF_MILLIS; clearMsgPreview(); _node_prefs = node_prefs; - _pin_code = pin_code; if (_display != NULL) { _display->turnOn(); } // strip off dash and commit hash by changing dash to null terminator // e.g: v1.2.3-abcdef -> v1.2.3 - char *version = strdup(firmware_version); + char *version = strdup(FIRMWARE_VERSION); char *dash = strchr(version, '-'); - if(dash){ + if (dash) { *dash = 0; } // v1.2.3 (1 Jan 2025) - sprintf(_version_info, "%s (%s)", version, build_date); + sprintf(_version_info, "%s (%s)", version, FIRMWARE_BUILD_DATE); #ifdef PIN_BUZZER buzzer.begin(); @@ -75,6 +75,7 @@ void UITask::begin(DisplayDriver* display, NodePrefs* node_prefs, const char* bu _userButton->onLongPress([this]() { handleButtonLongPress(); }); _userButton->onAnyPress([this]() { handleButtonAnyPress(); }); #endif + ui_started_at = millis(); } void UITask::soundBuzzer(UIEventType bet) { @@ -161,7 +162,16 @@ void UITask::renderCurrScreen() { if (_display == NULL) return; // assert() ?? char tmp[80]; - if (_origin[0] && _msg[0]) { // message preview + if (_alert[0]) { + _display->setTextSize(1.4); + uint16_t textWidth = _display->getTextWidth(_alert); + _display->setCursor((_display->width() - textWidth) / 2, 22); + _display->setColor(DisplayDriver::GREEN); + _display->print(_alert); + _alert[0] = 0; + _need_refresh = true; + return; + } else if (_origin[0] && _msg[0]) { // message preview // render message preview _display->setCursor(0, 0); _display->setTextSize(1); @@ -181,7 +191,7 @@ void UITask::renderCurrScreen() { sprintf(tmp, "%d", _msgcount); _display->print(tmp); _display->setColor(DisplayDriver::YELLOW); // last color will be kept on T114 - } else if (millis() < BOOT_SCREEN_MILLIS) { // boot screen + } else if ((millis() - ui_started_at) < BOOT_SCREEN_MILLIS) { // boot screen // meshcore logo _display->setColor(DisplayDriver::BLUE); int logoWidth = 128; @@ -215,11 +225,11 @@ void UITask::renderCurrScreen() { _display->print(tmp); // BT pin - if (!_connected && _pin_code != 0) { + if (!_connected && the_mesh.getBLEPin() != 0) { _display->setColor(DisplayDriver::RED); _display->setTextSize(2); _display->setCursor(0, 43); - sprintf(tmp, "Pin:%d", _pin_code); + sprintf(tmp, "Pin:%d", the_mesh.getBLEPin()); _display->print(tmp); _display->setColor(DisplayDriver::GREEN); } else { @@ -292,7 +302,7 @@ void UITask::loop() { if (_display != NULL && _display->isOn()) { static bool _firstBoot = true; - if(_firstBoot && millis() >= BOOT_SCREEN_MILLIS) { + if(_firstBoot && (millis() - ui_started_at) >= BOOT_SCREEN_MILLIS) { _need_refresh = true; _firstBoot = false; } @@ -334,14 +344,27 @@ void UITask::handleButtonShortPress() { // Otherwise, refresh the display _need_refresh = true; } + } else { + _need_refresh = true; // display just turned on, so we need to refresh } // Note: Display turn-on and auto-off timer extension are handled by handleButtonAnyPress } } void UITask::handleButtonDoublePress() { - MESH_DEBUG_PRINTLN("UITask: double press triggered"); - // Not implemented. TODO: possibly send an advert here? + MESH_DEBUG_PRINTLN("UITask: double press triggered, sending advert"); + // ADVERT + #ifdef PIN_BUZZER + soundBuzzer(UIEventType::ack); + #endif + if (the_mesh.advert()) { + MESH_DEBUG_PRINTLN("Advert sent!"); + sprintf(_alert, "Advert sent!"); + } else { + MESH_DEBUG_PRINTLN("Advert failed!"); + sprintf(_alert, "Advert failed.."); + } + _need_refresh = true; } void UITask::handleButtonTriplePress() { @@ -351,14 +374,20 @@ void UITask::handleButtonTriplePress() { if (buzzer.isQuiet()) { buzzer.quiet(false); soundBuzzer(UIEventType::ack); + sprintf(_alert, "Buzzer: ON"); } else { - soundBuzzer(UIEventType::ack); buzzer.quiet(true); + sprintf(_alert, "Buzzer: OFF"); } + _need_refresh = true; #endif } void UITask::handleButtonLongPress() { MESH_DEBUG_PRINTLN("UITask: long press triggered"); - shutdown(); + if (millis() - ui_started_at < 8000) { // long press in first 8 seconds since startup -> CLI/rescue + the_mesh.enterCLIRescue(); + } else { + shutdown(); + } } \ No newline at end of file diff --git a/examples/companion_radio/UITask.h b/examples/companion_radio/UITask.h index acf5237e..93a2ef89 100644 --- a/examples/companion_radio/UITask.h +++ b/examples/companion_radio/UITask.h @@ -29,14 +29,15 @@ class UITask { #endif unsigned long _next_refresh, _auto_off; bool _connected; - uint32_t _pin_code; NodePrefs* _node_prefs; char _version_info[32]; char _origin[62]; char _msg[80]; + char _alert[80]; int _msgcount; bool _need_refresh = true; bool _displayWasOn = false; // Track display state before button press + unsigned long ui_started_at; // Button handlers #if defined(PIN_USER_BTN) || defined(PIN_USER_BTN_ANA) @@ -58,10 +59,11 @@ class UITask { public: UITask(mesh::MainBoard* board) : _board(board), _display(NULL) { - _next_refresh = 0; + _next_refresh = 0; + ui_started_at = 0; _connected = false; } - void begin(DisplayDriver* display, NodePrefs* node_prefs, const char* build_date, const char* firmware_version, uint32_t pin_code); + void begin(DisplayDriver* display, NodePrefs* node_prefs); void setHasConnection(bool connected) { _connected = connected; } bool hasDisplay() const { return _display != NULL; } diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 4cc85dba..b46cdaab 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -1,71 +1,6 @@ #include // needed for PlatformIO #include - -#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) - #include -#elif defined(RP2040_PLATFORM) - #include -#elif defined(ESP32) - #include -#endif - -#include -#include -#include -#include -#include -#include "NodePrefs.h" -#include -#include - -/* ---------------------------------- CONFIGURATION ------------------------------------- */ - -#ifndef LORA_FREQ - #define LORA_FREQ 915.0 -#endif -#ifndef LORA_BW - #define LORA_BW 250 -#endif -#ifndef LORA_SF - #define LORA_SF 10 -#endif -#ifndef LORA_CR - #define LORA_CR 5 -#endif -#ifndef LORA_TX_POWER - #define LORA_TX_POWER 20 -#endif -#ifndef MAX_LORA_TX_POWER - #define MAX_LORA_TX_POWER LORA_TX_POWER -#endif - -#ifndef MAX_CONTACTS - #define MAX_CONTACTS 100 -#endif - -#ifndef OFFLINE_QUEUE_SIZE - #define OFFLINE_QUEUE_SIZE 16 -#endif - -#ifndef BLE_NAME_PREFIX - #define BLE_NAME_PREFIX "MeshCore-" -#endif - -#include - -#define SEND_TIMEOUT_BASE_MILLIS 500 -#define FLOOD_SEND_TIMEOUT_FACTOR 16.0f -#define DIRECT_SEND_PERHOP_FACTOR 6.0f -#define DIRECT_SEND_PERHOP_EXTRA_MILLIS 250 -#define LAZY_CONTACTS_WRITE_DELAY 5000 - -#define PUBLIC_GROUP_PSK "izOH6cXN6mrJ5e26oRXNcg==" - -#ifdef DISPLAY_CLASS - #include "UITask.h" - - static UITask ui_task(&board); -#endif +#include "MyMesh.h" // Believe it or not, this std C function is busted on some platforms! static uint32_t _atoi(const char* sp) { @@ -77,1525 +12,16 @@ static uint32_t _atoi(const char* sp) { return n; } -/*------------ Frame Protocol --------------*/ - -#define FIRMWARE_VER_CODE 5 - -#ifndef FIRMWARE_BUILD_DATE - #define FIRMWARE_BUILD_DATE "24 May 2025" -#endif - -#ifndef FIRMWARE_VERSION - #define FIRMWARE_VERSION "v1.6.2" -#endif - -#define CMD_APP_START 1 -#define CMD_SEND_TXT_MSG 2 -#define CMD_SEND_CHANNEL_TXT_MSG 3 -#define CMD_GET_CONTACTS 4 // with optional 'since' (for efficient sync) -#define CMD_GET_DEVICE_TIME 5 -#define CMD_SET_DEVICE_TIME 6 -#define CMD_SEND_SELF_ADVERT 7 -#define CMD_SET_ADVERT_NAME 8 -#define CMD_ADD_UPDATE_CONTACT 9 -#define CMD_SYNC_NEXT_MESSAGE 10 -#define CMD_SET_RADIO_PARAMS 11 -#define CMD_SET_RADIO_TX_POWER 12 -#define CMD_RESET_PATH 13 -#define CMD_SET_ADVERT_LATLON 14 -#define CMD_REMOVE_CONTACT 15 -#define CMD_SHARE_CONTACT 16 -#define CMD_EXPORT_CONTACT 17 -#define CMD_IMPORT_CONTACT 18 -#define CMD_REBOOT 19 -#define CMD_GET_BATTERY_VOLTAGE 20 -#define CMD_SET_TUNING_PARAMS 21 -#define CMD_DEVICE_QEURY 22 -#define CMD_EXPORT_PRIVATE_KEY 23 -#define CMD_IMPORT_PRIVATE_KEY 24 -#define CMD_SEND_RAW_DATA 25 -#define CMD_SEND_LOGIN 26 -#define CMD_SEND_STATUS_REQ 27 -#define CMD_HAS_CONNECTION 28 -#define CMD_LOGOUT 29 // 'Disconnect' -#define CMD_GET_CONTACT_BY_KEY 30 -#define CMD_GET_CHANNEL 31 -#define CMD_SET_CHANNEL 32 -#define CMD_SIGN_START 33 -#define CMD_SIGN_DATA 34 -#define CMD_SIGN_FINISH 35 -#define CMD_SEND_TRACE_PATH 36 -#define CMD_SET_DEVICE_PIN 37 -#define CMD_SET_OTHER_PARAMS 38 -#define CMD_SEND_TELEMETRY_REQ 39 -#define CMD_GET_CUSTOM_VARS 40 -#define CMD_SET_CUSTOM_VAR 41 - -#define RESP_CODE_OK 0 -#define RESP_CODE_ERR 1 -#define RESP_CODE_CONTACTS_START 2 // first reply to CMD_GET_CONTACTS -#define RESP_CODE_CONTACT 3 // multiple of these (after CMD_GET_CONTACTS) -#define RESP_CODE_END_OF_CONTACTS 4 // last reply to CMD_GET_CONTACTS -#define RESP_CODE_SELF_INFO 5 // reply to CMD_APP_START -#define RESP_CODE_SENT 6 // reply to CMD_SEND_TXT_MSG -#define RESP_CODE_CONTACT_MSG_RECV 7 // a reply to CMD_SYNC_NEXT_MESSAGE (ver < 3) -#define RESP_CODE_CHANNEL_MSG_RECV 8 // a reply to CMD_SYNC_NEXT_MESSAGE (ver < 3) -#define RESP_CODE_CURR_TIME 9 // a reply to CMD_GET_DEVICE_TIME -#define RESP_CODE_NO_MORE_MESSAGES 10 // a reply to CMD_SYNC_NEXT_MESSAGE -#define RESP_CODE_EXPORT_CONTACT 11 -#define RESP_CODE_BATTERY_VOLTAGE 12 // a reply to a CMD_GET_BATTERY_VOLTAGE -#define RESP_CODE_DEVICE_INFO 13 // a reply to CMD_DEVICE_QEURY -#define RESP_CODE_PRIVATE_KEY 14 // a reply to CMD_EXPORT_PRIVATE_KEY -#define RESP_CODE_DISABLED 15 -#define RESP_CODE_CONTACT_MSG_RECV_V3 16 // a reply to CMD_SYNC_NEXT_MESSAGE (ver >= 3) -#define RESP_CODE_CHANNEL_MSG_RECV_V3 17 // a reply to CMD_SYNC_NEXT_MESSAGE (ver >= 3) -#define RESP_CODE_CHANNEL_INFO 18 // a reply to CMD_GET_CHANNEL -#define RESP_CODE_SIGN_START 19 -#define RESP_CODE_SIGNATURE 20 -#define RESP_CODE_CUSTOM_VARS 21 - -// these are _pushed_ to client app at any time -#define PUSH_CODE_ADVERT 0x80 -#define PUSH_CODE_PATH_UPDATED 0x81 -#define PUSH_CODE_SEND_CONFIRMED 0x82 -#define PUSH_CODE_MSG_WAITING 0x83 -#define PUSH_CODE_RAW_DATA 0x84 -#define PUSH_CODE_LOGIN_SUCCESS 0x85 -#define PUSH_CODE_LOGIN_FAIL 0x86 -#define PUSH_CODE_STATUS_RESPONSE 0x87 -#define PUSH_CODE_LOG_RX_DATA 0x88 -#define PUSH_CODE_TRACE_DATA 0x89 -#define PUSH_CODE_NEW_ADVERT 0x8A -#define PUSH_CODE_TELEMETRY_RESPONSE 0x8B - -#define ERR_CODE_UNSUPPORTED_CMD 1 -#define ERR_CODE_NOT_FOUND 2 -#define ERR_CODE_TABLE_FULL 3 -#define ERR_CODE_BAD_STATE 4 -#define ERR_CODE_FILE_IO_ERROR 5 -#define ERR_CODE_ILLEGAL_ARG 6 - -/* -------------------------------------------------------------------------------------- */ - -#define REQ_TYPE_GET_STATUS 0x01 // same as _GET_STATS -#define REQ_TYPE_KEEP_ALIVE 0x02 -#define REQ_TYPE_GET_TELEMETRY_DATA 0x03 - -#define MAX_SIGN_DATA_LEN (8*1024) // 8K - -class MyMesh : public BaseChatMesh { - FILESYSTEM* _fs; - IdentityStore* _identity_store; - NodePrefs _prefs; - uint32_t pending_login; - uint32_t pending_status; - uint32_t pending_telemetry; - BaseSerialInterface* _serial; - ContactsIterator _iter; - uint32_t _iter_filter_since; - uint32_t _most_recent_lastmod; - uint32_t _active_ble_pin; - bool _iter_started; - uint8_t app_target_ver; - uint8_t* sign_data; - uint32_t sign_data_len; - unsigned long dirty_contacts_expiry; - uint8_t cmd_frame[MAX_FRAME_SIZE+1]; - uint8_t out_frame[MAX_FRAME_SIZE+1]; - CayenneLPP telemetry; - - struct Frame { - uint8_t len; - uint8_t buf[MAX_FRAME_SIZE]; - }; - int offline_queue_len; - Frame offline_queue[OFFLINE_QUEUE_SIZE]; - - struct AckTableEntry { - unsigned long msg_sent; - uint32_t ack; - }; - #define EXPECTED_ACK_TABLE_SIZE 8 - AckTableEntry expected_ack_table[EXPECTED_ACK_TABLE_SIZE]; // circular table - int next_ack_idx; - - void loadMainIdentity() { - if (!_identity_store->load("_main", self_id)) { - self_id = radio_new_identity(); // create new random identity - int count = 0; - while (count < 10 && (self_id.pub_key[0] == 0x00 || self_id.pub_key[0] == 0xFF)) { // reserved id hashes - self_id = radio_new_identity(); count++; - } - saveMainIdentity(self_id); - } - } - - bool saveMainIdentity(const mesh::LocalIdentity& identity) { - return _identity_store->save("_main", identity); - } - - void loadContacts() { - if (_fs->exists("/contacts3")) { - #if defined(RP2040_PLATFORM) - File file = _fs->open("/contacts3", "r"); - #else - File file = _fs->open("/contacts3"); - #endif - if (file) { - bool full = false; - while (!full) { - ContactInfo c; - uint8_t pub_key[32]; - uint8_t unused; - - bool success = (file.read(pub_key, 32) == 32); - success = success && (file.read((uint8_t *) &c.name, 32) == 32); - success = success && (file.read(&c.type, 1) == 1); - success = success && (file.read(&c.flags, 1) == 1); - success = success && (file.read(&unused, 1) == 1); - success = success && (file.read((uint8_t *) &c.sync_since, 4) == 4); // was 'reserved' - success = success && (file.read((uint8_t *) &c.out_path_len, 1) == 1); - success = success && (file.read((uint8_t *) &c.last_advert_timestamp, 4) == 4); - success = success && (file.read(c.out_path, 64) == 64); - success = success && (file.read((uint8_t *) &c.lastmod, 4) == 4); - success = success && (file.read((uint8_t *) &c.gps_lat, 4) == 4); - success = success && (file.read((uint8_t *) &c.gps_lon, 4) == 4); - - if (!success) break; // EOF - - c.id = mesh::Identity(pub_key); - if (!addContact(c)) full = true; - } - file.close(); - } - } - } - - void saveContacts() { #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) - _fs->remove("/contacts3"); - File file = _fs->open("/contacts3", FILE_O_WRITE); + #include + DataStore store(InternalFS, rtc_clock); #elif defined(RP2040_PLATFORM) - File file = _fs->open("/contacts3", "w"); -#else - File file = _fs->open("/contacts3", "w", true); + #include + DataStore store(LittleFS, rtc_clock); +#elif defined(ESP32) + #include + DataStore store(SPIFFS, rtc_clock); #endif - if (file) { - ContactsIterator iter; - ContactInfo c; - uint8_t unused = 0; - - while (iter.hasNext(this, c)) { - bool success = (file.write(c.id.pub_key, 32) == 32); - success = success && (file.write((uint8_t *) &c.name, 32) == 32); - success = success && (file.write(&c.type, 1) == 1); - success = success && (file.write(&c.flags, 1) == 1); - success = success && (file.write(&unused, 1) == 1); - success = success && (file.write((uint8_t *) &c.sync_since, 4) == 4); - success = success && (file.write((uint8_t *) &c.out_path_len, 1) == 1); - success = success && (file.write((uint8_t *) &c.last_advert_timestamp, 4) == 4); - success = success && (file.write(c.out_path, 64) == 64); - success = success && (file.write((uint8_t *) &c.lastmod, 4) == 4); - success = success && (file.write((uint8_t *) &c.gps_lat, 4) == 4); - success = success && (file.write((uint8_t *) &c.gps_lon, 4) == 4); - - if (!success) break; // write failed - } - file.close(); - } - } - - void loadChannels() { - if (_fs->exists("/channels2")) { - #if defined(RP2040_PLATFORM) - File file = _fs->open("/channels2", "r"); - #else - File file = _fs->open("/channels2"); - #endif - if (file) { - bool full = false; - uint8_t channel_idx = 0; - while (!full) { - ChannelDetails ch; - uint8_t unused[4]; - - bool success = (file.read(unused, 4) == 4); - success = success && (file.read((uint8_t *) ch.name, 32) == 32); - success = success && (file.read((uint8_t *) ch.channel.secret, 32) == 32); - - if (!success) break; // EOF - - if (setChannel(channel_idx, ch)) { - channel_idx++; - } else { - full = true; - } - } - file.close(); - } - } - } - - void saveChannels() { - #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) - _fs->remove("/channels2"); - File file = _fs->open("/channels2", FILE_O_WRITE); - #elif defined(RP2040_PLATFORM) - File file = _fs->open("/channels2", "w"); - #else - File file = _fs->open("/channels2", "w", true); - #endif - if (file) { - uint8_t channel_idx = 0; - ChannelDetails ch; - uint8_t unused[4]; - memset(unused, 0, 4); - - while (getChannel(channel_idx, ch)) { - bool success = (file.write(unused, 4) == 4); - success = success && (file.write((uint8_t *) ch.name, 32) == 32); - success = success && (file.write((uint8_t *) ch.channel.secret, 32) == 32); - - if (!success) break; // write failed - channel_idx++; - } - file.close(); - } - } - - int getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]) override { - char path[64]; - char fname[18]; - - if (key_len > 8) key_len = 8; // just use first 8 bytes (prefix) - mesh::Utils::toHex(fname, key, key_len); - sprintf(path, "/bl/%s", fname); - - if (_fs->exists(path)) { - #if defined(RP2040_PLATFORM) - File f = _fs->open(path, "r"); - #else - File f = _fs->open(path); - #endif - if (f) { - int len = f.read(dest_buf, 255); // currently MAX 255 byte blob len supported!! - f.close(); - return len; - } - } - return 0; // not found - } - - bool putBlobByKey(const uint8_t key[], int key_len, const uint8_t src_buf[], int len) override { - char path[64]; - char fname[18]; - - if (key_len > 8) key_len = 8; // just use first 8 bytes (prefix) - mesh::Utils::toHex(fname, key, key_len); - sprintf(path, "/bl/%s", fname); - - #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) - _fs->remove(path); - File f = _fs->open(path, FILE_O_WRITE); - #elif defined(RP2040_PLATFORM) - File f = _fs->open(path, "w"); - #else - File f = _fs->open(path, "w", true); - #endif - if (f) { - int n = f.write(src_buf, len); - f.close(); - if (n == len) return true; // success! - - _fs->remove(path); // blob was only partially written! - } - return false; // error - } - - void writeOKFrame() { - uint8_t buf[1]; - buf[0] = RESP_CODE_OK; - _serial->writeFrame(buf, 1); - } - void writeErrFrame(uint8_t err_code) { - uint8_t buf[2]; - buf[0] = RESP_CODE_ERR; - buf[1] = err_code; - _serial->writeFrame(buf, 2); - } - - void writeDisabledFrame() { - uint8_t buf[1]; - buf[0] = RESP_CODE_DISABLED; - _serial->writeFrame(buf, 1); - } - - void writeContactRespFrame(uint8_t code, const ContactInfo& contact) { - int i = 0; - out_frame[i++] = code; - memcpy(&out_frame[i], contact.id.pub_key, PUB_KEY_SIZE); i += PUB_KEY_SIZE; - out_frame[i++] = contact.type; - out_frame[i++] = contact.flags; - out_frame[i++] = contact.out_path_len; - memcpy(&out_frame[i], contact.out_path, MAX_PATH_SIZE); i += MAX_PATH_SIZE; - StrHelper::strzcpy((char *) &out_frame[i], contact.name, 32); i += 32; - memcpy(&out_frame[i], &contact.last_advert_timestamp, 4); i += 4; - memcpy(&out_frame[i], &contact.gps_lat, 4); i += 4; - memcpy(&out_frame[i], &contact.gps_lon, 4); i += 4; - memcpy(&out_frame[i], &contact.lastmod, 4); i += 4; - _serial->writeFrame(out_frame, i); - } - - void updateContactFromFrame(ContactInfo& contact, const uint8_t* frame, int len) { - int i = 0; - uint8_t code = frame[i++]; // eg. CMD_ADD_UPDATE_CONTACT - memcpy(contact.id.pub_key, &frame[i], PUB_KEY_SIZE); i += PUB_KEY_SIZE; - contact.type = frame[i++]; - contact.flags = frame[i++]; - contact.out_path_len = frame[i++]; - memcpy(contact.out_path, &frame[i], MAX_PATH_SIZE); i += MAX_PATH_SIZE; - memcpy(contact.name, &frame[i], 32); i += 32; - memcpy(&contact.last_advert_timestamp, &frame[i], 4); i += 4; - if (i + 8 >= len) { // optional fields - memcpy(&contact.gps_lat, &frame[i], 4); i += 4; - memcpy(&contact.gps_lon, &frame[i], 4); i += 4; - } - } - - void addToOfflineQueue(const uint8_t frame[], int len) { - if (offline_queue_len >= OFFLINE_QUEUE_SIZE) { - MESH_DEBUG_PRINTLN("ERROR: offline_queue is full!"); - } else { - offline_queue[offline_queue_len].len = len; - memcpy(offline_queue[offline_queue_len].buf, frame, len); - offline_queue_len++; - } - } - int getFromOfflineQueue(uint8_t frame[]) { - if (offline_queue_len > 0) { // check offline queue - size_t len = offline_queue[0].len; // take from top of queue - memcpy(frame, offline_queue[0].buf, len); - - offline_queue_len--; - for (int i = 0; i < offline_queue_len; i++) { // delete top item from queue - offline_queue[i] = offline_queue[i + 1]; - } - return len; - } - return 0; // queue is empty - } - -protected: - float getAirtimeBudgetFactor() const override { - return _prefs.airtime_factor; - } - - int getInterferenceThreshold() const override { - return 14; // hard-coded for now - } - - int calcRxDelay(float score, uint32_t air_time) const override { - if (_prefs.rx_delay_base <= 0.0f) return 0; - return (int) ((pow(_prefs.rx_delay_base, 0.85f - score) - 1.0) * air_time); - } - - void logRxRaw(float snr, float rssi, const uint8_t raw[], int len) override { - if (_serial->isConnected() && len+3 <= MAX_FRAME_SIZE) { - int i = 0; - out_frame[i++] = PUSH_CODE_LOG_RX_DATA; - out_frame[i++] = (int8_t)(snr * 4); - out_frame[i++] = (int8_t)(rssi); - memcpy(&out_frame[i], raw, len); i += len; - - _serial->writeFrame(out_frame, i); - } - } - - bool isAutoAddEnabled() const override { - return (_prefs.manual_add_contacts & 1) == 0; - } - - void onDiscoveredContact(ContactInfo& contact, bool is_new) override { - if (_serial->isConnected()) { - if (!isAutoAddEnabled() && is_new) { - writeContactRespFrame(PUSH_CODE_NEW_ADVERT, contact); - } else { - out_frame[0] = PUSH_CODE_ADVERT; - memcpy(&out_frame[1], contact.id.pub_key, PUB_KEY_SIZE); - _serial->writeFrame(out_frame, 1 + PUB_KEY_SIZE); - } - } else { - #ifdef DISPLAY_CLASS - ui_task.soundBuzzer(UIEventType::newContactMessage); - #endif - } - - dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); - } - - void onContactPathUpdated(const ContactInfo& contact) override { - out_frame[0] = PUSH_CODE_PATH_UPDATED; - memcpy(&out_frame[1], contact.id.pub_key, PUB_KEY_SIZE); - _serial->writeFrame(out_frame, 1 + PUB_KEY_SIZE); // NOTE: app may not be connected - - dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); - } - - bool processAck(const uint8_t *data) override { - // see if matches any in a table - for (int i = 0; i < EXPECTED_ACK_TABLE_SIZE; i++) { - if (memcmp(data, &expected_ack_table[i].ack, 4) == 0) { // got an ACK from recipient - out_frame[0] = PUSH_CODE_SEND_CONFIRMED; - memcpy(&out_frame[1], data, 4); - uint32_t trip_time = _ms->getMillis() - expected_ack_table[i].msg_sent; - memcpy(&out_frame[5], &trip_time, 4); - _serial->writeFrame(out_frame, 9); - - // NOTE: the same ACK can be received multiple times! - expected_ack_table[i].ack = 0; // clear expected hash, now that we have received ACK - return true; - } - } - return checkConnectionsAck(data); - } - - void queueMessage(const ContactInfo& from, uint8_t txt_type, mesh::Packet* pkt, uint32_t sender_timestamp, const uint8_t* extra, int extra_len, const char *text) { - int i = 0; - if (app_target_ver >= 3) { - out_frame[i++] = RESP_CODE_CONTACT_MSG_RECV_V3; - out_frame[i++] = (int8_t)(pkt->getSNR() * 4); - out_frame[i++] = 0; // reserved1 - out_frame[i++] = 0; // reserved2 - } else { - out_frame[i++] = RESP_CODE_CONTACT_MSG_RECV; - } - memcpy(&out_frame[i], from.id.pub_key, 6); i += 6; // just 6-byte prefix - uint8_t path_len = out_frame[i++] = pkt->isRouteFlood() ? pkt->path_len : 0xFF; - out_frame[i++] = txt_type; - memcpy(&out_frame[i], &sender_timestamp, 4); i += 4; - if (extra_len > 0) { - memcpy(&out_frame[i], extra, extra_len); i += extra_len; - } - int tlen = strlen(text); // TODO: UTF-8 ?? - if (i + tlen > MAX_FRAME_SIZE) { - tlen = MAX_FRAME_SIZE - i; - } - memcpy(&out_frame[i], text, tlen); i += tlen; - addToOfflineQueue(out_frame, i); - - if (_serial->isConnected()) { - uint8_t frame[1]; - frame[0] = PUSH_CODE_MSG_WAITING; // send push 'tickle' - _serial->writeFrame(frame, 1); - } else { - #ifdef DISPLAY_CLASS - ui_task.soundBuzzer(UIEventType::contactMessage); - #endif - } - #ifdef DISPLAY_CLASS - ui_task.newMsg(path_len, from.name, text, offline_queue_len); - #endif - } - - void onMessageRecv(const ContactInfo& from, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text) override { - markConnectionActive(from); // in case this is from a server, and we have a connection - queueMessage(from, TXT_TYPE_PLAIN, pkt, sender_timestamp, NULL, 0, text); - } - - void onCommandDataRecv(const ContactInfo& from, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text) override { - 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 onSignedMessageRecv(const ContactInfo& from, mesh::Packet* pkt, uint32_t sender_timestamp, const uint8_t *sender_prefix, const char *text) override { - markConnectionActive(from); - // from.sync_since change needs to be persisted - dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); - queueMessage(from, TXT_TYPE_SIGNED_PLAIN, pkt, sender_timestamp, sender_prefix, 4, text); - } - - void onChannelMessageRecv(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t timestamp, const char *text) override { - int i = 0; - if (app_target_ver >= 3) { - out_frame[i++] = RESP_CODE_CHANNEL_MSG_RECV_V3; - out_frame[i++] = (int8_t)(pkt->getSNR() * 4); - out_frame[i++] = 0; // reserved1 - out_frame[i++] = 0; // reserved2 - } else { - out_frame[i++] = RESP_CODE_CHANNEL_MSG_RECV; - } - - uint8_t channel_idx = findChannelIdx(channel); - out_frame[i++] = channel_idx; - uint8_t path_len = out_frame[i++] = pkt->isRouteFlood() ? pkt->path_len : 0xFF; - - out_frame[i++] = TXT_TYPE_PLAIN; - memcpy(&out_frame[i], ×tamp, 4); i += 4; - int tlen = strlen(text); // TODO: UTF-8 ?? - if (i + tlen > MAX_FRAME_SIZE) { - tlen = MAX_FRAME_SIZE - i; - } - memcpy(&out_frame[i], text, tlen); i += tlen; - addToOfflineQueue(out_frame, i); - - if (_serial->isConnected()) { - uint8_t frame[1]; - frame[0] = PUSH_CODE_MSG_WAITING; // send push 'tickle' - _serial->writeFrame(frame, 1); - } else { - #ifdef DISPLAY_CLASS - ui_task.soundBuzzer(UIEventType::channelMessage); - #endif - } - #ifdef DISPLAY_CLASS - // Get the channel name from the channel index - const char* channel_name = "Unknown"; - ChannelDetails channel_details; - if (getChannel(channel_idx, channel_details)) { - channel_name = channel_details.name; - } - ui_task.newMsg(path_len, channel_name, text, offline_queue_len); - #endif - } - - uint8_t onContactRequest(const ContactInfo& contact, uint32_t sender_timestamp, const uint8_t* data, uint8_t len, uint8_t* reply) override { - if (data[0] == REQ_TYPE_GET_TELEMETRY_DATA) { - uint8_t permissions = 0; - uint8_t cp = contact.flags >> 1; // LSB used as 'favourite' bit (so only use upper bits) - - if (_prefs.telemetry_mode_base == TELEM_MODE_ALLOW_ALL) { - permissions = TELEM_PERM_BASE; - } else if (_prefs.telemetry_mode_base == TELEM_MODE_ALLOW_FLAGS) { - permissions = cp & TELEM_PERM_BASE; - } - - if (_prefs.telemetry_mode_loc == TELEM_MODE_ALLOW_ALL) { - permissions |= TELEM_PERM_LOCATION; - } else if (_prefs.telemetry_mode_loc == TELEM_MODE_ALLOW_FLAGS) { - permissions |= cp & TELEM_PERM_LOCATION; - } - - if (_prefs.telemetry_mode_env == TELEM_MODE_ALLOW_ALL) { - permissions |= TELEM_PERM_ENVIRONMENT; - } else if (_prefs.telemetry_mode_env == TELEM_MODE_ALLOW_FLAGS) { - permissions |= cp & TELEM_PERM_ENVIRONMENT; - } - - if (permissions & TELEM_PERM_BASE) { // only respond if base permission bit is set - telemetry.reset(); - telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f); - // query other sensors -- target specific - sensors.querySensors(permissions, telemetry); - - memcpy(reply, &sender_timestamp, 4); // reflect sender_timestamp back in response packet (kind of like a 'tag') - - uint8_t tlen = telemetry.getSize(); - memcpy(&reply[4], telemetry.getBuffer(), tlen); - return 4 + tlen; - } - } - return 0; // unknown - } - - void onContactResponse(const ContactInfo& contact, const uint8_t* data, uint8_t len) override { - uint32_t tag; - memcpy(&tag, data, 4); - - if (pending_login && memcmp(&pending_login, contact.id.pub_key, 4) == 0) { // check for login response - // yes, is response to pending sendLogin() - pending_login = 0; - - int i = 0; - if (memcmp(&data[4], "OK", 2) == 0) { // legacy Repeater login OK response - out_frame[i++] = PUSH_CODE_LOGIN_SUCCESS; - out_frame[i++] = 0; // legacy: is_admin = false - memcpy(&out_frame[i], contact.id.pub_key, 6); i += 6; // pub_key_prefix - } else if (data[4] == RESP_SERVER_LOGIN_OK) { // new login response - uint16_t keep_alive_secs = ((uint16_t)data[5]) * 16; - if (keep_alive_secs > 0) { - startConnection(contact, keep_alive_secs); - } - out_frame[i++] = PUSH_CODE_LOGIN_SUCCESS; - out_frame[i++] = data[6]; // permissions (eg. is_admin) - memcpy(&out_frame[i], contact.id.pub_key, 6); i += 6; // pub_key_prefix - memcpy(&out_frame[i], &tag, 4); i += 4; // NEW: include server timestamp - } else { - out_frame[i++] = PUSH_CODE_LOGIN_FAIL; - out_frame[i++] = 0; // reserved - memcpy(&out_frame[i], contact.id.pub_key, 6); i += 6; // pub_key_prefix - } - _serial->writeFrame(out_frame, i); - } else if (len > 4 && // check for status response - pending_status && memcmp(&pending_status, contact.id.pub_key, 4) == 0 // legacy matching scheme - // FUTURE: tag == pending_status - ) { - pending_status = 0; - - int i = 0; - out_frame[i++] = PUSH_CODE_STATUS_RESPONSE; - out_frame[i++] = 0; // reserved - memcpy(&out_frame[i], contact.id.pub_key, 6); i += 6; // pub_key_prefix - memcpy(&out_frame[i], &data[4], len - 4); i += (len - 4); - _serial->writeFrame(out_frame, i); - } else if (len > 4 && tag == pending_telemetry) { // check for telemetry response - pending_telemetry = 0; - - int i = 0; - out_frame[i++] = PUSH_CODE_TELEMETRY_RESPONSE; - out_frame[i++] = 0; // reserved - memcpy(&out_frame[i], contact.id.pub_key, 6); i += 6; // pub_key_prefix - memcpy(&out_frame[i], &data[4], len - 4); i += (len - 4); - _serial->writeFrame(out_frame, i); - } - } - - void onRawDataRecv(mesh::Packet* packet) override { - if (packet->payload_len + 4 > sizeof(out_frame)) { - MESH_DEBUG_PRINTLN("onRawDataRecv(), payload_len too long: %d", packet->payload_len); - return; - } - int i = 0; - out_frame[i++] = PUSH_CODE_RAW_DATA; - out_frame[i++] = (int8_t)(_radio->getLastSNR() * 4); - out_frame[i++] = (int8_t)(_radio->getLastRSSI()); - out_frame[i++] = 0xFF; // reserved (possibly path_len in future) - memcpy(&out_frame[i], packet->payload, packet->payload_len); i += packet->payload_len; - - if (_serial->isConnected()) { - _serial->writeFrame(out_frame, i); - } else { - MESH_DEBUG_PRINTLN("onRawDataRecv(), data received while app offline"); - } - } - - void onTraceRecv(mesh::Packet* packet, uint32_t tag, uint32_t auth_code, uint8_t flags, const uint8_t* path_snrs, const uint8_t* path_hashes, uint8_t path_len) override { - int i = 0; - out_frame[i++] = PUSH_CODE_TRACE_DATA; - out_frame[i++] = 0; // reserved - out_frame[i++] = path_len; - out_frame[i++] = flags; - memcpy(&out_frame[i], &tag, 4); i += 4; - memcpy(&out_frame[i], &auth_code, 4); i += 4; - memcpy(&out_frame[i], path_hashes, path_len); i += path_len; - memcpy(&out_frame[i], path_snrs, path_len); i += path_len; - out_frame[i++] = (int8_t)(packet->getSNR() * 4); // extra/final SNR (to this node) - - if (_serial->isConnected()) { - _serial->writeFrame(out_frame, i); - } else { - MESH_DEBUG_PRINTLN("onTraceRecv(), data received while app offline"); - } - } - - uint32_t calcFloodTimeoutMillisFor(uint32_t pkt_airtime_millis) const override { - return SEND_TIMEOUT_BASE_MILLIS + (FLOOD_SEND_TIMEOUT_FACTOR * pkt_airtime_millis); - } - uint32_t calcDirectTimeoutMillisFor(uint32_t pkt_airtime_millis, uint8_t path_len) const override { - return SEND_TIMEOUT_BASE_MILLIS + - ( (pkt_airtime_millis*DIRECT_SEND_PERHOP_FACTOR + DIRECT_SEND_PERHOP_EXTRA_MILLIS) * (path_len + 1)); - } - - void onSendTimeout() override { - } - -public: - - MyMesh(mesh::Radio& radio, mesh::RNG& rng, mesh::RTCClock& rtc, SimpleMeshTables& tables) - : BaseChatMesh(radio, *new ArduinoMillis(), rng, rtc, *new StaticPoolPacketManager(16), tables), _serial(NULL), - telemetry(MAX_PACKET_PAYLOAD - 4) - { - _iter_started = false; - offline_queue_len = 0; - app_target_ver = 0; - _identity_store = NULL; - pending_login = pending_status = pending_telemetry = 0; - next_ack_idx = 0; - sign_data = NULL; - dirty_contacts_expiry = 0; - - // defaults - memset(&_prefs, 0, sizeof(_prefs)); - _prefs.airtime_factor = 1.0; // one half - strcpy(_prefs.node_name, "NONAME"); - _prefs.freq = LORA_FREQ; - _prefs.sf = LORA_SF; - _prefs.bw = LORA_BW; - _prefs.cr = LORA_CR; - _prefs.tx_power_dbm = LORA_TX_POWER; - //_prefs.rx_delay_base = 10.0f; enable once new algo fixed - } - - void loadPrefsInt(const char* filename) { -#if defined(RP2040_PLATFORM) - File file = _fs->open(filename, "r"); -#else - File file = _fs->open(filename); -#endif - if (file) { - uint8_t pad[8]; - - file.read((uint8_t *) &_prefs.airtime_factor, sizeof(float)); // 0 - file.read((uint8_t *) _prefs.node_name, sizeof(_prefs.node_name)); // 4 - file.read(pad, 4); // 36 - file.read((uint8_t *) &sensors.node_lat, sizeof(sensors.node_lat)); // 40 - file.read((uint8_t *) &sensors.node_lon, sizeof(sensors.node_lon)); // 48 - file.read((uint8_t *) &_prefs.freq, sizeof(_prefs.freq)); // 56 - file.read((uint8_t *) &_prefs.sf, sizeof(_prefs.sf)); // 60 - file.read((uint8_t *) &_prefs.cr, sizeof(_prefs.cr)); // 61 - file.read((uint8_t *) &_prefs.reserved1, sizeof(_prefs.reserved1)); // 62 - file.read((uint8_t *) &_prefs.manual_add_contacts, sizeof(_prefs.manual_add_contacts)); // 63 - file.read((uint8_t *) &_prefs.bw, sizeof(_prefs.bw)); // 64 - file.read((uint8_t *) &_prefs.tx_power_dbm, sizeof(_prefs.tx_power_dbm)); // 68 - file.read((uint8_t *) &_prefs.telemetry_mode_base, sizeof(_prefs.telemetry_mode_base)); // 69 - file.read((uint8_t *) &_prefs.telemetry_mode_loc, sizeof(_prefs.telemetry_mode_loc)); // 70 - file.read((uint8_t *) &_prefs.telemetry_mode_env, sizeof(_prefs.telemetry_mode_env)); // 71 - file.read((uint8_t *) &_prefs.rx_delay_base, sizeof(_prefs.rx_delay_base)); // 72 - file.read(pad, 4); // 76 - file.read((uint8_t *) &_prefs.ble_pin, sizeof(_prefs.ble_pin)); // 80 - - // sanitise bad pref values - _prefs.rx_delay_base = constrain(_prefs.rx_delay_base, 0, 20.0f); - _prefs.airtime_factor = constrain(_prefs.airtime_factor, 0, 9.0f); - _prefs.freq = constrain(_prefs.freq, 400.0f, 2500.0f); - _prefs.bw = constrain(_prefs.bw, 62.5f, 500.0f); - _prefs.sf = constrain(_prefs.sf, 7, 12); - _prefs.cr = constrain(_prefs.cr, 5, 8); - _prefs.tx_power_dbm = constrain(_prefs.tx_power_dbm, 1, MAX_LORA_TX_POWER); - - file.close(); - } - } - - void begin(FILESYSTEM& fs, bool has_display) { - _fs = &fs; - - BaseChatMesh::begin(); - - #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) - _identity_store = new IdentityStore(fs, ""); - #elif defined(RP2040_PLATFORM) - _identity_store = new IdentityStore(fs, "/identity"); - _identity_store->begin(); - #else - _identity_store = new IdentityStore(fs, "/identity"); - #endif - - loadMainIdentity(); - - // use hex of first 4 bytes of identity public key as default node name - char pub_key_hex[10]; - mesh::Utils::toHex(pub_key_hex, self_id.pub_key, 4); - strcpy(_prefs.node_name, pub_key_hex); - - // if name is provided as a build flag, use that as default node name instead - #ifdef ADVERT_NAME - strcpy(_prefs.node_name, ADVERT_NAME); - #endif - - // load persisted prefs - if (_fs->exists("/new_prefs")) { - loadPrefsInt("/new_prefs"); // new filename - } else if (_fs->exists("/node_prefs")) { - loadPrefsInt("/node_prefs"); - savePrefs(); // save to new filename - _fs->remove("/node_prefs"); // remove old - } - - #ifdef BLE_PIN_CODE - if (_prefs.ble_pin == 0) { - #ifdef DISPLAY_CLASS - if (has_display) { - StdRNG rng; - _active_ble_pin = rng.nextInt(100000, 999999); // random pin each session - } else { - _active_ble_pin = BLE_PIN_CODE; // otherwise static pin - } - #else - _active_ble_pin = BLE_PIN_CODE; // otherwise static pin - #endif - } else { - _active_ble_pin = _prefs.ble_pin; - } - #else - _active_ble_pin = 0; - #endif - - // init 'blob store' support - _fs->mkdir("/bl"); - - loadContacts(); - addChannel("Public", PUBLIC_GROUP_PSK); // pre-configure Andy's public channel - loadChannels(); - - radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); - radio_set_tx_power(_prefs.tx_power_dbm); - } - - const char* getNodeName() { return _prefs.node_name; } - NodePrefs* getNodePrefs() { - return &_prefs; - } - uint32_t getBLEPin() { return _active_ble_pin; } - - void startInterface(BaseSerialInterface& serial) { - _serial = &serial; - serial.enable(); - } - - void savePrefs() { -#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) - _fs->remove("/new_prefs"); - File file = _fs->open("/new_prefs", FILE_O_WRITE); -#elif defined(RP2040_PLATFORM) - File file = _fs->open("/new_prefs", "w"); -#else - File file = _fs->open("/new_prefs", "w", true); -#endif - if (file) { - uint8_t pad[8]; - memset(pad, 0, sizeof(pad)); - - file.write((uint8_t *) &_prefs.airtime_factor, sizeof(float)); // 0 - file.write((uint8_t *) _prefs.node_name, sizeof(_prefs.node_name)); // 4 - file.write(pad, 4); // 36 - file.write((uint8_t *) &sensors.node_lat, sizeof(sensors.node_lat)); // 40 - file.write((uint8_t *) &sensors.node_lon, sizeof(sensors.node_lon)); // 48 - file.write((uint8_t *) &_prefs.freq, sizeof(_prefs.freq)); // 56 - file.write((uint8_t *) &_prefs.sf, sizeof(_prefs.sf)); // 60 - file.write((uint8_t *) &_prefs.cr, sizeof(_prefs.cr)); // 61 - file.write((uint8_t *) &_prefs.reserved1, sizeof(_prefs.reserved1)); // 62 - file.write((uint8_t *) &_prefs.manual_add_contacts, sizeof(_prefs.manual_add_contacts)); // 63 - file.write((uint8_t *) &_prefs.bw, sizeof(_prefs.bw)); // 64 - file.write((uint8_t *) &_prefs.tx_power_dbm, sizeof(_prefs.tx_power_dbm)); // 68 - file.write((uint8_t *) &_prefs.telemetry_mode_base, sizeof(_prefs.telemetry_mode_base)); // 69 - file.write((uint8_t *) &_prefs.telemetry_mode_loc, sizeof(_prefs.telemetry_mode_loc)); // 70 - file.write((uint8_t *) &_prefs.telemetry_mode_env, sizeof(_prefs.telemetry_mode_env)); // 71 - file.write((uint8_t *) &_prefs.rx_delay_base, sizeof(_prefs.rx_delay_base)); // 72 - file.write(pad, 4); // 76 - file.write((uint8_t *) &_prefs.ble_pin, sizeof(_prefs.ble_pin)); // 80 - - file.close(); - } - } - - void handleCmdFrame(size_t len) { - if (cmd_frame[0] == CMD_DEVICE_QEURY && len >= 2) { // sent when app establishes connection - app_target_ver = cmd_frame[1]; // which version of protocol does app understand - - int i = 0; - out_frame[i++] = RESP_CODE_DEVICE_INFO; - out_frame[i++] = FIRMWARE_VER_CODE; - out_frame[i++] = MAX_CONTACTS / 2; // v3+ - out_frame[i++] = MAX_GROUP_CHANNELS; // v3+ - memcpy(&out_frame[i], &_prefs.ble_pin, 4); i += 4; - memset(&out_frame[i], 0, 12); - strcpy((char *) &out_frame[i], FIRMWARE_BUILD_DATE); i += 12; - StrHelper::strzcpy((char *) &out_frame[i], board.getManufacturerName(), 40); i += 40; - StrHelper::strzcpy((char *) &out_frame[i], FIRMWARE_VERSION, 20); i += 20; - _serial->writeFrame(out_frame, i); - } else if (cmd_frame[0] == CMD_APP_START && len >= 8) { // sent when app establishes connection, respond with node ID - // cmd_frame[1..7] reserved future - char* app_name = (char *) &cmd_frame[8]; - cmd_frame[len] = 0; // make app_name null terminated - MESH_DEBUG_PRINTLN("App %s connected", app_name); - - _iter_started = false; // stop any left-over ContactsIterator - int i = 0; - out_frame[i++] = RESP_CODE_SELF_INFO; - out_frame[i++] = ADV_TYPE_CHAT; // what this node Advert identifies as (maybe node's pronouns too?? :-) - out_frame[i++] = _prefs.tx_power_dbm; - out_frame[i++] = MAX_LORA_TX_POWER; - memcpy(&out_frame[i], self_id.pub_key, PUB_KEY_SIZE); i += PUB_KEY_SIZE; - - int32_t lat, lon; - lat = (sensors.node_lat * 1000000.0); - lon = (sensors.node_lon * 1000000.0); - memcpy(&out_frame[i], &lat, 4); i += 4; - memcpy(&out_frame[i], &lon, 4); i += 4; - out_frame[i++] = 0; // reserved - out_frame[i++] = 0; // reserved - out_frame[i++] = (_prefs.telemetry_mode_env << 4) | (_prefs.telemetry_mode_loc << 2) | (_prefs.telemetry_mode_base); // v5+ - out_frame[i++] = _prefs.manual_add_contacts; - - uint32_t freq = _prefs.freq * 1000; - memcpy(&out_frame[i], &freq, 4); i += 4; - uint32_t bw = _prefs.bw*1000; - memcpy(&out_frame[i], &bw, 4); i += 4; - out_frame[i++] = _prefs.sf; - out_frame[i++] = _prefs.cr; - - int tlen = strlen(_prefs.node_name); // revisit: UTF_8 ?? - memcpy(&out_frame[i], _prefs.node_name, tlen); i += tlen; - _serial->writeFrame(out_frame, i); - } else if (cmd_frame[0] == CMD_SEND_TXT_MSG && len >= 14) { - int i = 1; - uint8_t txt_type = cmd_frame[i++]; - uint8_t attempt = cmd_frame[i++]; - uint32_t msg_timestamp; - memcpy(&msg_timestamp, &cmd_frame[i], 4); i += 4; - 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)) { - 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) { - result = sendCommandData(*recipient, msg_timestamp, attempt, text, est_timeout); - expected_ack = 0; // no Ack expected - } else { - result = sendMessage(*recipient, msg_timestamp, attempt, text, expected_ack, est_timeout); - } - // TODO: add expected ACK to table - if (result == MSG_SEND_FAILED) { - writeErrFrame(ERR_CODE_TABLE_FULL); - } else { - if (expected_ack) { - expected_ack_table[next_ack_idx].msg_sent = _ms->getMillis(); // add to circular table - expected_ack_table[next_ack_idx].ack = expected_ack; - next_ack_idx = (next_ack_idx + 1) % EXPECTED_ACK_TABLE_SIZE; - } - - out_frame[0] = RESP_CODE_SENT; - out_frame[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0; - memcpy(&out_frame[2], &expected_ack, 4); - memcpy(&out_frame[6], &est_timeout, 4); - _serial->writeFrame(out_frame, 10); - } - } else { - writeErrFrame(recipient == NULL ? ERR_CODE_NOT_FOUND : ERR_CODE_UNSUPPORTED_CMD); // unknown recipient, or unsuported TXT_TYPE_* - } - } else if (cmd_frame[0] == CMD_SEND_CHANNEL_TXT_MSG) { // send GroupChannel msg - int i = 1; - uint8_t txt_type = cmd_frame[i++]; // should be TXT_TYPE_PLAIN - uint8_t channel_idx = cmd_frame[i++]; - uint32_t msg_timestamp; - memcpy(&msg_timestamp, &cmd_frame[i], 4); i += 4; - const char *text = (char *) &cmd_frame[i]; - - if (txt_type != TXT_TYPE_PLAIN) { - writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); - } else { - ChannelDetails channel; - bool success = getChannel(channel_idx, channel); - if (success && sendGroupMessage(msg_timestamp, channel.channel, _prefs.node_name, text, len - i)) { - writeOKFrame(); - } else { - writeErrFrame(ERR_CODE_NOT_FOUND); // bad channel_idx - } - } - } else if (cmd_frame[0] == CMD_GET_CONTACTS) { // get Contact list - if (_iter_started) { - writeErrFrame(ERR_CODE_BAD_STATE); // iterator is currently busy - } else { - if (len >= 5) { // has optional 'since' param - memcpy(&_iter_filter_since, &cmd_frame[1], 4); - } else { - _iter_filter_since = 0; - } - - uint8_t reply[5]; - reply[0] = RESP_CODE_CONTACTS_START; - uint32_t count = getNumContacts(); // total, NOT filtered count - memcpy(&reply[1], &count, 4); - _serial->writeFrame(reply, 5); - - // start iterator - _iter = startContactsIterator(); - _iter_started = true; - _most_recent_lastmod = 0; - } - } else if (cmd_frame[0] == CMD_SET_ADVERT_NAME && len >= 2) { - int nlen = len - 1; - if (nlen > sizeof(_prefs.node_name)-1) nlen = sizeof(_prefs.node_name)-1; // max len - memcpy(_prefs.node_name, &cmd_frame[1], nlen); - _prefs.node_name[nlen] = 0; // null terminator - savePrefs(); - writeOKFrame(); - } else if (cmd_frame[0] == CMD_SET_ADVERT_LATLON && len >= 9) { - int32_t lat, lon, alt = 0; - memcpy(&lat, &cmd_frame[1], 4); - memcpy(&lon, &cmd_frame[5], 4); - if (len >= 13) { - memcpy(&alt, &cmd_frame[9], 4); // for FUTURE support - } - if (lat <= 90*1E6 && lat >= -90*1E6 && lon <= 180*1E6 && lon >= -180*1E6) { - sensors.node_lat = ((double)lat) / 1000000.0; - sensors.node_lon = ((double)lon) / 1000000.0; - savePrefs(); - writeOKFrame(); - } else { - writeErrFrame(ERR_CODE_ILLEGAL_ARG); // invalid geo coordinate - } - } else if (cmd_frame[0] == CMD_GET_DEVICE_TIME) { - uint8_t reply[5]; - reply[0] = RESP_CODE_CURR_TIME; - uint32_t now = getRTCClock()->getCurrentTime(); - memcpy(&reply[1], &now, 4); - _serial->writeFrame(reply, 5); - } else if (cmd_frame[0] == CMD_SET_DEVICE_TIME && len >= 5) { - uint32_t secs; - memcpy(&secs, &cmd_frame[1], 4); - uint32_t curr = getRTCClock()->getCurrentTime(); - if (secs >= curr) { - getRTCClock()->setCurrentTime(secs); - writeOKFrame(); - } else { - writeErrFrame(ERR_CODE_ILLEGAL_ARG); - } - } else if (cmd_frame[0] == CMD_SEND_SELF_ADVERT) { - auto pkt = createSelfAdvert(_prefs.node_name, sensors.node_lat, sensors.node_lon); - if (pkt) { - if (len >= 2 && cmd_frame[1] == 1) { // optional param (1 = flood, 0 = zero hop) - sendFlood(pkt); - } else { - sendZeroHop(pkt); - } - writeOKFrame(); - } else { - writeErrFrame(ERR_CODE_TABLE_FULL); - } - } else if (cmd_frame[0] == CMD_RESET_PATH && len >= 1+32) { - uint8_t* pub_key = &cmd_frame[1]; - ContactInfo* recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); - if (recipient) { - recipient->out_path_len = -1; - //recipient->lastmod = ?? shouldn't be needed, app already has this version of contact - dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); - writeOKFrame(); - } else { - writeErrFrame(ERR_CODE_NOT_FOUND); // unknown contact - } - } else if (cmd_frame[0] == CMD_ADD_UPDATE_CONTACT && len >= 1+32+2+1) { - uint8_t* pub_key = &cmd_frame[1]; - ContactInfo* recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); - if (recipient) { - updateContactFromFrame(*recipient, cmd_frame, len); - //recipient->lastmod = ?? shouldn't be needed, app already has this version of contact - dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); - writeOKFrame(); - } else { - ContactInfo contact; - updateContactFromFrame(contact, cmd_frame, len); - contact.lastmod = getRTCClock()->getCurrentTime(); - contact.sync_since = 0; - if (addContact(contact)) { - dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); - writeOKFrame(); - } else { - writeErrFrame(ERR_CODE_TABLE_FULL); - } - } - } else if (cmd_frame[0] == CMD_REMOVE_CONTACT) { - uint8_t* pub_key = &cmd_frame[1]; - ContactInfo* recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); - if (recipient && removeContact(*recipient)) { - dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); - writeOKFrame(); - } else { - writeErrFrame(ERR_CODE_NOT_FOUND); // not found, or unable to remove - } - } else if (cmd_frame[0] == CMD_SHARE_CONTACT) { - uint8_t* pub_key = &cmd_frame[1]; - ContactInfo* recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); - if (recipient) { - if (shareContactZeroHop(*recipient)) { - writeOKFrame(); - } else { - writeErrFrame(ERR_CODE_TABLE_FULL); // unable to send - } - } else { - writeErrFrame(ERR_CODE_NOT_FOUND); - } - } else if (cmd_frame[0] == CMD_GET_CONTACT_BY_KEY) { - uint8_t* pub_key = &cmd_frame[1]; - ContactInfo* contact = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); - if (contact) { - writeContactRespFrame(RESP_CODE_CONTACT, *contact); - } else { - writeErrFrame(ERR_CODE_NOT_FOUND); // not found - } - } else if (cmd_frame[0] == CMD_EXPORT_CONTACT) { - if (len < 1 + PUB_KEY_SIZE) { - // export SELF - auto pkt = createSelfAdvert(_prefs.node_name, sensors.node_lat, sensors.node_lon); - if (pkt) { - pkt->header |= ROUTE_TYPE_FLOOD; // would normally be sent in this mode - - out_frame[0] = RESP_CODE_EXPORT_CONTACT; - uint8_t out_len = pkt->writeTo(&out_frame[1]); - releasePacket(pkt); // undo the obtainNewPacket() - _serial->writeFrame(out_frame, out_len + 1); - } else { - writeErrFrame(ERR_CODE_TABLE_FULL); // Error - } - } else { - uint8_t* pub_key = &cmd_frame[1]; - ContactInfo* recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); - uint8_t out_len; - if (recipient && (out_len = exportContact(*recipient, &out_frame[1])) > 0) { - out_frame[0] = RESP_CODE_EXPORT_CONTACT; - _serial->writeFrame(out_frame, out_len + 1); - } else { - writeErrFrame(ERR_CODE_NOT_FOUND); // not found - } - } - } else if (cmd_frame[0] == CMD_IMPORT_CONTACT && len > 2+32+64) { - if (importContact(&cmd_frame[1], len - 1)) { - writeOKFrame(); - } else { - writeErrFrame(ERR_CODE_ILLEGAL_ARG); - } - } else if (cmd_frame[0] == CMD_SYNC_NEXT_MESSAGE) { - int out_len; - if ((out_len = getFromOfflineQueue(out_frame)) > 0) { - _serial->writeFrame(out_frame, out_len); - #ifdef DISPLAY_CLASS - ui_task.msgRead(offline_queue_len); - #endif - } else { - out_frame[0] = RESP_CODE_NO_MORE_MESSAGES; - _serial->writeFrame(out_frame, 1); - } - } else if (cmd_frame[0] == CMD_SET_RADIO_PARAMS) { - int i = 1; - uint32_t freq; - memcpy(&freq, &cmd_frame[i], 4); i += 4; - uint32_t bw; - memcpy(&bw, &cmd_frame[i], 4); i += 4; - uint8_t sf = cmd_frame[i++]; - uint8_t cr = cmd_frame[i++]; - - if (freq >= 300000 && freq <= 2500000 && sf >= 7 && sf <= 12 && cr >= 5 && cr <= 8 && bw >= 7000 && bw <= 500000) { - _prefs.sf = sf; - _prefs.cr = cr; - _prefs.freq = (float)freq / 1000.0; - _prefs.bw = (float)bw / 1000.0; - savePrefs(); - - radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); - MESH_DEBUG_PRINTLN("OK: CMD_SET_RADIO_PARAMS: f=%d, bw=%d, sf=%d, cr=%d", freq, bw, (uint32_t)sf, (uint32_t)cr); - - writeOKFrame(); - } else { - MESH_DEBUG_PRINTLN("Error: CMD_SET_RADIO_PARAMS: f=%d, bw=%d, sf=%d, cr=%d", freq, bw, (uint32_t)sf, (uint32_t)cr); - writeErrFrame(ERR_CODE_ILLEGAL_ARG); - } - } else if (cmd_frame[0] == CMD_SET_RADIO_TX_POWER) { - if (cmd_frame[1] > MAX_LORA_TX_POWER) { - writeErrFrame(ERR_CODE_ILLEGAL_ARG); - } else { - _prefs.tx_power_dbm = cmd_frame[1]; - savePrefs(); - radio_set_tx_power(_prefs.tx_power_dbm); - writeOKFrame(); - } - } else if (cmd_frame[0] == CMD_SET_TUNING_PARAMS) { - int i = 1; - uint32_t rx, af; - memcpy(&rx, &cmd_frame[i], 4); i += 4; - memcpy(&af, &cmd_frame[i], 4); i += 4; - _prefs.rx_delay_base = ((float)rx) / 1000.0f; - _prefs.airtime_factor = ((float)af) / 1000.0f; - savePrefs(); - writeOKFrame(); - } else if (cmd_frame[0] == CMD_SET_OTHER_PARAMS) { - _prefs.manual_add_contacts = cmd_frame[1]; - if (len >= 3) { - _prefs.telemetry_mode_base = cmd_frame[2] & 0x03; // v5+ - _prefs.telemetry_mode_loc = (cmd_frame[2] >> 2) & 0x03; - _prefs.telemetry_mode_env = (cmd_frame[2] >> 4) & 0x03; - } - savePrefs(); - writeOKFrame(); - } else if (cmd_frame[0] == CMD_REBOOT && memcmp(&cmd_frame[1], "reboot", 6) == 0) { - if (dirty_contacts_expiry) { // is there are pending dirty contacts write needed? - saveContacts(); - } - board.reboot(); - } else if (cmd_frame[0] == CMD_GET_BATTERY_VOLTAGE) { - uint8_t reply[3]; - reply[0] = RESP_CODE_BATTERY_VOLTAGE; - uint16_t battery_millivolts = board.getBattMilliVolts(); - memcpy(&reply[1], &battery_millivolts, 2); - _serial->writeFrame(reply, 3); - } else if (cmd_frame[0] == CMD_EXPORT_PRIVATE_KEY) { - #if ENABLE_PRIVATE_KEY_EXPORT - uint8_t reply[65]; - reply[0] = RESP_CODE_PRIVATE_KEY; - self_id.writeTo(&reply[1], 64); - _serial->writeFrame(reply, 65); - #else - writeDisabledFrame(); - #endif - } else if (cmd_frame[0] == CMD_IMPORT_PRIVATE_KEY && len >= 65) { - #if ENABLE_PRIVATE_KEY_IMPORT - mesh::LocalIdentity identity; - identity.readFrom(&cmd_frame[1], 64); - if (saveMainIdentity(identity)) { - self_id = identity; - writeOKFrame(); - } else { - writeErrFrame(ERR_CODE_FILE_IO_ERROR); - } - #else - writeDisabledFrame(); - #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(); - } else { - writeErrFrame(ERR_CODE_TABLE_FULL); - } - } else { - writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); // flood, not supported (yet) - } - } else if (cmd_frame[0] == CMD_SEND_LOGIN && len >= 1+PUB_KEY_SIZE) { - uint8_t* pub_key = &cmd_frame[1]; - ContactInfo* recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); - char *password = (char *) &cmd_frame[1+PUB_KEY_SIZE]; - cmd_frame[len] = 0; // ensure null terminator in password - if (recipient) { - uint32_t est_timeout; - int result = sendLogin(*recipient, password, est_timeout); - if (result == MSG_SEND_FAILED) { - writeErrFrame(ERR_CODE_TABLE_FULL); - } else { - pending_telemetry = pending_status = 0; - memcpy(&pending_login, recipient->id.pub_key, 4); // match this to onContactResponse() - out_frame[0] = RESP_CODE_SENT; - out_frame[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0; - memcpy(&out_frame[2], &pending_login, 4); - memcpy(&out_frame[6], &est_timeout, 4); - _serial->writeFrame(out_frame, 10); - } - } else { - writeErrFrame(ERR_CODE_NOT_FOUND); // contact not found - } - } else if (cmd_frame[0] == CMD_SEND_STATUS_REQ && len >= 1+PUB_KEY_SIZE) { - uint8_t* pub_key = &cmd_frame[1]; - ContactInfo* recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); - if (recipient) { - uint32_t tag, est_timeout; - int result = sendRequest(*recipient, REQ_TYPE_GET_STATUS, tag, est_timeout); - if (result == MSG_SEND_FAILED) { - writeErrFrame(ERR_CODE_TABLE_FULL); - } else { - pending_telemetry = pending_login = 0; - // FUTURE: pending_status = tag; // match this in onContactResponse() - memcpy(&pending_status, recipient->id.pub_key, 4); // legacy matching scheme - out_frame[0] = RESP_CODE_SENT; - out_frame[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0; - memcpy(&out_frame[2], &tag, 4); - memcpy(&out_frame[6], &est_timeout, 4); - _serial->writeFrame(out_frame, 10); - } - } else { - writeErrFrame(ERR_CODE_NOT_FOUND); // contact not found - } - } else if (cmd_frame[0] == CMD_SEND_TELEMETRY_REQ && len >= 4+PUB_KEY_SIZE) { - uint8_t* pub_key = &cmd_frame[4]; - ContactInfo* recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); - if (recipient) { - uint32_t tag, est_timeout; - int result = sendRequest(*recipient, REQ_TYPE_GET_TELEMETRY_DATA, tag, est_timeout); - if (result == MSG_SEND_FAILED) { - writeErrFrame(ERR_CODE_TABLE_FULL); - } else { - pending_status = pending_login = 0; - pending_telemetry = tag; // match this in onContactResponse() - out_frame[0] = RESP_CODE_SENT; - out_frame[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0; - memcpy(&out_frame[2], &tag, 4); - memcpy(&out_frame[6], &est_timeout, 4); - _serial->writeFrame(out_frame, 10); - } - } else { - writeErrFrame(ERR_CODE_NOT_FOUND); // contact not found - } - } else if (cmd_frame[0] == CMD_HAS_CONNECTION && len >= 1+PUB_KEY_SIZE) { - uint8_t* pub_key = &cmd_frame[1]; - if (hasConnectionTo(pub_key)) { - writeOKFrame(); - } else { - writeErrFrame(ERR_CODE_NOT_FOUND); - } - } else if (cmd_frame[0] == CMD_LOGOUT && len >= 1+PUB_KEY_SIZE) { - uint8_t* pub_key = &cmd_frame[1]; - stopConnection(pub_key); - writeOKFrame(); - } else if (cmd_frame[0] == CMD_GET_CHANNEL && len >= 2) { - uint8_t channel_idx = cmd_frame[1]; - ChannelDetails channel; - if (getChannel(channel_idx, channel)) { - int i = 0; - out_frame[i++] = RESP_CODE_CHANNEL_INFO; - out_frame[i++] = channel_idx; - strcpy((char *)&out_frame[i], channel.name); i += 32; - memcpy(&out_frame[i], channel.channel.secret, 16); i += 16; // NOTE: only 128-bit supported - _serial->writeFrame(out_frame, i); - } else { - writeErrFrame(ERR_CODE_NOT_FOUND); - } - } else if (cmd_frame[0] == CMD_SET_CHANNEL && len >= 2+32+32) { - writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); // not supported (yet) - } else if (cmd_frame[0] == CMD_SET_CHANNEL && len >= 2+32+16) { - uint8_t channel_idx = cmd_frame[1]; - ChannelDetails channel; - StrHelper::strncpy(channel.name, (char *) &cmd_frame[2], 32); - memset(channel.channel.secret, 0, sizeof(channel.channel.secret)); - memcpy(channel.channel.secret, &cmd_frame[2+32], 16); // NOTE: only 128-bit supported - if (setChannel(channel_idx, channel)) { - saveChannels(); - writeOKFrame(); - } else { - writeErrFrame(ERR_CODE_NOT_FOUND); // bad channel_idx - } - } else if (cmd_frame[0] == CMD_SIGN_START) { - out_frame[0] = RESP_CODE_SIGN_START; - out_frame[1] = 0; // reserved - uint32_t len = MAX_SIGN_DATA_LEN; - memcpy(&out_frame[2], &len, 4); - _serial->writeFrame(out_frame, 6); - - if (sign_data) { - free(sign_data); - } - sign_data = (uint8_t *) malloc(MAX_SIGN_DATA_LEN); - sign_data_len = 0; - } else if (cmd_frame[0] == CMD_SIGN_DATA && len > 1) { - if (sign_data == NULL || sign_data_len + (len - 1) > MAX_SIGN_DATA_LEN) { - writeErrFrame(sign_data == NULL ? ERR_CODE_BAD_STATE : ERR_CODE_TABLE_FULL); // error: too long - } else { - memcpy(&sign_data[sign_data_len], &cmd_frame[1], len - 1); - sign_data_len += (len - 1); - writeOKFrame(); - } - } else if (cmd_frame[0] == CMD_SIGN_FINISH) { - if (sign_data) { - self_id.sign(&out_frame[1], sign_data, sign_data_len); - - free(sign_data); // don't need sign_data now - sign_data = NULL; - - out_frame[0] = RESP_CODE_SIGNATURE; - _serial->writeFrame(out_frame, 1 + SIGNATURE_SIZE); - } else { - writeErrFrame(ERR_CODE_BAD_STATE); - } - } else if (cmd_frame[0] == CMD_SEND_TRACE_PATH && len > 10 && len - 10 < MAX_PATH_SIZE) { - uint32_t tag, auth; - memcpy(&tag, &cmd_frame[1], 4); - memcpy(&auth, &cmd_frame[5], 4); - auto pkt = createTrace(tag, auth, cmd_frame[9]); - if (pkt) { - uint8_t path_len = len - 10; - sendDirect(pkt, &cmd_frame[10], path_len); - - uint32_t t = _radio->getEstAirtimeFor(pkt->payload_len + pkt->path_len + 2); - uint32_t est_timeout = calcDirectTimeoutMillisFor(t, path_len); - - out_frame[0] = RESP_CODE_SENT; - out_frame[1] = 0; - memcpy(&out_frame[2], &tag, 4); - memcpy(&out_frame[6], &est_timeout, 4); - _serial->writeFrame(out_frame, 10); - } else { - writeErrFrame(ERR_CODE_TABLE_FULL); - } - } else if (cmd_frame[0] == CMD_SET_DEVICE_PIN && len >= 5) { - - // get pin from command frame - uint32_t pin; - memcpy(&pin, &cmd_frame[1], 4); - - // ensure pin is zero, or a valid 6 digit pin - if(pin == 0 || (pin >= 100000 && pin <= 999999)){ - _prefs.ble_pin = pin; - savePrefs(); - writeOKFrame(); - } else { - writeErrFrame(ERR_CODE_ILLEGAL_ARG); - } - - } else if (cmd_frame[0] == CMD_GET_CUSTOM_VARS) { - out_frame[0] = RESP_CODE_CUSTOM_VARS; - char* dp = (char *) &out_frame[1]; - for (int i = 0; i < sensors.getNumSettings() && dp - (char *) &out_frame[1] < 140; i++) { - if (i > 0) { *dp++ = ','; } - strcpy(dp, sensors.getSettingName(i)); dp = strchr(dp, 0); - *dp++ = ':'; - strcpy(dp, sensors.getSettingValue(i)); dp = strchr(dp, 0); - } - _serial->writeFrame(out_frame, dp - (char *)out_frame); - } else if (cmd_frame[0] == CMD_SET_CUSTOM_VAR && len >= 4) { - cmd_frame[len] = 0; - char* sp = (char *) &cmd_frame[1]; - char* np = strchr(sp, ':'); // look for separator char - if (np) { - *np++ = 0; // modify 'cmd_frame', replace ':' with null - bool success = sensors.setSettingValue(sp, np); - if (success) { - writeOKFrame(); - } else { - writeErrFrame(ERR_CODE_ILLEGAL_ARG); - } - } else { - writeErrFrame(ERR_CODE_ILLEGAL_ARG); - } - } else { - writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); - MESH_DEBUG_PRINTLN("ERROR: unknown command: %02X", cmd_frame[0]); - } - } - - void loop() { - BaseChatMesh::loop(); - - size_t len = _serial->checkRecvFrame(cmd_frame); - if (len > 0) { - handleCmdFrame(len); - } else if (_iter_started // check if our ContactsIterator is 'running' - && !_serial->isWriteBusy() // don't spam the Serial Interface too quickly! - ) { - ContactInfo contact; - if (_iter.hasNext(this, contact)) { - if (contact.lastmod > _iter_filter_since) { // apply the 'since' filter - writeContactRespFrame(RESP_CODE_CONTACT, contact); - if (contact.lastmod > _most_recent_lastmod) { - _most_recent_lastmod = contact.lastmod; // save for the RESP_CODE_END_OF_CONTACTS frame - } - } - } else { // EOF - out_frame[0] = RESP_CODE_END_OF_CONTACTS; - memcpy(&out_frame[1], &_most_recent_lastmod, 4); // include the most recent lastmod, so app can update their 'since' - _serial->writeFrame(out_frame, 5); - _iter_started = false; - } - } else if (!_serial->isWriteBusy()) { - checkConnections(); - } - - // is there are pending dirty contacts write needed? - if (dirty_contacts_expiry && millisHasNowPassed(dirty_contacts_expiry)) { - saveContacts(); - dirty_contacts_expiry = 0; - } - - #ifdef DISPLAY_CLASS - ui_task.setHasConnection(_serial->isConnected()); - ui_task.loop(); - #endif - } -}; #ifdef ESP32 #ifdef WIFI_SSID @@ -1648,9 +74,16 @@ public: #error "need to define a serial interface" #endif +/* GLOBAL OBJECTS */ StdRNG fast_rng; SimpleMeshTables tables; -MyMesh the_mesh(radio_driver, fast_rng, rtc_clock, tables); +MyMesh the_mesh(radio_driver, fast_rng, rtc_clock, tables, store); + +#ifdef DISPLAY_CLASS + #include "UITask.h" + UITask ui_task(&board); +#endif +/* END GLOBAL OBJECTS */ void halt() { while (1) ; @@ -1677,7 +110,8 @@ void setup() { #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) InternalFS.begin(); - the_mesh.begin(InternalFS, + store.begin(); + the_mesh.begin( #ifdef DISPLAY_CLASS disp != NULL #else @@ -1695,7 +129,8 @@ void setup() { the_mesh.startInterface(serial_interface); #elif defined(RP2040_PLATFORM) LittleFS.begin(); - the_mesh.begin(LittleFS, + store.begin(); + the_mesh.begin( #ifdef DISPLAY_CLASS disp != NULL #else @@ -1720,7 +155,8 @@ void setup() { the_mesh.startInterface(serial_interface); #elif defined(ESP32) SPIFFS.begin(true); - the_mesh.begin(SPIFFS, + store.begin(); + the_mesh.begin( #ifdef DISPLAY_CLASS disp != NULL #else @@ -1750,7 +186,7 @@ void setup() { sensors.begin(); #ifdef DISPLAY_CLASS - ui_task.begin(disp, the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION, the_mesh.getBLEPin()); + ui_task.begin(disp, the_mesh.getNodePrefs()); // still want to pass this in as dependency, as prefs might be moved #endif } diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index 5db62ff1..c33cadda 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -22,11 +22,11 @@ /* ------------------------------ Config -------------------------------- */ #ifndef FIRMWARE_BUILD_DATE - #define FIRMWARE_BUILD_DATE "24 May 2025" + #define FIRMWARE_BUILD_DATE "7 Jun 2025" #endif #ifndef FIRMWARE_VERSION - #define FIRMWARE_VERSION "v1.6.2" + #define FIRMWARE_VERSION "v1.7.0" #endif #ifndef LORA_FREQ @@ -59,6 +59,14 @@ #define ADMIN_PASSWORD "password" #endif +#ifndef SERVER_RESPONSE_DELAY + #define SERVER_RESPONSE_DELAY 300 +#endif + +#ifndef TXT_ACK_DELAY + #define TXT_ACK_DELAY 200 +#endif + #ifdef DISPLAY_CLASS #include "UITask.h" static UITask ui_task(display); @@ -112,8 +120,7 @@ struct NeighbourInfo { int8_t snr; // multiplied by 4, user should divide to get float value }; -// NOTE: need to space the ACK and the reply text apart (in CLI) -#define CLI_REPLY_DELAY_MILLIS 1500 +#define CLI_REPLY_DELAY_MILLIS 1000 class MyMesh : public mesh::Mesh, public CommonCLICallbacks { FILESYSTEM* _fs; @@ -330,6 +337,9 @@ protected: int getInterferenceThreshold() const override { return _prefs.interference_threshold; } + int getAGCResetInterval() const override { + return ((int)_prefs.agc_reset_interval) * 4000; // milliseconds + } void onAnonDataRecv(mesh::Packet* packet, uint8_t type, const mesh::Identity& sender, uint8_t* data, size_t len) override { if (type == PAYLOAD_TYPE_ANON_REQ) { // received an initial request by a possible admin client (unknown at this stage) @@ -376,14 +386,14 @@ protected: // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response mesh::Packet* path = createPathReturn(sender, client->secret, packet->path, packet->path_len, PAYLOAD_TYPE_RESPONSE, reply_data, 12); - if (path) sendFlood(path); + if (path) sendFlood(path, SERVER_RESPONSE_DELAY); } else { mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, client->secret, reply_data, 12); if (reply) { if (client->out_path_len >= 0) { // we have an out_path, so send DIRECT - sendDirect(reply, client->out_path, client->out_path_len); + sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY); } else { - sendFlood(reply); + sendFlood(reply, SERVER_RESPONSE_DELAY); } } } @@ -446,14 +456,14 @@ protected: // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response mesh::Packet* path = createPathReturn(client->id, secret, packet->path, packet->path_len, PAYLOAD_TYPE_RESPONSE, reply_data, reply_len); - if (path) sendFlood(path); + if (path) sendFlood(path, SERVER_RESPONSE_DELAY); } else { mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, client->id, secret, reply_data, reply_len); if (reply) { if (client->out_path_len >= 0) { // we have an out_path, so send DIRECT - sendDirect(reply, client->out_path, client->out_path_len); + sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY); } else { - sendFlood(reply); + sendFlood(reply, SERVER_RESPONSE_DELAY); } } } @@ -482,9 +492,9 @@ protected: mesh::Packet* ack = createAck(ack_hash); if (ack) { if (client->out_path_len < 0) { - sendFlood(ack); + sendFlood(ack, TXT_ACK_DELAY); } else { - sendDirect(ack, client->out_path, client->out_path_len); + sendDirect(ack, client->out_path, client->out_path_len, TXT_ACK_DELAY); } } } @@ -568,7 +578,7 @@ public: _prefs.advert_interval = 1; // default to 2 minutes for NEW installs _prefs.flood_advert_interval = 3; // 3 hours _prefs.flood_max = 64; - _prefs.interference_threshold = 14; // DB + _prefs.interference_threshold = 0; // disabled } CommonCLI* getCLI() { return &_cli; } diff --git a/examples/simple_room_server/main.cpp b/examples/simple_room_server/main.cpp index 5ba6cbca..d9d9f122 100644 --- a/examples/simple_room_server/main.cpp +++ b/examples/simple_room_server/main.cpp @@ -22,11 +22,11 @@ /* ------------------------------ Config -------------------------------- */ #ifndef FIRMWARE_BUILD_DATE - #define FIRMWARE_BUILD_DATE "24 May 2025" + #define FIRMWARE_BUILD_DATE "7 Jun 2025" #endif #ifndef FIRMWARE_VERSION - #define FIRMWARE_VERSION "v1.6.2" + #define FIRMWARE_VERSION "v1.7.0" #endif #ifndef LORA_FREQ @@ -67,6 +67,14 @@ #define MAX_UNSYNCED_POSTS 32 #endif +#ifndef SERVER_RESPONSE_DELAY + #define SERVER_RESPONSE_DELAY 300 +#endif + +#ifndef TXT_ACK_DELAY + #define TXT_ACK_DELAY 200 +#endif + #ifdef DISPLAY_CLASS #include "UITask.h" static UITask ui_task(display); @@ -115,7 +123,7 @@ struct PostInfo { #define PUSH_TIMEOUT_BASE 4000 #define PUSH_ACK_TIMEOUT_FACTOR 2000 -#define CLIENT_KEEP_ALIVE_SECS 128 +#define CLIENT_KEEP_ALIVE_SECS 0 // Now Disabled (was 128) #define REQ_TYPE_GET_STATUS 0x01 // same as _GET_STATS #define REQ_TYPE_KEEP_ALIVE 0x02 @@ -204,7 +212,10 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { void pushPostToClient(ClientInfo* client, PostInfo& post) { int len = 0; memcpy(&reply_data[len], &post.post_timestamp, 4); len += 4; // this is a PAST timestamp... but should be accepted by client - reply_data[len++] = (TXT_TYPE_SIGNED_PLAIN << 2); // 'signed' plain text + + uint8_t attempt; + getRNG()->random(&attempt, 1); // need this for re-tries, so packet hash (and ACK) will be different + reply_data[len++] = (TXT_TYPE_SIGNED_PLAIN << 2) | (attempt & 3); // 'signed' plain text // encode prefix of post.author.pub_key memcpy(&reply_data[len], post.author.pub_key, 4); len += 4; // just first 4 bytes @@ -409,6 +420,9 @@ protected: int getInterferenceThreshold() const override { return _prefs.interference_threshold; } + int getAGCResetInterval() const override { + return ((int)_prefs.agc_reset_interval) * 4000; // milliseconds + } bool allowPacketForward(const mesh::Packet* packet) override { if (_prefs.disable_fwd) return false; @@ -468,14 +482,14 @@ protected: // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response mesh::Packet* path = createPathReturn(sender, client->secret, packet->path, packet->path_len, PAYLOAD_TYPE_RESPONSE, reply_data, 8 + 2); - if (path) sendFlood(path); + if (path) sendFlood(path, SERVER_RESPONSE_DELAY); } else { mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, client->secret, reply_data, 8 + 2); if (reply) { if (client->out_path_len >= 0) { // we have an out_path, so send DIRECT - sendDirect(reply, client->out_path, client->out_path_len); + sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY); } else { - sendFlood(reply); + sendFlood(reply, SERVER_RESPONSE_DELAY); } } } @@ -565,12 +579,12 @@ protected: mesh::Packet* ack = createAck(ack_hash); if (ack) { if (client->out_path_len < 0) { - sendFlood(ack); + sendFlood(ack, TXT_ACK_DELAY); } else { - sendDirect(ack, client->out_path, client->out_path_len); + sendDirect(ack, client->out_path, client->out_path_len, TXT_ACK_DELAY); } } - delay_millis = REPLY_DELAY_MILLIS; + delay_millis = TXT_ACK_DELAY + REPLY_DELAY_MILLIS; } else { delay_millis = 0; } @@ -589,9 +603,9 @@ protected: auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, secret, temp, 5 + text_len); if (reply) { if (client->out_path_len < 0) { - sendFlood(reply, delay_millis); + sendFlood(reply, delay_millis + SERVER_RESPONSE_DELAY); } else { - sendDirect(reply, client->out_path, client->out_path_len, delay_millis); + sendDirect(reply, client->out_path, client->out_path_len, delay_millis + SERVER_RESPONSE_DELAY); } } } @@ -634,7 +648,7 @@ protected: auto reply = createAck(ack_hash); if (reply) { reply->payload[reply->payload_len++] = getUnsyncedCount(client); // NEW: add unsynced counter to end of ACK packet - sendDirect(reply, client->out_path, client->out_path_len); + sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY); } } } else { @@ -644,14 +658,14 @@ protected: // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response mesh::Packet* path = createPathReturn(client->id, secret, packet->path, packet->path_len, PAYLOAD_TYPE_RESPONSE, reply_data, reply_len); - if (path) sendFlood(path); + if (path) sendFlood(path, SERVER_RESPONSE_DELAY); } else { mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, client->id, secret, reply_data, reply_len); if (reply) { if (client->out_path_len >= 0) { // we have an out_path, so send DIRECT - sendDirect(reply, client->out_path, client->out_path_len); + sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY); } else { - sendFlood(reply); + sendFlood(reply, SERVER_RESPONSE_DELAY); } } } @@ -714,7 +728,7 @@ public: _prefs.advert_interval = 1; // default to 2 minutes for NEW installs _prefs.flood_advert_interval = 3; // 3 hours _prefs.flood_max = 64; - _prefs.interference_threshold = 14; // DB + _prefs.interference_threshold = 0; // disabled #ifdef ROOM_PASSWORD StrHelper::strncpy(_prefs.guest_password, ROOM_PASSWORD, sizeof(_prefs.guest_password)); #endif diff --git a/examples/simple_secure_chat/main.cpp b/examples/simple_secure_chat/main.cpp index 63ff20da..a6b048a1 100644 --- a/examples/simple_secure_chat/main.cpp +++ b/examples/simple_secure_chat/main.cpp @@ -202,7 +202,7 @@ protected: return true; } - void onDiscoveredContact(ContactInfo& contact, bool is_new) override { + void onDiscoveredContact(ContactInfo& contact, bool is_new, uint8_t path_len, const uint8_t* path) override { // TODO: if not in favs, prompt to add as fav(?) Serial.printf("ADVERT from -> %s\n", contact.name); diff --git a/logo/meshcore.afdesign b/logo/meshcore.afdesign new file mode 100644 index 00000000..17cc02a9 Binary files /dev/null and b/logo/meshcore.afdesign differ diff --git a/logo/meshcore.png b/logo/meshcore.png new file mode 100644 index 00000000..1ec8508c Binary files /dev/null and b/logo/meshcore.png differ diff --git a/logo/meshcore.svg b/logo/meshcore.svg new file mode 100644 index 00000000..eb1b7bc4 --- /dev/null +++ b/logo/meshcore.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/logo/meshcore_tm.svg b/logo/meshcore_tm.svg new file mode 100644 index 00000000..b7e252d9 --- /dev/null +++ b/logo/meshcore_tm.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/platformio.ini b/platformio.ini index 94c961cf..90e7cfb0 100644 --- a/platformio.ini +++ b/platformio.ini @@ -27,6 +27,8 @@ build_flags = -w -DNDEBUG -DRADIOLIB_STATIC_ONLY=1 -DRADIOLIB_GODMODE=1 -D LORA_FREQ=869.525 -D LORA_BW=250 -D LORA_SF=11 + -D ENABLE_PRIVATE_KEY_IMPORT=1 ; NOTE: comment these out for more secure firmware + -D ENABLE_PRIVATE_KEY_EXPORT=1 build_src_filter = +<*.cpp> + @@ -35,7 +37,7 @@ build_src_filter = [esp32_base] extends = arduino_base -platform = espressif32 +platform = platformio/espressif32@^6.11.0 monitor_filters = esp32_exception_decoder extra_scripts = merge-bin.py build_flags = ${arduino_base.build_flags} @@ -50,7 +52,7 @@ lib_deps = ; esp32c6 uses arduino framework 3.x [esp32c6_base] extends = esp32_base -platform = https://github.com/pioarduino/platform-espressif32.git +platform = https://github.com/pioarduino/platform-espressif32/releases/download/stable/platform-espressif32.zip ; ----------------- NRF52 --------------------- @@ -59,6 +61,7 @@ extends = arduino_base platform = nordicnrf52 build_flags = ${arduino_base.build_flags} -D NRF52_PLATFORM + -D LFS_NO_ASSERT=1 [nrf52840_base] extends = nrf52_base @@ -72,6 +75,9 @@ lib_deps = [rp2040_base] extends = arduino_base +upload_protocol = picotool +board_build.core = earlephilhower +platform = https://github.com/maxgerhardt/platform-raspberrypi.git build_flags = ${arduino_base.build_flags} -D RP2040_PLATFORM @@ -88,4 +94,4 @@ build_flags = ${arduino_base.build_flags} build_src_filter = ${arduino_base.build_src_filter} + lib_deps = ${arduino_base.lib_deps} - file://arch/stm32/Adafruit_LittleFS_stm32 \ No newline at end of file + file://arch/stm32/Adafruit_LittleFS_stm32 diff --git a/src/Dispatcher.cpp b/src/Dispatcher.cpp index 7ac5cbe3..7f39dc49 100644 --- a/src/Dispatcher.cpp +++ b/src/Dispatcher.cpp @@ -87,6 +87,14 @@ void Dispatcher::loop() { } else { return; // can't do any more radio activity until send is complete or timed out } + + // going back into receive mode now... + next_agc_reset_time = futureMillis(getAGCResetInterval()); + } + + if (getAGCResetInterval() > 0 && millisHasNowPassed(next_agc_reset_time)) { + _radio->resetAGC(); + next_agc_reset_time = futureMillis(getAGCResetInterval()); } // check inbound (delayed) queue diff --git a/src/Dispatcher.h b/src/Dispatcher.h index bce13b6b..2200f81b 100644 --- a/src/Dispatcher.h +++ b/src/Dispatcher.h @@ -65,6 +65,8 @@ public: virtual void triggerNoiseFloorCalibrate(int threshold) { } + virtual void resetAGC() { } + virtual bool isInRecvMode() const = 0; /** @@ -116,7 +118,7 @@ class Dispatcher { unsigned long next_tx_time; unsigned long cad_busy_start; unsigned long radio_nonrx_start; - unsigned long next_floor_calib_time; + unsigned long next_floor_calib_time, next_agc_reset_time; bool prev_isrecv_mode; uint32_t n_sent_flood, n_sent_direct; uint32_t n_recv_flood, n_recv_direct; @@ -134,7 +136,7 @@ protected: { outbound = NULL; total_air_time = 0; next_tx_time = 0; cad_busy_start = 0; - next_floor_calib_time = 0; + next_floor_calib_time = next_agc_reset_time = 0; _err_flags = 0; radio_nonrx_start = 0; prev_isrecv_mode = true; @@ -154,6 +156,7 @@ protected: virtual uint32_t getCADFailRetryDelay() const; virtual uint32_t getCADFailMaxDuration() const; virtual int getInterferenceThreshold() const { return 0; } // disabled by default + virtual int getAGCResetInterval() const { return 0; } // disabled by default public: void begin(); diff --git a/src/Mesh.cpp b/src/Mesh.cpp index 6029c192..a6b06c07 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -142,7 +142,7 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { if (pkt->isRouteFlood()) { // send a reciprocal return path to sender, but send DIRECTLY! mesh::Packet* rpath = createPathReturn(&src_hash, secret, pkt->path, pkt->path_len, 0, NULL, 0); - if (rpath) sendDirect(rpath, path, path_len); + if (rpath) sendDirect(rpath, path, path_len, 500); } } } else { @@ -518,7 +518,11 @@ void Mesh::sendDirect(Packet* packet, const uint8_t* path, uint8_t path_len, uin pri = 5; // maybe make this configurable } else { memcpy(packet->path, path, packet->path_len = path_len); - pri = 0; + if (packet->getPayloadType() == PAYLOAD_TYPE_PATH) { + pri = 1; // slightly less priority + } else { + pri = 0; + } } _tables->hasSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us sendPacket(packet, pri, delay_millis); diff --git a/src/helpers/BaseChatMesh.cpp b/src/helpers/BaseChatMesh.cpp index 36ddcbb4..4649040e 100644 --- a/src/helpers/BaseChatMesh.cpp +++ b/src/helpers/BaseChatMesh.cpp @@ -1,6 +1,14 @@ #include #include +#ifndef SERVER_RESPONSE_DELAY + #define SERVER_RESPONSE_DELAY 300 +#endif + +#ifndef TXT_ACK_DELAY + #define TXT_ACK_DELAY 200 +#endif + mesh::Packet* BaseChatMesh::createSelfAdvert(const char* name, double lat, double lon) { uint8_t app_data[MAX_ADVERT_DATA_SIZE]; uint8_t app_data_len; @@ -50,7 +58,7 @@ void BaseChatMesh::onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, } ci.last_advert_timestamp = timestamp; ci.lastmod = getRTCClock()->getCurrentTime(); - onDiscoveredContact(ci, true); // let UI know + onDiscoveredContact(ci, true, packet->path_len, packet->path); // let UI know return; } @@ -81,7 +89,7 @@ void BaseChatMesh::onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, from->last_advert_timestamp = timestamp; from->lastmod = getRTCClock()->getCurrentTime(); - onDiscoveredContact(*from, is_new); // let UI know + onDiscoveredContact(*from, is_new, packet->path_len, packet->path); // let UI know } int BaseChatMesh::searchPeersByHash(const uint8_t* hash) { @@ -131,14 +139,14 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the ACK mesh::Packet* path = createPathReturn(from.id, secret, packet->path, packet->path_len, PAYLOAD_TYPE_ACK, (uint8_t *) &ack_hash, 4); - if (path) sendFlood(path); + if (path) sendFlood(path, TXT_ACK_DELAY); } else { mesh::Packet* ack = createAck(ack_hash); if (ack) { if (from.out_path_len < 0) { - sendFlood(ack); + sendFlood(ack, TXT_ACK_DELAY); } else { - sendDirect(ack, from.out_path, from.out_path_len); + sendDirect(ack, from.out_path, from.out_path_len, TXT_ACK_DELAY); } } } @@ -164,14 +172,14 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the ACK mesh::Packet* path = createPathReturn(from.id, secret, packet->path, packet->path_len, PAYLOAD_TYPE_ACK, (uint8_t *) &ack_hash, 4); - if (path) sendFlood(path); + if (path) sendFlood(path, TXT_ACK_DELAY); } else { mesh::Packet* ack = createAck(ack_hash); if (ack) { if (from.out_path_len < 0) { - sendFlood(ack); + sendFlood(ack, TXT_ACK_DELAY); } else { - sendDirect(ack, from.out_path, from.out_path_len); + sendDirect(ack, from.out_path, from.out_path_len, TXT_ACK_DELAY); } } } @@ -187,14 +195,14 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response mesh::Packet* path = createPathReturn(from.id, secret, packet->path, packet->path_len, PAYLOAD_TYPE_RESPONSE, temp_buf, reply_len); - if (path) sendFlood(path); + if (path) sendFlood(path, SERVER_RESPONSE_DELAY); } else { mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, from.id, secret, temp_buf, reply_len); if (reply) { if (from.out_path_len >= 0) { // we have an out_path, so send DIRECT - sendDirect(reply, from.out_path, from.out_path_len); + sendDirect(reply, from.out_path, from.out_path_len, SERVER_RESPONSE_DELAY); } else { - sendFlood(reply); + sendFlood(reply, SERVER_RESPONSE_DELAY); } } } @@ -691,6 +699,13 @@ int BaseChatMesh::findChannelIdx(const mesh::GroupChannel& ch) { } #endif +bool BaseChatMesh::getContactByIdx(uint32_t idx, ContactInfo& contact) { + if (idx >= num_contacts) return false; + + contact = contacts[idx]; + return true; +} + ContactsIterator BaseChatMesh::startContactsIterator() { return ContactsIterator(); } diff --git a/src/helpers/BaseChatMesh.h b/src/helpers/BaseChatMesh.h index 2222bf4e..de73ea78 100644 --- a/src/helpers/BaseChatMesh.h +++ b/src/helpers/BaseChatMesh.h @@ -7,19 +7,7 @@ #define MAX_TEXT_LEN (10*CIPHER_BLOCK_SIZE) // must be LESS than (MAX_PACKET_PAYLOAD - 4 - CIPHER_MAC_SIZE - 1) -struct ContactInfo { - mesh::Identity id; - char name[32]; - uint8_t type; // on of ADV_TYPE_* - uint8_t flags; - int8_t out_path_len; - uint8_t out_path[MAX_PATH_SIZE]; - uint32_t last_advert_timestamp; // by THEIR clock - uint8_t shared_secret[PUB_KEY_SIZE]; - uint32_t lastmod; // by OUR clock - int32_t gps_lat, gps_lon; // 6 dec places - uint32_t sync_since; -}; +#include "ContactInfo.h" #define MAX_SEARCH_RESULTS 8 @@ -61,10 +49,7 @@ struct ConnectionInfo { uint32_t expected_ack; }; -struct ChannelDetails { - mesh::GroupChannel channel; - char name[32]; -}; +#include "ChannelDetails.h" /** * \brief abstract Mesh class for common 'chat' client @@ -104,7 +89,7 @@ protected: // 'UI' concepts, for sub-classes to implement virtual bool isAutoAddEnabled() const { return true; } - virtual void onDiscoveredContact(ContactInfo& contact, bool is_new) = 0; + virtual void onDiscoveredContact(ContactInfo& contact, bool is_new, uint8_t path_len, const uint8_t* path) = 0; virtual bool processAck(const uint8_t *data) = 0; virtual void onContactPathUpdated(const ContactInfo& contact) = 0; virtual void onMessageRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text) = 0; @@ -158,6 +143,7 @@ public: bool removeContact(ContactInfo& contact); bool addContact(const ContactInfo& contact); int getNumContacts() const { return num_contacts; } + bool getContactByIdx(uint32_t idx, ContactInfo& contact); ContactsIterator startContactsIterator(); ChannelDetails* addChannel(const char* name, const char* psk_base64); bool getChannel(int idx, ChannelDetails& dest); diff --git a/src/helpers/ChannelDetails.h b/src/helpers/ChannelDetails.h new file mode 100644 index 00000000..b9d38d4f --- /dev/null +++ b/src/helpers/ChannelDetails.h @@ -0,0 +1,9 @@ +#pragma once + +#include +#include + +struct ChannelDetails { + mesh::GroupChannel channel; + char name[32]; +}; diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index baad8f40..63e2740c 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -53,7 +53,8 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { file.read((uint8_t *) &_prefs->allow_read_only, sizeof(_prefs->allow_read_only)); // 114 file.read((uint8_t *) &_prefs->reserved2, sizeof(_prefs->reserved2)); // 115 file.read((uint8_t *) &_prefs->bw, sizeof(_prefs->bw)); // 116 - file.read(pad, 4); // 120 + file.read((uint8_t *) &_prefs->agc_reset_interval, sizeof(_prefs->agc_reset_interval)); // 120 + file.read(pad, 3); // 121 file.read((uint8_t *) &_prefs->flood_max, sizeof(_prefs->flood_max)); // 124 file.read((uint8_t *) &_prefs->flood_advert_interval, sizeof(_prefs->flood_advert_interval)); // 125 file.read((uint8_t *) &_prefs->interference_threshold, sizeof(_prefs->interference_threshold)); // 126 @@ -107,7 +108,8 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) { file.write((uint8_t *) &_prefs->allow_read_only, sizeof(_prefs->allow_read_only)); // 114 file.write((uint8_t *) &_prefs->reserved2, sizeof(_prefs->reserved2)); // 115 file.write((uint8_t *) &_prefs->bw, sizeof(_prefs->bw)); // 116 - file.write(pad, 4); // 120 + file.write((uint8_t *) &_prefs->agc_reset_interval, sizeof(_prefs->agc_reset_interval)); // 120 + file.write(pad, 3); // 121 file.write((uint8_t *) &_prefs->flood_max, sizeof(_prefs->flood_max)); // 124 file.write((uint8_t *) &_prefs->flood_advert_interval, sizeof(_prefs->flood_advert_interval)); // 125 file.write((uint8_t *) &_prefs->interference_threshold, sizeof(_prefs->interference_threshold)); // 126 @@ -180,6 +182,8 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch 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, "agc.reset.interval", 18) == 0) { + sprintf(reply, "> %d", ((uint32_t) _prefs->agc_reset_interval) * 4); } else 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) { @@ -231,6 +235,10 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch _prefs->interference_threshold = atoi(&config[11]); savePrefs(); strcpy(reply, "OK"); + } else if (memcmp(config, "agc.reset.interval ", 19) == 0) { + _prefs->agc_reset_interval = atoi(&config[19]) / 4; + savePrefs(); + strcpy(reply, "OK"); } else if (memcmp(config, "allow.read.only ", 16) == 0) { _prefs->allow_read_only = memcmp(&config[16], "on", 2) == 0; savePrefs(); diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 37402c09..1778c715 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -25,6 +25,7 @@ struct NodePrefs { // persisted to file float bw; uint8_t flood_max; uint8_t interference_threshold; + uint8_t agc_reset_interval; // secs / 4 }; class CommonCLICallbacks { diff --git a/src/helpers/ContactInfo.h b/src/helpers/ContactInfo.h new file mode 100644 index 00000000..4a8038d3 --- /dev/null +++ b/src/helpers/ContactInfo.h @@ -0,0 +1,18 @@ +#pragma once + +#include +#include + +struct ContactInfo { + mesh::Identity id; + char name[32]; + uint8_t type; // on of ADV_TYPE_* + uint8_t flags; + int8_t out_path_len; + uint8_t out_path[MAX_PATH_SIZE]; + uint32_t last_advert_timestamp; // by THEIR clock + uint8_t shared_secret[PUB_KEY_SIZE]; + uint32_t lastmod; // by OUR clock + int32_t gps_lat, gps_lon; // 6 dec places + uint32_t sync_since; +}; diff --git a/src/helpers/CustomSX1262.h b/src/helpers/CustomSX1262.h index c9fb5b69..8ce742c7 100644 --- a/src/helpers/CustomSX1262.h +++ b/src/helpers/CustomSX1262.h @@ -9,6 +9,48 @@ class CustomSX1262 : public SX1262 { public: CustomSX1262(Module *mod) : SX1262(mod) { } + bool std_init(SPIClass* spi = NULL) { + #ifdef SX126X_DIO3_TCXO_VOLTAGE + float tcxo = SX126X_DIO3_TCXO_VOLTAGE; + #else + float tcxo = 1.6f; + #endif + + #ifdef LORA_CR + uint8_t cr = LORA_CR; + #else + uint8_t cr = 5; + #endif + + #if defined(P_LORA_SCLK) + #ifdef NRF52_PLATFORM + if (spi) { spi->setPins(P_LORA_MISO, P_LORA_SCLK, P_LORA_MOSI); spi->begin(); } + #else + if (spi) spi->begin(P_LORA_SCLK, P_LORA_MISO, P_LORA_MOSI); + #endif + #endif + int status = begin(LORA_FREQ, LORA_BW, LORA_SF, cr, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, LORA_TX_POWER, 8, tcxo); + if (status != RADIOLIB_ERR_NONE) { + Serial.print("ERROR: radio init failed: "); + Serial.println(status); + return false; // fail + } + + setCRC(1); + + #ifdef SX126X_CURRENT_LIMIT + setCurrentLimit(SX126X_CURRENT_LIMIT); + #endif + #ifdef SX126X_DIO2_AS_RF_SWITCH + setDio2AsRfSwitch(SX126X_DIO2_AS_RF_SWITCH); + #endif + #ifdef SX126X_RX_BOOSTED_GAIN + setRxBoostedGainMode(SX126X_RX_BOOSTED_GAIN); + #endif + + return true; // success + } + bool isReceiving() { uint16_t irq = getIrqFlags(); bool detected = (irq & SX126X_IRQ_HEADER_VALID) || (irq & SX126X_IRQ_PREAMBLE_DETECTED); diff --git a/src/helpers/RadioLibWrappers.cpp b/src/helpers/RadioLibWrappers.cpp index d37bc498..96fb7aab 100644 --- a/src/helpers/RadioLibWrappers.cpp +++ b/src/helpers/RadioLibWrappers.cpp @@ -9,6 +9,7 @@ #define STATE_INT_READY 16 #define NUM_NOISE_FLOOR_SAMPLES 64 +#define SAMPLING_THRESHOLD 14 static volatile uint8_t state = STATE_IDLE; @@ -46,23 +47,35 @@ void RadioLibWrapper::idle() { void RadioLibWrapper::triggerNoiseFloorCalibrate(int threshold) { _threshold = threshold; - if (threshold > 0 && _num_floor_samples >= NUM_NOISE_FLOOR_SAMPLES) { // ignore trigger if currently sampling + if (_num_floor_samples >= NUM_NOISE_FLOOR_SAMPLES) { // ignore trigger if currently sampling _num_floor_samples = 0; _floor_sample_sum = 0; } } +void RadioLibWrapper::resetAGC() { + // make sure we're not mid-receive of packet! + if ((state & STATE_INT_READY) != 0 || isReceivingPacket()) return; + + // NOTE: according to higher powers, just issuing RadioLib's startReceive() will reset the AGC. + // revisit this if a better impl is discovered. + state = STATE_IDLE; // trigger a startReceive() +} + void RadioLibWrapper::loop() { if (state == STATE_RX && _num_floor_samples < NUM_NOISE_FLOOR_SAMPLES) { if (!isReceivingPacket()) { int rssi = getCurrentRSSI(); - if (rssi < _noise_floor + _threshold) { // only consider samples below current floor+THRESHOLD + if (rssi < _noise_floor + SAMPLING_THRESHOLD) { // only consider samples below current floor + sampling THRESHOLD _num_floor_samples++; _floor_sample_sum += rssi; } } } else if (_num_floor_samples >= NUM_NOISE_FLOOR_SAMPLES && _floor_sample_sum != 0) { _noise_floor = _floor_sample_sum / NUM_NOISE_FLOOR_SAMPLES; + if (_noise_floor < -120) { + _noise_floor = -120; // clamp to lower bound of -120dBi + } _floor_sample_sum = 0; MESH_DEBUG_PRINTLN("RadioLibWrapper: noise_floor = %d", (int)_noise_floor); diff --git a/src/helpers/RadioLibWrappers.h b/src/helpers/RadioLibWrappers.h index bb308071..25cc5358 100644 --- a/src/helpers/RadioLibWrappers.h +++ b/src/helpers/RadioLibWrappers.h @@ -39,6 +39,7 @@ public: int getNoiseFloor() const override { return _noise_floor; } void triggerNoiseFloorCalibrate(int threshold) override; + void resetAGC() override; void loop() override; diff --git a/src/helpers/nrf52/ThinkNodeM1Board.cpp b/src/helpers/nrf52/ThinkNodeM1Board.cpp index ef1cf111..63768eea 100644 --- a/src/helpers/nrf52/ThinkNodeM1Board.cpp +++ b/src/helpers/nrf52/ThinkNodeM1Board.cpp @@ -26,6 +26,11 @@ void ThinkNodeM1Board::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 diff --git a/src/helpers/nrf52/ThinkNodeM1Board.h b/src/helpers/nrf52/ThinkNodeM1Board.h index cc87c96d..97334bd3 100644 --- a/src/helpers/nrf52/ThinkNodeM1Board.h +++ b/src/helpers/nrf52/ThinkNodeM1Board.h @@ -39,6 +39,15 @@ public: return startup_reason; } + #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-M1"; } diff --git a/src/helpers/rp2040/WaveshareBoard.cpp b/src/helpers/rp2040/WaveshareBoard.cpp new file mode 100644 index 00000000..2e622933 --- /dev/null +++ b/src/helpers/rp2040/WaveshareBoard.cpp @@ -0,0 +1,30 @@ +#include "WaveshareBoard.h" + +#include +#include + +void WaveshareBoard::begin() { + // for future use, sub-classes SHOULD call this from their begin() + startup_reason = BD_STARTUP_NORMAL; + +#ifdef P_LORA_TX_LED + pinMode(P_LORA_TX_LED, OUTPUT); +#endif + +#ifdef PIN_VBAT_READ + pinMode(PIN_VBAT_READ, INPUT); +#endif + +#if defined(PIN_BOARD_SDA) && defined(PIN_BOARD_SCL) + Wire.setSDA(PIN_BOARD_SDA); + Wire.setSCL(PIN_BOARD_SCL); +#endif + + Wire.begin(); + + delay(10); // give sx1262 some time to power up +} + +bool WaveshareBoard::startOTAUpdate(const char *id, char reply[]) { + return false; +} diff --git a/src/helpers/rp2040/WaveshareBoard.h b/src/helpers/rp2040/WaveshareBoard.h new file mode 100644 index 00000000..c2b5aff5 --- /dev/null +++ b/src/helpers/rp2040/WaveshareBoard.h @@ -0,0 +1,73 @@ +#pragma once + +#include +#include + +// LoRa radio module pins for Waveshare RP2040-LoRa-HF/LF +// https://files.waveshare.com/wiki/RP2040-LoRa/Rp2040-lora-sch.pdf + +#define P_LORA_DIO_1 16 +#define P_LORA_NSS 13 // CS +#define P_LORA_RESET 23 +#define P_LORA_BUSY 18 +#define P_LORA_SCLK 14 +#define P_LORA_MISO 24 +#define P_LORA_MOSI 15 +#define P_LORA_TX_LED 25 + +#define SX126X_DIO2_AS_RF_SWITCH true +#define SX126X_DIO3_TCXO_VOLTAGE 0 + +/* + * This board has no built-in way to read battery voltage. + * Nevertheless it's very easy to make it work, you only require two 1% resistors. + * + * BAT+ -----+ + * | + * VSYS --+ -/\/\/\/\- --+ + * 200k | + * +-- GPIO28 + * | + * GND --+ -/\/\/\/\- --+ + * | 100k + * BAT- -----+ + */ +#define PIN_VBAT_READ 28 +#define BATTERY_SAMPLES 8 +#define ADC_MULTIPLIER (3.0f * 3.3f * 1000) + +class WaveshareBoard : public mesh::MainBoard { +protected: + uint8_t startup_reason; + +public: + void begin(); + uint8_t getStartupReason() const override { return startup_reason; } + +#ifdef P_LORA_TX_LED + void onBeforeTransmit() override { digitalWrite(P_LORA_TX_LED, HIGH); } + void onAfterTransmit() override { digitalWrite(P_LORA_TX_LED, LOW); } +#endif + + uint16_t getBattMilliVolts() override { +#if defined(PIN_VBAT_READ) && defined(ADC_MULTIPLIER) + analogReadResolution(12); + + uint32_t raw = 0; + for (int i = 0; i < BATTERY_SAMPLES; i++) { + raw += analogRead(PIN_VBAT_READ); + } + raw = raw / BATTERY_SAMPLES; + + return (ADC_MULTIPLIER * raw) / 4096; +#else + return 0; +#endif + } + + const char *getManufacturerName() const override { return "Waveshare RP2040-LoRa"; } + + void reboot() override { rp2040.reboot(); } + + bool startOTAUpdate(const char *id, char reply[]) override; +}; diff --git a/src/helpers/ui/buzzer.cpp b/src/helpers/ui/buzzer.cpp index ccc18cd3..c8e5cfcc 100644 --- a/src/helpers/ui/buzzer.cpp +++ b/src/helpers/ui/buzzer.cpp @@ -11,6 +11,7 @@ void genericBuzzer::begin() { quiet(false); pinMode(PIN_BUZZER, OUTPUT); + digitalWrite(PIN_BUZZER, LOW); // need to pull low by default to avoid extreme power draw startup(); } diff --git a/variants/generic_espnow/platformio.ini b/variants/generic_espnow/platformio.ini index 8a033a62..dbc902f0 100644 --- a/variants/generic_espnow/platformio.ini +++ b/variants/generic_espnow/platformio.ini @@ -56,13 +56,11 @@ build_flags = ${Generic_ESPNOW.build_flags} -D MAX_CONTACTS=100 -D MAX_GROUP_CHANNELS=8 -; -D ENABLE_PRIVATE_KEY_IMPORT=1 -; -D ENABLE_PRIVATE_KEY_EXPORT=1 ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 ; NOTE: DO NOT ENABLE --> -D ESPNOW_DEBUG_LOGGING=1 build_src_filter = ${Generic_ESPNOW.build_src_filter} - +<../examples/companion_radio/main.cpp> + +<../examples/companion_radio> lib_deps = ${Generic_ESPNOW.lib_deps} densaugeo/base64 @ ~1.4.0 diff --git a/variants/heltec_ct62/HT-CT62Board.h b/variants/heltec_ct62/HT-CT62Board.h new file mode 100644 index 00000000..e5a627b8 --- /dev/null +++ b/variants/heltec_ct62/HT-CT62Board.h @@ -0,0 +1,33 @@ +#pragma once + +#include +#include + +#if defined(ESP_PLATFORM) + +#include + +class Heltec_CT62_Board : public ESP32Board { +public: + +uint16_t getBattMilliVolts() override { + #ifdef PIN_VBAT_READ + analogReadResolution(12); // ESP32-C3 ADC is 12-bit - 3.3/4096 (ref voltage/max counts) + uint32_t raw = 0; + for (int i = 0; i < 8; i++) { + raw += analogRead(PIN_VBAT_READ); + } + raw = raw / 8; + + return ((6.52 * raw) / 1024.0) * 1000; + #else + return 0; // not supported + #endif + } + + const char* getManufacturerName() const override { + return "Heltec CT62"; + } +}; + +#endif diff --git a/variants/heltec_ct62/platformio.ini b/variants/heltec_ct62/platformio.ini new file mode 100644 index 00000000..ba23a5a6 --- /dev/null +++ b/variants/heltec_ct62/platformio.ini @@ -0,0 +1,88 @@ +[Heltec_ct62] +extends = esp32_base +board = esp32-c3-devkitm-1 +build_flags = + ${esp32_base.build_flags} + -I variants/heltec_ct62 + -D HELTEC_HT_CT62=1 + -D RADIO_CLASS=CustomSX1262 + -D WRAPPER_CLASS=CustomSX1262Wrapper + -D ESP32_CPU_FREQ=80 + -D LORA_TX_POWER=22 + -D P_LORA_TX_LED=18 + -D PIN_BOARD_SDA=0 + -D PIN_BOARD_SCL=1 + ;-D PIN_USER_BTN=9 + -D PIN_VBAT_READ=2 + -D P_LORA_DIO_1=3 + -D P_LORA_NSS=8 + -D P_LORA_RESET=5 + -D P_LORA_DIO_0=RADIOLIB_NC + -D P_LORA_DIO_2=RADIOLIB_NC + -D P_LORA_BUSY=4 + -D P_LORA_SCLK=10 + -D P_LORA_MISO=6 + -D P_LORA_MOSI=7 + -D SX126X_DIO2_AS_RF_SWITCH=true + -D SX126X_DIO3_TCXO_VOLTAGE=1.8 + -D SX126X_CURRENT_LIMIT=140 + -D SX126X_RX_BOOSTED_GAIN=1 +build_src_filter = ${esp32_base.build_src_filter} + +<../variants/heltec_ct62> + +[env:Heltec_ct62_repeater] +extends = Heltec_ct62 +build_flags = + ${Heltec_ct62.build_flags} + ;-D ARDUINO_USB_MODE=1 + ;-D ARDUINO_USB_CDC_ON_BOOT=1 + -D ADVERT_NAME='"HT-CT62 Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=8 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${Heltec_ct62.build_src_filter} + +<../examples/simple_repeater> +lib_deps = + ${Heltec_ct62.lib_deps} + ${esp32_ota.lib_deps} + +[env:Heltec_ct62_companion_radio_usb] +extends = Heltec_ct62 +build_flags = + ${Heltec_ct62.build_flags} +; -D ARDUINO_USB_MODE=1 +; -D ARDUINO_USB_CDC_ON_BOOT=1 + -D MAX_CONTACTS=100 + -D MAX_GROUP_CHANNELS=8 + -D OFFLINE_QUEUE_SIZE=256 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${Heltec_ct62.build_src_filter} + +<../examples/companion_radio> +lib_deps = + ${Heltec_ct62.lib_deps} + ${esp32_ota.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:Heltec_ct62_companion_radio_ble] +extends = Heltec_ct62 +build_flags = + ${Heltec_ct62.build_flags} +; -D ARDUINO_USB_MODE=1 +; -D ARDUINO_USB_CDC_ON_BOOT=1 + -D MAX_CONTACTS=100 + -D MAX_GROUP_CHANNELS=8 + -D OFFLINE_QUEUE_SIZE=256 + -D BLE_PIN_CODE=123456 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${Heltec_ct62.build_src_filter} + +<../examples/companion_radio> + + +lib_deps = + ${Heltec_ct62.lib_deps} + ${esp32_ota.lib_deps} + densaugeo/base64 @ ~1.4.0 diff --git a/variants/heltec_ct62/target.cpp b/variants/heltec_ct62/target.cpp new file mode 100644 index 00000000..eae43bd1 --- /dev/null +++ b/variants/heltec_ct62/target.cpp @@ -0,0 +1,70 @@ +#include +#include "target.h" + +Heltec_CT62_Board board; + +RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); +WRAPPER_CLASS radio_driver(radio, board); + +ESP32RTCClock fallback_clock; +AutoDiscoverRTCClock rtc_clock(fallback_clock); +SensorManager sensors; + +#ifndef LORA_CR + #define LORA_CR 5 +#endif + +bool radio_init() { + fallback_clock.begin(); + rtc_clock.begin(Wire); + +#ifdef SX126X_DIO3_TCXO_VOLTAGE + float tcxo = SX126X_DIO3_TCXO_VOLTAGE; +#else + float tcxo = 1.6f; +#endif + +#if defined(P_LORA_SCLK) + SPI.begin(P_LORA_SCLK, P_LORA_MISO, P_LORA_MOSI); +#endif + int status = radio.begin(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, LORA_TX_POWER, 8, tcxo); + if (status != RADIOLIB_ERR_NONE) { + Serial.print("ERROR: radio init failed: "); + Serial.println(status); + return false; // fail + } + + radio.setCRC(1); + +#ifdef SX126X_CURRENT_LIMIT + radio.setCurrentLimit(SX126X_CURRENT_LIMIT); +#endif +#ifdef SX126X_DIO2_AS_RF_SWITCH + radio.setDio2AsRfSwitch(SX126X_DIO2_AS_RF_SWITCH); +#endif +#ifdef SX126X_RX_BOOSTED_GAIN + radio.setRxBoostedGainMode(SX126X_RX_BOOSTED_GAIN); +#endif + + return true; // success +} + +uint32_t radio_get_rng_seed() { + return radio.random(0x7FFFFFFF); +} + +void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) { + radio.setFrequency(freq); + radio.setSpreadingFactor(sf); + radio.setBandwidth(bw); + radio.setCodingRate(cr); +} + +void radio_set_tx_power(uint8_t dbm) { + radio.setOutputPower(dbm); +} + +mesh::LocalIdentity radio_new_identity() { + RadioNoiseListener rng(radio); + return mesh::LocalIdentity(&rng); // create new random identity +} \ No newline at end of file diff --git a/variants/heltec_ct62/target.h b/variants/heltec_ct62/target.h new file mode 100644 index 00000000..ff114e6e --- /dev/null +++ b/variants/heltec_ct62/target.h @@ -0,0 +1,20 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include "HT-CT62Board.h" +#include +#include +#include + +extern Heltec_CT62_Board board; +extern WRAPPER_CLASS radio_driver; +extern AutoDiscoverRTCClock rtc_clock; +extern SensorManager sensors; + +bool radio_init(); +uint32_t radio_get_rng_seed(); +void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr); +void radio_set_tx_power(uint8_t dbm); +mesh::LocalIdentity radio_new_identity(); \ No newline at end of file diff --git a/variants/heltec_tracker/platformio.ini b/variants/heltec_tracker/platformio.ini index d62771f4..8d9013a5 100644 --- a/variants/heltec_tracker/platformio.ini +++ b/variants/heltec_tracker/platformio.ini @@ -39,7 +39,7 @@ extends = Heltec_tracker_base build_flags = ${Heltec_tracker_base.build_flags} -I src/helpers/ui - ; -D ARDUINO_USB_CDC_ON_BOOT=1 ; need for debugging + -D ARDUINO_USB_CDC_ON_BOOT=1 ; need for Serial -D DISPLAY_ROTATION=1 -D DISPLAY_CLASS=ST7735Display -D MAX_CONTACTS=100 @@ -47,8 +47,6 @@ build_flags = -D BLE_PIN_CODE=123456 ; HWT will use display for pin -D OFFLINE_QUEUE_SIZE=256 ; -D BLE_DEBUG_LOGGING=1 -; -D ENABLE_PRIVATE_KEY_IMPORT=1 -; -D ENABLE_PRIVATE_KEY_EXPORT=1 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${Heltec_tracker_base.build_src_filter} diff --git a/variants/heltec_v2/platformio.ini b/variants/heltec_v2/platformio.ini index 495f20f8..562b309d 100644 --- a/variants/heltec_v2/platformio.ini +++ b/variants/heltec_v2/platformio.ini @@ -99,8 +99,6 @@ build_flags = -D BLE_PIN_CODE=0 -D BLE_DEBUG_LOGGING=1 -D OFFLINE_QUEUE_SIZE=256 -; -D ENABLE_PRIVATE_KEY_IMPORT=1 -; -D ENABLE_PRIVATE_KEY_EXPORT=1 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${Heltec_lora32_v2.build_src_filter} diff --git a/variants/heltec_v3/platformio.ini b/variants/heltec_v3/platformio.ini index 28c6d562..e8818fdd 100644 --- a/variants/heltec_v3/platformio.ini +++ b/variants/heltec_v3/platformio.ini @@ -91,8 +91,6 @@ build_flags = -D MAX_CONTACTS=100 -D MAX_GROUP_CHANNELS=8 -D DISPLAY_CLASS=SSD1306Display -; -D ENABLE_PRIVATE_KEY_IMPORT=1 -; -D ENABLE_PRIVATE_KEY_EXPORT=1 ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 build_src_filter = ${Heltec_lora32_v3.build_src_filter} @@ -112,8 +110,6 @@ build_flags = -D BLE_PIN_CODE=0 ; dynamic, random PIN -D BLE_DEBUG_LOGGING=1 -D OFFLINE_QUEUE_SIZE=256 -; -D ENABLE_PRIVATE_KEY_IMPORT=1 -; -D ENABLE_PRIVATE_KEY_EXPORT=1 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${Heltec_lora32_v3.build_src_filter} @@ -134,8 +130,6 @@ build_flags = -D WIFI_DEBUG_LOGGING=1 -D WIFI_SSID='"myssid"' -D WIFI_PWD='"mypwd"' -; -D ENABLE_PRIVATE_KEY_IMPORT=1 -; -D ENABLE_PRIVATE_KEY_EXPORT=1 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${Heltec_lora32_v3.build_src_filter} @@ -189,8 +183,6 @@ build_flags = -D BLE_PIN_CODE=123456 -D BLE_DEBUG_LOGGING=1 -D OFFLINE_QUEUE_SIZE=256 -; -D ENABLE_PRIVATE_KEY_IMPORT=1 -; -D ENABLE_PRIVATE_KEY_EXPORT=1 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${Heltec_lora32_v3.build_src_filter} diff --git a/variants/heltec_v3/target.cpp b/variants/heltec_v3/target.cpp index be99a1e3..de8e0f92 100644 --- a/variants/heltec_v3/target.cpp +++ b/variants/heltec_v3/target.cpp @@ -20,43 +20,16 @@ EnvironmentSensorManager sensors; DISPLAY_CLASS display; #endif -#ifndef LORA_CR - #define LORA_CR 5 -#endif - bool radio_init() { fallback_clock.begin(); rtc_clock.begin(Wire); -#ifdef SX126X_DIO3_TCXO_VOLTAGE - float tcxo = SX126X_DIO3_TCXO_VOLTAGE; -#else - float tcxo = 1.6f; -#endif - #if defined(P_LORA_SCLK) spi.begin(P_LORA_SCLK, P_LORA_MISO, P_LORA_MOSI); + return radio.std_init(&spi); +#else + return radio.std_init(); #endif - int status = radio.begin(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, LORA_TX_POWER, 8, tcxo); - if (status != RADIOLIB_ERR_NONE) { - Serial.print("ERROR: radio init failed: "); - Serial.println(status); - return false; // fail - } - - radio.setCRC(1); - -#ifdef SX126X_CURRENT_LIMIT - radio.setCurrentLimit(SX126X_CURRENT_LIMIT); -#endif -#ifdef SX126X_DIO2_AS_RF_SWITCH - radio.setDio2AsRfSwitch(SX126X_DIO2_AS_RF_SWITCH); -#endif -#ifdef SX126X_RX_BOOSTED_GAIN - radio.setRxBoostedGainMode(SX126X_RX_BOOSTED_GAIN); -#endif - - return true; // success } uint32_t radio_get_rng_seed() { diff --git a/variants/lilygo_t3s3/platformio.ini b/variants/lilygo_t3s3/platformio.ini index 94ec87af..f3a95e96 100644 --- a/variants/lilygo_t3s3/platformio.ini +++ b/variants/lilygo_t3s3/platformio.ini @@ -92,8 +92,6 @@ build_flags = -D DISPLAY_CLASS=SSD1306Display -D MAX_CONTACTS=100 -D MAX_GROUP_CHANNELS=8 -; -D ENABLE_PRIVATE_KEY_IMPORT=1 -; -D ENABLE_PRIVATE_KEY_EXPORT=1 ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 build_src_filter = ${LilyGo_T3S3_sx1262.build_src_filter} @@ -113,8 +111,6 @@ build_flags = -D BLE_PIN_CODE=123456 -D BLE_DEBUG_LOGGING=1 -D OFFLINE_QUEUE_SIZE=256 -; -D ENABLE_PRIVATE_KEY_IMPORT=1 -; -D ENABLE_PRIVATE_KEY_EXPORT=1 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${LilyGo_T3S3_sx1262.build_src_filter} diff --git a/variants/lilygo_t3s3_sx1276/platformio.ini b/variants/lilygo_t3s3_sx1276/platformio.ini new file mode 100644 index 00000000..1bcbbbf9 --- /dev/null +++ b/variants/lilygo_t3s3_sx1276/platformio.ini @@ -0,0 +1,119 @@ +[LilyGo_T3S3_sx1276] +extends = esp32_base +board = t3_s3_v1_x +build_flags = + ${esp32_base.build_flags} + -I variants/lilygo_t3s3_sx1276 + -D LILYGO_T3S3 + -D P_LORA_DIO_0=9 + -D P_LORA_DIO_1=33 + -D P_LORA_NSS=7 + -D P_LORA_RESET=8 + -D P_LORA_SCLK=5 + -D P_LORA_MISO=3 + -D P_LORA_MOSI=6 + -D P_LORA_TX_LED=37 + -D PIN_VBAT_READ=1 + -D PIN_USER_BTN=0 + -D PIN_BOARD_SDA=18 + -D PIN_BOARD_SCL=17 + -D PIN_OLED_RESET=21 + -D RADIO_CLASS=CustomSX1276 + -D WRAPPER_CLASS=CustomSX1276Wrapper + -D SX127X_CURRENT_LIMIT=120 + -D LORA_TX_POWER=20 +build_src_filter = ${esp32_base.build_src_filter} + +<../variants/lilygo_t3s3_sx1276> +lib_deps = + ${esp32_base.lib_deps} + adafruit/Adafruit SSD1306 @ ^2.5.13 + +; === LilyGo T3S3 with SX1276 environments === +[env:LilyGo_T3S3_sx1276_Repeater] +extends = LilyGo_T3S3_sx1276 +build_flags = + ${LilyGo_T3S3_sx1276.build_flags} + -D DISPLAY_CLASS=SSD1306Display + -D ADVERT_NAME='"T3S3-1276 Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=8 + ; -D MESH_PACKET_LOGGING=1 + ; -D MESH_DEBUG=1 +build_src_filter = ${LilyGo_T3S3_sx1276.build_src_filter} + + + +<../examples/simple_repeater> +lib_deps = + ${LilyGo_T3S3_sx1276.lib_deps} + ${esp32_ota.lib_deps} + +[env:LilyGo_T3S3_sx1276_terminal_chat] +extends = LilyGo_T3S3_sx1276 +build_flags = + ${LilyGo_T3S3_sx1276.build_flags} + -D MAX_CONTACTS=100 + -D MAX_GROUP_CHANNELS=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${LilyGo_T3S3_sx1276.build_src_filter} + +<../examples/simple_secure_chat/main.cpp> +lib_deps = + ${LilyGo_T3S3_sx1276.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:LilyGo_T3S3_sx1276_room_server] +extends = LilyGo_T3S3_sx1276 +build_flags = + ${LilyGo_T3S3_sx1276.build_flags} + -D DISPLAY_CLASS=SSD1306Display + -D ADVERT_NAME='"T3S3-1276 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 = ${LilyGo_T3S3_sx1276.build_src_filter} + + + +<../examples/simple_room_server> +lib_deps = + ${LilyGo_T3S3_sx1276.lib_deps} + ${esp32_ota.lib_deps} + +[env:LilyGo_T3S3_sx1276_companion_radio_usb] +extends = LilyGo_T3S3_sx1276 +upload_speed = 115200 +build_flags = + ${LilyGo_T3S3_sx1276.build_flags} + -D DISPLAY_CLASS=SSD1306Display + -D MAX_CONTACTS=100 + -D MAX_GROUP_CHANNELS=8 + -D MESH_PACKET_LOGGING=1 + -D MESH_DEBUG=1 +build_src_filter = ${LilyGo_T3S3_sx1276.build_src_filter} + + + +<../examples/companion_radio> +lib_deps = + ${LilyGo_T3S3_sx1276.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:LilyGo_T3S3_sx1276_companion_radio_ble] +extends = LilyGo_T3S3_sx1276 +build_flags = + ${LilyGo_T3S3_sx1276.build_flags} + -D DISPLAY_CLASS=SSD1306Display + -D MAX_CONTACTS=100 + -D MAX_GROUP_CHANNELS=8 + -D BLE_PIN_CODE=123456 + -D BLE_DEBUG_LOGGING=1 + -D OFFLINE_QUEUE_SIZE=256 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${LilyGo_T3S3_sx1276.build_src_filter} + + + + + +<../examples/companion_radio> +lib_deps = + ${LilyGo_T3S3_sx1276.lib_deps} + densaugeo/base64 @ ~1.4.0 \ No newline at end of file diff --git a/variants/lilygo_t3s3_sx1276/target.cpp b/variants/lilygo_t3s3_sx1276/target.cpp new file mode 100644 index 00000000..db11433a --- /dev/null +++ b/variants/lilygo_t3s3_sx1276/target.cpp @@ -0,0 +1,70 @@ +#include +#include "target.h" + +ESP32Board board; + +#if defined(P_LORA_SCLK) + static SPIClass spi; + RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_0, P_LORA_RESET, P_LORA_DIO_1, spi); +#else + RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_0, P_LORA_RESET, P_LORA_DIO_1); +#endif + +WRAPPER_CLASS radio_driver(radio, board); + +ESP32RTCClock fallback_clock; +AutoDiscoverRTCClock rtc_clock(fallback_clock); +SensorManager sensors; + +#ifdef DISPLAY_CLASS + DISPLAY_CLASS display; +#endif + +#ifndef LORA_CR + #define LORA_CR 5 +#endif + +bool radio_init() { + fallback_clock.begin(); + rtc_clock.begin(Wire); + +#if defined(P_LORA_SCLK) + spi.begin(P_LORA_SCLK, P_LORA_MISO, P_LORA_MOSI); +#endif + int status = radio.begin(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, LORA_TX_POWER, 8); + if (status != RADIOLIB_ERR_NONE) { + Serial.print("ERROR: radio init failed: "); + Serial.println(status); + return false; // fail + } + +#ifdef SX127X_CURRENT_LIMIT + radio.setCurrentLimit(SX127X_CURRENT_LIMIT); +#endif + + radio.setCRC(1); + + radio.setRfSwitchPins(21, 10); + + return true; // success +} + +uint32_t radio_get_rng_seed() { + return radio.random(0x7FFFFFFF); +} + +void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) { + radio.setFrequency(freq); + radio.setSpreadingFactor(sf); + radio.setBandwidth(bw); + radio.setCodingRate(cr); +} + +void radio_set_tx_power(uint8_t dbm) { + radio.setOutputPower(dbm); +} + +mesh::LocalIdentity radio_new_identity() { + RadioNoiseListener rng(radio); + return mesh::LocalIdentity(&rng); // create new random identity +} \ No newline at end of file diff --git a/variants/lilygo_t3s3_sx1276/target.h b/variants/lilygo_t3s3_sx1276/target.h new file mode 100644 index 00000000..c5fc0be0 --- /dev/null +++ b/variants/lilygo_t3s3_sx1276/target.h @@ -0,0 +1,27 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include +#include +#include +#include +#ifdef DISPLAY_CLASS + #include +#endif + +extern ESP32Board board; +extern WRAPPER_CLASS radio_driver; +extern AutoDiscoverRTCClock rtc_clock; +extern SensorManager sensors; + +#ifdef DISPLAY_CLASS + extern DISPLAY_CLASS display; +#endif + +bool radio_init(); +uint32_t radio_get_rng_seed(); +void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr); +void radio_set_tx_power(uint8_t dbm); +mesh::LocalIdentity radio_new_identity(); \ No newline at end of file diff --git a/variants/lilygo_tbeam/platformio.ini b/variants/lilygo_tbeam/platformio.ini index c471e44c..4369243d 100644 --- a/variants/lilygo_tbeam/platformio.ini +++ b/variants/lilygo_tbeam/platformio.ini @@ -33,8 +33,6 @@ build_flags = -D BLE_DEBUG_LOGGING=1 -D OFFLINE_QUEUE_SIZE=256 ; -D RADIOLIB_DEBUG_BASIC=1 -; -D ENABLE_PRIVATE_KEY_IMPORT=1 -; -D ENABLE_PRIVATE_KEY_EXPORT=1 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${LilyGo_TBeam.build_src_filter} diff --git a/variants/lilygo_tbeam_SX1262/platformio.ini b/variants/lilygo_tbeam_SX1262/platformio.ini index 517fc2e0..153fc6fa 100644 --- a/variants/lilygo_tbeam_SX1262/platformio.ini +++ b/variants/lilygo_tbeam_SX1262/platformio.ini @@ -38,8 +38,6 @@ build_flags = ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 ; -D RADIOLIB_DEBUG_BASIC=1 -; -D ENABLE_PRIVATE_KEY_IMPORT=1 -; -D ENABLE_PRIVATE_KEY_EXPORT=1 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${LilyGo_TBeam_SX1262.build_src_filter} diff --git a/variants/lilygo_tbeam_supreme_SX1262/platformio.ini b/variants/lilygo_tbeam_supreme_SX1262/platformio.ini index 4e6721f9..bcc51703 100644 --- a/variants/lilygo_tbeam_supreme_SX1262/platformio.ini +++ b/variants/lilygo_tbeam_supreme_SX1262/platformio.ini @@ -69,8 +69,6 @@ build_flags = -D BLE_PIN_CODE=123456 -D OFFLINE_QUEUE_SIZE=256 ; -D BLE_DEBUG_LOGGING=1 -; -D ENABLE_PRIVATE_KEY_IMPORT=1 -; -D ENABLE_PRIVATE_KEY_EXPORT=1 ; -D MESH_PACKET_LOGGING=8 ; -D MESH_DEBUG=1 build_src_filter = ${T_Beam_S3_Supreme_SX1262.build_src_filter} diff --git a/variants/lilygo_tlora_c6/platformio.ini b/variants/lilygo_tlora_c6/platformio.ini index 48e310c0..7f89c3da 100644 --- a/variants/lilygo_tlora_c6/platformio.ini +++ b/variants/lilygo_tlora_c6/platformio.ini @@ -30,7 +30,7 @@ build_flags = build_src_filter = ${esp32c6_base.build_src_filter} +<../variants/lilygo_tlora_c6> -[env:LilyGo_Tlora_c6_Repeater] +[env:LilyGo_Tlora_C6_repeater] extends = tlora_c6 build_src_filter = ${tlora_c6.build_src_filter} +<../examples/simple_repeater/main.cpp> @@ -47,7 +47,24 @@ lib_deps = ${tlora_c6.lib_deps} ; ${esp32_ota.lib_deps} -[env:LilyGo_Tlora_c6_companion_radio_ble] +[env:LilyGo_Tlora_C6_room_server] +extends = tlora_c6 +build_src_filter = ${tlora_c6.build_src_filter} + +<../examples/simple_room_server> +build_flags = + ${tlora_c6.build_flags} + -D ADVERT_NAME='"Tlora C6 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 +lib_deps = + ${tlora_c6.lib_deps} +; ${esp32_ota.lib_deps} + +[env:LilyGo_Tlora_C6_companion_radio_ble] extends = tlora_c6 build_flags = ${tlora_c6.build_flags} -D MAX_CONTACTS=100 @@ -55,8 +72,8 @@ build_flags = ${tlora_c6.build_flags} -D BLE_PIN_CODE=123456 -D BLE_DEBUG_LOGGING=1 -D OFFLINE_QUEUE_SIZE=256 -; -D ENABLE_PRIVATE_KEY_IMPORT=1 -; -D ENABLE_PRIVATE_KEY_EXPORT=1 + -D ENABLE_PRIVATE_KEY_IMPORT=1 + -D ENABLE_PRIVATE_KEY_EXPORT=1 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${tlora_c6.build_src_filter} diff --git a/variants/lilygo_tlora_v2_1/platformio.ini b/variants/lilygo_tlora_v2_1/platformio.ini index 5591a400..d9cecfc2 100644 --- a/variants/lilygo_tlora_v2_1/platformio.ini +++ b/variants/lilygo_tlora_v2_1/platformio.ini @@ -72,8 +72,6 @@ build_flags = ${LilyGo_TLora_V2_1_1_6.build_flags} -D MAX_CONTACTS=100 -D MAX_GROUP_CHANNELS=8 -; -D ENABLE_PRIVATE_KEY_IMPORT=1 -; -D ENABLE_PRIVATE_KEY_EXPORT=1 ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 build_src_filter = ${LilyGo_TLora_V2_1_1_6.build_src_filter} @@ -92,8 +90,6 @@ build_flags = -D BLE_PIN_CODE=123456 -D OFFLINE_QUEUE_SIZE=256 ; -D BLE_DEBUG_LOGGING=1 -; -D ENABLE_PRIVATE_KEY_IMPORT=1 -; -D ENABLE_PRIVATE_KEY_EXPORT=1 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${LilyGo_TLora_V2_1_1_6.build_src_filter} diff --git a/variants/nano_g2_ultra/platformio.ini b/variants/nano_g2_ultra/platformio.ini index 98feb35c..20928bdf 100644 --- a/variants/nano_g2_ultra/platformio.ini +++ b/variants/nano_g2_ultra/platformio.ini @@ -41,8 +41,6 @@ build_flags = -D OFFLINE_QUEUE_SIZE=256 -D DISPLAY_CLASS=SH1106Display -D PIN_BUZZER=4 -; -D ENABLE_PRIVATE_KEY_IMPORT=1 -; -D ENABLE_PRIVATE_KEY_EXPORT=1 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${Nano_G2_Ultra.build_src_filter} diff --git a/variants/picow/platformio.ini b/variants/picow/platformio.ini index ec27e6ee..8b6c2506 100644 --- a/variants/picow/platformio.ini +++ b/variants/picow/platformio.ini @@ -14,7 +14,7 @@ build_flags = ${rp2040_base.build_flags} -D LORA_TX_POWER=22 -D SX126X_RX_BOOSTED_GAIN=1 build_src_filter = ${rp2040_base.build_src_filter} - + + + +<../variants/picow> lib_deps = ${rp2040_base.lib_deps} @@ -49,8 +49,6 @@ extends = picow build_flags = ${picow.build_flags} -D MAX_CONTACTS=100 -D MAX_GROUP_CHANNELS=8 -; -D ENABLE_PRIVATE_KEY_IMPORT=1 -; -D ENABLE_PRIVATE_KEY_EXPORT=1 ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 build_src_filter = ${picow.build_src_filter} @@ -65,8 +63,6 @@ lib_deps = ${picow.lib_deps} ; -D MAX_GROUP_CHANNELS=8 ; -D BLE_PIN_CODE=123456 ; -D BLE_DEBUG_LOGGING=1 -; ; -D ENABLE_PRIVATE_KEY_IMPORT=1 -; ; -D ENABLE_PRIVATE_KEY_EXPORT=1 ; ; -D MESH_PACKET_LOGGING=1 ; ; -D MESH_DEBUG=1 ; build_src_filter = ${picow.build_src_filter} @@ -82,8 +78,6 @@ lib_deps = ${picow.lib_deps} ; -D WIFI_DEBUG_LOGGING=1 ; -D WIFI_SSID='"myssid"' ; -D WIFI_PWD='"mypwd"' -; ; -D ENABLE_PRIVATE_KEY_IMPORT=1 -; ; -D ENABLE_PRIVATE_KEY_EXPORT=1 ; ; -D MESH_PACKET_LOGGING=1 ; ; -D MESH_DEBUG=1 ; build_src_filter = ${picow.build_src_filter} diff --git a/variants/promicro/platformio.ini b/variants/promicro/platformio.ini index 11f73d81..e7099d1e 100644 --- a/variants/promicro/platformio.ini +++ b/variants/promicro/platformio.ini @@ -103,8 +103,6 @@ build_flags = ${Faketec.build_flags} -D MAX_GROUP_CHANNELS=8 -D BLE_PIN_CODE=123456 -D BLE_DEBUG_LOGGING=1 - -D ENABLE_PRIVATE_KEY_EXPORT=1 - -D ENABLE_PRIVATE_KEY_IMPORT=1 -D OFFLINE_QUEUE_SIZE=256 -D DISPLAY_CLASS=SSD1306Display ; -D MESH_PACKET_LOGGING=1 @@ -186,7 +184,7 @@ build_flags = ${ProMicroLLCC68.build_flags} ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 build_src_filter = ${ProMicroLLCC68.build_src_filter} - +<../examples/companion_radio/main.cpp> + +<../examples/companion_radio> lib_deps = ${ProMicroLLCC68.lib_deps} adafruit/RTClib @ ^2.1.3 densaugeo/base64 @ ~1.4.0 @@ -198,14 +196,12 @@ build_flags = ${ProMicroLLCC68.build_flags} -D MAX_GROUP_CHANNELS=8 -D BLE_PIN_CODE=123456 -D BLE_DEBUG_LOGGING=1 - -D ENABLE_PRIVATE_KEY_EXPORT=1 - -D ENABLE_PRIVATE_KEY_IMPORT=1 -D OFFLINE_QUEUE_SIZE=256 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${ProMicroLLCC68.build_src_filter} + - +<../examples/companion_radio/main.cpp> + +<../examples/companion_radio> lib_deps = ${ProMicroLLCC68.lib_deps} adafruit/RTClib @ ^2.1.3 densaugeo/base64 @ ~1.4.0 diff --git a/variants/rak4631/platformio.ini b/variants/rak4631/platformio.ini index c7f1fa53..3014bc59 100644 --- a/variants/rak4631/platformio.ini +++ b/variants/rak4631/platformio.ini @@ -64,8 +64,6 @@ build_flags = -D DISPLAY_CLASS=SSD1306Display -D MAX_CONTACTS=100 -D MAX_GROUP_CHANNELS=8 -; -D ENABLE_PRIVATE_KEY_IMPORT=1 -; -D ENABLE_PRIVATE_KEY_EXPORT=1 ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 build_src_filter = ${rak4631.build_src_filter} @@ -85,8 +83,6 @@ build_flags = -D BLE_PIN_CODE=123456 -D BLE_DEBUG_LOGGING=1 -D OFFLINE_QUEUE_SIZE=256 -; -D ENABLE_PRIVATE_KEY_IMPORT=1 -; -D ENABLE_PRIVATE_KEY_EXPORT=1 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${rak4631.build_src_filter} @@ -108,8 +104,6 @@ build_flags = -D BLE_DEBUG_LOGGING=1 -D OFFLINE_QUEUE_SIZE=256 -D ENV_INCLUDE_GPS=1 -; -D ENABLE_PRIVATE_KEY_IMPORT=1 -; -D ENABLE_PRIVATE_KEY_EXPORT=1 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${rak4631.build_src_filter} diff --git a/variants/station_g2/platformio.ini b/variants/station_g2/platformio.ini index d3f45eb0..8ba368bd 100644 --- a/variants/station_g2/platformio.ini +++ b/variants/station_g2/platformio.ini @@ -9,18 +9,23 @@ build_flags = -D WRAPPER_CLASS=CustomSX1262Wrapper -D LORA_TX_POWER=7 ; -D P_LORA_TX_LED=35 -; -D PIN_BOARD_SDA=5 -; -D PIN_BOARD_SCL=6 - -D PIN_USER_BTN=0 + -D PIN_BOARD_SDA=5 + -D PIN_BOARD_SCL=6 + -D PIN_USER_BTN=38 -D SX126X_DIO2_AS_RF_SWITCH=true -D SX126X_DIO3_TCXO_VOLTAGE=1.8 -D SX126X_CURRENT_LIMIT=140 ; -D SX126X_RX_BOOSTED_GAIN=1 - DO NOT ENABLE THIS! + -I src/helpers/ui + -D DISPLAY_CLASS=SH1106Display ; https://wiki.uniteng.com/en/meshtastic/station-g2#impact-of-lora-node-dense-areashigh-noise-environments-on-rf-performance build_src_filter = ${esp32_base.build_src_filter} +<../variants/station_g2> + + lib_deps = ${esp32_base.lib_deps} + adafruit/Adafruit SH110X @ ~2.1.13 + adafruit/Adafruit GFX Library @ ^1.12.1 [env:Station_G2_repeater] extends = Station_G2 @@ -55,3 +60,39 @@ build_flags = lib_deps = ${Station_G2.lib_deps} ${esp32_ota.lib_deps} + +[env:Station_G2_companion_radio_usb] +extends = Station_G2 +build_flags = + ${Station_G2.build_flags} + -D DISPLAY_CLASS=SH1106Display + -D MAX_CONTACTS=100 + -D MAX_GROUP_CHANNELS=8 +; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 +; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 +build_src_filter = ${Station_G2.build_src_filter} + + + +<../examples/companion_radio> +lib_deps = + ${Station_G2.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:Station_G2_companion_radio_ble] +extends = Station_G2 +build_flags = + ${Station_G2.build_flags} + -D DISPLAY_CLASS=SH1106Display + -D MAX_CONTACTS=100 + -D MAX_GROUP_CHANNELS=8 + -D BLE_PIN_CODE=123456 + -D BLE_DEBUG_LOGGING=1 + -D OFFLINE_QUEUE_SIZE=256 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${Station_G2.build_src_filter} + + + + + +<../examples/companion_radio> +lib_deps = + ${Station_G2.lib_deps} + densaugeo/base64 @ ~1.4.0 diff --git a/variants/station_g2/target.cpp b/variants/station_g2/target.cpp index e279b139..dca94f21 100644 --- a/variants/station_g2/target.cpp +++ b/variants/station_g2/target.cpp @@ -16,6 +16,10 @@ ESP32RTCClock fallback_clock; AutoDiscoverRTCClock rtc_clock(fallback_clock); SensorManager sensors; +#ifdef DISPLAY_CLASS + DISPLAY_CLASS display; +#endif + #ifndef LORA_CR #define LORA_CR 5 #endif diff --git a/variants/station_g2/target.h b/variants/station_g2/target.h index e44c2ebe..695e700a 100644 --- a/variants/station_g2/target.h +++ b/variants/station_g2/target.h @@ -8,11 +8,19 @@ #include #include +#ifdef DISPLAY_CLASS + #include +#endif + extern StationG2Board board; extern WRAPPER_CLASS radio_driver; extern AutoDiscoverRTCClock rtc_clock; extern SensorManager sensors; +#ifdef DISPLAY_CLASS + extern DISPLAY_CLASS display; +#endif + bool radio_init(); uint32_t radio_get_rng_seed(); void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr); diff --git a/variants/t1000-e/t1000e_sensors.cpp b/variants/t1000-e/t1000e_sensors.cpp new file mode 100644 index 00000000..17693022 --- /dev/null +++ b/variants/t1000-e/t1000e_sensors.cpp @@ -0,0 +1,119 @@ +#include +#include "t1000e_sensors.h" + +#define HEATER_NTC_BX 4250 // thermistor coefficient B +#define HEATER_NTC_RP 8250 // ohm, series resistance to thermistor +#define HEATER_NTC_KA 273.15 // 25 Celsius at Kelvin +#define NTC_REF_VCC 3000 // mV, output voltage of LDO +#define LIGHT_REF_VCC 2400 // + +static unsigned int ntc_res2[136]={ + 113347,107565,102116,96978,92132,87559,83242,79166,75316,71677, + 68237,64991,61919,59011,56258,53650,51178,48835,46613,44506, + 42506,40600,38791,37073,35442,33892,32420,31020,29689,28423, + 27219,26076,24988,23951,22963,22021,21123,20267,19450,18670, + 17926,17214,16534,15886,15266,14674,14108,13566,13049,12554, + 12081,11628,11195,10780,10382,10000,9634,9284,8947,8624, + 8315,8018,7734,7461,7199,6948,6707,6475,6253,6039, + 5834,5636,5445,5262,5086,4917,4754,4597,4446,4301, + 4161,4026,3896,3771,3651,3535,3423,3315,3211,3111, + 3014,2922,2834,2748,2666,2586,2509,2435,2364,2294, + 2228,2163,2100,2040,1981,1925,1870,1817,1766,1716, + 1669,1622,1578,1535,1493,1452,1413,1375,1338,1303, + 1268,1234,1202,1170,1139,1110,1081,1053,1026,999, + 974,949,925,902,880,858, +}; + +static char ntc_temp2[136]= +{ + -30,-29,-28,-27,-26,-25,-24,-23,-22,-21, + -20,-19,-18,-17,-16,-15,-14,-13,-12,-11, + -10,-9,-8,-7,-6,-5,-4,-3,-2,-1, + 0,1,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,48,49, + 50,51,52,53,54,55,56,57,58,59, + 60,61,62,63,64,65,66,67,68,69, + 70,71,72,73,74,75,76,77,78,79, + 80,81,82,83,84,85,86,87,88,89, + 90,91,92,93,94,95,96,97,98,99, + 100,101,102,103,104,105, +}; + +static float get_heater_temperature( unsigned int vcc_volt, unsigned int ntc_volt ) +{ + int i = 0; + float Vout = 0, Rt = 0, temp = 0; + Vout = ntc_volt; + + Rt = ( HEATER_NTC_RP * vcc_volt ) / Vout - HEATER_NTC_RP; + + for( i = 0; i < 136; i++ ) + { + if( Rt >= ntc_res2[i] ) + { + break; + } + } + + temp = ntc_temp2[i - 1] + 1 * ( ntc_res2[i - 1] - Rt ) / ( float )( ntc_res2[i - 1] - ntc_res2[i] ); + + temp = ( temp * 100 + 5 ) / 100; + return temp; +} + +static int get_light_lv( unsigned int light_volt ) +{ + float Vout = 0, Vin = 0, Rt = 0, temp = 0; + unsigned int light_level = 0; + + if( light_volt <= 80 ) + { + light_level = 0; + return light_level; + } + else if( light_volt >= 2480 ) + { + light_level = 100; + return light_level; + } + Vout = light_volt; + light_level = 100 * ( Vout - 80 ) / LIGHT_REF_VCC; + + return light_level; +} + +float t1000e_get_temperature( void ) +{ + unsigned int ntc_v, vcc_v; + + digitalWrite(PIN_3V3_EN, HIGH); + digitalWrite(SENSOR_EN, HIGH); + analogReference(AR_INTERNAL_3_0); + analogReadResolution(12); + delay(10); + vcc_v = (1000.0*(analogRead(BATTERY_PIN) * ADC_MULTIPLIER * AREF_VOLTAGE)) / 4096; + ntc_v = (1000.0 * AREF_VOLTAGE * analogRead(TEMP_SENSOR)) / 4096; + digitalWrite(PIN_3V3_EN, LOW); + digitalWrite(SENSOR_EN, LOW); + + return get_heater_temperature (vcc_v, ntc_v); +} + +uint32_t t1000e_get_light( void ) +{ + int lux = 0; + unsigned int lux_v = 0; + + digitalWrite(SENSOR_EN, HIGH); + analogReference(AR_INTERNAL_3_0); + analogReadResolution(12); + delay(10); + lux_v = 1000 * analogRead(LUX_SENSOR) * AREF_VOLTAGE / 4096; + lux = get_light_lv( lux_v ); + digitalWrite(SENSOR_EN, LOW); + + return lux; +} \ No newline at end of file diff --git a/variants/t1000-e/t1000e_sensors.h b/variants/t1000-e/t1000e_sensors.h new file mode 100644 index 00000000..8f3967ca --- /dev/null +++ b/variants/t1000-e/t1000e_sensors.h @@ -0,0 +1,8 @@ +#pragma once + +// Light and temperature sensors are on ADC ports +// functions adapted from Seeed examples to get values +// see : https://github.com/Seeed-Studio/Seeed-Tracker-T1000-E-for-LoRaWAN-dev-board + +extern uint32_t t1000e_get_light(); +extern float t1000e_get_temperature(); \ No newline at end of file diff --git a/variants/t1000-e/target.cpp b/variants/t1000-e/target.cpp index 29ca1acd..49ae26eb 100644 --- a/variants/t1000-e/target.cpp +++ b/variants/t1000-e/target.cpp @@ -1,4 +1,5 @@ #include +#include "t1000e_sensors.h" #include "target.h" #include @@ -160,6 +161,10 @@ bool T1000SensorManager::querySensors(uint8_t requester_permissions, CayenneLPP& if (requester_permissions & TELEM_PERM_LOCATION) { // does requester have permission? telemetry.addGPS(TELEM_CHANNEL_SELF, node_lat, node_lon, node_altitude); } + if (requester_permissions & TELEM_PERM_ENVIRONMENT) { + telemetry.addLuminosity(TELEM_CHANNEL_SELF, t1000e_get_light()); + telemetry.addTemperature(TELEM_CHANNEL_SELF, t1000e_get_temperature()); + } return true; } @@ -169,7 +174,7 @@ void T1000SensorManager::loop() { _nmea->loop(); if (millis() > next_gps_update) { - if (_nmea->isValid()) { + if (gps_active && _nmea->isValid()) { node_lat = ((double)_nmea->getLatitude())/1000000.; node_lon = ((double)_nmea->getLongitude())/1000000.; node_altitude = ((double)_nmea->getAltitude()) / 1000.0; diff --git a/variants/t114/platformio.ini b/variants/t114/platformio.ini index 37e31e6f..dac12da9 100644 --- a/variants/t114/platformio.ini +++ b/variants/t114/platformio.ini @@ -15,6 +15,7 @@ board = heltec_t114 board_build.ldscript = boards/nrf52840_s140_v6.ld build_flags = ${nrf52840_t114.build_flags} -I variants/t114 + -I src/helpers/ui -DHELTEC_T114 -D P_LORA_TX_LED=35 -D RADIO_CLASS=CustomSX1262 @@ -22,20 +23,27 @@ build_flags = ${nrf52840_t114.build_flags} -D LORA_TX_POWER=22 -D SX126X_CURRENT_LIMIT=140 -D SX126X_RX_BOOSTED_GAIN=1 + -D ST7789 + -D DISPLAY_CLASS=ST7789Display build_src_filter = ${nrf52840_t114.build_src_filter} + + +<../variants/t114> + + + + + + lib_deps = ${nrf52840_t114.lib_deps} stevemarple/MicroNMEA @ ^2.0.6 + adafruit/Adafruit GFX Library @ ^1.12.1 debug_tool = jlink upload_protocol = nrfutil [env:Heltec_t114_repeater] extends = Heltec_t114 build_src_filter = ${Heltec_t114.build_src_filter} - +<../examples/simple_repeater/main.cpp> + +<../examples/simple_repeater> + build_flags = ${Heltec_t114.build_flags} -D ADVERT_NAME='"Heltec_T114 Repeater"' @@ -64,27 +72,17 @@ build_flags = extends = Heltec_t114 build_flags = ${Heltec_t114.build_flags} - -I src/helpers/ui - -D ST7789 - -D DISPLAY_CLASS=ST7789Display -D MAX_CONTACTS=100 -D MAX_GROUP_CHANNELS=8 -D BLE_PIN_CODE=123456 - -D BLE_DEBUG_LOGGING=1 +; -D BLE_DEBUG_LOGGING=1 -D OFFLINE_QUEUE_SIZE=256 -; -D ENABLE_PRIVATE_KEY_IMPORT=1 -; -D ENABLE_PRIVATE_KEY_EXPORT=1 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${Heltec_t114.build_src_filter} - + + +<../examples/companion_radio> - + - + - + lib_deps = - adafruit/Adafruit GFX Library @ ^1.12.1 ${Heltec_t114.lib_deps} densaugeo/base64 @ ~1.4.0 @@ -96,13 +94,11 @@ build_flags = -D MAX_GROUP_CHANNELS=8 ; -D BLE_PIN_CODE=123456 ; -D BLE_DEBUG_LOGGING=1 -; -D ENABLE_PRIVATE_KEY_IMPORT=1 -; -D ENABLE_PRIVATE_KEY_EXPORT=1 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${Heltec_t114.build_src_filter} + - +<../examples/companion_radio/main.cpp> + +<../examples/companion_radio> lib_deps = ${Heltec_t114.lib_deps} densaugeo/base64 @ ~1.4.0 \ No newline at end of file diff --git a/variants/t114/target.cpp b/variants/t114/target.cpp index a57a55e2..d97c03f6 100644 --- a/variants/t114/target.cpp +++ b/variants/t114/target.cpp @@ -18,41 +18,10 @@ T114SensorManager sensors = T114SensorManager(nmea); DISPLAY_CLASS display; #endif -#ifndef LORA_CR - #define LORA_CR 5 -#endif - bool radio_init() { rtc_clock.begin(Wire); - -#ifdef SX126X_DIO3_TCXO_VOLTAGE - float tcxo = SX126X_DIO3_TCXO_VOLTAGE; -#else - float tcxo = 1.6f; -#endif - SPI.setPins(P_LORA_MISO, P_LORA_SCLK, P_LORA_MOSI); - SPI.begin(); - int status = radio.begin(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, LORA_TX_POWER, 8, tcxo); - if (status != RADIOLIB_ERR_NONE) { - Serial.print("ERROR: radio init failed: "); - Serial.println(status); - return false; // fail - } - - radio.setCRC(1); - -#ifdef SX126X_CURRENT_LIMIT - radio.setCurrentLimit(SX126X_CURRENT_LIMIT); -#endif -#ifdef SX126X_DIO2_AS_RF_SWITCH - radio.setDio2AsRfSwitch(SX126X_DIO2_AS_RF_SWITCH); -#endif -#ifdef SX126X_RX_BOOSTED_GAIN - radio.setRxBoostedGainMode(SX126X_RX_BOOSTED_GAIN); -#endif - - return true; // success + return radio.std_init(&SPI); } uint32_t radio_get_rng_seed() { diff --git a/variants/techo/platformio.ini b/variants/techo/platformio.ini index 95c9ff84..8ad5ca03 100644 --- a/variants/techo/platformio.ini +++ b/variants/techo/platformio.ini @@ -67,8 +67,6 @@ build_flags = -D BLE_DEBUG_LOGGING=1 -D DISPLAY_CLASS=GxEPDDisplay -D OFFLINE_QUEUE_SIZE=256 -; -D ENABLE_PRIVATE_KEY_IMPORT=1 -; -D ENABLE_PRIVATE_KEY_EXPORT=1 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${LilyGo_Techo.build_src_filter} diff --git a/variants/thinknode_m1/platformio.ini b/variants/thinknode_m1/platformio.ini index 06910c0a..2104a080 100644 --- a/variants/thinknode_m1/platformio.ini +++ b/variants/thinknode_m1/platformio.ini @@ -19,6 +19,7 @@ build_flags = ${nrf52840_thinknode_m1.build_flags} -D RADIO_CLASS=CustomSX1262 -D WRAPPER_CLASS=CustomSX1262Wrapper -D LORA_TX_POWER=22 + -D P_LORA_TX_LED=13 -D SX126X_CURRENT_LIMIT=140 -D SX126X_RX_BOOSTED_GAIN=1 build_src_filter = ${nrf52840_thinknode_m1.build_src_filter} @@ -75,8 +76,6 @@ build_flags = -D DISPLAY_CLASS=GxEPDDisplay -D OFFLINE_QUEUE_SIZE=256 -D PIN_BUZZER=6 -; -D ENABLE_PRIVATE_KEY_IMPORT=1 -; -D ENABLE_PRIVATE_KEY_EXPORT=1 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${ThinkNode_M1.build_src_filter} diff --git a/variants/waveshare_rp2040_lora/platformio.ini b/variants/waveshare_rp2040_lora/platformio.ini new file mode 100644 index 00000000..2730734d --- /dev/null +++ b/variants/waveshare_rp2040_lora/platformio.ini @@ -0,0 +1,107 @@ +; Waveshare RP2040-LoRa-HF/LF +; https://files.waveshare.com/wiki/RP2040-LoRa/Rp2040-lora-sch.pdf + +[waveshare_rp2040_lora] +extends = rp2040_base + +board = pico +board_build.filesystem_size = 0.5m + +build_flags = ${rp2040_base.build_flags} + -I variants/waveshare_rp2040_lora + -D SX126X_CURRENT_LIMIT=140 + -D RADIO_CLASS=CustomSX1262 + -D WRAPPER_CLASS=CustomSX1262Wrapper + -D LORA_TX_POWER=22 + -D SX126X_RX_BOOSTED_GAIN=1 +; Debug options + ; -D DEBUG_RP2040_WIRE=1 + ; -D DEBUG_RP2040_SPI=1 + ; -D DEBUG_RP2040_CORE=1 + ; -D RADIOLIB_DEBUG_SPI=1 + ; -D DEBUG_RP2040_PORT=Serial + +build_src_filter = ${rp2040_base.build_src_filter} + + + +<../variants/waveshare_rp2040_lora> + +lib_deps = ${rp2040_base.lib_deps} + +[env:waveshare_rp2040_lora_Repeater] +extends = waveshare_rp2040_lora +build_flags = ${waveshare_rp2040_lora.build_flags} + -D ADVERT_NAME='"RP2040-LoRa Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=8 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${waveshare_rp2040_lora.build_src_filter} + +<../examples/simple_repeater> + +[env:waveshare_rp2040_lora_room_server] +extends = waveshare_rp2040_lora +build_flags = ${waveshare_rp2040_lora.build_flags} + -D ADVERT_NAME='"RP2040-LoRa 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 = ${waveshare_rp2040_lora.build_src_filter} + +<../examples/simple_room_server> + +[env:waveshare_rp2040_lora_companion_radio_usb] +extends = waveshare_rp2040_lora +build_flags = ${waveshare_rp2040_lora.build_flags} + -D MAX_CONTACTS=100 + -D MAX_GROUP_CHANNELS=8 +; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 +; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 +build_src_filter = ${waveshare_rp2040_lora.build_src_filter} + +<../examples/companion_radio> +lib_deps = ${waveshare_rp2040_lora.lib_deps} + densaugeo/base64 @ ~1.4.0 + +; [env:waveshare_rp2040_lora_companion_radio_ble] +; extends = waveshare_rp2040_lora +; build_flags = ${waveshare_rp2040_lora.build_flags} +; -D MAX_CONTACTS=100 +; -D MAX_GROUP_CHANNELS=8 +; -D BLE_PIN_CODE=123456 +; -D BLE_DEBUG_LOGGING=1 +; ; -D MESH_PACKET_LOGGING=1 +; ; -D MESH_DEBUG=1 +; build_src_filter = ${waveshare_rp2040_lora.build_src_filter} +; +<../examples/companion_radio> +; lib_deps = ${waveshare_rp2040_lora.lib_deps} +; densaugeo/base64 @ ~1.4.0 + +; [env:waveshare_rp2040_lora_companion_radio_wifi] +; extends = waveshare_rp2040_lora +; build_flags = ${waveshare_rp2040_lora.build_flags} +; -D MAX_CONTACTS=100 +; -D MAX_GROUP_CHANNELS=8 +; -D WIFI_DEBUG_LOGGING=1 +; -D WIFI_SSID='"myssid"' +; -D WIFI_PWD='"mypwd"' +; ; -D MESH_PACKET_LOGGING=1 +; ; -D MESH_DEBUG=1 +; build_src_filter = ${waveshare_rp2040_lora.build_src_filter} +; +<../examples/companion_radio> +; lib_deps = ${waveshare_rp2040_lora.lib_deps} +; densaugeo/base64 @ ~1.4.0 + +[env:waveshare_rp2040_lora_terminal_chat] +extends = waveshare_rp2040_lora +build_flags = ${waveshare_rp2040_lora.build_flags} + -D MAX_CONTACTS=100 + -D MAX_GROUP_CHANNELS=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${waveshare_rp2040_lora.build_src_filter} + +<../examples/simple_secure_chat/main.cpp> +lib_deps = ${waveshare_rp2040_lora.lib_deps} + densaugeo/base64 @ ~1.4.0 diff --git a/variants/waveshare_rp2040_lora/target.cpp b/variants/waveshare_rp2040_lora/target.cpp new file mode 100644 index 00000000..895f6db2 --- /dev/null +++ b/variants/waveshare_rp2040_lora/target.cpp @@ -0,0 +1,81 @@ +#include "target.h" + +#include +#include + +WaveshareBoard board; + +RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI1); +WRAPPER_CLASS radio_driver(radio, board); + +VolatileRTCClock fallback_clock; +AutoDiscoverRTCClock rtc_clock(fallback_clock); +SensorManager sensors; + +#ifndef LORA_CR +#define LORA_CR 5 +#endif + +bool radio_init() { + rtc_clock.begin(Wire); + +#ifdef SX126X_DIO3_TCXO_VOLTAGE + float tcxo = SX126X_DIO3_TCXO_VOLTAGE; +#else + float tcxo = 1.6f; +#endif + + SPI1.setSCK(P_LORA_SCLK); + SPI1.setTX(P_LORA_MOSI); + SPI1.setRX(P_LORA_MISO); + + pinMode(P_LORA_NSS, OUTPUT); + digitalWrite(P_LORA_NSS, HIGH); + + SPI1.begin(false); + + int status = radio.begin(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, + LORA_TX_POWER, 8, tcxo); + + if (status != RADIOLIB_ERR_NONE) { + Serial.print("ERROR: radio init failed: "); + Serial.println(status); + return false; // fail + } + + radio.setCRC(1); + +#ifdef SX126X_CURRENT_LIMIT + radio.setCurrentLimit(SX126X_CURRENT_LIMIT); +#endif + +#ifdef SX126X_DIO2_AS_RF_SWITCH + radio.setDio2AsRfSwitch(SX126X_DIO2_AS_RF_SWITCH); +#endif + +#ifdef SX126X_RX_BOOSTED_GAIN + radio.setRxBoostedGainMode(SX126X_RX_BOOSTED_GAIN); +#endif + + return true; // success +} + +uint32_t radio_get_rng_seed() { + return radio.random(0x7FFFFFFF); +} + +void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) { + radio.setFrequency(freq); + radio.setSpreadingFactor(sf); + radio.setBandwidth(bw); + radio.setCodingRate(cr); +} + +void radio_set_tx_power(uint8_t dbm) { + radio.setOutputPower(dbm); +} + +mesh::LocalIdentity radio_new_identity() { + RadioNoiseListener rng(radio); + return mesh::LocalIdentity(&rng); // create new random identity +} diff --git a/variants/waveshare_rp2040_lora/target.h b/variants/waveshare_rp2040_lora/target.h new file mode 100644 index 00000000..3ebe0570 --- /dev/null +++ b/variants/waveshare_rp2040_lora/target.h @@ -0,0 +1,21 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 + +#include +#include +#include +#include +#include +#include + +extern WaveshareBoard board; +extern WRAPPER_CLASS radio_driver; +extern AutoDiscoverRTCClock rtc_clock; +extern SensorManager sensors; + +bool radio_init(); +uint32_t radio_get_rng_seed(); +void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr); +void radio_set_tx_power(uint8_t dbm); +mesh::LocalIdentity radio_new_identity(); diff --git a/variants/xiao_c3/platformio.ini b/variants/xiao_c3/platformio.ini index 6024905f..55baf2b8 100644 --- a/variants/xiao_c3/platformio.ini +++ b/variants/xiao_c3/platformio.ini @@ -4,6 +4,7 @@ board = seeed_xiao_esp32c3 build_flags = ${esp32_base.build_flags} -I variants/xiao_c3 + -D ESP32_CPU_FREQ=80 -D LORA_TX_BOOST_PIN=D3 -D P_LORA_TX_LED=D5 -D PIN_VBAT_READ=D0 diff --git a/variants/xiao_c3/target.cpp b/variants/xiao_c3/target.cpp index 0ed1609d..b3701ca7 100644 --- a/variants/xiao_c3/target.cpp +++ b/variants/xiao_c3/target.cpp @@ -16,43 +16,16 @@ ESP32RTCClock fallback_clock; AutoDiscoverRTCClock rtc_clock(fallback_clock); SensorManager sensors; -#ifndef LORA_CR - #define LORA_CR 5 -#endif - bool radio_init() { fallback_clock.begin(); rtc_clock.begin(Wire); - -#ifdef SX126X_DIO3_TCXO_VOLTAGE - float tcxo = SX126X_DIO3_TCXO_VOLTAGE; -#else - float tcxo = 1.6f; -#endif #if defined(P_LORA_SCLK) spi.begin(P_LORA_SCLK, P_LORA_MISO, P_LORA_MOSI); + return radio.std_init(&spi); +#else + return radio.std_init(); #endif - int status = radio.begin(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, LORA_TX_POWER, 8, tcxo); - if (status != RADIOLIB_ERR_NONE) { - Serial.print("ERROR: radio init failed: "); - Serial.println(status); - return false; // fail - } - - radio.setCRC(1); - -#ifdef SX126X_CURRENT_LIMIT - radio.setCurrentLimit(SX126X_CURRENT_LIMIT); -#endif -#ifdef SX126X_DIO2_AS_RF_SWITCH - radio.setDio2AsRfSwitch(SX126X_DIO2_AS_RF_SWITCH); -#endif -#ifdef SX126X_RX_BOOSTED_GAIN - radio.setRxBoostedGainMode(SX126X_RX_BOOSTED_GAIN); -#endif - - return true; // success } uint32_t radio_get_rng_seed() { diff --git a/variants/xiao_nrf52/platformio.ini b/variants/xiao_nrf52/platformio.ini index 2f468f4f..c4934e04 100644 --- a/variants/xiao_nrf52/platformio.ini +++ b/variants/xiao_nrf52/platformio.ini @@ -36,6 +36,8 @@ build_flags = ${nrf52840_xiao.build_flags} -D P_LORA_RESET=D2 -D P_LORA_BUSY=D3 -D P_LORA_NSS=D4 + -D SX126X_RXEN=D5 + -D SX126X_TXEN=RADIOLIB_NC -D SX126X_DIO2_AS_RF_SWITCH=1 -D SX126X_DIO3_TCXO_VOLTAGE=1.8 -D SX126X_CURRENT_LIMIT=140 @@ -63,12 +65,11 @@ build_flags = -D BLE_PIN_CODE=123456 -D OFFLINE_QUEUE_SIZE=256 ; -D BLE_DEBUG_LOGGING=1 -; -D ENABLE_PRIVATE_KEY_IMPORT=1 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${Xiao_nrf52.build_src_filter} + - +<../examples/companion_radio/main.cpp> + +<../examples/companion_radio> lib_deps = ${Xiao_nrf52.lib_deps} densaugeo/base64 @ ~1.4.0 @@ -79,12 +80,11 @@ build_flags = ${Xiao_nrf52.build_flags} -D MAX_CONTACTS=100 -D MAX_GROUP_CHANNELS=8 -; -D ENABLE_PRIVATE_KEY_IMPORT=1 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${Xiao_nrf52.build_src_filter} + - +<../examples/companion_radio/main.cpp> + +<../examples/companion_radio> lib_deps = ${Xiao_nrf52.lib_deps} densaugeo/base64 @ ~1.4.0 diff --git a/variants/xiao_nrf52/target.cpp b/variants/xiao_nrf52/target.cpp index c78ea159..853855cd 100644 --- a/variants/xiao_nrf52/target.cpp +++ b/variants/xiao_nrf52/target.cpp @@ -8,7 +8,8 @@ RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BU WRAPPER_CLASS radio_driver(radio, board); -VolatileRTCClock rtc_clock; +VolatileRTCClock fallback_clock; +AutoDiscoverRTCClock rtc_clock(fallback_clock); EnvironmentSensorManager sensors; #ifndef LORA_CR @@ -16,7 +17,7 @@ EnvironmentSensorManager sensors; #endif bool radio_init() { -// rtc_clock.begin(Wire); + rtc_clock.begin(Wire); #ifdef SX126X_DIO3_TCXO_VOLTAGE float tcxo = SX126X_DIO3_TCXO_VOLTAGE; @@ -35,6 +36,10 @@ bool radio_init() { radio.setCRC(1); +#if defined(SX126X_RXEN) && defined(SX126X_TXEN) + radio.setRfSwitchPins(SX126X_RXEN, SX126X_TXEN); +#endif + #ifdef SX126X_CURRENT_LIMIT radio.setCurrentLimit(SX126X_CURRENT_LIMIT); #endif diff --git a/variants/xiao_nrf52/target.h b/variants/xiao_nrf52/target.h index ec298a43..eb299006 100644 --- a/variants/xiao_nrf52/target.h +++ b/variants/xiao_nrf52/target.h @@ -5,12 +5,13 @@ #include #include #include +#include #include #include extern XiaoNrf52Board board; extern WRAPPER_CLASS radio_driver; -extern VolatileRTCClock rtc_clock; +extern AutoDiscoverRTCClock rtc_clock; extern EnvironmentSensorManager sensors; bool radio_init(); diff --git a/variants/xiao_s3_wio/platformio.ini b/variants/xiao_s3_wio/platformio.ini index 841a50c3..4d6fed88 100644 --- a/variants/xiao_s3_wio/platformio.ini +++ b/variants/xiao_s3_wio/platformio.ini @@ -15,6 +15,8 @@ build_flags = ${esp32_base.build_flags} -D P_LORA_MOSI=9 -D PIN_USER_BTN=21 -D PIN_STATUS_LED=48 + -D SX126X_RXEN=38 + -D SX126X_TXEN=RADIOLIB_NC -D SX126X_DIO2_AS_RF_SWITCH=true -D SX126X_DIO3_TCXO_VOLTAGE=1.8 -D SX126X_CURRENT_LIMIT=140 @@ -83,8 +85,6 @@ build_flags = -D DISPLAY_CLASS=SSD1306Display -D OFFLINE_QUEUE_SIZE=256 ; -D BLE_DEBUG_LOGGING=1 -; -D ENABLE_PRIVATE_KEY_IMPORT=1 -; -D ENABLE_PRIVATE_KEY_EXPORT=1 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${Xiao_S3_WIO.build_src_filter} @@ -108,7 +108,7 @@ build_flags = ; -D MESH_DEBUG=1 build_src_filter = ${Xiao_S3_WIO.build_src_filter} + - +<../examples/companion_radio/main.cpp> + +<../examples/companion_radio> lib_deps = ${Xiao_S3_WIO.lib_deps} densaugeo/base64 @ ~1.4.0 diff --git a/variants/xiao_s3_wio/target.cpp b/variants/xiao_s3_wio/target.cpp index 09ee5daa..2f443205 100644 --- a/variants/xiao_s3_wio/target.cpp +++ b/variants/xiao_s3_wio/target.cpp @@ -47,7 +47,11 @@ bool radio_init() { } radio.setCRC(1); - + +#if defined(SX126X_RXEN) && defined(SX126X_TXEN) + radio.setRfSwitchPins(SX126X_RXEN, SX126X_TXEN); +#endif + #ifdef SX126X_CURRENT_LIMIT radio.setCurrentLimit(SX126X_CURRENT_LIMIT); #endif