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
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 localTechnology Stack
| Layer | Technology | Version |
|---|---|---|
| Backend | Rust + Tokio | 1.78+ |
| HTTP/WS Server | Axum | 0.7.x |
| Frontend | React + Vite | 19.x / 5.x |
| State | Zustand + Immer | latest |
| UI | Radix UI + Tailwind CSS | latest |
| Sky Rendering | Three.js | r165+ |
| Desktop Shell | Tauri | v2 |
| Database | SQLite (rusqlite) | 0.31 |
| Charts | uPlot + Recharts | latest |
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
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
// 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
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)
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
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
# 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).
