Restore Mini 2 hardware volume buttons

This commit is contained in:
2026-11-10 15:59:38 +03:00
parent b005ebd64f
commit 6b483c9c08
4 changed files with 116 additions and 0 deletions
+2
View File
@@ -45,6 +45,8 @@ export DEVICE_TOKEN="$(cat device-token)"
## Автозапуск и музыка
Сенсорные кнопки `+` и `−` меняют общую громкость музыки и ответов на 5 процентных пунктов. Значение сохраняется между запусками.
Загрузочный скрипт `S99alice` запускает наш клиент, а `S98alice-music` поднимает Soloist и PulseAudio. Штатный `S99quasar` пропускает запуск, пока существует `/data/opt/alice-backend/stock-disabled`. Оригинал сохранён как `S99quasar.original` рядом с клиентом.
Для подготовки зависимостей Spotify запусти [скрипт сборки](../scripts/build-spotify-runtime.sh) на x86-64 Linux с Docker. Архив содержит проверяемый список файлов и версий. Распаковывай его потоком в `/data/opt/alice-backend/spotify`, поскольку BusyBox tar на Mini 2 принимает несжатый поток. В `spotify/etc/alice.pa` установи конфигурацию [pulse.conf](pulse.conf), а `pulse-control.sh` скопируй в `spotify/pactl`. Бинарник `music-control` собирается для Linux ARM64 из [music](../music), ключ Soloist хранится в файле `soloist-key` с правами `0600`. Сессия Spotify Connect в `soloist-data/`, кэш ограничен 32 МиБ в tmpfs.
+2
View File
@@ -592,6 +592,8 @@ func main() {
}
}
go listenVolumeButtons()
// Detect whole speech segments locally before opening a voice connection.
log.Println("Listening locally for enrolled phrase")
raw := make([]byte, hop*3*16)
+6
View File
@@ -57,6 +57,12 @@ while [ "$stopping" -eq 0 ]; do
sleep 1
done
if [ "$ready" -eq 1 ] && [ "$stopping" -eq 0 ]; then
if [ -r station-volume ]; then
volume=$(cat station-volume)
case "$volume" in
[0-9]|[1-9][0-9]|100) "$runtime/pactl" set-sink-volume station "$volume%" || true ;;
esac
fi
: >/tmp/alice-soloist.log
"$runtime/run" "$runtime/soloist/soloist" --device-name "Pepega Mini 2" --api-key "$(cat "$base/soloist-key")" --data-dir "$base/soloist-data" --cache-dir /tmp/alice-spotify-cache --cache-size 100 --ws 127.0.0.1:9090 >>/tmp/alice-soloist.log 2>&1 8>&- &
soloist=$!
+106
View File
@@ -0,0 +1,106 @@
package main
import (
"context"
"encoding/binary"
"encoding/json"
"errors"
"io"
"log"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"time"
)
const volumeFile = "/data/opt/alice-backend/station-volume"
const pulseControl = "/data/opt/alice-backend/spotify/pactl"
func listenVolumeButtons() {
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 volume buttons: %v", err)
return
}
defer device.Close()
// Linux ARM64 input_event starts with a 16-byte timeval.
var eventData [24]byte
for {
if _, err := io.ReadFull(device, eventData[:]); err != nil {
log.Printf("read volume buttons: %v", err)
return
}
if delta := volumeStep(eventData); delta != 0 {
if err := changeVolume(delta); err != nil {
log.Printf("volume button: %v", err)
}
}
}
}
log.Print("volume buttons input device unavailable")
}
func volumeStep(event [24]byte) int {
eventType := binary.LittleEndian.Uint16(event[16:18])
code := binary.LittleEndian.Uint16(event[18:20])
value := binary.LittleEndian.Uint32(event[20:24])
if eventType != 1 || (value != 1 && value != 2) {
return 0
}
switch code {
case 114: // KEY_VOLUMEDOWN
return -5
case 115: // KEY_VOLUMEUP
return 5
}
return 0
}
func changeVolume(delta int) error {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
var sinks []struct {
Name string
Volume map[string]struct{ Value int }
}
data, err := exec.CommandContext(ctx, pulseControl, "--format=json", "list", "sinks").Output()
if err != nil {
return err
}
if err := json.Unmarshal(data, &sinks); err != nil {
return err
}
for _, sink := range sinks {
if sink.Name != "station" {
continue
}
channel, ok := sink.Volume["front-left"]
if !ok {
break
}
current := (channel.Value*100 + 32768) / 65536
next := max(0, min(100, current+delta))
if next == current {
return nil
}
volume := strconv.Itoa(next)
if err := exec.CommandContext(ctx, pulseControl, "set-sink-volume", "station", volume+"%").Run(); err != nil {
return err
}
if err := os.WriteFile(volumeFile+".tmp", []byte(volume+"\n"), 0600); err != nil {
return err
}
return os.Rename(volumeFile+".tmp", volumeFile)
}
return errors.New("station sink unavailable")
}