Restore Mini 2 hardware volume buttons
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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=$!
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
Reference in New Issue
Block a user