diff --git a/boards/minewsemi_me25ls01.json b/boards/minewsemi_me25ls01.json new file mode 100644 index 00000000..4c943158 --- /dev/null +++ b/boards/minewsemi_me25ls01.json @@ -0,0 +1,59 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v7.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_WIO_WM1110 -DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + ["0x239A", "0x8029"], + ["0x239A", "0x0029"], + ["0x239A", "0x002A"], + ["0x239A", "0x802A"] + ], + "usb_product": "me25ls01-BOOT", + "mcu": "nrf52840", + "variant": "minewsemi_me25ls01", + "bsp": { + "name": "adafruit" + }, + "softdevice": { + "sd_flags": "-DS140", + "sd_name": "s140", + "sd_version": "7.3.0", + "sd_fwid": "0x0123" + }, + "bootloader": { + "settings_addr": "0xFF000" + } + }, + "connectivity": ["bluetooth"], + "debug": { + "jlink_device": "nRF52840_xxAA", + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52.cfg" + }, + "frameworks": ["arduino"], + "name": "Minewsemi ME25LS01", + "upload": { + "maximum_ram_size": 248832, + "maximum_size": 815104, + "speed": 115200, + "protocol": "nrfutil", + "protocols": [ + "jlink", + "nrfjprog", + "nrfutil", + "stlink", + "cmsis-dap", + "blackmagic" + ], + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true + }, + "url": "https://en.minewsemi.com/lora-module/lr1110-nrf52840-me25LS01", + "vendor": "MINEWSEMI" +} diff --git a/boards/seeed-wio-tracker-l1.json b/boards/seeed-wio-tracker-l1.json new file mode 100644 index 00000000..3602baab --- /dev/null +++ b/boards/seeed-wio-tracker-l1.json @@ -0,0 +1,61 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v7.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_SEEED_WIO_TRACKER_L1 -DNRF52840_XXAA -DSEEED_WIO_TRACKER_L1 ", + "f_cpu": "64000000L", + "hwids": [ + [ "0x2886", "0x1667" ], + [ "0x2886", "0x1668" ] + ], + "mcu": "nrf52840", + "variant": "Seeed_Wio_Tracker_L1", + "softdevice": { + "sd_flags": "-DS140", + "sd_name": "s140", + "sd_version": "7.3.0", + "sd_fwid": "0x0123" + }, + "bsp": { + "name": "adafruit" + }, + "bootloader": { + "settings_addr": "0xFF000" + }, + "usb_product": "Seeed Wio Tracker L1" + }, + "connectivity": [ + "bluetooth" + ], + "debug": { + "jlink_device": "nRF52840_xxAA", + "openocd_target": "nrf52.cfg", + "svd_path": "nrf52840.svd" + }, + "frameworks": [ + "arduino" + ], + "name": "Seeed Wio Tracker L1", + "upload": { + "maximum_ram_size": 248832, + "maximum_size": 815104, + "protocol": "nrfutil", + "speed": 115200, + "protocols": [ + "jlink", + "nrfjprog", + "nrfutil", + "cmsis-dap", + "sam-ba", + "blackmagic" + ], + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true + }, + "url": "https://wiki.seeedstudio.com/wio_tracker_l1_node/", + "vendor": "Seeed Studio" +} \ No newline at end of file diff --git a/boards/seeed_sensecap_solar.json b/boards/seeed_sensecap_solar.json new file mode 100644 index 00000000..d6630d82 --- /dev/null +++ b/boards/seeed_sensecap_solar.json @@ -0,0 +1,60 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v7.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_Seeed_XIAO_nRF52840 -DNRF52840_XXAA -DSEEED_XIAO_NRF52840 ", + "f_cpu": "64000000L", + "hwids": [ + [ "0x2886", "0x0059" ] + ], + "mcu": "nrf52840", + "variant": "Seeed_XIAO_nRF52840", + "softdevice": { + "sd_flags": "-DS140", + "sd_name": "s140", + "sd_version": "7.3.0", + "sd_fwid": "0x0123" + }, + "bsp": { + "name": "adafruit" + }, + "bootloader": { + "settings_addr": "0xFF000" + }, + "usb_product": "XIAO nRF52840" + }, + "connectivity": [ + "bluetooth" + ], + "debug": { + "jlink_device": "nRF52840_xxAA", + "openocd_target": "nrf52.cfg", + "svd_path": "nrf52840.svd" + }, + "frameworks": [ + "arduino" + ], + "name": "Seeed Studio XIAO nRF52840", + "upload": { + "maximum_ram_size": 248832, + "maximum_size": 815104, + "protocol": "nrfutil", + "speed": 115200, + "protocols": [ + "jlink", + "nrfjprog", + "nrfutil", + "cmsis-dap", + "sam-ba", + "blackmagic" + ], + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true + }, + "url": "https://wiki.seeedstudio.com/meshtastic_solar_node/", + "vendor": "Seeed Studio" +} \ No newline at end of file diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 879200b7..9331a50c 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -45,6 +45,10 @@ #define CMD_GET_CUSTOM_VARS 40 #define CMD_SET_CUSTOM_VAR 41 #define CMD_GET_ADVERT_PATH 42 +#define CMD_GET_TUNING_PARAMS 43 +// NOTE: CMD range 44..49 parked, potentially for WiFi operations +#define CMD_SEND_BINARY_REQ 50 +#define CMD_FACTORY_RESET 51 #define RESP_CODE_OK 0 #define RESP_CODE_ERR 1 @@ -69,6 +73,7 @@ #define RESP_CODE_SIGNATURE 20 #define RESP_CODE_CUSTOM_VARS 21 #define RESP_CODE_ADVERT_PATH 22 +#define RESP_CODE_TUNING_PARAMS 23 #define SEND_TIMEOUT_BASE_MILLIS 500 #define FLOOD_SEND_TIMEOUT_FACTOR 16.0f @@ -91,6 +96,7 @@ #define PUSH_CODE_TRACE_DATA 0x89 #define PUSH_CODE_NEW_ADVERT 0x8A #define PUSH_CODE_TELEMETRY_RESPONSE 0x8B +#define PUSH_CODE_BINARY_RESPONSE 0x8C #define ERR_CODE_UNSUPPORTED_CMD 1 #define ERR_CODE_NOT_FOUND 2 @@ -319,13 +325,17 @@ void MyMesh::queueMessage(const ContactInfo &from, uint8_t txt_type, mesh::Packe 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); + // we only want to show text messages on display, not cli data + bool should_display = txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_SIGNED_PLAIN; + if (should_display) { + ui_task.newMsg(path_len, from.name, text, offline_queue_len); + if (!_serial->isConnected()) { + ui_task.soundBuzzer(UIEventType::contactMessage); + } + } #endif } @@ -462,6 +472,7 @@ void MyMesh::onContactResponse(const ContactInfo &contact, const uint8_t *data, i += 6; // pub_key_prefix memcpy(&out_frame[i], &tag, 4); i += 4; // NEW: include server timestamp + out_frame[i++] = data[7]; // NEW (v7): ACL permissions } else { out_frame[i++] = PUSH_CODE_LOGIN_FAIL; out_frame[i++] = 0; // reserved @@ -484,7 +495,7 @@ void MyMesh::onContactResponse(const ContactInfo &contact, const uint8_t *data, 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 + } else if (len > 4 && tag == pending_telemetry) { // check for matching response tag pending_telemetry = 0; int i = 0; @@ -495,6 +506,17 @@ void MyMesh::onContactResponse(const ContactInfo &contact, const uint8_t *data, memcpy(&out_frame[i], &data[4], len - 4); i += (len - 4); _serial->writeFrame(out_frame, i); + } else if (len > 4 && tag == pending_req) { // check for matching response tag + pending_req = 0; + + int i = 0; + out_frame[i++] = PUSH_CODE_BINARY_RESPONSE; + out_frame[i++] = 0; // reserved + memcpy(&out_frame[i], &tag, 4); // app needs to match this to RESP_CODE_SENT.tag + i += 4; + memcpy(&out_frame[i], &data[4], len - 4); + i += (len - 4); + _serial->writeFrame(out_frame, i); } } @@ -560,7 +582,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe _cli_rescue = false; offline_queue_len = 0; app_target_ver = 0; - pending_login = pending_status = pending_telemetry = 0; + pending_login = pending_status = pending_telemetry = pending_req = 0; next_ack_idx = 0; sign_data = NULL; dirty_contacts_expiry = 0; @@ -1012,6 +1034,13 @@ void MyMesh::handleCmdFrame(size_t len) { _prefs.airtime_factor = ((float)af) / 1000.0f; savePrefs(); writeOKFrame(); + } else if (cmd_frame[0] == CMD_GET_TUNING_PARAMS) { + uint32_t rx = _prefs.rx_delay_base * 1000, af = _prefs.airtime_factor * 1000; + int i = 0; + out_frame[i++] = RESP_CODE_TUNING_PARAMS; + memcpy(&out_frame[i], &rx, 4); i += 4; + memcpy(&out_frame[i], &af, 4); i += 4; + _serial->writeFrame(out_frame, i); } else if (cmd_frame[0] == CMD_SET_OTHER_PARAMS) { _prefs.manual_add_contacts = cmd_frame[1]; if (len >= 3) { @@ -1090,7 +1119,7 @@ void MyMesh::handleCmdFrame(size_t len) { if (result == MSG_SEND_FAILED) { writeErrFrame(ERR_CODE_TABLE_FULL); } else { - pending_telemetry = pending_status = 0; + pending_req = 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; @@ -1110,7 +1139,7 @@ void MyMesh::handleCmdFrame(size_t len) { if (result == MSG_SEND_FAILED) { writeErrFrame(ERR_CODE_TABLE_FULL); } else { - pending_telemetry = pending_login = 0; + pending_req = 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; @@ -1122,7 +1151,7 @@ void MyMesh::handleCmdFrame(size_t len) { } else { writeErrFrame(ERR_CODE_NOT_FOUND); // contact not found } - } else if (cmd_frame[0] == CMD_SEND_TELEMETRY_REQ && len >= 4 + PUB_KEY_SIZE) { + } else if (cmd_frame[0] == CMD_SEND_TELEMETRY_REQ && len >= 4 + PUB_KEY_SIZE) { // can deprecate, in favour of CMD_SEND_BINARY_REQ uint8_t *pub_key = &cmd_frame[4]; ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); if (recipient) { @@ -1131,7 +1160,7 @@ void MyMesh::handleCmdFrame(size_t len) { if (result == MSG_SEND_FAILED) { writeErrFrame(ERR_CODE_TABLE_FULL); } else { - pending_status = pending_login = 0; + pending_status = pending_login = pending_req = 0; pending_telemetry = tag; // match this in onContactResponse() out_frame[0] = RESP_CODE_SENT; out_frame[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0; @@ -1157,6 +1186,27 @@ void MyMesh::handleCmdFrame(size_t len) { memcpy(&out_frame[i], telemetry.getBuffer(), tlen); i += tlen; _serial->writeFrame(out_frame, i); + } else if (cmd_frame[0] == CMD_SEND_BINARY_REQ && len >= 2 + PUB_KEY_SIZE) { + uint8_t *pub_key = &cmd_frame[1]; + ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); + if (recipient) { + uint8_t *req_data = &cmd_frame[1 + PUB_KEY_SIZE]; + uint32_t tag, est_timeout; + int result = sendRequest(*recipient, req_data, len - (1 + PUB_KEY_SIZE), tag, est_timeout); + if (result == MSG_SEND_FAILED) { + writeErrFrame(ERR_CODE_TABLE_FULL); + } else { + pending_status = pending_login = pending_telemetry = 0; + pending_req = 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)) { @@ -1312,6 +1362,15 @@ void MyMesh::handleCmdFrame(size_t len) { } else { writeErrFrame(ERR_CODE_NOT_FOUND); } + } else if (cmd_frame[0] == CMD_FACTORY_RESET && memcmp(&cmd_frame[1], "reset", 5) == 0) { + bool success = _store->formatFileSystem(); + if (success) { + writeOKFrame(); + delay(1000); + board.reboot(); // doesn't return + } else { + writeErrFrame(ERR_CODE_FILE_IO_ERROR); + } } else { writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); MESH_DEBUG_PRINTLN("ERROR: unknown command: %02X", cmd_frame[0]); diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 247a9b3d..81bf261e 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -7,14 +7,14 @@ #endif /*------------ Frame Protocol --------------*/ -#define FIRMWARE_VER_CODE 6 +#define FIRMWARE_VER_CODE 7 #ifndef FIRMWARE_BUILD_DATE -#define FIRMWARE_BUILD_DATE "2 Jul 2025" +#define FIRMWARE_BUILD_DATE "15 Jul 2025" #endif #ifndef FIRMWARE_VERSION -#define FIRMWARE_VERSION "v1.7.2" +#define FIRMWARE_VERSION "v1.7.3" #endif #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) @@ -160,7 +160,8 @@ private: NodePrefs _prefs; uint32_t pending_login; uint32_t pending_status; - uint32_t pending_telemetry; + uint32_t pending_telemetry; // pending _TELEMETRY_REQ + uint32_t pending_req; // pending _BINARY_REQ BaseSerialInterface *_serial; ContactsIterator _iter; diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index 71c9f24a..2ec94c4b 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 "2 Jul 2025" + #define FIRMWARE_BUILD_DATE "15 Jul 2025" #endif #ifndef FIRMWARE_VERSION - #define FIRMWARE_VERSION "v1.7.2" + #define FIRMWARE_VERSION "v1.7.3" #endif #ifndef LORA_FREQ @@ -149,7 +149,6 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { oldest->id = id; oldest->out_path_len = -1; // initially out_path is unknown oldest->last_timestamp = 0; - self_id.calcSharedSecret(oldest->secret, id); // calc ECDH shared secret return oldest; } @@ -341,8 +340,8 @@ protected: 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) + void onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) override { + if (packet->getPayloadType() == PAYLOAD_TYPE_ANON_REQ) { // received an initial request by a possible admin client (unknown at this stage) uint32_t timestamp; memcpy(×tamp, data, 4); @@ -369,6 +368,7 @@ protected: client->last_timestamp = timestamp; client->last_activity = getRTCClock()->getCurrentTime(); client->is_admin = is_admin; + memcpy(client->secret, secret, PUB_KEY_SIZE); uint32_t now = getRTCClock()->getCurrentTimeUnique(); memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp @@ -500,12 +500,12 @@ protected: } uint8_t temp[166]; - const char *command = (const char *) &data[5]; + char *command = (char *) &data[5]; char *reply = (char *) &temp[5]; if (is_retry) { *reply = 0; } else { - _cli.handleCommand(sender_timestamp, command, reply); + handleCommand(sender_timestamp, command, reply); } int text_len = strlen(reply); if (text_len > 0) { @@ -581,8 +581,6 @@ public: _prefs.interference_threshold = 0; // disabled } - CommonCLI* getCLI() { return &_cli; } - void begin(FILESYSTEM* fs) { mesh::Mesh::begin(); _fs = fs; @@ -706,6 +704,18 @@ public: ((SimpleMeshTables *)getTables())->resetStats(); } + void handleCommand(uint32_t sender_timestamp, char* command, char* reply) { + while (*command == ' ') command++; // skip leading spaces + + if (strlen(command) > 4 && command[2] == '|') { // optional prefix (for companion radio CLI) + memcpy(reply, command, 3); // reflect the prefix back + reply += 3; + command += 3; + } + + _cli.handleCommand(sender_timestamp, command, reply); // common CLI commands + } + void loop() { mesh::Mesh::loop(); @@ -817,7 +827,7 @@ void loop() { if (len > 0 && command[len - 1] == '\r') { // received complete line command[len - 1] = 0; // replace newline with C string null terminator char reply[160]; - the_mesh.getCLI()->handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial! + the_mesh.handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial! if (reply[0]) { Serial.print(" -> "); Serial.println(reply); } diff --git a/examples/simple_room_server/main.cpp b/examples/simple_room_server/main.cpp index c394f3e5..b8f74a0c 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 "2 Jul 2025" + #define FIRMWARE_BUILD_DATE "15 Jul 2025" #endif #ifndef FIRMWARE_VERSION - #define FIRMWARE_VERSION "v1.7.2" + #define FIRMWARE_VERSION "v1.7.3" #endif #ifndef LORA_FREQ @@ -188,7 +188,6 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { newClient->id = id; newClient->out_path_len = -1; // initially out_path is unknown newClient->last_timestamp = 0; - self_id.calcSharedSecret(newClient->secret, id); // calc ECDH shared secret return newClient; } @@ -334,7 +333,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { } return 0; // unknown command } - + protected: float getAirtimeBudgetFactor() const override { return _prefs.airtime_factor; @@ -432,8 +431,8 @@ protected: return true; } - 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) + void onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) override { + if (packet->getPayloadType() == PAYLOAD_TYPE_ANON_REQ) { // received an initial request by a possible admin client (unknown at this stage) uint32_t sender_timestamp, sender_sync_since; memcpy(&sender_timestamp, data, 4); memcpy(&sender_sync_since, &data[4], 4); // sender's "sync messags SINCE x" timestamp @@ -465,6 +464,7 @@ protected: client->sync_since = sender_sync_since; client->pending_ack = 0; client->push_failures = 0; + memcpy(client->secret, secret, PUB_KEY_SIZE); uint32_t now = getRTCClock()->getCurrentTime(); client->last_activity = now; @@ -555,7 +555,7 @@ protected: if (is_retry) { temp[5] = 0; // no reply } else { - _cli.handleCommand(sender_timestamp, (const char *) &data[5], (char *) &temp[5]); + handleCommand(sender_timestamp, (char *) &data[5], (char *) &temp[5]); temp[4] = (TXT_TYPE_CLI_DATA << 2); // attempt and flags, (NOTE: legacy was: TXT_TYPE_PLAIN) } send_ack = false; @@ -743,8 +743,6 @@ public: _num_posted = _num_post_pushes = 0; } - CommonCLI* getCLI() { return &_cli; } - void begin(FILESYSTEM* fs) { mesh::Mesh::begin(); _fs = fs; @@ -845,6 +843,18 @@ public: ((SimpleMeshTables *)getTables())->resetStats(); } + void handleCommand(uint32_t sender_timestamp, char* command, char* reply) { + while (*command == ' ') command++; // skip leading spaces + + if (strlen(command) > 4 && command[2] == '|') { // optional prefix (for companion radio CLI) + memcpy(reply, command, 3); // reflect the prefix back + reply += 3; + command += 3; + } + + _cli.handleCommand(sender_timestamp, command, reply); // common CLI commands + } + void loop() { mesh::Mesh::loop(); @@ -998,7 +1008,7 @@ void loop() { if (len > 0 && command[len - 1] == '\r') { // received complete line command[len - 1] = 0; // replace newline with C string null terminator char reply[160]; - the_mesh.getCLI()->handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial! + the_mesh.handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial! if (reply[0]) { Serial.print(" -> "); Serial.println(reply); } diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp new file mode 100644 index 00000000..5bbe9f4d --- /dev/null +++ b/examples/simple_sensor/SensorMesh.cpp @@ -0,0 +1,866 @@ +#include "SensorMesh.h" + +/* ------------------------------ Config -------------------------------- */ + +#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 ADVERT_NAME + #define ADVERT_NAME "sensor" +#endif +#ifndef ADVERT_LAT + #define ADVERT_LAT 0.0 +#endif +#ifndef ADVERT_LON + #define ADVERT_LON 0.0 +#endif + +#ifndef ADMIN_PASSWORD + #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 + +#ifndef SENSOR_READ_INTERVAL_SECS + #define SENSOR_READ_INTERVAL_SECS 60 +#endif + +/* ------------------------------ Code -------------------------------- */ + +#define REQ_TYPE_LOGIN 0x00 +#define REQ_TYPE_GET_STATUS 0x01 +#define REQ_TYPE_KEEP_ALIVE 0x02 +#define REQ_TYPE_GET_TELEMETRY_DATA 0x03 +#define REQ_TYPE_GET_AVG_MIN_MAX 0x04 +#define REQ_TYPE_GET_ACCESS_LIST 0x05 + +#define RESP_SERVER_LOGIN_OK 0 // response to ANON_REQ + +#define CLI_REPLY_DELAY_MILLIS 1000 + +#define LAZY_CONTACTS_WRITE_DELAY 5000 + +#define ALERT_ACK_EXPIRY_MILLIS 8000 // wait 8 secs for ACKs to alert messages + +static File openAppend(FILESYSTEM* _fs, const char* fname) { + #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + return _fs->open(fname, FILE_O_WRITE); + #elif defined(RP2040_PLATFORM) + return _fs->open(fname, "a"); + #else + return _fs->open(fname, "a", true); + #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 SensorMesh::loadContacts() { + num_contacts = 0; + if (_fs->exists("/s_contacts")) { + #if defined(RP2040_PLATFORM) + File file = _fs->open("/s_contacts", "r"); + #else + File file = _fs->open("/s_contacts"); + #endif + if (file) { + bool full = false; + while (!full) { + ContactInfo c; + uint8_t pub_key[32]; + uint8_t unused[6]; + + bool success = (file.read(pub_key, 32) == 32); + success = success && (file.read((uint8_t *) &c.permissions, 1) == 1); + success = success && (file.read(unused, 6) == 6); + success = success && (file.read((uint8_t *)&c.out_path_len, 1) == 1); + success = success && (file.read(c.out_path, 64) == 64); + success = success && (file.read(c.shared_secret, PUB_KEY_SIZE) == PUB_KEY_SIZE); + c.last_timestamp = 0; // transient + c.last_activity = 0; + + if (!success) break; // EOF + + c.id = mesh::Identity(pub_key); + if (num_contacts < MAX_CONTACTS) { + contacts[num_contacts++] = c; + } else { + full = true; + } + } + file.close(); + } + } +} + +void SensorMesh::saveContacts() { + File file = openWrite(_fs, "/s_contacts"); + if (file) { + uint8_t unused[5]; + memset(unused, 0, sizeof(unused)); + + for (int i = 0; i < num_contacts; i++) { + auto c = &contacts[i]; + if (c->permissions == 0) continue; // skip deleted entries + + bool success = (file.write(c->id.pub_key, 32) == 32); + success = success && (file.write((uint8_t *) &c->permissions, 1) == 1); + success = success && (file.write(unused, 6) == 6); + success = success && (file.write((uint8_t *)&c->out_path_len, 1) == 1); + success = success && (file.write(c->out_path, 64) == 64); + success = success && (file.write(c->shared_secret, PUB_KEY_SIZE) == PUB_KEY_SIZE); + + if (!success) break; // write failed + } + file.close(); + } +} + +static uint8_t getDataSize(uint8_t type) { + switch (type) { + case LPP_GPS: + return 9; + case LPP_POLYLINE: + return 8; // TODO: this is MINIMIUM + case LPP_GYROMETER: + case LPP_ACCELEROMETER: + return 6; + case LPP_GENERIC_SENSOR: + case LPP_FREQUENCY: + case LPP_DISTANCE: + case LPP_ENERGY: + case LPP_UNIXTIME: + return 4; + case LPP_COLOUR: + return 3; + case LPP_ANALOG_INPUT: + case LPP_ANALOG_OUTPUT: + case LPP_LUMINOSITY: + case LPP_TEMPERATURE: + case LPP_CONCENTRATION: + case LPP_BAROMETRIC_PRESSURE: + case LPP_RELATIVE_HUMIDITY: + case LPP_ALTITUDE: + case LPP_VOLTAGE: + case LPP_CURRENT: + case LPP_DIRECTION: + case LPP_POWER: + return 2; + } + return 1; +} + +static uint32_t getMultiplier(uint8_t type) { + switch (type) { + case LPP_CURRENT: + case LPP_DISTANCE: + case LPP_ENERGY: + return 1000; + case LPP_VOLTAGE: + case LPP_ANALOG_INPUT: + case LPP_ANALOG_OUTPUT: + return 100; + case LPP_TEMPERATURE: + case LPP_BAROMETRIC_PRESSURE: + case LPP_RELATIVE_HUMIDITY: + return 10; + } + return 1; +} + +static bool isSigned(uint8_t type) { + return type == LPP_ALTITUDE || type == LPP_TEMPERATURE || type == LPP_GYROMETER || + type == LPP_ANALOG_INPUT || type == LPP_ANALOG_OUTPUT || type == LPP_GPS || type == LPP_ACCELEROMETER; +} + +static float getFloat(const uint8_t * buffer, uint8_t size, uint32_t multiplier, bool is_signed) { + uint32_t value = 0; + for (uint8_t i = 0; i < size; i++) { + value = (value << 8) + buffer[i]; + } + + int sign = 1; + if (is_signed) { + uint32_t bit = 1ul << ((size * 8) - 1); + if ((value & bit) == bit) { + value = (bit << 1) - value; + sign = -1; + } + } + return sign * ((float) value / multiplier); +} + +static uint8_t putFloat(uint8_t * dest, float value, uint8_t size, uint32_t multiplier, bool is_signed) { + // check sign + bool sign = value < 0; + if (sign) value = -value; + + // get value to store + uint32_t v = value * multiplier; + + // format an uint32_t as if it was an int32_t + if (is_signed & sign) { + uint32_t mask = (1 << (size * 8)) - 1; + v = v & mask; + if (sign) v = mask - v + 1; + } + + // add bytes (MSB first) + for (uint8_t i=1; i<=size; i++) { + dest[size - i] = (v & 0xFF); + v >>= 8; + } + return size; +} + +uint8_t SensorMesh::handleRequest(uint8_t perms, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len) { + memcpy(reply_data, &sender_timestamp, 4); // reflect sender_timestamp back in response packet (kind of like a 'tag') + + if (req_type == REQ_TYPE_GET_TELEMETRY_DATA && (perms & PERM_GET_TELEMETRY) != 0) { + telemetry.reset(); + telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f); + // query other sensors -- target specific + sensors.querySensors(0xFF, telemetry); // allow all telemetry permissions for admin or guest + // TODO: let requester know permissions they have: telemetry.addPresence(TELEM_CHANNEL_SELF, perms); + + uint8_t tlen = telemetry.getSize(); + memcpy(&reply_data[4], telemetry.getBuffer(), tlen); + return 4 + tlen; // reply_len + } + if (req_type == REQ_TYPE_GET_AVG_MIN_MAX && (perms & PERM_GET_OTHER_STATS) != 0) { + uint32_t start_secs_ago, end_secs_ago; + memcpy(&start_secs_ago, &payload[0], 4); + memcpy(&end_secs_ago, &payload[4], 4); + uint8_t res1 = payload[8]; // reserved for future (extra query params) + uint8_t res2 = payload[9]; + + MinMaxAvg data[8]; + int n; + if (res1 == 0 && res2 == 0) { + n = querySeriesData(start_secs_ago, end_secs_ago, data, 8); + } else { + n = 0; + } + + uint8_t ofs = 4; + { + uint32_t now = getRTCClock()->getCurrentTime(); + memcpy(&reply_data[ofs], &now, 4); ofs += 4; + } + + for (int i = 0; i < n; i++) { + auto d = &data[i]; + reply_data[ofs++] = d->_channel; + reply_data[ofs++] = d->_lpp_type; + uint8_t sz = getDataSize(d->_lpp_type); + uint32_t mult = getMultiplier(d->_lpp_type); + bool is_signed = isSigned(d->_lpp_type); + ofs += putFloat(&reply_data[ofs], d->_min, sz, mult, is_signed); + ofs += putFloat(&reply_data[ofs], d->_max, sz, mult, is_signed); + ofs += putFloat(&reply_data[ofs], d->_avg, sz, mult, is_signed); + } + return ofs; + } + if (req_type == REQ_TYPE_GET_ACCESS_LIST && (perms & PERM_ACL_ROLE_MASK) == PERM_ACL_LEVEL3) { + uint8_t res1 = payload[0]; // reserved for future (extra query params) + uint8_t res2 = payload[1]; + if (res1 == 0 && res2 == 0) { + uint8_t ofs = 4; + for (int i = 0; i < num_contacts && ofs + 7 <= sizeof(reply_data) - 4; i++) { + auto c = &contacts[i]; + memcpy(&reply_data[ofs], c->id.pub_key, 6); ofs += 6; // just 6-byte pub_key prefix + reply_data[ofs++] = c->permissions; + } + return ofs; + } + } + return 0; // unknown command +} + +mesh::Packet* SensorMesh::createSelfAdvert() { + uint8_t app_data[MAX_ADVERT_DATA_SIZE]; + uint8_t app_data_len; + { + AdvertDataBuilder builder(ADV_TYPE_SENSOR, _prefs.node_name, _prefs.node_lat, _prefs.node_lon); + app_data_len = builder.encodeTo(app_data); + } + + return createAdvert(self_id, app_data, app_data_len); +} + +ContactInfo* SensorMesh::putContact(const mesh::Identity& id) { + uint32_t min_time = 0xFFFFFFFF; + ContactInfo* oldest = &contacts[MAX_CONTACTS - 1]; + for (int i = 0; i < num_contacts; i++) { + if (id.matches(contacts[i].id)) return &contacts[i]; // already known + if (!contacts[i].isAdmin() && contacts[i].last_activity < min_time) { + oldest = &contacts[i]; + min_time = oldest->last_activity; + } + } + + ContactInfo* c; + if (num_contacts < MAX_CONTACTS) { + c = &contacts[num_contacts++]; + } else { + c = oldest; // evict least active contact + } + memset(c, 0, sizeof(*c)); + c->id = id; + c->out_path_len = -1; // initially out_path is unknown + return c; +} + +void SensorMesh::applyContactPermissions(const uint8_t* pubkey, uint8_t perms) { + mesh::Identity id(pubkey); + auto c = putContact(id); + + if ((perms & PERM_ACL_ROLE_MASK) == PERM_ACL_GUEST) { // guest role is not persisted in contacts + memset(c, 0, sizeof(*c)); + } else { + c->permissions = perms; // update their permissions + self_id.calcSharedSecret(c->shared_secret, pubkey); + } + dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); // trigger saveContacts() +} + +void SensorMesh::sendAlert(ContactInfo* c, Trigger* t) { + int text_len = strlen(t->text); + + uint8_t data[MAX_PACKET_PAYLOAD]; + memcpy(data, &t->timestamp, 4); + data[4] = (TXT_TYPE_PLAIN << 2) | t->attempt; // attempt and flags + memcpy(&data[5], t->text, text_len); + + // calc expected ACK reply + mesh::Utils::sha256((uint8_t *)&t->expected_acks[t->attempt], 4, data, 5 + text_len, self_id.pub_key, PUB_KEY_SIZE); + t->attempt++; + + auto pkt = createDatagram(PAYLOAD_TYPE_TXT_MSG, c->id, c->shared_secret, data, 5 + text_len); + if (pkt) { + if (c->out_path_len >= 0) { // we have an out_path, so send DIRECT + sendDirect(pkt, c->out_path, c->out_path_len); + } else { + sendFlood(pkt); + } + } + t->send_expiry = futureMillis(ALERT_ACK_EXPIRY_MILLIS); +} + +void SensorMesh::alertIf(bool condition, Trigger& t, AlertPriority pri, const char* text) { + if (condition) { + if (!t.isTriggered() && num_alert_tasks < MAX_CONCURRENT_ALERTS) { + StrHelper::strncpy(t.text, text, sizeof(t.text)); + t.pri = pri; + t.send_expiry = 0; // signal that initial send is needed + t.attempt = 4; + t.curr_contact_idx = -1; // start iterating thru contacts[] + + alert_tasks[num_alert_tasks++] = &t; // add to queue + } + } else { + if (t.isTriggered()) { + t.text[0] = 0; + // remove 't' from alert queue + int i = 0; + while (i < num_alert_tasks && alert_tasks[i] != &t) i++; + + if (i < num_alert_tasks) { // found, now delete from array + num_alert_tasks--; + while (i < num_alert_tasks) { + alert_tasks[i] = alert_tasks[i + 1]; + i++; + } + } + } + } +} + +float SensorMesh::getAirtimeBudgetFactor() const { + return _prefs.airtime_factor; +} + +bool SensorMesh::allowPacketForward(const mesh::Packet* packet) { + if (_prefs.disable_fwd) return false; + if (packet->isRouteFlood() && packet->path_len >= _prefs.flood_max) return false; + return true; +} + +int SensorMesh::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); +} + +uint32_t SensorMesh::getRetransmitDelay(const mesh::Packet* packet) { + uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + packet->payload_len + 2) * _prefs.tx_delay_factor); + return getRNG()->nextInt(0, 6)*t; +} +uint32_t SensorMesh::getDirectRetransmitDelay(const mesh::Packet* packet) { + uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + packet->payload_len + 2) * _prefs.direct_tx_delay_factor); + return getRNG()->nextInt(0, 6)*t; +} +int SensorMesh::getInterferenceThreshold() const { + return _prefs.interference_threshold; +} +int SensorMesh::getAGCResetInterval() const { + return ((int)_prefs.agc_reset_interval) * 4000; // milliseconds +} + +uint8_t SensorMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data) { + if (strcmp((char *) data, _prefs.password) != 0) { // check for valid password + #if MESH_DEBUG + MESH_DEBUG_PRINTLN("Invalid password: %s", &data[4]); + #endif + return 0; + } + + auto client = putContact(sender); // add to contacts (if not already known) + if (sender_timestamp <= client->last_timestamp) { + MESH_DEBUG_PRINTLN("Possible login replay attack!"); + return 0; // FATAL: client table is full -OR- replay attack + } + + MESH_DEBUG_PRINTLN("Login success!"); + client->last_timestamp = sender_timestamp; + client->last_activity = getRTCClock()->getCurrentTime(); + client->permissions = PERM_ACL_LEVEL3 | PERM_RECV_ALERTS_HI | PERM_RECV_ALERTS_LO; // initially opt-in to receive alerts (can opt out) + memcpy(client->shared_secret, secret, PUB_KEY_SIZE); + + dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); + + uint32_t now = getRTCClock()->getCurrentTimeUnique(); + memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp + reply_data[4] = RESP_SERVER_LOGIN_OK; + reply_data[5] = 0; // NEW: recommended keep-alive interval (secs / 16) + reply_data[6] = 1; // 1 = is admin + reply_data[7] = client->permissions; + getRNG()->random(&reply_data[8], 4); // random blob to help packet-hash uniqueness + + return 12; // reply length +} + +void SensorMesh::handleCommand(uint32_t sender_timestamp, char* command, char* reply) { + while (*command == ' ') command++; // skip leading spaces + + if (strlen(command) > 4 && command[2] == '|') { // optional prefix (for companion radio CLI) + memcpy(reply, command, 3); // reflect the prefix back + reply += 3; + command += 3; + } + + // first, see if this is a custom-handled CLI command (ie. in main.cpp) + if (handleCustomCommand(sender_timestamp, command, reply)) { + return; // command has been handled + } + + // handle sensor-specific CLI commands + if (memcmp(command, "setperm ", 8) == 0) { // format: setperm {pubkey-hex} {permissions-int8} + char* hex = &command[8]; + char* sp = strchr(hex, ' '); // look for separator char + if (sp == NULL || sp - hex != PUB_KEY_SIZE*2) { + strcpy(reply, "Err - bad pubkey len"); + } else { + *sp++ = 0; // replace space with null terminator + + uint8_t pubkey[PUB_KEY_SIZE]; + if (mesh::Utils::fromHex(pubkey, PUB_KEY_SIZE, hex)) { + uint8_t perms = atoi(sp); + applyContactPermissions(pubkey, perms); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Err - bad pubkey"); + } + } + } else if (sender_timestamp == 0 && strcmp(command, "get acl") == 0) { + Serial.println("ACL:"); + for (int i = 0; i < num_contacts; i++) { + auto c = &contacts[i]; + + Serial.printf("%02X ", c->permissions); + mesh::Utils::printHex(Serial, c->id.pub_key, PUB_KEY_SIZE); + Serial.printf("\n"); + } + reply[0] = 0; + } else { + _cli.handleCommand(sender_timestamp, command, reply); // common CLI commands + } +} + +void SensorMesh::onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) { + if (packet->getPayloadType() == PAYLOAD_TYPE_ANON_REQ) { // received an initial request by a possible admin client (unknown at this stage) + uint32_t timestamp; + memcpy(×tamp, data, 4); + + data[len] = 0; // ensure null terminator + uint8_t reply_len = handleLoginReq(sender, secret, timestamp, &data[4]); + + if (reply_len == 0) return; // invalid request + + if (packet->isRouteFlood()) { + // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response + mesh::Packet* path = createPathReturn(sender, secret, packet->path, packet->path_len, + PAYLOAD_TYPE_RESPONSE, reply_data, reply_len); + if (path) sendFlood(path, SERVER_RESPONSE_DELAY); + } else { + mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, secret, reply_data, reply_len); + if (reply) sendFlood(reply, SERVER_RESPONSE_DELAY); + } + } +} + +int SensorMesh::searchPeersByHash(const uint8_t* hash) { + int n = 0; + for (int i = 0; i < num_contacts && n < MAX_SEARCH_RESULTS; i++) { + if (contacts[i].id.isHashMatch(hash)) { + matching_peer_indexes[n++] = i; // store the INDEXES of matching contacts (for subsequent 'peer' methods) + } + } + return n; +} + +void SensorMesh::getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) { + int i = matching_peer_indexes[peer_idx]; + if (i >= 0 && i < num_contacts) { + // lookup pre-calculated shared_secret + memcpy(dest_secret, contacts[i].shared_secret, PUB_KEY_SIZE); + } else { + MESH_DEBUG_PRINTLN("getPeerSharedSecret: Invalid peer idx: %d", i); + } +} + +void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_idx, const uint8_t* secret, uint8_t* data, size_t len) { + int i = matching_peer_indexes[sender_idx]; + if (i < 0 || i >= num_contacts) { + MESH_DEBUG_PRINTLN("onPeerDataRecv: Invalid sender idx: %d", i); + return; + } + + ContactInfo& from = contacts[i]; + + if (type == PAYLOAD_TYPE_REQ) { // request (from a known contact) + uint32_t timestamp; + memcpy(×tamp, data, 4); + + if (timestamp > from.last_timestamp) { // prevent replay attacks + uint8_t reply_len = handleRequest(from.isAdmin() ? 0xFFFF : from.permissions, timestamp, data[4], &data[5], len - 5); + if (reply_len == 0) return; // invalid command + + from.last_timestamp = timestamp; + from.last_activity = getRTCClock()->getCurrentTime(); + + if (packet->isRouteFlood()) { + // 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, reply_data, reply_len); + if (path) sendFlood(path, SERVER_RESPONSE_DELAY); + } else { + mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, from.id, secret, reply_data, 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, SERVER_RESPONSE_DELAY); + } else { + sendFlood(reply, SERVER_RESPONSE_DELAY); + } + } + } + } else { + MESH_DEBUG_PRINTLN("onPeerDataRecv: possible replay attack detected"); + } + } else if (type == PAYLOAD_TYPE_TXT_MSG && len > 5 && from.isAdmin()) { // a CLI command + uint32_t sender_timestamp; + memcpy(&sender_timestamp, data, 4); // timestamp (by sender's RTC clock - which could be wrong) + uint flags = (data[4] >> 2); // message attempt number, and other flags + + if (!(flags == TXT_TYPE_CLI_DATA)) { + MESH_DEBUG_PRINTLN("onPeerDataRecv: unsupported text type received: flags=%02x", (uint32_t)flags); + } else if (sender_timestamp > from.last_timestamp) { // prevent replay attacks + from.last_timestamp = sender_timestamp; + from.last_activity = getRTCClock()->getCurrentTime(); + + // len can be > original length, but 'text' will be padded with zeroes + data[len] = 0; // need to make a C string again, with null terminator + + uint8_t temp[166]; + char *command = (char *) &data[5]; + char *reply = (char *) &temp[5]; + handleCommand(sender_timestamp, command, reply); + + int text_len = strlen(reply); + if (text_len > 0) { + uint32_t timestamp = getRTCClock()->getCurrentTimeUnique(); + if (timestamp == sender_timestamp) { + // WORKAROUND: the two timestamps need to be different, in the CLI view + timestamp++; + } + memcpy(temp, ×tamp, 4); // mostly an extra blob to help make packet_hash unique + temp[4] = (TXT_TYPE_CLI_DATA << 2); + + auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, from.id, secret, temp, 5 + text_len); + if (reply) { + if (from.out_path_len < 0) { + sendFlood(reply, CLI_REPLY_DELAY_MILLIS); + } else { + sendDirect(reply, from.out_path, from.out_path_len, CLI_REPLY_DELAY_MILLIS); + } + } + } + } else { + MESH_DEBUG_PRINTLN("onPeerDataRecv: possible replay attack detected"); + } + } +} + +bool SensorMesh::onPeerPathRecv(mesh::Packet* packet, int sender_idx, const uint8_t* secret, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) { + int i = matching_peer_indexes[sender_idx]; + if (i < 0 || i >= num_contacts) { + MESH_DEBUG_PRINTLN("onPeerPathRecv: Invalid sender idx: %d", i); + return false; + } + + ContactInfo& from = contacts[i]; + + MESH_DEBUG_PRINTLN("PATH to contact, path_len=%d", (uint32_t) path_len); + // NOTE: for this impl, we just replace the current 'out_path' regardless, whenever sender sends us a new out_path. + // FUTURE: could store multiple out_paths per contact, and try to find which is the 'best'(?) + memcpy(from.out_path, path, from.out_path_len = path_len); // store a copy of path, for sendDirect() + from.last_activity = getRTCClock()->getCurrentTime(); + + // REVISIT: maybe make ALL out_paths non-persisted to minimise flash writes?? + if (from.isAdmin()) { + // only do saveContacts() (of this out_path change) if this is an admin + dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); + } + + // NOTE: no reciprocal path send!! + return false; +} + +void SensorMesh::onAckRecv(mesh::Packet* packet, uint32_t ack_crc) { + if (num_alert_tasks > 0) { + auto t = alert_tasks[0]; // check current alert task + for (int i = 0; i < t->attempt; i++) { + if (ack_crc == t->expected_acks[i]) { // matching ACK! + t->attempt = 4; // signal to move to next contact + t->send_expiry = 0; + packet->markDoNotRetransmit(); // ACK was for this node, so don't retransmit + return; + } + } + } +} + +SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables) + : mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(32), tables), + _cli(board, rtc, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4) +{ + num_contacts = 0; + next_local_advert = next_flood_advert = 0; + dirty_contacts_expiry = 0; + last_read_time = 0; + num_alert_tasks = 0; + + // defaults + memset(&_prefs, 0, sizeof(_prefs)); + _prefs.airtime_factor = 1.0; // one half + _prefs.rx_delay_base = 0.0f; // turn off by default, was 10.0; + _prefs.tx_delay_factor = 0.5f; // was 0.25f + StrHelper::strncpy(_prefs.node_name, ADVERT_NAME, sizeof(_prefs.node_name)); + _prefs.node_lat = ADVERT_LAT; + _prefs.node_lon = ADVERT_LON; + StrHelper::strncpy(_prefs.password, ADMIN_PASSWORD, sizeof(_prefs.password)); + _prefs.freq = LORA_FREQ; + _prefs.sf = LORA_SF; + _prefs.bw = LORA_BW; + _prefs.cr = LORA_CR; + _prefs.tx_power_dbm = LORA_TX_POWER; + _prefs.advert_interval = 1; // default to 2 minutes for NEW installs + _prefs.flood_advert_interval = 0; // disabled + _prefs.disable_fwd = true; + _prefs.flood_max = 64; + _prefs.interference_threshold = 0; // disabled +} + +void SensorMesh::begin(FILESYSTEM* fs) { + mesh::Mesh::begin(); + _fs = fs; + // load persisted prefs + _cli.loadPrefs(_fs); + + loadContacts(); + + radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); + radio_set_tx_power(_prefs.tx_power_dbm); + + updateAdvertTimer(); + updateFloodAdvertTimer(); +} + +bool SensorMesh::formatFileSystem() { +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + return InternalFS.format(); +#elif defined(RP2040_PLATFORM) + return LittleFS.format(); +#elif defined(ESP32) + return SPIFFS.format(); +#else + #error "need to implement file system erase" + return false; +#endif +} + +void SensorMesh::sendSelfAdvertisement(int delay_millis) { + mesh::Packet* pkt = createSelfAdvert(); + if (pkt) { + sendFlood(pkt, delay_millis); + } else { + MESH_DEBUG_PRINTLN("ERROR: unable to create advertisement packet!"); + } +} + +void SensorMesh::updateAdvertTimer() { + if (_prefs.advert_interval > 0) { // schedule local advert timer + next_local_advert = futureMillis( ((uint32_t)_prefs.advert_interval) * 2 * 60 * 1000); + } else { + next_local_advert = 0; // stop the timer + } +} +void SensorMesh::updateFloodAdvertTimer() { + if (_prefs.flood_advert_interval > 0) { // schedule flood advert timer + next_flood_advert = futureMillis( ((uint32_t)_prefs.flood_advert_interval) * 60 * 60 * 1000); + } else { + next_flood_advert = 0; // stop the timer + } +} + +void SensorMesh::setTxPower(uint8_t power_dbm) { + radio_set_tx_power(power_dbm); +} + +float SensorMesh::getTelemValue(uint8_t channel, uint8_t type) { + auto buf = telemetry.getBuffer(); + uint8_t size = telemetry.getSize(); + uint8_t i = 0; + + while (i + 2 < size) { + // Get channel # + uint8_t ch = buf[i++]; + // Get data type + uint8_t t = buf[i++]; + uint8_t sz = getDataSize(t); + + if (ch == channel && t == type) { + return getFloat(&buf[i], sz, getMultiplier(t), isSigned(t)); + } + i += sz; // skip + } + return 0.0f; // not found +} + +bool SensorMesh::getGPS(uint8_t channel, float& lat, float& lon, float& alt) { + if (channel == TELEM_CHANNEL_SELF) { + lat = sensors.node_lat; + lon = sensors.node_lon; + alt = sensors.node_altitude; + return true; + } + // REVISIT: custom GPS channels?? + return false; +} + +void SensorMesh::loop() { + mesh::Mesh::loop(); + + if (next_flood_advert && millisHasNowPassed(next_flood_advert)) { + mesh::Packet* pkt = createSelfAdvert(); + if (pkt) sendFlood(pkt); + + updateFloodAdvertTimer(); // schedule next flood advert + updateAdvertTimer(); // also schedule local advert (so they don't overlap) + } else if (next_local_advert && millisHasNowPassed(next_local_advert)) { + mesh::Packet* pkt = createSelfAdvert(); + if (pkt) sendZeroHop(pkt); + + updateAdvertTimer(); // schedule next local advert + } + + uint32_t curr = getRTCClock()->getCurrentTime(); + if (curr >= last_read_time + SENSOR_READ_INTERVAL_SECS) { + telemetry.reset(); + telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f); + // query other sensors -- target specific + sensors.querySensors(0xFF, telemetry); // allow all telemetry permissions + + onSensorDataRead(); + + last_read_time = curr; + } + + // check the alert send queue + if (num_alert_tasks > 0) { + auto t = alert_tasks[0]; // process head of queue + + if (millisHasNowPassed(t->send_expiry)) { // next send needed? + if (t->attempt >= 4) { // max attempts reached, try next contact + t->curr_contact_idx++; + if (t->curr_contact_idx >= num_contacts) { // no more contacts to try? + num_alert_tasks--; // remove t from queue + for (int i = 0; i < num_alert_tasks; i++) { + alert_tasks[i] = alert_tasks[i + 1]; + } + } else { + auto c = &contacts[t->curr_contact_idx]; + uint16_t pri_mask = (t->pri == HIGH_PRI_ALERT) ? PERM_RECV_ALERTS_HI : PERM_RECV_ALERTS_LO; + + if (c->permissions & pri_mask) { // contact wants alert + // reset attempts + t->attempt = (t->pri == LOW_PRI_ALERT) ? 3 : 0; // Low pri alerts, start at attempt #3 (ie. only make ONE attempt) + t->timestamp = getRTCClock()->getCurrentTimeUnique(); // need unique timestamp per contact + + sendAlert(c, t); // NOTE: modifies attempt, expected_acks[] and send_expiry + } else { + // next contact tested in next ::loop() + } + } + } else if (t->curr_contact_idx < num_contacts) { + auto c = &contacts[t->curr_contact_idx]; // send next attempt + sendAlert(c, t); // NOTE: modifies attempt, expected_acks[] and send_expiry + } else { + // contact list has likely been modified while waiting for alert ACK, cancel this task + t->attempt = 4; // next ::loop() will remove t from queue + } + } + } + + // is there are pending dirty contacts write needed? + if (dirty_contacts_expiry && millisHasNowPassed(dirty_contacts_expiry)) { + saveContacts(); + dirty_contacts_expiry = 0; + } +} diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h new file mode 100644 index 00000000..a357506f --- /dev/null +++ b/examples/simple_sensor/SensorMesh.h @@ -0,0 +1,168 @@ +#pragma once + +#include // needed for PlatformIO +#include + +#include "TimeSeriesData.h" + +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) +#include +#elif defined(RP2040_PLATFORM) +#include +#elif defined(ESP32) +#include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define PERM_ACL_ROLE_MASK 3 // lower 2 bits +#define PERM_ACL_GUEST 0 +#define PERM_ACL_LEVEL1 1 +#define PERM_ACL_LEVEL2 2 +#define PERM_ACL_LEVEL3 3 // admin + +#define PERM_GET_TELEMETRY (1 << 2) +#define PERM_GET_OTHER_STATS (1 << 3) +#define PERM_RESERVED1 (1 << 4) +#define PERM_RESERVED2 (1 << 5) +#define PERM_RECV_ALERTS_LO (1 << 6) // low priority alerts +#define PERM_RECV_ALERTS_HI (1 << 7) // high priority alerts + +struct ContactInfo { + mesh::Identity id; + uint8_t permissions; + int8_t out_path_len; + uint8_t out_path[MAX_PATH_SIZE]; + uint8_t shared_secret[PUB_KEY_SIZE]; + uint32_t last_timestamp; // by THEIR clock (transient) + uint32_t last_activity; // by OUR clock (transient) + + bool isAdmin() const { return (permissions & PERM_ACL_ROLE_MASK) == PERM_ACL_LEVEL3; } +}; + +#ifndef FIRMWARE_BUILD_DATE + #define FIRMWARE_BUILD_DATE "2 Jul 2025" +#endif + +#ifndef FIRMWARE_VERSION + #define FIRMWARE_VERSION "v1.7.2" +#endif + +#define FIRMWARE_ROLE "sensor" + +#define MAX_CONTACTS 20 + +#define MAX_SEARCH_RESULTS 8 +#define MAX_CONCURRENT_ALERTS 4 + +class SensorMesh : public mesh::Mesh, public CommonCLICallbacks { +public: + SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables); + void begin(FILESYSTEM* fs); + void loop(); + void handleCommand(uint32_t sender_timestamp, char* command, char* reply); + + // CommonCLI callbacks + const char* getFirmwareVer() override { return FIRMWARE_VERSION; } + const char* getBuildDate() override { return FIRMWARE_BUILD_DATE; } + const char* getRole() override { return FIRMWARE_ROLE; } + const char* getNodeName() { return _prefs.node_name; } + NodePrefs* getNodePrefs() { return &_prefs; } + void savePrefs() override { _cli.savePrefs(_fs); } + bool formatFileSystem() override; + void sendSelfAdvertisement(int delay_millis) override; + void updateAdvertTimer() override; + void updateFloodAdvertTimer() override; + void setLoggingOn(bool enable) override { } + void eraseLogFile() override { } + void dumpLogFile() override { } + void setTxPower(uint8_t power_dbm) override; + void formatNeighborsReply(char *reply) override { + strcpy(reply, "not supported"); + } + const uint8_t* getSelfIdPubKey() override { return self_id.pub_key; } + void clearStats() override { } + + float getTelemValue(uint8_t channel, uint8_t type); + +protected: + // current telemetry data queries + float getVoltage(uint8_t channel) { return getTelemValue(channel, LPP_VOLTAGE); } + float getCurrent(uint8_t channel) { return getTelemValue(channel, LPP_CURRENT); } + float getPower(uint8_t channel) { return getTelemValue(channel, LPP_POWER); } + float getTemperature(uint8_t channel) { return getTelemValue(channel, LPP_TEMPERATURE); } + float getRelativeHumidity(uint8_t channel) { return getTelemValue(channel, LPP_RELATIVE_HUMIDITY); } + float getBarometricPressure(uint8_t channel) { return getTelemValue(channel, LPP_BAROMETRIC_PRESSURE); } + float getAltitude(uint8_t channel) { return getTelemValue(channel, LPP_ALTITUDE); } + bool getGPS(uint8_t channel, float& lat, float& lon, float& alt); + + // alerts + enum AlertPriority { LOW_PRI_ALERT, HIGH_PRI_ALERT }; + + struct Trigger { + uint32_t timestamp; + AlertPriority pri; + uint32_t expected_acks[4]; + int8_t curr_contact_idx; + uint8_t attempt; + unsigned long send_expiry; + char text[MAX_PACKET_PAYLOAD]; + + Trigger() { text[0] = 0; } + bool isTriggered() const { return text[0] != 0; } + }; + void alertIf(bool condition, Trigger& t, AlertPriority pri, const char* text); + + virtual void onSensorDataRead() = 0; // for app to implement + virtual int querySeriesData(uint32_t start_secs_ago, uint32_t end_secs_ago, MinMaxAvg dest[], int max_num) = 0; // for app to implement + virtual bool handleCustomCommand(uint32_t sender_timestamp, char* command, char* reply) { return false; } + + // Mesh overrides + float getAirtimeBudgetFactor() const override; + bool allowPacketForward(const mesh::Packet* packet) override; + int calcRxDelay(float score, uint32_t air_time) const override; + uint32_t getRetransmitDelay(const mesh::Packet* packet) override; + uint32_t getDirectRetransmitDelay(const mesh::Packet* packet) override; + int getInterferenceThreshold() const override; + int getAGCResetInterval() const override; + void onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) override; + int searchPeersByHash(const uint8_t* hash) override; + void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) override; + void onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_idx, const uint8_t* secret, uint8_t* data, size_t len) override; + bool onPeerPathRecv(mesh::Packet* packet, int sender_idx, const uint8_t* secret, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override; + void onAckRecv(mesh::Packet* packet, uint32_t ack_crc) override; + +private: + FILESYSTEM* _fs; + unsigned long next_local_advert, next_flood_advert; + NodePrefs _prefs; + CommonCLI _cli; + uint8_t reply_data[MAX_PACKET_PAYLOAD]; + ContactInfo contacts[MAX_CONTACTS]; + int num_contacts; + unsigned long dirty_contacts_expiry; + CayenneLPP telemetry; + uint32_t last_read_time; + int matching_peer_indexes[MAX_SEARCH_RESULTS]; + int num_alert_tasks; + Trigger* alert_tasks[MAX_CONCURRENT_ALERTS]; + + void loadContacts(); + void saveContacts(); + uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data); + uint8_t handleRequest(uint8_t perms, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len); + mesh::Packet* createSelfAdvert(); + ContactInfo* putContact(const mesh::Identity& id); + void applyContactPermissions(const uint8_t* pubkey, uint8_t perms); + + void sendAlert(ContactInfo* c, Trigger* t); + +}; diff --git a/examples/simple_sensor/TimeSeriesData.cpp b/examples/simple_sensor/TimeSeriesData.cpp new file mode 100644 index 00000000..f6157f9a --- /dev/null +++ b/examples/simple_sensor/TimeSeriesData.cpp @@ -0,0 +1,45 @@ +#include "TimeSeriesData.h" + +void TimeSeriesData::recordData(mesh::RTCClock* clock, float value) { + uint32_t now = clock->getCurrentTime(); + if (now >= last_timestamp + interval_secs) { + last_timestamp = now; + + data[next] = value; // append to cycle table + next = (next + 1) % num_slots; + } +} + +void TimeSeriesData::calcMinMaxAvg(mesh::RTCClock* clock, uint32_t start_secs_ago, uint32_t end_secs_ago, MinMaxAvg* dest, uint8_t channel, uint8_t lpp_type) const { + int i = next, n = num_slots; + uint32_t ago = clock->getCurrentTime() - last_timestamp; + int num_values = 0; + float total = 0.0f; + + dest->_channel = channel; + dest->_lpp_type = lpp_type; + + // start at most recet recording, back-track through to oldest + while (n > 0) { + n--; + i = (i + num_slots - 1) % num_slots; // go back by one + if (ago >= end_secs_ago && ago < start_secs_ago) { // filter by the desired time range + float v = data[i]; + num_values++; + total += v; + if (num_values == 1) { + dest->_max = dest->_min = v; + } else { + if (v < dest->_min) dest->_min = v; + if (v > dest->_max) dest->_max = v; + } + } + ago += interval_secs; + } + // calc average + if (num_values > 0) { + dest->_avg = total / num_values; + } else { + dest->_max = dest->_min = dest->_avg = NAN; + } +} diff --git a/examples/simple_sensor/TimeSeriesData.h b/examples/simple_sensor/TimeSeriesData.h new file mode 100644 index 00000000..6efa7834 --- /dev/null +++ b/examples/simple_sensor/TimeSeriesData.h @@ -0,0 +1,29 @@ +#pragma once + +#include +#include + +struct MinMaxAvg { + float _min, _max, _avg; + uint8_t _lpp_type, _channel; +}; + +class TimeSeriesData { + float* data; + int num_slots, next; + uint32_t last_timestamp; + uint32_t interval_secs; + +public: + TimeSeriesData(float* array, int num, uint32_t secs) : num_slots(num), data(array), last_timestamp(0), next(0), interval_secs(secs) { + memset(data, 0, sizeof(float)*num); + } + TimeSeriesData(int num, uint32_t secs) : num_slots(num), last_timestamp(0), next(0), interval_secs(secs) { + data = new float[num]; + memset(data, 0, sizeof(float)*num); + } + + void recordData(mesh::RTCClock* clock, float value); + void calcMinMaxAvg(mesh::RTCClock* clock, uint32_t start_secs_ago, uint32_t end_secs_ago, MinMaxAvg* dest, uint8_t channel, uint8_t lpp_type) const; +}; + diff --git a/examples/simple_sensor/UITask.cpp b/examples/simple_sensor/UITask.cpp new file mode 100644 index 00000000..0694bc3c --- /dev/null +++ b/examples/simple_sensor/UITask.cpp @@ -0,0 +1,114 @@ +#include "UITask.h" +#include +#include + +#define AUTO_OFF_MILLIS 20000 // 20 seconds +#define BOOT_SCREEN_MILLIS 4000 // 4 seconds + +// 'meshcore', 128x13px +static const uint8_t meshcore_logo [] PROGMEM = { + 0x3c, 0x01, 0xe3, 0xff, 0xc7, 0xff, 0x8f, 0x03, 0x87, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, + 0x3c, 0x03, 0xe3, 0xff, 0xc7, 0xff, 0x8e, 0x03, 0x8f, 0xfe, 0x3f, 0xfe, 0x1f, 0xff, 0x1f, 0xfe, + 0x3e, 0x03, 0xc3, 0xff, 0x8f, 0xff, 0x0e, 0x07, 0x8f, 0xfe, 0x7f, 0xfe, 0x1f, 0xff, 0x1f, 0xfc, + 0x3e, 0x07, 0xc7, 0x80, 0x0e, 0x00, 0x0e, 0x07, 0x9e, 0x00, 0x78, 0x0e, 0x3c, 0x0f, 0x1c, 0x00, + 0x3e, 0x0f, 0xc7, 0x80, 0x1e, 0x00, 0x0e, 0x07, 0x1e, 0x00, 0x70, 0x0e, 0x38, 0x0f, 0x3c, 0x00, + 0x7f, 0x0f, 0xc7, 0xfe, 0x1f, 0xfc, 0x1f, 0xff, 0x1c, 0x00, 0x70, 0x0e, 0x38, 0x0e, 0x3f, 0xf8, + 0x7f, 0x1f, 0xc7, 0xfe, 0x0f, 0xff, 0x1f, 0xff, 0x1c, 0x00, 0xf0, 0x0e, 0x38, 0x0e, 0x3f, 0xf8, + 0x7f, 0x3f, 0xc7, 0xfe, 0x0f, 0xff, 0x1f, 0xff, 0x1c, 0x00, 0xf0, 0x1e, 0x3f, 0xfe, 0x3f, 0xf0, + 0x77, 0x3b, 0x87, 0x00, 0x00, 0x07, 0x1c, 0x0f, 0x3c, 0x00, 0xe0, 0x1c, 0x7f, 0xfc, 0x38, 0x00, + 0x77, 0xfb, 0x8f, 0x00, 0x00, 0x07, 0x1c, 0x0f, 0x3c, 0x00, 0xe0, 0x1c, 0x7f, 0xf8, 0x38, 0x00, + 0x73, 0xf3, 0x8f, 0xff, 0x0f, 0xff, 0x1c, 0x0e, 0x3f, 0xf8, 0xff, 0xfc, 0x70, 0x78, 0x7f, 0xf8, + 0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfe, 0x3c, 0x0e, 0x3f, 0xf8, 0xff, 0xfc, 0x70, 0x3c, 0x7f, 0xf8, + 0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfc, 0x3c, 0x0e, 0x1f, 0xf8, 0xff, 0xf8, 0x70, 0x3c, 0x7f, 0xf8, +}; + +void UITask::begin(NodePrefs* node_prefs, const char* build_date, const char* firmware_version) { + _prevBtnState = HIGH; + _auto_off = millis() + AUTO_OFF_MILLIS; + _node_prefs = node_prefs; + _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 *dash = strchr(version, '-'); + if(dash){ + *dash = 0; + } + + // v1.2.3 (1 Jan 2025) + sprintf(_version_info, "%s (%s)", version, build_date); +} + +void UITask::renderCurrScreen() { + char tmp[80]; + if (millis() < BOOT_SCREEN_MILLIS) { // boot screen + // meshcore logo + _display->setColor(DisplayDriver::BLUE); + int logoWidth = 128; + _display->drawXbm((_display->width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13); + + // version info + _display->setColor(DisplayDriver::LIGHT); + _display->setTextSize(1); + uint16_t versionWidth = _display->getTextWidth(_version_info); + _display->setCursor((_display->width() - versionWidth) / 2, 22); + _display->print(_version_info); + + // node type + const char* node_type = "< Sensor >"; + uint16_t typeWidth = _display->getTextWidth(node_type); + _display->setCursor((_display->width() - typeWidth) / 2, 35); + _display->print(node_type); + } else { // home screen + // node name + _display->setCursor(0, 0); + _display->setTextSize(1); + _display->setColor(DisplayDriver::GREEN); + _display->print(_node_prefs->node_name); + + // freq / sf + _display->setCursor(0, 20); + _display->setColor(DisplayDriver::YELLOW); + sprintf(tmp, "FREQ: %06.3f SF%d", _node_prefs->freq, _node_prefs->sf); + _display->print(tmp); + + // bw / cr + _display->setCursor(0, 30); + sprintf(tmp, "BW: %03.2f CR: %d", _node_prefs->bw, _node_prefs->cr); + _display->print(tmp); + } +} + +void UITask::loop() { +#ifdef PIN_USER_BTN + if (millis() >= _next_read) { + int btnState = digitalRead(PIN_USER_BTN); + if (btnState != _prevBtnState) { + if (btnState == LOW) { // pressed? + if (_display->isOn()) { + // TODO: any action ? + } else { + _display->turnOn(); + } + _auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer + } + _prevBtnState = btnState; + } + _next_read = millis() + 200; // 5 reads per second + } +#endif + + if (_display->isOn()) { + if (millis() >= _next_refresh) { + _display->startFrame(); + renderCurrScreen(); + _display->endFrame(); + + _next_refresh = millis() + 1000; // refresh every second + } + if (millis() > _auto_off) { + _display->turnOff(); + } + } +} diff --git a/examples/simple_sensor/UITask.h b/examples/simple_sensor/UITask.h new file mode 100644 index 00000000..a27259f1 --- /dev/null +++ b/examples/simple_sensor/UITask.h @@ -0,0 +1,19 @@ +#pragma once + +#include +#include + +class UITask { + DisplayDriver* _display; + unsigned long _next_read, _next_refresh, _auto_off; + int _prevBtnState; + NodePrefs* _node_prefs; + char _version_info[32]; + + void renderCurrScreen(); +public: + UITask(DisplayDriver& display) : _display(&display) { _next_read = _next_refresh = 0; } + void begin(NodePrefs* node_prefs, const char* build_date, const char* firmware_version); + + void loop(); +}; \ No newline at end of file diff --git a/examples/simple_sensor/main.cpp b/examples/simple_sensor/main.cpp new file mode 100644 index 00000000..c9e282a2 --- /dev/null +++ b/examples/simple_sensor/main.cpp @@ -0,0 +1,147 @@ +#include "SensorMesh.h" + +#ifdef DISPLAY_CLASS + #include "UITask.h" + static UITask ui_task(display); +#endif + +class MyMesh : public SensorMesh { +public: + MyMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables) + : SensorMesh(board, radio, ms, rng, rtc, tables), + battery_data(12*24, 5*60) // 24 hours worth of battery data, every 5 minutes + { + } + +protected: + /* ========================== custom logic here ========================== */ + Trigger low_batt, critical_batt; + TimeSeriesData battery_data; + + void onSensorDataRead() override { + float batt_voltage = getVoltage(TELEM_CHANNEL_SELF); + + battery_data.recordData(getRTCClock(), batt_voltage); // record battery + alertIf(batt_voltage < 3.4f, critical_batt, HIGH_PRI_ALERT, "Battery is critical!"); + alertIf(batt_voltage < 3.6f, low_batt, LOW_PRI_ALERT, "Battery is low"); + } + + int querySeriesData(uint32_t start_secs_ago, uint32_t end_secs_ago, MinMaxAvg dest[], int max_num) override { + battery_data.calcMinMaxAvg(getRTCClock(), start_secs_ago, end_secs_ago, &dest[0], TELEM_CHANNEL_SELF, LPP_VOLTAGE); + return 1; + } + + bool handleCustomCommand(uint32_t sender_timestamp, char* command, char* reply) override { + if (strcmp(command, "magic") == 0) { // example 'custom' command handling + strcpy(reply, "**Magic now done**"); + return true; // handled + } + return false; // not handled + } + /* ======================================================================= */ +}; + +StdRNG fast_rng; +SimpleMeshTables tables; + +MyMesh the_mesh(board, radio_driver, *new ArduinoMillis(), fast_rng, rtc_clock, tables); + +void halt() { + while (1) ; +} + +static char command[120]; + +void setup() { + Serial.begin(115200); + delay(1000); + + board.begin(); + +#ifdef DISPLAY_CLASS + if (display.begin()) { + display.startFrame(); + display.print("Please wait..."); + display.endFrame(); + } +#endif + + if (!radio_init()) { halt(); } + + fast_rng.begin(radio_get_rng_seed()); + + FILESYSTEM* fs; +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + InternalFS.begin(); + fs = &InternalFS; + IdentityStore store(InternalFS, ""); +#elif defined(ESP32) + SPIFFS.begin(true); + fs = &SPIFFS; + IdentityStore store(SPIFFS, "/identity"); +#elif defined(RP2040_PLATFORM) + LittleFS.begin(); + fs = &LittleFS; + IdentityStore store(LittleFS, "/identity"); + store.begin(); +#else + #error "need to define filesystem" +#endif + if (!store.load("_main", the_mesh.self_id)) { + MESH_DEBUG_PRINTLN("Generating new keypair"); + the_mesh.self_id = radio_new_identity(); // create new random identity + int count = 0; + while (count < 10 && (the_mesh.self_id.pub_key[0] == 0x00 || the_mesh.self_id.pub_key[0] == 0xFF)) { // reserved id hashes + the_mesh.self_id = radio_new_identity(); count++; + } + store.save("_main", the_mesh.self_id); + } + + Serial.print("Sensor ID: "); + mesh::Utils::printHex(Serial, the_mesh.self_id.pub_key, PUB_KEY_SIZE); Serial.println(); + + command[0] = 0; + + sensors.begin(); + + the_mesh.begin(fs); + +#ifdef DISPLAY_CLASS + ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION); +#endif + + // send out initial Advertisement to the mesh + the_mesh.sendSelfAdvertisement(16000); +} + +void loop() { + int len = strlen(command); + while (Serial.available() && len < sizeof(command)-1) { + char c = Serial.read(); + if (c != '\n') { + command[len++] = c; + command[len] = 0; + } + Serial.print(c); + } + if (len == sizeof(command)-1) { // command buffer full + command[sizeof(command)-1] = '\r'; + } + + if (len > 0 && command[len - 1] == '\r') { // received complete line + command[len - 1] = 0; // replace newline with C string null terminator + char reply[160]; + the_mesh.handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial! + if (reply[0]) { + Serial.print(" -> "); Serial.println(reply); + } + + command[0] = 0; // reset command buffer + } + + the_mesh.loop(); + sensors.loop(); +#ifdef DISPLAY_CLASS + ui_task.loop(); +#endif +} diff --git a/platformio.ini b/platformio.ini index 90e7cfb0..00a2d0fb 100644 --- a/platformio.ini +++ b/platformio.ini @@ -29,6 +29,20 @@ build_flags = -w -DNDEBUG -DRADIOLIB_STATIC_ONLY=1 -DRADIOLIB_GODMODE=1 -D LORA_SF=11 -D ENABLE_PRIVATE_KEY_IMPORT=1 ; NOTE: comment these out for more secure firmware -D ENABLE_PRIVATE_KEY_EXPORT=1 + -D RADIOLIB_EXCLUDE_CC1101=1 + -D RADIOLIB_EXCLUDE_RF69=1 + -D RADIOLIB_EXCLUDE_SX1231=1 + -D RADIOLIB_EXCLUDE_SI443X=1 + -D RADIOLIB_EXCLUDE_RFM2X=1 + -D RADIOLIB_EXCLUDE_SX128X=1 + -D RADIOLIB_EXCLUDE_AFSK=1 + -D RADIOLIB_EXCLUDE_AX25=1 + -D RADIOLIB_EXCLUDE_HELLSCHREIBER=1 + -D RADIOLIB_EXCLUDE_MORSE=1 + -D RADIOLIB_EXCLUDE_APRS=1 + -D RADIOLIB_EXCLUDE_BELL=1 + -D RADIOLIB_EXCLUDE_RTTY=1 + -D RADIOLIB_EXCLUDE_SSTV=1 build_src_filter = +<*.cpp> + @@ -63,14 +77,6 @@ build_flags = ${arduino_base.build_flags} -D NRF52_PLATFORM -D LFS_NO_ASSERT=1 -[nrf52840_base] -extends = nrf52_base -build_flags = ${nrf52_base.build_flags} -lib_deps = - ${nrf52_base.lib_deps} - rweather/Crypto @ ^0.4.0 - https://github.com/adafruit/Adafruit_nRF52_Arduino - ; ----------------- RP2040 --------------------- [rp2040_base] diff --git a/src/Mesh.cpp b/src/Mesh.cpp index 87f99878..f34f6f77 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -181,7 +181,7 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { uint8_t data[MAX_PACKET_PAYLOAD]; int len = Utils::MACThenDecrypt(secret, data, macAndData, pkt->payload_len - i); if (len > 0) { // success! - onAnonDataRecv(pkt, pkt->getPayloadType(), sender, data, len); + onAnonDataRecv(pkt, secret, sender, data, len); pkt->markDoNotRetransmit(); } } diff --git a/src/Mesh.h b/src/Mesh.h index 9649187c..b2047ac1 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -107,10 +107,10 @@ protected: /** * \brief A (now decrypted) data packet has been received. * NOTE: these can be received multiple times (per sender/contents), via different routes - * \param type one of: PAYLOAD_TYPE_ANON_REQ + * \param secret ECDH shared secret * \param sender public key provided by sender */ - virtual void onAnonDataRecv(Packet* packet, uint8_t type, const Identity& sender, uint8_t* data, size_t len) { } + virtual void onAnonDataRecv(Packet* packet, const uint8_t* secret, const Identity& sender, uint8_t* data, size_t len) { } /** * \brief A path TO 'sender' has been received. (also with optional 'extra' data encoded) diff --git a/src/helpers/AdvertDataHelpers.h b/src/helpers/AdvertDataHelpers.h index 6da18802..abe14cbd 100644 --- a/src/helpers/AdvertDataHelpers.h +++ b/src/helpers/AdvertDataHelpers.h @@ -8,7 +8,8 @@ #define ADV_TYPE_CHAT 1 #define ADV_TYPE_REPEATER 2 #define ADV_TYPE_ROOM 3 -//FUTURE: 4..15 +#define ADV_TYPE_SENSOR 4 +//FUTURE: 5..15 #define ADV_LATLON_MASK 0x10 #define ADV_FEAT1_MASK 0x20 // FUTURE diff --git a/src/helpers/BaseChatMesh.cpp b/src/helpers/BaseChatMesh.cpp index 271f28da..d6ba17b9 100644 --- a/src/helpers/BaseChatMesh.cpp +++ b/src/helpers/BaseChatMesh.cpp @@ -403,22 +403,52 @@ bool BaseChatMesh::importContact(const uint8_t src_buf[], uint8_t len) { } int BaseChatMesh::sendLogin(const ContactInfo& recipient, const char* password, uint32_t& est_timeout) { - int tlen; - uint8_t temp[24]; - uint32_t now = getRTCClock()->getCurrentTimeUnique(); - memcpy(temp, &now, 4); // mostly an extra blob to help make packet_hash unique - if (recipient.type == ADV_TYPE_ROOM) { - memcpy(&temp[4], &recipient.sync_since, 4); - int len = strlen(password); if (len > 15) len = 15; // max 15 chars currently - memcpy(&temp[8], password, len); - tlen = 8 + len; - } else { - int len = strlen(password); if (len > 15) len = 15; // max 15 chars currently - memcpy(&temp[4], password, len); - tlen = 4 + len; - } + mesh::Packet* pkt; + { + int tlen; + uint8_t temp[24]; + uint32_t now = getRTCClock()->getCurrentTimeUnique(); + memcpy(temp, &now, 4); // mostly an extra blob to help make packet_hash unique + if (recipient.type == ADV_TYPE_ROOM) { + memcpy(&temp[4], &recipient.sync_since, 4); + int len = strlen(password); if (len > 15) len = 15; // max 15 chars currently + memcpy(&temp[8], password, len); + tlen = 8 + len; + } else { + int len = strlen(password); if (len > 15) len = 15; // max 15 chars currently + memcpy(&temp[4], password, len); + tlen = 4 + len; + } - auto pkt = createAnonDatagram(PAYLOAD_TYPE_ANON_REQ, self_id, recipient.id, recipient.shared_secret, temp, tlen); + pkt = createAnonDatagram(PAYLOAD_TYPE_ANON_REQ, self_id, recipient.id, recipient.shared_secret, temp, tlen); + } + if (pkt) { + uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength()); + if (recipient.out_path_len < 0) { + sendFlood(pkt); + est_timeout = calcFloodTimeoutMillisFor(t); + return MSG_SEND_SENT_FLOOD; + } else { + sendDirect(pkt, recipient.out_path, recipient.out_path_len); + est_timeout = calcDirectTimeoutMillisFor(t, recipient.out_path_len); + return MSG_SEND_SENT_DIRECT; + } + } + return MSG_SEND_FAILED; +} + +int BaseChatMesh::sendRequest(const ContactInfo& recipient, const uint8_t* req_data, uint8_t data_len, uint32_t& tag, uint32_t& est_timeout) { + if (data_len > MAX_PACKET_PAYLOAD - 16) return MSG_SEND_FAILED; + + mesh::Packet* pkt; + { + uint8_t temp[MAX_PACKET_PAYLOAD]; + tag = getRTCClock()->getCurrentTimeUnique(); + memcpy(temp, &tag, 4); // mostly an extra blob to help make packet_hash unique + memcpy(&temp[4], req_data, data_len); + + pkt = createDatagram(PAYLOAD_TYPE_REQ, recipient.id, recipient.shared_secret, temp, 4 + data_len); + } if (pkt) { uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength()); if (recipient.out_path_len < 0) { @@ -435,14 +465,17 @@ int BaseChatMesh::sendLogin(const ContactInfo& recipient, const char* password, } int BaseChatMesh::sendRequest(const ContactInfo& recipient, uint8_t req_type, uint32_t& tag, uint32_t& est_timeout) { - uint8_t temp[13]; - tag = getRTCClock()->getCurrentTimeUnique(); - memcpy(temp, &tag, 4); // mostly an extra blob to help make packet_hash unique - temp[4] = req_type; - memset(&temp[5], 0, 4); // reserved (possibly for 'since' param) - getRNG()->random(&temp[9], 4); // random blob to help make packet-hash unique + mesh::Packet* pkt; + { + uint8_t temp[13]; + tag = getRTCClock()->getCurrentTimeUnique(); + memcpy(temp, &tag, 4); // mostly an extra blob to help make packet_hash unique + temp[4] = req_type; + memset(&temp[5], 0, 4); // reserved (possibly for 'since' param) + getRNG()->random(&temp[9], 4); // random blob to help make packet-hash unique - auto pkt = createDatagram(PAYLOAD_TYPE_REQ, recipient.id, recipient.shared_secret, temp, sizeof(temp)); + pkt = createDatagram(PAYLOAD_TYPE_REQ, recipient.id, recipient.shared_secret, temp, sizeof(temp)); + } if (pkt) { uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength()); if (recipient.out_path_len < 0) { diff --git a/src/helpers/BaseChatMesh.h b/src/helpers/BaseChatMesh.h index 53cd5018..903b4ece 100644 --- a/src/helpers/BaseChatMesh.h +++ b/src/helpers/BaseChatMesh.h @@ -134,6 +134,7 @@ public: bool sendGroupMessage(uint32_t timestamp, mesh::GroupChannel& channel, const char* sender_name, const char* text, int text_len); int sendLogin(const ContactInfo& recipient, const char* password, uint32_t& est_timeout); int sendRequest(const ContactInfo& recipient, uint8_t req_type, uint32_t& tag, uint32_t& est_timeout); + int sendRequest(const ContactInfo& recipient, const uint8_t* req_data, uint8_t data_len, uint32_t& tag, uint32_t& est_timeout); bool shareContactZeroHop(const ContactInfo& contact); uint8_t exportContact(const ContactInfo& contact, uint8_t dest_buf[]); bool importContact(const uint8_t src_buf[], uint8_t len); diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 323f3633..df510c28 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -120,21 +120,14 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) { #define MIN_LOCAL_ADVERT_INTERVAL 60 -void CommonCLI::checkAdvertInterval() { +void CommonCLI::savePrefs() { if (_prefs->advert_interval * 2 < MIN_LOCAL_ADVERT_INTERVAL) { _prefs->advert_interval = 0; // turn it off, now that device has been manually configured } + _callbacks->savePrefs(); } void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, char* reply) { - while (*command == ' ') command++; // skip leading spaces - - if (strlen(command) > 4 && command[2] == '|') { // optional prefix (for companion radio CLI) - memcpy(reply, command, 3); // reflect the prefix back - reply += 3; - command += 3; - } - if (memcmp(command, "reboot", 6) == 0) { _board->reboot(); // doesn't return } else if (memcmp(command, "advert", 6) == 0) { @@ -174,7 +167,6 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch } else if (memcmp(command, "password ", 9) == 0) { // change admin password StrHelper::strncpy(_prefs->password, &command[9], sizeof(_prefs->password)); - checkAdvertInterval(); savePrefs(); sprintf(reply, "password now: %s", _prefs->password); // echo back just to let admin know for sure!! } else if (memcmp(command, "clear stats", 11) == 0) { @@ -273,7 +265,6 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch strcpy(reply, "OK"); } else if (memcmp(config, "name ", 5) == 0) { StrHelper::strncpy(_prefs->node_name, &config[5], sizeof(_prefs->node_name)); - checkAdvertInterval(); savePrefs(); strcpy(reply, "OK"); } else if (memcmp(config, "repeat ", 7) == 0) { @@ -300,12 +291,10 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch } } else if (memcmp(config, "lat ", 4) == 0) { _prefs->node_lat = atof(&config[4]); - checkAdvertInterval(); savePrefs(); strcpy(reply, "OK"); } else if (memcmp(config, "lon ", 4) == 0) { _prefs->node_lon = atof(&config[4]); - checkAdvertInterval(); savePrefs(); strcpy(reply, "OK"); } else if (memcmp(config, "rxdelay ", 8) == 0) { diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 1778c715..b50bf7f3 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -55,10 +55,7 @@ class CommonCLI { char tmp[80]; mesh::RTCClock* getRTCClock() { return _rtc; } - void savePrefs() { _callbacks->savePrefs(); } - - void checkAdvertInterval(); - + void savePrefs(); void loadPrefsInt(FILESYSTEM* _fs, const char* filename); public: diff --git a/src/helpers/CustomLR1110.h b/src/helpers/CustomLR1110.h index 3451aac1..e82f48f5 100644 --- a/src/helpers/CustomLR1110.h +++ b/src/helpers/CustomLR1110.h @@ -9,6 +9,59 @@ class CustomLR1110 : public LR1110 { public: CustomLR1110(Module *mod) : LR1110(mod) { } + RadioLibTime_t getTimeOnAir(size_t len) override { + // calculate number of symbols + float N_symbol = 0; + if(this->codingRate <= RADIOLIB_LR11X0_LORA_CR_4_8_SHORT) { + // legacy coding rate - nice and simple + // get SF coefficients + float coeff1 = 0; + int16_t coeff2 = 0; + int16_t coeff3 = 0; + if(this->spreadingFactor < 7) { + // SF5, SF6 + coeff1 = 6.25; + coeff2 = 4*this->spreadingFactor; + coeff3 = 4*this->spreadingFactor; + } else if(this->spreadingFactor < 11) { + // SF7. SF8, SF9, SF10 + coeff1 = 4.25; + coeff2 = 4*this->spreadingFactor + 8; + coeff3 = 4*this->spreadingFactor; + } else { + // SF11, SF12 + coeff1 = 4.25; + coeff2 = 4*this->spreadingFactor + 8; + coeff3 = 4*(this->spreadingFactor - 2); + } + + // get CRC length + int16_t N_bitCRC = 16; + if(this->crcTypeLoRa == RADIOLIB_LR11X0_LORA_CRC_DISABLED) { + N_bitCRC = 0; + } + + // get header length + int16_t N_symbolHeader = 20; + if(this->headerType == RADIOLIB_LR11X0_LORA_HEADER_IMPLICIT) { + N_symbolHeader = 0; + } + + // calculate number of LoRa preamble symbols - NO! Lora preamble is already in symbols + // uint32_t N_symbolPreamble = (this->preambleLengthLoRa & 0x0F) * (uint32_t(1) << ((this->preambleLengthLoRa & 0xF0) >> 4)); + + // calculate the number of symbols - nope + // N_symbol = (float)N_symbolPreamble + coeff1 + 8.0f + ceilf((float)RADIOLIB_MAX((int16_t)(8 * len + N_bitCRC - coeff2 + N_symbolHeader), (int16_t)0) / (float)coeff3) * (float)(this->codingRate + 4); + // calculate the number of symbols - using only preamblelora because it's already in symbols + N_symbol = (float)preambleLengthLoRa + coeff1 + 8.0f + ceilf((float)RADIOLIB_MAX((int16_t)(8 * len + N_bitCRC - coeff2 + N_symbolHeader), (int16_t)0) / (float)coeff3) * (float)(this->codingRate + 4); + } else { + // long interleaving - not needed for this modem + } + + // get time-on-air in us + return(((uint32_t(1) << this->spreadingFactor) / this->bandwidthKhz) * N_symbol * 1000.0f); +} + bool isReceiving() { uint16_t irq = getIrqStatus(); bool detected = ((irq & LR1110_IRQ_HEADER_VALID) || (irq & LR1110_IRQ_HAS_PREAMBLE)); diff --git a/src/helpers/nrf52/SerialBLEInterface.cpp b/src/helpers/nrf52/SerialBLEInterface.cpp index f43a3767..a8c11d97 100644 --- a/src/helpers/nrf52/SerialBLEInterface.cpp +++ b/src/helpers/nrf52/SerialBLEInterface.cpp @@ -1,6 +1,27 @@ #include "SerialBLEInterface.h" +static SerialBLEInterface* instance; + +void SerialBLEInterface::onConnect(uint16_t connection_handle) { + BLE_DEBUG_PRINTLN("SerialBLEInterface: connected"); + if(instance){ + instance->_isDeviceConnected = true; + // no need to stop advertising on connect, as the ble stack does this automatically + } +} + +void SerialBLEInterface::onDisconnect(uint16_t connection_handle, uint8_t reason) { + BLE_DEBUG_PRINTLN("SerialBLEInterface: disconnected reason=%d", reason); + if(instance){ + instance->_isDeviceConnected = false; + instance->startAdv(); + } +} + void SerialBLEInterface::begin(const char* device_name, uint32_t pin_code) { + + instance = this; + char charpin[20]; sprintf(charpin, "%d", pin_code); @@ -13,11 +34,31 @@ void SerialBLEInterface::begin(const char* device_name, uint32_t pin_code) { Bluefruit.Security.setMITM(true); Bluefruit.Security.setPIN(charpin); + Bluefruit.Periph.setConnectCallback(onConnect); + Bluefruit.Periph.setDisconnectCallback(onDisconnect); + // To be consistent OTA DFU should be added first if it exists //bledfu.begin(); + + // Configure and start the BLE Uart service + bleuart.setPermission(SECMODE_ENC_WITH_MITM, SECMODE_ENC_WITH_MITM); + bleuart.begin(); + } void SerialBLEInterface::startAdv() { + + BLE_DEBUG_PRINTLN("SerialBLEInterface: starting advertising"); + + // clean restart if already advertising + if(Bluefruit.Advertising.isRunning()){ + BLE_DEBUG_PRINTLN("SerialBLEInterface: already advertising, stopping to allow clean restart"); + Bluefruit.Advertising.stop(); + } + + Bluefruit.Advertising.clearData(); // clear advertising data + Bluefruit.ScanResponse.clearData(); // clear scan response data + // Advertising packet Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE); Bluefruit.Advertising.addTxPower(); @@ -38,10 +79,25 @@ void SerialBLEInterface::startAdv() { * For recommended advertising interval * https://developer.apple.com/library/content/qa/qa1931/_index.html */ - Bluefruit.Advertising.restartOnDisconnect(true); + Bluefruit.Advertising.restartOnDisconnect(false); // don't restart automatically as we handle it in onDisconnect Bluefruit.Advertising.setInterval(32, 244); // in unit of 0.625 ms Bluefruit.Advertising.setFastTimeout(30); // number of seconds in fast mode - Bluefruit.Advertising.start(0); // 0 = Don't stop advertising after n seconds + Bluefruit.Advertising.start(0); // 0 = Don't stop advertising after n seconds + +} + +void SerialBLEInterface::stopAdv() { + + BLE_DEBUG_PRINTLN("SerialBLEInterface: stopping advertising"); + + // we only want to stop advertising if it's running, otherwise an invalid state error is logged by ble stack + if(!Bluefruit.Advertising.isRunning()){ + return; + } + + // stop advertising + Bluefruit.Advertising.stop(); + } // ---------- public methods @@ -52,25 +108,14 @@ void SerialBLEInterface::enable() { _isEnabled = true; clearBuffers(); - // Configure and start the BLE Uart service - bleuart.setPermission(SECMODE_ENC_WITH_MITM, SECMODE_ENC_WITH_MITM); - bleuart.begin(); - // Start advertising startAdv(); - - checkAdvRestart = false; } void SerialBLEInterface::disable() { _isEnabled = false; - BLE_DEBUG_PRINTLN("SerialBLEInterface::disable"); - - Bluefruit.Advertising.stop(); - - oldDeviceConnected = deviceConnected = false; - checkAdvRestart = false; + stopAdv(); } size_t SerialBLEInterface::writeFrame(const uint8_t src[], size_t len) { @@ -79,7 +124,7 @@ size_t SerialBLEInterface::writeFrame(const uint8_t src[], size_t len) { return 0; } - if (deviceConnected && len > 0) { + if (_isDeviceConnected && len > 0) { if (send_queue_len >= FRAME_QUEUE_SIZE) { BLE_DEBUG_PRINTLN("writeFrame(), send_queue is full!"); return 0; @@ -115,44 +160,14 @@ size_t SerialBLEInterface::checkRecvFrame(uint8_t dest[]) { } else { int len = bleuart.available(); if (len > 0) { - deviceConnected = true; // should probably use the callback to monitor cx bleuart.readBytes(dest, len); BLE_DEBUG_PRINTLN("readBytes: sz=%d, hdr=%d", len, (uint32_t) dest[0]); return len; } } - - if (Bluefruit.connected() == 0) deviceConnected = false; - - if (deviceConnected != oldDeviceConnected) { - if (!deviceConnected) { // disconnecting - clearBuffers(); - - BLE_DEBUG_PRINTLN("SerialBLEInterface -> disconnecting..."); - delay(500); // give the bluetooth stack the chance to get things ready - - checkAdvRestart = true; - } else { - BLE_DEBUG_PRINTLN("SerialBLEInterface -> stopping advertising"); - BLE_DEBUG_PRINTLN("SerialBLEInterface -> connecting..."); - // connecting - // do stuff here on connecting - Bluefruit.Advertising.stop(); - checkAdvRestart = false; - } - oldDeviceConnected = deviceConnected; - } - - if (checkAdvRestart) { - if (Bluefruit.connected() == 0) { - BLE_DEBUG_PRINTLN("SerialBLEInterface -> re-starting advertising"); - startAdv(); - } - checkAdvRestart = false; - } return 0; } bool SerialBLEInterface::isConnected() const { - return deviceConnected; //pServer != NULL && pServer->getConnectedCount() > 0; + return _isDeviceConnected; } diff --git a/src/helpers/nrf52/SerialBLEInterface.h b/src/helpers/nrf52/SerialBLEInterface.h index d5555f56..12a4f46a 100644 --- a/src/helpers/nrf52/SerialBLEInterface.h +++ b/src/helpers/nrf52/SerialBLEInterface.h @@ -5,10 +5,8 @@ class SerialBLEInterface : public BaseSerialInterface { BLEUart bleuart; - bool deviceConnected; - bool oldDeviceConnected; - bool checkAdvRestart; bool _isEnabled; + bool _isDeviceConnected; unsigned long _last_write; struct Frame { @@ -21,18 +19,19 @@ class SerialBLEInterface : public BaseSerialInterface { Frame send_queue[FRAME_QUEUE_SIZE]; void clearBuffers() { send_queue_len = 0; } - void startAdv(); + static void onConnect(uint16_t connection_handle); + static void onDisconnect(uint16_t connection_handle, uint8_t reason); public: SerialBLEInterface() { - deviceConnected = false; - oldDeviceConnected = false; - checkAdvRestart = false; _isEnabled = false; + _isDeviceConnected = false; _last_write = 0; send_queue_len = 0; } + void startAdv(); + void stopAdv(); void begin(const char* device_name, uint32_t pin_code); // BaseSerialInterface methods diff --git a/src/helpers/rp2040/XiaoRP2040Board.cpp b/src/helpers/rp2040/XiaoRP2040Board.cpp new file mode 100644 index 00000000..bb439706 --- /dev/null +++ b/src/helpers/rp2040/XiaoRP2040Board.cpp @@ -0,0 +1,30 @@ +#include "XiaoRP2040Board.h" + +#include +#include + +void XiaoRP2040Board::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 XiaoRP2040Board::startOTAUpdate(const char *id, char reply[]) { + return false; +} diff --git a/src/helpers/rp2040/XiaoRP2040Board.h b/src/helpers/rp2040/XiaoRP2040Board.h new file mode 100644 index 00000000..c9353906 --- /dev/null +++ b/src/helpers/rp2040/XiaoRP2040Board.h @@ -0,0 +1,75 @@ +#pragma once + +#include +#include + +// LoRa radio module pins for the Xiao RP2040 +// https://wiki.seeedstudio.com/XIAO-RP2040/ + +#define P_LORA_DIO_1 27 // D1 +#define P_LORA_NSS 6 // D4 +#define P_LORA_RESET 28 // D2 +#define P_LORA_BUSY 29 // D3 +#define P_LORA_TX_LED 17 + +#define SX126X_RXEN 7 // D5 +#define SX126X_TXEN -1 + +#define SX126X_DIO2_AS_RF_SWITCH true +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +/* + * 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. + * If your using the WIO SX1262 Addon for xaio, make sure you dont connect D0! + * + * BAT+ -----+ + * | + * VSYS --+ -/\/\/\/\- --+ + * 200k | + * +-- D0 + * | + * GND --+ -/\/\/\/\- --+ + * | 100k + * BAT- -----+ + */ +#define PIN_VBAT_READ 26 // D0 +#define BATTERY_SAMPLES 8 +#define ADC_MULTIPLIER (3.0f * 3.3f * 1000) + +class XiaoRP2040Board : 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 "Xiao RP2040"; } + + void reboot() override { rp2040.reboot(); } + + bool startOTAUpdate(const char *id, char reply[]) override; +}; diff --git a/src/helpers/sensors/EnvironmentSensorManager.cpp b/src/helpers/sensors/EnvironmentSensorManager.cpp index b672cdd4..ab5b459e 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.cpp +++ b/src/helpers/sensors/EnvironmentSensorManager.cpp @@ -24,6 +24,14 @@ static Adafruit_BME280 BME280; static Adafruit_BMP280 BMP280; #endif +#if ENV_INCLUDE_SHTC3 +#include +static Adafruit_SHTC3 SHTC3; +#endif + +#if ENV_INCLUDE_LPS22HB +#include +#endif #if ENV_INCLUDE_INA3221 #define TELEM_INA3221_ADDRESS 0x42 // INA3221 3 channel current sensor I2C address @@ -76,28 +84,48 @@ bool EnvironmentSensorManager::begin() { } #endif + #if ENV_INCLUDE_SHTC3 + if (SHTC3.begin()) { + MESH_DEBUG_PRINTLN("Found sensor: SHTC3"); + SHTC3_initialized = true; + } else { + SHTC3_initialized = false; + MESH_DEBUG_PRINTLN("SHTC3 was not found at I2C address %02X", 0x70); + } + #endif + + #if ENV_INCLUDE_LPS22HB + if (BARO.begin()) { + MESH_DEBUG_PRINTLN("Found sensor: LPS22HB"); + LPS22HB_initialized = true; + } else { + LPS22HB_initialized = false; + MESH_DEBUG_PRINTLN("LPS22HB was not found at I2C address %02X", 0x5C); + } + #endif + #if ENV_INCLUDE_INA3221 if (INA3221.begin(TELEM_INA3221_ADDRESS, &Wire)) { - MESH_DEBUG_PRINTLN("Found INA3221 at address: %02X", TELEM_INA3221_ADDRESS); - MESH_DEBUG_PRINTLN("%04X %04X", INA3221.getDieID(), INA3221.getManufacturerID()); + MESH_DEBUG_PRINTLN("Found INA3221 at address: %02X", TELEM_INA3221_ADDRESS); + MESH_DEBUG_PRINTLN("%04X %04X", INA3221.getDieID(), INA3221.getManufacturerID()); - for(int i = 0; i < 3; i++) { - INA3221.setShuntResistance(i, TELEM_INA3221_SHUNT_VALUE); - } - INA3221_initialized = true; + for(int i = 0; i < 3; i++) { + INA3221.setShuntResistance(i, TELEM_INA3221_SHUNT_VALUE); + } + INA3221_initialized = true; } else { - INA3221_initialized = false; - MESH_DEBUG_PRINTLN("INA3221 was not found at I2C address %02X", TELEM_INA3221_ADDRESS); + INA3221_initialized = false; + MESH_DEBUG_PRINTLN("INA3221 was not found at I2C address %02X", TELEM_INA3221_ADDRESS); } #endif #if ENV_INCLUDE_INA219 if (INA219.begin(&Wire)) { - MESH_DEBUG_PRINTLN("Found INA219 at address: %02X", TELEM_INA219_ADDRESS); - INA219_initialized = true; + MESH_DEBUG_PRINTLN("Found INA219 at address: %02X", TELEM_INA219_ADDRESS); + INA219_initialized = true; } else { - INA219_initialized = false; - MESH_DEBUG_PRINTLN("INA219 was not found at I2C address %02X", TELEM_INA219_ADDRESS); + INA219_initialized = false; + MESH_DEBUG_PRINTLN("INA219 was not found at I2C address %02X", TELEM_INA219_ADDRESS); } #endif @@ -126,7 +154,7 @@ bool EnvironmentSensorManager::querySensors(uint8_t requester_permissions, Cayen if (BME280_initialized) { telemetry.addTemperature(TELEM_CHANNEL_SELF, BME280.readTemperature()); telemetry.addRelativeHumidity(TELEM_CHANNEL_SELF, BME280.readHumidity()); - telemetry.addBarometricPressure(TELEM_CHANNEL_SELF, BME280.readPressure()); + telemetry.addBarometricPressure(TELEM_CHANNEL_SELF, BME280.readPressure()/100); telemetry.addAltitude(TELEM_CHANNEL_SELF, BME280.readAltitude(TELEM_BME280_SEALEVELPRESSURE_HPA)); } #endif @@ -139,6 +167,23 @@ bool EnvironmentSensorManager::querySensors(uint8_t requester_permissions, Cayen } #endif + #if ENV_INCLUDE_SHTC3 + if (SHTC3_initialized) { + sensors_event_t humidity, temp; + SHTC3.getEvent(&humidity, &temp); + + telemetry.addTemperature(TELEM_CHANNEL_SELF, temp.temperature); + telemetry.addRelativeHumidity(TELEM_CHANNEL_SELF, humidity.relative_humidity); + } + #endif + + #if ENV_INCLUDE_LPS22HB + if (LPS22HB_initialized) { + telemetry.addTemperature(TELEM_CHANNEL_SELF, BARO.readTemperature()); + telemetry.addBarometricPressure(TELEM_CHANNEL_SELF, BARO.readPressure()); + } + #endif + #if ENV_INCLUDE_INA3221 if (INA3221_initialized) { for(int i = 0; i < TELEM_INA3221_NUM_CHANNELS; i++) { @@ -157,10 +202,10 @@ bool EnvironmentSensorManager::querySensors(uint8_t requester_permissions, Cayen #if ENV_INCLUDE_INA219 if (INA219_initialized) { - telemetry.addVoltage(next_available_channel, INA219.getBusVoltage_V()); - telemetry.addCurrent(next_available_channel, INA219.getCurrent_mA() / 1000); - telemetry.addPower(next_available_channel, INA219.getPower_mW() / 1000); - next_available_channel++; + telemetry.addVoltage(next_available_channel, INA219.getBusVoltage_V()); + telemetry.addCurrent(next_available_channel, INA219.getCurrent_mA() / 1000); + telemetry.addPower(next_available_channel, INA219.getPower_mW() / 1000); + next_available_channel++; } #endif diff --git a/src/helpers/sensors/EnvironmentSensorManager.h b/src/helpers/sensors/EnvironmentSensorManager.h index 76bffc48..f7804431 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.h +++ b/src/helpers/sensors/EnvironmentSensorManager.h @@ -13,6 +13,8 @@ protected: bool BMP280_initialized = false; bool INA3221_initialized = false; bool INA219_initialized = false; + bool SHTC3_initialized = false; + bool LPS22HB_initialized = false; bool gps_detected = false; bool gps_active = false; diff --git a/src/helpers/ui/buzzer.cpp b/src/helpers/ui/buzzer.cpp index c8e5cfcc..ca469d17 100644 --- a/src/helpers/ui/buzzer.cpp +++ b/src/helpers/ui/buzzer.cpp @@ -46,6 +46,13 @@ void genericBuzzer::shutdown() { void genericBuzzer::quiet(bool buzzer_state) { _is_quiet = buzzer_state; +#ifdef PIN_BUZZER_EN + if (_is_quiet) { + digitalWrite(PIN_BUZZER_EN, LOW); + } else { + digitalWrite(PIN_BUZZER_EN, HIGH); + } +#endif } bool genericBuzzer::isQuiet() { diff --git a/variants/heltec_v3/platformio.ini b/variants/heltec_v3/platformio.ini index c39e9c76..edc683d2 100644 --- a/variants/heltec_v3/platformio.ini +++ b/variants/heltec_v3/platformio.ini @@ -23,9 +23,9 @@ build_flags = -D ENV_INCLUDE_INA3221=1 -D ENV_INCLUDE_INA219=1 -D ENV_INCLUDE_GPS=1 - -D PIN_GPS_RX=45 - -D PIN_GPS_TX=46 - -D PIN_GPS_EN=-1 + -D PIN_GPS_RX=47 + -D PIN_GPS_TX=48 + -D PIN_GPS_EN=26 build_src_filter = ${esp32_base.build_src_filter} +<../variants/heltec_v3> + @@ -198,3 +198,20 @@ build_src_filter = ${Heltec_lora32_v3.build_src_filter} lib_deps = ${Heltec_lora32_v3.lib_deps} densaugeo/base64 @ ~1.4.0 + +[env:Heltec_WSL3_sensor] +extends = Heltec_lora32_v3 +build_flags = + ${Heltec_lora32_v3.build_flags} + -D ADVERT_NAME='"Heltec Sensor"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${Heltec_lora32_v3.build_src_filter} + +<../examples/simple_sensor> +lib_deps = + ${Heltec_lora32_v3.lib_deps} + ${esp32_ota.lib_deps} + diff --git a/variants/heltec_wireless_paper/platformio.ini b/variants/heltec_wireless_paper/platformio.ini index 0b9ac38e..513ba4b9 100644 --- a/variants/heltec_wireless_paper/platformio.ini +++ b/variants/heltec_wireless_paper/platformio.ini @@ -40,7 +40,7 @@ build_flags = -D MAX_CONTACTS=100 -D MAX_GROUP_CHANNELS=8 -D DISPLAY_CLASS=E213Display - -D BLE_PIN_CODE=0 ; dynamic, random PIN + -D BLE_PIN_CODE=123456 ; dynamic, random PIN -D BLE_DEBUG_LOGGING=1 -D OFFLINE_QUEUE_SIZE=256 build_src_filter = ${Heltec_Wireless_Paper_base.build_src_filter} diff --git a/variants/heltec_wireless_paper/target.cpp b/variants/heltec_wireless_paper/target.cpp index 65eaab04..d434b241 100644 --- a/variants/heltec_wireless_paper/target.cpp +++ b/variants/heltec_wireless_paper/target.cpp @@ -1,11 +1,14 @@ #include "target.h" - #include HeltecV3Board board; -static SPIClass spi; -RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi); +#if defined(P_LORA_SCLK) + static SPIClass spi; + RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi); +#else + RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); +#endif WRAPPER_CLASS radio_driver(radio, board); @@ -21,7 +24,11 @@ DISPLAY_CLASS display; bool radio_init() { fallback_clock.begin(); rtc_clock.begin(Wire); +#if defined(P_LORA_SCLK) return radio.std_init(&spi); +#else + return radio.std_init(); +#endif } uint32_t radio_get_rng_seed() { @@ -42,4 +49,4 @@ void radio_set_tx_power(uint8_t 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_wireless_paper/target.h b/variants/heltec_wireless_paper/target.h index f06eead3..7d901c01 100644 --- a/variants/heltec_wireless_paper/target.h +++ b/variants/heltec_wireless_paper/target.h @@ -2,10 +2,10 @@ #define RADIOLIB_STATIC_ONLY 1 #include -#include -#include -#include #include +#include +#include +#include #include #ifdef DISPLAY_CLASS #include diff --git a/variants/lilygo_tbeam_SX1262/platformio.ini b/variants/lilygo_tbeam_SX1262/platformio.ini index 8bf2c5ab..eac899f0 100644 --- a/variants/lilygo_tbeam_SX1262/platformio.ini +++ b/variants/lilygo_tbeam_SX1262/platformio.ini @@ -69,4 +69,21 @@ build_src_filter = ${LilyGo_TBeam_SX1262.build_src_filter} +<../examples/simple_repeater> lib_deps = ${LilyGo_TBeam_SX1262.lib_deps} - ${esp32_ota.lib_deps} \ No newline at end of file + ${esp32_ota.lib_deps} + +[env:Tbeam_SX1262_room_server] +extends = LilyGo_TBeam_SX1262 +build_flags = + ${LilyGo_TBeam_SX1262.build_flags} + -D ADVERT_NAME='"Tbeam SX1262 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_TBeam_SX1262.build_src_filter} + +<../examples/simple_room_server> +lib_deps = + ${LilyGo_TBeam_SX1262.lib_deps} + ${esp32_ota.lib_deps} diff --git a/variants/meshadventurer/target.cpp b/variants/meshadventurer/target.cpp index 43e171c0..a1d6dcad 100644 --- a/variants/meshadventurer/target.cpp +++ b/variants/meshadventurer/target.cpp @@ -5,13 +5,8 @@ MeshadventurerBoard board; -#if defined(P_LORA_SCLK) - static SPIClass spi; - RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi); -#else - RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); -#endif - +static SPIClass spi; +RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi); WRAPPER_CLASS radio_driver(radio, board); ESP32RTCClock fallback_clock; @@ -30,40 +25,12 @@ MASensorManager sensors = MASensorManager(nmea); 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); - -#if defined(SX126X_RXEN) && defined(SX126X_TXEN) - radio.setRfSwitchPins(SX126X_RXEN, SX126X_TXEN); -#endif - -#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/minewsemi_me25ls01/MinewsemiME25LS01Board.cpp b/variants/minewsemi_me25ls01/MinewsemiME25LS01Board.cpp new file mode 100644 index 00000000..c41a6bc0 --- /dev/null +++ b/variants/minewsemi_me25ls01/MinewsemiME25LS01Board.cpp @@ -0,0 +1,91 @@ +#include +#include "MinewsemiME25LS01Board.h" +#include + +#include + +void MinewsemiME25LS01Board::begin() { + // for future use, sub-classes SHOULD call this from their begin() + startup_reason = BD_STARTUP_NORMAL; + btn_prev_state = HIGH; + + pinMode(PIN_VBAT_READ, INPUT); + + sd_power_mode_set(NRF_POWER_MODE_LOWPWR); + +#ifdef BUTTON_PIN + pinMode(BUTTON_PIN, INPUT); + pinMode(LED_PIN, OUTPUT); +#endif + +#if defined(PIN_BOARD_SDA) && defined(PIN_BOARD_SCL) + Wire.setPins(PIN_BOARD_SDA, PIN_BOARD_SCL); +#endif + + Wire.begin(); + +#ifdef P_LORA_TX_LED + pinMode(P_LORA_TX_LED, OUTPUT); + digitalWrite(P_LORA_TX_LED, LOW); +#endif + + delay(10); // give sx1262 some time to power up +} + +static BLEDfu bledfu; + +static void connect_callback(uint16_t conn_handle) { + (void)conn_handle; + MESH_DEBUG_PRINTLN("BLE client connected"); +} + +static void disconnect_callback(uint16_t conn_handle, uint8_t reason) { + (void)conn_handle; + (void)reason; + + MESH_DEBUG_PRINTLN("BLE client disconnected"); +} + + +bool MinewsemiME25LS01Board::startOTAUpdate(const char* id, char reply[]) { + // Config the peripheral connection with maximum bandwidth + // more SRAM required by SoftDevice + // Note: All config***() function must be called before begin() + Bluefruit.configPrphBandwidth(BANDWIDTH_MAX); + Bluefruit.configPrphConn(92, BLE_GAP_EVENT_LENGTH_MIN, 16, 16); + + Bluefruit.begin(1, 0); + // Set max power. Accepted values are: -40, -30, -20, -16, -12, -8, -4, 0, 4 + Bluefruit.setTxPower(4); + // Set the BLE device name + Bluefruit.setName("Minewsemi_OTA"); + + Bluefruit.Periph.setConnectCallback(connect_callback); + Bluefruit.Periph.setDisconnectCallback(disconnect_callback); + + // To be consistent OTA DFU should be added first if it exists + bledfu.begin(); + + // Set up and start advertising + // Advertising packet + Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE); + Bluefruit.Advertising.addTxPower(); + Bluefruit.Advertising.addName(); + + /* Start Advertising + - Enable auto advertising if disconnected + - Interval: fast mode = 20 ms, slow mode = 152.5 ms + - Timeout for fast mode is 30 seconds + - Start(timeout) with timeout = 0 will advertise forever (until connected) + + For recommended advertising interval + https://developer.apple.com/library/content/qa/qa1931/_index.html + */ + Bluefruit.Advertising.restartOnDisconnect(true); + Bluefruit.Advertising.setInterval(32, 244); // in unit of 0.625 ms + Bluefruit.Advertising.setFastTimeout(30); // number of seconds in fast mode + Bluefruit.Advertising.start(0); // 0 = Don't stop advertising after n seconds + + strcpy(reply, "OK - started"); + return true; +} \ No newline at end of file diff --git a/variants/minewsemi_me25ls01/MinewsemiME25LS01Board.h b/variants/minewsemi_me25ls01/MinewsemiME25LS01Board.h new file mode 100644 index 00000000..777606a6 --- /dev/null +++ b/variants/minewsemi_me25ls01/MinewsemiME25LS01Board.h @@ -0,0 +1,88 @@ +#pragma once + +#include +#include + +// LoRa and SPI pins + +#define P_LORA_DIO_1 (32 + 12) // P1.12 +#define P_LORA_NSS (32 + 13) // P1.13 +#define P_LORA_RESET (32 + 11) // P1.11 +#define P_LORA_BUSY (32 + 10) // P1.10 +#define P_LORA_SCLK (32 + 15) // P1.15 +#define P_LORA_MISO (0 + 29) // P0.29 +#define P_LORA_MOSI (0 + 2) // P0.2 + +#define LR11X0_DIO_AS_RF_SWITCH true +#define LR11X0_DIO3_TCXO_VOLTAGE 1.6 + +#define PIN_VBAT_READ BATTERY_PIN +#define ADC_MULTIPLIER (1.815f) // dependent on voltage divider resistors. TODO: more accurate battery tracking + + +class MinewsemiME25LS01Board : public mesh::MainBoard { +protected: + uint8_t startup_reason; + uint8_t btn_prev_state; + +public: + void begin(); + +#define BATTERY_SAMPLES 8 + + uint16_t getBattMilliVolts() override { + 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); + } + + uint8_t getStartupReason() const override { return startup_reason; } + + const char* getManufacturerName() const override { + return "Minewsemi"; + } + + void powerOff() override { + #ifdef HAS_GPS + digitalWrite(GPS_VRTC_EN, LOW); + digitalWrite(GPS_RESET, LOW); + digitalWrite(GPS_SLEEP_INT, LOW); + digitalWrite(GPS_RTC_INT, LOW); + pinMode(GPS_RESETB, OUTPUT); + digitalWrite(GPS_RESETB, LOW); + #endif + + #ifdef BUZZER_EN + digitalWrite(BUZZER_EN, LOW); + #endif + + #ifdef LED_PIN + digitalWrite(LED_PIN, LOW); + #endif + #ifdef BUTTON_PIN + nrf_gpio_cfg_sense_input(digitalPinToInterrupt(BUTTON_PIN), NRF_GPIO_PIN_PULLUP, NRF_GPIO_PIN_SENSE_HIGH); + #endif + sd_power_system_off(); + } + + #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 + + + void reboot() override { + NVIC_SystemReset(); + } + + bool startOTAUpdate(const char* id, char reply[]) override; +}; \ No newline at end of file diff --git a/variants/minewsemi_me25ls01/NullDisplayDriver.h b/variants/minewsemi_me25ls01/NullDisplayDriver.h new file mode 100644 index 00000000..38bf93f1 --- /dev/null +++ b/variants/minewsemi_me25ls01/NullDisplayDriver.h @@ -0,0 +1,24 @@ +#pragma once + +#include + +class NullDisplayDriver : public DisplayDriver { +public: + NullDisplayDriver() : DisplayDriver(128, 64) { } + bool begin() { return false; } // not present + + bool isOn() override { return false; } + void turnOn() override { } + void turnOff() override { } + void clear() override { } + void startFrame(Color bkg = DARK) override { } + void setTextSize(int sz) override { } + void setColor(Color c) override { } + void setCursor(int x, int y) override { } + void print(const char* str) override { } + void fillRect(int x, int y, int w, int h) override { } + void drawRect(int x, int y, int w, int h) override { } + void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override { } + uint16_t getTextWidth(const char* str) override { return 0; } + void endFrame() { } +}; diff --git a/variants/minewsemi_me25ls01/platformio.ini b/variants/minewsemi_me25ls01/platformio.ini new file mode 100644 index 00000000..f7265af4 --- /dev/null +++ b/variants/minewsemi_me25ls01/platformio.ini @@ -0,0 +1,164 @@ +; ----------------- NRF52 me25ls01--------------------- +[nrf52840_me25ls01] +extends = nrf52_base +platform_packages = framework-arduinoadafruitnrf52 +build_flags = ${nrf52_base.build_flags} + -I src/helpers/nrf52 + -I lib/nrf52/s140_nrf52_7.3.0_API/include + -I lib/nrf52/s140_nrf52_7.3.0_API/include/nrf52 +lib_ignore = + BluetoothOTA + lib5b4 +lib_deps = + ${nrf52_base.lib_deps} + rweather/Crypto @ ^0.4.0 + +[me25ls01] +extends = nrf52840_me25ls01 +board = minewsemi_me25ls01 +board_build.ldscript = boards/nrf52840_s140_v7.ld +build_flags = ${nrf52840_me25ls01.build_flags} + -I variants/minewsemi_me25ls01 + -D me25ls01 + -D PIN_USER_BTN=27 + -D USER_BTN_PRESSED=HIGH + -D PIN_STATUS_LED=39 + -D P_LORA_TX_LED=22 + -D RADIO_CLASS=CustomLR1110 + -D WRAPPER_CLASS=CustomLR1110Wrapper + -D LORA_TX_POWER=22 + -D ENV_INCLUDE_GPS=0 + -D ENV_INCLUDE_AHTX0=1 + -D ENV_INCLUDE_BME280=1 + -D ENV_INCLUDE_INA3221=1 + -D ENV_INCLUDE_INA219=1 +build_src_filter = ${nrf52840_me25ls01.build_src_filter} + + + +<../variants/minewsemi_me25ls01> + + +debug_tool = jlink +upload_protocol = nrfutil +lib_deps = ${nrf52840_me25ls01.lib_deps} + densaugeo/base64 @ ~1.4.0 + stevemarple/MicroNMEA @ ^2.0.6 + end2endzone/NonBlockingRTTTL@^1.3.0 + adafruit/Adafruit SSD1306 @ ^2.5.13 + adafruit/Adafruit INA3221 Library @ ^1.0.1 + adafruit/Adafruit INA219 @ ^1.2.3 + adafruit/Adafruit AHTX0 @ ^2.0.5 + adafruit/Adafruit BME280 Library @ ^2.3.0 + +[env:Minewsemi_me25ls01_companion_radio_ble] +extends = me25ls01 +build_flags = ${me25ls01.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 + -D OFFLINE_QUEUE_SIZE=256 + -D RX_BOOSTED_GAIN=true + -D RF_SWITCH_TABLE + -D DISPLAY_CLASS=NullDisplayDriver + ;-D PIN_BUZZER=25 + ;-D PIN_BUZZER_EN=37 +build_src_filter = ${me25ls01.build_src_filter} + + + +<../examples/companion_radio/*.cpp> +lib_deps = ${me25ls01.lib_deps} + adafruit/RTClib @ ^2.1.3 + + +[env:Minewsemi_me25ls01_repeater] +extends = me25ls01 +build_flags = ${me25ls01.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 + -D OFFLINE_QUEUE_SIZE=256 + -D RX_BOOSTED_GAIN=true + -D RF_SWITCH_TABLE + -D ADVERT_NAME='"ME25LS01 Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=8 + -D DISPLAY_CLASS=NullDisplayDriver +build_src_filter = ${me25ls01.build_src_filter} + +<../examples/simple_repeater> +lib_deps = ${me25ls01.lib_deps} + adafruit/RTClib @ ^2.1.3 + + + +[env:Minewsemi_me25ls01_room_server] +extends = me25ls01 +build_flags = ${me25ls01.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 + -D OFFLINE_QUEUE_SIZE=256 + -D RX_BOOSTED_GAIN=true + -D RF_SWITCH_TABLE + -D ADVERT_NAME='"ME25LS01 Room"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' + -D MAX_NEIGHBOURS=8 + -D DISPLAY_CLASS=NullDisplayDriver +build_src_filter = ${me25ls01.build_src_filter} + +<../examples/simple_room_server> +lib_deps = ${me25ls01.lib_deps} + adafruit/RTClib @ ^2.1.3 + +[env:Minewsemi_me25ls01_terminal_chat] +extends = me25ls01 +build_flags = ${me25ls01.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 + -D OFFLINE_QUEUE_SIZE=256 + -D RX_BOOSTED_GAIN=true + -D RF_SWITCH_TABLE + -D ADVERT_NAME='"ME25LS01 Chat"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' + -D MAX_NEIGHBOURS=8 + -D DISPLAY_CLASS=NullDisplayDriver +build_src_filter = ${me25ls01.build_src_filter} + +<../examples/simple_secure_chat/main.cpp> +lib_deps = ${me25ls01.lib_deps} + adafruit/RTClib @ ^2.1.3 + +[env:Minewsemi_me25ls01_companion_radio_usb] +extends = me25ls01 +build_flags = ${me25ls01.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 + -D OFFLINE_QUEUE_SIZE=256 + -D RX_BOOSTED_GAIN=true + -D RF_SWITCH_TABLE + -D DISPLAY_CLASS=NullDisplayDriver +build_src_filter = ${me25ls01.build_src_filter} + + + +<../examples/companion_radio> +lib_deps = ${me25ls01.lib_deps} + adafruit/RTClib @ ^2.1.3 + diff --git a/variants/minewsemi_me25ls01/target.cpp b/variants/minewsemi_me25ls01/target.cpp new file mode 100644 index 00000000..13306762 --- /dev/null +++ b/variants/minewsemi_me25ls01/target.cpp @@ -0,0 +1,98 @@ +#include +#include "target.h" + +MinewsemiME25LS01Board board; + +RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); + +WRAPPER_CLASS radio_driver(radio, board); + +VolatileRTCClock rtc_clock; +extern EnvironmentSensorManager sensors; +#if ENV_INCLUDE_GPS + #include + MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock); + EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea); +#else + EnvironmentSensorManager sensors; +#endif + +#ifdef DISPLAY_CLASS + NullDisplayDriver display; +#endif + +#ifndef LORA_CR + #define LORA_CR 5 +#endif + +#ifdef RF_SWITCH_TABLE +static const uint32_t rfswitch_dios[Module::RFSWITCH_MAX_PINS] = { + RADIOLIB_LR11X0_DIO5, + RADIOLIB_LR11X0_DIO6, + RADIOLIB_LR11X0_DIO7, + RADIOLIB_LR11X0_DIO8, + RADIOLIB_NC +}; + +static const Module::RfSwitchMode_t rfswitch_table[] = { + // mode DIO5 DIO6 DIO7 DIO8 + { LR11x0::MODE_STBY, {LOW, LOW, LOW, LOW }}, + { LR11x0::MODE_RX, {HIGH, LOW, LOW, HIGH }}, + { LR11x0::MODE_TX, {HIGH, HIGH, LOW, HIGH }}, + { LR11x0::MODE_TX_HP, {LOW, HIGH, LOW, HIGH }}, + { LR11x0::MODE_TX_HF, {LOW, LOW, LOW, LOW }}, + { LR11x0::MODE_GNSS, {LOW, LOW, HIGH, LOW }}, + { LR11x0::MODE_WIFI, {LOW, LOW, LOW, LOW }}, + END_OF_MODE_TABLE, +}; +#endif + +bool radio_init() { + //rtc_clock.begin(Wire); + +#ifdef LR11X0_DIO3_TCXO_VOLTAGE + float tcxo = LR11X0_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_LR11X0_LORA_SYNC_WORD_PRIVATE, LORA_TX_POWER, 16, tcxo); + if (status != RADIOLIB_ERR_NONE) { + Serial.print("ERROR: radio init failed: "); + Serial.println(status); + return false; // fail + } + + radio.setCRC(1); + +#ifdef RF_SWITCH_TABLE + radio.setRfSwitchTable(rfswitch_dios, rfswitch_table); +#endif +#ifdef RX_BOOSTED_GAIN + radio.setRxBoostedGainMode(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/minewsemi_me25ls01/target.h b/variants/minewsemi_me25ls01/target.h new file mode 100644 index 00000000..db832f91 --- /dev/null +++ b/variants/minewsemi_me25ls01/target.h @@ -0,0 +1,29 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef DISPLAY_CLASS + #include "NullDisplayDriver.h" +#endif + +#ifdef DISPLAY_CLASS + extern NullDisplayDriver display; +#endif + +extern MinewsemiME25LS01Board board; +extern WRAPPER_CLASS radio_driver; +extern VolatileRTCClock rtc_clock; +extern EnvironmentSensorManager 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/minewsemi_me25ls01/variant.cpp b/variants/minewsemi_me25ls01/variant.cpp new file mode 100644 index 00000000..5dbac9d3 --- /dev/null +++ b/variants/minewsemi_me25ls01/variant.cpp @@ -0,0 +1,70 @@ +#include "variant.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[PINS_COUNT + 1] = +{ + 0, // P0.00 + 1, // P0.01 + 2, // P0.02 + 3, // P0.03 + 4, // P0.04 + 5, // P0.05 + 6, // P0.06 + 7, // P0.07 + 8, // P0.08 + 9, // P0.09 + 10, // P0.10 + 11, // P0.11 + 12, // P0.12 + 13, // P0.13, PIN_SERIAL1_TX + 14, // P0.14, PIN_SERIAL1_RX + 15, // P0.15, PIN_SERIAL2_RX + 16, // P0.16, PIN_WIRE_SCL + 17, // P0.17, PIN_SERIAL2_TX + 18, // P0.18 + 19, // P0.19 + 20, // P0.20 + 21, // P0.21, PIN_WIRE_SDA + 22, // P0.22 + 23, // P0.23 + 24, // P0.24, + 25, // P0.25, + 26, // P0.26, + 27, // P0.27, + 28, // P0.28 + 29, // P0.29, + 30, // P0.30 + 31, // P0.31, BATTERY_PIN + 32, // P1.00 + 33, // P1.01, LORA_DIO_1 + 34, // P1.02 + 35, // P1.03, + 36, // P1.04 + 37, // P1.05, LR1110_EN + 38, // P1.06, + 39, // P1.07, + 40, // P1.08, PIN_SPI_MISO + 41, // P1.09, PIN_SPI_MOSI + 42, // P1.10, LORA_RESET + 43, // P1.11, GPS_EN + 44, // P1.12, GPS_SLEEP_INT + 45, // P1.13 + 46, // P1.14, GPS_RESETB + 47, // P1.15, PIN_GPS_RESET + 255, // NRFX_SPIM_PIN_NOT_USED +}; + +void initVariant() +{ + pinMode(BATTERY_PIN, INPUT); + pinMode(PIN_BUTTON1, INPUT); + + // pinMode(PIN_3V3_EN, OUTPUT); + // pinMode(PIN_3V3_ACC_EN, OUTPUT); + pinMode(LED_PIN, OUTPUT); + pinMode(P_LORA_TX_LED, OUTPUT); + + digitalWrite(LED_PIN, HIGH); + digitalWrite(P_LORA_TX_LED, LOW); +} diff --git a/variants/minewsemi_me25ls01/variant.h b/variants/minewsemi_me25ls01/variant.h new file mode 100644 index 00000000..a8bbbe3a --- /dev/null +++ b/variants/minewsemi_me25ls01/variant.h @@ -0,0 +1,94 @@ +#pragma once + +#include "WVariant.h" + +// Low frequency clock source +#define USE_LFXO // 32.768 kHz crystal oscillator +#define VARIANT_MCK (64000000ul) +// #define USE_LFRC // 32.768 kHz RC oscillator + +// Power +#define BATTERY_PIN (31) +#define BATTERY_IMMUTABLE +#define ADC_MULTIPLIER (2.0F) + +#define ADC_RESOLUTION (14) +#define BATTERY_SENSE_RES (12) + +// Number of pins +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (6) +#define NUM_ANALOG_OUTPUTS (0) + +// UART pin definition +#define PIN_SERIAL1_RX (14) // P0.14 +#define PIN_SERIAL1_TX (13) // P0.13 + +#define PIN_SERIAL2_RX (15) // P0.15 +#define PIN_SERIAL2_TX (17) // P0.17 + +// I2C pin definition +#define HAS_WIRE (1) +#define WIRE_INTERFACES_COUNT (1) + +#define PIN_WIRE_SDA (21) // P0.21 +#define PIN_WIRE_SCL (16) // P0.16 +#define I2C_NO_RESCAN + +// SPI pin definition +#define SPI_INTERFACES_COUNT (1) + +#define PIN_SPI_MISO (0 + 29) // P0.29 +#define PIN_SPI_MOSI (0 + 2) // P0.2 +#define PIN_SPI_SCK (32 + 15) // P1.15 +#define PIN_SPI_NSS (32 + 13) // P1.13 + +// Builtin LEDs +#define LED_BUILTIN (-1) +#define LED_RED (32 + 5) // P1.5 +#define LED_BLUE (32 + 7) // P1.7 +#define LED_PIN LED_BLUE +#define P_LORA_TX_LED LED_RED + +#define LED_STATE_ON HIGH + +// Builtin buttons + +#define PIN_BUTTON1 (0 + 27) // P0.6 +#define BUTTON_PIN PIN_BUTTON1 + +// LR1110 +#define LORA_DIO_1 (32 + 12) // P1.12 +#define LORA_DIO_2 (32 + 10) // P1.10 +#define LORA_NSS (PIN_SPI_NSS) // P1.13 +#define LORA_RESET (32 + 11) // P1.11 +#define LORA_BUSY (32 + 10) // P1.10 +#define LORA_SCLK (PIN_SPI_SCK) // P1.15 +#define LORA_MISO (PIN_SPI_MISO) // P0.29 +#define LORA_MOSI (PIN_SPI_MOSI) // P0.2 +#define LORA_CS PIN_SPI_NSS // P1.13 + +#define LR11X0_DIO_AS_RF_SWITCH true +#define LR11X0_DIO3_TCXO_VOLTAGE 1.6 + +#define LR1110_IRQ_PIN LORA_DIO_1 +#define LR1110_NRESET_PIN LORA_RESET +#define LR1110_BUSY_PIN LORA_DIO_2 +#define LR1110_SPI_NSS_PIN LORA_CS +#define LR1110_SPI_SCK_PIN LORA_SCLK +#define LR1110_SPI_MOSI_PIN LORA_MOSI +#define LR1110_SPI_MISO_PIN LORA_MISO + +// GPS +#define HAS_GPS 0 +#define GPS_RX_PIN PIN_SERIAL1_RX +#define GPS_TX_PIN PIN_SERIAL1_TX + +#define GPS_EN (-1) // P1.11 +#define GPS_RESET (-1) // P1.15 + +#define GPS_VRTC_EN (-1) // P0.8 +#define GPS_SLEEP_INT (-1) // P1.12 +#define GPS_RTC_INT (-1) // P0.15 +#define GPS_RESETB (-1) // P1.14 diff --git a/src/helpers/nrf52/PromicroBoard.cpp b/variants/promicro/PromicroBoard.cpp similarity index 100% rename from src/helpers/nrf52/PromicroBoard.cpp rename to variants/promicro/PromicroBoard.cpp diff --git a/src/helpers/nrf52/PromicroBoard.h b/variants/promicro/PromicroBoard.h similarity index 100% rename from src/helpers/nrf52/PromicroBoard.h rename to variants/promicro/PromicroBoard.h diff --git a/variants/promicro/platformio.ini b/variants/promicro/platformio.ini index 246019c2..4e5edc22 100644 --- a/variants/promicro/platformio.ini +++ b/variants/promicro/platformio.ini @@ -23,7 +23,6 @@ build_flags = ${nrf52_base.build_flags} -D ENV_INCLUDE_INA3221=1 -D ENV_INCLUDE_INA219=1 build_src_filter = ${nrf52_base.build_src_filter} - + + +<../variants/promicro> lib_deps= ${nrf52_base.lib_deps} @@ -130,7 +129,6 @@ build_flags = ${nrf52_base.build_flags} -D SX126X_RX_BOOSTED_GAIN=1 build_src_filter = ${nrf52_base.build_src_filter} - + + +<../variants/promicro> lib_deps= ${nrf52_base.lib_deps} diff --git a/variants/promicro/target.h b/variants/promicro/target.h index c634d18a..7646d459 100644 --- a/variants/promicro/target.h +++ b/variants/promicro/target.h @@ -3,7 +3,7 @@ #define RADIOLIB_STATIC_ONLY 1 #include #include -#include +#include #include #include #include diff --git a/variants/rak3x72/target.cpp b/variants/rak3x72/target.cpp index d7070eae..446783aa 100644 --- a/variants/rak3x72/target.cpp +++ b/variants/rak3x72/target.cpp @@ -38,7 +38,7 @@ bool radio_init() { radio.setRfSwitchTable(rfswitch_pins, rfswitch_table); - int status = radio.begin(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, LORA_TX_POWER, 8, STM32WL_TCXO_VOLTAGE, 0); + int status = radio.begin(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, LORA_TX_POWER, 16, STM32WL_TCXO_VOLTAGE, 0); if (status != RADIOLIB_ERR_NONE) { Serial.print("ERROR: radio init failed: "); diff --git a/src/helpers/nrf52/RAK4631Board.cpp b/variants/rak4631/RAK4631Board.cpp similarity index 100% rename from src/helpers/nrf52/RAK4631Board.cpp rename to variants/rak4631/RAK4631Board.cpp diff --git a/src/helpers/nrf52/RAK4631Board.h b/variants/rak4631/RAK4631Board.h similarity index 100% rename from src/helpers/nrf52/RAK4631Board.h rename to variants/rak4631/RAK4631Board.h diff --git a/variants/rak4631/platformio.ini b/variants/rak4631/platformio.ini index 9391f99b..d4f8028b 100644 --- a/variants/rak4631/platformio.ini +++ b/variants/rak4631/platformio.ini @@ -1,13 +1,11 @@ [rak4631] -extends = nrf52840_base +extends = nrf52_base platform = https://github.com/maxgerhardt/platform-nordicnrf52.git#rak board = wiscore_rak4631 board_check = true -build_flags = ${nrf52840_base.build_flags} +build_flags = ${nrf52_base.build_flags} -I variants/rak4631 -D RAK_4631 - -D PIN_USER_BTN=9 - -D PIN_USER_BTN_ANA=31 -D PIN_BOARD_SCL=14 -D PIN_BOARD_SDA=13 -D PIN_OLED_RESET=-1 @@ -16,11 +14,10 @@ build_flags = ${nrf52840_base.build_flags} -D LORA_TX_POWER=22 -D SX126X_CURRENT_LIMIT=140 -D SX126X_RX_BOOSTED_GAIN=1 -build_src_filter = ${nrf52840_base.build_src_filter} - + +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/rak4631> lib_deps = - ${nrf52840_base.lib_deps} + ${nrf52_base.lib_deps} adafruit/Adafruit SSD1306 @ ^2.5.13 stevemarple/MicroNMEA @ ^2.0.6 @@ -58,7 +55,7 @@ build_flags = build_src_filter = ${rak4631.build_src_filter} + +<../examples/simple_repeater> -lib_deps = +lib_deps = ${rak4631.lib_deps} sparkfun/SparkFun u-blox GNSS Arduino Library @ ^2.2.27 https://github.com/boschsensortec/Bosch-BSEC2-Library @@ -84,6 +81,8 @@ build_src_filter = ${rak4631.build_src_filter} extends = rak4631 build_flags = ${rak4631.build_flags} + -D PIN_USER_BTN=9 + -D PIN_USER_BTN_ANA=31 -D DISPLAY_CLASS=SSD1306Display -D MAX_CONTACTS=100 -D MAX_GROUP_CHANNELS=8 @@ -100,6 +99,8 @@ lib_deps = extends = rak4631 build_flags = ${rak4631.build_flags} + -D PIN_USER_BTN=9 + -D PIN_USER_BTN_ANA=31 -D DISPLAY_CLASS=SSD1306Display -D MAX_CONTACTS=100 -D MAX_GROUP_CHANNELS=8 @@ -120,6 +121,8 @@ lib_deps = extends = rak4631 build_flags = ${rak4631.build_flags} + -D PIN_USER_BTN=9 + -D PIN_USER_BTN_ANA=31 -D DISPLAY_CLASS=SSD1306Display -D MAX_CONTACTS=100 -D MAX_GROUP_CHANNELS=8 @@ -145,6 +148,8 @@ lib_deps = extends = rak4631 build_flags = ${rak4631.build_flags} + -D PIN_USER_BTN=9 + -D PIN_USER_BTN_ANA=31 -D MAX_CONTACTS=100 -D MAX_GROUP_CHANNELS=1 ; -D MESH_PACKET_LOGGING=1 diff --git a/variants/rak4631/target.h b/variants/rak4631/target.h index 3f26ab33..e1545581 100644 --- a/variants/rak4631/target.h +++ b/variants/rak4631/target.h @@ -3,7 +3,7 @@ #define RADIOLIB_STATIC_ONLY 1 #include #include -#include +#include #include #include #include diff --git a/variants/sensecap_solar/SenseCapSolarBoard.cpp b/variants/sensecap_solar/SenseCapSolarBoard.cpp new file mode 100644 index 00000000..d6c044d1 --- /dev/null +++ b/variants/sensecap_solar/SenseCapSolarBoard.cpp @@ -0,0 +1,81 @@ +#include +#include "SenseCapSolarBoard.h" + +#include +#include + +static BLEDfu bledfu; + +static void connect_callback(uint16_t conn_handle) { + (void)conn_handle; + MESH_DEBUG_PRINTLN("BLE client connected"); +} + +static void disconnect_callback(uint16_t conn_handle, uint8_t reason) { + (void)conn_handle; + (void)reason; + + MESH_DEBUG_PRINTLN("BLE client disconnected"); +} + +void SenseCapSolarBoard::begin() { + // for future use, sub-classes SHOULD call this from their begin() + startup_reason = BD_STARTUP_NORMAL; + +#if defined(PIN_WIRE_SDA) && defined(PIN_WIRE_SCL) + Wire.setPins(PIN_WIRE_SDA, PIN_WIRE_SCL); +#endif + + Wire.begin(); + +#ifdef P_LORA_TX_LED + pinMode(P_LORA_TX_LED, OUTPUT); + digitalWrite(P_LORA_TX_LED, LOW); +#endif + + delay(10); // give sx1262 some time to power up +} + +bool SenseCapSolarBoard::startOTAUpdate(const char* id, char reply[]) { + // Config the peripheral connection with maximum bandwidth + // more SRAM required by SoftDevice + // Note: All config***() function must be called before begin() + Bluefruit.configPrphBandwidth(BANDWIDTH_MAX); + Bluefruit.configPrphConn(92, BLE_GAP_EVENT_LENGTH_MIN, 16, 16); + + Bluefruit.begin(1, 0); + // Set max power. Accepted values are: -40, -30, -20, -16, -12, -8, -4, 0, 4 + Bluefruit.setTxPower(4); + // Set the BLE device name + Bluefruit.setName("SENSECAP_SOLAR_OTA"); + + Bluefruit.Periph.setConnectCallback(connect_callback); + Bluefruit.Periph.setDisconnectCallback(disconnect_callback); + + // To be consistent OTA DFU should be added first if it exists + bledfu.begin(); + + // Set up and start advertising + // Advertising packet + Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE); + Bluefruit.Advertising.addTxPower(); + Bluefruit.Advertising.addName(); + + /* Start Advertising + - Enable auto advertising if disconnected + - Interval: fast mode = 20 ms, slow mode = 152.5 ms + - Timeout for fast mode is 30 seconds + - Start(timeout) with timeout = 0 will advertise forever (until connected) + + For recommended advertising interval + https://developer.apple.com/library/content/qa/qa1931/_index.html + */ + Bluefruit.Advertising.restartOnDisconnect(true); + Bluefruit.Advertising.setInterval(32, 244); // in unit of 0.625 ms + Bluefruit.Advertising.setFastTimeout(30); // number of seconds in fast mode + Bluefruit.Advertising.start(0); // 0 = Don't stop advertising after n seconds + + strcpy(reply, "OK - started"); + + return true; +} diff --git a/variants/sensecap_solar/SenseCapSolarBoard.h b/variants/sensecap_solar/SenseCapSolarBoard.h new file mode 100644 index 00000000..b1e5f8f1 --- /dev/null +++ b/variants/sensecap_solar/SenseCapSolarBoard.h @@ -0,0 +1,42 @@ +#pragma once + +#include +#include + +class SenseCapSolarBoard : public mesh::MainBoard { +protected: + uint8_t startup_reason; + +public: + void begin(); + uint8_t getStartupReason() const override { 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 + + uint16_t getBattMilliVolts() override { + digitalWrite(VBAT_ENABLE, LOW); + int adcvalue = 0; + analogReadResolution(12); + analogReference(AR_INTERNAL_3_0); + delay(10); + adcvalue = analogRead(BATTERY_PIN); + return (adcvalue * ADC_MULTIPLIER * AREF_VOLTAGE) / 4.096; + } + + const char* getManufacturerName() const override { + return "Seeed SenseCap Solar"; + } + + void reboot() override { + NVIC_SystemReset(); + } + + bool startOTAUpdate(const char* id, char reply[]) override; +}; diff --git a/variants/sensecap_solar/platformio.ini b/variants/sensecap_solar/platformio.ini new file mode 100644 index 00000000..281f6688 --- /dev/null +++ b/variants/sensecap_solar/platformio.ini @@ -0,0 +1,75 @@ +[SenseCap_Solar] +extends = nrf52_base +board = seeed_sensecap_solar +board_build.ldscript = boards/nrf52840_s140_v7.ld +build_flags = ${nrf52_base.build_flags} + -I lib/nrf52/s140_nrf52_7.3.0_API/include + -I lib/nrf52/s140_nrf52_7.3.0_API/include/nrf52 + -I variants/sensecap_solar + -I src/helpers/nrf52 + -D NRF52_PLATFORM=1 + -D RADIO_CLASS=CustomSX1262 + -D WRAPPER_CLASS=CustomSX1262Wrapper + -D P_LORA_TX_LED=12 + -D P_LORA_DIO_1=1 + -D P_LORA_RESET=2 + -D P_LORA_BUSY=3 + -D P_LORA_NSS=4 + -D LORA_TX_POWER=22 + -D SX126X_RXEN=5 + -D SX126X_TXEN=RADIOLIB_NC + -D SX126X_DIO2_AS_RF_SWITCH=1 + -D SX126X_DIO3_TCXO_VOLTAGE=1.8 + -D SX126X_CURRENT_LIMIT=140 + -D SX126X_RX_BOOSTED_GAIN=1 + -D ENV_INCLUDE_AHTX0=1 + -D ENV_INCLUDE_BME280=1 + -D ENV_INCLUDE_BMP280=1 + -D ENV_INCLUDE_SHTC3=1 + -D ENV_INCLUDE_LPS22HB=1 + -D ENV_INCLUDE_INA3221=1 + -D ENV_INCLUDE_INA219=1 +build_src_filter = ${nrf52_base.build_src_filter} + + + + + + + +<../variants/SenseCap_Solar> +debug_tool = jlink +upload_protocol = nrfutil +lib_deps = + ${nrf52_base.lib_deps} + rweather/Crypto @ ^0.4.0 + adafruit/Adafruit INA3221 Library @ ^1.0.1 + adafruit/Adafruit INA219 @ ^1.2.3 + adafruit/Adafruit AHTX0 @ ^2.0.5 + adafruit/Adafruit BME280 Library @ ^2.3.0 + adafruit/Adafruit BMP280 Library @ ^2.6.8 + adafruit/Adafruit SHTC3 Library @ ^1.0.1 + arduino-libraries/Arduino_LPS22HB @ ^1.0.2 + +[env:SenseCap_Solar_repeater] +extends = SenseCap_Solar +build_flags = + ${SenseCap_Solar.build_flags} + -D ADVERT_NAME='"SenseCap_Solar 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 = ${SenseCap_Solar.build_src_filter} + +<../examples/simple_repeater/main.cpp> + +[env:SenseCap_Solar_room_server] +extends = SenseCap_Solar +build_flags = + ${SenseCap_Solar.build_flags} + -D ADVERT_NAME='"SenseCap_Solar Room"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${SenseCap_Solar.build_src_filter} + +<../examples/simple_room_server/main.cpp> \ No newline at end of file diff --git a/variants/sensecap_solar/target.cpp b/variants/sensecap_solar/target.cpp new file mode 100644 index 00000000..6bd7d31a --- /dev/null +++ b/variants/sensecap_solar/target.cpp @@ -0,0 +1,39 @@ +#include +#include "target.h" +#include + +SenseCapSolarBoard board; + +RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); + +WRAPPER_CLASS radio_driver(radio, board); + +VolatileRTCClock fallback_clock; +AutoDiscoverRTCClock rtc_clock(fallback_clock); +EnvironmentSensorManager sensors; + +bool radio_init() { + rtc_clock.begin(Wire); + + return radio.std_init(&SPI); +} + +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/sensecap_solar/target.h b/variants/sensecap_solar/target.h new file mode 100644 index 00000000..63506951 --- /dev/null +++ b/variants/sensecap_solar/target.h @@ -0,0 +1,21 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include +#include +#include +#include +#include + +extern SenseCapSolarBoard board; +extern WRAPPER_CLASS radio_driver; +extern AutoDiscoverRTCClock rtc_clock; +extern EnvironmentSensorManager 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/sensecap_solar/variant.cpp b/variants/sensecap_solar/variant.cpp new file mode 100644 index 00000000..05774c10 --- /dev/null +++ b/variants/sensecap_solar/variant.cpp @@ -0,0 +1,71 @@ +#include "variant.h" +#include "wiring_constants.h" +#include "wiring_digital.h" +#include "nrf.h" + +const uint32_t g_ADigitalPinMap[] = { + // D0 .. D10 - Peripheral control pins + 2, // D0 P0.02 (A0) GNSS_WAKEUP + 3, // D1 P0.03 (A1) LORA_DIO1 + 28, // D2 P0.28 (A2) LORA_RESET + 29, // D3 P0.29 (A3) LORA_BUSY + 4, // D4 P0.04 (A4/SDA) LORA_CS + 5, // D5 P0.05 (A5/SCL) LORA_SW + 43, // D6 P1.11 (UART_TX) GNSS_TX + 44, // D7 P1.12 (UART_RX) GNSS_RX + 45, // D8 P1.13 (SPI_SCK) LORA_SCK + 46, // D9 P1.14 (SPI_MISO) LORA_MISO + 47, // D10 P1.15 (SPI_MOSI) LORA_MOSI + + // D11-D12 - LED outputs + 15, // D11 P0.15 User LED + 19, // D12 P0.19 Breathing LED + + // D13 - User input + 33, // D13 P1.01 User Button + + // D14-D15 - Grove/NFC interface + 9, // D14 P0.09 NFC1/GROVE_D1 + 10, // D15 P0.10 NFC2/GROVE_D0 + + // D16 - Power management + // 31, // D16 P0.31 VBAT_ADC (Battery voltage) + 31, // D16 P0.31 VBAT_ADC (Battery voltage) + // D17 - GNSS control + 35, // D17 P1.03 GNSS_RESET + + 37, // D18 P1.05 GNSS_ENABLE + 14, // D19 P0.14 BAT_READ + 39, // D20 P1.07 USER_BUTTON + + // + 21, // D21 P0.21 (QSPI_SCK) + 25, // D22 P0.25 (QSPI_CSN) + 20, // D23 P0.20 (QSPI_SIO_0 DI) + 24, // D24 P0.24 (QSPI_SIO_1 DO) + 22, // D25 P0.22 (QSPI_SIO_2 WP) + 23, // D26 P0.23 (QSPI_SIO_3 HOLD) +}; + +void initVariant() { + pinMode(GPS_EN, OUTPUT); + digitalWrite(GPS_EN, LOW); + + pinMode(BATTERY_PIN, INPUT); + pinMode(VBAT_ENABLE, OUTPUT); + digitalWrite(VBAT_ENABLE, LOW); + + pinMode(PIN_QSPI_CS, OUTPUT); + digitalWrite(PIN_QSPI_CS, HIGH); + + pinMode(LED_GREEN, OUTPUT); + digitalWrite(LED_GREEN, LOW); + + pinMode(LED_BLUE, OUTPUT); + digitalWrite(LED_BLUE, LOW); + + /* disable gps until we actually support it. + pinMode(GPS_EN, OUTPUT); + digitalWrite(GPS_EN, HIGH); + */ +} diff --git a/variants/sensecap_solar/variant.h b/variants/sensecap_solar/variant.h new file mode 100644 index 00000000..76494f48 --- /dev/null +++ b/variants/sensecap_solar/variant.h @@ -0,0 +1,85 @@ +#ifndef _SEEED_SENSECAP_SOLAR_H_ +#define _SEEED_SENSECAP_SOLAR_H_ + +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +#define USE_LFXO // Board uses 32khz crystal for LF + +/*---------------------------------------------------------------------------- + * Headers + *----------------------------------------------------------------------------*/ + +#include "WVariant.h" + +#define PINS_COUNT (33) +#define NUM_DIGITAL_PINS (33) +#define NUM_ANALOG_INPUTS (8) +#define NUM_ANALOG_OUTPUTS (0) + +// LEDs +#define PIN_LED (12) +#define LED_PWR (PINS_COUNT) + +#define LED_BUILTIN (PIN_LED) + +#define LED_RED (PINS_COUNT) +#define LED_GREEN (12) +#define LED_BLUE (11) + +#define LED_STATE_ON (1) // State when LED is litted + +// Buttons +#define PIN_BUTTON1 (13) +#define PIN_BUTTON2 (20) + +#define VBAT_ENABLE (19) // Output LOW to enable reading of the BAT voltage. + +// Analog pins +#define BATTERY_PIN (16) // Read the BAT voltage. +#define AREF_VOLTAGE (3.0F) +#define ADC_MULTIPLIER (3.0F) // 1M, 512k divider bridge +#define ADC_RESOLUTION (12) + +// Serial interfaces +#define PIN_SERIAL1_RX (7) +#define PIN_SERIAL1_TX (6) + +// SPI Interfaces +#define SPI_INTERFACES_COUNT (1) + +#define PIN_SPI_MISO (9) +#define PIN_SPI_MOSI (10) +#define PIN_SPI_SCK (8) + +// Lora SPI is on SPI0 +#define P_LORA_SCLK PIN_SPI_SCK +#define P_LORA_MISO PIN_SPI_MISO +#define P_LORA_MOSI PIN_SPI_MOSI + +// Wire Interfaces +#define WIRE_INTERFACES_COUNT (1) + +#define PIN_WIRE_SDA (14) +#define PIN_WIRE_SCL (15) + +// GPS L76KB +#define GPS_BAUDRATE 9600 +#define GPS_THREAD_INTERVAL 50 +#define PIN_GPS_TX PIN_SERIAL1_RX +#define PIN_GPS_RX PIN_SERIAL1_TX +#define PIN_GPS_STANDBY (0) +#define GPS_EN (18) + +// QSPI Pins +#define PIN_QSPI_SCK (21) +#define PIN_QSPI_CS (22) +#define PIN_QSPI_IO0 (23) +#define PIN_QSPI_IO1 (24) +#define PIN_QSPI_IO2 (25) +#define PIN_QSPI_IO3 (26) + +#define EXTERNAL_FLASH_DEVICES P25Q16H +#define EXTERNAL_FLASH_USE_QSPI + +#endif \ No newline at end of file diff --git a/variants/t1000-e/target.cpp b/variants/t1000-e/target.cpp index f6fb1f04..06509c4f 100644 --- a/variants/t1000-e/target.cpp +++ b/variants/t1000-e/target.cpp @@ -61,7 +61,8 @@ bool radio_init() { return false; // fail } - radio.setCRC(1); + radio.setCRC(2); + radio.explicitHeader(); #ifdef RF_SWITCH_TABLE radio.setRfSwitchTable(rfswitch_dios, rfswitch_table); diff --git a/variants/wio-e5-dev/target.cpp b/variants/wio-e5-dev/target.cpp index 8ccbe384..42e900e4 100644 --- a/variants/wio-e5-dev/target.cpp +++ b/variants/wio-e5-dev/target.cpp @@ -35,7 +35,7 @@ bool radio_init() { radio.setRfSwitchTable(rfswitch_pins, rfswitch_table); - int status = radio.begin(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, LORA_TX_POWER, 8, 1.7, 0); + int status = radio.begin(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, LORA_TX_POWER, 16, 1.7, 0); if (status != RADIOLIB_ERR_NONE) { Serial.print("ERROR: radio init failed: "); diff --git a/variants/wio-e5-mini/platformio.ini b/variants/wio-e5-mini/platformio.ini index dfe4a090..93508d8e 100644 --- a/variants/wio-e5-mini/platformio.ini +++ b/variants/wio-e5-mini/platformio.ini @@ -25,6 +25,15 @@ build_flags = ${lora_e5_mini.build_flags} build_src_filter = ${lora_e5_mini.build_src_filter} +<../examples/simple_repeater/main.cpp> +[env:wio-e5-mini-sensor] +extends = lora_e5_mini +build_flags = ${lora_e5_mini.build_flags} + -D LORA_TX_POWER=22 + -D ADVERT_NAME='"wio-e5-mini Sensor"' + -D ADMIN_PASSWORD='"password"' +build_src_filter = ${lora_e5_mini.build_src_filter} + +<../examples/simple_sensor> + [env:wio-e5-mini_companion_radio_usb] extends = lora_e5_mini build_flags = ${lora_e5_mini.build_flags} diff --git a/variants/wio-e5-mini/target.cpp b/variants/wio-e5-mini/target.cpp index 6c045dd5..0e2358b8 100644 --- a/variants/wio-e5-mini/target.cpp +++ b/variants/wio-e5-mini/target.cpp @@ -33,7 +33,7 @@ bool radio_init() { radio.setRfSwitchTable(rfswitch_pins, rfswitch_table); - int status = radio.begin(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, LORA_TX_POWER, 8, 1.7, 0); + int status = radio.begin(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, LORA_TX_POWER, 16, 1.7, 0); if (status != RADIOLIB_ERR_NONE) { Serial.print("ERROR: radio init failed: "); diff --git a/variants/wio-tracker-l1/WioTrackerL1Board.cpp b/variants/wio-tracker-l1/WioTrackerL1Board.cpp new file mode 100644 index 00000000..c5c9db65 --- /dev/null +++ b/variants/wio-tracker-l1/WioTrackerL1Board.cpp @@ -0,0 +1,96 @@ +#include +#include "WioTrackerL1Board.h" + +#include +#include + +static BLEDfu bledfu; + +static void connect_callback(uint16_t conn_handle) { + (void)conn_handle; + MESH_DEBUG_PRINTLN("BLE client connected"); +} + +static void disconnect_callback(uint16_t conn_handle, uint8_t reason) { + (void)conn_handle; + (void)reason; + + MESH_DEBUG_PRINTLN("BLE client disconnected"); +} + +void WioTrackerL1Board::begin() { + // for future use, sub-classes SHOULD call this from their begin() + startup_reason = BD_STARTUP_NORMAL; + btn_prev_state = HIGH; + + pinMode(PIN_VBAT_READ, INPUT); // VBAT ADC input + // Set all button pins to INPUT_PULLUP + pinMode(PIN_BUTTON1, INPUT_PULLUP); + pinMode(PIN_BUTTON2, INPUT_PULLUP); + pinMode(PIN_BUTTON3, INPUT_PULLUP); + pinMode(PIN_BUTTON4, INPUT_PULLUP); + pinMode(PIN_BUTTON5, INPUT_PULLUP); + pinMode(PIN_BUTTON6, INPUT_PULLUP); + + + #if defined(PIN_WIRE_SDA) && defined(PIN_WIRE_SCL) + Wire.setPins(PIN_WIRE_SDA, PIN_WIRE_SCL); + #endif + + Wire.begin(); + + #ifdef P_LORA_TX_LED + pinMode(P_LORA_TX_LED, OUTPUT); + digitalWrite(P_LORA_TX_LED, LOW); + #endif + + delay(10); // give sx1262 some time to power up +} + +bool WioTrackerL1Board::startOTAUpdate(const char* id, char reply[]) { + // Config the peripheral connection with maximum bandwidth + // more SRAM required by SoftDevice + // Note: All config***() function must be called before begin() + Bluefruit.configPrphBandwidth(BANDWIDTH_MAX); + Bluefruit.configPrphConn(92, BLE_GAP_EVENT_LENGTH_MIN, 16, 16); + + Bluefruit.begin(1, 0); + // Set max power. Accepted values are: -40, -30, -20, -16, -12, -8, -4, 0, 4 + Bluefruit.setTxPower(4); + // Set the BLE device name + Bluefruit.setName("WioTrackerL1 OTA"); + + Bluefruit.Periph.setConnectCallback(connect_callback); + Bluefruit.Periph.setDisconnectCallback(disconnect_callback); + + // To be consistent OTA DFU should be added first if it exists + bledfu.begin(); + + // Set up and start advertising + // Advertising packet + Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE); + Bluefruit.Advertising.addTxPower(); + Bluefruit.Advertising.addName(); + + /* Start Advertising + - Enable auto advertising if disconnected + - Interval: fast mode = 20 ms, slow mode = 152.5 ms + - Timeout for fast mode is 30 seconds + - Start(timeout) with timeout = 0 will advertise forever (until connected) + + For recommended advertising interval + https://developer.apple.com/library/content/qa/qa1931/_index.html + */ + Bluefruit.Advertising.restartOnDisconnect(true); + Bluefruit.Advertising.setInterval(32, 244); // in unit of 0.625 ms + Bluefruit.Advertising.setFastTimeout(30); // number of seconds in fast mode + Bluefruit.Advertising.start(0); // 0 = Don't stop advertising after n seconds + + uint8_t mac_addr[6]; + memset(mac_addr, 0, sizeof(mac_addr)); + Bluefruit.getAddr(mac_addr); + sprintf(reply, "OK - mac: %02X:%02X:%02X:%02X:%02X:%02X", + mac_addr[5], mac_addr[4], mac_addr[3], mac_addr[2], mac_addr[1], mac_addr[0]); + + return true; +} diff --git a/variants/wio-tracker-l1/WioTrackerL1Board.h b/variants/wio-tracker-l1/WioTrackerL1Board.h new file mode 100644 index 00000000..03aef79c --- /dev/null +++ b/variants/wio-tracker-l1/WioTrackerL1Board.h @@ -0,0 +1,42 @@ +#pragma once + +#include +#include + +class WioTrackerL1Board : public mesh::MainBoard { +protected: + uint8_t startup_reason; + uint8_t btn_prev_state; + +public: + void begin(); + uint8_t getStartupReason() const override { 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 + + uint16_t getBattMilliVolts() override { + int adcvalue = 0; + analogReadResolution(12); + analogReference(AR_INTERNAL); + delay(10); + adcvalue = analogRead(PIN_VBAT_READ); + return (adcvalue * ADC_MULTIPLIER * AREF_VOLTAGE) / 4.096; + } + + const char* getManufacturerName() const override { + return "Seeed Wio Tracker L1"; + } + + void reboot() override { + NVIC_SystemReset(); + } + + bool startOTAUpdate(const char* id, char reply[]) override; +}; diff --git a/variants/wio-tracker-l1/platformio.ini b/variants/wio-tracker-l1/platformio.ini new file mode 100644 index 00000000..380ff90f --- /dev/null +++ b/variants/wio-tracker-l1/platformio.ini @@ -0,0 +1,92 @@ +[WioTrackerL1] +extends = nrf52_base +board = seeed-wio-tracker-l1 +board_build.ldscript = boards/nrf52840_s140_v7.ld +build_flags = ${nrf52_base.build_flags} + -I lib/nrf52/s140_nrf52_7.3.0_API/include + -I lib/nrf52/s140_nrf52_7.3.0_API/include/nrf52 + -I variants/wio-tracker-l1 + -D WIO_TRACKER_L1 + -D RADIO_CLASS=CustomSX1262 + -D WRAPPER_CLASS=CustomSX1262Wrapper + -D LORA_TX_POWER=22 + -D SX126X_CURRENT_LIMIT=140 + -D SX126X_RX_BOOSTED_GAIN=1 + -D PIN_OLED_RESET=-1 + ; -D MESH_DEBUG=1 +build_src_filter = ${nrf52_base.build_src_filter} + + + +<../variants/wio-tracker-l1> + + + + +lib_deps= ${nrf52_base.lib_deps} + adafruit/Adafruit SH110X @ ^2.1.13 + adafruit/Adafruit GFX Library @ ^1.12.1 + stevemarple/MicroNMEA @ ^2.0.6 + +[env:WioTrackerL1_Repeater] +extends = WioTrackerL1 +build_src_filter = ${WioTrackerL1.build_src_filter} + +<../examples/simple_repeater> +build_flags = + ${WioTrackerL1.build_flags} + -D ADVERT_NAME='"WioTrackerL1 Repeater"' + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=8 + -D DISPLAY_CLASS=SH1106Display +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +lib_deps = ${WioTrackerL1.lib_deps} + adafruit/RTClib @ ^2.1.3 + +[env:WioTrackerL1_room_server] +extends = WioTrackerL1 +build_src_filter = ${WioTrackerL1.build_src_filter} + +<../examples/simple_room_server> +build_flags = ${WioTrackerL1.build_flags} + -D ADVERT_NAME='"WioTrackerL1 Room"' + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' + -D DISPLAY_CLASS=SH1106Display +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +lib_deps = ${WioTrackerL1.lib_deps} + adafruit/RTClib @ ^2.1.3 + +[env:WioTrackerL1_companion_radio_usb] +extends = WioTrackerL1 +build_flags = ${WioTrackerL1.build_flags} + -D MAX_CONTACTS=100 + -D MAX_GROUP_CHANNELS=8 + -D DISPLAY_CLASS=SH1106Display +; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 +; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 +build_src_filter = ${WioTrackerL1.build_src_filter} + +<../examples/companion_radio> + + + + +lib_deps = ${WioTrackerL1.lib_deps} + adafruit/RTClib @ ^2.1.3 + densaugeo/base64 @ ~1.4.0 + end2endzone/NonBlockingRTTTL@^1.3.0 + +[env:WioTrackerL1_companion_radio_ble] +extends = WioTrackerL1 +build_flags = ${WioTrackerL1.build_flags} + -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 DISPLAY_CLASS=SH1106Display +; -D MESH_PACKET_LOGGING=1 + -D MESH_DEBUG=1 + -D PIN_BUZZER=12 +build_src_filter = ${WioTrackerL1.build_src_filter} + + + +<../examples/companion_radio> + + +lib_deps = ${WioTrackerL1.lib_deps} + adafruit/RTClib @ ^2.1.3 + densaugeo/base64 @ ~1.4.0 + end2endzone/NonBlockingRTTTL@^1.3.0 diff --git a/variants/wio-tracker-l1/target.cpp b/variants/wio-tracker-l1/target.cpp new file mode 100644 index 00000000..0809e19e --- /dev/null +++ b/variants/wio-tracker-l1/target.cpp @@ -0,0 +1,146 @@ +#include +#include "target.h" +#include +#include + +WioTrackerL1Board board; + +RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); + +WRAPPER_CLASS radio_driver(radio, board); + +VolatileRTCClock fallback_clock; +AutoDiscoverRTCClock rtc_clock(fallback_clock); +MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1); +WioTrackerL1SensorManager sensors = WioTrackerL1SensorManager(nmea); + +#ifdef DISPLAY_CLASS + DISPLAY_CLASS display; +#endif + +bool radio_init() { + rtc_clock.begin(Wire); + + return radio.std_init(&SPI); +} + +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); +} + +void WioTrackerL1SensorManager::start_gps() +{ + if (!gps_active) + { + MESH_DEBUG_PRINTLN("starting GPS"); + digitalWrite(PIN_GPS_STANDBY, HIGH); + gps_active = true; + } +} + +void WioTrackerL1SensorManager::stop_gps() +{ + if (gps_active) + { + MESH_DEBUG_PRINTLN("stopping GPS"); + digitalWrite(PIN_GPS_STANDBY, LOW); + gps_active = false; + } +} + +bool WioTrackerL1SensorManager::begin() +{ + Serial1.setPins(PIN_GPS_TX, PIN_GPS_RX); // be sure to tx into rx and rx into tx + Serial1.begin(GPS_BAUDRATE); + + pinMode(PIN_GPS_STANDBY, OUTPUT); + digitalWrite(PIN_GPS_STANDBY, HIGH); // Wake GPS from standby + delay(500); + + // We'll consider GPS detected if we see any data on Serial1 + if (Serial1.available() > 0) + { + MESH_DEBUG_PRINTLN("GPS detected"); + } + else + { + MESH_DEBUG_PRINTLN("No GPS detected"); + } + digitalWrite(PIN_GPS_STANDBY, LOW); // Put GPS back into standby mode + return true; +} + +bool WioTrackerL1SensorManager::querySensors(uint8_t requester_permissions, CayenneLPP &telemetry) +{ + if (requester_permissions & TELEM_PERM_LOCATION) + { // does requester have permission? + telemetry.addGPS(TELEM_CHANNEL_SELF, node_lat, node_lon, node_altitude); + } + return true; +} + +void WioTrackerL1SensorManager::loop() +{ + static long next_gps_update = 0; + _location->loop(); + if (millis() > next_gps_update && gps_active) // don't bother if gps position is not enabled + { + if (_location->isValid()) + { + node_lat = ((double)_location->getLatitude()) / 1000000.; + node_lon = ((double)_location->getLongitude()) / 1000000.; + node_altitude = ((double)_location->getAltitude()) / 1000.0; + MESH_DEBUG_PRINTLN("lat %f lon %f", node_lat, node_lon); + } + next_gps_update = millis() + (1000 * 60); // after initial update, only check every minute TODO: should be configurable + } +} + +int WioTrackerL1SensorManager::getNumSettings() const { return 1; } // just one supported: "gps" (power switch) + +const char *WioTrackerL1SensorManager::getSettingName(int i) const +{ + return i == 0 ? "gps" : NULL; +} + +const char *WioTrackerL1SensorManager::getSettingValue(int i) const +{ + if (i == 0) + { + return gps_active ? "1" : "0"; + } + return NULL; +} + +bool WioTrackerL1SensorManager::setSettingValue(const char *name, const char *value) +{ + if (strcmp(name, "gps") == 0) + { + if (strcmp(value, "0") == 0) + { + stop_gps(); + } + else + { + start_gps(); + } + return true; + } + return false; // not supported +} + +mesh::LocalIdentity radio_new_identity() { + RadioNoiseListener rng(radio); + return mesh::LocalIdentity(&rng); // create new random identity +} diff --git a/variants/wio-tracker-l1/target.h b/variants/wio-tracker-l1/target.h new file mode 100644 index 00000000..0aac6c59 --- /dev/null +++ b/variants/wio-tracker-l1/target.h @@ -0,0 +1,47 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include +#include +#include +#include +#ifdef DISPLAY_CLASS + #include +#endif +#include + +class WioTrackerL1SensorManager : public SensorManager +{ + bool gps_active = false; + LocationProvider *_location; + + void start_gps(); + void stop_gps(); + +public: + WioTrackerL1SensorManager(LocationProvider &location) : _location(&location) {} + bool begin() override; + bool querySensors(uint8_t requester_permissions, CayenneLPP &telemetry) override; + void loop() override; + int getNumSettings() const override; + const char *getSettingName(int i) const override; + const char *getSettingValue(int i) const override; + bool setSettingValue(const char *name, const char *value) override; +}; + + +extern WioTrackerL1Board board; +extern WRAPPER_CLASS radio_driver; +extern AutoDiscoverRTCClock rtc_clock; +extern WioTrackerL1SensorManager 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(); diff --git a/variants/wio-tracker-l1/variant.cpp b/variants/wio-tracker-l1/variant.cpp new file mode 100644 index 00000000..3db5ec9a --- /dev/null +++ b/variants/wio-tracker-l1/variant.cpp @@ -0,0 +1,73 @@ +#include "variant.h" +#include "wiring_constants.h" +#include "wiring_digital.h" +#include "nrf.h" + +const uint32_t g_ADigitalPinMap[] = { + // D0 .. D10 - Peripheral control pins + 41, // D0 P1.09 GNSS_WAKEUP + 7, // D1 P0.07 LORA_DIO1 + 39, // D2 P1,07 LORA_RESET + 42, // D3 P1.10 LORA_BUSY + 46, // D4 P1.14 (A4/SDA) LORA_CS + 40, // D5 P1.08 (A5/SCL) LORA_SW + 27, // D6 P0.27 (UART_TX) GNSS_TX + 26, // D7 P0.26 (UART_RX) GNSS_RX + 30, // D8 P0.30 (SPI_SCK) LORA_SCK + 3, // D9 P0.3 (SPI_MISO) LORA_MISO + 28, // D10 P0.28 (SPI_MOSI) LORA_MOSI + + // D11-D12 - LED outputs + 33, // D11 P1.1 User LED + // Buzzzer + 32, // D12 P1.0 Buzzer + + // D13 - User input + 8, // D13 P0.08 User Button + + // D14-D15 - OLED + 6, // D14 P0.06 OLED SDA + 5, // D15 P0.05 OLED SCL + + // D16 - Battery voltage ADC input + 31, // D16 P0.31 VBAT_ADC + // GROVE + 43, // D17 P0.00 GROVE SDA + 44, // D18 P0.01 GROVE SCL + + // FLASH + 21, // D19 P0.21 (QSPI_SCK) + 25, // D20 P0.25 (QSPI_CSN) + 20, // D21 P0.20 (QSPI_SIO_0 DI) + 24, // D22 P0.24 (QSPI_SIO_1 DO) + 22, // D23 P0.22 (QSPI_SIO_2 WP) + 23, // D24 P0.23 (QSPI_SIO_3 HOLD) + + // JOYSTICK + 36, // D25 TB_UP + 12, // D26 TB_DOWN + 11, // D27 TB_LEFT + 35, // D28 TB_RIGHT + 37, // D29 TB_PRESS + + // VBAT ENABLE + 4, // D30 BAT_CTL +}; + +void initVariant() { + pinMode(PIN_QSPI_CS, OUTPUT); + digitalWrite(PIN_QSPI_CS, HIGH); + + // VBAT_ENABLE + pinMode(VBAT_ENABLE, OUTPUT); + digitalWrite(VBAT_ENABLE, HIGH); + + // set LED pin as output and set it low + pinMode(PIN_LED, OUTPUT); + digitalWrite(PIN_LED, LOW); + + // set buzzer pin as output and set it low + pinMode(12, OUTPUT); + digitalWrite(12, LOW); + pinMode(12, OUTPUT); +} diff --git a/variants/wio-tracker-l1/variant.h b/variants/wio-tracker-l1/variant.h new file mode 100644 index 00000000..7f51dcb8 --- /dev/null +++ b/variants/wio-tracker-l1/variant.h @@ -0,0 +1,102 @@ +#ifndef _SEEED_WIO_TRACKER_L1_H_ +#define _SEEED_WIO_TRACKER_L1_H_ + +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +#define USE_LFXO // Board uses 32khz crystal for LF + +/*---------------------------------------------------------------------------- + * Headers + *----------------------------------------------------------------------------*/ + +#include "WVariant.h" + +#define PINS_COUNT (33) +#define NUM_DIGITAL_PINS (33) +#define NUM_ANALOG_INPUTS (8) +#define NUM_ANALOG_OUTPUTS (0) + +// LEDs +#define PIN_LED (11) +#define LED_BLUE (-1) // Disable annoying flashing caused by Bluefruit +#define LED_BUILTIN PIN_LED +#define P_LORA_TX_LED PIN_LED +#define LED_STATE_ON 1 + +// Buttons +#define PIN_BUTTON1 (13) // Menu / User Button +#define PIN_BUTTON2 (25) // Joystick Up +#define PIN_BUTTON3 (26) // Joystick Down +#define PIN_BUTTON4 (27) // Joystick Left +#define PIN_BUTTON5 (28) // Joystick Right +#define PIN_BUTTON6 (28) // Joystick Press +#define PIN_USER_BTN PIN_BUTTON1 +#define JOYSTICK_UP PIN_BUTTON2 +#define JOYSTICK_DOWN PIN_BUTTON3 +#define JOYSTICK_LEFT PIN_BUTTON4 +#define JOYSTICK_RIGHT PIN_BUTTON5 +#define JOYSTICK_PRESS PIN_BUTTON6 + +// Buzzer +// #define PIN_BUZZER (12) // Buzzer pin (defined per firmware type) + +#define VBAT_ENABLE (30) + +// Analog pins +#define PIN_VBAT_READ (16) +#define AREF_VOLTAGE (3.6F) +#define ADC_MULTIPLIER (2.0F) +#define ADC_RESOLUTION (12) + +// Serial interfaces +#define PIN_SERIAL1_RX (7) +#define PIN_SERIAL1_TX (6) + +// SPI Interfaces +#define SPI_INTERFACES_COUNT (1) + +#define PIN_SPI_MISO (9) +#define PIN_SPI_MOSI (10) +#define PIN_SPI_SCK (8) + +// Lora Pins +#define P_LORA_SCLK PIN_SPI_SCK +#define P_LORA_MISO PIN_SPI_MISO +#define P_LORA_MOSI PIN_SPI_MOSI +#define P_LORA_DIO_1 (1) +#define P_LORA_RESET (2) +#define P_LORA_BUSY (3) +#define P_LORA_NSS (4) +#define SX126X_RXEN (5) +#define SX126X_TXEN RADIOLIB_NC + +// Wire Interfaces +#define WIRE_INTERFACES_COUNT (2) + +#define PIN_WIRE_SDA (14) +#define PIN_WIRE_SCL (15) +#define PIN_WIRE1_SDA (17) +#define PIN_WIRE1_SCL (18) +#define I2C_NO_RESCAN +#define DISPLAY_ADDRESS 0x3D // SH1106 OLED I2C address + +// GPS L76KB +#define GPS_BAUDRATE 9600 +#define PIN_GPS_TX PIN_SERIAL1_RX +#define PIN_GPS_RX PIN_SERIAL1_TX +#define PIN_GPS_STANDBY (0) +#define PIN_GPS_EN (18) + +// QSPI Pins +#define PIN_QSPI_SCK (21) +#define PIN_QSPI_CS (22) +#define PIN_QSPI_IO0 (23) +#define PIN_QSPI_IO1 (24) +#define PIN_QSPI_IO2 (25) +#define PIN_QSPI_IO3 (26) + +#define EXTERNAL_FLASH_DEVICES P25Q16H +#define EXTERNAL_FLASH_USE_QSPI + +#endif \ No newline at end of file diff --git a/variants/xiao_c3/platformio.ini b/variants/xiao_c3/platformio.ini index 55baf2b8..3e4bfdb4 100644 --- a/variants/xiao_c3/platformio.ini +++ b/variants/xiao_c3/platformio.ini @@ -1,6 +1,27 @@ [Xiao_esp32_C3] extends = esp32_base board = seeed_xiao_esp32c3 +build_flags = + ${esp32_base.build_flags} + -I variants/xiao_c3 + -D ESP32_CPU_FREQ=80 + -D PIN_VBAT_READ=D0 + -D P_LORA_DIO_1=D1 + -D P_LORA_NSS=D4 + -D P_LORA_RESET=D2 + -D P_LORA_BUSY=D3 + -D PIN_BOARD_SDA=D6 + -D PIN_BOARD_SCL=D7 + -D SX126X_RXEN=D5 + -D SX126X_DIO2_AS_RF_SWITCH=true + -D SX126X_DIO3_TCXO_VOLTAGE=1.8 + -D SX126X_CURRENT_LIMIT=140 +build_src_filter = ${esp32_base.build_src_filter} + +<../variants/xiao_c3> + +[Xiao_esp32_C3_custom] +extends = esp32_base +board = seeed_xiao_esp32c3 build_flags = ${esp32_base.build_flags} -I variants/xiao_c3 @@ -30,7 +51,7 @@ build_flags = -D WRAPPER_CLASS=CustomSX1262Wrapper -D SX126X_RX_BOOSTED_GAIN=1 -D LORA_TX_POWER=22 - -D ADVERT_NAME='"Xiao Repeater"' + -D ADVERT_NAME='"Xiao C3 Repeater"' -D ADVERT_LAT=0.0 -D ADVERT_LON=0.0 -D ADMIN_PASSWORD='"password"' @@ -41,12 +62,78 @@ lib_deps = ${Xiao_esp32_C3.lib_deps} ${esp32_ota.lib_deps} -[env:Xiao_C3_Repeater_sx1268] +[env:Xiao_C3_companion_radio_ble] extends = Xiao_esp32_C3 build_src_filter = ${Xiao_esp32_C3.build_src_filter} - +<../examples/simple_repeater/main.cpp> + +<../examples/companion_radio> + + build_flags = ${Xiao_esp32_C3.build_flags} + -D RADIO_CLASS=CustomSX1262 + -D WRAPPER_CLASS=CustomSX1262Wrapper + -D SX126X_RX_BOOSTED_GAIN=1 + -D LORA_TX_POWER=22 + -D MAX_CONTACTS=100 + -D MAX_GROUP_CHANNELS=8 + -D BLE_PIN_CODE=123456 + -D OFFLINE_QUEUE_SIZE=256 + ; -D BLE_DEBUG_LOGGING=1 + ; -D MESH_PACKET_LOGGING=1 + ; -D MESH_DEBUG=1 +lib_deps = + ${Xiao_esp32_C3.lib_deps} + ${esp32_ota.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:Xiao_C3_companion_radio_usb] +extends = Xiao_esp32_C3 +build_src_filter = ${Xiao_esp32_C3.build_src_filter} + +<../examples/companion_radio> + + +build_flags = + ${Xiao_esp32_C3.build_flags} + -D RADIO_CLASS=CustomSX1262 + -D WRAPPER_CLASS=CustomSX1262Wrapper + -D SX126X_RX_BOOSTED_GAIN=1 + -D LORA_TX_POWER=22 + -D MAX_CONTACTS=100 + -D MAX_GROUP_CHANNELS=8 + -D OFFLINE_QUEUE_SIZE=256 + ; -D BLE_DEBUG_LOGGING=1 + ; -D MESH_PACKET_LOGGING=1 + ; -D MESH_DEBUG=1 +lib_deps = + ${Xiao_esp32_C3.lib_deps} + ${esp32_ota.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:Xiao_C3_Repeater_sx1262_custom] +extends = Xiao_esp32_C3_custom +build_src_filter = ${Xiao_esp32_C3_custom.build_src_filter} + +<../examples/simple_repeater/main.cpp> +build_flags = + ${Xiao_esp32_C3_custom.build_flags} + -D RADIO_CLASS=CustomSX1262 + -D WRAPPER_CLASS=CustomSX1262Wrapper + -D SX126X_RX_BOOSTED_GAIN=1 + -D LORA_TX_POWER=22 + -D ADVERT_NAME='"Xiao 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 +lib_deps = + ${Xiao_esp32_C3_custom.lib_deps} + ${esp32_ota.lib_deps} + +[env:Xiao_C3_Repeater_sx1268_custom] +extends = Xiao_esp32_C3_custom +build_src_filter = ${Xiao_esp32_C3_custom.build_src_filter} + +<../examples/simple_repeater/main.cpp> +build_flags = + ${Xiao_esp32_C3_custom.build_flags} -D RADIO_CLASS=CustomSX1268 -D WRAPPER_CLASS=CustomSX1268Wrapper -D LORA_TX_POWER=22 @@ -58,5 +145,5 @@ build_flags = ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 lib_deps = - ${Xiao_esp32_C3.lib_deps} - ${esp32_ota.lib_deps} + ${Xiao_esp32_C3_custom.lib_deps} + ${esp32_ota.lib_deps} \ No newline at end of file diff --git a/src/helpers/nrf52/XiaoNrf52Board.cpp b/variants/xiao_nrf52/XiaoNrf52Board.cpp similarity index 100% rename from src/helpers/nrf52/XiaoNrf52Board.cpp rename to variants/xiao_nrf52/XiaoNrf52Board.cpp diff --git a/src/helpers/nrf52/XiaoNrf52Board.h b/variants/xiao_nrf52/XiaoNrf52Board.h similarity index 92% rename from src/helpers/nrf52/XiaoNrf52Board.h rename to variants/xiao_nrf52/XiaoNrf52Board.h index e6c8e7f7..60b9f5bb 100644 --- a/src/helpers/nrf52/XiaoNrf52Board.h +++ b/variants/xiao_nrf52/XiaoNrf52Board.h @@ -5,20 +5,19 @@ #ifdef XIAO_NRF52 -// LoRa radio module pins for Seeed Xiao-nrf52 +// redefine lora pins if using the S3 variant of SX1262 board #ifdef SX1262_XIAO_S3_VARIANT #undef P_LORA_DIO_1 #undef P_LORA_BUSY #undef P_LORA_RESET #undef P_LORA_NSS + #undef SX126X_RXEN #define P_LORA_DIO_1 D0 #define P_LORA_BUSY D1 #define P_LORA_RESET D2 #define P_LORA_NSS D3 + #define SX126X_RXEN D4 #endif -//#define SX126X_POWER_EN 37 - - class XiaoNrf52Board : public mesh::MainBoard { protected: diff --git a/variants/xiao_nrf52/platformio.ini b/variants/xiao_nrf52/platformio.ini index c4934e04..bba3e632 100644 --- a/variants/xiao_nrf52/platformio.ini +++ b/variants/xiao_nrf52/platformio.ini @@ -50,8 +50,7 @@ build_flags = ${nrf52840_xiao.build_flags} -D ENV_INCLUDE_INA219=1 build_src_filter = ${nrf52840_xiao.build_src_filter} + - + - + + + +<../variants/xiao_nrf52> debug_tool = jlink upload_protocol = nrfutil diff --git a/variants/xiao_nrf52/target.h b/variants/xiao_nrf52/target.h index eb299006..c8c6a42a 100644 --- a/variants/xiao_nrf52/target.h +++ b/variants/xiao_nrf52/target.h @@ -3,7 +3,7 @@ #define RADIOLIB_STATIC_ONLY 1 #include #include -#include +#include #include #include #include diff --git a/variants/xiao_rp2040/platformio.ini b/variants/xiao_rp2040/platformio.ini new file mode 100644 index 00000000..960fdbba --- /dev/null +++ b/variants/xiao_rp2040/platformio.ini @@ -0,0 +1,104 @@ +[Xiao_rp2040] +extends = rp2040_base + +board = seeed_xiao_rp2040 +board_build.filesystem_size = 0.5m + +build_flags = ${rp2040_base.build_flags} + -I variants/xiao_rp2040 + -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/xiao_rp2040> + +lib_deps = ${rp2040_base.lib_deps} + +[env:Xiao_rp2040_Repeater] +extends = Xiao_rp2040 +build_flags = ${Xiao_rp2040.build_flags} + -D ADVERT_NAME='"Xiao 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 = ${Xiao_rp2040.build_src_filter} + +<../examples/simple_repeater> + +[env:Xiao_rp2040_room_server] +extends = Xiao_rp2040 +build_flags = ${Xiao_rp2040.build_flags} + -D ADVERT_NAME='"Xiao 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 = ${Xiao_rp2040.build_src_filter} + +<../examples/simple_room_server> + +[env:Xiao_rp2040_companion_radio_usb] +extends = Xiao_rp2040 +build_flags = ${Xiao_rp2040.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 = ${Xiao_rp2040.build_src_filter} + +<../examples/companion_radio> +lib_deps = ${Xiao_rp2040.lib_deps} + densaugeo/base64 @ ~1.4.0 + +; [env:Xiao_rp2040_companion_radio_ble] +; extends = Xiao_rp2040 +; build_flags = ${Xiao_rp2040.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 = ${Xiao_rp2040.build_src_filter} +; +<../examples/companion_radio> +; lib_deps = ${Xiao_rp2040.lib_deps} +; densaugeo/base64 @ ~1.4.0 + +; [env:Xiao_rp2040_companion_radio_wifi] +; extends = Xiao_rp2040 +; build_flags = ${Xiao_rp2040.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 = ${Xiao_rp2040.build_src_filter} +; +<../examples/companion_radio> +; lib_deps = ${Xiao_rp2040.lib_deps} +; densaugeo/base64 @ ~1.4.0 + +[env:Xiao_rp2040_terminal_chat] +extends = Xiao_rp2040 +build_flags = ${Xiao_rp2040.build_flags} + -D MAX_CONTACTS=100 + -D MAX_GROUP_CHANNELS=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${Xiao_rp2040.build_src_filter} + +<../examples/simple_secure_chat/main.cpp> +lib_deps = ${Xiao_rp2040.lib_deps} + densaugeo/base64 @ ~1.4.0 diff --git a/variants/xiao_rp2040/target.cpp b/variants/xiao_rp2040/target.cpp new file mode 100644 index 00000000..a801aae8 --- /dev/null +++ b/variants/xiao_rp2040/target.cpp @@ -0,0 +1,71 @@ +#include "target.h" + +#include +#include + +XiaoRP2040Board 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); + +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 + 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/xiao_rp2040/target.h b/variants/xiao_rp2040/target.h new file mode 100644 index 00000000..6a3c192b --- /dev/null +++ b/variants/xiao_rp2040/target.h @@ -0,0 +1,21 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 + +#include +#include +#include +#include +#include +#include + +extern XiaoRP2040Board 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();