Files
kit-busrouter/package/kit-busrouter/files/usr/lib/busrouter/telemetry.sh
T
allen f15ac69925 feat: kit-busrouter v2.0 — complete Synology + GL fleet router platform
Includes:
- package/ GL-XE3000 kit-busrouter (opkg)
- scripts/provision.sh (GL) and provision-synology.sh (Synology)
- syno-balance/ — new WAN balancer replacing aiwanbal (SmartWAN adapter)
- kit-connect/ — unified connectivity SPK (Tailscale + reverse SSH)
- docs/deployment/synology-rt2600ac-checklist.md — 62-point checklist
- docs/provisioning/device-identity.md — fleet identity spec
- docs/pilot/checklist.md — field pilot validation
- x4078_20260721.dss — reference config backup

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 00:07:14 +00:00

161 lines
6.5 KiB
Bash

#!/bin/sh
# telemetry.sh - GPS read and telemetry payload builder.
#
# Config (GPS):
# MODEM_BUS gl_modem -B arg (default: 1-1.2)
# MODEM_AT_CMD AT command binary (default: gl_modem)
#
# Config (telemetry POST):
# TELEMETRY_HUB POST endpoint (default: http://10.88.0.1:8080/api/telemetry)
# TELEMETRY_INTERVAL seconds between sends when used from daemon (default: 60)
# TELEMETRY_BUFFER_DIR disk buffer for failed POSTs (default: /tmp/busrouter/telemetry-buf)
#
# Public API:
# gps_fix echo 'lat lon fix_quality'; exit 1 if no fix
# telemetry_collect emit JSON payload to stdout from state files
# telemetry_flush retry all buffered payloads; returns 1 if hub still down
# telemetry_send collect + post; buffer on failure; never blocks caller
LIB_DIR="${LIB_DIR:-/usr/lib/busrouter}"
: "${MODEM_BUS:=1-1.2}"
: "${MODEM_AT_CMD:=gl_modem}"
: "${TELEMETRY_HUB:=http://10.88.0.1:8080/api/telemetry}"
: "${TELEMETRY_INTERVAL:=60}"
: "${TELEMETRY_BUFFER_DIR:=/tmp/busrouter/telemetry-buf}"
_TELEM_SEQ=0
# Parse AT+QGPSLOC=2 response -> 'lat lon fix_quality'.
# Format: +QGPSLOC: <utc>,<lat>,<lon>,<hdop>,<alt>,<fix>,...
# fix: 2=2D, 3=3D (1 or absent = no fix)
_gps_parse() {
awk '
/^\+QGPSLOC:/ {
sub(/^\+QGPSLOC: [^,]+,/, "")
n = split($0, a, ",")
lat = a[1]; lon = a[2]; fix = a[5]+0
if (fix >= 2) { printf "%s %s %d\n", lat, lon, fix; found=1; exit 0 }
}
END { if (!found) exit 1 }
'
}
# Read GPS position from modem via AT+QGPSLOC=2.
# Emits 'lat lon fix_quality' on success; exit 1 on no-fix or modem error.
gps_fix() {
local result
result=$(timeout 5 "$MODEM_AT_CMD" -B "$MODEM_BUS" AT AT+QGPSLOC=2 2>/dev/null | _gps_parse)
if [ -n "$result" ]; then
logger -t busrouter -p daemon.debug "gps_fix: $result"
echo "$result"; return 0
fi
logger -t busrouter "gps_fix: no fix"
return 1
}
# Build telemetry JSON from state files written by the control loop.
# Escape a string value for JSON (backslash and double-quote only).
_json_str() { printf '%s' "$1" | sed 's/\/\\/g; s/"/\\"/g'; }
# State files (in /tmp/busrouter/):
# gps_last "lat lon fix" (written by gps_fix caller)
# metric_modem_0001 "lat_ms dl ul jitter loss rsrp"
# metric_wan "lat_ms dl ul jitter loss"
# score_modem_0001 integer 0-100
# score_wan integer 0-100
# starlink_last "lat_ms dl_bps obstructed outage"
# sim_slot "1" or "2"
telemetry_collect() {
local now device_id uptime_s version
now=$(date +%s)
device_id=$(cat /etc/busrouter/device-id 2>/dev/null || hostname)
uptime_s=$(awk '{printf "%d", $1}' /proc/uptime 2>/dev/null)
version=$(cat /etc/busrouter/version 2>/dev/null || echo "dev")
local gps_lat gps_lon gps_fix
[ -f /tmp/busrouter/gps_last ] && read -r gps_lat gps_lon gps_fix < /tmp/busrouter/gps_last || true
local cell_score cell_lat cell_dl cell_ul cell_jitter cell_loss cell_rsrp
cell_score=$(cat /tmp/busrouter/score_modem_0001 2>/dev/null)
[ -f /tmp/busrouter/metric_modem_0001 ] && read -r cell_lat cell_dl cell_ul cell_jitter cell_loss cell_rsrp < /tmp/busrouter/metric_modem_0001 || true
local wan_score wan_lat wan_dl wan_ul wan_jitter wan_loss
wan_score=$(cat /tmp/busrouter/score_wan 2>/dev/null)
[ -f /tmp/busrouter/metric_wan ] && read -r wan_lat wan_dl wan_ul wan_jitter wan_loss < /tmp/busrouter/metric_wan || true
local sl_lat sl_dl sl_obs sl_outage
[ -f /tmp/busrouter/starlink_last ] && read -r sl_lat sl_dl sl_obs sl_outage < /tmp/busrouter/starlink_last || true
local sim_slot
sim_slot=$(cat /tmp/busrouter/sim_slot 2>/dev/null)
# sel_primary: written by the kit-balance engine to indicate the active WAN member.
local sel_primary
sel_primary=$(cat /tmp/busrouter/sel_primary 2>/dev/null)
local esc_id esc_ver esc_sel
esc_id=$(_json_str "$device_id")
esc_ver=$(_json_str "$version")
esc_sel=$(_json_str "$sel_primary")
printf '{"device_id":"%s","timestamp":%d,"uptime":%d,"version":"%s","sel_primary":"%s","gps":{"lat":%s,"lon":%s,"fix":%s},"wan":{"modem_0001":{"score":%s,"latency_ms":%s,"dl_mbps":%s,"ul_mbps":%s,"jitter_ms":%s,"loss_pct":%s,"rsrp_dbm":%s},"wan":{"score":%s,"latency_ms":%s,"dl_mbps":%s,"ul_mbps":%s,"jitter_ms":%s,"loss_pct":%s}},"sim_slot":%s,"starlink":{"latency_ms":%s,"dl_bps":%s,"obstructed":%s,"outage":%s}}\n' \
"${esc_id}" "${now:-0}" "${uptime_s:-0}" "${esc_ver}" "${esc_sel}" \
"${gps_lat:-null}" "${gps_lon:-null}" "${gps_fix:-null}" \
"${cell_score:-0}" "${cell_lat:-null}" "${cell_dl:-null}" "${cell_ul:-null}" "${cell_jitter:-null}" "${cell_loss:-null}" "${cell_rsrp:-null}" \
"${wan_score:-0}" "${wan_lat:-null}" "${wan_dl:-null}" "${wan_ul:-null}" "${wan_jitter:-null}" "${wan_loss:-null}" \
"${sim_slot:-null}" "${sl_lat:-null}" "${sl_dl:-null}" "${sl_obs:-0}" "${sl_outage:-0}"
}
# POST a JSON file to the hub. Returns 0 on HTTP 2xx, 1 on any failure.
_telemetry_try_post() {
local file="$1"
local code
code=$(curl -sf -X POST -H "Content-Type: application/json" \
--data-binary @"$file" -o /dev/null -w "%{http_code}" \
--max-time 10 "$TELEMETRY_HUB" 2>/dev/null)
case "$code" in 2*) return 0 ;; *) return 1 ;; esac
}
# Retry all buffered payloads in timestamp order.
# Stops on first failure to avoid hammering a down hub.
telemetry_flush() {
[ -d "$TELEMETRY_BUFFER_DIR" ] || return 0
local f
for f in "$TELEMETRY_BUFFER_DIR"/*.json; do
[ -f "$f" ] || continue
if _telemetry_try_post "$f"; then
rm -f "$f"
logger -t busrouter -p daemon.debug "telemetry_flush: sent $(basename "$f")"
else
logger -t busrouter "telemetry_flush: hub unreachable, stopping"
return 1
fi
done
}
# Collect metrics, flush old buffer, POST current payload.
# On POST failure: saves payload to TELEMETRY_BUFFER_DIR.
# Never blocks the caller on hub downtime.
telemetry_send() {
local payload
payload=$(telemetry_collect)
telemetry_flush 2>/dev/null || true
mkdir -p "$TELEMETRY_BUFFER_DIR"
_TELEM_SEQ=$(( _TELEM_SEQ + 1 ))
local tmpfile="${TELEMETRY_BUFFER_DIR}/$(date +%s)_$$_${_TELEM_SEQ}.json"
# Cap buffer to 60 files to protect tmpfs
buf_count=$(ls "$TELEMETRY_BUFFER_DIR"/*.json 2>/dev/null | wc -l)
if [ "${buf_count:-0}" -ge 60 ]; then
oldest=$(ls "$TELEMETRY_BUFFER_DIR"/*.json 2>/dev/null | sort | head -n 1)
[ -f "$oldest" ] && rm -f "$oldest"
fi
printf '%s\n' "$payload" > "$tmpfile"
if _telemetry_try_post "$tmpfile"; then
rm -f "$tmpfile"
logger -t busrouter -p daemon.debug "telemetry_send: posted ok"
else
logger -t busrouter "telemetry_send: hub unreachable, buffered $(basename "$tmpfile")"
fi
}