Add examples instead of __main__.py
This commit is contained in:
parent
db186e65a1
commit
b21b35f366
9 changed files with 320 additions and 269 deletions
|
|
@ -12,21 +12,35 @@ from .protocol import InteractionModelOpcode, ProtocolId, SecureProtocolOpcode
|
||||||
from . import session
|
from . import session
|
||||||
from .device_types.utility.root_node import RootNode
|
from .device_types.utility.root_node import RootNode
|
||||||
|
|
||||||
__version__ = "0.0.0"
|
__version__ = "0.1.0"
|
||||||
|
|
||||||
|
|
||||||
class CircuitMatter:
|
class CircuitMatter:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
socketpool,
|
socketpool=None,
|
||||||
mdns_server,
|
mdns_server=None,
|
||||||
random_source,
|
random_source=None,
|
||||||
state_filename,
|
state_filename="matter-device-state.json",
|
||||||
vendor_id=0xFFF1,
|
vendor_id=0xFFF1,
|
||||||
product_id=0x8000,
|
product_id=0x8000,
|
||||||
):
|
):
|
||||||
|
if socketpool is None:
|
||||||
|
import socket
|
||||||
|
|
||||||
|
socketpool = socket
|
||||||
self.socketpool = socketpool
|
self.socketpool = socketpool
|
||||||
|
|
||||||
|
if mdns_server is None:
|
||||||
|
from circuitmatter.utility.mdns.avahi import Avahi
|
||||||
|
|
||||||
|
mdns_server = Avahi()
|
||||||
self.mdns_server = mdns_server
|
self.mdns_server = mdns_server
|
||||||
|
|
||||||
|
if random_source is None:
|
||||||
|
from circuitmatter.utility import random
|
||||||
|
|
||||||
|
random_source = random
|
||||||
self.random = random_source
|
self.random = random_source
|
||||||
|
|
||||||
self.nonvolatile = nonvolatile.PersistentDictionary(state_filename)
|
self.nonvolatile = nonvolatile.PersistentDictionary(state_filename)
|
||||||
|
|
|
||||||
|
|
@ -1,264 +0,0 @@
|
||||||
"""Pure Python implementation of the Matter IOT protocol."""
|
|
||||||
|
|
||||||
import binascii
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import pathlib
|
|
||||||
import secrets
|
|
||||||
import socket
|
|
||||||
import subprocess
|
|
||||||
import time
|
|
||||||
|
|
||||||
import circuitmatter as cm
|
|
||||||
|
|
||||||
from circuitmatter.device_types.lighting import on_off
|
|
||||||
|
|
||||||
|
|
||||||
class ReplaySocket:
|
|
||||||
def __init__(self, replay_data):
|
|
||||||
self.replay_data = replay_data
|
|
||||||
|
|
||||||
def bind(self, address):
|
|
||||||
print("bind to", address)
|
|
||||||
|
|
||||||
def setblocking(self, value):
|
|
||||||
print("setblocking", value)
|
|
||||||
|
|
||||||
def recvfrom_into(self, buffer, nbytes=None):
|
|
||||||
if nbytes is None:
|
|
||||||
nbytes = len(buffer)
|
|
||||||
direction = "send"
|
|
||||||
while direction == "send":
|
|
||||||
direction, _, address, data_b64 = self.replay_data.pop(0)
|
|
||||||
decoded = binascii.a2b_base64(data_b64)
|
|
||||||
if len(decoded) > nbytes:
|
|
||||||
raise RuntimeError("Next replay packet is larger than buffer to read into")
|
|
||||||
buffer[: len(decoded)] = decoded
|
|
||||||
return len(decoded), address
|
|
||||||
|
|
||||||
def sendto(self, data, address):
|
|
||||||
if address is None:
|
|
||||||
raise ValueError("Address must be set")
|
|
||||||
# direction, _, address, data_b64 = self.replay_data.pop(0)
|
|
||||||
# if direction == "send":
|
|
||||||
# decoded = binascii.a2b_base64(data_b64)
|
|
||||||
# for i, b in enumerate(data):
|
|
||||||
# if b != decoded[i]:
|
|
||||||
# # print("sent", data.hex(" "))
|
|
||||||
# # print("old ", decoded.hex(" "))
|
|
||||||
# # print(i, hex(b), hex(decoded[i]))
|
|
||||||
# print("Next replay packet does not match sent data")
|
|
||||||
return len(data)
|
|
||||||
|
|
||||||
|
|
||||||
class ReplayRandom:
|
|
||||||
def __init__(self, replay_data):
|
|
||||||
self.replay_data = replay_data
|
|
||||||
|
|
||||||
def urandom(self, nbytes):
|
|
||||||
direction = None
|
|
||||||
while direction != "urandom":
|
|
||||||
direction, _, recorded_nbytes, data_b64 = self.replay_data.pop(0)
|
|
||||||
if recorded_nbytes != nbytes:
|
|
||||||
raise RuntimeError("Next replay random data is not the expected length")
|
|
||||||
decoded = binascii.a2b_base64(data_b64)
|
|
||||||
return decoded
|
|
||||||
|
|
||||||
def randbelow(self, n):
|
|
||||||
direction = None
|
|
||||||
while direction != "randbelow":
|
|
||||||
direction, _, recorded_n, value = self.replay_data.pop(0)
|
|
||||||
if recorded_n != n:
|
|
||||||
raise RuntimeError("Next replay randbelow is not the expected length")
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
class ReplaySocketPool:
|
|
||||||
AF_INET6 = 0
|
|
||||||
SOCK_DGRAM = 1
|
|
||||||
|
|
||||||
def __init__(self, replay_lines):
|
|
||||||
self.replay_data = replay_lines
|
|
||||||
self._socket_created = False
|
|
||||||
|
|
||||||
def socket(self, *args, **kwargs):
|
|
||||||
if self._socket_created:
|
|
||||||
raise RuntimeError("Only one socket can be created")
|
|
||||||
self._socket_created = True
|
|
||||||
return ReplaySocket(self.replay_data)
|
|
||||||
|
|
||||||
|
|
||||||
class DummyMDNS:
|
|
||||||
def advertise_service(
|
|
||||||
self,
|
|
||||||
service_type,
|
|
||||||
protocol,
|
|
||||||
port,
|
|
||||||
txt_records=[],
|
|
||||||
subtypes=[],
|
|
||||||
instance_name="",
|
|
||||||
):
|
|
||||||
print(f"Advertise service {service_type} {protocol} {port} {txt_records}")
|
|
||||||
|
|
||||||
|
|
||||||
class MDNSServer(DummyMDNS):
|
|
||||||
def __init__(self):
|
|
||||||
self.active_services = {}
|
|
||||||
self.publish_address = None
|
|
||||||
|
|
||||||
def advertise_service(
|
|
||||||
self,
|
|
||||||
service_type,
|
|
||||||
protocol,
|
|
||||||
port,
|
|
||||||
txt_records={},
|
|
||||||
subtypes=[],
|
|
||||||
instance_name="",
|
|
||||||
):
|
|
||||||
subtypes = [f"--subtype={subtype}" for subtype in subtypes]
|
|
||||||
txt_records = [f"{key}={value}" for key, value in txt_records.items()]
|
|
||||||
command = [
|
|
||||||
"avahi-publish-service",
|
|
||||||
*subtypes,
|
|
||||||
instance_name,
|
|
||||||
f"{service_type}.{protocol}",
|
|
||||||
str(port),
|
|
||||||
*txt_records,
|
|
||||||
]
|
|
||||||
print("running avahi", command)
|
|
||||||
self.active_services[service_type + instance_name] = subprocess.Popen(command)
|
|
||||||
if self.publish_address is None:
|
|
||||||
command = [
|
|
||||||
"avahi-publish-address",
|
|
||||||
"dalinar.local",
|
|
||||||
"fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", # "fe80::642:1aff:fe0c:9f2a",
|
|
||||||
]
|
|
||||||
print("run", command)
|
|
||||||
self.publish_address = subprocess.Popen(command)
|
|
||||||
|
|
||||||
def __del__(self):
|
|
||||||
for active_service in self.active_services.values():
|
|
||||||
active_service.kill()
|
|
||||||
if self.publish_address is not None:
|
|
||||||
self.publish_address.kill()
|
|
||||||
|
|
||||||
|
|
||||||
class RecordingRandom:
|
|
||||||
def __init__(self, record_file):
|
|
||||||
self.record_file = record_file
|
|
||||||
|
|
||||||
def urandom(self, nbytes):
|
|
||||||
data = os.urandom(nbytes)
|
|
||||||
entry = (
|
|
||||||
"urandom",
|
|
||||||
time.monotonic_ns(),
|
|
||||||
nbytes,
|
|
||||||
binascii.b2a_base64(data, newline=False).decode("utf-8"),
|
|
||||||
)
|
|
||||||
json.dump(entry, self.record_file)
|
|
||||||
self.record_file.write("\n")
|
|
||||||
return data
|
|
||||||
|
|
||||||
def randbelow(self, n):
|
|
||||||
value = secrets.randbelow(n)
|
|
||||||
entry = ("randbelow", time.monotonic_ns(), n, value)
|
|
||||||
json.dump(entry, self.record_file)
|
|
||||||
self.record_file.write("\n")
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
class RecordingSocket:
|
|
||||||
def __init__(self, record_file, socket):
|
|
||||||
self.record_file = record_file
|
|
||||||
self.socket = socket
|
|
||||||
|
|
||||||
def bind(self, address):
|
|
||||||
self.socket.bind(address)
|
|
||||||
|
|
||||||
def setblocking(self, value):
|
|
||||||
self.socket.setblocking(value)
|
|
||||||
|
|
||||||
def recvfrom_into(self, buffer, nbytes=None):
|
|
||||||
nbytes, addr = self.socket.recvfrom_into(buffer, nbytes)
|
|
||||||
entry = (
|
|
||||||
"receive",
|
|
||||||
time.monotonic_ns(),
|
|
||||||
addr,
|
|
||||||
binascii.b2a_base64(buffer[:nbytes], newline=False).decode("utf-8"),
|
|
||||||
)
|
|
||||||
json.dump(entry, self.record_file)
|
|
||||||
self.record_file.write("\n")
|
|
||||||
return nbytes, addr
|
|
||||||
|
|
||||||
def sendto(self, data, address):
|
|
||||||
entry = (
|
|
||||||
"send",
|
|
||||||
time.monotonic_ns(),
|
|
||||||
address,
|
|
||||||
binascii.b2a_base64(data, newline=False).decode("utf-8"),
|
|
||||||
)
|
|
||||||
json.dump(entry, self.record_file)
|
|
||||||
self.record_file.write("\n")
|
|
||||||
return self.socket.sendto(data, address)
|
|
||||||
|
|
||||||
|
|
||||||
class RecordingSocketPool:
|
|
||||||
AF_INET6 = socket.AF_INET6
|
|
||||||
SOCK_DGRAM = socket.SOCK_DGRAM
|
|
||||||
|
|
||||||
def __init__(self, record_file):
|
|
||||||
self.record_file = record_file
|
|
||||||
self._socket_created = False
|
|
||||||
|
|
||||||
def socket(self, *args, **kwargs):
|
|
||||||
if self._socket_created:
|
|
||||||
raise RuntimeError("Only one socket can be created")
|
|
||||||
self._socket_created = True
|
|
||||||
return RecordingSocket(self.record_file, socket.socket(*args, **kwargs))
|
|
||||||
|
|
||||||
|
|
||||||
class NeoPixel(on_off.OnOffLight):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def run(replay_file=None):
|
|
||||||
device_state = pathlib.Path("test_data/device_state.json")
|
|
||||||
replay_device_state = pathlib.Path("test_data/replay_device_state.json")
|
|
||||||
if replay_file:
|
|
||||||
replay_lines = []
|
|
||||||
with open(replay_file, "r") as f:
|
|
||||||
device_state_fn = f.readline().strip()
|
|
||||||
for line in f:
|
|
||||||
replay_lines.append(json.loads(line))
|
|
||||||
socketpool = ReplaySocketPool(replay_lines)
|
|
||||||
mdns_server = DummyMDNS()
|
|
||||||
random_source = ReplayRandom(replay_lines)
|
|
||||||
# Reset device state to before the captured run
|
|
||||||
device_state.write_text(pathlib.Path(device_state_fn).read_text())
|
|
||||||
else:
|
|
||||||
timestamp = time.strftime("%Y%m%d-%H%M%S")
|
|
||||||
record_file = open(f"test_data/recorded_packets-{timestamp}.jsonl", "w")
|
|
||||||
device_state_fn = f"test_data/device_state-{timestamp}.json"
|
|
||||||
record_file.write(f"{device_state_fn}\n")
|
|
||||||
socketpool = RecordingSocketPool(record_file)
|
|
||||||
mdns_server = MDNSServer()
|
|
||||||
random_source = RecordingRandom(record_file)
|
|
||||||
# Save device state before we run so replays can use it.
|
|
||||||
replay_device_state = pathlib.Path(device_state_fn)
|
|
||||||
replay_device_state.write_text(device_state.read_text())
|
|
||||||
|
|
||||||
matter = cm.CircuitMatter(socketpool, mdns_server, random_source, device_state)
|
|
||||||
led = NeoPixel("neopixel1")
|
|
||||||
matter.add_device(led)
|
|
||||||
while True:
|
|
||||||
matter.process_packets()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
import sys
|
|
||||||
|
|
||||||
print(sys.argv)
|
|
||||||
replay_file = None
|
|
||||||
if len(sys.argv) > 1:
|
|
||||||
replay_file = sys.argv[1]
|
|
||||||
run(replay_file=replay_file)
|
|
||||||
11
circuitmatter/utility/mdns/__init__.py
Normal file
11
circuitmatter/utility/mdns/__init__.py
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
class DummyMDNS:
|
||||||
|
def advertise_service(
|
||||||
|
self,
|
||||||
|
service_type,
|
||||||
|
protocol,
|
||||||
|
port,
|
||||||
|
txt_records=[],
|
||||||
|
subtypes=[],
|
||||||
|
instance_name="",
|
||||||
|
):
|
||||||
|
print(f"Advertise service {service_type} {protocol} {port} {txt_records}")
|
||||||
41
circuitmatter/utility/mdns/avahi.py
Normal file
41
circuitmatter/utility/mdns/avahi.py
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
|
||||||
|
class Avahi:
|
||||||
|
def __init__(self):
|
||||||
|
self.active_services = {}
|
||||||
|
self.publish_address = None
|
||||||
|
|
||||||
|
def advertise_service(
|
||||||
|
self,
|
||||||
|
service_type,
|
||||||
|
protocol,
|
||||||
|
port,
|
||||||
|
txt_records={},
|
||||||
|
subtypes=[],
|
||||||
|
instance_name="",
|
||||||
|
):
|
||||||
|
subtypes = [f"--subtype={subtype}" for subtype in subtypes]
|
||||||
|
txt_records = [f"{key}={value}" for key, value in txt_records.items()]
|
||||||
|
command = [
|
||||||
|
"avahi-publish-service",
|
||||||
|
*subtypes,
|
||||||
|
instance_name,
|
||||||
|
f"{service_type}.{protocol}",
|
||||||
|
str(port),
|
||||||
|
*txt_records,
|
||||||
|
]
|
||||||
|
self.active_services[service_type + instance_name] = subprocess.Popen(command)
|
||||||
|
if self.publish_address is None:
|
||||||
|
command = [
|
||||||
|
"avahi-publish-address",
|
||||||
|
"dalinar.local",
|
||||||
|
"fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", # "fe80::642:1aff:fe0c:9f2a",
|
||||||
|
]
|
||||||
|
self.publish_address = subprocess.Popen(command)
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
for active_service in self.active_services.values():
|
||||||
|
active_service.kill()
|
||||||
|
if self.publish_address is not None:
|
||||||
|
self.publish_address.kill()
|
||||||
6
circuitmatter/utility/random.py
Normal file
6
circuitmatter/utility/random.py
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
import os
|
||||||
|
import secrets
|
||||||
|
|
||||||
|
urandom = os.urandom
|
||||||
|
|
||||||
|
randbelow = secrets.randbelow
|
||||||
78
circuitmatter/utility/recording.py
Normal file
78
circuitmatter/utility/recording.py
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
import binascii
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
class RecordingRandom:
|
||||||
|
def __init__(self, record_file, random):
|
||||||
|
self.record_file = record_file
|
||||||
|
self._random = random
|
||||||
|
|
||||||
|
def urandom(self, nbytes):
|
||||||
|
data = self._random.urandom(nbytes)
|
||||||
|
entry = (
|
||||||
|
"urandom",
|
||||||
|
time.monotonic_ns(),
|
||||||
|
nbytes,
|
||||||
|
binascii.b2a_base64(data, newline=False).decode("utf-8"),
|
||||||
|
)
|
||||||
|
json.dump(entry, self.record_file)
|
||||||
|
self.record_file.write("\n")
|
||||||
|
return data
|
||||||
|
|
||||||
|
def randbelow(self, n):
|
||||||
|
value = self._random.randbelow(n)
|
||||||
|
entry = ("randbelow", time.monotonic_ns(), n, value)
|
||||||
|
json.dump(entry, self.record_file)
|
||||||
|
self.record_file.write("\n")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class RecordingSocket:
|
||||||
|
def __init__(self, record_file, socket):
|
||||||
|
self.record_file = record_file
|
||||||
|
self.socket = socket
|
||||||
|
|
||||||
|
def bind(self, address):
|
||||||
|
self.socket.bind(address)
|
||||||
|
|
||||||
|
def setblocking(self, value):
|
||||||
|
self.socket.setblocking(value)
|
||||||
|
|
||||||
|
def recvfrom_into(self, buffer, nbytes=None):
|
||||||
|
nbytes, addr = self.socket.recvfrom_into(buffer, nbytes)
|
||||||
|
entry = (
|
||||||
|
"receive",
|
||||||
|
time.monotonic_ns(),
|
||||||
|
addr,
|
||||||
|
binascii.b2a_base64(buffer[:nbytes], newline=False).decode("utf-8"),
|
||||||
|
)
|
||||||
|
json.dump(entry, self.record_file)
|
||||||
|
self.record_file.write("\n")
|
||||||
|
return nbytes, addr
|
||||||
|
|
||||||
|
def sendto(self, data, address):
|
||||||
|
entry = (
|
||||||
|
"send",
|
||||||
|
time.monotonic_ns(),
|
||||||
|
address,
|
||||||
|
binascii.b2a_base64(data, newline=False).decode("utf-8"),
|
||||||
|
)
|
||||||
|
json.dump(entry, self.record_file)
|
||||||
|
self.record_file.write("\n")
|
||||||
|
return self.socket.sendto(data, address)
|
||||||
|
|
||||||
|
|
||||||
|
class RecordingSocketPool:
|
||||||
|
def __init__(self, record_file, socket):
|
||||||
|
self.AF_INET6 = socket.AF_INET6
|
||||||
|
self.SOCK_DGRAM = socket.SOCK_DGRAM
|
||||||
|
self.record_file = record_file
|
||||||
|
self._socket_created = False
|
||||||
|
self._socket = socket
|
||||||
|
|
||||||
|
def socket(self, *args, **kwargs):
|
||||||
|
if self._socket_created:
|
||||||
|
raise RuntimeError("Only one socket can be created")
|
||||||
|
self._socket_created = True
|
||||||
|
return RecordingSocket(self.record_file, self._socket.socket(*args, **kwargs))
|
||||||
75
circuitmatter/utility/replay.py
Normal file
75
circuitmatter/utility/replay.py
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
import binascii
|
||||||
|
|
||||||
|
|
||||||
|
class ReplaySocket:
|
||||||
|
def __init__(self, replay_data):
|
||||||
|
self.replay_data = replay_data
|
||||||
|
|
||||||
|
def bind(self, address):
|
||||||
|
print("bind to", address)
|
||||||
|
|
||||||
|
def setblocking(self, value):
|
||||||
|
print("setblocking", value)
|
||||||
|
|
||||||
|
def recvfrom_into(self, buffer, nbytes=None):
|
||||||
|
if nbytes is None:
|
||||||
|
nbytes = len(buffer)
|
||||||
|
direction = "send"
|
||||||
|
while direction == "send":
|
||||||
|
direction, _, address, data_b64 = self.replay_data.pop(0)
|
||||||
|
decoded = binascii.a2b_base64(data_b64)
|
||||||
|
if len(decoded) > nbytes:
|
||||||
|
raise RuntimeError("Next replay packet is larger than buffer to read into")
|
||||||
|
buffer[: len(decoded)] = decoded
|
||||||
|
return len(decoded), address
|
||||||
|
|
||||||
|
def sendto(self, data, address):
|
||||||
|
if address is None:
|
||||||
|
raise ValueError("Address must be set")
|
||||||
|
# direction, _, address, data_b64 = self.replay_data.pop(0)
|
||||||
|
# if direction == "send":
|
||||||
|
# decoded = binascii.a2b_base64(data_b64)
|
||||||
|
# for i, b in enumerate(data):
|
||||||
|
# if b != decoded[i]:
|
||||||
|
# # print("sent", data.hex(" "))
|
||||||
|
# # print("old ", decoded.hex(" "))
|
||||||
|
# # print(i, hex(b), hex(decoded[i]))
|
||||||
|
# print("Next replay packet does not match sent data")
|
||||||
|
return len(data)
|
||||||
|
|
||||||
|
|
||||||
|
class ReplayRandom:
|
||||||
|
def __init__(self, replay_data):
|
||||||
|
self.replay_data = replay_data
|
||||||
|
|
||||||
|
def urandom(self, nbytes):
|
||||||
|
direction = None
|
||||||
|
while direction != "urandom":
|
||||||
|
direction, _, recorded_nbytes, data_b64 = self.replay_data.pop(0)
|
||||||
|
if recorded_nbytes != nbytes:
|
||||||
|
raise RuntimeError("Next replay random data is not the expected length")
|
||||||
|
decoded = binascii.a2b_base64(data_b64)
|
||||||
|
return decoded
|
||||||
|
|
||||||
|
def randbelow(self, n):
|
||||||
|
direction = None
|
||||||
|
while direction != "randbelow":
|
||||||
|
direction, _, recorded_n, value = self.replay_data.pop(0)
|
||||||
|
if recorded_n != n:
|
||||||
|
raise RuntimeError("Next replay randbelow is not the expected length")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class ReplaySocketPool:
|
||||||
|
AF_INET6 = 0
|
||||||
|
SOCK_DGRAM = 1
|
||||||
|
|
||||||
|
def __init__(self, replay_lines):
|
||||||
|
self.replay_data = replay_lines
|
||||||
|
self._socket_created = False
|
||||||
|
|
||||||
|
def socket(self, *args, **kwargs):
|
||||||
|
if self._socket_created:
|
||||||
|
raise RuntimeError("Only one socket can be created")
|
||||||
|
self._socket_created = True
|
||||||
|
return ReplaySocket(self.replay_data)
|
||||||
26
examples/circuitmatter_simpletest.py
Normal file
26
examples/circuitmatter_simpletest.py
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
"""Simple LED on and off as a light."""
|
||||||
|
|
||||||
|
import circuitmatter as cm
|
||||||
|
from circuitmatter.device_types.lighting import on_off
|
||||||
|
|
||||||
|
import digitalio
|
||||||
|
import board
|
||||||
|
|
||||||
|
|
||||||
|
class LED(on_off.OnOffLight):
|
||||||
|
def __init__(self, name, led):
|
||||||
|
super().__init__(name)
|
||||||
|
self._led = led
|
||||||
|
|
||||||
|
def on(self, session):
|
||||||
|
self._led.value = True
|
||||||
|
|
||||||
|
def off(self, session):
|
||||||
|
self._led.value = False
|
||||||
|
|
||||||
|
|
||||||
|
matter = cm.CircuitMatter(state_filename="test_data/device_state.json")
|
||||||
|
led = LED("led1", digitalio.DigitalInOut(board.D13))
|
||||||
|
matter.add_device(led)
|
||||||
|
while True:
|
||||||
|
matter.process_packets()
|
||||||
64
examples/replay.py
Normal file
64
examples/replay.py
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
"""Pure Python implementation of the Matter IOT protocol."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import pathlib
|
||||||
|
import socket
|
||||||
|
import time
|
||||||
|
|
||||||
|
import circuitmatter as cm
|
||||||
|
|
||||||
|
from circuitmatter.device_types.lighting import on_off
|
||||||
|
|
||||||
|
from circuitmatter.utility import random
|
||||||
|
from circuitmatter.utility.recording import RecordingSocketPool, RecordingRandom
|
||||||
|
from circuitmatter.utility.replay import ReplaySocketPool, ReplayRandom
|
||||||
|
|
||||||
|
from circuitmatter.utility.mdns import DummyMDNS
|
||||||
|
from circuitmatter.utility.mdns.avahi import Avahi
|
||||||
|
|
||||||
|
|
||||||
|
class NeoPixel(on_off.OnOffLight):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def run(replay_file=None):
|
||||||
|
device_state = pathlib.Path("test_data/device_state.json")
|
||||||
|
replay_device_state = pathlib.Path("test_data/replay_device_state.json")
|
||||||
|
if replay_file:
|
||||||
|
replay_lines = []
|
||||||
|
with open(replay_file, "r") as f:
|
||||||
|
device_state_fn = f.readline().strip()
|
||||||
|
for line in f:
|
||||||
|
replay_lines.append(json.loads(line))
|
||||||
|
socketpool = ReplaySocketPool(replay_lines)
|
||||||
|
mdns_server = DummyMDNS()
|
||||||
|
random_source = ReplayRandom(replay_lines)
|
||||||
|
# Reset device state to before the captured run
|
||||||
|
device_state.write_text(pathlib.Path(device_state_fn).read_text())
|
||||||
|
else:
|
||||||
|
timestamp = time.strftime("%Y%m%d-%H%M%S")
|
||||||
|
record_file = open(f"test_data/recorded_packets-{timestamp}.jsonl", "w")
|
||||||
|
device_state_fn = f"test_data/device_state-{timestamp}.json"
|
||||||
|
record_file.write(f"{device_state_fn}\n")
|
||||||
|
socketpool = RecordingSocketPool(record_file, socket)
|
||||||
|
mdns_server = Avahi()
|
||||||
|
random_source = RecordingRandom(record_file, random)
|
||||||
|
# Save device state before we run so replays can use it.
|
||||||
|
replay_device_state = pathlib.Path(device_state_fn)
|
||||||
|
replay_device_state.write_text(device_state.read_text())
|
||||||
|
|
||||||
|
matter = cm.CircuitMatter(socketpool, mdns_server, random_source, device_state)
|
||||||
|
led = NeoPixel("neopixel1")
|
||||||
|
matter.add_device(led)
|
||||||
|
while True:
|
||||||
|
matter.process_packets()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import sys
|
||||||
|
|
||||||
|
print(sys.argv)
|
||||||
|
replay_file = None
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
replay_file = sys.argv[1]
|
||||||
|
run(replay_file=replay_file)
|
||||||
Loading…
Reference in a new issue