Files
alro65 de25dcee57 feat(installer): J6412 USB installer + AR Electronics license activation server
installer/:
  - build_usb.py: dev tool — builds Flutter + AR-ECDIS, assembles USB pendrive
    image with serial.key, autorun.inf, and START_INSTALLER.bat
  - serial_generator.py: generates AR-XXXX-XXXX-XXXX serial numbers (48-bit
    entropy), logs to CSV for CRM integration
  - src/license.py: hardware fingerprint (Windows MachineGuid + primary MAC),
    serial validation, online activation POST, local cache with 30-day offline
    grace period
  - src/sysconfig.py: HKCU autostart registry entries, .lnk shortcuts (desktop
    + Start Menu via WScript.Shell), firewall rules (netsh), COM port detection
  - src/install.py: tkinter installer GUI — 9 sequential steps with per-step
    progress indicators, threaded execution, error dialogs, and silent mode

license_server/ (FastAPI service — deploy to arelectronics.com VPS):
  - POST /api/v1/activate: first activation accepted; same-HW re-activation
    refreshes heartbeat; different-HW rejected with 409
  - GET  /api/v1/validate/{serial}: heartbeat endpoint to refresh offline cache
  - Admin endpoints (X-Admin-Key): issue, list, revoke licenses
  - SQLAlchemy models: License (serial registry) + Activation (per-machine rows)
  - SQLite default, PostgreSQL-ready via DATABASE_URL env var

AR_electronics — AR-Autopilot Project
2026-05-24 01:36:24 -04:00

263 lines
9.4 KiB
Python

#!/usr/bin/env python3
# =============================================================================
# installer/build_usb.py — Build a USB pendrive installer image
# =============================================================================
#
# Developer tool that:
# 1. Builds the Flutter Windows release (optional — skip with --no-flutter)
# 2. Copies AR-ECDIS and AR-Autopilot binaries into dist/packages/
# 3. Generates a fresh serial number and writes serial.key
# 4. Creates autorun.inf and START_INSTALLER.bat
#
# Output: dist/ directory ready to be copied to a USB pendrive.
#
# Prerequisites (on the build machine):
# - Flutter SDK in PATH (for AR-Autopilot Display)
# - Python 3.11+
# - AR-ECDIS webecdis cloned next to AR-Autopilot (or set --ecdis-dir)
# - PyInstaller (pip install pyinstaller) — for AR-ECDIS .exe packaging
#
# Usage:
# cd installer
# python build_usb.py --vessel "BUQUE NORTE" --csv ../serials_log.csv
# python build_usb.py --no-flutter --no-ecdis # quick test build
# =============================================================================
from __future__ import annotations
import argparse
import shutil
import subprocess
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
DISPLAY_DIR = REPO_ROOT / "display"
INSTALLER_SRC = Path(__file__).resolve().parent / "src"
DIST_DIR = Path(__file__).resolve().parent / "dist"
# Default location for the AR-ECDIS repo (sibling of AR-Autopilot)
DEFAULT_ECDIS = REPO_ROOT.parent / "AR ECDIS" / "webecdis"
def run(cmd: list[str], cwd: Path | None = None, check: bool = True):
print(f" $ {' '.join(str(c) for c in cmd)}")
subprocess.run(cmd, cwd=cwd, check=check)
# ---------------------------------------------------------------------------
# Step 1 — Flutter Windows build
# ---------------------------------------------------------------------------
def build_flutter(flutter_cmd: str = "flutter") -> Path:
"""Build AR-Autopilot Display for Windows and return the build output dir."""
print("\n[1/5] Building Flutter (Windows release)…")
run([flutter_cmd, "build", "windows", "--release"], cwd=DISPLAY_DIR)
build_out = DISPLAY_DIR / "build" / "windows" / "x64" / "runner" / "Release"
if not build_out.exists():
raise FileNotFoundError(f"Flutter build output not found: {build_out}")
print(f" Output: {build_out}")
return build_out
# ---------------------------------------------------------------------------
# Step 2 — AR-ECDIS PyInstaller build
# ---------------------------------------------------------------------------
def build_ecdis(ecdis_dir: Path) -> Path:
"""Package AR-ECDIS with PyInstaller and return the dist directory."""
print("\n[2/5] Building AR-ECDIS (PyInstaller)…")
if not ecdis_dir.exists():
print(f" AR-ECDIS dir not found ({ecdis_dir}) — skipping.")
return Path()
main_py = ecdis_dir / "main.py"
if not main_py.exists():
print(f" AR-ECDIS main.py not found — skipping.")
return Path()
run(
[
sys.executable, "-m", "PyInstaller",
"--onedir",
"--name", "AR-ECDIS",
"--windowed",
"--clean",
str(main_py),
],
cwd=ecdis_dir,
check=False, # non-fatal — missing PyInstaller is warned, not fatal
)
out = ecdis_dir / "dist" / "AR-ECDIS"
if out.exists():
print(f" Output: {out}")
else:
print(" PyInstaller output not found — AR-ECDIS skipped.")
out = Path()
return out
# ---------------------------------------------------------------------------
# Step 3 — Assemble dist/ tree
# ---------------------------------------------------------------------------
def assemble_dist(
flutter_build: Path,
ecdis_build: Path,
serial: str,
) -> None:
print("\n[3/5] Assembling USB installer tree…")
# Clean previous dist
if DIST_DIR.exists():
shutil.rmtree(DIST_DIR)
DIST_DIR.mkdir(parents=True)
# Copy installer source files
pkg_installer = DIST_DIR
shutil.copytree(INSTALLER_SRC, pkg_installer / "src")
# Place the serial key
(DIST_DIR / "serial.key").write_text(serial, encoding="utf-8")
# Copy app packages
packages = DIST_DIR / "packages"
packages.mkdir()
if flutter_build.exists():
dest = packages / "AR-Autopilot"
shutil.copytree(flutter_build, dest)
print(f" AR-Autopilot → packages/AR-Autopilot/")
if ecdis_build.exists():
dest = packages / "AR-ECDIS"
shutil.copytree(ecdis_build, dest)
print(f" AR-ECDIS → packages/AR-ECDIS/")
print(f" Serial key → serial.key ({serial})")
# ---------------------------------------------------------------------------
# Step 4 — Write autorun + launcher batch
# ---------------------------------------------------------------------------
def write_autorun() -> None:
print("\n[4/5] Writing autorun.inf and START_INSTALLER.bat…")
autorun = (
"[autorun]\n"
"label=AR Electronics Installer\n"
"open=START_INSTALLER.bat\n"
"icon=src\\install.py,0\n"
)
(DIST_DIR / "autorun.inf").write_text(autorun, encoding="utf-8")
batch = (
"@echo off\n"
"title AR Electronics — Instalador J6412\n"
'echo Iniciando instalador AR Electronics...\n'
'cd /d "%~dp0"\n'
"python src\\install.py\n"
"if errorlevel 1 (\n"
" echo.\n"
" echo ERROR: La instalacion fallo.\n"
" pause\n"
")\n"
)
(DIST_DIR / "START_INSTALLER.bat").write_text(batch, encoding="utf-8")
# README for field technicians
readme = (
"=== AR Electronics — Instalador J6412 ===\n\n"
"1. Conecte este pendrive al mini PC J6412.\n"
"2. Abra el explorador de archivos y ejecute START_INSTALLER.bat.\n"
" (Si Windows pregunta, elija 'Más información' → 'Ejecutar de todas formas'.)\n"
"3. El instalador solicitará permisos de administrador — acepte.\n"
"4. Pulse INSTALAR y espere a que finalice.\n"
"5. Reinicie el equipo.\n\n"
"El sistema requiere conexión a internet para la activación de la licencia.\n\n"
"Soporte: soporte@arelectronics.com\n"
)
(DIST_DIR / "LEAME.txt").write_text(readme, encoding="utf-8")
# ---------------------------------------------------------------------------
# Step 5 — Summary
# ---------------------------------------------------------------------------
def print_summary(serial: str) -> None:
print("\n[5/5] Build complete.")
print(f"\n Directorio de salida : {DIST_DIR}")
print(f" Número de serie : {serial}")
size_mb = sum(f.stat().st_size for f in DIST_DIR.rglob("*") if f.is_file()) / 1e6
print(f" Tamaño total : {size_mb:.1f} MB")
print("\n Copie todo el contenido de dist/ al pendrive USB.")
print(" Asegúrese de que el pendrive tenga al menos 2 GB de espacio.\n")
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
parser = argparse.ArgumentParser(
description="AR Electronics — USB installer builder"
)
parser.add_argument("--vessel", default="",
help="Vessel name to tag the serial number with")
parser.add_argument("--csv",
help="Path to serials CSV log (created or appended)")
parser.add_argument("--ecdis-dir", type=Path, default=DEFAULT_ECDIS,
help=f"Path to AR-ECDIS webecdis directory (default: {DEFAULT_ECDIS})")
parser.add_argument("--flutter", default="flutter",
help="flutter command (default: 'flutter')")
parser.add_argument("--no-flutter", action="store_true",
help="Skip Flutter build (use existing build output)")
parser.add_argument("--no-ecdis", action="store_true",
help="Skip AR-ECDIS PyInstaller build")
parser.add_argument("--serial",
help="Use an existing serial number instead of generating one")
args = parser.parse_args()
print("=" * 60)
print(" AR Electronics — USB Installer Builder")
print("=" * 60)
# Resolve serial
if args.serial:
serial = args.serial
print(f"\n Using existing serial: {serial}")
else:
# Import here to avoid circular issues if running as a module
sys.path.insert(0, str(Path(__file__).resolve().parent))
from serial_generator import generate_batch, write_csv # noqa: PLC0415
records = generate_batch(1, vessel=args.vessel)
serial = records[0]["serial"]
print(f"\n Generated serial: {serial}")
if args.csv:
write_csv(records, args.csv)
# Flutter build
if args.no_flutter:
flutter_build = DISPLAY_DIR / "build" / "windows" / "x64" / "runner" / "Release"
print(f"\n[1/5] Skipping Flutter build — using {flutter_build}")
else:
flutter_build = build_flutter(args.flutter)
# AR-ECDIS build
if args.no_ecdis:
ecdis_build = Path()
print("\n[2/5] Skipping AR-ECDIS build.")
else:
ecdis_build = build_ecdis(args.ecdis_dir)
# Assemble
assemble_dist(flutter_build, ecdis_build, serial)
write_autorun()
print_summary(serial)
if __name__ == "__main__":
main()