From eb07f0f0de7924ba23879ef02b15b9ec5a5be3e7 Mon Sep 17 00:00:00 2001 From: Jordan Yates Date: Thu, 23 Jul 2026 13:58:00 +1000 Subject: [PATCH 1/2] generated: regenerate definitions Regenerate definitions from upstream. Signed-off-by: Jordan Yates --- src/infuse_iot/generated/kv_definitions.py | 42 +++++ src/infuse_iot/generated/rpc_definitions.py | 169 ++++++++++++++++++++ src/infuse_iot/generated/tdf_definitions.py | 21 +++ 3 files changed, 232 insertions(+) diff --git a/src/infuse_iot/generated/kv_definitions.py b/src/infuse_iot/generated/kv_definitions.py index 5b2f0d9..dbe9402 100644 --- a/src/infuse_iot/generated/kv_definitions.py +++ b/src/infuse_iot/generated/kv_definitions.py @@ -239,6 +239,32 @@ class broadcast_fixed_indoors(VLACompatLittleEndianStruct): ] _pack_ = 1 + class littlefs_fs_state(VLACompatLittleEndianStruct): + """State of LittleFS filesystem""" + + NAME = "LITTLEFS_FS_STATE" + BASE_ID = 12 + RANGE = 1 + _fields_ = [ + ("disk_version", ctypes.c_uint32), + ("block_size", ctypes.c_uint32), + ("block_count", ctypes.c_uint32), + ("blocks_used", ctypes.c_uint32), + ] + _pack_ = 1 + + class littlefs_algorithms_state(VLACompatLittleEndianStruct): + """State of algorithms on the LittleFS filesystem""" + + NAME = "LITTLEFS_ALGORITHMS_STATE" + BASE_ID = 13 + RANGE = 1 + _fields_ = [ + ("count", ctypes.c_uint8), + ("crc_xor", ctypes.c_uint32), + ] + _pack_ = 1 + class wifi_ssid(VLACompatLittleEndianStruct): """WiFi network name""" @@ -462,6 +488,19 @@ class gateway_bluetooth_forward_options(VLACompatLittleEndianStruct): ] _pack_ = 1 + class nrf_edge_ai_runtime_version(VLACompatLittleEndianStruct): + """Version of the nRF Edge AI runtime""" + + NAME = "NRF_EDGE_AI_RUNTIME_VERSION" + BASE_ID = 56 + RANGE = 1 + _fields_ = [ + ("major", ctypes.c_uint8), + ("minor", ctypes.c_uint8), + ("patch", ctypes.c_uint16), + ] + _pack_ = 1 + class gravity_reference(VLACompatLittleEndianStruct): """Reference gravity vector for tilt calculations""" @@ -588,6 +627,8 @@ class secure_storage_reserved(VLACompatLittleEndianStruct): 8: secondary_remote_public_key, 10: fixed_location, 11: broadcast_fixed_indoors, + 12: littlefs_fs_state, + 13: littlefs_algorithms_state, 20: wifi_ssid, 21: wifi_psk, 22: wifi_channels, @@ -608,6 +649,7 @@ class secure_storage_reserved(VLACompatLittleEndianStruct): 53: led_disable_daily_time_range, 54: memfault_disable, 55: gateway_bluetooth_forward_options, + 56: nrf_edge_ai_runtime_version, 60: gravity_reference, 100: geofence, 101: geofence, diff --git a/src/infuse_iot/generated/rpc_definitions.py b/src/infuse_iot/generated/rpc_definitions.py index fcca833..c3171f8 100644 --- a/src/infuse_iot/generated/rpc_definitions.py +++ b/src/infuse_iot/generated/rpc_definitions.py @@ -254,6 +254,28 @@ class rpc_struct_data_logger_flushed(VLACompatLittleEndianStruct): _pack_ = 1 +class rpc_struct_filesystem_file_metadata(VLACompatLittleEndianStruct): + """Metadata associated with file on the filesystem""" + + _fields_ = [ + ("timestamp", ctypes.c_uint32), + ("identifier", ctypes.c_uint32), + ("crc", ctypes.c_uint32), + ] + _pack_ = 1 + + +class rpc_struct_filesystem_file_info(VLACompatLittleEndianStruct): + """Information about file on the filesystem""" + + _fields_ = [ + ("metadata", rpc_struct_filesystem_file_metadata), + ("name", ctypes.c_uint32), + ("size", ctypes.c_uint32), + ] + _pack_ = 1 + + class rpc_enum_bt_le_addr_type(enum.IntEnum): """Bluetooth LE address type""" @@ -271,6 +293,7 @@ class rpc_enum_file_action(enum.IntEnum): BT_CTLR_CPATCH = 12 NRF91_MODEM_DIFF = 20 FILE_FOR_COPY = 30 + WRITE_LITTLEFS = 31 class rpc_enum_infuse_bt_characteristic(enum.IntEnum): @@ -335,6 +358,15 @@ class rpc_enum_support_status(enum.IntEnum): UNKNOWN = 255 +class rpc_enum_filesystem_folder(enum.IntEnum): + """Folder identifier""" + + GENERAL = 0 + ALGORITHMS = 1 + A_GNSS = 2 + COPY = 3 + + class RPCDefinitionBase: NAME: str HELP: str @@ -1016,6 +1048,38 @@ class response(VLACompatLittleEndianStruct): _pack_ = 1 +class coap_download_v3(RPCDefinitionBase): + """Download a file from a COAP server (Infuse-IoT DTLS protected)""" + + NAME = "coap_download_v3" + HELP = "Download a file from a COAP server (Infuse-IoT DTLS protected)" + DESCRIPTION = "Download a file from a COAP server (Infuse-IoT DTLS protected)" + COMMAND_ID = 33 + + class request(VLACompatLittleEndianStruct): + _fields_ = [ + ("server_address", 48 * ctypes.c_char), + ("server_port", ctypes.c_uint16), + ("block_timeout_ms", ctypes.c_uint16), + ("block_size", ctypes.c_uint16), + ("action", ctypes.c_uint8), + ("folder", ctypes.c_uint8), + ("filename", ctypes.c_uint32), + ("identifier", ctypes.c_uint32), + ("resource_len", ctypes.c_uint32), + ("resource_crc", ctypes.c_uint32), + ] + vla_field = ("resource", 0 * ctypes.c_char) + _pack_ = 1 + + class response(VLACompatLittleEndianStruct): + _fields_ = [ + ("resource_len", ctypes.c_uint32), + ("resource_crc", ctypes.c_uint32), + ] + _pack_ = 1 + + class file_write_basic(RPCDefinitionBase): """Write a file to the device""" @@ -1083,6 +1147,53 @@ class response(VLACompatLittleEndianStruct): _pack_ = 1 +class shipping_mode(RPCDefinitionBase): + """Enter/Exit shipping mode on a device""" + + NAME = "shipping_mode" + HELP = "Enter/Exit shipping mode on a device" + DESCRIPTION = "Enter/Exit shipping mode on a device" + COMMAND_ID = 43 + + class request(VLACompatLittleEndianStruct): + _fields_ = [ + ("enter", ctypes.c_uint8), + ] + _pack_ = 1 + + class response(VLACompatLittleEndianStruct): + _fields_ = [ + ("expected_delay_ms", ctypes.c_uint32), + ] + _pack_ = 1 + + +class file_write(RPCDefinitionBase): + """Write a file to the device""" + + NAME = "file_write" + HELP = "Write a file to the device" + DESCRIPTION = "Write a file to the device" + COMMAND_ID = 44 + + class request(VLACompatLittleEndianStruct): + _fields_ = [ + ("action", ctypes.c_uint8), + ("folder", ctypes.c_uint8), + ("filename", ctypes.c_uint32), + ("identifier", ctypes.c_uint32), + ("file_crc", ctypes.c_uint32), + ] + _pack_ = 1 + + class response(VLACompatLittleEndianStruct): + _fields_ = [ + ("recv_len", ctypes.c_uint32), + ("recv_crc", ctypes.c_uint32), + ] + _pack_ = 1 + + class bt_connect_infuse(RPCDefinitionBase): """Connect to an Infuse-IoT Bluetooth device""" @@ -1231,6 +1342,51 @@ class response(VLACompatLittleEndianStruct): _pack_ = 1 +class filesystem_ls(RPCDefinitionBase): + """Query files currently on the filesystem""" + + NAME = "filesystem_ls" + HELP = "Query files currently on the filesystem" + DESCRIPTION = "Query files currently on the filesystem" + COMMAND_ID = 61 + + class request(VLACompatLittleEndianStruct): + _fields_ = [ + ("folder", ctypes.c_uint8), + ("skip", ctypes.c_uint8), + ] + _pack_ = 1 + + class response(VLACompatLittleEndianStruct): + _fields_ = [ + ("total_files", ctypes.c_uint8), + ("contained_files", ctypes.c_uint8), + ] + vla_field = ("files", 0 * rpc_struct_filesystem_file_info) + vla_counted_by = "contained_files" + _pack_ = 1 + + +class filesystem_rm(RPCDefinitionBase): + """Delete file currently on the filesystem""" + + NAME = "filesystem_rm" + HELP = "Delete file currently on the filesystem" + DESCRIPTION = "Delete file currently on the filesystem" + COMMAND_ID = 62 + + class request(VLACompatLittleEndianStruct): + _fields_ = [ + ("folder", ctypes.c_uint8), + ("file", ctypes.c_uint32), + ] + _pack_ = 1 + + class response(VLACompatLittleEndianStruct): + _fields_ = [] + _pack_ = 1 + + class ubx_assist_now_ztp_creds(RPCDefinitionBase): """Retrieve U-blox AssistNow Zero Touch Provisioning credentials""" @@ -1413,15 +1569,20 @@ class response(VLACompatLittleEndianStruct): coap_download.COMMAND_ID: coap_download, zperf_upload.COMMAND_ID: zperf_upload, coap_download_v2.COMMAND_ID: coap_download_v2, + coap_download_v3.COMMAND_ID: coap_download_v3, file_write_basic.COMMAND_ID: file_write_basic, annotate.COMMAND_ID: annotate, tdf_data_logger_flush.COMMAND_ID: tdf_data_logger_flush, + shipping_mode.COMMAND_ID: shipping_mode, + file_write.COMMAND_ID: file_write, bt_connect_infuse.COMMAND_ID: bt_connect_infuse, bt_disconnect.COMMAND_ID: bt_disconnect, bt_file_copy_basic.COMMAND_ID: bt_file_copy_basic, bt_file_copy_coap.COMMAND_ID: bt_file_copy_coap, bt_mcumgr_reboot.COMMAND_ID: bt_mcumgr_reboot, gravity_reference_update.COMMAND_ID: gravity_reference_update, + filesystem_ls.COMMAND_ID: filesystem_ls, + filesystem_rm.COMMAND_ID: filesystem_rm, ubx_assist_now_ztp_creds.COMMAND_ID: ubx_assist_now_ztp_creds, security_state.COMMAND_ID: security_state, security_key_update.COMMAND_ID: security_key_update, @@ -1452,6 +1613,8 @@ class response(VLACompatLittleEndianStruct): "rpc_struct_public_key_info_256bit", "rpc_struct_thread_stats", "rpc_struct_data_logger_flushed", + "rpc_struct_filesystem_file_metadata", + "rpc_struct_filesystem_file_info", "rpc_enum_bt_le_addr_type", "rpc_enum_file_action", "rpc_enum_infuse_bt_characteristic", @@ -1461,6 +1624,7 @@ class response(VLACompatLittleEndianStruct): "rpc_enum_key_id", "rpc_enum_key_action", "rpc_enum_support_status", + "rpc_enum_filesystem_folder", "reboot", "fault", "time_get", @@ -1490,15 +1654,20 @@ class response(VLACompatLittleEndianStruct): "coap_download", "zperf_upload", "coap_download_v2", + "coap_download_v3", "file_write_basic", "annotate", "tdf_data_logger_flush", + "shipping_mode", + "file_write", "bt_connect_infuse", "bt_disconnect", "bt_file_copy_basic", "bt_file_copy_coap", "bt_mcumgr_reboot", "gravity_reference_update", + "filesystem_ls", + "filesystem_rm", "ubx_assist_now_ztp_creds", "security_state", "security_key_update", diff --git a/src/infuse_iot/generated/tdf_definitions.py b/src/infuse_iot/generated/tdf_definitions.py index 33cebbb..b36764f 100644 --- a/src/infuse_iot/generated/tdf_definitions.py +++ b/src/infuse_iot/generated/tdf_definitions.py @@ -1713,6 +1713,26 @@ class kvs_value_changed(TdfReadingBase): "value": "{}", } + class ambient_pressure(TdfReadingBase): + """Ambient pressure""" + + ID = 62 + NAME = "AMBIENT_PRESSURE" + _fields_ = [ + ("_pressure", ctypes.c_uint32), + ] + _pack_ = 1 + _postfix_ = { + "pressure": "kPA", + } + _display_fmt_ = { + "pressure": "{:.3f}", + } + + @property + def pressure(self): + return self._pressure * 0.001 + id_type_mapping: dict[int, type[TdfReadingBase]] = { readings.announce.ID: readings.announce, @@ -1775,6 +1795,7 @@ class kvs_value_changed(TdfReadingBase): readings.pcm_16bit_chan_right.ID: readings.pcm_16bit_chan_right, readings.pcm_16bit_chan_dual.ID: readings.pcm_16bit_chan_dual, readings.kvs_value_changed.ID: readings.kvs_value_changed, + readings.ambient_pressure.ID: readings.ambient_pressure, } __all__ = [ From ed6a036162feec2bb6f4e5b1107e097e550db48e Mon Sep 17 00:00:00 2001 From: Jordan Yates Date: Thu, 23 Jul 2026 13:48:25 +1000 Subject: [PATCH 2/2] tests: cpatch: added Add basic patching tests for the `cpatch` module. Signed-off-by: Jordan Yates --- tests/test_cpatch.py | 79 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 tests/test_cpatch.py diff --git a/tests/test_cpatch.py b/tests/test_cpatch.py new file mode 100644 index 0000000..faef563 --- /dev/null +++ b/tests/test_cpatch.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 + +import os +import subprocess +import sys + +import pytest + +from infuse_iot.cpatch import ValidationError, cpatch + +assert "TOXTEMPDIR" in os.environ, "you must run these tests using tox" + + +def apply_generated_patch(tmp_path, original: bytes, expected: bytes) -> bytes: + original_file = tmp_path / "original.bin" + new_file = tmp_path / "new.bin" + patch_file = tmp_path / "update.cpatch" + output_file = tmp_path / "output.bin" + + original_file.write_bytes(original) + new_file.write_bytes(expected) + + subprocess.run( + [sys.executable, "-m", "infuse_iot.cpatch", "generate", original_file, new_file, patch_file], + check=True, + stdout=subprocess.DEVNULL, + ) + subprocess.run( + [sys.executable, "-m", "infuse_iot.cpatch", "patch", original_file, patch_file, output_file], + check=True, + stdout=subprocess.DEVNULL, + ) + + return output_file.read_bytes() + + +@pytest.mark.parametrize( + ("original", "expected"), + [ + pytest.param(bytes(range(64)), bytes(range(64)), id="unchanged"), + pytest.param(bytes(range(64)), bytes(range(16)) + b"HELLO" + bytes(range(21, 64)), id="rewrite"), + pytest.param(bytes(range(64)), bytes(range(64)) + b"TAIL", id="append"), + pytest.param(bytes(range(64)), bytes(range(16)) + bytes(range(24, 64)), id="delete"), + pytest.param(b"abcdefgh" * 8 + b"ijklmnop" * 8, b"ijklmnop" * 8 + b"abcdefgh" * 8, id="reuse"), + ], +) +def test_cli_generates_patch_that_reconstructs_expected_output(tmp_path, original, expected): + assert apply_generated_patch(tmp_path, original, expected) == expected + + +def test_patch_rejects_wrong_original_contents(): + original = bytes(range(64)) + expected = bytes(range(16)) + b"HELLO" + bytes(range(21, 64)) + patch = cpatch.generate(original, expected, verbose=False) + + with pytest.raises(ValidationError, match="Original file CRC"): + cpatch.patch(bytes(reversed(original)), patch) + + +def test_validation_patch_reconstructs_original(): + original = bytes(range(256)) * 5 + patch = cpatch.validation(original, invalid_length=False, invalid_crc=False) + + assert cpatch.patch(original, patch) == original + + +@pytest.mark.parametrize( + ("invalid_length", "invalid_crc", "message"), + [ + pytest.param(True, False, "length does not match", id="invalid-length"), + pytest.param(False, True, "CRC does not match", id="invalid-crc"), + ], +) +def test_validation_patch_can_generate_invalid_output_metadata(invalid_length, invalid_crc, message): + original = bytes(range(256)) * 5 + patch = cpatch.validation(original, invalid_length=invalid_length, invalid_crc=invalid_crc) + + with pytest.raises(ValidationError, match=message): + cpatch.patch(original, patch)