Files
alice-backend/station/panel.go
T

292 lines
6.4 KiB
Go

package main
import (
"context"
"encoding/binary"
"encoding/json"
"io"
"log"
"os"
"path/filepath"
"strings"
"time"
)
const (
panelMinus = 114
panelPlus = 115
panelCenter = 582
panelMinTap = 18 * time.Millisecond
panelGap = 155 * time.Millisecond
panelMaxSwipe = 520 * time.Millisecond
panelHold = 500 * time.Millisecond
panelRepeat = 250 * time.Millisecond
panelWakeHold = 3 * time.Second
panelCooldown = 170 * time.Millisecond
panelMaxHold = 6 * time.Second
)
type panelAction string
const (
panelQuieter panelAction = "quieter"
panelLouder panelAction = "louder"
panelToggle panelAction = "toggle"
panelNext panelAction = "next"
panelPrevious panelAction = "swipe_previous"
panelWake panelAction = "wake"
)
type panelGesture struct {
path [3]uint16
count int
down uint8
start time.Time
lastRelease time.Time
nextHold time.Time
cooldown time.Time
held bool
invalid bool
ignoreToSync bool
}
func panelZone(code uint16) uint8 {
switch code {
case panelMinus:
return 1
case panelCenter:
return 2
case panelPlus:
return 4
}
return 0
}
func (g *panelGesture) finish(now time.Time) panelAction {
var action panelAction
if !g.held && !g.invalid {
switch {
case g.count == 1 && g.lastRelease.Sub(g.start) >= panelMinTap:
switch g.path[0] {
case panelMinus:
action = panelQuieter
case panelPlus:
action = panelLouder
case panelCenter:
action = panelToggle
}
case g.count == 3 && g.lastRelease.Sub(g.start) <= panelMaxSwipe:
if g.path == [3]uint16{panelMinus, panelCenter, panelPlus} {
action = panelNext
} else if g.path == [3]uint16{panelPlus, panelCenter, panelMinus} {
action = panelPrevious
}
}
}
cooldown := g.cooldown
if action == panelNext || action == panelPrevious {
cooldown = now.Add(panelCooldown)
}
*g = panelGesture{cooldown: cooldown}
return action
}
func (g *panelGesture) tick(now time.Time) panelAction {
if g.count == 0 {
return ""
}
if g.down == 0 && !now.Before(g.lastRelease.Add(panelGap)) {
return g.finish(now)
}
if g.down == 0 || g.invalid || g.count != 1 || now.Before(g.nextHold) {
return ""
}
g.held = true
if now.Sub(g.start) >= panelMaxHold {
g.invalid = true
return ""
}
g.nextHold = now.Add(panelRepeat)
switch g.path[0] {
case panelMinus:
return panelQuieter
case panelPlus:
return panelLouder
case panelCenter:
g.invalid = true
return panelWake
}
return ""
}
func (g *panelGesture) key(now time.Time, code uint16, value uint32) {
zone := panelZone(code)
if zone == 0 || g.ignoreToSync || value > 1 {
return
}
if value == 0 {
if g.down&zone != 0 {
g.down &^= zone
g.lastRelease = now
}
return
}
if g.down&zone != 0 {
return
}
g.down |= zone
if g.count == 0 {
g.start = now
g.nextHold = now.Add(panelHold)
if code == panelCenter {
g.nextHold = now.Add(panelWakeHold)
}
g.invalid = now.Before(g.cooldown)
}
if g.held || g.count == len(g.path) {
g.invalid = true
return
}
g.path[g.count] = code
g.count++
}
type panelEvent struct {
kind, code uint16
value uint32
}
func readPanel(device io.Reader, events chan<- panelEvent) error {
defer close(events)
// The ARM64 process sees a 16-byte timeval before the event fields.
var raw [24]byte
for {
if _, err := io.ReadFull(device, raw[:]); err != nil {
return err
}
events <- panelEvent{
kind: binary.LittleEndian.Uint16(raw[16:18]),
code: binary.LittleEndian.Uint16(raw[18:20]),
value: binary.LittleEndian.Uint32(raw[20:24]),
}
}
}
func listenPanel(playback string, dialog *conversation) {
names, _ := filepath.Glob("/sys/class/input/event*/device/name")
for _, path := range names {
name, err := os.ReadFile(path)
if err != nil || strings.TrimSpace(string(name)) != "sar0_pad0" {
continue
}
event := filepath.Base(filepath.Dir(filepath.Dir(path)))
device, err := os.Open(filepath.Join("/dev/input", event))
if err != nil {
log.Printf("open touch panel: %v", err)
return
}
defer device.Close()
log.Printf("touch panel ready on %s", event)
events := make(chan panelEvent, 24)
go func() {
if err := readPanel(device, events); err != nil {
log.Printf("read touch panel: %v", err)
}
}()
runPanel(events, playback, dialog)
return
}
log.Print("touch panel input device unavailable")
}
func runPanel(events <-chan panelEvent, playback string, dialog *conversation) {
volume := make(chan panelAction, 1)
music := make(chan panelAction, 1)
defer close(volume)
defer close(music)
go func() {
for action := range volume {
delta := 10
if action == panelQuieter {
delta = -10
}
level, err := changeVolume(delta)
if err != nil {
log.Printf("panel volume: %v", err)
continue
}
lights.showVolume(level)
go playVolumeTone(playback, level)
}
}()
go func() {
for action := range music {
args, _ := json.Marshal(map[string]string{"action": string(action)})
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
result := musicCallContext(ctx, "spotify_control", args)
cancel()
var reply struct {
Error, Action string
Accepted bool
}
if err := json.Unmarshal(result, &reply); err != nil || reply.Error != "" {
log.Printf("panel Spotify action %s failed", action)
lights.confirm("error")
} else if reply.Accepted {
feedback := reply.Action
if action == panelNext {
feedback = "next"
}
lights.confirm(feedback)
}
}
}()
emit := func(action panelAction) {
switch action {
case panelWake:
if unmuted() && !dialog.stop() {
select {
case dialog.panelWake <- struct{}{}:
default:
}
}
case panelQuieter, panelLouder:
select {
case volume <- action:
default:
}
case panelToggle, panelNext, panelPrevious:
if action == panelToggle && dialog.stop() {
return
}
select {
case music <- action:
default:
}
}
}
var gesture panelGesture
ticker := time.NewTicker(10 * time.Millisecond)
defer ticker.Stop()
for {
select {
case event, ok := <-events:
if !ok {
return
}
now := time.Now()
if event.kind == 0 && event.code == 3 { // SYN_DROPPED
gesture = panelGesture{ignoreToSync: true}
} else if event.kind == 0 && event.code == 0 {
gesture.ignoreToSync = false
} else if event.kind == 1 {
emit(gesture.tick(now))
gesture.key(now, event.code, event.value)
}
case now := <-ticker.C:
emit(gesture.tick(now))
}
}
}