Files
cabletest/ui.go
T

272 lines
6.7 KiB
Go

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(fbPath)
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) {
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()
fb.flush()
}