Er dient als Signal-Transceiver und Protokolldekoder.
Zweck und Funktion
Der SIGNALduino (bestehend aus Hardware-Stick und Firmware) ist ein leistungsstarkes I/O-Gerät zur Erfassung, zum Empfang und zur Verarbeitung von digitalen Funksignalen (typischerweise 433 MHz und 868 MHz).
Seine Hauptaufgabe ist es, Funksignale anhand von Mustern zu erkennen und sie in maximal detaillierter Form an die übergeordnete Hausautomations-Software (wie FHEM) zur Dekodierung weiterzugeben. Dadurch werden verschiedenste proprietäre Funkprotokolle für die Nutzung in Smart-Home-Systemen zugänglich.
Die verfügbare Hardware-Basis reicht von einfachen Arduino/nanoCUL-Lösungen bis hin zu erweiterten Varianten wie dem Maple-SignalDuino und dem ESP32-SignalDuino, die erweiterte Funktionen (z.B. WLAN) bieten.
Projektgeschichte und Entwicklung
Das SIGNALduino-Projekt entstand in der RFD-FHEM-Community als Open-Source-Initiative zur kostengünstigen Funkkommunikation im Smart-Home-Bereich.
Ursprung und Meilensteine
-
2010er Jahre: Entwicklung erster Arduino-basierter Transceiver mit CC1101-Chips für 433/868 MHz.
-
Perl-Ära: Die Protokollimplementierung erfolgte zunächst als FHEM-Modul
00_SIGNALduino.pmin Perl. -
Community-Wachstum: Eine aktive Entwickler- und Anwendergemeinschaft trieb die Erweiterung der unterstützten Protokolle voran.
Migration zu Python
-
PySignalduino: Mit dem Aufkommen moderner IoT-Architekturen wurde eine Python-Implementierung notwendig.
-
Asynchrone Verarbeitung: PySignalduino nutzt
asynciofür effiziente, nicht-blockierende Verarbeitung. -
MQTT-Integration: Eingebaute MQTT-Bridge für nahtlose Integration in moderne Smart-Home-Systeme.
Firmware-Entwicklung
Die SIGNALDuino-Firmware wird kontinuierlich im separaten Repository weiterentwickelt:
-
GitHub Repository: https://github.com/RFD-FHEM/SIGNALDuino
-
Aktuelle Version: v3.5.0 (Stand Dezember 2025)
-
Unterstützte Plattformen:
-
Arduino Nano mit CC1101
-
ESP32 (mit WiFi-Unterstützung)
-
STM32 (Maple Mini)
-
-
Build-System: PlatformIO und Arduino-IDE Projekte sind im Repository enthalten.
PySignalduino vs. Original
PySignalduino ist keine direkte Portierung, sondern eine Neuimplementierung mit folgenden Schwerpunkten: - Moderne Python-Praktiken: Typisierung, strukturierte Logging, Konfiguration über Umgebungsvariablen. - Erweiterte Transporte: Unterstützung für serielle und TCP-Verbindungen. - Testabdeckung: Umfangreiche Testsuite zur Gewährleistung der Codequalität.
Entwicklungsstatus
|
Warning
|
Entwicklungsstatus PySignalduino befindet sich noch in aktiver Entwicklung und hat noch kein offizielles Release veröffentlicht. Die API kann sich zwischen Versionen ändern. Entwickler sollten bei der Verwendung Vorsicht walten lassen und auf mögliche Breaking Changes vorbereitet sein. |
Unterstützte Protokolle
Eine breite Palette von Funkprotokollen wird unterstützt und ständig erweitert. Detaillierte Informationen zu den unterstützten Geräten und Protokollen finden Sie im Benutzerhandbuch.
Firmware
Die Firmware wird kontinuierlich weiterentwickelt und ist nicht auf jedem prinzipiell geeigneten Gerät lauffähig, da spezifische Anpassungen an die Hardware erforderlich sind.
Migration
PySignalduino wurde von einer Thread-basierten Architektur zu einer asynchronen asyncio-Architektur migriert. Falls Sie von einer Version vor 0.9.0 upgraden, lesen Sie die Migrationsleitfäden:
-
Asyncio-Migrationsleitfaden – Detaillierte Anleitung zur Anpassung Ihrer Skripte und Callbacks.
-
Manchester-Migrationsleitfaden – Informationen zur Integration der Manchester‑Protokoll‑Verarbeitung.
-
Methoden‑Migrations‑Übersicht – Liste aller geänderten Methoden und Klassen.
Installation
|
Note
|
PySignalduino ist noch in Entwicklung. Es gibt bisher keine stabile Version – nutzen Sie die Software mit entsprechender Vorsicht. |
Voraussetzungen
-
Python 3.8 oder höher
-
pip (Python Package Installer)
-
Ein SIGNALDuino-Gerät mit serieller oder TCP-Verbindung
-
Optional: Ein MQTT-Broker (z.B. Mosquitto) für die MQTT-Integration
Abhängigkeiten
PySignalduino benötigt folgende Python-Pakete:
-
pyserial– Serielle Kommunikation -
pyserial-asyncio– Asynchrone serielle Unterstützung -
aiomqtt– Asynchroner MQTT-Client (ersetztpaho-mqttin der asynchronen Version) -
python-dotenv– Laden von Umgebungsvariablen aus.env-Dateien -
requests– HTTP-Anfragen (für Firmware-Download)
Diese Abhängigkeiten werden automatisch installiert, wenn Sie das Paket mit pip install -e . installieren.
Installation via pip (empfohlen)
Die einfachste Methode ist die Installation aus dem geklonten Repository im Entwicklermodus:
git clone https://github.com/Ein-Einfaches-Beispiel/PySignalduino.git
cd PySignalduino
pip install -e .
Dadurch wird das Paket signalduino-mqtt in Ihrer Python-Umgebung installiert und alle Runtime-Abhängigkeiten werden erfüllt.
Alternative: Installation nur der Abhängigkeiten
Falls Sie das Paket nicht installieren, sondern nur die Abhängigkeiten nutzen möchten (z.B. für Skripte im Projektverzeichnis):
pip install -r requirements.txt
Die Datei requirements.txt enthält die gleichen Pakete wie oben aufgelistet.
Entwicklungsumgebung einrichten
Für Beiträge zum Projekt oder zum Ausführen der Tests installieren Sie zusätzlich die Entwicklungsabhängigkeiten:
pip install -r requirements-dev.txt
Dies installiert:
-
pytest– Testframework -
pytest-mock– Mocking-Unterstützung -
pytest-asyncio– Asynchrone Testunterstützung -
pytest-cov– Coverage-Berichte
Verifikation der Installation
Überprüfen Sie, ob die Installation erfolgreich war, indem Sie die Hilfe des Hauptprogramms aufrufen:
python3 main.py --help
Sie sollten eine Ausgabe mit allen verfügbaren Kommandozeilenoptionen sehen.
Docker / DevContainer
Für eine konsistente Entwicklungsumgebung steht eine DevContainer-Konfiguration bereit. Öffnen Sie das Projekt in Visual Studio Code mit der Remote-Containers-Erweiterung, um automatisch alle Abhängigkeiten in einem isolierten Container zu installieren.
Details finden Sie in der [DevContainer-Dokumentation](devcontainer_env.md).
Nächste Schritte
Nach der Installation können Sie:
-
Die Schnellstart-Anleitung befolgen.
-
Die Konfiguration über Umgebungsvariablen einrichten.
-
Die MQTT-Integration testen.
Grundlegende Nutzung
Die Hauptklasse SDProtocols stellt die Schnittstelle zur Protokollverarbeitung bereit.
def __init__(self):
self._protocols = self._load_protocols()
self._log_callback = None
self.set_defaults()
def _load_protocols(self) -> Dict[str, Any]:
"""Loads protocols from protocols.json."""
json_path = Path(__file__).resolve().parent / "protocols.json"
try:
with open(json_path, "r", encoding="utf-8") as f:
data = json.load(f)
return data.get("protocols", {})
except Exception as e:
# Fallback or error logging if needed, though for now we raise
# or return empty dict if file missing (should not happen in prod)
print(f"Error loading protocols.json: {e}")
return {}
def protocol_exists(self, pid: str) -> bool:
return pid in self._protocols
def get_protocol_list(self) -> dict:
return self._protocols
Integration
PySignalduino ist als Bibliothek konzipiert, die beispielsweise in MQTT-Bridges oder Home-Automation-Skripten verwendet werden kann. Sie übernimmt die Erkennung und Dekodierung der Rohdaten.
Logging
Für Debugging-Zwecke können Sie eine eigene Callback-Funktion registrieren:
def register_log_callback(self, callback):
"""Register a callback function for logging."""
if callable(callback):
self._log_callback = callback
def _logging(self, message: str, level: int = 3):
"""Log a message if a callback is registered."""
if self._log_callback:
self._log_callback(message, level)
MQTT Integration
PySignalduino bietet eine integrierte MQTT-Integration über die Klasse MqttPublisher. Diese ermöglicht das Veröffentlichen dekodierter Nachrichten an einen MQTT-Broker und das Empfangen von Befehlen über MQTT-Topics.
Einrichtung und Konfiguration
Die MQTT-Verbindung wird automatisch initialisiert, wenn die Umgebungsvariable MQTT_HOST gesetzt ist. Folgende Umgebungsvariablen können konfiguriert werden:
-
MQTT_HOST– Hostname oder IP-Adresse des MQTT-Brokers (Standard:localhost) -
MQTT_PORT– Port des Brokers (Standard:1883) -
MQTT_TOPIC– Basis-Topic für alle Nachrichten (Standard:signalduino) -
MQTT_USERNAME– Optionaler Benutzername für Authentifizierung -
MQTT_PASSWORD– Optionales Passwort für Authentifizierung -
MQTT_COMPRESSION_ENABLED– Boolescher Wert (0/1) zur Aktivierung der Payload-Kompression (Standard: 0)
Der MqttPublisher wird innerhalb des SignalduinoController verwendet und stellt eine asynchrone Context-Manager-Schnittstelle bereit:
# Transport initialisieren
transport = None
if args.serial:
logger.info(f"Initialisiere serielle Verbindung auf {args.serial} mit {args.baud} Baud...")
transport = SerialTransport(port=args.serial, baudrate=args.baud)
elif args.tcp:
logger.info(f"Initializing TCP connection to {args.tcp}:{args.port}...")
transport = TCPTransport(host=args.tcp, port=args.port)
# Wenn weder --serial noch --tcp (oder deren ENV-Defaults) gesetzt sind
if not transport:
logger.error("Kein gültiger Transport konfiguriert. Bitte geben Sie --serial oder --tcp an oder setzen Sie SIGNALDUINO_SERIAL_PORT / SIGNALDUINO_TCP_HOST in der Umgebung.")
sys.exit(1)
# Controller initialisieren
# Wir initialisieren den Controller zuerst (mit mqtt_publisher=None),
# um ihn als Argument an MqttPublisher übergeben zu können (zirkuläre Abhängigkeit).
controller = SignalduinoController(
transport=transport,
message_callback=message_callback,
logger=logger,
mqtt_publisher=None # Wird später zugewiesen
)
# MQTT Publisher explizit initialisieren, falls Host in Argumenten gesetzt
mqtt_publisher = None
# args.mqtt_host ist gesetzt, wenn es entweder als CLI-Argument übergeben wurde
# oder wenn es als Umgebungsvariable gesetzt war (Standardwert in main()).
# Wir prüfen hier nur, ob ein Wert vorhanden ist, da der Controller sonst
MQTT-Topics
-
{topic}/messages– JSON‑kodierte dekodierte Nachrichten (DecodedMessage) -
{topic}/commands/#– Topic für eingehende Befehle (Wildcard-Subscription) -
{topic}/responses– Antworten auf GET-Befehle, inkl.get/cc1101/frequency. -
{topic}/errors– Fehlerantworten. -
{topic}/status– Heartbeat‑ und Statusmeldungen (optional)
Heartbeat-Funktionalität
Der Publisher sendet regelmäßig einen Heartbeat („online“) unter {topic}/status, solange die Verbindung besteht.
Bei Verbindungsabbruch wird „offline“ gepublished.
Beispiel: Manuelle Nutzung des MqttPublisher
mock_client_instance.publish = AsyncMock()
mock_client_instance.subscribe = AsyncMock()
# Behebe den TypeError: 'MagicMock' object can't be awaited in signalduino/mqtt.py:54
MockClient.return_value.__aenter__ = AsyncMock(return_value=None)
Command Interface
PySignalduino stellt eine umfangreiche Befehls-API zur Steuerung des SIGNALDuino-Firmware-Geräts bereit. Die Klasse SignalduinoCommands kapselt alle verfügbaren seriellen Befehle und bietet eine asynchrone Schnittstelle.
Verfügbare Befehle
Die folgenden Befehle werden unterstützt (Auswahl):
-
Systembefehle:
-
get_version()– Firmware-Version abfragen (V) -
get_help()– Hilfe anzeigen (?) -
get_free_ram()– Freien RAM abfragen ® -
get_uptime()– Uptime in Sekunden (t) -
ping()– Ping-Gerät (P) -
get_cc1101_status()– CC1101-Status (s) -
disable_receiver()– Empfänger deaktivieren (XQ) -
enable_receiver()– Empfänger aktivieren (XE) -
factory_reset()– Werkseinstellungen wiederherstellen (e) -
Konfigurationsbefehle:
-
get_config()– Konfiguration lesen (CG) -
set_decoder_state(decoder, enabled)– Decoder aktivieren/deaktivieren (C<CMD><FLAG>) -
set_manchester_min_bit_length(length)– MC Min Bit Length setzen (CSmcmbl=) -
set_message_type_enabled(message_type, enabled)– Nachrichtentyp aktivieren/deaktivieren (C<FLAG><TYPE>) -
get_ccconf()– CC1101-Konfiguration abfragen (C0DnF) -
get_ccpatable()– CC1101 PA Table abfragen (C3E) -
read_cc1101_register(register)– CC1101-Register lesen (C<reg>) -
write_register(register, value)– EEPROM/CC1101-Register schreiben (W<reg><val>) -
read_eeprom(address)– EEPROM-Byte lesen (r<addr>) -
set_patable(value)– PA Table schreiben (x<val>) -
set_bwidth(value)– Bandbreite setzen (C10<val>) -
set_rampl(value)– Rampenlänge setzen (W1D<val>) -
set_sens(value)– Empfindlichkeit setzen (W1F<val>) -
Sendebefehle:
-
send_combined(params)– Kombinierten Sendebefehl (SC…) -
send_manchester(params)– Manchester senden (SM…) -
send_raw(params)– Rohdaten senden (SR…) -
send_xfsk(params)– xFSK senden (SN…) -
send_message(message)– Vorkodierte Nachricht senden
Persistenz-Funktionalität
Befehle, die die Hardware-Konfiguration ändern (z.
B. write_register, set_patable), werden in der Regel im EEPROM des SIGNALDuino persistent gespeichert.
Die Persistenz wird durch die Firmware gewährleistet; PySignalduino sendet lediglich die entsprechenden Kommandos.
Nutzung über MQTT
Wenn MQTT aktiviert ist, können Befehle über das Topic {base_topic}/commands/{command} gesendet werden. Die Basis für Antworten ist {base_topic}/responses (Erfolg) oder {base_topic}/errors (Fehler). Das base_topic ist standardmäßig signalduino/v1.
Die optionale req_id kann im Request-Payload gesendet werden und wird unverändert in die Response übernommen. Sie dient zur Korrelation von asynchronen Anfragen und Antworten.
CC1101 Frequenz abfragen (get/cc1101/frequency)
Dieser Befehl fragt die aktuell im CC1101-Transceiver eingestellte Funkfrequenz ab.
1. Request Topic und Payload (Senden)
-
Topic:
signalduino/v1/commands/get/cc1101/frequency(ersetzesignalduino/v1durch dein konfiguriertes{base_topic}) -
Payload: Muss eine
req_idzur Korrelation der Antwort enthalten. Ist keinereq_idim Payload enthalten, wird automatisch der Wert"NO_REQ_ID"verwendet.
{
"req_id": "client-12345-freq-req-A"
}
2. Response Topic und Payload (Empfangen)
-
Erfolgs-Topic:
signalduino/v1/responses -
Fehler-Topic:
signalduino/v1/errors
Erfolgreiche Antwort (auf `signalduino/v1/responses\`):
{
"command": "get/cc1101/frequency",
"success": true,
"req_id": "client-12345-freq-req-A",
"payload": {
"frequency_mhz": 433.920
}
}
Fehlerhafte Antwort (auf `signalduino/v1/errors\`):
{
"command": "get/cc1101/frequency",
"success": false,
"req_id": "client-12345-freq-req-A",
"error": "Hardware nicht initialisiert"
}
Beispiel mit mosquitto_pub und mosquitto_sub (angenommen base_topic ist signalduino/v1):
# Zum Senden des Requests
mosquitto_pub -h localhost -t "signalduino/v1/commands/get/cc1101/frequency" -m '{"req_id": "test-123"}'
# Zum Empfangen der Antwort
mosquitto_sub -h localhost -t "signalduino/v1/responses"
Allgemeine Command-Topics
Alle anderen allgemeinen Befehle folgen ebenfalls diesem Schema, wobei {command} den Pfad nach /commands/ darstellt.
Beispiel mit mosquitto_pub:
# Sende Request für System-Version
mosquitto_pub -h localhost -t "signalduino/v1/commands/get/system/version" -m '{"req_id": "test-version"}'
# Antwort empfängst du auf signalduino/v1/responses
Code-Beispiel: Direkte Nutzung der Command-API
mock_transport.close.assert_called_once()
@pytest.mark.asyncio
async def test_send_command_fire_and_forget(mock_transport, mock_parser, mock_controller_initialize):
"""Test sending a command without expecting a response."""
controller = SignalduinoController(transport=mock_transport, parser=mock_parser)
async with controller:
await controller.send_command("V", expect_response=False)
# Verify command was queued
assert controller._write_queue.qsize() == 1
Beispiel: Asynchrone Context-Manager Nutzung
# Transport initialisieren
transport = None
if args.serial:
logger.info(f"Initialisiere serielle Verbindung auf {args.serial} mit {args.baud} Baud...")
transport = SerialTransport(port=args.serial, baudrate=args.baud)
elif args.tcp:
logger.info(f"Initializing TCP connection to {args.tcp}:{args.port}...")
transport = TCPTransport(host=args.tcp, port=args.port)
# Wenn weder --serial noch --tcp (oder deren ENV-Defaults) gesetzt sind
if not transport:
logger.error("Kein gültiger Transport konfiguriert. Bitte geben Sie --serial oder --tcp an oder setzen Sie SIGNALDUINO_SERIAL_PORT / SIGNALDUINO_TCP_HOST in der Umgebung.")
sys.exit(1)
# Controller initialisieren
# Wir initialisieren den Controller zuerst (mit mqtt_publisher=None),
# um ihn als Argument an MqttPublisher übergeben zu können (zirkuläre Abhängigkeit).
controller = SignalduinoController(
transport=transport,
message_callback=message_callback,
logger=logger,
mqtt_publisher=None # Wird später zugewiesen
)
# MQTT Publisher explizit initialisieren, falls Host in Argumenten gesetzt
mqtt_publisher = None
# args.mqtt_host ist gesetzt, wenn es entweder als CLI-Argument übergeben wurde
# oder wenn es als Umgebungsvariable gesetzt war (Standardwert in main()).
# Wir prüfen hier nur, ob ein Wert vorhanden ist, da der Controller sonst
API-Referenz (Auszug)
Die folgenden Klassen und Schnittstellen sind für die Integration besonders relevant:
MqttPublisher
Die Klasse signalduino.mqtt.MqttPublisher bietet eine asynchrone Context-Manager-Schnittstelle zur Kommunikation mit einem MQTT-Broker.
-
Methoden:
-
async publish(message: DecodedMessage)– Veröffentlicht eine dekodierte Nachricht unter{topic}/messages -
async publish_simple(subtopic: str, payload: str, retain: bool = False)– Veröffentlicht eine einfache Zeichenkette unter{topic}/{subtopic} -
async is_connected() → bool– Prüft, ob die Verbindung zum Broker besteht -
register_command_callback(callback: Callable)– Registriert einen asynchronen Callback für eingehende Befehle -
Context-Manager:
async with MqttPublisher() as publisher:
SignalduinoCommands
Die Klasse signalduino.commands.SignalduinoCommands kapselt alle seriellen Befehle für die SIGNALDuino-Firmware.
-
Initialisierung: Erfordert eine asynchrone Sendefunktion (wird normalerweise vom
SignalduinoControllerbereitgestellt) -
Alle Methoden sind asynchron (
async def) und geben entwederstr(Antwort) zurück oderNone(keine Antwort erwartet) -
Umfang: Systembefehle, Konfiguration, Senden von Nachrichten (siehe Abschnitt „Command Interface“)
Asynchrone Context-Manager-Schnittstelle
Sowohl SignalduinoController als auch MqttPublisher und die Transportklassen (TcpTransport, SerialTransport) implementieren das asynchrone Context-Manager-Protokoll (aenter/aexit). Dies gewährleistet eine sichere Ressourcenverwaltung (Verbindungsauf‑/abbau, Hintergrundtasks).
Beispiel für verschachtelte Context-Manager:
# Transport initialisieren
transport = None
if args.serial:
logger.info(f"Initialisiere serielle Verbindung auf {args.serial} mit {args.baud} Baud...")
transport = SerialTransport(port=args.serial, baudrate=args.baud)
elif args.tcp:
logger.info(f"Initializing TCP connection to {args.tcp}:{args.port}...")
transport = TCPTransport(host=args.tcp, port=args.port)
# Wenn weder --serial noch --tcp (oder deren ENV-Defaults) gesetzt sind
if not transport:
logger.error("Kein gültiger Transport konfiguriert. Bitte geben Sie --serial oder --tcp an oder setzen Sie SIGNALDUINO_SERIAL_PORT / SIGNALDUINO_TCP_HOST in der Umgebung.")
sys.exit(1)
# Controller initialisieren
# Wir initialisieren den Controller zuerst (mit mqtt_publisher=None),
# um ihn als Argument an MqttPublisher übergeben zu können (zirkuläre Abhängigkeit).
controller = SignalduinoController(
transport=transport,
message_callback=message_callback,
logger=logger,
mqtt_publisher=None # Wird später zugewiesen
)
# MQTT Publisher explizit initialisieren, falls Host in Argumenten gesetzt
mqtt_publisher = None
# args.mqtt_host ist gesetzt, wenn es entweder als CLI-Argument übergeben wurde
# oder wenn es als Umgebungsvariable gesetzt war (Standardwert in main()).
# Wir prüfen hier nur, ob ein Wert vorhanden ist, da der Controller sonst
Weitere Klassen
-
SignalduinoController– Zentrale Steuerungsklasse, koordiniert Transport, Parser, MQTT und Befehle -
TcpTransport,SerialTransport– Asynchrone Transportimplementierungen für TCP bzw. serielle Verbindungen -
DecodedMessage,RawFrame– Datentypen für dekodierte Nachrichten und Rohframes
Eine vollständige API-Dokumentation kann mit pydoc oder mittels Sphinx generiert werden.
Troubleshooting
Dieser Abschnitt beschreibt häufige Probleme und deren Lösungen.
MQTT-Verbindungsprobleme
-
Keine Verbindung zum Broker: Stellen Sie sicher, dass die Umgebungsvariablen
MQTT_HOSTundMQTT_PORTkorrekt gesetzt sind. Der Broker muss erreichbar sein und keine Authentifizierung erfordern (oder Benutzername/Passwort müssen gesetzt sein). -
Verbindung bricht ab: Überprüfen Sie die Netzwerkverbindung und Broker-Konfiguration. Der MQTT-Client (
aiomqtt) versucht automatisch, die Verbindung wiederherzustellen. Falls die Verbindung dauerhaft abbricht, prüfen Sie Firewall-Einstellungen und Broker-Logs. -
MQTT-Nachrichten werden nicht empfangen: Stellen Sie sicher, dass das Topic
{topic}/commands/#abonniert ist. Der Command-Listener startet automatisch, wenn MQTT aktiviert ist. Überprüfen Sie die Log-Ausgabe auf Fehler.
Asyncio-spezifische Probleme
-
RuntimeError: no running event loop: Tritt auf, wenn asyncio-Funktionen außerhalb eines laufenden Event-Loops aufgerufen werden. Stellen Sie sicher, dass Ihr Code innerhalb einer asyncio-Coroutine läuft undasyncio.run()verwendet wird. Verwenden Sieasync withfür Context-Manager. -
Tasks hängen oder werden nicht abgebrochen: Alle Hintergrundtasks sollten auf das
_stop_eventreagieren. Bei manuell erstellten Tasks müssen Sieasyncio.CancelledErrorabfangen und Ressourcen freigeben. -
Deadlocks in Queues: Wenn eine Queue voll ist und kein Consumer mehr liest, kann
await queue.put()blockieren. Stellen Sie sicher, dass die Consumer-Tasks laufen und die Queue nicht überfüllt wird. Verwenden Sieasyncio.wait_formit Timeout.
Verbindungsprobleme zum SIGNALDuino-Gerät
-
Keine Antwort auf Befehle: Überprüfen Sie die serielle oder TCP-Verbindung. Stellen Sie sicher, dass das Gerät eingeschaltet ist und die korrekte Baudrate (115200) verwendet wird. Testen Sie mit einem Terminal-Programm, ob das Gerät auf
V(Version) antwortet. -
Timeout-Errors: Die Standard-Timeout für Befehle beträgt 2 Sekunden. Bei langsamen Verbindungen kann dies erhöht werden. Falls Timeouts trotzdem auftreten, könnte die Verbindung instabil sein.
-
Parser erkennt keine Protokolle: Überprüfen Sie, ob die Rohdaten im erwarteten Format ankommen (z.B.
+MU;…). Stellen Sie sicher, dass die Protokolldefinitionen (protocols.json) geladen werden und das Protokoll aktiviert ist.
Logging und Debugging
Aktivieren Sie Debug-Logging, um detaillierte Informationen zu erhalten:
# Konfiguration des Loggings
logging.basicConfig(
level=level,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
handlers=[
logging.StreamHandler(sys.stdout)
]
)
# Setze den Level auch auf den Root-Logger, falls basicConfig ihn nicht korrekt gesetzt hat (z.B. bei wiederholtem Aufruf)
Die Log-Ausgabe zeigt den Status von Transport, Parser und MQTT.
Bekannte Probleme und Workarounds
-
Entwicklungsstatus: Da PySignalduino noch in aktiver Entwicklung ist, können sich Verhalten und API zwischen Commits ändern. Bei unerwartetem Verhalten prüfen Sie bitte die aktuelle Codebasis und melden Sie Issues auf GitHub.
-
aiomqtt-Versionen: Verwenden Sieaiomqtt>=2.0.0. Ältere Versionen können Inkompatibilitäten aufweisen. -
Windows und asyncio: Unter Windows kann es bei seriellen Verbindungen zu Problemen mit asyncio kommen. Verwenden Sie
asyncio.ProactorEventLoopoder weichen Sie auf TCP-Transport aus. -
Memory Leaks: Bei langem Betrieb können asyncio-Tasks Speicher verbrauchen. Stellen Sie sicher, dass abgeschlossene Tasks garbage-collected werden. Verwenden Sie
asyncio.create_taskmit Referenzen, um Tasks später abbrechen zu können.
Bei weiteren Problemen öffnen Sie bitte ein Issue auf GitHub mit den relevanten Logs und Konfigurationsdetails.
Übersicht
PySignalduino ist modular aufgebaut und trennt die Protokolldefinitionen (JSON) strikt von der Verarbeitungslogik (Python). Seit der Migration zu asyncio (Version 0.9.0) folgt das System einer ereignisgesteuerten, asynchronen Architektur, die auf asyncio-Tasks und -Queues basiert. Dies ermöglicht eine effiziente Verarbeitung von Sensordaten, Kommandos und MQTT-Nachrichten ohne Blockierung.
Kernkomponenten
SDProtocols Klasse
Die Klasse SDProtocols (sd_protocols/sd_protocols.py) ist der zentrale Einstiegspunkt. Sie vereint Funktionalitäten durch Mehrfachvererbung von Mixins:
-
ProtocolHelpersMixin: Grundlegende Bit-Operationen.
-
ManchesterMixin: Spezifische Logik für Manchester-kodierte Signale (
mcBit2*Methoden). -
PostdemodulationMixin: Nachbearbeitung dekodierter Daten (
postDemo_*Methoden). -
RSLMixin: Handler für das RSL-Protokoll.
Protokolldefinition (JSON)
Die Datei sd_protocols/protocols.json enthält die statischen Definitionen. Jedes Protokoll besitzt eine ID und Eigenschaften wie:
-
format: Kodierung (z.B.manchester,twostate,pwm). -
preamble: Erkennungsmuster. -
method: Mapping auf die Python-Methode zur Dekodierung.
Parsing Chain (Manchester)
Der Ablauf bei Manchester-Signalen ist wie folgt:
1. Erkennung: Match anhand der Preamble/Muster.
2. Vorvalidierung: ManchesterMixin._demodulate_mc_data() prüft Länge und Taktung.
3. Dekodierung: Aufruf der spezifischen mcBit2*-Methode.
Hinweis: Einige Protokolle wie TFA (mcBit2TFA) oder Grothe (mcBit2Grothe) haben spezielle Anforderungen an die Längenprüfung oder Duplikatfilterung.
Asyncio-Architektur
PySignalduino verwendet asyncio für alle E/A-Operationen, um parallele Verarbeitung ohne Thread-Overhead zu ermöglichen. Die Architektur basiert auf drei Haupt-Tasks, die über asynchrone Queues kommunizieren:
-
Reader-Task: Liest kontinuierlich Zeilen vom Transport (Seriell/TCP) und legt sie in der
_raw_message_queueab. -
Parser-Task: Entnimmt Rohzeilen aus der Queue, dekodiert sie über den
SignalParserund veröffentlicht Ergebnisse via MQTT oder ruft denmessage_callbackauf. -
Writer-Task: Verarbeitet Kommandos aus der
_write_queue, sendet sie an das Gerät und wartet bei Bedarf auf Antworten.
Zusätzlich gibt es spezielle Tasks für Initialisierung, Heartbeat und MQTT-Command-Listener.
Asynchrone Queues und Synchronisation
-
_raw_message_queue(asyncio.Queue[str]): Rohdaten vom Reader zum Parser. -
_write_queue(asyncio.Queue[QueuedCommand]): Ausstehende Kommandos vom Controller zum Writer. -
_pending_responses(List[PendingResponse]): Verwaltet erwartete Antworten mit asyncio.Event für jede. -
_stop_event(asyncio.Event): Signalisiert allen Tasks, dass sie beenden sollen. -
_init_complete_event(asyncio.Event): Wird gesetzt, sobald die Geräteinitialisierung erfolgreich abgeschlossen ist.
Asynchrone Kontextmanager
Alle Ressourcen (Transport, MQTT-Client) implementieren aenter/aexit und werden mittels async with verwaltet. Der SignalduinoController selbst ist ein Kontextmanager, der die Lebensdauer der Verbindung steuert.
MQTT-Integration (v1 API)
Die MQTT-Integration wurde auf eine versionierte, konsistente Befehlsschnittstelle umgestellt, basierend auf dem Architecture Decision Record (ADR-001, ADR-002).
Architektur der Befehlsverarbeitung
Die Verarbeitung von eingehenden Befehlen erfolgt über ein dediziertes Command Dispatcher Pattern zur strikten Trennung von Netzwerk-Layer, Validierungslogik und Controller-Aktionen:
-
MqttPublisher (
signalduino/mqtt.py) empfängt eine Nachricht aufsignalduino/v1/commands/#. -
Der SignalduinoController leitet die rohe Payload an den MqttCommandDispatcher weiter.
-
Der Dispatcher (
signalduino/commands.py) validiert die Payload gegen ein JSON-Schema (ADR-002). -
Bei Erfolg wird die entsprechende asynchrone Methode im SignalduinoController aufgerufen.
-
Der Controller sendet serielle Kommandos (
W<reg><val>,V,CG) und verpackt die Firmware-Antwort. -
Die finale Antwort (
status: OKodererror: 400/500/502) wird an den Client zurückgesendet.
Topic-Struktur und Versionierung (ADR-001)
Alle Topics sind versioniert und verwenden das Präfix {MQTT_TOPIC}/v1.
Topic-Typ |
Topic-Struktur |
Zweck |
Command (Request) |
|
Steuerung und Abfrage von Parametern (z.B. |
Response (Success) |
|
Strukturierte Antwort auf Befehle ( |
Error (Failure) |
|
Strukturierte Fehlerinformationen ( |
Telemetry |
|
JSON-serialisierte, dekodierte Sensordaten ( |
Status |
|
Heartbeat- und Gerätestatus (z.B. |
Payload-Format
Alle Requests (Commands) und Responses (Responses/Errors) verwenden eine standardisierte JSON-Struktur, die eine req_id zur Korrelation von Anfrage und Antwort erfordert.
{
"req_id": "uuid-12345",
"data": "V 3.5.7+20250219" // Nur in Responses
}
Wichtige Architekturentscheidungen
Die vollständigen Architecture Decision Records (ADR) sind hier aufgelistet und werden im Dokument eingebettet:
001. Implementierung des MQTT-Befehls get/frequency
Status
Angenommen.
Kontext
Das PySignalduino-Projekt benötigt eine Methode, um die aktuell im CC1101-Transceiver eingestellte Funkfrequenz über das MQTT-Interface abzufragen, hauptsächlich für Diagnose- und Statuszwecke.
Die Frequenz-Konfiguration des CC1101 erfolgt über drei Register: FREQ2 (0x0D), FREQ1 (0x0E) und FREQ0 (0x0F), die zusammen den 24-Bit-Wert $F_{REG}$ bilden. Die Berechnung der tatsächlichen Frequenz basiert auf der CC1101-Dokumentation (Kapitel 18.2 Frequency Programming) und verwendet eine Quarzfrequenz von $26 \, \text{MHz}$ ($F_{XOSC}$).
Die Formel lautet: f_{RF} = \frac{F_{XOSC}}{2^{16}} \times F_{REG}
Bei $F_{XOSC} = 26 \, \text{MHz}$ ergibt sich: f_{RF} = \frac{26}{65536} \times F_{REG} \, \text{MHz}
Entscheidung
Wir implementieren den get/frequency Befehl als Teil der MqttHandler- und Commands-Klassen.
-
MQTT Topic: Der Befehl wird über
cmd/get/frequencyempfangen (komplettes Topic:<base_topic>/commands/get/frequency). -
Antwort Topic: Die Antwort wird über das etablierte Antwort-Topic (
<base_topic>/responses) veröffentlicht, um Konsistenz mit dem bestehendenget/system/versionBefehl zu gewährleisten. Der Payload muss das Feldcommandenthalten, um die Herkunft zu kennzeichnen. -
Berechnungslogik: Die Berechnung wird exakt nach der CC1101-Formel unter Verwendung von $F_{XOSC} = 26 \, \text{MHz}$ durchgeführt.
-
Wir erstellen eine asynchrone Methode in
signalduino/hardware.py(z.B.get_frequency_registers()) zum Auslesen von FREQ2, FREQ1, FREQ0. -
Wir implementieren die Berechnung in
signalduino/commands.py(z.B.get_frequency()), um die Abhängigkeit der Hardware vom Command Layer zu kapseln. -
Das Ergebnis wird auf 4 Dezimalstellen gerundet (in MHz), um eine hohe Genauigkeit bei der Anzeige zu gewährleisten.
-
Konsequenzen
-
Positiv: Benutzer können die eingestellte Frequenz einfach über MQTT abfragen, was die Diagnose erleichtert.
-
Positiv: Die Verwendung der offiziellen CC1101-Formel und der 26 MHz Oszillatorfrequenz gewährleistet Korrektheit und Konsistenz.
-
Negativ: Es müssen neue Methoden in
signalduino/hardware.py,signalduino/commands.pyundsignalduino/mqtt.pyimplementiert werden. -
Negativ: Die Hardware-Klasse muss um die Logik zum Lesen der drei Register erweitert werden, möglicherweise durch eine neue Abstraktionsebene, falls dies in Zukunft für andere Dreifachregister notwendig wird.
Alternativen
-
Alternative 1: Berechnung auf der Hardware-Ebene:
-
Beschreibung: Die Frequenzberechnung direkt in der
Hardware-Klasse durchführen. -
Begründung: Abgelehnt. Die
Commands-Klasse dient als Business-Logik-Schicht, während dieHardware-Klasse die I/O-Schicht ist. Die Berechnung gehört zur Business-Logik, nicht zur reinen Register-Abstraktion. -
Alternative 2: Keine dedizierte Frequenzberechnung:
-
Beschreibung: Nur $F_{REG}$ als rohen 24-Bit-Wert zurückgeben und die Berechnung dem MQTT-Client überlassen.
-
Begründung: Abgelehnt. Dies würde die Komplexität auf die Client-Seite verlagern und die Fehleranfälligkeit erhöhen. Das PySignalduino-Gateway sollte die kanonische Frequenz in einer Standardeinheit (MHz) bereitstellen.
ADR 002: Verwendung des MqttCommandDispatcher für die MQTT-Befehlsbehandlung
Kontext
Die MQTT-Befehlsbehandlung in signalduino/mqtt.py erfolgt derzeit über eine hartcodierte if/elif-Kette in der Methode _handle_command (Zeile 108). Diese Struktur ist schwer wartbar und skaliert schlecht, sobald neue Befehle hinzugefügt werden müssen, wie es für Factory Reset und das Abrufen von Hardware-Einstellungen erforderlich ist.
Der Code enthält bereits einen generischen, schema-validierenden Befehls-Dispatcher, den MqttCommandDispatcher (definiert in signalduino/commands.py). Dieser Dispatcher ist so konzipiert, dass er Befehlspfade gegen eine zentrale COMMAND_MAP prüft, Payloads validiert und die Ausführung an die entsprechenden Methoden im SignalduinoController delegiert.
Die zentrale Verwaltung der Befehle und deren Validierung ist eine bewährte Methode, um die Robustheit und Erweiterbarkeit der Schnittstelle zu gewährleisten.
Entscheidung
Die hartcodierte if/elif-Logik in signalduino/mqtt.py wird durch die Verwendung des MqttCommandDispatcher ersetzt.
-
Der
MqttPublisherinsignalduino/mqtt.pywird eine Instanz desMqttCommandDispatchererhalten. -
Die Methode
_handle_commandinMqttPublisherwird umgeschrieben, um den eingehenden Befehlspfad und Payload direkt anMqttCommandDispatcher.dispatch()zu übergeben. -
Die Fehler- und Erfolgsantworten werden vom
MqttCommandDispatcherzurückgegeben und vonMqttPublisheran die entsprechenden MQTT-Topics (/responsesund/errors) publiziert.
Konsequenzen
Positive Konsequenzen
-
Erweiterbarkeit: Neue MQTT-Befehle (wie Factory Reset und Hardware Settings) können einfach durch Hinzufügen eines Eintrags zur
COMMAND_MAPund der zugehörigen Controller-Methode hinzugefügt werden, ohnesignalduino/mqtt.pyzu ändern. -
Trennung der Zuständigkeiten: Die Verarbeitung der Befehlslogik (Validierung, Mapping, Ausführung) wird von der reinen MQTT-Transportlogik getrennt.
-
Validierung: Alle eingehenden MQTT-Payloads werden automatisch gegen definierte JSON-Schemata validiert, was die Fehleranfälligkeit der Implementierung reduziert.
-
Konsistente Fehlerbehandlung: Erfolgs- und Fehlerantworten werden an zentraler Stelle standardisiert.
Negative Konsequenzen
-
Refactoring-Aufwand: Die bestehende Logik in
signalduino/mqtt.pymuss entfernt und durch den Dispatcher-Aufruf ersetzt werden. -
Kopplung an Controller: Der Dispatcher ist direkt an den
SignalduinoControllergekoppelt (was bereits der Fall war und akzeptiert wird).
Alternativen
-
Beibehaltung der if/elif-Kette: Dies wurde abgelehnt, da es gegen die Prinzipien der Wartbarkeit und der Single Responsibility Principle (SRP) verstößt.
-
Anderer Dispatch-Mechanismus: Die Verwendung des vorhandenen
MqttCommandDispatcherist die pragmatischste Lösung, da die Klasse bereits existiert und die Validierungsinfrastruktur bietet.
003. Verwendung von CC1101-Standardformeln für Frequenz und Datenrate
Kontext
Die Implementierung der MQTT SET-Befehle für CC1101-Parameter (Frequenz, Datenrate) erfordert die Umrechnung von physikalischen Werten (MHz, kBaud) in die spezifischen Registerwerte des CC1101-Chips.
Andere Parameter wie Bandbreite, Sensitivity und Rampl verwendeten früher spezielle, abstraktere Kommandos des Signalduino-Firmware-Protokolls (C101, X4C, X5C).
Um die Steuerung der CC1101-Parameter zu konsolidieren, wurden die Spezialbefehle (X4C, X5C) in der Python-Implementierung durch generische Register-Writes (W) ersetzt, wobei die Umrechnungslogik (z.B.
Index in Registerwert) in Python implementiert wurde.
Für Frequenz und Datenrate existiert keine solche Abstraktion im Signalduino-Protokoll, oder die vorhandene Logik ist unvollständig/unzureichend für eine präzise Steuerung.
Entscheidung
Die Umrechnung von Frequenz (MHz) in die drei Registerwerte (FREQ2, FREQ1, FREQ0) und die Umrechnung der Datenrate (kBaud) in MDMCFG4/MDMCFG3-Registerwerte erfolgt direkt in der Python-Implementierung von SignalduinoCommands unter Verwendung der im CC1101-Datenblatt definierten Standardformeln (z.B.
Freq = f_xosc * FREQ / 2^16).
Diese Registerwerte werden dann über generische CC1101-Schreibbefehle des Signalduino-Protokolls (W<RegisterAddress><Value>) übertragen.
Nach dem Senden aller Register-SET-Befehle muss die Methode SignalduinoCommands.cc1101_write_init() aufgerufen werden, um die CC1101-Konfiguration erneut in das Chip-Register zu schreiben und die Änderungen zu aktivieren.
Konsequenzen
-
Positiv: Gewährleistet maximale Präzision bei der Einstellung von Frequenz und Datenrate, da die direkte CC1101-Berechnung verwendet wird. Die Logik ist in Python gekapselt und leicht testbar (Unit Tests).
-
Negativ: Erhöht die Komplexität der
SignalduinoCommands-Klasse, da sie nun die CC1101-Register-Berechnungslogik enthalten muss. -
Neutral: Alle CC1101-Set-Befehle, die Register schreiben (einschließlich Rampl und Sensitivity), verwenden nun die generischen
W<RegisterAddress><Value>-Befehle. Dies konsolidiert die Logik in Python (phys. Wert → Registerwert) und vereinfacht die Implementierung auf Kosten des Verzichts auf spezifische Firmware-Spezialbefehle (X4C,X5C). DerC101-Befehl für die Bandbreite wird vorerst beibehalten, da er eine Abstraktion der Firmware darstellt.
Alternativen
-
Alternative 1: Nur Signalduino-Spezialbefehle verwenden:
-
Ablehnungsgrund: Für Frequenz und Datenrate gibt es keine oder keine ausreichend präzisen/dokumentierten Signalduino-Spezialbefehle, die eine Einstellung über MQTT in physikalischen Einheiten (MHz, kBaud) ermöglichen.
-
Alternative 2: Berechnung in die Controller-Klasse verschieben:
-
Ablehnungsgrund: Die
SignalduinoCommands-Klasse ist der logische Ort für die Umrechnung von physikalischen Einheiten in serielle Protokolle, da sie die Schnittstelle zum physischen Gerät darstellt. DieSignalduinoController-Klasse soll nur die MQTT-Payload entpacken.
ADR-004: Strukturiertes Parsing serieller Antworten für MQTT GET-Befehle
Kontext
Die MQTT-Befehle get/cc1101/ (z.B. get/cc1101/config) und get/config/decoder schlagen mit Timeouts fehl, obwohl die serielle Kommunikation mit der SIGNALDuino-Firmware die Antworten empfängt. Die Ursache liegt darin, dass der MqttCommandDispatcher eine strukturierte JSON-Payload (ein Python-Dictionary) als data-Feld in der MQTT-Antwort erwartet. Die zugrundeliegenden SignalduinoCommands Methoden geben jedoch in diesen Fällen den *rohen String der seriellen Firmware-Antwort zurück.
Der MqttCommandDispatcher kann diese String-Antworten nicht direkt in das JSON-Antwortformat umwandeln, was zu einem Abbruch der Verarbeitung und damit zum Timeout führt.
Betroffene Befehle und ihre Rohantwortformate:
* get/config/decoder (CG): MS=1;MU=1;MC=1;Mred=1\n
* get/cc1101/config (C0DnF): C0Dn11=<Hex-Wert>\n
Zusätzlich müssen alle get Befehle, die einen rohen String zurückgeben, angepasst werden, um die Konsistenz des MQTT-API zu gewährleisten.
Entscheidung
Wir werden die SignalduinoCommands Methoden, die serielle GET-Befehle ausführen, so modifizieren, dass sie die rohe Firmware-Antwort parsen und ein konsistentes Python-Dictionary (Dict[str, Any]) zurückgeben. Dieses Dictionary wird dann vom MqttCommandDispatcher als JSON-Payload im data-Feld der MQTT-Antwort verwendet.
Dies stellt sicher, dass alle erfolgreichen GET Anfragen über MQTT eine strukturierte und maschinenlesbare JSON-Antwort erhalten und die Timeouts vermieden werden.
Detaillierte Logik-Anpassungen
-
get_config(CG):-
Wird eine private Hilfsfunktion
_parse_decoder_config(response: str) → Dict[str, int]in [signalduino/commands.py](signalduino/commands.py) implementiert. -
Diese Funktion parst den
key=value;String in ein Dictionary (z.B.{'MS': 1, 'MU': 1, 'MC': 1, 'Mred': 1}). -
Der Rückgabetyp von
get_configwird vonstraufDict[str, int]geändert.
-
-
get_ccconf(C0DnF):-
Diese Methode gibt einen String wie
C0Dn11=<Hex-Wert>zurück. -
Die Methode wird angepasst, um die rohe String-Antwort in ein Dictionary zu kapseln, z.B.
{'cc1101_config_string': response_string}. -
Der Rückgabetyp von
get_ccconfwird vonstraufDict[str, str]geändert.
-
-
Weitere einfache GET-Befehle:
-
Methoden wie
get_version,get_free_ram,get_uptimegeben bereits einen geparsten Wert zurück (String oder Int), der korrekt gekapselt wird. Diese Methoden bleiben unverändert, da sie bereits einen strukturierten Wert zurückgeben, der indirekt imdata-Feld des MQTT-Payloads landet.
-
Konsequenzen
Positive
-
Behebung der Timeouts: Die MQTT GET-Befehle für Konfigurationen werden korrekt beantwortet und die Timeouts behoben.
-
API-Konsistenz: Alle MQTT
GETAntworten liefern nun eine konsistente, JSON-serialisierbare Struktur. -
Wartbarkeit: Der Code wird robuster, da das Parsing der seriellen Antwort in der
commands.py-Schicht zentralisiert ist.
Negative
-
Refactoring: Es müssen kleinere Refactorings in [
signalduino/commands.py](signalduino/commands.py) durchgeführt werden, um die Rückgabetypen der Methoden anzupassen. -
Tests/Dokumentation: Die zugehörigen Unittests in [
tests/test_mqtt_commands.py](tests/test_mqtt_commands.py) und die MQTT API Dokumentation in [docs/01_user_guide/mqtt_api.adoc](docs/01_user_guide/mqtt_api.adoc) müssen aktualisiert werden.
Alternativen
-
Alternative 1: Parsing im
MqttCommandDispatcher: Die Rohergebnisse alsstrbeibehalten und das Parsen spezifischer Befehlsantworten direkt imMqttCommandDispatcherdurchführen.-
Nachteil: Vermischt die Zuständigkeiten. Der Dispatcher sollte nur das Routing und die Validierung übernehmen, während die
SignalduinoCommandsdie Logik für die Kommunikation und das Parsen der Firmware-spezifischen Antworten enthalten sollten. -
Abgelehnt wegen schlechter Architektur und Verstoß gegen das Single Responsibility Principle.
-
-
Alternative 2: Globaler, einfacher String-Wrapper im Dispatcher: Jede String-Antwort global in ein einfaches Dictionary wie
{'response': <String>}verpacken.-
Nachteil: Führt zu einer inkonsistenten API, da einige Befehle (wie
get/config/decoder) semantisch reiche, parsbare Daten liefern, die als roher String versteckt wären. -
Abgelehnt zugunsten einer semantisch korrekten, strukturierten Antwort.
-
ADR 005: Vereinheitlichung der MQTT-Antwortstruktur für CC1101-Parameter
Kontext
Aktuell weichen die JSON-Antwortstrukturen für die Abfrage einzelner CC1101-Parameter via MQTT (z.B. Topic get/cc1101/bandwidth) von der Struktur der Gesamt-Abfrage (Topic get/cc1101/settings) ab.
-
Aktuelle Einzelabfrage (angenommen):
get/cc1101/bandwidth→{"bandwidth": "X kHz"} -
Aktuelle Gesamtabfrage (angenommen):
get/cc1101/settings→{"cc1101": {"bandwidth": "X kHz", "rampl": "Y dbm", …}}
Diese Inkonsistenz erschwert die automatisierte Verarbeitung der Antworten, da Clients je nach Abfragetyp unterschiedliche JSON-Pfade parsen müssen. Ziel ist eine konsistente Struktur, bei der die JSON-Knotennamen für die einzelnen Parameter in beiden Abfragetypen identisch sind.
Entscheidung
Die JSON-Antwortstruktur für alle CC1101-Parameter-Abfragen wird vereinheitlicht. Die Schlüsselnamen der einzelnen Parameter in der JSON-Antwort werden in beiden Abfragetypen (Einzelparameter und Gesamt-Settings) identisch verwendet. Es wird entschieden, die Schlüssel der Einzelparameter ohne umschließendes Wrapper-Objekt zu verwenden.
-
Antwort auf
get/cc1101/parameter(z.B.get/cc1101/bandwidth):json {"bandwidth": "X kHz"} -
Antwort auf
get/cc1101/settings:Diejson { "bandwidth": "X kHz", "rampl": "Y dbm", "sensitivity": "Z", "datarate": "A kbps" }settings-Antwort ist somit eine direkte Aggregation der Einzelparameter-Antworten.
Konsequenzen
Positive Konsequenzen
-
Konsistenz: Vereinfacht das Parsen für MQTT-Clients, da die logischen Parameternamen (z.B.
bandwidth) immer als JSON-Schlüssel auf der obersten Ebene der jeweiligen Antwort verwendet werden. -
Wartbarkeit: Reduziert die Komplexität in der Implementierung, da die Logik zur Generierung der Parameterdaten wiederverwendet werden kann.
Negative Konsequenzen
-
Breaking Change: Bestehende Clients, die sich auf eine Wrapper-Struktur wie
{"cc1101": {…}}bei der Gesamt-Abfrage (get/cc1101/settings) verlassen, müssen angepasst werden. -
Migration: Die Server-Logik für die MQTT-Antworten in der PySignalduino-Implementierung muss entsprechend geändert werden.
Alternativen
-
Alternative A: Wrapper in Einzelabfragen beibehalten: Man könnte die Einzelabfrage um den CC1101-Wrapper erweitern (z.B.
get/cc1101/bandwidth→{"cc1101": {"bandwidth": "X kHz"}}). Dies wurde abgelehnt, da es unnötige Verschachtelung für Einzelwerte einführt und die Lesbarkeit des Payloads verschlechtert. -
Alternative B: Einzelabfragen als reiner Wert: Die Antwort könnte nur den reinen Wert zurückgeben (z.B.
get/cc1101/bandwidth→"X kHz"). Dies wurde abgelehnt, da es das JSON-Format verlässt und der Parametername im Payload verloren ginge, was die Eindeutigkeit erschwert.
Komponentendiagramm (Übersicht)
+-------------------+ +-------------------+ +-------------------+
| Transport | | Controller | | MQTT Publisher |
| (Serial/TCP) |----->| (asyncio Tasks) |----->| (aiomqtt) |
+-------------------+ +-------------------+ +-------------------+
^ | |
| v v
+-------------------+ +-------------------+ +-------------------+
| SIGNALDuino | | Parser | | MQTT Broker |
| Hardware |<-----| (SDProtocols) |<-----| (extern) |
+-------------------+ +-------------------+ +-------------------+
-
Transport: Abstrahiert die physikalische Verbindung (asynchrone Lese-/Schreiboperationen).
-
Controller: Orchestriert die drei Haupt-Tasks und verwaltet die Queues.
-
Parser: Wendet die Protokoll‑Definitions‑JSON an und dekodiert Rohdaten.
-
MQTT Publisher: Stellt die Verbindung zum Broker her, publiziert Nachrichten und empfängt Kommandos.
Datenfluss mit asynchronen Queues
-
Empfang: Hardware sendet Rohdaten → Transport liest Zeile → Reader‑Task legt Zeile in
_raw_message_queue. -
Verarbeitung: Parser‑Task entnimmt Zeile, erkennt Protokoll, dekodiert Nachricht.
-
Ausgabe: Dekodierte Nachricht wird an
message_callbackübergeben und/oder via MQTT publiziert. -
Kommando: Externe Quelle (MQTT oder API) ruft
send_commandauf → Kommando landet in_write_queue→ Writer‑Task sendet es an Hardware. -
Antwort: Falls Antwort erwartet wird, wartet der Controller auf das passende Event in
_pending_responses.
Alle Schritte sind asynchron und nicht‑blockierend; Tasks können parallel laufen, solange die Queues nicht leer sind.
Migration von Threading zu Asyncio
Die Architektur wurde von einer threading‑basierten Implementierung (Version 0.8.x) zu einer reinen asyncio‑Implementierung migriert. Wichtige Änderungen:
-
Ersetzung von
threading.Threaddurchasyncio.Task -
Ersetzung von
queue.Queuedurchasyncio.Queue -
Ersetzung von
threading.Eventdurchasyncio.Event -
async/awaitin allen E/A‑Methoden -
Asynchrone Kontextmanager für Ressourcenverwaltung
Details zur Migration sind im Dokument ASYNCIO_MIGRATION.md zu finden.
Dokumentations-Infrastruktur (Sitemap & SEO)
Die PySignalduino-Dokumentation wird automatisch mit einer dynamischen Sitemap und branch-spezifischen robots.txt-Dateien versehen, um die Auffindbarkeit in Suchmaschinen zu verbessern.
Sitemap-Generierung
Die Sitemap wird durch das Python-Skript tools/generate_sitemap.py generiert, das:
-
Den Build-Output-Ordner (
build/site/html) nach HTML-Dateien scannt -
Prioritäten (0.1–1.0) und Update-Frequenzen (
changefreq) basierend auf Dateipfaden zuweist -
Branch-spezifische Base-URLs unterstützt (main:
pysignalduino.rfd-fhem.github.io, preview:preview.rfd-fhem.github.io) -
Gültige XML-Sitemap gemäß sitemaps.org-Schema generiert
-
Fehlerbehandlung und Logging enthält
Das Skript kann manuell ausgeführt werden:
python tools/generate_sitemap.py --build-dir build/site/html --output sitemap.xml --branch main
robots.txt-Konfiguration
Die Datei docs/robots.txt wird im CI/CD-Workflow branch-spezifisch angepasst:
-
main-Branch: Erlaubt Crawling aller Pfade, schließt Preview-/Develop-Pfade aus
-
preview-Branch: Verbietet Crawling des
/preview/-Pfads -
develop-Branch: Verbietet Crawling des
/develop/-Pfads
Zusätzlich wird ein Crawl-delay: 2 gesetzt, um Serverlast zu reduzieren.
Integration in den CI/CD-Workflow
Der GitHub Actions Workflow .github/workflows/docs.yml wurde erweitert, um:
-
Nach dem Asciidoctor-Build das Sitemap-Generierungsskript auszuführen
-
Die robots.txt in das Ausgabeverzeichnis zu kopieren und branch-spezifisch anzupassen
-
Bei Fehlern der Sitemap-Generierung nicht den gesamten Build fehlschlagen zu lassen (
continue-on-error: true)
Statische Fallback-Dateien
Für den Fall, dass die dynamische Generierung fehlschlägt, stehen statische Vorlagen bereit:
-
docs/sitemap_template.xml– Grundlegende Sitemap mit den wichtigsten URLs -
docs/robots.txt– Generische robots.txt-Vorlage
Diese Dateien werden automatisch durch den CI/CD-Workflow verwendet.
SEO-Empfehlungen
-
Die Sitemap wird unter
https://pysignalduino.rfd-fhem.github.io/sitemap.xmlverfügbar sein -
Die robots.txt wird unter
https://pysignalduino.rfd-fhem.github.io/robots.txtverfügbar sein -
Es wird empfohlen, die Sitemap in der Google Search Console und Bing Webmaster Tools einzureichen
|
Note
|
Da PySignalduino noch in aktiver Entwicklung ist, können sich Code-Strukturen und APIs schnell ändern. Bitte synchronisieren Sie Ihren Fork regelmäßig mit dem upstream-Repository. |
Beiträge zum Projekt sind willkommen!
Workflow
-
Fork & Clone: Projekt forken und lokal klonen.
-
Branch: Feature-Branch erstellen (
git checkout -b feature/mein-feature). -
Entwicklung: Änderungen implementieren.
-
Tests: Sicherstellen, dass alle Tests bestehen (
pytest). -
Pull Request: PR auf GitHub öffnen.
Entwicklungsumgebung
Abhängigkeiten installieren
Das Projekt verwendet poetry für die Abhängigkeitsverwaltung. Installieren Sie die Entwicklungsabhängigkeiten mit:
pip install -r requirements-dev.txt
Oder verwenden Sie poetry install (falls Poetry konfiguriert ist).
Die wichtigsten Entwicklungsabhängigkeiten sind:
-
pytest– Testframework -
pytest-mock– Mocking-Unterstützung -
pytest-asyncio– Asyncio-Testunterstützung -
pytest-cov– Code-Coverage -
aiomqtt– Asynchrone MQTT-Client-Bibliothek (für Tests gemockt)
Code-Stil und Linting
Das Projekt folgt PEP 8. Verwenden Sie black für automatische Formatierung und ruff für Linting.
black signalduino/ tests/
ruff check signalduino/ tests/ --fix
Es gibt keine strikte CI-Prüfung, aber konsistenter Stil wird erwartet.
Dokumentationsstil
Für alle AsciiDoc-Dateien im docs/ Verzeichnis ist die Regel "Ein Satz pro Zeile" verpflichtend.
Dies erleichtert die Überprüfung von Änderungen mittels git diff.
|
Important
|
Jeder Satz muss auf einer neuen Zeile beginnen.
Ein Satz endet typischerweise mit einem Punkt ( |
Tests ausführen
Das Projekt nutzt pytest. Stellen Sie sicher, dass requirements-dev.txt installiert ist.
pytest
Für spezifische Testmodule:
pytest tests/test_controller.py -v
pytest tests/test_mqtt.py -v
Asyncio-Tests
Seit der Migration zu asyncio (Version 0.9.0) sind alle Tests asynchron und verwenden pytest-asyncio. Testfunktionen müssen mit @pytest.mark.asyncio dekoriert sein und async def verwenden.
Beispiel:
# Erstelle ein Mock für die Klasse, das die Mock-Instanz zurückgibt
MqttPublisherClassMock = MagicMock(return_value=mock_instance)
# Patch die Klasse in signalduino.controller
with patch("signalduino.controller.MqttPublisher", new=MqttPublisherClassMock) as mock:
yield mock
@pytest.fixture
def mock_controller_initialize(monkeypatch):
"""
Mocking asynchroner Objekte
Verwenden Sie AsyncMock aus unittest.mock, um asynchrone Methoden zu mocken. Achten Sie darauf, asynchrone Kontextmanager (aenter, aexit) korrekt zu mocken.
def mock_transport():
"""Fixture for a mocked async transport layer."""
transport = AsyncMock()
transport.is_open = True
transport.closed = Mock(return_value=False)
transport.write_line = AsyncMock()
async def aopen_mock():
transport.is_open = True
async def aclose_mock():
transport.is_open = False
transport.aopen.side_effect = aopen_mock
transport.aclose.side_effect = aclose_mock
transport.__aenter__.return_value = transport
transport.__aexit__.return_value = None
In Fixtures (siehe tests/conftest.py) werden Transport- und MQTT-Client-Mocks bereitgestellt.
Test-Coverage
Coverage-Bericht generieren:
pytest --cov=signalduino --cov-report=html
Der Bericht wird im Verzeichnis htmlcov/ erstellt.
Code-Stil und Best Practices für asyncio
Allgemeine Richtlinien
-
Verwenden Sie
async/awaitfür alle E/A-Operationen. -
Vermeiden Sie blockierende Aufrufe (z.B.
time.sleep, synchrones Lesen/Schreiben) in asynchronen Kontexten. Nutzen Sie stattdessenasyncio.sleep. -
Nutzen Sie asynchrone Iteratoren (
async for) und Kontextmanager (async with), wo passend.
Asynchrone Queues
-
Verwenden Sie
asyncio.Queuefür die Kommunikation zwischen Tasks. -
Achten Sie auf korrekte Behandlung von
Queue.task_done()undawait queue.join(). -
Setzen Sie angemessene Timeouts, um Deadlocks zu vermeiden.
Fehlerbehandlung
-
Fangen Sie
asyncio.CancelledErrorin Tasks, um saubere Beendigung zu ermöglichen. -
Verwenden Sie
asyncio.TimeoutErrorfür Timeouts beiasyncio.wait_for. -
Protokollieren Sie Ausnahmen mit
logger.exceptioninexcept-Blöcken.
Ressourcenverwaltung
-
Implementieren Sie
aenter/aexitfür Ressourcen, die geöffnet/geschlossen werden müssen (Transport, MQTT-Client). -
Stellen Sie sicher, dass
aexitauch bei Ausnahmen korrekt aufgeräumt wird.
Performance
-
Vermeiden Sie das Erstellen zu vieler gleichzeitiger Tasks; nutzen Sie
asyncio.gathermit angemessener Begrenzung. -
Verwenden Sie
asyncio.create_taskfür Hintergrundtasks, aber behalten Sie Referenzen, um sie später abbrechen zu können.
Pull-Request Prozess
-
Vor dem Einreichen: Stellen Sie sicher, dass Ihr Branch auf dem neuesten Stand von
mainist und alle Tests bestehen. -
Beschreibung: Geben Sie im PR eine klare Beschreibung der Änderungen, des Problems und der Lösung an.
-
Review: Mindestens ein Maintainer muss den PR reviewen und genehmigen.
-
Merge: Nach Genehmigung wird der PR gemergt (Squash-Merge bevorzugt).
Checkliste für PRs
-
❏ Tests hinzugefügt/aktualisiert und alle bestehenden Tests bestehen.
-
❏ Code folgt PEP 8 (Black/Ruff).
-
❏ Dokumentation aktualisiert (falls nötig).
-
❏ Keine neuen Warnungen oder Fehler im Linter.
-
❏ Changelog aktualisiert (optional, wird vom Maintainer übernommen).
AI‑Agenten Richtlinien
Für AI‑Agenten, die Code oder Systemkonfigurationen ändern, gelten zusätzliche verbindliche Vorgaben. Jede Änderung muss eine vollständige Analyse der Auswirkungen auf die zugehörige Dokumentation und die Testsuite umfassen.
Die detaillierten Richtlinien sind in AGENTS.md dokumentiert. Die wichtigsten Pflichten sind:
-
Dokumentationspflicht: Die Dokumentation muss synchron zu allen vorgenommenen Änderungen aktualisiert werden. Betroffen sind das
docs/‑Verzeichnis, Inline‑Kommentare, Docstrings, README.md und andere Markdown‑Dateien. -
Test‑Pflicht: Bestehende Tests sind zu überprüfen und anzupassen; bei Bedarf sind neue Tests zu erstellen, um eine vollständige Testabdeckung der neuen oder modifizierten Logik zu gewährleisten.
-
Verbindlichkeit: Diese Praxis ist für jede Änderung verbindlich und nicht verhandelbar. Ein Commit, der die Dokumentation oder Tests nicht entsprechend anpasst, ist unzulässig.
Vor dem Commit ist die Checkliste in AGENTS.md (Abschnitt „Mandatory Documentation and Test Maintenance“) abzuarbeiten.
Hinweise für Protokoll-Entwicklung
Falls Sie ein neues Funkprotokoll hinzufügen möchten:
-
Fügen Sie die Definition in
sd_protocols/protocols.jsonhinzu. -
Implementieren Sie die Dekodierungsmethode in der entsprechenden Mixin-Klasse (
ManchesterMixin,PostdemodulationMixin, etc.). -
Schreiben Sie Tests für das Protokoll in
tests/test_manchester_protocols.pyoder ähnlich. -
Dokumentieren Sie das Protokoll in
docs/03_protocol_reference/protocol_details.adoc.
Weitere Details finden Sie in der Architektur-Dokumentation (architecture.adoc).
PySignalduino unterstützt eine Vielzahl von Funkprotokollen im 433 MHz und 868 MHz Bereich.
Protokolldefinition
Die Datei sd_protocols/protocols.json ist die definitive Quelle für alle Protokollparameter (Timings, Preambles, Methoden).
Auszug unterstützter Protokolle
-
ID 10: Oregon Scientific v2/v3 (Manchester, 433 MHz)
-
ID 13: Flamingo FA21 Rauchmelder
-
ID 58: TFA Wettersensoren
-
ID 70: FHT80TF Tür-/Fensterkontakt (868 MHz)
-
ID 80: EM1000WZ Energiemonitor
Protokoll-Typen
-
Manchester: Selbsttaktend (z.B. Oregon, TFA).
-
TwoState / PWM: Kodierung über Pulslängen.
-
FSK: Frequenzumtastung (oft bei 868 MHz Geräten wie Lacrosse).
Neues Protokoll hinzufügen
-
Definition in
protocols.jsonergänzen. -
Dekodierungsmethode implementieren (z.B. in
sd_protocols/manchester.py). -
Tests hinzufügen.