import abc from math import nan from typing import Optional, List, Iterable, Union, Dict, Tuple from biopro.util.cli import * from biopro.util.console import hex_line, hex_value from .gpio import P3Pin from .interface_spi import SoftwareImplSpiInterface class LEDParameter: __slots__ = ('flash', 'red', 'green', 'blue') def __init__(self): self.flash: float = 1.0 self.red: Optional[float] = None self.green: Optional[float] = None self.blue: Optional[float] = None def set_color(self, flash: Optional[float] = nan, red: Optional[float] = nan, green: Optional[float] = nan, blue: Optional[float] = nan) -> 'LEDParameter': """ *flash*, *red*, *green* and *blue* should be a floating number locate in [0.0, 1.0] and ``None`` and ``NaN``. ``None`` value meaning the value follow the previous keyframe. ``NaN`` value meaning do not change current value. :param flash: :param red: :param green: :param blue: :return: """ if red == red: self.red = float(red) if red is not None else None if green == green: self.green = float(green) if green is not None else None if blue == blue: self.blue = float(blue) if blue is not None else None if flash == flash: self.flash = float(flash) if flash is not None else None return self def set_color_by_parameter(self, para: 'LEDParameter') -> 'LEDParameter': self.flash = para.flash self.red = para.red self.green = para.green self.blue = para.blue return self def to_byte(self, prev: Optional[bytes], global_flash: float = 1.0) -> bytes: f = self.flash if self.flash is not None else 1.0 b = prev[1] if prev else 0 g = prev[2] if prev else 0 r = prev[3] if prev else 0 return bytes([0xE0 | int(0x1F * f * global_flash) & 0x1F, int(0xFF * self.blue) & 0xFF if self.blue is not None else b, int(0xFF * self.green) & 0xFF if self.green is not None else g, int(0xFF * self.red) & 0xFF if self.red is not None else r]) def __str__(self): return '(%s,%s,%s,%s)' % ( '%02X' % (int(0x1F * self.flash) & 0x1F) if self.flash is not None else 'N', '%02X' % (int(0xFF * self.red) & 0xFF) if self.red is not None else 'N', '%02X' % (int(0xFF * self.green) & 0xFF) if self.green is not None else 'N', '%02X' % (int(0xFF * self.blue) & 0xFF) if self.blue is not None else 'N', ) class AbstractLEDKeyFrame(metaclass=abc.ABCMeta): __slots__ = ('time',) def __init__(self, time: float): self.time: float = time @abc.abstractmethod def foreach(self, total: int) -> Iterable[Optional[LEDParameter]]: pass def __repr__(self): return str(self) def __str__(self): return '[%.4f]' % self.time class AllLEDKeyFrame(AbstractLEDKeyFrame): __slots__ = ('parameter',) def __init__(self, time: float): super().__init__(time) self.parameter = LEDParameter() def foreach(self, total: int) -> Iterable[LEDParameter]: for _ in range(total): yield self.parameter def set_color(self, flash: Optional[float] = nan, red: Optional[float] = nan, green: Optional[float] = nan, blue: Optional[float] = nan) -> 'AllLEDKeyFrame': self.parameter.set_color(flash, red, green, blue) return self def set_color_by_frame(self, frame: Union[LEDParameter, 'AllLEDKeyFrame']) -> 'AllLEDKeyFrame': if isinstance(frame, LEDParameter): self.parameter.set_color_by_parameter(frame) elif isinstance(frame, AllLEDKeyFrame): self.parameter.set_color_by_parameter(frame.parameter) return self @classmethod def new(cls, time: float, flash: Optional[float] = None, red: Optional[float] = None, green: Optional[float] = None, blue: Optional[float] = None) -> 'AllLEDKeyFrame': ret = AllLEDKeyFrame(time) ret.parameter.set_color(flash, red, green, blue) return ret def __str__(self): return super().__str__() + str(self.parameter) class LEDKeyFrame(AbstractLEDKeyFrame): __slots__ = ('parameter',) def __init__(self, time: float): super().__init__(time) self.parameter: Dict[int, LEDParameter] = {} def foreach(self, total: int) -> Iterable[Optional[LEDParameter]]: for i in range(total): yield self.parameter.get(i, None) def __contains__(self, index: int) -> bool: return index in self.parameter def __getitem__(self, index: int) -> Optional[LEDParameter]: return self.parameter.get(index, None) def __setitem__(self, index: int, value: Union[LEDParameter, float, Tuple[float, ...]]): if isinstance(value, LEDParameter): self.parameter[index] = value elif isinstance(value, float): p = self.parameter.get(index, None) if p is None: p = LEDParameter() self.parameter[index] = p p.flash = value else: p = self.parameter.get(index, None) if p is None: p = LEDParameter() self.parameter[index] = p p.set_color(*value) def __delitem__(self, index: int): try: del self.parameter[index] except KeyError: pass def set_color(self, led: int, flash: Optional[float] = nan, red: Optional[float] = nan, green: Optional[float] = nan, blue: Optional[float] = nan) -> 'LEDKeyFrame': p = self.parameter.get(led, None) if p is None: p = LEDParameter() self.parameter[led] = p p.set_color(flash, red, green, blue) return self def set_color_by_frame(self, led: int, frame: Union[None, LEDParameter, AllLEDKeyFrame, 'LEDKeyFrame']) -> 'LEDKeyFrame': if frame is None: return self elif isinstance(frame, LEDKeyFrame): for _led, _p in frame.parameter.items(): self.set_color_by_frame(_led, _p) else: p = self.parameter.get(led, None) if p is None: p = LEDParameter() self.parameter[led] = p if isinstance(frame, LEDParameter): p.set_color_by_parameter(frame) elif isinstance(frame, AllLEDKeyFrame): p.set_color_by_parameter(frame.parameter) return self def __str__(self): return super().__str__() + '{' + ','.join(map(lambda i, p: '%d:%s' % (i, str(p)), self.parameter.items())) + '}' class LED: """ **LED device** **LED state** 1. power on 2. on connected 3. on working 4. on error 5. low battery **color present** (red, green, blue, flush_interval) """ COLOR_RED = AllLEDKeyFrame.new(0., 1., 1., 0., 0.) COLOR_ORANGE = AllLEDKeyFrame.new(0., 1., 1., .3, 0.) COLOR_YELLOW = AllLEDKeyFrame.new(0., 1., .5, .5, 0.) COLOR_GREEN = AllLEDKeyFrame.new(0., 1., 0., 1., 0.) COLOR_BLUE = AllLEDKeyFrame.new(0., 1., 0., 0., 1.) COLOR_CYAN = AllLEDKeyFrame.new(0., 1., 0., 1., 1.) COLOR_MAGENTA = AllLEDKeyFrame.new(0., 1.0, 1., 0., 1.) COLOR_WHITE = AllLEDKeyFrame.new(0., 1., .5, .9, .7) COLOR_BLACK = AllLEDKeyFrame.new(0., 0., 0., 0., 0.) LIGHT_OFF = COLOR_BLACK POWER_ON = 'power_on' POWER_OFF = 'power_off' AVAILABLE = 'available' CONNECTED = 'connected' ERROR = 'error' UPS_USING = 'ups_using' COLOR = { 'red': COLOR_RED, 'r': COLOR_RED, 'orange': COLOR_ORANGE, 'o': COLOR_ORANGE, 'yellow': COLOR_YELLOW, 'y': COLOR_YELLOW, 'green': COLOR_GREEN, 'g': COLOR_GREEN, 'blue': COLOR_BLUE, 'b': COLOR_BLUE, 'cyan': COLOR_CYAN, 'c': COLOR_CYAN, 'magenta': COLOR_MAGENTA, 'm': COLOR_MAGENTA, 'white': COLOR_WHITE, 'w': COLOR_WHITE, 'off': LIGHT_OFF, 'k': LIGHT_OFF, 'f0': AllLEDKeyFrame.new(0.0, 0.0, None, None, None), 'f1': AllLEDKeyFrame.new(0.0, 0.1, None, None, None), 'f2': AllLEDKeyFrame.new(0.0, 0.2, None, None, None), 'f3': AllLEDKeyFrame.new(0.0, 0.3, None, None, None), 'f4': AllLEDKeyFrame.new(0.0, 0.4, None, None, None), 'f5': AllLEDKeyFrame.new(0.0, 0.5, None, None, None), 'f6': AllLEDKeyFrame.new(0.0, 0.6, None, None, None), 'f7': AllLEDKeyFrame.new(0.0, 0.7, None, None, None), 'f8': AllLEDKeyFrame.new(0.0, 0.8, None, None, None), 'f9': AllLEDKeyFrame.new(0.0, 0.9, None, None, None), 'F': AllLEDKeyFrame.new(0.0, 1.0, None, None, None), POWER_ON: (AllLEDKeyFrame(0.5).set_color_by_frame(COLOR_GREEN), AllLEDKeyFrame(0.5).set_color_by_frame(COLOR_BLACK)), POWER_OFF: COLOR_BLACK, AVAILABLE: COLOR_GREEN, CONNECTED: COLOR_CYAN, UPS_USING: (AllLEDKeyFrame(1.2).set_color_by_frame(COLOR_ORANGE), AllLEDKeyFrame(0.8).set_color_by_frame(COLOR_BLACK)), ERROR: (AllLEDKeyFrame(0.5).set_color_by_frame(COLOR_RED), AllLEDKeyFrame(0.5).set_color_by_frame(COLOR_BLACK)), } _INS_START = bytes([0x00, 0x00, 0x00, 0x00]) _INS_STOP = bytes([0xFF, 0xFF, 0xFF, 0xFF]) _INS_OFF = bytes([0xE0, 0x00, 0x00, 0x00]) __slots__ = ('_prev', '_spi', '_total', '_flash') def __init__(self, total: int, interface: Optional[SoftwareImplSpiInterface]): self._total = total self._prev: List[Optional[bytes]] = [None for _ in range(total)] self._spi = interface self._flash = 1.0 @property def global_flash(self) -> float: return self._flash @global_flash.setter def global_flash(self, value: float): self._flash = value def update(self, keyframe: AbstractLEDKeyFrame): if self._spi is not None: with self._spi as spi: spi.send_byte_transmit(self._INS_START) for i, n in enumerate(keyframe.foreach(self._total)): b = n.to_byte(self._prev[i], global_flash=self._flash) spi.send_byte_transmit(b) self._prev[i] = b spi.send_byte_transmit(self._INS_STOP) def light_off(self): if self._spi is not None: with self._spi as spi: spi.send_byte_transmit(self._INS_START) for _ in range(self._total): spi.send_byte_transmit(self._INS_OFF) spi.send_byte_transmit(self._INS_STOP) # noinspection PyUnusedLocal class Main(CliReplMain): """LED control""" def __init__(self): super().__init__() self.count = 16 self._spi: Optional[SoftwareImplSpiInterface] = None self._led: Optional[LED] = None @cli_flags('-h', '--help', force_return=True) def _help(self, opt: str): """print help document""" self.print_help() @cli_options('-c', value='COUNT') def _count(self, opt: str, value: str): """led count""" self.count = int(value) def __enter__(self): self._spi = SoftwareImplSpiInterface(P3Pin.LED_SCL, P3Pin.LED_SDA, cs=33) self._spi.select = 33 self._led = LED(self.count, self._spi) return self def __exit__(self, exc_type, exc_val, exc_tb): if self._led is not None: self._led.light_off() if self._spi is not None: self._spi.close() self._spi: Optional[SoftwareImplSpiInterface] = None self._led: Optional[LED] = None @cli_command('h', 'help') def _help_c(self, cmd: str, argv: List[str]): """print help document""" self.print_help() @cli_command('send') def _send(self, cmd: str, argv: List[str]): """""" send = bytes([hex_value(v) for v in argv]) self._spi.send_byte(send) recv = self._spi.recv_byte(len(send)) if recv is not None: print(hex_line(recv)) @cli_command('flash') def _flash(self, cmd: str, argv: List[str]): """set global flash""" if len(argv) == 0: print(self._led.global_flash) else: self._led.global_flash = float(argv[0]) @cli_command('color') def _frame(self, cmd: str, argv: List[str]): """set led color""" if len(argv) == 0: print(' '.join(sorted(LED.COLOR.keys()))) elif len(argv) == 1: frame = LED.COLOR[argv[0]] self._led.update(frame) else: frame = AllLEDKeyFrame(0) frame.set_color(float(argv[3]) if len(argv) > 3 else None, float(argv[0]), float(argv[1]), float(argv[2])) self._led.update(frame) @cli_command('play') def _play(self, cmd: str, argv: List[str]): """play led""" a = Arguments(argv) if a.has_opt('-i'): interval = a.get_opt('-i', cast=float) else: interval = 1 repeat = a.has_opt('-r') frame = [ AllLEDKeyFrame(interval).set_color_by_frame(LED.COLOR[c]) for i, c in enumerate(a) ] self._play.track = frame try: self._play.play(repeat=repeat) except KeyboardInterrupt: pass finally: self._led.light_off() @cli_command('off') def _off(self, cmd: str, argv: List[str]): """light off""" self._led.light_off() # noinspection PyUnusedLocal class LEDControlMain(CliSubCommandMain): def __init__(self, command: str): super().__init__(command) self.count = 16 self.state: Optional[str] = None self.finalize = False @cli_flags('-h', '--help', force_return=True) def _help(self, opt: str): """print help document""" self.print_help() @cli_options('-c', value='COUNT') def _count(self, opt: str, value: str): """led count""" self.count = int(value) @cli_flags('-F', '--finalize') def _finalize(self, opt: str): """finalize resource""" self.finalize = True @cli_arguments(0, value='STATE') def _state(self, pos: int, value: str): """LED state""" self.state = value def run(self): if self.state is None: print(' , '.join(sorted(LED.COLOR.keys()))) return key_frame = LED.COLOR[self.state] if not isinstance(key_frame, AbstractLEDKeyFrame): raise ValueError() spi = SoftwareImplSpiInterface(P3Pin.LED_SCL, P3Pin.LED_SDA, cs=33) spi.select = 33 led = LED(self.count, spi) try: led.update(key_frame) finally: if self.finalize: led.light_off() spi.close() if __name__ == '__main__': Main().main()