diff --git a/programs/ae33.py b/programs/ae33.py new file mode 100644 index 0000000000000000000000000000000000000000..1477b522596451ae4016674696884abc6ac4c83b --- /dev/null +++ b/programs/ae33.py @@ -0,0 +1,55 @@ +import sys + +from umnp.microcontroller.communication.udp_communicator import UDPCommunicator +from umnp.microcontroller.devices.network.ethernet_w5500 import EthernetW5500 +from umnp.microcontroller.devices.network.udp import UDPSender, UDPReceiver +from umnp.microcontroller.measurementdevice import MeasurementDevice +from umnp.microcontroller.tasks.periodictask import PeriodicTask +from umnp.protocol.common import UDP_DATA_PORT, UDP_CMD_PORT + +if sys.implementation.name == "micropython": + # noinspection PyUnresolvedReferences + import machine + + # noinspection PyUnresolvedReferences + import ntptime + + # noinspection PyUnresolvedReferences + import uasyncio as asyncio +else: + from umnp.microcontroller.umock import machine + import asyncio + + +def report_time(*args) -> str: + return "" + + +def main(): + # configure network + device = MeasurementDevice() + spi = machine.SPI( + 0, 2_000_000, mosi=machine.Pin(19), miso=machine.Pin(16), sck=machine.Pin(18) + ) + ether = EthernetW5500( + spi, machine.Pin(17), machine.Pin(20), mac=device.generated_mac_raw(), dhcp=True + ) + + uart = machine.UART(1, 11) + + device.add_network_adapter(ether) + sender = UDPSender(ether.ip, ether.netmask, UDP_DATA_PORT) + receiver = UDPReceiver(ether.ip, UDP_CMD_PORT) + + communicator = UDPCommunicator( + receiver=receiver, sender=sender, device_id=device.identifier + ) + + timer = PeriodicTask(report_time, print, 1000) + communicator.add_task(task=timer, name="timer") + # start + asyncio.run(communicator.start()) + + +if __name__ == "__main__": + main() diff --git a/programs/base_program.py b/programs/base_program.py new file mode 100644 index 0000000000000000000000000000000000000000..47002bcc650d128620a7592a3f7e6709914f7913 --- /dev/null +++ b/programs/base_program.py @@ -0,0 +1,55 @@ +import sys + +from umnp.microcontroller.communication.udp_communicator import UDPCommunicator +from umnp.microcontroller.devices.network.ethernet_w5500 import EthernetW5500 +from umnp.microcontroller.devices.network.udp import UDPSender, UDPReceiver +from umnp.microcontroller.measurementdevice import MeasurementDevice +from umnp.microcontroller.tasks.periodictask import PeriodicTask +from umnp.protocol.common import UDP_DATA_PORT, UDP_CMD_PORT + +if sys.implementation.name == "micropython": + # noinspection PyUnresolvedReferences + import machine + + # noinspection PyUnresolvedReferences + import ntptime + + # noinspection PyUnresolvedReferences + import uasyncio as asyncio +else: + from umnp.microcontroller.umock import machine + import asyncio + + +def report_time(*args) -> str: + return "" + + +def main(): + # configure network + device = MeasurementDevice() + spi = machine.SPI( + 0, 2_000_000, mosi=machine.Pin(19), miso=machine.Pin(16), sck=machine.Pin(18) + ) + ether = EthernetW5500( + spi, machine.Pin(17), machine.Pin(20), mac=device.generated_mac_raw(), dhcp=True + ) + + ntptime.settime() + + device.add_network_adapter(ether) + sender = UDPSender(ether.ip, ether.netmask, UDP_DATA_PORT) + receiver = UDPReceiver(ether.ip, UDP_CMD_PORT) + + communicator = UDPCommunicator( + receiver=receiver, sender=sender, device_id=device.identifier + ) + + timer = PeriodicTask(report_time, print, 1000) + communicator.add_task(task=timer, name="timer") + # start + asyncio.run(communicator.start()) + + +if __name__ == "__main__": + main() diff --git a/programs/sensor_calibration.py b/programs/sensor_calibration.py index 9fcbb0e0bbb8a655b09aa50a8d5b44659418b9cc..27b43b661e5e289e82394e37718f0803b883bf91 100644 --- a/programs/sensor_calibration.py +++ b/programs/sensor_calibration.py @@ -32,7 +32,10 @@ def main(): spi = machine.SPI( 0, 2_000_000, mosi=machine.Pin(19), miso=machine.Pin(16), sck=machine.Pin(18) ) - ether = EthernetW5500(spi, 17, 20, mac=device.generated_mac_raw(), dhcp=True) + ether = EthernetW5500( + spi, machine.Pin(17), machine.Pin(20), mac=device.generated_mac_raw(), dhcp=True + ) + print(ether.mac) device.add_network_adapter(ether) i2c = machine.I2C(id=1, scl=machine.Pin(27), sda=machine.Pin(26)) @@ -49,3 +52,7 @@ def main(): comm.add_task(x, "test_function") # start asyncio.run(comm.start()) + + +if __name__ == "__main__": + main() diff --git a/scripts/ae33-info.py b/scripts/ae33-info.py new file mode 100644 index 0000000000000000000000000000000000000000..80d82a6902a420ead8fa2f252b0bac4fdef73f02 --- /dev/null +++ b/scripts/ae33-info.py @@ -0,0 +1,39 @@ +import time + +import serial + +conn = serial.Serial( + port="/dev/ttyUSB0", + baudrate=115200, + dsrdtr=True, + xonxoff=False, + rtscts=False, + timeout=0.5, +) + + +print("Connected") +time.sleep(1) +print() +print("get newest log") +conn.write(b"$AE33:L1\r") +print(conn.readline()) + +time.sleep(1) +print() +print("get tape advances") +conn.write(b"$AE33:A\r") +print(conn.readlines()) + +time.sleep(1) +print() +print("get setup file") +conn.write(b"$AE33:SG\r") +print(conn.readlines()) +print() + +time.sleep(1) +print("start measurement") +conn.write(b"$AE33:X1\r") +print(conn.readlines()) +print() diff --git a/scripts/ae33-measure.py b/scripts/ae33-measure.py new file mode 100644 index 0000000000000000000000000000000000000000..c11ed74333839f6284819e9c3516cb81cfcf2da3 --- /dev/null +++ b/scripts/ae33-measure.py @@ -0,0 +1,21 @@ +import time + +import serial + +conn = serial.Serial( + port="/dev/ttyUSB0", + baudrate=115200, + dsrdtr=True, + xonxoff=False, + rtscts=False, + timeout=0.5, +) + + +print("Connected") + + +for i in range(5): + conn.write(b"$AE33:D1\r") + print(conn.readline()) + time.sleep(0.2) diff --git a/scripts/helpers/__init__.py b/scripts/helpers/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/scripts/helpers/ampy.py b/scripts/helpers/ampy.py new file mode 100644 index 0000000000000000000000000000000000000000..c0a59f9ecb803e0d43a9c8c46dfd9cc7c78c9d0d --- /dev/null +++ b/scripts/helpers/ampy.py @@ -0,0 +1,42 @@ +import datetime +import os +import subprocess + + +def upload_file(port: str, local_path: str, remote_path: str | None = None): + if remote_path is None: + remote_path = local_path + if not os.path.exists(local_path): + raise ValueError("fFile {fn} does not exist") + if remote_path != local_path: + print(f"Uploading file {local_path} as {remote_path}") + else: + print(f"Uploading file {local_path}") + cmd = ["ampy", "--port", port, "put", local_path, remote_path] + subprocess.run(cmd) + + +def list_remote_file_dates_and_directories(port): + cmd = ["ampy", "--port", port, "run", "tools/list-files.py"] + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE) + files = {} + dirs = [] + for line in proc.stdout: + line = line.decode("utf-8") + fn, size, _, f_type, date = line.split() + while fn.startswith("/"): + fn = fn[1:] + if f_type == "f": + files[fn] = datetime.datetime.strptime(date, "%Y-%m-%dT%H:%M:%S") + elif f_type == "d": + dirs.append(fn) + + return files, dirs + + +def make_remote_directory(path: str, port: str): + if not path: + return + print(f"Creating directory {path}") + cmd = ["ampy", "--port", port, "mkdir", path] + subprocess.run(cmd) diff --git a/scripts/helpers/filesystem.py b/scripts/helpers/filesystem.py new file mode 100644 index 0000000000000000000000000000000000000000..e8b616e3d5017c47012439cc0c7d349f27b8881f --- /dev/null +++ b/scripts/helpers/filesystem.py @@ -0,0 +1,13 @@ +import os + + +def get_directory_tree_from_directories(directories: list[str]) -> list[str]: + dirs = list(set([os.path.split(d)[0] for d in directories])) + all_dirs = dirs + for d in dirs: + while d: + d = os.path.split(d)[0] + if d in all_dirs: + continue + all_dirs.append(d) + return list(sorted(list(set(dirs)))) diff --git a/scripts/helpers/git.py b/scripts/helpers/git.py new file mode 100644 index 0000000000000000000000000000000000000000..19d81dedc57e406f7f4e632cea30d26899d07198 --- /dev/null +++ b/scripts/helpers/git.py @@ -0,0 +1,58 @@ +import subprocess + + +def parse_git_status(state: str) -> str: + if len(state) == 0 or len(state) > 2: + raise ValueError + if "M" in state: + return "modified" + if "??" in state: + return "untracked" + if "D" in state: + return "deleted" + if "A" in state: + return "added" + if "T" in state: + return "type changed" + if "R" in state: + return "renamed" + if "C" in state: + return "copied" + + +def get_git_unclean_files() -> dict[str:str]: + """ + Returns a dictionary of unclean files of the git repository, in which the current directory resides. + + Raises + ------ + ValueError + If 'git status' can't be executed or fails with an error + + Returns + ------- + dict[str: str] + Keys of this dictionary are the paths of 'dirty' files (e.g. uncommitted and untracked files), + and a description as value. + """ + + git_status = {} + cmd = ["git", "status", "--porcelain=v1"] + try: + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + proc.wait() + except FileNotFoundError: + raise ValueError("git executable not found") + + if proc.returncode != 0: + error = "\n".join([x.decode("utf-8") for x in proc.stderr]) + raise ValueError(f"Error running {' '.join(cmd)}: {error}") + + for line in proc.stdout: + fields = line.decode("utf-8").split() + status = fields[0] + file = " ".join(fields[1:]) + if file.startswith('"') and file.endswith('"'): + file = file[1:-1] + git_status[file] = parse_git_status(status) + return git_status diff --git a/scripts/helpers/imports.py b/scripts/helpers/imports.py new file mode 100644 index 0000000000000000000000000000000000000000..ac949d2a8f3a7743afb68e516004774aaa92138b --- /dev/null +++ b/scripts/helpers/imports.py @@ -0,0 +1,55 @@ +import ast +import os + + +def get_module_names(node: ast.Import): + names = [n.name for n in node.names] + return names + + +def get_imported_files( + fn, + modules=None, + files=None, + filter_by: str | None = None, + ignore: str | None = None, +): + if modules is None: + modules = [] + if files is None: + files = [] + + current_modules = [] + + with open(fn) as f: + root = ast.parse(f.read(), fn) + for node in ast.iter_child_nodes(root): + if isinstance(node, ast.Import): + current_modules.extend(get_module_names(node)) + elif isinstance(node, ast.ImportFrom): + current_modules.append(node.module) + + current_modules = sorted(list(set(current_modules))) + for module in current_modules: + if module in modules: + pass + + module_fn = module.replace(".", os.sep) + + if (filter_by and module_fn.startswith(filter_by)) or filter_by is None: + if ignore is not None and ignore in module_fn: + continue + + if os.path.exists(module_fn) and os.path.isdir(module_fn): + module_fn = os.path.join(module_fn, "__init__.py") + else: + module_fn = module_fn + ".py" + modules.append(module) + files.append(module_fn) + files, modules = get_imported_files( + module_fn, modules, files, filter_by=filter_by, ignore=ignore + ) + + files = list(sorted(list(set(files)))) + modules = list(sorted(list(set(modules)))) + return files, modules diff --git a/scripts/upload-script.py b/scripts/upload-script.py new file mode 100644 index 0000000000000000000000000000000000000000..b6107cf2d560d1e3e0c13581c7c0a575d2d1041c --- /dev/null +++ b/scripts/upload-script.py @@ -0,0 +1,136 @@ +#! /usr/bin/env/python3 + +""" +upload-script.py + +Uploads a Python script, including imported dependencies to a microcontroller using ampy. +Unless otherwise specified, the upload will only occur, if the files do not contain any changes not yet committed +to git, i.e. the working tree (or the parts of it that will be uploaded) are not dirty. +If a file to be uploaded already exists on the microcontroller, it will only be uploaded if newer. + +Example: +python ./upload-script.py programs/sensor_calibration.py --allow-dirty --port "/dev/ttyACM0" + +""" +import argparse +import datetime +import os +import sys + +# this is ugly and probably not recommended, but enables imports relative to this file +sys.path.append(os.path.dirname(os.path.dirname(__file__))) + +from helpers.ampy import ( + upload_file, + list_remote_file_dates_and_directories, + make_remote_directory, +) +from helpers.filesystem import get_directory_tree_from_directories +from helpers.git import get_git_unclean_files +from helpers.imports import get_imported_files + + +def remote_file_older_or_missing(path: str, remote_file_date: datetime.datetime | None): + if not remote_file_date: + return True + + disk_date = datetime.datetime.fromtimestamp(os.path.getmtime(path)) + if disk_date > remote_file_date: + return True + + return False + + +def make_directories(port: str, local_dirs: list[str], remote_dirs: list[str]): + for path in local_dirs: + if path in remote_dirs: + print(f"Skipping creation of '{path}': directory already exists") + continue + make_remote_directory(path, port=port) + + +def upload_files(local_files, script, port: str, ignore_file_times=False): + remote_file_dates, remote_directories = list_remote_file_dates_and_directories(port) + local_directories = get_directory_tree_from_directories(local_files) + make_directories(port, local_directories, remote_directories) + + for file_path in local_files: + remote_date = remote_file_dates.get(file_path) + local_newer = remote_file_older_or_missing(file_path, remote_date) + if ignore_file_times or local_newer: + upload_file(port=port, local_path=file_path) + else: + print(f"Skipping '{file_path}': remote file is newer") + + remote_script_date = remote_file_dates.get("main.py") + if remote_file_older_or_missing(script, remote_script_date): + upload_file(port, script, "main.py") + else: + print(f"Skipping upload '{script}' as main.py: remote file is newer") + + +def check_for_unclean_files(files_to_upload: list[str], args): + if not args.no_git: + return + errors = 0 + unclean_files = get_git_unclean_files() + + for fn in files_to_upload: + if fn in unclean_files: + state = unclean_files[fn] + print(f"Warning: file '{fn}': {state}") + errors += 1 + + if errors == 0: + return + + print("Warning: files to upload contain uncommitted changes.") + if args.allow_dirty: + print("Continuing with dirty working tree") + return + + print("Please either commit and retry, or re-run this script with --dirty") + exit(1) + + +def main(args): + fn = args.script + files, modules = get_imported_files(fn, filter_by="umnp", ignore="mock") + check_for_unclean_files(files, args) + upload_files(files, fn, port=args.port, ignore_file_times=args.ignore_time) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(prog=sys.argv[0]) + parser.add_argument( + "--allow-dirty", + action="store_true", + help="allow upload even when changes to uploaded files are not yet committed to git", + ) + parser.add_argument( + "--ignore-time", + action="store_true", + help="ignore file timestamps and always upload, even when the " + "files on the disk are older than on the device", + ) + parser.add_argument( + "--no-git", action="store_true", help="don't check for git status" + ) + parser.add_argument( + "--port", help="address or port of the attached micro-controller", required=True + ) + + parser.add_argument( + "script", + help="Name of the python script file to upload. Will be renamed to main.py", + ) + + args_ = parser.parse_args() + if not os.path.exists(args_.script): + print(f"File '{args_.script}' does not exist") + exit(1) + if not os.path.exists(args_.port): + print(f"Can't connect to '{args_.port}': no such file") + exit(1) + + main(args_) diff --git a/tools/backup.py b/tools/backup.py deleted file mode 100644 index b30ff8f04a955b51b90c3be08a65166e33eebe77..0000000000000000000000000000000000000000 --- a/tools/backup.py +++ /dev/null @@ -1,11 +0,0 @@ -from fs import FileSystemInformation - -current_dir = "" - - -def main(): - fs = FileSystemInformation() - fs.print_fs_tree() - - -main() diff --git a/umnp/aux/__init__.py b/umnp/aux/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..7110bc0863601240e5f94501c3a3c3afdc7c7658 --- /dev/null +++ b/umnp/aux/__init__.py @@ -0,0 +1,9 @@ +import datetime + + +def utc_now_rounded_second() -> datetime.datetime: + now = datetime.datetime.now(tz=datetime.timezone.utc) + if now.microsecond >= 500 * 1000: + now = now + datetime.timedelta(seconds=1) + now = now.replace(microsecond=0) + return now diff --git a/umnp/aux/log_helper.py b/umnp/aux/log_helper.py new file mode 100644 index 0000000000000000000000000000000000000000..eda563d56e7305c4c0e3b8da1e55f3517cd84507 --- /dev/null +++ b/umnp/aux/log_helper.py @@ -0,0 +1,24 @@ +import logging +import os + + +def setup_logger(name: str, log_dir: str | None = None): + now = utc_now_rounded_second() + now_string = now.isoformat().replace(":", "-") + now_string = now_string.split("+")[0] + + logger = logging.getLogger() + logger.setLevel(logging.DEBUG) + + if log_dir: + if not os.path.exists(log_dir): + os.mkdir(log_dir) + log_file_fn = os.path.join(log_dir, f"{name}-{now_string}.log") + log_file = logging.FileHandler(log_file_fn) + log_file.setLevel(logging.DEBUG) + logger.addHandler(log_file) + + console_logger = logging.StreamHandler() + logger.addHandler(console_logger) + + return logger diff --git a/umnp/devices/aethalometer/ae33.py b/umnp/devices/aethalometer/ae33.py index 60f337ec1aeb1e7698fcc35154b17fd57f62ecb3..21eb348f54a92e6b7106d829f0015e5d57feba74 100644 --- a/umnp/devices/aethalometer/ae33.py +++ b/umnp/devices/aethalometer/ae33.py @@ -1,5 +1,7 @@ from umnp.communication.abstract_serial_connection import AbstractSerialConnection +AE33_BAUDRATE = 115200 + class SerialConnection: def __init__(self): diff --git a/umnp/microcontroller/communication/serial_uart.py b/umnp/microcontroller/communication/serial_uart.py index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..d943879eee3f3914f8ef7eb7cd33adbfbd0aa161 100644 --- a/umnp/microcontroller/communication/serial_uart.py +++ b/umnp/microcontroller/communication/serial_uart.py @@ -0,0 +1,13 @@ +import sys + +from umnp.communication.abstract_serial_connection import AbstractSerialConnection + +if sys.implementation == "micropython": + import machine +else: + from umnp.microcontroller.umock import machine + + +class UARTSerial(AbstractSerialConnection): + def __init__(self, address: machine.UART): + super().__init__(address) diff --git a/umnp/microcontroller/devices/network/ethernet_w5500.py b/umnp/microcontroller/devices/network/ethernet_w5500.py index 43425a25d810b0c81a79f16ddb6d4b11c3f1a003..bc54763f65a7a0a37f87abb8d852c88ba275baaf 100644 --- a/umnp/microcontroller/devices/network/ethernet_w5500.py +++ b/umnp/microcontroller/devices/network/ethernet_w5500.py @@ -13,18 +13,25 @@ else: class EthernetW5500(EthernetAdapter): - def __init__(self, spi: machine.SPI, pin1: int, pin2: int, mac: bytes, dhcp=False): + def __init__( + self, + spi: machine.SPI, + pin1: machine.Pin, + pin2: machine.Pin, + mac: bytes, + dhcp=False, + ): super().__init__() self._spi = spi - self._pin1 = machine.Pin(pin1) - self._pin2 = machine.Pin(pin2) - self._nic = network.WIZNET5K(spi, self._pin1, self._pin2, mac) - # self._nic.active(True) + self._pin1 = pin1 + self._pin2 = pin2 + self._nic = network.WIZNET5K(spi, self._pin1, self._pin2) + self._nic.active(True) self._nic.config(mac=mac) if dhcp: self.enable_dhcp() - def get_dhcp(self): + def enable_dhcp(self): while True: print("Requesting IP via DHCP") try: @@ -32,10 +39,7 @@ class EthernetW5500(EthernetAdapter): return except OSError: pass - - def enable_dhcp(self): - self.get_dhcp() - print(self._nic.ifconfig()) + print(self._nic.ifconfig()) @property def netmask(self): @@ -44,3 +48,7 @@ class EthernetW5500(EthernetAdapter): @property def ip(self): return self._nic.ifconfig()[0] + + @property + def mac(self): + return self._nic.config("mac") diff --git a/umnp/microcontroller/umock/machine/__init__.py b/umnp/microcontroller/umock/machine/__init__.py index 5bda3828b3e4f6bf7f12d9d65e8a8eec73bf73a4..4f6fadb0f3c4c452681f6893f086d0027eed20bb 100644 --- a/umnp/microcontroller/umock/machine/__init__.py +++ b/umnp/microcontroller/umock/machine/__init__.py @@ -84,3 +84,42 @@ class I2C: def writeto_mem(self, i2c_address, address, buffer): pass + + +class UART: + def __init__(self, id: int): ... + + def init( + self, + baudrate=9600, + bits=8, + parity=None, + stop=1, + tx=None, + rx=None, + rts=None, + cts=None, + txbuf=None, + rxbuf=None, + timeout=None, + timeout_chars=None, + invert=None, + flow=None, + ): ... + + def any(self) -> int: ... + + def deinit(self): ... + + def read(self, nbytes=None): ... + + def readinto(self, buf, nbytes=None): ... + + def readline(self): ... + def write(self, buf: bytes): ... + + def sendbread(self): ... + + def flush(self): ... + + def txdone(self): ... diff --git a/umnp/microcontroller/umock/network/__init__.py b/umnp/microcontroller/umock/network/__init__.py index fe11856f5ea5f325ade7f8d592fb952a79605114..9eb57007e7c11d61f4ed0b88c3542700759bd932 100644 --- a/umnp/microcontroller/umock/network/__init__.py +++ b/umnp/microcontroller/umock/network/__init__.py @@ -2,9 +2,9 @@ from umnp.microcontroller.umock.machine import SPI, Pin class WIZNET5K: - def __init__(self, spi: SPI, pin1: Pin, pin2: Pin, mac: bytes): + def __init__(self, spi: SPI, pin1: Pin, pin2: Pin): self._active: bool = False - self._mac = b'\xe6ad\x08Cx' # FIXME + self._mac = b"\xe6ad\x08Cx" # FIXME self._ip = "127.0.0.1" self._gateway = "127.0.0.2" self._netmask = "255.255.255.0" @@ -12,7 +12,7 @@ class WIZNET5K: self._spi = spi self._pin1 = pin1 self._pin2 = pin2 - self._mac = mac + self._mac = None def active(self, active: bool) -> None: self._active = active @@ -20,8 +20,8 @@ class WIZNET5K: def config(self, *args, **kwargs): if len(args) == 1 and args[0] == "mac": return self._mac - if 'mac' in kwargs: - self._mac = kwargs['mac'] + if "mac" in kwargs: + self._mac = kwargs["mac"] def ifconfig(self, *args, **kwargs): if len(args) == 1 and args[0] == "dhcp": diff --git a/umnp/protocol/common/__init__.py b/umnp/protocol/common/__init__.py index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..f7dbdf981942563ede6aa098dcba54b3648bffce 100644 --- a/umnp/protocol/common/__init__.py +++ b/umnp/protocol/common/__init__.py @@ -0,0 +1,3 @@ +UDP_DATA_PORT = 7777 +UDP_METADATA_PORT = 7778 +UDP_CMD_PORT = 7776 diff --git a/umnp/protocol/common/timestamp.py b/umnp/protocol/common/timestamp.py index 575a4e66f6ff8087f0b82d607f7dced7fbdcf473..f9f763546c480c1dec07851edc140a46abc8260e 100644 --- a/umnp/protocol/common/timestamp.py +++ b/umnp/protocol/common/timestamp.py @@ -1,13 +1,40 @@ +import sys +import time + try: import datetime except ImportError: from umnp.protocol.compat.datetime import datetime +if sys.implementation == "micropython": + # noinspection PyUnresolvedReferences + import machine + + +""" + rtc = machine.RTC() + now = rtc.datetime() + year, month, day, hour, minute, second, second_fraction = now +""" + + +def ts_utc_now_second() -> int: + if sys.implementation == "micropython": + rtc = machine.RTC() + # https://docs.micropython.org/en/latest/library/time.html#module-time + # Micropython's time.time() returns the number of seconds, as an integer, since the Epoch + return time.time() + + else: + ts = datetime.datetime.now(tz=datetime.timezone.utc).timestamp() + return int(round(ts)) + + class TimeStamp: def __init__(self, when=None): if when: - self._ts = int(round(datetime.datetime.now(datetime.timezone.utc).timestamp())) + self._ts = ts_utc_now_second() else: self._ts = when @@ -29,5 +56,3 @@ def valid_timestamp(when: TimeStamp | None) -> TimeStamp: raise ValueError("Expected None TimeStamp") return when - - diff --git a/umnp/readme.md b/umnp/readme.md new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/umnp/tools/__init__.py b/umnp/tools/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tools/fs/__init__.py b/umnp/tools/fs/__init__.py similarity index 63% rename from tools/fs/__init__.py rename to umnp/tools/fs/__init__.py index 5fb9dc5512a9b3a2a06b12e2bf52e305182a2635..b1cdf51993f19a305d0c7b6019a3fe43fc781bdb 100644 --- a/tools/fs/__init__.py +++ b/umnp/tools/fs/__init__.py @@ -1,30 +1,42 @@ import os -def fs_entries_recurse(current_directory="", total_path=""): +def human_readable_time(timestamp: int): + t = time.localtime(timestamp) + year, month, day = t[0:3] + hour, minute, second = t[3:6] + return f"{year}-{month:02d}-{day:02d} {hour:02d}:{minute:02d}:{second:02d}" + + +def fs_entries_recurse(current_directory="", total_path="/"): files = [] dirs = [] all_entries = [] - for fstats in os.ilistdir(current_directory): + for fstats in os.ilistdir(total_path): fn = fstats[0] ft = fstats[1] - all_entries.append(total_path + "/" + fn) + if total_path.endswith("/"): + target = total_path + fn + else: + target = total_path + "/" + fn + all_entries.append(target) if ft == 0x4000: dirs.append(total_path + "/" + fn) - f, current_directory, a = fs_entries_recurse(fn, total_path + "/" + fn) + f, current_directory, a = fs_entries_recurse(fn, target) files.extend(f) dirs.extend(current_directory) all_entries.extend(a) else: - files.append(total_path + "/" + fn) + files.append(target) return files, dirs, all_entries class FileSystemInformation: def __init__(self): self._size_cache = {} + self._atime_cache = {} self._files = [] self._directories = [] self._all_entries = [] @@ -38,8 +50,7 @@ class FileSystemInformation: self._size_total_kb = None self._longest_path = 0 self.update_file_system_info() - - self._files, self._directories, self._all_entries = fs_entries_recurse() + self._files, self._directories, self._all_entries = fs_entries_recurse("") for fn in self._all_entries: self._longest_path = max(self._longest_path, len(fn)) @@ -50,17 +61,22 @@ class FileSystemInformation: self._directories = [] self._all_entries = [] - def get_size(self, path): - if path in self._size_cache: - return self._size_cache[path] + def get_atime(self, path): + stats = os.stat(path) + return stats[7] - mode, inode, device_id, link_count, uid, gid, size, atime, mtime, ctime = os.stat(path) + def get_size(self, path): + # mode, inode, device_id, link_count, uid, gid, size, atime, mtime, ctime + mode, _, _, _, _, _, size, _, _, _ = os.stat(path) b = 0 if mode == 0x8000: b = size self._size_cache[path] = b return b + def is_dir(self, path): + return os.stat(path)[0] == 0x4000 + def update_file_system_info(self): if len(self._vfs_stats) == 0: self._vfs_stats = os.statvfs("/") @@ -77,7 +93,13 @@ class FileSystemInformation: def print_fs_tree(self): for fn in self._all_entries: spacer = " " * (self._longest_path + 2 - len(fn)) + size = self.get_size(fn) + is_dir = self.is_dir(fn) size_spacer = " " * (6 - len(str(size))) + a_time = " " * 19 + if not self.is_dir(fn): + t = self.get_atime(fn) + a_time = human_readable_time(t) - print(f"{fn}{spacer} {size_spacer}{size} bytes") + print(f"{fn}{spacer} {size_spacer}{size} bytes -- {a_time}") diff --git a/umnp/tools/list-files.py b/umnp/tools/list-files.py new file mode 100644 index 0000000000000000000000000000000000000000..e6a390374e45d7091984e8ab217649e48181957e --- /dev/null +++ b/umnp/tools/list-files.py @@ -0,0 +1,118 @@ +import os +import time + + +def human_readable_time(timestamp: int): + t = time.localtime(timestamp) + year, month, day = t[0:3] + hour, minute, second = t[3:6] + return f"{year}-{month:02d}-{day:02d}T{hour:02d}:{minute:02d}:{second:02d}" + + +def fs_entries_recurse(current_directory="", total_path="/"): + files = [] + dirs = [] + all_entries = [] + + for fstats in os.ilistdir(total_path): + fn = fstats[0] + ft = fstats[1] + if total_path.endswith("/"): + target = total_path + fn + else: + target = total_path + "/" + fn + all_entries.append(target) + if ft == 0x4000: + dirs.append(total_path + "/" + fn) + + f, current_directory, a = fs_entries_recurse(fn, target) + files.extend(f) + dirs.extend(current_directory) + all_entries.extend(a) + else: + files.append(target) + return files, dirs, all_entries + + +class FileSystemInformation: + def __init__(self): + self._size_cache = {} + self._atime_cache = {} + self._files = [] + self._directories = [] + self._all_entries = [] + self._vfs_stats = [] + self._usage = None + self._size_free_kb = None + self._blocks_free = None + self._max_fn_len = None + self._blocks_used = None + self._block_size = None + self._size_total_kb = None + self._longest_path = 0 + self.update_file_system_info() + self._files, self._directories, self._all_entries = fs_entries_recurse("") + for fn in self._all_entries: + self._longest_path = max(self._longest_path, len(fn)) + + def invalidate(self): + self._size_cache = {} + self._vfs_stats = [] + self._files = [] + self._directories = [] + self._all_entries = [] + + def get_atime(self, path): + stats = os.stat(path) + return stats[7] + + def get_size(self, path): + # mode, inode, device_id, link_count, uid, gid, size, atime, mtime, ctime + mode, _, _, _, _, _, size, _, _, _ = os.stat(path) + b = 0 + if mode == 0x8000: + b = size + self._size_cache[path] = b + return b + + def is_dir(self, path): + return os.stat(path)[0] == 0x4000 + + def update_file_system_info(self): + if len(self._vfs_stats) == 0: + self._vfs_stats = os.statvfs("/") + + b_size, _, b_count, b_count_free, _, _, _, _, _, max_fn_len = self._vfs_stats + self._usage = 1 - b_count_free / b_count + self._size_free_kb = b_size * b_count_free / 1024 + self._blocks_free = b_count_free + self._blocks_used = b_count - b_count_free + self._block_size = b_size + self._size_total_kb = b_size * b_count / 1024 + self._max_fn_len = max_fn_len + + def print_fs_tree(self): + for fn in self._all_entries: + spacer = " " * (self._longest_path + 2 - len(fn)) + + size = self.get_size(fn) + is_dir = self.is_dir(fn) + size_spacer = " " * (6 - len(str(size))) + a_time = "?" + " " * 18 + if is_dir: + ft = "d" + else: + ft = "f" + if not self.is_dir(fn): + t = self.get_atime(fn) + a_time = human_readable_time(t) + + print(f"{fn}{spacer} {size_spacer}{size} bytes {ft} {a_time}") + + +def main(): + fs = FileSystemInformation() + fs.print_fs_tree() + + +main()