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))
+21 -17
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,13 +185,21 @@ 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() {
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
}
if !up || !n.bothUp() {
continue
}
linked = true
@@ -194,16 +208,6 @@ func (n *noiser) run(done *atomic.Bool) {
unix.Write(n.ports[i].fd, n.ports[i].frame)
}
}
// 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
}
}
// Left up rather than wherever the cycle stopped, so a run never strands
// the ports down for whoever looks next.
n.setLinks(fd, true)
+343 -253
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,14 +74,24 @@ 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
headerH int
rowYs []int
axisY int
labelX int
nowX int
nowW int
cellsX int
cellW int
cellsW int
resetX int
resetW int
nowPanel rect
sincePanel rect
nowYs []int
sinceYs []int
resetBtn rect
holdStart time.Time
holdFrac float64
@@ -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
}
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))
func (d *display) hairline(y int) {
d.fb.rect(uiMargin, y, d.fb.w-2*uiMargin, 1, uiHair)
}
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 }},
// 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)
}
}
func (d *display) chipH() int { return d.grid.lineH + 2*chipPadY }
const (
markCheck = iota
markCross
)
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) colText(f *textFace, right, rowY int, s string, c rgb) {
d.textRightAt(f, right, rowY+(rowH-f.lineH)/2, s, c)
}
func (d *display) countsH() int {
return gridRows(len(errRows))*(d.countChipH()+chipGap) - chipGap
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)
}
// 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
// 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},
}
}
if phy.metresClass == clsBad {
good = false
word = phy.metres
sub = []seg{{d.text, "cable fault", uiInk2}}
}
// 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)
col := uiGood
if !good {
col = uiCrit
}
c := uiGreen
if recentCorrected > 0 {
c = uiAmber
}
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)
}
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)
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"
}
return y + d.countsH()
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 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) 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 (d *display) panel(p rect, bad bool) (int, int) {
fill, edge := uiOKFill, uiOKEdge
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)
}
// 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)
}