From dac34a46a257c6dc908a5d5ad171efb77ad4bdf8 Mon Sep 17 00:00:00 2001 From: XANTRONIX Development Date: Sat, 6 Jan 2024 18:52:52 -0500 Subject: [PATCH] Man this refactor has been hurting my head --- py/hexagram/cluster.py | 15 +- py/hexagram/icon.py | 64 +++++++ py/hexagram/status.py | 389 +++++++++++++++++++++++++++-------------- py/hexagram/svg.py | 2 +- 4 files changed, 329 insertions(+), 141 deletions(-) create mode 100644 py/hexagram/icon.py diff --git a/py/hexagram/cluster.py b/py/hexagram/cluster.py index e4d4bd5..9f5719b 100644 --- a/py/hexagram/cluster.py +++ b/py/hexagram/cluster.py @@ -11,7 +11,7 @@ from hexagram.speedo import Speedo from hexagram.tacho import Tacho from hexagram.fuel import FuelGauge from hexagram.thermo import Thermometer -from hexagram.status import VehicleStatus, StatusIcon, StatusIconBox +from hexagram.status import StatusIconBox, ColdStatus, ColdIcon class ShiftIndicator(Gauge): __slots__ = 'x', 'y', 'rpm_min', 'rpm_redline', 'rpm_max', @@ -100,7 +100,6 @@ class AmbientTemp(TextGauge): ICON_WIDTH = 40 ICON_HEIGHT = 40 ICON_PADDING = 16 - ICON_COLOR = '#fff' COLD_THRESHOLD = 4.0 # °C @@ -108,12 +107,7 @@ class AmbientTemp(TextGauge): super().__init__(x, y, -100, 100, align) self.value = 0 - self.icon = StatusIcon(VehicleStatus.COLD, - 'cold', - self.ICON_WIDTH, - self.ICON_HEIGHT, - self.ICON_COLOR, - 'path {stroke: %(s)s;stroke-width: 8}') + self.icon = ColdIcon(self.ICON_WIDTH, self.ICON_HEIGHT) def format_text(self): return "%.1f°C" % self.value @@ -148,8 +142,9 @@ class AmbientTemp(TextGauge): icon_y_offset = abs(self.icon.height - self.FONT_SIZE) * 2 self.icon.draw(cr, - text_x_offset + width - self.icon.width, - self.y - icon_y_offset) + text_x_offset + width - self.icon.width, + self.y - icon_y_offset, + ColdStatus.ON) class Clock(TextGauge): def __init__(self, x: float, y: float, align: Align): diff --git a/py/hexagram/icon.py b/py/hexagram/icon.py new file mode 100644 index 0000000..2d796f5 --- /dev/null +++ b/py/hexagram/icon.py @@ -0,0 +1,64 @@ +import os +import enum +import cairo + +from hexagram.svg import render_to_image + +class Icon(): + __slots__ = 'name', 'width', 'height', 'style', 'variants', \ + '_path', '_surfaces', + + __dirs__ = ('./icons', + '/usr/local/share/hexagram/icons' + '/usr/share/hexagram/icons') + + @staticmethod + def _locate(name: str): + for dir in Icon.__dirs__: + path = "%s/%s.svg" % (dir, name) + + if os.path.exists(path): + return path + + raise FileNotFoundError(name + '.svg') + + STYLE = 'rect,path,circle{stroke:%(s)s;}' + + def __init__(self, name: str, width: float, height: float, variants: dict, style: str=STYLE): + self.name = name + self.width = width + self.height = height + self.style = style + self.variants = variants + self._path = Icon._locate(name) + self._surfaces: dict[enum.Enum, cairo.ImageSurface] = dict() + + def __del__(self): + for key in self._surfaces: + self._surfaces[key].finish() + + def _surface(self, status) -> cairo.ImageSurface: + surface = self._surfaces.get(status) + + if surface is not None: + return surface + + color = self.variants[status] + surface = render_to_image(self._path, + self.width, + self.height, + self.style % {'s': color}) + + self._surfaces[status] = surface + + return surface + + def drawable(self, status: enum.Enum): + return status in self.variants + + def draw(self, cr: cairo.Context, x: float, y: float, status): + surface = self._surface(status) + + cr.set_source_surface(surface, x, y) + cr.rectangle(x, y, self.width, self.height) + cr.fill() diff --git a/py/hexagram/status.py b/py/hexagram/status.py index 0074c39..58e7dc4 100644 --- a/py/hexagram/status.py +++ b/py/hexagram/status.py @@ -4,147 +4,285 @@ import cairo from typing import Optional -from hexagram.svg import render_to_image from hexagram.gauge import Gauge +from hexagram.icon import Icon -class VehicleStatus(enum.Enum): - OK = 0 - ABS_FAULT = 1 << 0 - AIRBAG_FAULT = 1 << 1 - BATTERY_FAULT = 1 << 2 - BEAMS_FOG = 1 << 3 - BEAMS_HIGH = 1 << 4 - BEAMS_LOW = 1 << 5 - BEAMS_PARKING = 1 << 6 - BELT = 1 << 7 - CAUTION = 1 << 8 - COLD = 1 << 9 - COLLISION = 1 << 10 - COOLANT_LOW = 1 << 11 - COOLANT_OVERHEAT = 1 << 12 - CRUISE = 1 << 13 - FUEL_LOW = 1 << 14 - LANEKEEP_OFF = 1 << 15 - ENGINE_FAULT = 1 << 16 - OIL_LOW = 1 << 17 - OIL_OVERHEAT = 1 << 18 - PARKING_BRAKE_ON = 1 << 19 - PARKING_BRAKE_FAULT = 1 << 20 - STABILITY_OFF = 1 << 21 - TPMS_WARNING = 1 << 22 - TPMS_FAULT = 1 << 23 - TRACTION_OFF = 1 << 24 - TRACTION_FAULT = 1 << 25 - WARNING = 1 << 26 - WIPER_WASHER_LOW = 1 << 27 +class StatusIcon(Icon): + TYPE = enum.Enum - __strings__ = { - OK: 'OK', ABS_FAULT: "ABS fault", AIRBAG_FAULT: "airbag fault", - BATTERY_FAULT: "low battery voltage", BEAMS_FOG: "foglights on", - BEAMS_HIGH: "highbeams on", BEAMS_LOW: "lowbeams on", - BEAMS_PARKING: "parking beams on", BELT: "warning: fasten seatbelt", - CAUTION: "caution", COLLISION: "collision detected", COLD: "outside temperatures cold", - COOLANT_LOW: "coolant low", COOLANT_OVERHEAT: "engine overheating", - CRUISE: "cruise control on", FUEL_LOW: "fuel low", - LANEKEEP_OFF: "lane assist deactivated", ENGINE_FAULT: "check engine", - OIL_LOW: "oil level low", OIL_OVERHEAT: "oil overheating", - PARKING_BRAKE_ON: "parking brake on", PARKING_BRAKE_FAULT: "parking brake malfunction", - STABILITY_OFF: "stability control off", TPMS_WARNING: "check tire pressures", - TPMS_FAULT: "tire pressure monitoring system fault", - TRACTION_OFF: "traction control off", WARNING: "warning, drive with caution", - WIPER_WASHER_LOW: "wiper washer fluid low" + def __init__(self, width: float, height: float): + super().__init__(self.NAME, width, height, self.VARIANTS, self.STYLE) + +class CautionStatus(enum.Enum): + OFF = 0 + ON = 1 + WARNING = 2 + +class CollisionStatus(enum.Enum): + OK = 0 + DETECTED = 1 + +class CollisionIcon(StatusIcon): + TYPE = CollisionStatus + NAME = 'collision' + STYLE = 'path {stroke-width: 10px; stroke: %(s)s}' + VARIANTS = { + CollisionStatus.DETECTED: '#f00' } - def __str__(self): - return self.__strings__.get(self.value, "unknown") +class FuelStatus(enum.Enum): + OK = 0 + LOW = 1 + FAULT = 2 -class StatusIcon(): - __slots__ = 'status', 'name', 'width', 'height', 'color', 'surface', +class FuelIcon(StatusIcon): + TYPE = FuelStatus + NAME = 'fuel' + STYLE = '#g9 * {stroke: %(s)s} #path9 {fill: %(s)s}' + VARIANTS = { + FuelStatus.LOW: '#fa0', + FuelStatus.FAULT: '#f00' + } - __dirs__ = ('./icons', - '/usr/local/share/hexagram/icons' - '/usr/share/hexagram/icons') +class SeatBeltStatus(enum.Enum): + OK = 0 + UNFASTENED = 1 + FAULT = 2 - @staticmethod - def _locate(name: str): - for dir in StatusIcon.__dirs__: - path = "%s/%s.svg" % (dir, name) +class SeatBeltIcon(StatusIcon): + TYPE = SeatBeltStatus + NAME = 'belt' + STYLE = 'path {fill: %(s)s}' + VARIANTS = { + SeatBeltStatus.UNFASTENED: '#f00' + } - if os.path.exists(path): - return path +class CoolantStatus(enum.Enum): + OK = 0 + LOW = 1 + OVERHEAT = 2 + FAULT = 3 - raise FileNotFoundError(name + '.svg') +class CoolantIcon(StatusIcon): + TYPE = CoolantStatus + NAME = 'coolant' + STYLE = 'path, circle, rect {fill: %(s)s} #g9 path {stroke: %(s)s}' + VARIANTS = { + CoolantStatus.LOW: '#fa0', + CoolantStatus.OVERHEAT: '#f00' + } - def __init__(self, status: VehicleStatus, name: str, width: float, height: float, color: str, style: Optional[str]=None): - self.status = status - self.name = name - self.width = width - self.height = height - self.color = color +class OilStatus(enum.Enum): + OK = 0 + LOW = 1 + OVERHEAT = 2 + FAULT = 3 - if style is None: - style = 'rect,path,circle{stroke:%(s)s;}' +class OilIcon(StatusIcon): + TYPE = OilStatus + NAME = 'oil' + STYLE ='path {fill: %(s)s; stroke: %(s)s}' + VARIANTS = { + OilStatus.LOW: '#fa0', + OilStatus.OVERHEAT: '#f00' + } - path = StatusIcon._locate(name) +class ParkingBrakeStatus(enum.Enum): + OFF = 0 + ON = 1 + FAULT = 2 - self.surface = render_to_image(path, width, height, style % {'s': color}) +class ParkingBrakeIcon(StatusIcon): + TYPE = ParkingBrakeStatus + NAME = 'parking' + STYLE = '#path6, #path5 {stroke: %(s)s} circle {stroke: %(s)s} #path7 {fill: %(s)s}' + VARIANTS = { + ParkingBrakeStatus.ON: '#fa0' + } - def __del__(self): - if self.surface is not None: - self.surface.finish() +class WiperWasherStatus(enum.Enum): + OK = 0 + LOW = 1 + FAULT = 2 - def draw(self, cr: cairo.Context, x: float, y: float): - cr.set_source_surface(self.surface, x, y) - cr.rectangle(x, y, self.width, self.height) - cr.fill() +class WiperWasherIcon(StatusIcon): + TYPE = WiperWasherStatus + NAME = 'wiper-washer' + STYLE = 'rect,path,circle{stroke:%(s)s;}' + VARIANTS = { + WiperWasherStatus.LOW: '#fa0', + WiperWasherStatus.FAULT: '#f00' + } + +class LaneKeepStatus(enum.Enum): + OFF = 0 + ON = 1 + FAULT = 2 + +class LaneKeepIcon(StatusIcon): + TYPE = LaneKeepStatus + NAME = 'lane-departure' + STYLE = 'path {stroke: %(s)s; stroke-width: 8}' + VARIANTS = { + LaneKeepStatus.OFF: '#fa0', + LaneKeepStatus.ON: '#0f0', + LaneKeepStatus.FAULT: '#f00' + } + +class StabilityControlStatus(enum.Enum): + OFF = 0 + ON = 1 + FAULT = 2 + +class StabilityControlIcon(StatusIcon): + TYPE = StabilityControlStatus + NAME = 'stability' + STYLE = '#path9, #path8, #path7, #path6, #path5 {fill: %(s)s} #path10 {stroke: %(s)s}' + VARIANTS = { + StabilityControlStatus.OFF: '#fa0', + StabilityControlStatus.ON: '#0f0', + StabilityControlStatus.FAULT: '#f00' + } + +class TractionControlFault(enum.Enum): + OFF = 0 + ON = 1 + FAULT = 2 + +class TPMSStatus(enum.Enum): + OK = 0 + WARNING = 1 + FAULT = 2 + +class TPMSIcon(StatusIcon): + TYPE = TPMSStatus + NAME = 'tpms' + STYLE = 'path, circle {fill: %(s)s}' + VARIANTS = { + TPMSStatus.WARNING: '#fa0', + TPMSStatus.FAULT: '#f00' + } + +class ABSStatus(enum.Enum): + OFF = 0 + ON = 1 + FAULT = 2 + +class AirbagStatus(enum.Enum): + OFF = 0 + ON = 1 + FAULT = 2 + +class BatteryStatus(enum.Enum): + OK = 0 + FAULT = 1 + +class BatteryIcon(StatusIcon): + TYPE = BatteryStatus + NAME = 'battery', + STYLE = '#rect5, #rect6 {fill: %(s)s} #rect4, path {stroke: %(s)s}' + VARIANTS = { + BatteryStatus.FAULT: '#f00' + } + +class EngineStatus(enum.Enum): + OK = 0 + FAULT = 1 + +class FogBeamStatus(enum.Enum): + OFF = 0 + ON = 1 + FAULT = 2 + +class FogBeamIcon(StatusIcon): + TYPE = FogBeamStatus + NAME = 'beams-fog' + STYLE = '#g9 path {fill: %(s)s} #g5 path {stroke: %(s)s}' + VARIANTS = { + FogBeamStatus.ON: '#0f0', + FogBeamStatus.FAULT: '#f00' + } + +class ParkingBeamStatus(enum.Enum): + OFF = 0 + ON = 1 + FAULT = 2 + +class ParkingBeamIcon(StatusIcon): + TYPE = ParkingBeamStatus + NAME = 'beams-parking' + STYLE = 'path {stroke: %(s)s; stroke-width: 12}' + VARIANTS = { + ParkingBeamStatus.ON: '#ff0', + ParkingBeamStatus.FAULT: '#f00' + } + +class LowBeamStatus(enum.Enum): + OFF = 0 + ON = 1 + FAULT = 2 + +class LowBeamIcon(StatusIcon): + TYPE = LowBeamStatus + NAME = 'beams-low' + STYLE = 'path {fill: %(s)s; stroke: %(s)s}' + VARIANTS = { + LowBeamStatus.ON: '#0f0', + LowBeamStatus.FAULT: '#f00' + } + +class HighBeamStatus(enum.Enum): + OFF = 0 + ON = 1 + FAULT = 2 + +class HighBeamIcon(StatusIcon): + TYPE = HighBeamStatus + NAME = 'beams-high' + STYLE = 'path {fill: %(s)s; stroke: %(s)s}' + VARIANTS = { + HighBeamStatus.ON: '#00f', + HighBeamStatus.FAULT: '#f00' + } + +class ColdStatus(enum.Enum): + OFF = 0 + ON = 1 + +class ColdIcon(StatusIcon): + TYPE = ColdStatus + NAME = 'cold' + STYLE = 'path {stroke: %(s)s;stroke-width: 8}' + VARIANTS = { + ColdStatus.ON: '#fff' + } class StatusIconBox(Gauge): - __slots__ = 'x', 'y', 'width', 'height', 'value', 'surface', 'icons', '_redraw' - - __icons__ = ( - (VehicleStatus.BATTERY_FAULT, 'battery', '#f00', '#rect5, #rect6 {fill: %(s)s} #rect4, path {stroke: %(s)s}'), - (VehicleStatus.BEAMS_FOG, 'beams-fog', '#0f0', '#g9 path {fill: %(s)s} #g5 path {stroke: %(s)s}'), - (VehicleStatus.BEAMS_HIGH, 'beams-high', '#00f', 'path {fill: %(s)s; stroke: %(s)s}'), - (VehicleStatus.BEAMS_LOW, 'beams-low', '#0f0', 'path {fill: %(s)s; stroke: %(s)s}'), - (VehicleStatus.BEAMS_PARKING, 'beams-parking', '#ff0', 'path {stroke: %(s)s; stroke-width: 12}'), - (VehicleStatus.BELT, 'belt', '#f00', 'path {fill: %(s)s}'), - (VehicleStatus.COLLISION, 'collision', '#f00', 'path {stroke-width: 10px; stroke: %(s)s}'), - (VehicleStatus.COOLANT_LOW, 'coolant', '#fa0', 'path, circle, rect {fill: %(s)s} #g9 path {stroke: %(s)s}'), - (VehicleStatus.COOLANT_OVERHEAT, 'coolant', '#f00', 'path, circle, rect {fill: %(s)s} #g9 path {stroke: %(s)s}'), - (VehicleStatus.CRUISE, 'cruise', '#0f0', 'path, circle {fill: %(s)s; stroke: %(s)s}'), - (VehicleStatus.FUEL_LOW, 'fuel', '#fa0', '#g9 * {stroke: %(s)s} #path9 {fill: %(s)s}'), - (VehicleStatus.LANEKEEP_OFF, 'lane-departure', '#fa0', 'path {stroke: %(s)s; stroke-width: 8}'), - (VehicleStatus.OIL_LOW, 'oil', '#fa0', 'path {fill: %(s)s; stroke: %(s)s}'), - (VehicleStatus.OIL_OVERHEAT, 'oil', '#f00', 'path {fill: %(s)s; stroke: %(s)s}'), - (VehicleStatus.PARKING_BRAKE_ON, 'parking', '#fa0', '#path6, #path5 {stroke: %(s)s} circle {stroke: %(s)s} #path7 {fill: %(s)s}'), - (VehicleStatus.PARKING_BRAKE_FAULT, 'parking', '#f00', '#path6, #path5 {stroke: %(s)s} circle {stroke: %(s)s} #path7 {fill: %(s)s}'), - (VehicleStatus.STABILITY_OFF, 'stability', '#fa0', '#path9, #path8, #path7, #path6, #path5 {fill: %(s)s} #path10 {stroke: %(s)s}'), - (VehicleStatus.TPMS_WARNING, 'tpms', '#fa0', 'path, circle {fill: %(s)s}'), - (VehicleStatus.WIPER_WASHER_LOW, 'wiper-washer', '#fa0'), - ) + __slots__ = 'x', 'y', 'width', 'height', 'surface', 'statuses', 'icons', \ + '_redraw' ICON_WIDTH = 48 ICON_HEIGHT = 48 + ICONS = ( + CollisionIcon, FuelIcon, SeatBeltIcon, CoolantIcon, OilIcon, + ParkingBrakeIcon, WiperWasherIcon, LaneKeepIcon, StabilityControlIcon, + TPMSIcon, FogBeamIcon, ParkingBeamIcon, LowBeamIcon, HighBeamIcon + ) + def __init__(self, x: float, y: float, width: float, height: float): self.x = x self.y = y self.width = width self.height = height - self.value = VehicleStatus.OK.value self.surface = cairo.ImageSurface(cairo.FORMAT_ARGB32,int(width), int(height)) self._redraw = False - self.icons: dict[int, StatusIcon] = dict() + self.statuses: dict[enum.EnumType, enum.Enum] = dict() + self.icons: dict[enum.EnumType, StatusIcon] = dict() - for item in self.__icons__: - status, name, color = item[0:3] - style = 'rect,path,circle{stroke:%(s)s;}' if len(item) < 4 else item[3] - - self.icons[status.value] = StatusIcon(status, name, - self.ICON_WIDTH, - self.ICON_HEIGHT, - color, style) + for icon_type in self.ICONS: + self.icons[icon_type.TYPE] = icon_type(self.ICON_WIDTH, self.ICON_HEIGHT) def __del__(self): self.surface.finish() @@ -161,18 +299,18 @@ class StatusIconBox(Gauge): x, y = 0, self.height - self.ICON_HEIGHT - for status_value in self.icons: - icon = self.icons[status_value] + for typeof in self.statuses: + status = self.statuses[typeof] + icon = self.icons[typeof] - if self.value & status_value: - if icon is not None: - icon.draw(cr, x, y) + if icon.drawable(status): + icon.draw(cr, x, y) - x += self.ICON_WIDTH + x += self.ICON_WIDTH - if x >= self.width: - x = 0 - y -= self.ICON_HEIGHT + if x >= self.width: + x = 0 + y -= self.ICON_HEIGHT def draw_fg(self, cr: cairo.Context): if self._redraw: @@ -183,14 +321,5 @@ class StatusIconBox(Gauge): cr.rectangle(self.x, self.y, self.width, self.height) cr.fill() - def reset(self): - self.value = VehicleStatus.OK.value - self._redraw = True - - def set_status(self, status: VehicleStatus): - self.value |= status.value - self._redraw = True - - def clear_status(self, status: VehicleStatus): - self.value &= ~status.value - self._redraw = True + def set(self, status: enum.Enum): + self.statuses[type(status)] = status diff --git a/py/hexagram/svg.py b/py/hexagram/svg.py index 52072d2..bb5b145 100644 --- a/py/hexagram/svg.py +++ b/py/hexagram/svg.py @@ -23,7 +23,7 @@ def render_to_surface(path: str, surface: cairo.Surface, width: float, height: f svg.render_layer(cr, None, rect) -def render_to_image(path: str, width: float, height: float, style: Optional[str]=None) -> cairo.Surface: +def render_to_image(path: str, width: float, height: float, style: Optional[str]=None) -> cairo.ImageSurface: surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, int(width), int(height)) render_to_surface(path, surface, width, height, style)