237 lines
5.8 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|