feat: smart WAN role detection, aiwanbal integration, speedtest support
- Auto-detect cellular vs fiber/Starlink regardless of physical port: aiwanbal modem_type → Eyeride subnet (192.168.10.x) → gateway patterns - Integrate aiwanbal/SmartAiBalancer state as primary data source when available (fresh <120s); fall back to self-collected ping metrics - Add carrier detection: AT&T for fiber gateways, Eyeride for cellular - Add run_speedtest() with speedtest-cli/iperf3/curl download fallback - Extract jitter (mdev) from ping output for richer metrics - Consistent canonical GL format: modem_0001=cellular, wan=ethernet/fiber Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -5,41 +5,25 @@
|
|||||||
# from an external Eyeride device (192.168.10.1:8080) which provides GPS and
|
# from an external Eyeride device (192.168.10.1:8080) which provides GPS and
|
||||||
# signal metrics via OpenWrt ubus JSON-RPC.
|
# signal metrics via OpenWrt ubus JSON-RPC.
|
||||||
#
|
#
|
||||||
|
# Smart WAN role detection: cellular can be on ANY port (WAN1 or WAN2).
|
||||||
|
# The script auto-detects based on:
|
||||||
|
# 1. aiwanbal/SmartAiBalancer modem_type fields (eyeride/peplink/zte = cellular)
|
||||||
|
# 2. Eyeride reachability at 192.168.10.1
|
||||||
|
# 3. Gateway + carrier pattern analysis
|
||||||
|
# This ensures correct mapping regardless of physical port assignment.
|
||||||
|
#
|
||||||
|
# Data sources (preferred order):
|
||||||
|
# 1. aiwanbal state files (/tmp/aiwanbal/) — richer scoring + failover decisions
|
||||||
|
# 2. Self-collected ping metrics (fallback when aiwanbal not installed)
|
||||||
|
#
|
||||||
# GPS fallback chain:
|
# GPS fallback chain:
|
||||||
# 1. Eyeride ubus JSON-RPC (gps / location service) → fix=2 or 3
|
# 1. Eyeride ubus JSON-RPC (gps / location service) → fix=2 or 3
|
||||||
# 2. ip-api.com geolocation by external IP → fix=1 (approximate)
|
# 2. ip-api.com geolocation by external IP → fix=1 (approximate)
|
||||||
#
|
#
|
||||||
# WAN mapping (canonical GL format):
|
|
||||||
# WAN1 (Ethernet/primary uplink) → modem_0001
|
|
||||||
# WAN2 (Eyeride cellular) → wan (with rsrp_dbm/rsrq_db/sinr_db)
|
|
||||||
#
|
|
||||||
# Self-collected metrics (no aiwanbal dependency):
|
|
||||||
# - Ping latency/loss per interface (using -I <iface>)
|
|
||||||
# - Throughput from /sys/class/net/<iface>/statistics/ (bytes delta between cycles)
|
|
||||||
# - Score computed from latency + loss (simple heuristic)
|
|
||||||
# - Eyeride cellular signal via ubus (if available)
|
|
||||||
#
|
|
||||||
# Config (/etc/busrouter/telemetry.conf, sourced as shell):
|
# Config (/etc/busrouter/telemetry.conf, sourced as shell):
|
||||||
# TELEMETRY_HUB POST endpoint (default: http://10.88.0.1:8080/api/telemetry)
|
# TELEMETRY_HUB POST endpoint
|
||||||
# TELEMETRY_BUFFER_DIR disk buffer for failed POSTs (default: /tmp/busrouter/telemetry-buf)
|
# EYERIDE_IP/PORT/USER/PASSWORD Eyeride credentials
|
||||||
# EYERIDE_IP Eyeride OpenWrt IP (default: 192.168.10.1)
|
# WAN1_IFACE/WAN2_IFACE Physical interface names
|
||||||
# EYERIDE_PORT Eyeride ubus port (default: 8080)
|
|
||||||
# EYERIDE_USER Eyeride ubus username (default: benmashborn)
|
|
||||||
# EYERIDE_PASSWORD Eyeride password (required for GPS + signal)
|
|
||||||
# WAN1_IFACE Primary WAN interface (default: eth0)
|
|
||||||
# WAN2_IFACE Secondary/cellular WAN interface (default: eth2)
|
|
||||||
# PING_TARGET Host to ping for latency/loss (default: 8.8.8.8)
|
|
||||||
# STATE_DIR Runtime state directory (default: /tmp/busrouter)
|
|
||||||
#
|
|
||||||
# Public API:
|
|
||||||
# eyeride_gps echo 'lat lon fix'; exit 1 on failure
|
|
||||||
# ipgeo_gps echo 'lat lon fix'; exit 1 on failure
|
|
||||||
# gps_fix try eyeride_gps, fall back to ipgeo_gps
|
|
||||||
# telemetry_collect emit JSON payload to stdout (canonical GL format)
|
|
||||||
# telemetry_flush retry all buffered payloads; return 1 if hub still down
|
|
||||||
# telemetry_send collect + flush + post; buffer on failure
|
|
||||||
#
|
|
||||||
# Cron mode: runs telemetry_send when executed directly.
|
|
||||||
|
|
||||||
# ── Configuration ──────────────────────────────────────────────────────────
|
# ── Configuration ──────────────────────────────────────────────────────────
|
||||||
: "${TELEMETRY_HUB:=http://10.88.0.1:8080/api/telemetry}"
|
: "${TELEMETRY_HUB:=http://10.88.0.1:8080/api/telemetry}"
|
||||||
@@ -51,44 +35,43 @@
|
|||||||
: "${WAN2_IFACE:=eth2}"
|
: "${WAN2_IFACE:=eth2}"
|
||||||
: "${PING_TARGET:=8.8.8.8}"
|
: "${PING_TARGET:=8.8.8.8}"
|
||||||
: "${STATE_DIR:=/tmp/busrouter}"
|
: "${STATE_DIR:=/tmp/busrouter}"
|
||||||
|
: "${AIWANBAL_DIR:=/tmp/aiwanbal}"
|
||||||
|
|
||||||
# Source per-device overrides if present
|
|
||||||
[ -f /etc/busrouter/telemetry.conf ] && . /etc/busrouter/telemetry.conf
|
[ -f /etc/busrouter/telemetry.conf ] && . /etc/busrouter/telemetry.conf
|
||||||
|
|
||||||
_TELEM_SEQ=0
|
_TELEM_SEQ=0
|
||||||
|
|
||||||
# ── Helpers ────────────────────────────────────────────────────────────────
|
# ── Helpers ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
# Escape a string for JSON (backslash + double-quote only).
|
|
||||||
_json_str() { printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'; }
|
_json_str() { printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'; }
|
||||||
|
|
||||||
# BusyBox-compatible float arithmetic via awk.
|
# Read a file, return empty string if missing.
|
||||||
_awk_float() { awk "BEGIN { printf \"%.1f\", $1 }" 2>/dev/null || echo "null"; }
|
_read_file() { cat "$1" 2>/dev/null || true; }
|
||||||
|
|
||||||
|
# Extract a numeric field from JSON.
|
||||||
|
_json_get_num() {
|
||||||
|
echo "$1" | sed -n "s/.*\"$2\": *\(-\?[0-9.]*\).*/\1/p" | head -1
|
||||||
|
}
|
||||||
|
_json_get_str() {
|
||||||
|
echo "$1" | sed -n "s/.*\"$2\": *\"\([^\"]*\)\".*/\1/p" | head -1
|
||||||
|
}
|
||||||
|
|
||||||
# ── GPS: Eyeride ubus JSON-RPC ─────────────────────────────────────────────
|
# ── GPS: Eyeride ubus JSON-RPC ─────────────────────────────────────────────
|
||||||
|
|
||||||
# Authenticate to Eyeride ubus, return session token on stdout.
|
|
||||||
_eyeride_login() {
|
_eyeride_login() {
|
||||||
_pass="${EYERIDE_PASSWORD:-}"
|
_pass="${EYERIDE_PASSWORD:-}"
|
||||||
[ -n "$_pass" ] || return 1
|
[ -n "$_pass" ] || return 1
|
||||||
|
|
||||||
_login_body="{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"call\",\"params\":[\"00000000000000000000000000000000\",\"session\",\"login\",{\"username\":\"${EYERIDE_USER}\",\"password\":\"${_pass}\"}]}"
|
_login_body="{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"call\",\"params\":[\"00000000000000000000000000000000\",\"session\",\"login\",{\"username\":\"${EYERIDE_USER}\",\"password\":\"${_pass}\"}]}"
|
||||||
_login_resp=$(curl -s --connect-timeout 5 -X POST \
|
_login_resp=$(curl -s --connect-timeout 5 -X POST \
|
||||||
-H 'Content-Type: application/json' \
|
-H 'Content-Type: application/json' \
|
||||||
-d "$_login_body" \
|
-d "$_login_body" \
|
||||||
"http://${EYERIDE_IP}:${EYERIDE_PORT}/ubus" 2>/dev/null) || return 1
|
"http://${EYERIDE_IP}:${EYERIDE_PORT}/ubus" 2>/dev/null) || return 1
|
||||||
|
|
||||||
# Check for auth failure: ubus returns [6] for permission denied
|
|
||||||
if [ -z "$_login_resp" ] || echo "$_login_resp" | grep -q '"result":\[6\]'; then
|
if [ -z "$_login_resp" ] || echo "$_login_resp" | grep -q '"result":\[6\]'; then
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
_session=$(echo "$_login_resp" | sed 's/.*"ubus_rpc_session":"\([^"]*\)".*/\1/')
|
_session=$(echo "$_login_resp" | sed 's/.*"ubus_rpc_session":"\([^"]*\)".*/\1/')
|
||||||
[ -n "$_session" ] && [ "$_session" != "$_login_resp" ] || return 1
|
[ -n "$_session" ] && [ "$_session" != "$_login_resp" ] || return 1
|
||||||
printf '%s\n' "$_session"
|
printf '%s\n' "$_session"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Call a ubus method on the Eyeride with an active session token.
|
|
||||||
_eyeride_ubus_call() {
|
_eyeride_ubus_call() {
|
||||||
_session="$1" _svc="$2" _method="$3" _params="${4:-{}}"
|
_session="$1" _svc="$2" _method="$3" _params="${4:-{}}"
|
||||||
_body="{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"call\",\"params\":[\"${_session}\",\"${_svc}\",\"${_method}\",${_params}]}"
|
_body="{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"call\",\"params\":[\"${_session}\",\"${_svc}\",\"${_method}\",${_params}]}"
|
||||||
@@ -98,231 +81,226 @@ _eyeride_ubus_call() {
|
|||||||
"http://${EYERIDE_IP}:${EYERIDE_PORT}/ubus" 2>/dev/null
|
"http://${EYERIDE_IP}:${EYERIDE_PORT}/ubus" 2>/dev/null
|
||||||
}
|
}
|
||||||
|
|
||||||
# Extract a numeric field from JSON (handles both integer and float).
|
|
||||||
_json_get_num() {
|
|
||||||
echo "$1" | sed -n "s/.*\"$2\": *\(-\?[0-9.]*\).*/\1/p" | head -1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Extract a string field from JSON.
|
|
||||||
_json_get_str() {
|
|
||||||
echo "$1" | sed -n "s/.*\"$2\": *\"\([^\"]*\)\".*/\1/p" | head -1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Query GPS from Eyeride ubus. Emits 'lat lon fix'; exit 1 on failure.
|
|
||||||
eyeride_gps() {
|
eyeride_gps() {
|
||||||
_session=$(_eyeride_login) || return 1
|
_session=$(_eyeride_login) || return 1
|
||||||
|
|
||||||
# Try 'gps' service first, then 'location' service
|
|
||||||
_resp=$(_eyeride_ubus_call "$_session" "gps" "status") 2>/dev/null
|
_resp=$(_eyeride_ubus_call "$_session" "gps" "status") 2>/dev/null
|
||||||
if [ -z "$_resp" ] || echo "$_resp" | grep -q '"result":\[2\]'; then
|
if [ -z "$_resp" ] || echo "$_resp" | grep -q '"result":\[2\]'; then
|
||||||
_resp=$(_eyeride_ubus_call "$_session" "location" "status") 2>/dev/null
|
_resp=$(_eyeride_ubus_call "$_session" "location" "status") 2>/dev/null
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Extract lat/lon from various possible field names
|
|
||||||
_lat=$(_json_get_num "$_resp" "latitude")
|
_lat=$(_json_get_num "$_resp" "latitude")
|
||||||
_lon=$(_json_get_num "$_resp" "longitude")
|
_lon=$(_json_get_num "$_resp" "longitude")
|
||||||
[ -z "$_lat" ] && _lat=$(_json_get_num "$_resp" "lat")
|
[ -z "$_lat" ] && _lat=$(_json_get_num "$_resp" "lat")
|
||||||
[ -z "$_lon" ] && _lon=$(_json_get_num "$_resp" "lon")
|
[ -z "$_lon" ] && _lon=$(_json_get_num "$_resp" "lon")
|
||||||
[ -z "$_lat" ] && _lat=$(_json_get_num "$_resp" "lat_deg")
|
[ -z "$_lat" ] && _lat=$(_json_get_num "$_resp" "lat_deg")
|
||||||
[ -z "$_lon" ] && _lon=$(_json_get_num "$_resp" "lon_deg")
|
[ -z "$_lon" ] && _lon=$(_json_get_num "$_resp" "lon_deg")
|
||||||
|
|
||||||
if [ -n "$_lat" ] && [ -n "$_lon" ]; then
|
if [ -n "$_lat" ] && [ -n "$_lon" ]; then
|
||||||
_fix=2 # assume 2D fix from Eyeride
|
_fix=2
|
||||||
logger -t busrouter -p daemon.debug "eyeride_gps: ${_lat} ${_lon} fix=${_fix}"
|
logger -t busrouter -p daemon.debug "eyeride_gps: ${_lat} ${_lon} fix=${_fix}"
|
||||||
printf '%s %s %s\n' "$_lat" "$_lon" "$_fix"
|
printf '%s %s %s\n' "$_lat" "$_lon" "$_fix"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
logger -t busrouter "eyeride_gps: no fix from Eyeride"
|
logger -t busrouter "eyeride_gps: no fix from Eyeride"
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── GPS: IP Geolocation Fallback ───────────────────────────────────────────
|
|
||||||
|
|
||||||
# Query ip-api.com for approximate location. Emits 'lat lon fix'; exit 1 on failure.
|
|
||||||
ipgeo_gps() {
|
ipgeo_gps() {
|
||||||
_resp=$(curl -s --connect-timeout 5 "http://ip-api.com/json/" 2>/dev/null | tr -d '\n\r') || return 1
|
_resp=$(curl -s --connect-timeout 5 "http://ip-api.com/json/" 2>/dev/null | tr -d '\n\r') || return 1
|
||||||
[ -n "$_resp" ] || return 1
|
[ -n "$_resp" ] || return 1
|
||||||
|
|
||||||
_lat=$(_json_get_num "$_resp" "lat")
|
_lat=$(_json_get_num "$_resp" "lat")
|
||||||
_lon=$(_json_get_num "$_resp" "lon")
|
_lon=$(_json_get_num "$_resp" "lon")
|
||||||
|
|
||||||
if [ -n "$_lat" ] && [ -n "$_lon" ]; then
|
if [ -n "$_lat" ] && [ -n "$_lon" ]; then
|
||||||
_fix=1 # approximate (IP-based)
|
_fix=1
|
||||||
logger -t busrouter -p daemon.debug "ipgeo_gps: ${_lat} ${_lon} fix=${_fix} (IP geolocation)"
|
logger -t busrouter -p daemon.debug "ipgeo_gps: ${_lat} ${_lon} fix=${_fix} (IP geolocation)"
|
||||||
printf '%s %s %s\n' "$_lat" "$_lon" "$_fix"
|
printf '%s %s %s\n' "$_lat" "$_lon" "$_fix"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
logger -t busrouter "ipgeo_gps: geolocation failed"
|
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# GPS with fallback: try Eyeride first, then IP geolocation.
|
|
||||||
gps_fix() {
|
gps_fix() {
|
||||||
if eyeride_gps 2>/dev/null; then
|
if eyeride_gps 2>/dev/null; then return 0; fi
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
ipgeo_gps
|
ipgeo_gps
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Eyeride Cellular Signal ────────────────────────────────────────────────
|
# ── Smart WAN Role Detection ───────────────────────────────────────────────
|
||||||
|
# Detects which physical interface is cellular vs landline/Starlink.
|
||||||
|
# Uses multiple signals in priority order:
|
||||||
|
# 1. aiwanbal modem_type field (eyeride/peplink/zte/mofi/quectel = cellular)
|
||||||
|
# 2. Interface routes to Eyeride at 192.168.10.1
|
||||||
|
# 3. Gateway/carrier analysis
|
||||||
|
|
||||||
# Query cellular signal from Eyeride ubus (network/wwan services).
|
# Check if a modem_type string indicates a cellular modem.
|
||||||
# Returns: rsrp_dbm rsrq_db sinr_db technology carrier_str
|
_is_cellular_modem() {
|
||||||
_eyeride_signal() {
|
case "$1" in
|
||||||
_session=$(_eyeride_login) 2>/dev/null || return 1
|
eyeride|peplink|zte|mofi|quectel|telit|sierra|fibocom) return 0 ;;
|
||||||
|
none|generic|"") return 1 ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
# Try network.wan status for modem info
|
# Check if an interface appears to be cellular by attempting to reach the Eyeride.
|
||||||
_resp=$(_eyeride_ubus_call "$_session" "network" "status") 2>/dev/null
|
_iface_reaches_eyeride() {
|
||||||
if [ -z "$_resp" ] || echo "$_resp" | grep -q '"result":\[2\]'; then
|
_iface="$1"
|
||||||
# Try alternative: wwan or modem service
|
# Check if this interface has an IP in the Eyeride network range
|
||||||
_resp=$(_eyeride_ubus_call "$_session" "wwan" "status") 2>/dev/null
|
_ip=$(ip addr show "$_iface" 2>/dev/null | grep 'inet ' | awk '{print $2}' | cut -d/ -f1)
|
||||||
|
if echo "$_ip" | grep -q '^192\.168\.10\.'; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
# Check if a route to the Eyeride exists via this interface
|
||||||
|
if ip route get "$EYERIDE_IP" 2>/dev/null | grep -q "dev $_iface"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Returns: "cellular_iface ethernet_iface"
|
||||||
|
_detect_wan_roles() {
|
||||||
|
_cell="" _eth=""
|
||||||
|
|
||||||
|
# 1. Try aiwanbal modem_type first (most reliable)
|
||||||
|
if [ -d "$AIWANBAL_DIR" ]; then
|
||||||
|
_mod1=$(_read_file "${AIWANBAL_DIR}/wan1_modem_type")
|
||||||
|
_mod2=$(_read_file "${AIWANBAL_DIR}/wan2_modem_type")
|
||||||
|
if _is_cellular_modem "$_mod1" && ! _is_cellular_modem "$_mod2"; then
|
||||||
|
echo "$WAN1_IFACE $WAN2_IFACE"; return 0
|
||||||
|
elif _is_cellular_modem "$_mod2" && ! _is_cellular_modem "$_mod1"; then
|
||||||
|
echo "$WAN2_IFACE $WAN1_IFACE"; return 0
|
||||||
|
elif _is_cellular_modem "$_mod1" && _is_cellular_modem "$_mod2"; then
|
||||||
|
# Both cellular — default: WAN2 is cellular (Eyeride typical port)
|
||||||
|
echo "$WAN2_IFACE $WAN1_IFACE"; return 0
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
_rsrp=$(_json_get_num "$_resp" "rsrp")
|
# 2. Check Eyeride reachability
|
||||||
_rsrq=$(_json_get_num "$_resp" "rsrq")
|
if _iface_reaches_eyeride "$WAN2_IFACE"; then
|
||||||
_sinr=$(_json_get_num "$_resp" "sinr")
|
echo "$WAN2_IFACE $WAN1_IFACE"; return 0
|
||||||
_tech=$(_json_get_str "$_resp" "technology")
|
elif _iface_reaches_eyeride "$WAN1_IFACE"; then
|
||||||
_carrier=$(_json_get_str "$_resp" "operator")
|
echo "$WAN1_IFACE $WAN2_IFACE"; return 0
|
||||||
|
fi
|
||||||
|
|
||||||
# Also try signal-specific fields
|
# 3. Check gateway patterns for known carriers
|
||||||
[ -z "$_rsrp" ] && _rsrp=$(_json_get_num "$_resp" "signal")
|
for _iface in "$WAN2_IFACE" "$WAN1_IFACE"; do
|
||||||
[ -z "$_rsrp" ] && _rsrp=$(_json_get_num "$_resp" "rssi")
|
_gw=$(ip route show default 2>/dev/null | grep "$_iface" | awk '{print $3}' | head -1)
|
||||||
|
# Starlink: typically 192.168.100.1 or 100.64.x.x (CGNAT)
|
||||||
|
if echo "$_gw" | grep -qE '^192\.168\.100\.|^100\.(64|96|112)\.'; then
|
||||||
|
_cell="" # Starlink is not cellular
|
||||||
|
fi
|
||||||
|
# Eyeride: always 192.168.10.1
|
||||||
|
if [ "$_gw" = "192.168.10.1" ]; then
|
||||||
|
_cell="$_iface"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ -n "$_cell" ]; then
|
||||||
|
for _iface in "$WAN2_IFACE" "$WAN1_IFACE"; do
|
||||||
|
[ "$_iface" != "$_cell" ] && _eth="$_iface"
|
||||||
|
done
|
||||||
|
echo "${_cell} ${_eth}"; return 0
|
||||||
|
fi
|
||||||
|
|
||||||
printf '%s %s %s %s %s\n' "${_rsrp:-null}" "${_rsrq:-null}" "${_sinr:-null}" "${_tech:-unknown}" "${_carrier:-Eyeride}"
|
# 4. Default: WAN2 is cellular (most common Synology bus router layout)
|
||||||
return 0
|
echo "$WAN2_IFACE $WAN1_IFACE"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── WAN Metric Collection ──────────────────────────────────────────────────
|
# ── WAN Metric Collection ──────────────────────────────────────────────────
|
||||||
|
|
||||||
# Ping an interface to get latency and loss.
|
# Check if aiwanbal state is available and fresh (< 120s old).
|
||||||
# On Synology SRM: ping requires root. When running from cron (as root), it works.
|
_aiwanbal_available() {
|
||||||
# When run manually as non-root, gracefully returns null.
|
[ -d "$AIWANBAL_DIR" ] || return 1
|
||||||
# NOTE: ping returns exit 0 on success, 1 on any packet loss, 2 on error.
|
_mtime=$(stat -c %Y "${AIWANBAL_DIR}/wan1_score_avg" 2>/dev/null || stat -f %m "${AIWANBAL_DIR}/wan1_score_avg" 2>/dev/null)
|
||||||
# We use sudo first; if sudo ping exits 2 (real error), fall back to direct ping.
|
[ -n "$_mtime" ] || return 1
|
||||||
# Exit 1 (packet loss) is a valid result — do NOT fall back on it.
|
_now=$(date +%s)
|
||||||
# Usage: _ping_iface <iface> → "lat_ms loss_pct"
|
_age=$(( _now - _mtime ))
|
||||||
|
[ "$_age" -lt 120 ] && return 0
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ping-based fallback metrics (used when aiwanbal not available).
|
||||||
_ping_iface() {
|
_ping_iface() {
|
||||||
_iface="$1"
|
_iface="$1"
|
||||||
# Try sudo ping first. Only fall back to direct ping if sudo fails (exit 2+).
|
|
||||||
_out=$(sudo ping -I "$_iface" -c 3 -W 3 "$PING_TARGET" 2>/dev/null)
|
_out=$(sudo ping -I "$_iface" -c 3 -W 3 "$PING_TARGET" 2>/dev/null)
|
||||||
_rc=$?
|
_rc=$?
|
||||||
if [ $_rc -ge 2 ]; then
|
if [ $_rc -ge 2 ]; then
|
||||||
_out=$(ping -I "$_iface" -c 3 -W 3 "$PING_TARGET" 2>/dev/null)
|
_out=$(ping -I "$_iface" -c 3 -W 3 "$PING_TARGET" 2>/dev/null)
|
||||||
fi
|
fi
|
||||||
if [ -z "$_out" ]; then
|
if [ -z "$_out" ]; then
|
||||||
echo "null null"
|
echo "null null null"; return
|
||||||
return
|
|
||||||
fi
|
fi
|
||||||
# Check for permission denied / other errors
|
|
||||||
if echo "$_out" | grep -qE "permission denied|not permitted|unknown host|Network is unreachable"; then
|
if echo "$_out" | grep -qE "permission denied|not permitted|unknown host|Network is unreachable"; then
|
||||||
echo "null null"
|
echo "null null null"; return
|
||||||
return
|
|
||||||
fi
|
fi
|
||||||
# Extract avg rtt from last line.
|
# Extract avg rtt
|
||||||
# Standard ping: "rtt min/avg/max/mdev = 10.5/15.2/20.1/3.5 ms"
|
|
||||||
# BusyBox ping: "round-trip min/avg/max = 10.5/15.2/20.1 ms"
|
|
||||||
_avg=$(echo "$_out" | tail -1 | sed -n 's/.*=\ [0-9.]*\/\([0-9.]*\)\/.*/\1/p')
|
_avg=$(echo "$_out" | tail -1 | sed -n 's/.*=\ [0-9.]*\/\([0-9.]*\)\/.*/\1/p')
|
||||||
[ -z "$_avg" ] && _avg=$(echo "$_out" | tail -1 | sed -n 's/.*avg\/max.*=\ \([0-9.]*\)\/.*/\1/p')
|
[ -z "$_avg" ] && _avg=$(echo "$_out" | tail -1 | sed -n 's/.*avg\/max.*=\ \([0-9.]*\)\/.*/\1/p')
|
||||||
[ -z "$_avg" ] && _avg="null"
|
[ -z "$_avg" ] && _avg="null"
|
||||||
# Extract loss percentage: "3 packets transmitted, 3 received, 0% packet loss"
|
# Extract jitter (mdev from standard ping: rtt min/avg/max/mdev = ...)
|
||||||
|
_jitter=$(echo "$_out" | tail -1 | sed -n 's/.*=\ [0-9.]*\/[0-9.]*\/[0-9.]*\/\([0-9.]*\).*/\1/p')
|
||||||
|
[ -z "$_jitter" ] && _jitter="null"
|
||||||
|
# Extract loss
|
||||||
_loss=$(echo "$_out" | sed -n 's/.* \([0-9]*\)% packet loss.*/\1/p')
|
_loss=$(echo "$_out" | sed -n 's/.* \([0-9]*\)% packet loss.*/\1/p')
|
||||||
[ -z "$_loss" ] && _loss="null"
|
[ -z "$_loss" ] && _loss="null"
|
||||||
printf '%s %s\n' "${_avg}" "${_loss}" | tr -d '\n\r'
|
printf '%s %s %s\n' "${_avg}" "${_jitter}" "${_loss}" | tr -d '\n\r'
|
||||||
}
|
}
|
||||||
|
|
||||||
# Compute simple WAN score from latency and loss (0-100, higher = better).
|
# Compute score from latency + loss (0-100, higher = better).
|
||||||
# Score = max(0, 100 - latency_ms/2 - loss_pct*5)
|
|
||||||
_compute_score() {
|
_compute_score() {
|
||||||
_lat="$1" _loss="$2"
|
_lat="$1" _loss="$2"
|
||||||
if [ "$_lat" = "null" ] || [ "$_loss" = "null" ]; then
|
if [ "$_lat" = "null" ] || [ "$_loss" = "null" ]; then
|
||||||
echo "0"
|
echo "0"; return
|
||||||
return
|
|
||||||
fi
|
fi
|
||||||
_score=$(awk "BEGIN { s = 100 - ${_lat}/2 - ${_loss}*5; if (s < 0) s = 0; if (s > 100) s = 100; printf \"%d\", int(s) }" 2>/dev/null)
|
_score=$(awk "BEGIN { s = 100 - ${_lat}/2 - ${_loss}*5; if (s < 0) s = 0; if (s > 100) s = 100; printf \"%d\", int(s) }" 2>/dev/null)
|
||||||
echo "${_score:-0}"
|
echo "${_score:-0}"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Read bytes transferred from /sys/class/net/<iface>/statistics/.
|
|
||||||
# Returns delta from last reading stored in STATE_DIR.
|
|
||||||
_read_bytes_delta() {
|
|
||||||
_iface="$1" _direction="$2" # tx_bytes or rx_bytes
|
|
||||||
_stat_file="/sys/class/net/${_iface}/statistics/${_direction}"
|
|
||||||
_state_file="${STATE_DIR}/bytes_${_iface}_${_direction}"
|
|
||||||
[ -f "$_stat_file" ] || { echo "null"; return; }
|
|
||||||
mkdir -p "$STATE_DIR" 2>/dev/null
|
|
||||||
_current=$(cat "$_stat_file" 2>/dev/null)
|
|
||||||
_prev=$(cat "$_state_file" 2>/dev/null)
|
|
||||||
printf '%s\n' "$_current" > "$_state_file" 2>/dev/null || true
|
|
||||||
if [ -z "$_prev" ] || [ -z "$_current" ]; then
|
|
||||||
echo "null"; return
|
|
||||||
fi
|
|
||||||
_delta=$(( _current - _prev ))
|
|
||||||
if [ "$_delta" -lt 0 ]; then
|
|
||||||
_delta=0 # counter reset
|
|
||||||
fi
|
|
||||||
# Convert bytes to Mbps (bytes → Mb/s, assume ~60s interval)
|
|
||||||
_mbps=$(awk "BEGIN { printf \"%.1f\", ${_delta} * 8 / 60 / 1000000 }" 2>/dev/null)
|
|
||||||
echo "${_mbps:-0.0}"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Collect metrics for one WAN interface.
|
# Collect metrics for one WAN interface.
|
||||||
# Usage: _collect_wan <iface> → JSON object fragment
|
# Uses aiwanbal state if available, falls back to self-collected ping.
|
||||||
|
# Usage: _collect_wan <iface> <wan_num> → JSON object fragment
|
||||||
_collect_wan() {
|
_collect_wan() {
|
||||||
_iface="$1"
|
_iface="$1" _num="$2"
|
||||||
|
_score="0" _lat="null" _dl="null" _ul="null" _jitter="null" _loss="null"
|
||||||
|
|
||||||
# Ping metrics
|
if _aiwanbal_available; then
|
||||||
_ping_result=$(_ping_iface "$_iface")
|
# Use aiwanbal state (preferred — richer metrics)
|
||||||
_lat=$(echo "$_ping_result" | awk '{print $1}')
|
_score=$(_read_file "${AIWANBAL_DIR}/wan${_num}_score_avg")
|
||||||
_loss=$(echo "$_ping_result" | awk '{print $2}')
|
[ -z "$_score" ] && _score="0"
|
||||||
|
_lat=$(_read_file "${AIWANBAL_DIR}/wan${_num}_latency")
|
||||||
# Throughput (delta since last cycle)
|
[ -z "$_lat" ] && _lat="null"
|
||||||
_dl=$(_read_bytes_delta "$_iface" "rx_bytes")
|
_jitter=$(_read_file "${AIWANBAL_DIR}/wan${_num}_jitter")
|
||||||
_ul=$(_read_bytes_delta "$_iface" "tx_bytes")
|
[ -z "$_jitter" ] && _jitter="null"
|
||||||
|
_loss=$(_read_file "${AIWANBAL_DIR}/wan${_num}_packet_loss")
|
||||||
# Score
|
[ -z "$_loss" ] && _loss="null"
|
||||||
_score=$(_compute_score "$_lat" "$_loss")
|
# Convert throughput from B/s to Mbps
|
||||||
|
_tp=$(_read_file "${AIWANBAL_DIR}/wan${_num}_throughput")
|
||||||
# Jitter — rough estimate from ping variation (not true jitter, but indicative)
|
if [ -n "$_tp" ] && [ "$_tp" != "0" ]; then
|
||||||
_jitter="null"
|
_dl=$(awk "BEGIN { printf \"%.1f\", ${_tp} / 125000 }" 2>/dev/null)
|
||||||
|
fi
|
||||||
|
[ -z "$_dl" ] && _dl="0.0"
|
||||||
|
_up=$(_read_file "${AIWANBAL_DIR}/wan${_num}_upload")
|
||||||
|
if [ -n "$_up" ] && [ "$_up" != "0" ]; then
|
||||||
|
_ul=$(awk "BEGIN { printf \"%.1f\", ${_up} / 125000 }" 2>/dev/null)
|
||||||
|
fi
|
||||||
|
[ -z "$_ul" ] && _ul="0.0"
|
||||||
|
else
|
||||||
|
# Fallback: self-collected ping metrics
|
||||||
|
_ping_result=$(_ping_iface "$_iface")
|
||||||
|
_lat=$(echo "$_ping_result" | awk '{print $1}')
|
||||||
|
_jitter=$(echo "$_ping_result" | awk '{print $2}')
|
||||||
|
_loss=$(echo "$_ping_result" | awk '{print $3}')
|
||||||
|
_score=$(_compute_score "$_lat" "$_loss")
|
||||||
|
_dl="0.0"
|
||||||
|
_ul="0.0"
|
||||||
|
fi
|
||||||
|
|
||||||
printf '{"score":%s,"latency_ms":%s,"dl_mbps":%s,"ul_mbps":%s,"jitter_ms":%s,"loss_pct":%s}' \
|
printf '{"score":%s,"latency_ms":%s,"dl_mbps":%s,"ul_mbps":%s,"jitter_ms":%s,"loss_pct":%s}' \
|
||||||
"${_score}" "${_lat}" "${_dl}" "${_ul}" "${_jitter}" "${_loss}"
|
"${_score}" "${_lat}" "${_dl}" "${_ul}" "${_jitter}" "${_loss}"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Telemetry Collection ───────────────────────────────────────────────────
|
# ── Carrier Detection ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
# Detect which interface is the Eyeride cellular link.
|
# Derive carrier name for an interface.
|
||||||
# The Eyeride runs on 192.168.10.x subnet; the interface with an IP in that
|
|
||||||
# range is the cellular path. The other WAN interface is the primary/fiber uplink.
|
|
||||||
# Returns: cellular_iface ethernet_iface
|
|
||||||
_detect_wan_roles() {
|
|
||||||
# Check if WAN2 is on the Eyeride subnet
|
|
||||||
_wan2_ip=$(ip addr show "$WAN2_IFACE" 2>/dev/null | grep 'inet ' | awk '{print $2}' | cut -d/ -f1)
|
|
||||||
if echo "$_wan2_ip" | grep -q '^192\.168\.10\.'; then
|
|
||||||
echo "$WAN2_IFACE $WAN1_IFACE" # WAN2 = cellular, WAN1 = ethernet
|
|
||||||
else
|
|
||||||
# Check if WAN1 is on the Eyeride subnet
|
|
||||||
_wan1_ip=$(ip addr show "$WAN1_IFACE" 2>/dev/null | grep 'inet ' | awk '{print $2}' | cut -d/ -f1)
|
|
||||||
if echo "$_wan1_ip" | grep -q '^192\.168\.10\.'; then
|
|
||||||
echo "$WAN1_IFACE $WAN2_IFACE" # WAN1 = cellular, WAN2 = ethernet
|
|
||||||
else
|
|
||||||
# Default: WAN2 is cellular (Eyeride typically on secondary port)
|
|
||||||
echo "$WAN2_IFACE $WAN1_IFACE"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Derive carrier name for an interface. Uses reverse DNS on gateway or ISP detection.
|
|
||||||
# For the Eyeride (192.168.10.x), the carrier comes from the Eyeride's cellular operator.
|
|
||||||
_detect_carrier() {
|
_detect_carrier() {
|
||||||
_iface="$1"
|
_iface="$1"
|
||||||
_gw=$(ip route show default 2>/dev/null | grep "$_iface" | awk '{print $3}' | head -1)
|
# Check if this is the Eyeride cellular path
|
||||||
# Check if this is the Eyeride network
|
if _iface_reaches_eyeride "$_iface"; then
|
||||||
_ip=$(ip addr show "$_iface" 2>/dev/null | grep 'inet ' | awk '{print $2}' | cut -d/ -f1)
|
|
||||||
if echo "$_ip" | grep -q '^192\.168\.10\.'; then
|
|
||||||
# This is the Eyeride — carrier comes from the Eyeride's cellular operator
|
|
||||||
_sig=$(_eyeride_signal 2>/dev/null)
|
_sig=$(_eyeride_signal 2>/dev/null)
|
||||||
_sig_carrier=$(echo "$_sig" | awk '{print $5}')
|
_sig_carrier=$(echo "$_sig" | awk '{print $5}')
|
||||||
if [ -n "$_sig_carrier" ] && [ "$_sig_carrier" != "null" ] && [ "$_sig_carrier" != "Eyeride" ]; then
|
if [ -n "$_sig_carrier" ] && [ "$_sig_carrier" != "null" ] && [ "$_sig_carrier" != "Eyeride" ]; then
|
||||||
@@ -330,48 +308,119 @@ _detect_carrier() {
|
|||||||
fi
|
fi
|
||||||
echo "Eyeride"; return
|
echo "Eyeride"; return
|
||||||
fi
|
fi
|
||||||
# For the primary/ethernet interface, try to detect from gateway
|
# Try aiwanbal modem_type
|
||||||
|
_num=""
|
||||||
|
for _n in 1 2; do
|
||||||
|
_mod=$(_read_file "${AIWANBAL_DIR}/wan${_n}_modem_type" 2>/dev/null)
|
||||||
|
_active=$(_read_file "${AIWANBAL_DIR}/wan${_n}_state" 2>/dev/null)
|
||||||
|
if [ "$_mod" != "eyeride" ] && [ "$_mod" != "none" ] && [ -n "$_mod" ] && [ "$_active" = "up" ]; then
|
||||||
|
echo "$_mod"; return
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
# Check gateway for known ISP patterns
|
||||||
|
_gw=$(ip route show default 2>/dev/null | grep "$_iface" | awk '{print $3}' | head -1)
|
||||||
if [ -n "$_gw" ]; then
|
if [ -n "$_gw" ]; then
|
||||||
# Common AT&T gateways
|
|
||||||
if echo "$_gw" | grep -qE '^192\.168\.[01]\.'; then
|
if echo "$_gw" | grep -qE '^192\.168\.[01]\.'; then
|
||||||
echo "AT&T"; return
|
echo "AT&T"; return
|
||||||
fi
|
fi
|
||||||
# Common Spectrum/Charter
|
if echo "$_gw" | grep -qE '^192\.168\.100\.|^100\.(64|96)\.'; then
|
||||||
if echo "$_gw" | grep -qE '^192\.168\.[0-9]+\.'; then
|
echo "Starlink"; return
|
||||||
_rev=$(nslookup "$_gw" 2>/dev/null | grep -i name | head -1 | sed 's/.*= //')
|
fi
|
||||||
if echo "$_rev" | grep -qi 'att\|sbcglobal\|bellsouth'; then
|
if echo "$_gw" | grep -qE '^10\.|^172\.(1[6-9]|2[0-9]|3[0-1])\.'; then
|
||||||
echo "AT&T"; return
|
echo "Business"; return
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
# Fallback
|
|
||||||
echo ""
|
echo ""
|
||||||
}
|
}
|
||||||
|
|
||||||
# Add carrier and identity fields to a WAN JSON object.
|
# Add carrier and identity to a WAN JSON object.
|
||||||
# Usage: _add_wan_identity <wan_json> <iface>
|
|
||||||
_add_wan_identity() {
|
_add_wan_identity() {
|
||||||
_json="$1" _iface="$2"
|
_json="$1" _iface="$2"
|
||||||
_carrier=$(_detect_carrier "$_iface")
|
_carrier=$(_detect_carrier "$_iface")
|
||||||
_esc_carrier=$(_json_str "${_carrier}")
|
_prefix=$(printf '%s' "$_json" | sed 's/}$//')
|
||||||
# Strip closing brace, add carrier, re-close
|
|
||||||
printf '%s' "$_json" | sed 's/}$//'
|
|
||||||
if [ -n "$_carrier" ]; then
|
if [ -n "$_carrier" ]; then
|
||||||
printf ',"carrier":"%s"}' "${_esc_carrier}"
|
_esc_carrier=$(_json_str "${_carrier}")
|
||||||
|
printf '%s,"carrier":"%s"}' "$_prefix" "$_esc_carrier"
|
||||||
else
|
else
|
||||||
printf '}'
|
printf '%s}' "$_prefix"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Build the full telemetry JSON payload (canonical GL format).
|
# ── Eyeride Signal ─────────────────────────────────────────────────────────
|
||||||
# Emits JSON to stdout; never fails — uses defaults for missing fields.
|
_eyeride_signal() {
|
||||||
|
_session=$(_eyeride_login) 2>/dev/null || return 1
|
||||||
|
_resp=$(_eyeride_ubus_call "$_session" "network" "status") 2>/dev/null
|
||||||
|
if [ -z "$_resp" ] || echo "$_resp" | grep -q '"result":\[2\]'; then
|
||||||
|
_resp=$(_eyeride_ubus_call "$_session" "wwan" "status") 2>/dev/null
|
||||||
|
fi
|
||||||
|
_rsrp=$(_json_get_num "$_resp" "rsrp")
|
||||||
|
_rsrq=$(_json_get_num "$_resp" "rsrq")
|
||||||
|
_sinr=$(_json_get_num "$_resp" "sinr")
|
||||||
|
_tech=$(_json_get_str "$_resp" "technology")
|
||||||
|
_carrier=$(_json_get_str "$_resp" "operator")
|
||||||
|
[ -z "$_rsrp" ] && _rsrp=$(_json_get_num "$_resp" "signal")
|
||||||
|
[ -z "$_rsrp" ] && _rsrp=$(_json_get_num "$_resp" "rssi")
|
||||||
|
printf '%s %s %s %s %s\n' "${_rsrp:-null}" "${_rsrq:-null}" "${_sinr:-null}" "${_tech:-unknown}" "${_carrier:-Eyeride}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Speedtest ──────────────────────────────────────────────────────────────
|
||||||
|
# Run a speedtest bound to a specific interface. Reports dl_mbps ul_mbps.
|
||||||
|
run_speedtest() {
|
||||||
|
_iface="$1"
|
||||||
|
_result="0 0"
|
||||||
|
# Try speedtest-cli first
|
||||||
|
if which speedtest-cli >/dev/null 2>&1; then
|
||||||
|
_out=$(speedtest-cli --interface "$_iface" --json 2>/dev/null)
|
||||||
|
if [ -n "$_out" ]; then
|
||||||
|
_dl=$(_json_get_num "$_out" "download")
|
||||||
|
_ul=$(_json_get_num "$_out" "upload")
|
||||||
|
if [ -n "$_dl" ] && [ -n "$_ul" ]; then
|
||||||
|
_dl_mbps=$(awk "BEGIN { printf \"%.1f\", ${_dl} / 125000 }" 2>/dev/null)
|
||||||
|
_ul_mbps=$(awk "BEGIN { printf \"%.1f\", ${_ul} / 125000 }" 2>/dev/null)
|
||||||
|
echo "${_dl_mbps} ${_ul_mbps}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
# Fallback: iperf3 single-stream test to public server
|
||||||
|
if which iperf3 >/dev/null 2>&1; then
|
||||||
|
_out=$(timeout 15 iperf3 -B "$_ip" -c iperf.he.net -p 5201 -J 2>/dev/null)
|
||||||
|
if [ -n "$_out" ]; then
|
||||||
|
_dl=$(_json_get_num "$_out" "bits_per_second")
|
||||||
|
if [ -n "$_dl" ]; then
|
||||||
|
_dl_mbps=$(awk "BEGIN { printf \"%.1f\", ${_dl} / 1000000 }" 2>/dev/null)
|
||||||
|
echo "${_dl_mbps} 0"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
# Fallback: curl a known file and measure throughput
|
||||||
|
_tmp="/tmp/speedtest-$$"
|
||||||
|
_start=$(date +%s)
|
||||||
|
if curl -s --interface "$_iface" --max-time 10 -o "$_tmp" http://speedtest.tele2.net/1MB.zip 2>/dev/null; then
|
||||||
|
_end=$(date +%s)
|
||||||
|
_bytes=$(wc -c < "$_tmp" 2>/dev/null)
|
||||||
|
rm -f "$_tmp"
|
||||||
|
if [ "$_end" -gt "$_start" ] && [ -n "$_bytes" ]; then
|
||||||
|
_duration=$(( _end - _start ))
|
||||||
|
[ "$_duration" -lt 1 ] && _duration=1
|
||||||
|
_dl_mbps=$(awk "BEGIN { printf \"%.1f\", ${_bytes} * 8 / ${_duration} / 1000000 }" 2>/dev/null)
|
||||||
|
echo "${_dl_mbps:-0} 0"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
rm -f "$_tmp"
|
||||||
|
echo "0 0"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Telemetry Collection ───────────────────────────────────────────────────
|
||||||
telemetry_collect() {
|
telemetry_collect() {
|
||||||
now=$(date +%s)
|
now=$(date +%s)
|
||||||
device_id=$(cat /etc/busrouter/device-id 2>/dev/null || hostname)
|
device_id=$(cat /etc/busrouter/device-id 2>/dev/null || hostname)
|
||||||
uptime_s=$(awk '{printf "%d", $1}' /proc/uptime 2>/dev/null)
|
uptime_s=$(awk '{printf "%d", $1}' /proc/uptime 2>/dev/null)
|
||||||
version=$(cat /etc/busrouter/version 2>/dev/null || echo "dev")
|
version=$(cat /etc/busrouter/version 2>/dev/null || echo "dev")
|
||||||
|
|
||||||
# GPS (Eyeride → IP geolocation fallback)
|
# GPS
|
||||||
gps_lat="null" gps_lon="null" gps_fix="null"
|
gps_lat="null" gps_lon="null" gps_fix="null"
|
||||||
if gps_out=$(gps_fix 2>/dev/null); then
|
if gps_out=$(gps_fix 2>/dev/null); then
|
||||||
gps_lat=$(echo "$gps_out" | awk '{print $1}')
|
gps_lat=$(echo "$gps_out" | awk '{print $1}')
|
||||||
@@ -379,8 +428,7 @@ telemetry_collect() {
|
|||||||
gps_fix=$(echo "$gps_out" | awk '{print $3}')
|
gps_fix=$(echo "$gps_out" | awk '{print $3}')
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Auto-detect which interface is cellular vs ethernet/fiber
|
# Smart WAN role detection
|
||||||
_cell_iface="" _eth_iface=""
|
|
||||||
if _roles=$(_detect_wan_roles 2>/dev/null); then
|
if _roles=$(_detect_wan_roles 2>/dev/null); then
|
||||||
_cell_iface=$(echo "$_roles" | awk '{print $1}')
|
_cell_iface=$(echo "$_roles" | awk '{print $1}')
|
||||||
_eth_iface=$(echo "$_roles" | awk '{print $2}')
|
_eth_iface=$(echo "$_roles" | awk '{print $2}')
|
||||||
@@ -389,67 +437,67 @@ telemetry_collect() {
|
|||||||
_eth_iface="$WAN1_IFACE"
|
_eth_iface="$WAN1_IFACE"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Collect metrics for both WANs
|
# Map physical interfaces to aiwanbal wan numbers
|
||||||
_eth_json=$(_collect_wan "$_eth_iface")
|
_cell_num="2" _eth_num="1"
|
||||||
_cell_json=$(_collect_wan "$_cell_iface")
|
if [ "$_cell_iface" = "$WAN1_IFACE" ]; then
|
||||||
|
_cell_num="1"
|
||||||
|
fi
|
||||||
|
if [ "$_eth_iface" = "$WAN2_IFACE" ]; then
|
||||||
|
_eth_num="2"
|
||||||
|
fi
|
||||||
|
|
||||||
# Add carrier identity to each WAN
|
# Collect WAN metrics
|
||||||
_eth_json=$(_add_wan_identity "$_eth_json" "$_eth_iface")
|
_modem_json=$(_collect_wan "$_cell_iface" "$_cell_num")
|
||||||
_cell_json=$(_add_wan_identity "$_cell_json" "$_cell_iface")
|
_ether_json=$(_collect_wan "$_eth_iface" "$_eth_num")
|
||||||
|
|
||||||
# Eyeride cellular signal for the cellular interface
|
# Add carrier identity
|
||||||
|
_modem_json=$(_add_wan_identity "$_modem_json" "$_cell_iface")
|
||||||
|
_ether_json=$(_add_wan_identity "$_ether_json" "$_eth_iface")
|
||||||
|
|
||||||
|
# Enrich cellular WAN with Eyeride signal data
|
||||||
eyeride_sig=$(_eyeride_signal 2>/dev/null)
|
eyeride_sig=$(_eyeride_signal 2>/dev/null)
|
||||||
_rsrp=$(echo "$eyeride_sig" | awk '{print $1}')
|
_rsrp=$(echo "$eyeride_sig" | awk '{print $1}')
|
||||||
_rsrq=$(echo "$eyeride_sig" | awk '{print $2}')
|
_rsrq=$(echo "$eyeride_sig" | awk '{print $2}')
|
||||||
_sinr=$(echo "$eyeride_sig" | awk '{print $3}')
|
_sinr=$(echo "$eyeride_sig" | awk '{print $3}')
|
||||||
_tech=$(echo "$eyeride_sig" | awk '{print $4}')
|
_tech=$(echo "$eyeride_sig" | awk '{print $4}')
|
||||||
# Add cellular signal fields to the cellular WAN JSON
|
|
||||||
if echo "$_cell_json" | grep -q '"loss_pct"'; then
|
_modem_json=$(printf '%s' "$_modem_json" | sed 's/}$//')
|
||||||
_cell_json=$(printf '%s' "$_cell_json" | sed 's/}$//')
|
if [ "$_rsrp" != "null" ] && [ -n "$_rsrp" ]; then
|
||||||
if [ "$_rsrp" != "null" ] && [ -n "$_rsrp" ]; then
|
_esc_tech=$(_json_str "${_tech:-unknown}")
|
||||||
_esc_tech=$(_json_str "${_tech:-unknown}")
|
_modem_json="${_modem_json},\"rsrp_dbm\":${_rsrp},\"rsrq_db\":${_rsrq},\"sinr_db\":${_sinr},\"technology\":\"${_esc_tech}\"}"
|
||||||
_cell_json="${_cell_json},\"rsrp_dbm\":${_rsrp},\"rsrq_db\":${_rsrq},\"sinr_db\":${_sinr},\"technology\":\"${_esc_tech}\"}"
|
else
|
||||||
else
|
_modem_json="${_modem_json}}"
|
||||||
_cell_json="${_cell_json}}"
|
fi
|
||||||
|
|
||||||
|
# Active WAN determination
|
||||||
|
sel_primary="wan"
|
||||||
|
default_iface=$(ip route show default 2>/dev/null | awk '{print $5}' | head -1)
|
||||||
|
if [ -d "$AIWANBAL_DIR" ]; then
|
||||||
|
# Prefer aiwanbal mode for failover detection
|
||||||
|
_mode=$(_read_file "${AIWANBAL_DIR}/wan0_mode")
|
||||||
|
if [ "$_mode" = "failover" ]; then
|
||||||
|
_active=$(_read_file "${AIWANBAL_DIR}/wan2_state")
|
||||||
|
if [ "$_active" = "up" ]; then
|
||||||
|
sel_primary="modem_0001"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
# Fallback: use default route interface
|
||||||
# WAN mapping in canonical GL format:
|
|
||||||
# modem_0001 = cellular (Eyeride 5G)
|
|
||||||
# wan = ethernet/fiber (AT&T Uverse)
|
|
||||||
_wan_modem="$WAN2_IFACE" # which physical iface maps to modem_0001
|
|
||||||
_wan_ether="$WAN1_IFACE" # which physical iface maps to wan
|
|
||||||
|
|
||||||
# Check if roles were swapped by auto-detection
|
|
||||||
if [ "$_cell_iface" != "$WAN2_IFACE" ]; then
|
|
||||||
# WAN1 is cellular, WAN2 is ethernet
|
|
||||||
_modem_json="$_cell_json"
|
|
||||||
_ether_json="$_eth_json"
|
|
||||||
else
|
|
||||||
# WAN2 is cellular, WAN1 is ethernet (normal)
|
|
||||||
_modem_json="$_cell_json"
|
|
||||||
_ether_json="$_eth_json"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Active WAN: determine from routing table (default route interface)
|
|
||||||
sel_primary="wan" # default: fiber/ethernet is primary
|
|
||||||
default_iface=$(ip route show default 2>/dev/null | awk '{print $5}' | head -1)
|
|
||||||
if [ "$default_iface" = "$_cell_iface" ]; then
|
if [ "$default_iface" = "$_cell_iface" ]; then
|
||||||
sel_primary="modem_0001" # cellular is the active path
|
sel_primary="modem_0001"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Starlink — detect if present
|
# Starlink detection
|
||||||
sl_lat="null" sl_dl="null" sl_obs="0" sl_outage="0"
|
sl_lat="null" sl_dl="null" sl_obs="0" sl_outage="0"
|
||||||
if ip link show 2>/dev/null | grep -qi starlink; then
|
if ip link show 2>/dev/null | grep -qi starlink; then
|
||||||
sl_iface=$(ip link show 2>/dev/null | grep -i starlink | head -1 | awk -F": " '{print $2}' | awk '{print $1}')
|
sl_iface=$(ip link show 2>/dev/null | grep -i starlink | head -1 | awk -F": " '{print $2}' | awk '{print $1}')
|
||||||
if [ -n "$sl_iface" ]; then
|
if [ -n "$sl_iface" ]; then
|
||||||
sl_ping=$(_ping_iface "$sl_iface")
|
sl_ping=$(_ping_iface "$sl_iface")
|
||||||
sl_lat=$(echo "$sl_ping" | awk '{print $1}')
|
sl_lat=$(echo "$sl_ping" | awk '{print $1}')
|
||||||
sl_dl=$(_read_bytes_delta "$sl_iface" "rx_bytes")
|
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# State — autonomous indicator (always true for fleet routers)
|
# State
|
||||||
state='{"autonomous":true,"qos":{},"wanhealth":{}}'
|
state='{"autonomous":true,"qos":{},"wanhealth":{}}'
|
||||||
|
|
||||||
esc_id=$(_json_str "$device_id")
|
esc_id=$(_json_str "$device_id")
|
||||||
@@ -465,8 +513,6 @@ telemetry_collect() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# ── Hub POST + Buffering ───────────────────────────────────────────────────
|
# ── Hub POST + Buffering ───────────────────────────────────────────────────
|
||||||
|
|
||||||
# POST a JSON file to the hub. Returns 0 on HTTP 2xx, 1 on any failure.
|
|
||||||
_telemetry_try_post() {
|
_telemetry_try_post() {
|
||||||
file="$1"
|
file="$1"
|
||||||
code=$(curl -sf -X POST -H "Content-Type: application/json" \
|
code=$(curl -sf -X POST -H "Content-Type: application/json" \
|
||||||
@@ -475,8 +521,6 @@ _telemetry_try_post() {
|
|||||||
case "$code" in 2*) return 0 ;; *) return 1 ;; esac
|
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() {
|
telemetry_flush() {
|
||||||
[ -d "$TELEMETRY_BUFFER_DIR" ] || return 0
|
[ -d "$TELEMETRY_BUFFER_DIR" ] || return 0
|
||||||
for f in "$TELEMETRY_BUFFER_DIR"/*.json; do
|
for f in "$TELEMETRY_BUFFER_DIR"/*.json; do
|
||||||
@@ -491,28 +535,19 @@ telemetry_flush() {
|
|||||||
done
|
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() {
|
telemetry_send() {
|
||||||
mkdir -p "$STATE_DIR" 2>/dev/null
|
mkdir -p "$STATE_DIR" 2>/dev/null
|
||||||
|
|
||||||
payload=$(telemetry_collect)
|
payload=$(telemetry_collect)
|
||||||
|
|
||||||
telemetry_flush 2>/dev/null || true
|
telemetry_flush 2>/dev/null || true
|
||||||
|
|
||||||
mkdir -p "$TELEMETRY_BUFFER_DIR" 2>/dev/null
|
mkdir -p "$TELEMETRY_BUFFER_DIR" 2>/dev/null
|
||||||
_TELEM_SEQ=$(( _TELEM_SEQ + 1 ))
|
_TELEM_SEQ=$(( _TELEM_SEQ + 1 ))
|
||||||
tmpfile="${TELEMETRY_BUFFER_DIR}/$(date +%s)_$$_${_TELEM_SEQ}.json"
|
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)
|
buf_count=$(ls "$TELEMETRY_BUFFER_DIR"/*.json 2>/dev/null | wc -l)
|
||||||
if [ "${buf_count:-0}" -ge 60 ]; then
|
if [ "${buf_count:-0}" -ge 60 ]; then
|
||||||
oldest=$(ls "$TELEMETRY_BUFFER_DIR"/*.json 2>/dev/null | sort | head -n 1)
|
oldest=$(ls "$TELEMETRY_BUFFER_DIR"/*.json 2>/dev/null | sort | head -n 1)
|
||||||
[ -f "$oldest" ] && rm -f "$oldest"
|
[ -f "$oldest" ] && rm -f "$oldest"
|
||||||
fi
|
fi
|
||||||
printf '%s\n' "$payload" > "$tmpfile"
|
printf '%s\n' "$payload" > "$tmpfile"
|
||||||
|
|
||||||
if _telemetry_try_post "$tmpfile"; then
|
if _telemetry_try_post "$tmpfile"; then
|
||||||
rm -f "$tmpfile"
|
rm -f "$tmpfile"
|
||||||
logger -t busrouter -p daemon.debug "telemetry_send: posted ok"
|
logger -t busrouter -p daemon.debug "telemetry_send: posted ok"
|
||||||
@@ -524,6 +559,13 @@ telemetry_send() {
|
|||||||
# ── Entry Point ────────────────────────────────────────────────────────────
|
# ── Entry Point ────────────────────────────────────────────────────────────
|
||||||
case "${0##*/}" in
|
case "${0##*/}" in
|
||||||
telemetry-synology.sh|telemetry-synology)
|
telemetry-synology.sh|telemetry-synology)
|
||||||
|
# Run speedtest if requested via a trigger file (touch /tmp/busrouter/speedtest-request)
|
||||||
|
if [ -f "${STATE_DIR}/speedtest-request" ]; then
|
||||||
|
_iface=$(cat "${STATE_DIR}/speedtest-request" 2>/dev/null)
|
||||||
|
[ -z "$_iface" ] && _iface="$WAN1_IFACE"
|
||||||
|
run_speedtest "$_iface" > "${STATE_DIR}/speedtest-result"
|
||||||
|
rm -f "${STATE_DIR}/speedtest-request"
|
||||||
|
fi
|
||||||
telemetry_send
|
telemetry_send
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
Reference in New Issue
Block a user