From 9cc2da24469259fc13997838b39d986c3fe9a452 Mon Sep 17 00:00:00 2001 From: Henri Bergius Date: Mon, 27 Jul 2026 09:13:58 -1000 Subject: [PATCH 1/8] Initial Seeed T1000-e port --- Boards.h | 71 ++++ Config.h | 2 +- Makefile | 17 +- Modem.h | 1 + RNode_Firmware.ino | 7 +- Utilities.h | 6 + boards/tracker-t1000-e.json | 59 +++ extra_script.py | 2 + lr11x0.cpp | 765 ++++++++++++++++++++++++++++++++++++ lr11x0.h | 166 ++++++++ platformio.ini | 33 ++ variants/t1000e/WVariant.h | 23 ++ variants/t1000e/variant.cpp | 108 +++++ variants/t1000e/variant.h | 163 ++++++++ 14 files changed, 1420 insertions(+), 3 deletions(-) create mode 100644 boards/tracker-t1000-e.json create mode 100644 lr11x0.cpp create mode 100644 lr11x0.h create mode 100644 variants/t1000e/WVariant.h create mode 100644 variants/t1000e/variant.cpp create mode 100644 variants/t1000e/variant.h diff --git a/Boards.h b/Boards.h index 89f1ecda..42b88bcb 100644 --- a/Boards.h +++ b/Boards.h @@ -115,6 +115,11 @@ #define MODEL_16 0x16 // T-Echo 433 MHz #define MODEL_17 0x17 // T-Echo 868/915 MHz + #define PRODUCT_T1000E 0x16 // Seeed Tracker T1000-E (nRF52840 + LR1110) + #define BOARD_T1000E 0x46 + #define MODEL_18 0x18 // T1000-E, 433 MHz + #define MODEL_19 0x19 // T1000-E, 868/915 MHz + #define PRODUCT_RAK4631 0x10 #define BOARD_RAK4631 0x51 #define MODEL_11 0x11 // RAK4631, 433 Mhz @@ -171,6 +176,8 @@ #define MODEM SX1262 #elif BOARD_MODEL == BOARD_GENERIC_NRF52 #define MODEM SX1262 + #elif BOARD_MODEL == BOARD_T1000E + #define MODEM LR1110 #else #define MODEM SX1276 #endif @@ -1053,6 +1060,70 @@ const int DISPLAY_BL_PIN = PIN_T114_TFT_BLGT; const int DISPLAY_RST = PIN_T114_TFT_RST; + #elif BOARD_MODEL == BOARD_T1000E + // Seeed Tracker T1000-E: nRF52840 + Semtech LR1110 (LoRa + Wi-Fi/GNSS + // geolocation, though only LoRa is used here). The RF front end has an + // on-chip RF switch driven by the LR1110's DIO5/DIO6/DIO7/DIO8 pins + // (configured via SET_DIO_AS_RF_SWITCH) and a TCXO supplied through DIO3 + // at 1.6 V. Pin map adapted from the MeshCore t1000-e variant. + #define MODEM LR1110 + #define HAS_EEPROM false + #define HAS_DISPLAY false + #define HAS_BLUETOOTH false + #define HAS_BLE true + #define HAS_CONSOLE false + #define HAS_PMU true + #define HAS_NP false + #define HAS_SD false + #define HAS_TCXO true + #define HAS_BUSY true + #define HAS_INPUT true + #define CONFIG_UART_BUFFER_SIZE 6144 + #define CONFIG_QUEUE_SIZE 6144 + #define CONFIG_QUEUE_MAX_LENGTH 200 + #define EEPROM_SIZE 296 + #define EEPROM_OFFSET EEPROM_SIZE-EEPROM_RESERVED + #define BLE_MANUFACTURER "Seeed Studio" + #define BLE_MODEL "T1000-E" + + // SPI (shares the Arduino SPI instance via Config.h spiModem on SPIM2). + const int pin_sclk = 11; // P0.11 + const int pin_mosi = 41; // P1.9 + const int pin_miso = 40; // P1.8 + + // LR1110 control pins. + const int pin_cs = 12; // P0.12 + const int pin_reset = 42; // P1.10 + const int pin_busy = 7; // P0.7 + const int pin_dio = 33; // P1.1 (LR1110 DIO1 = IRQ line) + const int pin_tcxo_enable = -1; // TCXO powered via DIO3 (SET_TCXO_MODE) + const int pin_led_rx = LED_GREEN; // P0.24 + const int pin_led_tx = LED_GREEN; + + // User button (P0.6). + const int pin_btn_usr1 = 6; + + // TCXO: 1.6 V on DIO3. The LR11x0 TCXO delay register unit is ~30.52 us; + // 5000 us -> ~164, matching the RadioLib default wakeup window. + #define LR11X0_TCXO_TUNE 0x00 // 1.6 V + #define LR11X0_TCXO_DELAY_US 5000 + + // On-chip RF switch (DIO5/DIO6/DIO7/DIO8). The 8-byte SET_DIO_AS_RF_SWITCH + // payload below is derived from the MeshCore RF-switch table: enable bits + // DIO5..DIO8, then a per-mode DIO drive config (which of DIO5..DIO8 are + // HIGH) for [stby, rx, tx, tx_hp, tx_hf, gnss, wifi]. + #define LR11X0_RFSWITCH_ENABLE 0x0F // DIO5 | DIO6 | DIO7 | DIO8 + #define LR11X0_RFSW_STBY 0x00 + #define LR11X0_RFSW_RX 0x09 // DIO5 + DIO8 + #define LR11X0_RFSW_TX 0x0B // DIO5 + DIO6 + DIO8 + #define LR11X0_RFSW_TX_HP 0x0A // DIO6 + DIO8 + #define LR11X0_RFSW_TX_HF 0x00 + #define LR11X0_RFSW_GNSS 0x04 // DIO7 + #define LR11X0_RFSW_WIFI 0x00 + + // RX boosted gain improves sensitivity on the T1000-E front end. + #define LR11X0_RX_BOOSTED 1 + #else #error An unsupported nRF board was selected. Cannot compile RNode firmware. #endif diff --git a/Config.h b/Config.h index 5f2dee9b..6b4c501c 100644 --- a/Config.h +++ b/Config.h @@ -69,7 +69,7 @@ #define eeprom_addr(a) (a+EEPROM_OFFSET) #define config_addr(a) (a+CONFIG_OFFSET) - #if (MODEM == SX1262 || MODEM == SX1280) && defined(NRF52840_XXAA) + #if (MODEM == SX1262 || MODEM == SX1280 || MODEM == LR1110) && defined(NRF52840_XXAA) SPIClass spiModem(NRF_SPIM2, pin_miso, pin_sclk, pin_mosi); #endif diff --git a/Makefile b/Makefile index 4e5e89cc..c8b8c5c2 100644 --- a/Makefile +++ b/Makefile @@ -148,6 +148,12 @@ firmware-heltec_t114: firmware-techo: arduino-cli compile --log --fqbn adafruit:nrf52:pca10056 -e --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x44\"" +# Seeed Tracker T1000-E (nRF52840 + Semtech LR1110). This board has no stock +# arduino-cli FQBN, so the build goes through PlatformIO using the custom +# board (boards/tracker-t1000-e.json) and variant (variants/t1000e/). +firmware-t1000e: + pio run -e seeed_t1000e + firmware-xiao_s3: arduino-cli compile --log --fqbn "esp32:esp32:XIAO_ESP32S3" -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x3E\"" @@ -270,6 +276,11 @@ upload-techo: @sleep 6 rnodeconf /dev/ttyACM0 --firmware-hash $$(./partition_hashes from_device /dev/ttyACM0) +upload-t1000e: + pio run -e seeed_t1000e -t upload + @sleep 6 + rnodeconf /dev/ttyACM0 --firmware-hash $$(./partition_hashes from_device /dev/ttyACM0) + upload-xiao_s3: arduino-cli upload -p /dev/ttyACM0 --fqbn esp32:esp32:XIAO_ESP32S3 @sleep 1 @@ -279,7 +290,7 @@ upload-xiao_s3: release: release-all -release-all: console-site spiffs-image release-tbeam release-tbeam_sx1262 release-lora32_v10 release-lora32_v20 release-lora32_v21 release-lora32_v10_extled release-lora32_v20_extled release-lora32_v21_extled release-lora32_v21_tcxo release-featheresp32 release-genericesp32 release-heltec32_v2 release-heltec32_v3 release-heltec32_v4 release-heltec32_v2_extled release-heltec_t114 release-techo release-rnode_ng_20 release-rnode_ng_21 release-t3s3 release-t3s3_sx127x release-t3s3_sx1280_pa release-tdeck release-tbeam_supreme release-rak4631 release-xiao_s3 release-hashes +release-all: console-site spiffs-image release-tbeam release-tbeam_sx1262 release-lora32_v10 release-lora32_v20 release-lora32_v21 release-lora32_v10_extled release-lora32_v20_extled release-lora32_v21_extled release-lora32_v21_tcxo release-featheresp32 release-genericesp32 release-heltec32_v2 release-heltec32_v3 release-heltec32_v4 release-heltec32_v2_extled release-heltec_t114 release-techo release-rnode_ng_20 release-rnode_ng_21 release-t3s3 release-t3s3_sx127x release-t3s3_sx1280_pa release-tdeck release-tbeam_supreme release-rak4631 release-xiao_s3 release-t1000e release-hashes release-hashes: python ./release_hashes.py > ./Release/release.json @@ -507,6 +518,10 @@ release-techo: cp build/adafruit.nrf52.pca10056/RNode_Firmware.ino.hex build/rnode_firmware_techo.hex adafruit-nrfutil dfu genpkg --dev-type 0x0052 --application build/rnode_firmware_techo.hex Release/rnode_firmware_techo.zip +release-t1000e: + pio run -e seeed_t1000e + cp .pio/build/seeed_t1000e/firmware.zip Release/rnode_firmware_t1000e.zip + release-xiao_s3: arduino-cli compile --fqbn "esp32:esp32:XIAO_ESP32S3" -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x3E\"" cp ~/.arduino15/packages/esp32/hardware/esp32/$(ARDUINO_ESP_CORE_VER)/tools/partitions/boot_app0.bin build/rnode_firmware_xiao_esp32s3.boot_app0 diff --git a/Modem.h b/Modem.h index a8228721..3bcb7b3a 100644 --- a/Modem.h +++ b/Modem.h @@ -2,6 +2,7 @@ #define SX1278 0x02 #define SX1262 0x03 #define SX1280 0x04 +#define LR1110 0x05 // Sentinel for native targets where the modem family is chosen at runtime // from rnoded.conf. Drivers and MODEM-gated call sites use this to diff --git a/RNode_Firmware.ino b/RNode_Firmware.ino index eafd665a..359fa3ba 100644 --- a/RNode_Firmware.ino +++ b/RNode_Firmware.ino @@ -681,6 +681,8 @@ void setup() { LoRa->setPins(pin_cs, pin_reset, pin_dio, pin_busy, pin_rxen); #elif MODEM == SX1280 LoRa->setPins(pin_cs, pin_reset, pin_dio, pin_busy, pin_rxen, pin_txen); + #elif MODEM == LR1110 + LoRa->setPins(pin_cs, pin_reset, pin_dio, pin_busy); #endif #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52 || MCU_VARIANT == MCU_NATIVE @@ -1596,7 +1598,7 @@ void add_airtime(uint16_t written) { lora_symbols += lora_preamble_symbols + 0.25 + 8; packet_cost_ms += lora_symbols * lora_symbol_time_ms; - #elif MODEM == SX1262 || MODEM == SX1280 + #elif MODEM == SX1262 || MODEM == SX1280 || MODEM == LR1110 if (lora_sf < 7) { lora_symbols += (8*written + PHY_CRC_LORA_BITS - 4*lora_sf + PHY_HEADER_LORA_SYMBOLS); lora_symbols /= 4*lora_sf; @@ -1845,6 +1847,9 @@ void serial_callback(uint8_t sbyte) { #else if (txp > 13) txp = 13; #endif + #elif MODEM == LR1110 + // LR1110 high-power PA tops out at 22 dBm. + if (txp > 22) txp = 22; #else if (txp > 17) txp = 17; #endif diff --git a/Utilities.h b/Utilities.h index 875b95fb..27c7a70d 100644 --- a/Utilities.h +++ b/Utilities.h @@ -58,6 +58,9 @@ sx127x *LoRa = &sx127x_modem; #elif MODEM == SX1280 #include "sx128x.h" sx128x *LoRa = &sx128x_modem; +#elif MODEM == LR1110 +#include "lr11x0.h" +lr11x0 *LoRa = &lr11x0_modem; #endif #include "ROM.h" @@ -1484,6 +1487,9 @@ void setTXPower() { if (model == MODEL_E3) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN); if (model == MODEL_E8) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN); + if (model == MODEL_18) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN); + if (model == MODEL_19) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN); + if (model == MODEL_FE) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN); if (model == MODEL_FF) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_RFO_PIN); } diff --git a/boards/tracker-t1000-e.json b/boards/tracker-t1000-e.json new file mode 100644 index 00000000..1552de9d --- /dev/null +++ b/boards/tracker-t1000-e.json @@ -0,0 +1,59 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v7.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_NRF52840_FEATHER -DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + ["0x239A", "0x8029"], + ["0x239A", "0x0029"], + ["0x239A", "0x002A"], + ["0x239A", "0x802A"] + ], + "usb_product": "T1000-E-BOOT", + "mcu": "nrf52840", + "variant": "t1000e", + "bsp": { + "name": "adafruit" + }, + "softdevice": { + "sd_flags": "-DS140", + "sd_name": "s140", + "sd_version": "7.3.0", + "sd_fwid": "0x0123" + }, + "bootloader": { + "settings_addr": "0xFF000" + } + }, + "connectivity": ["bluetooth"], + "debug": { + "jlink_device": "nRF52840_xxAA", + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52.cfg" + }, + "frameworks": ["arduino"], + "name": "Seeed Tracker T1000-E", + "upload": { + "maximum_ram_size": 235520, + "maximum_size": 811008, + "speed": 115200, + "protocol": "nrfutil", + "protocols": [ + "jlink", + "nrfjprog", + "nrfutil", + "stlink", + "cmsis-dap", + "blackmagic" + ], + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true + }, + "url": "https://www.seeedstudio.com/SenseCAP-Card-Tracker-T1000-E-for-Meshtastic-p-5913.html", + "vendor": "Seeed Studio" +} diff --git a/extra_script.py b/extra_script.py index d5bda728..db612c36 100644 --- a/extra_script.py +++ b/extra_script.py @@ -162,6 +162,8 @@ def device_provision(env): env.Execute("rnodeconf --product 10 --model 14 --hwrev 1 --rom " + env.subst("$UPLOAD_PORT")) case "techo" | "techo_local": env.Execute("rnodeconf --product 15 --model 17 --hwrev 1 --rom " + env.subst("$UPLOAD_PORT")) + case "t1000e": + env.Execute("rnodeconf --product 16 --model 19 --hwrev 1 --rom " + env.subst("$UPLOAD_PORT")) case "heltec_t114" | "heltec_t114_local": env.Execute("rnodeconf --product c2 --model c7 --hwrev 1 --rom " + env.subst("$UPLOAD_PORT")) case _: diff --git a/lr11x0.cpp b/lr11x0.cpp new file mode 100644 index 00000000..107e83a9 --- /dev/null +++ b/lr11x0.cpp @@ -0,0 +1,765 @@ +// lr11x0.cpp - LoRa driver for the Semtech LR11x0 family (LR1110/LR1120/LR1121). +// +// Implements the ILoRaRadio interface against the LR11x0 SPI command set. +// See lr11x0.h for the protocol overview. The public API mirrors sx126x +// one-for-one so the firmware's radio-dispatch code can treat it uniformly. +// +// Ported from the RadioLib LR11x0 reference (jgromes/RadioLib) and the +// MeshCore CustomLR1110 wrapper (Seeed T1000-E). +// +// Licensed under the MIT license. + +#include "Boards.h" + +#if MODEM == LR1110 +#include "lr11x0.h" +#include + +#if MCU_VARIANT == MCU_ESP32 + #include "driver/gpio.h" + #define ISR_VECT IRAM_ATTR +#else + #define ISR_VECT +#endif + +// LR11x0 SPI commands (16-bit, MSB first). +#define LR11X0_CMD_NOP 0x0000 +#define LR11X0_CMD_GET_STATUS 0x0100 +#define LR11X0_CMD_GET_VERSION 0x0101 +#define LR11X0_CMD_CLEAR_ERRORS 0x010E +#define LR11X0_CMD_CALIBRATE 0x010F +#define LR11X0_CMD_SET_REG_MODE 0x0110 +#define LR11X0_CMD_CALIB_IMAGE 0x0111 +#define LR11X0_CMD_SET_DIO_AS_RF_SWITCH 0x0112 +#define LR11X0_CMD_SET_DIO_IRQ_PARAMS 0x0113 +#define LR11X0_CMD_CLEAR_IRQ 0x0114 +#define LR11X0_CMD_SET_TCXO_MODE 0x0117 +#define LR11X0_CMD_SET_SLEEP 0x011B +#define LR11X0_CMD_SET_STANDBY 0x011C +#define LR11X0_CMD_GET_RX_BUFFER_STATUS 0x0203 +#define LR11X0_CMD_GET_PACKET_STATUS 0x0204 +#define LR11X0_CMD_GET_RSSI_INST 0x0205 +#define LR11X0_CMD_SET_RX 0x0209 +#define LR11X0_CMD_SET_TX 0x020A +#define LR11X0_CMD_SET_RF_FREQUENCY 0x020B +#define LR11X0_CMD_SET_RX_TX_FALLBACK_MODE 0x0213 +#define LR11X0_CMD_SET_PACKET_TYPE 0x020E +#define LR11X0_CMD_SET_MODULATION_PARAMS 0x020F +#define LR11X0_CMD_SET_PACKET_PARAMS 0x0210 +#define LR11X0_CMD_SET_TX_PARAMS 0x0211 +#define LR11X0_CMD_SET_PA_CONFIG 0x0215 +#define LR11X0_CMD_SET_RX_BOOSTED 0x0227 +#define LR11X0_CMD_SET_LORA_SYNC_WORD 0x022B +#define LR11X0_CMD_WRITE_BUFFER 0x0109 +#define LR11X0_CMD_READ_BUFFER 0x010A + +// Packet types. +#define LR11X0_PACKET_TYPE_LORA 0x02 + +// LoRa bandwidth codes. +#define LR11X0_LORA_BW_62_5 0x03 +#define LR11X0_LORA_BW_125_0 0x04 +#define LR11X0_LORA_BW_250_0 0x05 +#define LR11X0_LORA_BW_500_0 0x06 + +// Coding rate (short interleaver). +#define LR11X0_LORA_CR_4_5 0x01 +#define LR11X0_LORA_CR_4_6 0x02 +#define LR11X0_LORA_CR_4_7 0x03 +#define LR11X0_LORA_CR_4_8 0x04 + +// Low data rate optimization. +#define LR11X0_LORA_LDRO_DISABLED 0x00 +#define LR11X0_LORA_LDRO_ENABLED 0x01 + +// Header / CRC. +#define LR11X0_LORA_HEADER_EXPLICIT 0x00 +#define LR11X0_LORA_HEADER_IMPLICIT 0x01 +#define LR11X0_LORA_CRC_DISABLED 0x00 +#define LR11X0_LORA_CRC_ENABLED 0x01 + +// Sync word. +#define LR11X0_LORA_SYNC_WORD_PRIVATE 0x12 + +// Misc. +#define LR11X0_PACKET_TYPE_LORA_VAL 0x02 +#define LR11X0_STDBY_RC 0x00 +#define LR11X0_REG_MODE_LDO 0x00 +#define LR11X0_FALLBACK_STBY_RC 0x01 +#define LR11X0_PA_RAMP_48U 0x02 +#define LR11X0_CALIBRATE_ALL 0x3F +#define LR11X0_TCXO_1_6V 0x00 + +// IRQ bits. +#define LR11X0_IRQ_TX_DONE (1UL << 2) +#define LR11X0_IRQ_RX_DONE (1UL << 3) +#define LR11X0_IRQ_PREAMBLE_DETECTED (1UL << 4) +#define LR11X0_IRQ_SYNC_WORD_HEADER_VALID (1UL << 5) +#define LR11X0_IRQ_HEADER_ERR (1UL << 6) +#define LR11X0_IRQ_CRC_ERR (1UL << 7) +#define LR11X0_IRQ_TIMEOUT (1UL << 10) +#define LR11X0_IRQ_ALL 0x1BF80FFCUL + +// Status register: GET_VERSION returns [hw, device, fwMajor, fwMinor]. +#define LR11X0_DEVICE_LR1110 0x01 + +// SPI / PLL constants. Frequency is rfFreq = freq_Hz (the LR11x0 takes the +// frequency directly in Hz — unlike the SX126x which divides by FREQ_STEP). +// (RadioLib: setRfFrequency((uint32_t)(freq*1000000.0f)).) +#define LR11X0_MAX_PKT_LENGTH 255 +#define LR11X0_RX_TIMEOUT_INF 0xFFFFFFUL +#define LR11X0_TX_TIMEOUT_NONE 0x000000UL + +#if defined(NRF52840_XXAA) + extern SPIClass spiModem; + #define SPI spiModem +#endif + +extern SPIClass SPI; + +#define MAX_PKT_LENGTH 255 + +// Preamble/header timing shared with the firmware's CSMA/airtime logic. +extern long lora_preamble_time_ms; +extern long lora_header_time_ms; +extern bool lora_low_datarate; + +lr11x0::lr11x0() : + _spiSettings(8E6, MSBFIRST, SPI_MODE0), + _ss(LORA_DEFAULT_SS_PIN), + _reset(LORA_DEFAULT_RESET_PIN), + _dio0(LORA_DEFAULT_DIO0_PIN), + _busy(LORA_DEFAULT_BUSY_PIN), + _frequency(0), + _txp(0), + _sf(0x07), + _bw(LR11X0_LORA_BW_125_0), + _cr(LR11X0_LORA_CR_4_5), + _ldro(0x00), + _packetIndex(0), + _preambleLength(18), + _implicitHeaderMode(0), + _payloadLength(255), + _crcMode(1), + _rxBufOffset(0), + _packet{0}, + _txLen(0), + _preinit_done(false), + _dio0_pending(false), + _onReceive(NULL) +{ setTimeout(0); } + +// ---- Low-level SPI primitives ------------------------------------------------ + +void lr11x0::waitOnBusy() { + if (_busy == -1) { return; } + unsigned long start = millis(); + #if MCU_VARIANT == MCU_ESP32 + while (gpio_get_level((gpio_num_t)_busy) == HIGH) { + #else + while (digitalRead(_busy) == HIGH) { + #endif + if (millis() >= (start + 100)) { break; } + } +} + +// Write command: single transaction [cmd_hi, cmd_lo, data...]. +void lr11x0::writeCommand(uint16_t cmd, const uint8_t *data, uint8_t len) { + waitOnBusy(); + digitalWrite(_ss, LOW); + SPI.beginTransaction(_spiSettings); + SPI.transfer(cmd >> 8); + SPI.transfer(cmd & 0xFF); + for (uint8_t i = 0; i < len; i++) { SPI.transfer(data[i]); } + SPI.endTransaction(); + digitalWrite(_ss, HIGH); +} + +// Read command: two transactions. +// phase 1: [cmd_hi, cmd_lo, request...] +// phase 2: clock out [status_byte, data...] (status discarded) +void lr11x0::readCommand(uint16_t cmd, const uint8_t *req, uint8_t reqlen, uint8_t *out, uint8_t outlen) { + waitOnBusy(); + digitalWrite(_ss, LOW); + SPI.beginTransaction(_spiSettings); + SPI.transfer(cmd >> 8); + SPI.transfer(cmd & 0xFF); + for (uint8_t i = 0; i < reqlen; i++) { SPI.transfer(req[i]); } + SPI.endTransaction(); + digitalWrite(_ss, HIGH); + + waitOnBusy(); + + digitalWrite(_ss, LOW); + SPI.beginTransaction(_spiSettings); + SPI.transfer(0x00); // status byte slot + for (uint8_t i = 0; i < outlen; i++) { out[i] = SPI.transfer(0x00); } + SPI.endTransaction(); + digitalWrite(_ss, HIGH); +} + +// Poll the always-available status frame: 6 bytes [stat1, stat2, irq(4 BE)]. +// No command byte is sent — the chip streams status while NSS is low. +void lr11x0::readStatusFrame(uint8_t *out6) { + waitOnBusy(); + digitalWrite(_ss, LOW); + SPI.beginTransaction(_spiSettings); + for (uint8_t i = 0; i < 6; i++) { out6[i] = SPI.transfer(0x00); } + SPI.endTransaction(); + digitalWrite(_ss, HIGH); +} + +uint16_t lr11x0::getIrqStatus() { + uint8_t buf[6]; + readStatusFrame(buf); + // The status frame is [stat1, stat2, irq_b3, irq_b2, irq_b1, irq_b0] + // (IRQ big-endian). Every IRQ bit the firmware acts on lives in the low + // 16 bits (bits 2..11), so return just those: byte 4 (15..8) | byte 5 (7..0). + return (uint16_t)((((uint16_t)buf[4] << 8) | (uint16_t)buf[5])); +} + +void lr11x0::setDioIrqParams(uint32_t irq, uint32_t dioMask) { + uint8_t buf[8] = { + (uint8_t)(irq >> 24), (uint8_t)(irq >> 16), (uint8_t)(irq >> 8), (uint8_t)(irq), + (uint8_t)(dioMask >> 24), (uint8_t)(dioMask >> 16), (uint8_t)(dioMask >> 8), (uint8_t)(dioMask), + }; + writeCommand(LR11X0_CMD_SET_DIO_IRQ_PARAMS, buf, 8); +} + +void lr11x0::clearIrq(uint32_t irq) { + uint8_t buf[4] = { (uint8_t)(irq >> 24), (uint8_t)(irq >> 16), (uint8_t)(irq >> 8), (uint8_t)(irq) }; + writeCommand(LR11X0_CMD_CLEAR_IRQ, buf, 4); +} + +void lr11x0::setPacketType(uint8_t type) { + writeCommand(LR11X0_CMD_SET_PACKET_TYPE, &type, 1); +} + +void lr11x0::setModulationParams(uint8_t sf, uint8_t bw, uint8_t cr, uint8_t ldro) { + uint8_t buf[4] = { sf, bw, cr, ldro }; + writeCommand(LR11X0_CMD_SET_MODULATION_PARAMS, buf, 4); +} + +void lr11x0::setPacketParams(uint16_t preambleLen, uint8_t hdrType, uint8_t payloadLen, uint8_t crcType) { + uint8_t buf[6] = { + (uint8_t)(preambleLen >> 8), (uint8_t)(preambleLen & 0xFF), + hdrType, payloadLen, crcType, 0x00 // standard IQ (no inversion) + }; + writeCommand(LR11X0_CMD_SET_PACKET_PARAMS, buf, 6); +} + +void lr11x0::setRfFrequency(uint32_t frf) { + uint8_t buf[4] = { (uint8_t)(frf >> 24), (uint8_t)(frf >> 16), (uint8_t)(frf >> 8), (uint8_t)(frf) }; + writeCommand(LR11X0_CMD_SET_RF_FREQUENCY, buf, 4); +} + +void lr11x0::setPaConfig(uint8_t paSel, uint8_t regPaSupply, uint8_t paDutyCycle, uint8_t paHpSel) { + uint8_t buf[4] = { paSel, regPaSupply, paDutyCycle, paHpSel }; + writeCommand(LR11X0_CMD_SET_PA_CONFIG, buf, 4); +} + +void lr11x0::setTxParams(int8_t pwr, uint8_t ramp) { + uint8_t buf[2] = { (uint8_t)pwr, ramp }; + writeCommand(LR11X0_CMD_SET_TX_PARAMS, buf, 2); +} + +void lr11x0::setTcxoMode(uint8_t tune, uint32_t delay) { + uint8_t buf[4] = { tune, (uint8_t)(delay >> 16), (uint8_t)(delay >> 8), (uint8_t)(delay) }; + writeCommand(LR11X0_CMD_SET_TCXO_MODE, buf, 4); +} + +void lr11x0::setRegMode(uint8_t mode) { + writeCommand(LR11X0_CMD_SET_REG_MODE, &mode, 1); +} + +void lr11x0::setStandby(uint8_t mode) { + writeCommand(LR11X0_CMD_SET_STANDBY, &mode, 1); +} + +void lr11x0::calibrate(uint8_t params) { + writeCommand(LR11X0_CMD_CALIBRATE, ¶ms, 1); +} + +void lr11x0::calibrateImageRejection(uint32_t frequency) { + // LR11x0 takes two bytes: freqMin and freqMax in units of ~4 MHz. + // RadioLib: floor((freqMin-1)/4), ceil((freqMax+1)/4) with the band ±band MHz. + float mhz = frequency / 1.0e6f; + uint8_t lo = (uint8_t)floor((mhz - 5.0f) / 4.0f); + uint8_t hi = (uint8_t)ceil((mhz + 5.0f) / 4.0f); + uint8_t buf[2] = { lo, hi }; + writeCommand(LR11X0_CMD_CALIB_IMAGE, buf, 2); +} + +void lr11x0::setRxBoostedGainMode(bool en) { + uint8_t buf[1] = { (uint8_t)en }; + writeCommand(LR11X0_CMD_SET_RX_BOOSTED, buf, 1); +} + +// ---- Init -------------------------------------------------------------------- + +bool lr11x0::preInit() { + pinMode(_ss, OUTPUT); + digitalWrite(_ss, HIGH); + + #if defined(NRF52840_XXAA) + SPI.setPins(pin_miso, pin_sclk, pin_mosi); + SPI.begin(); + #else + SPI.begin(); + #endif + + // Reset the chip so it lands in a known state. + reset(); + + // Read the version and confirm an LR1110 is present. + uint8_t ver[4] = { 0 }; + readCommand(LR11X0_CMD_GET_VERSION, NULL, 0, ver, 4); + // ver = [hw, device, fwMajor, fwMinor]; device 0x01 == LR1110. + if (ver[1] != LR11X0_DEVICE_LR1110) { + return false; + } + + _preinit_done = true; + return true; +} + +void lr11x0::enableTcxo() { + #if defined(LR11X0_TCXO_TUNE) + // TCXO wakeup delay register unit is ~30.52 us (RadioLib: delay/30.52). + uint32_t delayUs = LR11X0_TCXO_DELAY_US; + uint32_t delayVal = (uint32_t)((float)delayUs / 30.52f); + if (delayVal == 0) { delayVal = 1; } + setTcxoMode(LR11X0_TCXO_TUNE, delayVal); + #endif +} + +void lr11x0::configRfSwitch() { + #if defined(LR11X0_RFSWITCH_ENABLE) + // The board (Boards.h) supplies the 8-byte SET_DIO_AS_RF_SWITCH payload: + // enable bitmask followed by 7 per-mode DIO drive configs + // (stby, rx, tx, tx_hp, tx_hf, gnss, wifi). + uint8_t buf[8] = { + LR11X0_RFSWITCH_ENABLE, + LR11X0_RFSW_STBY, LR11X0_RFSW_RX, LR11X0_RFSW_TX, + LR11X0_RFSW_TX_HP, LR11X0_RFSW_TX_HF, LR11X0_RFSW_GNSS, LR11X0_RFSW_WIFI + }; + writeCommand(LR11X0_CMD_SET_DIO_AS_RF_SWITCH, buf, 8); + #endif +} + +int lr11x0::begin(uint32_t frequency) { + if (_busy != -1) { pinMode(_busy, INPUT); } + if (!_preinit_done) { if (!preInit()) { return false; } } + + // Clear any pending errors and IRQs from a prior session. + writeCommand(LR11X0_CMD_CLEAR_ERRORS, NULL, 0); + clearIrq(LR11X0_IRQ_ALL); + + // Standby (RC) is required before most configuration commands. + setStandby(LR11X0_STDBY_RC); + + // Regulator mode: LDO (matches the RadioLib/MeshCore default path). + setRegMode(LR11X0_REG_MODE_LDO); + + // Enable TCXO (DIO3) if the board defines a voltage. + enableTcxo(); + + // RX/Tx fallback to STDBY_RC after each transaction. + uint8_t fb = LR11X0_FALLBACK_STBY_RC; + writeCommand(LR11X0_CMD_SET_RX_TX_FALLBACK_MODE, &fb, 1); + + // Route the on-chip RF switch (DIO5..DIO8) if the board has one. + configRfSwitch(); + + // Select LoRa packet type. + setPacketType(LR11X0_PACKET_TYPE_LORA); + + // Calibrate all blocks, then image rejection for the operating band. + calibrate(LR11X0_CALIBRATE_ALL); + delay(5); + waitOnBusy(); + calibrateImageRejection(frequency); + + // Private LoRa network sync word (0x12) — matches RNode/SX126x default. + uint8_t sw = LR11X0_LORA_SYNC_WORD_PRIVATE; + writeCommand(LR11X0_CMD_SET_LORA_SYNC_WORD, &sw, 1); + + setFrequency(frequency); + setTxPower(2); + enableCrc(); + + // RX boosted gain improves sensitivity; the T1000-E front end expects it. + #if defined(LR11X0_RX_BOOSTED) + setRxBoostedGainMode(LR11X0_RX_BOOSTED); + #endif + + setModulationParams(_sf, _bw, _cr, _ldro); + setPacketParams(_preambleLength, _implicitHeaderMode, _payloadLength, _crcMode); + + return 1; +} + +void lr11x0::end() { sleep(); SPI.end(); _preinit_done = false; } + +void lr11x0::reset(void) { + if (_reset != -1) { + pinMode(_reset, OUTPUT); + digitalWrite(_reset, LOW); + delay(10); + digitalWrite(_reset, HIGH); + // The LR1110 typically needs ~273 ms after NRESET release. + delay(300); + } +} + +void lr11x0::standby() { setStandby(LR11X0_STDBY_RC); } + +void lr11x0::sleep() { + // retainConfig=1 (bit0), sleepTime=0. Keep config so a wake NSS toggle + // restores the radio without a full re-init. + uint8_t buf[5] = { 0x01, 0x00, 0x00, 0x00, 0x00 }; + writeCommand(LR11X0_CMD_SET_SLEEP, buf, 5); +} + +// ---- PHY parameter setters --------------------------------------------------- + +void lr11x0::setFrequency(uint32_t frequency) { + _frequency = frequency; + setRfFrequency(frequency); // LR11x0 takes the frequency directly in Hz. +} + +uint32_t lr11x0::getFrequency() { return _frequency; } + +void lr11x0::setSpreadingFactor(int sf) { + if (sf < 5) { sf = 5; } + else if (sf > 12) { sf = 12; } + _sf = sf; + handleLowDataRate(); + setModulationParams(_sf, _bw, _cr, _ldro); +} + +uint32_t lr11x0::getSignalBandwidth() { + switch (_bw) { + case LR11X0_LORA_BW_62_5: return 62500; + case LR11X0_LORA_BW_125_0: return 125000; + case LR11X0_LORA_BW_250_0: return 250000; + case LR11X0_LORA_BW_500_0: return 500000; + } + return 0; +} + +void lr11x0::handleLowDataRate() { + // Enable LDRO when the symbol time exceeds 16 ms (RadioLib rule). + if (long((1UL << _sf) / (getSignalBandwidth() / 1000)) > 16) { + _ldro = LR11X0_LORA_LDRO_ENABLED; lora_low_datarate = true; + } else { + _ldro = LR11X0_LORA_LDRO_DISABLED; lora_low_datarate = false; + } +} + +void lr11x0::setSignalBandwidth(uint32_t sbw) { + if (sbw <= 62500) { _bw = LR11X0_LORA_BW_62_5; } + else if (sbw <= 125000) { _bw = LR11X0_LORA_BW_125_0; } + else if (sbw <= 250000) { _bw = LR11X0_LORA_BW_250_0; } + else { _bw = LR11X0_LORA_BW_500_0; } + handleLowDataRate(); + setModulationParams(_sf, _bw, _cr, _ldro); +} + +void lr11x0::setCodingRate4(int denominator) { + if (denominator < 5) { denominator = 5; } + else if (denominator > 8) { denominator = 8; } + // LR11x0 short-interleaver codes: 4/5=0x01 .. 4/8=0x04. + _cr = (uint8_t)(denominator - 4); + setModulationParams(_sf, _bw, _cr, _ldro); +} + +void lr11x0::setPreambleLength(long preamble_symbols) { + _preambleLength = preamble_symbols; + setPacketParams(_preambleLength, _implicitHeaderMode, _payloadLength, _crcMode); +} + +void lr11x0::enableCrc() { _crcMode = 1; setPacketParams(_preambleLength, _implicitHeaderMode, _payloadLength, LR11X0_LORA_CRC_ENABLED); } +void lr11x0::disableCrc() { _crcMode = 0; setPacketParams(_preambleLength, _implicitHeaderMode, _payloadLength, LR11X0_LORA_CRC_DISABLED); } +void lr11x0::explicitHeaderMode() { _implicitHeaderMode = LR11X0_LORA_HEADER_EXPLICIT; setPacketParams(_preambleLength, _implicitHeaderMode, _payloadLength, _crcMode); } +void lr11x0::implicitHeaderMode() { _implicitHeaderMode = LR11X0_LORA_HEADER_IMPLICIT; setPacketParams(_preambleLength, _implicitHeaderMode, _payloadLength, _crcMode); } + +void lr11x0::setTxPower(int level, int outputPin) { + (void)outputPin; // PA selection is internal on the LR11x0. + + if (level > 22) { level = 22; } + else if (level < -9) { level = -9; } + + // High-power PA for >14 dBm, low-power PA otherwise (RadioLib LR1110 rule). + bool useHp = (level > 14); + setPaConfig((uint8_t)useHp, (uint8_t)useHp, 0x04, 0x07); + setTxParams((int8_t)level, LR11X0_PA_RAMP_48U); + _txp = level; +} + +uint8_t lr11x0::getTxPower() { return _txp; } + +void lr11x0::setPins(int ss, int reset, int dio0, int busy) { + _ss = ss; + _reset = reset; + _dio0 = dio0; + _busy = busy; +} + +void lr11x0::setSPIFrequency(uint32_t frequency) { _spiSettings = SPISettings(frequency, MSBFIRST, SPI_MODE0); } + +byte lr11x0::random() { + // GET_RSSI_INST doubles as a quick SPI round-trip; for true entropy the + // LR11x0 has a dedicated RNG opcode, but the firmware only uses this for + // a per-packet random header nibble where any varying byte suffices. + uint8_t b = 0; + uint8_t irq[6]; + readStatusFrame(irq); + b = irq[5] ^ irq[2] ^ (uint8_t)micros(); + return b; +} + +// ---- TX ---------------------------------------------------------------------- + +int lr11x0::beginPacket(int implicitHeader) { + standby(); + if (implicitHeader) { implicitHeaderMode(); } + else { explicitHeaderMode(); } + + _payloadLength = 0; + _txLen = 0; + return 1; +} + +size_t lr11x0::write(uint8_t byte) { return write(&byte, sizeof(byte)); } + +size_t lr11x0::write(const uint8_t *buffer, size_t size) { + // The firmware writes packets a byte at a time; the LR11x0 WRITE_BUFFER + // command has no auto-increment address exposed via this driver's API, so + // stage the whole payload here and flush it in endPacket(). + for (size_t i = 0; i < size; i++) { + if (_payloadLength >= MAX_PKT_LENGTH) { break; } + _txBuf[_payloadLength++] = buffer[i]; + } + _txLen = _payloadLength; + return size; +} + +int lr11x0::endPacket() { + // Commit the staged payload length, then write the whole buffer in one go. + setPacketParams(_preambleLength, _implicitHeaderMode, _txLen, _crcMode); + + waitOnBusy(); + digitalWrite(_ss, LOW); + SPI.beginTransaction(_spiSettings); + SPI.transfer(LR11X0_CMD_WRITE_BUFFER >> 8); + SPI.transfer(LR11X0_CMD_WRITE_BUFFER & 0xFF); + for (uint8_t i = 0; i < _txLen; i++) { SPI.transfer(_txBuf[i]); } + SPI.endTransaction(); + digitalWrite(_ss, HIGH); + + setDioIrqParams(LR11X0_IRQ_TX_DONE | LR11X0_IRQ_TIMEOUT, + LR11X0_IRQ_TX_DONE | LR11X0_IRQ_TIMEOUT); + clearIrq(LR11X0_IRQ_ALL); + + // Single TX mode with no timeout (the firmware polls the IRQ). + uint8_t timeout[3] = { 0, 0, 0 }; + writeCommand(LR11X0_CMD_SET_TX, timeout, 3); + + // Wait for BUSY to drop (PA ramp-up done), then poll TX_DONE. + waitOnBusy(); + uint32_t w_timeout = millis() + LORA_MODEM_TIMEOUT_MS; + bool timed_out = true; + while (millis() < w_timeout) { + if (getIrqStatus() & LR11X0_IRQ_TX_DONE) { timed_out = false; break; } + yield(); + } + clearIrq(LR11X0_IRQ_ALL); + standby(); + return timed_out ? 0 : 1; +} + +// ---- RX ---------------------------------------------------------------------- + +void lr11x0::onReceive(void(*callback)(int)) { + _onReceive = callback; + if (callback) { + pinMode(_dio0, INPUT); + // Route RX_DONE (+ timeout) to the IRQ/DIO1 pin. + setDioIrqParams(LR11X0_IRQ_RX_DONE | LR11X0_IRQ_TIMEOUT, + LR11X0_IRQ_RX_DONE | LR11X0_IRQ_TIMEOUT); + #if MCU_VARIANT != MCU_ESP32 && MCU_VARIANT != MCU_NRF52 && MCU_VARIANT != MCU_NATIVE + #ifdef SPI_HAS_NOTUSINGINTERRUPT + SPI.usingInterrupt(digitalPinToInterrupt(_dio0)); + #endif + #endif + attachInterrupt(digitalPinToInterrupt(_dio0), lr11x0::onDio0Rise, RISING); + } else { + detachInterrupt(digitalPinToInterrupt(_dio0)); + #if MCU_VARIANT != MCU_ESP32 && MCU_VARIANT != MCU_NRF52 && MCU_VARIANT != MCU_NATIVE + #ifdef SPI_HAS_NOTUSINGINTERRUPT + SPI.notUsingInterrupt(digitalPinToInterrupt(_dio0)); + #endif + #endif + } +} + +void lr11x0::receive(int size) { + if (size > 0) { + implicitHeaderMode(); + _payloadLength = size; + setPacketParams(_preambleLength, _implicitHeaderMode, (uint8_t)_payloadLength, _crcMode); + } else { + explicitHeaderMode(); + } + + setDioIrqParams(LR11X0_IRQ_RX_DONE | LR11X0_IRQ_TIMEOUT, + LR11X0_IRQ_RX_DONE | LR11X0_IRQ_TIMEOUT); + clearIrq(LR11X0_IRQ_ALL); + + // Continuous RX (infinite timeout). + uint8_t timeout[3] = { 0xFF, 0xFF, 0xFF }; + writeCommand(LR11X0_CMD_SET_RX, timeout, 3); +} + +int ISR_VECT lr11x0::available() { + uint8_t buf[2] = { 0 }; + readCommand(LR11X0_CMD_GET_RX_BUFFER_STATUS, NULL, 0, buf, 2); + _rxBufOffset = buf[1]; + return buf[0] - _packetIndex; +} + +int ISR_VECT lr11x0::read() { + if (!available()) { return -1; } + if (_packetIndex == 0) { + uint8_t status[2] = { 0 }; + readCommand(LR11X0_CMD_GET_RX_BUFFER_STATUS, NULL, 0, status, 2); + int pktLen = status[0]; + _rxBufOffset = status[1]; + // READ_BUFFER request: [offset, length], then read `length` payload bytes. + uint8_t req[2] = { _rxBufOffset, (uint8_t)pktLen }; + readCommand(LR11X0_CMD_READ_BUFFER, req, 2, _packet, (uint8_t)pktLen); + } + uint8_t byte = _packet[_packetIndex]; + _packetIndex++; + return byte; +} + +int lr11x0::peek() { + if (!available()) { return -1; } + if (_packetIndex == 0) { + uint8_t status[2] = { 0 }; + readCommand(LR11X0_CMD_GET_RX_BUFFER_STATUS, NULL, 0, status, 2); + int pktLen = status[0]; + _rxBufOffset = status[1]; + uint8_t req[2] = { _rxBufOffset, (uint8_t)pktLen }; + readCommand(LR11X0_CMD_READ_BUFFER, req, 2, _packet, (uint8_t)pktLen); + } + return _packet[_packetIndex]; +} + +void lr11x0::flush() { } + +// ---- Signal metrics ---------------------------------------------------------- + +int ISR_VECT lr11x0::currentRssi() { + uint8_t b = 0; + readCommand(LR11X0_CMD_GET_RSSI_INST, NULL, 0, &b, 1); + return -(int)b / 2; +} + +int ISR_VECT lr11x0::packetRssi() { + uint8_t buf[3] = { 0 }; + readCommand(LR11X0_CMD_GET_PACKET_STATUS, NULL, 0, buf, 3); + return -(int)buf[0] / 2; +} + +int ISR_VECT lr11x0::packetRssi(uint8_t pkt_snr_raw) { + (void)pkt_snr_raw; + return packetRssi(); +} + +uint8_t ISR_VECT lr11x0::packetSnrRaw() { + uint8_t buf[3] = { 0 }; + readCommand(LR11X0_CMD_GET_PACKET_STATUS, NULL, 0, buf, 3); + return buf[1]; +} + +float lr11x0::packetSnr() { + uint8_t buf[3] = { 0 }; + readCommand(LR11X0_CMD_GET_PACKET_STATUS, NULL, 0, buf, 3); + return (float)((int8_t)buf[1]) * 0.25; +} + +// ---- DCD / carrier detect ---------------------------------------------------- + +bool lr11x0::dcd() { + // The LR11x0 exposes preamble-detect and header-valid as IRQ bits, so DCD + // mirrors the sx126x logic using those flags. + uint16_t irq = getIrqStatus(); + uint32_t now = millis(); + + bool carrier_detected = false; + + if ((irq & LR11X0_IRQ_SYNC_WORD_HEADER_VALID) != 0) { carrier_detected = true; } + if ((irq & LR11X0_IRQ_PREAMBLE_DETECTED) != 0) { + carrier_detected = true; + // A preamble-detect latch without a following header-valid within the + // expected window is a false alarm — clear it so DCD can drop. + static unsigned long preamble_detected_at = 0; + if (preamble_detected_at == 0) { preamble_detected_at = now; } + if (now - preamble_detected_at > lora_preamble_time_ms + lora_header_time_ms) { + preamble_detected_at = 0; + clearIrq(LR11X0_IRQ_PREAMBLE_DETECTED); + } + } + return carrier_detected; +} + +// ---- DIO1 interrupt ---------------------------------------------------------- + +void lr11x0::handleDio0IfPending() { + if (_dio0_pending) { + _dio0_pending = false; + handleDio0Rise(); + } +} + +void ISR_VECT lr11x0::handleDio0Rise() { + uint16_t irq = getIrqStatus(); + clearIrq(LR11X0_IRQ_ALL); + + // Only deliver a packet on RX_DONE with no CRC error. + if ((irq & LR11X0_IRQ_RX_DONE) && + ((irq & LR11X0_IRQ_CRC_ERR) == 0) && + ((irq & LR11X0_IRQ_HEADER_ERR) == 0)) { + _packetIndex = 0; + uint8_t status[2] = { 0 }; + readCommand(LR11X0_CMD_GET_RX_BUFFER_STATUS, NULL, 0, status, 2); + int packetLength = status[0]; + if (_onReceive) { _onReceive(packetLength); } + } else if (irq & LR11X0_IRQ_HEADER_ERR) { + // LR1110 erratum: a corrupted header can shift subsequent packets; a + // standby() returns the radio to a known-good state (MeshCore workaround). + standby(); + } + + // Re-arm continuous RX. The LR11x0 drops back to its Rx/Tx fallback mode + // (STDBY_RC) after RX_DONE, so unlike the SX126x it does not auto-restart + // the receiver. Issuing SET_RX again here keeps it listening for the next + // packet, matching the firmware's expectation of an always-on RX. + uint8_t timeout[3] = { 0xFF, 0xFF, 0xFF }; + writeCommand(LR11X0_CMD_SET_RX, timeout, 3); +} + +void ISR_VECT lr11x0::onDio0Rise() { + #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52 || MCU_VARIANT == MCU_NATIVE + lr11x0_modem._dio0_pending = true; + #else + lr11x0_modem.handleDio0Rise(); + #endif +} + +lr11x0 lr11x0_modem; + +#endif diff --git a/lr11x0.h b/lr11x0.h new file mode 100644 index 00000000..3e8d7dc5 --- /dev/null +++ b/lr11x0.h @@ -0,0 +1,166 @@ +// lr11x0.h - LoRa driver for the Semtech LR11x0 family (LR1110/LR1120/LR1121). +// +// This driver implements the same ILoRaRadio interface as sx126x/sx127x/sx128x +// so it slots into the existing RNode firmware radio-dispatch machinery. The +// SPI command set and frame layout follow the LR11x0 datasheet (commands are +// 16-bit; reads use a two-phase transaction: send command, then clock out a +// status byte followed by the payload). +// +// Although the LR11x0 is a distinct silicon family from the SX126x, its LoRa +// packet engine is conceptually identical (modulation params, packet params, +// DIO-driven IRQ), so the public API mirrors sx126x one-for-one. +// +// Ported from the RadioLib LR11x0 reference (jgromes/RadioLib) and the +// MeshCore CustomLR1110 wrapper (which drives the Seeed T1000-E). +// +// Licensed under the MIT license. + +#ifndef LR11X0_H +#define LR11X0_H + +#include +#include +#include "Modem.h" +#include "LoRaRadio.h" + +#define LORA_DEFAULT_SS_PIN 10 +#define LORA_DEFAULT_RESET_PIN 9 +#define LORA_DEFAULT_DIO0_PIN 2 +#define LORA_DEFAULT_BUSY_PIN -1 +#define LORA_MODEM_TIMEOUT_MS 20E3 + +// PA output pin selectors (kept for ILoRaRadio setTxPower() signature +// compatibility; the LR11x0 has an internal high-power PA selected via +// setPaConfig rather than a pin). +#define PA_OUTPUT_RFO_PIN 0 +#define PA_OUTPUT_PA_BOOST_PIN 1 + +// RSSI scaling on the LR11x0: register value v maps to -v/2 dBm. +#define RSSI_OFFSET 157 + +class lr11x0 : public ILoRaRadio { +public: + lr11x0(); + + int begin(uint32_t frequency) override; + void end() override; + + int beginPacket(int implicitHeader = false) override; + int endPacket() override; + + int packetRssi() override; + int packetRssi(uint8_t pkt_snr_raw) override; + int currentRssi() override; + uint8_t packetSnrRaw() override; + float packetSnr(); + + // from Print + virtual size_t write(uint8_t byte); + virtual size_t write(const uint8_t *buffer, size_t size); + + // from Stream + virtual int available(); + virtual int read(); + virtual int peek(); + virtual void flush(); + + void onReceive(void(*callback)(int)) override; + + void receive(int size = 0) override; + void standby(); + void sleep(); + void reset(void) override; + + bool preInit() override; + uint8_t getTxPower() override; + void setTxPower(int level, int outputPin = PA_OUTPUT_PA_BOOST_PIN) override; + uint32_t getFrequency() override; + void setFrequency(uint32_t frequency) override; + void setSpreadingFactor(int sf) override; + uint32_t getSignalBandwidth() override; + void setSignalBandwidth(uint32_t sbw) override; + void setCodingRate4(int denominator) override; + void setPreambleLength(long preamble_symbols) override; + bool dcd() override; + void enableCrc() override; + void disableCrc(); + + // Runtime SX126x-style knobs. The native target calls these on the + // polymorphic LoRa pointer; on the LR11x0 they are no-ops (TCXO voltage + // and RF-switch routing are configured at begin() from board macros). + void setTcxoVoltage(uint8_t mode_byte) override { (void)mode_byte; } + void setDio2AsRfSwitch(bool enable) override { (void)enable; } + + void waitOnBusy(); + void handleDio0IfPending() override; + + void setPins(int ss = LORA_DEFAULT_SS_PIN, int reset = LORA_DEFAULT_RESET_PIN, int dio0 = LORA_DEFAULT_DIO0_PIN, int busy = LORA_DEFAULT_BUSY_PIN); + void setSPIFrequency(uint32_t frequency); + + byte random(); + +private: + // Low-level SPI primitives implementing the LR11x0 frame protocol. + // writeCommand(): single transaction — [cmd_hi, cmd_lo, data...] + // readCommand(): two transactions — phase 1 sends [cmd, request...], + // phase 2 clocks out [status_byte, data...]. + void writeCommand(uint16_t cmd, const uint8_t *data, uint8_t len); + void readCommand(uint16_t cmd, const uint8_t *req, uint8_t reqlen, uint8_t *out, uint8_t outlen); + // Poll the always-available status frame: returns [stat1, stat2, irq(4 BE)]. + void readStatusFrame(uint8_t *out6); + + uint16_t getIrqStatus(); + void setDioIrqParams(uint32_t irq, uint32_t dioMask); + void clearIrq(uint32_t irq); + + void setPacketType(uint8_t type); + void setModulationParams(uint8_t sf, uint8_t bw, uint8_t cr, uint8_t ldro); + void setPacketParams(uint16_t preambleLen, uint8_t hdrType, uint8_t payloadLen, uint8_t crcType); + void setRfFrequency(uint32_t frf); + void setPaConfig(uint8_t paSel, uint8_t regPaSupply, uint8_t paDutyCycle, uint8_t paHpSel); + void setTxParams(int8_t pwr, uint8_t ramp); + void setTcxoMode(uint8_t tune, uint32_t delay); + void setRegMode(uint8_t mode); + void setStandby(uint8_t mode); + void calibrate(uint8_t params); + void calibrateImageRejection(uint32_t frequency); + void setRxBoostedGainMode(bool en); + void enableTcxo(); + void configRfSwitch(); + + void explicitHeaderMode(); + void implicitHeaderMode(); + void handleLowDataRate(); + + void handleDio0Rise(); + static void onDio0Rise(); + +private: + SPISettings _spiSettings; + int _ss; + int _reset; + int _dio0; // LR11x0 IRQ pin (DIO1 on the chip) + int _busy; + long _frequency; + int _txp; + uint8_t _sf; + uint8_t _bw; // LR11x0 bandwidth code + uint8_t _cr; // raw LR11x0 coding-rate code + uint8_t _ldro; + int _packetIndex; + int _preambleLength; + int _implicitHeaderMode; + int _payloadLength; + int _crcMode; + uint8_t _rxBufOffset; // LR11x0 RX buffer start offset (moves per packet) + uint8_t _packet[255]; + uint8_t _txBuf[255]; // LoRa TX is staged here (the firmware writes byte-by-byte) + uint8_t _txLen; + bool _preinit_done; + volatile bool _dio0_pending; + void (*_onReceive)(int); +}; + +extern lr11x0 lr11x0_modem; + +#endif diff --git a/platformio.ini b/platformio.ini index 8c42f8a6..1ea0b641 100644 --- a/platformio.ini +++ b/platformio.ini @@ -1020,6 +1020,39 @@ lib_deps = upload_protocol = nrfutil upload_port = /dev/cu.usbmodem101 +; MCU: Nordic nRF52840 (32-bit ARM Cortex-M4, 64 MHz) +; SRAM: 256 KB +; PSRAM: N/A +; Flash: 1 MB internal +; External Flash Interface: N/A +; LoRa: Semtech LR1110 (LoRa + Wi-Fi/GNSS geolocation) +; NOTE: Must double-click RESET to enter DFU mode before uploading. +[env:seeed_t1000e] +extends = env:embedded-nrf52-minimal +platform = nordicnrf52 +board = tracker-t1000-e +custom_variant = t1000e +board_build.variants_dir = variants +board_build.variant = t1000e +board_build.filesystem = littlefs +; Required so the linker pulls in Adafruit_TinyUSB_API.cpp's strong definition +; of TinyUSB_Device_Init() (see lilygo-t-echo env for the same rationale). +lib_archive = no +build_unflags = + ${env:embedded-nrf52-minimal.build_unflags} +build_flags = + ${env:embedded-nrf52-minimal.build_flags} + -DBOARD_MODEL=BOARD_T1000E + -DRNS_DEFAULT_ALLOCATOR=RNS_HEAP_POOL_ALLOCATOR + -DRNS_HEAP_POOL_BUFFER_SIZE=98304 + -DUSTORE_USE_INTERNALFS + -DURTN_PATH_TABLE_MAX_RECS=25 + -DRNS_PATH_TABLE_SEGMENT_SIZE=2048 ; 2 KB + -DRNS_PATH_TABLE_SEGMENT_COUNT=6 +lib_deps = + ${env:embedded-nrf52.lib_deps} +upload_protocol = nrfutil + ; Local dev builds diff --git a/variants/t1000e/WVariant.h b/variants/t1000e/WVariant.h new file mode 100644 index 00000000..a16f1652 --- /dev/null +++ b/variants/t1000e/WVariant.h @@ -0,0 +1,23 @@ +/* + WVariant.h - Seeed Tracker T1000-E (nRF52840 + LR1110) + Standard Adafruit nRF52 variant glue. +*/ + +#pragma once + +#include +#include +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +extern const uint32_t g_ADigitalPinMap[]; + +#ifdef __cplusplus +} // extern "C" +#endif diff --git a/variants/t1000e/variant.cpp b/variants/t1000e/variant.cpp new file mode 100644 index 00000000..142b24f5 --- /dev/null +++ b/variants/t1000e/variant.cpp @@ -0,0 +1,108 @@ +/* + * variant.cpp - Seeed Tracker T1000-E (nRF52840 + LR1110) + * + * GPIO-to-physical-pin map and board bring-up. Adapted from the MeshCore + * t1000-e variant (which is in turn derived from Seeed's official BSP). + */ + +#include "variant.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +#ifdef __cplusplus +extern "C" { +#endif + +// Maps Arduino logical pin numbers (P0.00..P0.31 = 0..31, P1.00..P1.15 = 32..47) +// to their nRF52840 physical GPIO numbers. +const uint32_t g_ADigitalPinMap[PINS_COUNT + 1] = { + 0, // P0.00 + 1, // P0.01 + 2, // P0.02 AIN0 BATTERY_PIN + 3, // P0.03 + 4, // P0.04 SENSOR_EN + 5, // P0.05 EXT_PWR_DETECT + 6, // P0.06 PIN_BUTTON1 + 7, // P0.07 LORA_BUSY + 8, // P0.08 GPS_VRTC_EN + 9, // P0.09 + 10, // P0.10 + 11, // P0.11 PIN_SPI_SCK + 12, // P0.12 PIN_SPI_NSS + 13, // P0.13 PIN_SERIAL1_TX + 14, // P0.14 PIN_SERIAL1_RX + 15, // P0.15 GPS_RTC_INT + 16, // P0.16 PIN_SERIAL2_TX + 17, // P0.17 PIN_SERIAL2_RX + 18, // P0.18 + 19, // P0.19 + 20, // P0.20 + 21, // P0.21 + 22, // P0.22 + 23, // P0.23 + 24, // P0.24 LED_GREEN + 25, // P0.25 BUZZER_PIN + 26, // P0.26 PIN_WIRE_SDA + 27, // P0.27 PIN_WIRE_SCL + 28, // P0.28 + 29, // P0.29 AIN5 LUX_SENSOR + 30, // P0.30 + 31, // P0.31 AIN7 TEMP_SENSOR + 32, // P1.00 + 33, // P1.01 LORA_DIO_1 + 34, // P1.02 QMA6100P_INT + 35, // P1.03 EXT_CHRG_DETECT + 36, // P1.04 + 37, // P1.05 BUZZER_EN + 38, // P1.06 PIN_3V3_EN + 39, // P1.07 PIN_3V3_ACC_EN + 40, // P1.08 PIN_SPI_MISO + 41, // P1.09 PIN_SPI_MOSI + 42, // P1.10 LORA_RESET + 43, // P1.11 GPS_EN + 44, // P1.12 GPS_SLEEP_INT + 45, // P1.13 FLASH_ENABLE + 46, // P1.14 GPS_RESETB + 47, // P1.15 GPS_RESET + 255 // not used +}; + +void initVariant() { + // Configure sensor/battery/GPS pins to safe default levels, mirroring + // Seeed's official T1000-E bring-up so the board boots with peripherals + // unpowered until the application drives them. + + pinMode(BATTERY_PIN, INPUT); + pinMode(TEMP_SENSOR, INPUT); + pinMode(LUX_SENSOR, INPUT); + pinMode(EXT_CHRG_DETECT, INPUT); + pinMode(EXT_PWR_DETECT, INPUT); + pinMode(GPS_RESETB, INPUT); + pinMode(PIN_BUTTON1, INPUT); + + pinMode(PIN_3V3_EN, OUTPUT); + pinMode(PIN_3V3_ACC_EN, OUTPUT); + pinMode(BUZZER_EN, OUTPUT); + pinMode(SENSOR_EN, OUTPUT); + pinMode(GPS_EN, OUTPUT); + pinMode(GPS_RESET, OUTPUT); + pinMode(GPS_VRTC_EN, OUTPUT); + pinMode(GPS_SLEEP_INT, OUTPUT); + pinMode(GPS_RTC_INT, OUTPUT); + pinMode(LED_PIN, OUTPUT); + + digitalWrite(PIN_3V3_EN, LOW); + digitalWrite(PIN_3V3_ACC_EN, LOW); + digitalWrite(BUZZER_EN, LOW); + digitalWrite(SENSOR_EN, LOW); + digitalWrite(GPS_EN, LOW); + digitalWrite(GPS_RESET, LOW); + digitalWrite(GPS_VRTC_EN, LOW); + digitalWrite(GPS_SLEEP_INT, HIGH); + digitalWrite(GPS_RTC_INT, LOW); + digitalWrite(LED_PIN, LOW); +} + +#ifdef __cplusplus +} +#endif diff --git a/variants/t1000e/variant.h b/variants/t1000e/variant.h new file mode 100644 index 00000000..4ba38e04 --- /dev/null +++ b/variants/t1000e/variant.h @@ -0,0 +1,163 @@ +/* + * variant.h - Seeed Tracker T1000-E (nRF52840 + LR1110) + * + * Adapted from the MeshCore t1000-e variant for the Adafruit nRF52 Arduino + * core used by this firmware. Pin map verified against the Seeed schematic: + * https://github.com/Seeed-Studio/Seeed-Tracker-T1000-E-for-LoRaWAN-dev-board + */ + +#pragma once + +#ifndef _VARIANT_T1000E_ +#define _VARIANT_T1000E_ + +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +// 32.768 kHz low-frequency clock source: crystal oscillator. +#define USE_LFXO +// #define USE_LFRC + +/*---------------------------------------------------------------------------- + * Headers + *----------------------------------------------------------------------------*/ +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// Number of pins defined in the pin-description array. +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (6) +#define NUM_ANALOG_OUTPUTS (0) + +/*---------------------------------------------------------------------------- + * Power + *----------------------------------------------------------------------------*/ + +#define NRF_APM // detect USB power + +#define BATTERY_PIN (2) // P0.02 / AIN0 +#define BATTERY_IMMUTABLE +#define ADC_MULTIPLIER (2.0F) +#define AREF_VOLTAGE (3.0) + +#define EXT_CHRG_DETECT (35) // P1.03 +#define EXT_PWR_DETECT (5) // P0.05 + +#define ADC_RESOLUTION (14) + +/*---------------------------------------------------------------------------- + * LEDs + *----------------------------------------------------------------------------*/ + +#define LED_BUILTIN (-1) +#define LED_BLUE (-1) +#define LED_GREEN (24) // P0.24 +#define LED_PIN LED_GREEN + +#define LED_STATE_ON HIGH + +/*---------------------------------------------------------------------------- + * Buttons + *----------------------------------------------------------------------------*/ + +#define PIN_BUTTON1 (6) // P0.06 +#define BUTTON_PIN PIN_BUTTON1 + +/*---------------------------------------------------------------------------- + * Serial + *----------------------------------------------------------------------------*/ + +#define PIN_SERIAL1_RX (14) // P0.14 (GPS) +#define PIN_SERIAL1_TX (13) // P0.13 + +#define PIN_SERIAL2_RX (17) // P0.17 +#define PIN_SERIAL2_TX (16) // P0.16 + +/*---------------------------------------------------------------------------- + * Wire (I2C) + *----------------------------------------------------------------------------*/ + +#define HAS_WIRE (1) +#define WIRE_INTERFACES_COUNT (1) + +#define PIN_WIRE_SDA (26) // P0.26 +#define PIN_WIRE_SCL (27) // P0.27 + +/*---------------------------------------------------------------------------- + * SPI (shared with the LR1110) + *----------------------------------------------------------------------------*/ + +#define SPI_INTERFACES_COUNT (1) + +#define PIN_SPI_MISO (40) // P1.08 +#define PIN_SPI_MOSI (41) // P1.09 +#define PIN_SPI_SCK (11) // P0.11 +#define PIN_SPI_NSS (12) // P0.12 + +static const uint8_t SS = PIN_SPI_NSS; +static const uint8_t MOSI = PIN_SPI_MOSI; +static const uint8_t MISO = PIN_SPI_MISO; +static const uint8_t SCK = PIN_SPI_SCK; + +/*---------------------------------------------------------------------------- + * LR1110 + *----------------------------------------------------------------------------*/ + +#define LORA_DIO_1 (33) // P1.01 (IRQ) +#define LORA_NSS (PIN_SPI_NSS) +#define LORA_RESET (42) // P1.10 +#define LORA_BUSY (7) // P0.07 +#define LORA_SCLK (PIN_SPI_SCK) +#define LORA_MISO (PIN_SPI_MISO) +#define LORA_MOSI (PIN_SPI_MOSI) + +// TCXO supplied via DIO3 at 1.6 V; on-chip RF switch driven by DIO5..DIO8. +#define LR11X0_DIO_AS_RF_SWITCH true +#define LR11X0_DIO3_TCXO_VOLTAGE 1.6 + +/*---------------------------------------------------------------------------- + * GPS + *----------------------------------------------------------------------------*/ + +#define HAS_GPS 1 +#define GPS_RX_PIN PIN_SERIAL1_RX +#define GPS_TX_PIN PIN_SERIAL1_TX + +#define GPS_EN (43) // P1.11 +#define GPS_RESET (47) // P1.15 + +#define GPS_VRTC_EN (8) // P0.08 +#define GPS_SLEEP_INT (44) // P1.12 +#define GPS_RTC_INT (15) // P0.15 +#define GPS_RESETB (46) // P1.14 + +/*---------------------------------------------------------------------------- + * Sensors / power rails + *----------------------------------------------------------------------------*/ + +#define PIN_3V3_EN (38) // P1.06 — power to sensors / 3V3 rail +#define PIN_3V3_ACC_EN (39) // P1.07 +#define SENSOR_EN (4) // P0.04 +#define TEMP_SENSOR (31) // P0.31 / AIN7 +#define LUX_SENSOR (29) // P0.29 / AIN5 + +/*---------------------------------------------------------------------------- + * Buzzer + *----------------------------------------------------------------------------*/ + +#define BUZZER_EN (37) // P1.05 +#define BUZZER_PIN (25) // P0.25 + +#ifdef __cplusplus +} +#endif + +/*---------------------------------------------------------------------------- + * Arduino objects - C++ only + *----------------------------------------------------------------------------*/ + +#endif From 0dcf5792694088a37beb07b43f77b4ae72591ce3 Mon Sep 17 00:00:00 2001 From: Henri Bergius Date: Mon, 27 Jul 2026 09:29:23 -1000 Subject: [PATCH 2/8] T1000-e power management --- Power.h | 28 ++++++++++++++++++++++++++-- README.md | 3 +++ Utilities.h | 12 +++++++++++- 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/Power.h b/Power.h index 7e72d827..1ad6cc14 100644 --- a/Power.h +++ b/Power.h @@ -213,6 +213,24 @@ float pmu_temperature = PMU_TEMP_MIN-1; bool bat_voltage_dropping = false; float bat_delay_v = 0; float bat_state_change_v = 0; +#elif BOARD_MODEL == BOARD_T1000E + // Seeed T1000-E: battery on P0.02/AIN0 via a 1:2 divider, ADC reference + // 3.0 V, 12-bit resolution (Seeed datasheet / MeshCore T1000eBoard). + #define BAT_V_MIN 3.15 + #define BAT_V_MAX 4.2 + #define BAT_V_CHG 4.48 + #define BAT_V_FLOAT 4.33 + #define BAT_SAMPLES 7 + const uint8_t pin_vbat = BATTERY_PIN; // P0.02 + float bat_p_samples[BAT_SAMPLES]; + float bat_v_samples[BAT_SAMPLES]; + uint8_t bat_samples_count = 0; + int bat_discharging_samples = 0; + int bat_charging_samples = 0; + int bat_charged_samples = 0; + bool bat_voltage_dropping = false; + float bat_delay_v = 0; + float bat_state_change_v = 0; #endif uint32_t last_pmu_update = 0; @@ -235,7 +253,7 @@ void measure_temperature() { } void measure_battery() { - #if BOARD_MODEL == BOARD_RNODE_NG_21 || BOARD_MODEL == BOARD_LORA32_V2_1 || BOARD_MODEL == BOARD_HELTEC32_V3 || BOARD_MODEL == BOARD_HELTEC32_V4 || BOARD_MODEL == BOARD_HELTEC_TRACKER_V2 || BOARD_MODEL == BOARD_TDECK || BOARD_MODEL == BOARD_T3S3 || BOARD_MODEL == BOARD_HELTEC_T114 || BOARD_MODEL == BOARD_TECHO || BOARD_MODEL == BOARD_RAK4631 || BOARD_MODEL == BOARD_RAK3401 + #if BOARD_MODEL == BOARD_RNODE_NG_21 || BOARD_MODEL == BOARD_LORA32_V2_1 || BOARD_MODEL == BOARD_HELTEC32_V3 || BOARD_MODEL == BOARD_HELTEC32_V4 || BOARD_MODEL == BOARD_HELTEC_TRACKER_V2 || BOARD_MODEL == BOARD_TDECK || BOARD_MODEL == BOARD_T3S3 || BOARD_MODEL == BOARD_HELTEC_T114 || BOARD_MODEL == BOARD_TECHO || BOARD_MODEL == BOARD_RAK4631 || BOARD_MODEL == BOARD_RAK3401 || BOARD_MODEL == BOARD_T1000E battery_installed = true; #if BOARD_MODEL == BOARD_HELTEC32_V3 || BOARD_MODEL == BOARD_HELTEC32_V4 || BOARD_MODEL == BOARD_HELTEC_TRACKER_V2 battery_indeterminate = false; @@ -258,6 +276,9 @@ void measure_battery() { float battery_measurement = (float)(analogRead(pin_vbat)) * 0.007067; #elif BOARD_MODEL == BOARD_RAK4631 || BOARD_MODEL == BOARD_RAK3401 float battery_measurement = (float)(analogRead(pin_vbat)) * 0.005068; + #elif BOARD_MODEL == BOARD_T1000E + // 12-bit ADC, 1:2 divider, 3.0 V reference (Seeed T1000-E). + float battery_measurement = (float)(analogRead(pin_vbat)) * 3.0f * 2.0f / 4096.0f; #else float battery_measurement = (float)(analogRead(pin_vbat)) / 4095.0*7.26; #endif @@ -450,11 +471,14 @@ bool init_pmu() { pmu_temp_sensor_ready = true; #endif - #if BOARD_MODEL == BOARD_RNODE_NG_21 || BOARD_MODEL == BOARD_LORA32_V2_1 || BOARD_MODEL == BOARD_TDECK || BOARD_MODEL == BOARD_T3S3 || BOARD_MODEL == BOARD_TECHO || BOARD_MODEL == BOARD_RAK4631 || BOARD_MODEL == BOARD_RAK3401 + #if BOARD_MODEL == BOARD_RNODE_NG_21 || BOARD_MODEL == BOARD_LORA32_V2_1 || BOARD_MODEL == BOARD_TDECK || BOARD_MODEL == BOARD_T3S3 || BOARD_MODEL == BOARD_TECHO || BOARD_MODEL == BOARD_RAK4631 || BOARD_MODEL == BOARD_RAK3401 || BOARD_MODEL == BOARD_T1000E pinMode(pin_vbat, INPUT); #if BOARD_MODEL == BOARD_RAK4631 || BOARD_MODEL == BOARD_RAK3401 analogReference(AR_INTERNAL_3_0); analogReadResolution(10); + #elif BOARD_MODEL == BOARD_T1000E + analogReference(AR_INTERNAL_3_0); + analogReadResolution(12); #endif return true; #elif BOARD_MODEL == BOARD_HELTEC32_V3 diff --git a/README.md b/README.md index defe8dfe..92f8b1ce 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,7 @@ Build a single environment (board): pio run -e ttgo-t-beam pio run -e heltec-wireless-tracker-v2 pio run -e wiscore_rak4631 +pio run -e seeed_t1000e ``` Build and upload a single environment (board): @@ -74,6 +75,7 @@ Build and upload a single environment (board): pio run -e ttgo-t-beam -t upload pio run -e heltec-wireless-tracker-v2 -t upload pio run -e wiscore_rak4631 -t upload +pio run -e seeed_t1000e -t upload ``` Build and package a single environment (board): @@ -81,6 +83,7 @@ Build and package a single environment (board): pio run -e ttgo-t-beam -t package pio run -e heltec-wireless-tracker-v2 -t package pio run -e wiscore_rak4631 -t package +pio run -e seeed_t1000e -t package ``` On erased EEPROM, the Heltec Wireless Tracker V2 enables BLE and explicitly diff --git a/Utilities.h b/Utilities.h index 27c7a70d..90725565 100644 --- a/Utilities.h +++ b/Utilities.h @@ -417,6 +417,14 @@ extern RNS::Reticulum reticulum; void led_tx_off() { digitalWrite(pin_led_tx, LED_OFF); } void led_id_on() { } void led_id_off() { } + #elif BOARD_MODEL == BOARD_T1000E + // T1000-E has a single green LED (P0.24, active high) shared for RX/TX. + void led_rx_on() { digitalWrite(pin_led_rx, HIGH); } + void led_rx_off() { digitalWrite(pin_led_rx, LOW); } + void led_tx_on() { digitalWrite(pin_led_tx, HIGH); } + void led_tx_off() { digitalWrite(pin_led_tx, LOW); } + void led_id_on() { } + void led_id_off() { } #endif #endif @@ -1745,7 +1753,7 @@ bool eeprom_product_valid() { #elif PLATFORM == PLATFORM_ESP32 if (rval == PRODUCT_RNODE || rval == BOARD_RNODE_NG_20 || rval == BOARD_RNODE_NG_21 || rval == PRODUCT_HMBRW || rval == PRODUCT_TBEAM || rval == PRODUCT_T32_10 || rval == PRODUCT_T32_20 || rval == PRODUCT_T32_21 || rval == PRODUCT_H32_V2 || rval == PRODUCT_H32_V3 || rval == PRODUCT_H32_V4 || rval == PRODUCT_HTRACKER_V2 || rval == PRODUCT_TDECK_V1 || rval == PRODUCT_TBEAM_S_V1 || rval == PRODUCT_XIAO_S3) { #elif PLATFORM == PLATFORM_NRF52 - if (rval == PRODUCT_RAK4631 || rval == PRODUCT_HELTEC_T114 || rval == PRODUCT_TECHO || rval == PRODUCT_HMBRW) { + if (rval == PRODUCT_RAK4631 || rval == PRODUCT_HELTEC_T114 || rval == PRODUCT_TECHO || rval == PRODUCT_T1000E || rval == PRODUCT_HMBRW) { #elif PLATFORM == PLATFORM_NATIVE if (rval == PRODUCT_NATIVE_LINUX) { #else @@ -1804,6 +1812,8 @@ bool eeprom_model_valid() { if (model == MODEL_11 || model == MODEL_12) { #elif BOARD_MODEL == BOARD_RAK3401 if (model == MODEL_13 || model == MODEL_14) { + #elif BOARD_MODEL == BOARD_T1000E + if (model == MODEL_18 || model == MODEL_19) { #elif BOARD_MODEL == BOARD_HUZZAH32 if (model == MODEL_FF) { #elif BOARD_MODEL == BOARD_GENERIC_ESP32 From f2b6d5734af5023dcb09e3e53f0284a0e446cb75 Mon Sep 17 00:00:00 2001 From: Henri Bergius Date: Tue, 28 Jul 2026 15:19:44 -1000 Subject: [PATCH 3/8] Show charging state via LED --- Power.h | 9 +++++ RNode_Firmware.ino | 1 + Utilities.h | 98 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 108 insertions(+) diff --git a/Power.h b/Power.h index 1ad6cc14..0ca8ee18 100644 --- a/Power.h +++ b/Power.h @@ -459,6 +459,15 @@ void measure_battery() { } void update_pmu() { + #if MCU_VARIANT == MCU_NRF52 + // The nRF52840 POWER peripheral exposes USB VBUS presence directly. + // VBUSDETECT reads true whenever USB power is connected (independent + // of enumeration), which is what the LED/battery UX treats as + // "external power". Sampled every pass so hot-plug is reflected promptly. + #if defined(NRF_POWER) && defined(POWER_USBREGSTATUS_VBUSDETECT_Msk) + external_power = (NRF_POWER->USBREGSTATUS & POWER_USBREGSTATUS_VBUSDETECT_Msk) != 0; + #endif + #endif if (millis()-last_pmu_update >= pmu_update_interval) { measure_battery(); measure_temperature(); diff --git a/RNode_Firmware.ino b/RNode_Firmware.ino index 359fa3ba..dc59d978 100644 --- a/RNode_Firmware.ino +++ b/RNode_Firmware.ino @@ -2723,6 +2723,7 @@ void loop() { tx_queue_handler(); check_modem_status(); + led_update(); #if MCU_VARIANT == MCU_NATIVE // Drop a TCP host client that's gone silent past the idle window. // poll_accept() in buffer_serial() handles the connect side; this diff --git a/Utilities.h b/Utilities.h index 90725565..ae2b5de3 100644 --- a/Utilities.h +++ b/Utilities.h @@ -428,6 +428,104 @@ extern RNS::Reticulum reticulum; #endif #endif +// --------------------------------------------------------------------------- +// T1000-E event-driven single-LED policy. +// +// With only one green LED and no display, the T1000-E uses it the way +// Meshtastic does rather than the firmware's default level-hold semantics: +// * USB / external power + idle -> slow "breathing" pulse (alive & charging) +// * battery + idle -> off (power budget) +// * packet received -> one short dim blip +// * packet transmitted -> two short dim blips +// +// Boot / error / warning indications still go through led_tx_on()/led_rx_on() +// above (direct GPIO): they run as blocking sequences from the main loop at +// moments when led_update() is not being called, so there is no contention. +// +// The normal-operation LED hooks (carrier-detect and TX-duration level-hold) +// are suppressed for this board in RNode_Firmware.ino; led_update() owns the +// pin while the radio is online. +// --------------------------------------------------------------------------- +#if BOARD_MODEL == BOARD_T1000E + #define T1000E_LED_BLIP_LEVEL 48 // dim blip brightness (0-255) + #define T1000E_LED_BLIP_ON_MS 45 // blip ON width + #define T1000E_LED_BLIP_GAP_MS 45 // OFF gap between blips in a burst + #define T1000E_LED_BLIP_REST_MS 220 // pause after a burst before idle + #define T1000E_LED_BREATH_MAX 110 // peak breathing brightness + #define T1000E_LED_BREATH_STEP_MS 10 // ms between breathing steps + + // Blips requested since last pass (1 per RX, 2 per TX). volatile because + // the RX path can run from the deferred-DIO callback context. + volatile uint8_t t1000e_led_blips = 0; + uint8_t t1000e_led_state = 0; // 0 = idle, 1 = blip ON, 2 = blip gap + uint32_t t1000e_led_time = 0; // next permitted transition + uint16_t t1000e_led_breath = 0; // current breathing PWM value + int16_t t1000e_led_bdir = 1; + uint32_t t1000e_led_breath_time = 0; // breathing step throttle + + // Queue N blips. Safe from deferred-RX / ISR context (volatile +=). + inline void led_event_blip(uint8_t n) { t1000e_led_blips += n; } + + // Drive the LED. Call every main-loop iteration while the radio is online. + inline void led_update() { + uint32_t now = millis(); + + // (1) If we are mid-burst, finish the current on/gap step first. + if (t1000e_led_state) { + if ((int32_t)(now - t1000e_led_time) < 0) return; + if (t1000e_led_state == 1) { // was ON -> start gap + analogWrite(pin_led_rx, 0); + t1000e_led_state = 2; + t1000e_led_time = now + T1000E_LED_BLIP_GAP_MS; + } else { // gap done -> next blip or finish + if (t1000e_led_blips > 0) t1000e_led_blips--; + if (t1000e_led_blips > 0) { + analogWrite(pin_led_rx, T1000E_LED_BLIP_LEVEL); + t1000e_led_state = 1; + t1000e_led_time = now + T1000E_LED_BLIP_ON_MS; + } else { + analogWrite(pin_led_rx, 0); + t1000e_led_state = 0; + t1000e_led_time = now + T1000E_LED_BLIP_REST_MS; + } + } + return; + } + + // (2) A new burst may have been requested. + if (t1000e_led_blips > 0) { + t1000e_led_blips--; + analogWrite(pin_led_rx, T1000E_LED_BLIP_LEVEL); + t1000e_led_state = 1; + t1000e_led_time = now + T1000E_LED_BLIP_ON_MS; + return; + } + + // (3) Post-burst rest: hold the LED idle until it elapses. + if ((int32_t)(now - t1000e_led_time) < 0) return; + + // (4) Idle: breathe on external power, dark on battery. + if (external_power) { + if ((int32_t)(now - t1000e_led_breath_time) >= 0) { + t1000e_led_breath_time = now + T1000E_LED_BREATH_STEP_MS; + t1000e_led_breath += t1000e_led_bdir; + if (t1000e_led_breath >= T1000E_LED_BREATH_MAX) { + t1000e_led_breath = T1000E_LED_BREATH_MAX; t1000e_led_bdir = -1; + } else if (t1000e_led_breath == 0) { + t1000e_led_bdir = 1; + } + analogWrite(pin_led_rx, t1000e_led_breath); + } + } else { + analogWrite(pin_led_rx, 0); + } + } +#else + // Boards without the event-LED policy: nothing to do per loop. + inline void led_update() { } + inline void led_event_blip(uint8_t) { } +#endif + void hard_reset(void) { #if MCU_VARIANT == MCU_1284P || MCU_VARIANT == MCU_2560 wdt_enable(WDTO_15MS); From 5fee09d130633717a013731c8ae0c57c903ff919 Mon Sep 17 00:00:00 2001 From: Henri Bergius Date: Tue, 28 Jul 2026 15:23:51 -1000 Subject: [PATCH 4/8] UF2 generator --- uf2.py | 130 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 uf2.py diff --git a/uf2.py b/uf2.py new file mode 100644 index 00000000..15bc31f8 --- /dev/null +++ b/uf2.py @@ -0,0 +1,130 @@ +# +# uf2.py - Minimal Intel-HEX -> UF2 converter for nRF52840 boards that flash +# via the Adafruit/Seeed mass-storage (UF2) bootloader, e.g. the Seeed +# Tracker T1000-E. +# +# UF2 is Microsoft's USB Flashing Format: +# https://github.com/microsoft/uf2 +# Each 512-byte block carries a 256-byte payload plus the absolute target +# address, so the converted file is independent of any particular flash +# layout (the addresses come straight from the .hex the linker emitted). +# +# Licensed under the MIT license. + +import struct + +# UF2 on-wire constants. +UF2_MAGIC_START0 = 0x0A324655 # "UF2\n" +UF2_MAGIC_START1 = 0x9E5D5157 +UF2_MAGIC_END = 0x0AB16F30 +UF2_FLAG_FAMILY_ID_PRESENT = 0x00002000 +UF2_BLOCK_SIZE = 512 +UF2_PAYLOAD_SIZE = 256 + +# Family ID for Adafruit Bluefruit / Seeed nRF52840 UF2 bootloaders. +# (See microsoft/uf2 uf2families.json.) May be overridden by the caller. +UF2_FAMILY_NRF52840 = 0xADA52840 + + +def _read_ihex(path): + """Parse an Intel HEX file into a list of (absolute_addr, data_bytes) + contiguous segments, honouring type-04 extended-linear-address records + so flash above 64 KB is handled correctly.""" + segments = [] + base_hi = 0 # upper 16 bits from the last type-04 record + seg_addr = None # start address of the current segment + seg_data = bytearray() + + def flush(): + nonlocal seg_addr, seg_data + if seg_addr is not None and seg_data: + segments.append((seg_addr, bytes(seg_data))) + seg_addr = None + seg_data = bytearray() + + with open(path, "r") as f: + for raw_line in f: + line = raw_line.strip() + if not line or not line.startswith(":"): + continue + buf = bytes.fromhex(line[1:]) + dlen = buf[0] + addr = (buf[1] << 8) | buf[2] + rtype = buf[3] + data = buf[4:4 + dlen] + if rtype == 0x00: # data + abs_addr = base_hi + addr + if seg_addr is None: + seg_addr = abs_addr + elif abs_addr != seg_addr + len(seg_data): + flush() + seg_addr = abs_addr + seg_data.extend(data) + elif rtype == 0x04: # extended linear address + flush() + base_hi = ((data[0] << 8) | data[1]) << 16 + elif rtype == 0x01: # EOF + break + # type 02/03 (segment/extended segment) are unused for nRF52. + flush() + return segments + + +def generate_uf2(hex_path, uf2_path, family_id=UF2_FAMILY_NRF52840): + """Convert an Intel HEX firmware image to a UF2 file. Returns a tuple + (output_path, num_blocks). Falls back to a raw .bin image at a given base + if the .hex is absent (caller passes bin_path + bin_base).""" + segments = _read_ihex(hex_path) + + # Slice every segment into 256-byte UF2 payloads. + blocks = [] + for base, data in segments: + for off in range(0, len(data), UF2_PAYLOAD_SIZE): + chunk = data[off:off + UF2_PAYLOAD_SIZE] + if len(chunk) < UF2_PAYLOAD_SIZE: + chunk = chunk + bytes(UF2_PAYLOAD_SIZE - len(chunk)) + blocks.append((base + off, chunk)) + + total = len(blocks) + if total == 0: + raise RuntimeError("UF2: no data records found in %s" % hex_path) + + with open(uf2_path, "wb") as out: + for index, (addr, payload) in enumerate(blocks): + block = bytearray(UF2_BLOCK_SIZE) + struct.pack_into( + " Date: Tue, 28 Jul 2026 15:29:48 -1000 Subject: [PATCH 5/8] DFU process --- Boards.h | 2 +- extra_script.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/Boards.h b/Boards.h index 42b88bcb..853e6f94 100644 --- a/Boards.h +++ b/Boards.h @@ -1118,7 +1118,7 @@ #define LR11X0_RFSW_TX 0x0B // DIO5 + DIO6 + DIO8 #define LR11X0_RFSW_TX_HP 0x0A // DIO6 + DIO8 #define LR11X0_RFSW_TX_HF 0x00 - #define LR11X0_RFSW_GNSS 0x04 // DIO7 + #define LR11X0_RFSW_GNSS 0x00 // GNSS switch unused (LoRa only) #define LR11X0_RFSW_WIFI 0x00 // RX boosted gain improves sensitivity on the T1000-E front end. diff --git a/extra_script.py b/extra_script.py index db612c36..ecb57a8a 100644 --- a/extra_script.py +++ b/extra_script.py @@ -4,6 +4,7 @@ import platform as platformlib from firmware_image import esp_image_sha256, firmware_hash_kiss_frame +from uf2 import generate_uf2, find_dfu_volume # # Helpier functions @@ -68,6 +69,47 @@ def target_package(target, source, env): # Upload actions # +# ---- UF2 (Adafruit/Seeed mass-storage bootloader, e.g. T1000-E) ---- +# +# Boards such as the Seeed Tracker T1000-E are not flashed over serial +# DFU. They expose a UF2 mass-storage volume in bootloader (DFU) mode and +# the firmware is copied there as a .uf2 file. The converter in uf2.py turns +# the linked .hex into a .uf2 with absolute addresses, so no flash base has +# to be hardcoded. + +def generate_uf2_action(source, target, env): + build_dir = env.subst("$BUILD_DIR") + progname = env.subst("$PROGNAME") + hex_path = build_dir + "/" + progname + ".hex" + uf2_path = build_dir + "/" + progname + ".uf2" + try: + out, blocks = generate_uf2(hex_path, uf2_path) + print("*** Generated UF2: %s (%d blocks)" % (out, blocks)) + return uf2_path + except Exception as exc: + print("*** UF2 generation failed: %s" % exc) + return None + +def uf2_upload_action(source, target, env): + uf2_path = generate_uf2_action(source, target, env) + if not uf2_path: + return + vol = find_dfu_volume("T1000") + if vol: + dest = vol + "/" + os.path.basename(uf2_path) + shutil.copy(uf2_path, dest) + print("*** Copied %s -> %s" % (uf2_path, dest)) + print("*** The board reboots into the application once the copy completes.") + else: + print("***") + print("*** No T1000-E DFU volume was found.") + print("*** To flash:") + print("*** 1. Put the board in DFU mode: rapidly disconnect and reconnect") + print("*** USB (or double-tap reset) until a T1000-E drive mounts.") + print("*** 2. Drag this file onto it:") + print("*** %s" % uf2_path) + print("***") + def pre_upload(source, target, env): print("*** Executing pre_upload steps...") # do some actions From 3946f6e6eb85991e05501fb637c23619058feacd Mon Sep 17 00:00:00 2001 From: Henri Bergius Date: Tue, 28 Jul 2026 16:31:19 -1000 Subject: [PATCH 6/8] DFU process docs --- README.md | 25 ++++++++++++++++++++++++ RNode_Firmware.ino | 18 +++++++++++++++-- boards/tracker-t1000-e.json | 6 +++--- extra_script.py | 39 ++++++++++++++++++++++++++++++++++--- 4 files changed, 80 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 92f8b1ce..8c36a467 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,31 @@ Build and package all environments (boards): pio run -t package ``` +### Flashing the Seeed Tracker T1000-E (UF2) + +Unlike the other supported boards, the T1000-E does **not** flash over serial +DFU. Its bootloader exposes a **UF2 mass-storage** volume, and you flash by +dragging a `.uf2` firmware file onto it. + +The build produces this file automatically alongside the usual artifacts: + +``` +.pio/build/seeed_t1000e/rnode_firmware_t1000e.uf2 +``` +(generate it explicitly with `pio run -e seeed_t1000e -t uf2`, or find the +packaged copy at `Release/rnode_firmware_t1000e.uf2` after `-t package`.) + +To flash: + +1. Put the board in **DFU mode**: rapidly disconnect and reconnect the USB + cable (or double-tap the reset button) until a `T1000-E` drive mounts. +2. Drag `rnode_firmware_t1000e.uf2` onto that drive. The board flashes and + reboots into the application automatically when the copy finishes. + +`pio run -e seeed_t1000e -t upload` automates this: it builds the `.uf2`, +looks for the mounted DFU volume and copies the file there. If no volume is +found it prints the drag-and-drop instructions above as a fallback. + Write version info: python release_hashes.py > Release/release.json diff --git a/RNode_Firmware.ino b/RNode_Firmware.ino index dc59d978..5570bf6c 100644 --- a/RNode_Firmware.ino +++ b/RNode_Firmware.ino @@ -1361,6 +1361,9 @@ void ISR_VECT receive_callback(int packet_size) { } if (ready) { + #if BOARD_MODEL == BOARD_T1000E + led_event_blip(1); + #endif #if MCU_VARIANT != MCU_ESP32 && MCU_VARIANT != MCU_NRF52 && MCU_VARIANT != MCU_NATIVE // We first signal the RSSI of the // recieved packet to the host. @@ -1487,7 +1490,11 @@ volatile bool queue_flushing = false; void flush_queue(void) { if (!queue_flushing) { queue_flushing = true; - led_tx_on(); + #if BOARD_MODEL == BOARD_T1000E + led_event_blip(2); + #else + led_tx_on(); + #endif #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52 || MCU_VARIANT == MCU_NATIVE while (!fifo16_isempty(&packet_starts)) { @@ -1527,7 +1534,12 @@ void flush_queue(void) { void pop_queue() { if (!queue_flushing) { - queue_flushing = true; led_tx_on(); + queue_flushing = true; + #if BOARD_MODEL == BOARD_T1000E + led_event_blip(2); + #else + led_tx_on(); + #endif #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52 || MCU_VARIANT == MCU_NATIVE if (!fifo16_isempty(&packet_starts)) { @@ -2410,6 +2422,7 @@ void update_modem_status() { if (carrier_detected) { dcd = true; } else { dcd = false; } dcd_led = dcd; + #if BOARD_MODEL != BOARD_T1000E if (dcd_led) { led_rx_on(); } else { if (interference_detected && noise_floor_sampled) { @@ -2419,6 +2432,7 @@ void update_modem_status() { else { led_rx_off(); led_id_off(); } } } + #endif update_noise_floor(); } diff --git a/boards/tracker-t1000-e.json b/boards/tracker-t1000-e.json index 1552de9d..f21c3ee2 100644 --- a/boards/tracker-t1000-e.json +++ b/boards/tracker-t1000-e.json @@ -50,9 +50,9 @@ "cmsis-dap", "blackmagic" ], - "use_1200bps_touch": true, - "require_upload_port": true, - "wait_for_upload_port": true + "use_1200bps_touch": false, + "require_upload_port": false, + "wait_for_upload_port": false }, "url": "https://www.seeedstudio.com/SenseCAP-Card-Tracker-T1000-E-for-Meshtastic-p-5913.html", "vendor": "Seeed Studio" diff --git a/extra_script.py b/extra_script.py index ecb57a8a..da609976 100644 --- a/extra_script.py +++ b/extra_script.py @@ -1,6 +1,7 @@ import time import hashlib import shutil +import os import platform as platformlib from firmware_image import esp_image_sha256, firmware_hash_kiss_frame @@ -295,6 +296,10 @@ def firmware_package(env): env.Execute(zip_cmd) elif (platform == "nordicnrf52"): env.Execute("cp " + build_dir + "/" + env.subst("$PROGNAME") + ".zip " + project_dir + "/Release/.") + # UF2 image for Adafruit/Seeed mass-storage-bootloader boards (T1000-E). + uf2_artifact = build_dir + "/" + env.subst("$PROGNAME") + ".uf2" + if os.path.exists(uf2_artifact): + env.Execute("cp " + uf2_artifact + " " + project_dir + "/Release/.") else: env.Execute("cp " + build_dir + "/" + env.subst("$PROGNAME") + " " + build_dir + "/rnoded") env.Execute("rm -f " + project_dir + "/Release/rnoded-" + get_target() + ".zip") @@ -343,6 +348,11 @@ def firmware_package(env): # remove --specs=nano.specs to allow exceptions to work if '--specs=nano.specs' in env['LINKFLAGS']: env['LINKFLAGS'].remove('--specs=nano.specs') + # Generate a UF2 image alongside the linked .hex for boards that flash + # via the Adafruit/Seeed mass-storage bootloader (e.g. T1000-E). The + # .hex is emitted by the Arduino core's objcopy step, so this post-action + # fires once it exists. Harmless on boards that never use the .uf2. + env.AddPostAction("$BUILD_DIR/${PROGNAME}.hex", generate_uf2_action) env.AddCustomTarget( name="package", dependencies="$BUILD_DIR/${PROGNAME}.zip", @@ -363,6 +373,29 @@ def firmware_package(env): description="Package native daemon for delivery" ) -# Register actions -env.AddPreAction("upload", pre_upload) -env.AddPostAction("upload", post_upload) +# Register upload actions. +# +# The T1000-E flashes by dragging a .uf2 onto its DFU mass-storage volume, +# not over serial DFU, so its `upload` target generates the .uf2 and copies +# it to the mounted volume (with drag-and-drop instructions as a fallback). +# The serial-DFU / rnodeconf pre- and post-upload steps are only wired up for +# the other boards that actually use nrfutil. +variant = env.GetProjectOption("custom_variant", "") +if variant == "t1000e": + env.AddCustomTarget( + name="uf2", + dependencies="$BUILD_DIR/${PROGNAME}.hex", + actions=[generate_uf2_action], + title="Generate UF2", + description="Build a .uf2 file for drag-and-drop flashing" + ) + env.AddCustomTarget( + name="upload", + dependencies="$BUILD_DIR/${PROGNAME}.hex", + actions=[generate_uf2_action, uf2_upload_action], + title="Upload", + description="Upload via UF2 drag-and-drop to the T1000-E DFU volume" + ) +else: + env.AddPreAction("upload", pre_upload) + env.AddPostAction("upload", post_upload) From f1000c01109dde18b418bfbae4f5c0578cd7bb8e Mon Sep 17 00:00:00 2001 From: Henri Bergius Date: Tue, 28 Jul 2026 17:09:47 -1000 Subject: [PATCH 7/8] Fix build --- boards/tracker-t1000-e.json | 2 +- extra_script.py | 53 ++++++++++++++-------------- platformio.ini | 4 ++- uf2.py | 69 +++++++++++++++++++++++++++++++++++++ 4 files changed, 98 insertions(+), 30 deletions(-) diff --git a/boards/tracker-t1000-e.json b/boards/tracker-t1000-e.json index f21c3ee2..44637aa4 100644 --- a/boards/tracker-t1000-e.json +++ b/boards/tracker-t1000-e.json @@ -1,7 +1,7 @@ { "build": { "arduino": { - "ldscript": "nrf52840_s140_v7.ld" + "ldscript": "nrf52840_s140_v6.ld" }, "core": "nRF5", "cpu": "cortex-m4", diff --git a/extra_script.py b/extra_script.py index da609976..2c879312 100644 --- a/extra_script.py +++ b/extra_script.py @@ -91,26 +91,6 @@ def generate_uf2_action(source, target, env): print("*** UF2 generation failed: %s" % exc) return None -def uf2_upload_action(source, target, env): - uf2_path = generate_uf2_action(source, target, env) - if not uf2_path: - return - vol = find_dfu_volume("T1000") - if vol: - dest = vol + "/" + os.path.basename(uf2_path) - shutil.copy(uf2_path, dest) - print("*** Copied %s -> %s" % (uf2_path, dest)) - print("*** The board reboots into the application once the copy completes.") - else: - print("***") - print("*** No T1000-E DFU volume was found.") - print("*** To flash:") - print("*** 1. Put the board in DFU mode: rapidly disconnect and reconnect") - print("*** USB (or double-tap reset) until a T1000-E drive mounts.") - print("*** 2. Drag this file onto it:") - print("*** %s" % uf2_path) - print("***") - def pre_upload(source, target, env): print("*** Executing pre_upload steps...") # do some actions @@ -295,7 +275,11 @@ def firmware_package(env): zip_cmd += build_dir + "/" + env.subst("$PROGNAME") + ".map " env.Execute(zip_cmd) elif (platform == "nordicnrf52"): - env.Execute("cp " + build_dir + "/" + env.subst("$PROGNAME") + ".zip " + project_dir + "/Release/.") + # The T1000-E ships a UF2 image, not an nrfutil DFU .zip (its + # upload_protocol is "custom"), so only copy the .zip for the + # serial-DFU boards that actually produce one. + if variant != "t1000e": + env.Execute("cp " + build_dir + "/" + env.subst("$PROGNAME") + ".zip " + project_dir + "/Release/.") # UF2 image for Adafruit/Seeed mass-storage-bootloader boards (T1000-E). uf2_artifact = build_dir + "/" + env.subst("$PROGNAME") + ".uf2" if os.path.exists(uf2_artifact): @@ -353,9 +337,15 @@ def firmware_package(env): # .hex is emitted by the Arduino core's objcopy step, so this post-action # fires once it exists. Harmless on boards that never use the .uf2. env.AddPostAction("$BUILD_DIR/${PROGNAME}.hex", generate_uf2_action) + # The T1000-E is a UF2 board: with upload_protocol = custom the nrfutil + # DFU .zip is never produced, so package the .hex (whose post-action above + # also emits the distributable .uf2) instead of the .zip. + _package_dep = ("$BUILD_DIR/${PROGNAME}.hex" + if env.GetProjectOption("custom_variant", "") == "t1000e" + else "$BUILD_DIR/${PROGNAME}.zip") env.AddCustomTarget( name="package", - dependencies="$BUILD_DIR/${PROGNAME}.zip", + dependencies=_package_dep, actions=[ target_package ], @@ -389,12 +379,19 @@ def firmware_package(env): title="Generate UF2", description="Build a .uf2 file for drag-and-drop flashing" ) - env.AddCustomTarget( - name="upload", - dependencies="$BUILD_DIR/${PROGNAME}.hex", - actions=[generate_uf2_action, uf2_upload_action], - title="Upload", - description="Upload via UF2 drag-and-drop to the T1000-E DFU volume" + # The T1000-E flashes via UF2 mass storage, not serial DFU. Drive the + # platform's own "upload" target (seeed_t1000e sets upload_protocol = + # custom) with a UPLOADCMD that converts the linked .hex to UF2 and copies + # it onto the mounted DFU volume. + # + # We deliberately do NOT call env.AddCustomTarget(name="upload", ...): + # extra_script.py runs as a `pre:` script, i.e. *before* the nordicnrf52 + # platform registers its "upload" target in main.py. Registering the same + # name twice trips `assert name not in env["__PIO_TARGETS"]` inside PIO's + # piotarget.AddTarget and aborts the build. + project_dir = env.subst("$PROJECT_DIR") + env.Replace( + UPLOADCMD='"$PYTHONEXE" "%s/uf2.py" --hex "$SOURCE"' % project_dir ) else: env.AddPreAction("upload", pre_upload) diff --git a/platformio.ini b/platformio.ini index 1ea0b641..82407057 100644 --- a/platformio.ini +++ b/platformio.ini @@ -1051,7 +1051,9 @@ build_flags = -DRNS_PATH_TABLE_SEGMENT_COUNT=6 lib_deps = ${env:embedded-nrf52.lib_deps} -upload_protocol = nrfutil + https://github.com/attermann/microStore.git + https://github.com/attermann/microReticulum.git +upload_protocol = custom diff --git a/uf2.py b/uf2.py index 15bc31f8..1fbd3b1a 100644 --- a/uf2.py +++ b/uf2.py @@ -128,3 +128,72 @@ def find_dfu_volume(name_token="T1000"): if os.path.exists(os.path.join(candidate, "INFO_UF2.TXT")): return candidate return None + + +def upload_via_uf2(hex_path, uf2_path=None, volume_token="T1000"): + """Generate a UF2 from an Intel HEX image and, if a UF2 mass-storage + bootloader volume is currently mounted, copy it there. Intended to back + the platform's `upload` target for boards such as the Seeed T1000-E that + flash by drag-and-drop rather than serial DFU. Returns 0 on success. + + When no DFU volume is found this prints step-by-step drag-and-drop + instructions instead of failing, so the same command works whether or not + the board is currently in bootloader mode.""" + import shutil + + if not uf2_path: + base, _ = os.path.splitext(hex_path) + uf2_path = base + ".uf2" + + try: + out, blocks = generate_uf2(hex_path, uf2_path) + print("*** Generated UF2: %s (%d blocks)" % (out, blocks)) + except Exception as exc: + print("*** UF2 generation failed: %s" % exc) + return 1 + + vol = find_dfu_volume(volume_token) + if vol: + dest = os.path.join(vol, os.path.basename(uf2_path)) + shutil.copy(uf2_path, dest) + print("*** Copied %s -> %s" % (uf2_path, dest)) + print("*** The board reboots into the application once the copy completes.") + else: + print("***") + print("*** No %s DFU volume was found." % volume_token) + print("*** To flash:") + print("*** 1. Put the board in DFU mode: rapidly disconnect and reconnect") + print("*** USB (or double-tap reset) until a %s drive mounts." % volume_token) + print("*** 2. Drag this file onto it:") + print("*** %s" % uf2_path) + print("***") + return 0 + + +def _cli_main(argv=None): + """Command-line entry point: `python uf2.py --hex firmware.hex`. + + Used as UPLOADCMD for the T1000-E upload target so the UF2 conversion + + drag-and-drop copy can run outside the SCons action callbacks.""" + import argparse + import sys + + parser = argparse.ArgumentParser( + description="Convert an Intel HEX firmware image to UF2 and copy it " + "to a mounted UF2 (mass-storage) bootloader volume.") + parser.add_argument("--hex", required=True, + help="Path to the input Intel HEX firmware image.") + parser.add_argument("--uf2", + help="Path for the generated UF2 file. Defaults to the " + ".hex path with a .uf2 extension.") + parser.add_argument("--volume-token", default="T1000", + help="Token used to locate the mounted DFU volume " + "(matched case-insensitively against the volume " + "name). Default: T1000.") + args = parser.parse_args(argv) + return upload_via_uf2(args.hex, args.uf2, args.volume_token) + + +if __name__ == "__main__": + import sys + sys.exit(_cli_main()) From 54e4fb8db64c914743f7063cb88af82d8b441b8e Mon Sep 17 00:00:00 2001 From: Henri Bergius Date: Tue, 28 Jul 2026 17:14:52 -1000 Subject: [PATCH 8/8] Fix upload --- extra_script.py | 2 +- uf2.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/extra_script.py b/extra_script.py index 2c879312..7b773192 100644 --- a/extra_script.py +++ b/extra_script.py @@ -86,7 +86,7 @@ def generate_uf2_action(source, target, env): try: out, blocks = generate_uf2(hex_path, uf2_path) print("*** Generated UF2: %s (%d blocks)" % (out, blocks)) - return uf2_path + return None except Exception as exc: print("*** UF2 generation failed: %s" % exc) return None diff --git a/uf2.py b/uf2.py index 1fbd3b1a..be150058 100644 --- a/uf2.py +++ b/uf2.py @@ -11,6 +11,8 @@ # # Licensed under the MIT license. +import os +import shutil import struct # UF2 on-wire constants.