Skip to content

Commit d5c1d48

Browse files
authored
Merge pull request perdo1305#10 from perdo1305/worktree-foxglove-bag-upload
Worktree foxglove bag upload
2 parents 343a799 + 7fac435 commit d5c1d48

8 files changed

Lines changed: 443 additions & 2 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ install/
2626
log/
2727

2828
.vscode/
29+
deploy/.env
2930

3031
# Superpowers subagent-driven-development scratch (progress ledger, etc.)
3132
.superpowers/

deploy/.env.example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Foxglove Data Platform API key. Create one from your Foxglove org's
2+
# Settings -> API Keys page (needs the recordings-upload capability).
3+
# Copy this file to .env in the same directory and fill in the value —
4+
# deploy/.env is gitignored and never committed.
5+
FOXGLOVE_API_KEY=

deploy/foxglove-upload.service

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Install on the car (as root):
2+
# cp deploy/foxglove-upload.service deploy/foxglove-upload.timer /etc/systemd/system/
3+
# systemctl daemon-reload
4+
# systemctl enable --now foxglove-upload.timer
5+
[Unit]
6+
Description=Upload finalized precharge bags to Foxglove
7+
After=network-online.target
8+
Wants=network-online.target
9+
10+
[Service]
11+
Type=oneshot
12+
User=lart2026
13+
Group=lart2026
14+
WorkingDirectory=/home/lart2026/GIT/data_station/deploy
15+
ExecStart=/usr/bin/python3 /home/lart2026/GIT/data_station/deploy/foxglove_upload.py

deploy/foxglove-upload.timer

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[Unit]
2+
Description=Run foxglove-upload.service every 2 minutes
3+
4+
[Timer]
5+
OnBootSec=2min
6+
OnUnitActiveSec=2min
7+
Unit=foxglove-upload.service
8+
9+
[Install]
10+
WantedBy=timers.target

deploy/foxglove_upload.py

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
#!/usr/bin/env python3
2+
"""Uploads finalized precharge bag sessions to Foxglove when online.
3+
4+
Run every 2 minutes by foxglove-upload.timer (systemd). Each run:
5+
1. Loads FOXGLOVE_API_KEY from deploy/.env (skips if unset).
6+
2. Checks api.foxglove.dev is reachable (skips if not).
7+
3. Reads bag_dir from src/lart_bringup/config/rpi_config.yaml — the same
8+
directory bag_recorder writes sessions into.
9+
4. Uploads any session that has metadata.yaml (finalized by bag_recorder)
10+
and no .uploaded marker yet.
11+
12+
Never exits nonzero for expected/transient conditions (offline, no key,
13+
upload failure) — only for genuine local bugs — so systemd doesn't
14+
accumulate failed-unit spam for states that just mean "try again later".
15+
"""
16+
17+
import socket
18+
import sys
19+
from datetime import datetime
20+
from pathlib import Path
21+
from typing import List, Optional
22+
23+
import requests
24+
import yaml
25+
from foxglove.client import Client
26+
27+
SCRIPT_DIR = Path(__file__).resolve().parent
28+
REPO_ROOT = SCRIPT_DIR.parent
29+
ENV_PATH = SCRIPT_DIR / ".env"
30+
RPI_CONFIG_PATH = REPO_ROOT / "src" / "lart_bringup" / "config" / "rpi_config.yaml"
31+
32+
FOXGLOVE_HOST = "api.foxglove.dev"
33+
FOXGLOVE_PORT = 443
34+
UPLOADED_MARKER = ".uploaded"
35+
36+
37+
def load_api_key(env_path: Path) -> Optional[str]:
38+
if not env_path.is_file():
39+
return None
40+
for line in env_path.read_text().splitlines():
41+
line = line.strip()
42+
if not line or line.startswith("#") or "=" not in line:
43+
continue
44+
key, _, value = line.partition("=")
45+
if key.strip() != "FOXGLOVE_API_KEY":
46+
continue
47+
value = value.strip().strip('"').strip("'")
48+
return value or None
49+
return None
50+
51+
52+
def check_connectivity(
53+
host: str = FOXGLOVE_HOST, port: int = FOXGLOVE_PORT, timeout: float = 5.0
54+
) -> bool:
55+
try:
56+
with socket.create_connection((host, port), timeout=timeout):
57+
return True
58+
except OSError:
59+
return False
60+
61+
62+
def read_bag_dir(config_path: Path) -> Path:
63+
with config_path.open() as f:
64+
config = yaml.safe_load(f)
65+
raw = config["bag_recorder"]["ros__parameters"]["bag_dir"]
66+
return Path(raw).expanduser()
67+
68+
69+
def find_mcap_file(session_dir: Path) -> Optional[Path]:
70+
mcaps = sorted(session_dir.glob("*.mcap"))
71+
if len(mcaps) != 1:
72+
return None
73+
return mcaps[0]
74+
75+
76+
def find_pending_sessions(bag_dir: Path) -> List[Path]:
77+
if not bag_dir.is_dir():
78+
return []
79+
pending = []
80+
for session in sorted(bag_dir.iterdir()):
81+
if not session.is_dir():
82+
continue
83+
if not (session / "metadata.yaml").exists():
84+
continue
85+
if (session / UPLOADED_MARKER).exists():
86+
continue
87+
pending.append(session)
88+
return pending
89+
90+
91+
def upload_session(client: Client, session_dir: Path) -> bool:
92+
mcap_path = find_mcap_file(session_dir)
93+
if mcap_path is None:
94+
print(
95+
f"{session_dir.name}: expected exactly one .mcap file, skipping",
96+
file=sys.stderr,
97+
)
98+
return False
99+
100+
try:
101+
with mcap_path.open("rb") as f:
102+
result = client.upload_data(filename=mcap_path.name, data=f, key=session_dir.name)
103+
except Exception as exc: # noqa: BLE001 — one session's upload error must not crash the loop
104+
print(f"{session_dir.name}: upload error: {exc}", file=sys.stderr)
105+
return False
106+
107+
if not (200 <= result["code"] < 300):
108+
print(
109+
f"{session_dir.name}: upload failed ({result['code']}): {result['text']}",
110+
file=sys.stderr,
111+
)
112+
return False
113+
114+
try:
115+
(session_dir / UPLOADED_MARKER).write_text(datetime.now().isoformat() + "\n")
116+
except OSError as exc:
117+
print(
118+
f"{session_dir.name}: uploaded but failed to write marker: {exc}",
119+
file=sys.stderr,
120+
)
121+
return False
122+
123+
print(f"{session_dir.name}: uploaded.")
124+
return True
125+
126+
127+
def main() -> int:
128+
api_key = load_api_key(ENV_PATH)
129+
if not api_key:
130+
print("FOXGLOVE_API_KEY not set — skipping.", file=sys.stderr)
131+
return 0
132+
133+
if not check_connectivity():
134+
print("Foxglove unreachable — skipping.")
135+
return 0
136+
137+
try:
138+
bag_dir = read_bag_dir(RPI_CONFIG_PATH)
139+
except (OSError, yaml.YAMLError, KeyError, TypeError) as exc:
140+
print(f"Failed to read bag_dir from config: {exc}", file=sys.stderr)
141+
return 0
142+
143+
pending = find_pending_sessions(bag_dir)
144+
if not pending:
145+
return 0
146+
147+
client = Client(token=api_key)
148+
for session in pending:
149+
upload_session(client, session)
150+
151+
return 0
152+
153+
154+
if __name__ == "__main__":
155+
sys.exit(main())

0 commit comments

Comments
 (0)