Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/infuse_iot/generated/kv_definitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,17 @@ class lora_config(VLACompatLittleEndianStruct):
]
_pack_ = 1

class bluetooth_throughput_limit(VLACompatLittleEndianStruct):
"""Request connected Bluetooth peers to limit throughtput"""

NAME = "BLUETOOTH_THROUGHPUT_LIMIT"
BASE_ID = 52
RANGE = 1
_fields_ = [
("limit_kbps", ctypes.c_uint16),
]
_pack_ = 1

class gravity_reference(VLACompatLittleEndianStruct):
"""Reference gravity vector for tilt calculations"""

Expand Down Expand Up @@ -395,6 +406,7 @@ class secure_storage_reserved(VLACompatLittleEndianStruct):
46: lte_networking_modes,
50: bluetooth_peer,
51: lora_config,
52: bluetooth_throughput_limit,
60: gravity_reference,
100: geofence,
101: geofence,
Expand Down
51 changes: 51 additions & 0 deletions src/infuse_iot/generated/rpc_definitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ class rpc_enum_file_action(enum.IntEnum):
APP_CPATCH = 11
BT_CTLR_CPATCH = 12
NRF91_MODEM_DIFF = 20
FILE_FOR_COPY = 30


class rpc_enum_infuse_bt_characteristic(enum.IntEnum):
Expand Down Expand Up @@ -665,6 +666,32 @@ class response(VLACompatLittleEndianStruct):
_pack_ = 1


class data_logger_read_available:
"""Read data from data logger, with auto-updating start_block"""

HELP = "Read data from data logger, with auto-updating start_block"
DESCRIPTION = "Read data from data logger, with auto-updating start_block"
COMMAND_ID = 22

class request(VLACompatLittleEndianStruct):
_fields_ = [
("logger", ctypes.c_uint8),
("start_block", ctypes.c_uint32),
("num_blocks", ctypes.c_uint32),
]
_pack_ = 1

class response(VLACompatLittleEndianStruct):
_fields_ = [
("sent_len", ctypes.c_uint32),
("sent_crc", ctypes.c_uint32),
("current_block", ctypes.c_uint32),
("start_block_actual", ctypes.c_uint32),
("block_size", ctypes.c_uint16),
]
_pack_ = 1


class coap_download:
"""Download a file from a COAP server (Infuse-IoT DTLS protected)"""

Expand Down Expand Up @@ -812,6 +839,30 @@ class response(VLACompatLittleEndianStruct):
_pack_ = 1


class bt_file_copy_basic:
"""Copy a local file to a remote device over Bluetooth"""

HELP = "Copy a local file to a remote device over Bluetooth"
DESCRIPTION = "Copy a local file to a remote device over Bluetooth"
COMMAND_ID = 52

class request(VLACompatLittleEndianStruct):
_fields_ = [
("peer", rpc_struct_bt_addr_le),
("action", ctypes.c_uint8),
("file_idx", ctypes.c_uint8),
("file_len", ctypes.c_uint32),
("file_crc", ctypes.c_uint32),
("ack_period", ctypes.c_uint8),
("pipelining", ctypes.c_uint8),
]
_pack_ = 1

class response(VLACompatLittleEndianStruct):
_fields_ = []
_pack_ = 1


class gravity_reference_update:
"""Store the current accelerometer vector as the gravity reference"""

Expand Down
10 changes: 9 additions & 1 deletion src/infuse_iot/rpc_wrappers/data_logger_read.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env python3

import binascii
import time

import infuse_iot.generated.rpc_definitions as defs
from infuse_iot.commands import InfuseRpcCommand
Expand Down Expand Up @@ -31,6 +32,7 @@ def __init__(self, args):
raise NotImplementedError
self.expected_offset = 0
self.output = b""
self.start_time = time.time()

def request_struct(self):
return self.request(self.logger, self.start, self.last)
Expand All @@ -39,6 +41,8 @@ def request_json(self):
return {"logger": self.logger.name, "start_block": self.start, "last_block": self.last}

def data_recv_cb(self, offset: int, data: bytes) -> None:
if self.expected_offset == 0:
self.start_time = time.time()
if offset != self.expected_offset:
missing = offset - self.expected_offset
print(f"Missed {missing:d} bytes from offset 0x{self.expected_offset:08x}")
Expand All @@ -49,6 +53,7 @@ def data_recv_cb(self, offset: int, data: bytes) -> None:
self.expected_offset = offset + len(data)

def handle_response(self, return_code, response):
end_time = time.time()
if return_code != 0:
print(f"Failed to read data logger ({errno.strerror(-return_code)})")
return
Expand All @@ -59,8 +64,11 @@ def handle_response(self, return_code, response):
if response.sent_crc != binascii.crc32(self.output):
print(f"Unexpected received CRC ({response.sent_crc:08x} != {binascii.crc32(self.output):08x})")

duration = end_time - self.start_time
bitrate = (len(self.output) * 8) / duration / 1024

file_prefix = f"{self.infuse_id:016x}" if self.infuse_id else "gateway"
output_file = f"{file_prefix}_{self.logger.name}.bin"
with open(output_file, "wb") as f:
f.write(self.output)
print(f"Wrote {response.sent_len:d} bytes to {output_file}")
print(f"Wrote {response.sent_len:d} bytes to {output_file} in {duration:.2f} sec ({bitrate:.3f} kbps)")
7 changes: 7 additions & 0 deletions src/infuse_iot/rpc_wrappers/file_write_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,13 @@ def add_parser(cls, parser):
const=rpc_enum_file_action.NRF91_MODEM_DIFF,
help="nRF91 LTE modem diff upgrade",
)
group.add_argument(
"--for-copy",
dest="action",
action="store_const",
const=rpc_enum_file_action.FILE_FOR_COPY,
help="File to copy to other device",
)

def __init__(self, args):
self.file = args.file
Expand Down
124 changes: 100 additions & 24 deletions src/infuse_iot/tools/ota_upgrade.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
__author__ = "Jordan Yates"
__copyright__ = "Copyright 2024, Embeint Inc"

import argparse
import binascii
import sys
import time

from rich.live import Live
Expand All @@ -18,15 +20,17 @@
from rich.table import Table

from infuse_iot.commands import InfuseCommand
from infuse_iot.epacket.packet import Auth
from infuse_iot.generated.rpc_definitions import file_write_basic, rpc_enum_file_action
from infuse_iot.common import InfuseID
from infuse_iot.epacket.packet import Auth, HopReceived
from infuse_iot.generated.rpc_definitions import bt_file_copy_basic, file_write_basic, rpc_enum_file_action
from infuse_iot.rpc_client import RpcClient
from infuse_iot.socket_comms import (
GatewayRequestConnectionRequest,
LocalClient,
default_multicast_address,
)
from infuse_iot.util.argparse import ValidFile, ValidRelease
from infuse_iot.zephyr.errno import errno


class SubCommand(InfuseCommand):
Expand All @@ -39,7 +43,19 @@ def __init__(self, args):
self._conn_timeout = args.conn_timeout
self._min_rssi: int | None = args.rssi
self._explicit_ids: list[int] = []
self._release: ValidRelease = args.release
if args.release:
self._release: ValidRelease = args.release
self._single_diff = None
elif args.single:
# Find the associated release
diff_folder = args.single.parent
release_folder = diff_folder.parent
if diff_folder.name != "diffs":
raise argparse.ArgumentTypeError(f"{args.single} is not in a diff folder")
self._release = ValidRelease(str(release_folder))
self._single_diff = args.single
else:
raise NotImplementedError("Unknow upgrade type")
self._app_name = self._release.metadata["application"]["primary"]
self._app_id = self._release.metadata["application"]["id"]
self._new_ver = self._release.metadata["application"]["version"]
Expand Down Expand Up @@ -72,9 +88,9 @@ def __init__(self, args):

@classmethod
def add_parser(cls, parser):
parser.add_argument(
"--release", "-r", type=ValidRelease, required=True, help="Application release to upgrade to"
)
upgrade_type = parser.add_mutually_exclusive_group(required=True)
upgrade_type.add_argument("--release", "-r", type=ValidRelease, help="Application release to upgrade to")
upgrade_type.add_argument("--single", type=ValidFile, help="Single diff")
parser.add_argument("--rssi", type=int, help="Minimum RSSI to attempt upgrade process")
parser.add_argument("--log", type=str, help="File to write upgrade results to")
parser.add_argument(
Expand Down Expand Up @@ -116,7 +132,73 @@ def data_progress_cb(self, offset):
self.task = self.progress.add_task("", total=len(self.patch_file))
self.progress.update(self.task, completed=offset)

def gateway_diff_load(self):
assert self._single_diff is not None
with self._single_diff.open("rb") as f:
patch_file = f.read()

with self._client.connection(InfuseID.GATEWAY, GatewayRequestConnectionRequest.DataType.COMMAND, 10) as _mtu:
rpc_client = RpcClient(self._client, _mtu, InfuseID.GATEWAY)
params = file_write_basic.request(rpc_enum_file_action.FILE_FOR_COPY, binascii.crc32(patch_file))

print(f"Writing '{self._single_diff}' to gateway")
hdr, _rsp = rpc_client.run_data_send_cmd(
file_write_basic.COMMAND_ID,
Auth.DEVICE,
bytes(params),
patch_file,
None,
file_write_basic.response.from_buffer_copy,
)
if hdr.return_code != 0:
sys.exit(f"Failed to save diff file to gateway (({errno.strerror(-hdr.return_code)}))")
print(f"'{self._single_diff}' written to gateway")

def run_file_upload(self, live: Live, mtu: int, source: HopReceived):
self.state_update(live, f"Uploading patch file to {source.infuse_id:016X}")
rpc_client = RpcClient(self._client, mtu, source.infuse_id)

params = file_write_basic.request(rpc_enum_file_action.APP_CPATCH, binascii.crc32(self.patch_file))

hdr, _rsp = rpc_client.run_data_send_cmd(
file_write_basic.COMMAND_ID,
Auth.DEVICE,
bytes(params),
self.patch_file,
self.data_progress_cb,
file_write_basic.response.from_buffer_copy,
)

if hdr.return_code == 0:
self._pending[source.infuse_id] = time.time() + 60

def run_file_copy(self, live: Live, mtu: int, source: HopReceived):
self.state_update(live, f"Copying patch file to {source.infuse_id:016X}")
rpc_client = RpcClient(self._client, mtu, InfuseID.GATEWAY)

params = bt_file_copy_basic.request(
source.interface_address.val.to_rpc_struct(),
rpc_enum_file_action.APP_CPATCH,
0,
len(self.patch_file),
binascii.crc32(self.patch_file),
1,
3,
)

hdr, _rsp = rpc_client.run_standard_cmd(
bt_file_copy_basic.COMMAND_ID,
Auth.DEVICE,
bytes(params),
bt_file_copy_basic.response.from_buffer_copy,
)
if hdr.return_code == 0:
self._pending[source.infuse_id] = time.time() + 60

def run(self):
if self._single_diff:
self.gateway_diff_load()

with Live(self.progress_table(), refresh_per_second=4) as live:
for source, announce in self._client.observe_announce():
self.state_update(live, "Scanning")
Expand Down Expand Up @@ -169,6 +251,14 @@ def run(self):

# Do we have a valid diff?
diff_file = self._release.dir / "diffs" / f"{v_str}.bin"
if self._single_diff and self._single_diff != diff_file:
# Not the file we've copied to the gateway flash
self._missing_diffs.add(v_str)
self._handled.append(source.infuse_id)
self._no_diff += 1
self.state_update(live, "Scanning")
continue

if not diff_file.exists():
# Is this a single diff from a different application we know about?
diff_file = self._release.dir / "diffs" / f"0x{announce.application:08x}" / f"{v_str}.bin"
Expand All @@ -193,24 +283,10 @@ def run(self):
with self._client.connection(
source.infuse_id, GatewayRequestConnectionRequest.DataType.COMMAND, self._conn_timeout
) as mtu:
self.state_update(live, f"Uploading patch file to {source.infuse_id:016X}")
rpc_client = RpcClient(self._client, mtu, source.infuse_id)

params = file_write_basic.request(
rpc_enum_file_action.APP_CPATCH, binascii.crc32(self.patch_file)
)

hdr, _rsp = rpc_client.run_data_send_cmd(
file_write_basic.COMMAND_ID,
Auth.DEVICE,
bytes(params),
self.patch_file,
self.data_progress_cb,
file_write_basic.response.from_buffer_copy,
)

if hdr.return_code == 0:
self._pending[source.infuse_id] = time.time() + 60
if self._single_diff:
self.run_file_copy(live, mtu, source)
else:
self.run_file_upload(live, mtu, source)

except ConnectionRefusedError:
self.state_update(live, "Scanning")
Expand Down
6 changes: 3 additions & 3 deletions src/infuse_iot/util/soc/nrf.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,15 +141,15 @@ def unique_device_id_len(self) -> int:
def unique_device_id(self) -> int:
device_id_addr = self.family.FICR_ADDRESS + self.family.DEVICE_ID_OFFSET

result = self._exec(["x-read", "--address", hex(device_id_addr), "--bytes", "8", "--direct"])
result = self._exec(["read", "--address", hex(device_id_addr), "--bytes", "8", "--direct"])
data_bytes = result[0]["devices"][0]["memoryData"][0]["values"]
dev_id_bytes = bytes(data_bytes)
return int.from_bytes(dev_id_bytes, "big")

def read_provisioned_data(self, num: int) -> bytes:
customer_addr = self.uicr_base + self.family.CUSTOMER_OFFSET

result = self._exec(["x-read", "--address", hex(customer_addr), "--bytes", str(num), "--direct"])
result = self._exec(["read", "--address", hex(customer_addr), "--bytes", str(num), "--direct"])
data_bytes = result[0]["devices"][0]["memoryData"][0]["values"]

return bytes(data_bytes)
Expand All @@ -164,4 +164,4 @@ def write_provisioning_data(self, data: bytes):
chunk_bytes += b"\xff" * (4 - len(chunk_bytes))
data_word = int.from_bytes(chunk_bytes, byteorder="little")

self._exec(["x-write", "--address", hex(customer_addr + offset), "--value", hex(data_word)])
self._exec(["write", "--address", hex(customer_addr + offset), "--value", hex(data_word)])
Loading