diff --git a/.github/actions/setup-build-environment/action.yml b/.github/actions/setup-build-environment/action.yml index 02aaf4249..cba2d2e56 100644 --- a/.github/actions/setup-build-environment/action.yml +++ b/.github/actions/setup-build-environment/action.yml @@ -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 diff --git a/.github/workflows/build-companion-firmwares.yml b/.github/workflows/build-companion-firmwares.yml index 771fa6d5b..8fd796f74 100644 --- a/.github/workflows/build-companion-firmwares.yml +++ b/.github/workflows/build-companion-firmwares.yml @@ -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/* \ No newline at end of file + build-companion-firmwares: + uses: ./.github/workflows/firmware-builder.yml + with: + firmware_type: 'companion' + release_title_prefix: 'Companion Firmware' diff --git a/.github/workflows/build-repeater-firmwares.yml b/.github/workflows/build-repeater-firmwares.yml index 3185d4b21..46b9076cc 100644 --- a/.github/workflows/build-repeater-firmwares.yml +++ b/.github/workflows/build-repeater-firmwares.yml @@ -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/* \ No newline at end of file + build-repeater-firmwares: + uses: ./.github/workflows/firmware-builder.yml + with: + firmware_type: 'repeater' + release_title_prefix: 'Repeater Firmware' diff --git a/.github/workflows/build-room-server-firmwares.yml b/.github/workflows/build-room-server-firmwares.yml index 127095a8d..c8bd19f7f 100644 --- a/.github/workflows/build-room-server-firmwares.yml +++ b/.github/workflows/build-room-server-firmwares.yml @@ -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/* \ No newline at end of file + build-room-server-firmwares: + uses: ./.github/workflows/firmware-builder.yml + with: + firmware_type: 'room-server' + release_title_prefix: 'Room Server Firmware' diff --git a/.github/workflows/firmware-builder.yml b/.github/workflows/firmware-builder.yml new file mode 100644 index 000000000..a2f422f98 --- /dev/null +++ b/.github/workflows/firmware-builder.yml @@ -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/* diff --git a/.github/workflows/run-unit-tests.yml b/.github/workflows/run-unit-tests.yml index e3af3aafb..5d48f4c67 100644 --- a/.github/workflows/run-unit-tests.yml +++ b/.github/workflows/run-unit-tests.yml @@ -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. diff --git a/boards/heltec_v4.json b/boards/heltec_v4.json index 36cdfc043..0d3a52cd0 100644 --- a/boards/heltec_v4.json +++ b/boards/heltec_v4.json @@ -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" -} \ No newline at end of file +} diff --git a/docs/kiss_modem_protocol.md b/docs/kiss_modem_protocol.md index 1e7114be1..e000b485c 100644 --- a/docs/kiss_modem_protocol.md +++ b/docs/kiss_modem_protocol.md @@ -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 diff --git a/docs/qr_codes.md b/docs/qr_codes.md index 364efa8ac..3516a76a4 100644 --- a/docs/qr_codes.md +++ b/docs/qr_codes.md @@ -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 diff --git a/examples/kiss_modem/KissModem.cpp b/examples/kiss_modem/KissModem.cpp index eeab1501d..7dcfc8d7d 100644 --- a/examples/kiss_modem/KissModem.cpp +++ b/examples/kiss_modem/KissModem.cpp @@ -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); } diff --git a/examples/kiss_modem/KissModem.h b/examples/kiss_modem/KissModem.h index bbe99d6de..a23e459be 100644 --- a/examples/kiss_modem/KissModem.h +++ b/examples/kiss_modem/KissModem.h @@ -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; } }; diff --git a/examples/kiss_modem/main.cpp b/examples/kiss_modem/main.cpp index 5a1ce8a2c..3ce35cdae 100644 --- a/examples/kiss_modem/main.cpp +++ b/examples/kiss_modem/main.cpp @@ -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(); diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index b614d3aae..571799e27 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -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; diff --git a/examples/simple_room_server/MyMesh.h b/examples/simple_room_server/MyMesh.h index cf6716784..065e5e8e8 100644 --- a/examples/simple_room_server/MyMesh.h +++ b/examples/simple_room_server/MyMesh.h @@ -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"); diff --git a/platformio.ini b/platformio.ini index fbe5c207c..2818b8323 100644 --- a/platformio.ini +++ b/platformio.ini @@ -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 diff --git a/src/helpers/NRF52Board.cpp b/src/helpers/NRF52Board.cpp index 69731874a..17997d344 100644 --- a/src/helpers/NRF52Board.cpp +++ b/src/helpers/NRF52Board.cpp @@ -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) { diff --git a/src/helpers/esp32/SerialBLEInterface.cpp b/src/helpers/esp32/SerialBLEInterface.cpp index 6002042a1..d714b3cdb 100644 --- a/src/helpers/esp32/SerialBLEInterface.cpp +++ b/src/helpers/esp32/SerialBLEInterface.cpp @@ -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) { diff --git a/src/helpers/esp32/SerialBLEInterface.h b/src/helpers/esp32/SerialBLEInterface.h index 24e6c110b..1a673e317 100644 --- a/src/helpers/esp32/SerialBLEInterface.h +++ b/src/helpers/esp32/SerialBLEInterface.h @@ -6,6 +6,8 @@ #include #include #include +#include +#include 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); } diff --git a/src/helpers/esp32/TBeamBoard.cpp b/src/helpers/esp32/TBeamBoard.cpp index 6f835ce89..0da325b59 100644 --- a/src/helpers/esp32/TBeamBoard.cpp +++ b/src/helpers/esp32/TBeamBoard.cpp @@ -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 diff --git a/src/helpers/radiolib/CustomLR1110.h b/src/helpers/radiolib/CustomLR1110.h index 6f7bb1a7d..551156526 100644 --- a/src/helpers/radiolib/CustomLR1110.h +++ b/src/helpers/radiolib/CustomLR1110.h @@ -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; } diff --git a/src/helpers/radiolib/CustomLR1110Wrapper.h b/src/helpers/radiolib/CustomLR1110Wrapper.h index dac91a3a7..4c7473ecf 100644 --- a/src/helpers/radiolib/CustomLR1110Wrapper.h +++ b/src/helpers/radiolib/CustomLR1110Wrapper.h @@ -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: diff --git a/src/helpers/radiolib/CustomSX1262.h b/src/helpers/radiolib/CustomSX1262.h index 40c3e146e..1ef152e73 100644 --- a/src/helpers/radiolib/CustomSX1262.h +++ b/src/helpers/radiolib/CustomSX1262.h @@ -3,14 +3,16 @@ #include #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 diff --git a/src/helpers/radiolib/CustomSX1262Wrapper.h b/src/helpers/radiolib/CustomSX1262Wrapper.h index b781361d7..ee5626c3b 100644 --- a/src/helpers/radiolib/CustomSX1262Wrapper.h +++ b/src/helpers/radiolib/CustomSX1262Wrapper.h @@ -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: diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index a0e708591..3c8a105c2 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -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}; +} diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index 3493a9213..c65f8e706 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -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; } diff --git a/src/helpers/sensors/MicroNMEALocationProvider.h b/src/helpers/sensors/MicroNMEALocationProvider.h index ece4e99f0..964ff22c9 100644 --- a/src/helpers/sensors/MicroNMEALocationProvider.h +++ b/src/helpers/sensors/MicroNMEALocationProvider.h @@ -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 } diff --git a/src/helpers/ui/SH1106Display.cpp b/src/helpers/ui/SH1106Display.cpp index f383bb005..57ccfc2e4 100644 --- a/src/helpers/ui/SH1106Display.cpp +++ b/src/helpers/ui/SH1106Display.cpp @@ -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() diff --git a/test/mocks/Arduino.h b/test/mocks/Arduino.h new file mode 100644 index 000000000..77499fe41 --- /dev/null +++ b/test/mocks/Arduino.h @@ -0,0 +1,17 @@ +#pragma once + +#include +#include +#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; +} diff --git a/test/mocks/CayenneLPP.h b/test/mocks/CayenneLPP.h new file mode 100644 index 000000000..9d51c0fcf --- /dev/null +++ b/test/mocks/CayenneLPP.h @@ -0,0 +1,11 @@ +#pragma once + +#include +#include + +class CayenneLPP { +public: + explicit CayenneLPP(size_t) {} + const uint8_t* getBuffer() const { return nullptr; } + uint16_t getSize() const { return 0; } +}; diff --git a/test/mocks/Identity.h b/test/mocks/Identity.h new file mode 100644 index 000000000..5c3d25e94 --- /dev/null +++ b/test/mocks/Identity.h @@ -0,0 +1,39 @@ +#pragma once + +#include +#include +#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); + } +}; + +} diff --git a/test/mocks/Mesh.h b/test/mocks/Mesh.h new file mode 100644 index 000000000..b6c263c19 --- /dev/null +++ b/test/mocks/Mesh.h @@ -0,0 +1,27 @@ +#pragma once + +#include + +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() {} +}; + +} diff --git a/test/mocks/Stream.h b/test/mocks/Stream.h index 2c4a6c3aa..db2cba55e 100644 --- a/test/mocks/Stream.h +++ b/test/mocks/Stream.h @@ -4,13 +4,25 @@ #include // 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; } }; diff --git a/test/mocks/Utils.h b/test/mocks/Utils.h new file mode 100644 index 000000000..9bb6b060b --- /dev/null +++ b/test/mocks/Utils.h @@ -0,0 +1,44 @@ +#pragma once + +#include +#include +#include +#include + +#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; + } +}; + +} diff --git a/test/mocks/helpers/SensorManager.h b/test/mocks/helpers/SensorManager.h new file mode 100644 index 000000000..d1a41cb5d --- /dev/null +++ b/test/mocks/helpers/SensorManager.h @@ -0,0 +1,10 @@ +#pragma once + +#include +#include "CayenneLPP.h" + +class SensorManager { +public: + virtual ~SensorManager() = default; + virtual bool querySensors(uint8_t, CayenneLPP&) { return false; } +}; diff --git a/test/test_kiss_modem/test_tx_backpressure.cpp b/test/test_kiss_modem/test_tx_backpressure.cpp new file mode 100644 index 000000000..39a5e3b74 --- /dev/null +++ b/test/test_kiss_modem/test_tx_backpressure.cpp @@ -0,0 +1,294 @@ +#include + +#include +#include +#include +#include +#include +#include +#include + +#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& bytes) { + std::lock_guard lock(_mutex); + for (uint8_t b : bytes) { + _rx.push(b); + } + } + + void setBlockWrites(bool blocked) { + { + std::lock_guard lock(_mutex); + _block_writes = blocked; + } + _cv.notify_all(); + } + + bool isWriteBlocked() const { + return _entered_block.load(); + } + + size_t writesCount() const { + std::lock_guard lock(_mutex); + return _writes.size(); + } + + std::vector writesSnapshot() const { + std::lock_guard lock(_mutex); + return _writes; + } + + int availableForWrite() override { + std::lock_guard lock(_mutex); + return _block_writes ? 0 : TEST_TX_AVAILABLE_BYTES; + } + + void setMaxWriteChunk(size_t chunk) { + std::lock_guard lock(_mutex); + _max_write_chunk = chunk; + } + + size_t write(const uint8_t* buffer, size_t size) override { + std::unique_lock 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 lock(_mutex); + return static_cast(_rx.size()); + } + + int read() override { + std::lock_guard 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 _rx; + std::vector _writes; + bool _block_writes = false; + std::atomic _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 dataFrame(const std::vector& packet) { + std::vector 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 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 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 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 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 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 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 packet(KISS_MAX_PACKET_SIZE, KISS_FEND); + std::vector 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(); +} diff --git a/variants/heltec_v4/platformio.ini b/variants/heltec_v4/platformio.ini index 2523780bb..73692767b 100644 --- a/variants/heltec_v4/platformio.ini +++ b/variants/heltec_v4/platformio.ini @@ -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/> diff --git a/variants/lilygo_tbeam_SX1276/platformio.ini b/variants/lilygo_tbeam_SX1276/platformio.ini index 8756f8fee..7f6d6e6ea 100644 --- a/variants/lilygo_tbeam_SX1276/platformio.ini +++ b/variants/lilygo_tbeam_SX1276/platformio.ini @@ -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 diff --git a/variants/lilygo_tbeam_supreme_SX1262/platformio.ini b/variants/lilygo_tbeam_supreme_SX1262/platformio.ini index d210aad71..354ece651 100644 --- a/variants/lilygo_tbeam_supreme_SX1262/platformio.ini +++ b/variants/lilygo_tbeam_supreme_SX1262/platformio.ini @@ -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 diff --git a/variants/station_g2/platformio.ini b/variants/station_g2/platformio.ini index 0ccc0fc5a..779149d62 100644 --- a/variants/station_g2/platformio.ini +++ b/variants/station_g2/platformio.ini @@ -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/> diff --git a/variants/thinknode_m3/ThinkNodeM3Board.h b/variants/thinknode_m3/ThinkNodeM3Board.h index 396d80d1a..9e8f49894 100644 --- a/variants/thinknode_m3/ThinkNodeM3Board.h +++ b/variants/thinknode_m3/ThinkNodeM3Board.h @@ -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(); diff --git a/variants/thinknode_m3/platformio.ini b/variants/thinknode_m3/platformio.ini index 75bc83b1c..905762055 100644 --- a/variants/thinknode_m3/platformio.ini +++ b/variants/thinknode_m3/platformio.ini @@ -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 diff --git a/variants/thinknode_m3/variant.cpp b/variants/thinknode_m3/variant.cpp index dad0f3f55..b47b83543 100644 --- a/variants/thinknode_m3/variant.cpp +++ b/variants/thinknode_m3/variant.cpp @@ -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); } diff --git a/variants/thinknode_m3/variant.h b/variants/thinknode_m3/variant.h index 02ed78a84..78dfab85d 100644 --- a/variants/thinknode_m3/variant.h +++ b/variants/thinknode_m3/variant.h @@ -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 \ No newline at end of file +#define BUZZER_PIN (25) // P0.25 diff --git a/variants/xiao_nrf52/XiaoNrf52Board.h b/variants/xiao_nrf52/XiaoNrf52Board.h index b2638a440..e71e7f651 100644 --- a/variants/xiao_nrf52/XiaoNrf52Board.h +++ b/variants/xiao_nrf52/XiaoNrf52Board.h @@ -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();