Merge branch 'dev'

This commit is contained in:
Scott Powell
2025-08-31 23:43:33 +10:00
174 changed files with 5171 additions and 605 deletions

5
.gitignore vendored
View File

@@ -9,3 +9,8 @@ out/
.DS_Store
.vscode/settings.json
.vscode/extensions.json
.idea
cmake-*
.cache
.ccls
compile_commands.json

View File

@@ -0,0 +1,61 @@
{
"build": {
"arduino": {
"ldscript": "nrf52840_s140_v6.ld"
},
"core": "nRF5",
"cpu": "cortex-m4",
"extra_flags": "-DARDUINO_NRF52840_FEATHER -DNRF52840_XXAA",
"f_cpu": "64000000L",
"hwids": [
["0x239A","0x8029"],
["0x239A","0x0029"],
["0x239A","0x002A"],
["0x239A","0x802A"]
],
"usb_product": "HT-n5262",
"mcu": "nrf52840",
"variant": "heltec_mesh_solar",
"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",
"svd_path": "nrf52840.svd",
"openocd_target": "nrf52.cfg"
},
"frameworks": [
"arduino"
],
"name": "Heltec Mesh Solar Board",
"upload": {
"maximum_ram_size": 248832,
"maximum_size": 815104,
"speed": 115200,
"protocol": "nrfutil",
"protocols": [
"jlink",
"nrfjprog",
"nrfutil",
"stlink"
],
"use_1200bps_touch": true,
"require_upload_port": true,
"wait_for_upload_port": true
},
"url": "https://heltec.org/",
"vendor": "Heltec"
}

View File

@@ -0,0 +1,44 @@
{
"build": {
"arduino": {
"ldscript": "esp32s3_out.ld",
"partitions": "default_16MB.csv",
"memory_type": "qio_opi"
},
"core": "esp32",
"extra_flags": [
"-DBOARD_HAS_PSRAM",
"-DARDUINO_USB_MODE=0",
"-DARDUINO_USB_CDC_ON_BOOT=1",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1"
],
"f_cpu": "240000000L",
"f_flash": "80000000L",
"flash_mode": "qio",
"psram_type": "opi",
"hwids": [
["0x303A", "0x1001"],
["0x303A", "0x0002"]
],
"mcu": "esp32s3",
"variant": "heltec_vision_master_e213"
},
"connectivity": ["wifi", "bluetooth", "lora"],
"debug": {
"openocd_target": "esp32s3.cfg"
},
"frameworks": ["arduino", "espidf"],
"name": "Heltec Vision Master E213",
"upload": {
"flash_size": "16MB",
"maximum_ram_size": 8388608,
"maximum_size": 16777216,
"use_1200bps_touch": true,
"wait_for_upload_port": true,
"require_upload_port": true,
"speed": 921600
},
"url": "https://heltec.org/project/vision-master-e213/",
"vendor": "Heltec"
}

View File

@@ -0,0 +1,44 @@
{
"build": {
"arduino": {
"ldscript": "esp32s3_out.ld",
"partitions": "default_16MB.csv",
"memory_type": "qio_opi"
},
"core": "esp32",
"extra_flags": [
"-DBOARD_HAS_PSRAM",
"-DARDUINO_USB_MODE=0",
"-DARDUINO_USB_CDC_ON_BOOT=1",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1"
],
"f_cpu": "240000000L",
"f_flash": "80000000L",
"flash_mode": "qio",
"psram_type": "opi",
"hwids": [
["0x303A", "0x1001"],
["0x303A", "0x0002"]
],
"mcu": "esp32s3",
"variant": "heltec_vision_master_e290"
},
"connectivity": ["wifi", "bluetooth", "lora"],
"debug": {
"openocd_target": "esp32s3.cfg"
},
"frameworks": ["arduino", "espidf"],
"name": "Heltec Vision Master E290",
"upload": {
"flash_size": "16MB",
"maximum_ram_size": 8388608,
"maximum_size": 16777216,
"use_1200bps_touch": true,
"wait_for_upload_port": true,
"require_upload_port": true,
"speed": 921600
},
"url": "https://heltec.org/project/vision-master-e290/",
"vendor": "Heltec"
}

View File

@@ -0,0 +1,44 @@
{
"build": {
"arduino": {
"ldscript": "esp32s3_out.ld",
"partitions": "default_16MB.csv",
"memory_type": "qio_opi"
},
"core": "esp32",
"extra_flags": [
"-DBOARD_HAS_PSRAM",
"-DARDUINO_USB_MODE=0",
"-DARDUINO_USB_CDC_ON_BOOT=1",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1"
],
"f_cpu": "240000000L",
"f_flash": "80000000L",
"flash_mode": "qio",
"psram_type": "opi",
"hwids": [
["0x303A", "0x1001"],
["0x303A", "0x0002"]
],
"mcu": "esp32s3",
"variant": "heltec_vision_master_t190"
},
"connectivity": ["wifi", "bluetooth", "lora"],
"debug": {
"openocd_target": "esp32s3.cfg"
},
"frameworks": ["arduino", "espidf"],
"name": "Heltec Vision Master T190",
"upload": {
"flash_size": "16MB",
"maximum_ram_size": 8388608,
"maximum_size": 16777216,
"use_1200bps_touch": true,
"wait_for_upload_port": true,
"require_upload_port": true,
"speed": 921600
},
"url": "https://heltec.org/project/vision-master-t190/",
"vendor": "Heltec"
}

View File

@@ -40,8 +40,8 @@
],
"name": "Seeed Wio Tracker L1",
"upload": {
"maximum_ram_size": 248832,
"maximum_size": 815104,
"maximum_ram_size": 237568,
"maximum_size": 811008,
"protocol": "nrfutil",
"speed": 115200,
"protocols": [

View File

@@ -3,6 +3,7 @@
# usage
# sh build.sh build-firmware RAK_4631_Repeater
# sh build.sh build-firmwares
# sh build.sh build-matching-firmwares RAK_4631
# sh build.sh build-companion-firmwares
# sh build.sh build-repeater-firmwares
# sh build.sh build-room-server-firmwares
@@ -144,6 +145,16 @@ mkdir -p out
if [[ $1 == "build-firmware" ]]; then
if [ "$2" ]; then
build_firmware $2
else
echo "usage: $0 build-firmware <target>"
exit 1
fi
elif [[ $1 == "build-matching-firmwares" ]]; then
if [ "$2" ]; then
build_all_firmwares_matching $2
else
echo "usage: $0 build-matching-firmwares <build-match-spec>"
exit 1
fi
elif [[ $1 == "build-firmwares" ]]; then
build_firmwares

64
build_as_lib.py Normal file
View File

@@ -0,0 +1,64 @@
from os.path import realpath
Import("env") # type: ignore
menv=env # type: ignore
src_filter = [
'+<*.cpp>',
'+<helpers/*.cpp>',
'+<helpers/sensors>',
'+<helpers/radiolib/*.cpp>',
'+<helpers/ui/MomentaryButton.cpp>',
'+<helpers/ui/buzzer.cpp>',
]
# add build and include dirs according to CPPDEFINES
for item in menv.get("CPPDEFINES", []):
# PLATFORM HANDLING
if item == "STM32_PLATFORM":
src_filter.append("+<helpers/stm32/*>")
elif item == "ESP32":
src_filter.append("+<helpers/esp32/*>")
elif item == "NRF52_PLATFORM":
src_filter.append("+<helpers/nrf52/*>")
elif item == "RP2040_PLATFORM":
src_filter.append("+<helpers/rp2040/*>")
# DISPLAY HANDLING
elif isinstance(item, tuple) and item[0] == "DISPLAY_CLASS":
display_class = item[1]
src_filter.append(f"+<helpers/ui/{display_class}.cpp>")
if (display_class == "ST7789Display") :
src_filter.append(f"+<helpers/ui/OLEDDisplay.cpp>")
src_filter.append(f"+<helpers/ui/OLEDDisplayFonts.cpp>")
# VARIANTS HANDLING
elif isinstance(item, tuple) and item[0] == "MC_VARIANT":
variant_name = item[1]
src_filter.append(f"+<../variants/{variant_name}>")
# INCLUDE EXAMPLE CODE IN BUILD (to provide your own support files without touching the tree)
elif isinstance(item, tuple) and item[0] == "BUILD_EXAMPLE":
example_name = item[1]
src_filter.append(f"+<../examples/{example_name}/*.cpp>")
# EXCLUDE A SOURCE FILE FROM AN EXAMPLE (must be placed after example name or boom)
elif isinstance(item, tuple) and item[0] == "EXCLUDE_FROM_EXAMPLE":
exclude_name = item[1]
if example_name is None:
print("***** PLEASE DEFINE EXAMPLE FIRST *****")
break
src_filter.append(f"-<../examples/{example_name}/{exclude_name}>")
# DEAL WITH UI VARIANT FOR AN EXAMPLE
elif isinstance(item, tuple) and item[0] == "MC_UI_FLAVOR":
ui_flavor = item[1]
if example_name is None:
print("***** PLEASE DEFINE EXAMPLE FIRST *****")
break
src_filter.append(f"+<../examples/{example_name}/{ui_flavor}/*.cpp>")
menv.Replace(SRC_FILTER=src_filter)
#print (menv.Dump())

View File

@@ -0,0 +1,46 @@
#pragma once
#include <MeshCore.h>
#include <helpers/ui/DisplayDriver.h>
#include <helpers/ui/UIScreen.h>
#include <helpers/SensorManager.h>
#include <helpers/BaseSerialInterface.h>
#include <Arduino.h>
#ifdef PIN_BUZZER
#include <helpers/ui/buzzer.h>
#endif
#include "NodePrefs.h"
enum class UIEventType {
none,
contactMessage,
channelMessage,
roomMessage,
newContactMessage,
ack
};
class AbstractUITask {
protected:
mesh::MainBoard* _board;
BaseSerialInterface* _serial;
bool _connected;
AbstractUITask(mesh::MainBoard* board, BaseSerialInterface* serial) : _board(board), _serial(serial) {
_connected = false;
}
public:
void setHasConnection(bool connected) { _connected = connected; }
bool hasConnection() const { return _connected; }
uint16_t getBattMilliVolts() const { return _board->getBattMilliVolts(); }
bool isSerialEnabled() const { return _serial->isEnabled(); }
void enableSerial() { _serial->enable(); }
void disableSerial() { _serial->disable(); }
virtual void msgRead(int msgcount) = 0;
virtual void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) = 0;
virtual void soundBuzzer(UIEventType bet = UIEventType::none) = 0;
virtual void loop() = 0;
};

View File

@@ -41,7 +41,7 @@
#define CMD_SEND_TRACE_PATH 36
#define CMD_SET_DEVICE_PIN 37
#define CMD_SET_OTHER_PARAMS 38
#define CMD_SEND_TELEMETRY_REQ 39
#define CMD_SEND_TELEMETRY_REQ 39 // can deprecate this
#define CMD_GET_CUSTOM_VARS 40
#define CMD_SET_CUSTOM_VAR 41
#define CMD_GET_ADVERT_PATH 42
@@ -49,6 +49,7 @@
// NOTE: CMD range 44..49 parked, potentially for WiFi operations
#define CMD_SEND_BINARY_REQ 50
#define CMD_FACTORY_RESET 51
#define CMD_SEND_PATH_DISCOVERY_REQ 52
#define RESP_CODE_OK 0
#define RESP_CODE_ERR 1
@@ -97,6 +98,7 @@
#define PUSH_CODE_NEW_ADVERT 0x8A
#define PUSH_CODE_TELEMETRY_RESPONSE 0x8B
#define PUSH_CODE_BINARY_RESPONSE 0x8C
#define PUSH_CODE_PATH_DISCOVERY_RESPONSE 0x8D
#define ERR_CODE_UNSUPPORTED_CMD 1
#define ERR_CODE_NOT_FOUND 2
@@ -107,10 +109,6 @@
#define MAX_SIGN_DATA_LEN (8 * 1024) // 8K
#ifdef DISPLAY_CLASS
#include "UITask.h"
#endif
void MyMesh::writeOKFrame() {
uint8_t buf[1];
buf[0] = RESP_CODE_OK;
@@ -152,7 +150,7 @@ void MyMesh::writeContactRespFrame(uint8_t code, const ContactInfo &contact) {
_serial->writeFrame(out_frame, i);
}
void MyMesh::updateContactFromFrame(ContactInfo &contact, const uint8_t *frame, int len) {
void MyMesh::updateContactFromFrame(ContactInfo &contact, uint32_t& last_mod, const uint8_t *frame, int len) {
int i = 0;
uint8_t code = frame[i++]; // eg. CMD_ADD_UPDATE_CONTACT
memcpy(contact.id.pub_key, &frame[i], PUB_KEY_SIZE);
@@ -166,11 +164,14 @@ void MyMesh::updateContactFromFrame(ContactInfo &contact, const uint8_t *frame,
i += 32;
memcpy(&contact.last_advert_timestamp, &frame[i], 4);
i += 4;
if (i + 8 >= len) { // optional fields
if (len >= i + 8) { // optional fields
memcpy(&contact.gps_lat, &frame[i], 4);
i += 4;
memcpy(&contact.gps_lon, &frame[i], 4);
i += 4;
if (len >= i + 4) {
memcpy(&last_mod, &frame[i], 4);
}
}
}
@@ -242,7 +243,7 @@ void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path
}
} else {
#ifdef DISPLAY_CLASS
ui_task.soundBuzzer(UIEventType::newContactMessage);
if (_ui) _ui->soundBuzzer(UIEventType::newContactMessage);
#endif
}
@@ -262,6 +263,7 @@ void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path
}
memcpy(p->pubkey_prefix, contact.id.pub_key, sizeof(p->pubkey_prefix));
strcpy(p->name, contact.name);
p->recv_timestamp = getRTCClock()->getCurrentTime();
p->path_len = path_len;
memcpy(p->path, path, p->path_len);
@@ -270,6 +272,20 @@ void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
}
static int sort_by_recent(const void *a, const void *b) {
return ((AdvertPath *) b)->recv_timestamp - ((AdvertPath *) a)->recv_timestamp;
}
int MyMesh::getRecentlyHeard(AdvertPath dest[], int max_num) {
if (max_num > ADVERT_PATH_TABLE_SIZE) max_num = ADVERT_PATH_TABLE_SIZE;
qsort(advert_paths, ADVERT_PATH_TABLE_SIZE, sizeof(advert_paths[0]), sort_by_recent);
for (int i = 0; i < max_num; i++) {
dest[i] = advert_paths[i];
}
return max_num;
}
void MyMesh::onContactPathUpdated(const ContactInfo &contact) {
out_frame[0] = PUSH_CODE_PATH_UPDATED;
memcpy(&out_frame[1], contact.id.pub_key, PUB_KEY_SIZE);
@@ -334,10 +350,10 @@ void MyMesh::queueMessage(const ContactInfo &from, uint8_t txt_type, mesh::Packe
#ifdef DISPLAY_CLASS
// we only want to show text messages on display, not cli data
bool should_display = txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_SIGNED_PLAIN;
if (should_display) {
ui_task.newMsg(path_len, from.name, text, offline_queue_len);
if (should_display && _ui) {
_ui->newMsg(path_len, from.name, text, offline_queue_len);
if (!_serial->isConnected()) {
ui_task.soundBuzzer(UIEventType::contactMessage);
_ui->soundBuzzer(UIEventType::contactMessage);
}
}
#endif
@@ -396,7 +412,7 @@ void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packe
_serial->writeFrame(frame, 1);
} else {
#ifdef DISPLAY_CLASS
ui_task.soundBuzzer(UIEventType::channelMessage);
if (_ui) _ui->soundBuzzer(UIEventType::channelMessage);
#endif
}
#ifdef DISPLAY_CLASS
@@ -406,7 +422,7 @@ void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packe
if (getChannel(channel_idx, channel_details)) {
channel_name = channel_details.name;
}
ui_task.newMsg(path_len, channel_name, text, offline_queue_len);
if (_ui) _ui->newMsg(path_len, channel_name, text, offline_queue_len);
#endif
}
@@ -434,6 +450,9 @@ uint8_t MyMesh::onContactRequest(const ContactInfo &contact, uint32_t sender_tim
permissions |= cp & TELEM_PERM_ENVIRONMENT;
}
uint8_t perm_mask = ~(data[1]); // NEW: first reserved byte (of 4), is now inverse mask to apply to permissions
permissions &= perm_mask;
if (permissions & TELEM_PERM_BASE) { // only respond if base permission bit is set
telemetry.reset();
telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f);
@@ -524,6 +543,39 @@ void MyMesh::onContactResponse(const ContactInfo &contact, const uint8_t *data,
}
}
bool MyMesh::onContactPathRecv(ContactInfo& contact, uint8_t* in_path, uint8_t in_path_len, uint8_t* out_path, uint8_t out_path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) {
if (extra_type == PAYLOAD_TYPE_RESPONSE && extra_len > 4) {
uint32_t tag;
memcpy(&tag, extra, 4);
if (tag == pending_discovery) { // check for matching response tag)
pending_discovery = 0;
if (in_path_len > MAX_PATH_SIZE || out_path_len > MAX_PATH_SIZE) {
MESH_DEBUG_PRINTLN("onContactPathRecv, invalid path sizes: %d, %d", in_path_len, out_path_len);
} else {
int i = 0;
out_frame[i++] = PUSH_CODE_PATH_DISCOVERY_RESPONSE;
out_frame[i++] = 0; // reserved
memcpy(&out_frame[i], contact.id.pub_key, 6);
i += 6; // pub_key_prefix
out_frame[i++] = out_path_len;
memcpy(&out_frame[i], out_path, out_path_len);
i += out_path_len;
out_frame[i++] = in_path_len;
memcpy(&out_frame[i], in_path, in_path_len);
i += in_path_len;
// NOTE: telemetry data in 'extra' is discarded at present
_serial->writeFrame(out_frame, i);
}
return false; // DON'T send reciprocal path!
}
}
// let base class handle received path and data
return BaseChatMesh::onContactPathRecv(contact, in_path, in_path_len, out_path, out_path_len, extra_type, extra, extra_len);
}
void MyMesh::onRawDataRecv(mesh::Packet *packet) {
if (packet->payload_len + 4 > sizeof(out_frame)) {
MESH_DEBUG_PRINTLN("onRawDataRecv(), payload_len too long: %d", packet->payload_len);
@@ -579,14 +631,14 @@ uint32_t MyMesh::calcDirectTimeoutMillisFor(uint32_t pkt_airtime_millis, uint8_t
void MyMesh::onSendTimeout() {}
MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store)
MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store, AbstractUITask* ui)
: BaseChatMesh(radio, *new ArduinoMillis(), rng, rtc, *new StaticPoolPacketManager(16), tables),
_serial(NULL), telemetry(MAX_PACKET_PAYLOAD - 4), _store(&store) {
_serial(NULL), telemetry(MAX_PACKET_PAYLOAD - 4), _store(&store), _ui(ui) {
_iter_started = false;
_cli_rescue = false;
offline_queue_len = 0;
app_target_ver = 0;
pending_login = pending_status = pending_telemetry = pending_req = 0;
clearPendingReqs();
next_ack_idx = 0;
sign_data = NULL;
dirty_contacts_expiry = 0;
@@ -658,6 +710,7 @@ void MyMesh::begin(bool has_display) {
_active_ble_pin = 0;
#endif
resetContacts();
_store->loadContacts(this);
addChannel("Public", PUBLIC_GROUP_PSK); // pre-configure Andy's public channel
_store->loadChannels(this);
@@ -896,15 +949,16 @@ void MyMesh::handleCmdFrame(size_t len) {
} else if (cmd_frame[0] == CMD_ADD_UPDATE_CONTACT && len >= 1 + 32 + 2 + 1) {
uint8_t *pub_key = &cmd_frame[1];
ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
uint32_t last_mod = getRTCClock()->getCurrentTime(); // fallback value if not present in cmd_frame
if (recipient) {
updateContactFromFrame(*recipient, cmd_frame, len);
// recipient->lastmod = ?? shouldn't be needed, app already has this version of contact
updateContactFromFrame(*recipient, last_mod, cmd_frame, len);
recipient->lastmod = last_mod;
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
writeOKFrame();
} else {
ContactInfo contact;
updateContactFromFrame(contact, cmd_frame, len);
contact.lastmod = getRTCClock()->getCurrentTime();
updateContactFromFrame(contact, last_mod, cmd_frame, len);
contact.lastmod = last_mod;
contact.sync_since = 0;
if (addContact(contact)) {
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
@@ -983,7 +1037,7 @@ void MyMesh::handleCmdFrame(size_t len) {
if ((out_len = getFromOfflineQueue(out_frame)) > 0) {
_serial->writeFrame(out_frame, out_len);
#ifdef DISPLAY_CLASS
ui_task.msgRead(offline_queue_len);
if (_ui) _ui->msgRead(offline_queue_len);
#endif
} else {
out_frame[0] = RESP_CODE_NO_MORE_MESSAGES;
@@ -1093,6 +1147,9 @@ void MyMesh::handleCmdFrame(size_t len) {
if (_store->saveMainIdentity(identity)) {
self_id = identity;
writeOKFrame();
// re-load contacts, to recalc shared secrets
resetContacts();
_store->loadContacts(this);
} else {
writeErrFrame(ERR_CODE_FILE_IO_ERROR);
}
@@ -1126,7 +1183,7 @@ void MyMesh::handleCmdFrame(size_t len) {
if (result == MSG_SEND_FAILED) {
writeErrFrame(ERR_CODE_TABLE_FULL);
} else {
pending_req = pending_telemetry = pending_status = 0;
clearPendingReqs();
memcpy(&pending_login, recipient->id.pub_key, 4); // match this to onContactResponse()
out_frame[0] = RESP_CODE_SENT;
out_frame[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0;
@@ -1146,7 +1203,7 @@ void MyMesh::handleCmdFrame(size_t len) {
if (result == MSG_SEND_FAILED) {
writeErrFrame(ERR_CODE_TABLE_FULL);
} else {
pending_req = pending_telemetry = pending_login = 0;
clearPendingReqs();
// FUTURE: pending_status = tag; // match this in onContactResponse()
memcpy(&pending_status, recipient->id.pub_key, 4); // legacy matching scheme
out_frame[0] = RESP_CODE_SENT;
@@ -1158,6 +1215,35 @@ void MyMesh::handleCmdFrame(size_t len) {
} else {
writeErrFrame(ERR_CODE_NOT_FOUND); // contact not found
}
} else if (cmd_frame[0] == CMD_SEND_PATH_DISCOVERY_REQ && cmd_frame[1] == 0 && len >= 2 + PUB_KEY_SIZE) {
uint8_t *pub_key = &cmd_frame[2];
ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
if (recipient) {
uint32_t tag, est_timeout;
// 'Path Discovery' is just a special case of flood + Telemetry req
uint8_t req_data[9];
req_data[0] = REQ_TYPE_GET_TELEMETRY_DATA;
req_data[1] = ~(TELEM_PERM_BASE); // NEW: inverse permissions mask (ie. we only want BASE telemetry)
memset(&req_data[2], 0, 3); // reserved
getRNG()->random(&req_data[5], 4); // random blob to help make packet-hash unique
auto save = recipient->out_path_len; // temporarily force sendRequest() to flood
recipient->out_path_len = -1;
int result = sendRequest(*recipient, req_data, sizeof(req_data), tag, est_timeout);
recipient->out_path_len = save;
if (result == MSG_SEND_FAILED) {
writeErrFrame(ERR_CODE_TABLE_FULL);
} else {
clearPendingReqs();
pending_discovery = tag; // match this in onContactResponse()
out_frame[0] = RESP_CODE_SENT;
out_frame[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0;
memcpy(&out_frame[2], &tag, 4);
memcpy(&out_frame[6], &est_timeout, 4);
_serial->writeFrame(out_frame, 10);
}
} else {
writeErrFrame(ERR_CODE_NOT_FOUND); // contact not found
}
} else if (cmd_frame[0] == CMD_SEND_TELEMETRY_REQ && len >= 4 + PUB_KEY_SIZE) { // can deprecate, in favour of CMD_SEND_BINARY_REQ
uint8_t *pub_key = &cmd_frame[4];
ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
@@ -1167,7 +1253,7 @@ void MyMesh::handleCmdFrame(size_t len) {
if (result == MSG_SEND_FAILED) {
writeErrFrame(ERR_CODE_TABLE_FULL);
} else {
pending_status = pending_login = pending_req = 0;
clearPendingReqs();
pending_telemetry = tag; // match this in onContactResponse()
out_frame[0] = RESP_CODE_SENT;
out_frame[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0;
@@ -1203,7 +1289,7 @@ void MyMesh::handleCmdFrame(size_t len) {
if (result == MSG_SEND_FAILED) {
writeErrFrame(ERR_CODE_TABLE_FULL);
} else {
pending_status = pending_login = pending_telemetry = 0;
clearPendingReqs();
pending_req = tag; // match this in onContactResponse()
out_frame[0] = RESP_CODE_SENT;
out_frame[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0;
@@ -1553,7 +1639,7 @@ void MyMesh::loop() {
}
#ifdef DISPLAY_CLASS
ui_task.setHasConnection(_serial->isConnected());
if (_ui) _ui->setHasConnection(_serial->isConnected());
#endif
}

View File

@@ -2,19 +2,17 @@
#include <Arduino.h>
#include <Mesh.h>
#ifdef DISPLAY_CLASS
#include "UITask.h"
#endif
#include "AbstractUITask.h"
/*------------ Frame Protocol --------------*/
#define FIRMWARE_VER_CODE 7
#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "24 Jul 2025"
#define FIRMWARE_BUILD_DATE "31 Aug 2025"
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.7.4"
#define FIRMWARE_VERSION "v1.8.0"
#endif
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
@@ -77,9 +75,17 @@
#define REQ_TYPE_KEEP_ALIVE 0x02
#define REQ_TYPE_GET_TELEMETRY_DATA 0x03
struct AdvertPath {
uint8_t pubkey_prefix[7];
uint8_t path_len;
char name[32];
uint32_t recv_timestamp;
uint8_t path[MAX_PATH_SIZE];
};
class MyMesh : public BaseChatMesh, public DataStoreHost {
public:
MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store);
MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store, AbstractUITask* ui=NULL);
void begin(bool has_display);
void startInterface(BaseSerialInterface &serial);
@@ -93,6 +99,8 @@ public:
bool advert();
void enterCLIRescue();
int getRecentlyHeard(AdvertPath dest[], int max_num);
protected:
float getAirtimeBudgetFactor() const override;
int getInterferenceThreshold() const override;
@@ -101,6 +109,7 @@ protected:
void logRxRaw(float snr, float rssi, const uint8_t raw[], int len) override;
bool isAutoAddEnabled() const override;
bool onContactPathRecv(ContactInfo& from, uint8_t* in_path, uint8_t in_path_len, uint8_t* out_path, uint8_t out_path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override;
void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) override;
void onContactPathUpdated(const ContactInfo &contact) override;
bool processAck(const uint8_t *data) override;
@@ -133,12 +142,16 @@ protected:
bool onChannelLoaded(uint8_t channel_idx, const ChannelDetails& ch) override { return setChannel(channel_idx, ch); }
bool getChannelForSave(uint8_t channel_idx, ChannelDetails& ch) override { return getChannel(channel_idx, ch); }
void clearPendingReqs() {
pending_login = pending_status = pending_telemetry = pending_discovery = pending_req = 0;
}
private:
void writeOKFrame();
void writeErrFrame(uint8_t err_code);
void writeDisabledFrame();
void writeContactRespFrame(uint8_t code, const ContactInfo &contact);
void updateContactFromFrame(ContactInfo &contact, const uint8_t *frame, int len);
void updateContactFromFrame(ContactInfo &contact, uint32_t& last_mod, const uint8_t *frame, int len);
void addToOfflineQueue(const uint8_t frame[], int len);
int getFromOfflineQueue(uint8_t frame[]);
int getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]) override {
@@ -161,9 +174,10 @@ private:
NodePrefs _prefs;
uint32_t pending_login;
uint32_t pending_status;
uint32_t pending_telemetry; // pending _TELEMETRY_REQ
uint32_t pending_telemetry, pending_discovery; // pending _TELEMETRY_REQ
uint32_t pending_req; // pending _BINARY_REQ
BaseSerialInterface *_serial;
AbstractUITask* _ui;
ContactsIterator _iter;
uint32_t _iter_filter_since;
@@ -196,17 +210,8 @@ private:
AckTableEntry expected_ack_table[EXPECTED_ACK_TABLE_SIZE]; // circular table
int next_ack_idx;
struct AdvertPath {
uint8_t pubkey_prefix[7];
uint8_t path_len;
uint32_t recv_timestamp;
uint8_t path[MAX_PATH_SIZE];
};
#define ADVERT_PATH_TABLE_SIZE 16
AdvertPath advert_paths[ADVERT_PATH_TABLE_SIZE]; // circular table
};
extern MyMesh the_mesh;
#ifdef DISPLAY_CLASS
extern UITask ui_task;
#endif

View File

@@ -75,14 +75,19 @@ static uint32_t _atoi(const char* sp) {
#endif
/* GLOBAL OBJECTS */
StdRNG fast_rng;
SimpleMeshTables tables;
MyMesh the_mesh(radio_driver, fast_rng, rtc_clock, tables, store);
#ifdef DISPLAY_CLASS
#include "UITask.h"
UITask ui_task(&board);
UITask ui_task(&board, &serial_interface);
#endif
StdRNG fast_rng;
SimpleMeshTables tables;
MyMesh the_mesh(radio_driver, fast_rng, rtc_clock, tables, store
#ifdef DISPLAY_CLASS
, &ui_task
#endif
);
/* END GLOBAL OBJECTS */
void halt() {
@@ -99,7 +104,10 @@ void setup() {
if (display.begin()) {
disp = &display;
disp->startFrame();
disp->print("Please wait...");
#ifdef ST7789
disp->setTextSize(2);
#endif
disp->drawTextCentered(disp->width() / 2, 28, "Loading...");
disp->endFrame();
}
#endif

View File

@@ -0,0 +1,612 @@
#include "UITask.h"
#include <helpers/TxtDataHelpers.h>
#include "../MyMesh.h"
#include "target.h"
#define AUTO_OFF_MILLIS 15000 // 15 seconds
#define BOOT_SCREEN_MILLIS 3000 // 3 seconds
#ifdef PIN_STATUS_LED
#define LED_ON_MILLIS 20
#define LED_ON_MSG_MILLIS 200
#define LED_CYCLE_MILLIS 4000
#endif
#define LONG_PRESS_MILLIS 1200
#ifndef UI_RECENT_LIST_SIZE
#define UI_RECENT_LIST_SIZE 4
#endif
#define PRESS_LABEL "long press"
#include "icons.h"
class SplashScreen : public UIScreen {
UITask* _task;
unsigned long dismiss_after;
char _version_info[12];
public:
SplashScreen(UITask* task) : _task(task) {
// strip off dash and commit hash by changing dash to null terminator
// e.g: v1.2.3-abcdef -> v1.2.3
const char *ver = FIRMWARE_VERSION;
const char *dash = strchr(ver, '-');
int len = dash ? dash - ver : strlen(ver);
if (len >= sizeof(_version_info)) len = sizeof(_version_info) - 1;
memcpy(_version_info, ver, len);
_version_info[len] = 0;
dismiss_after = millis() + BOOT_SCREEN_MILLIS;
}
int render(DisplayDriver& display) override {
// meshcore logo
display.setColor(DisplayDriver::BLUE);
int logoWidth = 128;
display.drawXbm((display.width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13);
// version info
display.setColor(DisplayDriver::LIGHT);
display.setTextSize(2);
display.drawTextCentered(display.width()/2, 22, _version_info);
display.setTextSize(1);
display.drawTextCentered(display.width()/2, 42, FIRMWARE_BUILD_DATE);
return 1000;
}
void poll() override {
if (millis() >= dismiss_after) {
_task->gotoHomeScreen();
}
}
};
class HomeScreen : public UIScreen {
enum HomePage {
FIRST,
RECENT,
RADIO,
BLUETOOTH,
ADVERT,
SHUTDOWN,
Count // keep as last
};
UITask* _task;
mesh::RTCClock* _rtc;
SensorManager* _sensors;
NodePrefs* _node_prefs;
uint8_t _page;
bool _shutdown_init;
AdvertPath recent[UI_RECENT_LIST_SIZE];
void renderBatteryIndicator(DisplayDriver& display, uint16_t batteryMilliVolts) {
// Convert millivolts to percentage
const int minMilliVolts = 3000; // Minimum voltage (e.g., 3.0V)
const int maxMilliVolts = 4200; // Maximum voltage (e.g., 4.2V)
int batteryPercentage = ((batteryMilliVolts - minMilliVolts) * 100) / (maxMilliVolts - minMilliVolts);
if (batteryPercentage < 0) batteryPercentage = 0; // Clamp to 0%
if (batteryPercentage > 100) batteryPercentage = 100; // Clamp to 100%
// battery icon
int iconWidth = 24;
int iconHeight = 10;
int iconX = display.width() - iconWidth - 5; // Position the icon near the top-right corner
int iconY = 0;
display.setColor(DisplayDriver::GREEN);
// battery outline
display.drawRect(iconX, iconY, iconWidth, iconHeight);
// battery "cap"
display.fillRect(iconX + iconWidth, iconY + (iconHeight / 4), 3, iconHeight / 2);
// fill the battery based on the percentage
int fillWidth = (batteryPercentage * (iconWidth - 4)) / 100;
display.fillRect(iconX + 2, iconY + 2, fillWidth, iconHeight - 4);
}
public:
HomeScreen(UITask* task, mesh::RTCClock* rtc, SensorManager* sensors, NodePrefs* node_prefs)
: _task(task), _rtc(rtc), _sensors(sensors), _node_prefs(node_prefs), _page(0), _shutdown_init(false) { }
void poll() override {
if (_shutdown_init && !_task->isButtonPressed()) { // must wait for USR button to be released
_task->shutdown();
}
}
int render(DisplayDriver& display) override {
char tmp[80];
// node name
display.setCursor(0, 0);
display.setTextSize(1);
display.setColor(DisplayDriver::GREEN);
display.print(_node_prefs->node_name);
// battery voltage
renderBatteryIndicator(display, _task->getBattMilliVolts());
// curr page indicator
int y = 14;
int x = display.width() / 2 - 25;
for (uint8_t i = 0; i < HomePage::Count; i++, x += 10) {
if (i == _page) {
display.fillRect(x-1, y-1, 3, 3);
} else {
display.fillRect(x, y, 1, 1);
}
}
if (_page == HomePage::FIRST) {
display.setColor(DisplayDriver::YELLOW);
display.setTextSize(2);
sprintf(tmp, "MSG: %d", _task->getMsgCount());
display.drawTextCentered(display.width() / 2, 20, tmp);
if (_task->hasConnection()) {
display.setColor(DisplayDriver::GREEN);
display.setTextSize(1);
display.drawTextCentered(display.width() / 2, 43, "< Connected >");
} else if (the_mesh.getBLEPin() != 0) { // BT pin
display.setColor(DisplayDriver::RED);
display.setTextSize(2);
sprintf(tmp, "Pin:%d", the_mesh.getBLEPin());
display.drawTextCentered(display.width() / 2, 43, tmp);
}
} else if (_page == HomePage::RECENT) {
the_mesh.getRecentlyHeard(recent, UI_RECENT_LIST_SIZE);
display.setColor(DisplayDriver::GREEN);
int y = 20;
for (int i = 0; i < UI_RECENT_LIST_SIZE; i++, y += 11) {
auto a = &recent[i];
if (a->name[0] == 0) continue; // empty slot
display.setCursor(0, y);
display.print(a->name);
int secs = _rtc->getCurrentTime() - a->recv_timestamp;
if (secs < 60) {
sprintf(tmp, "%ds", secs);
} else if (secs < 60*60) {
sprintf(tmp, "%dm", secs / 60);
} else {
sprintf(tmp, "%dh", secs / (60*60));
}
display.setCursor(display.width() - display.getTextWidth(tmp) - 1, y);
display.print(tmp);
}
} else if (_page == HomePage::RADIO) {
display.setColor(DisplayDriver::YELLOW);
display.setTextSize(1);
// freq / sf
display.setCursor(0, 20);
sprintf(tmp, "FQ: %06.3f SF: %d", _node_prefs->freq, _node_prefs->sf);
display.print(tmp);
display.setCursor(0, 31);
sprintf(tmp, "BW: %03.2f CR: %d", _node_prefs->bw, _node_prefs->cr);
display.print(tmp);
// tx power, noise floor
display.setCursor(0, 42);
sprintf(tmp, "TX: %ddBm", _node_prefs->tx_power_dbm);
display.print(tmp);
display.setCursor(0, 53);
sprintf(tmp, "Noise floor: %d", radio_driver.getNoiseFloor());
display.print(tmp);
} else if (_page == HomePage::BLUETOOTH) {
display.setColor(DisplayDriver::GREEN);
display.drawXbm((display.width() - 32) / 2, 18,
_task->isSerialEnabled() ? bluetooth_on : bluetooth_off,
32, 32);
display.setTextSize(1);
display.drawTextCentered(display.width() / 2, 64 - 11, "toggle: " PRESS_LABEL);
} else if (_page == HomePage::ADVERT) {
display.setColor(DisplayDriver::GREEN);
display.drawXbm((display.width() - 32) / 2, 18, advert_icon, 32, 32);
display.drawTextCentered(display.width() / 2, 64 - 11, "advert: " PRESS_LABEL);
} else if (_page == HomePage::SHUTDOWN) {
display.setColor(DisplayDriver::GREEN);
display.setTextSize(1);
if (_shutdown_init) {
display.drawTextCentered(display.width() / 2, 34, "hibernating...");
} else {
display.drawXbm((display.width() - 32) / 2, 18, power_icon, 32, 32);
display.drawTextCentered(display.width() / 2, 64 - 11, "hibernate: " PRESS_LABEL);
}
}
return 5000; // next render after 5000 ms
}
bool handleInput(char c) override {
if (c == KEY_LEFT) {
_page = (_page + HomePage::Count - 1) % HomePage::Count;
return true;
}
if (c == KEY_RIGHT || c == KEY_SELECT) {
_page = (_page + 1) % HomePage::Count;
if (_page == HomePage::RECENT) {
_task->showAlert("Recent adverts", 800);
}
return true;
}
if (c == KEY_ENTER && _page == HomePage::BLUETOOTH) {
if (_task->isSerialEnabled()) { // toggle Bluetooth on/off
_task->disableSerial();
} else {
_task->enableSerial();
}
return true;
}
if (c == KEY_ENTER && _page == HomePage::ADVERT) {
#ifdef PIN_BUZZER
_task->soundBuzzer(UIEventType::ack);
#endif
if (the_mesh.advert()) {
_task->showAlert("Advert sent!", 1000);
} else {
_task->showAlert("Advert failed..", 1000);
}
return true;
}
if (c == KEY_ENTER && _page == HomePage::SHUTDOWN) {
_shutdown_init = true; // need to wait for button to be released
return true;
}
return false;
}
};
class MsgPreviewScreen : public UIScreen {
UITask* _task;
mesh::RTCClock* _rtc;
struct MsgEntry {
uint32_t timestamp;
char origin[62];
char msg[78];
};
#define MAX_UNREAD_MSGS 32
int num_unread;
MsgEntry unread[MAX_UNREAD_MSGS];
public:
MsgPreviewScreen(UITask* task, mesh::RTCClock* rtc) : _task(task), _rtc(rtc) { num_unread = 0; }
void addPreview(uint8_t path_len, const char* from_name, const char* msg) {
if (num_unread >= MAX_UNREAD_MSGS) return; // full
auto p = &unread[num_unread++];
p->timestamp = _rtc->getCurrentTime();
if (path_len == 0xFF) {
sprintf(p->origin, "(D) %s:", from_name);
} else {
sprintf(p->origin, "(%d) %s:", (uint32_t) path_len, from_name);
}
StrHelper::strncpy(p->msg, msg, sizeof(p->msg));
}
int render(DisplayDriver& display) override {
char tmp[16];
display.setCursor(0, 0);
display.setTextSize(1);
display.setColor(DisplayDriver::GREEN);
sprintf(tmp, "Unread: %d", num_unread);
display.print(tmp);
auto p = &unread[0];
int secs = _rtc->getCurrentTime() - p->timestamp;
if (secs < 60) {
sprintf(tmp, "%ds", secs);
} else if (secs < 60*60) {
sprintf(tmp, "%dm", secs / 60);
} else {
sprintf(tmp, "%dh", secs / (60*60));
}
display.setCursor(display.width() - display.getTextWidth(tmp) - 2, 0);
display.print(tmp);
display.drawRect(0, 11, display.width(), 1); // horiz line
display.setCursor(0, 14);
display.setColor(DisplayDriver::YELLOW);
display.print(p->origin);
display.setCursor(0, 25);
display.setColor(DisplayDriver::LIGHT);
display.printWordWrap(p->msg, display.width());
return 1000; // next render after 1000 ms
}
bool handleInput(char c) override {
if (c == KEY_SELECT || c == KEY_RIGHT) {
num_unread--;
if (num_unread == 0) {
_task->gotoHomeScreen();
} else {
// delete first/curr item from unread queue
for (int i = 0; i < num_unread; i++) {
unread[i] = unread[i + 1];
}
}
return true;
}
if (c == KEY_ENTER) {
num_unread = 0; // clear unread queue
_task->gotoHomeScreen();
return true;
}
return false;
}
};
void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* node_prefs) {
_display = display;
_sensors = sensors;
_auto_off = millis() + AUTO_OFF_MILLIS;
#if defined(PIN_USER_BTN)
user_btn.begin();
#endif
_node_prefs = node_prefs;
if (_display != NULL) {
_display->turnOn();
}
#ifdef PIN_BUZZER
buzzer.begin();
#endif
ui_started_at = millis();
_alert_expiry = 0;
splash = new SplashScreen(this);
home = new HomeScreen(this, &rtc_clock, sensors, node_prefs);
msg_preview = new MsgPreviewScreen(this, &rtc_clock);
setCurrScreen(splash);
}
void UITask::showAlert(const char* text, int duration_millis) {
strcpy(_alert, text);
_alert_expiry = millis() + duration_millis;
}
void UITask::soundBuzzer(UIEventType bet) {
#if defined(PIN_BUZZER)
switch(bet){
case UIEventType::contactMessage:
// gemini's pick
buzzer.play("MsgRcv3:d=4,o=6,b=200:32e,32g,32b,16c7");
break;
case UIEventType::channelMessage:
buzzer.play("kerplop:d=16,o=6,b=120:32g#,32c#");
break;
case UIEventType::ack:
buzzer.play("ack:d=32,o=8,b=120:c");
break;
case UIEventType::roomMessage:
case UIEventType::newContactMessage:
case UIEventType::none:
default:
break;
}
#endif
}
void UITask::msgRead(int msgcount) {
_msgcount = msgcount;
if (msgcount == 0) {
gotoHomeScreen();
}
}
void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) {
_msgcount = msgcount;
((MsgPreviewScreen *) msg_preview)->addPreview(path_len, from_name, text);
setCurrScreen(msg_preview);
if (_display != NULL) {
if (!_display->isOn()) _display->turnOn();
_auto_off = millis() + AUTO_OFF_MILLIS; // extend the auto-off timer
_next_refresh = 0; // trigger refresh
}
}
void UITask::userLedHandler() {
#ifdef PIN_STATUS_LED
static int state = 0;
static int next_change = 0;
static int last_increment = 0;
int cur_time = millis();
if (cur_time > next_change) {
if (state == 0) {
state = 1;
if (_msgcount > 0) {
last_increment = LED_ON_MSG_MILLIS;
} else {
last_increment = LED_ON_MILLIS;
}
next_change = cur_time + last_increment;
} else {
state = 0;
next_change = cur_time + LED_CYCLE_MILLIS - last_increment;
}
digitalWrite(PIN_STATUS_LED, state);
}
#endif
}
void UITask::setCurrScreen(UIScreen* c) {
curr = c;
_next_refresh = 0;
}
/*
hardware-agnostic pre-shutdown activity should be done here
*/
void UITask::shutdown(bool restart){
#ifdef PIN_BUZZER
/* note: we have a choice here -
we can do a blocking buzzer.loop() with non-deterministic consequences
or we can set a flag and delay the shutdown for a couple of seconds
while a non-blocking buzzer.loop() plays out in UITask::loop()
*/
buzzer.shutdown();
uint32_t buzzer_timer = millis(); // fail-safe shutdown
while (buzzer.isPlaying() && (millis() - 2500) < buzzer_timer)
buzzer.loop();
#endif // PIN_BUZZER
if (restart) {
_board->reboot();
} else {
_display->turnOff();
_board->powerOff();
}
}
bool UITask::isButtonPressed() const {
#ifdef PIN_USER_BTN
return user_btn.isPressed();
#else
return false;
#endif
}
void UITask::loop() {
char c = 0;
#if defined(PIN_USER_BTN)
int ev = user_btn.check();
if (ev == BUTTON_EVENT_CLICK) {
c = checkDisplayOn(KEY_SELECT);
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
c = handleLongPress(KEY_ENTER);
}
#endif
#if defined(WIO_TRACKER_L1)
ev = joystick_left.check();
if (ev == BUTTON_EVENT_CLICK) {
c = checkDisplayOn(KEY_LEFT);
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
c = handleLongPress(KEY_LEFT);
}
ev = joystick_right.check();
if (ev == BUTTON_EVENT_CLICK) {
c = checkDisplayOn(KEY_RIGHT);
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
c = handleLongPress(KEY_RIGHT);
}
#endif
if (c != 0 && curr) {
curr->handleInput(c);
_auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer
_next_refresh = 0; // trigger refresh
}
userLedHandler();
#ifdef PIN_BUZZER
if (buzzer.isPlaying()) buzzer.loop();
#endif
if (curr) curr->poll();
if (_display != NULL && _display->isOn()) {
if (millis() >= _next_refresh && curr) {
_display->startFrame();
int delay_millis = curr->render(*_display);
if (millis() < _alert_expiry) { // render alert popup
_display->setTextSize(1);
int y = _display->height() / 3;
int p = _display->height() / 32;
_display->setColor(DisplayDriver::DARK);
_display->fillRect(p, y, _display->width() - p*2, y);
_display->setColor(DisplayDriver::LIGHT); // draw box border
_display->drawRect(p, y, _display->width() - p*2, y);
_display->drawTextCentered(_display->width() / 2, y + p*3, _alert);
_next_refresh = _alert_expiry; // will need refresh when alert is dismissed
} else {
_next_refresh = millis() + delay_millis;
}
_display->endFrame();
}
if (millis() > _auto_off) {
_display->turnOff();
}
}
#ifdef AUTO_SHUTDOWN_MILLIVOLTS
if (millis() > next_batt_chck) {
uint16_t milliVolts = getBattMilliVolts();
if (milliVolts > 0 && milliVolts < AUTO_SHUTDOWN_MILLIVOLTS) {
// show low battery shutdown alert
// we should only do this for eink displays, which will persist after power loss
#ifdef THINKNODE_M1
if (_display != NULL) {
_display->startFrame();
_display->setTextSize(2);
_display->setColor(DisplayDriver::RED);
_display->drawTextCentered(_display->width() / 2, 20, "Low Battery.");
_display->drawTextCentered(_display->width() / 2, 40, "Shutting Down!");
_display->endFrame();
}
#endif
shutdown();
}
next_batt_chck = millis() + 8000;
}
#endif
}
char UITask::checkDisplayOn(char c) {
if (_display != NULL) {
if (!_display->isOn()) {
_display->turnOn(); // turn display on and consume event
c = 0;
}
_auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer
_next_refresh = 0; // trigger refresh
}
return c;
}
char UITask::handleLongPress(char c) {
if (millis() - ui_started_at < 8000) { // long press in first 8 seconds since startup -> CLI/rescue
the_mesh.enterCLIRescue();
c = 0; // consume event
}
return c;
}
/*
void UITask::handleButtonTriplePress() {
MESH_DEBUG_PRINTLN("UITask: triple press triggered");
// Toggle buzzer quiet mode
#ifdef PIN_BUZZER
if (buzzer.isQuiet()) {
buzzer.quiet(false);
soundBuzzer(UIEventType::ack);
showAlert("Buzzer: ON", 600);
} else {
buzzer.quiet(true);
showAlert("Buzzer: OFF", 600);
}
_next_refresh = 0; // trigger refresh
#endif
}
*/

View File

@@ -0,0 +1,65 @@
#pragma once
#include <MeshCore.h>
#include <helpers/ui/DisplayDriver.h>
#include <helpers/ui/UIScreen.h>
#include <helpers/SensorManager.h>
#include <helpers/BaseSerialInterface.h>
#include <Arduino.h>
#ifdef PIN_BUZZER
#include <helpers/ui/buzzer.h>
#endif
#include "../AbstractUITask.h"
#include "../NodePrefs.h"
class UITask : public AbstractUITask {
DisplayDriver* _display;
SensorManager* _sensors;
#ifdef PIN_BUZZER
genericBuzzer buzzer;
#endif
unsigned long _next_refresh, _auto_off;
NodePrefs* _node_prefs;
char _alert[80];
unsigned long _alert_expiry;
int _msgcount;
unsigned long ui_started_at, next_batt_chck;
UIScreen* splash;
UIScreen* home;
UIScreen* msg_preview;
UIScreen* curr;
void userLedHandler();
// Button action handlers
char checkDisplayOn(char c);
char handleLongPress(char c);
void setCurrScreen(UIScreen* c);
public:
UITask(mesh::MainBoard* board, BaseSerialInterface* serial) : AbstractUITask(board, serial), _display(NULL), _sensors(NULL) {
next_batt_chck = _next_refresh = 0;
ui_started_at = 0;
curr = NULL;
}
void begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* node_prefs);
void gotoHomeScreen() { setCurrScreen(home); }
void showAlert(const char* text, int duration_millis);
int getMsgCount() const { return _msgcount; }
bool hasDisplay() const { return _display != NULL; }
bool isButtonPressed() const;
// from AbstractUITask
void msgRead(int msgcount) override;
void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) override;
void soundBuzzer(UIEventType bet = UIEventType::none) override;
void loop() override;
void shutdown(bool restart = false);
};

View File

@@ -0,0 +1,118 @@
#pragma once
#include <stdint.h>
// 'meshcore', 128x13px
static const uint8_t meshcore_logo [] = {
0x3c, 0x01, 0xe3, 0xff, 0xc7, 0xff, 0x8f, 0x03, 0x87, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe,
0x3c, 0x03, 0xe3, 0xff, 0xc7, 0xff, 0x8e, 0x03, 0x8f, 0xfe, 0x3f, 0xfe, 0x1f, 0xff, 0x1f, 0xfe,
0x3e, 0x03, 0xc3, 0xff, 0x8f, 0xff, 0x0e, 0x07, 0x8f, 0xfe, 0x7f, 0xfe, 0x1f, 0xff, 0x1f, 0xfc,
0x3e, 0x07, 0xc7, 0x80, 0x0e, 0x00, 0x0e, 0x07, 0x9e, 0x00, 0x78, 0x0e, 0x3c, 0x0f, 0x1c, 0x00,
0x3e, 0x0f, 0xc7, 0x80, 0x1e, 0x00, 0x0e, 0x07, 0x1e, 0x00, 0x70, 0x0e, 0x38, 0x0f, 0x3c, 0x00,
0x7f, 0x0f, 0xc7, 0xfe, 0x1f, 0xfc, 0x1f, 0xff, 0x1c, 0x00, 0x70, 0x0e, 0x38, 0x0e, 0x3f, 0xf8,
0x7f, 0x1f, 0xc7, 0xfe, 0x0f, 0xff, 0x1f, 0xff, 0x1c, 0x00, 0xf0, 0x0e, 0x38, 0x0e, 0x3f, 0xf8,
0x7f, 0x3f, 0xc7, 0xfe, 0x0f, 0xff, 0x1f, 0xff, 0x1c, 0x00, 0xf0, 0x1e, 0x3f, 0xfe, 0x3f, 0xf0,
0x77, 0x3b, 0x87, 0x00, 0x00, 0x07, 0x1c, 0x0f, 0x3c, 0x00, 0xe0, 0x1c, 0x7f, 0xfc, 0x38, 0x00,
0x77, 0xfb, 0x8f, 0x00, 0x00, 0x07, 0x1c, 0x0f, 0x3c, 0x00, 0xe0, 0x1c, 0x7f, 0xf8, 0x38, 0x00,
0x73, 0xf3, 0x8f, 0xff, 0x0f, 0xff, 0x1c, 0x0e, 0x3f, 0xf8, 0xff, 0xfc, 0x70, 0x78, 0x7f, 0xf8,
0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfe, 0x3c, 0x0e, 0x3f, 0xf8, 0xff, 0xfc, 0x70, 0x3c, 0x7f, 0xf8,
0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfc, 0x3c, 0x0e, 0x1f, 0xf8, 0xff, 0xf8, 0x70, 0x3c, 0x7f, 0xf8,
};
static const uint8_t bluetooth_on[] = {
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x30, 0x00, 0x00,
0x00, 0x3C, 0x00, 0x00,
0x00, 0x3E, 0x00, 0x00,
0x00, 0x3F, 0x80, 0x00,
0x00, 0x3F, 0xC0, 0x00,
0x00, 0x3B, 0xE0, 0x00,
0x30, 0x38, 0xF8, 0x00,
0x3C, 0x38, 0x7C, 0x00,
0x3E, 0x38, 0x7C, 0x00,
0x1F, 0xB8, 0xF8, 0x70,
0x07, 0xF9, 0xF0, 0x78,
0x03, 0xFF, 0xC0, 0x78,
0x00, 0xFF, 0x80, 0x3C,
0x00, 0x7F, 0x07, 0x1C,
0x00, 0x7E, 0x07, 0x1C,
0x03, 0xFF, 0x82, 0x1C,
0x03, 0xFF, 0xC0, 0x78,
0x07, 0xFB, 0xE0, 0x78,
0x0F, 0xB8, 0xF8, 0x70,
0x3E, 0x38, 0x7C, 0x00,
0x3C, 0x38, 0x7C, 0x00,
0x38, 0x38, 0xF8, 0x00,
0x00, 0x39, 0xF0, 0x00,
0x00, 0x3F, 0xC0, 0x00,
0x00, 0x3F, 0x80, 0x00,
0x00, 0x3E, 0x00, 0x00,
0x00, 0x3C, 0x00, 0x00,
0x00, 0x38, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
};
static const uint8_t bluetooth_off[] = {
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x03, 0x80, 0x00,
0x00, 0x03, 0xC0, 0x00,
0x00, 0x03, 0xE0, 0x00,
0x38, 0x03, 0xF8, 0x00,
0x3C, 0x03, 0xFC, 0x00,
0x3E, 0x03, 0xBF, 0x00,
0x0F, 0x83, 0x8F, 0x80,
0x07, 0xC3, 0x87, 0xC0,
0x03, 0xF0, 0x03, 0xC0,
0x00, 0xF8, 0x0F, 0x80,
0x00, 0x7C, 0x0F, 0x00,
0x00, 0x1F, 0x0E, 0x00,
0x00, 0x0F, 0x80, 0x00,
0x00, 0x07, 0xE0, 0x00,
0x00, 0x07, 0xF0, 0x00,
0x00, 0x0F, 0xF8, 0x00,
0x00, 0x3F, 0xBE, 0x00,
0x00, 0x7F, 0x9F, 0x00,
0x00, 0xFB, 0x8F, 0xC0,
0x03, 0xE3, 0x83, 0xE0,
0x03, 0xC3, 0x87, 0xF0,
0x03, 0x83, 0x8F, 0xFC,
0x00, 0x03, 0xBF, 0x3C,
0x00, 0x03, 0xFC, 0x1C,
0x00, 0x03, 0xF8, 0x00,
0x00, 0x03, 0xE0, 0x00,
0x00, 0x03, 0xC0, 0x00,
0x00, 0x03, 0x80, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
};
static const uint8_t power_icon[] = {
0x00, 0x01, 0x80, 0x00, 0x00, 0x03, 0xC0, 0x00, 0x00, 0x03, 0xC0, 0x00,
0x00, 0x33, 0xCC, 0x00, 0x00, 0xF3, 0xCF, 0x00, 0x01, 0xF3, 0xCF, 0x80,
0x03, 0xF3, 0xCF, 0xC0, 0x07, 0xF3, 0xCF, 0xE0, 0x0F, 0xE3, 0xC7, 0xF0,
0x1F, 0xC3, 0xC3, 0xF8, 0x1F, 0x83, 0xC1, 0xF8, 0x3F, 0x03, 0xC0, 0xFC,
0x3E, 0x03, 0xC0, 0x7C, 0x3E, 0x03, 0xC0, 0x7C, 0x7E, 0x01, 0x80, 0x7E,
0x7C, 0x00, 0x00, 0x3E, 0x7C, 0x00, 0x00, 0x3E, 0x7C, 0x00, 0x00, 0x3E,
0x7C, 0x00, 0x00, 0x3E, 0x7C, 0x00, 0x00, 0x3E, 0x3E, 0x00, 0x00, 0x7C,
0x3E, 0x00, 0x00, 0x7C, 0x3F, 0x00, 0x00, 0xFC, 0x1F, 0x80, 0x01, 0xF8,
0x1F, 0xC0, 0x03, 0xF8, 0x0F, 0xE0, 0x07, 0xF0, 0x0F, 0xF8, 0x1F, 0xF0,
0x07, 0xFF, 0xFF, 0xE0, 0x03, 0xFF, 0xFF, 0xC0, 0x00, 0xFF, 0xFF, 0x00,
0x00, 0x3F, 0xFC, 0x00, 0x00, 0x0F, 0xF0, 0x00,
};
static const uint8_t advert_icon[] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x30,
0x1C, 0x00, 0x00, 0x38, 0x18, 0x00, 0x00, 0x18, 0x30, 0x00, 0x00, 0x0C,
0x30, 0x60, 0x06, 0x0C, 0x60, 0xE0, 0x07, 0x06, 0x61, 0xC0, 0x03, 0x86,
0xE1, 0x81, 0x81, 0x87, 0xC3, 0x07, 0xE0, 0xC3, 0xC3, 0x0F, 0xF0, 0xC3,
0xC3, 0x0F, 0xF0, 0xC3, 0xC3, 0x0F, 0xF0, 0xC3, 0xC3, 0x0F, 0xF0, 0xC3,
0xC3, 0x07, 0xE0, 0xC3, 0xC1, 0x83, 0xC1, 0x83, 0x61, 0x80, 0x01, 0x86,
0x60, 0xC0, 0x03, 0x06, 0x70, 0xE0, 0x07, 0x0E, 0x30, 0x40, 0x02, 0x0C,
0x38, 0x00, 0x00, 0x1C, 0x18, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, 0x30,
0x04, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};

View File

@@ -1,8 +1,7 @@
#include "UITask.h"
#include <Arduino.h>
#include <helpers/TxtDataHelpers.h>
#include "NodePrefs.h"
#include "MyMesh.h"
#include "../MyMesh.h"
#define AUTO_OFF_MILLIS 15000 // 15 seconds
#define BOOT_SCREEN_MILLIS 3000 // 3 seconds

View File

@@ -9,28 +9,18 @@
#include <helpers/ui/buzzer.h>
#endif
#include "NodePrefs.h"
#include "../AbstractUITask.h"
#include "../NodePrefs.h"
#include "Button.h"
enum class UIEventType
{
none,
contactMessage,
channelMessage,
roomMessage,
newContactMessage,
ack
};
class UITask {
class UITask : public AbstractUITask {
DisplayDriver* _display;
mesh::MainBoard* _board;
SensorManager* _sensors;
#ifdef PIN_BUZZER
genericBuzzer buzzer;
#endif
unsigned long _next_refresh, _auto_off;
bool _connected;
NodePrefs* _node_prefs;
char _version_info[32];
char _origin[62];
@@ -64,19 +54,20 @@ class UITask {
public:
UITask(mesh::MainBoard* board) : _board(board), _display(NULL), _sensors(NULL) {
UITask(mesh::MainBoard* board, BaseSerialInterface* serial) : AbstractUITask(board, serial), _display(NULL), _sensors(NULL) {
_next_refresh = 0;
ui_started_at = 0;
_connected = false;
}
void begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* node_prefs);
void setHasConnection(bool connected) { _connected = connected; }
bool hasDisplay() const { return _display != NULL; }
void clearMsgPreview();
void msgRead(int msgcount);
void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount);
void soundBuzzer(UIEventType bet = UIEventType::none);
// from AbstractUITask
void msgRead(int msgcount) override;
void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) override;
void soundBuzzer(UIEventType bet = UIEventType::none) override;
void loop() override;
void shutdown(bool restart = false);
void loop();
};

View File

@@ -22,11 +22,11 @@
/* ------------------------------ Config -------------------------------- */
#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "24 Jul 2025"
#define FIRMWARE_BUILD_DATE "31 Aug 2025"
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.7.4"
#define FIRMWARE_VERSION "v1.8.0"
#endif
#ifndef LORA_FREQ
@@ -98,6 +98,7 @@ struct RepeaterStats {
uint16_t err_events; // was 'n_full_events'
int16_t last_snr; // x 4
uint16_t n_direct_dups, n_flood_dups;
uint32_t total_rx_air_time_secs;
};
struct ClientInfo {
@@ -158,7 +159,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
}
void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr) {
#if MAX_NEIGHBOURS // check if neighbours enabled
#if MAX_NEIGHBOURS // check if neighbours enabled
// find existing neighbour, else use least recently updated
uint32_t oldest_timestamp = 0xFFFFFFFF;
NeighbourInfo* neighbour = &neighbours[0];
@@ -208,16 +209,19 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
stats.last_snr = (int16_t)(radio_driver.getLastSNR() * 4);
stats.n_direct_dups = ((SimpleMeshTables *)getTables())->getNumDirectDups();
stats.n_flood_dups = ((SimpleMeshTables *)getTables())->getNumFloodDups();
stats.total_rx_air_time_secs = getReceiveAirTime() / 1000;
memcpy(&reply_data[4], &stats, sizeof(stats));
return 4 + sizeof(stats); // reply_len
}
case REQ_TYPE_GET_TELEMETRY_DATA: {
uint8_t perm_mask = ~(payload[1]); // NEW: first reserved byte (of 4), is now inverse mask to apply to permissions
telemetry.reset();
telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f);
// query other sensors -- target specific
sensors.querySensors(sender->is_admin ? 0xFF : 0x00, telemetry);
sensors.querySensors((sender->is_admin ? 0xFF : 0x00) & perm_mask, telemetry);
uint8_t tlen = telemetry.getSize();
memcpy(&reply_data[4], telemetry.getBuffer(), tlen);
@@ -585,7 +589,7 @@ public:
_prefs.cr = LORA_CR;
_prefs.tx_power_dbm = LORA_TX_POWER;
_prefs.advert_interval = 1; // default to 2 minutes for NEW installs
_prefs.flood_advert_interval = 3; // 3 hours
_prefs.flood_advert_interval = 12; // 12 hours
_prefs.flood_max = 64;
_prefs.interference_threshold = 0; // disabled
}
@@ -607,8 +611,8 @@ public:
const char* getBuildDate() override { return FIRMWARE_BUILD_DATE; }
const char* getRole() override { return FIRMWARE_ROLE; }
const char* getNodeName() { return _prefs.node_name; }
NodePrefs* getNodePrefs() {
return &_prefs;
NodePrefs* getNodePrefs() {
return &_prefs;
}
void savePrefs() override {
@@ -715,7 +719,18 @@ public:
*dp = 0; // null terminator
}
const uint8_t* getSelfIdPubKey() override { return self_id.pub_key; }
void removeNeighbor(const uint8_t* pubkey, int key_len) override {
#if MAX_NEIGHBOURS
for (int i = 0; i < MAX_NEIGHBOURS; i++) {
NeighbourInfo* neighbour = &neighbours[i];
if(memcmp(neighbour->id.pub_key, pubkey, key_len) == 0){
neighbours[i] = NeighbourInfo(); // clear neighbour entry
}
}
#endif
}
mesh::LocalIdentity& getSelfId() override { return self_id; }
void clearStats() override {
radio_driver.resetStats();
@@ -778,7 +793,7 @@ void halt() {
while (1) ;
}
static char command[80];
static char command[160];
void setup() {
Serial.begin(115200);

View File

@@ -22,11 +22,11 @@
/* ------------------------------ Config -------------------------------- */
#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "24 Jul 2025"
#define FIRMWARE_BUILD_DATE "31 Aug 2025"
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.7.4"
#define FIRMWARE_VERSION "v1.8.0"
#endif
#ifndef LORA_FREQ
@@ -298,7 +298,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
// uint32_t now = getRTCClock()->getCurrentTimeUnique();
// memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp
memcpy(reply_data, &sender_timestamp, 4); // reflect sender_timestamp back in response packet (kind of like a 'tag')
switch (payload[0]) {
case REQ_TYPE_GET_STATUS: {
ServerStats stats;
@@ -326,10 +326,12 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
}
case REQ_TYPE_GET_TELEMETRY_DATA: {
uint8_t perm_mask = ~(payload[1]); // NEW: first reserved byte (of 4), is now inverse mask to apply to permissions
telemetry.reset();
telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f);
// query other sensors -- target specific
sensors.querySensors(sender->permission == RoomPermission::ADMIN ? 0xFF : 0x00, telemetry);
sensors.querySensors((sender->permission == RoomPermission::ADMIN ? 0xFF : 0x00) & perm_mask, telemetry);
uint8_t tlen = telemetry.getSize();
memcpy(&reply_data[4], telemetry.getBuffer(), tlen);
@@ -744,9 +746,9 @@ public:
_prefs.tx_power_dbm = LORA_TX_POWER;
_prefs.disable_fwd = 1;
_prefs.advert_interval = 1; // default to 2 minutes for NEW installs
_prefs.flood_advert_interval = 3; // 3 hours
_prefs.flood_advert_interval = 12; // 12 hours
_prefs.flood_max = 64;
_prefs.interference_threshold = 0; // disabled
_prefs.interference_threshold = 0; // disabled
#ifdef ROOM_PASSWORD
StrHelper::strncpy(_prefs.guest_password, ROOM_PASSWORD, sizeof(_prefs.guest_password));
#endif
@@ -776,8 +778,8 @@ public:
const char* getBuildDate() override { return FIRMWARE_BUILD_DATE; }
const char* getRole() override { return FIRMWARE_ROLE; }
const char* getNodeName() { return _prefs.node_name; }
NodePrefs* getNodePrefs() {
return &_prefs;
NodePrefs* getNodePrefs() {
return &_prefs;
}
void savePrefs() override {
@@ -861,7 +863,7 @@ public:
strcpy(reply, "not supported");
}
const uint8_t* getSelfIdPubKey() override { return self_id.pub_key; }
mesh::LocalIdentity& getSelfId() override { return self_id; }
void clearStats() override {
radio_driver.resetStats();

View File

@@ -244,10 +244,12 @@ uint8_t SensorMesh::handleRequest(uint8_t perms, uint32_t sender_timestamp, uint
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
uint8_t perm_mask = ~(payload[0]); // NEW: first reserved byte (of 4), is now inverse mask to apply to permissions
telemetry.reset();
telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f);
// query other sensors -- target specific
sensors.querySensors(0xFF, 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);
uint8_t tlen = telemetry.getSize();
@@ -545,7 +547,26 @@ void SensorMesh::handleCommand(uint32_t sender_timestamp, char* command, char* r
Serial.printf("\n");
}
reply[0] = 0;
} else {
} else if (memcmp(command, "io ", 2) == 0) { // io {value}: write, io: read
if (command[2] == ' ') { // it's a write
uint32_t val;
uint32_t g = board.getGpio();
if (command[3] == 'r') { // reset bits
sscanf(&command[4], "%x", &val);
val = g & ~val;
} else if (command[3] == 's') { // set bits
sscanf(&command[4], "%x", &val);
val |= g;
} else if (command[3] == 't') { // toggle bits
sscanf(&command[4], "%x", &val);
val ^= g;
} else { // set value
sscanf(&command[3], "%x", &val);
}
board.setGpio(val);
}
sprintf(reply, "%x", board.getGpio());
} else{
_cli.handleCommand(sender_timestamp, command, reply); // common CLI commands
}
}
@@ -592,6 +613,23 @@ void SensorMesh::getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) {
}
}
void SensorMesh::sendAckTo(const ContactInfo& dest, uint32_t ack_hash) {
if (dest.out_path_len < 0) {
mesh::Packet* ack = createAck(ack_hash);
if (ack) sendFlood(ack, TXT_ACK_DELAY);
} else {
uint32_t d = TXT_ACK_DELAY;
if (getExtraAckTransmitCount() > 0) {
mesh::Packet* a1 = createMultiAck(ack_hash, 1);
if (a1) sendDirect(a1, dest.out_path, dest.out_path_len, d);
d += 300;
}
mesh::Packet* a2 = createAck(ack_hash);
if (a2) sendDirect(a2, dest.out_path, dest.out_path_len, d);
}
}
void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_idx, const uint8_t* secret, uint8_t* data, size_t len) {
int i = matching_peer_indexes[sender_idx];
if (i < 0 || i >= num_contacts) {
@@ -635,38 +673,55 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i
memcpy(&sender_timestamp, data, 4); // timestamp (by sender's RTC clock - which could be wrong)
uint flags = (data[4] >> 2); // message attempt number, and other flags
if (!(flags == TXT_TYPE_CLI_DATA)) {
MESH_DEBUG_PRINTLN("onPeerDataRecv: unsupported text type received: flags=%02x", (uint32_t)flags);
} else if (sender_timestamp > from.last_timestamp) { // prevent replay attacks
from.last_timestamp = sender_timestamp;
from.last_activity = getRTCClock()->getCurrentTime();
if (sender_timestamp > from.last_timestamp) { // prevent replay attacks
if (flags == TXT_TYPE_PLAIN) {
bool handled = handleIncomingMsg(from, sender_timestamp, &data[5], flags, len - 5);
if (handled) { // if msg was handled then send an ack
uint32_t ack_hash; // calc truncated hash of the message timestamp + text + sender pub_key, to prove to sender that we got it
mesh::Utils::sha256((uint8_t *) &ack_hash, 4, data, 5 + strlen((char *)&data[5]), from.id.pub_key, PUB_KEY_SIZE);
// len can be > original length, but 'text' will be padded with zeroes
data[len] = 0; // need to make a C string again, with null terminator
uint8_t temp[166];
char *command = (char *) &data[5];
char *reply = (char *) &temp[5];
handleCommand(sender_timestamp, command, reply);
int text_len = strlen(reply);
if (text_len > 0) {
uint32_t timestamp = getRTCClock()->getCurrentTimeUnique();
if (timestamp == sender_timestamp) {
// WORKAROUND: the two timestamps need to be different, in the CLI view
timestamp++;
}
memcpy(temp, &timestamp, 4); // mostly an extra blob to help make packet_hash unique
temp[4] = (TXT_TYPE_CLI_DATA << 2);
auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, from.id, secret, temp, 5 + text_len);
if (reply) {
if (from.out_path_len < 0) {
sendFlood(reply, CLI_REPLY_DELAY_MILLIS);
if (packet->isRouteFlood()) {
// 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);
} else {
sendDirect(reply, from.out_path, from.out_path_len, CLI_REPLY_DELAY_MILLIS);
sendAckTo(from, ack_hash);
}
}
} else if (flags == TXT_TYPE_CLI_DATA) {
from.last_timestamp = sender_timestamp;
from.last_activity = getRTCClock()->getCurrentTime();
// len can be > original length, but 'text' will be padded with zeroes
data[len] = 0; // need to make a C string again, with null terminator
uint8_t temp[166];
char *command = (char *) &data[5];
char *reply = (char *) &temp[5];
handleCommand(sender_timestamp, command, reply);
int text_len = strlen(reply);
if (text_len > 0) {
uint32_t timestamp = getRTCClock()->getCurrentTimeUnique();
if (timestamp == sender_timestamp) {
// WORKAROUND: the two timestamps need to be different, in the CLI view
timestamp++;
}
memcpy(temp, &timestamp, 4); // mostly an extra blob to help make packet_hash unique
temp[4] = (TXT_TYPE_CLI_DATA << 2);
auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, from.id, secret, temp, 5 + text_len);
if (reply) {
if (from.out_path_len < 0) {
sendFlood(reply, CLI_REPLY_DELAY_MILLIS);
} else {
sendDirect(reply, from.out_path, from.out_path_len, CLI_REPLY_DELAY_MILLIS);
}
}
}
} else {
MESH_DEBUG_PRINTLN("onPeerDataRecv: unsupported text type received: flags=%02x", (uint32_t)flags);
}
} else {
MESH_DEBUG_PRINTLN("onPeerDataRecv: possible replay attack detected");
@@ -674,6 +729,15 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i
}
}
bool SensorMesh::handleIncomingMsg(ContactInfo& from, uint32_t timestamp, uint8_t* data, uint flags, size_t len) {
MESH_DEBUG_PRINT("handleIncomingMsg: unhandled msg from ");
#ifdef MESH_DEBUG
mesh::Utils::printHex(Serial, from.id.pub_key, PUB_KEY_SIZE);
Serial.printf(": %s\n", data);
#endif
return false;
}
bool SensorMesh::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) {
int i = matching_peer_indexes[sender_idx];
if (i < 0 || i >= num_contacts) {

View File

@@ -49,11 +49,11 @@ struct ContactInfo {
};
#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "24 Jul 2025"
#define FIRMWARE_BUILD_DATE "31 Aug 2025"
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.7.4"
#define FIRMWARE_VERSION "v1.8.0"
#endif
#define FIRMWARE_ROLE "sensor"
@@ -88,7 +88,7 @@ public:
void formatNeighborsReply(char *reply) override {
strcpy(reply, "not supported");
}
const uint8_t* getSelfIdPubKey() override { return self_id.pub_key; }
mesh::LocalIdentity& getSelfId() override { return self_id; }
void clearStats() override { }
void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) override;
@@ -140,7 +140,8 @@ protected:
void onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_idx, const uint8_t* secret, uint8_t* data, size_t 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 onAckRecv(mesh::Packet* packet, uint32_t ack_crc) override;
virtual bool handleIncomingMsg(ContactInfo& from, uint32_t timestamp, uint8_t* data, uint flags, size_t len);
void sendAckTo(const ContactInfo& dest, uint32_t ack_hash);
private:
FILESYSTEM* _fs;
unsigned long next_local_advert, next_flood_advert;

View File

@@ -50,7 +50,7 @@ void halt() {
while (1) ;
}
static char command[120];
static char command[160];
void setup() {
Serial.begin(115200);

16
library.json Normal file
View File

@@ -0,0 +1,16 @@
{
"name": "MeshCore",
"version" : "1.7.4",
"dependencies": {
"SPI": "*",
"Wire": "*",
"jgromes/RadioLib": "^7.1.2",
"rweather/Crypto": "^0.4.0",
"adafruit/RTClib": "^2.1.3",
"melopero/Melopero RV3028": "^1.1.0",
"electroniccats/CayenneLPP": "1.4.0"
},
"build": {
"extraScript": "build_as_lib.py"
}
}

View File

@@ -47,6 +47,7 @@ build_src_filter =
+<*.cpp>
+<helpers/*.cpp>
+<helpers/radiolib/*.cpp>
+<helpers/ui/MomentaryButton.cpp>
; ----------------- ESP32 ---------------------
@@ -67,7 +68,7 @@ lib_deps =
; esp32c6 uses arduino framework 3.x
[esp32c6_base]
extends = esp32_base
platform = https://github.com/pioarduino/platform-espressif32/releases/download/stable/platform-espressif32.zip
platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.12/platform-espressif32.zip
; ----------------- NRF52 ---------------------
@@ -102,3 +103,33 @@ build_src_filter = ${arduino_base.build_src_filter}
+<helpers/stm32>
lib_deps = ${arduino_base.lib_deps}
file://arch/stm32/Adafruit_LittleFS_stm32
[sensor_base]
build_flags =
-D ENV_INCLUDE_GPS=1
-D ENV_INCLUDE_AHTX0=1
-D ENV_INCLUDE_BME280=1
-D ENV_INCLUDE_BMP280=1
-D ENV_INCLUDE_SHTC3=1
-D ENV_INCLUDE_SHT4X=1
-D ENV_INCLUDE_LPS22HB=1
-D ENV_INCLUDE_INA3221=1
-D ENV_INCLUDE_INA219=1
-D ENV_INCLUDE_INA226=1
-D ENV_INCLUDE_INA260=1
-D ENV_INCLUDE_MLX90614=1
-D ENV_INCLUDE_VL53L0X=1
lib_deps =
adafruit/Adafruit INA3221 Library @ ^1.0.1
adafruit/Adafruit INA219 @ ^1.2.3
robtillaart/INA226 @ ^0.6.4
adafruit/Adafruit INA260 Library @ ^1.5.3
adafruit/Adafruit AHTX0 @ ^2.0.5
adafruit/Adafruit BME280 Library @ ^2.3.0
adafruit/Adafruit BMP280 Library @ ^2.6.8
adafruit/Adafruit SHTC3 Library @ ^1.0.1
sensirion/Sensirion I2C SHT4x @ ^1.1.2
arduino-libraries/Arduino_LPS22HB @ ^1.0.2
adafruit/Adafruit MLX90614 Library @ ^2.1.5
adafruit/Adafruit_VL53L0X @ ^1.2.4
stevemarple/MicroNMEA @ ^2.0.6

View File

@@ -159,6 +159,7 @@ void Dispatcher::checkRecv() {
pkt->_snr = _radio->getLastSNR() * 4.0f;
score = _radio->packetScore(_radio->getLastSNR(), len);
air_time = _radio->getEstAirtimeFor(len);
rx_air_time += air_time;
}
}
}
@@ -169,9 +170,9 @@ void Dispatcher::checkRecv() {
if (pkt) {
#if MESH_PACKET_LOGGING
Serial.print(getLogDateTime());
Serial.printf(": RX, len=%d (type=%d, route=%s, payload_len=%d) SNR=%d RSSI=%d score=%d",
Serial.printf(": RX, len=%d (type=%d, route=%s, payload_len=%d) SNR=%d RSSI=%d score=%d time=%d",
pkt->getRawLength(), pkt->getPayloadType(), pkt->isRouteDirect() ? "D" : "F", pkt->payload_len,
(int)pkt->getSNR(), (int)_radio->getLastRSSI(), (int)(score*1000));
(int)pkt->getSNR(), (int)_radio->getLastRSSI(), (int)(score*1000), air_time);
static uint8_t packet_hash[MAX_HASH_SIZE];
pkt->calculatePacketHash(packet_hash);

View File

@@ -114,7 +114,7 @@ typedef uint32_t DispatcherAction;
*/
class Dispatcher {
Packet* outbound; // current outbound packet
unsigned long outbound_expiry, outbound_start, total_air_time;
unsigned long outbound_expiry, outbound_start, total_air_time, rx_air_time;
unsigned long next_tx_time;
unsigned long cad_busy_start;
unsigned long radio_nonrx_start;
@@ -134,7 +134,9 @@ protected:
Dispatcher(Radio& radio, MillisecondClock& ms, PacketManager& mgr)
: _radio(&radio), _ms(&ms), _mgr(&mgr)
{
outbound = NULL; total_air_time = 0; next_tx_time = 0;
outbound = NULL;
total_air_time = rx_air_time = 0;
next_tx_time = 0;
cad_busy_start = 0;
next_floor_calib_time = next_agc_reset_time = 0;
_err_flags = 0;
@@ -167,6 +169,7 @@ public:
void sendPacket(Packet* packet, uint8_t priority, uint32_t delay_millis=0);
unsigned long getTotalAirTime() const { return total_air_time; } // in milliseconds
unsigned long getReceiveAirTime() const {return rx_air_time; }
uint32_t getNumSentFlood() const { return n_sent_flood; }
uint32_t getNumSentDirect() const { return n_sent_direct; }
uint32_t getNumRecvFlood() const { return n_recv_flood; }

View File

@@ -41,6 +41,8 @@ public:
virtual void onAfterTransmit() { }
virtual void reboot() = 0;
virtual void powerOff() { /* no op */ }
virtual uint32_t getGpio() { return 0; }
virtual void setGpio(uint32_t values) {}
virtual uint8_t getStartupReason() const = 0;
virtual bool startOTAUpdate(const char* id, char reply[]) { return false; } // not supported
};

View File

@@ -235,9 +235,13 @@ bool BaseChatMesh::onPeerPathRecv(mesh::Packet* packet, int sender_idx, const ui
ContactInfo& from = contacts[i];
// NOTE: for this impl, we just replace the current 'out_path' regardless, whenever sender sends us a new out_path.
return onContactPathRecv(from, packet->path, packet->path_len, path, path_len, extra_type, extra, extra_len);
}
bool BaseChatMesh::onContactPathRecv(ContactInfo& from, uint8_t* in_path, uint8_t in_path_len, uint8_t* out_path, uint8_t out_path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) {
// NOTE: default impl, we just replace the current 'out_path' regardless, whenever sender sends us a new out_path.
// FUTURE: could store multiple out_paths per contact, and try to find which is the 'best'(?)
memcpy(from.out_path, path, from.out_path_len = path_len); // store a copy of path, for sendDirect()
memcpy(from.out_path, out_path, from.out_path_len = out_path_len); // store a copy of path, for sendDirect()
from.lastmod = getRTCClock()->getCurrentTime();
onContactPathUpdated(from);

View File

@@ -88,11 +88,14 @@ protected:
memset(connections, 0, sizeof(connections));
}
void resetContacts() { num_contacts = 0; }
// 'UI' concepts, for sub-classes to implement
virtual bool isAutoAddEnabled() const { return true; }
virtual void onDiscoveredContact(ContactInfo& contact, bool is_new, uint8_t path_len, const uint8_t* path) = 0;
virtual bool processAck(const uint8_t *data) = 0;
virtual void onContactPathUpdated(const ContactInfo& contact) = 0;
virtual bool onContactPathRecv(ContactInfo& from, uint8_t* in_path, uint8_t in_path_len, uint8_t* out_path, uint8_t out_path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len);
virtual void onMessageRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text) = 0;
virtual void onCommandDataRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text) = 0;
virtual void onSignedMessageRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const uint8_t *sender_prefix, const char *text) = 0;

View File

@@ -165,6 +165,17 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
}
} else if (memcmp(command, "neighbors", 9) == 0) {
_callbacks->formatNeighborsReply(reply);
} else if (memcmp(command, "neighbor.remove ", 16) == 0) {
const char* hex = &command[16];
uint8_t pubkey[PUB_KEY_SIZE];
int hex_len = min((int)strlen(hex), PUB_KEY_SIZE*2);
int pubkey_len = hex_len / 2;
if (mesh::Utils::fromHex(pubkey, pubkey_len, hex)) {
_callbacks->removeNeighbor(pubkey, pubkey_len);
strcpy(reply, "OK");
} else {
strcpy(reply, "ERR: bad pubkey");
}
} else if (memcmp(command, "tempradio ", 10) == 0) {
strcpy(tmp, &command[10]);
const char *parts[5];
@@ -206,6 +217,11 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
sprintf(reply, "> %d", ((uint32_t) _prefs->advert_interval) * 2);
} else if (memcmp(config, "guest.password", 14) == 0) {
sprintf(reply, "> %s", _prefs->guest_password);
} else if (sender_timestamp == 0 && memcmp(config, "prv.key", 7) == 0) { // from serial command line only
uint8_t prv_key[PRV_KEY_SIZE];
int len = _callbacks->getSelfId().writeTo(prv_key, PRV_KEY_SIZE);
mesh::Utils::toHex(tmp, prv_key, len);
sprintf(reply, "> %s", tmp);
} else if (memcmp(config, "name", 4) == 0) {
sprintf(reply, "> %s", _prefs->node_name);
} else if (memcmp(config, "repeat", 6) == 0) {
@@ -233,7 +249,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->freq));
} else if (memcmp(config, "public.key", 10) == 0) {
strcpy(reply, "> ");
mesh::Utils::toHex(&reply[2], _callbacks->getSelfIdPubKey(), PUB_KEY_SIZE);
mesh::Utils::toHex(&reply[2], _callbacks->getSelfId().pub_key, PUB_KEY_SIZE);
} else if (memcmp(config, "role", 4) == 0) {
sprintf(reply, "> %s", _callbacks->getRole());
} else {
@@ -252,7 +268,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
} else if (memcmp(config, "agc.reset.interval ", 19) == 0) {
_prefs->agc_reset_interval = atoi(&config[19]) / 4;
savePrefs();
strcpy(reply, "OK");
sprintf(reply, "OK - interval rounded to %d", ((uint32_t) _prefs->agc_reset_interval) * 4);
} else if (memcmp(config, "multi.acks ", 11) == 0) {
_prefs->multi_acks = atoi(&config[11]);
savePrefs();
@@ -285,6 +301,15 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
StrHelper::strncpy(_prefs->guest_password, &config[15], sizeof(_prefs->guest_password));
savePrefs();
strcpy(reply, "OK");
} else if (sender_timestamp == 0 && memcmp(config, "prv.key ", 8) == 0) { // from serial command line only
uint8_t prv_key[PRV_KEY_SIZE];
bool success = mesh::Utils::fromHex(prv_key, PRV_KEY_SIZE, &config[8]);
if (success) {
_callbacks->getSelfId().readFrom(prv_key, PRV_KEY_SIZE);
strcpy(reply, "OK");
} else {
strcpy(reply, "Error, invalid key");
}
} else if (memcmp(config, "name ", 5) == 0) {
StrHelper::strncpy(_prefs->node_name, &config[5], sizeof(_prefs->node_name));
savePrefs();

View File

@@ -43,7 +43,10 @@ public:
virtual void dumpLogFile() = 0;
virtual void setTxPower(uint8_t power_dbm) = 0;
virtual void formatNeighborsReply(char *reply) = 0;
virtual const uint8_t* getSelfIdPubKey() = 0;
virtual void removeNeighbor(const uint8_t* pubkey, int key_len) {
// no op by default
};
virtual mesh::LocalIdentity& getSelfId() = 0;
virtual void clearStats() = 0;
virtual void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) = 0;
};
@@ -53,7 +56,7 @@ class CommonCLI {
NodePrefs* _prefs;
CommonCLICallbacks* _callbacks;
mesh::MainBoard* _board;
char tmp[80];
char tmp[PRV_KEY_SIZE*2 + 4];
mesh::RTCClock* getRTCClock() { return _rtc; }
void savePrefs();

View File

@@ -5,25 +5,25 @@
class RefCountedDigitalPin {
uint8_t _pin;
int8_t _claims = 0;
uint8_t _active = 0;
public:
RefCountedDigitalPin(uint8_t pin): _pin(pin) { }
RefCountedDigitalPin(uint8_t pin,uint8_t active=HIGH): _pin(pin), _active(active) { }
void begin() {
pinMode(_pin, OUTPUT);
digitalWrite(_pin, LOW); // initial state
digitalWrite(_pin, !_active); // initial state
}
void claim() {
_claims++;
if (_claims > 0) {
digitalWrite(_pin, HIGH);
digitalWrite(_pin, _active);
}
}
void release() {
_claims--;
if (_claims == 0) {
digitalWrite(_pin, LOW);
digitalWrite(_pin, !_active);
}
}
};

View File

@@ -83,6 +83,7 @@ void SerialBLEInterface::onConnect(BLEServer* pServer) {
void SerialBLEInterface::onConnect(BLEServer* pServer, esp_ble_gatts_cb_param_t *param) {
BLE_DEBUG_PRINTLN("onConnect(), conn_id=%d, mtu=%d", param->connect.conn_id, pServer->getPeerMTU(param->connect.conn_id));
last_conn_id = param->connect.conn_id;
}
void SerialBLEInterface::onMtuChanged(BLEServer* pServer, esp_ble_gatts_cb_param_t* param) {
@@ -143,6 +144,7 @@ void SerialBLEInterface::disable() {
BLE_DEBUG_PRINTLN("SerialBLEInterface::disable");
pServer->getAdvertising()->stop();
pServer->disconnect(last_conn_id);
pService->stop();
oldDeviceConnected = deviceConnected = false;
adv_restart_time = 0;

View File

@@ -13,6 +13,7 @@ class SerialBLEInterface : public BaseSerialInterface, BLESecurityCallbacks, BLE
bool deviceConnected;
bool oldDeviceConnected;
bool _isEnabled;
uint16_t last_conn_id;
uint32_t _pin_code;
unsigned long _last_write;
unsigned long adv_restart_time;
@@ -56,6 +57,7 @@ public:
adv_restart_time = 0;
_isEnabled = false;
_last_write = 0;
last_conn_id = 0;
send_queue_len = recv_queue_len = 0;
}

View File

@@ -44,7 +44,18 @@ bool SerialWifiInterface::isWriteBusy() const {
}
size_t SerialWifiInterface::checkRecvFrame(uint8_t dest[]) {
if (!client) client = server.available();
// check if new client connected
auto newClient = server.available();
if (newClient) {
// disconnect existing client
deviceConnected = false;
client.stop();
// switch active connection to new client
client = newClient;
}
if (client.connected()) {
if (!deviceConnected) {

View File

@@ -115,6 +115,20 @@ void SerialBLEInterface::enable() {
void SerialBLEInterface::disable() {
_isEnabled = false;
BLE_DEBUG_PRINTLN("SerialBLEInterface::disable");
#ifdef RAK_BOARD
Bluefruit.disconnect(Bluefruit.connHandle());
#else
uint16_t conn_id;
if (Bluefruit.getConnectedHandles(&conn_id, 1) > 0) {
Bluefruit.disconnect(conn_id);
}
#endif
Bluefruit.Advertising.restartOnDisconnect(false);
Bluefruit.Advertising.stop();
Bluefruit.Advertising.clearData();
stopAdv();
}

View File

@@ -26,6 +26,45 @@ void T114Board::begin() {
pinMode(PIN_VBAT_READ, INPUT);
// Enable SoftDevice low-power mode
sd_power_mode_set(NRF_POWER_MODE_LOWPWR);
// Enable DC/DC converter for better efficiency (REG1 stage)
NRF_POWER->DCDCEN = 1;
// Power down unused communication peripherals
// UART1 - Not used on T114
NRF_UARTE1->ENABLE = 0;
// SPIM2/SPIS2 - Not used (SPI is on SPIM0)
NRF_SPIM2->ENABLE = 0;
NRF_SPIS2->ENABLE = 0;
// TWI1 (I2C1) - Not used (I2C is on TWI0)
NRF_TWIM1->ENABLE = 0;
NRF_TWIS1->ENABLE = 0;
// PWM modules - Not used for standard T114 functions
NRF_PWM1->ENABLE = 0;
NRF_PWM2->ENABLE = 0;
NRF_PWM3->ENABLE = 0;
// PDM (Digital Microphone Interface) - Not used
NRF_PDM->ENABLE = 0;
// I2S - Not used
NRF_I2S->ENABLE = 0;
// QSPI - Not used (no external flash)
NRF_QSPI->ENABLE = 0;
// Disable unused analog peripherals
// SAADC channels - only keep what's needed for battery monitoring
NRF_SAADC->ENABLE = 0; // Re-enable only when needed for measurements
// COMP - Comparator not used
NRF_COMP->ENABLE = 0;
#if defined(PIN_BOARD_SDA) && defined(PIN_BOARD_SCL)
Wire.setPins(PIN_BOARD_SDA, PIN_BOARD_SCL);
#endif

View File

@@ -40,6 +40,9 @@ public:
uint16_t getBattMilliVolts() override {
int adcvalue = 0;
NRF_SAADC->ENABLE = 1;
analogReadResolution(12);
analogReference(AR_INTERNAL_3_0);
pinMode(PIN_BAT_CTL, OUTPUT); // battery adc can be read only ctrl pin 6 set to high
@@ -49,6 +52,8 @@ public:
adcvalue = analogRead(PIN_VBAT_READ);
digitalWrite(6, 0);
NRF_SAADC->ENABLE = 0;
return (uint16_t)((float)adcvalue * MV_LSB * 4.9);
}
@@ -60,5 +65,9 @@ public:
NVIC_SystemReset();
}
void powerOff() override {
sd_power_system_off();
}
bool startOTAUpdate(const char* id, char reply[]) override;
};

View File

@@ -43,6 +43,25 @@ public:
return "LilyGo T-Echo";
}
void powerOff() override {
#ifdef LED_RED
digitalWrite(LED_RED, LOW);
#endif
#ifdef LED_GREEN
digitalWrite(LED_GREEN, LOW);
#endif
#ifdef LED_BLUE
digitalWrite(LED_BLUE, LOW);
#endif
#ifdef DISP_BACKLIGHT
digitalWrite(DISP_BACKLIGHT, LOW);
#endif
#ifdef PIN_PWR_EN
digitalWrite(PIN_PWR_EN, LOW);
#endif
sd_power_system_off();
}
void reboot() override {
NVIC_SystemReset();
}

View File

@@ -55,4 +55,16 @@ public:
void reboot() override {
NVIC_SystemReset();
}
void powerOff() override {
// turn off all leds, sd_power_system_off will not do this for us
#ifdef P_LORA_TX_LED
digitalWrite(P_LORA_TX_LED, LOW);
#endif
// power off board
sd_power_system_off();
}
};

View File

@@ -35,6 +35,12 @@ static Adafruit_BMP280 BMP280;
static Adafruit_SHTC3 SHTC3;
#endif
#if ENV_INCLUDE_SHT4X
#define TELEM_SHT4X_ADDRESS 0x44 //0x44 - 0x46
#include <SensirionI2cSht4x.h>
static SensirionI2cSht4x SHT4X;
#endif
#if ENV_INCLUDE_LPS22HB
#include <Arduino_LPS22HB.h>
#endif
@@ -53,6 +59,20 @@ static Adafruit_INA3221 INA3221;
static Adafruit_INA219 INA219(TELEM_INA219_ADDRESS);
#endif
#if ENV_INCLUDE_INA260
#define TELEM_INA260_ADDRESS 0x41 // INA260 single channel current sensor I2C address
#include <Adafruit_INA260.h>
static Adafruit_INA260 INA260;
#endif
#if ENV_INCLUDE_INA226
#define TELEM_INA226_ADDRESS 0x44
#define TELEM_INA226_SHUNT_VALUE 0.100
#define TELEM_INA226_MAX_AMP 0.8
#include <INA226.h>
static INA226 INA226(TELEM_INA226_ADDRESS);
#endif
#if ENV_INCLUDE_MLX90614
#define TELEM_MLX90614_ADDRESS 0x5A // MLX90614 IR temperature sensor I2C address
#include <Adafruit_MLX90614.h>
@@ -130,6 +150,21 @@ bool EnvironmentSensorManager::begin() {
}
#endif
#if ENV_INCLUDE_SHT4X
SHT4X.begin(*TELEM_WIRE, TELEM_SHT4X_ADDRESS);
uint32_t serialNumber = 0;
int16_t sht4x_error;
sht4x_error = SHT4X.serialNumber(serialNumber);
if (sht4x_error == 0) {
MESH_DEBUG_PRINTLN("Found SHT4X at address: %02X", TELEM_SHT4X_ADDRESS);
SHT4X_initialized = true;
} else {
SHT4X_initialized = false;
MESH_DEBUG_PRINTLN("SHT4X was not found at I2C address %02X", TELEM_SHT4X_ADDRESS);
}
#endif
#if ENV_INCLUDE_LPS22HB
if (BARO.begin()) {
MESH_DEBUG_PRINTLN("Found sensor: LPS22HB");
@@ -165,6 +200,27 @@ bool EnvironmentSensorManager::begin() {
}
#endif
#if ENV_INCLUDE_INA260
if (INA260.begin(TELEM_INA260_ADDRESS, TELEM_WIRE)) {
MESH_DEBUG_PRINTLN("Found INA260 at address: %02X", TELEM_INA260_ADDRESS);
INA260_initialized = true;
} else {
INA260_initialized = false;
MESH_DEBUG_PRINTLN("INA260 was not found at I2C address %02X", TELEM_INA219_ADDRESS);
}
#endif
#if ENV_INCLUDE_INA226
if (INA226.begin()) {
MESH_DEBUG_PRINTLN("Found INA226 at address: %02X", TELEM_INA226_ADDRESS);
INA226.setMaxCurrentShunt(TELEM_INA226_MAX_AMP, TELEM_INA226_SHUNT_VALUE);
INA226_initialized = true;
} else {
INA226_initialized = false;
MESH_DEBUG_PRINTLN("INA226 was not found at I2C address %02X", TELEM_INA226_ADDRESS);
}
#endif
#if ENV_INCLUDE_MLX90614
if (MLX90614.begin(TELEM_MLX90614_ADDRESS, TELEM_WIRE)) {
MESH_DEBUG_PRINTLN("Found MLX90614 at address: %02X", TELEM_MLX90614_ADDRESS);
@@ -219,7 +275,7 @@ bool EnvironmentSensorManager::querySensors(uint8_t requester_permissions, Cayen
if (BMP280_initialized) {
telemetry.addTemperature(TELEM_CHANNEL_SELF, BMP280.readTemperature());
telemetry.addBarometricPressure(TELEM_CHANNEL_SELF, BMP280.readPressure()/100);
telemetry.addAltitude(TELEM_CHANNEL_SELF, BME280.readAltitude(TELEM_BME280_SEALEVELPRESSURE_HPA));
telemetry.addAltitude(TELEM_CHANNEL_SELF, BMP280.readAltitude(TELEM_BMP280_SEALEVELPRESSURE_HPA));
}
#endif
@@ -233,6 +289,18 @@ bool EnvironmentSensorManager::querySensors(uint8_t requester_permissions, Cayen
}
#endif
#if ENV_INCLUDE_SHT4X
if (SHT4X_initialized) {
float sht4x_humidity, sht4x_temperature;
int16_t sht4x_error;
sht4x_error = SHT4X.measureLowestPrecision(sht4x_temperature, sht4x_humidity);
if (sht4x_error == 0) {
telemetry.addTemperature(TELEM_CHANNEL_SELF, sht4x_temperature);
telemetry.addRelativeHumidity(TELEM_CHANNEL_SELF, sht4x_humidity);
}
}
#endif
#if ENV_INCLUDE_LPS22HB
if (LPS22HB_initialized) {
telemetry.addTemperature(TELEM_CHANNEL_SELF, BARO.readTemperature());
@@ -265,6 +333,24 @@ bool EnvironmentSensorManager::querySensors(uint8_t requester_permissions, Cayen
}
#endif
#if ENV_INCLUDE_INA260
if (INA260_initialized) {
telemetry.addVoltage(next_available_channel, INA260.readBusVoltage() / 1000);
telemetry.addCurrent(next_available_channel, INA260.readCurrent() / 1000);
telemetry.addPower(next_available_channel, INA260.readPower() / 1000);
next_available_channel++;
}
#endif
#if ENV_INCLUDE_INA226
if (INA226_initialized) {
telemetry.addVoltage(next_available_channel, INA226.getBusVoltage());
telemetry.addCurrent(next_available_channel, INA226.getCurrent_mA() / 1000.0);
telemetry.addPower(next_available_channel, INA226.getPower_mW() / 1000.0);
next_available_channel++;
}
#endif
#if ENV_INCLUDE_MLX90614
if (MLX90614_initialized) {
telemetry.addTemperature(TELEM_CHANNEL_SELF, MLX90614.readObjectTempC());

View File

@@ -13,10 +13,13 @@ protected:
bool BMP280_initialized = false;
bool INA3221_initialized = false;
bool INA219_initialized = false;
bool INA260_initialized = false;
bool INA226_initialized = false;
bool SHTC3_initialized = false;
bool LPS22HB_initialized = false;
bool MLX90614_initialized = false;
bool VL53L0X_initialized = false;
bool SHT4X_initialized = false;
bool gps_detected = false;
bool gps_active = false;

View File

@@ -21,9 +21,15 @@ public:
virtual void setColor(Color c) = 0;
virtual void setCursor(int x, int y) = 0;
virtual void print(const char* str) = 0;
virtual void printWordWrap(const char* str, int max_width) { print(str); } // fallback to basic print() if no override
virtual void fillRect(int x, int y, int w, int h) = 0;
virtual void drawRect(int x, int y, int w, int h) = 0;
virtual void drawXbm(int x, int y, const uint8_t* bits, int w, int h) = 0;
virtual uint16_t getTextWidth(const char* str) = 0;
virtual void drawTextCentered(int mid_x, int y, const char* str) { // helper method (override to optimise)
int w = getTextWidth(str);
setCursor(mid_x - w/2, y);
print(str);
}
virtual void endFrame() = 0;
};

View File

@@ -2,20 +2,58 @@
#include "../../MeshCore.h"
BaseDisplay* E213Display::detectEInk()
{
// Test 1: Logic of BUSY pin
// Determines controller IC manufacturer
// Fitipower: busy when LOW
// Solomon Systech: busy when HIGH
// Force display BUSY by holding reset pin active
pinMode(DISP_RST, OUTPUT);
digitalWrite(DISP_RST, LOW);
delay(10);
// Read whether pin is HIGH or LOW while busy
pinMode(DISP_BUSY, INPUT);
bool busyLogic = digitalRead(DISP_BUSY);
// Test complete. Release pin
pinMode(DISP_RST, INPUT);
if (busyLogic == LOW) {
#ifdef VISION_MASTER_E213
return new EInkDisplay_VisionMasterE213 ;
#else
return new EInkDisplay_WirelessPaperV1_1 ;
#endif
} else {// busy HIGH
#ifdef VISION_MASTER_E213
return new EInkDisplay_VisionMasterE213V1_1 ;
#else
return new EInkDisplay_WirelessPaperV1_1_1 ;
#endif
}
}
bool E213Display::begin() {
if (_init) return true;
powerOn();
display.begin();
if(display==NULL) {
display = detectEInk();
}
display->begin();
// Set to landscape mode rotated 180 degrees
display.setRotation(3);
display->setRotation(3);
_init = true;
_isOn = true;
clear();
display.fastmodeOn(); // Enable fast mode for quicker (partial) updates
display->fastmodeOn(); // Enable fast mode for quicker (partial) updates
return true;
}
@@ -23,15 +61,23 @@ bool E213Display::begin() {
void E213Display::powerOn() {
#ifdef PIN_VEXT_EN
pinMode(PIN_VEXT_EN, OUTPUT);
#ifdef PIN_VEXT_EN_ACTIVE
digitalWrite(PIN_VEXT_EN, PIN_VEXT_EN_ACTIVE);
#else
digitalWrite(PIN_VEXT_EN, LOW); // Active low
#endif
delay(50); // Allow power to stabilize
#endif
}
void E213Display::powerOff() {
#ifdef PIN_VEXT_EN
#ifdef PIN_VEXT_EN_ACTIVE
digitalWrite(PIN_VEXT_EN, !PIN_VEXT_EN_ACTIVE);
#else
digitalWrite(PIN_VEXT_EN, HIGH); // Turn off power
#endif
#endif
}
void E213Display::turnOn() {
@@ -46,21 +92,23 @@ void E213Display::turnOff() {
}
void E213Display::clear() {
display.clear();
display->clear();
}
void E213Display::startFrame(Color bkg) {
// Fill screen with white first to ensure clean background
display.fillRect(0, 0, width(), height(), WHITE);
display->fillRect(0, 0, width(), height(), WHITE);
if (bkg == LIGHT) {
// Fill with black if light background requested (inverted for e-ink)
display.fillRect(0, 0, width(), height(), BLACK);
display->fillRect(0, 0, width(), height(), BLACK);
}
}
void E213Display::setTextSize(int sz) {
// The library handles text size internally
display.setTextSize(sz);
display->setTextSize(sz);
}
void E213Display::setColor(Color c) {
@@ -68,19 +116,19 @@ void E213Display::setColor(Color c) {
}
void E213Display::setCursor(int x, int y) {
display.setCursor(x, y);
display->setCursor(x, y);
}
void E213Display::print(const char *str) {
display.print(str);
display->print(str);
}
void E213Display::fillRect(int x, int y, int w, int h) {
display.fillRect(x, y, w, h, BLACK);
display->fillRect(x, y, w, h, BLACK);
}
void E213Display::drawRect(int x, int y, int w, int h) {
display.drawRect(x, y, w, h, BLACK);
display->drawRect(x, y, w, h, BLACK);
}
void E213Display::drawXbm(int x, int y, const uint8_t *bits, int w, int h) {
@@ -98,7 +146,7 @@ void E213Display::drawXbm(int x, int y, const uint8_t *bits, int w, int h) {
// If the bit is set, draw the pixel
if (bitSet) {
display.drawPixel(x + bx, y + by, BLACK);
display->drawPixel(x + bx, y + by, BLACK);
}
}
}
@@ -107,10 +155,10 @@ void E213Display::drawXbm(int x, int y, const uint8_t *bits, int w, int h) {
uint16_t E213Display::getTextWidth(const char *str) {
int16_t x1, y1;
uint16_t w, h;
display.getTextBounds(str, 0, 0, &x1, &y1, &w, &h);
display->getTextBounds(str, 0, 0, &x1, &y1, &w, &h);
return w;
}
void E213Display::endFrame() {
display.update();
display->update();
}

View File

@@ -8,13 +8,17 @@
// Display driver for E213 e-ink display
class E213Display : public DisplayDriver {
EInkDisplay_VisionMasterE213 display;
BaseDisplay* display=NULL;
bool _init = false;
bool _isOn = false;
public:
E213Display() : DisplayDriver(250, 122) {}
~E213Display(){
if(display!=NULL) {
delete display;
}
}
bool begin();
bool isOn() override { return _isOn; }
void turnOn() override;
@@ -32,6 +36,7 @@ public:
void endFrame() override;
private:
BaseDisplay* detectEInk();
void powerOn();
void powerOff();
};

View File

@@ -0,0 +1,116 @@
#include "E290Display.h"
#include "../../MeshCore.h"
bool E290Display::begin() {
if (_init) return true;
powerOn();
display.begin();
// Set to landscape mode rotated 180 degrees
display.setRotation(3);
_init = true;
_isOn = true;
clear();
display.fastmodeOn(); // Enable fast mode for quicker (partial) updates
return true;
}
void E290Display::powerOn() {
#ifdef PIN_VEXT_EN
pinMode(PIN_VEXT_EN, OUTPUT);
digitalWrite(PIN_VEXT_EN, PIN_VEXT_EN_ACTIVE);
delay(50); // Allow power to stabilize
#endif
}
void E290Display::powerOff() {
#ifdef PIN_VEXT_EN
digitalWrite(PIN_VEXT_EN, !PIN_VEXT_EN_ACTIVE); // Turn off power
#endif
}
void E290Display::turnOn() {
if (!_init) begin();
powerOn();
_isOn = true;
}
void E290Display::turnOff() {
powerOff();
_isOn = false;
}
void E290Display::clear() {
display.clear();
}
void E290Display::startFrame(Color bkg) {
// Fill screen with white first to ensure clean background
display.fillRect(0, 0, width(), height(), WHITE);
if (bkg == LIGHT) {
// Fill with black if light background requested (inverted for e-ink)
display.fillRect(0, 0, width(), height(), BLACK);
}
}
void E290Display::setTextSize(int sz) {
// The library handles text size internally
display.setTextSize(sz);
}
void E290Display::setColor(Color c) {
// implemented in individual display methods
}
void E290Display::setCursor(int x, int y) {
display.setCursor(x, y);
}
void E290Display::print(const char *str) {
display.print(str);
}
void E290Display::fillRect(int x, int y, int w, int h) {
display.fillRect(x, y, w, h, BLACK);
}
void E290Display::drawRect(int x, int y, int w, int h) {
display.drawRect(x, y, w, h, BLACK);
}
void E290Display::drawXbm(int x, int y, const uint8_t *bits, int w, int h) {
// Width in bytes for bitmap processing
uint16_t widthInBytes = (w + 7) / 8;
// Process the bitmap row by row
for (int by = 0; by < h; by++) {
// Scan across the row bit by bit
for (int bx = 0; bx < w; bx++) {
// Get the current bit using MSB ordering (like GxEPDDisplay)
uint16_t byteOffset = (by * widthInBytes) + (bx / 8);
uint8_t bitMask = 0x80 >> (bx & 7);
bool bitSet = bits[byteOffset] & bitMask;
// If the bit is set, draw the pixel
if (bitSet) {
display.drawPixel(x + bx, y + by, BLACK);
}
}
}
}
uint16_t E290Display::getTextWidth(const char *str) {
int16_t x1, y1;
uint16_t w, h;
display.getTextBounds(str, 0, 0, &x1, &y1, &w, &h);
return w;
}
void E290Display::endFrame() {
display.update();
}

View File

@@ -0,0 +1,37 @@
#pragma once
#include "DisplayDriver.h"
#include <SPI.h>
#include <Wire.h>
#include <heltec-eink-modules.h>
// Display driver for E290 e-ink display
class E290Display : public DisplayDriver {
EInkDisplay_VisionMasterE290 display;
bool _init = false;
bool _isOn = false;
public:
E290Display() : DisplayDriver(296, 128) {}
bool begin();
bool isOn() override { return _isOn; }
void turnOn() override;
void turnOff() override;
void clear() override;
void startFrame(Color bkg = DARK) override;
void setTextSize(int sz) override;
void setColor(Color 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;
private:
void powerOn();
void powerOff();
};

View File

@@ -29,8 +29,8 @@ void GxEPDDisplay::turnOn() {
if (!_init) begin();
#if DISP_BACKLIGHT
digitalWrite(DISP_BACKLIGHT, HIGH);
_isOn = true;
#endif
_isOn = true;
}
void GxEPDDisplay::turnOff() {
@@ -47,6 +47,7 @@ void GxEPDDisplay::clear() {
void GxEPDDisplay::startFrame(Color bkg) {
display.fillScreen(GxEPD_WHITE);
display.setTextColor(_curr_color = GxEPD_BLACK);
}
void GxEPDDisplay::setTextSize(int sz) {
@@ -67,7 +68,12 @@ void GxEPDDisplay::setTextSize(int sz) {
}
void GxEPDDisplay::setColor(Color c) {
display.setTextColor(GxEPD_BLACK);
// colours need to be inverted for epaper displays
if (c == DARK) {
display.setTextColor(_curr_color = GxEPD_WHITE);
} else {
display.setTextColor(_curr_color = GxEPD_BLACK);
}
}
void GxEPDDisplay::setCursor(int x, int y) {
@@ -79,11 +85,11 @@ void GxEPDDisplay::print(const char* str) {
}
void GxEPDDisplay::fillRect(int x, int y, int w, int h) {
display.fillRect(x*SCALE_X, y*SCALE_Y, w*SCALE_X, h*SCALE_Y, GxEPD_BLACK);
display.fillRect(x*SCALE_X, y*SCALE_Y, w*SCALE_X, h*SCALE_Y, _curr_color);
}
void GxEPDDisplay::drawRect(int x, int y, int w, int h) {
display.drawRect(x*SCALE_X, y*SCALE_Y, w*SCALE_X, h*SCALE_Y, GxEPD_BLACK);
display.drawRect(x*SCALE_X, y*SCALE_Y, w*SCALE_X, h*SCALE_Y, _curr_color);
}
void GxEPDDisplay::drawXbm(int x, int y, const uint8_t* bits, int w, int h) {
@@ -116,7 +122,7 @@ void GxEPDDisplay::drawXbm(int x, int y, const uint8_t* bits, int w, int h) {
// If the bit is set, draw a block of pixels
if (bitSet) {
// Draw the block as a filled rectangle
display.fillRect(x1, y1, block_w, block_h, GxEPD_BLACK);
display.fillRect(x1, y1, block_w, block_h, _curr_color);
}
}
}
@@ -126,7 +132,7 @@ uint16_t GxEPDDisplay::getTextWidth(const char* str) {
int16_t x1, y1;
uint16_t w, h;
display.getTextBounds(str, 0, 0, &x1, &y1, &w, &h);
return w / SCALE_X;
return ceil((w + 1) / SCALE_X);
}
void GxEPDDisplay::endFrame() {

View File

@@ -28,6 +28,7 @@ class GxEPDDisplay : public DisplayDriver {
GxEPD2_BW<GxEPD2_150_BN, 200> display;
bool _init = false;
bool _isOn = false;
uint16_t _curr_color;
public:
// there is a margin in y...

View File

@@ -0,0 +1,75 @@
#include "MomentaryButton.h"
MomentaryButton::MomentaryButton(int8_t pin, int long_press_millis, bool reverse, bool pulldownup) {
_pin = pin;
_reverse = reverse;
_pull = pulldownup;
down_at = 0;
prev = _reverse ? HIGH : LOW;
cancel = 0;
_long_millis = long_press_millis;
}
void MomentaryButton::begin() {
if (_pin >= 0) {
pinMode(_pin, _pull ? (_reverse ? INPUT_PULLUP : INPUT_PULLDOWN) : INPUT);
}
}
bool MomentaryButton::isPressed() const {
return isPressed(digitalRead(_pin));
}
void MomentaryButton::cancelClick() {
cancel = 1;
}
bool MomentaryButton::isPressed(int level) const {
if (_reverse) {
return level == LOW;
} else {
return level != LOW;
}
}
int MomentaryButton::check(bool repeat_click) {
if (_pin < 0) return BUTTON_EVENT_NONE;
int event = BUTTON_EVENT_NONE;
int btn = digitalRead(_pin);
if (btn != prev) {
if (isPressed(btn)) {
down_at = millis();
} else {
// button UP
if (_long_millis > 0) {
if (down_at > 0 && (unsigned long)(millis() - down_at) < _long_millis) { // only a CLICK if still within the long_press millis
event = BUTTON_EVENT_CLICK;
}
} else {
event = BUTTON_EVENT_CLICK; // any UP results in CLICK event when NOT using long_press feature
}
if (event == BUTTON_EVENT_CLICK && cancel) {
event = BUTTON_EVENT_NONE;
}
down_at = 0;
}
prev = btn;
}
if (!isPressed(btn) && cancel) { // always clear the pending 'cancel' once button is back in UP state
cancel = 0;
}
if (_long_millis > 0 && down_at > 0 && (unsigned long)(millis() - down_at) >= _long_millis) {
event = BUTTON_EVENT_LONG_PRESS;
down_at = 0;
}
if (down_at > 0 && repeat_click) {
unsigned long diff = (unsigned long)(millis() - down_at);
if (diff >= 700) {
event = BUTTON_EVENT_CLICK; // wait 700 millis before repeating the click events
}
}
return event;
}

View File

@@ -0,0 +1,25 @@
#pragma once
#include <Arduino.h>
#define BUTTON_EVENT_NONE 0
#define BUTTON_EVENT_CLICK 1
#define BUTTON_EVENT_LONG_PRESS 2
class MomentaryButton {
int8_t _pin;
int8_t prev, cancel;
bool _reverse, _pull;
int _long_millis;
unsigned long down_at;
bool isPressed(int level) const;
public:
MomentaryButton(int8_t pin, int long_press_mills=0, bool reverse=false, bool pulldownup=false);
void begin();
int check(bool repeat_click=false); // returns one of BUTTON_EVENT_*
void cancelClick(); // suppress next BUTTON_EVENT_CLICK (if already in DOWN state)
uint8_t getPin() { return _pin; }
bool isPressed() const;
};

View File

@@ -7,6 +7,9 @@ bool SSD1306Display::i2c_probe(TwoWire& wire, uint8_t addr) {
}
bool SSD1306Display::begin() {
#ifdef DISPLAY_ROTATION
display.setRotation(DISPLAY_ROTATION);
#endif
return display.begin(SSD1306_SWITCHCAPVCC, DISPLAY_ADDRESS, true, false) && i2c_probe(Wire, DISPLAY_ADDRESS);
}

View File

@@ -18,7 +18,11 @@ bool ST7789Display::begin() {
pinMode(PIN_TFT_VDD_CTL, OUTPUT);
pinMode(PIN_TFT_LEDA_CTL, OUTPUT);
digitalWrite(PIN_TFT_VDD_CTL, LOW);
#ifdef PIN_TFT_LEDA_CTL_ACTIVE
digitalWrite(PIN_TFT_LEDA_CTL, PIN_TFT_LEDA_CTL_ACTIVE);
#else
digitalWrite(PIN_TFT_LEDA_CTL, LOW);
#endif
digitalWrite(PIN_TFT_RST, HIGH);
display.init();
@@ -43,15 +47,22 @@ void ST7789Display::turnOn() {
delay(20);
// Now turn on the backlight
#ifdef PIN_TFT_LEDA_CTL_ACTIVE
digitalWrite(PIN_TFT_LEDA_CTL, PIN_TFT_LEDA_CTL_ACTIVE);
#else
digitalWrite(PIN_TFT_LEDA_CTL, LOW);
#endif
_isOn = true;
}
}
void ST7789Display::turnOff() {
digitalWrite(PIN_TFT_VDD_CTL, HIGH);
#ifdef PIN_TFT_LEDA_CTL_ACTIVE
digitalWrite(PIN_TFT_LEDA_CTL, !PIN_TFT_LEDA_CTL_ACTIVE);
#else
digitalWrite(PIN_TFT_LEDA_CTL, HIGH);
#endif
digitalWrite(PIN_TFT_RST, LOW);
_isOn = false;
}
@@ -62,6 +73,9 @@ void ST7789Display::clear() {
void ST7789Display::startFrame(Color bkg) {
display.clear();
_color = ST77XX_WHITE;
display.setRGB(_color);
display.setFont(ArialMT_Plain_16);
}
void ST7789Display::setTextSize(int sz) {
@@ -81,7 +95,9 @@ void ST7789Display::setColor(Color c) {
switch (c) {
case DisplayDriver::DARK :
_color = ST77XX_BLACK;
display.setColor(OLEDDISPLAY_COLOR::BLACK);
break;
#if 0
case DisplayDriver::LIGHT :
_color = ST77XX_WHITE;
break;
@@ -100,8 +116,10 @@ void ST7789Display::setColor(Color c) {
case DisplayDriver::ORANGE :
_color = ST77XX_ORANGE;
break;
#endif
default:
_color = ST77XX_WHITE;
display.setColor(OLEDDISPLAY_COLOR::WHITE);
break;
}
display.setRGB(_color);
@@ -116,6 +134,10 @@ void ST7789Display::print(const char* str) {
display.drawString(_x, _y, str);
}
void ST7789Display::printWordWrap(const char* str, int max_width) {
display.drawStringMaxWidth(_x, _y, max_width*SCALE_X, str);
}
void ST7789Display::fillRect(int x, int y, int w, int h) {
display.fillRect(x*SCALE_X + X_OFFSET, y*SCALE_Y + Y_OFFSET, w*SCALE_X, h*SCALE_Y);
}

View File

@@ -4,7 +4,7 @@
#include <Wire.h>
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <ST7789Spi.h>
#include "ST7789Spi.h"
class ST7789Display : public DisplayDriver {
ST7789Spi display;
@@ -14,8 +14,11 @@ class ST7789Display : public DisplayDriver {
bool i2c_probe(TwoWire& wire, uint8_t addr);
public:
#ifdef HELTEC_VISION_MASTER_T190
ST7789Display() : DisplayDriver(128, 64), display(&SPI, PIN_TFT_RST, PIN_TFT_DC, PIN_TFT_CS, GEOMETRY_RAWMODE, 320, 170,PIN_TFT_SDA,-1,PIN_TFT_SCL) {_isOn = false;}
#else
ST7789Display() : DisplayDriver(128, 64), display(&SPI1, PIN_TFT_RST, PIN_TFT_DC, PIN_TFT_CS, GEOMETRY_RAWMODE, 240, 135) {_isOn = false;}
#endif
bool begin();
bool isOn() override { return _isOn; }
@@ -27,6 +30,7 @@ public:
void setColor(Color c) override;
void setCursor(int x, int y) override;
void print(const char* str) override;
void printWordWrap(const char* str, int max_width) 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;

21
src/helpers/ui/UIScreen.h Normal file
View File

@@ -0,0 +1,21 @@
#pragma once
#include "DisplayDriver.h"
#define KEY_LEFT 0xB4
#define KEY_UP 0xB5
#define KEY_DOWN 0xB6
#define KEY_RIGHT 0xB7
#define KEY_SELECT 10
#define KEY_ENTER 13
#define KEY_BACK 27 // Esc
class UIScreen {
protected:
UIScreen() { }
public:
virtual int render(DisplayDriver& display) =0; // return value is number of millis until next render
virtual bool handleInput(char c) { return false; }
virtual void poll() { }
};

View File

@@ -26,7 +26,7 @@ build_src_filter = ${esp32_base.build_src_filter}
extends = Generic_ESPNOW
build_flags =
${Generic_ESPNOW.build_flags}
-D MAX_CONTACTS=100
-D MAX_CONTACTS=300
-D MAX_GROUP_CHANNELS=1
build_src_filter = ${Generic_ESPNOW.build_src_filter}
+<../examples/simple_secure_chat/main.cpp>
@@ -54,13 +54,13 @@ lib_deps =
extends = Generic_ESPNOW
build_flags =
${Generic_ESPNOW.build_flags}
-D MAX_CONTACTS=100
-D MAX_CONTACTS=300
-D MAX_GROUP_CHANNELS=8
; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1
; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1
; NOTE: DO NOT ENABLE --> -D ESPNOW_DEBUG_LOGGING=1
build_src_filter = ${Generic_ESPNOW.build_src_filter}
+<../examples/companion_radio>
+<../examples/companion_radio/*.cpp>
lib_deps =
${Generic_ESPNOW.lib_deps}
densaugeo/base64 @ ~1.4.0

View File

@@ -8,9 +8,35 @@
#include <helpers/ESP32Board.h>
class Heltec_CT62_Board : public ESP32Board {
public:
uint32_t gpio_state = 0;
uint16_t getBattMilliVolts() override {
public:
void begin() {
ESP32Board::begin();
#if defined(PIN_BOARD_RELAY_CH1) && defined(PIN_BOARD_RELAY_CH2)
pinMode(PIN_BOARD_RELAY_CH1, OUTPUT);
pinMode(PIN_BOARD_RELAY_CH2, OUTPUT);
#endif
#if defined(PIN_BOARD_DIGITAL_IN)
pinMode(PIN_BOARD_DIGITAL_IN, INPUT);
#endif
}
uint32_t getGpio() override {
#if defined(PIN_BOARD_DIGITAL_IN)
return gpio_state | (digitalRead(PIN_BOARD_DIGITAL_IN) ? 1 : 0);
#else
return 0;
#endif
}
void setGpio(uint32_t values) override {
#if defined(PIN_BOARD_RELAY_CH1) && defined(PIN_BOARD_RELAY_CH2)
gpio_state = values;
digitalWrite(PIN_BOARD_RELAY_CH1, values & 2);
digitalWrite(PIN_BOARD_RELAY_CH2, values & 4);
#endif
}
uint16_t getBattMilliVolts() override {
#ifdef PIN_VBAT_READ
analogReadResolution(12); // ESP32-C3 ADC is 12-bit - 3.3/4096 (ref voltage/max counts)
uint32_t raw = 0;

View File

@@ -55,13 +55,13 @@ build_flags =
${Heltec_ct62.build_flags}
; -D ARDUINO_USB_MODE=1
; -D ARDUINO_USB_CDC_ON_BOOT=1
-D MAX_CONTACTS=100
-D MAX_CONTACTS=300
-D MAX_GROUP_CHANNELS=8
-D OFFLINE_QUEUE_SIZE=256
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${Heltec_ct62.build_src_filter}
+<../examples/companion_radio>
+<../examples/companion_radio/*.cpp>
lib_deps =
${Heltec_ct62.lib_deps}
${esp32_ota.lib_deps}
@@ -73,16 +73,37 @@ build_flags =
${Heltec_ct62.build_flags}
; -D ARDUINO_USB_MODE=1
; -D ARDUINO_USB_CDC_ON_BOOT=1
-D MAX_CONTACTS=100
-D MAX_CONTACTS=300
-D MAX_GROUP_CHANNELS=8
-D OFFLINE_QUEUE_SIZE=256
-D BLE_PIN_CODE=123456
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${Heltec_ct62.build_src_filter}
+<../examples/companion_radio>
+<../examples/companion_radio/*.cpp>
+<helpers/esp32/SerialBLEInterface.cpp>
lib_deps =
${Heltec_ct62.lib_deps}
${esp32_ota.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:Heltec_ct62_sensor]
extends = Heltec_ct62
build_flags =
${Heltec_ct62.build_flags}
-D ADVERT_NAME='"HT-CT62 Sensor"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D PIN_BOARD_SDA=-1
-D PIN_BOARD_SCL=-1
-D PIN_BOARD_RELAY_CH1=0
-D PIN_BOARD_RELAY_CH2=1
-D PIN_BOARD_DIGITAL_IN=19
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${Heltec_ct62.build_src_filter}
+<../examples/simple_sensor>
lib_deps =
${Heltec_ct62.lib_deps}
${esp32_ota.lib_deps}

View File

@@ -0,0 +1,77 @@
#include <Arduino.h>
#include "MeshSolarBoard.h"
#include <bluefruit.h>
#include <Wire.h>
static BLEDfu bledfu;
static void connect_callback(uint16_t conn_handle)
{
(void)conn_handle;
MESH_DEBUG_PRINTLN("BLE client connected");
}
static void disconnect_callback(uint16_t conn_handle, uint8_t reason)
{
(void)conn_handle;
(void)reason;
MESH_DEBUG_PRINTLN("BLE client disconnected");
}
void MeshSolarBoard::begin() {
// for future use, sub-classes SHOULD call this from their begin()
startup_reason = BD_STARTUP_NORMAL;
meshSolarStart();
#if defined(PIN_BOARD_SDA) && defined(PIN_BOARD_SCL)
Wire.setPins(PIN_BOARD_SDA, PIN_BOARD_SCL);
#endif
Wire.begin();
}
bool MeshSolarBoard::startOTAUpdate(const char* id, char reply[]) {
// Config the peripheral connection with maximum bandwidth
// more SRAM required by SoftDevice
// Note: All config***() function must be called before begin()
Bluefruit.configPrphBandwidth(BANDWIDTH_MAX);
Bluefruit.configPrphConn(92, BLE_GAP_EVENT_LENGTH_MIN, 16, 16);
Bluefruit.begin(1, 0);
// Set max power. Accepted values are: -40, -30, -20, -16, -12, -8, -4, 0, 4
Bluefruit.setTxPower(4);
// Set the BLE device name
Bluefruit.setName("MESH_SOLAR_OTA");
Bluefruit.Periph.setConnectCallback(connect_callback);
Bluefruit.Periph.setDisconnectCallback(disconnect_callback);
// To be consistent OTA DFU should be added first if it exists
bledfu.begin();
// Set up and start advertising
// Advertising packet
Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE);
Bluefruit.Advertising.addTxPower();
Bluefruit.Advertising.addName();
/* Start Advertising
- Enable auto advertising if disconnected
- Interval: fast mode = 20 ms, slow mode = 152.5 ms
- Timeout for fast mode is 30 seconds
- Start(timeout) with timeout = 0 will advertise forever (until connected)
For recommended advertising interval
https://developer.apple.com/library/content/qa/qa1931/_index.html
*/
Bluefruit.Advertising.restartOnDisconnect(true);
Bluefruit.Advertising.setInterval(32, 244); // in unit of 0.625 ms
Bluefruit.Advertising.setFastTimeout(30); // number of seconds in fast mode
Bluefruit.Advertising.start(0); // 0 = Don't stop advertising after n seconds
strcpy(reply, "OK - started");
return true;
}

View File

@@ -0,0 +1,44 @@
#pragma once
#include <MeshCore.h>
#include <Arduino.h>
#ifdef HELTEC_MESH_SOLAR
#include "meshSolarApp.h"
#endif
// LoRa radio module pins for Heltec T114
#define P_LORA_DIO_1 20
#define P_LORA_NSS 24
#define P_LORA_RESET 25
#define P_LORA_BUSY 17
#define P_LORA_SCLK 19
#define P_LORA_MISO 23
#define P_LORA_MOSI 22
#define SX126X_DIO2_AS_RF_SWITCH true
#define SX126X_DIO3_TCXO_VOLTAGE 1.8
class MeshSolarBoard : public mesh::MainBoard {
protected:
uint8_t startup_reason;
public:
void begin();
uint8_t getStartupReason() const override { return startup_reason; }
uint16_t getBattMilliVolts() override {
return meshSolarGetBattVoltage();
}
const char* getManufacturerName() const override {
return "Heltec Mesh Solar";
}
void reboot() override {
NVIC_SystemReset();
}
bool startOTAUpdate(const char* id, char reply[]) override;
};

View File

@@ -0,0 +1,91 @@
[Heltec_mesh_solar]
extends = nrf52_base
board = heltec_mesh_solar
platform_packages = framework-arduinoadafruitnrf52
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/heltec_mesh_solar
-D HELTEC_MESH_SOLAR
-D RADIO_CLASS=CustomSX1262
-D WRAPPER_CLASS=CustomSX1262Wrapper
-D LORA_TX_POWER=22
-D SX126X_CURRENT_LIMIT=140
-D SX126X_RX_BOOSTED_GAIN=1
build_src_filter = ${nrf52_base.build_src_filter}
+<helpers/*.cpp>
+<../variants/heltec_mesh_solar>
lib_deps =
${nrf52_base.lib_deps}
rweather/Crypto @ ^0.4.0
stevemarple/MicroNMEA @ ^2.0.6
adafruit/Adafruit NeoPixel@^1.10.0
https://github.com/NMIoT/meshsolar/archive/dfc5330dad443982e6cdd37a61d33fc7252f468b.zip
debug_tool = jlink
upload_protocol = nrfutil
[env:Heltec_mesh_solar_repeater]
extends = Heltec_mesh_solar
build_src_filter = ${Heltec_mesh_solar.build_src_filter}
+<../examples/simple_repeater>
build_flags =
${Heltec_mesh_solar.build_flags}
-D ADVERT_NAME='"Heltec_Mesh_Solar Repeater"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D MAX_NEIGHBOURS=8
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
[env:Heltec_mesh_solar_room_server]
extends = Heltec_mesh_solar
build_src_filter = ${Heltec_mesh_solar.build_src_filter}
+<../examples/simple_room_server>
build_flags =
${Heltec_mesh_solar.build_flags}
-D ADVERT_NAME='"Heltec_Mesh_Solar 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
[env:Heltec_mesh_solar_companion_radio_ble]
extends = Heltec_mesh_solar
build_flags =
${Heltec_mesh_solar.build_flags}
-D MAX_CONTACTS=100
-D MAX_GROUP_CHANNELS=8
-D BLE_PIN_CODE=123456
; -D BLE_DEBUG_LOGGING=1
-D OFFLINE_QUEUE_SIZE=256
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${Heltec_mesh_solar.build_src_filter}
+<helpers/nrf52/SerialBLEInterface.cpp>
+<../examples/companion_radio/*.cpp>
lib_deps =
${Heltec_mesh_solar.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:Heltec_mesh_solar_companion_radio_usb]
extends = Heltec_mesh_solar
build_flags =
${Heltec_mesh_solar.build_flags}
-D MAX_CONTACTS=100
-D MAX_GROUP_CHANNELS=8
; -D BLE_PIN_CODE=123456
; -D BLE_DEBUG_LOGGING=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${Heltec_mesh_solar.build_src_filter}
+<helpers/nrf52/*.cpp>
+<../examples/companion_radio/*.cpp>
lib_deps =
${Heltec_mesh_solar.lib_deps}
densaugeo/base64 @ ~1.4.0

View File

@@ -0,0 +1,123 @@
#include <Arduino.h>
#include "target.h"
#include <helpers/ArduinoHelpers.h>
#include <helpers/sensors/MicroNMEALocationProvider.h>
MeshSolarBoard 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);
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1);
SolarSensorManager sensors = SolarSensorManager(nmea);
#ifdef DISPLAY_CLASS
DISPLAY_CLASS display;
#endif
bool radio_init() {
rtc_clock.begin(Wire);
return radio.std_init(&SPI);
}
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(uint8_t dbm) {
radio.setOutputPower(dbm);
}
mesh::LocalIdentity radio_new_identity() {
RadioNoiseListener rng(radio);
return mesh::LocalIdentity(&rng); // create new random identity
}
void SolarSensorManager::start_gps() {
if (!gps_active) {
gps_active = true;
_location->begin();
}
}
void SolarSensorManager::stop_gps() {
if (gps_active) {
gps_active = false;
_location->stop();
}
}
bool SolarSensorManager::begin() {
Serial1.begin(9600);
// We'll consider GPS detected if we see any data on Serial1
gps_detected = (Serial1.available() > 0);
if (gps_detected) {
MESH_DEBUG_PRINTLN("GPS detected");
} else {
MESH_DEBUG_PRINTLN("No GPS detected");
}
return true;
}
bool SolarSensorManager::querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) {
if (requester_permissions & TELEM_PERM_LOCATION) { // does requester have permission?
telemetry.addGPS(TELEM_CHANNEL_SELF, node_lat, node_lon, node_altitude);
}
return true;
}
void SolarSensorManager::loop() {
static long next_gps_update = 0;
_location->loop();
if (millis() > next_gps_update) {
if (_location->isValid()) {
node_lat = ((double)_location->getLatitude())/1000000.;
node_lon = ((double)_location->getLongitude())/1000000.;
node_altitude = ((double)_location->getAltitude()) / 1000.0;
MESH_DEBUG_PRINTLN("lat %f lon %f", node_lat, node_lon);
}
next_gps_update = millis() + 1000;
}
}
int SolarSensorManager::getNumSettings() const {
return gps_detected ? 1 : 0; // only show GPS setting if GPS is detected
}
const char* SolarSensorManager::getSettingName(int i) const {
return (gps_detected && i == 0) ? "gps" : NULL;
}
const char* SolarSensorManager::getSettingValue(int i) const {
if (gps_detected && i == 0) {
return gps_active ? "1" : "0";
}
return NULL;
}
bool SolarSensorManager::setSettingValue(const char* name, const char* value) {
if (gps_detected && strcmp(name, "gps") == 0) {
if (strcmp(value, "0") == 0) {
stop_gps();
} else {
start_gps();
}
return true;
}
return false; // not supported
}

View File

@@ -0,0 +1,46 @@
#pragma once
#define RADIOLIB_STATIC_ONLY 1
#include <RadioLib.h>
#include <helpers/radiolib/RadioLibWrappers.h>
#include <MeshSolarBoard.h>
#include <helpers/radiolib/CustomSX1262Wrapper.h>
#include <helpers/AutoDiscoverRTCClock.h>
#include <helpers/SensorManager.h>
#include <helpers/sensors/LocationProvider.h>
#ifdef DISPLAY_CLASS
#include <helpers/ui/ST7789Display.h>
#endif
class SolarSensorManager : public SensorManager {
bool gps_active = false;
bool gps_detected = false;
LocationProvider* _location;
void start_gps();
void stop_gps();
public:
SolarSensorManager(LocationProvider &location): _location(&location) { }
bool begin() override;
bool querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) override;
void loop() override;
int getNumSettings() const override;
const char* getSettingName(int i) const override;
const char* getSettingValue(int i) const override;
bool setSettingValue(const char* name, const char* value) override;
};
extern MeshSolarBoard board;
extern WRAPPER_CLASS radio_driver;
extern AutoDiscoverRTCClock rtc_clock;
extern SolarSensorManager sensors;
#ifdef DISPLAY_CLASS
extern DISPLAY_CLASS display;
#endif
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(uint8_t dbm);
mesh::LocalIdentity radio_new_identity();

View File

@@ -0,0 +1,16 @@
#include "variant.h"
#include "wiring_constants.h"
#include "wiring_digital.h"
const uint32_t g_ADigitalPinMap[] = {
0xff, 0xff, 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, 32, 33, 34, 35, 36, 37, 38, 39,
40, 41, 42, 43, 44, 45, 46, 47
};
void initVariant()
{
pinMode(PIN_USER_BTN, INPUT);
pinMode(BQ4050_EMERGENCY_SHUTDOWN_PIN, INPUT);
}

View File

@@ -0,0 +1,127 @@
/*
* 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)
////////////////////////////////////////////////////////////////////////////////
// Power
#define NRF_APM
////////////////////////////////////////////////////////////////////////////////
// Number of pins
#define PINS_COUNT (48)
#define NUM_DIGITAL_PINS (48)
#define NUM_ANALOG_INPUTS (1)
#define NUM_ANALOG_OUTPUTS (0)
////////////////////////////////////////////////////////////////////////////////
// UART pin definition
#define PIN_SERIAL1_RX (37)
#define PIN_SERIAL1_TX (39)
#define PIN_SERIAL2_RX (9)
#define PIN_SERIAL2_TX (10)
////////////////////////////////////////////////////////////////////////////////
// I2C pin definition
#define WIRE_INTERFACES_COUNT (2)
#define PIN_WIRE_SDA (6)
#define PIN_WIRE_SCL (26)
#define PIN_WIRE1_SDA (30)
#define PIN_WIRE1_SCL (5)
////////////////////////////////////////////////////////////////////////////////
// SPI pin definition
#define SPI_INTERFACES_COUNT (2)
#define PIN_SPI_MISO (23)
#define PIN_SPI_MOSI (22)
#define PIN_SPI_SCK (19)
#define PIN_SPI_NSS (24)
////////////////////////////////////////////////////////////////////////////////
// Builtin LEDs
#define LED_BUILTIN (12)
#define PIN_LED LED_BUILTIN
#define LED_RED LED_BUILTIN
#define LED_BLUE (-1) // No blue led, prevents Bluefruit flashing the green LED during advertising
#define LED_PIN LED_BUILTIN
#define LED_STATE_ON LOW
#define PIN_NEOPIXEL (47)
#define NEOPIXEL_NUM (1)
////////////////////////////////////////////////////////////////////////////////
// Builtin buttons
#define PIN_BUTTON1 (42)
#define BUTTON_PIN PIN_BUTTON1
// #define PIN_BUTTON2 (11)
// #define BUTTON_PIN2 PIN_BUTTON2
#define PIN_USER_BTN BUTTON_PIN
#define EXTERNAL_FLASH_DEVICES MX25R1635F
#define EXTERNAL_FLASH_USE_QSPI
////////////////////////////////////////////////////////////////////////////////
// Lora
#define USE_SX1262
#define LORA_CS (24)
#define SX126X_DIO1 (20)
#define SX126X_BUSY (17)
#define SX126X_RESET (25)
#define SX126X_DIO2_AS_RF_SWITCH
#define SX126X_DIO3_TCXO_VOLTAGE 1.8
#define PIN_SPI1_MISO (43)
#define PIN_SPI1_MOSI (41)
#define PIN_SPI1_SCK (40)
////////////////////////////////////////////////////////////////////////////////
// Buzzer
// #define PIN_BUZZER (46)
////////////////////////////////////////////////////////////////////////////////
// GPS
#define GPS_RESET (38)
////////////////////////////////////////////////////////////////////////////////
// TFT
// #define PIN_TFT_SCL (40)
// #define PIN_TFT_SDA (41)
// #define PIN_TFT_RST (2)
// #define PIN_TFT_VDD_CTL (3)
// #define PIN_TFT_LEDA_CTL (15)
// #define PIN_TFT_CS (11)
// #define PIN_TFT_DC (12)
////////////////////////////////////////////////////////////////////////////////
#define BQ4050_SDA_PIN (33) // I2C data line pin
#define BQ4050_SCL_PIN (32) // I2C clock line pin
#define BQ4050_EMERGENCY_SHUTDOWN_PIN (35) // Emergency shutdown pin

View File

@@ -39,10 +39,11 @@ extends = Heltec_tracker_base
build_flags =
${Heltec_tracker_base.build_flags}
-I src/helpers/ui
-I examples/companion_radio/ui-new
-D ARDUINO_USB_CDC_ON_BOOT=1 ; need for Serial
-D DISPLAY_ROTATION=1
-D DISPLAY_CLASS=ST7735Display
-D MAX_CONTACTS=100
-D MAX_CONTACTS=300
-D MAX_GROUP_CHANNELS=8
-D BLE_PIN_CODE=123456 ; HWT will use display for pin
-D OFFLINE_QUEUE_SIZE=256
@@ -51,7 +52,9 @@ build_flags =
; -D MESH_DEBUG=1
build_src_filter = ${Heltec_tracker_base.build_src_filter}
+<helpers/esp32/*.cpp>
+<../examples/companion_radio>
+<helpers/ui/MomentaryButton.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
+<helpers/ui/ST7735Display.cpp>
lib_deps =
${Heltec_tracker_base.lib_deps}

View File

@@ -21,6 +21,7 @@ HWTSensorManager sensors = HWTSensorManager(nmea);
#ifdef DISPLAY_CLASS
DISPLAY_CLASS display(&board.periph_power); // peripheral power pin is shared
MomentaryButton user_btn(PIN_USER_BTN, 1000, true);
#endif
bool radio_init() {

View File

@@ -10,6 +10,7 @@
#include <helpers/sensors/LocationProvider.h>
#ifdef DISPLAY_CLASS
#include <helpers/ui/ST7735Display.h>
#include <helpers/ui/MomentaryButton.h>
#endif
class HWTSensorManager : public SensorManager {
@@ -36,6 +37,7 @@ extern HWTSensorManager sensors;
#ifdef DISPLAY_CLASS
extern DISPLAY_CLASS display;
extern MomentaryButton user_btn;
#endif
bool radio_init();

View File

@@ -35,6 +35,7 @@ build_flags =
build_src_filter = ${Heltec_lora32_v2.build_src_filter}
+<../examples/simple_repeater>
+<helpers/ui/SSD1306Display.cpp>
+<helpers/ui/MomentaryButton.cpp>
lib_deps =
${Heltec_lora32_v2.lib_deps}
${esp32_ota.lib_deps}
@@ -53,6 +54,7 @@ build_flags =
; -D MESH_DEBUG=1
build_src_filter = ${Heltec_lora32_v2.build_src_filter}
+<helpers/ui/SSD1306Display.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<../examples/simple_room_server>
lib_deps =
${Heltec_lora32_v2.lib_deps}
@@ -76,6 +78,7 @@ lib_deps =
extends = Heltec_lora32_v2
build_flags =
${Heltec_lora32_v2.build_flags}
-I examples/companion_radio/ui-new
-D DISPLAY_CLASS=SSD1306Display
-D MAX_CONTACTS=100
-D MAX_GROUP_CHANNELS=8
@@ -84,7 +87,9 @@ build_flags =
build_src_filter = ${Heltec_lora32_v2.build_src_filter}
+<helpers/esp32/*.cpp>
+<helpers/ui/SSD1306Display.cpp>
+<../examples/companion_radio>
+<helpers/ui/MomentaryButton.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${Heltec_lora32_v2.lib_deps}
densaugeo/base64 @ ~1.4.0
@@ -93,6 +98,7 @@ lib_deps =
extends = Heltec_lora32_v2
build_flags =
${Heltec_lora32_v2.build_flags}
-I examples/companion_radio/ui-new
-D DISPLAY_CLASS=SSD1306Display
-D MAX_CONTACTS=100
-D MAX_GROUP_CHANNELS=8
@@ -104,7 +110,9 @@ build_flags =
build_src_filter = ${Heltec_lora32_v2.build_src_filter}
+<helpers/esp32/*.cpp>
+<helpers/ui/SSD1306Display.cpp>
+<../examples/companion_radio>
+<helpers/ui/MomentaryButton.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${Heltec_lora32_v2.lib_deps}
densaugeo/base64 @ ~1.4.0

View File

@@ -18,6 +18,7 @@ SensorManager sensors;
#ifdef DISPLAY_CLASS
DISPLAY_CLASS display;
MomentaryButton user_btn(PIN_USER_BTN, 1000, true);
#endif
bool radio_init() {

View File

@@ -9,6 +9,7 @@
#include <helpers/SensorManager.h>
#ifdef DISPLAY_CLASS
#include <helpers/ui/SSD1306Display.h>
#include <helpers/ui/MomentaryButton.h>
#endif
extern HeltecV2Board board;
@@ -18,6 +19,7 @@ extern SensorManager sensors;
#ifdef DISPLAY_CLASS
extern DISPLAY_CLASS display;
extern MomentaryButton user_btn;
#endif
bool radio_init();

View File

@@ -3,8 +3,10 @@ extends = esp32_base
board = esp32-s3-devkitc-1
build_flags =
${esp32_base.build_flags}
${sensor_base.build_flags}
-I variants/heltec_v3
-D HELTEC_LORA_V3
-D ESP32_CPU_FREQ=80
-D RADIO_CLASS=CustomSX1262
-D WRAPPER_CLASS=CustomSX1262Wrapper
-D LORA_TX_POWER=22
@@ -15,14 +17,8 @@ build_flags =
-D PIN_VEXT_EN=36
-D SX126X_DIO2_AS_RF_SWITCH=true
-D SX126X_DIO3_TCXO_VOLTAGE=1.8
-D SX126X_CURRENT_LIMIT=140
-D SX126X_CURRENT_LIMIT=160
-D SX126X_RX_BOOSTED_GAIN=1
-D ENV_INCLUDE_AHTX0=1
-D ENV_INCLUDE_BME280=1
-D ENV_INCLUDE_BMP280=1
-D ENV_INCLUDE_INA3221=1
-D ENV_INCLUDE_INA219=1
-D ENV_INCLUDE_GPS=1
-D PIN_GPS_RX=47
-D PIN_GPS_TX=48
-D PIN_GPS_EN=26
@@ -31,13 +27,7 @@ build_src_filter = ${esp32_base.build_src_filter}
+<helpers/sensors>
lib_deps =
${esp32_base.lib_deps}
adafruit/Adafruit SSD1306 @ ^2.5.13
adafruit/Adafruit INA3221 Library @ ^1.0.1
adafruit/Adafruit INA219 @ ^1.2.3
adafruit/Adafruit AHTX0 @ ^2.0.5
adafruit/Adafruit BME280 Library @ ^2.3.0
adafruit/Adafruit BMP280 Library@^2.6.8
stevemarple/MicroNMEA @ ^2.0.6
${sensor_base.lib_deps}
[env:Heltec_v3_repeater]
extends = Heltec_lora32_v3
@@ -57,6 +47,7 @@ build_src_filter = ${Heltec_lora32_v3.build_src_filter}
lib_deps =
${Heltec_lora32_v3.lib_deps}
${esp32_ota.lib_deps}
bakercp/CRC32 @ ^2.0.0
[env:Heltec_v3_room_server]
extends = Heltec_lora32_v3
@@ -81,7 +72,7 @@ lib_deps =
extends = Heltec_lora32_v3
build_flags =
${Heltec_lora32_v3.build_flags}
-D MAX_CONTACTS=100
-D MAX_CONTACTS=300
-D MAX_GROUP_CHANNELS=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
@@ -95,6 +86,7 @@ lib_deps =
extends = Heltec_lora32_v3
build_flags =
${Heltec_lora32_v3.build_flags}
-I examples/companion_radio/ui-new
-D MAX_CONTACTS=100
-D MAX_GROUP_CHANNELS=8
-D DISPLAY_CLASS=SSD1306Display
@@ -102,7 +94,9 @@ build_flags =
; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1
build_src_filter = ${Heltec_lora32_v3.build_src_filter}
+<helpers/ui/SSD1306Display.cpp>
+<../examples/companion_radio>
+<helpers/ui/MomentaryButton.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${Heltec_lora32_v3.lib_deps}
densaugeo/base64 @ ~1.4.0
@@ -111,18 +105,22 @@ lib_deps =
extends = Heltec_lora32_v3
build_flags =
${Heltec_lora32_v3.build_flags}
-D MAX_CONTACTS=100
-I examples/companion_radio/ui-new
-D MAX_CONTACTS=160
-D MAX_GROUP_CHANNELS=8
-D DISPLAY_CLASS=SSD1306Display
-D BLE_PIN_CODE=123456 ; dynamic, random PIN
-D AUTO_SHUTDOWN_MILLIVOLTS=3400
-D BLE_DEBUG_LOGGING=1
-D OFFLINE_QUEUE_SIZE=256
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${Heltec_lora32_v3.build_src_filter}
+<helpers/ui/SSD1306Display.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<helpers/esp32/*.cpp>
+<../examples/companion_radio>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${Heltec_lora32_v3.lib_deps}
densaugeo/base64 @ ~1.4.0
@@ -131,6 +129,7 @@ lib_deps =
extends = Heltec_lora32_v3
build_flags =
${Heltec_lora32_v3.build_flags}
-I examples/companion_radio/ui-new
-D MAX_CONTACTS=100
-D MAX_GROUP_CHANNELS=8
-D DISPLAY_CLASS=SSD1306Display
@@ -141,8 +140,10 @@ build_flags =
; -D MESH_DEBUG=1
build_src_filter = ${Heltec_lora32_v3.build_src_filter}
+<helpers/ui/SSD1306Display.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<helpers/esp32/*.cpp>
+<../examples/companion_radio>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${Heltec_lora32_v3.lib_deps}
densaugeo/base64 @ ~1.4.0
@@ -157,8 +158,6 @@ build_flags =
-D ADMIN_PASSWORD='"password"'
-D ENV_PIN_SDA=33
-D ENV_PIN_SCL=34
-D ENV_INCLUDE_MLX90614=1
-D ENV_INCLUDE_VL53L0X=1
-D DISPLAY_CLASS=SSD1306Display
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
@@ -168,8 +167,6 @@ build_src_filter = ${Heltec_lora32_v3.build_src_filter}
lib_deps =
${Heltec_lora32_v3.lib_deps}
${esp32_ota.lib_deps}
adafruit/Adafruit MLX90614 Library @ ^2.1.5
adafruit/Adafruit_VL53L0X @ ^1.2.4
[env:Heltec_WSL3_repeater]
extends = Heltec_lora32_v3
@@ -187,6 +184,7 @@ build_src_filter = ${Heltec_lora32_v3.build_src_filter}
lib_deps =
${Heltec_lora32_v3.lib_deps}
${esp32_ota.lib_deps}
bakercp/CRC32 @ ^2.0.0
[env:Heltec_WSL3_room_server]
extends = Heltec_lora32_v3
@@ -218,7 +216,21 @@ build_flags =
; -D MESH_DEBUG=1
build_src_filter = ${Heltec_lora32_v3.build_src_filter}
+<helpers/esp32/*.cpp>
+<../examples/companion_radio>
+<../examples/companion_radio/*.cpp>
lib_deps =
${Heltec_lora32_v3.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:Heltec_WSL3_companion_radio_usb]
extends = Heltec_lora32_v3
build_flags =
${Heltec_lora32_v3.build_flags}
-D MAX_CONTACTS=140
-D MAX_GROUP_CHANNELS=8
; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1
; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1
build_src_filter = ${Heltec_lora32_v3.build_src_filter}
+<../examples/companion_radio/*.cpp>
lib_deps =
${Heltec_lora32_v3.lib_deps}
densaugeo/base64 @ ~1.4.0

View File

@@ -25,6 +25,7 @@ AutoDiscoverRTCClock rtc_clock(fallback_clock);
#ifdef DISPLAY_CLASS
DISPLAY_CLASS display;
MomentaryButton user_btn(PIN_USER_BTN, 1000, true);
#endif
bool radio_init() {

View File

@@ -10,6 +10,7 @@
#include <helpers/sensors/EnvironmentSensorManager.h>
#ifdef DISPLAY_CLASS
#include <helpers/ui/SSD1306Display.h>
#include <helpers/ui/MomentaryButton.h>
#endif
extern HeltecV3Board board;
@@ -19,6 +20,7 @@ extern EnvironmentSensorManager sensors;
#ifdef DISPLAY_CLASS
extern DISPLAY_CLASS display;
extern MomentaryButton user_btn;
#endif
bool radio_init();

View File

@@ -0,0 +1,69 @@
#include "HeltecE213Board.h"
void HeltecE213Board::begin() {
ESP32Board::begin();
pinMode(PIN_ADC_CTRL, OUTPUT);
digitalWrite(PIN_ADC_CTRL, LOW); // Initially inactive
periph_power.begin();
esp_reset_reason_t reason = esp_reset_reason();
if (reason == ESP_RST_DEEPSLEEP) {
long wakeup_source = esp_sleep_get_ext1_wakeup_status();
if (wakeup_source & (1 << P_LORA_DIO_1)) { // received a LoRa packet (while in deep sleep)
startup_reason = BD_STARTUP_RX_PACKET;
}
rtc_gpio_hold_dis((gpio_num_t)P_LORA_NSS);
rtc_gpio_deinit((gpio_num_t)P_LORA_DIO_1);
}
}
void HeltecE213Board::enterDeepSleep(uint32_t secs, int pin_wake_btn) {
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
// Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep
rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY);
rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1);
rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS);
if (pin_wake_btn < 0) {
esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet
} else {
esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn
}
if (secs > 0) {
esp_sleep_enable_timer_wakeup(secs * 1000000);
}
// Finally set ESP32 into sleep
esp_deep_sleep_start(); // CPU halts here and never returns!
}
void HeltecE213Board::powerOff() {
// TODO: re-enable this when there is a definite wake-up source pin:
// enterDeepSleep(0);
}
uint16_t HeltecE213Board::getBattMilliVolts() {
analogReadResolution(10);
digitalWrite(PIN_ADC_CTRL, HIGH);
uint32_t raw = 0;
for (int i = 0; i < 8; i++) {
raw += analogRead(PIN_VBAT_READ);
}
raw = raw / 8;
digitalWrite(PIN_ADC_CTRL, LOW);
return (5.42 * (3.3 / 1024.0) * raw) * 1000;
}
const char* HeltecE213Board::getManufacturerName() const {
return "Heltec E213";
}

View File

@@ -0,0 +1,30 @@
#pragma once
#include <Arduino.h>
#include <helpers/RefCountedDigitalPin.h>
#include <helpers/ESP32Board.h>
#include <driver/rtc_io.h>
// LoRa radio module pins for heltec_vision_master_e213
#define P_LORA_DIO_1 14
#define P_LORA_NSS 8
#define P_LORA_RESET 12
#define P_LORA_BUSY 13
#define P_LORA_SCLK 9
#define P_LORA_MISO 11
#define P_LORA_MOSI 10
class HeltecE213Board : public ESP32Board {
public:
RefCountedDigitalPin periph_power;
HeltecE213Board() : periph_power(PIN_VEXT_EN,PIN_VEXT_EN_ACTIVE) { }
void begin();
void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1);
void powerOff() override;
uint16_t getBattMilliVolts() override;
const char* getManufacturerName() const override ;
};

View File

@@ -0,0 +1,61 @@
#ifndef Pins_Arduino_h
#define Pins_Arduino_h
#include <stdint.h>
static const uint8_t LED_BUILTIN = 45; // LED is not populated on earliest board variant
#define BUILTIN_LED LED_BUILTIN // Backward compatibility
#define LED_BUILTIN LED_BUILTIN
static const uint8_t TX = 43;
static const uint8_t RX = 44;
static const uint8_t SDA = 39;
static const uint8_t SCL = 38;
static const uint8_t SS = 8;
static const uint8_t MOSI = 10;
static const uint8_t MISO = 11;
static const uint8_t SCK = 9;
static const uint8_t A0 = 1;
static const uint8_t A1 = 2;
static const uint8_t A2 = 3;
static const uint8_t A3 = 4;
static const uint8_t A4 = 5;
static const uint8_t A5 = 6;
static const uint8_t A6 = 7;
static const uint8_t A7 = 8;
static const uint8_t A8 = 9;
static const uint8_t A9 = 10;
static const uint8_t A10 = 11;
static const uint8_t A11 = 12;
static const uint8_t A12 = 13;
static const uint8_t A13 = 14;
static const uint8_t A14 = 15;
static const uint8_t A15 = 16;
static const uint8_t A16 = 17;
static const uint8_t A17 = 18;
static const uint8_t A18 = 19;
static const uint8_t A19 = 20;
static const uint8_t T1 = 1;
static const uint8_t T2 = 2;
static const uint8_t T3 = 3;
static const uint8_t T4 = 4;
static const uint8_t T5 = 5;
static const uint8_t T6 = 6;
static const uint8_t T7 = 7;
static const uint8_t T8 = 8;
static const uint8_t T9 = 9;
static const uint8_t T10 = 10;
static const uint8_t T11 = 11;
static const uint8_t T12 = 12;
static const uint8_t T13 = 13;
static const uint8_t T14 = 14;
static const uint8_t RST_LoRa = 12;
static const uint8_t BUSY_LoRa = 13;
static const uint8_t DIO1 = 14;
#endif /* Pins_Arduino_h */

View File

@@ -0,0 +1,86 @@
[Heltec_Vision_Master_E213_base]
extends = esp32_base
board = heltec_vision_master_e213
build_flags =
${esp32_base.build_flags}
-I variants/heltec_vision_master_e213
-D HELTEC_VISION_MASTER_E213
-D RADIO_CLASS=CustomSX1262
-D WRAPPER_CLASS=CustomSX1262Wrapper
-D LORA_TX_POWER=22
-D P_LORA_TX_LED=45
-D PIN_USER_BTN=0
-D PIN_VEXT_EN=18
-D PIN_VEXT_EN_ACTIVE=HIGH
-D PIN_VBAT_READ=7
-D PIN_ADC_CTRL=46
-D SX126X_DIO2_AS_RF_SWITCH=true
-D SX126X_DIO3_TCXO_VOLTAGE=1.8
-D SX126X_CURRENT_LIMIT=140
-D SX126X_RX_BOOSTED_GAIN=1
-D PIN_BOARD_SDA=39
-D PIN_BOARD_SCL=38
-D DISP_CS=5
-D DISP_BUSY=1
-D DISP_DC=2
-D DISP_RST=3
-D DISP_SCLK=4
-D DISP_MOSI=6
-D Vision_Master_E213
build_src_filter = ${esp32_base.build_src_filter}
+<../variants/heltec_vision_master_e213>
lib_deps =
${esp32_base.lib_deps}
https://github.com/Quency-D/heltec-eink-modules/archive/563dd41fd850a1bc3039b8723da4f3a20fe1c800.zip
[env:Heltec_Vision_Master_E213_radio_ble]
extends = Heltec_Vision_Master_E213_base
build_flags =
${Heltec_Vision_Master_E213_base.build_flags}
-I examples/companion_radio/ui-new
-D MAX_CONTACTS=300
-D MAX_GROUP_CHANNELS=8
-D DISPLAY_CLASS=E213Display
-D BLE_PIN_CODE=123456 ; dynamic, random PIN
-D BLE_DEBUG_LOGGING=1
-D OFFLINE_QUEUE_SIZE=256
build_src_filter = ${Heltec_Vision_Master_E213_base.build_src_filter}
+<helpers/ui/E213Display.cpp>
+<helpers/esp32/*.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${Heltec_Vision_Master_E213_base.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:Heltec_Vision_Master_E213_repeater]
extends = Heltec_Vision_Master_E213_base
build_flags =
${Heltec_Vision_Master_E213_base.build_flags}
-D DISPLAY_CLASS=E213Display
-D ADVERT_NAME='"Heltec E213 Repeater"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
build_src_filter = ${Heltec_Vision_Master_E213_base.build_src_filter}
+<helpers/ui/E213Display.cpp>
+<../examples/simple_repeater>
lib_deps =
${Heltec_Vision_Master_E213_base.lib_deps}
${esp32_ota.lib_deps}
[env:Heltec_Vision_Master_E213_room_server]
extends = Heltec_Vision_Master_E213_base
build_flags =
${Heltec_Vision_Master_E213_base.build_flags}
-D DISPLAY_CLASS=E213Display
-D ADVERT_NAME='"Heltec E213 Room"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D ROOM_PASSWORD='"hello"'
build_src_filter = ${Heltec_Vision_Master_E213_base.build_src_filter}
+<helpers/ui/E213Display.cpp>
+<../examples/simple_room_server>
lib_deps =
${Heltec_Vision_Master_E213_base.lib_deps}
${esp32_ota.lib_deps}

View File

@@ -0,0 +1,54 @@
#include "target.h"
#include <Arduino.h>
HeltecE213Board board;
#if defined(P_LORA_SCLK)
static SPIClass spi(FSPI);
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi);
#else
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY);
#endif
WRAPPER_CLASS radio_driver(radio, board);
ESP32RTCClock fallback_clock;
AutoDiscoverRTCClock rtc_clock(fallback_clock);
SensorManager sensors;
#ifdef DISPLAY_CLASS
DISPLAY_CLASS display;
MomentaryButton user_btn(PIN_USER_BTN, 1000, true);
#endif
bool radio_init() {
fallback_clock.begin();
rtc_clock.begin(Wire);
#if defined(P_LORA_SCLK)
return radio.std_init(&spi);
#else
return radio.std_init();
#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(uint8_t dbm) {
radio.setOutputPower(dbm);
}
mesh::LocalIdentity radio_new_identity() {
RadioNoiseListener rng(radio);
return mesh::LocalIdentity(&rng); // create new random identity
}

View File

@@ -0,0 +1,29 @@
#pragma once
#define RADIOLIB_STATIC_ONLY 1
#include <RadioLib.h>
#include <helpers/radiolib/RadioLibWrappers.h>
#include <HeltecE213Board.h>
#include <helpers/radiolib/CustomSX1262Wrapper.h>
#include <helpers/AutoDiscoverRTCClock.h>
#include <helpers/SensorManager.h>
#ifdef DISPLAY_CLASS
#include <helpers/ui/E213Display.h>
#include <helpers/ui/MomentaryButton.h>
#endif
extern HeltecE213Board board;
extern WRAPPER_CLASS radio_driver;
extern AutoDiscoverRTCClock rtc_clock;
extern SensorManager sensors;
#ifdef DISPLAY_CLASS
extern DISPLAY_CLASS display;
extern MomentaryButton user_btn;
#endif
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(uint8_t dbm);
mesh::LocalIdentity radio_new_identity();

View File

@@ -0,0 +1,69 @@
#include "HeltecE290Board.h"
void HeltecE290Board::begin() {
ESP32Board::begin();
pinMode(PIN_ADC_CTRL, OUTPUT);
digitalWrite(PIN_ADC_CTRL, LOW); // Initially inactive
periph_power.begin();
esp_reset_reason_t reason = esp_reset_reason();
if (reason == ESP_RST_DEEPSLEEP) {
long wakeup_source = esp_sleep_get_ext1_wakeup_status();
if (wakeup_source & (1 << P_LORA_DIO_1)) { // received a LoRa packet (while in deep sleep)
startup_reason = BD_STARTUP_RX_PACKET;
}
rtc_gpio_hold_dis((gpio_num_t)P_LORA_NSS);
rtc_gpio_deinit((gpio_num_t)P_LORA_DIO_1);
}
}
void HeltecE290Board::enterDeepSleep(uint32_t secs, int pin_wake_btn) {
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
// Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep
rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY);
rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1);
rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS);
if (pin_wake_btn < 0) {
esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet
} else {
esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn
}
if (secs > 0) {
esp_sleep_enable_timer_wakeup(secs * 1000000);
}
// Finally set ESP32 into sleep
esp_deep_sleep_start(); // CPU halts here and never returns!
}
void HeltecE290Board::powerOff() {
// TODO: re-enable this when there is a definite wake-up source pin:
// enterDeepSleep(0);
}
uint16_t HeltecE290Board::getBattMilliVolts() {
analogReadResolution(10);
digitalWrite(PIN_ADC_CTRL, HIGH);
uint32_t raw = 0;
for (int i = 0; i < 8; i++) {
raw += analogRead(PIN_VBAT_READ);
}
raw = raw / 8;
digitalWrite(PIN_ADC_CTRL, LOW);
return (5.42 * (3.3 / 1024.0) * raw) * 1000;
}
const char* HeltecE290Board::getManufacturerName() const {
return "Heltec E290";
}

View File

@@ -0,0 +1,30 @@
#pragma once
#include <Arduino.h>
#include <helpers/RefCountedDigitalPin.h>
#include <helpers/ESP32Board.h>
#include <driver/rtc_io.h>
// LoRa radio module pins for heltec_vision_master_e290
#define P_LORA_DIO_1 14
#define P_LORA_NSS 8
#define P_LORA_RESET 12
#define P_LORA_BUSY 13
#define P_LORA_SCLK 9
#define P_LORA_MISO 11
#define P_LORA_MOSI 10
class HeltecE290Board : public ESP32Board {
public:
RefCountedDigitalPin periph_power;
HeltecE290Board() : periph_power(PIN_VEXT_EN) { }
void begin();
void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1);
void powerOff() override;
uint16_t getBattMilliVolts() override;
const char* getManufacturerName() const override ;
};

View File

@@ -0,0 +1,61 @@
#ifndef Pins_Arduino_h
#define Pins_Arduino_h
#include <stdint.h>
static const uint8_t LED_BUILTIN = 45; // LED is not populated on earliest board variant
#define BUILTIN_LED LED_BUILTIN // Backward compatibility
#define LED_BUILTIN LED_BUILTIN
static const uint8_t TX = 43;
static const uint8_t RX = 44;
static const uint8_t SDA = 39;
static const uint8_t SCL = 38;
static const uint8_t SS = 8;
static const uint8_t MOSI = 10;
static const uint8_t MISO = 11;
static const uint8_t SCK = 9;
static const uint8_t A0 = 1;
static const uint8_t A1 = 2;
static const uint8_t A2 = 3;
static const uint8_t A3 = 4;
static const uint8_t A4 = 5;
static const uint8_t A5 = 6;
static const uint8_t A6 = 7;
static const uint8_t A7 = 8;
static const uint8_t A8 = 9;
static const uint8_t A9 = 10;
static const uint8_t A10 = 11;
static const uint8_t A11 = 12;
static const uint8_t A12 = 13;
static const uint8_t A13 = 14;
static const uint8_t A14 = 15;
static const uint8_t A15 = 16;
static const uint8_t A16 = 17;
static const uint8_t A17 = 18;
static const uint8_t A18 = 19;
static const uint8_t A19 = 20;
static const uint8_t T1 = 1;
static const uint8_t T2 = 2;
static const uint8_t T3 = 3;
static const uint8_t T4 = 4;
static const uint8_t T5 = 5;
static const uint8_t T6 = 6;
static const uint8_t T7 = 7;
static const uint8_t T8 = 8;
static const uint8_t T9 = 9;
static const uint8_t T10 = 10;
static const uint8_t T11 = 11;
static const uint8_t T12 = 12;
static const uint8_t T13 = 13;
static const uint8_t T14 = 14;
static const uint8_t RST_LoRa = 12;
static const uint8_t BUSY_LoRa = 13;
static const uint8_t DIO1 = 14;
#endif /* Pins_Arduino_h */

View File

@@ -0,0 +1,80 @@
[Heltec_Vision_Master_E290_base]
extends = esp32_base
board = heltec_vision_master_e290
build_flags =
${esp32_base.build_flags}
-I variants/heltec_vision_master_e290
-D HELTEC_VISION_MASTER_E290
-D RADIO_CLASS=CustomSX1262
-D WRAPPER_CLASS=CustomSX1262Wrapper
-D LORA_TX_POWER=22
-D P_LORA_TX_LED=45
-D PIN_USER_BTN=0
-D PIN_VEXT_EN=18
-D PIN_VEXT_EN_ACTIVE=HIGH
-D PIN_VBAT_READ=7
-D PIN_ADC_CTRL=46
-D SX126X_DIO2_AS_RF_SWITCH=true
-D SX126X_DIO3_TCXO_VOLTAGE=1.8
-D SX126X_CURRENT_LIMIT=140
-D SX126X_RX_BOOSTED_GAIN=1
-D PIN_BOARD_SDA=39
-D PIN_BOARD_SCL=38
-D Vision_Master_E290
build_src_filter = ${esp32_base.build_src_filter}
+<../variants/heltec_vision_master_e290>
lib_deps =
${esp32_base.lib_deps}
https://github.com/Quency-D/heltec-eink-modules/archive/563dd41fd850a1bc3039b8723da4f3a20fe1c800.zip
[env:Heltec_Vision_Master_E290_radio_ble]
extends = Heltec_Vision_Master_E290_base
build_flags =
${Heltec_Vision_Master_E290_base.build_flags}
-I examples/companion_radio/ui-new
-D MAX_CONTACTS=300
-D MAX_GROUP_CHANNELS=8
-D DISPLAY_CLASS=E290Display
-D BLE_PIN_CODE=123456 ; dynamic, random PIN
-D BLE_DEBUG_LOGGING=1
-D OFFLINE_QUEUE_SIZE=256
build_src_filter = ${Heltec_Vision_Master_E290_base.build_src_filter}
+<helpers/ui/E290Display.cpp>
+<helpers/esp32/*.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${Heltec_Vision_Master_E290_base.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:Heltec_Vision_Master_E290_repeater]
extends = Heltec_Vision_Master_E290_base
build_flags =
${Heltec_Vision_Master_E290_base.build_flags}
-D DISPLAY_CLASS=E290Display
-D ADVERT_NAME='"Heltec E290 Repeater"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
build_src_filter = ${Heltec_Vision_Master_E290_base.build_src_filter}
+<helpers/ui/E290Display.cpp>
+<../examples/simple_repeater>
lib_deps =
${Heltec_Vision_Master_E290_base.lib_deps}
${esp32_ota.lib_deps}
[env:Heltec_Vision_Master_E290_room_server]
extends = Heltec_Vision_Master_E290_base
build_flags =
${Heltec_Vision_Master_E290_base.build_flags}
-D DISPLAY_CLASS=E290Display
-D ADVERT_NAME='"Heltec E290 Room"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D ROOM_PASSWORD='"hello"'
build_src_filter = ${Heltec_Vision_Master_E290_base.build_src_filter}
+<helpers/ui/E290Display.cpp>
+<../examples/simple_room_server>
lib_deps =
${Heltec_Vision_Master_E290_base.lib_deps}
${esp32_ota.lib_deps}

View File

@@ -0,0 +1,54 @@
#include "target.h"
#include <Arduino.h>
HeltecE290Board board;
#if defined(P_LORA_SCLK)
static SPIClass spi(FSPI);
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi);
#else
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY);
#endif
WRAPPER_CLASS radio_driver(radio, board);
ESP32RTCClock fallback_clock;
AutoDiscoverRTCClock rtc_clock(fallback_clock);
SensorManager sensors;
#ifdef DISPLAY_CLASS
DISPLAY_CLASS display;
MomentaryButton user_btn(PIN_USER_BTN, 1000, true);
#endif
bool radio_init() {
fallback_clock.begin();
rtc_clock.begin(Wire);
#if defined(P_LORA_SCLK)
return radio.std_init(&spi);
#else
return radio.std_init();
#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(uint8_t dbm) {
radio.setOutputPower(dbm);
}
mesh::LocalIdentity radio_new_identity() {
RadioNoiseListener rng(radio);
return mesh::LocalIdentity(&rng); // create new random identity
}

View File

@@ -0,0 +1,29 @@
#pragma once
#define RADIOLIB_STATIC_ONLY 1
#include <RadioLib.h>
#include <helpers/radiolib/RadioLibWrappers.h>
#include <HeltecE290Board.h>
#include <helpers/radiolib/CustomSX1262Wrapper.h>
#include <helpers/AutoDiscoverRTCClock.h>
#include <helpers/SensorManager.h>
#ifdef DISPLAY_CLASS
#include <helpers/ui/E290Display.h>
#include <helpers/ui/MomentaryButton.h>
#endif
extern HeltecE290Board board;
extern WRAPPER_CLASS radio_driver;
extern AutoDiscoverRTCClock rtc_clock;
extern SensorManager sensors;
#ifdef DISPLAY_CLASS
extern DISPLAY_CLASS display;
extern MomentaryButton user_btn;
#endif
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(uint8_t dbm);
mesh::LocalIdentity radio_new_identity();

View File

@@ -0,0 +1,69 @@
#include "HeltecT190Board.h"
void HeltecT190Board::begin() {
ESP32Board::begin();
pinMode(PIN_ADC_CTRL, OUTPUT);
digitalWrite(PIN_ADC_CTRL, LOW); // Initially inactive
periph_power.begin();
esp_reset_reason_t reason = esp_reset_reason();
if (reason == ESP_RST_DEEPSLEEP) {
long wakeup_source = esp_sleep_get_ext1_wakeup_status();
if (wakeup_source & (1 << P_LORA_DIO_1)) { // received a LoRa packet (while in deep sleep)
startup_reason = BD_STARTUP_RX_PACKET;
}
rtc_gpio_hold_dis((gpio_num_t)P_LORA_NSS);
rtc_gpio_deinit((gpio_num_t)P_LORA_DIO_1);
}
}
void HeltecT190Board::enterDeepSleep(uint32_t secs, int pin_wake_btn) {
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
// Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep
rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY);
rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1);
rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS);
if (pin_wake_btn < 0) {
esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet
} else {
esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn
}
if (secs > 0) {
esp_sleep_enable_timer_wakeup(secs * 1000000);
}
// Finally set ESP32 into sleep
esp_deep_sleep_start(); // CPU halts here and never returns!
}
void HeltecT190Board::powerOff() {
// TODO: re-enable this when there is a definite wake-up source pin:
// enterDeepSleep(0);
}
uint16_t HeltecT190Board::getBattMilliVolts() {
analogReadResolution(10);
digitalWrite(PIN_ADC_CTRL, HIGH);
delay(10);
uint32_t raw = 0;
for (int i = 0; i < 8; i++) {
raw += analogRead(PIN_VBAT_READ);
}
raw = raw / 8;
digitalWrite(PIN_ADC_CTRL, LOW);
return (5.42 * (3.3 / 1024.0) * raw) * 1000;
}
const char* HeltecT190Board::getManufacturerName() const {
return "Heltec T190";
}

View File

@@ -0,0 +1,30 @@
#pragma once
#include <Arduino.h>
#include <helpers/RefCountedDigitalPin.h>
#include <helpers/ESP32Board.h>
#include <driver/rtc_io.h>
// LoRa radio module pins for heltec_vision_master_e290
#define P_LORA_DIO_1 14
#define P_LORA_NSS 8
#define P_LORA_RESET 12
#define P_LORA_BUSY 13
#define P_LORA_SCLK 9
#define P_LORA_MISO 11
#define P_LORA_MOSI 10
class HeltecT190Board : public ESP32Board {
public:
RefCountedDigitalPin periph_power;
HeltecT190Board() : periph_power(PIN_VEXT_EN,PIN_VEXT_EN_ACTIVE) { }
void begin();
void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1);
void powerOff() override;
uint16_t getBattMilliVolts() override;
const char* getManufacturerName() const override ;
};

View File

@@ -0,0 +1,61 @@
#ifndef Pins_Arduino_h
#define Pins_Arduino_h
#include <stdint.h>
static const uint8_t LED_BUILTIN = 45; // LED is not populated on earliest board variant
#define BUILTIN_LED LED_BUILTIN // Backward compatibility
#define LED_BUILTIN LED_BUILTIN
static const uint8_t TX = 43;
static const uint8_t RX = 44;
static const uint8_t SDA = 2;
static const uint8_t SCL = 1;
static const uint8_t SS = 8;
static const uint8_t MOSI = 10;
static const uint8_t MISO = 11;
static const uint8_t SCK = 9;
static const uint8_t A0 = 1;
static const uint8_t A1 = 2;
static const uint8_t A2 = 3;
static const uint8_t A3 = 4;
static const uint8_t A4 = 5;
static const uint8_t A5 = 6;
static const uint8_t A6 = 7;
static const uint8_t A7 = 8;
static const uint8_t A8 = 9;
static const uint8_t A9 = 10;
static const uint8_t A10 = 11;
static const uint8_t A11 = 12;
static const uint8_t A12 = 13;
static const uint8_t A13 = 14;
static const uint8_t A14 = 15;
static const uint8_t A15 = 16;
static const uint8_t A16 = 17;
static const uint8_t A17 = 18;
static const uint8_t A18 = 19;
static const uint8_t A19 = 20;
static const uint8_t T1 = 1;
static const uint8_t T2 = 2;
static const uint8_t T3 = 3;
static const uint8_t T4 = 4;
static const uint8_t T5 = 5;
static const uint8_t T6 = 6;
static const uint8_t T7 = 7;
static const uint8_t T8 = 8;
static const uint8_t T9 = 9;
static const uint8_t T10 = 10;
static const uint8_t T11 = 11;
static const uint8_t T12 = 12;
static const uint8_t T13 = 13;
static const uint8_t T14 = 14;
static const uint8_t RST_LoRa = 12;
static const uint8_t BUSY_LoRa = 13;
static const uint8_t DIO1 = 14;
#endif /* Pins_Arduino_h */

View File

@@ -0,0 +1,87 @@
[Heltec_Vision_Master_T190_base]
extends = esp32_base
board = heltec_vision_master_t190
build_flags =
${esp32_base.build_flags}
-I variants/heltec_vision_master_t190
-I src/helpers/ui
-D HELTEC_VISION_MASTER_T190
-D RADIO_CLASS=CustomSX1262
-D WRAPPER_CLASS=CustomSX1262Wrapper
-D LORA_TX_POWER=22
-D PIN_USER_BTN=0
-D PIN_VEXT_EN=5
-D PIN_VEXT_EN_ACTIVE=HIGH
-D PIN_VBAT_READ=6
-D PIN_ADC_CTRL=46
-D SX126X_DIO2_AS_RF_SWITCH=true
-D SX126X_DIO3_TCXO_VOLTAGE=1.8
-D SX126X_CURRENT_LIMIT=140
-D SX126X_RX_BOOSTED_GAIN=1
-D PIN_BOARD_SDA=2
-D PIN_BOARD_SCL=1
-D PIN_TFT_SCL=38
-D PIN_TFT_SDA=48
-D PIN_TFT_RST=40
-D PIN_TFT_VDD_CTL=7
-D PIN_TFT_LEDA_CTL=17
-D PIN_TFT_LEDA_CTL_ACTIVE=HIGH
-D PIN_TFT_CS=39
-D PIN_TFT_DC=47
-D ST7789
-D DISPLAY_CLASS=ST7789Display
build_src_filter = ${esp32_base.build_src_filter}
+<../variants/heltec_vision_master_t190>
+<helpers/*.cpp>
+<helpers/ui/ST7789Display.cpp>
+<helpers/ui/OLEDDisplay.cpp>
+<helpers/ui/OLEDDisplayFonts.cpp>
lib_deps =
${esp32_base.lib_deps}
adafruit/Adafruit GFX Library @ ^1.12.1
[env:Heltec_Vision_Master_T190_radio_ble]
extends = Heltec_Vision_Master_T190_base
build_flags =
${Heltec_Vision_Master_T190_base.build_flags}
-I examples/companion_radio/ui-new
-D MAX_CONTACTS=300
-D MAX_GROUP_CHANNELS=8
-D BLE_PIN_CODE=123456 ; dynamic, random PIN
-D BLE_DEBUG_LOGGING=1
-D OFFLINE_QUEUE_SIZE=256
build_src_filter = ${Heltec_Vision_Master_T190_base.build_src_filter}
+<helpers/esp32/*.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${Heltec_Vision_Master_T190_base.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:Heltec_Vision_Master_T190_repeater]
extends = Heltec_Vision_Master_T190_base
build_flags =
${Heltec_Vision_Master_T190_base.build_flags}
-D ADVERT_NAME='"Heltec T190 Repeater"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
build_src_filter = ${Heltec_Vision_Master_T190_base.build_src_filter}
+<../examples/simple_repeater>
lib_deps =
${Heltec_Vision_Master_T190_base.lib_deps}
${esp32_ota.lib_deps}
[env:Heltec_Vision_Master_T190_room_server]
extends = Heltec_Vision_Master_T190_base
build_flags =
${Heltec_Vision_Master_T190_base.build_flags}
-D ADVERT_NAME='"Heltec T190 Room"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D ROOM_PASSWORD='"hello"'
build_src_filter = ${Heltec_Vision_Master_T190_base.build_src_filter}
+<../examples/simple_room_server>
lib_deps =
${Heltec_Vision_Master_T190_base.lib_deps}
${esp32_ota.lib_deps}

View File

@@ -0,0 +1,54 @@
#include "target.h"
#include <Arduino.h>
HeltecT190Board board;
#if defined(P_LORA_SCLK)
static SPIClass spi(FSPI);
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi);
#else
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY);
#endif
WRAPPER_CLASS radio_driver(radio, board);
ESP32RTCClock fallback_clock;
AutoDiscoverRTCClock rtc_clock(fallback_clock);
SensorManager sensors;
#ifdef DISPLAY_CLASS
DISPLAY_CLASS display;
MomentaryButton user_btn(PIN_USER_BTN, 1000, true);
#endif
bool radio_init() {
fallback_clock.begin();
rtc_clock.begin(Wire);
#if defined(P_LORA_SCLK)
return radio.std_init(&spi);
#else
return radio.std_init();
#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(uint8_t dbm) {
radio.setOutputPower(dbm);
}
mesh::LocalIdentity radio_new_identity() {
RadioNoiseListener rng(radio);
return mesh::LocalIdentity(&rng); // create new random identity
}

View File

@@ -0,0 +1,29 @@
#pragma once
#define RADIOLIB_STATIC_ONLY 1
#include <RadioLib.h>
#include <helpers/radiolib/RadioLibWrappers.h>
#include <HeltecT190Board.h>
#include <helpers/radiolib/CustomSX1262Wrapper.h>
#include <helpers/AutoDiscoverRTCClock.h>
#include <helpers/SensorManager.h>
#ifdef DISPLAY_CLASS
#include <helpers/ui/ST7789Display.h>
#include <helpers/ui/MomentaryButton.h>
#endif
extern HeltecT190Board board;
extern WRAPPER_CLASS radio_driver;
extern AutoDiscoverRTCClock rtc_clock;
extern SensorManager sensors;
#ifdef DISPLAY_CLASS
extern DISPLAY_CLASS display;
extern MomentaryButton user_btn;
#endif
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(uint8_t dbm);
mesh::LocalIdentity radio_new_identity();

View File

@@ -28,16 +28,17 @@ build_flags =
-D ARDUINO_heltec_wifi_lora_32_V3
-D WIRELESS_PAPER
build_src_filter = ${esp32_base.build_src_filter}
+<../variants/heltec_wireless_paper>
+<../variants/heltec_wireless_paper>
lib_deps =
${esp32_base.lib_deps}
todd-herbert/heltec-eink-modules @ 4.5.0
https://github.com/todd-herbert/heltec-eink-modules/archive/9207eb6ab2b96f66298e0488740218c17b006af7.zip
[env:Heltec_Wireless_Paper_companion_radio_ble]
extends = Heltec_Wireless_Paper_base
build_flags =
${Heltec_Wireless_Paper_base.build_flags}
-D MAX_CONTACTS=100
-I examples/companion_radio/ui-new
-D MAX_CONTACTS=300
-D MAX_GROUP_CHANNELS=8
-D DISPLAY_CLASS=E213Display
-D BLE_PIN_CODE=123456 ; dynamic, random PIN
@@ -46,7 +47,8 @@ build_flags =
build_src_filter = ${Heltec_Wireless_Paper_base.build_src_filter}
+<helpers/ui/E213Display.cpp>
+<helpers/esp32/*.cpp>
+<../examples/companion_radio>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${Heltec_Wireless_Paper_base.lib_deps}
densaugeo/base64 @ ~1.4.0

View File

@@ -19,6 +19,7 @@ SensorManager sensors;
#ifdef DISPLAY_CLASS
DISPLAY_CLASS display;
MomentaryButton user_btn(PIN_USER_BTN, 1000, true);
#endif
bool radio_init() {

View File

@@ -9,6 +9,7 @@
#include <helpers/SensorManager.h>
#ifdef DISPLAY_CLASS
#include <helpers/ui/E213Display.h>
#include <helpers/ui/MomentaryButton.h>
#endif
extern HeltecV3Board board;
@@ -18,6 +19,7 @@ extern SensorManager sensors;
#ifdef DISPLAY_CLASS
extern DISPLAY_CLASS display;
extern MomentaryButton user_btn;
#endif
bool radio_init();

View File

@@ -0,0 +1,99 @@
#ifdef XIAO_NRF52
#include <Arduino.h>
#include "ikoka_stick_nrf_board.h"
#include <bluefruit.h>
#include <Wire.h>
static BLEDfu bledfu;
static void connect_callback(uint16_t conn_handle)
{
(void)conn_handle;
MESH_DEBUG_PRINTLN("BLE client connected");
}
static void disconnect_callback(uint16_t conn_handle, uint8_t reason)
{
(void)conn_handle;
(void)reason;
MESH_DEBUG_PRINTLN("BLE client disconnected");
}
void ikoka_stick_nrf_board::begin() {
// for future use, sub-classes SHOULD call this from their begin()
startup_reason = BD_STARTUP_NORMAL;
pinMode(PIN_VBAT, INPUT);
pinMode(VBAT_ENABLE, OUTPUT);
digitalWrite(VBAT_ENABLE, HIGH);
#ifdef PIN_USER_BTN
pinMode(PIN_USER_BTN, INPUT_PULLUP);
#endif
#if defined(PIN_WIRE_SDA) && defined(PIN_WIRE_SCL)
Wire.setPins(PIN_WIRE_SDA, PIN_WIRE_SCL);
#endif
Wire.begin();
#ifdef P_LORA_TX_LED
pinMode(P_LORA_TX_LED, OUTPUT);
digitalWrite(P_LORA_TX_LED, HIGH);
#endif
// pinMode(SX126X_POWER_EN, OUTPUT);
// digitalWrite(SX126X_POWER_EN, HIGH);
delay(10); // give sx1262 some time to power up
}
bool ikoka_stick_nrf_board::startOTAUpdate(const char* id, char reply[]) {
// Config the peripheral connection with maximum bandwidth
// more SRAM required by SoftDevice
// Note: All config***() function must be called before begin()
Bluefruit.configPrphBandwidth(BANDWIDTH_MAX);
Bluefruit.configPrphConn(92, BLE_GAP_EVENT_LENGTH_MIN, 16, 16);
Bluefruit.begin(1, 0);
// Set max power. Accepted values are: -40, -30, -20, -16, -12, -8, -4, 0, 4
Bluefruit.setTxPower(4);
// Set the BLE device name
Bluefruit.setName("XIAO_NRF52_OTA");
Bluefruit.Periph.setConnectCallback(connect_callback);
Bluefruit.Periph.setDisconnectCallback(disconnect_callback);
// To be consistent OTA DFU should be added first if it exists
bledfu.begin();
// Set up and start advertising
// Advertising packet
Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE);
Bluefruit.Advertising.addTxPower();
Bluefruit.Advertising.addName();
/* Start Advertising
- Enable auto advertising if disconnected
- Interval: fast mode = 20 ms, slow mode = 152.5 ms
- Timeout for fast mode is 30 seconds
- Start(timeout) with timeout = 0 will advertise forever (until connected)
For recommended advertising interval
https://developer.apple.com/library/content/qa/qa1931/_index.html
*/
Bluefruit.Advertising.restartOnDisconnect(true);
Bluefruit.Advertising.setInterval(32, 244); // in unit of 0.625 ms
Bluefruit.Advertising.setFastTimeout(30); // number of seconds in fast mode
Bluefruit.Advertising.start(0); // 0 = Don't stop advertising after n seconds
strcpy(reply, "OK - started");
return true;
return false;
}
#endif

View File

@@ -0,0 +1,52 @@
#pragma once
#include <MeshCore.h>
#include <Arduino.h>
#ifdef XIAO_NRF52
class ikoka_stick_nrf_board : public mesh::MainBoard {
protected:
uint8_t startup_reason;
public:
void begin();
uint8_t getStartupReason() const override { return startup_reason; }
#if defined(P_LORA_TX_LED)
void onBeforeTransmit() override {
digitalWrite(P_LORA_TX_LED, LOW); // turn TX LED on
}
void onAfterTransmit() override {
digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED off
}
#endif
uint16_t getBattMilliVolts() override {
// Please read befor going further ;)
// https://wiki.seeedstudio.com/XIAO_BLE#q3-what-are-the-considerations-when-using-xiao-nrf52840-sense-for-battery-charging
// We can't drive VBAT_ENABLE to HIGH as long
// as we don't know wether we are charging or not ...
// this is a 3mA loss (4/1500)
digitalWrite(VBAT_ENABLE, LOW);
int adcvalue = 0;
analogReadResolution(12);
analogReference(AR_INTERNAL_3_0);
delay(10);
adcvalue = analogRead(PIN_VBAT);
return (adcvalue * ADC_MULTIPLIER * AREF_VOLTAGE) / 4.096;
}
const char* getManufacturerName() const override {
return "Ikoka Stick (Xiao-nrf52)";
}
void reboot() override {
NVIC_SystemReset();
}
bool startOTAUpdate(const char* id, char reply[]) override;
};
#endif

View File

@@ -0,0 +1,307 @@
[nrf52840_xiao]
extends = nrf52_base
platform_packages =
toolchain-gccarmnoneeabi@~1.100301.0
framework-arduinoadafruitnrf52
board = seeed-xiao-afruitnrf52-nrf52840
board_build.ldscript = boards/nrf52840_s140_v7.ld
build_flags = ${nrf52_base.build_flags}
-D NRF52_PLATFORM -D XIAO_NRF52
-I lib/nrf52/s140_nrf52_7.3.0_API/include
-I lib/nrf52/s140_nrf52_7.3.0_API/include/nrf52
lib_ignore =
BluetoothOTA
lvgl
lib5b4
lib_deps =
${nrf52_base.lib_deps}
rweather/Crypto @ ^0.4.0
adafruit/Adafruit INA3221 Library @ ^1.0.1
adafruit/Adafruit INA219 @ ^1.2.3
adafruit/Adafruit AHTX0 @ ^2.0.5
adafruit/Adafruit BME280 Library @ ^2.3.0
adafruit/Adafruit SSD1306 @ ^2.5.13
[ikoka_stick_nrf_baseboard]
extends = nrf52840_xiao
;board_build.ldscript = boards/nrf52840_s140_v7.ld
build_flags = ${nrf52840_xiao.build_flags}
-D P_LORA_TX_LED=11
-I variants/ikoka_stick_nrf
-I src/helpers/nrf52
-D DISPLAY_CLASS=SSD1306Display
-D DISPLAY_ROTATION=2
-D RADIO_CLASS=CustomSX1262
-D WRAPPER_CLASS=CustomSX1262Wrapper
-D P_LORA_DIO_1=D1
-D P_LORA_RESET=D2
-D P_LORA_BUSY=D3
-D P_LORA_NSS=D4
-D SX126X_RXEN=D5
-D SX126X_TXEN=RADIOLIB_NC
-D SX126X_DIO2_AS_RF_SWITCH=1
-D SX126X_DIO3_TCXO_VOLTAGE=1.8
-D SX126X_CURRENT_LIMIT=140
-D SX126X_RX_BOOSTED_GAIN=1
-D PIN_USER_BTN=0
-D PIN_WIRE_SCL=7
-D PIN_WIRE_SDA=6
-D ENV_INCLUDE_AHTX0=1
-D ENV_INCLUDE_BME280=1
-D ENV_INCLUDE_INA3221=1
-D ENV_INCLUDE_INA219=1
debug_tool = jlink
upload_protocol = nrfutil
;;; abstracted hardware variants
[ikoka_stick_nrf_e22_22dbm]
extends = ikoka_stick_nrf_baseboard
; No PA in this model, full 22dBm
build_flags =
${ikoka_stick_nrf_baseboard.build_flags}
-D LORA_TX_POWER=22
build_src_filter = ${nrf52840_xiao.build_src_filter}
+<helpers/*.cpp>
+<helpers/sensors>
+<helpers/ui/MomentaryButton.cpp>
+<helpers/ui/SSD1306Display.cpp>
+<../variants/ikoka_stick_nrf>
[ikoka_stick_nrf_e22_30dbm]
extends = ikoka_stick_nrf_baseboard
; limit txpower to 20dBm on E22-900M30S. Anything higher will
; cause distortion in the PA output. 20dBm in -> 30dBm out
build_flags =
${ikoka_stick_nrf_baseboard.build_flags}
-D LORA_TX_POWER=20
build_src_filter = ${nrf52840_xiao.build_src_filter}
+<helpers/*.cpp>
+<helpers/sensors>
+<helpers/ui/MomentaryButton.cpp>
+<helpers/ui/SSD1306Display.cpp>
+<../variants/ikoka_stick_nrf>
[ikoka_stick_nrf_e22_33dbm]
extends = ikoka_stick_nrf_baseboard
; limit txpower to 9dBm on E22-900M33S to avoid hardware damage
; to the rf amplifier frontend. 9dBm in -> 33dBm out
build_flags =
${ikoka_stick_nrf_baseboard.build_flags}
-D LORA_TX_POWER=9
build_src_filter = ${nrf52840_xiao.build_src_filter}
+<helpers/*.cpp>
+<helpers/sensors>
+<helpers/ui/MomentaryButton.cpp>
+<helpers/ui/SSD1306Display.cpp>
+<../variants/ikoka_stick_nrf>
;;; abstracted firmware roles
[ikoka_stick_nrf_companion_radio_ble]
extends = ikoka_stick_nrf_baseboard
build_flags =
${ikoka_stick_nrf_baseboard.build_flags}
-D MAX_CONTACTS=100
-D MAX_GROUP_CHANNELS=8
-D BLE_PIN_CODE=123456
-D OFFLINE_QUEUE_SIZE=256
-I examples/companion_radio/ui-new
; -D BLE_DEBUG_LOGGING=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${ikoka_stick_nrf_baseboard.build_src_filter}
+<helpers/nrf52/SerialBLEInterface.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${ikoka_stick_nrf_baseboard.lib_deps}
densaugeo/base64 @ ~1.4.0
[ikoka_stick_nrf_companion_radio_usb]
extends = ikoka_stick_nrf_baseboard
build_flags =
${ikoka_stick_nrf_baseboard.build_flags}
-D MAX_CONTACTS=100
-D MAX_GROUP_CHANNELS=8
-I examples/companion_radio/ui-new
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${ikoka_stick_nrf_baseboard.build_src_filter}
+<helpers/nrf52/SerialBLEInterface.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${ikoka_stick_nrf_baseboard.lib_deps}
densaugeo/base64 @ ~1.4.0
[ikoka_stick_nrf_repeater]
extends = ikoka_stick_nrf_baseboard
build_flags =
${ikoka_stick_nrf_baseboard.build_flags}
-D ADVERT_NAME='"Ikoka Stick Repeater"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D MAX_NEIGHBOURS=8
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${ikoka_stick_nrf_baseboard.build_src_filter}
+<helpers/ui/SSD1306Display.cpp>
+<../examples/simple_repeater/*.cpp>
[ikoka_stick_nrf_room_server]
extends = ikoka_stick_nrf_baseboard
build_flags =
${ikoka_stick_nrf_baseboard.build_flags}
-D ADVERT_NAME='"Ikoka Stick Room"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${ikoka_stick_nrf_baseboard.build_src_filter}
+<../examples/simple_room_server/*.cpp>
;;; hardware + firmware variants
;;; 22dBm EBYTE E22-900M22 variants
[env:ikoka_stick_nrf_22dbm_companion_radio_usb]
extends =
ikoka_stick_nrf_e22_22dbm
ikoka_stick_nrf_companion_radio_usb
build_flags =
${ikoka_stick_nrf_companion_radio_usb.build_flags}
${ikoka_stick_nrf_e22_22dbm.build_flags}
build_src_filter =
${ikoka_stick_nrf_companion_radio_usb.build_src_filter}
${ikoka_stick_nrf_e22_22dbm.build_src_filter}
[env:ikoka_stick_nrf_22dbm_companion_radio_ble]
extends =
ikoka_stick_nrf_e22_22dbm
ikoka_stick_nrf_companion_radio_ble
build_flags =
${ikoka_stick_nrf_companion_radio_ble.build_flags}
${ikoka_stick_nrf_e22_22dbm.build_flags}
build_src_filter =
${ikoka_stick_nrf_companion_radio_ble.build_src_filter}
${ikoka_stick_nrf_e22_22dbm.build_src_filter}
[env:ikoka_stick_nrf_22dbm_repeater]
extends =
ikoka_stick_nrf_e22_22dbm
ikoka_stick_nrf_repeater
build_flags =
${ikoka_stick_nrf_repeater.build_flags}
${ikoka_stick_nrf_e22_22dbm.build_flags}
build_src_filter =
${ikoka_stick_nrf_repeater.build_src_filter}
${ikoka_stick_nrf_e22_22dbm.build_src_filter}
[env:ikoka_stick_nrf_22dbm_room_server]
extends =
ikoka_stick_nrf_e22_22dbm
ikoka_stick_nrf_room_server
build_flags =
${ikoka_stick_nrf_room_server.build_flags}
${ikoka_stick_nrf_e22_22dbm.build_flags}
build_src_filter =
${ikoka_stick_nrf_room_server.build_src_filter}
${ikoka_stick_nrf_e22_22dbm.build_src_filter}
;;; 30dBm EBYTE E22-900M30 variants
[env:ikoka_stick_nrf_30dbm_companion_radio_usb]
extends =
ikoka_stick_nrf_e22_30dbm
ikoka_stick_nrf_companion_radio_usb
build_flags =
${ikoka_stick_nrf_companion_radio_usb.build_flags}
${ikoka_stick_nrf_e22_30dbm.build_flags}
build_src_filter =
${ikoka_stick_nrf_companion_radio_usb.build_src_filter}
${ikoka_stick_nrf_e22_30dbm.build_src_filter}
[env:ikoka_stick_nrf_30dbm_companion_radio_ble]
extends =
ikoka_stick_nrf_e22_30dbm
ikoka_stick_nrf_companion_radio_ble
build_flags =
${ikoka_stick_nrf_companion_radio_ble.build_flags}
${ikoka_stick_nrf_e22_30dbm.build_flags}
build_src_filter =
${ikoka_stick_nrf_companion_radio_ble.build_src_filter}
${ikoka_stick_nrf_e22_30dbm.build_src_filter}
[env:ikoka_stick_nrf_30dbm_repeater]
extends =
ikoka_stick_nrf_e22_30dbm
ikoka_stick_nrf_repeater
build_flags =
${ikoka_stick_nrf_repeater.build_flags}
${ikoka_stick_nrf_e22_30dbm.build_flags}
build_src_filter =
${ikoka_stick_nrf_repeater.build_src_filter}
${ikoka_stick_nrf_e22_30dbm.build_src_filter}
[env:ikoka_stick_nrf_30dbm_room_server]
extends =
ikoka_stick_nrf_e22_30dbm
ikoka_stick_nrf_room_server
build_flags =
${ikoka_stick_nrf_room_server.build_flags}
${ikoka_stick_nrf_e22_30dbm.build_flags}
build_src_filter =
${ikoka_stick_nrf_room_server.build_src_filter}
${ikoka_stick_nrf_e22_30dbm.build_src_filter}
;;; 33dBm EBYTE E22-900M33 variants
[env:ikoka_stick_nrf_33dbm_companion_radio_usb]
extends =
ikoka_stick_nrf_e22_33dbm
ikoka_stick_nrf_companion_radio_usb
build_flags =
${ikoka_stick_nrf_companion_radio_usb.build_flags}
${ikoka_stick_nrf_e22_33dbm.build_flags}
build_src_filter =
${ikoka_stick_nrf_companion_radio_usb.build_src_filter}
${ikoka_stick_nrf_e22_33dbm.build_src_filter}
[env:ikoka_stick_nrf_33dbm_companion_radio_ble]
extends =
ikoka_stick_nrf_e22_33dbm
ikoka_stick_nrf_companion_radio_ble
build_flags =
${ikoka_stick_nrf_companion_radio_ble.build_flags}
${ikoka_stick_nrf_e22_33dbm.build_flags}
build_src_filter =
${ikoka_stick_nrf_companion_radio_ble.build_src_filter}
${ikoka_stick_nrf_e22_33dbm.build_src_filter}
[env:ikoka_stick_nrf_33dbm_repeater]
extends =
ikoka_stick_nrf_e22_33dbm
ikoka_stick_nrf_repeater
build_flags =
${ikoka_stick_nrf_repeater.build_flags}
${ikoka_stick_nrf_e22_33dbm.build_flags}
build_src_filter =
${ikoka_stick_nrf_repeater.build_src_filter}
${ikoka_stick_nrf_e22_33dbm.build_src_filter}
[env:ikoka_stick_nrf_33dbm_room_server]
extends =
ikoka_stick_nrf_e22_33dbm
ikoka_stick_nrf_room_server
build_flags =
${ikoka_stick_nrf_room_server.build_flags}
${ikoka_stick_nrf_e22_33dbm.build_flags}
build_src_filter =
${ikoka_stick_nrf_room_server.build_src_filter}
${ikoka_stick_nrf_e22_33dbm.build_src_filter}

Some files were not shown because too many files have changed in this diff Show More