From de1437109dffd1fad43a13cd5e6c49883792b145 Mon Sep 17 00:00:00 2001 From: Dariusz Koryto Date: Fri, 14 Aug 2026 00:53:49 +0200 Subject: [PATCH 1/5] Fix PTT release and sample playback error handling The playback loop had no cleanup guard, so any exception between switching PTT on and off left the transmitter keyed. RPi.GPIO does not reset pins on interpreter exit, so the pin stayed HIGH until reboot. - extract PTT release into ptt_off(), idempotent, covering serial and GPIO - register it via atexit and SIGTERM/SIGINT/SIGHUP handlers - wrap the playback loop in try/finally that always calls it The loop itself could raise in several ways. voice_channel was not reset between iterations, so a failed play() either raised NameError on the first sample or silently reused the previous, already finished channel, dropping the sample without a trace. The bare except also swallowed KeyboardInterrupt, making Ctrl+C unable to stop playback. - reset voice_channel at the start of every iteration and skip the wait loop when playback failed - catch KeyError and pygame.error instead of everything, and log the failure - cover sndarray.make_sound() with the same handler --- sr0wx.py | 125 +++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 84 insertions(+), 41 deletions(-) diff --git a/sr0wx.py b/sr0wx.py index 4d1750f3..dd0af518 100755 --- a/sr0wx.py +++ b/sr0wx.py @@ -97,6 +97,10 @@ # socket for checking for other program instances import socket +# atexit and signal for releasing PTT no matter how the script ends +import atexit +import signal + # colorcodes from colorcodes import * @@ -579,6 +583,52 @@ def run_module(args): f"Failed to open serial port {config.serial_port}@{config.serial_baud_rate}: {e}" ) +# PTT must be released even if the script dies in the middle of playback. +# RPi.GPIO does not reset pins on interpreter exit, so a crash between +# switching PTT on and off would leave the transmitter keyed indefinitely. + +ptt_released = False + + +def ptt_off(): + """Release PTT (serial and GPIO). Safe to call more than once.""" + global ptt_released + if ptt_released: + return + ptt_released = True + + if config.serial_port is not None: + try: + ser.close() + logger.info(COLOR_OKGREEN + "Serial PTT: OFF" + COLOR_ENDC) + except NameError: + # sudo gpasswd --add ${USER} dialout + logger.exception("Couldn't close serial port") + except Exception as e: + logger.error(f"Unable to close serial port, got error: {e}") + + if not nopi: + try: + GPIO.output(config.rpi_pin, GPIO.LOW) + logger.info(COLOR_OKGREEN + f"GPIO PTT: OFF, PIN: {config.rpi_pin}" + COLOR_ENDC) + GPIO.cleanup() + except Exception as e: + logger.error(f"Unable to turn GPIO ptt off, got error: {e}") + + +def ptt_signal_handler(signum, frame): + logger.warning(f"Got signal {signum}, releasing PTT and exiting...") + ptt_off() + sys.exit(1) + + +# atexit covers every exit() call, signal handlers cover SIGTERM from +# cron/systemd, which atexit alone would not see +atexit.register(ptt_off) +for sig_name in ("SIGTERM", "SIGINT", "SIGHUP"): + sig = getattr(signal, sig_name, None) + if sig is not None: + signal.signal(sig, ptt_signal_handler) pygame.time.delay(config.marginDelay) @@ -595,54 +645,47 @@ def run_module(args): logger.info("playing sound samples...\n") -for el in message: - if config.showSamples or showSamplesOverwrite: - print(el, end=" ", flush=True) - if el == "_": - pygame.time.wait(config.delayValue) - else: - if "upper" in dir(el): - try: +try: + for el in message: + if config.showSamples or showSamplesOverwrite: + print(el, end=" ", flush=True) + if el == "_": + pygame.time.wait(config.delayValue) + continue + + # reset on every iteration, otherwise a failed playback would fall + # through to the channel of the previous sample (or to no channel + # at all on the very first one) + voice_channel = None + try: + if "upper" in dir(el): voice_channel = sound_samples[el].play() - except: - a = 1 - - elif "upper" not in dir(el): - sound = pygame.sndarray.make_sound(el) - if config.pygame_bug == 1: - sound = pygame.sndarray.make_sound( - pygame.sndarray.array(sound)[ - : int(len(pygame.sndarray.array(sound)) / 2) - ] - ) - voice_channel = sound.play() - while voice_channel.get_busy(): - pygame.time.Clock().tick(config.clockTick) + else: + sound = pygame.sndarray.make_sound(el) + if config.pygame_bug == 1: + arr = pygame.sndarray.array(sound) + sound = pygame.sndarray.make_sound(arr[: len(arr) // 2]) + voice_channel = sound.play() + except (KeyError, pygame.error) as e: + logger.error(COLOR_FAIL + f"Couldn't play sample '{el}': {e}" + COLOR_ENDC) + + if voice_channel is not None: + while voice_channel.get_busy(): + pygame.time.Clock().tick(config.clockTick) pygame.time.delay(config.timeDelay) -# The following four lines give us a one second break (for CTCSS, PTT and -# other stuff) before closing the ``pygame`` mixer and display some debug -# informations. +finally: + # The following lines give us a one second break (for CTCSS, PTT and + # other stuff) before closing the ``pygame`` mixer and display some debug + # informations. -time_playing = time.time() + time_playing = time.time() -pygame.time.delay(config.marginDelay) + pygame.time.delay(config.marginDelay) -logger.info(COLOR_WARNING + "finishing...\n" + COLOR_ENDC) + logger.info(COLOR_WARNING + "finishing...\n" + COLOR_ENDC) -# If we've opened serial it's now time to close it. -try: - if config.serial_port is not None: - ser.close() - logger.info(COLOR_OKGREEN + "Serial PTT: OFF" + COLOR_ENDC) -except NameError: - # sudo gpasswd --add ${USER} dialout - logger.exception("Couldn't close serial port") - -if not nopi: - GPIO.output(config.rpi_pin, GPIO.LOW) - logger.info(COLOR_OKGREEN + f"GPIO PTT: OFF, PIN: {config.rpi_pin}" + COLOR_ENDC) - GPIO.cleanup() + ptt_off() # Save the message to an audio file From 17bfcf8f8ac580d7e17626c5739762eb9fe0f2d6 Mon Sep 17 00:00:00 2001 From: Dariusz Koryto Date: Fri, 14 Aug 2026 00:56:50 +0200 Subject: [PATCH 2/5] Fix container build and runtime setup The image could not be built at all: .dockerignore excluded .env and .git while the Dockerfile copied both, so COPY failed on a missing source. On a fresh clone .env does not exist either, since it is gitignored. - drop COPY of .env, secrets go in at runtime via --env-file, they no longer end up readable in an image layer - drop COPY of .git (108 MB) and the git package, they were only there for the update check - add -y and --no-install-recommends to apt-get, drop the bare apt-get upgrade that aborts on the confirmation prompt in a non-interactive build - clean up apt lists, install requirements before copying the code so a code change does not rebuild the dependency layer - build the user with the host UID/GID, otherwise the mounted logs directory and the PulseAudio socket are not writable - switch to ENTRYPOINT so arguments can be passed through to sr0wx.py run-docker.sh mounted the PulseAudio cookie into /root, while the container runs as sr0wx, and passed no devices, so PTT silently never keyed. - mount the cookie in the container user's home - pass /dev/gpiomem and, via SR0WX_SERIAL, the serial device - require .env, init the pyliczba submodule, persist logs, forward arguments - skip -it when there is no terminal, so cron and systemd runs work Both config files refused to start without a .env file, which made --env-file useless. They now load .env when it is there, fall back to the environment, and report which variables are actually missing. This also fixes METEOSTATION_IP crashing with AttributeError when unset. --- .dockerignore | 14 ++++++++++ Dockerfile | 47 +++++++++++++++++++------------- config.py | 36 +++++++++++++++---------- config_baltyk.py | 23 ++++++++++------ run-docker.sh | 70 +++++++++++++++++++++++++++++++++++++++++++----- 5 files changed, 144 insertions(+), 46 deletions(-) diff --git a/.dockerignore b/.dockerignore index 17f4f56d..2fe5b097 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,7 +1,21 @@ .git +.gitignore +.gitmodules +.dockerignore +Dockerfile .env +.env.example +.vscode wxenv logs/ cache/ __pycache__/ *.pyc +audio_generator/ +scripts/ +README.md +LICENSE +setup.sh +run-docker.sh +sr0wx.wav +requirements-rpi.txt diff --git a/Dockerfile b/Dockerfile index 7eed68b5..f2900ea1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,29 +1,40 @@ # Dockerfile for SR0WX FROM debian:12 -RUN apt-get update && apt-get upgrade -RUN apt-get install -y python3 python3-pip python3-venv pulseaudio git +# UID/GID of the host user, so that the mounted logs directory and the +# PulseAudio socket stay writable from inside the container +ARG UID=1000 +ARG GID=1000 -RUN useradd --create-home --shell /bin/bash -G audio sr0wx +RUN apt-get update \ + && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + python3 python3-pip python3-venv pulseaudio \ + && rm -rf /var/lib/apt/lists/* -USER sr0wx - -WORKDIR /home/sr0wx - -RUN mkdir ./sr0wx -RUN mkdir ./sr0wx/logs +RUN groupadd -g ${GID} sr0wx \ + && useradd --create-home --shell /bin/bash -u ${UID} -g ${GID} -G audio,dialout sr0wx +USER sr0wx WORKDIR /home/sr0wx/sr0wx -COPY requirements.txt ./ -COPY sr0wx.py config.py .env colorcodes.py sr0wx_module.py ./ -COPY modules/ ./modules/ -COPY pl_google/ ./pl_google/ -COPY pyliczba/ ./pyliczba/ -COPY .git/ ./.git/ +RUN mkdir -p logs cache + +# requirements first, so that a code change does not invalidate the +# (slow) dependency layer +COPY --chown=sr0wx:sr0wx requirements.txt ./ +RUN python3 -m venv wxenv \ + && ./wxenv/bin/pip install --no-cache-dir --upgrade pip \ + && ./wxenv/bin/pip install --no-cache-dir -r requirements.txt -RUN python3 -m venv wxenv +COPY --chown=sr0wx:sr0wx sr0wx.py config.py colorcodes.py sr0wx_module.py ./ +COPY --chown=sr0wx:sr0wx modules/ ./modules/ +COPY --chown=sr0wx:sr0wx pl_google/ ./pl_google/ +COPY --chown=sr0wx:sr0wx pyliczba/ ./pyliczba/ -RUN . ./wxenv/bin/activate && pip install --upgrade pip && pip install -r requirements.txt +# .env is deliberately not copied -- image layers are readable by anyone who +# can pull the image. Pass the secrets at runtime with `docker run --env-file`. +# .git is not copied either, so the update check in sr0wx.py will log a single +# error and carry on; set check_for_updates = False in config.py to silence it. -CMD ["./wxenv/bin/python3", "sr0wx.py"] +ENTRYPOINT ["./wxenv/bin/python3", "sr0wx.py"] +CMD [] diff --git a/config.py b/config.py index b4d46863..843fa6d8 100755 --- a/config.py +++ b/config.py @@ -62,22 +62,30 @@ def my_import(name): }, } -# dane z pliku .env +# dane z pliku .env, a przy uruchomieniu w kontenerze prosto ze środowiska +# (docker run --env-file), dlatego brak pliku nie jest błędem if os.path.exists(".env"): load_dotenv() - airly_key = os.getenv("AIRLY_KEY") - openweather_key = os.getenv("OPENWEATHER_KEY") - meteostation_ip = os.getenv("METEOSTATION_IP").split(",") - LATITUDE = float(os.getenv("LATITUDE")) - LONGITUDE = float(os.getenv("LONGITUDE")) - MAP_CALL = os.getenv("MAP_CALL") - MAP_INFO = os.getenv("MAP_INFO") - HEALTHCHECK_UUID = os.getenv("HEALTHCHECK_UUID") - APRS_CALL = os.getenv("APRS_CALL") - APRS_PASSWD = os.getenv("APRS_PASSWD") - APRS_COMMENT = os.getenv("APRS_COMMENT") -else: - raise FileNotFoundError("No .env file present.") + +missing = [k for k in ("LATITUDE", "LONGITUDE") if not os.getenv(k)] +if missing: + raise RuntimeError( + "Brak wymaganej konfiguracji: " + + ", ".join(missing) + + ". Skopiuj .env.example do .env i uzupełnij, albo podaj wartości w zmiennych środowiskowych." + ) + +airly_key = os.getenv("AIRLY_KEY") +openweather_key = os.getenv("OPENWEATHER_KEY") +meteostation_ip = os.getenv("METEOSTATION_IP", "").split(",") +LATITUDE = float(os.getenv("LATITUDE")) +LONGITUDE = float(os.getenv("LONGITUDE")) +MAP_CALL = os.getenv("MAP_CALL") +MAP_INFO = os.getenv("MAP_INFO") +HEALTHCHECK_UUID = os.getenv("HEALTHCHECK_UUID") +APRS_CALL = os.getenv("APRS_CALL") +APRS_PASSWD = os.getenv("APRS_PASSWD") +APRS_COMMENT = os.getenv("APRS_COMMENT") ##################### diff --git a/config_baltyk.py b/config_baltyk.py index e98ba223..584f0ea9 100755 --- a/config_baltyk.py +++ b/config_baltyk.py @@ -62,16 +62,23 @@ def my_import(name): }, } -# dane z pliku .env +# dane z pliku .env, a przy uruchomieniu w kontenerze prosto ze środowiska +# (docker run --env-file), dlatego brak pliku nie jest błędem if os.path.exists(".env"): load_dotenv() - LATITUDE = float(os.getenv("LATITUDE")) - LONGITUDE = float(os.getenv("LONGITUDE")) - MAP_INFO_BALTYK=os.getenv("MAP_INFO_BALTYK") - CALL = os.getenv("CALL") - -else: - raise FileNotFoundError("No .env file present.") + +missing = [k for k in ("LATITUDE", "LONGITUDE") if not os.getenv(k)] +if missing: + raise RuntimeError( + "Brak wymaganej konfiguracji: " + + ", ".join(missing) + + ". Skopiuj .env.example do .env i uzupełnij, albo podaj wartości w zmiennych środowiskowych." + ) + +LATITUDE = float(os.getenv("LATITUDE")) +LONGITUDE = float(os.getenv("LONGITUDE")) +MAP_INFO_BALTYK = os.getenv("MAP_INFO_BALTYK") +CALL = os.getenv("CALL") ##################### diff --git a/run-docker.sh b/run-docker.sh index 6aa8a8eb..a1f1fc6a 100755 --- a/run-docker.sh +++ b/run-docker.sh @@ -1,6 +1,64 @@ -docker build -t sr0wx:latest . -docker run -it \ - -e PULSE_SERVER=unix:${XDG_RUNTIME_DIR}/pulse/native \ - -v ${XDG_RUNTIME_DIR}/pulse/native:${XDG_RUNTIME_DIR}/pulse/native \ - -v ~/.config/pulse/cookie:/root/.config/pulse/cookie \ - sr0wx:latest \ No newline at end of file +#!/bin/bash -e + +# Build and run SR0WX in a container. +# Any arguments given here are passed straight to sr0wx.py, e.g.: +# ./run-docker.sh -t -m sun_rise_set + +cd "$(dirname "$(realpath "$0")")" + +if [ ! -f .env ]; then + echo "No .env file. Copy .env.example to .env and fill it in first." >&2 + exit 1 +fi + +# pl_google needs the pyliczba submodule, an empty directory breaks the import +git submodule update --init --recursive + +docker build \ + --build-arg UID="$(id -u)" \ + --build-arg GID="$(id -g)" \ + -t sr0wx:latest . + +mkdir -p logs + +ARGS=( + --rm + --env-file .env + -v "$(pwd)/logs:/home/sr0wx/sr0wx/logs" +) + +# interactive only when there is a terminal, so cron/systemd runs work too +if [ -t 0 ]; then + ARGS+=(-it) +fi + +# audio via the host PulseAudio daemon +if [ -n "${XDG_RUNTIME_DIR}" ] && [ -S "${XDG_RUNTIME_DIR}/pulse/native" ]; then + ARGS+=( + -e "PULSE_SERVER=unix:${XDG_RUNTIME_DIR}/pulse/native" + -v "${XDG_RUNTIME_DIR}/pulse/native:${XDG_RUNTIME_DIR}/pulse/native" + ) +else + echo "Warning: no PulseAudio socket found, there will be no audio output." >&2 +fi + +# the cookie lives in the container user's home, not in /root +if [ -f "${HOME}/.config/pulse/cookie" ]; then + ARGS+=(-v "${HOME}/.config/pulse/cookie:/home/sr0wx/.config/pulse/cookie:ro") +fi + +# PTT via Raspberry Pi GPIO +if [ -e /dev/gpiomem ]; then + ARGS+=(--device /dev/gpiomem) +fi + +# PTT via serial, set SR0WX_SERIAL to the device path (e.g. /dev/ttyUSB0) +if [ -n "${SR0WX_SERIAL}" ]; then + if [ -e "${SR0WX_SERIAL}" ]; then + ARGS+=(--device "${SR0WX_SERIAL}") + else + echo "Warning: SR0WX_SERIAL=${SR0WX_SERIAL} does not exist, skipping." >&2 + fi +fi + +docker run "${ARGS[@]}" sr0wx:latest "$@" From b55047743880df36e23bd78df54b093c61a18892 Mon Sep 17 00:00:00 2001 From: Dariusz Koryto Date: Fri, 14 Aug 2026 01:09:22 +0200 Subject: [PATCH 3/5] Fix log rotation, logs were never cleaned up The log filename carried a timestamp evaluated at import time, so every run opened a new file and RotatingFileHandler never had anything to rotate. A station running hourly left ~8800 files a year on the SD card. - switch both configs to TimedRotatingFileHandler with a fixed filename, rotating at midnight and keeping 30 days - set encoding to utf-8, the messages contain Polish text - create the logs directory on import, it does not exist in a fresh clone - ignore rotated files too, logs/*.log missed sr0wx.log.YYYY-MM-DD - drop the now unused datetime import --- .gitignore | 3 ++- config.py | 16 ++++++++++------ config_baltyk.py | 17 +++++++++++------ 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index 323fdf65..0a2bd303 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,5 @@ audio_generator/mp3 sr0wx.wav wxenv -logs/*.log +logs/* +!logs/.keep diff --git a/config.py b/config.py index 843fa6d8..447f3174 100755 --- a/config.py +++ b/config.py @@ -8,7 +8,6 @@ import os import logging import logging.handlers -from datetime import datetime from colorcodes import * @@ -21,6 +20,9 @@ def my_import(name): return mod +# katalog na logi, na świeżym klonie repo go nie ma +os.makedirs("logs", exist_ok=True) + # logger dict_log_config = { "version": 1, @@ -49,15 +51,17 @@ def my_import(name): "formatter": "colored_console", "stream": "ext://sys.stdout", }, + # stała nazwa pliku, rotacja o północy, 30 dni historii. Znacznik + # czasu w nazwie dawał nowy plik przy każdym uruchomieniu, więc + # rotacja nigdy się nie odpalała i pliki narastały bez końca. "file": { "level": "INFO", - "class": "logging.handlers.RotatingFileHandler", + "class": "logging.handlers.TimedRotatingFileHandler", "formatter": "format_for_file", - "filename": "logs/" - + str(datetime.now().strftime("%Y-%m-%d_%H:%M")) - + ".log", - "maxBytes": 500000, + "filename": "logs/sr0wx.log", + "when": "midnight", "backupCount": 30, + "encoding": "utf-8", }, }, } diff --git a/config_baltyk.py b/config_baltyk.py index 584f0ea9..c205e3d1 100755 --- a/config_baltyk.py +++ b/config_baltyk.py @@ -8,7 +8,6 @@ import os import logging import logging.handlers -from datetime import datetime from colorcodes import * @@ -21,6 +20,10 @@ def my_import(name): return mod +# katalog na logi, na świeżym klonie repo go nie ma +os.makedirs("logs", exist_ok=True) + + # logger dict_log_config = { "version": 1, @@ -49,15 +52,17 @@ def my_import(name): "formatter": "colored_console", "stream": "ext://sys.stdout", }, + # stała nazwa pliku, rotacja o północy, 30 dni historii. Znacznik + # czasu w nazwie dawał nowy plik przy każdym uruchomieniu, więc + # rotacja nigdy się nie odpalała i pliki narastały bez końca. "file": { "level": "INFO", - "class": "logging.handlers.RotatingFileHandler", + "class": "logging.handlers.TimedRotatingFileHandler", "formatter": "format_for_file", - "filename": "logs/baltyk_" - + str(datetime.now().strftime("%Y-%m-%d_%H:%M")) - + ".log", - "maxBytes": 500000, + "filename": "logs/baltyk.log", + "when": "midnight", "backupCount": 30, + "encoding": "utf-8", }, }, } From 45501115e8e47596a18c59562a8341525ea709ca Mon Sep 17 00:00:00 2001 From: Dariusz Koryto Date: Fri, 14 Aug 2026 01:13:56 +0200 Subject: [PATCH 4/5] Make setup.sh re-runnable and let it install the Raspberry Pi requirements Rerunning the script to repair or update the virtualenv overwrote .env with the empty example, wiping the API keys and the APRS passcode. - keep an existing .env, only create it when it is missing - accept -r to install requirements-rpi.txt, which additionally pulls RPi.GPIO - cd to the script directory, so the virtualenv is not created wherever the script happened to be called from --- setup.sh | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/setup.sh b/setup.sh index 7ef42995..34cd5849 100755 --- a/setup.sh +++ b/setup.sh @@ -1,5 +1,15 @@ #!/bin/bash -e +# Safe to re-run, e.g. after changing requirements.txt. +# Pass -r to install the Raspberry Pi requirements instead. + +cd "$(dirname "$(realpath "$0")")" + +REQUIREMENTS="requirements.txt" +if [ "$1" = "-r" ]; then + REQUIREMENTS="requirements-rpi.txt" +fi + # Init git submodules (pyliczba) git submodule update --init --recursive @@ -9,7 +19,13 @@ source wxenv/bin/activate # Update and install requirements pip install --upgrade pip -pip install -r requirements.txt +pip install -r "${REQUIREMENTS}" -# Initiate .env file -cp .env.example .env +# Initiate .env file, never overwrite the one already there -- it holds the +# API keys and the APRS passcode +if [ -f .env ]; then + echo ".env already exists, leaving it alone." +else + cp .env.example .env + echo "Created .env from .env.example, fill it in before running sr0wx.py." +fi From 798e1e463f5f97d0f2680de4da24c38a04ed5b54 Mon Sep 17 00:00:00 2001 From: Dariusz Koryto Date: Fri, 14 Aug 2026 01:17:02 +0200 Subject: [PATCH 5/5] Regenerate the I, W and Z letter samples Shorter recordings, same format (Ogg Vorbis, mono, 22050 Hz): I 0.53 s -> 0.26 s, W 0.54 s -> 0.34 s, Z 0.61 s -> 0.37 s --- pl_google/samples/I.ogg | Bin 6434 -> 4584 bytes pl_google/samples/W.ogg | Bin 6633 -> 5386 bytes pl_google/samples/Z.ogg | Bin 6852 -> 5487 bytes 3 files changed, 0 insertions(+), 0 deletions(-) diff --git a/pl_google/samples/I.ogg b/pl_google/samples/I.ogg index 40cdc45dc060b88d2183b6c7f30e8158e45a83f8..197a41fe613f1a23f9e2befdb29969c7ad7aa574 100644 GIT binary patch delta 1221 zcmV;$1UmboGUy{3PiJRS00IC200000005|I2rvKu00533cLI?bE)1w?2rvNv005Tz z7fG>H3=My+761SM0001}X$UX^0000;q;VMzN>fKxQ&m$|QczG>O+)+|`n(NB9B>9| zKLJv3b?KttrCw;Z@^xGLY;G){+uuH2y}Xq8#mI|oPxrm}c6xI_)PbNHN`t7M&p9jk z6VDgH8cG7N+F!f_Oqo{3LwLdz;6KnOrO?8Wav+G zb}bOlzA4up+FvqB03d+lR^_HKZyPvvU^*6txA!SsYtrL)bia-qCM}&ky(+9O?daoe zPYHhj0l=(O@y5UlS_;~Io~hq0FCA!#T{QrJW?2LP2%|j!k|g_QSNc?TbL)ZSU#FLr z*cSp2tsKj`1ONb(E7s1T0bn+(vAmY-FQtA<`5D9qqI!`g7D^C znN42Mp}L<`huJBC*HN!7llc@+!@WdSQZecaJ3lgy#h7Hv!l~Xw?dV3h7y?PqdO?4a ze3%0O(4(9JK02Bo1WS8;gRC2`fv7(bD-kbe@^bTF4M!6LM`$f?ZnY0s!Fr7o!4R8X9kK`(0iMx=1&E zg3kmA0JF>i0M;dIW&7i@vRQK7`5k|3G7PM6y5QtbTL7T19r|(X`CyYHR;44n8?p5u zP8_|DT3XYLboN6QDgRiPH5&lV#R2{a8ZIzCSA7X|s(qY$G&M=UuvQuXVEynlfBh^S zUbn=U>5IN1Hdn@_V=M&#oNta8<|SuP)N{T+ZQIEPQT^75JXOe0`s^RCq6U99WC)M*-1YH2{F&iVY6HTmV3jGWDQav?V_vpAE07ItBd6-1qNhU?AK4@1$?pX8-|!j{$$ahXD-+w1FFL z2f*W;KY?NE?#=#eCoXNiOg=rB&hlrvT{5*C8FSN&HNk5Tnld?*Gz&$3HjRmZiN*ag zrBI&AjRE3pBfJs-0PtD>{1?VqX40 j*SY)f0$>qvztB?yE?yCZ!7hLPqgn#47l+FU!1dw)uF@v? delta 3085 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 bSs3Sr2_hHr2l{!NTZ(}fyl6m?snJ~l4-C

9&k0LelULwng2Z2aD>6c$Fy1mvd zbSm=9lO?A2VNX6DEY5Hy2i^Ih(OXSU!b z`^A50HkY+jjktkMT=^PHy=V}|Dc;}@B7z@y8q|#w=q>_PM4;{p}#BxW}teHOifyH?Wf#a331e=?2NZ67UujB)R~m0jl8-O|cD2rt4n zv|>Lh_0LbK^c29b*#EFHoMM{#8RWg?P|oJ|_C`~C1K1(7&gR_H$X*hBV z`2e!i+|bF$Uk!dGuDN1(C9H*8Fy05O*Fb*))uw~sC={dfM=*MMbxOSIPXEl+TXIi* zRX^m^*xaPZG=n6jq~VDX*Kg0Q3Cj|Ch~5|Ecez~-zZ z@LOGt1g+5BErRJO#l2pvmoRROpzMv4UCk|p)&ZVM1O`O358__++~9QL=yU)Ht(CwPY{f zOC?+A0*WN&j?y`2xRM1ifENJ2mmXU8OFT_jxmu)6noo0FySLjaDNDcdbYP6$nq&-M z;s-MOpfh@>1ZA%_n4tu6_i_OJYHe3a=jwux{|E#n(p60DX`1)e!(D2AZ_{ zLel_=f%JCA?(UvlyGoE_z4R6U-g%4$lzXTPw8L~C(};?nHkGpClNwwIKZ`EgtB$hz zP-v@NUGF;Y1tkZ}k(gV`8nd;pTh+=U!BL6b>QG{VyNVzS{Xd$N4y^gDI-T|Y`y>C# z05I4Tzveun#T0)_zCNlPOk44=UpT58cL1I#lnAuj!w%X#b7LFX9UV=`seqH}HG_Wpl07Ij268~|SVqY9K8RDjm> zMs#M9;|dSVqSc!y!@=50W8SZsSZo}gvbf}VUz%DHNGr_2*uZ5`r|kF1A7g;l1}K-A zY&Fq>J!qIPgRCj>1a-?fx^%E5RW}s`+5m91{+hN#klPu9LX+b4&zN;?dC=Zz8DsGP z{%1oDzi6 zwJ1~I0sfa;goUdN7V#c#4+F3=CMHE$dmJm1pFe+m#UQ{jdA;Qhc1(*RkLZ29T-~VW zYTU$KZYlACd)_^&yip~zRc_x45K=z0&)SgQTK_`pQ;y#(w`)&s1IQH?rQ0 z=xt{g68_hEi46cAb^vT!lGCt)kGA^H-7gxkmR2O69H4{r`)V-bqmvJhj?>hlnSFhy z5MeoD1z=|5^qyV6WNVaHGrlNYDeh@6 LB@qBD000002HVe= delta 3286 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 delta 3541 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