Skip to content

Commit 36708f2

Browse files
author
sidey79
committed
feat: implement mqtt get/set frequency commands
1 parent 2ed9ad2 commit 36708f2

9 files changed

Lines changed: 656 additions & 22 deletions

File tree

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,4 @@ SIGNALDuino-Firmware/
88
.devcontainer/.devcontainer.env
99
.devcontainer/mosquitto/data/
1010
.devcontainer/mosquitto/log/
11-
.roo/mcp.json
11+
.roo/mcp.json

docs/01_user_guide/usage.adoc

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ include::../../main.py[lines=55..84]
4848

4949
* `{topic}/messages` – JSON‑kodierte dekodierte Nachrichten (DecodedMessage)
5050
* `{topic}/commands/#` – Topic für eingehende Befehle (Wildcard-Subscription)
51-
* `{topic}/result/{command}` – Antworten auf Befehle (z. B. `signalduino/result/version`)
51+
* `{topic}/responses` – Antworten auf GET-Befehle, inkl. `get/cc1101/frequency`.
52+
* `{topic}/errors` – Fehlerantworten.
5253
* `{topic}/status` – Heartbeat‑ und Statusmeldungen (optional)
5354

5455
==== Heartbeat-Funktionalität
@@ -109,17 +110,83 @@ Befehle, die die Hardware-Konfiguration ändern (z. B. `write_register`, `set_
109110

110111
==== Nutzung über MQTT
111112

112-
Wenn MQTT aktiviert ist, können Befehle über das Topic `signalduino/commands/{command}` gesendet werden. Die Antwort erscheint unter `signalduino/result/{command}`.
113+
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`.
114+
115+
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.
116+
117+
===== CC1101 Frequenz abfragen (`get/cc1101/frequency`)
118+
119+
Dieser Befehl fragt die aktuell im CC1101-Transceiver eingestellte Funkfrequenz ab.
120+
121+
**1. Request Topic und Payload (Senden)**
122+
123+
* **Topic:** `signalduino/v1/commands/get/cc1101/frequency` (ersetze `signalduino/v1` durch dein konfiguriertes `{base_topic}`)
124+
* **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.
125+
[source,json]
126+
----
127+
{
128+
"req_id": "client-12345-freq-req-A"
129+
}
130+
----
131+
132+
**2. Response Topic und Payload (Empfangen)**
133+
134+
* **Erfolgs-Topic:** `signalduino/v1/responses`
135+
* **Fehler-Topic:** `signalduino/v1/errors`
136+
137+
*Erfolgreiche Antwort (auf \`signalduino/v1/responses\`):*
138+
[source,json]
139+
----
140+
{
141+
"command": "get/cc1101/frequency",
142+
"success": true,
143+
"req_id": "client-12345-freq-req-A",
144+
"payload": {
145+
"frequency_mhz": 433.920
146+
}
147+
}
148+
----
149+
150+
*Fehlerhafte Antwort (auf \`signalduino/v1/errors\`):*
151+
[source,json]
152+
----
153+
{
154+
"command": "get/cc1101/frequency",
155+
"success": false,
156+
"req_id": "client-12345-freq-req-A",
157+
"error": "Hardware nicht initialisiert"
158+
}
159+
----
160+
161+
Beispiel mit `mosquitto_pub` und `mosquitto_sub` (angenommen `base_topic` ist `signalduino/v1`):
162+
163+
[source,bash]
164+
----
165+
# Zum Senden des Requests
166+
mosquitto_pub -h localhost -t "signalduino/v1/commands/get/cc1101/frequency" -m '{"req_id": "test-123"}'
167+
168+
# Zum Empfangen der Antwort
169+
mosquitto_sub -h localhost -t "signalduino/v1/responses"
170+
----
171+
172+
===== Allgemeine Command-Topics
173+
174+
Alle anderen allgemeinen Befehle folgen ebenfalls diesem Schema, wobei `{command}` den Pfad nach `/commands/` darstellt.
113175

114176
Beispiel mit `mosquitto_pub`:
115177

116178
[source,bash]
117179
----
118-
include::../examples/bash/mosquitto_pub_example.sh[]
180+
# Sende Request für System-Version
181+
mosquitto_pub -h localhost -t "signalduino/v1/commands/get/system/version" -m '{"req_id": "test-version"}'
182+
183+
# Antwort empfängst du auf signalduino/v1/responses
119184
----
120185

121186
==== Code-Beispiel: Direkte Nutzung der Command-API
122187

188+
==== Code-Beispiel: Direkte Nutzung der Command-API
189+
123190
[source,python]
124191
----
125192
include::../../tests/test_controller.py[lines=120..130]
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
= 001. Implementierung des MQTT-Befehls get/frequency
2+
:author: Roo
3+
:revdate: 2026-01-03
4+
:status: Accepted
5+
6+
[[status]]
7+
== Status
8+
9+
Angenommen.
10+
11+
[[context]]
12+
== Kontext
13+
14+
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.
15+
16+
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}$).
17+
18+
Die Formel lautet:
19+
$$f_{RF} = \frac{F_{XOSC}}{2^{16}} \times F_{REG}$$
20+
21+
Bei $F_{XOSC} = 26 \, \text{MHz}$ ergibt sich:
22+
$$f_{RF} = \frac{26}{65536} \times F_{REG} \, \text{MHz}$$
23+
24+
[[decision]]
25+
== Entscheidung
26+
27+
Wir implementieren den `get/frequency` Befehl als Teil der `MqttHandler`- und `Commands`-Klassen.
28+
29+
1. *MQTT Topic*: Der Befehl wird über `cmd/get/frequency` empfangen (komplettes Topic: `<base_topic>/commands/get/frequency`).
30+
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.
31+
3. *Berechnungslogik*: Die Berechnung wird exakt nach der CC1101-Formel unter Verwendung von $F_{XOSC} = 26 \, \text{MHz}$ durchgeführt.
32+
* Wir erstellen eine asynchrone Methode in `signalduino/hardware.py` (z.B. `get_frequency_registers()`) zum Auslesen von FREQ2, FREQ1, FREQ0.
33+
* Wir implementieren die Berechnung in `signalduino/commands.py` (z.B. `get_frequency()`), um die Abhängigkeit der Hardware vom Command Layer zu kapseln.
34+
* Das Ergebnis wird auf 4 Dezimalstellen gerundet (in MHz), um eine hohe Genauigkeit bei der Anzeige zu gewährleisten.
35+
36+
[[consequences]]
37+
== Konsequenzen
38+
39+
* *Positiv*: Benutzer können die eingestellte Frequenz einfach über MQTT abfragen, was die Diagnose erleichtert.
40+
* *Positiv*: Die Verwendung der offiziellen CC1101-Formel und der 26 MHz Oszillatorfrequenz gewährleistet Korrektheit und Konsistenz.
41+
* *Negativ*: Es müssen neue Methoden in `signalduino/hardware.py`, `signalduino/commands.py` und `signalduino/mqtt.py` implementiert werden.
42+
* *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.
43+
44+
[[alternatives]]
45+
== Alternativen
46+
47+
* *Alternative 1: Berechnung auf der Hardware-Ebene:*
48+
* *Beschreibung:* Die Frequenzberechnung direkt in der `Hardware`-Klasse durchführen.
49+
* *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.
50+
51+
* *Alternative 2: Keine dedizierte Frequenzberechnung:*
52+
* *Beschreibung:* Nur $F_{REG}$ als rohen 24-Bit-Wert zurückgeben und die Berechnung dem MQTT-Client überlassen.
53+
* *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.
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
= Architektur-Proposal: MQTT-Kommando `get/cc1101/frequency`
2+
:author: Roo
3+
:revdate: 2026-01-03
4+
:page-layout: proposal
5+
:sectnums:
6+
:toc: left
7+
:toclevels: 3
8+
9+
[[section-hintergrund]]
10+
== 1. Hintergrund und Motivation
11+
12+
Dieses Proposal beschreibt die Implementierung des MQTT-Kommandos `get/cc1101/frequency`, das es Benutzern ermöglicht, die aktuell im CC1101-Transceiver eingestellte Funkfrequenz abzufragen. Dies ist ein notwendiges Feature zur Diagnose und Verifikation der Hardwarekonfiguration und stellt eine Ergänzung zu den bestehenden `get`-Kommandos dar.
13+
14+
[[section-losungsansatz]]
15+
== 2. Lösungsansatz und Komponenten
16+
17+
Der Befehl wird über das MQTT-Topic `[base_topic]/commands/get/cc1101/frequency` empfangen und löst eine Kette von Funktionsaufrufen aus:
18+
19+
1. *`signalduino/mqtt.py`*: Empfängt das Kommando und ruft die Kommando-Logik auf.
20+
2. *`signalduino/commands.py`*: Implementiert die High-Level-Logik, welche die Registerwerte von der Hardware abruft und die Frequenz berechnet.
21+
3. *`signalduino/hardware.py`*: Stellt eine neue Methode zum Lesen der FREQ2, FREQ1 und FREQ0 Register bereit.
22+
23+
Das Ergebnis wird als JSON-Objekt auf dem zentralen Antwort-Topic (`<base_topic>/responses`) veröffentlicht, um Konsistenz mit bestehenden Befehlen zu gewährleisten.
24+
25+
[[section-komponenten-details]]
26+
== 3. Komponenten-Details
27+
28+
=== 3.1. `signalduino/hardware.py`
29+
30+
Wir benötigen eine Methode, um die drei Frequenzregister des CC1101 auszulesen. Da PySignalduino bereits die Methode `read_register(address)` in der Hardware-Klasse (wahrscheinlich in `signalduino/hardware.py`) implementiert, ist eine neue, dedizierte Methode in der `Hardware`-Klasse erforderlich.
31+
32+
[source, python]
33+
----
34+
# In signalduino/hardware.py (angenommene Klasse)
35+
async def get_frequency_registers(self) -> int:
36+
"""Liest die CC1101 Frequenzregister (FREQ2, FREQ1, FREQ0) und kombiniert sie zu einem 24-Bit-Wert (F_REG)."""
37+
# Adressen der Register
38+
FREQ2 = 0x0D
39+
FREQ1 = 0x0E
40+
FREQ0 = 0x0F
41+
42+
# Annahme: read_register gibt den Wert des Registers zurück
43+
freq2 = await self.read_register(FREQ2)
44+
freq1 = await self.read_register(FREQ1)
45+
freq0 = await self.read_register(FREQ0)
46+
47+
# Die Register bilden eine 24-Bit-Zahl: (FREQ2 << 16) | (FREQ1 << 8) | FREQ0
48+
f_reg = (freq2 << 16) | (freq1 << 8) | freq0
49+
return f_reg
50+
----
51+
52+
=== 3.2. `signalduino/commands.py`
53+
54+
Die Logik zur Frequenzberechnung wird hier implementiert. Die Frequenzformel lautet:
55+
$$f_{RF} = \frac{26000000}{65536} \times F_{REG} \times 10^{-6} \, \text{MHz}$$
56+
oder vereinfacht:
57+
$$f_{RF} = \frac{26}{65536} \times F_{REG} \, \text{MHz}$$
58+
59+
[source, python]
60+
----
61+
# In signalduino/commands.py
62+
async def get_frequency(self) -> float:
63+
"""Ruft die Frequenzregister ab und berechnet die Frequenz in MHz."""
64+
# F_REG: 24-Bit-Wert aus FREQ2, FREQ1, FREQ0
65+
f_reg = await self.hardware.get_frequency_registers()
66+
67+
# Quarzfrequenz (FXOSC) ist 26 MHz
68+
DIVIDER = 65536.0
69+
70+
# Die Frequenz in MHz ist: (26.0 / 65536.0) * F_REG
71+
frequency_mhz = (26.0 / DIVIDER) * f_reg
72+
73+
return frequency_mhz
74+
----
75+
76+
=== 3.3. `signalduino/mqtt.py`
77+
78+
Ein neuer Command Handler wird in der `MqttHandler`-Klasse registriert. Die Antwort folgt dem `<base_topic>/responses` Schema.
79+
80+
[source, python]
81+
----
82+
# In signalduino/mqtt.py (_handle_command Methode)
83+
elif command_name == "get/cc1101/frequency":
84+
try:
85+
# Payload-String zu Dict konvertieren
86+
payload_dict = json.loads(payload)
87+
req_id = payload_dict.get("req_id", "NO_REQ_ID")
88+
89+
# Aufruf der asynchronen Controller-Methode
90+
frequency_mhz = await self.controller.get_frequency(payload_dict)
91+
92+
response = {
93+
"command": command_name,
94+
"success": True,
95+
"req_id": req_id,
96+
"payload": {
97+
"frequency_mhz": round(frequency_mhz, 4)
98+
},
99+
}
100+
101+
await self.publish_simple(
102+
subtopic="responses",
103+
payload=json.dumps(response),
104+
retain=False
105+
)
106+
self.logger.info("Successfully published current frequency for req_id %s.", req_id)
107+
108+
except Exception as e:
109+
self.logger.exception("Error processing %s command.", command_name)
110+
111+
# Versuch, req_id zu extrahieren, falls JSON-Parsing erfolgreich war
112+
try:
113+
req_id = json.loads(payload).get("req_id", "NO_REQ_ID")
114+
except json.JSONDecodeError:
115+
req_id = "NO_REQ_ID"
116+
117+
await self.publish_simple(
118+
subtopic="errors",
119+
payload=json.dumps({
120+
"command": command_name,
121+
"success": False,
122+
"req_id": req_id,
123+
"error": f"Internal error processing command: {e}",
124+
}),
125+
retain=False
126+
)
127+
128+
# Registrierung des Handlers im Haupt-Loop ist implizit über die _handle_command Methode
129+
----
130+
131+
[[section-mqtt-payload]]
132+
== 4. MQTT Request- und Response-Payload-Format
133+
134+
=== 4.1. Request Payload (auf \`[base_topic]/commands/get/cc1101/frequency\`)
135+
136+
Der Request MUSS einen JSON-Payload enthalten, der eine `req_id` zur Korrelation der Antwort bereitstellt.
137+
138+
[cols="1,1,2"]
139+
|===
140+
| Feld | Typ | Beschreibung
141+
| `req_id` | string | Eine vom Client generierte eindeutige ID, um die Antwort (Response/Error) dem Request zuzuordnen. Wird keine `req_id` bereitgestellt, wird automatisch der Wert `"NO_REQ_ID"` verwendet.
142+
|===
143+
144+
*Beispiel Request Payload:*
145+
[source, json]
146+
----
147+
{
148+
"req_id": "client-12345-freq-req-A"
149+
}
150+
----
151+
152+
153+
=== 4.2. Response Payload (auf \`<base_topic>/responses\`)
154+
155+
Die Antwort wird auf dem Topic `<base_topic>/responses` im JSON-Format veröffentlicht (bei Erfolg).
156+
157+
[cols="1,1,2"]
158+
|===
159+
| Feld | Typ | Beschreibung
160+
| `command` | string | Der ursprünglich ausgeführte Befehl (`get/cc1101/frequency`).
161+
| `success` | boolean | Status der Operation (`true` oder `false`).
162+
| `req_id` | string | Die `req_id` aus dem Request-Payload.
163+
| `payload` | object | Enthält die Nutzdaten (nur bei `success: true`).
164+
| `payload.frequency_mhz` | number | Die berechnete Frequenz in MHz, gerundet auf 4 Dezimalstellen.
165+
| `error` | string | Nur bei `success: false`, enthält die Fehlerbeschreibung.
166+
|===
167+
168+
*Erfolgreiche Antwort (auf \`<base_topic>/responses\`):*
169+
[source, json]
170+
----
171+
{
172+
"command": "get/cc1101/frequency",
173+
"success": true,
174+
"req_id": "client-12345-freq-req-A",
175+
"payload": {
176+
"frequency_mhz": 433.920
177+
}
178+
}
179+
----
180+
181+
*Fehlerhafte Antwort (auf \`<base_topic>/errors\`):*
182+
[source, json]
183+
----
184+
{
185+
"command": "get/cc1101/frequency",
186+
"success": false,
187+
"req_id": "client-12345-freq-req-A",
188+
"error": "Hardware nicht initialisiert"
189+
}
190+
----
191+
192+
[[section-sequenzdiagramm]]
193+
== 5. Sequenzdiagramm
194+
195+
Das folgende Sequenzdiagramm visualisiert den Nachrichtenfluss für den `get/cc1101/frequency`-Befehl.
196+
197+
[mermaid]
198+
----
199+
sequenceDiagram
200+
participant U as Benutzer (MQTT Client)
201+
participant M as MqttHandler (signalduino/mqtt.py)
202+
participant C as Commands (signalduino/commands.py)
203+
participant H as Hardware (signalduino/hardware.py)
204+
205+
U->>M: PUBLISH <base_topic>/commands/get/cc1101/frequency {req_id: "..."}
206+
M->>C: get_frequency({req_id: "..."})
207+
C->>H: get_frequency_registers()
208+
H->>H: read_register(FREQ2)
209+
H->>H: read_register(FREQ1)
210+
H->>H: read_register(FREQ0)
211+
H-->>C: F_REG (24-bit integer)
212+
C->>C: Berechne Frequenz: (26.0 / 65536.0) * F_REG
213+
C-->>M: frequency_mhz (float)
214+
M->>M: Erstelle JSON Payload {command: "get/cc1101/frequency", success: true, req_id: "...", payload: {frequency_mhz: 433.920}}
215+
M->>U: PUBLISH <base_topic>/responses {payload}
216+
----

0 commit comments

Comments
 (0)