Repository navigation
APS ack: no retransmission, timer starts before enqueue, timed-out frames still sent #64
Copy link
Copy link
Open
Description
Found while comparing radio stacks for zigpy/zigpy#1891. The full analysis is in zigpy/zigpy#1891 (comment). Related: #56 (same symptom on sleepy end devices behind routers; that one was APS counter reuse, fixed by #57).
In zigpy/zigpy#1891, the coordinator most likely gives up on the APS ack while the sleepy device's parent still holds the frame for it. ziggurat largely avoids that for its own sleepy children and for destinations zigpy flags with extended_timeout: it waits 10 s for the APS ack, longer than the 7.68 s indirect hold, and reports INDIRECT_EXPIRED when an own child never polls. A few related gaps remain in the APS ack path (checked at 392d4c7):
- No APS-level retransmission: there's a single wait (
aps_ack_timeout5 s,aps_ack_timeout_indirect10 s, constants.rs#L352-L357), thenApsAckTimeout. Only NWK/MAC-level retries exist. The spec (2.2.8.4.4) calls for up toapscMaxFrameRetries(3) retransmissions, each waitingapscAckWaitDuration, and APS duplicate rejection relies on them keeping the APS counter. Recovery is left to zigpy's retries instead, which get new APS counters, so the receiver can't drop duplicates. - The ack deadline is set before the frame is queued (aps.rs#L294-L304). Route discovery (up to
route_discovery_time, 10 s, constants.rs#L318), queueing, NWK retries and a router's typical 7.68 s hold all come out of the same 5 s or 10 s window. So for a flagged sleepy device behind a router, a slow route discovery can still use up the window before the frame reaches the router. - Misleading status, and the frame still goes out: if the deadline passes while the frame is still waiting, the host gets
APS_ACK_TIMEOUTinstead of the real cause. With one discovery attempt (the default), an acked send that needs a new route discovery to an unreachable destination, and isn't flagged sleepy, reportsAPS_ACK_TIMEOUTafter 5 s instead ofROUTE_DISCOVERY_TIMEOUT. The timeout also doesn't remove the frame from the queue, and the host can't cancel it either, because the send tracker forgets the request once it has emitted the confirm. So the frame can still go out after zigpy has already retried. - Devices behind a router rely only on the host flag (
sleepy_destination). The stack only detects its own sleepy children (aps.rs#L194-L200). Without the flag, the wait is 5 s, shorter than a router's hold. Through zigpy'sDevice.request, this only happens when the node descriptor doesn't say end device.
Possible direction (combining what Z-Stack and EmberZNet do)
- Keep registering the pending ack before enqueue, so fast replies are caught, but arm its deadline only when the frame is handed off to the MAC (which for an own sleepy child already happens at the poll).
- For sleepy destinations behind a router, add the indirect hold time on top of the base wait.
- Retransmit up to
apscMaxFrameRetriestimes with the same APS counter. APS-secured frames need re-encrypting with a new frame counter (spec 2.2.8.4.4), and for an own sleepy child, don't queue another copy while one is still waiting for a poll. - Drop queued frames whose ack deadline has already expired.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels
Type
Fields
Priority
None yet
Size
None yet
Area
None yet
Activity