mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-09-14 15:16:40 +00:00
Merge upstream companion-v1.17.0 (CAD) into power-saving
Adopts hardware Channel Activity Detection (wired into RadioLibWrapper::isChannelActive() alongside our RSSI-threshold check and RX duty-cycle power-save), MCU temperature telemetry, LR2021 standby workaround, DISPLAY_SCALE/FLIP overrides, NRF52Board shutdownPeripherals() refactor, and misc upstream fixes. Declines upstream's ConfigSerializer-based NodePrefs rewrite, MultiSerialInterface/interface_manager, and UIColor palette system — each would have broken large parts of the Solo-specific feature set (NodePrefs fields, per-variant single serial_interface, enum-based DisplayDriver::Color). Flagged as candidate follow-up migrations, not permanent no's. Also fixes several pre-existing bugs surfaced while chasing silent merge breaks (stale newMsg() override signature in ui-tiny/ui-orig, dead UIEventType::newContactMessage case, missing ContactsIterator init), bumps FIRMWARE_VERSION/MESHCORE_VERSION to 1.17, and fixes a missing <cstdlib> include that broke the native ConfigSerializer unit tests. Verified via 13+ pio run builds across ESP32/nRF52, all 3 companion UI variants, and 7 display drivers, plus the full native unit test suite (33/33 passing). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cmath>
|
||||
#include "Stream.h"
|
||||
|
||||
inline uint32_t g_mock_millis = 0;
|
||||
|
||||
using std::isnan;
|
||||
|
||||
inline uint32_t millis() {
|
||||
return g_mock_millis;
|
||||
}
|
||||
|
||||
inline void delay(uint32_t ms) {
|
||||
g_mock_millis += ms;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
class CayenneLPP {
|
||||
public:
|
||||
explicit CayenneLPP(size_t) {}
|
||||
const uint8_t* getBuffer() const { return nullptr; }
|
||||
uint16_t getSize() const { return 0; }
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include "Utils.h"
|
||||
|
||||
namespace mesh {
|
||||
|
||||
class Identity {
|
||||
public:
|
||||
uint8_t pub_key[PUB_KEY_SIZE];
|
||||
|
||||
Identity() {
|
||||
std::memset(pub_key, 0, sizeof(pub_key));
|
||||
}
|
||||
|
||||
explicit Identity(const uint8_t* src) {
|
||||
std::memcpy(pub_key, src, PUB_KEY_SIZE);
|
||||
}
|
||||
|
||||
bool verify(const uint8_t*, const uint8_t*, int) const {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
class LocalIdentity : public Identity {
|
||||
public:
|
||||
LocalIdentity() : Identity() {}
|
||||
|
||||
void sign(uint8_t* sig, const uint8_t*, int) const {
|
||||
std::memset(sig, 0x5A, SIGNATURE_SIZE);
|
||||
}
|
||||
|
||||
void calcSharedSecret(uint8_t* secret, const uint8_t*) const {
|
||||
std::memset(secret, 0x11, PUB_KEY_SIZE);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace mesh {
|
||||
|
||||
class Radio {
|
||||
public:
|
||||
virtual ~Radio() = default;
|
||||
virtual bool isReceiving() { return false; }
|
||||
virtual uint32_t getEstAirtimeFor(uint16_t) { return 10; }
|
||||
virtual bool startSendRaw(const uint8_t*, uint16_t) { return true; }
|
||||
virtual bool isSendComplete() { return true; }
|
||||
virtual void onSendFinished() {}
|
||||
virtual int16_t getNoiseFloor() { return -120; }
|
||||
};
|
||||
|
||||
class MainBoard {
|
||||
public:
|
||||
virtual ~MainBoard() = default;
|
||||
virtual uint16_t getBattMilliVolts() { return 4200; }
|
||||
virtual float getMCUTemperature() { return 25.0f; }
|
||||
virtual const char* getManufacturerName() { return "mock-board"; }
|
||||
virtual void reboot() {}
|
||||
};
|
||||
|
||||
}
|
||||
+25
-4
@@ -3,12 +3,33 @@
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
// Mock SHA256 class for testing
|
||||
// Provides minimal interface to allow Utils.cpp to compile
|
||||
// Mock SHA256 for native testing — deterministic but not cryptographic.
|
||||
// finalize() writes real (non-garbage) output so calculatePacketHash() produces
|
||||
// distinguishable results for packets with different payloads.
|
||||
#include <string.h>
|
||||
|
||||
class SHA256 {
|
||||
uint8_t _state[32];
|
||||
size_t _len;
|
||||
public:
|
||||
void update(const uint8_t* data, size_t len) {}
|
||||
void finalize(uint8_t* hash, size_t hashLen) {}
|
||||
SHA256() : _len(0) { memset(_state, 0, sizeof(_state)); }
|
||||
|
||||
void update(const void* data, size_t len) {
|
||||
const uint8_t* bytes = static_cast<const uint8_t*>(data);
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
uint8_t b = bytes[i];
|
||||
_state[_len % 32] ^= b;
|
||||
_state[(_len + 1) % 32] += (uint8_t)((b >> 1) | (b << 7));
|
||||
_len++;
|
||||
}
|
||||
}
|
||||
|
||||
void finalize(uint8_t* hash, size_t hashLen) {
|
||||
for (size_t i = 0; i < hashLen; i++) {
|
||||
hash[i] = _state[i % 32];
|
||||
}
|
||||
}
|
||||
|
||||
void resetHMAC(const uint8_t* key, size_t keyLen) {}
|
||||
void finalizeHMAC(const uint8_t* key, size_t keyLen, uint8_t* hash, size_t hashLen) {}
|
||||
};
|
||||
|
||||
+66
-3
@@ -1,10 +1,73 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
// Mock Stream class for native testing
|
||||
// Provides minimal interface needed by Utils.h
|
||||
|
||||
class Stream {
|
||||
#define DEC 10
|
||||
#define HEX 16
|
||||
#define OCT 8
|
||||
#define BIN 2
|
||||
|
||||
class Print
|
||||
{
|
||||
public:
|
||||
virtual void print(char c) {}
|
||||
virtual void print(const char* str) {}
|
||||
virtual size_t write(uint8_t b) { return 1; }
|
||||
size_t write(const char *str)
|
||||
{
|
||||
if(str == NULL) {
|
||||
return 0;
|
||||
}
|
||||
return write((const uint8_t *) str, strlen(str));
|
||||
}
|
||||
virtual size_t write(const uint8_t *buffer, size_t size) {
|
||||
size_t t = 0;
|
||||
for (int i = 0; i < size; i++) { t += write(buffer[i]); }
|
||||
return t;
|
||||
}
|
||||
size_t write(const char *buffer, size_t size)
|
||||
{
|
||||
return write((const uint8_t *) buffer, size);
|
||||
}
|
||||
|
||||
virtual size_t print(unsigned char b, int r = DEC) { return 0; }
|
||||
virtual size_t print(int v, int r = DEC) { return 0; }
|
||||
virtual size_t print(unsigned int v, int r = DEC) { return 0; }
|
||||
virtual size_t print(long v, int r = DEC) { return 0; }
|
||||
virtual size_t print(unsigned long v, int r = DEC) { return 0; }
|
||||
virtual size_t print(long long v, int r = DEC) { return 0; }
|
||||
virtual size_t print(unsigned long long v, int r = DEC) { return 0; }
|
||||
virtual size_t print(double v, int p = 2) { return 0; }
|
||||
|
||||
size_t print(char c) { return write(c); }
|
||||
size_t print(const char* str) { return write(str); }
|
||||
|
||||
//size_t println(void) { return 0; }
|
||||
|
||||
virtual void flush() { /* Empty implementation for backward compatibility */ }
|
||||
};
|
||||
|
||||
class Stream: public Print
|
||||
{
|
||||
public:
|
||||
virtual ~Stream() = default;
|
||||
virtual int available() { return 0; }
|
||||
virtual int availableForWrite() { return 0; }
|
||||
virtual int read() { return -1; }
|
||||
virtual int peek() { return 0; }
|
||||
|
||||
virtual size_t readBytes(char *buffer, size_t length) {
|
||||
size_t i = 0;
|
||||
while (i < length && available()) {
|
||||
buffer[i++] = read();
|
||||
}
|
||||
return i;
|
||||
}
|
||||
virtual size_t readBytes(uint8_t *buffer, size_t length)
|
||||
{
|
||||
return readBytes((char *) buffer, length);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <algorithm>
|
||||
|
||||
#define PUB_KEY_SIZE 32
|
||||
#define PRV_KEY_SIZE 64
|
||||
#define SIGNATURE_SIZE 64
|
||||
#define CIPHER_MAC_SIZE 16
|
||||
|
||||
namespace mesh {
|
||||
|
||||
class RNG {
|
||||
public:
|
||||
virtual ~RNG() = default;
|
||||
virtual void random(uint8_t* dest, size_t sz) = 0;
|
||||
};
|
||||
|
||||
class Utils {
|
||||
public:
|
||||
static void sha256(uint8_t* hash, size_t hash_len, const uint8_t*, int) {
|
||||
std::memset(hash, 0, hash_len);
|
||||
}
|
||||
|
||||
static int encryptThenMAC(const uint8_t*, uint8_t* dest, const uint8_t* src, int src_len) {
|
||||
int out_len = src_len + CIPHER_MAC_SIZE;
|
||||
std::memset(dest, 0xAA, CIPHER_MAC_SIZE);
|
||||
std::memcpy(dest + CIPHER_MAC_SIZE, src, src_len);
|
||||
return out_len;
|
||||
}
|
||||
|
||||
static int MACThenDecrypt(const uint8_t*, uint8_t* dest, const uint8_t* src, int src_len) {
|
||||
if (src_len < CIPHER_MAC_SIZE) {
|
||||
return 0;
|
||||
}
|
||||
int out_len = src_len - CIPHER_MAC_SIZE;
|
||||
std::memcpy(dest, src + CIPHER_MAC_SIZE, out_len);
|
||||
return out_len;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include "CayenneLPP.h"
|
||||
|
||||
class SensorManager {
|
||||
public:
|
||||
virtual ~SensorManager() = default;
|
||||
virtual bool querySensors(uint8_t, CayenneLPP&) { return false; }
|
||||
};
|
||||
@@ -0,0 +1,180 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include "helpers/ConfigSerializer.h"
|
||||
|
||||
#define TEST_INT_S "56"
|
||||
#define TEST_INT 56
|
||||
#define TEST_FLOAT_S "-6.123"
|
||||
#define TEST_FLOAT -6.1230f
|
||||
#define TEST_DOUBLE_S "12.123456"
|
||||
#define TEST_DOUBLE 12.123456
|
||||
|
||||
class MockInputStream : public Stream {
|
||||
const char* _text;
|
||||
int pos, len;
|
||||
public:
|
||||
MockInputStream(const char* text) : _text(text) { pos = 0; len = strlen(text); }
|
||||
int available() override { return len - pos; }
|
||||
int read() override { if (pos < len) { return _text[pos++]; } return -1; }
|
||||
int peek() override { if (pos < len) { return _text[pos]; } return -1; }
|
||||
};
|
||||
|
||||
class MockPrintStream : public Stream {
|
||||
int len = 0;
|
||||
uint8_t _buf[1024];
|
||||
public:
|
||||
size_t write(uint8_t b) override {
|
||||
if (len < sizeof(_buf)) {
|
||||
_buf[len++] = b;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t print(unsigned char b, int r) override { if (b == TEST_INT) return Print::print(TEST_INT_S); return 0; }
|
||||
size_t print(int v, int r) override { if (v == TEST_INT) return Print::print(TEST_INT_S); return 0; }
|
||||
size_t print(unsigned int v, int r) override { if (v == TEST_INT) return Print::print(TEST_INT_S); return 0; }
|
||||
size_t print(long v, int r) override { if (v == TEST_INT) return Print::print(TEST_INT_S); return 0; }
|
||||
size_t print(unsigned long v, int r) override { if (v == TEST_INT) return Print::print(TEST_INT_S); return 0; }
|
||||
size_t print(long long v, int r) override { if (v == TEST_INT) return Print::print(TEST_INT_S); return 0; }
|
||||
size_t print(unsigned long long v, int r) override { if (v == TEST_INT) return Print::print(TEST_INT_S); return 0; }
|
||||
size_t print(double v, int p = 2) override {
|
||||
if (p == 6) return Print::print(TEST_DOUBLE_S);
|
||||
if (p == 4) return Print::print(TEST_FLOAT_S);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int getLength() const { return len; }
|
||||
const uint8_t* getBytes() const { return _buf; }
|
||||
};
|
||||
|
||||
class TestStruct : public ConfigSerializer {
|
||||
protected:
|
||||
void structure() override {
|
||||
def("age", age);
|
||||
def("flags", flags);
|
||||
def("name", name, sizeof(name));
|
||||
}
|
||||
public:
|
||||
int32_t age;
|
||||
char name[16];
|
||||
uint8_t flags;
|
||||
};
|
||||
|
||||
// ── saveSerial: basic ───────────────────────────────────────────────────────
|
||||
|
||||
TEST(ConfigSerializer, SaveSerial_Basic) {
|
||||
MockPrintStream s;
|
||||
TestStruct data;
|
||||
|
||||
data.age = TEST_INT;
|
||||
data.flags = TEST_INT;
|
||||
strcpy(data.name, "Scott");
|
||||
|
||||
bool success = data.saveSerial(s);
|
||||
EXPECT_TRUE(success);
|
||||
|
||||
auto l = s.getLength();
|
||||
const char* expect = "{age:" TEST_INT_S ",flags:" TEST_INT_S ",name:\"Scott\"}";
|
||||
EXPECT_EQ(strlen(expect), l);
|
||||
|
||||
bool match = memcmp(s.getBytes(), expect, l) == 0;
|
||||
EXPECT_TRUE(match);
|
||||
}
|
||||
|
||||
|
||||
TEST(ConfigSerializer, SaveSerial_EscChars) {
|
||||
MockPrintStream s;
|
||||
TestStruct data;
|
||||
|
||||
data.age = TEST_INT;
|
||||
data.flags = TEST_INT;
|
||||
strcpy(data.name, "\"Scott\"\n");
|
||||
|
||||
bool success = data.saveSerial(s);
|
||||
EXPECT_TRUE(success);
|
||||
|
||||
auto l = s.getLength();
|
||||
const char* expect = "{age:" TEST_INT_S ",flags:" TEST_INT_S ",name:\"\\\"Scott\\\"\\n\"}";
|
||||
EXPECT_EQ(strlen(expect), l);
|
||||
|
||||
bool match = memcmp(s.getBytes(), expect, l) == 0;
|
||||
EXPECT_TRUE(match);
|
||||
}
|
||||
|
||||
// ── loadSerial: basic ───────────────────────────────────────────────────────
|
||||
|
||||
TEST(ConfigSerializer, LoadSerial_Basic) {
|
||||
MockInputStream s("{age:" TEST_INT_S ",flags:" TEST_INT_S ",name:\"Scott\"}");
|
||||
TestStruct data;
|
||||
|
||||
bool success = data.loadSerial(s);
|
||||
EXPECT_TRUE(success);
|
||||
|
||||
EXPECT_EQ(TEST_INT, data.age);
|
||||
EXPECT_EQ(TEST_INT, data.flags);
|
||||
bool match = strcmp("Scott", data.name) == 0;
|
||||
EXPECT_TRUE(match);
|
||||
}
|
||||
|
||||
TEST(ConfigSerializer, LoadSerial_HandleWhitespace) {
|
||||
MockInputStream s(" { age: " TEST_INT_S " , flags: " TEST_INT_S " , name: \"Scott\" } ");
|
||||
TestStruct data;
|
||||
|
||||
bool success = data.loadSerial(s);
|
||||
EXPECT_TRUE(success);
|
||||
|
||||
EXPECT_EQ(TEST_INT, data.age);
|
||||
EXPECT_EQ(TEST_INT, data.flags);
|
||||
bool match = strcmp("Scott", data.name) == 0;
|
||||
EXPECT_TRUE(match);
|
||||
}
|
||||
|
||||
TEST(ConfigSerializer, LoadSerial_EscChars) {
|
||||
MockInputStream s("{age:" TEST_INT_S ",flags:" TEST_INT_S ",name:\"\\\"Scott\\\"\\n\"}");
|
||||
TestStruct data;
|
||||
|
||||
bool success = data.loadSerial(s);
|
||||
EXPECT_TRUE(success);
|
||||
|
||||
bool match = strcmp("\"Scott\"\n", data.name) == 0;
|
||||
EXPECT_TRUE(match);
|
||||
}
|
||||
|
||||
TEST(ConfigSerializer, LoadSerial_UnmatchedBraces) {
|
||||
MockInputStream s("{age:" TEST_INT_S ",flags:" TEST_INT_S ",name:\"Scott\"");
|
||||
TestStruct data;
|
||||
|
||||
bool success = data.loadSerial(s);
|
||||
EXPECT_FALSE(success);
|
||||
}
|
||||
|
||||
TEST(ConfigSerializer, LoadSerial_MissingCommas) {
|
||||
MockInputStream s("{age:" TEST_INT_S " flags:" TEST_INT_S " name:\"Scott\"}");
|
||||
TestStruct data;
|
||||
|
||||
bool success = data.loadSerial(s);
|
||||
EXPECT_FALSE(success);
|
||||
}
|
||||
|
||||
TEST(ConfigSerializer, LoadSerial_IgnoreUnknowns) {
|
||||
MockInputStream s("{age:" TEST_INT_S ",xxx:" TEST_INT_S ",name:\"Scott\"}");
|
||||
TestStruct data;
|
||||
data.flags = 1;
|
||||
|
||||
// should ignore the 'xxx' property
|
||||
bool success = data.loadSerial(s);
|
||||
EXPECT_TRUE(success);
|
||||
|
||||
EXPECT_EQ(TEST_INT, data.age);
|
||||
EXPECT_EQ(1, data.flags); // flags should be unmodified
|
||||
bool match = strcmp("Scott", data.name) == 0;
|
||||
EXPECT_TRUE(match);
|
||||
}
|
||||
|
||||
|
||||
// ── main ───────────────────────────────────────────────────────
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <condition_variable>
|
||||
#include <future>
|
||||
#include <mutex>
|
||||
#include <queue>
|
||||
#include <vector>
|
||||
|
||||
#include "KissModem.h"
|
||||
|
||||
static constexpr int TEST_TX_AVAILABLE_BYTES = 4096;
|
||||
static constexpr size_t TEST_DEFAULT_MAX_WRITE_CHUNK = SIZE_MAX;
|
||||
static constexpr size_t TEST_PARTIAL_WRITE_CHUNK = 2;
|
||||
static constexpr int TEST_PARTIAL_WRITE_FLUSH_LOOPS = 3;
|
||||
static constexpr uint8_t TEST_SNR = 8;
|
||||
static constexpr uint8_t TEST_RSSI = 200;
|
||||
|
||||
class BlockingStream : public Stream {
|
||||
public:
|
||||
void pushRx(const std::vector<uint8_t>& bytes) {
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
for (uint8_t b : bytes) {
|
||||
_rx.push(b);
|
||||
}
|
||||
}
|
||||
|
||||
void setBlockWrites(bool blocked) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
_block_writes = blocked;
|
||||
}
|
||||
_cv.notify_all();
|
||||
}
|
||||
|
||||
bool isWriteBlocked() const {
|
||||
return _entered_block.load();
|
||||
}
|
||||
|
||||
size_t writesCount() const {
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
return _writes.size();
|
||||
}
|
||||
|
||||
std::vector<uint8_t> writesSnapshot() const {
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
return _writes;
|
||||
}
|
||||
|
||||
int availableForWrite() override {
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
return _block_writes ? 0 : TEST_TX_AVAILABLE_BYTES;
|
||||
}
|
||||
|
||||
void setMaxWriteChunk(size_t chunk) {
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
_max_write_chunk = chunk;
|
||||
}
|
||||
|
||||
size_t write(const uint8_t* buffer, size_t size) override {
|
||||
std::unique_lock<std::mutex> lock(_mutex);
|
||||
while (_block_writes) {
|
||||
_entered_block.store(true);
|
||||
_cv.wait(lock);
|
||||
}
|
||||
const size_t chunk = (size < _max_write_chunk) ? size : _max_write_chunk;
|
||||
for (size_t i = 0; i < chunk; i++) {
|
||||
_writes.push_back(buffer[i]);
|
||||
}
|
||||
return chunk;
|
||||
}
|
||||
|
||||
size_t write(uint8_t b) override {
|
||||
return write(&b, 1);
|
||||
}
|
||||
|
||||
int available() override {
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
return static_cast<int>(_rx.size());
|
||||
}
|
||||
|
||||
int read() override {
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
if (_rx.empty()) {
|
||||
return -1;
|
||||
}
|
||||
int b = _rx.front();
|
||||
_rx.pop();
|
||||
return b;
|
||||
}
|
||||
|
||||
private:
|
||||
mutable std::mutex _mutex;
|
||||
std::condition_variable _cv;
|
||||
std::queue<uint8_t> _rx;
|
||||
std::vector<uint8_t> _writes;
|
||||
bool _block_writes = false;
|
||||
std::atomic<bool> _entered_block = false;
|
||||
size_t _max_write_chunk = TEST_DEFAULT_MAX_WRITE_CHUNK;
|
||||
};
|
||||
|
||||
class FakeRNG : public mesh::RNG {
|
||||
public:
|
||||
void random(uint8_t* dest, size_t sz) override {
|
||||
for (size_t i = 0; i < sz; i++) {
|
||||
dest[i] = 0;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class FakeRadio : public mesh::Radio {
|
||||
public:
|
||||
bool isReceiving() override { return false; }
|
||||
uint32_t getEstAirtimeFor(uint16_t) override { return 10; }
|
||||
bool startSendRaw(const uint8_t*, uint16_t) override {
|
||||
_start_send_count++;
|
||||
return _start_send_result;
|
||||
}
|
||||
bool isSendComplete() override { return _send_complete; }
|
||||
void onSendFinished() override { _send_finished_count++; }
|
||||
int16_t getNoiseFloor() override { return -120; }
|
||||
|
||||
void setStartSendResult(bool result) { _start_send_result = result; }
|
||||
void setSendComplete(bool complete) { _send_complete = complete; }
|
||||
int startSendCount() const { return _start_send_count; }
|
||||
int sendFinishedCount() const { return _send_finished_count; }
|
||||
|
||||
private:
|
||||
bool _start_send_result = true;
|
||||
bool _send_complete = true;
|
||||
int _start_send_count = 0;
|
||||
int _send_finished_count = 0;
|
||||
};
|
||||
|
||||
class FakeBoard : public mesh::MainBoard {
|
||||
public:
|
||||
uint16_t getBattMilliVolts() override { return 4200; }
|
||||
float getMCUTemperature() override { return 24.0f; }
|
||||
const char* getManufacturerName() override { return "test-board"; }
|
||||
void reboot() override {}
|
||||
};
|
||||
|
||||
class FakeSensors : public SensorManager {
|
||||
public:
|
||||
bool querySensors(uint8_t, CayenneLPP&) override { return false; }
|
||||
};
|
||||
|
||||
class KissModemFixture : public ::testing::Test {
|
||||
protected:
|
||||
BlockingStream serial;
|
||||
mesh::LocalIdentity identity;
|
||||
FakeRNG rng;
|
||||
FakeRadio radio;
|
||||
FakeBoard board;
|
||||
FakeSensors sensors;
|
||||
KissModem modem;
|
||||
|
||||
KissModemFixture()
|
||||
: modem(serial, identity, rng, radio, board, sensors) {
|
||||
modem.begin();
|
||||
}
|
||||
|
||||
static std::vector<uint8_t> dataFrame(const std::vector<uint8_t>& packet) {
|
||||
std::vector<uint8_t> frame = {KISS_FEND, KISS_CMD_DATA};
|
||||
frame.insert(frame.end(), packet.begin(), packet.end());
|
||||
frame.push_back(KISS_FEND);
|
||||
return frame;
|
||||
}
|
||||
|
||||
void advanceToTxSending() {
|
||||
modem.loop();
|
||||
modem.loop();
|
||||
delay((uint32_t)KISS_DEFAULT_TXDELAY * 10);
|
||||
modem.loop();
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(KissModemFixture, PingResponseShouldNotStallLoopUnderTxBackpressure) {
|
||||
serial.setBlockWrites(true);
|
||||
serial.pushRx({KISS_FEND, KISS_CMD_SETHARDWARE, HW_CMD_PING, KISS_FEND});
|
||||
|
||||
auto future = std::async(std::launch::async, [this]() {
|
||||
modem.loop();
|
||||
});
|
||||
|
||||
auto status = future.wait_for(std::chrono::milliseconds(100));
|
||||
EXPECT_EQ(status, std::future_status::ready) << "KissModem::loop blocked in serial write under TX backpressure";
|
||||
EXPECT_FALSE(serial.isWriteBlocked()) << "KissModem entered blocking write path";
|
||||
|
||||
serial.setBlockWrites(false);
|
||||
future.wait();
|
||||
modem.loop();
|
||||
EXPECT_GT(serial.writesCount(), 0U) << "KissModem did not flush queued response after backpressure cleared";
|
||||
}
|
||||
|
||||
TEST_F(KissModemFixture, PingResponseKeepsStandardKissFraming) {
|
||||
serial.pushRx({KISS_FEND, KISS_CMD_SETHARDWARE, HW_CMD_PING, KISS_FEND});
|
||||
modem.loop();
|
||||
|
||||
const std::vector<uint8_t> expected = {KISS_FEND, KISS_CMD_SETHARDWARE, HW_RESP(HW_CMD_PING), KISS_FEND};
|
||||
EXPECT_EQ(serial.writesSnapshot(), expected);
|
||||
}
|
||||
|
||||
TEST_F(KissModemFixture, PingResponseKeepsFramingWithPartialBulkWrites) {
|
||||
serial.setMaxWriteChunk(TEST_PARTIAL_WRITE_CHUNK);
|
||||
serial.pushRx({KISS_FEND, KISS_CMD_SETHARDWARE, HW_CMD_PING, KISS_FEND});
|
||||
for (int i = 0; i < TEST_PARTIAL_WRITE_FLUSH_LOOPS; i++) {
|
||||
modem.loop();
|
||||
}
|
||||
|
||||
const std::vector<uint8_t> expected = {KISS_FEND, KISS_CMD_SETHARDWARE, HW_RESP(HW_CMD_PING), KISS_FEND};
|
||||
EXPECT_EQ(serial.writesSnapshot(), expected);
|
||||
}
|
||||
|
||||
TEST_F(KissModemFixture, PacketAndMetaAreQueuedTogetherUnderBackpressure) {
|
||||
static constexpr uint8_t TEST_PACKET[] = {0x01, 0x02, 0x03};
|
||||
|
||||
serial.setBlockWrites(true);
|
||||
modem.onPacketReceived((int8_t)TEST_SNR, (int8_t)TEST_RSSI, TEST_PACKET, sizeof(TEST_PACKET));
|
||||
serial.setBlockWrites(false);
|
||||
modem.loop();
|
||||
modem.loop();
|
||||
|
||||
const std::vector<uint8_t> expected = {
|
||||
KISS_FEND, KISS_CMD_DATA, TEST_PACKET[0], TEST_PACKET[1], TEST_PACKET[2], KISS_FEND,
|
||||
KISS_FEND, KISS_CMD_SETHARDWARE, HW_RESP_RX_META, TEST_SNR, TEST_RSSI, KISS_FEND};
|
||||
EXPECT_EQ(serial.writesSnapshot(), expected);
|
||||
}
|
||||
|
||||
TEST_F(KissModemFixture, RadioTxCompletionAdvancesWhileHostOutputIsBackedUp) {
|
||||
serial.pushRx(dataFrame({0x42}));
|
||||
advanceToTxSending();
|
||||
ASSERT_EQ(radio.startSendCount(), 1);
|
||||
|
||||
serial.setBlockWrites(true);
|
||||
modem.loop();
|
||||
EXPECT_EQ(radio.sendFinishedCount(), 1);
|
||||
EXPECT_TRUE(modem.isTxBusy());
|
||||
|
||||
serial.setBlockWrites(false);
|
||||
modem.loop();
|
||||
|
||||
const std::vector<uint8_t> expected = {KISS_FEND, KISS_CMD_SETHARDWARE, HW_RESP_TX_DONE, 0x01, KISS_FEND};
|
||||
EXPECT_EQ(serial.writesSnapshot(), expected);
|
||||
EXPECT_FALSE(modem.isTxBusy());
|
||||
}
|
||||
|
||||
TEST_F(KissModemFixture, QueueFullReportsBusyWithoutDroppingQueuedFrames) {
|
||||
static constexpr uint8_t TEST_PACKET_ONE[] = {0x11, 0x12};
|
||||
static constexpr uint8_t TEST_PACKET_TWO[] = {0x21, 0x22};
|
||||
|
||||
serial.setBlockWrites(true);
|
||||
modem.onPacketReceived((int8_t)TEST_SNR, (int8_t)TEST_RSSI, TEST_PACKET_ONE, sizeof(TEST_PACKET_ONE));
|
||||
modem.onPacketReceived((int8_t)TEST_SNR, (int8_t)TEST_RSSI, TEST_PACKET_TWO, sizeof(TEST_PACKET_TWO));
|
||||
serial.setBlockWrites(false);
|
||||
modem.loop();
|
||||
|
||||
const std::vector<uint8_t> expected = {
|
||||
KISS_FEND, KISS_CMD_DATA, TEST_PACKET_ONE[0], TEST_PACKET_ONE[1], KISS_FEND,
|
||||
KISS_FEND, KISS_CMD_SETHARDWARE, HW_RESP_RX_META, TEST_SNR, TEST_RSSI, KISS_FEND,
|
||||
KISS_FEND, KISS_CMD_SETHARDWARE, HW_RESP_ERROR, HW_ERR_TX_BUSY, KISS_FEND};
|
||||
EXPECT_EQ(serial.writesSnapshot(), expected);
|
||||
}
|
||||
|
||||
TEST_F(KissModemFixture, QueuedEncoderEscapesKissSpecialBytes) {
|
||||
static constexpr uint8_t TEST_PACKET[] = {KISS_FEND, KISS_FESC, 0x01};
|
||||
|
||||
modem.onPacketReceived((int8_t)TEST_SNR, (int8_t)TEST_RSSI, TEST_PACKET, sizeof(TEST_PACKET));
|
||||
|
||||
const std::vector<uint8_t> expected = {
|
||||
KISS_FEND, KISS_CMD_DATA, KISS_FESC, KISS_TFEND, KISS_FESC, KISS_TFESC, 0x01, KISS_FEND,
|
||||
KISS_FEND, KISS_CMD_SETHARDWARE, HW_RESP_RX_META, TEST_SNR, TEST_RSSI, KISS_FEND};
|
||||
EXPECT_EQ(serial.writesSnapshot(), expected);
|
||||
}
|
||||
|
||||
TEST_F(KissModemFixture, MaxPacketWorstCaseEscapingFitsQueuedFrame) {
|
||||
std::vector<uint8_t> packet(KISS_MAX_PACKET_SIZE, KISS_FEND);
|
||||
std::vector<uint8_t> expected = {KISS_FEND, KISS_CMD_DATA};
|
||||
for (size_t i = 0; i < packet.size(); i++) {
|
||||
expected.push_back(KISS_FESC);
|
||||
expected.push_back(KISS_TFEND);
|
||||
}
|
||||
expected.push_back(KISS_FEND);
|
||||
expected.insert(expected.end(), {KISS_FEND, KISS_CMD_SETHARDWARE, HW_RESP_RX_META, TEST_SNR, TEST_RSSI, KISS_FEND});
|
||||
|
||||
modem.onPacketReceived((int8_t)TEST_SNR, (int8_t)TEST_RSSI, packet.data(), packet.size());
|
||||
EXPECT_EQ(serial.writesSnapshot(), expected);
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include "helpers/SimpleMeshTables.h"
|
||||
|
||||
using namespace mesh;
|
||||
|
||||
// Build a packet that calculatePacketHash() distinguishes by payload content.
|
||||
// header selects ROUTE_TYPE_FLOOD so isRouteDirect() returns false.
|
||||
static Packet makeFloodPacket(uint8_t seed) {
|
||||
Packet p;
|
||||
p.header = ROUTE_TYPE_FLOOD | (PAYLOAD_TYPE_ACK << PH_TYPE_SHIFT);
|
||||
p.payload[0] = seed;
|
||||
p.payload_len = 1;
|
||||
p.path_len = 0;
|
||||
return p;
|
||||
}
|
||||
|
||||
static Packet makeDirectPacket(uint8_t seed) {
|
||||
Packet p;
|
||||
p.header = ROUTE_TYPE_DIRECT | (PAYLOAD_TYPE_ACK << PH_TYPE_SHIFT);
|
||||
p.payload[0] = seed;
|
||||
p.payload_len = 1;
|
||||
p.path_len = 0;
|
||||
return p;
|
||||
}
|
||||
|
||||
// ── wasSeen: pure query ───────────────────────────────────────────────────────
|
||||
|
||||
TEST(SimpleMeshTables, WasSeen_ReturnsFalseForUnseen) {
|
||||
SimpleMeshTables t;
|
||||
Packet p = makeFloodPacket(0x01);
|
||||
EXPECT_FALSE(t.wasSeen(&p));
|
||||
}
|
||||
|
||||
// wasSeen shouldn't change state
|
||||
TEST(SimpleMeshTables, WasSeen_IsPureQuery_DoesNotInsert) {
|
||||
SimpleMeshTables t;
|
||||
Packet p = makeFloodPacket(0x01);
|
||||
EXPECT_FALSE(t.wasSeen(&p));
|
||||
EXPECT_FALSE(t.wasSeen(&p));
|
||||
}
|
||||
|
||||
// ── markSeen + wasSeen ───────────────────────────────────────────────────────
|
||||
|
||||
TEST(SimpleMeshTables, MarkSeen_MakesWasSeenReturnTrue) {
|
||||
SimpleMeshTables t;
|
||||
Packet p = makeFloodPacket(0x01);
|
||||
t.markSeen(&p);
|
||||
EXPECT_TRUE(t.wasSeen(&p));
|
||||
}
|
||||
|
||||
TEST(SimpleMeshTables, MarkSeen_DoesNotAffectOtherPackets) {
|
||||
SimpleMeshTables t;
|
||||
Packet p1 = makeFloodPacket(0x01);
|
||||
Packet p2 = makeFloodPacket(0x02);
|
||||
t.markSeen(&p1);
|
||||
EXPECT_FALSE(t.wasSeen(&p2));
|
||||
}
|
||||
|
||||
// Canonical pattern used at every onRecvPacket call site:
|
||||
// if (!wasSeen(pkt)) { markSeen(pkt); process(pkt); }
|
||||
TEST(SimpleMeshTables, QueryThenMark_WorksCorrectly) {
|
||||
SimpleMeshTables t;
|
||||
Packet p = makeFloodPacket(0x01);
|
||||
EXPECT_FALSE(t.wasSeen(&p));
|
||||
t.markSeen(&p);
|
||||
EXPECT_TRUE(t.wasSeen(&p));
|
||||
}
|
||||
|
||||
// ── dup stats ────────────────────────────────────────────────────────────────
|
||||
|
||||
TEST(SimpleMeshTables, WasSeen_IncrementsFloodDupStat) {
|
||||
SimpleMeshTables t;
|
||||
Packet p = makeFloodPacket(0x01);
|
||||
t.markSeen(&p);
|
||||
t.wasSeen(&p);
|
||||
EXPECT_EQ(1u, t.getNumFloodDups());
|
||||
EXPECT_EQ(0u, t.getNumDirectDups());
|
||||
}
|
||||
|
||||
TEST(SimpleMeshTables, WasSeen_IncrementsDirectDupStat) {
|
||||
SimpleMeshTables t;
|
||||
Packet p = makeDirectPacket(0x01);
|
||||
t.markSeen(&p);
|
||||
t.wasSeen(&p);
|
||||
EXPECT_EQ(0u, t.getNumFloodDups());
|
||||
EXPECT_EQ(1u, t.getNumDirectDups());
|
||||
}
|
||||
|
||||
// ── clear ────────────────────────────────────────────────────────────────────
|
||||
|
||||
TEST(SimpleMeshTables, Clear_RemovesSeenPacket) {
|
||||
SimpleMeshTables t;
|
||||
Packet p = makeFloodPacket(0x01);
|
||||
t.markSeen(&p);
|
||||
ASSERT_TRUE(t.wasSeen(&p));
|
||||
t.clear(&p);
|
||||
EXPECT_FALSE(t.wasSeen(&p));
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <helpers/UTF8Helpers.h>
|
||||
|
||||
TEST(UTF8Helpers, KeepsCompleteNameWithinLimit) {
|
||||
const char* name = "Example RPT 🔋🇵🇱";
|
||||
|
||||
EXPECT_EQ(24u, mesh::validUtf8PrefixLength(name, 24));
|
||||
}
|
||||
|
||||
TEST(UTF8Helpers, StopsBeforeCodePointCrossingLimit) {
|
||||
const char* name = "Example RPT 🔋🇵🇱";
|
||||
|
||||
EXPECT_EQ(20u, mesh::validUtf8PrefixLength(name, 23));
|
||||
}
|
||||
|
||||
TEST(UTF8Helpers, RejectsMalformedAndTruncatedSequences) {
|
||||
const char overlong[] = {'A', static_cast<char>(0xC0), static_cast<char>(0xAF), 0};
|
||||
const char surrogate[] = {'A', static_cast<char>(0xED), static_cast<char>(0xA0), static_cast<char>(0x80), 0};
|
||||
const char out_of_range[] = {'A', static_cast<char>(0xF4), static_cast<char>(0x90), static_cast<char>(0x80), static_cast<char>(0x80), 0};
|
||||
const char truncated[] = {'A', static_cast<char>(0xF0), static_cast<char>(0x9F), 0};
|
||||
|
||||
EXPECT_EQ(1u, mesh::validUtf8PrefixLength(overlong, sizeof(overlong)));
|
||||
EXPECT_EQ(1u, mesh::validUtf8PrefixLength(surrogate, sizeof(surrogate)));
|
||||
EXPECT_EQ(1u, mesh::validUtf8PrefixLength(out_of_range, sizeof(out_of_range)));
|
||||
EXPECT_EQ(1u, mesh::validUtf8PrefixLength(truncated, sizeof(truncated)));
|
||||
}
|
||||
|
||||
TEST(UTF8Helpers, RejectsUnexpectedContinuationByte) {
|
||||
const char invalid[] = {'A', static_cast<char>(0x80), 'B', 0};
|
||||
|
||||
EXPECT_EQ(1u, mesh::validUtf8PrefixLength(invalid, sizeof(invalid)));
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
Reference in New Issue
Block a user