Skip to content
Snippets Groups Projects
Commit 37d32124 authored by David Huss's avatar David Huss :speech_balloon:
Browse files
parents bde8fd95 dd044b33
No related branches found
No related tags found
No related merge requests found
LICENSE 0 → 100644
MIT License
Copyright (c) 2025 David Huss
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
...@@ -8,14 +8,42 @@ mediactl sports a touchscreen that allows control of the media technology in tha ...@@ -8,14 +8,42 @@ mediactl sports a touchscreen that allows control of the media technology in tha
## architecture ## architecture
There is a fastapi web service (`localhost:8000`) running behind an nginx reverse proxy that exposes it to the world. The service has a systemd unit file located at `/etc/systemd/system` (see section below). There is a fastapi web service (`localhost:8000`) running behind an nginx reverse proxy that exposes it to the mediahell network. The service has a systemd unit file located at `/etc/systemd/system/mediactl.service` (see section below).
The touchpanel is started with raspi-autologin and the `.xinit` file located in the `d0` user directory (see section below). The files of the running webservice are located at `/home/d0/mediactl` (see section below for hints if you want to edit those files). The touchpanel is started with raspi-autologin and the `.xinit` file located in the `d0` user directory (see section below). The files of the running webservice are located at `/home/d0/mediactl` (see section below for hints if you want to edit those files).
## autostart on RPI ```mermaid
See [this](https://blog.r0b.io/post/minimal-rpi-kiosk/) for a description of how to start chromium via `/home/d0/.xinit` – note: this file contains the screen blanking settings (`xset s 600 0` ) %%{init: {'theme':'neutral'}}%%
graph LR;
xinit -.->|runs| Chrome
Chrome((Chrome<br>Kiosk)) <--->|TCP| Nginx
Chrome <--->|Websocket| Nginx
Remote((Remote<br>Browser)) <-..->|TCP| Nginx
Remote <-..->|Websocket| Nginx
Nginx <---->|TCP| FastAPI["FastAPI<br>(mediactl)"]
Nginx <---->|Websocket| FastAPI
FastAPI <--->|GPIO| Periphery[[Periphery]]
FastAPI <--->|Ethernet| Periphery[[Periphery]]
FastAPI <--->|USB| Periphery[[Periphery]]
systemd -.->|runs| FastAPI
```
The fastapi code receives commands via Websockets from a browser and then uses the connector implementations in [src/devices](src/mediactl/devices/) to send/receive commands to these devices. The results are then sent back via websocket to said browser to give a quick feedback of what is going on. The javascript handling that is in [static/modules](static/modules).
The entry point handling the websockets on the client (browser) side is [ws.js](master/static/ws.js). The javascript part could be organized better and more generically, but it works and is highly customizable.
On the server side (FastAPI) most of the system state (including the device connectors mentioned above) is organized in the class `System` in [src/mediactl/system.py](src/mediactl/system.py).
The main entry point of the whole application [src/mediactl/main.py](src/mediactl/main.py) is relatively typical for a FastAPI application, the only noteworthy thing is that the application maintains *two* websocket connections at the same time, one for sending/receiving commands, one for scheduled sending of the current system status back to the connected browsers. I tried for half a day to do this with one websocket, but it turned out to be more trouble than it was worth.
Within that `main.py` file the async function `handle_incoming_messages(data)` is responsible for handling received commands. If one wanted to add new commands, it would be here.
## Autostart chromium in kiosk mode
See [this](https://blog.r0b.io/post/minimal-rpi-kiosk/) for a description of how to start chromium via `/home/d0/.xinit` – note: this file contains the screen blanking settings (`xset s 600 0` ) which are responsible for blacking out the screen.
## systemd service
## Systemd service
**Check Service Status** **Check Service Status**
```bash ```bash
...@@ -29,7 +57,50 @@ sudo systemctl restart mediactl.service ...@@ -29,7 +57,50 @@ sudo systemctl restart mediactl.service
## note about apt # Periphery Connections
The mediactl (FastAPI) application controls all kind of periphery.
## Connection table
| Device | Connection Kind | Purpose | Kind | Implementation |
| ------------------- | --------------------------------------- | ---------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------- |
| Allen&Heath AHM16 | Ethernet | Audio DSP and Mixer | [TCP Protocol](https://www.allen-heath.com/content/uploads/2023/11/AHM-TCP-Protocol-V1.4.pdf) (essentially MIDI over IP) | [ahm16.py](src/mediactl/devices/ahm16.py) |
| Rpi 3B "projctl" | Ethernet/WIFI (REST via mediahell wifi) | Controlling and monitoring the projector | see [code.hfbk.net](https://code.hfbk.net/medientechnik/ext-lib/projctl) | [projector.py](src/mediactl/devices/projector.py) |
| Kramer VS-411X | GPIO and GND | Switching HDMI-Sources, Audio Extractor | GPIO, Pull port to GND | [kramer.py](src/mediactl/devices/kramer.py) |
| ~~Quadro DSP~~ | ~~(USB-Serial)~~ | ~~Poweramp, Check State~~ | [Reverse Engineering](https://reverseengineering.stackexchange.com/questions/25066/t-amp-quadro-500-dsp-calculate-checksum) | - |
| ~~Sennheiser ewG4~~ | ~~(Ethernet)~~ | ~~Wireless Mic Receiver, Check State~~ | [Manual](https://www.sennheiser.com/globalassets/digizuite/41944-en-ti_1254_metromediensteuerung_ewg4_en.pdf) | - |
| AC123 Remote | GPIO and GND | Controls Screen motor via Radio (Up/Down/Stop) | GPIO, Pull port to GND to switch | [screen.py](src/mediactl/devices/screen.py) |
### GPIO Connections
| GPIO PIN | Device | Function | Kind | Color |
| -------- | ------------ | ----------------- | --------------------- | --------------------- |
| GND (39) | Kramer | GND | GND | Black |
| GPIO 21 | Kramer | Switch to Input 4 | Pull to GND to switch | Brown+Brown/White |
| GPIO 20 | Kramer | Switch to Input 3 | Pull to GND to switch | Blue + Blue/White |
| GPIO 26 | Kramer | Switch to Input 2 | Pull to GND to switch | Orange + Orange/White |
| GPIO 16 | Kramer | Switch to Input 1 | Pull to GND to switch | Green + Green/White |
| GND (34) | AC123 Remote | GND | GND | Black |
| GPIO 13 | AC123 Remote | Screen moves Up | Pull to GND to switch | Green |
| GPIO 6 | AC123 Remote | Screen stops | Pull to GND to switch | White |
| GPIO 5 | AC123 Remote | Screen moves Down | Pull to GND to switch | Red |
## AC123-Remote
This is the screen remote we got from the company who added the motorized screen to the room. I tried to reverse engineer the custom 433 Mhz protocol, but then decided to just use the remote control which offers 3.3V pins. This has the benefit that mediactl could always "know" wheter the screen has previously been moved down or not (except if you shutdown the system inbetween).
![](images/ac123-pinout.jpg)
# Development
## Isolation in mediahell, how to use apt
Devices can't reach the outside world in mediahell (hence the name). This also includes things like `apt`. To update/install packages you have to use a SSH-tunnel to your dev machine.
In `/etc/apt/apt.conf.d/proxy.conf` there is a proxy connection setup: In `/etc/apt/apt.conf.d/proxy.conf` there is a proxy connection setup:
...@@ -57,35 +128,3 @@ sudo sshfs -o allow_other,default_permissions -o IdentityFile=/home/YOURUSER/.ss ...@@ -57,35 +128,3 @@ sudo sshfs -o allow_other,default_permissions -o IdentityFile=/home/YOURUSER/.ss
``` ```
**⚠ Important:** If you edit any of the static files (css/images/js/etc) you need to run the `./deploy_static.sh` script afterwards, else those changes will not be used in production ! **⚠ Important:** If you edit any of the static files (css/images/js/etc) you need to run the `./deploy_static.sh` script afterwards, else those changes will not be used in production !
## Connection diagram
| Device | Connection Kind | Purpose | |
| ----------------- | --------------------------------------- | ---------------------------------------------- | ------------------------------------------------------------ |
| Allen&Heath AHM16 | Ethernet | Audio DSP and Mixer | [TCP Protocol](https://www.allen-heath.com/content/uploads/2023/11/AHM-TCP-Protocol-V1.4.pdf) (essentially MIDI over IP) |
| RPI 3B "projctl" | Ethernet/WIFI (REST via mediahell wifi) | Controlling and monitoring the projector | see [code.hfbk.net](https://code.hfbk.net/medientechnik/ext-lib/projctl) |
| Kramer VS-411X | GPIO and GND | Switching HDMI-Sources, Audio Extractor | GPIO, Pull port to GND |
| Quadro DSP | (USB-Serial) | Poweramp, Check State | [Reverse Engineering](https://reverseengineering.stackexchange.com/questions/25066/t-amp-quadro-500-dsp-calculate-checksum) |
| Sennheiser ewG4 | (Ethernet) | Wireless Mic Receiver, Check State | [Manual](https://www.sennheiser.com/globalassets/digizuite/41944-en-ti_1254_metromediensteuerung_ewg4_en.pdf) |
| AC123 Remote | GPIO and GND | Controls Screen motor via Radio (Up/Down/Stop) | GPIO, Pull port to GND to switch |
### GPIO Connections
| GPIO PIN | Device | Function | Kind | Color |
| -------- | ------------ | ----------------- | --------------------- | --------------------- |
| GND (39) | Kramer | GND | GND | Black |
| GPIO 21 | Kramer | Switch to Input 4 | Pull to GND to switch | Brown+Brown/White |
| GPIO 20 | Kramer | Switch to Input 3 | Pull to GND to switch | Blue + Blue/White |
| GPIO 26 | Kramer | Switch to Input 2 | Pull to GND to switch | Orange + Orange/White |
| GPIO 16 | Kramer | Switch to Input 1 | Pull to GND to switch | Green + Green/White |
| GND (34) | AC123 Remote | GND | GND | Black |
| GPIO 13 | AC123 Remote | Screen moves Up | Pull to GND to switch | Green |
| GPIO 6 | AC123 Remote | Screen stops | Pull to GND to switch | White |
| GPIO 5 | AC123 Remote | Screen moves Down | Pull to GND to switch | Red |
## AC123-Remote
![](images/ac123-pinout.jpg)
[project] [project]
name = "mediactl" name = "mediactl"
version = "0.1.0" version = "0.1.1"
description = "Add your description here" description = "Add your description here"
dependencies = [ dependencies = [
"fastapi[standard]>=0.114.2", "fastapi[standard]>=0.114.2",
......
import asyncio import asyncio
import aioping import aioping
import errno
from datetime import datetime from datetime import datetime
from typing import Union, List from typing import Union, List
from result import Ok, Err, Result, is_ok, is_err # noqa: F401 from result import Ok, Err, Result, is_ok, is_err # noqa: F401
...@@ -96,6 +97,7 @@ class PowerState(mediactl.EnumState): ...@@ -96,6 +97,7 @@ class PowerState(mediactl.EnumState):
on = "on" on = "on"
off = "off" off = "off"
unknown = "unknown" unknown = "unknown"
disconnected = "disconnected"
def is_on(self) -> bool: def is_on(self) -> bool:
return self in [PowerState.on] return self in [PowerState.on]
...@@ -103,6 +105,9 @@ class PowerState(mediactl.EnumState): ...@@ -103,6 +105,9 @@ class PowerState(mediactl.EnumState):
def is_off(self) -> bool: def is_off(self) -> bool:
return not self.is_on() return not self.is_on()
def is_disconnected(self) -> bool:
return self in [PowerState.disconnected]
class Channel(mediactl.WithLogger): class Channel(mediactl.WithLogger):
def __init__(self, number: int, logger=None): def __init__(self, number: int, logger=None):
...@@ -233,6 +238,17 @@ class Ahm16(mediactl.WithLogger): ...@@ -233,6 +238,17 @@ class Ahm16(mediactl.WithLogger):
else str(self.status["last-seen"]) else str(self.status["last-seen"])
) )
self.log_error(f"AHM could not_be_reached. Last seen: {last}") self.log_error(f"AHM could not_be_reached. Last seen: {last}")
except OSError as e:
if e.errno == errno.ENETUNREACH:
self.status["power"] = PowerState.disconnected
last = "never" if self.status["last-seen"] is None else str(self.status["last-seen"])
self.log_error(f"AHM Dante Network is down or AHM unreachable {e.errno}: {e.strerror}. Last seen: {last}")
else:
# Othernetwork‐level failure (no route, interface down, etc.)
self.status["power"] = PowerState.unknown
last = "never" if self.status["last-seen"] is None else str(self.status["last-seen"])
self.log_error(f"AHM ping OSError {e.errno}: {e.strerror}. Last seen: {last}")
async def get_status(self, name=False): async def get_status(self, name=False):
await self.ping() await self.ping()
......
...@@ -34,6 +34,15 @@ main { ...@@ -34,6 +34,15 @@ main {
display: none; display: none;
} }
@keyframes blink-red {
0%, 45% {
background-color: var(--color-red);
}
60%, 100% {
background-color: transparent;
}
}
header { header {
width: 100%; width: 100%;
border-bottom: 1px solid white; border-bottom: 1px solid white;
...@@ -90,6 +99,10 @@ header { ...@@ -90,6 +99,10 @@ header {
.warmup .status_text { color: var(--color-orange); } .warmup .status_text { color: var(--color-orange); }
.off .led { background-color: var(--color-red); } .off .led { background-color: var(--color-red); }
.off .status_text { color: var(--color-red); } .off .status_text { color: var(--color-red); }
.disconnected .led {
background-color: transparent;
animation: blink-red 1s infinite;
}
} }
main.disconnected{ main.disconnected{
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment