package main import ( "fmt" "math" "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} uiButton = rgb{0x25, 0x2b, 0x33} uiFg = rgb{0xe6, 0xe8, 0xea} uiDim = rgb{0x9a, 0xa2, 0xac} uiCyan = rgb{0x5c, 0xc8, 0xe0} uiGreen = rgb{0x6c, 0xdc, 0x86} uiRed = rgb{0xf0, 0x6b, 0x6b} uiYellow = rgb{0xe0, 0xb0, 0x40} ) const ( uiMargin = 16 uiPad = 14 uiBorder = 12 rowGap = 5 blockGap = 16 btnW = 240 btnH = 64 holdDuration = time.Second ) 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 huge *textFace grid *textFace gridB *textFace nowPanel rect sincePanel rect nowH int sinceH int resetBtn rect holdStart time.Time holdFrac float64 fired bool } func newDisplay() (*display, error) { fb, err := openFramebuffer() if err != nil { return nil, err } d := &display{fb: fb} for _, spec := range []struct { dst **textFace bold bool size float64 }{ {&d.huge, true, 60}, {&d.grid, false, 22}, {&d.gridB, true, 22}, } { 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) } // Guessed heights collide on a screen this small, so the split follows what // the loaded faces actually measure. gridLine := d.grid.cellH + rowGap errH := len(errRows) * gridLine d.nowH = d.huge.cellH + rowGap + gridLine + blockGap + errH d.sinceH = 4*gridLine + blockGap + errH + blockGap + btnH inner := fb.w - 2*uiMargin chrome := 2 * (uiBorder + uiPad) avail := fb.h - 2*uiMargin - blockGap h1 := (avail-2*chrome)*d.nowH/(d.nowH+d.sinceH) + chrome d.nowPanel = rect{uiMargin, uiMargin, inner, h1} d.sincePanel = rect{uiMargin, uiMargin + h1 + blockGap, inner, avail - h1} d.resetBtn = rect{ x: d.sincePanel.x + (inner-btnW)/2, y: d.sincePanel.y + d.sincePanel.h - uiBorder - uiPad - btnH, w: btnW, h: btnH, } return d, nil } // Tracks a press and hold on the reset button, returning true once it has been // held long enough. 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 } func (d *display) drawResetButton() { r := d.resetBtn d.fb.rect(r.x, r.y, r.w, r.h, uiButton) if d.holdFrac > 0 { w := int(float64(r.w) * math.Min(d.holdFrac, 1)) d.fb.rect(r.x, r.y, w, r.h, uiCyan) } for _, e := range []rect{ {r.x, r.y, r.w, 2}, {r.x, r.y + r.h - 2, r.w, 2}, {r.x, r.y, 2, r.h}, {r.x + r.w - 2, r.y, 2, r.h}, } { d.fb.rect(e.x, e.y, e.w, e.h, uiCyan) } label := "RESET" lx := r.x + (r.w-len(label)*d.gridB.cellW)/2 ly := r.y + (r.h-d.gridB.cellH)/2 // The label straddles the fill, so each glyph takes the colour that reads // against whatever is behind it. split := r.x + int(float64(r.w)*math.Min(d.holdFrac, 1)) for i, c := range label { gx := lx + i*d.gridB.cellW col := uiFg if gx+d.gridB.cellW/2 < split { col = uiBg } d.gridB.draw(d.fb, gx, ly, string(c), col) } } func (d *display) close() { d.fb.close() } func (d *display) right(f *textFace, xEnd, y int, s string, col rgb) { f.draw(d.fb, xEnd-len([]rune(s))*f.cellW, y, s, col) } func (d *display) row(f *textFace, x, w, y int, label, value string, col rgb) int { f.draw(d.fb, x, y, label, uiDim) d.right(f, x+w, y, value, col) return y + f.cellH + rowGap } func (d *display) rateRow(x, w, y int, label string, gb, target float64) int { d.gridB.draw(d.fb, x, y+(d.huge.cellH-d.gridB.cellH)/2, label, uiDim) d.right(d.huge, x+w, y, fmt.Sprintf("%.2f Gb/s", gb), rateColor(gb, target)) return y + d.huge.cellH + rowGap } var errRows = []struct { label string get func(errs) uint64 }{ {"lost", func(e errs) uint64 { return e.lost }}, {"late", func(e errs) uint64 { return e.late }}, {"crc", func(e errs) uint64 { return e.crc }}, {"bad magic", func(e errs) uint64 { return e.badMagic }}, {"bad length", func(e errs) uint64 { return e.badLen }}, {"kernel drops", func(e errs) uint64 { return e.kdrop }}, {"link", func(e errs) uint64 { return e.link }}, } func (d *display) errBlock(x, w, y int, e errs) int { for _, r := range errRows { n := r.get(e) y = d.row(d.grid, x, w, y, r.label, commas(n), errColor(n)) } return y } func (d *display) panel(p rect, e errs, contentH int) (int, int, int) { fill, edge := uiOKFill, uiOKEdge if e.total() > 0 { 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 x, w := p.x+inset, p.w-2*inset y := p.y + inset if slack := (p.y + p.h - inset) - y - contentH; slack > 0 { y += slack / 2 } return x, w, y } // Rendered rates are quantised so the text only changes when the value moves // meaningfully. Without this the low digits churn every frame no matter how // long the averaging window is. func roundPPS(v float64) uint64 { const unit = 1000 if v < 0 { return 0 } return uint64((v+unit/2)/unit) * unit } func errColor(n uint64) rgb { if n == 0 { return uiGreen } return uiRed } func rateColor(gb, target float64) rgb { switch { case gb >= target*rateGreenFrac: return uiGreen case gb >= target*rateYellowFrac: return uiYellow default: return uiRed } } func (d *display) render(v view, elapsed time.Duration, target float64, cable string) error { fb := d.fb fb.fill(uiBg) x, w, y := d.panel(d.nowPanel, v.window, d.nowH) y = d.rateRow(x, w, y, "RX", v.rxGbps, target) y = d.row(d.grid, x, w, y, "packets/s", commas(roundPPS(v.rxPPS)), uiFg) d.errBlock(x, w, y+blockGap, v.window) x, w, y = d.panel(d.sincePanel, v.since, d.sinceH) y = d.row(d.grid, x, w, y, "uptime", uptime(elapsed), uiFg) y = d.row(d.grid, x, w, y, "frames", commas(v.rxFrames), uiFg) y = d.row(d.grid, x, w, y, "data", humanBytes(v.rxGot), uiFg) y = d.row(d.grid, x, w, y, "cable", cable, uiCyan) d.errBlock(x, w, y+blockGap, v.since) d.drawResetButton() return fb.flush() }