OpenSkyLabOpenSkyLab

Architecture

System Architecture

OpenSkyLab follows a clean layered architecture: Rust backend with trait-based device abstraction, Axum HTTP/WebSocket server, and React frontend bundled via Tauri v2 for desktop distribution.

Architecture Overview

data-flow.txt
Frontend (React 19 + Vite)
    ↕ HTTP REST /api/v1/*
    ↕ WebSocket ws://localhost:7624/ws
Backend Daemon (Rust / Axum)  port 7624
    ↕ DAL traits (osl-core)
    ├── INDI Adapter    → INDI Server (port 7625)
    ├── ASCOM Adapter   → ASCOM Alpaca REST (port 11111)
    ├── PHD2 Adapter    → PHD2 JSON-RPC (port 4400)
    ├── Solver Service  → ASTAP process (spawn)
    └── Catalog Service → SQLite local

Technology Stack

LayerTechnologyVersion
BackendRust + Tokio1.78+
HTTP/WS ServerAxum0.7.x
FrontendReact + Vite19.x / 5.x
StateZustand + Immerlatest
UIRadix UI + Tailwind CSSlatest
Sky RenderingThree.jsr165+
Desktop ShellTauriv2
DatabaseSQLite (rusqlite)0.31
ChartsuPlot + Rechartslatest

Device Abstraction Layer (DAL)

The DAL is the heart of OpenSkyLab. All device interfaces are defined as async Rust traits in osl-core. Adapters (INDI, ASCOM, Simulator) implement these traits, and the rest of the system only talks to trait objects.

The frontend never knows what hardware is connected. INDI, ASCOM, simulators — the DAL abstracts everything behind the same interface.

Core Traits

osl-core/src/traits.rs
pub trait Mount: Send + Sync {
    async fn goto(&self, ra: Hours, dec: Degrees) -> Result<()>;
    async fn sync(&self, ra: Hours, dec: Degrees) -> Result<()>;
    async fn abort_slew(&self) -> Result<()>;
    async fn set_tracking(&self, mode: TrackingMode) -> Result<()>;
    async fn park(&self) -> Result<()>;
    async fn unpark(&self) -> Result<()>;
    async fn coordinates(&self) -> Result<MountCoordinates>;
    async fn status(&self) -> Result<MountStatus>;
    async fn pier_side(&self) -> Result<PierSide>;
    async fn meridian_flip(&self) -> Result<()>;
    // ... and more
}

pub trait Camera: Send + Sync {
    async fn expose(&self, params: ExposureParams) -> Result<ExposureHandle>;
    async fn abort_exposure(&self) -> Result<()>;
    async fn download_frame(&self, handle: ExposureHandle) -> Result<FitsFrame>;
    async fn set_cooler(&self, enabled: bool, target_c: f32) -> Result<()>;
    async fn camera_info(&self) -> Result<CameraInfo>;
    // ... and more
}

pub trait Focuser: Send + Sync { /* move_to, position, temperature */ }
pub trait FilterWheel: Send + Sync { /* set_filter, current_filter */ }
pub trait Rotator: Send + Sync { /* rotate_to, position, sync */ }
pub trait Dome: Send + Sync { /* open_shutter, close_shutter, goto_az */ }
pub trait WeatherStation: Send + Sync { /* conditions */ }
pub trait FlatPanel: Send + Sync { /* open, close, set_brightness */ }

Domain Types

osl-core/src/types.rs
// Newtypes prevent unit confusion (hours vs degrees)
pub struct Hours(pub f64);     // RA in hours
pub struct Degrees(pub f64);   // DEC, Alt, Az in degrees
pub struct Arcsec(pub f64);    // arcseconds

pub enum TrackingMode { Sidereal, Lunar, Solar, Off }
pub enum PierSide { East, West, Unknown }
pub enum FrameType { Light, Dark, Flat, Bias }
pub enum BayerPattern { RGGB, BGGR, GRBG, GBRG }
pub enum ShutterStatus { Open, Closed, Opening, Closing, Unknown }

Backend Crate Map

dependency graph
osl-core          Zero deps. DAL traits + domain types.
osl-plugin-sdk    Zero deps. Public plugin contract.
                  ↓
osl-sim           Implements osl-core traits (simulators)
osl-indi          Implements osl-core traits (INDI TCP/XML)
osl-ascom         Implements osl-core traits (ASCOM Alpaca REST)
osl-phd2          PHD2 JSON-RPC client
osl-solver        ASTAP + astrometry.net plate solver
osl-catalog       SQLite catalog service
osl-sequence      Sequencer state machine
osl-scheduler     Multi-night observation scheduler
                  ↓
osl-api           Axum HTTP/WS server (binary, uses all above)

Each crate has a CLAUDE.md documenting its scope, public interfaces, and internal state. This enforces clear boundaries — no circular dependencies, no trait leakage.

AppState (Central Container)

osl-api/src/state.rs
pub struct AppState {
    pub devices: Arc<RwLock<DeviceRegistry>>,
    pub event_tx: broadcast::Sender<WsEvent>,
    pub config: Arc<RwLock<AppConfig>>,
    pub sequencer: Arc<SequencerEngine>,
    pub scheduler: Arc<SchedulerEngine>,
    pub frame_history: Arc<RwLock<Vec<FrameRecord>>>,
    pub live_stack: Arc<RwLock<LiveStackState>>,
    pub plugin_registry: Arc<PluginRegistry>,
    pub alignment: Arc<RwLock<AlignmentModel>>,
    // ... and more
}

Frontend Structure

apps/web/src/
api/
  client.ts          Fetch wrapper, configurable base URL, auth token
  ws.ts              WebSocket client with auto-reconnect
  types.ts           TypeScript types mirroring backend schemas

stores/              Zustand stores (one per domain)
  mount-store.ts     camera-store.ts     focuser-store.ts
  guiding-store.ts   sequencer-store.ts  scheduler-store.ts
  solver-store.ts    dome-store.ts       config-store.ts
  layout-store.ts    toast-store.ts      catalog-store.ts

features/            One directory per domain
  skymap/            Three.js WebGL sky map
  mount/             Mount control panel
  camera/            Camera, FITS viewer, flat wizard
  focuser/           Focuser control + autofocus
  guiding/           PHD2 integration + uPlot charts
  sequencer/         Sequence builder + template browser
  scheduler/         Multi-night planner
  mosaic/            Mosaic grid planner
  dome/              Dome shutter + azimuth
  framing/           Framing assistant (DSS + FOV)
  rotator/           Rotator control
  settings/          App settings, sky cache, cloud sync
  dashboard/         Device overview + onboarding

components/layout/   App shell, sidebar, dock, floating panels
hooks/               useWsEvent, useMountStatus, etc.
i18n/locales/        PT / EN / ES (18 namespaces each)

14 panels available in the UI: dashboard, mount, camera, focuser, guiding, solver, sequencer, scheduler, mosaic, dome, session, settings, rotator, framing.

Device Adapters

INDI Adapter (osl-indi)

TCP/XML protocol on port 7625. Maps INDI properties to DAL traits: EQUATORIAL_EOD_COORD → coordinates, ON_COORD_SET → goto, CCD_EXPOSURE → expose. BLOB download with base64 decode for frame data. Auto-reconnect with exponential backoff (1s → 30s).

ASCOM Alpaca Adapter (osl-ascom)

REST API on port 11111. Implements 4 device types: Telescope (Mount), Camera, Focuser, FilterWheel. Each method maps to a PUT/GET call to the Alpaca endpoint. Auto-retry with exponential backoff (500ms → 2s, 3 retries max).

PHD2 Adapter (osl-phd2)

JSON-RPC 2.0 on port 4400. Guide, dither, stop, get_status. Receives GuideStep events at ~1Hz with RA/DEC corrections and RMS stats. Auto-reconnect on connection loss.

Simulator Adapter (osl-sim)

Realistic simulators for every device type. SimMount simulates slew velocity and tracking drift. SimCamera generates FITS with synthetic stars and noise. SimFocuser models a HFR parabola for autofocus testing.

Data Flow

All communication between frontend and backend uses two channels:

  • REST API — Command/response for actions (GoTo, expose, set filter). All endpoints under /api/v1/
  • WebSocket — Real-time events pushed from backend (mount position at 10Hz, guiding stats at 1Hz, frame completion events). Single connection at ws://localhost:7624/ws

Frontend Zustand stores subscribe to WebSocket events via the useWsEvent hook and update state automatically. Components re-render on state changes.

Security Model

Local Mode (Default)

Backend binds to 127.0.0.1:7624 only. No authentication required. Tauri CSP restricts frontend connections to localhost.

Remote Mode

terminal
# Enable remote access (local network only)
./openskylab-daemon --bind 0.0.0.0 --token <your-token>

# Token auto-generated on first boot, saved in:
# ~/.config/openskylab/config.toml

# Frontend sends: Authorization: Bearer <token>

Never expose the daemon to the public internet. Remote access is designed for local network use (e.g., mini-PC at the telescope, control from indoor laptop).