522 lines
15 KiB
Python
522 lines
15 KiB
Python
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()
|