Panel rebuilt as the heat matrix: verdict header (ring + newest fault named with its age, cable cell top right), one row per check with NOW / last-90s-at-5s-a-cell / since-reset columns, noise as a blue context lane; history.go keeps the 18-slot wall-time ring (per-class fault deltas, SNR minima, rate verdicts, measuring gray) plus since-reset aggregates and fault ages that clear on reset; noise cycle grid-locked to the shared slot clock so cells never straddle a phase and flicker, presence granted at boot until the first up-phase verdict; check/cross stroked as capsule marks (font has neither), line rate is a pure verdict everywhere, footer is a three-line stat stack beside hold-to-reset; old panel/chip/stat-grid machinery deleted; mockups/ gitignored

This commit is contained in:
flamingcow
2026-08-17 12:22:55 -07:00
parent e3c3f945c5
commit fb4b4596e9
8 changed files with 641 additions and 337 deletions
+1
View File
@@ -1 +1,2 @@
/shots/
/mockups/
+11 -10
View File
@@ -65,21 +65,22 @@ func drawFatal(cause error) error {
return err
}
const margin, lineGap = 32, 8
fb.fill(uiBg)
noteY := fb.h - spaceRow - body.lineH
y := spaceRow
title.draw(fb, spaceRow, y-title.capTop, "FATAL", uiRed)
y += title.lineH + spaceRow
for _, line := range wrapWords(cause.Error(), (fb.w-2*spaceRow)/body.cellW) {
if y+body.lineH > noteY-spaceRow {
body.draw(fb, spaceRow, y-body.capTop, "...", uiFg)
noteY := fb.h - margin - body.lineH
y := margin
title.draw(fb, margin, y-title.capTop, "FATAL", uiCrit)
y += title.lineH + margin
for _, line := range wrapWords(cause.Error(), (fb.w-2*margin)/body.cellW) {
if y+body.lineH > noteY-margin {
body.draw(fb, margin, y-body.capTop, "...", uiInk)
break
}
body.draw(fb, spaceRow, y-body.capTop, line, uiFg)
y += body.lineH + spaceTight
body.draw(fb, margin, y-body.capTop, line, uiInk)
y += body.lineH + lineGap
}
note := fmt.Sprintf("rebooting in %d minutes", int(fatalRebootDelay.Minutes()))
body.draw(fb, spaceRow, noteY-body.capTop, note, uiDim)
body.draw(fb, margin, noteY-body.capTop, note, uiMuted)
return fb.flush()
}
+26
View File
@@ -432,6 +432,32 @@ func (fb *framebuffer) roundRect(x0, y0, w, h, r int, c rgb) {
fb.rect(x0+w-r, y0+r, r-1, h-2*r, c)
}
// A round-capped stroke between two points, drawn as a capsule: coverage from
// distance to the segment, so marks the font lacks (check, cross) match its
// antialiasing.
func (fb *framebuffer) stroke(x0, y0, x1, y1, t float64, c rgb) {
r := t / 2
minx, maxx := int(math.Floor(math.Min(x0, x1)-r))-1, int(math.Ceil(math.Max(x0, x1)+r))+1
miny, maxy := int(math.Floor(math.Min(y0, y1)-r))-1, int(math.Ceil(math.Max(y0, y1)+r))+1
dx, dy := x1-x0, y1-y0
len2 := dx*dx + dy*dy
for y := miny; y <= maxy; y++ {
for x := minx; x <= maxx; x++ {
px, py := float64(x)-x0, float64(y)-y0
u := 0.0
if len2 > 0 {
u = math.Max(0, math.Min(1, (px*dx+py*dy)/len2))
}
ex, ey := px-u*dx, py-u*dy
cov := r - math.Sqrt(ex*ex+ey*ey) + 0.5
if cov <= 0 {
continue
}
fb.blend(x, y, c, uint8(math.Min(cov, 1)*255))
}
}
}
// Blends src over the existing pixel, with cov as 0-255 coverage.
func (fb *framebuffer) blend(x, y int, c rgb, cov uint8) {
if x < 0 || y < 0 || x >= fb.w || y >= fb.h || cov == 0 {
+186
View File
@@ -0,0 +1,186 @@
package main
import (
"sync"
"time"
)
// The panel's recent-history ring: 18 slots of 5 seconds, sampled once a
// second from the same figures the console row prints. Slots are wall-time
// and survive a reset — recently is recently, whatever the counters were
// re-based to — while the since-reset aggregates and fault ages clear.
const (
histSlots = 18
histSlotSpan = 5 * time.Second
histSpan = histSlots * histSlotSpan
// Line rate is a verdict here, not a number: the headline is smeared and
// briefly gated at startup, so anything close to the target counts.
rateOKFrac = 0.98
)
var faultClasses = []struct {
label string
noun string
get func(errs) uint64
}{
{"lost", "loss", func(e errs) uint64 { return e.lost }},
{"corrupt", "corrupt", func(e errs) uint64 { return e.corrupt }},
{"link", "link fault", func(e errs) uint64 { return e.link }},
{"internal", "internal", func(e errs) uint64 { return e.internal }},
}
type histSlot struct {
seq int64
sampled bool
measuring bool
faults [4]uint64
corrected uint64
rateLow bool
haveSNR bool
snrMin float64
noiseOn int
noiseN int
}
type history struct {
mu sync.Mutex
start time.Time
slots [histSlots]histSlot
seq int64
prevValid bool
prevSince errs
prevCorrected uint64
lastFault [4]time.Time
sinceRateLow bool
sinceHaveSNR bool
sinceSNRMin float64
}
func newHistory(now time.Time) *history {
return &history{start: now}
}
func (h *history) slot(seq int64) *histSlot {
s := &h.slots[seq%histSlots]
if s.seq != seq || !s.sampled {
*s = histSlot{seq: seq, sampled: true}
}
return s
}
// Counters only move forward between samples except across a reset, where
// they re-base to zero; a backward step is that re-base, not negative faults.
func delta(now, prev uint64) uint64 {
if now < prev {
return 0
}
return now - prev
}
func (h *history) sample(now time.Time, v view, phy phyDisplay, nv noiseView,
measuring bool, target float64) {
h.mu.Lock()
defer h.mu.Unlock()
h.seq = int64(now.Sub(h.start) / histSlotSpan)
s := h.slot(h.seq)
if h.prevValid {
for i, c := range faultClasses {
d := delta(c.get(v.since), c.get(h.prevSince))
s.faults[i] += d
if d > 0 {
h.lastFault[i] = now
}
}
s.corrected += delta(phy.corrected, h.prevCorrected)
}
h.prevValid = true
h.prevSince = v.since
h.prevCorrected = phy.corrected
if measuring {
s.measuring = true
} else {
if v.rxGbps < rateOKFrac*target {
s.rateLow = true
h.sinceRateLow = true
}
if phy.haveSNR {
if !s.haveSNR || phy.worstMargin < s.snrMin {
s.haveSNR, s.snrMin = true, phy.worstMargin
}
if !h.sinceHaveSNR || phy.worstMargin < h.sinceSNRMin {
h.sinceHaveSNR, h.sinceSNRMin = true, phy.worstMargin
}
}
}
s.noiseN++
if nv.on {
s.noiseOn++
}
}
// The wall-time slots stay: recently is recently. Everything judged against
// the reset origin clears.
func (h *history) reset() {
h.mu.Lock()
h.prevValid = false
h.lastFault = [4]time.Time{}
h.sinceRateLow = false
h.sinceHaveSNR = false
h.sinceSNRMin = 0
h.mu.Unlock()
}
type histView struct {
// Oldest first; a slot that predates the ring or was never sampled has
// sampled false and paints as unknown.
slots [histSlots]histSlot
lastFault [4]time.Time
sinceRateLow bool
sinceHaveSNR bool
sinceSNRMin float64
}
func (h *history) view() histView {
h.mu.Lock()
defer h.mu.Unlock()
var out histView
for k := 0; k < histSlots; k++ {
seq := h.seq - int64(histSlots-1-k)
if seq < 0 {
continue
}
s := h.slots[seq%histSlots]
if s.seq == seq && s.sampled {
out.slots[k] = s
}
}
out.lastFault = h.lastFault
out.sinceRateLow = h.sinceRateLow
out.sinceHaveSNR = h.sinceHaveSNR
out.sinceSNRMin = h.sinceSNRMin
return out
}
// The newest fault on record names the verdict; the cable's own verdict
// outranks traffic faults because it is the thing under test.
func (hv histView) newestFault(now time.Time) (i int, age time.Duration, ok bool) {
best := -1
for c, t := range hv.lastFault {
if t.IsZero() {
continue
}
if best < 0 || t.After(hv.lastFault[best]) {
best = c
}
}
if best < 0 {
return 0, 0, false
}
return best, now.Sub(hv.lastFault[best]), true
}
+13 -6
View File
@@ -341,9 +341,9 @@ func (d *direction) counters(now counterSet) view {
// 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 {
func phyView(diag *cableDiag, modules []*phyModule) (phyDisplay, bool) {
info, measuring := diag.snapshot()
return phyDisplayFrom(info, measuring, modules[0].view(), modules[1].view())
return phyDisplayFrom(info, measuring, modules[0].view(), modules[1].view()), measuring
}
func totalView(views []view) view {
@@ -773,12 +773,16 @@ func run(aName, bName string) (err error) {
}
start := time.Now()
// Anchored to the same clock as the noise cycle, so slot boundaries and
// phase transitions coincide.
hist := newHistory(rateEpochStart)
// 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)
hist.reset()
}
}
close(startTx)
@@ -811,9 +815,10 @@ func run(aName, bName string) (err error) {
for i, d := range dirs {
views[i] = d.displayView()
}
v, phy := totalView(views), phyView(diag, modules)
if err := disp.render(v, now.Sub(start), phy,
noise.view()); err != nil {
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 {
return err
}
case now := <-tick.C:
@@ -821,7 +826,9 @@ func run(aName, bName string) (err error) {
for i, d := range dirs {
rows[i] = d.displayView()
}
v, phy := totalView(rows), phyView(diag, modules)
v := totalView(rows)
phy, measuring := phyView(diag, modules)
hist.sample(now, v, phy, noise.view(), measuring, target)
for _, m := range modules {
for _, n := range m.takeNotes() {
fmt.Println(stats.rule(n))
+26 -22
View File
@@ -28,8 +28,6 @@ import (
const (
noiseDriver = "i40e"
noiseFrameLen = 1514
noiseUpSpan = 5 * time.Second
noiseDownSpan = 5 * time.Second
noiseFrameGap = 10 * time.Millisecond
noiseEther uint16 = etherBase + numStreams
@@ -94,6 +92,10 @@ func newNoiser() (*noiser, error) {
}
n := &noiser{eps: [2]endpoint{a, b}}
// The grid-locked cycle can start mid-down-slot, where absence would be
// our own doing: presence is granted until the first up phase delivers a
// verdict.
n.connected.Store(true)
for i, p := range [][2]endpoint{{a, b}, {b, a}} {
fd, err := openTxSocket(p[0].idx)
if err != nil {
@@ -169,6 +171,10 @@ func (n *noiser) bothUp() bool {
// comes and goes under the cycle, and a frame this side declined to send is as
// good as one the wire mangled. What matters is only ever what the test cable
// counted.
//
// The cycle is locked to the panel's history grid — up on even 5 s slots,
// down on odd, transitions on the shared slot boundaries — so a history cell
// never straddles a phase and flickers between the two answers.
func (n *noiser) run(done *atomic.Bool) {
fd, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, 0)
if err != nil {
@@ -179,29 +185,27 @@ func (n *noiser) run(done *atomic.Bool) {
tick := time.NewTicker(noiseFrameGap)
defer tick.Stop()
applied, wasUp, linked := false, false, false
for !done.Load() {
n.setLinks(fd, true)
n.radiating.Store(true)
linked := false
for end := time.Now().Add(noiseUpSpan); time.Now().Before(end) && !done.Load(); {
<-tick.C
if !n.bothUp() {
continue
}
linked = true
n.connected.Store(true)
for i := range n.ports {
unix.Write(n.ports[i].fd, n.ports[i].frame)
<-tick.C
up := (time.Since(rateEpochStart)/histSlotSpan)%2 == 0
if !applied || up != wasUp {
// A whole up phase with no link is many times the ~1s the wire
// needs to train, so at its end the silence is the cable's answer.
if applied && wasUp {
n.connected.Store(linked)
}
n.setLinks(fd, up)
n.radiating.Store(up)
applied, wasUp, linked = true, up, false
}
// A whole up phase with no link is many times the ~1s the wire needs
// to train, so by now the silence is the cable's answer.
n.connected.Store(linked)
n.setLinks(fd, false)
n.radiating.Store(false)
for end := time.Now().Add(noiseDownSpan); time.Now().Before(end) && !done.Load(); {
<-tick.C
if !up || !n.bothUp() {
continue
}
linked = true
n.connected.Store(true)
for i := range n.ports {
unix.Write(n.ports[i].fd, n.ports[i].frame)
}
}
// Left up rather than wherever the cycle stopped, so a run never strands
+356 -266
View File
@@ -4,69 +4,66 @@ import (
"fmt"
"math"
"runtime/debug"
"strings"
"time"
)
var (
uiBg = rgb{0x12, 0x14, 0x18}
uiOKFill = rgb{0x18, 0x42, 0x26}
uiOKEdge = rgb{0x3c, 0xe0, 0x70}
uiErrFil = rgb{0x54, 0x18, 0x1c}
uiErrEdg = rgb{0xff, 0x46, 0x46}
uiFg = rgb{0xe6, 0xe8, 0xea}
uiDim = rgb{0x9a, 0xa2, 0xac}
uiCyan = rgb{0x5c, 0xc8, 0xe0}
uiGreen = rgb{0x6c, 0xdc, 0x86}
uiAmber = rgb{0xe8, 0xc4, 0x52}
uiRed = rgb{0xf0, 0x6b, 0x6b}
uiBg = rgb{0x0d, 0x0d, 0x0d}
uiInk = rgb{0xf2, 0xf2, 0xf0}
uiInk2 = rgb{0xc3, 0xc2, 0xb7}
uiMuted = rgb{0x89, 0x87, 0x81}
uiHair = rgb{0x28, 0x28, 0x26}
uiGood = rgb{0x0c, 0xa3, 0x0c}
uiWarn = rgb{0xfa, 0xb2, 0x19}
uiCrit = rgb{0xd0, 0x3b, 0x3b}
uiCellOK = rgb{0x0a, 0x5d, 0x0a}
uiCellNA = rgb{0x3a, 0x3a, 0x37}
uiNoise = rgb{0x1c, 0x5c, 0xab}
uiNoise0 = rgb{0x24, 0x24, 0x23}
uiAccent = rgb{0x39, 0x87, 0xe5}
)
func classColor(c int) rgb {
switch c {
case clsGood:
return uiGreen
return uiGood
case clsWarn:
return uiAmber
return uiWarn
case clsBad:
return uiRed
return uiCrit
}
return uiDim
return uiMuted
}
// Distances between ink, since layout measures a line from the top of a digit
// to the baseline rather than across a cell with accent and descender slack in
// it. Values carried over from spacing cells will look too small here.
const (
step = 4
uiMargin = 24
uiColGap = 12
headerTop = 26
ringD = 88
ringEdge = 5
spaceTight = step * 2
spaceGroup = step * 4
spaceRow = step * 8
)
cellH = 20
cellR = 4
cellGapX = 3
rowH = 46
const (
uiMargin = spaceGroup
uiPad = spaceGroup
uiBorder = step * 3
pairGap = spaceGroup
blockGap = spaceGroup
btnW = 300
btnH = 80
btnW = 220
btnH = 72
btnRadius = 10
btnBorder = 2
holdDuration = time.Second
versionSpotSide = 200
chipRadius = spaceTight
chipBorder = 2
gridCols = 2
chipPadY = step * 4
chipLineGap = step * 3
chipGap = spaceTight
statRowGap = spaceRow
)
// The panel is the heat matrix: one row per check, columns for now, the last
// 90 seconds at 5 s a cell, and since reset. Noise rides at the bottom as
// context — state, not a verdict.
var panelRows = []string{
"line rate", "lost", "corrupt", "link", "internal", "corrected", "SNR dB", "noise",
}
type rect struct {
x, y, w, h int
}
@@ -77,18 +74,28 @@ func (r rect) contains(x, y int) bool {
type display struct {
fb *framebuffer
huge *textFace
big *textFace
grid *textFace
gridB *textFace
text *textFace
textB *textFace
small *textFace
nowPanel rect
sincePanel rect
nowYs []int
sinceYs []int
resetBtn rect
holdStart time.Time
holdFrac float64
fired bool
headerH int
rowYs []int
axisY int
labelX int
nowX int
nowW int
cellsX int
cellW int
cellsW int
resetX int
resetW int
resetBtn rect
holdStart time.Time
holdFrac float64
fired bool
version string
versionSpot rect
@@ -106,9 +113,11 @@ func newDisplay() (*display, error) {
bold bool
size float64
}{
{&d.huge, true, 40},
{&d.big, true, 36},
{&d.grid, false, 30},
{&d.gridB, true, 30},
{&d.text, false, 22},
{&d.textB, true, 22},
{&d.small, false, 17},
} {
face, err := loadFace(spec.bold, spec.size)
if err != nil {
@@ -117,11 +126,6 @@ func newDisplay() (*display, error) {
}
*spec.dst = face
}
if d.grid.cellW != d.gridB.cellW {
fb.close()
return nil, fmt.Errorf("grid faces disagree on cell width: %d vs %d",
d.grid.cellW, d.gridB.cellW)
}
if err := d.layout(fb.w, fb.h); err != nil {
fb.close()
return nil, err
@@ -130,55 +134,45 @@ func newDisplay() (*display, error) {
}
func (d *display) layout(w, h int) error {
now := []int{d.statsH(d.big, 3), d.chipsH()}
since := []int{d.statsH(d.gridB, 4), d.statsH(d.gridB, 1), d.countsH(), btnH}
// One gap for both panels, and vertically the frame is the border alone.
// Insetting by uiPad as well would add it to the gaps at a panel's ends but
// not to the ones between blocks, which is not equal spacing however evenly
// the remainder is divided.
gaps := len(now) + len(since) + 2
spare := h - 2*uiMargin - blockGap - 4*uiBorder - sum(now) - sum(since)
if spare < 0 {
return fmt.Errorf("panel content is %dpx taller than the screen", -spare)
labelW := 0
for _, r := range panelRows {
labelW = max(labelW, len(r)*d.text.cellW)
}
gap := spare / gaps
d.labelX = uiMargin
d.nowW = 5 * d.textB.cellW
d.resetW = 6 * d.textB.cellW
d.nowX = d.labelX + labelW + uiColGap
d.resetX = w - uiMargin - d.resetW
inner := w - 2*uiMargin
nowH := 2*uiBorder + sum(now) + (len(now)+1)*gap
sinceH := 2*uiBorder + sum(since) + (len(since)+1)*gap
d.nowPanel = rect{uiMargin, uiMargin, inner, nowH}
d.sincePanel = rect{uiMargin, uiMargin + nowH + blockGap, inner, sinceH}
cellsX := d.nowX + d.nowW + uiColGap
cellsW := d.resetX - uiColGap - cellsX
d.cellW = (cellsW - (histSlots-1)*cellGapX) / histSlots
if d.cellW < 6 {
return fmt.Errorf("heat cells would be %dpx wide", d.cellW)
}
d.cellsW = histSlots*d.cellW + (histSlots-1)*cellGapX
// The leftover from rounding sits between the cells and the reset column.
d.cellsX = cellsX
d.nowYs = stack(d.nowPanel.y+uiBorder+gap, now, gap)
d.sinceYs = stack(d.sincePanel.y+uiBorder+gap, since, gap)
d.resetBtn = rect{
x: d.sincePanel.x + (inner-btnW)/2,
y: d.sinceYs[len(d.sinceYs)-1],
w: btnW,
h: btnH,
d.headerH = headerTop + ringD + 24
colHdrH := d.small.lineH + 18
d.rowYs = make([]int, len(panelRows))
y := d.headerH + colHdrH
for i := range panelRows {
d.rowYs[i] = y
y += rowH
}
d.axisY = y + 8
d.resetBtn = rect{w - uiMargin - btnW, h - uiMargin - btnH, btnW, btnH}
if d.axisY+d.small.lineH > d.resetBtn.y {
return fmt.Errorf("panel content is %dpx taller than the screen leaves",
d.axisY+d.small.lineH-d.resetBtn.y)
}
d.versionSpot = rect{w - versionSpotSide, 0, versionSpotSide, versionSpotSide}
return nil
}
func sum(hs []int) int {
var t int
for _, h := range hs {
t += h
}
return t
}
func stack(y int, hs []int, gap int) []int {
ys := make([]int, len(hs))
for i, h := range hs {
ys[i] = y
y += h + gap
}
return ys
}
// Lifting or sliding off cancels, and the press has to be released before it
// can arm again.
func (d *display) holdReset(x, y int, down bool, now time.Time) bool {
@@ -201,37 +195,37 @@ func (d *display) holdReset(x, y int, down bool, now time.Time) bool {
return true
}
// Cyan rather than the status colours because it is something to press, not
// Accent rather than the status colours because it is something to press, not
// something being reported.
func (d *display) drawResetButton() {
r := d.resetBtn
d.fb.roundRect(r.x, r.y, r.w, r.h, chipRadius, uiCyan)
d.fb.roundRect(r.x+chipBorder, r.y+chipBorder, r.w-2*chipBorder, r.h-2*chipBorder,
chipRadius-chipBorder, uiBg)
d.fb.roundRect(r.x, r.y, r.w, r.h, btnRadius, uiAccent)
d.fb.roundRect(r.x+btnBorder, r.y+btnBorder, r.w-2*btnBorder, r.h-2*btnBorder,
btnRadius-btnBorder, uiBg)
split := r.x + chipBorder
split := r.x + btnBorder
if d.holdFrac > 0 {
w := int(float64(r.w-2*chipBorder) * math.Min(d.holdFrac, 1))
d.fb.roundRect(r.x+chipBorder, r.y+chipBorder, w, r.h-2*chipBorder,
chipRadius-chipBorder, uiCyan)
w := int(float64(r.w-2*btnBorder) * math.Min(d.holdFrac, 1))
d.fb.roundRect(r.x+btnBorder, r.y+btnBorder, w, r.h-2*btnBorder,
btnRadius-btnBorder, uiAccent)
split += w
}
label, base := "RESET", uiCyan
label, base := "RESET", uiAccent
if d.showVersion {
label, base = d.version, uiDim
label, base = d.version, uiMuted
}
lx := r.x + (r.w-len(label)*d.gridB.cellW)/2
ly := r.y + (r.h-d.gridB.lineH)/2 - d.gridB.capTop
lx := r.x + (r.w-len(label)*d.textB.cellW)/2
ly := r.y + (r.h-d.textB.lineH)/2 - d.textB.capTop
// The label straddles the fill, so each glyph takes the colour that reads
// against whatever is behind it.
for i, c := range label {
gx := lx + i*d.gridB.cellW
gx := lx + i*d.textB.cellW
col := base
if gx+d.gridB.cellW/2 < split {
if gx+d.textB.cellW/2 < split {
col = uiBg
}
d.gridB.draw(d.fb, gx, ly, string(c), col)
d.textB.draw(d.fb, gx, ly, string(c), col)
}
}
@@ -267,194 +261,290 @@ func (d *display) close() {
d.fb.close()
}
// y is the top of the line as read, not the top of the cell, so text and a
// bordered box placed the same distance apart look it. Returns the top of the
// next line, gapless — the caller owns the spacing below.
func (d *display) centerIn(f *textFace, x, w, y int, s string, col rgb) int {
f.draw(d.fb, x+(w-len([]rune(s))*f.cellW)/2, y-f.capTop, s, col)
return y + f.lineH
type seg struct {
f *textFace
s string
c rgb
}
type statCell struct {
value string
label string
col rgb
func (d *display) segs(x, y int, ss []seg) int {
for _, g := range ss {
g.f.draw(d.fb, x, y-g.f.capTop, g.s, g.c)
x += len([]rune(g.s)) * g.f.cellW
}
return x
}
func (d *display) statPairH(vf *textFace) int {
return vf.lineH + pairGap + d.grid.lineH
func (d *display) textRightAt(f *textFace, right, y int, s string, c rgb) {
f.draw(d.fb, right-len([]rune(s))*f.cellW, y-f.capTop, s, c)
}
func gridRows(n int) int { return (n + gridCols - 1) / gridCols }
func (d *display) statsH(vf *textFace, n int) int {
return gridRows(n)*(d.statPairH(vf)+statRowGap) - statRowGap
// The font has no check or cross, so both are stroked to match its weight.
func (d *display) check(cx, cy int, s float64, c rgb) {
t := math.Max(2.4, s*0.16)
fx, fy := float64(cx), float64(cy)
d.fb.stroke(fx-0.38*s, fy+0.04*s, fx-0.12*s, fy+0.30*s, t, c)
d.fb.stroke(fx-0.12*s, fy+0.30*s, fx+0.40*s, fy-0.28*s, t, c)
}
// A last row that does not fill the grid is centred.
func gridCell(i, n, cols, x, w int) (cx, cw int) {
cw = (w - (cols-1)*chipGap) / cols
inRow := min(n-(i/cols)*cols, cols)
cx = x + (w-(inRow*cw+(inRow-1)*chipGap))/2 + (i%cols)*(cw+chipGap)
return cx, cw
func (d *display) cross(cx, cy int, s float64, c rgb) {
t := math.Max(2.4, s*0.16)
fx, fy := float64(cx), float64(cy)
d.fb.stroke(fx-0.30*s, fy-0.30*s, fx+0.30*s, fy+0.30*s, t, c)
d.fb.stroke(fx-0.30*s, fy+0.30*s, fx+0.30*s, fy-0.30*s, t, c)
}
// An empty value takes its space without drawing, so nothing below it moves
// when it arrives.
func (d *display) stats(vf *textFace, x, w, y int, cells []statCell) int {
for i, c := range cells {
if c.value == "" {
continue
func (d *display) hairline(y int) {
d.fb.rect(uiMargin, y, d.fb.w-2*uiMargin, 1, uiHair)
}
// A mark or short value in the NOW/RESET columns, right-aligned to the
// column's edge and vertically centred in the row.
func (d *display) colMark(right, rowY int, kind int, c rgb) {
cy := rowY + rowH/2
switch kind {
case markCheck:
d.check(right-11, cy, 20, c)
case markCross:
d.cross(right-11, cy, 20, c)
}
}
const (
markCheck = iota
markCross
)
func (d *display) colText(f *textFace, right, rowY int, s string, c rgb) {
d.textRightAt(f, right, rowY+(rowH-f.lineH)/2, s, c)
}
func (d *display) heatCell(rowY, k int, c rgb) {
x := d.cellsX + k*(d.cellW+cellGapX)
d.fb.roundRect(x, rowY+(rowH-cellH)/2, d.cellW, cellH, cellR, c)
}
// The verdict names the newest thing wrong at any horizon: the cable's own
// fault first, then the most recent traffic fault with its age, and only a
// clean record reads GOOD.
func (d *display) drawHeader(v view, phy phyDisplay, hv histView, now time.Time) {
good := true
word, sub := "GOOD", []seg{{d.text, "no faults", uiInk2}}
if i, age, ok := hv.newestFault(now); ok {
good = false
word = scaleCount(faultClasses[i].get(v.since)) + " " +
strings.ToUpper(faultClasses[i].label)
sub = []seg{
{d.text, "last " + faultClasses[i].noun + " ", uiInk2},
{d.textB, scaleTime(age) + " ago", uiInk},
}
cx, cw := gridCell(i, len(cells), gridCols, x, w)
cy := y + (i/gridCols)*(d.statPairH(vf)+statRowGap)
ly := d.centerIn(vf, cx, cw, cy, c.value, c.col) + pairGap
d.centerIn(d.grid, cx, cw, ly, c.label, uiDim)
}
return y + d.statsH(vf, len(cells))
}
var errRows = []struct {
label string
get func(errs) uint64
}{
{"lost", func(e errs) uint64 { return e.lost }},
{"corrupt", func(e errs) uint64 { return e.corrupt }},
{"link", func(e errs) uint64 { return e.link }},
{"internal", func(e errs) uint64 { return e.internal }},
}
func (d *display) chipH() int { return d.grid.lineH + 2*chipPadY }
func (d *display) countChipH() int { return d.chipH() + d.gridB.lineH + chipLineGap }
// The error chips plus corrected and the noise cable's, which share their
// row grid.
func (d *display) chipsH() int {
return gridRows(len(errRows)+2)*(d.chipH()+chipGap) - chipGap
}
func (d *display) countsH() int {
return gridRows(len(errRows))*(d.countChipH()+chipGap) - chipGap
}
// Outlined by drawing the border colour and sinking a smaller well of
// background into it, so both curves get the same antialiasing.
func (d *display) chipAt(i, n, cols, x, w, y, h int, c rgb) (int, int, int) {
cx, cw := gridCell(i, n, cols, x, w)
cy := y + (i/cols)*(h+chipGap)
d.fb.roundRect(cx, cy, cw, h, chipRadius, c)
d.fb.roundRect(cx+chipBorder, cy+chipBorder,
cw-2*chipBorder, h-2*chipBorder, chipRadius-chipBorder, uiBg)
return cx, cw, cy
}
// Whether rather than how many: over a window this short a count changes faster
// than it can be read. The noise chip rides along at the end, presence rather
// than health: red is the cable missing, not the cable failing. The cycle
// phase stays off the panel (the console column still carries it).
func (d *display) errChips(x, w, y int, e errs, recentCorrected uint64, nv noiseView) int {
n := len(errRows) + 2
for i, r := range errRows {
c := errColor(r.get(e))
cx, cw, cy := d.chipAt(i, n, gridCols, x, w, y, d.chipH(), c)
d.centerIn(d.grid, cx, cw, cy+chipPadY, r.label, c)
if phy.metresClass == clsBad {
good = false
word = phy.metres
sub = []seg{{d.text, "cable fault", uiInk2}}
}
c := uiGreen
if recentCorrected > 0 {
c = uiAmber
col := uiGood
if !good {
col = uiCrit
}
cx, cw, cy := d.chipAt(len(errRows), n, gridCols, x, w, y, d.chipH(), c)
d.centerIn(d.grid, cx, cw, cy+chipPadY, "corrected", c)
c = errColor(nv.missing)
cx, cw, cy = d.chipAt(len(errRows)+1, n, gridCols, x, w, y, d.chipH(), c)
d.centerIn(d.grid, cx, cw, cy+chipPadY, "noise", c)
return y + d.chipsH()
rx, ry := uiMargin, headerTop
d.fb.roundRect(rx, ry, ringD, ringD, ringD/2, col)
d.fb.roundRect(rx+ringEdge, ry+ringEdge, ringD-2*ringEdge, ringD-2*ringEdge,
ringD/2-ringEdge, uiBg)
if good {
d.check(rx+ringD/2, ry+ringD/2, 44, col)
} else {
d.cross(rx+ringD/2, ry+ringD/2, 40, col)
}
tx := rx + ringD + 22
d.huge.draw(d.fb, tx, ry+8-d.huge.capTop, word, col)
d.segs(tx, ry+8+d.huge.lineH+16, sub)
// The cable's own cell, top right: the one per-measure fact.
unit := ""
if phy.metresClass == clsGood {
unit = " m"
}
right := d.fb.w - uiMargin
vw := len([]rune(phy.metres))*d.big.cellW + len([]rune(unit))*d.small.cellW
d.segs(right-vw, ry+8, []seg{
{d.big, phy.metres, classColor(phy.metresClass)},
{d.small, unit, uiMuted},
})
d.textRightAt(d.small, right, ry+8+d.big.lineH+14, "cable", uiMuted)
}
func (d *display) errCounts(x, w, y int, e errs) int {
for i, r := range errRows {
n := r.get(e)
c := errColor(n)
cx, cw, cy := d.chipAt(i, len(errRows), gridCols, x, w, y, d.countChipH(), c)
ty := d.centerIn(d.gridB, cx, cw, cy+chipPadY, scaleCount(n), c) + chipLineGap
d.centerIn(d.grid, cx, cw, ty, r.label, c)
}
return y + d.countsH()
func (d *display) drawColHeaders() {
y := d.headerH + 8
d.textRightAt(d.small, d.nowX+d.nowW, y, "NOW", uiMuted)
hdr := "LAST 90 S"
hw := len([]rune(hdr)) * d.small.cellW
d.small.draw(d.fb, d.cellsX+(d.cellsW-hw)/2, y-d.small.capTop, hdr, uiMuted)
d.textRightAt(d.small, d.resetX+d.resetW, y, "RESET", uiMuted)
d.hairline(d.rowYs[0] - 1)
}
func metresStat(phy phyDisplay) statCell {
col := uiFg
unit := "m"
if phy.metresClass != clsGood {
col = classColor(phy.metresClass)
unit = "cable"
}
return statCell{phy.metres, unit, col}
func (d *display) drawAxis() {
d.small.draw(d.fb, d.cellsX, d.axisY-d.small.capTop, "90s ago", uiMuted)
d.textRightAt(d.small, d.cellsX+d.cellsW, d.axisY, "now", uiMuted)
}
func (d *display) panel(p rect, bad bool) (int, int) {
fill, edge := uiOKFill, uiOKEdge
// One row of history cells: na for a slot with nothing believable (never
// sampled, or spent inside a measure), otherwise judged by the row.
func (d *display) heatRow(rowY int, hv histView, judge func(histSlot) rgb) {
for k, s := range hv.slots {
c := uiCellNA
if s.sampled && !s.measuring {
c = judge(s)
}
d.heatCell(rowY, k, c)
}
}
func okOr(bad bool, c rgb) rgb {
if bad {
fill, edge = uiErrFil, uiErrEdg
return c
}
d.fb.rect(p.x, p.y, p.w, p.h, edge)
d.fb.rect(p.x+uiBorder, p.y+uiBorder,
p.w-2*uiBorder, p.h-2*uiBorder, fill)
inset := uiBorder + uiPad
return p.x + inset, p.w - 2*inset
return uiCellOK
}
func errColor(n uint64) rgb {
if n == 0 {
return uiGreen
func (d *display) drawRows(v view, phy phyDisplay, measuring bool, nv noiseView,
hv histView, target float64) {
nowR := d.nowX + d.nowW
resetR := d.resetX + d.resetW
for i, label := range panelRows {
y := d.rowYs[i]
d.text.draw(d.fb, d.labelX, y+(rowH-d.text.lineH)/2-d.text.capTop, label, uiInk2)
d.hairline(y + rowH - 1)
switch label {
case "line rate":
if measuring {
d.colText(d.text, nowR, y, "-", uiMuted)
} else if v.rxGbps >= rateOKFrac*target {
d.colMark(nowR, y, markCheck, uiGood)
} else {
d.colMark(nowR, y, markCross, uiCrit)
}
d.heatRow(y, hv, func(s histSlot) rgb { return okOr(s.rateLow, uiCrit) })
if hv.sinceRateLow {
d.colMark(resetR, y, markCross, uiCrit)
} else {
d.colMark(resetR, y, markCheck, uiGood)
}
case "corrected":
if phy.recent > 0 {
d.colText(d.textB, nowR, y, scaleCount(phy.recent), uiWarn)
} else {
d.colMark(nowR, y, markCheck, uiGood)
}
d.heatRow(y, hv, func(s histSlot) rgb { return okOr(s.corrected > 0, uiWarn) })
if phy.corrected > 0 {
d.colText(d.textB, resetR, y, scaleCount(phy.corrected), uiWarn)
} else {
d.colText(d.text, resetR, y, "0", uiMuted)
}
case "SNR dB":
if phy.haveSNR {
d.colText(d.textB, nowR, y, fmt.Sprintf("%+.1f", phy.worstMargin),
snrInk(phy.worstMargin))
} else {
d.colText(d.text, nowR, y, "-", uiMuted)
}
d.heatRow(y, hv, func(s histSlot) rgb {
if !s.haveSNR {
return uiCellNA
}
return okOr(snrClass(s.snrMin) != clsGood, classColor(snrClass(s.snrMin)))
})
if hv.sinceHaveSNR {
d.colText(d.textB, resetR, y, fmt.Sprintf("%+.1f", hv.sinceSNRMin),
snrInk(hv.sinceSNRMin))
} else {
d.colText(d.text, resetR, y, "-", uiMuted)
}
case "noise":
state := "off"
if nv.on {
state = "on"
}
if nv.missing > 0 {
d.colMark(nowR, y, markCross, uiCrit)
} else {
d.colText(d.text, nowR, y, state, uiInk2)
}
for k, s := range hv.slots {
c := uiCellNA
if s.sampled && s.noiseN > 0 {
c = uiNoise0
if 2*s.noiseOn >= s.noiseN {
c = uiNoise
}
}
d.heatCell(y, k, c)
}
if nv.missing > 0 {
d.colMark(resetR, y, markCross, uiCrit)
} else {
d.colMark(resetR, y, markCheck, uiGood)
}
default:
f := i - 1
if n := faultClasses[f].get(v.window); n > 0 {
d.colText(d.textB, nowR, y, scaleCount(n), uiCrit)
} else {
d.colMark(nowR, y, markCheck, uiGood)
}
d.heatRow(y, hv, func(s histSlot) rgb { return okOr(s.faults[f] > 0, uiCrit) })
if n := faultClasses[f].get(v.since); n > 0 {
d.colText(d.textB, resetR, y, scaleCount(n), uiCrit)
} else {
d.colText(d.text, resetR, y, "0", uiMuted)
}
}
}
return uiRed
}
func snrStat(phy phyDisplay) statCell {
if !phy.haveSNR {
return statCell{"-", "dB margin", uiDim}
// SNR is a number wherever it appears, tinted only when it is worth worrying
// about.
func snrInk(m float64) rgb {
if c := snrClass(m); c != clsGood {
return classColor(c)
}
col := uiFg
if c := snrClass(phy.worstMargin); c != clsGood {
col = classColor(c)
}
return statCell{fmt.Sprintf("%+.1f", phy.worstMargin), "dB margin", col}
return uiInk
}
func correctedStat(v uint64) statCell {
col := uiFg
if v > 0 {
col = uiAmber
// One fact per line, stacked beside the reset button: short lines can never
// crowd it, whatever the values grow to.
func (d *display) drawFooter(v view, elapsed time.Duration) {
const gap = 6
lines := [][]seg{
{{d.small, scaleTime(elapsed), uiInk2}},
{{d.small, scaleCount(v.rxFrames), uiInk2}, {d.small, " pkts", uiMuted}},
{{d.small, scaleCount(v.rxBytes), uiInk2}, {d.small, "B", uiMuted}},
}
total := len(lines)*d.small.lineH + (len(lines)-1)*gap
y := d.resetBtn.y + (btnH-total)/2
for _, l := range lines {
d.segs(uiMargin, y, l)
y += d.small.lineH + gap
}
return statCell{scaleCount(v), "corrected", col}
}
func (d *display) render(v view, elapsed time.Duration, phy phyDisplay, nv noiseView) error {
func (d *display) render(v view, elapsed time.Duration, phy phyDisplay,
measuring bool, nv noiseView, hv histView, target float64) error {
fb := d.fb
fb.fill(uiBg)
x, w := d.panel(d.nowPanel, v.window.total() > 0)
d.stats(d.big, x, w, d.nowYs[0], []statCell{
{scaleSI(v.rxGbps * 1e9), "bits/s", uiFg},
{scaleSI(v.rxPPS), "packets/s", uiFg},
snrStat(phy),
})
d.errChips(x, w, d.nowYs[1], v.window, phy.recent, nv)
x, w = d.panel(d.sincePanel, v.since.total() > 0 || phy.metresClass == clsBad)
d.stats(d.gridB, x, w, d.sinceYs[0], []statCell{
{scaleTime(elapsed), "elapsed", uiFg},
{scaleCount(v.rxFrames), "packets", uiFg},
{scaleCount(v.rxBytes), "bytes", uiFg},
correctedStat(phy.corrected),
})
d.stats(d.gridB, x, w, d.sinceYs[1], []statCell{metresStat(phy)})
d.errCounts(x, w, d.sinceYs[2], v.since)
d.drawHeader(v, phy, hv, time.Now())
d.hairline(d.headerH - 1)
d.drawColHeaders()
d.drawRows(v, phy, measuring, nv, hv, target)
d.drawAxis()
d.drawFooter(v, elapsed)
d.drawResetButton()
return fb.flush()
}
+22 -33
View File
@@ -2,41 +2,19 @@ package main
import "testing"
func TestGridCellFullRow(t *testing.T) {
x0, w0 := gridCell(0, 2, gridCols, 0, 100)
x1, w1 := gridCell(1, 2, gridCols, 0, 100)
if w0 != w1 {
t.Errorf("cells differ in width: %d vs %d", w0, w1)
}
if x0 != 0 {
t.Errorf("first cell x = %d, want 0", x0)
}
if x1+w1 != 100 {
t.Errorf("row ends at %d, want 100", x1+w1)
}
if got := x1 - (x0 + w0); got != chipGap {
t.Errorf("gap between cells = %d, want %d", got, chipGap)
}
}
// A last row that does not fill the grid is centred.
func TestGridCellShortLastRow(t *testing.T) {
cx, cw := gridCell(2, 3, gridCols, 0, 100)
if left, right := cx, 100-(cx+cw); left != right {
t.Errorf("lone cell has %d left and %d right, want centred", left, right)
}
}
func TestPanelFitsScreen(t *testing.T) {
func testDisplay(t *testing.T) *display {
t.Helper()
d := &display{}
for _, spec := range []struct {
dst **textFace
bold bool
size float64
}{
{&d.big, true, 40},
{&d.grid, false, 34},
{&d.gridB, true, 34},
{&d.huge, true, 40},
{&d.big, true, 36},
{&d.text, false, 22},
{&d.textB, true, 22},
{&d.small, false, 17},
} {
face, err := loadFace(spec.bold, spec.size)
if err != nil {
@@ -44,12 +22,23 @@ func TestPanelFitsScreen(t *testing.T) {
}
*spec.dst = face
}
return d
}
func TestPanelFitsScreen(t *testing.T) {
d := testDisplay(t)
if err := d.layout(600, 1024); err != nil {
t.Fatal(err)
}
gap := d.nowYs[0] - (d.nowPanel.y + uiBorder)
t.Logf("panel gap %dpx, since panel ends at %dpx", gap, d.sincePanel.y+d.sincePanel.h)
if gap < spaceTight {
t.Errorf("panel gap is %dpx, want at least %d", gap, spaceTight)
if d.cellW < 6 {
t.Errorf("heat cells %dpx wide, want at least 6", d.cellW)
}
if end := d.cellsX + d.cellsW; end > d.resetX {
t.Errorf("heat cells run to %d, past the reset column at %d", end, d.resetX)
}
if last := d.rowYs[len(d.rowYs)-1] + rowH; last > d.resetBtn.y {
t.Errorf("rows end at %d, past the footer at %d", last, d.resetBtn.y)
}
t.Logf("cells %dpx wide, rows end at %d, footer at %d",
d.cellW, d.axisY+d.small.lineH, d.resetBtn.y)
}