From 791a7e1b9c8bdce604a6aa3e2d69ad957bc6e6dd Mon Sep 17 00:00:00 2001 From: Clint Lawrence Date: Wed, 29 May 2024 17:35:14 +1000 Subject: [PATCH 01/41] Initial work of untangling VirtualMux and Address Handlers [skip ci] --- src/fixate/core/switching.py | 434 +++++++++++++++++++++++++++++++++++ 1 file changed, 434 insertions(+) create mode 100644 src/fixate/core/switching.py diff --git a/src/fixate/core/switching.py b/src/fixate/core/switching.py new file mode 100644 index 00000000..a4f33308 --- /dev/null +++ b/src/fixate/core/switching.py @@ -0,0 +1,434 @@ +from __future__ import annotations + +import time +from typing import Optional, Callable, Mapping, Sequence +from dataclasses import dataclass + +Signal = str +PinName = str + + +@dataclass(frozen=True) +class PinSetState: + off: set[PinName] + on: set[PinName] + + +AddressHandlerUpdateCallback = Callable[[PinSetState, PinSetState, float, bool], None] + + +class VirtualMux: + pin_list: Sequence[PinName] = [] + default_signal = "" + clearing_time = 0.0 + + def __init__(self, update_pins: Optional[AddressHandlerUpdateCallback] = None): + # The last time this mux changed state. This is used in some jigs + # to enforce a minimum settling time. Perhaps is would be nice to + # deprecate this and add a `settle_at_least()` method? + self.state_update_time = 0.0 # time.time() + + self._update_pins: AddressHandlerUpdateCallback + if update_pins is None: + self._update_pins = self._default_update_pins + else: + self._update_pins = update_pins + + # We force the pin_list to be an ordered sequence, because if the + # mux is defined with a map_tree, we need the ordering. But after + # initialisation, we only need set operations on the pin list, so + # we convert here and keep a reference on the object for future use. + self._pin_set = set(self.pin_list) + + self._state = "" + + self._signal_map: dict[Signal, set[PinName]] = self.map_signals() + + # If it wasn't already defined, define the implicit signal "" which + # can be used for no pins to be set. + if "" not in self._signal_map: + self._signal_map[""] = set() + + def __call__(self, signal_output: Signal, trigger_update: bool = True) -> None: + """ + Convenience to avoid having to type jig.mux..multiplex. + + With this you can just type jig.mux. which is a small, but + useful saving for the most common method call. + """ + self.multiplex(signal_output, trigger_update) + + @staticmethod + def _default_update_pins( + setup: PinSetState, + final: PinSetState, + minimum_change_time: float = 0.0, + trigger_update: bool = True + ) -> None: + """ + Output callback to effect a state change in the mux. + + This is a default implementation which simply prints the planned state change to. + stdout. When instantiated as part of a jig driver, this will end up connected + to an AddressHandler to do the actual pin changes in hardware. + """ + print(setup, final, minimum_change_time, trigger_update) + + def multiplex(self, signal_output, trigger_update=True): + """ + Update the multiplexer state to signal_output. + + The update is a two-step processes. By default, the change happens on + the second step. This can be modified by subclassing and overriding the + _calculate_pins method. + + If trigger_update is true, the output will update immediately. If false, + multiple mux changes can be set and then when trigger_update is finally + set to True all changes will happen at once. + + If the signal_output is different to the previous state, + self.state_update_time is updated to the current time. + """ + if signal_output not in self._signal_map: + name = self.__class__.__name__ + raise ValueError(f"Signal '{signal_output}' not valid for multiplexer '{name}'") + + setup, final = self._calculate_pins(self._state, signal_output) + self._update_pins(setup, final, self.clearing_time, trigger_update) + if signal_output != self._state: + self.state_update_time = time.time() + self._state = signal_output + + def _calculate_pins(self, old_signal, new_signal) -> (PinSetState, PinSetState): + """ + Calculate the pin sets for the two-step state change. + + The two-step state change allows us to implement break-before-make or + make-before-break behaviour. By default, the first state changes + no pins and the second state sets the pins required for new_signal. + + Subclasses can override this method to change the behaviour. + """ + setup = PinSetState(set(), set()) + on_pins = self._signal_map[new_signal] + final = PinSetState(self._pin_set - on_pins, on_pins) + return setup, final + + def defaults(self): + """ + Set the multiplexer to the default state. + """ + self.multiplex(self.default_signal) + + def map_signals(self) -> dict[Signal, set[PinName]]: + """ + Default implementation of the signal mapping + + We need to construct a dictionary mapping signals to a set of pins. + In the case the self.map_list is set, the is pretty trival. + If the mux is defined with self.map_tree we have more work to + do... + + If subclassed, you can use any scheme to create the mapping that + returns a suitable dictionary. + """ + if hasattr(self, "map_tree"): + raise NotImplementedError + elif hasattr(self, "map_list"): + return {sig: set(pins) for sig, *pins in self.map_list} + else: + raise ValueError("VirtualMux subclass must define either map_tree or map_list") + + # try: + # self.map_list + # except AttributeError: + # pass + # else: + # for signal in self.map_list: + # self.map_single(*signal) + # else: + # self._map_tree(map_tree, 0, 0) + + def _map_tree(self, branch, base_offset, base_bits): + """recursively add nested signal lists to the signal map. + branch: is the current sub branch to be added. At the first call + level, this would be initialised with self.map_tree. It can be + any sequence, possibly nested. + + base_offset: should be the integer value of the address + where the branch enters into the top level multiplexer. + + base_bits: At each call level, this represents the number + of less significant address bits that are used to represent + multiplexers higher up the chain. + + example: + This shows 10 signal, routed through a number of multiplexers. + Mux B and Mux B' are distinct, but address of common control + signals. Mux C and Mux B/B' are nested to various levels into + the final multiplexer Mux A. + + The pin_list defines the control signals from least to most significant + The map_tree defines the signals into each multipler. Nesting containers + reflects the nesting of mux's. + __________ + a0-------------------------------| | + ________ | | + a1_b0----------------| |--| Mux A | + a1_b1----------------| Mux B | | 4:1 | + a1_b2----------------| 4:1 | | | + (None)--|_x3__x2_| | | + | | + ________ | | + a2_b0----------------| | | | + _______ | |--| |------ Output + a2_b1_c0--| Mux C |--| Mux B' | | | + a2_b1_c1--| 2:1 | | 4:1 | | | + |___x4__| | | | | + | | | | + a2_b2----------------| | | | + a2_b3----------------| | | | + |_x3__x2_| | | + | | + a3-------------------------------| | + |__x1__x0__| + + class Mux(VirtualMux): + pin_list = ("x0", "x1", "x2", "x3", "x4") + map_tree = ("a0", + (#a1 + "a1_b0", + "a1_b1", + "a1_b2", + None, + ), + (#a2 + "a2_b0", + (#b1 + "a2_b1_c0", + "a2_b1_c1", + ), + "a2_b2", + "a2_b3", + ), + "a3" + ) + + Alternatively: + + class Mux(VirtualMux): + pin_list = ("x0", "x1", "x2", "x3", "x4") + + mux_c = ("a2_b1_c0", "a2_b1_c1") + mux_b1 = ("a1_b0", "a1_b1", "a1_b2", None) + mux_b2 = ("a2_b0", mux_c, "a2_b2", "a2_b3") + + map_tree = ("a0", mux_b1, mux_b2, "a3") + + Final mapping: + addr signal + -------------- + 0 a0 + 1 a1_b0 + 2 a2_b0 + 3 a3 + 4 + 5 a1_b1 + 6 a2_b1_c0 + 7 + 8 + 9 a1_b2 + 10 a2_b2 + 11 + 12 + 13 (None) + 14 a2_b3 + 15 + 16 + 17 + 18 + 19 + 20 + 21 + 22 a2_b1_c1 + 23 + 24 + 25 + 26 + 27 + 28 + 29 + 30 + 31 + + For Multiplexers that depend on separate control pins, try using the shift_nested function to help + with sparse mapping + __________ + a0-------------------------------| | + ________ | | + a1_b0----------------| |--| | + a1_b1----------------| Mux B | | | + a1_b2----------------| 4:1 | | | + (None)--| x3 x2 | | | + |________| | | + | Mux A | + a2-------------------------------| 4:1 | + ________ | | + a3_c0----------------| |--| | + a3_c1----------------| Mux C | | | + a3_c2----------------| 4:1 | | | + (None)--| x5 x4 | | | + |________| | | + |__x1__x0__| + + class Mux(VirtualMux): + pin_list = ("x0", "x1", "x2", "x3", "x4") + + mux_c = ("a3_c0", "a3_c1", "a3_c2", None) + mux_b = ("a1_b0", "a1_b1", "a1_b2", None) + + map_tree = ( + "a0", + mux_b, + shift_nested(mux_c, [2]), # 2 in indicative on how many pins to skip. This case is (x2, x3) from mux_b + "a3") + + """ + for i, signal in enumerate(branch): + current_index = (i * 1 << base_bits) + base_offset + + if isinstance(signal, str): + # Add signal to out mapping + self._check_duplicates(current_index, signal) + self.signal_map[signal] = current_index + elif signal is None: + pass + else: + # We have a nested signal definition, so we recurse. + # number of addr bits needed for the current branch: + current_bits = int(ceil(log(len(branch), 2))) + self._map_tree(signal, current_index, base_bits + current_bits) + + def __repr__(self): + return self.__class__.__name__ + + +class VirtualSwitch(VirtualMux): + """ + A VirtualMux that controls a single pin. + + A virtual switch is a multiplexer with a single pin. The multiplex + function can accept either boolean values or the strings 'TRUE' and + 'FALSE'. The virtual address used to switch can be defined as a list + with the single element (as if it were a multiplexer) or by using the + shorthand which is to define the pin_name attribute as a string. + """ + + pin_name = "" + map_tree = ("FALSE", "TRUE") + + def multiplex(self, signal_output, trigger_update=True): + if signal_output is True: + signal = "TRUE" + elif signal_output is False: + signal = "FALSE" + else: + signal = signal_output + super().multiplex(signal, trigger_update=trigger_update) + + def __init__(self, pin_name=None): + if pin_name is None: + pin_name = self.pin_name + if not self.pin_list: + self.pin_list = [pin_name] + super().__init__() + + +class RelayMatrixMux(VirtualMux): + clearing_time = 0.01 + + def _calculate_pins(self, old_signal, new_signal) -> (PinSetState, PinSetState): + """ + Override of _calculate_pins to implement break-before-make switching. + """ + setup = PinSetState(off=self._pin_set, on=set()) + on_pins = self._signal_map[new_signal] + final = PinSetState(off=self._pin_set - on_pins, on=on_pins) + return setup, final + + +class JigMeta(type): + """ + usage: + Metaclass for Jig Driver + Dynamically adds multiplexers and multiplexer groups to the Jig Class definition + """ + + def __new__(mcs, clsname, bases, dct): + muxes = dct.get("multiplexers", None) + if muxes is not None: + mux_dct = {mux.__class__.__name__: mux for mux in muxes} + dct["mux"] = type("MuxController", (), mux_dct) + return super().__new__(mcs, clsname, bases, dct) + + +class JigDriver(metaclass=JigMeta): + """ + :attribute address_handlers: Iterable of Address Handlers + [,... + ""] + + :attribute multiplexers: Iterable Virtual Muxes + {,... + "} + + :attribute defaults: Iterable of the default pins to set high on driver reset + """ + + multiplexers = () + address_handlers = () + defaults = () + + def __init__(self): + super().__init__() + self.virtual_map = VirtualAddressMap() + for addr_hand in self.address_handlers: + self.virtual_map.install_address_handler(addr_hand) + for mux in self.multiplexers: + self.virtual_map.install_multiplexer(mux) + + def __setitem__(self, key, value): + self.virtual_map.update_pin_by_name(key, value) + + def __getitem__(self, item): + return self.virtual_map[item] + + def active_pins(self): + return self.virtual_map.active_pins() + + def reset(self): + """ + Reset the multiplexers to the default values + Raises exception if failed + :return: None + """ + self.virtual_map.update_defaults() # TODO Test if this is required + for _, mux in self.mux.__dict__.items(): + if isinstance(mux, VirtualMux): + mux.defaults() + + def iterate_all_mux_paths(self): + for _, mux in self.mux.__dict__.items(): + if isinstance(mux, VirtualMux): + yield from self.iterate_mux_paths(mux) + + def iterate_mux_paths(self, mux): + """ + :param mux: Multiplexer as an object + :return: Generator of multiplexer signal paths + """ + for pth in mux.signal_map: + if pth is not None: + mux(pth) + yield "{}: {}".format(mux.__class__.__name__, pth) + mux.defaults() \ No newline at end of file From ed0604f80e01c0213fa5072e523541b5db2702a4 Mon Sep 17 00:00:00 2001 From: Clint Lawrence Date: Thu, 30 May 2024 17:44:00 +1000 Subject: [PATCH 02/41] Implement map_tree and start on AddressHandler --- src/fixate/core/switching.py | 367 +++++++++++++++++++++++++++-------- test/core/test_switching.py | 111 +++++++++++ 2 files changed, 400 insertions(+), 78 deletions(-) create mode 100644 test/core/test_switching.py diff --git a/src/fixate/core/switching.py b/src/fixate/core/switching.py index a4f33308..0bf31915 100644 --- a/src/fixate/core/switching.py +++ b/src/fixate/core/switching.py @@ -1,34 +1,41 @@ from __future__ import annotations +import itertools import time -from typing import Optional, Callable, Mapping, Sequence +from typing import Optional, Callable, Set, Sequence, TypeVar, Generator, Union, Collection, Dict from dataclasses import dataclass Signal = str PinName = str +PinList = Sequence[PinName] +PinSet = Set[PinName] +SignalMap = Dict[Signal, PinSet] @dataclass(frozen=True) class PinSetState: - off: set[PinName] - on: set[PinName] + off: PinSet + on: PinSet -AddressHandlerUpdateCallback = Callable[[PinSetState, PinSetState, float, bool], None] +PinUpdateCallback = Callable[[PinSetState, PinSetState, float, bool], None] class VirtualMux: - pin_list: Sequence[PinName] = [] - default_signal = "" - clearing_time = 0.0 + pin_list: PinList = () + default_signal: Signal = "" + clearing_time: float = 0.0 - def __init__(self, update_pins: Optional[AddressHandlerUpdateCallback] = None): + ########################################################################### + # This methods are the public API for the class + + def __init__(self, update_pins: Optional[PinUpdateCallback] = None): # The last time this mux changed state. This is used in some jigs # to enforce a minimum settling time. Perhaps is would be nice to # deprecate this and add a `settle_at_least()` method? self.state_update_time = 0.0 # time.time() - self._update_pins: AddressHandlerUpdateCallback + self._update_pins: PinUpdateCallback if update_pins is None: self._update_pins = self._default_update_pins else: @@ -42,7 +49,7 @@ def __init__(self, update_pins: Optional[AddressHandlerUpdateCallback] = None): self._state = "" - self._signal_map: dict[Signal, set[PinName]] = self.map_signals() + self._signal_map: SignalMap = self._map_signals() # If it wasn't already defined, define the implicit signal "" which # can be used for no pins to be set. @@ -58,23 +65,7 @@ def __call__(self, signal_output: Signal, trigger_update: bool = True) -> None: """ self.multiplex(signal_output, trigger_update) - @staticmethod - def _default_update_pins( - setup: PinSetState, - final: PinSetState, - minimum_change_time: float = 0.0, - trigger_update: bool = True - ) -> None: - """ - Output callback to effect a state change in the mux. - - This is a default implementation which simply prints the planned state change to. - stdout. When instantiated as part of a jig driver, this will end up connected - to an AddressHandler to do the actual pin changes in hardware. - """ - print(setup, final, minimum_change_time, trigger_update) - - def multiplex(self, signal_output, trigger_update=True): + def multiplex(self, signal_output: Signal, trigger_update=True): """ Update the multiplexer state to signal_output. @@ -88,6 +79,9 @@ def multiplex(self, signal_output, trigger_update=True): If the signal_output is different to the previous state, self.state_update_time is updated to the current time. + + In general, subclasses should not override. (VirtualSwitch does, but then + delegate the real work to this method to ensure consistent behaviour.) """ if signal_output not in self._signal_map: name = self.__class__.__name__ @@ -99,7 +93,16 @@ def multiplex(self, signal_output, trigger_update=True): self.state_update_time = time.time() self._state = signal_output - def _calculate_pins(self, old_signal, new_signal) -> (PinSetState, PinSetState): + def defaults(self): + """ + Set the multiplexer to the default state. + """ + self.multiplex(self.default_signal) + + ########################################################################### + # The following methods are potential candidates to override in a subclass + + def _calculate_pins(self, old_signal: Signal, new_signal: Signal) -> tuple[PinSetState, PinSetState]: """ Calculate the pin sets for the two-step state change. @@ -107,20 +110,21 @@ def _calculate_pins(self, old_signal, new_signal) -> (PinSetState, PinSetState): make-before-break behaviour. By default, the first state changes no pins and the second state sets the pins required for new_signal. - Subclasses can override this method to change the behaviour. + Subclasses can override this method to change the behaviour. It is + marked as private to discourage use, but it is intended to be subclassed. + For example, RelayMatrix overrides this to implement break-before-make + switching. """ setup = PinSetState(set(), set()) on_pins = self._signal_map[new_signal] final = PinSetState(self._pin_set - on_pins, on_pins) return setup, final - def defaults(self): - """ - Set the multiplexer to the default state. - """ - self.multiplex(self.default_signal) + ########################################################################### + # The following methods are intended as implementation detail and + # subclasses should avoid overriding. - def map_signals(self) -> dict[Signal, set[PinName]]: + def _map_signals(self) -> SignalMap: """ Default implementation of the signal mapping @@ -129,38 +133,29 @@ def map_signals(self) -> dict[Signal, set[PinName]]: If the mux is defined with self.map_tree we have more work to do... - If subclassed, you can use any scheme to create the mapping that - returns a suitable dictionary. + Avoid subclassing. Consider creating helper functions to build + map_tree or map_list. Although """ if hasattr(self, "map_tree"): - raise NotImplementedError + return self._map_tree(self.map_tree, self.pin_list, fixed_pins=set()) elif hasattr(self, "map_list"): return {sig: set(pins) for sig, *pins in self.map_list} else: raise ValueError("VirtualMux subclass must define either map_tree or map_list") - # try: - # self.map_list - # except AttributeError: - # pass - # else: - # for signal in self.map_list: - # self.map_single(*signal) - # else: - # self._map_tree(map_tree, 0, 0) - - def _map_tree(self, branch, base_offset, base_bits): + def _map_tree(self, tree, pins: PinList, fixed_pins: PinSet) -> SignalMap: """recursively add nested signal lists to the signal map. - branch: is the current sub branch to be added. At the first call + tree: is the current sub-branch to be added. At the first call level, this would be initialised with self.map_tree. It can be any sequence, possibly nested. - base_offset: should be the integer value of the address - where the branch enters into the top level multiplexer. + pins: The list of pins, taken as LSB to MSB that are assigned + to the signals in order. - base_bits: At each call level, this represents the number - of less significant address bits that are used to represent - multiplexers higher up the chain. + fixed_pins: At each call level, this represents the pins that + must be set for each signal in at this level. In the example + below, these are the bits for a given input to Mux A, when + mapping all the nested Mux B signals. example: This shows 10 signal, routed through a number of multiplexers. @@ -169,7 +164,7 @@ def _map_tree(self, branch, base_offset, base_bits): the final multiplexer Mux A. The pin_list defines the control signals from least to most significant - The map_tree defines the signals into each multipler. Nesting containers + The map_tree defines the signals into each multiplexer. Nesting containers reflects the nesting of mux's. __________ a0-------------------------------| | @@ -294,24 +289,47 @@ class Mux(VirtualMux): "a3") """ - for i, signal in enumerate(branch): - current_index = (i * 1 << base_bits) + base_offset - - if isinstance(signal, str): - # Add signal to out mapping - self._check_duplicates(current_index, signal) - self.signal_map[signal] = current_index - elif signal is None: - pass + signal_map: SignalMap = dict() + + bits_at_this_level = (len(tree) - 1).bit_length() + pins_at_this_level = pins[:bits_at_this_level] + + for signal_or_tree, pins_for_signal in zip(tree, generate_bit_sets(pins_at_this_level)): + if signal_or_tree is None: + continue + if isinstance(signal_or_tree, Signal): + signal_map[signal_or_tree] = set(pins_for_signal) | fixed_pins else: - # We have a nested signal definition, so we recurse. - # number of addr bits needed for the current branch: - current_bits = int(ceil(log(len(branch), 2))) - self._map_tree(signal, current_index, base_bits + current_bits) + signal_map.update(self._map_tree( + tree=signal_or_tree, + pins=pins[bits_at_this_level:], + fixed_pins=set(pins_for_signal) | fixed_pins, + )) + + return signal_map def __repr__(self): return self.__class__.__name__ + @staticmethod + def _default_update_pins( + setup: PinSetState, + final: PinSetState, + minimum_change_time: float = 0.0, + trigger_update: bool = True + ) -> None: + """ + Output callback to effect a state change in the mux. + + This is a default implementation which simply prints the planned state change to. + stdout. When instantiated as part of a jig driver, this will end up connected + to an AddressHandler to do the actual pin changes in hardware. + + In general, this method shouldn't be overridden in a subclass. An alternative + can be provided to __init__. + """ + print(setup, final, minimum_change_time, trigger_update) + class VirtualSwitch(VirtualMux): """ @@ -324,10 +342,10 @@ class VirtualSwitch(VirtualMux): shorthand which is to define the pin_name attribute as a string. """ - pin_name = "" + pin_name: PinName = "" map_tree = ("FALSE", "TRUE") - def multiplex(self, signal_output, trigger_update=True): + def multiplex(self, signal_output: Union[Signal, bool], trigger_update: bool = True): if signal_output is True: signal = "TRUE" elif signal_output is False: @@ -336,18 +354,19 @@ def multiplex(self, signal_output, trigger_update=True): signal = signal_output super().multiplex(signal, trigger_update=trigger_update) - def __init__(self, pin_name=None): - if pin_name is None: - pin_name = self.pin_name + def __init__( + self, + update_pins: Optional[PinUpdateCallback] = None, + ): if not self.pin_list: - self.pin_list = [pin_name] - super().__init__() + self.pin_list = [self.pin_name] + super().__init__(update_pins) class RelayMatrixMux(VirtualMux): clearing_time = 0.01 - def _calculate_pins(self, old_signal, new_signal) -> (PinSetState, PinSetState): + def _calculate_pins(self, old_signal: Signal, new_signal: Signal) -> tuple[PinSetState, PinSetState]: """ Override of _calculate_pins to implement break-before-make switching. """ @@ -357,6 +376,184 @@ def _calculate_pins(self, old_signal, new_signal) -> (PinSetState, PinSetState): return setup, final +class AddressHandler: + """ + Controls the IO for a set of pins. + + For output, it is assumed that all the pins under the of a given + AddressHandler are updated in one operation. + + This base class doesn't give you much. You need to create a subclass + that implement a set_pins() method. + + :param pin_list: Sequence of pins + :param pin_defaults: Sequence of pins (type string subset of pin_list) that should default to high logic on reset + """ + + pin_list: Sequence[PinName] = () + pin_defaults = () + + def set_pins(self, pins: Collection[PinName]): + raise NotImplementedError + + +def bit_generator() -> Generator[int, None, None]: + """b1, b10, b100, b1000, ...""" + return (1 << counter for counter in itertools.count()) + + +class PinValueAddressHandler(AddressHandler): + """Maps pins to bit values then combines the bit values for an update""" + + def __init__(self): + super().__init__() + self._pin_lookup = {pin: bit for pin, bit in zip(self.pin_list, bit_generator())} + + def set_pins(self, pins: Collection[PinName]): + value = sum(self._pin_lookup[pin] for pin in pins) + self._update_output(value) + + def _update_output(self, value: int): + # perhaps it's easy to compose by passing the output + # function into __init__, like what we did with the VirtualMux? + bits = len(self.pin_list) + print(f"0b{value:0{bits}b}") + + +class FTDIAddressHandler(PinValueAddressHandler): + """Lets define this for the common case?""" + + + +class VirtualAddressMap: + """ + The supervisor loops through the attached virtual multiplexers each time a mux update is triggered. + """ + + def __init__(self): + pass + # self.address_handlers = [] + # self.virtual_pin_list = [] + # self._virtual_pin_values = 0b0 + # self._virtual_pin_values_active = 0b0 + # self._virtual_pin_values_clear = 0b0 + # self.mux_assigned_pins = {} + # self._clearing_time = 0 + + # Not used in scripts + # @property + # def pin_values(self): + # return list( + # zip( + # self.virtual_pin_list, + # bits( + # self._virtual_pin_values_active, + # num_bits=len(self.virtual_pin_list), + # order="LSB", + # ), + # ) + # ) + + def active_pins(self): + pass + # Used in J474 Scripts:8:as2081_validation_tests.py + # return [ + # ( + # self.virtual_pin_list[pin], + # self.mux_assigned_pins[self.virtual_pin_list[pin]], + # ) + # for pin, value in enumerate( + # bits( + # self._virtual_pin_values_active, + # num_bits=len(self.virtual_pin_list), + # order="LSB", + # ) + # ) + # if value + # ] + + def install_address_handler(self, handler): + ... + + def install_multiplexer(self, mux): + ... + + # one reference el relays: 35:elv_jig.py: 1010: self.virtual_map.update_defaults() + # also used below in the jig driver + def update_defaults(self): + """ + Writes the initialisation values to the address handlers as the default values set in the handlers + """ + # pin_values = [] + # self._virtual_pin_values = 0 + # for _, handler in self.address_handlers: + # pin_values.extend(handler.defaults()) + # self.update_pins_by_name(pin_values) + + # def update_output(self): + # """ + # Iterates through the address_handlers and send a bit shifted and masked value of the _virtual_pin_values + # relevant to the address handlers update function. + # :return: + # """ + # start_addr = 0x00 + # for addr, handler in self.address_handlers: + # shifted = self._virtual_pin_values >> start_addr + # mask = (1 << (addr - start_addr)) - 1 + # handler.update_output(shifted & mask) + # start_addr = addr + # + # def update_clearing_output(self): + # start_addr = 0x00 + # for addr, handler in self.address_handlers: + # shifted = self._virtual_pin_values_clear >> start_addr + # mask = (1 << (addr - start_addr)) - 1 + # handler.update_output(shifted & mask) + # start_addr = addr + + ####################### + # I'm ignoring input for now... + ####################### + # def update_input(self): + # """ + # Iterates through the address_handlers and reads the values back to update the pin values for the digital inputs + # :return: + # """ + # start_addr = 0x00 + # for addr, handler in self.address_handlers: + # values = handler.update_input() + # if values is not None: # Handler can return valid input values + # pin_values = [] + # for index, b in enumerate( + # bits(values, num_bits=len(handler.pin_list), order="LSB") + # ): + # pin_values.append((index + start_addr, b)) + # self.update_pin_values(pin_values, trigger_update=False) + # start_addr = addr + + # These were only used internally, as far as I can tell... + #def update_pin_values(self, values, trigger_update=True): + + # def update_clearing_pin_values(self, values, clearing_time): + + # used in a few scripts + def update_pin_by_name(self, name, value, trigger_update=True): + pass + + # not used in any scripts + def update_pins_by_name(self, pins, trigger_update=True): + pass + + def __getitem__(self, item): + pass + # self.update_input() + # return bool((1 << self.virtual_pin_list.index(item)) & self._virtual_pin_values) + + def __setitem__(self, key, value): + pass + # index = self.virtual_pin_list.index(key) + # self.update_pin_values([(index, value)]) + class JigMeta(type): """ usage: @@ -431,4 +628,18 @@ def iterate_mux_paths(self, mux): if pth is not None: mux(pth) yield "{}: {}".format(mux.__class__.__name__, pth) - mux.defaults() \ No newline at end of file + mux.defaults() + + +T = TypeVar("T") + + +def generate_bit_sets(bits: Sequence[T]) -> Generator[set[T], None, None]: + """ + Create subsets of bits, representing bits of a list of integers + + This is easier to explain with an example + list(generate_bit_set(["x0", "x1"])) -> [set(), {'x0'}, {'x1'}, {'x0', 'x1'}] + """ + int_list = range(1 << len(bits)) if len(bits) != 0 else range(0) + return ({bit for i, bit in enumerate(bits) if (1 << i) & index} for index in int_list) diff --git a/test/core/test_switching.py b/test/core/test_switching.py new file mode 100644 index 00000000..57e27e0c --- /dev/null +++ b/test/core/test_switching.py @@ -0,0 +1,111 @@ +from fixate.core.switching import generate_bit_sets, VirtualMux, bit_generator + + +################################################################ +# generate_bit_sets + + +def test_generate_bit_sets_empty(): + assert list(generate_bit_sets([])) == [] + + +def test_generate_bit_sets_one_bit(): + assert list(generate_bit_sets(["b0"])) == [set(), {"b0"}] + + +def test_generate_bit_sets_multiple_bits(): + expected = [ + set(), + {"b0"}, + {"b1"}, + {"b1", "b0"}, + {"b2"}, + {"b2", "b0"}, + {"b2", "b1"}, + {"b2", "b1", "b0"} + ] + assert list(generate_bit_sets(["b0", "b1", "b2"])) == expected + + +################################################################ +# generate_bit_sets + + +def test_VirtualMux_simple_tree_map(): + class SimpleVirtualMux(VirtualMux): + pin_list = ["b0"] + map_tree = ["sig0", "sig1"] + + mux = SimpleVirtualMux() + # the empty signal "" get automatically added + assert mux._signal_map == {"": set(), "sig0": set(), "sig1": {"b0"}} + + +def test_VirtualMux_larger_tree_map(): + class LargerVirtualMux(VirtualMux): + pin_list = ["b0", "b1", "b2"] + map_tree = ["sig0", "sig1", "sig2", "sig3", "sig4", "sig5", "sig6", "sig7"] + + mux = LargerVirtualMux() + # the empty signal "" get automatically added + assert mux._signal_map == { + "": set(), + "sig0": set(), + "sig1": {"b0"}, + "sig2": {"b1"}, + "sig3": {"b1", "b0"}, + "sig4": {"b2"}, + "sig5": {"b2", "b0"}, + "sig6": {"b2", "b1"}, + "sig7": {"b2", "b1", "b0"}, + } + + +def test_VirtualMux_nested_tree_map(): + class NestedVirtualMux(VirtualMux): + # Final mapping: + # addr signal + # -------------- + # 0 a0 + # 1 a1_b0 + # 2 a2_b0 + # 3 a3 + # 5 a1_b1 + # 6 a2_b1_c0 + # 9 a1_b2 + # 10 a2_b2 + # 14 a2_b3 + # 22 a2_b1_c1 + pin_list = ("x0", "x1", "x2", "x3", "x4") + + mux_c = ("a2_b1_c0", "a2_b1_c1") + mux_b1 = ("a1_b0", "a1_b1", "a1_b2", None) + mux_b2 = ("a2_b0", mux_c, "a2_b2", "a2_b3") + + map_tree = ("a0", mux_b1, mux_b2, "a3") + + mux = NestedVirtualMux() + # the empty signal "" get automatically added + assert mux._signal_map == { + "": set(), + "a0": set(), + "a1_b0": {"x0"}, + "a2_b0": {"x1"}, + "a3": {"x1", "x0"}, + "a1_b1": {"x0", "x2"}, + "a2_b1_c0": {"x1", "x2"}, + "a1_b2": {"x0", "x3"}, + "a2_b2": {"x1", "x3"}, + "a2_b3": {"x1", "x2", "x3"}, + "a2_b1_c1": {"x1", "x2", "x4"}, + } + + + +def test_bit_generator(): + """b1, b10, b100, b1000, ...""" + bit_gen = bit_generator() + + actual = [next(bit_gen) for _ in range(8)] + expected = [1, 2, 4, 8, 16, 32, 64, 128] + assert actual == expected From 93846731031577a5004cd312c6e8f5645b7d2981 Mon Sep 17 00:00:00 2001 From: Clint Lawrence Date: Fri, 31 May 2024 18:19:56 +1000 Subject: [PATCH 03/41] minor tweaks and comment updates --- src/fixate/core/switching.py | 150 +++++++++++++++++++++++++---------- 1 file changed, 109 insertions(+), 41 deletions(-) diff --git a/src/fixate/core/switching.py b/src/fixate/core/switching.py index 0bf31915..bba96521 100644 --- a/src/fixate/core/switching.py +++ b/src/fixate/core/switching.py @@ -1,3 +1,33 @@ +""" + +JigDriver is all about switching signals in a jig (and also input, but I'm +ignoring that for now...) At the script level, the key abstraction is a +`VirtualMux`, which switching between a number of `Signals`. Each signal is +defined by a number of `Pins`. When using a relay matrix, a Signal often +corresponds to a single Pin, but it doesn't have to. + +Each VirtualMux connects to the VirtualAddressMap. When the multiplex is +called on a mux, the mux determines which pin must be set or cleared + + + _____________ ______________________ + SIG_1 ---| pin0 | | VirtualAddressMap | _____________________ + SIG_2 ---| MUX pin1 |---| | | AddressHandler | + SIG_3 ---| | | pin0 |---| pin0, pin1, pin2 | + SIG_4 ---|____________| | pin1 | |___________________| + | pin2 | ____________________ + | | | AddressHandler | + _____________ | pin3 |---| pin3, pin4 | + SIG_5 ---| pin2 | | pin4 | |__________________| + SIG_6 ---| MUX pin3 |---| | ___________________ + SIG_7 ---| pin4 | | pin5 |---| AddressHandler | + SIG_8 ---| pin5 | | | | pin5 | + |____________| |_____________________| |__________________| + ^ ^ + update_pins callback AddressHandler.set_pins() + VirtualAddressMap.add_update() +""" + from __future__ import annotations import itertools @@ -6,9 +36,9 @@ from dataclasses import dataclass Signal = str -PinName = str -PinList = Sequence[PinName] -PinSet = Set[PinName] +Pin = str +PinList = Sequence[Pin] +PinSet = Set[Pin] SignalMap = Dict[Signal, PinSet] @@ -18,7 +48,14 @@ class PinSetState: on: PinSet -PinUpdateCallback = Callable[[PinSetState, PinSetState, float, bool], None] +@dataclass(frozen=True) +class PinUpdate: + setup: PinSetState + final: PinSetState + minimum_change_time: float + + +PinUpdateCallback = Callable[[PinUpdate, bool], None] class VirtualMux: @@ -27,7 +64,7 @@ class VirtualMux: clearing_time: float = 0.0 ########################################################################### - # This methods are the public API for the class + # These methods are the public API for the class def __init__(self, update_pins: Optional[PinUpdateCallback] = None): # The last time this mux changed state. This is used in some jigs @@ -41,10 +78,10 @@ def __init__(self, update_pins: Optional[PinUpdateCallback] = None): else: self._update_pins = update_pins - # We force the pin_list to be an ordered sequence, because if the + # We annotate the pin_list to be an ordered sequence, because if the # mux is defined with a map_tree, we need the ordering. But after # initialisation, we only need set operations on the pin list, so - # we convert here and keep a reference on the object for future use. + # we convert here and keep a reference to the set for future use. self._pin_set = set(self.pin_list) self._state = "" @@ -52,7 +89,7 @@ def __init__(self, update_pins: Optional[PinUpdateCallback] = None): self._signal_map: SignalMap = self._map_signals() # If it wasn't already defined, define the implicit signal "" which - # can be used for no pins to be set. + # can be used to signify no pins active. if "" not in self._signal_map: self._signal_map[""] = set() @@ -81,14 +118,14 @@ def multiplex(self, signal_output: Signal, trigger_update=True): self.state_update_time is updated to the current time. In general, subclasses should not override. (VirtualSwitch does, but then - delegate the real work to this method to ensure consistent behaviour.) + delegates the real work to this method to ensure consistent behaviour.) """ if signal_output not in self._signal_map: name = self.__class__.__name__ raise ValueError(f"Signal '{signal_output}' not valid for multiplexer '{name}'") setup, final = self._calculate_pins(self._state, signal_output) - self._update_pins(setup, final, self.clearing_time, trigger_update) + self._update_pins(PinUpdate(setup, final, self.clearing_time), trigger_update) if signal_output != self._state: self.state_update_time = time.time() self._state = signal_output @@ -313,9 +350,7 @@ def __repr__(self): @staticmethod def _default_update_pins( - setup: PinSetState, - final: PinSetState, - minimum_change_time: float = 0.0, + pin_updates: PinUpdate, trigger_update: bool = True ) -> None: """ @@ -328,7 +363,7 @@ def _default_update_pins( In general, this method shouldn't be overridden in a subclass. An alternative can be provided to __init__. """ - print(setup, final, minimum_change_time, trigger_update) + print(pin_updates, trigger_update) class VirtualSwitch(VirtualMux): @@ -342,7 +377,7 @@ class VirtualSwitch(VirtualMux): shorthand which is to define the pin_name attribute as a string. """ - pin_name: PinName = "" + pin_name: Pin = "" map_tree = ("FALSE", "TRUE") def multiplex(self, signal_output: Union[Signal, bool], trigger_update: bool = True): @@ -390,10 +425,10 @@ class AddressHandler: :param pin_defaults: Sequence of pins (type string subset of pin_list) that should default to high logic on reset """ - pin_list: Sequence[PinName] = () + pin_list: Sequence[Pin] = () pin_defaults = () - def set_pins(self, pins: Collection[PinName]): + def set_pins(self, pins: Collection[Pin]): raise NotImplementedError @@ -409,7 +444,7 @@ def __init__(self): super().__init__() self._pin_lookup = {pin: bit for pin, bit in zip(self.pin_list, bit_generator())} - def set_pins(self, pins: Collection[PinName]): + def set_pins(self, pins: Collection[Pin]): value = sum(self._pin_lookup[pin] for pin in pins) self._update_output(value) @@ -421,7 +456,15 @@ def _update_output(self, value: int): class FTDIAddressHandler(PinValueAddressHandler): - """Lets define this for the common case?""" + """ + An address handler which uses the ftdi driver to control pins. + + We create this concrete address handler because we use it most + often. FT232 is used to bit-bang to shift register that are control + the switching in a jig. + """ + + @@ -429,16 +472,39 @@ class VirtualAddressMap: """ The supervisor loops through the attached virtual multiplexers each time a mux update is triggered. """ + def __init__(self, handlers: Sequence[AddressHandler]): + self._pin_to_handler: dict[Pin, AddressHandler] = {} + + for handler in handlers: + self._pin_to_handler.update({pin: handler for pin in handler.pin_list}) + + self._pending_updates: list[PinUpdate] = [] + + def add_update( + self, + pin_update: PinUpdate, + trigger_update: bool = True + ) -> None: + """This method should be registered with each virtual mux to route pin changes.""" + self._pending_updates.append(pin_update) + + if trigger_update: + self._do_pending_updates() + + def _do_pending_updates(self): + """ + Collate pending updates and send pins to respective address handlers. + + 1. For each pending update, combine pins to clear and pins to set. + 2. Find the longest `minimum_change_time` of all the updates + 3. Find the AddressHandler required to set each pin. + 4. Check if there is actually anything to change. Only write out + changes that are required. + 5. Do the setup phase + 6. Wait the required change time + 7. Do the final phase + """ - def __init__(self): - pass - # self.address_handlers = [] - # self.virtual_pin_list = [] - # self._virtual_pin_values = 0b0 - # self._virtual_pin_values_active = 0b0 - # self._virtual_pin_values_clear = 0b0 - # self.mux_assigned_pins = {} - # self._clearing_time = 0 # Not used in scripts # @property @@ -472,11 +538,6 @@ def active_pins(self): # if value # ] - def install_address_handler(self, handler): - ... - - def install_multiplexer(self, mux): - ... # one reference el relays: 35:elv_jig.py: 1010: self.virtual_map.update_defaults() # also used below in the jig driver @@ -531,7 +592,7 @@ def update_defaults(self): # self.update_pin_values(pin_values, trigger_update=False) # start_addr = addr - # These were only used internally, as far as I can tell... + # These were only used internally, as far as I can tell... Delete them #def update_pin_values(self, values, trigger_update=True): # def update_clearing_pin_values(self, values, clearing_time): @@ -582,17 +643,24 @@ class JigDriver(metaclass=JigMeta): :attribute defaults: Iterable of the default pins to set high on driver reset """ - multiplexers = () - address_handlers = () - defaults = () + # I want this to be + # multiplexers: Collection[Callable[[PinUpdateCallback], VirtualMux]] = () + # but for now it needs to be: + multiplexers: Collection[VirtualMux] = () + address_handlers: Collection[Callable[[], AddressHandler]] = () + + # defaults = () not used in any scripts def __init__(self): super().__init__() - self.virtual_map = VirtualAddressMap() - for addr_hand in self.address_handlers: - self.virtual_map.install_address_handler(addr_hand) + _address_map_instances = [factory() for factory in self.address_handlers] + self.virtual_map = VirtualAddressMap(_address_map_instances) + for mux in self.multiplexers: - self.virtual_map.install_multiplexer(mux) + # I want to change this. Ideally we would instantiate the virtual mux here + # and pass in the virtual_map.add_update. But I need to switch self.multiplexers + # to be factories first and work out what that means for JigMeta etc. + mux._update_pins = self.virtual_map.add_update def __setitem__(self, key, value): self.virtual_map.update_pin_by_name(key, value) From 5035e36932f925aba94e11f6218e45683355d5b1 Mon Sep 17 00:00:00 2001 From: Clint Lawrence Date: Sat, 1 Jun 2024 19:41:02 +1000 Subject: [PATCH 04/41] more typing --- src/fixate/core/switching.py | 43 +++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/src/fixate/core/switching.py b/src/fixate/core/switching.py index bba96521..646ddf68 100644 --- a/src/fixate/core/switching.py +++ b/src/fixate/core/switching.py @@ -41,6 +41,8 @@ PinSet = Set[Pin] SignalMap = Dict[Signal, PinSet] +TreeDef = Sequence[Union[Signal, TreeDef]] + @dataclass(frozen=True) class PinSetState: @@ -130,12 +132,27 @@ def multiplex(self, signal_output: Signal, trigger_update=True): self.state_update_time = time.time() self._state = signal_output - def defaults(self): + def defaults(self) -> None: """ Set the multiplexer to the default state. """ self.multiplex(self.default_signal) + def switch_through_all_signals(self) -> Generator[str, None, None]: + # not sure if we should keep this. + # probably better to have a method that retuns all + # signals then a helper somewhere else that loops and + # switches. I don't like changing state and printing + # buried in a generator. + # This was iterate_mux_paths on the JigDriver, but then it had + # to used internal implementation details. Better to have this + # as a method on VirtualMux + for signal in self._signal_map: + if signal is not None: + self.multiplex(signal) + yield f"{self.__class__.__name__}: {signal}" + self.defaults() + ########################################################################### # The following methods are potential candidates to override in a subclass @@ -491,7 +508,7 @@ def add_update( if trigger_update: self._do_pending_updates() - def _do_pending_updates(self): + def _do_pending_updates(self) -> None: """ Collate pending updates and send pins to respective address handlers. @@ -520,7 +537,7 @@ def _do_pending_updates(self): # ) # ) - def active_pins(self): + def active_pins(self) -> None: pass # Used in J474 Scripts:8:as2081_validation_tests.py # return [ @@ -541,7 +558,7 @@ def active_pins(self): # one reference el relays: 35:elv_jig.py: 1010: self.virtual_map.update_defaults() # also used below in the jig driver - def update_defaults(self): + def update_defaults(self) -> None: """ Writes the initialisation values to the address handlers as the default values set in the handlers """ @@ -598,11 +615,11 @@ def update_defaults(self): # def update_clearing_pin_values(self, values, clearing_time): # used in a few scripts - def update_pin_by_name(self, name, value, trigger_update=True): + def update_pin_by_name(self, name: Pin, value: bool, trigger_update=True) -> None: pass # not used in any scripts - def update_pins_by_name(self, pins, trigger_update=True): + def update_pins_by_name(self, pins: Collection[Pin], trigger_update=True) -> None: pass def __getitem__(self, item): @@ -682,21 +699,11 @@ def reset(self): if isinstance(mux, VirtualMux): mux.defaults() - def iterate_all_mux_paths(self): + def iterate_all_mux_paths(self) -> Generator[str, None, None]: for _, mux in self.mux.__dict__.items(): if isinstance(mux, VirtualMux): - yield from self.iterate_mux_paths(mux) + yield from mux.switch_through_all_signals() - def iterate_mux_paths(self, mux): - """ - :param mux: Multiplexer as an object - :return: Generator of multiplexer signal paths - """ - for pth in mux.signal_map: - if pth is not None: - mux(pth) - yield "{}: {}".format(mux.__class__.__name__, pth) - mux.defaults() T = TypeVar("T") From 3bd974a0a2754996fd58b526b60e93d9a0567062 Mon Sep 17 00:00:00 2001 From: Clint Lawrence Date: Mon, 3 Jun 2024 21:44:11 +1000 Subject: [PATCH 05/41] More annotaions and JigDriver changes --- mypy.ini | 11 ++++ script.py | 36 ++++++++++++ src/fixate/core/switching.py | 106 +++++++++++++++++++++++------------ 3 files changed, 116 insertions(+), 37 deletions(-) create mode 100644 script.py diff --git a/mypy.ini b/mypy.ini index 469e3f1b..999f6c15 100644 --- a/mypy.ini +++ b/mypy.ini @@ -66,7 +66,18 @@ exclude = (?x) ) ) ) +warn_unused_configs = True +warn_redundant_casts = True +[mypy-fixate.core.switching] +# Enable strict options for new code +warn_unused_ignores = True +strict_equality = True +extra_checks = True +check_untyped_defs = True +disallow_untyped_calls = True +disallow_incomplete_defs = True +disallow_untyped_defs = True # mypy will also analyse modules if they are imported by a module - even if they are excluded! diff --git a/script.py b/script.py new file mode 100644 index 00000000..2912a0c6 --- /dev/null +++ b/script.py @@ -0,0 +1,36 @@ + +from dataclasses import dataclass, field +from fixate.core.switching import VirtualMux, JigDriver, MuxGroup, PinValueAddressHandler + +class MuxOne(VirtualMux): + pin_list = ("x0", "x1", "x2") + map_list = ( + ("sig1", "x0"), + ("sig2", "x1"), + ("sig3", "x2"), + ) + +class MuxTwo(VirtualMux): + pin_list = ("x3", "x4", "x5") + map_tree = ( + "sig4", + "sig5", + "sig6", + ( + "sig7", + "sig8", + ), + ) + +class Handler(PinValueAddressHandler): + pin_list = ("x0", "x1", "x2", "x3", "x4", "x5") + + + +@dataclass +class JigMuxGroup(MuxGroup): + mux_one: MuxOne = field(default_factory=MuxOne) + mux_two: MuxTwo = field(default_factory=MuxTwo) + +jig = JigDriver(JigMuxGroup, [Handler()]) + diff --git a/src/fixate/core/switching.py b/src/fixate/core/switching.py index 646ddf68..61a7533b 100644 --- a/src/fixate/core/switching.py +++ b/src/fixate/core/switching.py @@ -32,7 +32,13 @@ import itertools import time -from typing import Optional, Callable, Set, Sequence, TypeVar, Generator, Union, Collection, Dict +from typing import ( + Generic, Optional, Callable, + Set, Sequence, TypeVar, + Generator, Union, Collection, Dict, Any, + TYPE_CHECKING, + Type, + ) from dataclasses import dataclass Signal = str @@ -41,7 +47,10 @@ PinSet = Set[Pin] SignalMap = Dict[Signal, PinSet] -TreeDef = Sequence[Union[Signal, TreeDef]] +if TYPE_CHECKING: + TreeDef = Sequence[Union[Signal, TreeDef]] +else: + TreeDef = Sequence[Any] @dataclass(frozen=True) @@ -104,7 +113,7 @@ def __call__(self, signal_output: Signal, trigger_update: bool = True) -> None: """ self.multiplex(signal_output, trigger_update) - def multiplex(self, signal_output: Signal, trigger_update=True): + def multiplex(self, signal_output: Signal, trigger_update: bool =True) -> None: """ Update the multiplexer state to signal_output. @@ -197,7 +206,7 @@ def _map_signals(self) -> SignalMap: else: raise ValueError("VirtualMux subclass must define either map_tree or map_list") - def _map_tree(self, tree, pins: PinList, fixed_pins: PinSet) -> SignalMap: + def _map_tree(self, tree: TreeDef, pins: PinList, fixed_pins: PinSet) -> SignalMap: """recursively add nested signal lists to the signal map. tree: is the current sub-branch to be added. At the first call level, this would be initialised with self.map_tree. It can be @@ -362,7 +371,7 @@ class Mux(VirtualMux): return signal_map - def __repr__(self): + def __repr__(self) -> str: return self.__class__.__name__ @staticmethod @@ -397,7 +406,7 @@ class VirtualSwitch(VirtualMux): pin_name: Pin = "" map_tree = ("FALSE", "TRUE") - def multiplex(self, signal_output: Union[Signal, bool], trigger_update: bool = True): + def multiplex(self, signal_output: Union[Signal, bool], trigger_update: bool = True) -> None: if signal_output is True: signal = "TRUE" elif signal_output is False: @@ -445,7 +454,7 @@ class AddressHandler: pin_list: Sequence[Pin] = () pin_defaults = () - def set_pins(self, pins: Collection[Pin]): + def set_pins(self, pins: Collection[Pin]) -> None: raise NotImplementedError @@ -457,15 +466,15 @@ def bit_generator() -> Generator[int, None, None]: class PinValueAddressHandler(AddressHandler): """Maps pins to bit values then combines the bit values for an update""" - def __init__(self): + def __init__(self) -> None: super().__init__() self._pin_lookup = {pin: bit for pin, bit in zip(self.pin_list, bit_generator())} - def set_pins(self, pins: Collection[Pin]): + def set_pins(self, pins: Collection[Pin]) -> None: value = sum(self._pin_lookup[pin] for pin in pins) self._update_output(value) - def _update_output(self, value: int): + def _update_output(self, value: int) -> None: # perhaps it's easy to compose by passing the output # function into __init__, like what we did with the VirtualMux? bits = len(self.pin_list) @@ -615,23 +624,24 @@ def update_defaults(self) -> None: # def update_clearing_pin_values(self, values, clearing_time): # used in a few scripts - def update_pin_by_name(self, name: Pin, value: bool, trigger_update=True) -> None: + def update_pin_by_name(self, name: Pin, value: bool, trigger_update: bool =True) -> None: pass # not used in any scripts - def update_pins_by_name(self, pins: Collection[Pin], trigger_update=True) -> None: + def update_pins_by_name(self, pins: Collection[Pin], trigger_update: bool=True) -> None: pass - def __getitem__(self, item): - pass + def __getitem__(self, item: Pin) -> bool: + return True # self.update_input() # return bool((1 << self.virtual_pin_list.index(item)) & self._virtual_pin_values) - def __setitem__(self, key, value): + def __setitem__(self, key: Pin, value: bool) -> None: pass # index = self.virtual_pin_list.index(key) # self.update_pin_values([(index, value)]) + class JigMeta(type): """ usage: @@ -639,15 +649,23 @@ class JigMeta(type): Dynamically adds multiplexers and multiplexer groups to the Jig Class definition """ - def __new__(mcs, clsname, bases, dct): - muxes = dct.get("multiplexers", None) + def __new__(cls: Type[JigMeta], name: str, bases: tuple, classdict: dict) -> JigMeta: + muxes = classdict.get("multiplexers", None) if muxes is not None: mux_dct = {mux.__class__.__name__: mux for mux in muxes} - dct["mux"] = type("MuxController", (), mux_dct) - return super().__new__(mcs, clsname, bases, dct) + classdict["mux"] = type("MuxController", (), mux_dct) + return super().__new__(cls, name, bases, classdict) + + +class MuxGroup: + def get_multiplexers(self) -> list[VirtualMux]: + return [attr for attr in self.__dict__.values() if isinstance(attr, VirtualMux)] -class JigDriver(metaclass=JigMeta): +JigSpecificMuxGroup = TypeVar("JigSpecificMuxGroup", bound=MuxGroup) + + +class JigDriver(Generic[JigSpecificMuxGroup]): """ :attribute address_handlers: Iterable of Address Handlers [,... @@ -663,32 +681,46 @@ class JigDriver(metaclass=JigMeta): # I want this to be # multiplexers: Collection[Callable[[PinUpdateCallback], VirtualMux]] = () # but for now it needs to be: - multiplexers: Collection[VirtualMux] = () - address_handlers: Collection[Callable[[], AddressHandler]] = () + # multiplexers: Collection[VirtualMux] = () + # address_handlers: Collection[Callable[[], AddressHandler]] = () # defaults = () not used in any scripts - - def __init__(self): - super().__init__() - _address_map_instances = [factory() for factory in self.address_handlers] - self.virtual_map = VirtualAddressMap(_address_map_instances) - - for mux in self.multiplexers: - # I want to change this. Ideally we would instantiate the virtual mux here - # and pass in the virtual_map.add_update. But I need to switch self.multiplexers - # to be factories first and work out what that means for JigMeta etc. + + # mux is added by the metaclass. I suspect we're better off + # doing away with that entirely and getting any script that + # using a Jig Driver to build it's own "MuxGroup" or similar + # that then gets assigned to JigDriver.mux. i.e. JigDriver + # becomes a generic + # + # Note that for now, the only place we use jig here does an + # isinstance check, so the Any makes no differece internally. + # But for test scripts that define a jig driver, it means there + # is no type info available for mux (i.e. - no IDE autocomplete :()) + mux: JigSpecificMuxGroup + + def __init__(self, mux_group_factory: Callable[[],JigSpecificMuxGroup], handlers: Sequence[AddressHandler]): + # _address_map_instances = [factory() for factory in self.address_handlers] + self.virtual_map = VirtualAddressMap(handlers) + + self.mux = mux_group_factory() + for mux in self.mux.get_multiplexers(): + # Perhaps we should instantiate the virtual mux here + # and pass in the virtual_map.add_update. But we'd have to do some + # magic in the MuxGroup call to pass add_update to each VirtualMux + # constructor, and I was hoping to just use a dataclass... mux._update_pins = self.virtual_map.add_update - def __setitem__(self, key, value): + def __setitem__(self, key: Pin, value: bool) -> None: self.virtual_map.update_pin_by_name(key, value) - def __getitem__(self, item): + def __getitem__(self, item: Pin) -> bool: return self.virtual_map[item] - def active_pins(self): - return self.virtual_map.active_pins() + def active_pins(self) -> None: + #return self.virtual_map.active_pins() + pass - def reset(self): + def reset(self) -> None: """ Reset the multiplexers to the default values Raises exception if failed From 5ddeab424e176f0c6378b8b20f52366b6455a5e6 Mon Sep 17 00:00:00 2001 From: Clint Lawrence Date: Tue, 4 Jun 2024 16:16:38 +1000 Subject: [PATCH 06/41] Glue handlers and muxs, typing, comments etc --- script.py | 32 ++- src/fixate/core/switching.py | 380 ++++++++++++++++------------------- test/core/test_switching.py | 44 +++- 3 files changed, 236 insertions(+), 220 deletions(-) diff --git a/script.py b/script.py index 2912a0c6..7fc1ebe3 100644 --- a/script.py +++ b/script.py @@ -1,6 +1,10 @@ - +""" +This file is just a test playground that shows how the update jig classes will +fit together. +""" from dataclasses import dataclass, field -from fixate.core.switching import VirtualMux, JigDriver, MuxGroup, PinValueAddressHandler +from fixate.core.switching import VirtualMux, JigDriver, MuxGroup, PinValueAddressHandler, VirtualSwitch + class MuxOne(VirtualMux): pin_list = ("x0", "x1", "x2") @@ -22,15 +26,39 @@ class MuxTwo(VirtualMux): ), ) +class MuxThree(VirtualMux): + pin_list = ("x101", "x123") + map_list = (("", "x101", "x123"),) + class Handler(PinValueAddressHandler): pin_list = ("x0", "x1", "x2", "x3", "x4", "x5") +# Problem! +# our existing scripts/jig driver, the name of the mux is the +# class of the virtual mux. This scheme below will not allow that +# to work. +# Assuming an existing script with a mux called NewVirtualMux +# 1. Update every reference in the script +# dm.jig.mux.NewVirtualMux -> dm.jig.mux.new_virtual_mux +# 2. Change the class name, but keep the attribute name +# @dataclass +# class JigMuxGroup(MuxGroup): +# NewVirtualMux: _NewVirtualMux +# Then the references in the script stay this same. +# jig.mux.NewVirtualMux +# 3. Change the attribute name on mux, like in the example below, +# but add some compatibility code to MuxGroup base class so that +# attribute lookups that match the Class of one of its attributes +# get magically mapped to the correct attribute. @dataclass class JigMuxGroup(MuxGroup): mux_one: MuxOne = field(default_factory=MuxOne) mux_two: MuxTwo = field(default_factory=MuxTwo) + mux_three: MuxThree = field(default_factory=MuxThree) jig = JigDriver(JigMuxGroup, [Handler()]) +jig.mux.mux_one("sig2", trigger_update=False) +jig.mux.mux_two("sig5") diff --git a/src/fixate/core/switching.py b/src/fixate/core/switching.py index 61a7533b..58722f5c 100644 --- a/src/fixate/core/switching.py +++ b/src/fixate/core/switching.py @@ -1,5 +1,4 @@ """ - JigDriver is all about switching signals in a jig (and also input, but I'm ignoring that for now...) At the script level, the key abstraction is a `VirtualMux`, which switching between a number of `Signals`. Each signal is @@ -33,21 +32,31 @@ import itertools import time from typing import ( - Generic, Optional, Callable, - Set, Sequence, TypeVar, - Generator, Union, Collection, Dict, Any, TYPE_CHECKING, - Type, - ) + Generic, + Optional, + Callable, + Sequence, + TypeVar, + Generator, + Union, + Collection, + Dict, + Any, + FrozenSet, Set +) from dataclasses import dataclass +from functools import reduce +from operator import or_ Signal = str Pin = str PinList = Sequence[Pin] -PinSet = Set[Pin] +PinSet = FrozenSet[Pin] SignalMap = Dict[Signal, PinSet] if TYPE_CHECKING: + # The self reference doesn't work at runtime, by mypy knows what it means. TreeDef = Sequence[Union[Signal, TreeDef]] else: TreeDef = Sequence[Any] @@ -55,15 +64,29 @@ @dataclass(frozen=True) class PinSetState: - off: PinSet - on: PinSet + off: PinSet = frozenset() + on: PinSet = frozenset() + + def __or__(self, other: PinSetState) -> PinSetState: + if isinstance(other, PinSetState): + return PinSetState(self.off | other.off, self.on | other.on) + return NotImplemented @dataclass(frozen=True) class PinUpdate: - setup: PinSetState - final: PinSetState - minimum_change_time: float + setup: PinSetState = PinSetState() + final: PinSetState = PinSetState() + minimum_change_time: float = 0.0 + + def __or__(self, other: PinUpdate) -> PinUpdate: + if isinstance(other, PinUpdate): + return PinUpdate( + setup=self.setup | other.setup, + final=self.final | other.final, + minimum_change_time=max(self.minimum_change_time, other.minimum_change_time) + ) + return NotImplemented PinUpdateCallback = Callable[[PinUpdate, bool], None] @@ -71,7 +94,6 @@ class PinUpdate: class VirtualMux: pin_list: PinList = () - default_signal: Signal = "" clearing_time: float = 0.0 ########################################################################### @@ -93,7 +115,7 @@ def __init__(self, update_pins: Optional[PinUpdateCallback] = None): # mux is defined with a map_tree, we need the ordering. But after # initialisation, we only need set operations on the pin list, so # we convert here and keep a reference to the set for future use. - self._pin_set = set(self.pin_list) + self._pin_set = frozenset(self.pin_list) self._state = "" @@ -102,7 +124,7 @@ def __init__(self, update_pins: Optional[PinUpdateCallback] = None): # If it wasn't already defined, define the implicit signal "" which # can be used to signify no pins active. if "" not in self._signal_map: - self._signal_map[""] = set() + self._signal_map[""] = frozenset() def __call__(self, signal_output: Signal, trigger_update: bool = True) -> None: """ @@ -113,7 +135,7 @@ def __call__(self, signal_output: Signal, trigger_update: bool = True) -> None: """ self.multiplex(signal_output, trigger_update) - def multiplex(self, signal_output: Signal, trigger_update: bool =True) -> None: + def multiplex(self, signal_output: Signal, trigger_update: bool = True) -> None: """ Update the multiplexer state to signal_output. @@ -141,17 +163,11 @@ def multiplex(self, signal_output: Signal, trigger_update: bool =True) -> None: self.state_update_time = time.time() self._state = signal_output - def defaults(self) -> None: - """ - Set the multiplexer to the default state. - """ - self.multiplex(self.default_signal) - def switch_through_all_signals(self) -> Generator[str, None, None]: # not sure if we should keep this. - # probably better to have a method that retuns all - # signals then a helper somewhere else that loops and - # switches. I don't like changing state and printing + # probably better to have a method that returns all + # signals and use that in a helper somewhere else that loops and + # switches. I don't like state changes and printing # buried in a generator. # This was iterate_mux_paths on the JigDriver, but then it had # to used internal implementation details. Better to have this @@ -160,7 +176,9 @@ def switch_through_all_signals(self) -> Generator[str, None, None]: if signal is not None: self.multiplex(signal) yield f"{self.__class__.__name__}: {signal}" - self.defaults() + + def reset(self, trigger_update: bool=True) -> None: + self.multiplex("", trigger_update) ########################################################################### # The following methods are potential candidates to override in a subclass @@ -177,8 +195,12 @@ def _calculate_pins(self, old_signal: Signal, new_signal: Signal) -> tuple[PinSe marked as private to discourage use, but it is intended to be subclassed. For example, RelayMatrix overrides this to implement break-before-make switching. + + old_signal isn't currently used, but it is provided in case a future + subclass needs to it calculate the update. In particular, it could be + useful for make-before-break behaviour. """ - setup = PinSetState(set(), set()) + setup = PinSetState() on_pins = self._signal_map[new_signal] final = PinSetState(self._pin_set - on_pins, on_pins) return setup, final @@ -192,17 +214,17 @@ def _map_signals(self) -> SignalMap: Default implementation of the signal mapping We need to construct a dictionary mapping signals to a set of pins. - In the case the self.map_list is set, the is pretty trival. + In the case that self.map_list is set, the is pretty trival. If the mux is defined with self.map_tree we have more work to - do... + do, which is recursively delegated to _map_tree Avoid subclassing. Consider creating helper functions to build - map_tree or map_list. Although + map_tree or map_list. """ if hasattr(self, "map_tree"): - return self._map_tree(self.map_tree, self.pin_list, fixed_pins=set()) + return self._map_tree(self.map_tree, self.pin_list, fixed_pins=frozenset()) elif hasattr(self, "map_list"): - return {sig: set(pins) for sig, *pins in self.map_list} + return {sig: frozenset(pins) for sig, *pins in self.map_list} else: raise ValueError("VirtualMux subclass must define either map_tree or map_list") @@ -221,8 +243,8 @@ def _map_tree(self, tree: TreeDef, pins: PinList, fixed_pins: PinSet) -> SignalM mapping all the nested Mux B signals. example: - This shows 10 signal, routed through a number of multiplexers. - Mux B and Mux B' are distinct, but address of common control + This shows 10 signals, routed through a number of multiplexers. + Mux B and Mux B' are distinct, but addressed with common control signals. Mux C and Mux B/B' are nested to various levels into the final multiplexer Mux A. @@ -320,7 +342,8 @@ class Mux(VirtualMux): 31 For Multiplexers that depend on separate control pins, try using the shift_nested function to help - with sparse mapping + with sparse mapping. Note that shift_nested() hasn't been copied over from jig_mapping. + __________ a0-------------------------------| | ________ | | @@ -340,17 +363,30 @@ class Mux(VirtualMux): |__x1__x0__| class Mux(VirtualMux): - pin_list = ("x0", "x1", "x2", "x3", "x4") + pin_list = ("x0", "x1", "x2", "x3", "x4", "x5") mux_c = ("a3_c0", "a3_c1", "a3_c2", None) mux_b = ("a1_b0", "a1_b1", "a1_b2", None) map_tree = ( - "a0", - mux_b, - shift_nested(mux_c, [2]), # 2 in indicative on how many pins to skip. This case is (x2, x3) from mux_b - "a3") + "a0", + mux_b, + "a2", + shift_nested(mux_c, [2]), # 2 in indicative on how many pins to skip. This case is (x2, x3) from mux_b + ) + + Note: + If shift_nested is needed, I think I'd re-implement something like TreeMap that + can be used to define the mux. + + class TreeMap: + signals: Sequence[pin | TreeMap] + pins: PinSet + class Mux(VirtualMux): + mux_c = TreeMap(("a3_c0", "a3_c1", "a3_c2", None), ("x4", "x5")) + mux_b = TreeMap(("a1_b0", "a1_b1", "a1_b2", None), ("x2", "x3")) + map_tree = TreeMap(("a0", mux_b, "a2", mux_c), ("x1", "x0")) """ signal_map: SignalMap = dict() @@ -361,18 +397,18 @@ class Mux(VirtualMux): if signal_or_tree is None: continue if isinstance(signal_or_tree, Signal): - signal_map[signal_or_tree] = set(pins_for_signal) | fixed_pins + signal_map[signal_or_tree] = frozenset(pins_for_signal) | fixed_pins else: signal_map.update(self._map_tree( tree=signal_or_tree, pins=pins[bits_at_this_level:], - fixed_pins=set(pins_for_signal) | fixed_pins, + fixed_pins=frozenset(pins_for_signal) | fixed_pins, )) return signal_map def __repr__(self) -> str: - return self.__class__.__name__ + return f"{self.__class__.__name__}('{self._state}')" @staticmethod def _default_update_pins( @@ -431,7 +467,7 @@ def _calculate_pins(self, old_signal: Signal, new_signal: Signal) -> tuple[PinSe """ Override of _calculate_pins to implement break-before-make switching. """ - setup = PinSetState(off=self._pin_set, on=set()) + setup = PinSetState(off=self._pin_set, on=frozenset()) on_pins = self._signal_map[new_signal] final = PinSetState(off=self._pin_set - on_pins, on=on_pins) return setup, final @@ -448,19 +484,18 @@ class AddressHandler: that implement a set_pins() method. :param pin_list: Sequence of pins - :param pin_defaults: Sequence of pins (type string subset of pin_list) that should default to high logic on reset """ pin_list: Sequence[Pin] = () - pin_defaults = () def set_pins(self, pins: Collection[Pin]) -> None: - raise NotImplementedError - + """ + Called by the VirtualAddressMap to write out pin changes. -def bit_generator() -> Generator[int, None, None]: - """b1, b10, b100, b1000, ...""" - return (1 << counter for counter in itertools.count()) + : param pins: is a collection of pins that should be made active. All other + pins defined by the AddressHandler should be cleared. + """ + raise NotImplementedError class PinValueAddressHandler(AddressHandler): @@ -489,9 +524,8 @@ class FTDIAddressHandler(PinValueAddressHandler): often. FT232 is used to bit-bang to shift register that are control the switching in a jig. """ - - - + def _update_output(self, value: int) -> None: + raise NotImplementedError class VirtualAddressMap: @@ -499,12 +533,16 @@ class VirtualAddressMap: The supervisor loops through the attached virtual multiplexers each time a mux update is triggered. """ def __init__(self, handlers: Sequence[AddressHandler]): - self._pin_to_handler: dict[Pin, AddressHandler] = {} - + # used to work out which pins get routed to which address handler + self._handler_pin_sets: list[tuple[PinSet, AddressHandler]] = [] for handler in handlers: - self._pin_to_handler.update({pin: handler for pin in handler.pin_list}) + self._handler_pin_sets.append((frozenset(handler.pin_list), handler)) + self._all_pins = frozenset(itertools.chain.from_iterable(handler.pin_list for handler in handlers)) + # a list of updates that haven't been sent to address handlers yet. This + # allows a few mux changes to get updated at the same time. self._pending_updates: list[PinUpdate] = [] + self._active_pins: set[Pin] = set() def add_update( self, @@ -530,176 +568,102 @@ def _do_pending_updates(self) -> None: 6. Wait the required change time 7. Do the final phase """ + collated = reduce(or_, self._pending_updates, PinUpdate()) + self._pending_updates = [] + + if in_both := collated.setup.on & collated.setup.off: + raise ValueError(f"The following pins need to be on and off {in_both}") + + if in_both := collated.final.on & collated.final.off: + raise ValueError(f"The following pins need to be on and off {in_both}") + + self._dispatch_pin_state(collated.setup) + time.sleep(collated.minimum_change_time) + self._dispatch_pin_state(collated.final) + + def _dispatch_pin_state(self, new_state: PinSetState) -> None: + # check all pins actually have an address handler to send to + if unknown_pins := (new_state.on | new_state.off) - self._all_pins: + raise ValueError(f"Can't switch unknown pin(s) {', '.join(unknown_pins)}.") + + new_active_pins = (self._active_pins | new_state.on) - new_state.off + if new_active_pins != self._active_pins: + self._active_pins = new_active_pins + for pin_set, handler in self._handler_pin_sets: + handler.set_pins(pin_set & self._active_pins) + def active_pins(self) -> Set[Pin]: + return self._active_pins - # Not used in scripts - # @property - # def pin_values(self): - # return list( - # zip( - # self.virtual_pin_list, - # bits( - # self._virtual_pin_values_active, - # num_bits=len(self.virtual_pin_list), - # order="LSB", - # ), - # ) - # ) - - def active_pins(self) -> None: - pass - # Used in J474 Scripts:8:as2081_validation_tests.py - # return [ - # ( - # self.virtual_pin_list[pin], - # self.mux_assigned_pins[self.virtual_pin_list[pin]], - # ) - # for pin, value in enumerate( - # bits( - # self._virtual_pin_values_active, - # num_bits=len(self.virtual_pin_list), - # order="LSB", - # ) - # ) - # if value - # ] - - - # one reference el relays: 35:elv_jig.py: 1010: self.virtual_map.update_defaults() - # also used below in the jig driver - def update_defaults(self) -> None: + def reset(self) -> None: + """ + Sets all pins to be inactive. + """ + self._dispatch_pin_state(PinSetState(off=self._all_pins)) + + + def update_input(self) -> None: """ - Writes the initialisation values to the address handlers as the default values set in the handlers + Iterates through the address_handlers and reads the values back to update the pin values for the digital inputs + :return: """ - # pin_values = [] - # self._virtual_pin_values = 0 - # for _, handler in self.address_handlers: - # pin_values.extend(handler.defaults()) - # self.update_pins_by_name(pin_values) - - # def update_output(self): - # """ - # Iterates through the address_handlers and send a bit shifted and masked value of the _virtual_pin_values - # relevant to the address handlers update function. - # :return: - # """ - # start_addr = 0x00 - # for addr, handler in self.address_handlers: - # shifted = self._virtual_pin_values >> start_addr - # mask = (1 << (addr - start_addr)) - 1 - # handler.update_output(shifted & mask) - # start_addr = addr - # - # def update_clearing_output(self): - # start_addr = 0x00 - # for addr, handler in self.address_handlers: - # shifted = self._virtual_pin_values_clear >> start_addr - # mask = (1 << (addr - start_addr)) - 1 - # handler.update_output(shifted & mask) - # start_addr = addr - - ####################### - # I'm ignoring input for now... - ####################### - # def update_input(self): - # """ - # Iterates through the address_handlers and reads the values back to update the pin values for the digital inputs - # :return: - # """ - # start_addr = 0x00 - # for addr, handler in self.address_handlers: - # values = handler.update_input() - # if values is not None: # Handler can return valid input values - # pin_values = [] - # for index, b in enumerate( - # bits(values, num_bits=len(handler.pin_list), order="LSB") - # ): - # pin_values.append((index + start_addr, b)) - # self.update_pin_values(pin_values, trigger_update=False) - # start_addr = addr - - # These were only used internally, as far as I can tell... Delete them - #def update_pin_values(self, values, trigger_update=True): - - # def update_clearing_pin_values(self, values, clearing_time): + raise NotImplementedError # used in a few scripts def update_pin_by_name(self, name: Pin, value: bool, trigger_update: bool =True) -> None: - pass + raise NotImplementedError # not used in any scripts def update_pins_by_name(self, pins: Collection[Pin], trigger_update: bool=True) -> None: - pass + raise NotImplementedError def __getitem__(self, item: Pin) -> bool: - return True - # self.update_input() - # return bool((1 << self.virtual_pin_list.index(item)) & self._virtual_pin_values) + """Get the value of a pin. (only inputs? or state of outputs also?)""" + raise NotImplementedError def __setitem__(self, key: Pin, value: bool) -> None: - pass - # index = self.virtual_pin_list.index(key) - # self.update_pin_values([(index, value)]) + """Set a pin""" + raise NotImplementedError -class JigMeta(type): - """ - usage: - Metaclass for Jig Driver - Dynamically adds multiplexers and multiplexer groups to the Jig Class definition +class MuxGroup: """ + Group multiple VirtualMux's, for use in a single Jig Driver. - def __new__(cls: Type[JigMeta], name: str, bases: tuple, classdict: dict) -> JigMeta: - muxes = classdict.get("multiplexers", None) - if muxes is not None: - mux_dct = {mux.__class__.__name__: mux for mux in muxes} - classdict["mux"] = type("MuxController", (), mux_dct) - return super().__new__(cls, name, bases, classdict) + If a test script, it is expected that MuxGroup will be subclassed, with attributes + being each required VirtualMux subclass. This can be done using a dataclass: - -class MuxGroup: + @dataclass + class JigMuxGroup(MuxGroup): + mux_one: MuxOne = field(default_factory=MuxOne) + mux_two: MuxTwo = field(default_factory=MuxTwo) + """ def get_multiplexers(self) -> list[VirtualMux]: return [attr for attr in self.__dict__.values() if isinstance(attr, VirtualMux)] + def reset(self) -> None: + mux_list = self.get_multiplexers() + if len(mux_list) == 0: + return + + for mux in mux_list[:-1]: + mux.reset(trigger_update=False) + mux_list[-1].reset(trigger_update=True) + + def active_signals(self) -> list[str]: + return [str(mux) for mux in self.get_multiplexers()] + JigSpecificMuxGroup = TypeVar("JigSpecificMuxGroup", bound=MuxGroup) class JigDriver(Generic[JigSpecificMuxGroup]): """ - :attribute address_handlers: Iterable of Address Handlers - [,... - ""] - - :attribute multiplexers: Iterable Virtual Muxes - {,... - "} + Combine multiple VirtualMux's and multiple AddressHandler's. - :attribute defaults: Iterable of the default pins to set high on driver reset + The jig driver joins muxes to handlers by matching up pin definitions. """ - - # I want this to be - # multiplexers: Collection[Callable[[PinUpdateCallback], VirtualMux]] = () - # but for now it needs to be: - # multiplexers: Collection[VirtualMux] = () - # address_handlers: Collection[Callable[[], AddressHandler]] = () - - # defaults = () not used in any scripts - - # mux is added by the metaclass. I suspect we're better off - # doing away with that entirely and getting any script that - # using a Jig Driver to build it's own "MuxGroup" or similar - # that then gets assigned to JigDriver.mux. i.e. JigDriver - # becomes a generic - # - # Note that for now, the only place we use jig here does an - # isinstance check, so the Any makes no differece internally. - # But for test scripts that define a jig driver, it means there - # is no type info available for mux (i.e. - no IDE autocomplete :()) - mux: JigSpecificMuxGroup - def __init__(self, mux_group_factory: Callable[[],JigSpecificMuxGroup], handlers: Sequence[AddressHandler]): - # _address_map_instances = [factory() for factory in self.address_handlers] self.virtual_map = VirtualAddressMap(handlers) self.mux = mux_group_factory() @@ -716,20 +680,18 @@ def __setitem__(self, key: Pin, value: bool) -> None: def __getitem__(self, item: Pin) -> bool: return self.virtual_map[item] - def active_pins(self) -> None: - #return self.virtual_map.active_pins() - pass + def active_pins(self) -> Set[Pin]: + return self.virtual_map.active_pins() def reset(self) -> None: """ - Reset the multiplexers to the default values - Raises exception if failed - :return: None + Reset all pins + + This leaves multiplexers in the current state, which may not + match up with the real pin state. To reset all the multiplexers, + use JigDriver.mux.reset() instead. """ - self.virtual_map.update_defaults() # TODO Test if this is required - for _, mux in self.mux.__dict__.items(): - if isinstance(mux, VirtualMux): - mux.defaults() + self.virtual_map.reset() def iterate_all_mux_paths(self) -> Generator[str, None, None]: for _, mux in self.mux.__dict__.items(): @@ -737,7 +699,6 @@ def iterate_all_mux_paths(self) -> Generator[str, None, None]: yield from mux.switch_through_all_signals() - T = TypeVar("T") @@ -750,3 +711,8 @@ def generate_bit_sets(bits: Sequence[T]) -> Generator[set[T], None, None]: """ int_list = range(1 << len(bits)) if len(bits) != 0 else range(0) return ({bit for i, bit in enumerate(bits) if (1 << i) & index} for index in int_list) + + +def bit_generator() -> Generator[int, None, None]: + """b1, b10, b100, b1000, ...""" + return (1 << counter for counter in itertools.count()) diff --git a/test/core/test_switching.py b/test/core/test_switching.py index 57e27e0c..b943d8ee 100644 --- a/test/core/test_switching.py +++ b/test/core/test_switching.py @@ -1,4 +1,4 @@ -from fixate.core.switching import generate_bit_sets, VirtualMux, bit_generator +from fixate.core.switching import generate_bit_sets, VirtualMux, bit_generator, PinSetState, PinUpdate ################################################################ @@ -26,9 +26,16 @@ def test_generate_bit_sets_multiple_bits(): ] assert list(generate_bit_sets(["b0", "b1", "b2"])) == expected +def test_bit_generator(): + """b1, b10, b100, b1000, ...""" + bit_gen = bit_generator() + + actual = [next(bit_gen) for _ in range(8)] + expected = [1, 2, 4, 8, 16, 32, 64, 128] + assert actual == expected ################################################################ -# generate_bit_sets +# virtual mux definitions def test_VirtualMux_simple_tree_map(): @@ -100,12 +107,27 @@ class NestedVirtualMux(VirtualMux): "a2_b1_c1": {"x1", "x2", "x4"}, } - - -def test_bit_generator(): - """b1, b10, b100, b1000, ...""" - bit_gen = bit_generator() - - actual = [next(bit_gen) for _ in range(8)] - expected = [1, 2, 4, 8, 16, 32, 64, 128] - assert actual == expected +################################################################ +# Helper dataclasses + +def test_pin_set_state_or(): + a = PinSetState(frozenset("ab"), frozenset("xy")) + b = PinSetState(frozenset("cd"), frozenset("x")) + assert a | b == PinSetState(frozenset("abcd"), frozenset("xy")) + +def test_pin_update_or(): + a = PinUpdate( + PinSetState(frozenset("a"), frozenset("b")), + PinSetState(frozenset(), frozenset("yz")), + 1.0) + b = PinUpdate( + PinSetState(frozenset("x"), frozenset()), + PinSetState(frozenset("c"), frozenset("d")), + 2.0 + ) + expected = PinUpdate( + PinSetState(frozenset("ax"), frozenset("b")), + PinSetState(frozenset("c"), frozenset("yzd")), + 2.0 + ) + assert expected == a | b \ No newline at end of file From 8b94e31cf60f89a0840ff2dd647c8ddfc78fbef8 Mon Sep 17 00:00:00 2001 From: Clint Lawrence Date: Tue, 4 Jun 2024 20:40:49 +1000 Subject: [PATCH 07/41] A bunch of random stuff... - Run black - Bump mypy version - Drop 3.7 and add 3.12 to CI test runs - Fix some 3.7 specific imports. --- .github/workflows/test.yml | 2 +- setup.cfg | 4 +- src/fixate/core/switching.py | 93 ++++++++++++++++++++++------------ src/fixate/drivers/__init__.py | 6 +-- test/core/test_switching.py | 24 ++++++--- tox.ini | 4 +- 6 files changed, 84 insertions(+), 49 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 45ba0c43..47f8cc17 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,7 +17,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v3 diff --git a/setup.cfg b/setup.cfg index fe6d196d..3189289b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -22,7 +22,7 @@ package_dir = packages = find: include_package_data = True zip_safe = False -python_requires = ~=3.7 +python_requires = ~=3.8 install_requires = pyvisa @@ -35,8 +35,6 @@ install_requires = numpy PyDAQmx # for typing.protocol - typing_extensions ; python_version < "3.8" - importlib-metadata >= 1.0 ; python_version < "3.8" platformdirs [options.packages.find] diff --git a/src/fixate/core/switching.py b/src/fixate/core/switching.py index 58722f5c..3801c504 100644 --- a/src/fixate/core/switching.py +++ b/src/fixate/core/switching.py @@ -43,7 +43,8 @@ Collection, Dict, Any, - FrozenSet, Set + FrozenSet, + Set, ) from dataclasses import dataclass from functools import reduce @@ -84,7 +85,9 @@ def __or__(self, other: PinUpdate) -> PinUpdate: return PinUpdate( setup=self.setup | other.setup, final=self.final | other.final, - minimum_change_time=max(self.minimum_change_time, other.minimum_change_time) + minimum_change_time=max( + self.minimum_change_time, other.minimum_change_time + ), ) return NotImplemented @@ -155,7 +158,9 @@ def multiplex(self, signal_output: Signal, trigger_update: bool = True) -> None: """ if signal_output not in self._signal_map: name = self.__class__.__name__ - raise ValueError(f"Signal '{signal_output}' not valid for multiplexer '{name}'") + raise ValueError( + f"Signal '{signal_output}' not valid for multiplexer '{name}'" + ) setup, final = self._calculate_pins(self._state, signal_output) self._update_pins(PinUpdate(setup, final, self.clearing_time), trigger_update) @@ -177,13 +182,15 @@ def switch_through_all_signals(self) -> Generator[str, None, None]: self.multiplex(signal) yield f"{self.__class__.__name__}: {signal}" - def reset(self, trigger_update: bool=True) -> None: + def reset(self, trigger_update: bool = True) -> None: self.multiplex("", trigger_update) - + ########################################################################### # The following methods are potential candidates to override in a subclass - def _calculate_pins(self, old_signal: Signal, new_signal: Signal) -> tuple[PinSetState, PinSetState]: + def _calculate_pins( + self, old_signal: Signal, new_signal: Signal + ) -> tuple[PinSetState, PinSetState]: """ Calculate the pin sets for the two-step state change. @@ -226,7 +233,9 @@ def _map_signals(self) -> SignalMap: elif hasattr(self, "map_list"): return {sig: frozenset(pins) for sig, *pins in self.map_list} else: - raise ValueError("VirtualMux subclass must define either map_tree or map_list") + raise ValueError( + "VirtualMux subclass must define either map_tree or map_list" + ) def _map_tree(self, tree: TreeDef, pins: PinList, fixed_pins: PinSet) -> SignalMap: """recursively add nested signal lists to the signal map. @@ -393,17 +402,21 @@ class Mux(VirtualMux): bits_at_this_level = (len(tree) - 1).bit_length() pins_at_this_level = pins[:bits_at_this_level] - for signal_or_tree, pins_for_signal in zip(tree, generate_bit_sets(pins_at_this_level)): + for signal_or_tree, pins_for_signal in zip( + tree, generate_bit_sets(pins_at_this_level) + ): if signal_or_tree is None: continue if isinstance(signal_or_tree, Signal): signal_map[signal_or_tree] = frozenset(pins_for_signal) | fixed_pins else: - signal_map.update(self._map_tree( - tree=signal_or_tree, - pins=pins[bits_at_this_level:], - fixed_pins=frozenset(pins_for_signal) | fixed_pins, - )) + signal_map.update( + self._map_tree( + tree=signal_or_tree, + pins=pins[bits_at_this_level:], + fixed_pins=frozenset(pins_for_signal) | fixed_pins, + ) + ) return signal_map @@ -412,8 +425,7 @@ def __repr__(self) -> str: @staticmethod def _default_update_pins( - pin_updates: PinUpdate, - trigger_update: bool = True + pin_updates: PinUpdate, trigger_update: bool = True ) -> None: """ Output callback to effect a state change in the mux. @@ -442,7 +454,9 @@ class VirtualSwitch(VirtualMux): pin_name: Pin = "" map_tree = ("FALSE", "TRUE") - def multiplex(self, signal_output: Union[Signal, bool], trigger_update: bool = True) -> None: + def multiplex( + self, signal_output: Union[Signal, bool], trigger_update: bool = True + ) -> None: if signal_output is True: signal = "TRUE" elif signal_output is False: @@ -463,7 +477,9 @@ def __init__( class RelayMatrixMux(VirtualMux): clearing_time = 0.01 - def _calculate_pins(self, old_signal: Signal, new_signal: Signal) -> tuple[PinSetState, PinSetState]: + def _calculate_pins( + self, old_signal: Signal, new_signal: Signal + ) -> tuple[PinSetState, PinSetState]: """ Override of _calculate_pins to implement break-before-make switching. """ @@ -503,7 +519,9 @@ class PinValueAddressHandler(AddressHandler): def __init__(self) -> None: super().__init__() - self._pin_lookup = {pin: bit for pin, bit in zip(self.pin_list, bit_generator())} + self._pin_lookup = { + pin: bit for pin, bit in zip(self.pin_list, bit_generator()) + } def set_pins(self, pins: Collection[Pin]) -> None: value = sum(self._pin_lookup[pin] for pin in pins) @@ -524,6 +542,7 @@ class FTDIAddressHandler(PinValueAddressHandler): often. FT232 is used to bit-bang to shift register that are control the switching in a jig. """ + def _update_output(self, value: int) -> None: raise NotImplementedError @@ -532,23 +551,22 @@ class VirtualAddressMap: """ The supervisor loops through the attached virtual multiplexers each time a mux update is triggered. """ + def __init__(self, handlers: Sequence[AddressHandler]): # used to work out which pins get routed to which address handler self._handler_pin_sets: list[tuple[PinSet, AddressHandler]] = [] for handler in handlers: self._handler_pin_sets.append((frozenset(handler.pin_list), handler)) - self._all_pins = frozenset(itertools.chain.from_iterable(handler.pin_list for handler in handlers)) + self._all_pins = frozenset( + itertools.chain.from_iterable(handler.pin_list for handler in handlers) + ) # a list of updates that haven't been sent to address handlers yet. This # allows a few mux changes to get updated at the same time. self._pending_updates: list[PinUpdate] = [] self._active_pins: set[Pin] = set() - def add_update( - self, - pin_update: PinUpdate, - trigger_update: bool = True - ) -> None: + def add_update(self, pin_update: PinUpdate, trigger_update: bool = True) -> None: """This method should be registered with each virtual mux to route pin changes.""" self._pending_updates.append(pin_update) @@ -601,7 +619,6 @@ def reset(self) -> None: """ self._dispatch_pin_state(PinSetState(off=self._all_pins)) - def update_input(self) -> None: """ Iterates through the address_handlers and reads the values back to update the pin values for the digital inputs @@ -610,11 +627,15 @@ def update_input(self) -> None: raise NotImplementedError # used in a few scripts - def update_pin_by_name(self, name: Pin, value: bool, trigger_update: bool =True) -> None: + def update_pin_by_name( + self, name: Pin, value: bool, trigger_update: bool = True + ) -> None: raise NotImplementedError # not used in any scripts - def update_pins_by_name(self, pins: Collection[Pin], trigger_update: bool=True) -> None: + def update_pins_by_name( + self, pins: Collection[Pin], trigger_update: bool = True + ) -> None: raise NotImplementedError def __getitem__(self, item: Pin) -> bool: @@ -638,6 +659,7 @@ class JigMuxGroup(MuxGroup): mux_one: MuxOne = field(default_factory=MuxOne) mux_two: MuxTwo = field(default_factory=MuxTwo) """ + def get_multiplexers(self) -> list[VirtualMux]: return [attr for attr in self.__dict__.values() if isinstance(attr, VirtualMux)] @@ -663,14 +685,19 @@ class JigDriver(Generic[JigSpecificMuxGroup]): The jig driver joins muxes to handlers by matching up pin definitions. """ - def __init__(self, mux_group_factory: Callable[[],JigSpecificMuxGroup], handlers: Sequence[AddressHandler]): + + def __init__( + self, + mux_group_factory: Callable[[], JigSpecificMuxGroup], + handlers: Sequence[AddressHandler], + ): self.virtual_map = VirtualAddressMap(handlers) - + self.mux = mux_group_factory() for mux in self.mux.get_multiplexers(): # Perhaps we should instantiate the virtual mux here - # and pass in the virtual_map.add_update. But we'd have to do some - # magic in the MuxGroup call to pass add_update to each VirtualMux + # and pass in the virtual_map.add_update. But we'd have to do some + # magic in the MuxGroup call to pass add_update to each VirtualMux # constructor, and I was hoping to just use a dataclass... mux._update_pins = self.virtual_map.add_update @@ -710,7 +737,9 @@ def generate_bit_sets(bits: Sequence[T]) -> Generator[set[T], None, None]: list(generate_bit_set(["x0", "x1"])) -> [set(), {'x0'}, {'x1'}, {'x0', 'x1'}] """ int_list = range(1 << len(bits)) if len(bits) != 0 else range(0) - return ({bit for i, bit in enumerate(bits) if (1 << i) & index} for index in int_list) + return ( + {bit for i, bit in enumerate(bits) if (1 << i) & index} for index in int_list + ) def bit_generator() -> Generator[int, None, None]: diff --git a/src/fixate/drivers/__init__.py b/src/fixate/drivers/__init__.py index eb24a5b7..6504c298 100644 --- a/src/fixate/drivers/__init__.py +++ b/src/fixate/drivers/__init__.py @@ -1,8 +1,4 @@ -try: - from typing import Protocol -except ImportError: - # Protocol added in python 3.8 - from typing_extensions import Protocol +from typing import Protocol import pubsub.pub diff --git a/test/core/test_switching.py b/test/core/test_switching.py index b943d8ee..fa428030 100644 --- a/test/core/test_switching.py +++ b/test/core/test_switching.py @@ -1,4 +1,10 @@ -from fixate.core.switching import generate_bit_sets, VirtualMux, bit_generator, PinSetState, PinUpdate +from fixate.core.switching import ( + generate_bit_sets, + VirtualMux, + bit_generator, + PinSetState, + PinUpdate, +) ################################################################ @@ -22,10 +28,11 @@ def test_generate_bit_sets_multiple_bits(): {"b2"}, {"b2", "b0"}, {"b2", "b1"}, - {"b2", "b1", "b0"} + {"b2", "b1", "b0"}, ] assert list(generate_bit_sets(["b0", "b1", "b2"])) == expected + def test_bit_generator(): """b1, b10, b100, b1000, ...""" bit_gen = bit_generator() @@ -34,6 +41,7 @@ def test_bit_generator(): expected = [1, 2, 4, 8, 16, 32, 64, 128] assert actual == expected + ################################################################ # virtual mux definitions @@ -107,27 +115,31 @@ class NestedVirtualMux(VirtualMux): "a2_b1_c1": {"x1", "x2", "x4"}, } + ################################################################ # Helper dataclasses + def test_pin_set_state_or(): a = PinSetState(frozenset("ab"), frozenset("xy")) b = PinSetState(frozenset("cd"), frozenset("x")) assert a | b == PinSetState(frozenset("abcd"), frozenset("xy")) + def test_pin_update_or(): a = PinUpdate( PinSetState(frozenset("a"), frozenset("b")), PinSetState(frozenset(), frozenset("yz")), - 1.0) + 1.0, + ) b = PinUpdate( PinSetState(frozenset("x"), frozenset()), PinSetState(frozenset("c"), frozenset("d")), - 2.0 + 2.0, ) expected = PinUpdate( PinSetState(frozenset("ax"), frozenset("b")), PinSetState(frozenset("c"), frozenset("yzd")), - 2.0 + 2.0, ) - assert expected == a | b \ No newline at end of file + assert expected == a | b diff --git a/tox.ini b/tox.ini index 4634a858..de5a566e 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py37,py38,py39,py310,py311,black,mypy +envlist = py38,py39,py310,py311,py212,black,mypy isolated_build = True [testenv] @@ -38,7 +38,7 @@ markers = [testenv:mypy] basepython = python3 -deps = mypy==1.3 +deps = mypy==1.10.0 # mypy gives different results if you actually install the stuff before you check it # separate cache to stop weirdness around sharing cache with other instances of mypy commands = mypy --cache-dir="{envdir}/mypy_cache" --config-file=mypy.ini From cebeadfc2d7614a76de952237674a3c743324f41 Mon Sep 17 00:00:00 2001 From: Clint Lawrence Date: Wed, 5 Jun 2024 09:53:24 +1000 Subject: [PATCH 08/41] Stringify 'TreeDef' to avoid if TYPE_CHECKING --- src/fixate/core/switching.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/fixate/core/switching.py b/src/fixate/core/switching.py index 3801c504..c5723bc3 100644 --- a/src/fixate/core/switching.py +++ b/src/fixate/core/switching.py @@ -32,7 +32,6 @@ import itertools import time from typing import ( - TYPE_CHECKING, Generic, Optional, Callable, @@ -42,7 +41,6 @@ Union, Collection, Dict, - Any, FrozenSet, Set, ) @@ -55,12 +53,7 @@ PinList = Sequence[Pin] PinSet = FrozenSet[Pin] SignalMap = Dict[Signal, PinSet] - -if TYPE_CHECKING: - # The self reference doesn't work at runtime, by mypy knows what it means. - TreeDef = Sequence[Union[Signal, TreeDef]] -else: - TreeDef = Sequence[Any] +TreeDef = Sequence[Union[Signal, "TreeDef"]] @dataclass(frozen=True) From 6a71fd20bcf1974cc7713a72c3dcab60d0a3f400 Mon Sep 17 00:00:00 2001 From: Clint Lawrence Date: Wed, 5 Jun 2024 13:22:54 +1000 Subject: [PATCH 09/41] Implement a Relay Matrix address handler --- src/fixate/core/switching.py | 61 +++++++++++++++++++++++++++++++++++- test/core/test_switching.py | 10 ++++++ 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/src/fixate/core/switching.py b/src/fixate/core/switching.py index c5723bc3..3f788cbc 100644 --- a/src/fixate/core/switching.py +++ b/src/fixate/core/switching.py @@ -48,6 +48,8 @@ from functools import reduce from operator import or_ +from fixate.drivers import ftdi + Signal = str Pin = str PinList = Sequence[Pin] @@ -506,6 +508,15 @@ def set_pins(self, pins: Collection[Pin]) -> None: """ raise NotImplementedError + def close(self) -> None: + """ + Optional close method to clean-up resources. + + This will be called automatically by the JigDriver for any + address handlers passed into the JigDriver when it as created. + """ + pass + class PinValueAddressHandler(AddressHandler): """Maps pins to bit values then combines the bit values for an update""" @@ -527,6 +538,16 @@ def _update_output(self, value: int) -> None: print(f"0b{value:0{bits}b}") +def _pins_for_one_relay_matrix(relay_matrix_num: int) -> list[Pin]: + """ + A helper to create pin names for relay matrix cards. + + Returns 16 pin names. If relay_matrix_num is 1: + 1K1, 1K2, 1K3, ..., 1K16 + """ + return [f"{relay_matrix_num}K{relay}" for relay in range(1, 17)] + + class FTDIAddressHandler(PinValueAddressHandler): """ An address handler which uses the ftdi driver to control pins. @@ -535,9 +556,41 @@ class FTDIAddressHandler(PinValueAddressHandler): often. FT232 is used to bit-bang to shift register that are control the switching in a jig. """ + def __init__( + self, + ftdi_description: str, + relay_matrix_count: int, + extra_pins: Sequence[Pin] = tuple() + ) -> None: + relay_matrix_pin_list = tuple( + itertools.chain.from_iterable( + _pins_for_one_relay_matrix(rm_number) + for rm_number in range(1, relay_matrix_count + 1) + ) + ) + self.pin_list = relay_matrix_pin_list + tuple(extra_pins) + # call the base class super _after_ we create the pin list + super().__init__() + + # how many bytes? enough for every pin to get a bit. We might + # end up with some left-over bits. The +7 in the expression + # ensure we round up. + bytes_required = (len(self.pin_list) + 7) // 8 + self._ftdi = ftdi.open(ftdi_description=ftdi_description) + self._ftdi.configure_bit_bang( + ftdi.BIT_MODE.FT_BITMODE_ASYNC_BITBANG, + bytes_required=bytes_required, + data_mask=4, + clk_mask=2, + latch_mask=1 + ) + self._ftdi.baud_rate = 115200 + + def close(self) -> None: + self._ftdi.close() def _update_output(self, value: int) -> None: - raise NotImplementedError + self._ftdi.serial_shift_bit_bang(value) class VirtualAddressMap: @@ -684,6 +737,8 @@ def __init__( mux_group_factory: Callable[[], JigSpecificMuxGroup], handlers: Sequence[AddressHandler], ): + # keep a reference to handlers so that we can close them if required. + self._handlers = handlers self.virtual_map = VirtualAddressMap(handlers) self.mux = mux_group_factory() @@ -700,6 +755,10 @@ def __setitem__(self, key: Pin, value: bool) -> None: def __getitem__(self, item: Pin) -> bool: return self.virtual_map[item] + def close(self) -> None: + for handler in self._handlers: + handler.close() + def active_pins(self) -> Set[Pin]: return self.virtual_map.active_pins() diff --git a/test/core/test_switching.py b/test/core/test_switching.py index fa428030..9b837451 100644 --- a/test/core/test_switching.py +++ b/test/core/test_switching.py @@ -4,6 +4,7 @@ bit_generator, PinSetState, PinUpdate, + _pins_for_one_relay_matrix, ) @@ -143,3 +144,12 @@ def test_pin_update_or(): 2.0, ) assert expected == a | b + + +################################################################ +# FTDI/Relay Matrix + + +def test_pins_for_one_relay_matrix(): + expected = "3K1 3K2 3K3 3K4 3K5 3K6 3K7 3K8 3K9 3K10 3K11 3K12 3K13 3K14 3K15 3K16".split() + assert _pins_for_one_relay_matrix(3) == expected From 6a05a7ec47a367a6f564e03b1a7a5512857688e6 Mon Sep 17 00:00:00 2001 From: Clint Lawrence Date: Wed, 5 Jun 2024 14:30:49 +1000 Subject: [PATCH 10/41] Move Relay Matrix address handler to avoid ftdi imports --- src/fixate/core/switching.py | 55 ------------------------ src/fixate/drivers/handlers.py | 76 ++++++++++++++++++++++++++++++++++ test/core/test_switching.py | 9 ---- 3 files changed, 76 insertions(+), 64 deletions(-) create mode 100644 src/fixate/drivers/handlers.py diff --git a/src/fixate/core/switching.py b/src/fixate/core/switching.py index 3f788cbc..acc5181e 100644 --- a/src/fixate/core/switching.py +++ b/src/fixate/core/switching.py @@ -538,61 +538,6 @@ def _update_output(self, value: int) -> None: print(f"0b{value:0{bits}b}") -def _pins_for_one_relay_matrix(relay_matrix_num: int) -> list[Pin]: - """ - A helper to create pin names for relay matrix cards. - - Returns 16 pin names. If relay_matrix_num is 1: - 1K1, 1K2, 1K3, ..., 1K16 - """ - return [f"{relay_matrix_num}K{relay}" for relay in range(1, 17)] - - -class FTDIAddressHandler(PinValueAddressHandler): - """ - An address handler which uses the ftdi driver to control pins. - - We create this concrete address handler because we use it most - often. FT232 is used to bit-bang to shift register that are control - the switching in a jig. - """ - def __init__( - self, - ftdi_description: str, - relay_matrix_count: int, - extra_pins: Sequence[Pin] = tuple() - ) -> None: - relay_matrix_pin_list = tuple( - itertools.chain.from_iterable( - _pins_for_one_relay_matrix(rm_number) - for rm_number in range(1, relay_matrix_count + 1) - ) - ) - self.pin_list = relay_matrix_pin_list + tuple(extra_pins) - # call the base class super _after_ we create the pin list - super().__init__() - - # how many bytes? enough for every pin to get a bit. We might - # end up with some left-over bits. The +7 in the expression - # ensure we round up. - bytes_required = (len(self.pin_list) + 7) // 8 - self._ftdi = ftdi.open(ftdi_description=ftdi_description) - self._ftdi.configure_bit_bang( - ftdi.BIT_MODE.FT_BITMODE_ASYNC_BITBANG, - bytes_required=bytes_required, - data_mask=4, - clk_mask=2, - latch_mask=1 - ) - self._ftdi.baud_rate = 115200 - - def close(self) -> None: - self._ftdi.close() - - def _update_output(self, value: int) -> None: - self._ftdi.serial_shift_bit_bang(value) - - class VirtualAddressMap: """ The supervisor loops through the attached virtual multiplexers each time a mux update is triggered. diff --git a/src/fixate/drivers/handlers.py b/src/fixate/drivers/handlers.py new file mode 100644 index 00000000..f6f6af4e --- /dev/null +++ b/src/fixate/drivers/handlers.py @@ -0,0 +1,76 @@ +""" +This module implements concrete AddressHandler type, that +can be used to implement IO for the fixate.core.switching module. +""" +from __future__ import annotations + +from typing import Sequence +import itertools + +from fixate.core.switching import Pin, PinValueAddressHandler +from fixate.drivers import ftdi + + +def _pins_for_one_relay_matrix(relay_matrix_num: int) -> list[Pin]: + """ + A helper to create pin names for relay matrix cards. + + Returns 16 pin names. If relay_matrix_num is 1: + 1K1, 1K2, 1K3, ..., 1K16 + """ + return [f"{relay_matrix_num}K{relay}" for relay in range(1, 17)] + + +# This is a real quick test. Not worth the effort unravelling +# imports when ftdi isn't importable, just to get this into a +# proper test right now... +__expected = ( + "3K1 3K2 3K3 3K4 3K5 3K6 3K7 3K8 3K9 3K10 3K11 3K12 3K13 3K14 3K15 3K16".split() +) +assert _pins_for_one_relay_matrix(3) == __expected + + +class RelayMatrixAddressHandler(PinValueAddressHandler): + """ + An address handler which uses the ftdi driver to control pins. + + We create this concrete address handler because we use it most + often. FT232 is used to bit-bang to shift register that are control + the switching in a jig. + """ + + def __init__( + self, + ftdi_description: str, + relay_matrix_count: int, + extra_pins: Sequence[Pin] = tuple(), + ) -> None: + relay_matrix_pin_list = tuple( + itertools.chain.from_iterable( + _pins_for_one_relay_matrix(rm_number) + for rm_number in range(1, relay_matrix_count + 1) + ) + ) + self.pin_list = relay_matrix_pin_list + tuple(extra_pins) + # call the base class super _after_ we create the pin list + super().__init__() + + # how many bytes? enough for every pin to get a bit. We might + # end up with some left-over bits. The +7 in the expression + # ensure we round up. + bytes_required = (len(self.pin_list) + 7) // 8 + self._ftdi = ftdi.open(ftdi_description=ftdi_description) + self._ftdi.configure_bit_bang( + ftdi.BIT_MODE.FT_BITMODE_ASYNC_BITBANG, + bytes_required=bytes_required, + data_mask=4, + clk_mask=2, + latch_mask=1, + ) + self._ftdi.baud_rate = 115200 + + def close(self) -> None: + self._ftdi.close() + + def _update_output(self, value: int) -> None: + self._ftdi.serial_shift_bit_bang(value) diff --git a/test/core/test_switching.py b/test/core/test_switching.py index 9b837451..3554f93c 100644 --- a/test/core/test_switching.py +++ b/test/core/test_switching.py @@ -144,12 +144,3 @@ def test_pin_update_or(): 2.0, ) assert expected == a | b - - -################################################################ -# FTDI/Relay Matrix - - -def test_pins_for_one_relay_matrix(): - expected = "3K1 3K2 3K3 3K4 3K5 3K6 3K7 3K8 3K9 3K10 3K11 3K12 3K13 3K14 3K15 3K16".split() - assert _pins_for_one_relay_matrix(3) == expected From 32a97c3a300150ec2ef8d5aa27411943e2e03bc9 Mon Sep 17 00:00:00 2001 From: Clint Lawrence Date: Wed, 5 Jun 2024 14:35:10 +1000 Subject: [PATCH 11/41] Remove the import that the whole last commit was meant to avoid... --- src/fixate/core/switching.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/fixate/core/switching.py b/src/fixate/core/switching.py index acc5181e..a65eb161 100644 --- a/src/fixate/core/switching.py +++ b/src/fixate/core/switching.py @@ -48,7 +48,6 @@ from functools import reduce from operator import or_ -from fixate.drivers import ftdi Signal = str Pin = str From 8dfffd2486e36754cc97585b7e1fa73aa3d287d5 Mon Sep 17 00:00:00 2001 From: Clint Lawrence Date: Wed, 5 Jun 2024 14:37:36 +1000 Subject: [PATCH 12/41] why test locally when you can just push, eh? --- test/core/test_switching.py | 1 - 1 file changed, 1 deletion(-) diff --git a/test/core/test_switching.py b/test/core/test_switching.py index 3554f93c..fa428030 100644 --- a/test/core/test_switching.py +++ b/test/core/test_switching.py @@ -4,7 +4,6 @@ bit_generator, PinSetState, PinUpdate, - _pins_for_one_relay_matrix, ) From 266431ea048acfee991c72e3b26a543e54b10df0 Mon Sep 17 00:00:00 2001 From: Clint Lawrence Date: Thu, 6 Jun 2024 19:08:34 +1000 Subject: [PATCH 13/41] fix python 3.12 env --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index de5a566e..25390149 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py38,py39,py310,py311,py212,black,mypy +envlist = py38,py39,py310,py311,py312,black,mypy isolated_build = True [testenv] From 6f24ea8c05f23dea119a4e1b292f6fac5cc1984c Mon Sep 17 00:00:00 2001 From: Clint Lawrence Date: Tue, 11 Jun 2024 21:30:44 +1000 Subject: [PATCH 14/41] Add some test for VirtulMux --- test/core/test_switching.py | 62 ++++++++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/test/core/test_switching.py b/test/core/test_switching.py index fa428030..57c35d52 100644 --- a/test/core/test_switching.py +++ b/test/core/test_switching.py @@ -6,6 +6,7 @@ PinUpdate, ) +import pytest ################################################################ # generate_bit_sets @@ -116,7 +117,66 @@ class NestedVirtualMux(VirtualMux): } -################################################################ +# ############################################################### +# VirtualMux Behaviour + + +class MuxA(VirtualMux): + """A mux definitioned used by a few scripts""" + pin_list = ("a0", "a1") + map_list = (("sig_a1", "a0", "a1"), ("sig_a2", "a1")) + + +def test_virtual_mux_basic(): + updates = [] + mux_a = MuxA(lambda x, y: updates.append((x, y))) + + # test both the __call__ and multiplex methods trigger + # the appropriate update callback. + mux_a("sig_a1") + mux_a.multiplex("sig_a2", trigger_update=False) + mux_a("") + + clear = PinSetState(off=frozenset({"a0", "a1"})) + a1 = PinSetState(on=frozenset({"a0", "a1"})) + a2 = PinSetState(on=frozenset({"a1"}), off=frozenset({"a0"})) + assert updates == [ + (PinUpdate(PinSetState(), a1), True), + (PinUpdate(PinSetState(), a2), False), + (PinUpdate(PinSetState(), clear), True), + ] + + +def test_virtual_mux_reset(): + """Check that reset sends an update that sets all pins off""" + + updates = [] + mux_a = MuxA(lambda x, y: updates.append((x, y))) + mux_a.reset() + assert updates == [ + (PinUpdate(PinSetState(), PinSetState(off=frozenset({"a1", "a0"}))), True), + ] + + +def test_virtual_mux_invalid_signal(): + """Check an invalid signal raises an error.""" + + mux_a = MuxA() + with pytest.raises(ValueError): + mux_a("invalid signal") + + +def test_invalid_signal_map_raises(): + """A virtual mux needs one of tree_map or list_map defined""" + + class BadMux(VirtualMux): + pass + + with pytest.raises(ValueError): + bm = BadMux() + + +# ############################################################### # Helper dataclasses From 12c562c2bb42cd21f079f3a344e27a57856bd744 Mon Sep 17 00:00:00 2001 From: Clint Lawrence Date: Wed, 12 Jun 2024 09:14:36 +1000 Subject: [PATCH 15/41] I think I have a black version mis-match somewhere --- test/core/test_switching.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/core/test_switching.py b/test/core/test_switching.py index 57c35d52..7ef39f89 100644 --- a/test/core/test_switching.py +++ b/test/core/test_switching.py @@ -123,6 +123,7 @@ class NestedVirtualMux(VirtualMux): class MuxA(VirtualMux): """A mux definitioned used by a few scripts""" + pin_list = ("a0", "a1") map_list = (("sig_a1", "a0", "a1"), ("sig_a2", "a1")) From 73691ee8d813b79582a76951220c8c345c41aa32 Mon Sep 17 00:00:00 2001 From: Clint Lawrence Date: Wed, 12 Jun 2024 09:33:56 +1000 Subject: [PATCH 16/41] Add a test for VirtualSwitch --- test/core/test_switching.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/test/core/test_switching.py b/test/core/test_switching.py index 7ef39f89..956dd78b 100644 --- a/test/core/test_switching.py +++ b/test/core/test_switching.py @@ -4,6 +4,7 @@ bit_generator, PinSetState, PinUpdate, + VirtualSwitch, ) import pytest @@ -176,6 +177,34 @@ class BadMux(VirtualMux): with pytest.raises(ValueError): bm = BadMux() +# ############################################################### +# VirtualSwitch Behaviour + + +def test_virtual_switch(): + class Sw(VirtualSwitch): + pin_name = "x" + updates = [] + sw = Sw(lambda x, y: updates.append((x, y))) + + sw(True) + sw(False) + sw("TRUE", trigger_update=False) + sw("FALSE") + sw("") + + on = PinSetState(on=frozenset("x")) + off = PinSetState(off=frozenset("x")) + + assert updates == [ + (PinUpdate(PinSetState(), on), True), + (PinUpdate(PinSetState(), off), True), + (PinUpdate(PinSetState(), on), False), + (PinUpdate(PinSetState(), off), True), + (PinUpdate(PinSetState(), off), True), + ] + + # ############################################################### # Helper dataclasses From 3d87f63e9bab4b98630f61b746e808c5ca473aa6 Mon Sep 17 00:00:00 2001 From: Clint Lawrence Date: Wed, 12 Jun 2024 09:43:56 +1000 Subject: [PATCH 17/41] Change VirtualSwitch to use On/Off --- src/fixate/core/switching.py | 12 +++++++++--- test/core/test_switching.py | 7 ++++--- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/fixate/core/switching.py b/src/fixate/core/switching.py index a65eb161..8fdf8eba 100644 --- a/src/fixate/core/switching.py +++ b/src/fixate/core/switching.py @@ -446,19 +446,25 @@ class VirtualSwitch(VirtualMux): """ pin_name: Pin = "" - map_tree = ("FALSE", "TRUE") + map_tree = ("Off", "On") def multiplex( self, signal_output: Union[Signal, bool], trigger_update: bool = True ) -> None: if signal_output is True: - signal = "TRUE" + signal = "On" elif signal_output is False: - signal = "FALSE" + signal = "Off" else: signal = signal_output super().multiplex(signal, trigger_update=trigger_update) + def __call__( + self, signal_output: Union[Signal, bool], trigger_update: bool = True + ) -> None: + """Override call to set the type on signal_output correctly.""" + self.multiplex(signal_output, trigger_update) + def __init__( self, update_pins: Optional[PinUpdateCallback] = None, diff --git a/test/core/test_switching.py b/test/core/test_switching.py index 956dd78b..2f34e521 100644 --- a/test/core/test_switching.py +++ b/test/core/test_switching.py @@ -177,6 +177,7 @@ class BadMux(VirtualMux): with pytest.raises(ValueError): bm = BadMux() + # ############################################################### # VirtualSwitch Behaviour @@ -184,13 +185,14 @@ class BadMux(VirtualMux): def test_virtual_switch(): class Sw(VirtualSwitch): pin_name = "x" + updates = [] sw = Sw(lambda x, y: updates.append((x, y))) sw(True) sw(False) - sw("TRUE", trigger_update=False) - sw("FALSE") + sw("On", trigger_update=False) + sw("Off") sw("") on = PinSetState(on=frozenset("x")) @@ -205,7 +207,6 @@ class Sw(VirtualSwitch): ] - # ############################################################### # Helper dataclasses From 1ac5a3149ed282850dcbe410cb5d49a33dedb7e8 Mon Sep 17 00:00:00 2001 From: Clint Lawrence Date: Wed, 12 Jun 2024 09:57:57 +1000 Subject: [PATCH 18/41] add test for RelayMatrixMux --- test/core/test_switching.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/test/core/test_switching.py b/test/core/test_switching.py index 2f34e521..59610219 100644 --- a/test/core/test_switching.py +++ b/test/core/test_switching.py @@ -5,6 +5,7 @@ PinSetState, PinUpdate, VirtualSwitch, + RelayMatrixMux, ) import pytest @@ -207,6 +208,37 @@ class Sw(VirtualSwitch): ] +# ############################################################### +# RelayMatrixMux Behaviour + + +def test_relay_matrix_mux(): + class RMMux(RelayMatrixMux): + pin_list = ("a", "b") + map_list = ( + ("sig1", "a"), + ("sig2", "b"), + ) + + sig1 = PinSetState(off=frozenset("b"), on=frozenset("a")) + sig2 = PinSetState(off=frozenset("a"), on=frozenset("b")) + off = PinSetState(off=frozenset("ab")) + + updates = [] + rm = RMMux(lambda x, y: updates.append((x, y))) + rm("sig1") + rm("sig2") + + # compared to the standard mux, the setup of the PinUpdate + # sets all pins off. The standard mux does nothing for the + # setup phase. And we technically shouldn't compare floats + # for equality, but it should be fine here... until it's not :D + assert updates == [ + (PinUpdate(off, sig1, 0.01), True), + (PinUpdate(off, sig2, 0.01), True), + ] + + # ############################################################### # Helper dataclasses From 250be7e0977769ec1e9189c7a214336d7d7578dd Mon Sep 17 00:00:00 2001 From: Clint Lawrence Date: Wed, 12 Jun 2024 10:36:57 +1000 Subject: [PATCH 19/41] Don't allow explicitly defining signal '' --- src/fixate/core/switching.py | 11 +++++++++-- test/core/test_switching.py | 18 ++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/fixate/core/switching.py b/src/fixate/core/switching.py index 8fdf8eba..855b1f0c 100644 --- a/src/fixate/core/switching.py +++ b/src/fixate/core/switching.py @@ -118,10 +118,17 @@ def __init__(self, update_pins: Optional[PinUpdateCallback] = None): self._signal_map: SignalMap = self._map_signals() - # If it wasn't already defined, define the implicit signal "" which - # can be used to signify no pins active. + # Define the implicit signal "" which can be used to turn off all pins. + # If the signal map already has this defined, raise an error. In the old + # implementation, it allows the map to set this, but when switching the + # behaviour was hard coded. Any mux that changed the definition would + # have silently done the "wrong" thing. We can revisit this if we find + # a good application to override, but for now, don't silently allow something + # that probably isn't correct. if "" not in self._signal_map: self._signal_map[""] = frozenset() + else: + raise ValueError('The empty signal, "", should not be explicitly defined') def __call__(self, signal_output: Signal, trigger_update: bool = True) -> None: """ diff --git a/test/core/test_switching.py b/test/core/test_switching.py index 59610219..bcab68ab 100644 --- a/test/core/test_switching.py +++ b/test/core/test_switching.py @@ -119,6 +119,24 @@ class NestedVirtualMux(VirtualMux): } +def test_empty_signal_should_not_be_defined(): + class BadMux1(VirtualMux): + pin_list = ["x"] + map_list = [["", "x"]] + + class BadMux2(VirtualMux): + pin_list = ["x"] + # Even though this is the "correct" definition, it is still + # not allowed + map_list = [[""]] + + with pytest.raises(ValueError): + BadMux1() + + with pytest.raises(ValueError): + BadMux2() + + # ############################################################### # VirtualMux Behaviour From 28895f7ba80871d6df72761e7cec0c73d09d5d0c Mon Sep 17 00:00:00 2001 From: Clint Lawrence Date: Wed, 12 Jun 2024 11:28:45 +1000 Subject: [PATCH 20/41] Don't allow 'default_signal' on VirtualMux --- src/fixate/core/switching.py | 3 +++ test/core/test_switching.py | 10 ++++++++++ 2 files changed, 13 insertions(+) diff --git a/src/fixate/core/switching.py b/src/fixate/core/switching.py index 855b1f0c..6394cef5 100644 --- a/src/fixate/core/switching.py +++ b/src/fixate/core/switching.py @@ -130,6 +130,9 @@ def __init__(self, update_pins: Optional[PinUpdateCallback] = None): else: raise ValueError('The empty signal, "", should not be explicitly defined') + if hasattr(self, "default_signal"): + raise ValueError("'default_signal' should not be set on a VirtualMux") + def __call__(self, signal_output: Signal, trigger_update: bool = True) -> None: """ Convenience to avoid having to type jig.mux..multiplex. diff --git a/test/core/test_switching.py b/test/core/test_switching.py index bcab68ab..55e5b1fa 100644 --- a/test/core/test_switching.py +++ b/test/core/test_switching.py @@ -137,6 +137,16 @@ class BadMux2(VirtualMux): BadMux2() +def test_default_signal_on_mux_raises(): + class BadMux(VirtualMux): + pin_list = ["x"] + map_list = [["sig1", "x"]] + default_signal = "sig1" + + with pytest.raises(ValueError): + BadMux() + + # ############################################################### # VirtualMux Behaviour From d6550aa8cbe692311d3e24834b85118e24c657d2 Mon Sep 17 00:00:00 2001 From: Clint Lawrence Date: Wed, 12 Jun 2024 19:28:23 +1000 Subject: [PATCH 21/41] A few unrelated changes - Don't allow pin_defaults on AddressHandler - Change RelayMatrixAddressHandler to FTDIAddressHandler - FTDIAddressHandler can be passed a pin list - Provide some helper functions to help build common pin_lists --- script.py | 30 +++----- src/fixate/core/switching.py | 47 +++++++++++++ src/fixate/drivers/handlers.py | 34 ++------- test/core/test_switching.py | 125 ++++++++++++++++++++++++++++++++- 4 files changed, 185 insertions(+), 51 deletions(-) diff --git a/script.py b/script.py index 7fc1ebe3..25a0ad83 100644 --- a/script.py +++ b/script.py @@ -26,39 +26,29 @@ class MuxTwo(VirtualMux): ), ) -class MuxThree(VirtualMux): - pin_list = ("x101", "x123") - map_list = (("", "x101", "x123"),) + +class MuxThree(VirtualSwitch): + pin_name = "x101" + class Handler(PinValueAddressHandler): - pin_list = ("x0", "x1", "x2", "x3", "x4", "x5") - + pin_list = ("x0", "x1", "x2", "x3", "x4", "x5", "x101") -# Problem! +# Note! # our existing scripts/jig driver, the name of the mux is the # class of the virtual mux. This scheme below will not allow that -# to work. -# Assuming an existing script with a mux called NewVirtualMux -# 1. Update every reference in the script -# dm.jig.mux.NewVirtualMux -> dm.jig.mux.new_virtual_mux -# 2. Change the class name, but keep the attribute name -# @dataclass -# class JigMuxGroup(MuxGroup): -# NewVirtualMux: _NewVirtualMux -# Then the references in the script stay this same. -# jig.mux.NewVirtualMux -# 3. Change the attribute name on mux, like in the example below, -# but add some compatibility code to MuxGroup base class so that -# attribute lookups that match the Class of one of its attributes -# get magically mapped to the correct attribute. +# to work. Instead, define an attribute name on the MuxGroup @dataclass class JigMuxGroup(MuxGroup): mux_one: MuxOne = field(default_factory=MuxOne) mux_two: MuxTwo = field(default_factory=MuxTwo) mux_three: MuxThree = field(default_factory=MuxThree) + jig = JigDriver(JigMuxGroup, [Handler()]) jig.mux.mux_one("sig2", trigger_update=False) jig.mux.mux_two("sig5") +jig.mux.mux_three("On") +jig.mux.mux_three(False) diff --git a/src/fixate/core/switching.py b/src/fixate/core/switching.py index 6394cef5..ccc31e5d 100644 --- a/src/fixate/core/switching.py +++ b/src/fixate/core/switching.py @@ -43,6 +43,7 @@ Dict, FrozenSet, Set, + Iterable, ) from dataclasses import dataclass from functools import reduce @@ -514,6 +515,10 @@ class AddressHandler: pin_list: Sequence[Pin] = () + def __init__(self): + if hasattr(self, "pin_defaults"): + raise ValueError("'pin_defaults' should not be set on a AddressHandler") + def set_pins(self, pins: Collection[Pin]) -> None: """ Called by the VirtualAddressMap to write out pin changes. @@ -757,3 +762,45 @@ def generate_bit_sets(bits: Sequence[T]) -> Generator[set[T], None, None]: def bit_generator() -> Generator[int, None, None]: """b1, b10, b100, b1000, ...""" return (1 << counter for counter in itertools.count()) + + +def generate_pin_group( + group_designator: int, *, pin_count: int = 16, prefix: str = "" +) -> tuple[Pin, ...]: + """ + A helper to create pin names groups of pins, especially relay matrices. + + By default, returns 16 pin names, suitable for a 16 relay matrix. + Changing the default values for pin_count and prefix can be used to generate + alternate naming schemes. + + generate_pin_group(1) -> ("1K1", "1K2", "1K3", ..., "1K16") + generate_pin_group(3, prefix="RM") -> ("RM1K1", "RM1K2", "RM1K3", ..., "RM1K16") + generate_pin_group(5, pin_count=8, prefix="U") -> ("U3K1", "U3K2, "U3K3", ..., "U3K8") + """ + return tuple( + f"{prefix}{group_designator}K{relay}" for relay in range(1, pin_count + 1) + ) + + +def generate_relay_matrix_pin_list( + designators: Iterable[int], prefix: str = "" +) -> tuple[Pin, ...]: + """ + Create a pin list for multiple relay matrix modules. + + Each module is allocated 16 pins + generate_relay_matrix_pin_list([1,2,3]) -> + ("1K1", "1K2", ..., "1K16", "2K1", ..., "2K16", "3K1", ..., "3K16") + + generate_relay_matrix_pin_list([2,3,1], prefix="RM") -> + ("RM2K1", "RM2K2", ..., "RM2K16", "RM3K1", ..., "RM3K16", "RM1K1", ..., "RM1K16") + + Combination generate_relay_matrix_pin_list and generate_pin_group to create pins + as needed for a specific jig configuration. + """ + return tuple( + itertools.chain.from_iterable( + generate_pin_group(rm_number, prefix=prefix) for rm_number in designators + ) + ) diff --git a/src/fixate/drivers/handlers.py b/src/fixate/drivers/handlers.py index f6f6af4e..3a412a6d 100644 --- a/src/fixate/drivers/handlers.py +++ b/src/fixate/drivers/handlers.py @@ -5,32 +5,12 @@ from __future__ import annotations from typing import Sequence -import itertools from fixate.core.switching import Pin, PinValueAddressHandler from fixate.drivers import ftdi -def _pins_for_one_relay_matrix(relay_matrix_num: int) -> list[Pin]: - """ - A helper to create pin names for relay matrix cards. - - Returns 16 pin names. If relay_matrix_num is 1: - 1K1, 1K2, 1K3, ..., 1K16 - """ - return [f"{relay_matrix_num}K{relay}" for relay in range(1, 17)] - - -# This is a real quick test. Not worth the effort unravelling -# imports when ftdi isn't importable, just to get this into a -# proper test right now... -__expected = ( - "3K1 3K2 3K3 3K4 3K5 3K6 3K7 3K8 3K9 3K10 3K11 3K12 3K13 3K14 3K15 3K16".split() -) -assert _pins_for_one_relay_matrix(3) == __expected - - -class RelayMatrixAddressHandler(PinValueAddressHandler): +class FTDIAddressHandler(PinValueAddressHandler): """ An address handler which uses the ftdi driver to control pins. @@ -42,16 +22,10 @@ class RelayMatrixAddressHandler(PinValueAddressHandler): def __init__( self, ftdi_description: str, - relay_matrix_count: int, - extra_pins: Sequence[Pin] = tuple(), + pins: Sequence[Pin] = tuple(), ) -> None: - relay_matrix_pin_list = tuple( - itertools.chain.from_iterable( - _pins_for_one_relay_matrix(rm_number) - for rm_number in range(1, relay_matrix_count + 1) - ) - ) - self.pin_list = relay_matrix_pin_list + tuple(extra_pins) + + self.pin_list = tuple(pins) # call the base class super _after_ we create the pin list super().__init__() diff --git a/test/core/test_switching.py b/test/core/test_switching.py index 55e5b1fa..5f1dcff7 100644 --- a/test/core/test_switching.py +++ b/test/core/test_switching.py @@ -6,12 +6,15 @@ PinUpdate, VirtualSwitch, RelayMatrixMux, + PinValueAddressHandler, + generate_pin_group, + generate_relay_matrix_pin_list, ) import pytest ################################################################ -# generate_bit_sets +# helper to generate data def test_generate_bit_sets_empty(): @@ -45,6 +48,113 @@ def test_bit_generator(): assert actual == expected +def test_generate_pin_group(): + assert list(generate_pin_group(3)) == ( + "3K1 3K2 3K3 3K4 3K5 3K6 3K7 3K8 3K9 3K10 3K11 3K12 3K13 3K14 3K15 3K16".split() + ) + assert list(generate_pin_group(1, prefix="RM")) == ( + "RM1K1 RM1K2 RM1K3 RM1K4 RM1K5 RM1K6 RM1K7 RM1K8 RM1K9 RM1K10 RM1K11 RM1K12 RM1K13 RM1K14 RM1K15 RM1K16".split() + ) + assert list(generate_pin_group(10, pin_count=8, prefix="U")) == ( + "U10K1 U10K2 U10K3 U10K4 U10K5 U10K6 U10K7 U10K8".split() + ) + + +# fmt:off +large_pin_list_example1 = [ + "RC1K1", "RC1K2", "RC1K3", "RC1K4", "RC1K5", "RC1K6", "RC1K7", "RC1K8","RC1K9", "RC1K10", "RC1K11", "RC1K12", "RC1K13", "RC1K14", "RC1K15", "RC1K16", + "RC2K1", "RC2K2", "RC2K3", "RC2K4", "RC2K5", "RC2K6", "RC2K7", "RC2K8", "RC2K9", "RC2K10", "RC2K11", "RC2K12", "RC2K13", "RC2K14", "RC2K15", "RC2K16", + "RA1K1", "RA1K2", "RA1K3", "RA1K4", "RA1K5", "RA1K6", "RA1K7", "RA1K8", "RA1K9", "RA1K10", "RA1K11", "RA1K12", "RA1K13", "RA1K14", "RA1K15", "RA1K16", + "RA2K1", "RA2K2", "RA2K3", "RA2K4", "RA2K5", "RA2K6", "RA2K7", "RA2K8", "RA2K9", "RA2K10", "RA2K11", "RA2K12", "RA2K13", "RA2K14", "RA2K15", "RA2K16", + "RA3K1", "RA3K2", "RA3K3", "RA3K4", "RA3K5", "RA3K6", "RA3K7", "RA3K8", "RA3K9", "RA3K10", "RA3K11", "RA3K12", "RA3K13", "RA3K14", "RA3K15", "RA3K16", + "RP1K1", "RP1K2", "RP1K3", "RP1K4", "RP1K5", "RP1K6", "RP1K7", "RP1K8", "RP1K9", "RP1K10", "RP1K11", "RP1K12", "RP1K13", "RP1K14", "RP1K15", "RP1K16", + "RP2K1", "RP2K2", "RP2K3", "RP2K4", "RP2K5", "RP2K6", "RP2K7", "RP2K8", "RP2K9", "RP2K10", "RP2K11", "RP2K12", "RP2K13", "RP2K14", "RP2K15", "RP2K16", + "RH1K1", "RH1K2", "RH1K3", "RH1K4", "RH1K5", "RH1K6", "RH1K7", "RH1K8", "RH1K9", "RH1K10", "RH1K11", "RH1K12", "RH1K13", "RH1K14", "RH1K15", "RH1K16", +] + +large_pin_list_example2 = [ + # RM19 + "RM19K1", "RM19K2", "RM19K3", "RM19K4", "RM19K5", "RM19K6", "RM19K7", "RM19K8", + "RM19K9", "RM19K10", "RM19K11", "RM19K12", "RM19K13", "RM19K14", "RM19K15", "RM19K16", + # RM18 + "RM18K1", "RM18K2", "RM18K3", "RM18K4", "RM18K5", "RM18K6", "RM18K7", "RM18K8", + "RM18K9", "RM18K10", "RM18K11", "RM18K12", "RM18K13", "RM18K14", "RM18K15", "RM18K16", + # RM17 + "RM17K1", "RM17K2", "RM17K3", "RM17K4", "RM17K5", "RM17K6", "RM17K7", "RM17K8", + "RM17K9", "RM17K10", "RM17K11", "RM17K12", "RM17K13", "RM17K14", "RM17K15", "RM17K16", + # RM16 + "RM16K1", "RM16K2", "RM16K3", "RM16K4", "RM16K5", "RM16K6", "RM16K7", "RM16K8", + "RM16K9", "RM16K10", "RM16K11", "RM16K12", "RM16K13", "RM16K14", "RM16K15", "RM16K16", + # RM15 + "RM15K1", "RM15K2", "RM15K3", "RM15K4", "RM15K5", "RM15K6", "RM15K7", "RM15K8", + "RM15K9", "RM15K10", "RM15K11", "RM15K12", "RM15K13", "RM15K14", "RM15K15", "RM15K16", + # RM14 + "RM14K1", "RM14K2", "RM14K3", "RM14K4", "RM14K5", "RM14K6", "RM14K7", "RM14K8", + "RM14K9", "RM14K10", "RM14K11", "RM14K12", "RM14K13", "RM14K14", "RM14K15", "RM14K16", + # RM13 + "RM13K1", "RM13K2", "RM13K3", "RM13K4", "RM13K5", "RM13K6", "RM13K7", "RM13K8", + "RM13K9", "RM13K10", "RM13K11", "RM13K12", "RM13K13", "RM13K14", "RM13K15", "RM13K16", + # RM10 + "RM10K1", "RM10K2", "RM10K3", "RM10K4", "RM10K5", "RM10K6", "RM10K7", "RM10K8", + "RM10K9", "RM10K10", "RM10K11", "RM10K12", "RM10K13", "RM10K14", "RM10K15", "RM10K16", + # RM11 + "RM11K1", "RM11K2", "RM11K3", "RM11K4", "RM11K5", "RM11K6", "RM11K7", "RM11K8", + "RM11K9", "RM11K10", "RM11K11", "RM11K12", "RM11K13", "RM11K14", "RM11K15", "RM11K16", + # RM12 + "RM12K1", "RM12K2", "RM12K3", "RM12K4", "RM12K5", "RM12K6", "RM12K7", "RM12K8", + "RM12K9", "RM12K10", "RM12K11", "RM12K12", "RM12K13", "RM12K14", "RM12K15", "RM12K16", + # RM1 + "RM1K1", "RM1K2", "RM1K3", "RM1K4", "RM1K5", "RM1K6", "RM1K7", "RM1K8", + "RM1K9", "RM1K10", "RM1K11", "RM1K12", "RM1K13", "RM1K14", "RM1K15", "RM1K16", + # RM2 + "RM2K1", "RM2K2", "RM2K3", "RM2K4", "RM2K5", "RM2K6", "RM2K7", "RM2K8", + "RM2K9", "RM2K10", "RM2K11", "RM2K12", "RM2K13", "RM2K14", "RM2K15", "RM2K16", + # RM3 + "RM3K1", "RM3K2", "RM3K3", "RM3K4", "RM3K5", "RM3K6", "RM3K7", "RM3K8", + "RM3K9", "RM3K10", "RM3K11", "RM3K12", "RM3K13", "RM3K14", "RM3K15", "RM3K16", + # RM4 + "RM4K1", "RM4K2", "RM4K3", "RM4K4", "RM4K5", "RM4K6", "RM4K7", "RM4K8", + "RM4K9", "RM4K10", "RM4K11", "RM4K12", "RM4K13", "RM4K14", "RM4K15", "RM4K16", + # RM5 + "RM5K1", "RM5K2", "RM5K3", "RM5K4", "RM5K5", "RM5K6", "RM5K7", "RM5K8", + "RM5K9", "RM5K10", "RM5K11", "RM5K12", "RM5K13", "RM5K14", "RM5K15", "RM5K16", + # RM6 + "RM6K1", "RM6K2", "RM6K3", "RM6K4", "RM6K5", "RM6K6", "RM6K7", "RM6K8", + "RM6K9", "RM6K10", "RM6K11", "RM6K12", "RM6K13", "RM6K14", "RM6K15", "RM6K16", + # RM7 + "RM7K1", "RM7K2", "RM7K3", "RM7K4", "RM7K5", "RM7K6", "RM7K7", "RM7K8", + "RM7K9", "RM7K10", "RM7K11", "RM7K12", "RM7K13", "RM7K14", "RM7K15", "RM7K16", + # RM8 + "RM8K1", "RM8K2", "RM8K3", "RM8K4", "RM8K5", "RM8K6", "RM8K7", "RM8K8", + "RM8K9", "RM8K10", "RM8K11", "RM8K12", "RM8K13", "RM8K14", "RM8K15", "RM8K16", + # RM9 + "RM9K1", "RM9K2", "RM9K3", "RM9K4", "RM9K5", "RM9K6", "RM9K7", "RM9K8", + "RM9K9", "RM9K10", "RM9K11", "RM9K12", "RM9K13", "RM9K14", "RM9K15", "RM9K16", + # U2 + "U2K1", "U2K3", "U2K4", "U2K5", "U2K6", "U2SC6", "U2SC7", "U2SC8", +] +# fmt:on + + +def test_generate_relay_matrix_pin_list(): + pin_list = list( + generate_relay_matrix_pin_list([1, 2], "RC") + + generate_relay_matrix_pin_list([1, 2, 3], "RA") + + generate_relay_matrix_pin_list([1, 2], "RP") + + generate_relay_matrix_pin_list([1], "RH") + ) + assert pin_list == large_pin_list_example1 + + pin_list = list( + generate_relay_matrix_pin_list( + [19, 18, 17, 16, 15, 14, 13, 10, 11, 12, 1, 2, 3, 4, 5, 6, 7, 8, 9], + prefix="RM", + ) + + ("U2K1", "U2K3", "U2K4", "U2K5", "U2K6", "U2SC6", "U2SC7", "U2SC8") + ) + assert pin_list == large_pin_list_example2 + + ################################################################ # virtual mux definitions @@ -267,6 +377,19 @@ class RMMux(RelayMatrixMux): ] +# ############################################################### +# AddressHandler + + +def test_pin_default_on_address_handler_raise(): + class BadHandler(PinValueAddressHandler): + pin_list = ("x", "y") + pin_defaults = ("x",) + + with pytest.raises(ValueError): + BadHandler() + + # ############################################################### # Helper dataclasses From f4a0db0d0ce14d74d5878abd501d94b758d71d77 Mon Sep 17 00:00:00 2001 From: Clint Lawrence Date: Wed, 12 Jun 2024 19:36:34 +1000 Subject: [PATCH 22/41] Fix a missing type hint --- src/fixate/core/switching.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fixate/core/switching.py b/src/fixate/core/switching.py index ccc31e5d..a6018e13 100644 --- a/src/fixate/core/switching.py +++ b/src/fixate/core/switching.py @@ -515,7 +515,7 @@ class AddressHandler: pin_list: Sequence[Pin] = () - def __init__(self): + def __init__(self) -> None: if hasattr(self, "pin_defaults"): raise ValueError("'pin_defaults' should not be set on a AddressHandler") From 16b81e3acf806f1cd8624f1cdb37e1043218bcb1 Mon Sep 17 00:00:00 2001 From: Clint Lawrence Date: Thu, 13 Jun 2024 14:27:44 +1000 Subject: [PATCH 23/41] Just for Jason... --- src/fixate/core/switching.py | 9 +++++---- test/core/test_switching.py | 22 ++++++++++++++++++---- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/fixate/core/switching.py b/src/fixate/core/switching.py index a6018e13..b9e968df 100644 --- a/src/fixate/core/switching.py +++ b/src/fixate/core/switching.py @@ -765,7 +765,7 @@ def bit_generator() -> Generator[int, None, None]: def generate_pin_group( - group_designator: int, *, pin_count: int = 16, prefix: str = "" + group_designator: int, *, pin_count: int = 16, prefix: str = "", sep: str = "" ) -> tuple[Pin, ...]: """ A helper to create pin names groups of pins, especially relay matrices. @@ -779,12 +779,12 @@ def generate_pin_group( generate_pin_group(5, pin_count=8, prefix="U") -> ("U3K1", "U3K2, "U3K3", ..., "U3K8") """ return tuple( - f"{prefix}{group_designator}K{relay}" for relay in range(1, pin_count + 1) + f"{prefix}{group_designator}{sep}K{relay}" for relay in range(1, pin_count + 1) ) def generate_relay_matrix_pin_list( - designators: Iterable[int], prefix: str = "" + designators: Iterable[int], *, prefix: str = "", sep: str = "" ) -> tuple[Pin, ...]: """ Create a pin list for multiple relay matrix modules. @@ -801,6 +801,7 @@ def generate_relay_matrix_pin_list( """ return tuple( itertools.chain.from_iterable( - generate_pin_group(rm_number, prefix=prefix) for rm_number in designators + generate_pin_group(rm_number, prefix=prefix, sep=sep) + for rm_number in designators ) ) diff --git a/test/core/test_switching.py b/test/core/test_switching.py index 5f1dcff7..fe55875d 100644 --- a/test/core/test_switching.py +++ b/test/core/test_switching.py @@ -133,15 +133,22 @@ def test_generate_pin_group(): # U2 "U2K1", "U2K3", "U2K4", "U2K5", "U2K6", "U2SC6", "U2SC7", "U2SC8", ] + +large_pin_list_example3 = [ + "RM1_K1", "RM1_K2", "RM1_K3", "RM1_K4", "RM1_K5", "RM1_K6", "RM1_K7", "RM1_K8", + "RM1_K9", "RM1_K10", "RM1_K11", "RM1_K12", "RM1_K13", "RM1_K14", "RM1_K15", "RM1_K16", + "RM2_K1", "RM2_K2", "RM2_K3", "RM2_K4", "RM2_K5", "RM2_K6", "RM2_K7", "RM2_K8", + "RM2_K9", "RM2_K10", "RM2_K11", "RM2_K12", "RM2_K13", "RM2_K14", "RM2_K15", "RM2_K16", +] # fmt:on def test_generate_relay_matrix_pin_list(): pin_list = list( - generate_relay_matrix_pin_list([1, 2], "RC") - + generate_relay_matrix_pin_list([1, 2, 3], "RA") - + generate_relay_matrix_pin_list([1, 2], "RP") - + generate_relay_matrix_pin_list([1], "RH") + generate_relay_matrix_pin_list([1, 2], prefix="RC") + + generate_relay_matrix_pin_list([1, 2, 3], prefix="RA") + + generate_relay_matrix_pin_list([1, 2], prefix="RP") + + generate_relay_matrix_pin_list([1], prefix="RH") ) assert pin_list == large_pin_list_example1 @@ -154,6 +161,13 @@ def test_generate_relay_matrix_pin_list(): ) assert pin_list == large_pin_list_example2 + pin_list = list(generate_relay_matrix_pin_list([1, 2], prefix="RM", sep="_")) + assert pin_list == large_pin_list_example3 + + assert "U5_K1 U5_K2 U5_K3".split() == list( + generate_pin_group(5, pin_count=3, prefix="U", sep="_") + ) + ################################################################ # virtual mux definitions From 03932d34710fa77ecbcad322928fbf86d6543978 Mon Sep 17 00:00:00 2001 From: Clint Lawrence Date: Thu, 13 Jun 2024 14:29:55 +1000 Subject: [PATCH 24/41] Tweak the comments a little bit --- src/fixate/drivers/handlers.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/fixate/drivers/handlers.py b/src/fixate/drivers/handlers.py index 3a412a6d..6c776500 100644 --- a/src/fixate/drivers/handlers.py +++ b/src/fixate/drivers/handlers.py @@ -24,14 +24,13 @@ def __init__( ftdi_description: str, pins: Sequence[Pin] = tuple(), ) -> None: - + # pin_list must be defined before calling the base class __init__ self.pin_list = tuple(pins) - # call the base class super _after_ we create the pin list super().__init__() # how many bytes? enough for every pin to get a bit. We might # end up with some left-over bits. The +7 in the expression - # ensure we round up. + # ensures we round up. bytes_required = (len(self.pin_list) + 7) // 8 self._ftdi = ftdi.open(ftdi_description=ftdi_description) self._ftdi.configure_bit_bang( From 8caead4378408afbcded29d51ae054f672926bb5 Mon Sep 17 00:00:00 2001 From: Clint Lawrence Date: Thu, 13 Jun 2024 16:31:51 +1000 Subject: [PATCH 25/41] Tidy up some debug functions --- src/fixate/core/switching.py | 87 +++++++++++++----------------------- 1 file changed, 32 insertions(+), 55 deletions(-) diff --git a/src/fixate/core/switching.py b/src/fixate/core/switching.py index b9e968df..18af9a09 100644 --- a/src/fixate/core/switching.py +++ b/src/fixate/core/switching.py @@ -173,19 +173,8 @@ def multiplex(self, signal_output: Signal, trigger_update: bool = True) -> None: self.state_update_time = time.time() self._state = signal_output - def switch_through_all_signals(self) -> Generator[str, None, None]: - # not sure if we should keep this. - # probably better to have a method that returns all - # signals and use that in a helper somewhere else that loops and - # switches. I don't like state changes and printing - # buried in a generator. - # This was iterate_mux_paths on the JigDriver, but then it had - # to used internal implementation details. Better to have this - # as a method on VirtualMux - for signal in self._signal_map: - if signal is not None: - self.multiplex(signal) - yield f"{self.__class__.__name__}: {signal}" + def all_signals(self) -> tuple[Signal, ...]: + return tuple(self._signal_map.keys()) def reset(self, trigger_update: bool = True) -> None: self.multiplex("", trigger_update) @@ -621,42 +610,26 @@ def _dispatch_pin_state(self, new_state: PinSetState) -> None: for pin_set, handler in self._handler_pin_sets: handler.set_pins(pin_set & self._active_pins) - def active_pins(self) -> Set[Pin]: - return self._active_pins + def active_pins(self) -> frozenset[Pin]: + return frozenset(self._active_pins) def reset(self) -> None: """ Sets all pins to be inactive. + + Note: this does not change the state of any VirtualMux's, so it is + possible the state of each VirtualMux and its related pins will not + be in sync. """ self._dispatch_pin_state(PinSetState(off=self._all_pins)) def update_input(self) -> None: """ - Iterates through the address_handlers and reads the values back to update the pin values for the digital inputs - :return: + Currently not implemented. A few jigs implement digital input, but not many. It is the intention + to implement this eventually, but for now, the old jig_mapping.py version can be used. """ raise NotImplementedError - # used in a few scripts - def update_pin_by_name( - self, name: Pin, value: bool, trigger_update: bool = True - ) -> None: - raise NotImplementedError - - # not used in any scripts - def update_pins_by_name( - self, pins: Collection[Pin], trigger_update: bool = True - ) -> None: - raise NotImplementedError - - def __getitem__(self, item: Pin) -> bool: - """Get the value of a pin. (only inputs? or state of outputs also?)""" - raise NotImplementedError - - def __setitem__(self, key: Pin, value: bool) -> None: - """Set a pin""" - raise NotImplementedError - class MuxGroup: """ @@ -714,33 +687,37 @@ def __init__( # constructor, and I was hoping to just use a dataclass... mux._update_pins = self.virtual_map.add_update - def __setitem__(self, key: Pin, value: bool) -> None: - self.virtual_map.update_pin_by_name(key, value) - - def __getitem__(self, item: Pin) -> bool: - return self.virtual_map[item] - def close(self) -> None: for handler in self._handlers: handler.close() - def active_pins(self) -> Set[Pin]: + def active_pins(self) -> frozenset[Pin]: return self.virtual_map.active_pins() + def debug_set_pin(self, pin: Pin, value: bool) -> None: + # pin is a str, which is iterable... so we can't just throw it into + # frozen set, or we end up with frozenset deconstructing it! so + # wrap it into another single element list first + if value: + update = PinSetState(on=frozenset([pin])) + else: + update = PinSetState(off=frozenset([pin])) + self.virtual_map.add_update(PinUpdate(final=update)) + + def debug_set_pins( + self, on: Collection[Pin] = frozenset(), off: Collection[Pin] = frozenset() + ) -> None: + update = PinSetState(on=frozenset(on), off=frozenset(off)) + self.virtual_map.add_update(PinUpdate(final=update)) + + def all_mux_signals(self) -> tuple[tuple[VirtualMux, tuple[Signal, ...]], ...]: + return tuple(((mux, mux.all_signals()) for mux in self.mux.get_multiplexers())) + def reset(self) -> None: """ - Reset all pins - - This leaves multiplexers in the current state, which may not - match up with the real pin state. To reset all the multiplexers, - use JigDriver.mux.reset() instead. + Reset all VirtualMux's to the default signal "" (all pins off) """ - self.virtual_map.reset() - - def iterate_all_mux_paths(self) -> Generator[str, None, None]: - for _, mux in self.mux.__dict__.items(): - if isinstance(mux, VirtualMux): - yield from mux.switch_through_all_signals() + self.mux.reset() T = TypeVar("T") From db289a123a70a25e647a34d7cc608e4264135512 Mon Sep 17 00:00:00 2001 From: Clint Lawrence Date: Thu, 13 Jun 2024 17:29:05 +1000 Subject: [PATCH 26/41] Add VirtualMux.wait_at_least() --- src/fixate/core/switching.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/fixate/core/switching.py b/src/fixate/core/switching.py index 18af9a09..6b5d0fd3 100644 --- a/src/fixate/core/switching.py +++ b/src/fixate/core/switching.py @@ -42,14 +42,12 @@ Collection, Dict, FrozenSet, - Set, Iterable, ) from dataclasses import dataclass from functools import reduce from operator import or_ - Signal = str Pin = str PinList = Sequence[Pin] @@ -98,10 +96,7 @@ class VirtualMux: # These methods are the public API for the class def __init__(self, update_pins: Optional[PinUpdateCallback] = None): - # The last time this mux changed state. This is used in some jigs - # to enforce a minimum settling time. Perhaps is would be nice to - # deprecate this and add a `settle_at_least()` method? - self.state_update_time = 0.0 # time.time() + self._last_update_time = time.monotonic() self._update_pins: PinUpdateCallback if update_pins is None: @@ -155,9 +150,6 @@ def multiplex(self, signal_output: Signal, trigger_update: bool = True) -> None: multiple mux changes can be set and then when trigger_update is finally set to True all changes will happen at once. - If the signal_output is different to the previous state, - self.state_update_time is updated to the current time. - In general, subclasses should not override. (VirtualSwitch does, but then delegates the real work to this method to ensure consistent behaviour.) """ @@ -170,7 +162,7 @@ def multiplex(self, signal_output: Signal, trigger_update: bool = True) -> None: setup, final = self._calculate_pins(self._state, signal_output) self._update_pins(PinUpdate(setup, final, self.clearing_time), trigger_update) if signal_output != self._state: - self.state_update_time = time.time() + self._last_update_time = time.monotonic() self._state = signal_output def all_signals(self) -> tuple[Signal, ...]: @@ -179,6 +171,18 @@ def all_signals(self) -> tuple[Signal, ...]: def reset(self, trigger_update: bool = True) -> None: self.multiplex("", trigger_update) + def wait_at_least(self, duration: float) -> None: + """ + Ensure at least `duration` seconds have elapsed since the signal was switched. + + This can be used to ensure a minimum settling time has passed since + a particular signal was enabled. + """ + now = time.monotonic() + wait_until = self._last_update_time + duration + if wait_until > now: + time.sleep(wait_until - now) + ########################################################################### # The following methods are potential candidates to override in a subclass From f464c4e3c5841bb9ff98fae8ed6f937e31f4cec6 Mon Sep 17 00:00:00 2001 From: Clint Lawrence Date: Fri, 14 Jun 2024 10:52:02 +1000 Subject: [PATCH 27/41] Add tests for VirtualAddressMap --- src/fixate/core/switching.py | 4 ++ test/core/test_switching.py | 109 +++++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+) diff --git a/src/fixate/core/switching.py b/src/fixate/core/switching.py index 6b5d0fd3..2a58dae3 100644 --- a/src/fixate/core/switching.py +++ b/src/fixate/core/switching.py @@ -612,6 +612,10 @@ def _dispatch_pin_state(self, new_state: PinSetState) -> None: if new_active_pins != self._active_pins: self._active_pins = new_active_pins for pin_set, handler in self._handler_pin_sets: + # Note that we might send an empty set here. We need to do that + # so if there are pins to clear, they get cleared. This might + # end up in redundant handler updates, but unless we track active_pins + # per-handler I don't think we can avoid that. handler.set_pins(pin_set & self._active_pins) def active_pins(self) -> frozenset[Pin]: diff --git a/test/core/test_switching.py b/test/core/test_switching.py index fe55875d..d7c0d7f2 100644 --- a/test/core/test_switching.py +++ b/test/core/test_switching.py @@ -9,6 +9,8 @@ PinValueAddressHandler, generate_pin_group, generate_relay_matrix_pin_list, + AddressHandler, + VirtualAddressMap, ) import pytest @@ -404,6 +406,113 @@ class BadHandler(PinValueAddressHandler): BadHandler() +# ############################################################### +# VirtualAddressMap + + +class TestHandler(AddressHandler): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.updates = [] + + def set_pins(self, pins): + self.updates.append(pins) + + +class HandlerXY(TestHandler): + pin_list = ("x", "y") + + +class HandlerAB(TestHandler): + pin_list = ("a", "b") + + +def test_virtual_address_map_init_no_active_pins(): + vam = VirtualAddressMap([HandlerAB()]) + assert vam.active_pins() == frozenset() + + +def test_virtual_address_map_single_handler(): + """ + Go through some basic address map operations with a single handler + - set pins on, check it was sent to the handler + - do a reset and check it was sent to the handler + - ensure active_pins is as expected at each step + """ + ab = HandlerAB() + vam = VirtualAddressMap([ab]) + vam.add_update(PinUpdate(final=PinSetState(on=frozenset("ab")))) + assert ab.updates[0] == frozenset("ab") + assert vam.active_pins() == frozenset("ab") + vam.reset() + assert ab.updates[1] == frozenset() + assert vam.active_pins() == frozenset() + + +def test_virtual_address_map_single_handler_delay_trigger(): + """ + Go through some basic address map operations with a single handler, + but this time, we split the operation up into steps using + trigger_update = False. + """ + ab = HandlerAB() + vam = VirtualAddressMap([ab]) + vam.add_update( + PinUpdate(final=PinSetState(on=frozenset("a"))), trigger_update=False + ) + assert len(ab.updates) == 0 + vam.add_update(PinUpdate(final=PinSetState(on=frozenset("b"))), trigger_update=True) + assert len(ab.updates) == 1 + assert ab.updates[0] == frozenset("ab") + + +def test_virtual_address_map_setup_then_final(): + """ + Go through some basic address map operations with a single handler, + but this time, we split the operation up into steps using + trigger_update = False. + """ + ab = HandlerAB() + vam = VirtualAddressMap([ab]) + vam.add_update( + PinUpdate( + setup=PinSetState(on=frozenset("b")), + final=PinSetState(on=frozenset("a")), # note we are not turning b off here + ) + ) + assert len(ab.updates) == 2 + assert ab.updates[0] == frozenset("b") + assert ab.updates[1] == frozenset("ab") + + +def test_virtual_address_map_multiple_handlers(): + ab = HandlerAB() + xy = HandlerXY() + vam = VirtualAddressMap([ab, xy]) + vam.add_update( + PinUpdate( + setup=PinSetState(on=frozenset("b")), + final=PinSetState(on=frozenset("ay")), + ), + ) + assert len(ab.updates) == 2 + assert len(xy.updates) == 2 + assert ab.updates[0] == frozenset("b") + assert ab.updates[1] == frozenset("ab") + assert xy.updates[0] == frozenset() + assert xy.updates[1] == frozenset("y") + + vam.add_update( + PinUpdate( + final=PinSetState(off=frozenset("ay"), on=frozenset("x")), + ), + ) + assert len(ab.updates) == 3 + assert len(xy.updates) == 3 + assert ab.updates[2] == frozenset("b") + assert xy.updates[2] == frozenset("x") + + # ############################################################### # Helper dataclasses From bf50400ea60ed6c95b3ee3ab4b4411b24bd5d4f0 Mon Sep 17 00:00:00 2001 From: Clint Lawrence Date: Fri, 14 Jun 2024 14:00:04 +1000 Subject: [PATCH 28/41] Add some notes about ftdi baudrate --- src/fixate/drivers/handlers.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/fixate/drivers/handlers.py b/src/fixate/drivers/handlers.py index 6c776500..ae03e74e 100644 --- a/src/fixate/drivers/handlers.py +++ b/src/fixate/drivers/handlers.py @@ -40,7 +40,15 @@ def __init__( clk_mask=2, latch_mask=1, ) - self._ftdi.baud_rate = 115200 + # Measurement of baudrate vs bit-bang. The programming manual say 16 x, but that + # only appears to be true for lower clock rates. Keeping the actual value at 115200 + # since that was used regularly in the past + # baudrate bit-bang update rate + # 1_000_000 ~2 MHz + # 750_000 ~2.4 MHz + # 115_200 ~926 kHz + # 10_000 ~160 kHz + self._ftdi.baud_rate = 115_200 def close(self) -> None: self._ftdi.close() From 7917808b39e4a7b985fe967e1a5a101c0c0e94dc Mon Sep 17 00:00:00 2001 From: Clint Lawrence Date: Fri, 14 Jun 2024 14:29:10 +1000 Subject: [PATCH 29/41] Rename signal_output to signal --- src/fixate/core/switching.py | 38 +++++++++++++++++------------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/src/fixate/core/switching.py b/src/fixate/core/switching.py index 2a58dae3..8cefbcd1 100644 --- a/src/fixate/core/switching.py +++ b/src/fixate/core/switching.py @@ -129,18 +129,18 @@ def __init__(self, update_pins: Optional[PinUpdateCallback] = None): if hasattr(self, "default_signal"): raise ValueError("'default_signal' should not be set on a VirtualMux") - def __call__(self, signal_output: Signal, trigger_update: bool = True) -> None: + def __call__(self, signal: Signal, trigger_update: bool = True) -> None: """ Convenience to avoid having to type jig.mux..multiplex. With this you can just type jig.mux. which is a small, but useful saving for the most common method call. """ - self.multiplex(signal_output, trigger_update) + self.multiplex(signal, trigger_update) - def multiplex(self, signal_output: Signal, trigger_update: bool = True) -> None: + def multiplex(self, signal: Signal, trigger_update: bool = True) -> None: """ - Update the multiplexer state to signal_output. + Update the multiplexer state to signal. The update is a two-step processes. By default, the change happens on the second step. This can be modified by subclassing and overriding the @@ -153,17 +153,15 @@ def multiplex(self, signal_output: Signal, trigger_update: bool = True) -> None: In general, subclasses should not override. (VirtualSwitch does, but then delegates the real work to this method to ensure consistent behaviour.) """ - if signal_output not in self._signal_map: + if signal not in self._signal_map: name = self.__class__.__name__ - raise ValueError( - f"Signal '{signal_output}' not valid for multiplexer '{name}'" - ) + raise ValueError(f"Signal '{signal}' not valid for multiplexer '{name}'") - setup, final = self._calculate_pins(self._state, signal_output) + setup, final = self._calculate_pins(self._state, signal) self._update_pins(PinUpdate(setup, final, self.clearing_time), trigger_update) - if signal_output != self._state: + if signal != self._state: self._last_update_time = time.monotonic() - self._state = signal_output + self._state = signal def all_signals(self) -> tuple[Signal, ...]: return tuple(self._signal_map.keys()) @@ -453,21 +451,21 @@ class VirtualSwitch(VirtualMux): map_tree = ("Off", "On") def multiplex( - self, signal_output: Union[Signal, bool], trigger_update: bool = True + self, signal: Union[Signal, bool], trigger_update: bool = True ) -> None: - if signal_output is True: - signal = "On" - elif signal_output is False: - signal = "Off" + if signal is True: + converted_signal = "On" + elif signal is False: + converted_signal = "Off" else: - signal = signal_output - super().multiplex(signal, trigger_update=trigger_update) + converted_signal = signal + super().multiplex(converted_signal, trigger_update=trigger_update) def __call__( - self, signal_output: Union[Signal, bool], trigger_update: bool = True + self, signal: Union[Signal, bool], trigger_update: bool = True ) -> None: """Override call to set the type on signal_output correctly.""" - self.multiplex(signal_output, trigger_update) + self.multiplex(signal, trigger_update) def __init__( self, From 19f95a45ff4567c9ee41ec330aef946fb6a422c3 Mon Sep 17 00:00:00 2001 From: Clint Lawrence Date: Wed, 19 Jun 2024 11:30:28 +1000 Subject: [PATCH 30/41] re-organise how we export public symbols --- script.py | 36 ++++++++++++++++++- src/fixate/__init__.py | 32 +++++++++++++++++ .../core/{switching.py => _switching.py} | 10 +++--- src/fixate/drivers/handlers.py | 2 +- test/core/test_switching.py | 14 ++++---- 5 files changed, 80 insertions(+), 14 deletions(-) rename src/fixate/core/{switching.py => _switching.py} (99%) diff --git a/script.py b/script.py index 25a0ad83..4e012717 100644 --- a/script.py +++ b/script.py @@ -2,8 +2,42 @@ This file is just a test playground that shows how the update jig classes will fit together. """ +from __future__ import annotations from dataclasses import dataclass, field -from fixate.core.switching import VirtualMux, JigDriver, MuxGroup, PinValueAddressHandler, VirtualSwitch +from fixate.core._switching import VirtualMux, JigDriver, MuxGroup, PinValueAddressHandler, VirtualSwitch, Signal, Pin + +from typing import TypeVar, Generic, Union, Annotated, Literal + +S = TypeVar("S") + + +class VirtualMux(Generic[S]): + def __init__(self): + self._signal_map: dict[Signal, set[Pin]] = {} + + def __call__(self, signal: S, trigger_update: bool = False) -> None: + self.multiplex(signal, trigger_update) + + def multiplex(self, signal: S, trigger_update: bool = False) -> None: + print(self._signal_map[signal]) + + +MuxOneSigDef = Union[ + Annotated[Literal["sig1"], ("x0",)], + Annotated[Literal["sig2"], ("x1",)], + Annotated[Literal["sig3"], ("x0", "x1")], +] + +class MuxOne(VirtualMux[MuxOneSigDef]): + pass + +@dataclass +class JigMuxGroup(MuxGroup): + mux_one: MuxOne = field(default_factory=MuxOne) + + +class MuxOne(VirtualMux[MuxOneSigDef]): + pass class MuxOne(VirtualMux): diff --git a/src/fixate/__init__.py b/src/fixate/__init__.py index 22049ab2..38e865fe 100644 --- a/src/fixate/__init__.py +++ b/src/fixate/__init__.py @@ -1 +1,33 @@ +# from x import y as y tell linters that we intend +# to export the symbol as part of our public interface +# https://github.com/microsoft/pyright/blob/main/docs/typed-libraries.md#library-interface + +# Originally, test scripts had to reach into the internal +# packages and modules for imports. However, we will start +# to move the API intended for use in test scripts into the +# top level package namespace and try to be clearer about what +# is public vs private. +from fixate.core._switching import ( # noqa + # Type Alias + Signal as Signal, + Pin as Pin, + PinList as PinList, + PinSet as PinSet, + SignalMap as SignalMap, + TreeDef as TreeDef, + PinUpdateCallback as PinUpdateCallback, + # Runtime API + PinSetState as PinSetState, + PinUpdate as PinUpdate, + VirtualMux as VirtualMux, + VirtualSwitch as VirtualSwitch, + RelayMatrixMux as RelayMatrixMux, + AddressHandler as AddressHandler, + PinValueAddressHandler as PinValueAddressHandler, + MuxGroup as MuxGroup, + JigDriver as JigDriver, + generate_pin_group as generate_pin_group, + generate_relay_matrix_pin_list as generate_relay_matrix_pin_list, +) + __version__ = "0.6.2" diff --git a/src/fixate/core/switching.py b/src/fixate/core/_switching.py similarity index 99% rename from src/fixate/core/switching.py rename to src/fixate/core/_switching.py index 8cefbcd1..64e75396 100644 --- a/src/fixate/core/switching.py +++ b/src/fixate/core/_switching.py @@ -399,7 +399,7 @@ class Mux(VirtualMux): pins_at_this_level = pins[:bits_at_this_level] for signal_or_tree, pins_for_signal in zip( - tree, generate_bit_sets(pins_at_this_level) + tree, _generate_bit_sets(pins_at_this_level) ): if signal_or_tree is None: continue @@ -535,7 +535,7 @@ class PinValueAddressHandler(AddressHandler): def __init__(self) -> None: super().__init__() self._pin_lookup = { - pin: bit for pin, bit in zip(self.pin_list, bit_generator()) + pin: bit for pin, bit in zip(self.pin_list, _bit_generator()) } def set_pins(self, pins: Collection[Pin]) -> None: @@ -726,10 +726,10 @@ def reset(self) -> None: self.mux.reset() -T = TypeVar("T") +_T = TypeVar("_T") -def generate_bit_sets(bits: Sequence[T]) -> Generator[set[T], None, None]: +def _generate_bit_sets(bits: Sequence[_T]) -> Generator[set[_T], None, None]: """ Create subsets of bits, representing bits of a list of integers @@ -742,7 +742,7 @@ def generate_bit_sets(bits: Sequence[T]) -> Generator[set[T], None, None]: ) -def bit_generator() -> Generator[int, None, None]: +def _bit_generator() -> Generator[int, None, None]: """b1, b10, b100, b1000, ...""" return (1 << counter for counter in itertools.count()) diff --git a/src/fixate/drivers/handlers.py b/src/fixate/drivers/handlers.py index ae03e74e..e28ac114 100644 --- a/src/fixate/drivers/handlers.py +++ b/src/fixate/drivers/handlers.py @@ -6,7 +6,7 @@ from typing import Sequence -from fixate.core.switching import Pin, PinValueAddressHandler +from fixate.core._switching import Pin, PinValueAddressHandler from fixate.drivers import ftdi diff --git a/test/core/test_switching.py b/test/core/test_switching.py index d7c0d7f2..1adf7a9e 100644 --- a/test/core/test_switching.py +++ b/test/core/test_switching.py @@ -1,7 +1,7 @@ -from fixate.core.switching import ( - generate_bit_sets, +from fixate.core._switching import ( + _generate_bit_sets, VirtualMux, - bit_generator, + _bit_generator, PinSetState, PinUpdate, VirtualSwitch, @@ -20,11 +20,11 @@ def test_generate_bit_sets_empty(): - assert list(generate_bit_sets([])) == [] + assert list(_generate_bit_sets([])) == [] def test_generate_bit_sets_one_bit(): - assert list(generate_bit_sets(["b0"])) == [set(), {"b0"}] + assert list(_generate_bit_sets(["b0"])) == [set(), {"b0"}] def test_generate_bit_sets_multiple_bits(): @@ -38,12 +38,12 @@ def test_generate_bit_sets_multiple_bits(): {"b2", "b1"}, {"b2", "b1", "b0"}, ] - assert list(generate_bit_sets(["b0", "b1", "b2"])) == expected + assert list(_generate_bit_sets(["b0", "b1", "b2"])) == expected def test_bit_generator(): """b1, b10, b100, b1000, ...""" - bit_gen = bit_generator() + bit_gen = _bit_generator() actual = [next(bit_gen) for _ in range(8)] expected = [1, 2, 4, 8, 16, 32, 64, 128] From bf043dadd34bb793e8d26a8159e5e5f7b49d10e6 Mon Sep 17 00:00:00 2001 From: Clint Lawrence Date: Wed, 19 Jun 2024 12:20:15 +1000 Subject: [PATCH 31/41] Implement a check on jig definitions --- src/fixate/core/_switching.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/fixate/core/_switching.py b/src/fixate/core/_switching.py index 64e75396..d9f7dae7 100644 --- a/src/fixate/core/_switching.py +++ b/src/fixate/core/_switching.py @@ -181,6 +181,12 @@ def wait_at_least(self, duration: float) -> None: if wait_until > now: time.sleep(wait_until - now) + def pins(self) -> frozenset[Pin]: + """ + Return the set off all pins used by this mux + """ + return self._pin_set + ########################################################################### # The following methods are potential candidates to override in a subclass @@ -693,6 +699,8 @@ def __init__( # constructor, and I was hoping to just use a dataclass... mux._update_pins = self.virtual_map.add_update + self._validate() + def close(self) -> None: for handler in self._handlers: handler.close() @@ -725,6 +733,31 @@ def reset(self) -> None: """ self.mux.reset() + def _validate(self) -> None: + """ + Do some basic sanity checks on the jig definition. + + - Ensure all pins that are used in muxes are defined by + some address handler. + + Note: It is O.K. for there to be AddressHandler pins that + are not used anywhere. Eventually we might choose to + warn about them. This it is necessary to define some jigs. + """ + all_handler_pins: set[Pin] = reduce( + or_, (set(handler.pin_list) for handler in self._handlers), set() + ) + mux_missing_pins = [] + + for mux in self.mux.get_multiplexers(): + if unknown_pins := mux.pins() - all_handler_pins: + mux_missing_pins.append((mux, unknown_pins)) + + if mux_missing_pins: + raise ValueError( + f"One or more VirtualMux uses unknown pins:\n{mux_missing_pins}" + ) + _T = TypeVar("_T") From 1dbe04db6aad2c088afa5cd3adf80d581cc9d7ed Mon Sep 17 00:00:00 2001 From: Clint Lawrence Date: Wed, 19 Jun 2024 13:43:31 +1000 Subject: [PATCH 32/41] Add test for JigDriver._validate() --- test/core/test_switching.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/test/core/test_switching.py b/test/core/test_switching.py index 1adf7a9e..753ba8da 100644 --- a/test/core/test_switching.py +++ b/test/core/test_switching.py @@ -11,6 +11,8 @@ generate_relay_matrix_pin_list, AddressHandler, VirtualAddressMap, + MuxGroup, + JigDriver, ) import pytest @@ -513,6 +515,36 @@ def test_virtual_address_map_multiple_handlers(): assert xy.updates[2] == frozenset("x") +# ############################################################### +# Jig Driver + + +def test_jig_driver_with_unknown_pins(): + class Handler1(AddressHandler): + pin_list = ("x0",) + + class Handler2(AddressHandler): + pin_list = ("x2",) + + class Handler3(AddressHandler): + pin_list = ("x1",) + + class Mux(VirtualMux): + pin_list = ("x0", "x1") # "x1" isn't in either handler + map_list = ("sig1", "x1") + + class Group(MuxGroup): + def __init__(self): + self.mux = Mux() + + # This is O.K., because all the pins are included + JigDriver(Group, [Handler1(), Handler2(), Handler3()]) + + with pytest.raises(ValueError): + # This should raise, because no handler implements "x1" + JigDriver(Group, [Handler1(), Handler2()]) + + # ############################################################### # Helper dataclasses From ed21cf7830e92c1527ad8b0235a243d4d7358e80 Mon Sep 17 00:00:00 2001 From: Clint Lawrence Date: Wed, 19 Jun 2024 15:17:06 +1000 Subject: [PATCH 33/41] Remove type def signal stuff from the test script. --- script.py | 42 ++++++++---------------------------------- 1 file changed, 8 insertions(+), 34 deletions(-) diff --git a/script.py b/script.py index 4e012717..c9955b24 100644 --- a/script.py +++ b/script.py @@ -4,40 +4,13 @@ """ from __future__ import annotations from dataclasses import dataclass, field -from fixate.core._switching import VirtualMux, JigDriver, MuxGroup, PinValueAddressHandler, VirtualSwitch, Signal, Pin - -from typing import TypeVar, Generic, Union, Annotated, Literal - -S = TypeVar("S") - - -class VirtualMux(Generic[S]): - def __init__(self): - self._signal_map: dict[Signal, set[Pin]] = {} - - def __call__(self, signal: S, trigger_update: bool = False) -> None: - self.multiplex(signal, trigger_update) - - def multiplex(self, signal: S, trigger_update: bool = False) -> None: - print(self._signal_map[signal]) - - -MuxOneSigDef = Union[ - Annotated[Literal["sig1"], ("x0",)], - Annotated[Literal["sig2"], ("x1",)], - Annotated[Literal["sig3"], ("x0", "x1")], -] - -class MuxOne(VirtualMux[MuxOneSigDef]): - pass - -@dataclass -class JigMuxGroup(MuxGroup): - mux_one: MuxOne = field(default_factory=MuxOne) - - -class MuxOne(VirtualMux[MuxOneSigDef]): - pass +from fixate import ( + VirtualMux, + JigDriver, + MuxGroup, + PinValueAddressHandler, + VirtualSwitch, +) class MuxOne(VirtualMux): @@ -48,6 +21,7 @@ class MuxOne(VirtualMux): ("sig3", "x2"), ) + class MuxTwo(VirtualMux): pin_list = ("x3", "x4", "x5") map_tree = ( From 50c89cd72e0e3e6c05191b00e070c52066e5d2d4 Mon Sep 17 00:00:00 2001 From: Clint Lawrence Date: Wed, 19 Jun 2024 19:29:10 +1000 Subject: [PATCH 34/41] Another crack at AddressHandler init. Still not sure about this... --- script.py | 9 ++++----- src/fixate/core/_switching.py | 31 +++++++++++++++++++------------ src/fixate/drivers/handlers.py | 28 ++++++++++++++++++---------- test/core/test_switching.py | 26 ++++++++++---------------- 4 files changed, 51 insertions(+), 43 deletions(-) diff --git a/script.py b/script.py index c9955b24..4d90f642 100644 --- a/script.py +++ b/script.py @@ -39,10 +39,6 @@ class MuxThree(VirtualSwitch): pin_name = "x101" -class Handler(PinValueAddressHandler): - pin_list = ("x0", "x1", "x2", "x3", "x4", "x5", "x101") - - # Note! # our existing scripts/jig driver, the name of the mux is the # class of the virtual mux. This scheme below will not allow that @@ -54,7 +50,10 @@ class JigMuxGroup(MuxGroup): mux_three: MuxThree = field(default_factory=MuxThree) -jig = JigDriver(JigMuxGroup, [Handler()]) +jig = JigDriver( + JigMuxGroup, + [PinValueAddressHandler(("x0", "x1", "x2", "x3", "x4", "x5", "x101"))] +) jig.mux.mux_one("sig2", trigger_update=False) jig.mux.mux_two("sig5") diff --git a/src/fixate/core/_switching.py b/src/fixate/core/_switching.py index d9f7dae7..bce8d08e 100644 --- a/src/fixate/core/_switching.py +++ b/src/fixate/core/_switching.py @@ -504,15 +504,21 @@ class AddressHandler: For output, it is assumed that all the pins under the of a given AddressHandler are updated in one operation. - This base class doesn't give you much. You need to create a subclass - that implement a set_pins() method. - - :param pin_list: Sequence of pins + An AddressHandler should lazily open any required hardware + resource. Ideally, it should be possible to instantiate and + inspect the AddressHandler without requiring hardware to + be connected. When `set_pins` is called, the implementation + should check for the required hardware connection and open + that driver when first called. + + Further, calling close() may "uninitialize" the driver. The + next time set_pins is called, the handler will re-open hardware. """ - pin_list: Sequence[Pin] = () - - def __init__(self) -> None: + def __init__(self, pins: Sequence[Pin]) -> None: + # we convert the pin list to an immutable tuple, incase the + # caller passing in a mutable sequence that gets modified... + self.pin_list = tuple(pins) if hasattr(self, "pin_defaults"): raise ValueError("'pin_defaults' should not be set on a AddressHandler") @@ -520,7 +526,10 @@ def set_pins(self, pins: Collection[Pin]) -> None: """ Called by the VirtualAddressMap to write out pin changes. - : param pins: is a collection of pins that should be made active. All other + If the underlying hardware required for the IO isn't open, + open it. + + :param pins: is a collection of pins that should be made active. All other pins defined by the AddressHandler should be cleared. """ raise NotImplementedError @@ -538,8 +547,8 @@ def close(self) -> None: class PinValueAddressHandler(AddressHandler): """Maps pins to bit values then combines the bit values for an update""" - def __init__(self) -> None: - super().__init__() + def __init__(self, pins: Sequence[Pin]) -> None: + super().__init__(pins) self._pin_lookup = { pin: bit for pin, bit in zip(self.pin_list, _bit_generator()) } @@ -549,8 +558,6 @@ def set_pins(self, pins: Collection[Pin]) -> None: self._update_output(value) def _update_output(self, value: int) -> None: - # perhaps it's easy to compose by passing the output - # function into __init__, like what we did with the VirtualMux? bits = len(self.pin_list) print(f"0b{value:0{bits}b}") diff --git a/src/fixate/drivers/handlers.py b/src/fixate/drivers/handlers.py index e28ac114..0cc64021 100644 --- a/src/fixate/drivers/handlers.py +++ b/src/fixate/drivers/handlers.py @@ -4,9 +4,9 @@ """ from __future__ import annotations -from typing import Sequence +from typing import Sequence, Optional -from fixate.core._switching import Pin, PinValueAddressHandler +from fixate import Pin, PinValueAddressHandler from fixate.drivers import ftdi @@ -21,19 +21,20 @@ class FTDIAddressHandler(PinValueAddressHandler): def __init__( self, + pins: Sequence[Pin], ftdi_description: str, - pins: Sequence[Pin] = tuple(), ) -> None: - # pin_list must be defined before calling the base class __init__ - self.pin_list = tuple(pins) - super().__init__() + super().__init__(pins) + self._ftdi_description = ftdi_description + self._ftdi: Optional[ftdi.FTDI2xx] = None + def _open(self) -> ftdi.FTDI2xx: # how many bytes? enough for every pin to get a bit. We might # end up with some left-over bits. The +7 in the expression # ensures we round up. bytes_required = (len(self.pin_list) + 7) // 8 - self._ftdi = ftdi.open(ftdi_description=ftdi_description) - self._ftdi.configure_bit_bang( + ftdi_handle = ftdi.open(ftdi_description=self._ftdi_description) + ftdi_handle.configure_bit_bang( ftdi.BIT_MODE.FT_BITMODE_ASYNC_BITBANG, bytes_required=bytes_required, data_mask=4, @@ -48,10 +49,17 @@ def __init__( # 750_000 ~2.4 MHz # 115_200 ~926 kHz # 10_000 ~160 kHz - self._ftdi.baud_rate = 115_200 + ftdi_handle.baud_rate = 115_200 + return ftdi_handle def close(self) -> None: - self._ftdi.close() + if self._ftdi is not None: + self._ftdi.close() + self._ftdi = None def _update_output(self, value: int) -> None: + # We implement the required semantics of set_pin here, + # by ensuring the ftdi device is open. + if self._ftdi is None: + self._ftdi = self._open() self._ftdi.serial_shift_bit_bang(value) diff --git a/test/core/test_switching.py b/test/core/test_switching.py index 753ba8da..4eb74b24 100644 --- a/test/core/test_switching.py +++ b/test/core/test_switching.py @@ -401,11 +401,10 @@ class RMMux(RelayMatrixMux): def test_pin_default_on_address_handler_raise(): class BadHandler(PinValueAddressHandler): - pin_list = ("x", "y") pin_defaults = ("x",) with pytest.raises(ValueError): - BadHandler() + BadHandler(("x", "y")) # ############################################################### @@ -421,12 +420,12 @@ def set_pins(self, pins): self.updates.append(pins) -class HandlerXY(TestHandler): - pin_list = ("x", "y") +def HandlerXY(): + return TestHandler("xy") -class HandlerAB(TestHandler): - pin_list = ("a", "b") +def HandlerAB(): + return TestHandler("ab") def test_virtual_address_map_init_no_active_pins(): @@ -520,14 +519,9 @@ def test_virtual_address_map_multiple_handlers(): def test_jig_driver_with_unknown_pins(): - class Handler1(AddressHandler): - pin_list = ("x0",) - - class Handler2(AddressHandler): - pin_list = ("x2",) - - class Handler3(AddressHandler): - pin_list = ("x1",) + handler1 = AddressHandler(("x0",)) + handler2 = AddressHandler(("x2",)) + handler3 = AddressHandler(("x1",)) class Mux(VirtualMux): pin_list = ("x0", "x1") # "x1" isn't in either handler @@ -538,11 +532,11 @@ def __init__(self): self.mux = Mux() # This is O.K., because all the pins are included - JigDriver(Group, [Handler1(), Handler2(), Handler3()]) + JigDriver(Group, [handler1, handler2, handler3]) with pytest.raises(ValueError): # This should raise, because no handler implements "x1" - JigDriver(Group, [Handler1(), Handler2()]) + JigDriver(Group, [handler1, handler2]) # ############################################################### From 2860792ae69ae98b4bd028e38eb7f2e2ac0392b7 Mon Sep 17 00:00:00 2001 From: Clint Lawrence Date: Thu, 20 Jun 2024 11:56:19 +1000 Subject: [PATCH 35/41] Relay matrix should open to switch to the same signal --- src/fixate/core/_switching.py | 5 +++++ test/core/test_switching.py | 30 ++++++++++++++++++++++++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/fixate/core/_switching.py b/src/fixate/core/_switching.py index bce8d08e..afade427 100644 --- a/src/fixate/core/_switching.py +++ b/src/fixate/core/_switching.py @@ -494,6 +494,11 @@ def _calculate_pins( setup = PinSetState(off=self._pin_set, on=frozenset()) on_pins = self._signal_map[new_signal] final = PinSetState(off=self._pin_set - on_pins, on=on_pins) + + # if the signal doesn't change, we don't want open then close again + # so return the 'final' state for both setup and final. + if old_signal == new_signal: + return final, final return setup, final diff --git a/test/core/test_switching.py b/test/core/test_switching.py index 4eb74b24..06a87344 100644 --- a/test/core/test_switching.py +++ b/test/core/test_switching.py @@ -387,14 +387,40 @@ class RMMux(RelayMatrixMux): # compared to the standard mux, the setup of the PinUpdate # sets all pins off. The standard mux does nothing for the - # setup phase. And we technically shouldn't compare floats - # for equality, but it should be fine here... until it's not :D + # setup phase. assert updates == [ (PinUpdate(off, sig1, 0.01), True), (PinUpdate(off, sig2, 0.01), True), ] +def test_relay_matrix_mux_no_signal_change(): + """If the new signal is as-per previous, don't open & close again""" + + class RMMux(RelayMatrixMux): + pin_list = ("a", "b") + map_list = ( + ("sig1", "a"), + ("sig2", "b"), + ) + + sig1 = PinSetState(off=frozenset("b"), on=frozenset("a")) + off = PinSetState(off=frozenset("ab")) + + updates = [] + rm = RMMux(lambda x, y: updates.append((x, y))) + rm("sig1") + rm("sig1") + + # we don't care about the first update, that just ensure the mux in + # in the right initial state. Note that there is a bit of implementation + # detail leaking here. We could also test that no pins a added to the + # pin update. I will keep as is for now, but if we change the implementation + # it is reasonable to update this test. + assert len(updates) == 2 + assert updates[1] == (PinUpdate(sig1, sig1, 0.01), True) + + # ############################################################### # AddressHandler From 68714b3486b34a1d9f79e719bb037435ae5fa0b8 Mon Sep 17 00:00:00 2001 From: Clint Lawrence Date: Thu, 20 Jun 2024 12:55:40 +1000 Subject: [PATCH 36/41] fix mypy config --- mypy.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy.ini b/mypy.ini index 999f6c15..7fe53135 100644 --- a/mypy.ini +++ b/mypy.ini @@ -69,7 +69,7 @@ exclude = (?x) warn_unused_configs = True warn_redundant_casts = True -[mypy-fixate.core.switching] +[mypy-fixate.core._switching] # Enable strict options for new code warn_unused_ignores = True strict_equality = True From 056eb5fcb971801e29e8191c983a816f659162a5 Mon Sep 17 00:00:00 2001 From: Clint Lawrence Date: Thu, 20 Jun 2024 12:57:14 +1000 Subject: [PATCH 37/41] Move script.py to examples --- script.py => src/fixate/examples/jig_driver.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename script.py => src/fixate/examples/jig_driver.py (100%) diff --git a/script.py b/src/fixate/examples/jig_driver.py similarity index 100% rename from script.py rename to src/fixate/examples/jig_driver.py From d8ae0417a58295fd26dc50e6cbf9467dbbe5b5dc Mon Sep 17 00:00:00 2001 From: Clint Lawrence Date: Thu, 20 Jun 2024 13:19:51 +1000 Subject: [PATCH 38/41] Move _switching up out of core. This silences a warning that were importing an private module from fixate.core in the top level __init__.py. So partly this is just to silence PyCharm, but it is a resonable change in general. --- src/fixate/__init__.py | 2 +- src/fixate/{core => }/_switching.py | 0 test/{core => }/test_switching.py | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename src/fixate/{core => }/_switching.py (100%) rename test/{core => }/test_switching.py (99%) diff --git a/src/fixate/__init__.py b/src/fixate/__init__.py index 38e865fe..a655e50f 100644 --- a/src/fixate/__init__.py +++ b/src/fixate/__init__.py @@ -7,7 +7,7 @@ # to move the API intended for use in test scripts into the # top level package namespace and try to be clearer about what # is public vs private. -from fixate.core._switching import ( # noqa +from fixate._switching import ( # Type Alias Signal as Signal, Pin as Pin, diff --git a/src/fixate/core/_switching.py b/src/fixate/_switching.py similarity index 100% rename from src/fixate/core/_switching.py rename to src/fixate/_switching.py diff --git a/test/core/test_switching.py b/test/test_switching.py similarity index 99% rename from test/core/test_switching.py rename to test/test_switching.py index 06a87344..3beb2110 100644 --- a/test/core/test_switching.py +++ b/test/test_switching.py @@ -1,4 +1,4 @@ -from fixate.core._switching import ( +from fixate._switching import ( _generate_bit_sets, VirtualMux, _bit_generator, From 0a20ba5188975e75c2337c38282359e8aa87ba50 Mon Sep 17 00:00:00 2001 From: Clint Lawrence Date: Thu, 20 Jun 2024 13:25:25 +1000 Subject: [PATCH 39/41] blacken the jig_driver example --- src/fixate/examples/jig_driver.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/fixate/examples/jig_driver.py b/src/fixate/examples/jig_driver.py index 4d90f642..6d56cdf6 100644 --- a/src/fixate/examples/jig_driver.py +++ b/src/fixate/examples/jig_driver.py @@ -51,8 +51,7 @@ class JigMuxGroup(MuxGroup): jig = JigDriver( - JigMuxGroup, - [PinValueAddressHandler(("x0", "x1", "x2", "x3", "x4", "x5", "x101"))] + JigMuxGroup, [PinValueAddressHandler(("x0", "x1", "x2", "x3", "x4", "x5", "x101"))] ) jig.mux.mux_one("sig2", trigger_update=False) From 2add8c852d44ec58f1d070c45293e831944ca2c1 Mon Sep 17 00:00:00 2001 From: Clint Lawrence Date: Thu, 20 Jun 2024 13:26:57 +1000 Subject: [PATCH 40/41] grammar fix before I get in trouble from Jason --- test/test_switching.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_switching.py b/test/test_switching.py index 3beb2110..527f1d2c 100644 --- a/test/test_switching.py +++ b/test/test_switching.py @@ -280,7 +280,7 @@ class BadMux(VirtualMux): class MuxA(VirtualMux): - """A mux definitioned used by a few scripts""" + """A mux definition used by a few tests""" pin_list = ("a0", "a1") map_list = (("sig_a1", "a0", "a1"), ("sig_a2", "a1")) From 5475f76b6aee0b91e94f33ee163497b2fc8c47a4 Mon Sep 17 00:00:00 2001 From: Clint Lawrence Date: Thu, 20 Jun 2024 13:34:09 +1000 Subject: [PATCH 41/41] fix mypy config. Again... --- mypy.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy.ini b/mypy.ini index 7fe53135..765bff0e 100644 --- a/mypy.ini +++ b/mypy.ini @@ -69,7 +69,7 @@ exclude = (?x) warn_unused_configs = True warn_redundant_casts = True -[mypy-fixate.core._switching] +[mypy-fixate._switching] # Enable strict options for new code warn_unused_ignores = True strict_equality = True