Track compressed LoRa OTA transfer progress

This commit is contained in:
mikecarper committed 2026-10-20 22:24:15 -07:00
1 parent 38f5d8a61a
commit c509b675c9
2 files changed
+104 -4

No files matched your search

+17 -4
View File
@@ -4099,9 +4099,12 @@ class SeederProcess:
``motatool serve -v`` logs each successful random-access read. A read
at a payload block boundary means the source has reached that block's
response job. This is a passive progress signal: unlike remote
``ota status``, reading it consumes no LoRa airtime and cannot collide
with the transfer it is observing.
response job. Compressed transport instead names the logical block;
its offsets are compressed-slice offsets, not package-file offsets.
This run serves one package at native index zero. Count each successful
compressed slice/retry as activity without counting its block twice.
These reads are passive progress, not proof of destination completion;
the monitor still requires the destination's exact ready-session reply.
"""
try:
detail = self._read_log()
@@ -4116,7 +4119,17 @@ class SeederProcess:
package.payload_offset + index * package.block_size
for index in range(total)
}
payload_reads = [offset for offset in offsets if offset in boundaries]
payload_reads = [
(offset - package.payload_offset) // package.block_size
for offset in offsets if offset in boundaries
]
for block, offset in re.findall(
r"\[dev\][ \t]+DEFLATE[ \t]+0[ \t]+block=([0-9]+)"
r"[ \t]+@([0-9]+)[ \t]+OK(?=[ \t]*(?:\r?\n|$))", detail
):
block_index, compressed_offset = int(block), int(offset)
if block_index < total and compressed_offset <= 0xFFFF:
payload_reads.append(block_index)
return len(set(payload_reads)), total, len(payload_reads)
def stop(self) -> None:
+87
View File
@@ -2637,6 +2637,42 @@ class DownloadSessionTests(unittest.TestCase):
self.assertEqual(controller.commands, ["ota status"])
self.assertEqual(seeder.calls, total)
def test_complete_compressed_source_progress_still_requires_ready_destination(self) -> None:
controller = self.Controller([
f"OTA | download: downloading 6/9 id={self.package.manifest_id} 2s",
f"OTA | download: ready to install 9/9 id={self.package.manifest_id} 3s",
])
args = argparse.Namespace(
target="remote", transfer_timeout_minutes=1, poll_seconds=1,
)
total = (
self.package.payload_size + self.package.block_size - 1
) // self.package.block_size
seeder = object.__new__(ota.SeederProcess)
seeder.log_path = Path("serve.log")
seeder.log_file = None
seeder.process = mock.Mock()
seeder.process.poll.return_value = None
detail = "".join(
f"[dev] DEFLATE 0 block={index} @0 OK\n" for index in range(total)
)
elapsed = [0.0]
def advance(seconds: float) -> None:
elapsed[0] += seconds
with (
mock.patch.object(Path, "read_text", return_value=detail),
mock.patch.object(ota.time, "monotonic", side_effect=lambda: elapsed[0]),
mock.patch.object(ota.time, "sleep", side_effect=advance),
mock.patch.object(ota, "transfer_tail_guard_seconds", return_value=0),
contextlib.redirect_stdout(io.StringIO()),
):
status = ota.monitor_download(controller, args, self.package, seeder)
self.assertIn("ready to install", status)
self.assertEqual(controller.commands, ["ota status", "ota status"])
self.assertGreaterEqual(elapsed[0], ota.passive_progress_stall_seconds(args, self.package))
def test_monitor_uses_quiet_window_when_source_log_is_unavailable(self) -> None:
args = argparse.Namespace(
target="remote", transfer_timeout_minutes=1, poll_seconds=1,
@@ -5448,6 +5484,57 @@ class ReliabilityTests(unittest.TestCase):
seeder.log_file = None
self.assertEqual(seeder.payload_read_progress(package), (2, 3, 2))
def test_seeder_progress_tracks_compressed_slices_and_retries(self) -> None:
package = ota.parse_mota(mota_blob(firmware(b"B" * 2500, VERSION_NEW)))
seeder = object.__new__(ota.SeederProcess)
seeder.log_path = Path("serve.log")
seeder.log_file = None
detail = (
"[dev] DEFLATE 0 block=0 @0 OK\n" # Length query or first slice.
"[dev] DEFLATE 0 block=0 @190 OK\n"
"[dev] DEFLATE 0 block=2 @0 OK\n"
"[dev] DEFLATE 0 block=2 @0 OK\n" # Retry remains activity.
)
with mock.patch.object(Path, "read_text", return_value=detail):
self.assertEqual(seeder.payload_read_progress(package), (2, 3, 4))
def test_seeder_progress_merges_raw_and_compressed_logical_blocks(self) -> None:
package = ota.parse_mota(mota_blob(firmware(b"B" * 2500, VERSION_NEW)))
seeder = object.__new__(ota.SeederProcess)
seeder.log_path = Path("serve.log")
seeder.log_file = None
detail = (
f"[dev] READ 0 @{package.payload_offset} OK\n"
"[dev] DEFLATE 0 block=0 @0 OK\n"
# This compressed offset happens to equal raw block zero's file
# offset. The logical block field, not that offset, identifies it.
f"[dev] DEFLATE 0 block=2 @{package.payload_offset} OK\n"
)
with mock.patch.object(Path, "read_text", return_value=detail):
self.assertEqual(seeder.payload_read_progress(package), (2, 3, 3))
def test_seeder_progress_ignores_invalid_compressed_requests(self) -> None:
package = ota.parse_mota(mota_blob(firmware(b"B" * 2500, VERSION_NEW)))
seeder = object.__new__(ota.SeederProcess)
seeder.log_path = Path("serve.log")
seeder.log_file = None
detail = (
"[dev] DEFLATE 0 block=0 @0 ERR\n"
"[dev] DEFLATE 1 block=0 @0 OK\n"
"[dev] DEFLATE 00 block=0 @0 OK\n"
"[dev] DEFLATE 0 block=3 @0 OK\n"
"[dev] DEFLATE 0 block=-1 @0 OK\n"
"[dev] DEFLATE 0 block=x @0 OK\n"
"[dev] DEFLATE 0 block=0 @-1 OK\n"
"[dev] DEFLATE 0 block=0 @65536 OK\n"
"[dev] DEFLATE 0 block=0 @0\n"
"[dev] DEFLATE 0 block=0 @0 OK trailing\n"
"[dev] DEFLATE 0 block=0\n@0 OK\n"
"DEFLATE 0 block=0 @0 OK\n"
)
with mock.patch.object(Path, "read_text", return_value=detail):
self.assertEqual(seeder.payload_read_progress(package), (0, 3, 0))
def test_seeder_progress_missing_log_keeps_all_three_counters(self) -> None:
package = ota.parse_mota(mota_blob(firmware(b"B" * 2500, VERSION_NEW)))
with tempfile.TemporaryDirectory() as directory: