2026-07-25 17:24:29 -07:00
|
|
|
package main
|
|
|
|
|
|
2026-07-25 17:58:54 -07:00
|
|
|
import (
|
|
|
|
|
"flag"
|
|
|
|
|
"fmt"
|
|
|
|
|
"net"
|
|
|
|
|
"os"
|
|
|
|
|
"os/signal"
|
|
|
|
|
"sync"
|
|
|
|
|
"sync/atomic"
|
|
|
|
|
"syscall"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"golang.org/x/sys/unix"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type endpoint struct {
|
2026-08-04 21:58:52 -07:00
|
|
|
name string
|
|
|
|
|
tag string
|
|
|
|
|
idx int
|
|
|
|
|
mac [6]byte
|
|
|
|
|
mtu int
|
2026-07-25 18:13:58 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (e endpoint) macString() string {
|
|
|
|
|
return fmt.Sprintf("%02x:%02x:%02x:%02x:%02x:%02x",
|
|
|
|
|
e.mac[0], e.mac[1], e.mac[2], e.mac[3], e.mac[4], e.mac[5])
|
2026-07-25 17:58:54 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func lookupEndpoint(name string) (endpoint, error) {
|
|
|
|
|
ifi, err := net.InterfaceByName(name)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return endpoint{}, err
|
|
|
|
|
}
|
|
|
|
|
if len(ifi.HardwareAddr) != 6 {
|
|
|
|
|
return endpoint{}, fmt.Errorf("%s: expected 6-byte MAC, got %q", name, ifi.HardwareAddr)
|
|
|
|
|
}
|
|
|
|
|
var mac [6]byte
|
|
|
|
|
copy(mac[:], ifi.HardwareAddr)
|
2026-08-04 21:58:52 -07:00
|
|
|
return endpoint{name: name, idx: ifi.Index, mac: mac, mtu: ifi.MTU}, nil
|
2026-07-25 17:58:54 -07:00
|
|
|
}
|
|
|
|
|
|
2026-07-25 21:41:21 -07:00
|
|
|
// Returns the new start time, so the uptime shown alongside the totals counts
|
2026-08-17 11:25:31 -07:00
|
|
|
// from the reset rather than from launch; the elapsed clock restarting is the
|
|
|
|
|
// visible mark of the re-baseline.
|
|
|
|
|
func resetAll(dirs []*direction, mods []*phyModule) time.Time {
|
2026-07-25 21:41:21 -07:00
|
|
|
for _, d := range dirs {
|
|
|
|
|
d.reset()
|
|
|
|
|
}
|
2026-08-13 07:10:39 -07:00
|
|
|
for _, m := range mods {
|
|
|
|
|
m.reset()
|
|
|
|
|
}
|
2026-07-25 21:41:21 -07:00
|
|
|
return time.Now()
|
2026-07-25 19:29:00 -07:00
|
|
|
}
|
|
|
|
|
|
2026-08-17 18:56:55 -07:00
|
|
|
// Nothing to hold or revert here: failures during a measure were never
|
|
|
|
|
// counted, so the view is always the counters as they stand.
|
|
|
|
|
func phyView(diag *cableDiag, modules []*phyModule) (phyDisplay, bool) {
|
|
|
|
|
info, measuring := diag.snapshot()
|
|
|
|
|
return phyDisplayFrom(info, measuring, modules[0].view(), modules[1].view()), measuring
|
2026-07-25 17:58:54 -07:00
|
|
|
}
|
|
|
|
|
|
2026-07-25 18:13:58 -07:00
|
|
|
var intervalCols = []colSpec{
|
2026-08-04 14:02:53 -07:00
|
|
|
{group: "NOW", title: "bits/s", width: 9, right: true},
|
|
|
|
|
{group: "NOW", title: "packets/s", width: 9, right: true},
|
2026-08-13 07:10:39 -07:00
|
|
|
{group: "NOW", title: "snr", width: 6, right: true},
|
2026-08-04 14:02:53 -07:00
|
|
|
{group: "NOW", title: "lost", width: 7, right: true},
|
|
|
|
|
{group: "NOW", title: "corrupt", width: 7, right: true},
|
|
|
|
|
{group: "NOW", title: "link", width: 7, right: true},
|
|
|
|
|
{group: "NOW", title: "internal", width: 8, right: true},
|
2026-08-13 07:10:39 -07:00
|
|
|
{group: "NOW", title: "corrected", width: 9, right: true},
|
2026-08-05 10:49:23 -07:00
|
|
|
{group: "NOW", title: "noise", width: 7, right: true},
|
2026-08-04 14:02:53 -07:00
|
|
|
{group: "OVERALL", title: "elapsed", width: 9, right: true},
|
|
|
|
|
{group: "OVERALL", title: "packets", width: 9, right: true},
|
|
|
|
|
{group: "OVERALL", title: "bytes", width: 9, right: true},
|
2026-08-17 11:25:31 -07:00
|
|
|
{group: "OVERALL", title: "cable", width: 6, right: true},
|
2026-08-13 07:10:39 -07:00
|
|
|
{group: "OVERALL", title: "corrected", width: 9, right: true},
|
2026-08-04 14:02:53 -07:00
|
|
|
{group: "OVERALL", title: "lost", width: 9, right: true},
|
|
|
|
|
{group: "OVERALL", title: "corrupt", width: 9, right: true},
|
|
|
|
|
{group: "OVERALL", title: "link", width: 9, right: true},
|
|
|
|
|
{group: "OVERALL", title: "internal", width: 9, right: true},
|
2026-07-25 18:13:58 -07:00
|
|
|
}
|
|
|
|
|
|
2026-08-05 10:49:23 -07:00
|
|
|
// The same figures the panel draws, in the same order: the last second as
|
|
|
|
|
// rates and error flags with the noise cable riding at the end of them, then
|
|
|
|
|
// everything since the reset.
|
2026-08-13 14:04:16 -07:00
|
|
|
func totalRow(elapsed time.Duration, v view, target float64, phy phyDisplay, nv noiseView) []string {
|
2026-07-25 18:13:58 -07:00
|
|
|
return []string{
|
2026-08-01 16:07:44 -07:00
|
|
|
rateCell(v.rxGbps*1e9, target*1e9),
|
2026-08-04 14:02:53 -07:00
|
|
|
scaleSI(v.rxPPS),
|
2026-08-13 07:10:39 -07:00
|
|
|
snrCell(phy),
|
2026-08-04 14:02:53 -07:00
|
|
|
flagCell(v.window.lost),
|
|
|
|
|
flagCell(v.window.corrupt),
|
|
|
|
|
flagCell(v.window.link),
|
|
|
|
|
flagCell(v.window.internal),
|
2026-08-13 07:10:39 -07:00
|
|
|
correctedFlag(phy.recent),
|
2026-08-13 14:04:16 -07:00
|
|
|
noiseCell(nv),
|
2026-08-04 14:02:53 -07:00
|
|
|
scaleTime(elapsed),
|
|
|
|
|
scaleCount(v.rxFrames),
|
|
|
|
|
scaleCount(v.rxBytes),
|
2026-08-13 07:10:39 -07:00
|
|
|
phy.metres,
|
|
|
|
|
correctedCell(phy.corrected),
|
2026-07-26 10:27:41 -07:00
|
|
|
statusCell(v.since.lost),
|
2026-07-31 17:10:25 -07:00
|
|
|
statusCell(v.since.corrupt),
|
2026-07-26 10:27:41 -07:00
|
|
|
statusCell(v.since.link),
|
2026-07-31 17:10:25 -07:00
|
|
|
statusCell(v.since.internal),
|
2026-07-25 18:13:58 -07:00
|
|
|
}
|
2026-07-25 17:58:54 -07:00
|
|
|
}
|
|
|
|
|
|
2026-08-04 13:31:14 -07:00
|
|
|
const (
|
|
|
|
|
numStreams = 7
|
|
|
|
|
batchSize = 64
|
|
|
|
|
|
X520 live as the product NIC: rate buckets re-keyed to read time (one shared clock, one commit per drained batch) with the backward smear as a settled window — each completed bucket enters once, donates excess to the nearest earlier deficits once, locks for display when nothing later can refill it (per-sample recompute double-counted excess and read >20G on a 20G wire); timestamp machinery deleted (ts.go, SO_TIMESTAMPING, rx_filter=ALL check) after the audit confirmed the buckets were its sole consumer; testDriver ixgbe; RollBall mailbox hardened against orphaned-command completions (never sample status before a poll gap, value from the same block read, devad/reg echo required — a killed session's 7.60 read answered a PHY ID as 0x0006); full hardware pass: 20.00G flat both directions, zero errors, ECD 42m, SNR +7.4dB
2026-08-17 09:35:58 -07:00
|
|
|
testDriver = "ixgbe"
|
2026-08-05 10:49:23 -07:00
|
|
|
|
2026-08-04 21:58:52 -07:00
|
|
|
// A constant rather than the negotiated speed, since this has to come up
|
|
|
|
|
// with no cable in the port and nothing to negotiate.
|
|
|
|
|
linkSpeed = 10.0
|
2026-08-04 13:31:14 -07:00
|
|
|
)
|
|
|
|
|
|
2026-08-04 22:12:07 -07:00
|
|
|
// The mac appends the fcs, so 60 and 1514 here are the smallest and largest
|
2026-08-13 08:04:26 -07:00
|
|
|
// standard frames, 64 and 1518 on the wire; 9014 fills the 9000 MTU.
|
|
|
|
|
var frameSizes = []int{60, 128, 256, 512, 1024, 1280, 1514, 9014}
|
2026-07-25 17:24:29 -07:00
|
|
|
|
|
|
|
|
func main() {
|
2026-08-05 10:49:23 -07:00
|
|
|
// Left empty, the test pair is found by driver name instead: as PID 1 there
|
|
|
|
|
// is no udev to pin names and no command line to pass, and which port gets
|
|
|
|
|
// which ethN shifts with every driver built into the kernel.
|
X520 live as the product NIC: rate buckets re-keyed to read time (one shared clock, one commit per drained batch) with the backward smear as a settled window — each completed bucket enters once, donates excess to the nearest earlier deficits once, locks for display when nothing later can refill it (per-sample recompute double-counted excess and read >20G on a 20G wire); timestamp machinery deleted (ts.go, SO_TIMESTAMPING, rx_filter=ALL check) after the audit confirmed the buckets were its sole consumer; testDriver ixgbe; RollBall mailbox hardened against orphaned-command completions (never sample status before a poll gap, value from the same block read, devad/reg echo required — a killed session's 7.60 read answered a PHY ID as 0x0006); full hardware pass: 20.00G flat both directions, zero errors, ECD 42m, SNR +7.4dB
2026-08-17 09:35:58 -07:00
|
|
|
aName := flag.String("a", "", "first interface (default: the ixgbe pair)")
|
2026-08-05 10:49:23 -07:00
|
|
|
bName := flag.String("b", "", "second interface")
|
2026-07-25 17:58:54 -07:00
|
|
|
flag.Parse()
|
|
|
|
|
|
2026-08-13 07:10:39 -07:00
|
|
|
if err := run(*aName, *bName); err != nil {
|
2026-08-05 21:46:15 -07:00
|
|
|
fatal(err)
|
2026-07-25 17:58:54 -07:00
|
|
|
}
|
2026-08-05 08:18:44 -07:00
|
|
|
// A clean return is ctrl-alt-delete, which the kernel hands PID 1 as a
|
|
|
|
|
// SIGINT. Exiting on it would panic the kernel over the reboot it was asking
|
|
|
|
|
// for, so init asks for the reboot by name.
|
|
|
|
|
if os.Getpid() == 1 {
|
|
|
|
|
if err := unix.Reboot(unix.LINUX_REBOOT_CMD_RESTART); err != nil {
|
|
|
|
|
panic(err)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-07-25 17:58:54 -07:00
|
|
|
}
|
|
|
|
|
|
2026-07-25 21:30:37 -07:00
|
|
|
const (
|
|
|
|
|
reportInterval = time.Second
|
2026-08-01 16:14:12 -07:00
|
|
|
// Deliberately not tied to the refresh: letting a slow or blocked draw set
|
|
|
|
|
// the sampling clock would stretch the window it reports.
|
2026-07-31 16:15:16 -07:00
|
|
|
sampleInterval = 16 * time.Millisecond
|
2026-08-04 21:29:03 -07:00
|
|
|
// How far back the shown errors reach. The rate is not taken from this ring
|
X520 live as the product NIC: rate buckets re-keyed to read time (one shared clock, one commit per drained batch) with the backward smear as a settled window — each completed bucket enters once, donates excess to the nearest earlier deficits once, locks for display when nothing later can refill it (per-sample recompute double-counted excess and read >20G on a 20G wire); timestamp machinery deleted (ts.go, SO_TIMESTAMPING, rx_filter=ALL check) after the audit confirmed the buckets were its sole consumer; testDriver ixgbe; RollBall mailbox hardened against orphaned-command completions (never sample status before a poll gap, value from the same block read, devad/reg echo required — a killed session's 7.60 read answered a PHY ID as 0x0006); full hardware pass: 20.00G flat both directions, zero errors, ECD 42m, SNR +7.4dB
2026-08-17 09:35:58 -07:00
|
|
|
// but from the smeared receive buckets, which are keyed by the read clock.
|
2026-07-31 15:45:03 -07:00
|
|
|
rateWindowSpan = time.Second
|
2026-07-25 21:30:37 -07:00
|
|
|
)
|
2026-07-25 18:20:34 -07:00
|
|
|
|
2026-08-04 13:45:16 -07:00
|
|
|
// One sampler for both directions, so they are read back to back on one clock
|
|
|
|
|
// rather than drifting apart on two.
|
2026-07-31 16:15:16 -07:00
|
|
|
type sampler struct {
|
|
|
|
|
dirs []*direction
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *sampler) run(done *atomic.Bool, startTx <-chan struct{}) {
|
|
|
|
|
<-startTx
|
|
|
|
|
|
|
|
|
|
tick := time.NewTicker(sampleInterval)
|
|
|
|
|
defer tick.Stop()
|
|
|
|
|
|
|
|
|
|
for !done.Load() {
|
2026-08-04 13:45:16 -07:00
|
|
|
<-tick.C
|
2026-07-31 16:15:16 -07:00
|
|
|
for _, d := range s.dirs {
|
2026-08-04 13:45:16 -07:00
|
|
|
d.sample()
|
2026-07-31 16:15:16 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-13 07:10:39 -07:00
|
|
|
func run(aName, bName string) (err error) {
|
2026-08-05 21:46:15 -07:00
|
|
|
defer func() {
|
|
|
|
|
if p := recover(); p != nil {
|
|
|
|
|
if os.Getpid() != 1 {
|
|
|
|
|
panic(p)
|
|
|
|
|
}
|
|
|
|
|
err = fmt.Errorf("%v", p)
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
2026-07-26 15:50:55 -07:00
|
|
|
if err := reportChecks("BOOT", bootstrap()); err != nil {
|
2026-07-26 10:54:19 -07:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-05 10:49:23 -07:00
|
|
|
if aName == "" || bName == "" {
|
|
|
|
|
var err error
|
|
|
|
|
aName, bName, err = driverPair(testDriver)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-07-25 17:58:54 -07:00
|
|
|
a, err := lookupEndpoint(aName)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
b, err := lookupEndpoint(bName)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2026-08-05 10:49:23 -07:00
|
|
|
noise, err := newNoiser()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
defer noise.close()
|
2026-07-25 17:58:54 -07:00
|
|
|
|
2026-08-05 10:49:23 -07:00
|
|
|
a.tag, b.tag = "TEST A", "TEST B"
|
|
|
|
|
noise.eps[0].tag, noise.eps[1].tag = "NOISE A", "NOISE B"
|
2026-07-25 18:07:25 -07:00
|
|
|
ifnames := []string{a.name, b.name}
|
2026-07-25 18:13:58 -07:00
|
|
|
|
2026-08-04 13:31:14 -07:00
|
|
|
ethertypes := make([]uint16, numStreams)
|
2026-07-25 19:08:11 -07:00
|
|
|
for i := range ethertypes {
|
|
|
|
|
ethertypes[i] = uint16(etherBase + i)
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-13 08:04:26 -07:00
|
|
|
modules, moduleIDs, err := openModules([2]string{a.name, b.name})
|
|
|
|
|
if err != nil {
|
2026-07-26 10:54:19 -07:00
|
|
|
return err
|
2026-07-25 18:07:25 -07:00
|
|
|
}
|
|
|
|
|
|
2026-08-13 08:04:26 -07:00
|
|
|
checks := configureSystem(ifnames, ethertypes)
|
|
|
|
|
checks = append(checks, moduleChecks(modules, [2]string{a.name, b.name})...)
|
|
|
|
|
checks = append(checks, configureNoise(noise.names())...)
|
|
|
|
|
if err := reportChecks("SETTINGS", checks); err != nil {
|
2026-08-13 07:10:39 -07:00
|
|
|
return err
|
|
|
|
|
}
|
2026-08-13 08:04:26 -07:00
|
|
|
|
|
|
|
|
// The MTU check may have just raised them, so both are re-read before the
|
|
|
|
|
// frame sizes are judged.
|
|
|
|
|
for _, e := range []*endpoint{&a, &b} {
|
|
|
|
|
fresh, err := lookupEndpoint(e.name)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
e.mtu = fresh.mtu
|
|
|
|
|
for _, s := range frameSizes {
|
|
|
|
|
if s > e.mtu+ethHdrLen {
|
|
|
|
|
return fmt.Errorf("size %d exceeds %s MTU %d (max frame %d)", s, e.name, e.mtu, e.mtu+ethHdrLen)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
diag := newCableDiag(modules, cableInfo{})
|
2026-08-13 07:10:39 -07:00
|
|
|
|
2026-07-25 17:58:54 -07:00
|
|
|
var dirs []*direction
|
2026-07-25 23:03:31 -07:00
|
|
|
for _, p := range [][2]endpoint{{a, b}, {b, a}} {
|
2026-08-17 11:25:31 -07:00
|
|
|
d, err := buildDirection(p[0].name+"->"+p[1].name, p[0], p[1], &diag.measuring)
|
2026-07-25 17:58:54 -07:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2026-07-25 23:03:31 -07:00
|
|
|
dirs = append(dirs, d)
|
2026-07-25 17:58:54 -07:00
|
|
|
}
|
|
|
|
|
defer func() {
|
|
|
|
|
for _, d := range dirs {
|
|
|
|
|
d.close()
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
2026-07-25 18:13:58 -07:00
|
|
|
var linkRows [][]string
|
2026-08-13 08:04:26 -07:00
|
|
|
for i, e := range []endpoint{a, b, noise.eps[0], noise.eps[1]} {
|
|
|
|
|
mod := ""
|
|
|
|
|
if i < len(moduleIDs) {
|
|
|
|
|
mod = moduleIDs[i]
|
|
|
|
|
}
|
2026-07-25 18:13:58 -07:00
|
|
|
linkRows = append(linkRows, []string{
|
2026-08-13 08:04:26 -07:00
|
|
|
paint(e.tag, cCyan), e.name, e.macString(), fmt.Sprintf("%d", e.mtu), mod,
|
2026-07-25 18:13:58 -07:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
fmt.Println(renderBox("LINKS",
|
2026-08-13 08:04:26 -07:00
|
|
|
[]string{"TAG", "INTERFACE", "MAC", "MTU", "MODULE"},
|
|
|
|
|
[]bool{false, false, false, true, false}, linkRows))
|
2026-08-04 21:58:52 -07:00
|
|
|
fmt.Println()
|
2026-07-25 18:13:58 -07:00
|
|
|
|
2026-08-04 14:02:53 -07:00
|
|
|
// One row carries both directions, so line rate is both links at once.
|
2026-08-04 21:58:52 -07:00
|
|
|
target := linkSpeed * float64(len(dirs))
|
2026-07-25 17:58:54 -07:00
|
|
|
|
2026-08-04 22:12:07 -07:00
|
|
|
var done atomic.Bool
|
2026-07-25 17:58:54 -07:00
|
|
|
var wg sync.WaitGroup
|
|
|
|
|
var rxReady sync.WaitGroup
|
|
|
|
|
startTx := make(chan struct{})
|
|
|
|
|
for _, d := range dirs {
|
2026-08-13 07:10:39 -07:00
|
|
|
rxReady.Add(len(d.rxFDs))
|
2026-07-25 17:58:54 -07:00
|
|
|
}
|
|
|
|
|
for _, d := range dirs {
|
2026-08-04 22:12:07 -07:00
|
|
|
d.start(&wg, &done, &rxReady, startTx)
|
2026-07-25 17:58:54 -07:00
|
|
|
}
|
2026-07-31 16:15:16 -07:00
|
|
|
samp := &sampler{dirs: dirs}
|
|
|
|
|
wg.Add(1)
|
|
|
|
|
go func() {
|
|
|
|
|
defer wg.Done()
|
2026-08-05 21:46:15 -07:00
|
|
|
defer holdPanic()
|
2026-08-04 22:12:07 -07:00
|
|
|
samp.run(&done, startTx)
|
2026-07-31 16:15:16 -07:00
|
|
|
}()
|
2026-08-05 10:49:23 -07:00
|
|
|
// Not gated on startTx: the cycle and the connected verdict are wanted the
|
|
|
|
|
// moment the panel is, and nothing it does touches the measurement.
|
|
|
|
|
wg.Add(1)
|
|
|
|
|
go func() {
|
|
|
|
|
defer wg.Done()
|
2026-08-05 21:46:15 -07:00
|
|
|
defer holdPanic()
|
2026-08-05 10:49:23 -07:00
|
|
|
noise.run(&done)
|
|
|
|
|
}()
|
2026-08-13 07:10:39 -07:00
|
|
|
for _, m := range modules {
|
|
|
|
|
wg.Add(1)
|
|
|
|
|
go func() {
|
|
|
|
|
defer wg.Done()
|
|
|
|
|
defer holdPanic()
|
|
|
|
|
m.run(&done)
|
|
|
|
|
}()
|
|
|
|
|
}
|
2026-08-04 22:26:07 -07:00
|
|
|
// Every return from here on stops the workers before the deferred closes
|
|
|
|
|
// pull their sockets out from under them: otherwise the sampler panics on a
|
|
|
|
|
// closed fd and can mask the error that actually ended the run. An error
|
|
|
|
|
// before the gate opens closes it here, or the wait would hang on
|
|
|
|
|
// goroutines still parked at startTx.
|
|
|
|
|
defer func() {
|
|
|
|
|
done.Store(true)
|
|
|
|
|
select {
|
|
|
|
|
case <-startTx:
|
|
|
|
|
default:
|
|
|
|
|
close(startTx)
|
|
|
|
|
}
|
|
|
|
|
wg.Wait()
|
|
|
|
|
}()
|
2026-08-05 21:46:15 -07:00
|
|
|
// A worker that panics before signalling ready would hang a bare Wait.
|
|
|
|
|
ready := make(chan struct{})
|
|
|
|
|
go func() {
|
|
|
|
|
rxReady.Wait()
|
|
|
|
|
close(ready)
|
|
|
|
|
}()
|
|
|
|
|
select {
|
|
|
|
|
case <-ready:
|
|
|
|
|
case p := <-fatalCh:
|
|
|
|
|
return fmt.Errorf("%v", p)
|
|
|
|
|
}
|
2026-07-25 17:58:54 -07:00
|
|
|
|
|
|
|
|
sig := make(chan os.Signal, 1)
|
|
|
|
|
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
|
|
|
|
|
|
2026-07-25 19:29:00 -07:00
|
|
|
space, restoreTerm := watchSpace()
|
|
|
|
|
defer restoreTerm()
|
|
|
|
|
|
2026-07-25 19:41:53 -07:00
|
|
|
disp, err := newDisplay()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("display: %w", err)
|
|
|
|
|
}
|
|
|
|
|
defer disp.close()
|
|
|
|
|
|
2026-07-26 10:27:41 -07:00
|
|
|
touch, err := watchTouch(disp.fb.pw, disp.fb.ph)
|
2026-07-25 21:41:21 -07:00
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("touchscreen: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-26 10:27:41 -07:00
|
|
|
for _, d := range dirs {
|
|
|
|
|
d.primeCounters()
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-25 17:58:54 -07:00
|
|
|
start := time.Now()
|
2026-08-17 12:22:55 -07:00
|
|
|
// Anchored to the same clock as the noise cycle, so slot boundaries and
|
|
|
|
|
// phase transitions coincide.
|
|
|
|
|
hist := newHistory(rateEpochStart)
|
2026-08-17 11:25:31 -07:00
|
|
|
// A reset resets at the press, then re-measures; the measure's failures
|
|
|
|
|
// are suppressed at their sources while it runs, so there is nothing to
|
|
|
|
|
// hide or revert afterwards.
|
|
|
|
|
kickMeasure := func() {
|
|
|
|
|
if diag.kick(&done) {
|
|
|
|
|
start = resetAll(dirs, modules)
|
2026-08-17 12:22:55 -07:00
|
|
|
hist.reset()
|
2026-08-17 11:25:31 -07:00
|
|
|
}
|
|
|
|
|
}
|
2026-07-25 17:58:54 -07:00
|
|
|
close(startTx)
|
2026-08-13 08:04:26 -07:00
|
|
|
// The first measure rides the same async path as a reset, so startup never
|
2026-08-17 11:25:31 -07:00
|
|
|
// waits on it.
|
|
|
|
|
kickMeasure()
|
2026-07-25 18:33:36 -07:00
|
|
|
tick := time.NewTicker(reportInterval)
|
2026-07-25 17:58:54 -07:00
|
|
|
defer tick.Stop()
|
|
|
|
|
|
2026-07-25 21:30:37 -07:00
|
|
|
views := make([]view, len(dirs))
|
2026-07-25 22:38:52 -07:00
|
|
|
rows := make([]view, len(dirs))
|
2026-07-25 18:13:58 -07:00
|
|
|
stats := &streamTable{cols: intervalCols, headerEvery: 20}
|
2026-07-25 17:58:54 -07:00
|
|
|
for {
|
|
|
|
|
select {
|
2026-08-05 21:46:15 -07:00
|
|
|
case p := <-fatalCh:
|
|
|
|
|
return fmt.Errorf("%v", p)
|
2026-07-25 17:58:54 -07:00
|
|
|
case <-sig:
|
2026-07-25 18:33:36 -07:00
|
|
|
return nil
|
2026-08-17 11:25:31 -07:00
|
|
|
// The verdict lives in the cable cell, not its own line.
|
2026-07-25 19:29:00 -07:00
|
|
|
case <-space:
|
2026-08-17 11:25:31 -07:00
|
|
|
kickMeasure()
|
2026-07-31 16:46:11 -07:00
|
|
|
case <-disp.fb.flips:
|
|
|
|
|
now := time.Now()
|
2026-07-26 10:27:41 -07:00
|
|
|
px, py, down := touch.get()
|
|
|
|
|
x, y := disp.fb.fromPanel(px, py)
|
|
|
|
|
if disp.holdReset(x, y, down, now) {
|
2026-08-17 11:25:31 -07:00
|
|
|
kickMeasure()
|
2026-07-25 21:41:21 -07:00
|
|
|
}
|
2026-08-05 16:13:20 -07:00
|
|
|
disp.showVersion = down && disp.versionSpot.contains(x, y)
|
2026-07-25 21:30:37 -07:00
|
|
|
for i, d := range dirs {
|
2026-08-04 13:49:21 -07:00
|
|
|
views[i] = d.displayView()
|
2026-07-25 21:30:37 -07:00
|
|
|
}
|
2026-08-17 12:22:55 -07:00
|
|
|
v := totalView(views)
|
|
|
|
|
phy, measuring := phyView(diag, modules)
|
|
|
|
|
if err := disp.render(v, now.Sub(start), phy, measuring,
|
|
|
|
|
noise.view(), hist.view(), target); err != nil {
|
2026-07-31 16:46:11 -07:00
|
|
|
return err
|
|
|
|
|
}
|
2026-07-25 17:58:54 -07:00
|
|
|
case now := <-tick.C:
|
2026-07-25 18:33:36 -07:00
|
|
|
elapsed := now.Sub(start)
|
2026-07-25 22:38:52 -07:00
|
|
|
for i, d := range dirs {
|
2026-08-04 14:02:53 -07:00
|
|
|
rows[i] = d.displayView()
|
2026-07-25 22:38:52 -07:00
|
|
|
}
|
2026-08-17 12:22:55 -07:00
|
|
|
v := totalView(rows)
|
|
|
|
|
phy, measuring := phyView(diag, modules)
|
|
|
|
|
hist.sample(now, v, phy, noise.view(), measuring, target)
|
2026-08-13 16:32:14 -07:00
|
|
|
for _, m := range modules {
|
|
|
|
|
for _, n := range m.takeNotes() {
|
|
|
|
|
fmt.Println(stats.rule(n))
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-13 10:49:01 -07:00
|
|
|
for _, line := range stats.emit(totalRow(elapsed, v, target, phy,
|
2026-08-13 14:04:16 -07:00
|
|
|
noise.view())) {
|
2026-08-04 14:02:53 -07:00
|
|
|
fmt.Println(line)
|
2026-07-25 17:58:54 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-07-25 17:24:29 -07:00
|
|
|
}
|