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.

Übersicht: Hardware und Funktion
Figure 1. Übersicht: Hardware und Funktion

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.pm in 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 asyncio fü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:

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 (ersetzt paho-mqtt in 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:

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 (ersetze signalduino/v1 durch dein konfiguriertes {base_topic})

  • Payload: Muss eine req_id zur Korrelation der Antwort enthalten. Ist keine req_id im 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 SignalduinoController bereitgestellt)

  • Alle Methoden sind asynchron (async def) und geben entweder str (Antwort) zurück oder None (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_HOST und MQTT_PORT korrekt 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 und asyncio.run() verwendet wird. Verwenden Sie async with für Context-Manager.

  • Tasks hängen oder werden nicht abgebrochen: Alle Hintergrundtasks sollten auf das _stop_event reagieren. Bei manuell erstellten Tasks müssen Sie asyncio.CancelledError abfangen 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 Sie asyncio.wait_for mit 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 Sie aiomqtt>=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.ProactorEventLoop oder 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_task mit 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_queue ab.

  • Parser-Task: Entnimmt Rohzeilen aus der Queue, dekodiert sie über den SignalParser und veröffentlicht Ergebnisse via MQTT oder ruft den message_callback auf.

  • 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:

  1. MqttPublisher (signalduino/mqtt.py) empfängt eine Nachricht auf signalduino/v1/commands/#.

  2. Der SignalduinoController leitet die rohe Payload an den MqttCommandDispatcher weiter.

  3. Der Dispatcher (signalduino/commands.py) validiert die Payload gegen ein JSON-Schema (ADR-002).

  4. Bei Erfolg wird die entsprechende asynchrone Methode im SignalduinoController aufgerufen.

  5. Der Controller sendet serielle Kommandos (W<reg><val>, V, CG) und verpackt die Firmware-Antwort.

  6. Die finale Antwort (status: OK oder error: 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)

signalduino/v1/commands/<type>/<target>/<param>

Steuerung und Abfrage von Parametern (z.B. get/system/version)

Response (Success)

signalduino/v1/responses/<type>/<target>/<param>

Strukturierte Antwort auf Befehle ("status": "OK")

Error (Failure)

signalduino/v1/errors/<type>/<target>/<param>

Strukturierte Fehlerinformationen ("error_code": 400/500/502)

Telemetry

signalduino/v1/state/messages

JSON-serialisierte, dekodierte Sensordaten (DecodedMessage)

Status

signalduino/v1/status/{alive,data}

Heartbeat- und Gerätestatus (z.B. free_ram, uptime)

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.

  1. MQTT Topic: Der Befehl wird über cmd/get/frequency empfangen (komplettes Topic: <base_topic>/commands/get/frequency).

  2. Antwort Topic: Die Antwort wird über das etablierte Antwort-Topic (<base_topic>/responses) veröffentlicht, um Konsistenz mit dem bestehenden get/system/version Befehl zu gewährleisten. Der Payload muss das Feld command enthalten, um die Herkunft zu kennzeichnen.

  3. 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.py und signalduino/mqtt.py implementiert 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 die Hardware-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.

  1. Der MqttPublisher in signalduino/mqtt.py wird eine Instanz des MqttCommandDispatcher erhalten.

  2. Die Methode _handle_command in MqttPublisher wird umgeschrieben, um den eingehenden Befehlspfad und Payload direkt an MqttCommandDispatcher.dispatch() zu übergeben.

  3. Die Fehler- und Erfolgsantworten werden vom MqttCommandDispatcher zurückgegeben und von MqttPublisher an die entsprechenden MQTT-Topics (/responses und /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_MAP und der zugehörigen Controller-Methode hinzugefügt werden, ohne signalduino/mqtt.py zu ä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.py muss entfernt und durch den Dispatcher-Aufruf ersetzt werden.

  • Kopplung an Controller: Der Dispatcher ist direkt an den SignalduinoController gekoppelt (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 MqttCommandDispatcher ist 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). Der C101-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. Die SignalduinoController-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
  1. 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_config wird von str auf Dict[str, int] geändert.

  2. 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_ccconf wird von str auf Dict[str, str] geändert.

  3. Weitere einfache GET-Befehle:

    • Methoden wie get_version, get_free_ram, get_uptime geben 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 im data-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 GET Antworten 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
  1. Alternative 1: Parsing im MqttCommandDispatcher: Die Rohergebnisse als str beibehalten und das Parsen spezifischer Befehlsantworten direkt im MqttCommandDispatcher durchführen.

    • Nachteil: Vermischt die Zuständigkeiten. Der Dispatcher sollte nur das Routing und die Validierung übernehmen, während die SignalduinoCommands die 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.

  2. 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: json { "bandwidth": "X kHz", "rampl": "Y dbm", "sensitivity": "Z", "datarate": "A kbps" } Die 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

  1. Empfang: Hardware sendet Rohdaten → Transport liest Zeile → Reader‑Task legt Zeile in _raw_message_queue.

  2. Verarbeitung: Parser‑Task entnimmt Zeile, erkennt Protokoll, dekodiert Nachricht.

  3. Ausgabe: Dekodierte Nachricht wird an message_callback übergeben und/oder via MQTT publiziert.

  4. Kommando: Externe Quelle (MQTT oder API) ruft send_command auf → Kommando landet in _write_queue → Writer‑Task sendet es an Hardware.

  5. 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.Thread durch asyncio.Task

  • Ersetzung von queue.Queue durch asyncio.Queue

  • Ersetzung von threading.Event durch asyncio.Event

  • async/await in 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:

  1. Den Build-Output-Ordner (build/site/html) nach HTML-Dateien scannt

  2. Prioritäten (0.1–1.0) und Update-Frequenzen (changefreq) basierend auf Dateipfaden zuweist

  3. Branch-spezifische Base-URLs unterstützt (main: pysignalduino.rfd-fhem.github.io, preview: preview.rfd-fhem.github.io)

  4. Gültige XML-Sitemap gemäß sitemaps.org-Schema generiert

  5. 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:

  1. Nach dem Asciidoctor-Build das Sitemap-Generierungsskript auszuführen

  2. Die robots.txt in das Ausgabeverzeichnis zu kopieren und branch-spezifisch anzupassen

  3. 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

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

  1. Fork & Clone: Projekt forken und lokal klonen.

  2. Branch: Feature-Branch erstellen (git checkout -b feature/mein-feature).

  3. Entwicklung: Änderungen implementieren.

  4. Tests: Sicherstellen, dass alle Tests bestehen (pytest).

  5. 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 (.), Ausrufezeichen (!) oder Fragezeichen (?). Achten Sie darauf, dass Codeblöcke und Tabellen nicht betroffen sind.

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/await für alle E/A-Operationen.

  • Vermeiden Sie blockierende Aufrufe (z.B. time.sleep, synchrones Lesen/Schreiben) in asynchronen Kontexten. Nutzen Sie stattdessen asyncio.sleep.

  • Nutzen Sie asynchrone Iteratoren (async for) und Kontextmanager (async with), wo passend.

Asynchrone Queues

  • Verwenden Sie asyncio.Queue für die Kommunikation zwischen Tasks.

  • Achten Sie auf korrekte Behandlung von Queue.task_done() und await queue.join().

  • Setzen Sie angemessene Timeouts, um Deadlocks zu vermeiden.

Fehlerbehandlung

  • Fangen Sie asyncio.CancelledError in Tasks, um saubere Beendigung zu ermöglichen.

  • Verwenden Sie asyncio.TimeoutError für Timeouts bei asyncio.wait_for.

  • Protokollieren Sie Ausnahmen mit logger.exception in except-Blöcken.

Ressourcenverwaltung

  • Implementieren Sie aenter/aexit für Ressourcen, die geöffnet/geschlossen werden müssen (Transport, MQTT-Client).

  • Stellen Sie sicher, dass aexit auch bei Ausnahmen korrekt aufgeräumt wird.

Performance

  • Vermeiden Sie das Erstellen zu vieler gleichzeitiger Tasks; nutzen Sie asyncio.gather mit angemessener Begrenzung.

  • Verwenden Sie asyncio.create_task für Hintergrundtasks, aber behalten Sie Referenzen, um sie später abbrechen zu können.

Pull-Request Prozess

  1. Vor dem Einreichen: Stellen Sie sicher, dass Ihr Branch auf dem neuesten Stand von main ist und alle Tests bestehen.

  2. Beschreibung: Geben Sie im PR eine klare Beschreibung der Änderungen, des Problems und der Lösung an.

  3. Review: Mindestens ein Maintainer muss den PR reviewen und genehmigen.

  4. 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:

  1. Fügen Sie die Definition in sd_protocols/protocols.json hinzu.

  2. Implementieren Sie die Dekodierungsmethode in der entsprechenden Mixin-Klasse (ManchesterMixin, PostdemodulationMixin, etc.).

  3. Schreiben Sie Tests für das Protokoll in tests/test_manchester_protocols.py oder ähnlich.

  4. 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

  1. Definition in protocols.json ergänzen.

  2. Dekodierungsmethode implementieren (z.B. in sd_protocols/manchester.py).

  3. Tests hinzufügen.