Skip to content

Commit ffb28db

Browse files
authored
Merge pull request #7 from taigrr/cd/tag-darwin-cli-mains
fix(sensordash): preserve negative peaks in downsampling
2 parents 60d6f1a + 131947e commit ffb28db

3 files changed

Lines changed: 178 additions & 120 deletions

File tree

cmd/sensordash/main.go

Lines changed: 30 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -20,30 +20,30 @@ import (
2020
"github.com/charmbracelet/fang"
2121
"github.com/spf13/cobra"
2222
"github.com/taigrr/apple-silicon-accelerometer/detector"
23+
"github.com/taigrr/apple-silicon-accelerometer/internal/renderutil"
2324
"github.com/taigrr/apple-silicon-accelerometer/shm"
2425
)
2526

2627
var version = "dev"
2728

2829
// ANSI escape codes.
2930
const (
30-
rst = "\033[0m"
31-
bold = "\033[1m"
32-
dim = "\033[2m"
33-
red = "\033[31m"
34-
grn = "\033[32m"
35-
yel = "\033[33m"
36-
cyn = "\033[36m"
37-
bred = "\033[91m"
38-
bwht = "\033[97m"
39-
hideCur = "\033[?25l"
40-
showCur = "\033[?25h"
41-
altOn = "\033[?1049h"
42-
altOff = "\033[?1049l"
43-
clear = "\033[2J\033[H"
44-
45-
width = 76
46-
blocks = " ▁▂▃▄▅▆▇█"
31+
rst = "\x1b[0m"
32+
bold = "\x1b[1m"
33+
dim = "\x1b[2m"
34+
red = "\x1b[31m"
35+
grn = "\x1b[32m"
36+
yel = "\x1b[33m"
37+
cyn = "\x1b[36m"
38+
bred = "\x1b[91m"
39+
bwht = "\x1b[97m"
40+
hideCur = "\x1b[?25l"
41+
showCur = "\x1b[?25h"
42+
altOn = "\x1b[?1049h"
43+
altOff = "\x1b[?1049l"
44+
clear = "\x1b[2J\x1b[H"
45+
46+
width = 76
4747
)
4848

4949
func main() {
@@ -188,13 +188,13 @@ func render(det *detector.Detector, tStart time.Time, lidAngle float64, lidValid
188188
gw := width - 4
189189

190190
line := func(content string) {
191-
vl := visLen(content)
191+
vl := renderutil.VisibleLen(content)
192192
pad := max(0, width-vl)
193193
fmt.Fprintf(&b, "%s│%s%s%s│%s\n", dim, rst, content, strings.Repeat(" ", pad), rst)
194194
}
195195
sep := func(label string) {
196196
if label != "" {
197-
rest := width - visLen(label) - 1
197+
rest := width - renderutil.VisibleLen(label) - 1
198198
fmt.Fprintf(&b, "%s├─%s%s┤%s\n", dim, label, strings.Repeat("─", rest), rst)
199199
} else {
200200
fmt.Fprintf(&b, "%s├%s┤%s\n", dim, strings.Repeat("─", width), rst)
@@ -220,8 +220,8 @@ func render(det *detector.Detector, tStart time.Time, lidAngle float64, lidValid
220220
mx = math.Abs(v)
221221
}
222222
}
223-
ds := downsample(wav, gw)
224-
line(fmt.Sprintf(" %s%s%s", grn, sparkline(ds, gw, mx), rst))
223+
ds := renderutil.Downsample(wav, gw)
224+
line(fmt.Sprintf(" %s%s%s", grn, renderutil.Sparkline(ds, gw, mx), rst))
225225
line(fmt.Sprintf(" %s%.5fg%s%s0g%s", dim, mx, strings.Repeat(" ", gw-22), rst, rst))
226226
} else {
227227
line(fmt.Sprintf(" %swaiting...%s", dim, rst))
@@ -247,9 +247,9 @@ func render(det *detector.Detector, tStart time.Time, lidAngle float64, lidValid
247247
}
248248
}
249249
}
250-
line(fmt.Sprintf(" %sX%s %s%s", red, rst, sparkline(downsample(xs, aw), aw, amx), rst))
251-
line(fmt.Sprintf(" %sY%s %s%s", grn, rst, sparkline(downsample(ys, aw), aw, amx), rst))
252-
line(fmt.Sprintf(" %sZ%s %s%s", cyn, rst, sparkline(downsample(zs, aw), aw, amx), rst))
250+
line(fmt.Sprintf(" %sX%s %s%s", red, rst, renderutil.Sparkline(renderutil.Downsample(xs, aw), aw, amx), rst))
251+
line(fmt.Sprintf(" %sY%s %s%s", grn, rst, renderutil.Sparkline(renderutil.Downsample(ys, aw), aw, amx), rst))
252+
line(fmt.Sprintf(" %sZ%s %s%s", cyn, rst, renderutil.Sparkline(renderutil.Downsample(zs, aw), aw, amx), rst))
253253
} else {
254254
line(fmt.Sprintf(" %sX%s", dim, rst))
255255
line(fmt.Sprintf(" %sY%s", dim, rst))
@@ -260,7 +260,7 @@ func render(det *detector.Detector, tStart time.Time, lidAngle float64, lidValid
260260
sep(" RMS trend 10s ")
261261
rms := det.RMSTrend.Slice()
262262
if len(rms) > 0 {
263-
line(fmt.Sprintf(" %s%s%s", yel, sparkline(rms, gw, 0), rst))
263+
line(fmt.Sprintf(" %s%s%s", yel, renderutil.Sparkline(rms, gw, 0), rst))
264264
} else {
265265
line(fmt.Sprintf(" %saccumulating...%s", dim, rst))
266266
}
@@ -269,7 +269,7 @@ func render(det *detector.Detector, tStart time.Time, lidAngle float64, lidValid
269269
sep(" Detectors ")
270270
names := [3]string{"fast", "med ", "slow"}
271271
for i := range 3 {
272-
sp := sparkline(det.STALTARings[i].Slice(), 25, det.STALTAOn(i)*2)
272+
sp := renderutil.Sparkline(det.STALTARings[i].Slice(), 25, det.STALTAOn(i)*2)
273273
r := det.STALTALatest[i]
274274
thr := det.STALTAOn(i)
275275
mark := " "
@@ -300,7 +300,7 @@ func render(det *detector.Detector, tStart time.Time, lidAngle float64, lidValid
300300
acCeil = math.Abs(v) * 1.2
301301
}
302302
}
303-
line(fmt.Sprintf(" %s%s%s", cyn, sparkline(det.ACorrRing, gw, acCeil), rst))
303+
line(fmt.Sprintf(" %s%s%s", cyn, renderutil.Sparkline(det.ACorrRing, gw, acCeil), rst))
304304
} else {
305305
line(fmt.Sprintf(" %saccumulating...%s", dim, rst))
306306
}
@@ -331,9 +331,9 @@ func render(det *detector.Detector, tStart time.Time, lidAngle float64, lidValid
331331
sep(" Orientation ")
332332
orient := det.GetOrientation()
333333
ow := width - 18
334-
line(fmt.Sprintf(" %sRoll %s %s%s%s %+7.1f°", dim, rst, cyn, gauge(orient.Roll, -180, 180, ow), rst, orient.Roll))
335-
line(fmt.Sprintf(" %sPitch%s %s%s%s %+7.1f°", dim, rst, cyn, gauge(orient.Pitch, -90, 90, ow), rst, orient.Pitch))
336-
line(fmt.Sprintf(" %sYaw %s %s%s%s %+7.1f°", dim, rst, cyn, gauge(orient.Yaw, -180, 180, ow), rst, orient.Yaw))
334+
line(fmt.Sprintf(" %sRoll %s %s%s%s %+7.1f°", dim, rst, cyn, renderutil.Gauge(orient.Roll, -180, 180, ow), rst, orient.Roll))
335+
line(fmt.Sprintf(" %sPitch%s %s%s%s %+7.1f°", dim, rst, cyn, renderutil.Gauge(orient.Pitch, -90, 90, ow), rst, orient.Pitch))
336+
line(fmt.Sprintf(" %sYaw %s %s%s%s %+7.1f°", dim, rst, cyn, renderutil.Gauge(orient.Yaw, -180, 180, ow), rst, orient.Yaw))
337337
line(fmt.Sprintf(" %sω: %+6.2f %+6.2f %+6.2f °/s%s",
338338
dim, det.GyroLatest[0], det.GyroLatest[1], det.GyroLatest[2], rst))
339339

@@ -379,96 +379,6 @@ func render(det *detector.Detector, tStart time.Time, lidAngle float64, lidValid
379379
return b.String()
380380
}
381381

382-
func sparkline(data []float64, width int, ceil float64) string {
383-
if len(data) == 0 {
384-
return strings.Repeat(" ", width)
385-
}
386-
d := data
387-
if len(d) < width {
388-
pad := make([]float64, width-len(d))
389-
d = append(pad, d...)
390-
} else if len(d) > width {
391-
d = d[len(d)-width:]
392-
}
393-
if ceil <= 0 {
394-
for _, v := range d {
395-
if math.Abs(v) > ceil {
396-
ceil = math.Abs(v)
397-
}
398-
}
399-
}
400-
if ceil <= 0 {
401-
ceil = 1
402-
}
403-
blk := []rune(blocks)
404-
var b strings.Builder
405-
for _, v := range d {
406-
frac := math.Min(1, math.Abs(v)/ceil)
407-
idx := min(8, int(frac*8))
408-
b.WriteRune(blk[idx])
409-
}
410-
return b.String()
411-
}
412-
413-
func gauge(value, vmin, vmax float64, width int) string {
414-
rng := vmax - vmin
415-
if rng == 0 {
416-
rng = 1
417-
}
418-
t := math.Max(0, math.Min(1, (value-vmin)/rng))
419-
pos := int(t * float64(width-1))
420-
center := int((0 - vmin) / rng * float64(width-1))
421-
bar := make([]rune, width)
422-
for i := range bar {
423-
bar[i] = '─'
424-
}
425-
if center >= 0 && center < width {
426-
bar[center] = '┼'
427-
}
428-
bar[max(0, min(width-1, pos))] = '●'
429-
return string(bar)
430-
}
431-
432-
func downsample(data []float64, width int) []float64 {
433-
n := len(data)
434-
if n <= width {
435-
return data
436-
}
437-
step := float64(n) / float64(width)
438-
out := make([]float64, width)
439-
for c := range width {
440-
si := int(float64(c) * step)
441-
ei := int(float64(c+1) * step)
442-
mx := data[si]
443-
for j := si + 1; j < ei && j < n; j++ {
444-
if data[j] > mx {
445-
mx = data[j]
446-
}
447-
}
448-
out[c] = mx
449-
}
450-
return out
451-
}
452-
453-
func visLen(s string) int {
454-
n := 0
455-
inEsc := false
456-
for _, r := range s {
457-
if r == '\033' {
458-
inEsc = true
459-
continue
460-
}
461-
if inEsc {
462-
if r == 'm' {
463-
inEsc = false
464-
}
465-
continue
466-
}
467-
n++
468-
}
469-
return n
470-
}
471-
472382
func sevColor(sev string) string {
473383
switch sev {
474384
case "CHOC_MAJEUR":

internal/renderutil/renderutil.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package renderutil
2+
3+
import (
4+
"math"
5+
"strings"
6+
)
7+
8+
const blocks = " ▁▂▃▄▅▆▇█"
9+
10+
func Sparkline(data []float64, width int, ceil float64) string {
11+
if len(data) == 0 {
12+
return strings.Repeat(" ", width)
13+
}
14+
d := data
15+
if len(d) < width {
16+
pad := make([]float64, width-len(d))
17+
d = append(pad, d...)
18+
} else if len(d) > width {
19+
d = d[len(d)-width:]
20+
}
21+
if ceil <= 0 {
22+
for _, value := range d {
23+
if math.Abs(value) > ceil {
24+
ceil = math.Abs(value)
25+
}
26+
}
27+
}
28+
if ceil <= 0 {
29+
ceil = 1
30+
}
31+
blk := []rune(blocks)
32+
var b strings.Builder
33+
for _, value := range d {
34+
frac := math.Min(1, math.Abs(value)/ceil)
35+
idx := min(8, int(frac*8))
36+
b.WriteRune(blk[idx])
37+
}
38+
return b.String()
39+
}
40+
41+
func Gauge(value, vmin, vmax float64, width int) string {
42+
rng := vmax - vmin
43+
if rng == 0 {
44+
rng = 1
45+
}
46+
t := math.Max(0, math.Min(1, (value-vmin)/rng))
47+
pos := int(t * float64(width-1))
48+
center := int((0 - vmin) / rng * float64(width-1))
49+
bar := make([]rune, width)
50+
for idx := range bar {
51+
bar[idx] = '─'
52+
}
53+
if center >= 0 && center < width {
54+
bar[center] = '┼'
55+
}
56+
bar[max(0, min(width-1, pos))] = '●'
57+
return string(bar)
58+
}
59+
60+
func Downsample(data []float64, width int) []float64 {
61+
n := len(data)
62+
if n <= width {
63+
return data
64+
}
65+
step := float64(n) / float64(width)
66+
out := make([]float64, width)
67+
for column := range width {
68+
startIdx := int(float64(column) * step)
69+
endIdx := int(float64(column+1) * step)
70+
selected := data[startIdx]
71+
for sampleIdx := startIdx + 1; sampleIdx < endIdx && sampleIdx < n; sampleIdx++ {
72+
if math.Abs(data[sampleIdx]) > math.Abs(selected) {
73+
selected = data[sampleIdx]
74+
}
75+
}
76+
out[column] = selected
77+
}
78+
return out
79+
}
80+
81+
func VisibleLen(s string) int {
82+
n := 0
83+
inEsc := false
84+
for _, r := range s {
85+
if r == '\033' {
86+
inEsc = true
87+
continue
88+
}
89+
if inEsc {
90+
if r == 'm' {
91+
inEsc = false
92+
}
93+
continue
94+
}
95+
n++
96+
}
97+
return n
98+
}
99+
100+
func min(a, b int) int {
101+
if a < b {
102+
return a
103+
}
104+
return b
105+
}
106+
107+
func max(a, b int) int {
108+
if a > b {
109+
return a
110+
}
111+
return b
112+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package renderutil
2+
3+
import (
4+
"reflect"
5+
"testing"
6+
)
7+
8+
func TestDownsamplePreservesLargestMagnitudePerBucket(t *testing.T) {
9+
data := []float64{-0.9, 0.1, 0.2, -0.8, 0.3, 0.4}
10+
got := Downsample(data, 3)
11+
want := []float64{-0.9, -0.8, 0.4}
12+
if !reflect.DeepEqual(got, want) {
13+
t.Fatalf("Downsample() = %v, want %v", got, want)
14+
}
15+
}
16+
17+
func TestVisibleLenIgnoresANSIEscapeCodes(t *testing.T) {
18+
input := "\x1b[31malert\x1b[0m ok"
19+
if got, want := VisibleLen(input), len("alert ok"); got != want {
20+
t.Fatalf("VisibleLen() = %d, want %d", got, want)
21+
}
22+
}
23+
24+
func TestGaugePlacesMarkerAtClampedPosition(t *testing.T) {
25+
got := Gauge(2, -1, 1, 5)
26+
want := "──┼─●"
27+
if got != want {
28+
t.Fatalf("Gauge() = %q, want %q", got, want)
29+
}
30+
31+
got = Gauge(-2, -1, 1, 5)
32+
want = "●─┼──"
33+
if got != want {
34+
t.Fatalf("Gauge() with low clamp = %q, want %q", got, want)
35+
}
36+
}

0 commit comments

Comments
 (0)