mirror of
https://github.com/alexhopeoconnor/firmware.git
synced 2026-10-15 07:51:19 +10:00
* Add ARCH_PORTDUINO_WASM build: meshtasticd in WebAssembly over WebUSB
Compile the full portduino firmware to WebAssembly (Emscripten) so a real node runs in a browser tab or headless Node, driving a LoRa radio over WebUSB through a CH341 — the desktop Ch341Hal path with its libusb backend swapped for a WebUSB one. The native/desktop portduino build is unchanged.
New build env under src/platform/portduino/wasm/ (excluded from the native build_src_filter): WebUSB libpinedio backend, config/FS/region/MAC/PhoneAPI glue, wasm setup/loop, JS WebUSB runtime, and build stubs. bin/build-portduino-wasm.sh runs a standalone cached emcc build to build/wasm/meshnode.{mjs,wasm}.
Six firmware sources gain #ifdef ARCH_PORTDUINO_WASM guards (single-threaded cooperative emscripten_sleep loop, continuous RX, US region default, std RNG, no-popen exec); none affect non-wasm builds.
* PortDuino WASM: CI build job, cross-platform libdeps, configurable adapter
bin/build-portduino-wasm.sh: auto-detect native-macos (macOS) vs native (Linux/CI) libdeps with a NATIVE_ENV override, and use the SAME env's Crypto with an XEdDSA-present guard. Drops the heltec-v3/Crypto borrow — the meshtastic/Crypto pin already ships XEdDSA; the native libdeps cache was just stale. No env pin change.
Add .github/workflows/build_portduino_wasm.yml: build the ARCH_PORTDUINO_WASM target in CI (ubuntu + emsdk, native libdeps) so it can't silently bit-rot; asserts build/wasm/meshnode.{mjs,wasm}.
src/platform/portduino/wasm: add wasm_set_lora_* setters so the JS host can configure any CH341 LoRa adapter (module, USB ids, DIO/TCXO, SPI speed, pins); wasm_config_apply falls back to the MeshToad defaults when unset.
* WASM: build as a first-class [env:wasm] via platform-wasm (retire emcc script)
Replace the standalone emcc build (bin/build-portduino-wasm.sh) with a normal
PlatformIO env, `pio run -e wasm`, using the new meshtastic/platform-wasm
platform (emcc/em++ + Asyncify, the WASM sibling of platform-native). The
portduino WebAssembly node is now built the same way as every other target.
- variants/native/portduino/platformio.ini: add [env:wasm] (platform pinned to
platform-wasm, board wasm). Translates the script's source set into a curated
build_src_filter, the ~30 EXCLUDE_* defines, and the lib set; defines
ARCH_PORTDUINO_WASM in-repo so correctness doesn't hinge on the platform's
board.json.
- extra_scripts/wasm_link_flags.py: the firmware-specific emcc *link* settings
(EXPORT_NAME, EXPORTED_RUNTIME_METHODS, EXPORTED_FUNCTIONS, ASYNCIFY_IMPORTS).
PlatformIO feeds build_flags to compile only, so these must ride LINKFLAGS;
without it the WebUSB Asyncify seam and the JS host's runtime methods are
dropped (the _wasm_* exports survive only via EMSCRIPTEN_KEEPALIVE).
- PortduinoGlue.{h,cpp}: guard the yaml-cpp dependency out of the WASM build
(#ifndef ARCH_PORTDUINO_WASM around the include, emit_yaml/loadConfig/
readGPIOFromYaml). The browser node configures via the wasm_set_lora_* setters
and dead-strips the YAML path; this drops the host yaml-cpp build dependency
entirely. Native is unchanged (guards are inert there).
- portduino_glue_wasm.cpp / portduino_main_wasm.cpp: repair EM_ASM JS that a
formatter had mangled (!== -> != =, regex split) in the prior landing; the
emcc link succeeds regardless, so CI now runs `node --check meshnode.mjs`.
- .github/workflows/build_portduino_wasm.yml: build via `pio run -e wasm`
(artifacts under .pio/build/wasm/), trigger on the shared inputs the env
inherits (root platformio.ini, bin/platformio-*.py).
- NodeDB.cpp: drop the dead ARCH_PORTDUINO_WASM region-default branch (region
now defaults the same as native).
- Crypto renovate pins: add the missing gitBranch so they track upstream.
Output: .pio/build/wasm/meshnode.{mjs,wasm} (ES module, factory createMeshNode).
Verified: pio run -e wasm (against the published platform archive), node --check,
module instantiates in Node with all exports; native-macos + Docker native unit
tests (450/450) still pass.
* Fix name on the Piggystick
* wasm: pin platform-wasm at the GPL-3.0-relicensed commit
platform-wasm's LICENSE was always GPLv3 (matching this firmware), but its
platform.json/README still declared Apache-2.0 (mis-copied from platform-native).
That's fixed upstream in b83fa5b; bump the [env:wasm] pin to it. Build output is
unchanged (license metadata only). Verified: pio run -e wasm against b83fa5b.
* wasm: make reboot() actually restart the node (was a no-op)
In wasm the reboot path is live (main.cpp -> Power::powerCommandsCheck ->
Power::reboot), but Power::reboot's ARCH_PORTDUINO arm tore down SPI/Wire/Serial
and then called the no-op ::reboot() stub — leaving the node running with a dead
radio until the tab was manually reloaded. Triggers include an admin/phone
reboot, factory reset, the "reconfigure failed" path, and the 60 s stuck-TX
hardware watchdog (RadioLibInterface).
- Power::reboot(): add an ARCH_PORTDUINO_WASM arm (before ARCH_PORTDUINO, since
the wasm build defines both) that skips the host teardown and just calls
::reboot(). notifyReboot already let modules persist.
- ::reboot() (glue): hand off to the JS host — browser reloads the tab (NodeDB
state survives via IDBFS, same identity returns); headless calls Module.onReboot
if provided, else logs. Loose !=/== so clang-format doesn't mangle the EM_ASM JS.
- README: document the reboot handoff + the Module.onReboot hook.
Verified: pio run -e wasm + node --check (EM_ASM intact); native-macos unaffected.
* wasm: rename env to native-wasm and run it in the main CI matrix
Rename [env:wasm] -> [env:native-wasm] for consistency with the portduino
native family (native, native-macos, native-tft). The build dir follows to
.pio/build/native-wasm/ (artifact is still meshnode.{mjs,wasm}); the PIOENV
guard in extra_scripts/wasm_link_flags.py, the README, and the companion wrapper
move with it. The board stays `wasm`.
Also wire the build into normal CI: build_portduino_wasm.yml becomes a reusable
workflow (workflow_call) invoked as the `build-wasm` job of main_matrix.yml, so
the WebAssembly node is built like every other platform instead of on a separate
path trigger.
* native-wasm: auto-locate the Emscripten SDK (pre-build script)
`pio run -e native-wasm` failed with "emcc not found" whenever it was invoked
from a shell that hadn't sourced emsdk_env.sh — a VS Code task, an IDE build
button, a bare terminal. Add a pre: extra script that probes the usual emsdk
locations ($EMSDK_ENV, $EMSDK, ~/emsdk, ./.emsdk, the sibling companion
checkout), sources emsdk_env.sh, and imports the resulting environment so the
platform builder and emcc see PATH/EMSDK/EM_CONFIG. No-op when emcc is already
reachable (CI), silent when no SDK is found (the platform emits its own error).
* wasm: address PR review feedback
- js/bridge.js: import CH341 from "./ch341.js" (sibling in this layout), not
"../src/ch341.js" which doesn't resolve here.
- js/ch341.js: a zero-length transferIn while MISO bytes are still outstanding
now throws instead of breaking out with a partially-filled buffer — silent SPI
corruption becomes a loud error, matching the comment above it.
- libpinedio_webusb.c: webusb_set_auto_cs honors the AUTO_CS option (? 1 : 0)
instead of the always-on ? 1 : 1. Runtime behavior is unchanged — Ch341Hal sets
AUTO_CS=0 right after pinedio_init (RadioLib drives the active-low NSS); the
option just isn't set yet at init, so this now correctly defaults off.
- SX126xInterface.cpp: the RX-start error log now names the method actually
called (startReceive vs startReceiveDutyCycleAuto) instead of hardcoding the
duty-cycle name in the WASM branch.
* native-wasm: drop the emsdk bootstrap shim (now in platform-wasm)
The Emscripten SDK auto-location moved into the platform-wasm builder, so the
firmware no longer needs its own pre: extra script. Remove
extra_scripts/wasm_emsdk_env.py and bump the platform pin to the build that
carries the bootstrap. The wasm_link_flags.py post script stays — those exported
fns / runtime methods / Asyncify import seam are firmware-app-specific.
* wasm: use the canonical companion name (meshtasticd-wasm-node)
The companion repo was renamed meshtastic-web-node -> meshtasticd-wasm-node; fix
the stale name in the wasm README and bump the platform pin to the build that
promotes the canonical name in its emsdk auto-location.
* wasm: re-entrancy guard for the API/region entry points + flaky-open retry
Two robustness fixes for the browser node:
- Re-entrancy guard. The node is single-threaded + Asyncify: while setup()/loop()
is suspended inside a WebUSB transfer, the JS event loop is free, so a stray
DOM/timer callback that re-enters a wasm_* entry point starts a second Asyncify
unwind ("async operation already in flight" abort) or clobbers shared PhoneAPI
state (observed as a "PhoneAPI::available unexpected state" flood). Add a
g_wasm_in_firmware flag set around setup()/loop() (portduino_main_wasm.cpp); the
wasm_set_region / wasm_api_to_radio / wasm_api_from_radio / wasm_api_available
entry points now reject a mid-tick call (return busy) instead of corrupting or
aborting. The host must still call them between ticks — this is the safety net
the design lacked, not a substitute for the JS queue.
- CH341 open retry (js/bridge.js). First-connect WebUSB is flaky — the interface
is briefly unclaimable right after the grant, or held by a prior session,
giving a transient "Could not open SPI: -1". Retry the open with a short
backoff, closing the device between attempts so claimInterface starts clean.
* wasm: exclude emscripten-only sources from cppcheck
The `check` board matrix runs `pio check` (cppcheck) over all of src/,
including src/platform/portduino/wasm/. cppcheck can't parse the EM_ASYNC_JS/
EM_JS macros (Syntax Error: AST broken at libpinedio_webusb.c:39,
internalAstError) and these sources are not part of any checked board build
([env:native-wasm] is board_level=extra, compiled by the build-wasm CI job).
Suppress the wasm dir in suppressions.txt, the same way generated/ and .pio/
are already excluded.
* wasm: coalesce FS.syncfs so two never run at once
IDBFS syncfs is async; the explicit wasm_fs_sync (5s timer + post-save +
beforeunload) could overlap a prior in-flight sync, warning "2 FS.syncfs
operations in flight at once". Serialize: if a sync is running, mark a pending
re-sync and let the in-flight one chain it on completion — at most one in flight,
trailing writes still flushed. (Companion drops IDBFS autoPersist so this is the
single persistence path.)
* wasm: silence false-positive SAST on the emscripten glue
- extra_scripts/wasm_link_flags.py: restore the trunk-ignore-all(ruff/F821,
flake8/F821) header every other SCons extra_script carries; Import/env are
SConscript-injected globals, so ruff/flake8 flag them as undefined.
- .semgrepignore: exclude src/platform/portduino/wasm/js/ (browser WebUSB glue,
not part of the firmware binary). The unsafe-formatstring rule false-positives
on its benign retry/diagnostic console logs.
* Update .github/workflows/build_portduino_wasm.yml
Co-authored-by: Austin <vidplace7@gmail.com>
---------
Co-authored-by: Austin <vidplace7@gmail.com>
497 lines
19 KiB
C++
497 lines
19 KiB
C++
#if RADIOLIB_EXCLUDE_SX126X != 1
|
|
#include "SX126xInterface.h"
|
|
#include "configuration.h"
|
|
#include "error.h"
|
|
#include "mesh/NodeDB.h"
|
|
#ifdef ARCH_PORTDUINO
|
|
#include "PortduinoGlue.h"
|
|
#endif
|
|
#if defined(ARCH_ESP32)
|
|
#include <driver/rtc_io.h>
|
|
#include <esp_sleep.h>
|
|
#endif
|
|
|
|
#include "Throttle.h"
|
|
|
|
// Particular boards might define a different max power based on what their hardware can do, default to max power output if not
|
|
// specified (may be dangerous if using external PA and SX126x power config forgotten)
|
|
#if ARCH_PORTDUINO
|
|
#define SX126X_MAX_POWER portduino_config.sx126x_max_power
|
|
#endif
|
|
#ifndef SX126X_MAX_POWER
|
|
#define SX126X_MAX_POWER 22
|
|
#endif
|
|
|
|
template <typename T>
|
|
SX126xInterface<T>::SX126xInterface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst,
|
|
RADIOLIB_PIN_TYPE busy)
|
|
: RadioLibInterface(hal, cs, irq, rst, busy, &lora), lora(&module)
|
|
{
|
|
LOG_DEBUG("SX126xInterface(cs=%d, irq=%d, rst=%d, busy=%d)", cs, irq, rst, busy);
|
|
}
|
|
|
|
/// Initialise the Driver transport hardware and software.
|
|
/// Make sure the Driver is properly configured before calling init().
|
|
/// \return true if initialisation succeeded.
|
|
template <typename T> bool SX126xInterface<T>::init()
|
|
{
|
|
|
|
// Typically, the RF switch on SX126x boards is controlled by two signals, which are negations of each other (switched RFIO
|
|
// paths). The negation is usually performed in hardware, or (suboptimal design) TXEN and RXEN are the two inputs to this style of
|
|
// RF switch. On some boards, there is no hardware negation between CTRL and ¬CTRL, but CTRL is internally connected to DIO2, and
|
|
// DIO2's switching is done by the SX126X itself, so the MCU can't control ¬CTRL at exactly the same time. One solution would be
|
|
// to set ¬CTRL as SX126X_TXEN or SX126X_RXEN, but they may already be used for another purpose, such as controlling another
|
|
// PA/LNA. Keeping ¬CTRL high seems to work, as long CTRL=1, ¬CTRL=1 has the opposite and stable RF path effect as CTRL=0 and
|
|
// ¬CTRL=1, this depends on the RF switch, but it seems this usually works. Better hardware design, which is done most the time,
|
|
// means this workaround is not necessary.
|
|
#ifdef SX126X_ANT_SW // Perhaps add RADIOLIB_NC check, and beforehand define as such if it is undefined, but it is not commonly
|
|
// used and not part of the 'default' set of pin definitions.
|
|
digitalWrite(SX126X_ANT_SW, HIGH);
|
|
pinMode(SX126X_ANT_SW, OUTPUT);
|
|
#endif
|
|
|
|
#ifdef SX126X_POWER_EN // Perhaps add RADIOLIB_NC check, and beforehand define as such if it is undefined, but it is not commonly
|
|
// used and not part of the 'default' set of pin definitions.
|
|
pinMode(SX126X_POWER_EN, OUTPUT);
|
|
digitalWrite(SX126X_POWER_EN, HIGH);
|
|
#endif
|
|
|
|
#if HAS_LORA_FEM
|
|
loraFEMInterface.init();
|
|
// Apply saved FEM LNA mode from config
|
|
if (loraFEMInterface.isLnaCanControl()) {
|
|
loraFEMInterface.setLNAEnable(config.lora.fem_lna_mode != meshtastic_Config_LoRaConfig_FEM_LNA_Mode_DISABLED);
|
|
}
|
|
#endif
|
|
|
|
#ifdef RF95_FAN_EN
|
|
pinMode(RF95_FAN_EN, OUTPUT);
|
|
digitalWrite(RF95_FAN_EN, HIGH);
|
|
#endif
|
|
|
|
#if ARCH_PORTDUINO
|
|
tcxoVoltage = (float)portduino_config.dio3_tcxo_voltage / 1000;
|
|
if (portduino_config.lora_sx126x_ant_sw_pin.pin != RADIOLIB_NC) {
|
|
digitalWrite(portduino_config.lora_sx126x_ant_sw_pin.pin, HIGH);
|
|
pinMode(portduino_config.lora_sx126x_ant_sw_pin.pin, OUTPUT);
|
|
}
|
|
#endif
|
|
if (tcxoVoltage == 0.0)
|
|
LOG_DEBUG("SX126X_DIO3_TCXO_VOLTAGE not defined, not using DIO3 as TCXO reference voltage");
|
|
else
|
|
LOG_DEBUG("SX126X_DIO3_TCXO_VOLTAGE defined, using DIO3 as TCXO reference voltage at %f V", tcxoVoltage);
|
|
setTransmitEnable(false);
|
|
// FIXME: May want to set depending on a definition, currently all SX126x variant files use the DC-DC regulator option
|
|
bool useRegulatorLDO = false; // Seems to depend on the connection to pin 9/DCC_SW - if an inductor DCDC?
|
|
|
|
RadioLibInterface::init();
|
|
|
|
limitPower(SX126X_MAX_POWER);
|
|
// Make sure we reach the minimum power supported to turn the chip on (-9dBm)
|
|
if (power < -9)
|
|
power = -9;
|
|
|
|
int res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage, useRegulatorLDO);
|
|
|
|
#ifdef SX126X_PA_RAMP_US
|
|
// Set custom PA ramp time for boards requiring longer stabilization (e.g., T-Beam 1W needs >800us)
|
|
if (res == RADIOLIB_ERR_NONE) {
|
|
lora.setPaRampTime(SX126X_PA_RAMP_US);
|
|
}
|
|
#endif
|
|
// \todo Display actual typename of the adapter, not just `SX126x`
|
|
LOG_INFO("SX126x init result %d", res);
|
|
if (res == RADIOLIB_ERR_CHIP_NOT_FOUND || res == RADIOLIB_ERR_SPI_CMD_FAILED)
|
|
return false;
|
|
|
|
LOG_INFO("Frequency set to %f", getFreq());
|
|
LOG_INFO("Bandwidth set to %f", bw);
|
|
LOG_INFO("Power output set to %d", power);
|
|
|
|
// Overriding current limit
|
|
// (https://github.com/jgromes/RadioLib/blob/690a050ebb46e6097c5d00c371e961c1caa3b52e/src/modules/SX126x/SX126x.cpp#L85) using
|
|
// value in SX126xInterface.h (currently 140 mA) It may or may not be necessary, depending on how RadioLib functions, from
|
|
// SX1261/2 datasheet: OCP after setting DeviceSel with SetPaConfig(): SX1261 - 60 mA, SX1262 - 140 mA For the SX1268 the IC
|
|
// defaults to 140mA no matter the set power level, but RadioLib set it lower, this would need further checking Default values
|
|
// are: SX1262, SX1268: 0x38 (140 mA), SX1261: 0x18 (60 mA)
|
|
// FIXME: Not ideal to increase SX1261 current limit above 60mA as it can only transmit max 15dBm, should probably only do it
|
|
// if using SX1262 or SX1268
|
|
res = lora.setCurrentLimit(currentLimit);
|
|
LOG_DEBUG("Current limit set to %f", currentLimit);
|
|
LOG_DEBUG("Current limit set result %d", res);
|
|
|
|
if (res == RADIOLIB_ERR_NONE) {
|
|
#ifdef SX126X_DIO2_AS_RF_SWITCH
|
|
bool dio2AsRfSwitch = true;
|
|
#elif defined(ARCH_PORTDUINO)
|
|
bool dio2AsRfSwitch = false;
|
|
if (portduino_config.dio2_as_rf_switch) {
|
|
dio2AsRfSwitch = true;
|
|
}
|
|
#else
|
|
bool dio2AsRfSwitch = false;
|
|
#endif
|
|
res = lora.setDio2AsRfSwitch(dio2AsRfSwitch);
|
|
LOG_DEBUG("Set DIO2 as %sRF switch, result: %d", dio2AsRfSwitch ? "" : "not ", res);
|
|
}
|
|
|
|
// If a pin isn't defined, we set it to RADIOLIB_NC, it is safe to always do external RF switching with RADIOLIB_NC as it has
|
|
// no effect
|
|
#if ARCH_PORTDUINO
|
|
if (res == RADIOLIB_ERR_NONE) {
|
|
LOG_DEBUG("Use MCU pin %i as RXEN and pin %i as TXEN to control RF switching", portduino_config.lora_rxen_pin.pin,
|
|
portduino_config.lora_txen_pin.pin);
|
|
lora.setRfSwitchPins(portduino_config.lora_rxen_pin.pin, portduino_config.lora_txen_pin.pin);
|
|
}
|
|
#else
|
|
#ifndef SX126X_RXEN
|
|
#define SX126X_RXEN RADIOLIB_NC
|
|
LOG_DEBUG("SX126X_RXEN not defined, defaulting to RADIOLIB_NC");
|
|
#endif
|
|
#ifndef SX126X_TXEN
|
|
#define SX126X_TXEN RADIOLIB_NC
|
|
LOG_DEBUG("SX126X_TXEN not defined, defaulting to RADIOLIB_NC");
|
|
#endif
|
|
if (res == RADIOLIB_ERR_NONE) {
|
|
LOG_DEBUG("Use MCU pin %i as RXEN and pin %i as TXEN to control RF switching", SX126X_RXEN, SX126X_TXEN);
|
|
lora.setRfSwitchPins(SX126X_RXEN, SX126X_TXEN);
|
|
}
|
|
#endif
|
|
if (config.lora.sx126x_rx_boosted_gain) {
|
|
uint16_t result = lora.setRxBoostedGainMode(true);
|
|
LOG_INFO("Set RX gain to boosted mode; result: %d", result);
|
|
} else {
|
|
uint16_t result = lora.setRxBoostedGainMode(false);
|
|
LOG_INFO("Set RX gain to power saving mode (boosted mode off); result: %d", result);
|
|
}
|
|
|
|
// Undocumented SX1262 register patch recommended by Heltec/Semtech for improved RX sensitivity.
|
|
// Sets bit 0 of register 0x8B5.
|
|
if (module.SPIsetRegValue(0x8B5, 0x01, 0, 0) == RADIOLIB_ERR_NONE) {
|
|
LOG_INFO("Applied SX1262 register 0x8B5 patch for RX improvement");
|
|
} else {
|
|
LOG_WARN("Failed to apply SX1262 register 0x8B5 patch for RX improvement");
|
|
}
|
|
|
|
#if 0
|
|
// Read/write a register we are not using (only used for FSK mode) to test SPI comms
|
|
uint8_t crcLSB = 0;
|
|
int err = lora.readRegister(SX126X_REG_CRC_POLYNOMIAL_LSB, &crcLSB, 1);
|
|
if(err != RADIOLIB_ERR_NONE)
|
|
RECORD_CRITICALERROR(CriticalErrorCode_SX1262Failure);
|
|
|
|
//if(crcLSB != 0x0f)
|
|
// RECORD_CRITICALERROR(CriticalErrorCode_SX1262Failure);
|
|
|
|
crcLSB = 0x5a;
|
|
err = lora.writeRegister(SX126X_REG_CRC_POLYNOMIAL_LSB, &crcLSB, 1);
|
|
if(err != RADIOLIB_ERR_NONE)
|
|
RECORD_CRITICALERROR(CriticalErrorCode_SX1262Failure);
|
|
|
|
err = lora.readRegister(SX126X_REG_CRC_POLYNOMIAL_LSB, &crcLSB, 1);
|
|
if(err != RADIOLIB_ERR_NONE)
|
|
RECORD_CRITICALERROR(CriticalErrorCode_SX1262Failure);
|
|
|
|
if(crcLSB != 0x5a)
|
|
RECORD_CRITICALERROR(CriticalErrorCode_SX1262Failure);
|
|
// If we got this far register accesses (and therefore SPI comms) are good
|
|
#endif
|
|
|
|
if (res == RADIOLIB_ERR_NONE)
|
|
res = lora.setCRC(RADIOLIB_SX126X_LORA_CRC_ON);
|
|
|
|
if (res == RADIOLIB_ERR_NONE)
|
|
startReceive(); // start receiving
|
|
|
|
return res == RADIOLIB_ERR_NONE;
|
|
}
|
|
|
|
template <typename T> bool SX126xInterface<T>::reconfigure()
|
|
{
|
|
RadioLibInterface::reconfigure();
|
|
|
|
// set mode to standby
|
|
setStandby();
|
|
|
|
// configure publicly accessible settings
|
|
int err = lora.setSpreadingFactor(sf);
|
|
if (err != RADIOLIB_ERR_NONE)
|
|
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
|
|
|
|
err = lora.setBandwidth(bw);
|
|
if (err != RADIOLIB_ERR_NONE)
|
|
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
|
|
|
|
err = lora.setCodingRate(cr);
|
|
if (err != RADIOLIB_ERR_NONE)
|
|
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
|
|
|
|
err = lora.setSyncWord(syncWord);
|
|
if (err != RADIOLIB_ERR_NONE)
|
|
LOG_ERROR("SX126X setSyncWord %s%d", radioLibErr, err);
|
|
assert(err == RADIOLIB_ERR_NONE);
|
|
|
|
err = lora.setCurrentLimit(currentLimit);
|
|
if (err != RADIOLIB_ERR_NONE)
|
|
LOG_ERROR("SX126X setCurrentLimit %s%d", radioLibErr, err);
|
|
assert(err == RADIOLIB_ERR_NONE);
|
|
|
|
err = lora.setPreambleLength(preambleLength);
|
|
if (err != RADIOLIB_ERR_NONE)
|
|
LOG_ERROR("SX126X setPreambleLength %s%d", radioLibErr, err);
|
|
assert(err == RADIOLIB_ERR_NONE);
|
|
|
|
err = lora.setFrequency(getFreq());
|
|
if (err != RADIOLIB_ERR_NONE)
|
|
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
|
|
|
|
limitPower(SX126X_MAX_POWER);
|
|
// Make sure we reach the minimum power supported to turn the chip on (-9dBm)
|
|
if (power < -9)
|
|
power = -9;
|
|
|
|
err = lora.setOutputPower(power);
|
|
if (err != RADIOLIB_ERR_NONE)
|
|
LOG_ERROR("SX126X setOutputPower %s%d", radioLibErr, err);
|
|
assert(err == RADIOLIB_ERR_NONE);
|
|
|
|
// Apply RX gain mode — valid in STDBY (datasheet §9.6), matches resetAGC() pattern
|
|
err = lora.setRxBoostedGainMode(config.lora.sx126x_rx_boosted_gain);
|
|
if (err != RADIOLIB_ERR_NONE)
|
|
LOG_WARN("SX126X setRxBoostedGainMode %s%d", radioLibErr, err);
|
|
|
|
startReceive(); // restart receiving
|
|
|
|
return true;
|
|
}
|
|
|
|
template <typename T> int16_t SX126xInterface<T>::getCurrentRSSI()
|
|
{
|
|
float rssi = lora.getRSSI(false);
|
|
return (int16_t)round(rssi);
|
|
}
|
|
|
|
template <typename T> void SX126xInterface<T>::disableInterrupt()
|
|
{
|
|
lora.clearDio1Action();
|
|
}
|
|
|
|
template <typename T> void SX126xInterface<T>::setStandby()
|
|
{
|
|
checkNotification(); // handle any pending interrupts before we force standby
|
|
|
|
int err = lora.standby();
|
|
|
|
if (err != RADIOLIB_ERR_NONE)
|
|
LOG_DEBUG("SX126x standby %s%d", radioLibErr, err);
|
|
#ifdef ARCH_PORTDUINO
|
|
if (err != RADIOLIB_ERR_NONE)
|
|
portduino_status.LoRa_in_error = true;
|
|
#else
|
|
assert(err == RADIOLIB_ERR_NONE);
|
|
#endif
|
|
isReceiving = false; // If we were receiving, not any more
|
|
activeReceiveStart = 0;
|
|
disableInterrupt();
|
|
completeSending(); // If we were sending, not anymore
|
|
RadioLibInterface::setStandby();
|
|
}
|
|
|
|
/**
|
|
* Add SNR data to received messages
|
|
*/
|
|
template <typename T> void SX126xInterface<T>::addReceiveMetadata(meshtastic_MeshPacket *mp)
|
|
{
|
|
// LOG_DEBUG("PacketStatus %x", lora.getPacketStatus());
|
|
mp->rx_snr = lora.getSNR();
|
|
mp->rx_rssi = lround(lora.getRSSI());
|
|
LOG_DEBUG("Corrected frequency offset: %f", lora.getFrequencyError());
|
|
}
|
|
|
|
/** We override to turn on transmitter power as needed.
|
|
*/
|
|
template <typename T> void SX126xInterface<T>::configHardwareForSend()
|
|
{
|
|
setTransmitEnable(true);
|
|
RadioLibInterface::configHardwareForSend();
|
|
}
|
|
|
|
// For power draw measurements, helpful to force radio to stay sleeping
|
|
// #define SLEEP_ONLY
|
|
|
|
template <typename T> void SX126xInterface<T>::startReceive()
|
|
{
|
|
#ifdef SLEEP_ONLY
|
|
sleep();
|
|
#else
|
|
|
|
setTransmitEnable(false);
|
|
setStandby();
|
|
|
|
#ifdef ARCH_PORTDUINO_WASM
|
|
// Continuous RX in the browser: duty-cycle sleep parks BUSY high between RX
|
|
// windows and stalls the slow WebUSB SPI link. No battery to save here.
|
|
int err = lora.startReceive(RADIOLIB_SX126X_RX_TIMEOUT_INF, MESHTASTIC_RADIOLIB_IRQ_RX_FLAGS);
|
|
const char *rxMethod = "startReceive";
|
|
#else
|
|
// We use a 16 bit preamble so this should save some power by letting radio sit in standby mostly.
|
|
int err = lora.startReceiveDutyCycleAuto(preambleLength, 8, MESHTASTIC_RADIOLIB_IRQ_RX_FLAGS);
|
|
const char *rxMethod = "startReceiveDutyCycleAuto";
|
|
#endif
|
|
if (err != RADIOLIB_ERR_NONE)
|
|
LOG_ERROR("SX126X %s %s%d", rxMethod, radioLibErr, err);
|
|
#ifdef ARCH_PORTDUINO
|
|
if (err != RADIOLIB_ERR_NONE)
|
|
portduino_status.LoRa_in_error = true;
|
|
#else
|
|
assert(err == RADIOLIB_ERR_NONE);
|
|
#endif
|
|
|
|
RadioLibInterface::startReceive();
|
|
|
|
// Must be done AFTER, starting transmit, because startTransmit clears (possibly stale) interrupt pending register bits
|
|
enableInterrupt(isrRxLevel0);
|
|
checkRxDoneIrqFlag();
|
|
#endif
|
|
}
|
|
|
|
/** Is the channel currently active? */
|
|
template <typename T> bool SX126xInterface<T>::isChannelActive()
|
|
{
|
|
// check if we can detect a LoRa preamble on the current channel
|
|
ChannelScanConfig_t cfg = {.cad = {.symNum = NUM_SYM_CAD,
|
|
.detPeak = RADIOLIB_SX126X_CAD_PARAM_DEFAULT,
|
|
.detMin = RADIOLIB_SX126X_CAD_PARAM_DEFAULT,
|
|
.exitMode = RADIOLIB_SX126X_CAD_PARAM_DEFAULT,
|
|
.timeout = 0,
|
|
.irqFlags = RADIOLIB_IRQ_CAD_DEFAULT_FLAGS,
|
|
.irqMask = RADIOLIB_IRQ_CAD_DEFAULT_MASK}};
|
|
int16_t result;
|
|
setTransmitEnable(false);
|
|
setStandby();
|
|
result = lora.scanChannel(cfg);
|
|
if (result == RADIOLIB_LORA_DETECTED)
|
|
return true;
|
|
if (result != RADIOLIB_CHANNEL_FREE)
|
|
LOG_ERROR("SX126X scanChannel %s%d", radioLibErr, result);
|
|
#ifdef ARCH_PORTDUINO
|
|
if (result == RADIOLIB_ERR_WRONG_MODEM)
|
|
portduino_status.LoRa_in_error = true;
|
|
#else
|
|
assert(result != RADIOLIB_ERR_WRONG_MODEM);
|
|
#endif
|
|
|
|
return false;
|
|
}
|
|
|
|
/** Could we send right now (i.e. either not actively receiving or transmitting)? */
|
|
template <typename T> bool SX126xInterface<T>::isActivelyReceiving()
|
|
{
|
|
// The IRQ status will be cleared when we start our read operation. Check if we've started a header, but haven't yet
|
|
// received and handled the interrupt for reading the packet/handling errors.
|
|
return receiveDetected(lora.getIrqFlags(), RADIOLIB_SX126X_IRQ_HEADER_VALID, RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED);
|
|
}
|
|
|
|
template <typename T> bool SX126xInterface<T>::sleep()
|
|
{
|
|
// Not keeping config is busted - next time nrf52 board boots lora sending fails tcxo related? - see datasheet
|
|
// \todo Display actual typename of the adapter, not just `SX126x`
|
|
LOG_DEBUG("SX126x entering sleep mode"); // (FIXME, don't keep config)
|
|
setStandby(); // Stop any pending operations
|
|
|
|
// turn off TCXO if it was powered
|
|
// FIXME - this isn't correct
|
|
// lora.setTCXO(0);
|
|
|
|
// put chipset into sleep mode (we've already disabled interrupts by now)
|
|
bool keepConfig = true;
|
|
lora.sleep(keepConfig); // Note: we do not keep the config, full reinit will be needed
|
|
|
|
#ifdef SX126X_POWER_EN
|
|
digitalWrite(SX126X_POWER_EN, LOW);
|
|
#endif
|
|
|
|
#if HAS_LORA_FEM
|
|
loraFEMInterface.setSleepModeEnable();
|
|
#endif
|
|
|
|
return true;
|
|
}
|
|
|
|
template <typename T> void SX126xInterface<T>::resetAGC()
|
|
{
|
|
// Safety: don't reset mid-packet
|
|
if (sendingPacket != NULL || (isReceiving && isActivelyReceiving()))
|
|
return;
|
|
|
|
LOG_DEBUG("SX126x AGC reset: warm sleep + Calibrate(0x7F)");
|
|
|
|
// 1. Warm sleep — powers down the entire analog frontend, resetting AGC state.
|
|
// A plain standby→startReceive cycle does NOT reset the AGC.
|
|
lora.sleep(true);
|
|
|
|
// 2. Wake to RC standby for stable calibration
|
|
lora.standby(RADIOLIB_SX126X_STANDBY_RC, true);
|
|
|
|
// 3. Calibrate all blocks (ADC, PLL, image, RC oscillators)
|
|
uint8_t calData = RADIOLIB_SX126X_CALIBRATE_ALL;
|
|
module.SPIwriteStream(RADIOLIB_SX126X_CMD_CALIBRATE, &calData, 1, true, false);
|
|
|
|
// 4. Wait for calibration to complete (BUSY pin goes low)
|
|
module.hal->delay(5);
|
|
uint32_t start = millis();
|
|
while (module.hal->digitalRead(module.getGpio())) {
|
|
if (millis() - start > 50)
|
|
break;
|
|
module.hal->yield();
|
|
}
|
|
|
|
if (module.hal->digitalRead(module.getGpio())) {
|
|
LOG_WARN("SX126x AGC reset: calibration did not complete within 50ms");
|
|
startReceive();
|
|
return;
|
|
}
|
|
|
|
// 5. Re-calibrate image rejection for actual operating frequency
|
|
// Calibrate(0x7F) defaults to 902-928 MHz which is wrong for other regions.
|
|
lora.calibrateImage(getFreq());
|
|
|
|
// Re-apply settings that calibration may have reset
|
|
|
|
// DIO2 as RF switch
|
|
#ifdef SX126X_DIO2_AS_RF_SWITCH
|
|
lora.setDio2AsRfSwitch(true);
|
|
#elif defined(ARCH_PORTDUINO)
|
|
if (portduino_config.dio2_as_rf_switch)
|
|
lora.setDio2AsRfSwitch(true);
|
|
#endif
|
|
|
|
// RX boosted gain mode
|
|
lora.setRxBoostedGainMode(config.lora.sx126x_rx_boosted_gain);
|
|
|
|
// Re-apply the undocumented 0x8B5 RX sensitivity patch that was set in init().
|
|
// The CALIBRATE_ALL (0x7F) command above clears bit 0 of register 0x8B5, which
|
|
// silently removes the RX sensitivity improvement introduced in #9571 / #9777.
|
|
// Without this re-apply, every SX1262 node loses its RX boost ~60s after boot
|
|
// and never recovers until reboot. See empirical evidence in the PR description.
|
|
if (module.SPIsetRegValue(0x8B5, 0x01, 0, 0) != RADIOLIB_ERR_NONE) {
|
|
LOG_WARN("SX126x resetAGC: failed to re-apply 0x8B5 RX sensitivity patch");
|
|
}
|
|
|
|
// 6. Resume receiving
|
|
startReceive();
|
|
}
|
|
|
|
/** Control PA mode for GC1109 FEM - CPS pin selects full PA (txon=true) or bypass mode (txon=false) */
|
|
template <typename T> void SX126xInterface<T>::setTransmitEnable(bool txon)
|
|
{
|
|
#if HAS_LORA_FEM
|
|
if (txon) {
|
|
loraFEMInterface.setTxModeEnable();
|
|
} else {
|
|
loraFEMInterface.setRxModeEnable();
|
|
}
|
|
#endif
|
|
}
|
|
|
|
#endif |