f15ac69925
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>
161 lines
6.5 KiB
Bash
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
|
|
}
|