Render the live status to the framebuffer in Atkinson Hyperlegible Mono
This commit is contained in:
@@ -0,0 +1,168 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
fbioGetVScreenInfo = 0x4600
|
||||||
|
fbioBlank = 0x4611
|
||||||
|
fbBlankUnblank = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
// The variable screen info is a flat run of 40 u32s, so it is read as an array
|
||||||
|
// rather than a struct to sidestep any question of padding.
|
||||||
|
const (
|
||||||
|
viXres = 0
|
||||||
|
viYres = 1
|
||||||
|
viBitsPerPixel = 6
|
||||||
|
viRedOffset = 8
|
||||||
|
viGreenOffset = 11
|
||||||
|
viBlueOffset = 14
|
||||||
|
viScreenInfoLen = 40
|
||||||
|
)
|
||||||
|
|
||||||
|
type framebuffer struct {
|
||||||
|
file *os.File
|
||||||
|
mem []byte
|
||||||
|
back []byte
|
||||||
|
w int
|
||||||
|
h int
|
||||||
|
stride int
|
||||||
|
rShift uint
|
||||||
|
gShift uint
|
||||||
|
bShift uint
|
||||||
|
}
|
||||||
|
|
||||||
|
func openFramebuffer(path string) (*framebuffer, error) {
|
||||||
|
f, err := os.OpenFile(path, os.O_RDWR, 0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var vi [viScreenInfoLen]uint32
|
||||||
|
if _, _, errno := unix.Syscall(unix.SYS_IOCTL, f.Fd(),
|
||||||
|
fbioGetVScreenInfo, uintptr(unsafe.Pointer(&vi[0]))); errno != 0 {
|
||||||
|
f.Close()
|
||||||
|
return nil, fmt.Errorf("get screen info: %w", errno)
|
||||||
|
}
|
||||||
|
if vi[viBitsPerPixel] != 32 {
|
||||||
|
f.Close()
|
||||||
|
return nil, fmt.Errorf("only 32bpp supported, got %d", vi[viBitsPerPixel])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wake the panel; the console blanks it after a timeout.
|
||||||
|
unix.Syscall(unix.SYS_IOCTL, f.Fd(), fbioBlank, fbBlankUnblank)
|
||||||
|
|
||||||
|
stride, ok := readUint("/sys/class/graphics/fb0/stride")
|
||||||
|
if !ok {
|
||||||
|
f.Close()
|
||||||
|
return nil, fmt.Errorf("cannot read framebuffer stride")
|
||||||
|
}
|
||||||
|
|
||||||
|
fb := &framebuffer{
|
||||||
|
file: f,
|
||||||
|
w: int(vi[viXres]),
|
||||||
|
h: int(vi[viYres]),
|
||||||
|
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,
|
||||||
|
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)
|
||||||
|
return fb, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fb *framebuffer) close() {
|
||||||
|
unix.Munmap(fb.mem)
|
||||||
|
fb.file.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fb *framebuffer) pixel(c rgb) uint32 {
|
||||||
|
return uint32(c.r)<<fb.rShift | uint32(c.g)<<fb.gShift | uint32(c.b)<<fb.bShift
|
||||||
|
}
|
||||||
|
|
||||||
|
type rgb struct {
|
||||||
|
r, g, b uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fb *framebuffer) fill(c rgb) {
|
||||||
|
v := fb.pixel(c)
|
||||||
|
row := make([]byte, fb.stride)
|
||||||
|
for x := 0; x+4 <= fb.stride; x += 4 {
|
||||||
|
row[x+0] = byte(v)
|
||||||
|
row[x+1] = byte(v >> 8)
|
||||||
|
row[x+2] = byte(v >> 16)
|
||||||
|
row[x+3] = byte(v >> 24)
|
||||||
|
}
|
||||||
|
for y := 0; y < fb.h; y++ {
|
||||||
|
copy(fb.back[y*fb.stride:], row)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fb *framebuffer) rect(x0, y0, w, h int, c rgb) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blends src over the existing pixel, with cov as 0-255 coverage.
|
||||||
|
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
|
||||||
|
if cov == 255 {
|
||||||
|
v := fb.pixel(c)
|
||||||
|
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)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
a := uint32(cov)
|
||||||
|
old := uint32(fb.back[o+0]) | uint32(fb.back[o+1])<<8 |
|
||||||
|
uint32(fb.back[o+2])<<16 | uint32(fb.back[o+3])<<24
|
||||||
|
orr := uint8(old >> fb.rShift)
|
||||||
|
og := uint8(old >> fb.gShift)
|
||||||
|
ob := uint8(old >> fb.bShift)
|
||||||
|
mix := rgb{
|
||||||
|
r: uint8((uint32(c.r)*a + uint32(orr)*(255-a)) / 255),
|
||||||
|
g: uint8((uint32(c.g)*a + uint32(og)*(255-a)) / 255),
|
||||||
|
b: uint8((uint32(c.b)*a + uint32(ob)*(255-a)) / 255),
|
||||||
|
}
|
||||||
|
v := fb.pixel(mix)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fb *framebuffer) flush() {
|
||||||
|
copy(fb.mem, fb.back)
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
@@ -2,4 +2,8 @@ module g.fc.run/theater/cabletest
|
|||||||
|
|
||||||
go 1.26.5
|
go 1.26.5
|
||||||
|
|
||||||
require golang.org/x/sys v0.47.0 // indirect
|
require (
|
||||||
|
golang.org/x/image v0.44.0 // indirect
|
||||||
|
golang.org/x/sys v0.47.0 // indirect
|
||||||
|
golang.org/x/text v0.40.0 // indirect
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,2 +1,6 @@
|
|||||||
|
golang.org/x/image v0.44.0 h1:+tDekMZED9+LrtB3G5xzRggpVh9CARjZqROla3R3R+I=
|
||||||
|
golang.org/x/image v0.44.0/go.mod h1:V8K3KE9KKKE+pLpQDOeN18w9oacNSvy1tDOirTu4xtY=
|
||||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
|
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
|
||||||
|
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
|
||||||
|
|||||||
@@ -156,38 +156,60 @@ var intervalCols = []colSpec{
|
|||||||
{title: "ERRORS", width: 11, right: true},
|
{title: "ERRORS", width: 11, right: true},
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *direction) intervalRow(elapsed time.Duration, secs, target float64) []string {
|
// 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, errors uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *direction) view(secs float64) view {
|
||||||
now := d.snapshot()
|
now := d.snapshot()
|
||||||
p := d.prev
|
p := d.prev
|
||||||
d.prev = now
|
d.prev = now
|
||||||
|
d.sampleDrops()
|
||||||
|
|
||||||
txF := now.txFrames - p.txFrames
|
txF := now.txFrames - p.txFrames
|
||||||
txB := now.txBytes - p.txBytes
|
|
||||||
rxF := now.rxFrames - p.rxFrames
|
rxF := now.rxFrames - p.rxFrames
|
||||||
rxB := now.rxBytes - p.rxBytes
|
|
||||||
|
|
||||||
d.sampleDrops()
|
|
||||||
b := d.errBase
|
b := d.errBase
|
||||||
lost := now.lost - b.lost
|
v := view{
|
||||||
late := now.late - b.late
|
txPPS: float64(txF) / secs,
|
||||||
crc := now.crcErr - b.crcErr
|
rxPPS: float64(rxF) / secs,
|
||||||
badMagic := now.badMagic - b.badMagic
|
txGbps: gbps(now.txBytes-p.txBytes, txF, secs),
|
||||||
badLen := now.badLen - b.badLen
|
rxGbps: gbps(now.rxBytes-p.rxBytes, rxF, secs),
|
||||||
drops := d.drops - d.dropBase
|
txFrames: now.txFrames,
|
||||||
|
txSent: now.txBytes,
|
||||||
|
rxFrames: now.rxFrames,
|
||||||
|
rxGot: now.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,
|
||||||
|
}
|
||||||
|
v.errors = v.lost + v.crc + v.badMagic + (now.badLen - b.badLen) + v.kdrop
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *direction) row(elapsed time.Duration, v view, target float64) []string {
|
||||||
return []string{
|
return []string{
|
||||||
uptime(elapsed),
|
uptime(elapsed),
|
||||||
paint(d.short, cCyan),
|
paint(d.short, cCyan),
|
||||||
commas(uint64(float64(txF) / secs)),
|
commas(uint64(v.txPPS)),
|
||||||
rateCell(gbps(txB, txF, secs), target),
|
rateCell(v.txGbps, target),
|
||||||
commas(uint64(float64(rxF) / secs)),
|
commas(uint64(v.rxPPS)),
|
||||||
rateCell(gbps(rxB, rxF, secs), target),
|
rateCell(v.rxGbps, target),
|
||||||
statusCell(lost),
|
statusCell(v.lost),
|
||||||
statusCell(late),
|
statusCell(v.late),
|
||||||
statusCell(crc),
|
statusCell(v.crc),
|
||||||
statusCell(badMagic),
|
statusCell(v.badMagic),
|
||||||
statusCell(drops),
|
statusCell(v.kdrop),
|
||||||
statusCell(lost + crc + badMagic + badLen + drops),
|
statusCell(v.errors),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -442,6 +464,14 @@ func run(aName, bName, sizesArg, patArg string,
|
|||||||
space, restoreTerm := watchSpace()
|
space, restoreTerm := watchSpace()
|
||||||
defer restoreTerm()
|
defer restoreTerm()
|
||||||
|
|
||||||
|
disp, err := newDisplay()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("display: %w", err)
|
||||||
|
}
|
||||||
|
defer disp.close()
|
||||||
|
disp.config = fmt.Sprintf("%s sizes %s %d streams batch %d crc32c verify",
|
||||||
|
patterns[patIdx].name, strings.Join(sizeStrs, ","), nStreams, batch)
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
close(startTx)
|
close(startTx)
|
||||||
tick := time.NewTicker(reportInterval)
|
tick := time.NewTicker(reportInterval)
|
||||||
@@ -466,8 +496,10 @@ func run(aName, bName, sizesArg, patArg string,
|
|||||||
secs := now.Sub(last).Seconds()
|
secs := now.Sub(last).Seconds()
|
||||||
last = now
|
last = now
|
||||||
elapsed := now.Sub(start)
|
elapsed := now.Sub(start)
|
||||||
for _, d := range dirs {
|
views := make([]view, len(dirs))
|
||||||
for _, line := range stats.emit(d.intervalRow(elapsed, secs, target)) {
|
for i, d := range dirs {
|
||||||
|
views[i] = d.view(secs)
|
||||||
|
for _, line := range stats.emit(d.row(elapsed, views[i], target)) {
|
||||||
fmt.Println(line)
|
fmt.Println(line)
|
||||||
}
|
}
|
||||||
for _, line := range d.reportNIC() {
|
for _, line := range d.reportNIC() {
|
||||||
@@ -483,6 +515,7 @@ func run(aName, bName, sizesArg, patArg string,
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
disp.render(dirs, views, elapsed, target)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"image/draw"
|
||||||
|
|
||||||
|
"golang.org/x/image/font"
|
||||||
|
"golang.org/x/image/font/opentype"
|
||||||
|
"golang.org/x/image/math/fixed"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed font/AtkinsonHyperlegibleMono-Regular.ttf font/AtkinsonHyperlegibleMono-Bold.ttf
|
||||||
|
var fontFS embed.FS
|
||||||
|
|
||||||
|
type glyph struct {
|
||||||
|
// Coverage values, cellW*cellH, origin at the cell's top left.
|
||||||
|
cov []uint8
|
||||||
|
w, h int
|
||||||
|
}
|
||||||
|
|
||||||
|
type textFace struct {
|
||||||
|
face font.Face
|
||||||
|
cellW int
|
||||||
|
cellH int
|
||||||
|
ascent int
|
||||||
|
cache map[rune]*glyph
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadFace(bold bool, sizePx float64) (*textFace, error) {
|
||||||
|
name := "font/AtkinsonHyperlegibleMono-Regular.ttf"
|
||||||
|
if bold {
|
||||||
|
name = "font/AtkinsonHyperlegibleMono-Bold.ttf"
|
||||||
|
}
|
||||||
|
raw, err := fontFS.ReadFile(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
parsed, err := opentype.Parse(raw)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
face, err := opentype.NewFace(parsed, &opentype.FaceOptions{
|
||||||
|
Size: sizePx,
|
||||||
|
DPI: 72,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
m := face.Metrics()
|
||||||
|
adv, ok := face.GlyphAdvance('0')
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("font has no digit glyphs")
|
||||||
|
}
|
||||||
|
return &textFace{
|
||||||
|
face: face,
|
||||||
|
cellW: adv.Ceil(),
|
||||||
|
cellH: (m.Ascent + m.Descent).Ceil(),
|
||||||
|
ascent: m.Ascent.Ceil(),
|
||||||
|
cache: make(map[rune]*glyph),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rasterises once per rune and keeps the coverage mask, since the same few
|
||||||
|
// dozen runes are redrawn every frame.
|
||||||
|
func (t *textFace) glyph(r rune) *glyph {
|
||||||
|
if g, ok := t.cache[r]; ok {
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
g := &glyph{w: t.cellW, h: t.cellH, cov: make([]uint8, t.cellW*t.cellH)}
|
||||||
|
|
||||||
|
dr, mask, maskp, _, ok := t.face.Glyph(
|
||||||
|
fixed.P(0, t.ascent), r)
|
||||||
|
if ok {
|
||||||
|
alpha := image.NewAlpha(image.Rect(0, 0, t.cellW, t.cellH))
|
||||||
|
draw.DrawMask(alpha, dr, image.NewUniform(image.White.C), image.Point{},
|
||||||
|
mask, maskp, draw.Src)
|
||||||
|
for y := 0; y < t.cellH; y++ {
|
||||||
|
for x := 0; x < t.cellW; x++ {
|
||||||
|
g.cov[y*t.cellW+x] = alpha.AlphaAt(x, y).A
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.cache[r] = g
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *textFace) draw(fb *framebuffer, x, y int, s string, c rgb) int {
|
||||||
|
for _, r := range s {
|
||||||
|
if r == ' ' {
|
||||||
|
x += t.cellW
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
g := t.glyph(r)
|
||||||
|
for gy := 0; gy < g.h; gy++ {
|
||||||
|
row := gy * g.w
|
||||||
|
for gx := 0; gx < g.w; gx++ {
|
||||||
|
if cov := g.cov[row+gx]; cov != 0 {
|
||||||
|
fb.blend(x+gx, y+gy, c, cov)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
x += t.cellW
|
||||||
|
}
|
||||||
|
return x
|
||||||
|
}
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
uiBg = rgb{0x12, 0x14, 0x18}
|
||||||
|
uiPanel = rgb{0x1c, 0x20, 0x26}
|
||||||
|
uiFg = rgb{0xe6, 0xe8, 0xea}
|
||||||
|
uiDim = rgb{0x7a, 0x82, 0x8c}
|
||||||
|
uiCyan = rgb{0x5c, 0xc8, 0xe0}
|
||||||
|
uiGreen = rgb{0x4c, 0xc2, 0x6a}
|
||||||
|
uiRed = rgb{0xe0, 0x4b, 0x4b}
|
||||||
|
uiYellow = rgb{0xe0, 0xb0, 0x40}
|
||||||
|
)
|
||||||
|
|
||||||
|
type display struct {
|
||||||
|
fb *framebuffer
|
||||||
|
huge *textFace
|
||||||
|
big *textFace
|
||||||
|
small *textFace
|
||||||
|
config string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDisplay() (*display, error) {
|
||||||
|
fb, err := openFramebuffer("/dev/fb0")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
huge, err := loadFace(true, 46)
|
||||||
|
if err != nil {
|
||||||
|
fb.close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
big, err := loadFace(true, 24)
|
||||||
|
if err != nil {
|
||||||
|
fb.close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
small, err := loadFace(false, 18)
|
||||||
|
if err != nil {
|
||||||
|
fb.close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &display{fb: fb, huge: huge, big: big, small: small}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *display) close() {
|
||||||
|
d.fb.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *display) right(f *textFace, xEnd, y int, s string, c rgb) {
|
||||||
|
f.draw(d.fb, xEnd-len([]rune(s))*f.cellW, y, s, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *display) center(f *textFace, x0, w, y int, s string, c rgb) {
|
||||||
|
f.draw(d.fb, x0+(w-len([]rune(s))*f.cellW)/2, y, s, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func errColor(n uint64) rgb {
|
||||||
|
if n == 0 {
|
||||||
|
return uiGreen
|
||||||
|
}
|
||||||
|
return uiRed
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *display) 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(dirs []*direction, views []view, elapsed time.Duration, target float64) {
|
||||||
|
fb := d.fb
|
||||||
|
fb.fill(uiBg)
|
||||||
|
|
||||||
|
d.small.draw(fb, 16, 12, "cabletest", uiCyan)
|
||||||
|
if len(dirs) > 0 {
|
||||||
|
d.small.draw(fb, 16+11*d.small.cellW, 12,
|
||||||
|
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-16, 12, uptime(elapsed), uiDim)
|
||||||
|
|
||||||
|
var total uint64
|
||||||
|
for _, v := range views {
|
||||||
|
total += v.errors
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status band, readable from across the room.
|
||||||
|
bandY, bandH := 44, 74
|
||||||
|
fb.rect(0, bandY, fb.w, bandH, uiPanel)
|
||||||
|
fb.rect(0, bandY, 6, bandH, errColor(total))
|
||||||
|
word, wc := "NO ERRORS", uiGreen
|
||||||
|
if total > 0 {
|
||||||
|
word, wc = fmt.Sprintf("%s ERRORS", commas(total)), uiRed
|
||||||
|
}
|
||||||
|
d.center(d.huge, 0, fb.w, bandY+(bandH-d.huge.cellH)/2+2, word, wc)
|
||||||
|
|
||||||
|
panelY := bandY + bandH + 12
|
||||||
|
footerH := d.small.cellH + 16
|
||||||
|
panelH := (fb.h - panelY - footerH) / max(len(dirs), 1)
|
||||||
|
for i, dir := range dirs {
|
||||||
|
d.renderDirection(dir, views[i], 12, panelY+i*panelH, fb.w-24, panelH-10, target)
|
||||||
|
}
|
||||||
|
d.footer(fb.h - footerH + 4)
|
||||||
|
fb.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *display) renderDirection(dir *direction, v view, x, y, w, h int, target float64) {
|
||||||
|
fb := d.fb
|
||||||
|
fb.rect(x, y, w, h, uiPanel)
|
||||||
|
|
||||||
|
// The font has no arrow glyph, so the direction marker is drawn.
|
||||||
|
d.big.draw(fb, x+14, y+12, dir.tx.tag, uiCyan)
|
||||||
|
d.arrow(x+14+2*d.big.cellW, y+12+d.big.cellH/2, 22, uiDim)
|
||||||
|
d.big.draw(fb, x+14+2*d.big.cellW+30, y+12, dir.rx.tag, uiCyan)
|
||||||
|
|
||||||
|
col := x + 110
|
||||||
|
d.rateBlock(col, y+10, "TX", v.txPPS, v.txGbps, target)
|
||||||
|
d.rateBlock(col+300, y+10, "RX", v.rxPPS, v.rxGbps, target)
|
||||||
|
|
||||||
|
ex := col + 620
|
||||||
|
d.errLine(ex, y+10, "lost", v.lost)
|
||||||
|
d.errLine(ex, y+10+d.small.cellH+2, "crc", v.crc)
|
||||||
|
d.errLine(ex, y+10+2*(d.small.cellH+2), "kdrop", v.kdrop)
|
||||||
|
d.errLine(ex, y+10+3*(d.small.cellH+2), "late", v.late)
|
||||||
|
|
||||||
|
totals := fmt.Sprintf("sent %s frames %s received %s frames %s",
|
||||||
|
commas(v.txFrames), humanBytes(v.txSent),
|
||||||
|
commas(v.rxFrames), humanBytes(v.rxGot))
|
||||||
|
d.small.draw(fb, x+14, y+h-d.small.cellH-8, totals, uiDim)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *display) arrow(x, y, w int, c rgb) {
|
||||||
|
d.fb.rect(x, y-1, w-6, 3, c)
|
||||||
|
for i := 0; i < 7; i++ {
|
||||||
|
d.fb.rect(x+w-7+i, y-6+i, 1, 13-2*i, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *display) rateBlock(x, y int, label string, pps, gb, target float64) {
|
||||||
|
d.small.draw(d.fb, x, y, label, uiDim)
|
||||||
|
d.big.draw(d.fb, x+3*d.small.cellW, y-2,
|
||||||
|
fmt.Sprintf("%6.2f Gb/s", gb), d.rateColor(gb, target))
|
||||||
|
d.small.draw(d.fb, x+3*d.small.cellW, y+d.big.cellH,
|
||||||
|
fmt.Sprintf("%s pps", commas(uint64(pps))), uiFg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *display) footer(y int) {
|
||||||
|
d.small.draw(d.fb, 16, y, d.config, uiDim)
|
||||||
|
d.right(d.small, d.fb.w-16, y, "space resets counts", uiDim)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *display) errLine(x, y int, label string, n uint64) {
|
||||||
|
d.small.draw(d.fb, x, y, label, uiDim)
|
||||||
|
d.small.draw(d.fb, x+7*d.small.cellW, y, commas(n), errColor(n))
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user