Add an auto-measure toggle with a cable length range, and console keys through the harness
This commit is contained in:
@@ -20,6 +20,8 @@ import (
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
var errInterrupted = errors.New("interrupted")
|
||||
@@ -43,18 +45,40 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
restoreTerm := rawKeys()
|
||||
|
||||
var err error
|
||||
if *fuzzN > 0 {
|
||||
err = fuzz(*fuzzN, *runFor, flag.Args())
|
||||
} else {
|
||||
err = run(*runFor, *at, os.Stdout, flag.Args())
|
||||
}
|
||||
restoreTerm()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Console keys reach cabletest as they are pressed, the same as at the
|
||||
// appliance; isig stays on so ^C still stops the run through the relay.
|
||||
func rawKeys() func() {
|
||||
fd := int(os.Stdin.Fd())
|
||||
orig, err := unix.IoctlGetTermios(fd, unix.TCGETS)
|
||||
if err != nil {
|
||||
return func() {}
|
||||
}
|
||||
saved := *orig
|
||||
raw := *orig
|
||||
raw.Lflag &^= unix.ICANON | unix.ECHO
|
||||
raw.Cc[unix.VMIN] = 1
|
||||
raw.Cc[unix.VTIME] = 0
|
||||
if unix.IoctlSetTermios(fd, unix.TCSETS, &raw) != nil {
|
||||
return func() {}
|
||||
}
|
||||
return func() { unix.IoctlSetTermios(fd, unix.TCSETS, &saved) }
|
||||
}
|
||||
|
||||
func fuzz(n int, runFor time.Duration, args []string) error {
|
||||
failed := 0
|
||||
for i := 1; i <= n; i++ {
|
||||
@@ -133,6 +157,13 @@ func run(runFor time.Duration, at string, out io.Writer, args []string) error {
|
||||
// killed when go run dies, and an orphan holding the wire poisons every
|
||||
// measurement after it.
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||
// The child's own process group makes the tty off limits to it (reading
|
||||
// from the background raises SIGTTIN), so the harness reads and forwards.
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go io.Copy(stdin, os.Stdin)
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -160,11 +160,11 @@ func watchTouch(w, h int) (*touchState, error) {
|
||||
return state, nil
|
||||
}
|
||||
|
||||
// Delivers a value every time space is pressed. Puts the terminal in
|
||||
// non-canonical mode so the keypress arrives without waiting for a newline,
|
||||
// and returns a function that restores the original settings.
|
||||
func watchSpace() (<-chan struct{}, func()) {
|
||||
ch := make(chan struct{}, 1)
|
||||
// Delivers each key as it is pressed. Puts the terminal in non-canonical mode
|
||||
// so keypresses arrive without waiting for a newline, and returns a function
|
||||
// that restores the original settings.
|
||||
func watchKeys() (<-chan byte, func()) {
|
||||
ch := make(chan byte, 8)
|
||||
restore := func() {}
|
||||
fd := int(os.Stdin.Fd())
|
||||
|
||||
@@ -187,11 +187,8 @@ func watchSpace() (<-chan struct{}, func()) {
|
||||
return
|
||||
}
|
||||
for _, b := range buf[:n] {
|
||||
if b != ' ' {
|
||||
continue
|
||||
}
|
||||
select {
|
||||
case ch <- struct{}{}:
|
||||
case ch <- b:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,8 +56,8 @@ func resetAll(dirs []*direction, mods []*phyModule) time.Time {
|
||||
// Nothing to hold or revert here: failures during a measure were never
|
||||
// counted, so the view is always the counters as they stand.
|
||||
func phyView(diag *cableDiag, modules []*phyModule) (phyDisplay, bool) {
|
||||
info, measuring := diag.snapshot()
|
||||
return phyDisplayFrom(info, measuring, modules[0].view(), modules[1].view()), measuring
|
||||
info, rng, measuring := diag.snapshot()
|
||||
return phyDisplayFrom(info, rng, measuring, modules[0].view(), modules[1].view()), measuring
|
||||
}
|
||||
|
||||
var intervalCols = []colSpec{
|
||||
@@ -141,7 +141,8 @@ func main() {
|
||||
}
|
||||
|
||||
const (
|
||||
reportInterval = time.Second
|
||||
reportInterval = time.Second
|
||||
autoMeasurePeriod = 60 * time.Second
|
||||
// Deliberately not tied to the refresh: letting a slow or blocked draw set
|
||||
// the sampling clock would stretch the window it reports.
|
||||
sampleInterval = 16 * time.Millisecond
|
||||
@@ -320,7 +321,7 @@ func run(aName, bName string) (err error) {
|
||||
sig := make(chan os.Signal, 1)
|
||||
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
space, restoreTerm := watchSpace()
|
||||
keys, restoreTerm := watchKeys()
|
||||
defer restoreTerm()
|
||||
|
||||
disp, err := newDisplay()
|
||||
@@ -345,6 +346,7 @@ func run(aName, bName string) (err error) {
|
||||
// hide or revert afterwards.
|
||||
kickMeasure := func() {
|
||||
if diag.kick(&done) {
|
||||
diag.resetRange()
|
||||
start = resetAll(dirs, modules)
|
||||
hist.reset()
|
||||
}
|
||||
@@ -359,6 +361,8 @@ func run(aName, bName string) (err error) {
|
||||
views := make([]view, len(dirs))
|
||||
rows := make([]view, len(dirs))
|
||||
stats := &streamTable{cols: intervalCols, headerEvery: 20}
|
||||
var autoOn bool
|
||||
var autoAnchor time.Time
|
||||
for {
|
||||
select {
|
||||
case p := <-fatalCh:
|
||||
@@ -366,8 +370,13 @@ func run(aName, bName string) (err error) {
|
||||
case <-sig:
|
||||
return nil
|
||||
// The verdict lives in the cable cell, not its own line.
|
||||
case <-space:
|
||||
kickMeasure()
|
||||
case b := <-keys:
|
||||
switch b {
|
||||
case ' ', 'r':
|
||||
kickMeasure()
|
||||
case 'a':
|
||||
autoOn, autoAnchor = !autoOn, time.Now()
|
||||
}
|
||||
case <-disp.fb.flips:
|
||||
now := time.Now()
|
||||
px, py, down := touch.get()
|
||||
@@ -375,6 +384,17 @@ func run(aName, bName string) (err error) {
|
||||
if disp.holdReset(x, y, down, now) {
|
||||
kickMeasure()
|
||||
}
|
||||
if disp.tapAuto(x, y, down) {
|
||||
autoOn, autoAnchor = !autoOn, now
|
||||
}
|
||||
autoFrac := 0.0
|
||||
if autoOn {
|
||||
autoFrac = float64(now.Sub(autoAnchor)) / float64(autoMeasurePeriod)
|
||||
if autoFrac >= 1 {
|
||||
diag.kick(&done)
|
||||
autoAnchor, autoFrac = now, 0
|
||||
}
|
||||
}
|
||||
disp.showVersion = down && disp.versionSpot.contains(x, y)
|
||||
for i, d := range dirs {
|
||||
views[i] = d.displayView()
|
||||
@@ -383,7 +403,7 @@ func run(aName, bName string) (err error) {
|
||||
phy, measuring := phyView(diag, modules)
|
||||
hist.sample(now, v, phy, measuring, target)
|
||||
if err := disp.render(v, now.Sub(start), phy, measuring,
|
||||
hist.view(now), target); err != nil {
|
||||
hist.view(now), target, autoOn, autoFrac); err != nil {
|
||||
return err
|
||||
}
|
||||
case now := <-tick.C:
|
||||
|
||||
@@ -267,7 +267,7 @@ type cableInfo struct {
|
||||
haveMaps [2]bool
|
||||
}
|
||||
|
||||
func (c cableInfo) metresString() string {
|
||||
func (c cableInfo) metresValue() (int, bool) {
|
||||
sum, n := 0, 0
|
||||
for i, v := range c.ecd.verdicts {
|
||||
if v == pairOK {
|
||||
@@ -276,9 +276,39 @@ func (c cableInfo) metresString() string {
|
||||
}
|
||||
}
|
||||
if n == 0 {
|
||||
return 0, false
|
||||
}
|
||||
return (sum + n/2) / n, true
|
||||
}
|
||||
|
||||
func (c cableInfo) metresString() string {
|
||||
m, ok := c.metresValue()
|
||||
if !ok {
|
||||
return "-"
|
||||
}
|
||||
return fmt.Sprintf("%d", (sum+n/2)/n)
|
||||
return fmt.Sprintf("%d", m)
|
||||
}
|
||||
|
||||
type cableRange struct {
|
||||
have bool
|
||||
min, max int
|
||||
}
|
||||
|
||||
func (r *cableRange) add(m int) {
|
||||
if !r.have || m < r.min {
|
||||
r.min = m
|
||||
}
|
||||
if !r.have || m > r.max {
|
||||
r.max = m
|
||||
}
|
||||
r.have = true
|
||||
}
|
||||
|
||||
func (r cableRange) String() string {
|
||||
if r.min == r.max {
|
||||
return fmt.Sprintf("%d", r.min)
|
||||
}
|
||||
return fmt.Sprintf("%d-%d", r.min, r.max)
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -348,7 +378,7 @@ func cableSummary(cable cableInfo, measuring bool) (string, int) {
|
||||
|
||||
// The margin is the worst pair across the ends that measure SNR (the Wiitek's
|
||||
// IEEE 1.133–136), gated on the whole pair being fresh and linked.
|
||||
func phyDisplayFrom(cable cableInfo, measuring bool, a, b phyModView) phyDisplay {
|
||||
func phyDisplayFrom(cable cableInfo, rng cableRange, measuring bool, a, b phyModView) phyDisplay {
|
||||
d := phyDisplay{
|
||||
haveSNR: a.fresh && b.fresh && a.link && b.link && (a.haveSNR || b.haveSNR),
|
||||
corrected: a.blocks + a.ber + a.retrain + b.blocks + b.ber + b.retrain,
|
||||
@@ -369,6 +399,9 @@ func phyDisplayFrom(cable cableInfo, measuring bool, a, b phyModView) phyDisplay
|
||||
}
|
||||
}
|
||||
d.metres, d.metresClass = cableSummary(cable, measuring)
|
||||
if d.metresClass == clsGood && rng.have {
|
||||
d.metres = rng.String()
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
@@ -444,16 +477,23 @@ type cableDiag struct {
|
||||
|
||||
mu sync.Mutex
|
||||
info cableInfo
|
||||
rng cableRange
|
||||
}
|
||||
|
||||
func newCableDiag(mods []*phyModule, info cableInfo) *cableDiag {
|
||||
return &cableDiag{mods: mods, info: info}
|
||||
}
|
||||
|
||||
func (c *cableDiag) snapshot() (cableInfo, bool) {
|
||||
func (c *cableDiag) snapshot() (cableInfo, cableRange, bool) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.info, c.measuring.Load()
|
||||
return c.info, c.rng, c.measuring.Load()
|
||||
}
|
||||
|
||||
func (c *cableDiag) resetRange() {
|
||||
c.mu.Lock()
|
||||
c.rng = cableRange{}
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
// A measure that errors means the diag transport or the µC is broken, and a
|
||||
@@ -471,6 +511,11 @@ func (c *cableDiag) kick(done *atomic.Bool) bool {
|
||||
}
|
||||
c.mu.Lock()
|
||||
c.info = info
|
||||
if _, cls := cableSummary(info, false); cls == clsGood {
|
||||
if m, ok := info.metresValue(); ok {
|
||||
c.rng.add(m)
|
||||
}
|
||||
}
|
||||
c.mu.Unlock()
|
||||
c.measuring.Store(false)
|
||||
}()
|
||||
|
||||
+35
-5
@@ -14,7 +14,7 @@ func TestPhyDisplayWorstMarginAndFault(t *testing.T) {
|
||||
}
|
||||
a := freshMod([4]float64{5, 5, 2, 0.5})
|
||||
b := freshMod([4]float64{4, 5, 5, 5})
|
||||
d := phyDisplayFrom(cable, false, a, b)
|
||||
d := phyDisplayFrom(cable, cableRange{}, false, a, b)
|
||||
|
||||
if !d.haveSNR || d.worstMargin != 0.5 {
|
||||
t.Errorf("worstMargin = %v (have %v), want 0.5", d.worstMargin, d.haveSNR)
|
||||
@@ -27,15 +27,15 @@ func TestPhyDisplayWorstMarginAndFault(t *testing.T) {
|
||||
func TestPhyDisplaySNREndOwnsMargin(t *testing.T) {
|
||||
wiitek := freshMod([4]float64{9.2, 7.2, 7.0, 8.7})
|
||||
fs := phyModView{fresh: true, link: true}
|
||||
d := phyDisplayFrom(cableInfo{}, false, wiitek, fs)
|
||||
d := phyDisplayFrom(cableInfo{}, cableRange{}, false, wiitek, fs)
|
||||
if !d.haveSNR || d.worstMargin != 7.0 {
|
||||
t.Errorf("worstMargin = %v (have %v), want the SNR end's 7.0", d.worstMargin, d.haveSNR)
|
||||
}
|
||||
d = phyDisplayFrom(cableInfo{}, false, fs, wiitek)
|
||||
d = phyDisplayFrom(cableInfo{}, cableRange{}, false, fs, wiitek)
|
||||
if !d.haveSNR || d.worstMargin != 7.0 {
|
||||
t.Errorf("swapped ends: worstMargin = %v, want 7.0", d.worstMargin)
|
||||
}
|
||||
d = phyDisplayFrom(cableInfo{}, false, fs, fs)
|
||||
d = phyDisplayFrom(cableInfo{}, cableRange{}, false, fs, fs)
|
||||
if d.haveSNR {
|
||||
t.Error("a pair with no SNR end should withhold snr")
|
||||
}
|
||||
@@ -71,7 +71,7 @@ func TestPhyDisplayStaleGoesDim(t *testing.T) {
|
||||
maps: [2]byte{pairIdentityMap, pairIdentityMap},
|
||||
haveMaps: [2]bool{true, true},
|
||||
}
|
||||
d := phyDisplayFrom(cable, false, freshMod([4]float64{5, 5, 5, 5}), phyModView{})
|
||||
d := phyDisplayFrom(cable, cableRange{}, false, freshMod([4]float64{5, 5, 5, 5}), phyModView{})
|
||||
if d.haveSNR {
|
||||
t.Error("one silent module should withhold snr")
|
||||
}
|
||||
@@ -93,6 +93,36 @@ func TestCableMetresString(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCableRange(t *testing.T) {
|
||||
var r cableRange
|
||||
for _, m := range []int{42, 40, 44, 41} {
|
||||
r.add(m)
|
||||
}
|
||||
if r.String() != "40-44" {
|
||||
t.Errorf("range = %q, want 40-44", r.String())
|
||||
}
|
||||
one := cableRange{}
|
||||
one.add(42)
|
||||
if one.String() != "42" {
|
||||
t.Errorf("single measure = %q, want 42", one.String())
|
||||
}
|
||||
|
||||
cable := cableInfo{
|
||||
ecd: ecdResult{verdicts: [4]int{pairOK, pairOK, pairOK, pairOK}, metres: [4]int{45, 45, 41, 46}},
|
||||
maps: [2]byte{pairIdentityMap, pairIdentityMap},
|
||||
haveMaps: [2]bool{true, true},
|
||||
}
|
||||
d := phyDisplayFrom(cable, r, false, freshMod([4]float64{5, 5, 5, 5}), freshMod([4]float64{5, 5, 5, 5}))
|
||||
if d.metres != "40-44" || d.metresClass != clsGood {
|
||||
t.Errorf("metres = %q class %d, want the range shown good", d.metres, d.metresClass)
|
||||
}
|
||||
faulted := cableInfo{ecd: ecdResult{verdicts: [4]int{pairOK, pairOpen, pairOK, pairOK}}}
|
||||
d = phyDisplayFrom(faulted, r, false, freshMod([4]float64{5, 5, 5, 5}), freshMod([4]float64{5, 5, 5, 5}))
|
||||
if d.metres != "OPEN" {
|
||||
t.Errorf("fault shows %q, want the verdict to beat the range", d.metres)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSNRClass(t *testing.T) {
|
||||
for _, c := range []struct {
|
||||
margin float64
|
||||
|
||||
@@ -70,7 +70,7 @@ const (
|
||||
cellGapX = 3
|
||||
rowHMin = 46
|
||||
|
||||
btnW = 220
|
||||
btnW = 190
|
||||
btnH = 72
|
||||
btnRadius = 10
|
||||
btnBorder = 2
|
||||
@@ -124,6 +124,9 @@ type display struct {
|
||||
holdFrac float64
|
||||
fired bool
|
||||
|
||||
autoBtn rect
|
||||
autoDown bool
|
||||
|
||||
version string
|
||||
versionSpot rect
|
||||
showVersion bool
|
||||
@@ -186,6 +189,7 @@ func (d *display) layout(w, h int) error {
|
||||
d.headerH = headerTop + ringD + 24
|
||||
colHdrH := d.small.lineH + 18
|
||||
d.resetBtn = rect{w - uiMargin - btnW, h - uiMargin - btnH, btnW, btnH}
|
||||
d.autoBtn = rect{d.resetBtn.x - uiColGap - btnW, d.resetBtn.y, btnW, btnH}
|
||||
|
||||
// Rows stretch to spend the whole band between the column headers and the
|
||||
// footer, cells riding along at their design proportion of the row.
|
||||
@@ -229,33 +233,26 @@ func (d *display) holdReset(x, y int, down bool, now time.Time) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// Accent rather than the status colours because it is something to press, not
|
||||
// something being reported.
|
||||
func (d *display) drawResetButton() {
|
||||
r := d.resetBtn
|
||||
d.fb.roundRect(r.x, r.y, r.w, r.h, btnRadius, uiAccent)
|
||||
func (d *display) fillButton(r rect, frac float64, label string, frame, ink rgb) {
|
||||
d.fb.roundRect(r.x, r.y, r.w, r.h, btnRadius, frame)
|
||||
d.fb.roundRect(r.x+btnBorder, r.y+btnBorder, r.w-2*btnBorder, r.h-2*btnBorder,
|
||||
btnRadius-btnBorder, uiBg)
|
||||
|
||||
split := r.x + btnBorder
|
||||
if d.holdFrac > 0 {
|
||||
w := int(float64(r.w-2*btnBorder) * math.Min(d.holdFrac, 1))
|
||||
if frac > 0 {
|
||||
w := int(float64(r.w-2*btnBorder) * math.Min(frac, 1))
|
||||
d.fb.roundRect(r.x+btnBorder, r.y+btnBorder, w, r.h-2*btnBorder,
|
||||
btnRadius-btnBorder, uiAccent)
|
||||
btnRadius-btnBorder, frame)
|
||||
split += w
|
||||
}
|
||||
|
||||
label, base := "RESET", uiAccent
|
||||
if d.showVersion {
|
||||
label, base = d.version, uiMuted
|
||||
}
|
||||
lx := r.x + (r.w-len(label)*d.textB.cellW)/2
|
||||
ly := r.y + (r.h-d.textB.lineH)/2 - d.textB.capTop
|
||||
// The label straddles the fill, so each glyph takes the colour that reads
|
||||
// against whatever is behind it.
|
||||
for i, c := range label {
|
||||
gx := lx + i*d.textB.cellW
|
||||
col := base
|
||||
col := ink
|
||||
if gx+d.textB.cellW/2 < split {
|
||||
col = uiBg
|
||||
}
|
||||
@@ -263,6 +260,32 @@ func (d *display) drawResetButton() {
|
||||
}
|
||||
}
|
||||
|
||||
// Accent rather than the status colours because it is something to press, not
|
||||
// something being reported.
|
||||
func (d *display) drawResetButton() {
|
||||
label, ink := "RESET", uiAccent
|
||||
if d.showVersion {
|
||||
label, ink = d.version, uiMuted
|
||||
}
|
||||
d.fillButton(d.resetBtn, d.holdFrac, label, uiAccent, ink)
|
||||
}
|
||||
|
||||
// Muted until switched on; the fill is the time already spent toward the next
|
||||
// automatic measure.
|
||||
func (d *display) drawAutoButton(on bool, frac float64) {
|
||||
c := uiMuted
|
||||
if on {
|
||||
c = uiAccent
|
||||
}
|
||||
d.fillButton(d.autoBtn, frac, "AUTO", c, c)
|
||||
}
|
||||
|
||||
func (d *display) tapAuto(x, y int, down bool) bool {
|
||||
was := d.autoDown
|
||||
d.autoDown = down
|
||||
return down && !was && d.autoBtn.contains(x, y)
|
||||
}
|
||||
|
||||
// go run never stamps VCS info, so dev builds have no revision to show.
|
||||
func buildVersion() string {
|
||||
info, ok := debug.ReadBuildInfo()
|
||||
@@ -574,7 +597,7 @@ func (d *display) drawFooter(v view, elapsed time.Duration) {
|
||||
}
|
||||
|
||||
func (d *display) render(v view, elapsed time.Duration, phy phyDisplay,
|
||||
measuring bool, hv histView, target float64) error {
|
||||
measuring bool, hv histView, target float64, autoOn bool, autoFrac float64) error {
|
||||
fb := d.fb
|
||||
fb.fill(uiBg)
|
||||
|
||||
@@ -583,6 +606,7 @@ func (d *display) render(v view, elapsed time.Duration, phy phyDisplay,
|
||||
d.drawColHeaders()
|
||||
d.drawRows(v, phy, measuring, hv, target)
|
||||
d.drawFooter(v, elapsed)
|
||||
d.drawAutoButton(autoOn, autoFrac)
|
||||
d.drawResetButton()
|
||||
return fb.flush()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user