Files

237 lines
5.8 KiB
Go

package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"sync"
"time"
)
type deviceTool struct {
ID string `json:"id"`
Name string `json:"name"`
Arguments json.RawMessage `json:"arguments"`
}
func runDeviceTool(ctx context.Context, req deviceTool) json.RawMessage {
if req.Name == "pc_audio" {
return pcAudioTool(ctx, req.Arguments)
}
if req.Name != "station_volume" {
return musicCallContext(ctx, req.Name, req.Arguments)
}
var args struct{ Level, Delta *int }
if json.Unmarshal(req.Arguments, &args) != nil || (args.Level == nil) == (args.Delta == nil) {
return json.RawMessage(`{"error":"invalid_volume"}`)
}
var level int
var err error
if args.Delta != nil && (*args.Delta == -10 || *args.Delta == 10) {
level, err = changeVolume(*args.Delta)
} else if args.Level != nil && *args.Level >= 0 && *args.Level <= 100 {
level = *args.Level
err = setStationVolume(level)
} else {
return json.RawMessage(`{"error":"invalid_volume"}`)
}
if err != nil {
log.Printf("station volume: %v", err)
return json.RawMessage(`{"error":"station_volume_failed"}`)
}
lights.showVolume(level)
return json.RawMessage(fmt.Sprintf(`{"ok":true,"level":%d}`, level))
}
type limitedOutput struct {
bytes.Buffer
overflow bool
}
func (b *limitedOutput) Write(p []byte) (int, error) {
n := len(p)
room := 16384 - b.Len()
if room < n {
b.overflow = true
if room < 0 {
room = 0
}
p = p[:room]
}
_, _ = b.Buffer.Write(p)
return n, nil
}
var musicMu sync.Mutex
func musicCall(name string, args json.RawMessage) json.RawMessage {
return musicCallContext(context.Background(), name, args)
}
func musicCallContext(ctx context.Context, name string, args json.RawMessage) json.RawMessage {
if name != "duck" {
musicMu.Lock()
defer musicMu.Unlock()
}
fail := func(s string) json.RawMessage { b, _ := json.Marshal(map[string]string{"error": s}); return b }
if name != "spotify_play" && name != "spotify_control" && name != "duck" {
return fail("unsupported device tool")
}
if len(args) == 0 {
args = json.RawMessage(`{}`)
}
if !json.Valid(args) {
return fail("invalid arguments")
}
input, e := json.Marshal(map[string]interface{}{"name": name, "arguments": args})
if e != nil {
return fail("invalid arguments")
}
path := os.Getenv("ALICE_MUSIC_HELPER")
if path == "" {
path = "/data/opt/alice-backend/music-control"
}
ctx, cancel := context.WithTimeout(ctx, 12*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, path)
terminateWithClient(cmd)
cmd.Stdin = bytes.NewReader(input)
var out limitedOutput
cmd.Stdout = &out
if e = cmd.Run(); e != nil {
return fail(fmt.Sprintf("music helper failed: %v", e))
}
if out.overflow || !json.Valid(bytes.TrimSpace(out.Bytes())) {
return fail("invalid music helper output")
}
return json.RawMessage(bytes.TrimSpace(out.Bytes()))
}
type ring struct {
mu sync.Mutex
state string
done chan struct{}
finished chan struct{}
once sync.Once
volume int
volumeUntil time.Time
feedback string
feedbackUntil time.Time
}
var lights = &ring{state: "idle"}
func (r *ring) set(s string) { r.mu.Lock(); r.state = s; r.mu.Unlock() }
func (r *ring) showVolume(level int) {
r.mu.Lock()
defer r.mu.Unlock()
r.volume = level
r.volumeUntil = time.Now().Add(900 * time.Millisecond)
}
func volumeFrame(level int) [6][3]int {
var frame [6][3]int
red := level
green := 100 - level
// Fill six segments proportionally, with a green zero-level marker.
for i := range frame {
fill := max(0, min(100, 6*level-100*i))
frame[i] = [3]int{red * fill / 100, green * fill / 100, 0}
}
if level == 0 {
frame[0] = [3]int{0, 15, 0}
}
return frame
}
func (r *ring) confirm(state string) {
r.mu.Lock()
defer r.mu.Unlock()
r.feedback, r.feedbackUntil = state, time.Now().Add(650*time.Millisecond)
}
func (r *ring) start() {
r.done = make(chan struct{})
r.finished = make(chan struct{})
go func() {
defer close(r.finished)
patterns := make(map[string][]ledStep)
for _, state := range []string{"listening", "processing", "speaking", "error", "pause", "resume", "next", "previous"} {
patterns[state] = loadLedPattern(state)
}
previousState := ""
changed := time.Now()
t := time.NewTicker(25 * time.Millisecond)
defer t.Stop()
tick := 0
var last [6][3]int
first := true
for {
r.mu.Lock()
s, level, volumeUntil := r.state, r.volume, r.volumeUntil
if s == "idle" && time.Now().Before(r.feedbackUntil) {
s = r.feedback
}
r.mu.Unlock()
if !unmuted() {
s = "mute"
}
if s != previousState {
previousState, changed = s, time.Now()
}
var rgb [3]int
switch s {
case "listening":
rgb = [3]int{80, 8, 125}
case "processing":
rgb = [3]int{8, 0, 18}
case "speaking":
rgb = [3]int{0, 35, 3}
case "mute":
rgb = [3]int{45, 0, 0}
case "error":
if tick%4 < 2 {
rgb = [3]int{50, 0, 0}
}
}
var frame [6][3]int
for i := range frame {
frame[i] = rgb
}
if steps := patterns[s]; len(steps) > 0 {
frame = patternFrame(steps, time.Since(changed))
}
if time.Now().Before(volumeUntil) {
frame = volumeFrame(level)
}
if first || frame != last {
r.write(frame)
last = frame
first = false
}
tick++
select {
case <-r.done:
r.write([6][3]int{})
return
case <-t.C:
}
}
}()
}
func (r *ring) stop() { r.once.Do(func() { close(r.done); <-r.finished }) }
func (r *ring) flashError() { r.set("error"); time.Sleep(1200 * time.Millisecond); r.set("idle") }
func (r *ring) write(frame [6][3]int) {
for i, rgb := range frame {
for j, color := range []string{"red", "green", "blue"} {
path := filepath.Join("/sys/class/leds", fmt.Sprintf("mini2:%s:ring%d", color, i+1), "brightness")
_ = os.WriteFile(path, []byte(fmt.Sprint(rgb[j])), 0)
}
}
}