OmniConnection.close: send ClientSessionTerminated to free panel slot
Found via live testing against the user's Omni Pro II (firmware 2.12)
on UDP. Without this, the panel holds the session slot (it's
single-client by design) and rejects new sessions from us with
ControllerCannotStartNewSession (packet type 0x07) until its idle
timeout fires (60s+ in our testing).
src/omni_pca/connection.py:
close() now sends Packet(type=ClientSessionTerminated) before
tearing down the socket, but only if we got past CONNECTING (no
point sending termination if we never had a session). For TCP we
drain the writer briefly so the byte hits the wire before FIN.
For UDP the sendto is fire-and-forget. Wrapped in try/except so
close() stays idempotent.
Live-validation findings (real Omni Pro II, firmware 2.12):
✓ UDP handshake works end-to-end. The four-packet exchange
(NewSession ack / SecureSession ack) round-trips cleanly,
confirming the two non-public protocol quirks (session key
XOR mix + per-block whitening) are correctly implemented.
✗ Post-handshake encrypted messages get no reply from this
firmware (tried v1 OmniLinkMessage and v2 OmniLink2Message;
both arrive at the panel — verified via tcpdump — and the
panel sends nothing back). Suspected: firmware 2.12 either
requires an explicit Login first or has a different opcode
set than PC Access 3.17 documents. Needs more RE work.
The handshake-quirks validation is the headline win — we've now
proven that part of the protocol against real hardware, which no
public Omni-Link client has done. Post-handshake message dispatch
is the next investigation.
357 tests still pass.
This commit is contained in:
parent
81725b4dbf
commit
d91561a6d2
@ -227,8 +227,36 @@ class OmniConnection:
|
|||||||
if self._closed:
|
if self._closed:
|
||||||
return
|
return
|
||||||
self._closed = True
|
self._closed = True
|
||||||
|
previous_state = self._state
|
||||||
self._state = ConnectionState.DISCONNECTED
|
self._state = ConnectionState.DISCONNECTED
|
||||||
|
|
||||||
|
# Politely tell the controller we're done — Omni is single-client,
|
||||||
|
# and on UDP it has no other way to know we've gone (TCP gets a
|
||||||
|
# FIN; UDP just sees datagrams stop). Without this, the panel
|
||||||
|
# holds the session slot until its idle timeout and rejects new
|
||||||
|
# connections from us with ControllerCannotStartNewSession.
|
||||||
|
if previous_state in (
|
||||||
|
ConnectionState.NEW_SESSION,
|
||||||
|
ConnectionState.SECURE,
|
||||||
|
ConnectionState.ONLINE,
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
term_seq = self._claim_seq()
|
||||||
|
term = Packet(
|
||||||
|
seq=term_seq,
|
||||||
|
type=PacketType.ClientSessionTerminated,
|
||||||
|
data=b"",
|
||||||
|
)
|
||||||
|
self._write_packet(term)
|
||||||
|
# Best-effort flush so the byte hits the wire before we
|
||||||
|
# tear down the socket. UDP is fire-and-forget; TCP needs
|
||||||
|
# a tick for the writer to drain.
|
||||||
|
if self._writer is not None:
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
await self._writer.drain()
|
||||||
|
except Exception as exc: # noqa: BLE001 - close() must be idempotent
|
||||||
|
_log.debug("close: failed to send ClientSessionTerminated: %s", exc)
|
||||||
|
|
||||||
# Cancel anyone still waiting for a reply.
|
# Cancel anyone still waiting for a reply.
|
||||||
for fut in self._pending.values():
|
for fut in self._pending.values():
|
||||||
if not fut.done():
|
if not fut.done():
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user