Everything you can do with the base station after it's online. Each command lists its syntax, an example, the bot's reply in the success and failure cases, and when you'd use it.
If you're setting up a fresh device for the first time, start with USER_GUIDE.md instead — it walks through the out-of-box flow. This document is the reference manual you'll come back to when you have a specific command in mind.
| Command | What it does |
|---|---|
/help |
Print every command the bot knows. Aliased as /start. |
/status |
List paired gates, last-seen timestamps, sequence counters. |
/status GATE-XXXX |
Live query: ask the gate over LoRa what state it's in right now. |
/pair GATE-XXXX <key> ["Name"] |
Register a gate. Must be sent in a private DM with the bot. |
/unpair GATE-XXXX |
Remove a gate. Requires /confirm. |
/rename GATE-XXXX "New Name" |
Change a gate's display name. |
/relay GATE-XXXX [seconds] |
Show or set how long the gate holds its relay closed (default 1s). |
/open [GATE-XXXX] |
Open a gate via the LoRa challenge/command sequence. |
/close [GATE-XXXX] |
Close a gate. |
/factory_reset |
Wipe Wi-Fi, Telegram, and gate state; reboot into the captive portal. Requires /confirm. |
/confirm <token> |
Acknowledge the most recent destructive prompt. |
/cancel |
Abort any pending /confirm. |
You do this once, before plugging in the base station for the first time.
Open Telegram on your phone, search for @BotFather, and start a
chat with it. Then:
- Send
/newbot. - Pick a display name (any human-readable string).
- Pick a username — must end in
bot, e.g.MyRanchAlertBot. - BotFather replies with a token that looks like
123456789:ABCdef.... This is yourTELEGRAM_TOKEN. Keep it private; anyone with this token can send messages as your bot.
The token never expires by default. If you ever leak it, send
/revoke to BotFather and start over.
The chat ID is the numeric identifier of the conversation the bot should send alerts to. For a 1-on-1 chat with the bot, this is your personal user ID; for a group chat, it's the group ID.
Easiest path:
- Send any message to your bot ("hi" works).
- In a browser, visit
https://api.telegram.org/bot<TOKEN>/getUpdates(substitute your real token). - Look for
"chat":{"id":123456789, …}in the JSON response. That number is yourTELEGRAM_CHAT_ID.
For a group chat:
- Create the group, invite the bot.
- Send any message in the group.
- Hit
getUpdatesthe same way. The group's chat ID is negative (e.g.-1001234567890); preserve the leading minus.
Telegram bots can't initiate conversations — they can only reply. Until you message your bot at least once from the chat you want to use, the bot has no permission to send to it. The "hi" is the trigger.
If you skip this step, the base station's first online ping will fail with HTTP 403 ("bot can't initiate conversation with a user") and the device will flip back to setup mode with an error banner.
When you first power on the base station, it broadcasts a Wi-Fi
network called BaseStation_Setup (no password). Connect to it,
open http://10.42.0.1/, log in with admin and the portal
password from the device's sticker, and fill in:
- Your home Wi-Fi SSID + password
- The bot token from step 1
- The chat ID from step 2
After you submit, the base station joins your Wi-Fi, the setup AP disappears, and the first thing it does is send a "Gate Monitor Base Station is Online." ping to the configured chat. If you don't see that ping within about a minute, something's wrong with the credentials — see Troubleshooting.
For the full setup walkthrough including photos, see USER_GUIDE.md.
These arrive unprompted, in response to physical events at the gates.
📡 Gate Monitor Base Station BASE-9A22 is Online.
Sent once on every boot of the base-station service, after the
device has confirmed its system clock is plausible (NTP) and the
Telegram TLS handshake works. This is the device's "I'm up" beacon.
The device ID (BASE-XXXX) makes it unambiguous which base just
came online if you've got more than one base reporting to the same
chat.
You'll see it:
- After the first captive-portal setup (means setup succeeded).
- After every reboot.
- After every successful Wi-Fi recovery (e.g. the operator re-entered Wi-Fi credentials following a router replacement).
🔓 Front Pasture (GATE-A1B2C3): OPEN 🔒 Front Pasture (GATE-A1B2C3): CLOSED
Sent whenever a paired gate's state changes — both opens and closes.
The leading emoji (🔓 / 🔒) makes the state visible at a glance in
the notification tray. If the gate has a display name (set via
/pair or /rename), the message shows it with the gate ID in
parentheses; otherwise just the ID.
Dedup rules: the base only fires a Telegram message on a real
state transition. A /status GATE-X reply that comes back showing
the gate in the same state it was already known to be in does
not also fire an unsolicited notification — that would
double-spam every status check. The de-dup compares against the
most recently logged event in the SQLite gate-events table.
- No periodic heartbeat. If you want one, point an external
uptime monitor at the base station's Telegram online-ping
cadence — at most an outage of
Restart=on-failure+ the watchdog threshold (30 min) will go undetected. - No batched / queued alerts. A network outage means alerts fired during the outage are logged locally but not flushed when connectivity returns — by the time you'd be reading them they'd be misleading.
- No diagnostic chatter. The bot stays quiet between alerts and
your commands. If you want to see what's happening on the device,
SSH into a dev image and read
journalctl.
Every command below works in the configured chat — whoever's in that chat (whether it's just you, or a group of trusted family members) can issue any of these. The bot rejects messages from any other chat silently.
Syntax: /help (alias: /start)
Example:
/help
Reply: A grouped list of every command the bot understands, ending with the security note that anyone in the chat can drive the device.
Use this if you forget a syntax in the field. The bot will echo it back to you on demand.
Syntax: /status (alias: /gates)
Example:
/status
Reply (no gates):
📋 Base: BASE-9A22 • Wi-Fi: HomeNetwork
No gates registered. Pair one with /pair GATE-XXXX <key> [name].
Reply (with gates):
📋 Base: BASE-9A22 • Wi-Fi: HomeNetwork
2 gate(s) registered:
• Front Pasture (GATE-A1B2C3): 🔓 OPEN
⏱ open ~15s (n=14) · close ~16s (n=18) · 🔘 press 1s
• Back Gate (GATE-B7Z3K4): 🔒 last seen CLOSED (no live reply)
⏱ open ~30s (warmup) · close ~30s (warmup) · 🔘 press 1.5s
The header line carries the base's device ID and the currently-
connected Wi-Fi SSID, so if you've got multiple base stations
reporting into one chat you can tell which one replied, and you can
spot the "base is on the wrong / a backup network" failure mode
without having to SSH in. SSID is (unknown) if NetworkManager
can't report an active wireless connection (e.g. the operator
landed on the captive portal AP somehow, or NM is wedged).
Per-gate state comes from a live LoRa status_req to each registered
gate. A gate that responds in time shows its current state
(🔓 OPEN / 🔒 CLOSED). A gate that doesn't reply within the LoRa
timeout falls back to the most recent state from the SQLite event
log, marked last seen X (no live reply) so you can tell it might
be stale. A freshly-paired gate that has never reported and isn't
responding shows ❓ no data (no live reply).
The ⏱ line under each gate is the adaptive grace period the base
will wait for /open and /close against that gate, separately for
each direction. (n=X) means the threshold is computed from this
gate's last X successful actuation cycles; (warmup) means the
gate hasn't logged enough samples yet (fewer than 5) and the base
is falling back to the 30s default ceiling. As the gate gets more
real-world use the threshold tightens to match the gate's actual
cycle time. Newly-paired gates start in warmup until you've used
/open or /close against them enough to fill the buffer.
The 🔘 press cell on the same line is the relay pulse duration
you've set for that gate with /relay (1s by default) — unlike the
adaptive grace periods, that's a value you configure rather than one
the device learns.
The list is sorted by pairing time, oldest first.
Syntax: /status GATE-XXXX
Example:
/status GATE-A1B2C3
Reply (success):
🔒 Front Pasture (GATE-A1B2C3): CLOSED (live).
⏱ open ~15s (n=14) · close ~16s (n=18) · 🔘 press 1s
Reply (gate offline / out of range):
❌ Front Pasture (GATE-A1B2C3) did not answer the challenge. Is the gate powered on and in LoRa range?
⏱ open ~15s (n=14) · close ~16s (n=18) · 🔘 press 1s
Reply (base-side radio failure):
❌ Could not transmit to GATE-A1B2C3 — the LoRa serial write failed. Check the device log; this is a base-side problem, not the gate.
⏱ open ~15s (n=14) · close ~16s (n=18) · 🔘 press 1s
This sends a real status_req packet over LoRa and waits up to 5
seconds for the gate to reply. Different from /status (no arg),
which only reads the local database. The second line carries the
adaptive /open and /close grace periods plus the configured
relay press time (🔘 press) for this gate (see the no-arg
/status section above for the (n=X) vs (warmup) explanation
and the /relay section for the press time). That line is stable
metadata about the gate's actuation profile, so it appears on both
success and failure replies — even when the gate didn't answer the
live query, you can still see the wait you'd face on the next
attempt.
Use this when:
- The gate hasn't sent an alert in a while and you want to confirm it's still alive.
- You're verifying a gate is wired correctly after physical work.
Syntax: /pair GATE-XXXX <fernet-key> ["Optional Name"]
Example:
/pair GATE-A1B2C3 gAAAAABl1234567890_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx= "Front Pasture"
The key comes from the gate's factory sticker. The name is optional; quote it if it contains spaces. If you omit the name, alerts show just the gate ID.
Reply (success, new gate):
✅ Paired Front Pasture (GATE-A1B2C3).
The bot also immediately deletes your /pair message from the chat,
because the message contained your Fernet key. Telegram's own
backups will still have it for up to 48 hours — see
The chat-history caveat.
Reply (gate already paired):
GATE-A1B2C3 is already paired (last_seq=147).
Confirm with `/confirm 7f3a` within 60s to overwrite. The Fernet key has already been redacted from your /pair line; this prompt does not echo it. Send /cancel to abort.
Re-pairing an existing gate with a new key resets the gate's
sequence counter — required because the new key implies a new gate
device with seq starting at 0. That's destructive (the old sequence
data is gone), so the bot routes it through /confirm.
Reply (invalid key):
Invalid Fernet key — keys are 44 url-safe-base64 characters. Check the gate's factory sticker and try /pair again. The key was redacted from your message but is still visible to anyone with access to Telegram backups for up to 48 hours.
Reply (sent from a group):
/pair must be sent in a private DM with the bot, not a group chat — the Fernet key would leak to every group member. DM me directly and try again.
- DM-only. Even if your bot is in a group chat for alerts,
/paironly works in a private DM with the bot. Open a 1-on-1 chat with the bot to pair, then add it back to the group if that's where you want alerts. - Rate limited. Max 5
/pairattempts per hour per user. Helps against typo storms that would otherwise pollute Telegram's backups with leaked keys. - Name length cap. 64 characters max. Long names truncate the alert text on small phone screens.
The Fernet key is a bearer credential for the gate. Anyone with the key can decrypt that gate's alerts, forge alerts as that gate, and (if the command channel is enabled) drive the gate's relay.
The bot calls deleteMessage on your /pair line the instant it
parses the command, but Telegram retains the message in its own
backups for up to 48 hours per their published policy, and any
client that already cached it before the deletion still has a copy.
There is currently no pairing path that avoids Telegram — the
captive portal collects Wi-Fi and Telegram credentials only, not
gate keys — so this exposure window is inherent to pairing. If a
key may have been compromised, re-provision the gate with a fresh
key (provision_gate.py) and /pair it again.
Syntax: /unpair GATE-XXXX
Example:
/unpair GATE-A1B2C3
Reply:
Confirm with `/confirm 7f3a` within 60s.
This will remove Front Pasture (GATE-A1B2C3) (last seen 2026-05-22 14:18:55, last_seq=147). Gate hardware will be unaffected; you can re-pair anytime with /pair. Send /cancel to abort.
After /confirm <token>:
✅ Removed Front Pasture (GATE-A1B2C3). Event history kept; re-pair anytime with /pair.
Reply (unknown gate):
GATE-NOPEXX is not registered.
What this affects:
- The gate's row in
registered_gatesis deleted; the base station ignores any future alerts from that gate ID. - The event history (
gate_eventsrows) is kept — useful for investigating "did this gate ever open last week?" after a gate's been physically replaced. - The gate's SD card is not touched. You can re-pair the same
gate with the same key any time, and it'll pick up where it left
off — except the
last_seqresets to 0, so you'll see one "Replay or out-of-order packet" warning in the device log per message until the gate's persisted seq counter catches back up (or you re-flash the gate, which resets its seq too).
Syntax: /rename GATE-XXXX "New Name"
Example:
/rename GATE-A1B2C3 "North Driveway"
Reply:
✅ Renamed GATE-A1B2C3 → North Driveway.
Reply (unknown gate):
GATE-NOPEXX is not registered. Pair it first with /pair.
/rename does not require /confirm — it's idempotent and easy
to undo. Just /rename it back to the previous name if you change
your mind. Alerts from that gate will use the new name immediately.
Syntax: /relay GATE-XXXX [seconds]
Most gate openers trigger on a momentary contact closure — a brief
"button press". One second works for the majority of openers, which
is the default. Some openers need a longer hold to register, or a
shorter tap to avoid a double-cycle. /relay tunes that press time
per gate, and it's kept separate from /pair so pairing stays
focused on the key.
Run it with no number to see the current value:
/relay GATE-A1B2C3
🔘 North Driveway (GATE-A1B2C3) relay press time is 1s. Change it with /relay GATE-A1B2C3 <seconds>.
Run it with a number (in seconds, decimals allowed) to set it:
/relay GATE-A1B2C3 1.5
🔘 North Driveway (GATE-A1B2C3) relay press time set to 1.5s. Takes effect on the next /open or /close.
Reply (out of range):
❌ Press time must be between 0.1s and 30s.
The value is stored on the base station and travels inside every
/open and /close command frame, so no gate re-flash is needed
— the change applies on the very next actuation. A gate you've never
tuned uses the 1-second default, identical to older behavior.
/relay does not require /confirm — like /rename, it's
non-destructive and trivially reversible. The current value also
shows up on the metadata line in /status and /status GATE-XXXX
(🔘 press 1.5s), next to the adaptive grace periods.
Safety note: the gate clamps whatever it receives to a sane range (0.1–30s) regardless, so a bad value can never latch the relay closed indefinitely. The base enforces the same bounds before storing.
Syntax: /open [GATE-XXXX]
The gate ID is optional when exactly one gate is paired — the base will auto-select it. With zero or multiple gates paired the base asks you to specify, rather than guessing (driving the wrong gate physically moves something).
Example:
/open GATE-A1B2C3
Example (single-gate install, no ID needed):
/open
Reply (no gates paired):
❌ No gates registered. Pair one with /pair GATE-XXXX <key> [name] before /open.
Reply (multiple gates, ambiguous):
❓ Multiple gates paired (Front Pasture, Back Pasture). Specify which: /open GATE-XXXX
Reply (success):
🔓 Opened Front Pasture (GATE-A1B2C3).
Reply (gate was already open):
ℹ️ Front Pasture (GATE-A1B2C3) was already open; no relay pulse fired.
Reply (gate didn't answer):
❌ Front Pasture (GATE-A1B2C3) did not answer the challenge. Is the gate powered on and in LoRa range?
Reply (gate answered, but state didn't confirm):
⚠️ Front Pasture (GATE-A1B2C3) accepted the challenge but did not confirm. The action may still have fired — send `/status GATE-A1B2C3` to check.
The base station sends an authenticated challenge_req to the gate.
The gate replies with a single-use, 15-second-lifetime random nonce.
The base then sends a command(open, nonce, relay_ms) packet. The
gate verifies the nonce, pulses the relay for the configured press
time (1 second by default; tune it per gate with /relay), and the
new state propagates back as a normal alert packet.
If you see the success ack but the gate didn't physically move, the
issue is between the relay and the gate itself (wiring, motor
power) — the base's view of "the relay pulsed" is whatever the gate
reported back. /status GATE-XXXX is the live truth check.
Syntax: /close [GATE-XXXX]
Identical to /open in every way, with closed as the target
state. Gate ID is optional under the same single-gate auto-select
rule. The success reply leads with 🔒 (matching the close-state
emoji):
🔒 Closed Front Pasture (GATE-A1B2C3).
Same failure modes as /open, same need for /status to
double-check on timeout.
Syntax: /factory_reset (no arguments)
Example:
/factory_reset
Reply:
Confirm with `/confirm 9c12` within 60s.
This will wipe:
• Wi-Fi credentials (currently: "home-2.4G")
• Telegram bot token and chat ID
• 3 paired gate(s): Front Pasture, Driveway, Back Pasture
• Event history
The device will then reboot the captive portal AP (BaseStation_Setup) and you'll need to re-enter all of the above. Send /cancel to abort.
After /confirm <token>:
🔄 Resetting now. You will lose this chat until the device joins a new Wi-Fi via the BaseStation_Setup captive portal.
The bot delivers that ack first, then the device:
- Sleeps 2 seconds to ensure the ack flushed through Telegram.
- Deletes
events.dbandbase_config.envfrom/var/lib/base_station/. - Removes the NetworkManager connection profile for your home Wi-Fi.
- Starts the captive-portal service.
- Exits its own process; systemd lets it stay dead because the config file is gone.
After the reset, the base station broadcasts BaseStation_Setup
again and waits for you to come back through the captive portal —
exactly like the first time you set it up, except the portal
password on the sticker still works (it's stored in /boot/, which
isn't wiped).
- You're changing home networks (new ISP, new router, new SSID).
- You're handing the device to someone else — though if you want to wipe the portal password too, you need to re-flash the SD card.
- Something's deeply wrong with the device state and you want a known-good starting point.
- The portal password (migrated on first boot from
/boot/provision_creds.envto/var/lib/base_station/) — needed to log back into the captive portal after the reset. - The device's hostname and Buildroot OS — this is a config reset,
not an OS re-flash. To wipe even the portal password, re-flash
with
flash_base_station.py.
There isn't one. The base station has no upload target it can
trust — Telegram chat would echo the gate Fernet keys, same problem
as /pair. The realistic recovery path is "the operator still has
the sticker on each gate; the sticker is the source of truth."
/factory_reset deliberately trusts that property.
Syntax: /confirm <token>
/unpair, /pair-with-overwrite, and /factory_reset don't run
immediately. They issue a short 4-hex-char token and wait up to 60
seconds for you to send /confirm <token> back.
Reply (success): Depends on the command — see each section.
Reply (wrong token):
Token doesn't match the most recent prompt. Re-check the 4-char code, or /cancel to start over.
Reply (expired):
That token expired (60s limit on /unpair GATE-A1B2C3). Re-issue the command if you still want to run it.
Reply (nothing pending):
Nothing to confirm — no pending action for you (or it already expired).
- One pending action per user. If you send a second destructive command before confirming the first, the first is dropped and you get a fresh token for the second.
- Single use. A confirmed token can't be replayed.
- 60-second TTL. Measured against the device's monotonic clock, not wall time, so changing the system clock can't extend it.
- Constant-time compare on the token — no timing side channel.
- Same operator only. The user ID on the
/confirmmust match the user ID who issued the original command. Useful when more than one person is in the chat: Alice's/unpaircan't be confirmed by Bob with the same token.
Syntax: /cancel
Reply (something pending):
🛑 Cancelled /unpair GATE-A1B2C3.
Reply (nothing pending):
Nothing pending to cancel.
Cheap fallback for "wait, I changed my mind." If you don't send
/cancel, the pending action expires on its own after 60 seconds.
The auth boundary is the configured TELEGRAM_CHAT_ID. Any message
in that chat is accepted; anything outside it is silently dropped.
There is no per-user allow-list. The operator owns the chat:
- If you DM the bot 1-on-1, the chat is just you. Only you can issue commands.
- If you invite the bot to a group, every member of that group can
issue every command — including
/open,/close, and/factory_reset. Adding the bot to a group is granting that group full control over your gates. Choose group membership with that in mind. - If a third party knows the bot's
@usernameand DMs it directly, the chat ID won't match the configured one, and their messages are dropped before any handler runs. There is no path for "outsider adds the bot to their own chat" to drive a real device.
/pair is still DM-only regardless of how the rest of the chat
is configured. That rule isn't about who's authorized — it's about
which channel the Fernet key transits. A group chat would broadcast
the key to every member at message-receive time, before any
deleteMessage fires.
What an attacker with the token can do:
- Send fake alerts to your chat (impersonate the bot). Detectable if the operator pays attention — fake alerts won't correspond to real gate state — but disruptive.
- Read the chat history visible to the bot. For an alert-only
chat, this is whatever you've already received. For a control
chat where
/pairhappened, it briefly includes the Fernet keys (Telegram retains messages for ~48h even afterdeleteMessage). - Cannot drive the gates unless they can also post into the
configured
TELEGRAM_CHAT_ID. The command channel rejects messages from any other chat. So the token alone is not enough. - Cannot read LoRa traffic. The token gives no path to the gate Fernet keys.
Storage:
- On device:
/var/lib/base_station/base_config.env, mode 0600, owned bybasesetup. ext4, where Unix permissions are actually enforced. - In the build artifact: none. The token is not baked into the golden image; the captive portal writes it on first boot.
- In the manufacturing inventory: none. The factory mints per-device portal passwords and gate Fernet keys, but the operator supplies the Telegram token at setup time.
Rotation: send /revoke to BotFather, get a new token, run
/factory_reset on the base station and re-enter the new token in
the captive portal. There is no in-place rotation that preserves
state today (it's on the roadmap).
Standard HTTPS. The base station verifies the server certificate
against the system CA bundle that Buildroot installed
(/etc/ssl/certs/ca-certificates.crt via ca-certificates). This
requires the system clock to be roughly correct, which on a
freshly-booted Pi means waiting for NTP — see
base_station.py:_wait_for_clock_sync for how that's guaranteed
before the first send attempt.
The fallback _force_ntp_sync speaks UDP/123 directly to bypass a
Buildroot-specific systemd-timesyncd + PrivateTmp interaction —
the timesyncd unit has a drop-in disabling PrivateTmp so the
sandbox can read the operator's /etc/resolv.conf, and the manual
NTP path is a belt-and-braces fallback. See the file header on
base_station.py:_force_ntp_sync for the full explanation.
Telegram has the messages in cleartext on their servers. There is no end-to-end encryption between the base station and your phone for bot traffic. (Telegram's "secret chats" are user-to-user only; bots can't participate.) Anyone with read access to your bot's chat — including you, anyone you've shared it with, and Telegram itself — sees all alerts.
For most ranch deployments that's an acceptable trust assumption.
If it isn't for you, swap the Telegram notifier for an
end-to-end-encrypted alternative (Signal CLI, Matrix with E2EE, a
self-hosted ntfy instance). The notifier surface in
base_station.py is one class (TelegramNotifier); replacing it
is a contained change.
- Read
base_config.envand harvest the Telegram token. - Read
events.dband harvest every gate's Fernet key (registered gates live in that database). - This is the strongest argument for either physical security on the base station (the easy thing — it's indoors) or full-disk encryption with a TPM-backed unlock (the hard thing — Pi has no TPM by default).
- Read
/var/lib/gate-client/gate_config.envand harvest that one gate's Fernet key. - The factory provisioner writes the key to
/boot/gate_config.env(FAT32 — the only partition the host laptop can write without ext4 tooling), thengate-config-migrate.servicemoves it to/var/lib/gate-client/gate_config.env(ext4, mode 0600) on first boot and deletes the FAT32 source. So an attacker who pulls the card off a deployed gate sees ext4, not FAT32. - The narrow remaining window is a card pulled between flash and first boot, while the key is still on FAT32. After first boot, the attacker needs ext4 read tools — excludes the casual "plug it into Windows and look around" case.
Connect to the BaseStation_Setup AP again. If it doesn't appear:
- The device joined Wi-Fi and is online but the Telegram credentials
are wrong. SSH in (dev image), check
journalctl -u base-station -b | grep -i telegram. - The device couldn't join Wi-Fi (typo, wrong password, signal too
weak). It will retry the setup AP automatically after 30 minutes
of no upstream connectivity via the watchdog. Or you can SSH in
to a dev image and
nmcli device wifi rescan.
If the AP did reappear with an error banner, the banner text tells you what Telegram rejected: typically HTTP 401 (bad token) or HTTP 403 (you never messaged the bot first; see step 3 of bot setup).
Run /status GATE-XXXX to query the gate directly:
- Gets a reply → gate is alive and the LoRa link works; the issue
is either the gate sensor wiring or the gate's
last_seqlagging. Pull the gate's SD card and checkjournalctl -u gate-client. - "did not answer the challenge" → gate is offline, out of LoRa range, or the gate radio failed.
- "base-side problem" → the base's LoRa serial port is broken.
Check wiring; check
journalctl -u base-station -b | grep -i lora.
From any machine that can reach api.telegram.org:
curl -s "https://api.telegram.org/bot<TOKEN>/sendMessage" \
--data-urlencode "chat_id=<CHAT_ID>" \
--data-urlencode "text=test"If that arrives in your chat, your TOKEN and CHAT_ID are
correct. If the base station is still silent, the problem is on the
device (clock, TLS, Wi-Fi, the long-poll thread).
On a dev image:
sqlite3 /var/lib/base_station/events.db <<'EOF'
.headers on
SELECT gate_id, name, last_seq, registered_at FROM registered_gates;
SELECT timestamp, gate_id, message FROM gate_events ORDER BY id DESC LIMIT 20;
EOFIf you can SSH to the device, the most useful single-command diagnostic is:
systemctl status base-station base-provision ranch-wifi-watchdog \
| head -80
journalctl -u base-station -b --no-pager | tail -60
nmcli device status
ip route
dateThat tells you whether the right service is running, what it last logged, whether Wi-Fi is up, whether the route to the internet exists, and whether the clock is sane. 90% of "the bot is silent" cases resolve at exactly one of those checks.
- USER_GUIDE.md — end-to-end operator journey from unboxing to daily use.
- BUILDING.md — how to build the OS images, flash devices, and run the test suite.
- ../tests/README.md — what the unit test suite covers and how to add a new test.