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
5.7 KiB
Bash
Executable File
161 lines
5.7 KiB
Bash
Executable File
#!/bin/sh
|
|
# daemon.sh - busrouter control loop.
|
|
# Sources all libs and runs the scoring/balancing/telemetry cycle.
|
|
# Helper functions are defined in global scope so tests can load this file
|
|
# without starting the loop. The loop only runs via busrouter_main().
|
|
|
|
CONFIG=/etc/busrouter/busrouter.conf
|
|
[ -f "$CONFIG" ] && . "$CONFIG"
|
|
|
|
LIB_DIR="${LIB_DIR:-/usr/lib/busrouter}"
|
|
|
|
. "${LIB_DIR}/lib-decide.sh"
|
|
. "${LIB_DIR}/lib-score.sh"
|
|
. "${LIB_DIR}/metrics-net.sh"
|
|
. "${LIB_DIR}/speedtest.sh"
|
|
. "${LIB_DIR}/modem-sim.sh"
|
|
. "${LIB_DIR}/starlink.sh"
|
|
. "${LIB_DIR}/wan-mwan.sh"
|
|
. "${LIB_DIR}/telemetry.sh"
|
|
|
|
: "${INTERVAL:=30}"
|
|
: "${CELL_IFACE:=rmnet_mhi0}"
|
|
: "${ETH_IFACE:=eth0}"
|
|
: "${CELL_MBR:=modem_0001}"
|
|
: "${ETH_MBR:=wan}"
|
|
: "${STATE_DIR:=/tmp/busrouter}"
|
|
: "${SCORE_SAMPLES:=3}"
|
|
|
|
# Update score history for one WAN member (keeps last SCORE_SAMPLES scores).
|
|
# Prints current history (space-separated) for use with rolling_avg.
|
|
_score_history_update() {
|
|
local key="$1" new_score="$2"
|
|
local hist_file="${STATE_DIR}/score_history_${key}"
|
|
local hist
|
|
hist=$(cat "$hist_file" 2>/dev/null || echo "50 50 50")
|
|
local new_hist
|
|
new_hist=$(echo "$hist $new_score" | awk -v n="${SCORE_SAMPLES:-3}" '{
|
|
start = (NF > n) ? NF - n + 1 : 1
|
|
for (i = start; i <= NF; i++) printf "%s%s", $i, (i < NF ? " " : "\n")
|
|
}')
|
|
printf '%s\n' "$new_hist" > "$hist_file"
|
|
echo "$new_hist"
|
|
}
|
|
|
|
# Collect net+speed metrics for one WAN and write state files.
|
|
# Usage: _collect_wan <linux_iface> <mwan3_mbr> [rsrp_dBm]
|
|
# Writes: STATE_DIR/metric_<mbr>, STATE_DIR/score_<mbr>
|
|
# Prints rolling-average score on stdout.
|
|
_collect_wan() {
|
|
local iface="$1" mbr="$2" rsrp="${3:-}"
|
|
mkdir -p "$STATE_DIR"
|
|
|
|
# Net probe: lat_ms jitter_ms loss_pct (fail-open: use worst-case)
|
|
local net_out lat jitter loss
|
|
net_out=$(net_probe_iface "$iface" 2>/dev/null) || net_out="0 0 100"
|
|
lat=$(echo "$net_out" | awk '{print $1}'); : "${lat:=0}"
|
|
jitter=$(echo "$net_out" | awk '{print $2}'); : "${jitter:=0}"
|
|
loss=$(echo "$net_out" | awk '{print $3}'); : "${loss:=100}"
|
|
|
|
# Speed: use cached value unless a new test is due
|
|
local dl ul speed_out
|
|
if speed_test_due "$iface" 2>/dev/null; then
|
|
speed_out=$(speed_test_iface "$iface" 2>/dev/null) || speed_out="0 0"
|
|
dl=$(echo "$speed_out" | awk '{print $1}'); : "${dl:=0}"
|
|
ul=$(echo "$speed_out" | awk '{print $2}'); : "${ul:=0}"
|
|
printf '%s %s\n' "$dl" "$ul" > "${STATE_DIR}/speed_cache_${iface}"
|
|
else
|
|
speed_out=$(cat "${STATE_DIR}/speed_cache_${iface}" 2>/dev/null || echo "0 0")
|
|
dl=$(echo "$speed_out" | awk '{print $1}'); : "${dl:=0}"
|
|
ul=$(echo "$speed_out" | awk '{print $2}'); : "${ul:=0}"
|
|
fi
|
|
|
|
# Score composite + rolling average
|
|
local score hist avg
|
|
if [ -n "$rsrp" ]; then
|
|
score=$(score_composite "$lat" "$dl" "$ul" "$jitter" "$loss" "$rsrp" 2>/dev/null) || score=0
|
|
printf '%s %s %s %s %s %s\n' "$lat" "$dl" "$ul" "$jitter" "$loss" "$rsrp" > "${STATE_DIR}/metric_${mbr}"
|
|
else
|
|
score=$(score_composite "$lat" "$dl" "$ul" "$jitter" "$loss" 2>/dev/null) || score=0
|
|
printf '%s %s %s %s %s\n' "$lat" "$dl" "$ul" "$jitter" "$loss" > "${STATE_DIR}/metric_${mbr}"
|
|
fi
|
|
: "${score:=0}"
|
|
hist=$(_score_history_update "$mbr" "$score")
|
|
avg=$(rolling_avg $hist 2>/dev/null) || avg=50
|
|
printf '%d\n' "${avg:-50}" > "${STATE_DIR}/score_${mbr}"
|
|
|
|
logger -t busrouter "metrics[$iface]: lat=${lat}ms dl=${dl} ul=${ul} jitter=${jitter}ms loss=${loss}% rsrp=${rsrp:-N/A} score=${score} avg=${avg}"
|
|
echo "${avg:-50}"
|
|
}
|
|
|
|
# One control loop iteration. Exported as a function for bench testing.
|
|
busrouter_cycle() {
|
|
logger -t busrouter -p daemon.debug "cycle start: $(date +%s)"
|
|
|
|
# Cellular signal (fail-open: rsrp stays empty)
|
|
local cell_rsrp sig_out
|
|
cell_rsrp=""
|
|
sig_out=$(modem_signal 2>/dev/null) && {
|
|
cell_rsrp=$(echo "$sig_out" | awk '{print $1}')
|
|
} || logger -t busrouter "daemon: modem_signal unavailable (continuing)"
|
|
|
|
# Per-WAN metrics + rolling scores
|
|
local cell_avg eth_avg
|
|
cell_avg=$(_collect_wan "$CELL_IFACE" "$CELL_MBR" "$cell_rsrp" 2>/dev/null) || cell_avg=50
|
|
eth_avg=$(_collect_wan "$ETH_IFACE" "$ETH_MBR" 2>/dev/null) || eth_avg=50
|
|
|
|
# Starlink status
|
|
local sl_out
|
|
sl_out=$(starlink_status 2>/dev/null) && {
|
|
printf '%s\n' "$sl_out" > "${STATE_DIR}/starlink_last"
|
|
} || logger -t busrouter -p daemon.debug "daemon: starlink_status unavailable"
|
|
|
|
# Weight decision (mwan_set_weight has internal hysteresis guard)
|
|
local new_cell_w new_eth_w
|
|
new_cell_w=$(wan_weight "$cell_avg" "$eth_avg" 2>/dev/null) || new_cell_w=50
|
|
new_eth_w=$(wan_weight "$eth_avg" "$cell_avg" 2>/dev/null) || new_eth_w=50
|
|
mwan_set_weight "$CELL_MBR" "$new_cell_w" 2>/dev/null || true
|
|
mwan_set_weight "$ETH_MBR" "$new_eth_w" 2>/dev/null || true
|
|
|
|
# Dead-gateway check (cellular only; Starlink manages its own uplink)
|
|
if ! gw_probe_iface "$CELL_IFACE" 2>/dev/null; then
|
|
logger -t busrouter "daemon: cellular gateway dead, cycling $CELL_MBR"
|
|
ifdown "$CELL_MBR" 2>/dev/null; ifup "$CELL_MBR" 2>/dev/null
|
|
fi
|
|
|
|
# SIM auto-switch
|
|
[ -n "$cell_rsrp" ] && sim_check_and_switch "$cell_rsrp" 2>/dev/null || true
|
|
|
|
# GPS
|
|
local gps_out
|
|
gps_out=$(gps_fix 2>/dev/null) && {
|
|
printf '%s\n' "$gps_out" > "${STATE_DIR}/gps_last"
|
|
} || true
|
|
|
|
# SIM slot (for telemetry)
|
|
local sim_slot
|
|
sim_slot=$(modem_sim_slot 2>/dev/null) && {
|
|
printf '%s\n' "$sim_slot" > "${STATE_DIR}/sim_slot"
|
|
} || true
|
|
|
|
# Telemetry
|
|
telemetry_send 2>/dev/null || true
|
|
|
|
logger -t busrouter -p daemon.debug "cycle done"
|
|
}
|
|
|
|
# Main loop — runs only when this file is executed directly (not sourced).
|
|
busrouter_main() {
|
|
mkdir -p "$STATE_DIR"
|
|
local ver
|
|
ver=$(cat /etc/busrouter/version 2>/dev/null || echo "0.2.0")
|
|
logger -t busrouter "daemon started (v${ver})"
|
|
while :; do
|
|
busrouter_cycle
|
|
sleep "${INTERVAL:-30}"
|
|
done
|
|
}
|
|
|
|
# Guard: only start loop when executed directly, not when sourced for testing.
|
|
case "${0##*/}" in daemon.sh) busrouter_main ;; esac
|