diff --git a/misc/draw_memory_map.py b/misc/draw_memory_map.py index b676542..37a3513 100644 --- a/misc/draw_memory_map.py +++ b/misc/draw_memory_map.py @@ -4,7 +4,7 @@ from typing import Iterable, List from table_drawing import table -from srnemqtt import parse_packet +from srnemqtt.protocol import parse_packet def memory_table( diff --git a/misc/test_bleuart.py b/misc/test_bleuart.py index 028ebe0..1f28414 100644 --- a/misc/test_bleuart.py +++ b/misc/test_bleuart.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- -from srnemqtt import MAC, construct_request, write -from srnemqtt.feasycom_ble import BTLEUart +from srnemqtt.constants import MAC +from srnemqtt.lib.feasycom_ble import BTLEUart +from srnemqtt.protocol import construct_request, write with BTLEUart(MAC, timeout=1) as x: diff --git a/srnemqtt/__init__.py b/srnemqtt/__init__.py old mode 100755 new mode 100644 index 8dfff7c..e69de29 --- a/srnemqtt/__init__.py +++ b/srnemqtt/__init__.py @@ -1,362 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -import datetime -import struct -import sys -import time -from io import RawIOBase -from typing import Callable, Collection, Optional - -from libscrc import modbus - -from .feasycom_ble import BTLEUart -from .solar_types import DATA_BATTERY_STATE, HISTORICAL_DATA, DataItem - -MAC = "DC:0D:30:9C:61:BA" - -# write_service = "0000ffd0-0000-1000-8000-00805f9b34fb" -# read_service = "0000fff0-0000-1000-8000-00805f9b34fb" - -ACTION_READ = 0x03 -ACTION_WRITE = 0x03 - -POSSIBLE_MARKER = (0x01, 0xFD, 0xFE, 0xFF) - -# get(255, 12, 2) -# "ff 03 00 0c 00 02" -CMD_GET_1 = b"\xff\x03\x00\x0c\x00\x02" -# > ff 03 04 20 20 20 20 - -# get(255, 12, 8) -# ff 03 00 0c 00 08 -CMD_GET_MODEL = b"\xff\x03\x00\x0c\x00\x08" -# > ff 03 10 20 20 20 20 4d 4c 32 34 32 30 20 20 20 20 20 20 -# Device SKU: ML2420 - -# get(255, 20, 4) -# ff 03 00 14 00 04 -CMD_GET_VERSION = b"\xff\x03\x00\x14\x00\x04" -# > ff 03 08 00 04 02 00 02 00 00 03 -# CC ?? 11 22 33 ?? 44 55 66 -# Version: 4.2.0 - -# get(255, 24, 3) -# ff 03 00 18 00 03 -CMD_GET_SERIAL = b"\xff\x03\x00\x18\x00\x03" -# > ff 03 06 3c 13 02 67 00 01 -# CC 11 22 33 33 ?? ?? -# SN: 60-19-0615 - -# get(255, 256, 7) -# ff 03 01 00 00 07 -CMD_GET_BATTERY_STATE = b"\xff\x03\x01\x00\x00\x07" -# > ff 03 0e 00 48 00 7e 00 1d 0e 0d 00 7e 00 1c 00 03 -# CC 11 11 22 22 33 33 44 55 66 66 77 77 88 88 -# 1: Battery charge: 72 % -# 2: Battery voltage: 12.6 V -# 3: Battery current: 0.29 A -# 4: Internal temperature? -# 5: External temperature probe for battery signet 8bit: 13 degC -# 6: Load voltage: 12.6 V -# 7: Load current: 0.28 A -# 8: Load power: 3 W - -# get(255, 263, 4) -# ff 03 01 07 00 04 -CMD_GET_PANEL_STATUS = b"\xff\x03\x01\x07\x00\x04" -# > ff 03 08 00 c8 00 14 00 04 00 01 -# CC 11 11 22 22 33 33 ?? ?? -# 1: Panel voltage: 20.0 V -# 2: Panel current: 0.20 A -# 3: Panel power: 4 W -# Charging status? - -# set(255, 266, 1 or 0) -# ff 06 01 0a 00 01 -CMD_ENABLE_LOAD = b"\xff\x06\x01\x0a\x00\x01" -CMD_DISABLE_LOAD = b"\xff\x06\x01\x0a\x00\x00" -REG_LOAD_ENABLE = 0x010A - -# get(255, 267, 21) -# ff 03 01 0b 00 15 -CMD_GET_LOAD_PARAMETERS = b"\xff\x03\x01\x0b\x00\x15" -# > ff 03 2a 00 7c 00 7f 00 51 00 20 00 0a 00 03 00 00 00 00 00 -# > 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 -# > 00 00 00 00 00 - -# get(255, 288, 3) -# ff 03 01 20 00 03 -CMD_GET_2 = b"\xff\x03\x01\x20\x00\x03" -# > ff 03 06 80 02 00 00 00 00 -# CC 11 22 33 33 33 33 -# 1: boolean flag?: 1 -# 2: ?: 2 -# 3: ?: 0 - -# get(255, 57345, 33) -# ff 03 e0 01 00 21 -CMD_GET_BATTERY_PARAMETERS = b"\xff\x03\xe0\x01\x00\x21" -# > ff 03 42 07 d0 00 c8 ff 0c 00 02 00 a0 00 9b 00 92 00 90 00 -# > 8a 00 84 00 7e 00 78 00 6f 00 6a 64 32 00 05 00 78 00 78 00 -# > 1e 00 03 00 41 00 a3 00 4b 00 a3 00 00 00 00 00 00 00 00 00 -# > 0f 00 05 00 05 00 04 01 00 -# 33 * uint16 - -# get(1, 61440, 10) -# 01 03 f0 00 00 0a -CMD_GET_HISTORICAL_TODAY = b"\x01\x03\xf0\x00\x00\x0a" -CMD_GET_HISTORICAL_YESTERDAY = b"\x01\x03\xf0\x01\x00\x0a" -CMD_GET_HISTORICAL_D2 = b"\x01\x03\xf0\x02\x00\x0a" -CMD_GET_HISTORICAL_D3 = b"\x01\x03\xf0\x03\x00\x0a" - -# ,- battery_min_voltage -# | ,- battery_max_voltage -# | | ,- ?1 max charge %? -# | | | ,- ?2 -# | | | | ,- charge_max_power -# | | | | | ,- discharge_max_power -# | | | | | | ,- charge_amp_hour -# | | | | | | | ,- discharge_amp_hour -# | | | | | | | | ,- production_power -# | | | | | | | | | ,- consumption_power -# _|___ _|___ _|___ _|___ _|___ _|___ _|___ _|___ _|___ _|___ -# > 01 03 14 00 7c 00 7f 00 51 00 20 00 0a 00 03 00 00 00 00 00 00 00 00 -# > 01 03 14 00 7c 00 7f 00 53 00 20 00 0a 00 03 00 00 00 00 00 00 00 00 -# battery_min_voltage = 12.4 V -# battery_max_voltage = 12.7 V -# ?1 = 83 % ? -# ?2 = -# charge_max_power = 10 W -# discharge_max_power = 3 W -# charge_amp_hour = 0 Ah -# discharge_amp_hour = 0 Ah -# production_power = 0 Wh -# consumption_power = 0 Wh - -# ff 78 00 00 00 01 -CMD_ = b"\xff\x78\x00\x00\x00\x01" - -# CMD_GET_BATTERY_STATE = b'\xff\x03\x01\x00\x00\x07' -# > ff 03 0e 00 48 00 7e 00 1d 0e 0d 00 7e 00 1c 00 03 -# CC 11 11 22 22 33 33 44 55 66 66 77 77 88 88 -# 1: Battery charge: 72 % -# 2: Battery voltage: 12.6 V -# 3: Battery current: 0.29 A -# 4: Internal temperature? -# 5: External temperature probe for battery signed 8bit: 13 degC -# 6: Load voltage: 12.6 V -# 7: Load current: 0.28 A -# 8: Load power: 3 W - -# CMD_GET_PANEL_STATUS = b'\xff\x03\x01\x07\x00\x04' -# > ff 03 08 00 c8 00 14 00 04 00 01 -# CC 11 11 22 22 33 33 ?? ?? -# > ff 03 08 00 00 00 00 00 00 00 00 -# 1: Panel voltage: 20.0 V -# 2: Panel current: 0.20 A -# 3: Panel power: 4 W -# ?: load_enabled - - -# Only factor of 1000 -SI_PREFIXES_LARGE = "kMGTPEZY" -SI_PREFIXES_SMALL = "mµnpfazy" - - -def humanize_number(data, unit: str = ""): - counter = 0 - - while data >= 1000: - data /= 1000 - counter += 1 - if counter >= len(SI_PREFIXES_LARGE): - break - - while data < 1: - data *= 1000 - counter -= 1 - if abs(counter) >= len(SI_PREFIXES_SMALL): - break - - if not counter: - prefix = "" - elif counter > 0: - prefix = SI_PREFIXES_LARGE[counter - 1] - elif counter < 0: - prefix = SI_PREFIXES_SMALL[abs(counter) - 1] - - return f"{data:.3g} {prefix}{unit}" - - -def parse(data: bytes, items: Collection[DataItem], offset: int = 0) -> dict: - pos = offset - res = {} - - for i in items: - res[i.name] = i.transform(struct.unpack_from(i.st_format, data, offset=pos)[0]) - pos += i.st_size - - return res - - -# GET_BATTERY_STATE -def parse_battery_state(data: bytes) -> dict: - return parse(data, DATA_BATTERY_STATE) - - -def parse_historical_entry(data: bytes) -> dict: - res = parse(data, HISTORICAL_DATA[:10]) - - res_datalen = sum([x.st_size for x in HISTORICAL_DATA[:10]]) - - if len(data) > res_datalen: - res.update(parse(data, HISTORICAL_DATA[10:], offset=res_datalen)) - - return res - - -def write(fh, data): - bdata = bytes(data) - crc = modbus(bdata) - bcrc = bytes([crc & 0xFF, (crc & 0xFF00) >> 8]) - fh.write(data + bcrc) - - -def construct_request(address, words=1, action=ACTION_READ, marker=0xFF): - assert marker in POSSIBLE_MARKER, f"marker should be one of {POSSIBLE_MARKER}" - return struct.pack("!BBHH", marker, action, address, words) - - -def log(*message: object, **kwargs): - print(datetime.datetime.utcnow().isoformat(" "), *message, **kwargs) - sys.stdout.flush() - - -def parse_packet(data): - tag, operation, size = struct.unpack_from("BBB", data) - _unpacked = struct.unpack_from(f"<{size}BH", data, offset=3) - crc = _unpacked[-1] - payload = _unpacked[:-1] - calculated_crc = modbus(bytes([tag, operation, size, *payload])) - - if crc != calculated_crc: - e = ValueError(f"CRC missmatch: expected {crc:04X}, got {calculated_crc:04X}.") - e.tag = tag - e.operation = operation - e.size = size - e.payload = payload - e.crc = crc - e.calculated_crc = calculated_crc - raise e - - return payload - - -def discardUntil(fh: RawIOBase, byte: int, timeout=10) -> Optional[int]: - assert byte >= 0 and byte < 256, f"byte: Expected 8bit unsigned int, got {byte}" - - def expand(b: Optional[bytes]): - if b is None: - return b - return b[0] - - start = time.time() - discarded = 0 - read_byte = expand(fh.read(1)) - while read_byte != byte: - - if read_byte is not None: - if not discarded: - log("Discarding", end="") - discarded += 1 - print(f" {read_byte:02X}", end="") - sys.stdout.flush() - - if time.time() - start > timeout: - read_byte = None - break - - read_byte = expand(fh.read(1)) - - if discarded: - print() - sys.stdout.flush() - - return read_byte - - -def readMemory(fh: RawIOBase, address: int, words: int = 1) -> Optional[bytes]: - # log(f"Reading {words} words from 0x{address:04X}") - request = construct_request(address, words=words) - # log("Request:", request) - write(fh, request) - - tag = discardUntil(fh, 0xFF) - if tag is None: - return None - - header = fh.read(2) - if header and len(header) == 2: - operation, size = header - data = fh.read(size) - _crc = fh.read(2) - if data and _crc: - try: - crc = struct.unpack_from(" bool: - if now is None: - now = time.time() - - if (now - self.prev) >= self.interval: - skipped, overshoot = divmod(now - self.prev, self.interval) - skipped -= 1 - if skipped: - log("Skipped:", skipped, overshoot, now - self.prev, self.interval) - self.prev = now - overshoot - return True - - return False - - -def try_read_parse( - dev: BTLEUart, - address: int, - words: int = 1, - parser: Optional[Callable] = None, - attempts=5, -) -> Optional[dict]: - while attempts: - attempts -= 1 - res = readMemory(dev, address, words) - if res: - try: - if parser: - return parser(res) - except struct.error as e: - log(e) - log("0x0100 Unpack error:", len(res), res) - log("Flushed from read buffer; ", dev.read(timeout=0.5)) - else: - log(f"No data read, expected {words*2} bytes (attempts left: {attempts})") - return None diff --git a/srnemqtt/__main__.py b/srnemqtt/__main__.py index 2e1d9ed..70cb669 100755 --- a/srnemqtt/__main__.py +++ b/srnemqtt/__main__.py @@ -7,17 +7,12 @@ from typing import cast from bluepy import btle -from . import ( - MAC, - Periodical, - log, - parse_battery_state, - parse_historical_entry, - try_read_parse, -) from .config import get_config, get_consumers -from .feasycom_ble import BTLEUart +from .constants import MAC +from .lib.feasycom_ble import BTLEUart +from .protocol import parse_battery_state, parse_historical_entry, try_read_parse from .solar_types import DataName +from .util import Periodical, log def main(): diff --git a/srnemqtt/constants.py b/srnemqtt/constants.py new file mode 100644 index 0000000..2a13eac --- /dev/null +++ b/srnemqtt/constants.py @@ -0,0 +1,145 @@ +# -*- coding: utf-8 -*- +MAC = "DC:0D:30:9C:61:BA" + +# write_service = "0000ffd0-0000-1000-8000-00805f9b34fb" +# read_service = "0000fff0-0000-1000-8000-00805f9b34fb" + +ACTION_READ = 0x03 +ACTION_WRITE = 0x03 + +POSSIBLE_MARKER = (0x01, 0xFD, 0xFE, 0xFF) + +# get(255, 12, 2) +# "ff 03 00 0c 00 02" +CMD_GET_1 = b"\xff\x03\x00\x0c\x00\x02" +# > ff 03 04 20 20 20 20 + +# get(255, 12, 8) +# ff 03 00 0c 00 08 +CMD_GET_MODEL = b"\xff\x03\x00\x0c\x00\x08" +# > ff 03 10 20 20 20 20 4d 4c 32 34 32 30 20 20 20 20 20 20 +# Device SKU: ML2420 + +# get(255, 20, 4) +# ff 03 00 14 00 04 +CMD_GET_VERSION = b"\xff\x03\x00\x14\x00\x04" +# > ff 03 08 00 04 02 00 02 00 00 03 +# CC ?? 11 22 33 ?? 44 55 66 +# Version: 4.2.0 + +# get(255, 24, 3) +# ff 03 00 18 00 03 +CMD_GET_SERIAL = b"\xff\x03\x00\x18\x00\x03" +# > ff 03 06 3c 13 02 67 00 01 +# CC 11 22 33 33 ?? ?? +# SN: 60-19-0615 + +# get(255, 256, 7) +# ff 03 01 00 00 07 +CMD_GET_BATTERY_STATE = b"\xff\x03\x01\x00\x00\x07" +# > ff 03 0e 00 48 00 7e 00 1d 0e 0d 00 7e 00 1c 00 03 +# CC 11 11 22 22 33 33 44 55 66 66 77 77 88 88 +# 1: Battery charge: 72 % +# 2: Battery voltage: 12.6 V +# 3: Battery current: 0.29 A +# 4: Internal temperature? +# 5: External temperature probe for battery signet 8bit: 13 degC +# 6: Load voltage: 12.6 V +# 7: Load current: 0.28 A +# 8: Load power: 3 W + +# get(255, 263, 4) +# ff 03 01 07 00 04 +CMD_GET_PANEL_STATUS = b"\xff\x03\x01\x07\x00\x04" +# > ff 03 08 00 c8 00 14 00 04 00 01 +# CC 11 11 22 22 33 33 ?? ?? +# 1: Panel voltage: 20.0 V +# 2: Panel current: 0.20 A +# 3: Panel power: 4 W +# Charging status? + +# set(255, 266, 1 or 0) +# ff 06 01 0a 00 01 +CMD_ENABLE_LOAD = b"\xff\x06\x01\x0a\x00\x01" +CMD_DISABLE_LOAD = b"\xff\x06\x01\x0a\x00\x00" +REG_LOAD_ENABLE = 0x010A + +# get(255, 267, 21) +# ff 03 01 0b 00 15 +CMD_GET_LOAD_PARAMETERS = b"\xff\x03\x01\x0b\x00\x15" +# > ff 03 2a 00 7c 00 7f 00 51 00 20 00 0a 00 03 00 00 00 00 00 +# > 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +# > 00 00 00 00 00 + +# get(255, 288, 3) +# ff 03 01 20 00 03 +CMD_GET_2 = b"\xff\x03\x01\x20\x00\x03" +# > ff 03 06 80 02 00 00 00 00 +# CC 11 22 33 33 33 33 +# 1: boolean flag?: 1 +# 2: ?: 2 +# 3: ?: 0 + +# get(255, 57345, 33) +# ff 03 e0 01 00 21 +CMD_GET_BATTERY_PARAMETERS = b"\xff\x03\xe0\x01\x00\x21" +# > ff 03 42 07 d0 00 c8 ff 0c 00 02 00 a0 00 9b 00 92 00 90 00 +# > 8a 00 84 00 7e 00 78 00 6f 00 6a 64 32 00 05 00 78 00 78 00 +# > 1e 00 03 00 41 00 a3 00 4b 00 a3 00 00 00 00 00 00 00 00 00 +# > 0f 00 05 00 05 00 04 01 00 +# 33 * uint16 + +# get(1, 61440, 10) +# 01 03 f0 00 00 0a +CMD_GET_HISTORICAL_TODAY = b"\x01\x03\xf0\x00\x00\x0a" +CMD_GET_HISTORICAL_YESTERDAY = b"\x01\x03\xf0\x01\x00\x0a" +CMD_GET_HISTORICAL_D2 = b"\x01\x03\xf0\x02\x00\x0a" +CMD_GET_HISTORICAL_D3 = b"\x01\x03\xf0\x03\x00\x0a" + +# ,- battery_min_voltage +# | ,- battery_max_voltage +# | | ,- ?1 max charge %? +# | | | ,- ?2 +# | | | | ,- charge_max_power +# | | | | | ,- discharge_max_power +# | | | | | | ,- charge_amp_hour +# | | | | | | | ,- discharge_amp_hour +# | | | | | | | | ,- production_power +# | | | | | | | | | ,- consumption_power +# _|___ _|___ _|___ _|___ _|___ _|___ _|___ _|___ _|___ _|___ +# > 01 03 14 00 7c 00 7f 00 51 00 20 00 0a 00 03 00 00 00 00 00 00 00 00 +# > 01 03 14 00 7c 00 7f 00 53 00 20 00 0a 00 03 00 00 00 00 00 00 00 00 +# battery_min_voltage = 12.4 V +# battery_max_voltage = 12.7 V +# ?1 = 83 % ? +# ?2 = +# charge_max_power = 10 W +# discharge_max_power = 3 W +# charge_amp_hour = 0 Ah +# discharge_amp_hour = 0 Ah +# production_power = 0 Wh +# consumption_power = 0 Wh + +# ff 78 00 00 00 01 +CMD_ = b"\xff\x78\x00\x00\x00\x01" + +# CMD_GET_BATTERY_STATE = b'\xff\x03\x01\x00\x00\x07' +# > ff 03 0e 00 48 00 7e 00 1d 0e 0d 00 7e 00 1c 00 03 +# CC 11 11 22 22 33 33 44 55 66 66 77 77 88 88 +# 1: Battery charge: 72 % +# 2: Battery voltage: 12.6 V +# 3: Battery current: 0.29 A +# 4: Internal temperature? +# 5: External temperature probe for battery signed 8bit: 13 degC +# 6: Load voltage: 12.6 V +# 7: Load current: 0.28 A +# 8: Load power: 3 W + +# CMD_GET_PANEL_STATUS = b'\xff\x03\x01\x07\x00\x04' +# > ff 03 08 00 c8 00 14 00 04 00 01 +# CC 11 11 22 22 33 33 ?? ?? +# > ff 03 08 00 00 00 00 00 00 00 00 +# 1: Panel voltage: 20.0 V +# 2: Panel current: 0.20 A +# 3: Panel power: 4 W +# ?: load_enabled diff --git a/srnemqtt/lib/__init__.py b/srnemqtt/lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/srnemqtt/feasycom_ble.py b/srnemqtt/lib/feasycom_ble.py similarity index 100% rename from srnemqtt/feasycom_ble.py rename to srnemqtt/lib/feasycom_ble.py diff --git a/srnemqtt/protocol.py b/srnemqtt/protocol.py new file mode 100644 index 0000000..160da0b --- /dev/null +++ b/srnemqtt/protocol.py @@ -0,0 +1,158 @@ +# -*- coding: utf-8 -*- +import struct +import sys +import time +from io import RawIOBase +from typing import Callable, Collection, Optional + +from libscrc import modbus + +from .constants import ACTION_READ, POSSIBLE_MARKER +from .lib.feasycom_ble import BTLEUart +from .solar_types import DATA_BATTERY_STATE, HISTORICAL_DATA, DataItem +from .util import log + + +def write(fh, data): + bdata = bytes(data) + crc = modbus(bdata) + bcrc = bytes([crc & 0xFF, (crc & 0xFF00) >> 8]) + fh.write(data + bcrc) + + +def construct_request(address, words=1, action=ACTION_READ, marker=0xFF): + assert marker in POSSIBLE_MARKER, f"marker should be one of {POSSIBLE_MARKER}" + return struct.pack("!BBHH", marker, action, address, words) + + +def parse(data: bytes, items: Collection[DataItem], offset: int = 0) -> dict: + pos = offset + res = {} + + for i in items: + res[i.name] = i.transform(struct.unpack_from(i.st_format, data, offset=pos)[0]) + pos += i.st_size + + return res + + +# GET_BATTERY_STATE +def parse_battery_state(data: bytes) -> dict: + return parse(data, DATA_BATTERY_STATE) + + +def parse_historical_entry(data: bytes) -> dict: + res = parse(data, HISTORICAL_DATA[:10]) + + res_datalen = sum([x.st_size for x in HISTORICAL_DATA[:10]]) + + if len(data) > res_datalen: + res.update(parse(data, HISTORICAL_DATA[10:], offset=res_datalen)) + + return res + + +def parse_packet(data): + tag, operation, size = struct.unpack_from("BBB", data) + _unpacked = struct.unpack_from(f"<{size}BH", data, offset=3) + crc = _unpacked[-1] + payload = _unpacked[:-1] + calculated_crc = modbus(bytes([tag, operation, size, *payload])) + + if crc != calculated_crc: + e = ValueError(f"CRC missmatch: expected {crc:04X}, got {calculated_crc:04X}.") + e.tag = tag + e.operation = operation + e.size = size + e.payload = payload + e.crc = crc + e.calculated_crc = calculated_crc + raise e + + return payload + + +def discardUntil(fh: RawIOBase, byte: int, timeout=10) -> Optional[int]: + assert byte >= 0 and byte < 256, f"byte: Expected 8bit unsigned int, got {byte}" + + def expand(b: Optional[bytes]): + if b is None: + return b + return b[0] + + start = time.time() + discarded = 0 + read_byte = expand(fh.read(1)) + while read_byte != byte: + + if read_byte is not None: + if not discarded: + log("Discarding", end="") + discarded += 1 + print(f" {read_byte:02X}", end="") + sys.stdout.flush() + + if time.time() - start > timeout: + read_byte = None + break + + read_byte = expand(fh.read(1)) + + if discarded: + print() + sys.stdout.flush() + + return read_byte + + +def readMemory(fh: RawIOBase, address: int, words: int = 1) -> Optional[bytes]: + # log(f"Reading {words} words from 0x{address:04X}") + request = construct_request(address, words=words) + # log("Request:", request) + write(fh, request) + + tag = discardUntil(fh, 0xFF) + if tag is None: + return None + + header = fh.read(2) + if header and len(header) == 2: + operation, size = header + data = fh.read(size) + _crc = fh.read(2) + if data and _crc: + try: + crc = struct.unpack_from(" Optional[dict]: + while attempts: + attempts -= 1 + res = readMemory(dev, address, words) + if res: + try: + if parser: + return parser(res) + except struct.error as e: + log(e) + log("0x0100 Unpack error:", len(res), res) + log("Flushed from read buffer; ", dev.read(timeout=0.5)) + else: + log(f"No data read, expected {words*2} bytes (attempts left: {attempts})") + return None diff --git a/srnemqtt/util.py b/srnemqtt/util.py new file mode 100644 index 0000000..b641e70 --- /dev/null +++ b/srnemqtt/util.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- + +import datetime +import sys +import time +from typing import Optional + +# Only factor of 1000 +SI_PREFIXES_LARGE = "kMGTPEZY" +SI_PREFIXES_SMALL = "mµnpfazy" + + +def humanize_number(data, unit: str = ""): + counter = 0 + + while data >= 1000: + data /= 1000 + counter += 1 + if counter >= len(SI_PREFIXES_LARGE): + break + + while data < 1: + data *= 1000 + counter -= 1 + if abs(counter) >= len(SI_PREFIXES_SMALL): + break + + if not counter: + prefix = "" + elif counter > 0: + prefix = SI_PREFIXES_LARGE[counter - 1] + elif counter < 0: + prefix = SI_PREFIXES_SMALL[abs(counter) - 1] + + return f"{data:.3g} {prefix}{unit}" + + +def log(*message: object, **kwargs): + print(datetime.datetime.utcnow().isoformat(" "), *message, **kwargs) + sys.stdout.flush() + + +class Periodical: + prev: float + interval: float + + def __init__(self, interval: float, start: Optional[float] = None): + self.prev = time.time() - interval if start is None else start + self.interval = interval + + def __call__(self, now: Optional[float] = None) -> bool: + if now is None: + now = time.time() + + if (now - self.prev) >= self.interval: + skipped, overshoot = divmod(now - self.prev, self.interval) + skipped -= 1 + if skipped: + log("Skipped:", skipped, overshoot, now - self.prev, self.interval) + self.prev = now - overshoot + return True + + return False