Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions bellows/exception.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ class InvalidCommandError(EzspError):
pass


class PayloadTooLongError(InvalidCommandError):
pass


class InvalidCommandPayload(InvalidCommandError):
def __init__(self, msg: str, raw_bytes: bytes) -> None:
super().__init__(msg)
Expand Down
48 changes: 47 additions & 1 deletion bellows/ezsp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,16 @@

from bellows.ash import NcpFailure
import bellows.config as conf
from bellows.exception import EzspError, InvalidCommandError, InvalidCommandPayload
from bellows.exception import (
EzspError,
InvalidCommandError,
InvalidCommandPayload,
PayloadTooLongError,
)
from bellows.ezsp import xncp
from bellows.ezsp.config import DEFAULT_CONFIG, RuntimeConfig, ValueConfig
from bellows.ezsp.xncp import (
MAX_XNCP_PAYLOAD_LENGTH,
FirmwareFeatures,
FlowControlType,
GetRouteTableEntryRsp,
Expand Down Expand Up @@ -785,6 +791,46 @@ async def xncp_set_manual_source_route(
)
)

def xncp_prepare_unicast(
self,
destination: t.NWK,
aps_frame: t.EmberApsFrame,
message_tag: t.uint8_t,
data: bytes,
*,
source_route: list[t.NWK] | None = None,
extended_timeout: tuple[t.EUI64, bool] | None = None,
) -> xncp.SendUnicastReq:
"""Prepare a combined unicast command."""
flags = xncp.SendUnicastFlags.NONE
req = xncp.SendUnicastReq(
flags=flags,
destination=destination,
aps_frame=aps_frame,
message_tag=message_tag,
data=xncp.Bytes(data),
)

if extended_timeout is not None:
req.flags |= xncp.SendUnicastFlags.EXTENDED_TIMEOUT
req.ieee, req.extended_timeout = extended_timeout

if source_route is not None:
req.flags |= xncp.SendUnicastFlags.SOURCE_ROUTE
req.source_route = source_route

if len(req.serialize()) > MAX_XNCP_PAYLOAD_LENGTH:
raise PayloadTooLongError()

return req

async def xncp_send_unicast(
self, request: xncp.SendUnicastReq
) -> tuple[t.sl_Status, t.uint8_t]:
"""Send a combined unicast command."""
rsp = await self.send_xncp_frame(request)
return rsp.status, rsp.sequence

async def xncp_get_mfg_token_override(self, token: t.EzspMfgTokenId) -> bytes:
"""Get manufacturing token override."""
rsp = await self.send_xncp_frame(xncp.GetMfgTokenOverrideReq(token=token))
Expand Down
54 changes: 53 additions & 1 deletion bellows/ezsp/xncp.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,19 @@

import zigpy.types as t

from bellows.types import EmberStatus, EzspMfgTokenId, RouteRecordStatus
from bellows.types import (
EmberApsFrame,
EmberStatus,
EzspMfgTokenId,
RouteRecordStatus,
sl_Status,
)

_LOGGER = logging.getLogger(__name__)

MAX_XNCP_FRAME_LENGTH = 119
MAX_XNCP_PAYLOAD_LENGTH = MAX_XNCP_FRAME_LENGTH - 3 # u16 + u8

COMMANDS: dict[XncpCommandId, type[XncpCommandPayload]] = {}
REV_COMMANDS: dict[type[XncpCommandPayload], XncpCommandId] = {}

Expand Down Expand Up @@ -43,6 +52,7 @@ class XncpCommandId(t.enum16):
SET_ROUTE_TABLE_ENTRY_REQ = 0x0006
GET_ROUTE_TABLE_ENTRY_REQ = 0x0007
GET_TX_POWER_INFO_REQ = 0x0008
SEND_UNICAST_REQ = 0x0009

GET_SUPPORTED_FEATURES_RSP = GET_SUPPORTED_FEATURES_REQ | 0x8000
SET_SOURCE_ROUTE_RSP = SET_SOURCE_ROUTE_REQ | 0x8000
Expand All @@ -53,6 +63,7 @@ class XncpCommandId(t.enum16):
SET_ROUTE_TABLE_ENTRY_RSP = SET_ROUTE_TABLE_ENTRY_REQ | 0x8000
GET_ROUTE_TABLE_ENTRY_RSP = GET_ROUTE_TABLE_ENTRY_REQ | 0x8000
GET_TX_POWER_INFO_RSP = GET_TX_POWER_INFO_REQ | 0x8000
SEND_UNICAST_RSP = SEND_UNICAST_REQ | 0x8000

UNKNOWN = 0xFFFF

Expand Down Expand Up @@ -123,6 +134,10 @@ class FirmwareFeatures(t.bitmap32):
# Recommended and maximum TX power can be queried by country code
TX_POWER_INFO = 1 << 7

# A unicast can be sent, its source route and extended timeout applied, in a
# single command
COMBINED_SEND = 1 << 8


class XncpCommandPayload(t.Struct):
pass
Expand Down Expand Up @@ -233,6 +248,43 @@ class GetTxPowerInfoRsp(XncpCommandPayload):
max_power_dbm: t.int8s


class SendUnicastFlags(t.bitmap8):
NONE = 0

# The extended timeout for the destination should be set before sending
EXTENDED_TIMEOUT = 1 << 0

# A manual source route should be installed before sending
SOURCE_ROUTE = 1 << 1


@register_command(XncpCommandId.SEND_UNICAST_REQ)
class SendUnicastReq(XncpCommandPayload):
flags: SendUnicastFlags
destination: t.NWK
aps_frame: EmberApsFrame
message_tag: t.uint8_t

ieee: t.EUI64 = t.StructField(
requires=lambda s: SendUnicastFlags.EXTENDED_TIMEOUT in s.flags
)
extended_timeout: t.Bool = t.StructField(
requires=lambda s: SendUnicastFlags.EXTENDED_TIMEOUT in s.flags
)

source_route: t.LVList[t.NWK, t.uint8_t] = t.StructField(
requires=lambda s: SendUnicastFlags.SOURCE_ROUTE in s.flags
)

data: Bytes


@register_command(XncpCommandId.SEND_UNICAST_RSP)
class SendUnicastRsp(XncpCommandPayload):
status: sl_Status
sequence: t.uint8_t


@register_command(XncpCommandId.UNKNOWN)
class Unknown(XncpCommandPayload):
pass
95 changes: 68 additions & 27 deletions bellows/zigbee/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
ControllerError,
EzspError,
InvalidCommandError,
PayloadTooLongError,
StackAlreadyRunning,
)
import bellows.ezsp
Expand Down Expand Up @@ -1028,45 +1029,85 @@ async def send_packet(self, packet: zigpy.types.ZigbeePacket) -> None:

try:
async with self._req_lock:
if packet.dst.addr_mode == zigpy.types.AddrMode.NWK:
if device is not None:
await self._ezsp.set_extended_timeout(
nwk=device.nwk,
ieee=device.ieee,
extended_timeout=extended_timeout,
)
data = packet.data.serialize()

if packet.source_route is not None:
if (
if packet.dst.addr_mode == zigpy.types.AddrMode.NWK:
# Manual source routes are installed through XNCP; native
# source routes cannot be folded into the combined command
use_manual_source_route = (
packet.source_route is not None
and (
FirmwareFeatures.MANUAL_SOURCE_ROUTE
in self._ezsp._xncp_features
and self.config[CONF_BELLOWS_CONFIG][
CONF_MANUAL_SOURCE_ROUTING
]
):
await self._ezsp.xncp_set_manual_source_route(
)
and self.config[CONF_BELLOWS_CONFIG][
CONF_MANUAL_SOURCE_ROUTING
]
)

# XNCP combined unicasts can fail in a few ways so it's simpler
# to prepare the request in advance
xncp_unicast = None

if (
FirmwareFeatures.COMBINED_SEND in self._ezsp._xncp_features
and (packet.source_route is None or use_manual_source_route)
):
try:
xncp_unicast = self._ezsp.xncp_prepare_unicast(
destination=packet.dst.address,
route=packet.source_route,
aps_frame=aps_frame,
message_tag=message_tag,
data=data,
source_route=(
packet.source_route
if use_manual_source_route
else None
),
extended_timeout=(
(device.ieee, extended_timeout)
if device is not None
else None
),
)
else:
await self._ezsp.set_source_route(
nwk=packet.dst.address,
relays=packet.source_route,
except PayloadTooLongError:
xncp_unicast = None

if xncp_unicast is not None:
status, _ = await self._ezsp.xncp_send_unicast(xncp_unicast)
else:
if device is not None:
await self._ezsp.set_extended_timeout(
nwk=device.nwk,
ieee=device.ieee,
extended_timeout=extended_timeout,
)

status, _ = await self._ezsp.send_unicast(
nwk=packet.dst.address,
aps_frame=aps_frame,
message_tag=message_tag,
data=packet.data.serialize(),
)
if packet.source_route is not None:
if use_manual_source_route:
await self._ezsp.xncp_set_manual_source_route(
destination=packet.dst.address,
route=packet.source_route,
)
else:
await self._ezsp.set_source_route(
nwk=packet.dst.address,
relays=packet.source_route,
)

status, _ = await self._ezsp.send_unicast(
nwk=packet.dst.address,
aps_frame=aps_frame,
message_tag=message_tag,
data=data,
)
elif packet.dst.addr_mode == zigpy.types.AddrMode.Group:
status, _ = await self._ezsp.send_multicast(
aps_frame=aps_frame,
radius=packet.radius,
non_member_radius=packet.non_member_radius,
message_tag=message_tag,
data=packet.data.serialize(),
data=data,
)
elif packet.dst.addr_mode == zigpy.types.AddrMode.Broadcast:
status, _ = await self._ezsp.send_broadcast(
Expand All @@ -1075,7 +1116,7 @@ async def send_packet(self, packet: zigpy.types.ZigbeePacket) -> None:
radius=packet.radius,
message_tag=message_tag,
aps_sequence=packet.tsn,
data=packet.data.serialize(),
data=data,
)

if status != t.sl_Status.OK:
Expand Down
Loading
Loading