Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .roo/mcp.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"mcpServers":{"filesystem":{"command":"npx","args":["-y","@modelcontextprotocol/server-filesystem","/workspaces/PySignalduino"],"alwaysAllow":["edit_file","read_text_file","search_files","read_multiple_files"]},"git":{"command":"uvx","args":["mcp-server-git","--repository","/workspaces/PySignalduino"],"alwaysAllow":["git_diff_unstaged","git_checkout"]}}}
{"mcpServers":{"filesystem":{"command":"npx","args":["-y","@modelcontextprotocol/server-filesystem","/workspaces/PySignalduino"],"alwaysAllow":["edit_file","read_text_file","search_files","read_multiple_files","create_directory","list_directory","directory_tree"]},"git":{"command":"uvx","args":["mcp-server-git","--repository","/workspaces/PySignalduino"],"alwaysAllow":["git_diff_unstaged","git_checkout"]}}}
73 changes: 57 additions & 16 deletions docs/01_user_guide/mqtt_api.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,47 @@ Eine erfolgreiche Response auf `signalduino/v1/responses` hat folgende Struktur:
}
----

[[_cli_tool]]
== CLI Tool zur Steuerung (`tools/sd_mqtt_cli.py`)

Das Skript `tools/sd_mqtt_cli.py` dient als einfaches Python-Kommandozeilen-Tool, um Befehle an das PySignalduino MQTT-Gateway zu senden und die Antworten zu empfangen.

=== Installation und Ausführung

Das Tool benötigt die `paho-mqtt` Abhängigkeit, die in der `requirements-dev.txt` enthalten ist.

[source,bash]
----
pip install paho-mqtt
python3 tools/sd_mqtt_cli.py --help
----

=== Verfügbare Kommandos

|===
| Kommando | Beschreibung | Beispiel

| `reset`
| Führt einen Factory Reset durch (`set/factory_reset`).
| `python3 tools/sd_mqtt_cli.py reset`

| `get all-settings`
| Fragt alle wichtigen CC1101-Einstellungen in einer aggregierten Nachricht ab.
| `python3 tools/sd_mqtt_cli.py get all-settings`

| `get hardware-status --parameter <param>`
| Fragt einen spezifischen CC1101-Parameter ab. Parameter: `frequency`, `bandwidth`, `rampl`, `sensitivity`, `datarate`.
| `python3 tools/sd_mqtt_cli.py get hardware-status --parameter frequency`

| `get system-status --parameter <param>`
| **NEU:** Fragt einen spezifischen System-Parameter ab. Parameter: `version`, `freeram`, `uptime`.
| `python3 tools/sd_mqtt_cli.py get system-status --parameter freeram`

| `poll`
| **NEU:** Fragt nacheinander alle verfügbaren System- und CC1101-Parameter ab. Nützlich zur Diagnose des aktuellen Gerätezustands.
| `python3 tools/sd_mqtt_cli.py poll`
|===

[[_get_commands]]
== GET Commands (Status und Konfiguration abrufen)

Expand All @@ -79,51 +120,51 @@ GET-Befehle benötigen eine leere Payload (`{}`) oder nur eine `req_id`.
| Firmware-Version.

| `get/system/freeram`
| `"1234"`
| Verfügbarer RAM-Speicher.
| `1234`
| Verfügbarer RAM-Speicher (`int`).

| `get/system/uptime`
| `"56789"`
| System-Laufzeit.
| `56789`
| System-Laufzeit (`int`).

| `get/config/decoder`
| `"MS=1;MU=1;MC=1;MN=1"`
| Aktuelle Decoder-Konfiguration (aktivierte Protokollfamilien).
| `{"MS": 1, "MU": 1, "MC": 1, "MN": 1}`
| Aktuelle Decoder-Konfiguration (aktivierte Protokollfamilien) als geparstes Dictionary.

| `get/cc1101/config`
| `"C0D11=0F"`
| CC1101 Konfigurationsregister-Dump.
| `{"cc1101_config_string": "C0D11=0F"}`
| CC1101 Konfigurationsregister-Dump als gekapselter String.

| `get/cc1101/patable`
| `"C3E = C0 C1 C2 C3 C4 C5 C6 C7"`
| `{"pa_table_hex": "C3E = C0 C1 C2 C3 C4 C5 C6 C7"}`
| CC1101 PA-Tabelle.

| `get/cc1101/register`
| `"C00 = 29"`
| `{"register_value": "C00 = 29"}`
| Liest den Wert eines einzelnen CC1101-Registers (Adresse 0x00). Der Befehl nimmt keinen Wert in der Payload entgegen und liest standardmäßig Register 0x00.

| `get/cc1101/frequency`
| `{"frequency_mhz": 868.3500}`
| `{"frequency": 868.3500}`
| Aktuelle RF-Frequenz in MHz.

| `get/cc1101/bandwidth`
| `102.0`
| `{"bandwidth": 102.0}`
| Aktuelle IF-Bandbreite in kHz.

| `get/cc1101/rampl`
| `30`
| `{"rampl": 30}`
| Aktuelle Empfängerverstärkung (LNA Gain) in dB. Mögliche Werte: `24, 27, 30, 33, 36, 38, 40, 42`.

| `get/cc1101/sensitivity`
| `12`
| `{"sensitivity": 12}`
| Aktuelle Empfindlichkeit in dB. Mögliche Werte: `4, 8, 12, 16`.

| `get/cc1101/datarate`
| `4.8`
| `{"datarate": 4.8}`
| Aktuelle Datenrate in kBaud.

| `get/cc1101/settings`
| `{"frequency_mhz": 868.35, "bandwidth": 102.0, "rampl": 30, "sens": 12, "datarate": 4.8}`
| `{"frequency": 868.35, "bandwidth": 102.0, "rampl": 30, "sensitivity": 12, "datarate": 4.8}`
| Aggregierte Abfrage aller CC1101-Haupteinstellungen.
|===

Expand Down
57 changes: 57 additions & 0 deletions docs/architecture/decisions/ADR-004-mqtt-response-parsing.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
= ADR-004: Strukturiertes Parsing serieller Antworten für MQTT GET-Befehle
:revdate: 2026-01-06
:author: Roo

== 1. 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.

== 2. 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.

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

== 4. 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.
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
= ADR 005: Vereinheitlichung der MQTT-Antwortstruktur für CC1101-Parameter
:doctype: article :encoding: utf-8 :lang: de :status: Proposed :decided-at: 2026-01-07 :decided-by: Roo
:toc: left

[[kontext]]
== 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]]
== 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]]
== 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]]
== 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.
Loading
Loading