Merge upstream dev companion CLI updates

This commit is contained in:
mikecarper committed 2026-08-28 17:31:28 -07:00
commit 2143d3e04b
46 files changed
+1976 -163

No files matched your search

+1 -1
View File
@@ -263,7 +263,7 @@ role-specific terminal on TCP port 5002. Credential writes reply before
restarting the WiFi station, so a TCP client should expect to reconnect at the
new address; USB password input is masked. Binary
Companion clients can use command bytes
`0x46` and `0x47` over USB, BLE, or TCP port 5000 without the terminal-start
`0x7C` and `0x7D` over USB, BLE, or TCP port 5000 without the terminal-start
token. WiFi-only Companions accept all three modes. Full Companion rejects
`none` because its simultaneous BLE transport requires modem sleep. Companion
device `powersaving` and LoRa `radio.rxps` remain independent. On an ESP32 Full
+29 -7
View File
@@ -234,20 +234,39 @@ catalog; bytes `0x2C`-`0x31` are parked and `0x35` is unused.
| `0x3E` | `CMD_SEND_CHANNEL_DATA` | Send a channel binary datagram. |
| `0x3F` / `0x40` | `CMD_SET_DEFAULT_FLOOD_SCOPE` / `CMD_GET_DEFAULT_FLOOD_SCOPE` | Write or read the default flood scope. |
| `0x41` | `CMD_SEND_RAW_PACKET` | Queue a fully encoded raw mesh packet. |
| `0x42` / `0x43` | `CMD_GET_RADIO_FEM_RXGAIN` / `CMD_SET_RADIO_FEM_RXGAIN` | Read or set FEM receive gain. |
| `0x44` / `0x45` | `CMD_GET_RADIO_RXGAIN` / `CMD_SET_RADIO_RXGAIN` | Read or set the radio chip's boosted receive-gain mode. |
| `0x46` / `0x47` | `CMD_GET_WIFI_POWER_SAVE` / `CMD_SET_WIFI_POWER_SAVE` | Read or set ESP32 Companion WiFi modem sleep. |
| `0x48` / `0x49` | `CMD_GET_BLUETOOTH_NAME` / `CMD_SET_BLUETOOTH_NAME` | Read or set the independent Bluetooth device name. |
| `0x42` | `CMD_RUN_CLI_COMMAND` | Run a local CLI command (protocol v14+). |
| `0x4A` | `CMD_EXEC_LOCAL_OTA_CONTROL` | Run one bounded local TempRadio or OTA command on a Full Companion. |
| `0x4B` | `CMD_BLE_MOTA_SOURCE` | Query, start, or stop an nRF52 Full Companion's Bluetooth-backed LoRa mOTA source. |
| `0x78` / `0x79` | `CMD_GET_RADIO_FEM_RXGAIN` / `CMD_SET_RADIO_FEM_RXGAIN` | Fork extension: read or set FEM receive gain. |
| `0x7A` / `0x7B` | `CMD_GET_RADIO_RXGAIN` / `CMD_SET_RADIO_RXGAIN` | Fork extension: read or set the radio chip's boosted receive-gain mode. |
| `0x7C` / `0x7D` | `CMD_GET_WIFI_POWER_SAVE` / `CMD_SET_WIFI_POWER_SAVE` | Fork extension: read or set ESP32 Companion WiFi modem sleep. |
| `0x7E` / `0x7F` | `CMD_GET_BLUETOOTH_NAME` / `CMD_SET_BLUETOOTH_NAME` | Fork extension: read or set the independent Bluetooth device name. |
The sections below detail the most common frames. Refer to the source named
above for command bodies that are not expanded here.
The additive hardware-setting commands `0x42`-`0x47` do not change any existing
legacy frame layout. Clients should probe the command they need and treat
`CMD_RUN_CLI_COMMAND` is followed by the local CLI text without a terminating
NUL. The device returns `RESP_CODE_CLI_REPLY` (`0x1D`) followed by the reply
text. This is separate from sending a remote on-air CLI command with
`CMD_SEND_TXT_MSG` and `TXT_TYPE_CLI_COMMAND`. The body must contain at least
one byte and must not contain an embedded NUL. An unknown command is returned
as the normal CLI reply text `Unknown command`, not as an error frame.
The fork-specific hardware-setting commands occupy the host-to-device range
`0x78`-`0x7F`, the highest currently unused block below the push-frame bit.
Command IDs must stay below `0x80`; IDs with bit 7 set identify asynchronous
device-to-host push frames. These commands do not change any existing frame
layout. Clients should probe the command they need and treat
`ERR_CODE_UNSUPPORTED_CMD` as feature absence.
Firmware from this fork predating the upstream `0x42` allocation used
`0x42`-`0x49` for these eight settings. This firmware accepts those values as
deprecated aliases so existing clients continue to work. A one-byte `0x42`
frame is the legacy FEM-gain GET; `0x42` followed by text is the official
`CMD_RUN_CLI_COMMAND`. New clients should use `0x78`-`0x7F` (or send the
equivalent text through `0x42`) because future upstream commands may reuse the
remaining legacy IDs.
Both gain-command pairs can be used over the normal binary Companion
connection; USB does not need to enter terminal mode. Inside the transport's
normal Companion frame, send a GET command as its single command byte. Send a
@@ -257,7 +276,9 @@ SET command followed by one byte (`0` for off, `1` for on). A GET reply is
busy radio returns `ERR_CODE_BAD_STATE`; unsupported boosted gain returns
`ERR_CODE_UNSUPPORTED_CMD`. The FEM pair also returns
`ERR_CODE_UNSUPPORTED_CMD` when the board cannot control its external LNA. Raw
text such as `set radio.rxgain off` still requires the USB terminal start token.
unframed text such as `set radio.rxgain off` still requires the USB terminal
start token; the same text may instead be carried as a framed `0x42` command
over USB, BLE, or TCP.
The WiFi power-save pair is available on ESP32 WiFi Companion builds over the
normal binary USB, BLE, or TCP port 5000 transport. `CMD_GET_WIFI_POWER_SAVE`
@@ -868,6 +889,7 @@ Byte values are authoritative; names are aliases. When reading firmware source,
| `0x1A` | `RESP_ALLOWED_REPEAT_FREQ` | Allowed repeat-frequency ranges. |
| `0x1B` | `RESP_CODE_CHANNEL_DATA_RECV` | Queued channel datagram. |
| `0x1C` | `RESP_CODE_DEFAULT_FLOOD_SCOPE` | Default flood-scope data. |
| `0x1D` | `RESP_CODE_CLI_REPLY` | Text returned by `CMD_RUN_CLI_COMMAND`. |
### Asynchronous push types
+2 -1
View File
@@ -175,8 +175,9 @@ txt_type
| Value | Description | Message content |
|--------|---------------------------|--------------------------------------------------------------------------|
| `0x00` | plain text message | the plain text of the message |
| `0x01` | CLI command | the command text of the message |
| `0x01` | CLI data | CLI command OR reply text |
| `0x02` | signed plain text message | first four bytes is sender pubkey prefix, followed by plain text message |
| `0x03` | CLI command | (since v1.18+) CLI command text (explicit) |
For a room post, companion firmware uses its own monotonic clock for the
on-air timestamp and preserves that timestamp across application retries. Room
+418 -72
View File
@@ -121,14 +121,31 @@ static const uint32_t COMMAND_RADIO_APPLY_TIMEOUT_MS = 5000UL;
#define CMD_SET_DEFAULT_FLOOD_SCOPE 63
#define CMD_GET_DEFAULT_FLOOD_SCOPE 64
#define CMD_SEND_RAW_PACKET 65
#define CMD_GET_RADIO_FEM_RXGAIN 66
#define CMD_SET_RADIO_FEM_RXGAIN 67
#define CMD_GET_RADIO_RXGAIN 68
#define CMD_SET_RADIO_RXGAIN 69
#define CMD_GET_WIFI_POWER_SAVE 70
#define CMD_SET_WIFI_POWER_SAVE 71
#define CMD_GET_BLUETOOTH_NAME 72
#define CMD_SET_BLUETOOTH_NAME 73
#define CMD_RUN_CLI_COMMAND 66 // v14+
// Fork-specific host-to-device commands use the highest currently unused
// block below 0x80. Bit 7 is reserved by the transport for device-to-host
// push frames, so command IDs must remain in the low half of the byte range.
#define CMD_GET_RADIO_FEM_RXGAIN 0x78
#define CMD_SET_RADIO_FEM_RXGAIN 0x79
#define CMD_GET_RADIO_RXGAIN 0x7A
#define CMD_SET_RADIO_RXGAIN 0x7B
#define CMD_GET_WIFI_POWER_SAVE 0x7C
#define CMD_SET_WIFI_POWER_SAVE 0x7D
#define CMD_GET_BLUETOOTH_NAME 0x7E
#define CMD_SET_BLUETOOTH_NAME 0x7F
// Compatibility aliases used by this fork before upstream assigned 0x42 to
// CMD_RUN_CLI_COMMAND. A one-byte 0x42 frame remains distinguishable from the
// official command, whose frame always includes CLI text.
#define LEGACY_CMD_GET_RADIO_FEM_RXGAIN 0x42
#define LEGACY_CMD_SET_RADIO_FEM_RXGAIN 0x43
#define LEGACY_CMD_GET_RADIO_RXGAIN 0x44
#define LEGACY_CMD_SET_RADIO_RXGAIN 0x45
#define LEGACY_CMD_GET_WIFI_POWER_SAVE 0x46
#define LEGACY_CMD_SET_WIFI_POWER_SAVE 0x47
#define LEGACY_CMD_GET_BLUETOOTH_NAME 0x48
#define LEGACY_CMD_SET_BLUETOOTH_NAME 0x49
#if defined(RADIO_FEM_RXGAIN) && (RADIO_FEM_RXGAIN == 0)
static constexpr uint8_t DEFAULT_FEM_RX_GAIN = 0;
@@ -170,6 +187,7 @@ static constexpr uint8_t DEFAULT_FEM_RX_GAIN = 1;
#define RESP_ALLOWED_REPEAT_FREQ 26
#define RESP_CODE_CHANNEL_DATA_RECV 27
#define RESP_CODE_DEFAULT_FLOOD_SCOPE 28
#define RESP_CODE_CLI_REPLY 29 // v14+, a reply to CMD_RUN_CLI_COMMAND
#define MAX_CHANNEL_DATA_LENGTH (MAX_FRAME_SIZE - 9)
@@ -943,8 +961,7 @@ void MyMesh::onMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t
queueMessage(from, TXT_TYPE_PLAIN, pkt, sender_timestamp, NULL, 0, text);
}
void MyMesh::onCommandDataRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp,
const char *text) {
void MyMesh::onCommandDataRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp, const char *text) {
markConnectionActive(from); // in case this is from a server, and we have a connection
bool terminal_command_reply = false;
uint32_t terminal_command_elapsed_millis = 0;
@@ -957,6 +974,18 @@ void MyMesh::onCommandDataRecv(const ContactInfo &from, mesh::Packet *pkt, uint3
terminal_command_reply, terminal_command_elapsed_millis);
}
void MyMesh::onCLICommandRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp,
const char *text, char* reply) {
markConnectionActive(from); // in case this is from a server, and we have a connection
if (from.isRemoteCLIAllowed()) {
if (!handleCommand(text, sender_timestamp, reply)) {
strcat(reply, "Unknown command"); // reply may have cmd prefix from 'text'
}
} else {
queueMessage(from, TXT_TYPE_CLI_COMMAND, pkt, sender_timestamp, NULL, 0, text);
}
}
void MyMesh::onSignedMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp,
const uint8_t *sender_prefix, const char *text) {
markConnectionActive(from);
@@ -1739,6 +1768,8 @@ void MyMesh::begin(bool has_display, bool radio_available) {
}
void MyMesh::configureRadioFromPrefs() {
board.attachDynamicPrefs(_prefs.getCustom());
if (!_radio_available) {
saved_radio_apply_pending = false;
board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain);
@@ -2039,6 +2070,81 @@ bool MyMesh::handleLocalControlCommand(const char* command, char* reply,
if (handleCadCommand(command, reply, reply_size)) return true;
if (strcmp(command, "get radio.rxgain") == 0) {
if (!radio_driver.supportsRxBoostedGainMode()) {
snprintf(reply, reply_size, "Error: unsupported");
} else {
snprintf(reply, reply_size, "> %s",
_prefs.rx_boosted_gain ? "on" : "off");
}
return true;
}
if (strncmp(command, "set radio.rxgain ", 17) == 0) {
const char* value = command + 17;
if (strcmp(value, "on") != 0 && strcmp(value, "off") != 0) {
snprintf(reply, reply_size,
"Error: use set radio.rxgain on|off");
} else if (!radio_driver.supportsRxBoostedGainMode()) {
snprintf(reply, reply_size, "Error: unsupported");
} else if (!applyAndSaveRxBoostedGain(strcmp(value, "on") == 0)) {
snprintf(reply, reply_size, "Error: radio busy or save failed");
} else {
snprintf(reply, reply_size, "OK - radio.rxgain %s", value);
}
return true;
}
if (strcmp(command, "get radio.fem.rxgain") == 0) {
if (!board.canControlLoRaFemLna()) {
snprintf(reply, reply_size, "Error: unsupported");
} else {
snprintf(reply, reply_size, "> %s",
board.isLoRaFemLnaEnabled() ? "on" : "off");
}
return true;
}
if (strncmp(command, "set radio.fem.rxgain ", 21) == 0) {
const char* value = command + 21;
if (strcmp(value, "on") != 0 && strcmp(value, "off") != 0) {
snprintf(reply, reply_size,
"Error: use set radio.fem.rxgain on|off");
} else if (!board.canControlLoRaFemLna()) {
snprintf(reply, reply_size, "Error: unsupported");
} else if (!applyAndSaveFemRxGain(strcmp(value, "on") == 0)) {
snprintf(reply, reply_size, "Error: failed to apply or save FEM RX gain");
} else {
snprintf(reply, reply_size, "OK - radio.fem.rxgain %s", value);
}
return true;
}
if (strcmp(command, "get radio.fem.txgain") == 0) {
if (!board.canControlLoRaFemPaGain()) {
snprintf(reply, reply_size, "Error: unsupported");
} else {
snprintf(reply, reply_size, "> %s",
board.isLoRaFemPaGainEnabled() ? "on" : "off");
}
return true;
}
if (strncmp(command, "set radio.fem.txgain ", 21) == 0) {
const char* value = command + 21;
if (strcmp(value, "on") != 0 && strcmp(value, "off") != 0) {
snprintf(reply, reply_size,
"Error: use set radio.fem.txgain on|off");
} else if (!board.canControlLoRaFemPaGain()) {
snprintf(reply, reply_size, "Error: unsupported");
} else if (!applyAndSaveFemTxGain(strcmp(value, "on") == 0)) {
snprintf(reply, reply_size, "Error: failed to apply or save FEM TX gain");
} else {
snprintf(reply, reply_size, "OK - radio.fem.txgain %s", value);
}
return true;
}
if (strcmp(command, "get display.rotation") == 0) {
if (_ui == NULL || !_ui->supportsDisplayRotation()) {
snprintf(reply, reply_size, "Error: display rotation is unsupported");
@@ -3099,6 +3205,24 @@ void MyMesh::handleCmdFrame(size_t len) {
memcpy(&out_frame[i], _prefs.node_name, tlen);
i += tlen;
_serial->writeFrame(out_frame, i);
} else if (cmd_frame[0] == CMD_RUN_CLI_COMMAND && len >= 2) { // V14+
int i = 1;
char *text = (char *)&cmd_frame[i];
int tlen = len - i;
if (memchr(text, 0, tlen) != NULL) {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
} else {
text[tlen] = 0; // ensure null
reply_buf[0] = 0;
if (!handleCommand(text, 0, reply_buf)) {
strcat(reply_buf, "Unknown command"); // reply_buf may have cmd prefix from 'text'
}
out_frame[0] = RESP_CODE_CLI_REPLY;
int rlen = strlen(reply_buf);
memcpy(&out_frame[1], reply_buf, rlen);
_serial->writeFrame(out_frame, 1 + rlen);
}
} else if (cmd_frame[0] == CMD_SEND_TXT_MSG && len >= 14) {
int i = 1;
uint8_t txt_type = cmd_frame[i++];
@@ -3109,7 +3233,7 @@ void MyMesh::handleCmdFrame(size_t len) {
uint8_t *pub_key_prefix = &cmd_frame[i];
i += 6;
ContactInfo *recipient = lookupContactByPubKey(pub_key_prefix, 6);
if (recipient && (txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_CLI_DATA)) {
if (recipient && (txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_CLI_DATA || txt_type == TXT_TYPE_CLI_COMMAND)) {
char *text = (char *)&cmd_frame[i];
int tlen = len - i;
uint32_t est_timeout;
@@ -3126,11 +3250,11 @@ void MyMesh::handleCmdFrame(size_t len) {
int result;
uint32_t expected_ack;
if (txt_type == TXT_TYPE_CLI_DATA) {
if (txt_type == TXT_TYPE_CLI_DATA || txt_type == TXT_TYPE_CLI_COMMAND) {
const uint32_t logical_request_id = msg_timestamp;
msg_timestamp = getRTCClock()->getCurrentTimeUnique(); // Use node's RTC instead of app timestamp to avoid tripping replay protection
result = sendCommandData(*recipient, msg_timestamp, attempt, text,
est_timeout, logical_request_id);
result = sendCommandData(*recipient, msg_timestamp, attempt, txt_type,
text, est_timeout, logical_request_id);
expected_ack = 0; // no Ack expected
} else {
const uint32_t app_timestamp = msg_timestamp;
@@ -3989,8 +4113,11 @@ void MyMesh::handleCmdFrame(size_t len) {
} else {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
}
} else if (cmd_frame[0] == CMD_GET_RADIO_FEM_RXGAIN) {
if (!board.canControlLoRaFemLna()) {
} else if (cmd_frame[0] == CMD_GET_RADIO_FEM_RXGAIN
|| cmd_frame[0] == LEGACY_CMD_GET_RADIO_FEM_RXGAIN) {
if (len != 1) {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
} else if (!board.canControlLoRaFemLna()) {
writeErrFrame(ERR_CODE_UNSUPPORTED_CMD);
} else {
out_frame[0] = RESP_CODE_OK;
@@ -3998,30 +4125,37 @@ void MyMesh::handleCmdFrame(size_t len) {
memcpy(&out_frame[1], &value, 1);
_serial->writeFrame(out_frame, 2);
}
} else if (cmd_frame[0] == CMD_SET_RADIO_FEM_RXGAIN && len >= 2) {
uint8_t value = cmd_frame[1];
if (!board.canControlLoRaFemLna()) {
} else if (cmd_frame[0] == CMD_SET_RADIO_FEM_RXGAIN
|| cmd_frame[0] == LEGACY_CMD_SET_RADIO_FEM_RXGAIN) {
uint8_t value = len >= 2 ? cmd_frame[1] : 0;
if (len != 2) {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
} else if (!board.canControlLoRaFemLna()) {
writeErrFrame(ERR_CODE_UNSUPPORTED_CMD);
} else if (value <= 1) {
if (applyAndSaveFemRxGain(value != 0)) {
writeOKFrame();
} else {
writeErrFrame(ERR_CODE_UNSUPPORTED_CMD);
writeErrFrame(ERR_CODE_BAD_STATE);
}
} else {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
}
} else if (cmd_frame[0] == CMD_GET_RADIO_RXGAIN) {
if (!radio_driver.supportsRxBoostedGainMode()) {
} else if (cmd_frame[0] == CMD_GET_RADIO_RXGAIN
|| cmd_frame[0] == LEGACY_CMD_GET_RADIO_RXGAIN) {
if (len != 1) {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
} else if (!radio_driver.supportsRxBoostedGainMode()) {
writeErrFrame(ERR_CODE_UNSUPPORTED_CMD);
} else {
out_frame[0] = RESP_CODE_OK;
out_frame[1] = _prefs.rx_boosted_gain ? 1 : 0;
_serial->writeFrame(out_frame, 2);
}
} else if (cmd_frame[0] == CMD_SET_RADIO_RXGAIN && len >= 2) {
uint8_t value = cmd_frame[1];
if (value > 1) {
} else if (cmd_frame[0] == CMD_SET_RADIO_RXGAIN
|| cmd_frame[0] == LEGACY_CMD_SET_RADIO_RXGAIN) {
uint8_t value = len >= 2 ? cmd_frame[1] : 0;
if (len != 2 || value > 1) {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
} else if (!radio_driver.supportsRxBoostedGainMode()) {
writeErrFrame(ERR_CODE_UNSUPPORTED_CMD);
@@ -4030,41 +4164,57 @@ void MyMesh::handleCmdFrame(size_t len) {
} else {
writeOKFrame();
}
} else if (cmd_frame[0] == CMD_GET_WIFI_POWER_SAVE) {
#if defined(ESP32) && defined(WIFI_SSID)
out_frame[0] = RESP_CODE_OK;
out_frame[1] = getCompanionWiFiPowerSave();
_serial->writeFrame(out_frame, 2);
#else
writeErrFrame(ERR_CODE_UNSUPPORTED_CMD);
#endif
} else if (cmd_frame[0] == CMD_SET_WIFI_POWER_SAVE && len >= 2) {
#if defined(ESP32) && defined(WIFI_SSID)
const uint8_t value = cmd_frame[1];
if (value > mesh::wifi::kPowerSaveMax) {
} else if (cmd_frame[0] == CMD_GET_WIFI_POWER_SAVE
|| cmd_frame[0] == LEGACY_CMD_GET_WIFI_POWER_SAVE) {
if (len != 1) {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
} else {
char reply[160];
if (applyAndSaveWiFiPowerSaving(
companionWiFiPowerSaveName(value), reply, sizeof(reply))) {
writeOKFrame();
} else {
writeErrFrame(ERR_CODE_BAD_STATE);
}
}
#if defined(ESP32) && defined(WIFI_SSID)
out_frame[0] = RESP_CODE_OK;
out_frame[1] = getCompanionWiFiPowerSave();
_serial->writeFrame(out_frame, 2);
#else
writeErrFrame(ERR_CODE_UNSUPPORTED_CMD);
writeErrFrame(ERR_CODE_UNSUPPORTED_CMD);
#endif
} else if (cmd_frame[0] == CMD_GET_BLUETOOTH_NAME) {
out_frame[0] = RESP_CODE_OK;
out_frame[1] = mesh::companion::hasCustomBluetoothName(
_prefs.bluetooth_name) ? 1 : 0;
char* effective_name = reinterpret_cast<char*>(&out_frame[2]);
mesh::companion::formatBluetoothName(
effective_name, MAX_FRAME_SIZE - 1, _prefs.bluetooth_name,
BLE_NAME_PREFIX, _prefs.node_name);
_serial->writeFrame(out_frame, 2 + strlen(effective_name));
} else if (cmd_frame[0] == CMD_SET_BLUETOOTH_NAME) {
}
} else if (cmd_frame[0] == CMD_SET_WIFI_POWER_SAVE
|| cmd_frame[0] == LEGACY_CMD_SET_WIFI_POWER_SAVE) {
if (len != 2) {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
} else {
#if defined(ESP32) && defined(WIFI_SSID)
const uint8_t value = cmd_frame[1];
if (value > mesh::wifi::kPowerSaveMax) {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
} else {
char reply[160];
if (applyAndSaveWiFiPowerSaving(
companionWiFiPowerSaveName(value), reply, sizeof(reply))) {
writeOKFrame();
} else {
writeErrFrame(ERR_CODE_BAD_STATE);
}
}
#else
writeErrFrame(ERR_CODE_UNSUPPORTED_CMD);
#endif
}
} else if (cmd_frame[0] == CMD_GET_BLUETOOTH_NAME
|| cmd_frame[0] == LEGACY_CMD_GET_BLUETOOTH_NAME) {
if (len != 1) {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
} else {
out_frame[0] = RESP_CODE_OK;
out_frame[1] = mesh::companion::hasCustomBluetoothName(
_prefs.bluetooth_name) ? 1 : 0;
char* effective_name = reinterpret_cast<char*>(&out_frame[2]);
mesh::companion::formatBluetoothName(
effective_name, MAX_FRAME_SIZE - 1, _prefs.bluetooth_name,
BLE_NAME_PREFIX, _prefs.node_name);
_serial->writeFrame(out_frame, 2 + strlen(effective_name));
}
} else if (cmd_frame[0] == CMD_SET_BLUETOOTH_NAME
|| cmd_frame[0] == LEGACY_CMD_SET_BLUETOOTH_NAME) {
const size_t name_len = len - 1;
if (name_len > mesh::companion::BLUETOOTH_NAME_MAX_BYTES
|| memchr(&cmd_frame[1], 0, name_len) != NULL) {
@@ -4362,22 +4512,37 @@ void MyMesh::scheduleContactWriteAfterRelease(const ContactInfo& contact) {
bool MyMesh::applyAndSaveFemRxGain(bool enabled) {
if (!board.canControlLoRaFemLna()) return false;
const bool changed = board.isLoRaFemLnaEnabled() != enabled;
const bool previous_hardware = board.isLoRaFemLnaEnabled();
const uint8_t previous_pref = _prefs.radio_fem_rxgain;
const uint8_t previous_override = _prefs.radio_fem_rxgain_override;
const bool changed = previous_hardware != enabled;
if (!board.setLoRaFemLnaEnabled(enabled)) return false;
if (changed && _radio_available) _radio->recalibrateNoiseFloor();
_prefs.radio_fem_rxgain = enabled ? 1 : 0;
_prefs.radio_fem_rxgain_override = 1;
savePrefs();
if (!savePrefs()) {
_prefs.radio_fem_rxgain = previous_pref;
_prefs.radio_fem_rxgain_override = previous_override;
board.setLoRaFemLnaEnabled(previous_hardware);
if (changed && _radio_available) _radio->recalibrateNoiseFloor();
return false;
}
return true;
}
bool MyMesh::applyAndSaveFemTxGain(bool enabled) {
if (!board.canControlLoRaFemPaGain()) return false;
const bool previous_hardware = board.isLoRaFemPaGainEnabled();
const uint8_t previous_pref = _prefs.radio_fem_txgain;
if (!board.setLoRaFemPaGainEnabled(enabled)) return false;
_prefs.radio_fem_txgain = enabled ? 1 : 0;
savePrefs();
if (!savePrefs()) {
_prefs.radio_fem_txgain = previous_pref;
board.setLoRaFemPaGainEnabled(previous_hardware);
return false;
}
return true;
}
@@ -4387,8 +4552,15 @@ bool MyMesh::applyAndSaveRxBoostedGain(bool enabled) {
return false;
}
const uint8_t previous_pref = _prefs.rx_boosted_gain;
_prefs.rx_boosted_gain = enabled ? 1 : 0;
savePrefs();
if (!savePrefs()) {
_prefs.rx_boosted_gain = previous_pref;
if (_radio_available) {
radio_driver.setRxBoostedGainMode(previous_pref != 0);
}
return false;
}
return true;
}
@@ -5140,9 +5312,9 @@ void MyMesh::sendTerminalCommand(ContactInfo& recipient,
const size_t command_len =
on_air_command == NULL ? 0 : strlen(on_air_command);
if (command_len == 0 || command_len > MAX_TEXT_LEN) {
if (command_len == 0 || command_len > MAX_CORRELATED_CLI_TEXT_LEN) {
terminalOutput().printf(" ERROR: remote command must be 1-%u UTF-8 bytes\r\n",
(unsigned)MAX_TEXT_LEN);
(unsigned)MAX_CORRELATED_CLI_TEXT_LEN);
return;
}
@@ -5151,7 +5323,16 @@ void MyMesh::sendTerminalCommand(ContactInfo& recipient,
const uint32_t timestamp = getRTCClock()->getCurrentTimeUnique();
const uint32_t command_started_at = _ms->getMillis();
uint32_t est_timeout = 0;
const int result = sendCommandData(recipient, timestamp, 0, on_air_command,
// Legacy server roles used CLI_DATA for requests as well as replies. Keep
// that interoperable request form for deployed Repeaters, Rooms, and
// Sensors; Chat/Companion nodes need the explicit CLI_COMMAND direction.
const bool is_server_role = recipient.type == ADV_TYPE_REPEATER
|| recipient.type == ADV_TYPE_ROOM
|| recipient.type == ADV_TYPE_SENSOR;
const uint8_t txt_type = is_server_role
? TXT_TYPE_CLI_DATA : TXT_TYPE_CLI_COMMAND;
const int result = sendCommandData(recipient, timestamp, 0,
txt_type, on_air_command,
est_timeout, logical_request_id);
if (result == MSG_SEND_FAILED) {
terminalOutput().print(" ERROR: unable to send remote command\r\n");
@@ -5891,6 +6072,176 @@ void MyMesh::enterCLIRescue() {
Serial.println("========= CLI Rescue =========");
}
static bool isCompanionRadioPrefsCommand(const char* command) {
if (command == NULL) return false;
static const char* const exact_commands[] = {
"get radio", "get freq", "get af", "get dutycycle", "get tx",
"get rxdelay", "get path.hash.mode", "get multi.acks"
};
for (const char* candidate : exact_commands) {
if (strcmp(command, candidate) == 0) return true;
}
static const char* const set_prefixes[] = {
"set radio ", "set af ", "set dutycycle ", "set rxdelay ",
"set path.hash.mode ", "set multi.acks "
};
for (const char* prefix : set_prefixes) {
if (strncmp(command, prefix, strlen(prefix)) == 0) return true;
}
return false;
}
bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp,
char* reply) {
if (command == NULL || reply == NULL) return false;
size_t reply_capacity = 160;
while (*command == ' ' || *command == '\t') command++;
if (strlen(command) > 3 && command[2] == '|') {
// Optional two-character request prefix used by Companion CLI clients.
memcpy(reply, command, 3);
reply += 3;
reply_capacity -= 3;
*reply = 0;
command += 3;
while (*command == ' ' || *command == '\t') command++;
}
if (handleLocalControlCommand(command, reply, reply_capacity)) return true;
if (strncmp(command, "set tx ", 7) == 0) {
int32_t parsed = 0;
if (!mesh::cli::parseIntegerStrict(command + 7, parsed)) {
strcpy(reply, "Error: invalid TX power");
} else if (parsed < -9 || parsed > MAX_LORA_TX_POWER) {
snprintf(reply, reply_capacity, "Error: TX power must be -9 to %d",
MAX_LORA_TX_POWER);
} else {
const int8_t previous = _prefs.tx_power_dbm;
const int8_t requested = static_cast<int8_t>(parsed);
if (_radio_available && !radio_driver.setTxPower(requested)) {
strcpy(reply, "Error: radio busy or TX power rejected");
} else {
_prefs.tx_power_dbm = requested;
if (!savePrefs()) {
_prefs.tx_power_dbm = previous;
if (_radio_available) radio_driver.setTxPower(previous);
strcpy(reply, "Error: TX power changed but save failed");
} else {
strcpy(reply, "OK");
}
}
}
return true;
}
if (isCompanionRadioPrefsCommand(command)) {
const float previous_freq = _prefs.freq;
const float previous_bw = _prefs.bw;
const uint8_t previous_sf = _prefs.sf;
const uint8_t previous_cr = _prefs.cr;
const float previous_af = _prefs.airtime_factor;
const float previous_rxdelay = _prefs.rx_delay_base;
const uint8_t previous_hash_mode = _prefs.path_hash_mode;
const uint8_t previous_multi_acks = _prefs.multi_acks;
const uint32_t previous_rx_us = _prefs.rx_ps_rx_us;
const uint32_t previous_sleep_us = _prefs.rx_ps_sleep_us;
if (!_prefs.getRadioPrefs()->handleCommand(
command, sender_timestamp, reply)) {
return false;
}
if (_prefs.getRadioPrefs()->isDirty()) {
if (strncmp(command, "set radio ", 10) == 0) {
recalcRxPowerSavingFromLevel(
_prefs.rx_ps_level, _prefs.sf, _prefs.bw,
_prefs.rx_ps_preamble, &_prefs.rx_ps_rx_us,
&_prefs.rx_ps_sleep_us);
}
if (!savePrefs()) {
_prefs.freq = previous_freq;
_prefs.bw = previous_bw;
_prefs.sf = previous_sf;
_prefs.cr = previous_cr;
_prefs.airtime_factor = previous_af;
_prefs.rx_delay_base = previous_rxdelay;
_prefs.path_hash_mode = previous_hash_mode;
_prefs.multi_acks = previous_multi_acks;
_prefs.rx_ps_rx_us = previous_rx_us;
_prefs.rx_ps_sleep_us = previous_sleep_us;
_prefs.clearDirty();
strcpy(reply, "Error: setting changed but save failed");
}
}
return true;
}
// Hook for future variant-specific commands not covered by the shared,
// runtime-aware FEM handlers above.
if (board.handleCommand(command, sender_timestamp, reply)) {
if (_prefs.isDirty() && !savePrefs()) {
strcpy(reply, "Error: board setting changed but save failed");
}
return true;
}
if (strncmp(command, "set name ", 9) == 0) {
const char* name = command + 9;
if (!AdvertDataParser::isValidName(name)) {
strcpy(reply, "Error, bad chars");
} else {
char previous[sizeof(_prefs.node_name)];
memcpy(previous, _prefs.node_name, sizeof(previous));
StrHelper::strncpy(_prefs.node_name, name, sizeof(_prefs.node_name));
if (!savePrefs()) {
memcpy(_prefs.node_name, previous, sizeof(_prefs.node_name));
strcpy(reply, "Error: name changed but save failed");
} else {
strcpy(reply, "OK");
}
}
return true;
}
if (strcmp(command, "get name") == 0) {
snprintf(reply, reply_capacity, "> %s", _prefs.node_name);
return true;
}
if (strncmp(command, "set pin ", 8) == 0) {
int32_t parsed = 0;
if (!mesh::cli::parseIntegerStrict(command + 8, parsed)
|| parsed < 0 || parsed > 999999) {
strcpy(reply, "Error: pin must be 0-999999");
} else {
const uint32_t previous = _prefs.ble_pin;
_prefs.ble_pin = static_cast<uint32_t>(parsed);
if (!savePrefs()) {
_prefs.ble_pin = previous;
strcpy(reply, "Error: pin changed but save failed");
} else {
snprintf(reply, reply_capacity, "> pin is now %06lu",
(unsigned long)_prefs.ble_pin);
}
}
return true;
}
if (strcmp(command, "board") == 0) {
snprintf(reply, reply_capacity, "%s", board.getManufacturerName());
return true;
}
if (strcmp(command, "ver") == 0) {
snprintf(reply, reply_capacity, "%s (Build: %s)", FIRMWARE_VERSION,
FIRMWARE_BUILD_DATE);
return true;
}
return false;
}
void MyMesh::checkCLIRescueCmd() {
int len = strlen(cli_command);
while (Serial.available() && len < sizeof(cli_command)-1) {
@@ -5908,15 +6259,10 @@ void MyMesh::checkCLIRescueCmd() {
if (len > 0 && cli_command[len - 1] == '\r') { // received complete line
cli_command[len - 1] = 0; // replace newline with C string null terminator
if (memcmp(cli_command, "set ", 4) == 0) {
const char* config = &cli_command[4];
if (memcmp(config, "pin ", 4) == 0) {
_prefs.ble_pin = atoi(&config[4]);
savePrefs();
Serial.printf(" > pin is now %06d\n", _prefs.ble_pin);
} else {
Serial.printf(" Error: unknown config: %s\n", config);
}
reply_buf[0] = 0;
if (handleCommand(cli_command, 0, reply_buf)) {
// command was handled, print reply output
Serial.print(" "); Serial.print(reply_buf); Serial.println();
} else if (strcmp(cli_command, "rebuild") == 0) {
bool success = _store->formatFileSystem();
if (success) {
+8 -1
View File
@@ -249,6 +249,8 @@ protected:
const char *text) override;
void onCommandDataRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp,
const char *text) override;
void onCLICommandRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp,
const char *text, char* reply) override;
void onSignedMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp,
const uint8_t *sender_prefix, const char *text) override;
void onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packet *pkt, uint32_t timestamp,
@@ -281,7 +283,10 @@ protected:
public:
bool savePrefs() {
return _store->savePrefs(_prefs, sensors.node_lat, sensors.node_lon);
const bool saved =
_store->savePrefs(_prefs, sensors.node_lat, sensors.node_lon);
if (saved) _prefs.clearDirty();
return saved;
}
#if ENV_INCLUDE_GPS == 1
@@ -313,6 +318,7 @@ private:
}
void checkCLIRescueCmd();
bool handleCommand(const char* text, uint32_t sender_timestamp, char* reply);
void checkSerialInterface();
bool applyAndSaveFemRxGain(bool enabled);
bool applyAndSaveFemTxGain(bool enabled);
@@ -466,6 +472,7 @@ private:
#endif
bool send_unscoped; // force un-scoped flood (instead of using send_scope)
char cli_command[80];
char reply_buf[166];
uint8_t app_target_ver;
uint8_t *sign_data;
uint32_t sign_data_len;
+178 -49
View File
@@ -1,6 +1,11 @@
#pragma once
#include <Arduino.h>
#include <cstdint> // For uint8_t, uint32_t
#include <cstdint>
#include <helpers/CommonRadioPrefs.h>
#include <helpers/DynamicConfigSerializer.h>
#include "BluetoothName.h"
#define TELEM_MODE_DENY 0
@@ -16,57 +21,181 @@
// an explicit user choice remains persistent.
#define COMPANION_POWERSAVING_POLICY_VERSION 1
struct CompanionNodePrefs { // persisted to file
float airtime_factor;
char node_name[32];
float freq;
uint8_t sf;
uint8_t cr;
uint8_t multi_acks;
uint8_t manual_add_contacts;
float bw;
int8_t tx_power_dbm;
uint8_t telemetry_mode_base;
uint8_t telemetry_mode_loc;
uint8_t telemetry_mode_env;
float rx_delay_base;
uint32_t ble_pin;
uint8_t advert_loc_policy;
uint8_t buzzer_quiet;
uint8_t gps_enabled; // GPS enabled flag (0=disabled, 1=enabled)
uint32_t gps_interval; // GPS read interval in seconds; 0 uses the 1-second default
uint8_t autoadd_config; // bitmask for auto-add contacts config
uint8_t rx_boosted_gain; // SX126x RX boosted gain mode (0=power saving, 1=boosted)
uint8_t radio_fem_rxgain; // LoRa FEM RX gain setting
uint8_t client_repeat;
uint8_t path_hash_mode; // which path mode to use when sending
uint8_t autoadd_max_hops; // 0 = no limit, 1 = direct (0 hops), N = up to N-1 hops (max 64)
char default_scope_name[31];
uint8_t default_scope_key[16];
uint8_t radio_fem_rxgain_override; // 1 once the user overrides the build default
uint8_t vibe_quiet; // haptic quiet mode; appended for prefs compatibility
uint8_t radio_fem_txgain; // LoRa FEM TX gain; appended for prefs compatibility
uint8_t rx_powersaving_enabled; // SX126x/LR11xx receive duty cycling
uint32_t rx_ps_rx_us; // receive window in microseconds
uint32_t rx_ps_sleep_us; // sleep window in microseconds
uint8_t rx_ps_level; // 0=manual timings, 1..10=level-derived
uint8_t rx_ps_preamble; // 0=auto from SF, otherwise 16 or 32
uint8_t powersaving_enabled; // device CPU/GPS idle power saving
uint8_t wifi_enabled; // Companion WiFi radio and services
uint8_t powersaving_policy_version; // one-time default migration marker
uint8_t usb_logging_enabled; // live USB packet/debug output
char bluetooth_name[mesh::companion::BLUETOOTH_NAME_SIZE];
// exact BLE name; empty uses BLE_NAME_PREFIX + node_name
uint16_t display_rotation_degrees; // 0=board default; otherwise 90/180/270
uint8_t cad_enabled; // hardware channel activity detection
uint16_t cad_scan_timeout_ms; // 0=derive from SF/BW
uint16_t cad_retry_delay_ms; // 0=Mesh adaptive default
uint16_t cad_max_duration_ms; // 0=Dispatcher default
// Companion preferences remain stored by DataStore's explicit, append-only
// binary field list. The adapter objects are appended after every persisted
// field, and node latitude/longitude deliberately remain separate DataStore
// arguments, so the established field offsets and on-device format do not move.
class CompanionNodePrefs {
public:
float airtime_factor = 0;
char node_name[32] = {};
float freq = 0;
uint8_t sf = 0;
uint8_t cr = 0;
uint8_t multi_acks = 0;
uint8_t manual_add_contacts = 0;
float bw = 0;
int8_t tx_power_dbm = 0;
uint8_t telemetry_mode_base = 0;
uint8_t telemetry_mode_loc = 0;
uint8_t telemetry_mode_env = 0;
float rx_delay_base = 0;
uint32_t ble_pin = 0;
uint8_t advert_loc_policy = 0;
uint8_t buzzer_quiet = 0;
uint8_t gps_enabled = 0; // GPS enabled flag (0=disabled, 1=enabled)
uint32_t gps_interval = 0; // seconds; 0 uses the 1-second default
uint8_t autoadd_config = 0; // bitmask for auto-add contacts config
uint8_t rx_boosted_gain = 0; // SX126x RX boosted gain mode
uint8_t radio_fem_rxgain = 0; // external LoRa FEM RX gain
uint8_t client_repeat = 0;
uint8_t path_hash_mode = 0;
uint8_t autoadd_max_hops = 0;
char default_scope_name[31] = {};
uint8_t default_scope_key[16] = {};
// Append-only binary preference tail. Do not insert persisted fields above
// or between these entries without updating DataStore's compatibility map.
uint8_t radio_fem_rxgain_override = 0;
uint8_t vibe_quiet = 0;
uint8_t radio_fem_txgain = 0;
uint8_t rx_powersaving_enabled = 0;
uint32_t rx_ps_rx_us = 0;
uint32_t rx_ps_sleep_us = 0;
uint8_t rx_ps_level = 0;
uint8_t rx_ps_preamble = 0;
uint8_t powersaving_enabled = 0;
uint8_t wifi_enabled = 0;
uint8_t powersaving_policy_version = 0;
uint8_t usb_logging_enabled = 0;
char bluetooth_name[mesh::companion::BLUETOOTH_NAME_SIZE] = {};
uint16_t display_rotation_degrees = 0;
uint8_t cad_enabled = 0;
uint16_t cad_scan_timeout_ms = 0;
uint16_t cad_retry_delay_ms = 0;
uint16_t cad_max_duration_ms = 0;
private:
class RadioPrefs : public CommonRadioPrefs {
CompanionNodePrefs* _parent;
protected:
void structure() override {
def("freq", _parent->freq);
def("bw", _parent->bw);
def("sf", _parent->sf);
def("cr", _parent->cr);
def("cad", _parent->cad_enabled);
def("rxgain", _parent->rx_boosted_gain);
def("fem_rxgain", _parent->radio_fem_rxgain);
def("fem_txgain", _parent->radio_fem_txgain);
def("tx", _parent->tx_power_dbm);
def("af", _parent->airtime_factor);
def("rxdelay", _parent->rx_delay_base);
def("hash_mode", _parent->path_hash_mode);
def("multi_ack", _parent->multi_acks);
}
public:
explicit RadioPrefs(CompanionNodePrefs* parent) : _parent(parent) { }
float getFreq() const override { return _parent->freq; }
void setFreq(float value) override {
_parent->freq = value;
markDirty();
}
float getBandwidth() const override { return _parent->bw; }
void setBandwidth(float value) override {
_parent->bw = value;
markDirty();
}
uint8_t getSpreadFactor() const override { return _parent->sf; }
void setSpreadFactor(uint8_t value) override {
_parent->sf = value;
markDirty();
}
uint8_t getCodingRate() const override { return _parent->cr; }
void setCodingRate(uint8_t value) override {
_parent->cr = value;
markDirty();
}
float getAirtimeFactor() const override { return _parent->airtime_factor; }
void setAirtimeFactor(float value) override {
_parent->airtime_factor = value;
markDirty();
}
bool isCadEnabled() const override { return _parent->cad_enabled != 0; }
void setCadEnabled(bool enabled) override {
_parent->cad_enabled = enabled ? 1 : 0;
markDirty();
}
uint8_t getIntThresh() const override { return 0; }
void setIntThresh(uint8_t) override { }
uint8_t getRxGain() const override { return _parent->rx_boosted_gain; }
void setRxGain(uint8_t value) override {
_parent->rx_boosted_gain = value;
markDirty();
}
int8_t getTxPower() const override { return _parent->tx_power_dbm; }
void setTxPower(int8_t value) override {
_parent->tx_power_dbm = value;
markDirty();
}
float getRxDelay() const override { return _parent->rx_delay_base; }
void setRxDelay(float value) override {
_parent->rx_delay_base = value;
markDirty();
}
uint8_t getAgcResetInt() const override { return 0; }
void setAgcResetInt(uint8_t) override { }
uint8_t getHashMode() const override { return _parent->path_hash_mode; }
void setHashMode(uint8_t value) override {
_parent->path_hash_mode = value;
markDirty();
}
uint8_t getMultiAcks() const override { return _parent->multi_acks; }
void setMultiAcks(uint8_t value) override {
_parent->multi_acks = value;
markDirty();
}
float getFloodTxDelay() const override { return 0.5f; }
void setFloodTxDelay(float) override { }
float getDirectTxDelay() const override { return 0.2f; }
void setDirectTxDelay(float) override { }
uint8_t getFEMRxGain() const override { return _parent->radio_fem_rxgain; }
void setFEMRxGain(uint8_t value) override {
_parent->radio_fem_rxgain = value;
_parent->radio_fem_rxgain_override = 1;
markDirty();
}
uint8_t getFEMTxGain() const override { return _parent->radio_fem_txgain; }
void setFEMTxGain(uint8_t value) override {
_parent->radio_fem_txgain = value;
markDirty();
}
};
// Runtime-only adapters must remain after the persisted field block above.
RadioPrefs radio;
DynamicConfigSerializer custom;
public:
CompanionNodePrefs() : radio(this), custom(&radio) { }
// Keep the upstream repeat API while retaining the existing binary prefs
// layout used by this branch.
bool isRepeatEn() const { return client_repeat != 0; }
void setRepeatEn(bool enabled) { client_repeat = enabled ? 1 : 0; }
CommonRadioPrefs* getRadioPrefs() { return &radio; }
KeyValueStore* getCustom() { return &custom; }
bool isDirty() const {
return radio.isDirty() || custom.isDirty();
}
void clearDirty() {
radio.clearDirty();
custom.clearDirty();
}
};
inline bool migrateCompanionPowerSavingDefault(CompanionNodePrefs& prefs) {
+3 -1
View File
@@ -2691,7 +2691,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
memcpy(&sender_timestamp, data, 4); // timestamp (by sender's RTC clock - which could be wrong)
uint8_t flags = (data[4] >> 2); // message attempt number, and other flags
if (!(flags == TXT_TYPE_PLAIN || flags == TXT_TYPE_CLI_DATA)) {
if (!(flags == TXT_TYPE_PLAIN || flags == TXT_TYPE_CLI_DATA || flags == TXT_TYPE_CLI_COMMAND)) {
MESH_DEBUG_PRINTLN("onPeerDataRecv: unsupported text type received: flags=%02x", (uint32_t)flags);
} else {
// len can be > original length, but 'text' will be padded with zeroes
@@ -3563,6 +3563,8 @@ void MyMesh::begin(FILESYSTEM *fs) {
board.setLoRaFemPaGainEnabled(_prefs.radio_fem_txgain);
setRxPowerSaving(_prefs.rx_powersaving_enabled, _prefs.rx_ps_rx_us, _prefs.rx_ps_sleep_us);
board.attachDynamicPrefs(_prefs.getCustom());
updateAdvertTimer();
updateFloodAdvertTimer();
next_recent_repeater_sweep = futureMillis(RECENT_REPEATER_SWEEP_INTERVAL_MILLIS);
+4 -2
View File
@@ -800,7 +800,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
memcpy(&sender_timestamp, data, 4); // timestamp (by sender's RTC clock - which could be wrong)
uint8_t flags = (data[4] >> 2); // message attempt number, and other flags
if (!(flags == TXT_TYPE_PLAIN || flags == TXT_TYPE_CLI_DATA)) {
if (!(flags == TXT_TYPE_PLAIN || flags == TXT_TYPE_CLI_DATA || flags == TXT_TYPE_CLI_COMMAND)) {
MESH_DEBUG_PRINTLN("onPeerDataRecv: unsupported command flags received: flags=%02x", (uint32_t)flags);
return;
}
@@ -845,7 +845,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
// advanced this client's room-post replay timestamp.
send_ack = true;
}
} else { // TXT_TYPE_CLI_DATA
} else { // TXT_TYPE_CLI_DATA or TXT_TYPE_CLI_COMMAND
uint32_t request_id = sender_timestamp;
mesh::RemoteCliRequest::parse(data, len, 5, request_id);
const uint32_t command_fingerprint =
@@ -1316,6 +1316,8 @@ void MyMesh::begin(FILESYSTEM *fs) {
board.setLoRaFemPaGainEnabled(_prefs.radio_fem_txgain);
setRxPowerSaving(_prefs.rx_powersaving_enabled, _prefs.rx_ps_rx_us, _prefs.rx_ps_sleep_us);
board.attachDynamicPrefs(_prefs.getCustom());
updateAdvertTimer();
updateFloodAdvertTimer();
+4
View File
@@ -257,6 +257,10 @@ protected:
void onCommandDataRecv(const ContactInfo& from, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text) override {
}
void onCLICommandRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text, char* reply) override {
}
void onSignedMessageRecv(const ContactInfo& from, mesh::Packet* pkt, uint32_t sender_timestamp, const uint8_t *sender_prefix, const char *text) override {
}
+3 -1
View File
@@ -774,7 +774,7 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i
sendAckTo(*from, ack_hash, packet->getPathHashSize());
}
}
} else if (flags == TXT_TYPE_CLI_DATA) {
} else if (flags == TXT_TYPE_CLI_DATA || flags == TXT_TYPE_CLI_COMMAND) {
from->last_timestamp = sender_timestamp;
from->last_activity = getRTCClock()->getCurrentTime();
@@ -1004,6 +1004,8 @@ void SensorMesh::begin(FILESYSTEM* fs) {
board.setLoRaFemPaGainEnabled(_prefs.radio_fem_txgain);
setRxPowerSaving(_prefs.rx_powersaving_enabled, _prefs.rx_ps_rx_us, _prefs.rx_ps_sleep_us);
board.attachDynamicPrefs(_prefs.getCustom());
updateAdvertTimer();
updateFloodAdvertTimer();
+2
View File
@@ -354,6 +354,8 @@ build_src_filter =
+<../src/helpers/UserGpio.cpp>
+<../src/helpers/ConfigSerializer.cpp>
+<../src/helpers/ArduinoSerialInterface.cpp>
+<../src/helpers/DynamicConfigSerializer.cpp>
+<../src/helpers/CommonRadioPrefs.cpp>
lib_deps =
google/googletest @ 1.17.0
bblanchon/ArduinoJson @ 7.4.3
+7
View File
@@ -159,6 +159,13 @@ public:
virtual bool isPowerManagementInitialized() const { return false; }
virtual bool supportsVoltageWake() const { return false; }
virtual bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply) {
(void)command;
(void)sender_timestamp;
(void)reply;
return false;
}
inline static uint32_t n_cad_busy = 0;
};
+9
View File
@@ -264,6 +264,15 @@ bool Utils::isHexChar(char c) {
return c == '0' || hexVal(c) > 0;
}
bool Utils::isZeroes(const uint8_t* buf, size_t len) {
while (len > 0) {
if (*buf != 0) return false;
buf++;
len--;
}
return true;
}
bool Utils::fromHex(uint8_t* dest, int dest_size, const char *src_hex) {
if (dest == NULL || src_hex == NULL || dest_size < 0) return false;
int len = strlen(src_hex);
+2
View File
@@ -82,6 +82,8 @@ public:
static int parseTextParts(char* text, const char* parts[], int max_num, char separator=',');
static bool isHexChar(char c);
static bool isZeroes(const uint8_t* buf, size_t len);
};
}
+8
View File
@@ -28,6 +28,14 @@
return i;
}
bool AdvertDataParser::isValidName(const char *n) {
while (*n) {
if (*n == '[' || *n == ']' || *n == '\\' || *n == ':' || *n == ',' || *n == '?' || *n == '*') return false;
n++;
}
return true;
}
AdvertDataParser::AdvertDataParser(const uint8_t app_data[], uint8_t app_data_len) {
_name[0] = 0;
_lat = _lon = 0;
+2
View File
@@ -50,6 +50,8 @@ class AdvertDataParser {
public:
AdvertDataParser(const uint8_t app_data[], uint8_t app_data_len);
static bool isValidName(const char* name);
bool isValid() const { return _valid; }
uint8_t getType() const { return _flags & 0x0F; }
uint16_t getFeat1() const { return _extra1; }
+47 -12
View File
@@ -15,6 +15,8 @@
#define TXT_ACK_DELAY 200
#endif
#define CLI_REPLY_DELAY_MILLIS 600
bool BaseChatMesh::sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis) {
return sendFlood(pkt, delay_millis);
}
@@ -274,8 +276,8 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender
ContactInfo& from = contacts[i];
if (type == PAYLOAD_TYPE_TXT_MSG && len > 5) {
uint32_t timestamp;
memcpy(&timestamp, data, 4); // timestamp (by sender's RTC clock - which could be wrong)
uint32_t sender_timestamp;
memcpy(&sender_timestamp, data, 4); // timestamp (by sender's RTC clock - which could be wrong)
uint8_t flags = data[4] >> 2; // message attempt number, and other flags
// len can be > original length, but 'text' will be padded with zeroes
@@ -283,7 +285,7 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender
if (flags == TXT_TYPE_PLAIN) {
from.lastmod = getRTCClock()->getCurrentTime(); // update last heard time
onMessageRecv(from, packet, timestamp, (const char *) &data[5]); // let UI know
onMessageRecv(from, packet, sender_timestamp, (const char *) &data[5]); // let UI know
int text_len = strlen((char *)&data[5]);
uint8_t ack_hash[6]; // calc truncated hash of the message timestamp + text + sender pub_key, to prove to sender that we got it
@@ -301,20 +303,50 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender
sendAckTo(from, ack_hash, 6);
}
} else if (flags == TXT_TYPE_CLI_DATA) {
onCommandDataRecv(from, packet, timestamp, (const char *) &data[5]); // let UI know
char *text = (char *)&data[5];
onCommandDataRecv(from, packet, sender_timestamp, text); // let UI know
// NOTE: no ack expected for CLI_DATA replies
if (packet->isRouteFlood()) {
// let this sender know path TO here, so they can use sendDirect() (NOTE: no ACK as extra)
mesh::Packet* path = createPathReturn(from.id, secret, packet->path, packet->path_len, 0, NULL, 0);
// Let this sender learn a direct path without adding an ACK.
mesh::Packet* path = createPathReturn(from.id, secret, packet->path,
packet->path_len, 0, NULL, 0);
if (path) sendFloodScoped(from, path);
}
} else if (flags == TXT_TYPE_CLI_COMMAND) {
uint8_t temp[166];
char *command = (char *)&data[5];
char *reply = (char *)&temp[5];
*reply = 0;
onCLICommandRecv(from, packet, sender_timestamp, command, reply); // let UI know
// NOTE: no ack expected for CLI_COMMAND replies
int text_len = strlen(reply);
if (text_len > 0) {
uint32_t timestamp = getRTCClock()->getCurrentTimeUnique();
if (timestamp == sender_timestamp) {
// WORKAROUND: the two timestamps need to be different, in the CLI view
timestamp++;
}
memcpy(temp, &timestamp, 4);
temp[4] = (TXT_TYPE_CLI_DATA << 2);
auto reply_pkt = createDatagram(PAYLOAD_TYPE_TXT_MSG, from.id, secret, temp, 5 + text_len);
if (reply_pkt) {
if (from.out_path_len == OUT_PATH_UNKNOWN) {
sendFloodScoped(from, reply_pkt, CLI_REPLY_DELAY_MILLIS);
} else {
sendDirect(reply_pkt, from.out_path, from.out_path_len, CLI_REPLY_DELAY_MILLIS);
}
}
}
} else if (flags == TXT_TYPE_SIGNED_PLAIN) {
if (timestamp > from.sync_since) { // make sure 'sync_since' is up-to-date
from.sync_since = timestamp;
if (sender_timestamp > from.sync_since) { // make sure 'sync_since' is up-to-date
from.sync_since = sender_timestamp;
}
from.lastmod = getRTCClock()->getCurrentTime(); // update last heard time
onSignedMessageRecv(from, packet, timestamp, &data[5], (const char *) &data[9]); // let UI know
onSignedMessageRecv(from, packet, sender_timestamp, &data[5], (const char *) &data[9]); // let UI know
uint32_t ack_hash; // calc truncated hash of the message timestamp + text + OUR pub_key, to prove to sender that we got it
mesh::Utils::sha256((uint8_t *) &ack_hash, 4, data, 9 + strlen((char *)&data[9]), self_id.pub_key, PUB_KEY_SIZE);
@@ -535,15 +567,18 @@ int BaseChatMesh::sendMessage(const ContactInfo& recipient, uint32_t timestamp,
int BaseChatMesh::sendCommandData(const ContactInfo& recipient,
uint32_t timestamp, uint8_t attempt,
const char* text, uint32_t& est_timeout,
uint8_t txt_type, const char* text,
uint32_t& est_timeout,
uint32_t logical_request_id) {
est_timeout = 0;
int text_len = strlen(text);
if (text_len > MAX_TEXT_LEN) return MSG_SEND_FAILED;
const size_t max_text_len = logical_request_id == 0
? MAX_TEXT_LEN : MAX_CORRELATED_CLI_TEXT_LEN;
if ((size_t)text_len > max_text_len) return MSG_SEND_FAILED;
uint8_t temp[5 + MAX_TEXT_LEN + mesh::RemoteCliRequest::EXTENSION_SIZE];
memcpy(temp, &timestamp, 4); // mostly an extra blob to help make packet_hash unique
temp[4] = (attempt & 3) | (TXT_TYPE_CLI_DATA << 2);
temp[4] = (attempt & 3) | (txt_type << 2);
memcpy(&temp[5], text, text_len);
size_t payload_len = 5 + text_len;
+9 -1
View File
@@ -3,10 +3,17 @@
#include <Arduino.h> // needed for PlatformIO
#include <Mesh.h>
#include <helpers/AdvertDataHelpers.h>
#include <helpers/RemoteCliRequest.h>
#include <helpers/TxtDataHelpers.h>
#define MAX_TEXT_LEN (10*CIPHER_BLOCK_SIZE) // must be LESS than (MAX_PACKET_PAYLOAD - 4 - CIPHER_MAC_SIZE - 1)
static constexpr size_t MAX_CORRELATED_CLI_TEXT_LEN =
MAX_PACKET_PAYLOAD - CIPHER_MAC_SIZE - (CIPHER_BLOCK_SIZE - 1)
- 5 - mesh::RemoteCliRequest::EXTENSION_SIZE;
static_assert(MAX_CORRELATED_CLI_TEXT_LEN <= MAX_TEXT_LEN,
"correlated CLI text must fit the encrypted datagram");
#include "ContactInfo.h"
#define MAX_SEARCH_RESULTS 8
@@ -139,6 +146,7 @@ protected:
virtual bool onContactPathRecv(ContactInfo& from, uint8_t* in_path, uint8_t in_path_len, uint8_t* out_path, uint8_t out_path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len);
virtual void onMessageRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text) = 0;
virtual void onCommandDataRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text) = 0;
virtual void onCLICommandRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text, char* reply) = 0;
virtual void onSignedMessageRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const uint8_t *sender_prefix, const char *text) = 0;
virtual uint32_t calcFloodTimeoutMillisFor(uint32_t pkt_airtime_millis) const = 0;
virtual uint32_t calcDirectTimeoutMillisFor(uint32_t pkt_airtime_millis, uint8_t path_len) const = 0;
@@ -185,7 +193,7 @@ public:
const uint8_t* replace_retry_key = NULL,
const uint8_t* message_retry_key = NULL);
int sendCommandData(const ContactInfo& recipient, uint32_t timestamp,
uint8_t attempt, const char* text,
uint8_t attempt, uint8_t txt_type, const char* text,
uint32_t& est_timeout,
uint32_t logical_request_id = 0);
bool sendGroupMessage(uint32_t timestamp, mesh::GroupChannel& channel, const char* sender_name, const char* text, int text_len);
+3 -10
View File
@@ -2352,6 +2352,7 @@ void CommonCLI::savePrefs(PrefsSaveRouting::Scope scope) {
_callbacks->updateAdvertTimer();
}
_callbacks->savePrefs(scope);
_prefs->clearDirty();
}
void CommonCLI::saveObserverPrefs() {
@@ -2854,6 +2855,8 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
_callbacks->formatRadioStatsReply(reply);
} else if (sender_timestamp == 0 && memcmp(command, "stats-core", 10) == 0 && (command[10] == 0 || command[10] == ' ')) {
_callbacks->formatStatsReply(reply);
} else if (_board->handleCommand(command, sender_timestamp, reply)) {
if (_prefs->isDirty()) savePrefs();
} else {
strcpy(reply, "Unknown command");
}
@@ -4054,16 +4057,6 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
*dp = 0;
savePrefs();
strcpy(reply, "OK");
} else if (memcmp(config, "path.hash.mode ", 15) == 0) {
config += 15;
uint8_t mode = atoi(config);
if (mode < 3) {
_prefs->path_hash_mode = mode;
savePrefs();
strcpy(reply, "OK");
} else {
strcpy(reply, "Error, must be 0,1, or 2");
}
} else if (memcmp(config, "loop.detect ", 12) == 0) {
config += 12;
uint8_t mode;
+48 -2
View File
@@ -10,6 +10,8 @@
#include <helpers/RegionMap.h>
#include <helpers/ConfigSerializer.h>
#include <helpers/bridges/ESPNowBridgeFormat.h>
#include <helpers/CommonRadioPrefs.h>
#include <helpers/DynamicConfigSerializer.h>
#ifndef DEFAULT_CAD_ENABLED
#define DEFAULT_CAD_ENABLED 0
@@ -240,7 +242,7 @@ public:
uint8_t system_watchdog_enabled = 0; // nRF52 main-loop hardware watchdog (boolean; default on)
private:
class RadioPrefs : public ConfigSerializer {
class RadioPrefs : public CommonRadioPrefs {
NodePrefs* _parent;
protected:
@@ -266,6 +268,41 @@ private:
public:
explicit RadioPrefs(NodePrefs* parent) : _parent(parent) { }
// CommonRadioPrefs interface
float getFreq() const override { return _parent->freq; }
void setFreq(float f) override { _parent->freq = f; markDirty(); }
float getBandwidth() const override { return _parent->bw; }
void setBandwidth(float bw) override { _parent->bw = bw; markDirty(); }
uint8_t getSpreadFactor() const override { return _parent->sf; }
void setSpreadFactor(uint8_t sf) override { _parent->sf = sf; markDirty(); }
uint8_t getCodingRate() const override { return _parent->cr; }
void setCodingRate(uint8_t cr) override { _parent->cr = cr; markDirty(); }
float getAirtimeFactor() const override { return _parent->airtime_factor; }
void setAirtimeFactor(float af) override { _parent->airtime_factor = af; markDirty(); }
bool isCadEnabled() const override { return _parent->cad_enabled; }
void setCadEnabled(bool en) override { _parent->cad_enabled = en; markDirty(); }
uint8_t getIntThresh() const override { return _parent->interference_threshold; }
void setIntThresh(uint8_t t) override { _parent->interference_threshold = t; markDirty(); }
uint8_t getRxGain() const override { return _parent->rx_boosted_gain; }
void setRxGain(uint8_t g) override { _parent->rx_boosted_gain = g; markDirty(); }
int8_t getTxPower() const override { return _parent->tx_power_dbm; }
void setTxPower(int8_t dbm) override { _parent->tx_power_dbm = dbm; markDirty(); }
float getRxDelay() const override { return _parent->rx_delay_base; }
void setRxDelay(float d) override { _parent->rx_delay_base = d; markDirty(); }
uint8_t getAgcResetInt() const override { return _parent->agc_reset_interval * 4; }
void setAgcResetInt(uint8_t secs) override { _parent->agc_reset_interval = secs / 4; markDirty(); }
uint8_t getHashMode() const override { return _parent->path_hash_mode; }
void setHashMode(uint8_t m) override { _parent->path_hash_mode = m; markDirty(); }
uint8_t getMultiAcks() const override { return _parent->multi_acks; }
void setMultiAcks(uint8_t m) override { _parent->multi_acks = m; markDirty(); }
float getFloodTxDelay() const override { return _parent->tx_delay_factor; }
void setFloodTxDelay(float d) override { _parent->tx_delay_factor = d; markDirty(); }
float getDirectTxDelay() const override { return _parent->direct_tx_delay_factor; }
void setDirectTxDelay(float d) override { _parent->direct_tx_delay_factor = d; markDirty(); }
uint8_t getFEMRxGain() const override { return _parent->radio_fem_rxgain; }
void setFEMRxGain(uint8_t g) override { _parent->radio_fem_rxgain = g; markDirty(); }
uint8_t getFEMTxGain() const override { return _parent->radio_fem_txgain; }
void setFEMTxGain(uint8_t g) override { _parent->radio_fem_txgain = g; markDirty(); }
};
RadioPrefs radio;
@@ -349,6 +386,8 @@ private:
};
RoomPrefs room;
DynamicConfigSerializer custom;
protected:
void structure() override {
def("name", node_name, sizeof(node_name));
@@ -366,12 +405,19 @@ protected:
def("repeat", repeat);
def("room", room);
def("power", power);
def("custom", custom);
}
public:
NodePrefs()
: ConfigSerializer(), radio(this), bridge(this), gps(this), power(this),
repeat(this), room(this) { }
repeat(this), room(this), custom(&radio) { }
CommonRadioPrefs* getRadioPrefs() { return &radio; }
KeyValueStore* getCustom() { return &custom; }
bool isDirty() const override { return ConfigSerializer::isDirty() || radio.isDirty() || custom.isDirty(); }
void clearDirty() override { ConfigSerializer::clearDirty(); radio.clearDirty(); custom.clearDirty(); }
};
#ifdef WITH_MQTT_BRIDGE
+433
View File
@@ -0,0 +1,433 @@
#include "CommonRadioPrefs.h"
#include "TxtDataHelpers.h"
#include "target.h"
#include <math.h>
#include <stdio.h>
#include <string.h>
namespace {
constexpr size_t RADIO_ARGS_CAPACITY = 132;
size_t boundedLength(const char* text, size_t capacity) {
if (text == nullptr) return capacity;
size_t length = 0;
while (length < capacity && text[length] != 0) length++;
return length;
}
bool startsWith(const char* text, const char* prefix) {
if (text == nullptr || prefix == nullptr) return false;
const size_t prefix_len = strlen(prefix);
return strncmp(text, prefix, prefix_len) == 0;
}
bool parseIntStrict(const char* text, int32_t min_value, int32_t max_value,
int32_t& result) {
if (text == nullptr || *text == 0 || min_value > max_value) return false;
bool negative = false;
if (*text == '-') {
negative = true;
text++;
}
if (*text == 0) return false;
int64_t magnitude = 0;
while (*text != 0) {
if (*text < '0' || *text > '9') return false;
magnitude = magnitude * 10 + (*text++ - '0');
if (magnitude > 2147483648LL) return false;
}
const int64_t parsed = negative ? -magnitude : magnitude;
if (parsed < min_value || parsed > max_value) return false;
result = static_cast<int32_t>(parsed);
return true;
}
// Accept only a plain fixed-point decimal. Exponents, NaN/Inf, whitespace,
// and trailing characters are rejected so malformed CLI values never become
// zero through atoi()/atof() fallback behavior.
bool parseDecimalStrict(const char* text, float& result) {
if (text == nullptr || *text == 0) return false;
const char* cursor = text;
bool negative = false;
if (*cursor == '-') {
negative = true;
cursor++;
}
bool saw_digit = false;
double value = 0.0;
while (*cursor >= '0' && *cursor <= '9') {
value = value * 10.0 + (*cursor++ - '0');
if (!isfinite(value)) return false;
saw_digit = true;
}
if (*cursor == '.') {
cursor++;
double place = 0.1;
while (*cursor >= '0' && *cursor <= '9') {
value += (*cursor++ - '0') * place;
place *= 0.1;
saw_digit = true;
}
}
if (!saw_digit || *cursor != 0) return false;
value = negative ? -value : value;
if (!isfinite(value) || value > 3.402823466e+38
|| value < -3.402823466e+38) {
return false;
}
result = static_cast<float>(value);
return isfinite(result);
}
bool splitRadioArgs(char* text, const char* parts[4]) {
if (text == nullptr || *text == 0) return false;
size_t count = 0;
char* field = text;
while (true) {
if (*field == 0 || count >= 4) return false;
parts[count++] = field;
char* comma = strchr(field, ',');
if (comma == nullptr) break;
*comma = 0;
field = comma + 1;
}
return count == 4;
}
bool bwMatches(float bw, float allowed) {
return fabsf(bw - allowed) <= 0.001f;
}
bool isValidLoRaBandwidth(float bw) {
#if defined(USE_LR1110)
return bwMatches(bw, 62.5f)
|| bwMatches(bw, 125.0f)
|| bwMatches(bw, 250.0f)
|| bwMatches(bw, 500.0f);
#elif defined(USE_LLCC68) || defined(USE_SX1272)
return bwMatches(bw, 125.0f)
|| bwMatches(bw, 250.0f)
|| bwMatches(bw, 500.0f);
#else
return bwMatches(bw, 7.8f)
|| bwMatches(bw, 10.4f)
|| bwMatches(bw, 15.6f)
|| bwMatches(bw, 20.8f)
|| bwMatches(bw, 31.25f)
|| bwMatches(bw, 41.7f)
|| bwMatches(bw, 62.5f)
|| bwMatches(bw, 125.0f)
|| bwMatches(bw, 250.0f)
|| bwMatches(bw, 500.0f);
#endif
}
bool copyValue(char* destination, size_t capacity, const char* value) {
if (destination == nullptr || capacity == 0 || value == nullptr) return false;
const size_t value_len = strlen(value);
if (value_len >= capacity) {
destination[0] = 0;
return false;
}
memcpy(destination, value, value_len + 1);
return true;
}
} // namespace
bool CommonRadioPrefs::getByKey(const char* key, char* value, size_t max_len) {
if (key == nullptr || value == nullptr || max_len == 0) return false;
if (strcmp(key, "fem_rxgain") == 0) {
return copyValue(value, max_len, getFEMRxGain() == 0 ? "0" : "1");
}
if (strcmp(key, "fem_txgain") == 0) {
return copyValue(value, max_len, getFEMTxGain() == 0 ? "0" : "1");
}
return false;
}
bool CommonRadioPrefs::setByKey(const char* key, const char* value) {
if (key == nullptr || value == nullptr
|| (strcmp(value, "0") != 0 && strcmp(value, "1") != 0)) {
return false;
}
const uint8_t enabled = strcmp(value, "1") == 0 ? 1 : 0;
if (strcmp(key, "fem_rxgain") == 0) {
setFEMRxGain(enabled);
markDirty();
return true;
}
if (strcmp(key, "fem_txgain") == 0) {
setFEMTxGain(enabled);
markDirty();
return true;
}
return false;
}
bool CommonRadioPrefs::handleCommand(const char* command, uint32_t sender_timestamp, char* reply) {
(void)sender_timestamp;
if (command == nullptr || reply == nullptr) return false;
if (strcmp(command, "get radio") == 0) {
char freq[16], bw[16];
snprintf(freq, sizeof(freq), "%s", StrHelper::ftoa(getFreq()));
snprintf(bw, sizeof(bw), "%s", StrHelper::ftoa3(getBandwidth()));
sprintf(reply, "> %s,%s,%d,%d", freq, bw, (uint32_t)getSpreadFactor(), (uint32_t)getCodingRate());
return true;
}
if (startsWith(command, "set radio ")) {
const char* args = command + strlen("set radio ");
const size_t args_len = boundedLength(args, RADIO_ARGS_CAPACITY);
if (args_len >= RADIO_ARGS_CAPACITY) {
strcpy(reply, "Error, invalid radio params");
return true;
}
char tmp[RADIO_ARGS_CAPACITY];
memcpy(tmp, args, args_len + 1);
const char *parts[4];
float freq = 0.0f;
float bw = 0.0f;
int32_t sf = 0;
int32_t cr = 0;
if (splitRadioArgs(tmp, parts)
&& parseDecimalStrict(parts[0], freq)
&& parseDecimalStrict(parts[1], bw)
&& parseIntStrict(parts[2], 5, 12, sf)
&& parseIntStrict(parts[3], 5, 8, cr)
&& freq >= 150.0f && freq <= 2500.0f
&& isValidLoRaBandwidth(bw)) {
setSpreadFactor(static_cast<uint8_t>(sf));
setCodingRate(static_cast<uint8_t>(cr));
setFreq(freq);
setBandwidth(bw);
strcpy(reply, "OK - reboot to apply");
} else {
strcpy(reply, "Error, invalid radio params");
}
return true;
}
if (strcmp(command, "get freq") == 0) {
sprintf(reply, "> %s", StrHelper::ftoa(getFreq()));
return true;
}
if (strcmp(command, "get af") == 0) {
sprintf(reply, "> %s", StrHelper::ftoa(getAirtimeFactor()));
return true;
}
if (startsWith(command, "set af ")) {
float factor = 0.0f;
if (parseDecimalStrict(command + strlen("set af "), factor)
&& factor >= 0.0f && factor <= 9.0f) {
setAirtimeFactor(factor);
strcpy(reply, "OK");
} else {
strcpy(reply, "Error, invalid airtime factor");
}
return true;
}
if (strcmp(command, "get dutycycle") == 0) {
const float factor = getAirtimeFactor();
if (!isfinite(factor) || factor < 0.0f) {
strcpy(reply, "Error: invalid airtime factor");
} else {
const int tenths = static_cast<int>(1000.0f / (factor + 1.0f) + 0.5f);
sprintf(reply, "> %d.%d%%", tenths / 10, tenths % 10);
}
return true;
}
if (startsWith(command, "set dutycycle ")) {
float dc = 0.0f;
if (!parseDecimalStrict(command + strlen("set dutycycle "), dc)
|| dc < 1.0f || dc > 100.0f) {
strcpy(reply, "ERROR: dutycycle must be 1-100");
} else {
setAirtimeFactor((100.0f / dc) - 1.0f);
const float actual = 100.0f / (getAirtimeFactor() + 1.0f);
const int tenths = static_cast<int>(actual * 10.0f + 0.5f);
sprintf(reply, "OK - %d.%d%%", tenths / 10, tenths % 10);
}
return true;
}
if (strcmp(command, "get int.thresh") == 0) {
sprintf(reply, "> %d", (uint32_t) getIntThresh());
return true;
}
if (startsWith(command, "set int.thresh ")) {
int32_t threshold = 0;
if (parseIntStrict(command + strlen("set int.thresh "), 0, 255, threshold)) {
setIntThresh(static_cast<uint8_t>(threshold));
strcpy(reply, "OK");
} else {
strcpy(reply, "Error, must be 0-255");
}
return true;
}
if (strcmp(command, "get cad") == 0) {
sprintf(reply, "> %s", isCadEnabled() ? "on" : "off");
return true;
}
if (startsWith(command, "set cad ")) {
const char* value = command + strlen("set cad ");
if (strcmp(value, "on") == 0 || strcmp(value, "off") == 0) {
setCadEnabled(strcmp(value, "on") == 0);
strcpy(reply, "OK");
} else {
strcpy(reply, "Error: use set cad on|off");
}
return true;
}
if (strcmp(command, "get radio.rxgain") == 0) {
sprintf(reply, "> %s", getRxGain() != 0 ? "on" : "off");
return true;
}
if (startsWith(command, "set radio.rxgain ")) {
const char* value = command + strlen("set radio.rxgain ");
if (strcmp(value, "on") != 0 && strcmp(value, "off") != 0) {
strcpy(reply, "Error: use set radio.rxgain on|off");
return true;
}
const bool enabled = strcmp(value, "on") == 0;
if (radio_driver.setRxBoostedGainMode(enabled)) {
setRxGain(enabled ? 1 : 0);
strcpy(reply, "OK");
} else {
strcpy(reply, "Error: unsupported");
}
return true;
}
if (strcmp(command, "get tx") == 0) {
sprintf(reply, "> %d", static_cast<int>(getTxPower()));
return true;
}
if (startsWith(command, "set tx ")) {
int32_t dbm = 0;
if (!parseIntStrict(command + strlen("set tx "), -128, 127, dbm)) {
strcpy(reply, "Error: invalid TX power");
} else if (!radio_driver.setTxPower(static_cast<int8_t>(dbm))) {
strcpy(reply, "Error: TX power rejected");
} else {
setTxPower(static_cast<int8_t>(dbm));
strcpy(reply, "OK");
}
return true;
}
if (strcmp(command, "get rxdelay") == 0) {
sprintf(reply, "> %s", StrHelper::ftoa(getRxDelay()));
return true;
}
if (startsWith(command, "set rxdelay ")) {
float db = 0.0f;
if (parseDecimalStrict(command + strlen("set rxdelay "), db)
&& db >= 0.0f && db <= 20.0f) {
setRxDelay(db);
strcpy(reply, "OK");
} else {
strcpy(reply, "Error, must be 0-20");
}
return true;
}
if (strcmp(command, "get agc.reset.interval") == 0) {
sprintf(reply, "> %d", (uint32_t) getAgcResetInt());
return true;
}
if (startsWith(command, "set agc.reset.interval ")) {
int32_t seconds = 0;
if (parseIntStrict(command + strlen("set agc.reset.interval "), 0, 255, seconds)) {
setAgcResetInt(static_cast<uint8_t>(seconds));
sprintf(reply, "OK - interval rounded to %d", (uint32_t) getAgcResetInt());
} else {
strcpy(reply, "Error, must be 0-255");
}
return true;
}
if (strcmp(command, "get path.hash.mode") == 0) {
sprintf(reply, "> %d", (uint32_t)getHashMode());
return true;
}
if (startsWith(command, "set path.hash.mode ")) {
int32_t mode = 0;
if (parseIntStrict(command + strlen("set path.hash.mode "), 0, 2, mode)) {
setHashMode(static_cast<uint8_t>(mode));
strcpy(reply, "OK");
} else {
strcpy(reply, "Error, must be 0,1, or 2");
}
return true;
}
if (strcmp(command, "get multi.acks") == 0) {
sprintf(reply, "> %d", (uint32_t) getMultiAcks());
return true;
}
if (startsWith(command, "set multi.acks ")) {
int32_t enabled = 0;
if (parseIntStrict(command + strlen("set multi.acks "), 0, 1, enabled)) {
setMultiAcks(static_cast<uint8_t>(enabled));
strcpy(reply, "OK");
} else {
strcpy(reply, "Error, must be 0 or 1");
}
return true;
}
if (strcmp(command, "get txdelay") == 0) {
sprintf(reply, "> %s", StrHelper::ftoa(getFloodTxDelay()));
return true;
}
if (startsWith(command, "set txdelay ")) {
float f = 0.0f;
if (parseDecimalStrict(command + strlen("set txdelay "), f)
&& f >= 0.0f && f <= 2.0f) {
setFloodTxDelay(f);
strcpy(reply, "OK");
} else {
strcpy(reply, "Error, must be 0-2");
}
return true;
}
if (strcmp(command, "get direct.txdelay") == 0) {
sprintf(reply, "> %s", StrHelper::ftoa(getDirectTxDelay()));
return true;
}
if (startsWith(command, "set direct.txdelay ")) {
float f = 0.0f;
if (parseDecimalStrict(command + strlen("set direct.txdelay "), f)
&& f >= 0.0f && f <= 2.0f) {
setDirectTxDelay(f);
strcpy(reply, "OK");
} else {
strcpy(reply, "Error, must be 0-2");
}
return true;
}
return false; // not handled
}
+69
View File
@@ -0,0 +1,69 @@
#pragma once
#include "ConfigSerializer.h"
#include "KeyValueStore.h"
class CommonRadioPrefs : public ConfigSerializer, public KeyValueStore {
bool _is_dirty = false;
protected:
CommonRadioPrefs() { }
public:
void markDirty() { _is_dirty = true; }
void clearDirty() { _is_dirty = false; }
bool isDirty() const { return _is_dirty; }
virtual float getFreq() const = 0;
virtual void setFreq(float f) = 0;
virtual float getBandwidth() const = 0;
virtual void setBandwidth(float bw) = 0;
virtual uint8_t getSpreadFactor() const = 0;
virtual void setSpreadFactor(uint8_t sf) = 0;
virtual uint8_t getCodingRate() const = 0;
virtual void setCodingRate(uint8_t cr) = 0;
virtual float getAirtimeFactor() const = 0;
virtual void setAirtimeFactor(float af) = 0;
virtual bool isCadEnabled() const = 0;
virtual void setCadEnabled(bool en) = 0;
virtual uint8_t getIntThresh() const = 0;
virtual void setIntThresh(uint8_t t) = 0;
virtual uint8_t getRxGain() const = 0;
virtual void setRxGain(uint8_t g) = 0;
virtual int8_t getTxPower() const = 0;
virtual void setTxPower(int8_t dbm) = 0;
virtual float getRxDelay() const = 0;
virtual void setRxDelay(float d) = 0;
virtual uint8_t getAgcResetInt() const = 0;
virtual void setAgcResetInt(uint8_t secs) = 0;
virtual uint8_t getHashMode() const = 0;
virtual void setHashMode(uint8_t m) = 0;
virtual uint8_t getMultiAcks() const = 0;
virtual void setMultiAcks(uint8_t m) = 0;
virtual float getFloodTxDelay() const = 0;
virtual void setFloodTxDelay(float d) = 0;
virtual float getDirectTxDelay() const = 0;
virtual void setDirectTxDelay(float d) = 0;
virtual uint8_t getFEMRxGain() const = 0;
virtual void setFEMRxGain(uint8_t g) = 0;
virtual uint8_t getFEMTxGain() const = 0;
virtual void setFEMTxGain(uint8_t g) = 0;
bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply);
bool setByKey(const char* key, const char* value) override; // for dynamic key/value access
bool getByKey(const char* key, char* value, size_t max_len) override;
};
+6
View File
@@ -90,6 +90,12 @@ int ConfigSerializer::Context::readNext() {
case EXPECT_COMMA_OR_KEY:
if (c == ',') { rd_mode = EXPECT_KEY; return TOK_WHITESPACE; }
case EXPECT_KEY:
// Empty objects are valid and are emitted by serializers such as the
// dynamic `custom` preferences object before it has any entries.
if (rd_len == 0 && c == '}') {
rd_mode = EXPECT_COMMA_OR_KEY_OR_CLOSE;
return TOK_END_OBJ;
}
if (rd_len > 0 && c == ':') { rd_buf[rd_len] = 0; rd_len = 0; rd_mode = EXPECT_VAL_OR_OBJ; return TOK_KEY; }
if (rd_len == 0 && is_whitespace(c)) return TOK_WHITESPACE;
if (rd_len < CONFIG_MAX_KEYLEN-1 && is_key_char(c)) { rd_buf[rd_len++] = c; return TOK_WHITESPACE; }
+9 -1
View File
@@ -17,7 +17,9 @@
class ConfigSerializer {
bool _first;
int8_t _depth;
bool _dirty = false;
protected:
enum OP { READ, WRITE };
class Context {
@@ -38,13 +40,14 @@ class ConfigSerializer {
const char* getToken() const { return rd_buf; }
bool keyMatch(int8_t depth, const char* key) { return strcmp(key, _keys[depth]) == 0; }
void setKey(uint8_t depth, const char* key) { strcpy(_keys[depth], key); }
const char* getKey(uint8_t depth) { return _keys[depth]; }
};
Context* _context = NULL;
int8_t getDepth() const { return _depth; }
void writeComma();
protected:
ConfigSerializer() { }
void def(const char* key, char* value, size_t max_len); // max_len inclusive of null
@@ -62,7 +65,12 @@ protected:
virtual void structure() = 0;
void markDirty() { _dirty = true; }
public:
bool loadSerial(Stream& s);
bool saveSerial(Stream& s);
virtual bool isDirty() const { return _dirty; }
virtual void clearDirty() { _dirty = false; }
};
+6
View File
@@ -36,6 +36,12 @@ struct ContactInfo {
return shared_secret;
}
bool isFav() const { return flags & 0x01; }
bool isTelemBaseAllowed() const { return flags & 0x02; }
bool isTelemLocAllowed() const { return flags & 0x04; }
bool isTelemEnvAllowed() const { return flags & 0x08; }
bool isRemoteCLIAllowed() const { return flags & 0x10; }
private:
mutable uint8_t shared_secret[PUB_KEY_SIZE];
};
+228
View File
@@ -0,0 +1,228 @@
#include "DynamicConfigSerializer.h"
#include <string.h>
#define PROP_SEP_CHAR '|'
#define KEY_SEP_CHAR ':'
namespace {
size_t boundedLength(const char* text, size_t capacity) {
if (text == nullptr) return capacity;
size_t length = 0;
while (length < capacity && text[length] != 0) length++;
return length;
}
bool isValidKey(const char* key, size_t& key_len) {
key_len = boundedLength(key, CONFIG_MAX_KEYLEN);
if (key_len == 0 || key_len >= CONFIG_MAX_KEYLEN) return false;
for (size_t i = 0; i < key_len; i++) {
const char c = key[i];
if (!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_')) {
return false;
}
}
return true;
}
bool isValidValue(const char* value, size_t& value_len) {
value_len = boundedLength(value, MAX_DYNAMIC_CONFG);
if (value_len >= MAX_DYNAMIC_CONFG) return false;
for (size_t i = 0; i < value_len; i++) {
const uint8_t c = static_cast<uint8_t>(value[i]);
// ':' and '|' are internal delimiters. Control characters would produce
// invalid serialized configuration because ConfigSerializer only escapes
// a subset of them.
if (c == static_cast<uint8_t>(KEY_SEP_CHAR)
|| c == static_cast<uint8_t>(PROP_SEP_CHAR)
|| c < 0x20 || c == 0x7F) {
return false;
}
}
return true;
}
bool appendEntry(char* destination, size_t capacity, size_t& used,
const char* key, size_t key_len,
const char* value, size_t value_len) {
if (destination == nullptr || key == nullptr || value == nullptr
|| used >= capacity) {
return false;
}
const size_t separator_len = used == 0 ? 0 : 1;
const size_t available = capacity - used;
if (separator_len + key_len + 1 + value_len + 1 > available) return false;
if (separator_len != 0) destination[used++] = PROP_SEP_CHAR;
memcpy(destination + used, key, key_len);
used += key_len;
destination[used++] = KEY_SEP_CHAR;
memcpy(destination + used, value, value_len);
used += value_len;
destination[used] = 0;
return true;
}
} // namespace
bool DynamicConfigSerializer::setByKeyPrv(const char* key, const char* value) {
size_t key_len = 0;
size_t value_len = 0;
if (!isValidKey(key, key_len) || !isValidValue(value, value_len)) return false;
if (_fallback && _fallback->setByKey(key, value)) return true;
const size_t config_len = boundedLength(_config, sizeof(_config));
if (config_len >= sizeof(_config)) return false;
char tmp[MAX_DYNAMIC_CONFG];
memcpy(tmp, _config, config_len + 1);
char new_config[MAX_DYNAMIC_CONFG];
new_config[0] = 0;
size_t used = 0;
char* item = tmp;
while (*item != 0) {
char* next = strchr(item, PROP_SEP_CHAR);
if (next != nullptr) *next = 0;
char* item_separator = strchr(item, KEY_SEP_CHAR);
if (item_separator == nullptr || item_separator == item) return false;
*item_separator = 0;
size_t item_key_len = 0;
size_t item_value_len = 0;
const char* item_value = item_separator + 1;
if (!isValidKey(item, item_key_len)
|| !isValidValue(item_value, item_value_len)) {
return false;
}
const bool replacing = item_key_len == key_len
&& memcmp(item, key, key_len) == 0;
if (!replacing
&& !appendEntry(new_config, sizeof(new_config), used,
item, item_key_len, item_value, item_value_len)) {
return false;
}
if (next == nullptr) break;
item = next + 1;
if (*item == 0) return false; // reject a trailing/consecutive separator
}
if (!appendEntry(new_config, sizeof(new_config), used,
key, key_len, value, value_len)) {
return false;
}
memcpy(_config, new_config, used + 1);
return true;
}
bool DynamicConfigSerializer::setByKey(const char* key, const char* value) {
if (setByKeyPrv(key, value)) {
markDirty();
return true;
}
return false;
}
bool DynamicConfigSerializer::getByKey(const char* key, char* value, size_t max_len) {
if (value == nullptr || max_len == 0) return false;
size_t key_len = 0;
if (!isValidKey(key, key_len)) {
value[0] = 0;
return false;
}
if (_fallback && _fallback->getByKey(key, value, max_len)) return true;
value[0] = 0;
const size_t config_len = boundedLength(_config, sizeof(_config));
if (config_len >= sizeof(_config)) return false;
char tmp[MAX_DYNAMIC_CONFG];
memcpy(tmp, _config, config_len + 1);
char* item = tmp;
while (*item != 0) {
char* next = strchr(item, PROP_SEP_CHAR);
if (next != nullptr) *next = 0;
char* item_separator = strchr(item, KEY_SEP_CHAR);
if (item_separator == nullptr || item_separator == item) return false;
*item_separator = 0;
size_t item_key_len = 0;
size_t item_value_len = 0;
const char* item_value = item_separator + 1;
if (!isValidKey(item, item_key_len)
|| !isValidValue(item_value, item_value_len)) {
return false;
}
if (item_key_len == key_len && memcmp(item, key, key_len) == 0) {
if (item_value_len >= max_len) return false;
memcpy(value, item_value, item_value_len + 1);
return true;
}
if (next == nullptr) break;
item = next + 1;
if (*item == 0) return false;
}
return false;
}
void DynamicConfigSerializer::structure() {
if (_context->op() == OP::WRITE) {
const size_t config_len = boundedLength(_config, sizeof(_config));
if (config_len >= sizeof(_config)) {
_context->success = false;
return;
}
char tmp[MAX_DYNAMIC_CONFG];
memcpy(tmp, _config, config_len + 1);
char* item = tmp;
while (*item != 0) {
char* next = strchr(item, PROP_SEP_CHAR);
if (next != nullptr) *next = 0;
char* item_separator = strchr(item, KEY_SEP_CHAR);
if (item_separator == nullptr || item_separator == item) {
_context->success = false;
return;
}
*item_separator = 0;
size_t item_key_len = 0;
size_t item_value_len = 0;
char* item_value = item_separator + 1;
if (!isValidKey(item, item_key_len)
|| !isValidValue(item_value, item_value_len)) {
_context->success = false;
return;
}
def(item, item_value, item_value_len + 1);
if (next == nullptr) break;
item = next + 1;
if (*item == 0) {
_context->success = false;
return;
}
}
} else {
if (!setByKeyPrv(_context->getKey(getDepth()), _context->getToken())) {
_context->success = false;
}
}
}
+23
View File
@@ -0,0 +1,23 @@
#pragma once
#include "ConfigSerializer.h"
#include "KeyValueStore.h"
#ifndef MAX_DYNAMIC_CONFG
#define MAX_DYNAMIC_CONFG 128
#endif
class DynamicConfigSerializer : public ConfigSerializer, public KeyValueStore {
char _config[MAX_DYNAMIC_CONFG];
KeyValueStore* _fallback;
bool setByKeyPrv(const char* key, const char* value);
protected:
void structure() override;
public:
DynamicConfigSerializer(KeyValueStore* fallback = NULL) : _fallback(fallback) { _config[0] = 0; }
bool setByKey(const char* key, const char* value) override;
bool getByKey(const char* key, char* value, size_t max_len) override;
};
+3
View File
@@ -25,6 +25,7 @@
#if !defined(LIGHTWEIGHT_WIFI_OTA)
class AsyncWebServer;
#endif
#include <helpers/KeyValueStore.h>
class ESP32Board : public mesh::MainBoard {
protected:
@@ -72,6 +73,8 @@ public:
#endif
}
void attachDynamicPrefs(KeyValueStore* prefs) { (void)prefs; } // no-op
// Temperature from ESP32 MCU
float getMCUTemperature() override {
uint32_t raw = 0;
+11
View File
@@ -0,0 +1,11 @@
#pragma once
#include <stdint.h>
#include <string.h>
class KeyValueStore {
protected:
KeyValueStore() { }
public:
virtual bool setByKey(const char* key, const char* value) { return false; }
virtual bool getByKey(const char* key, char* value, size_t max_len) { return false; }
};
+3
View File
@@ -2,6 +2,7 @@
#include <Arduino.h>
#include <MeshCore.h>
#include <helpers/KeyValueStore.h>
#if defined(NRF52_PLATFORM)
@@ -73,6 +74,8 @@ public:
bool isUsbDataConnected() override;
bool isUsbHostConnected() override;
void attachDynamicPrefs(KeyValueStore* prefs) { (void)prefs; } // no-op
#ifdef NRF52_POWER_MANAGEMENT
uint16_t getBootVoltage() override { return boot_voltage_mv; }
virtual uint32_t getResetReason() const override { return reset_reason; }
+3 -1
View File
@@ -4,8 +4,10 @@
#include <stdint.h>
#define TXT_TYPE_PLAIN 0 // a plain text message
#define TXT_TYPE_CLI_DATA 1 // a CLI command
#define TXT_TYPE_CLI_DATA 1 // a CLI command -or- reply
#define TXT_TYPE_SIGNED_PLAIN 2 // plain text, signed by sender
#define TXT_TYPE_CLI_COMMAND 3 // a CLI command (explictly)
#define DATA_TYPE_RESERVED 0x0000 // reserved for future use
#define DATA_TYPE_DEV 0xFFFF // developer namespace for experimenting with group/channel datagrams and building apps
+4 -1
View File
@@ -2,6 +2,7 @@
#include <MeshCore.h>
#include <Arduino.h>
#include <helpers/KeyValueStore.h>
class STM32Board : public mesh::MainBoard {
protected:
@@ -12,6 +13,8 @@ public:
startup_reason = BD_STARTUP_NORMAL;
}
void attachDynamicPrefs(KeyValueStore* prefs) { (void)prefs; } // no-op
uint8_t getStartupReason() const override { return startup_reason; }
uint16_t getBattMilliVolts() override {
@@ -42,4 +45,4 @@ public:
#endif
bool startOTAUpdate(const char* id, char reply[], bool force_ap = false) override { return false; };
};
};
+14
View File
@@ -0,0 +1,14 @@
#pragma once
#include <stdint.h>
// CommonRadioPrefs applies live settings through the target's global radio
// driver. Native serializer tests do not build a hardware variant, so provide
// the narrow interface that helper needs.
class NativeRadioDriverMock {
public:
bool setRxBoostedGainMode(bool) { return true; }
bool setTxPower(int8_t) { return true; }
};
inline NativeRadioDriverMock radio_driver;
@@ -14,6 +14,16 @@ static bool enqueue(TestFrame queue[], size_t& count, size_t capacity,
return mesh::enqueueCompanionFrame(queue, count, capacity, &code, 1);
}
TEST(CompanionFrameQueue, HighBitSeparatesResponsesFromPushes) {
const uint8_t highest_command_or_response = 0x7F;
const uint8_t first_push = 0x80;
const uint8_t unknown_high_code = 0x98;
EXPECT_FALSE(mesh::isCompanionPushFrame(&highest_command_or_response, 1));
EXPECT_TRUE(mesh::isCompanionPushFrame(&first_push, 1));
EXPECT_TRUE(mesh::isCompanionPushFrame(&unknown_high_code, 1));
}
TEST(CompanionFrameQueue, PushTrafficLeavesOneResponseSlot) {
TestFrame queue[4] = {};
size_t count = 0;
@@ -1,5 +1,6 @@
#include <gtest/gtest.h>
#include "helpers/ConfigSerializer.h"
#include "helpers/DynamicConfigSerializer.h"
class NativeFileSystem {
public:
@@ -314,6 +315,90 @@ TEST(NodePrefs, ExplicitWrappedBridgeFormatReplacesRawInMemoryValue) {
EXPECT_EQ(mesh::bridge::ESPNOW_FORMAT_WRAPPED, loaded.bridge_format);
}
TEST(DynamicConfigSerializer, GetSet_Basic) {
DynamicConfigSerializer data;
bool s1 = data.setByKey("age", "11");
bool s2 = data.setByKey("name", "Scott");
EXPECT_TRUE(s1 && s2);
char tmp[32];
bool g1 = data.getByKey("age", tmp, 31);
EXPECT_TRUE(g1);
EXPECT_STREQ("11", tmp);
bool g2 = data.getByKey("name", tmp, 31);
EXPECT_TRUE(g2);
EXPECT_STREQ("Scott", tmp);
}
TEST(DynamicConfigSerializer, Set_Replaces) {
DynamicConfigSerializer data;
bool s1 = data.setByKey("age", "11");
bool s2 = data.setByKey("name", "Scott");
EXPECT_TRUE(s1 && s2);
bool s3 = data.setByKey("age", "333");
EXPECT_TRUE(s3);
char tmp[32];
bool g1 = data.getByKey("age", tmp, 31);
EXPECT_TRUE(g1);
EXPECT_STREQ("333", tmp);
bool g2 = data.getByKey("name", tmp, 31);
EXPECT_TRUE(g2);
EXPECT_STREQ("Scott", tmp);
}
TEST(DynamicConfigSerializer, GetUnknown_Fail) {
DynamicConfigSerializer data;
bool s1 = data.setByKey("age", "11");
EXPECT_TRUE(s1);
char tmp[32];
bool g2 = data.getByKey("name", tmp, 31);
EXPECT_FALSE(g2);
}
TEST(DynamicConfigSerializer, SaveCustom_Basic) {
MockPrintStream s;
DynamicConfigSerializer data;
bool s1 = data.setByKey("age", "11");
bool s2 = data.setByKey("name", "Scott");
EXPECT_TRUE(s1 && s2);
bool success = data.saveSerial(s);
EXPECT_TRUE(success);
auto l = s.getLength();
char tmp[128];
memcpy(tmp, s.getBytes(), l);
tmp[l] = 0;
const char* expect = "{age:\"11\",name:\"Scott\"}";
EXPECT_STREQ(expect, tmp);
}
TEST(DynamicConfigSerializer, LoadCustom_Basic) {
MockInputStream s("{age:\"" TEST_INT_S "\",name:\"Scott\"}");
DynamicConfigSerializer data;
bool success = data.loadSerial(s);
EXPECT_TRUE(success);
char tmp[32];
bool g1 = data.getByKey("age", tmp, 31);
EXPECT_TRUE(g1);
EXPECT_STREQ(TEST_INT_S, tmp);
bool g2 = data.getByKey("name", tmp, 31);
EXPECT_TRUE(g2);
EXPECT_STREQ("Scott", tmp);
}
// ── main ───────────────────────────────────────────────────────
+54
View File
@@ -138,3 +138,57 @@ bool T096Board::canControlLoRaFemLna() const {
bool T096Board::isLoRaFemLnaEnabled() const {
return loRaFEMControl.isLNAEnabled();
}
void T096Board::attachDynamicPrefs(KeyValueStore* prefs) {
_prefs = prefs;
if (_prefs == nullptr) {
return;
}
char radio_fem_rxgain[8] = { 0 };
_prefs->getByKey("fem_rxgain", radio_fem_rxgain, 7); // get initial values
setLoRaFemLnaEnabled(strcmp(radio_fem_rxgain, "1") == 0);
}
bool T096Board::handleCommand(const char* command, uint32_t sender_timestamp, char* reply) {
(void)sender_timestamp;
if (command == nullptr || reply == nullptr) {
return false;
}
if (strcmp(command, "get radio.fem.rxgain") == 0) {
if (!loRaFEMControl.isLnaCanControl()) {
strcpy(reply, "Error: unsupported");
} else {
sprintf(reply, "> %s", isLoRaFemLnaEnabled() ? "on" : "off");
}
return true;
}
if (strncmp(command, "set radio.fem.rxgain ", 21) == 0) {
if (!loRaFEMControl.isLnaCanControl()) {
strcpy(reply, "Error: unsupported");
} else if (_prefs == nullptr) {
strcpy(reply, "Error: preferences unavailable");
} else if (strcmp(&command[21], "on") == 0) {
if (setLoRaFemLnaEnabled(true)) {
_prefs->setByKey("fem_rxgain", "1");
strcpy(reply, "OK - LoRa FEM RX gain on");
} else {
strcpy(reply, "Error: failed to apply LoRa FEM RX gain");
}
} else if (strcmp(&command[21], "off") == 0) {
if (setLoRaFemLnaEnabled(false)) {
_prefs->setByKey("fem_rxgain", "0");
strcpy(reply, "OK - LoRa FEM RX gain off");
} else {
strcpy(reply, "Error: failed to apply LoRa FEM RX gain");
}
} else {
strcpy(reply, "Error: state must be on or off");
}
return true;
}
return false; // not handled
}
+5
View File
@@ -4,9 +4,12 @@
#include <Arduino.h>
#include <helpers/NRF52Board.h>
#include <helpers/RefCountedDigitalPin.h>
#include <helpers/KeyValueStore.h>
#include "LoRaFEMControl.h"
class T096Board : public NRF52BoardDCDC {
KeyValueStore* _prefs = NULL;
protected:
#ifdef NRF52_POWER_MANAGEMENT
void initiateShutdown(uint8_t reason) override;
@@ -19,6 +22,8 @@ public:
T096Board() :periph_power(PIN_VEXT_EN,PIN_VEXT_EN_ACTIVE), NRF52Board("T096_OTA") {}
void begin();
void attachDynamicPrefs(KeyValueStore* prefs);
bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply) override;
void onBeforeTransmit(void) override;
void onAfterTransmit(void) override;
@@ -91,3 +91,57 @@ void HeltecTrackerV2Board::begin() {
bool HeltecTrackerV2Board::isLoRaFemLnaEnabled() const {
return loRaFEMControl.isLNAEnabled();
}
void HeltecTrackerV2Board::attachDynamicPrefs(KeyValueStore* prefs) {
_prefs = prefs;
if (_prefs == nullptr) {
return;
}
char radio_fem_rxgain[8] = { 0 };
_prefs->getByKey("fem_rxgain", radio_fem_rxgain, 7); // get initial values
setLoRaFemLnaEnabled(strcmp(radio_fem_rxgain, "1") == 0);
}
bool HeltecTrackerV2Board::handleCommand(const char* command, uint32_t sender_timestamp, char* reply) {
(void)sender_timestamp;
if (command == nullptr || reply == nullptr) {
return false;
}
if (strcmp(command, "get radio.fem.rxgain") == 0) {
if (!loRaFEMControl.isLnaCanControl()) {
strcpy(reply, "Error: unsupported");
} else {
sprintf(reply, "> %s", isLoRaFemLnaEnabled() ? "on" : "off");
}
return true;
}
if (strncmp(command, "set radio.fem.rxgain ", 21) == 0) {
if (!loRaFEMControl.isLnaCanControl()) {
strcpy(reply, "Error: unsupported");
} else if (_prefs == nullptr) {
strcpy(reply, "Error: preferences unavailable");
} else if (strcmp(&command[21], "on") == 0) {
if (setLoRaFemLnaEnabled(true)) {
_prefs->setByKey("fem_rxgain", "1");
strcpy(reply, "OK - LoRa FEM RX gain on");
} else {
strcpy(reply, "Error: failed to apply LoRa FEM RX gain");
}
} else if (strcmp(&command[21], "off") == 0) {
if (setLoRaFemLnaEnabled(false)) {
_prefs->setByKey("fem_rxgain", "0");
strcpy(reply, "OK - LoRa FEM RX gain off");
} else {
strcpy(reply, "Error: failed to apply LoRa FEM RX gain");
}
} else {
strcpy(reply, "Error: state must be on or off");
}
return true;
}
return false; // not handled
}
@@ -6,6 +6,7 @@
#include "LoRaFEMControl.h"
class HeltecTrackerV2Board : public ESP32Board {
KeyValueStore* _prefs = NULL;
public:
RefCountedDigitalPin periph_power;
@@ -14,6 +15,9 @@ public:
HeltecTrackerV2Board() : periph_power(PIN_VEXT_EN,PIN_VEXT_EN_ACTIVE) { }
void begin();
void attachDynamicPrefs(KeyValueStore* prefs);
bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply) override;
void onBeforeTransmit(void) override;
void onAfterTransmit(void) override;
void powerOff() override;
+54
View File
@@ -80,3 +80,57 @@ void HeltecV4Board::begin() {
bool HeltecV4Board::isLoRaFemLnaEnabled() const {
return loRaFEMControl.isLNAEnabled();
}
void HeltecV4Board::attachDynamicPrefs(KeyValueStore* prefs) {
_prefs = prefs;
if (_prefs == nullptr) {
return;
}
char radio_fem_rxgain[8] = { 0 };
_prefs->getByKey("fem_rxgain", radio_fem_rxgain, 7); // get initial values
setLoRaFemLnaEnabled(strcmp(radio_fem_rxgain, "1") == 0);
}
bool HeltecV4Board::handleCommand(const char* command, uint32_t sender_timestamp, char* reply) {
(void)sender_timestamp;
if (command == nullptr || reply == nullptr) {
return false;
}
if (strcmp(command, "get radio.fem.rxgain") == 0) {
if (!loRaFEMControl.isLnaCanControl()) {
strcpy(reply, "Error: unsupported");
} else {
sprintf(reply, "> %s", isLoRaFemLnaEnabled() ? "on" : "off");
}
return true;
}
if (strncmp(command, "set radio.fem.rxgain ", 21) == 0) {
if (!loRaFEMControl.isLnaCanControl()) {
strcpy(reply, "Error: unsupported");
} else if (_prefs == nullptr) {
strcpy(reply, "Error: preferences unavailable");
} else if (strcmp(&command[21], "on") == 0) {
if (setLoRaFemLnaEnabled(true)) {
_prefs->setByKey("fem_rxgain", "1");
strcpy(reply, "OK - LoRa FEM RX gain on");
} else {
strcpy(reply, "Error: failed to apply LoRa FEM RX gain");
}
} else if (strcmp(&command[21], "off") == 0) {
if (setLoRaFemLnaEnabled(false)) {
_prefs->setByKey("fem_rxgain", "0");
strcpy(reply, "OK - LoRa FEM RX gain off");
} else {
strcpy(reply, "Error: failed to apply LoRa FEM RX gain");
}
} else {
strcpy(reply, "Error: state must be on or off");
}
return true;
}
return false; // not handled
}
+4
View File
@@ -10,6 +10,7 @@
#endif
class HeltecV4Board : public ESP32Board {
KeyValueStore* _prefs = NULL;
protected:
float adc_mult = ADC_MULTIPLIER;
@@ -20,6 +21,9 @@ public:
HeltecV4Board() : periph_power(PIN_VEXT_EN,PIN_VEXT_EN_ACTIVE) { }
void begin();
void attachDynamicPrefs(KeyValueStore* prefs);
bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply) override;
void onBeforeTransmit(void) override;
void onAfterTransmit(void) override;
void powerOff() override;
+3
View File
@@ -2,6 +2,7 @@
#include <MeshCore.h>
#include <Arduino.h>
#include <helpers/KeyValueStore.h>
// built-ins
#define PIN_VBAT_READ 26
@@ -16,6 +17,8 @@ public:
void begin();
uint8_t getStartupReason() const override { return startup_reason; }
void attachDynamicPrefs(KeyValueStore* prefs) { (void)prefs; } // no-op
void onBeforeTransmit() override {
digitalWrite(LED_BUILTIN, HIGH); // turn TX LED on
}
@@ -44,3 +44,95 @@ bool StationG3Board::canControlLoRaFemPaGain() const {
bool StationG3Board::isLoRaFemPaGainEnabled() const {
return loRaFEMControl.isPAGainEnabled();
}
void StationG3Board::attachDynamicPrefs(KeyValueStore* prefs) {
_prefs = prefs;
if (_prefs == nullptr) {
return;
}
char gain[8];
gain[0] = 0;
_prefs->getByKey("fem_rxgain", gain, 7); // get initial values
setLoRaFemLnaEnabled(strcmp(gain, "1") == 0);
gain[0] = 0;
_prefs->getByKey("fem_txgain", gain, 7); // get initial values
setLoRaFemPaGainEnabled(strcmp(gain, "1") == 0);
}
bool StationG3Board::handleCommand(const char* command, uint32_t sender_timestamp, char* reply) {
(void)sender_timestamp;
if (command == nullptr || reply == nullptr) {
return false;
}
if (strcmp(command, "get radio.fem.rxgain") == 0) {
if (!loRaFEMControl.canControlLNA()) {
strcpy(reply, "Error: unsupported");
} else {
sprintf(reply, "> %s", isLoRaFemLnaEnabled() ? "on" : "off");
}
return true;
}
if (strncmp(command, "set radio.fem.rxgain ", 21) == 0) {
if (!loRaFEMControl.canControlLNA()) {
strcpy(reply, "Error: unsupported");
} else if (_prefs == nullptr) {
strcpy(reply, "Error: preferences unavailable");
} else if (strcmp(&command[21], "on") == 0) {
if (setLoRaFemLnaEnabled(true)) {
_prefs->setByKey("fem_rxgain", "1");
strcpy(reply, "OK - LoRa FEM RX gain on");
} else {
strcpy(reply, "Error: failed to apply LoRa FEM RX gain");
}
} else if (strcmp(&command[21], "off") == 0) {
if (setLoRaFemLnaEnabled(false)) {
_prefs->setByKey("fem_rxgain", "0");
strcpy(reply, "OK - LoRa FEM RX gain off");
} else {
strcpy(reply, "Error: failed to apply LoRa FEM RX gain");
}
} else {
strcpy(reply, "Error: state must be on or off");
}
return true;
}
if (strcmp(command, "get radio.fem.txgain") == 0) {
if (!loRaFEMControl.canControlPAGain()) {
strcpy(reply, "Error: unsupported");
} else {
sprintf(reply, "> %s", isLoRaFemPaGainEnabled() ? "on" : "off");
}
return true;
}
if (strncmp(command, "set radio.fem.txgain ", 21) == 0) {
if (!loRaFEMControl.canControlPAGain()) {
strcpy(reply, "Error: unsupported");
} else if (_prefs == nullptr) {
strcpy(reply, "Error: preferences unavailable");
} else if (strcmp(&command[21], "on") == 0) {
if (setLoRaFemPaGainEnabled(true)) {
_prefs->setByKey("fem_txgain", "1");
strcpy(reply, "OK - LoRa FEM TX gain on");
} else {
strcpy(reply, "Error: failed to apply LoRa FEM TX gain");
}
} else if (strcmp(&command[21], "off") == 0) {
if (setLoRaFemPaGainEnabled(false)) {
_prefs->setByKey("fem_txgain", "0");
strcpy(reply, "OK - LoRa FEM TX gain off");
} else {
strcpy(reply, "Error: failed to apply LoRa FEM TX gain");
}
} else {
strcpy(reply, "Error: state must be on or off");
}
return true;
}
return false; // not handled
}
@@ -6,6 +6,8 @@
#include "LoRaFEMControl.h"
class StationG3Board : public ESP32Board {
KeyValueStore* _prefs = NULL;
public:
LoRaFEMControl loRaFEMControl;
@@ -25,6 +27,10 @@ public:
}
}
void attachDynamicPrefs(KeyValueStore* prefs);
bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply) override;
void setPrimaryLNAEnable(bool enabled) {
loRaFEMControl.setLNAEnable(enabled);
}
@@ -2,6 +2,7 @@
#include <Arduino.h>
#include <MeshCore.h>
#include <helpers/KeyValueStore.h>
// LoRa radio module pins for Waveshare RP2040-LoRa-HF/LF
// https://files.waveshare.com/wiki/RP2040-LoRa/Rp2040-lora-sch.pdf
@@ -32,6 +33,8 @@ public:
void begin();
uint8_t getStartupReason() const override { return startup_reason; }
void attachDynamicPrefs(KeyValueStore* prefs) { (void)prefs; } // no-op
#ifdef P_LORA_TX_LED
void onBeforeTransmit() override { digitalWrite(P_LORA_TX_LED, HIGH); }
void onAfterTransmit() override { digitalWrite(P_LORA_TX_LED, LOW); }
+3
View File
@@ -2,6 +2,7 @@
#include <Arduino.h>
#include <MeshCore.h>
#include <helpers/KeyValueStore.h>
/*
* This board has no built-in way to read battery voltage.
@@ -30,6 +31,8 @@ public:
void begin();
uint8_t getStartupReason() const override { return startup_reason; }
void attachDynamicPrefs(KeyValueStore* prefs) { (void)prefs; } // no-op
#ifdef P_LORA_TX_LED
void onBeforeTransmit() override { digitalWrite(P_LORA_TX_LED, HIGH); }
void onAfterTransmit() override { digitalWrite(P_LORA_TX_LED, LOW); }