Skip to content

Commit 1b1b179

Browse files
authored
Merge pull request #4 from badgerloop-software/codex/telemetry-influx-writer-contract
Document Influx telemetry writer contract
2 parents 9e4bd6b + 3d3b872 commit 1b1b179

23 files changed

Lines changed: 1251 additions & 481 deletions

Makefile

Lines changed: 28 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,28 @@
1-
CC ?= cc
2-
CFLAGS ?= -std=c11 -O2 -Wall -Wextra -Wpedantic
3-
CFLAGS += -Isrc -Ithird_party
4-
5-
SRC = \
6-
src/main.c \
7-
src/config.c \
8-
src/influx.c \
9-
src/format_loader.c \
10-
src/can_reader.c \
11-
src/decoder.c \
12-
src/encoder.c \
13-
src/db_watcher.c \
14-
src/writer.c \
15-
src/serial_radio.c \
16-
third_party/cJSON.c
17-
18-
TARGET = can_telem
19-
LDFLAGS ?= -lcurl -lsqlite3 -lpthread -lm
20-
21-
$(TARGET): $(SRC)
22-
$(CC) $(CFLAGS) $(SRC) -o $@ $(LDFLAGS)
23-
24-
clean:
25-
rm -f $(TARGET)
26-
27-
.PHONY: clean
1+
CC ?= cc
2+
CFLAGS ?= -std=c11 -O2 -Wall -Wextra -Wpedantic
3+
CFLAGS += -Isrc -Ithird_party
4+
5+
SRC = \
6+
src/main.c \
7+
src/config.c \
8+
src/influx.c \
9+
src/format_loader.c \
10+
src/can_reader.c \
11+
src/decoder.c \
12+
src/encoder.c \
13+
src/db_watcher.c \
14+
src/writer.c \
15+
src/serial_radio.c \
16+
src/gnss_reader.c \
17+
third_party/cJSON.c
18+
19+
TARGET = can_telem
20+
LDFLAGS ?= -lcurl -lsqlite3 -lpthread -lm
21+
22+
$(TARGET): $(SRC)
23+
$(CC) $(CFLAGS) $(SRC) -o $@ $(LDFLAGS)
24+
25+
clean:
26+
rm -f $(TARGET)
27+
28+
.PHONY: clean

README.md

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,15 @@ A lightweight C daemon for Raspberry Pi that reads raw CAN frames from a SocketC
88
| **InfluxDB** | Batches samples and uploads to InfluxDB Cloud on a configurable interval |
99
| **Serial radio** | Periodically serializes the latest value of every active signal and writes it to a UART radio (e.g. RFD900A) for wireless ground-station reception |
1010

11+
The production mobile app reads the InfluxDB sink as long/tag telemetry:
12+
13+
```text
14+
telemetry_snapshot,signal=<name> value=<number>
15+
```
16+
17+
In that contract, the telemetry name comes from the `signal` tag and the
18+
numeric reading comes from the `value` field.
19+
1120
---
1221

1322
## Architecture
@@ -59,9 +68,69 @@ flowchart TD
5968
|-----------|---------|
6069
| CAN hat | MCP2515 on `can0`, 500 kbps |
6170
| USB log drive | `ext4` mounted at `/mnt/usb` |
71+
| RTC module | DS3231 on I2C (`dtoverlay=i2c-rtc,ds3231` in `/boot/firmware/config.txt`) |
6272
| LTE modem | Quectel EG25-G on `/dev/ttyUSB1–4`; NTP via `systemd-timesyncd` for accurate timestamps |
6373
| Serial radio | RFD900A on `/dev/ttyUSB0` (Silicon Labs CP2102); 115200 baud, NET\_ID 420 |
6474

75+
### Real-time clock (DS3231)
76+
77+
Telemetry timestamps come from `CLOCK_REALTIME`. On the driverio Pi, a DS3231 RTC keeps time when the vehicle is offline and seeds the system clock on boot.
78+
79+
Timekeeping flow:
80+
81+
1. **Boot:** kernel reads `/dev/rtc0` and sets system time.
82+
2. **Online:** `systemd-timesyncd` syncs system time over NTP (LTE).
83+
3. **Write-back:** system time is copied to the RTC hourly and on shutdown so the module stays accurate between power cycles.
84+
85+
Install the RTC sync units shipped in this repo:
86+
87+
```bash
88+
sudo cp deploy/rtc-sync.service deploy/rtc-sync.timer deploy/rtc-sync-shutdown.service /etc/systemd/system/
89+
sudo systemctl daemon-reload
90+
sudo systemctl enable --now systemd-timesyncd rtc-sync.timer rtc-sync-shutdown.service
91+
sudo hwclock --systohc --utc
92+
```
93+
94+
Verify:
95+
96+
```bash
97+
timedatectl status # expect: System clock synchronized: yes
98+
sudo hwclock --show --utc # should match date -u within ~1 second
99+
```
100+
101+
If the RTC has never been set (or lost its battery), set system time first, then write it to the hardware clock:
102+
103+
```bash
104+
sudo timedatectl set-time "2026-06-12 14:00:00"
105+
sudo hwclock --systohc --utc
106+
```
107+
108+
### Internet connectivity (WiFi first, LTE fallback)
109+
110+
InfluxDB uploads require internet at boot. The Pi prefers any saved WiFi profile and falls back to the Quectel EG25-G LTE modem when no known network is in range.
111+
112+
Install the connectivity service:
113+
114+
```bash
115+
sudo cp deploy/network-connect.default /etc/default/network-connect
116+
sudo cp deploy/network-connect.service /etc/systemd/system/
117+
chmod +x deploy/network-connect.sh
118+
sudo nmcli connection modify lte connection.autoconnect no
119+
sudo systemctl disable --now sc2-lte.service 2>/dev/null || true
120+
sudo systemctl daemon-reload
121+
sudo systemctl enable --now network-connect.service
122+
```
123+
124+
Saved WiFi profiles (`nmcli connection show`) are tried automatically. LTE uses APN `fast.t-mobile.com` (Tello/T-Mobile) and can be overridden in `/etc/default/network-connect`.
125+
126+
Verify:
127+
128+
```bash
129+
systemctl status network-connect.service
130+
ip route show default
131+
curl -s -o /dev/null -w "%{http_code}\n" https://us-east-1-1.aws.cloud2.influxdata.com/health
132+
```
133+
65134
### Bring up CAN interface
66135

67136
```bash
@@ -111,11 +180,13 @@ format_file = /home/sunpi/can-telem-cloud/sc-data-format/format.json
111180
output_dir = /mnt/usb
112181

113182
# ── InfluxDB Cloud (optional) ───────────────────────────────────────
183+
influx_enabled = true
114184
influx_url = https://us-east-1-1.aws.cloud2.influxdata.com
115185
influx_org = your-org
116186
influx_bucket = telemetry
117187
influx_token = your-token
118-
influx_flush_interval = 5
188+
influx_upload_interval_ms = 1000
189+
influx_measurement = telemetry_snapshot
119190

120191
# ── Serial radio (optional) ─────────────────────────────────────────
121192
radio_enabled = true
@@ -124,6 +195,13 @@ radio_baud = 115200
124195
radio_flush_interval_ms = 1000
125196
```
126197

198+
The default InfluxDB measurement is `telemetry_snapshot`, which matches the
199+
mobile app. Each uploaded reading is written as:
200+
201+
```text
202+
telemetry_snapshot,signal=<signal_name> value=<number>
203+
```
204+
127205
### CLI flags
128206

129207
```
@@ -222,11 +300,13 @@ can-telem-cloud/
222300
│ ├── writer.[ch] — CSV append sink
223301
│ ├── influx.[ch] — InfluxDB Cloud batch upload sink
224302
│ ├── serial_radio.[ch] — UART radio sink (RFD900A / CP2102)
303+
│ ├── gnss_reader.[ch] — GNSS cache reader (lat/lon/elev injection)
225304
│ ├── encoder.[ch] — CAN signal encoder (for TX path)
226305
│ └── db_watcher.[ch] — SQLite DB watcher for TX signals
227306
├── third_party/
228307
│ └── cJSON.[ch] — JSON parser
229308
├── sc-data-format/ — git submodule containing format.json and format_exp.md
309+
├── deploy/ — systemd unit files (RTC sync, network connect)
230310
├── can_telem.conf.example
231311
└── Makefile
232312
```

can_telem.conf.example

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,11 @@ can_interface = can0
1818
# influx_token = paste-token-here
1919
# Or omit influx_token and run: export INFLUX_TOKEN=...
2020
# influx_upload_interval_ms = 1000
21-
# influx_measurement = can_telem
21+
# Production mobile app contract:
22+
# telemetry_snapshot,signal=<name> value=<number>
23+
# Keep this unset to use the default telemetry_snapshot measurement, or set it
24+
# explicitly for clarity.
25+
# influx_measurement = telemetry_snapshot
2226

2327
# --- Optional: DB-driven CAN transmit (SQLite) ---
2428
# Enable this to poll values from a SQLite DB and transmit updates on CAN for
@@ -42,3 +46,17 @@ can_interface = can0
4246
# radio_device = /dev/ttyUSB0
4347
# radio_baud = 9600
4448
# radio_flush_interval_ms = 1000
49+
50+
# --- Optional: signal role filters (without editing sc-data-format) ---
51+
# Comma-separated signal names. Leave empty/missing to allow all.
52+
# rx_signals = pack_voltage,pack_current,soc,lat,lon,elev
53+
# tx_signals = torque_cmd,drive_mode
54+
55+
# --- Optional: GNSS merge ---
56+
# Pull lat/lon/elev from /run/can_telem/gnss.json (written by gnss_service.py)
57+
# gnss_enabled = true
58+
# gnss_poll_interval_ms = 1000
59+
# gnss_cache_path = /run/can_telem/gnss.json
60+
# gnss_lat_signal = lat
61+
# gnss_lon_signal = lon
62+
# gnss_elev_signal = elev

deploy/can-telem-gnss.service

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[Unit]
2+
Description=can-telem GNSS cache service
3+
After=network-online.target ModemManager.service
4+
Wants=network-online.target
5+
6+
[Service]
7+
Type=simple
8+
ExecStart=/usr/bin/python3 /home/sunpi/can-telem-cloud/tools/gnss_service.py
9+
Restart=always
10+
RestartSec=2
11+
User=root
12+
Group=root
13+
14+
[Install]
15+
WantedBy=multi-user.target

deploy/network-connect.default

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Copy to /etc/default/network-connect to override defaults.
2+
LTE_APN=fast.t-mobile.com
3+
LTE_CONN_NAME=lte
4+
WIFI_WAIT_SEC=45
5+
LTE_WAIT_SEC=60
6+
PING_TARGET=1.1.1.1
7+
WIFI_METRIC=100
8+
LTE_METRIC=600

deploy/network-connect.service

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[Unit]
2+
Description=Prefer saved WiFi, fall back to LTE for internet
3+
After=NetworkManager.service ModemManager.service
4+
Before=network-online.target
5+
Wants=NetworkManager.service ModemManager.service
6+
7+
[Service]
8+
Type=oneshot
9+
EnvironmentFile=-/etc/default/network-connect
10+
ExecStart=/home/sunpi/can-telem-cloud/deploy/network-connect.sh
11+
RemainAfterExit=yes
12+
13+
[Install]
14+
WantedBy=multi-user.target

0 commit comments

Comments
 (0)