Files
2026-08-03 16:37:20 +08:00

316 lines
7.5 KiB
Python

from enum import Enum
from typing_extensions import List, Literal, Optional, Tuple, Union
from pydantic import BaseModel, model_validator
from pydantic_core import from_json, PydanticCustomError
class StrEnum(str, Enum):
@staticmethod
def _generate_next_value_(name, start, count, last_values):
return name
def __str__(self) -> str:
return self.value
ADCNameType = Literal["MAIN", "EXT", "SPI"]
class ADC(BaseModel):
name: ADCNameType
adc: str
sample_time: Optional[str] = None
dma: Optional[str] = None
dma_channel: Optional[str] = None
dma_stream: Optional[str] = None
dma_stream_irq: Optional[str] = None
dma_stream_irq_handler: Optional[str] = None
# SPI ADC fields
gpio_pin_sck: Optional[str] = None
gpio_pin_miso: Optional[str] = None
gpio_pin_mosi: Optional[str] = None
gpio_pin_cs: Optional[str] = None
StickEnum = StrEnum(
"StickEnum",
(
# 2 Gimbal radios
"LH",
"LV",
"RV",
"RH",
# Surface radios
"ST",
"TH",
),
)
MAX_POTS = 8
MAX_SLIDERS = 8
MAX_EXT = 16
MAX_RAW = 8
FlexInputEnum = StrEnum(
"FlexInputEnum",
(
[f"P{i}" for i in range(1, MAX_POTS)]
+ [f"SL{i}" for i in range(1, MAX_SLIDERS)]
+ [f"EXT{i}" for i in range(1, MAX_EXT)]
+ ["JSx", "JSy"]
),
)
SwitchInputEnum = StrEnum(
"SwitchInputEnum",
list([f"SW{chr(i)}" for i in range(ord("A"), ord("Z") + 1)]),
)
RawInputEnum = StrEnum("RawInputEnum", list([f"RAW{i}" for i in range(1, MAX_RAW)]))
InputNameType = Union[StickEnum, FlexInputEnum, SwitchInputEnum, RawInputEnum]
InputType = StrEnum(
"InputType",
(
"STICK",
"FLEX",
"SWITCH",
"VBAT",
"RTC_BAT",
"RAW",
"LUX",
),
)
FlexType = StrEnum(
"FlexType",
(
"NONE",
"POT",
"POT_CENTER",
"SLIDER",
"MULTIPOS",
"AXIS_X",
"AXIS_Y",
),
)
class StickInput(BaseModel):
name: StickEnum
type: Literal["STICK"]
adc: Optional[ADCNameType] = None
gpio: Optional[str] = None
pin: Optional[str] = None
channel: Optional[Union[str, int]] = None
inverted: Optional[bool] = False
pwm_channel: Optional[int] = None
class FlexInput(BaseModel):
name: FlexInputEnum
type: Literal["FLEX"]
adc: ADCNameType
gpio: Optional[str] = None
pin: Optional[str] = None
channel: Optional[Union[str, int]] = None
inverted: Optional[bool] = False
default: Optional[FlexType] = None
label: Optional[str] = None
short_label: Optional[str] = None
class SwitchInput(BaseModel):
name: SwitchInputEnum
type: Literal["SWITCH"]
adc: ADCNameType
gpio: Optional[str] = None
pin: Optional[str] = None
channel: Optional[Union[str, int]] = None
inverted: Optional[bool] = False
class RawInput(BaseModel):
name: RawInputEnum
type: Literal["RAW"]
adc: ADCNameType
gpio: Optional[str] = None
pin: Optional[str] = None
channel: Optional[Union[str, int]] = None
class VBatInput(BaseModel):
name: Literal["VBAT"]
type: Literal["VBAT"]
adc: ADCNameType
gpio: Optional[str] = None
pin: Optional[str] = None
channel: Optional[Union[str, int]] = None
class RTCBatInput(BaseModel):
name: Literal["RTC_BAT"]
type: Literal["RTC_BAT"]
adc: Literal["MAIN", "EXT"]
channel: str
class LuxInput(BaseModel):
name: Literal["LUX"]
type: Literal["LUX"]
adc: ADCNameType
gpio: Optional[str] = None
pin: Optional[str] = None
channel: Optional[Union[str, int]] = None
Input = Union[StickInput, FlexInput, SwitchInput, RawInput, VBatInput, RTCBatInput, LuxInput]
class ADCInputs(BaseModel):
adcs: List[ADC]
inputs: List[Input]
# MUST be the same order as 'EnumKeys'
KeyEnum = StrEnum(
"KeyEnum",
(
"KEY_MENU",
"KEY_EXIT",
"KEY_ENTER",
"KEY_PAGEUP",
"KEY_PAGEDN",
"KEY_UP",
"KEY_DOWN",
"KEY_LEFT",
"KEY_RIGHT",
"KEY_PLUS",
"KEY_MINUS",
"KEY_MODEL",
"KEY_TELE",
"KEY_SYS",
"KEY_SHIFT",
"KEY_BIND",
),
)
class Key(BaseModel):
name: str
label: str
key: KeyEnum
active_low: Optional[bool] = False
gpio: Optional[str] = None
pin: Optional[str] = None
@model_validator(mode="after")
def check_hardware(self: "Key") -> "Key":
if bool(self.gpio) != bool(self.pin):
raise PydanticCustomError(
"KeyHardwareError",
"Key missing either 'gpio' or 'pin'",
)
return self
SwitchHardwareTypeEnum = StrEnum(
"SwitchTypeEnum",
(
"2POS",
"3POS",
"ADC",
"FSWITCH",
),
)
SwitchTypeEnum = StrEnum(
"SwitchTypeEnum",
(
"2POS",
"3POS",
"TOGGLE",
"NONE",
),
)
SwitchDisplayType = Union[Tuple[int, int], Tuple]
class Switch(BaseModel):
name: str
type: SwitchHardwareTypeEnum
default: Optional[SwitchTypeEnum] = SwitchTypeEnum.NONE
flags: Optional[int] = 0
inverted: Optional[bool] = False
is_cfs: Optional[bool] = False
cfs_idx: Optional[int] = None
adc_input: Optional[str] = None
gpio: Optional[str] = None
pin: Optional[str] = None
gpio_high: Optional[str] = None
pin_high: Optional[str] = None
gpio_low: Optional[str] = None
pin_low: Optional[str] = None
display: Optional[SwitchDisplayType] = None
def _uses_single_gpio(self: "Switch") -> bool:
return bool(self.pin)
def _uses_two_gpios(self: "Switch") -> bool:
return bool(self.pin_high or self.pin_low)
def _uses_gpio(self: "Switch") -> bool:
return bool(self.gpio_high or self.gpio_low or self.gpio)
@model_validator(mode="after")
def check_hardware(self: "Switch") -> "Switch":
if bool(self.adc_input) == self._uses_gpio():
if self.adc_input:
raise PydanticCustomError(
"SwitchHardwareError",
"A switch is either using ADC or it's own GPIO pin(s)",
)
if self._uses_gpio() and (self._uses_single_gpio() == self._uses_two_gpios()):
raise PydanticCustomError(
"SwitchHardwareError",
"A switch based on GPIO pin(s) uses either a single or two GPIOs",
)
if self._uses_single_gpio():
if str(self.type) not in ["2POS", "FSWITCH"]:
raise PydanticCustomError(
"SwitchHardwareError",
"Single GPIO switch type is either '2POS' or 'FSWITCH'",
)
# TODO: check 'default' as well?
if self._uses_two_gpios():
if not self.pin_high or not self.pin_low:
raise PydanticCustomError(
"SwitchHardwareError",
"Switch missing 'pin_high' or 'pin_low'",
)
if str(self.type) != "3POS":
raise PydanticCustomError(
"SwitchHardwareError",
"Dual GPIO switch type is always '3POS",
)
# TODO: check 'default' as well?
return self
class HardwareDefinition(BaseModel):
adc_inputs: ADCInputs
switches: List[Switch]
keys: List[Key]
@staticmethod
def from_json(data: Union[str, bytes, bytearray]) -> "HardwareDefinition":
return HardwareDefinition.model_validate(from_json(data))