argus

package module
v1.0.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 12, 2026 License: MIT Imports: 15 Imported by: 0

README

Argus

Real-time OpenWrt device presence & static-IP dashboard — multi-source fusion, sub-second events, zero-dep Web UI 多源融合、秒级事件、零依赖 Web UI 的 OpenWrt 接入设备观察库

Go Reference Go Report Card Go version Tests Release

Dashboard

EN — Argus is a Go library + CLI for real-time WiFi/wired device presence on OpenWrt routers. It fuses six data sources (ahsapd · hostapd · logread · DHCP leases · ARP · ICMP) into a single sub-second event stream — Online / Offline / Change — and ships an opt-in Web UI with static-IP reservations, device aliases, and one-click recovery tools. Zero-dep, works on stock OpenWrt + MediaTek vendor firmwares (C-Life and similar). Named after the hundred-eyed giant of Greek myth — whose eyes never all slept.

中文 — Argus 是一个针对 OpenWrt 路由器的实时设备感知库与命令行工具,融合六路数据源(ahsapd · hostapd · syslog · DHCP 租约 · ARP · ICMP)形成秒级事件流:上线(Online) / 离线(Offline) / 状态变更(Change),并内置零依赖 Web UI(静态 IP 预留、设备别名、一键修复)。名字取自希腊神话中的百眼巨人——他的眼睛永远不会同时闭上——永不沉睡的守望者。

Quick start · 30 秒跑起来:

# 下载 · Releases page: https://github.com/xxl6097/argusd/releases
scp argusd root@192.168.1.1:/tmp/ && ssh root@192.168.1.1 \
  '/tmp/argusd -listen :8080 -aliases /etc/argusd/aliases.json'
# 浏览器 · Open http://<router-ip>:8080/

目录 · Table of Contents

  1. 特性 · Features
  2. 快速开始 · Quick Start
  3. Web UI · 内置仪表盘
  4. 架构 · Architecture
  5. API 速查 · API Overview
  6. 配置调优 · Configuration
  7. 可观测性 · Observability
  8. 路线图 · Roadmap
  9. 兼容性 · Compatibility
  10. 贡献 · Contributing

特性 · Features

  • 🔀 多源融合 · Multi-source fusion EN — ahsapd + hostapd + logread -f + DHCP leases + ARP states + ICMP probe, all merged into one stream. 中文 — 厂商 ubus(ahsapd) + 官方 hostapd + 系统日志 + DHCP 租约 + ARP 状态 + ICMP 探测,六路同步汇聚为单一事件流。

  • 🏭 零配置多厂商兼容 · Vendor-agnostic zero-config EN — Auto-detects ahsapd (vendor firmware) or hostapd.* (stock OpenWrt) at startup. 中文 — 启动时自动探测 ubus 可用服务,厂商固件与官方 OpenWrt 无需配置切换。

  • 毫秒级事件 · Sub-second events EN — Kernel log streaming (New Sta, Del Sta, Deauth, DHCPACK…) delivers online/offline in 1-2 s. 中文 — 通过实时日志(内核关联 / 断开 / Deauth / DHCP 分配)在 1-2 秒内识别上下线。

  • 🛡️ 多维离线判定 · Multi-dimensional offline detection EN — Three-layer decision: ICMP ping filter + AP association table with RSSI tiers + ARP FAILED/INCOMPLETE shortcut. 中文 — 三层判定:ICMP 可达性 + AP 关联表感知(息屏保护) & RSSI 信号分级 + ARP 失败加速。

  • 🌊 抗抖动 · Flap suppression EN — 90 s cooldown plus 30 s same-kind suppression window eliminates weak-signal thrashing. Both independently toggleable via Config.DisableCooldown / DisableFlapSuppression. 中文 — 90 秒冷却期 + 30 秒同类事件压制,弱信号边缘设备不再反复上下线。通过 Config.DisableCooldown / DisableFlapSuppression 可独立关闭任一机制。

  • 🧩 纯标准库 · Pure stdlib, single static binary EN — ~2.6 MB static binary (CGO_ENABLED=0, GOARCH=arm64). Drop into /tmp and run. 中文 — 纯 Go 标准库,静态编译,约 2.6 MB,直接丢到 OpenWrt 路由器 /tmp 即可运行。

  • 🔬 可观测性 · Observability EN — Four hook surfaces, all opt-in and zero-cost when unused: DecisionHandler (1.7 ns/op, 0 allocs) surfaces 17 internal branch decisions; WithLogger emits structured logs (slog/zap/zerolog adapter in ~5 lines); WithSpanRecorder emits distributed-tracing spans (OTel adapter ~15 lines); the argusmetrics subpackage ships zero-dependency counters (Counters / LabeledCounters) ready to bridge to Prometheus / OTLP. 中文 — 四路可观测性出口,全部 opt-in, 未注册时零成本: DecisionHandler(1.7 ns/op, 0 分配)暴露 17 种内部判定分支; WithLogger 结构化日志(~5 行桥接 slog/zap/zerolog); WithSpanRecorder 分布式追踪(OTel ~15 行); argusmetrics 子包自带零依赖计数器(Counters / LabeledCounters), 可直接桥接 Prometheus / OTLP。

  • 🔒 安全硬化 · Security hardened EN — IP regex + net.ParseIP double validation, interface whitelist — no command injection. 中文 — IP 双重校验(正则 + net.ParseIP)、hostapd 接口名白名单,杜绝命令注入。

  • 🧵 并发安全 · Concurrency-safe ENsync.Mutex protects shared state; events emitted outside the lock; go test -race clean across 60+ tests and 9 lifecycle tests. 中文sync.Mutex 保护共享状态,事件在锁外发射;60+ 测试 + 9 个生命周期测试均通过 -race

  • 🛟 Panic 隔离 · Panic-safe callbacks EN — User callbacks (EventHandler / ErrorHandler / DecisionHandler) are wrapped with defer recover. An EventHandler panic is reported via onError and does not kill any Watcher goroutine. 中文 — 用户回调(EventHandler / ErrorHandler / DecisionHandler)全部被 defer recover 包裹。业务 handler panic 会经 onError 上报,不会杀死 Watcher 的任何 goroutine。

  • 🔄 可热重载 · Hot-reload lifecycle (v0.5.0+) ENWatcher.Stop(ctx) + re-run preserves known / cooldown / flap state across config reload (SIGHUP pattern). Real-router validated: 10 restarts on MT7981 show zero goroutine leak (Threads: 15 → 15). 中文Watcher.Stop(ctx) + 再次 Run() 在热重载配置时保留 known / 冷却 / 抖动状态(SIGHUP 模式)。MT7981 真机验证:10 次重启后线程数 15 → 15,零泄漏。详见 docs/SIGHUP-real-device-test.md

  • 🎯 Typed errors · Sentinel errors + structured validation ENErrHandlerRequired / ErrInvalidConfig / ErrNoFetcher / ErrFetchFailed / ErrAlreadyRunning, all errors.Is-compatible. Config.Validate returns *ConfigError with field-level detail reachable via errors.As — ideal for web config UIs. 中文 — 5 种 sentinel 错误,全部支持 errors.Is 判别;Config.Validate 返回 *ConfigError(字段/值/原因),通过 errors.As 取字段级详情,非常适合 Web 配置 UI 做表单校验。


快速开始 · Quick Start

作为库使用 · Use as a library
import (
    "context"
    "fmt"
    "log"
    "os/signal"
    "syscall"

    argus "github.com/xxl6097/argusd"
)

func main() {
    ctx, stop := signal.NotifyContext(context.Background(),
        syscall.SIGINT, syscall.SIGTERM)
    defer stop()

    w := argus.New(
        argus.OnFetcherDetected(func(k argus.FetcherKind) {
            log.Printf("data source / 数据源: %s", k)
        }),
    )

    err := w.Run(ctx, func(e argus.Event) {
        switch e.Kind {
        case argus.EventOnline:
            fmt.Printf("[+] %s joined / 上线 %s\n", e.Device.MAC, e.Device.IP)
        case argus.EventOffline:
            fmt.Printf("[-] %s left / 离线\n", e.Device.MAC)
        case argus.EventChange:
            for _, c := range e.Changes {
                fmt.Printf("[~] %s %s: %q → %q\n",
                    e.Device.MAC, c.Field, c.Old, c.New)
            }
        }
    }, nil)
    if err != nil {
        log.Fatal(err)
    }
}
作为 CLI 使用 · Use as a CLI

EN — Prebuilt binaries for common OpenWrt CPU architectures are published on the Releases page (amd64 / arm64 / armv5 / armv7 / mips / mipsle / mips64 / mips64le / riscv64 / 386, all static). 中文 — 常见 OpenWrt 架构的预编译二进制发布在 Releases 页面(amd64 / arm64 / armv5 / armv7 / mips / mipsle / mips64 / mips64le / riscv64 / 386, 全部静态链接)。

# EN: Download the matching archive, verify, and deploy.
# 中文: 下载对应架构的包, 校验, 上传路由器。
VER=v0.1.0        # 替换为实际版本
TARGET=linux-mipsle-softfloat   # 替换为你的架构
curl -LO "https://github.com/xxl6097/argusd/releases/download/${VER}/argusd_${VER}_${TARGET}.tar.gz"
curl -LO "https://github.com/xxl6097/argusd/releases/download/${VER}/SHA256SUMS"
sha256sum -c SHA256SUMS --ignore-missing
tar -xzf argusd_${VER}_${TARGET}.tar.gz
scp argusd_${VER}_${TARGET}/argusd root@192.168.1.1:/tmp/argusd
ssh root@192.168.1.1 '/tmp/argusd'

Or build from source · 或从源码构建:

# EN: Cross-compile for OpenWrt (aarch64 example).
# 中文: 跨编译到 OpenWrt (以 aarch64 路由器为例)。
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 \
    go build -trimpath -ldflags="-s -w" \
    -o argusd ./cmd/argusd

# EN: Deploy and run.
# 中文: 上传并运行。
scp argusd root@192.168.1.1:/tmp/
ssh root@192.168.1.1 '/tmp/argusd'

Sample output · 输出示例:

2026/05/09 18:40:21 data source / 数据源: ahsapd
MAC 地址             IP 地址          主机名            厂商     类型    信号         无线
──────────────────────────────────────────────────────────────────────────────────────────
2C:CF:67:1D:27:AC    192.168.1.11     raspberrypi       rasp..   PC      -            wired
B0:FC:36:32:94:61    192.168.1.5      lenovo            DESK..   Phone   -38(极强)    5G/avgb-5G
BA:79:97:73:89:8D    192.168.1.213    BA799773898D      -        Phone   -44(极强)    5G/avgb-5G
──────────────────────────────────────────────────────────────────────────────────────────
4 devices online · 4 台设备在线 (WiFi: 3, Wired 有线: 1)

[2026-05-09 18:42:03] [syslog/系统日志] WIFI_CONNECT    BA:79:...
[2026-05-09 18:42:03] [syslog/系统日志] DHCP_ACK        BA:79:... IP=192.168.1.213
[2026-05-09 18:42:03] [event/事件]   ONLINE / 上线      BA:79:... 192.168.1.213 iPhone -44(极强) 5G/avgb-5G

Web UI · 内置仪表盘 (v0.13.0+)

EN — Argus ships an opt-in, zero-dependency HTTP + Server-Sent-Events dashboard in the argusweb subpackage. Single embedded HTML file, vanilla JS, mobile-responsive, Chinese-only labels (v0.15.1). Pass -listen :8080 to argusd, or wire argusweb.NewServer into your own http.Handler tree.

中文 — Argus 在 argusweb 子包内置了零依赖的 HTTP + SSE 仪表盘:单一嵌入式 HTML、原生 JS、移动端自适应、纯中文界面 (v0.15.1)。argusd-listen :8080 即可启动,或在你自己的 HTTP 服务中挂载 argusweb.NewServer

界面概览 · Screens

桌面端主视图 — 左侧设备表(状态/MAC/IP/主机名/厂商/信号/类型 七列,IP 列带 🔒 静态标记和 📌 一键预约按钮,主机名列带 ✎ 重命名按钮),右侧实时事件流(SSE 推送),右上角是连接状态、在线/离线计数和系统按钮(重启网络/重启路由器)。

桌面端

静态 IP 弹窗 — 点 📌 进入,可直接指定 IP、填名称(支持中文/空格);勾选"立即生效(重启 WiFi)"则执行 wifi reload / ahsapd restart 让所有客户端瞬断 3~5 秒自动重连,新 IP 立刻生效;不勾选则只写配置 + 尝试踢该设备,适用于厂商固件支持单机踢的环境。

静态 IP 弹窗

别名重命名 — 点 ✎ 进入行内重命名表单,允许中文、空格、点号、连字符,空字符串即清除别名;持久化到 aliases.json(原子写入)。

重命名

IP 冲突一键替换 — 当目标 IP 已被其它 MAC 预留时,后端返回 409 Conflict,前端弹出确认框并标明占用者 MAC;点"确定"自动 DELETE 原占用者再重试 POST,点"取消"两端配置都不变。

IP 冲突

移动端(宽度 ≤ 640px) — 自动切卡片布局,每台设备一张卡,MAC / 状态徽章 / 主机名 / 厂商 / 无线 / 信号 自上而下排列,适合手机查看与操作。

移动端

功能 · Features
功能 · Feature 说明 · Description 版本 · Since
实时设备表 · Live device table EN SSE-driven; MAC / IP / 主机名 / 品牌 / 类型 / 信号 / 无线 / 状态 columns · 中文 SSE 推送,8 列实时刷新 v0.13.0
在线/离线状态 · Online/Offline column EN Offline rows retained per WithOfflineRetention (default 7d / 512 entries); 相对时间"2m ago" · 中文 离线设备保留可配置,默认 7 天 / 512 条;离线后以"X 分钟前"相对时间显示 v0.13.3
移动端自适应 · Mobile responsive EN Card layout below 640px breakpoint · 中文 640px 以下自动切换卡片布局 v0.13.1
响应式列宽 · Adaptive columns EN table-layout:auto with per-column min-widths; columns expand to full content when screen is wide, truncate with ellipsis + hover tooltip only when cramped · 中文 屏幕够宽时列自动撑满,窄屏才按最小宽度截断并保留 hover tooltip v0.15.5
防抖动 · Reconnect coalescing EN OFFLINE→ONLINE bursts within 10s collapse into one RECONNECT row · 中文 10 秒内的 OFFLINE→ONLINE 抖动合并为一次 RECONNECT v0.13.2
品牌列 · Vendor column EN OUI lookup, ellipsis + tooltip for long names · 中文 OUI 品牌查询,超长省略号显示并悬停提示 v0.15.0
设备别名 · Aliases (renamable) EN ✎ inline-rename button · UTF-8 (Chinese / spaces / dots all accepted) · file-backed JSON, atomic writes · 中文 ✎ 行内重命名,允许中文/空格/点号,JSON 持久化、原子写 v0.14.0 / v0.15.4
静态 IP 预留 · Static DHCP reservations EN 📌 button → modal; UCI-backed; optional immediate-apply (reload + lease prune + ARP flush + station kick) · 中文 📌 按钮弹窗预约,UCI 持久化,可选"立即生效"执行配置重载 + 租约清理 + ARP 清理 + 踢设备 v0.15.0 / v0.15.2 / v0.15.7
IP 冲突保护 · IP conflict guard EN 409 Conflict if target IP already bound to a different MAC; UI offers 1-click "replace" (delete old, then retry) · 中文 目标 IP 已占用时返回 409,UI 提供一键"替换"(先删旧再改) v0.15.3 / v0.15.5
一键修复 · Recovery endpoint EN POST /api/dhcp?purge_argus=1 removes every dhcp.argus_* section · 中文 一键清除所有 dhcp.argus_* 段,用于配置被污染时恢复 v0.15.3
立即生效(可选) · Opt-in WiFi restart EN Save-dialog checkbox runs wifi reload / ahsapd restart so every client re-associates within seconds — nuclear option for firmwares where per-station kick is a no-op · 中文 弹窗里勾选后执行 wifi reload / 重启 ahsapd,所有客户端秒级重连;用于厂商固件不支持单机踢的场景 v0.15.8
系统按钮 · System actions EN Header "重启网络" (soft, 5-15s LAN blip, config preserved) and "重启路由器" (hard, 30-60s full reboot) buttons, each with confirmation prompts · 中文 右上角 "重启网络"(软重启,5-15 秒 LAN 瞬断,配置保留)和 "重启路由器"(硬重启,30-60 秒全断)两个按钮,各自带确认对话框 v0.15.9
写操作鉴权 · Write auth EN WithWriteAuth(predicate) gates every POST/DELETE (aliases / dhcp / system); default allows loopback + RFC1918 · 中文 默认仅放行环回与内网,可自定义;覆盖所有写操作(别名 / DHCP / 系统) v0.14.0
UI 细节 · UI details
  • 状态徽章 · 连接状态(已连接 / 重连中…)、在线/离线计数常驻右上角。
  • 🔒 图标 · 已配置静态 IP 的设备 IP 前显示锁符,hover 文字 "已静态分配"。
  • 📌 按钮 · 点开静态 IP 弹窗;若该 MAC 已预留,底部会多出红色"移除"按钮。
  • ✎ 按钮 · 点击进入行内重命名表单,回车保存,Esc 取消,空字符串即清除别名。
  • 事件徽章颜色 · 上线/重连 绿色、离线/抖动 红色、变更 黄色。
  • 长文本 hover · 任何列被 ellipsis 截断后,鼠标悬停显示完整内容。
  • Toast 反馈 · 保存静态 IP 后底部弹出多行状态条:已重载 / 已清除旧租约 / 已清除 ARP 缓存 / 已踢出 / 已重启 WiFi,一眼看懂服务端到底做了什么。
  • 离线设备仍可管理 · 离线条目半透明显示,但 ✎ / 📌 按钮仍可点,可以提前为不在线的设备设置别名和静态 IP。
启动 · Running
# CLI: bind on all interfaces, port 8080
./argusd -listen :8080 \
         -aliases /etc/argusd/aliases.json   # 可选: 启用别名存储
# 浏览器访问 http://<router-ip>:8080/

或在 Go 代码里挂载 · Or mount in your own server:

w := argus.New(argus.WithFetcher(...))

aliases := argusweb.NewAliasStore("/etc/argusd/aliases.json")
dhcp, _ := argusweb.NewUCIDHCPManager() // 非 OpenWrt 主机返回 ErrDHCPManagerUnavailable

srv := argusweb.NewServer(w,
    argusweb.WithAliases(aliases),
    argusweb.WithDHCPManager(dhcp),
    argusweb.WithOfflineRetention(7*24*time.Hour),
    argusweb.WithOfflineMax(512),
    argusweb.WithWriteAuth(func(r *http.Request) bool {
        // 自定义鉴权 · custom auth predicate
        return r.Header.Get("X-Token") == os.Getenv("ARGUS_TOKEN")
    }),
)
w.RegisterEventHandler(srv.OnEvent) // 让 SSE 流转发事件
go http.ListenAndServe(":8080", srv)
HTTP API

所有响应均为 JSON,写操作受 WithWriteAuth 控制(默认环回 + RFC1918 放行,其它返回 403)。

路由 · Route 方法 说明
/ GET 仪表盘 HTML(单文件嵌入)
/api/devices GET {count, online, offline, capabilities:{aliases,dhcp}, devices:[...]};每行含 status / offline_at_ms / alias
/api/events GET SSE 流,事件名 = EventKind.String()(ONLINE / OFFLINE / CHANGE)
/api/aliases GET / POST / DELETE MAC ↔ 友好名 CRUD;503 表示未挂 WithAliases
/api/dhcp GET / POST / DELETE 静态 DHCP 预留 CRUD;503 表示未挂 WithDHCPManager;POST/DELETE 支持 ?restart_wifi=1 触发"立即生效"(v0.15.8+)
/api/dhcp?purge_argus=1 POST 一键清除全部 dhcp.argus_* 段(恢复工具,v0.15.3+)
/api/system/restart-network POST /etc/init.d/network restart 软重启网络服务(v0.15.9+)
/api/system/reboot POST /sbin/reboot 彻底重启路由器(v0.15.9+)

POST /api/dhcp 错误码:

  • 400 — MAC / IP / name 非法
  • 403 — 写操作鉴权未通过
  • 409 — 目标 IP 已被其它 MAC 预留;body {error, ip, owner_mac} 指明冲突方 (v0.15.3+)
  • 503 — 服务未挂载 DHCPManager

applyReport(所有 DHCP 写操作响应 apply 字段)包含的状态: reloaded[] · pruned[] · arp_flushed · kicked · wifi_restarted,前端据此渲染 toast。

完整 wire shape 见 STABILITY.md(自 v0.13.0 起为稳定 API 表面)。

兼容性 · DHCP backend compatibility

NewUCIDHCPManager() 仅在 OpenWrt(任何带 uci CLI 的系统)上可用;其它平台返回 ErrDHCPManagerUnavailable。已在 MediaTek MT7981 / C-Life 厂商固件(odhcpd)与官方 OpenWrt(dnsmasq)上验证。

注意双 DHCP 服务器 · 如果 LAN 里有"旁路由"(iStoreOS / OpenClash 等)默认开启 DHCP,会和主路由抢答,导致设备网关随机变成旁路由 IP、静态预留间歇失效。排查:主路由上 ip neigh 看各设备网关;修复:在旁路由上 uci set dhcp.lan.ignore=1 && uci commit dhcp && /etc/init.d/dnsmasq restart


架构 · Architecture

EN — Six feeds enter the Event Fusion Engine; the Watcher emits events (business), decisions (observability), and errors (failures). 中文 — 六路数据进入融合引擎,由 Watcher 统一产出三类回调:业务事件 / 决策观测 / 错误上报。

                       ┌──────────────┐
                       │   logread    │ ← realtime kernel events / 实时内核事件
                       │      -f      │   (Connect/Disconnect/Deauth/DHCPACK)
                       └──────┬───────┘
                              │
 ┌─ ubus call ────┐    ┌──────┼──────┐     ┌─ ARP state ──┐
 │ ahsapd.sta or  │ →  │  Event      │  ←  │ ip neigh     │
 │ hostapd.<iface>│    │  Fusion     │     │ FAILED/OK    │
 └────────────────┘    │  Engine     │     └──────────────┘
                       │  融合引擎    │
 ┌─ DHCP leases ──┐    │             │     ┌─ ICMP probe ─┐
 │ /tmp/dhcp.     │ →  │             │  ←  │ ping -c 1    │
 │   leases       │    │             │     │ -W 1         │
 └────────────────┘    └──────┬──────┘     └──────────────┘
                              │
                        ┌─────▼──────┐
                        │  Watcher   │  ← diff + cooldown + flap-suppress
                        │  监听器     │
                        └─────┬──────┘
                              │
                 ┌────────────┼────────────┐
                 ▼            ▼            ▼
            EventHandler  DecisionHandler  ErrorHandler
            业务事件       决策观测         错误上报
            (business)    (observability)  (failures)

EN — See ONLINE.md and OFFLINE.md for detailed decision flows. 中文 — 完整判定流程参见 ONLINE.mdOFFLINE.md


API 速查 · API Overview

Type · 类型 Purpose · 用途
argus.Watcher EN Main entry · 中文 主入口;New(opts...) *Watcher, Run, Stop, List, Known, EnsureFetcher, FetcherKind
argus.Event / EventKind EN Business events (Online/Offline/Change) · 中文 业务事件
argus.Decision / DecisionKind EN Internal decision trace (17 branches) · 中文 内部判定链路(17 种分支)
argus.Config / argus.ConfigError EN Tunable thresholds + structured validation errors (v0.9.0+) · 中文 阈值配置 + 结构化校验错误
argus.Fetcher EN Data source interface, auto-detected · 中文 数据源接口,自动探测
argus.Prober EN Liveness probe; default ICMPProber{Timeout: 1s} · 中文 活性探测,默认 ICMP
argus.Hint / argus.HintSource / argus.DefaultHintSource EN Injectable enrichment (v0.7.0+) — DHCP/ARP on non-OpenWrt targets · 中文 可注入的补全来源
argus.LoggerHandler / LogLevel / LogAttr EN Structured logging hook (v0.9.0+) · 中文 结构化日志钩子
argus.SpanRecorder / SpanRecorderFunc EN Distributed-tracing hook (v0.12.0+) · 中文 分布式追踪钩子
argus.SyslogEvent EN Raw syslog parse result · 中文 原始系统日志解析结果
argus.DetectLocalLocation() EN Parse /etc/TZ*time.Location (no global mutation) · 中文 解析 /etc/TZ,不修改全局状态
argus.SetupLocalTimezone() EN Deprecated. Mutates time.Local · 中文 已废弃,修改全局 time.Local
argus.ErrHandlerRequired / ErrInvalidConfig / ErrNoFetcher / ErrFetchFailed / ErrAlreadyRunning EN Sentinel errors (errors.Is-compatible) · 中文 Sentinel 错误,可用 errors.Is 判别
github.com/xxl6097/argusd/argusmetrics EN Zero-dep Counters + LabeledCounters (v0.7.0 / v0.10.0+) · 中文 零依赖计数器
github.com/xxl6097/argusd/argustest EN FixedFetcher / FakeProber for downstream tests (v0.6.0+) · 中文 下游测试用的数据源 fixture

Functional options · 函数式选项:

argus.WithConfig(cfg)                      // EN: override defaults · 中文: 覆盖默认
argus.WithFetcher(custom)                  // EN: custom data source · 中文: 注入自定义数据源
argus.WithProber(nil)                      // EN: disable liveness probe · 中文: 关闭活性探测
argus.WithBaseline(old.Known())            // EN: seed known-set on restart · 中文: 热重载保留设备表
argus.WithHintSource(custom)               // EN: custom DHCP/ARP enrichment · 中文: 自定义补全源 (v0.7.0+)
argus.WithLogger(h)                        // EN: structured logging · 中文: 结构化日志 (v0.9.0+)
argus.WithSpanRecorder(r)                  // EN: distributed tracing · 中文: 分布式追踪 (v0.12.0+)
argus.OnFetcherDetected(func(k) {...})     // EN: detection callback · 中文: 自动探测回调
argus.WithDecisionHandler(func(d) {...})   // EN: decision trace · 中文: 决策观测

配置调优 · Configuration

EN — All thresholds live in argus.Config. Zero values preserve defaults. 中文 — 所有阈值集中在 argus.Config,传零值保留默认。

w := argus.New(argus.WithConfig(argus.Config{
    // Polling cadence · 轮询节奏
    PollInterval:  1 * time.Second,   // default · 默认 1s
    OfflineMisses: 5,                 // default · 默认 5
    FetchTimeout:  3 * time.Second,   // default · 默认 3s

    // Anti-flap · 抗抖动
    OfflineCooldown:            90 * time.Second,
    CooldownReleaseRSSI:        -65,
    WeakRSSI:                   -80,
    ExtremelyWeakRSSI:          -88,
    WeakMissThreshold:          5,
    ExtremelyWeakMissThreshold: 2,
    FlapSuppressionWindow:      30 * time.Second,
}))

Guidelines · 使用建议:

Scenario · 场景 Suggested change · 建议配置
Aggressive IoT gateway · 激进响应、容忍噪音 FlapSuppressionWindow: 0, OfflineCooldown: time.Nanosecond
Home/away automation · 家庭自动化 EN keep defaults · 中文 保留默认
Crowded WiFi environment · 拥挤无线环境 WeakRSSI: -75, WeakMissThreshold: 10
Trust AP table only · 完全信任 AP 关联表 WithProber(nil)

可观测性 · Observability

EN — Argus exposes five opt-in observability channels; pick the right one for the right audience. 中文 — Watcher 对外暴露五路 opt-in 可观测性通道, 不同受众用不同通道。

Channel · 通道 Type · 类型 Frequency · 频率 Use case · 用途
EventHandler (arg to Run) Event Sparse · 稀疏 EN Business logic · 中文 业务逻辑(home/away 自动化)
ErrorHandler (arg to Run) error Rare · 罕见 EN Non-fatal failures · 中文 非致命错误
WithDecisionHandler Decision Dense · 密集 EN Tuning / debugging · 中文 调参 / 排障
WithLogger (v0.9.0+) LogLevel + attrs Lifecycle + anomaly EN slog/zap/zerolog bridge · 中文 结构化日志桥接
WithSpanRecorder (v0.12.0+) span start/finish Per Run + per disconnect EN OTel / Datadog tracing · 中文 分布式追踪

Plus argusmetrics subpackage for in-process counter aggregation (bridgeable to Prometheus / OTLP in ~10 lines; see godoc).

EN — For raw syslog mirroring, call WatchSyslog(ctx, func(SyslogEvent), onError) directly — it's a standalone helper, not a Watcher option. 中文 — 需要镜像原始系统日志时,直接调用 WatchSyslog(ctx, func(SyslogEvent), onError),它是独立函数,非 Watcher 选项。

Sample decision trace · 决策跟踪示例:

[decision/决策] CONNECT_HINT     BA:79:... (IP=192.168.1.213)
[decision/决策] CONNECT_EMIT     BA:79:... (IP=192.168.1.213)
[event/事件]    ONLINE           BA:79:... 192.168.1.213 iPhone -44(极强) 5G/avgb-5G
[decision/决策] POLL_WEAK_MISS   BA:79:... (RSSI=-82 misses=3/5)
[decision/决策] POLL_WEAK_MISS   BA:79:... (RSSI=-85 misses=5/5)
[decision/决策] OFFLINE_EMIT     BA:79:... (via=poll RSSI=-85)
[event/事件]    OFFLINE          BA:79:...

ENDecisionHandler is zero-cost when not registered — no allocations, no time calls. 中文 — 不注册 DecisionHandler 时完全零成本:不分配对象、不调用 time.Now()


路线图 · Roadmap

  • EN ahsapd / hostapd dual fetcher with auto-detection 中文 ahsapd / hostapd 双数据源 + 自动探测
  • EN syslog logread -f real-time stream 中文 logread -f 实时日志流
  • EN ICMP liveness probe with parallel semaphore 中文 ICMP 活性探测 + 并发信号量
  • EN Cooldown + flap suppression 中文 冷却期 + 抖动抑制
  • EN Decision handler observability 中文 决策回调可观测性
  • EN go test -race clean (multi-Go-version matrix, 1.21–1.25) 中文 竞态检测全部通过(多 Go 版本矩阵 1.21–1.25)
  • EN Lifecycle: Stop + restart (v0.5.0) 中文 生命周期:Stop + 热重启(v0.5.0)
  • EN Portability: HintSource abstraction (v0.7.0) 中文 可移植性:HintSource 抽象(v0.7.0)
  • EN Metrics: argusmetrics.Counters + LabeledCounters (v0.7.0 / v0.10.0) 中文 指标:argusmetrics.Counters + LabeledCounters(v0.7.0 / v0.10.0)
  • EN Structured logging hook WithLogger (v0.9.0) 中文 结构化日志钩子 WithLogger(v0.9.0)
  • EN Structured validation errors ConfigError (v0.9.0) 中文 结构化配置校验错误 ConfigError(v0.9.0)
  • EN Distributed tracing hook SpanRecorder (v0.12.0) 中文 分布式追踪钩子 SpanRecorder(v0.12.0)
  • EN Fuzz targets for syslog / DHCP lease parsers (v0.12.0) 中文 Syslog / DHCP 租约解析器 fuzz 目标(v0.12.0)
  • EN Built-in Web UI (HTTP + SSE, v0.13.0) · 中文 内置 Web UI(HTTP + SSE, v0.13.0)
  • EN Device aliases with UTF-8 names (v0.14.0 / v0.15.4) · 中文 设备别名,允许中文(v0.14.0 / v0.15.4)
  • EN Static DHCP reservations via UCI + immediate-apply (v0.15.0 / v0.15.2 / v0.15.7 / v0.15.8) · 中文 静态 IP 预留 + 立即生效(v0.15.0 / v0.15.2 / v0.15.7 / v0.15.8)
  • EN IP-conflict 409 + one-click replace + PurgeArgusOwned recovery (v0.15.3 / v0.15.5) · 中文 IP 冲突 409 + 一键替换 + 一键修复(v0.15.3 / v0.15.5)
  • EN System endpoints: reboot + restart-network (v0.15.9) · 中文 系统接口:重启路由器 + 重启网络(v0.15.9)
  • EN v1.0 tagged — Stable surface locked under SemVer v1 rules (v1.0.0) · 中文 v1.0 已发布 — SemVer v1 规则下稳定表面锁定(v1.0.0)
  • EN Direct ubus socket integration (skip CLI) · 中文 直连 ubus socket,跳过 CLI
  • EN IPv6-only device support · 中文 仅 IPv6 设备支持
  • EN Home Assistant device_tracker bridge · 中文 Home Assistant 桥接
  • EN Prometheus /metrics endpoint (argusweb bridge) · 中文 Prometheus /metrics 出口(argusweb 桥接)

兼容性 · Compatibility

Platform · 平台 Data source · 数据源 Status · 状态
MediaTek MT7981 vendor fw · 厂商固件 ahsapd EN Reference target · 中文 参考目标
OpenWrt 23.05+ stock · 官方 hostapd.* 🧪 EN Theoretical · 中文 待实测
Any Linux with logread+ubus syslog-only ⚠️ EN Events only, no device table · 中文 仅事件,无设备列表

EN — Go 1.21+ (N-2 policy: current + two preceding minor versions). No cgo. Cross-compiles to any GOOS/GOARCH that runs OpenWrt. 中文 — Go 1.21+(N-2 策略:当前版本 + 前两个 minor 版本)。不使用 cgo, 可跨编译到任何 OpenWrt 支持的 GOOS/GOARCH。


贡献 · Contributing

EN — PRs welcome. See CONTRIBUTING.md. Before submitting: 中文 — 欢迎 PR,详见 CONTRIBUTING.md。提交前请本地通过:

go vet ./...
go test -race ./...
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build ./cmd/argusd

更多文档 · More Docs

  • CHANGELOG.mdEN version history (features & fixes) · 中文 版本历史 (新特性 & Bug 修复)
  • STABILITY.mdEN API stability guarantees & v1.0 criteria · 中文 API 稳定性承诺与 v1.0 条件
  • ONLINE.mdEN online decision deep-dive · 中文 上线判定深度解析
  • OFFLINE.mdEN offline + cooldown analysis · 中文 离线与冷却机制解析
  • docs/SIGHUP-real-device-test.mdEN v0.5.0 Stop+Restart real-router validation report · 中文 v0.5.0 SIGHUP 热重载真机测试报告
  • docs/blog/ios-static-ip.mdEN Debugging story: the 3 ways "set static IP" silently fails on iOS + OpenWrt · 中文 调试故事:OpenWrt + iPhone 静态 IP 不生效的三种死法
  • GoDoc — API reference · API 文档

许可证 · License

MIT © 2026 — see LICENSE


"Every station. Every event. Every eye open." "每一台设备,每一次事件,每一只眼睛都不闭上。"

Documentation

Overview

Package argus provides real-time device presence detection for OpenWrt routers.

Argus — named after the hundred-eyed giant of Greek myth whose eyes never all slept — fuses six data sources into a single millisecond-grade event stream (Online / Offline / Change): ahsapd (vendor) or hostapd.* (stock) via ubus, logread -f syslog, /tmp/dhcp.leases, ip neigh, and ICMP liveness probes.

Quick start

w := argus.New()                          // auto-detect fetcher
devices, _ := w.List(ctx)                 // one-shot snapshot
err := w.Run(ctx, func(e argus.Event) {   // real-time stream
    // handle EventOnline / EventOffline / EventChange
}, nil)                                   // nil ErrorHandler = discard errors

Architecture

                  ┌───────────────────────────────┐
                  │   HintSource (enrichment)     │
                  │   /tmp/dhcp.leases + ip neigh │
                  └────────────────┬──────────────┘
                                   │ {IP, Hostname}
                                   ▼
┌──────────┐    ┌───────────┐    ┌──────┐    ┌────────────────┐    ┌────────────────┐
│ Fetcher  │───▶│ baseline  │───▶│ diff │───▶│ DecisionHandler│───▶│ EventHandler   │
│ (ahsapd/ │    │ (known)   │    │      │    │  (optional,    │    │  Online/       │
│  hostapd)│    └───────────┘    └───┬──┘    │   zero-cost    │    │  Offline/      │
└──────────┘                         │       │   when nil)    │    │  Change        │
      ▲                              ▼       └────────────────┘    └────────────────┘
      │                       ┌──────────┐
      │                       │  Prober  │ (ICMP liveness; filters out fake-online)
      │                       └──────────┘
      │
┌────┴───────────┐
│ logread -f     │ (syslog hint stream; disconnect / deauth / assoc / DHCP)
└────────────────┘

The diff stage also consults an offlineCooldown map (90 s default) to suppress weak-signal flaps at the edge, and a FlapSuppressionWindow (30 s default) to collapse rapid-fire reconnects.

Extension points

  • WithFetcher: plug in a custom data source (tested with [argustest.FixedFetcher]).
  • WithProber: replace ICMP liveness check (e.g. a faked in-memory map for tests).
  • WithHintSource: inject a custom enrichment source for non-OpenWrt firmwares with /var/lib/misc/dnsmasq.leases or equivalent.
  • WithDecisionHandler: receive internal decision traces (zero-cost when unset).
  • WithLogger: structured logging adapter (slog / zap / zerolog in ~5 lines).

Lifecycle

A single *Watcher may be Watcher.Run-Watcher.Stop-[Watcher.Run]ed repeatedly (SIGHUP hot-reload pattern). State preserved across restart: known set, offlineCooldown, lastEventAt, detected Fetcher. State reset: in-flight misses, disconnect dedup map, syslog hint channel, drop counter. Concurrent Run calls on the same Watcher fast-fail with ErrAlreadyRunning.

Observability

  • Logs via WithLogger (library never logs from the hot path; only lifecycle and recoverable anomalies).
  • Metrics via the github.com/xxl6097/argusd/argusmetrics subpackage ([argusmetrics.Counters] for totals, [argusmetrics.LabeledCounters] for per-SSID / per-MAC / per-band bucketing), both zero-dependency.
  • Decision traces via WithDecisionHandler — every internal choice the diff engine makes surfaces as a Decision record.

Error handling

Library errors are matchable with errors.Is:

errors.Is(err, argus.ErrHandlerRequired)  // nil callback
errors.Is(err, argus.ErrInvalidConfig)    // Config.Validate rejected
errors.Is(err, argus.ErrNoFetcher)        // ubus detection found nothing
errors.Is(err, argus.ErrFetchFailed)      // baseline fetch failed
errors.Is(err, argus.ErrAlreadyRunning)   // concurrent Run on same Watcher

Config.Validate returns a *ConfigError exposing the offending field, reachable via errors.As for form-level UI feedback.

Stability

See the STABILITY.md document in the repository. The 0.x line is "minor-zero stable" (no breaking change to listed Stable surface within a 0.x.y window); v1.0 will lock that surface under SemVer v1 rules.

Supported Go versions

The current Go release and the two preceding minors (N-2 policy). CI matrix tests every commit on Go 1.21 – 1.25.

Chinese summary · 中文摘要

Argus 是一个针对 OpenWrt 路由器的实时设备感知库, 融合 ahsapd / hostapd / syslog / DHCP / ARP / ICMP 六路数据源形成毫秒级事件流 (Online / Offline / Change)。 名字取自希腊神话中的百眼巨人 —— 他的眼睛永远不会同时闭上。零第三方依赖, 内置 argusmetrics 零分配计数器, 通过 WithDecisionHandler 暴露决策 trace, 生命周期 Stop / Restart 支持热重载。

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrHandlerRequired 表示必填的回调 (典型: Run 的 onEvent) 为 nil。
	ErrHandlerRequired = errors.New("argus: handler required")
	// ErrInvalidConfig 表示 Config 字段取值非法 (由 Config.Validate 判定)。
	// Config.Validate 返回的具体错误会 wrap 这个 sentinel 且实现为 *ConfigError,
	// 允许 errors.As 拆出字段级详情。
	ErrInvalidConfig = errors.New("argus: invalid config")
	// ErrNoFetcher 表示未显式提供 Fetcher 且自动探测 ubus 时未找到可用数据源。
	ErrNoFetcher = errors.New("argus: no fetcher available")
	// ErrFetchFailed 包裹 Fetcher.Fetch 的失败, 便于上层通过 errors.Is 过滤拉取类错误。
	ErrFetchFailed = errors.New("argus: fetch failed")
	// ErrAlreadyRunning 表示 Watcher 已有一个 Run 正在执行。同一 Watcher 在任何时刻
	// 只能有一个 Run 活跃; 并发调用 Run 的后来者会立刻返回此错误, 避免共享状态被破坏。
	// 允许的模式是先 Stop 再 Run (重启语义), 参见 (*Watcher).Stop。
	ErrAlreadyRunning = errors.New("argus: watcher already running")
)

Sentinel 错误, 可通过 errors.Is 判别。

使用方式:

if err := w.Run(ctx, onEvent, nil); err != nil {
    switch {
    case errors.Is(err, argus.ErrHandlerRequired):
        // 忘传回调
    case errors.Is(err, argus.ErrInvalidConfig):
        // Config 非法 (可用 argus.AsConfigError 取出具体字段)
    case errors.Is(err, argus.ErrNoFetcher):
        // ubus 上没有 ahsapd / hostapd 服务
    }
}

Functions

func DetectFetcher

func DetectFetcher(ctx context.Context, timeout time.Duration) (Fetcher, FetcherKind, error)

DetectFetcher 探测路由器本机 ubus 上可用的接入设备数据源, 优先 ahsapd, 回退到 hostapd; 都不存在时返回错误。timeout 限制单次 ubus 探测耗时。

同时返回选中的 FetcherKind, 调用方可据此打日志。

func DetectLocalLocation

func DetectLocalLocation() *time.Location

DetectLocalLocation 按 OpenWrt 习惯探测路由器本机时区, 不修改全局状态。 优先级:

  1. /etc/TZ (POSIX 格式, 如 "CST-8")
  2. TZ 环境变量

探测失败时返回 nil。

func RenderTable

func RenderTable(devs []Device) string

RenderTable 把设备列表渲染为完整表格 (表头 + 分隔 + 行 + 分隔 + 汇总)。

func SetupLocalTimezone deprecated

func SetupLocalTimezone() *time.Location

SetupLocalTimezone 显式将 time.Local 设置为路由器本机时区。 **修改全局状态** time.Local, 影响整个进程。建议在 main 函数最早期调用一次。

注意: 此函数会写 time.Local, 在测试中调用会污染全局状态。 若库使用者已自行管理时区, 改用 DetectLocalLocation() 获取 *time.Location 即可。

在交叉编译且未引入 time/tzdata 的二进制中, 默认 time.Local 会回退到 UTC。 此函数能让 OpenWrt 路由器上的时间输出与系统时间一致。

返回选中的时区, 未识别时返回 UTC 不修改 time.Local。

Deprecated: 修改全局 time.Local 是库层面的反模式。库的使用方应改用 DetectLocalLocation() 获取 *time.Location, 然后在自己的代码里通过 time.Time.In(loc) 做本地化格式化, 或在 main 里自行设置 time.Local。 此函数保留用于向后兼容, 不会在后续版本中移除, 但不推荐新代码使用。

func TableHeader

func TableHeader() (header, separator string)

TableHeader 返回设备表格的表头和分隔线 (调用方可自行决定是否使用)。

func WatchSyslog

func WatchSyslog(ctx context.Context, onEvent SyslogHandler, onError ErrorHandler) error

WatchSyslog 启动 `logread -f` 实时监听系统日志, 解析 WiFi / DHCP 相关事件 并通过 onEvent 回调上报。阻塞直到 ctx 取消或子进程退出。

ctx 取消时会终止子进程并关闭 stdout 管道; 非 ctx 取消导致的退出通过返回值上报, 调用方应视为监听失败, 必要时自行重启。

系统日志是被动监听, 不产生额外 CPU / 网络开销。

Types

type AhsapdFetcher

type AhsapdFetcher struct {
	// Timeout 限制单次 ubus 调用耗时; 0 表示不超时。
	Timeout time.Duration
}

AhsapdFetcher 调用厂商私有 ubus 服务 `ahsapd.sta getStaInfo` 拉取设备列表。 适用于带 ahsapd 的厂商固件 (例如 MediaTek 7981 平台)。

func (AhsapdFetcher) Fetch

func (f AhsapdFetcher) Fetch(ctx context.Context) ([]Device, error)

Fetch 实现 Fetcher 接口。

type Change

type Change struct {
	Field string `json:"field"` // "IP" / "Hostname" / "Radio" / "SSID"
	Old   string `json:"old"`
	New   string `json:"new"`
}

Change 描述某个字段从旧值变为新值的细节, 仅在 EventChange 中使用。

type Config

type Config struct {
	// PollInterval 是相邻两次拉取之间的间隔。默认 1s。
	PollInterval time.Duration `json:"poll_interval,omitempty"`
	// OfflineMisses 是设备连续多少次未出现在拉取结果里才判定为离线 (默认路径, 不含 RSSI/ARP 加速)。
	// 默认 5。
	OfflineMisses int `json:"offline_misses,omitempty"`
	// FetchTimeout 限制单次拉取调用的耗时。默认 3s。
	FetchTimeout time.Duration `json:"fetch_timeout,omitempty"`

	// OfflineCooldown 设备刚判离线后的冷却期: 冷却期内重复的 EventOffline 被压制,
	// 且 RSSI < CooldownReleaseRSSI 的重新接入不触发 EventOnline。
	//
	// 只要设备持续处于弱信号状态, 冷却期会被每次轮询刷新, 避免抑制后再次触发重复离线。
	// 设为 time.Nanosecond 可实际禁用冷却期。默认值见 DefaultConfig。
	OfflineCooldown time.Duration `json:"offline_cooldown,omitempty"`
	// CooldownReleaseRSSI 冷却期内 RSSI 恢复到此值 (含) 之上才允许触发 EventOnline。
	// RSSI 为负值, 默认 -65 (强信号)。零值 (0 dBm) 无实际意义, 用作"使用默认"的标记。
	CooldownReleaseRSSI int `json:"cooldown_release_rssi,omitempty"`
	// WeakRSSI 信号弱的阈值, 低于此值触发 diff 的加速离线判定。默认 -80。
	WeakRSSI int `json:"weak_rssi,omitempty"`
	// ExtremelyWeakRSSI 信号极弱的阈值, 触发更激进的 threshold。默认 -88。
	ExtremelyWeakRSSI int `json:"extremely_weak_rssi,omitempty"`
	// WeakMissThreshold WeakRSSI ~ ExtremelyWeakRSSI 区间内的离线计数阈值。默认 5。
	WeakMissThreshold int `json:"weak_miss_threshold,omitempty"`
	// ExtremelyWeakMissThreshold < ExtremelyWeakRSSI 的离线计数阈值。默认 2。
	ExtremelyWeakMissThreshold int `json:"extremely_weak_miss_threshold,omitempty"`

	// FlapSuppressionWindow 抖动抑制窗口: 一台设备在此窗口内不会连续触发两个同类事件
	// (两次 Online / 两次 Offline)。用于抵消中等信号设备的快闪。默认 30s。
	// WithConfig 对零值按"保留默认"处理; 若需显式关闭, 请使用 DisableFlapSuppression。
	FlapSuppressionWindow time.Duration `json:"flap_suppression_window,omitempty"`

	// DisableCooldown 显式关闭冷却期机制。设置为 true 时, OfflineCooldown 相关的
	// 所有抑制逻辑 (COOLDOWN_SUPPRESS_ONLINE / COOLDOWN_SUPPRESS_OFFLINE /
	// COOLDOWN_CLEARED) 都不再触发, 离线事件后重新出现的设备立即触发上线。
	// 默认 false (启用冷却期)。此字段与 OfflineCooldown 数值不冲突: true 时忽略数值。
	DisableCooldown bool `json:"disable_cooldown,omitempty"`
	// DisableFlapSuppression 显式关闭抖动抑制窗口 (与 FlapSuppressionWindow=0 等价,
	// 但语义更清晰, 不依赖"零值 = 关闭"的约定)。默认 false (启用抑制)。
	DisableFlapSuppression bool `json:"disable_flap_suppression,omitempty"`
}

Config 控制 Watcher 的轮询节奏、离线判定阈值、冷却期与弱信号分级。 字段为零值时会在 New 中回退到默认值, 因此最小用法是 New() 不带任何 Option。

JSON / TOML / YAML 反序列化支持: 字段带 `json:` 标签, 下游可以直接从 配置文件加载。零值保留默认的约定在反序列化后依然生效 (通过 WithConfig 合并), 所以缺失字段不会把 Config 重置为零。

Example

ExampleConfig shows how to tune cooldown / flap-suppression knobs.

Zero values preserve defaults; to fully disable a feature, use DisableCooldown / DisableFlapSuppression rather than a magic value.

package main

import (
	argus "github.com/xxl6097/argusd"
)

func main() {
	_ = argus.New(argus.WithConfig(argus.Config{
		// Crowded WiFi environment: raise the "weak" threshold so more
		// borderline devices are treated as weak-signal.
		WeakRSSI:          -75,
		WeakMissThreshold: 10,

		// Turn off flap suppression entirely (e.g. aggressive IoT gateway
		// that wants every edge event, even duplicates).
		DisableFlapSuppression: true,
	}))
}
Example (JsonReload)

ExampleConfig_jsonReload shows loading Config from a JSON config file (e.g. /etc/argusd.json). Field names are stable across the v0.x line.

package main

import (
	"encoding/json"
	"log"

	argus "github.com/xxl6097/argusd"
)

func main() {
	jsonBlob := []byte(`{
        "poll_interval":         2000000000,
        "offline_misses":        7,
        "weak_rssi":             -75,
        "disable_flap_suppression": true
    }`)

	var cfg argus.Config
	if err := json.Unmarshal(jsonBlob, &cfg); err != nil {
		log.Fatal(err)
	}

	w := argus.New(argus.WithConfig(cfg))
	_ = w
}

func DefaultConfig

func DefaultConfig() Config

DefaultConfig 返回库的默认配置:

  • 上线 / 状态变更 ≈1s
  • 正常路径离线检测 ≈5s (OfflineMisses × PollInterval)
  • 信号极弱时离线检测 ≈2s
  • 弱信号边缘设备的上下线抖动被 OfflineCooldown (默认 90s) 压制

所有字段均可通过 WithConfig 覆盖, 传零值保留默认。

func (Config) String

func (c Config) String() string

String 返回配置的可读摘要, 用于启动 banner。

func (Config) Validate

func (c Config) Validate() error

Validate 校验配置合法性, 防止零值 / 负值导致死循环或 panic。

type ConfigError

type ConfigError struct {
	// Field is the Go-level struct field name (e.g. "OfflineMisses"),
	// stable across 0.x releases for any field in the Stable JSON list.
	Field string
	// Value is the offending value as passed by the caller (nil-safe —
	// zero values are included verbatim).
	Value any
	// Reason is a short human-readable description of the constraint
	// that was violated (e.g. "must be >= 1").
	Reason string
}

ConfigError describes a single invalid Config field. Returned by Config.Validate wrapped under ErrInvalidConfig.

Typical consumer (e.g. a web config UI) uses errors.As to extract the offending field for form-level feedback:

var ce *argus.ConfigError
if errors.As(err, &ce) {
    formErrors[ce.Field] = ce.Error()
}

The Unwrap chain matches ErrInvalidConfig so errors.Is(err, ErrInvalidConfig) still works for coarse-grained matching.

Example

ExampleConfigError shows how to extract field-level details from a Config validation failure — useful when surfacing errors in a web UI where each form field needs its own message.

package main

import (
	"errors"
	"fmt"

	argus "github.com/xxl6097/argusd"
)

func main() {
	cfg := argus.DefaultConfig()
	cfg.WeakMissThreshold = 0 // invalid

	err := cfg.Validate()
	if errors.Is(err, argus.ErrInvalidConfig) {
		var ce *argus.ConfigError
		if errors.As(err, &ce) {
			fmt.Printf("field=%s reason=%s", ce.Field, ce.Reason)
		}
	}
}
Output:
field=WeakMissThreshold reason=must be > 0

func (*ConfigError) Error

func (e *ConfigError) Error() string

func (*ConfigError) Unwrap

func (e *ConfigError) Unwrap() error

Unwrap lets errors.Is(err, ErrInvalidConfig) succeed.

type Decision

type Decision struct {
	Time   time.Time    `json:"time"`
	Kind   DecisionKind `json:"kind"`
	MAC    string       `json:"mac"`
	Detail string       `json:"detail,omitempty"` // 可选的人类可读上下文 (如 "RSSI=-75 misses=3/5")
}

Decision 是一次内部决策的观测记录, 通过 DecisionHandler 暴露给上层。 用于日志 / 监控 / 调试; 业务侧通常只关心 Event, 但需要调参或排障时 Decision 提供了完整的判定链路信息。

JSON 序列化: Kind 用英文稳定标识 (见 DecisionKind.MarshalJSON)。

func (Decision) String

func (d Decision) String() string

String 返回紧凑单行表示, 适合直接写入日志。

type DecisionHandler

type DecisionHandler func(Decision)

DecisionHandler 接收内部决策观测。可为 nil, 为 nil 时决策不收集, 完全零成本。

type DecisionKind

type DecisionKind int

DecisionKind 表示判定链路上的决策点类型。 每个决策代表 Watcher 内部的一次具体判断 (进入/跳过某个分支), 供上层观测库的"为什么触发/为什么没触发"过程, 便于调参和调试。

const (

	// DecisionConnectHintReceived: 收到 syslog 接入事件 hint (WPA完成/MAC表新增/DHCP分配)。
	DecisionConnectHintReceived DecisionKind = 1
	// DecisionConnectSkippedKnown: 已在 known 中, 防重复跳过。
	DecisionConnectSkippedKnown DecisionKind = 2
	// DecisionConnectEmitted: 基于 hint 信息构建基础记录并触发 EventOnline。
	DecisionConnectEmitted DecisionKind = 3
	// DecisionCooldownSuppressOnline: 冷却期 + 弱信号, 静默更新 known, 不触发 Online。
	DecisionCooldownSuppressOnline DecisionKind = 4
	// DecisionCooldownCleared: 冷却期内信号恢复到强 (或 RSSI 未知), 清除冷却, 允许 Online。
	DecisionCooldownCleared DecisionKind = 5
	// DecisionFlapSuppressOnline: 窗口期内同类 Online 事件被压制。
	DecisionFlapSuppressOnline DecisionKind = 6

	// DecisionDisconnectHintReceived: 收到 syslog 断开事件 hint (Disconnect/Deauth/Del Sta)。
	DecisionDisconnectHintReceived DecisionKind = 20
	// DecisionDisconnectIgnoredUnknown: 不在 known 中, 忽略。
	DecisionDisconnectIgnoredUnknown DecisionKind = 21
	// DecisionDisconnectPingOK: 500ms 后 ping 仍可达 (漫游), 不触发离线。
	DecisionDisconnectPingOK DecisionKind = 22
	// DecisionOfflineEmitted: 触发 EventOffline。
	DecisionOfflineEmitted DecisionKind = 23
	// DecisionFlapSuppressOffline: 窗口期内同类 Offline 事件被压制。
	DecisionFlapSuppressOffline DecisionKind = 24
	// DecisionCooldownSuppressOffline: 冷却期内重复离线被静默移除, 不重复触发事件。
	DecisionCooldownSuppressOffline DecisionKind = 25
	// DecisionDisconnectSkippedInflight: 同一 MAC 已有正在处理的 disconnect worker,
	// 后续重复 hint (典型: disconnect/deauth/Del Sta 三连发) 直接跳过, 不再各自
	// 进入 500ms Sleep + ping 流程。
	DecisionDisconnectSkippedInflight DecisionKind = 26

	// DecisionPollAPSleepProtected: 设备在 AP 关联表, RSSI 正常 ping 不通 → 息屏保护, 不计离线。
	DecisionPollAPSleepProtected DecisionKind = 40
	// DecisionPollWeakSignalMiss: 弱信号且 ping 不通, 累加 miss 计数 (可能尚未达阈值)。
	DecisionPollWeakSignalMiss DecisionKind = 41
	// DecisionPollARPFailedOffline: AP 关联表也没了, ARP 状态 FAILED/INCOMPLETE, 立即离线。
	DecisionPollARPFailedOffline DecisionKind = 42
	// DecisionPollMissesExhausted: 默认 miss 计数达阈值, 触发离线。
	DecisionPollMissesExhausted DecisionKind = 43
)

func (DecisionKind) Label

func (k DecisionKind) Label() string

Label 返回中文文案, 适合直接展示。

func (DecisionKind) MarshalJSON

func (k DecisionKind) MarshalJSON() ([]byte, error)

MarshalJSON 序列化为 DecisionKind.String() 的英文稳定标识 (如 "CONNECT_EMIT"), 而非整数值。整数值在 STABILITY.md 中被标为 Evolving, 字符串则稳定。

func (DecisionKind) String

func (k DecisionKind) String() string

String 返回决策类型的稳定英文标识, 便于日志 / grep / 序列化。

type DefaultHintSource

type DefaultHintSource struct {
	// LeasesPath DHCP 租约文件路径, 留空使用默认 /tmp/dhcp.leases。
	LeasesPath string
	// ARPCommand 列出 ARP 表的命令 (argv 形式, 不走 shell), 留空使用默认
	// []string{"ip", "neigh", "show"}。
	ARPCommand []string
	// CacheTTL 缓存过期时长, 0 或负值使用默认 5s。设为极短值 (如 1ns) 可实际禁用。
	CacheTTL time.Duration
	// contains filtered or unexported fields
}

DefaultHintSource 是库的默认 HintSource 实现, 读取 OpenWrt 标准路径。 可通过修改字段覆盖路径 / 命令, 适用于定制固件。

并发安全: 内部带 TTL 缓存, 默认 5 秒, 避免每秒多次 fork 子进程。

func (*DefaultHintSource) Hints

func (h *DefaultHintSource) Hints(ctx context.Context) map[string]Hint

Hints 实现 HintSource 接口。

type Device

type Device struct {
	MAC        string        `json:"mac"`                   // 小写冒号格式, 例如 "aa:bb:cc:dd:ee:ff"
	IP         string        `json:"ip,omitempty"`          // IPv4 地址, 拿不到时为空
	Hostname   string        `json:"hostname,omitempty"`    // 主机名, 拿不到时为空
	Vendor     string        `json:"vendor,omitempty"`      // 厂商或设备型号 (来自 ahsapd staVendor)
	Type       string        `json:"type,omitempty"`        // 设备类别, 如 "Phone" / "PC", 拿不到时为空
	Radio      string        `json:"radio,omitempty"`       // "2.4G" / "5G", 有线接入时为空
	SSID       string        `json:"ssid,omitempty"`        // 关联的 SSID, 有线时为空
	Channel    int           `json:"channel,omitempty"`     // 信道号, 0 表示未知
	RSSI       int           `json:"rssi,omitempty"`        // 信号强度 dBm, 0 表示未知
	UpTime     time.Duration `json:"uptime_ns,omitempty"`   // 已接入时长 (纳秒, 用 Duration.Nanoseconds)
	AccessTime time.Time     `json:"access_time,omitempty"` // 设备接入时刻 (路由器本机时区)
	LastSeen   time.Time     `json:"last_seen,omitempty"`   // 库最近一次观察到该设备的时刻
}

Device 是已接入设备的归一化记录, 由 Fetcher 产出, 也是事件回调中携带的载荷。

JSON 序列化: 字段名固化在下方 `json:` 标签中, 从 v0.6.0 起这些 JSON key 属于 STABILITY.md 中的 Stable 公共面, 可作为下游 Kafka / HTTP webhook / 数据库 列名安全使用。

调用方需要的所有信息都在结构体字段里, 不再依赖原始 ubus 字符串。

func (Device) Row

func (d Device) Row() string

Row 返回该设备在表格中对应的一行 (列已按显示宽度对齐)。

func (Device) String

func (d Device) String() string

String 返回适合事件日志一行的紧凑形式, 不做列对齐。

func (Device) Wired

func (d Device) Wired() bool

Wired 判断设备是否走有线接入 (没有无线频段信息)。

type ErrorHandler

type ErrorHandler func(error)

ErrorHandler 接收非致命的拉取错误。可为 nil; 为 nil 时错误被丢弃。

type Event

type Event struct {
	Time    time.Time `json:"time"`
	Kind    EventKind `json:"kind"`
	Device  Device    `json:"device"`            // 当前快照 (对 EventOffline 表示设备最后一次的快照)
	Changes []Change  `json:"changes,omitempty"` // 仅 EventChange 时填充
}

Event 是一次设备状态迁移的完整描述, 通过 Watcher.Run 的回调投递。

JSON 序列化: 字段名固化在下方 `json:` 标签中, 从 v0.6.0 起这些 JSON key 属于 STABILITY.md 中的 Stable 公共面。Kind 序列化为英文字符串 (ONLINE / OFFLINE / CHANGE), 保证跨版本稳定——详见 EventKind.MarshalJSON。

type EventHandler

type EventHandler func(Event)

EventHandler 接收实时事件回调。回调内不应阻塞过久, 以免影响下一轮拉取。

type EventKind

type EventKind int

EventKind 表示设备状态迁移的类型。

const (
	// EventOnline: 之前未见到的设备出现在了拉取结果里。
	EventOnline EventKind = iota + 1
	// EventOffline: 已知设备连续多次未出现在拉取结果里。
	EventOffline
	// EventChange: 已知设备的关键属性 (IP / 主机名 / 频段 / SSID) 发生了变化。
	EventChange
)

func (EventKind) Label

func (k EventKind) Label() string

Label 返回事件类型对应的中文文案, 适合直接展示。

func (EventKind) MarshalJSON

func (k EventKind) MarshalJSON() ([]byte, error)

MarshalJSON 将 EventKind 序列化为稳定字符串 (String() 的结果), 而非整数值。整数值可能在 minor 版本间变化; 字符串保证稳定。

func (EventKind) String

func (k EventKind) String() string

String 返回事件类型的稳定状态码 (英文, 适合日志 / 序列化 / 上报)。 需要展示给终端用户的中文文案请使用 Label。

func (*EventKind) UnmarshalJSON

func (k *EventKind) UnmarshalJSON(data []byte) error

UnmarshalJSON 支持双向兼容: 既接受字符串 ("ONLINE" / "OFFLINE" / "CHANGE"), 也接受老的整数表示, 便于从外部数据源回读。

type Fetcher

type Fetcher interface {
	Fetch(ctx context.Context) ([]Device, error)
}

Fetcher 抽象一次性拉取当前接入设备列表的能力。 默认通过 DetectFetcher 自动从 ubus 上选择 AhsapdFetcher (厂商私有) 或 HostapdFetcher (OpenWrt 官方 hostapd); 测试或对接其它服务时, 业务方可 自行实现该接口并通过 WithFetcher 注入。

type FetcherKind

type FetcherKind string

FetcherKind 标识自动选择的 Fetcher 类型, 便于打日志或上报。

const (
	FetcherAhsapd  FetcherKind = "ahsapd"
	FetcherHostapd FetcherKind = "hostapd"
)

type Hint

type Hint struct {
	IP       string
	Hostname string
}

Hint 是 HintSource 返回的单条辅助信息, 用于在 Fetcher 数据缺失时 填补 Device 的 IP / Hostname 字段。

导出于 v0.7.0, 便于自定义 HintSource 实现。

type HintSource

type HintSource interface {
	Hints(ctx context.Context) map[string]Hint
}

HintSource 抽象"从 DHCP 租约 / ARP 表获取设备辅助信息"的能力, 允许使用方在非 OpenWrt 平台 (Docker 容器 / 自研固件) 注入自定义实现。

默认由 DefaultHintSource 提供 (读 /tmp/dhcp.leases + `ip neigh show`)。 返回值按小写 MAC 索引; 实现可自行决定是否带缓存。

type HostapdFetcher

type HostapdFetcher struct {
	// Interfaces 是要查询的 hostapd 服务名列表 (例如 ["hostapd.wlan0", "hostapd.wlan1"])。
	// 留空表示动态探测。
	Interfaces []string
	// Timeout 限制单次 ubus 调用耗时; 0 表示不超时。
	Timeout time.Duration
}

HostapdFetcher 适用于 OpenWrt 官方固件: 通过 `ubus call hostapd.<iface> get_clients` 列出每个无线接口的关联终端, 通过 `get_status` 取 SSID / 频率 / 信道, 并合并 ARP 表 中的有线设备, 最后用 DHCP 租约补全主机名 / IP。

Interfaces 留空时会在每次 Fetch 前自动探测 (调用 `ubus list hostapd.*`)。

func (HostapdFetcher) Fetch

func (f HostapdFetcher) Fetch(ctx context.Context) ([]Device, error)

Fetch 实现 Fetcher 接口。

type ICMPProber

type ICMPProber struct {
	Timeout time.Duration
}

ICMPProber 基于本机 `ping` 命令实现 Prober。 Timeout 既是单次 ping 的等待秒数, 也是 Reachable 调用的最大耗时。

func (ICMPProber) Reachable

func (p ICMPProber) Reachable(ctx context.Context, ip string) bool

Reachable 实现 Prober 接口。空 IP 视为不可探测, 直接返回 true (信任 AP 关联表)。

type LogAttr

type LogAttr struct {
	Key   string
	Value any
}

LogAttr is a single structured log field. Key is always a stable identifier (e.g. "mac", "kind", "elapsed_ms"); Value can be any type slog / zap / zerolog can format.

type LogLevel

type LogLevel int

LogLevel is the severity the library emits for structured log events. Matches log/slog conventions (Debug < Info < Warn < Error) but is a plain int so consumers not on slog (zap/zerolog/stdlib log) can pattern match without importing log/slog.

const (
	// LogLevelDebug is for verbose internal tracing (hot-path detail).
	// Argus itself currently emits nothing at Debug; reserved for future use.
	LogLevelDebug LogLevel = -4
	// LogLevelInfo is for one-off lifecycle messages (detector picks a
	// Fetcher, Run starts, Stop completes).
	LogLevelInfo LogLevel = 0
	// LogLevelWarn is for recoverable anomalies (syslog buffer drops,
	// cache refresh failures, ubus subprocess killed).
	LogLevelWarn LogLevel = 4
	// LogLevelError is for non-recoverable failures that the library
	// still surfaces to onError. Every LogLevelError call has a
	// matching ErrorHandler invocation.
	LogLevelError LogLevel = 8
)

func (LogLevel) String

func (l LogLevel) String() string

String returns the slog-compatible level name.

type LoggerHandler

type LoggerHandler func(ctx context.Context, level LogLevel, msg string, attrs ...LogAttr)

LoggerHandler is called by the library to emit structured log lines. It is called synchronously from the calling goroutine and MUST NOT block.

The library never logs from the hot decision path (emitDecision, safeInvokeEvent): every log call site is a one-off lifecycle or recoverable-anomaly event, dispatched at most a few times per second.

When LoggerHandler is nil (the default, see WithLogger), library log call sites bail out on a nil check without invoking the handler. The variadic attrs slice may still be constructed at the call site — this is fine for the paths Argus logs from; the hot path has no log calls.

Typical adapters:

// log/slog
argus.WithLogger(func(ctx context.Context, level argus.LogLevel, msg string, attrs ...argus.LogAttr) {
    sa := make([]slog.Attr, len(attrs))
    for i, a := range attrs {
        sa[i] = slog.Any(a.Key, a.Value)
    }
    slog.LogAttrs(ctx, slog.Level(level), msg, sa...)
})

// zap
argus.WithLogger(func(_ context.Context, level argus.LogLevel, msg string, attrs ...argus.LogAttr) {
    fields := make([]zap.Field, len(attrs))
    for i, a := range attrs {
        fields[i] = zap.Any(a.Key, a.Value)
    }
    switch level {
    case argus.LogLevelWarn: zapLogger.Warn(msg, fields...)
    case argus.LogLevelError: zapLogger.Error(msg, fields...)
    default: zapLogger.Info(msg, fields...)
    }
})

ErrorHandler remains the primary error-reporting surface for programmatic error handling (errors.Is matching). LoggerHandler is complementary — it surfaces the same events with structured context for observability pipelines (slog / zap / OpenTelemetry), without forcing consumers to parse error.Error() strings.

type Option

type Option func(*Watcher)

Option 用于自定义 Watcher。

func OnFetcherDetected

func OnFetcherDetected(cb func(FetcherKind)) Option

OnFetcherDetected 注册回调, 在自动探测完成时被调用一次, 报告选中的 Fetcher 类型。 显式 WithFetcher 注入时不会触发此回调。

func WithBaseline

func WithBaseline(baseline map[string]Device) Option

WithBaseline 以 baseline 为已知设备基线初始化 Watcher。Watcher 在启动时会跳过 对这些 MAC 的"新上线"识别 (不触发 EventOnline), 直接视为历史已知。

典型场景: 进程重启 / 热重载时, 把旧 Watcher 的 Known() 快照传给新 Watcher, 避免重启瞬间所有设备被识别为"新上线"导致业务事件风暴。

注意: baseline 是浅拷贝; 传入后请勿再并发修改这份 map。 不会触发 EnsureFetcher, 传入的设备字段由调用方保证正确。

Example

ExampleWithBaseline demonstrates process hot-reload: carry the known-set from the old Watcher into the new one so no spurious "new online" events fire on restart.

package main

import (
	"context"
	"fmt"

	argus "github.com/xxl6097/argusd"
)

func main() {
	ctx := context.Background()

	// Old Watcher somewhere in your code
	old := argus.New()
	_, _ = old.List(ctx)

	// Capture the state snapshot (thread-safe deep copy)
	snapshot := old.Known()

	// Build a new Watcher seeded with that state; startup will not re-emit
	// these MACs as EventOnline.
	fresh := argus.New(argus.WithBaseline(snapshot))
	fmt.Printf("Seeded with %d known devices\n", len(fresh.Known()))
}

func WithConfig

func WithConfig(c Config) Option

WithConfig 设置自定义轮询配置。零值字段会保留默认值 (Config 字段的零值 0 通常不合法, 安全地作为"使用默认"的哨兵)。如需显式关闭冷却期或抖动抑制, 请使用 Config.DisableCooldown / DisableFlapSuppression 而非 OfflineCooldown=0。

func WithDecisionHandler

func WithDecisionHandler(cb DecisionHandler) Option

WithDecisionHandler 注册决策观测回调, 用于记录 Watcher 内部判定链路 (上线/离线/ 冷却期/抖动抑制等决策点)。适合日志 / 调参 / 排障, 业务侧通常用 EventHandler 即可。 回调内不应阻塞, 决策产生频率较高。传 nil (或不调用本 Option) 完全不收集决策。

Example

ExampleWithDecisionHandler enables the decision trace to inspect why a particular online/offline was (or wasn't) emitted.

DecisionHandler is zero-cost when not registered.

package main

import (
	"context"
	"log"
	"os/signal"
	"syscall"

	argus "github.com/xxl6097/argusd"
)

func main() {
	w := argus.New(
		argus.WithDecisionHandler(func(d argus.Decision) {
			log.Printf("[decision] %-24s %s %s", d.Kind, d.MAC, d.Detail)
		}),
	)

	ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT)
	defer stop()

	_ = w.Run(ctx, func(e argus.Event) {
		log.Printf("[event] %s %s", e.Kind, e.Device.MAC)
	}, nil)
}

func WithFetcher

func WithFetcher(f Fetcher) Option

WithFetcher 注入自定义 Fetcher (测试 / 接其他数据源)。

func WithHintSource

func WithHintSource(h HintSource) Option

WithHintSource 注入自定义的 HintSource 实现。用于:

  • 非 OpenWrt 平台 (Docker 容器 / 自研固件) 覆盖默认路径
  • 单元测试中注入固定 hints 避免依赖真实 /tmp/dhcp.leases
  • 从其他来源获取 hostname / IP (例如 DNS / DHCP server API)

传 nil 回退到包级默认 (DefaultHintSource 读 /tmp/dhcp.leases + ip neigh show)。 导出于 v0.7.0。

func WithLogger

func WithLogger(h LoggerHandler) Option

WithLogger registers a structured logger. May be called multiple times — only the last registration takes effect.

When unregistered (the default), library log call sites bail on a nil check without invoking the handler.

Example

ExampleWithLogger shows how to bridge Argus's structured log events to a third-party logger (here log/slog stand-in via fmt). The hot decision path does not log; this hook only fires for lifecycle events (watcher starting/stopping, fetcher detection) and recoverable anomalies (syslog buffer overflow, fetch failures).

package main

import (
	"context"
	"fmt"

	argus "github.com/xxl6097/argusd"
)

func main() {
	w := argus.New(
		argus.WithLogger(func(_ context.Context, level argus.LogLevel, msg string, attrs ...argus.LogAttr) {
			// Adapt to slog: slog.LogAttrs(ctx, slog.Level(level), msg, ...)
			fmt.Printf("[%s] %s", level, msg)
			for _, a := range attrs {
				fmt.Printf(" %s=%v", a.Key, a.Value)
			}
			fmt.Println()
		}),
	)
	_ = w
}

func WithProber

func WithProber(p Prober) Option

WithProber 注入活性探测器, 用于识别 AP 关联表残留 (例如手机走出信号范围 但 AP 还未将其踢出关联表的场景)。传 nil 可关闭活性探测。 默认使用 ICMPProber{Timeout: 1s}。

func WithSpanRecorder

func WithSpanRecorder(r SpanRecorder) Option

WithSpanRecorder registers a tracing hook. May be called multiple times; only the last registration takes effect.

When unregistered (the default), all library [Watcher.startSpan] call sites bail on a single nil check; no context.Context wrapping, no closure allocation, no observable cost on the hot path.

type Prober

type Prober interface {
	Reachable(ctx context.Context, ip string) bool
}

Prober 抽象设备活性探测; 用于识别"AP 关联表里仍然挂着, 但实际已经走出无线范围 或离开 LAN"的场景。Reachable 应在内部自行处理超时, 返回 true 表示可达。

type SpanRecorder

type SpanRecorder interface {
	// Start opens a new span. Returns a context.Context that should be
	// used by the spanned operation, and a finish function that the
	// caller must invoke exactly once. The finish function takes the
	// final error (or nil on success).
	Start(ctx context.Context, name string) (context.Context, func(err error))
}

SpanRecorder is an optional tracing hook the library uses to emit distributed-tracing spans for the multi-stage decision pipeline (syslog hint → ping check → emit Online/Offline).

It is the tracing analogue of LoggerHandler: a tiny interface the library calls into, with zero third-party dependency. Adapters to OpenTelemetry / OpenTracing / Datadog APM are typically ~15 lines.

Start opens a span scoped to the returned context. The returned finish function MUST be called exactly once with the operation's final error (or nil on success). Span lifecycle is owned by the caller; the library never panics if Start is called from a hot path with a non-nil recorder, but in practice it is only called from the lifecycle paths that already log (see WithLogger).

A nil SpanRecorder is the default: every Start call site short- circuits via a single nil check, no allocation.

Example adapter for OpenTelemetry:

import "go.opentelemetry.io/otel"
tracer := otel.Tracer("argus")
argus.WithSpanRecorder(argus.SpanRecorderFunc(
    func(ctx context.Context, name string) (context.Context, func(error)) {
        ctx, span := tracer.Start(ctx, name)
        return ctx, func(err error) {
            if err != nil {
                span.RecordError(err)
            }
            span.End()
        }
    },
))

type SpanRecorderFunc

type SpanRecorderFunc func(ctx context.Context, name string) (context.Context, func(err error))

SpanRecorderFunc adapts a plain function to the SpanRecorder interface, mirroring net/http.HandlerFunc style. Useful for inline declarations in WithSpanRecorder.

func (SpanRecorderFunc) Start

func (f SpanRecorderFunc) Start(ctx context.Context, name string) (context.Context, func(err error))

Start implements SpanRecorder.

type SyslogEvent

type SyslogEvent struct {
	Time  time.Time
	Kind  SyslogKind
	MAC   string // 小写冒号格式
	IP    string // DHCP 事件时有值
	Iface string // 无线接口名 (如 rax0)
	Raw   string // 原始日志行
}

SyslogEvent 是从 OpenWrt 系统日志中解析出的设备事件。

type SyslogHandler

type SyslogHandler func(SyslogEvent)

SyslogHandler 接收系统日志事件回调。

type SyslogKind

type SyslogKind int

SyslogKind 标识系统日志事件类型。

const (
	// SyslogWifiConnect: 无线设备完成关联 + 4-way 握手 (内核 wifi_sys_conn_act)。
	SyslogWifiConnect SyslogKind = iota + 1
	// SyslogWifiDisconnect: 无线设备断开关联 (内核 wifi_sys_disconn_act)。
	SyslogWifiDisconnect
	// SyslogDeauth: AP 收到或发出 Deauth 帧 (ap_peer_deauth_action)。
	SyslogDeauth
	// SyslogMacTableDelete: AP 从 MAC 表中删除设备 (MacTableDeleteEntry)。
	// 走出信号范围时 AP 不一定产生 disconnect/deauth, 但一定会 Del Sta。
	SyslogMacTableDelete
	// SyslogWPAComplete: WPA 握手完成 (AP SETKEYS DONE)。
	SyslogWPAComplete
	// SyslogMacTableInsert: AP 将新设备插入 MAC 表 (MacTableInsertEntry New Sta)。
	SyslogMacTableInsert
	// SyslogDHCPAck: DHCP 分配确认 (dnsmasq-dhcp DHCPACK)。
	SyslogDHCPAck
)

func (SyslogKind) IsConnect

func (k SyslogKind) IsConnect() bool

IsConnect 报告事件是否表示设备接入 (WPA 握手完成、MAC 表新增或 DHCP 分配)。

func (SyslogKind) IsDisconnect

func (k SyslogKind) IsDisconnect() bool

IsDisconnect 报告事件是否表示设备断开。 MacTableDeleteEntry 是设备离开信号范围后 AP 必定产生的最终事件, 即使 disconnect_act / deauth 被跳过。

func (SyslogKind) Label

func (k SyslogKind) Label() string

Label 返回中文文案。

func (SyslogKind) String

func (k SyslogKind) String() string

String 返回事件类型的稳定英文标识。

type Watcher

type Watcher struct {
	// contains filtered or unexported fields
}

Watcher 是库的主入口, 管理一份"已知设备"的状态, 通过周期拉取识别变化。

w := argus.New()
devices, _ := w.List(ctx)               // 一次性拉取
w.Run(ctx, func(e Event) {...}, nil)    // 实时监听 (阻塞)

未通过 WithFetcher 显式指定时, 首次 List/Run 会用 DetectFetcher 在 ubus 上 自动识别 ahsapd / hostapd 并选择对应实现; 探测结果会被缓存。

Run 内部同时启动 logread -f 监听系统日志: 当收到 wifi_sys_disconn_act / deauth / Del Sta 等事件时, 立即触发对该设备的活性探测并可即时判定离线。

并发模型: 轮询 diff、日志驱动 handleConnectHint 和 handleDisconnectHint 可能 在不同 goroutine 中被调度, 统一通过 stateMu 保护 known / misses / cooldown。

func New

func New(opts ...Option) *Watcher

New 创建一个 Watcher。Option 用于覆盖默认配置或注入自定义组件。

Example

ExampleNew demonstrates the minimal setup: build a Watcher with defaults, then run it until SIGINT/SIGTERM is received.

On OpenWrt, the library auto-detects ahsapd (vendor) or hostapd.* (stock); no options are required for the common case.

package main

import (
	"context"
	"log"
	"os/signal"
	"syscall"

	argus "github.com/xxl6097/argusd"
)

func main() {
	ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
	defer stop()

	w := argus.New()

	err := w.Run(ctx, func(e argus.Event) {
		switch e.Kind {
		case argus.EventOnline:
			log.Printf("[+] %s joined %s", e.Device.MAC, e.Device.IP)
		case argus.EventOffline:
			log.Printf("[-] %s left", e.Device.MAC)
		case argus.EventChange:
			log.Printf("[~] %s changed: %+v", e.Device.MAC, e.Changes)
		}
	}, nil)
	if err != nil {
		log.Fatal(err)
	}
}

func (*Watcher) EnsureFetcher

func (w *Watcher) EnsureFetcher(ctx context.Context) error

EnsureFetcher 在 fetcher 未显式指定时, 触发一次 ubus 探测。多次调用安全。 通常无需手动调用 - List / Run 内部会自动触发。

func (*Watcher) FetcherKind

func (w *Watcher) FetcherKind() FetcherKind

FetcherKind 返回当前 Watcher 使用的 Fetcher 类型。 仅在首次 List / Run 调用 (或显式调用 EnsureFetcher) 之后才有意义。 用户显式 WithFetcher 注入时返回空串。

func (*Watcher) Known

func (w *Watcher) Known() map[string]Device

Known 返回当前库认为"在线"的设备集的深拷贝快照, 按小写 MAC 索引。 并发安全, 可随时调用。

典型用途: 进程热重载时, 把快照传给新 Watcher 的 WithBaseline 以避免 重启瞬间所有设备被识别为"新上线"。

func (*Watcher) List

func (w *Watcher) List(ctx context.Context) ([]Device, error)

List 立即拉取一次当前接入设备列表, 不会启动后台监听。 启用了 Prober 时, 不可达的"假在线"设备会被过滤掉; 如果 Run 正在运行且设备处于 离线冷却期 (刚判离线且 RSSI 仍弱), 该设备也会被过滤, 与 Run 的"在线"定义保持一致。 返回切片按 MAC 排序。

Example

ExampleWatcher_List shows a one-shot snapshot of the currently-associated devices without starting background monitoring.

package main

import (
	"context"
	"fmt"
	"log"

	argus "github.com/xxl6097/argusd"
)

func main() {
	w := argus.New()
	ctx := context.Background()

	devices, err := w.List(ctx)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(argus.RenderTable(devices))
}

func (*Watcher) Run

func (w *Watcher) Run(ctx context.Context, onEvent EventHandler, onError ErrorHandler) error

Run 启动后台监听并阻塞直到 ctx 被取消:

  • 启动时先做一次基线拉取, 这次不会触发 onEvent;
  • 之后每个轮询周期通过 onEvent 投递设备上线 / 离线 / 状态变更事件;
  • 系统日志 hint 在独立 goroutine 中异步处理, 不阻塞主循环;
  • 单次拉取失败通过 onError 上报但不会终止循环 (onError 可为 nil)。

仅在初始基线拉取失败时返回 error, ctx 取消时返回 nil。

并发与生命周期:

  • 同一 Watcher 在任意时刻只能有一个 Run 活跃; 并发调用 Run 的后来者 会立即返回 ErrAlreadyRunning。先调用 Stop 或等当前 Run 返回后, 可以 再次 Run。
  • Run 返回时保留 known / offlineCooldown / lastEventAt 和探测缓存的 Fetcher / detectKind, 支持重启复用。
  • Run 入口会重置瞬态状态 (misses / disconnectInFlight / droppedHints / syslogHints channel), 避免上一轮遗留影响新一轮判定。

错误可通过 errors.Is 判别: ErrHandlerRequired / ErrInvalidConfig / ErrNoFetcher / ErrFetchFailed / ErrAlreadyRunning。

func (*Watcher) Stop

func (w *Watcher) Stop(stopCtx context.Context) error

Stop 优雅停止当前 Run, 等待所有 in-flight goroutine (syslog / consumer / hint worker) 退出或 stopCtx 超时。

约定:

  • 幂等: 没有 Run 在运行时立即返回 nil。
  • 可从任意 goroutine 调用, 包括与 Run 并发。
  • stopCtx 超时返回 context.DeadlineExceeded; 此时内部 ctx 已取消, 未退出的 goroutine 会自行清理, 但 Run 本身可能尚未返回。
  • Stop 后可以再次 Run; 瞬态状态 (misses / disconnectInFlight / syslogHints / droppedHints) 会在 Run 入口重置, timeless 状态 (known / offlineCooldown / lastEventAt) 保留。

典型用法 (SIGHUP 热重载配置):

sighup := make(chan os.Signal, 1)
signal.Notify(sighup, syscall.SIGHUP)
for {
    go w.Run(ctx, onEvent, onError)
    <-sighup
    stopCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    w.Stop(stopCtx)
    cancel()
    // 用新配置继续下一轮
}
Example

ExampleWatcher_Stop shows the SIGHUP hot-reload pattern: the Watcher survives config changes without re-emitting Online for every known device.

State preservation across Stop → Run:

  • preserved: known, offlineCooldown, lastEventAt, detected Fetcher
  • reset: misses, disconnectInFlight, syslogHints channel, droppedHints
package main

import (
	"context"
	"log"
	"os"
	"os/signal"
	"syscall"
	"time"

	argus "github.com/xxl6097/argusd"
)

func main() {
	w := argus.New()

	sighup := make(chan os.Signal, 1)
	signal.Notify(sighup, syscall.SIGHUP)
	sigterm := make(chan os.Signal, 1)
	signal.Notify(sigterm, syscall.SIGTERM, syscall.SIGINT)

	for {
		ctx, cancel := context.WithCancel(context.Background())
		runErr := make(chan error, 1)
		go func() { runErr <- w.Run(ctx, onEvent, onError) }()

		select {
		case <-sighup:
			// Hot-reload config
			stopCtx, stopCancel := context.WithTimeout(context.Background(), 5*time.Second)
			if err := w.Stop(stopCtx); err != nil {
				log.Printf("stop timed out: %v", err)
			}
			stopCancel()
			cancel()
			<-runErr
			log.Println("reloaded config; restarting watcher")
			// loop continues, fresh Run with new config

		case <-sigterm:
			cancel()
			<-runErr
			return
		}
	}
}

// Example callback helpers for ExampleWatcher_Stop.
func onEvent(e argus.Event) {
	log.Printf("[event] %s %s", e.Kind, e.Device.MAC)
}
func onError(err error) {
	log.Printf("[error] %v", err)
}

Directories

Path Synopsis
Package argusmetrics provides in-process metric counters for Argus decision traces, without pulling in any third-party metrics library.
Package argusmetrics provides in-process metric counters for Argus decision traces, without pulling in any third-party metrics library.
Package argustest 提供 argus 库使用者编写单元测试时可复用的测试替身。
Package argustest 提供 argus 库使用者编写单元测试时可复用的测试替身。
Package argusweb exposes a built-in HTTP + Server-Sent Events (SSE) dashboard on top of an argus.Watcher.
Package argusweb exposes a built-in HTTP + Server-Sent Events (SSE) dashboard on top of an argus.Watcher.
cmd
argusd command
Command argusd is the reference consumer of the argus library.
Command argusd is the reference consumer of the argus library.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL