squawk
Differences
This shows you the differences between two versions of the page.
Both sides previous revisionPrevious revisionNext revision | Previous revision | ||
squawk [2024-11-18 22:41] – [RAM /tmp] praxidyke | squawk [2024-12-07 21:22] (current) – aria | ||
---|---|---|---|
Line 1: | Line 1: | ||
- | ====== | + | ====== |
+ | |||
+ | 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 a pi (original B), amplifier and speakers for the purpose and documents them thus: | 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 a pi (original B), amplifier and speakers for the purpose and documents them thus: | ||
Line 25: | Line 27: | ||
mosquitto_pub -h mqtt -t ' | mosquitto_pub -h mqtt -t ' | ||
</ | </ | ||
- | |||
- | **Theory:** You could direct sound two only two rooms at once by playing the left or right channel only. By default mpg123 is called with the -m option to ensure that the output is mono and thus the same sound comes out every speaker. | ||
===== Adding new sounds ===== | ===== Adding new sounds ===== | ||
- | Use the web file browser at http:// | + | <del>Use the web file browser at http:// |
+ | |||
+ | For now, use scp to squawk.hacklab. | ||
===== Raspberry Pi OS ===== | ===== Raspberry Pi OS ===== | ||
Latest Raspberry Pi OS, installed using Raspberry Pi Imager. | Latest Raspberry Pi OS, installed using Raspberry Pi Imager. | ||
+ | |||
==== raspi-config ==== | ==== raspi-config ==== | ||
Line 59: | 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: | + | |
- | logging.error(' | + | |
- | return | + | |
- | filename = random.choice(candidates) | + | |
- | else: | + | |
- | # single file requested | + | |
- | if filename not in allfiles: | + | |
- | logging.error(' | + | |
- | return | + | |
- | 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(" | + | |
- | client.subscribe(" | + | |
- | 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 status | + | WorkingDirectory=/ |
- | | + | ExecStart=/home/pi/ |
+ | 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 ===== |
- | <code bash> | + | The code for squawk is stored in < |
- | # | + | |
- | pico2wave -l en-GB -w / | + | |
- | play -q / | + | |
- | rm -f / | + | |
- | </code> | + | |
- | + | ||
- | ==== /home/pi/startup.sh ==== | + | |
- | + | ||
- | < | + | |
- | # | + | |
- | sleep 3 | + | |
- | _IP4=$(hostname -I | cut -d ' ' -f 1) || true | + | |
- | + | ||
- | mpg123 -m -q / | + | |
- | / | + | |
- | </ | + | |
===== Audio files ===== | ===== Audio files ===== | ||
Line 387: | Line 118: | ||
</ | </ | ||
- | ===== Pico TTS ===== | + | ===== Hardware |
- | < | + | {{ :photo_2016-10-10_16-35-17.jpg? |
- | mkdir ~/pico | + | |
- | cd ~/pico | + | |
- | wget http:// | + | |
- | tar -zxf picotts-raspi.tar.gz | + | |
- | sudo cp -R usr / | + | |
- | cd / | + | |
- | sudo dpkg -i libttspico-data_1.0+git20110131-2_all.deb | + | |
- | sudo dpkg -i libttspico0_1.0+git20110131-2_armhf.deb | + | |
- | sudo dpkg -i libttspico-utils_1.0+git20110131-2_armhf.deb | + | |
- | rm -rf ~/pico | + | |
- | </ | + | |
- | ===== Hardware ===== | + | The " |
- | {{ : | + | |
- | The " | + | |
- | There are four speakers connected, two per channel, 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). | + | 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). |
+ | |||
+ | 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. | ||
+ | |||
+ | Note: photo shows older version, prior to addition of relay board. | ||
==== Raspberry Pi ==== | ==== Raspberry Pi ==== | ||
- | Standard pi other than having some wires soldered on the underside of the board to connect to the 3.5mm audio jack. Powered via the header rather than USB. | + | 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. |
==== Amplifier ==== | ==== Amplifier ==== | ||
Line 430: | Line 152: | ||
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. | 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 ==== | ==== Speakers ==== | ||
Line 438: | Line 159: | ||
* Dual cone | * Dual cone | ||
- | Two wired in parallel to each channel, as per the following diagram. This roughly equates | + | Located |
+ | |||
+ | The wiring | ||
{{: | {{: | ||
+ |
squawk.1731969696.txt.gz · Last modified: 2024-11-18 22:41 by praxidyke