mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-23 15:42:29 +00:00
Merge upstream dev companion CLI updates
This commit is contained in:
commit
2143d3e04b
46 files changed
+1976
-163
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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(×tamp, 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, ×tamp, 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, ×tamp, 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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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; }
|
||||
|
||||
@@ -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; }
|
||||
};
|
||||
@@ -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];
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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; }
|
||||
};
|
||||
@@ -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; }
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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; };
|
||||
};
|
||||
};
|
||||
@@ -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 ───────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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); }
|
||||
|
||||
@@ -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); }
|
||||
|
||||
Reference in new issue
Block a user