Merge upstream dev into keymindCascade

This commit is contained in:
mikecarper committed 2026-07-24 01:23:20 -07:00
commit 566ef08f25
44 files changed
+1082 -214

No files matched your search

@@ -25,5 +25,14 @@ runs:
- name: Extract Version from Git Tag
shell: bash
run: |
GIT_TAG_NAME="${GITHUB_REF#refs/tags/}"
echo "GIT_TAG_VERSION=${GIT_TAG_NAME##*-}" >> $GITHUB_ENV
if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
# triggered by a tag push (e.g: refs/tags/companion-v1.2.3)
GIT_TAG_NAME="${GITHUB_REF#refs/tags/}"
VERSION_STRING="${GIT_TAG_NAME##*-}"
else
# triggered by a workflow dispatch (e.g: refs/heads/main)
# strip "refs/heads/" prefix and replace any remaining "/" with "-" to protect file paths
BRANCH_NAME="${GITHUB_REF#refs/heads/}"
VERSION_STRING=$(echo "$BRANCH_NAME" | tr '/' '-')
fi
echo "GIT_TAG_VERSION=${VERSION_STRING}" >> $GITHUB_ENV
@@ -10,33 +10,8 @@ on:
- 'companion-*'
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Clone Repo
uses: actions/checkout@v6
- name: Setup Build Environment
uses: ./.github/actions/setup-build-environment
- name: Build Firmwares
env:
FIRMWARE_VERSION: ${{ env.GIT_TAG_VERSION }}
run: /usr/bin/env bash build.sh build-companion-firmwares
- name: Upload Workflow Artifacts
uses: actions/upload-artifact@v7
with:
name: companion-firmwares
path: out
- name: Create Release
uses: softprops/action-gh-release@v3
if: startsWith(github.ref, 'refs/tags/')
with:
name: Companion Firmware ${{ env.GIT_TAG_VERSION }}
body: ""
draft: true
files: out/*
build-companion-firmwares:
uses: ./.github/workflows/firmware-builder.yml
with:
firmware_type: 'companion'
release_title_prefix: 'Companion Firmware'
+5 -30
View File
@@ -10,33 +10,8 @@ on:
- 'repeater-*'
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Clone Repo
uses: actions/checkout@v6
- name: Setup Build Environment
uses: ./.github/actions/setup-build-environment
- name: Build Firmwares
env:
FIRMWARE_VERSION: ${{ env.GIT_TAG_VERSION }}
run: /usr/bin/env bash build.sh build-repeater-firmwares
- name: Upload Workflow Artifacts
uses: actions/upload-artifact@v7
with:
name: repeater-firmwares
path: out
- name: Create Release
uses: softprops/action-gh-release@v3
if: startsWith(github.ref, 'refs/tags/')
with:
name: Repeater Firmware ${{ env.GIT_TAG_VERSION }}
body: ""
draft: true
files: out/*
build-repeater-firmwares:
uses: ./.github/workflows/firmware-builder.yml
with:
firmware_type: 'repeater'
release_title_prefix: 'Repeater Firmware'
@@ -10,33 +10,8 @@ on:
- 'room-server-*'
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Clone Repo
uses: actions/checkout@v6
- name: Setup Build Environment
uses: ./.github/actions/setup-build-environment
- name: Build Firmwares
env:
FIRMWARE_VERSION: ${{ env.GIT_TAG_VERSION }}
run: /usr/bin/env bash build.sh build-room-server-firmwares
- name: Upload Workflow Artifacts
uses: actions/upload-artifact@v7
with:
name: room-server-firmwares
path: out
- name: Create Release
uses: softprops/action-gh-release@v3
if: startsWith(github.ref, 'refs/tags/')
with:
name: Room Server Firmware ${{ env.GIT_TAG_VERSION }}
body: ""
draft: true
files: out/*
build-room-server-firmwares:
uses: ./.github/workflows/firmware-builder.yml
with:
firmware_type: 'room-server'
release_title_prefix: 'Room Server Firmware'
+90
View File
@@ -0,0 +1,90 @@
name: Firmware Builder
on:
workflow_call:
inputs:
firmware_type:
required: true
type: string
release_title_prefix:
required: true
type: string
jobs:
generate-build-matrix:
runs-on: ubuntu-latest
outputs:
targets: ${{ steps.get-build-targets.outputs.targets }}
steps:
- name: Clone Repo
uses: actions/checkout@v6
- name: Setup Build Environment
uses: ./.github/actions/setup-build-environment
- name: Get Build Targets
id: get-build-targets
run: |
# get list of firmwares to build
TARGET_LIST=$(/usr/bin/env bash build.sh get-${{ inputs.firmware_type }}-firmwares-to-build)
# convert targets separated by new line into a json array string
JSON_ARRAY=$(echo "$TARGET_LIST" | jq -R -s -c 'split("\n") | map(select(length > 0))')
# use json array as targets result
echo "targets=$JSON_ARRAY" >> $GITHUB_OUTPUT
build:
needs: generate-build-matrix
runs-on: ubuntu-latest
continue-on-error: true # don't fail entire build if one board fails to build
strategy:
matrix:
target: ${{ fromJson(needs.generate-build-matrix.outputs.targets) }}
fail-fast: false # don't cancel other builds if one board fails to build
steps:
- name: Clone Repo
uses: actions/checkout@v6
- name: Setup Build Environment
uses: ./.github/actions/setup-build-environment
- name: Build Firmware
env:
FIRMWARE_VERSION: ${{ env.GIT_TAG_VERSION }}
run: /usr/bin/env bash build.sh build-firmware ${{ matrix.target }}
- name: Upload Workflow Artifacts
uses: actions/upload-artifact@v7
with:
name: "${{ matrix.target }}"
path: out
create-release:
needs: build
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/') # only create release for tagged builds
steps:
- name: Clone Repo
uses: actions/checkout@v6
- name: Setup Build Environment
uses: ./.github/actions/setup-build-environment
- name: Download All Artifacts
uses: actions/download-artifact@v8
with:
merge-multiple: true
path: out
- name: Create Release
uses: softprops/action-gh-release@v3
with:
name: "${{ inputs.release_title_prefix }} ${{ env.GIT_TAG_VERSION }}"
body: ""
draft: true
files: out/*
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
uses: ./.github/actions/setup-build-environment
- name: Run Unit Tests
run: pio test -e native -vv
run: pio test -e native -e native_kiss_modem -vv
- name: Upload Test Results
# Upload test results even if the test step failed.
+2 -2
View File
@@ -9,7 +9,7 @@
"extra_flags": [
"-DBOARD_HAS_PSRAM",
"-DARDUINO_USB_CDC_ON_BOOT=1",
"-DARDUINO_USB_MODE=0",
"-DARDUINO_USB_MODE=1",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1"
],
@@ -40,4 +40,4 @@
},
"url": "https://heltec.org/",
"vendor": "heltec"
}
}
+10 -4
View File
@@ -41,7 +41,7 @@ Maximum unescaped frame size: 512 bytes.
| Command | Value | Data | Description |
|-------------|--------|--------------------|-------------------------------------------------------------|
| Data | `0x00` | Raw packet | Queue packet for transmission |
| Data | `0x00` | Raw packet | Queue packet for transmission (one pending at a time) |
| TXDELAY | `0x01` | Delay (1 byte) | Transmitter keyup delay in 10ms units (default: 50 = 500ms) |
| Persistence | `0x02` | P (1 byte) | CSMA persistence parameter 0-255 (default: 63) |
| SlotTime | `0x03` | Interval (1 byte) | CSMA slot interval in 10ms units (default: 10 = 100ms) |
@@ -58,6 +58,12 @@ Maximum unescaped frame size: 512 bytes.
Data frames carry raw packet data only, with no metadata prepended. The Data command payload is limited to 255 bytes to match the MeshCore maximum transmission unit (MAX_TRANS_UNIT); frames larger than 255 bytes are silently dropped. The KISS specification recommends at least 1024 bytes for general-purpose TNCs; this modem is intended for MeshCore packets only, whose protocol MTU is 255 bytes.
Only one packet may be pending for radio transmission at a time. If the host sends a second Data frame before the first has completed, the modem responds with Error (0xF1) and TxBusy (0x07).
### Host Output Backpressure
Outbound frames are encoded into a 2-slot queue and flushed when serial output space is available; `loop()` never blocks on writes. Radio TX state advances independently of host read speed. TxDone is retained until it can be queued. If the outbound queue is full, the modem responds with Error (0xF1) and TxBusy (0x07). Hosts should read serial promptly to avoid delayed responses.
### CSMA Behavior
The TNC implements p-persistent CSMA for half-duplex operation:
@@ -156,15 +162,15 @@ Response codes use the high-bit convention: `response = command | 0x80`. Generic
| MacFailed | `0x04` | MAC verification failed |
| UnknownCmd | `0x05` | Unknown sub-command |
| EncryptFailed | `0x06` | Encryption failed |
| TxBusy | `0x07` | Transmit busy |
| TxBusy | `0x07` | Radio TX busy, or host output queue full |
### Unsolicited Events
The TNC sends these SetHardware frames without a preceding request:
**TxDone (0xF8)**: Sent after a packet has been transmitted. Contains a single byte: 0x01 for success, 0x00 for failure.
**TxDone (0xF8)**: Sent after radio transmission completes. Contains a single byte: 0x01 for success, 0x00 for failure. Delivery to the host may be delayed under serial backpressure but is not dropped.
**RxMeta (0xF9)**: Sent immediately after each standard data frame (type 0x00) with metadata for the received packet. Contains SNR (1 byte, signed, value x4 for 0.25 dB precision) followed by RSSI (1 byte, signed, dBm). Enabled by default; can be toggled with SetSignalReport. Standard KISS clients ignore this frame.
**RxMeta (0xF9)**: Sent after each standard data frame (type 0x00) with SNR (1 byte, signed, value x4) and RSSI (1 byte, signed, dBm). Queued with the data frame; omitted if the data frame cannot be queued. Enabled by default; toggle with SetSignalReport. Standard KISS clients ignore this frame.
## Data Formats
+3 -1
View File
@@ -12,8 +12,10 @@ meshcore://channel/add?name=Public&secret=8b3387e9c5cdea6ac9e5edbaa115cd72
**Parameters**:
- `name`: Channel name (URL-encoded if needed)
- `name`: Channel name (URL-encoded)
- `secret`: 16-byte secret represented as 32 hex characters
- `region_scope`: Region Scope (optional, URL-encoded if provided)
- Supported by MeshCore App v1.47.0+
## Add Contact
+166 -33
View File
@@ -22,6 +22,7 @@ KissModem::KissModem(Stream& serial, mesh::LocalIdentity& identity, mesh::RNG& r
_getStatsCallback = nullptr;
_config = {0, 0, 0, 0, 0};
_signal_report_enabled = true;
resetOutputQueue();
}
void KissModem::begin() {
@@ -30,37 +31,168 @@ void KissModem::begin() {
_rx_active = false;
_has_pending_tx = false;
_tx_state = TX_IDLE;
resetOutputQueue();
}
void KissModem::writeByte(uint8_t b) {
if (b == KISS_FEND) {
_serial.write(KISS_FESC);
_serial.write(KISS_TFEND);
} else if (b == KISS_FESC) {
_serial.write(KISS_FESC);
_serial.write(KISS_TFESC);
} else {
_serial.write(b);
void KissModem::resetOutputQueue() {
_tx_frame_head = 0;
_tx_frame_tail = 0;
_tx_frame_count = 0;
_tx_busy_error_pending = false;
_tx_done_pending = false;
_tx_done_result = 0;
}
void KissModem::popTxFrame() {
_tx_frame_head = (uint8_t)((_tx_frame_head + 1) % KISS_TX_FRAME_QUEUE_DEPTH);
_tx_frame_count--;
}
uint16_t KissModem::appendEscapedByte(uint8_t* dest, uint16_t idx, uint16_t max_len, uint8_t b) {
if (b == KISS_FEND || b == KISS_FESC) {
if (idx + 2 > max_len) {
return 0;
}
dest[idx++] = KISS_FESC;
dest[idx++] = (b == KISS_FEND) ? KISS_TFEND : KISS_TFESC;
return idx;
}
if (idx + 1 > max_len) {
return 0;
}
dest[idx++] = b;
return idx;
}
void KissModem::writeFrame(uint8_t type, const uint8_t* data, uint16_t len) {
_serial.write(KISS_FEND);
writeByte(type);
uint16_t KissModem::encodeFrame(uint8_t type, const uint8_t* data, uint16_t len, uint8_t* dest, uint16_t max_len) {
if (max_len < KISS_FRAME_BOUNDARY_BYTES) {
return 0;
}
uint16_t idx = 0;
dest[idx++] = KISS_FEND;
idx = appendEscapedByte(dest, idx, max_len, type);
if (idx == 0) {
return 0;
}
for (uint16_t i = 0; i < len; i++) {
writeByte(data[i]);
idx = appendEscapedByte(dest, idx, max_len, data[i]);
if (idx == 0) {
return 0;
}
}
_serial.write(KISS_FEND);
if (idx + 1 > max_len) {
return 0;
}
dest[idx++] = KISS_FEND;
return idx;
}
void KissModem::writeHardwareFrame(uint8_t sub_cmd, const uint8_t* data, uint16_t len) {
_serial.write(KISS_FEND);
writeByte(KISS_CMD_SETHARDWARE);
writeByte(sub_cmd);
for (uint16_t i = 0; i < len; i++) {
writeByte(data[i]);
bool KissModem::tryFlushFrames() {
while (_tx_frame_count > 0) {
const uint8_t idx = _tx_frame_head;
const uint16_t frame_len = _tx_frame_len[idx];
uint16_t written_len = _tx_frame_written[idx];
if (written_len >= frame_len) {
popTxFrame();
continue;
}
const int available = _serial.availableForWrite();
if (available <= 0) {
return false;
}
const uint16_t remaining = frame_len - written_len;
const uint16_t chunk_len = (available < (int)remaining) ? (uint16_t)available : remaining;
if (chunk_len == 0) {
return false;
}
size_t chunk_written = _serial.write(_tx_frame_buf[idx] + written_len, chunk_len);
if (chunk_written == 0) {
return false;
}
written_len += (uint16_t)chunk_written;
_tx_frame_written[idx] = written_len;
if (written_len < frame_len) {
return false;
}
popTxFrame();
}
_serial.write(KISS_FEND);
return true;
}
bool KissModem::queueFrame(uint8_t type, const uint8_t* data, uint16_t len, bool mark_busy_error) {
if (_tx_frame_count >= KISS_TX_FRAME_QUEUE_DEPTH && !tryFlushFrames()) {
if (mark_busy_error) {
_tx_busy_error_pending = true;
}
return false;
}
const uint8_t idx = _tx_frame_tail;
uint16_t frame_len = encodeFrame(type, data, len, _tx_frame_buf[idx], sizeof(_tx_frame_buf[idx]));
if (frame_len == 0) {
return false;
}
_tx_frame_len[idx] = frame_len;
_tx_frame_written[idx] = 0;
_tx_frame_tail = (uint8_t)((_tx_frame_tail + 1) % KISS_TX_FRAME_QUEUE_DEPTH);
_tx_frame_count++;
tryFlushFrames();
return true;
}
bool KissModem::queuePendingBusyError() {
if (!_tx_busy_error_pending) {
return true;
}
const uint8_t err = HW_ERR_TX_BUSY;
if (!queueHardwareFrame(HW_RESP_ERROR, &err, 1, false)) {
return false;
}
_tx_busy_error_pending = false;
return true;
}
bool KissModem::queueHardwareFrame(uint8_t sub_cmd, const uint8_t* data, uint16_t len, bool mark_busy_error) {
if (len > KISS_MAX_FRAME_SIZE) {
return false;
}
_tx_hw_payload[0] = sub_cmd;
if (len > 0) {
memcpy(_tx_hw_payload + 1, data, len);
}
return queueFrame(KISS_CMD_SETHARDWARE, _tx_hw_payload, len + 1, mark_busy_error);
}
bool KissModem::queuePendingTxDone() {
if (!_tx_done_pending) {
return true;
}
if (!queueHardwareFrame(HW_RESP_TX_DONE, &_tx_done_result, 1, false)) {
return false;
}
_tx_done_pending = false;
return true;
}
void KissModem::setTxDonePending(uint8_t result) {
_tx_done_result = result;
_tx_done_pending = true;
_tx_state = TX_DONE_PENDING;
}
bool KissModem::writeHardwareFrame(uint8_t sub_cmd, const uint8_t* data, uint16_t len) {
return queueHardwareFrame(sub_cmd, data, len, true);
}
void KissModem::writeHardwareError(uint8_t error_code) {
@@ -68,6 +200,8 @@ void KissModem::writeHardwareError(uint8_t error_code) {
}
void KissModem::loop() {
tryFlushFrames();
while (_serial.available()) {
uint8_t b = _serial.read();
@@ -106,6 +240,8 @@ void KissModem::loop() {
}
processTx();
tryFlushFrames();
queuePendingBusyError();
}
void KissModem::processFrame() {
@@ -295,10 +431,7 @@ void KissModem::processTx() {
_tx_timer = millis();
_tx_state = TX_SENDING;
} else {
uint8_t result = 0x00;
writeHardwareFrame(HW_RESP_TX_DONE, &result, 1);
_has_pending_tx = false;
_tx_state = TX_IDLE;
setTxDonePending(0x00);
}
}
break;
@@ -306,14 +439,15 @@ void KissModem::processTx() {
case TX_SENDING:
if (_radio.isSendComplete()) {
_radio.onSendFinished();
uint8_t result = 0x01;
writeHardwareFrame(HW_RESP_TX_DONE, &result, 1);
_has_pending_tx = false;
_tx_state = TX_IDLE;
setTxDonePending(0x01);
} else if (millis() - _tx_timer >= _radio.getEstAirtimeFor(_pending_tx_len) * KISS_TX_TIMEOUT_FACTOR) {
_radio.onSendFinished();
uint8_t result = 0x00;
writeHardwareFrame(HW_RESP_TX_DONE, &result, 1);
setTxDonePending(0x00);
}
break;
case TX_DONE_PENDING:
if (queuePendingTxDone()) {
_has_pending_tx = false;
_tx_state = TX_IDLE;
}
@@ -322,8 +456,7 @@ void KissModem::processTx() {
}
void KissModem::onPacketReceived(int8_t snr, int8_t rssi, const uint8_t* packet, uint16_t len) {
writeFrame(KISS_CMD_DATA, packet, len);
if (_signal_report_enabled) {
if (queueFrame(KISS_CMD_DATA, packet, len) && _signal_report_enabled) {
uint8_t meta[2] = { (uint8_t)snr, (uint8_t)rssi };
writeHardwareFrame(HW_RESP_RX_META, meta, 2);
}
+32 -4
View File
@@ -13,6 +13,14 @@
#define KISS_MAX_FRAME_SIZE 512
#define KISS_MAX_PACKET_SIZE 255
#define KISS_FRAME_BOUNDARY_BYTES 2
#define KISS_TYPE_BYTES 1
#define KISS_HW_SUBCMD_BYTES 1
#define KISS_MAX_ESCAPABLE_BYTES (KISS_MAX_FRAME_SIZE + KISS_TYPE_BYTES + KISS_HW_SUBCMD_BYTES)
#define KISS_MAX_ESCAPED_PAYLOAD_SIZE (2 * KISS_MAX_ESCAPABLE_BYTES)
#define KISS_MAX_ENCODED_FRAME_SIZE (KISS_FRAME_BOUNDARY_BYTES + KISS_MAX_ESCAPED_PAYLOAD_SIZE)
#define KISS_TX_FRAME_QUEUE_DEPTH 2
#define KISS_HW_MAX_PAYLOAD_SIZE (KISS_MAX_FRAME_SIZE + KISS_HW_SUBCMD_BYTES)
#define KISS_CMD_DATA 0x00
#define KISS_CMD_TXDELAY 0x01
@@ -94,7 +102,8 @@ enum TxState {
TX_WAIT_CLEAR,
TX_SLOT_WAIT,
TX_DELAY,
TX_SENDING
TX_SENDING,
TX_DONE_PENDING
};
class KissModem {
@@ -130,10 +139,28 @@ class KissModem {
RadioConfig _config;
bool _signal_report_enabled;
uint8_t _tx_frame_buf[KISS_TX_FRAME_QUEUE_DEPTH][KISS_MAX_ENCODED_FRAME_SIZE];
uint16_t _tx_frame_len[KISS_TX_FRAME_QUEUE_DEPTH];
uint16_t _tx_frame_written[KISS_TX_FRAME_QUEUE_DEPTH];
uint8_t _tx_frame_head;
uint8_t _tx_frame_tail;
uint8_t _tx_frame_count;
bool _tx_busy_error_pending;
bool _tx_done_pending;
uint8_t _tx_done_result;
uint8_t _tx_hw_payload[KISS_HW_MAX_PAYLOAD_SIZE];
void writeByte(uint8_t b);
void writeFrame(uint8_t type, const uint8_t* data, uint16_t len);
void writeHardwareFrame(uint8_t sub_cmd, const uint8_t* data, uint16_t len);
static uint16_t appendEscapedByte(uint8_t* dest, uint16_t idx, uint16_t max_len, uint8_t b);
static uint16_t encodeFrame(uint8_t type, const uint8_t* data, uint16_t len, uint8_t* dest, uint16_t max_len);
void resetOutputQueue();
void popTxFrame();
bool tryFlushFrames();
bool queueFrame(uint8_t type, const uint8_t* data, uint16_t len, bool mark_busy_error = true);
bool queuePendingBusyError();
bool queueHardwareFrame(uint8_t sub_cmd, const uint8_t* data, uint16_t len, bool mark_busy_error);
bool queuePendingTxDone();
void setTxDonePending(uint8_t result);
bool writeHardwareFrame(uint8_t sub_cmd, const uint8_t* data, uint16_t len);
void writeHardwareError(uint8_t error_code);
void processFrame();
void handleHardwareCommand(uint8_t sub_cmd, const uint8_t* data, uint16_t len);
@@ -182,4 +209,5 @@ public:
bool isTxBusy() const { return _tx_state != TX_IDLE; }
/** True only when radio is actually transmitting; use to skip recvRaw in main loop. */
bool isActuallyTransmitting() const { return _tx_state == TX_SENDING; }
bool isHostOutputBackedUp() const { return _tx_frame_count > 0 || _tx_busy_error_pending || _tx_done_pending; }
};
+7 -1
View File
@@ -20,6 +20,8 @@
#define NOISE_FLOOR_CALIB_INTERVAL_MS 2000
#define AGC_RESET_INTERVAL_MS 30000
#define USB_TX_TIMEOUT_MS 50
#define USB_TX_BUFFER_SIZE 1024
StdRNG rng;
mesh::LocalIdentity identity;
@@ -111,6 +113,10 @@ void setup() {
uint32_t start = millis();
while (!Serial && millis() - start < 3000) delay(10);
delay(100);
#if defined(ESP32) && ARDUINO_USB_MODE
Serial.setTxTimeoutMs(USB_TX_TIMEOUT_MS);
Serial.setTxBufferSize(USB_TX_BUFFER_SIZE);
#endif
modem = new KissModem(Serial, identity, rng, radio_driver, board, sensors);
#endif
@@ -129,7 +135,7 @@ void loop() {
#endif
modem->loop();
if (!modem->isActuallyTransmitting()) {
if (!modem->isActuallyTransmitting() && !modem->isHostOutputBackedUp()) {
if (!modem->isTxBusy()) {
if ((uint32_t)(millis() - next_agc_reset_ms) >= AGC_RESET_INTERVAL_MS) {
radio_driver.resetAGC();
+14
View File
@@ -774,6 +774,14 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
_prefs.gps_enabled = 0;
_prefs.gps_interval = 0;
_prefs.advert_loc_policy = ADVERT_LOC_PREFS;
#if defined(USE_SX1262) || defined(USE_SX1268)
#ifdef SX126X_RX_BOOSTED_GAIN
_prefs.rx_boosted_gain = SX126X_RX_BOOSTED_GAIN;
#else
_prefs.rx_boosted_gain = 1; // enabled by default;
#endif
#endif
_prefs.radio_fem_rxgain = 1;
// Observer defaults (alert.*, etc.) moved to applyMQTTDefaults() - they live
@@ -829,6 +837,7 @@ void MyMesh::begin(FILESYSTEM *fs) {
saved_radio_apply_pending = !applySavedRadioParams();
if (!saved_radio_apply_pending) {
radio_driver.setTxPower(_prefs.tx_power_dbm);
radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain);
}
const bool fem_gain_changed = board.canControlLoRaFemLna()
&& board.isLoRaFemLnaEnabled() != (_prefs.radio_fem_rxgain != 0);
@@ -1043,6 +1052,9 @@ void MyMesh::getRxPsWatchdogCounts(uint32_t* soft, uint32_t* hard) {
*hard = radio_driver.getRxPsWatchdogHardCount();
}
bool MyMesh::setRxBoostedGain(bool enable) {
return radio_driver.setRxBoostedGainMode(enable);
}
void MyMesh::saveIdentity(const mesh::LocalIdentity &new_id) {
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
@@ -1480,6 +1492,7 @@ void MyMesh::loop() {
if (applySavedRadioParams()) {
if (saved_radio_apply_pending) {
radio_driver.setTxPower(_prefs.tx_power_dbm);
radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain);
}
set_radio_at = revert_radio_at = 0;
temp_radio_applied = false;
@@ -1524,6 +1537,7 @@ void MyMesh::loop() {
&& radio_apply_ready && !radio_apply_failed) {
if (applySavedRadioParams()) {
radio_driver.setTxPower(_prefs.tx_power_dbm);
radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain);
saved_radio_apply_pending = false;
radio_apply_retry_at = 0;
radio_apply_failures = 0;
+1
View File
@@ -267,6 +267,7 @@ public:
bool setRxPowerSaving(bool enable, uint32_t rx_us, uint32_t sleep_us) override;
void recalibrateNoiseFloor() override { _radio->recalibrateNoiseFloor(); }
void getRxPsWatchdogCounts(uint32_t* soft, uint32_t* hard) override;
bool setRxBoostedGain(bool enable) override;
void formatNeighborsReply(char *reply) override {
strcpy(reply, "not supported");
+16
View File
@@ -244,6 +244,7 @@ build_flags = -std=c++17
-I src
-I test/mocks
test_build_src = yes
test_filter = test_utils
build_src_filter =
-<*>
+<../src/Utils.cpp>
@@ -260,3 +261,18 @@ build_src_filter =
+<../src/helpers/ota/detools/detools.c>
lib_deps =
google/googletest @ 1.17.0
[env:native_kiss_modem]
platform = native
test_framework = googletest
build_flags = -std=c++17
-I test/mocks
-I src
-I examples/kiss_modem
test_build_src = yes
test_filter = test_kiss_modem
build_src_filter =
-<*>
+<../examples/kiss_modem/KissModem.cpp>
lib_deps =
google/googletest @ 1.17.0
+27 -3
View File
@@ -439,14 +439,38 @@ float NRF52Board::getMCUTemperature() {
void NRF52Board::shutdownPeripherals() {
// Power off the display if any
#ifdef DISPLAY_CLASS
display.turnOff();
if (display.isOn()) {
display.turnOff();
}
#endif
// Prep LoRa radio for power down
#ifdef P_LORA_RESET
digitalWrite(P_LORA_RESET, HIGH); // preload OUT latch so pinMode can't glitch NRESET low
pinMode(P_LORA_RESET, OUTPUT);
digitalWrite(P_LORA_RESET, LOW); // deliberate hardware reset (datasheet: >=100us)
delayMicroseconds(200);
digitalWrite(P_LORA_RESET, HIGH);
#endif
#if defined(P_LORA_SCLK) && defined(P_LORA_MISO) && defined(P_LORA_MOSI)
SPI.setPins(P_LORA_MISO, P_LORA_SCLK, P_LORA_MOSI);
SPI.begin(); // SPI may not be started on some shutdown paths, need it to shut down radio
#endif
#ifdef P_LORA_BUSY
pinMode(P_LORA_BUSY, INPUT);
uint32_t started_at = millis();
while (digitalRead(P_LORA_BUSY) && millis() - started_at < 10) {} //wait for radio to be ready
#endif
#ifdef P_LORA_NSS
pinMode(P_LORA_NSS, OUTPUT);
digitalWrite(P_LORA_NSS, HIGH);
#endif
// Power off LoRa
radio_driver.powerOff();
// Keep LoRa inactive during deepsleep
digitalWrite(P_LORA_NSS, HIGH);
#ifdef P_LORA_NSS
digitalWrite(P_LORA_NSS, HIGH);
#endif
// Power off GPS if any
if(sensors.getLocationProvider() != NULL) {
+18 -19
View File
@@ -136,8 +136,6 @@ void SerialBLEInterface::onDisconnect(BLEServer* pServer) {
if (pTxDescriptor != NULL) pTxDescriptor->setNotifications(false);
if (_isEnabled) {
adv_restart_time = millis() + ADVERT_RESTART_DELAY;
// loop() will detect this on next loop, and set deviceConnected to false
}
}
@@ -149,12 +147,14 @@ void SerialBLEInterface::onWrite(BLECharacteristic* pCharacteristic, esp_ble_gat
if (len > MAX_FRAME_SIZE) {
BLE_DEBUG_PRINTLN("ERROR: onWrite(), frame too big, len=%d", len);
} else if (recv_queue_len >= FRAME_QUEUE_SIZE) {
BLE_DEBUG_PRINTLN("ERROR: onWrite(), recv_queue is full!");
} else {
recv_queue[recv_queue_len].len = len;
memcpy(recv_queue[recv_queue_len].buf, rxValue, len);
recv_queue_len++;
Frame frame = {};
frame.len = len;
memcpy(frame.buf, rxValue, len);
if (xQueueSend(recv_queue, &frame, 0) != pdTRUE) {
BLE_DEBUG_PRINTLN("ERROR: onWrite(), recv_queue is full!");
}
}
}
@@ -169,6 +169,11 @@ void SerialBLEInterface::onStatus(BLECharacteristic* pCharacteristic, Status sta
// ---------- public methods
void SerialBLEInterface::clearBuffers() {
xQueueReset(recv_queue);
send_queue_len = 0;
}
void SerialBLEInterface::enable() {
if (_isEnabled) return;
@@ -223,7 +228,7 @@ size_t SerialBLEInterface::writeFrame(const uint8_t src[], size_t len) {
#define BLE_WRITE_MIN_INTERVAL 60
bool SerialBLEInterface::isReadBusy() const {
return (recv_queue_len > 0);
return uxQueueMessagesWaiting(recv_queue) > 0;
}
bool SerialBLEInterface::isWriteBusy() const {
@@ -258,17 +263,11 @@ size_t SerialBLEInterface::checkRecvFrame(uint8_t dest[]) {
}
}
if (deviceConnected && recv_queue_len > 0) { // check recv queue after authentication
size_t len = recv_queue[0].len; // take from top of queue
memcpy(dest, recv_queue[0].buf, len);
BLE_DEBUG_PRINTLN("readBytes: sz=%d, hdr=%d", len, (uint32_t) dest[0]);
recv_queue_len--;
for (int i = 0; i < recv_queue_len; i++) { // delete top item from queue
recv_queue[i] = recv_queue[i + 1];
}
return len;
Frame frame;
if (deviceConnected && xQueueReceive(recv_queue, &frame, 0) == pdTRUE) {
memcpy(dest, frame.buf, frame.len);
BLE_DEBUG_PRINTLN("readBytes: sz=%d, hdr=%d", (uint32_t) frame.len, (uint32_t) dest[0]);
return frame.len;
}
if (deviceConnected != oldDeviceConnected) {
+13 -5
View File
@@ -6,6 +6,8 @@
#include <BLEUtils.h>
#include <BLE2902.h>
#include <atomic>
#include <freertos/FreeRTOS.h>
#include <freertos/queue.h>
class SerialBLEInterface : public BaseSerialInterface, BLESecurityCallbacks, BLEServerCallbacks, BLECharacteristicCallbacks {
BLEServer *pServer;
@@ -28,12 +30,13 @@ class SerialBLEInterface : public BaseSerialInterface, BLESecurityCallbacks, BLE
};
#define FRAME_QUEUE_SIZE 4
int recv_queue_len;
Frame recv_queue[FRAME_QUEUE_SIZE];
StaticQueue_t recv_queue_state;
uint8_t recv_queue_storage[FRAME_QUEUE_SIZE * sizeof(Frame)];
QueueHandle_t recv_queue;
int send_queue_len;
Frame send_queue[FRAME_QUEUE_SIZE];
void clearBuffers() { recv_queue_len = 0; send_queue_len = 0; }
void clearBuffers();
protected:
// BLESecurityCallbacks methods
@@ -66,7 +69,10 @@ public:
_isEnabled = false;
_last_write = 0;
last_conn_id = 0;
send_queue_len = recv_queue_len = 0;
recv_queue = xQueueCreateStatic(
FRAME_QUEUE_SIZE, sizeof(Frame), recv_queue_storage, &recv_queue_state
);
send_queue_len = 0;
}
/**
@@ -86,7 +92,9 @@ public:
bool isReadBusy() const override;
bool isWriteBusy() const override;
bool hasPendingIO() const override { return recv_queue_len > 0 || send_queue_len > 0; }
bool hasPendingIO() const override {
return uxQueueMessagesWaiting(recv_queue) > 0 || send_queue_len > 0;
}
bool takePairingRequest() override {
return _pairingRequestPending.exchange(false, std::memory_order_acq_rel);
}
+10
View File
@@ -16,6 +16,16 @@ void TBeamBoard::begin() {
ESP32Board::begin();
#ifdef TBEAM_SUPREME_SX1262
// On the T-Beam S3 Supreme the PMU + RTC sit on Wire1 (GPIO 42/41, brought
// up by XPowersLib), while the SH1106 OLED and the BME280/QMC6310 sensors
// sit on the primary bus, Wire (GPIO PIN_BOARD_SDA/PIN_BOARD_SCL). Nothing
// else initialises Wire on this board, so the display driver would
// otherwise talk on the wrong (default) pins and display.begin() fails,
// leaving the screen blank. Bring the OLED bus up here.
Wire.begin(PIN_BOARD_SDA, PIN_BOARD_SCL);
#endif
power_init();
//Configure user button
+48 -4
View File
@@ -4,6 +4,10 @@
#include "MeshCore.h"
class CustomLR1110 : public LR1110 {
uint32_t _preambleMillis = 66;
uint32_t _maxPayloadMillis = 3934;
uint32_t _activityAt = 0;
bool _headerSeen = false;
bool _rx_boosted = false;
public:
@@ -21,7 +25,7 @@ class CustomLR1110 : public LR1110 {
}
return len;
}
float getFreqMHz() const { return freqMHz; }
int16_t startReceiveDutyCycle(uint32_t rxPeriod, uint32_t sleepPeriod,
@@ -80,9 +84,49 @@ class CustomLR1110 : public LR1110 {
bool isReceiving() {
if (isChipBusy()) return false; // asleep, cannot be mid-receive
uint16_t irq = getIrqStatus();
bool detected = ((irq & RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID) || (irq & RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED));
return detected;
uint32_t irq = getIrqStatus();
bool preamble = irq & RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED; // bit 4
bool header = irq & RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID; // bit 5
bool hdrErr = irq & RADIOLIB_LR11X0_IRQ_HEADER_ERR; // bit 6
uint32_t now = millis();
if (hdrErr) {
clearIrqState(RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED | RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID | RADIOLIB_LR11X0_IRQ_HEADER_ERR);
_activityAt = 0;
_headerSeen = false;
return false;
}
if (header) {
if (!_headerSeen) { _headerSeen = true; _activityAt = now; };
if (now - _activityAt > _maxPayloadMillis) {
MESH_DEBUG_PRINTLN("Clearing header IRQ after %ums", _maxPayloadMillis);
clearIrqState(RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED | RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID | RADIOLIB_LR11X0_IRQ_HEADER_ERR);
_activityAt = 0; _headerSeen = false;
return false;
}
return true;
}
if (preamble) {
if (_activityAt == 0) _activityAt = now;
if (now - _activityAt > _preambleMillis) {
clearIrqState(RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED);
_activityAt = 0;
MESH_DEBUG_PRINTLN("Clearing preamble IRQ after %ums", _preambleMillis);
return false;
}
return true;
}
_activityAt = 0; _headerSeen = false;
return false;
}
void setPreambleMillis(uint32_t preambleMillis) {
_preambleMillis = preambleMillis;
MESH_DEBUG_PRINTLN("Set _preambleMillis=%u", _preambleMillis);
}
void setMaxPayloadMillis(uint32_t payloadMillis) {
_maxPayloadMillis = payloadMillis;
MESH_DEBUG_PRINTLN("Set _maxPayloadMillis=%u", _maxPayloadMillis);
}
uint8_t getSpreadingFactor() const { return spreadingFactor; }
+7 -1
View File
@@ -14,11 +14,17 @@ public:
protected:
bool applyParams(float freq, float bw, uint8_t sf, uint8_t cr) override {
return ((CustomLR1110 *)_radio)->setFrequency(freq) == RADIOLIB_ERR_NONE
bool success = ((CustomLR1110 *)_radio)->setFrequency(freq) == RADIOLIB_ERR_NONE
&& ((CustomLR1110 *)_radio)->setSpreadingFactor(sf) == RADIOLIB_ERR_NONE
&& ((CustomLR1110 *)_radio)->setBandwidth(bw) == RADIOLIB_ERR_NONE
&& ((CustomLR1110 *)_radio)->setCodingRate(cr) == RADIOLIB_ERR_NONE
&& updatePreamble(sf);
if (!success) return false;
PacketMillis pm = calcMaxPacketMillis(sf, bw, cr, preambleLengthForSF(sf));
((CustomLR1110 *)_radio)->setPreambleMillis(pm.preambleMillis);
((CustomLR1110 *)_radio)->setMaxPayloadMillis(pm.payloadMillis);
return true;
}
public:
+48 -6
View File
@@ -3,14 +3,16 @@
#include <RadioLib.h>
#include "MeshCore.h"
#define SX126X_IRQ_HEADER_VALID 0b0000010000 // 4 4 valid LoRa header received
#define SX126X_IRQ_PREAMBLE_DETECTED 0x04
#ifndef SX126X_TX_BUSY_TIMEOUT_MS
#define SX126X_TX_BUSY_TIMEOUT_MS 1000UL
#endif
class CustomSX1262 : public SX1262 {
uint32_t _preambleMillis = 66;
uint32_t _maxPayloadMillis = 3934;
uint32_t _activityAt = 0;
bool _headerSeen = false;
public:
CustomSX1262(Module *mod) : SX1262(mod) { }
@@ -141,9 +143,49 @@ class CustomSX1262 : public SX1262 {
bool isReceiving() {
if (isChipBusy()) return false; // asleep, cannot be mid-receive
uint16_t irq = getIrqFlags();
bool detected = (irq & SX126X_IRQ_HEADER_VALID) || (irq & SX126X_IRQ_PREAMBLE_DETECTED);
return detected;
uint32_t irq = getIrqFlags();
bool preamble = irq & RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED; // bit 2
bool header = irq & RADIOLIB_SX126X_IRQ_HEADER_VALID; // bit 4
bool hdrErr = irq & RADIOLIB_SX126X_IRQ_HEADER_ERR; // bit 5
uint32_t now = millis();
if (hdrErr) {
clearIrqFlags(RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED | RADIOLIB_SX126X_IRQ_HEADER_VALID | RADIOLIB_SX126X_IRQ_HEADER_ERR | RADIOLIB_SX126X_IRQ_SYNC_WORD_VALID);
_activityAt = 0;
_headerSeen = false;
return false;
}
if (header) {
if (!_headerSeen) { _headerSeen = true; _activityAt = now; };
if (now - _activityAt > _maxPayloadMillis) {
MESH_DEBUG_PRINTLN("Clearing header IRQ after %ums", _maxPayloadMillis);
clearIrqFlags(RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED | RADIOLIB_SX126X_IRQ_HEADER_VALID | RADIOLIB_SX126X_IRQ_HEADER_ERR | RADIOLIB_SX126X_IRQ_SYNC_WORD_VALID);
_activityAt = 0; _headerSeen = false;
return false;
}
return true;
}
if (preamble) {
if (_activityAt == 0) _activityAt = now;
if (now - _activityAt > _preambleMillis) {
clearIrqFlags(RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED);
_activityAt = 0;
MESH_DEBUG_PRINTLN("Clearing preamble IRQ after %ums", _preambleMillis);
return false;
}
return true;
}
_activityAt = 0; _headerSeen = false;
return false;
}
void setPreambleMillis(uint32_t preambleMillis) {
_preambleMillis = preambleMillis;
MESH_DEBUG_PRINTLN("Set _preambleMillis=%u", _preambleMillis);
}
void setMaxPayloadMillis(uint32_t payloadMillis) {
_maxPayloadMillis = payloadMillis;
MESH_DEBUG_PRINTLN("Set _maxPayloadMillis=%u", _maxPayloadMillis);
}
// Port of Semtech's sx126x_stop_rtc() (same registers as RadioLib's
+7 -1
View File
@@ -14,11 +14,17 @@ public:
protected:
bool applyParams(float freq, float bw, uint8_t sf, uint8_t cr) override {
return ((CustomSX1262 *)_radio)->setFrequency(freq) == RADIOLIB_ERR_NONE
bool success = ((CustomSX1262 *)_radio)->setFrequency(freq) == RADIOLIB_ERR_NONE
&& ((CustomSX1262 *)_radio)->setSpreadingFactor(sf) == RADIOLIB_ERR_NONE
&& ((CustomSX1262 *)_radio)->setBandwidth(bw) == RADIOLIB_ERR_NONE
&& ((CustomSX1262 *)_radio)->setCodingRate(cr) == RADIOLIB_ERR_NONE
&& updatePreamble(sf);
if (!success) return false;
PacketMillis pm = calcMaxPacketMillis(sf, bw, cr, preambleLengthForSF(sf));
((CustomSX1262 *)_radio)->setPreambleMillis(pm.preambleMillis);
((CustomSX1262 *)_radio)->setMaxPayloadMillis(pm.payloadMillis);
return true;
}
public:
+23 -1
View File
@@ -713,7 +713,7 @@ static float snr_threshold[] = {
-17.5,// SF11 needs at least -17.5 dB SNR
-20 // SF12 needs at least -20 dB SNR
};
float RadioLibWrapper::packetScoreInt(float snr, int sf, int packet_len) {
if (sf < 7) return 0.0f;
@@ -724,3 +724,25 @@ float RadioLibWrapper::packetScoreInt(float snr, int sf, int packet_len) {
return max(0.0, min(1.0, success_rate_based_on_snr * collision_penalty));
}
PacketMillis RadioLibWrapper::calcMaxPacketMillis(uint8_t sf, float bw, uint8_t cr, uint8_t preambleSymbols) {
// based on RadioLib's calculateTimeOnAir()
uint32_t tsym_us = ((uint32_t)10000 << sf) / (bw * 10);
uint32_t sfCoeff1_x4 = (sf == 5 || sf == 6) ? 25 : 17; // 6.25 : 4.25, semtech magic numbers to account for sync word + sfd
// preamble + syncword + sfd + header
uint32_t preamble_us = (((preambleSymbols + 8) * 4 + sfCoeff1_x4) * tsym_us) / 4;
// airtime for max packet at current radio settings
uint32_t total_us = _radio->getTimeOnAir(MAX_TRANS_UNIT);
// airtime for payload only (no preamble, header or SOF)
const uint32_t fallback_total_us = 4000000UL;
uint32_t payload_us = total_us > preamble_us
? total_us - preamble_us
: (fallback_total_us > preamble_us ? fallback_total_us - preamble_us
: fallback_total_us);
// rescale payload_us for max possible CR
if (cr >= 5 && cr < 8) { payload_us = (payload_us * 8) / cr; }
return PacketMillis {(preamble_us + 999) / 1000, (payload_us + 999) / 1000};
}
+6
View File
@@ -11,6 +11,11 @@
#define RX_PS_FALLBACK_RX_US 65625UL
#define RX_PS_FALLBACK_SLEEP_US 60000UL
struct PacketMillis {
uint32_t preambleMillis; // preamble-detect -> header-valid deadline
uint32_t payloadMillis; // header-valid -> rx-done deadline
};
class RadioLibWrapper : public mesh::Radio {
protected:
PhysicalLayer* _radio;
@@ -151,6 +156,7 @@ public:
_preamble_sf = sf;
return true;
}
PacketMillis calcMaxPacketMillis(uint8_t sf, float bw, uint8_t cr, uint8_t preambleSymbols);
virtual int16_t performChannelScan();
int getNoiseFloor() const override { return _noise_floor; }
+18 -14
View File
@@ -13,8 +13,12 @@
#endif
#endif
#ifndef PIN_GPS_EN_ACTIVE
#define PIN_GPS_EN_ACTIVE HIGH
#ifndef GPS_EN_ACTIVE
#ifdef PIN_GPS_EN_ACTIVE
#define GPS_EN_ACTIVE PIN_GPS_EN_ACTIVE
#else
#define GPS_EN_ACTIVE HIGH
#endif
#endif
#ifndef GPS_RESET
@@ -25,11 +29,11 @@
#endif
#endif
#ifndef GPS_RESET_FORCE
#ifndef GPS_RESET_ACTIVE
#ifdef PIN_GPS_RESET_ACTIVE
#define GPS_RESET_FORCE PIN_GPS_RESET_ACTIVE
#define GPS_RESET_ACTIVE PIN_GPS_RESET_ACTIVE
#else
#define GPS_RESET_FORCE LOW
#define GPS_RESET_ACTIVE LOW
#endif
#endif
@@ -52,11 +56,11 @@ public :
nmea(_nmeaBuffer, sizeof(_nmeaBuffer)), _clock(clock), _gps_serial(&ser), _peripher_power(peripher_power), _pin_reset(pin_reset), _pin_en(pin_en) {
if (_pin_reset != -1) {
pinMode(_pin_reset, OUTPUT);
digitalWrite(_pin_reset, GPS_RESET_FORCE);
digitalWrite(_pin_reset, GPS_RESET_ACTIVE);
}
if (_pin_en != -1) {
pinMode(_pin_en, OUTPUT);
digitalWrite(_pin_en, LOW);
digitalWrite(_pin_en, !GPS_EN_ACTIVE);
}
}
@@ -74,27 +78,27 @@ public :
void begin() override {
claim();
if (_pin_en != -1) {
digitalWrite(_pin_en, PIN_GPS_EN_ACTIVE);
digitalWrite(_pin_en, GPS_EN_ACTIVE);
}
if (_pin_reset != -1) {
digitalWrite(_pin_reset, !GPS_RESET_FORCE);
digitalWrite(_pin_reset, !GPS_RESET_ACTIVE);
}
}
void reset() override {
if (_pin_reset != -1) {
digitalWrite(_pin_reset, GPS_RESET_FORCE);
digitalWrite(_pin_reset, GPS_RESET_ACTIVE);
delay(10);
digitalWrite(_pin_reset, !GPS_RESET_FORCE);
digitalWrite(_pin_reset, !GPS_RESET_ACTIVE);
}
}
void stop() override {
if (_pin_en != -1) {
digitalWrite(_pin_en, !PIN_GPS_EN_ACTIVE);
digitalWrite(_pin_en, !GPS_EN_ACTIVE);
}
if (_pin_reset != -1) {
digitalWrite(_pin_reset, GPS_RESET_FORCE);
digitalWrite(_pin_reset, GPS_RESET_ACTIVE);
}
release();
}
@@ -103,7 +107,7 @@ public :
// directly read the enable pin if present as gps can be
// activated/deactivated outside of here ...
if (_pin_en != -1) {
return digitalRead(_pin_en) == PIN_GPS_EN_ACTIVE;
return digitalRead(_pin_en) == GPS_EN_ACTIVE;
} else {
return true; // no enable so must be active
}
+4 -1
View File
@@ -11,7 +11,10 @@ bool SH1106Display::i2c_probe(TwoWire &wire, uint8_t addr)
bool SH1106Display::begin()
{
return display.begin(DISPLAY_ADDRESS, true) && i2c_probe(Wire, DISPLAY_ADDRESS);
// Wire must already be initialised by board.begin() before this is called.
// Boards with non-standard SH1106 addresses should define DISPLAY_ADDRESS
// in their variant/platformio configuration.
return i2c_probe(Wire, DISPLAY_ADDRESS) && display.begin(DISPLAY_ADDRESS, true);
}
void SH1106Display::turnOn()
+17
View File
@@ -0,0 +1,17 @@
#pragma once
#include <cstdint>
#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;
}
+11
View File
@@ -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; }
};
+39
View File
@@ -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);
}
};
}
+27
View File
@@ -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() {}
};
}
+16 -4
View File
@@ -4,13 +4,25 @@
#include <cstdint>
// Mock Stream class for native testing
// Provides minimal interface needed by Utils.h
// Provides the Arduino Stream surface needed by native tests.
class Stream {
public:
virtual void print(char c) {}
virtual void print(const char* str) {}
virtual ~Stream() = default;
virtual size_t write(uint8_t) { return 1; }
virtual size_t write(const uint8_t* buffer, size_t size) {
size_t total = 0;
for (size_t i = 0; i < size; i++) {
total += write(buffer[i]);
}
return total;
}
virtual int available() { return 0; }
virtual int availableForWrite() { return 0; }
virtual int read() { return -1; }
virtual void flush() {}
virtual void print(char) {}
virtual void print(const char*) {}
virtual void println() {}
virtual size_t readBytes(uint8_t*, size_t) { return 0; }
virtual size_t write(const uint8_t*, size_t) { return 0; }
};
+44
View File
@@ -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;
}
};
}
+10
View File
@@ -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,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();
}
+5
View File
@@ -751,5 +751,10 @@ lib_deps =
[env:heltec_v4_kiss_modem]
extends = Heltec_lora32_v4
build_unflags =
-DARDUINO_USB_MODE=0
build_flags =
${Heltec_lora32_v4.build_flags}
-DARDUINO_USB_MODE=1
build_src_filter = ${Heltec_lora32_v4.build_src_filter}
+<../examples/kiss_modem/>
+1 -1
View File
@@ -45,7 +45,7 @@ build_flags =
${LilyGo_TBeam_SX1276.build_flags}
-I examples/companion_radio/ui-new
-D MAX_CONTACTS=160
-D MAX_GROUP_CHANNELS=8
-D MAX_GROUP_CHANNELS=40
-D BLE_PIN_CODE=123456
; -D BLE_DEBUG_LOGGING=1
-D OFFLINE_QUEUE_SIZE=128
@@ -19,6 +19,7 @@ build_flags =
-D RADIO_CLASS=CustomSX1262
-D WRAPPER_CLASS=CustomSX1262Wrapper
-D DISPLAY_CLASS=SH1106Display
-D DISPLAY_ADDRESS=0x3D
-D LORA_TX_POWER=22
-D P_LORA_TX_LED=6
-D PIN_BOARD_SDA=17
+3
View File
@@ -311,10 +311,13 @@ lib_deps =
[env:Station_G2_kiss_modem]
extends = Station_G2
build_unflags =
-DARDUINO_USB_MODE=0
build_flags =
${Station_G2.build_flags}
-UENABLE_OTA
-D DISABLE_LORA_OTA=1
-DARDUINO_USB_MODE=1
build_src_filter = ${Station_G2.build_src_filter}
+<../examples/kiss_modem/>
+6 -6
View File
@@ -18,12 +18,12 @@ public:
void begin();
uint16_t getBattMilliVolts() override;
#if defined(P_LORA_TX_LED)
#ifdef P_LORA_TX_LED
void onBeforeTransmit() override {
digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED on
digitalWrite(P_LORA_TX_LED, LED_STATE_ON); // turn TX LED on
}
void onAfterTransmit() override {
digitalWrite(P_LORA_TX_LED, LOW); // turn TX LED off
digitalWrite(P_LORA_TX_LED, !LED_STATE_ON); // turn TX LED off
}
#endif
@@ -44,9 +44,9 @@ public:
void powerOff() override {
// turn off all leds, sd_power_system_off will not do this for us
#ifdef P_LORA_TX_LED
digitalWrite(P_LORA_TX_LED, LOW);
#endif
digitalWrite(PIN_LED_BLUE, !LED_STATE_ON);
digitalWrite(PIN_LED_GREEN, !LED_STATE_ON);
digitalWrite(PIN_LED_RED, !LED_STATE_ON);
// power off board
NRF52Board::powerOff();
-2
View File
@@ -25,7 +25,6 @@ build_flags = ${rak4631_hw.build_flags}
-D P_LORA_MOSI=46
-D P_LORA_RESET=42
-D P_LORA_TX_LED=PIN_LED_BLUE
-D P_LORA_TX_LED_ON=LOW
-D LR11X0_DIO_AS_RF_SWITCH=true
-D LR11X0_DIO3_TCXO_VOLTAGE=3.3
-D MESH_DEBUG=1
@@ -106,7 +105,6 @@ build_flags = ${ThinkNode_M3.build_flags}
-D BLE_TX_POWER=0
; -D BLE_DEBUG_LOGGING=1
; -D MESH_PACKET_LOGGING=1
-D GPS_NMEA_DEBUG
-D OFFLINE_QUEUE_SIZE=256
-D DISPLAY_CLASS=NullDisplayDriver
-D PIN_BUZZER=23
+5 -3
View File
@@ -80,16 +80,18 @@ void initVariant()
digitalWrite(LED_POWER, HIGH);
pinMode(PIN_LED_BLUE, OUTPUT);
digitalWrite(PIN_LED_BLUE, !LED_STATE_ON);
pinMode(PIN_LED_GREEN, OUTPUT);
digitalWrite(PIN_LED_GREEN, !LED_STATE_ON);
pinMode(PIN_LED_RED, OUTPUT);
digitalWrite(PIN_LED_RED, !LED_STATE_ON);
pinMode(BUTTON_PIN, INPUT_PULLUP);
pinMode(PIN_GPS_POWER, OUTPUT);
pinMode(PIN_GPS_EN, OUTPUT);
pinMode(PIN_GPS_RESET, OUTPUT);
// Power on gps but in standby
digitalWrite(PIN_GPS_EN, LOW);
digitalWrite(PIN_GPS_POWER, HIGH);
digitalWrite(PIN_GPS_EN, !GPS_EN_ACTIVE);
digitalWrite(PIN_GPS_POWER, GPS_POWER_ACTIVE);
}
+5 -4
View File
@@ -32,7 +32,7 @@
#define EXT_CHRG_DETECT (32) // P1.3
#define EXT_PWR_DETECT (31) // P0.5
#define PIN_VBAT_READ (5)
#define PIN_VBAT_READ (5)
#define AREF_VOLTAGE (2.4f)
#define ADC_MULTIPLIER (2.0) //(1.75f)
// 2.0 gives more coherent value, 4.2V when charged, needs tweaking
@@ -92,18 +92,19 @@
// GPS
#define HAS_GPS 1
#define PIN_GPS_RX (22)
#define PIN_GPS_RX (22)
#define PIN_GPS_TX (20)
#define PIN_GPS_POWER (14)
#define PIN_GPS_EN (21) // STANDBY
#define PIN_GPS_RESET (25) // REINIT
#define GPS_RESET_ACTIVE LOW
#define GPS_POWER_ACTIVE HIGH
#define GPS_EN_ACTIVE HIGH
#define GPS_RESET (-1)
#define GPS_BAUDRATE 9600
////////////////////////////////////////////////////////////////////////////////
// Buzzer
#define BUZZER_EN (37) // P1.5
#define BUZZER_PIN (25) // P0.25
#define BUZZER_PIN (25) // P0.25
+1 -1
View File
@@ -47,7 +47,7 @@ public:
#ifdef PIN_USER_BTN
// configure button press to wake up when in powered off state
nrf_gpio_cfg_sense_input(digitalPinToInterrupt(g_ADigitalPinMap[PIN_USER_BTN]), NRF_GPIO_PIN_NOPULL, NRF_GPIO_PIN_SENSE_LOW);
nrf_gpio_cfg_sense_input(digitalPinToInterrupt(g_ADigitalPinMap[PIN_USER_BTN]), NRF_GPIO_PIN_PULLUP, NRF_GPIO_PIN_SENSE_LOW);
#endif
NRF52Board::powerOff();