Files
alice-backend/station/device.go
T

146 lines
3.2 KiB
Go

package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"sync"
"time"
)
type deviceTool struct {
ID string `json:"id"`
Name string `json:"name"`
Arguments json.RawMessage `json:"arguments"`
}
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 {
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(context.Background(), 15*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
}
var lights = &ring{state: "idle"}
func (r *ring) set(s string) { r.mu.Lock(); r.state = s; r.mu.Unlock() }
func (r *ring) start() {
r.done = make(chan struct{})
r.finished = make(chan struct{})
go func() {
defer close(r.finished)
t := time.NewTicker(150 * time.Millisecond)
defer t.Stop()
tick := 0
var last [3]int
first := true
for {
r.mu.Lock()
s := r.state
r.mu.Unlock()
if !unmuted() {
s = "mute"
}
var rgb [3]int
switch s {
case "listening":
rgb = [3]int{12, 0, 70}
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}
}
}
if first || rgb != last {
r.write(rgb)
last = rgb
first = false
}
tick++
select {
case <-r.done:
r.write([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(rgb [3]int) {
for i, color := range []string{"red", "green", "blue"} {
for n := 1; n <= 6; n++ {
path := filepath.Join("/sys/class/leds", fmt.Sprintf("mini2:%s:ring%d", color, n), "brightness")
_ = os.WriteFile(path, []byte(fmt.Sprint(rgb[i])), 0)
}
}
}