Merge branch 'dev' into ext-trace

This commit is contained in:
Scott Powell
2025-11-25 15:17:48 +11:00
12 changed files with 475 additions and 44 deletions

312
docs/stats_binary_frames.md Normal file
View File

@@ -0,0 +1,312 @@
# Stats Binary Frame Structures
Binary frame structures for companion radio stats commands. All multi-byte integers use little-endian byte order.
## Command Codes
| Command | Code | Description |
|---------|------|-------------|
| `CMD_GET_STATS` | 56 | Get statistics (2-byte command: code + sub-type) |
### Stats Sub-Types
The `CMD_GET_STATS` command uses a 2-byte frame structure:
- **Byte 0:** `CMD_GET_STATS` (56)
- **Byte 1:** Stats sub-type:
- `STATS_TYPE_CORE` (0) - Get core device statistics
- `STATS_TYPE_RADIO` (1) - Get radio statistics
- `STATS_TYPE_PACKETS` (2) - Get packet statistics
## Response Codes
| Response | Code | Description |
|----------|------|-------------|
| `RESP_CODE_STATS` | 24 | Statistics response (2-byte response: code + sub-type) |
### Stats Response Sub-Types
The `RESP_CODE_STATS` response uses a 2-byte header structure:
- **Byte 0:** `RESP_CODE_STATS` (24)
- **Byte 1:** Stats sub-type (matches command sub-type):
- `STATS_TYPE_CORE` (0) - Core device statistics response
- `STATS_TYPE_RADIO` (1) - Radio statistics response
- `STATS_TYPE_PACKETS` (2) - Packet statistics response
---
## RESP_CODE_STATS + STATS_TYPE_CORE (24, 0)
**Total Frame Size:** 11 bytes
| Offset | Size | Type | Field Name | Description | Range/Notes |
|--------|------|------|------------|-------------|-------------|
| 0 | 1 | uint8_t | response_code | Always `0x18` (24) | - |
| 1 | 1 | uint8_t | stats_type | Always `0x00` (STATS_TYPE_CORE) | - |
| 2 | 2 | uint16_t | battery_mv | Battery voltage in millivolts | 0 - 65,535 |
| 4 | 4 | uint32_t | uptime_secs | Device uptime in seconds | 0 - 4,294,967,295 |
| 8 | 2 | uint16_t | errors | Error flags bitmask | - |
| 10 | 1 | uint8_t | queue_len | Outbound packet queue length | 0 - 255 |
### Example Structure (C/C++)
```c
struct StatsCore {
uint8_t response_code; // 0x18
uint8_t stats_type; // 0x00 (STATS_TYPE_CORE)
uint16_t battery_mv;
uint32_t uptime_secs;
uint16_t errors;
uint8_t queue_len;
} __attribute__((packed));
```
---
## RESP_CODE_STATS + STATS_TYPE_RADIO (24, 1)
**Total Frame Size:** 14 bytes
| Offset | Size | Type | Field Name | Description | Range/Notes |
|--------|------|------|------------|-------------|-------------|
| 0 | 1 | uint8_t | response_code | Always `0x18` (24) | - |
| 1 | 1 | uint8_t | stats_type | Always `0x01` (STATS_TYPE_RADIO) | - |
| 2 | 2 | int16_t | noise_floor | Radio noise floor in dBm | -140 to +10 |
| 4 | 1 | int8_t | last_rssi | Last received signal strength in dBm | -128 to +127 |
| 5 | 1 | int8_t | last_snr | SNR scaled by 4 | Divide by 4.0 for dB |
| 6 | 4 | uint32_t | tx_air_secs | Cumulative transmit airtime in seconds | 0 - 4,294,967,295 |
| 10 | 4 | uint32_t | rx_air_secs | Cumulative receive airtime in seconds | 0 - 4,294,967,295 |
### Example Structure (C/C++)
```c
struct StatsRadio {
uint8_t response_code; // 0x18
uint8_t stats_type; // 0x01 (STATS_TYPE_RADIO)
int16_t noise_floor;
int8_t last_rssi;
int8_t last_snr; // Divide by 4.0 to get actual SNR in dB
uint32_t tx_air_secs;
uint32_t rx_air_secs;
} __attribute__((packed));
```
---
## RESP_CODE_STATS + STATS_TYPE_PACKETS (24, 2)
**Total Frame Size:** 26 bytes
| Offset | Size | Type | Field Name | Description | Range/Notes |
|--------|------|------|------------|-------------|-------------|
| 0 | 1 | uint8_t | response_code | Always `0x18` (24) | - |
| 1 | 1 | uint8_t | stats_type | Always `0x02` (STATS_TYPE_PACKETS) | - |
| 2 | 4 | uint32_t | recv | Total packets received | 0 - 4,294,967,295 |
| 6 | 4 | uint32_t | sent | Total packets sent | 0 - 4,294,967,295 |
| 10 | 4 | uint32_t | flood_tx | Packets sent via flood routing | 0 - 4,294,967,295 |
| 14 | 4 | uint32_t | direct_tx | Packets sent via direct routing | 0 - 4,294,967,295 |
| 18 | 4 | uint32_t | flood_rx | Packets received via flood routing | 0 - 4,294,967,295 |
| 22 | 4 | uint32_t | direct_rx | Packets received via direct routing | 0 - 4,294,967,295 |
### Notes
- Counters are cumulative from boot and may wrap.
- `recv = flood_rx + direct_rx`
- `sent = flood_tx + direct_tx`
### Example Structure (C/C++)
```c
struct StatsPackets {
uint8_t response_code; // 0x18
uint8_t stats_type; // 0x02 (STATS_TYPE_PACKETS)
uint32_t recv;
uint32_t sent;
uint32_t flood_tx;
uint32_t direct_tx;
uint32_t flood_rx;
uint32_t direct_rx;
} __attribute__((packed));
```
---
## Command Usage Example (Python)
```python
# Send CMD_GET_STATS command
def send_get_stats_core(serial_interface):
"""Send command to get core stats"""
cmd = bytes([56, 0]) # CMD_GET_STATS (56) + STATS_TYPE_CORE (0)
serial_interface.write(cmd)
def send_get_stats_radio(serial_interface):
"""Send command to get radio stats"""
cmd = bytes([56, 1]) # CMD_GET_STATS (56) + STATS_TYPE_RADIO (1)
serial_interface.write(cmd)
def send_get_stats_packets(serial_interface):
"""Send command to get packet stats"""
cmd = bytes([56, 2]) # CMD_GET_STATS (56) + STATS_TYPE_PACKETS (2)
serial_interface.write(cmd)
```
---
## Response Parsing Example (Python)
```python
import struct
def parse_stats_core(frame):
"""Parse RESP_CODE_STATS + STATS_TYPE_CORE frame (11 bytes)"""
response_code, stats_type, battery_mv, uptime_secs, errors, queue_len = \
struct.unpack('<B B H I H B', frame)
assert response_code == 24 and stats_type == 0, "Invalid response type"
return {
'battery_mv': battery_mv,
'uptime_secs': uptime_secs,
'errors': errors,
'queue_len': queue_len
}
def parse_stats_radio(frame):
"""Parse RESP_CODE_STATS + STATS_TYPE_RADIO frame (14 bytes)"""
response_code, stats_type, noise_floor, last_rssi, last_snr, tx_air_secs, rx_air_secs = \
struct.unpack('<B B h b b I I', frame)
assert response_code == 24 and stats_type == 1, "Invalid response type"
return {
'noise_floor': noise_floor,
'last_rssi': last_rssi,
'last_snr': last_snr / 4.0, # Unscale SNR
'tx_air_secs': tx_air_secs,
'rx_air_secs': rx_air_secs
}
def parse_stats_packets(frame):
"""Parse RESP_CODE_STATS + STATS_TYPE_PACKETS frame (26 bytes)"""
response_code, stats_type, recv, sent, flood_tx, direct_tx, flood_rx, direct_rx = \
struct.unpack('<B B I I I I I I', frame)
assert response_code == 24 and stats_type == 2, "Invalid response type"
return {
'recv': recv,
'sent': sent,
'flood_tx': flood_tx,
'direct_tx': direct_tx,
'flood_rx': flood_rx,
'direct_rx': direct_rx
}
```
---
## Command Usage Example (JavaScript/TypeScript)
```typescript
// Send CMD_GET_STATS command
const CMD_GET_STATS = 56;
const STATS_TYPE_CORE = 0;
const STATS_TYPE_RADIO = 1;
const STATS_TYPE_PACKETS = 2;
function sendGetStatsCore(serialInterface: SerialPort): void {
const cmd = new Uint8Array([CMD_GET_STATS, STATS_TYPE_CORE]);
serialInterface.write(cmd);
}
function sendGetStatsRadio(serialInterface: SerialPort): void {
const cmd = new Uint8Array([CMD_GET_STATS, STATS_TYPE_RADIO]);
serialInterface.write(cmd);
}
function sendGetStatsPackets(serialInterface: SerialPort): void {
const cmd = new Uint8Array([CMD_GET_STATS, STATS_TYPE_PACKETS]);
serialInterface.write(cmd);
}
```
---
## Response Parsing Example (JavaScript/TypeScript)
```typescript
interface StatsCore {
battery_mv: number;
uptime_secs: number;
errors: number;
queue_len: number;
}
interface StatsRadio {
noise_floor: number;
last_rssi: number;
last_snr: number;
tx_air_secs: number;
rx_air_secs: number;
}
interface StatsPackets {
recv: number;
sent: number;
flood_tx: number;
direct_tx: number;
flood_rx: number;
direct_rx: number;
}
function parseStatsCore(buffer: ArrayBuffer): StatsCore {
const view = new DataView(buffer);
const response_code = view.getUint8(0);
const stats_type = view.getUint8(1);
if (response_code !== 24 || stats_type !== 0) {
throw new Error('Invalid response type');
}
return {
battery_mv: view.getUint16(2, true),
uptime_secs: view.getUint32(4, true),
errors: view.getUint16(8, true),
queue_len: view.getUint8(10)
};
}
function parseStatsRadio(buffer: ArrayBuffer): StatsRadio {
const view = new DataView(buffer);
const response_code = view.getUint8(0);
const stats_type = view.getUint8(1);
if (response_code !== 24 || stats_type !== 1) {
throw new Error('Invalid response type');
}
return {
noise_floor: view.getInt16(2, true),
last_rssi: view.getInt8(4),
last_snr: view.getInt8(5) / 4.0, // Unscale SNR
tx_air_secs: view.getUint32(6, true),
rx_air_secs: view.getUint32(10, true)
};
}
function parseStatsPackets(buffer: ArrayBuffer): StatsPackets {
const view = new DataView(buffer);
const response_code = view.getUint8(0);
const stats_type = view.getUint8(1);
if (response_code !== 24 || stats_type !== 2) {
throw new Error('Invalid response type');
}
return {
recv: view.getUint32(2, true),
sent: view.getUint32(6, true),
flood_tx: view.getUint32(10, true),
direct_tx: view.getUint32(14, true),
flood_rx: view.getUint32(18, true),
direct_rx: view.getUint32(22, true)
};
}
```
---
## Field Size Considerations
- Packet counters (uint32_t): May wrap after extended high-traffic operation.
- Time fields (uint32_t): Max ~136 years.
- SNR (int8_t, scaled by 4): Range -32 to +31.75 dB, 0.25 dB precision.

View File

@@ -52,6 +52,12 @@
#define CMD_SEND_PATH_DISCOVERY_REQ 52
#define CMD_SET_FLOOD_SCOPE 54 // v8+
#define CMD_SEND_CONTROL_DATA 55 // v8+
#define CMD_GET_STATS 56 // v8+, second byte is stats type
// Stats sub-types for CMD_GET_STATS
#define STATS_TYPE_CORE 0
#define STATS_TYPE_RADIO 1
#define STATS_TYPE_PACKETS 2
#define RESP_CODE_OK 0
#define RESP_CODE_ERR 1
@@ -77,6 +83,7 @@
#define RESP_CODE_CUSTOM_VARS 21
#define RESP_CODE_ADVERT_PATH 22
#define RESP_CODE_TUNING_PARAMS 23
#define RESP_CODE_STATS 24 // v8+, second byte is stats type
#define SEND_TIMEOUT_BASE_MILLIS 500
#define FLOOD_SEND_TIMEOUT_FACTOR 16.0f
@@ -1541,6 +1548,55 @@ void MyMesh::handleCmdFrame(size_t len) {
} else {
writeErrFrame(ERR_CODE_NOT_FOUND);
}
} else if (cmd_frame[0] == CMD_GET_STATS && len >= 2) {
uint8_t stats_type = cmd_frame[1];
if (stats_type == STATS_TYPE_CORE) {
int i = 0;
out_frame[i++] = RESP_CODE_STATS;
out_frame[i++] = STATS_TYPE_CORE;
uint16_t battery_mv = board.getBattMilliVolts();
uint32_t uptime_secs = _ms->getMillis() / 1000;
uint8_t queue_len = (uint8_t)_mgr->getOutboundCount(0xFFFFFFFF);
memcpy(&out_frame[i], &battery_mv, 2); i += 2;
memcpy(&out_frame[i], &uptime_secs, 4); i += 4;
memcpy(&out_frame[i], &_err_flags, 2); i += 2;
out_frame[i++] = queue_len;
_serial->writeFrame(out_frame, i);
} else if (stats_type == STATS_TYPE_RADIO) {
int i = 0;
out_frame[i++] = RESP_CODE_STATS;
out_frame[i++] = STATS_TYPE_RADIO;
int16_t noise_floor = (int16_t)_radio->getNoiseFloor();
int8_t last_rssi = (int8_t)radio_driver.getLastRSSI();
int8_t last_snr = (int8_t)(radio_driver.getLastSNR() * 4); // scaled by 4 for 0.25 dB precision
uint32_t tx_air_secs = getTotalAirTime() / 1000;
uint32_t rx_air_secs = getReceiveAirTime() / 1000;
memcpy(&out_frame[i], &noise_floor, 2); i += 2;
out_frame[i++] = last_rssi;
out_frame[i++] = last_snr;
memcpy(&out_frame[i], &tx_air_secs, 4); i += 4;
memcpy(&out_frame[i], &rx_air_secs, 4); i += 4;
_serial->writeFrame(out_frame, i);
} else if (stats_type == STATS_TYPE_PACKETS) {
int i = 0;
out_frame[i++] = RESP_CODE_STATS;
out_frame[i++] = STATS_TYPE_PACKETS;
uint32_t recv = radio_driver.getPacketsRecv();
uint32_t sent = radio_driver.getPacketsSent();
uint32_t n_sent_flood = getNumSentFlood();
uint32_t n_sent_direct = getNumSentDirect();
uint32_t n_recv_flood = getNumRecvFlood();
uint32_t n_recv_direct = getNumRecvDirect();
memcpy(&out_frame[i], &recv, 4); i += 4;
memcpy(&out_frame[i], &sent, 4); i += 4;
memcpy(&out_frame[i], &n_sent_flood, 4); i += 4;
memcpy(&out_frame[i], &n_sent_direct, 4); i += 4;
memcpy(&out_frame[i], &n_recv_flood, 4); i += 4;
memcpy(&out_frame[i], &n_recv_direct, 4); i += 4;
_serial->writeFrame(out_frame, i);
} else {
writeErrFrame(ERR_CODE_ILLEGAL_ARG); // invalid stats sub-type
}
} else if (cmd_frame[0] == CMD_FACTORY_RESET && memcmp(&cmd_frame[1], "reset", 5) == 0) {
bool success = _store->formatFileSystem();
if (success) {

View File

@@ -82,7 +82,7 @@ void MyMesh::putNeighbour(const mesh::Identity &id, uint32_t timestamp, float sn
#endif
}
uint8_t MyMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data) {
uint8_t MyMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood) {
ClientInfo* client = NULL;
if (data[0] == 0) { // blank password, just check if sender is in ACL
client = acl.getClient(sender.pub_key, PUB_KEY_SIZE);
@@ -123,6 +123,10 @@ uint8_t MyMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* secr
}
}
if (is_flood) {
client->out_path_len = -1; // need to rediscover out_path
}
uint32_t now = getRTCClock()->getCurrentTimeUnique();
memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp
reply_data[4] = RESP_SERVER_LOGIN_OK;
@@ -438,7 +442,7 @@ void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const m
data[len] = 0; // ensure null terminator
uint8_t reply_len;
if (data[4] == 0 || data[4] >= ' ') { // is password, ie. a login request
reply_len = handleLoginReq(sender, secret, timestamp, &data[4]);
reply_len = handleLoginReq(sender, secret, timestamp, &data[4], packet->isRouteFlood());
//} else if (data[4] == ANON_REQ_TYPE_*) { // future type codes
// TODO
} else {
@@ -710,6 +714,8 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
_prefs.gps_enabled = 0;
_prefs.gps_interval = 0;
_prefs.advert_loc_policy = ADVERT_LOC_PREFS;
_prefs.adc_multiplier = 0.0f; // 0.0f means use default board multiplier
}
void MyMesh::begin(FILESYSTEM *fs) {
@@ -733,6 +739,8 @@ void MyMesh::begin(FILESYSTEM *fs) {
updateAdvertTimer();
updateFloodAdvertTimer();
board.setAdcMultiplier(_prefs.adc_multiplier);
#if ENV_INCLUDE_GPS == 1
applyGpsPrefs();
#endif

View File

@@ -113,7 +113,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
#endif
void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr);
uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data);
uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood);
int handleRequest(ClientInfo* sender, uint32_t sender_timestamp, uint8_t* payload, size_t payload_len);
mesh::Packet* createSelfAdvert();

View File

@@ -332,6 +332,10 @@ void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const m
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
}
if (packet->isRouteFlood()) {
client->out_path_len = -1; // need to rediscover out_path
}
uint32_t now = getRTCClock()->getCurrentTimeUnique();
memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp
// TODO: maybe reply with count of messages waiting to be synced for THIS client?
@@ -641,6 +645,8 @@ void MyMesh::begin(FILESYSTEM *fs) {
updateAdvertTimer();
updateFloodAdvertTimer();
board.setAdcMultiplier(_prefs.adc_multiplier);
#if ENV_INCLUDE_GPS == 1
applyGpsPrefs();
#endif

View File

@@ -326,7 +326,7 @@ int SensorMesh::getAGCResetInterval() const {
return ((int)_prefs.agc_reset_interval) * 4000; // milliseconds
}
uint8_t SensorMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data) {
uint8_t SensorMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood) {
ClientInfo* client;
if (data[0] == 0) { // blank password, just check if sender is in ACL
client = acl.getClient(sender.pub_key, PUB_KEY_SIZE);
@@ -359,6 +359,10 @@ uint8_t SensorMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t*
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
}
if (is_flood) {
client->out_path_len = -1; // need to rediscover out_path
}
uint32_t now = getRTCClock()->getCurrentTimeUnique();
memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp
reply_data[4] = RESP_SERVER_LOGIN_OK;
@@ -451,7 +455,7 @@ void SensorMesh::onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, con
data[len] = 0; // ensure null terminator
uint8_t reply_len;
if (data[4] == 0 || data[4] >= ' ') { // is password, ie. a login request
reply_len = handleLoginReq(sender, secret, timestamp, &data[4]);
reply_len = handleLoginReq(sender, secret, timestamp, &data[4], packet->isRouteFlood());
//} else if (data[4] == ANON_REQ_TYPE_*) { // future type codes
// TODO
} else {
@@ -740,6 +744,8 @@ void SensorMesh::begin(FILESYSTEM* fs) {
updateAdvertTimer();
updateFloodAdvertTimer();
board.setAdcMultiplier(_prefs.adc_multiplier);
#if ENV_INCLUDE_GPS == 1
applyGpsPrefs();
#endif

View File

@@ -148,7 +148,7 @@ private:
uint8_t pending_sf;
uint8_t pending_cr;
uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data);
uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood);
uint8_t handleRequest(uint8_t perms, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len);
mesh::Packet* createSelfAdvert();

View File

@@ -42,6 +42,8 @@ namespace mesh {
class MainBoard {
public:
virtual uint16_t getBattMilliVolts() = 0;
virtual bool setAdcMultiplier(float multiplier) { return false; };
virtual float getAdcMultiplier() const { return 0.0f; }
virtual const char* getManufacturerName() const = 0;
virtual void onBeforeTransmit() { }
virtual void onAfterTransmit() { }

View File

@@ -70,7 +70,8 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
file.read((uint8_t *)&_prefs->gps_interval, sizeof(_prefs->gps_interval)); // 157
file.read((uint8_t *)&_prefs->advert_loc_policy, sizeof (_prefs->advert_loc_policy)); // 161
file.read((uint8_t *)&_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp)); // 162
// 166
file.read((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166
// 170
// sanitise bad pref values
_prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f);
@@ -83,6 +84,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
_prefs->cr = constrain(_prefs->cr, 5, 8);
_prefs->tx_power_dbm = constrain(_prefs->tx_power_dbm, 1, 30);
_prefs->multi_acks = constrain(_prefs->multi_acks, 0, 1);
_prefs->adc_multiplier = constrain(_prefs->adc_multiplier, 0.0f, 10.0f);
// sanitise bad bridge pref values
_prefs->bridge_enabled = constrain(_prefs->bridge_enabled, 0, 1);
@@ -148,7 +150,8 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) {
file.write((uint8_t *)&_prefs->gps_interval, sizeof(_prefs->gps_interval)); // 157
file.write((uint8_t *)&_prefs->advert_loc_policy, sizeof(_prefs->advert_loc_policy)); // 161
file.write((uint8_t *)&_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp)); // 162
// 166
file.write((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166
// 170
file.close();
}
@@ -331,6 +334,13 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
} else if (memcmp(config, "bridge.secret", 13) == 0) {
sprintf(reply, "> %s", _prefs->bridge_secret);
#endif
} else if (memcmp(config, "adc.multiplier", 14) == 0) {
float adc_mult = _board->getAdcMultiplier();
if (adc_mult == 0.0f) {
strcpy(reply, "Error: unsupported by this board");
} else {
sprintf(reply, "> %.3f", adc_mult);
}
} else {
sprintf(reply, "??: %s", config);
}
@@ -523,6 +533,19 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
savePrefs();
strcpy(reply, "OK");
#endif
} else if (memcmp(config, "adc.multiplier ", 15) == 0) {
_prefs->adc_multiplier = atof(&config[15]);
if (_board->setAdcMultiplier(_prefs->adc_multiplier)) {
savePrefs();
if (_prefs->adc_multiplier == 0.0f) {
strcpy(reply, "OK - using default board multiplier");
} else {
sprintf(reply, "OK - multiplier set to %.3f", _prefs->adc_multiplier);
}
} else {
_prefs->adc_multiplier = 0.0f;
strcpy(reply, "Error: unsupported by this board");
};
} else {
sprintf(reply, "unknown config: %s", config);
}

View File

@@ -47,6 +47,7 @@ struct NodePrefs { // persisted to file
uint32_t gps_interval; // in seconds
uint8_t advert_loc_policy;
uint32_t discovery_mod_timestamp;
float adc_multiplier;
};
class CommonCLICallbacks {

View File

@@ -23,6 +23,7 @@ class PromicroBoard : public mesh::MainBoard {
protected:
uint8_t startup_reason;
uint8_t btn_prev_state;
float adc_mult = ADC_MULTIPLIER;
public:
void begin();
@@ -39,7 +40,23 @@ public:
raw += analogRead(PIN_VBAT_READ);
}
raw = raw / BATTERY_SAMPLES;
return (ADC_MULTIPLIER * raw);
return (adc_mult * raw);
}
bool setAdcMultiplier(float multiplier) override {
if (multiplier == 0.0f) {
adc_mult = ADC_MULTIPLIER;}
else {
adc_mult = multiplier;
}
return true;
}
float getAdcMultiplier() const override {
if (adc_mult == 0.0f) {
return ADC_MULTIPLIER;
} else {
return adc_mult;
}
}
const char* getManufacturerName() const override {

View File

@@ -1,9 +1,9 @@
[Faketec]
[Promicro]
extends = nrf52_base
board = promicro_nrf52840
build_flags = ${nrf52_base.build_flags}
-I variants/promicro
-D FAKETEC
-D PROMICRO
-D RADIO_CLASS=CustomSX1262
-D WRAPPER_CLASS=CustomSX1262Wrapper
-D LORA_TX_POWER=22
@@ -34,15 +34,15 @@ lib_deps= ${nrf52_base.lib_deps}
adafruit/Adafruit BMP280 Library@^2.6.8
stevemarple/MicroNMEA @ ^2.0.6
[env:Faketec_repeater]
extends = Faketec
build_src_filter = ${Faketec.build_src_filter}
[env:ProMicro_repeater]
extends = Promicro
build_src_filter = ${Promicro.build_src_filter}
+<../examples/simple_repeater>
+<helpers/ui/SSD1306Display.cpp>
+<helpers/ui/MomentaryButton.cpp>
build_flags =
${Faketec.build_flags}
-D ADVERT_NAME='"Faketec Repeater"'
${Promicro.build_flags}
-D ADVERT_NAME='"ProMicro Repeater"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
@@ -50,17 +50,17 @@ build_flags =
-D DISPLAY_CLASS=SSD1306Display
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
lib_deps = ${Faketec.lib_deps}
lib_deps = ${Promicro.lib_deps}
adafruit/RTClib @ ^2.1.3
[env:Faketec_room_server]
extends = Faketec
build_src_filter = ${Faketec.build_src_filter}
[env:ProMicro_room_server]
extends = Promicro
build_src_filter = ${Promicro.build_src_filter}
+<../examples/simple_room_server>
+<helpers/ui/SSD1306Display.cpp>
+<helpers/ui/MomentaryButton.cpp>
build_flags = ${Faketec.build_flags}
-D ADVERT_NAME='"Faketec Room"'
build_flags = ${Promicro.build_flags}
-D ADVERT_NAME='"ProMicro Room"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
@@ -68,47 +68,47 @@ build_flags = ${Faketec.build_flags}
-D DISPLAY_CLASS=SSD1306Display
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
lib_deps = ${Faketec.lib_deps}
lib_deps = ${Promicro.lib_deps}
adafruit/RTClib @ ^2.1.3
[env:Faketec_terminal_chat]
extends = Faketec
build_flags = ${Faketec.build_flags}
[env:ProMicro_terminal_chat]
extends = Promicro
build_flags = ${Promicro.build_flags}
-D MAX_CONTACTS=100
-D MAX_GROUP_CHANNELS=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${Faketec.build_src_filter}
build_src_filter = ${Promicro.build_src_filter}
+<../examples/simple_secure_chat/main.cpp>
lib_deps = ${Faketec.lib_deps}
lib_deps = ${Promicro.lib_deps}
densaugeo/base64 @ ~1.4.0
adafruit/RTClib @ ^2.1.3
[env:Faketec_companion_radio_usb]
extends = Faketec
[env:ProMicro_companion_radio_usb]
extends = Promicro
board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
board_upload.maximum_size = 712704
build_flags = ${Faketec.build_flags}
build_flags = ${Promicro.build_flags}
-I examples/companion_radio/ui-new
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
-D DISPLAY_CLASS=SSD1306Display
; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1
; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1
build_src_filter = ${Faketec.build_src_filter}
build_src_filter = ${Promicro.build_src_filter}
+<helpers/ui/SSD1306Display.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps = ${Faketec.lib_deps}
lib_deps = ${Promicro.lib_deps}
adafruit/RTClib @ ^2.1.3
densaugeo/base64 @ ~1.4.0
[env:Faketec_companion_radio_ble]
extends = Faketec
[env:ProMicro_companion_radio_ble]
extends = Promicro
board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
board_upload.maximum_size = 712704
build_flags = ${Faketec.build_flags}
build_flags = ${Promicro.build_flags}
-I examples/companion_radio/ui-new
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
@@ -118,30 +118,30 @@ build_flags = ${Faketec.build_flags}
-D DISPLAY_CLASS=SSD1306Display
; -D MESH_PACKET_LOGGING=1
-D MESH_DEBUG=1
build_src_filter = ${Faketec.build_src_filter}
build_src_filter = ${Promicro.build_src_filter}
+<helpers/nrf52/SerialBLEInterface.cpp>
+<helpers/ui/SSD1306Display.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps = ${Faketec.lib_deps}
lib_deps = ${Promicro.lib_deps}
adafruit/RTClib @ ^2.1.3
densaugeo/base64 @ ~1.4.0
[env:Faketec_sensor]
extends = Faketec
[env:ProMicro_sensor]
extends = Promicro
build_flags =
${Faketec.build_flags}
-D ADVERT_NAME='"Faketec Sensor"'
${Promicro.build_flags}
-D ADVERT_NAME='"ProMicro Sensor"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D DISPLAY_CLASS=SSD1306Display
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${Faketec.build_src_filter}
build_src_filter = ${Promicro.build_src_filter}
+<helpers/ui/SSD1306Display.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<../examples/simple_sensor>
lib_deps =
${Faketec.lib_deps}
${Promicro.lib_deps}