squawk
Differences
This shows you the differences between two versions of the page.
Both sides previous revisionPrevious revisionNext revision | Previous revision | ||
squawk [2016-10-09 22:28] – tim | squawk [2024-12-07 21:22] (current) – aria | ||
---|---|---|---|
Line 1: | Line 1: | ||
- | ====== | + | ====== |
- | Traditionally this was handled by doorpi. However, that pi has too much stuff hanging off it, and was very out of date. There was an issue of a loud pop occurring before any sound played, this was fixed in later firmware updates. Rob has now dedicated | + | Squawk is the Hacklab' |
+ | |||
+ | Traditionally this was handled by doorpi. However, that pi has too much stuff hanging off it, and was very out of date. There was an issue of a loud pop occurring before any sound played, this was fixed in later firmware updates. Rob has purchased | ||
===== Usage ===== | ===== Usage ===== | ||
Line 9: | Line 11: | ||
Examples for testing might be something like: | Examples for testing might be something like: | ||
+ | Play a sound file: | ||
+ | < | ||
+ | mosquitto_pub -h mqtt -t ' | ||
+ | </ | ||
+ | Play a list of sound files with no gap: | ||
+ | < | ||
+ | mosquitto_pub -h mqtt -t ' | ||
+ | </ | ||
+ | Text-to-speech message (with a subtle notification bong): | ||
< | < | ||
- | mosquitto_pub -h mqtt -t ' | ||
mosquitto_pub -h mqtt -t ' | mosquitto_pub -h mqtt -t ' | ||
</ | </ | ||
+ | Text-to-speech message (with a clear airport/ | ||
+ | < | ||
+ | mosquitto_pub -h mqtt -t ' | ||
+ | </ | ||
+ | |||
+ | ===== Adding new sounds ===== | ||
+ | |||
+ | < | ||
+ | |||
+ | For now, use scp to squawk.hacklab. | ||
- | ===== Raspbian | + | ===== Raspberry Pi OS ===== |
- | Latest | + | Latest Raspberry Pi OS, installed using Raspberry Pi Imager. |
==== raspi-config ==== | ==== raspi-config ==== | ||
Line 22: | Line 42: | ||
* Expand filesystem | * Expand filesystem | ||
* Medium overclock | * Medium overclock | ||
- | * Graphics mem reduced to 16M | ||
* Audio output forced to 3.5mm | * Audio output forced to 3.5mm | ||
* Hostname set | * Hostname set | ||
- | ==== RAM /tmp ==== | + | ==== RAM /tmp / |
+ | Appended to /etc/fstab: | ||
< | < | ||
- | sudo systemctl enable | + | tmpfs /var/log tmpfs defaults, |
+ | tmpfs /tmp tmpfs defaults, | ||
+ | tmpfs /var/tmp tmpfs defaults, | ||
+ | tmpfs /var/log tmpfs defaults, | ||
+ | tmpfs /var/run tmpfs defaults, | ||
+ | tmpfs / | ||
</ | </ | ||
Line 37: | Line 62: | ||
sudo apt-get install mpg123 | sudo apt-get install mpg123 | ||
sudo apt-get install sox | sudo apt-get install sox | ||
- | sudo apt-get install | + | sudo apt-get install |
- | sudo pip install paho-mqtt | + | sudo pip install |
</ | </ | ||
- | ==== / | + | ==== systemd service |
- | These lines added before exit: | + | / |
< | < | ||
- | su -c " | + | [Unit] |
- | su -c "/ | + | Description=Runs the squawk listener script |
- | </ | + | |
- | ===== Scripts ===== | + | |
- | + | ||
- | Various scripts to make things happen. | + | |
- | + | ||
- | ==== / | + | |
- | + | ||
- | <code python> | + | |
- | # | + | |
- | + | ||
- | import paho.mqtt.client as mqtt | + | |
- | import subprocess | + | |
- | import os | + | |
- | import logging | + | |
- | import signal | + | |
- | import time | + | |
- | import random | + | |
- | + | ||
- | logging.basicConfig(level=logging.INFO) | + | |
- | + | ||
- | max_playtime | + | |
- | sounds_path = "/ | + | |
- | + | ||
- | status = ' | + | |
- | + | ||
- | # runs a command and terminates it after a specified timeout | + | |
- | def call_with_timeout(command, | + | |
- | logging.info(' | + | |
- | class TimeoutException(Exception): | + | |
- | pass | + | |
- | def alrm_handler(signum, | + | |
- | raise TimeoutException() | + | |
- | try: | + | |
- | old_handler = signal.signal(signal.SIGALRM, | + | |
- | signal.alarm(timeout) | + | |
- | p = subprocess.Popen(command) | + | |
- | retcode = p.wait() | + | |
- | logging.info(' | + | |
- | except TimeoutException: | + | |
- | logging.info(' | + | |
- | p.terminate() | + | |
- | retcode = p.wait() | + | |
- | finally: | + | |
- | signal.signal(signal.SIGALRM, | + | |
- | signal.alarm(0) | + | |
- | return retcode | + | |
- | + | ||
- | # waffle waffle | + | |
- | def speak(data, timeout=max_playtime): | + | |
- | command = ['/ | + | |
- | | + | |
- | + | ||
- | def getfiles(path, | + | |
- | allfiles = [] | + | |
- | for dirpath, dirnames, filenames in os.walk(path): | + | |
- | for filename in filenames: | + | |
- | base, ext = os.path.splitext(filename) | + | |
- | if ext in exts: | + | |
- | # | + | |
- | allfiles.append(os.path.join(dirpath, | + | |
- | return allfiles | + | |
- | + | ||
- | # make some noise for the vengaboys | + | |
- | def play(filename, | + | |
- | allfiles = getfiles(sounds_path) | + | |
- | filename = os.path.join(sounds_path, | + | |
- | + | ||
- | if filename.endswith('/' | + | |
- | # pick a random file from a directory | + | |
- | candidates = [] | + | |
- | for f in allfiles: | + | |
- | if f.startswith(filename): | + | |
- | candidates.append(f) | + | |
- | if len(candidates) == 0: | + | |
- | raise Exception(' | + | |
- | filename = random.choice(candidates) | + | |
- | else: | + | |
- | # single file requested | + | |
- | if filename not in allfiles: | + | |
- | raise Exception(' | + | |
- | + | ||
- | base, ext = os.path.splitext(filename) | + | |
- | if ext == ' | + | |
- | command = [' | + | |
- | call_with_timeout(command, | + | |
- | else: | + | |
- | command = [' | + | |
- | call_with_timeout(command, | + | |
- | + | ||
- | def on_connect(client, | + | |
- | client.subscribe(" | + | |
- | client.subscribe(" | + | |
- | + | ||
- | def on_message(client, | + | |
- | + | ||
- | global status | + | |
- | + | ||
- | if msg.topic == ' | + | |
- | if msg.payload == ' | + | |
- | | + | |
- | else: | + | |
- | | + | |
- | + | ||
- | # ignore retained (non-realtime) messages | + | |
- | if msg.retain: | + | |
- | return | + | |
- | + | ||
- | if msg.topic == ' | + | |
- | play(msg.payload) | + | |
- | + | ||
- | if msg.topic == ' | + | |
- | play(' | + | |
- | speak(msg.payload) | + | |
- | + | ||
- | if msg.topic == ' | + | |
- | play(' | + | |
- | speak(msg.payload) | + | |
- | if msg.topic | + | [Service] |
- | if msg.payload | + | WorkingDirectory=/home/pi/ |
- | | + | ExecStart=/ |
+ | User=pi | ||
+ | Restart=always | ||
+ | RestartSec=3 | ||
- | m = mqtt.Client() | + | [Install] |
- | m.on_connect | + | WantedBy=multi-user.target |
- | m.on_message = on_message | + | |
- | m.connect(" | + | |
- | m.loop_forever() | + | |
</ | </ | ||
- | ==== / | + | ...and enabled with |
- | < | + | < |
- | # | + | $ sudo systemctl start squawk |
- | # | + | $ sudo systemctl enable squawk |
- | # Tim Hawes < | + | |
- | # April 2015 | + | |
- | # | + | |
- | + | ||
- | import argparse | + | |
- | import logging | + | |
- | import logging.handlers | + | |
- | import os | + | |
- | import signal | + | |
- | import subprocess | + | |
- | import sys | + | |
- | import time | + | |
- | + | ||
- | process = None | + | |
- | hup_received = False | + | |
- | term_received = False | + | |
- | + | ||
- | parser = argparse.ArgumentParser(description=' | + | |
- | parser.add_argument(' | + | |
- | parser.add_argument(' | + | |
- | parser.add_argument(' | + | |
- | parser.add_argument(' | + | |
- | parser.add_argument(' | + | |
- | parser.add_argument(' | + | |
- | parser.add_argument(' | + | |
- | parser.add_argument(' | + | |
- | args = parser.parse_args() | + | |
- | + | ||
- | def setup_logging(log_level=logging.INFO, | + | |
- | logger = logging.getLogger() | + | |
- | logger.setLevel(log_level) | + | |
- | if syslog: | + | |
- | syslog_format_string = ident + " | + | |
- | syslog_handler = logging.handlers.SysLogHandler(address="/ | + | |
- | syslog_handler.log_format_string = "< | + | |
- | syslog_handler.setFormatter(logging.Formatter(fmt=syslog_format_string)) | + | |
- | syslog_handler.setLevel(log_level) | + | |
- | logger.addHandler(syslog_handler) | + | |
- | if stdout: | + | |
- | stream_format_string = " | + | |
- | stream_handler = logging.StreamHandler(stream=sys.__stdout__) | + | |
- | stream_handler.setFormatter(logging.Formatter(fmt=stream_format_string)) | + | |
- | stream_handler.setLevel(log_level) | + | |
- | logger.addHandler(stream_handler) | + | |
- | + | ||
- | def run(): | + | |
- | global process | + | |
- | + | ||
- | start_time = time.time() | + | |
- | process = subprocess.Popen(args.command, | + | |
- | while True: | + | |
- | line = process.stdout.readline() | + | |
- | if line == '': | + | |
- | break | + | |
- | logging.info("< | + | |
- | returncode = process.wait() | + | |
- | runtime = time.time()-start_time | + | |
- | process = None | + | |
- | if returncode == 0: | + | |
- | logging.info(' | + | |
- | else: | + | |
- | logging.warning(' | + | |
- | return returncode, runtime | + | |
- | + | ||
- | def hup_handler(signum, | + | |
- | global process | + | |
- | global hup_received | + | |
- | + | ||
- | if process is not None: | + | |
- | logging.warning(" | + | |
- | hup_received = True | + | |
- | process.send_signal(signal.SIGTERM) | + | |
- | else: | + | |
- | logging.warning(" | + | |
- | + | ||
- | def term_handler(signum, | + | |
- | global process | + | |
- | global term_received | + | |
- | + | ||
- | if process is not None: | + | |
- | logging.warning(" | + | |
- | term_received = True | + | |
- | process.send_signal(signal.SIGTERM) | + | |
- | else: | + | |
- | logging.warning(" | + | |
- | term_received = True | + | |
- | + | ||
- | signal.signal(signal.SIGHUP, | + | |
- | signal.signal(signal.SIGTERM, | + | |
- | + | ||
- | ident = os.path.basename(sys.argv[0]) | + | |
- | if args.name is not None: | + | |
- | ident = ident + "/" | + | |
- | + | ||
- | if args.debug: | + | |
- | setup_logging(log_level=logging.DEBUG, | + | |
- | else: | + | |
- | setup_logging(log_level=logging.INFO, | + | |
- | + | ||
- | if args.delay > args.min_backoff: | + | |
- | logging.debug(' | + | |
- | args.min_backoff = args.delay | + | |
- | if args.min_backoff > args.max_backoff: | + | |
- | logging.debug(' | + | |
- | args.max_backoff = args.min_backoff | + | |
- | + | ||
- | exit_requested = False | + | |
- | backoff = args.min_backoff | + | |
- | + | ||
- | logging.info(" | + | |
- | + | ||
- | while True: | + | |
- | start_time = time.time() | + | |
- | returncode, runtime = run() | + | |
- | if term_received: | + | |
- | logging.debug(" | + | |
- | break | + | |
- | if hup_received: | + | |
- | logging.debug(" | + | |
- | hup_received = False | + | |
- | continue | + | |
- | if returncode == 0: | + | |
- | if runtime > args.backoff_reset_after: | + | |
- | backoff = args.min_backoff | + | |
- | logging.debug(' | + | |
- | else: | + | |
- | logging.debug(' | + | |
- | time.sleep(args.delay) | + | |
- | else: | + | |
- | logging.info(' | + | |
- | time.sleep(backoff) | + | |
- | backoff = min(backoff*2, | + | |
- | logging.debug(' | + | |
- | + | ||
- | logging.info(' | + | |
</ | </ | ||
- | ==== / | + | ===== Scripts ===== |
- | < | + | The code for squawk is stored in < |
- | #!/bin/bash | + | |
- | pico2wave -l en-GB -w /tmp/ | + | |
- | play -q / | + | |
- | rm -f / | + | |
- | </ | + | |
===== Audio files ===== | ===== Audio files ===== | ||
- | These live in / | + | These live in / |
< | < | ||
Line 350: | Line 118: | ||
</ | </ | ||
- | ===== Pico TTS ===== | + | ===== Hardware |
- | < | + | {{ : |
- | mkdir ~/pico | + | |
- | cd ~/pico | + | The " |
- | wget http:// | + | |
- | tar -zxf picotts-raspi.tar.gz | + | There are four speakers connected, located in each of the rooms on the ceiling. The speakers are 8 Ohm moisture resistant cheap ceiling speakers that have exceeded expectations and produce a surprisingly full sound. This opens up the potential for using them for background music in the future (e.g. via MPD). |
- | sudo cp -R usr / | + | |
- | cd / | + | A relay board selects which speakers a played sound is directed to. The left channel goes to two relays, and the right to two others, so by selecting channels and relays, the speakers can be selected. |
- | sudo dpkg -i libttspico-data_1.0+git20110131-2_all.deb | + | |
- | sudo dpkg -i libttspico0_1.0+git20110131-2_armhf.deb | + | Note: photo shows older version, prior to addition of relay board. |
- | sudo dpkg -i libttspico-utils_1.0+git20110131-2_armhf.deb | + | |
- | rm -rf ~/pico | + | ==== Raspberry Pi ==== |
- | </ | + | |
+ | Model B. Powered via the header rather than USB. Replaced by River Dec 2024 after previous one suffered a smoke containment issue related to repair of the power supply. | ||
- | ===== Hardware | + | ==== Amplifier |
+ | |||
+ | * TPA3116 | ||
+ | * "100 W" | ||
+ | * 12-24v (more volts = more power) | ||
+ | * Volume pot | ||
+ | * 3-pin header input or 3.5mm socket | ||
+ | |||
+ | Audio is input via the 3-pin header. | ||
+ | |||
+ | Power is input on the centre screw terminal, with left and right speaker outputs either side. | ||
+ | |||
+ | ==== Power Supply ==== | ||
+ | |||
+ | The system runs off a 19.5v laptop PSU located in the ceiling space. This is directly connected into the amplifier board, which is rated up to 24v. A spur from this connects to a DC-DC buck converter, supplying the Raspberry Pi with 5v. The original mini converter used was overheating and melting the heatshrink. It's now been replaced with a larger type which so far is running relatively cool. | ||
+ | |||
+ | Care should be taken when re-wiring as connecting the laptop PSU accidentally reverse polarity, to the speaker terminals, or to the speakers themselves will likely result in one or more of those being instantly destroyed. | ||
+ | |||
+ | ==== Speakers ==== | ||
+ | |||
+ | * 8 Ohm | ||
+ | * 80 W | ||
+ | * Spade terminals | ||
+ | * Dual cone | ||
+ | |||
+ | Located in the ceilings of G1, G2, G8, and G11. | ||
+ | |||
+ | The wiring to G8 and G11 is in some really awful aluminium Cat 5 which can break if you touch it. | ||
+ | |||
+ | {{: | ||
- | Pi lives above door. Connected to shit amp board. Amp board and speaker being replaced very soon - will document this then. |
squawk.1476052089.txt.gz · Last modified: by tim