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
111 changes: 111 additions & 0 deletions scripts/apn_set.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
#!/usr/bin/env python3

import argparse
import ctypes

from rich.live import Live
from rich.status import Status
from rich.table import Table

import infuse_iot.generated.kv_definitions as kv
import infuse_iot.generated.rpc_definitions as rpc
from infuse_iot.epacket.packet import Auth
from infuse_iot.generated.tdf_definitions import readings
from infuse_iot.rpc_client import RpcClient
from infuse_iot.socket_comms import (
GatewayRequestConnectionRequest,
LocalClient,
default_multicast_address,
)
from infuse_iot.tdf import TDF
from infuse_iot.util.ctypes import VLACompatLittleEndianStruct


class APNSetter:
def __init__(self, args: argparse.Namespace):
self.app_id = args.app
self.apn = args.apn
self.client = LocalClient(default_multicast_address(), 1.0)
self.decoder = TDF()
self.state = "Scanning"

self.already: list[int] = []
self.updated: list[int] = []

def progress_table(self):
table = Table()
table.add_column()
table.add_column("Count")
table.add_row("Updated", str(len(self.updated)))
table.add_row("Already", str(len(self.already)))

meta = Table(box=None)
meta.add_column()
meta.add_row(Status(self.state))
meta.add_row(table)

return meta

class response(VLACompatLittleEndianStruct):
_fields_ = []
vla_field = ("rc", ctypes.c_int16)

def state_update(self, live: Live, state: str):
self.state = state
live.update(self.progress_table())

def announce_observed(self, live: Live, infuse_id: int, pkt: readings.announce):
if infuse_id in self.updated or infuse_id in self.already:
return
if pkt.application != self.app_id:
return
try:
self.state_update(live, f"Connecting to {infuse_id:016X}")
with self.client.connection(infuse_id, GatewayRequestConnectionRequest.DataType.COMMAND) as mtu:
rpc_client = RpcClient(self.client, mtu, infuse_id)

ipv4 = 0
family_bytes = ipv4.to_bytes(1, "little")
apn_bytes = self.apn.encode("utf-8") + b"\x00"
val_bytes = family_bytes + len(apn_bytes).to_bytes(1, "little") + apn_bytes

all_vals = (
bytes(rpc.rpc_struct_kv_store_value(kv.slots.lte_pdp_config.BASE_ID, len(val_bytes))) + val_bytes
)
params = bytes(rpc.kv_write.request(1)) + all_vals

hdr, rsp = rpc_client.run_standard_cmd(
rpc.kv_write.COMMAND_ID, Auth.DEVICE, params, self.response.from_buffer_copy
)
if hdr.return_code == 0:
assert rsp is not None and hasattr(rsp, "rc")
if rsp.rc[0] == 0:
self.already.append(infuse_id)
else:
self.updated.append(infuse_id)

except ConnectionRefusedError:
self.state_update(live, "Scanning")
return
self.state_update(live, "Scanning")

def run(self):
with Live(self.progress_table(), refresh_per_second=4) as live:
for source, announce in self.client.observe_announce():
self.announce_observed(live, source.infuse_id, announce)
live.update(self.progress_table())


if __name__ == "__main__":
parser = argparse.ArgumentParser("Configure the LTE APN of devices")
addr_group = parser.add_mutually_exclusive_group(required=True)
addr_group.add_argument("--app", type=lambda x: int(x, 0), help="Application ID to configure")
parser.add_argument("--apn", type=str, required=True, help="LTE APN value")

args = parser.parse_args()

try:
tool = APNSetter(args)
tool.run()
except KeyboardInterrupt:
pass
97 changes: 97 additions & 0 deletions scripts/reboot_count_reset.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#!/usr/bin/env python3

import argparse

from rich.live import Live
from rich.status import Status
from rich.table import Table

import infuse_iot.generated.kv_definitions as kv
import infuse_iot.generated.rpc_definitions as rpc
from infuse_iot.epacket.packet import Auth
from infuse_iot.generated.tdf_definitions import readings
from infuse_iot.rpc_client import RpcClient
from infuse_iot.socket_comms import (
GatewayRequestConnectionRequest,
LocalClient,
default_multicast_address,
)
from infuse_iot.tdf import TDF


class RebootCountResetter:
def __init__(self, args: argparse.Namespace):
self.app_id = args.app
self.count = args.count
self.client = LocalClient(default_multicast_address(), 1.0)
self.decoder = TDF()
self.state = "Scanning"

self.already: list[int] = []
self.updated: list[int] = []

def progress_table(self):
table = Table()
table.add_column()
table.add_column("Count")
table.add_row("Updated", str(len(self.updated)))
table.add_row("Already", str(len(self.already)))

meta = Table(box=None)
meta.add_column()
meta.add_row(table)
meta.add_row(Status(self.state))

return meta

def state_update(self, live: Live, state: str):
self.state = state
live.update(self.progress_table())

def announce_observed(self, live: Live, infuse_id: int, pkt: readings.announce):
if pkt.application != self.app_id:
return
if pkt.reboots == self.count:
if (infuse_id not in self.already) and (infuse_id not in self.updated):
self.already.append(infuse_id)
return
try:
self.state_update(live, f"Connecting to {infuse_id:016X}")
with self.client.connection(infuse_id, GatewayRequestConnectionRequest.DataType.COMMAND) as mtu:
rpc_client = RpcClient(self.client, mtu, infuse_id)

key_val = bytes(kv.slots.reboots(self.count))
all_vals = bytes(rpc.rpc_struct_kv_store_value(kv.slots.reboots.BASE_ID, len(key_val))) + key_val
params = bytes(rpc.kv_write.request(1)) + all_vals

hdr, _ = rpc_client.run_standard_cmd(
rpc.kv_write.COMMAND_ID, Auth.DEVICE, params, rpc.kv_write.response.from_buffer_copy
)
if hdr.return_code == 0:
self.updated.append(infuse_id)

except ConnectionRefusedError:
self.state_update(live, "Scanning")
return
self.state_update(live, "Scanning")

def run(self):
with Live(self.progress_table(), refresh_per_second=4) as live:
for source, announce in self.client.observe_announce():
self.announce_observed(live, source.infuse_id, announce)
live.update(self.progress_table())


if __name__ == "__main__":
parser = argparse.ArgumentParser("Reset reboot counters to a common value")
addr_group = parser.add_mutually_exclusive_group(required=True)
addr_group.add_argument("--app", type=lambda x: int(x, 0), help="Application ID to reset")
parser.add_argument("--count", type=int, default=0, help="Value to reset count to")

args = parser.parse_args()

try:
tool = RebootCountResetter(args)
tool.run()
except KeyboardInterrupt:
pass
22 changes: 22 additions & 0 deletions src/infuse_iot/rpc_wrappers/zbus_channel_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ class LocationChannel(ctypes.LittleEndianStructure):
id = 0x43210004
data = defs.readings.gcs_wgs84_llha

class NavPvtUbxChannel(ctypes.LittleEndianStructure):
id = 0x43210007
data = defs.readings.ubx_nav_pvt

class NavPvtNRFChannel(ctypes.LittleEndianStructure):
id = 0x43210008
data = defs.readings.nrf9x_gnss_pvt

@classmethod
def add_parser(cls, parser):
group = parser.add_mutually_exclusive_group(required=True)
Expand All @@ -56,6 +64,20 @@ def add_parser(cls, parser):
const=cls.LocationChannel,
help="Location channel",
)
group.add_argument(
"--nav-pvt-ubx",
dest="channel",
action="store_const",
const=cls.NavPvtUbxChannel,
help="ublox NAV-PVT channel",
)
group.add_argument(
"--nav-pvt-nrf",
dest="channel",
action="store_const",
const=cls.NavPvtNRFChannel,
help="nRF9x NAV-PVT channel",
)

def __init__(self, args):
self._channel = args.channel
Expand Down
39 changes: 39 additions & 0 deletions src/infuse_iot/socket_comms.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class Type(enum.IntEnum):
CONNECTION_FAILED = 1
CONNECTION_CREATED = 2
CONNECTION_DROPPED = 3
KNOWN_DEVICES = 4

def to_json(self) -> dict:
"""Convert class to json dictionary"""
Expand All @@ -44,6 +45,8 @@ def from_json(cls, values: dict) -> Self:
return cast(Self, ClientNotificationConnectionCreated.from_json(values))
elif values["type"] == cls.Type.CONNECTION_DROPPED:
return cast(Self, ClientNotificationConnectionDropped.from_json(values))
elif values["type"] == cls.Type.KNOWN_DEVICES:
return cast(Self, ClientNotificationObservedDevices.from_json(values))
raise NotImplementedError


Expand All @@ -62,6 +65,23 @@ def from_json(cls, values: dict) -> Self:
return cls(PacketReceived.from_json(values["epacket"]))


class ClientNotificationObservedDevices(ClientNotification):
TYPE = ClientNotification.Type.KNOWN_DEVICES

def __init__(self, devices: dict[int, dict]):
self.devices = devices

def to_json(self) -> dict:
"""Convert class to json dictionary"""
return {"type": int(self.TYPE), "devices": json.dumps(self.devices)}

@classmethod
def from_json(cls, values: dict) -> Self:
raw = json.loads(values["devices"])
decoded = {int(k): v for k, v in raw.items()}
return cls(decoded)


class ClientNotificationConnection(ClientNotification):
TYPE = 0

Expand Down Expand Up @@ -110,6 +130,7 @@ class Type(enum.IntEnum):
EPACKET_SEND = 0
CONNECTION_REQUEST = 1
CONNECTION_RELEASE = 2
KNOWN_DEVICES = 3

def to_json(self) -> dict:
"""Convert class to json dictionary"""
Expand All @@ -124,6 +145,8 @@ def from_json(cls, values: dict) -> Self:
return cast(Self, GatewayRequestConnectionRequest.from_json(values))
elif values["type"] == cls.Type.CONNECTION_RELEASE:
return cast(Self, GatewayRequestConnectionRelease.from_json(values))
elif values["type"] == cls.Type.KNOWN_DEVICES:
return cast(Self, GatewayRequestObservedDevices.from_json(values))
raise NotImplementedError


Expand All @@ -143,6 +166,22 @@ def from_json(cls, values: dict) -> Self:
return cls(PacketOutput.from_json(values["epacket"]))


class GatewayRequestObservedDevices(GatewayRequest):
"""Request list of known devices"""

TYPE = GatewayRequest.Type.KNOWN_DEVICES

def __init__(self):
pass

def to_json(self) -> dict:
return {"type": int(self.TYPE)}

@classmethod
def from_json(cls, values: dict) -> Self:
return cls()


class GatewayRequestConnection(GatewayRequest):
TYPE = 0

Expand Down
Loading