Man this refactor has been hurting my head

This commit is contained in:
XANTRONIX 2024-01-06 18:52:52 -05:00
parent a86461ac6b
commit dac34a46a2
4 changed files with 329 additions and 141 deletions

View file

@ -11,7 +11,7 @@ from hexagram.speedo import Speedo
from hexagram.tacho import Tacho from hexagram.tacho import Tacho
from hexagram.fuel import FuelGauge from hexagram.fuel import FuelGauge
from hexagram.thermo import Thermometer from hexagram.thermo import Thermometer
from hexagram.status import VehicleStatus, StatusIcon, StatusIconBox from hexagram.status import StatusIconBox, ColdStatus, ColdIcon
class ShiftIndicator(Gauge): class ShiftIndicator(Gauge):
__slots__ = 'x', 'y', 'rpm_min', 'rpm_redline', 'rpm_max', __slots__ = 'x', 'y', 'rpm_min', 'rpm_redline', 'rpm_max',
@ -100,7 +100,6 @@ class AmbientTemp(TextGauge):
ICON_WIDTH = 40 ICON_WIDTH = 40
ICON_HEIGHT = 40 ICON_HEIGHT = 40
ICON_PADDING = 16 ICON_PADDING = 16
ICON_COLOR = '#fff'
COLD_THRESHOLD = 4.0 # °C COLD_THRESHOLD = 4.0 # °C
@ -108,12 +107,7 @@ class AmbientTemp(TextGauge):
super().__init__(x, y, -100, 100, align) super().__init__(x, y, -100, 100, align)
self.value = 0 self.value = 0
self.icon = StatusIcon(VehicleStatus.COLD, self.icon = ColdIcon(self.ICON_WIDTH, self.ICON_HEIGHT)
'cold',
self.ICON_WIDTH,
self.ICON_HEIGHT,
self.ICON_COLOR,
'path {stroke: %(s)s;stroke-width: 8}')
def format_text(self): def format_text(self):
return "%.1f°C" % self.value return "%.1f°C" % self.value
@ -149,7 +143,8 @@ class AmbientTemp(TextGauge):
self.icon.draw(cr, self.icon.draw(cr,
text_x_offset + width - self.icon.width, text_x_offset + width - self.icon.width,
self.y - icon_y_offset) self.y - icon_y_offset,
ColdStatus.ON)
class Clock(TextGauge): class Clock(TextGauge):
def __init__(self, x: float, y: float, align: Align): def __init__(self, x: float, y: float, align: Align):

64
py/hexagram/icon.py Normal file
View file

@ -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()

View file

@ -4,147 +4,285 @@ import cairo
from typing import Optional from typing import Optional
from hexagram.svg import render_to_image
from hexagram.gauge import Gauge from hexagram.gauge import Gauge
from hexagram.icon import Icon
class VehicleStatus(enum.Enum): class StatusIcon(Icon):
TYPE = enum.Enum
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 OK = 0
ABS_FAULT = 1 << 0 DETECTED = 1
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
__strings__ = { class CollisionIcon(StatusIcon):
OK: 'OK', ABS_FAULT: "ABS fault", AIRBAG_FAULT: "airbag fault", TYPE = CollisionStatus
BATTERY_FAULT: "low battery voltage", BEAMS_FOG: "foglights on", NAME = 'collision'
BEAMS_HIGH: "highbeams on", BEAMS_LOW: "lowbeams on", STYLE = 'path {stroke-width: 10px; stroke: %(s)s}'
BEAMS_PARKING: "parking beams on", BELT: "warning: fasten seatbelt", VARIANTS = {
CAUTION: "caution", COLLISION: "collision detected", COLD: "outside temperatures cold", CollisionStatus.DETECTED: '#f00'
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 __str__(self): class FuelStatus(enum.Enum):
return self.__strings__.get(self.value, "unknown") OK = 0
LOW = 1
FAULT = 2
class StatusIcon(): class FuelIcon(StatusIcon):
__slots__ = 'status', 'name', 'width', 'height', 'color', 'surface', TYPE = FuelStatus
NAME = 'fuel'
STYLE = '#g9 * {stroke: %(s)s} #path9 {fill: %(s)s}'
VARIANTS = {
FuelStatus.LOW: '#fa0',
FuelStatus.FAULT: '#f00'
}
__dirs__ = ('./icons', class SeatBeltStatus(enum.Enum):
'/usr/local/share/hexagram/icons' OK = 0
'/usr/share/hexagram/icons') UNFASTENED = 1
FAULT = 2
@staticmethod class SeatBeltIcon(StatusIcon):
def _locate(name: str): TYPE = SeatBeltStatus
for dir in StatusIcon.__dirs__: NAME = 'belt'
path = "%s/%s.svg" % (dir, name) STYLE = 'path {fill: %(s)s}'
VARIANTS = {
SeatBeltStatus.UNFASTENED: '#f00'
}
if os.path.exists(path): class CoolantStatus(enum.Enum):
return path 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): class OilStatus(enum.Enum):
self.status = status OK = 0
self.name = name LOW = 1
self.width = width OVERHEAT = 2
self.height = height FAULT = 3
self.color = color
if style is None: class OilIcon(StatusIcon):
style = 'rect,path,circle{stroke:%(s)s;}' 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): class WiperWasherStatus(enum.Enum):
if self.surface is not None: OK = 0
self.surface.finish() LOW = 1
FAULT = 2
def draw(self, cr: cairo.Context, x: float, y: float): class WiperWasherIcon(StatusIcon):
cr.set_source_surface(self.surface, x, y) TYPE = WiperWasherStatus
cr.rectangle(x, y, self.width, self.height) NAME = 'wiper-washer'
cr.fill() 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): class StatusIconBox(Gauge):
__slots__ = 'x', 'y', 'width', 'height', 'value', 'surface', 'icons', '_redraw' __slots__ = 'x', 'y', 'width', 'height', 'surface', 'statuses', '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'),
)
ICON_WIDTH = 48 ICON_WIDTH = 48
ICON_HEIGHT = 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): def __init__(self, x: float, y: float, width: float, height: float):
self.x = x self.x = x
self.y = y self.y = y
self.width = width self.width = width
self.height = height self.height = height
self.value = VehicleStatus.OK.value
self.surface = cairo.ImageSurface(cairo.FORMAT_ARGB32,int(width), int(height)) self.surface = cairo.ImageSurface(cairo.FORMAT_ARGB32,int(width), int(height))
self._redraw = False 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__: for icon_type in self.ICONS:
status, name, color = item[0:3] self.icons[icon_type.TYPE] = icon_type(self.ICON_WIDTH, self.ICON_HEIGHT)
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)
def __del__(self): def __del__(self):
self.surface.finish() self.surface.finish()
@ -161,11 +299,11 @@ class StatusIconBox(Gauge):
x, y = 0, self.height - self.ICON_HEIGHT x, y = 0, self.height - self.ICON_HEIGHT
for status_value in self.icons: for typeof in self.statuses:
icon = self.icons[status_value] status = self.statuses[typeof]
icon = self.icons[typeof]
if self.value & status_value: if icon.drawable(status):
if icon is not None:
icon.draw(cr, x, y) icon.draw(cr, x, y)
x += self.ICON_WIDTH x += self.ICON_WIDTH
@ -183,14 +321,5 @@ class StatusIconBox(Gauge):
cr.rectangle(self.x, self.y, self.width, self.height) cr.rectangle(self.x, self.y, self.width, self.height)
cr.fill() cr.fill()
def reset(self): def set(self, status: enum.Enum):
self.value = VehicleStatus.OK.value self.statuses[type(status)] = status
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

View file

@ -23,7 +23,7 @@ def render_to_surface(path: str, surface: cairo.Surface, width: float, height: f
svg.render_layer(cr, None, rect) 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)) surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, int(width), int(height))
render_to_surface(path, surface, width, height, style) render_to_surface(path, surface, width, height, style)