commit 5f552ca8ab29160ccccb039065f3a90d4cab3395 Author: Alvaro Romero Date: Fri Jul 3 12:18:12 2026 -0400 feat: AR-ElecArrangement initial commit — Python FastAPI + uvicorn (LAN desktop app, packaged as .exe via PyInstaller) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ffbfb94 --- /dev/null +++ b/.gitignore @@ -0,0 +1,147 @@ +# ============================================================================ +# AR-Autopilot — .gitignore +# ============================================================================ + +# ---------------------------------------------------------------------------- +# Python +# ---------------------------------------------------------------------------- +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +# Exception: Flutter display app source is in display/lib/ — track it +!display/lib/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# Virtual environments +.venv/ +venv/ +env/ +ENV/ +.env + +# pytest / coverage / mypy / ruff +.pytest_cache/ +.coverage +.coverage.* +htmlcov/ +.tox/ +.nox/ +coverage.xml +*.cover +.hypothesis/ +.mypy_cache/ +.dmypy.json +dmypy.json +.ruff_cache/ + +# Jupyter +.ipynb_checkpoints/ + +# ---------------------------------------------------------------------------- +# Flutter / Dart (display app) +# ---------------------------------------------------------------------------- +display/.dart_tool/ +display/.flutter-plugins +display/.flutter-plugins-dependencies +display/.packages +display/.pub-cache/ +display/.pub/ +display/build/ +display/**/build/ +display/**/.dart_tool/ +display/**/.idea/ +display/**/*.iml +display/ios/Pods/ +display/ios/.symlinks/ +display/android/.gradle/ +display/android/local.properties +display/android/captures/ +display/android/gradlew +display/android/gradlew.bat +display/android/gradle-wrapper.jar +display/windows/flutter/ephemeral/ +display/linux/flutter/ephemeral/ +display/macos/Flutter/ephemeral/ + +# ---------------------------------------------------------------------------- +# PlatformIO / ESP32 firmware +# ---------------------------------------------------------------------------- +firmware/**/.pio/ +firmware/**/.pioenvs/ +firmware/**/.piolibdeps/ +firmware/**/.vscode/ +firmware/**/.clang_complete +firmware/**/.gcc-flags.json + +# ---------------------------------------------------------------------------- +# IDEs / Editors +# ---------------------------------------------------------------------------- +.idea/ +.vscode/ +*.swp +*.swo +*~ +.project +.pydevproject +.settings/ + +# ---------------------------------------------------------------------------- +# OS files +# ---------------------------------------------------------------------------- +.DS_Store +Thumbs.db +Desktop.ini +*.lnk + +# ---------------------------------------------------------------------------- +# Build artifacts / installers +# ---------------------------------------------------------------------------- +installer/output/ +installer/build/ +*.msi +*.exe +*.appack + +# ---------------------------------------------------------------------------- +# Local config / secrets +# ---------------------------------------------------------------------------- +*.local.yaml +*.local.json +secrets/ +.env.local +.secrets/ + +# ---------------------------------------------------------------------------- +# Examples output +# ---------------------------------------------------------------------------- +examples/output/ + +# ---------------------------------------------------------------------------- +# Logs +# ---------------------------------------------------------------------------- +*.log +logs/ + +# ---------------------------------------------------------------------------- +# Claude Code local settings + worktrees (personal — not committed) +# ---------------------------------------------------------------------------- +.claude/settings.local.json +.claude/worktrees/ diff --git a/.gitignore (conflicted copy 2026-05-12 150215) b/.gitignore (conflicted copy 2026-05-12 150215) new file mode 100644 index 0000000..07646f4 --- /dev/null +++ b/.gitignore (conflicted copy 2026-05-12 150215) @@ -0,0 +1,72 @@ +# ── Python ─────────────────────────────────────────────────────────────────── +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# ── Environments ───────────────────────────────────────────────────────────── +.env +.venv +venv/ +env/ +ENV/ +env.bak/ + +# ── PyInstaller ────────────────────────────────────────────────────────────── +*.spec +!arelec.spec # keep the canonical spec checked in + +# ── IDE / Editor ───────────────────────────────────────────────────────────── +.vscode/ +.idea/ +*.swp +*.swo +.DS_Store +Thumbs.db + +# ── Frontend (Vue/Vite/Node) ───────────────────────────────────────────────── +frontend/node_modules/ +frontend/dist/ +frontend/.vite/ +frontend/coverage/ +*.tsbuildinfo +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# ── Project files (user data) ──────────────────────────────────────────────── +*.area +!sample_projects/*.area +projects/ + +# ── Logs ───────────────────────────────────────────────────────────────────── +logs/ +*.log + +# ── Tests ──────────────────────────────────────────────────────────────────── +.pytest_cache/ +.coverage +htmlcov/ + +# ── OS / temp ──────────────────────────────────────────────────────────────── +*.tmp +*.bak +*~ diff --git a/LICENSE (conflicted copy 2026-05-12 150210).txt b/LICENSE (conflicted copy 2026-05-12 150210).txt new file mode 100644 index 0000000..1b50c00 --- /dev/null +++ b/LICENSE (conflicted copy 2026-05-12 150210).txt @@ -0,0 +1,17 @@ +AR-ElecArrangement +Copyright © 2026 Alvaro Enrique Romero Donado. Todos los derechos reservados. +All rights reserved. + +Este software es propiedad exclusiva de su autor. Ningún derecho de uso, +copia, modificación, distribución, ingeniería inversa o redistribución se +concede sin licencia escrita expresa del titular. + +Para licenciamiento contactar: alro65@gmail.com + +------------------------------------------------------------------------------ + +This software is the exclusive property of its author. No right of use, +copy, modification, distribution, reverse engineering, or redistribution +is granted without express written license from the holder. + +For licensing inquiries: alro65@gmail.com diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..2a3a6ce --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,58 @@ +AR-Autopilot +Copyright (c) 2026 Alvaro Romero. All Rights Reserved. + +PROPRIETARY AND CONFIDENTIAL — NOT FOR REDISTRIBUTION +================================================================================ + +This software, including but not limited to its source code, firmware, +configuration files, default tuning parameters, schematics, documentation, +and any accompanying assets (collectively, the "Software"), is the exclusive +property of Alvaro Romero ("the Author") and is protected by international +copyright law and treaty provisions. + +PERMITTED USE +-------------------------------------------------------------------------------- +No license, express or implied, is granted to any person or entity to: + + (a) use, copy, modify, merge, publish, distribute, sublicense, or sell + copies of the Software, in whole or in part; + + (b) reverse-engineer, decompile, disassemble, or otherwise attempt to + derive the source code from compiled binaries or firmware images; + + (c) extract, reuse, or redistribute the default PID tuning parameters, + gain schedules, actuator profiles, vessel profiles, or any other + proprietary parameter set bundled with the Software, which constitute + trade secrets of the Author; + + (d) use the Software, or any derivative thereof, for the development of + competing autopilot, dynamic positioning, or vessel control products; + +except under the terms of a separate written commercial license agreement +signed by the Author. + +COMMERCIAL LICENSING +-------------------------------------------------------------------------------- +Commercial deployment of the Software on board a vessel requires a per-vessel +license bound to the unique hardware identifier (HWID) of the installation, +issued by the Author. Contact the Author for licensing inquiries. + +SAFETY-CRITICAL DISCLAIMER +-------------------------------------------------------------------------------- +THE SOFTWARE CONTROLS VESSEL STEERING SYSTEMS AND IS SAFETY-CRITICAL. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES, INJURY, LOSS OF LIFE, LOSS OF +VESSEL, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. + +Installation, commissioning, and operation of the Software must comply with +the relevant maritime regulations and standards applicable to the vessel and +its area of operation, including but not limited to ISO 11674, ISO 16329, and +IMO MSC.64(67). + +================================================================================ +For licensing inquiries: alro65@gmail.com +================================================================================ diff --git a/README (conflicted copy 2026-05-12 150226).md b/README (conflicted copy 2026-05-12 150226).md new file mode 100644 index 0000000..e78c8d8 --- /dev/null +++ b/README (conflicted copy 2026-05-12 150226).md @@ -0,0 +1,70 @@ +# AR-ElecArrangement + +Aplicación para el diseño completo de la instalación eléctrica de un buque +desde la silueta hasta el plano de arrangement firmable. + +Familia AR ShipDesign. Cubre yates a motor, yates a vela, pesqueros, lanchas +de pasaje y embarcaciones de trabajo de hasta ~50 m. + +## Arquitectura + +Servidor Windows + clientes web. Un solo `.exe` empaca el backend FastAPI y +el frontend estático. El PC del usuario es el servidor; iPad, Android u otro +PC se conectan por navegador a `http://:5505`. + +``` +.exe (Windows) + ├─ Backend Python (FastAPI, cálculo eléctrico, exports) + ├─ Frontend web (HTML + Vue 3 + Konva.js) + └─ Auto-abre browser a http://localhost:5505 + +Tablets / otros PC → http://:5505 (LAN, sin Internet) +``` + +Sin telemetría. Offline absoluto. Catálogos editables por el usuario. + +## Normativas soportadas + +- ABYC E-11 (small craft USA) +- IEC 60092 (mercante / clase) +- NMEA 2000 y NMEA 0183 +- IEEE 45 (buques grandes) +- ISO 10133 (DC small craft) e ISO 13297 (AC small craft) + +## Stack + +| Capa | Tecnología | +|---|---| +| Backend | Python 3.11 + FastAPI + uvicorn | +| Frontend | Vue 3 + Konva.js | +| Cálculo | numpy, scipy, pandas, networkx | +| Exports | ezdxf (DXF), reportlab (PDF), openpyxl (Excel BOM) | +| Persistencia | `.area` (ZIP con JSON + assets) | +| Empaquetado | PyInstaller | + +## Desarrollo local + +```bash +# Backend +python -m venv venv +venv\Scripts\activate +pip install -r backend/requirements.txt +python -m uvicorn backend.main:app --reload --port 5505 + +# Frontend (dev) +cd frontend +npm install +npm run dev # http://localhost:5173 con proxy al backend +``` + +## Empaquetado .exe + +```bash +pyinstaller arelec.spec +# dist/AR-ElecArrangement/AR-ElecArrangement.exe +``` + +## Licencia + +Propietaria. Copyright © 2026 Alvaro Enrique Romero Donado. Ver `LICENSE.txt`. +Para licenciamiento contactar: alro65@gmail.com diff --git a/README.md b/README.md new file mode 100644 index 0000000..9a115df --- /dev/null +++ b/README.md @@ -0,0 +1,100 @@ +# AR-Autopilot + +Professional marine autopilot for vessels in the 30-40 m range (motor yachts, motor sailboats, fishing vessels, small ferries, coastal patrol boats). + +Part of the **AR Suite** alongside AR-ECDIS, VMS-Sailor, AR-ShipDesign, AR-ElecArrangement, and AR-StabCol. Sold standalone or bundled with AR-ECDIS. + +> **NOT** Dynamic Positioning. **NOT** joystick docking. This is a classic heading-and-track autopilot with intelligent drift compensation, controlling rudder actuators (hydraulic or electric). + +--- + +## Status + +**Sprint 0 — Foundations (in progress).** + +This sprint delivers the repository structure, core data model, seed library, and a passing test suite. No functional firmware, Studio GUI, or display yet — those start in Sprint 1. + +See [`docs/AR_Autopilot_brief.md`](docs/AR_Autopilot_brief.md) for the complete project brief, scope, and roadmap. + +--- + +## Components + +| Component | Tech | Purpose | +|---|---|---| +| **Studio** (`arautopilot/studio/`) | Python 3.11 + PySide6 | Project configurator (integrator-side, not shipped to customers). Generates per-vessel `.appack` packages | +| **Firmware** (`firmware/ar_autopilot_v1/`) | C++ on ESP32 via PlatformIO | Real-time PID control, NMEA 2000 + Modbus, safety logic. Runs on the AR-NMEA-IO v1.0 board (shared with VMS-Sailor) | +| **Display** (`display/`) | Flutter Desktop (Win + Linux) | Dedicated bridge cockpit-feel touch display with rotary knob input | +| **Core models** (`arautopilot/core/`) | Pydantic v2 | Shared data model (vessel config, PID config, actuator config, alarms, modes, knob state) | +| **Library** (`arautopilot/library/`) | YAML + JSON | Curated seed: actuator profiles, default tunings per vessel type | + +--- + +## Requirements + +- Python **3.11** or newer +- Git +- (Later sprints) PlatformIO, Flutter SDK, WiX Toolset + +--- + +## Quick start (Sprint 0) + +```powershell +# Create venv and install +python -m venv .venv +.\.venv\Scripts\Activate.ps1 +python -m pip install -U pip +pip install -e ".[dev]" + +# Run tests +pytest + +# Run the Sprint 0 demo (creates, saves, reloads a project config) +python examples/sprint0_demo.py +``` + +--- + +## Repository layout + +``` +AR-Autopilot/ +├── arautopilot/ # Python package (core models, library, studio stubs, tests) +├── firmware/ # ESP32 firmware (Sprint 1+; only pinout.h in Sprint 0) +├── display/ # Flutter dedicated display (Sprint 4+) +├── examples/ # Runnable demos +├── docs/ # Brief + per-sprint design docs +├── installer/ # WiX MSI scripts (later) +└── tools/ # Helper scripts (later) +``` + +See [`docs/architecture.md`](docs/architecture.md) for a one-page architecture overview. + +--- + +## Sprint roadmap + +| Sprint | Focus | +|---|---| +| **0** | Foundations: repo structure, core data model, seed library, tests | +| 1 | Firmware base (I/O, Modbus, NMEA 2000 read, STANDBY mode) | +| 2 | PID inner loop (rudder position control) | +| 3 | PID outer loop + Heading Hold (with ROT feed-forward & gain scheduling) | +| 4 | Studio + basic dedicated display | +| 5 | True Course + Track Keeping (smooth XTE correction) | +| 6 | Safety, alarms, NMEA 2000 publish, VMS alarm consumption | +| 7 | Knob + commissioning + offline auto-tuning | +| 8 | EKF + adaptive tuning + telemetry + VPN | +| 9 | Hardening + integrated testing | +| 10+ | Phase 2 (wind modes for sailboats) and beyond | + +Full detail in the brief. + +--- + +## License + +Proprietary. All rights reserved. See [`LICENSE.txt`](LICENSE.txt). + +Commercial deployment requires a per-vessel license bound to the installation HWID. Contact for licensing. diff --git a/arelec.spec b/arelec.spec new file mode 100644 index 0000000..8636939 --- /dev/null +++ b/arelec.spec @@ -0,0 +1,82 @@ +# -*- mode: python ; coding: utf-8 -*- +""" +PyInstaller spec para AR-ElecArrangement. + +Empaca: +- Backend Python (backend/main.py como entrypoint) +- Frontend static (frontend/dist/ como recurso bundled) +- Data (data/ con catálogos JSON) + +Resultado: dist/AR-ElecArrangement/AR-ElecArrangement.exe +Cuando se ejecuta, inicia el server en 0.0.0.0:5505 y abre el browser. +""" + +from PyInstaller.utils.hooks import collect_data_files + +block_cipher = None + +# Recursos que el .exe debe llevar dentro de sys._MEIPASS +datas = [ + ('frontend/dist', 'frontend/dist'), + ('data', 'data'), +] +# Catálogos (futuro): garantiza que cualquier JSON de data/ se incluya +# automáticamente cuando agreguemos más. + +# Dependencias que PyInstaller a veces no detecta solo (uvicorn loaders) +hiddenimports = [ + 'uvicorn.logging', + 'uvicorn.loops', + 'uvicorn.loops.auto', + 'uvicorn.protocols', + 'uvicorn.protocols.http', + 'uvicorn.protocols.http.auto', + 'uvicorn.protocols.websockets', + 'uvicorn.protocols.websockets.auto', + 'uvicorn.lifespan', + 'uvicorn.lifespan.on', +] + +a = Analysis( + ['backend/main.py'], + pathex=['backend'], + binaries=[], + datas=datas, + hiddenimports=hiddenimports, + hookspath=[], + runtime_hooks=[], + excludes=['tkinter', 'matplotlib', 'PySide6', 'PyQt5', 'PyQt6'], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher, + noarchive=False, +) +pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) + +exe = EXE( + pyz, + a.scripts, + [], + exclude_binaries=True, + name='AR-ElecArrangement', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + console=True, # mantiene ventana CMD visible — útil para ver logs + disable_windowed_traceback=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, + icon=None, # TODO Sprint 14: agregar icon.ico +) +coll = COLLECT( + exe, + a.binaries, + a.zipfiles, + a.datas, + strip=False, + upx=True, + upx_exclude=[], + name='AR-ElecArrangement', +) diff --git a/backend/arelec/__init__.py b/backend/arelec/__init__.py new file mode 100644 index 0000000..1afcd8b --- /dev/null +++ b/backend/arelec/__init__.py @@ -0,0 +1,9 @@ +""" +AR-ElecArrangement — backend package. + +Aplicación servidor para diseño eléctrico de buques. El cliente (browser) +consume la API REST + WebSocket expuesta por ``arelec.main``. +""" + +__version__ = "0.1.0" +__author__ = "Alvaro Enrique Romero Donado" diff --git a/backend/arelec/core/__init__.py b/backend/arelec/core/__init__.py new file mode 100644 index 0000000..ebabdcc --- /dev/null +++ b/backend/arelec/core/__init__.py @@ -0,0 +1 @@ +"""Modelo de datos del proyecto: ship, decks, zones, appliances, panels, cables…""" diff --git a/backend/arelec/core/project.py b/backend/arelec/core/project.py new file mode 100644 index 0000000..0054221 --- /dev/null +++ b/backend/arelec/core/project.py @@ -0,0 +1,119 @@ +""" +Raíz del proyecto y serialización al formato ``.area``. + +Un ``.area`` es un archivo ZIP que contiene: + +:: + + project.json — metadata del proyecto y modelo serializado + assets/ — recursos (íconos custom, capturas, fotografías) + +El formato JSON usa el modelo declarado por el resto de ``arelec.core`` y +está versionado por el campo ``schema_version``. Los .area producidos por +una versión del software pueden ser leídos por versiones posteriores +(migraciones aditivas), nunca por anteriores. + +En Sprint 0 el modelo es mínimo (solo metadata) — los sub-modelos (ship, +decks, appliances, panels, cables…) se agregan en sprints siguientes. +""" + +from __future__ import annotations + +import json +import zipfile +from dataclasses import dataclass, field, asdict +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + +SCHEMA_VERSION = 1 + + +@dataclass +class ProjectMetadata: + """Datos administrativos del proyecto.""" + + name: str = "Nuevo proyecto" + author: str = "" + company: str = "" + notes: str = "" + schema_version: int = SCHEMA_VERSION + created_at: str = field( + default_factory=lambda: datetime.now(timezone.utc).isoformat() + ) + modified_at: str = field( + default_factory=lambda: datetime.now(timezone.utc).isoformat() + ) + + +@dataclass +class Project: + """ + Raíz del modelo de datos. Sprint 0 contiene solo metadata; los sub-modelos + (ship, decks, appliances, panels, cable_runs, batteries, etc.) se acoplan + a esta clase en sprints siguientes. + + La instancia se serializa íntegra como ``project.json`` dentro del ZIP + ``.area``. + """ + + metadata: ProjectMetadata = field(default_factory=ProjectMetadata) + # Sub-modelos futuros se irán agregando aquí: + # ship: Ship | None = None + # decks: list[Deck] = field(default_factory=list) + # appliances: list[Appliance] = field(default_factory=list) + # batteries: list[BatteryBank] = field(default_factory=list) + # panels: list[Panel] = field(default_factory=list) + # cable_runs: list[CableRun] = field(default_factory=list) + + # ── Serialization ─────────────────────────────────────────────────────── + def to_json(self) -> dict[str, Any]: + """Convertir a dict serializable (JSON-safe).""" + return asdict(self) + + @classmethod + def from_json(cls, data: dict[str, Any]) -> "Project": + """Reconstruir desde dict cargado de ``project.json``.""" + meta_dict = data.get("metadata", {}) + return cls(metadata=ProjectMetadata(**meta_dict)) + + # ── .area I/O ─────────────────────────────────────────────────────────── + def save(self, path: str | Path) -> None: + """ + Persistir el proyecto a ``path`` (extensión ``.area`` sugerida). + + Toca ``metadata.modified_at`` con la hora UTC actual antes de + serializar. + """ + path = Path(path) + self.metadata.modified_at = datetime.now(timezone.utc).isoformat() + payload = json.dumps(self.to_json(), indent=2, ensure_ascii=False) + # ``ZIP_DEFLATED`` para que los .area no crezcan absurdo en proyectos + # grandes — el contenido es texto y comprime ~10×. + with zipfile.ZipFile(path, mode="w", compression=zipfile.ZIP_DEFLATED) as zf: + zf.writestr("project.json", payload) + + @classmethod + def load(cls, path: str | Path) -> "Project": + """ + Cargar un proyecto desde ``path``. Lanza: + + - ``FileNotFoundError`` si el archivo no existe + - ``zipfile.BadZipFile`` si el archivo no es ZIP válido + - ``ValueError`` si ``schema_version`` es mayor que ``SCHEMA_VERSION`` + (el .area fue creado por una versión más nueva del software) + - ``KeyError`` si falta ``project.json`` en el ZIP + """ + path = Path(path) + with zipfile.ZipFile(path, mode="r") as zf: + with zf.open("project.json") as f: + data = json.loads(f.read().decode("utf-8")) + + version = data.get("metadata", {}).get("schema_version", 1) + if version > SCHEMA_VERSION: + raise ValueError( + f"El proyecto requiere schema_version={version}, " + f"este software soporta hasta {SCHEMA_VERSION}. " + f"Actualiza AR-ElecArrangement." + ) + return cls.from_json(data) diff --git a/backend/arelec/core/units.py b/backend/arelec/core/units.py new file mode 100644 index 0000000..2838f07 --- /dev/null +++ b/backend/arelec/core/units.py @@ -0,0 +1,110 @@ +""" +Conversiones SI ↔ imperial usadas en la UI. + +REGLA INTERNA DEL PROYECTO: todo el modelo de datos y todos los cálculos +trabajan en SI (m, kg, A, V, W). Las conversiones a unidades imperiales +(pies, AWG, BTU/h) ocurren ÚNICAMENTE en el borde UI cuando el usuario lo +pide. Persistir AWG o pies en .area está prohibido. + +Tablas +------ +- AWG ↔ mm²: ABYC E-11 Tabla VI / IEC 60228 (relación logarítmica + valores + tabulados estándar). +- ft ↔ m: 1 ft = 0.3048 m exacto (NIST). +- lb ↔ kg: 1 lb = 0.45359237 kg exacto (NIST). +""" + +from __future__ import annotations + +# ── Lengths ───────────────────────────────────────────────────────────────── +M_PER_FT: float = 0.3048 +FT_PER_M: float = 1.0 / M_PER_FT + + +def m_to_ft(m: float) -> float: + return m * FT_PER_M + + +def ft_to_m(ft: float) -> float: + return ft * M_PER_FT + + +# ── Mass ──────────────────────────────────────────────────────────────────── +KG_PER_LB: float = 0.45359237 +LB_PER_KG: float = 1.0 / KG_PER_LB + + +def kg_to_lb(kg: float) -> float: + return kg * LB_PER_KG + + +def lb_to_kg(lb: float) -> float: + return lb * KG_PER_LB + + +# ── Wire calibre — AWG ↔ mm² ──────────────────────────────────────────────── +# Tabla estándar de conductores (no se usa la fórmula logarítmica analítica +# porque los valores comerciales están redondeados). +# Cobertura: AWG 24 a 4/0 y mm² 0.2 a 120 — rango típico marino. +AWG_TO_MM2: dict[str, float] = { + "24": 0.205, + "22": 0.324, + "20": 0.519, + "18": 0.823, + "16": 1.31, + "14": 2.08, + "12": 3.31, + "10": 5.26, + "8": 8.37, + "6": 13.3, + "4": 21.2, + "2": 33.6, + "1": 42.4, + "1/0": 53.5, + "2/0": 67.4, + "3/0": 85.0, + "4/0": 107.0, +} + +# Lista de mm² comerciales IEC para la conversión inversa. +COMMERCIAL_MM2: tuple[float, ...] = ( + 0.5, 0.75, 1.0, 1.5, 2.5, 4.0, 6.0, 10.0, 16.0, 25.0, 35.0, + 50.0, 70.0, 95.0, 120.0, 150.0, 185.0, 240.0, +) + + +def awg_to_mm2(awg: str) -> float: + """Convertir designación AWG (p.ej. ``"10"``, ``"1/0"``) a mm².""" + if awg not in AWG_TO_MM2: + raise ValueError(f"AWG no soportado: {awg!r}. Válidos: {list(AWG_TO_MM2)}") + return AWG_TO_MM2[awg] + + +def mm2_to_awg(mm2: float) -> str: + """Mapear área (mm²) al AWG cuya área es ≥ la pedida (criterio conservador).""" + if mm2 <= 0: + raise ValueError(f"mm² debe ser > 0, recibido: {mm2}") + # AWG_TO_MM2 está ordenado de menor a mayor área + for awg, area in AWG_TO_MM2.items(): + if area >= mm2: + return awg + raise ValueError(f"mm² {mm2} excede el AWG máximo soportado (4/0 = 107 mm²)") + + +# ── Power / energy ────────────────────────────────────────────────────────── +def hp_to_kw(hp: float) -> float: + """Caballos métricos (CV / PS) a kW: 1 hp métrico = 0.7355 kW.""" + return hp * 0.7355 + + +def kw_to_hp(kw: float) -> float: + return kw / 0.7355 + + +def btu_per_h_to_kw(btu_h: float) -> float: + """BTU/h a kW (capacidad frigorífica de A/C).""" + return btu_h * 0.000293071 + + +def kw_to_btu_per_h(kw: float) -> float: + return kw / 0.000293071 diff --git a/backend/main.py b/backend/main.py new file mode 100644 index 0000000..1e68d1e --- /dev/null +++ b/backend/main.py @@ -0,0 +1,196 @@ +""" +AR-ElecArrangement — entrypoint del servidor. + +Lanza FastAPI/uvicorn en ``0.0.0.0:5505`` (accesible desde la LAN para que +los tablets se conecten) y sirve el frontend compilado de ``frontend/dist/``. + +Cuando se ejecuta como ``python -m backend.main`` o desde el ``.exe`` empacado +por PyInstaller, después de iniciar el server espera 1 s y abre el browser +predeterminado apuntando a ``http://localhost:5505``. +""" + +from __future__ import annotations + +import os +import re +import sys +import tempfile +import threading +import time +import webbrowser +from contextlib import asynccontextmanager +from pathlib import Path +from typing import Any + +import uvicorn +from fastapi import FastAPI, HTTPException, UploadFile, File +from fastapi.middleware.cors import CORSMiddleware +from fastapi.middleware.gzip import GZipMiddleware +from fastapi.responses import FileResponse +from fastapi.staticfiles import StaticFiles + +from arelec import __version__ +from arelec.core.project import Project + +# ── Paths ──────────────────────────────────────────────────────────────────── +# En desarrollo: __file__ = backend/main.py → BACKEND_DIR = backend/ +# Empacado: __file__ está dentro de _MEIPASS → BACKEND_DIR = sys._MEIPASS/backend +if getattr(sys, "frozen", False): + BASE_DIR = Path(sys._MEIPASS) # type: ignore[attr-defined] +else: + BASE_DIR = Path(__file__).resolve().parent.parent + +FRONTEND_DIST = BASE_DIR / "frontend" / "dist" +DATA_DIR = BASE_DIR / "data" + +HOST = "0.0.0.0" +PORT = 5505 + + +# ── FastAPI app ────────────────────────────────────────────────────────────── +@asynccontextmanager +async def lifespan(app: FastAPI): + """Hook de arranque/apagado del server.""" + print(f"[arelec] Servidor iniciado en http://localhost:{PORT} (v{__version__})") + print(f"[arelec] Tablets en LAN: http://:{PORT}") + yield + print("[arelec] Servidor detenido.") + + +app = FastAPI( + title="AR-ElecArrangement", + version=__version__, + description="Diseño eléctrico de buques — backend", + lifespan=lifespan, +) + +# Acceso LAN sin restricciones (la app está pensada para LAN privada del usuario, +# no para Internet pública). Si en el futuro se ofrece cloud, restringir aquí. +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_methods=["*"], + allow_headers=["*"], +) +app.add_middleware(GZipMiddleware, minimum_size=1024) + + +# ── API ────────────────────────────────────────────────────────────────────── +@app.get("/api/health") +def health() -> dict[str, str]: + """Heartbeat para clientes que verifican que el server está vivo.""" + return {"status": "ok", "version": __version__} + + +# ── Project I/O (Sprint 0 — sólo metadata, expandirá en Sprint 1+) ────────── +@app.post("/api/project/new") +def project_new() -> dict[str, Any]: + """Crear un proyecto vacío y devolver su JSON serializado.""" + return Project().to_json() + + +@app.post("/api/project/open") +async def project_open(file: UploadFile = File(...)) -> dict[str, Any]: + """ + Abrir un archivo ``.area`` subido por el cliente. Lo guarda en un tempfile, + lo carga vía ``Project.load``, devuelve el JSON serializado. + """ + if not (file.filename or "").lower().endswith(".area"): + raise HTTPException(400, "El archivo debe tener extensión .area") + data = await file.read() + if not data: + raise HTTPException(400, "Archivo vacío") + with tempfile.NamedTemporaryFile(suffix=".area", delete=False) as tmp: + tmp.write(data) + tmp_path = Path(tmp.name) + try: + proj = Project.load(tmp_path) + return proj.to_json() + except Exception as e: # noqa: BLE001 + # No exponemos str(e) al cliente para evitar filtrar rutas internas + # o detalles de implementación. El tipo de error es suficiente. + raise HTTPException(400, "No es un .area válido o el archivo está dañado.") from e + finally: + tmp_path.unlink(missing_ok=True) + + +_WINDOWS_RESERVED_RE = re.compile( + r"^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(\.|$)", re.IGNORECASE +) + + +def _safe_filename(name: str) -> str: + """Sanitiza el nombre del proyecto para uso seguro como nombre de archivo. + + - Elimina caracteres no permitidos en Windows/POSIX (``/``, ``\\``, ``:``, + ``*``, ``?``, ``"``, ``<``, ``>``, ``|``, bytes nulos). + - Colapsa espacios/guiones bajos múltiples consecutivos. + - Trunca a 200 caracteres para evitar rutas demasiado largas. + - Sustituye cadena vacía o nombres reservados de Windows por ``proyecto``. + """ + safe = re.sub(r'[\\/:*?"<>|\x00]', "_", name) + safe = re.sub(r"[_\s]{2,}", "_", safe).strip("_").strip() + safe = safe[:200] + if not safe or _WINDOWS_RESERVED_RE.match(safe): + safe = "proyecto" + return safe + + +@app.post("/api/project/save") +async def project_save(payload: dict[str, Any]) -> FileResponse: + """ + Recibe un proyecto en JSON, lo serializa a ``.area`` y lo devuelve para + descarga directa por el browser. + """ + proj = Project.from_json(payload) + out_dir = Path(tempfile.mkdtemp(prefix="arelec_save_")) + out_path = out_dir / (_safe_filename(proj.metadata.name) + ".area") + proj.save(out_path) + return FileResponse( + path=out_path, + media_type="application/zip", + filename=out_path.name, + ) + + +# ── Frontend (montaje al final para que /api/* tenga precedencia) ──────────── +if FRONTEND_DIST.exists(): + app.mount("/", StaticFiles(directory=str(FRONTEND_DIST), html=True), name="frontend") +else: + @app.get("/") + def _no_frontend_msg() -> dict[str, str]: + return { + "warning": "frontend/dist no encontrado", + "hint": "Compilar frontend: cd frontend && npm install && npm run build", + } + + +# ── Auto-launch del browser cuando corre como .exe ─────────────────────────── +def _open_browser_when_ready() -> None: + """Espera 1 s y abre el browser default. Llamado solo desde .exe / __main__.""" + time.sleep(1.0) + url = f"http://localhost:{PORT}" + try: + webbrowser.open(url) + except Exception as e: # noqa: BLE001 + print(f"[arelec] No se pudo abrir el browser automáticamente: {e}") + print(f"[arelec] Abre manualmente: {url}") + + +def main() -> None: + """Entrypoint cuando se corre como ``python -m backend.main`` o ``.exe``.""" + # Solo abrir browser cuando no estamos en modo --reload (dev) + if os.environ.get("ARELEC_NO_BROWSER") != "1": + threading.Thread(target=_open_browser_when_ready, daemon=True).start() + + uvicorn.run( + "backend.main:app" if not getattr(sys, "frozen", False) else app, + host=HOST, + port=PORT, + log_level="info", + reload=False, + ) + + +if __name__ == "__main__": + main() diff --git a/backend/requirements-dev.txt b/backend/requirements-dev.txt new file mode 100644 index 0000000..4fe8edb --- /dev/null +++ b/backend/requirements-dev.txt @@ -0,0 +1,10 @@ +-r requirements.txt + +# ── Tests ──────────────────────────────────────────────────────────────────── +pytest>=8.3 +pytest-asyncio>=0.25 +httpx>=0.28 + +# ── Linting / formato ──────────────────────────────────────────────────────── +ruff>=0.8 +mypy>=1.13 diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..f46016f --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,20 @@ +# ── Server ─────────────────────────────────────────────────────────────────── +# Versiones mínimas conocidas-buenas; sin pin estricto para que pip resuelva +# la última disponible compatible con la versión de Python instalada. +# Probado contra Python 3.11–3.14. +fastapi>=0.115 +uvicorn[standard]>=0.34 +pydantic>=2.11 # Python 3.14 wheels disponibles desde 2.11 +websockets>=13.1 +python-multipart>=0.0.20 # uploads en /api/project/open + +# ── Cálculo (Sprint 4+) ────────────────────────────────────────────────────── +# numpy y pandas se agregan cuando arranquen los motores de cálculo. +# scipy se agrega cuando salga rueda para Python 3.14 (ETA: ya en pre-release). +# networkx se agrega en Sprint 9 (routing automático). + +# ── Exports (Sprint 10) ────────────────────────────────────────────────────── +# ezdxf, reportlab, openpyxl entran cuando empecemos exports DXF/PDF/Excel. + +# ── Empaquetado .exe (Sprint 14) ───────────────────────────────────────────── +# pyinstaller se agrega al cierre cuando empacamos el .exe final. diff --git a/backend/tests/__init__.py b/backend/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/test_project_serialization.py b/backend/tests/test_project_serialization.py new file mode 100644 index 0000000..77b4e7c --- /dev/null +++ b/backend/tests/test_project_serialization.py @@ -0,0 +1,75 @@ +""" +Sprint 0 — sanity: un Project recién creado se guarda y se carga sin pérdida. +""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +from arelec.core.project import Project, ProjectMetadata, SCHEMA_VERSION + + +def test_project_default_metadata() -> None: + p = Project() + assert p.metadata.name == "Nuevo proyecto" + assert p.metadata.schema_version == SCHEMA_VERSION + assert p.metadata.created_at != "" + + +def test_save_and_load_roundtrip(tmp_path: Path) -> None: + path = tmp_path / "demo.area" + original = Project(metadata=ProjectMetadata( + name="Yate Demo", + author="ARD", + company="AR ShipDesign", + notes="Sprint 0 test", + )) + original.save(path) + assert path.exists() + assert path.stat().st_size > 0 + + loaded = Project.load(path) + assert loaded.metadata.name == "Yate Demo" + assert loaded.metadata.author == "ARD" + assert loaded.metadata.company == "AR ShipDesign" + assert loaded.metadata.notes == "Sprint 0 test" + assert loaded.metadata.schema_version == SCHEMA_VERSION + + +def test_save_touches_modified_at(tmp_path: Path) -> None: + p = Project() + original_modified = p.metadata.modified_at + p.save(tmp_path / "t.area") + # Save siempre actualiza modified_at (aunque sea el mismo segundo, queda con + # nueva timestamp ISO — comparamos que fue tocado). + assert p.metadata.modified_at >= original_modified + + +def test_load_rejects_future_schema(tmp_path: Path) -> None: + """Un .area creado por una versión más nueva debe rechazarse limpio.""" + path = tmp_path / "future.area" + p = Project(metadata=ProjectMetadata(schema_version=SCHEMA_VERSION + 99)) + p.save(path) + with pytest.raises(ValueError, match="schema_version"): + Project.load(path) + + +def test_load_rejects_missing_project_json(tmp_path: Path) -> None: + import zipfile + + path = tmp_path / "empty.area" + with zipfile.ZipFile(path, mode="w") as zf: + zf.writestr("readme.txt", "no project.json") + with pytest.raises(KeyError): + Project.load(path) + + +def test_load_rejects_non_zip(tmp_path: Path) -> None: + import zipfile + + path = tmp_path / "garbage.area" + path.write_bytes(b"this is not a zip file") + with pytest.raises(zipfile.BadZipFile): + Project.load(path) diff --git a/backend/tests/test_units.py b/backend/tests/test_units.py new file mode 100644 index 0000000..74a2e7e --- /dev/null +++ b/backend/tests/test_units.py @@ -0,0 +1,65 @@ +""" +Sprint 0 — sanity: conversiones SI ↔ imperial round-trip y casos límite. +""" + +from __future__ import annotations + +import pytest + +from arelec.core.units import ( + AWG_TO_MM2, + awg_to_mm2, + ft_to_m, + kg_to_lb, + kw_to_hp, + lb_to_kg, + m_to_ft, + mm2_to_awg, + hp_to_kw, +) + + +def test_length_roundtrip() -> None: + assert m_to_ft(1.0) == pytest.approx(3.28084, rel=1e-4) + assert ft_to_m(m_to_ft(42.0)) == pytest.approx(42.0) + + +def test_mass_roundtrip() -> None: + assert kg_to_lb(1.0) == pytest.approx(2.20462, rel=1e-4) + assert lb_to_kg(kg_to_lb(100.0)) == pytest.approx(100.0) + + +def test_power_roundtrip() -> None: + assert hp_to_kw(1.0) == pytest.approx(0.7355) + assert kw_to_hp(hp_to_kw(150.0)) == pytest.approx(150.0) + + +def test_awg_lookup_known_values() -> None: + # ABYC E-11 tabla VI — valores publicados estándar + assert awg_to_mm2("10") == 5.26 + assert awg_to_mm2("4/0") == 107.0 + assert awg_to_mm2("14") == 2.08 + + +def test_mm2_to_awg_picks_next_larger() -> None: + # 5.0 mm² no es exactamente AWG 10 (5.26), pero AWG 10 cubre el cable. + # Criterio conservador: elegir AWG cuya área es ≥ pedida. + assert mm2_to_awg(5.0) == "10" + assert mm2_to_awg(5.26) == "10" + assert mm2_to_awg(2.0) == "14" # 2.08 + assert mm2_to_awg(100.0) == "4/0" # 107 + + +def test_mm2_to_awg_rejects_invalid() -> None: + with pytest.raises(ValueError): + mm2_to_awg(0) + with pytest.raises(ValueError): + mm2_to_awg(-1.0) + with pytest.raises(ValueError): + mm2_to_awg(500.0) # más grande que 4/0 + + +def test_awg_table_monotonic() -> None: + """La tabla AWG debe ir de menor a mayor área (asumido por mm2_to_awg).""" + areas = list(AWG_TO_MM2.values()) + assert areas == sorted(areas) diff --git a/data/icons/ar_logo_full.png b/data/icons/ar_logo_full.png new file mode 100644 index 0000000..42ad7e9 Binary files /dev/null and b/data/icons/ar_logo_full.png differ diff --git a/install.bat b/install.bat new file mode 100644 index 0000000..9775df5 --- /dev/null +++ b/install.bat @@ -0,0 +1,41 @@ +@echo off +title AR-ElecArrangement — Instalación de dependencias +cd /d "%~dp0" + +echo. +echo ===================================================== +echo AR-ElecArrangement — Setup del entorno Python +echo ===================================================== +echo. + +:: 1. Crear venv si no existe +if not exist "%~dp0venv\Scripts\python.exe" ( + echo [1/3] Creando entorno virtual... + python -m venv venv + if errorlevel 1 ( + echo ERROR: no se pudo crear venv. Verifica que Python 3.11+ esté instalado. + pause + exit /b 1 + ) +) else ( + echo [1/3] Entorno virtual ya existe, salto. +) + +:: 2. Actualizar pip +echo [2/3] Actualizando pip... +"%~dp0venv\Scripts\python.exe" -m pip install --upgrade pip >nul + +:: 3. Instalar dependencias +echo [3/3] Instalando dependencias (esto tarda 1-2 minutos la primera vez)... +"%~dp0venv\Scripts\python.exe" -m pip install -r backend\requirements-dev.txt +if errorlevel 1 ( + echo ERROR durante pip install. + pause + exit /b 1 +) + +echo. +echo ===================================================== +echo Instalación lista. Arranca con: start.bat +echo ===================================================== +pause diff --git a/pyproject (conflicted copy 2026-05-12 150234).toml b/pyproject (conflicted copy 2026-05-12 150234).toml new file mode 100644 index 0000000..e4e241f --- /dev/null +++ b/pyproject (conflicted copy 2026-05-12 150234).toml @@ -0,0 +1,33 @@ +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "ar-elecarrangement" +version = "0.1.0" +description = "Diseño eléctrico de buques — servidor + clientes web" +readme = "README.md" +requires-python = ">=3.11" +license = { text = "Proprietary" } +authors = [ + { name = "Alvaro Enrique Romero Donado", email = "alro65@gmail.com" }, +] + +[tool.ruff] +line-length = 100 +target-version = "py311" + +[tool.ruff.lint] +select = ["E", "F", "I", "N", "W", "B", "UP", "PL", "RUF"] +ignore = ["PLR0913"] # too many arguments — common in marine engineering APIs + +[tool.pytest.ini_options] +testpaths = ["backend/tests"] +python_files = ["test_*.py"] +asyncio_mode = "auto" + +[tool.mypy] +python_version = "3.11" +strict = true +warn_return_any = true +warn_unused_configs = true diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..62edd13 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,185 @@ +[build-system] +requires = ["setuptools>=68", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "arautopilot" +version = "0.1.0" +description = "AR-Autopilot — Professional marine autopilot for 30-40 m vessels (Studio + firmware + display)" +readme = "README.md" +license = { file = "LICENSE.txt" } +requires-python = ">=3.11" +authors = [{ name = "Alvaro Romero", email = "alro65@gmail.com" }] +keywords = ["marine", "autopilot", "pid", "nmea2000", "esp32", "vessel-control"] +classifiers = [ + "Development Status :: 2 - Pre-Alpha", + "Intended Audience :: Manufacturing", + "License :: Other/Proprietary License", + "Operating System :: Microsoft :: Windows", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Scientific/Engineering", + "Topic :: System :: Hardware", +] + +# Runtime dependencies — kept intentionally minimal for Sprint 0. +# GUI (PySide6), Modbus (pymodbus), serial, etc. are added in later sprints. +dependencies = [ + "pydantic>=2.6,<3.0", + "pyyaml>=6.0", + "python-dateutil>=2.8", +] + +[project.optional-dependencies] +dev = [ + "pytest>=8.0", + "pytest-cov>=4.1", + "ruff>=0.4", + "mypy>=1.10", + "types-PyYAML", + "types-python-dateutil", +] +# Studio GUI -- Sprint 2.5+. Heavy (~80 MB), kept optional so the core can +# be installed in lean environments (CI, headless test bench). +# PySide6 >= 6.6 includes QtSerialPort on all platforms — no extra dep needed. +studio = [ + "PySide6>=6.6", + "pyserial>=3.5", + "platformio>=6.1", +] +# Installer tooling — required on the developer's build machine. +installer = [ + "requests>=2.31", +] +# License server — deploy to arelectronics.com VPS. +license-server = [ + "fastapi>=0.111", + "uvicorn[standard]>=0.29", + "sqlalchemy>=2.0", + "pydantic>=2.7", + "python-dotenv>=1.0", +] +# AR Display Manager — multi-monitor app switcher for the Integrated Bridge System. +# Same PySide6 dep as the Studio; listed separately so it can be installed standalone. +display-manager = [ + "PySide6>=6.6", +] + +[project.urls] +Homepage = "https://github.com/alro65/AR-Autopilot" + +[tool.setuptools.packages.find] +where = ["."] +include = ["arautopilot*"] +exclude = ["arautopilot.tests*"] + +[tool.setuptools.package-data] +"arautopilot.library" = [ + "actuators/*.json", + "default_tunings/*.yaml", + "vessel_profiles/*.yaml", + "_schemas/*.json", +] + +# ---------------------------------------------------------------------------- +# pytest +# ---------------------------------------------------------------------------- +[tool.pytest.ini_options] +minversion = "8.0" +testpaths = ["arautopilot/tests"] +python_files = ["test_*.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] +addopts = [ + "-ra", + "--strict-markers", + "--strict-config", + "--showlocals", +] +markers = [ + "slow: marks tests as slow (deselect with '-m \"not slow\"')", + "integration: marks integration tests", +] + +# ---------------------------------------------------------------------------- +# Coverage +# ---------------------------------------------------------------------------- +[tool.coverage.run] +source = ["arautopilot"] +omit = [ + "arautopilot/tests/*", + "arautopilot/studio/*", # GUI not in scope for Sprint 0 +] + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "raise NotImplementedError", + "if TYPE_CHECKING:", + "if __name__ == .__main__.:", +] + +# ---------------------------------------------------------------------------- +# Ruff (linting + formatting) +# ---------------------------------------------------------------------------- +[tool.ruff] +line-length = 100 +target-version = "py311" +extend-exclude = [ + "firmware", + "display", + "installer", + ".venv", + "build", + "dist", +] + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "UP", # pyupgrade + "N", # pep8-naming + "SIM", # flake8-simplify +] +ignore = [ + "E501", # line too long (handled by formatter) + "B008", # function calls in default args (common with pydantic Field) +] + +[tool.ruff.lint.per-file-ignores] +"arautopilot/tests/*" = ["N802", "N803"] # test names + +# ---------------------------------------------------------------------------- +# mypy +# ---------------------------------------------------------------------------- +[tool.mypy] +python_version = "3.11" +strict = true +warn_unused_ignores = true +warn_redundant_casts = true +plugins = ["pydantic.mypy"] +exclude = [ + "build/", + "dist/", + "firmware/", + "display/", + "installer/", + "arautopilot/studio/", # GUI stubs, not in scope for Sprint 0 +] + +[[tool.mypy.overrides]] +module = "arautopilot.tests.*" +disallow_untyped_defs = false + +[tool.pydantic-mypy] +init_forbid_extra = true +init_typed = true +warn_required_dynamic_aliases = true diff --git a/start.bat b/start.bat new file mode 100644 index 0000000..a370990 --- /dev/null +++ b/start.bat @@ -0,0 +1,34 @@ +@echo off +title AR-ElecArrangement Server +cd /d "%~dp0" + +:: Matar proceso previo en el puerto 5505 (si quedó colgado de una corrida anterior) +for /f "tokens=5" %%a in ('netstat -ano 2^>nul ^| findstr ":5505 " ^| findstr "LISTENING"') do ( + taskkill /F /PID %%a >nul 2>&1 +) +timeout /t 1 /nobreak >nul + +echo. +echo ===================================================== +echo AR-ElecArrangement +echo Servidor: http://localhost:5505 +echo LAN: http://%COMPUTERNAME%:5505 +echo ===================================================== +echo. + +:: Usar venv si existe, si no buscar python en el PATH +if exist "%~dp0venv\Scripts\python.exe" ( + set "PY=%~dp0venv\Scripts\python.exe" +) else ( + set "PY=python" +) + +:: PYTHONPATH para que ``backend.main`` resuelva ``arelec`` del subpaquete +set "PYTHONPATH=%~dp0backend" + +:loop +"%PY%" -m backend.main +echo. +echo Servidor detenido. Reiniciando en 3 segundos... (Ctrl+C para salir) +timeout /t 3 /nobreak >nul +goto loop