116 lines
3.0 KiB
Python
116 lines
3.0 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import Dict, List, Literal, Optional, Union
|
|
from pydantic import BaseModel, ConfigDict, Field
|
|
|
|
|
|
# -------------------------
|
|
# HIL blocks (v1.3)
|
|
# -------------------------
|
|
|
|
class TankLevelBlock(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
type: Literal["tank_level"] = "tank_level"
|
|
|
|
level_out: str
|
|
inlet_cmd: str
|
|
outlet_cmd: str
|
|
|
|
dt: float = 0.1
|
|
area: float = 1.0
|
|
max_level: float = 1.0
|
|
inflow_rate: float = 0.25
|
|
outflow_rate: float = 0.25
|
|
leak_rate: float = 0.0
|
|
|
|
initial_level: Optional[float] = None
|
|
|
|
|
|
class BottleLineBlock(BaseModel):
|
|
"""
|
|
Minimal bottle + conveyor dynamics (Strada A):
|
|
- bottle_at_filler_out = 1 when conveyor_cmd <= 0.5 else 0
|
|
- bottle_fill_level_out increases when at_filler==1
|
|
- bottle_fill_level_out decreases slowly when conveyor ON (new/empty bottle coming)
|
|
"""
|
|
model_config = ConfigDict(extra="forbid")
|
|
type: Literal["bottle_line"] = "bottle_line"
|
|
|
|
conveyor_cmd: str
|
|
bottle_at_filler_out: str
|
|
bottle_fill_level_out: str
|
|
|
|
dt: float = 0.1
|
|
fill_rate: float = 0.25 # per second
|
|
drain_rate: float = 0.40 # per second when conveyor ON (reset toward 0)
|
|
initial_fill: float = 0.0
|
|
|
|
|
|
HILBlock = Union[TankLevelBlock, BottleLineBlock]
|
|
|
|
|
|
class IRHIL(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
name: str
|
|
logic: str
|
|
|
|
outputs_init: Dict[str, float] = Field(default_factory=dict)
|
|
blocks: List[HILBlock] = Field(default_factory=list)
|
|
|
|
|
|
# -------------------------
|
|
# PLC rules (v1.2)
|
|
# -------------------------
|
|
|
|
class HysteresisFillRule(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
type: Literal["hysteresis_fill"] = "hysteresis_fill"
|
|
|
|
level_in: str
|
|
low: float = 0.2
|
|
high: float = 0.8
|
|
|
|
inlet_out: str
|
|
outlet_out: str
|
|
|
|
enable_input: Optional[str] = None
|
|
|
|
# Signal range for converting normalized thresholds to absolute values
|
|
# If signal_max=1000, then low=0.2 becomes 200, high=0.8 becomes 800
|
|
signal_max: float = 1.0 # Default 1.0 means thresholds are already absolute
|
|
|
|
|
|
class ThresholdOutputRule(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
type: Literal["threshold_output"] = "threshold_output"
|
|
|
|
input_id: str
|
|
threshold: float = 0.2
|
|
op: Literal["lt"] = "lt"
|
|
|
|
output_id: str
|
|
true_value: int = 1
|
|
false_value: int = 0
|
|
|
|
# Signal range for converting normalized threshold to absolute value
|
|
# If signal_max=200, then threshold=0.2 becomes 40
|
|
signal_max: float = 1.0 # Default 1.0 means threshold is already absolute
|
|
|
|
|
|
PLCRule = Union[HysteresisFillRule, ThresholdOutputRule]
|
|
|
|
|
|
class IRPLC(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
name: str
|
|
logic: str
|
|
|
|
rules: List[PLCRule] = Field(default_factory=list)
|
|
|
|
|
|
class IRSpec(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
version: Literal["ir_v1"] = "ir_v1"
|
|
plcs: List[IRPLC] = Field(default_factory=list)
|
|
hils: List[IRHIL] = Field(default_factory=list)
|