44from typing import Optional
55
66from ..events import Event , EventType
7+ from ..packets import CommandType
78from .base import CommandHandlerBase , DestinationType , _validate_destination
89
910logger = logging .getLogger ("meshcore" )
@@ -13,7 +14,7 @@ class DeviceCommands(CommandHandlerBase):
1314 async def send_appstart (self ) -> Event :
1415 logger .debug ("Sending appstart command" )
1516 b1 = bytearray (b"\x01 \x03 mccli" )
16- return await self .send (b1 , [EventType .SELF_INFO ])
17+ return await self .send (b1 , [EventType .SELF_INFO , EventType . ERROR ])
1718
1819 async def send_device_query (self ) -> Event :
1920 logger .debug ("Sending device query command" )
@@ -129,32 +130,50 @@ async def set_other_params_from_infos(self, infos) -> Event:
129130 return await self .send (data , [EventType .OK , EventType .ERROR ])
130131
131132 async def set_telemetry_mode_base (self , telemetry_mode_base : int ) -> Event :
132- infos = (await self .send_appstart ()).payload
133+ result = await self .send_appstart ()
134+ if result .is_error ():
135+ return result
136+ infos = result .payload
133137 infos ["telemetry_mode_base" ] = telemetry_mode_base
134138 return await self .set_other_params_from_infos (infos )
135139
136140 async def set_telemetry_mode_loc (self , telemetry_mode_loc : int ) -> Event :
137- infos = (await self .send_appstart ()).payload
141+ result = await self .send_appstart ()
142+ if result .is_error ():
143+ return result
144+ infos = result .payload
138145 infos ["telemetry_mode_loc" ] = telemetry_mode_loc
139146 return await self .set_other_params_from_infos (infos )
140147
141148 async def set_telemetry_mode_env (self , telemetry_mode_env : int ) -> Event :
142- infos = (await self .send_appstart ()).payload
149+ result = await self .send_appstart ()
150+ if result .is_error ():
151+ return result
152+ infos = result .payload
143153 infos ["telemetry_mode_env" ] = telemetry_mode_env
144154 return await self .set_other_params_from_infos (infos )
145155
146156 async def set_manual_add_contacts (self , manual_add_contacts : bool ) -> Event :
147- infos = (await self .send_appstart ()).payload
157+ result = await self .send_appstart ()
158+ if result .is_error ():
159+ return result
160+ infos = result .payload
148161 infos ["manual_add_contacts" ] = manual_add_contacts
149162 return await self .set_other_params_from_infos (infos )
150163
151164 async def set_advert_loc_policy (self , advert_loc_policy : int ) -> Event :
152- infos = (await self .send_appstart ()).payload
165+ result = await self .send_appstart ()
166+ if result .is_error ():
167+ return result
168+ infos = result .payload
153169 infos ["adv_loc_policy" ] = advert_loc_policy
154170 return await self .set_other_params_from_infos (infos )
155171
156172 async def set_multi_acks (self , multi_acks : int ) -> Event :
157- infos = (await self .send_appstart ()).payload
173+ result = await self .send_appstart ()
174+ if result .is_error ():
175+ return result
176+ infos = result .payload
158177 infos ["multi_acks" ] = multi_acks
159178 return await self .set_other_params_from_infos (infos )
160179
@@ -273,20 +292,89 @@ async def sign(self, data: bytes, chunk_size: int = 120, timeout: Optional[float
273292
274293 return await self .sign_finish (timeout = timeout , data_size = len (data ))
275294
295+ async def has_connection (self ) -> Event :
296+ """N09: Check if the device has an active connection (CMD 28).
297+
298+ Returns:
299+ Event with a 1-byte response indicating connection status,
300+ or ERROR.
301+ """
302+ logger .debug ("Checking device connection status" )
303+ return await self .send (b"\x1c " , [EventType .OK , EventType .ERROR ])
304+
305+ async def get_tuning (self ) -> Event :
306+ """N03/N09: Request current tuning parameters (CMD_GET_TUNING_PARAMS = 43).
307+
308+ Firmware responds with RESP_CODE_TUNING_PARAMS (23): 9 bytes containing
309+ rx_delay (4 bytes LE) and airtime_factor (4 bytes LE).
310+
311+ Returns:
312+ Event of type TUNING_PARAMS with rx_delay and airtime_factor,
313+ or ERROR.
314+ """
315+ logger .debug ("Getting tuning parameters" )
316+ return await self .send (b"\x2b " , [EventType .TUNING_PARAMS , EventType .ERROR ])
317+
318+ async def request_factory_reset (self ) -> str :
319+ """N09: Request a factory reset token (step 1 of 2).
320+
321+ This method returns a confirmation token string. Pass it to
322+ ``confirm_factory_reset(token)`` to actually execute the reset.
323+ The two-step pattern is a Python-side safety measure; the firmware
324+ itself has no token verification.
325+
326+ Returns:
327+ A confirmation token string to pass to confirm_factory_reset().
328+ """
329+ import secrets
330+ token = secrets .token_hex (8 )
331+ logger .warning (
332+ "Factory reset requested. Call confirm_factory_reset('%s') to proceed. "
333+ "This will ERASE ALL DATA on the device." , token
334+ )
335+ # Store the token on the instance for validation
336+ self ._factory_reset_token = token
337+ return token
338+
339+ async def confirm_factory_reset (self , token : str ) -> Event :
340+ """N09: Execute factory reset after token confirmation (step 2 of 2).
341+
342+ Args:
343+ token: The token returned by request_factory_reset().
344+
345+ Returns:
346+ Event with OK or ERROR.
347+
348+ Raises:
349+ ValueError: If the token does not match.
350+ """
351+ expected = getattr (self , "_factory_reset_token" , None )
352+ if expected is None or token != expected :
353+ raise ValueError (
354+ "Invalid or expired factory reset token. "
355+ "Call request_factory_reset() first."
356+ )
357+ self ._factory_reset_token = None # Consume the token
358+ logger .warning ("Executing factory reset — all device data will be erased" )
359+ return await self .send (b"\x33 " , [EventType .OK , EventType .ERROR ])
360+
276361 async def get_stats_core (self ) -> Event :
277362 logger .debug ("Getting core statistics" )
278- # CMD_GET_STATS (56) + STATS_TYPE_CORE (0)
279- return await self .send (b"\x38 \x00 " , [EventType .STATS_CORE , EventType .ERROR ])
363+ # R04: Use CommandType enum instead of literal bytes
364+ cmd = bytes ([CommandType .GET_STATS .value , 0x00 ]) # GET_STATS + STATS_TYPE_CORE
365+ return await self .send (cmd , [EventType .STATS_CORE , EventType .ERROR ])
280366
281367 async def get_stats_radio (self ) -> Event :
282368 logger .debug ("Getting radio statistics" )
283- # CMD_GET_STATS (56) + STATS_TYPE_RADIO (1)
284- return await self .send (b"\x38 \x01 " , [EventType .STATS_RADIO , EventType .ERROR ])
369+ # R04: Use CommandType enum instead of literal bytes
370+ cmd = bytes ([CommandType .GET_STATS .value , 0x01 ]) # GET_STATS + STATS_TYPE_RADIO
371+ return await self .send (cmd , [EventType .STATS_RADIO , EventType .ERROR ])
285372
286373 async def get_stats_packets (self ) -> Event :
287374 logger .debug ("Getting packet statistics" )
288- # CMD_GET_STATS (56) + STATS_TYPE_PACKETS (2)
289- return await self .send (b"\x38 \x02 " , [EventType .STATS_PACKETS , EventType .ERROR ])
375+ # R04: Use CommandType enum instead of literal bytes
376+ cmd = bytes ([CommandType .GET_STATS .value , 0x02 ]) # GET_STATS + STATS_TYPE_PACKETS
377+ return await self .send (cmd , [EventType .STATS_PACKETS , EventType .ERROR ])
290378
291379 async def get_allowed_repeat_freq (self ) -> Event :
292380 logger .debug ("Getting allowed repeat freqs" )
0 commit comments