mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-11-04 08:37:53 +00:00
Merge upstream dev fixes
Keep our radio transactions, notification defaults, display settings and pinned RadioLib release. Integrate contact and sensor safety fixes, saturated statistics and compatible board/display updates.
This commit is contained in:
commit
49d70fe0c6
32 files changed
+675
-103
No files matched your search
@@ -399,6 +399,9 @@ jobs:
|
||||
- name: Run Unit Tests
|
||||
run: pio test -e native -e native_kiss_modem -e native_ota_channel -vv
|
||||
|
||||
- name: Verify sensor reply bounds and duplicate counter saturation
|
||||
run: python3 -B test/test_upstream_cli_bounds.py -v
|
||||
|
||||
- name: Verify real GPS NMEA parser and provider with address and undefined sanitizers
|
||||
run: pio test -e native_gps -vv
|
||||
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
# Companion CLI
|
||||
|
||||
Companions accept local CLI commands through the Companion protocol. Remote
|
||||
commands require the configured admin permission. Available commands depend on
|
||||
the board and firmware profile; use `help` for the commands in your build.
|
||||
|
||||
## Common commands
|
||||
|
||||
| Purpose | Commands |
|
||||
| --- | --- |
|
||||
| Firmware version | `ver` |
|
||||
| Node name | `get name`, `set name <name>` |
|
||||
| Radio profile | `get radio`, `set radio <MHz>,<kHz>,<SF>,<CR>` |
|
||||
| Frequency | `get freq`, `set freq <MHz>` |
|
||||
| Transmit power | `get tx`, `set tx <dBm>` |
|
||||
| Radio apply status | `get radio.apply` |
|
||||
| Timezone, hours | `get tz.offset`, `set tz.offset <hours>` |
|
||||
| Airtime limit | `get dutycycle`, `set dutycycle <percent>` |
|
||||
| Receive gain | `get radio.rxgain`, `set radio.rxgain <on/off>` |
|
||||
| Receive power saving | `get radio.rxps`, `get radio.rxps.config` |
|
||||
| FEM receive gain | `get radio.fem.rxgain`, `set radio.fem.rxgain <on/off>` |
|
||||
| FEM transmit gain | `get radio.fem.txgain`, `set radio.fem.txgain <on/off>` |
|
||||
| BLE PIN | `set pin <0-999999>` |
|
||||
| Restart | `reboot` |
|
||||
| Power off, where supported | `poweroff` or `shutdown` |
|
||||
|
||||
Supported frequencies, bandwidths and power limits depend on the radio and
|
||||
board. RF changes may wait for active traffic; `get radio.apply` reports the
|
||||
apply state. Transmit power is the radio-chip setting, which can differ from
|
||||
antenna output on amplified boards.
|
||||
|
||||
See [CLI commands](cli_commands.md) for setting ranges and access restrictions,
|
||||
and [radio profiles](radio_profiles.md) for radio2, temporary profiles and
|
||||
schedules.
|
||||
|
||||
## Wi-Fi
|
||||
|
||||
Wi-Fi builds expose `get wifi.status`, `get wifi.ip`, and
|
||||
`get/set wifi.enabled`. Configure credentials with `set wifi.ssid <SSID>` and
|
||||
`set wifi.pwd <password>`. Credential access follows the normal local/admin
|
||||
restrictions. Reboot after saving Wi-Fi settings to apply them.
|
||||
|
||||
## Display and notifications
|
||||
|
||||
This fork uses the saved `display.*` controls described in
|
||||
[display controls](cli_commands.md#set-display-modes-and-timeouts):
|
||||
|
||||
```text
|
||||
get display.mode
|
||||
get display.timeout
|
||||
get display.usb.mode
|
||||
get display.usb.timeout
|
||||
get display.inbox
|
||||
```
|
||||
|
||||
Timeouts default to 900 seconds on e-paper and 15 seconds on other screens.
|
||||
Battery and USB settings are independent. Display settings live in a separate
|
||||
checked preferences file and preserve the existing node-preferences layout.
|
||||
The upstream `ui.*` settings are not aliases in this fork.
|
||||
|
||||
For sound, vibration and custom notification rules, see
|
||||
[notifications](notifications.md).
|
||||
@@ -617,11 +617,7 @@ class HomeScreen : public UIScreen {
|
||||
sensors_scroll_offset = 0;
|
||||
return;
|
||||
}
|
||||
#if AUTO_OFF_MILLIS > 0
|
||||
const uint32_t refresh_interval = 5000;
|
||||
#else
|
||||
const uint32_t refresh_interval = 60000;
|
||||
#endif
|
||||
const uint32_t refresh_interval = display.isEink() ? 60000 : 5000;
|
||||
// Empty data requests an initial/forced sample. Elapsed time also handles
|
||||
// returning to this page after more than half a millis() cycle.
|
||||
if (sensors_lpp.getSize() == 0
|
||||
@@ -2293,11 +2289,7 @@ public:
|
||||
#endif
|
||||
renderChannelFilter(display);
|
||||
|
||||
#if AUTO_OFF_MILLIS==0 // probably e-ink
|
||||
return UI_BUTTON_READER_HINT == 1 ? 3000 : 10000;
|
||||
#else
|
||||
return 1000; // next render after 1000 ms
|
||||
#endif
|
||||
return display.isEink() ? (UI_BUTTON_READER_HINT == 1 ? 3000 : 10000) : 1000;
|
||||
}
|
||||
|
||||
bool handleInput(char c) override {
|
||||
@@ -2735,9 +2727,13 @@ void UITask::shutdown(bool restart){
|
||||
if (restart) {
|
||||
_board->reboot();
|
||||
} else {
|
||||
display.forceFullRefresh();
|
||||
display.clear();
|
||||
display.endFrame();
|
||||
if (_display != NULL && _display->isEink()) {
|
||||
if (_display->isOn()) _display->turnOff();
|
||||
} else {
|
||||
display.forceFullRefresh();
|
||||
display.clear();
|
||||
display.endFrame();
|
||||
}
|
||||
// Power off board including radio, display, GPS and components
|
||||
_board->powerOff();
|
||||
}
|
||||
@@ -2875,11 +2871,11 @@ void UITask::loop() {
|
||||
#endif
|
||||
#if defined(UI_HAS_ROTARY_INPUT)
|
||||
RotaryInputEvent rotaryEv = rotary_input.poll();
|
||||
if (c == 0 && _display != NULL && _display->isOn()) {
|
||||
if (c == 0 && _display != NULL) {
|
||||
if (rotaryEv == RotaryInputEvent::Next) {
|
||||
c = KEY_NEXT;
|
||||
c = checkDisplayOn(KEY_NEXT);
|
||||
} else if (rotaryEv == RotaryInputEvent::Prev) {
|
||||
c = KEY_PREV;
|
||||
c = checkDisplayOn(KEY_PREV);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -761,6 +761,7 @@ void UITask::loop() {
|
||||
userLedHandler();
|
||||
|
||||
#ifdef PIN_BUZZER
|
||||
if (buzzer.isQuiet() != (bool)_node_prefs->buzzer_quiet) buzzer.quiet(_node_prefs->buzzer_quiet);
|
||||
if (buzzer.isPlaying()) buzzer.loop();
|
||||
#endif
|
||||
#if defined(PIN_BUZZER) && MESH_GPS_VOICE
|
||||
|
||||
@@ -998,6 +998,7 @@ void UITask::loop() {
|
||||
userLedHandler();
|
||||
|
||||
#ifdef PIN_BUZZER
|
||||
if (buzzer.isQuiet() != (bool)_node_prefs->buzzer_quiet) buzzer.quiet(_node_prefs->buzzer_quiet);
|
||||
if (buzzer.isPlaying()) buzzer.loop();
|
||||
#endif
|
||||
|
||||
|
||||
@@ -846,8 +846,8 @@ int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t
|
||||
stats.n_recv_direct = getNumRecvDirect();
|
||||
stats.err_events = _err_flags;
|
||||
stats.last_snr = (int16_t)(radio_driver.getLastSNR() * 4);
|
||||
stats.n_direct_dups = ((SimpleMeshTables *)getTables())->getNumDirectDups();
|
||||
stats.n_flood_dups = ((SimpleMeshTables *)getTables())->getNumFloodDups();
|
||||
stats.n_direct_dups = std::min<uint32_t>(((SimpleMeshTables *)getTables())->getNumDirectDups(), 0xFFFF);
|
||||
stats.n_flood_dups = std::min<uint32_t>(((SimpleMeshTables *)getTables())->getNumFloodDups(), 0xFFFF);
|
||||
stats.total_rx_air_time_secs = getReceiveAirTime() / 1000;
|
||||
stats.n_recv_errors = radio_driver.getPacketsRecvErrors();
|
||||
memcpy(&reply_data[4], &stats, sizeof(stats));
|
||||
|
||||
@@ -1383,8 +1383,8 @@ int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t
|
||||
stats.n_recv_direct = getNumRecvDirect();
|
||||
stats.err_events = _err_flags;
|
||||
stats.last_snr = (int16_t)(radio_driver.getLastSNR() * 4);
|
||||
stats.n_direct_dups = ((SimpleMeshTables *)getTables())->getNumDirectDups();
|
||||
stats.n_flood_dups = ((SimpleMeshTables *)getTables())->getNumFloodDups();
|
||||
stats.n_direct_dups = std::min<uint32_t>(((SimpleMeshTables *)getTables())->getNumDirectDups(), 0xFFFF);
|
||||
stats.n_flood_dups = std::min<uint32_t>(((SimpleMeshTables *)getTables())->getNumFloodDups(), 0xFFFF);
|
||||
stats.n_posted = _num_posted;
|
||||
stats.n_post_push = _num_post_pushes;
|
||||
|
||||
|
||||
@@ -244,6 +244,7 @@ void BaseChatMesh::onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id,
|
||||
|
||||
ContactInfo* from = NULL;
|
||||
for (int i = 0; i < num_contacts; i++) {
|
||||
if (contacts[i].isEmptySlot()) continue;
|
||||
if (id.matches(contacts[i].id)) { // is from one of our contacts
|
||||
from = &contacts[i];
|
||||
if (timestamp <= from->last_advert_timestamp) { // check for replay attacks!!
|
||||
@@ -327,11 +328,14 @@ int BaseChatMesh::searchPeersByHash(const uint8_t* hash) {
|
||||
// A matching transient slot may have the same identity as an accepted contact.
|
||||
// Try persistent contacts first so the transient slot cannot swallow their DMs.
|
||||
for (int i = MAX_ANON_CONTACTS; i < num_contacts && n < MAX_SEARCH_RESULTS; i++) {
|
||||
if (contacts[i].isEmptySlot()) continue;
|
||||
if (contacts[i].id.isHashMatch(hash)) {
|
||||
matching_peer_indexes[n++] = i;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < MAX_ANON_CONTACTS && n < MAX_SEARCH_RESULTS; i++) {
|
||||
// Empty anonymous slots must not consume matches or derive a null key.
|
||||
if (contacts[i].isEmptySlot()) continue;
|
||||
if (contacts[i].id.isHashMatch(hash)) {
|
||||
matching_peer_indexes[n++] = i; // store the INDEXES of matching contacts (for subsequent 'peer' methods)
|
||||
}
|
||||
@@ -748,6 +752,11 @@ int BaseChatMesh::sendCommandData(const ContactInfo& recipient,
|
||||
}
|
||||
|
||||
bool BaseChatMesh::sendGroupMessage(uint32_t timestamp, mesh::GroupChannel& channel, const char* sender_name, const char* text, int text_len) {
|
||||
if (text_len < 0) {
|
||||
MESH_DEBUG_PRINTLN("sendGroupMessage: invalid negative text_len=%d", text_len);
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t temp[5+MAX_TEXT_LEN+32];
|
||||
memcpy(temp, ×tamp, 4); // mostly an extra blob to help make packet_hash unique
|
||||
temp[4] = 0; // TXT_TYPE_PLAIN
|
||||
@@ -1086,6 +1095,7 @@ ContactInfo* BaseChatMesh::searchContactsByPrefix(const char* name_prefix) {
|
||||
int len = strlen(name_prefix);
|
||||
for (int i = 0; i < num_contacts; i++) {
|
||||
auto c = &contacts[i];
|
||||
if (c->isEmptySlot() || c->name[0] == 0) continue;
|
||||
if (memcmp(c->name, name_prefix, len) == 0) return c;
|
||||
}
|
||||
return NULL; // not found
|
||||
@@ -1094,6 +1104,7 @@ ContactInfo* BaseChatMesh::searchContactsByPrefix(const char* name_prefix) {
|
||||
ContactInfo* BaseChatMesh::lookupContactByPubKey(const uint8_t* pub_key, int prefix_len) {
|
||||
for (int i = 0; i < num_contacts; i++) {
|
||||
auto c = &contacts[i];
|
||||
if (c->isEmptySlot()) continue;
|
||||
if (memcmp(c->id.pub_key, pub_key, prefix_len) == 0) return c;
|
||||
}
|
||||
return NULL; // not found
|
||||
@@ -1102,6 +1113,7 @@ ContactInfo* BaseChatMesh::lookupContactByPubKey(const uint8_t* pub_key, int pre
|
||||
ContactInfo* BaseChatMesh::lookupTransientContactByPubKey(const uint8_t* pub_key, int prefix_len) {
|
||||
for (int i = 0; i < MAX_ANON_CONTACTS; i++) {
|
||||
auto c = &contacts[i];
|
||||
if (c->isEmptySlot()) continue;
|
||||
if (memcmp(c->id.pub_key, pub_key, prefix_len) == 0) return c;
|
||||
}
|
||||
return NULL; // not found
|
||||
@@ -1110,6 +1122,7 @@ ContactInfo* BaseChatMesh::lookupTransientContactByPubKey(const uint8_t* pub_key
|
||||
ContactInfo* BaseChatMesh::lookupPersistentContactByPubKey(const uint8_t* pub_key, int prefix_len) {
|
||||
for (int i = MAX_ANON_CONTACTS; i < num_contacts; i++) {
|
||||
auto c = &contacts[i];
|
||||
if (c->isEmptySlot()) continue;
|
||||
if (memcmp(c->id.pub_key, pub_key, prefix_len) == 0) return c;
|
||||
}
|
||||
return NULL; // not found
|
||||
|
||||
@@ -4166,18 +4166,26 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
|
||||
if (start >= end) {
|
||||
strcpy(reply, "no custom var");
|
||||
} else {
|
||||
const int reply_max = 160; // callers pass a 160-byte reply buffer
|
||||
const int next_reserve = 16; // room for "... next:NNN"
|
||||
sprintf(dp, "%d vars\n", end);
|
||||
dp = strchr(dp, 0);
|
||||
int i;
|
||||
for (i = start; i < end && (dp-reply < 134); i++) {
|
||||
sprintf(dp, "%s=%s\n",
|
||||
_sensors->getSettingName(i),
|
||||
_sensors->getSettingValue(i));
|
||||
for (i = start; i < end; i++) {
|
||||
const char* name = _sensors->getSettingName(i);
|
||||
const char* value = _sensors->getSettingValue(i);
|
||||
if (name == NULL) name = "?";
|
||||
if (value == NULL) value = "";
|
||||
int room = reply_max - next_reserve - (int)(dp - reply);
|
||||
int need = snprintf(NULL, 0, "%s=%s\n", name, value);
|
||||
if (need >= room && i > start) break; // continue on the next page
|
||||
snprintf(dp, room, "%s=%s\n", name, value); // a single oversized entry is truncated
|
||||
dp = strchr(dp, 0);
|
||||
if (need >= room) { i++; break; }
|
||||
}
|
||||
if (i < end) {
|
||||
sprintf(dp, "... next:%d", i);
|
||||
} else {
|
||||
snprintf(dp, reply_max - (int)(dp - reply), "... next:%d", i);
|
||||
} else if (dp > reply && *(dp-1) == '\n') {
|
||||
*(dp-1) = 0; // remove last CR
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,6 +95,12 @@ struct ContactInfo {
|
||||
bool isTelemLocAllowed() const { return flags & 0x04; }
|
||||
bool isTelemEnvAllowed() const { return flags & 0x08; }
|
||||
bool isRemoteCLIAllowed() const { return flags & 0x10; }
|
||||
bool isEmptySlot() const {
|
||||
for (int i = 0; i < PUB_KEY_SIZE; i++) {
|
||||
if (id.pub_key[i] != 0) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
#if !MESH_CONTACT_CACHE
|
||||
|
||||
Vendored
+2
-2
@@ -1406,8 +1406,8 @@
|
||||
}
|
||||
],
|
||||
"noncompact_tokens_sha256": {
|
||||
"src/helpers/CommonCLI.cpp": "fd13053f606a8d008c10a690cbc7729c2701f44f84e433bb3cd2c776b9e61b0c",
|
||||
"examples/simple_repeater/MyMesh.cpp": "e92d418e64bd3e2ddff19d196aafac130cef02dd6463a687735dfe11cd273725",
|
||||
"src/helpers/CommonCLI.cpp": "ba486e64f3c948c44f08a6cb2ea4ef82bf1f3844fe0287e94f653654d3675dea",
|
||||
"examples/simple_repeater/MyMesh.cpp": "9e688372f2b35a2c0f20f0eae00255877200ac9d84c9e5a30870296fbb928ca8",
|
||||
"src/helpers/RadioProfileCLI.cpp": "d641d16e872eba3e73b9b5a50f9718c10b0b7da72782f793706ac36b3724a624",
|
||||
"src/helpers/WirelessControl.h": "f19bb602999118cc423ec74c7de7100ed91cb2c553295038077e52e321bb4e73",
|
||||
"src/helpers/PowerSavingDiagnostics.h": "18b78f5780046ea8c772c4ce6c337741ae0c027039d9399c94c5f4205d114000",
|
||||
|
||||
+7
-1
@@ -81,6 +81,10 @@ struct Task {
|
||||
void toggleGPS() { ++toggles; }
|
||||
};
|
||||
|
||||
struct Display {
|
||||
bool isEink() const { return TEST_EINK; }
|
||||
} display;
|
||||
|
||||
enum class HomePage { OTHER, SENSORS };
|
||||
|
||||
struct Home {
|
||||
@@ -97,7 +101,9 @@ struct Home {
|
||||
}
|
||||
};
|
||||
|
||||
static constexpr uint32_t interval_ms = AUTO_OFF_MILLIS > 0 ? 5000u : 60000u;
|
||||
static constexpr uint32_t interval_ms = TEST_NEW_UI
|
||||
? (TEST_EINK ? 60000u : 5000u)
|
||||
: (AUTO_OFF_MILLIS > 0 ? 5000u : 60000u);
|
||||
|
||||
static void begin_case(const char* name, uint32_t initial_ms) {
|
||||
active_case = name;
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <cstring>
|
||||
#include <ed_25519.h>
|
||||
|
||||
#include "helpers/AdvertDataHelpers.cpp"
|
||||
#include "helpers/BaseChatMesh.cpp"
|
||||
|
||||
namespace {
|
||||
|
||||
class DummyRadio : public mesh::Radio {
|
||||
public:
|
||||
int recvRaw(uint8_t*, int) override { return 0; }
|
||||
uint32_t getEstAirtimeFor(int) override { return 10; }
|
||||
float packetScore(float, int) override { return 1.0f; }
|
||||
bool startSendRaw(const uint8_t*, int) override { return true; }
|
||||
bool isSendComplete() override { return true; }
|
||||
void onSendFinished() override {}
|
||||
bool isInRecvMode() const override { return true; }
|
||||
};
|
||||
|
||||
class DummyMillisecondClock : public mesh::MillisecondClock {
|
||||
public:
|
||||
unsigned long getMillis() override { return 1000; }
|
||||
};
|
||||
|
||||
class DummyRNG : public mesh::RNG {
|
||||
public:
|
||||
void random(uint8_t* dest, size_t len) override { memset(dest, 0x42, len); }
|
||||
};
|
||||
|
||||
class DummyRTCClock : public mesh::RTCClock {
|
||||
uint32_t _now = 1700000000;
|
||||
public:
|
||||
uint32_t getCurrentTime() override { return _now; }
|
||||
void setCurrentTime(uint32_t time) override { _now = time; }
|
||||
};
|
||||
|
||||
class DummyPacketManager : public mesh::PacketManager {
|
||||
public:
|
||||
unsigned allocations = 0;
|
||||
mesh::Packet* allocNew() override { ++allocations; return nullptr; }
|
||||
void free(mesh::Packet*) override {}
|
||||
bool queueOutbound(mesh::Packet*, uint8_t, uint32_t) override { return false; }
|
||||
mesh::Packet* getNextOutbound(uint32_t) override { return nullptr; }
|
||||
int getOutboundCount(uint32_t) const override { return 0; }
|
||||
int getOutboundTotal() const override { return 0; }
|
||||
int getFreeCount() const override { return 16; }
|
||||
mesh::Packet* getOutboundByIdx(int) override { return nullptr; }
|
||||
mesh::Packet* removeOutboundByIdx(int) override { return nullptr; }
|
||||
void queueInbound(mesh::Packet*, uint32_t) override {}
|
||||
mesh::Packet* getNextInbound(uint32_t) override { return nullptr; }
|
||||
};
|
||||
|
||||
class DummyMeshTables : public mesh::MeshTables {
|
||||
public:
|
||||
bool wasSeen(const mesh::Packet*) override { return false; }
|
||||
void markSeen(const mesh::Packet*) override {}
|
||||
void markSent(const mesh::Packet*) override {}
|
||||
void clear(const mesh::Packet*) override {}
|
||||
};
|
||||
|
||||
class TestChatMesh : public BaseChatMesh {
|
||||
public:
|
||||
int messages_received = 0;
|
||||
|
||||
TestChatMesh(mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng,
|
||||
mesh::RTCClock& rtc, mesh::PacketManager& mgr, mesh::MeshTables& tables)
|
||||
: BaseChatMesh(radio, ms, rng, rtc, mgr, tables) {}
|
||||
|
||||
void onDiscoveredContact(ContactInfo&, bool, uint8_t, const uint8_t*) override {}
|
||||
bool processAck(const uint8_t*, ContactInfo*& peer) override {
|
||||
peer = nullptr;
|
||||
return false;
|
||||
}
|
||||
void onContactPathUpdated(const ContactInfo&) override {}
|
||||
void onMessageRecv(const ContactInfo&, mesh::Packet*, uint32_t, const char*) override {
|
||||
messages_received++;
|
||||
}
|
||||
void onCommandDataRecv(const ContactInfo&, mesh::Packet*, uint32_t, const char*) override {}
|
||||
void onCLICommandRecv(const ContactInfo&, mesh::Packet*, uint32_t, const char*, char*) override {}
|
||||
bool onSignedMessageRecv(const ContactInfo&, mesh::Packet*, uint32_t, const uint8_t*, const char*) override { return true; }
|
||||
void onChannelMessageRecv(const mesh::GroupChannel&, mesh::Packet*, uint32_t, const char*) override {}
|
||||
uint8_t onContactRequest(const ContactInfo&, uint32_t, const uint8_t*, uint8_t, uint8_t*) override { return 0; }
|
||||
void onContactResponse(const ContactInfo&, const uint8_t*, uint8_t) override {}
|
||||
uint32_t calcFloodTimeoutMillisFor(uint32_t) const override { return 1000; }
|
||||
uint32_t calcDirectTimeoutMillisFor(uint32_t, uint8_t) const override { return 1000; }
|
||||
void onSendTimeout() override {}
|
||||
|
||||
using BaseChatMesh::resetContacts;
|
||||
using BaseChatMesh::searchPeersByHash;
|
||||
using BaseChatMesh::getPeerSharedSecret;
|
||||
using BaseChatMesh::onPeerDataRecv;
|
||||
using mesh::Mesh::onRecvPacket;
|
||||
};
|
||||
|
||||
struct MeshFixture {
|
||||
DummyRadio radio;
|
||||
DummyMillisecondClock ms;
|
||||
DummyRNG rng;
|
||||
DummyRTCClock rtc;
|
||||
DummyPacketManager mgr;
|
||||
DummyMeshTables tables;
|
||||
TestChatMesh chat{radio, ms, rng, rtc, mgr, tables};
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(EmptyContactSlots, ZeroPubKeyProducesAllZeroSharedSecretForAnyPrivateKey) {
|
||||
ContactInfo empty_slot;
|
||||
EXPECT_TRUE(empty_slot.isEmptySlot());
|
||||
|
||||
uint8_t raw_prv[PRV_KEY_SIZE];
|
||||
for (int i = 0; i < PRV_KEY_SIZE; i++) {
|
||||
raw_prv[i] = static_cast<uint8_t>(0xA5 ^ (i * 13 + 7));
|
||||
}
|
||||
uint8_t secret[PUB_KEY_SIZE];
|
||||
ed25519_key_exchange(secret, empty_slot.id.pub_key, raw_prv);
|
||||
|
||||
uint8_t expected_zeroes[PUB_KEY_SIZE] = { 0 };
|
||||
EXPECT_EQ(0, memcmp(secret, expected_zeroes, PUB_KEY_SIZE));
|
||||
}
|
||||
|
||||
TEST(EmptyContactSlots, NullKeyPeerPacketIsRejectedWhenAnonSlotsAreEmpty) {
|
||||
MeshFixture fx;
|
||||
fx.chat.self_id.pub_key[0] = 0x42;
|
||||
|
||||
// Craft a PAYLOAD_TYPE_TXT_MSG addressed to self_id (dest_hash=0x42) with src_hash=0x00,
|
||||
// encrypted and MAC'd under the all-zero 32-byte key that an empty ContactInfo slot derives.
|
||||
uint8_t zero_secret[PUB_KEY_SIZE] = { 0 };
|
||||
uint8_t plaintext[16] = { 0 };
|
||||
uint32_t ts = 1700000001;
|
||||
memcpy(plaintext, &ts, 4);
|
||||
plaintext[4] = (TXT_TYPE_PLAIN << 2);
|
||||
strcpy(reinterpret_cast<char*>(&plaintext[5]), "forged");
|
||||
|
||||
mesh::Packet pkt;
|
||||
pkt.header = ROUTE_TYPE_FLOOD | (PAYLOAD_TYPE_TXT_MSG << PH_TYPE_SHIFT);
|
||||
pkt.path_len = 0;
|
||||
pkt.payload[0] = 0x42; // dest_hash (matches fx.chat.self_id)
|
||||
pkt.payload[1] = 0x00; // src_hash (matches all-zero pub_key if empty slots are not skipped)
|
||||
int enc_len = mesh::Utils::encryptThenMAC(zero_secret, &pkt.payload[2], plaintext, 12);
|
||||
pkt.payload_len = static_cast<uint8_t>(2 + enc_len);
|
||||
|
||||
fx.chat.onRecvPacket(&pkt);
|
||||
EXPECT_EQ(0, fx.chat.messages_received);
|
||||
}
|
||||
|
||||
TEST(EmptyContactSlots, SearchPeersByHashSkipsEmptyAnonSlotsAndFindsRealZeroPrefixPeer) {
|
||||
MeshFixture fx;
|
||||
const uint8_t zero_hash = 0x00;
|
||||
|
||||
// Freshly reset contacts has MAX_ANON_CONTACTS (8) zeroed transient slots.
|
||||
EXPECT_EQ(0, fx.chat.searchPeersByHash(&zero_hash));
|
||||
|
||||
// Add a legitimate permanent contact whose public key starts with 0x00.
|
||||
ContactInfo peer;
|
||||
peer.type = ADV_TYPE_CHAT;
|
||||
peer.lastmod = 1700000100;
|
||||
strcpy(peer.name, "Alice");
|
||||
peer.id.pub_key[0] = 0x00;
|
||||
for (int i = 1; i < PUB_KEY_SIZE; i++) {
|
||||
peer.id.pub_key[i] = static_cast<uint8_t>(i);
|
||||
}
|
||||
ASSERT_TRUE(fx.chat.addContact(peer));
|
||||
|
||||
// Only the populated contact at index MAX_ANON_CONTACTS should match.
|
||||
EXPECT_EQ(1, fx.chat.searchPeersByHash(&zero_hash));
|
||||
|
||||
// Lookup by pubkey and name prefix must also skip the empty transient slots.
|
||||
uint8_t zero_prefix[6] = { 0 };
|
||||
EXPECT_EQ(nullptr, fx.chat.lookupContactByPubKey(zero_prefix, sizeof(zero_prefix)));
|
||||
EXPECT_EQ(nullptr, fx.chat.lookupTransientContactByPubKey(zero_prefix, sizeof(zero_prefix)));
|
||||
EXPECT_EQ(nullptr, fx.chat.lookupPersistentContactByPubKey(zero_prefix, sizeof(zero_prefix)));
|
||||
|
||||
ContactInfo* found = fx.chat.lookupContactByPubKey(peer.id.pub_key, 6);
|
||||
ASSERT_NE(nullptr, found);
|
||||
EXPECT_STREQ("Alice", found->name);
|
||||
|
||||
ContactInfo* by_name = fx.chat.searchContactsByPrefix("");
|
||||
ASSERT_NE(nullptr, by_name);
|
||||
EXPECT_STREQ("Alice", by_name->name);
|
||||
}
|
||||
|
||||
TEST(EmptyContactSlots, PopulatedTransientAnonSlotStillMatchesEvenWhenLastmodIsZero) {
|
||||
MeshFixture fx;
|
||||
const uint8_t zero_hash = 0x00;
|
||||
|
||||
ContactInfo anon;
|
||||
anon.type = ADV_TYPE_NONE;
|
||||
anon.lastmod = 0; // e.g. RTC not yet initialised on nRF52
|
||||
anon.id.pub_key[0] = 0x00;
|
||||
anon.id.pub_key[1] = 0x7F;
|
||||
ASSERT_TRUE(fx.chat.addContact(anon));
|
||||
|
||||
EXPECT_FALSE(anon.isEmptySlot());
|
||||
EXPECT_EQ(1, fx.chat.searchPeersByHash(&zero_hash));
|
||||
EXPECT_NE(nullptr, fx.chat.lookupContactByPubKey(anon.id.pub_key, PUB_KEY_SIZE));
|
||||
}
|
||||
|
||||
TEST(EmptyContactSlots, AcceptedContactKeepsPriorityOverMatchingTransientPeer) {
|
||||
MeshFixture fx;
|
||||
ContactInfo anon;
|
||||
anon.id.pub_key[1] = 0x7F;
|
||||
ASSERT_TRUE(fx.chat.addContact(anon));
|
||||
ContactInfo accepted = anon;
|
||||
accepted.type = ADV_TYPE_CHAT;
|
||||
accepted.out_path_len = OUT_PATH_UNKNOWN;
|
||||
strcpy(accepted.name, "Alice");
|
||||
ASSERT_TRUE(fx.chat.addContact(accepted));
|
||||
const uint8_t zero_hash = 0;
|
||||
ASSERT_EQ(2, fx.chat.searchPeersByHash(&zero_hash));
|
||||
|
||||
mesh::Packet packet;
|
||||
packet.header = ROUTE_TYPE_FLOOD | (PAYLOAD_TYPE_TXT_MSG << PH_TYPE_SHIFT);
|
||||
packet.path_len = 0;
|
||||
uint8_t secret[PUB_KEY_SIZE] = {};
|
||||
uint8_t text[16] = {};
|
||||
text[4] = TXT_TYPE_PLAIN << 2;
|
||||
strcpy(reinterpret_cast<char*>(text + 5), "hello");
|
||||
fx.chat.onPeerDataRecv(&packet, PAYLOAD_TYPE_TXT_MSG, 0, secret, text, 11);
|
||||
EXPECT_EQ(1, fx.chat.messages_received);
|
||||
}
|
||||
|
||||
TEST(EmptyContactSlots, NegativeGroupTextLengthFailsBeforeReadingInputOrAllocating) {
|
||||
MeshFixture fx;
|
||||
mesh::GroupChannel channel;
|
||||
EXPECT_FALSE(fx.chat.sendGroupMessage(1, channel, nullptr, nullptr, -1));
|
||||
EXPECT_EQ(0u, fx.mgr.allocations);
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
@@ -67,13 +67,14 @@ class HomeSensorRefreshTest(unittest.TestCase):
|
||||
for ui in ('ui-new', 'ui-tiny'):
|
||||
source = work / (ui + '.cpp')
|
||||
source.write_text(harness(ui, mutation), encoding='ascii')
|
||||
for auto_off in (15000, 0):
|
||||
with self.subTest(ui=ui, auto_off=auto_off):
|
||||
binary = work / (ui + '-' + str(auto_off) +
|
||||
for auto_off, eink in ((15000, False), (15000, True), (0, False), (0, True)):
|
||||
with self.subTest(ui=ui, auto_off=auto_off, eink=eink):
|
||||
binary = work / (ui + '-' + str(auto_off) + '-' + str(int(eink)) +
|
||||
('.exe' if os.name == 'nt' else ''))
|
||||
compiled = subprocess.run([
|
||||
compiler, '-std=c++17', '-O1', '-g', '-Wall', '-Wextra', '-Werror',
|
||||
*sanitize, '-DAUTO_OFF_MILLIS=' + str(auto_off),
|
||||
'-DTEST_EINK=' + str(int(eink)),
|
||||
'-DTEST_NEW_UI=' + str(int(ui == 'ui-new')),
|
||||
str(source), '-o', str(binary),
|
||||
], text=True, capture_output=True, timeout=60)
|
||||
|
||||
@@ -12,6 +12,9 @@ static Packet makeFlood(uint8_t route_type, uint8_t payload_type, uint8_t hops)
|
||||
p.transport_codes[1] = 0;
|
||||
p.setPathHashSizeAndCount(1, hops);
|
||||
p.payload_len = 1;
|
||||
if (route_type == ROUTE_TYPE_TRANSPORT_FLOOD) {
|
||||
p.transport_codes[0] = 1; // has to be non-zero
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
"""Exercise upstream sensor-list bounds and duplicate-stat wire saturation."""
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from test_replay_reset_integration import extract_braced
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
SENSOR_HARNESS = r'''
|
||||
#include <array>
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@ATOI@
|
||||
|
||||
struct Setting { const char* name; const char* value; };
|
||||
struct Sensors {
|
||||
std::vector<Setting> settings;
|
||||
int reads = 0;
|
||||
int getNumSettings() const { return static_cast<int>(settings.size()); }
|
||||
const char* getSettingName(int i) {
|
||||
assert(i >= 0 && i < getNumSettings());
|
||||
++reads;
|
||||
return settings[i].name;
|
||||
}
|
||||
const char* getSettingValue(int i) {
|
||||
assert(i >= 0 && i < getNumSettings());
|
||||
++reads;
|
||||
return settings[i].value;
|
||||
}
|
||||
};
|
||||
struct Fixture {
|
||||
Sensors* _sensors;
|
||||
void handle(char* command, char* reply) {
|
||||
@SENSOR_LIST@
|
||||
else { assert(false && "fixture accepts only sensor list"); }
|
||||
}
|
||||
};
|
||||
|
||||
static std::string page(Fixture& fixture, int start = -1) {
|
||||
// A long trailing guard also makes the old unbounded write fail our own
|
||||
// assertion, without depending on a particular platform's stack layout.
|
||||
std::array<unsigned char, 32 + 160 + 8192> guarded;
|
||||
guarded.fill(0xA5);
|
||||
char* reply = reinterpret_cast<char*>(guarded.data() + 32);
|
||||
std::memset(reply, 0, 160);
|
||||
std::string text = "sensor list";
|
||||
if (start >= 0) text += " " + std::to_string(start);
|
||||
std::vector<char> command(text.begin(), text.end());
|
||||
command.push_back(0);
|
||||
fixture.handle(command.data(), reply);
|
||||
for (size_t i = 0; i < 32; ++i) assert(guarded[i] == 0xA5);
|
||||
for (size_t i = 32 + 160; i < guarded.size(); ++i) assert(guarded[i] == 0xA5);
|
||||
assert(std::memchr(reply, 0, 160) != nullptr);
|
||||
return reply;
|
||||
}
|
||||
|
||||
static int nextPage(const std::string& text) {
|
||||
const size_t marker = text.find("... next:");
|
||||
return marker == std::string::npos ? -1 : std::atoi(text.c_str() + marker + 9);
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
assert(argc == 2);
|
||||
const std::string scenario = argv[1];
|
||||
if (scenario == "nulls") {
|
||||
Sensors sensors{{{nullptr, nullptr}, {"enabled", nullptr}, {nullptr, "ok"}}};
|
||||
Fixture fixture{&sensors};
|
||||
assert(page(fixture) == "3 vars\n?=\nenabled=\n?=ok");
|
||||
sensors.settings = {{"", ""}, {"name", "1"}};
|
||||
assert(page(fixture) == "2 vars\n=\nname=1");
|
||||
sensors.settings.clear(); sensors.reads = 0;
|
||||
assert(page(fixture) == "no custom var" && sensors.reads == 0);
|
||||
sensors.settings = {{"one", "1"}};
|
||||
assert(page(fixture, 1) == "no custom var");
|
||||
assert(page(fixture, 999) == "no custom var" && sensors.reads == 0);
|
||||
} else if (scenario == "oversized") {
|
||||
const std::string name(1024, 'n'), value(3072, 'v');
|
||||
Sensors sensors{{{name.c_str(), value.c_str()}}};
|
||||
Fixture fixture{&sensors};
|
||||
const std::string output = page(fixture);
|
||||
assert(output.size() < 160 && output.find("1 vars\n") == 0);
|
||||
assert(nextPage(output) == -1);
|
||||
} else if (scenario == "pagination") {
|
||||
std::vector<std::string> names, values;
|
||||
names.reserve(30); values.reserve(30);
|
||||
Sensors sensors;
|
||||
for (int i = 0; i < 30; ++i) {
|
||||
names.push_back("sensor" + std::to_string(i));
|
||||
values.push_back(std::string(20, 'a' + i % 26));
|
||||
sensors.settings.push_back({names.back().c_str(), values.back().c_str()});
|
||||
}
|
||||
Fixture fixture{&sensors};
|
||||
int start = 0, pages = 0;
|
||||
std::string all;
|
||||
do {
|
||||
const std::string output = page(fixture, start);
|
||||
assert(output.find("30 vars\n") == 0);
|
||||
all += output + "\n";
|
||||
const int next = nextPage(output);
|
||||
if (next < 0) break;
|
||||
assert(next > start && next < 30 && ++pages < 30);
|
||||
start = next;
|
||||
} while (true);
|
||||
assert(pages > 1);
|
||||
for (size_t i = 0; i < names.size(); ++i) {
|
||||
const std::string row = names[i] + "=" + values[i] + "\n";
|
||||
const size_t found = all.find(row);
|
||||
assert(found != std::string::npos);
|
||||
assert(all.find(row, found + row.size()) == std::string::npos);
|
||||
}
|
||||
// An oversized entry encountered after a fitting entry is deferred, then
|
||||
// consumed once on the next page, so it cannot trap the paging cursor.
|
||||
const std::string huge(2048, 'z');
|
||||
sensors.settings = {{"first", "ok"}, {"large", huge.c_str()}, {"last", "ok"}};
|
||||
const std::string first = page(fixture, 0);
|
||||
assert(first.find("first=ok\n") != std::string::npos && nextPage(first) == 1);
|
||||
assert(nextPage(page(fixture, 1)) == 2);
|
||||
assert(page(fixture, 2) == "3 vars\nlast=ok");
|
||||
} else if (scenario == "boundaries") {
|
||||
// Move each entry across every remaining-space and truncation boundary.
|
||||
for (size_t length = 0; length <= 260; ++length) {
|
||||
const std::string value(length, 'x');
|
||||
Sensors sensors{{{"edge", value.c_str()}, {"tail", "done"}}};
|
||||
Fixture fixture{&sensors};
|
||||
const std::string first = page(fixture);
|
||||
const int next = nextPage(first);
|
||||
if (next >= 0) {
|
||||
assert(next == 1);
|
||||
assert(page(fixture, next) == "2 vars\ntail=done");
|
||||
} else {
|
||||
assert(first.find("tail=done") != std::string::npos);
|
||||
}
|
||||
}
|
||||
} else { assert(false && "unknown scenario"); }
|
||||
return 0;
|
||||
}
|
||||
'''
|
||||
|
||||
STATS_HARNESS = r'''
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
@STRUCT@;
|
||||
using WireStats = @STATS_TYPE@;
|
||||
static_assert(sizeof(WireStats::n_direct_dups) == 2, "direct duplicate wire width");
|
||||
static_assert(sizeof(WireStats::n_flood_dups) == 2, "flood duplicate wire width");
|
||||
struct SimpleMeshTables {
|
||||
uint32_t direct, flood;
|
||||
uint32_t getNumDirectDups() const { return direct; }
|
||||
uint32_t getNumFloodDups() const { return flood; }
|
||||
};
|
||||
struct Fixture {
|
||||
SimpleMeshTables tables;
|
||||
void* getTables() { return &tables; }
|
||||
WireStats read() {
|
||||
WireStats stats{};
|
||||
@ASSIGNMENTS@
|
||||
return stats;
|
||||
}
|
||||
};
|
||||
int main() {
|
||||
const uint32_t values[] = {0, 1, 65530, 65534, 65535, 65536, 65537, 131065, UINT32_MAX};
|
||||
for (const uint32_t direct : values) for (const uint32_t flood : values) {
|
||||
Fixture fixture{{direct, flood}};
|
||||
const WireStats stats = fixture.read();
|
||||
assert(stats.n_direct_dups == (direct > 65535 ? 65535 : direct));
|
||||
assert(stats.n_flood_dups == (flood > 65535 ? 65535 : flood));
|
||||
assert(fixture.tables.direct == direct && fixture.tables.flood == flood);
|
||||
unsigned char wire[sizeof(stats)];
|
||||
std::memcpy(wire, &stats, sizeof(stats));
|
||||
WireStats decoded{};
|
||||
std::memcpy(&decoded, wire, sizeof(decoded));
|
||||
assert(decoded.n_direct_dups == stats.n_direct_dups);
|
||||
assert(decoded.n_flood_dups == stats.n_flood_dups);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
class UpstreamCliBoundsTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.compiler = shutil.which("g++") or shutil.which("clang++")
|
||||
if not cls.compiler:
|
||||
raise unittest.SkipTest("a host C++17 compiler is required")
|
||||
cls.temporary = tempfile.TemporaryDirectory(prefix="meshcore-upstream-bounds-")
|
||||
cls.addClassCleanup(cls.temporary.cleanup)
|
||||
cls.work = Path(cls.temporary.name)
|
||||
source = (ROOT / "src/helpers/CommonCLI.cpp").read_text(encoding="utf-8")
|
||||
cls.sensor_code = SENSOR_HARNESS.replace(
|
||||
"@ATOI@", extract_braced(source, "static uint32_t _atoi("))
|
||||
cls.sensor_code = cls.sensor_code.replace(
|
||||
"@SENSOR_LIST@", extract_braced(source, 'if (memcmp(command, "sensor list", 11) == 0)'))
|
||||
cls.sensor = cls.compile(cls.sensor_code, "sensors")
|
||||
|
||||
@classmethod
|
||||
def compile(cls, code, name):
|
||||
source = cls.work / (name + ".cpp")
|
||||
binary = cls.work / (name + (".exe" if sys.platform == "win32" else ""))
|
||||
source.write_text(code, encoding="utf-8")
|
||||
sanitizers = (["-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-no-pie"]
|
||||
if sys.platform.startswith("linux") else [])
|
||||
built = subprocess.run([cls.compiler, "-std=c++17", "-O1", "-g", "-Wall", "-Wextra",
|
||||
"-Werror", *sanitizers, str(source), "-o", str(binary)],
|
||||
capture_output=True, text=True, timeout=60)
|
||||
if built.returncode:
|
||||
raise AssertionError(built.stdout + built.stderr)
|
||||
return binary
|
||||
|
||||
def run_binary(self, binary, *arguments, expect_failure=False):
|
||||
result = subprocess.run([str(binary), *arguments], capture_output=True, text=True, timeout=30)
|
||||
if expect_failure:
|
||||
self.assertNotEqual(result.returncode, 0, "unsafe negative control unexpectedly passed")
|
||||
else:
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
|
||||
def test_sensor_list_handles_null_empty_and_exhausted_entries(self):
|
||||
self.run_binary(self.sensor, "nulls")
|
||||
|
||||
def test_sensor_list_truncates_one_oversized_entry_without_overflow(self):
|
||||
self.run_binary(self.sensor, "oversized")
|
||||
|
||||
def test_sensor_list_paginates_without_losing_entries_or_stalling(self):
|
||||
self.run_binary(self.sensor, "pagination")
|
||||
|
||||
def test_sensor_list_remaining_space_boundaries(self):
|
||||
self.run_binary(self.sensor, "boundaries")
|
||||
|
||||
def test_previous_unbounded_sensor_write_is_detected(self):
|
||||
unsafe = self.sensor_code.replace('snprintf(dp, room, "%s=%s\\n", name, value);',
|
||||
'sprintf(dp, "%s=%s\\n", name, value);')
|
||||
self.assertNotEqual(unsafe, self.sensor_code)
|
||||
self.run_binary(self.compile(unsafe, "unsafe-sensors"), "oversized", expect_failure=True)
|
||||
|
||||
def stats_code(self, role, unsafe=False):
|
||||
source = (ROOT / "examples" / role / "MyMesh.cpp").read_text(encoding="utf-8")
|
||||
if role == "simple_repeater":
|
||||
struct_source = (ROOT / "examples" / role / "MyMesh.h").read_text(encoding="utf-8")
|
||||
stats_type = "RepeaterStats"
|
||||
else:
|
||||
struct_source = source
|
||||
stats_type = "ServerStats"
|
||||
request = extract_braced(source, "int MyMesh::handleRequest(")
|
||||
status = extract_braced(request, "if (payload[0] == REQ_TYPE_GET_STATUS)")
|
||||
assignments = []
|
||||
for field in ("direct", "flood"):
|
||||
match = re.search(r"stats\.n_" + field + r"_dups\s*=[^;]+;", status)
|
||||
self.assertIsNotNone(match, role + " " + field)
|
||||
text = match.group()
|
||||
if unsafe:
|
||||
text = re.sub(r"std::min<uint32_t>\((.*), 0xFFFF\)", r"\1", text)
|
||||
self.assertNotEqual(text, match.group())
|
||||
assignments.append(text)
|
||||
return (STATS_HARNESS.replace("@STRUCT@", extract_braced(struct_source, "struct " + stats_type))
|
||||
.replace("@STATS_TYPE@", stats_type).replace("@ASSIGNMENTS@", "\n".join(assignments)))
|
||||
|
||||
def test_duplicate_stats_saturate_both_roles_at_the_wire_boundary(self):
|
||||
for role in ("simple_repeater", "simple_room_server"):
|
||||
with self.subTest(role=role):
|
||||
self.run_binary(self.compile(self.stats_code(role), role + "-stats"))
|
||||
|
||||
def test_previous_wrapping_duplicate_stats_are_detected(self):
|
||||
for role in ("simple_repeater", "simple_room_server"):
|
||||
with self.subTest(role=role):
|
||||
self.run_binary(self.compile(self.stats_code(role, unsafe=True), role + "-unsafe-stats"),
|
||||
expect_failure=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -32,6 +32,9 @@ build_flags = ${rak4631_hw.build_flags}
|
||||
-D PIN_GPS_RESET_ACTIVE=LOW
|
||||
-D TELEM_BME280_ADDRESS=0x77
|
||||
-D DISPLAY_CLASS=GxEPDDisplay
|
||||
-D EINK_DISPLAY_MODEL=GxEPD2_154_D67
|
||||
-D UI_RECENT_LIST_SIZE=9
|
||||
-D UI_MSG_PREVIEW_SIZE=165
|
||||
-D BACKLIGHT_BTN=PIN_BUTTON2
|
||||
-D AUTO_OFF_MILLIS=0
|
||||
build_src_filter = ${rak4631_hw.build_src_filter}
|
||||
|
||||
@@ -131,6 +131,12 @@ extern const int SCK;
|
||||
#define DISP_BUSY (3)
|
||||
#define DISP_BACKLIGHT (43)
|
||||
|
||||
// used by GxEPDDisplay when EINK_DISPLAY_MODEL is set
|
||||
#define PIN_DISPLAY_CS DISP_CS
|
||||
#define PIN_DISPLAY_DC DISP_DC
|
||||
#define PIN_DISPLAY_RST DISP_RST
|
||||
#define PIN_DISPLAY_BUSY DISP_BUSY
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// GPS
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
void MeshTrackerX1Board::begin() {
|
||||
NRF52BoardDCDC::begin();
|
||||
btn_prev_state = LOW; // button is active HIGH
|
||||
|
||||
#ifdef BUTTON_PIN
|
||||
pinMode(BATTERY_PIN, INPUT);
|
||||
|
||||
@@ -5,9 +5,6 @@
|
||||
#include <helpers/NRF52Board.h>
|
||||
|
||||
class MeshTrackerX1Board : public NRF52BoardDCDC {
|
||||
protected:
|
||||
uint8_t btn_prev_state;
|
||||
|
||||
public:
|
||||
MeshTrackerX1Board() : NRF52Board("X1_OTA") {}
|
||||
void begin();
|
||||
@@ -50,17 +47,6 @@ public:
|
||||
return "Seeed SenseCAP MeshTracker X1";
|
||||
}
|
||||
|
||||
int buttonStateChanged() {
|
||||
#ifdef BUTTON_PIN
|
||||
uint8_t v = digitalRead(BUTTON_PIN);
|
||||
if (v != btn_prev_state) {
|
||||
btn_prev_state = v;
|
||||
return (v == USER_BTN_PRESSED) ? 1 : -1;
|
||||
}
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
|
||||
void shutdownPeripherals() override {
|
||||
NRF52Board::shutdownPeripherals();
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
void PromicroBoard::begin() {
|
||||
NRF52Board::begin();
|
||||
btn_prev_state = HIGH;
|
||||
|
||||
pinMode(PIN_VBAT_READ, INPUT);
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
|
||||
class PromicroBoard : public NRF52BoardDCDC {
|
||||
protected:
|
||||
uint8_t btn_prev_state;
|
||||
float adc_mult = ADC_MULTIPLIER;
|
||||
|
||||
public:
|
||||
@@ -51,15 +50,4 @@ public:
|
||||
const char* getManufacturerName() const override {
|
||||
return "ProMicro DIY";
|
||||
}
|
||||
|
||||
int buttonStateChanged() {
|
||||
#ifdef BUTTON_PIN
|
||||
uint8_t v = digitalRead(BUTTON_PIN);
|
||||
if (v != btn_prev_state) {
|
||||
btn_prev_state = v;
|
||||
return (v == LOW) ? 1 : -1;
|
||||
}
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
void T1000eBoard::begin() {
|
||||
NRF52BoardDCDC::begin();
|
||||
btn_prev_state = HIGH;
|
||||
|
||||
#ifdef BUTTON_PIN
|
||||
pinMode(BATTERY_PIN, INPUT);
|
||||
|
||||
@@ -5,9 +5,6 @@
|
||||
#include <helpers/NRF52Board.h>
|
||||
|
||||
class T1000eBoard : public NRF52BoardDCDC {
|
||||
protected:
|
||||
uint8_t btn_prev_state;
|
||||
|
||||
public:
|
||||
T1000eBoard() : NRF52Board("T1000E_OTA") {}
|
||||
void begin();
|
||||
@@ -46,17 +43,6 @@ public:
|
||||
return "Seeed Tracker T1000-E";
|
||||
}
|
||||
|
||||
int buttonStateChanged() {
|
||||
#ifdef BUTTON_PIN
|
||||
uint8_t v = digitalRead(BUTTON_PIN);
|
||||
if (v != btn_prev_state) {
|
||||
btn_prev_state = v;
|
||||
return (v == USER_BTN_PRESSED) ? 1 : -1;
|
||||
}
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
|
||||
void shutdownPeripherals() override {
|
||||
NRF52Board::shutdownPeripherals();
|
||||
|
||||
|
||||
@@ -25,6 +25,11 @@ build_flags = ${rak4631_hw.build_flags}
|
||||
-D SX126X_RX_BOOSTED_GAIN=1
|
||||
-D LORA_TX_POWER=22
|
||||
-D P_LORA_TX_LED=13
|
||||
-D UI_RECENT_LIST_SIZE=9
|
||||
-D UI_MSG_PREVIEW_SIZE=165
|
||||
-D EINK_DISPLAY_MODEL=GxEPD2_154_D67
|
||||
-D BACKLIGHT_BTN=PIN_BUTTON2
|
||||
-D DISABLE_DIAGNOSTIC_OUTPUT
|
||||
build_src_filter = ${rak4631_hw.build_src_filter}
|
||||
+<helpers/*.cpp>
|
||||
+<ThinkNodeM1Board.cpp>
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
#define BUTTON_PIN PIN_BUTTON1
|
||||
#define PIN_USER_BTN BUTTON_PIN
|
||||
|
||||
#define PIN_BUTTON2 (11)
|
||||
#define PIN_BUTTON2 (39)
|
||||
#define BUTTON_PIN2 PIN_BUTTON2
|
||||
|
||||
#define EXTERNAL_FLASH_DEVICES MX25R1635F
|
||||
@@ -131,6 +131,11 @@ extern const int SCK;
|
||||
#define DISP_BUSY (3)
|
||||
#define DISP_BACKLIGHT (43)
|
||||
|
||||
#define PIN_DISPLAY_CS DISP_CS
|
||||
#define PIN_DISPLAY_DC DISP_DC
|
||||
#define PIN_DISPLAY_RST DISP_RST
|
||||
#define PIN_DISPLAY_BUSY DISP_BUSY
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// GPS
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
|
||||
void ThinkNodeM3Board::begin() {
|
||||
NRF52Board::begin();
|
||||
btn_prev_state = HIGH;
|
||||
|
||||
Wire.begin();
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ protected:
|
||||
#if NRF52_POWER_MANAGEMENT
|
||||
void initiateShutdown(uint8_t reason) override;
|
||||
#endif
|
||||
uint8_t btn_prev_state;
|
||||
|
||||
public:
|
||||
ThinkNodeM3Board() : NRF52Board("THINKNODE_M3_OTA") {}
|
||||
@@ -38,17 +37,6 @@ public:
|
||||
return "Elecrow ThinkNode M3";
|
||||
}
|
||||
|
||||
int buttonStateChanged() {
|
||||
#ifdef BUTTON_PIN
|
||||
uint8_t v = digitalRead(BUTTON_PIN);
|
||||
if (v != btn_prev_state) {
|
||||
btn_prev_state = v;
|
||||
return (v == LOW) ? 1 : -1;
|
||||
}
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
|
||||
void shutdownPeripherals() override {
|
||||
NRF52Board::shutdownPeripherals();
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
|
||||
void ThinkNodeM4Board::begin() {
|
||||
NRF52Board::begin();
|
||||
btn_prev_state = HIGH;
|
||||
|
||||
Wire.begin();
|
||||
battery_serial->begin(4800);
|
||||
|
||||
@@ -9,7 +9,6 @@ protected:
|
||||
#if NRF52_POWER_MANAGEMENT
|
||||
void initiateShutdown(uint8_t reason) override;
|
||||
#endif
|
||||
uint8_t btn_prev_state;
|
||||
HardwareSerial * battery_serial;
|
||||
uint16_t bat_level_mv = 0;
|
||||
uint8_t bat_level_percent =0;
|
||||
@@ -37,17 +36,6 @@ public:
|
||||
return "Elecrow ThinkNode M4";
|
||||
}
|
||||
|
||||
int buttonStateChanged() {
|
||||
#ifdef BUTTON_PIN
|
||||
uint8_t v = digitalRead(BUTTON_PIN);
|
||||
if (v != btn_prev_state) {
|
||||
btn_prev_state = v;
|
||||
return (v == USER_BTN_PRESSED) ? 1 : -1;
|
||||
}
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
|
||||
void shutdownPeripherals() override {
|
||||
// shutdown common peripherals
|
||||
NRF52Board::shutdownPeripherals();
|
||||
|
||||
@@ -26,6 +26,7 @@ build_flags = ${esp32_base.build_flags}
|
||||
-D BACKLIGHT_BTN=PIN_BUTTON2
|
||||
-D AUTO_OFF_MILLIS=0
|
||||
-D UI_RECENT_LIST_SIZE=9
|
||||
-D UI_MSG_PREVIEW_SIZE=165
|
||||
-D DISABLE_DIAGNOSTIC_OUTPUT
|
||||
-D SX126X_DIO2_AS_RF_SWITCH=true
|
||||
-D SX126X_DIO3_TCXO_VOLTAGE=3.3
|
||||
@@ -174,12 +175,14 @@ build_flags =
|
||||
-D MAX_GROUP_CHANNELS=40
|
||||
-D OFFLINE_QUEUE_SIZE=256
|
||||
-D ENABLE_USB_INTERFACE
|
||||
-D ENABLE_WIFI_INTERFACE
|
||||
build_src_filter = ${ThinkNode_M5.build_src_filter}
|
||||
+<helpers/esp32/*.cpp>
|
||||
+<helpers/ui/MomentaryButton.cpp>
|
||||
+<../examples/companion_radio/*.cpp>
|
||||
+<../examples/companion_radio/ui-new/*.cpp>
|
||||
+<helpers/ui/buzzer.cpp>
|
||||
+<helpers/wifi/*.cpp>
|
||||
lib_deps =
|
||||
${ThinkNode_M5.lib_deps}
|
||||
densaugeo/base64 @ ~1.4.0
|
||||
|
||||
@@ -36,7 +36,7 @@ build_flags = ${nrf52_base.build_flags}
|
||||
-D LORA_TX_POWER=22
|
||||
build_src_filter = ${nrf52_base.build_src_filter}
|
||||
+<helpers/*.cpp>
|
||||
+<helpers/sensors/EnvironmentSensorManager.cpp>
|
||||
+<helpers/sensors>
|
||||
+<ThinkNodeM8Board.cpp>
|
||||
+<../variants/thinknode_m8>
|
||||
lib_deps =
|
||||
|
||||
Reference in new issue
Block a user