mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-11-02 19:02:00 +00:00
Merge pull request #3402 from meshcore-dev/sensor-improvements
Sensor improvements
This commit is contained in:
+44
-8
@@ -93,6 +93,7 @@ Returned path messages provide a description of the route a packet took from the
|
|||||||
| Field | Size (bytes) | Description |
|
| Field | Size (bytes) | Description |
|
||||||
|--------------|-----------------|------------------------------------------|
|
|--------------|-----------------|------------------------------------------|
|
||||||
| timestamp | 4 | sender time (unix timestamp) |
|
| timestamp | 4 | sender time (unix timestamp) |
|
||||||
|
| req type | 1 | request sub type |
|
||||||
| request data | rest of payload | application-defined request payload body |
|
| request data | rest of payload | application-defined request payload body |
|
||||||
|
|
||||||
For the common chat/server helpers in `BaseChatMesh`, the current request type values are:
|
For the common chat/server helpers in `BaseChatMesh`, the current request type values are:
|
||||||
@@ -100,7 +101,13 @@ For the common chat/server helpers in `BaseChatMesh`, the current request type v
|
|||||||
| Value | Name | Description |
|
| Value | Name | Description |
|
||||||
|--------|-----------|----------------------------------------------------|
|
|--------|-----------|----------------------------------------------------|
|
||||||
| `0x01` | get stats | get stats of repeater or room server |
|
| `0x01` | get stats | get stats of repeater or room server |
|
||||||
| `0x02` | keepalive | keep-alive request used for maintained connections |
|
| `0x02` | keepalive | (deprecated) |
|
||||||
|
| `0x03` | get telemetry | request node telemetry |
|
||||||
|
| `0x04` | get min/max/avg | get sensor node stats on time series data |
|
||||||
|
| `0x05` | get acl | node ACL query |
|
||||||
|
| `0x06` | get neighbors | node neighbors query |
|
||||||
|
| `0x08` | subscribe | subscribe to telemetry push |
|
||||||
|
| `0x09` | ubsubscribe | unsubscribe from telemetry push |
|
||||||
|
|
||||||
#### Get stats
|
#### Get stats
|
||||||
|
|
||||||
@@ -125,21 +132,50 @@ Gets information about the node, possibly including the following:
|
|||||||
* Number posted (?)
|
* Number posted (?)
|
||||||
* Number of post pushes (?)
|
* Number of post pushes (?)
|
||||||
|
|
||||||
#### Get telemetry data
|
|
||||||
|
|
||||||
Not defined in `BaseChatMesh`. Sensor- and application-specific request payloads may be implemented by higher-level firmware.
|
|
||||||
|
|
||||||
#### Get Telemetry
|
#### Get Telemetry
|
||||||
|
|
||||||
Not defined in `BaseChatMesh`.
|
| Field | Size (bytes) | Description |
|
||||||
|
|----------------|--------------|------------------------------|
|
||||||
|
| timestamp | 4 | sender time (unix timestamp) |
|
||||||
|
| req type | 1 | 0x03 (request sub type) |
|
||||||
|
| permission mask | 1 | bitwise inverse mask to AND to permissions (0 = get ALL telem values) |
|
||||||
|
|
||||||
#### Get Min/Max/Ave (Sensor nodes)
|
#### Get Min/Max/Ave (Sensor nodes)
|
||||||
|
|
||||||
Not defined in `BaseChatMesh`.
|
| Field | Size (bytes) | Description |
|
||||||
|
|----------------|--------------|------------------------------|
|
||||||
|
| timestamp | 4 | sender time (unix timestamp) |
|
||||||
|
| req type | 1 | 0x04 (request sub type) |
|
||||||
|
| start | 4 | starting time, seconds ago |
|
||||||
|
| end | 4 | ending time, seconds ago |
|
||||||
|
| reserved | 2 | should be zeroes |
|
||||||
|
|
||||||
|
#### Subscribe to Telemetry push - (Sensor nodes)
|
||||||
|
|
||||||
|
| Field | Size (bytes) | Description |
|
||||||
|
|----------------|--------------|------------------------------|
|
||||||
|
| timestamp | 4 | sender time (unix timestamp) |
|
||||||
|
| req type | 1 | 0x08 (request sub type) |
|
||||||
|
| push tag | 4 | 32-bit tag to be used in telemetry push _REPLY payloads |
|
||||||
|
| timeout secs | 2 | subscription timeout (seconds) |
|
||||||
|
| reserved | 1 | should be zero |
|
||||||
|
| min deltas len | 1 | byte length of LPP encoded min_deltas |
|
||||||
|
| min deltas | (variable) | LPP encoded min_deltas |
|
||||||
|
|
||||||
|
#### Unsubscribe from Telemetry push - (Sensor nodes)
|
||||||
|
|
||||||
|
| Field | Size (bytes) | Description |
|
||||||
|
|----------------|--------------|------------------------------|
|
||||||
|
| timestamp | 4 | sender time (unix timestamp) |
|
||||||
|
| req type | 1 | 0x09 (request sub type) |
|
||||||
|
|
||||||
#### Get Access List
|
#### Get Access List
|
||||||
|
|
||||||
Not defined in `BaseChatMesh`.
|
| Field | Size (bytes) | Description |
|
||||||
|
|----------------|--------------|------------------------------|
|
||||||
|
| timestamp | 4 | sender time (unix timestamp) |
|
||||||
|
| req type | 1 | 0x05 (request sub type) |
|
||||||
|
| reserved | 2 | should be zeroes |
|
||||||
|
|
||||||
#### Get Neighbors
|
#### Get Neighbors
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
#include "SensorMesh.h"
|
#include "SensorMesh.h"
|
||||||
|
#include <helpers/RoutingPolicy.h>
|
||||||
|
#include <helpers/sensors/LPPDataHelpers.h>
|
||||||
|
|
||||||
/* ------------------------------ Config -------------------------------- */
|
/* ------------------------------ Config -------------------------------- */
|
||||||
|
|
||||||
@@ -54,6 +56,10 @@
|
|||||||
#define REQ_TYPE_GET_TELEMETRY_DATA 0x03
|
#define REQ_TYPE_GET_TELEMETRY_DATA 0x03
|
||||||
#define REQ_TYPE_GET_AVG_MIN_MAX 0x04
|
#define REQ_TYPE_GET_AVG_MIN_MAX 0x04
|
||||||
#define REQ_TYPE_GET_ACCESS_LIST 0x05
|
#define REQ_TYPE_GET_ACCESS_LIST 0x05
|
||||||
|
#define REQ_TYPE_GET_NEIGHBOURS 0x06 // repeater only (at present)
|
||||||
|
|
||||||
|
#define REQ_TYPE_SUBSCRIBE 0x08
|
||||||
|
#define REQ_TYPE_UNSUBSCRIBE 0x09
|
||||||
|
|
||||||
#define RESP_SERVER_LOGIN_OK 0 // response to ANON_REQ
|
#define RESP_SERVER_LOGIN_OK 0 // response to ANON_REQ
|
||||||
|
|
||||||
@@ -73,104 +79,70 @@ static File openAppend(FILESYSTEM* _fs, const char* fname) {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
static uint8_t getDataSize(uint8_t type) {
|
/* --------------------- Cayenne LPP helpers ----------------------------*/
|
||||||
switch (type) {
|
|
||||||
case LPP_GPS:
|
static float findTelemValue(const uint8_t* buf, uint8_t size, uint8_t channel, uint8_t type, float def_value) {
|
||||||
return 9;
|
uint8_t i = 0;
|
||||||
case LPP_POLYLINE:
|
|
||||||
return 8; // TODO: this is MINIMIUM
|
while (i + 2 < size) {
|
||||||
case LPP_GYROMETER:
|
// Get channel #
|
||||||
case LPP_ACCELEROMETER:
|
uint8_t ch = buf[i++];
|
||||||
return 6;
|
// Get data type
|
||||||
case LPP_GENERIC_SENSOR:
|
uint8_t t = buf[i++];
|
||||||
case LPP_FREQUENCY:
|
uint8_t sz = LPPData::getDataSize(t);
|
||||||
case LPP_DISTANCE:
|
|
||||||
case LPP_ENERGY:
|
if (ch == channel && t == type) {
|
||||||
case LPP_UNIXTIME:
|
return LPPData::getFloat(&buf[i], sz, LPPData::getMultiplier(t), LPPData::isSigned(t));
|
||||||
return 4;
|
|
||||||
case LPP_COLOUR:
|
|
||||||
return 3;
|
|
||||||
case LPP_ANALOG_INPUT:
|
|
||||||
case LPP_ANALOG_OUTPUT:
|
|
||||||
case LPP_LUMINOSITY:
|
|
||||||
case LPP_TEMPERATURE:
|
|
||||||
case LPP_CONCENTRATION:
|
|
||||||
case LPP_BAROMETRIC_PRESSURE:
|
|
||||||
case LPP_RELATIVE_HUMIDITY:
|
|
||||||
case LPP_ALTITUDE:
|
|
||||||
case LPP_VOLTAGE:
|
|
||||||
case LPP_CURRENT:
|
|
||||||
case LPP_DIRECTION:
|
|
||||||
case LPP_POWER:
|
|
||||||
return 2;
|
|
||||||
}
|
}
|
||||||
return 1;
|
i += sz; // skip
|
||||||
}
|
|
||||||
|
|
||||||
static uint32_t getMultiplier(uint8_t type) {
|
|
||||||
switch (type) {
|
|
||||||
case LPP_CURRENT:
|
|
||||||
case LPP_DISTANCE:
|
|
||||||
case LPP_ENERGY:
|
|
||||||
return 1000;
|
|
||||||
case LPP_VOLTAGE:
|
|
||||||
case LPP_ANALOG_INPUT:
|
|
||||||
case LPP_ANALOG_OUTPUT:
|
|
||||||
return 100;
|
|
||||||
case LPP_TEMPERATURE:
|
|
||||||
case LPP_BAROMETRIC_PRESSURE:
|
|
||||||
case LPP_RELATIVE_HUMIDITY:
|
|
||||||
return 10;
|
|
||||||
}
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool isSigned(uint8_t type) {
|
|
||||||
return type == LPP_ALTITUDE || type == LPP_TEMPERATURE || type == LPP_GYROMETER ||
|
|
||||||
type == LPP_ANALOG_INPUT || type == LPP_ANALOG_OUTPUT || type == LPP_GPS || type == LPP_ACCELEROMETER;
|
|
||||||
}
|
|
||||||
|
|
||||||
static float getFloat(const uint8_t * buffer, uint8_t size, uint32_t multiplier, bool is_signed) {
|
|
||||||
uint32_t value = 0;
|
|
||||||
for (uint8_t i = 0; i < size; i++) {
|
|
||||||
value = (value << 8) + buffer[i];
|
|
||||||
}
|
}
|
||||||
|
return def_value; // not found
|
||||||
|
}
|
||||||
|
|
||||||
int sign = 1;
|
/* ------------------ end Cayenne LPP helpers ----------------------*/
|
||||||
if (is_signed) {
|
|
||||||
uint32_t bit = 1ul << ((size * 8) - 1);
|
bool SensorMesh::telemHasChanged(ClientInfo* c) {
|
||||||
if ((value & bit) == bit) {
|
auto buf = telemetry.getBuffer();
|
||||||
value = (bit << 1) - value;
|
uint8_t size = telemetry.getSize();
|
||||||
sign = -1;
|
uint8_t i = 0;
|
||||||
|
bool changed = false;
|
||||||
|
|
||||||
|
while (i + 2 < c->extra.sensor.min_deltas_len) {
|
||||||
|
uint8_t ch = c->extra.sensor.min_deltas[i]; // Get channel #
|
||||||
|
uint8_t t = c->extra.sensor.min_deltas[i + 1]; // Get data type
|
||||||
|
uint8_t sz = LPPData::getDataSize(t);
|
||||||
|
|
||||||
|
float min_delta = LPPData::getFloat(&c->extra.sensor.min_deltas[i + 2], sz, LPPData::getMultiplier(t), LPPData::isSigned(t));
|
||||||
|
float pv = LPPData::getFloat(&c->extra.sensor.prev_telem[i + 2], sz, LPPData::getMultiplier(t), LPPData::isSigned(t));
|
||||||
|
|
||||||
|
float v = findTelemValue(buf, size, ch, t, 0.0f);
|
||||||
|
if (abs(v - pv) > min_delta) changed = true; // Yes, has changed
|
||||||
|
|
||||||
|
i += 2 + sz; // skip
|
||||||
|
}
|
||||||
|
if (changed) {
|
||||||
|
// take snapshot of all _monitored_ telem values, for next cycle
|
||||||
|
i = 0;
|
||||||
|
while (i + 2 < c->extra.sensor.min_deltas_len) {
|
||||||
|
uint8_t ch = c->extra.sensor.min_deltas[i]; // Get channel #
|
||||||
|
uint8_t t = c->extra.sensor.min_deltas[i + 1]; // Get data type
|
||||||
|
uint8_t sz = LPPData::getDataSize(t);
|
||||||
|
|
||||||
|
c->extra.sensor.prev_telem[i] = ch;
|
||||||
|
c->extra.sensor.prev_telem[i + 1] = t;
|
||||||
|
|
||||||
|
float v = findTelemValue(buf, size, ch, t, 0.0f);
|
||||||
|
LPPData::putFloat(&c->extra.sensor.prev_telem[i + 2], v, sz, LPPData::getMultiplier(t), LPPData::isSigned(t));
|
||||||
|
|
||||||
|
i += 2 + sz; // skip
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return sign * ((float) value / multiplier);
|
return changed;
|
||||||
}
|
}
|
||||||
|
|
||||||
static uint8_t putFloat(uint8_t * dest, float value, uint8_t size, uint32_t multiplier, bool is_signed) {
|
uint8_t SensorMesh::handleRequest(ClientInfo* from, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len) {
|
||||||
// check sign
|
uint8_t perms = from->isAdmin() ? 0xFF : from->permissions;
|
||||||
bool sign = value < 0;
|
|
||||||
if (sign) value = -value;
|
|
||||||
|
|
||||||
// get value to store
|
|
||||||
uint32_t v = value * multiplier;
|
|
||||||
|
|
||||||
// format an uint32_t as if it was an int32_t
|
|
||||||
if (is_signed & sign) {
|
|
||||||
uint32_t mask = (1 << (size * 8)) - 1;
|
|
||||||
v = v & mask;
|
|
||||||
if (sign) v = mask - v + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// add bytes (MSB first)
|
|
||||||
for (uint8_t i=1; i<=size; i++) {
|
|
||||||
dest[size - i] = (v & 0xFF);
|
|
||||||
v >>= 8;
|
|
||||||
}
|
|
||||||
return size;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint8_t SensorMesh::handleRequest(uint8_t perms, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len) {
|
|
||||||
memcpy(reply_data, &sender_timestamp, 4); // reflect sender_timestamp back in response packet (kind of like a 'tag')
|
memcpy(reply_data, &sender_timestamp, 4); // reflect sender_timestamp back in response packet (kind of like a 'tag')
|
||||||
|
|
||||||
if (req_type == REQ_TYPE_GET_TELEMETRY_DATA) { // allow all
|
if (req_type == REQ_TYPE_GET_TELEMETRY_DATA) { // allow all
|
||||||
@@ -181,6 +153,10 @@ uint8_t SensorMesh::handleRequest(uint8_t perms, uint32_t sender_timestamp, uint
|
|||||||
// query other sensors -- target specific
|
// query other sensors -- target specific
|
||||||
sensors.querySensors(0xFF & perm_mask, telemetry); // allow all telemetry permissions for admin or guest
|
sensors.querySensors(0xFF & perm_mask, telemetry); // allow all telemetry permissions for admin or guest
|
||||||
// TODO: let requester know permissions they have: telemetry.addPresence(TELEM_CHANNEL_SELF, perms);
|
// TODO: let requester know permissions they have: telemetry.addPresence(TELEM_CHANNEL_SELF, perms);
|
||||||
|
float temperature = board.getMCUTemperature();
|
||||||
|
if (!isnan(temperature)) { // Supported boards with built-in temperature sensor. ESP32-C3 may return NAN
|
||||||
|
telemetry.addTemperature(TELEM_CHANNEL_SELF, temperature); // Built-in MCU Temperature
|
||||||
|
}
|
||||||
|
|
||||||
uint8_t tlen = telemetry.getSize();
|
uint8_t tlen = telemetry.getSize();
|
||||||
memcpy(&reply_data[4], telemetry.getBuffer(), tlen);
|
memcpy(&reply_data[4], telemetry.getBuffer(), tlen);
|
||||||
@@ -211,12 +187,12 @@ uint8_t SensorMesh::handleRequest(uint8_t perms, uint32_t sender_timestamp, uint
|
|||||||
auto d = &data[i];
|
auto d = &data[i];
|
||||||
reply_data[ofs++] = d->_channel;
|
reply_data[ofs++] = d->_channel;
|
||||||
reply_data[ofs++] = d->_lpp_type;
|
reply_data[ofs++] = d->_lpp_type;
|
||||||
uint8_t sz = getDataSize(d->_lpp_type);
|
uint8_t sz = LPPData::getDataSize(d->_lpp_type);
|
||||||
uint32_t mult = getMultiplier(d->_lpp_type);
|
uint32_t mult = LPPData::getMultiplier(d->_lpp_type);
|
||||||
bool is_signed = isSigned(d->_lpp_type);
|
bool is_signed = LPPData::isSigned(d->_lpp_type);
|
||||||
ofs += putFloat(&reply_data[ofs], d->_min, sz, mult, is_signed);
|
ofs += LPPData::putFloat(&reply_data[ofs], d->_min, sz, mult, is_signed);
|
||||||
ofs += putFloat(&reply_data[ofs], d->_max, sz, mult, is_signed);
|
ofs += LPPData::putFloat(&reply_data[ofs], d->_max, sz, mult, is_signed);
|
||||||
ofs += putFloat(&reply_data[ofs], d->_avg, sz, mult, is_signed);
|
ofs += LPPData::putFloat(&reply_data[ofs], d->_avg, sz, mult, is_signed);
|
||||||
}
|
}
|
||||||
return ofs;
|
return ofs;
|
||||||
}
|
}
|
||||||
@@ -234,6 +210,45 @@ uint8_t SensorMesh::handleRequest(uint8_t perms, uint32_t sender_timestamp, uint
|
|||||||
return ofs;
|
return ofs;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (req_type == REQ_TYPE_SUBSCRIBE && payload_len >= 8 && (perms & PERM_ACL_ROLE_MASK) >= PERM_ACL_READ_ONLY) {
|
||||||
|
memcpy(&from->extra.sensor.push_tag, &payload[0], 4);
|
||||||
|
uint16_t timeout_secs;
|
||||||
|
memcpy(&timeout_secs, &payload[4], 2);
|
||||||
|
uint8_t reserved = payload[6];
|
||||||
|
uint8_t min_deltas_len = payload[7];
|
||||||
|
RegionEntry* r;
|
||||||
|
if (recv_pkt_region && !recv_pkt_region->isWildcard()) { // use request scope
|
||||||
|
r = recv_pkt_region;
|
||||||
|
} else { // use default scope
|
||||||
|
r = region_map.getDefaultRegion();
|
||||||
|
}
|
||||||
|
uint8_t reply_len;
|
||||||
|
if (r && min_deltas_len >= 3 && min_deltas_len <= sizeof(from->extra.sensor.min_deltas)) {
|
||||||
|
from->extra.sensor.scope_region_id = r->id;
|
||||||
|
from->extra.sensor.expiry_timestamp = getRTCClock()->getCurrentTime() + timeout_secs;
|
||||||
|
from->extra.sensor.min_deltas_len = min_deltas_len;
|
||||||
|
memcpy(from->extra.sensor.min_deltas, &payload[8], min_deltas_len);
|
||||||
|
// reply with actual expiry seconds (we could modify/impose restriction)
|
||||||
|
memcpy(&reply_data[4], &timeout_secs, 2);
|
||||||
|
memset(&reply_data[6], 0, 2); // reserved
|
||||||
|
strcpy((char *)&reply_data[8], r ? r->name : ""); // reply with name of scope that will be used
|
||||||
|
reply_len = 8 + strlen((char *)&reply_data[8]);
|
||||||
|
} else {
|
||||||
|
memset(&reply_data[4], 0, 4); // expiry secs (0 for error)
|
||||||
|
reply_len = 8;
|
||||||
|
}
|
||||||
|
return reply_len;
|
||||||
|
}
|
||||||
|
if (req_type == REQ_TYPE_UNSUBSCRIBE && (perms & PERM_ACL_ROLE_MASK) >= PERM_ACL_READ_ONLY) {
|
||||||
|
from->extra.sensor.scope_region_id = 0;
|
||||||
|
from->extra.sensor.push_tag = 0;
|
||||||
|
from->extra.sensor.expiry_timestamp = 0;
|
||||||
|
from->extra.sensor.min_deltas_len = 0;
|
||||||
|
// REVISIT: maybe return some stats, eg total number of telemetry pushes since SUBSCRIBE?
|
||||||
|
memset(&reply_data[4], 0, 8); // success
|
||||||
|
getRNG()->random(&reply_data[12], 2); // just some entropy for better packet-hash uniqueness
|
||||||
|
return 12 + 2;
|
||||||
|
}
|
||||||
return 0; // unknown command
|
return 0; // unknown command
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,7 +277,7 @@ void SensorMesh::sendAlert(const ClientInfo* c, Trigger* t) {
|
|||||||
sendDirect(pkt, c->out_path, c->out_path_len);
|
sendDirect(pkt, c->out_path, c->out_path_len);
|
||||||
} else {
|
} else {
|
||||||
unsigned long delay_millis = 0;
|
unsigned long delay_millis = 0;
|
||||||
sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1);
|
sendFloodScoped(default_scope, pkt, delay_millis, _prefs.path_hash_mode + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
t->send_expiry = futureMillis(ALERT_ACK_EXPIRY_MILLIS);
|
t->send_expiry = futureMillis(ALERT_ACK_EXPIRY_MILLIS);
|
||||||
@@ -330,6 +345,25 @@ int SensorMesh::getAGCResetInterval() const {
|
|||||||
return ((int)_prefs.agc_reset_interval) * 4000; // milliseconds
|
return ((int)_prefs.agc_reset_interval) * 4000; // milliseconds
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SensorMesh::startRegionsLoad() {
|
||||||
|
temp_map.resetFrom(region_map); // rebuild regions in a temp instance
|
||||||
|
memset(load_stack, 0, sizeof(load_stack));
|
||||||
|
load_stack[0] = &temp_map.getWildcard();
|
||||||
|
region_load_active = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SensorMesh::saveRegions() {
|
||||||
|
return region_map.save(_fs);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SensorMesh::onDefaultRegionChanged(const RegionEntry* r) {
|
||||||
|
if (r) {
|
||||||
|
region_map.getTransportKeysFor(*r, &default_scope, 1);
|
||||||
|
} else {
|
||||||
|
memset(default_scope.key, 0, sizeof(default_scope.key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
uint8_t SensorMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood) {
|
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;
|
ClientInfo* client;
|
||||||
if (data[0] == 0) { // blank password, just check if sender is in ACL
|
if (data[0] == 0) { // blank password, just check if sender is in ACL
|
||||||
@@ -343,7 +377,7 @@ uint8_t SensorMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t*
|
|||||||
} else {
|
} else {
|
||||||
if (strcmp((char *) data, _prefs.password) != 0) { // check for valid admin password
|
if (strcmp((char *) data, _prefs.password) != 0) { // check for valid admin password
|
||||||
#if MESH_DEBUG
|
#if MESH_DEBUG
|
||||||
MESH_DEBUG_PRINTLN("Invalid password: %s", &data[4]);
|
MESH_DEBUG_PRINTLN("Invalid password: %s", &data[0]);
|
||||||
#endif
|
#endif
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -379,7 +413,41 @@ uint8_t SensorMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t*
|
|||||||
return 13; // reply length
|
return 13; // reply length
|
||||||
}
|
}
|
||||||
|
|
||||||
void SensorMesh::handleCommand(uint32_t sender_timestamp, char* command, char* reply) {
|
void SensorMesh::handleCommand(ClientInfo* from, uint32_t sender_timestamp, char* command, char* reply) {
|
||||||
|
if (region_load_active) {
|
||||||
|
if (StrHelper::isBlank(command)) { // empty/blank line, signal to terminate 'load' operation
|
||||||
|
region_map = temp_map; // copy over the temp instance as new current map
|
||||||
|
region_load_active = false;
|
||||||
|
|
||||||
|
sprintf(reply, "OK - loaded %d regions", region_map.getCount());
|
||||||
|
} else {
|
||||||
|
char *np = command;
|
||||||
|
while (*np == ' ') np++; // skip indent
|
||||||
|
int indent = np - command;
|
||||||
|
|
||||||
|
char *ep = np;
|
||||||
|
while (RegionMap::is_name_char(*ep)) ep++;
|
||||||
|
if (*ep) { *ep++ = 0; } // set null terminator for end of name
|
||||||
|
|
||||||
|
while (*ep && *ep != 'F') ep++; // look for (optional) flags
|
||||||
|
|
||||||
|
if (indent > 0 && indent < 8 && strlen(np) > 0) {
|
||||||
|
auto parent = load_stack[indent - 1];
|
||||||
|
if (parent) {
|
||||||
|
auto old = region_map.findByName(np);
|
||||||
|
auto nw = temp_map.putRegion(np, parent->id, old ? old->id : 0); // carry-over the current ID (if name already exists)
|
||||||
|
if (nw) {
|
||||||
|
nw->flags = old ? old->flags : (*ep == 'F' ? 0 : REGION_DENY_FLOOD); // carry-over flags from curr
|
||||||
|
|
||||||
|
load_stack[indent] = nw; // keep pointers to parent regions, to resolve parent_id's
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reply[0] = 0;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
while (*command == ' ') command++; // skip leading spaces
|
while (*command == ' ') command++; // skip leading spaces
|
||||||
|
|
||||||
if (strlen(command) > 4 && command[2] == '|') { // optional prefix (for companion radio CLI)
|
if (strlen(command) > 4 && command[2] == '|') { // optional prefix (for companion radio CLI)
|
||||||
@@ -400,10 +468,11 @@ void SensorMesh::handleCommand(uint32_t sender_timestamp, char* command, char* r
|
|||||||
if (sp == NULL) {
|
if (sp == NULL) {
|
||||||
strcpy(reply, "Err - bad params");
|
strcpy(reply, "Err - bad params");
|
||||||
} else {
|
} else {
|
||||||
|
int hex_len = min(sp - hex, PUB_KEY_SIZE*2);
|
||||||
|
uint8_t pubkey[PUB_KEY_SIZE];
|
||||||
|
|
||||||
*sp++ = 0; // replace space with null terminator
|
*sp++ = 0; // replace space with null terminator
|
||||||
|
|
||||||
uint8_t pubkey[PUB_KEY_SIZE];
|
|
||||||
int hex_len = min(sp - hex, PUB_KEY_SIZE*2);
|
|
||||||
if (mesh::Utils::fromHex(pubkey, hex_len / 2, hex)) {
|
if (mesh::Utils::fromHex(pubkey, hex_len / 2, hex)) {
|
||||||
uint8_t perms = atoi(sp);
|
uint8_t perms = atoi(sp);
|
||||||
if (acl.applyPermissions(self_id, pubkey, hex_len / 2, perms)) {
|
if (acl.applyPermissions(self_id, pubkey, hex_len / 2, perms)) {
|
||||||
@@ -451,6 +520,21 @@ void SensorMesh::handleCommand(uint32_t sender_timestamp, char* command, char* r
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mesh::DispatcherAction SensorMesh::onRecvPacket(mesh::Packet* pkt) {
|
||||||
|
if (pkt->getRouteType() == ROUTE_TYPE_TRANSPORT_FLOOD) {
|
||||||
|
recv_pkt_region = region_map.findMatch(pkt, REGION_DENY_FLOOD);
|
||||||
|
} else if (pkt->getRouteType() == ROUTE_TYPE_FLOOD) {
|
||||||
|
if (region_map.getWildcard().flags & REGION_DENY_FLOOD) {
|
||||||
|
recv_pkt_region = NULL;
|
||||||
|
} else {
|
||||||
|
recv_pkt_region = ®ion_map.getWildcard();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
recv_pkt_region = NULL;
|
||||||
|
}
|
||||||
|
return Mesh::onRecvPacket(pkt);
|
||||||
|
}
|
||||||
|
|
||||||
void SensorMesh::onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) {
|
void SensorMesh::onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) {
|
||||||
if (packet->getPayloadType() == PAYLOAD_TYPE_ANON_REQ) { // received an initial request by a possible admin client (unknown at this stage)
|
if (packet->getPayloadType() == PAYLOAD_TYPE_ANON_REQ) { // received an initial request by a possible admin client (unknown at this stage)
|
||||||
uint32_t timestamp;
|
uint32_t timestamp;
|
||||||
@@ -472,10 +556,10 @@ void SensorMesh::onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, con
|
|||||||
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response
|
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response
|
||||||
mesh::Packet* path = createPathReturn(sender, secret, packet->path, packet->path_len,
|
mesh::Packet* path = createPathReturn(sender, secret, packet->path, packet->path_len,
|
||||||
PAYLOAD_TYPE_RESPONSE, reply_data, reply_len);
|
PAYLOAD_TYPE_RESPONSE, reply_data, reply_len);
|
||||||
if (path) sendFlood(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
|
if (path) sendFloodReply(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
|
||||||
} else {
|
} else {
|
||||||
mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, secret, reply_data, reply_len);
|
mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, secret, reply_data, reply_len);
|
||||||
if (reply) sendFlood(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
|
if (reply) sendFloodReply(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -503,7 +587,7 @@ void SensorMesh::getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) {
|
|||||||
void SensorMesh::sendAckTo(const ClientInfo& dest, uint32_t ack_hash, uint8_t path_hash_size) {
|
void SensorMesh::sendAckTo(const ClientInfo& dest, uint32_t ack_hash, uint8_t path_hash_size) {
|
||||||
if (dest.out_path_len == OUT_PATH_UNKNOWN) {
|
if (dest.out_path_len == OUT_PATH_UNKNOWN) {
|
||||||
mesh::Packet* ack = createAck(ack_hash);
|
mesh::Packet* ack = createAck(ack_hash);
|
||||||
if (ack) sendFlood(ack, TXT_ACK_DELAY, path_hash_size);
|
if (ack) sendFloodScoped(default_scope, ack, TXT_ACK_DELAY, path_hash_size);
|
||||||
} else {
|
} else {
|
||||||
uint32_t d = TXT_ACK_DELAY;
|
uint32_t d = TXT_ACK_DELAY;
|
||||||
if (getExtraAckTransmitCount() > 0) {
|
if (getExtraAckTransmitCount() > 0) {
|
||||||
@@ -531,7 +615,7 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i
|
|||||||
memcpy(×tamp, data, 4);
|
memcpy(×tamp, data, 4);
|
||||||
|
|
||||||
if (timestamp > from->last_timestamp) { // prevent replay attacks
|
if (timestamp > from->last_timestamp) { // prevent replay attacks
|
||||||
uint8_t reply_len = handleRequest(from->isAdmin() ? 0xFF : from->permissions, timestamp, data[4], &data[5], len - 5);
|
uint8_t reply_len = handleRequest(from, timestamp, data[4], &data[5], len - 5);
|
||||||
if (reply_len == 0) return; // invalid command
|
if (reply_len == 0) return; // invalid command
|
||||||
|
|
||||||
from->last_timestamp = timestamp;
|
from->last_timestamp = timestamp;
|
||||||
@@ -541,14 +625,14 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i
|
|||||||
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response
|
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response
|
||||||
mesh::Packet* path = createPathReturn(from->id, secret, packet->path, packet->path_len,
|
mesh::Packet* path = createPathReturn(from->id, secret, packet->path, packet->path_len,
|
||||||
PAYLOAD_TYPE_RESPONSE, reply_data, reply_len);
|
PAYLOAD_TYPE_RESPONSE, reply_data, reply_len);
|
||||||
if (path) sendFlood(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
|
if (path) sendFloodReply(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
|
||||||
} else {
|
} else {
|
||||||
mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, from->id, secret, reply_data, reply_len);
|
mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, from->id, secret, reply_data, reply_len);
|
||||||
if (reply) {
|
if (reply) {
|
||||||
if (from->out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT
|
if (from->out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT
|
||||||
sendDirect(reply, from->out_path, from->out_path_len, SERVER_RESPONSE_DELAY);
|
sendDirect(reply, from->out_path, from->out_path_len, SERVER_RESPONSE_DELAY);
|
||||||
} else {
|
} else {
|
||||||
sendFlood(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
|
sendFloodReply(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -571,7 +655,7 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i
|
|||||||
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the ACK
|
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the ACK
|
||||||
mesh::Packet* path = createPathReturn(from->id, secret, packet->path, packet->path_len,
|
mesh::Packet* path = createPathReturn(from->id, secret, packet->path, packet->path_len,
|
||||||
PAYLOAD_TYPE_ACK, (uint8_t *) &ack_hash, 4);
|
PAYLOAD_TYPE_ACK, (uint8_t *) &ack_hash, 4);
|
||||||
if (path) sendFlood(path, TXT_ACK_DELAY, packet->getPathHashSize());
|
if (path) sendFloodReply(path, TXT_ACK_DELAY, packet->getPathHashSize());
|
||||||
} else {
|
} else {
|
||||||
sendAckTo(*from, ack_hash, packet->getPathHashSize());
|
sendAckTo(*from, ack_hash, packet->getPathHashSize());
|
||||||
}
|
}
|
||||||
@@ -586,7 +670,7 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i
|
|||||||
uint8_t temp[166];
|
uint8_t temp[166];
|
||||||
char *command = (char *) &data[5];
|
char *command = (char *) &data[5];
|
||||||
char *reply = (char *) &temp[5];
|
char *reply = (char *) &temp[5];
|
||||||
handleCommand(sender_timestamp, command, reply);
|
handleCommand(from, sender_timestamp, command, reply);
|
||||||
|
|
||||||
int text_len = strlen(reply);
|
int text_len = strlen(reply);
|
||||||
if (text_len > 0) {
|
if (text_len > 0) {
|
||||||
@@ -601,7 +685,7 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i
|
|||||||
auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, from->id, secret, temp, 5 + text_len);
|
auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, from->id, secret, temp, 5 + text_len);
|
||||||
if (reply) {
|
if (reply) {
|
||||||
if (from->out_path_len == OUT_PATH_UNKNOWN) {
|
if (from->out_path_len == OUT_PATH_UNKNOWN) {
|
||||||
sendFlood(reply, CLI_REPLY_DELAY_MILLIS, packet->getPathHashSize());
|
sendFloodReply(reply, CLI_REPLY_DELAY_MILLIS, packet->getPathHashSize());
|
||||||
} else {
|
} else {
|
||||||
sendDirect(reply, from->out_path, from->out_path_len, CLI_REPLY_DELAY_MILLIS);
|
sendDirect(reply, from->out_path, from->out_path_len, CLI_REPLY_DELAY_MILLIS);
|
||||||
}
|
}
|
||||||
@@ -699,7 +783,7 @@ void SensorMesh::onAckRecv(mesh::Packet* packet, uint32_t ack_crc) {
|
|||||||
|
|
||||||
SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables)
|
SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables)
|
||||||
: mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(32), tables),
|
: mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(32), tables),
|
||||||
region_map(key_store),
|
region_map(key_store), temp_map(key_store),
|
||||||
_cli(board, rtc, sensors, region_map, acl, &_prefs, this),
|
_cli(board, rtc, sensors, region_map, acl, &_prefs, this),
|
||||||
telemetry(MAX_PACKET_PAYLOAD - 4)
|
telemetry(MAX_PACKET_PAYLOAD - 4)
|
||||||
{
|
{
|
||||||
@@ -708,6 +792,8 @@ SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::Millise
|
|||||||
last_read_time = 0;
|
last_read_time = 0;
|
||||||
num_alert_tasks = 0;
|
num_alert_tasks = 0;
|
||||||
set_radio_at = revert_radio_at = 0;
|
set_radio_at = revert_radio_at = 0;
|
||||||
|
recv_pkt_region = NULL;
|
||||||
|
region_load_active = false;
|
||||||
|
|
||||||
// defaults
|
// defaults
|
||||||
_prefs.airtime_factor = 1.0;
|
_prefs.airtime_factor = 1.0;
|
||||||
@@ -820,11 +906,43 @@ void SensorMesh::applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t
|
|||||||
revert_radio_at = futureMillis(2000 + timeout_mins*60*1000); // schedule when to revert radio params
|
revert_radio_at = futureMillis(2000 + timeout_mins*60*1000); // schedule when to revert radio params
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SensorMesh::sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint32_t delay_millis, uint8_t path_hash_size) {
|
||||||
|
if (scope.isNull()) {
|
||||||
|
sendFlood(pkt, delay_millis, path_hash_size);
|
||||||
|
} else {
|
||||||
|
uint16_t codes[2];
|
||||||
|
codes[0] = scope.calcTransportCode(pkt);
|
||||||
|
codes[1] = 0; // REVISIT: set to 'home' Region, for sender/return region?
|
||||||
|
sendFlood(pkt, codes, delay_millis, path_hash_size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SensorMesh::sendFloodReply(mesh::Packet* packet, unsigned long delay_millis, uint8_t path_hash_size) {
|
||||||
|
TransportKey req_scope;
|
||||||
|
bool is_wildcard = recv_pkt_region != NULL && recv_pkt_region->isWildcard();
|
||||||
|
bool req_scope_known = recv_pkt_region != NULL && !is_wildcard
|
||||||
|
&& region_map.getTransportKeysFor(*recv_pkt_region, &req_scope, 1) > 0;
|
||||||
|
|
||||||
|
switch (mesh::chooseReplyScope(req_scope_known, is_wildcard, !default_scope.isNull())) {
|
||||||
|
case mesh::REPLY_SCOPE_REQUEST:
|
||||||
|
sendFloodScoped(req_scope, packet, delay_millis, path_hash_size); // reply with same scope as request
|
||||||
|
break;
|
||||||
|
case mesh::REPLY_SCOPE_DEFAULT:
|
||||||
|
// requester's scope is unknown: DIRECT request (no transport codes), or code matched no Region.
|
||||||
|
// un-scoped would be dropped at hop 0 by repeaters running flood.max.unscoped=0
|
||||||
|
sendFloodScoped(default_scope, packet, delay_millis, path_hash_size);
|
||||||
|
break;
|
||||||
|
case mesh::REPLY_SCOPE_NONE:
|
||||||
|
sendFlood(packet, delay_millis, path_hash_size); // send un-scoped
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void SensorMesh::sendSelfAdvertisement(int delay_millis, bool flood) {
|
void SensorMesh::sendSelfAdvertisement(int delay_millis, bool flood) {
|
||||||
mesh::Packet* pkt = createSelfAdvert();
|
mesh::Packet* pkt = createSelfAdvert();
|
||||||
if (pkt) {
|
if (pkt) {
|
||||||
if (flood) {
|
if (flood) {
|
||||||
sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1);
|
sendFloodScoped(default_scope, pkt, delay_millis, _prefs.path_hash_mode + 1);
|
||||||
} else {
|
} else {
|
||||||
sendZeroHop(pkt, delay_millis);
|
sendZeroHop(pkt, delay_millis);
|
||||||
}
|
}
|
||||||
@@ -866,23 +984,7 @@ void SensorMesh::formatPacketStatsReply(char *reply) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
float SensorMesh::getTelemValue(uint8_t channel, uint8_t type) {
|
float SensorMesh::getTelemValue(uint8_t channel, uint8_t type) {
|
||||||
auto buf = telemetry.getBuffer();
|
return findTelemValue(telemetry.getBuffer(), telemetry.getSize(), channel, type, 0.0f);
|
||||||
uint8_t size = telemetry.getSize();
|
|
||||||
uint8_t i = 0;
|
|
||||||
|
|
||||||
while (i + 2 < size) {
|
|
||||||
// Get channel #
|
|
||||||
uint8_t ch = buf[i++];
|
|
||||||
// Get data type
|
|
||||||
uint8_t t = buf[i++];
|
|
||||||
uint8_t sz = getDataSize(t);
|
|
||||||
|
|
||||||
if (ch == channel && t == type) {
|
|
||||||
return getFloat(&buf[i], sz, getMultiplier(t), isSigned(t));
|
|
||||||
}
|
|
||||||
i += sz; // skip
|
|
||||||
}
|
|
||||||
return 0.0f; // not found
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool SensorMesh::getGPS(uint8_t channel, float& lat, float& lon, float& alt) {
|
bool SensorMesh::getGPS(uint8_t channel, float& lat, float& lon, float& alt) {
|
||||||
@@ -902,7 +1004,7 @@ void SensorMesh::loop() {
|
|||||||
if (next_flood_advert && millisHasNowPassed(next_flood_advert)) {
|
if (next_flood_advert && millisHasNowPassed(next_flood_advert)) {
|
||||||
mesh::Packet* pkt = createSelfAdvert();
|
mesh::Packet* pkt = createSelfAdvert();
|
||||||
unsigned long delay_millis = 0;
|
unsigned long delay_millis = 0;
|
||||||
if (pkt) sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1);
|
if (pkt) sendFloodScoped(default_scope, pkt, delay_millis, _prefs.path_hash_mode + 1);
|
||||||
|
|
||||||
updateFloodAdvertTimer(); // schedule next flood advert
|
updateFloodAdvertTimer(); // schedule next flood advert
|
||||||
updateAdvertTimer(); // also schedule local advert (so they don't overlap)
|
updateAdvertTimer(); // also schedule local advert (so they don't overlap)
|
||||||
@@ -931,6 +1033,39 @@ void SensorMesh::loop() {
|
|||||||
telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f);
|
telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f);
|
||||||
// query other sensors -- target specific
|
// query other sensors -- target specific
|
||||||
sensors.querySensors(0xFF, telemetry); // allow all telemetry permissions
|
sensors.querySensors(0xFF, telemetry); // allow all telemetry permissions
|
||||||
|
// This MCU temperature will be overridden by external sensors (if any)
|
||||||
|
float temperature = board.getMCUTemperature();
|
||||||
|
if (!isnan(temperature)) { // Supported boards with built-in temperature sensor. ESP32-C3 may return NAN
|
||||||
|
telemetry.addTemperature(TELEM_CHANNEL_SELF, temperature); // Built-in MCU Temperature
|
||||||
|
}
|
||||||
|
|
||||||
|
// compare with previous telemetry, check if any deltas are greater than subscriber minimums
|
||||||
|
for (int i = 0; i < acl.getNumClients(); i++) {
|
||||||
|
auto c = acl.getClientByIdx(i);
|
||||||
|
if (c->permissions == 0 || c->extra.sensor.scope_region_id == 0 || c->extra.sensor.min_deltas_len == 0) continue; // skip deleted entries, or Not subscribed to deltas
|
||||||
|
RegionEntry* r = region_map.findById(c->extra.sensor.scope_region_id);
|
||||||
|
if (r == NULL) continue; // unknown region scope
|
||||||
|
if (curr > c->extra.sensor.expiry_timestamp) continue; // subscription now expired
|
||||||
|
if (telemHasChanged(c)) {
|
||||||
|
TransportKey scope;
|
||||||
|
if (region_map.getTransportKeysFor(*r, &scope, 1) > 0) {
|
||||||
|
uint8_t tlen = telemetry.getSize();
|
||||||
|
memcpy(reply_data, &c->extra.sensor.push_tag, 4);
|
||||||
|
uint32_t timestamp = getRTCClock()->getCurrentTimeUnique();
|
||||||
|
memcpy(&reply_data[4], ×tamp, 4);
|
||||||
|
memcpy(&reply_data[8], telemetry.getBuffer(), tlen);
|
||||||
|
|
||||||
|
mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, c->id, c->shared_secret, reply_data, 8 + tlen);
|
||||||
|
if (reply) {
|
||||||
|
if (c->out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT
|
||||||
|
sendDirect(reply, c->out_path, c->out_path_len, 0);
|
||||||
|
} else {
|
||||||
|
sendFloodScoped(scope, reply, 0, _prefs.path_hash_mode + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onSensorDataRead();
|
onSensorDataRead();
|
||||||
|
|
||||||
@@ -973,7 +1108,7 @@ void SensorMesh::loop() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// is there are pending dirty contacts write needed?
|
// pending dirty contacts write needed?
|
||||||
if (dirty_contacts_expiry && millisHasNowPassed(dirty_contacts_expiry)) {
|
if (dirty_contacts_expiry && millisHasNowPassed(dirty_contacts_expiry)) {
|
||||||
acl.save(_fs);
|
acl.save(_fs);
|
||||||
dirty_contacts_expiry = 0;
|
dirty_contacts_expiry = 0;
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ public:
|
|||||||
SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables);
|
SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables);
|
||||||
void begin(FILESYSTEM* fs);
|
void begin(FILESYSTEM* fs);
|
||||||
void loop();
|
void loop();
|
||||||
void handleCommand(uint32_t sender_timestamp, char* command, char* reply);
|
void handleCommand(ClientInfo* from, uint32_t sender_timestamp, char* command, char* reply);
|
||||||
|
|
||||||
// CommonCLI callbacks
|
// CommonCLI callbacks
|
||||||
const char* getFirmwareVer() override { return FIRMWARE_VERSION; }
|
const char* getFirmwareVer() override { return FIRMWARE_VERSION; }
|
||||||
@@ -78,9 +78,15 @@ public:
|
|||||||
void saveIdentity(const mesh::LocalIdentity& new_id) override;
|
void saveIdentity(const mesh::LocalIdentity& new_id) override;
|
||||||
void clearStats() override { }
|
void clearStats() override { }
|
||||||
void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) override;
|
void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) override;
|
||||||
|
void startRegionsLoad() override;
|
||||||
|
bool saveRegions() override;
|
||||||
|
void onDefaultRegionChanged(const RegionEntry* r) override;
|
||||||
|
|
||||||
float getTelemValue(uint8_t channel, uint8_t type);
|
float getTelemValue(uint8_t channel, uint8_t type);
|
||||||
|
|
||||||
|
void sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint32_t delay_millis, uint8_t path_hash_size);
|
||||||
|
void sendFloodReply(mesh::Packet* packet, unsigned long delay_millis, uint8_t path_hash_size);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
// current telemetry data queries
|
// current telemetry data queries
|
||||||
float getVoltage(uint8_t channel) { return getTelemValue(channel, LPP_VOLTAGE); }
|
float getVoltage(uint8_t channel) { return getTelemValue(channel, LPP_VOLTAGE); }
|
||||||
@@ -93,12 +99,12 @@ protected:
|
|||||||
bool getGPS(uint8_t channel, float& lat, float& lon, float& alt);
|
bool getGPS(uint8_t channel, float& lat, float& lon, float& alt);
|
||||||
|
|
||||||
// alerts
|
// alerts
|
||||||
enum AlertPriority { LOW_PRI_ALERT, HIGH_PRI_ALERT };
|
enum AlertPriority : uint8_t { LOW_PRI_ALERT, HIGH_PRI_ALERT };
|
||||||
|
|
||||||
struct Trigger {
|
struct Trigger {
|
||||||
uint32_t timestamp;
|
uint32_t timestamp;
|
||||||
AlertPriority pri;
|
|
||||||
uint32_t expected_acks[4];
|
uint32_t expected_acks[4];
|
||||||
|
AlertPriority pri;
|
||||||
int8_t curr_contact_idx;
|
int8_t curr_contact_idx;
|
||||||
uint8_t attempt;
|
uint8_t attempt;
|
||||||
unsigned long send_expiry;
|
unsigned long send_expiry;
|
||||||
@@ -122,6 +128,7 @@ protected:
|
|||||||
int getInterferenceThreshold() const override;
|
int getInterferenceThreshold() const override;
|
||||||
bool getCADEnabled() const override;
|
bool getCADEnabled() const override;
|
||||||
int getAGCResetInterval() const override;
|
int getAGCResetInterval() const override;
|
||||||
|
mesh::DispatcherAction onRecvPacket(mesh::Packet* pkt) override;
|
||||||
void onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) override;
|
void onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) override;
|
||||||
int searchPeersByHash(const uint8_t* hash) override;
|
int searchPeersByHash(const uint8_t* hash) override;
|
||||||
void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) override;
|
void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) override;
|
||||||
@@ -129,6 +136,7 @@ protected:
|
|||||||
bool onPeerPathRecv(mesh::Packet* packet, int sender_idx, const uint8_t* secret, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override;
|
bool onPeerPathRecv(mesh::Packet* packet, int sender_idx, const uint8_t* secret, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override;
|
||||||
void onControlDataRecv(mesh::Packet* packet) override;
|
void onControlDataRecv(mesh::Packet* packet) override;
|
||||||
void onAckRecv(mesh::Packet* packet, uint32_t ack_crc) override;
|
void onAckRecv(mesh::Packet* packet, uint32_t ack_crc) override;
|
||||||
|
|
||||||
virtual bool handleIncomingMsg(ClientInfo& from, uint32_t timestamp, uint8_t* data, uint8_t flags, size_t len);
|
virtual bool handleIncomingMsg(ClientInfo& from, uint32_t timestamp, uint8_t* data, uint8_t flags, size_t len);
|
||||||
void sendAckTo(const ClientInfo& dest, uint32_t ack_hash, uint8_t path_hash_size=1);
|
void sendAckTo(const ClientInfo& dest, uint32_t ack_hash, uint8_t path_hash_size=1);
|
||||||
private:
|
private:
|
||||||
@@ -141,7 +149,9 @@ private:
|
|||||||
unsigned long dirty_contacts_expiry;
|
unsigned long dirty_contacts_expiry;
|
||||||
CayenneLPP telemetry;
|
CayenneLPP telemetry;
|
||||||
TransportKeyStore key_store;
|
TransportKeyStore key_store;
|
||||||
RegionMap region_map;
|
RegionMap region_map, temp_map;
|
||||||
|
RegionEntry* recv_pkt_region;
|
||||||
|
RegionEntry* load_stack[8];
|
||||||
TransportKey default_scope;
|
TransportKey default_scope;
|
||||||
uint32_t last_read_time;
|
uint32_t last_read_time;
|
||||||
int matching_peer_indexes[MAX_SEARCH_RESULTS];
|
int matching_peer_indexes[MAX_SEARCH_RESULTS];
|
||||||
@@ -152,9 +162,11 @@ private:
|
|||||||
float pending_bw;
|
float pending_bw;
|
||||||
uint8_t pending_sf;
|
uint8_t pending_sf;
|
||||||
uint8_t pending_cr;
|
uint8_t pending_cr;
|
||||||
|
bool region_load_active;
|
||||||
|
|
||||||
|
bool telemHasChanged(ClientInfo* c);
|
||||||
uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood);
|
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);
|
uint8_t handleRequest(ClientInfo* from, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len);
|
||||||
mesh::Packet* createSelfAdvert();
|
mesh::Packet* createSelfAdvert();
|
||||||
|
|
||||||
void sendAlert(const ClientInfo* c, Trigger* t);
|
void sendAlert(const ClientInfo* c, Trigger* t);
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ void loop() {
|
|||||||
if (len > 0 && command[len - 1] == '\r') { // received complete line
|
if (len > 0 && command[len - 1] == '\r') { // received complete line
|
||||||
command[len - 1] = 0; // replace newline with C string null terminator
|
command[len - 1] = 0; // replace newline with C string null terminator
|
||||||
char reply[160];
|
char reply[160];
|
||||||
the_mesh.handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial!
|
the_mesh.handleCommand(NULL, 0, command, reply); // NOTE: there is no sender_timestamp via serial!
|
||||||
if (reply[0]) {
|
if (reply[0]) {
|
||||||
Serial.print(" -> "); Serial.println(reply);
|
Serial.print(" -> "); Serial.println(reply);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,14 @@ struct ClientInfo {
|
|||||||
unsigned long ack_timeout;
|
unsigned long ack_timeout;
|
||||||
uint8_t push_failures;
|
uint8_t push_failures;
|
||||||
} room;
|
} room;
|
||||||
|
struct {
|
||||||
|
uint32_t expiry_timestamp; // epoch seconds
|
||||||
|
uint32_t push_tag;
|
||||||
|
uint16_t scope_region_id; // scope to use when sending telemetry to this client/subscriber
|
||||||
|
uint8_t min_deltas_len;
|
||||||
|
uint8_t min_deltas[14]; // LPP encoded
|
||||||
|
uint8_t prev_telem[14]; // LPP encoded
|
||||||
|
} sensor;
|
||||||
} extra;
|
} extra;
|
||||||
|
|
||||||
bool isAdmin() const { return (permissions & PERM_ACL_ROLE_MASK) == PERM_ACL_ADMIN; }
|
bool isAdmin() const { return (permissions & PERM_ACL_ROLE_MASK) == PERM_ACL_ADMIN; }
|
||||||
|
|||||||
@@ -63,12 +63,66 @@
|
|||||||
#define LPP_ERROR_OVERFLOW 1
|
#define LPP_ERROR_OVERFLOW 1
|
||||||
#define LPP_ERROR_UNKOWN_TYPE 2
|
#define LPP_ERROR_UNKOWN_TYPE 2
|
||||||
|
|
||||||
class LPPReader {
|
class LPPData {
|
||||||
const uint8_t* _buf;
|
public:
|
||||||
uint8_t _len;
|
static uint8_t getDataSize(uint8_t type) {
|
||||||
uint8_t _pos;
|
switch (type) {
|
||||||
|
case LPP_GPS:
|
||||||
|
return 9;
|
||||||
|
case LPP_POLYLINE:
|
||||||
|
return 8; // TODO: this is MINIMIUM
|
||||||
|
case LPP_GYROMETER:
|
||||||
|
case LPP_ACCELEROMETER:
|
||||||
|
return 6;
|
||||||
|
case LPP_GENERIC_SENSOR:
|
||||||
|
case LPP_FREQUENCY:
|
||||||
|
case LPP_DISTANCE:
|
||||||
|
case LPP_ENERGY:
|
||||||
|
case LPP_UNIXTIME:
|
||||||
|
return 4;
|
||||||
|
case LPP_COLOUR:
|
||||||
|
return 3;
|
||||||
|
case LPP_ANALOG_INPUT:
|
||||||
|
case LPP_ANALOG_OUTPUT:
|
||||||
|
case LPP_LUMINOSITY:
|
||||||
|
case LPP_TEMPERATURE:
|
||||||
|
case LPP_CONCENTRATION:
|
||||||
|
case LPP_BAROMETRIC_PRESSURE:
|
||||||
|
case LPP_RELATIVE_HUMIDITY:
|
||||||
|
case LPP_ALTITUDE:
|
||||||
|
case LPP_VOLTAGE:
|
||||||
|
case LPP_CURRENT:
|
||||||
|
case LPP_DIRECTION:
|
||||||
|
case LPP_POWER:
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
float getFloat(const uint8_t * buffer, uint8_t size, uint32_t multiplier, bool is_signed) {
|
static uint32_t getMultiplier(uint8_t type) {
|
||||||
|
switch (type) {
|
||||||
|
case LPP_CURRENT:
|
||||||
|
case LPP_DISTANCE:
|
||||||
|
case LPP_ENERGY:
|
||||||
|
return 1000;
|
||||||
|
case LPP_VOLTAGE:
|
||||||
|
case LPP_ANALOG_INPUT:
|
||||||
|
case LPP_ANALOG_OUTPUT:
|
||||||
|
return 100;
|
||||||
|
case LPP_TEMPERATURE:
|
||||||
|
case LPP_BAROMETRIC_PRESSURE:
|
||||||
|
case LPP_RELATIVE_HUMIDITY:
|
||||||
|
return 10;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool isSigned(uint8_t type) {
|
||||||
|
return type == LPP_ALTITUDE || type == LPP_TEMPERATURE || type == LPP_GYROMETER ||
|
||||||
|
type == LPP_ANALOG_INPUT || type == LPP_ANALOG_OUTPUT || type == LPP_GPS || type == LPP_ACCELEROMETER;
|
||||||
|
}
|
||||||
|
|
||||||
|
static float getFloat(const uint8_t * buffer, uint8_t size, uint32_t multiplier, bool is_signed) {
|
||||||
uint32_t value = 0;
|
uint32_t value = 0;
|
||||||
for (uint8_t i = 0; i < size; i++) {
|
for (uint8_t i = 0; i < size; i++) {
|
||||||
value = (value << 8) + buffer[i];
|
value = (value << 8) + buffer[i];
|
||||||
@@ -85,6 +139,36 @@ class LPPReader {
|
|||||||
return sign * ((float) value / multiplier);
|
return sign * ((float) value / multiplier);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static uint8_t putFloat(uint8_t * dest, float value, uint8_t size, uint32_t multiplier, bool is_signed) {
|
||||||
|
// check sign
|
||||||
|
bool sign = value < 0;
|
||||||
|
if (sign) value = -value;
|
||||||
|
|
||||||
|
// get value to store
|
||||||
|
uint32_t v = value * multiplier;
|
||||||
|
|
||||||
|
// format an uint32_t as if it was an int32_t
|
||||||
|
if (is_signed & sign) {
|
||||||
|
uint32_t mask = (1 << (size * 8)) - 1;
|
||||||
|
v = v & mask;
|
||||||
|
if (sign) v = mask - v + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// add bytes (MSB first)
|
||||||
|
for (uint8_t i=1; i<=size; i++) {
|
||||||
|
dest[size - i] = (v & 0xFF);
|
||||||
|
v >>= 8;
|
||||||
|
}
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
class LPPReader {
|
||||||
|
const uint8_t* _buf;
|
||||||
|
uint8_t _len;
|
||||||
|
uint8_t _pos;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
LPPReader(const uint8_t buf[], uint8_t len) : _buf(buf), _len(len), _pos(0) { }
|
LPPReader(const uint8_t buf[], uint8_t len) : _buf(buf), _len(len), _pos(0) { }
|
||||||
|
|
||||||
@@ -103,72 +187,42 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool readGPS(float& lat, float& lon, float& alt) {
|
bool readGPS(float& lat, float& lon, float& alt) {
|
||||||
lat = getFloat(&_buf[_pos], 3, 10000, true); _pos += 3;
|
lat = LPPData::getFloat(&_buf[_pos], 3, 10000, true); _pos += 3;
|
||||||
lon = getFloat(&_buf[_pos], 3, 10000, true); _pos += 3;
|
lon = LPPData::getFloat(&_buf[_pos], 3, 10000, true); _pos += 3;
|
||||||
alt = getFloat(&_buf[_pos], 3, 100, true); _pos += 3;
|
alt = LPPData::getFloat(&_buf[_pos], 3, 100, true); _pos += 3;
|
||||||
return _pos <= _len;
|
return _pos <= _len;
|
||||||
}
|
}
|
||||||
bool readVoltage(float& voltage) {
|
bool readVoltage(float& voltage) {
|
||||||
voltage = getFloat(&_buf[_pos], 2, 100, false); _pos += 2;
|
voltage = LPPData::getFloat(&_buf[_pos], 2, 100, false); _pos += 2;
|
||||||
return _pos <= _len;
|
return _pos <= _len;
|
||||||
}
|
}
|
||||||
bool readCurrent(float& amps) {
|
bool readCurrent(float& amps) {
|
||||||
amps = getFloat(&_buf[_pos], 2, 1000, true); _pos += 2;
|
amps = LPPData::getFloat(&_buf[_pos], 2, 1000, true); _pos += 2;
|
||||||
return _pos <= _len;
|
return _pos <= _len;
|
||||||
}
|
}
|
||||||
bool readPower(float& watts) {
|
bool readPower(float& watts) {
|
||||||
watts = getFloat(&_buf[_pos], 2, 1, false); _pos += 2;
|
watts = LPPData::getFloat(&_buf[_pos], 2, 1, false); _pos += 2;
|
||||||
return _pos <= _len;
|
return _pos <= _len;
|
||||||
}
|
}
|
||||||
bool readTemperature(float& degrees_c) {
|
bool readTemperature(float& degrees_c) {
|
||||||
degrees_c = getFloat(&_buf[_pos], 2, 10, true); _pos += 2;
|
degrees_c = LPPData::getFloat(&_buf[_pos], 2, 10, true); _pos += 2;
|
||||||
return _pos <= _len;
|
return _pos <= _len;
|
||||||
}
|
}
|
||||||
bool readPressure(float& pa) {
|
bool readPressure(float& pa) {
|
||||||
pa = getFloat(&_buf[_pos], 2, 10, false); _pos += 2;
|
pa = LPPData::getFloat(&_buf[_pos], 2, 10, false); _pos += 2;
|
||||||
return _pos <= _len;
|
return _pos <= _len;
|
||||||
}
|
}
|
||||||
bool readRelativeHumidity(float& pct) {
|
bool readRelativeHumidity(float& pct) {
|
||||||
pct = getFloat(&_buf[_pos], 1, 2, false); _pos += 1;
|
pct = LPPData::getFloat(&_buf[_pos], 1, 2, false); _pos += 1;
|
||||||
return _pos <= _len;
|
return _pos <= _len;
|
||||||
}
|
}
|
||||||
bool readAltitude(float& m) {
|
bool readAltitude(float& m) {
|
||||||
m = getFloat(&_buf[_pos], 2, 1, true); _pos += 2;
|
m = LPPData::getFloat(&_buf[_pos], 2, 1, true); _pos += 2;
|
||||||
return _pos <= _len;
|
return _pos <= _len;
|
||||||
}
|
}
|
||||||
|
|
||||||
void skipData(uint8_t type) {
|
void skipData(uint8_t type) {
|
||||||
switch (type) {
|
_pos += LPPData::getDataSize(type);
|
||||||
case LPP_GPS:
|
|
||||||
_pos += 9; break;
|
|
||||||
case LPP_POLYLINE:
|
|
||||||
_pos += 8; break; // TODO: this is MINIMUM
|
|
||||||
case LPP_GYROMETER:
|
|
||||||
case LPP_ACCELEROMETER:
|
|
||||||
_pos += 6; break;
|
|
||||||
case LPP_GENERIC_SENSOR:
|
|
||||||
case LPP_FREQUENCY:
|
|
||||||
case LPP_DISTANCE:
|
|
||||||
case LPP_ENERGY:
|
|
||||||
case LPP_UNIXTIME:
|
|
||||||
_pos += 4; break;
|
|
||||||
case LPP_COLOUR:
|
|
||||||
_pos += 3; break;
|
|
||||||
case LPP_ANALOG_INPUT:
|
|
||||||
case LPP_ANALOG_OUTPUT:
|
|
||||||
case LPP_LUMINOSITY:
|
|
||||||
case LPP_TEMPERATURE:
|
|
||||||
case LPP_CONCENTRATION:
|
|
||||||
case LPP_BAROMETRIC_PRESSURE:
|
|
||||||
case LPP_ALTITUDE:
|
|
||||||
case LPP_VOLTAGE:
|
|
||||||
case LPP_CURRENT:
|
|
||||||
case LPP_DIRECTION:
|
|
||||||
case LPP_POWER:
|
|
||||||
_pos += 2; break;
|
|
||||||
default:
|
|
||||||
_pos++;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -185,6 +239,19 @@ class LPPWriter {
|
|||||||
public:
|
public:
|
||||||
LPPWriter(uint8_t buf[], uint8_t max_len): _buf(buf), _max_len(max_len), _len(0) { }
|
LPPWriter(uint8_t buf[], uint8_t max_len): _buf(buf), _max_len(max_len), _len(0) { }
|
||||||
|
|
||||||
|
bool writeData(uint8_t channel, uint8_t type, float v) {
|
||||||
|
uint8_t sz = LPPData::getDataSize(type);
|
||||||
|
bool s = LPPData::isSigned(type);
|
||||||
|
uint32_t mul = LPPData::getMultiplier(type);
|
||||||
|
if (_len + 2 + sz <= _max_len) {
|
||||||
|
_buf[_len++] = channel;
|
||||||
|
_buf[_len++] = type;
|
||||||
|
_len += LPPData::putFloat(&_buf[_len], v, sz, mul, s);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
bool writeVoltage(uint8_t channel, float voltage) {
|
bool writeVoltage(uint8_t channel, float voltage) {
|
||||||
if (_len + 4 <= _max_len) {
|
if (_len + 4 <= _max_len) {
|
||||||
_buf[_len++] = channel;
|
_buf[_len++] = channel;
|
||||||
|
|||||||
Reference in New Issue
Block a user