mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-09-02 13:24:09 +00:00
509 lines
22 KiB
C++
509 lines
22 KiB
C++
/*
|
|
* SPDX-License-Identifier: MIT
|
|
* LoRa radio base class — shared state and algorithms.
|
|
* Subclasses implement hw*() primitives only.
|
|
*/
|
|
|
|
#pragma once
|
|
|
|
#include <mesh/Radio.h>
|
|
#include <mesh/Board.h>
|
|
#include <NodePrefs.h>
|
|
#include <zephyr/kernel.h>
|
|
#include <zephyr/device.h>
|
|
#include "radio_common.h"
|
|
|
|
namespace mesh {
|
|
|
|
class LoRaRadioBase : public Radio {
|
|
public:
|
|
LoRaRadioBase(const struct device *lora_dev, MainBoard &board,
|
|
NodePrefs *prefs = nullptr);
|
|
|
|
void setPrefs(NodePrefs *prefs) { _prefs = prefs; }
|
|
|
|
/* Callbacks */
|
|
void setRxCallback(RadioRxCallback cb, void *user_data) {
|
|
_rx_cb = cb;
|
|
_rx_cb_user_data = user_data;
|
|
}
|
|
void setTxDoneCallback(RadioTxDoneCallback cb, void *user_data) {
|
|
_tx_done_cb = cb;
|
|
_tx_done_cb_user_data = user_data;
|
|
}
|
|
|
|
/* Radio interface (all implemented in base) */
|
|
void begin() override;
|
|
void reconfigure();
|
|
void reconfigureWithParams(float freq, float bw, uint8_t sf, uint8_t cr);
|
|
|
|
/* Temporary radio override — applies freq/bw/sf/cr without mutating
|
|
* _prefs. Used by tempradio so the saved prefs survive intact and
|
|
* concurrent savePrefs() calls don't poison flash. clearRadioOverride()
|
|
* reverts to whatever _prefs holds at that moment. */
|
|
void setRadioOverride(float freq, float bw, uint8_t sf, uint8_t cr);
|
|
void clearRadioOverride();
|
|
bool hasRadioOverride() const { return _has_radio_override; }
|
|
int recvRaw(uint8_t *bytes, int sz) override;
|
|
uint32_t getEstAirtimeFor(int len_bytes) override;
|
|
float packetScore(float snr, int packet_len) override;
|
|
bool startSendRaw(const uint8_t *bytes, int len) override;
|
|
bool isSendComplete() override;
|
|
void onSendFinished() override;
|
|
bool isInRecvMode() const override;
|
|
float getLastRSSI() const override;
|
|
float getLastSNR() const override;
|
|
bool isRadioReady() override;
|
|
|
|
/* Packet statistics */
|
|
uint32_t getPacketsRecv() const override { return (uint32_t)atomic_get(&_packets_recv); }
|
|
uint32_t getPacketsSent() const override { return (uint32_t)atomic_get(&_packets_sent); }
|
|
uint32_t getPacketsRecvErrors() const override { return (uint32_t)atomic_get(&_packets_recv_errors); }
|
|
/* Virtual so radios with extra accumulators can clear them on the same
|
|
* `clear stats`. Reached through a LoRaRadioBase& from
|
|
* RepeaterMesh::clearStats() via getRadioDriver(), so a shadowing
|
|
* non-virtual override would silently never run. */
|
|
virtual void resetStats() {
|
|
atomic_set(&_packets_recv, 0);
|
|
atomic_set(&_packets_sent, 0);
|
|
atomic_set(&_packets_recv_errors, 0);
|
|
}
|
|
|
|
/* Advanced radio features */
|
|
int getNoiseFloor() const override;
|
|
void triggerNoiseFloorCalibrate(int threshold) override;
|
|
bool isReceiving() override;
|
|
void recoverRxState() override;
|
|
|
|
/* Extended API */
|
|
bool isChannelActive(int threshold = 0);
|
|
|
|
/* Power saving */
|
|
void enableRxDutyCycle(bool enable);
|
|
bool isRxDutyCycleEnabled() const { return _rx_duty_cycle_enabled; }
|
|
/* Returns false when the chip has no RX boost feature (SX127x). */
|
|
virtual bool setRxBoost(bool enable);
|
|
bool isRxBoostEnabled() const { return _rx_boost_enabled; }
|
|
|
|
/* External FEM/LNA gain during RX. Boards whose front-end module has a
|
|
* software-selectable receive path wire that select line to the radio
|
|
* (lna-bypass-gpios); disabling this routes RX around the FEM's LNA,
|
|
* trading its gain for its supply current while leaving the antenna
|
|
* connected. The transmit path and the driver's idle gating are
|
|
* unaffected. Returns false on radios that do not implement the knob
|
|
* (everything but the native SX126x today) and on SX126x boards that
|
|
* wire no such line -- including boards whose only FEM control is the
|
|
* chip enable, where there is no gain to trade, only a path to cut. */
|
|
virtual bool setFemRxEnable(bool enable) { (void)enable; return false; }
|
|
|
|
/* Multi-SF receive via LoRa side detectors. Only the LR2021 has them;
|
|
* every other radio reports the feature as unsupported. `num` = 0
|
|
* disables. Returns false if the radio has no side detectors or the
|
|
* requested set violates a chip constraint (see
|
|
* lr20xx_configure_side_detectors). */
|
|
virtual bool configSideDetectors(const uint8_t *sfs, uint8_t num) {
|
|
(void)sfs; (void)num;
|
|
return false;
|
|
}
|
|
|
|
/* Read-only view of the modem config currently used by buildModemConfig().
|
|
* These honor temporary radio overrides for freq/bw/sf/cr and the same TX
|
|
* clamps as the actual lora_config() path. */
|
|
uint32_t getActiveFrequencyHz() const;
|
|
uint16_t getActiveBandwidthKHzX10() const;
|
|
uint8_t getActiveSpreadingFactor() const;
|
|
uint8_t getActiveCodingRate() const;
|
|
uint16_t getActivePreambleLength() const;
|
|
uint8_t getActiveSyncWord() const;
|
|
int8_t getConfiguredTxPower() const;
|
|
bool isTxActive() const { return atomic_get(&_tx_active) != 0; }
|
|
|
|
/* Duty-cycle preamble false-positive counter.
|
|
* Incremented by the driver whenever RX_TX_TIMEOUT fires in
|
|
* duty-cycle mode and the chip is silently re-armed. High
|
|
* values indicate a noisy RF environment or too-loose preamble
|
|
* detection — each event extends real RX time past the nominal
|
|
* duty cycle, inflating current draw.
|
|
* Default returns 0 on radios that don't support the stat. */
|
|
virtual uint32_t getDutyCycleTimeoutRestarts() const { return 0; }
|
|
virtual void resetDutyCycleTimeoutRestarts() {}
|
|
|
|
/* Adaptive CAD (LBT detPeak calibration) */
|
|
void setCadParams(bool auto_enabled, int8_t offset,
|
|
uint16_t probe_interval_s, uint8_t busycap_pct,
|
|
uint8_t stored_base = 0) override;
|
|
uint8_t cadBasePeak() override;
|
|
void cadMaintenance() override;
|
|
/** Deaf-aware AGC unstick + temperature-drift recalibration.
|
|
* Called from Dispatcher::maintenanceLoop(); never on the packet path. */
|
|
void radioMaintenance() override;
|
|
|
|
private:
|
|
/* The two unrelated jobs radioMaintenance() drives; see its comment. */
|
|
void agcIdleMaintenance(uint32_t now);
|
|
void imageCalMaintenance(uint32_t now);
|
|
public:
|
|
uint32_t msUntilNextMaintenance() override;
|
|
int8_t getCadOffset() const override { return _cad_offset; }
|
|
|
|
/* Offset bounds the controller may actually use: the static
|
|
* [CAD_LEVEL_MIN, CAD_LEVEL_MAX] window narrowed to whatever the
|
|
* hardware clamp leaves distinguishable at the current base. Every
|
|
* range decision goes through these; the raw constants stay in use only
|
|
* for indexing _cad_stats[], which is sized to the static window. */
|
|
int8_t cadLevelMinEff();
|
|
int8_t cadLevelMaxEff();
|
|
void resetCadStats() override;
|
|
int formatCadStatus(char *buf, int cap) override;
|
|
|
|
protected:
|
|
/* ── Hardware primitives — subclass MUST implement ─────────── */
|
|
|
|
virtual bool hwConfigure(const struct lora_modem_config &cfg) = 0;
|
|
virtual void hwCancelReceive() = 0;
|
|
virtual int hwSendAsync(uint8_t *buf, uint32_t len,
|
|
struct k_poll_signal *sig) = 0;
|
|
virtual int16_t hwGetCurrentRSSI() = 0;
|
|
/** Read `n` instantaneous RSSI samples spaced `spacing_us` apart.
|
|
*
|
|
* Exists so the whole burst can be bracketed ONCE by whatever a family
|
|
* needs to take a reading, instead of once per sample. On the LR
|
|
* families a single RSSI read stands the duty cycle down, enters
|
|
* continuous Rx, waits 1 ms to settle, reads, and re-arms the cycle --
|
|
* and that re-arm clears every IRQ and zeroes the RX-busy latch. Called
|
|
* eight times for one median that is eight cycle tear-downs, eight latch
|
|
* wipes and 8 ms of settle, every sampling interval, whether or not a
|
|
* CAD probe follows.
|
|
*
|
|
* Returns the number of valid samples written (< n means the read was
|
|
* refused partway and the caller should abandon the burst), or a NEGATIVE
|
|
* value when the burst must be abandoned for a reason that is not a read
|
|
* failure: -EAGAIN says the receiver detected a preamble or header inside
|
|
* the window, so the samples describe that signal and not the floor.
|
|
* Worth its own return because the caller's counters exist to tell a
|
|
* failing bus from a busy channel, and only a family that brackets its own
|
|
* Rx entry is in a position to notice the latter.
|
|
*
|
|
* The default implementation is the old per-sample loop, which is already
|
|
* correct for radios whose RSSI read has no such bracket (SX126x bails on
|
|
* BUSY and touches nothing; SX127x has no duty cycle at all) and which
|
|
* therefore never reports -EAGAIN. */
|
|
virtual int hwGetRssiBurst(int16_t *out, int n, uint32_t spacing_us);
|
|
/* Non-destructive read of the radio's "currently receiving" signal —
|
|
* latch + raw IRQ bits, never clears. Backs LoRaRadioBase::isReceiving(). */
|
|
virtual bool hwIsReceiving() = 0;
|
|
virtual void hwSetRxBoost(bool enable) = 0;
|
|
|
|
/** GPIO-only BUSY check (no SPI). Default false for chips without duty-cycle sleep. */
|
|
virtual bool hwIsChipBusy() { return false; }
|
|
|
|
/* ── Adaptive-CAD primitives — defaults suit chips without hardware
|
|
* CAD (SX127x): probing unsupported, offset ignored. ───────────── */
|
|
|
|
/** Blocking calibration CAD at (family base detPeak + level).
|
|
* Returns 0 = free (chip in STANDBY, caller restarts RX),
|
|
* 1 = busy, chip in STANDBY, caller restarts RX,
|
|
* 2 = busy, chip left in RX on the detected signal (CAD_RX
|
|
* exit mode) -- caller must NOT restart RX, and reads the
|
|
* outcome later via hwCadRxOutcome(),
|
|
* <0 = error / unsupported. */
|
|
virtual int hwCadProbe(int8_t level) { (void)level; return -ENOSYS; }
|
|
|
|
/** Outcome of the RX a hwCadProbe() == 2 left the chip in.
|
|
* 1 = a packet completed, so the detection was real;
|
|
* 2 = the chip's own CAD timeout expired with nothing decoded;
|
|
* 0 = not armed, or the terminal interrupt has not arrived yet.
|
|
* Reads driver state only -- no chip access, no polling loop. */
|
|
virtual int hwCadRxOutcome() { return 0; }
|
|
|
|
/** How long the chip may stay in a CAD_RX-entered RX before it raises
|
|
* its own timeout. Bounds the wait before hwCadRxOutcome() is read. */
|
|
virtual uint32_t hwCadRxTimeoutMs() { return 0; }
|
|
/** Apply the operating detPeak offset for all subsequent LBT CADs. */
|
|
virtual void hwCadSetPeakOffset(int8_t offset) { (void)offset; }
|
|
/** Per-SF base detPeak for the current config (0 = unsupported). */
|
|
virtual uint8_t hwCadBasePeak() { return 0; }
|
|
|
|
/** Absolute detPeak range the driver will actually program, inclusive.
|
|
* 0/0 means "no known limit" and the offset range stays as-is.
|
|
*
|
|
* This exists because the offset window and the hardware clamp are two
|
|
* different things, and when they disagree the controller explores
|
|
* levels that are physically identical: on the LR2021 at SF7 the base is
|
|
* 51 and the driver clamps to 48, so offsets -3 through -8 all programmed
|
|
* the same peak. The staircase then compared three rungs of the same
|
|
* configuration, found only sampling noise between them, and random-walked
|
|
* into the floor with nothing to climb back out on. */
|
|
virtual uint8_t hwCadPeakMin() { return 0; }
|
|
virtual uint8_t hwCadPeakMax() { return 0; }
|
|
|
|
/* ── Receiver hygiene — see LoRaRadioBase::radioMaintenance() ─── */
|
|
|
|
/** Unstick a jammed AGC: warm sleep to drop the analog front end, then
|
|
* recalibrate on the way back up. Semtech's stated remedy; the
|
|
* datasheets do not describe it, so do not "simplify" it away on
|
|
* datasheet grounds alone. Must leave the driver out of RX so the
|
|
* caller's startReceive() performs a real re-entry rather than hitting
|
|
* an idempotent fast path. Default: unsupported, no-op. */
|
|
virtual void hwResetAgc() {}
|
|
|
|
/** Redo the frequency-dependent calibrations (image / front end, and
|
|
* PLL+AAF where the part separates them) at the current operating
|
|
* frequency. Called on temperature drift, never on the packet path.
|
|
* Same RX-state contract as hwResetAgc(). Default: unsupported. */
|
|
virtual void hwRecalibrate() {}
|
|
|
|
/** Chip junction temperature in whole degrees C, or INT16_MIN when the
|
|
* backend cannot measure it (which disables drift recalibration). */
|
|
/* Does this family actually suffer the jammed-AGC fault the reset is a
|
|
* remedy for? Only the SX126x does. Semtech prescribe warm sleep plus
|
|
* recalibration for it, ZephCore inherited the idea from Arduino
|
|
* MeshCore's `agc_reset_interval`, and both are SX126x-era. Neither the
|
|
* LR11xx UM nor the LR2021 DS describes such a fault; those parts need
|
|
* image/front-end recalibration on temperature drift, which is a
|
|
* different operation on a different trigger (hwRecalibrate()).
|
|
*
|
|
* Running it anyway is not free. Measured on a T1000-E 2026-08-23: the
|
|
* 60 s after an AGC reset carried a 7.4% packet-miss rate against 0.6%
|
|
* elsewhere, and one deaf stretch began at one reset and ended at the
|
|
* next — the reset was recovering damage it had caused, on a part with
|
|
* no AGC fault to fix. */
|
|
virtual bool hwNeedsAgcReset() { return false; }
|
|
|
|
/* Does this family specify a temperature threshold for image/front-end
|
|
* recalibration? LR11xx and LR2021 do; the SX126x datasheet does not,
|
|
* and drift recalibration stays inactive there exactly as before. */
|
|
virtual bool hwHasDriftRecal() { return false; }
|
|
|
|
/** Radio deaf time per duty-cycle wake transition (context restore +
|
|
* PLL lock + TCXO startup where fitted), in microseconds. Counts
|
|
* against the duty-cycle preamble-catch budget: per SX126x DS rev 2.2
|
|
* §13.1.7 the TCXO startup delay is inserted between the sleep and RX
|
|
* periods, outside both.
|
|
*
|
|
* This default is a fallback for backends that expose no per-device
|
|
* figure; it suits XTAL parts only. SX126x, LR11xx and LR2021 all
|
|
* override it, because a board with a TCXO powers the regulator down
|
|
* during duty-cycle sleep and pays the oscillator restart on every
|
|
* wake — several milliseconds, dwarfing this number. Leaving a TCXO
|
|
* board on the default oversizes the sleep window and drops
|
|
* window-edge preambles regardless of signal strength. */
|
|
virtual uint32_t hwWakeupTimeUs() { return 1500; }
|
|
|
|
/* Set to true by subclasses using the loramac-node driver backend.
|
|
* Disables the direction-only fast path in configureTx()/configureRx():
|
|
* loramac-node calls Radio.SetTxConfig() and Radio.SetRxConfig() which
|
|
* configure completely disjoint internal state — skipping either leaves
|
|
* TxTimeout/RxConfig uninitialized in the loramac-node library. */
|
|
bool _loramac_node;
|
|
|
|
/* ── Shared helpers available to subclasses ────────────────── */
|
|
|
|
void buildModemConfig(struct lora_modem_config &cfg, bool tx);
|
|
/* Shared body for configureRx()/configureTx(): builds the modem config for
|
|
* the given direction, honours the params-unchanged and direction-only
|
|
* fast paths, then programs the radio via hwConfigure(). */
|
|
void configure(bool tx);
|
|
void configureRx();
|
|
void configureTx();
|
|
void startReceive();
|
|
void startTxThread(k_thread_stack_t *stack, size_t stack_size);
|
|
|
|
const struct device *_dev;
|
|
NodePrefs *_prefs;
|
|
MainBoard *_board;
|
|
atomic_t _in_recv_mode;
|
|
atomic_t _tx_active;
|
|
volatile float _last_rssi; /* word-aligned: atomic on ARM */
|
|
volatile float _last_snr; /* word-aligned: atomic on ARM */
|
|
|
|
/* RX ring buffer */
|
|
struct RxPacket {
|
|
uint8_t data[256];
|
|
uint16_t len;
|
|
int16_t rssi;
|
|
int8_t snr;
|
|
};
|
|
RxPacket _rx_ring[RX_RING_SIZE];
|
|
atomic_t _rx_head;
|
|
atomic_t _rx_tail;
|
|
|
|
/* TX buffer + signal */
|
|
uint8_t _tx_buf[256];
|
|
struct k_poll_signal _tx_signal;
|
|
|
|
/* Noise floor calibration state */
|
|
int _noise_floor;
|
|
int _calibration_threshold;
|
|
uint8_t _ema_unguarded; /* tick counter for warmup + periodic bypass */
|
|
/* Absolute uptime deadline of the next floor sample. The sampler used
|
|
* to run on every housekeeping tick, which pinned its cadence to the
|
|
* 5 s timer; owning its own deadline is what lets that timer go away.
|
|
* Advanced by NOISE_FLOOR_INTERVAL_MS after a sample lands, and by the
|
|
* shorter retry when an attempt is turned away because the radio was
|
|
* mid-packet / transmitting / in its duty-cycle sleep window. */
|
|
int64_t _noise_floor_next_ms;
|
|
uint8_t _noise_floor_retries; /* consecutive blocked attempts, capped */
|
|
/* Shared cadence for every periodic radio measurement (floor sample +
|
|
* CAD probe). Runtime, from the probe.interval pref. */
|
|
uint32_t _measure_interval_ms;
|
|
/* Latest floor sample, published for cadMaintenance() so the CAD probe
|
|
* shares this measurement instead of taking its own single RSSI read.
|
|
* _sample_fresh is true only within the pass that produced it. */
|
|
int16_t _sample_rssi;
|
|
bool _sample_channel_quiet;
|
|
bool _sample_fresh;
|
|
/* Cycle stamp of the last host-driven RX entry, used to skip a floor
|
|
* sample taken before GetRssiInst has settled (DS Table 13-82). */
|
|
uint32_t _rx_entry_cyc;
|
|
/* Median-of-N quality accounting, surfaced by `get cad` as sp:<mean>/<%>.
|
|
* The median only rejects outliers if the N reads are independent; if
|
|
* they land inside one RSSI averaging window they are the same sample
|
|
* N times over and the median is decorative. Spread (max-min of the
|
|
* burst) and the share of zero-spread bursts make that visible without
|
|
* a debug build. */
|
|
uint32_t _rssi_bursts;
|
|
uint32_t _rssi_spread_sum;
|
|
uint32_t _rssi_degenerate;
|
|
|
|
/* Adaptive CAD state */
|
|
struct CadLevelStats {
|
|
uint16_t probes; /* probes run at this level */
|
|
uint16_t busy; /* raw busy verdicts */
|
|
uint16_t fp; /* busy that passed the ground-truth filter (suspected false positive) */
|
|
uint16_t tp; /* busy confirmed by RX activity right after */
|
|
};
|
|
CadLevelStats _cad_stats[CAD_NUM_LEVELS];
|
|
bool _cad_auto; /* staircase acts on the stats */
|
|
int8_t _cad_offset; /* operating detPeak offset (levels) */
|
|
uint16_t _probe_interval_s; /* 0 = CAD probing disabled; drives _measure_interval_ms */
|
|
uint8_t _cad_busycap_pct; /* airtime cap: max % TX deferred (0 = off) */
|
|
/* A CAD_RX probe awaiting its terminal event. _cad_pending_level is the
|
|
* stats rung the result belongs to; _cad_pending_deadline_ms is when the
|
|
* chip guarantees it has resolved, so the answer is READ once at that
|
|
* point rather than polled for. Level INT8_MIN means nothing pending. */
|
|
int8_t _cad_pending_level;
|
|
int64_t _cad_pending_deadline_ms;
|
|
int64_t _cad_last_probe_ms;
|
|
int64_t _cad_last_decay_ms;
|
|
/* Earliest uptime at which a due-but-blocked probe may be retried. The
|
|
* interval check in cadMaintenance() is against _cad_last_probe_ms,
|
|
* which only advances on a probe that actually ran — without this a
|
|
* blocked probe would report "due now" forever and spin the wake. */
|
|
uint8_t _cad_probe_rr; /* round-robin index (sweep) / frontier mix counter */
|
|
|
|
int8_t pickCadProbeLevel();
|
|
void decayCadStats();
|
|
void cadStaircaseStep();
|
|
|
|
/* Power saving */
|
|
bool _rx_duty_cycle_enabled;
|
|
bool _rx_boost_enabled;
|
|
|
|
/* Last duty-cycle timing handed to the driver — used to log timing
|
|
* changes once at INF instead of on every RX restart. 0/0 = never
|
|
* computed; UINT32_MAX rx = continuous-RX fallback active. */
|
|
uint32_t _dc_last_rx_us;
|
|
|
|
uint32_t _dc_last_sleep_us;
|
|
|
|
/* agcIdleMaintenance() bookkeeping. RX activity is inferred by sampling the
|
|
* existing packet counters rather than timestamping in the RX callback,
|
|
* so nothing is added to the ISR path. */
|
|
uint32_t _agc_rx_count_shadow;
|
|
uint32_t _agc_last_activity_ms;
|
|
|
|
/* Stuck-AGC corroboration. The noise-floor sampler already produces an
|
|
* RSSI every interval; a desensitised front end reports a frozen one.
|
|
* Free evidence — no extra command, no extra wake. */
|
|
int16_t _agc_rssi_last;
|
|
uint8_t _agc_rssi_frozen;
|
|
|
|
/* Diagnostics only — no behaviour depends on these.
|
|
*
|
|
* The RSSI sampler discards a whole burst if any read comes back busy,
|
|
* and on a duty-cycled LR1110 it completes ~0.2% of its attempts
|
|
* (measured 5 bursts in 9 h against 1007 on an SX1262 beside it). That
|
|
* is either "a few reads land in the sleep phase" or "essentially every
|
|
* read is refused", and the two want opposite fixes. These counters say
|
|
* which, and are reported by `get cad`. */
|
|
uint32_t _rssi_reads_ok;
|
|
uint32_t _rssi_reads_busy;
|
|
uint32_t _rssi_bursts_abandoned;
|
|
/* Attempts turned away BEFORE a burst starts, by the isRadioReady()
|
|
* guard — i.e. the duty-cycle sleep window. The three counters above
|
|
* cannot see this: they only move once a burst is already running, so a
|
|
* sampler that is being refused outright reads as b0/a0, which is
|
|
* indistinguishable from one that is running perfectly. That ambiguity
|
|
* is what makes the "does the floor go stale under duty cycle?" question
|
|
* unanswerable today, and it is the missing denominator for the ~0.2%
|
|
* completion rate noted above. */
|
|
uint32_t _rssi_dc_blocked;
|
|
|
|
/* Silence tracking for deafness hunting. Reports only; the recovery
|
|
* decision lives in agcIdleMaintenance() and is family-gated. */
|
|
uint32_t _silence_last_report_ms;
|
|
int16_t _image_cal_last_temp_c;
|
|
|
|
/* Image-calibration temperature polling. Deliberately much slower than
|
|
* the maintenance cadence — see imageCalMaintenance(). */
|
|
uint32_t _image_cal_last_ms;
|
|
uint32_t _image_cal_wait_ms;
|
|
bool _image_cal_started;
|
|
bool _image_cal_confirming;
|
|
|
|
/* Start of the most recent transmit, for the post-TX quiet window the
|
|
* temperature poll waits out. Start-referenced rather than end-
|
|
* referenced so it can be stamped at the single startSendRaw() site
|
|
* instead of all five _tx_active clear paths; airtime is bounded by
|
|
* seconds and the quiet window by a minute, so the difference does not
|
|
* matter. */
|
|
uint32_t _last_tx_start_ms;
|
|
|
|
/* Config cache — skip redundant hwConfigure() */
|
|
struct lora_modem_config _last_cfg;
|
|
bool _config_cached;
|
|
|
|
/* Radio param override — when set, buildModemConfig() uses these
|
|
* for freq/bw/sf/cr instead of _prefs. Everything else (tx_power,
|
|
* preamble) still comes from _prefs. */
|
|
bool _has_radio_override;
|
|
float _override_freq;
|
|
float _override_bw;
|
|
uint8_t _override_sf;
|
|
uint8_t _override_cr;
|
|
|
|
/* ISR RX callback — passed to lora_recv_async() / lora_recv_duty_cycle() */
|
|
static void rxCallbackStatic(const struct device *dev, uint8_t *data,
|
|
uint16_t size, int16_t rssi, int8_t snr,
|
|
void *user_data);
|
|
|
|
private:
|
|
/* RX notification callback */
|
|
RadioRxCallback _rx_cb;
|
|
void *_rx_cb_user_data;
|
|
|
|
/* TX done callback */
|
|
RadioTxDoneCallback _tx_done_cb;
|
|
void *_tx_done_cb_user_data;
|
|
|
|
/* TX completion thread */
|
|
static void txWaitThreadFn(void *p1, void *p2, void *p3);
|
|
uint32_t txWaitBudgetMs() const;
|
|
struct k_thread _tx_wait_thread;
|
|
struct k_sem _tx_start_sem;
|
|
bool _tx_thread_running;
|
|
/* Length of the transmit in flight, for txWaitBudgetMs(). Written by
|
|
* startSendRaw() before it releases _tx_start_sem, read by the wait
|
|
* thread after it takes that sem — the sem is the handoff, so no
|
|
* additional synchronisation is needed. */
|
|
uint16_t _tx_len;
|
|
|
|
/* Packet statistics */
|
|
atomic_t _packets_recv;
|
|
atomic_t _packets_sent;
|
|
atomic_t _packets_recv_errors;
|
|
};
|
|
|
|
} /* namespace mesh */
|