Merge upstream dev into keymindCascade
This commit is contained in:
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'
|
||||
@@ -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'
|
||||
@@ -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/*
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
};
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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};
|
||||
}
|
||||
@@ -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; }
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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() {}
|
||||
};
|
||||
|
||||
}
|
||||
+16
-4
@@ -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; }
|
||||
};
|
||||
@@ -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,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();
|
||||
}
|
||||
@@ -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/>
|
||||
@@ -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
|
||||
|
||||
@@ -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/>
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user