diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp new file mode 100644 index 00000000..4265ca6a --- /dev/null +++ b/examples/simple_repeater/MyMesh.cpp @@ -0,0 +1,701 @@ +#include "MyMesh.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 "repeater" +#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 + +#define REQ_TYPE_GET_STATUS 0x01 // same as _GET_STATS +#define REQ_TYPE_KEEP_ALIVE 0x02 +#define REQ_TYPE_GET_TELEMETRY_DATA 0x03 + +#define RESP_SERVER_LOGIN_OK 0 // response to ANON_REQ + +#define CLI_REPLY_DELAY_MILLIS 600 + + +ClientInfo *MyMesh::putClient(const mesh::Identity &id) { + uint32_t min_time = 0xFFFFFFFF; + ClientInfo *oldest = &known_clients[0]; + for (int i = 0; i < MAX_CLIENTS; i++) { + if (known_clients[i].last_activity < min_time) { + oldest = &known_clients[i]; + min_time = oldest->last_activity; + } + if (id.matches(known_clients[i].id)) return &known_clients[i]; // already known + } + + oldest->id = id; + oldest->out_path_len = -1; // initially out_path is unknown + oldest->last_timestamp = 0; + return oldest; +} + +void MyMesh::putNeighbour(const mesh::Identity &id, uint32_t timestamp, float snr) { +#if MAX_NEIGHBOURS // check if neighbours enabled + // find existing neighbour, else use least recently updated + uint32_t oldest_timestamp = 0xFFFFFFFF; + NeighbourInfo *neighbour = &neighbours[0]; + for (int i = 0; i < MAX_NEIGHBOURS; i++) { + // if neighbour already known, we should update it + if (id.matches(neighbours[i].id)) { + neighbour = &neighbours[i]; + break; + } + + // otherwise we should update the least recently updated neighbour + if (neighbours[i].heard_timestamp < oldest_timestamp) { + neighbour = &neighbours[i]; + oldest_timestamp = neighbour->heard_timestamp; + } + } + + // update neighbour info + neighbour->id = id; + neighbour->advert_timestamp = timestamp; + neighbour->heard_timestamp = getRTCClock()->getCurrentTime(); + neighbour->snr = (int8_t)(snr * 4); +#endif +} + +int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t *payload, + size_t payload_len) { + // uint32_t now = getRTCClock()->getCurrentTimeUnique(); + // memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp + memcpy(reply_data, &sender_timestamp, + 4); // reflect sender_timestamp back in response packet (kind of like a 'tag') + + switch (payload[0]) { + case REQ_TYPE_GET_STATUS: { // guests can also access this now + RepeaterStats stats; + stats.batt_milli_volts = board.getBattMilliVolts(); + stats.curr_tx_queue_len = _mgr->getOutboundCount(0xFFFFFFFF); + stats.noise_floor = (int16_t)_radio->getNoiseFloor(); + stats.last_rssi = (int16_t)radio_driver.getLastRSSI(); + stats.n_packets_recv = radio_driver.getPacketsRecv(); + stats.n_packets_sent = radio_driver.getPacketsSent(); + stats.total_air_time_secs = getTotalAirTime() / 1000; + stats.total_up_time_secs = _ms->getMillis() / 1000; + stats.n_sent_flood = getNumSentFlood(); + stats.n_sent_direct = getNumSentDirect(); + stats.n_recv_flood = getNumRecvFlood(); + stats.n_recv_direct = getNumRecvDirect(); + stats.err_events = _err_flags; + stats.last_snr = (int16_t)(radio_driver.getLastSNR() * 4); + stats.n_direct_dups = ((SimpleMeshTables *)getTables())->getNumDirectDups(); + stats.n_flood_dups = ((SimpleMeshTables *)getTables())->getNumFloodDups(); + stats.total_rx_air_time_secs = getReceiveAirTime() / 1000; + + memcpy(&reply_data[4], &stats, sizeof(stats)); + + return 4 + sizeof(stats); // reply_len + } + case REQ_TYPE_GET_TELEMETRY_DATA: { + uint8_t perm_mask = ~(payload[1]); // NEW: first reserved byte (of 4), is now inverse mask to apply to permissions + + telemetry.reset(); + telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f); + // query other sensors -- target specific + sensors.querySensors((sender->is_admin ? 0xFF : 0x00) & perm_mask, telemetry); + + uint8_t tlen = telemetry.getSize(); + memcpy(&reply_data[4], telemetry.getBuffer(), tlen); + return 4 + tlen; // reply_len + } + } + return 0; // unknown command +} + +mesh::Packet *MyMesh::createSelfAdvert() { + uint8_t app_data[MAX_ADVERT_DATA_SIZE]; + uint8_t app_data_len; + { + AdvertDataBuilder builder(ADV_TYPE_REPEATER, _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); +} + +File MyMesh::openAppend(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 +} + +bool MyMesh::allowPacketForward(const mesh::Packet *packet) { + if (_prefs.disable_fwd) return false; + if (packet->isRouteFlood() && packet->path_len >= _prefs.flood_max) return false; + return true; +} + +const char *MyMesh::getLogDateTime() { + static char tmp[32]; + uint32_t now = getRTCClock()->getCurrentTime(); + DateTime dt = DateTime(now); + sprintf(tmp, "%02d:%02d:%02d - %d/%d/%d U", dt.hour(), dt.minute(), dt.second(), dt.day(), dt.month(), + dt.year()); + return tmp; +} + +void MyMesh::logRxRaw(float snr, float rssi, const uint8_t raw[], int len) { +#if MESH_PACKET_LOGGING + Serial.print(getLogDateTime()); + Serial.print(" RAW: "); + mesh::Utils::printHex(Serial, raw, len); + Serial.println(); +#endif +} + +void MyMesh::logRx(mesh::Packet *pkt, int len, float score) { + if (_logging) { + File f = openAppend(PACKET_LOG_FILE); + if (f) { + f.print(getLogDateTime()); + f.printf(": RX, len=%d (type=%d, route=%s, payload_len=%d) SNR=%d RSSI=%d score=%d", len, + pkt->getPayloadType(), pkt->isRouteDirect() ? "D" : "F", pkt->payload_len, + (int)_radio->getLastSNR(), (int)_radio->getLastRSSI(), (int)(score * 1000)); + + if (pkt->getPayloadType() == PAYLOAD_TYPE_PATH || pkt->getPayloadType() == PAYLOAD_TYPE_REQ || + pkt->getPayloadType() == PAYLOAD_TYPE_RESPONSE || pkt->getPayloadType() == PAYLOAD_TYPE_TXT_MSG) { + f.printf(" [%02X -> %02X]\n", (uint32_t)pkt->payload[1], (uint32_t)pkt->payload[0]); + } else { + f.printf("\n"); + } + f.close(); + } + } +} + +void MyMesh::logTx(mesh::Packet *pkt, int len) { +#ifdef WITH_BRIDGE + bridge.onPacketTransmitted(pkt); +#endif + if (_logging) { + File f = openAppend(PACKET_LOG_FILE); + if (f) { + f.print(getLogDateTime()); + f.printf(": TX, len=%d (type=%d, route=%s, payload_len=%d)", len, pkt->getPayloadType(), + pkt->isRouteDirect() ? "D" : "F", pkt->payload_len); + + if (pkt->getPayloadType() == PAYLOAD_TYPE_PATH || pkt->getPayloadType() == PAYLOAD_TYPE_REQ || + pkt->getPayloadType() == PAYLOAD_TYPE_RESPONSE || pkt->getPayloadType() == PAYLOAD_TYPE_TXT_MSG) { + f.printf(" [%02X -> %02X]\n", (uint32_t)pkt->payload[1], (uint32_t)pkt->payload[0]); + } else { + f.printf("\n"); + } + f.close(); + } + } +} + +void MyMesh::logTxFail(mesh::Packet *pkt, int len) { + if (_logging) { + File f = openAppend(PACKET_LOG_FILE); + if (f) { + f.print(getLogDateTime()); + f.printf(": TX FAIL!, len=%d (type=%d, route=%s, payload_len=%d)\n", len, pkt->getPayloadType(), + pkt->isRouteDirect() ? "D" : "F", pkt->payload_len); + f.close(); + } + } +} + +int MyMesh::calcRxDelay(float score, uint32_t air_time) const { + if (_prefs.rx_delay_base <= 0.0f) return 0; + return (int)((pow(_prefs.rx_delay_base, 0.85f - score) - 1.0) * air_time); +} + +uint32_t MyMesh::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 MyMesh::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; +} + +void MyMesh::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); + + bool is_admin; + data[len] = 0; // ensure null terminator + if (strcmp((char *)&data[4], _prefs.password) == 0) { // check for valid password + is_admin = true; + } else if (strcmp((char *)&data[4], _prefs.guest_password) == 0) { // check guest password + is_admin = false; + } else { +#if MESH_DEBUG + MESH_DEBUG_PRINTLN("Invalid password: %s", &data[4]); +#endif + return; + } + + auto client = putClient(sender); // add to known clients (if not already known) + if (timestamp <= client->last_timestamp) { + MESH_DEBUG_PRINTLN("Possible login replay attack!"); + return; // FATAL: client table is full -OR- replay attack + } + + MESH_DEBUG_PRINTLN("Login success!"); + 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 +#if 0 + memcpy(&reply_data[4], "OK", 2); // legacy response +#else + reply_data[4] = RESP_SERVER_LOGIN_OK; + reply_data[5] = 0; // NEW: recommended keep-alive interval (secs / 16) + reply_data[6] = is_admin ? 1 : 0; + reply_data[7] = 0; // FUTURE: reserved + getRNG()->random(&reply_data[8], 4); // random blob to help packet-hash uniqueness +#endif + + 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, client->secret, packet->path, packet->path_len, + PAYLOAD_TYPE_RESPONSE, reply_data, 12); + if (path) sendFlood(path, SERVER_RESPONSE_DELAY); + } else { + mesh::Packet *reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, client->secret, reply_data, 12); + if (reply) { + if (client->out_path_len >= 0) { // we have an out_path, so send DIRECT + sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY); + } else { + sendFlood(reply, SERVER_RESPONSE_DELAY); + } + } + } + } +} + +int MyMesh::searchPeersByHash(const uint8_t *hash) { + int n = 0; + for (int i = 0; i < MAX_CLIENTS; i++) { + if (known_clients[i].id.isHashMatch(hash)) { + matching_peer_indexes[n++] = i; // store the INDEXES of matching contacts (for subsequent 'peer' methods) + } + } + return n; +} + +void MyMesh::getPeerSharedSecret(uint8_t *dest_secret, int peer_idx) { + int i = matching_peer_indexes[peer_idx]; + if (i >= 0 && i < MAX_CLIENTS) { + // lookup pre-calculated shared_secret + memcpy(dest_secret, known_clients[i].secret, PUB_KEY_SIZE); + } else { + MESH_DEBUG_PRINTLN("getPeerSharedSecret: Invalid peer idx: %d", i); + } +} + +void MyMesh::onAdvertRecv(mesh::Packet *packet, const mesh::Identity &id, uint32_t timestamp, + const uint8_t *app_data, size_t app_data_len) { + mesh::Mesh::onAdvertRecv(packet, id, timestamp, app_data, app_data_len); // chain to super impl + + // if this a zero hop advert, add it to neighbours + if (packet->path_len == 0) { + AdvertDataParser parser(app_data, app_data_len); + if (parser.isValid() && parser.getType() == ADV_TYPE_REPEATER) { // just keep neigbouring Repeaters + putNeighbour(id, timestamp, packet->getSNR()); + } + } +} + +void MyMesh::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 >= MAX_CLIENTS) { // get from our known_clients table (sender SHOULD already be known in this context) + MESH_DEBUG_PRINTLN("onPeerDataRecv: invalid peer idx: %d", i); + return; + } + auto client = &known_clients[i]; + if (type == PAYLOAD_TYPE_REQ) { // request (from a Known admin client!) + uint32_t timestamp; + memcpy(×tamp, data, 4); + + if (timestamp > client->last_timestamp) { // prevent replay attacks + int reply_len = handleRequest(client, timestamp, &data[4], len - 4); + if (reply_len == 0) return; // invalid command + + client->last_timestamp = timestamp; + client->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(client->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, client->id, secret, reply_data, reply_len); + if (reply) { + if (client->out_path_len >= 0) { // we have an out_path, so send DIRECT + sendDirect(reply, client->out_path, client->out_path_len, 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 && client->is_admin) { // 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_PLAIN || flags == TXT_TYPE_CLI_DATA)) { + MESH_DEBUG_PRINTLN("onPeerDataRecv: unsupported text type received: flags=%02x", (uint32_t)flags); + } else if (sender_timestamp >= client->last_timestamp) { // prevent replay attacks + bool is_retry = (sender_timestamp == client->last_timestamp); + client->last_timestamp = sender_timestamp; + client->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 + + if (flags == TXT_TYPE_PLAIN) { // for legacy CLI, send Acks + uint32_t ack_hash; // calc truncated hash of the message timestamp + text + sender pub_key, to prove + // to sender that we got it + mesh::Utils::sha256((uint8_t *)&ack_hash, 4, data, 5 + strlen((char *)&data[5]), client->id.pub_key, + PUB_KEY_SIZE); + + mesh::Packet *ack = createAck(ack_hash); + if (ack) { + if (client->out_path_len < 0) { + sendFlood(ack, TXT_ACK_DELAY); + } else { + sendDirect(ack, client->out_path, client->out_path_len, TXT_ACK_DELAY); + } + } + } + + uint8_t temp[166]; + char *command = (char *)&data[5]; + char *reply = (char *)&temp[5]; + if (is_retry) { + *reply = 0; + } else { + 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); // NOTE: legacy was: TXT_TYPE_PLAIN + + auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, secret, temp, 5 + text_len); + if (reply) { + if (client->out_path_len < 0) { + sendFlood(reply, CLI_REPLY_DELAY_MILLIS); + } else { + sendDirect(reply, client->out_path, client->out_path_len, CLI_REPLY_DELAY_MILLIS); + } + } + } + } else { + MESH_DEBUG_PRINTLN("onPeerDataRecv: possible replay attack detected"); + } + } +} + +bool MyMesh::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) { + // TODO: prevent replay attacks + int i = matching_peer_indexes[sender_idx]; + + if (i >= 0 && + i < MAX_CLIENTS) { // get from our known_clients table (sender SHOULD already be known in this context) + MESH_DEBUG_PRINTLN("PATH to client, path_len=%d", (uint32_t)path_len); + auto client = &known_clients[i]; + memcpy(client->out_path, path, client->out_path_len = path_len); // store a copy of path, for sendDirect() + } else { + MESH_DEBUG_PRINTLN("onPeerPathRecv: invalid peer idx: %d", i); + } + + // NOTE: no reciprocal path send!! + return false; +} + +MyMesh::MyMesh(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) +#if defined(WITH_RS232_BRIDGE) + , bridge(WITH_RS232_BRIDGE, _mgr, &rtc) +#elif defined(WITH_ESPNOW_BRIDGE) + , bridge(_mgr, &rtc) +#endif +{ + memset(known_clients, 0, sizeof(known_clients)); + next_local_advert = next_flood_advert = 0; + set_radio_at = revert_radio_at = 0; + _logging = false; + +#if MAX_NEIGHBOURS + memset(neighbours, 0, sizeof(neighbours)); +#endif + + // 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 = 12; // 12 hours + _prefs.flood_max = 64; + _prefs.interference_threshold = 0; // disabled +} + +void MyMesh::begin(FILESYSTEM *fs) { + mesh::Mesh::begin(); + _fs = fs; + // load persisted prefs + _cli.loadPrefs(_fs); + +#ifdef WITH_BRIDGE + bridge.begin(); +#endif + + radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); + radio_set_tx_power(_prefs.tx_power_dbm); + + updateAdvertTimer(); + updateFloodAdvertTimer(); +} + +void MyMesh::applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) { + set_radio_at = futureMillis(2000); // give CLI reply some time to be sent back, before applying temp radio params + pending_freq = freq; + pending_bw = bw; + pending_sf = sf; + pending_cr = cr; + + revert_radio_at = futureMillis(2000 + timeout_mins * 60 * 1000); // schedule when to revert radio params +} + +bool MyMesh::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 MyMesh::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 MyMesh::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 MyMesh::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 MyMesh::dumpLogFile() { +#if defined(RP2040_PLATFORM) + File f = _fs->open(PACKET_LOG_FILE, "r"); +#else + File f = _fs->open(PACKET_LOG_FILE); +#endif + if (f) { + while (f.available()) { + int c = f.read(); + if (c < 0) break; + Serial.print((char)c); + } + f.close(); + } +} + +void MyMesh::setTxPower(uint8_t power_dbm) { + radio_set_tx_power(power_dbm); +} + +void MyMesh::formatNeighborsReply(char *reply) { + char *dp = reply; + +#if MAX_NEIGHBOURS + for (int i = 0; i < MAX_NEIGHBOURS && dp - reply < 134; i++) { + NeighbourInfo *neighbour = &neighbours[i]; + if (neighbour->heard_timestamp == 0) continue; // skip empty slots + + // add new line if not first item + if (i > 0) *dp++ = '\n'; + + char hex[10]; + // get 4 bytes of neighbour id as hex + mesh::Utils::toHex(hex, neighbour->id.pub_key, 4); + + // add next neighbour + uint32_t secs_ago = getRTCClock()->getCurrentTime() - neighbour->heard_timestamp; + sprintf(dp, "%s:%d:%d", hex, secs_ago, neighbour->snr); + while (*dp) + dp++; // find end of string + } +#endif + if (dp == reply) { // no neighbours, need empty response + strcpy(dp, "-none-"); + dp += 6; + } + *dp = 0; // null terminator +} + +void MyMesh::removeNeighbor(const uint8_t *pubkey, int key_len) { +#if MAX_NEIGHBOURS + for (int i = 0; i < MAX_NEIGHBOURS; i++) { + NeighbourInfo *neighbour = &neighbours[i]; + if (memcmp(neighbour->id.pub_key, pubkey, key_len) == 0) { + neighbours[i] = NeighbourInfo(); // clear neighbour entry + } + } +#endif +} + +void MyMesh::saveIdentity(const mesh::LocalIdentity &new_id) { + self_id = new_id; +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + IdentityStore store(*_fs, ""); +#elif defined(ESP32) + IdentityStore store(*_fs, "/identity"); +#elif defined(RP2040_PLATFORM) + IdentityStore store(*_fs, "/identity"); +#else +#error "need to define saveIdentity()" +#endif + store.save("_main", self_id); +} + +void MyMesh::clearStats() { + radio_driver.resetStats(); + resetStats(); + ((SimpleMeshTables *)getTables())->resetStats(); +} + +void MyMesh::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 MyMesh::loop() { +#ifdef WITH_BRIDGE + bridge.loop(); +#endif + + 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 + } + + if (set_radio_at && millisHasNowPassed(set_radio_at)) { // apply pending (temporary) radio params + set_radio_at = 0; // clear timer + radio_set_params(pending_freq, pending_bw, pending_sf, pending_cr); + MESH_DEBUG_PRINTLN("Temp radio params"); + } + + if (revert_radio_at && millisHasNowPassed(revert_radio_at)) { // revert radio params to orig + revert_radio_at = 0; // clear timer + radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); + MESH_DEBUG_PRINTLN("Radio params restored"); + } +} diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h new file mode 100644 index 00000000..06e7ebe4 --- /dev/null +++ b/examples/simple_repeater/MyMesh.h @@ -0,0 +1,192 @@ +#pragma once + +#include +#include +#include + +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + #include +#elif defined(RP2040_PLATFORM) + #include +#elif defined(ESP32) + #include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef WITH_RS232_BRIDGE +#include "helpers/bridges/RS232Bridge.h" +#define WITH_BRIDGE +#endif + +#ifdef WITH_ESPNOW_BRIDGE +#include "helpers/bridges/ESPNowBridge.h" +#define WITH_BRIDGE +#endif + +#ifdef WITH_BRIDGE +extern AbstractBridge* bridge; +#endif + +struct RepeaterStats { + uint16_t batt_milli_volts; + uint16_t curr_tx_queue_len; + int16_t noise_floor; + int16_t last_rssi; + uint32_t n_packets_recv; + uint32_t n_packets_sent; + uint32_t total_air_time_secs; + uint32_t total_up_time_secs; + uint32_t n_sent_flood, n_sent_direct; + uint32_t n_recv_flood, n_recv_direct; + uint16_t err_events; // was 'n_full_events' + int16_t last_snr; // x 4 + uint16_t n_direct_dups, n_flood_dups; + uint32_t total_rx_air_time_secs; +}; + +struct ClientInfo { + mesh::Identity id; + uint32_t last_timestamp, last_activity; + uint8_t secret[PUB_KEY_SIZE]; + bool is_admin; + int8_t out_path_len; + uint8_t out_path[MAX_PATH_SIZE]; +}; + +#ifndef MAX_CLIENTS + #define MAX_CLIENTS 32 +#endif + +struct NeighbourInfo { + mesh::Identity id; + uint32_t advert_timestamp; + uint32_t heard_timestamp; + int8_t snr; // multiplied by 4, user should divide to get float value +}; + +#ifndef FIRMWARE_BUILD_DATE + #define FIRMWARE_BUILD_DATE "1 Sep 2025" +#endif + +#ifndef FIRMWARE_VERSION + #define FIRMWARE_VERSION "v1.8.1" +#endif + +#define FIRMWARE_ROLE "repeater" + +#define PACKET_LOG_FILE "/packet_log" + +class MyMesh : public mesh::Mesh, public CommonCLICallbacks { + FILESYSTEM* _fs; + unsigned long next_local_advert, next_flood_advert; + bool _logging; + NodePrefs _prefs; + CommonCLI _cli; + uint8_t reply_data[MAX_PACKET_PAYLOAD]; + ClientInfo known_clients[MAX_CLIENTS]; +#if MAX_NEIGHBOURS + NeighbourInfo neighbours[MAX_NEIGHBOURS]; +#endif + CayenneLPP telemetry; + unsigned long set_radio_at, revert_radio_at; + float pending_freq; + float pending_bw; + uint8_t pending_sf; + uint8_t pending_cr; + int matching_peer_indexes[MAX_CLIENTS]; +#if defined(WITH_RS232_BRIDGE) + RS232Bridge bridge; +#elif defined(WITH_ESPNOW_BRIDGE) + ESPNowBridge bridge; +#endif + + ClientInfo* putClient(const mesh::Identity& id); + void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr); + int handleRequest(ClientInfo* sender, uint32_t sender_timestamp, uint8_t* payload, size_t payload_len); + mesh::Packet* createSelfAdvert(); + + File openAppend(const char* fname); + +protected: + float getAirtimeBudgetFactor() const override { + return _prefs.airtime_factor; + } + + bool allowPacketForward(const mesh::Packet* packet) override; + const char* getLogDateTime() override; + void logRxRaw(float snr, float rssi, const uint8_t raw[], int len) override; + + void logRx(mesh::Packet* pkt, int len, float score) override; + void logTx(mesh::Packet* pkt, int len) override; + void logTxFail(mesh::Packet* pkt, int len) 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 { + return _prefs.interference_threshold; + } + int getAGCResetInterval() const override { + return ((int)_prefs.agc_reset_interval) * 4000; // milliseconds + } + uint8_t getExtraAckTransmitCount() const override { + return _prefs.multi_acks; + } + + 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 onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, uint32_t timestamp, const uint8_t* app_data, size_t app_data_len); + 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; + +public: + MyMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables); + + void begin(FILESYSTEM* fs); + + 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); + } + + void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) override; + bool formatFileSystem() override; + void sendSelfAdvertisement(int delay_millis) override; + void updateAdvertTimer() override; + void updateFloodAdvertTimer() override; + + void setLoggingOn(bool enable) override { _logging = enable; } + + void eraseLogFile() override { + _fs->remove(PACKET_LOG_FILE); + } + + void dumpLogFile() override; + void setTxPower(uint8_t power_dbm) override; + void formatNeighborsReply(char *reply) override; + void removeNeighbor(const uint8_t* pubkey, int key_len) override; + + mesh::LocalIdentity& getSelfId() override { return self_id; } + + void saveIdentity(const mesh::LocalIdentity& new_id) override; + void clearStats() override; + void handleCommand(uint32_t sender_timestamp, char* command, char* reply); + void loop(); +}; diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index e4d8a454..7db0e7d4 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -1,834 +1,13 @@ #include // needed for PlatformIO #include -#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) - #include -#elif defined(RP2040_PLATFORM) - #include -#elif defined(ESP32) - #include -#endif - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -/* ------------------------------ Config -------------------------------- */ - -#ifndef FIRMWARE_BUILD_DATE - #define FIRMWARE_BUILD_DATE "1 Sep 2025" -#endif - -#ifndef FIRMWARE_VERSION - #define FIRMWARE_VERSION "v1.8.1" -#endif - -#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 "repeater" -#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 +#include "MyMesh.h" #ifdef DISPLAY_CLASS #include "UITask.h" static UITask ui_task(display); #endif -#define FIRMWARE_ROLE "repeater" - -#define PACKET_LOG_FILE "/packet_log" - -/* ------------------------------ Code -------------------------------- */ - -#ifdef WITH_RS232_BRIDGE -#include "helpers/bridges/RS232Bridge.h" -#define WITH_BRIDGE -#endif - -#ifdef WITH_ESPNOW_BRIDGE -#include "helpers/bridges/ESPNowBridge.h" -#define WITH_BRIDGE -#endif - -#define REQ_TYPE_GET_STATUS 0x01 // same as _GET_STATS -#define REQ_TYPE_KEEP_ALIVE 0x02 -#define REQ_TYPE_GET_TELEMETRY_DATA 0x03 - -#define RESP_SERVER_LOGIN_OK 0 // response to ANON_REQ - - struct RepeaterStats { - uint16_t batt_milli_volts; - uint16_t curr_tx_queue_len; - int16_t noise_floor; - int16_t last_rssi; - uint32_t n_packets_recv; - uint32_t n_packets_sent; - uint32_t total_air_time_secs; - uint32_t total_up_time_secs; - uint32_t n_sent_flood, n_sent_direct; - uint32_t n_recv_flood, n_recv_direct; - uint16_t err_events; // was 'n_full_events' - int16_t last_snr; // x 4 - uint16_t n_direct_dups, n_flood_dups; - uint32_t total_rx_air_time_secs; - }; - -struct ClientInfo { - mesh::Identity id; - uint32_t last_timestamp, last_activity; - uint8_t secret[PUB_KEY_SIZE]; - bool is_admin; - int8_t out_path_len; - uint8_t out_path[MAX_PATH_SIZE]; -}; - -#ifndef MAX_CLIENTS - #define MAX_CLIENTS 32 -#endif - -struct NeighbourInfo { - mesh::Identity id; - uint32_t advert_timestamp; - uint32_t heard_timestamp; - int8_t snr; // multiplied by 4, user should divide to get float value -}; - -#define CLI_REPLY_DELAY_MILLIS 600 - -class MyMesh : public mesh::Mesh, public CommonCLICallbacks { - FILESYSTEM* _fs; - unsigned long next_local_advert, next_flood_advert; - bool _logging; - NodePrefs _prefs; - CommonCLI _cli; - uint8_t reply_data[MAX_PACKET_PAYLOAD]; - ClientInfo known_clients[MAX_CLIENTS]; -#if MAX_NEIGHBOURS - NeighbourInfo neighbours[MAX_NEIGHBOURS]; -#endif - CayenneLPP telemetry; - unsigned long set_radio_at, revert_radio_at; - float pending_freq; - float pending_bw; - uint8_t pending_sf; - uint8_t pending_cr; -#if defined(WITH_RS232_BRIDGE) - RS232Bridge bridge; -#elif defined(WITH_ESPNOW_BRIDGE) - ESPNowBridge bridge; -#endif - - ClientInfo* putClient(const mesh::Identity& id) { - uint32_t min_time = 0xFFFFFFFF; - ClientInfo* oldest = &known_clients[0]; - for (int i = 0; i < MAX_CLIENTS; i++) { - if (known_clients[i].last_activity < min_time) { - oldest = &known_clients[i]; - min_time = oldest->last_activity; - } - if (id.matches(known_clients[i].id)) return &known_clients[i]; // already known - } - - oldest->id = id; - oldest->out_path_len = -1; // initially out_path is unknown - oldest->last_timestamp = 0; - return oldest; - } - - void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr) { - #if MAX_NEIGHBOURS // check if neighbours enabled - // find existing neighbour, else use least recently updated - uint32_t oldest_timestamp = 0xFFFFFFFF; - NeighbourInfo* neighbour = &neighbours[0]; - for (int i = 0; i < MAX_NEIGHBOURS; i++) { - // if neighbour already known, we should update it - if (id.matches(neighbours[i].id)) { - neighbour = &neighbours[i]; - break; - } - - // otherwise we should update the least recently updated neighbour - if (neighbours[i].heard_timestamp < oldest_timestamp) { - neighbour = &neighbours[i]; - oldest_timestamp = neighbour->heard_timestamp; - } - } - - // update neighbour info - neighbour->id = id; - neighbour->advert_timestamp = timestamp; - neighbour->heard_timestamp = getRTCClock()->getCurrentTime(); - neighbour->snr = (int8_t) (snr * 4); - #endif - } - - int handleRequest(ClientInfo* sender, uint32_t sender_timestamp, uint8_t* payload, size_t payload_len) { - // uint32_t now = getRTCClock()->getCurrentTimeUnique(); - // memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp - memcpy(reply_data, &sender_timestamp, 4); // reflect sender_timestamp back in response packet (kind of like a 'tag') - - switch (payload[0]) { - case REQ_TYPE_GET_STATUS: { // guests can also access this now - RepeaterStats stats; - stats.batt_milli_volts = board.getBattMilliVolts(); - stats.curr_tx_queue_len = _mgr->getOutboundCount(0xFFFFFFFF); - stats.noise_floor = (int16_t)_radio->getNoiseFloor(); - stats.last_rssi = (int16_t) radio_driver.getLastRSSI(); - stats.n_packets_recv = radio_driver.getPacketsRecv(); - stats.n_packets_sent = radio_driver.getPacketsSent(); - stats.total_air_time_secs = getTotalAirTime() / 1000; - stats.total_up_time_secs = _ms->getMillis() / 1000; - stats.n_sent_flood = getNumSentFlood(); - stats.n_sent_direct = getNumSentDirect(); - stats.n_recv_flood = getNumRecvFlood(); - stats.n_recv_direct = getNumRecvDirect(); - stats.err_events = _err_flags; - stats.last_snr = (int16_t)(radio_driver.getLastSNR() * 4); - stats.n_direct_dups = ((SimpleMeshTables *)getTables())->getNumDirectDups(); - stats.n_flood_dups = ((SimpleMeshTables *)getTables())->getNumFloodDups(); - stats.total_rx_air_time_secs = getReceiveAirTime() / 1000; - - memcpy(&reply_data[4], &stats, sizeof(stats)); - - return 4 + sizeof(stats); // reply_len - } - case REQ_TYPE_GET_TELEMETRY_DATA: { - uint8_t perm_mask = ~(payload[1]); // NEW: first reserved byte (of 4), is now inverse mask to apply to permissions - - telemetry.reset(); - telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f); - // query other sensors -- target specific - sensors.querySensors((sender->is_admin ? 0xFF : 0x00) & perm_mask, telemetry); - - uint8_t tlen = telemetry.getSize(); - memcpy(&reply_data[4], telemetry.getBuffer(), tlen); - return 4 + tlen; // reply_len - } - } - return 0; // unknown command - } - - mesh::Packet* createSelfAdvert() { - uint8_t app_data[MAX_ADVERT_DATA_SIZE]; - uint8_t app_data_len; - { - AdvertDataBuilder builder(ADV_TYPE_REPEATER, _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); - } - - File openAppend(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 - } - -protected: - float getAirtimeBudgetFactor() const override { - return _prefs.airtime_factor; - } - - bool allowPacketForward(const mesh::Packet* packet) override { - if (_prefs.disable_fwd) return false; - if (packet->isRouteFlood() && packet->path_len >= _prefs.flood_max) return false; - return true; - } - - const char* getLogDateTime() override { - static char tmp[32]; - uint32_t now = getRTCClock()->getCurrentTime(); - DateTime dt = DateTime(now); - sprintf(tmp, "%02d:%02d:%02d - %d/%d/%d U", dt.hour(), dt.minute(), dt.second(), dt.day(), dt.month(), dt.year()); - return tmp; - } - - void logRxRaw(float snr, float rssi, const uint8_t raw[], int len) override { - #if MESH_PACKET_LOGGING - Serial.print(getLogDateTime()); - Serial.print(" RAW: "); - mesh::Utils::printHex(Serial, raw, len); - Serial.println(); - #endif - } - - void logRx(mesh::Packet* pkt, int len, float score) override { - if (_logging) { - File f = openAppend(PACKET_LOG_FILE); - if (f) { - f.print(getLogDateTime()); - f.printf(": RX, len=%d (type=%d, route=%s, payload_len=%d) SNR=%d RSSI=%d score=%d", - len, pkt->getPayloadType(), pkt->isRouteDirect() ? "D" : "F", pkt->payload_len, - (int)_radio->getLastSNR(), (int)_radio->getLastRSSI(), (int)(score*1000)); - - if (pkt->getPayloadType() == PAYLOAD_TYPE_PATH || pkt->getPayloadType() == PAYLOAD_TYPE_REQ - || pkt->getPayloadType() == PAYLOAD_TYPE_RESPONSE || pkt->getPayloadType() == PAYLOAD_TYPE_TXT_MSG) { - f.printf(" [%02X -> %02X]\n", (uint32_t)pkt->payload[1], (uint32_t)pkt->payload[0]); - } else { - f.printf("\n"); - } - f.close(); - } - } - } - void logTx(mesh::Packet* pkt, int len) override { -#ifdef WITH_BRIDGE - bridge.onPacketTransmitted(pkt); -#endif - if (_logging) { - File f = openAppend(PACKET_LOG_FILE); - if (f) { - f.print(getLogDateTime()); - f.printf(": TX, len=%d (type=%d, route=%s, payload_len=%d)", - len, pkt->getPayloadType(), pkt->isRouteDirect() ? "D" : "F", pkt->payload_len); - - if (pkt->getPayloadType() == PAYLOAD_TYPE_PATH || pkt->getPayloadType() == PAYLOAD_TYPE_REQ - || pkt->getPayloadType() == PAYLOAD_TYPE_RESPONSE || pkt->getPayloadType() == PAYLOAD_TYPE_TXT_MSG) { - f.printf(" [%02X -> %02X]\n", (uint32_t)pkt->payload[1], (uint32_t)pkt->payload[0]); - } else { - f.printf("\n"); - } - f.close(); - } - } - } - void logTxFail(mesh::Packet* pkt, int len) override { - if (_logging) { - File f = openAppend(PACKET_LOG_FILE); - if (f) { - f.print(getLogDateTime()); - f.printf(": TX FAIL!, len=%d (type=%d, route=%s, payload_len=%d)\n", - len, pkt->getPayloadType(), pkt->isRouteDirect() ? "D" : "F", pkt->payload_len); - f.close(); - } - } - } - - int calcRxDelay(float score, uint32_t air_time) const override { - if (_prefs.rx_delay_base <= 0.0f) return 0; - return (int) ((pow(_prefs.rx_delay_base, 0.85f - score) - 1.0) * air_time); - } - - uint32_t getRetransmitDelay(const mesh::Packet* packet) override { - uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + packet->payload_len + 2) * _prefs.tx_delay_factor); - return getRNG()->nextInt(0, 6)*t; - } - uint32_t getDirectRetransmitDelay(const mesh::Packet* packet) override { - uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + packet->payload_len + 2) * _prefs.direct_tx_delay_factor); - return getRNG()->nextInt(0, 6)*t; - } - int getInterferenceThreshold() const override { - return _prefs.interference_threshold; - } - int getAGCResetInterval() const override { - return ((int)_prefs.agc_reset_interval) * 4000; // milliseconds - } - uint8_t getExtraAckTransmitCount() const override { - return _prefs.multi_acks; - } - - 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); - - bool is_admin; - data[len] = 0; // ensure null terminator - if (strcmp((char *) &data[4], _prefs.password) == 0) { // check for valid password - is_admin = true; - } else if (strcmp((char *) &data[4], _prefs.guest_password) == 0) { // check guest password - is_admin = false; - } else { -#if MESH_DEBUG - MESH_DEBUG_PRINTLN("Invalid password: %s", &data[4]); -#endif - return; - } - - auto client = putClient(sender); // add to known clients (if not already known) - if (timestamp <= client->last_timestamp) { - MESH_DEBUG_PRINTLN("Possible login replay attack!"); - return; // FATAL: client table is full -OR- replay attack - } - - MESH_DEBUG_PRINTLN("Login success!"); - 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 -#if 0 - memcpy(&reply_data[4], "OK", 2); // legacy response -#else - reply_data[4] = RESP_SERVER_LOGIN_OK; - reply_data[5] = 0; // NEW: recommended keep-alive interval (secs / 16) - reply_data[6] = is_admin ? 1 : 0; - reply_data[7] = 0; // FUTURE: reserved - getRNG()->random(&reply_data[8], 4); // random blob to help packet-hash uniqueness -#endif - - 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, client->secret, packet->path, packet->path_len, - PAYLOAD_TYPE_RESPONSE, reply_data, 12); - if (path) sendFlood(path, SERVER_RESPONSE_DELAY); - } else { - mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, client->secret, reply_data, 12); - if (reply) { - if (client->out_path_len >= 0) { // we have an out_path, so send DIRECT - sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY); - } else { - sendFlood(reply, SERVER_RESPONSE_DELAY); - } - } - } - } - } - - int matching_peer_indexes[MAX_CLIENTS]; - - int searchPeersByHash(const uint8_t* hash) override { - int n = 0; - for (int i = 0; i < MAX_CLIENTS; i++) { - if (known_clients[i].id.isHashMatch(hash)) { - matching_peer_indexes[n++] = i; // store the INDEXES of matching contacts (for subsequent 'peer' methods) - } - } - return n; - } - - void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) override { - int i = matching_peer_indexes[peer_idx]; - if (i >= 0 && i < MAX_CLIENTS) { - // lookup pre-calculated shared_secret - memcpy(dest_secret, known_clients[i].secret, PUB_KEY_SIZE); - } else { - MESH_DEBUG_PRINTLN("getPeerSharedSecret: Invalid peer idx: %d", i); - } - } - - void onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, uint32_t timestamp, const uint8_t* app_data, size_t app_data_len) { - mesh::Mesh::onAdvertRecv(packet, id, timestamp, app_data, app_data_len); // chain to super impl - - // if this a zero hop advert, add it to neighbours - if (packet->path_len == 0) { - AdvertDataParser parser(app_data, app_data_len); - if (parser.isValid() && parser.getType() == ADV_TYPE_REPEATER) { // just keep neigbouring Repeaters - putNeighbour(id, timestamp, packet->getSNR()); - } - } - } - - void onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_idx, const uint8_t* secret, uint8_t* data, size_t len) override { - int i = matching_peer_indexes[sender_idx]; - if (i < 0 || i >= MAX_CLIENTS) { // get from our known_clients table (sender SHOULD already be known in this context) - MESH_DEBUG_PRINTLN("onPeerDataRecv: invalid peer idx: %d", i); - return; - } - auto client = &known_clients[i]; - if (type == PAYLOAD_TYPE_REQ) { // request (from a Known admin client!) - uint32_t timestamp; - memcpy(×tamp, data, 4); - - if (timestamp > client->last_timestamp) { // prevent replay attacks - int reply_len = handleRequest(client, timestamp, &data[4], len - 4); - if (reply_len == 0) return; // invalid command - - client->last_timestamp = timestamp; - client->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(client->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, client->id, secret, reply_data, reply_len); - if (reply) { - if (client->out_path_len >= 0) { // we have an out_path, so send DIRECT - sendDirect(reply, client->out_path, client->out_path_len, 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 && client->is_admin) { // 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_PLAIN || flags == TXT_TYPE_CLI_DATA)) { - MESH_DEBUG_PRINTLN("onPeerDataRecv: unsupported text type received: flags=%02x", (uint32_t)flags); - } else if (sender_timestamp >= client->last_timestamp) { // prevent replay attacks - bool is_retry = (sender_timestamp == client->last_timestamp); - client->last_timestamp = sender_timestamp; - client->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 - - if (flags == TXT_TYPE_PLAIN) { // for legacy CLI, send Acks - uint32_t ack_hash; // calc truncated hash of the message timestamp + text + sender pub_key, to prove to sender that we got it - mesh::Utils::sha256((uint8_t *) &ack_hash, 4, data, 5 + strlen((char *)&data[5]), client->id.pub_key, PUB_KEY_SIZE); - - mesh::Packet* ack = createAck(ack_hash); - if (ack) { - if (client->out_path_len < 0) { - sendFlood(ack, TXT_ACK_DELAY); - } else { - sendDirect(ack, client->out_path, client->out_path_len, TXT_ACK_DELAY); - } - } - } - - uint8_t temp[166]; - char *command = (char *) &data[5]; - char *reply = (char *) &temp[5]; - if (is_retry) { - *reply = 0; - } else { - 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); // NOTE: legacy was: TXT_TYPE_PLAIN - - auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, secret, temp, 5 + text_len); - if (reply) { - if (client->out_path_len < 0) { - sendFlood(reply, CLI_REPLY_DELAY_MILLIS); - } else { - sendDirect(reply, client->out_path, client->out_path_len, CLI_REPLY_DELAY_MILLIS); - } - } - } - } else { - MESH_DEBUG_PRINTLN("onPeerDataRecv: possible replay attack detected"); - } - } - } - - 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 { - // TODO: prevent replay attacks - int i = matching_peer_indexes[sender_idx]; - - if (i >= 0 && i < MAX_CLIENTS) { // get from our known_clients table (sender SHOULD already be known in this context) - MESH_DEBUG_PRINTLN("PATH to client, path_len=%d", (uint32_t) path_len); - auto client = &known_clients[i]; - memcpy(client->out_path, path, client->out_path_len = path_len); // store a copy of path, for sendDirect() - } else { - MESH_DEBUG_PRINTLN("onPeerPathRecv: invalid peer idx: %d", i); - } - - // NOTE: no reciprocal path send!! - return false; - } - -public: - MyMesh(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) -#if defined(WITH_RS232_BRIDGE) - , bridge(WITH_RS232_BRIDGE, _mgr, &rtc) -#elif defined(WITH_ESPNOW_BRIDGE) - , bridge(_mgr, &rtc) -#endif - { - memset(known_clients, 0, sizeof(known_clients)); - next_local_advert = next_flood_advert = 0; - set_radio_at = revert_radio_at = 0; - _logging = false; - -#if MAX_NEIGHBOURS - memset(neighbours, 0, sizeof(neighbours)); -#endif - - // 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 = 12; // 12 hours - _prefs.flood_max = 64; - _prefs.interference_threshold = 0; // disabled - } - - void begin(FILESYSTEM* fs) { - mesh::Mesh::begin(); - _fs = fs; - // load persisted prefs - _cli.loadPrefs(_fs); - - #ifdef WITH_BRIDGE - bridge.begin(); - #endif - - radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); - radio_set_tx_power(_prefs.tx_power_dbm); - - updateAdvertTimer(); - updateFloodAdvertTimer(); - } - - 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); - } - - void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) override { - set_radio_at = futureMillis(2000); // give CLI reply some time to be sent back, before applying temp radio params - pending_freq = freq; - pending_bw = bw; - pending_sf = sf; - pending_cr = cr; - - revert_radio_at = futureMillis(2000 + timeout_mins*60*1000); // schedule when to revert radio params - } - - bool formatFileSystem() override { -#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 sendSelfAdvertisement(int delay_millis) override { - mesh::Packet* pkt = createSelfAdvert(); - if (pkt) { - sendFlood(pkt, delay_millis); - } else { - MESH_DEBUG_PRINTLN("ERROR: unable to create advertisement packet!"); - } - } - - void updateAdvertTimer() override { - 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 updateFloodAdvertTimer() override { - 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 setLoggingOn(bool enable) override { _logging = enable; } - - void eraseLogFile() override { - _fs->remove(PACKET_LOG_FILE); - } - - void dumpLogFile() override { -#if defined(RP2040_PLATFORM) - File f = _fs->open(PACKET_LOG_FILE, "r"); -#else - File f = _fs->open(PACKET_LOG_FILE); -#endif - if (f) { - while (f.available()) { - int c = f.read(); - if (c < 0) break; - Serial.print((char)c); - } - f.close(); - } - } - - void setTxPower(uint8_t power_dbm) override { - radio_set_tx_power(power_dbm); - } - - void formatNeighborsReply(char *reply) override { - char *dp = reply; - -#if MAX_NEIGHBOURS - for (int i = 0; i < MAX_NEIGHBOURS && dp - reply < 134; i++) { - NeighbourInfo* neighbour = &neighbours[i]; - if (neighbour->heard_timestamp == 0) continue; // skip empty slots - - // add new line if not first item - if (i > 0) *dp++ = '\n'; - - char hex[10]; - // get 4 bytes of neighbour id as hex - mesh::Utils::toHex(hex, neighbour->id.pub_key, 4); - - // add next neighbour - uint32_t secs_ago = getRTCClock()->getCurrentTime() - neighbour->heard_timestamp; - sprintf(dp, "%s:%d:%d", hex, secs_ago, neighbour->snr); - while (*dp) dp++; // find end of string - } -#endif - if (dp == reply) { // no neighbours, need empty response - strcpy(dp, "-none-"); dp += 6; - } - *dp = 0; // null terminator - } - - void removeNeighbor(const uint8_t* pubkey, int key_len) override { -#if MAX_NEIGHBOURS - for (int i = 0; i < MAX_NEIGHBOURS; i++) { - NeighbourInfo* neighbour = &neighbours[i]; - if(memcmp(neighbour->id.pub_key, pubkey, key_len) == 0){ - neighbours[i] = NeighbourInfo(); // clear neighbour entry - } - } -#endif - } - - mesh::LocalIdentity& getSelfId() override { return self_id; } - - void saveIdentity(const mesh::LocalIdentity& new_id) override { - self_id = new_id; -#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) - IdentityStore store(*_fs, ""); -#elif defined(ESP32) - IdentityStore store(*_fs, "/identity"); -#elif defined(RP2040_PLATFORM) - IdentityStore store(*_fs, "/identity"); -#else - #error "need to define saveIdentity()" -#endif - store.save("_main", self_id); - } - - void clearStats() override { - radio_driver.resetStats(); - resetStats(); - ((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() { -#ifdef WITH_BRIDGE - bridge.loop(); -#endif - - 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 - } - - if (set_radio_at && millisHasNowPassed(set_radio_at)) { // apply pending (temporary) radio params - set_radio_at = 0; // clear timer - radio_set_params(pending_freq, pending_bw, pending_sf, pending_cr); - MESH_DEBUG_PRINTLN("Temp radio params"); - } - - if (revert_radio_at && millisHasNowPassed(revert_radio_at)) { // revert radio params to orig - revert_radio_at = 0; // clear timer - radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); - MESH_DEBUG_PRINTLN("Radio params restored"); - } - - #ifdef DISPLAY_CLASS - ui_task.loop(); - #endif - } -}; - StdRNG fast_rng; SimpleMeshTables tables; @@ -932,4 +111,7 @@ void loop() { the_mesh.loop(); sensors.loop(); +#ifdef DISPLAY_CLASS + ui_task.loop(); +#endif } diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp new file mode 100644 index 00000000..83d18137 --- /dev/null +++ b/examples/simple_room_server/MyMesh.cpp @@ -0,0 +1,809 @@ +#include "MyMesh.h" + +#define REPLY_DELAY_MILLIS 1500 +#define PUSH_NOTIFY_DELAY_MILLIS 2000 +#define SYNC_PUSH_INTERVAL 1200 + +#define PUSH_ACK_TIMEOUT_FLOOD 12000 +#define PUSH_TIMEOUT_BASE 4000 +#define PUSH_ACK_TIMEOUT_FACTOR 2000 + +#define POST_SYNC_DELAY_SECS 6 + +#define CLIENT_KEEP_ALIVE_SECS 0 // Now Disabled (was 128) + +#define REQ_TYPE_GET_STATUS 0x01 // same as _GET_STATS +#define REQ_TYPE_KEEP_ALIVE 0x02 +#define REQ_TYPE_GET_TELEMETRY_DATA 0x03 + +#define RESP_SERVER_LOGIN_OK 0 // response to ANON_REQ + +struct ServerStats { + uint16_t batt_milli_volts; + uint16_t curr_tx_queue_len; + int16_t noise_floor; + int16_t last_rssi; + uint32_t n_packets_recv; + uint32_t n_packets_sent; + uint32_t total_air_time_secs; + uint32_t total_up_time_secs; + uint32_t n_sent_flood, n_sent_direct; + uint32_t n_recv_flood, n_recv_direct; + uint16_t err_events; // was 'n_full_events' + int16_t last_snr; // x 4 + uint16_t n_direct_dups, n_flood_dups; + uint16_t n_posted, n_post_push; +}; + +ClientInfo *MyMesh::putClient(const mesh::Identity &id) { + for (int i = 0; i < num_clients; i++) { + if (id.matches(known_clients[i].id)) return &known_clients[i]; // already known + } + ClientInfo *newClient; + if (num_clients < MAX_CLIENTS) { + newClient = &known_clients[num_clients++]; + } else { // table is currently full + // evict least active client + uint32_t oldest_timestamp = 0xFFFFFFFF; + newClient = &known_clients[0]; + for (int i = 0; i < num_clients; i++) { + auto c = &known_clients[i]; + if (c->last_activity < oldest_timestamp) { + oldest_timestamp = c->last_activity; + newClient = c; + } + } + } + newClient->id = id; + newClient->out_path_len = -1; // initially out_path is unknown + newClient->last_timestamp = 0; + return newClient; +} + +void MyMesh::evict(ClientInfo *client) { + client->last_activity = 0; // this slot will now be re-used (will be oldest) + memset(client->id.pub_key, 0, sizeof(client->id.pub_key)); + memset(client->secret, 0, sizeof(client->secret)); + client->pending_ack = 0; +} + +void MyMesh::addPost(ClientInfo *client, const char *postData) { + // TODO: suggested postData format: /<descrption> + posts[next_post_idx].author = client->id; // add to cyclic queue + StrHelper::strncpy(posts[next_post_idx].text, postData, MAX_POST_TEXT_LEN); + + posts[next_post_idx].post_timestamp = getRTCClock()->getCurrentTimeUnique(); + next_post_idx = (next_post_idx + 1) % MAX_UNSYNCED_POSTS; + + next_push = futureMillis(PUSH_NOTIFY_DELAY_MILLIS); + _num_posted++; // stats +} + +void MyMesh::pushPostToClient(ClientInfo *client, PostInfo &post) { + int len = 0; + memcpy(&reply_data[len], &post.post_timestamp, 4); + len += 4; // this is a PAST timestamp... but should be accepted by client + + uint8_t attempt; + getRNG()->random(&attempt, 1); // need this for re-tries, so packet hash (and ACK) will be different + reply_data[len++] = (TXT_TYPE_SIGNED_PLAIN << 2) | (attempt & 3); // 'signed' plain text + + // encode prefix of post.author.pub_key + memcpy(&reply_data[len], post.author.pub_key, 4); + len += 4; // just first 4 bytes + + int text_len = strlen(post.text); + memcpy(&reply_data[len], post.text, text_len); + len += text_len; + + // calc expected ACK reply + mesh::Utils::sha256((uint8_t *)&client->pending_ack, 4, reply_data, len, client->id.pub_key, PUB_KEY_SIZE); + client->push_post_timestamp = post.post_timestamp; + + auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, client->secret, reply_data, len); + if (reply) { + if (client->out_path_len < 0) { + sendFlood(reply); + client->ack_timeout = futureMillis(PUSH_ACK_TIMEOUT_FLOOD); + } else { + sendDirect(reply, client->out_path, client->out_path_len); + client->ack_timeout = + futureMillis(PUSH_TIMEOUT_BASE + PUSH_ACK_TIMEOUT_FACTOR * (client->out_path_len + 1)); + } + _num_post_pushes++; // stats + } else { + client->pending_ack = 0; + MESH_DEBUG_PRINTLN("Unable to push post to client"); + } +} + +uint8_t MyMesh::getUnsyncedCount(ClientInfo *client) { + uint8_t count = 0; + for (int k = 0; k < MAX_UNSYNCED_POSTS; k++) { + if (posts[k].post_timestamp > client->sync_since // is new post for this Client? + && !posts[k].author.matches(client->id)) { // don't push posts to the author + count++; + } + } + return count; +} + +bool MyMesh::processAck(const uint8_t *data) { + for (int i = 0; i < num_clients; i++) { + auto client = &known_clients[i]; + if (client->pending_ack && memcmp(data, &client->pending_ack, 4) == 0) { // got an ACK from Client! + client->pending_ack = 0; // clear this, so next push can happen + client->push_failures = 0; + client->sync_since = client->push_post_timestamp; // advance Client's SINCE timestamp, to sync next post + return true; + } + } + return false; +} + +mesh::Packet *MyMesh::createSelfAdvert() { + uint8_t app_data[MAX_ADVERT_DATA_SIZE]; + uint8_t app_data_len; + { + AdvertDataBuilder builder(ADV_TYPE_ROOM, _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); +} + +File MyMesh::openAppend(const char *fname) { +#if defined(NRF52_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 +} + +int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t *payload, + size_t payload_len) { + // uint32_t now = getRTCClock()->getCurrentTimeUnique(); + // memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp + memcpy(reply_data, &sender_timestamp, 4); // reflect sender_timestamp back in response packet (kind of like a 'tag') + + switch (payload[0]) { + case REQ_TYPE_GET_STATUS: { + ServerStats stats; + stats.batt_milli_volts = board.getBattMilliVolts(); + stats.curr_tx_queue_len = _mgr->getOutboundCount(0xFFFFFFFF); + stats.noise_floor = (int16_t)_radio->getNoiseFloor(); + stats.last_rssi = (int16_t)radio_driver.getLastRSSI(); + stats.n_packets_recv = radio_driver.getPacketsRecv(); + stats.n_packets_sent = radio_driver.getPacketsSent(); + stats.total_air_time_secs = getTotalAirTime() / 1000; + stats.total_up_time_secs = _ms->getMillis() / 1000; + stats.n_sent_flood = getNumSentFlood(); + stats.n_sent_direct = getNumSentDirect(); + stats.n_recv_flood = getNumRecvFlood(); + stats.n_recv_direct = getNumRecvDirect(); + stats.err_events = _err_flags; + stats.last_snr = (int16_t)(radio_driver.getLastSNR() * 4); + stats.n_direct_dups = ((SimpleMeshTables *)getTables())->getNumDirectDups(); + stats.n_flood_dups = ((SimpleMeshTables *)getTables())->getNumFloodDups(); + stats.n_posted = _num_posted; + stats.n_post_push = _num_post_pushes; + + memcpy(&reply_data[4], &stats, sizeof(stats)); + return 4 + sizeof(stats); + } + + case REQ_TYPE_GET_TELEMETRY_DATA: { + uint8_t perm_mask = ~(payload[1]); // NEW: first reserved byte (of 4), is now inverse mask to apply to permissions + + telemetry.reset(); + telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f); + // query other sensors -- target specific + sensors.querySensors((sender->permission == RoomPermission::ADMIN ? 0xFF : 0x00) & perm_mask, telemetry); + + uint8_t tlen = telemetry.getSize(); + memcpy(&reply_data[4], telemetry.getBuffer(), tlen); + return 4 + tlen; // reply_len + } + } + return 0; // unknown command +} + +void MyMesh::logRxRaw(float snr, float rssi, const uint8_t raw[], int len) { +#if MESH_PACKET_LOGGING + Serial.print(getLogDateTime()); + Serial.print(" RAW: "); + mesh::Utils::printHex(Serial, raw, len); + Serial.println(); +#endif +} + +void MyMesh::logRx(mesh::Packet *pkt, int len, float score) { + if (_logging) { + File f = openAppend(PACKET_LOG_FILE); + if (f) { + f.print(getLogDateTime()); + f.printf(": RX, len=%d (type=%d, route=%s, payload_len=%d) SNR=%d RSSI=%d score=%d", len, + pkt->getPayloadType(), pkt->isRouteDirect() ? "D" : "F", pkt->payload_len, + (int)_radio->getLastSNR(), (int)_radio->getLastRSSI(), (int)(score * 1000)); + + if (pkt->getPayloadType() == PAYLOAD_TYPE_PATH || pkt->getPayloadType() == PAYLOAD_TYPE_REQ || + pkt->getPayloadType() == PAYLOAD_TYPE_RESPONSE || pkt->getPayloadType() == PAYLOAD_TYPE_TXT_MSG) { + f.printf(" [%02X -> %02X]\n", (uint32_t)pkt->payload[1], (uint32_t)pkt->payload[0]); + } else { + f.printf("\n"); + } + f.close(); + } + } +} +void MyMesh::logTx(mesh::Packet *pkt, int len) { + if (_logging) { + File f = openAppend(PACKET_LOG_FILE); + if (f) { + f.print(getLogDateTime()); + f.printf(": TX, len=%d (type=%d, route=%s, payload_len=%d)", len, pkt->getPayloadType(), + pkt->isRouteDirect() ? "D" : "F", pkt->payload_len); + + if (pkt->getPayloadType() == PAYLOAD_TYPE_PATH || pkt->getPayloadType() == PAYLOAD_TYPE_REQ || + pkt->getPayloadType() == PAYLOAD_TYPE_RESPONSE || pkt->getPayloadType() == PAYLOAD_TYPE_TXT_MSG) { + f.printf(" [%02X -> %02X]\n", (uint32_t)pkt->payload[1], (uint32_t)pkt->payload[0]); + } else { + f.printf("\n"); + } + f.close(); + } + } +} +void MyMesh::logTxFail(mesh::Packet *pkt, int len) { + if (_logging) { + File f = openAppend(PACKET_LOG_FILE); + if (f) { + f.print(getLogDateTime()); + f.printf(": TX FAIL!, len=%d (type=%d, route=%s, payload_len=%d)\n", len, pkt->getPayloadType(), + pkt->isRouteDirect() ? "D" : "F", pkt->payload_len); + f.close(); + } + } +} + +int MyMesh::calcRxDelay(float score, uint32_t air_time) const { + if (_prefs.rx_delay_base <= 0.0f) return 0; + return (int)((pow(_prefs.rx_delay_base, 0.85f - score) - 1.0) * air_time); +} + +const char *MyMesh::getLogDateTime() { + static char tmp[32]; + uint32_t now = getRTCClock()->getCurrentTime(); + DateTime dt = DateTime(now); + sprintf(tmp, "%02d:%02d:%02d - %d/%d/%d U", dt.hour(), dt.minute(), dt.second(), dt.day(), dt.month(), + dt.year()); + return tmp; +} + +uint32_t MyMesh::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 MyMesh::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; +} + +bool MyMesh::allowPacketForward(const mesh::Packet *packet) { + if (_prefs.disable_fwd) return false; + if (packet->isRouteFlood() && packet->path_len >= _prefs.flood_max) return false; + return true; +} + +void MyMesh::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 sender_timestamp, sender_sync_since; + memcpy(&sender_timestamp, data, 4); + memcpy(&sender_sync_since, &data[4], 4); // sender's "sync messags SINCE x" timestamp + + RoomPermission perm; + data[len] = 0; // ensure null terminator + if (strcmp((char *)&data[8], _prefs.password) == 0) { // check for valid admin password + perm = RoomPermission::ADMIN; + } else { + if (strcmp((char *)&data[8], _prefs.guest_password) == 0) { // check the room/public password + perm = RoomPermission::GUEST; + } else if (_prefs.allow_read_only) { + perm = RoomPermission::READ_ONLY; + } else { + MESH_DEBUG_PRINTLN("Incorrect room password"); + return; // no response. Client will timeout + } + } + + auto client = putClient(sender); // add to known clients (if not already known) + if (sender_timestamp <= client->last_timestamp) { + MESH_DEBUG_PRINTLN("possible replay attack!"); + return; + } + + MESH_DEBUG_PRINTLN("Login success!"); + client->permission = perm; + client->last_timestamp = sender_timestamp; + 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; + + now = getRTCClock()->getCurrentTimeUnique(); + memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp + // TODO: maybe reply with count of messages waiting to be synced for THIS client? + reply_data[4] = RESP_SERVER_LOGIN_OK; + reply_data[5] = (CLIENT_KEEP_ALIVE_SECS >> 4); // NEW: recommended keep-alive interval (secs / 16) + reply_data[6] = (perm == RoomPermission::ADMIN ? 1 : (perm == RoomPermission::GUEST ? 0 : 2)); + reply_data[7] = getUnsyncedCount(client); // NEW + memcpy(&reply_data[8], "OK", 2); // REVISIT: not really needed + + next_push = futureMillis(PUSH_NOTIFY_DELAY_MILLIS); // delay next push, give RESPONSE packet time to arrive first + + 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, client->secret, packet->path, packet->path_len, + PAYLOAD_TYPE_RESPONSE, reply_data, 8 + 2); + if (path) sendFlood(path, SERVER_RESPONSE_DELAY); + } else { + mesh::Packet *reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, client->secret, reply_data, 8 + 2); + if (reply) { + if (client->out_path_len >= 0) { // we have an out_path, so send DIRECT + sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY); + } else { + sendFlood(reply, SERVER_RESPONSE_DELAY); + } + } + } + } +} + +int MyMesh::searchPeersByHash(const uint8_t *hash) { + int n = 0; + for (int i = 0; i < num_clients; i++) { + if (known_clients[i].id.isHashMatch(hash)) { + matching_peer_indexes[n++] = i; // store the INDEXES of matching contacts (for subsequent 'peer' methods) + } + } + return n; +} + +void MyMesh::getPeerSharedSecret(uint8_t *dest_secret, int peer_idx) { + int i = matching_peer_indexes[peer_idx]; + if (i >= 0 && i < num_clients) { + // lookup pre-calculated shared_secret + memcpy(dest_secret, known_clients[i].secret, PUB_KEY_SIZE); + } else { + MESH_DEBUG_PRINTLN("getPeerSharedSecret: Invalid peer idx: %d", i); + } +} + +void MyMesh::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_clients) { // get from our known_clients table (sender SHOULD already be known in this context) + MESH_DEBUG_PRINTLN("onPeerDataRecv: invalid peer idx: %d", i); + return; + } + auto client = &known_clients[i]; + if (type == PAYLOAD_TYPE_TXT_MSG && len > 5) { // a CLI command or new Post + 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_PLAIN || flags == TXT_TYPE_CLI_DATA)) { + MESH_DEBUG_PRINTLN("onPeerDataRecv: unsupported command flags received: flags=%02x", (uint32_t)flags); + } else if (sender_timestamp >= client->last_timestamp) { // prevent replay attacks, but send Acks for retries + bool is_retry = (sender_timestamp == client->last_timestamp); + client->last_timestamp = sender_timestamp; + + uint32_t now = getRTCClock()->getCurrentTimeUnique(); + client->last_activity = now; + client->push_failures = 0; // reset so push can resume (if prev failed) + + // 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 + + uint32_t ack_hash; // calc truncated hash of the message timestamp + text + sender pub_key, to prove to + // sender that we got it + mesh::Utils::sha256((uint8_t *)&ack_hash, 4, data, 5 + strlen((char *)&data[5]), client->id.pub_key, + PUB_KEY_SIZE); + + uint8_t temp[166]; + bool send_ack; + if (flags == TXT_TYPE_CLI_DATA) { + if (client->permission == RoomPermission::ADMIN) { + if (is_retry) { + temp[5] = 0; // no reply + } else { + 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; + } else { + temp[5] = 0; // no reply + send_ack = false; // and no ACK... user shoudn't be sending these + } + } else { // TXT_TYPE_PLAIN + if (client->permission == RoomPermission::READ_ONLY) { + temp[5] = 0; // no reply + send_ack = false; // no ACK + } else { + if (!is_retry) { + addPost(client, (const char *)&data[5]); + } + temp[5] = 0; // no reply (ACK is enough) + send_ack = true; + } + } + + uint32_t delay_millis; + if (send_ack) { + if (client->out_path_len < 0) { + mesh::Packet *ack = createAck(ack_hash); + if (ack) sendFlood(ack, TXT_ACK_DELAY); + delay_millis = TXT_ACK_DELAY + REPLY_DELAY_MILLIS; + } else { + uint32_t d = TXT_ACK_DELAY; + if (getExtraAckTransmitCount() > 0) { + mesh::Packet *a1 = createMultiAck(ack_hash, 1); + if (a1) sendDirect(a1, client->out_path, client->out_path_len, d); + d += 300; + } + + mesh::Packet *a2 = createAck(ack_hash); + if (a2) sendDirect(a2, client->out_path, client->out_path_len, d); + delay_millis = d + REPLY_DELAY_MILLIS; + } + } else { + delay_millis = 0; + } + + int text_len = strlen((char *)&temp[5]); + if (text_len > 0) { + if (now == sender_timestamp) { + // WORKAROUND: the two timestamps need to be different, in the CLI view + now++; + } + memcpy(temp, &now, 4); // mostly an extra blob to help make packet_hash unique + + // calc expected ACK reply + // mesh::Utils::sha256((uint8_t *)&expected_ack_crc, 4, temp, 5 + text_len, self_id.pub_key, + // PUB_KEY_SIZE); + + auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, secret, temp, 5 + text_len); + if (reply) { + if (client->out_path_len < 0) { + sendFlood(reply, delay_millis + SERVER_RESPONSE_DELAY); + } else { + sendDirect(reply, client->out_path, client->out_path_len, delay_millis + SERVER_RESPONSE_DELAY); + } + } + } + } else { + MESH_DEBUG_PRINTLN("onPeerDataRecv: possible replay attack detected"); + } + } else if (type == PAYLOAD_TYPE_REQ && len >= 5) { + uint32_t sender_timestamp; + memcpy(&sender_timestamp, data, 4); // timestamp (by sender's RTC clock - which could be wrong) + if (sender_timestamp < client->last_timestamp) { // prevent replay attacks + MESH_DEBUG_PRINTLN("onPeerDataRecv: possible replay attack detected"); + } else { + client->last_timestamp = sender_timestamp; + + uint32_t now = getRTCClock()->getCurrentTime(); + client->last_activity = now; // <-- THIS will keep client connection alive + client->push_failures = 0; // reset so push can resume (if prev failed) + + if (data[4] == REQ_TYPE_KEEP_ALIVE && packet->isRouteDirect()) { // request type + uint32_t forceSince = 0; + if (len >= 9) { // optional - last post_timestamp client received + memcpy(&forceSince, &data[5], 4); // NOTE: this may be 0, if part of decrypted PADDING! + } else { + memcpy(&data[5], &forceSince, 4); // make sure there are zeroes in payload (for ack_hash calc below) + } + if (forceSince > 0) { + client->sync_since = forceSince; // force-update the 'sync since' + } + + client->pending_ack = 0; + + // TODO: Throttle KEEP_ALIVE requests! + // if client sends too quickly, evict() + + // RULE: only send keep_alive response DIRECT! + if (client->out_path_len >= 0) { + uint32_t ack_hash; // calc ACK to prove to sender that we got request + mesh::Utils::sha256((uint8_t *)&ack_hash, 4, data, 9, client->id.pub_key, PUB_KEY_SIZE); + + auto reply = createAck(ack_hash); + if (reply) { + reply->payload[reply->payload_len++] = getUnsyncedCount(client); // NEW: add unsynced counter to end of ACK packet + sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY); + } + } + } else { + int reply_len = handleRequest(client, sender_timestamp, &data[4], len - 4); + if (reply_len > 0) { // valid command + if (packet->isRouteFlood()) { + // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response + mesh::Packet *path = createPathReturn(client->id, secret, packet->path, packet->path_len, + PAYLOAD_TYPE_RESPONSE, reply_data, reply_len); + if (path) sendFlood(path, SERVER_RESPONSE_DELAY); + } else { + mesh::Packet *reply = createDatagram(PAYLOAD_TYPE_RESPONSE, client->id, secret, reply_data, reply_len); + if (reply) { + if (client->out_path_len >= 0) { // we have an out_path, so send DIRECT + sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY); + } else { + sendFlood(reply, SERVER_RESPONSE_DELAY); + } + } + } + } + } + } + } +} + +bool MyMesh::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) { + // TODO: prevent replay attacks + int i = matching_peer_indexes[sender_idx]; + + if (i >= 0 && i < num_clients) { // get from our known_clients table (sender SHOULD already be known in this context) + MESH_DEBUG_PRINTLN("PATH to client, path_len=%d", (uint32_t)path_len); + auto client = &known_clients[i]; + memcpy(client->out_path, path, client->out_path_len = path_len); // store a copy of path, for sendDirect() + } else { + MESH_DEBUG_PRINTLN("onPeerPathRecv: invalid peer idx: %d", i); + } + + if (extra_type == PAYLOAD_TYPE_ACK && extra_len >= 4) { + // also got an encoded ACK! + processAck(extra); + } + + // NOTE: no reciprocal path send!! + return false; +} + +void MyMesh::onAckRecv(mesh::Packet *packet, uint32_t ack_crc) { + if (processAck((uint8_t *)&ack_crc)) { + packet->markDoNotRetransmit(); // ACK was for this node, so don't retransmit + } +} + +MyMesh::MyMesh(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) { + next_local_advert = next_flood_advert = 0; + _logging = false; + set_radio_at = revert_radio_at = 0; + + // defaults + memset(&_prefs, 0, sizeof(_prefs)); + _prefs.airtime_factor = 1.0; // one half + _prefs.rx_delay_base = 0.0f; // 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.disable_fwd = 1; + _prefs.advert_interval = 1; // default to 2 minutes for NEW installs + _prefs.flood_advert_interval = 12; // 12 hours + _prefs.flood_max = 64; + _prefs.interference_threshold = 0; // disabled +#ifdef ROOM_PASSWORD + StrHelper::strncpy(_prefs.guest_password, ROOM_PASSWORD, sizeof(_prefs.guest_password)); +#endif + + num_clients = 0; + next_post_idx = 0; + next_client_idx = 0; + next_push = 0; + memset(posts, 0, sizeof(posts)); + _num_posted = _num_post_pushes = 0; +} + +void MyMesh::begin(FILESYSTEM *fs) { + mesh::Mesh::begin(); + _fs = fs; + // load persisted prefs + _cli.loadPrefs(_fs); + + radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); + radio_set_tx_power(_prefs.tx_power_dbm); + + updateAdvertTimer(); + updateFloodAdvertTimer(); +} + +void MyMesh::applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) { + set_radio_at = futureMillis(2000); // give CLI reply some time to be sent back, before applying temp radio params + pending_freq = freq; + pending_bw = bw; + pending_sf = sf; + pending_cr = cr; + + revert_radio_at = futureMillis(2000 + timeout_mins * 60 * 1000); // schedule when to revert radio params +} + +bool MyMesh::formatFileSystem() { +#if defined(NRF52_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 MyMesh::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 MyMesh::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 MyMesh::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 MyMesh::dumpLogFile() { +#if defined(RP2040_PLATFORM) + File f = _fs->open(PACKET_LOG_FILE, "r"); +#else + File f = _fs->open(PACKET_LOG_FILE); +#endif + if (f) { + while (f.available()) { + int c = f.read(); + if (c < 0) break; + Serial.print((char)c); + } + f.close(); + } +} + +void MyMesh::setTxPower(uint8_t power_dbm) { + radio_set_tx_power(power_dbm); +} + +void MyMesh::saveIdentity(const mesh::LocalIdentity &new_id) { + self_id = new_id; +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + IdentityStore store(*_fs, ""); +#elif defined(ESP32) + IdentityStore store(*_fs, "/identity"); +#elif defined(RP2040_PLATFORM) + IdentityStore store(*_fs, "/identity"); +#else +#error "need to define saveIdentity()" +#endif + store.save("_main", self_id); +} + +void MyMesh::clearStats() { + radio_driver.resetStats(); + resetStats(); + ((SimpleMeshTables *)getTables())->resetStats(); +} + +void MyMesh::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 MyMesh::loop() { + mesh::Mesh::loop(); + + if (millisHasNowPassed(next_push) && num_clients > 0) { + // check for ACK timeouts + for (int i = 0; i < num_clients; i++) { + auto c = &known_clients[i]; + if (c->pending_ack && millisHasNowPassed(c->ack_timeout)) { + c->push_failures++; + c->pending_ack = 0; // reset (TODO: keep prev expected_ack's in a list, incase they arrive LATER, after we retry) + MESH_DEBUG_PRINTLN("pending ACK timed out: push_failures: %d", (uint32_t)c->push_failures); + } + } + // check next Round-Robin client, and sync next new post + auto client = &known_clients[next_client_idx]; + bool did_push = false; + if (client->pending_ack == 0 && client->last_activity != 0 && + client->push_failures < 3) { // not already waiting for ACK, AND not evicted, AND retries not max + MESH_DEBUG_PRINTLN("loop - checking for client %02X", (uint32_t)client->id.pub_key[0]); + uint32_t now = getRTCClock()->getCurrentTime(); + for (int k = 0, idx = next_post_idx; k < MAX_UNSYNCED_POSTS; k++) { + auto p = &posts[idx]; + if (now >= p->post_timestamp + POST_SYNC_DELAY_SECS && + p->post_timestamp > client->sync_since // is new post for this Client? + && !p->author.matches(client->id)) { // don't push posts to the author + // push this post to Client, then wait for ACK + pushPostToClient(client, *p); + did_push = true; + MESH_DEBUG_PRINTLN("loop - pushed to client %02X: %s", (uint32_t)client->id.pub_key[0], p->text); + break; + } + idx = (idx + 1) % MAX_UNSYNCED_POSTS; // wrap to start of cyclic queue + } + } else { + MESH_DEBUG_PRINTLN("loop - skipping busy (or evicted) client %02X", (uint32_t)client->id.pub_key[0]); + } + next_client_idx = (next_client_idx + 1) % num_clients; // round robin polling for each client + + if (did_push) { + next_push = futureMillis(SYNC_PUSH_INTERVAL); + } else { + // were no unsynced posts for curr client, so proccess next client much quicker! (in next loop()) + next_push = futureMillis(SYNC_PUSH_INTERVAL / 8); + } + } + + 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 + } + + if (set_radio_at && millisHasNowPassed(set_radio_at)) { // apply pending (temporary) radio params + set_radio_at = 0; // clear timer + radio_set_params(pending_freq, pending_bw, pending_sf, pending_cr); + MESH_DEBUG_PRINTLN("Temp radio params"); + } + + if (revert_radio_at && millisHasNowPassed(revert_radio_at)) { // revert radio params to orig + revert_radio_at = 0; // clear timer + radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); + MESH_DEBUG_PRINTLN("Radio params restored"); + } + + // TODO: periodically check for OLD/inactive entries in known_clients[], and evict +} diff --git a/examples/simple_room_server/MyMesh.h b/examples/simple_room_server/MyMesh.h new file mode 100644 index 00000000..945cca7d --- /dev/null +++ b/examples/simple_room_server/MyMesh.h @@ -0,0 +1,220 @@ +#pragma once + +#include <Arduino.h> // needed for PlatformIO +#include <Mesh.h> + +#if defined(NRF52_PLATFORM) + #include <InternalFileSystem.h> +#elif defined(RP2040_PLATFORM) + #include <LittleFS.h> +#elif defined(ESP32) + #include <SPIFFS.h> +#endif + +#include <helpers/ArduinoHelpers.h> +#include <helpers/StaticPoolPacketManager.h> +#include <helpers/SimpleMeshTables.h> +#include <helpers/IdentityStore.h> +#include <helpers/AdvertDataHelpers.h> +#include <helpers/TxtDataHelpers.h> +#include <helpers/CommonCLI.h> +#include <RTClib.h> +#include <target.h> + +/* ------------------------------ Config -------------------------------- */ + +#ifndef FIRMWARE_BUILD_DATE + #define FIRMWARE_BUILD_DATE "1 Sep 2025" +#endif + +#ifndef FIRMWARE_VERSION + #define FIRMWARE_VERSION "v1.8.1" +#endif + +#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 "Test BBS" +#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 MAX_CLIENTS + #define MAX_CLIENTS 32 +#endif + +#ifndef MAX_UNSYNCED_POSTS + #define MAX_UNSYNCED_POSTS 32 +#endif + +#ifndef SERVER_RESPONSE_DELAY + #define SERVER_RESPONSE_DELAY 300 +#endif + +#ifndef TXT_ACK_DELAY + #define TXT_ACK_DELAY 200 +#endif + +#define FIRMWARE_ROLE "room_server" + +#define PACKET_LOG_FILE "/packet_log" + +enum RoomPermission { + ADMIN, + GUEST, + READ_ONLY +}; + +struct ClientInfo { + mesh::Identity id; + uint32_t last_timestamp; // by THEIR clock + uint32_t last_activity; // by OUR clock + uint32_t sync_since; // sync messages SINCE this timestamp (by OUR clock) + uint32_t pending_ack; + uint32_t push_post_timestamp; + unsigned long ack_timeout; + RoomPermission permission; + uint8_t push_failures; + uint8_t secret[PUB_KEY_SIZE]; + int out_path_len; + uint8_t out_path[MAX_PATH_SIZE]; +}; + +#define MAX_POST_TEXT_LEN (160-9) + +struct PostInfo { + mesh::Identity author; + uint32_t post_timestamp; // by OUR clock + char text[MAX_POST_TEXT_LEN+1]; +}; + +class MyMesh : public mesh::Mesh, public CommonCLICallbacks { + FILESYSTEM* _fs; + unsigned long next_local_advert, next_flood_advert; + bool _logging; + NodePrefs _prefs; + CommonCLI _cli; + uint8_t reply_data[MAX_PACKET_PAYLOAD]; + int num_clients; + ClientInfo known_clients[MAX_CLIENTS]; + unsigned long next_push; + uint16_t _num_posted, _num_post_pushes; + int next_client_idx; // for round-robin polling + int next_post_idx; + PostInfo posts[MAX_UNSYNCED_POSTS]; // cyclic queue + CayenneLPP telemetry; + unsigned long set_radio_at, revert_radio_at; + float pending_freq; + float pending_bw; + uint8_t pending_sf; + uint8_t pending_cr; + int matching_peer_indexes[MAX_CLIENTS]; + + ClientInfo* putClient(const mesh::Identity& id); + void evict(ClientInfo* client); + void addPost(ClientInfo* client, const char* postData); + void pushPostToClient(ClientInfo* client, PostInfo& post); + uint8_t getUnsyncedCount(ClientInfo* client); + bool processAck(const uint8_t *data); + mesh::Packet* createSelfAdvert(); + File openAppend(const char* fname); + int handleRequest(ClientInfo* sender, uint32_t sender_timestamp, uint8_t* payload, size_t payload_len); + +protected: + float getAirtimeBudgetFactor() const override { + return _prefs.airtime_factor; + } + + void logRxRaw(float snr, float rssi, const uint8_t raw[], int len) override; + void logRx(mesh::Packet* pkt, int len, float score) override; + void logTx(mesh::Packet* pkt, int len) override; + void logTxFail(mesh::Packet* pkt, int len) override; + + int calcRxDelay(float score, uint32_t air_time) const override; + const char* getLogDateTime() override; + uint32_t getRetransmitDelay(const mesh::Packet* packet) override; + uint32_t getDirectRetransmitDelay(const mesh::Packet* packet) override; + + int getInterferenceThreshold() const override { + return _prefs.interference_threshold; + } + int getAGCResetInterval() const override { + return ((int)_prefs.agc_reset_interval) * 4000; // milliseconds + } + uint8_t getExtraAckTransmitCount() const override { + return _prefs.multi_acks; + } + + bool allowPacketForward(const mesh::Packet* packet) 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; + +public: + MyMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables); + + void begin(FILESYSTEM* fs); + + 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); + } + + void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) override; + bool formatFileSystem() override; + void sendSelfAdvertisement(int delay_millis) override; + void updateAdvertTimer() override; + void updateFloodAdvertTimer() override; + + void setLoggingOn(bool enable) override { _logging = enable; } + + void eraseLogFile() override { + _fs->remove(PACKET_LOG_FILE); + } + + void dumpLogFile() override; + void setTxPower(uint8_t power_dbm) override; + + void formatNeighborsReply(char *reply) override { + strcpy(reply, "not supported"); + } + + mesh::LocalIdentity& getSelfId() override { return self_id; } + + void saveIdentity(const mesh::LocalIdentity& new_id) override; + void clearStats() override; + void handleCommand(uint32_t sender_timestamp, char* command, char* reply); + void loop(); +}; diff --git a/examples/simple_room_server/main.cpp b/examples/simple_room_server/main.cpp index aa9c8e37..8f6b6d58 100644 --- a/examples/simple_room_server/main.cpp +++ b/examples/simple_room_server/main.cpp @@ -1,979 +1,13 @@ #include <Arduino.h> // needed for PlatformIO #include <Mesh.h> -#if defined(NRF52_PLATFORM) - #include <InternalFileSystem.h> -#elif defined(RP2040_PLATFORM) - #include <LittleFS.h> -#elif defined(ESP32) - #include <SPIFFS.h> -#endif - -#include <helpers/ArduinoHelpers.h> -#include <helpers/StaticPoolPacketManager.h> -#include <helpers/SimpleMeshTables.h> -#include <helpers/IdentityStore.h> -#include <helpers/AdvertDataHelpers.h> -#include <helpers/TxtDataHelpers.h> -#include <helpers/CommonCLI.h> -#include <RTClib.h> -#include <target.h> - -/* ------------------------------ Config -------------------------------- */ - -#ifndef FIRMWARE_BUILD_DATE - #define FIRMWARE_BUILD_DATE "1 Sep 2025" -#endif - -#ifndef FIRMWARE_VERSION - #define FIRMWARE_VERSION "v1.8.1" -#endif - -#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 "Test BBS" -#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 MAX_CLIENTS - #define MAX_CLIENTS 32 -#endif - -#ifndef MAX_UNSYNCED_POSTS - #define MAX_UNSYNCED_POSTS 32 -#endif - -#ifndef SERVER_RESPONSE_DELAY - #define SERVER_RESPONSE_DELAY 300 -#endif - -#ifndef TXT_ACK_DELAY - #define TXT_ACK_DELAY 200 -#endif +#include "MyMesh.h" #ifdef DISPLAY_CLASS #include "UITask.h" static UITask ui_task(display); #endif -#define FIRMWARE_ROLE "room_server" - -#define PACKET_LOG_FILE "/packet_log" - -/* ------------------------------ Code -------------------------------- */ - -enum RoomPermission { - ADMIN, - GUEST, - READ_ONLY -}; - -struct ClientInfo { - mesh::Identity id; - uint32_t last_timestamp; // by THEIR clock - uint32_t last_activity; // by OUR clock - uint32_t sync_since; // sync messages SINCE this timestamp (by OUR clock) - uint32_t pending_ack; - uint32_t push_post_timestamp; - unsigned long ack_timeout; - RoomPermission permission; - uint8_t push_failures; - uint8_t secret[PUB_KEY_SIZE]; - int out_path_len; - uint8_t out_path[MAX_PATH_SIZE]; -}; - -#define MAX_POST_TEXT_LEN (160-9) - -struct PostInfo { - mesh::Identity author; - uint32_t post_timestamp; // by OUR clock - char text[MAX_POST_TEXT_LEN+1]; -}; - -#define REPLY_DELAY_MILLIS 1500 -#define PUSH_NOTIFY_DELAY_MILLIS 2000 -#define SYNC_PUSH_INTERVAL 1200 - -#define PUSH_ACK_TIMEOUT_FLOOD 12000 -#define PUSH_TIMEOUT_BASE 4000 -#define PUSH_ACK_TIMEOUT_FACTOR 2000 - -#define POST_SYNC_DELAY_SECS 6 - -#define CLIENT_KEEP_ALIVE_SECS 0 // Now Disabled (was 128) - -#define REQ_TYPE_GET_STATUS 0x01 // same as _GET_STATS -#define REQ_TYPE_KEEP_ALIVE 0x02 -#define REQ_TYPE_GET_TELEMETRY_DATA 0x03 - -#define RESP_SERVER_LOGIN_OK 0 // response to ANON_REQ - -struct ServerStats { - uint16_t batt_milli_volts; - uint16_t curr_tx_queue_len; - int16_t noise_floor; - int16_t last_rssi; - uint32_t n_packets_recv; - uint32_t n_packets_sent; - uint32_t total_air_time_secs; - uint32_t total_up_time_secs; - uint32_t n_sent_flood, n_sent_direct; - uint32_t n_recv_flood, n_recv_direct; - uint16_t err_events; // was 'n_full_events' - int16_t last_snr; // x 4 - uint16_t n_direct_dups, n_flood_dups; - uint16_t n_posted, n_post_push; -}; - -class MyMesh : public mesh::Mesh, public CommonCLICallbacks { - FILESYSTEM* _fs; - unsigned long next_local_advert, next_flood_advert; - bool _logging; - NodePrefs _prefs; - CommonCLI _cli; - uint8_t reply_data[MAX_PACKET_PAYLOAD]; - int num_clients; - ClientInfo known_clients[MAX_CLIENTS]; - unsigned long next_push; - uint16_t _num_posted, _num_post_pushes; - int next_client_idx; // for round-robin polling - int next_post_idx; - PostInfo posts[MAX_UNSYNCED_POSTS]; // cyclic queue - CayenneLPP telemetry; - unsigned long set_radio_at, revert_radio_at; - float pending_freq; - float pending_bw; - uint8_t pending_sf; - uint8_t pending_cr; - - ClientInfo* putClient(const mesh::Identity& id) { - for (int i = 0; i < num_clients; i++) { - if (id.matches(known_clients[i].id)) return &known_clients[i]; // already known - } - ClientInfo* newClient; - if (num_clients < MAX_CLIENTS) { - newClient = &known_clients[num_clients++]; - } else { // table is currently full - // evict least active client - uint32_t oldest_timestamp = 0xFFFFFFFF; - newClient = &known_clients[0]; - for (int i = 0; i < num_clients; i++) { - auto c = &known_clients[i]; - if (c->last_activity < oldest_timestamp) { - oldest_timestamp = c->last_activity; - newClient = c; - } - } - } - newClient->id = id; - newClient->out_path_len = -1; // initially out_path is unknown - newClient->last_timestamp = 0; - return newClient; - } - - void evict(ClientInfo* client) { - client->last_activity = 0; // this slot will now be re-used (will be oldest) - memset(client->id.pub_key, 0, sizeof(client->id.pub_key)); - memset(client->secret, 0, sizeof(client->secret)); - client->pending_ack = 0; - } - - void addPost(ClientInfo* client, const char* postData) { - // TODO: suggested postData format: <title>/<descrption> - posts[next_post_idx].author = client->id; // add to cyclic queue - StrHelper::strncpy(posts[next_post_idx].text, postData, MAX_POST_TEXT_LEN); - - posts[next_post_idx].post_timestamp = getRTCClock()->getCurrentTimeUnique(); - next_post_idx = (next_post_idx + 1) % MAX_UNSYNCED_POSTS; - - next_push = futureMillis(PUSH_NOTIFY_DELAY_MILLIS); - _num_posted++; // stats - } - - void pushPostToClient(ClientInfo* client, PostInfo& post) { - int len = 0; - memcpy(&reply_data[len], &post.post_timestamp, 4); len += 4; // this is a PAST timestamp... but should be accepted by client - - uint8_t attempt; - getRNG()->random(&attempt, 1); // need this for re-tries, so packet hash (and ACK) will be different - reply_data[len++] = (TXT_TYPE_SIGNED_PLAIN << 2) | (attempt & 3); // 'signed' plain text - - // encode prefix of post.author.pub_key - memcpy(&reply_data[len], post.author.pub_key, 4); len += 4; // just first 4 bytes - - int text_len = strlen(post.text); - memcpy(&reply_data[len], post.text, text_len); len += text_len; - - // calc expected ACK reply - mesh::Utils::sha256((uint8_t *)&client->pending_ack, 4, reply_data, len, client->id.pub_key, PUB_KEY_SIZE); - client->push_post_timestamp = post.post_timestamp; - - auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, client->secret, reply_data, len); - if (reply) { - if (client->out_path_len < 0) { - sendFlood(reply); - client->ack_timeout = futureMillis(PUSH_ACK_TIMEOUT_FLOOD); - } else { - sendDirect(reply, client->out_path, client->out_path_len); - client->ack_timeout = futureMillis(PUSH_TIMEOUT_BASE + PUSH_ACK_TIMEOUT_FACTOR * (client->out_path_len + 1)); - } - _num_post_pushes++; // stats - } else { - client->pending_ack = 0; - MESH_DEBUG_PRINTLN("Unable to push post to client"); - } - } - - uint8_t getUnsyncedCount(ClientInfo* client) { - uint8_t count = 0; - for (int k = 0; k < MAX_UNSYNCED_POSTS; k++) { - if (posts[k].post_timestamp > client->sync_since // is new post for this Client? - && !posts[k].author.matches(client->id)) { // don't push posts to the author - count++; - } - } - return count; - } - - bool processAck(const uint8_t *data) { - for (int i = 0; i < num_clients; i++) { - auto client = &known_clients[i]; - if (client->pending_ack && memcmp(data, &client->pending_ack, 4) == 0) { // got an ACK from Client! - client->pending_ack = 0; // clear this, so next push can happen - client->push_failures = 0; - client->sync_since = client->push_post_timestamp; // advance Client's SINCE timestamp, to sync next post - return true; - } - } - return false; - } - - mesh::Packet* createSelfAdvert() { - uint8_t app_data[MAX_ADVERT_DATA_SIZE]; - uint8_t app_data_len; - { - AdvertDataBuilder builder(ADV_TYPE_ROOM, _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); - } - - File openAppend(const char* fname) { - #if defined(NRF52_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 - } - - int handleRequest(ClientInfo* sender, uint32_t sender_timestamp, uint8_t* payload, size_t payload_len) { - // uint32_t now = getRTCClock()->getCurrentTimeUnique(); - // memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp - memcpy(reply_data, &sender_timestamp, 4); // reflect sender_timestamp back in response packet (kind of like a 'tag') - - switch (payload[0]) { - case REQ_TYPE_GET_STATUS: { - ServerStats stats; - stats.batt_milli_volts = board.getBattMilliVolts(); - stats.curr_tx_queue_len = _mgr->getOutboundCount(0xFFFFFFFF); - stats.noise_floor = (int16_t)_radio->getNoiseFloor(); - stats.last_rssi = (int16_t) radio_driver.getLastRSSI(); - stats.n_packets_recv = radio_driver.getPacketsRecv(); - stats.n_packets_sent = radio_driver.getPacketsSent(); - stats.total_air_time_secs = getTotalAirTime() / 1000; - stats.total_up_time_secs = _ms->getMillis() / 1000; - stats.n_sent_flood = getNumSentFlood(); - stats.n_sent_direct = getNumSentDirect(); - stats.n_recv_flood = getNumRecvFlood(); - stats.n_recv_direct = getNumRecvDirect(); - stats.err_events = _err_flags; - stats.last_snr = (int16_t)(radio_driver.getLastSNR() * 4); - stats.n_direct_dups = ((SimpleMeshTables *)getTables())->getNumDirectDups(); - stats.n_flood_dups = ((SimpleMeshTables *)getTables())->getNumFloodDups(); - stats.n_posted = _num_posted; - stats.n_post_push = _num_post_pushes; - - memcpy(&reply_data[4], &stats, sizeof(stats)); - return 4 + sizeof(stats); - } - - case REQ_TYPE_GET_TELEMETRY_DATA: { - uint8_t perm_mask = ~(payload[1]); // NEW: first reserved byte (of 4), is now inverse mask to apply to permissions - - telemetry.reset(); - telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f); - // query other sensors -- target specific - sensors.querySensors((sender->permission == RoomPermission::ADMIN ? 0xFF : 0x00) & perm_mask, telemetry); - - uint8_t tlen = telemetry.getSize(); - memcpy(&reply_data[4], telemetry.getBuffer(), tlen); - return 4 + tlen; // reply_len - } - } - return 0; // unknown command - } - -protected: - float getAirtimeBudgetFactor() const override { - return _prefs.airtime_factor; - } - - void logRxRaw(float snr, float rssi, const uint8_t raw[], int len) override { - #if MESH_PACKET_LOGGING - Serial.print(getLogDateTime()); - Serial.print(" RAW: "); - mesh::Utils::printHex(Serial, raw, len); - Serial.println(); - #endif - } - - void logRx(mesh::Packet* pkt, int len, float score) override { - if (_logging) { - File f = openAppend(PACKET_LOG_FILE); - if (f) { - f.print(getLogDateTime()); - f.printf(": RX, len=%d (type=%d, route=%s, payload_len=%d) SNR=%d RSSI=%d score=%d", - len, pkt->getPayloadType(), pkt->isRouteDirect() ? "D" : "F", pkt->payload_len, - (int)_radio->getLastSNR(), (int)_radio->getLastRSSI(), (int)(score*1000)); - - if (pkt->getPayloadType() == PAYLOAD_TYPE_PATH || pkt->getPayloadType() == PAYLOAD_TYPE_REQ - || pkt->getPayloadType() == PAYLOAD_TYPE_RESPONSE || pkt->getPayloadType() == PAYLOAD_TYPE_TXT_MSG) { - f.printf(" [%02X -> %02X]\n", (uint32_t)pkt->payload[1], (uint32_t)pkt->payload[0]); - } else { - f.printf("\n"); - } - f.close(); - } - } - } - void logTx(mesh::Packet* pkt, int len) override { - if (_logging) { - File f = openAppend(PACKET_LOG_FILE); - if (f) { - f.print(getLogDateTime()); - f.printf(": TX, len=%d (type=%d, route=%s, payload_len=%d)", - len, pkt->getPayloadType(), pkt->isRouteDirect() ? "D" : "F", pkt->payload_len); - - if (pkt->getPayloadType() == PAYLOAD_TYPE_PATH || pkt->getPayloadType() == PAYLOAD_TYPE_REQ - || pkt->getPayloadType() == PAYLOAD_TYPE_RESPONSE || pkt->getPayloadType() == PAYLOAD_TYPE_TXT_MSG) { - f.printf(" [%02X -> %02X]\n", (uint32_t)pkt->payload[1], (uint32_t)pkt->payload[0]); - } else { - f.printf("\n"); - } - f.close(); - } - } - } - void logTxFail(mesh::Packet* pkt, int len) override { - if (_logging) { - File f = openAppend(PACKET_LOG_FILE); - if (f) { - f.print(getLogDateTime()); - f.printf(": TX FAIL!, len=%d (type=%d, route=%s, payload_len=%d)\n", - len, pkt->getPayloadType(), pkt->isRouteDirect() ? "D" : "F", pkt->payload_len); - f.close(); - } - } - } - - int calcRxDelay(float score, uint32_t air_time) const override { - if (_prefs.rx_delay_base <= 0.0f) return 0; - return (int) ((pow(_prefs.rx_delay_base, 0.85f - score) - 1.0) * air_time); - } - - const char* getLogDateTime() override { - static char tmp[32]; - uint32_t now = getRTCClock()->getCurrentTime(); - DateTime dt = DateTime(now); - sprintf(tmp, "%02d:%02d:%02d - %d/%d/%d U", dt.hour(), dt.minute(), dt.second(), dt.day(), dt.month(), dt.year()); - return tmp; - } - - uint32_t getRetransmitDelay(const mesh::Packet* packet) override { - uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + packet->payload_len + 2) * _prefs.tx_delay_factor); - return getRNG()->nextInt(0, 6)*t; - } - uint32_t getDirectRetransmitDelay(const mesh::Packet* packet) override { - uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + packet->payload_len + 2) * _prefs.direct_tx_delay_factor); - return getRNG()->nextInt(0, 6)*t; - } - int getInterferenceThreshold() const override { - return _prefs.interference_threshold; - } - int getAGCResetInterval() const override { - return ((int)_prefs.agc_reset_interval) * 4000; // milliseconds - } - uint8_t getExtraAckTransmitCount() const override { - return _prefs.multi_acks; - } - - bool allowPacketForward(const mesh::Packet* packet) override { - if (_prefs.disable_fwd) return false; - if (packet->isRouteFlood() && packet->path_len >= _prefs.flood_max) return false; - return true; - } - - 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 - - RoomPermission perm; - data[len] = 0; // ensure null terminator - if (strcmp((char *) &data[8], _prefs.password) == 0) { // check for valid admin password - perm = RoomPermission::ADMIN; - } else { - if (strcmp((char *) &data[8], _prefs.guest_password) == 0) { // check the room/public password - perm = RoomPermission::GUEST; - } else if (_prefs.allow_read_only) { - perm = RoomPermission::READ_ONLY; - } else { - MESH_DEBUG_PRINTLN("Incorrect room password"); - return; // no response. Client will timeout - } - } - - auto client = putClient(sender); // add to known clients (if not already known) - if (sender_timestamp <= client->last_timestamp) { - MESH_DEBUG_PRINTLN("possible replay attack!"); - return; - } - - MESH_DEBUG_PRINTLN("Login success!"); - client->permission = perm; - client->last_timestamp = sender_timestamp; - 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; - - now = getRTCClock()->getCurrentTimeUnique(); - memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp - // TODO: maybe reply with count of messages waiting to be synced for THIS client? - reply_data[4] = RESP_SERVER_LOGIN_OK; - reply_data[5] = (CLIENT_KEEP_ALIVE_SECS >> 4); // NEW: recommended keep-alive interval (secs / 16) - reply_data[6] = (perm == RoomPermission::ADMIN ? 1 : (perm == RoomPermission::GUEST ? 0 : 2)); - reply_data[7] = getUnsyncedCount(client); // NEW - memcpy(&reply_data[8], "OK", 2); // REVISIT: not really needed - - next_push = futureMillis(PUSH_NOTIFY_DELAY_MILLIS); // delay next push, give RESPONSE packet time to arrive first - - 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, client->secret, packet->path, packet->path_len, - PAYLOAD_TYPE_RESPONSE, reply_data, 8 + 2); - if (path) sendFlood(path, SERVER_RESPONSE_DELAY); - } else { - mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, client->secret, reply_data, 8 + 2); - if (reply) { - if (client->out_path_len >= 0) { // we have an out_path, so send DIRECT - sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY); - } else { - sendFlood(reply, SERVER_RESPONSE_DELAY); - } - } - } - } - } - - int matching_peer_indexes[MAX_CLIENTS]; - - int searchPeersByHash(const uint8_t* hash) override { - int n = 0; - for (int i = 0; i < num_clients; i++) { - if (known_clients[i].id.isHashMatch(hash)) { - matching_peer_indexes[n++] = i; // store the INDEXES of matching contacts (for subsequent 'peer' methods) - } - } - return n; - } - - void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) override { - int i = matching_peer_indexes[peer_idx]; - if (i >= 0 && i < num_clients) { - // lookup pre-calculated shared_secret - memcpy(dest_secret, known_clients[i].secret, PUB_KEY_SIZE); - } else { - MESH_DEBUG_PRINTLN("getPeerSharedSecret: Invalid peer idx: %d", i); - } - } - - void onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_idx, const uint8_t* secret, uint8_t* data, size_t len) override { - int i = matching_peer_indexes[sender_idx]; - if (i < 0 || i >= num_clients) { // get from our known_clients table (sender SHOULD already be known in this context) - MESH_DEBUG_PRINTLN("onPeerDataRecv: invalid peer idx: %d", i); - return; - } - auto client = &known_clients[i]; - if (type == PAYLOAD_TYPE_TXT_MSG && len > 5) { // a CLI command or new Post - 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_PLAIN || flags == TXT_TYPE_CLI_DATA)) { - MESH_DEBUG_PRINTLN("onPeerDataRecv: unsupported command flags received: flags=%02x", (uint32_t)flags); - } else if (sender_timestamp >= client->last_timestamp) { // prevent replay attacks, but send Acks for retries - bool is_retry = (sender_timestamp == client->last_timestamp); - client->last_timestamp = sender_timestamp; - - uint32_t now = getRTCClock()->getCurrentTimeUnique(); - client->last_activity = now; - client->push_failures = 0; // reset so push can resume (if prev failed) - - // 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 - - uint32_t ack_hash; // calc truncated hash of the message timestamp + text + sender pub_key, to prove to sender that we got it - mesh::Utils::sha256((uint8_t *) &ack_hash, 4, data, 5 + strlen((char *)&data[5]), client->id.pub_key, PUB_KEY_SIZE); - - uint8_t temp[166]; - bool send_ack; - if (flags == TXT_TYPE_CLI_DATA) { - if (client->permission == RoomPermission::ADMIN) { - if (is_retry) { - temp[5] = 0; // no reply - } else { - 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; - } else { - temp[5] = 0; // no reply - send_ack = false; // and no ACK... user shoudn't be sending these - } - } else { // TXT_TYPE_PLAIN - if (client->permission == RoomPermission::READ_ONLY) { - temp[5] = 0; // no reply - send_ack = false; // no ACK - } else { - if (!is_retry) { - addPost(client, (const char *) &data[5]); - } - temp[5] = 0; // no reply (ACK is enough) - send_ack = true; - } - } - - uint32_t delay_millis; - if (send_ack) { - if (client->out_path_len < 0) { - mesh::Packet* ack = createAck(ack_hash); - if (ack) sendFlood(ack, TXT_ACK_DELAY); - delay_millis = TXT_ACK_DELAY + REPLY_DELAY_MILLIS; - } else { - uint32_t d = TXT_ACK_DELAY; - if (getExtraAckTransmitCount() > 0) { - mesh::Packet* a1 = createMultiAck(ack_hash, 1); - if (a1) sendDirect(a1, client->out_path, client->out_path_len, d); - d += 300; - } - - mesh::Packet* a2 = createAck(ack_hash); - if (a2) sendDirect(a2, client->out_path, client->out_path_len, d); - delay_millis = d + REPLY_DELAY_MILLIS; - } - } else { - delay_millis = 0; - } - - int text_len = strlen((char *) &temp[5]); - if (text_len > 0) { - if (now == sender_timestamp) { - // WORKAROUND: the two timestamps need to be different, in the CLI view - now++; - } - memcpy(temp, &now, 4); // mostly an extra blob to help make packet_hash unique - - // calc expected ACK reply - //mesh::Utils::sha256((uint8_t *)&expected_ack_crc, 4, temp, 5 + text_len, self_id.pub_key, PUB_KEY_SIZE); - - auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, secret, temp, 5 + text_len); - if (reply) { - if (client->out_path_len < 0) { - sendFlood(reply, delay_millis + SERVER_RESPONSE_DELAY); - } else { - sendDirect(reply, client->out_path, client->out_path_len, delay_millis + SERVER_RESPONSE_DELAY); - } - } - } - } else { - MESH_DEBUG_PRINTLN("onPeerDataRecv: possible replay attack detected"); - } - } else if (type == PAYLOAD_TYPE_REQ && len >= 5) { - uint32_t sender_timestamp; - memcpy(&sender_timestamp, data, 4); // timestamp (by sender's RTC clock - which could be wrong) - if (sender_timestamp < client->last_timestamp) { // prevent replay attacks - MESH_DEBUG_PRINTLN("onPeerDataRecv: possible replay attack detected"); - } else { - client->last_timestamp = sender_timestamp; - - uint32_t now = getRTCClock()->getCurrentTime(); - client->last_activity = now; // <-- THIS will keep client connection alive - client->push_failures = 0; // reset so push can resume (if prev failed) - - if (data[4] == REQ_TYPE_KEEP_ALIVE && packet->isRouteDirect()) { // request type - uint32_t forceSince = 0; - if (len >= 9) { // optional - last post_timestamp client received - memcpy(&forceSince, &data[5], 4); // NOTE: this may be 0, if part of decrypted PADDING! - } else { - memcpy(&data[5], &forceSince, 4); // make sure there are zeroes in payload (for ack_hash calc below) - } - if (forceSince > 0) { - client->sync_since = forceSince; // force-update the 'sync since' - } - - client->pending_ack = 0; - - // TODO: Throttle KEEP_ALIVE requests! - // if client sends too quickly, evict() - - // RULE: only send keep_alive response DIRECT! - if (client->out_path_len >= 0) { - uint32_t ack_hash; // calc ACK to prove to sender that we got request - mesh::Utils::sha256((uint8_t *) &ack_hash, 4, data, 9, client->id.pub_key, PUB_KEY_SIZE); - - auto reply = createAck(ack_hash); - if (reply) { - reply->payload[reply->payload_len++] = getUnsyncedCount(client); // NEW: add unsynced counter to end of ACK packet - sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY); - } - } - } else { - int reply_len = handleRequest(client, sender_timestamp, &data[4], len - 4); - if (reply_len > 0) { // valid command - if (packet->isRouteFlood()) { - // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response - mesh::Packet* path = createPathReturn(client->id, secret, packet->path, packet->path_len, - PAYLOAD_TYPE_RESPONSE, reply_data, reply_len); - if (path) sendFlood(path, SERVER_RESPONSE_DELAY); - } else { - mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, client->id, secret, reply_data, reply_len); - if (reply) { - if (client->out_path_len >= 0) { // we have an out_path, so send DIRECT - sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY); - } else { - sendFlood(reply, SERVER_RESPONSE_DELAY); - } - } - } - } - } - } - } - } - - 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 { - // TODO: prevent replay attacks - int i = matching_peer_indexes[sender_idx]; - - if (i >= 0 && i < num_clients) { // get from our known_clients table (sender SHOULD already be known in this context) - MESH_DEBUG_PRINTLN("PATH to client, path_len=%d", (uint32_t) path_len); - auto client = &known_clients[i]; - memcpy(client->out_path, path, client->out_path_len = path_len); // store a copy of path, for sendDirect() - } else { - MESH_DEBUG_PRINTLN("onPeerPathRecv: invalid peer idx: %d", i); - } - - if (extra_type == PAYLOAD_TYPE_ACK && extra_len >= 4) { - // also got an encoded ACK! - processAck(extra); - } - - // NOTE: no reciprocal path send!! - return false; - } - - void onAckRecv(mesh::Packet* packet, uint32_t ack_crc) override { - if (processAck((uint8_t *)&ack_crc)) { - packet->markDoNotRetransmit(); // ACK was for this node, so don't retransmit - } - } - -public: - MyMesh(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) - { - next_local_advert = next_flood_advert = 0; - _logging = false; - set_radio_at = revert_radio_at = 0; - - // defaults - memset(&_prefs, 0, sizeof(_prefs)); - _prefs.airtime_factor = 1.0; // one half - _prefs.rx_delay_base = 0.0f; // 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.disable_fwd = 1; - _prefs.advert_interval = 1; // default to 2 minutes for NEW installs - _prefs.flood_advert_interval = 12; // 12 hours - _prefs.flood_max = 64; - _prefs.interference_threshold = 0; // disabled - #ifdef ROOM_PASSWORD - StrHelper::strncpy(_prefs.guest_password, ROOM_PASSWORD, sizeof(_prefs.guest_password)); - #endif - - num_clients = 0; - next_post_idx = 0; - next_client_idx = 0; - next_push = 0; - memset(posts, 0, sizeof(posts)); - _num_posted = _num_post_pushes = 0; - } - - void begin(FILESYSTEM* fs) { - mesh::Mesh::begin(); - _fs = fs; - // load persisted prefs - _cli.loadPrefs(_fs); - - radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); - radio_set_tx_power(_prefs.tx_power_dbm); - - updateAdvertTimer(); - updateFloodAdvertTimer(); - } - - 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); - } - - void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) override { - set_radio_at = futureMillis(2000); // give CLI reply some time to be sent back, before applying temp radio params - pending_freq = freq; - pending_bw = bw; - pending_sf = sf; - pending_cr = cr; - - revert_radio_at = futureMillis(2000 + timeout_mins*60*1000); // schedule when to revert radio params - } - - bool formatFileSystem() override { - #if defined(NRF52_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 sendSelfAdvertisement(int delay_millis) override { - mesh::Packet* pkt = createSelfAdvert(); - if (pkt) { - sendFlood(pkt, delay_millis); - } else { - MESH_DEBUG_PRINTLN("ERROR: unable to create advertisement packet!"); - } - } - - void updateAdvertTimer() override { - 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 updateFloodAdvertTimer() override { - 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 setLoggingOn(bool enable) override { _logging = enable; } - - void eraseLogFile() override { - _fs->remove(PACKET_LOG_FILE); - } - - void dumpLogFile() override { - #if defined(RP2040_PLATFORM) - File f = _fs->open(PACKET_LOG_FILE, "r"); - #else - File f = _fs->open(PACKET_LOG_FILE); - #endif - if (f) { - while (f.available()) { - int c = f.read(); - if (c < 0) break; - Serial.print((char)c); - } - f.close(); - } - } - - void setTxPower(uint8_t power_dbm) override { - radio_set_tx_power(power_dbm); - } - - void formatNeighborsReply(char *reply) override { - strcpy(reply, "not supported"); - } - - mesh::LocalIdentity& getSelfId() override { return self_id; } - - void saveIdentity(const mesh::LocalIdentity& new_id) override { - self_id = new_id; -#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) - IdentityStore store(*_fs, ""); -#elif defined(ESP32) - IdentityStore store(*_fs, "/identity"); -#elif defined(RP2040_PLATFORM) - IdentityStore store(*_fs, "/identity"); -#else - #error "need to define saveIdentity()" -#endif - store.save("_main", self_id); - } - - void clearStats() override { - radio_driver.resetStats(); - resetStats(); - ((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(); - - if (millisHasNowPassed(next_push) && num_clients > 0) { - // check for ACK timeouts - for (int i = 0; i < num_clients; i++) { - auto c = &known_clients[i]; - if (c->pending_ack && millisHasNowPassed(c->ack_timeout)) { - c->push_failures++; - c->pending_ack = 0; // reset (TODO: keep prev expected_ack's in a list, incase they arrive LATER, after we retry) - MESH_DEBUG_PRINTLN("pending ACK timed out: push_failures: %d", (uint32_t)c->push_failures); - } - } - // check next Round-Robin client, and sync next new post - auto client = &known_clients[next_client_idx]; - bool did_push = false; - if (client->pending_ack == 0 && client->last_activity != 0 && client->push_failures < 3) { // not already waiting for ACK, AND not evicted, AND retries not max - MESH_DEBUG_PRINTLN("loop - checking for client %02X", (uint32_t) client->id.pub_key[0]); - uint32_t now = getRTCClock()->getCurrentTime(); - for (int k = 0, idx = next_post_idx; k < MAX_UNSYNCED_POSTS; k++) { - auto p = &posts[idx]; - if (now >= p->post_timestamp + POST_SYNC_DELAY_SECS && p->post_timestamp > client->sync_since // is new post for this Client? - && !p->author.matches(client->id)) { // don't push posts to the author - // push this post to Client, then wait for ACK - pushPostToClient(client, *p); - did_push = true; - MESH_DEBUG_PRINTLN("loop - pushed to client %02X: %s", (uint32_t) client->id.pub_key[0], p->text); - break; - } - idx = (idx + 1) % MAX_UNSYNCED_POSTS; // wrap to start of cyclic queue - } - } else { - MESH_DEBUG_PRINTLN("loop - skipping busy (or evicted) client %02X", (uint32_t) client->id.pub_key[0]); - } - next_client_idx = (next_client_idx + 1) % num_clients; // round robin polling for each client - - if (did_push) { - next_push = futureMillis(SYNC_PUSH_INTERVAL); - } else { - // were no unsynced posts for curr client, so proccess next client much quicker! (in next loop()) - next_push = futureMillis(SYNC_PUSH_INTERVAL / 8); - } - } - - 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 - } - - if (set_radio_at && millisHasNowPassed(set_radio_at)) { // apply pending (temporary) radio params - set_radio_at = 0; // clear timer - radio_set_params(pending_freq, pending_bw, pending_sf, pending_cr); - MESH_DEBUG_PRINTLN("Temp radio params"); - } - - if (revert_radio_at && millisHasNowPassed(revert_radio_at)) { // revert radio params to orig - revert_radio_at = 0; // clear timer - radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); - MESH_DEBUG_PRINTLN("Radio params restored"); - } - - #ifdef DISPLAY_CLASS - ui_task.loop(); - #endif - - // TODO: periodically check for OLD/inactive entries in known_clients[], and evict - } -}; - StdRNG fast_rng; SimpleMeshTables tables; MyMesh the_mesh(board, radio_driver, *new ArduinoMillis(), fast_rng, rtc_clock, tables); @@ -1073,4 +107,7 @@ void loop() { the_mesh.loop(); sensors.loop(); +#ifdef DISPLAY_CLASS + ui_task.loop(); +#endif } diff --git a/variants/generic-e22/platformio.ini b/variants/generic-e22/platformio.ini index c9a67220..2f61f412 100644 --- a/variants/generic-e22/platformio.ini +++ b/variants/generic-e22/platformio.ini @@ -30,7 +30,7 @@ lib_deps = [env:Generic_E22_sx1262_repeater] extends = Generic_E22 build_src_filter = ${Generic_E22.build_src_filter} - +<../examples/simple_repeater/main.cpp> + +<../examples/simple_repeater/*.cpp> build_flags = ${Generic_E22.build_flags} -D RADIO_CLASS=CustomSX1262 @@ -51,7 +51,7 @@ lib_deps = ; extends = Generic_E22 ; build_src_filter = ${Generic_E22.build_src_filter} ; +<helpers/bridges/RS232Bridge.cpp> -; +<../examples/simple_repeater/main.cpp> +; +<../examples/simple_repeater/*.cpp> ; build_flags = ; ${Generic_E22.build_flags} ; -D RADIO_CLASS=CustomSX1262 @@ -75,7 +75,7 @@ lib_deps = extends = Generic_E22 build_src_filter = ${Generic_E22.build_src_filter} +<helpers/bridges/ESPNowBridge.cpp> - +<../examples/simple_repeater/main.cpp> + +<../examples/simple_repeater/*.cpp> build_flags = ${Generic_E22.build_flags} -D RADIO_CLASS=CustomSX1262 @@ -97,7 +97,7 @@ lib_deps = [env:Generic_E22_sx1268_repeater] extends = Generic_E22 build_src_filter = ${Generic_E22.build_src_filter} - +<../examples/simple_repeater/main.cpp> + +<../examples/simple_repeater/*.cpp> build_flags = ${Generic_E22.build_flags} -D RADIO_CLASS=CustomSX1268 @@ -118,7 +118,7 @@ lib_deps = ; extends = Generic_E22 ; build_src_filter = ${Generic_E22.build_src_filter} ; +<helpers/bridges/RS232Bridge.cpp> -; +<../examples/simple_repeater/main.cpp> +; +<../examples/simple_repeater/*.cpp> ; build_flags = ; ${Generic_E22.build_flags} ; -D RADIO_CLASS=CustomSX1268 @@ -142,7 +142,7 @@ lib_deps = extends = Generic_E22 build_src_filter = ${Generic_E22.build_src_filter} +<helpers/bridges/ESPNowBridge.cpp> - +<../examples/simple_repeater/main.cpp> + +<../examples/simple_repeater/*.cpp> build_flags = ${Generic_E22.build_flags} -D RADIO_CLASS=CustomSX1268 diff --git a/variants/generic_espnow/platformio.ini b/variants/generic_espnow/platformio.ini index 1c14dfee..db87a81b 100644 --- a/variants/generic_espnow/platformio.ini +++ b/variants/generic_espnow/platformio.ini @@ -44,7 +44,7 @@ build_flags = -D ADMIN_PASSWORD='"password"' -D MAX_NEIGHBOURS=8 build_src_filter = ${Generic_ESPNOW.build_src_filter} - +<../examples/simple_repeater/main.cpp> + +<../examples/simple_repeater/*.cpp> lib_deps = ${Generic_ESPNOW.lib_deps} ${esp32_ota.lib_deps} @@ -75,7 +75,7 @@ build_flags = -D ADMIN_PASSWORD='"password"' -D ROOM_PASSWORD='"hello"' build_src_filter = ${Generic_ESPNOW.build_src_filter} - +<../examples/simple_room_server/main.cpp> + +<../examples/simple_room_server/*.cpp> lib_deps = ${Generic_ESPNOW.lib_deps} ${esp32_ota.lib_deps} diff --git a/variants/lilygo_tlora_c6/platformio.ini b/variants/lilygo_tlora_c6/platformio.ini index 76a897d6..f8a935cd 100644 --- a/variants/lilygo_tlora_c6/platformio.ini +++ b/variants/lilygo_tlora_c6/platformio.ini @@ -33,7 +33,7 @@ build_src_filter = ${esp32c6_base.build_src_filter} [env:LilyGo_Tlora_C6_repeater] extends = tlora_c6 build_src_filter = ${tlora_c6.build_src_filter} - +<../examples/simple_repeater/main.cpp> + +<../examples/simple_repeater/*.cpp> build_flags = ${tlora_c6.build_flags} -D ADVERT_NAME='"Tlora C6 Repeater"' diff --git a/variants/rak3x72/platformio.ini b/variants/rak3x72/platformio.ini index 2fee1a76..da9b2f22 100644 --- a/variants/rak3x72/platformio.ini +++ b/variants/rak3x72/platformio.ini @@ -19,7 +19,7 @@ build_flags = ${rak3x72.build_flags} -D ADVERT_NAME='"RAK3x72 Repeater"' -D ADMIN_PASSWORD='"password"' build_src_filter = ${rak3x72.build_src_filter} - +<../examples/simple_repeater/main.cpp> + +<../examples/simple_repeater/*.cpp> [env:rak3x72-sensor] extends = rak3x72 diff --git a/variants/rak_wismesh_tag/platformio.ini b/variants/rak_wismesh_tag/platformio.ini index 572919eb..6b505073 100644 --- a/variants/rak_wismesh_tag/platformio.ini +++ b/variants/rak_wismesh_tag/platformio.ini @@ -35,6 +35,7 @@ build_src_filter = ${nrf52_base.build_src_filter} lib_deps = ${nrf52_base.lib_deps} ${sensor_base.lib_deps} + end2endzone/NonBlockingRTTTL@^1.3.0 [env:RAK_WisMesh_Tag_Repeater] extends = rak_wismesh_tag @@ -79,7 +80,6 @@ build_src_filter = ${rak_wismesh_tag.build_src_filter} lib_deps = ${rak_wismesh_tag.lib_deps} densaugeo/base64 @ ~1.4.0 - end2endzone/NonBlockingRTTTL@^1.3.0 [env:RAK_WisMesh_Tag_companion_radio_ble] extends = rak_wismesh_tag @@ -100,7 +100,6 @@ build_src_filter = ${rak_wismesh_tag.build_src_filter} lib_deps = ${rak4631.lib_deps} densaugeo/base64 @ ~1.4.0 - end2endzone/NonBlockingRTTTL@^1.3.0 [env:RAK_WisMesh_Tag_sensor] extends = rak4631 diff --git a/variants/sensecap_solar/platformio.ini b/variants/sensecap_solar/platformio.ini index 649ace84..ee9acf0d 100644 --- a/variants/sensecap_solar/platformio.ini +++ b/variants/sensecap_solar/platformio.ini @@ -57,7 +57,7 @@ build_flags = ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${SenseCap_Solar.build_src_filter} - +<../examples/simple_repeater/main.cpp> + +<../examples/simple_repeater/*.cpp> [env:SenseCap_Solar_room_server] extends = SenseCap_Solar @@ -70,7 +70,7 @@ build_flags = ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${SenseCap_Solar.build_src_filter} - +<../examples/simple_room_server/main.cpp> + +<../examples/simple_room_server/*.cpp> [env:SenseCap_Solar_companion_radio_ble] extends = SenseCap_Solar diff --git a/variants/tenstar_c3/platformio.ini b/variants/tenstar_c3/platformio.ini index c22b3771..25bf6713 100644 --- a/variants/tenstar_c3/platformio.ini +++ b/variants/tenstar_c3/platformio.ini @@ -26,7 +26,7 @@ build_src_filter = ${esp32_base.build_src_filter} [env:Tenstar_C3_Repeater_sx1262] extends = Tenstar_esp32_C3 build_src_filter = ${Tenstar_esp32_C3.build_src_filter} - +<../examples/simple_repeater/main.cpp> + +<../examples/simple_repeater/*.cpp> build_flags = ${Tenstar_esp32_C3.build_flags} -D RADIO_CLASS=CustomSX1262 @@ -48,7 +48,7 @@ lib_deps = ; extends = Tenstar_esp32_C3 ; build_src_filter = ${Tenstar_esp32_C3.build_src_filter} ; +<helpers/bridges/RS232Bridge.cpp> -; +<../examples/simple_repeater/main.cpp> +; +<../examples/simple_repeater/*.cpp> ; build_flags = ; ${Tenstar_esp32_C3.build_flags} ; -D RADIO_CLASS=CustomSX1262 @@ -73,7 +73,7 @@ lib_deps = extends = Tenstar_esp32_C3 build_src_filter = ${Tenstar_esp32_C3.build_src_filter} +<helpers/bridges/ESPNowBridge.cpp> - +<../examples/simple_repeater/main.cpp> + +<../examples/simple_repeater/*.cpp> build_flags = ${Tenstar_esp32_C3.build_flags} -D RADIO_CLASS=CustomSX1262 @@ -96,7 +96,7 @@ lib_deps = [env:Tenstar_C3_Repeater_sx1268] extends = Tenstar_esp32_C3 build_src_filter = ${Tenstar_esp32_C3.build_src_filter} - +<../examples/simple_repeater/main.cpp> + +<../examples/simple_repeater/*.cpp> build_flags = ${Tenstar_esp32_C3.build_flags} -D RADIO_CLASS=CustomSX1268 @@ -117,7 +117,7 @@ lib_deps = ; extends = Tenstar_esp32_C3 ; build_src_filter = ${Tenstar_esp32_C3.build_src_filter} ; +<helpers/bridges/RS232Bridge.cpp> -; +<../examples/simple_repeater/main.cpp> +; +<../examples/simple_repeater/*.cpp> ; build_flags = ; ${Tenstar_esp32_C3.build_flags} ; -D RADIO_CLASS=CustomSX1268 @@ -141,7 +141,7 @@ lib_deps = extends = Tenstar_esp32_C3 build_src_filter = ${Tenstar_esp32_C3.build_src_filter} +<helpers/bridges/ESPNowBridge.cpp> - +<../examples/simple_repeater/main.cpp> + +<../examples/simple_repeater/*.cpp> build_flags = ${Tenstar_esp32_C3.build_flags} -D RADIO_CLASS=CustomSX1268 diff --git a/variants/thinknode_m1/platformio.ini b/variants/thinknode_m1/platformio.ini index eeeb692e..7a8f5a3c 100644 --- a/variants/thinknode_m1/platformio.ini +++ b/variants/thinknode_m1/platformio.ini @@ -46,7 +46,7 @@ build_flags = ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${ThinkNode_M1.build_src_filter} - +<../examples/simple_repeater/main.cpp> + +<../examples/simple_repeater/*.cpp> lib_deps = ${ThinkNode_M1.lib_deps} @@ -61,7 +61,7 @@ build_flags = ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${ThinkNode_M1.build_src_filter} - +<../examples/simple_room_server/main.cpp> + +<../examples/simple_room_server/*.cpp> lib_deps = ${ThinkNode_M1.lib_deps} diff --git a/variants/wio-e5-dev/platformio.ini b/variants/wio-e5-dev/platformio.ini index ce96ad2a..350d79fb 100644 --- a/variants/wio-e5-dev/platformio.ini +++ b/variants/wio-e5-dev/platformio.ini @@ -20,7 +20,7 @@ build_flags = ${lora_e5.build_flags} -D ADVERT_NAME='"WIO-E5 Repeater"' -D ADMIN_PASSWORD='"password"' build_src_filter = ${lora_e5.build_src_filter} - +<../examples/simple_repeater/main.cpp> + +<../examples/simple_repeater/*.cpp> [env:wio-e5_companion_radio_usb] extends = lora_e5 diff --git a/variants/wio-e5-mini/platformio.ini b/variants/wio-e5-mini/platformio.ini index 3d98d93e..30c8ef87 100644 --- a/variants/wio-e5-mini/platformio.ini +++ b/variants/wio-e5-mini/platformio.ini @@ -23,7 +23,7 @@ build_flags = ${lora_e5_mini.build_flags} -D ADVERT_NAME='"wio-e5-mini Repeater"' -D ADMIN_PASSWORD='"password"' build_src_filter = ${lora_e5_mini.build_src_filter} - +<../examples/simple_repeater/main.cpp> + +<../examples/simple_repeater/*.cpp> [env:wio-e5-mini-sensor] extends = lora_e5_mini diff --git a/variants/xiao_c3/platformio.ini b/variants/xiao_c3/platformio.ini index de79fb10..e139fef3 100644 --- a/variants/xiao_c3/platformio.ini +++ b/variants/xiao_c3/platformio.ini @@ -22,7 +22,7 @@ build_src_filter = ${esp32_base.build_src_filter} [env:Xiao_C3_Repeater_sx1262] extends = Xiao_esp32_C3 build_src_filter = ${Xiao_esp32_C3.build_src_filter} - +<../examples/simple_repeater/main.cpp> + +<../examples/simple_repeater/*.cpp> build_flags = ${Xiao_esp32_C3.build_flags} -D RADIO_CLASS=CustomSX1262 diff --git a/variants/xiao_c6/platformio.ini b/variants/xiao_c6/platformio.ini index 24a17e06..83698d0a 100644 --- a/variants/xiao_c6/platformio.ini +++ b/variants/xiao_c6/platformio.ini @@ -33,7 +33,7 @@ build_src_filter = ${esp32c6_base.build_src_filter} [env:Xiao_C6_Repeater] extends = Xiao_C6 build_src_filter = ${Xiao_C6.build_src_filter} - +<../examples/simple_repeater/main.cpp> + +<../examples/simple_repeater/*.cpp> build_flags = ${Xiao_C6.build_flags} -D ADVERT_NAME='"Xiao C6 Repeater"' @@ -93,7 +93,7 @@ build_flags = [env:Meshimi_Repeater] extends = Meshimi build_src_filter = ${Meshimi.build_src_filter} - +<../examples/simple_repeater/main.cpp> + +<../examples/simple_repeater/*.cpp> build_flags = ${Meshimi.build_flags} -D ADVERT_NAME='"Meshimi Repeater"' @@ -150,7 +150,7 @@ build_flags = [env:WHY2025_badge_Repeater] extends = WHY2025_badge build_src_filter = ${WHY2025_badge.build_src_filter} - +<../examples/simple_repeater/main.cpp> + +<../examples/simple_repeater/*.cpp> build_flags = ${WHY2025_badge.build_flags} -D ADVERT_NAME='"WHY2025 Badge Repeater"' diff --git a/variants/xiao_nrf52/platformio.ini b/variants/xiao_nrf52/platformio.ini index 10807476..ea1cdd3d 100644 --- a/variants/xiao_nrf52/platformio.ini +++ b/variants/xiao_nrf52/platformio.ini @@ -107,7 +107,7 @@ build_flags = ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${Xiao_nrf52.build_src_filter} - +<../examples/simple_repeater/main.cpp> + +<../examples/simple_repeater/*.cpp> [env:Xiao_nrf52_alt_pinout_repeater] extends = env:Xiao_nrf52_repeater @@ -126,4 +126,4 @@ build_flags = ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${Xiao_nrf52.build_src_filter} - +<../examples/simple_room_server/main.cpp> \ No newline at end of file + +<../examples/simple_room_server/*.cpp> \ No newline at end of file diff --git a/variants/xiao_s3_wio/platformio.ini b/variants/xiao_s3_wio/platformio.ini index 7408f85d..e6d2357d 100644 --- a/variants/xiao_s3_wio/platformio.ini +++ b/variants/xiao_s3_wio/platformio.ini @@ -30,7 +30,7 @@ build_src_filter = ${esp32_base.build_src_filter} [env:Xiao_S3_WIO_Repeater] extends = Xiao_S3_WIO build_src_filter = ${Xiao_S3_WIO.build_src_filter} - +<../examples/simple_repeater/main.cpp> + +<../examples/simple_repeater/*.cpp> build_flags = ${Xiao_S3_WIO.build_flags} -D ADVERT_NAME='"XiaoS3 Repeater"' @@ -48,7 +48,7 @@ lib_deps = ; extends = Xiao_S3_WIO ; build_src_filter = ${Xiao_S3_WIO.build_src_filter} ; +<helpers/bridges/RS232Bridge.cpp> -; +<../examples/simple_repeater/main.cpp> +; +<../examples/simple_repeater/*.cpp> ; build_flags = ; ${Xiao_S3_WIO.build_flags} ; -D ADVERT_NAME='"RS232 Bridge"' @@ -69,7 +69,7 @@ lib_deps = extends = Xiao_S3_WIO build_src_filter = ${Xiao_S3_WIO.build_src_filter} +<helpers/bridges/ESPNowBridge.cpp> - +<../examples/simple_repeater/main.cpp> + +<../examples/simple_repeater/*.cpp> build_flags = ${Xiao_S3_WIO.build_flags} -D ADVERT_NAME='"ESPNow Bridge"'