sprint-0: fundaciones VMS-Sailor
Sprint 0 completo del producto VMS-Sailor (Vessel Management System integrado para buques 30-40m). Brief de referencia en VMS_Sailor_v2_Parte_*.md (intacto). Core (vmssailor.core, 95.17% coverage, 99 tests verde): - ShipCoord: sistema naval x_pp/y_cl/z_bl frozen - Vessel, Deck, Bulkhead - Equipment, EquipmentModel, Sensor, EquipmentSpec - Tag, AlarmConfig, TagBinding, Scaling - CardInstance, Bus, Topology con validacion 21 puntos I/O AR-NMEA-IO-v1.0 - Alarm, PermissiveRule, Condition - Project agregado raiz con validacion cross-entity - Persistencia portable .vmsproj (SQLite) con roundtrip verificable Biblioteca curada seed (vmssailor.library): - systems_catalog.json completo (catalogo maestro Parte 1 sec 7) - 2 vessels: Sunseeker 76, Ferretti 850 - 2 motores: MTU 12V 2000 M96, Volvo D13-900 - 1 genset: Northern Lights M65C13 - yacht_motor_planeo.yaml (reglas heuristicas) - TODO marcado data_source=seed_estimate - requiere validacion datasheets Tools: - vms-validate-library: CLI valida biblioteca completa - vms-generate-test-project: CLI demo + verificacion roundtrip persistencia Design System + 8 mockups HTML estaticos: - docs/design_system.md (paleta Deep Ocean, gradientes, typography, motion) - docs/brand/ (logo + variantes SVG) - docs/mockups/splash, studio_main, runtime_overview, runtime_mimic_fuel (P&ID animado), runtime_alarms, runtime_trim (panel estrella con horizonte artificial), mobile_overview, mobile_trim - docs/mockups/index.html (galeria) Firmware (Sprint 12+ implementacion): - firmware/ar_nmea_io_v1/src/config/pinout.h con macros GPIO Decisiones autonomas documentadas en docs/decisions_sprint0.md. Stack: Python 3.11 + uv + Pydantic v2 + SQLite stdlib + hatchling + pytest 9 + ruff + mypy. Sin PySide6, FastAPI, Flutter ni firmware funcional (entran en sprints siguientes). Criterio de aceptacion Sprint 0: cumplido. - uv sync: OK - pytest: 99/99 verde - cov vmssailor.core: 95.17% (objetivo >=80%) - ruff: clean - vms-validate-library: OK - vms-generate-test-project: INTEGRIDAD OK Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
"""vmssailor.library — Biblioteca curada (Sprint 0).
|
||||
|
||||
Contiene:
|
||||
- `systems_catalog.json` — catálogo maestro de sistemas instalables
|
||||
- `vessels/` — plantillas de buques (Sunseeker 76, Ferretti 850, ...)
|
||||
- `equipment/` — modelos de equipos (motores, gensets, bombas, ...)
|
||||
- `rules/` — reglas heurísticas YAML por tipo de buque
|
||||
- `loader.py` — carga y validación de toda la biblioteca
|
||||
|
||||
**REGLA DE ORO #7:** La biblioteca curada es ORO. Cambios de formato
|
||||
requieren migración para los proyectos existentes.
|
||||
|
||||
**REGLA DE ORO #5:** Esta biblioteca pertenece a Álvaro (propiedad
|
||||
intelectual). El cliente final nunca la ve cruda — sólo el resultado de
|
||||
aplicarla en el .vmspack.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
LIBRARY_ROOT: Path = Path(__file__).parent
|
||||
"""Raíz del directorio de biblioteca curada (paquete instalado o source)."""
|
||||
|
||||
from vmssailor.library.loader import ( # noqa: E402
|
||||
LibraryLoadResult,
|
||||
load_library,
|
||||
load_systems_catalog,
|
||||
)
|
||||
|
||||
__all__ = ["LIBRARY_ROOT", "LibraryLoadResult", "load_library", "load_systems_catalog"]
|
||||
@@ -0,0 +1,130 @@
|
||||
{
|
||||
"id": "mtu_12v_2000_m96",
|
||||
"manufacturer": "MTU",
|
||||
"model_name": "12V 2000 M96",
|
||||
"category": "engine_main",
|
||||
"typical_systems": ["main_engine"],
|
||||
"specs": {
|
||||
"power_kw": 1432,
|
||||
"rpm_nominal": 2450,
|
||||
"weight_kg": 2600,
|
||||
"length_m": 2.13,
|
||||
"width_m": 1.18,
|
||||
"height_m": 1.27,
|
||||
"fuel_consumption_lph": 320
|
||||
},
|
||||
"description": "MTU Series 2000, 12 cilindros en V, 24.0 L, 2-stage turbo. Aplicación yates rápidos / patrulleros / fast ferries. Common Rail. Habla J1939 nativo en su MCU.",
|
||||
"data_source": "seed_estimate",
|
||||
"default_sensors": [
|
||||
{
|
||||
"id": "rpm",
|
||||
"name": "RPM",
|
||||
"unit_si": "rpm",
|
||||
"range_normal_min": 0,
|
||||
"range_normal_max": 2600,
|
||||
"alarm_high_value": 2550,
|
||||
"alarm_high_priority": "high",
|
||||
"default_signal_type": "pulse_magnetic_pickup",
|
||||
"description": "Régimen del cigüeñal."
|
||||
},
|
||||
{
|
||||
"id": "oil_press",
|
||||
"name": "Presión de aceite",
|
||||
"unit_si": "bar",
|
||||
"range_normal_min": 3.5,
|
||||
"range_normal_max": 6.5,
|
||||
"alarm_low_value": 1.5,
|
||||
"alarm_low_priority": "emergency",
|
||||
"default_signal_type": "4-20ma",
|
||||
"description": "Presión aceite lubricante en galería principal."
|
||||
},
|
||||
{
|
||||
"id": "oil_temp",
|
||||
"name": "Temperatura de aceite",
|
||||
"unit_si": "C",
|
||||
"range_normal_min": 60,
|
||||
"range_normal_max": 110,
|
||||
"alarm_high_value": 120,
|
||||
"alarm_high_priority": "high",
|
||||
"default_signal_type": "rtd_pt100"
|
||||
},
|
||||
{
|
||||
"id": "coolant_temp",
|
||||
"name": "Temperatura refrigerante",
|
||||
"unit_si": "C",
|
||||
"range_normal_min": 65,
|
||||
"range_normal_max": 95,
|
||||
"alarm_high_value": 100,
|
||||
"alarm_high_priority": "emergency",
|
||||
"default_signal_type": "rtd_pt100"
|
||||
},
|
||||
{
|
||||
"id": "boost_press",
|
||||
"name": "Presión de sobrealimentación",
|
||||
"unit_si": "bar",
|
||||
"range_normal_min": 0.0,
|
||||
"range_normal_max": 2.5,
|
||||
"default_signal_type": "4-20ma"
|
||||
},
|
||||
{
|
||||
"id": "fuel_press",
|
||||
"name": "Presión de combustible",
|
||||
"unit_si": "bar",
|
||||
"range_normal_min": 3.0,
|
||||
"range_normal_max": 6.0,
|
||||
"alarm_low_value": 2.0,
|
||||
"alarm_low_priority": "high",
|
||||
"default_signal_type": "4-20ma"
|
||||
},
|
||||
{
|
||||
"id": "alternator_v",
|
||||
"name": "Voltaje alternador",
|
||||
"unit_si": "V",
|
||||
"range_normal_min": 27.0,
|
||||
"range_normal_max": 29.0,
|
||||
"alarm_low_value": 24.0,
|
||||
"alarm_low_priority": "high",
|
||||
"default_signal_type": "voltage_divider"
|
||||
},
|
||||
{
|
||||
"id": "load_pct",
|
||||
"name": "Carga del motor",
|
||||
"unit_si": "%",
|
||||
"range_normal_min": 0,
|
||||
"range_normal_max": 100,
|
||||
"default_signal_type": "4-20ma"
|
||||
},
|
||||
{
|
||||
"id": "running_hours",
|
||||
"name": "Horas totales",
|
||||
"unit_si": "h",
|
||||
"range_normal_min": 0,
|
||||
"range_normal_max": 80000,
|
||||
"default_signal_type": "4-20ma"
|
||||
},
|
||||
{
|
||||
"id": "start_cmd",
|
||||
"name": "Comando arranque",
|
||||
"unit_si": "bool",
|
||||
"default_signal_type": "relay_no"
|
||||
},
|
||||
{
|
||||
"id": "stop_cmd",
|
||||
"name": "Comando parada",
|
||||
"unit_si": "bool",
|
||||
"default_signal_type": "relay_no"
|
||||
},
|
||||
{
|
||||
"id": "running_state",
|
||||
"name": "Estado motor en marcha",
|
||||
"unit_si": "bool",
|
||||
"default_signal_type": "dry_contact"
|
||||
},
|
||||
{
|
||||
"id": "estop_active",
|
||||
"name": "E-stop activado",
|
||||
"unit_si": "bool",
|
||||
"default_signal_type": "dry_contact"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
{
|
||||
"id": "volvo_d13_900hp",
|
||||
"manufacturer": "Volvo Penta",
|
||||
"model_name": "D13-900",
|
||||
"category": "engine_main",
|
||||
"typical_systems": ["main_engine"],
|
||||
"specs": {
|
||||
"power_kw": 662,
|
||||
"rpm_nominal": 2300,
|
||||
"weight_kg": 1430,
|
||||
"length_m": 1.69,
|
||||
"width_m": 0.99,
|
||||
"height_m": 1.16,
|
||||
"fuel_consumption_lph": 160
|
||||
},
|
||||
"description": "Volvo Penta D13 inline-6, 12.8 L, common rail, twin-entry turbo. Aplicación yates motor 60-90 pies. Habla J1939 nativo.",
|
||||
"data_source": "seed_estimate",
|
||||
"default_sensors": [
|
||||
{
|
||||
"id": "rpm",
|
||||
"name": "RPM",
|
||||
"unit_si": "rpm",
|
||||
"range_normal_min": 0,
|
||||
"range_normal_max": 2400,
|
||||
"alarm_high_value": 2350,
|
||||
"alarm_high_priority": "high",
|
||||
"default_signal_type": "pulse_magnetic_pickup"
|
||||
},
|
||||
{
|
||||
"id": "oil_press",
|
||||
"name": "Presión de aceite",
|
||||
"unit_si": "bar",
|
||||
"range_normal_min": 3.0,
|
||||
"range_normal_max": 5.5,
|
||||
"alarm_low_value": 1.5,
|
||||
"alarm_low_priority": "emergency",
|
||||
"default_signal_type": "4-20ma"
|
||||
},
|
||||
{
|
||||
"id": "oil_temp",
|
||||
"name": "Temperatura de aceite",
|
||||
"unit_si": "C",
|
||||
"range_normal_min": 60,
|
||||
"range_normal_max": 110,
|
||||
"alarm_high_value": 120,
|
||||
"alarm_high_priority": "high",
|
||||
"default_signal_type": "rtd_pt100"
|
||||
},
|
||||
{
|
||||
"id": "coolant_temp",
|
||||
"name": "Temperatura refrigerante",
|
||||
"unit_si": "C",
|
||||
"range_normal_min": 70,
|
||||
"range_normal_max": 92,
|
||||
"alarm_high_value": 98,
|
||||
"alarm_high_priority": "emergency",
|
||||
"default_signal_type": "rtd_pt100"
|
||||
},
|
||||
{
|
||||
"id": "boost_press",
|
||||
"name": "Presión de sobrealimentación",
|
||||
"unit_si": "bar",
|
||||
"range_normal_min": 0.0,
|
||||
"range_normal_max": 2.2,
|
||||
"default_signal_type": "4-20ma"
|
||||
},
|
||||
{
|
||||
"id": "alternator_v",
|
||||
"name": "Voltaje alternador",
|
||||
"unit_si": "V",
|
||||
"range_normal_min": 13.5,
|
||||
"range_normal_max": 14.5,
|
||||
"alarm_low_value": 12.0,
|
||||
"alarm_low_priority": "high",
|
||||
"default_signal_type": "voltage_divider"
|
||||
},
|
||||
{
|
||||
"id": "load_pct",
|
||||
"name": "Carga del motor",
|
||||
"unit_si": "%",
|
||||
"range_normal_min": 0,
|
||||
"range_normal_max": 100,
|
||||
"default_signal_type": "4-20ma"
|
||||
},
|
||||
{
|
||||
"id": "running_hours",
|
||||
"name": "Horas totales",
|
||||
"unit_si": "h",
|
||||
"range_normal_min": 0,
|
||||
"range_normal_max": 80000,
|
||||
"default_signal_type": "4-20ma"
|
||||
},
|
||||
{
|
||||
"id": "start_cmd",
|
||||
"name": "Comando arranque",
|
||||
"unit_si": "bool",
|
||||
"default_signal_type": "relay_no"
|
||||
},
|
||||
{
|
||||
"id": "stop_cmd",
|
||||
"name": "Comando parada",
|
||||
"unit_si": "bool",
|
||||
"default_signal_type": "relay_no"
|
||||
},
|
||||
{
|
||||
"id": "running_state",
|
||||
"name": "Estado motor en marcha",
|
||||
"unit_si": "bool",
|
||||
"default_signal_type": "dry_contact"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
{
|
||||
"id": "northern_lights_m65c13",
|
||||
"manufacturer": "Northern Lights",
|
||||
"model_name": "M65C13",
|
||||
"category": "genset",
|
||||
"typical_systems": ["genset"],
|
||||
"specs": {
|
||||
"power_kw": 52,
|
||||
"rpm_nominal": 1800,
|
||||
"weight_kg": 880,
|
||||
"length_m": 1.55,
|
||||
"width_m": 0.74,
|
||||
"height_m": 0.96,
|
||||
"voltage_v": 230,
|
||||
"current_a": 226,
|
||||
"fuel_consumption_lph": 16.3
|
||||
},
|
||||
"description": "Genset marino diésel Northern Lights M65C13 (Lugger), 65 kVA / 52 kW @ 1800 rpm 60 Hz (válido también a 50 Hz con curva diferente). Motor John Deere 4045 base. Aplicación yates motor 70-90 pies y patrulleros medianos.",
|
||||
"data_source": "seed_estimate",
|
||||
"default_sensors": [
|
||||
{
|
||||
"id": "rpm",
|
||||
"name": "RPM",
|
||||
"unit_si": "rpm",
|
||||
"range_normal_min": 0,
|
||||
"range_normal_max": 1850,
|
||||
"default_signal_type": "pulse_magnetic_pickup"
|
||||
},
|
||||
{
|
||||
"id": "oil_press",
|
||||
"name": "Presión de aceite",
|
||||
"unit_si": "bar",
|
||||
"range_normal_min": 2.5,
|
||||
"range_normal_max": 4.5,
|
||||
"alarm_low_value": 1.0,
|
||||
"alarm_low_priority": "emergency",
|
||||
"default_signal_type": "4-20ma"
|
||||
},
|
||||
{
|
||||
"id": "coolant_temp",
|
||||
"name": "Temperatura refrigerante",
|
||||
"unit_si": "C",
|
||||
"range_normal_min": 70,
|
||||
"range_normal_max": 95,
|
||||
"alarm_high_value": 102,
|
||||
"alarm_high_priority": "emergency",
|
||||
"default_signal_type": "rtd_pt100"
|
||||
},
|
||||
{
|
||||
"id": "voltage_l1",
|
||||
"name": "Tensión L1",
|
||||
"unit_si": "V",
|
||||
"range_normal_min": 220,
|
||||
"range_normal_max": 240,
|
||||
"alarm_low_value": 200,
|
||||
"alarm_low_priority": "high",
|
||||
"alarm_high_value": 250,
|
||||
"alarm_high_priority": "high",
|
||||
"default_signal_type": "voltage_divider"
|
||||
},
|
||||
{
|
||||
"id": "current_l1",
|
||||
"name": "Corriente L1",
|
||||
"unit_si": "A",
|
||||
"range_normal_min": 0,
|
||||
"range_normal_max": 230,
|
||||
"alarm_high_value": 250,
|
||||
"alarm_high_priority": "high",
|
||||
"default_signal_type": "4-20ma"
|
||||
},
|
||||
{
|
||||
"id": "freq",
|
||||
"name": "Frecuencia",
|
||||
"unit_si": "Hz",
|
||||
"range_normal_min": 49.5,
|
||||
"range_normal_max": 60.5,
|
||||
"default_signal_type": "pulse_inductive"
|
||||
},
|
||||
{
|
||||
"id": "load_pct",
|
||||
"name": "Carga",
|
||||
"unit_si": "%",
|
||||
"range_normal_min": 0,
|
||||
"range_normal_max": 100,
|
||||
"default_signal_type": "4-20ma"
|
||||
},
|
||||
{
|
||||
"id": "running_hours",
|
||||
"name": "Horas totales",
|
||||
"unit_si": "h",
|
||||
"range_normal_min": 0,
|
||||
"range_normal_max": 80000,
|
||||
"default_signal_type": "4-20ma"
|
||||
},
|
||||
{
|
||||
"id": "start_cmd",
|
||||
"name": "Comando arranque",
|
||||
"unit_si": "bool",
|
||||
"default_signal_type": "relay_no"
|
||||
},
|
||||
{
|
||||
"id": "stop_cmd",
|
||||
"name": "Comando parada",
|
||||
"unit_si": "bool",
|
||||
"default_signal_type": "relay_no"
|
||||
},
|
||||
{
|
||||
"id": "breaker_status",
|
||||
"name": "Estado breaker principal",
|
||||
"unit_si": "bool",
|
||||
"default_signal_type": "dry_contact"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
"""Carga y validación de la biblioteca curada.
|
||||
|
||||
Funciones públicas:
|
||||
|
||||
- `load_systems_catalog()` — devuelve el dict del catálogo maestro
|
||||
- `load_library(root=None)` — carga TODA la biblioteca y devuelve un
|
||||
`LibraryLoadResult` con vessels, equipment,
|
||||
rules y la lista de issues.
|
||||
|
||||
El loader es defensivo: NO levanta excepciones por archivos individuales
|
||||
malos. Acumula issues y deja al caller decidir si abortar.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
from pydantic import ValidationError
|
||||
|
||||
from vmssailor.core.equipment import EquipmentModel
|
||||
from vmssailor.core.vessel import Vessel
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class LibraryIssue:
|
||||
"""Un problema encontrado al cargar la biblioteca."""
|
||||
|
||||
severity: str # "error" | "warning" | "info"
|
||||
path: str
|
||||
message: str
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.severity.upper():7s} {self.path:60s} {self.message}"
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class LibraryLoadResult:
|
||||
"""Resultado de cargar la biblioteca completa."""
|
||||
|
||||
vessels: list[Vessel] = field(default_factory=list)
|
||||
equipment_models: list[EquipmentModel] = field(default_factory=list)
|
||||
rules: dict[str, dict[str, Any]] = field(default_factory=dict)
|
||||
systems_catalog: dict[str, Any] = field(default_factory=dict)
|
||||
issues: list[LibraryIssue] = field(default_factory=list)
|
||||
|
||||
@property
|
||||
def errors(self) -> list[LibraryIssue]:
|
||||
return [i for i in self.issues if i.severity == "error"]
|
||||
|
||||
@property
|
||||
def warnings(self) -> list[LibraryIssue]:
|
||||
return [i for i in self.issues if i.severity == "warning"]
|
||||
|
||||
def ok(self) -> bool:
|
||||
return len(self.errors) == 0
|
||||
|
||||
def format(self) -> str:
|
||||
lines: list[str] = []
|
||||
lines.append("Library load summary:")
|
||||
lines.append(f" Vessels : {len(self.vessels)}")
|
||||
lines.append(f" EquipmentModels: {len(self.equipment_models)}")
|
||||
lines.append(f" Rules : {len(self.rules)}")
|
||||
lines.append(f" Issues : {len(self.errors)} errors, {len(self.warnings)} warnings")
|
||||
if self.issues:
|
||||
lines.append("")
|
||||
for i in self.issues:
|
||||
lines.append(f" {i}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def load_systems_catalog(root: Path | None = None) -> dict[str, Any]:
|
||||
"""Carga el catálogo maestro de sistemas."""
|
||||
base = root or _default_root()
|
||||
catalog_path = base / "systems_catalog.json"
|
||||
if not catalog_path.exists():
|
||||
raise FileNotFoundError(f"systems_catalog.json no encontrado en {base}")
|
||||
return json.loads(catalog_path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def load_library(root: Path | None = None) -> LibraryLoadResult:
|
||||
"""Carga la biblioteca completa y devuelve un `LibraryLoadResult`."""
|
||||
base = root or _default_root()
|
||||
result = LibraryLoadResult()
|
||||
|
||||
# systems_catalog
|
||||
try:
|
||||
result.systems_catalog = load_systems_catalog(base)
|
||||
except Exception as exc:
|
||||
result.issues.append(
|
||||
LibraryIssue("error", "systems_catalog.json", f"{type(exc).__name__}: {exc}")
|
||||
)
|
||||
|
||||
# vessels/
|
||||
vessels_dir = base / "vessels"
|
||||
if vessels_dir.exists():
|
||||
for f in sorted(vessels_dir.glob("*.json")):
|
||||
try:
|
||||
data = json.loads(f.read_text(encoding="utf-8"))
|
||||
v = Vessel(**data)
|
||||
result.vessels.append(v)
|
||||
if v.data_source == "seed_estimate":
|
||||
result.issues.append(
|
||||
LibraryIssue(
|
||||
"info",
|
||||
str(f.relative_to(base)),
|
||||
"data_source=seed_estimate — requiere validación de Álvaro.",
|
||||
)
|
||||
)
|
||||
except ValidationError as exc:
|
||||
result.issues.append(
|
||||
LibraryIssue("error", str(f.relative_to(base)), f"Pydantic: {exc.errors()}")
|
||||
)
|
||||
except Exception as exc:
|
||||
result.issues.append(
|
||||
LibraryIssue(
|
||||
"error", str(f.relative_to(base)), f"{type(exc).__name__}: {exc}"
|
||||
)
|
||||
)
|
||||
|
||||
# equipment/**/*.json
|
||||
eq_dir = base / "equipment"
|
||||
if eq_dir.exists():
|
||||
for f in sorted(eq_dir.glob("**/*.json")):
|
||||
try:
|
||||
data = json.loads(f.read_text(encoding="utf-8"))
|
||||
em = EquipmentModel(**data)
|
||||
result.equipment_models.append(em)
|
||||
if em.data_source == "seed_estimate":
|
||||
result.issues.append(
|
||||
LibraryIssue(
|
||||
"info",
|
||||
str(f.relative_to(base)),
|
||||
"data_source=seed_estimate — requiere validación.",
|
||||
)
|
||||
)
|
||||
except ValidationError as exc:
|
||||
result.issues.append(
|
||||
LibraryIssue("error", str(f.relative_to(base)), f"Pydantic: {exc.errors()}")
|
||||
)
|
||||
except Exception as exc:
|
||||
result.issues.append(
|
||||
LibraryIssue(
|
||||
"error", str(f.relative_to(base)), f"{type(exc).__name__}: {exc}"
|
||||
)
|
||||
)
|
||||
|
||||
# rules/*.yaml
|
||||
rules_dir = base / "rules"
|
||||
if rules_dir.exists():
|
||||
for f in sorted(rules_dir.glob("*.yaml")):
|
||||
try:
|
||||
data = yaml.safe_load(f.read_text(encoding="utf-8")) or {}
|
||||
meta = data.get("meta") or {}
|
||||
rule_id = meta.get("rule_id") or f.stem
|
||||
result.rules[rule_id] = data
|
||||
except Exception as exc:
|
||||
result.issues.append(
|
||||
LibraryIssue(
|
||||
"error", str(f.relative_to(base)), f"{type(exc).__name__}: {exc}"
|
||||
)
|
||||
)
|
||||
|
||||
# Validación cross-references
|
||||
_validate_cross_refs(result)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _validate_cross_refs(result: LibraryLoadResult) -> None:
|
||||
"""Chequea referencias entre rules → equipment models → systems."""
|
||||
em_ids = {em.id for em in result.equipment_models}
|
||||
valid_systems = set()
|
||||
for cat in result.systems_catalog.get("categories", []):
|
||||
for s in cat.get("systems", []):
|
||||
valid_systems.add(s["id"])
|
||||
|
||||
# Chequear que equipment.typical_systems referencien sistemas válidos
|
||||
for em in result.equipment_models:
|
||||
for sys_id in em.typical_systems:
|
||||
if sys_id.value not in valid_systems:
|
||||
result.issues.append(
|
||||
LibraryIssue(
|
||||
"error",
|
||||
f"equipment/.../{em.id}.json",
|
||||
f"typical_systems contiene '{sys_id.value}' que no existe en "
|
||||
"systems_catalog.json.",
|
||||
)
|
||||
)
|
||||
|
||||
# Chequear que rules referencien EquipmentModel.id existentes
|
||||
for rule_id, rule_data in result.rules.items():
|
||||
proposals = rule_data.get("equipment_proposals") or {}
|
||||
for _sys_id, sys_proposals in proposals.items():
|
||||
if not isinstance(sys_proposals, dict):
|
||||
continue
|
||||
for cand in sys_proposals.get("candidates", []) or []:
|
||||
model_ref = cand.get("model_ref")
|
||||
if model_ref and model_ref not in em_ids:
|
||||
result.issues.append(
|
||||
LibraryIssue(
|
||||
"warning",
|
||||
f"rules/{rule_id}.yaml",
|
||||
f"Rule referencia model_ref='{model_ref}' que no existe en "
|
||||
"equipment_models cargados.",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _default_root() -> Path:
|
||||
return Path(__file__).parent
|
||||
@@ -0,0 +1,161 @@
|
||||
# Reglas heurísticas para yates motor planeo 20-30 m.
|
||||
#
|
||||
# Este archivo captura el conocimiento de Álvaro sobre qué sistemas y qué
|
||||
# equipos lleva típicamente un yate motor planeo del segmento objetivo.
|
||||
# El motor de reglas del Studio (Sprint 2) consulta este archivo en el
|
||||
# Paso 5 del wizard para proponer equipos al integrador.
|
||||
#
|
||||
# Filosofía: PROPONE, no impone. El integrador siempre puede ajustar.
|
||||
# data_source de cada propuesta es "seed_estimate" y queda en
|
||||
# docs/seed_data_notes.md hasta que Álvaro lo valide contra proyectos reales.
|
||||
|
||||
meta:
|
||||
version: 1
|
||||
rule_id: yacht_motor_planeo
|
||||
applies_to:
|
||||
vessel_types: ["yacht_motor"]
|
||||
vessel_subtypes: ["planing", "semi_planing"]
|
||||
length_overall_m:
|
||||
min: 18.0
|
||||
max: 32.0
|
||||
data_source: seed_estimate
|
||||
|
||||
# ----- Sistemas que típicamente se incluyen -------------------------------
|
||||
|
||||
systems_default_enabled:
|
||||
- main_engine
|
||||
- transmission
|
||||
- shaft_propeller
|
||||
- thruster
|
||||
- trim_tabs
|
||||
- genset
|
||||
- shore_power
|
||||
- msb
|
||||
- solar
|
||||
- fuel
|
||||
- lube_oil
|
||||
- fw_cooling
|
||||
- sw_cooling
|
||||
- bilge
|
||||
- potable_water
|
||||
- watermaker
|
||||
- fire_detection
|
||||
- fire_extinguishing
|
||||
- hvac
|
||||
- engine_vent
|
||||
- nav_lights
|
||||
- deck_lights
|
||||
- interior_lights
|
||||
- emergency_lights
|
||||
- fuel_tanks
|
||||
- water_tanks
|
||||
- grey_black_tanks
|
||||
- windlass
|
||||
- anchor_system
|
||||
|
||||
systems_optional:
|
||||
- gyrostabilizer # Seakeeper se vuelve muy común en este rango
|
||||
- joystick_docking
|
||||
- inverter_charger
|
||||
- battery_bank
|
||||
- searchlights
|
||||
- davits
|
||||
- gangway
|
||||
|
||||
# ----- Equipos propuestos por sistema --------------------------------------
|
||||
|
||||
equipment_proposals:
|
||||
|
||||
main_engine:
|
||||
# Para yates de 20-25 m, MTU o Volvo en pares. Para 25-32 m, MTU.
|
||||
count: 2
|
||||
candidates:
|
||||
- model_ref: mtu_12v_2000_m96
|
||||
when:
|
||||
length_overall_m: { min: 22.0, max: 32.0 }
|
||||
rationale: "Estándar de oro en este rango. Buena disponibilidad de partes y servicio."
|
||||
- model_ref: volvo_d13_900hp
|
||||
when:
|
||||
length_overall_m: { min: 18.0, max: 26.0 }
|
||||
rationale: "Más liviano y económico que MTU 2000. Servicio mundial Volvo Penta."
|
||||
location_template:
|
||||
port: { x_pp_pct: 0.25, y_cl: -0.9, z_bl: 1.2 }
|
||||
starboard: { x_pp_pct: 0.25, y_cl: 0.9, z_bl: 1.2 }
|
||||
tag_prefix_template: "ME_{side}"
|
||||
sides: ["PORT", "STBD"]
|
||||
|
||||
genset:
|
||||
count: 1
|
||||
candidates:
|
||||
- model_ref: northern_lights_m65c13
|
||||
when:
|
||||
length_overall_m: { min: 18.0, max: 30.0 }
|
||||
rationale: "Confiabilidad probada. Aceptado por clase RINA/Lloyd's con poco trámite."
|
||||
location_template:
|
||||
default: { x_pp_pct: 0.20, y_cl: 0.0, z_bl: 1.0 }
|
||||
tag_prefix_template: "GEN_{idx}"
|
||||
|
||||
fuel:
|
||||
# Sin modelo concreto — el integrador definirá tanques estructurales en Paso 6.
|
||||
sensors_per_tank:
|
||||
- level
|
||||
- temperature
|
||||
typical_tank_count: 2
|
||||
tag_prefix_template: "TANK_FUEL_{idx}"
|
||||
|
||||
bilge:
|
||||
typical_pump_count: 3
|
||||
tag_prefix_template: "BILGE_{location}"
|
||||
locations_template: ["FWD", "MID", "AFT"]
|
||||
|
||||
# ----- Permissives típicos a sugerir ---------------------------------------
|
||||
|
||||
permissives_template:
|
||||
|
||||
- id: start_main_engine
|
||||
action_id_template: "START_{tag_prefix}"
|
||||
apply_to: ["main_engine"]
|
||||
conditions:
|
||||
- tag_ref_template: "{tag_prefix}.OIL_PRESS"
|
||||
operator: ">"
|
||||
threshold: 0.3
|
||||
message_on_fail: "Presión aceite previa al arranque demasiado baja (lubricación insuficiente)."
|
||||
- tag_ref_template: "{tag_prefix}.COOLANT_TEMP"
|
||||
operator: ">"
|
||||
threshold: 5.0
|
||||
message_on_fail: "Refrigerante por debajo de 5°C — pre-calentar antes de arrancar."
|
||||
- tag_ref_template: "{tag_prefix}.ESTOP_ACTIVE"
|
||||
operator: "is_false"
|
||||
message_on_fail: "Pulsador E-stop activado — desbloquear antes de arrancar."
|
||||
on_fail_message: "Pre-condiciones de arranque del motor principal no cumplidas."
|
||||
|
||||
- id: start_genset
|
||||
action_id_template: "START_{tag_prefix}"
|
||||
apply_to: ["genset"]
|
||||
conditions:
|
||||
- tag_ref_template: "{tag_prefix}.OIL_PRESS"
|
||||
operator: ">"
|
||||
threshold: 0.3
|
||||
message_on_fail: "Presión aceite previa baja."
|
||||
- tag_ref_template: "{tag_prefix}.COOLANT_TEMP"
|
||||
operator: ">"
|
||||
threshold: 0.0
|
||||
message_on_fail: "Refrigerante demasiado frío para arranque seguro."
|
||||
on_fail_message: "Pre-condiciones de arranque del genset no cumplidas."
|
||||
|
||||
# ----- Topología sugerida de tarjetas AR-NMEA-IO --------------------------
|
||||
|
||||
topology_template:
|
||||
# Patrón típico para yate planeo con 2 motores + 1 genset + tanques + auxiliares:
|
||||
# 5-7 tarjetas distribuidas. Una maestra Modbus en el PC industrial.
|
||||
cards_estimate:
|
||||
min: 5
|
||||
typical: 6
|
||||
max: 8
|
||||
buses:
|
||||
- id: bus_main
|
||||
protocol: modbus_rtu
|
||||
role: "Maestra en PC industrial central. Esclavas distribuidas."
|
||||
- id: bus_n2k
|
||||
protocol: nmea2000
|
||||
role: "Backbone NMEA 2000 del buque. Motores y gensets en modo dual."
|
||||
@@ -0,0 +1,151 @@
|
||||
{
|
||||
"_meta": {
|
||||
"version": 1,
|
||||
"source": "VMS_Sailor_v2_Parte_01.md section 7",
|
||||
"notes": "Catálogo maestro completo. Sirve de checklist para el wizard del Studio (paso 4). Lo que el integrador marca define el menú lateral del Runtime. SystemId values deben coincidir con vmssailor.core.enums.SystemId."
|
||||
},
|
||||
"categories": [
|
||||
{
|
||||
"id": "propulsion",
|
||||
"name": "Propulsión y maquinaria",
|
||||
"systems": [
|
||||
{ "id": "main_engine", "name": "Máquina principal", "name_en": "Main engine", "default_for": ["yacht_motor", "fishing", "patrol", "ferry", "offshore_support"] },
|
||||
{ "id": "transmission", "name": "Transmisiones / reductoras", "name_en": "Transmissions" },
|
||||
{ "id": "shaft_propeller", "name": "Ejes y hélices", "name_en": "Shafts & propellers" },
|
||||
{ "id": "thruster", "name": "Hélices de proa/popa", "name_en": "Bow/stern thrusters" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "maneuvering",
|
||||
"name": "Maniobra y trimado",
|
||||
"systems": [
|
||||
{ "id": "trim_sterndrive", "name": "Trim de motores / sterndrives", "name_en": "Engine trim" },
|
||||
{ "id": "trim_tabs", "name": "Trim tabs", "name_en": "Trim tabs (Bennett, Lenco)" },
|
||||
{ "id": "cpp", "name": "Hélices de paso variable", "name_en": "Controllable Pitch Propellers" },
|
||||
{ "id": "gyrostabilizer", "name": "Estabilizadores girostáticos", "name_en": "Gyrostabilizers (Seakeeper, Quick MC²)" },
|
||||
{ "id": "fin_stabilizer", "name": "Estabilizadores de aletas activas", "name_en": "Active fin stabilizers" },
|
||||
{ "id": "joystick_docking", "name": "Joystick docking", "name_en": "Joystick docking" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "electrical_generation",
|
||||
"name": "Generación eléctrica",
|
||||
"systems": [
|
||||
{ "id": "genset", "name": "Gensets diésel", "name_en": "Diesel gensets", "default_for": ["yacht_motor", "fishing", "patrol", "ferry", "offshore_support"] },
|
||||
{ "id": "shore_power", "name": "Shore power con transferencia", "name_en": "Shore power with ATS" },
|
||||
{ "id": "inverter_charger", "name": "Inversores/cargadores combinados", "name_en": "Inverter/chargers (Victron, Mastervolt)" },
|
||||
{ "id": "battery_bank", "name": "Bancos de baterías litio con BMS", "name_en": "Lithium battery banks with BMS" },
|
||||
{ "id": "msb", "name": "Cuadros principales (MSB)", "name_en": "Main switchboards", "default_for": ["yacht_motor", "fishing", "patrol", "ferry"] },
|
||||
{ "id": "esb", "name": "Cuadros de emergencia (ESB)", "name_en": "Emergency switchboards" },
|
||||
{ "id": "ups", "name": "UPS", "name_en": "UPS" },
|
||||
{ "id": "solar", "name": "Paneles solares + MPPT", "name_en": "Solar + MPPT" },
|
||||
{ "id": "smart_dc_busbar", "name": "Smart busbars DC", "name_en": "Smart DC busbars (Lynx Smart BMS)" },
|
||||
{ "id": "smart_panel", "name": "Tableros inteligentes con monitoreo", "name_en": "Smart panels with embedded monitoring" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "electrical_isolation",
|
||||
"name": "Aislamiento eléctrico",
|
||||
"systems": [
|
||||
{ "id": "sectionalizing", "name": "Aislamiento por sectores", "name_en": "Sectionalizing" },
|
||||
{ "id": "emergency_isolation", "name": "Aislamiento total de emergencia", "name_en": "Total emergency isolation" },
|
||||
{ "id": "breakers", "name": "Breakers configurables", "name_en": "Configurable breakers" },
|
||||
{ "id": "lockout_tagout", "name": "Lockout-tagout digital", "name_en": "Digital lockout-tagout" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "fluids",
|
||||
"name": "Fluidos del buque",
|
||||
"systems": [
|
||||
{ "id": "fuel", "name": "Combustible (DO/MDO)", "name_en": "Fuel oil", "default_for": ["yacht_motor", "fishing", "patrol", "ferry", "offshore_support"] },
|
||||
{ "id": "lube_oil", "name": "Aceite lubricante", "name_en": "Lube oil" },
|
||||
{ "id": "hydraulic_oil", "name": "Aceite hidráulico", "name_en": "Hydraulic oil" },
|
||||
{ "id": "fw_cooling", "name": "Refrigeración agua dulce", "name_en": "Fresh water cooling" },
|
||||
{ "id": "sw_cooling", "name": "Refrigeración agua salada", "name_en": "Sea water cooling" },
|
||||
{ "id": "starting_air", "name": "Aire de arranque / aire comprimido", "name_en": "Starting / compressed air" },
|
||||
{ "id": "bilge", "name": "Sentinas", "name_en": "Bilge", "default_for": ["yacht_motor", "fishing", "patrol", "ferry", "offshore_support"] },
|
||||
{ "id": "ballast", "name": "Lastre", "name_en": "Ballast" },
|
||||
{ "id": "grey_water", "name": "Aguas grises", "name_en": "Grey water" },
|
||||
{ "id": "black_water", "name": "Aguas negras", "name_en": "Black water" },
|
||||
{ "id": "potable_water", "name": "Agua potable", "name_en": "Potable water", "default_for": ["yacht_motor", "ferry"] },
|
||||
{ "id": "sw_service", "name": "Agua salada de servicio", "name_en": "Sea water service" },
|
||||
{ "id": "watermaker", "name": "Watermaker", "name_en": "Watermaker (desalinator)" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "safety",
|
||||
"name": "Seguridad",
|
||||
"systems": [
|
||||
{ "id": "fire_detection", "name": "Detección de incendio", "name_en": "Fire detection", "default_for": ["yacht_motor", "fishing", "patrol", "ferry", "offshore_support"] },
|
||||
{ "id": "fire_extinguishing", "name": "Extinción (CO₂, HiFog, espuma)", "name_en": "Fire extinguishing" },
|
||||
{ "id": "fifi_external", "name": "FiFi externo", "name_en": "External FiFi monitors" },
|
||||
{ "id": "emergency_bilge", "name": "Achique de emergencia", "name_en": "Emergency bilge" },
|
||||
{ "id": "gas_detection", "name": "Detección de gases", "name_en": "Gas detection" },
|
||||
{ "id": "mob", "name": "Hombre al agua (MOB)", "name_en": "Man overboard" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "environment",
|
||||
"name": "Ambiente y confort",
|
||||
"systems": [
|
||||
{ "id": "hvac", "name": "HVAC / aire acondicionado", "name_en": "HVAC", "default_for": ["yacht_motor", "ferry"] },
|
||||
{ "id": "engine_vent", "name": "Ventilación de máquinas", "name_en": "Engine room ventilation", "default_for": ["yacht_motor", "fishing", "patrol", "ferry", "offshore_support"] },
|
||||
{ "id": "heating", "name": "Calefacción", "name_en": "Heating" },
|
||||
{ "id": "refrigeration", "name": "Refrigeración (cámaras, neveras)", "name_en": "Refrigeration" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "lighting",
|
||||
"name": "Iluminación",
|
||||
"systems": [
|
||||
{ "id": "nav_lights", "name": "Luces de navegación", "name_en": "Navigation lights", "default_for": ["yacht_motor", "fishing", "patrol", "ferry", "offshore_support"] },
|
||||
{ "id": "deck_lights", "name": "Luces de cubierta", "name_en": "Deck lights" },
|
||||
{ "id": "interior_lights", "name": "Luces interiores por sector", "name_en": "Interior lights" },
|
||||
{ "id": "emergency_lights", "name": "Luces de emergencia", "name_en": "Emergency lights" },
|
||||
{ "id": "searchlights", "name": "Reflectores", "name_en": "Searchlights" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "structural_tanks",
|
||||
"name": "Tanques estructurales",
|
||||
"systems": [
|
||||
{ "id": "fuel_tanks", "name": "Tanques de combustible", "name_en": "Fuel tanks", "default_for": ["yacht_motor", "fishing", "patrol", "ferry", "offshore_support"] },
|
||||
{ "id": "water_tanks", "name": "Tanques de agua", "name_en": "Water tanks" },
|
||||
{ "id": "grey_black_tanks", "name": "Tanques de aguas grises/negras", "name_en": "Grey/black water tanks" },
|
||||
{ "id": "voids", "name": "Voids", "name_en": "Voids" },
|
||||
{ "id": "cofferdams", "name": "Cofferdams", "name_en": "Cofferdams" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "deck_maneuvering",
|
||||
"name": "Cubierta y maniobra",
|
||||
"systems": [
|
||||
{ "id": "windlass", "name": "Cabrestantes / molinetes", "name_en": "Windlasses" },
|
||||
{ "id": "anchor_system", "name": "Sistema de anclas", "name_en": "Anchor system" },
|
||||
{ "id": "mooring", "name": "Sistema de amarre", "name_en": "Mooring system" },
|
||||
{ "id": "davits", "name": "Davits / pescantes", "name_en": "Davits" },
|
||||
{ "id": "gangway", "name": "Pasarelas / portalones", "name_en": "Gangways" },
|
||||
{ "id": "crane", "name": "Grúas", "name_en": "Cranes" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "vessel_specific",
|
||||
"name": "Específicos por tipo de buque",
|
||||
"systems": [
|
||||
{ "id": "fishing_machinery", "name": "Maquinaria de pesca", "name_en": "Fishing machinery", "default_for": ["fishing"] },
|
||||
{ "id": "large_fridge_holds", "name": "Cámaras frigoríficas grandes", "name_en": "Large refrigerated holds", "default_for": ["fishing"] },
|
||||
{ "id": "rov", "name": "ROV / equipos sumergibles", "name_en": "ROV / submersibles", "default_for": ["offshore_support"] },
|
||||
{ "id": "diving_system", "name": "Sistema de buceo", "name_en": "Diving system" }
|
||||
]
|
||||
}
|
||||
],
|
||||
"_excluded_from_vms_sailor": {
|
||||
"note": "Estos pertenecen al AR-ECDIS, NO se incluyen en el VMS-Sailor.",
|
||||
"items": [
|
||||
"ECDIS / radar / AIS",
|
||||
"Piloto automático",
|
||||
"Comunicaciones VHF/HF/SatCom",
|
||||
"GPS y sensores de actitud (vienen del AR-ECDIS por NMEA 2000)"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"id": "ferretti_850",
|
||||
"name": "Ferretti 850",
|
||||
"type": "yacht_motor",
|
||||
"subtype": "planing",
|
||||
"length_overall_m": 26.04,
|
||||
"beam_max_m": 6.28,
|
||||
"draft_m": 1.98,
|
||||
"displacement_kg": 68000,
|
||||
"description": "Yate motor planeo italiano de 26 m, casco semi-V con propulsión convencional, 3 cubiertas, 4 cabinas + tripulación. Motores en V centrales, 2 gensets, A/A multizona. Tope típico ~30 nudos.",
|
||||
"data_source": "seed_estimate",
|
||||
"decks": [
|
||||
{ "id": "lower", "name": "Cubierta inferior", "z_bl_bottom": 0.5, "z_bl_top": 2.7, "polygon_xy": [] },
|
||||
{ "id": "main", "name": "Cubierta principal", "z_bl_bottom": 2.7, "z_bl_top": 4.9, "polygon_xy": [] },
|
||||
{ "id": "flybridge", "name": "Flybridge", "z_bl_bottom": 4.9, "z_bl_top": 6.6, "polygon_xy": [] }
|
||||
],
|
||||
"bulkheads": [
|
||||
{ "id": "collision", "name": "Mamparo de colisión", "x_pp": 23.5, "description": "" },
|
||||
{ "id": "er_fwd", "name": "Mamparo proa SM", "x_pp": 8.0, "description": "" },
|
||||
{ "id": "er_aft", "name": "Mamparo popa SM", "x_pp": 4.0, "description": "" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"id": "sunseeker_76",
|
||||
"name": "Sunseeker 76 Yacht",
|
||||
"type": "yacht_motor",
|
||||
"subtype": "planing",
|
||||
"length_overall_m": 23.45,
|
||||
"beam_max_m": 5.65,
|
||||
"draft_m": 1.85,
|
||||
"displacement_kg": 55000,
|
||||
"description": "Yate motor planeo británico de 23.4 m, casco semi-V profundo, 3 cubiertas (lower, main, flybridge), 4 cabinas. Sala de máquinas central con 2 motores principales en V, 1-2 gensets, sistemas de A/A y refrigeración. Tope típico ~32 nudos.",
|
||||
"data_source": "seed_estimate",
|
||||
"decks": [
|
||||
{
|
||||
"id": "lower",
|
||||
"name": "Cubierta inferior (Lower Deck)",
|
||||
"z_bl_bottom": 0.5,
|
||||
"z_bl_top": 2.6,
|
||||
"polygon_xy": []
|
||||
},
|
||||
{
|
||||
"id": "main",
|
||||
"name": "Cubierta principal (Main Deck)",
|
||||
"z_bl_bottom": 2.6,
|
||||
"z_bl_top": 4.8,
|
||||
"polygon_xy": []
|
||||
},
|
||||
{
|
||||
"id": "flybridge",
|
||||
"name": "Flybridge",
|
||||
"z_bl_bottom": 4.8,
|
||||
"z_bl_top": 6.4,
|
||||
"polygon_xy": []
|
||||
}
|
||||
],
|
||||
"bulkheads": [
|
||||
{ "id": "collision", "name": "Mamparo de colisión", "x_pp": 21.0, "description": "Mamparo de colisión (proa)" },
|
||||
{ "id": "er_fwd", "name": "Mamparo proa sala de máquinas", "x_pp": 7.0, "description": "" },
|
||||
{ "id": "er_aft", "name": "Mamparo popa sala de máquinas", "x_pp": 3.5, "description": "" }
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user