Skip to content

Commit

Permalink
Merge pull request #32 from kardia-as/fix-radio-probing
Browse files Browse the repository at this point in the history
Fix radio probing configuration and clean some stuff
  • Loading branch information
DamKast authored Nov 17, 2023
2 parents 2647936 + 5505497 commit f61fcfc
Show file tree
Hide file tree
Showing 10 changed files with 91 additions and 85 deletions.
27 changes: 12 additions & 15 deletions zigpy_zboss/api.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Module for nRF api interface."""
"""Module for ZBOSS api interface."""
from __future__ import annotations

import asyncio
Expand Down Expand Up @@ -31,11 +31,11 @@
DEFAULT_TIMEOUT = 5


class NRF:
"""Class linking zigpy with the nRF SoC."""
class ZBOSS:
"""Class linking zigpy with ZBOSS running on nRF SoC."""

def __init__(self, config: conf.ConfigType):
"""Initialize NRF class."""
"""Initialize ZBOSS class."""
self._uart = None
self._app = None
self._config = config
Expand All @@ -54,7 +54,7 @@ def __init__(self, config: conf.ConfigType):
self._ncp_debug = None

def set_application(self, app):
"""Set the application using the NRF class."""
"""Set the application using the ZBOSS class."""
assert self._app is None
self._app = app

Expand All @@ -63,8 +63,8 @@ def _port_path(self) -> str:
return self._config[conf.CONF_DEVICE][conf.CONF_DEVICE_PATH]

@property
def _nrf_config(self) -> conf.ConfigType:
return self._config[conf.CONF_NRF_CONFIG]
def _zboss_config(self) -> conf.ConfigType:
return self._config[conf.CONF_ZBOSS_CONFIG]

async def connect(self) -> None:
"""Connect to serial device.
Expand Down Expand Up @@ -98,16 +98,16 @@ def connection_lost(self, exc) -> None:
"""Port has been closed.
Called by the UART object to indicate that the port was closed.
Propagates up to the `ControllerApplication` that owns this NRF
Propagates up to the `ControllerApplication` that owns this ZBOSS
instance.
"""
LOGGER.debug("We were disconnected from %s: %s", self._port_path, exc)

def close(self) -> None:
"""Clean up resources, namely the listener queues.
Calling this will reset NRF to the same internal state as a fresh NRF
instance.
Calling this will reset ZBOSS to the same internal state as a fresh
ZBOSS instance.
"""
self._app = None
self.version = None
Expand Down Expand Up @@ -140,10 +140,7 @@ def frame_received(self, frame: Frame) -> bool:

command_cls = c.COMMANDS_BY_ID[frame.hl_packet.header]

try:
command, _ = command_cls.from_frame(frame)
except ValueError:
raise
command = command_cls.from_frame(frame)

LOGGER.debug("Received command: %s", command)
matched = False
Expand Down Expand Up @@ -253,7 +250,7 @@ def wait_for_response(self, response: t.CommandBase) -> asyncio.Future:

def remove_listener(self, listener: BaseResponseListener) -> None:
"""
Unbinds a listener from NRF.
Unbinds a listener from ZBOSS.
Used by `wait_for_responses` to remove listeners for completed futures,
regardless of their completion reason.
Expand Down
4 changes: 2 additions & 2 deletions zigpy_zboss/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def validator(config):
return validator


CONF_NRF_CONFIG = "nrf_config"
CONF_ZBOSS_CONFIG = "zboss_config"
CONF_TX_POWER = "tx_power"
CONF_LED_MODE = "led_mode"
CONF_SKIP_BOOTLOADER = "skip_bootloader"
Expand All @@ -74,7 +74,7 @@ def validator(config):
CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
{
vol.Required(CONF_DEVICE): SCHEMA_DEVICE,
vol.Optional(CONF_NRF_CONFIG, default={}): vol.Schema(
vol.Optional(CONF_ZBOSS_CONFIG, default={}): vol.Schema(
vol.All(
{
vol.Optional(CONF_TX_POWER, default=None): vol.Any(
Expand Down
6 changes: 3 additions & 3 deletions zigpy_zboss/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Zigpy nrf exceptions."""
"""Zigpy Zboss exceptions."""


class InvalidFrame(ValueError):
Expand All @@ -17,5 +17,5 @@ class ModuleHardwareResetError(Exception):
"""Module hardware reset error."""


class NrfResponseError(Exception):
"""nRF response error."""
class ZbossResponseError(Exception):
"""ZBOSS response error."""
2 changes: 2 additions & 0 deletions zigpy_zboss/nvram.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ async def read(self, nv_id: t.DatasetId, item_type):
DatasetId=nv_id
)
)
if res.StatusCode != 0:
return

if not res.DatasetId == nv_id:
raise
Expand Down
4 changes: 2 additions & 2 deletions zigpy_zboss/tools/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# zigpy-zboss tools

- Factory reset the nRF NCP:
- Factory reset the zboss NCP:

`$ python -m zigpy_zboss.tools.factory_reset_ncp`
`$ python -m zigpy_zboss.tools.factory_reset_ncp <device path>`

- Get firmware, zigbee stack and protocol version:

Expand Down
8 changes: 4 additions & 4 deletions zigpy_zboss/tools/factory_reset_ncp.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@
import serial
import asyncio

from zigpy_zboss.api import NRF
from zigpy_zboss.api import ZBOSS
from zigpy_zboss import types as t

from zigpy_zboss.tools.config import get_config


async def factory_reset_ncp(config):
"""Send factory reset command to NCP."""
nrf = NRF(config)
await nrf.connect()
await nrf.reset(option=t.ResetOptions(2))
zboss = ZBOSS(config)
await zboss.connect()
await zboss.reset(option=t.ResetOptions(2))


async def main(argv):
Expand Down
8 changes: 4 additions & 4 deletions zigpy_zboss/tools/get_ncp_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@
import serial
import asyncio

from zigpy_zboss.api import NRF
from zigpy_zboss.api import ZBOSS

from zigpy_zboss.tools.config import get_config


async def get_ncp_version(config):
"""Get the NCP firmware version."""
nrf = NRF(config)
await nrf.connect()
version = await nrf.version()
zboss = ZBOSS(config)
await zboss.connect()
version = await zboss.version()
print("Current NCP versions: \n"
f"FW: {version[0]}\n"
f"Stack: {version[1]}\n"
Expand Down
15 changes: 8 additions & 7 deletions zigpy_zboss/types/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,13 @@ def from_frame(cls, frame, *, align=False) -> "CommandBase":
else:
params[param.name], data = param.type.deserialize(data)
except ValueError:
if frame.hl_packet.header.control_type == ControlType.RSP:
# If the response to a request failed, the status code
# is different from 0 and the NCP does not send more data.
# Return a partial command object including the status.
status_code = params["StatusCode"]
if status_code != 0:
return cls(**params, partial=True)
if not data and param.optional:
# If we're out of data and the parameter is optional,
# we're done
Expand All @@ -517,13 +524,7 @@ def from_frame(cls, frame, *, align=False) -> "CommandBase":
else:
# Otherwise, let the exception happen
raise

# if data:
# raise ValueError(
# f"Frame {frame} contains trailing data after parsing: {data}"
# )

return cls(**params), data
return cls(**params)

def matches(self, other: "CommandBase") -> bool:
"""Match parameters and values with other CommandBase."""
Expand Down
94 changes: 50 additions & 44 deletions zigpy_zboss/zigbee/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@
import zigpy_zboss.config as conf

from typing import Any, Dict
from zigpy_zboss.api import NRF
from zigpy_zboss.api import ZBOSS
from zigpy_zboss import commands as c
from zigpy.exceptions import DeliveryError
from .device import NrfCoordinator, NrfDevice
from zigpy_zboss.exceptions import NrfResponseError
from .device import ZbossCoordinator, ZbossDevice
from zigpy_zboss.exceptions import ZbossResponseError
from zigpy_zboss.config import CONFIG_SCHEMA, SCHEMA_DEVICE

LOGGER = logging.getLogger(__name__)
Expand All @@ -40,19 +40,19 @@ class ControllerApplication(zigpy.application.ControllerApplication):
def __init__(self, config: Dict[str, Any]):
"""Initialize instance."""
super().__init__(config=zigpy.config.ZIGPY_SCHEMA(config))
self._api: NRF | None = None
self._api: ZBOSS | None = None
self._reset_task = None
self.version = None

async def connect(self):
"""Connect to the zigbee module."""
assert self._api is None
is_responsive = await self.probe(self.config)
is_responsive = await self.probe(self.config.get(conf.CONF_DEVICE, {}))
if not is_responsive:
raise NrfResponseError
nrf = NRF(self.config)
await nrf.connect()
self._api = nrf
raise ZbossResponseError
zboss = ZBOSS(self.config)
await zboss.connect()
self._api = zboss
self._api.set_application(self)
self._bind_callbacks()

Expand All @@ -76,7 +76,7 @@ async def start_network(self):

await self.register_endpoints()

self.devices[self.state.node_info.ieee] = NrfCoordinator(
self.devices[self.state.node_info.ieee] = ZbossCoordinator(
self, self.state.node_info.ieee, self.state.node_info.nwk
)

Expand Down Expand Up @@ -348,35 +348,37 @@ async def load_network_info(self, *, load_devices=False):
t_zboss.DatasetId.ZB_IB_COUNTERS,
t_zboss.DSIbCounters
)
self.state.network_info.network_key = zigpy.state.Key(
key=common.nwk_key,
tx_counter=counters.nib_counter,
rx_counter=0,
seq=common.nwk_key_seq,
partner_ieee=self.state.node_info.ieee,
)

if self.state.node_info.logical_type == \
zdo_t.LogicalType.Coordinator:
self.state.network_info.tc_link_key = zigpy.state.Key(
key=common.tc_standard_key,
tx_counter=0,
if common and counters:
self.state.network_info.network_key = zigpy.state.Key(
key=common.nwk_key,
tx_counter=counters.nib_counter,
rx_counter=0,
seq=0,
seq=common.nwk_key_seq,
partner_ieee=self.state.node_info.ieee,
)
else:
res = await self._api.request(
c.NcpConfig.GetTrustCenterAddr.Req(TSN=self.get_sequence()))
self.state.network_info.tc_link_key = (
zigpy.state.Key(
key=None,

if self.state.node_info.logical_type == \
zdo_t.LogicalType.Coordinator:
self.state.network_info.tc_link_key = zigpy.state.Key(
key=common.tc_standard_key,
tx_counter=0,
rx_counter=0,
seq=0,
partner_ieee=res.TCIEEE,
),
)
partner_ieee=self.state.node_info.ieee,
)
else:
res = await self._api.request(
c.NcpConfig.GetTrustCenterAddr.Req(
TSN=self.get_sequence()))
self.state.network_info.tc_link_key = (
zigpy.state.Key(
key=None,
tx_counter=0,
rx_counter=0,
seq=0,
partner_ieee=res.TCIEEE,
),
)

res = await self._api.request(
c.NcpConfig.GetRxOnWhenIdle.Req(TSN=self.get_sequence()))
Expand Down Expand Up @@ -466,35 +468,39 @@ def permit_with_key(self, node, code, time_s=60):
raise NotImplementedError

@property
def nrf_config(self) -> conf.ConfigType:
"""Shortcut property to access the NRF radio config."""
return self.config[conf.CONF_NRF_CONFIG]
def zboss_config(self) -> conf.ConfigType:
"""Shortcut property to access the ZBOSS radio config."""
return self.config[conf.CONF_ZBOSS_CONFIG]

@classmethod
async def probe(cls, device_config: dict) -> bool:
async def probe(
cls, device_config: dict[str, Any]) -> bool | dict[str, Any]:
"""Probe the NCP.
Checks whether the NCP device is responding to request.
Checks whether the NCP device is responding to requests.
"""
nrf = NRF(device_config)
config = cls.SCHEMA(
{conf.CONF_DEVICE: cls.SCHEMA_DEVICE(device_config)})
zboss = ZBOSS(config)
try:
await nrf.connect()
await zboss.connect()
async with async_timeout.timeout(PROBE_TIMEOUT):
await nrf.request(
await zboss.request(
c.NcpConfig.GetZigbeeRole.Req(TSN=1), timeout=1)
return True
except asyncio.TimeoutError:
return False
else:
return device_config
finally:
nrf.close()
zboss.close()

# Overwrites zigpy because of custom ZDO layer required for ZBOSS.
def add_device(self, ieee: t.EUI64, nwk: t.NWK):
"""Create zigpy `Device` object with the provided IEEE and NWK addr."""
assert isinstance(ieee, t.EUI64)
# TODO: Shut down existing device

dev = NrfDevice(self, ieee, nwk)
dev = ZbossDevice(self, ieee, nwk)
self.devices[ieee] = dev
return dev

Expand Down
8 changes: 4 additions & 4 deletions zigpy_zboss/zigbee/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from zigpy.zdo import ZDO as ZigpyZDO


class NrfZDO(ZigpyZDO):
class ZbossZDO(ZigpyZDO):
"""The ZDO endpoint of a device."""

def handle_mgmt_permit_joining_req(
Expand Down Expand Up @@ -225,18 +225,18 @@ async def Mgmt_NWK_Update_req(self, nwkUpdate):
return (None, res.ScannedChannels, None, None, res.EnergyValues)


class NrfDevice(zigpy.device.Device):
class ZbossDevice(zigpy.device.Device):
"""Class representing an nRF device."""

def __init__(self, *args, **kwargs):
"""Initialize instance."""
super().__init__(*args, **kwargs)
assert hasattr(self, "zdo")
self.zdo = NrfZDO(self)
self.zdo = ZbossZDO(self)
self.endpoints[0] = self.zdo


class NrfCoordinator(NrfDevice):
class ZbossCoordinator(ZbossDevice):
"""Zigpy Device representing the controller."""

def __init__(self, *args, **kwargs):
Expand Down

0 comments on commit f61fcfc

Please sign in to comment.