Skip to content

Commit f8ebbad

Browse files
committed
feat: commands and persistence for mqtt
1 parent 8964ba5 commit f8ebbad

2 files changed

Lines changed: 191 additions & 0 deletions

File tree

signalduino/commands.py

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
"""
2+
Encapsulates all serial commands for the SIGNALDuino firmware.
3+
"""
4+
5+
from typing import Any, Callable, Optional, Pattern
6+
import re
7+
8+
class SignalduinoCommands:
9+
"""
10+
Provides methods to construct and send commands to the SIGNALDuino.
11+
12+
This class abstracts the raw serial commands documented in AI_AGENT_COMMANDS.md.
13+
"""
14+
15+
def __init__(self, send_command_func: Callable[[str, bool, float, Optional[Pattern[str]]], Any]):
16+
"""
17+
Initialize with a function to send commands.
18+
19+
Args:
20+
send_command_func: A callable that accepts (payload, expect_response, timeout, response_pattern)
21+
and returns the response (if expected).
22+
"""
23+
self._send = send_command_func
24+
25+
# --- System Commands ---
26+
27+
def get_version(self, timeout: float = 2.0) -> str:
28+
"""Query firmware version (V)."""
29+
pattern = re.compile(r"V\s.*SIGNAL(?:duino|ESP|STM).*", re.IGNORECASE)
30+
return self._send("V", expect_response=True, timeout=timeout, response_pattern=pattern)
31+
32+
def get_help(self) -> str:
33+
"""Show help (?)."""
34+
return self._send("?", expect_response=True, timeout=2.0, response_pattern=None)
35+
36+
def get_free_ram(self) -> str:
37+
"""Query free RAM (R)."""
38+
return self._send("R", expect_response=True, timeout=2.0, response_pattern=None)
39+
40+
def get_uptime(self) -> str:
41+
"""Query uptime in seconds (t)."""
42+
return self._send("t", expect_response=True, timeout=2.0, response_pattern=None)
43+
44+
def ping(self) -> str:
45+
"""Ping device (P)."""
46+
return self._send("P", expect_response=True, timeout=2.0, response_pattern=re.compile(r"OK"))
47+
48+
def get_cc1101_status(self) -> str:
49+
"""Query CC1101 status (s)."""
50+
return self._send("s", expect_response=True, timeout=2.0, response_pattern=None)
51+
52+
def disable_receiver(self) -> None:
53+
"""Disable reception (XQ)."""
54+
self._send("XQ", expect_response=False, timeout=0, response_pattern=None)
55+
56+
def enable_receiver(self) -> None:
57+
"""Enable reception (XE)."""
58+
self._send("XE", expect_response=False, timeout=0, response_pattern=None)
59+
60+
def factory_reset(self) -> str:
61+
"""Factory reset CC1101 and load EEPROM defaults (e)."""
62+
return self._send("e", expect_response=True, timeout=5.0, response_pattern=None)
63+
64+
# --- Configuration Commands ---
65+
66+
def get_config(self) -> str:
67+
"""Read configuration (CG)."""
68+
return self._send("CG", expect_response=True, timeout=2.0, response_pattern=None)
69+
70+
def set_decoder_state(self, decoder: str, enabled: bool) -> None:
71+
"""
72+
Configure decoder (C<CMD><FLAG>).
73+
74+
Args:
75+
decoder: One of 'MS', 'MU', 'MC', 'Mred', 'AFC', 'WMBus', 'WMBus_T'
76+
Internal mapping: S=MS, U=MU, C=MC, R=Mred, A=AFC, W=WMBus, T=WMBus_T
77+
enabled: True to enable, False to disable
78+
"""
79+
decoder_map = {
80+
"MS": "S",
81+
"MU": "U",
82+
"MC": "C",
83+
"Mred": "R",
84+
"AFC": "A",
85+
"WMBus": "W",
86+
"WMBus_T": "T"
87+
}
88+
if decoder not in decoder_map:
89+
raise ValueError(f"Unknown decoder: {decoder}")
90+
91+
cmd_char = decoder_map[decoder]
92+
flag_char = "E" if enabled else "D"
93+
command = f"C{cmd_char}{flag_char}"
94+
self._send(command, expect_response=False, timeout=0, response_pattern=None)
95+
96+
def set_manchester_min_bit_length(self, length: int) -> str:
97+
"""Set MC Min Bit Length (CSmcmbl=<val>)."""
98+
return self._send(f"CSmcmbl={length}", expect_response=True, timeout=2.0, response_pattern=None)
99+
100+
def read_cc1101_register(self, register: int) -> str:
101+
"""Read CC1101 register (C<reg>). Register is int, sent as 2-digit hex."""
102+
reg_hex = f"{register:02X}"
103+
return self._send(f"C{reg_hex}", expect_response=True, timeout=2.0, response_pattern=None)
104+
105+
def write_register(self, register: int, value: int) -> str:
106+
"""Write to EEPROM/CC1101 register (W<reg><val>)."""
107+
reg_hex = f"{register:02X}"
108+
val_hex = f"{value:02X}"
109+
return self._send(f"W{reg_hex}{val_hex}", expect_response=True, timeout=2.0, response_pattern=None)
110+
111+
def init_wmbus(self) -> str:
112+
"""Initialize WMBus mode (WS34)."""
113+
return self._send("WS34", expect_response=True, timeout=2.0, response_pattern=None)
114+
115+
def read_eeprom(self, address: int) -> str:
116+
"""Read EEPROM byte (r<addr>)."""
117+
addr_hex = f"{address:02X}"
118+
return self._send(f"r{addr_hex}", expect_response=True, timeout=2.0, response_pattern=None)
119+
120+
def read_eeprom_block(self, address: int) -> str:
121+
"""Read EEPROM block (r<addr>n)."""
122+
addr_hex = f"{address:02X}"
123+
return self._send(f"r{addr_hex}n", expect_response=True, timeout=2.0, response_pattern=None)
124+
125+
def set_patable(self, value: int) -> str:
126+
"""Write PA Table (x<val>)."""
127+
val_hex = f"{value:02X}"
128+
return self._send(f"x{val_hex}", expect_response=True, timeout=2.0, response_pattern=None)
129+
130+
# --- Send Commands ---
131+
# These typically don't expect a response, or the response is just an echo/OK which might be hard to sync with async rx
132+
133+
def send_combined(self, params: str) -> None:
134+
"""Send Combined (SC...). params should be the full string after SC, e.g. ';R=4...'"""
135+
self._send(f"SC{params}", expect_response=False, timeout=0, response_pattern=None)
136+
137+
def send_manchester(self, params: str) -> None:
138+
"""Send Manchester (SM...). params should be the full string after SM."""
139+
self._send(f"SM{params}", expect_response=False, timeout=0, response_pattern=None)
140+
141+
def send_raw(self, params: str) -> None:
142+
"""Send Raw (SR...). params should be the full string after SR."""
143+
self._send(f"SR{params}", expect_response=False, timeout=0, response_pattern=None)
144+
145+
def send_xfsk(self, params: str) -> None:
146+
"""Send xFSK (SN...). params should be the full string after SN."""
147+
self._send(f"SN{params}", expect_response=False, timeout=0, response_pattern=None)

signalduino/persistence.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import json
2+
import os
3+
import uuid
4+
import logging
5+
from typing import Optional
6+
7+
# Todo: Pfad anpassen
8+
CLIENT_ID_FILE = os.path.join(os.path.expanduser("~"), ".signalduino_id")
9+
logger = logging.getLogger(__name__)
10+
11+
def get_or_create_client_id() -> str:
12+
"""
13+
Liest die persistente Client-ID aus der Datei oder generiert eine neue und speichert sie.
14+
"""
15+
client_id = None
16+
17+
# 1. Versuche, die ID aus der Konfigurationsdatei zu lesen
18+
try:
19+
if os.path.exists(CLIENT_ID_FILE):
20+
with open(CLIENT_ID_FILE, "r", encoding="utf-8") as f:
21+
config = json.load(f)
22+
client_id = config.get("client_id")
23+
except Exception as e:
24+
logger.warning("Fehler beim Lesen der Client-ID aus %s: %s", CLIENT_ID_FILE, e)
25+
26+
# 2. Wenn keine ID gefunden wurde, generiere eine neue
27+
if not client_id:
28+
client_id = f"signalduino-{uuid.uuid4().hex}"
29+
logger.info("Neue Client-ID generiert: %s", client_id)
30+
31+
# 3. Speichere die ID persistent
32+
try:
33+
with open(CLIENT_ID_FILE, "w", encoding="utf-8") as f:
34+
json.dump({"client_id": client_id}, f, indent=4)
35+
logger.info("Client-ID dauerhaft gespeichert in %s", CLIENT_ID_FILE)
36+
except Exception as e:
37+
logger.error("Fehler beim Speichern der Client-ID in %s: %s", CLIENT_ID_FILE, e)
38+
39+
return client_id
40+
41+
if __name__ == "__main__":
42+
# Beispiel für die Verwendung
43+
logging.basicConfig(level=logging.INFO)
44+
print(f"Client ID: {get_or_create_client_id()}")

0 commit comments

Comments
 (0)