Merge upstream dev into keymindCascade

This commit is contained in:
mikecarper committed 2026-09-24 00:51:07 -07:00
commit c31a0e5ed5
40 files changed
+2405 -200

No files matched your search

+72
View File
@@ -0,0 +1,72 @@
{
"build": {
"arduino": {
"ldscript": "nrf52840_s140_v6.ld"
},
"core": "nRF5",
"cpu": "cortex-m4",
"extra_flags": "-DARDUINO_NRF52840_MUZI_BASE -DNRF52840_XXAA",
"f_cpu": "64000000L",
"hwids": [
[
"0x239A",
"0x4405"
],
[
"0x239A",
"0x0029"
],
[
"0x239A",
"0x002A"
]
],
"usb_product": "muzi_base",
"mcu": "nrf52840",
"variant": "MUZI_BASE",
"bsp": {
"name": "adafruit"
},
"softdevice": {
"sd_flags": "-DS140",
"sd_name": "s140",
"sd_version": "6.1.1",
"sd_fwid": "0x00B6"
},
"bootloader": {
"settings_addr": "0xFF000"
}
},
"connectivity": [
"bluetooth"
],
"debug": {
"jlink_device": "nRF52840_xxAA",
"onboard_tools": [
"jlink"
],
"svd_path": "nrf52840.svd",
"openocd_target": "nrf52.cfg"
},
"frameworks": [
"arduino"
],
"name": "Muzi Base",
"upload": {
"maximum_ram_size": 248832,
"maximum_size": 815104,
"speed": 115200,
"use_1200bps_touch": true,
"require_upload_port": true,
"wait_for_upload_port": true,
"protocol": "nrfutil",
"protocols": [
"jlink",
"nrfjprog",
"nrfutil",
"stlink"
]
},
"url": "https://github.com/muzi-works",
"vendor": "MuziWorks"
}
+72
View File
@@ -0,0 +1,72 @@
{
"build": {
"arduino": {
"ldscript": "nrf52840_s140_v6.ld"
},
"core": "nRF5",
"cpu": "cortex-m4",
"extra_flags": "-DNRF52840_XXAA",
"f_cpu": "64000000L",
"hwids": [
[
"0x239A",
"0x4405"
],
[
"0x239A",
"0x0029"
],
[
"0x239A",
"0x002A"
]
],
"usb_product": "elecrow_thinknode_m4",
"mcu": "nrf52840",
"variant": "ELECROW-ThinkNode-M4",
"bsp": {
"name": "adafruit"
},
"softdevice": {
"sd_flags": "-DS140",
"sd_name": "s140",
"sd_version": "6.1.1",
"sd_fwid": "0x00B6"
},
"bootloader": {
"settings_addr": "0xFF000"
}
},
"connectivity": [
"bluetooth"
],
"debug": {
"jlink_device": "nRF52840_xxAA",
"onboard_tools": [
"jlink"
],
"svd_path": "nrf52840.svd",
"openocd_target": "nrf52.cfg"
},
"frameworks": [
"arduino"
],
"name": "elecrow nrf",
"upload": {
"maximum_ram_size": 235520,
"maximum_size": 815104,
"speed": 115200,
"use_1200bps_touch": true,
"require_upload_port": true,
"wait_for_upload_port": true,
"protocol": "nrfutil",
"protocols": [
"jlink",
"nrfjprog",
"nrfutil",
"stlink"
]
},
"url": "https://github.com/Elecrow-RD",
"vendor": "ELECROW"
}
+1 -1
View File
@@ -2394,7 +2394,7 @@ hardware busy-result count.
- `set agc.reset.interval <value>`
**Parameters:**
- `value`: Interval in seconds rounded down to a multiple of 4 (17 becomes 16). 0 to disable.
- `value`: Interval in seconds, 0-1020, rounded down to a multiple of 4 (17 becomes 16). 0 to disable.
**Default:** `0.0`
+44 -8
View File
@@ -97,6 +97,7 @@ Returned path messages provide a description of the route a packet took from the
| Field | Size (bytes) | Description |
|--------------|-----------------|------------------------------------------|
| timestamp | 4 | sender time (unix timestamp) |
| req type | 1 | request sub type |
| request data | rest of payload | application-defined request payload body |
For the common chat/server helpers in `BaseChatMesh`, the current request type values are:
@@ -104,7 +105,13 @@ For the common chat/server helpers in `BaseChatMesh`, the current request type v
| Value | Name | Description |
|--------|-----------|----------------------------------------------------|
| `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
@@ -129,21 +136,50 @@ Gets information about the node, possibly including the following:
* Number posted (?)
* 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
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)
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
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
+8
View File
@@ -8824,6 +8824,14 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp,
return true;
}
if (sender_timestamp == 0
&& (strcmp(command, "poweroff") == 0
|| strcmp(command, "shutdown") == 0)) {
strcpy(reply, "OK");
board.powerOff();
return true;
}
if (strncmp(command, "set name ", 9) == 0) {
const char* name = command + 9;
if (!AdvertDataParser::isValidName(name)) {
+2 -2
View File
@@ -171,8 +171,8 @@ private:
}
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(); }
uint16_t getAgcResetInt() const override { return _parent->agc_reset_interval * 4; }
void setAgcResetInt(uint16_t secs) override { _parent->agc_reset_interval = secs / 4; markDirty(); }
uint8_t getHashMode() const override { return _parent->path_hash_mode; }
void setHashMode(uint8_t value) override {
_parent->path_hash_mode = value;
+1
View File
@@ -3290,6 +3290,7 @@ void loop() {
#endif
#endif
rtc_clock.tick();
board.loop();
#ifdef TBEAM_1W
board.updateFanControl();
#endif
+2
View File
@@ -229,6 +229,8 @@ void loop() {
}
}
board.loop();
if ((uint32_t)(millis() - next_noise_floor_calib_ms) >= NOISE_FLOOR_CALIB_INTERVAL_MS) {
radio_driver.triggerNoiseFloorCalibrate(0);
next_noise_floor_calib_ms = millis();
+3 -1
View File
@@ -949,8 +949,10 @@ void UITask::loop() {
}
if (_powering_off_at > 0) { // power off timer armed
#ifdef LED_PIN
#if defined(LED_PIN) && defined(LED_STATE_ON)
digitalWrite(LED_PIN, LED_STATE_ON); // switch on the led until poweroff
delay(1000);
digitalWrite(LED_PIN, !LED_STATE_ON); // off LED
#endif
if (millisReached(millis(), _powering_off_at)) {
_board->powerOff(); // should not return
+2
View File
@@ -390,6 +390,8 @@ static void __attribute__((noinline)) serviceCommandInterfaces() {
command[0] = 0; // reset command buffer
}
board.loop(); // let the board feed its watchdog, run periodic housekeeping
#ifdef ETHERNET_ENABLED
ethernet_loop_maintain();
if (ethernet_take_session_reset() || !ethernet_client.connected()) {
+1
View File
@@ -280,6 +280,7 @@ void loop() {
if (display_ready) ui_task.loop();
#endif
rtc_clock.tick();
board.loop();
#if MESH_ESP32_USB_CONSOLE_COOPERATIVE
mesh::serviceUsbTerminalPort();
#endif
+273 -131
View File
@@ -4,7 +4,9 @@
#include <helpers/ClientLoginPersistence.h>
#include <helpers/ClientPathPersistence.h>
#include <helpers/LazyPersistence.h>
#include <helpers/RoutingPolicy.h>
#include <helpers/radiolib/RXPowerSaving.h>
#include <helpers/sensors/LPPDataHelpers.h>
#include <helpers/UsbLogging.h>
#if defined(ENABLE_OTA)
#include <helpers/ota/OtaContext.h>
@@ -71,6 +73,10 @@ static uint32_t nextRadioApplyRetryDelay(uint8_t& failure_count) {
#define REQ_TYPE_GET_TELEMETRY_DATA 0x03
#define REQ_TYPE_GET_AVG_MIN_MAX 0x04
#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
@@ -90,78 +96,68 @@ static File openAppend(FILESYSTEM* _fs, const char* fname) {
#endif
}
static uint8_t getDataSize(uint8_t type) {
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;
/* --------------------- Cayenne LPP helpers ----------------------------*/
static float findTelemValue(const uint8_t* buf, uint8_t size, uint8_t channel, uint8_t type, float def_value) {
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 = LPPData::getDataSize(t);
if (sz > size - i) break;
if (ch == channel && t == type) {
return LPPData::getFloat(&buf[i], sz, LPPData::getMultiplier(t), LPPData::isSigned(t));
}
return 1;
}
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];
i += sz; // skip
}
return def_value; // not found
}
int sign = 1;
if (is_signed) {
uint32_t bit = 1ul << ((size * 8) - 1);
if ((value & bit) == bit) {
value = (bit << 1) - value;
sign = -1;
/* ------------------ end Cayenne LPP helpers ----------------------*/
bool SensorMesh::telemHasChanged(ClientInfo* c) {
auto buf = telemetry.getBuffer();
uint8_t size = telemetry.getSize();
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);
if (sz > c->extra.sensor.min_deltas_len - i - 2) return false;
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 (fabsf(v - pv) > min_delta) changed = true;
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);
if (sz > c->extra.sensor.min_deltas_len - i - 2) return false;
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 getTelemetryPermissions(uint8_t acl_perms, uint8_t telemetry_access) {
@@ -194,33 +190,14 @@ void SensorMesh::updateGpsTelemetryPolicy() {
sensors.setTelemetryLocationAccessAvailable(hasLocationTelemetryClient());
}
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;
uint8_t SensorMesh::handleRequest(ClientInfo* from, uint32_t sender_timestamp,
uint8_t req_type, uint8_t* payload,
size_t payload_len) {
const uint8_t perms = from->isAdmin() ? 0xFF : from->permissions;
// 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')
if (req_type == REQ_TYPE_GET_TELEMETRY_DATA) {
if (req_type == REQ_TYPE_GET_TELEMETRY_DATA && payload_len >= 1) {
uint8_t perm_mask = ~(payload[0]); // NEW: first reserved byte (of 4), is now inverse mask to apply to permissions
uint8_t telemetry_permissions = getTelemetryPermissions(perms, _prefs.telemetry_access) & perm_mask;
@@ -242,12 +219,12 @@ uint8_t SensorMesh::handleRequest(uint8_t perms, uint32_t sender_timestamp, uint
}
sensors.querySensors(telemetry_permissions, telemetry);
// TODO: let requester know permissions they have: telemetry.addPresence(TELEM_CHANNEL_SELF, perms);
uint8_t tlen = telemetry.getSize();
memcpy(&reply_data[4], telemetry.getBuffer(), tlen);
return 4 + tlen; // reply_len
}
if (req_type == REQ_TYPE_GET_AVG_MIN_MAX && (perms & PERM_ACL_ROLE_MASK) >= PERM_ACL_READ_ONLY) {
if (req_type == REQ_TYPE_GET_AVG_MIN_MAX && payload_len >= 10
&& (perms & PERM_ACL_ROLE_MASK) >= PERM_ACL_READ_ONLY) {
uint32_t start_secs_ago, end_secs_ago;
memcpy(&start_secs_ago, &payload[0], 4);
memcpy(&end_secs_ago, &payload[4], 4);
@@ -272,16 +249,17 @@ uint8_t SensorMesh::handleRequest(uint8_t perms, uint32_t sender_timestamp, uint
auto d = &data[i];
reply_data[ofs++] = d->_channel;
reply_data[ofs++] = d->_lpp_type;
uint8_t sz = getDataSize(d->_lpp_type);
uint32_t mult = getMultiplier(d->_lpp_type);
bool is_signed = isSigned(d->_lpp_type);
ofs += putFloat(&reply_data[ofs], d->_min, sz, mult, is_signed);
ofs += putFloat(&reply_data[ofs], d->_max, sz, mult, is_signed);
ofs += putFloat(&reply_data[ofs], d->_avg, sz, mult, is_signed);
uint8_t sz = LPPData::getDataSize(d->_lpp_type);
uint32_t mult = LPPData::getMultiplier(d->_lpp_type);
bool is_signed = LPPData::isSigned(d->_lpp_type);
ofs += LPPData::putFloat(&reply_data[ofs], d->_min, sz, mult, is_signed);
ofs += LPPData::putFloat(&reply_data[ofs], d->_max, sz, mult, is_signed);
ofs += LPPData::putFloat(&reply_data[ofs], d->_avg, sz, mult, is_signed);
}
return ofs;
}
if (req_type == REQ_TYPE_GET_ACCESS_LIST && (perms & PERM_ACL_ROLE_MASK) == PERM_ACL_ADMIN) {
if (req_type == REQ_TYPE_GET_ACCESS_LIST && payload_len >= 2
&& (perms & PERM_ACL_ROLE_MASK) == PERM_ACL_ADMIN) {
uint8_t res1 = payload[0]; // reserved for future (extra query params)
uint8_t res2 = payload[1];
if (res1 == 0 && res2 == 0) {
@@ -295,6 +273,56 @@ uint8_t SensorMesh::handleRequest(uint8_t perms, uint32_t sender_timestamp, uint
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);
const 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;
bool valid_deltas = min_deltas_len >= 3
&& min_deltas_len <= sizeof(from->extra.sensor.min_deltas)
&& min_deltas_len <= payload_len - 8;
for (size_t i = 0; valid_deltas && i < min_deltas_len;) {
if (i + 2 > min_deltas_len) {
valid_deltas = false;
break;
}
const size_t item_len = 2 + LPPData::getDataSize(payload[8 + i + 1]);
valid_deltas = item_len <= min_deltas_len - i;
i += item_len;
}
if (r && valid_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
}
@@ -323,7 +351,7 @@ void SensorMesh::sendAlert(const ClientInfo* c, Trigger* t) {
sendDirect(pkt, c->out_path, c->out_path_len);
} else {
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);
@@ -462,6 +490,25 @@ int SensorMesh::getAGCResetInterval() const {
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) {
ClientInfo* existing_client = acl.getClient(sender.pub_key, PUB_KEY_SIZE);
ClientInfo* client = existing_client;
@@ -477,7 +524,7 @@ uint8_t SensorMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t*
} else {
if (strcmp((char *) data, _prefs.password) != 0) { // check for valid admin password
#if MESH_DEBUG
MESH_DEBUG_PRINTLN("Invalid password: %s", &data[4]);
MESH_DEBUG_PRINTLN("Invalid password: %s", &data[0]);
#endif
return 0;
}
@@ -598,6 +645,37 @@ void SensorMesh::handleCommand(uint32_t sender_timestamp, char* command, char* r
_gpio_reply_tracker.beginCommand(gpio_client_index, gpio_path_hash_size,
gpio_client_key);
#endif
if (region_load_active) {
if (StrHelper::isBlank(command)) {
region_map = temp_map;
region_load_active = false;
onDefaultRegionChanged(region_map.getDefaultRegion());
sprintf(reply, "OK - loaded %d regions", region_map.getCount());
} else {
char* name = command;
while (*name == ' ') name++;
const int indent = name - command;
char* end = name;
while (RegionMap::is_name_char(*end)) end++;
if (*end) *end++ = 0;
while (*end && *end != 'F') end++;
if (indent > 0 && indent < 8 && *name) {
RegionEntry* parent = load_stack[indent - 1];
if (parent) {
RegionEntry* old = region_map.findByName(name);
RegionEntry* next = temp_map.putRegion(
name, parent->id, old ? old->id : 0);
if (next) {
next->flags = old ? old->flags
: (*end == 'F' ? 0 : REGION_DENY_FLOOD);
load_stack[indent] = next;
}
}
}
reply[0] = 0;
}
return;
}
while (*command == ' ') command++; // skip leading spaces
if (strlen(command) > 4 && command[2] == '|') { // optional prefix (for companion radio CLI)
@@ -695,6 +773,18 @@ void SensorMesh::onGroupPacketRecv(mesh::Packet* packet) {
_clock_sync.observeGroupPacket(packet);
}
mesh::DispatcherAction SensorMesh::onRecvPacket(mesh::Packet* packet) {
if (packet->getRouteType() == ROUTE_TYPE_TRANSPORT_FLOOD) {
recv_pkt_region = region_map.findMatch(packet, REGION_DENY_FLOOD);
} else if (packet->getRouteType() == ROUTE_TYPE_FLOOD
&& !(region_map.getWildcard().flags & REGION_DENY_FLOOD)) {
recv_pkt_region = &region_map.getWildcard();
} else {
recv_pkt_region = nullptr;
}
return mesh::Mesh::onRecvPacket(packet);
}
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 (len < 4) {
@@ -720,10 +810,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
mesh::Packet* path = createPathReturn(sender, secret, packet->path, packet->path_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 {
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());
}
}
}
@@ -751,7 +841,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) {
if (dest.out_path_len == OUT_PATH_UNKNOWN) {
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 {
uint32_t d = TXT_ACK_DELAY;
if (getExtraAckTransmitCount() > 0) {
@@ -779,7 +869,7 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i
memcpy(&timestamp, data, 4);
if (timestamp > from->last_timestamp) { // prevent replay attacks
uint8_t reply_len = handleRequest(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
from->last_timestamp = timestamp;
@@ -789,14 +879,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
mesh::Packet* path = createPathReturn(from->id, secret, packet->path, packet->path_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 {
mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, from->id, secret, reply_data, reply_len);
if (reply) {
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);
} else {
sendFlood(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
sendFloodReply(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
}
}
}
@@ -819,7 +909,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
mesh::Packet* path = createPathReturn(from->id, secret, packet->path, packet->path_len,
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 {
sendAckTo(*from, ack_hash, packet->getPathHashSize());
}
@@ -851,7 +941,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);
if (reply) {
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 {
sendDirect(reply, from->out_path, from->out_path_len, CLI_REPLY_DELAY_MILLIS);
}
@@ -951,7 +1041,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)
: 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),
_clock_sync(radio, ms, rtc, acl, sensors, _prefs.tx_delay_factor, this),
telemetry(MAX_PACKET_PAYLOAD - 4)
@@ -967,6 +1057,8 @@ SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::Millise
saved_radio_apply_pending = false;
radio_apply_retry_at = 0;
radio_apply_failures = 0;
recv_pkt_region = nullptr;
region_load_active = false;
// defaults
_prefs.airtime_factor = 1.0; // one half
@@ -1129,11 +1221,42 @@ bool SensorMesh::scheduleNormalRadio() {
return true;
}
void SensorMesh::sendFloodScoped(const TransportKey& scope, mesh::Packet* packet,
uint32_t delay_millis, uint8_t path_hash_size) {
if (scope.isNull()) {
sendFlood(packet, delay_millis, path_hash_size);
} else {
uint16_t codes[2] = {scope.calcTransportCode(packet), 0};
sendFlood(packet, codes, delay_millis, path_hash_size);
}
}
void SensorMesh::sendFloodReply(mesh::Packet* packet,
unsigned long delay_millis,
uint8_t path_hash_size) {
TransportKey request_scope;
const bool wildcard = recv_pkt_region && recv_pkt_region->isWildcard();
const bool request_scope_known = recv_pkt_region && !wildcard
&& region_map.getTransportKeysFor(*recv_pkt_region, &request_scope, 1) > 0;
switch (mesh::chooseReplyScope(request_scope_known, wildcard,
!default_scope.isNull())) {
case mesh::REPLY_SCOPE_REQUEST:
sendFloodScoped(request_scope, packet, delay_millis, path_hash_size);
break;
case mesh::REPLY_SCOPE_DEFAULT:
sendFloodScoped(default_scope, packet, delay_millis, path_hash_size);
break;
case mesh::REPLY_SCOPE_NONE:
sendFlood(packet, delay_millis, path_hash_size);
break;
}
}
void SensorMesh::sendSelfAdvertisement(int delay_millis, bool flood) {
mesh::Packet* pkt = createSelfAdvert();
if (pkt) {
if (flood) {
sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1);
sendFloodScoped(default_scope, pkt, delay_millis, _prefs.path_hash_mode + 1);
} else {
sendZeroHop(pkt, delay_millis);
}
@@ -1184,23 +1307,7 @@ void SensorMesh::formatPacketStatsReply(char *reply) {
}
float SensorMesh::getTelemValue(uint8_t channel, uint8_t type) {
auto buf = telemetry.getBuffer();
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
return findTelemValue(telemetry.getBuffer(), telemetry.getSize(), channel, type, 0.0f);
}
bool SensorMesh::getGPS(uint8_t channel, float& lat, float& lon, float& alt) {
@@ -1222,7 +1329,7 @@ void SensorMesh::loop() {
if (next_flood_advert && millisHasNowPassed(next_flood_advert)) {
mesh::Packet* pkt = createSelfAdvert();
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
updateAdvertTimer(); // also schedule local advert (so they don't overlap)
@@ -1307,6 +1414,41 @@ void SensorMesh::loop() {
telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f);
// query other sensors -- target specific
sensors.querySensors(TELEM_PERM_BASE | TELEM_PERM_ENVIRONMENT, telemetry);
const float temperature = board.getMCUTemperature();
if (!isnan(temperature)) {
telemetry.addTemperature(TELEM_CHANNEL_SELF, temperature);
}
for (int i = 0; i < acl.getNumClients(); i++) {
ClientInfo* client = acl.getClientByIdx(i);
if ((client->permissions & PERM_ACL_ROLE_MASK) < PERM_ACL_READ_ONLY
|| client->extra.sensor.scope_region_id == 0
|| client->extra.sensor.min_deltas_len == 0
|| curr > client->extra.sensor.expiry_timestamp) {
continue;
}
RegionEntry* region =
region_map.findById(client->extra.sensor.scope_region_id);
if (!region || !telemHasChanged(client)) continue;
TransportKey scope;
if (region_map.getTransportKeysFor(*region, &scope, 1) == 0) continue;
const uint8_t telemetry_len = telemetry.getSize();
if (telemetry_len > sizeof(reply_data) - 8) continue;
memcpy(reply_data, &client->extra.sensor.push_tag, 4);
const uint32_t timestamp = getRTCClock()->getCurrentTimeUnique();
memcpy(&reply_data[4], &timestamp, 4);
memcpy(&reply_data[8], telemetry.getBuffer(), telemetry_len);
mesh::Packet* reply = createDatagram(
PAYLOAD_TYPE_RESPONSE, client->id, client->shared_secret,
reply_data, 8 + telemetry_len);
if (reply) {
if (client->out_path_len != OUT_PATH_UNKNOWN) {
sendDirect(reply, client->out_path, client->out_path_len, 0);
} else {
sendFloodScoped(scope, reply, 0, _prefs.path_hash_mode + 1);
}
}
}
onSensorDataRead();
@@ -1349,7 +1491,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)) {
const bool saved = acl.save(_fs);
if (saved) {
+16 -4
View File
@@ -97,6 +97,9 @@ public:
mesh::Radio* getProfileRadio() override { return _radio; }
void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins, uint16_t preamble = 0) override;
bool scheduleNormalRadio() override;
void startRegionsLoad() override;
bool saveRegions() override;
void onDefaultRegionChanged(const RegionEntry* r) override;
#if defined(ESP32_PLATFORM) || defined(USER_GPIO_CONTROL)
uint32_t getUserGpioRequestSource() const override {
return _gpio_reply_tracker.requestSource();
@@ -113,6 +116,9 @@ public:
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:
bool isTempRadioActive() const override {
return temp_radio_applied && revert_radio_at != 0 && !millisHasNowPassed(revert_radio_at);
@@ -128,12 +134,12 @@ protected:
bool getGPS(uint8_t channel, float& lat, float& lon, float& alt);
// alerts
enum AlertPriority { LOW_PRI_ALERT, HIGH_PRI_ALERT };
enum AlertPriority : uint8_t { LOW_PRI_ALERT, HIGH_PRI_ALERT };
struct Trigger {
uint32_t timestamp;
AlertPriority pri;
uint32_t expected_acks[4];
AlertPriority pri;
int8_t curr_contact_idx;
uint8_t attempt;
unsigned long send_expiry;
@@ -173,6 +179,7 @@ protected:
uint8_t getDefaultTxCodingRate() const override {
return active_cr;
}
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;
int searchPeersByHash(const uint8_t* hash) override;
void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) override;
@@ -202,7 +209,9 @@ private:
uint8_t contacts_save_failures;
CayenneLPP telemetry;
TransportKeyStore key_store;
RegionMap region_map;
RegionMap region_map, temp_map;
RegionEntry* recv_pkt_region;
RegionEntry* load_stack[8];
TransportKey default_scope;
uint32_t last_read_time;
int matching_peer_indexes[MAX_SEARCH_RESULTS];
@@ -224,9 +233,12 @@ private:
uint32_t limitSleepToMillisTimer(unsigned long timestamp,
uint32_t sleep_secs) const;
bool hasPendingWork() const;
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 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);
bool hasLocationTelemetryClient();
void updateGpsTelemetryPolicy();
mesh::Packet* createSelfAdvert();
+2
View File
@@ -254,6 +254,8 @@ void loop() {
command[0] = 0; // reset command buffer
}
board.loop(); // let the board feed its watchdog, run periodic housekeeping
the_mesh.loop();
sensors.loop();
#ifdef DISPLAY_CLASS
+3
View File
@@ -251,6 +251,9 @@ public:
return false;
}
// Let a board feed its watchdog and perform periodic housekeeping.
virtual void loop() {}
inline static uint32_t n_cad_busy = 0;
};
+8
View File
@@ -40,6 +40,14 @@ struct ClientInfo {
unsigned long ack_timeout;
uint8_t push_failures;
} 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;
bool isAdmin() const { return (permissions & PERM_ACL_ROLE_MASK) == PERM_ACL_ADMIN; }
+2 -2
View File
@@ -300,8 +300,8 @@ private:
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(); }
uint16_t getAgcResetInt() const override { return _parent->agc_reset_interval * 4; }
void setAgcResetInt(uint16_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; }
+2 -2
View File
@@ -41,8 +41,8 @@ public:
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 uint16_t getAgcResetInt() const = 0;
virtual void setAgcResetInt(uint16_t secs) = 0;
virtual uint8_t getHashMode() const = 0;
virtual void setHashMode(uint8_t m) = 0;
+7 -2
View File
@@ -43,8 +43,13 @@ bool StrHelper::isBlank(const char* str) {
}
#include <Arduino.h>
#include <stdio.h>
#if __has_include(<api/itoa.h>)
// https://github.com/stm32duino/Arduino_Core_STM32/issues/3084
#include <api/itoa.h>
#endif
union int32_Float_t
union int32_Float_t
{
int32_t Long;
float Float;
@@ -116,7 +121,7 @@ static void _ftoa(float f, char *p, int *status)
*p++ = '-';
if (int_part == 0)
*p++ = '0';
else
else
{
#if defined(STM32_PLATFORM)
// STM32 Arduino exposes utoa, but recent cores no longer declare ltoa.
+105
View File
@@ -0,0 +1,105 @@
#pragma once
#include <RadioLib.h>
#include "MeshCore.h"
class CustomLR1121 : public LR1121 {
uint32_t _preambleMillis = 66;
uint32_t _maxPayloadMillis = 3934;
uint32_t _activityAt = 0;
bool _headerSeen = false;
bool _rx_boosted = false;
public:
CustomLR1121(Module *mod) : LR1121(mod) { }
int16_t begin(float freq = 434.0, float bw = 125.0, uint8_t sf = 9, uint8_t cr = 7,
uint8_t syncWord = RADIOLIB_LR11X0_LORA_SYNC_WORD_PRIVATE, int8_t power = 10,
uint16_t preambleLength = 8, float tcxoVoltage = 1.6) {
int16_t state = LR1121::begin(freq, bw, sf, cr, syncWord, power, preambleLength,
tcxoVoltage);
// RadioLib begin() defaults to LDO; use the LR1121 DC/DC regulator.
if (state == RADIOLIB_ERR_NONE) state = setRegulatorDCDC();
return state;
}
size_t getPacketLength(bool update) override {
size_t len = LR1121::getPacketLength(update);
if (len == 0 && getIrqStatus() & RADIOLIB_LR11X0_IRQ_HEADER_ERR) {
// we've just received a corrupted packet
// this may have triggered a bug causing subsequent packets to be shifted
// call standby() to return radio to known-good state
// recvRaw will call startReceive() to restart rx
MESH_DEBUG_PRINTLN("LR1121: got header err, calling standby()");
standby();
}
return len;
}
float getFreqMHz() const { return freqMHz; }
int16_t setRxBoostedGainMode(bool en) {
_rx_boosted = en;
return LR1121::setRxBoostedGainMode(en);
}
bool getRxBoostedGainMode() const { return _rx_boosted; }
int16_t startReceive() override {
// include the PREAMBLE_DETECTED irq bit in reported flags.
return LR1121::startReceive(RADIOLIB_LR11X0_RX_TIMEOUT_INF, RADIOLIB_IRQ_RX_DEFAULT_FLAGS | (1UL << RADIOLIB_IRQ_PREAMBLE_DETECTED), RADIOLIB_IRQ_RX_DEFAULT_MASK, 0);
}
bool isReceiving() {
uint32_t irq = getIrqStatus();
bool preamble = irq & RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED; // bit 4
bool header = irq & RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID; // bit 5
bool hdrErr = irq & RADIOLIB_LR11X0_IRQ_HEADER_ERR; // bit 6
uint32_t now = millis();
if (hdrErr) {
clearIrqState(RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED | RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID | RADIOLIB_LR11X0_IRQ_HEADER_ERR);
_activityAt = 0;
_headerSeen = false;
return false;
}
if (!header && _headerSeen) {
// something cleared the header flag, reset our state.
_activityAt = 0; _headerSeen = false;
return false;
}
if (header) {
if (!_headerSeen) { _headerSeen = true; _activityAt = now; };
if (now - _activityAt > _maxPayloadMillis) {
MESH_DEBUG_PRINTLN("Clearing header IRQ after %ums", _maxPayloadMillis);
clearIrqState(RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED | RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID | RADIOLIB_LR11X0_IRQ_HEADER_ERR);
_activityAt = 0; _headerSeen = false;
return false;
}
return true;
}
if (preamble) {
if (_activityAt == 0) _activityAt = now;
if (now - _activityAt > _preambleMillis) {
clearIrqState(RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED);
_activityAt = 0;
MESH_DEBUG_PRINTLN("Clearing preamble IRQ after %ums", _preambleMillis);
return false;
}
return true;
}
_activityAt = 0; _headerSeen = false;
return false;
}
void setPreambleMillis(uint32_t preambleMillis) {
_preambleMillis = preambleMillis;
MESH_DEBUG_PRINTLN("Set _preambleMillis=%u", _preambleMillis);
}
void setMaxPayloadMillis(uint32_t payloadMillis) {
_maxPayloadMillis = payloadMillis;
MESH_DEBUG_PRINTLN("Set _maxPayloadMillis=%u", _maxPayloadMillis);
}
uint8_t getSpreadingFactor() const { return spreadingFactor; }
};
@@ -0,0 +1,53 @@
#pragma once
#include "CustomLR1121.h"
#include "RadioLibWrappers.h"
#include "LR11x0Reset.h"
class CustomLR1121Wrapper : public RadioLibWrapper {
public:
CustomLR1121Wrapper(CustomLR1121& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
void setParams(float freq, float bw, uint8_t sf, uint8_t cr) override {
((CustomLR1121 *)_radio)->setFrequency(freq);
((CustomLR1121 *)_radio)->setSpreadingFactor(sf);
((CustomLR1121 *)_radio)->setBandwidth(bw);
((CustomLR1121 *)_radio)->setCodingRate(cr);
updatePreamble(sf);
PacketMillis pm = calcMaxPacketMillis(sf, bw, cr, preambleLengthForSF(sf));
((CustomLR1121 *)_radio)->setPreambleMillis(pm.preambleMillis);
((CustomLR1121 *)_radio)->setMaxPayloadMillis(pm.payloadMillis);
}
void doResetAGC() override { lr11x0ResetAGC((LR11x0 *)_radio, ((CustomLR1121 *)_radio)->getFreqMHz(), getRxBoostedGainMode()); }
bool isReceivingPacket() override {
return ((CustomLR1121 *)_radio)->isReceiving();
}
float getCurrentRSSI() override {
float rssi = -110;
((CustomLR1121 *)_radio)->getRssiInst(&rssi);
return rssi;
}
void onSendFinished() override {
RadioLibWrapper::onSendFinished();
_radio->setPreambleLength(preambleLengthForSF(getSpreadingFactor())); // overcomes weird issues with small and big pkts
}
uint32_t getEstAirtimeFor(int len_bytes) override {
auto airtime = RadioLibWrapper::getEstAirtimeFor(len_bytes);
return airtime < 200 ? 200 : airtime; // at least 200 millis
}
float getLastRSSI() const override { return ((CustomLR1121 *)_radio)->getRSSI(); }
float getLastSNR() const override { return ((CustomLR1121 *)_radio)->getSNR(); }
uint8_t getSpreadingFactor() const override { return ((CustomLR1121 *)_radio)->getSpreadingFactor(); }
bool setRxBoostedGainMode(bool en) override {
return ((CustomLR1121 *)_radio)->setRxBoostedGainMode(en) == RADIOLIB_ERR_NONE;
}
bool getRxBoostedGainMode() const override {
return ((CustomLR1121 *)_radio)->getRxBoostedGainMode();
}
};
+113 -46
View File
@@ -63,12 +63,66 @@
#define LPP_ERROR_OVERFLOW 1
#define LPP_ERROR_UNKOWN_TYPE 2
class LPPReader {
const uint8_t* _buf;
uint8_t _len;
uint8_t _pos;
class LPPData {
public:
static uint8_t getDataSize(uint8_t type) {
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;
for (uint8_t i = 0; i < size; i++) {
value = (value << 8) + buffer[i];
@@ -85,6 +139,36 @@ class LPPReader {
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:
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) {
lat = getFloat(&_buf[_pos], 3, 10000, true); _pos += 3;
lon = getFloat(&_buf[_pos], 3, 10000, true); _pos += 3;
alt = getFloat(&_buf[_pos], 3, 100, true); _pos += 3;
lat = LPPData::getFloat(&_buf[_pos], 3, 10000, true); _pos += 3;
lon = LPPData::getFloat(&_buf[_pos], 3, 10000, true); _pos += 3;
alt = LPPData::getFloat(&_buf[_pos], 3, 100, true); _pos += 3;
return _pos <= _len;
}
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;
}
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;
}
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;
}
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;
}
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;
}
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;
}
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;
}
void skipData(uint8_t type) {
switch (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++;
}
_pos += LPPData::getDataSize(type);
}
};
@@ -185,6 +239,19 @@ class LPPWriter {
public:
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) {
if (_len + 4 <= _max_len) {
_buf[_len++] = channel;
+113
View File
@@ -0,0 +1,113 @@
#include "SH1107Display.h"
#include <Adafruit_GrayOLED.h>
#include "Adafruit_SH110X.h"
#ifndef DISPLAY_ROTATION
#define DISPLAY_ROTATION 0
#endif
ColorVal UIColor::window_bkg = SH110X_BLACK;
ColorVal UIColor::title_bkg = SH110X_BLACK;
ColorVal UIColor::title_txt = SH110X_WHITE;
ColorVal UIColor::primary_txt = SH110X_WHITE;
ColorVal UIColor::secondary_txt = SH110X_WHITE;
ColorVal UIColor::warning_txt = SH110X_WHITE;
ColorVal UIColor::popup_bkg = SH110X_BLACK;
ColorVal UIColor::popup_txt = SH110X_WHITE;
ColorVal UIColor::corp_blue = SH110X_WHITE;
bool SH1107Display::i2c_probe(TwoWire &wire, uint8_t addr)
{
wire.beginTransmission(addr);
uint8_t error = wire.endTransmission();
return (error == 0);
}
bool SH1107Display::begin()
{
bool result = display.begin(DISPLAY_ADDRESS, true) && i2c_probe(Wire, DISPLAY_ADDRESS);
if (result) {
display.setRotation(DISPLAY_ROTATION);
}
return result;
}
void SH1107Display::turnOn()
{
display.oled_command(SH110X_DISPLAYON);
uint8_t cmd[] = {0xD5, 0xF0};
display.oled_commandList(cmd, 2);
_isOn = true;
}
void SH1107Display::turnOff()
{
display.oled_command(SH110X_DISPLAYOFF);
_isOn = false;
}
void SH1107Display::clear()
{
display.clearDisplay();
display.display();
}
void SH1107Display::startFrame(ColorVal bkg)
{
display.clearDisplay(); // TODO: apply 'bkg'
display.setContrast(120); // 0-127. default setting was causing some flickering.
// display.SH110X_SETPRECHARGE(255);
_color = SH110X_WHITE;
display.setTextColor(_color);
display.setTextSize(1);
display.cp437(true); // Use full 256 char 'Code Page 437' font
}
void SH1107Display::setTextSize(int sz)
{
display.setTextSize(sz);
}
void SH1107Display::setColor(ColorVal c)
{
_color = (c != 0) ? SH110X_WHITE : SH110X_BLACK;
display.setTextColor(_color);
}
void SH1107Display::setCursor(int x, int y)
{
display.setCursor(x, y);
}
void SH1107Display::print(const char *str)
{
display.print(str);
}
void SH1107Display::fillRect(int x, int y, int w, int h)
{
display.fillRect(x, y, w, h, _color);
}
void SH1107Display::drawRect(int x, int y, int w, int h)
{
display.drawRect(x, y, w, h, _color);
}
void SH1107Display::drawXbm(int x, int y, const uint8_t *bits, int w, int h)
{
display.drawBitmap(x, y, bits, w, h, SH110X_WHITE);
}
uint16_t SH1107Display::getTextWidth(const char *str)
{
int16_t x1, y1;
uint16_t w, h;
display.getTextBounds(str, 0, 0, &x1, &y1, &w, &h);
return w;
}
void SH1107Display::endFrame()
{
display.display();
}
+43
View File
@@ -0,0 +1,43 @@
#pragma once
#include "DisplayDriver.h"
#include <Wire.h>
#include <Adafruit_GFX.h>
#define SH110X_NO_SPLASH
#include <Adafruit_SH110X.h>
#ifndef PIN_OLED_RESET
#define PIN_OLED_RESET -1
#endif
#ifndef DISPLAY_ADDRESS
#define DISPLAY_ADDRESS 0x3c
#endif
class SH1107Display : public DisplayDriver
{
Adafruit_SH1107 display;
bool _isOn;
uint8_t _color;
bool i2c_probe(TwoWire &wire, uint8_t addr);
public:
SH1107Display() : DisplayDriver(128, 128), display(128, 128, &Wire, PIN_OLED_RESET) { _isOn = false; }
bool begin();
bool isOn() override { return _isOn; }
void turnOn() override;
void turnOff() override;
void clear() override;
void startFrame(ColorVal bkg = UIColor::window_bkg) override;
void setTextSize(int sz) override;
void setColor(ColorVal c) override;
void setCursor(int x, int y) override;
void print(const char *str) override;
void fillRect(int x, int y, int w, int h) override;
void drawRect(int x, int y, int w, int h) override;
void drawXbm(int x, int y, const uint8_t *bits, int w, int h) override;
uint16_t getTextWidth(const char *str) override;
void endFrame() override;
};
+1 -1
View File
@@ -1,7 +1,7 @@
[Heltec_ct62]
extends = esp32_base
board = esp32-c3-devkitm-1
board_build.partitions = variants/dual_ota_1536k.csv
board_build.partitions = variants/dual_ota_full_4MB.csv
build_flags =
${esp32_base.build_flags}
-I variants/heltec_ct62
+64
View File
@@ -0,0 +1,64 @@
#include <Arduino.h>
#include <Wire.h>
#include "MuziBaseBoard.h"
#ifdef NRF52_POWER_MANAGEMENT
const PowerMgtConfig power_config = {
.lpcomp_ain_channel = PWRMGT_LPCOMP_AIN,
.lpcomp_refsel = PWRMGT_LPCOMP_REFSEL,
.voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK
};
void MuziBaseBoard::initiateShutdown(uint8_t reason) {
// Disable LoRa module power before shutdown
if (reason == SHUTDOWN_REASON_LOW_VOLTAGE ||
reason == SHUTDOWN_REASON_BOOT_PROTECT) {
configureVoltageWake(power_config.lpcomp_ain_channel, power_config.lpcomp_refsel);
}
enterSystemOff(reason);
}
#endif // NRF52_POWER_MANAGEMENT
void MuziBaseBoard::begin() {
NRF52BoardDCDC::begin();
pinMode(PIN_VBAT_READ, INPUT);
pinMode(PIN_BATTERY_CHARGING, INPUT);
pinMode(PIN_CHARGER_FAULT, INPUT);
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, LOW);
// output latches default to HIGH, so pull these low right after enabling
pinMode(BUZZER_PIN, OUTPUT);
digitalWrite(BUZZER_PIN, LOW);
// gps power is driven by the sensor manager (mode switch). off to start.
pinMode(PIN_GPS_EN, OUTPUT);
digitalWrite(PIN_GPS_EN, LOW);
// 12V rail is only needed for the superIO display
pinMode(SCREEN_12V_ENABLE, OUTPUT);
#ifdef MUZI_BASE_SUPERIO
digitalWrite(SCREEN_12V_ENABLE, HIGH); // Enable 12V power for SH1107 display
delay(250);
#else
digitalWrite(SCREEN_12V_ENABLE, LOW);
#endif
Wire.begin();
// delay(1000); // wait for display to initialize. otherwise it doesn't come up on boot.
#ifdef PIN_USER_BTN
pinMode(PIN_USER_BTN, INPUT_PULLUP);
#endif
pinMode(PIN_BUTTON1, INPUT_PULLUP);
pinMode(PIN_BUTTON2, INPUT_PULLUP);
pinMode(PIN_BUTTON3, INPUT_PULLUP);
pinMode(PIN_BUTTON4, INPUT_PULLUP);
pinMode(PIN_BUTTON5, INPUT_PULLUP);
pinMode(PIN_BUTTON6, INPUT_PULLUP);
// #if defined(PIN_BOARD_SDA) && defined(PIN_BOARD_SCL)
// Wire.setPins(PIN_BOARD_SDA, PIN_BOARD_SCL);
// #endif
#ifdef NRF52_POWER_MANAGEMENT
checkBootVoltage(&power_config);
#endif
}
+48
View File
@@ -0,0 +1,48 @@
#pragma once
#include <MeshCore.h>
#include <Arduino.h>
#include <helpers/NRF52Board.h>
// The Muzi Base PCB ships in two radio flavors. Keep the user-facing identity
// (OTA/DFU name and reported manufacturer) distinct per radio even though the
// board logic is shared.
#if defined(USE_SX1262)
#define MUZI_BASE_OTA_NAME "MUZI_BASE_UNO_OTA"
#define MUZI_BASE_MFR_NAME "Muzi Base Uno"
#else
#define MUZI_BASE_OTA_NAME "MUZI_BASE_DUO_OTA"
#define MUZI_BASE_MFR_NAME "Muzi Base Duo"
#endif
class MuziBaseBoard : public NRF52BoardDCDC {
protected:
#ifdef NRF52_POWER_MANAGEMENT
void initiateShutdown(uint8_t reason) override;
#endif
public:
MuziBaseBoard() : NRF52Board(MUZI_BASE_OTA_NAME) {}
void begin();
#define BATTERY_SAMPLES 8
uint16_t getBattMilliVolts() override {
analogReadResolution(12);
analogReference(AR_INTERNAL_3_0);
delay(1);
uint32_t raw = 0;
for (int i = 0; i < BATTERY_SAMPLES; i++) {
raw += analogRead(PIN_VBAT_READ);
}
raw = raw / BATTERY_SAMPLES;
// ADC_MULTIPLIER is the voltage divider ratio
return (raw * ADC_MULTIPLIER * AREF_VOLTAGE) / 4.096;
}
const char* getManufacturerName() const override {
return MUZI_BASE_MFR_NAME;
}
};
+322
View File
@@ -0,0 +1,322 @@
; ============================================================================
; Muzi Base (MuziWorks)
;
; One nRF52840 board, shipped in two radio flavors. The radio is selected per
; environment; all board logic lives in variants/muzi_base and is shared:
; muzi_base_duo_* -> LR1121 (USE_LR1121)
; muzi_base_uno_* -> SX1262 (USE_SX1262)
; ============================================================================
[muzi_base_common]
extends = nrf52_base
board = muzi_base
board_build.ldscript = boards/nrf52840_s140_v6.ld
build_flags = ${nrf52_base.build_flags}
-I src/helpers/nrf52
-I lib/nrf52/s140_nrf52_6.1.1_API/include
-I lib/nrf52/s140_nrf52_6.1.1_API/include/nrf52
-I variants/muzi_base
-I src/helpers/ui
-D MUZI_BASE
-D NRF52_POWER_MANAGEMENT
-D PIN_USER_BTN=PIN_BUTTON1
-D USER_BTN_PRESSED=LOW
-D PIN_STATUS_LED=35
-D LORA_TX_POWER=22
-D P_LORA_BUSY=LORA_BUSY
-D P_LORA_SCLK=LORA_SCLK
-D P_LORA_NSS=LORA_NSS
-D P_LORA_DIO_1=LORA_DIO_1
-D P_LORA_MISO=LORA_MISO
-D P_LORA_MOSI=LORA_MOSI
-D P_LORA_RESET=LORA_RESET
-D QSPIFLASH=1
build_src_filter = ${nrf52_base.build_src_filter}
+<helpers/*.cpp>
+<helpers/sensors>
+<../variants/muzi_base>
debug_tool = jlink
upload_protocol = nrfutil
lib_deps =
${nrf52_base.lib_deps}
${sensor_base.lib_deps}
; ---------------------------------------------------------------------------
; Radio selection (Duo = LR1121, Uno = SX1262). The matching radio pins / TCXO
; / RF-switch defines are picked up in variant.h via USE_LR1121 / USE_SX1262.
; ---------------------------------------------------------------------------
[muzi_base_duo]
extends = muzi_base_common
build_flags = ${muzi_base_common.build_flags}
-D USE_LR1121
-D RADIO_CLASS=CustomLR1121
-D WRAPPER_CLASS=CustomLR1121Wrapper
-D RF_SWITCH_TABLE
-D RX_BOOSTED_GAIN=true
-D LR11X0_DIO_AS_RF_SWITCH=true
-D LR11X0_DIO3_TCXO_VOLTAGE=3.0
[muzi_base_uno]
extends = muzi_base_common
build_flags = ${muzi_base_common.build_flags}
-D USE_SX1262
-D RADIO_CLASS=CustomSX1262
-D WRAPPER_CLASS=CustomSX1262Wrapper
-D SX126X_RX_BOOSTED_GAIN=1
-D SX126X_CURRENT_LIMIT=140
-D SX126X_DIO2_AS_RF_SWITCH=1
-D SX126X_DIO3_TCXO_VOLTAGE=3.3
; ===========================================================================
; Duo (LR1121) environments
; ===========================================================================
[env:muzi_base_duo_repeater]
extends = muzi_base_duo
build_flags = ${muzi_base_duo.build_flags}
-I examples/companion_radio/ui-new
-D ADVERT_NAME='"Muzi Base Repeater"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D MAX_NEIGHBOURS=50
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${muzi_base_duo.build_src_filter}
+<../examples/simple_repeater>
lib_deps = ${muzi_base_duo.lib_deps}
[env:muzi_base_duo_room_server]
extends = muzi_base_duo
build_flags = ${muzi_base_duo.build_flags}
-I examples/companion_radio/ui-new
-D ADVERT_NAME='"Muzi Base Room"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D ROOM_PASSWORD='"hello"'
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${muzi_base_duo.build_src_filter}
+<../examples/simple_room_server>
lib_deps = ${muzi_base_duo.lib_deps}
[env:muzi_base_duo_companion_radio_usb]
extends = muzi_base_duo
board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
board_upload.maximum_size = 708608
build_flags = ${muzi_base_duo.build_flags}
-I examples/companion_radio/ui-new
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
-D OFFLINE_QUEUE_SIZE=256
-D DISPLAY_CLASS=NullDisplayDriver
build_src_filter = ${muzi_base_duo.build_src_filter}
+<helpers/ui/NullDisplayDriver.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps = ${muzi_base_duo.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:muzi_base_duo_companion_radio_ble]
extends = muzi_base_duo
board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
board_upload.maximum_size = 708608
build_flags = ${muzi_base_duo.build_flags}
-I examples/companion_radio/ui-new
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
-D BLE_PIN_CODE=123456
-D QSPIFLASH=1
; -D BLE_DEBUG_LOGGING=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
-D OFFLINE_QUEUE_SIZE=256
-D DISPLAY_CLASS=NullDisplayDriver
; -D ADVERT_NAME='"@@MAC"'
build_src_filter = ${muzi_base_duo.build_src_filter}
+<helpers/nrf52/SerialBLEInterface.cpp>
+<helpers/sensors>
+<helpers/ui/NullDisplayDriver.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps = ${muzi_base_duo.lib_deps}
densaugeo/base64 @ ~1.4.0
[muzi_base_duo_superIO]
extends = muzi_base_duo
board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
board_upload.maximum_size = 708608
build_flags = ${muzi_base_duo.build_flags}
-D MUZI_BASE_SUPERIO
-D UI_HAS_JOYSTICK=1
-D OFFLINE_QUEUE_SIZE=256
-D DISPLAY_CLASS=SH1107Display
-D DISPLAY_ROTATION=2
-D ENV_INCLUDE_GPS=1
-D ENV_SKIP_GPS_DETECT
-D PIN_BUZZER=22
build_src_filter = ${muzi_base_duo.build_src_filter}
+<helpers/nrf52/SerialBLEInterface.cpp>
+<helpers/sensors>
+<helpers/ui/SH1107Display.cpp>
+<helpers/ui/buzzer.cpp>
+<helpers/ui/MomentaryButton.cpp>
lib_deps = ${muzi_base_duo.lib_deps}
densaugeo/base64 @ ~1.4.0
stevemarple/MicroNMEA @ ^2.0.6
end2endzone/NonBlockingRTTTL@^1.3.0
adafruit/Adafruit SH110X@^2.1.14
artronshop/ArtronShop_RX8130CE@1.0.0
adafruit/Adafruit GFX Library @ ^1.12.1
debug_tool = jlink
upload_protocol = nrfutil
[env:muzi_base_duo_superIO_companion_radio_ble]
extends = muzi_base_duo_superIO
board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
board_upload.maximum_size = 708608
build_flags = ${muzi_base_duo_superIO.build_flags}
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
-I examples/companion_radio/ui-new
-D BLE_PIN_CODE=123456
-D QSPIFLASH=1
; -D BLE_DEBUG_LOGGING=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
-D OFFLINE_QUEUE_SIZE=256
build_src_filter = ${muzi_base_duo_superIO.build_src_filter}
+<helpers/nrf52/SerialBLEInterface.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${muzi_base_duo_superIO.lib_deps}
; ===========================================================================
; Uno (SX1262) environments
; ===========================================================================
[env:muzi_base_uno_repeater]
extends = muzi_base_uno
build_flags = ${muzi_base_uno.build_flags}
-I examples/companion_radio/ui-new
-D ADVERT_NAME='"Muzi Base Repeater"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D MAX_NEIGHBOURS=50
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${muzi_base_uno.build_src_filter}
+<../examples/simple_repeater>
lib_deps = ${muzi_base_uno.lib_deps}
[env:muzi_base_uno_room_server]
extends = muzi_base_uno
build_flags = ${muzi_base_uno.build_flags}
-I examples/companion_radio/ui-new
-D ADVERT_NAME='"Muzi Base Room"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D ROOM_PASSWORD='"hello"'
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${muzi_base_uno.build_src_filter}
+<../examples/simple_room_server>
lib_deps = ${muzi_base_uno.lib_deps}
[env:muzi_base_uno_companion_radio_usb]
extends = muzi_base_uno
board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
board_upload.maximum_size = 708608
build_flags = ${muzi_base_uno.build_flags}
-I examples/companion_radio/ui-new
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
-D OFFLINE_QUEUE_SIZE=256
-D DISPLAY_CLASS=NullDisplayDriver
build_src_filter = ${muzi_base_uno.build_src_filter}
+<helpers/ui/NullDisplayDriver.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps = ${muzi_base_uno.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:muzi_base_uno_companion_radio_ble]
extends = muzi_base_uno
board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
board_upload.maximum_size = 708608
build_flags = ${muzi_base_uno.build_flags}
-I examples/companion_radio/ui-new
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
-D BLE_PIN_CODE=123456
-D QSPIFLASH=1
; -D BLE_DEBUG_LOGGING=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
-D OFFLINE_QUEUE_SIZE=256
-D DISPLAY_CLASS=NullDisplayDriver
; -D ADVERT_NAME='"@@MAC"'
build_src_filter = ${muzi_base_uno.build_src_filter}
+<helpers/nrf52/SerialBLEInterface.cpp>
+<helpers/sensors>
+<helpers/ui/NullDisplayDriver.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps = ${muzi_base_uno.lib_deps}
densaugeo/base64 @ ~1.4.0
[muzi_base_uno_superIO]
extends = muzi_base_uno
board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
board_upload.maximum_size = 708608
build_flags = ${muzi_base_uno.build_flags}
-D MUZI_BASE_SUPERIO
-D UI_HAS_JOYSTICK=1
-D OFFLINE_QUEUE_SIZE=256
-D DISPLAY_CLASS=SH1107Display
-D DISPLAY_ROTATION=2
-D ENV_INCLUDE_GPS=1
-D ENV_SKIP_GPS_DETECT
-D PIN_BUZZER=22
build_src_filter = ${muzi_base_uno.build_src_filter}
+<helpers/nrf52/SerialBLEInterface.cpp>
+<helpers/sensors>
+<helpers/ui/SH1107Display.cpp>
+<helpers/ui/buzzer.cpp>
+<helpers/ui/MomentaryButton.cpp>
lib_deps = ${muzi_base_uno.lib_deps}
densaugeo/base64 @ ~1.4.0
stevemarple/MicroNMEA @ ^2.0.6
end2endzone/NonBlockingRTTTL@^1.3.0
adafruit/Adafruit SH110X@^2.1.14
artronshop/ArtronShop_RX8130CE@1.0.0
adafruit/Adafruit GFX Library @ ^1.12.1
debug_tool = jlink
upload_protocol = nrfutil
[env:muzi_base_uno_superIO_companion_radio_ble]
extends = muzi_base_uno_superIO
board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
board_upload.maximum_size = 708608
build_flags = ${muzi_base_uno_superIO.build_flags}
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
-I examples/companion_radio/ui-new
-D BLE_PIN_CODE=123456
-D QSPIFLASH=1
; -D BLE_DEBUG_LOGGING=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
-D OFFLINE_QUEUE_SIZE=256
build_src_filter = ${muzi_base_uno_superIO.build_src_filter}
+<helpers/nrf52/SerialBLEInterface.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${muzi_base_uno_superIO.lib_deps}
+162
View File
@@ -0,0 +1,162 @@
#include <Arduino.h>
#include <nrf_gpio.h>
#include "target.h"
#include "variant.h"
MuziBaseBoard board;
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI);
WRAPPER_CLASS radio_driver(radio, board);
VolatileRTCClock fallback_clock;
AutoDiscoverRTCClock rtc_clock(fallback_clock);
#if ENV_INCLUDE_GPS
#include <helpers/sensors/MicroNMEALocationProvider.h>
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock);
MuziBaseSensorManager sensors = MuziBaseSensorManager(nmea);
#else
MuziBaseSensorManager sensors;
#endif
// user button for the N-click power-off. single-click mode (multiclick off) so
// each press is its own CLICK; we count them ourselves. reliable across the
// idle-sleep loop because it's the same MomentaryButton the UI navigates with.
static MomentaryButton pwr_btn(PIN_USER_BTN, 0, true, true, false);
// power off, arming the user button as a wake source first so a later press
// turns it back on. wait for the button to release (the 5th click already fires
// on release, so this returns quickly), then set nRF52 GPIO SENSE (press = LOW);
// it survives into SYSTEMOFF because shutdownPeripherals() doesn't touch this pin.
static void muziPowerOff() {
uint32_t t0 = millis();
while (digitalRead(PIN_USER_BTN) == USER_BTN_PRESSED && (millis() - t0) < 3000) { delay(5); }
delay(50); // settle
nrf_gpio_cfg_sense_input(g_ADigitalPinMap[PIN_USER_BTN], NRF_GPIO_PIN_PULLUP, NRF_GPIO_PIN_SENSE_LOW);
board.powerOff(); // shutdownPeripherals() + SYSTEMOFF
}
bool MuziBaseSensorManager::begin() {
pwr_btn.begin();
bool ok = EnvironmentSensorManager::begin();
#if ENV_INCLUDE_GPS
pinMode(PIN_GPS_SWITCH, INPUT);
_last_gps_sw = digitalRead(PIN_GPS_SWITCH); // initial gps state from the switch
if (_last_gps_sw == HIGH) start_gps(); else stop_gps();
#endif
return ok;
}
void MuziBaseSensorManager::loop() {
// user button: USER_BTN_POWER_OFF_CLICKS quick clicks -> power off
if (pwr_btn.check() == BUTTON_EVENT_CLICK) {
unsigned long t = millis();
if (t - _pwr_last_click > 2000) _pwr_clicks = 0; // restart if too slow
_pwr_last_click = t;
if (++_pwr_clicks >= USER_BTN_POWER_OFF_CLICKS) muziPowerOff();
}
#if ENV_INCLUDE_GPS
unsigned long now = millis();
if (now > _next_sw_check) { // check the mode switch ~once a sec
_next_sw_check = now + 1000;
int sw = digitalRead(PIN_GPS_SWITCH);
if (sw != _last_gps_sw) {
_last_gps_sw = sw;
if (sw == HIGH) start_gps(); else stop_gps();
}
}
#endif
EnvironmentSensorManager::loop();
}
#ifdef DISPLAY_CLASS
DISPLAY_CLASS display;
MomentaryButton user_btn(PIN_USER_BTN, 1000, true, false, false);
MomentaryButton joystick_left(JOYSTICK_LEFT, 1000, true, false, false);
MomentaryButton joystick_right(JOYSTICK_RIGHT, 1000, true, false, false);
MomentaryButton back_btn(PIN_BACK_BTN, 1000, true, false, true);
#endif
#if defined(USE_LR1121)
#ifndef LORA_CR
#define LORA_CR 5
#endif
#ifdef RF_SWITCH_TABLE
static const uint32_t rfswitch_dios[Module::RFSWITCH_MAX_PINS] = {
RADIOLIB_LR11X0_DIO5,
RADIOLIB_LR11X0_DIO6,
RADIOLIB_NC
};
static const Module::RfSwitchMode_t rfswitch_table[] = {
// mode DIO5 DIO6
{ LR11x0::MODE_STBY, {LOW, LOW}},
{ LR11x0::MODE_RX, {HIGH, LOW}},
{ LR11x0::MODE_TX, {LOW, HIGH}},
{ LR11x0::MODE_TX_HP, {LOW, HIGH}},
{ LR11x0::MODE_TX_HF, {LOW, LOW}},
{ LR11x0::MODE_GNSS, {LOW, LOW}},
{ LR11x0::MODE_WIFI, {LOW, LOW}},
END_OF_MODE_TABLE,
};
#endif
#endif // USE_LR1121
bool radio_init() {
//rtc_clock.begin(Wire);
#if defined(USE_LR1121)
#ifdef LR11X0_DIO3_TCXO_VOLTAGE
float tcxo = LR11X0_DIO3_TCXO_VOLTAGE;
#else
float tcxo = 1.6f;
#endif
SPI.setPins(P_LORA_MISO, P_LORA_SCLK, P_LORA_MOSI);
SPI.begin();
int status = radio.begin(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR, RADIOLIB_LR11X0_LORA_SYNC_WORD_PRIVATE, LORA_TX_POWER, 16, tcxo);
if (status != RADIOLIB_ERR_NONE) {
Serial.print("ERROR: radio init failed: ");
Serial.println(status);
return false; // fail
}
radio.setCRC(2);
radio.explicitHeader();
#ifdef RF_SWITCH_TABLE
radio.setRfSwitchTable(rfswitch_dios, rfswitch_table);
#endif
#ifdef RX_BOOSTED_GAIN
radio.setRxBoostedGainMode(RX_BOOSTED_GAIN);
#endif
return true; // success
#else // USE_SX1262
// CustomSX1262::std_init() configures the SPI pins, runs begin() with the
// TCXO-voltage fallback, sets CRC, current limit, the DIO2 RF switch, and
// RX boosted gain from the SX126X_* build flags.
return radio.std_init(&SPI);
#endif
}
uint32_t radio_get_rng_seed() {
return radio.random(0x7FFFFFFF);
}
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) {
radio.setFrequency(freq);
radio.setSpreadingFactor(sf);
radio.setBandwidth(bw);
radio.setCodingRate(cr);
}
void radio_set_tx_power(int8_t dbm) {
radio.setOutputPower(dbm);
}
mesh::LocalIdentity radio_new_identity() {
RadioNoiseListener rng(radio);
return mesh::LocalIdentity(&rng); // create new random identity
}
+64
View File
@@ -0,0 +1,64 @@
#pragma once
#define RADIOLIB_STATIC_ONLY 1
#include <RadioLib.h>
#include <helpers/radiolib/RadioLibWrappers.h>
#include "MuziBaseBoard.h"
#if defined(USE_LR1121)
#include <helpers/radiolib/CustomLR1121Wrapper.h>
#elif defined(USE_SX1262)
#include <helpers/radiolib/CustomSX1262Wrapper.h>
#else
#error "muzi_base: no radio selected (define USE_LR1121 or USE_SX1262)"
#endif
#include <helpers/ArduinoHelpers.h>
#include <helpers/SensorManager.h>
#include <helpers/sensors/LocationProvider.h>
#include <helpers/AutoDiscoverRTCClock.h>
#include <helpers/sensors/EnvironmentSensorManager.h> // Added: Include for EnvironmentSensorManager
#include <helpers/ui/MomentaryButton.h>
#ifdef MUZI_BASE_SUPERIO
#include <helpers/ui/SH1107Display.h>
extern DISPLAY_CLASS display;
extern MomentaryButton user_btn;
extern MomentaryButton joystick_left;
extern MomentaryButton joystick_right;
extern MomentaryButton back_btn;
#elif defined(DISPLAY_CLASS)
#include "helpers/ui/NullDisplayDriver.h"
extern DISPLAY_CLASS display;
extern MomentaryButton user_btn;
#endif
extern MuziBaseBoard board;
extern WRAPPER_CLASS radio_driver;
extern AutoDiscoverRTCClock rtc_clock;
// muzi sensor manager, polled from main loop via sensors.loop():
// - user button: USER_BTN_POWER_OFF_CLICKS quick presses -> power off (all builds)
// - gps mode switch on/off (superIO only, see thinknode_m1)
class MuziBaseSensorManager : public EnvironmentSensorManager {
unsigned long _pwr_last_click = 0;
uint8_t _pwr_clicks = 0;
#if ENV_INCLUDE_GPS
int _last_gps_sw = -1;
unsigned long _next_sw_check = 0;
#endif
public:
#if ENV_INCLUDE_GPS
MuziBaseSensorManager(LocationProvider& location) : EnvironmentSensorManager(location) {}
#else
MuziBaseSensorManager() {}
#endif
bool begin() override;
void loop() override;
};
extern MuziBaseSensorManager sensors;
bool radio_init();
uint32_t radio_get_rng_seed();
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr);
void radio_set_tx_power(int8_t dbm);
mesh::LocalIdentity radio_new_identity();
+62
View File
@@ -0,0 +1,62 @@
/*
* variant.cpp
* Copyright (C) 2023 Seeed K.K.
* MIT License
*/
#include "variant.h"
#include "wiring_constants.h"
#include "wiring_digital.h"
const uint32_t g_ADigitalPinMap[PINS_COUNT + 1] =
{
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30,
31,
// P1 pins.
32,
33,
34,
35,
36,
37,
38,
39,
40,
41,
42,
43,
44,
45,
46,
47,
};
+169
View File
@@ -0,0 +1,169 @@
/*
* variant.h
* Copyright (C) 2023 Seeed K.K.
* MIT License
*/
#pragma once
#include "WVariant.h"
////////////////////////////////////////////////////////////////////////////////
// Low frequency clock source
#define USE_LFXO // 32.768 kHz crystal oscillator
#define VARIANT_MCK (64000000ul)
// #define USE_LFRC // 32.768 kHz RC oscillator
////////////////////////////////////////////////////////////////////////////////
// Power
#define PIN_VBAT_READ (31) // P0.31
#define BATTERY_SENSE_RESOLUTION_BITS 12
#define BATTERY_SENSE_RESOLUTION 4096.0
#define AREF_VOLTAGE 3.0
#define VBAT_AR_INTERNAL AR_INTERNAL_3_0
#define ADC_MULTIPLIER 1.537
#define ADC_RESOLUTION 14
#define PIN_BATTERY_CHARGING (32+2) // P1.02 STAT2
#define PIN_CHARGER_FAULT (27) // P0.27 STAT1 this pin is disabled on meshtastic.
// BQ25185 has 2 status pins: STAT1 and STAT2. Both are high when not charging. STAT1 high, STAT2 low: charging. Recoverable fault: STAT1 low, STAT2 high. Unrecoverable fault: both low.
// We only need to detect charging vs not charging, but someone else can use the fault pin to log when the battery gets too hot or cold.
// Power management boot protection threshold (millivolts)
#define PWRMGT_VOLTAGE_BOOTLOCK 3100 // Won't boot below this voltage (mV). BB15 battery min voltage is 3v, 3100mV is minimum batt in meshtastic code.
// LPCOMP wake configuration (voltage recovery from SYSTEMOFF)
#define PWRMGT_LPCOMP_AIN 7 // AIN7 = P0.31 = PIN_VBAT_READ
#define PWRMGT_LPCOMP_REFSEL 4 // 5/8 VDD (~3.13-3.44V) was the default on RAK4631. should still apply here.
// Other pins
#define PIN_AREF (-1)
#define SCREEN_12V_ENABLE (23) // SH1107 OLED controller has a pin that needs to be enabled to turn on the screen.
static const uint8_t AREF = (PIN_AREF); // not used
////////////////////////////////////////////////////////////////////////////////
// Number of pins
#define PINS_COUNT (48)
#define NUM_DIGITAL_PINS (48)
#define NUM_ANALOG_INPUTS (6)
#define NUM_ANALOG_OUTPUTS (0)
////////////////////////////////////////////////////////////////////////////////
// UART pin definition
#define PIN_SERIAL1_RX (19) // P0.19 used for GPS RX
#define PIN_SERIAL1_TX (20) // P0.20 used for GPS TX
////////////////////////////////////////////////////////////////////////////////
// I2C pin definition
#define HAS_WIRE (1)
#define WIRE_INTERFACES_COUNT (2)
#define PIN_WIRE1_SDA (4) // P0.4
#define PIN_WIRE1_SCL (6) // P0.6
#define PIN_WIRE_SDA (24) // P0.24 OLED I2C
#define PIN_WIRE_SCL (25) // P0.25 OLED I2C
#define I2C_NO_RESCAN
// #define I2C_NO_RESCAN
// #define HAS_QMA6100P
// #define QMA_6100P_INT_PIN (34) // P1.2
////////////////////////////////////////////////////////////////////////////////
// SPI pin definition
#define SPI_INTERFACES_COUNT (1)
#define PIN_SPI_MISO (32+15) // internally connected to p1.15
#define PIN_SPI_MOSI (32+14) // internally connected to p1.14
#define PIN_SPI_SCK (32+13) // internally connected to p1.13
#define PIN_SPI_NSS (32+12) // internally connected to p1.12
////////////////////////////////////////////////////////////////////////////////
// Builtin LEDs
#define LED_BUILTIN (35)
#define LED_BLUE (-1) // P1.04 turned off, because the blue LED was annoying.
// #define LED_GREEN (35) // P1.03
#define LED_PIN LED_BUILTIN
#define LED_STATE_ON LOW
////////////////////////////////////////////////////////////////////////////////
// Builtin buttons
#define PIN_BUTTON1 (10) // P0.10 Menu / User Button | on superIO, this is in the center of the "D-Pad", but it's also the button on the Uno/Duo.
#define PIN_BUTTON2 (21) // Joystick Up
#define PIN_BUTTON3 (17) // Joystick Down
#define PIN_BUTTON4 (37) // Joystick Left
#define PIN_BUTTON5 (16) // Joystick Right
#define PIN_BUTTON6 (15) // Back / Cancel Button.
#define JOYSTICK_PRESS PIN_BUTTON1
#define JOYSTICK_UP PIN_BUTTON2
#define JOYSTICK_DOWN PIN_BUTTON3
#define JOYSTICK_LEFT PIN_BUTTON4
#define JOYSTICK_RIGHT PIN_BUTTON5
#define PIN_BACK_BTN PIN_BUTTON6
// quick user-button presses that power the device off (per Muzi's docs)
#ifndef USER_BTN_POWER_OFF_CLICKS
#define USER_BTN_POWER_OFF_CLICKS 5
#endif
////////////////////////////////////////////////////////////////////////////////
// LoRa radio
//
// The Muzi Base PCB is populated with one of two radios:
// * Base Uno -> SX1262 (USE_SX1262)
// * Base Duo -> LR1121 (USE_LR1121)
// Both share NSS/SCLK/MISO/MOSI/RESET/BUSY. The only pin difference is the
// radio IRQ (DIO1): the SX1262 routes it to P1.06, the LR1121 to P1.08.
// (see Meshtastic muzi_base variant, which defines both radios on this board)
#define LORA_NSS (PIN_SPI_NSS) // P1.12
#define LORA_RESET (32+10) // P1.10
#define LORA_BUSY (32+11) // P1.11
#define LORA_SCLK (PIN_SPI_SCK) // P1.13
#define LORA_MISO (PIN_SPI_MISO) // P1.15
#define LORA_MOSI (PIN_SPI_MOSI) // P1.14
// only the IRQ pin differs per radio. rf-switch/tcxo defines are in platformio.ini
#if defined(USE_LR1121)
#define LORA_DIO_1 (32+8) // P1.08 LR1121 IRQ/DIO1
#elif defined(USE_SX1262)
#define LORA_DIO_1 (32+6) // P1.06 SX1262 IRQ/DIO1
#else
#error "muzi_base: no radio selected (define USE_LR1121 or USE_SX1262)"
#endif
////////////////////////////////////////////////////////////////////////////////
// QSPI Flash
#define PIN_QSPI_SCK (0 + 3)
#define PIN_QSPI_CS (0 + 26)
#define PIN_QSPI_IO0 (0 + 30)
#define PIN_QSPI_IO1 (0 + 29)
#define PIN_QSPI_IO2 (0 + 28)
#define PIN_QSPI_IO3 (0 + 2)
#define EXTERNAL_FLASH_DEVICES W25Q128JVPQ
#define EXTERNAL_FLASH_USE_QSPI
////////////////////////////////////////////////////////////////////////////////
// GPS
#define HAS_GPS 1
#define PIN_GPS_RX PIN_SERIAL1_RX
#define PIN_GPS_TX PIN_SERIAL1_TX
#define PIN_GPS_EN (32+1) // P1.01 PWR_IO2 on schematic. gps power, driven by the sensor manager.
// superIO 3-position mode switch (pins from the meshtastic muzi_base variant).
// "Mode 2" reads HIGH on PIN_GPS_SWITCH and turns the gps on.
#define SWITCH_MODE1 (32+9) // P1.09
#define SWITCH_MODE2 (12) // P0.12
#define PIN_GPS_SWITCH SWITCH_MODE2
////////////////////////////////////////////////////////////////////////////////
// Buzzer
#define BUZZER_PIN (22) // P0.22 same load switch design as GPS_EN.
@@ -0,0 +1,46 @@
#include <Arduino.h>
#include "ThinkNodeM4Board.h"
#include <Wire.h>
#include <bluefruit.h>
void ThinkNodeM4Board::begin() {
NRF52Board::begin();
btn_prev_state = HIGH;
Wire.begin();
battery_serial->begin(4800);
delay(10); // give sx1262 some time to power up
}
uint16_t ThinkNodeM4Board::getBattMilliVolts() {
int tentatives = 10;
uint8_t data[5];
uint8_t b;
if (battery_serial->available() < 10)
return bat_level_mv;
// discard old data
while (battery_serial->available() > 10)
battery_serial->read();
// synchronize
while((b = battery_serial->read()) != 0xFE) {
if (tentatives-- == 0) {
MESH_DEBUG_PRINTLN("Could not find battery frame start %x", b);
return bat_level_mv;
}
}
battery_serial->readBytes(data, 5);
if (data[4] != 0xFD) {
MESH_DEBUG_PRINTLN("Invalid battery frame end %x", data[4]);
return bat_level_mv;
}
bat_level_percent = data[0];
//MESH_DEBUG_PRINTLN("Battery level %d\%", bat_level_percent);
bat_level_mv = 2 * (data[1]*1000. + data[2]*10. + data[3]/10.);
return bat_level_mv;
}
+75
View File
@@ -0,0 +1,75 @@
#pragma once
#include <Arduino.h>
#include <MeshCore.h>
#include <helpers/NRF52Board.h>
class ThinkNodeM4Board : public NRF52BoardDCDC {
protected:
#if NRF52_POWER_MANAGEMENT
void initiateShutdown(uint8_t reason) override;
#endif
uint8_t btn_prev_state;
HardwareSerial * battery_serial;
uint16_t bat_level_mv = 0;
uint8_t bat_level_percent =0;
public:
ThinkNodeM4Board() : NRF52Board("THINKNODE_M4_OTA"), battery_serial(&Serial2) {}
void begin();
uint16_t getBattMilliVolts() override;
#ifdef P_LORA_TX_LED
void onBeforeTransmit() override {
digitalWrite(P_LORA_TX_LED, LED_STATE_ON); // turn TX LED on
}
void onAfterTransmit() override {
digitalWrite(P_LORA_TX_LED, !LED_STATE_ON); // turn TX LED off
}
#endif
const char* getManufacturerName() const override {
return "Elecrow ThinkNode M4";
}
int buttonStateChanged() {
#ifdef BUTTON_PIN
uint8_t v = digitalRead(BUTTON_PIN);
if (v != btn_prev_state) {
btn_prev_state = v;
return (v == USER_BTN_PRESSED) ? 1 : -1;
}
#endif
return 0;
}
void shutdownPeripherals() override {
// shutdown common peripherals
NRF52Board::shutdownPeripherals();
#ifdef LED_STATUS
digitalWrite(LED_STATUS, HIGH);
#endif
#ifdef BUTTON_PIN
while(!digitalRead(BUTTON_PIN));
#endif
#ifdef LED_STATUS
digitalWrite(LED_STATUS, LOW);
#endif
digitalWrite(LED_BAT1, LOW);
digitalWrite(LED_BAT2, LOW);
digitalWrite(LED_BAT3, LOW);
digitalWrite(LED_BAT4, LOW);
digitalWrite(LED_STATUS, LOW);
digitalWrite(LED_PIN, LOW);
digitalWrite(PIN_PWR_EN, LOW);
digitalWrite(I2C_POWER, !I2C_POWER_ACTIVE);
digitalWrite(PIN_GPS_POWER, !GPS_POWER_ACTIVE);
#ifdef BUTTON_PIN
nrf_gpio_cfg_sense_input(BUTTON_PIN, NRF_GPIO_PIN_NOPULL, NRF_GPIO_PIN_SENSE_LOW);
#endif
}
};
+130
View File
@@ -0,0 +1,130 @@
[ThinkNode_M4]
extends = nrf52_base
board = thinknode_m4
board_build.ldscript = boards/nrf52840_s140_v6.ld
build_flags = ${nrf52_base.build_flags}
-I src/helpers/nrf52
-I lib/nrf52/s140_nrf52_6.1.1_API/include
-I lib/nrf52/s140_nrf52_6.1.1_API/include/nrf52
-I variants/thinknode_m4
-I src/helpers/ui
-D THINKNODE_M4
-D PIN_USER_BTN=4
-D PIN_STATUS_LED=LED_STATUS
-D RADIO_CLASS=CustomLR1110
-D WRAPPER_CLASS=CustomLR1110Wrapper
-D LORA_TX_POWER=22
-D RF_SWITCH_TABLE
-D RX_BOOSTED_GAIN=true
-D P_LORA_BUSY=26
-D P_LORA_SCLK=6
-D P_LORA_NSS=27
-D P_LORA_DIO_1=12
-D P_LORA_MISO=8
-D P_LORA_MOSI=7
-D P_LORA_RESET=40
-D P_LORA_TX_LED=LED_TX
-D LR11X0_DIO_AS_RF_SWITCH=true
-D LR11X0_DIO3_TCXO_VOLTAGE=1.6
-D ENV_INCLUDE_GPS=1
; -D MESH_DEBUG=1
; -D GPS_NMEA_DEBUG=1
build_src_filter = ${nrf52_base.build_src_filter}
+<helpers/*.cpp>
+<../variants/thinknode_m4>
+<helpers/sensors>
debug_tool = stlink
upload_protocol = nrfutil
lib_deps= ${nrf52_base.lib_deps}
[env:ThinkNode_M4_repeater]
extends = ThinkNode_M4
build_flags = ${ThinkNode_M4.build_flags}
-I examples/companion_radio/ui-orig
-D ADVERT_NAME='"ThinkNode_M4 Repeater"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D MAX_NEIGHBOURS=50
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${ThinkNode_M4.build_src_filter}
+<../examples/simple_repeater>
lib_deps = ${ThinkNode_M4.lib_deps}
stevemarple/MicroNMEA @ ^2.0.6
[env:ThinkNode_M4_room_server]
extends = ThinkNode_M4
build_flags = ${ThinkNode_M4.build_flags}
-I examples/companion_radio/ui-orig
-D ADVERT_NAME='"ThinkNode_M4 Room"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D ROOM_PASSWORD='"hello"'
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
-D RF_SWITCH_TABLE
build_src_filter = ${ThinkNode_M4.build_src_filter}
+<../examples/simple_room_server>
lib_deps = ${ThinkNode_M4.lib_deps}
stevemarple/MicroNMEA @ ^2.0.6
[env:ThinkNode_M4_companion_radio_usb]
extends = ThinkNode_M4
board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
board_upload.maximum_size = 708608
build_flags = ${ThinkNode_M4.build_flags}
-I examples/companion_radio/ui-orig
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
-D OFFLINE_QUEUE_SIZE=256
-D DISPLAY_CLASS=NullDisplayDriver
-D PIN_BUZZER=23
-D PIN_BUZZER_EN=36
-D ENABLE_USB_INTERFACE
build_src_filter = ${ThinkNode_M4.build_src_filter}
+<helpers/ui/buzzer.cpp>
+<helpers/ui/NullDisplayDriver.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-orig/*.cpp>
lib_deps = ${ThinkNode_M4.lib_deps}
densaugeo/base64 @ ~1.4.0
stevemarple/MicroNMEA @ ^2.0.6
end2endzone/NonBlockingRTTTL@^1.3.0
[env:ThinkNode_M4_companion_radio_ble]
extends = ThinkNode_M4
board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
board_upload.maximum_size = 708608
build_flags = ${ThinkNode_M4.build_flags}
-I examples/companion_radio/ui-orig
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
-D BLE_PIN_CODE=123456
-D BLE_TX_POWER=0
; -D BLE_DEBUG_LOGGING=1
; -D MESH_PACKET_LOGGING=1
-D OFFLINE_QUEUE_SIZE=256
-D DISPLAY_CLASS=NullDisplayDriver
-D PIN_BUZZER=23
-D PIN_BUZZER_EN=36
build_src_filter = ${ThinkNode_M4.build_src_filter}
+<helpers/nrf52/SerialBLEInterface.cpp>
+<helpers/ui/buzzer.cpp>
+<helpers/ui/NullDisplayDriver.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-orig/*.cpp>
lib_deps = ${ThinkNode_M4.lib_deps}
densaugeo/base64 @ ~1.4.0
stevemarple/MicroNMEA @ ^2.0.6
end2endzone/NonBlockingRTTTL@^1.3.0
[env:ThinkNode_M4_kiss_modem]
extends = ThinkNode_M4
build_src_filter = ${ThinkNode_M4.build_src_filter}
+<../examples/kiss_modem/>
lib_deps = ${ThinkNode_M4.lib_deps}
stevemarple/MicroNMEA @ ^2.0.6
+84
View File
@@ -0,0 +1,84 @@
#include <Arduino.h>
#include "target.h"
#include <helpers/sensors/MicroNMEALocationProvider.h>
ThinkNodeM4Board board;
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI);
WRAPPER_CLASS radio_driver(radio, board);
VolatileRTCClock fallback_clock;
AutoDiscoverRTCClock rtc_clock(fallback_clock);
#ifdef ENV_INCLUDE_GPS
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock);
EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea);
#else
EnvironmentSensorManager sensors = EnvironmentSensorManager();
#endif
#ifdef DISPLAY_CLASS
NullDisplayDriver display;
#endif
#ifndef LORA_CR
#define LORA_CR 5
#endif
#ifdef RF_SWITCH_TABLE
static const uint32_t rfswitch_dios[Module::RFSWITCH_MAX_PINS] = {
RADIOLIB_LR11X0_DIO5,
RADIOLIB_LR11X0_DIO6,
RADIOLIB_NC,
RADIOLIB_NC,
RADIOLIB_NC
};
static const Module::RfSwitchMode_t rfswitch_table[] = {
// mode DIO5 DIO6
{ LR11x0::MODE_STBY, {LOW , LOW }},
{ LR11x0::MODE_RX, {HIGH, LOW }},
{ LR11x0::MODE_TX, {HIGH, HIGH }},
{ LR11x0::MODE_TX_HP, {LOW , HIGH }},
{ LR11x0::MODE_TX_HF, {LOW , LOW }},
{ LR11x0::MODE_GNSS, {LOW , LOW }},
{ LR11x0::MODE_WIFI, {LOW , LOW }},
END_OF_MODE_TABLE,
};
#endif
bool radio_init() {
rtc_clock.begin(Wire);
#ifdef LR11X0_DIO3_TCXO_VOLTAGE
float tcxo = LR11X0_DIO3_TCXO_VOLTAGE;
#else
float tcxo = 1.6f;
#endif
SPI.setPins(P_LORA_MISO, P_LORA_SCLK, P_LORA_MOSI);
SPI.begin();
int status = radio.begin(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR, RADIOLIB_LR11X0_LORA_SYNC_WORD_PRIVATE, LORA_TX_POWER, 16, tcxo);
if (status != RADIOLIB_ERR_NONE) {
Serial.print("ERROR: radio init failed: ");
Serial.println(status);
return false; // fail
}
radio.setCRC(2);
radio.explicitHeader();
#ifdef RF_SWITCH_TABLE
radio.setRfSwitchTable(rfswitch_dios, rfswitch_table);
#endif
#ifdef RX_BOOSTED_GAIN
radio.setRxBoostedGainMode(RX_BOOSTED_GAIN);
#endif
return true; // success
}
mesh::LocalIdentity radio_new_identity() {
RadioNoiseListener rng(radio);
return mesh::LocalIdentity(&rng); // create new random identity
}
+26
View File
@@ -0,0 +1,26 @@
#pragma once
#define RADIOLIB_STATIC_ONLY 1
#include <RadioLib.h>
#include <helpers/radiolib/RadioLibWrappers.h>
#include "ThinkNodeM4Board.h"
#include <helpers/radiolib/CustomLR1110Wrapper.h>
#include <helpers/ArduinoHelpers.h>
#include <helpers/sensors/EnvironmentSensorManager.h>
#include <helpers/sensors/LocationProvider.h>
#include <helpers/AutoDiscoverRTCClock.h>
#ifdef DISPLAY_CLASS
#include "NullDisplayDriver.h"
#endif
#ifdef DISPLAY_CLASS
extern NullDisplayDriver display;
#endif
extern ThinkNodeM4Board board;
extern WRAPPER_CLASS radio_driver;
extern AutoDiscoverRTCClock rtc_clock;
extern EnvironmentSensorManager sensors;
bool radio_init();
mesh::LocalIdentity radio_new_identity();
+94
View File
@@ -0,0 +1,94 @@
/*
* variant.cpp
* Copyright (C) 2023 Seeed K.K.
* MIT License
*/
#include "variant.h"
#include "wiring_constants.h"
#include "wiring_digital.h"
const uint32_t g_ADigitalPinMap[] =
{
0xff, // P0.00 - LFXO (do not use)
0xff, // P0.01 - LFXO (do not use)
2, // P0.02
3, // P0.03
4, // P0.04
5, // P0.05
6, // P0.06
7, // P0.07
8, // P0.08
9, // P0.09
10, // P0.10
11, // P0.11
12, // P0.12
13, // P0.13
14, // P0.14
15, // P0.15
16, // P0.16
17, // P0.17
18, // P0.18
19, // P0.19
20, // P0.20
21, // P0.21
22, // P0.22
23, // P0.23
24, // P0.24
25, // P0.25
26, // P0.26
27, // P0.27
28, // P0.28
29, // P0.29
30, // P0.30
31, // P0.31
32, // P1.00
33, // P1.01
34, // P1.02
35, // P1.03
36, // P1.04
37, // P1.05
38, // P1.06
39, // P1.07
40, // P1.08
41, // P1.09
42, // P1.10
43, // P1.11
44, // P1.12
45, // P1.13
46, // P1.14
47, // P1.15
};
void initVariant()
{
/* TODO */
pinMode(PIN_PWR_EN, OUTPUT);
pinMode(I2C_POWER, OUTPUT);
digitalWrite(PIN_PWR_EN, HIGH);
digitalWrite(I2C_POWER, I2C_POWER_ACTIVE);
pinMode(LED_STATUS, OUTPUT);
pinMode(LED_PIN, OUTPUT);
pinMode(LED_BAT1, OUTPUT);
pinMode(LED_BAT2, OUTPUT);
pinMode(LED_BAT3, OUTPUT);
pinMode(LED_BAT4, OUTPUT);
digitalWrite(LED_BAT1, LOW);
digitalWrite(LED_BAT2, LOW);
digitalWrite(LED_BAT3, LOW);
digitalWrite(LED_BAT4, LOW);
digitalWrite(LED_STATUS, LOW);
digitalWrite(LED_PIN, LOW);
pinMode(BUTTON_PIN, INPUT_PULLUP);
pinMode(PIN_GPS_POWER, OUTPUT);
pinMode(PIN_GPS_EN, OUTPUT);
// Power on gps but in standby
digitalWrite(PIN_GPS_EN, !GPS_EN_ACTIVE);
digitalWrite(PIN_GPS_POWER, GPS_POWER_ACTIVE);
}
+106
View File
@@ -0,0 +1,106 @@
/*
* variant.h
* Copyright (C) 2023 Seeed K.K.
* MIT License
*/
#pragma once
#include "WVariant.h"
////////////////////////////////////////////////////////////////////////////////
// Low frequency clock source
#define USE_LFXO // 32.768 kHz crystal oscillator
#define VARIANT_MCK (64000000ul)
// #define USE_LFRC // 32.768 kHz RC oscillator
////////////////////////////////////////////////////////////////////////////////
// Number of pins
#define PINS_COUNT (48)
#define NUM_DIGITAL_PINS (48)
#define NUM_ANALOG_INPUTS (1)
#define NUM_ANALOG_OUTPUTS (0)
////////////////////////////////////////////////////////////////////////////////
// Power
#define NRF_APM // detect usb power
#define EXT_CHRG_DETECT (38)
// Power to radio
#define PIN_PWR_EN (11)
// I2C bus power
#define I2C_POWER (32)
#define I2C_POWER_ACTIVE LOW
#define PIN_BAT_RX (5)
#define PIN_BAT_TX (30)
////////////////////////////////////////////////////////////////////////////////
// UART pin definition
#define PIN_SERIAL1_RX PIN_GPS_TX
#define PIN_SERIAL1_TX PIN_GPS_RX
#define PIN_SERIAL2_RX PIN_BAT_TX
#define PIN_SERIAL2_TX PIN_BAT_RX
////////////////////////////////////////////////////////////////////////////////
// I2C pin definition
#define HAS_WIRE (1)
#define WIRE_INTERFACES_COUNT (1)
#define PIN_WIRE_SDA (23)
#define PIN_WIRE_SCL (25)
#define I2C_NO_RESCAN
////////////////////////////////////////////////////////////////////////////////
// SPI pin definition
#define SPI_INTERFACES_COUNT (1)
#define PIN_SPI_MISO (8)
#define PIN_SPI_MOSI (7)
#define PIN_SPI_SCK (6)
#define PIN_SPI_NSS (27)
////////////////////////////////////////////////////////////////////////////////
// Builtin LEDs
#define LED_BLUE (-1) // disable blue led
#define LED_STATUS (13) // blue
#define LED_PIN (41) // red
#define LED_TX LED_PIN
#define LED_BUILTIN LED_BLUE
#define LED_STATE_ON HIGH
#define LED_BAT1 (15)
#define LED_BAT2 (17)
#define LED_BAT3 (34)
#define LED_BAT4 (36)
////////////////////////////////////////////////////////////////////////////////
// Builtin buttons
#define PIN_BUTTON1 (4)
#define BUTTON_PIN PIN_BUTTON1
#define USER_BTN_PRESSED LOW
////////////////////////////////////////////////////////////////////////////////
// GPS
#define HAS_GPS 1
#define GPS_BAUDRATE 9600
#define PIN_GPS_RX (44)
#define PIN_GPS_TX (46)
#define PIN_GPS_POWER (14)
#define GPS_POWER_ACTIVE LOW
#define PIN_GPS_EN (43)
#define GPS_EN_ACTIVE LOW
#define PIN_GPS_RESET (3)
#define GPS_RESET_ACTIVE HIGH
+4
View File
@@ -99,11 +99,15 @@ build_flags =
-D MAX_GROUP_CHANNELS=40
-D BLE_PIN_CODE=123456
-D OFFLINE_QUEUE_SIZE=512
-D ENABLE_WIFI_INTERFACE
-D WIFI_SSID='""'
-D WIFI_PWD='""'
; -D BLE_DEBUG_LOGGING=1
; -D MESH_PACKET_LOGGING=1
build_src_filter = ${ThinkNode_M7.build_src_filter}
${ThinkNode_M7_ethernet.build_src_filter}
+<helpers/esp32/*.cpp>
+<helpers/wifi/*.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<helpers/ui/NullDisplayDriver.cpp>
+<../examples/companion_radio/*.cpp>