feat: add MQTT repeater support, web config panel, and EastMesh-specific firmware workflows

This commit is contained in:
Jared Dohrman
2026-04-09 15:00:53 +10:00
parent 2ab5f5128f
commit 98fe78a379
27 changed files with 3412 additions and 103 deletions
+229
View File
@@ -461,6 +461,9 @@ void MyMesh::logRx(mesh::Packet *pkt, int len, float score) {
bridge.sendPacket(pkt);
}
#endif
#ifdef WITH_MQTT_UPLINK
mqtt.publishPacket(*pkt, false, (int)_radio->getLastRSSI(), _radio->getLastSNR());
#endif
if (_logging) {
File f = openAppend(PACKET_LOG_FILE);
@@ -487,6 +490,9 @@ void MyMesh::logTx(mesh::Packet *pkt, int len) {
bridge.sendPacket(pkt);
}
#endif
#ifdef WITH_MQTT_UPLINK
mqtt.publishPacket(*pkt, true, (int)_radio->getLastRSSI(), _radio->getLastSNR());
#endif
if (_logging) {
File f = openAppend(PACKET_LOG_FILE);
@@ -840,6 +846,9 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
#if defined(WITH_ESPNOW_BRIDGE)
, bridge(&_prefs, _mgr, &rtc)
#endif
#if defined(WITH_MQTT_UPLINK)
, mqtt(rtc, self_id)
#endif
{
last_millis = 0;
uptime_millis = 0;
@@ -915,6 +924,12 @@ void MyMesh::begin(FILESYSTEM *fs) {
bridge.begin();
}
#endif
#if defined(WITH_MQTT_UPLINK)
board.setInhibitSleep(true);
mqtt.setNodeNameSource(_prefs.node_name);
mqtt.setWebCommandRunner(this);
mqtt.begin(_fs);
#endif
radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
radio_set_tx_power(_prefs.tx_power_dbm);
@@ -1079,6 +1094,10 @@ void MyMesh::formatPacketStatsReply(char *reply) {
getNumRecvFlood(), getNumRecvDirect());
}
void MyMesh::formatMemoryReply(char *reply, size_t reply_size) {
StatsFormatHelper::formatMemoryStats(reply, reply_size);
}
void MyMesh::saveIdentity(const mesh::LocalIdentity &new_id) {
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
IdentityStore store(*_fs, "");
@@ -1285,11 +1304,201 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply
sendNodeDiscoverReq();
strcpy(reply, "OK - Discover sent");
}
#ifdef WITH_MQTT_UPLINK
} else if (memcmp(command, "mqtt.owner ", 11) == 0) {
if (mqtt.setOwnerPublicKey(&command[11])) {
strcpy(reply, "OK");
} else {
strcpy(reply, "Err - owner must be 64 hex chars");
}
} else if (memcmp(command, "mqtt.email ", 11) == 0) {
if (mqtt.setOwnerEmail(&command[11])) {
strcpy(reply, "OK");
} else {
strcpy(reply, "Err - bad mqtt.email");
}
} else if (memcmp(command, "get mqtt.status", 15) == 0) {
mqtt.formatStatusReply(reply, 160);
} else if (strcmp(command, "get web.status") == 0 || strcmp(command, "get web") == 0) {
mqtt.formatWebStatusReply(reply, 160);
} else if (memcmp(command, "get wifi.status", 15) == 0) {
mqtt.formatWifiStatusReply(reply, 160);
} else if (memcmp(command, "get wifi.ssid", 13) == 0) {
sprintf(reply, "> %s", mqtt.getWifiSSID()[0] ? mqtt.getWifiSSID() : "-");
} else if (memcmp(command, "get wifi.powersaving", 20) == 0) {
sprintf(reply, "> %s", mqtt.getWifiPowerSave());
} else if (memcmp(command, "get mqtt.iata", 13) == 0) {
sprintf(reply, "> %s", mqtt.getIata());
} else if (memcmp(command, "get mqtt.owner", 14) == 0) {
sprintf(reply, "> %s", mqtt.getOwnerPublicKey()[0] ? mqtt.getOwnerPublicKey() : "-");
} else if (memcmp(command, "get mqtt.email", 14) == 0) {
sprintf(reply, "> %s", mqtt.getOwnerEmail()[0] ? mqtt.getOwnerEmail() : "-");
} else if (memcmp(command, "get mqtt.packets", 16) == 0) {
sprintf(reply, "> %s", mqtt.isPacketsEnabled() ? "on" : "off");
} else if (memcmp(command, "get mqtt.raw", 12) == 0) {
sprintf(reply, "> %s", mqtt.isRawEnabled() ? "on" : "off");
} else if (memcmp(command, "get mqtt.tx", 11) == 0) {
sprintf(reply, "> %s", mqtt.isTxEnabled() ? "on" : "off");
} else if (memcmp(command, "get mqtt.eastmesh-au", 20) == 0 || memcmp(command, "get mqtt.eastmesh.au", 20) == 0) {
sprintf(reply, "> %s", mqtt.isEndpointEnabled(0x01) ? "on" : "off");
} else if (memcmp(command, "get mqtt.letsmesh-eu", 21) == 0 || memcmp(command, "get mqtt.letsmesh.eu", 21) == 0) {
sprintf(reply, "> %s", mqtt.isEndpointEnabled(0x02) ? "on" : "off");
} else if (memcmp(command, "get mqtt.letsmesh-us", 21) == 0 || memcmp(command, "get mqtt.letsmesh.us", 21) == 0) {
sprintf(reply, "> %s", mqtt.isEndpointEnabled(0x04) ? "on" : "off");
} else if (memcmp(command, "set mqtt.tx ", 12) == 0) {
mqtt.setTxEnabled(memcmp(&command[12], "on", 2) == 0);
strcpy(reply, "OK");
} else if (memcmp(command, "set web ", 8) == 0) {
mqtt.setWebEnabled(memcmp(&command[8], "on", 2) == 0);
strcpy(reply, "OK");
} else if (memcmp(command, "set.web ", 8) == 0) {
mqtt.setWebEnabled(memcmp(&command[8], "on", 2) == 0);
strcpy(reply, "OK");
} else if (memcmp(command, "set wifi.ssid ", 14) == 0) {
if (mqtt.setWifiSSID(&command[14])) {
strcpy(reply, "OK");
} else {
strcpy(reply, "Err - bad wifi.ssid");
}
} else if (memcmp(command, "set wifi.pwd ", 13) == 0) {
if (mqtt.setWifiPassword(&command[13])) {
strcpy(reply, "OK");
} else {
strcpy(reply, "Err - bad wifi.pwd");
}
} else if (memcmp(command, "set wifi.powersaving ", 21) == 0) {
if (mqtt.setWifiPowerSave(&command[21])) {
strcpy(reply, "OK");
} else {
strcpy(reply, "Err - use none|min|max");
}
} else if (memcmp(command, "set mqtt.iata ", 14) == 0) {
if (mqtt.setIata(&command[14])) {
strcpy(reply, "OK");
} else {
strcpy(reply, "Err - bad mqtt.iata");
}
} else if (memcmp(command, "set mqtt.owner ", 15) == 0) {
if (mqtt.setOwnerPublicKey(&command[15])) {
strcpy(reply, "OK");
} else {
strcpy(reply, "Err - owner must be 64 hex chars");
}
} else if (memcmp(command, "set mqtt.email ", 15) == 0) {
if (mqtt.setOwnerEmail(&command[15])) {
strcpy(reply, "OK");
} else {
strcpy(reply, "Err - bad mqtt.email");
}
} else if (memcmp(command, "set mqtt.packets ", 17) == 0) {
mqtt.setPacketsEnabled(memcmp(&command[17], "on", 2) == 0);
strcpy(reply, "OK");
} else if (memcmp(command, "set mqtt.raw ", 13) == 0) {
mqtt.setRawEnabled(memcmp(&command[13], "on", 2) == 0);
strcpy(reply, "OK");
} else if (memcmp(command, "set mqtt.eastmesh-au ", 21) == 0 || memcmp(command, "set mqtt.eastmesh.au ", 21) == 0) {
mqtt.setEndpointEnabled(0x01, memcmp(&command[21], "on", 2) == 0);
strcpy(reply, "OK");
} else if (memcmp(command, "set mqtt.letsmesh-eu ", 21) == 0 || memcmp(command, "set mqtt.letsmesh.eu ", 21) == 0) {
mqtt.setEndpointEnabled(0x02, memcmp(&command[21], "on", 2) == 0);
strcpy(reply, "OK");
} else if (memcmp(command, "set mqtt.letsmesh-us ", 21) == 0 || memcmp(command, "set mqtt.letsmesh.us ", 21) == 0) {
mqtt.setEndpointEnabled(0x04, memcmp(&command[21], "on", 2) == 0);
strcpy(reply, "OK");
#endif
} else{
_cli.handleCommand(sender_timestamp, command, reply); // common CLI commands
}
}
void MyMesh::runWebCommand(const char* command, char* reply, size_t reply_size) {
if (reply_size == 0) {
return;
}
reply[0] = 0;
if (command == nullptr) {
strncpy(reply, "Err - empty command", reply_size - 1);
reply[reply_size - 1] = 0;
return;
}
auto matches_exact = [command](const char* candidate) -> bool {
return strcmp(command, candidate) == 0;
};
auto matches_prefix = [command](const char* candidate) -> bool {
size_t len = strlen(candidate);
return strncmp(command, candidate, len) == 0;
};
bool allowed =
matches_exact("get mqtt.status") ||
matches_exact("get web.status") ||
matches_exact("get web") ||
matches_exact("advert") ||
matches_exact("reboot") ||
matches_exact("start ota") ||
matches_exact("get wifi.status") ||
matches_exact("get wifi.powersaving") ||
matches_exact("memory") ||
matches_exact("get mqtt.iata") ||
matches_exact("get mqtt.owner") ||
matches_exact("get mqtt.email") ||
matches_exact("get mqtt.packets") ||
matches_exact("get mqtt.raw") ||
matches_exact("get mqtt.tx") ||
matches_exact("get mqtt.eastmesh-au") ||
matches_exact("get mqtt.eastmesh.au") ||
matches_exact("get mqtt.letsmesh-eu") ||
matches_exact("get mqtt.letsmesh.eu") ||
matches_exact("get mqtt.letsmesh-us") ||
matches_exact("get mqtt.letsmesh.us") ||
matches_exact("get name") ||
matches_exact("get lat") ||
matches_exact("get lon") ||
matches_exact("get advert.interval") ||
matches_exact("get flood.advert.interval") ||
matches_exact("get flood.max") ||
matches_exact("get owner.info") ||
matches_exact("get guest.password") ||
matches_prefix("set wifi.ssid ") ||
matches_prefix("set wifi.pwd ") ||
matches_prefix("set wifi.powersaving ") ||
matches_prefix("set mqtt.iata ") ||
matches_prefix("set mqtt.owner ") ||
matches_prefix("set mqtt.email ") ||
matches_prefix("set mqtt.packets ") ||
matches_prefix("set mqtt.raw ") ||
matches_prefix("set mqtt.tx ") ||
matches_prefix("set web ") ||
matches_prefix("set.web ") ||
matches_prefix("set mqtt.eastmesh-au ") ||
matches_prefix("set mqtt.eastmesh.au ") ||
matches_prefix("set mqtt.letsmesh-eu ") ||
matches_prefix("set mqtt.letsmesh.eu ") ||
matches_prefix("set mqtt.letsmesh-us ") ||
matches_prefix("set mqtt.letsmesh.us ") ||
matches_prefix("set name ") ||
matches_prefix("set lat ") ||
matches_prefix("set lon ") ||
matches_prefix("set guest.password ") ||
matches_prefix("set prv.key ") ||
matches_prefix("set advert.interval ") ||
matches_prefix("set flood.advert.interval ") ||
matches_prefix("set flood.max ") ||
matches_prefix("set owner.info ");
if (!allowed) {
strncpy(reply, "Err - command not allowlisted for web access", reply_size - 1);
reply[reply_size - 1] = 0;
return;
}
char command_buf[192];
StrHelper::strncpy(command_buf, command, sizeof(command_buf));
handleCommand(0, command_buf, reply);
reply[reply_size - 1] = 0;
}
void MyMesh::loop() {
#ifdef WITH_BRIDGE
bridge.loop();
@@ -1329,6 +1538,23 @@ void MyMesh::loop() {
dirty_contacts_expiry = 0;
}
#ifdef WITH_MQTT_UPLINK
MQTTStatusSnapshot mqtt_status{};
mqtt_status.battery_mv = static_cast<int>(board.getBattMilliVolts());
mqtt_status.uptime_secs = static_cast<uint32_t>(uptime_millis / 1000);
mqtt_status.error_flags = _err_flags;
mqtt_status.queue_len = static_cast<uint16_t>(_mgr->getOutboundTotal());
mqtt_status.noise_floor = static_cast<int>(_radio->getNoiseFloor());
mqtt_status.tx_air_secs = static_cast<uint32_t>(getTotalAirTime() / 1000);
mqtt_status.rx_air_secs = static_cast<uint32_t>(getReceiveAirTime() / 1000);
mqtt_status.recv_errors = radio_driver.getPacketsRecvErrors();
mqtt_status.radio_freq = _prefs.freq;
mqtt_status.radio_bw = _prefs.bw;
mqtt_status.radio_sf = _prefs.sf;
mqtt_status.radio_cr = _prefs.cr;
mqtt.loop(mqtt_status);
#endif
// update uptime
uint32_t now = millis();
uptime_millis += now - last_millis;
@@ -1339,6 +1565,9 @@ void MyMesh::loop() {
bool MyMesh::hasPendingWork() const {
#if defined(WITH_BRIDGE)
if (bridge.isRunning()) return true; // bridge needs WiFi radio, can't sleep
#endif
#if defined(WITH_MQTT_UPLINK)
if (mqtt.isActive()) return true;
#endif
return _mgr->getOutboundTotal() > 0;
}