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:
@@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user