Turn the display portrait and split it into live and since-reset panels
This commit is contained in:
@@ -8,39 +8,27 @@ import (
|
||||
|
||||
var (
|
||||
uiBg = rgb{0x12, 0x14, 0x18}
|
||||
uiPanel = rgb{0x1c, 0x20, 0x26}
|
||||
uiRule = rgb{0x2e, 0x34, 0x3c}
|
||||
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{0x7a, 0x82, 0x8c}
|
||||
uiDim = rgb{0x9a, 0xa2, 0xac}
|
||||
uiCyan = rgb{0x5c, 0xc8, 0xe0}
|
||||
uiGreen = rgb{0x4c, 0xc2, 0x6a}
|
||||
uiRed = rgb{0xe0, 0x4b, 0x4b}
|
||||
uiGreen = rgb{0x6c, 0xdc, 0x86}
|
||||
uiRed = rgb{0xf0, 0x6b, 0x6b}
|
||||
uiYellow = rgb{0xe0, 0xb0, 0x40}
|
||||
)
|
||||
|
||||
// Everything tabular sits on one monospace cell grid and every number is
|
||||
// right-aligned to a column end, so columns line up by construction.
|
||||
const (
|
||||
uiMargin = 16
|
||||
uiPad = 14
|
||||
uiBorder = 12
|
||||
rowGap = 5
|
||||
blockGap = 16
|
||||
|
||||
cellDirA = 0
|
||||
cellArrow = 2
|
||||
cellDirB = 4
|
||||
|
||||
colTxGbEnd = 14
|
||||
colTxPPSEnd = 27
|
||||
colRxGbEnd = 37
|
||||
colRxPPSEnd = 50
|
||||
|
||||
colFramesEnd = 18
|
||||
colDataEnd = 27
|
||||
colLostEnd = 38
|
||||
colLateEnd = 46
|
||||
colCRCEnd = 53
|
||||
colKdropEnd = 62
|
||||
|
||||
btnW = 200
|
||||
btnW = 240
|
||||
btnH = 64
|
||||
holdDuration = time.Second
|
||||
)
|
||||
@@ -58,12 +46,15 @@ type display struct {
|
||||
huge *textFace
|
||||
grid *textFace
|
||||
gridB *textFace
|
||||
small *textFace
|
||||
|
||||
resetBtn rect
|
||||
holdStart time.Time
|
||||
holdFrac float64
|
||||
fired bool
|
||||
nowPanel rect
|
||||
sincePanel rect
|
||||
nowH int
|
||||
sinceH int
|
||||
resetBtn rect
|
||||
holdStart time.Time
|
||||
holdFrac float64
|
||||
fired bool
|
||||
}
|
||||
|
||||
func newDisplay() (*display, error) {
|
||||
@@ -77,10 +68,9 @@ func newDisplay() (*display, error) {
|
||||
bold bool
|
||||
size float64
|
||||
}{
|
||||
{&d.huge, true, 72},
|
||||
{&d.grid, false, 24},
|
||||
{&d.gridB, true, 24},
|
||||
{&d.small, false, 18},
|
||||
{&d.huge, true, 60},
|
||||
{&d.grid, false, 22},
|
||||
{&d.gridB, true, 22},
|
||||
} {
|
||||
face, err := loadFace(spec.bold, spec.size)
|
||||
if err != nil {
|
||||
@@ -94,9 +84,22 @@ func newDisplay() (*display, error) {
|
||||
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: fb.w - uiMargin - btnW,
|
||||
y: fb.h - uiMargin - btnH,
|
||||
x: d.sincePanel.x + (inner-btnW)/2,
|
||||
y: d.sincePanel.y + d.sincePanel.h - uiBorder - uiPad - btnH,
|
||||
w: btnW,
|
||||
h: btnH,
|
||||
}
|
||||
@@ -163,28 +166,59 @@ func (d *display) close() {
|
||||
d.fb.close()
|
||||
}
|
||||
|
||||
func (d *display) cellX(c int) int {
|
||||
return uiMargin + c*d.grid.cellW
|
||||
}
|
||||
|
||||
func (d *display) at(c, y int, s string, col rgb) {
|
||||
d.grid.draw(d.fb, d.cellX(c), y, s, col)
|
||||
}
|
||||
|
||||
func (d *display) atBold(c, y int, s string, col rgb) {
|
||||
d.gridB.draw(d.fb, d.cellX(c), y, s, col)
|
||||
}
|
||||
|
||||
func (d *display) rightAt(cEnd, y int, s string, col rgb) {
|
||||
d.grid.draw(d.fb, d.cellX(cEnd)-len([]rune(s))*d.grid.cellW, y, s, col)
|
||||
}
|
||||
|
||||
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) center(f *textFace, y int, s string, col rgb) {
|
||||
f.draw(d.fb, (d.fb.w-len([]rune(s))*f.cellW)/2, 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
|
||||
@@ -216,101 +250,22 @@ func rateColor(gb, target float64) rgb {
|
||||
}
|
||||
}
|
||||
|
||||
func (d *display) render(dirs []*direction, views []view, elapsed time.Duration, target float64, cable string) {
|
||||
func (d *display) render(v view, elapsed time.Duration, target float64, cable string) {
|
||||
fb := d.fb
|
||||
fb.fill(uiBg)
|
||||
|
||||
d.small.draw(fb, uiMargin, 10, "cabletest", uiCyan)
|
||||
d.small.draw(fb, uiMargin+11*d.small.cellW, 10,
|
||||
fmt.Sprintf("%s %s %s %s", dirs[0].tx.tag, dirs[0].tx.name,
|
||||
dirs[0].rx.tag, dirs[0].rx.name), uiDim)
|
||||
d.right(d.small, fb.w-uiMargin, 10, uptime(elapsed), uiDim)
|
||||
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)
|
||||
|
||||
var total uint64
|
||||
for _, v := range views {
|
||||
total += v.errors
|
||||
}
|
||||
|
||||
bandY, bandH := 40, 150
|
||||
fb.rect(0, bandY, fb.w, bandH, uiPanel)
|
||||
fb.rect(0, bandY, 8, bandH, errColor(total))
|
||||
word, wc := "NO ERRORS", uiGreen
|
||||
if total > 0 {
|
||||
word, wc = fmt.Sprintf("%s ERRORS", commas(total)), uiRed
|
||||
}
|
||||
d.center(d.huge, bandY+(bandH-d.huge.cellH)/2, word, wc)
|
||||
|
||||
// Two tight blocks rather than rows spread over the whole panel, centred in
|
||||
// what is left below the band.
|
||||
lineH := d.grid.cellH + 4
|
||||
sectionH := d.small.cellH + 10 + (1+len(dirs))*lineH
|
||||
gap := 30
|
||||
avail := fb.h - (bandY + bandH) - btnH - 2*uiMargin
|
||||
y := bandY + bandH + 8 + (avail-2*sectionH-gap)/2
|
||||
|
||||
y = d.section(y, "RATE", "")
|
||||
d.rightAt(colTxGbEnd, y, "TX Gb/s", uiDim)
|
||||
d.rightAt(colTxPPSEnd, y, "TX pps", uiDim)
|
||||
d.rightAt(colRxGbEnd, y, "RX Gb/s", uiDim)
|
||||
d.rightAt(colRxPPSEnd, y, "RX pps", uiDim)
|
||||
y += lineH
|
||||
for i, dir := range dirs {
|
||||
d.dirTag(dir, y)
|
||||
v := views[i]
|
||||
d.rightAt(colTxGbEnd, y, fmt.Sprintf("%.2f", v.txGbps), rateColor(v.txGbps, target))
|
||||
d.rightAt(colTxPPSEnd, y, commas(roundPPS(v.txPPS)), uiFg)
|
||||
d.rightAt(colRxGbEnd, y, fmt.Sprintf("%.2f", v.rxGbps), rateColor(v.rxGbps, target))
|
||||
d.rightAt(colRxPPSEnd, y, commas(roundPPS(v.rxPPS)), uiFg)
|
||||
y += lineH
|
||||
}
|
||||
|
||||
y += gap
|
||||
y = d.section(y, "OVERALL", cable)
|
||||
d.rightAt(colFramesEnd, y, "frames", uiDim)
|
||||
d.rightAt(colDataEnd, y, "data", uiDim)
|
||||
d.rightAt(colLostEnd, y, "lost", uiDim)
|
||||
d.rightAt(colLateEnd, y, "late", uiDim)
|
||||
d.rightAt(colCRCEnd, y, "crc", uiDim)
|
||||
d.rightAt(colKdropEnd, y, "kdrop", uiDim)
|
||||
y += lineH
|
||||
for i, dir := range dirs {
|
||||
d.dirTag(dir, y)
|
||||
v := views[i]
|
||||
d.rightAt(colFramesEnd, y, commas(v.txFrames), uiFg)
|
||||
d.rightAt(colDataEnd, y, humanBytes(v.txSent), uiFg)
|
||||
d.rightAt(colLostEnd, y, commas(v.lost), errColor(v.lost))
|
||||
d.rightAt(colLateEnd, y, commas(v.late), errColor(v.late))
|
||||
d.rightAt(colCRCEnd, y, commas(v.crc), errColor(v.crc))
|
||||
d.rightAt(colKdropEnd, y, commas(v.kdrop), errColor(v.kdrop))
|
||||
y += lineH
|
||||
}
|
||||
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()
|
||||
}
|
||||
|
||||
func (d *display) section(y int, title, right string) int {
|
||||
d.small.draw(d.fb, uiMargin, y, title, uiFg)
|
||||
if right != "" {
|
||||
d.right(d.small, d.cellX(colKdropEnd), y, right, uiCyan)
|
||||
}
|
||||
ruleY := y + d.small.cellH + 3
|
||||
d.fb.rect(uiMargin, ruleY, d.fb.w-2*uiMargin, 1, uiRule)
|
||||
return ruleY + 7
|
||||
}
|
||||
|
||||
func (d *display) dirTag(dir *direction, y int) {
|
||||
d.atBold(cellDirA, y, dir.tx.tag, uiCyan)
|
||||
d.arrow(d.cellX(cellArrow), y+d.grid.cellH/2, uiDim)
|
||||
d.atBold(cellDirB, y, dir.rx.tag, uiCyan)
|
||||
}
|
||||
|
||||
// Drawn because the font has no arrow glyph. Occupies exactly one cell so the
|
||||
// grid is unaffected.
|
||||
func (d *display) arrow(x, y int, c rgb) {
|
||||
w := d.grid.cellW
|
||||
d.fb.rect(x, y-1, w-5, 2, c)
|
||||
for i := 0; i < 6; i++ {
|
||||
d.fb.rect(x+w-6+i, y-5+i, 1, 11-2*i, c)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user