Files
edgetx-gx12/radio/util/hw_defs/models.py
T

454 lines
12 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
class EXTI(BaseModel):
irq: str
priority: int
class Timers(BaseModel):
cpu_freq: int
peri1_frequency: int
peri2_frequency: int
timer_mult_apb1: int
timer_mult_apb2: int
ms_timer: str
ms_timer_irqn: str
ms_timer_irqhandler: str
mixer_scheduler_timer: str
mixer_scheduler_timer_freq: str
mixer_scheduler_timer_irqn: str
mixer_scheduler_timer_irqhandler: str
class Backlight(BaseModel):
has_backlight_color: Optional[bool] = False
backlight_gpio: Optional[str] = None
backlight_timer: Optional[str] = None
backlight_timer_channel: Optional[str] = None
backlight_gpio_af: Optional[str] = None
backlight_timer_freq: Optional[str] = None
backlight_bdtr: Optional[str] = None
class Display(BaseModel):
lcd_w: int
lcd_h: int
lcd_phys_w: int
lcd_phys_h: int
lcd_depth: int
oled_screen: Optional[bool] = False
lcd_horizontal_invert: Optional[bool] = False
lcd_vertical_invert: Optional[bool] = False
ltdc_irq_prio: Optional[int] = None
dma_screen_irq_prio: Optional[int] = None
class LEDS(BaseModel):
led_strip_length: Optional[int] = None
bling_led_strip_start: Optional[int] = None
bling_led_strip_length: Optional[int] = None
cfs_led_strip_start: Optional[int] = None
cfs_led_strip_length: Optional[int] = None
cfs_leds_per_switch: Optional[int] = None
led_strip_gpio: Optional[str] = None
led_strip_gpio_af: Optional[str] = None
led_strip_timer: Optional[str] = None
led_strip_timer_freq: Optional[str] = None
led_strip_timer_channel: Optional[str] = None
led_strip_timer_dma: Optional[str] = None
led_strip_timer_dma_channel: Optional[str] = None
led_strip_timer_dma_stream: Optional[str] = None
led_strip_timer_dma_irqn: Optional[str] = None
led_strip_timer_dma_irqhandler: Optional[str] = None
led_strip_refresh_period: Optional[int] = None
status_leds: Optional[bool] = False
gpio_led_gpio_on: Optional[str] = None
gpio_led_gpio_off: Optional[str] = None
led_red_gpio: Optional[str] = None
led_red2_gpio: Optional[str] = None
led_green_gpio: Optional[str] = None
led_blue_gpio: Optional[str] = None
class IMU(BaseModel):
imu_i2c_bus: Optional[str] = None
imu_int_gpio: Optional[str] = None
imu_i2c_address: Optional[str] = None
exti: Optional[List[EXTI]] = None
imu_invert_x: Optional[bool] = None
imu_invert_y: Optional[bool] = None
class RotEnc(BaseModel):
rotary_encoder_inverted: Optional[bool] = None
rotary_encoder_gpio: Optional[str] = None
rotary_encoder_gpio_a: Optional[str] = None
rotary_encoder_gpio_b: Optional[str] = None
rotary_encoder_gpio_pin_a: Optional[str] = None
rotary_encoder_gpio_pin_b: Optional[str] = None
rotary_encoder_position: Optional[str] = None
rotary_encoder_exti_line1: Optional[str] = None
rotary_encoder_exti_line2: Optional[str] = None
rotary_encoder_exti_port: Optional[str] = None
rotary_encoder_exti_port_a: Optional[str] = None
rotary_encoder_exti_port_b: Optional[str] = None
rotary_encoder_exti_sys_line1: Optional[str] = None
rotary_encoder_exti_sys_line2: Optional[str] = None
rotary_encoder_timer: Optional[str] = None
rotary_encoder_timer_irqn: Optional[str] = None
rotary_encoder_timer_irqhandler: Optional[str] = None
exti: Optional[List[EXTI]] = None
class Haptic(BaseModel):
haptic_pwm: Optional[bool] = None
haptic_gpio: Optional[str] = None
haptic_gpio_timer: Optional[str] = None
haptic_gpio_af: Optional[str] = None
haptic_timer_output_enable: Optional[str] = None
haptic_timer_mode: Optional[str] = None
haptic_timer_compare_value: Optional[str] = None
haptic_timer: Optional[str] = None
haptic_timer_freq: Optional[str] = None
haptic_counter_register: Optional[str] = None
haptic_ccmr1: Optional[str] = None
haptic_ccmr2: Optional[str] = None
haptic_ccer: Optional[str] = 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]
display: Display
leds: Optional[LEDS] = None
backlight: Optional[Backlight] = None
timers: Timers
imu: Optional[IMU] = None
rotenc: Optional[RotEnc] = None
haptic: Optional[Haptic] = None
key_lock_combo: Optional[List[KeyEnum]] = None
@model_validator(mode="after")
def check_key_lock_combo(self: "HardwareDefinition") -> "HardwareDefinition":
if self.key_lock_combo is None:
return self
if len(self.key_lock_combo) != 2:
raise PydanticCustomError(
"KeyLockComboError",
"'key_lock_combo' must list exactly 2 keys",
)
if self.key_lock_combo[0] == self.key_lock_combo[1]:
raise PydanticCustomError(
"KeyLockComboError",
"'key_lock_combo' entries must be distinct",
)
defined = {k.key for k in self.keys}
for k in self.key_lock_combo:
if k not in defined:
raise PydanticCustomError(
"KeyLockComboError",
f"'key_lock_combo' references {k} which is not in 'keys'",
)
return self
@staticmethod
def from_json(data: Union[str, bytes, bytearray]) -> "HardwareDefinition":
return HardwareDefinition.model_validate(from_json(data))