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 */ }
pub trait DewHeater: Send + Sync { /* status, set_power, set_auto */ }
pub trait SwitchHub: Send + Sync { /* switches, status, set_switch */ }
pub trait SafetyMonitor: Send + Sync { /* status */ }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 session_stats: Arc<RwLock<SessionStats>>,
pub live_stack: Arc<RwLock<LiveStackState>>,
pub hips_cache: Arc<HipsCache>,
pub cloud_sync: Arc<CloudSyncService>,
pub plugin_registry: Arc<PluginRegistry>,
pub template_store: Arc<TemplateStore>,
pub alignment: Arc<RwLock<AlignmentModel>>,
pub sky_flat: Arc<RwLock<SkyFlatState>>,
pub log_buffer: Arc<RwLock<VecDeque<LogEntry>>>,
}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 (home screen) mount/ Mount control panel camera/ Camera, FITS viewer, flat wizard, plate solve (merged from Solver panel) 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 session/ Session report (metrics, per-filter breakdown) framing/ Framing assistant (DSS + FOV) rotator/ Rotator control settings/ App settings, sky cache, cloud sync components/layout/ App shell, docking system (footer/sidebar/floating panels) hooks/ useWsEvent, useMountStatus, etc. i18n/locales/ PT / EN / ES (~24 namespaces each)
The sky map is the home screen, not one tab among many. A docking system (footer, left sidebar, or floating windows — not a tab bar) hosts the rest of the panels: mount, camera (now includes plate solving), focuser, guiding, sequencer, scheduler, dome, session, dew heater, and switches. Settings, logs, and help open on demand rather than living in the dock by default.
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).
