Reset everything from a held on-screen button or space
This commit is contained in:
@@ -1,11 +1,158 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
evSyn = 0x00
|
||||||
|
evKey = 0x01
|
||||||
|
evAbs = 0x03
|
||||||
|
synReport = 0x00
|
||||||
|
btnTouch = 0x14a
|
||||||
|
absX = 0x00
|
||||||
|
absY = 0x01
|
||||||
|
|
||||||
|
inputPropDirect = 0x01
|
||||||
|
sizeofInputEvent = 24
|
||||||
|
)
|
||||||
|
|
||||||
|
func evIoc(nr, size uintptr) uintptr {
|
||||||
|
const iocRead = 2
|
||||||
|
return iocRead<<30 | size<<16 | 'E'<<8 | nr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Current finger position and whether it is down. Held as state rather than
|
||||||
|
// delivered as events, so a dropped release can never leave a hold armed.
|
||||||
|
type touchState struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
x, y int
|
||||||
|
down bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *touchState) set(x, y int, down bool) {
|
||||||
|
t.mu.Lock()
|
||||||
|
t.x, t.y, t.down = x, y, down
|
||||||
|
t.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *touchState) get() (x, y int, down bool) {
|
||||||
|
t.mu.Lock()
|
||||||
|
defer t.mu.Unlock()
|
||||||
|
return t.x, t.y, t.down
|
||||||
|
}
|
||||||
|
|
||||||
|
// Picks the direct-input device that reports absolute X and Y, which is the
|
||||||
|
// touchscreen; a mouse is relative and a keyboard has no absolute axes.
|
||||||
|
func findTouchscreen() (*os.File, error) {
|
||||||
|
paths, err := filepath.Glob("/dev/input/event*")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, p := range paths {
|
||||||
|
f, err := os.OpenFile(p, os.O_RDONLY, 0)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var props, absBits uint64
|
||||||
|
if _, _, errno := unix.Syscall(unix.SYS_IOCTL, f.Fd(),
|
||||||
|
evIoc(0x09, 8), uintptr(unsafe.Pointer(&props))); errno != 0 {
|
||||||
|
f.Close()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, _, errno := unix.Syscall(unix.SYS_IOCTL, f.Fd(),
|
||||||
|
evIoc(0x20+evAbs, 8), uintptr(unsafe.Pointer(&absBits))); errno != 0 {
|
||||||
|
f.Close()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if props&(1<<inputPropDirect) != 0 &&
|
||||||
|
absBits&(1<<absX) != 0 && absBits&(1<<absY) != 0 {
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
|
f.Close()
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("no touchscreen found among /dev/input/event*")
|
||||||
|
}
|
||||||
|
|
||||||
|
func axisRange(f *os.File, axis uintptr) (min, max int32, err error) {
|
||||||
|
var info [6]int32
|
||||||
|
if _, _, errno := unix.Syscall(unix.SYS_IOCTL, f.Fd(),
|
||||||
|
evIoc(0x40+axis, 24), uintptr(unsafe.Pointer(&info[0]))); errno != 0 {
|
||||||
|
return 0, 0, errno
|
||||||
|
}
|
||||||
|
return info[1], info[2], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tracks the finger position, scaled to the given screen size.
|
||||||
|
func watchTouch(w, h int) (*touchState, error) {
|
||||||
|
f, err := findTouchscreen()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
minX, maxX, err := axisRange(f, absX)
|
||||||
|
if err != nil {
|
||||||
|
f.Close()
|
||||||
|
return nil, fmt.Errorf("x range: %w", err)
|
||||||
|
}
|
||||||
|
minY, maxY, err := axisRange(f, absY)
|
||||||
|
if err != nil {
|
||||||
|
f.Close()
|
||||||
|
return nil, fmt.Errorf("y range: %w", err)
|
||||||
|
}
|
||||||
|
if maxX <= minX || maxY <= minY {
|
||||||
|
f.Close()
|
||||||
|
return nil, fmt.Errorf("touchscreen axes are degenerate: x %d..%d y %d..%d",
|
||||||
|
minX, maxX, minY, maxY)
|
||||||
|
}
|
||||||
|
|
||||||
|
state := &touchState{}
|
||||||
|
go func() {
|
||||||
|
defer f.Close()
|
||||||
|
buf := make([]byte, sizeofInputEvent*32)
|
||||||
|
var rawX, rawY int32
|
||||||
|
var down bool
|
||||||
|
for {
|
||||||
|
n, err := f.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for o := 0; o+sizeofInputEvent <= n; o += sizeofInputEvent {
|
||||||
|
typ := *(*uint16)(unsafe.Pointer(&buf[o+16]))
|
||||||
|
code := *(*uint16)(unsafe.Pointer(&buf[o+18]))
|
||||||
|
val := *(*int32)(unsafe.Pointer(&buf[o+20]))
|
||||||
|
switch typ {
|
||||||
|
case evAbs:
|
||||||
|
switch code {
|
||||||
|
case absX:
|
||||||
|
rawX = val
|
||||||
|
case absY:
|
||||||
|
rawY = val
|
||||||
|
}
|
||||||
|
case evKey:
|
||||||
|
if code == btnTouch {
|
||||||
|
down = val != 0
|
||||||
|
}
|
||||||
|
case evSyn:
|
||||||
|
if code != synReport {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
state.set(
|
||||||
|
int(int64(rawX-minX)*int64(w)/int64(maxX-minX)),
|
||||||
|
int(int64(rawY-minY)*int64(h)/int64(maxY-minY)),
|
||||||
|
down)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return state, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Delivers a value every time space is pressed. Puts the terminal in
|
// Delivers a value every time space is pressed. Puts the terminal in
|
||||||
// non-canonical mode so the keypress arrives without waiting for a newline,
|
// non-canonical mode so the keypress arrives without waiting for a newline,
|
||||||
// and returns a function that restores the original settings.
|
// and returns a function that restores the original settings.
|
||||||
|
|||||||
@@ -251,11 +251,25 @@ func (d *direction) snapshot() sample {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Counters keep climbing in the workers, so resetting just moves the origin
|
// Counters keep climbing in the workers, so resetting just moves the origin
|
||||||
// the display subtracts from.
|
// everything is measured from. Rates are deliberately left running, since they
|
||||||
func (d *direction) resetErrors() {
|
// are instantaneous and would only blink to zero and back.
|
||||||
|
func (d *direction) reset() {
|
||||||
d.sampleDrops()
|
d.sampleDrops()
|
||||||
d.errBase = d.snapshot()
|
d.errBase = d.snapshot()
|
||||||
d.dropBase = d.drops
|
d.dropBase = d.drops
|
||||||
|
d.heldFrames = heldValue{}
|
||||||
|
d.heldSent = heldValue{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the new start time, so the uptime shown alongside the totals counts
|
||||||
|
// from the reset rather than from launch.
|
||||||
|
func resetAll(dirs []*direction, stats *streamTable) time.Time {
|
||||||
|
for _, d := range dirs {
|
||||||
|
d.reset()
|
||||||
|
}
|
||||||
|
stats.sinceHeader = 0
|
||||||
|
fmt.Println(stats.rule("counters reset"))
|
||||||
|
return time.Now()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *direction) sampleDrops() {
|
func (d *direction) sampleDrops() {
|
||||||
@@ -300,10 +314,10 @@ type view struct {
|
|||||||
func (d *direction) counters(now sample) view {
|
func (d *direction) counters(now sample) view {
|
||||||
b := d.errBase
|
b := d.errBase
|
||||||
v := view{
|
v := view{
|
||||||
txFrames: now.txFrames,
|
txFrames: now.txFrames - b.txFrames,
|
||||||
txSent: now.txBytes,
|
txSent: now.txBytes - b.txBytes,
|
||||||
rxFrames: now.rxFrames,
|
rxFrames: now.rxFrames - b.rxFrames,
|
||||||
rxGot: now.rxBytes,
|
rxGot: now.rxBytes - b.rxBytes,
|
||||||
lost: now.lost - b.lost,
|
lost: now.lost - b.lost,
|
||||||
late: now.late - b.late,
|
late: now.late - b.late,
|
||||||
crc: now.crcErr - b.crcErr,
|
crc: now.crcErr - b.crcErr,
|
||||||
@@ -644,6 +658,11 @@ func run(aName, bName, sizesArg, patArg string,
|
|||||||
}
|
}
|
||||||
defer disp.close()
|
defer disp.close()
|
||||||
|
|
||||||
|
touch, err := watchTouch(disp.fb.w, disp.fb.h)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("touchscreen: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
close(startTx)
|
close(startTx)
|
||||||
tick := time.NewTicker(reportInterval)
|
tick := time.NewTicker(reportInterval)
|
||||||
@@ -666,12 +685,11 @@ func run(aName, bName, sizesArg, patArg string,
|
|||||||
wg.Wait()
|
wg.Wait()
|
||||||
return nil
|
return nil
|
||||||
case <-space:
|
case <-space:
|
||||||
for _, d := range dirs {
|
start = resetAll(dirs, stats)
|
||||||
d.resetErrors()
|
|
||||||
}
|
|
||||||
stats.sinceHeader = 0
|
|
||||||
fmt.Println(stats.rule("error counts reset"))
|
|
||||||
case now := <-frame.C:
|
case now := <-frame.C:
|
||||||
|
if x, y, down := touch.get(); disp.holdReset(x, y, down, now) {
|
||||||
|
start = resetAll(dirs, stats)
|
||||||
|
}
|
||||||
for i, d := range dirs {
|
for i, d := range dirs {
|
||||||
views[i] = d.displayView(now)
|
views[i] = d.displayView(now)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -9,6 +10,7 @@ var (
|
|||||||
uiBg = rgb{0x12, 0x14, 0x18}
|
uiBg = rgb{0x12, 0x14, 0x18}
|
||||||
uiPanel = rgb{0x1c, 0x20, 0x26}
|
uiPanel = rgb{0x1c, 0x20, 0x26}
|
||||||
uiRule = rgb{0x2e, 0x34, 0x3c}
|
uiRule = rgb{0x2e, 0x34, 0x3c}
|
||||||
|
uiButton = rgb{0x25, 0x2b, 0x33}
|
||||||
uiFg = rgb{0xe6, 0xe8, 0xea}
|
uiFg = rgb{0xe6, 0xe8, 0xea}
|
||||||
uiDim = rgb{0x7a, 0x82, 0x8c}
|
uiDim = rgb{0x7a, 0x82, 0x8c}
|
||||||
uiCyan = rgb{0x5c, 0xc8, 0xe0}
|
uiCyan = rgb{0x5c, 0xc8, 0xe0}
|
||||||
@@ -37,14 +39,31 @@ const (
|
|||||||
colLateEnd = 46
|
colLateEnd = 46
|
||||||
colCRCEnd = 53
|
colCRCEnd = 53
|
||||||
colKdropEnd = 62
|
colKdropEnd = 62
|
||||||
|
|
||||||
|
btnW = 200
|
||||||
|
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 {
|
type display struct {
|
||||||
fb *framebuffer
|
fb *framebuffer
|
||||||
huge *textFace
|
huge *textFace
|
||||||
grid *textFace
|
grid *textFace
|
||||||
gridB *textFace
|
gridB *textFace
|
||||||
small *textFace
|
small *textFace
|
||||||
|
|
||||||
|
resetBtn rect
|
||||||
|
holdStart time.Time
|
||||||
|
holdFrac float64
|
||||||
|
fired bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func newDisplay() (*display, error) {
|
func newDisplay() (*display, error) {
|
||||||
@@ -75,9 +94,71 @@ func newDisplay() (*display, error) {
|
|||||||
return nil, fmt.Errorf("grid faces disagree on cell width: %d vs %d",
|
return nil, fmt.Errorf("grid faces disagree on cell width: %d vs %d",
|
||||||
d.grid.cellW, d.gridB.cellW)
|
d.grid.cellW, d.gridB.cellW)
|
||||||
}
|
}
|
||||||
|
d.resetBtn = rect{
|
||||||
|
x: fb.w - uiMargin - btnW,
|
||||||
|
y: fb.h - uiMargin - btnH,
|
||||||
|
w: btnW,
|
||||||
|
h: btnH,
|
||||||
|
}
|
||||||
return d, nil
|
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() {
|
func (d *display) close() {
|
||||||
d.fb.close()
|
d.fb.close()
|
||||||
}
|
}
|
||||||
@@ -164,7 +245,7 @@ func (d *display) render(dirs []*direction, views []view, elapsed time.Duration,
|
|||||||
lineH := d.grid.cellH + 4
|
lineH := d.grid.cellH + 4
|
||||||
sectionH := d.small.cellH + 10 + (1+len(dirs))*lineH
|
sectionH := d.small.cellH + 10 + (1+len(dirs))*lineH
|
||||||
gap := 30
|
gap := 30
|
||||||
avail := fb.h - (bandY + bandH) - 16
|
avail := fb.h - (bandY + bandH) - btnH - 2*uiMargin
|
||||||
y := bandY + bandH + 8 + (avail-2*sectionH-gap)/2
|
y := bandY + bandH + 8 + (avail-2*sectionH-gap)/2
|
||||||
|
|
||||||
y = d.section(y, "RATE")
|
y = d.section(y, "RATE")
|
||||||
@@ -204,6 +285,7 @@ func (d *display) render(dirs []*direction, views []view, elapsed time.Duration,
|
|||||||
y += lineH
|
y += lineH
|
||||||
}
|
}
|
||||||
|
|
||||||
|
d.drawResetButton()
|
||||||
fb.flush()
|
fb.flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user