fix(protocol): reassemble blockwise OBSERVE notifications
A notification carries only the first block of a large representation (RFC 7959 §2.6). _dispatch_coap handed that block straight to on_notification, so consumers decoded a truncated CBOR buffer. Reported twice on /mode/vs/0: #37 and mbillow/localthings#361. The recovery is a re-read from block 0 on a fresh 4-byte one-shot token, not a §2.6 continuation. §3.4 rules out reusing the observation's token, and this server drops a transfer that opens at NUM>0 under a token it has not seen, so a continuation is the one shape that cannot work here. A truncated notification is now withheld and queued to a worker thread that re-reads the resource and delivers the reassembled representation. When the re-read fails the notification is dropped at debug level and the poll tiers carry freshness, which is what they already did. The re-read has to run off the reader thread: _dispatch_coap runs there and the transfer waits on an event only that same thread can set. The worker is serialized and paces between transfers, so a notification storm stays under the firmware request ceiling. Also in the Block2 loop, now extracted and shared by both paths: - compare the response's Block2 NUM against the one requested, so a retransmitted block is no longer concatenated as if it were the next - compare ETags across blocks (§2.4) and restart once when the representation changes mid-transfer - recompute the next block number from the accumulated byte offset when the server negotiates the block size down - re-check reader liveness while waiting on a block, so a mid-transfer reader death fails fast instead of burning the whole timeout - guard the token counters and _pending with a lock, now that the session issues concurrent reads of its own Closes #39
This commit is contained in:
1 parent
dec84c9ad8
commit
231e88a8c6
4 files changed
+829
-71
No files matched your search
@@ -13,6 +13,7 @@ from ..errors import MalformedMessageError
|
||||
URI_PATH = 11
|
||||
URI_QUERY = 15
|
||||
OBSERVE = 6
|
||||
ETAG = 4
|
||||
CONTENT_FORMAT = 12
|
||||
ACCEPT = 17
|
||||
BLOCK2 = 23
|
||||
@@ -121,6 +122,14 @@ def block_value(num, more, szx):
|
||||
return struct.pack('>I', v)[1:]
|
||||
|
||||
|
||||
def block_fields(value):
|
||||
"""Decode a CoAP Block-N option value. Inverse of block_value().
|
||||
Returns (num, more, szx). An empty value means block 0, no more,
|
||||
SZX=0 — RFC 7959 §2.2 allows a zero-length option to elide it."""
|
||||
v = int.from_bytes(value, 'big')
|
||||
return v >> 4, (v >> 3) & 1, v & 0x07
|
||||
|
||||
|
||||
def fmt_code(c):
|
||||
"""0x45 → '2.05', 0x84 → '4.04'. Used in log lines."""
|
||||
return f"{c >> 5}.{c & 0x1F:02d}"
|
||||
|
||||
Reference in new issue
Block a user