From 4df30912b1a0a4fa4898800ca7031f1c0c90c9aa Mon Sep 17 00:00:00 2001 From: flamingcow Date: Mon, 17 Aug 2026 21:48:29 -0700 Subject: [PATCH] =?UTF-8?q?Ghost=20SNR=20check=20removed=20as=20asked:=20n?= =?UTF-8?q?o=20bounds,=20no=20panic,=20no=20latch=20re-read=20=E2=80=94=20?= =?UTF-8?q?a=20raw=200x0000=20in=201.133-136=20decodes=20as=20no-margin-th?= =?UTF-8?q?is-poll=20(the=20untrained=20resting=20value,=20which=20appears?= =?UTF-8?q?=20before=201.1=20reports=20a=20drop,=20so=20no=20read=20orderi?= =?UTF-8?q?ng=20could=20prove=20a=20ghost);=20panel=20value=20columns=20bo?= =?UTF-8?q?unded=20by=20scalePanelCount=20(4=20chars=20forever)=20with=20R?= =?UTF-8?q?ESET=20narrowed=20to=205=20chars=20so=20counts=20can=20never=20?= =?UTF-8?q?reach=20the=20timeline;=20rows=20and=20heat=20cells=20stretch?= =?UTF-8?q?=20proportionally=20to=20fill=20the=20band=20between=20header?= =?UTF-8?q?=20and=20footer=20instead=20of=20leaving=20the=20bottom=20half?= =?UTF-8?q?=20dead?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rollball.go | 28 +++++--------------- ui.go | 76 +++++++++++++++++++++++++++++++++++++---------------- ui_test.go | 2 +- 3 files changed, 61 insertions(+), 45 deletions(-) diff --git a/rollball.go b/rollball.go index fb763d1..ca1452a 100644 --- a/rollball.go +++ b/rollball.go @@ -32,11 +32,6 @@ const ( rbPHYIDHi uint16 = 0x002B rbPHYIDLo uint16 = 0x0BF4 - - // IEEE margins land near 7-9 dB on a healthy short cable; far outside is - // another register's data. - rbGhostLow = -10.0 - rbGhostHigh = 25.0 ) // Only the registers proven safe on this PHY (docs/modules/wiitek/): single @@ -170,11 +165,9 @@ func (r *rollball) identify() (ident string, err error) { return } -// An untrained PHY honestly serves 0x0000 here, so an out-of-band value is -// only a ghost if the link stayed up across the reads. The caller drained the -// PMA 1.1 latch when it checked the link, so one latched read now answers -// that: a drop since means the unplug race — discard the batch, ok false, and -// the next poll reads the link down. +// 0x0000 is the untrained PHY's resting value in these offset-0x8000 +// registers, and the margins zero out before 1.1 reports a drop — so a zero +// read is no margin this poll, never data. func (r *rollball) snrMargins() (out [4]float64, ok bool, err error) { r.exec(func() { for i := range out { @@ -182,19 +175,10 @@ func (r *rollball) snrMargins() (out [4]float64, ok bool, err error) { if v, err = r.mdioRead(1, uint16(133+i)); err != nil { return } - m := (float64(v) - 0x8000) / 10 - if m < rbGhostLow || m > rbGhostHigh { - var link uint16 - if link, err = r.mdioRead(1, 1); err != nil { - return - } - if link&0x0004 == 0 { - return - } - panic(fmt.Sprintf("%s: ghost SNR margin %.1f dB (1.%d=%#04x) with the link up", - r.ifname, m, 133+i, v)) + if v == 0 { + return } - out[i] = m + out[i] = (float64(v) - 0x8000) / 10 } ok = true }) diff --git a/ui.go b/ui.go index e281018..6fb1178 100644 --- a/ui.go +++ b/ui.go @@ -4,10 +4,31 @@ import ( "fmt" "math" "runtime/debug" + "strconv" "strings" "time" ) +// Panel counts fit four characters forever: whole numbers to 9999, then a +// magnitude letter with at most one decimal. The value columns are sized to +// this bound, so no count can ever grow into the timeline. +func scalePanelCount(v uint64) string { + if v < 10000 { + return strconv.FormatUint(v, 10) + } + f := float64(v) + for _, mag := range []string{"k", "M", "G", "T", "P"} { + f /= 1000 + if f < 9.95 { + return fmt.Sprintf("%.1f%s", f, mag) + } + if f < 999.5 { + return fmt.Sprintf("%.0f%s", f, mag) + } + } + return fmt.Sprintf("%.0fE", f/1000) +} + var ( uiBg = rgb{0x0d, 0x0d, 0x0d} uiInk = rgb{0xf2, 0xf2, 0xf0} @@ -41,10 +62,12 @@ const ( ringD = 88 ringEdge = 5 - cellH = 20 - cellR = 4 + // Minimum row and cell heights; layout stretches both proportionally to + // fill the height between the header and the footer. + cellHMin = 20 + cellRMin = 4 cellGapX = 3 - rowH = 46 + rowHMin = 46 btnW = 220 btnH = 72 @@ -79,6 +102,9 @@ type display struct { headerH int rowYs []int + rowH int + cellH int + cellR int axisY int labelX int nowX int @@ -137,7 +163,7 @@ func (d *display) layout(w, h int) error { } d.labelX = uiMargin d.nowW = 5 * d.textB.cellW - d.resetW = 6 * d.textB.cellW + d.resetW = 5 * d.textB.cellW d.nowX = d.labelX + labelW + uiColGap d.resetX = w - uiMargin - d.resetW @@ -153,19 +179,25 @@ func (d *display) layout(w, h int) error { d.headerH = headerTop + ringD + 24 colHdrH := d.small.lineH + 18 + d.resetBtn = rect{w - uiMargin - btnW, h - uiMargin - btnH, btnW, btnH} + + // Rows stretch to spend the whole band between the column headers and the + // footer, cells riding along at their design proportion of the row. + rowsTop := d.headerH + colHdrH + d.rowH = (d.resetBtn.y - uiMargin - d.small.lineH - 8 - rowsTop) / len(panelRows) + if d.rowH < rowHMin { + return fmt.Errorf("rows would be %dpx tall, under the %dpx minimum", d.rowH, rowHMin) + } + d.cellH = d.rowH * cellHMin / rowHMin + d.cellR = max(cellRMin, d.cellH*cellRMin/cellHMin) + d.rowYs = make([]int, len(panelRows)) - y := d.headerH + colHdrH + y := rowsTop for i := range panelRows { d.rowYs[i] = y - y += rowH + y += d.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 } @@ -298,7 +330,7 @@ func (d *display) hairline(y int) { // 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 + cy := rowY + d.rowH/2 switch kind { case markCheck: d.check(right-11, cy, 20, c) @@ -313,12 +345,12 @@ const ( ) func (d *display) colText(f *textFace, right, rowY int, s string, c rgb) { - d.textRightAt(f, right, rowY+(rowH-f.lineH)/2, s, c) + d.textRightAt(f, right, rowY+(d.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) + d.fb.roundRect(x, rowY+(d.rowH-d.cellH)/2, d.cellW, d.cellH, d.cellR, c) } // The verdict names the newest thing wrong at any horizon: the cable's own @@ -331,7 +363,7 @@ func (d *display) drawHeader(v view, phy phyDisplay, hv histView, measuring bool 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)) + " " + + word = scalePanelCount(faultClasses[i].get(v.since)) + " " + strings.ToUpper(faultClasses[i].label) sub = []seg{ {d.text, "last " + faultClasses[i].noun + " ", uiInk2}, @@ -421,8 +453,8 @@ func (d *display) drawRows(v view, phy phyDisplay, measuring bool, 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) + d.text.draw(d.fb, d.labelX, y+(d.rowH-d.text.lineH)/2-d.text.capTop, label, uiInk2) + d.hairline(y + d.rowH - 1) switch label { case "line rate": @@ -441,13 +473,13 @@ func (d *display) drawRows(v view, phy phyDisplay, measuring bool, } case "corrected": if phy.recent > 0 { - d.colText(d.textB, nowR, y, scaleCount(phy.recent), uiWarn) + d.colText(d.textB, nowR, y, scalePanelCount(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) + d.colText(d.textB, resetR, y, scalePanelCount(phy.corrected), uiWarn) } else { d.colText(d.text, resetR, y, "0", uiMuted) } @@ -482,13 +514,13 @@ func (d *display) drawRows(v view, phy phyDisplay, measuring bool, panic("panel row " + label + " has no fault class") } if n := faultClasses[f].get(v.window); n > 0 { - d.colText(d.textB, nowR, y, scaleCount(n), uiCrit) + d.colText(d.textB, nowR, y, scalePanelCount(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) + d.colText(d.textB, resetR, y, scalePanelCount(n), uiCrit) } else { d.colText(d.text, resetR, y, "0", uiMuted) } diff --git a/ui_test.go b/ui_test.go index dad4131..9f664b0 100644 --- a/ui_test.go +++ b/ui_test.go @@ -36,7 +36,7 @@ func TestPanelFitsScreen(t *testing.T) { 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 { + if last := d.rowYs[len(d.rowYs)-1] + d.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",