2 Commits
v1.1.0 ... main

Author SHA1 Message Date
661fec975d Refactoring stats gathering
Some checks failed
Run tests / test (1.25) (push) Failing after 1m0s
Run tests / test (stable) (push) Failing after 1m0s
2026-03-17 09:51:16 +01:00
03047907f1 Added StatsReceiver interface
Some checks failed
Run tests / test (1.25) (push) Failing after 1m0s
Run tests / test (stable) (push) Failing after 1m0s
2026-03-17 08:35:33 +01:00
2 changed files with 135 additions and 65 deletions

View File

@@ -25,6 +25,7 @@ type repeaterDriver struct {
lastFrameAt time.Time
info repeaterInfo
stats chan map[string]any
rawStats map[string]any
err error
}
@@ -104,6 +105,7 @@ func newRepeaterDriver(conn io.ReadWriteCloser) *repeaterDriver {
conn: conn,
waiting: make(chan *repeaterDriverWaiting, 16),
stats: make(chan map[string]any, 2),
rawStats: make(map[string]any),
}
}
@@ -122,7 +124,6 @@ func (drv *repeaterDriver) Setup() (err error) {
if err = drv.queryDeviceInfo(); err != nil {
return err
}
go drv.pollStats()
return drv.err
}
@@ -191,6 +192,14 @@ func (drv *repeaterDriver) Stats() <-chan map[string]any {
}
func (drv *repeaterDriver) queryDeviceInfo() (err error) {
// Start with sending blank lines:
for i := 0; i < 3; i++ {
if _, err = fmt.Fprintf(drv.conn, "\r\n"); err != nil {
return
}
time.Sleep(time.Millisecond * 100)
}
if drv.info.FirmwareVersion, err = drv.writeCommand("ver"); err != nil {
return
}
@@ -253,7 +262,10 @@ func (drv *repeaterDriver) queryDeviceInfo() (err error) {
return
}
return err
// Now that we're online, start the stats collector;
go drv.pollStats()
return nil
}
func (drv *repeaterDriver) writeCommand(command string, args ...string) (response string, err error) {
@@ -400,12 +412,19 @@ func (drv *repeaterDriver) poll() {
Logger.Tracef("meshcore: handle %q", line)
switch {
case strings.HasPrefix(line, "-> "):
line = strings.TrimPrefix(line[3:], "> ")
select {
case waiting := <-drv.waiting:
waiting.Respond(strings.TrimPrefix(line[3:], "> "))
Logger.Tracef("meshcore: send response to waiting: %q", line)
waiting.Respond(line)
default:
if strings.HasPrefix(line, "{") {
Logger.Tracef("meshcore: handle stats update %q", line)
drv.processStats(line)
} else {
Logger.Warnf("meshcore: unhandled response %q", line[3:])
}
}
case strings.Contains(line, " RAW: "):
// "10:53:04 - 15/5/2024 U RAW: 0917081DE1B03B130A270E01233EA0FB261218CCABBAA02278F51E97585B9B3285B95EFEEC83BE91E3D1E4F79D88B2C9484AA6882EB217C992B5C3C99C"
@@ -417,6 +436,9 @@ func (drv *repeaterDriver) poll() {
case strings.Contains(line, ": TX,"):
// ignore (for now)
case strings.HasPrefix(line, "stats-"):
// ignore echo of the stats commands we send
default:
Logger.Tracef("meshcore: repeater sent gibberish %q", line)
}
@@ -428,6 +450,7 @@ func (drv *repeaterDriver) pollStats() {
defer ticker.Stop()
for {
/*
stats := make(map[string]any)
neighbors, err := drv.getNeighbors()
@@ -500,11 +523,53 @@ func (drv *repeaterDriver) pollStats() {
default:
Logger.Warn("meshcore: stats channel full, dropping stats")
}
*/
// Request the stats, we let the main poll loop handle the response, as we don't want to have multiple concurrent requests.
if _, err := drv.writeCommand("stats-core"); err != nil {
Logger.Warnf("meshcore: failed to request stats: %v", err)
}
time.Sleep(time.Millisecond * 100) // small delay to avoid overwhelming the device
if _, err := drv.writeCommand("stats-radio"); err != nil {
Logger.Warnf("meshcore: failed to request stats: %v", err)
}
time.Sleep(time.Millisecond * 100) // small delay to avoid overwhelming the device
if _, err := drv.writeCommand("stats-packets"); err != nil {
Logger.Warnf("meshcore: failed to request stats: %v", err)
}
time.Sleep(time.Millisecond * 100) // small delay to avoid overwhelming the device
<-ticker.C
}
}
func (drv *repeaterDriver) processStats(line string) {
var stats map[string]any
if err := json.Unmarshal([]byte(line), &stats); err != nil {
Logger.Warnf("meshcore: failed to decode stats: %v", err)
return
}
// TRAC[2026-03-17T09:42:10+01:00] meshcore: handle "-> {\"battery_mv\":4454,\"uptime_secs\":3751,\"errors\":0,\"queue_len\":0}"
// TRAC[2026-03-17T09:42:10+01:00] meshcore: handle "-> {\"noise_floor\":-115,\"last_rssi\":-78,\"last_snr\":11.50,\"tx_air_secs\":0,\"rx_air_secs\":147}"
// TRAC[2026-03-17T09:42:10+01:00] meshcore: handle "-> {\"recv\":404,\"sent\":1,\"flood_tx\":0,\"direct_tx\":1,\"flood_rx\":403,\"direct_rx\":1,\"recv_errors\":0}"
if _, ok := stats["battery_mv"]; ok {
drv.rawStats["core"] = stats
} else if _, ok := stats["noise_floor"]; ok {
drv.rawStats["radio"] = stats
} else if _, ok := stats["recv"]; ok {
drv.rawStats["packets"] = stats
select {
case drv.stats <- drv.rawStats:
Logger.Trace("meshcore: published stats update")
default:
Logger.Warn("meshcore: stats channel full, dropping stats")
}
} else {
Logger.Warnf("meshcore: unknown stats update: %v", stats)
}
}
func (drv *repeaterDriver) getNeighbors() (neighbors map[string]float64, err error) {
// "neighbors" command returns a list of neighbors, one per line, in the format: "78CCC5A3:470:47\n"
var response string

View File

@@ -35,6 +35,11 @@ type PacketReceiver interface {
RawPackets() <-chan *Packet
}
type StatsReceiver interface {
// Stats returns a channel that receives stats updates.
Stats() <-chan map[string]any
}
type PacketTransmitter interface {
radio.Device