Merge branch 'dev'

This commit is contained in:
Scott Powell 2025-06-07 16:15:58 +10:00
commit 3f1b2c5fc5
92 changed files with 4085 additions and 1987 deletions

84
.clang-format Normal file
View file

@ -0,0 +1,84 @@
# .clang-format
Language: Cpp
AccessModifierOffset: -2
AlignAfterOpenBracket: Align
AlignConsecutiveAssignments: false
AlignConsecutiveDeclarations: false
AlignConsecutiveMacros:
Enabled: true
AcrossEmptyLines: true
AcrossComments: true
AlignOperands: true
AlignTrailingComments: true
AllowAllParametersOfDeclarationOnNextLine: false
AllowShortBlocksOnASingleLine: false
AllowShortCaseLabelsOnASingleLine: false
AllowShortFunctionsOnASingleLine: Inline
AllowShortIfStatementsOnASingleLine: true
AllowShortLoopsOnASingleLine: false
AlwaysBreakAfterDefinitionReturnType: None
AlwaysBreakAfterReturnType: None
AlwaysBreakBeforeMultilineStrings: false
AlwaysBreakTemplateDeclarations: No
BinPackArguments: true
BinPackParameters: true
BraceWrapping:
AfterClass: false
AfterControlStatement: false
AfterEnum: false
AfterFunction: false
AfterNamespace: false
AfterObjCDeclaration: false
AfterStruct: false
AfterUnion: false
BeforeCatch: true
BeforeElse: true
IndentBraces: false
BreakBeforeBinaryOperators: None
BreakBeforeBraces: Attach
BreakBeforeTernaryOperators: true
BreakConstructorInitializersBeforeComma: false
ColumnLimit: 110
CommentPragmas: '^ IWYU pragma:'
CompactNamespaces: false
ConstructorInitializerAllOnOneLineOrOnePerLine: false
Cpp11BracedListStyle: false
DerivePointerAlignment: false
DisableFormat: false
IncludeBlocks: Regroup
IndentCaseLabels: false
IndentPPDirectives: None
IndentWidth: 2
IndentWrappedFunctionNames: false
KeepEmptyLinesAtTheStartOfBlocks: true
MacroBlockBegin: ''
MacroBlockEnd: ''
MaxEmptyLinesToKeep: 1
NamespaceIndentation: None
ObjCBinPackProtocolList: Auto
PenaltyBreakBeforeFirstCallParameter: 19
PenaltyBreakComment: 300
PenaltyBreakFirstLessLess: 120
PenaltyBreakString: 1000
PenaltyExcessCharacter: 100000
PenaltyReturnTypeOnItsOwnLine: 60
PointerAlignment: Right
ReflowComments: true
SortIncludes: true
SpaceAfterCStyleCast: false
SpaceAfterTemplateKeyword: true
SpaceBeforeAssignmentOperators: true
SpaceBeforeCtorInitializerColon: true
SpaceBeforeInheritanceColon: true
SpaceBeforeParens: ControlStatements
SpaceInEmptyParentheses: false
SpacesBeforeTrailingComments: 1
SpacesInAngles: false
SpacesInContainerLiterals: false
SpacesInCStyleCastParentheses: false
SpacesInParentheses: false
SpacesInSquareBrackets: false
Standard: Auto
TabWidth: 2
UseTab: Never
AlignEscapedNewlines: LeftWithLastLine

1
.gitignore vendored
View file

@ -8,3 +8,4 @@ out/
.direnv/
.DS_Store
.vscode/settings.json
.vscode/extensions.json

View file

@ -0,0 +1,125 @@
#include "Button.h"
Button::Button(uint8_t pin, bool activeState)
: _pin(pin), _activeState(activeState), _isAnalog(false), _analogThreshold(20) {
_currentState = false; // Initialize as not pressed
_lastState = _currentState;
}
Button::Button(uint8_t pin, bool activeState, bool isAnalog, uint16_t analogThreshold)
: _pin(pin), _activeState(activeState), _isAnalog(isAnalog), _analogThreshold(analogThreshold) {
_currentState = false; // Initialize as not pressed
_lastState = _currentState;
}
void Button::begin() {
_currentState = readButton();
_lastState = _currentState;
}
void Button::update() {
uint32_t now = millis();
// Read button at specified interval
if (now - _lastReadTime < BUTTON_READ_INTERVAL_MS) {
return;
}
_lastReadTime = now;
bool newState = readButton();
// Check if state has changed
if (newState != _lastState) {
_stateChangeTime = now;
}
// Debounce check
if ((now - _stateChangeTime) > BUTTON_DEBOUNCE_TIME_MS) {
if (newState != _currentState) {
_currentState = newState;
handleStateChange();
}
}
_lastState = newState;
// Handle multi-click timeout
if (_state == WAITING_FOR_MULTI_CLICK && (now - _releaseTime) > BUTTON_CLICK_TIMEOUT_MS) {
// Timeout reached, process the clicks
if (_clickCount == 1) {
triggerEvent(SHORT_PRESS);
} else if (_clickCount == 2) {
triggerEvent(DOUBLE_PRESS);
} else if (_clickCount >= 3) {
triggerEvent(TRIPLE_PRESS);
}
_clickCount = 0;
_state = IDLE;
}
// Handle long press while button is held
if (_state == PRESSED && (now - _pressTime) > BUTTON_LONG_PRESS_TIME_MS) {
triggerEvent(LONG_PRESS);
_state = IDLE; // Prevent multiple press events
_clickCount = 0;
}
}
bool Button::readButton() {
if (_isAnalog) {
return (analogRead(_pin) < _analogThreshold);
} else {
return (digitalRead(_pin) == _activeState);
}
}
void Button::handleStateChange() {
uint32_t now = millis();
if (_currentState) {
// Button pressed
_pressTime = now;
_state = PRESSED;
triggerEvent(ANY_PRESS);
} else {
// Button released
if (_state == PRESSED) {
uint32_t pressDuration = now - _pressTime;
if (pressDuration < BUTTON_LONG_PRESS_TIME_MS) {
// Short press detected
_clickCount++;
_releaseTime = now;
_state = WAITING_FOR_MULTI_CLICK;
} else {
// Long press already handled in update()
_state = IDLE;
_clickCount = 0;
}
}
}
}
void Button::triggerEvent(EventType event) {
_lastEvent = event;
switch (event) {
case ANY_PRESS:
if (_onAnyPress) _onAnyPress();
break;
case SHORT_PRESS:
if (_onShortPress) _onShortPress();
break;
case DOUBLE_PRESS:
if (_onDoublePress) _onDoublePress();
break;
case TRIPLE_PRESS:
if (_onTriplePress) _onTriplePress();
break;
case LONG_PRESS:
if (_onLongPress) _onLongPress();
break;
default:
break;
}
}

View file

@ -0,0 +1,77 @@
#pragma once
#include <Arduino.h>
#include <functional>
// Button timing configuration
#define BUTTON_DEBOUNCE_TIME_MS 50 // Debounce time in ms
#define BUTTON_CLICK_TIMEOUT_MS 500 // Max time between clicks for multi-click
#define BUTTON_LONG_PRESS_TIME_MS 3000 // Time to trigger long press (3 seconds)
#define BUTTON_READ_INTERVAL_MS 10 // How often to read the button
class Button {
public:
enum EventType {
NONE,
SHORT_PRESS,
DOUBLE_PRESS,
TRIPLE_PRESS,
LONG_PRESS,
ANY_PRESS
};
using EventCallback = std::function<void()>;
Button(uint8_t pin, bool activeState = LOW);
Button(uint8_t pin, bool activeState, bool isAnalog, uint16_t analogThreshold = 20);
void begin();
void update();
// Set callbacks for different events
void onShortPress(EventCallback callback) { _onShortPress = callback; }
void onDoublePress(EventCallback callback) { _onDoublePress = callback; }
void onTriplePress(EventCallback callback) { _onTriplePress = callback; }
void onLongPress(EventCallback callback) { _onLongPress = callback; }
void onAnyPress(EventCallback callback) { _onAnyPress = callback; }
// State getters
bool isPressed() const { return _currentState; }
EventType getLastEvent() const { return _lastEvent; }
private:
enum State {
IDLE,
PRESSED,
RELEASED,
WAITING_FOR_MULTI_CLICK
};
uint8_t _pin;
bool _activeState;
bool _isAnalog;
uint16_t _analogThreshold;
State _state = IDLE;
bool _currentState;
bool _lastState;
uint32_t _stateChangeTime = 0;
uint32_t _pressTime = 0;
uint32_t _releaseTime = 0;
uint32_t _lastReadTime = 0;
uint8_t _clickCount = 0;
EventType _lastEvent = NONE;
// Callbacks
EventCallback _onShortPress = nullptr;
EventCallback _onDoublePress = nullptr;
EventCallback _onTriplePress = nullptr;
EventCallback _onLongPress = nullptr;
EventCallback _onAnyPress = nullptr;
bool readButton();
void handleStateChange();
void triggerEvent(EventType event);
};

View file

@ -0,0 +1,391 @@
#include <Arduino.h>
#include "DataStore.h"
DataStore::DataStore(FILESYSTEM& fs, mesh::RTCClock& clock) : _fs(&fs), _clock(&clock),
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
identity_store(fs, "")
#elif defined(RP2040_PLATFORM)
identity_store(fs, "/identity")
#else
identity_store(fs, "/identity")
#endif
{
}
static File openWrite(FILESYSTEM* _fs, const char* filename) {
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
_fs->remove(filename);
return _fs->open(filename, FILE_O_WRITE);
#elif defined(RP2040_PLATFORM)
return _fs->open(filename, "w");
#else
return _fs->open(filename, "w", true);
#endif
}
void DataStore::begin() {
#if defined(RP2040_PLATFORM)
identity_store.begin();
#endif
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
checkAdvBlobFile();
#else
// init 'blob store' support
_fs->mkdir("/bl");
#endif
}
#if defined(ESP32)
#include <SPIFFS.h>
#elif defined(RP2040_PLATFORM)
#include <LittleFS.h>
#endif
File DataStore::openRead(const char* filename) {
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
return _fs->open(filename, FILE_O_READ);
#elif defined(RP2040_PLATFORM)
return _fs->open(filename, "r");
#else
return _fs->open(filename, "r", false);
#endif
}
bool DataStore::removeFile(const char* filename) {
return _fs->remove(filename);
}
bool DataStore::formatFileSystem() {
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
return _fs->format();
#elif defined(RP2040_PLATFORM)
return LittleFS.format();
#elif defined(ESP32)
return ((fs::SPIFFSFS *)_fs)->format();
#else
#error "need to implement format()"
#endif
}
bool DataStore::loadMainIdentity(mesh::LocalIdentity &identity) {
return identity_store.load("_main", identity);
}
bool DataStore::saveMainIdentity(const mesh::LocalIdentity &identity) {
return identity_store.save("_main", identity);
}
void DataStore::loadPrefs(NodePrefs& prefs, double& node_lat, double& node_lon) {
if (_fs->exists("/new_prefs")) {
loadPrefsInt("/new_prefs", prefs, node_lat, node_lon); // new filename
} else if (_fs->exists("/node_prefs")) {
loadPrefsInt("/node_prefs", prefs, node_lat, node_lon);
savePrefs(prefs, node_lat, node_lon); // save to new filename
_fs->remove("/node_prefs"); // remove old
}
}
void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& node_lat, double& node_lon) {
#if defined(RP2040_PLATFORM)
File file = _fs->open(filename, "r");
#else
File file = _fs->open(filename);
#endif
if (file) {
uint8_t pad[8];
file.read((uint8_t *)&_prefs.airtime_factor, sizeof(float)); // 0
file.read((uint8_t *)_prefs.node_name, sizeof(_prefs.node_name)); // 4
file.read(pad, 4); // 36
file.read((uint8_t *)&node_lat, sizeof(node_lat)); // 40
file.read((uint8_t *)&node_lon, sizeof(node_lon)); // 48
file.read((uint8_t *)&_prefs.freq, sizeof(_prefs.freq)); // 56
file.read((uint8_t *)&_prefs.sf, sizeof(_prefs.sf)); // 60
file.read((uint8_t *)&_prefs.cr, sizeof(_prefs.cr)); // 61
file.read((uint8_t *)&_prefs.reserved1, sizeof(_prefs.reserved1)); // 62
file.read((uint8_t *)&_prefs.manual_add_contacts, sizeof(_prefs.manual_add_contacts)); // 63
file.read((uint8_t *)&_prefs.bw, sizeof(_prefs.bw)); // 64
file.read((uint8_t *)&_prefs.tx_power_dbm, sizeof(_prefs.tx_power_dbm)); // 68
file.read((uint8_t *)&_prefs.telemetry_mode_base, sizeof(_prefs.telemetry_mode_base)); // 69
file.read((uint8_t *)&_prefs.telemetry_mode_loc, sizeof(_prefs.telemetry_mode_loc)); // 70
file.read((uint8_t *)&_prefs.telemetry_mode_env, sizeof(_prefs.telemetry_mode_env)); // 71
file.read((uint8_t *)&_prefs.rx_delay_base, sizeof(_prefs.rx_delay_base)); // 72
file.read(pad, 4); // 76
file.read((uint8_t *)&_prefs.ble_pin, sizeof(_prefs.ble_pin)); // 80
file.close();
}
}
void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_lon) {
File file = openWrite(_fs, "/new_prefs");
if (file) {
uint8_t pad[8];
memset(pad, 0, sizeof(pad));
file.write((uint8_t *)&_prefs.airtime_factor, sizeof(float)); // 0
file.write((uint8_t *)_prefs.node_name, sizeof(_prefs.node_name)); // 4
file.write(pad, 4); // 36
file.write((uint8_t *)&node_lat, sizeof(node_lat)); // 40
file.write((uint8_t *)&node_lon, sizeof(node_lon)); // 48
file.write((uint8_t *)&_prefs.freq, sizeof(_prefs.freq)); // 56
file.write((uint8_t *)&_prefs.sf, sizeof(_prefs.sf)); // 60
file.write((uint8_t *)&_prefs.cr, sizeof(_prefs.cr)); // 61
file.write((uint8_t *)&_prefs.reserved1, sizeof(_prefs.reserved1)); // 62
file.write((uint8_t *)&_prefs.manual_add_contacts, sizeof(_prefs.manual_add_contacts)); // 63
file.write((uint8_t *)&_prefs.bw, sizeof(_prefs.bw)); // 64
file.write((uint8_t *)&_prefs.tx_power_dbm, sizeof(_prefs.tx_power_dbm)); // 68
file.write((uint8_t *)&_prefs.telemetry_mode_base, sizeof(_prefs.telemetry_mode_base)); // 69
file.write((uint8_t *)&_prefs.telemetry_mode_loc, sizeof(_prefs.telemetry_mode_loc)); // 70
file.write((uint8_t *)&_prefs.telemetry_mode_env, sizeof(_prefs.telemetry_mode_env)); // 71
file.write((uint8_t *)&_prefs.rx_delay_base, sizeof(_prefs.rx_delay_base)); // 72
file.write(pad, 4); // 76
file.write((uint8_t *)&_prefs.ble_pin, sizeof(_prefs.ble_pin)); // 80
file.close();
}
}
void DataStore::loadContacts(DataStoreHost* host) {
if (_fs->exists("/contacts3")) {
#if defined(RP2040_PLATFORM)
File file = _fs->open("/contacts3", "r");
#else
File file = _fs->open("/contacts3");
#endif
if (file) {
bool full = false;
while (!full) {
ContactInfo c;
uint8_t pub_key[32];
uint8_t unused;
bool success = (file.read(pub_key, 32) == 32);
success = success && (file.read((uint8_t *)&c.name, 32) == 32);
success = success && (file.read(&c.type, 1) == 1);
success = success && (file.read(&c.flags, 1) == 1);
success = success && (file.read(&unused, 1) == 1);
success = success && (file.read((uint8_t *)&c.sync_since, 4) == 4); // was 'reserved'
success = success && (file.read((uint8_t *)&c.out_path_len, 1) == 1);
success = success && (file.read((uint8_t *)&c.last_advert_timestamp, 4) == 4);
success = success && (file.read(c.out_path, 64) == 64);
success = success && (file.read((uint8_t *)&c.lastmod, 4) == 4);
success = success && (file.read((uint8_t *)&c.gps_lat, 4) == 4);
success = success && (file.read((uint8_t *)&c.gps_lon, 4) == 4);
if (!success) break; // EOF
c.id = mesh::Identity(pub_key);
if (!host->onContactLoaded(c)) full = true;
}
file.close();
}
}
}
void DataStore::saveContacts(DataStoreHost* host) {
File file = openWrite(_fs, "/contacts3");
if (file) {
uint32_t idx = 0;
ContactInfo c;
uint8_t unused = 0;
while (host->getContactForSave(idx, c)) {
bool success = (file.write(c.id.pub_key, 32) == 32);
success = success && (file.write((uint8_t *)&c.name, 32) == 32);
success = success && (file.write(&c.type, 1) == 1);
success = success && (file.write(&c.flags, 1) == 1);
success = success && (file.write(&unused, 1) == 1);
success = success && (file.write((uint8_t *)&c.sync_since, 4) == 4);
success = success && (file.write((uint8_t *)&c.out_path_len, 1) == 1);
success = success && (file.write((uint8_t *)&c.last_advert_timestamp, 4) == 4);
success = success && (file.write(c.out_path, 64) == 64);
success = success && (file.write((uint8_t *)&c.lastmod, 4) == 4);
success = success && (file.write((uint8_t *)&c.gps_lat, 4) == 4);
success = success && (file.write((uint8_t *)&c.gps_lon, 4) == 4);
if (!success) break; // write failed
idx++; // advance to next contact
}
file.close();
}
}
void DataStore::loadChannels(DataStoreHost* host) {
if (_fs->exists("/channels2")) {
#if defined(RP2040_PLATFORM)
File file = _fs->open("/channels2", "r");
#else
File file = _fs->open("/channels2");
#endif
if (file) {
bool full = false;
uint8_t channel_idx = 0;
while (!full) {
ChannelDetails ch;
uint8_t unused[4];
bool success = (file.read(unused, 4) == 4);
success = success && (file.read((uint8_t *)ch.name, 32) == 32);
success = success && (file.read((uint8_t *)ch.channel.secret, 32) == 32);
if (!success) break; // EOF
if (host->onChannelLoaded(channel_idx, ch)) {
channel_idx++;
} else {
full = true;
}
}
file.close();
}
}
}
void DataStore::saveChannels(DataStoreHost* host) {
File file = openWrite(_fs, "/channels2");
if (file) {
uint8_t channel_idx = 0;
ChannelDetails ch;
uint8_t unused[4];
memset(unused, 0, 4);
while (host->getChannelForSave(channel_idx, ch)) {
bool success = (file.write(unused, 4) == 4);
success = success && (file.write((uint8_t *)ch.name, 32) == 32);
success = success && (file.write((uint8_t *)ch.channel.secret, 32) == 32);
if (!success) break; // write failed
channel_idx++;
}
file.close();
}
}
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
#define MAX_ADVERT_PKT_LEN (2 + 32 + PUB_KEY_SIZE + 4 + SIGNATURE_SIZE + MAX_ADVERT_DATA_SIZE)
struct BlobRec {
uint32_t timestamp;
uint8_t key[7];
uint8_t len;
uint8_t data[MAX_ADVERT_PKT_LEN];
};
void DataStore::checkAdvBlobFile() {
if (!_fs->exists("/adv_blobs")) {
File file = openWrite(_fs, "/adv_blobs");
if (file) {
BlobRec zeroes;
memset(&zeroes, 0, sizeof(zeroes));
for (int i = 0; i < 20; i++) { // pre-allocate to fixed size
file.write((uint8_t *) &zeroes, sizeof(zeroes));
}
file.close();
}
}
}
uint8_t DataStore::getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]) {
File file = _fs->open("/adv_blobs");
uint8_t len = 0; // 0 = not found
if (file) {
BlobRec tmp;
while (file.read((uint8_t *) &tmp, sizeof(tmp)) == sizeof(tmp)) {
if (memcmp(key, tmp.key, sizeof(tmp.key)) == 0) { // only match by 7 byte prefix
len = tmp.len;
memcpy(dest_buf, tmp.data, len);
break;
}
}
file.close();
}
return len;
}
bool DataStore::putBlobByKey(const uint8_t key[], int key_len, const uint8_t src_buf[], uint8_t len) {
if (len < PUB_KEY_SIZE+4+SIGNATURE_SIZE || len > MAX_ADVERT_PKT_LEN) return false;
checkAdvBlobFile();
File file = _fs->open("/adv_blobs", FILE_O_WRITE);
if (file) {
uint32_t pos = 0, found_pos = 0;
uint32_t min_timestamp = 0xFFFFFFFF;
// search for matching key OR evict by oldest timestmap
BlobRec tmp;
file.seek(0);
while (file.read((uint8_t *) &tmp, sizeof(tmp)) == sizeof(tmp)) {
if (memcmp(key, tmp.key, sizeof(tmp.key)) == 0) { // only match by 7 byte prefix
found_pos = pos;
break;
}
if (tmp.timestamp < min_timestamp) {
min_timestamp = tmp.timestamp;
found_pos = pos;
}
pos += sizeof(tmp);
}
memcpy(tmp.key, key, sizeof(tmp.key)); // just record 7 byte prefix of key
memcpy(tmp.data, src_buf, len);
tmp.len = len;
tmp.timestamp = _clock->getCurrentTime();
file.seek(found_pos);
file.write((uint8_t *) &tmp, sizeof(tmp));
file.close();
return true;
}
return false; // error
}
#else
uint8_t DataStore::getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]) {
char path[64];
char fname[18];
if (key_len > 8) key_len = 8; // just use first 8 bytes (prefix)
mesh::Utils::toHex(fname, key, key_len);
sprintf(path, "/bl/%s", fname);
if (_fs->exists(path)) {
#if defined(RP2040_PLATFORM)
File f = _fs->open(path, "r");
#else
File f = _fs->open(path);
#endif
if (f) {
int len = f.read(dest_buf, 255); // currently MAX 255 byte blob len supported!!
f.close();
return len;
}
}
return 0; // not found
}
bool DataStore::putBlobByKey(const uint8_t key[], int key_len, const uint8_t src_buf[], uint8_t len) {
char path[64];
char fname[18];
if (key_len > 8) key_len = 8; // just use first 8 bytes (prefix)
mesh::Utils::toHex(fname, key, key_len);
sprintf(path, "/bl/%s", fname);
File f = openWrite(_fs, path);
if (f) {
int n = f.write(src_buf, len);
f.close();
if (n == len) return true; // success!
_fs->remove(path); // blob was only partially written!
}
return false; // error
}
#endif

View file

@ -0,0 +1,42 @@
#pragma once
#include <helpers/IdentityStore.h>
#include <helpers/ContactInfo.h>
#include <helpers/ChannelDetails.h>
#include "NodePrefs.h"
class DataStoreHost {
public:
virtual bool onContactLoaded(const ContactInfo& contact) =0;
virtual bool getContactForSave(uint32_t idx, ContactInfo& contact) =0;
virtual bool onChannelLoaded(uint8_t channel_idx, const ChannelDetails& ch) =0;
virtual bool getChannelForSave(uint8_t channel_idx, ChannelDetails& ch) =0;
};
class DataStore {
FILESYSTEM* _fs;
mesh::RTCClock* _clock;
IdentityStore identity_store;
void loadPrefsInt(const char *filename, NodePrefs& prefs, double& node_lat, double& node_lon);
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
void checkAdvBlobFile();
#endif
public:
DataStore(FILESYSTEM& fs, mesh::RTCClock& clock);
void begin();
bool formatFileSystem();
bool loadMainIdentity(mesh::LocalIdentity &identity);
bool saveMainIdentity(const mesh::LocalIdentity &identity);
void loadPrefs(NodePrefs& prefs, double& node_lat, double& node_lon);
void savePrefs(const NodePrefs& prefs, double node_lat, double node_lon);
void loadContacts(DataStoreHost* host);
void saveContacts(DataStoreHost* host);
void loadChannels(DataStoreHost* host);
void saveChannels(DataStoreHost* host);
uint8_t getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]);
bool putBlobByKey(const uint8_t key[], int key_len, const uint8_t src_buf[], uint8_t len);
File openRead(const char* filename);
bool removeFile(const char* filename);
};

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,201 @@
#pragma once
#include <Arduino.h>
#include <Mesh.h>
#ifdef DISPLAY_CLASS
#include "UITask.h"
#endif
/*------------ Frame Protocol --------------*/
#define FIRMWARE_VER_CODE 5
#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "7 Jun 2025"
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.7.0"
#endif
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
#include <InternalFileSystem.h>
#elif defined(RP2040_PLATFORM)
#include <LittleFS.h>
#elif defined(ESP32)
#include <SPIFFS.h>
#endif
#include "DataStore.h"
#include "NodePrefs.h"
#include <RTClib.h>
#include <helpers/ArduinoHelpers.h>
#include <helpers/BaseSerialInterface.h>
#include <helpers/IdentityStore.h>
#include <helpers/SimpleMeshTables.h>
#include <helpers/StaticPoolPacketManager.h>
#include <target.h>
/* ---------------------------------- CONFIGURATION ------------------------------------- */
#ifndef LORA_FREQ
#define LORA_FREQ 915.0
#endif
#ifndef LORA_BW
#define LORA_BW 250
#endif
#ifndef LORA_SF
#define LORA_SF 10
#endif
#ifndef LORA_CR
#define LORA_CR 5
#endif
#ifndef LORA_TX_POWER
#define LORA_TX_POWER 20
#endif
#ifndef MAX_LORA_TX_POWER
#define MAX_LORA_TX_POWER LORA_TX_POWER
#endif
#ifndef MAX_CONTACTS
#define MAX_CONTACTS 100
#endif
#ifndef OFFLINE_QUEUE_SIZE
#define OFFLINE_QUEUE_SIZE 16
#endif
#ifndef BLE_NAME_PREFIX
#define BLE_NAME_PREFIX "MeshCore-"
#endif
#include <helpers/BaseChatMesh.h>
/* -------------------------------------------------------------------------------------- */
#define REQ_TYPE_GET_STATUS 0x01 // same as _GET_STATS
#define REQ_TYPE_KEEP_ALIVE 0x02
#define REQ_TYPE_GET_TELEMETRY_DATA 0x03
class MyMesh : public BaseChatMesh, public DataStoreHost {
public:
MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store);
void begin(bool has_display);
void startInterface(BaseSerialInterface &serial);
const char *getNodeName();
NodePrefs *getNodePrefs();
uint32_t getBLEPin();
void loop();
void handleCmdFrame(size_t len);
bool advert();
void enterCLIRescue();
protected:
float getAirtimeBudgetFactor() const override;
int getInterferenceThreshold() const override;
int calcRxDelay(float score, uint32_t air_time) const override;
void logRxRaw(float snr, float rssi, const uint8_t raw[], int len) override;
bool isAutoAddEnabled() const override;
void onDiscoveredContact(ContactInfo &contact, bool is_new) override;
void onContactPathUpdated(const ContactInfo &contact) override;
bool processAck(const uint8_t *data) override;
void queueMessage(const ContactInfo &from, uint8_t txt_type, mesh::Packet *pkt, uint32_t sender_timestamp,
const uint8_t *extra, int extra_len, const char *text);
void onMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp,
const char *text) override;
void onCommandDataRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp,
const char *text) override;
void onSignedMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp,
const uint8_t *sender_prefix, const char *text) override;
void onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packet *pkt, uint32_t timestamp,
const char *text) override;
uint8_t onContactRequest(const ContactInfo &contact, uint32_t sender_timestamp, const uint8_t *data,
uint8_t len, uint8_t *reply) override;
void onContactResponse(const ContactInfo &contact, const uint8_t *data, uint8_t len) override;
void onRawDataRecv(mesh::Packet *packet) override;
void onTraceRecv(mesh::Packet *packet, uint32_t tag, uint32_t auth_code, uint8_t flags,
const uint8_t *path_snrs, const uint8_t *path_hashes, uint8_t path_len) override;
uint32_t calcFloodTimeoutMillisFor(uint32_t pkt_airtime_millis) const override;
uint32_t calcDirectTimeoutMillisFor(uint32_t pkt_airtime_millis, uint8_t path_len) const override;
void onSendTimeout() override;
// DataStoreHost methods
bool onContactLoaded(const ContactInfo& contact) override { return addContact(contact); }
bool getContactForSave(uint32_t idx, ContactInfo& contact) override { return getContactByIdx(idx, contact); }
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); }
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 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 {
return _store->getBlobByKey(key, key_len, dest_buf);
}
bool putBlobByKey(const uint8_t key[], int key_len, const uint8_t src_buf[], int len) override {
return _store->putBlobByKey(key, key_len, src_buf, len);
}
void checkCLIRescueCmd();
void checkSerialInterface();
// helpers, short-cuts
void savePrefs() { _store->savePrefs(_prefs, sensors.node_lat, sensors.node_lon); }
void saveChannels() { _store->saveChannels(this); }
void saveContacts() { _store->saveContacts(this); }
private:
DataStore* _store;
NodePrefs _prefs;
uint32_t pending_login;
uint32_t pending_status;
uint32_t pending_telemetry;
BaseSerialInterface *_serial;
ContactsIterator _iter;
uint32_t _iter_filter_since;
uint32_t _most_recent_lastmod;
uint32_t _active_ble_pin;
bool _iter_started;
bool _cli_rescue;
char cli_command[80];
uint8_t app_target_ver;
uint8_t *sign_data;
uint32_t sign_data_len;
unsigned long dirty_contacts_expiry;
uint8_t cmd_frame[MAX_FRAME_SIZE + 1];
uint8_t out_frame[MAX_FRAME_SIZE + 1];
CayenneLPP telemetry;
struct Frame {
uint8_t len;
uint8_t buf[MAX_FRAME_SIZE];
};
int offline_queue_len;
Frame offline_queue[OFFLINE_QUEUE_SIZE];
struct AckTableEntry {
unsigned long msg_sent;
uint32_t ack;
};
#define EXPECTED_ACK_TABLE_SIZE 8
AckTableEntry expected_ack_table[EXPECTED_ACK_TABLE_SIZE]; // circular table
int next_ack_idx;
};
extern MyMesh the_mesh;
#ifdef DISPLAY_CLASS
extern UITask ui_task;
#endif

View file

@ -1,6 +1,4 @@
#ifndef NODE_PREFS_H
#define NODE_PREFS_H
#pragma once
#include <cstdint> // For uint8_t, uint32_t
#define TELEM_MODE_DENY 0
@ -22,6 +20,4 @@ struct NodePrefs { // persisted to file
uint8_t telemetry_mode_env;
float rx_delay_base;
uint32_t ble_pin;
};
#endif // NODE_PREFS_H
};

View file

@ -2,6 +2,7 @@
#include <Arduino.h>
#include <helpers/TxtDataHelpers.h>
#include "NodePrefs.h"
#include "MyMesh.h"
#define AUTO_OFF_MILLIS 15000 // 15 seconds
#define BOOT_SCREEN_MILLIS 4000 // 4 seconds
@ -33,30 +34,48 @@ static const uint8_t meshcore_logo [] PROGMEM = {
0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfc, 0x3c, 0x0e, 0x1f, 0xf8, 0xff, 0xf8, 0x70, 0x3c, 0x7f, 0xf8,
};
void UITask::begin(DisplayDriver* display, NodePrefs* node_prefs, const char* build_date, const char* firmware_version, uint32_t pin_code) {
void UITask::begin(DisplayDriver* display, NodePrefs* node_prefs) {
_display = display;
_auto_off = millis() + AUTO_OFF_MILLIS;
clearMsgPreview();
_node_prefs = node_prefs;
_pin_code = pin_code;
if (_display != NULL) {
_display->turnOn();
}
// strip off dash and commit hash by changing dash to null terminator
// e.g: v1.2.3-abcdef -> v1.2.3
char *version = strdup(firmware_version);
char *version = strdup(FIRMWARE_VERSION);
char *dash = strchr(version, '-');
if(dash){
if (dash) {
*dash = 0;
}
// v1.2.3 (1 Jan 2025)
sprintf(_version_info, "%s (%s)", version, build_date);
sprintf(_version_info, "%s (%s)", version, FIRMWARE_BUILD_DATE);
#ifdef PIN_BUZZER
buzzer.begin();
#endif
// Initialize button with appropriate configuration
#if defined(PIN_USER_BTN) || defined(PIN_USER_BTN_ANA)
#ifdef PIN_USER_BTN
_userButton = new Button(PIN_USER_BTN, USER_BTN_PRESSED);
#else
_userButton = new Button(PIN_USER_BTN_ANA, USER_BTN_PRESSED, true, 20);
#endif
_userButton->begin();
// Set up button callbacks
_userButton->onShortPress([this]() { handleButtonShortPress(); });
_userButton->onDoublePress([this]() { handleButtonDoublePress(); });
_userButton->onTriplePress([this]() { handleButtonTriplePress(); });
_userButton->onLongPress([this]() { handleButtonLongPress(); });
_userButton->onAnyPress([this]() { handleButtonAnyPress(); });
#endif
ui_started_at = millis();
}
void UITask::soundBuzzer(UIEventType bet) {
@ -69,6 +88,9 @@ switch(bet){
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:
@ -140,7 +162,16 @@ void UITask::renderCurrScreen() {
if (_display == NULL) return; // assert() ??
char tmp[80];
if (_origin[0] && _msg[0]) { // message preview
if (_alert[0]) {
uint16_t textWidth = _display->getTextWidth(_alert);
_display->setCursor((_display->width() - textWidth) / 2, 22);
_display->setTextSize(1.4);
_display->setColor(DisplayDriver::GREEN);
_display->print(_alert);
_alert[0] = 0;
_need_refresh = true;
return;
} else if (_origin[0] && _msg[0]) { // message preview
// render message preview
_display->setCursor(0, 0);
_display->setTextSize(1);
@ -194,11 +225,11 @@ void UITask::renderCurrScreen() {
_display->print(tmp);
// BT pin
if (!_connected && _pin_code != 0) {
if (!_connected && the_mesh.getBLEPin() != 0) {
_display->setColor(DisplayDriver::RED);
_display->setTextSize(2);
_display->setCursor(0, 43);
sprintf(tmp, "Pin:%d", _pin_code);
sprintf(tmp, "Pin:%d", the_mesh.getBLEPin());
_display->print(tmp);
_display->setColor(DisplayDriver::GREEN);
} else {
@ -233,53 +264,36 @@ void UITask::userLedHandler() {
#endif
}
void UITask::buttonHandler() {
#if defined(PIN_USER_BTN) || defined(PIN_USER_BTN_ANA)
static int prev_btn_state = !USER_BTN_PRESSED;
static int prev_btn_state_ana = !USER_BTN_PRESSED;
static unsigned long btn_state_change_time = 0;
static unsigned long next_read = 0;
int cur_time = millis();
if (cur_time >= next_read) {
int btn_state = 0;
int btn_state_ana = 0;
#ifdef PIN_USER_BTN
btn_state = digitalRead(PIN_USER_BTN);
#endif
#ifdef PIN_USER_BTN_ANA
btn_state_ana = (analogRead(PIN_USER_BTN_ANA) < 20); // analogRead returns a value hopefully below 20 when button is pressed.
#endif
if (btn_state != prev_btn_state || btn_state_ana != prev_btn_state_ana) { // check for either digital or analogue button change of state
if (btn_state == USER_BTN_PRESSED || btn_state_ana == USER_BTN_PRESSED) { // pressed?
if (_display != NULL) {
if (_display->isOn()) {
clearMsgPreview();
} else {
_display->turnOn();
_need_refresh = true;
}
_auto_off = cur_time + AUTO_OFF_MILLIS; // extend auto-off timer
}
} else { // unpressed ? check pressed time ...
if ((cur_time - btn_state_change_time) > 5000) {
#ifdef PIN_STATUS_LED
digitalWrite(PIN_STATUS_LED, LOW);
delay(10);
#endif
_board->powerOff();
}
}
btn_state_change_time = millis();
prev_btn_state = btn_state;
prev_btn_state_ana = btn_state_ana;
}
next_read = millis() + 100; // 10 reads per second
}
#endif
}
/*
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
_board->powerOff();
}
void UITask::loop() {
buttonHandler();
#if defined(PIN_USER_BTN) || defined(PIN_USER_BTN_ANA)
if (_userButton) {
_userButton->update();
}
#endif
userLedHandler();
#ifdef PIN_BUZZER
@ -304,3 +318,72 @@ void UITask::loop() {
}
}
}
void UITask::handleButtonAnyPress() {
MESH_DEBUG_PRINTLN("UITask: any press triggered");
// called on any button press before other events, to wake up the display quickly
// do not refresh the display here, as it may block the button handler
if (_display != NULL) {
_displayWasOn = _display->isOn(); // Track display state before any action
if (!_displayWasOn) {
_display->turnOn();
}
_auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer
}
}
void UITask::handleButtonShortPress() {
MESH_DEBUG_PRINTLN("UITask: short press triggered");
if (_display != NULL) {
// Only clear message preview if display was already on before button press
if (_displayWasOn) {
// If display was on and showing message preview, clear it
if (_origin[0] && _msg[0]) {
clearMsgPreview();
} else {
// Otherwise, refresh the display
_need_refresh = true;
}
}
// Note: Display turn-on and auto-off timer extension are handled by handleButtonAnyPress
}
}
void UITask::handleButtonDoublePress() {
MESH_DEBUG_PRINTLN("UITask: double press triggered, sending advert");
// ADVERT
#ifdef PIN_BUZZER
soundBuzzer(UIEventType::ack);
#endif
if (the_mesh.advert()) {
MESH_DEBUG_PRINTLN("Advert sent!");
sprintf(_alert, "Advert sent!");
} else {
MESH_DEBUG_PRINTLN("Advert failed!");
sprintf(_alert, "Advert failed..");
}
_need_refresh = true;
}
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);
} else {
soundBuzzer(UIEventType::ack);
buzzer.quiet(true);
}
#endif
}
void UITask::handleButtonLongPress() {
MESH_DEBUG_PRINTLN("UITask: long press triggered");
if (millis() - ui_started_at < 8000) { // long press in first 8 seconds since startup -> CLI/rescue
the_mesh.enterCLIRescue();
} else {
shutdown();
}
}

View file

@ -9,6 +9,7 @@
#endif
#include "NodePrefs.h"
#include "Button.h"
enum class UIEventType
{
@ -16,7 +17,8 @@
contactMessage,
channelMessage,
roomMessage,
newContactMessage
newContactMessage,
ack
};
class UITask {
@ -27,27 +29,41 @@ class UITask {
#endif
unsigned long _next_refresh, _auto_off;
bool _connected;
uint32_t _pin_code;
NodePrefs* _node_prefs;
char _version_info[32];
char _origin[62];
char _msg[80];
char _alert[80];
int _msgcount;
bool _need_refresh = true;
bool _displayWasOn = false; // Track display state before button press
unsigned long ui_started_at;
// Button handlers
#if defined(PIN_USER_BTN) || defined(PIN_USER_BTN_ANA)
Button* _userButton = nullptr;
#endif
void renderCurrScreen();
void buttonHandler();
void userLedHandler();
void renderBatteryIndicator(uint16_t batteryMilliVolts);
// Button action handlers
void handleButtonAnyPress();
void handleButtonShortPress();
void handleButtonDoublePress();
void handleButtonTriplePress();
void handleButtonLongPress();
public:
UITask(mesh::MainBoard* board) : _board(board), _display(NULL) {
_next_refresh = 0;
_next_refresh = 0;
ui_started_at = 0;
_connected = false;
}
void begin(DisplayDriver* display, NodePrefs* node_prefs, const char* build_date, const char* firmware_version, uint32_t pin_code);
void begin(DisplayDriver* display, NodePrefs* node_prefs);
void setHasConnection(bool connected) { _connected = connected; }
bool hasDisplay() const { return _display != NULL; }
@ -55,5 +71,6 @@ public:
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);
void shutdown(bool restart = false);
void loop();
};

File diff suppressed because it is too large Load diff

View file

@ -22,11 +22,11 @@
/* ------------------------------ Config -------------------------------- */
#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "24 May 2025"
#define FIRMWARE_BUILD_DATE "7 Jun 2025"
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.6.2"
#define FIRMWARE_VERSION "v1.7.0"
#endif
#ifndef LORA_FREQ
@ -59,6 +59,14 @@
#define ADMIN_PASSWORD "password"
#endif
#ifndef SERVER_RESPONSE_DELAY
#define SERVER_RESPONSE_DELAY 300
#endif
#ifndef TXT_ACK_DELAY
#define TXT_ACK_DELAY 200
#endif
#ifdef DISPLAY_CLASS
#include "UITask.h"
static UITask ui_task(display);
@ -79,7 +87,7 @@
struct RepeaterStats {
uint16_t batt_milli_volts;
uint16_t curr_tx_queue_len;
uint16_t curr_free_queue_len;
int16_t noise_floor;
int16_t last_rssi;
uint32_t n_packets_recv;
uint32_t n_packets_sent;
@ -112,8 +120,7 @@ struct NeighbourInfo {
int8_t snr; // multiplied by 4, user should divide to get float value
};
// NOTE: need to space the ACK and the reply text apart (in CLI)
#define CLI_REPLY_DELAY_MILLIS 1500
#define CLI_REPLY_DELAY_MILLIS 1000
class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
FILESYSTEM* _fs;
@ -183,7 +190,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
RepeaterStats stats;
stats.batt_milli_volts = board.getBattMilliVolts();
stats.curr_tx_queue_len = _mgr->getOutboundCount(0xFFFFFFFF);
stats.curr_free_queue_len = _mgr->getFreeCount();
stats.noise_floor = (int16_t)_radio->getNoiseFloor();
stats.last_rssi = (int16_t) radio_driver.getLastRSSI();
stats.n_packets_recv = radio_driver.getPacketsRecv();
stats.n_packets_sent = radio_driver.getPacketsSent();
@ -327,6 +334,9 @@ protected:
uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + packet->payload_len + 2) * _prefs.direct_tx_delay_factor);
return getRNG()->nextInt(0, 6)*t;
}
int getInterferenceThreshold() const override {
return _prefs.interference_threshold;
}
void onAnonDataRecv(mesh::Packet* packet, uint8_t type, const mesh::Identity& sender, uint8_t* data, size_t len) override {
if (type == PAYLOAD_TYPE_ANON_REQ) { // received an initial request by a possible admin client (unknown at this stage)
@ -373,14 +383,14 @@ protected:
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response
mesh::Packet* path = createPathReturn(sender, client->secret, packet->path, packet->path_len,
PAYLOAD_TYPE_RESPONSE, reply_data, 12);
if (path) sendFlood(path);
if (path) sendFlood(path, SERVER_RESPONSE_DELAY);
} else {
mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, client->secret, reply_data, 12);
if (reply) {
if (client->out_path_len >= 0) { // we have an out_path, so send DIRECT
sendDirect(reply, client->out_path, client->out_path_len);
sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY);
} else {
sendFlood(reply);
sendFlood(reply, SERVER_RESPONSE_DELAY);
}
}
}
@ -443,14 +453,14 @@ protected:
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response
mesh::Packet* path = createPathReturn(client->id, secret, packet->path, packet->path_len,
PAYLOAD_TYPE_RESPONSE, reply_data, reply_len);
if (path) sendFlood(path);
if (path) sendFlood(path, SERVER_RESPONSE_DELAY);
} else {
mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, client->id, secret, reply_data, reply_len);
if (reply) {
if (client->out_path_len >= 0) { // we have an out_path, so send DIRECT
sendDirect(reply, client->out_path, client->out_path_len);
sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY);
} else {
sendFlood(reply);
sendFlood(reply, SERVER_RESPONSE_DELAY);
}
}
}
@ -479,9 +489,9 @@ protected:
mesh::Packet* ack = createAck(ack_hash);
if (ack) {
if (client->out_path_len < 0) {
sendFlood(ack);
sendFlood(ack, TXT_ACK_DELAY);
} else {
sendDirect(ack, client->out_path, client->out_path_len);
sendDirect(ack, client->out_path, client->out_path_len, TXT_ACK_DELAY);
}
}
}
@ -565,6 +575,7 @@ public:
_prefs.advert_interval = 1; // default to 2 minutes for NEW installs
_prefs.flood_advert_interval = 3; // 3 hours
_prefs.flood_max = 64;
_prefs.interference_threshold = 14; // DB
}
CommonCLI* getCLI() { return &_cli; }

View file

@ -22,11 +22,11 @@
/* ------------------------------ Config -------------------------------- */
#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "24 May 2025"
#define FIRMWARE_BUILD_DATE "7 Jun 2025"
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.6.2"
#define FIRMWARE_VERSION "v1.7.0"
#endif
#ifndef LORA_FREQ
@ -67,6 +67,14 @@
#define MAX_UNSYNCED_POSTS 32
#endif
#ifndef SERVER_RESPONSE_DELAY
#define SERVER_RESPONSE_DELAY 300
#endif
#ifndef TXT_ACK_DELAY
#define TXT_ACK_DELAY 200
#endif
#ifdef DISPLAY_CLASS
#include "UITask.h"
static UITask ui_task(display);
@ -126,7 +134,7 @@ struct PostInfo {
struct ServerStats {
uint16_t batt_milli_volts;
uint16_t curr_tx_queue_len;
uint16_t curr_free_queue_len;
int16_t noise_floor;
int16_t last_rssi;
uint32_t n_packets_recv;
uint32_t n_packets_sent;
@ -204,7 +212,10 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
void pushPostToClient(ClientInfo* client, PostInfo& post) {
int len = 0;
memcpy(&reply_data[len], &post.post_timestamp, 4); len += 4; // this is a PAST timestamp... but should be accepted by client
reply_data[len++] = (TXT_TYPE_SIGNED_PLAIN << 2); // 'signed' plain text
uint8_t attempt;
getRNG()->random(&attempt, 1); // need this for re-tries, so packet hash (and ACK) will be different
reply_data[len++] = (TXT_TYPE_SIGNED_PLAIN << 2) | (attempt & 3); // 'signed' plain text
// encode prefix of post.author.pub_key
memcpy(&reply_data[len], post.author.pub_key, 4); len += 4; // just first 4 bytes
@ -287,7 +298,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
ServerStats stats;
stats.batt_milli_volts = board.getBattMilliVolts();
stats.curr_tx_queue_len = _mgr->getOutboundCount(0xFFFFFFFF);
stats.curr_free_queue_len = _mgr->getFreeCount();
stats.noise_floor = (int16_t)_radio->getNoiseFloor();
stats.last_rssi = (int16_t) radio_driver.getLastRSSI();
stats.n_packets_recv = radio_driver.getPacketsRecv();
stats.n_packets_sent = radio_driver.getPacketsSent();
@ -406,6 +417,9 @@ protected:
uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + packet->payload_len + 2) * _prefs.direct_tx_delay_factor);
return getRNG()->nextInt(0, 6)*t;
}
int getInterferenceThreshold() const override {
return _prefs.interference_threshold;
}
bool allowPacketForward(const mesh::Packet* packet) override {
if (_prefs.disable_fwd) return false;
@ -465,14 +479,14 @@ protected:
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response
mesh::Packet* path = createPathReturn(sender, client->secret, packet->path, packet->path_len,
PAYLOAD_TYPE_RESPONSE, reply_data, 8 + 2);
if (path) sendFlood(path);
if (path) sendFlood(path, SERVER_RESPONSE_DELAY);
} else {
mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, client->secret, reply_data, 8 + 2);
if (reply) {
if (client->out_path_len >= 0) { // we have an out_path, so send DIRECT
sendDirect(reply, client->out_path, client->out_path_len);
sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY);
} else {
sendFlood(reply);
sendFlood(reply, SERVER_RESPONSE_DELAY);
}
}
}
@ -562,12 +576,12 @@ protected:
mesh::Packet* ack = createAck(ack_hash);
if (ack) {
if (client->out_path_len < 0) {
sendFlood(ack);
sendFlood(ack, TXT_ACK_DELAY);
} else {
sendDirect(ack, client->out_path, client->out_path_len);
sendDirect(ack, client->out_path, client->out_path_len, TXT_ACK_DELAY);
}
}
delay_millis = REPLY_DELAY_MILLIS;
delay_millis = TXT_ACK_DELAY + REPLY_DELAY_MILLIS;
} else {
delay_millis = 0;
}
@ -586,9 +600,9 @@ protected:
auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, secret, temp, 5 + text_len);
if (reply) {
if (client->out_path_len < 0) {
sendFlood(reply, delay_millis);
sendFlood(reply, delay_millis + SERVER_RESPONSE_DELAY);
} else {
sendDirect(reply, client->out_path, client->out_path_len, delay_millis);
sendDirect(reply, client->out_path, client->out_path_len, delay_millis + SERVER_RESPONSE_DELAY);
}
}
}
@ -631,7 +645,7 @@ protected:
auto reply = createAck(ack_hash);
if (reply) {
reply->payload[reply->payload_len++] = getUnsyncedCount(client); // NEW: add unsynced counter to end of ACK packet
sendDirect(reply, client->out_path, client->out_path_len);
sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY);
}
}
} else {
@ -641,14 +655,14 @@ protected:
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response
mesh::Packet* path = createPathReturn(client->id, secret, packet->path, packet->path_len,
PAYLOAD_TYPE_RESPONSE, reply_data, reply_len);
if (path) sendFlood(path);
if (path) sendFlood(path, SERVER_RESPONSE_DELAY);
} else {
mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, client->id, secret, reply_data, reply_len);
if (reply) {
if (client->out_path_len >= 0) { // we have an out_path, so send DIRECT
sendDirect(reply, client->out_path, client->out_path_len);
sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY);
} else {
sendFlood(reply);
sendFlood(reply, SERVER_RESPONSE_DELAY);
}
}
}
@ -711,6 +725,7 @@ public:
_prefs.advert_interval = 1; // default to 2 minutes for NEW installs
_prefs.flood_advert_interval = 3; // 3 hours
_prefs.flood_max = 64;
_prefs.interference_threshold = 14; // DB
#ifdef ROOM_PASSWORD
StrHelper::strncpy(_prefs.guest_password, ROOM_PASSWORD, sizeof(_prefs.guest_password));
#endif

View file

@ -27,6 +27,8 @@ build_flags = -w -DNDEBUG -DRADIOLIB_STATIC_ONLY=1 -DRADIOLIB_GODMODE=1
-D LORA_FREQ=869.525
-D LORA_BW=250
-D LORA_SF=11
-D ENABLE_PRIVATE_KEY_IMPORT=1 ; NOTE: comment these out for more secure firmware
-D ENABLE_PRIVATE_KEY_EXPORT=1
build_src_filter =
+<*.cpp>
+<helpers/*.cpp>

View file

@ -10,6 +10,10 @@ namespace mesh {
#define MAX_RX_DELAY_MILLIS 32000 // 32 seconds
#ifndef NOISE_FLOOR_CALIB_INTERVAL
#define NOISE_FLOOR_CALIB_INTERVAL 2000 // 2 seconds
#endif
void Dispatcher::begin() {
n_sent_flood = n_sent_direct = 0;
n_recv_flood = n_recv_direct = 0;
@ -36,6 +40,12 @@ uint32_t Dispatcher::getCADFailMaxDuration() const {
}
void Dispatcher::loop() {
if (millisHasNowPassed(next_floor_calib_time)) {
_radio->triggerNoiseFloorCalibrate(getInterferenceThreshold());
next_floor_calib_time = futureMillis(NOISE_FLOOR_CALIB_INTERVAL);
}
_radio->loop();
// check for radio 'stuck' in mode other than Rx
bool is_recv = _radio->isInRecvMode();
if (is_recv != prev_isrecv_mode) {

View file

@ -56,6 +56,15 @@ public:
*/
virtual void onSendFinished() = 0;
/**
* \brief do any processing needed on each loop cycle
*/
virtual void loop() { }
virtual int getNoiseFloor() const { return 0; }
virtual void triggerNoiseFloorCalibrate(int threshold) { }
virtual bool isInRecvMode() const = 0;
/**
@ -107,6 +116,7 @@ class Dispatcher {
unsigned long next_tx_time;
unsigned long cad_busy_start;
unsigned long radio_nonrx_start;
unsigned long next_floor_calib_time;
bool prev_isrecv_mode;
uint32_t n_sent_flood, n_sent_direct;
uint32_t n_recv_flood, n_recv_direct;
@ -124,6 +134,7 @@ protected:
{
outbound = NULL; total_air_time = 0; next_tx_time = 0;
cad_busy_start = 0;
next_floor_calib_time = 0;
_err_flags = 0;
radio_nonrx_start = 0;
prev_isrecv_mode = true;
@ -142,6 +153,7 @@ protected:
virtual int calcRxDelay(float score, uint32_t air_time) const;
virtual uint32_t getCADFailRetryDelay() const;
virtual uint32_t getCADFailMaxDuration() const;
virtual int getInterferenceThreshold() const { return 0; } // disabled by default
public:
void begin();

View file

@ -142,7 +142,7 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
if (pkt->isRouteFlood()) {
// send a reciprocal return path to sender, but send DIRECTLY!
mesh::Packet* rpath = createPathReturn(&src_hash, secret, pkt->path, pkt->path_len, 0, NULL, 0);
if (rpath) sendDirect(rpath, path, path_len);
if (rpath) sendDirect(rpath, path, path_len, 500);
}
}
} else {
@ -518,7 +518,11 @@ void Mesh::sendDirect(Packet* packet, const uint8_t* path, uint8_t path_len, uin
pri = 5; // maybe make this configurable
} else {
memcpy(packet->path, path, packet->path_len = path_len);
pri = 0;
if (packet->getPayloadType() == PAYLOAD_TYPE_PATH) {
pri = 1; // slightly less priority
} else {
pri = 0;
}
}
_tables->hasSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us
sendPacket(packet, pri, delay_millis);

View file

@ -1,6 +1,14 @@
#include <helpers/BaseChatMesh.h>
#include <Utils.h>
#ifndef SERVER_RESPONSE_DELAY
#define SERVER_RESPONSE_DELAY 300
#endif
#ifndef TXT_ACK_DELAY
#define TXT_ACK_DELAY 200
#endif
mesh::Packet* BaseChatMesh::createSelfAdvert(const char* name, double lat, double lon) {
uint8_t app_data[MAX_ADVERT_DATA_SIZE];
uint8_t app_data_len;
@ -131,14 +139,14 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender
// 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);
if (path) sendFlood(path, TXT_ACK_DELAY);
} else {
mesh::Packet* ack = createAck(ack_hash);
if (ack) {
if (from.out_path_len < 0) {
sendFlood(ack);
sendFlood(ack, TXT_ACK_DELAY);
} else {
sendDirect(ack, from.out_path, from.out_path_len);
sendDirect(ack, from.out_path, from.out_path_len, TXT_ACK_DELAY);
}
}
}
@ -164,14 +172,14 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender
// 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);
if (path) sendFlood(path, TXT_ACK_DELAY);
} else {
mesh::Packet* ack = createAck(ack_hash);
if (ack) {
if (from.out_path_len < 0) {
sendFlood(ack);
sendFlood(ack, TXT_ACK_DELAY);
} else {
sendDirect(ack, from.out_path, from.out_path_len);
sendDirect(ack, from.out_path, from.out_path_len, TXT_ACK_DELAY);
}
}
}
@ -187,14 +195,14 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response
mesh::Packet* path = createPathReturn(from.id, secret, packet->path, packet->path_len,
PAYLOAD_TYPE_RESPONSE, temp_buf, reply_len);
if (path) sendFlood(path);
if (path) sendFlood(path, SERVER_RESPONSE_DELAY);
} else {
mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, from.id, secret, temp_buf, reply_len);
if (reply) {
if (from.out_path_len >= 0) { // we have an out_path, so send DIRECT
sendDirect(reply, from.out_path, from.out_path_len);
sendDirect(reply, from.out_path, from.out_path_len, SERVER_RESPONSE_DELAY);
} else {
sendFlood(reply);
sendFlood(reply, SERVER_RESPONSE_DELAY);
}
}
}
@ -691,6 +699,13 @@ int BaseChatMesh::findChannelIdx(const mesh::GroupChannel& ch) {
}
#endif
bool BaseChatMesh::getContactByIdx(uint32_t idx, ContactInfo& contact) {
if (idx >= num_contacts) return false;
contact = contacts[idx];
return true;
}
ContactsIterator BaseChatMesh::startContactsIterator() {
return ContactsIterator();
}

View file

@ -7,19 +7,7 @@
#define MAX_TEXT_LEN (10*CIPHER_BLOCK_SIZE) // must be LESS than (MAX_PACKET_PAYLOAD - 4 - CIPHER_MAC_SIZE - 1)
struct ContactInfo {
mesh::Identity id;
char name[32];
uint8_t type; // on of ADV_TYPE_*
uint8_t flags;
int8_t out_path_len;
uint8_t out_path[MAX_PATH_SIZE];
uint32_t last_advert_timestamp; // by THEIR clock
uint8_t shared_secret[PUB_KEY_SIZE];
uint32_t lastmod; // by OUR clock
int32_t gps_lat, gps_lon; // 6 dec places
uint32_t sync_since;
};
#include "ContactInfo.h"
#define MAX_SEARCH_RESULTS 8
@ -61,10 +49,7 @@ struct ConnectionInfo {
uint32_t expected_ack;
};
struct ChannelDetails {
mesh::GroupChannel channel;
char name[32];
};
#include "ChannelDetails.h"
/**
* \brief abstract Mesh class for common 'chat' client
@ -158,6 +143,7 @@ public:
bool removeContact(ContactInfo& contact);
bool addContact(const ContactInfo& contact);
int getNumContacts() const { return num_contacts; }
bool getContactByIdx(uint32_t idx, ContactInfo& contact);
ContactsIterator startContactsIterator();
ChannelDetails* addChannel(const char* name, const char* psk_base64);
bool getChannel(int idx, ChannelDetails& dest);

View file

@ -0,0 +1,9 @@
#pragma once
#include <Arduino.h>
#include <Mesh.h>
struct ChannelDetails {
mesh::GroupChannel channel;
char name[32];
};

View file

@ -56,6 +56,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
file.read(pad, 4); // 120
file.read((uint8_t *) &_prefs->flood_max, sizeof(_prefs->flood_max)); // 124
file.read((uint8_t *) &_prefs->flood_advert_interval, sizeof(_prefs->flood_advert_interval)); // 125
file.read((uint8_t *) &_prefs->interference_threshold, sizeof(_prefs->interference_threshold)); // 126
// sanitise bad pref values
_prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f);
@ -109,6 +110,7 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) {
file.write(pad, 4); // 120
file.write((uint8_t *) &_prefs->flood_max, sizeof(_prefs->flood_max)); // 124
file.write((uint8_t *) &_prefs->flood_advert_interval, sizeof(_prefs->flood_advert_interval)); // 125
file.write((uint8_t *) &_prefs->interference_threshold, sizeof(_prefs->interference_threshold)); // 126
file.close();
}
@ -176,6 +178,8 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
const char* config = &command[4];
if (memcmp(config, "af", 2) == 0) {
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->airtime_factor));
} else if (memcmp(config, "int.thresh", 10) == 0) {
sprintf(reply, "> %d", (uint32_t) _prefs->interference_threshold);
} else if (memcmp(config, "allow.read.only", 15) == 0) {
sprintf(reply, "> %s", _prefs->allow_read_only ? "on" : "off");
} else if (memcmp(config, "flood.advert.interval", 21) == 0) {
@ -223,6 +227,10 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
_prefs->airtime_factor = atof(&config[3]);
savePrefs();
strcpy(reply, "OK");
} else if (memcmp(config, "int.thresh ", 11) == 0) {
_prefs->interference_threshold = atoi(&config[11]);
savePrefs();
strcpy(reply, "OK");
} else if (memcmp(config, "allow.read.only ", 16) == 0) {
_prefs->allow_read_only = memcmp(&config[16], "on", 2) == 0;
savePrefs();

View file

@ -24,6 +24,7 @@ struct NodePrefs { // persisted to file
uint8_t reserved2;
float bw;
uint8_t flood_max;
uint8_t interference_threshold;
};
class CommonCLICallbacks {

18
src/helpers/ContactInfo.h Normal file
View file

@ -0,0 +1,18 @@
#pragma once
#include <Arduino.h>
#include <Mesh.h>
struct ContactInfo {
mesh::Identity id;
char name[32];
uint8_t type; // on of ADV_TYPE_*
uint8_t flags;
int8_t out_path_len;
uint8_t out_path[MAX_PATH_SIZE];
uint32_t last_advert_timestamp; // by THEIR clock
uint8_t shared_secret[PUB_KEY_SIZE];
uint32_t lastmod; // by OUR clock
int32_t gps_lat, gps_lon; // 6 dec places
uint32_t sync_since;
};

View file

@ -6,18 +6,11 @@
class CustomLLCC68Wrapper : public RadioLibWrapper {
public:
CustomLLCC68Wrapper(CustomLLCC68& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
bool isReceiving() override {
if (((CustomLLCC68 *)_radio)->isReceiving()) return true;
idle(); // put sx126x into standby
// do some basic CAD (blocks for ~12780 micros (on SF 10)!)
bool activity = (((CustomLLCC68 *)_radio)->scanChannel() == RADIOLIB_LORA_DETECTED);
if (activity) {
startRecv();
} else {
idle();
}
return activity;
bool isReceivingPacket() override {
return ((CustomLLCC68 *)_radio)->isReceiving();
}
float getCurrentRSSI() override {
return ((CustomLLCC68 *)_radio)->getRSSI(false);
}
float getLastRSSI() const override { return ((CustomLLCC68 *)_radio)->getRSSI(); }
float getLastSNR() const override { return ((CustomLLCC68 *)_radio)->getSNR(); }

View file

@ -6,18 +6,13 @@
class CustomLR1110Wrapper : public RadioLibWrapper {
public:
CustomLR1110Wrapper(CustomLR1110& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
bool isReceiving() override {
if (((CustomLR1110 *)_radio)->isReceiving()) return true;
idle(); // put sx126x into standby
// do some basic CAD (blocks for ~12780 micros (on SF 10)!)
bool activity = (((CustomLR1110 *)_radio)->scanChannel() == RADIOLIB_LORA_DETECTED);
if (activity) {
startRecv();
} else {
idle();
}
return activity;
bool isReceivingPacket() override {
return ((CustomLR1110 *)_radio)->isReceiving();
}
float getCurrentRSSI() override {
float rssi = -110;
((CustomLR1110 *)_radio)->getRssiInst(&rssi);
return rssi;
}
void onSendFinished() override {

View file

@ -7,18 +7,11 @@
class CustomSTM32WLxWrapper : public RadioLibWrapper {
public:
CustomSTM32WLxWrapper(CustomSTM32WLx& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
bool isReceiving() override {
if (((CustomSTM32WLx *)_radio)->isReceiving()) return true;
idle(); // put sx126x into standby
// do some basic CAD (blocks for ~12780 micros (on SF 10)!)
bool activity = (((CustomSTM32WLx *)_radio)->scanChannel() == RADIOLIB_LORA_DETECTED);
if (activity) {
startRecv();
} else {
idle();
}
return activity;
bool isReceivingPacket() override {
return ((CustomSTM32WLx *)_radio)->isReceiving();
}
float getCurrentRSSI() override {
return ((CustomSTM32WLx *)_radio)->getRSSI(false);
}
float getLastRSSI() const override { return ((CustomSTM32WLx *)_radio)->getRSSI(); }
float getLastSNR() const override { return ((CustomSTM32WLx *)_radio)->getSNR(); }

View file

@ -2,23 +2,15 @@
#include "CustomSX1262.h"
#include "RadioLibWrappers.h"
#include <math.h>
class CustomSX1262Wrapper : public RadioLibWrapper {
public:
CustomSX1262Wrapper(CustomSX1262& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
bool isReceiving() override {
if (((CustomSX1262 *)_radio)->isReceiving()) return true;
idle(); // put sx126x into standby
// do some basic CAD (blocks for ~12780 micros (on SF 10)!)
bool activity = (((CustomSX1262 *)_radio)->scanChannel() == RADIOLIB_LORA_DETECTED);
if (activity) {
startRecv();
} else {
idle();
}
return activity;
bool isReceivingPacket() override {
return ((CustomSX1262 *)_radio)->isReceiving();
}
float getCurrentRSSI() override {
return ((CustomSX1262 *)_radio)->getRSSI(false);
}
float getLastRSSI() const override { return ((CustomSX1262 *)_radio)->getRSSI(); }
float getLastSNR() const override { return ((CustomSX1262 *)_radio)->getSNR(); }

View file

@ -6,18 +6,11 @@
class CustomSX1268Wrapper : public RadioLibWrapper {
public:
CustomSX1268Wrapper(CustomSX1268& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
bool isReceiving() override {
if (((CustomSX1268 *)_radio)->isReceiving()) return true;
idle(); // put sx126x into standby
// do some basic CAD (blocks for ~12780 micros (on SF 10)!)
bool activity = (((CustomSX1268 *)_radio)->scanChannel() == RADIOLIB_LORA_DETECTED);
if (activity) {
startRecv();
} else {
idle();
}
return activity;
bool isReceivingPacket() override {
return ((CustomSX1268 *)_radio)->isReceiving();
}
float getCurrentRSSI() override {
return ((CustomSX1268 *)_radio)->getRSSI(false);
}
float getLastRSSI() const override { return ((CustomSX1268 *)_radio)->getRSSI(); }
float getLastSNR() const override { return ((CustomSX1268 *)_radio)->getSNR(); }

View file

@ -6,18 +6,11 @@
class CustomSX1276Wrapper : public RadioLibWrapper {
public:
CustomSX1276Wrapper(CustomSX1276& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
bool isReceiving() override {
if (((CustomSX1276 *)_radio)->isReceiving()) return true;
idle(); // put into standby
// do some basic CAD (blocks for ~12780 micros (on SF 10)!)
bool activity = (((CustomSX1276 *)_radio)->tryScanChannel() == RADIOLIB_PREAMBLE_DETECTED);
if (activity) {
startRecv();
} else {
idle();
}
return activity;
bool isReceivingPacket() override {
return ((CustomSX1276 *)_radio)->isReceiving();
}
float getCurrentRSSI() override {
return ((CustomSX1276 *)_radio)->getRSSI(false);
}
float getLastRSSI() const override { return ((CustomSX1276 *)_radio)->getRSSI(); }
float getLastSNR() const override { return ((CustomSX1276 *)_radio)->getSNR(); }

View file

@ -2,7 +2,7 @@
#include "ESP32Board.h"
#if defined(ADMIN_PASSWORD) // Repeater or Room Server only
#if defined(ADMIN_PASSWORD) && !defined(DISABLE_WIFI_OTA) // Repeater or Room Server only
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>

View file

@ -8,6 +8,8 @@
#define STATE_TX_DONE 4
#define STATE_INT_READY 16
#define NUM_NOISE_FLOOR_SAMPLES 64
static volatile uint8_t state = STATE_IDLE;
// this function is called when a complete packet
@ -28,6 +30,13 @@ void RadioLibWrapper::begin() {
if (_board->getStartupReason() == BD_STARTUP_RX_PACKET) { // received a LoRa packet (while in deep sleep)
setFlag(); // LoRa packet is already received
}
_noise_floor = 0;
_threshold = 0;
// start average out some samples
_num_floor_samples = 0;
_floor_sample_sum = 0;
}
void RadioLibWrapper::idle() {
@ -35,6 +44,34 @@ void RadioLibWrapper::idle() {
state = STATE_IDLE; // need another startReceive()
}
void RadioLibWrapper::triggerNoiseFloorCalibrate(int threshold) {
_threshold = threshold;
if (threshold > 0 && _num_floor_samples >= NUM_NOISE_FLOOR_SAMPLES) { // ignore trigger if currently sampling
_num_floor_samples = 0;
_floor_sample_sum = 0;
}
}
void RadioLibWrapper::loop() {
if (state == STATE_RX && _num_floor_samples < NUM_NOISE_FLOOR_SAMPLES) {
if (!isReceivingPacket()) {
int rssi = getCurrentRSSI();
if (rssi < _noise_floor + _threshold) { // only consider samples below current floor+THRESHOLD
_num_floor_samples++;
_floor_sample_sum += rssi;
}
}
} else if (_num_floor_samples >= NUM_NOISE_FLOOR_SAMPLES && _floor_sample_sum != 0) {
_noise_floor = _floor_sample_sum / NUM_NOISE_FLOOR_SAMPLES;
if (_noise_floor < -120) {
_noise_floor = -120; // clamp to lower bound of -120dBi
}
_floor_sample_sum = 0;
MESH_DEBUG_PRINTLN("RadioLibWrapper: noise_floor = %d", (int)_noise_floor);
}
}
void RadioLibWrapper::startRecv() {
int err = _radio->startReceive();
if (err == RADIOLIB_ERR_NONE) {
@ -108,6 +145,12 @@ void RadioLibWrapper::onSendFinished() {
state = STATE_IDLE;
}
bool RadioLibWrapper::isChannelActive() {
return _threshold == 0
? false // interference check is disabled
: getCurrentRSSI() > _noise_floor + _threshold;
}
float RadioLibWrapper::getLastRSSI() const {
return _radio->getRSSI();
}

View file

@ -8,10 +8,14 @@ protected:
PhysicalLayer* _radio;
mesh::MainBoard* _board;
uint32_t n_recv, n_sent;
int16_t _noise_floor, _threshold;
uint16_t _num_floor_samples;
int32_t _floor_sample_sum;
void idle();
void startRecv();
float packetScoreInt(float snr, int sf, int packet_len);
virtual bool isReceivingPacket() =0;
public:
RadioLibWrapper(PhysicalLayer& radio, mesh::MainBoard& board) : _radio(&radio), _board(&board) { n_recv = n_sent = 0; }
@ -23,6 +27,20 @@ public:
bool isSendComplete() override;
void onSendFinished() override;
bool isInRecvMode() const override;
bool isChannelActive();
bool isReceiving() override {
if (isReceivingPacket()) return true;
return isChannelActive();
}
virtual float getCurrentRSSI() =0;
int getNoiseFloor() const override { return _noise_floor; }
void triggerNoiseFloorCalibrate(int threshold) override;
void loop() override;
uint32_t getPacketsRecv() const { return n_recv; }
uint32_t getPacketsSent() const { return n_sent; }

View file

@ -7,6 +7,9 @@
// Defined using AXP2102
#define XPOWERS_CHIP_AXP2101
#define PIN_BOARD_SDA1 42 //SDA for PMU and PFC8563 (RTC)
#define PIN_BOARD_SCL1 41 //SCL for PMU and PFC8563 (RTC)
#define PIN_PMU_IRQ 40 //IRQ pin for PMU
// LoRa radio module pins for TBeam
#define P_LORA_DIO_0 26
@ -28,15 +31,13 @@
#include <driver/rtc_io.h>
class TBeamBoard : public ESP32Board {
XPowersAXP2101 power;
XPowersLibInterface *PMU = NULL;
public:
bool power_init();
void printPMU();
void begin() {
ESP32Board::begin();
power.setALDO2Voltage(3300);
power.enableALDO2();
pinMode(38, INPUT_PULLUP);
esp_reset_reason_t reason = esp_reset_reason();
@ -49,6 +50,7 @@ public:
rtc_gpio_hold_dis((gpio_num_t)P_LORA_NSS);
rtc_gpio_deinit((gpio_num_t)P_LORA_DIO_1);
}
power_init();
}
void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1) {
@ -75,7 +77,8 @@ public:
}
uint16_t getBattMilliVolts() override {
return power.getBattVoltage();
if(PMU) return PMU->getBattVoltage();
else return 0;
}
const char* getManufacturerName() const override {

View file

@ -22,7 +22,7 @@
#define PIN_BOARD_SCL1 41 //SCL for PMU and PFC8563 (RTC)
#define PIN_PMU_IRQ 40 //IRQ pin for PMU
#define PIN_USER_BTN 0
//#define PIN_USER_BTN 0
#define P_BOARD_SPI_MOSI 35 //SPI for SD Card and QMI8653 (IMU)
#define P_BOARD_SPI_MISO 37 //SPI for SD Card and QMI8653 (IMU)
@ -61,10 +61,10 @@ public:
void begin() {
power_init();
ESP32Board::begin();
power_init();
esp_reset_reason_t reason = esp_reset_reason();
if (reason == ESP_RST_DEEPSLEEP) {
long wakeup_source = esp_sleep_get_ext1_wakeup_status();
@ -75,6 +75,7 @@ public:
rtc_gpio_hold_dis((gpio_num_t)P_LORA_NSS);
rtc_gpio_deinit((gpio_num_t)P_LORA_DIO_1);
}
power_init();
}
void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1) {

View file

@ -80,6 +80,11 @@ bool RAK4631Board::startOTAUpdate(const char* id, char reply[]) {
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");
uint8_t mac_addr[6];
memset(mac_addr, 0, sizeof(mac_addr));
Bluefruit.getAddr(mac_addr);
sprintf(reply, "OK - mac: %02X:%02X:%02X:%02X:%02X:%02X",
mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]);
return true;
}

View file

@ -12,6 +12,17 @@
#define P_LORA_MISO 45
#define P_LORA_MOSI 44
#define SX126X_POWER_EN 37
#define P_GPS_SDA 13 //GPS SDA pin (output option)
#define P_GPS_SCL 14 //GPS SCL pin (output option)
#define P_GPS_TX 16 //GPS TX pin
#define P_GPS_RX 15 //GPS RX pin
#define P_GPS_STANDBY_A 34 //GPS Reset/Standby pin (IO2 for socket A)
#define P_GPS_STANDBY_C 4 //GPS Reset/Standby pin (IO4 for socket C)
#define P_GPS_STANDBY_F 9 //GPS Reset/Standby pin (IO5 for socket F)
#define P_GPS_1PPS 17 //GPS PPS pin
#define GPS_BAUD_RATE 9600
#define GPS_ADDRESS 0x42 //i2c address for GPS
#define SX126X_DIO2_AS_RF_SWITCH true
#define SX126X_DIO3_TCXO_VOLTAGE 1.8

View file

@ -4,13 +4,19 @@
class LocationProvider {
protected:
bool _time_sync_needed = true;
public:
virtual void syncTime() { _time_sync_needed = true; }
virtual bool waitingTimeSync() { return _time_sync_needed; }
virtual long getLatitude() = 0;
virtual long getLongitude() = 0;
virtual long getAltitude() = 0;
virtual long satellitesCount() = 0;
virtual bool isValid() = 0;
virtual long getTimestamp() = 0;
virtual void sendSentence(const char * sentence);
virtual void reset();
virtual void begin();
virtual void stop();

View file

@ -19,13 +19,16 @@
class MicroNMEALocationProvider : public LocationProvider {
char _nmeaBuffer[100];
MicroNMEA nmea;
mesh::RTCClock* _clock;
Stream* _gps_serial;
int _pin_reset;
int _pin_en;
long next_check = 0;
long time_valid = 0;
public :
MicroNMEALocationProvider(Stream& ser, int pin_reset = GPS_RESET, int pin_en = GPS_EN) :
_gps_serial(&ser), nmea(_nmeaBuffer, sizeof(_nmeaBuffer)), _pin_reset(pin_reset), _pin_en(pin_en) {
MicroNMEALocationProvider(Stream& ser, mesh::RTCClock* clock = NULL, int pin_reset = GPS_RESET, int pin_en = GPS_EN) :
_gps_serial(&ser), nmea(_nmeaBuffer, sizeof(_nmeaBuffer)), _pin_reset(pin_reset), _pin_en(pin_en), _clock(clock) {
if (_pin_reset != -1) {
pinMode(_pin_reset, OUTPUT);
digitalWrite(_pin_reset, GPS_RESET_FORCE);
@ -59,6 +62,7 @@ public :
}
}
void syncTime() override { nmea.clear(); LocationProvider::syncTime(); }
long getLatitude() override { return nmea.getLatitude(); }
long getLongitude() override { return nmea.getLongitude(); }
long getAltitude() override {
@ -66,6 +70,7 @@ public :
nmea.getAltitude(alt);
return alt;
}
long satellitesCount() override { return nmea.getNumSatellites(); }
bool isValid() override { return nmea.isValid(); }
long getTimestamp() override {
@ -73,7 +78,12 @@ public :
return dt.unixtime();
}
void sendSentence(const char *sentence) override {
nmea.sendSentence(*_gps_serial, sentence);
}
void loop() override {
while (_gps_serial->available()) {
char c = _gps_serial->read();
#ifdef GPS_NMEA_DEBUG
@ -81,5 +91,20 @@ public :
#endif
nmea.process(c);
}
if (!isValid()) time_valid = 0;
if (millis() > next_check) {
next_check = millis() + 1000;
if (_time_sync_needed && time_valid > 2) {
if (_clock != NULL) {
_clock->setCurrentTime(getTimestamp());
_time_sync_needed = false;
}
}
if (isValid()) {
time_valid ++;
}
}
}
};

View file

@ -126,11 +126,14 @@ InternalFileSystem::InternalFileSystem(void)
bool InternalFileSystem::begin(void)
{
volatile bool format_fs;
#ifdef FORMAT_FS
this->format();
format_fs = true;
#else
format_fs = false; // you can always use debugger to force formatting ;)
#endif
// failed to mount, erase all sector then format and mount again
if ( !Adafruit_LittleFS::begin() )
if ( format_fs || !Adafruit_LittleFS::begin() )
{
// lfs format
this->format();

View file

@ -5,22 +5,15 @@
#define DISPLAY_ROTATION 3
#endif
#ifdef TECHO_ZOOM
#define SCALE_X (1.5625f * 1.5f) // 200 / 128 (with 1.5 scale)
#define SCALE_Y (1.5625f * 1.5f) // 200 / 128 (with 1.5 scale)
#else
#define SCALE_X 1.5625f // 200 / 128
#define SCALE_Y 1.5625f // 200 / 128
#endif
#define SCALE_X 1.5625f // 200 / 128
#define SCALE_Y 1.5625f // 200 / 128
bool GxEPDDisplay::begin() {
display.epd2.selectSPI(SPI1, SPISettings(4000000, MSBFIRST, SPI_MODE0));
SPI1.begin();
display.init(115200, true, 2, false);
display.setRotation(DISPLAY_ROTATION);
#ifdef TECHO_ZOOM
display.setFont(&FreeMono9pt7b);
#endif
setTextSize(1); // Default to size 1
display.setPartialWindow(0, 0, display.width(), display.height());
display.fillScreen(GxEPD_WHITE);
@ -57,7 +50,20 @@ void GxEPDDisplay::startFrame(Color bkg) {
}
void GxEPDDisplay::setTextSize(int sz) {
display.setTextSize(sz);
switch(sz) {
case 1: // Small
display.setFont(&FreeSans9pt7b);
break;
case 2: // Medium Bold
display.setFont(&FreeSansBold12pt7b);
break;
case 3: // Large
display.setFont(&FreeSans18pt7b);
break;
default:
display.setFont(&FreeSans9pt7b);
break;
}
}
void GxEPDDisplay::setColor(Color c) {
@ -81,7 +87,39 @@ void GxEPDDisplay::drawRect(int x, int y, int w, int h) {
}
void GxEPDDisplay::drawXbm(int x, int y, const uint8_t* bits, int w, int h) {
display.drawBitmap(x*SCALE_X, (y*SCALE_Y) + 10, bits, w, h, GxEPD_BLACK);
// Calculate the base position in display coordinates
uint16_t startX = x * SCALE_X;
uint16_t startY = y * SCALE_Y;
// Width in bytes for bitmap processing
uint16_t widthInBytes = (w + 7) / 8;
// Process the bitmap row by row
for (uint16_t by = 0; by < h; by++) {
// Calculate the target y-coordinates for this logical row
int y1 = startY + (int)(by * SCALE_Y);
int y2 = startY + (int)((by + 1) * SCALE_Y);
int block_h = y2 - y1;
// Scan across the row bit by bit
for (uint16_t bx = 0; bx < w; bx++) {
// Calculate the target x-coordinates for this logical column
int x1 = startX + (int)(bx * SCALE_X);
int x2 = startX + (int)((bx + 1) * SCALE_X);
int block_w = x2 - x1;
// Get the current bit
uint16_t byteOffset = (by * widthInBytes) + (bx / 8);
uint8_t bitMask = 0x80 >> (bx & 7);
bool bitSet = pgm_read_byte(bits + byteOffset) & bitMask;
// 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);
}
}
}
}
uint16_t GxEPDDisplay::getTextWidth(const char* str) {

View file

@ -9,7 +9,9 @@
#include <GxEPD2_3C.h>
#include <GxEPD2_4C.h>
#include <GxEPD2_7C.h>
#include <Fonts/FreeMono9pt7b.h>
#include <Fonts/FreeSans9pt7b.h>
#include <Fonts/FreeSansBold12pt7b.h>
#include <Fonts/FreeSans18pt7b.h>
#define GxEPD2_DISPLAY_CLASS GxEPD2_BW
#define GxEPD2_DRIVER_CLASS GxEPD2_150_BN // DEPG0150BN 200x200, SSD1681, (FPC8101), TTGO T5 V2.4.1

View file

@ -11,6 +11,7 @@ void genericBuzzer::begin() {
quiet(false);
pinMode(PIN_BUZZER, OUTPUT);
digitalWrite(PIN_BUZZER, LOW); // need to pull low by default to avoid extreme power draw
startup();
}

View file

@ -56,13 +56,11 @@ build_flags =
${Generic_ESPNOW.build_flags}
-D MAX_CONTACTS=100
-D MAX_GROUP_CHANNELS=8
; -D ENABLE_PRIVATE_KEY_IMPORT=1
; -D ENABLE_PRIVATE_KEY_EXPORT=1
; 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/main.cpp>
+<../examples/companion_radio>
lib_deps =
${Generic_ESPNOW.lib_deps}
densaugeo/base64 @ ~1.4.0

View file

@ -39,7 +39,7 @@ extends = Heltec_tracker_base
build_flags =
${Heltec_tracker_base.build_flags}
-I src/helpers/ui
; -D ARDUINO_USB_CDC_ON_BOOT=1 ; need for debugging
-D ARDUINO_USB_CDC_ON_BOOT=1 ; need for Serial
-D DISPLAY_ROTATION=1
-D DISPLAY_CLASS=ST7735Display
-D MAX_CONTACTS=100
@ -47,8 +47,6 @@ build_flags =
-D BLE_PIN_CODE=123456 ; HWT will use display for pin
-D OFFLINE_QUEUE_SIZE=256
; -D BLE_DEBUG_LOGGING=1
; -D ENABLE_PRIVATE_KEY_IMPORT=1
; -D ENABLE_PRIVATE_KEY_EXPORT=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${Heltec_tracker_base.build_src_filter}

View file

@ -99,8 +99,6 @@ build_flags =
-D BLE_PIN_CODE=0
-D BLE_DEBUG_LOGGING=1
-D OFFLINE_QUEUE_SIZE=256
; -D ENABLE_PRIVATE_KEY_IMPORT=1
; -D ENABLE_PRIVATE_KEY_EXPORT=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${Heltec_lora32_v2.build_src_filter}

View file

@ -17,11 +17,20 @@ build_flags =
-D SX126X_DIO3_TCXO_VOLTAGE=1.8
-D SX126X_CURRENT_LIMIT=140
-D SX126X_RX_BOOSTED_GAIN=1
-D ENV_INCLUDE_AHTX0=1
-D ENV_INCLUDE_BME280=1
-D ENV_INCLUDE_INA3221=1
-D ENV_INCLUDE_INA219=1
build_src_filter = ${esp32_base.build_src_filter}
+<../variants/heltec_v3>
+<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
[env:Heltec_v3_repeater]
extends = Heltec_lora32_v3
@ -82,8 +91,6 @@ build_flags =
-D MAX_CONTACTS=100
-D MAX_GROUP_CHANNELS=8
-D DISPLAY_CLASS=SSD1306Display
; -D ENABLE_PRIVATE_KEY_IMPORT=1
; -D ENABLE_PRIVATE_KEY_EXPORT=1
; 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}
@ -103,8 +110,6 @@ build_flags =
-D BLE_PIN_CODE=0 ; dynamic, random PIN
-D BLE_DEBUG_LOGGING=1
-D OFFLINE_QUEUE_SIZE=256
; -D ENABLE_PRIVATE_KEY_IMPORT=1
; -D ENABLE_PRIVATE_KEY_EXPORT=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${Heltec_lora32_v3.build_src_filter}
@ -125,8 +130,6 @@ build_flags =
-D WIFI_DEBUG_LOGGING=1
-D WIFI_SSID='"myssid"'
-D WIFI_PWD='"mypwd"'
; -D ENABLE_PRIVATE_KEY_IMPORT=1
; -D ENABLE_PRIVATE_KEY_EXPORT=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${Heltec_lora32_v3.build_src_filter}
@ -180,8 +183,6 @@ build_flags =
-D BLE_PIN_CODE=123456
-D BLE_DEBUG_LOGGING=1
-D OFFLINE_QUEUE_SIZE=256
; -D ENABLE_PRIVATE_KEY_IMPORT=1
; -D ENABLE_PRIVATE_KEY_EXPORT=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${Heltec_lora32_v3.build_src_filter}

View file

@ -14,7 +14,7 @@ WRAPPER_CLASS radio_driver(radio, board);
ESP32RTCClock fallback_clock;
AutoDiscoverRTCClock rtc_clock(fallback_clock);
SensorManager sensors;
EnvironmentSensorManager sensors;
#ifdef DISPLAY_CLASS
DISPLAY_CLASS display;

View file

@ -7,6 +7,7 @@
#include <helpers/CustomSX1262Wrapper.h>
#include <helpers/AutoDiscoverRTCClock.h>
#include <helpers/SensorManager.h>
#include <helpers/sensors/EnvironmentSensorManager.h>
#ifdef DISPLAY_CLASS
#include <helpers/ui/SSD1306Display.h>
#endif
@ -14,7 +15,7 @@
extern HeltecV3Board board;
extern WRAPPER_CLASS radio_driver;
extern AutoDiscoverRTCClock rtc_clock;
extern SensorManager sensors;
extern EnvironmentSensorManager sensors;
#ifdef DISPLAY_CLASS
extern DISPLAY_CLASS display;

View file

@ -92,8 +92,6 @@ build_flags =
-D DISPLAY_CLASS=SSD1306Display
-D MAX_CONTACTS=100
-D MAX_GROUP_CHANNELS=8
; -D ENABLE_PRIVATE_KEY_IMPORT=1
; -D ENABLE_PRIVATE_KEY_EXPORT=1
; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1
; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1
build_src_filter = ${LilyGo_T3S3_sx1262.build_src_filter}
@ -113,8 +111,6 @@ build_flags =
-D BLE_PIN_CODE=123456
-D BLE_DEBUG_LOGGING=1
-D OFFLINE_QUEUE_SIZE=256
; -D ENABLE_PRIVATE_KEY_IMPORT=1
; -D ENABLE_PRIVATE_KEY_EXPORT=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${LilyGo_T3S3_sx1262.build_src_filter}

View file

@ -33,8 +33,6 @@ build_flags =
-D BLE_DEBUG_LOGGING=1
-D OFFLINE_QUEUE_SIZE=256
; -D RADIOLIB_DEBUG_BASIC=1
; -D ENABLE_PRIVATE_KEY_IMPORT=1
; -D ENABLE_PRIVATE_KEY_EXPORT=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${LilyGo_TBeam.build_src_filter}

View file

@ -3,6 +3,14 @@
TBeamBoard board;
// Using PMU AXP2102
#define PMU_WIRE_PORT Wire
bool pmuIntFlag = false;
static void setPMUIntFlag(){
pmuIntFlag = true;
}
#if defined(P_LORA_SCLK)
static SPIClass spi;
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_0, P_LORA_RESET, P_LORA_DIO_1, spi);
@ -24,6 +32,112 @@ SensorManager sensors;
#define LORA_CR 5
#endif
bool TBeamBoard::power_init()
{
if (!PMU)
{
PMU = new XPowersAXP2101(PMU_WIRE_PORT);
if (!PMU->init())
{
// Serial.println("Warning: Failed to find AXP2101 power management");
delete PMU;
PMU = NULL;
}
else
{
// Serial.println("AXP2101 PMU init succeeded, using AXP2101 PMU");
}
}
if (!PMU)
{
PMU = new XPowersAXP192(PMU_WIRE_PORT);
if (!PMU->init())
{
// Serial.println("Warning: Failed to find AXP192 power management");
delete PMU;
PMU = NULL;
}
else
{
// Serial.println("AXP192 PMU init succeeded, using AXP192 PMU");
}
}
if (!PMU)
{
MESH_DEBUG_PRINTLN("PMU init failed.");
return false;
}
// Serial.printf("PMU ID:0x%x\n", PMU->getChipID());
// printPMU();
if (PMU->getChipModel() == XPOWERS_AXP192)
{
// lora radio power channel
PMU->setPowerChannelVoltage(XPOWERS_LDO2, 3300);
PMU->enablePowerOutput(XPOWERS_LDO2);
// oled module power channel,
// disable it will cause abnormal communication between boot and AXP power supply,
// do not turn it off
PMU->setPowerChannelVoltage(XPOWERS_DCDC1, 3300);
// enable oled power
PMU->enablePowerOutput(XPOWERS_DCDC1);
// gnss module power channel
PMU->setPowerChannelVoltage(XPOWERS_LDO3, 3300);
// power->enablePowerOutput(XPOWERS_LDO3);
// protected oled power source
PMU->setProtectedChannel(XPOWERS_DCDC1);
// protected esp32 power source
PMU->setProtectedChannel(XPOWERS_DCDC3);
// disable not use channel
PMU->disablePowerOutput(XPOWERS_DCDC2);
// disable all axp chip interrupt
PMU->disableIRQ(XPOWERS_AXP192_ALL_IRQ);
PMU->setChargerConstantCurr(XPOWERS_AXP192_CHG_CUR_550MA);
}
else if (PMU->getChipModel() == XPOWERS_AXP2101)
{
// gnss module power channel
PMU->setPowerChannelVoltage(XPOWERS_ALDO4, 3300);
PMU->enablePowerOutput(XPOWERS_ALDO4);
// lora radio power channel
PMU->setPowerChannelVoltage(XPOWERS_ALDO3, 3300);
PMU->enablePowerOutput(XPOWERS_ALDO3);
// m.2 interface
PMU->setPowerChannelVoltage(XPOWERS_DCDC3, 3300);
PMU->enablePowerOutput(XPOWERS_DCDC3);
// power->setPowerChannelVoltage(XPOWERS_DCDC4, 3300);
// power->enablePowerOutput(XPOWERS_DCDC4);
// not use channel
PMU->disablePowerOutput(XPOWERS_DCDC2); // not elicited
PMU->disablePowerOutput(XPOWERS_DCDC5); // not elicited
PMU->disablePowerOutput(XPOWERS_DLDO1); // Invalid power channel, it does not exist
PMU->disablePowerOutput(XPOWERS_DLDO2); // Invalid power channel, it does not exist
PMU->disablePowerOutput(XPOWERS_VBACKUP);
// disable all axp chip interrupt
PMU->disableIRQ(XPOWERS_AXP2101_ALL_IRQ);
PMU->setChargerConstantCurr(XPOWERS_AXP2101_CHG_CUR_500MA);
// Set up PMU interrupts
// Serial.println("Setting up PMU interrupts");
pinMode(PIN_PMU_IRQ, INPUT_PULLUP);
attachInterrupt(PIN_PMU_IRQ, setPMUIntFlag, FALLING);
// Reset and re-enable PMU interrupts
// Serial.println("Re-enable interrupts");
PMU->disableIRQ(XPOWERS_AXP2101_ALL_IRQ);
PMU->clearIrqStatus();
PMU->enableIRQ(
XPOWERS_AXP2101_BAT_INSERT_IRQ | XPOWERS_AXP2101_BAT_REMOVE_IRQ | // Battery interrupts
XPOWERS_AXP2101_VBUS_INSERT_IRQ | XPOWERS_AXP2101_VBUS_REMOVE_IRQ | // VBUS interrupts
XPOWERS_AXP2101_PKEY_SHORT_IRQ | XPOWERS_AXP2101_PKEY_LONG_IRQ | // Power Key interrupts
XPOWERS_AXP2101_BAT_CHG_DONE_IRQ | XPOWERS_AXP2101_BAT_CHG_START_IRQ // Charging interrupts
);
}
return true;
}
bool radio_init() {
fallback_clock.begin();
rtc_clock.begin(Wire);
@ -66,3 +180,24 @@ mesh::LocalIdentity radio_new_identity() {
RadioNoiseListener rng(radio);
return mesh::LocalIdentity(&rng); // create new random identity
}
#ifdef MESH_DEBUG
void TBeamBoard::printPMU()
{
Serial.print("isCharging:"); Serial.println(PMU->isCharging() ? "YES" : "NO");
Serial.print("isDischarge:"); Serial.println(PMU->isDischarge() ? "YES" : "NO");
Serial.print("isVbusIn:"); Serial.println(PMU->isVbusIn() ? "YES" : "NO");
Serial.print("getBattVoltage:"); Serial.print(PMU->getBattVoltage()); Serial.println("mV");
Serial.print("getVbusVoltage:"); Serial.print(PMU->getVbusVoltage()); Serial.println("mV");
Serial.print("getSystemVoltage:"); Serial.print(PMU->getSystemVoltage()); Serial.println("mV");
// The battery percentage may be inaccurate at first use, the PMU will automatically
// learn the battery curve and will automatically calibrate the battery percentage
// after a charge and discharge cycle
if (PMU->isBatteryConnect()) {
Serial.print("getBatteryPercent:"); Serial.print(PMU->getBatteryPercent()); Serial.println("%");
}
Serial.println();
}
#endif

View file

@ -38,8 +38,6 @@ build_flags =
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
; -D RADIOLIB_DEBUG_BASIC=1
; -D ENABLE_PRIVATE_KEY_IMPORT=1
; -D ENABLE_PRIVATE_KEY_EXPORT=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${LilyGo_TBeam_SX1262.build_src_filter}

View file

@ -8,19 +8,22 @@ build_flags =
-D P_LORA_TX_LED=6
-D PIN_BOARD_SDA=17
-D PIN_BOARD_SCL=18
-D PIN_USER_BTN=0
-D RADIO_CLASS=CustomSX1262
-D WRAPPER_CLASS=CustomSX1262Wrapper
;-D DISPLAY_CLASS=SSD1306Display ;Needs to be modified for SH1106
-D DISPLAY_CLASS=SH1106Display
-D SX126X_RX_BOOSTED_GAIN=1
-D SX126X_CURRENT_LIMIT=140
build_src_filter = ${esp32_base.build_src_filter}
+<../variants/lilygo_tbeam_supreme_SX1262>
+<helpers/ui/SH1106Display.cpp>
board_build.partitions = min_spiffs.csv ; get around 4mb flash limit
lib_deps =
${esp32_base.lib_deps}
lewisxhe/XPowersLib @ ^0.2.7
;adafruit/Adafruit SSD1306 @ ^2.5.13
adafruit/Adafruit SH110X @ ^2.1.13
stevemarple/MicroNMEA @ ^2.0.6
adafruit/Adafruit BME280 Library @ ^2.3.0
; === LILYGO T-Beam S3 Supreme with SX1262 environments ===
[env:T_Beam_S3_Supreme_SX1262_repeater]
@ -64,10 +67,8 @@ 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 ENABLE_PRIVATE_KEY_IMPORT=1
; -D ENABLE_PRIVATE_KEY_EXPORT=1
; -D BLE_DEBUG_LOGGING=1
; -D MESH_PACKET_LOGGING=8
; -D MESH_DEBUG=1
build_src_filter = ${T_Beam_S3_Supreme_SX1262.build_src_filter}

View file

@ -4,6 +4,10 @@
TBeamS3SupremeBoard board;
#ifdef DISPLAY_CLASS
DISPLAY_CLASS display;
#endif
bool pmuIntFlag;
#ifndef LORA_CR
@ -46,7 +50,7 @@ void scanDevices(TwoWire *w)
switch (addr) {
case 0x77:
case 0x76:
Serial.println("\tFound BMX280 Sensor");
Serial.println("\tFound BME280 Sensor");
deviceOnline |= BME280_ONLINE;
break;
case 0x34:
@ -107,6 +111,26 @@ void TBeamS3SupremeBoard::printPMU()
Serial.println();
}
void TbeamSupSensorManager::printBMEValues() {
Serial.print("Temperature = ");
Serial.print(bme.readTemperature());
Serial.println(" *C");
Serial.print("Pressure = ");
Serial.print(bme.readPressure() / 100.0F);
Serial.println(" hPa");
Serial.print("Approx. Altitude = ");
Serial.print(bme.readAltitude(SEALEVELPRESSURE_HPA));
Serial.println(" m");
Serial.print("Humidity = ");
Serial.print(bme.readHumidity());
Serial.println(" %");
Serial.println();
}
#endif
bool TBeamS3SupremeBoard::power_init()
@ -119,9 +143,9 @@ bool TBeamS3SupremeBoard::power_init()
PMU.setChargingLedMode(XPOWERS_CHG_LED_CTRL_CHG);
// Set up PMU interrupts
// MESH_DEBUG_PRINTLN("Setting up PMU interrupts");
// pinMode(PIN_PMU_IRQ, INPUT_PULLUP);
// attachInterrupt(PIN_PMU_IRQ, setPMUIntFlag, FALLING);
MESH_DEBUG_PRINTLN("Setting up PMU interrupts");
pinMode(PIN_PMU_IRQ, INPUT_PULLUP);
attachInterrupt(PIN_PMU_IRQ, setPMUIntFlag, FALLING);
// GPS
MESH_DEBUG_PRINTLN("Setting and enabling a-ldo4 for GPS");
@ -164,22 +188,22 @@ bool TBeamS3SupremeBoard::power_init()
PMU.enableBLDO1();
// Out to header pins
// MESH_DEBUG_PRINTLN("Setting and enabling b-ldo2 for output to header");
// PMU.setBLDO2Voltage(3300);
// PMU.enableBLDO2();
MESH_DEBUG_PRINTLN("Setting and enabling b-ldo2 for output to header");
PMU.setBLDO2Voltage(3300);
PMU.enableBLDO2();
// MESH_DEBUG_PRINTLN("Setting and enabling dcdc4 for output to header");
// PMU.setDC4Voltage(XPOWERS_AXP2101_DCDC4_VOL2_MAX); // 1.8V
// PMU.enableDC4();
MESH_DEBUG_PRINTLN("Setting and enabling dcdc4 for output to header");
PMU.setDC4Voltage(XPOWERS_AXP2101_DCDC4_VOL2_MAX); // 1.8V
PMU.enableDC4();
// MESH_DEBUG_PRINTLN("Setting and enabling dcdc5 for output to header");
// PMU.setDC5Voltage(3300);
// PMU.enableDC5();
MESH_DEBUG_PRINTLN("Setting and enabling dcdc5 for output to header");
PMU.setDC5Voltage(3300);
PMU.enableDC5();
// Unused power rails
MESH_DEBUG_PRINTLN("Disabling unused supplies dcdc2, dldo1 and dldo2");
MESH_DEBUG_PRINTLN("Disabling unused supplies dcdc2, dcdc5, dldo1 and dldo2");
PMU.disableDC2();
PMU.disableDC5();
//PMU.disableDC5();
PMU.disableDLDO1();
PMU.disableDLDO2();
@ -199,18 +223,18 @@ bool TBeamS3SupremeBoard::power_init()
PMU.enableVbusVoltageMeasure();
// Reset and re-enable PMU interrupts
// MESH_DEBUG_PRINTLN("Re-enable interrupts");
// PMU.disableIRQ(XPOWERS_AXP2101_ALL_IRQ);
// PMU.clearIrqStatus();
// PMU.enableIRQ(
// XPOWERS_AXP2101_BAT_INSERT_IRQ | XPOWERS_AXP2101_BAT_REMOVE_IRQ | // Battery interrupts
// XPOWERS_AXP2101_VBUS_INSERT_IRQ | XPOWERS_AXP2101_VBUS_REMOVE_IRQ | // VBUS interrupts
// XPOWERS_AXP2101_PKEY_SHORT_IRQ | XPOWERS_AXP2101_PKEY_LONG_IRQ | // Power Key interrupts
// XPOWERS_AXP2101_BAT_CHG_DONE_IRQ | XPOWERS_AXP2101_BAT_CHG_START_IRQ // Charging interrupts
// );
MESH_DEBUG_PRINTLN("Re-enable interrupts");
PMU.disableIRQ(XPOWERS_AXP2101_ALL_IRQ);
PMU.clearIrqStatus();
PMU.enableIRQ(
XPOWERS_AXP2101_BAT_INSERT_IRQ | XPOWERS_AXP2101_BAT_REMOVE_IRQ | // Battery interrupts
XPOWERS_AXP2101_VBUS_INSERT_IRQ | XPOWERS_AXP2101_VBUS_REMOVE_IRQ | // VBUS interrupts
XPOWERS_AXP2101_PKEY_SHORT_IRQ | XPOWERS_AXP2101_PKEY_LONG_IRQ | // Power Key interrupts
XPOWERS_AXP2101_BAT_CHG_DONE_IRQ | XPOWERS_AXP2101_BAT_CHG_START_IRQ // Charging interrupts
);
#ifdef MESH_DEBUG
// scanDevices(&Wire);
// scanDevices(&Wire1);
scanDevices(&Wire);
scanDevices(&Wire1);
printPMU();
#endif
@ -235,60 +259,14 @@ static bool readStringUntil(Stream& s, char dest[], size_t max_len, char term, u
return millis() < timeout; // false, if timed out
}
static bool l76kProbe()
{
bool result = false;
uint32_t startTimeout ;
Serial1.write("$PCAS03,0,0,0,0,0,0,0,0,0,0,,,0,0*02\r\n");
delay(5);
// Get version information
startTimeout = millis() + 3000;
MESH_DEBUG_PRINTLN("Trying to init L76K GPS");
// Serial1.flush();
while (Serial1.available()) {
int c = Serial1.read();
// Serial.write(c);
// Serial.print(".");
// Serial.flush();
// Serial1.flush();
if (millis() > startTimeout) {
MESH_DEBUG_PRINTLN("L76K NMEA timeout!");
return false;
}
};
Serial1.flush();
delay(200);
Serial1.write("$PCAS06,0*1B\r\n");
char ver[100];
if (!readStringUntil(Serial1, ver, sizeof(ver), '\n', 500)) {
MESH_DEBUG_PRINTLN("Get L76K timeout!");
return false;
}
if (memcmp(ver, "$GPTXT,01,01,02", 15) == 0) {
MESH_DEBUG_PRINTLN("L76K GNSS init succeeded, using L76K GNSS Module\n");
result = true;
}
delay(500);
// Initialize the L76K Chip, use GPS + GLONASS
Serial1.write("$PCAS04,5*1C\r\n");
delay(250);
// only ask for RMC and GGA
Serial1.write("$PCAS03,1,0,0,0,1,0,0,0,0,0,,,0,0*02\r\n");
delay(250);
// Switch to Vehicle Mode, since SoftRF enables Aviation < 2g
Serial1.write("$PCAS11,3*1E\r\n");
return result;
}
bool radio_init() {
fallback_clock.begin();
rtc_clock.begin(Wire1);
// #ifdef MESH_DEBUG
// printBMEValues();
// #endif
#ifdef SX126X_DIO3_TCXO_VOLTAGE
float tcxo = SX126X_DIO3_TCXO_VOLTAGE;
@ -340,55 +318,95 @@ void TbeamSupSensorManager::sleep_gps() {
}
bool TbeamSupSensorManager::begin() {
//init BME280
if (! bme.begin(0x77, &Wire)) {
MESH_DEBUG_PRINTLN("Could not find a valid BME280 sensor");
bme_active = false;
}
else
MESH_DEBUG_PRINTLN("BME280 found and init!");
bme_active = true;
// init GPS port
Serial1.begin(GPS_BAUD_RATE, SERIAL_8N1, P_GPS_RX, P_GPS_TX);
bool result = false;
for ( int i = 0; i < 3; ++i) {
result = l76kProbe();
if (result) {
gps_active = true;
return result;
}
}
return result;
MESH_DEBUG_PRINTLN("Sleeping GPS for initial state");
sleep_gps();
return true;
}
bool TbeamSupSensorManager::querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) {
if (requester_permissions & TELEM_PERM_LOCATION) { // does requester have permission?
if (requester_permissions & TELEM_PERM_LOCATION && gps_active) { // does requester have permission?
telemetry.addGPS(TELEM_CHANNEL_SELF, node_lat, node_lon, node_altitude);
}
if (requester_permissions & TELEM_PERM_ENVIRONMENT && bme_active) { // does requester have permission?
telemetry.addTemperature(TELEM_CHANNEL_SELF, node_temp);
telemetry.addRelativeHumidity(TELEM_CHANNEL_SELF, node_hum);
telemetry.addBarometricPressure(TELEM_CHANNEL_SELF, node_pres);
//telemetry.addAltitude(TELEM_CHANNEL_SELF, node_alt);
}
return true;
}
void TbeamSupSensorManager::loop() {
static long next_gps_update = 0;
static long next_update = 0;
_nmea->loop();
if (millis() > next_gps_update) {
if (_nmea->isValid()) {
if (millis() > next_update) {
if (_nmea->isValid() && gps_active) {
node_lat = ((double)_nmea->getLatitude())/1000000.;
node_lon = ((double)_nmea->getLongitude())/1000000.;
node_altitude = ((double)_nmea->getAltitude()) / 1000.0;
//Serial.printf("lat %f lon %f\r\n", _lat, _lon);
MESH_DEBUG_PRINT("lat %f lon %f alt %f\r\n", node_lat, node_lon, node_altitude);
}
next_gps_update = millis() + 1000;
//read BME280 values
if(bme_active){
//node_alt = bme.readAltitude(SEALEVELPRESSURE_HPA);
node_temp = bme.readTemperature();
node_hum = bme.readHumidity();
node_pres = (bme.readPressure() / 100.0F);
#ifdef MESH_DEBUG
// Serial.print("Temperature = ");
// Serial.print(node_temp);
// Serial.println(" *C");
// Serial.print("Humidity = ");
// Serial.print(node_hum);
// Serial.println(" %");
// Serial.print("Pressure = ");
// Serial.print(node_pres);
// Serial.println(" hPa");
// Serial.print("Approx. Altitude = ");
// Serial.print(node_alt);
// Serial.println(" m");
#endif
}
next_update = millis() + 1000;
}
}
int TbeamSupSensorManager::getNumSettings() const { return 1; } // just one supported: "gps" (power switch)
int TbeamSupSensorManager::getNumSettings() const {
return 1;
}
const char* TbeamSupSensorManager::getSettingName(int i) const {
return i == 0 ? "gps" : NULL;
switch(i){
case 0: return "gps";
default: NULL;
}
}
const char* TbeamSupSensorManager::getSettingValue(int i) const {
if (i == 0) {
return gps_active ? "1" : "0";
switch(i){
case 0: return gps_active == true ? "1" : "0";
default: NULL;
}
return NULL;
}
bool TbeamSupSensorManager::setSettingValue(const char* name, const char* value) {

View file

@ -8,15 +8,21 @@
#include <helpers/AutoDiscoverRTCClock.h>
#include <helpers/SensorManager.h>
#include <helpers/sensors/LocationProvider.h>
#include <Adafruit_BME280.h>
class TbeamSupSensorManager: public SensorManager {
bool gps_active = false;
bool bme_active = false;
LocationProvider * _nmea;
Adafruit_BME280 bme;
double node_temp, node_hum, node_pres;
#define SEALEVELPRESSURE_HPA (1013.25)
void start_gps();
void sleep_gps();
public:
TbeamSupSensorManager(LocationProvider &nmea): _nmea(&nmea) { }
TbeamSupSensorManager(LocationProvider &nmea): _nmea(&nmea) {node_temp = 0; node_hum = 0; node_pres = 0;}
bool begin() override;
bool querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) override;
void loop() override;
@ -24,6 +30,11 @@ class TbeamSupSensorManager: public SensorManager {
const char* getSettingName(int i) const override;
const char* getSettingValue(int i) const override;
bool setSettingValue(const char* name, const char* value) override;
#ifdef MESH_DEBUG
void printBMEValues();
#endif
};
extern TBeamS3SupremeBoard board;
@ -31,6 +42,11 @@ extern WRAPPER_CLASS radio_driver;
extern AutoDiscoverRTCClock rtc_clock;
extern TbeamSupSensorManager sensors;
#ifdef DISPLAY_CLASS
#include <helpers/ui/SH1106Display.h>
extern DISPLAY_CLASS display;
#endif
enum {
POWERMANAGE_ONLINE = _BV(0),
DISPLAY_ONLINE = _BV(1),
@ -48,7 +64,6 @@ enum {
OSC32768_ONLINE = _BV(13),
};
bool power_init();
bool radio_init();
uint32_t radio_get_rng_seed();
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr);

View file

@ -72,8 +72,6 @@ build_flags =
${LilyGo_TLora_V2_1_1_6.build_flags}
-D MAX_CONTACTS=100
-D MAX_GROUP_CHANNELS=8
; -D ENABLE_PRIVATE_KEY_IMPORT=1
; -D ENABLE_PRIVATE_KEY_EXPORT=1
; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1
; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1
build_src_filter = ${LilyGo_TLora_V2_1_1_6.build_src_filter}
@ -92,8 +90,6 @@ build_flags =
-D BLE_PIN_CODE=123456
-D OFFLINE_QUEUE_SIZE=256
; -D BLE_DEBUG_LOGGING=1
; -D ENABLE_PRIVATE_KEY_IMPORT=1
; -D ENABLE_PRIVATE_KEY_EXPORT=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${LilyGo_TLora_V2_1_1_6.build_src_filter}

View file

@ -41,8 +41,6 @@ build_flags =
-D OFFLINE_QUEUE_SIZE=256
-D DISPLAY_CLASS=SH1106Display
-D PIN_BUZZER=4
; -D ENABLE_PRIVATE_KEY_IMPORT=1
; -D ENABLE_PRIVATE_KEY_EXPORT=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${Nano_G2_Ultra.build_src_filter}

View file

@ -49,8 +49,6 @@ extends = picow
build_flags = ${picow.build_flags}
-D MAX_CONTACTS=100
-D MAX_GROUP_CHANNELS=8
; -D ENABLE_PRIVATE_KEY_IMPORT=1
; -D ENABLE_PRIVATE_KEY_EXPORT=1
; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1
; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1
build_src_filter = ${picow.build_src_filter}
@ -65,8 +63,6 @@ lib_deps = ${picow.lib_deps}
; -D MAX_GROUP_CHANNELS=8
; -D BLE_PIN_CODE=123456
; -D BLE_DEBUG_LOGGING=1
; ; -D ENABLE_PRIVATE_KEY_IMPORT=1
; ; -D ENABLE_PRIVATE_KEY_EXPORT=1
; ; -D MESH_PACKET_LOGGING=1
; ; -D MESH_DEBUG=1
; build_src_filter = ${picow.build_src_filter}
@ -82,8 +78,6 @@ lib_deps = ${picow.lib_deps}
; -D WIFI_DEBUG_LOGGING=1
; -D WIFI_SSID='"myssid"'
; -D WIFI_PWD='"mypwd"'
; ; -D ENABLE_PRIVATE_KEY_IMPORT=1
; ; -D ENABLE_PRIVATE_KEY_EXPORT=1
; ; -D MESH_PACKET_LOGGING=1
; ; -D MESH_DEBUG=1
; build_src_filter = ${picow.build_src_filter}

View file

@ -103,8 +103,6 @@ build_flags = ${Faketec.build_flags}
-D MAX_GROUP_CHANNELS=8
-D BLE_PIN_CODE=123456
-D BLE_DEBUG_LOGGING=1
-D ENABLE_PRIVATE_KEY_EXPORT=1
-D ENABLE_PRIVATE_KEY_IMPORT=1
-D OFFLINE_QUEUE_SIZE=256
-D DISPLAY_CLASS=SSD1306Display
; -D MESH_PACKET_LOGGING=1
@ -186,7 +184,7 @@ build_flags = ${ProMicroLLCC68.build_flags}
; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1
; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1
build_src_filter = ${ProMicroLLCC68.build_src_filter}
+<../examples/companion_radio/main.cpp>
+<../examples/companion_radio>
lib_deps = ${ProMicroLLCC68.lib_deps}
adafruit/RTClib @ ^2.1.3
densaugeo/base64 @ ~1.4.0
@ -198,14 +196,12 @@ build_flags = ${ProMicroLLCC68.build_flags}
-D MAX_GROUP_CHANNELS=8
-D BLE_PIN_CODE=123456
-D BLE_DEBUG_LOGGING=1
-D ENABLE_PRIVATE_KEY_EXPORT=1
-D ENABLE_PRIVATE_KEY_IMPORT=1
-D OFFLINE_QUEUE_SIZE=256
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${ProMicroLLCC68.build_src_filter}
+<helpers/nrf52/SerialBLEInterface.cpp>
+<../examples/companion_radio/main.cpp>
+<../examples/companion_radio>
lib_deps = ${ProMicroLLCC68.lib_deps}
adafruit/RTClib @ ^2.1.3
densaugeo/base64 @ ~1.4.0

View file

@ -22,6 +22,8 @@ build_src_filter = ${nrf52840_base.build_src_filter}
lib_deps =
${nrf52840_base.lib_deps}
adafruit/Adafruit SSD1306 @ ^2.5.13
stevemarple/MicroNMEA @ ^2.0.6
sparkfun/SparkFun u-blox GNSS Arduino Library @ ^2.2.27
[env:RAK_4631_Repeater]
extends = rak4631
@ -62,8 +64,6 @@ build_flags =
-D DISPLAY_CLASS=SSD1306Display
-D MAX_CONTACTS=100
-D MAX_GROUP_CHANNELS=8
; -D ENABLE_PRIVATE_KEY_IMPORT=1
; -D ENABLE_PRIVATE_KEY_EXPORT=1
; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1
; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1
build_src_filter = ${rak4631.build_src_filter}
@ -83,8 +83,27 @@ build_flags =
-D BLE_PIN_CODE=123456
-D BLE_DEBUG_LOGGING=1
-D OFFLINE_QUEUE_SIZE=256
; -D ENABLE_PRIVATE_KEY_IMPORT=1
; -D ENABLE_PRIVATE_KEY_EXPORT=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${rak4631.build_src_filter}
+<helpers/ui/SSD1306Display.cpp>
+<helpers/nrf52/SerialBLEInterface.cpp>
+<../examples/companion_radio>
lib_deps =
${rak4631.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:RAK_4631_GPS_companion_radio_ble]
extends = rak4631
build_flags =
${rak4631.build_flags}
-D DISPLAY_CLASS=SSD1306Display
-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 ENV_INCLUDE_GPS=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${rak4631.build_src_filter}

View file

@ -1,6 +1,7 @@
#include <Arduino.h>
#include "target.h"
#include <helpers/ArduinoHelpers.h>
#include <helpers/sensors/MicroNMEALocationProvider.h>
RAK4631Board board;
@ -10,7 +11,13 @@ WRAPPER_CLASS radio_driver(radio, board);
VolatileRTCClock fallback_clock;
AutoDiscoverRTCClock rtc_clock(fallback_clock);
SensorManager sensors;
#if ENV_INCLUDE_GPS
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Wire);
RAK4631SensorManager sensors = RAK4631SensorManager(nmea);
#else
RAK4631SensorManager sensors;
#endif
#ifdef DISPLAY_CLASS
DISPLAY_CLASS display;
@ -20,6 +27,52 @@ SensorManager sensors;
#define LORA_CR 5
#endif
#ifdef MESH_DEBUG
uint32_t deviceOnline = 0x00;
void scanDevices(TwoWire *w)
{
uint8_t err, addr;
int nDevices = 0;
uint32_t start = 0;
Serial.println("Scanning I2C for Devices");
for (addr = 1; addr < 127; addr++) {
start = millis();
w->beginTransmission(addr); delay(2);
err = w->endTransmission();
if (err == 0) {
nDevices++;
switch (addr) {
case 0x42:
Serial.println("\tFound RAK12500 GPS Sensor");
deviceOnline |= RAK12500_ONLINE;
break;
default:
Serial.print("\tI2C device found at address 0x");
if (addr < 16) {
Serial.print("0");
}
Serial.print(addr, HEX);
Serial.println(" !");
break;
}
} else if (err == 4) {
Serial.print("Unknow error at address 0x");
if (addr < 16) {
Serial.print("0");
}
Serial.println(addr, HEX);
}
}
if (nDevices == 0)
Serial.println("No I2C devices found\n");
Serial.println("Scan for devices is complete.");
Serial.println("\n");
}
#endif
bool radio_init() {
rtc_clock.begin(Wire);
@ -68,6 +121,142 @@ void radio_set_tx_power(uint8_t dbm) {
radio.setOutputPower(dbm);
}
#if ENV_INCLUDE_GPS
void RAK4631SensorManager::start_gps()
{
//function currently not used
gps_active = true;
pinMode(disStandbyPin, OUTPUT);
digitalWrite(disStandbyPin, 1);
MESH_DEBUG_PRINTLN("GPS should be on now");
}
void RAK4631SensorManager::stop_gps()
{
//function currently not used
gps_active = false;
pinMode(disStandbyPin, OUTPUT);
digitalWrite(disStandbyPin, 0);
MESH_DEBUG_PRINTLN("GPS should be off now");
}
void RAK4631SensorManager::sleep_gps() {
gps_active = false;
ublox_GNSS.powerSaveMode();
MESH_DEBUG_PRINTLN("GPS should be sleeping now");
}
void RAK4631SensorManager::wake_gps() {
gps_active = true;
ublox_GNSS.powerSaveMode(false);
MESH_DEBUG_PRINTLN("GPS should be waking now");
}
bool RAK4631SensorManager::gpsIsAwake(uint32_t ioPin){
int pinInitialState = 0;
//set initial waking state
pinMode(ioPin,OUTPUT);
digitalWrite(ioPin,0);
delay(1000);
digitalWrite(ioPin,1);
delay(1000);
if (ublox_GNSS.begin(Wire) == true){
MESH_DEBUG_PRINTLN("GPS init correctly and GPS is turned on");
ublox_GNSS.setI2COutput(COM_TYPE_NMEA);
ublox_GNSS.saveConfigSelective(VAL_CFG_SUBSEC_IOPORT);
disStandbyPin = ioPin;
gps_active = true;
gps_present = true;
return true;
}
else
MESH_DEBUG_PRINTLN("GPS failed to init on this IO pin... try the next");
//digitalWrite(ioPin,pinInitialState); //reset the IO pin to initial state
return false;
}
#endif
bool RAK4631SensorManager::begin() {
#ifdef MESH_DEBUG
scanDevices(&Wire);
#endif
#if ENV_INCLUDE_GPS
//search for the correct IO standby pin depending on socket used
if(gpsIsAwake(P_GPS_STANDBY_A)){
MESH_DEBUG_PRINTLN("GPS is on socket A");
}
else if(gpsIsAwake(P_GPS_STANDBY_C)){
MESH_DEBUG_PRINTLN("GPS is on socket C");
}
else if(gpsIsAwake(P_GPS_STANDBY_F)){
MESH_DEBUG_PRINTLN("GPS is on socket F");
}
else{
MESH_DEBUG_PRINTLN("Error: No GPS found on sockets A, C or F");
gps_active = false;
gps_present = false;
return false;
}
//Now that GPS is found and set up, set to sleep for initial state
stop_gps();
#endif
}
#if ENV_INCLUDE_GPS
bool RAK4631SensorManager::querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) {
if (requester_permissions & TELEM_PERM_LOCATION && gps_active) { // does requester have permission?
telemetry.addGPS(TELEM_CHANNEL_SELF, node_lat, node_lon, node_altitude);
}
return true;
}
void RAK4631SensorManager::loop() {
static long next_update = 0;
_nmea->loop();
if (millis() > next_update && gps_active) {
node_lat = (double)ublox_GNSS.getLatitude()/10000000.;
node_lon = (double)ublox_GNSS.getLongitude()/10000000.;
node_altitude = (double)ublox_GNSS.getAltitude()/1000.;
MESH_DEBUG_PRINT("lat %f lon %f alt %f\r\n", node_lat, node_lon, node_altitude);
next_update = millis() + 1000;
}
}
int RAK4631SensorManager::getNumSettings() const { return 1; } // just one supported: "gps" (power switch)
const char* RAK4631SensorManager::getSettingName(int i) const {
return i == 0 ? "gps" : NULL;
}
const char* RAK4631SensorManager::getSettingValue(int i) const {
if (i == 0) {
return gps_active ? "1" : "0";
}
return NULL;
}
bool RAK4631SensorManager::setSettingValue(const char* name, const char* value) {
if (strcmp(name, "gps") == 0) {
if (strcmp(value, "0") == 0) {
stop_gps();
} else {
start_gps();
}
return true;
}
return false; // not supported
}
#endif
mesh::LocalIdentity radio_new_identity() {
RadioNoiseListener rng(radio);
return mesh::LocalIdentity(&rng); // create new random identity

View file

@ -7,19 +7,74 @@
#include <helpers/CustomSX1262Wrapper.h>
#include <helpers/AutoDiscoverRTCClock.h>
#include <helpers/SensorManager.h>
#if ENV_INCLUDE_GPS
#include <helpers/sensors/LocationProvider.h>
#include <SparkFun_u-blox_GNSS_Arduino_Library.h>
#endif
#ifdef DISPLAY_CLASS
#include <helpers/ui/SSD1306Display.h>
#endif
#define _BV(x) (1 << x)
class RAK4631SensorManager: public SensorManager {
#if ENV_INCLUDE_GPS
bool gps_active = false;
bool gps_present = false;
LocationProvider * _nmea;
SFE_UBLOX_GNSS ublox_GNSS;
uint32_t disStandbyPin = 0;
void start_gps();
void stop_gps();
void sleep_gps();
void wake_gps();
bool gpsIsAwake(uint32_t ioPin);
#endif
public:
#if ENV_INCLUDE_GPS
RAK4631SensorManager(LocationProvider &nmea): _nmea(&nmea) { }
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;
#else
RAK4631SensorManager() { }
#endif
bool begin() override;
};
extern RAK4631Board board;
extern WRAPPER_CLASS radio_driver;
extern AutoDiscoverRTCClock rtc_clock;
extern SensorManager sensors;
extern RAK4631SensorManager sensors;
#ifdef DISPLAY_CLASS
extern DISPLAY_CLASS display;
#endif
enum {
POWERMANAGE_ONLINE = _BV(0),
DISPLAY_ONLINE = _BV(1),
RADIO_ONLINE = _BV(2),
GPS_ONLINE = _BV(3),
PSRAM_ONLINE = _BV(4),
SDCARD_ONLINE = _BV(5),
AXDL345_ONLINE = _BV(6),
BME280_ONLINE = _BV(7),
BMP280_ONLINE = _BV(8),
BME680_ONLINE = _BV(9),
QMC6310_ONLINE = _BV(10),
QMI8658_ONLINE = _BV(11),
PCF8563_ONLINE = _BV(12),
OSC32768_ONLINE = _BV(13),
RAK12500_ONLINE = _BV(14),
};
bool radio_init();
uint32_t radio_get_rng_seed();
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr);

View file

@ -9,18 +9,23 @@ build_flags =
-D WRAPPER_CLASS=CustomSX1262Wrapper
-D LORA_TX_POWER=7
; -D P_LORA_TX_LED=35
; -D PIN_BOARD_SDA=5
; -D PIN_BOARD_SCL=6
-D PIN_USER_BTN=0
-D PIN_BOARD_SDA=5
-D PIN_BOARD_SCL=6
-D PIN_USER_BTN=38
-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 - DO NOT ENABLE THIS!
-I src/helpers/ui
-D DISPLAY_CLASS=SH1106Display
; https://wiki.uniteng.com/en/meshtastic/station-g2#impact-of-lora-node-dense-areashigh-noise-environments-on-rf-performance
build_src_filter = ${esp32_base.build_src_filter}
+<../variants/station_g2>
+<helpers/ui/SH1106Display.cpp>
lib_deps =
${esp32_base.lib_deps}
adafruit/Adafruit SH110X @ ~2.1.13
adafruit/Adafruit GFX Library @ ^1.12.1
[env:Station_G2_repeater]
extends = Station_G2

View file

@ -16,6 +16,10 @@ ESP32RTCClock fallback_clock;
AutoDiscoverRTCClock rtc_clock(fallback_clock);
SensorManager sensors;
#ifdef DISPLAY_CLASS
DISPLAY_CLASS display;
#endif
#ifndef LORA_CR
#define LORA_CR 5
#endif

View file

@ -8,11 +8,19 @@
#include <helpers/AutoDiscoverRTCClock.h>
#include <helpers/SensorManager.h>
#ifdef DISPLAY_CLASS
#include <helpers/ui/SH1106Display.h>
#endif
extern StationG2Board board;
extern WRAPPER_CLASS radio_driver;
extern AutoDiscoverRTCClock rtc_clock;
extern SensorManager 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);

View file

@ -0,0 +1,119 @@
#include <Arduino.h>
#include "t1000e_sensors.h"
#define HEATER_NTC_BX 4250 // thermistor coefficient B
#define HEATER_NTC_RP 8250 // ohm, series resistance to thermistor
#define HEATER_NTC_KA 273.15 // 25 Celsius at Kelvin
#define NTC_REF_VCC 3000 // mV, output voltage of LDO
#define LIGHT_REF_VCC 2400 //
static unsigned int ntc_res2[136]={
113347,107565,102116,96978,92132,87559,83242,79166,75316,71677,
68237,64991,61919,59011,56258,53650,51178,48835,46613,44506,
42506,40600,38791,37073,35442,33892,32420,31020,29689,28423,
27219,26076,24988,23951,22963,22021,21123,20267,19450,18670,
17926,17214,16534,15886,15266,14674,14108,13566,13049,12554,
12081,11628,11195,10780,10382,10000,9634,9284,8947,8624,
8315,8018,7734,7461,7199,6948,6707,6475,6253,6039,
5834,5636,5445,5262,5086,4917,4754,4597,4446,4301,
4161,4026,3896,3771,3651,3535,3423,3315,3211,3111,
3014,2922,2834,2748,2666,2586,2509,2435,2364,2294,
2228,2163,2100,2040,1981,1925,1870,1817,1766,1716,
1669,1622,1578,1535,1493,1452,1413,1375,1338,1303,
1268,1234,1202,1170,1139,1110,1081,1053,1026,999,
974,949,925,902,880,858,
};
static char ntc_temp2[136]=
{
-30,-29,-28,-27,-26,-25,-24,-23,-22,-21,
-20,-19,-18,-17,-16,-15,-14,-13,-12,-11,
-10,-9,-8,-7,-6,-5,-4,-3,-2,-1,
0,1,2,3,4,5,6,7,8,9,
10,11,12,13,14,15,16,17,18,19,
20,21,22,23,24,25,26,27,28,29,
30,31,32,33,34,35,36,37,38,39,
40,41,42,43,44,45,46,47,48,49,
50,51,52,53,54,55,56,57,58,59,
60,61,62,63,64,65,66,67,68,69,
70,71,72,73,74,75,76,77,78,79,
80,81,82,83,84,85,86,87,88,89,
90,91,92,93,94,95,96,97,98,99,
100,101,102,103,104,105,
};
static float get_heater_temperature( unsigned int vcc_volt, unsigned int ntc_volt )
{
int i = 0;
float Vout = 0, Rt = 0, temp = 0;
Vout = ntc_volt;
Rt = ( HEATER_NTC_RP * vcc_volt ) / Vout - HEATER_NTC_RP;
for( i = 0; i < 136; i++ )
{
if( Rt >= ntc_res2[i] )
{
break;
}
}
temp = ntc_temp2[i - 1] + 1 * ( ntc_res2[i - 1] - Rt ) / ( float )( ntc_res2[i - 1] - ntc_res2[i] );
temp = ( temp * 100 + 5 ) / 100;
return temp;
}
static int get_light_lv( unsigned int light_volt )
{
float Vout = 0, Vin = 0, Rt = 0, temp = 0;
unsigned int light_level = 0;
if( light_volt <= 80 )
{
light_level = 0;
return light_level;
}
else if( light_volt >= 2480 )
{
light_level = 100;
return light_level;
}
Vout = light_volt;
light_level = 100 * ( Vout - 80 ) / LIGHT_REF_VCC;
return light_level;
}
float t1000e_get_temperature( void )
{
unsigned int ntc_v, vcc_v;
digitalWrite(PIN_3V3_EN, HIGH);
digitalWrite(SENSOR_EN, HIGH);
analogReference(AR_INTERNAL_3_0);
analogReadResolution(12);
delay(10);
vcc_v = (1000.0*(analogRead(BATTERY_PIN) * ADC_MULTIPLIER * AREF_VOLTAGE)) / 4096;
ntc_v = (1000.0 * AREF_VOLTAGE * analogRead(TEMP_SENSOR)) / 4096;
digitalWrite(PIN_3V3_EN, LOW);
digitalWrite(SENSOR_EN, LOW);
return get_heater_temperature (vcc_v, ntc_v);
}
uint32_t t1000e_get_light( void )
{
int lux = 0;
unsigned int lux_v = 0;
digitalWrite(SENSOR_EN, HIGH);
analogReference(AR_INTERNAL_3_0);
analogReadResolution(12);
delay(10);
lux_v = 1000 * analogRead(LUX_SENSOR) * AREF_VOLTAGE / 4096;
lux = get_light_lv( lux_v );
digitalWrite(SENSOR_EN, LOW);
return lux;
}

View file

@ -0,0 +1,8 @@
#pragma once
// Light and temperature sensors are on ADC ports
// functions adapted from Seeed examples to get values
// see : https://github.com/Seeed-Studio/Seeed-Tracker-T1000-E-for-LoRaWAN-dev-board
extern uint32_t t1000e_get_light();
extern float t1000e_get_temperature();

View file

@ -1,4 +1,5 @@
#include <Arduino.h>
#include "t1000e_sensors.h"
#include "target.h"
#include <helpers/sensors/MicroNMEALocationProvider.h>
@ -9,7 +10,7 @@ RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BU
WRAPPER_CLASS radio_driver(radio, board);
VolatileRTCClock rtc_clock;
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1);
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock);
T1000SensorManager sensors = T1000SensorManager(nmea);
#ifdef DISPLAY_CLASS
@ -160,6 +161,10 @@ bool T1000SensorManager::querySensors(uint8_t requester_permissions, CayenneLPP&
if (requester_permissions & TELEM_PERM_LOCATION) { // does requester have permission?
telemetry.addGPS(TELEM_CHANNEL_SELF, node_lat, node_lon, node_altitude);
}
if (requester_permissions & TELEM_PERM_ENVIRONMENT) {
telemetry.addLuminosity(TELEM_CHANNEL_SELF, t1000e_get_light());
telemetry.addTemperature(TELEM_CHANNEL_SELF, t1000e_get_temperature());
}
return true;
}

View file

@ -70,17 +70,14 @@ build_flags =
-D MAX_CONTACTS=100
-D MAX_GROUP_CHANNELS=8
-D BLE_PIN_CODE=123456
-D BLE_DEBUG_LOGGING=1
; -D BLE_DEBUG_LOGGING=1
-D OFFLINE_QUEUE_SIZE=256
; -D ENABLE_PRIVATE_KEY_IMPORT=1
; -D ENABLE_PRIVATE_KEY_EXPORT=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${Heltec_t114.build_src_filter}
+<helpers/nrf52/T114Board.cpp>
+<helpers/nrf52/SerialBLEInterface.cpp>
+<../examples/companion_radio/main.cpp>
+<../examples/companion_radio/UITask.cpp>
+<../examples/companion_radio>
+<helpers/ui/ST7789Display.cpp>
+<helpers/ui/OLEDDisplay.cpp>
+<helpers/ui/OLEDDisplayFonts.cpp>
@ -97,13 +94,11 @@ build_flags =
-D MAX_GROUP_CHANNELS=8
; -D BLE_PIN_CODE=123456
; -D BLE_DEBUG_LOGGING=1
; -D ENABLE_PRIVATE_KEY_IMPORT=1
; -D ENABLE_PRIVATE_KEY_EXPORT=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${Heltec_t114.build_src_filter}
+<helpers/nrf52/*.cpp>
+<../examples/companion_radio/main.cpp>
+<../examples/companion_radio>
lib_deps =
${Heltec_t114.lib_deps}
densaugeo/base64 @ ~1.4.0

View file

@ -25,6 +25,9 @@ build_src_filter = ${nrf52840_techo.build_src_filter}
+<helpers/*.cpp>
+<helpers/nrf52/TechoBoard.cpp>
+<../variants/techo>
lib_deps =
${nrf52840_techo.lib_deps}
stevemarple/MicroNMEA @ ^2.0.6
debug_tool = jlink
upload_protocol = nrfutil
@ -64,8 +67,6 @@ build_flags =
-D BLE_DEBUG_LOGGING=1
-D DISPLAY_CLASS=GxEPDDisplay
-D OFFLINE_QUEUE_SIZE=256
; -D ENABLE_PRIVATE_KEY_IMPORT=1
; -D ENABLE_PRIVATE_KEY_EXPORT=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${LilyGo_Techo.build_src_filter}

View file

@ -1,6 +1,7 @@
#include <Arduino.h>
#include "target.h"
#include <helpers/ArduinoHelpers.h>
#include <helpers/sensors/MicroNMEALocationProvider.h>
TechoBoard board;
@ -10,7 +11,8 @@ WRAPPER_CLASS radio_driver(radio, board);
VolatileRTCClock fallback_clock;
AutoDiscoverRTCClock rtc_clock(fallback_clock);
SensorManager sensors;
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1);
TechoSensorManager sensors = TechoSensorManager(nmea);
#ifdef DISPLAY_CLASS
DISPLAY_CLASS display;
@ -72,3 +74,80 @@ mesh::LocalIdentity radio_new_identity() {
RadioNoiseListener rng(radio);
return mesh::LocalIdentity(&rng); // create new random identity
}
void TechoSensorManager::start_gps() {
if (!gps_active) {
gps_active = true;
_location->begin();
}
}
void TechoSensorManager::stop_gps() {
if (gps_active) {
gps_active = false;
_location->stop();
}
}
bool TechoSensorManager::begin() {
Serial1.begin(9600);
// GPS enabled pin
pinMode(GPS_EN, OUTPUT);
return true;
}
bool TechoSensorManager::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 TechoSensorManager::loop() {
static long next_gps_update = 0;
if (!gps_active) {
return; // GPS is not active, skip further processing
}
_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 TechoSensorManager::getNumSettings() const {
return 1; // always show GPS setting
}
const char* TechoSensorManager::getSettingName(int i) const {
return (i == 0) ? "gps" : NULL;
}
const char* TechoSensorManager::getSettingValue(int i) const {
if (i == 0) {
return gps_active ? "1" : "0";
}
return NULL;
}
bool TechoSensorManager::setSettingValue(const char* name, const char* value) {
if (strcmp(name, "gps") == 0) {
if (strcmp(value, "0") == 0) {
stop_gps();
} else {
start_gps();
}
return true;
}
return false; // not supported
}

View file

@ -7,15 +7,32 @@
#include <helpers/CustomSX1262Wrapper.h>
#include <helpers/AutoDiscoverRTCClock.h>
#include <helpers/SensorManager.h>
#include <helpers/sensors/LocationProvider.h>
#ifdef DISPLAY_CLASS
#include <helpers/ui/GxEPDDisplay.h>
#endif
class TechoSensorManager : public SensorManager {
bool gps_active = false;
LocationProvider* _location;
void start_gps();
void stop_gps();
public:
TechoSensorManager(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 TechoBoard board;
extern WRAPPER_CLASS radio_driver;
extern AutoDiscoverRTCClock rtc_clock;
extern SensorManager sensors;
extern TechoSensorManager sensors;
#ifdef DISPLAY_CLASS
extern DISPLAY_CLASS display;

View file

@ -29,6 +29,6 @@ void initVariant() {
digitalWrite(PIN_TXCO, HIGH);
// shutdown gps
pinMode(PIN_GPS_STANDBY, OUTPUT);
digitalWrite(PIN_GPS_STANDBY, LOW);
pinMode(GPS_EN, OUTPUT);
digitalWrite(GPS_EN, LOW);
}

View file

@ -40,8 +40,8 @@
////////////////////////////////////////////////////////////////////////////////
// UART pin definition
#define PIN_SERIAL1_RX (41) // GPS TX
#define PIN_SERIAL1_TX (40) // GPS RX
#define PIN_SERIAL1_RX PIN_GPS_TX
#define PIN_SERIAL1_TX PIN_GPS_RX
////////////////////////////////////////////////////////////////////////////////
// I2C pin definition
@ -125,9 +125,8 @@ extern const int SCK;
////////////////////////////////////////////////////////////////////////////////
// GPS
#define PIN_GPS_RX (41)
#define PIN_GPS_TX (40)
#define PIN_GPS_WAKEUP (34)
#define PIN_GPS_RX (40)
#define PIN_GPS_TX (41)
#define GPS_EN (34)
#define PIN_GPS_RESET (37)
#define PIN_GPS_PPS (36)
#define PIN_GPS_STANDBY (34)

View file

@ -25,6 +25,9 @@ build_src_filter = ${nrf52840_thinknode_m1.build_src_filter}
+<helpers/*.cpp>
+<helpers/nrf52/ThinkNodeM1Board.cpp>
+<../variants/thinknode_m1>
lib_deps =
${nrf52840_thinknode_m1.lib_deps}
stevemarple/MicroNMEA @ ^2.0.6
debug_tool = jlink
upload_protocol = nrfutil
@ -71,16 +74,17 @@ build_flags =
-D DISPLAY_ROTATION=4
-D DISPLAY_CLASS=GxEPDDisplay
-D OFFLINE_QUEUE_SIZE=256
; -D ENABLE_PRIVATE_KEY_IMPORT=1
; -D ENABLE_PRIVATE_KEY_EXPORT=1
-D PIN_BUZZER=6
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${ThinkNode_M1.build_src_filter}
+<helpers/nrf52/ThinkNodeM1.cpp>
+<helpers/nrf52/SerialBLEInterface.cpp>
+<helpers/ui/GxEPDDisplay.cpp>
+<helpers/ui/buzzer.cpp>
+<../examples/companion_radio>
lib_deps =
${ThinkNode_M1.lib_deps}
densaugeo/base64 @ ~1.4.0
zinggjm/GxEPD2 @ 1.6.2
end2endzone/NonBlockingRTTTL@^1.3.0

View file

@ -1,6 +1,7 @@
#include <Arduino.h>
#include "target.h"
#include <helpers/ArduinoHelpers.h>
#include <helpers/sensors/MicroNMEALocationProvider.h>
ThinkNodeM1Board board;
@ -10,7 +11,8 @@ WRAPPER_CLASS radio_driver(radio, board);
VolatileRTCClock fallback_clock;
AutoDiscoverRTCClock rtc_clock(fallback_clock);
SensorManager sensors;
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1);
ThinkNodeM1SensorManager sensors = ThinkNodeM1SensorManager(nmea);
#ifdef DISPLAY_CLASS
DISPLAY_CLASS display;
@ -72,3 +74,110 @@ mesh::LocalIdentity radio_new_identity() {
RadioNoiseListener rng(radio);
return mesh::LocalIdentity(&rng); // create new random identity
}
void ThinkNodeM1SensorManager::start_gps() {
if (!gps_active) {
gps_active = true;
_location->begin();
}
}
void ThinkNodeM1SensorManager::stop_gps() {
if (gps_active) {
gps_active = false;
_location->stop();
}
}
bool ThinkNodeM1SensorManager::begin() {
Serial1.begin(9600);
// Initialize GPS switch pin
pinMode(PIN_GPS_SWITCH, INPUT);
last_gps_switch_state = digitalRead(PIN_GPS_SWITCH);
// Initialize GPS power pin
pinMode(GPS_EN, OUTPUT);
// Check initial switch state to determine if GPS should be active
if (last_gps_switch_state == HIGH) { // Switch is HIGH when ON
start_gps();
}
return true;
}
bool ThinkNodeM1SensorManager::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 ThinkNodeM1SensorManager::loop() {
static long next_gps_update = 0;
static long last_switch_check = 0;
// Check GPS switch state every second
if (millis() - last_switch_check > 1000) {
bool current_switch_state = digitalRead(PIN_GPS_SWITCH);
// Detect switch state change
if (current_switch_state != last_gps_switch_state) {
last_gps_switch_state = current_switch_state;
if (current_switch_state == HIGH) { // Switch is ON
MESH_DEBUG_PRINTLN("GPS switch ON");
start_gps();
} else { // Switch is OFF
MESH_DEBUG_PRINTLN("GPS switch OFF");
stop_gps();
}
}
last_switch_check = millis();
}
if (!gps_active) {
return; // GPS is not active, skip further processing
}
_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 ThinkNodeM1SensorManager::getNumSettings() const {
return 1; // always show GPS setting
}
const char* ThinkNodeM1SensorManager::getSettingName(int i) const {
return (i == 0) ? "gps" : NULL;
}
const char* ThinkNodeM1SensorManager::getSettingValue(int i) const {
if (i == 0) {
return gps_active ? "1" : "0";
}
return NULL;
}
bool ThinkNodeM1SensorManager::setSettingValue(const char* name, const char* value) {
if (strcmp(name, "gps") == 0) {
if (strcmp(value, "0") == 0) {
stop_gps();
} else {
start_gps();
}
return true;
}
return false; // not supported
}

View file

@ -7,14 +7,33 @@
#include <helpers/CustomSX1262Wrapper.h>
#include <helpers/AutoDiscoverRTCClock.h>
#include <helpers/SensorManager.h>
#include <helpers/sensors/LocationProvider.h>
#ifdef DISPLAY_CLASS
#include <helpers/ui/GxEPDDisplay.h>
#endif
class ThinkNodeM1SensorManager : public SensorManager {
bool gps_active = false;
bool last_gps_switch_state = false;
LocationProvider* _location;
void start_gps();
void stop_gps();
public:
ThinkNodeM1SensorManager(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 ThinkNodeM1Board board;
extern WRAPPER_CLASS radio_driver;
extern AutoDiscoverRTCClock rtc_clock;
extern SensorManager sensors;
extern ThinkNodeM1SensorManager sensors;
#ifdef DISPLAY_CLASS
extern DISPLAY_CLASS display;

View file

@ -40,8 +40,8 @@
////////////////////////////////////////////////////////////////////////////////
// UART pin definition
#define PIN_SERIAL1_RX (41) // GPS TX
#define PIN_SERIAL1_TX (40) // GPS RX
#define PIN_SERIAL1_RX PIN_GPS_TX
#define PIN_SERIAL1_TX PIN_GPS_RX
////////////////////////////////////////////////////////////////////////////////
// I2C pin definition
@ -125,9 +125,10 @@ extern const int SCK;
////////////////////////////////////////////////////////////////////////////////
// GPS
#define PIN_GPS_RX (41)
#define PIN_GPS_TX (40)
#define PIN_GPS_WAKEUP (34)
#define PIN_GPS_RX (40)
#define PIN_GPS_TX (41)
#define GPS_EN (34)
#define PIN_GPS_RESET (37)
#define PIN_GPS_PPS (36)
#define PIN_GPS_STANDBY (34)
#define PIN_GPS_SWITCH (33)

View file

@ -7,9 +7,9 @@ build_flags = ${stm32_base.build_flags}
-D WRAPPER_CLASS=CustomSTM32WLxWrapper
-D SPI_INTERFACES_COUNT=0
-D RX_BOOSTED_GAIN=true
-I variants/wio-e5
-I variants/wio-e5-dev
build_src_filter = ${stm32_base.build_src_filter}
+<../variants/wio-e5>
+<../variants/wio-e5-dev>
[env:wio-e5-repeater]
extends = lora_e5

View file

@ -11,7 +11,13 @@
class WIOE5Board : public STM32Board {
public:
const char* getManufacturerName() const override {
return "Seeed Wio E5";
return "Seeed Wio E5 Dev Board";
}
// Just returns ADC value for now to test adc
uint16_t getBattMilliVolts() override {
uint32_t raw = analogRead(PIN_A3);
return raw;
}
};

View file

@ -0,0 +1,10 @@
#pragma once
// UART Definitions
// #ifndef SERIAL_UART_INSTANCE
// #define SERIAL_UART_INSTANCE 101
// #endif
#include <variant_generic.h>
#undef RNG

View file

@ -0,0 +1,34 @@
[lora_e5_mini]
extends = stm32_base
board = lora_e5_mini
board_upload.maximum_size = 229376 ; 32kb for FS
build_flags = ${stm32_base.build_flags}
-D RADIO_CLASS=CustomSTM32WLx
-D WRAPPER_CLASS=CustomSTM32WLxWrapper
-D SPI_INTERFACES_COUNT=0
-D RX_BOOSTED_GAIN=true
-I variants/wio-e5-mini
build_src_filter = ${stm32_base.build_src_filter}
+<../variants/wio-e5-mini>
lib_deps = ${stm32_base.lib_deps}
finitespace/BME280 @ ^3.0.0
[env:wio-e5-mini-repeater]
extends = lora_e5_mini
build_flags = ${lora_e5_mini.build_flags}
-D LORA_TX_POWER=22
-D ADVERT_NAME='"wio-e5-mini Repeater"'
-D ADMIN_PASSWORD='"password"'
build_src_filter = ${lora_e5_mini.build_src_filter}
+<../examples/simple_repeater/main.cpp>
[env:wio-e5-mini_companion_radio_usb]
extends = lora_e5_mini
build_flags = ${lora_e5_mini.build_flags}
-D LORA_TX_POWER=22
-D MAX_CONTACTS=100
-D MAX_GROUP_CHANNELS=8
build_src_filter = ${lora_e5_mini.build_src_filter}
+<../examples/companion_radio/*.cpp>
lib_deps = ${lora_e5_mini.lib_deps}
densaugeo/base64 @ ~1.4.0

View file

@ -0,0 +1,90 @@
#include <Arduino.h>
#include "target.h"
#include <helpers/ArduinoHelpers.h>
WIOE5Board board;
RADIO_CLASS radio = new STM32WLx_Module();
WRAPPER_CLASS radio_driver(radio, board);
static const uint32_t rfswitch_pins[] = {PA4, PA5, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC};
static const Module::RfSwitchMode_t rfswitch_table[] = {
{STM32WLx::MODE_IDLE, {LOW, LOW}},
{STM32WLx::MODE_RX, {HIGH, LOW}},
{STM32WLx::MODE_TX_HP, {LOW, HIGH}}, // for LoRa-E5 mini
// {STM32WLx::MODE_TX_LP, {HIGH, HIGH}}, // for LoRa-E5-LE mini
END_OF_MODE_TABLE,
};
VolatileRTCClock rtc_clock;
WIOE5SensorManager sensors;
#ifndef LORA_CR
#define LORA_CR 5
#endif
bool radio_init() {
Wire.begin();
radio.setRfSwitchTable(rfswitch_pins, rfswitch_table);
int status = radio.begin(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, LORA_TX_POWER, 8, 1.7, 0);
if (status != RADIOLIB_ERR_NONE) {
Serial.print("ERROR: radio init failed: ");
Serial.println(status);
return false; // fail
}
#ifdef RX_BOOSTED_GAIN
radio.setRxBoostedGainMode(RX_BOOSTED_GAIN);
#endif
radio.setCRC(1);
return true; // success
}
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
}
bool WIOE5SensorManager::querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) {
if (!has_bme) return false;
float temp(NAN), hum(NAN), pres(NAN);
BME280::TempUnit tempUnit(BME280::TempUnit_Celsius);
BME280::PresUnit presUnit(BME280::PresUnit_hPa);
bme.read(pres, temp, hum, tempUnit, presUnit);
telemetry.addTemperature(TELEM_CHANNEL_SELF, temp);
telemetry.addRelativeHumidity(TELEM_CHANNEL_SELF, hum);
telemetry.addBarometricPressure(TELEM_CHANNEL_SELF, pres);
return true;
}
bool WIOE5SensorManager::begin() {
has_bme = bme.begin();
return has_bme;
}

View file

@ -0,0 +1,46 @@
#pragma once
#define RADIOLIB_STATIC_ONLY 1
#include <RadioLib.h>
#include <helpers/RadioLibWrappers.h>
#include <helpers/stm32/STM32Board.h>
#include <helpers/CustomSTM32WLxWrapper.h>
#include <helpers/ArduinoHelpers.h>
#include <helpers/SensorManager.h>
#include <BME280I2C.h>
#include <Wire.h>
class WIOE5Board : public STM32Board {
public:
const char* getManufacturerName() const override {
return "Seeed Wio E5 mini";
}
uint16_t getBattMilliVolts() override {
analogReadResolution(12);
uint32_t raw = analogRead(PIN_A3);
return raw;
}
};
class WIOE5SensorManager : public SensorManager {
BME280I2C bme;
bool has_bme = false;
public:
WIOE5SensorManager() {}
bool begin() override;
bool querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) override;
};
extern WIOE5Board board;
extern WRAPPER_CLASS radio_driver;
extern VolatileRTCClock rtc_clock;
extern WIOE5SensorManager sensors;
bool radio_init();
uint32_t radio_get_rng_seed();
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr);
void radio_set_tx_power(uint8_t dbm);
mesh::LocalIdentity radio_new_identity();

View file

@ -1,9 +1,9 @@
#pragma once
// UART Definitions
#ifndef SERIAL_UART_INSTANCE
#define SERIAL_UART_INSTANCE 101
#endif
// #ifndef SERIAL_UART_INSTANCE
// #define SERIAL_UART_INSTANCE 101
// #endif
#include <variant_LORA_E5_MINI.h>

View file

@ -4,6 +4,7 @@ board = seeed_xiao_esp32c3
build_flags =
${esp32_base.build_flags}
-I variants/xiao_c3
-D ESP32_CPU_FREQ=80
-D LORA_TX_BOOST_PIN=D3
-D P_LORA_TX_LED=D5
-D PIN_VBAT_READ=D0

View file

@ -36,6 +36,8 @@ build_flags = ${nrf52840_xiao.build_flags}
-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
@ -63,12 +65,11 @@ build_flags =
-D BLE_PIN_CODE=123456
-D OFFLINE_QUEUE_SIZE=256
; -D BLE_DEBUG_LOGGING=1
; -D ENABLE_PRIVATE_KEY_IMPORT=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${Xiao_nrf52.build_src_filter}
+<helpers/nrf52/SerialBLEInterface.cpp>
+<../examples/companion_radio/main.cpp>
+<../examples/companion_radio>
lib_deps =
${Xiao_nrf52.lib_deps}
densaugeo/base64 @ ~1.4.0
@ -79,12 +80,11 @@ build_flags =
${Xiao_nrf52.build_flags}
-D MAX_CONTACTS=100
-D MAX_GROUP_CHANNELS=8
; -D ENABLE_PRIVATE_KEY_IMPORT=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${Xiao_nrf52.build_src_filter}
+<helpers/nrf52/SerialBLEInterface.cpp>
+<../examples/companion_radio/main.cpp>
+<../examples/companion_radio>
lib_deps =
${Xiao_nrf52.lib_deps}
densaugeo/base64 @ ~1.4.0

View file

@ -35,6 +35,10 @@ bool radio_init() {
radio.setCRC(1);
#if defined(SX126X_RXEN) && defined(SX126X_TXEN)
radio.setRfSwitchPins(SX126X_RXEN, SX126X_TXEN);
#endif
#ifdef SX126X_CURRENT_LIMIT
radio.setCurrentLimit(SX126X_CURRENT_LIMIT);
#endif

View file

@ -15,6 +15,8 @@ build_flags = ${esp32_base.build_flags}
-D P_LORA_MOSI=9
-D PIN_USER_BTN=21
-D PIN_STATUS_LED=48
-D SX126X_RXEN=38
-D SX126X_TXEN=RADIOLIB_NC
-D SX126X_DIO2_AS_RF_SWITCH=true
-D SX126X_DIO3_TCXO_VOLTAGE=1.8
-D SX126X_CURRENT_LIMIT=140
@ -83,8 +85,6 @@ build_flags =
-D DISPLAY_CLASS=SSD1306Display
-D OFFLINE_QUEUE_SIZE=256
; -D BLE_DEBUG_LOGGING=1
; -D ENABLE_PRIVATE_KEY_IMPORT=1
; -D ENABLE_PRIVATE_KEY_EXPORT=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${Xiao_S3_WIO.build_src_filter}
@ -108,7 +108,7 @@ build_flags =
; -D MESH_DEBUG=1
build_src_filter = ${Xiao_S3_WIO.build_src_filter}
+<helpers/esp32/*.cpp>
+<../examples/companion_radio/main.cpp>
+<../examples/companion_radio>
lib_deps =
${Xiao_S3_WIO.lib_deps}
densaugeo/base64 @ ~1.4.0

View file

@ -47,7 +47,11 @@ bool radio_init() {
}
radio.setCRC(1);
#if defined(SX126X_RXEN) && defined(SX126X_TXEN)
radio.setRfSwitchPins(SX126X_RXEN, SX126X_TXEN);
#endif
#ifdef SX126X_CURRENT_LIMIT
radio.setCurrentLimit(SX126X_CURRENT_LIMIT);
#endif