mirror of
https://github.com/meshtastic/firmware.git
synced 2026-11-06 06:14:45 -05:00
* LoRa LBT: fix SX128x/SX126x CAD encoding + slot model, ESP32 sleep race Three fixes from the "are we really listening?" LBT audit: - SX128x CAD: pass RADIOLIB_SX128X_CAD_ON_4_SYMB instead of the raw count 4. The raw value set bits 7:5 = 000, i.e. a 1-symbol scan, leaving LBT nearly blind on 2.4 GHz. Now runs the intended 4-symbol CAD. - SX126x CAD / slot time: name the symNum value explicitly as CAD_ON_4_SYMB (identical 0x02 raw value, so no on-air change) and add a virtual getCadSymbolCount() so computeSlotTimeMsec() models the real ~4.5-symbol scan for the SX126x/LLCC68/STM32WL family only, leaving LR11x0/SX127x at their correct 2.5 symbols. Fixes the too-short contention-window slots. - ESP32 light sleep: if the DIO1/RF95_IRQ line is already asserted just before esp_light_sleep_start(), skip the sleep so the main loop services the pending packet instead of stranding it for up to a full sleep chunk (the detachInterrupt-clears-the-GPIO-wake disarm race). clod helped too * LoRa LBT: keep software-poll preamble flags latched while idle (anti-stomp) Finding #10 from the "are we really listening?" audit, LORA_DIO1_SOFTWARE_POLL boards (meshnology-w10). The 1 ms poll cleared latched PREAMBLE_DETECTED|HEADER_VALID whenever they were the only bits set, so for the rest of a reception isActivelyReceiving() read idle. A TX queued during the payload phase then passed the send gate and startTransmit overwrote the shared chip buffer, losing the inbound packet. That clear only exists to stop a noise preamble from permanently blocking our own TX (starvation). Starvation can only happen when we actually have something to send, so gate the clear on a non-empty txQueue. While the queue is empty the bits stay latched, isActivelyReceiving() reports the in-progress packet, and a later TX defers instead of stomping it. A queued TX still gets the escape hatch. The poll cadence is a fixed 1 ms reschedule either way, so latched bits don't spin the loop; readData() clears everything once a real RX_DONE arrives. clod helped * LoRa LBT: optional RSSI energy second-gate before TX (default off) Findings #4/#5 of the "are we really listening?" audit. CAD only detects a preamble. A neighbour's packet whose preamble already aired - during our own TX, a CAD scan, or a standby gap - is mid-payload and invisible to CAD, so we key up on top of it. Add an RSSI energy check as a second listen-before- talk gate in the TX-decision path: channelBusyByRSSI() compares the instantaneous in-RX RSSI against the rolling noise floor (getNoiseFloor() + margin) and defers if the channel is hot. It reads RSSI before isChannelActive()'s setStandby() via the || short-circuit, so the sample is taken while still in RX where GET_RSSI_INST is valid - no mode juggling. Orthogonal to CAD: CAD has processing gain and catches weak preambles below the noise floor; RSSI catches strong energy at any packet phase, including the mid- payload case CAD is blind to. Neither subsumes the other. DEFAULT OFF. A mis-set threshold makes a node read "busy" on noise and stop transmitting entirely, which is worse than the bug, so it must be tuned on hardware before it ships live. Compiled out unless LORA_RSSI_LBT_MARGIN_DB is defined at build time; when off it is a compile-time `return false` (zero behaviour change). Fails open: returns not-busy unless in RX with a full noise-floor window and a sane dBm reading, so it can never persistently silence a node. Native build (gate off) links clean. clod helped * LoRa LBT: RSSI margin profiler to tune RSSI_LBT_MARGIN_DB (default off) Instrumentation to pick the #4/#5 RSSI second-gate margin from measured data instead of guessing. Build with -DLORA_RSSI_LBT_PROFILE; compiled out otherwise. It accumulates (RSSI - rolling noise floor) in two channel states: - idle (reuses updateNoiseFloor()'s existing RX sample, no extra SPI): the noise- jitter distribution. Its high tail is the floor the margin must clear. - signal (decoded-packet RSSI, hooked in handleReceiveInterrupt): the real-traffic rise. Its low tail is the ceiling the margin must stay under. A throttled log dump prints both distributions (min/mean/max + percentiles) and the derived usable window (idle p99, signal p05). Placement: the stats/histogram engine is a dependency-free, radio-agnostic header (RssiProfiler.h) so it stays out of the core file and is testable on its own; the two hooks and the RSSI/log I/O live on RadioLibInterface where the state is. Independent of the gate flag (LORA_RSSI_LBT_MARGIN_DB) on purpose: profile first to choose the margin, then build the gate on. Native build (profiler on) compiles clean. clod helped * docs: describe the RSSI LBT margin profiler Feature doc for the RSSI listen-before-talk margin profiler (-DLORA_RSSI_LBT_PROFILE): what it measures (idle noise-jitter vs decoded-signal distributions of RSSI-minus-noise-floor), how it's wired (RssiProfiler.h engine + RadioLibInterface hooks, idle sample reused from updateNoiseFloor), how to build and run it, and how to read the log output to choose RSSI_LBT_MARGIN_DB. clod helped * LoRa LBT: draft #3/#6 fixes behind MESHTASTIC_RADIOLIB_HAS_RESUME_RECEIVE (inactive) Firmware side of the CAD->RX handoff (#3) and ISR_RX re-arm (#6) from the LBT audit. Both need to re-arm RX WITHOUT the standby() that startReceive() forces (which aborts an in-flight reception) - unavailable in stock RadioLib, provided by resumeReceive() in NomDeTom/RadioLib@rxResume. Guarded by MESHTASTIC_RADIOLIB_HAS_RESUME_RECEIVE, left UNDEFINED: against stock RadioLib these paths compile out and behaviour is unchanged. Define it (via build_flags) only when linking a RadioLib that has resumeReceive(). - resumeReceiveInPlace(bool startRx): base-class helper - iface->resumeReceive() (remap RX IRQ, optional SetRx, no standby) + re-attach the MCU ISR + base RX bookkeeping. One place, chip-agnostic via iface. - #3: SX126x/LR11x0 isChannelActive() CAD exitMode -> GOTO_RX; the TX-decision path splits the busy check into rssi/cad flags and, on a CAD detect, hands the packet to RX via resumeReceiveInPlace(false) instead of startReceive(). Macro-off, the split is exactly the old `channelBusyByRSSI() || isChannelActive()` short-circuit. - #6: ISR_RX re-arms via resumeReceiveInPlace(true). Open (flagged in-code, for review + HW validation): whether re-issuing SetRx (#6) disturbs an on-chip in-flight reception, and the #3 handoff's interaction with a beacon-target packet on the home config. clod helped * LoRa LBT #4: synthesize GPIO wake cause when light sleep is skipped doLightSleep() skips esp_light_sleep_start() when a LoRa IRQ is already asserted, but still read esp_sleep_get_wakeup_cause() unconditionally - so the skip path returned the previous real sleep's cause (e.g. TIMER) and missed the ESP_SLEEP_WAKEUP_GPIO branch that sets runASAP. Capture whether we skipped and return a synthetic GPIO cause in that case so PowerFSM takes the packet-service path. Reads the ESP-IDF cause only after an actual sleep. clod helped too * LoRa LBT #10: keep HEADER_VALID latched in the software IRQ poll The LORA_DIO1_SOFTWARE_POLL anti-starvation escape hatch cleared both PREAMBLE_DETECTED and HEADER_VALID whenever a TX was queued. HEADER_VALID is evidence of a real inbound frame, so clearing it made isActivelyReceiving() read idle mid-payload and let the queued TX stomp the frame. Gate the hatch on a bare-preamble IRQ (PREAMBLE set, HEADER_VALID clear) and clear only PREAMBLE_DETECTED. A stuck header still can't block TX forever - receiveDetected() ages it out after maxPacketTimeMsec - and readData() clears everything on RX_DONE. clod helped too * LoRa LBT #3/#6: CAD->RX handoff + continuous-RX re-arm on stock RadioLib Reimplements the two audit fixes that previously needed the NomDeTom/RadioLib @rxResume fork (MESHTASTIC_RADIOLIB_HAS_RESUME_RECEIVE), using only stock RadioLib, behind a new default-off flag MESHTASTIC_LBT_CAD_TO_RX. Per the RadioLib maintainer's feedback, no new RX API is needed. (GOTO_RX) with the RX IRQs pre-mapped in the scan config, so the chip's own CAD->RX transition delivers RX_DONE with no library call in between. irqFlags (status-enable) carries the CAD verdict + full RX set incl. PREAMBLE/HEADER so isActivelyReceiving() can read them; irqMask (DIO trigger) carries the CAD verdict + terminal RX events only - NOT PREAMBLE/HEADER, which would fire the ISR mid-frame and run readData() on an incomplete packet. On detection it hands off in place: clear the latched CAD verdict, re-attach the MCU ISR, mark receiving - no standby (which would abort the packet just detected). standby), so re-attach the MCU ISR instead of re-issuing SetRx (which could disturb a second packet already arriving on-chip). Removes resumeReceiveInPlace() and the fork macro. SX126x-only: LR11x0's startChannelScan conflates irqFlags/irqMask (drives the DIO from irqFlags), so it keeps the safe GOTO_STDBY path, re-arming on detect to stay coherent with onNotify when the flag is on. Default-off: needs on-hardware validation and assumes continuous RX (the default preamble=16/minSymbols=8 degenerates startReceiveDutyCycleAuto to continuous). Native builds pass both flag-off and flag-on. clod helped too * LoRa LBT #1: re-arm RX unconditionally when deferring a busy channel The post-CAD/RSSI defer path wrapped its startReceive() re-arm in a !hasTargetRadioSettings(txp) guard, so a beacon-target packet that hit a busy channel took setTransmitDelay() with the radio left in standby. When the beacon's target config coincides with the home config the radio is on the home channel, so the node went deaf on home traffic until the next TX cycle - and kept skipping the re-arm every cycle the channel stayed busy. Drop the guard: re-arm unconditionally and keep listening while we wait to key up. A beacon's job is to get a packet out, but it should still mirror the main RX path. (No effect under MESHTASTIC_LBT_CAD_TO_RX, where the chip is already left in RX.) clod helped too * docs: fix RSSI profiler fences (MD040) + RAM figure, note negative-bin limit - Tag the formula and sample-output fenced blocks as `text` so markdownlint MD040 passes. - Correct the RAM figure: RssiDeltaStats is 148 bytes (12 scalar + 34*4 histogram), so two instances are ~296 bytes, not ~280. - Add a limitations note that all negative deltas collapse into bin[0], so a percentile landing there returns the minimum sample. This does not affect the margin (keyed off the always-positive idle p99) but can understate signal p05 when a node decodes below-floor packets; never biases toward a smaller margin. clod helped too * docs: RSSI profiler - record histogram bucketing design choices Add a "Histogram bucketing" section laying out why a fixed histogram is used over exact percentiles, what the axis has to resolve (the margin window sits at roughly -a few..+20 dB), and the arguments for five bucketing schemes: the current positive-only layout (A), a cost-neutral re-centred layout with negative-range resolution (B, recommended if signal p05 must be trusted), wider full-range bins (C), more bins (D), and exact/off-box (E). Explains the current choice and when to switch. clod helped too * LoRa LBT: move RSSI profiler + energy second-gate to listening-now-rssi Split the RSSI work off this branch so the active CAD/sleep fixes and the #3/#6 CAD->RX rework can land without the tuning-dependent RSSI pieces. Removed here (preserved intact on branch listening-now-rssi): - RSSI margin profiler: RssiProfiler.h, the LORA_RSSI_LBT_PROFILE hooks, and docs/rssi_lbt_margin_profiler.md. - RSSI energy second-gate: channelBusyByRSSI() and the LORA_RSSI_LBT_MARGIN_DB gate. The pre-existing noise-floor sampler (updateNoiseFloor/getNoiseFloor, already on develop and consumed by telemetry) is untouched. The TX-decision block reverts to a plain CAD scan; #3/#6 (behind MESHTASTIC_LBT_CAD_TO_RX) and #1 unchanged. Native build green. clod helped too * reduce to bare essentials * LoRa LBT: calibrate CAD symbol count and detPeak against vendor data NUM_SYM_CAD carried two incompatible meanings: computeSlotTimeMsec() read it as a literal symbol count, while the drivers passed it into scanChannel(), where symNum is a register encoding on SX126x/SX128x. On SX126x the raw 2 encodes CAD_ON_4_SYMB, so the chip has scanned 4 symbols since #5749/#5772 while the slot time was priced at 2.5 - a 1.8x mis-scaling of the CSMA backoff unit, live since January 2025. Give the constant one meaning (a literal count of 4) and have each driver translate at its own call site. getCadSymbolCount() reports the real scan length so slotTimeMsec matches it; RF95 overrides it because SX127x CAD is fixed in hardware and its legacy scanChannel() takes no symNum. AN1200.48 Table 1 recommends 4 symbols for SF9-SF12, which is where most shipped presets sit, so 4 is the right target. The comment claiming 2 was reading that note's SF7/SF8 row. detPeak is calibrated per (SF, symbol count), and on LR11x0 per bandwidth as well. Both LR drivers ran thresholds meant for a 2-symbol scan, leaving them less sensitive than measured-best and biased toward missed detections. Now sourced explicitly: LR20x0 LR20xx datasheet rev 2.2 Table 6-19, indexed by symbols x SF LR11x0 Semtech SWSD003 lr11xx/apps/cad/main_cad.c optimized_parameters, indexed by symbols x bandwidth x SF SX126x RadioLib's SF+13, which sits at or below every AN1200.48 value and so errs toward deferring rather than transmitting Combinations the vendor data does not characterise fall back to the RadioLib default rather than guessing. LR2021 also pins fastCad, which scanChannel() reads off the radio object rather than the config struct. Also fix the 2.4 GHz slot term: (2 * sf + 3) / 32 is integer division that truncates to 0 for every legal SF, so the AN1200.22 fractional term never contributed anything. Not hardware-verified. * LoRa LBT: drop audit-doc pointers and cross-chip asides from comments The LBT comments referenced an external audit document by item number (#1, #3, #3/#6, #10, and a see-.notes/lbt pointer), which means nothing to anyone reading the tree on its own. They also described what sibling drivers do, which goes stale silently the moment one of them changes and belongs in that driver's own file anyway. Keep the reasoning, drop the pointer and the comparison. Where a comment explained a rule that genuinely spans drivers - NUM_SYM_CAD's register- encoding caveat - state it as a rule rather than a roll-call of chips. No behaviour change; comments only. * LoRa LBT: don't index the LR11x0 CAD table with a 2.4 GHz bandwidth The detPeak lookup picked its bandwidth row by range, so wideLora's 406.25 kHz fell into the sub-GHz 500 kHz row - SWSD003's table is the sub-GHz LoRa modem (BW enums 62.5-500), and 2.4 GHz is a separate modem with its own calibration. Match the four bandwidths exactly instead, so anything unmatched falls back to the RadioLib default rather than borrowing a row it has no claim to. Those four are the whole sub-GHz set today - LR11x0::setBandwidth() rejects anything else - so note that a newly unlocked bandwidth, or a symbol count outside 2/4/8/16, needs a row adding here and fails silently if it doesn't get one. Same note on LR20x0 for symbol counts above 4. Also correct two citations. The 2.4 GHz slot term is the SX1280 datasheet's own CAD duration formula, and the CAD symbol-count guidance is AN1200.77, not AN1200.22 - which is "LoRa Modulation Basics" from 2015 and predates the part. Record that SX1280 ignores detPeak/detMin/exitMode entirely: SetCadParams carries only cadSymbolNum, and the PNR threshold lives in register 0x942. * LoRa LBT: set the SX1280 CAD PNR threshold and scan 8 symbols SetCadParams on this part carries only cadSymbolNum - detPeak/detMin in the scan config are inert - so CAD sensitivity is governed entirely by register 0x942, which nothing was writing. The scan therefore ran at the chip's reset value of 0x32, against the 19-36 that AN1200.77 Table 1 calibrates per SF and bandwidth: at LONG_FAST wideLora the recommendation is 27 and we were using 50. A threshold that high needs a much stronger correlation peak, so 2.4 GHz LBT was under-detecting on top of the symbol-count bug already fixed. Write the table value before each scan, indexed by bandwidth x SF. Unmatched bandwidths write nothing and leave the chip's default, as on the LR parts. This goes through our own Module rather than lora.writeRegister(), which RadioLib keeps protected unless RADIOLIB_LOW_LEVEL unlocks raw register access across every driver. SX128x::writeRegister() is a straight SPIwriteRegisterBurst on the same Module we already own, and begin() has pointed spiConfig at the 0x18 WriteRegister opcode long before any scan runs, so the transaction is identical - no subclass needed. Also raise the CAD window from 4 to 8 symbols, matching RadioLib's own SX128x default. AN1200.77 measures a longer window as moving P(Detection) further from P(False Detection); its thresholds are stated to hold for any window duration, so the table above is unaffected. NUM_SYM_CAD_24GHZ moves in step so the slot time still reflects the real scan. Not hardware-verified. * LoRa LBT: hand CAD straight to RX on the LR parts CAD exited to standby, and the busy-channel path then issued a full startReceive() - which standbys the radio and re-enters RX from scratch, throwing away the reception CAD had just detected. Both LR families expose a CAD-to-RX exit mode that avoids the round trip, and it reaches the chip through the public startChannelScan(cfg), so nothing here needs a library patch. A note in an earlier commit claimed this was impossible because RadioLib collapses irqFlags and irqMask into one set. That reasoning is SX126x's, where irqMask is a status gate and an event omitted from it never latches at all. The LR parts have no such gate: SetDioIrqParams takes a mask per interrupt pin and SetDioIrqConfig maps IRQs to one DIO, while getIrqStatus() reads the status bytes returned on every SPI transaction and consults no pin mapping. So the CAD IRQ set now carries the verdict plus terminal RX events only - preamble and header stay off the pin, where they would fire the ISR mid-frame and run readData() on a partial packet - and isActivelyReceiving() still sees them. rearmReceive() cannot be unconditionally standby-free the way SX126x's is. cad_timeout 0 means "RX until one packet arrives, then standby", so the two re-arm paths leave the chip in different states: already listening after the handoff, back in standby after that packet's RX_DONE. It branches on a flag set at detection and cleared on use, so the first re-arm skips the standby and the next restores continuous RX. Forcing continuous RX through the timeout instead is not reliable - RadioLib converts it to RTC ticks through a float divide, so the 0xFFFFFF "no timeout" encoding is not addressable. On a CAD false positive the chip now waits in single-shot RX rather than returning to standby. Still listening, but a state the old code never entered. Not hardware-verified. * LoRa LBT: size the 2.4 GHz CAD slot per chip, not per band computeSlotTimeMsec() branches on myRegion->wideLora, which is a region rather than a part. LR1120 and LR2021 both run there, and both scan NUM_SYM_CAD symbols - but that branch read the flat NUM_SYM_CAD_24GHZ, so raising it to 8 for SX128x moved the slot for two families whose scan never changed. At LONG_FAST wideLora that priced ~22 ms of CAD against a ~10 ms scan. getCadSymbolCount() exists precisely so the slot tracks the real scan, but only the sub-GHz branch consulted it. Use it in both, and give SX128x an override returning NUM_SYM_CAD_24GHZ. That constant stops being a fleet-wide 2.4 GHz value and becomes what one driver reports; SX128x is unchanged at 8.78 symbols, while the LR parts on 2.4 GHz drop from a wrong 8.78 to 4.78. The error was conservative - slots too long, so throughput lost rather than collisions caused - but it is the same scan/slot mismatch the rest of this work removes. * LoRa LBT: declare CAD symbols per chip and per band, 8 on 2.4 GHz A 2.4 GHz region is shared: an LR1120 and an SX1280 can sit on the same mesh. Leaving the LR parts at 4 symbols there while SX1280 scanned 8 gave them different slot times, and so a different CSMA backoff scale between neighbours - the cross-family asymmetry this branch removed for sub-GHz, reintroduced one band up. Both LR families now scan 8 on 2.4 GHz, matching SX1280. Rather than switch on the band inside a shared constant, each driver states its own count for each band it works in, and derives the symNum it passes from the same accessor - so the scan and the slot cannot drift apart: SX126x 4 sub-GHz SX128x 8 on 2.4 GHz LR11x0 4 sub-GHz, 8 on 2.4 GHz LR20x0 4 sub-GHz, 8 on 2.4 GHz RF95 2 sub-GHz, its CAD being fixed in hardware and not settable NUM_SYM_CAD and NUM_SYM_CAD_24GHZ become base-class defaults nothing falls through to; they now record the fleet convention rather than any part's behaviour. LR20xx Table 6-19 stops at 4 symbols, so an 8-symbol scan reuses that row - the last measured point, erring high and so slightly less sensitive than a true 8-symbol value. Noted at the table. Also a comment pass over the branch: drop retrospective phrasing that describes what the code used to do, cut trailing justification clauses, and fix comments the per-band change left stale - one still claimed the LR parts scan fewer symbols than SX1280 on 2.4 GHz, which is now the opposite of the truth. * LoRa LBT: sweep up a CAD->RX handoff that delivers nothing A handoff leaves the chip listening by its own exit mode, so nothing in the driver arms the radio and no return value says whether it worked. If the detection was false, or the exit mode was not honoured, the node waits in a state we never verified - and on the LR parts that is single-shot RX, which does not return to continuous listening on its own. Stamp the handoff and let the existing ~1s poll re-arm if a whole max-length airtime passes with no packet, the same bound receiveDetected() uses. Clearing it on a real RX_DONE is obvious; clearing it in startSend() is not, but a TX can intervene before any packet arrives and its completion re-arms RX anyway, so leaving the marker set would have the poll re-arm a radio already listening. Reads the clock through Time::getMillis(), the same source Throttle compares against, so the site stays testable across the millis() wrap. 0 is the "none outstanding" sentinel and is tested before the elapsed compare. Also document on rearmReceive() that an override must never call the chip's own startReceive() - its standby+SetRx aborts the packet CAD just detected, which is the entire reason the override exists. * define txen pin to allow sx128x testing * LoRa LBT: bound the CAD->RX handoff in hardware, and re-arm after it delivers cadTimeout only applies to the CAD->RX exit mode. Every driver on develop exited CAD to standby, so the field was ignored and its value - 0, which no datasheet defines - was dead data. Switching the exit mode to RX made it load-bearing while the line itself never changed, so the diff showed nothing. All three handoff parts now bound that RX at one max-length airtime, taken from getPacketTime() so it tracks the preset. A detection that delivers nothing expires to standby and raises TIMEOUT, already mapped to the pin, so the existing ISR re-arms and the queued TX retries on its own backoff. handleReceiveInterrupt() returns early on TIMEOUT without RX_DONE; otherwise each dud detection would reach readData() and be logged as a bad packet. That RX is single-shot, so the arm after it delivers must be a full one. SX126x re-armed unconditionally without a standby, which left the chip in standby while the interface believed it was listening - and nothing swept it, since RX_DONE has already cleared the handoff stamp. A node that received a handoff packet then went deaf until its next transmit. It now carries the same cadHandedToRx flag the LR drivers already had. Sourcing, since the parts disagree and RadioLib's uniform ChannelScanConfig_t hides it: SX126x DS_SX1261-2 p98 (15.625us steps), LR11x0 UM Table 8-8 (31.25us), LR2021 lr20xx_driver cad_timeout_in_pll_step (31.25us, contra the datasheet's "32 MHz periods"). RadioLib scales both LR parts as 30.52us, so the programmed value lands ~2% long - harmless, and left for upstream. Also marks the three SWSD003 cells at 2 symbols/BW500/SF10-12 as that SDK's fallback rather than measurements. * LoRa LBT: own the CAD->RX handoff in one place, and void it on standby The handoff state was split across two classes: cadHandoffRxStart in RadioLibInterface, cadHandedToRx in each of SX126x/LR11x0/LR20x0, whose rearmReceive() overrides were byte-identical. Neither field was cleared by setStandby(), so a standby taken while a handoff was outstanding could strand cadHandedToRx set - and the next rearmReceive() would then re-attach the ISR to a chip sitting in standby, marking the interface as receiving with no RX_DONE ever to come and no recovery path. A stale cadHandoffRxStart was milder but still re-armed the radio over whatever was arriving at the time. Hold both in the base, arm them together in noteCadHandoffToRx(), clear both in setStandby(), and collapse the three overrides into one non-virtual base rearmReceive(). SX128x and RF95 never arm the flag, so they keep taking the plain startReceive() branch. Document on isChannelActive() that a true return must be followed by rearmReceive() - the invariant the whole scheme rests on. Two fixes fall out of the shape: - The handoff re-arm attaches a rising-edge ISR to a line that is not known to be low, unlike startReceive() which clears the IRQ status first. Follow it with checkRxDoneIrqFlag(), as startReceive() already does. - checkCadHandoffTimeout() used one max-length airtime, the same figure the chip's own cadTimeout now uses, so the two expired together. Losing that race left a queued ISR_RX to run against flags the software path had just cleared, falling past the TIMEOUT guard into readData() on an empty buffer. Double it so the hardware always expires first; the backstop then only covers an RX whose timer stopped on a header that never completed. * LoRa LBT: re-arm RX when a queued packet is dropped for tx_enabled=false Pre-existing on develop, not introduced by this branch, but it is the same failure this series exists to remove. TRANSMIT_DELAY_COMPLETED reaches startSend() only through isChannelActive(), which leaves the chip in standby for the transmit about to follow. When that transmit is then dropped because LoRa TX is disabled, the early return frees the packet and returns without arming anything: isReceiving stays false, so pollMissedIrqs() skips the radio too, and the node is deaf until something else re-arms it. Narrow - tx_enabled has to change between enqueue and dequeue, since send() checks it as well - but the fix is one call on a path that already exists. * LoRa LBT: say what the CAD slot time actually does per chip Two comments in RadioInterface.h described intentions the code does not carry out. NUM_SYM_CAD/NUM_SYM_CAD_24GHZ claimed the per-band values keep the CW backoff scale equal across a mesh. They do not: the slot has to match the scan each part really runs, and RF95's CAD is fixed in hardware at 2.25 symbols while SX126x/LR scan 4. A mixed sub-GHz mesh therefore runs two slot lengths, which is correct per part and worth stating rather than implying otherwise. The slotTimeMsec initializer runs during RadioInterface's own construction, so the virtual getCadSymbolCount() it calls always resolves to the fallbacks here, never a driver override. That is harmless only because applyModemConfig() recomputes it before anything reads it - left as-is deliberately, since initializing to 0 would make random(0, 2^CW) * slotTimeMsec fire an immediate transmit if anything ever did read it early. * LoRa LBT: set the SX128x CAD threshold at config time, not per scan isChannelActive() looked up the AN1200.77 PNR row and wrote register 0x0942 on every scan, but sf and bw only change in applyModemConfig(). That is an SPI round trip between the channel scan and the transmit that follows it, on the one path this file's own comments ask to keep short. Move it to applyCadDetPeak(), called from reconfigure() after the sf/bw setters it depends on, and from init() as well - init() does not route through reconfigure(), so it needs its own call. * ESP32: stop skipping light sleep forever on a stuck LoRa IRQ The skip added earlier in this series returns instead of sleeping whenever DIO1 already reads high, so the main loop can service the packet rather than strand it for the sleep chunk. It had no bound: a line that latches high with nothing clearing it makes every subsequent call skip too, and the node quietly stops sleeping at all, announced only by a LOG_INFO each time. Count consecutive skips - warn at 3, sleep through at 5, reset on any real sleep. If the IRQ has survived five attempts the main loop is not going to clear it, and never sleeping costs more than one stranded packet. Also drop the LORA_DIO1_EXTENDED_IO branch in loraWakeIrqAsserted(): those boards return from doLightSleep() before it is ever called. * nrf52 promicro: gate the P1.06 TXEN pin, and add an optional SX1280 build P1.06 was wired to SX126X_TXEN unconditionally earlier in this series, to drive an external PA on a bench unit. That is not safe to ship: three envs share this variant.h, and P1.06 is PIN_WIRE_SCL on the EasyProMicro pinout and PIN_EINK_BUSY on the InkHUD one. The default builds go back to RADIOLIB_NC. Put it behind -D PROMICRO_SX126X_TXEN, refused wherever the pin is already claimed. The EasyProMicro arrangement has its own SX126X_TXEN on P1.00 in the #else branch and never reaches this, so only the e-ink exclusion is needed. The pin was wanted for SX1280 in the first place - every other SX128X line is already assigned on this board and TXEN was the one missing - so add that too, behind -D PROMICRO_SX1280 and a new nrf52_promicro_diy-sx1280 env. Both opt-ins claim P1.06, so asking for both is an #error rather than a silent pick. Costs about 10.7 KB: the image ends at 0xE5140, 19 KB clear of the warm region, against 0xE2710 and 30 KB for the base env. * LoRa LBT: fix two defects in the CAD->RX handoff found in review Both reported by Copilot against 1fcd219af, both confirmed against the code as it stands after the handoff was hoisted into RadioLibInterface. rearmReceive() armed the MCU ISR before marking the interface as receiving. An ISR firing in that window queues ISR_RX while isReceiving is still false, so handleReceiveInterrupt() bails with "called while not in rx mode" and the packet is dropped - the exact packet the CAD handoff existed to catch. Reversed, which also makes this path match the order the drivers' own startReceive() has always used: base state, then arm, then checkRxDoneIrqFlag(). noteCadHandoffToRx() stored Time::getMillis() raw, but 0 is the "no handoff outstanding" sentinel that checkCadHandoffTimeout() tests before its elapsed comparison. A handoff armed on the one millisecond per wrap where getMillis() returns 0 reads as "none outstanding" and the backstop never fires. Step past it to 1, the same hand-remap Throttle.h's own TODO records GPS.cpp using for fixHoldEnds. One millisecond of error, once per 49.7 days. * ESP32: wake on SX128X_DIO1, which five SX1280 boards define instead of LORA_DIO1 enableLoraInterrupt() only ever looked at LORA_DIO1 and RF95_IRQ. RadioInterface builds an SX1280Interface from SX128X_DIO1, and several variants name the IRQ pin only that way, or put it on a different pin from LORA_DIO1: tlora_v2_1_18 LORA_DIO1 undefined SX128X_DIO1 26 betafpv_2400_tx_micro LORA_DIO1 undefined SX128X_DIO1 4 t-eth-elite LORA_DIO1 16 SX128X_DIO1 8 tlora_t3s3_v1 LORA_DIO1 33 // TCXO_EN? SX128X_DIO1 9 tlora_t3s3_epaper LORA_DIO1 33 // TCXO_EN? SX128X_DIO1 9 Every branch of the function is gated on defined(LORA_DIO1), so on the first two it preprocessed to an empty function: esp_sleep_enable_gpio_wakeup() still enabled GPIO as a wake class, but no pin was ever armed for the radio. On the other three it armed the wrong pin - GPIO33 there being the line the variant's own comment questions as TCXO_EN. Those boards could not be woken from light sleep by an incoming packet; they waited out the 30s chunk and pollMissedIrqs() recovered the latched RX_DONE about a second after waking. Delayed, not lost, but deaf for the duration. loraWakeIrqAsserted(), added earlier in this series, inherited the same blind spot and was dead code on the two boards with no LORA_DIO1 at all - so the skip-the-sleep fix never once fired on an SX1280. Resolve the pin per radio in loraWakeDio1Pin() and use it for arming, polling and teardown alike, so the three cannot drift apart again, and widen their guards to accept either macro. Behaviour is unchanged wherever it already worked: for any non-SX1280 radio the helper returns LORA_DIO1, including on RF95 boards where the SOC_PM_SUPPORT_EXT_WAKEUP branch arms it regardless of radio type. LORA_DIO1_EXTENDED_IO still short-circuits first. Note this is a power-profile change for those five boards, not only a fix: they gain a LoRa wake source they have never had, so on a busy channel their light sleep will now be cut short by traffic like every other board's. * LoRa LBT: trim three-line comments to the repo's two-line limit copilot-instructions.md:340 says one or two lines; the P1.06 availability note and the LR2021 DIO routing note were both three. CodeRabbit flagged both. * LoRa LBT: trace the CAD scan, its exits and the handoff outcome Eight short lines, all in the base class so they cost one set of strings rather than one per chip driver. The scan logs arm/busy/free; the in-chip GOTO_RX handoff logs started, then exactly one of pkt, empty, timeout or void - so a dropped or double-counted handoff is visible by inspection. void and timeout are LOG_WARN because both mean an invariant broke: a standby discarded a live handoff, or the chip's own timer failed to fire and the software backstop had to re-arm. The rest are LOG_DEBUG, so a node at normal log level stays quiet unless something is actually wrong. The two pre-existing handoff strings were 44 and 41 bytes of flash; cut to "CAD>RX timeout" and "CAD>RX empty", they pay for most of the new set. The shared "CAD" prefix does not dedup - only string suffixes merge - and splitting it out behind a %s measured 8 bytes worse on Cortex-M once the extra pointer argument per call site is counted. Also fixes the delivered-packet trace, which sat above the TIMEOUT check and so claimed a packet on a window that expired empty. * LoRa LBT: restore the SX1280 CAD threshold after a chip-state recovery The calibrated AN1200.77 PNR threshold moved from a per-scan register write to a config-time one, so it now has to be re-applied wherever the chip's state is rebuilt. recoverChipStateLoss() hardware-resets the chip and reprograms only the modem params, which left a recovered SX1280 doing CAD against the vendor default threshold until the next reconfigure() - and on a node whose config never changes, that is the rest of its uptime. * LoRa LBT: retire RX flags that plain RX never completes Plain RX runs with no chip timeout (startReceiveDutyCycleAuto(16, 8) falls back to RX_TIMEOUT_INF) and the DIO mask carries RX_DONE only, so a PREAMBLE_DETECTED, HEADER_VALID or HEADER_ERR that never completes stays latched until something re-arms RX. On a node with nothing to send, nothing does. The CAD->RX handoff already has this recovery (cadTimeout plus checkCadHandoffTimeout()); plain RX had none. checkStaleRxFlags() is its twin on the ~1 s pollMissedIrqs() tick. It notes when flags are first seen, and if no RX_DONE has consumed them one max-length packet later: - a header (valid or errored) re-arms RX. RX_DONE clears every flag in readData(), so a header still latched has no frame behind it. SX1280 DS rev 3.3 section 16.2: a header error leaves RX wedged with no RX_DONE or timeout, and only a re-arm recovers it. - a bare preamble is only cleared. ClearIrqStatus/ClearIrq changes the IRQ register alone on SX126x, SX1280, LR11x0 and LR20x0, so it never aborts a reception; startReceive() goes through standby and would. HEADER_VALID is never cleared on its own: RadioLib's readData() reads it at RX_DONE, and on LR2021 its absence reports the packet as header damaged. Skipped while a CAD handoff is outstanding (its own timeout owns that RX) and while RX_DONE is pending. RF95 opts out: its carrier sense reads modem status rather than latched flags, and SX127x maps neither PREAMBLE_DETECTED nor HEADER_ERR. The decision is staleRxFlagAction() in RadioInterface.h, pinned in test_radio at both sides of the window and for each action. * LoRa LBT: hold TX on a seen preamble instead of judging it by a deadline receiveDetected() declared a bare PREAMBLE_DETECTED false once 2 * preambleTimeMsec passed without HEADER_VALID - about 8 ms at SF7/BW500. Nothing reads the flags on that cadence: they are latched status bits, never routed to DIO, and the only reader is the TX path, once per CSMA backoff (0-56 ms at SF7/BW500). The verdict was usually reached by a read older than its own deadline, and the queued TX went out over the frame. Reported in #11933 by @jp-bennett, whose two-node bench measured own TX a median 43 ms after the verdict, inside a ~102 ms frame. This branch's longer CAD slot widens it to LONG_FAST: 0-308 ms of backoff at CWmin against a 262 ms deadline. A read that sees a bare preamble now clears PREAMBLE_DETECTED and holds TX for one max-length airtime. The clear touches only the IRQ register (ClearIrqStatus/ClearIrq on SX126x, SX1280, LR11x0, LR20x0), so it never aborts a reception. Reads during the hold report busy, so the normal random backoff still spreads retries, and any RX interrupt ends the hold early. A preamble that fires again during the hold stays latched, and the first read after it holds again: a source that keeps triggering keeps deferring TX, one airtime at a time, while a single stale flag cannot block it. HEADER_VALID is never cleared here - RadioLib's readData() reads it at RX_DONE, and on LR2021 its absence reports the packet as header-damaged - and keeps its one-airtime ageing. The SX126x software IRQ poll routes its bare-preamble case through the same hold instead of clearing on every tick while a TX is queued, which would have released TX before the next read could see a refire. That poll read the private txQueue directly, so meshnology_w10 - the only env that defines LORA_DIO1_SOFTWARE_POLL - did not compile. hasQueuedTx() exposes the one fact it needs. #11937 addresses the same defect by deferring the verdict when the previous read was stale; the two conflict in receiveDetected(). * LoRa: re-arm RX before logging the packet that just went out After TX_DONE the chip sits in standby, deaf, until startReceive(). The ISR_TX path used to log airtime ("Packet TX") and printPacket() the sent packet first, which kept it deaf for ~2-5 ms more depending on where the log goes. Measured on an nRF52 SX1262 bench node: the TX_DONE-to-RX gap drops from ~8.8 ms to ~3.7 ms with USB serial logging (~11.3 to ~6.2 ms with the log also going to a phone over BLE). completeSending() is split into detachSentPacket(), which clears sendingPacket and runs packetReleased() so RX re-arms on the home config, and finishSentPacket(), which does the airtime, counters, log and pool release. setStandby() callers keep the combined completeSending(). * SX126x: keep the TCXO powered in standby RadioLib leaves standbyXOSC off, so standby and the post-TX/RX fallback both use STDBY_RC, which powers a DIO3-controlled TCXO down. Every SetRx and SetTx after that first waits out the TCXO start-up delay programmed into the chip (RadioLib's setTCXO() default, 5000 us). On an nRF52 SX1262 bench node the RX start after TX_DONE took ~6.0 ms at SF7, SF9 and SF11 alike; with setStandbyXOSC(true) it takes ~0.95 ms, and the TX_DONE-to-RX gap falls to ~1.1 ms together with the previous commit. The result held across the periodic AGC reset. Only boards that drive a TCXO from DIO3 change. The cost is the TCXO and XOSC current while the chip sits in standby, which the firmware keeps brief; duty-cycled RX still sleeps, and powers the TCXO down, between windows. * LoRa: skip the periodic AGC reset while a packet is queued resetAGC() ends in startReceive(), and trySetStandby() first runs checkNotification() to drain pending events. With a packet queued that can be TRANSMIT_DELAY_COMPLETED: CAD passes, startSend() keys up, and trySetStandby() then puts the chip in standby over the transmission it just started. completeSending() still reports the packet as sent and counts its airtime. A bench node logged this twice in 20 minutes; one was a packet's last retry, so it never went on air. Skip the reset for this tick when a TX is queued. The queue drains within seconds, and the reset comes round again on the next interval. * GPS: re-enable on a LoRa config save only when the region was unset #10386 keeps the GPS off until a region is set and turns it on when a LoRa config save sets one. The check never looked at the old region, so a GPS the probe had disabled ("GPS not detected; not present this boot") was re-enabled by every LoRa config save, preset changes included, and the probe ran again. The probe blocks the cooperative main loop: on a bench node with no GPS fitted, TX_DONE handling stalled by 0.34-0.89 s on 4 of 9 packets after a preset change, leaving the radio deaf in standby for that long. * LR20x0: CAD exit to RX is 0x01 on the LR2021 RadioLib (510e00cf) defines RADIOLIB_LR2021_CAD_EXIT_MODE_RX as 0x02. The LR2021 takes SetLoraCadParams exit modes 0x00 CAD only, 0x01 RX on detection and 0x10 TX when clear, as the LR11x0 does, and refuses 0x02 with a processing error (-706). With the CAD>RX handoff passing 0x02, every scan failed before the CAD ran and isChannelActive() reported the channel clear: on a Seeed Mesh-Tracker X1, 539 scans against live traffic gave no busy verdict, so the node transmitted with no carrier sense. Pass 0x01, and warn when a scan fails rather than reporting it clear in silence. * LoRa LBT: take out the preamble hold on a seen preamble Revertbe3bf1f38. receiveDetected() judges a preamble by its two-preamble deadline again, and the SX126x IRQ poll clears a bare preamble directly. hasQueuedTx() stays: periodicRadioMaintenance() still uses it. * SX126x: read the queued-TX state through hasQueuedTx() in the IRQ poll957fbd3f5restored the poll's direct txQueue.empty() read along with the preamble clear. txQueue is private to RadioLibInterface, so meshnology_w10, the only env that defines LORA_DIO1_SOFTWARE_POLL, no longer compiled. * SX126x: retry a standby that reports a stale command timeout After a CAD-to-RX handoff's bounded RX times out, the status byte that comes back with the next SET_STANDBY still carries command status 0x3 (timeout). RadioLib reports that as RADIOLIB_ERR_SPI_CMD_TIMEOUT although the chip took the command, so every "CAD>RX empty" was followed by a failed standby, a failed RX start and a full chip re-init. Send SET_STANDBY once more on that error; the second one sees a fresh status. Measured on a CH341 host (SHORT_TURBO): chip re-inits went from 3 to 0, and all 7 retries returned 0. Co-Authored-By: Jonathan Bennett <jbennett@incomsystems.biz> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01REkPVFh6kvG4AZJ5A8AtM9 (cherry picked from commit57d6a86b71) --------- Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
726 lines
26 KiB
C++
726 lines
26 KiB
C++
#include "configuration.h"
|
|
|
|
#if !MESHTASTIC_EXCLUDE_GPS
|
|
#include "GPS.h"
|
|
#endif
|
|
|
|
#include "Default.h"
|
|
#include "MeshRadio.h"
|
|
#include "MeshService.h"
|
|
#include "NodeDB.h"
|
|
#include "PowerMon.h"
|
|
#include "TransmitHistory.h"
|
|
#include "detect/LoRaRadioType.h"
|
|
#include "error.h"
|
|
#include "main.h"
|
|
#include "modules/StatusLEDModule.h"
|
|
#include "sleep.h"
|
|
#include "target_specific.h"
|
|
|
|
#ifdef ARCH_ESP32
|
|
#include "esp_pm.h"
|
|
#if HAS_WIFI
|
|
#include "mesh/wifi/WiFiAPClient.h"
|
|
#endif
|
|
#include "rom/rtc.h"
|
|
#include <RadioLib.h>
|
|
#include <driver/rtc_io.h>
|
|
#include <driver/uart.h>
|
|
|
|
esp_sleep_source_t wakeCause; // the reason we booted this time
|
|
#endif
|
|
#include "Throttle.h"
|
|
|
|
#ifdef USE_PCA95X5
|
|
#include PCA95X5_INC
|
|
extern PCA95X5_CLS io;
|
|
#endif
|
|
|
|
#ifdef HAS_PPM
|
|
#include <XPowersLib.h>
|
|
extern XPowersPPM *PPM;
|
|
#endif
|
|
|
|
#ifndef INCLUDE_vTaskSuspend
|
|
#define INCLUDE_vTaskSuspend 0
|
|
#endif
|
|
|
|
/// Called to ask any observers if they want to veto sleep. Return 1 to veto or 0 to allow sleep to happen
|
|
Observable<void *> preflightSleep;
|
|
|
|
/// Called to tell observers we are now entering (deep) sleep and you should prepare. Must return 0
|
|
Observable<void *> notifyDeepSleep;
|
|
|
|
/// Called to tell observers we are rebooting ASAP. Must return 0
|
|
Observable<void *> notifyReboot;
|
|
|
|
#ifdef ARCH_ESP32
|
|
/// Called to tell observers that light sleep is about to begin
|
|
Observable<void *> notifyLightSleep;
|
|
|
|
/// Called to tell observers that light sleep has just ended, and why it ended
|
|
Observable<esp_sleep_wakeup_cause_t> notifyLightSleepEnd;
|
|
#endif
|
|
|
|
// deep sleep support
|
|
RTC_DATA_ATTR int bootCount = 0;
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Application
|
|
// -----------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Control CPU core speed (80MHz vs 240MHz)
|
|
*
|
|
* We leave CPU at full speed during init, but once loop is called switch to low speed (for a 50% power savings)
|
|
*
|
|
*/
|
|
void setCPUFast(bool on)
|
|
{
|
|
#if defined(ARCH_ESP32) && HAS_WIFI && !HAS_TFT && !defined(T_LORA_PAGER) && !defined(T_DECK)
|
|
|
|
if (isWifiAvailable()) {
|
|
/*
|
|
*
|
|
* There's a newly introduced bug in the espressif framework where WiFi is
|
|
* unstable when the frequency is less than 240MHz.
|
|
*
|
|
* This mostly impacts WiFi AP mode but we'll bump the frequency for
|
|
* all WiFi use cases.
|
|
* (Added: Dec 23, 2021 by Jm Casler)
|
|
*/
|
|
#ifndef CONFIG_IDF_TARGET_ESP32C3
|
|
LOG_DEBUG("Set CPU to 240MHz because WiFi is in use");
|
|
setCpuFrequencyMhz(240);
|
|
#endif
|
|
return;
|
|
}
|
|
|
|
// The Heltec LORA32 V1 runs at 26 MHz base frequency and doesn't react well to switching to 80 MHz...
|
|
#if !defined(ARDUINO_HELTEC_WIFI_LORA_32) && !defined(CONFIG_IDF_TARGET_ESP32C3)
|
|
setCpuFrequencyMhz(on ? 240 : 80);
|
|
#endif
|
|
|
|
#endif
|
|
}
|
|
|
|
// Perform power on init that we do on each wake from deep sleep
|
|
void initDeepSleep()
|
|
{
|
|
#ifdef ARCH_ESP32
|
|
bootCount++;
|
|
const char *reason;
|
|
wakeCause = esp_sleep_get_wakeup_cause();
|
|
|
|
switch (wakeCause) {
|
|
case ESP_SLEEP_WAKEUP_EXT0:
|
|
reason = "ext0 RTC_IO";
|
|
break;
|
|
case ESP_SLEEP_WAKEUP_EXT1:
|
|
reason = "ext1 RTC_CNTL";
|
|
break;
|
|
case ESP_SLEEP_WAKEUP_TIMER:
|
|
reason = "timer";
|
|
break;
|
|
case ESP_SLEEP_WAKEUP_TOUCHPAD:
|
|
reason = "touchpad";
|
|
break;
|
|
case ESP_SLEEP_WAKEUP_ULP:
|
|
reason = "ULP program";
|
|
break;
|
|
default:
|
|
reason = "reset";
|
|
break;
|
|
}
|
|
/*
|
|
Not using yet because we are using wake on all buttons being low
|
|
|
|
wakeButtons = esp_sleep_get_ext1_wakeup_status(); // If one of these buttons is set it was the reason we woke
|
|
if (wakeCause == ESP_SLEEP_WAKEUP_EXT1 && !wakeButtons) // we must have been using the 'all buttons rule for waking' to
|
|
support busted boards, assume button one was pressed wakeButtons = ((uint64_t)1) << buttons.gpios[0];
|
|
*/
|
|
|
|
#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE)
|
|
// If we booted because our timer ran out or the user pressed reset, send those as fake events
|
|
RESET_REASON hwReason = rtc_get_reset_reason(0);
|
|
|
|
#ifdef CONFIG_IDF_TARGET_ESP32P4
|
|
if (hwReason == BROWN_OUT_RESET)
|
|
reason = "brownout";
|
|
else if (hwReason == HP_CORE_HP_WDT_RESET)
|
|
reason = "taskWatchdog";
|
|
else if (hwReason == HP_CORE_LP_WDT_RESET)
|
|
reason = "intWatchdog";
|
|
else if (hwReason == CHIP_LP_WDT_RESET)
|
|
reason = "chipWatchdog";
|
|
else if (hwReason == SUPER_WDT_RESET)
|
|
reason = "superWatchdog";
|
|
else if (hwReason == HP_SYS_HP_WDT_RESET)
|
|
reason = "systemWatchdog";
|
|
else if (hwReason == HP_SYS_LP_WDT_RESET)
|
|
reason = "systemLowPowerWatchdog";
|
|
#else
|
|
if (hwReason == RTCWDT_BROWN_OUT_RESET)
|
|
reason = "brownout";
|
|
else if (hwReason == RTCWDT_RTC_RESET)
|
|
reason = "rtcWatchdog";
|
|
else if (hwReason == TG0WDT_SYS_RESET)
|
|
reason = "taskWatchdog";
|
|
else if (hwReason == TG1WDT_SYS_RESET)
|
|
reason = "intWatchdog";
|
|
#endif
|
|
LOG_INFO("Booted, wake cause %d (boot count %d), reset_reason=%s", wakeCause, bootCount, reason);
|
|
#endif
|
|
|
|
#if SOC_RTCIO_HOLD_SUPPORTED
|
|
// If waking from sleep, release any and all RTC GPIOs
|
|
if (wakeCause != ESP_SLEEP_WAKEUP_UNDEFINED) {
|
|
LOG_DEBUG("Disable any holds on RTC IO pads");
|
|
for (uint8_t i = 0; i <= GPIO_NUM_MAX; i++) {
|
|
if (rtc_gpio_is_valid_gpio((gpio_num_t)i))
|
|
rtc_gpio_hold_dis((gpio_num_t)i);
|
|
|
|
// ESP32 (original)
|
|
else if (GPIO_IS_VALID_OUTPUT_GPIO((gpio_num_t)i))
|
|
gpio_hold_dis((gpio_num_t)i);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
#endif
|
|
}
|
|
|
|
bool doPreflightSleep(bool deepSleep)
|
|
{
|
|
// Observers only get a void*: non-NULL means the hardware (radio) is about to be powered
|
|
// down (deep sleep / shutdown), NULL means a light sleep where the radio keeps running
|
|
static const bool deepSleepFlag = true;
|
|
if (preflightSleep.notifyObservers(deepSleep ? (void *)&deepSleepFlag : NULL) != 0)
|
|
return false; // vetoed
|
|
else
|
|
return true;
|
|
}
|
|
|
|
/// Tell devices we are going to sleep and wait for them to handle things
|
|
static void waitEnterSleep(bool skipPreflight, bool deepSleep)
|
|
{
|
|
if (!skipPreflight) {
|
|
uint32_t now = millis();
|
|
while (!doPreflightSleep(deepSleep)) {
|
|
delay(100); // Kinda yucky - wait until radio says say we can shutdown (finished in process sends/receives)
|
|
|
|
if (!Throttle::isWithinTimespanMs(now,
|
|
THIRTY_SECONDS_MS)) { // If we wait too long just report an error and go to sleep
|
|
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_SLEEP_ENTER_WAIT);
|
|
assert(0); // FIXME - for now we just restart, need to fix bug #167
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Code that still needs to be moved into notifyObservers
|
|
console->flush(); // send all our characters before we stop cpu clock
|
|
setBluetoothEnable(false); // has to be off before calling light sleep
|
|
}
|
|
|
|
void doDeepSleep(uint32_t msecToWake, bool skipPreflight = false, bool skipSaveNodeDb = false)
|
|
{
|
|
if (INCLUDE_vTaskSuspend && (msecToWake == portMAX_DELAY)) {
|
|
LOG_INFO("Enter deep sleep forever");
|
|
} else {
|
|
LOG_INFO("Enter deep sleep for %u seconds", msecToWake / 1000);
|
|
}
|
|
|
|
// not using wifi yet, but once we are this is needed to shutoff the radio hw
|
|
// esp_wifi_stop();
|
|
waitEnterSleep(skipPreflight, true);
|
|
|
|
#if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_BLUETOOTH
|
|
// Full shutdown of bluetooth hardware
|
|
if (nimbleBluetooth)
|
|
nimbleBluetooth->deinit();
|
|
#endif
|
|
|
|
#ifdef ARCH_ESP32
|
|
if (!shouldLoraWake(msecToWake))
|
|
notifyDeepSleep.notifyObservers(NULL);
|
|
#else
|
|
notifyDeepSleep.notifyObservers(NULL);
|
|
#endif
|
|
|
|
powerMon->setState(meshtastic_PowerMon_State_CPU_DeepSleep);
|
|
if (screen)
|
|
screen->doDeepSleep(); // datasheet says this will draw only 10ua
|
|
|
|
if (!skipSaveNodeDb) {
|
|
nodeDB->saveToDisk();
|
|
}
|
|
|
|
// Persist broadcast transmit times so throttle survives reboot
|
|
if (transmitHistory)
|
|
transmitHistory->saveToDisk();
|
|
|
|
#ifdef PIN_POWER_EN
|
|
digitalWrite(PIN_POWER_EN, LOW);
|
|
pinMode(PIN_POWER_EN, INPUT); // power off peripherals
|
|
#endif
|
|
|
|
#ifdef RAK_WISMESH_TAP_V2
|
|
digitalWrite(SDCARD_CS, LOW);
|
|
#endif
|
|
|
|
#if defined(TRACKER_T1000_E) || defined(MESH_TRACKER_X1)
|
|
digitalWrite(GPS_VRTC_EN, LOW);
|
|
digitalWrite(PIN_GPS_RESET, LOW);
|
|
digitalWrite(GPS_SLEEP_INT, LOW);
|
|
digitalWrite(GPS_RTC_INT, LOW);
|
|
#ifdef GPS_RESETB_OUT
|
|
pinMode(GPS_RESETB_OUT, OUTPUT);
|
|
digitalWrite(GPS_RESETB_OUT, LOW);
|
|
#endif
|
|
|
|
#ifdef BUZZER_EN_PIN
|
|
digitalWrite(BUZZER_EN_PIN, LOW);
|
|
#endif
|
|
|
|
#ifdef PIN_DRV_EN
|
|
digitalWrite(PIN_DRV_EN, LOW);
|
|
#endif
|
|
|
|
#ifdef PIN_3V3_EN
|
|
digitalWrite(PIN_3V3_EN, LOW);
|
|
#endif
|
|
#ifdef PIN_WD_EN
|
|
digitalWrite(PIN_WD_EN, LOW);
|
|
#endif
|
|
#endif
|
|
statusLEDModule->setPowerLED(false);
|
|
#ifdef RESET_OLED
|
|
digitalWrite(RESET_OLED, 1); // put the display in reset before killing its power
|
|
#endif
|
|
|
|
#if defined(VEXT_ENABLE)
|
|
digitalWrite(VEXT_ENABLE, !VEXT_ON_VALUE); // turn on the display power
|
|
#endif
|
|
|
|
#ifdef ARCH_ESP32
|
|
if (shouldLoraWake(msecToWake)) {
|
|
enableLoraInterrupt();
|
|
}
|
|
#ifdef BUTTON_PIN
|
|
// Avoid leakage through button pin
|
|
if (GPIO_IS_VALID_OUTPUT_GPIO(BUTTON_PIN)) {
|
|
#ifdef BUTTON_NEED_PULLUP
|
|
pinMode(BUTTON_PIN, INPUT_PULLUP);
|
|
#else
|
|
pinMode(BUTTON_PIN, INPUT);
|
|
#endif
|
|
// A held pad ignores ext1_wakeup_prepare()'s re-route to RTC, so never hold the pin we wake on.
|
|
if (config.device.button_gpio && config.device.button_gpio != BUTTON_PIN)
|
|
gpio_hold_en((gpio_num_t)BUTTON_PIN);
|
|
}
|
|
#endif
|
|
#ifdef SENSECAP_INDICATOR
|
|
// Portexpander definition does not pass GPIO_IS_VALID_OUTPUT_GPIO
|
|
pinMode(LORA_CS, OUTPUT);
|
|
digitalWrite(LORA_CS, HIGH);
|
|
gpio_hold_en((gpio_num_t)LORA_CS);
|
|
#elif defined(ELECROW_PANEL)
|
|
// Elecrow panels do not use LORA_CS, do nothing
|
|
#else
|
|
if (GPIO_IS_VALID_OUTPUT_GPIO(LORA_CS)) {
|
|
// LoRa CS (RADIO_NSS) needs to stay HIGH, even during deep sleep
|
|
pinMode(LORA_CS, OUTPUT);
|
|
digitalWrite(LORA_CS, HIGH);
|
|
gpio_hold_en((gpio_num_t)LORA_CS);
|
|
}
|
|
#endif
|
|
#endif
|
|
|
|
#ifdef HAS_PPM
|
|
if (PPM) {
|
|
// BQ25896 PMIC shutdown is a hard power-off state.
|
|
// Only use it for "sleep forever" / explicit shutdown, because timed deep sleep
|
|
// must remain wakeable by RTC timer.
|
|
if (msecToWake == portMAX_DELAY) {
|
|
LOG_INFO("PPM shutdown");
|
|
console->flush();
|
|
PPM->shutdown();
|
|
}
|
|
}
|
|
#endif
|
|
|
|
#ifdef HAS_PMU
|
|
if (pmu_found && PMU) {
|
|
// Obsolete comment: from back when we we used to receive lora packets while CPU was in deep sleep.
|
|
// We no longer do that, because our light-sleep current draws are low enough and it provides fast start/low cost
|
|
// wake. We currently use deep sleep only for 'we want our device to actually be off - because our battery is
|
|
// critically low'. So in deep sleep we DO shut down power to LORA (and when we boot later we completely reinit it)
|
|
//
|
|
// No need to turn this off if the power draw in sleep mode really is just 0.2uA and turning it off would
|
|
// leave floating input for the IRQ line
|
|
// If we want to leave the radio receiving in would be 11.5mA current draw, but most of the time it is just waiting
|
|
// in its sequencer (true?) so the average power draw should be much lower even if we were listening for packets
|
|
// all the time.
|
|
PMU->setChargingLedMode(XPOWERS_CHG_LED_OFF);
|
|
|
|
uint8_t model = PMU->getChipModel();
|
|
if (model == XPOWERS_AXP2101) {
|
|
if (HW_VENDOR == meshtastic_HardwareModel_TBEAM) {
|
|
// t-beam v1.2 radio power channel
|
|
PMU->disablePowerOutput(XPOWERS_ALDO2); // lora radio power channel
|
|
} else if (HW_VENDOR == meshtastic_HardwareModel_LILYGO_TBEAM_S3_CORE ||
|
|
HW_VENDOR == meshtastic_HardwareModel_T_WATCH_S3 || HW_VENDOR == meshtastic_HardwareModel_T_WATCH_ULTRA) {
|
|
PMU->disablePowerOutput(XPOWERS_ALDO3); // lora radio power channel
|
|
}
|
|
} else if (model == XPOWERS_AXP192) {
|
|
// t-beam v1.1 radio power channel
|
|
PMU->disablePowerOutput(XPOWERS_LDO2); // lora radio power channel
|
|
}
|
|
if (msecToWake == portMAX_DELAY) {
|
|
LOG_INFO("PMU shutdown");
|
|
console->flush();
|
|
PMU->shutdown();
|
|
}
|
|
}
|
|
#endif
|
|
|
|
#if !MESHTASTIC_EXCLUDE_I2C && defined(ARCH_ESP32) && defined(I2C_SDA)
|
|
// Added by https://github.com/meshtastic/firmware/pull/4418
|
|
// Possibly to support Heltec Capsule Sensor?
|
|
Wire.end();
|
|
pinMode(I2C_SDA, ANALOG);
|
|
pinMode(I2C_SCL, ANALOG);
|
|
#endif
|
|
|
|
console->flush();
|
|
cpuDeepSleep(msecToWake);
|
|
}
|
|
|
|
#ifdef ARCH_ESP32
|
|
/**
|
|
* The GPIO an SX126x/SX128x/LR part raises its IRQ on, or -1 if there is no usable pin.
|
|
*
|
|
* Nearly every variant spells it LORA_DIO1, but an SX1280 board may define SX128X_DIO1 on a different
|
|
* pin (t-eth-elite, tlora_t3s3_v1) or as the only one of the two (tlora_v2_1_18, betafpv_2400_tx_micro),
|
|
* because that is the name RadioInterface hands to the driver. Resolve it per radio rather than per
|
|
* macro, so arming, polling and teardown below cannot pick different pins. RF95 keeps RF95_IRQ.
|
|
*/
|
|
static int loraWakeDio1Pin()
|
|
{
|
|
#if defined(SX128X_DIO1) && (SX128X_DIO1 != RADIOLIB_NC)
|
|
if (radioType == SX1280_RADIO)
|
|
return (int)SX128X_DIO1;
|
|
#endif
|
|
#if defined(LORA_DIO1) && (LORA_DIO1 != RADIOLIB_NC)
|
|
return (int)LORA_DIO1;
|
|
#endif
|
|
return -1;
|
|
}
|
|
|
|
/**
|
|
* Is the LoRa IRQ line already asserted right now?
|
|
*
|
|
* DIO1 (SX126x/SX128x) and RF95_IRQ are active-high, so a HIGH level means the radio has already
|
|
* latched an interrupt (typically an RX_DONE). If that edge lands in the small window between
|
|
* enableLoraInterrupt() and esp_light_sleep_start(), the DIO ISR fires and detachInterrupt() clears
|
|
* the per-pin GPIO wake enable as a side effect - the node would then sleep with the packet latched
|
|
* but the wake source disarmed, stranding it for the rest of the sleep chunk (up to 30s). Reading the
|
|
* raw line just before sleeping lets us skip that sleep and let the main loop service the packet.
|
|
* Boards where DIO1 is a virtual/expander pin never reach here - doLightSleep() returns early on
|
|
* LORA_DIO1_EXTENDED_IO before this is called.
|
|
*/
|
|
static bool loraWakeIrqAsserted()
|
|
{
|
|
#if defined(RF95_IRQ) && (RF95_IRQ != RADIOLIB_NC)
|
|
if (radioType == RF95_RADIO)
|
|
return gpio_get_level((gpio_num_t)RF95_IRQ) == 1;
|
|
#endif
|
|
const int dio1 = loraWakeDio1Pin();
|
|
if (dio1 >= 0 && radioType != RF95_RADIO)
|
|
return gpio_get_level((gpio_num_t)dio1) == 1;
|
|
return false;
|
|
}
|
|
|
|
// Consecutive sleeps skipped because the LoRa IRQ was already asserted. A line stuck high would
|
|
// otherwise stop the node sleeping for good, so give up on skipping once it stops looking transient.
|
|
static uint8_t loraIrqSkippedSleeps = 0;
|
|
#define LORA_IRQ_SKIP_SLEEP_WARN 3
|
|
#define LORA_IRQ_SKIP_SLEEP_MAX 5
|
|
|
|
/**
|
|
* enter light sleep (preserves ram but stops everything about CPU).
|
|
*
|
|
* Returns (after restoring hw state) when the user presses a button or we get a LoRa interrupt
|
|
*/
|
|
esp_sleep_wakeup_cause_t doLightSleep(uint64_t sleepMsec) // FIXME, use a more reasonable default
|
|
{
|
|
// LOG_DEBUG("Enter light sleep");
|
|
|
|
// LORA_DIO1 is an extended IO pin (on an I/O expander). Setting it as a wake-up pin will cause problems,
|
|
// such as the device not entering light sleep. Boards opt in with LORA_DIO1_EXTENDED_IO in their variant.
|
|
#if defined(LORA_DIO1_EXTENDED_IO)
|
|
return ESP_SLEEP_WAKEUP_TIMER;
|
|
#endif
|
|
|
|
waitEnterSleep(false, false);
|
|
notifyLightSleep.notifyObservers(NULL); // Button interrupts are detached here
|
|
|
|
uint64_t sleepUsec = sleepMsec * 1000LL;
|
|
|
|
// NOTE! ESP docs say we must disable bluetooth and wifi before light sleep
|
|
|
|
#if SOC_PM_SUPPORT_RTC_PERIPH_PD
|
|
// We want RTC peripherals to stay on
|
|
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
|
|
#endif
|
|
|
|
#if defined(BUTTON_PIN) && defined(BUTTON_NEED_PULLUP)
|
|
gpio_pullup_en((gpio_num_t)BUTTON_PIN);
|
|
#endif
|
|
|
|
#ifdef SERIAL0_RX_GPIO
|
|
// We treat the serial port as a GPIO for a fast/low power way of waking, if we see a rising edge that means
|
|
// someone started to send something
|
|
|
|
// gpio 3 is RXD for serialport 0 on ESP32
|
|
// Send a few Z characters to wake the port
|
|
|
|
// this doesn't work on TBEAMs when the USB is depowered (causes bogus interrupts)
|
|
// So we disable this "wake on serial" feature - because now when a TBEAM (only) has power connected it
|
|
// never tries to go to sleep if the user is using the API
|
|
// gpio_wakeup_enable((gpio_num_t)SERIAL0_RX_GPIO, GPIO_INTR_LOW_LEVEL);
|
|
|
|
// doesn't help - I think the USB-UART chip losing power is pulling the signal low
|
|
// gpio_pullup_en((gpio_num_t)SERIAL0_RX_GPIO);
|
|
|
|
// alas - can only work if using the refclock, which is limited to about 9600 bps
|
|
// assert(uart_set_wakeup_threshold(UART_NUM_0, 3) == ESP_OK);
|
|
// assert(esp_sleep_enable_uart_wakeup(0) == ESP_OK);
|
|
#endif
|
|
#ifdef ROTARY_PRESS
|
|
// The enableLoraInterrupt() method is using ext0_wakeup, so we are forced to use GPIO wakeup
|
|
gpio_wakeup_enable((gpio_num_t)ROTARY_PRESS, GPIO_INTR_LOW_LEVEL);
|
|
#endif
|
|
#ifdef KB_INT
|
|
#if KB_INT_WAKE_ON_HIGH
|
|
gpio_wakeup_enable((gpio_num_t)KB_INT, GPIO_INTR_HIGH_LEVEL);
|
|
#else
|
|
gpio_wakeup_enable((gpio_num_t)KB_INT, GPIO_INTR_LOW_LEVEL);
|
|
#endif // KB_INT_WAKE_ON_HIGH
|
|
#endif // KB_INT
|
|
#ifdef BOARD_PCA9535_INT
|
|
// Side-key interrupt line from PCA9535 expander (active low).
|
|
gpio_wakeup_enable((gpio_num_t)BOARD_PCA9535_INT, GPIO_INTR_LOW_LEVEL);
|
|
#endif
|
|
#ifdef BUTTON_PIN
|
|
gpio_num_t pin = (gpio_num_t)(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN);
|
|
gpio_wakeup_enable(pin, GPIO_INTR_LOW_LEVEL);
|
|
#endif
|
|
#if defined(INPUTDRIVER_TWO_WAY_ROCKER_BTN) || defined(INPUTDRIVER_ENCODER_BTN)
|
|
#if defined(INPUTDRIVER_TWO_WAY_ROCKER_BTN)
|
|
#define INPUTDRIVER_WAKE_BTN_PIN INPUTDRIVER_TWO_WAY_ROCKER_BTN
|
|
#else
|
|
#define INPUTDRIVER_WAKE_BTN_PIN INPUTDRIVER_ENCODER_BTN
|
|
#endif
|
|
// Most of these switches idle high and pull to ground, but not all. Arming the wrong level on a
|
|
// button that idles low means the wake condition is already true, and light sleep ends the
|
|
// instant it begins. Defaults to low, so only a board that says otherwise changes behaviour.
|
|
#if defined(INPUTDRIVER_ENCODER_BTN_ACTIVE_LOW) && !INPUTDRIVER_ENCODER_BTN_ACTIVE_LOW
|
|
gpio_wakeup_enable((gpio_num_t)INPUTDRIVER_WAKE_BTN_PIN, GPIO_INTR_HIGH_LEVEL);
|
|
#else
|
|
gpio_wakeup_enable((gpio_num_t)INPUTDRIVER_WAKE_BTN_PIN, GPIO_INTR_LOW_LEVEL);
|
|
#endif
|
|
#endif
|
|
#if defined(WAKE_ON_TOUCH)
|
|
gpio_wakeup_enable((gpio_num_t)SCREEN_TOUCH_INT, GPIO_INTR_LOW_LEVEL);
|
|
#endif
|
|
#ifdef MOTION_WAKE_INT_PIN
|
|
// Only arm motion wake when the user asked for it, otherwise every tilt costs a wakeup.
|
|
if (config.display.wake_on_tap_or_motion)
|
|
gpio_wakeup_enable((gpio_num_t)MOTION_WAKE_INT_PIN,
|
|
MOTION_WAKE_INT_ACTIVE_HIGH ? GPIO_INTR_HIGH_LEVEL : GPIO_INTR_LOW_LEVEL);
|
|
#endif
|
|
enableLoraInterrupt();
|
|
#ifdef PMU_IRQ
|
|
// wake due to PMU can happen repeatedly if there is no battery installed or the battery fills
|
|
if (pmu_found)
|
|
gpio_wakeup_enable((gpio_num_t)PMU_IRQ, GPIO_INTR_LOW_LEVEL); // pmu irq
|
|
#endif
|
|
|
|
auto res = esp_sleep_enable_gpio_wakeup();
|
|
if (res != ESP_OK) {
|
|
LOG_ERROR("esp_sleep_enable_gpio_wakeup result %d", res);
|
|
}
|
|
assert(res == ESP_OK);
|
|
res = esp_sleep_enable_timer_wakeup(sleepUsec);
|
|
if (res != ESP_OK) {
|
|
LOG_ERROR("esp_sleep_enable_timer_wakeup result %d", res);
|
|
}
|
|
assert(res == ESP_OK);
|
|
|
|
console->flush();
|
|
const bool loraIrqPending = loraWakeIrqAsserted() && loraIrqSkippedSleeps < LORA_IRQ_SKIP_SLEEP_MAX;
|
|
if (loraIrqPending) {
|
|
// A LoRa IRQ is already pending (see loraWakeIrqAsserted). Don't sleep on top of it - return so
|
|
// the main loop services the packet now instead of stranding it until the next wake.
|
|
if (++loraIrqSkippedSleeps >= LORA_IRQ_SKIP_SLEEP_WARN)
|
|
LOG_WARN("LoRa IRQ still pending after %u skipped sleeps", loraIrqSkippedSleeps);
|
|
else
|
|
LOG_INFO("LoRa IRQ pending, skip light sleep to service packet");
|
|
res = ESP_OK;
|
|
} else {
|
|
// Either nothing pending, or the line has stayed high past LORA_IRQ_SKIP_SLEEP_MAX and the main
|
|
// loop is clearly not going to clear it. Never sleeping costs more than one stranded packet.
|
|
loraIrqSkippedSleeps = 0;
|
|
res = esp_light_sleep_start();
|
|
}
|
|
if (res != ESP_OK) {
|
|
LOG_ERROR("esp_light_sleep_start result %d", res);
|
|
}
|
|
// commented out because it's not that crucial;
|
|
// if it sporadically happens the node will go into light sleep during the next round
|
|
// assert(res == ESP_OK);
|
|
#ifdef ROTARY_PRESS
|
|
gpio_wakeup_disable((gpio_num_t)ROTARY_PRESS);
|
|
#endif
|
|
#ifdef KB_INT
|
|
gpio_wakeup_disable((gpio_num_t)KB_INT);
|
|
#endif
|
|
#ifdef BOARD_PCA9535_INT
|
|
gpio_wakeup_disable((gpio_num_t)BOARD_PCA9535_INT);
|
|
#endif
|
|
#ifdef BUTTON_PIN
|
|
// Disable wake-on-button interrupt. Re-attach normal button-interrupts
|
|
gpio_wakeup_disable(pin);
|
|
#endif
|
|
#ifdef INPUTDRIVER_WAKE_BTN_PIN
|
|
gpio_wakeup_disable((gpio_num_t)INPUTDRIVER_WAKE_BTN_PIN);
|
|
#undef INPUTDRIVER_WAKE_BTN_PIN
|
|
#endif
|
|
#if defined(WAKE_ON_TOUCH)
|
|
gpio_wakeup_disable((gpio_num_t)SCREEN_TOUCH_INT);
|
|
#endif
|
|
#ifdef MOTION_WAKE_INT_PIN
|
|
// Unconditional: the config can have changed while we were asleep.
|
|
gpio_wakeup_disable((gpio_num_t)MOTION_WAKE_INT_PIN);
|
|
#endif
|
|
#if !defined(SOC_PM_SUPPORT_EXT_WAKEUP) && (defined(LORA_DIO1) || defined(SX128X_DIO1))
|
|
const int wokeDio1 = loraWakeDio1Pin(); // must match what enableLoraInterrupt() armed
|
|
if (wokeDio1 >= 0 && radioType != RF95_RADIO) {
|
|
gpio_wakeup_disable((gpio_num_t)wokeDio1);
|
|
}
|
|
#endif
|
|
#if defined(RF95_IRQ) && (RF95_IRQ != RADIOLIB_NC)
|
|
if (radioType == RF95_RADIO) {
|
|
gpio_wakeup_disable((gpio_num_t)RF95_IRQ);
|
|
}
|
|
#endif
|
|
|
|
// When we skipped the sleep because a LoRa IRQ was already pending, esp_sleep_get_wakeup_cause()
|
|
// would report the previous real sleep's cause (e.g. TIMER); synthesize a GPIO cause so the
|
|
// handling below (and PowerFSM) takes the packet-service path instead of a stale timer path.
|
|
esp_sleep_wakeup_cause_t cause = loraIrqPending ? ESP_SLEEP_WAKEUP_GPIO : esp_sleep_get_wakeup_cause();
|
|
notifyLightSleepEnd.notifyObservers(cause); // Button interrupts are reattached here
|
|
|
|
if (cause == ESP_SLEEP_WAKEUP_GPIO) {
|
|
LOG_INFO("Exit light sleep gpio");
|
|
// If we woke because of a GPIO, it's possible power needs to run to handle.
|
|
power->setIntervalFromNow(0);
|
|
runASAP = true;
|
|
} else {
|
|
LOG_INFO("Exit light sleep cause: %d", cause);
|
|
}
|
|
|
|
return cause;
|
|
}
|
|
|
|
// not legal on the stock android ESP build
|
|
|
|
/**
|
|
* enable modem sleep mode as needed and available. Should lower our CPU current draw to an average of about 20mA.
|
|
*
|
|
* per https://docs.espressif.com/projects/esp-idf/en/latest/api-reference/system/power_management.html
|
|
*
|
|
* supposedly according to https://github.com/espressif/arduino-esp32/issues/475 this is already done in arduino
|
|
*/
|
|
void enableModemSleep()
|
|
{
|
|
static esp_pm_config_t esp32_config; // filled with zeros because bss
|
|
#if CONFIG_IDF_TARGET_ESP32S3
|
|
esp32_config.max_freq_mhz = CONFIG_ESP32S3_DEFAULT_CPU_FREQ_MHZ;
|
|
#elif CONFIG_IDF_TARGET_ESP32S2
|
|
esp32_config.max_freq_mhz = CONFIG_ESP32S2_DEFAULT_CPU_FREQ_MHZ;
|
|
#elif CONFIG_IDF_TARGET_ESP32C6
|
|
esp32_config.max_freq_mhz = CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ;
|
|
#elif CONFIG_IDF_TARGET_ESP32C3
|
|
esp32_config.max_freq_mhz = CONFIG_ESP32C3_DEFAULT_CPU_FREQ_MHZ;
|
|
#elif CONFIG_IDF_TARGET_ESP32P4
|
|
#if CONFIG_ESP32P4_REV_MIN_FULL < 300
|
|
esp32_config.max_freq_mhz = 360;
|
|
#else
|
|
esp32_config.max_freq_mhz = 400;
|
|
#endif
|
|
#else
|
|
esp32_config.max_freq_mhz = CONFIG_ESP32_DEFAULT_CPU_FREQ_MHZ;
|
|
#endif
|
|
esp32_config.min_freq_mhz = 20; // 10Mhz is minimum recommended
|
|
esp32_config.light_sleep_enable = false;
|
|
int rv = esp_pm_configure(&esp32_config);
|
|
LOG_DEBUG("Sleep request result %x", rv);
|
|
}
|
|
|
|
bool shouldLoraWake(uint32_t msecToWake)
|
|
{
|
|
return msecToWake < portMAX_DELAY && (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER ||
|
|
config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE);
|
|
}
|
|
|
|
void enableLoraInterrupt()
|
|
{
|
|
#if defined(LORA_DIO1_EXTENDED_IO)
|
|
// DIO1 is a virtual pin on an I/O expander - it cannot be a GPIO wakeup source
|
|
#elif SOC_PM_SUPPORT_EXT_WAKEUP && (defined(LORA_DIO1) || defined(SX128X_DIO1))
|
|
const int loraDio1 = loraWakeDio1Pin();
|
|
esp_err_t res;
|
|
if (loraDio1 >= 0) {
|
|
res = gpio_pulldown_en((gpio_num_t)loraDio1);
|
|
if (res != ESP_OK) {
|
|
LOG_ERROR("gpio_pulldown_en(lora dio1) result %d", res);
|
|
}
|
|
}
|
|
#if defined(LORA_RESET) && (LORA_RESET != RADIOLIB_NC)
|
|
res = gpio_pullup_en((gpio_num_t)LORA_RESET);
|
|
if (res != ESP_OK) {
|
|
LOG_ERROR("gpio_pullup_en(LORA_RESET) result %d", res);
|
|
}
|
|
#endif
|
|
#if defined(LORA_CS) && (LORA_CS != RADIOLIB_NC) && !defined(ELECROW_PANEL)
|
|
gpio_pullup_en((gpio_num_t)LORA_CS);
|
|
#endif
|
|
|
|
#if HAS_LORA_FEM
|
|
loraFEMInterface.setRxModeEnableWhenMCUSleep();
|
|
#endif
|
|
|
|
if (loraDio1 >= 0) {
|
|
LOG_INFO("Wake on LoRa DIO1 (GPIO%02d) gpio interrupt", loraDio1);
|
|
gpio_wakeup_enable((gpio_num_t)loraDio1, GPIO_INTR_HIGH_LEVEL);
|
|
}
|
|
|
|
#elif defined(LORA_DIO1) || defined(SX128X_DIO1)
|
|
const int loraDio1 = loraWakeDio1Pin();
|
|
if (loraDio1 >= 0 && radioType != RF95_RADIO) {
|
|
LOG_INFO("Wake on LoRa DIO1 (GPIO%02d) gpio interrupt", loraDio1);
|
|
gpio_wakeup_enable((gpio_num_t)loraDio1, GPIO_INTR_HIGH_LEVEL); // SX126x/SX128x interrupt, active high
|
|
}
|
|
#endif
|
|
#if defined(RF95_IRQ) && (RF95_IRQ != RADIOLIB_NC)
|
|
if (radioType == RF95_RADIO) {
|
|
LOG_INFO("Wake on RF95_IRQ (GPIO%02d) gpio interrupt", RF95_IRQ);
|
|
gpio_wakeup_enable((gpio_num_t)RF95_IRQ, GPIO_INTR_HIGH_LEVEL); // RF95 interrupt, active high
|
|
}
|
|
#endif
|
|
}
|
|
#endif
|