292 lines
6.4 KiB
Go
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))
|
|
}
|
|
}
|
|
}
|