
4dapter
timville85/4dapter
This project combines the NES, SNES, N64 and Genesis DaemonBite Retro Controllers projects together with a custom PCB to support 4 different controllers with a single socketed Arduino Pro Micro.

ESP32 + LoRa radio powered reed switch door sensor
Loragate tht is an open-source printed circuit board by andrewgho, from the andrewgho/loragate repository on GitHub. It is a 2-layer board of 30 × 58.5 mm with 10 parts on 20 nets, routed with 60 tracks and 0 vias.
It is built around the MCP1700-3302E/TO and MF-R050. Its parts include 3 ICs, 3 connectors, 2 capacitors, 1 resistor and 1 fuse. It belongs with the radio and wireless and sensors and measurement boards and the ESP32 and RISC-V boards of this library.
A battery-powered door/gate sensor that reports open, close, and "still open" events over LoRa to an always-on receiver, which forwards them to a WiFi-connected monitoring server.
tx/ — the outdoor transmitter: XIAO ESP32-C6 + Wio-SX1262,
reed switch, 3x AA lithium cells. Deep-sleeps between events.rx/ — the indoor receiver: same radio hardware, USB-powered,
always on, WiFi-connected. Acks tx, forwards events to a
monitoring server over HTTP, and lights a local held-open beacon.test/ — host-side tests (no ESP32 toolchain needed) for the
parts of tx/rx written to be testable without hardware: the wire
format, tx's wake-decision logic, and rx's retry-dedup logic.LoRa, not WiFi or cellular, for the transmitter. WiFi's join/auth cost alone is worse than LoRa's entire transmit burst, and there's no cellular plan to pay for recurring. LoRa trades bandwidth (a few bytes a message) for exactly what this needs: long range at near-zero duty cycle, license-free. The receiver is WiFi-connected, but it's mains-powered — it absorbs that cost so the battery-powered side doesn't have to.
XIAO ESP32-C6, deep-sleep by default, event-driven wake. Polling
the reed switch on a timer would mean staying awake far more than the
door actually changes state. Instead, tx deep-sleeps and wakes only
on a reed-switch edge or a backstop RTC timer (see "Software" below) —
the C6's RTC domain is what makes both of those possible as a true
low-power wake source, and RTC_NOINIT_ATTR memory is what lets a few
bytes of state (sequence counter, last-reported door state) survive
both sleep and an unexpected restart without re-deriving them from
scratch every wake.
Primary AA lithium (Energizer L91), not rechargeable Li-ion, through
an LDO. A sealed outdoor enclosure in direct sun runs hotter than
ambient, and that's exactly where Li-ion/LiPo capacity fade and
pouch-cell failure modes get worse — the alarm industry ships
door/window sensors on primary lithium for the same reason. The
XIAO's 3V3 pin is wired straight to the chip's core supply with no
protection, and a fresh pack can sit close to the chip's own voltage
ceiling with no margin — an LDO (plus a PTC fuse ahead of it) fixes
both the regulation and the short-circuit exposure in one part. See
WIRING.md's Transmitter "Power" for the specific part and the alternative
considered.
Simple ack/retry over the radio link, not a heavier protocol. Each
event is one small packet (type + sequence number + retry count);
tx retries with backoff until rx acks it or it gives up. That's
enough reliability for "a handful of events a day, on a link that's
usually clean," without the complexity of a synchronized/meshed
protocol this traffic pattern doesn't need.
The wake/sleep decision logic is pure C++, separate from the
Arduino/radio code. tx/door_wake_logic.h decides what a wake
means and should do with no Arduino, radio, or Serial dependency;
tx/tx.ino is just the I/O shell that reads the pin and carries out
the decision. That split is what lets test/test_tx_wake_logic.cpp
exercise the actual decision rules — including regression cases for
real bugs caught along the way (see "History" below) — with a plain
C++ compiler, no hardware required.
Deep-sleep current, measured live on the built transmitter with a Nordic PPK2 (see "History" below for how the first measurement — 1.5mA, not microamps — got root-caused):
| Source | Current |
|---|---|
| Deep sleep, door closed | 21.9µA |
| Deep sleep, door open | 18.5µA |
| Active transmit (~120ms burst, +22dBm) | ~125mA |
Budgeting conservatively for ~54 radio events/day (20 door transitions + an hourly heartbeat floor + occasional held-open checks — see "Software" below) against ~2,100mAh realistically extractable from a 3x AA L91 pack:
sleep: 0.0219mA * 24h ≈ 0.526 mAh/day
active: 54 * 0.120s * 125mA / 3600 ≈ 0.225 mAh/day
total ≈ 0.751 mAh/day
2,100 mAh / (0.751 mAh/day * 365) ≈ 7.7 yearsComfortably past the "2+ years would be satisfying" bar, with enough margin to absorb temperature, pack-tolerance, and retry-rate uncertainty the measurement doesn't cover. Not modeled: a worst case of constant ack-retries would cost more than the single-burst-per-event assumption above — live field logs show this is rare in practice, but it's not measured.
3V3 connection with no
margin against the chip's voltage ceiling. Both forced real design
changes — see WIRING.md's Transmitter pin table and LDO section.gpio_hold_en()); and
the Wio-SX1262 HAT's own onboard button shares the reed switch's
D0 net with a pull-up that drew current whenever the door was
closed (confirmed against Seeed's schematic, fixed by bypassing D0
around the HAT in hardware — see WIRING.md). Final measured number
is the 21.9µA/18.5µA above.while (!Serial) wait-for-USB-monitor loop in tx's setup()
burned up to 3 seconds of fully-awake CPU time on every single wake
in the field — a real battery-life cost, not just a latency
annoyance — because deep sleep drops the USB peripheral and nothing
was ever there to satisfy the wait. Fixed by gating it behind
-DDEBUG.Both nodes use the same radio hardware — a Seeed XIAO ESP32-C6 paired with a Seeed Wio-SX1262 (the "for XIAO ESP32S3" through-hole-header variant; Seeed doesn't sell this exact pairing, so the pinout is confirmed against the physical boards, not a datasheet — see WIRING.md's "Shared radio stack"), each with the small ribbon antenna that ships with the Wio-SX1262. What differs is what's attached, how each is powered, and what the modules mount on.
This section covers what the pieces are and why; WIRING.md has the full pin tables, part numbers/links, and physical build/wiring instructions, in matching Transmitter/Receiver sections — that's the doc to open to actually order parts or wire up a unit.
Reed switch on D0 (the only free deep-sleep-wake-capable pin), 3x AA
lithium pack through an LDO and a PTC fuse, on a custom KiCad PCB
(kicad/) in an IP65 enclosure with the antenna inside. No USB power
in the field. The PCB doesn't stack the modules as a HAT: it seats them
side by side, with the Wio flipped on its back for easier routing. That
lets it leave the Wio's D0 unconnected, which isolates the reed
switch from the HAT's onboard button without modifying either module.
A UART header on D6/D7 takes an FTDI adapter for production-build
serial monitoring. A 3D-printed mount (mount/) holds the PCB and
battery holder in the enclosure. (The original Perma-Proto protoboard
build is still documented in WIRING.md.)
USB-powered, indoors, no battery or enclosure concerns — it just needs
to stay on and stay on WiFi. The usual XIAO + Wio HAT stack plugs into
a 1"x1" protoboard wired as a dual 7-pin socket, which also carries
the one peripheral: a small 5V LED rotating beacon, lit while the gate
is held open, switched from D0 by a low-side logic-level MOSFET.
RADIOLIB_ERR_CHIP_NOT_FOUND).D0 net (see "Power budget" → "History" above) — a
hardware quirk of the stacked-header build, not something visible
from either board's datasheet alone. The protoboard build fixed it by
snipping D0 out of the stack; the PCB avoids it by design.D6, reasoning that rx's
Serial runs over native USB so UART0's pins were free. The gate sat
at ~3V whatever the firmware wrote. Root cause, confirmed against the
installed core's sdkconfig rather than a web search: the prebuilt
ESP-IDF has its console on UART0 regardless of CDCOnBoot (an SDK
configuration choice, not a silicon limit, as an earlier draft
wrongly claimed). Moved to D0. D7 is ruled out for the same
reason.D0, the gate read a steady ~1.6V and the beacon
stayed off. That's almost exactly the divider voltage for an
undriven D0 (the HAT's 10kΩ pull-up against the beacon's
220Ω+10kΩ pulldown). The cause was the macOS flashing box's stale
checkout, still flashing the D6 build. A pull and reflash fixed it,
with no hardware change. Check git pull before chasing an
electrical-looking symptom.D0 island instead of a
GND one, which would have tied D0 straight to the MOSFET source.
It was caught in review, before soldering, by tracing each hole
against the header assignment rather than trusting the layout's
prose labels. The as-built board instead puts the circuit on the
protoboard that sockets the stack, with a 2-pin output header.tx/)Deep sleeps by default. Each wake runs setup() once (there's no
persistent loop()), decides what — if anything — to send, and goes
back to sleep from inside that decision:
RTC_NOINIT_ATTR memory, validated
by a checksum (tx/rtc_checksum.h) since that memory section isn't
zeroed by the bootloader the way RTC_DATA_ATTR would be.radio.begin() happens to fail on that particular
wake.tx/door_wake_logic.h —
see "Why these choices" above for why that's a separate, pure file.rx/)Always on, always listening:
tx stops retrying) and dedups by
sequence number before acting on it, since an ack-loss retry resends
the same event.rx/local.h — see "Build, test, and flash").RX_HOSTNAME (DHCP hostname, so the router shows it
and serves local DNS for it) and advertises it over mDNS as
<name>.local.beacon_should_be_on(). For now
it's lit whenever the door's open (testing phase); the intended final
behavior, lit only after a held-open alert fires, is a one-line
change there.One packet type covers acks (PKT_ACK); the rest cover transmitter
events (PKT_HEARTBEAT, PKT_DOOR_OPEN, PKT_DOOR_CLOSE,
PKT_DOOR_HELD_OPEN, PKT_DIAGNOSTIC) — see lora_packet.h (kept
byte-identical between tx/ and rx/, checked by make test) for
the exact struct. tx sends, waits for a matching ack, and retries
with backoff (capped attempts) before giving up on that one event.
Toolchain: arduino-cli with
the ESP32 board package, plus the RadioLib library:
arduino-cli lib install RadioLibFrom the repo root:
make compile — compiles tx/ and rx/ against the installed
ESP32 core + RadioLib. No upload, no hardware needed.make test — host-side tests (test/), no ESP32 toolchain needed
at all, just a C++ compiler. Covers the wire format and the pure
logic described above; does not reach actual hardware behavior
(real deep sleep, a real reed-switch edge, radio range, WiFi/HTTP) —
only flashing a real board confirms those.make flash-tx / make flash-rx — flash tx/rx.Each sketch directory also has its own Makefile (make flash,
make debug, make monitor, make clean). make debug substitutes
a delay()+restart loop for deep sleep so USB/serial stays up for
monitoring, and builds with -DDEBUG (enables verbose logging and,
on tx, the wait-for-monitor loop that production builds skip — see
"History" below).
rx/ needs rx/local.h (gitignored) for WiFi credentials and the
HTTP URL templates it forwards events to — copy rx/local.h.example
and fill it in. tx/ needs no local.h (no WiFi).
esp_sleep_enable_ext0_wakeup(); the ESP32-C6 doesn't implement
ext0 at all. esp_sleep_enable_ext1_wakeup() (a GPIO bitmask, not a
pin number) replaced it.RTC_DATA_ATTR
is only documented to survive deep sleep, not ESP.restart()
(the rapid-slam/bounce guard's own restart) — switching to
RTC_NOINIT_ATTR plus a checksum fixed it.radio.begin() ever failed on the very first wake after a true
power-on, a later successful wake could silently adopt whatever the
door happened to be doing then as the starting baseline — losing
any real transition that happened in between with no event ever
sent. Fixed by establishing the baseline from the pin alone, before
the radio is even touched.localtime= field were added, then removed:
the actual timing gap they were meant to close (radio-level latency)
is usually under a second and gets swamped by the monitoring
channel's own latency anyway — not worth the added wire-format and
firmware complexity.| Qty | Value | Footprint | References |
|---|---|---|---|
| 2 | 1uF | Capacitor_THT:C_Disc_D5.0mm_W2.5mm_P5.00mm | C1, C2 |
| 1 | 1M | Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P10.16mm_Horizontal | R1 |
| 1 | BATT | Connector_Molex:Molex_KK-254_AE-6410-02A_1x02_P2.54mm_Vertical | J1 |
| 1 | MCP1700-3302E/TO | Package_TO_SOT_THT:TO-92_Inline_Wide | U1 |
| 1 | MF-R050 | Capacitor_THT:C_Disc_D8.0mm_W2.5mm_P5.00mm | F1 |
| 1 | SWITCH | Connector_PinHeader_2.54mm:PinHeader_1x02_P2.54mm_Vertical | J2 |
| 1 | UART | Connector_PinHeader_2.54mm:PinHeader_1x03_P2.54mm_Vertical | J3 |
| 1 | XIAO_ESP32C6 | LoRa_Gate_Local:XIAO_Module_Socket_2x7_P2.54mm_W15.24mm | U2 |
| 1 | XIAO_WioSX1262 | LoRa_Gate_Local:XIAO_Module_Pin_Flipped_2x7_P2.54mm_W15.24mm | U3 |
It measures 30 × 58.5 mm and has 2 copper layers, 1.6 mm thick.
10 parts, built around the MCP1700-3302E/TO and MF-R050. The full list of its parts is on this page, with links to each part's symbol and footprint where PCBKit's library has them.
From the andrewgho/loragate repository on GitHub, at the commit this page was made from: the board is kicad/loragate_tht/loragate_tht.kicad_pcb and its schematic kicad/loragate_tht/loragate_tht.kicad_sch. PCBKit opens the board in the browser, with nothing to install.
Its repository states no licence, so its author keeps all rights: ask andrewgho before reusing the design.

timville85/4dapter
This project combines the NES, SNES, N64 and Genesis DaemonBite Retro Controllers projects together with a custom PCB to support 4 different controllers with a single socketed Arduino Pro Micro.



Jeffrey-McCormick/Project-Heartbeat
End-to-end embedded system project to accurately measure the heart rate of a person


MengqiS21/TECHIN514_FinalProject
A wearable sensing tag for pets and a tabletop display that visualizes daily activity and proximity-based companionship.

MengqiS21/TECHIN515_final_project-individual-
Vehicle engine faults such as air leaks, ignition misfires, and abnormal idle behavior often produce characteristic acoustic signatures that trained mechanics can identify by ear.
