Turn the display portrait and split it into live and since-reset panels

This commit is contained in:
flamingcow
2026-07-26 10:27:41 -07:00
parent 4d102f377e
commit 102d53fb9b
4 changed files with 276 additions and 221 deletions
+36 -21
View File
@@ -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)
+142 -48
View File
@@ -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
-9
View File
@@ -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),
+98 -143
View File
@@ -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)
}
}