2323
2424import bellows .config as conf
2525from bellows .exception import EzspError , InvalidCommandError
26+ from bellows .ezsp import xncp
2627from bellows .ezsp .config import DEFAULT_CONFIG , RuntimeConfig , ValueConfig
28+ from bellows .ezsp .xncp import FirmwareFeatures , FlowControlType
2729import bellows .types as t
2830import bellows .uart
2931
@@ -62,6 +64,7 @@ def __init__(self, device_config: dict, application: Any | None = None):
6264 self ._callbacks = {}
6365 self ._ezsp_event = asyncio .Event ()
6466 self ._ezsp_version = v4 .EZSPv4 .VERSION
67+ self ._xncp_features = FirmwareFeatures .NONE
6568 self ._gw = None
6669 self ._protocol = None
6770 self ._application = application
@@ -124,6 +127,7 @@ async def startup_reset(self) -> None:
124127 await self .reset ()
125128
126129 await self .version ()
130+ await self .get_xncp_features ()
127131
128132 async def connect (self , * , use_thread : bool = True ) -> None :
129133 assert self ._gw is None
@@ -167,13 +171,22 @@ async def version(self):
167171 if ver != self .ezsp_version :
168172 self ._switch_protocol_version (ver )
169173 await self ._command ("version" , desiredProtocolVersion = ver )
174+
170175 LOGGER .debug (
171- "EZSP Stack Type: %s, Stack Version: %04x, Protocol version: %s" ,
176+ ( "EZSP Stack Type: %s" " , Stack Version: %04x" " , Protocol version: %s") ,
172177 stack_type ,
173178 stack_version ,
174179 ver ,
175180 )
176181
182+ async def get_xncp_features (self ) -> None :
183+ try :
184+ self ._xncp_features = await self .xncp_get_supported_firmware_features ()
185+ except InvalidCommandError :
186+ self ._xncp_features = xncp .FirmwareFeatures .NONE
187+
188+ LOGGER .debug ("XNCP features: %s" , self ._xncp_features )
189+
177190 async def disconnect (self ):
178191 self .stop_ezsp ()
179192 if self ._gw :
@@ -308,11 +321,10 @@ async def get_board_info(
308321 ) -> tuple [str , str , str | None ] | tuple [None , None , str | None ]:
309322 """Return board info."""
310323
311- tokens = {}
324+ tokens : dict [ t . EzspMfgTokenId , str | None ] = {}
312325
313- for token in (t .EzspMfgTokenId .MFG_STRING , t .EzspMfgTokenId .MFG_BOARD_NAME ):
314- (value ,) = await self .getMfgToken (tokenId = token )
315- LOGGER .debug ("Read %s token: %s" , token .name , value )
326+ for token_id in (t .EzspMfgTokenId .MFG_STRING , t .EzspMfgTokenId .MFG_BOARD_NAME ):
327+ value = await self .get_mfg_token (token_id )
316328
317329 # Tokens are fixed-length and initially filled with \xFF but also can end
318330 # with \x00
@@ -324,10 +336,7 @@ async def get_board_info(
324336 except UnicodeDecodeError :
325337 result = "0x" + value .hex ().upper ()
326338
327- if not result :
328- result = None
329-
330- tokens [token ] = result
339+ tokens [token_id ] = result or None
331340
332341 (status , ver_info_bytes ) = await self .getValue (
333342 valueId = t .EzspValueId .VALUE_VERSION_INFO
@@ -342,6 +351,14 @@ async def get_board_info(
342351 special , ver_info_bytes = t .uint8_t .deserialize (ver_info_bytes )
343352 version = f"{ major } .{ minor } .{ patch } .{ special } build { build } "
344353
354+ try :
355+ build_string = await self .xncp_get_build_string ()
356+ except InvalidCommandError :
357+ build_string = None
358+
359+ if build_string :
360+ version = f"{ version } ({ build_string } )"
361+
345362 return (
346363 tokens [t .EzspMfgTokenId .MFG_STRING ],
347364 tokens [t .EzspMfgTokenId .MFG_BOARD_NAME ],
@@ -369,9 +386,23 @@ async def _get_nv3_restored_eui64_key(self) -> t.NV3KeyId | None:
369386
370387 return None
371388
389+ async def get_mfg_token (self , token : t .EzspMfgTokenId ) -> bytes :
390+ (value ,) = await self .getMfgToken (tokenId = token )
391+ LOGGER .debug ("Read manufacturing token %s: %s" , token .name , value )
392+
393+ override_value = None
394+
395+ if FirmwareFeatures .MFG_TOKEN_OVERRIDES in self ._xncp_features :
396+ with contextlib .suppress (InvalidCommandError ):
397+ override_value = await self .xncp_get_mfg_token_override (token )
398+
399+ LOGGER .debug ("XNCP override token %s: %s" , token .name , override_value )
400+
401+ return override_value or value
402+
372403 async def _get_mfg_custom_eui_64 (self ) -> t .EUI64 | None :
373404 """Get the custom EUI 64 manufacturing token, if it has a valid value."""
374- ( data ,) = await self .getMfgToken ( tokenId = t .EzspMfgTokenId .MFG_CUSTOM_EUI_64 )
405+ data = await self .get_mfg_token ( t .EzspMfgTokenId .MFG_CUSTOM_EUI_64 )
375406
376407 # Manufacturing tokens do not exist in RCP firmware: all reads are empty
377408 if not data :
@@ -616,3 +647,53 @@ async def write_config(self, config: dict) -> None:
616647 status ,
617648 )
618649 continue
650+
651+ async def send_xncp_frame (
652+ self , payload : xncp .XncpCommandPayload
653+ ) -> xncp .XncpCommandPayload :
654+ """Send an XNCP frame."""
655+ req_frame = xncp .XncpCommand .from_payload (payload )
656+ LOGGER .debug ("Sending XNCP frame: %s" , req_frame )
657+ status , data = await self .customFrame (req_frame .serialize ())
658+
659+ if status != t .EmberStatus .SUCCESS :
660+ raise InvalidCommandError ("XNCP is not supported" )
661+
662+ rsp_frame = xncp .XncpCommand .from_bytes (data )
663+ LOGGER .debug ("Received XNCP frame: %s" , rsp_frame )
664+
665+ if rsp_frame .status != t .EmberStatus .SUCCESS :
666+ raise InvalidCommandError (f"XNCP response error: { rsp_frame .status } " )
667+
668+ return rsp_frame .payload
669+
670+ async def xncp_get_supported_firmware_features (self ) -> xncp .FirmwareFeatures :
671+ """Get supported firmware extensions."""
672+ rsp = await self .send_xncp_frame (xncp .GetSupportedFeaturesReq ())
673+ return rsp .features
674+
675+ async def xncp_set_manual_source_route (
676+ self , destination : t .NWK , route : list [t .NWK ]
677+ ) -> None :
678+ """Set a manual source route."""
679+ await self .send_xncp_frame (
680+ xncp .SetSourceRouteReq (
681+ destination = destination ,
682+ source_route = route ,
683+ )
684+ )
685+
686+ async def xncp_get_mfg_token_override (self , token : t .EzspMfgTokenId ) -> bytes :
687+ """Get manufacturing token override."""
688+ rsp = await self .send_xncp_frame (xncp .GetMfgTokenOverrideReq (token = token ))
689+ return rsp .value
690+
691+ async def xncp_get_build_string (self ) -> str :
692+ """Get build string."""
693+ rsp = await self .send_xncp_frame (xncp .GetBuildStringReq ())
694+ return rsp .build_string .decode ("utf-8" )
695+
696+ async def xncp_get_flow_control_type (self ) -> FlowControlType :
697+ """Get flow control type."""
698+ rsp = await self .send_xncp_frame (xncp .GetFlowControlTypeReq ())
699+ return rsp .flow_control_type
0 commit comments