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
+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()
}