Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 7 additions & 8 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,17 @@
# ghcr.io/<owner>/marusya-server
# ghcr.io/<owner>/marusya-wake
#
# Tags: "latest" and "master" from master, "1.2.3" / "1.2" from v1.2.3 tags,
# "sha-<short>" from every push. Pull requests only build, nothing is pushed.
# Each platform is built on a native runner (no QEMU), then the digests are
# merged into one multi-arch manifest.
#
# The plugin catalog under registry/ has its own workflow (registry.yml).
# Pushes that only touch docs, YAML or the license build nothing.
# Tags: "latest" and "master" from master, "dev" and "stage" from stage,
# "1.2.3" / "1.2" from v1.2.3 tags, "sha-<short>" from every push.
# Pull requests only build, nothing is pushed. Each platform is built on a
# native runner (no QEMU), then the digests are merged into one multi-arch
# manifest.

name: docker

on:
push:
branches: [master]
branches: [master, stage]
tags: ["v*"]
paths-ignore:
- "registry/**"
Expand Down Expand Up @@ -164,6 +162,7 @@ jobs:
type=semver,pattern={{major}}.{{minor}}
type=sha
type=raw,value=latest,enable={{is_default_branch}}
type=raw,value=dev,enable=${{ github.ref == 'refs/heads/stage' }}

- uses: docker/setup-buildx-action@v3

Expand Down
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,9 +199,10 @@ docker compose -f deploy/server/docker-compose.yaml up -d --build
docker compose -f deploy/server/docker-compose.yaml logs -f
```

Готовые образы для amd64 и arm64 CI собирает из `master` и тегов `v*`
Готовые образы для amd64 и arm64 CI собирает из `master`, `stage` и тегов `v*`
(`.github/workflows/docker.yml`): `ghcr.io/yawaflua/marusya-server` и
`ghcr.io/yawaflua/marusya-wake`. Чтобы не собирать локально:
`ghcr.io/yawaflua/marusya-wake`. Тег `latest` соответствует `master`, тег `dev`
соответствует `stage`. Чтобы не собирать локально:

```bash
docker compose -f deploy/server/docker-compose.yaml pull
Expand Down Expand Up @@ -841,6 +842,9 @@ log.Fatal(p.Run())
на колонке можно оставить пустым;
- событие `state` сообщает, что делает Маруся: `idle`, `listening`, `thinking`, `speaking`,
`error`, `announcing`, `disconnected`. Например, по `listening` можно приглушить свою музыку;
- событие `volume` приходит при любой смене громкости колонки (голосом, кнопками, из панели
или другим плагином): `data.level` от 0 до 100 и `data.muted`. По нему `airplay` двигает
ползунок громкости на телефоне;
- функции плагина колонки попадают в список функций модели, когда колонка подключается
(`hello.plugin_tools`). Модель вызывает их, сервер отправляет вызов на колонку, агент
передаёт его плагину;
Expand Down
9 changes: 8 additions & 1 deletion cmd/marusya-agent/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import (
"github.com/yawaflua/marusya-gemini/internal/controller/mpv"
"github.com/yawaflua/marusya-gemini/internal/controller/playerctl"
"github.com/yawaflua/marusya-gemini/internal/controller/stream"
"github.com/yawaflua/marusya-gemini/internal/controller/volumecmd"
"github.com/yawaflua/marusya-gemini/internal/dbusapi"
"github.com/yawaflua/marusya-gemini/internal/display/seg7"
"github.com/yawaflua/marusya-gemini/internal/evdev"
Expand Down Expand Up @@ -93,6 +94,7 @@ var (

mixerCard = flag.String("mixer_card", config.Env("MARUSYA_MIXER_CARD", ""), "ALSA card for volume control.")
mixerControl = flag.String("mixer_control", config.Env("MARUSYA_MIXER_CONTROL", "Master"), "ALSA simple mixer control for volume. Empty disables volume control.")
volumeCmd = flag.String("volume_cmd", config.Env("MARUSYA_VOLUME_CMD", ""), "External volume program, called as `<cmd> get`, `<cmd> set <0..100>` and `<cmd> mute on|off`. Takes precedence over mixer_control.")

bluetooth = flag.Bool("bluetooth", config.EnvBool("MARUSYA_BLUETOOTH", true), "Enable Bluetooth control via bluetoothctl.")
btPairCommand = flag.String("bt_pair_command", config.Env("MARUSYA_BT_PAIR_COMMAND", "auto"), `Pairing tool, e.g. "bt-pair" or "bt-pair {timeout}". "auto" uses bt-pair if it is in PATH; empty uses the built-in bluetoothctl flow.`)
Expand Down Expand Up @@ -228,7 +230,9 @@ func run() error {

// Device controllers. Leave interfaces nil to disable an area.
dispatcher := &controller.Dispatcher{}
if *mixerControl != "" {
if *volumeCmd != "" {
dispatcher.Volume = &volumecmd.Volume{Command: *volumeCmd}
} else if *mixerControl != "" {
dispatcher.Volume = &amixer.Volume{Card: *mixerCard, Control: *mixerControl}
}
if *bluetooth {
Expand Down Expand Up @@ -384,6 +388,9 @@ func run() error {
if *pluginsDir != "" {
plugins = newSpeakerPlugins(*pluginsDir, pluginsdk.Device{ID: id, Language: *language, Location: *location, TimeZone: *timezone}, dispatcher, log)
indicators = append(indicators, plugins)
if dispatcher.Volume != nil {
dispatcher.Volume = &controller.ObservedVolume{Volume: dispatcher.Volume, OnChange: plugins.Volume}
}
}
deps := agent.Deps{
Capture: alsa.NewCapture(alsaCfg, models.DeviceInputFormat),
Expand Down
6 changes: 6 additions & 0 deletions cmd/marusya-agent/plugins.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,12 @@ func (p *speakerPlugins) Show(st indicator.State) {
p.m.Event(pluginsdk.Event{Kind: pluginsdk.EventState, Device: p.dev.ID, Data: map[string]any{"state": st.String()}})
}

// Volume tells the plugins that the speaker's volume changed; it is the
// OnChange of a [controller.ObservedVolume].
func (p *speakerPlugins) Volume(st models.VolumeState) {
p.m.Event(pluginsdk.Event{Kind: pluginsdk.EventVolume, Device: p.dev.ID, Data: map[string]any{"level": st.Level, "muted": st.Muted}})
}

// Command implements [plugin.Handler]: actions run on this speaker.
func (p *speakerPlugins) Command(ctx context.Context, plugin, _, action string, args map[string]any) (map[string]any, error) {
res := p.dispatcher.Execute(ctx, &models.Command{ID: "plugin-" + plugin, Action: models.Action(action), Args: args})
Expand Down
4 changes: 4 additions & 0 deletions configs/agent.conf.d.example
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ MARUSYA_TIMEZONE=
MARUSYA_CAPTURE_DEVICE=default
MARUSYA_PLAYBACK_DEVICE=default
MARUSYA_MIXER_CONTROL=Master
# External volume program, called as `<cmd> get`, `<cmd> set <0..100>`
# and `<cmd> mute on|off`. Takes precedence over MARUSYA_MIXER_CONTROL.
# See configs/volume-capsule-ipc.example.sh.
MARUSYA_VOLUME_CMD=

# Keyword spotter: reads 16 kHz mono S16LE on stdin, prints a line per hit.
MARUSYA_WAKE_COMMAND=
Expand Down
47 changes: 47 additions & 0 deletions configs/volume-capsule-ipc.example.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#!/bin/sh
# Example volume program for marusya-agent -volume_cmd
# (MARUSYA_VOLUME_CMD). It adapts capsule-ipc, the vendor IPC client that the
# VK Capsule Mini panel uses, to the generic contract:
#
# get -> "<level> <state>"
# set <0..100> -> set the level
# mute on|off -> mute or unmute
#
# Install as /usr/local/bin/marusya-volume, chmod 755, then set
# MARUSYA_VOLUME_CMD=/usr/local/bin/marusya-volume.
#
# Why not amixer: on this speaker the panel and AirPlay write the level
# through the same IPC, so changing only the ALSA control makes the panel
# and the phone slider disagree. capsule-ipc goes through that IPC.
set -eu

IPC=${CAPSULE_IPC:-/usr/local/bin/capsule-ipc}

case "${1:-}" in
get)
# capsule-ipc GET replies "OK <level> <muted> ...".
set -- $("$IPC" GET)
[ "${1:-}" = OK ] || { echo "$IPC GET: unexpected reply" >&2; exit 1; }
level=${2:?}
if [ "${3:-0}" = 1 ]; then state=muted; else state=unmuted; fi
printf '%s %s\n' "$level" "$state"
;;
set)
level=${2:?}
"$IPC" "VOL $level" >/dev/null
;;
mute)
want=${2:?}
set -- $("$IPC" GET)
muted=${3:-0}
case "$want:$muted" in
on:1 | off:0) exit 0 ;;
on:0 | off:1) "$IPC" MUTE >/dev/null ;;
*) echo "$0 mute: expected on or off, got $want" >&2; exit 2 ;;
esac
;;
*)
echo "usage: $0 get | set <0..100> | mute on|off" >&2
exit 2
;;
esac
Loading
Loading