146 lines
3.2 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|