
Sensor Node
souptik-samanta/Sensor-Node
A compact environmental monitoring node built around the STM32L052 microcontroller. The board measures temperature, humidity, while providing onboard storage and support for external SX1262 LoRa modules.
LILYGO T-SIM7080G S3 based tracker
Ae tracker db is an open-source printed circuit board by Aces-Electronics, from the Aces-Electronics/ae-nv-gps-tracker repository on GitHub. It is a 2-layer board of 29 × 41.1 mm with 29 parts on 34 nets, routed with 306 tracks and 53 vias.
It is built around the SN65HVD231D, BQ25176JDSG and LIS3DH, with the SMBJ18A among its other parts. Its parts include 11 resistors, 6 capacitors, 4 ICs, 3 connectors and 1 diode.
LilyGo T-SIM7080G-S3 based GPS tracker with NB-IoT connectivity for the AE-NV ecosystem.
ae-tracker-db — LIS3DH accelerometer, SN65HVD231 CAN
transceiver, and a BQ25176J charger that tops the LiPo up from the jetski's
supply through an MCU-switchable cutoff. Plugs onto the LilyGo's 16-pin
expansion header; the J4 pin map is in src/utilities.h.Unknown without itdocs/can/seadoo-can.md.The tracker publishes the following data via MQTT:
{
"mac": "860016043157614",
"model": "ae-sim7080g-tracker",
"fix": true, // Did this wake get a GPS fix at all
"lat": -28.02575, // lat/lon/alt/speed/sats/hdop are omitted if !fix
"lon": 153.3879,
"alt": -11.981,
"speed": 0,
"sats": 11,
"supply_adc_raw": 9216, // Ski supply, raw LIS3DH aux-ADC count
"supply_voltage": 12.18, // ...converted, once calibrated (absent if not)
"voltage": 0.00, // USB VBUS placeholder, debug frames only
"device_voltage": 4.177, // Internal battery voltage
"battery_voltage": 4.177, // Legacy alias
"rssi": 19,
"orientation": "Flat",
"charge_state": "Charging", // Charging | Idle | Fault | No Input
"supply_enabled": true, // Is the jetski allowed to feed the charger
"wake_reason": "Motion", // Cold Boot | Timer | Motion
"accel_up": 1.00, // Vehicle frame, g. Omitted if no IMU
"accel_fwd": -0.02,
"accel_stbd": 0.03,
"engine_bus": true, // Was the ski's CAN bus awake at all
"ski_running": true, // Drives the reporting cadence# Build firmware
pio run -d /path/to/ae-sim7080g-tracker
# Flash to device
pio run -d /path/to/ae-sim7080g-tracker -t upload --upload-port /dev/ttyACM6
# Monitor serial output
pio device monitor --port /dev/ttyACM6 -b 115200A separate environment boots straight into a listen-only CAN sniffer and never reaches the tracker's normal cycle — no modem, no MQTT, no sleep:
pio run -e can-sniffer -t upload && pio device monitor -b 115200It scans 250k → 500k → 125k, keeps whichever rate hears frames, prints a table
of distinct IDs with hit counts and per-byte ranges, then runs one probe: an
8-second learn phase, a single *** GO ***, and a 40-second window that records
every frame and narrates which bytes changed while you work a control.
The learn phase is the trick. It marks every byte that moves while the vehicle is left alone — engine signals, counters, checksums — so afterwards it can report only the bytes that held still and then moved. Operate one control and the output is a short list naming that control's bytes.
Two things worth knowing before relying on it:
GO for each, and the natural thing was to act on the
second one, which was no longer recording. Three sessions were lost that way.This is how the iBR up/down decode was found: the position byte and its command byte both showed up in the narration within seconds of pressing a button.
Listen-only never drives the bus, not even the ACK bit, so this is safe to
plug into a running ski before anything is known about the bus or its rate. The
shipping firmware is unaffected — default_envs keeps this out of a bare
pio run, and the linker drops the sniffer and the TWAI driver from it.
Note that the daughter board's CAN termination jumper (JP1) is bridged by default. A vehicle bus is already terminated at both ends, so JP1 most likely wants cutting before connecting to a real ski.
The tracker can be configured via BLE during the 15-second window on cold boot, or by holding the boot button through a reset. A connected client holds the window open for as long as it stays, up to a five-minute ceiling — 15 seconds is less than a connect, a service discovery and a look at the screen, so without that the app was routinely cut off mid-session by the device closing BLE underneath it. The ceiling is what stops a phone left connected in somebody's pocket from holding the tracker out of deep sleep indefinitely.
Service 4fafc203-1fb5-459e-8fcc-c5c9c331914b.
| Characteristic | UUID suffix | Direction | Payload |
|---|---|---|---|
| GPS | …2030 | read/notify | lat,lon,speed,sats,hdop,sats_in_view,fix |
| Status | …2031 | read/notify | volts,soc,csq,Net: <creg> |
| Name | …2040 | read/write | device name suffix |
| Interval | …2050 | read/write | uint32 LE, minutes |
| Motion sensitivity | …2051 | read/write | one byte, 0 Low / 1 Medium / 2 High |
| Verbose telemetry | …2052 | read/write | one byte, 0 off / non-zero on |
| Diagnostics: Device | …2060 | read/notify | JSON |
| Diagnostics: Position | …2061 | read/notify | JSON |
| Diagnostics: Power | …2062 | read/notify | JSON |
| Diagnostics: Motion | …2063 | read/notify | JSON |
| Diagnostics: Engine | …2064 | read/notify | JSON |
| Pairing | ACDC1234-…-1234567890CB | read (encrypted) | trigger only |
Plus APN (ae000101-…) and the MQTT broker/user/pass characteristics
(…2645/…2646/…2647).
Bonding with MITM protection and Secure Connections, with a six-digit passkey derived from the last three bytes of the Wi-Fi base MAC — the same derivation every other AE product uses, and the same one the app displays.
This was previously off: the source said // Security DISABLED for verification,
which meant the PIN the app showed on the tracker screen was decoration. Nothing
ever asked for it, and anyone within radio range during a config window could
rewrite the APN, the broker and the MQTT credentials.
What is gated, and what is not:
READ_ENC) — all five diagnostics sections.WRITE_ENC, and READ_ENC where readable) — every
configuration characteristic: name, APN, broker, MQTT user and password,
interval, motion sensitivity, verbose telemetry.The app reads the pairing characteristic on connect purely to force the link to encrypt before it subscribes to anything — a subscribe on an encrypted characteristic across an unencrypted link fails, and it fails quietly. That read is service-agnostic in the app, so exposing the characteristic is all that was needed on this side.
The device has no display, so the PIN comes from the app: it is shown against the device while it is being connected to (which is when the OS prompts for it) and on the tracker's own screen afterwards. It is also printed to the serial console on every connect.
The GPS line's last two fields were appended to what was a five-field format,
so an older app reads the first five and ignores the rest. sats_in_view is the
number that moves during acquisition — sats counts the ones used in a fix and
sits at zero until there is one, so a screen showing only that looks broken for
two minutes and then works.
The five diagnostics characteristics carry the whole telemetry report, section
by section, refreshed once a second while a client is connected. They are built
by buildTelemetry() — the same function that builds the MQTT frame — so what
the app's debug screen shows is what the device publishes, by construction. They
are sectioned because a BLE characteristic value stops at 512 bytes and a full
verbose report is about twice that; buildTelemetry() decides which section each
field belongs to, so a field added later has to be given a home rather than
quietly failing to reach BLE.
Unlike the MQTT frame, the diagnostics are always verbose. The
debug_payload setting decides what is worth paying for over the cellular link;
it has nothing to say about what a phone already standing next to the ski may
look at.
Monitor real-time debug output:
pio device monitor --port /dev/ttyACM0 -b 115200The firmware includes comprehensive diagnostic logging:
GPS Acquisition:
[DEBUG] Raw CGNSINF: 1,1,20260206025753.000,-28.025740,153.387809,...
GPS FIX! Sats: 10 (lat: -28.02574, lon: 153.38780)Network Registration:
[DEBUG] Checking network status...
[DEBUG] CEREG: +CEREG: 0,2 // 0,2 = registered on home network
[DEBUG] COPS: +COPS: 0,0,"Telstra Mobile",7 // Connected operator
[DEBUG] Signal Quality after registration: 20 // RSSI in dBmMQTT Publish:
[DEBUG] Using Topic: ae-nv/tracker/860016043157614/up
[MQTT] Publishing Payload: {"mac":"860016043157614",...}
[MQTT] Publish OKNo GPS Fix:
Network Registration Fails:
0,0 = not registered, 0,2 = registeredMQTT Publish Fails:
telstra.internet)Device Not Appearing in UI:
mac field matches device IDFor detailed AT command logging, uncomment in platformio.ini:
build_flags =
-DTINY_GSM_DEBUG=SerialThis will show all modem AT commands and responses.
The firmware uses robust satellite count parsing with fallback logic:
+CGNSINF response (index 15)voltage: External supply voltage (0.00V placeholder for future charging hardware)device_voltage: Internal PMU battery voltage (~4.2V when fully charged)battery_voltage: Legacy field for backward compatibilityActive Mode: GPS acquisition + MQTT publish
BLE Window: 15 seconds on cold boot, extended while a client is connected, to a five-minute ceiling
Charge cutoff: the jetski feed is cut above 4.10V and restored below 3.70V, decided at report time against the PMU's battery reading and latched across sleep in RTC memory.
4.10V sits below the BQ25176J's 4.2V regulation point, so the firmware — not the charger — is what ends a charge, opening the switch on the way up before the cell is ever held at full. The wide gap down to 3.70V is what makes that worth doing: a LiPo ages fastest sitting at full charge, so the pair cycles the cell through roughly 3.7–4.1 rather than floating it at 4.2. With only a sleeping tracker as a load, one traverse of that band takes a long time, so it costs very few cycles to buy. Both thresholds read the AXP2101's cell voltage — the jetski's own voltage is not available to decide on until the supply-sense divider is reworked.
Deep Sleep: Cadence follows the ski, not the clock.
Running (CAN bus awake) → the configured report_interval_mins,
5 minutes by default, settable over BLE. GPS-failure backoff applies
(5/15/30/60/180 min).
Parked → a 1440-minute (daily) heartbeat. GPS-failure backoff is not applied; stretching a day further on one missed fix trades away the only thing a heartbeat is for.
A parked heartbeat that fails to publish is retried sooner, at 60 → 180 → 360 minutes before falling back to the daily cadence. Without this, one bad modem session meant 48 hours of silence — and silence is indistinguishable from a stolen ski, a flat battery, or a dead tracker. Escalating rather than fixed because the usual cause is no coverage, and retrying hourly forever in a dead zone spends exactly the battery the daily cadence exists to protect.
Note the daily timer is clocked by the ESP32's internal RC oscillator, not a crystal, so a 1440-minute sleep can legitimately land up to roughly an hour either side. A heartbeat that is late by tens of minutes is normal; one that is hours late is not.
CAN traffic is the running signal because the ski's bus only wakes with the
ignition. Supply voltage above ~13V would be the better test — it survives a
CAN fault — but the supply-sense divider is mis-scaled and reads nothing
usable yet (see R15/R16 in src/utilities.h).
Wake on motion: The LIS3DH interrupt is an ext1 deep-sleep wake source.
Motion reports are rate-limited to one per 120s, checked before the modem is
powered, so a suppressed wake costs ~200ms rather than a GPS acquisition.
Tapping the board will not wake it, and that is the design. INT1_DURATION
is counted in ODR periods, so the default three samples at 50Hz require 60ms
of continuous over-threshold motion, and normal mode band-limits to roughly
ODR/2 = 25Hz before the comparator sees anything. A finger tap is a few
milliseconds of mostly high-frequency energy — filtered down, and over long
before the duration counter arrives. Shake the board instead.
To check the path live rather than by waiting out a sleep cycle, the
db-bench build watches INT1 for 15 seconds on startup and prints every
latched event with the axes that caused it.
On every wake from sleep the firmware reads the LIS3DH's interrupt block back before re-arming it and says whether it survived. Those registers are volatile, so retained values prove the part held its 3.3V rail through the sleep and reset defaults prove it did not — which settles the question a missed wake otherwise only raises. (It should always survive: the daughter board's 3V3 is J4.1 = AXP2101 DCDC1, the same rail that keeps the ESP32's own RTC domain alive. Nothing in this firmware touches DCDC1.)
Motion sensitivity: three selectable levels, each setting the floor of the adaptive ladder rather than a fixed threshold — the ski still finds its own level from wherever it starts. Values are chosen against a measured noise floor of 24mg (high-pass filter settled, board at rest), so the margins are known rather than guessed:
| Level | Floor | Margin | Ceiling | For |
|---|---|---|---|---|
| Low | 704mg | ~29× noise | 2032mg | A deliberate shove; a ski somewhere lively, where less would report the weather |
| Medium | 352mg | ~15× noise | 1408mg | Shipped default. Registers ordinary handling |
| High | 256mg | ~11× noise | 1024mg | A quiet mooring, where a nudge should count |
These were doubled from an original 352/176/128 after the first real deployment: a unit on Medium riding in a car woke nine times in 65 minutes and spent the hour climbing away from a floor too low to start from. The old Medium is the new Low, which is the honest description of what 176mg proved to be worth.
All three sit exactly on the INT1_THS grid (16mg steps at ±2g) — 44, 22 and
16 counts — so none truncate. Worth checking when retuning: 56mg, for example,
is not representable and would silently become 48mg, only 2× the noise floor.
Settable over BLE on characteristic beb5483e-36e1-4688-b7f5-ea07361b2051
(read/write, one byte: 0 Low, 1 Medium, 2 High). A write out of that
range is rejected rather than clamped — clamping would silently land on
Medium and the value read back afterwards would not be the one sent. The
change takes effect immediately, re-arming the accelerometer rather than
waiting for the next boot, which for a parked ski would be a day away.
motion_sensitivity and motion_threshold_mg are published on every
frame, not just debug ones. They used to be gated behind debug directly
under a comment saying they existed so the adaptive logic could be seen from
the backend rather than a serial cable — which the gate prevented. A unit sat
at 528mg on a 176mg floor for an hour and nothing in the cloud said so.
TODO — web UI control. The setting is stored in NVS (mot_sens), carried
in TrackerSettings.motion_sensitivity, published as motion_sensitivity,
and writable over BLE — but the web app has no control for it yet.
Adaptive threshold, which moves in both directions:
Up. After a motion wake, GPS arbitrates: a fix is travel if it shows 2 km/h or more, or if it has moved 100m or more from the reference position. Anything else counts as unexplained, and three consecutive unexplained wakes raise the threshold one step. A wake with no fix changes nothing — a garage roof is not a false positive.
Step and ceiling both scale with the selected floor (step = the floor,
ceiling = 4× it), so three unexplained wakes cost the same fraction of the
range at every setting. They used to be flat 176mg/1408mg — Medium's numbers
applied to everything — which made the control a floor and nothing else: a
unit on High could be walked to eleven times the sensitivity its owner asked
for. INT1_THS caps the ceiling at 2032mg (7 bits, 16mg steps at ±2g), which
is why Low's 2816mg is clamped.
Position is gated on PDOP, not HDOP. Measured on a stationary bench, two
fixes three seconds apart read 35m apart: one at PDOP 25.0, the other at PDOP
2.0. HDOP on the bad one was a respectable 3.9 — the error was almost entirely
vertical (VDOP 24.7), so only PDOP shows it. A DOP of 0 means the field was
absent, not that the fix was perfect, and is refused everywhere.
Up, faster, on rate alone. Motion wakes inside the 120s report limit are counted rather than discarded. Ten of them between two reports raises the threshold immediately, without waiting for GPS — the rate is the evidence, and those wakes are precisely the ones that never got a fix to be judged by. Previously they were invisible to this logic, which left a hole exactly where it was most needed: a hull working against a mooring produces mostly suppressed wakes.
Down. Genuine travel resets straight to the floor — that is proof the tracker should be at its most sensitive, and it contradicts whatever evidence built the climb. Failing that, three consecutive reports with no motion activity at all (no wake, none suppressed) ease it down one step. Without this the ladder is a ratchet, and a ski moved to a calmer berth stays deaf forever — the worst failure available here, because it is self-sustaining: the deafer it gets, the less likely anything crosses the threshold to say otherwise, including a theft.
The current value is persisted in NVS and published as motion_threshold_mg,
next to motion_sensitivity so a false-wake report can tell what was chosen
from what the ski decided, and motion_suppressed so the wake rate is visible
at all.
Changing the floor — by firmware retune or by the setting — discards a stored threshold derived from a different one, so a change actually reaches a unit that already has a value saved.
Wake LED: the AXP2101 charge LED is lit while awake and switched off immediately before every deep sleep, so a dark unit is a sleeping one. A 4Hz blink instead of steady means the wake came from the accelerometer, which is the only way to tell a motion wake from a timer wake on battery, where there is no serial port. Note this takes the LED away from the charger's own control; charge state is reported over MQTT instead.
(NEOPIXEL_PIN in utilities.h is inherited from other S3
boards and drives nothing — this board has no addressable RGB LED. The
remaining strip calls in main.cpp are dead code.)
No migration is needed for already-provisioned trackers: a stored 5-minute interval now describes the running case, which is what it was always for.
The daughter board divides the jetski supply into the LIS3DH's auxiliary ADC. With R16 swapped to 200k the divider is 1.8M/200k = 1/10, mapping 8–16V onto 800–1600mV — which is the LIS3DH aux ADC's actual window (1200mV ±400mV).
The divider taps SUPPLY, downstream of the TPS1H000 switch, so it reads
nothing whenever the charge policy has the feed cut — which is most of the time
by design. powerReadSupplyRaw() therefore closes the switch briefly (200ms
settle, covering six time constants of 180k × 100nF as well as the switch's own
turn-on), takes the sample, and restores the switch to exactly the state it was
in. Leaving it closed because a measurement happened would quietly undo the
charge cutoff.
Calibration is two-point and measured, not derived. ST does not document whether the aux-ADC code is inverted, and the real ratio depends on resistor tolerance and an input impedance ST does not specify — so a formula from the schematic would be a guess wearing a unit. Measured on hardware:
| point | meter | raw | 10-bit |
|---|---|---|---|
| low | 12.06V | 10188 | 159 |
| high | 14.33V | −7508 | −118 |
The code is inverted — 0V reads 32512, and the count falls as voltage rises. That is exactly the ambiguity the datasheet leaves open, and it is why this is measured. The slope works out at ~122 counts/V against ~128 predicted from the design, i.e. within ~5% — comfortably inside resistor tolerance.
To calibrate a board, flash the supply-cal environment and drive it over
serial: L <volts> and H <volts> store the two points into the same NVS keys
the tracker reads (sup_raw_lo/sup_v_lo, sup_raw_hi/sup_v_hi). Readings
are averaged over 16 samples, because one sample of a 10-bit ADC on a 180k
source is noisy enough to bake that noise into the fit. supply_voltage is
published only once both points exist; until then only supply_adc_raw goes
out, so an uncalibrated unit reports nothing rather than something wrong.
Useful range is ~8–16V. Below about 8V the ADC floors and the reading is optimistic — a flat jetski battery reads around 9V rather than as flat.
Note charge_state: "Fault" with no LiPo attached is expected: the
BQ25176J blinks STAT when it has nothing to charge.
Downloads used to die part-way with +CAURC: buffer full followed by
Socket closed early — 397468 of 680816 bytes on the observed failure.
The cause is arithmetic, not a bug. TinyGSM drains a socket one byte at a
time over the modem UART, and at 115200 baud that is ~11.5 KB/s. LTE Cat-M1
delivers up to ~37 KB/s, so the network fills the modem's socket buffer about
three times faster than it can be emptied — which is precisely what
+CAURC: buffer full reports. A 680 KB image also needs ≥59s of pure UART time
at 115200 before any AT overhead.
Three changes:
AT+IPR is not persisted without AT&W, so a modem left
fast would be unreachable on the next boot when Serial1 opens at 115200.
The switch verifies the link at the new rate and reverts on failure — the only
remote repair path for a fielded tracker is the OTA itself, so a half-applied
baud change would be unrecoverable. Both 460800 and 921600 were confirmed on
hardware with the baud-test environment.TINY_GSM_DEBUG is no longer in the release build. It prints a line per
data URC — ~660 of them per OTA — onto the same serial port the download is
competing with. Use the modem-debug environment when AT tracing is wanted.The tracker integrates with the AE-NV web dashboard:
The tracker includes a high-fidelity crash handling system for remote diagnostics:
.elf files for every build, allowing the backend to translate raw addresses into source code lines (e.g., main.cpp:142).See the agent prompts in /docs/agent-prompts/ for:
| Qty | Value | Footprint | References |
|---|---|---|---|
| 5 | 10k | Resistor_SMD:R_0603_1608Metric | R8, R9, R10, R11, R12 |
| 2 | 1.8M | Resistor_SMD:R_0603_1608Metric | R2, R15 |
| 2 | 100nF | Capacitor_SMD:C_0603_1608Metric | C1, C6 |
| 2 | 1uF | Capacitor_SMD:C_0603_1608Metric | C2, C5 |
| 1 | 1.5A 1206 | Resistor_SMD:R_1206_3216Metric | F1 |
| 1 | 100k | Resistor_SMD:R_0603_1608Metric | R16 |
| 1 | 10nF | Capacitor_SMD:C_0603_1608Metric | C3 |
| 1 | 10uF | Capacitor_SMD:C_0603_1608Metric | C4 |
| 1 | 120R | Resistor_SMD:R_0603_1608Metric | R7 |
| 1 | 27.4k | Resistor_SMD:R_0603_1608Metric | R13 |
| 1 | 3453k | Resistor_SMD:R_0603_1608Metric | TH1 |
| 1 | 590R | Resistor_SMD:R_0603_1608Metric | R14 |
| 1 | BQ25176J | Package_SON:WSON-8-1EP_2x2mm_P0.5mm_EP0.9x1.6mm | U3 |
| 1 | Conn_01x01_Pin | Connector_PinHeader_2.54mm:PinHeader_1x01_P2.54mm_Vertical | J2 |
| 1 | Conn_01x04_Socket | Connector_Phoenix_MC:PhoenixContact_MC_1,5_4-GF-3.5_1x04_P3.50mm_Horizontal_ThreadedFlange | J3 |
| 1 | Conn_01x16_Pin | Connector_PinHeader_2.54mm:PinHeader_1x16_P2.54mm_Horizontal | J4 |
| 1 | Jumper_2_Bridged | Jumper:SolderJumper-2_P1.3mm_Bridged_RoundedPad1.0x1.5mm | JP1 |
| 1 | LIS3DH | Package_LGA:LGA-16_3x3mm_P0.5mm_LayoutBorder3x5y | U2 |
| 1 | LOGO | Acea:ae_5mm | G*** |
| 1 | SMBJ18A | Diode_SMD:D_SMB | D1 |
| 1 | SN65HVD231 | Package_SO:SOIC-8_3.9x4.9mm_P1.27mm | U1 |
| 1 | TPS1H000-Q1 | Package_SO:MSOP-8-1EP_3x3mm_P0.65mm_EP1.68x1.88mm | U4 |
It measures 29 × 41.1 mm and has 2 copper layers, 1.6 mm thick.
29 parts, built around the SN65HVD231D, BQ25176JDSG, LIS3DH and SMBJ18A. 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 Aces-Electronics/ae-nv-gps-tracker repository on GitHub, at the commit this page was made from: the board is hardware/ae-tracker-db/ae-tracker-db.kicad_pcb and its schematic hardware/ae-tracker-db/ae-tracker-db.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 Aces-Electronics before reusing the design.

souptik-samanta/Sensor-Node
A compact environmental monitoring node built around the STM32L052 microcontroller. The board measures temperature, humidity, while providing onboard storage and support for external SX1262 LoRa modules.



ysoyjcgoxohxoh/Bird3
Drop-in replacement Bird Brain design for Bird 3 scooters.

bobbycar-graz/Telemetry-PCB
PCB with ESP32, CANbus, microSD, GPS and LoRa


