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.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):

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 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

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