Skip to content

How TankSync Works

Ravi Singh edited this page May 23, 2026 · 1 revision

How TankSync Works

Under-the-hood deep dive for the curious: what each part of the system does, how packets flow, what the sleep cycle looks like, how data ends up in your dashboard.

If you just want to use TankSync, Quick Start is enough. This page is for people who want to modify it, port it, or just understand it.


System bird's-eye

flowchart LR
    TX[TX node<br/>solar + 18650] -->|LoRa 865 MHz<br/>~250 ms per wake| RX[RX hub<br/>USB-C]
    RX -->|Wi-Fi + MQTT TLS| Broker[(MQTT broker<br/>local or cloud)]
    Broker --> HA[Home Assistant<br/>HACS integration]
    Broker --> PWA[TankSync PWA<br/>tanksync.smartghar.org]
Loading

One TX per tank. One RX per property. Up to 6 TXs per RX. Broker is your own Mosquitto, HA's built-in broker, or mqtt.smartghar.org.


TX sleep cycle — the battery story

The TX is the power-constrained side. Default behaviour:

  1. Wake (from deep-sleep timer or hard reset). ~3 ms boot from RTC memory.
  2. Power up sensor rail — drive the 5V high-side gate HIGH, wait PWR_SETTLE_MS (50 ms) for the AJ-SR04M analog front-end to stabilize.
  3. Sample 5 ultrasonic readings, 50 ms apart. Median = the level. Reject readings <5 cm or >400 cm.
  4. Read INA219 (or ADC for Variant A): battery voltage + signed current (+ve = discharge, -ve = charge).
  5. Build TANK packet — current sensor state, battery, RSSI estimate, sensor_status byte.
  6. Transmit + wait for ACK — up to 3 s. Up to 3 retries.
  7. Power down 5V rail.
  8. Deep sleep for SLEEP_INTERVAL_S (default 300 s).

Awake duration: ~250 ms typical. Sleep current: ~10 µA. Wake current: ~70 mA peak during TX. Solar charging keeps the 18650 topped up indefinitely; without solar, a single 18650 lasts 3-6 months.

See reference_tx_power_budget_2026_05_19 for bench-measured numbers.


LoRa protocol — what's in a packet

The RYLR998 modules talk AT-command-style UART. TankSync wraps a simple protocol on top:

TANK packet (TX → RX, every wake cycle)

TANK|<addr>|<seq>|<dist_cm>|<bat_v>|<cur_ma>|<sensor_status>|<rssi_est>
Field Bytes Description
TANK 4 Magic prefix
addr 1-5 TX address (1, 2, 3… on this hub)
seq 1-5 Per-TX sequence number (wraps at 65535)
dist_cm 1-4 Median distance reading in cm; 0 if read failed
bat_v 4-5 Battery voltage with 2 decimals (e.g. 3.92)
cur_ma 1-5 Signed current in mA (+ = discharging, - = charging)
sensor_status 1 'o'=ok, 'e'=error, 'u'=unknown
rssi_est 3-4 TX's last-known RX RSSI (so RX can graph link quality)

Pipe-separated ASCII for human-readability in serial logs. Total ~30 bytes per packet — fits comfortably in RYLR998's payload limit.

TANK_ACK (RX → TX, immediately after receiving)

ACK|<addr>|<seq>|<next_check_in_s>|<config_blob_or_empty>

next_check_in_s lets the hub adjust the TX's sleep interval dynamically (lower during pump events, higher when stranded). config_blob carries calibration updates pushed from PWA.

PAIR_REQ (TX → RX, during pair window)

PAIR_REQ|<mac>|<requested_addr>|<seq>

MAC included since tx-v2.0.11 — enables tombstone restore on re-pair (preserves name + capacity + alerts across delete cycles).

PAIR_ACK (RX → TX, only during pair window)

PAIR_ACK|<assigned_addr>|<netid>|<initial_config>

Assigns the TX a small int address (1, 2, 3…) instead of letting it pick randomly. NETID stays fixed across pairs.


NETID — radio-level multi-hub isolation

Each RYLR998 listens for packets matching its configured NETID. Packets with a different NETID are filtered at the radio level — they never reach the MCU.

When a hub powers on for the first time, it generates a random NETID in 1-200 and persists it to NVS. The NETID never rotates afterwards (rx-v2.7.10+). All TXs paired to that hub get configured with the same NETID during PAIR_ACK.

Two hubs at the same physical location → 0.5% chance of colliding NETIDs (~10 hubs before the birthday-bound matters). If it happens, factory-reset one hub; its next first-pair picks a different value.


Hub state machine

flowchart LR
    Boot[Boot] --> Setup{Wi-Fi creds<br/>in NVS?}
    Setup -->|No| AP[Setup mode<br/>TankSync-Setup-XXXX AP]
    Setup -->|Yes| WiFi[Connect to Wi-Fi]
    WiFi --> Live[Live mode<br/>LoRa RX + MQTT pub]
    Live -->|BOOT button 5s| AP
    AP -->|Captive portal config| WiFi
Loading

In Live mode, the hub:

  • Listens for LoRa packets continuously (RYLR998 always-on, ~30 mA).
  • Acks every TANK packet within ~50 ms.
  • Publishes per-tank readings to MQTT (retained topics).
  • Computes "things worth knowing" insights locally (total reserve, yesterday's draw, sediment drift) every 30 min and publishes them.
  • Serves the local web UI on port 80.

The hub never does cloud round-trips for control decisions. The cloud + PWA are observers; the hub is the source of truth for pairing and state.


MQTT topic structure

All topics scoped under tanksync/<device_id>/ where <device_id> is the hub's MAC-derived ID.

Topic Direction Retained? What
tanksync/<hub>/status hub → broker Yes Hub-level state (online, version, NETID)
tanksync/<hub>/tank/<addr>/state hub → broker Yes Per-tank current state (level, battery, signal, last-seen)
tanksync/<hub>/tank/<addr>/history hub → broker No History buffer dumps (PWA → graphs)
tanksync/<hub>/insights hub → broker Yes "Things worth knowing" derived insights
tanksync/<hub>/cmd PWA → broker → hub No Remote commands (pair, calibrate, remove_tx, identify)
tanksync/<hub>/cmd_ack hub → broker No Ack for the command above
homeassistant/sensor/<discovery>/config hub → broker Yes HA auto-discovery payloads (if enabled)

Home Assistant auto-discovery means tanks show up as entities automatically — no YAML config needed.


How the PWA finds the hub

Two paths:

Cloud path (tanksync.smartghar.org)

  • PWA logs into your account.
  • Account row in cloud DB lists the device IDs you've claimed.
  • PWA subscribes to tanksync/<device_id>/+/state topics via the cloud broker.
  • Updates stream in via WebSocket.

Local path (LAN-only)

  • PWA cannot fetch local-IP HTTP URLs (mixed-content blocked).
  • Instead, PWA shows a "Open hub web UI" button that opens http://<hub-ip>/ in a new tab when you're on the same Wi-Fi.

How does the PWA know the hub's local IP?

The hub publishes its current local IP in the retained <hub>/status topic. The PWA reads it from there. Works as long as you've claimed the hub at least once.


What's open, what's not

Component License Public
RX firmware AGPL-3.0
TX firmware AGPL-3.0
Hardware (schematics, BOM, STL) CC-BY-SA 4.0
HACS integration MIT
PWA (tanksync.smartghar.org) Proprietary 🚫
Cloud server (Node + MQTT bridge + DB) Proprietary 🚫

The open parts are enough to build a fully working TankSync system without ever touching the cloud. The cloud + PWA are the SaaS convenience layer — paid users get it hosted; everyone else can self-host with Mosquitto + Home Assistant.


Going deeper

Clone this wiki locally