Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6a6075376 | ||
|
|
71d7afdc51 | ||
|
|
76af77484e | ||
|
|
6a1b51e2b4 | ||
|
|
2595e603e8 | ||
|
|
20296973bd | ||
|
|
a881399d23 | ||
|
|
51426559c8 | ||
|
|
ec192b0ee8 | ||
|
|
f3f97bb7f7 | ||
|
|
48043d06e9 | ||
|
|
95f2e58c3a | ||
|
|
44870ca149 | ||
|
|
9e2074ccf9 |
@@ -1,6 +1,24 @@
|
||||
# Wyoming Satellite
|
||||
|
||||
Remote satellite using the [Wyoming protocol](https://github.com/rhasspy/wyoming).
|
||||
Remote voice satellite using the [Wyoming protocol](https://github.com/rhasspy/wyoming).
|
||||
|
||||
* Works with [Home Assistant](https://www.home-assistant.io/integrations/wyoming)
|
||||
* Local wake word detection using [Wyoming services](https://github.com/rhasspy/wyoming#wyoming-projects)
|
||||
* Audio enhancements using [webrtc](https://github.com/rhasspy/webrtc-noise-gain/)
|
||||
|
||||
See [the tutorial](docs/tutorial_2mic.md) to build a satellite using a [Raspberry Pi Zero 2 W](https://www.raspberrypi.com/products/raspberry-pi-zero-2-w/) and a [ReSpeaker 2Mic HAT](https://wiki.keyestudio.com/Ks0314_keyestudio_ReSpeaker_2-Mic_Pi_HAT_V1.0).
|
||||
|
||||
Video tutorials:
|
||||
|
||||
* [Wyoming Voice Satellite with ChatGPT](https://www.youtube.com/watch?v=eTKgc0YDCwE)
|
||||
* [Local ChatGPT Voice Assistant](https://www.youtube.com/watch?v=pAKqKTkx5X4)
|
||||
|
||||
---
|
||||
|
||||
Requires:
|
||||
|
||||
* Python 3.7+ (tested on 3.9+)
|
||||
* A microphone
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -94,11 +112,13 @@ script/run \
|
||||
--mic-command 'arecord -r 16000 -c 1 -f S16_LE -t raw' \
|
||||
--snd-command 'aplay -r 22050 -c 1 -f S16_LE -t raw' \
|
||||
--wake-uri 'tcp://127.0.0.1:10400' \
|
||||
--wake-word 'ok_nabu'
|
||||
--wake-word-name 'ok_nabu'
|
||||
```
|
||||
|
||||
Audio will only be streamed to the server after the wake word has been detected.
|
||||
|
||||
Note that `--vad` is unnecessary when connecting to a local instance of openwakeword.
|
||||
|
||||
## Sounds
|
||||
|
||||
You can play a WAV file when the wake word is detected (locally or remotely), and when speech-to-text has completed:
|
||||
@@ -125,7 +145,7 @@ script/run \
|
||||
--mic-noise-suppression 2
|
||||
```
|
||||
|
||||
Automatic gain control is between 0-31, which 31 being the loudest.
|
||||
Automatic gain control is between 0-31 dbFS, which 31 being the loudest.
|
||||
Noise suppression is from 0-4, with 4 being maximum suppression (may cause audio distortion).
|
||||
|
||||
You can also use `--mic-volume-multiplier X` to multiply all audio samples by `X`. For example, using 2 for `X` will double the microphone volume (but may cause audio distortion). The corresponding `--snd-volume-multiplier` does the same for audio playback.
|
||||
|
||||
@@ -0,0 +1,387 @@
|
||||
# Tutorial with 2mic HAT
|
||||
|
||||
Create a voice satellite using a [Raspberry Pi Zero 2 W](https://www.raspberrypi.com/products/raspberry-pi-zero-2-w/) and a [ReSpeaker 2Mic HAT](https://wiki.keyestudio.com/Ks0314_keyestudio_ReSpeaker_2-Mic_Pi_HAT_V1.0).
|
||||
|
||||
This tutorial should work for almost any Raspberry Pi and USB microphone. Audio enhancements and local wake word detection may require a 64-bit operating system, however.
|
||||
|
||||
## Install OS
|
||||
|
||||
Follow instructions to [install Raspberry Pi OS](https://www.raspberrypi.com/software/). Under "Choose OS", pick "Raspberry Pi OS (other)" and "Raspberry Pi OS (Legacy, **64-bit**) Lite".
|
||||
|
||||
When asking if you'd like to apply customization settings, choose "Edit Settings" and:
|
||||
|
||||
* Set a username/password
|
||||
* Configure the wireless LAN
|
||||
* Under the Services tab, enable SSH and use password authentication
|
||||
|
||||
## Install Software
|
||||
|
||||
After flashing and booting the satellite, connect to it over SSH using the username/password you configured during flashing.
|
||||
|
||||
**On the satellite**, make sure system dependencies are installed:
|
||||
|
||||
```sh
|
||||
sudo apt-get update
|
||||
sudo apt-get install --no-install-recommends \
|
||||
git \
|
||||
python3-venv
|
||||
```
|
||||
|
||||
Clone the `wyoming-satellite` repository:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/rhasspy/wyoming-satellite.git
|
||||
```
|
||||
|
||||
If you have the ReSpeaker 2Mic or 4Mic HAT, install the drivers:
|
||||
|
||||
```sh
|
||||
cd wyoming-satellite/
|
||||
sudo bash etc/install-respeaker-drivers.sh
|
||||
```
|
||||
|
||||
After install the drivers, you must reboot the satellite:
|
||||
|
||||
```sh
|
||||
sudo reboot
|
||||
```
|
||||
|
||||
Once the satellite has rebooted, reconnect over SSH and continue the installation:
|
||||
|
||||
```sh
|
||||
cd wyoming-satellite/
|
||||
python3 -m venv .venv
|
||||
.venv/bin/pip3 install --upgrade pip
|
||||
.venv/bin/pip3 install --upgrade wheel setuptools
|
||||
.venv/bin/pip3 install \
|
||||
-f 'https://synesthesiam.github.io/prebuilt-apps/' \
|
||||
-r requirements.txt \
|
||||
-r requirements_extra.txt
|
||||
```
|
||||
|
||||
If the installation was successful, you should be able to run:
|
||||
|
||||
```sh
|
||||
script/run --help
|
||||
```
|
||||
|
||||
## Determine Audio Devices
|
||||
|
||||
Picking the correct microphone/speaker devices is critical for the satellite to work. We'll do a test recording and playback in this section.
|
||||
|
||||
List your available microphones with:
|
||||
|
||||
```sh
|
||||
arecord -L
|
||||
```
|
||||
|
||||
If you have the ReSpeaker 2Mic HAT, you should see:
|
||||
|
||||
```
|
||||
plughw:CARD=seeed2micvoicec,DEV=0
|
||||
seeed-2mic-voicecard, bcm2835-i2s-wm8960-hifi wm8960-hifi-0
|
||||
Hardware device with all software conversions
|
||||
```
|
||||
|
||||
For other microphones, prefer ones that start with `plughw:` or just use `default` if you don't know what to use.
|
||||
|
||||
Record a 5 second sample from your chosen microphone:
|
||||
|
||||
```sh
|
||||
arecord -D plughw:CARD=seeed2micvoicec,DEV=0 -r 16000 -c 1 -f S16_LE -t wav -d 5 test.wav
|
||||
```
|
||||
|
||||
Say something while `arecord` is running. If you get errors, try a different microphone device by changing `-D <device>`.
|
||||
|
||||
List your available speakers with:
|
||||
|
||||
```sh
|
||||
aplay -L
|
||||
```
|
||||
|
||||
If you have the ReSpeaker 2Mic HAT, you should see:
|
||||
|
||||
```
|
||||
plughw:CARD=seeed2micvoicec,DEV=0
|
||||
seeed-2mic-voicecard, bcm2835-i2s-wm8960-hifi wm8960-hifi-0
|
||||
Hardware device with all software conversions
|
||||
```
|
||||
|
||||
For other speakers, prefer ones that start with `plughw:` or just use `default` if you don't know what to use.
|
||||
|
||||
Play back your recorded sample WAV:
|
||||
|
||||
```sh
|
||||
aplay -D plughw:CARD=seeed2micvoicec,DEV=0 test.wav
|
||||
```
|
||||
|
||||
You should hear your recorded sample. If there are problems, try a different speaker device by changing `-D <device>`.
|
||||
|
||||
Make note of your microphone and speaker devices for the next step.
|
||||
|
||||
## Running the Satellite
|
||||
|
||||
In the `wyoming-satellite` directory, run:
|
||||
|
||||
```sh
|
||||
script/run \
|
||||
--debug \
|
||||
--name 'my satellite' \
|
||||
--uri 'tcp://0.0.0.0:10700' \
|
||||
--mic-command 'arecord -D plughw:CARD=seeed2micvoicec,DEV=0 -r 16000 -c 1 -f S16_LE -t raw' \
|
||||
--snd-command 'aplay -D plughw:CARD=seeed2micvoicec,DEV=0 -r 22050 -c 1 -f S16_LE -t raw'
|
||||
```
|
||||
|
||||
Change the `-D <device>` for `arecord` and `aplay` to match the audio devices from the previous section.
|
||||
You can set `--name <NAME>` to whatever you want, but it should stay the same every time you run the satellite.
|
||||
|
||||
In Home Assistant, check the "Devices & services" section in Settings. After some time, you should see your satellite show up as "Discovered" (Wyoming Protocol). Click the "Configure" button and "Submit". Choose the area that your satellite is located, and click "Finish".
|
||||
|
||||
Your satellite should say "Streaming audio", and you can use the wake word of your preferred pipeline.
|
||||
|
||||
## Create Services
|
||||
|
||||
You can run wyoming-satellite as a systemd service by first creating a service file:
|
||||
|
||||
``` sh
|
||||
sudo systemctl edit --force --full wyoming-satellite.service
|
||||
```
|
||||
|
||||
Paste in the following template, and change both `/home/pi` and the `script/run` arguments to match your set up:
|
||||
|
||||
```text
|
||||
[Unit]
|
||||
Description=Wyoming Satellite
|
||||
Wants=network-online.target
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/home/pi/wyoming-satellite/script/run --name 'my satellite' --uri 'tcp://0.0.0.0:10700' --mic-command 'arecord -D plughw:CARD=seeed2micvoicec,DEV=0 -r 16000 -c 1 -f S16_LE -t raw' --snd-command 'aplay -D plughw:CARD=seeed2micvoicec,DEV=0 -r 22050 -c 1 -f S16_LE -t raw'
|
||||
WorkingDirectory=/home/pi/wyoming-satellite
|
||||
Restart=always
|
||||
RestartSec=1
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
```
|
||||
|
||||
Save the file and exit your editor. Next, enable the service to start at boot and run it:
|
||||
|
||||
``` sh
|
||||
sudo systemctl enable --now wyoming-satellite.service
|
||||
```
|
||||
|
||||
(you may need to hit CTRL+C to get back to a shell prompt)
|
||||
|
||||
With the service running, you can view logs in real-time with:
|
||||
|
||||
``` sh
|
||||
journalctl -u wyoming-satellite.service -f
|
||||
```
|
||||
|
||||
If needed, disable and stop the service with:
|
||||
|
||||
``` sh
|
||||
sudo systemctl disable --now wyoming-satellite.service
|
||||
```
|
||||
|
||||
## Audio Enhancements
|
||||
|
||||
You can run the satellite with automatic gain control and noise suppression:
|
||||
|
||||
``` sh
|
||||
script/run \
|
||||
... \
|
||||
--mic-auto-gain 5 \
|
||||
--mic-noise-suppression 2
|
||||
```
|
||||
|
||||
Automatic gain control is between 0-31 dbFS, which 31 being the loudest.
|
||||
Noise suppression is from 0-4, with 4 being maximum suppression (may cause audio distortion).
|
||||
|
||||
You can also use `--mic-volume-multiplier X` to multiply all audio samples by `X`. For example, using 2 for `X` will double the microphone volume (but may cause audio distortion). The corresponding `--snd-volume-multiplier` does the same for audio playback.
|
||||
|
||||
## Local Wake Word Detection
|
||||
|
||||
Install the necessary system dependencies:
|
||||
|
||||
```sh
|
||||
sudo apt-get update
|
||||
sudo apt-get install --no-install-recommends \
|
||||
libopenblas-dev
|
||||
```
|
||||
|
||||
From your home directory, install the openWakeWord Wyoming service:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/rhasspy/wyoming-openwakeword.git
|
||||
cd wyoming-openwakeword
|
||||
script/setup
|
||||
```
|
||||
|
||||
Create a systemd service for it:
|
||||
|
||||
``` sh
|
||||
sudo systemctl edit --force --full wyoming-openwakeword.service
|
||||
```
|
||||
|
||||
Paste in the following template, and change both `/home/pi` and the `script/run` arguments to match your set up:
|
||||
|
||||
```text
|
||||
[Unit]
|
||||
Description=Wyoming openWakeWord
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/home/pi/wyoming-openwakeword/script/run --uri 'tcp://127.0.0.1:10400'
|
||||
WorkingDirectory=/home/pi/wyoming-openwakeword
|
||||
Restart=always
|
||||
RestartSec=1
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
```
|
||||
|
||||
Save the file and exit your editor.
|
||||
|
||||
You can now update your satellite service:
|
||||
|
||||
``` sh
|
||||
sudo systemctl edit --force --full wyoming-satellite.service
|
||||
```
|
||||
|
||||
Update just the parts below:
|
||||
|
||||
```text
|
||||
[Unit]
|
||||
...
|
||||
Requires=wyoming-openwakeword.service
|
||||
|
||||
[Service]
|
||||
...
|
||||
ExecStart=/home/pi/wyoming-satellite/script/run ... --wake-uri 'tcp://127.0.0.1:10400' --wake-word-name 'ok_nabu'
|
||||
...
|
||||
|
||||
[Install]
|
||||
...
|
||||
```
|
||||
|
||||
Reload and restart the satellite service:
|
||||
|
||||
``` sh
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl restart wyoming-satellite.service
|
||||
```
|
||||
|
||||
You should see the wake service get automatically loaded:
|
||||
|
||||
``` sh
|
||||
sudo systemctl status wyoming-satellite.service wyoming-openwakeword.service
|
||||
```
|
||||
|
||||
They should all be "active (running)" and green.
|
||||
|
||||
Test out your satellite by saying "ok, nabu" and a voice command. Use `journalctl` to check the logs of services for errors:
|
||||
|
||||
``` sh
|
||||
journalctl -u wyoming-openwakeword.service -f
|
||||
```
|
||||
|
||||
Make sure to run `sudo systemctl daemon-reload` every time you make changes to the service.
|
||||
|
||||
## LED Service
|
||||
|
||||
Example event services for the ReSpeaker 2Mic and 4Mic HATs are included in `wyoming-satellite/examples` that will change the LED color depending on the satellite state. The example below is for the 2Mic HAT, using `2mic_service.py`. If you're using the 4Mic HAT, use `4mic_service.py` instead as the LEDs and GPIO pins are slightly different.
|
||||
|
||||
Install it from your home directory:
|
||||
|
||||
```sh
|
||||
cd wyoming-satellite/examples
|
||||
python3 -m venv --system-site-packages .venv
|
||||
.venv/bin/pip3 install --upgrade pip
|
||||
.venv/bin/pip3 install --upgrade wheel setuptools
|
||||
.venv/bin/pip3 install 'wyoming==1.4.1'
|
||||
```
|
||||
|
||||
The `--system-site-packages` argument is used to access the pre-installed `gpiozero` and `spidev` Python packages. If these are **not already installed** in your system, run:
|
||||
|
||||
```sh
|
||||
sudo apt-get install python3-spidev python3-gpiozero
|
||||
```
|
||||
|
||||
Test the service with:
|
||||
|
||||
```sh
|
||||
.venv/bin/python3 2mic_service.py --help
|
||||
```
|
||||
|
||||
Create a systemd service for it:
|
||||
|
||||
``` sh
|
||||
sudo systemctl edit --force --full 2mic_leds.service
|
||||
```
|
||||
|
||||
Paste in the following template, and change both `/home/pi` and the `script/run` arguments to match your set up:
|
||||
|
||||
```text
|
||||
[Unit]
|
||||
Description=2Mic LEDs
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/home/pi/wyoming-satellite/examples/.venv/bin/python3 2mic_service.py --uri 'tcp://127.0.0.1:10500'
|
||||
WorkingDirectory=/home/pi/wyoming-satellite/examples
|
||||
Restart=always
|
||||
RestartSec=1
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
```
|
||||
|
||||
Save the file and exit your editor.
|
||||
|
||||
You can now update your satellite service:
|
||||
|
||||
``` sh
|
||||
sudo systemctl edit --force --full wyoming-satellite.service
|
||||
```
|
||||
|
||||
Update just the parts below:
|
||||
|
||||
```text
|
||||
[Unit]
|
||||
...
|
||||
Requires=2mic_leds.service
|
||||
|
||||
[Service]
|
||||
...
|
||||
ExecStart=/home/pi/wyoming-satellite/script/run ... --event-uri 'tcp://127.0.0.1:10500'
|
||||
...
|
||||
|
||||
[Install]
|
||||
...
|
||||
```
|
||||
|
||||
Reload and restart the satellite service:
|
||||
|
||||
``` sh
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl restart wyoming-satellite.service
|
||||
```
|
||||
|
||||
You should see the service get automatically loaded:
|
||||
|
||||
``` sh
|
||||
sudo systemctl status wyoming-satellite.service 2mic_leds.service
|
||||
```
|
||||
|
||||
They should all be "active (running)" and green.
|
||||
|
||||
Try a voice command and see if the LEDs change. Use `journalctl` to check the logs of services for errors:
|
||||
|
||||
``` sh
|
||||
journalctl -u 2mic_leds.service -f
|
||||
```
|
||||
|
||||
Make sure to run `sudo systemctl daemon-reload` every time you make changes to the service.
|
||||
Executable
+66
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Installs drivers for the ReSpeaker 2mic and 4mic HATs on Raspberry Pi OS.
|
||||
# Must be run with sudo.
|
||||
# Requires: curl raspberrypi-kernel-headers dkms i2c-tools libasound2-plugins alsa-utils
|
||||
|
||||
set -eo pipefail
|
||||
|
||||
apt-get update
|
||||
apt-get install --no-install-recommends --yes \
|
||||
curl raspberrypi-kernel-headers dkms i2c-tools libasound2-plugins alsa-utils
|
||||
|
||||
temp_dir="$(mktemp -d)"
|
||||
|
||||
function finish {
|
||||
rm -rf "${temp_dir}"
|
||||
}
|
||||
|
||||
trap finish EXIT
|
||||
|
||||
pushd "${temp_dir}"
|
||||
|
||||
# Download source code to temporary directory
|
||||
# NOTE: There are different branches in the repo for different kernel versions.
|
||||
# We use v6.1 here.
|
||||
echo 'Downloading source code'
|
||||
curl -L -o - 'https://github.com/HinTak/seeed-voicecard/archive/refs/heads/v6.1.tar.gz' | \
|
||||
tar -xzf -
|
||||
|
||||
cd seeed-voicecard-6.1/
|
||||
|
||||
# 1. Build kernel module
|
||||
echo 'Building kernel module'
|
||||
ver='0.3'
|
||||
mod='seeed-voicecard'
|
||||
src='./'
|
||||
kernel="$(uname -r)"
|
||||
marker='0.0.0'
|
||||
|
||||
mkdir -p "/usr/src/${mod}-${ver}"
|
||||
cp -a "${src}"/* "/usr/src/${mod}-${ver}/"
|
||||
|
||||
dkms add -m "${mod}" -v "${ver}"
|
||||
dkms build -k "${kernel}" -m "${mod}" -v "${ver}" && {
|
||||
dkms install --force -k "${kernel}" -m "${mod}" -v "${ver}"
|
||||
}
|
||||
|
||||
mkdir -p "/var/lib/dkms/${mod}/${ver}/${marker}"
|
||||
|
||||
# 2. Install kernel module and configure
|
||||
echo 'Updating boot configuration'
|
||||
config='/boot/config.txt'
|
||||
|
||||
cp seeed-*-voicecard.dtbo /boot/overlays
|
||||
grep -q "^snd-soc-ac108$" /etc/modules || echo "snd-soc-ac108" >> /etc/modules
|
||||
sed -i -e 's:#dtparam=i2c_arm=on:dtparam=i2c_arm=on:g' "${config}"
|
||||
echo "dtoverlay=i2s-mmap" >> "${config}"
|
||||
echo "dtparam=i2s=on" >> "${config}"
|
||||
mkdir -p /etc/voicecard
|
||||
cp *.conf *.state /etc/voicecard
|
||||
cp seeed-voicecard /usr/bin/
|
||||
cp seeed-voicecard.service /lib/systemd/system/
|
||||
systemctl enable --now seeed-voicecard.service
|
||||
|
||||
echo 'Done. Please reboot the system.'
|
||||
popd
|
||||
@@ -0,0 +1,278 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Controls the LEDs on the ReSpeaker 4mic HAT."""
|
||||
import argparse
|
||||
import asyncio
|
||||
import logging
|
||||
import time
|
||||
from functools import partial
|
||||
from math import ceil
|
||||
from typing import Tuple
|
||||
|
||||
import gpiozero
|
||||
import spidev
|
||||
from wyoming.asr import Transcript
|
||||
from wyoming.event import Event
|
||||
from wyoming.satellite import RunSatellite, StreamingStarted, StreamingStopped
|
||||
from wyoming.server import AsyncEventHandler, AsyncServer
|
||||
from wyoming.vad import VoiceStarted
|
||||
from wyoming.wake import Detection
|
||||
|
||||
_LOGGER = logging.getLogger()
|
||||
|
||||
NUM_LEDS = 12
|
||||
LEDS_GPIO = 5
|
||||
RGB_MAP = {
|
||||
"rgb": [3, 2, 1],
|
||||
"rbg": [3, 1, 2],
|
||||
"grb": [2, 3, 1],
|
||||
"gbr": [2, 1, 3],
|
||||
"brg": [1, 3, 2],
|
||||
"bgr": [1, 2, 3],
|
||||
}
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Main entry point."""
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--uri", required=True, help="unix:// or tcp://")
|
||||
#
|
||||
parser.add_argument("--debug", action="store_true", help="Log DEBUG messages")
|
||||
args = parser.parse_args()
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG if args.debug else logging.INFO)
|
||||
_LOGGER.debug(args)
|
||||
|
||||
_LOGGER.info("Ready")
|
||||
|
||||
# Turn on power to LEDs
|
||||
led_power = gpiozero.LED(LEDS_GPIO, active_high=True)
|
||||
led_power.on()
|
||||
|
||||
leds = APA102(num_led=NUM_LEDS)
|
||||
|
||||
# Start server
|
||||
server = AsyncServer.from_uri(args.uri)
|
||||
|
||||
try:
|
||||
await server.run(partial(LEDsEventHandler, args, leds))
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
leds.cleanup()
|
||||
led_power.off()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
_BLACK = (0, 0, 0)
|
||||
_WHITE = (255, 255, 255)
|
||||
_RED = (255, 0, 0)
|
||||
_YELLOW = (255, 255, 0)
|
||||
_BLUE = (0, 0, 255)
|
||||
_GREEN = (0, 255, 0)
|
||||
|
||||
|
||||
class LEDsEventHandler(AsyncEventHandler):
|
||||
"""Event handler for clients."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
cli_args: argparse.Namespace,
|
||||
leds: "APA102",
|
||||
*args,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.cli_args = cli_args
|
||||
self.client_id = str(time.monotonic_ns())
|
||||
self.leds = leds
|
||||
|
||||
_LOGGER.debug("Client connected: %s", self.client_id)
|
||||
|
||||
async def handle_event(self, event: Event) -> bool:
|
||||
_LOGGER.debug(event)
|
||||
|
||||
if StreamingStarted.is_type(event.type):
|
||||
self.color(_YELLOW)
|
||||
elif Detection.is_type(event.type):
|
||||
self.color(_BLUE)
|
||||
await asyncio.sleep(1.0) # show for 1 sec
|
||||
elif VoiceStarted.is_type(event.type):
|
||||
self.color(_YELLOW)
|
||||
elif Transcript.is_type(event.type):
|
||||
self.color(_GREEN)
|
||||
await asyncio.sleep(1.0) # show for 1 sec
|
||||
elif StreamingStopped.is_type(event.type):
|
||||
self.color(_BLACK)
|
||||
elif RunSatellite.is_type(event.type):
|
||||
self.color(_BLACK)
|
||||
|
||||
return True
|
||||
|
||||
def color(self, rgb: Tuple[int, int, int]) -> None:
|
||||
for i in range(NUM_LEDS):
|
||||
self.leds.set_pixel(i, rgb[0], rgb[1], rgb[2])
|
||||
|
||||
self.leds.show()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class APA102:
|
||||
"""
|
||||
Driver for APA102 LEDS (aka "DotStar").
|
||||
(c) Martin Erzberger 2016-2017
|
||||
"""
|
||||
|
||||
# Constants
|
||||
MAX_BRIGHTNESS = 0b11111 # Safeguard: Set to a value appropriate for your setup
|
||||
LED_START = 0b11100000 # Three "1" bits, followed by 5 brightness bits
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
num_led,
|
||||
global_brightness=MAX_BRIGHTNESS,
|
||||
order="rgb",
|
||||
bus=0,
|
||||
device=1,
|
||||
max_speed_hz=8000000,
|
||||
):
|
||||
self.num_led = num_led # The number of LEDs in the Strip
|
||||
order = order.lower()
|
||||
self.rgb = RGB_MAP.get(order, RGB_MAP["rgb"])
|
||||
# Limit the brightness to the maximum if it's set higher
|
||||
if global_brightness > self.MAX_BRIGHTNESS:
|
||||
self.global_brightness = self.MAX_BRIGHTNESS
|
||||
else:
|
||||
self.global_brightness = global_brightness
|
||||
|
||||
self.leds = [self.LED_START, 0, 0, 0] * self.num_led # Pixel buffer
|
||||
self.spi = spidev.SpiDev() # Init the SPI device
|
||||
self.spi.open(bus, device) # Open SPI port 0, slave device (CS) 1
|
||||
# Up the speed a bit, so that the LEDs are painted faster
|
||||
if max_speed_hz:
|
||||
self.spi.max_speed_hz = max_speed_hz
|
||||
|
||||
def clock_start_frame(self):
|
||||
"""Sends a start frame to the LED strip.
|
||||
|
||||
This method clocks out a start frame, telling the receiving LED
|
||||
that it must update its own color now.
|
||||
"""
|
||||
self.spi.xfer2([0] * 4) # Start frame, 32 zero bits
|
||||
|
||||
def clock_end_frame(self):
|
||||
"""Sends an end frame to the LED strip.
|
||||
|
||||
As explained above, dummy data must be sent after the last real colour
|
||||
information so that all of the data can reach its destination down the line.
|
||||
The delay is not as bad as with the human example above.
|
||||
It is only 1/2 bit per LED. This is because the SPI clock line
|
||||
needs to be inverted.
|
||||
|
||||
Say a bit is ready on the SPI data line. The sender communicates
|
||||
this by toggling the clock line. The bit is read by the LED
|
||||
and immediately forwarded to the output data line. When the clock goes
|
||||
down again on the input side, the LED will toggle the clock up
|
||||
on the output to tell the next LED that the bit is ready.
|
||||
|
||||
After one LED the clock is inverted, and after two LEDs it is in sync
|
||||
again, but one cycle behind. Therefore, for every two LEDs, one bit
|
||||
of delay gets accumulated. For 300 LEDs, 150 additional bits must be fed to
|
||||
the input of LED one so that the data can reach the last LED.
|
||||
|
||||
Ultimately, we need to send additional numLEDs/2 arbitrary data bits,
|
||||
in order to trigger numLEDs/2 additional clock changes. This driver
|
||||
sends zeroes, which has the benefit of getting LED one partially or
|
||||
fully ready for the next update to the strip. An optimized version
|
||||
of the driver could omit the "clockStartFrame" method if enough zeroes have
|
||||
been sent as part of "clockEndFrame".
|
||||
"""
|
||||
|
||||
self.spi.xfer2([0xFF] * 4)
|
||||
|
||||
# Round up num_led/2 bits (or num_led/16 bytes)
|
||||
# for _ in range((self.num_led + 15) // 16):
|
||||
# self.spi.xfer2([0x00])
|
||||
|
||||
def set_pixel(self, led_num, red, green, blue, bright_percent=100):
|
||||
"""Sets the color of one pixel in the LED stripe.
|
||||
|
||||
The changed pixel is not shown yet on the Stripe, it is only
|
||||
written to the pixel buffer. Colors are passed individually.
|
||||
If brightness is not set the global brightness setting is used.
|
||||
"""
|
||||
if led_num < 0:
|
||||
return # Pixel is invisible, so ignore
|
||||
if led_num >= self.num_led:
|
||||
return # again, invisible
|
||||
|
||||
# Calculate pixel brightness as a percentage of the
|
||||
# defined global_brightness. Round up to nearest integer
|
||||
# as we expect some brightness unless set to 0
|
||||
brightness = int(ceil(bright_percent * self.global_brightness / 100.0))
|
||||
|
||||
# LED startframe is three "1" bits, followed by 5 brightness bits
|
||||
ledstart = (brightness & 0b00011111) | self.LED_START
|
||||
|
||||
start_index = 4 * led_num
|
||||
self.leds[start_index] = ledstart
|
||||
self.leds[start_index + self.rgb[0]] = red
|
||||
self.leds[start_index + self.rgb[1]] = green
|
||||
self.leds[start_index + self.rgb[2]] = blue
|
||||
|
||||
def set_pixel_rgb(self, led_num, rgb_color, bright_percent=100):
|
||||
"""Sets the color of one pixel in the LED stripe.
|
||||
|
||||
The changed pixel is not shown yet on the Stripe, it is only
|
||||
written to the pixel buffer.
|
||||
Colors are passed combined (3 bytes concatenated)
|
||||
If brightness is not set the global brightness setting is used.
|
||||
"""
|
||||
self.set_pixel(
|
||||
led_num,
|
||||
(rgb_color & 0xFF0000) >> 16,
|
||||
(rgb_color & 0x00FF00) >> 8,
|
||||
rgb_color & 0x0000FF,
|
||||
bright_percent,
|
||||
)
|
||||
|
||||
def rotate(self, positions=1):
|
||||
"""Rotate the LEDs by the specified number of positions.
|
||||
|
||||
Treating the internal LED array as a circular buffer, rotate it by
|
||||
the specified number of positions. The number could be negative,
|
||||
which means rotating in the opposite direction.
|
||||
"""
|
||||
cutoff = 4 * (positions % self.num_led)
|
||||
self.leds = self.leds[cutoff:] + self.leds[:cutoff]
|
||||
|
||||
def show(self):
|
||||
"""Sends the content of the pixel buffer to the strip.
|
||||
|
||||
Todo: More than 1024 LEDs requires more than one xfer operation.
|
||||
"""
|
||||
self.clock_start_frame()
|
||||
# xfer2 kills the list, unfortunately. So it must be copied first
|
||||
# SPI takes up to 4096 Integers. So we are fine for up to 1024 LEDs.
|
||||
data = list(self.leds)
|
||||
while data:
|
||||
self.spi.xfer2(data[:32])
|
||||
data = data[32:]
|
||||
self.clock_end_frame()
|
||||
|
||||
def cleanup(self):
|
||||
"""Release the SPI device; Call this method at the end"""
|
||||
|
||||
self.spi.close() # Close SPI port
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
asyncio.run(main())
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
wyoming==1.4.1
|
||||
wyoming==1.4.2
|
||||
zeroconf==0.88.0
|
||||
pyring-buffer==1.0.0
|
||||
|
||||
@@ -17,7 +17,7 @@ if requirements_path.is_file():
|
||||
|
||||
setup(
|
||||
name="wyoming_satellite",
|
||||
version="1.0.0",
|
||||
version="1.0.1",
|
||||
description="Wyoming server for remote voice satellite",
|
||||
url="http://github.com/rhasspy/wyoming-satellite",
|
||||
author="Michael Hansen",
|
||||
|
||||
@@ -439,8 +439,12 @@ def _split(command: Optional[str]) -> Optional[List[str]]:
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
if __name__ == "__main__":
|
||||
def run():
|
||||
try:
|
||||
asyncio.run(main())
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
|
||||
Reference in New Issue
Block a user