From 102d53fb9b72826581dbd33d30bfc2355276ab15 Mon Sep 17 00:00:00 2001 From: flamingcow Date: Sun, 26 Jul 2026 10:27:41 -0700 Subject: [PATCH] Turn the display portrait and split it into live and since-reset panels --- fb.go | 57 ++++++++----- main.go | 190 ++++++++++++++++++++++++++++++++----------- probe.go | 9 --- ui.go | 241 ++++++++++++++++++++++--------------------------------- 4 files changed, 276 insertions(+), 221 deletions(-) diff --git a/fb.go b/fb.go index 0879e8c..ef353a6 100644 --- a/fb.go +++ b/fb.go @@ -31,6 +31,9 @@ const ( viScreenInfoLen = 40 ) +// w and h are the logical canvas, which is portrait; pw and ph are the panel, +// which is landscape. Every draw is turned a quarter turn on its way to memory, +// so logical top lands on the panel's right edge. type framebuffer struct { file *os.File tty *os.File @@ -38,12 +41,22 @@ type framebuffer struct { back []byte w int h int + pw int + ph int stride int rShift uint gShift uint bShift uint } +func (fb *framebuffer) offset(x, y int) int { + return x*fb.stride + (fb.pw-1-y)*4 +} + +func (fb *framebuffer) fromPanel(x, y int) (int, int) { + return y, fb.pw - 1 - x +} + // The kernel console draws into the same framebuffer, including a blinking // cursor and any keyboard echo, which fights every frame we write. Putting the // active console into graphics mode stops it touching the framebuffer at all. @@ -88,21 +101,23 @@ func openFramebuffer(path string) (*framebuffer, error) { fb := &framebuffer{ file: f, - w: int(vi[viXres]), - h: int(vi[viYres]), + pw: int(vi[viXres]), + ph: int(vi[viYres]), + w: int(vi[viYres]), + h: int(vi[viXres]), stride: int(stride), rShift: uint(vi[viRedOffset]), gShift: uint(vi[viGreenOffset]), bShift: uint(vi[viBlueOffset]), } - fb.mem, err = unix.Mmap(int(f.Fd()), 0, fb.stride*fb.h, + fb.mem, err = unix.Mmap(int(f.Fd()), 0, fb.stride*fb.ph, unix.PROT_READ|unix.PROT_WRITE, unix.MAP_SHARED) if err != nil { f.Close() return nil, fmt.Errorf("mmap: %w", err) } - fb.back = make([]byte, fb.stride*fb.h) + fb.back = make([]byte, fb.stride*fb.ph) fb.tty, err = takeConsole() if err != nil { @@ -139,28 +154,28 @@ func (fb *framebuffer) fill(c rgb) { row[x+2] = byte(v >> 16) row[x+3] = byte(v >> 24) } - for y := 0; y < fb.h; y++ { + for y := 0; y < fb.ph; y++ { copy(fb.back[y*fb.stride:], row) } } +// One logical column is contiguous after the turn, so it fills a span at a time. func (fb *framebuffer) rect(x0, y0, w, h int, c rgb) { + x1, y1 := min(x0+w, fb.w), min(y0+h, fb.h) + x0, y0 = max(x0, 0), max(y0, 0) + if x0 >= x1 || y0 >= y1 { + return + } v := fb.pixel(c) - for y := y0; y < y0+h; y++ { - if y < 0 || y >= fb.h { - continue - } - base := y * fb.stride - for x := x0; x < x0+w; x++ { - if x < 0 || x >= fb.w { - continue - } - o := base + x*4 - fb.back[o+0] = byte(v) - fb.back[o+1] = byte(v >> 8) - fb.back[o+2] = byte(v >> 16) - fb.back[o+3] = byte(v >> 24) - } + span := make([]byte, (y1-y0)*4) + for i := 0; i+4 <= len(span); i += 4 { + span[i+0] = byte(v) + span[i+1] = byte(v >> 8) + span[i+2] = byte(v >> 16) + span[i+3] = byte(v >> 24) + } + for x := x0; x < x1; x++ { + copy(fb.back[fb.offset(x, y1-1):], span) } } @@ -169,7 +184,7 @@ func (fb *framebuffer) blend(x, y int, c rgb, cov uint8) { if x < 0 || y < 0 || x >= fb.w || y >= fb.h || cov == 0 { return } - o := y*fb.stride + x*4 + o := fb.offset(x, y) if cov == 255 { v := fb.pixel(c) fb.back[o+0] = byte(v) diff --git a/main.go b/main.go index 4c2691f..8f21357 100644 --- a/main.go +++ b/main.go @@ -58,8 +58,14 @@ type direction struct { drops uint64 errBase sample dropBase uint64 + nicRaw uint64 nicNow uint64 nicBase uint64 + + recent errs + recentBase sample + recentDrops uint64 + recentNic uint64 } // Smoothing has to be steady against high-frequency noise yet still chase a @@ -84,7 +90,13 @@ type rateEstimator struct { n int } -func (e *rateEstimator) update(x float64) { +// While the rate window is still growing it already averages everything there +// is, so smoothing it again would only average in the startup ramp twice. +func (e *rateEstimator) update(x float64, windowFull bool) { + if !windowFull { + e.est, e.shown, e.mad, e.n = x, x, 0, 0 + return + } if e.n == 0 { e.est, e.shown, e.n = x, x, 1 return @@ -147,6 +159,27 @@ type rateSample struct { txFrames, txBytes, rxFrames, rxBytes uint64 } +// Late is counted but left out of the total, since a reordered frame arrived. +type errs struct { + lost, late uint64 + crc, badMagic uint64 + badLen uint64 + kdrop, link uint64 +} + +func (e errs) total() uint64 { + return e.lost + e.crc + e.badMagic + e.badLen + e.kdrop + e.link +} + +func (e errs) add(o errs) errs { + return errs{ + lost: e.lost + o.lost, late: e.late + o.late, + crc: e.crc + o.crc, badMagic: e.badMagic + o.badMagic, + badLen: e.badLen + o.badLen, + kdrop: e.kdrop + o.kdrop, link: e.link + o.link, + } +} + // A sliding window: the counters are sampled every frame and the rate is taken // across the whole window, so the figure moves every frame while still being // measured over a long enough span to be steady. @@ -311,40 +344,51 @@ var intervalCols = []colSpec{ // One interval's numbers, shared by the console table and the framebuffer so // both always show the same figures. type view struct { - txPPS, rxPPS float64 - txGbps, rxGbps float64 - txFrames, txSent uint64 - rxFrames, rxGot uint64 - lost, late uint64 - crc, badMagic uint64 - kdrop, link uint64 - errors uint64 - cable cableView + txPPS, rxPPS float64 + txGbps, rxGbps float64 + rxFrames, rxGot uint64 + since errs + window errs + cable cableView } // Cumulative fields, which need no rate window and are identical for both the // console and the display. func (d *direction) counters(now sample) view { b := d.errBase - v := view{ - txFrames: now.txFrames - b.txFrames, - txSent: now.txBytes - b.txBytes, + return view{ rxFrames: now.rxFrames - b.rxFrames, rxGot: now.rxBytes - b.rxBytes, - lost: now.lost - b.lost, - late: now.late - b.late, - crc: now.crcErr - b.crcErr, - badMagic: now.badMagic - b.badMagic, - kdrop: d.drops - d.dropBase, cable: d.cable.view(), + since: errs{ + lost: now.lost - b.lost, + late: now.late - b.late, + crc: now.crcErr - b.crcErr, + badMagic: now.badMagic - b.badMagic, + badLen: now.badLen - b.badLen, + kdrop: d.drops - d.dropBase, + // A frame the stack refused and a frame the driver dropped are the same + // failure seen from either side of the ring, and never the same frame + // twice: a send that fails never reaches the driver to be dropped. + link: d.nicNow - d.nicBase + + (now.txErrs - b.txErrs) + (now.rxErrs - b.rxErrs), + }, } - // A frame the stack refused and a frame the driver dropped are the same - // failure seen from either side of the ring, and never the same frame twice: - // a send that fails never reaches the driver to be dropped. - v.link = d.nicNow - d.nicBase + - (now.txErrs - b.txErrs) + (now.rxErrs - b.rxErrs) - v.errors = v.lost + v.crc + v.badMagic + (now.badLen - b.badLen) + v.kdrop + v.link - return v +} + +func totalView(views []view) view { + var t view + for _, v := range views { + t.txPPS += v.txPPS + t.rxPPS += v.rxPPS + t.txGbps += v.txGbps + t.rxGbps += v.rxGbps + t.rxFrames += v.rxFrames + t.rxGot += v.rxGot + t.since = t.since.add(v.since) + t.window = t.window.add(v.window) + } + return t } func (d *direction) view(prev *sample, secs float64) view { @@ -369,8 +413,9 @@ func (d *direction) displayView(t time.Time) view { d.win.push(rateSample{t, now.txFrames, now.txBytes, now.rxFrames, now.rxBytes}) v := d.counters(now) - v.txFrames = d.heldFrames.get(t, v.txFrames) - v.txSent = d.heldSent.get(t, v.txSent) + v.window = d.recent + v.rxFrames = d.heldFrames.get(t, v.rxFrames) + v.rxGot = d.heldSent.get(t, v.rxGot) o, n, ok := d.win.span() if !ok { @@ -382,10 +427,11 @@ func (d *direction) displayView(t time.Time) view { } txF := n.txFrames - o.txFrames rxF := n.rxFrames - o.rxFrames - d.est.txPPS.update(float64(txF) / secs) - d.est.rxPPS.update(float64(rxF) / secs) - d.est.txGbps.update(gbps(n.txBytes-o.txBytes, txF, secs)) - d.est.rxGbps.update(gbps(n.rxBytes-o.rxBytes, rxF, secs)) + full := d.win.filled + d.est.txPPS.update(float64(txF)/secs, full) + d.est.rxPPS.update(float64(rxF)/secs, full) + d.est.txGbps.update(gbps(n.txBytes-o.txBytes, txF, secs), full) + d.est.rxGbps.update(gbps(n.rxBytes-o.rxBytes, rxF, secs), full) v.txPPS = d.est.txPPS.value() v.rxPPS = d.est.rxPPS.value() @@ -402,24 +448,64 @@ func (d *direction) row(elapsed time.Duration, v view, target float64, length st rateCell(v.txGbps, target), commas(uint64(v.rxPPS)), rateCell(v.rxGbps, target), - statusCell(v.lost), - statusCell(v.late), - statusCell(v.crc), - statusCell(v.badMagic), - statusCell(v.kdrop), - statusCell(v.link), - statusCell(v.errors), + statusCell(v.since.lost), + statusCell(v.since.late), + statusCell(v.since.crc), + statusCell(v.since.badMagic), + statusCell(v.since.kdrop), + statusCell(v.since.link), + statusCell(v.since.total()), paint(v.cable.minText(), cCyan), paint(length, cCyan), } } -// Sampled once a second, since these are sysfs reads; the display uses whatever -// the last sample left behind. +// Sampled once a second, since these are sysfs reads. The recent errors roll +// here rather than over the rate window because the nic counters only move at +// this rate, and a shorter span would alias them into a flicker. func (d *direction) sampleNIC() { + d.accumulateNIC() + + now := d.snapshot() + b := d.recentBase + d.recent = errs{ + lost: now.lost - b.lost, + late: now.late - b.late, + crc: now.crcErr - b.crcErr, + badMagic: now.badMagic - b.badMagic, + badLen: now.badLen - b.badLen, + kdrop: d.drops - d.recentDrops, + link: d.nicNow - d.recentNic + + (now.txErrs - b.txErrs) + (now.rxErrs - b.rxErrs), + } + d.recentBase, d.recentDrops, d.recentNic = now, d.drops, d.nicNow +} + +func (d *direction) readNICTotal() uint64 { tx := readNIC(d.tx.name) rx := readNIC(d.rx.name) - d.nicNow = tx.tx + rx.rx + tx.carrierDown + return tx.tx + rx.rx + tx.carrierDown +} + +// Sysfs nic counters run from boot and restart from zero whenever the driver +// resets its statistics, so only their forward motion is accumulated. Taking +// raw differences instead charges a boot's worth of errors to the first sample +// and turns a reset into a near-2^64 underflow. +func (d *direction) accumulateNIC() { + raw := d.readNICTotal() + if raw > d.nicRaw { + d.nicNow += raw - d.nicRaw + } + d.nicRaw = raw +} + +// Whatever the interfaces counted before now is not ours, and no interval has +// elapsed yet, so every baseline starts here and nothing is reported until the +// first one completes. +func (d *direction) primeCounters() { + d.nicRaw = d.readNICTotal() + d.reset() + d.recentBase, d.recentDrops, d.recentNic = d.snapshot(), d.drops, d.nicNow } func buildDirection(label string, tx, rx endpoint, sizes []int, cfg config) (*direction, error) { @@ -471,9 +557,6 @@ func buildDirection(label string, tx, rx endpoint, sizes []int, cfg config) (*di } d.probeRxFD = fd - // Whatever the interfaces have counted before now is not ours. - d.sampleNIC() - d.nicBase = d.nicNow return d, nil } @@ -550,7 +633,7 @@ func main() { streams = flag.Int("streams", 7, "independent streams per direction, capped by rx rings; each gets its own ethertype, steered by a flow rule to its own rx queue") batch = flag.Int("batch", 64, "frames per sendmmsg/recvmmsg call") - nsPerM = flag.Float64("ns-per-m", 5.3, "mean of both directions, per metre of cable") + nsPerM = flag.Float64("ns-per-m", 5.2, "mean of both directions, per metre of cable") ) flag.Parse() @@ -703,11 +786,15 @@ func run(aName, bName, sizesArg string, } defer disp.close() - touch, err := watchTouch(disp.fb.w, disp.fb.h) + touch, err := watchTouch(disp.fb.pw, disp.fb.ph) if err != nil { return fmt.Errorf("touchscreen: %w", err) } + for _, d := range dirs { + d.primeCounters() + } + start := time.Now() close(startTx) tick := time.NewTicker(reportInterval) @@ -733,13 +820,20 @@ func run(aName, bName, sizesArg string, case <-space: start = resetAll(dirs, stats) case now := <-frame.C: - if x, y, down := touch.get(); disp.holdReset(x, y, down, now) { + px, py, down := touch.get() + x, y := disp.fb.fromPanel(px, py) + if disp.holdReset(x, y, down, now) { start = resetAll(dirs, stats) } for i, d := range dirs { views[i] = d.displayView(now) } - disp.render(dirs, views, now.Sub(start), target, cfg.cableText(views)) + cable := "-" + if m, ok := cfg.cableMetres(views); ok { + cable = fmt.Sprintf("%.1f m", m) + } + disp.render(totalView(views), now.Sub(start), + target*float64(len(dirs)), cable) case now := <-tick.C: secs := now.Sub(last).Seconds() last = now diff --git a/probe.go b/probe.go index eae459b..ab521b5 100644 --- a/probe.go +++ b/probe.go @@ -1,7 +1,6 @@ package main import ( - "fmt" "sync" "sync/atomic" "time" @@ -69,14 +68,6 @@ func (c config) cableMetres(views []view) (float64, bool) { return excess / float64(len(views)) / c.nsPerM, true } -func (c config) cableText(views []view) string { - m, ok := c.cableMetres(views) - if !ok { - return "" - } - return fmt.Sprintf("cable %.1f m", m) -} - func newCableStats() *cableStats { return &cableStats{ txPend: make(map[uint64]int64, probePendCap), diff --git a/ui.go b/ui.go index 6fb47e7..0537677 100644 --- a/ui.go +++ b/ui.go @@ -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) - } -}