Skip to content

APS ack: no retransmission, timer starts before enqueue, timed-out frames still sent #64

Open

Description

@zigpy-review-bot

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):

  1. No APS-level retransmission: there's a single wait (aps_ack_timeout 5 s, aps_ack_timeout_indirect 10 s, constants.rs#L352-L357), then ApsAckTimeout. Only NWK/MAC-level retries exist. The spec (2.2.8.4.4) calls for up to apscMaxFrameRetries (3) retransmissions, each waiting apscAckWaitDuration, 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.
  2. 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.
  3. Misleading status, and the frame still goes out: if the deadline passes while the frame is still waiting, the host gets APS_ACK_TIMEOUT instead 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, reports APS_ACK_TIMEOUT after 5 s instead of ROUTE_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.
  4. 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's Device.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 apscMaxFrameRetries times 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Fields

    Priority

    None yet

    Size

    None yet

    Area

    None yet

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions