module_* may depend only on ESP-IDF drivers (I2C, UART, GPIO, etc.),
never on system_*
task_* may depend on system_types, system_queues, system_state,
system_faults and module_*
API_* may depend on task-facing command queues and system_* but never on hardware
Design rationale
The goal is clean, stable firmware, especially by enforcing one service per critical hardware block.
This guarantees zero concurrent access to the same peripheral, even in unexpected edge cases, which is a major stability gain.
This point is absolutely central for a system designed to run unattended for five years, or close to it,
such as a remote LoRa repeater deployed in the field.
the real-world lifetime of alkaline D cells outdoors (You’re right: this is likely the main structural limitation of the current design), and
PCB protection in case a cell leaks.
I About alkaline D-cell leakage
Leakage can occur for two main reasons:
1. Deep discharge inside a series pack
At end of life, in any series string, the weakest cell will eventually be forced into reverse polarity if current keeps flowing.
This pushes the chemistry outside its intended operating region, causing:
gas generation,
internal overpressure,
seal rupture,
→ leading to electrolyte leakage.
In my design, I continuously monitor pack voltage and put the device into a almost full shutdown if the voltage drops too low.
The whole point is to avoid that deep-discharge region which dramatically increases leakage risk.
2. Mechanical/assembly defects in cheap cells
This is the downside of the “500 Wh for €13” deal.
At that price point, you don’t get telecom-grade cells with industrial QC.
It’s a compromise.
As it stands, this is a structural limitation of the LoRaTube concept.
II Protecting the PCB/electronics in case of leakage
The mechanical design answers most concerns:
the PCB is above,
the battery pack is below,
and the two volumes are physically separated by:
the 50 mm “coffee-cup” internal support, screwed into the 40 mm tube,
plus the piece that provides the positive terminal contact for the last cell.
The electronics compartment and the battery compartment are mechanically isolated.
If a cell leaks, the electrolyte cannot reach the PCB.
And if the cells become impossible to remove, the entire tube section can simply be discarded (a 2-meter PVC tube costs around €2).
III/ Toward a more resilient battery architecture
The comments are valid: if we’re aiming for extreme robustness, having the whole system fail because of one leaking cell or one failed cell is not ideal.
I’m considering a new architecture:
instead of one long 18S1 string,
use three parallel packs of 7 cells in series each → 7SP3 configuration.
This gives an input voltage between ~7 V and ~12 V depending on the state of charge.
The buck converter runs more efficiently and with lower losses in that range.
The obvious risk with parallel packs is cross-charging, which would destroy the weaker packs quickly.
Low-tech fix: Schottky diodes (passive ORing)
The simplest and most reliable solution is to place a Schottky diode in series with each 7-cell string:
each 7S pack has its own diode,
the three diodes meet at the input rail of the buck converter.
Expected behavior
Example:
Pack A: 10.5 V
Pack B: 10.2 V
Pack C: 10.0 V
Schottky drop ≈ 0.3 V
Effective voltages seen at the bus:
A → ~10.2 V
B → ~9.9 V
C → ~9.7 V
Result:
Only Pack A supplies current initially (it has the highest voltage).
As it discharges, its voltage drops.
Once it falls below Pack B (diode-included), Pack B automatically takes over.
Then Pack C, etc.
This gives a very primitive form of current balancing, with zero active electronics.
Benefits
The system keeps running even if one pack fails, or even two packs.
Leakage or a broken contact in one 7-cell string no longer kills the entire node.
You move from a single point of failure (18S1)
to a system with graceful degradation (7S3 + diodes).
This matches the project philosophy:
long lifetime outdoors with simple, low-cost, repairable technology.
We could even consider 3, 4 or 5 parallel packs depending on:
I will complete the log later with the elements I was able to collect from the article on hackaday.com about the LoRaTube. There were a lot of comments about the long-term behavior of D cells. You’re right, I do have a modification to address this issue.
In the meantime, here is, through this diagram, the answer to one of the issues that was raised: there is indeed a clear separation between the electronics (which are in any case above the batteries) and the batteries, via the part that holds the 50 mm tube and looks like a coffee cup.
V4 is good enough to start firmware work !
This log takes stock of the bring-up: what works, what still blocks, and what will be fixed in V5.
Power, Supercap and Precharge
On the power front, the foundation is solid: the 5V comes out as expected, and the nominally 3.3V rail is currently about 3.0V. This is actually good for consumption, but offers less brownout margin. Still, in the current configuration everything starts and runs fine. To recover a true 3.3V, for V5 just set R45 to about 267kΩ.
The 5V supercap performs exactly as intended: at 33dBm TX, the buck handles E22 bursts without flinching. On a generous burst of 1000 characters, cap voltage only drops from 4.91V to 4.79V, showing that the buck supplies its max 500mA and the supercap delivers the rest (about 700mA at 33dBm TX).
However, an important point was confirmed for power-up sequencing: if you try to start with the supercap at 0V, the buck sees a near-short, goes into protection and collapses. By precharging the supercap to ~4V before enabling the buck, everything latches instantly and the system stabilizes. V5 will have to integrate this behavior in the power-up strategy (not sure yet how).
How I Measure Power Consumption: Reading the Supercap
To quantify power consumption, especially in ECO mode, the only truly reliable method is to watch the slope of the supercap voltage over time.
The buck does not draw a steady current: it operates in recharge bursts spaced by a few minutes. Between bursts, current seen at the battery pack drops to about 3.4µA (buck off). In that phase, the supercap supplies the 5V rail.
I ≈ C × ΔV / Δt
with C ≈ 20F for the supercap.
E22 NORMAL mode + ESP32-C3 on:
Measurements: 5.10V at t=0s and 4.91V after 240s. The 0.19V drop over 240s corresponds to an average current of: I ≈ 20 × 0.19 / 240 ≈ 15.8 mA
Which is in the 16–18 mA range, consistent with what is expected for TX/active mode.
E22 WOR mode + ESP32-C3 on:
Same method, 5.06V down to 4.992V after 120s, i.e. 0.068V drop. I ≈ 20 × 0.068 / 120 ≈ 11.3 mA
Which matches the announced ~11 mA.
WOR + ESP32-C3 deep sleep:
Measures go from 4.915V to 4.907V over 330s (8mV drop): I ≈ 20 × 0.008 / 330 ≈ 0.000485 A ≈ 485µA
So about 480µA in “deep idle” (C3 deep sleep, E22 in WOR, RTC & FRAM powered). Theoretically, with no TX, this gives several decades of autonomy on a pack of 19 LR20s at 18Ah (513Wh). In reality, the cells will die of old age before they run out. Exactly the goal: hardware is no longer the limiting factor.
From the Pack’s Point of View in PWM Mode (no ECO)
With the buck in classic PWM mode (ECO disabled), pack-side measurements confirm the orders of magnitude but the buck’s quiescent current in PWM mode is very high.
With C3, RTC, FRAM and E22 in RX: current is about 9.44mA @ 25V.
Switching E22 to WOR drops it to ~7.62mA.
By difference, E22 in RX thus consumes about 1.82mA @ 25V, i.e. about 9mA @ 5V : matching the module datasheet and the current calculated from the supercap voltage drop.
The C3 itself, active but idle, is around 2.82mA @ 25V, i.e. ~22mA @ 3.3V (about 70mW). No surprise here either.
Power Rail Noise: PWM vs ECO
I re-did noise measurements with a T3100 probe and a proper ground spring. This completely changes the result from my previous bring-up, where the probe ground was hacked and grossly overestimated the noise (especially in TX, where I picked up noise from the LoRa module’s RF emission).
In WOR mode, buck in ECO, noise on the 5V rail is between 0.8 and 1.6mV peak-to-peak. This is excellent. At 33dBm TX, still in ECO, it climbs to about 6.4mV p-p, with occasional peaks around 7.2mV. Switch the buck to PWM, and you get up to about 8mV p-p. Paradoxically, it's a bit noisier (juste a little bit).
But it’s still very, very good. In all cases, the measured spectrum stays clean, flat, no significant spurs. Conclusion: dynamic buck mode control via PCA9536 expander brings nothing for noise. ECO mode will be the nominal config for consumption, and software control of the buck mode will likely be dropped in V5.
ESP32-C3, I²C Bus, RTC, Expander and UART
On the digital side, everything is OK.
The ESP32-C3 boots and flashes without trouble, logs work, test program hits all the essential hardware blocks and behaves as intended.
The I²C scanner detects RTC, FRAM and PCA9536 expander correctly. Communication is stable, even over time.
On the RTC, register read and write works, CLKOUT is cleanly disabled, and the SQW/INT line is now well-managed: put in Hi-Z when needed, no weird levels or parasitic leakage like during V3 bring-up.
The PCA9536 initializes with no surprises; outputs GP0–GP3 drive E22 power, both LEDs, and buck 5V mode. For V4, this last point is useful for ECO mode characterization, but the final firmware will probably just use a hardware strap.
Radio side: UART1 at 9600 baud works after fixing a missing solder on RX (I'd forgotten to solder the center pin of the E22’s female header). C1 00 07 command returns a header and seven coherent parameter bytes, which validates the C3 ↔ E22 link. TX power register writes work, and a full-power TX test with 1000-char bursts, 2 seconds apart, passes with no issue.
VSENSE: Measuring and Calibrating Pack Voltage
The pack voltage measurement uses a divider (10 MΩ / 680 kΩ) behind an input diode (about 0.5V drop).
I swept the pack voltage from ~13V to ~31V and logged for each plateau: average of raw ADC readings (moyenne_raw), max-min spread (ecart_raw), and actual voltage via DMM. The ADC has a noise of a few LSB (which increases with pack voltage), but the overall trend is very clean despite the high divider impedance, probably because measurement is slow.
On the plot: mean raw value (X), pack voltage (Y), noise shown as horizontal error bars (half-width xerr = ecart_raw / 2). Points line up well, justifying a polynomial fit.
In Python, a simple 2nd order numpy.polyfit gives:
where K is the raw ADC value. This law matches closely what I had estimated “by hand”; numerical fit confirms and refines the digits.
In embedded C, it will be:
double adc_to_voltage(double K) {
return -1.604208e-7 * K * K
+ 1.191807e-2 * K
+ 6.024925e-1;
}
The final firmware will use this law directly to convert ADC readings to pack voltage.
Mechanical and Thermal Integration
Mechanically everything is OK. The PCB fits cleanly into the PVC tube with the 3D-printed endcaps, all in line with the LoRaTube concept. Everything is secure and seems well-arranged.
Thermally: Continuous TX tests (33dBm, 1000-char frames, 2s pause, several minutes) show the 5V buck just gets warm. The PCB under the converter heats up by a few degrees, barely perceptible to the touch. No hot spots; dissipation appears sufficient for long-term use.
What Doesn't Work (Yet) & What I’ll Change in V5
TPL5010: Logic Fine, Integration Needs Rework
The TPL5010 behaves as expected: it generates periodic resets, and the frequency matches the delay resistor value. The problem is the chosen configuration. With the current R4 (jumper-selectable: can set ~1h or ~900ms), we get about a 900ms period, which is too short to boot the system, initialize all peripherals, and perform a realistic work cycle between resets.
Another issue: to force a new R4 value, I currently have to short the 3.3V buck output so it reboots and reads the new delay. Not acceptable. In V5, R4 will be raised to ~22kΩ for a ~1 minute period, and TPL5010 power will be behind a jumper or switch for easy disable during development — no more violence to the 3.3V rail.
2A Fuse: Too Optimistic
The 2A input fuse turned out to be too optimistic. It blows during initial supercap charge, even with reasonable current limiting, simply because inrush and recharge peaks exceed what it can handle. In steady-state, we’re well under 1A (e.g., TX tests show ~110mA @ 25V at the pack, or 500mA from the buck @ 5V, leaving 700–800mA to supercap charging), but what matters here is the “precharge” peak. That’s likely when it fails. Once shorted, everything works but there’s no protection.
For V5, I’ll use a time-lag (slow-blow) 5×20mm, T 3.15A, 250V fuse. This gives a comfortable margin for average current, handles supercap and TX burst surges, and still protects against real downstream shorts.
M0 Control on E22
M0 control via an analog switch was a bad idea, plus wiring was botched. The goals were: force default WOR mode at power-up, avoid any backfeed to the E22 MCU via that pin during hard resets (controllable by MCU via the expander’s output 0), and allow clean resets by cutting 5V to the module. In practice, none of these goals are met.
For bring-up, it works “well enough”: C3 can drive M0 and talk to the module, but pull-up isn’t guaranteed. For V5, I’ll simply drop this analog switch and go with something simpler: fixed pull-up to 5V for default WOR, maybe a diode or MOSFET to isolate 5V from the MCU pin driving M0.
Voltage Supervisor: Reference Mistake, Plain and Simple
Here, a pure blunder. I chose a TPS3703B5180 as voltage supervisor, noting I’d need to change its reference for ~2.5V threshold. And forgot to do it!
The “B5180” monitors around 1.8V, with a UV/OV logic that makes no sense for a 3.3V rail. Result: it “sees” imaginary overvoltages, tries to protect against a non-existent problem, and interferes at startup causing reset/reboot loops. On this rail, the right choice would have been a TPS3703E5250: E version, single undervoltage threshold at 2.5V, aligned with a true 3.3V operating limit. Luckily I’d added a jumper to kill its action on RESETn, which limits V4’s trouble.
Anyway, it brings insufficient benefit to justify another BOM item. For V5, I won’t just fix the reference: I’ll delete the supervisor. The C3’s software brownout detector is sufficient.
V5: Corrections Roadmap
Restore the 3.3V rail by adjusting R45 to ~267kΩ (?).
Properly size the supercap fuse (time-lag, 3.15A should do).
Simplify M0 pull-up for E22.
Hard-strap the buck 5V in ECO mode, no dynamic expander control (or keep, it doesn't hurt).
The rest of the hardware (power chain, C3, I²C, RTC, FRAM, expander, VSENSE, UART to E22) is validated and will serve as the base for the final firmware.
Test Program and Next Steps
The supplied software is a bring-up code. It scans I²C at boot, configures RTC (CLKOUT off, IRQ management), initializes PCA9536 and blinks the LEDs, powers E22 and 5V buck, does a radio smoke test (CONFIG mode, register reads, power setup, switch to WOR), continually tracks pack voltage via VSENSE, logs C3 internal temperature, and can (optionally) run continuous TX via an e22_burst_tx_task.
It’s deliberately rough, just to validate hardware as fast as possible!
But the final firmware will be clean and structured: FreeRTOS, hardware (with DONE, WAKE of the TPL5010) and software watchdog, separated modules, clear state machine, well-controlled wake/sleep sequences... ;)
Conclusion
V4 is validated as the hardware base for firmware implementation. All essential functions are there, consumption numbers match the initial goals, power noise is very low, thermal is managed, tube integration is clean.
Some design errors remain : notably the too-nervous watchdog, fuse sizing, and voltage supervisor to be removed, jumper place around TPL5010, but they’re easy fixes for V5.
I received the assembled V4 PCB today, along with five unassembled boards. Many thanks to PCBWay. I’m short on time and have a lot to catch up on right now, but I’ll get to the bring-up soon.
After four months outside and the first cold spells, here’s a status update on the mechanical side.
I dropped the LoRaTube and broke the part at the end of the tube (the piece that’s glued to the PVC pipe).
In addition, the end piece of the assembly, the one bearing the weight of the whole battery stack and the contact pressure from the springs, had been printed too “light” and had slowly deformed under load. I redesigned it as a beefier part, matching the diameter of the new glued sleeves, and printed it with higher density (40% infill + gyroid pattern).
New Design
In hindsight, I had printed it too thin and not dense enough. I reworked the cap as well as the glued end-piece of the PVC tube, increasing the wall thicknesses and printing at 40% infill with a gyroid pattern.
I should also mention that I changed printers, moving from my old Ender 3 V2 to a Bambu Lab machine (huge upgrade). The part, even though it is still PLA, comes out noticeably denser. Under-extrusion on the Ender? Slicer differences? Whatever the cause, the whole assembly feels much more solid now.
Here is the new end piece of the assembly, which both closes the tube and holds the batteries in place. Here are the new end caps for the PVC tube.
And here is the new mounting bracket that holds the 50 mm PVC tube (the tube that carries the electronics).
And the end part of the tube :
I also changed the part that holds the 50 mm tube. It’s thicker, I increased its diameter, and I printed it with 40% gyroid infill as well. It’s the green part in the photos below.
Resilience
In any case, it’s by iterating that I’ll converge towards a system that’s robust in all conditions (Soyuz technique!).
The length of the 40 mm-diameter PVC tube to cut is 115.8 mm. By cutting it to the correct length, I was able to remove the crescent-shaped spacer I had added earlier.
In the last photo, you can see the device responsible for making contact with the positive terminal of the last cell.
It consists of two parts that can move relative to each other. Contact pressure is provided by three compression springs. The travel is about 15 mm. The system looks robust and works well so far…
I’ll leave the device assembled over the coming months. I’ll only bring it back into the lab when I receive the V4 PCB, if it looks OK for an outdoor test.
Here are a few photos I received this morning of the fully assembled PCB, manufactured by PCBWay.
This is Revision V4.
The build quality is excellent ; clean soldering, crisp edges, and a layout that finally looks exactly as intended.
Once the board is in my hands, I will publish a detailed bring-up report for this V4 revision as for the V3 revision: power-on tests, rail validation, firmware loading, radio checks, and early measurements.
The Gerbers and BOM have been sent to production : the V4 is now in production.
This revision marks, I hope, the closure of the hardware development phase and the beginning of firmware optimization.
Next phase:
bring-up of the V4 in few weeks
firmware (il everything is OK) : Focus will shift to thefinite-state control, FRAM ring buffer, and the orchestration of wake/sleep cycles with deterministic timing and energy budgets.
Many thanks again to PCBWay for their continuous support in this next iteration.
Logs are critical for monitoring LoRaTube, confirming real-world performance over time, and, if necessary, identifying failures or design flaws.
This document records the reasoning and choices that led to the selected memory technology. The aim is to ensure reliable long-term logging with minimal power consumption and maximum resilience.
SD cards, though common in consumer projects, were ruled out. Their fragility is well known: rapid cell wear, poor reliability over long periods without activity, unpredictable wear-leveling, and a tendency to become corrupted or unwritable after power loss. Random corruption, freezes, access slowness, and inrush current are all deal-breakers for an ultra-low-power, resilient system.
Serial EEPROMs were also considered. They are inexpensive and widely available. However, their endurance is limited (typically one million write cycles per cell, much less in harsh temperature conditions), and write operations are page-based, which is not well-suited for the intended logging patterns. Write times can be significant, increasing the risk of data corruption during resets or brownouts. Data retention is often not guaranteed beyond a few years, especially with wide temperature variations.
SPI Flash offers large storage but shares many drawbacks: limited endurance, mandatory page erase before writing, complex software wear-leveling, and a real risk of “bricking” the chip if power is lost during write operations. Frequent updates to a few bytes always force a compromise between wear and software complexity. Additionally, available pin count on the C3 is limited, with no other active SPI devices in the design.
FRAM (Ferroelectric RAM) emerged as the solution. Key features:
Very fast access (read/write in a few microseconds)
No write latency
Virtually unlimited endurance (ideal for frequently updated counters)
Very low operating power
FRAM requires no pre-erasure or software wear-leveling, never blocks on sudden power loss, and provides full-speed access regardless of the frequency or distribution of writes. Data can be written or read byte by byte. It is also known for its resistance to EMI, and data retention exceeds ten years, making it well suited for embedded data loggers requiring high reliability.
There are two minor trade-offs: the chosen device (one of the few available on Aliexpress) draws about 9 µA in idle. While this is low, it is not entirely negligible over several years.
The FRAM is mounted on a large breakout board and is installed via female headers, allowing easy extraction for content analysis without soldering.
Daily Log Format (16 bytes, little-endian)
Available storage is 32 kB, so a daily log format with counters was selected. Counters are stored directly in FRAM to take advantage of its resilience to repeated writes and absence of paging.
Assuming a five-year lifespan:
32 kB / (5 years × 365 days/year) ≈ 17 bytes per day.
The chosen log format is 16 bytes per day:
Field
Bytes
Description
timestamp
4
Unix UTC (seconds)
midnight_temp
1
Temperature at midnight (−15…+35 °C normal, −50…+70 °C if TEMP_EXT=1)
Calculation is performed at log write time (integer division, clamped to 255).
This format is compact but contains all relevant information: battery voltage trends, effect of temperature, presence of electromagnetic noise, and radio efficiency (repeater performance).
A CRC8 allows verification of log integrity (1/256 chance for a single undetected error per 16-byte log).
Flags (1 byte)
Flags in byte 13 provide information about the system status and faults:
Bit
Name
Function
0
TEMP_EXT
Extended temperature range active (modifies min/noon temp range)
1
E22_RESET
Radio module reset requested
2
C3_MISSED_TIME
MCU reset detected (time/logs mismatch)
3
LOW_VOLT
Low voltage detected
4
WAKE_UART
Wake by UART (should stay zero in normal use; monitors AUX and IRQ)
5
CORRUPTED_FRAM
Invalid CRC on index or counter slot
6
C3_RESET
3.3 V rail reset requested by firmware
7
C3_BROWNOUT
Brownout detected on C3
Circular Buffer Logic
FRAM is used as a circular buffer:
Each write increments the index.
When index reaches maximum, it wraps to zero.
Each log has its own timestamp for strict chronological ordering.
No magic pointers or implicit dependencies—structure is fully autonomous.
Index and Counter Storage
Log index and daily counters (for RX, system status, etc.) are also stored in FRAM. RTC RAM of the C3 is not used for resilience reasons.
Data is duplicated in two slots (A and B). Slot A is at the start of FRAM, slot B at the end. Both hold identical data (except during update).
Each slot includes a magic word, payload, and CRC8 (poly 0x31, reflect in/out; same as for the log).
Purpose of two distant slots:
Protection against localized corruption: single-bit errors or interrupted writes may damage one slot, but simultaneous corruption is extremely unlikely.
Protection against external factors: EMI, ionizing radiation, and cosmic rays can cause isolated bit flips; distant slots are less likely to be affected simultaneously.
This structure enables error detection and correction at startup, with no need for an internal journal or sequencer—CRC and magic word are sufficient.
Write Policy
Always write A, then B, with identical values.
Write A.
Verify CRC and readback.
Retry up to n times if failed.
Write B.
Verify CRC and readback.
Retry up to n times if failed.
At boot:
If both A and B are valid: use A, do not set CORRUPTED_FRAM.
If only one is valid: use it and set CORRUPTED_FRAM.
If neither is valid: reset all to zero and set CORRUPTED_FRAM.
FRAM is extremely robust; irreversible hardware failure rate is about 10 FIT (failures per billion hours), corresponding to one failure every 11,000 years per device.