package main import ( "fmt" "math" "runtime/debug" "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} ) func classColor(c int) rgb { switch c { case clsGood: return uiGreen case clsWarn: return uiAmber case clsBad: return uiRed } return uiDim } // 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 spaceTight = step * 2 spaceGroup = step * 4 spaceRow = step * 8 ) const ( uiMargin = spaceGroup uiPad = spaceGroup uiBorder = step * 3 pairGap = spaceGroup blockGap = spaceGroup btnW = 300 btnH = 80 holdDuration = time.Second versionSpotSide = 200 chipRadius = spaceTight chipBorder = 2 gridCols = 2 chipPadY = step * 4 chipLineGap = step * 3 chipGap = spaceTight statRowGap = spaceRow ) type rect struct { x, y, w, h int } func (r rect) contains(x, y int) bool { return x >= r.x && x < r.x+r.w && y >= r.y && y < r.y+r.h } type display struct { fb *framebuffer big *textFace grid *textFace gridB *textFace nowPanel rect sincePanel rect nowYs []int sinceYs []int resetBtn rect holdStart time.Time holdFrac float64 fired bool version string versionSpot rect showVersion bool } func newDisplay() (*display, error) { fb, err := openFramebuffer() if err != nil { return nil, err } d := &display{fb: fb, version: buildVersion()} for _, spec := range []struct { dst **textFace bold bool size float64 }{ {&d.big, true, 36}, {&d.grid, false, 30}, {&d.gridB, true, 30}, } { face, err := loadFace(spec.bold, spec.size) if err != nil { fb.close() return nil, err } *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 } return d, nil } 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) } gap := spare / gaps 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} 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.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 { if !down { d.holdStart, d.holdFrac, d.fired = time.Time{}, 0, false return false } if d.fired || !d.resetBtn.contains(x, y) { d.holdStart, d.holdFrac = time.Time{}, 0 return false } if d.holdStart.IsZero() { d.holdStart = now } d.holdFrac = now.Sub(d.holdStart).Seconds() / holdDuration.Seconds() if d.holdFrac < 1 { return false } d.holdFrac, d.fired, d.holdStart = 0, true, time.Time{} return true } // Cyan 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) split := r.x + chipBorder 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) split += w } label, base := "RESET", uiCyan if d.showVersion { label, base = d.version, uiDim } lx := r.x + (r.w-len(label)*d.gridB.cellW)/2 ly := r.y + (r.h-d.gridB.lineH)/2 - d.gridB.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 col := base if gx+d.gridB.cellW/2 < split { col = uiBg } d.gridB.draw(d.fb, gx, ly, string(c), col) } } // go run never stamps VCS info, so dev builds have no revision to show. func buildVersion() string { info, ok := debug.ReadBuildInfo() if !ok { return "dev" } var rev string var dirty bool for _, s := range info.Settings { switch s.Key { case "vcs.revision": rev = s.Value case "vcs.modified": dirty = s.Value == "true" } } if rev == "" { return "dev" } if len(rev) > 9 { rev = rev[:9] } if dirty { rev += "+" } return rev } 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 statCell struct { value string label string col rgb } func (d *display) statPairH(vf *textFace) int { return vf.lineH + pairGap + d.grid.lineH } 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 } // 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 } // 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)) } 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) } 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() } 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 metresStat(phy phyDisplay) statCell { col := uiFg if phy.metresClass != clsGood { col = classColor(phy.metresClass) } return statCell{phy.metres, "m", col} } func (d *display) panel(p rect, bad bool) (int, int) { fill, edge := uiOKFill, uiOKEdge if bad { fill, edge = uiErrFil, uiErrEdg } 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 } func errColor(n uint64) rgb { if n == 0 { return uiGreen } return uiRed } func snrStat(phy phyDisplay) statCell { if !phy.haveSNR { return statCell{"-", "dB margin", uiDim} } col := uiFg if c := snrClass(phy.worstMargin); c != clsGood { col = classColor(c) } return statCell{fmt.Sprintf("%+.1f", phy.worstMargin), "dB margin", col} } func correctedStat(v uint64) statCell { col := uiFg if v > 0 { col = uiAmber } return statCell{scaleCount(v), "corrected", col} } func (d *display) render(v view, elapsed time.Duration, phy phyDisplay, nv noiseView) 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.drawResetButton() return fb.flush() }