From 9ba8b9f19e8b0f2c962185ff6a761ab76ef4af06 Mon Sep 17 00:00:00 2001 From: Nate Gay Date: Sat, 11 Jan 2025 23:28:40 -0600 Subject: [PATCH] MultiBitFlag -> Counter, ByteArray stub/mock --- .vscode/settings.json | 3 +- lib/pysquared/bitflags/bitflags.py | 42 -------- lib/pysquared/bitflags/multi_bit_flag.py | 38 ------- lib/pysquared/functions.py | 6 +- lib/pysquared/hardware/nvm.py | 20 ---- lib/pysquared/nvm/__init__.py | 3 + lib/pysquared/nvm/bitflags.py | 21 ++++ lib/pysquared/nvm/counter.py | 27 +++++ lib/pysquared/pysquared.py | 70 +------------ main.py | 8 +- mocks/circuitpython/byte_array.py | 24 +++++ stubs/circuitpython/byte_array.py | 45 +++++++++ .../pysquared/bitflags/test_multi_bit_flag.py | 98 ------------------- tests/unit/lib/pysquared/nvm/test_counter.py | 33 +++++++ 14 files changed, 169 insertions(+), 269 deletions(-) delete mode 100755 lib/pysquared/bitflags/bitflags.py delete mode 100644 lib/pysquared/bitflags/multi_bit_flag.py delete mode 100644 lib/pysquared/hardware/nvm.py create mode 100644 lib/pysquared/nvm/__init__.py create mode 100755 lib/pysquared/nvm/bitflags.py create mode 100644 lib/pysquared/nvm/counter.py create mode 100644 mocks/circuitpython/byte_array.py create mode 100644 stubs/circuitpython/byte_array.py delete mode 100644 tests/unit/lib/pysquared/bitflags/test_multi_bit_flag.py create mode 100644 tests/unit/lib/pysquared/nvm/test_counter.py diff --git a/.vscode/settings.json b/.vscode/settings.json index 0fadb543..d162cf77 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,5 +3,6 @@ "tests/unit" ], "python.testing.unittestEnabled": false, - "python.testing.pytestEnabled": true + "python.testing.pytestEnabled": true, + "makefile.configureOnOpen": false } diff --git a/lib/pysquared/bitflags/bitflags.py b/lib/pysquared/bitflags/bitflags.py deleted file mode 100755 index b75b150c..00000000 --- a/lib/pysquared/bitflags/bitflags.py +++ /dev/null @@ -1,42 +0,0 @@ -class bitFlag: - """ - Single bit register WITHIN a byte that can be read or set - values are 'bool' - - Assumes register is read MSB! - - """ - - def __init__(self, register, bit): - self.bit_mask = 1 << (bit % 8) # the bitmask *within* the byte! - self.byte = register - - def __get__(self, obj, objtype=None): - return bool(obj.micro.nvm[self.byte] & self.bit_mask) - - def __set__(self, obj, value): - if value: - obj.micro.nvm[self.byte] |= self.bit_mask - else: - obj.micro.nvm[self.byte] &= ~self.bit_mask - - -class multiByte: - """ - must be whole bytes - MSB - """ - - def __init__(self, num_bytes, lowest_register): - self.maxval = (1 << (8 * num_bytes)) - 1 - self.start = lowest_register - self.stop = lowest_register + num_bytes - self.num_bytes = num_bytes - - def __get__(self, obj, objtype=None): - return int.from_bytes(obj.micro.nvm[self.start : self.stop], "big") - - def __set__(self, obj, value): - if value >= self.maxval: - value = self.maxval - obj.micro.nvm[self.start : self.stop] = value.to_bytes(self.num_bytes, "big") diff --git a/lib/pysquared/bitflags/multi_bit_flag.py b/lib/pysquared/bitflags/multi_bit_flag.py deleted file mode 100644 index 91a6aea6..00000000 --- a/lib/pysquared/bitflags/multi_bit_flag.py +++ /dev/null @@ -1,38 +0,0 @@ -try: - from typing import Any, Callable -except ImportError: - pass - - -class multiBitFlag: - """ - Multi-bit value WITHIN a byte that can be read or set - values are int - - Assumes register is read MSB! - 0x80 = 0b10000000 - bit#:76543210 - """ - - def __init__( - self, - index: int, - bit_length: int, - nvm_reader: Callable[[Any, int], int], - nvm_writer: Callable[[Any, int, int], None], - ): - self._index = index - self._nvm_reader = nvm_reader - self._nvm_writer = nvm_writer - - self._maxval = (1 << bit_length) - 1 - self._bit_mask = self._maxval - - def __get__(self, obj, objtype=None): - return self._nvm_reader(self._index) & self._bit_mask - - def __set__(self, obj, value): - reg = self._nvm_reader(self._index) - reg &= ~self._bit_mask - value = min(reg | value, self._maxval) - self._nvm_writer(self._index, reg | value) diff --git a/lib/pysquared/functions.py b/lib/pysquared/functions.py index e673fd2d..fde36c1f 100755 --- a/lib/pysquared/functions.py +++ b/lib/pysquared/functions.py @@ -133,7 +133,7 @@ def beacon(self) -> None: lora_beacon: str = ( f"{self.callsign} Hello I am {self.cubesatName}! I am: " + str(self.cubesat.power_mode) - + f" UT:{self.cubesat.uptime} BN:{self.cubesat.c_boot} EC:{self.cubesat.c_error_count} " + + f" UT:{self.cubesat.uptime} BN:{self.cubesat.boot_count.get()} EC:{self.cubesat.error_count.get()} " + f"IHBPFJASTMNE! {self.callsign}" ) except Exception as e: @@ -192,12 +192,12 @@ def state_of_health(self) -> None: f"ID:{self.cubesat.current_draw}", f"IC:{self.cubesat.charge_current}", f"UT:{self.cubesat.uptime}", - f"BN:{self.cubesat.c_boot}", + f"BN:{self.cubesat.boot_count.get()}", f"MT:{self.cubesat.micro.cpu.temperature}", f"RT:{self.last_radio_temp()}", f"AT:{self.cubesat.internal_temperature}", f"BT:{self.last_battery_temp}", - f"EC:{self.cubesat.c_error_count}", + f"EC:{self.cubesat.error_count.get()}", f"AB:{int(self.cubesat.burned)}", f"BO:{int(self.cubesat.f_brownout)}", f"FK:{int(self.cubesat.f_fsk)}", diff --git a/lib/pysquared/hardware/nvm.py b/lib/pysquared/hardware/nvm.py deleted file mode 100644 index fc04e5b7..00000000 --- a/lib/pysquared/hardware/nvm.py +++ /dev/null @@ -1,20 +0,0 @@ -""" -Hardware abstraction for the CircuitPython non-volatile memory API -https://docs.circuitpython.org/en/stable/shared-bindings/nvm/index.html -""" - -import microcontroller - - -def reader(index: int | slice) -> int | bytearray: - """ - reader reads from the non-volatile memory - """ - return microcontroller.nvm[index] - - -def writer(index: int | slice, value: int) -> None: - """ - writer writes to the non-volatile memory - """ - microcontroller.nvm[index] = value diff --git a/lib/pysquared/nvm/__init__.py b/lib/pysquared/nvm/__init__.py new file mode 100644 index 00000000..762cffd9 --- /dev/null +++ b/lib/pysquared/nvm/__init__.py @@ -0,0 +1,3 @@ +""" +The NVM package is a collection of functionality that interacts with non-volatile memory +""" diff --git a/lib/pysquared/nvm/bitflags.py b/lib/pysquared/nvm/bitflags.py new file mode 100755 index 00000000..9819b52d --- /dev/null +++ b/lib/pysquared/nvm/bitflags.py @@ -0,0 +1,21 @@ +class bitFlag: + """ + Single bit register WITHIN a byte that can be read or set + values are 'bool' + + Assumes register is read MSB! + + """ + + def __init__(self, register, bit): + self.bit_mask = 1 << (bit % 8) # the bitmask *within* the byte! + self.byte = register + + def __get__(self, obj, objtype=None): + return bool(obj.micro.nvm[self.byte] & self.bit_mask) + + def __set__(self, obj, value): + if value: + obj.micro.nvm[self.byte] |= self.bit_mask + else: + obj.micro.nvm[self.byte] &= ~self.bit_mask diff --git a/lib/pysquared/nvm/counter.py b/lib/pysquared/nvm/counter.py new file mode 100644 index 00000000..8af7d355 --- /dev/null +++ b/lib/pysquared/nvm/counter.py @@ -0,0 +1,27 @@ +try: + from stubs.circuitpython.byte_array import ByteArray +except ImportError: + pass + + +class Counter: + def __init__( + self, + index: int, + datastore: ByteArray, + ) -> None: + self._index = index + self._datastore = datastore + + def get(self) -> int: + """ + get returns the value of the counter + """ + return self._datastore[self._index] + + def increment(self) -> None: + """ + increment increases the counter by one + """ + value: int = (self.get() + 1) & 0xFF # 8-bit counter with rollover + self._datastore[self._index] = value diff --git a/lib/pysquared/pysquared.py b/lib/pysquared/pysquared.py index 67d23452..df2c9ae7 100755 --- a/lib/pysquared/pysquared.py +++ b/lib/pysquared/pysquared.py @@ -30,10 +30,9 @@ import lib.pysquared.rv3028 as rv3028 # Real Time Clock from lib.adafruit_lsm6ds.lsm6dsox import LSM6DSOX # IMU from lib.adafruit_rfm import rfm9x, rfm9xfsk # Radio -from lib.pysquared.bitflags.bitflags import bitFlag -from lib.pysquared.bitflags.multi_bit_flag import multiBitFlag from lib.pysquared.debugcolor import co -from lib.pysquared.hardware import nvm +from lib.pysquared.nvm.bitflags import bitFlag +from lib.pysquared.nvm.counter import Counter try: from typing import Any, OrderedDict, TextIO, Union @@ -45,11 +44,7 @@ # NVM register numbers _BOOTCNT = const(0) -_VBUSRST = const(6) _ERRORCNT = const(7) -_TOUTS = const(9) -_ICHRG = const(11) -_DIST = const(13) _FLAG = const(16) SEND_BUFF: bytearray = bytearray(252) @@ -61,54 +56,8 @@ class Satellite: """ # General NVM counters - c_boot: multiBitFlag = multiBitFlag( - index=_BOOTCNT, - bit_length=8, - nvm_reader=nvm.reader, - nvm_writer=nvm.writer, - ) - c_vbusrst: multiBitFlag = multiBitFlag( - index=_VBUSRST, - bit_length=8, - nvm_reader=nvm.reader, - nvm_writer=nvm.writer, - ) - c_error_count: multiBitFlag = multiBitFlag( - index=_ERRORCNT, - bit_length=8, - nvm_reader=nvm.reader, - nvm_writer=nvm.writer, - ) - c_distance: multiBitFlag = multiBitFlag( - index=_DIST, - bit_length=8, - nvm_reader=nvm.reader, - nvm_writer=nvm.writer, - ) - c_ichrg: multiBitFlag = multiBitFlag( - index=_ICHRG, - bit_length=8, - nvm_reader=nvm.reader, - nvm_writer=nvm.writer, - ) - c_error_count: multiBitFlag = multiBitFlag( - index=_ERRORCNT, - bit_length=8, - nvm_reader=nvm.reader, - nvm_writer=nvm.writer, - ) - c_distance: multiBitFlag = multiBitFlag( - index=_DIST, - bit_length=8, - nvm_reader=nvm.reader, - nvm_writer=nvm.writer, - ) - c_ichrg: multiBitFlag = multiBitFlag( - index=_ICHRG, - bit_length=8, - nvm_reader=nvm.reader, - nvm_writer=nvm.writer, - ) + boot_count: Counter = Counter(index=_BOOTCNT, datastore=microcontroller.nvm) + error_count: Counter = Counter(index=_ERRORCNT, datastore=microcontroller.nvm) # Define NVM flags f_softboot: bitFlag = bitFlag(register=_FLAG, bit=0) @@ -128,9 +77,7 @@ def debug_print(self, statement: Any) -> None: print(co("[pysquared]" + str(statement), "green", "bold")) def error_print(self, statement: Any) -> None: - self.c_error_count: multiBitFlag = ( - self.c_error_count + 1 - ) & 0xFF # Limited to 255 errors + self.error_count.increment() if self.debug: print(co("[pysquared]" + str(statement), "red", "bold")) @@ -203,7 +150,6 @@ def __init__(self) -> None: """ Define the boot time and current time """ - self.c_boot += 1 self.BOOTTIME: int = 1577836800 self.debug_print(f"Boot time: {self.BOOTTIME}s") self.CURRENTTIME: int = self.BOOTTIME @@ -241,12 +187,6 @@ def __init__(self) -> None: ] ) - """ - NVM Parameter Resets - """ - if self.c_boot > 200: - self.c_boot = 0 - if self.f_softboot: self.f_softboot = False diff --git a/main.py b/main.py index 76c3ca22..8edb9e0f 100644 --- a/main.py +++ b/main.py @@ -11,9 +11,13 @@ import time import microcontroller +from micropython import const import lib.pysquared.pysquared as pysquared +_BOOTCNT = const(0) +_ERRORCNT = const(7) + print("=" * 70) print("Hello World!") print("PySquared FC Board Circuit Python Software Version: 2.0.0") @@ -54,8 +58,8 @@ def initial_boot(): # c.watchdog_pet() try: - c.c_boot += 1 # Increment boot number - debug_print("Boot number: " + str(c.c_boot)) + c.boot_count.increment() + debug_print("Boot number: " + str(c.boot_count.get())) debug_print(str(gc.mem_free()) + " Bytes remaining") initial_boot() diff --git a/mocks/circuitpython/byte_array.py b/mocks/circuitpython/byte_array.py new file mode 100644 index 00000000..2cea6ed2 --- /dev/null +++ b/mocks/circuitpython/byte_array.py @@ -0,0 +1,24 @@ +from typing import Union + +from circuitpython_typing import ReadableBuffer + +from stubs.circuitpython.byte_array import ByteArray as ByteArrayStub + + +class ByteArray(ByteArrayStub): + """ + ByteArray is a class that mocks the implementaion of the CircuitPython non-volatile memory API. + """ + + def __init__(self, size: int = 1024) -> None: + self.memory = bytearray(size) + + def __getitem__(self, index: Union[slice, int]) -> Union[bytearray, int]: + if isinstance(index, slice): + return bytearray(self.memory[index]) + return int(self.memory[index]) + + def __setitem__( + self, index: Union[slice, int], value: Union[ReadableBuffer, int] + ) -> None: + self.memory[index] = value diff --git a/stubs/circuitpython/byte_array.py b/stubs/circuitpython/byte_array.py new file mode 100644 index 00000000..8b11e56c --- /dev/null +++ b/stubs/circuitpython/byte_array.py @@ -0,0 +1,45 @@ +""" +Stub for Circuit Python ByteArray class +https://docs.circuitpython.org/en/stable/shared-bindings/nvm/index.html#nvm.ByteArray + +This stub has been contributed to the Adafruit CircuitPython Typing repo and can be removed after it has been approved and merged: +https://github.com/adafruit/Adafruit_CircuitPython_Typing/pull/46/files +""" + +from typing import Union, overload + +from circuitpython_typing import ReadableBuffer +from typing_extensions import Protocol + + +class ByteArray(Protocol): + """ + Presents a stretch of non-volatile memory as a bytearray. + + Non-volatile memory is available as a byte array that persists over reloads and power cycles. Each assignment causes an erase and write cycle so its recommended to assign all values to change at once. + """ + + def __bool__(self) -> bool: ... + + def __len__(self) -> int: + """Return the length. This is used by (len)""" + + @overload + def __getitem__(self, index: slice) -> bytearray: ... + + @overload + def __getitem__(self, index: int) -> int: ... + + def __getitem__(self, index: Union[slice, int]) -> Union[bytearray, int]: + """Returns the value at the given index.""" + + @overload + def __setitem__(self, index: slice, value: ReadableBuffer) -> None: ... + + @overload + def __setitem__(self, index: int, value: int) -> None: ... + + def __setitem__( + self, index: Union[slice, int], value: Union[ReadableBuffer, int] + ) -> None: + """Set the value at the given index.""" diff --git a/tests/unit/lib/pysquared/bitflags/test_multi_bit_flag.py b/tests/unit/lib/pysquared/bitflags/test_multi_bit_flag.py deleted file mode 100644 index d8af61c2..00000000 --- a/tests/unit/lib/pysquared/bitflags/test_multi_bit_flag.py +++ /dev/null @@ -1,98 +0,0 @@ -from collections import defaultdict -from typing import Any, Callable, Tuple - -import pytest - -import lib.pysquared.bitflags.multi_bit_flag as multi_bit_flag - - -def mock_nvm() -> Tuple[Callable[[Any, int], int], Callable[[Any, int, int], None]]: - data = defaultdict(int) - - def reader(index: int) -> int: - return data[index] - - def writer(index: int, value: int) -> None: - data[index] = value - - return reader, writer - - -@pytest.mark.parametrize( - "bit_length, initial_value, expected_value", - [ - (4, 0, 0), # minimum 4 bit value - (4, 15, 15), # maximum 4 bit value - (4, 999, 15), # value greater than 4 bits - (4, -4, 12), # value less than 4 bits - (8, 0, 0), # minimum 8 bit value - (8, 255, 255), # maximum 8 bit value - (8, 999, 255), # value greater than 8 bits - (8, -300, 212), # value less than 8 bits - ], -) -def test_multi_bit_flag_bounds(bit_length, initial_value, expected_value): - """ - Test that the multiBitFlag class correctly handles values that are inside and outside the bounds of its bit length - """ - index = 0 - - class MultiBitFlagTester: - mock_nvm_reader, mock_nvm_writer = mock_nvm() - - mbf = multi_bit_flag.multiBitFlag( - index, - bit_length, - mock_nvm_reader, - mock_nvm_writer, - ) - - c = MultiBitFlagTester() - c.mbf = initial_value - assert c.mbf == expected_value - - -def test_arithmetic(): - index, bit_length = 0, 8 - - class MultiBitFlagTester: - mock_nvm_reader, mock_nvm_writer = mock_nvm() - - mbf = multi_bit_flag.multiBitFlag( - index, - bit_length, - mock_nvm_reader, - mock_nvm_writer, - ) - - c = MultiBitFlagTester() - c.mbf = 1 - c.mbf += 2 - assert c.mbf == 3 - - -def test_writing_to_multiple_nvms(): - bit_length, index1, index2 = 8, 0, 1 - - class MultiBitFlagTester: - mock_nvm_reader, mock_nvm_writer = mock_nvm() - - mbf1 = multi_bit_flag.multiBitFlag( - index1, - bit_length, - mock_nvm_reader, - mock_nvm_writer, - ) - - mbf2 = multi_bit_flag.multiBitFlag( - index2, - bit_length, - mock_nvm_reader, - mock_nvm_writer, - ) - - c = MultiBitFlagTester() - c.mbf1 = 1 - c.mbf2 = 2 - assert c.mbf1 == 1 - assert c.mbf2 == 2 diff --git a/tests/unit/lib/pysquared/nvm/test_counter.py b/tests/unit/lib/pysquared/nvm/test_counter.py new file mode 100644 index 00000000..ed306e30 --- /dev/null +++ b/tests/unit/lib/pysquared/nvm/test_counter.py @@ -0,0 +1,33 @@ +import lib.pysquared.nvm.counter as counter +from mocks.circuitpython.byte_array import ByteArray + + +def test_counter_bounds(): + """ + Test that the counter class correctly handles values that are inside and outside the bounds of its bit length + """ + datastore = ByteArray(size=1) + + index = 0 + count = counter.Counter(index, datastore) + assert count.get() == 0 + + count.increment() + assert count.get() == 1 + + datastore[index] = 255 + assert count.get() == 255 + + count.increment() + assert count.get() == 0 + + +def test_writing_to_multiple_counters_in_same_datastore(): + datastore = ByteArray(size=2) + + count_1 = counter.Counter(0, datastore) + count_2 = counter.Counter(1, datastore) + + count_2.increment() + assert count_1.get() == 0 + assert count_2.get() == 1