Cross-platform headless astronomy controller for Raspberry Pi, ARM64 SBCs, and Windows mini PCs.
⚠️ N.I.N.A. Polaris is a community-driven fork of N.I.N.A. It is not affiliated with or supported by the official N.I.N.A. development team. Please don't ask them for support with this fork, open issues here instead.
N.I.N.A. Polaris is a lightweight, browser-controlled astrophotography system built on ASP.NET Core. It brings the power of N.I.N.A. (Nighttime Imaging 'N' Astronomy) to single-board computers and small-form-factor PCs, with a responsive Web UI accessible from any device on the network.
The Raspberry Pi (or Windows mini PC) acts as a data broker, controlling hardware, saving FITS files, and streaming images, while your laptop, tablet, or phone handles all the heavy rendering in the browser.
Browser (laptop / tablet / phone) Raspberry Pi / Mini PC
┌──────────────────────────────┐ ┌──────────────────────────┐
│ Web UI (Alpine.js) │◄──HTTP─►│ ASP.NET Core + Kestrel │
│ Live preview (Canvas/WebGL) │◄──WS───►│ INDI client (TCP 7624) │
│ Image processing (client) │ │ Plate solving (ASTAP) │
│ Sky explorer │ │ Sequence engine │
└──────────────────────────────┘ │ Live stacking │
└──────────────────────────┘
- Features
- Equipment Control via INDI
- DSLR / Mirrorless cameras
- Real-Time Image Streaming
- Live Stacking (EAA)
- Plate Solving & Centering
- Guiding (PHD2), full management
- Manual Focus Assist (no motor required)
- Auto-Focus (V-Curve)
- Meridian Flip Automation
- Advanced Sequencer (tree-based)
- Multi-Target Night Planner (PLAN)
- Mosaic Planner
- Plugin System
- Dithering
- Sky Catalog & Sky Atlas
- Sky Map
- Weather Forecast
- Tonight's Best
- Studio (post-processing)
- External tools (Siril + GraXpert)
- File explorer
- Sequence Engine + Image Persistence
- Flat Wizard
- Web UI
- Equipment Rigs (multi-rig support)
- Telescope + Accessory Catalogue
- Profile Management
- Remote Access (Relay Server)
- Network Resilience
- Discovery & Cross-Platform Drivers
- Remote Terminal (SSH from the browser)
- Software Self-Update (SBC)
- Polar Alignment (TPPA)
- Architecture
- Getting Started
- Deployment
- API Reference
- Configuration
- Performance Targets
- Support the project
- Contributing
- License
Looking for the full tooling matrix? See REQUIREMENTS.md for the complete required + optional dependency list per platform (Windows / Linux ARM-RPi / Linux x64), with firewall rules and hardware sizing guidance.
Full INDI protocol client with support for 400+ Linux drivers:
- Camera, Capture, exposure control, gain, binning, ROI, cooler temperature
- Telescope/Mount, Slew, GoTo, park/unpark, tracking (sidereal/lunar/solar), NSEW manual control
- Focuser, Absolute/relative move, step control, temperature readout
- Filter Wheel, Position selection by slot number or filter name
- Guider, Pulse guiding in 4 directions, guide camera exposure
- Dome, Azimuth slew, shutter open/close, park/unpark, slave mode
- Rotator, Angle positioning, reverse toggle
- Weather, Temperature, humidity, dew point, wind, pressure, cloud cover, SQM, rain, safety status
- Flat Panel, Light on/off, brightness control, dust cap open/close
The RIGS → INDI control panel sub-tab is a built-in, in-browser
replacement for the standalone indi_control_panel Qt app (which
recent Raspberry Pi OS / libindi 2.x releases no longer package). It
renders the full device → group → property tree for every connected
INDI device and lets you read and edit any property without xpra or
an external Qt install:
- Number / switch / text / light properties with type-aware editors (inputs, radio/checkbox, read-only indicators), live state dots, per-device grouping, search, and auto-refresh.
- Edits POST through the same
IndiClientsetters the rest of the app uses, then auto-save to the driver's~/.indi/*_config.xmlso they survive reconnects. - Property descriptions / help. The INDI protocol carries no
description field (drivers advertise only a name and a label), so
every property gets a small (?) help icon. Hovering shows a
plain-language English explanation as a tooltip; clicking opens a
little editor where you can read the built-in description and write
your own note. Built-in descriptions for ~80 common INDI standard
properties ship in
wwwroot/data/indi-property-help.json; your own notes are saved in the profile (keyed by property name, so a note onCCD_TEMPERATUREshows for every camera) and take priority over the built-in text.
Beyond the dedicated astronomy cameras INDI exposes, Polaris speaks
to consumer DSLR / mirrorless bodies through a shared ICamera
abstraction with driver-specific backends:
- Linux, use the existing INDI
indi_gphoto_ccddriver (wraps libgphoto2, supports hundreds of Canon / Nikon / Sony / Fuji bodies). Zero extra Polaris code; pickdriver=indiin the Equipment card and the gphoto-exposed camera shows up alongside the astro CCDs. Setup walkthrough indocs/dslr-linux.md. - Canon (Windows), native Canon EDSDK integration with full
capture path: RAW + JPEG dual delivery, ISO + shutter + bulb
control, automatic SaveTo=Host. CR2 files land verbatim under
{rig}/lights/.../; the embedded JPEG drives the live preview. Install instructions + EULA caveats indocs/dslr-windows-canon.md. - Nikon (Windows), skeleton driver wired into the Equipment UI
and ICamera dispatch. Implementation path documented (vendor the
MIT-licensed MekNikon
MAID bindings, or build against the Nikon Imaging SDK for Z
series) in
docs/dslr-windows-nikon.md. - Sony (Windows + Linux), skeleton driver covering two
complementary paths: the legacy Wi-Fi Camera Remote API v1.90
(HTTP/JSON, cross-platform, easiest to implement, reference:
nantcom/SonyCameraSDK)
for older α / NEX bodies, and the modern USB Camera Remote SDK
v2.x for α7 III onward. Full landscape in
docs/dslr-windows-sony.md.
The UI auto-detects which vendor SDKs are reachable on the running host, shows install banners (with direct doc links) for any that aren't installed, surfaces an ISO dropdown instead of the Gain field for DSLR-class cameras, and hides cooler / binning controls that don't apply. Captured RAW files (CR2 / NEF / ARW) are saved verbatim alongside the camera-native pipeline, the embedded JPEG becomes the on-screen preview while the RAW waits for the Studio panel (or PixInsight / Siril if you'd rather process there).
Single ASIAIR / StellaVita-style circular shutter unifies capture across LIVE, PREVIEW, FOCUS Manual Assist, VIDEO Capture and AUTORUN. SVG progress ring around the button shows real-time exposure / cycle / sequence progress; inner icon morphs to STOP when active.
Gesture model (same across every tab):
- Tap when idle → single capture / start
- Long-press 600ms when idle → enter loop mode (LIVE / PREVIEW / FOCUS); same as tap on tabs that don't have a distinct loop (VIDEO / AUTORUN). The ring fills during the hold for visual feedback, snapping into loop mode at 600ms.
- Tap during capture → abort / stop
Shutter lives centered vertically + horizontally in the .quick-controls sidebar on the right. AUTORUN gained a new right sidebar (it was footer-style before) so it follows the same layout. Mobile (≤900px) collapses each pane to a single column with the shutter sidebar docked below the canvas / sequence list.
Dual-mode WebSocket image streaming with automatic format negotiation:
- JPEG mode (default), Server-side auto-stretch and JPEG encoding, works on all browsers (~300KB per frame)
- Raw mode, LZ4-compressed 16-bit pixel data with client-side WebGL debayer and MTF stretch (~3-10MB per frame)
- Backpressure handling, slow clients skip frames instead of falling behind
- Dead client eviction after consecutive send failures
- REST endpoint for latest preview image (
/api/image/latest/preview)
Real-time stacking for electronically assisted astronomy:
- Star detection via flood-fill HFR algorithm
- Triangle-based star matching for alignment
- Affine transform registration (translation + rotation + scale)
- Running average accumulation buffer
- Start/stop/reset controls with frame counter
- Per-frame median HFR + star count piggy-backed on the alignment pass (no extra detection cost), surfaced in WebSocket status so the LIVE tab can show drift over time
Auto re-focus + auto re-center triggers (LSTR): two independent trigger axes that fire automatically during long EAA / comet-hunting sessions without leaving the LIVE tab.
Re-focus triggers (any combination, first to cross fires):
- Every N integrated frames
- Every N minutes since last refocus
- Sensor temperature drift ≥ ±X°C
- HFR degradation ≥ Y% above HFR right after last AF run
Re-center triggers (same OR-combine pattern):
- Every N integrated frames
- Every N minutes
- Plate-solve drift ≥ X arcsec (per-frame solve, heavy on RPi 4, default off)
Reference RA/Dec for re-center comes from a one-shot plate solve on
the first integrated frame (true astrometric position, not the mount's
report). Trigger handlers run sequentially inside AddFrameAsync,
the upstream capture pipeline naturally pauses during AF / re-center
since whoever's pushing frames is awaiting that call. Reentry guard
prevents concurrent AF + re-center on overlapping triggers.
Per-rig persistence via EquipmentProfile.LiveStackTriggers so each
setup keeps its own thermal + drift policy. UI is a collapsible
<details> panel inside the LIVE tab below the stack controls;
▶ Now buttons bypass gates for manual fires.
Per-frame pre-processing (LSPP): calibration (subtract master
dark/bias, divide by master flat) and/or BGE (GraXpert background
extraction) run on every incoming frame before it lands in the
live-stack accumulator. Calibration auto-matches masters from the
FrameLibrary by (gain, exposure, filter) -- counters + chosen master
file names land in the WS payload + LIVE-tab status block. BGE runs
client-side via the existing browser WebAssembly pipeline (the same
one FILES and EDITOR use), so it's available only when the rig's
stack compute mode is "client" or "auto + a WASM-capable browser
attached"; server-mode shows a banner explaining the constraint.
Failure is fail-safe -- any per-frame error logs a warning, the raw
frame goes into the stack instead, the session never aborts. New
<details> panel inside LIVE below the triggers block. See
docs/user-guide/live-stacking.md.
Refocus suggestion (REFSUG): trend-based advisory chip + LIVE-tab callout for manual-focuser users (or motorized rigs with auto-refocus disabled). The detector watches per-frame HFR + star count, sets a baseline equal to the 5th-percentile of the last 20 stable samples, and fires when the rolling mean + slope show systematic degradation (>15% above baseline + positive slope + 5-frame extrapolated change
30% of baseline). Star-count crash (>30% drop) is a secondary trigger. Auto-clears when HFR + star count both recover for 3 consecutive frames; "I refocused" button manually resets the baseline. No profile fields, detection is fully automatic. See
docs/user-guide/live-stacking.md.
SNR + ETA to target (SNR): background SNR
(mean(signal) − mean(background)) / σ(background) computed on every
frame and on the cumulative stack. Per-rig Target SNR (with a
session-level override in the LIVE overlay) plus a log-log √N fit drive
the ETA widget at the top of the LIVE history panel: SNR atual ·
Frames · "~12 min to SNR 50". SNR also shows up in the PREVIEW + LIVE
bottom bars and on each history thumbnail; the chart pairs cumulative
SNR (cyan) with HFR (amber) so you can see signal building while focus
holds. R² < 0.6 / weak slope / out-of-reach targets all surface as
— instead of fantasy numbers, and the WASM client-side stacker
forwards the same SNR fields so MetricsOnly mode behaves identically
to server-full mode.
Strategy-based plate-solving dispatcher with four interchangeable backends and a primary + blind-fallback pipeline:
- ASTAP, fast offline solver, hint-driven, the default primary
- PlateSolve3, PlaneWave's CLI, excellent at long focal lengths and small FOVs (≤10 stars), requires hints
- Astrometry.net online, REST API at nova.astrometry.net, truly blind, slow but robust (API key required)
- Astrometry.net local,
solve-fieldwrapper with ±20% pixel-scale window, blind-capable - Configurable per-installation:
PlateSolve:PrimarySolver,PlateSolve:BlindSolver,PlateSolve:UseBlindFallback - Slew & Center, Automated loop: slew to target → capture → solve → compute error → sync → re-slew (converges in 2-3 iterations, ~30-60s total)
- Configurable tolerance (default: 30 arcsec), async job tracking with real-time status polling
- Result carries
SolverUsedso the UI knows which backend produced it
PHD2 is a first-class managed device, not just a black box we send commands to.
Connection + telemetry:
- JSON-RPC 2.0 line-framed protocol with async event loop on TCP (default port 4400)
- Ring buffer of last 300 GuideSteps with running RMS RA / RMS Dec / total RMS + peak values
- Live RA/Dec error chart (Chart.js) with auto-scaling Y-axis
- Calibration data captured automatically after a successful CalibrationComplete event
- Alert + settle status surfaced in the UI as toasts and banners
- Shows which guide camera + mount PHD2 is actually using (via
get_current_equipment)
Management, control PHD2 itself from the Web UI:
- Profile switcher, list every PHD2 profile, switch with one click (auto-disconnects equipment first as PHD2 requires)
- Equipment connect/disconnect, tell PHD2 to wire up its own gear
- Exposure, dropdown populated from
get_exposure_durations(e.g. "1.0s" / "100ms") - Dec guide mode, Auto / North / South / Off
- Auto-detect install location, walks the well-known PHD2 install paths per OS (Windows: Program Files / Program Files (x86) /
%LocalAppData%\Programs; macOS:/Applications/PHD2.app; Linux:/usr/bin,/usr/local/bin,/opt/phd2/bin,/snap/bin, plus a$PATHwalk). When not detected the Guider tab surfaces an inline "Download PHD2" banner with a direct link - Launch / Shutdown PHD2 process, when an executable is detected (or
PHD2:ExecutablePathis set), N.I.N.A. Polaris can spawn PHD2 on the same host (loopback only) and gracefully shut it down via theshutdownRPC (falls back to process kill only if we own the process) - Auto-start on boot, a single checkbox in the Guider tab makes the headless app launch PHD2 and connect the JSON-RPC client ~2s after every startup. Persisted per profile; survives restarts. Backed by a hosted service that retries the connect 5× in case PHD2's event server is slow to come up
- Commands: start guiding / stop / loop / pause / resume / dither (with settle pixels + time + timeout) / auto-select star / clear calibration / clear history
Deep integration (PH2X):
- Rig ↔ PHD2 profile sync (1:1), each Polaris rig maps to a PHD2 profile of the same name. Switching rigs automatically switches the PHD2 profile via RPC + applies the rig's algorithm preset + any per-rig algorithm overrides. When a profile is missing, the GUI surfaces a banner pointing to the embedded PHD2 GUI tab where the user can run the Wizard.
- Smart Calibrate, one button: Polaris reads pixel scale, computes a sane calibration step from
(distance_px × pixel_scale) / guide_rate, optionally slews the main mount to the celestial equator, clears calibration, finds a star, triggersguide(recalibrate=true), monitors the calibration to completion via the AppState event stream, validates orthogonality + non-zero rate, and surfaces results. State machine + progress streamed live via/ws/status→guider.calibrateJob. - Algorithm tuning presets,
Default/Reactive/Smoothcurated bundles for Hysteresis (RA) + Resist-Switch (DEC) algorithms; applied viaset_algo_paramwith silent skip for params the current algorithm doesn't expose. Advanced disclosure shows every live knob (get_algo_param_names+ per-nameget_algo_param); editing any knob flips the preset toCustomand persists the bag on the rig. - Embedded PHD2 GUI — the GUIDE tab has a tabstrip: Control (JSON-RPC UI) | PHD2 GUI (PHD2's native window embedded in an iframe). Lets you run PHD2's Profile Wizard, Brain dialog, Guiding Assistant, dark library, etc. remotely without opening a separate VNC/SSH session. Backend chosen by host OS: Linux uses xpra (docs/phd2-gui-embedding.md), Windows uses TightVNC + noVNC (docs/phd2-gui-windows.md). On macOS the Control tab works fully; the GUI tab shows an OS-not-supported banner.
A client-driven capture loop that turns the FOCUS tab into a manual-focus aid, designed for rigs without an electronic focuser and as a fine-tune helper after V-curve completes on motorised rigs.
- HFR trend loop: captures a frame every N seconds, parses HFR / FWHM / star count / Laplacian variance from the standard stats pipeline, plots HFR vs time on a 60-sample scrolling chart. A dashed baseline at the lowest HFR seen since the last Reset shows immediately when you've overshot focus.
- Bahtinov mask analysis: optional checkbox runs a radial line integration on the brightest star, finds the 3 diffraction spikes, identifies the central spike, and reports the perpendicular offset of the central line from the V's intersection. Canvas overlay draws the star cross + 3 spike lines + intersection circle + offset label. Sidebar surfaces a colour-coded readout (green / amber / red) with a rotate-inward / rotate-outward cue.
- Coexists with motorised focusers: subtabs
Manual Assist(default when no motor) andAuto V-curve, with the manual stepper always available above the subtabs. - See docs/user-guide/focus.md for the full workflow.
Automated focus point determination via symmetric sweep:
- Captures N exposures around the current focuser position, measures HFR per sample via flood-fill star detection (median HFR for robustness against outliers)
- Least-squares parabola fit through valid samples; moves to the vertex
- Configurable step size, point count, exposure, minimum stars, backlash compensation, post-focus confirmation frame
- Live V-curve chart (Chart.js scatter) with fitted parabola and best-position marker
- Live frame preview, every AF sweep exposure pipes through the same
/ws/image-streamchannel as LIVE, rendered into a dedicated canvas on the Focus tab with a HUD chip showingpos {N} · HFR {x.xx} · ★ {stars}per sample. Lets you watch the focuser converge in real time without switching tabs. - Restores starting position automatically on cancel or failure
Hands-off pier-side change during a sequence:
- Static LST/GMST math validated against USNO J2000 reference
- Workflow: pause PHD2 → re-slew to target (mount firmware flips) → settle → plate-solve recenter via Slew & Center → optional auto-focus → resume PHD2 guiding
- Configurable minutes-after-meridian trigger threshold (default 5 min)
- Live "Meridian in 1h 23m" countdown in the Sequence tab
- Safe failure paths: errors and cancels always try to resume guiding
A full conditional-execution engine alongside (not replacing) the legacy Simple Sequencer. Toggle the default tab via Settings → Sequencer → "Use Advanced Sequencer by default", both stay available either way.
Tree model:
- Containers group children:
Sequential(run in order),Parallel(run concurrently, fail-fast on any child),DeepSkyObject(slew & plate-solve-center on a target before running children),Templated(paste-in a saved reusable fragment) - Instructions are atomic actions; 30+ shipping:
- Mount: Slew, Center (plate-solve), Park / Unpark, SetTracking, SolveAndSync
- Camera: TakeExposure (N frames + filter / gain / binning / image type), CoolCamera (setpoint + tolerance), WarmCamera (gradual ramp at °C/min)
- Focuser: AutoFocus (V-curve), MoveFocuser, MoveToFilterOffset (looks up the active rig's per-filter offset table; safe no-op when the filter isn't configured)
- Filter wheel: SwitchFilter (by name or position)
- Guider: StartGuiding, StopGuiding, Dither, AutoSelectStar
- Dome: Open / Close shutter, Park, SlewToAzimuth, SyncToScope (Alt/Az math)
- Flat panel: Open/Close cover, SetBrightness, ToggleLight
- Rotator: RotateToAngle
- Flow control: WaitForTime, WaitUntilTime (UTC), WaitUntilAltitude, WaitForSunBelowHorizon (low-precision sun alt for twilight), WaitForMoon (Above / Below altitude)
- External: RunExternalScript (stdout/stderr captured, exit-code aware), SendHttpRequest (webhooks for Discord / Slack / dashboards)
- Conditions are loop predicates (containers with
isLoop=truekeep iterating while every condition holds): Until Time / Altitude / N Exposures / Duration / Moon Sets / While Safe (cloud cover + wind from weather device) - Triggers fire between every child step:
- Auto-focus on Temperature Change / HFR Increase / Every N Minutes / Filter Change
(HFR trigger reads the median HFR from a StarDetector run that
TakeExposureInstructionperforms after every successful frame and parks inctx.Scratch["Frame:LastHfr"]) - Meridian Flip (delegates to the existing service)
- Dither After N Exposures (skipped silently when PHD2 isn't guiding)
- Center After Drift (periodic plate-solve check against pinned coords)
- Safety (cloud cover / wind / mount disconnect → graceful abort with reason)
- Auto-focus on Temperature Change / HFR Increase / Every N Minutes / Filter Change
(HFR trigger reads the median HFR from a StarDetector run that
Persistence + execution:
- Polymorphic JSON format with
$typediscriminator andVersionfield; every entity carries a stable Id so the editor can reference nodes across edits - Validate() bubbles up errors with breadcrumb paths
(
[DSO 'M31'/Lights] Exposure must be positive); the engine refuses Start on a failing tree - File-based template store (
Sequencer:TemplateDir, default./sequencer-templates/) for reusable fragments hydrated intoTemplatedContainerat load time - Background runner with cancellation that propagates to every child;
containers honour an
AbortRequestedflag (set by the Safety trigger) between every step
Tree editor UI:
- New "Adv" tab opens a tri-pane layout: palette (categorised by device), tree (status colour-coded per node), properties (auto-generated form by field type)
- Sortable.js drag-handle on the type badge to reorder siblings
- Save / Download JSON / Upload JSON / Validate / Start / Stop in the toolbar
- Live status mirroring, during a run the tree colours update every 2s showing what's Running / Completed / Failed / Skipped, plus the Safety-trigger abort reason if one fires
ASIAIR-style whole-night planning. The PLAN tab queues several targets, each with its own frame list, and runs them in order with automatic slew + plate-solve-center between targets:
- Plan library stored in the profile (runnable with any active rig), with new / duplicate / delete and JSON export / import
- Per-target frame lists plus per-target re-center / re-focus / dither every N frames and a manual start delay
- Add targets from catalog search, manual RA/DEC, the current mount position, or by framing visually in the Sky map
- Scheduling: start now or at a clock time; end when all frames are done, at astronomical dawn, or at a set time
- Plan-level automation: auto guiding, auto cooling, per-target/initial auto-focus, and auto meridian flip with exposed tuning (minutes-after, recenter, autofocus-on-flip) that pauses/resumes the active guider
- End actions: warm + cooler off, park/go-home, focuser to zero, and a confirm-gated host shutdown
- View plan whole-night elevation timeline + a pinned run bar with current-target and total progress
PLAN compiles to an Advanced-Sequencer document and runs on the same engine as the ADV tab, so only one of the two runs at a time. See docs/user-guide/plan.md.
Build a multi-panel mosaic centred on any selected sky target. The 🧩 Plan mosaic button in the Sky tab opens a modal where you set cols × rows + overlap %, with the per-panel FOV auto-filled from the active rig (sensor + focal length). A live yellow grid overlay appears on the sky map showing where each panel will land, adjust until it covers your target.
- cos(δ) correction on RA, so the grid sits true at any declination
- Serpentine column order on alternating rows minimises slew distance
- Time estimate (slew + plate-solve + N × exposure) from configurable per-knob defaults
- Export to Advanced Sequencer lowers the whole grid into a
SequentialContainerwith oneDeepSkyObjectContainerper panel (each plate-solves + centers + takes N exposures). "Export & load now" jumps straight into the Adv tab with the tree loaded and ready to run.
Drop a third-party .dll into ./plugins/ (or
Plugins:Directory) and on next startup the host loads it into its
own AssemblyLoadContext, scans for INinaPolarisPlugin
implementations, and registers their contributed sequencer entities
into the polymorphic JSON converter + palette. From the user's point
of view the plugin's entities appear in the Advanced Sequencer
palette under whatever category the plugin chose.
- Isolated load context per plugin, failures don't take the host down
- Plugin assemblies reference the host's existing types (
ISequenceEntity,SequenceContext,ILogger) directly, no SDK package to publish - Contract surface in
Services/Plugins/:INinaPolarisPlugin,Name/Version/Description/Author+Register(IPluginRegistry)IPluginRegistry,RegisterSequencerEntity<T>(category)
GET /api/pluginslists what loaded + the entity discriminators each plugin contributed- Sample plugin in
samples/sample-plugin/with build + drop-in instructions
Random pixel-offset between frames to defeat fixed-pattern noise:
- Calls PHD2
ditherRPC after every N successfully captured frames - Waits for SettleDone event before next exposure
- Configurable dither pixels, every-N-frames, RA-only toggle, settle parameters
- Silent skip with debug log when PHD2 isn't connected or guiding, sequence never aborts
Bundled deep sky catalog with ~14,500 objects indexed by SQLite
- R*tree (
wwwroot/catalogs/dso/dso.db, ~2.6 MB committed to the repo). Covers:
- Complete NGC (~7570) + IC (~5000)
- Complete Messier (107) + Caldwell (104 of 109 NGC/IC-backed entries)
- Arp Atlas of Peculiar Galaxies (592)
- Sharpless 2 HII Regions (313)
- Hickson Compact Groups (100)
- Abell-Corwin-Olowin galaxy clusters (767, brightest 30% of the Abell 1989 set)
See docs/user-guide/sky-explorer.md
for the full per-catalog license + attribution table. Rebuild from
the original sources with python scripts/build-dso-catalog.py
(stdlib-only, no external Python deps).
- Search by designation, common name, or cross-ref alias ("NGC 7331", "Andromeda", "M31", "Arp 273", "Sh2-279", "HCG 92")
- Filtered browser with catalog / type / constellation / magnitude range / declination range, sorted brightest first
- Cone search endpoint
GET /api/sky/catalog/near?ra=&dec=&radius=for "what's inside my FOV" + future mosaic auto-suggest - Altitude chart, target altitude across tonight's window (sunset → sunrise) with civil / nautical / astronomical twilight
- Object metadata: coordinates (J2000), magnitude, size in arcmin, type, common names, constellation, source catalog
Embedded sandboxed sky viewer for visual target selection, powered by
stellarium-web-engine
running as a WebGL2 iframe sub-app under /sky/:
- Gaia stars to ~mag 16, DSO surveys with image overlays, IAU constellation art + names in multiple cultures, atmosphere + horizon, sun + moon + planets + bright asteroids, HiPS Milky Way tiles
- Fully offline when the skydata bundle is present (~300 MB, bundled
with
dotnet publishby default) - Camera-FOV overlay calculated from sensor + focal length (cos(Dec)-corrected). Mount rectangle (blue, anchored on current scope pointing) + target rectangle (red dashed, anchored at viewport centre, ASIAIR-style drag-to-frame)
- Click-to-pick targets, "Center on mount" + "Center selected" buttons
- Stellarium Remote Control sync, pull the currently-selected object from Stellarium with one click
WebGL2 required. The SKY tab gracefully degrades to a banner on hosts without WebGL2 (e.g. running the local browser on a Pi 2 framebuffer), open Polaris from a modern desktop or tablet browser instead.
Astronomy-specific cloud / seeing / transparency forecast for tonight and the next two nights:
- Source, 7Timer ASTRO API (free, no API key, 3-day window in 3-hour slots).
- Per-slot observation score (0-100) combining cloud cover, seeing, transparency and humidity, zero on precipitation. Colour-coded chip per slot (green ≥ 70 / amber 40-69 / red < 40).
- Tonight's best windows callout, top three continuous runs of high-score slots between sunset and sunrise, ranked by total duration × average score.
- Per-day moon phase + illumination alongside sunrise / sunset / astronomical twilight times (computed locally via SunCalc).
- Weather emoji per slot (☀ / 🌤 / ☁ / 🌧 / 🌨 / 🌫) with a moon glyph during the night hours.
- Server-side cache (15 min TTL) so multiple browsers on the same LAN share one upstream fetch. Falls back to a clear "Forecast unavailable" message when offline.
Ranked list of objects worth observing right now from the observer's location:
- Categories: deep-sky catalog objects (peak altitude ≥ 30°), the Moon, planets (Mercury through Neptune, peak altitude ≥ 10°), and a curated set of bright periodic comets.
- Score combines magnitude and altitude, balanced across categories so the Moon and bright planets don't push everything else off the list.
- Each card shows a thumbnail (NASA Image Library with Wikipedia fallback, cached on disk), name and common name, RA / Dec, magnitude, angular size, current and peak altitude, a 12 h altitude chart, and a compass arrow on the current azimuth.
- Fits FOV badge, when a camera is connected, each candidate is measured against the active rig's field of view (focal length + sensor) and flagged ✓ Fits / ⊘ Larger, with a chip filter to show only what fits.
- Go to, when a mount is connected, one click jumps to the Sky tab, centres the map on the target and kicks off Slew & Center (slew + plate-solve + re-centre).
- Image prefetch in Settings pulls thumbnails for the full catalog + Moon + planets + comets to disk so the panel stays usable at offline observing sites.
The FILES tab is the unified workbench for everything that lives on disk. The same file browser sits on the left; the right side switches between two sub-tabs:
- Stack -- batch post-processing (master frames, calibration, integration, channel combine, color calibration, Siril). Slot-based UX: multi-select lights in the browser, click Add to Lights on the Stack slot card; repeat for darks / flats / biases; click the action button.
- Edit -- single-frame Lightroom-style editor with sliders, AI cleanup (GraXpert), crop and export.
A Show FITS metadata toggle in the browser toolbar adds Type / Filter / Target / Exposure columns to the listing (powered by the SQLite frame library cache; sub-100 ms even with hundreds of rows in view). The setting is remembered across sessions.
See docs/user-guide/files.md for the full walkthrough.
The technical details below describe the Stack pipeline that powers the unified panel.
Files are auto-organised under {ImageOutputDir}/{rig}/...:
{rig}/lights/{target}/{filter}/{session}/light_*.fits
{rig}/calibration/dark/{exp}s_g{gain}/dark_*.fits
{rig}/calibration/bias/g{gain}/bias_*.fits
{rig}/calibration/flat/{filter}_g{gain}/flat_*.fits
{rig}/calibration/darkflat/{exp}s_g{gain}/darkflat_*.fits
{rig}/calibration/masters/master_*.fits
{rig}/calibrated/{target}/{filter}/cal_*.fits
{rig}/integrated/{target}/{filter}/master_light_*.fits
{rig}/processed/{target}/*.{fits,tif,png,jpg}
Session date follows the astronomical noon-to-noon convention, a capture at 02:30 local time still belongs to the previous evening.
Frame browser:
- SQLite-backed metadata index, header-only FITS scan keeps a 2000-frame session re-walkable in under a second.
- Filter by type / filter / target / date range; thumbnail grid with auto-stretched 256 px JPEGs generated on demand and cached on disk.
- Multi-select with status-bar counts that drive the batch operations.
Single-frame viewer:
- Double-click any thumbnail to open the fullscreen viewer (OpenSeadragon).
- Black / midtone / white sliders with auto-stretch defaults (MTF) and live debounced preview.
- Star annotation overlay, log-scale histogram (Chart.js), full statistics table.
- Export to 16-bit linear TIFF (preserves dynamic range for downstream PixInsight / Siril), 8-bit stretched PNG, or JPEG.
Master calibration frames (bias / dark / flat / dark-flat):
- Select ≥ 2 raw calibration frames → "Create master from selection" → choose integration method (Mean / Median / Sigma-clipped mean default, 3σ low and high, 2 iterations).
- Background job with progress bar; output carries
NSUBS,INTMETH,IMAGETYP=MASTER{TYPE}headers. - Cross-frame dimension validation guards against mixed inputs.
Light-frame calibration:
- Select N raw lights → "Calibrate lights" → backend applies
(light − dark) / normalised_flatand writes the result with aCALSTATheader (B / D / F letters per SBIG convention) plusMDARK/MFLAT/MBIASfilenames for traceability. - Auto-match per light, for each light, picks the master with the same gain and closest exposure (darks), same gain and filter (flats), or same gain (bias). Manual override per dropdown is available.
- Bias is only applied when no dark is provided (darks already contain the bias signal). Dark-flats are preferred over bias as the flat calibrator.
- Per-light failures don't abort the batch; the job reports OK / failed counts.
Batch stack (offline alignment + integration):
- "Integrate (stack)" detects stars in each frame, aligns by affine star-matching against the reference (the frame with the most stars), resamples to the reference's coordinate system and reduces per-pixel with the chosen method.
- Frames that fail to align are skipped and reported as dropped.
- Output carries
NCOMBINE,EXPTOTAL,INTMETH,REJECT,STACKREFheaders andIMAGETYP=MASTERLIGHT.
Per-frame operations (in the viewer):
- 🎨 Debayer, bilinear demosaic of RGGB / GRBG / GBRG / BGGR; the
output is a Rec.601 luminance plane with
DEBAYER/BAYERINheaders. - ◐ Remove gradient, sample-grid (8 × 6 default) median with
MAD-based stellar rejection, 2D polynomial (degree 2) least-squares
fit, subtracted relative to the fitted minimum so global brightness
survives.
BGSUB/BGSAMPX/BGSAMPY/BGDEGheaders. - ⌇ Noise reduce, separable Gaussian blur.
NRMETHOD/NRRADIUSheaders. - ✦ Sharpen, unsharp mask with optional threshold guard for the
noise floor.
SHARPEN/SHARPAMT/SHARPRAD/SHARPTHRheaders.
Each operation writes a new FITS under {rig}/processed/{target}/ and
auto-refreshes the library.
Polaris drives two external CLIs when they're installed on the host machine: Siril for preprocessing + stacking, and GraXpert for AI-based background extraction, deconvolution, and denoising.
Detection happens automatically on startup, the Settings tab's External tools section shows the detected version and binary path (or "Not detected" with install hints).
Siril (siril.org) becomes the preferred
stacking engine the moment it's detected. The STUDIO tab gains a
⚡ Stack with Siril button that runs your chosen .ssf
script against the selected frames. Polaris ships 9 curated
preprocessing scripts (OSC + Mono × the with/without dark/flat/DBF
matrix, plus OSC narrowband extraction), and also picks up your
personal scripts from the standard Siril scripts dir so anything
you wrote works the same way. See
docs/siril-setup.md.
GraXpert (graxpert.com) offers three operations:
- 🌅 BGE (background extraction), removes gradients.
- ✨ Deconvolution (v3.0+), sharpens a stacked master.
- 🔇 Denoise (v3.0+), AI noise reduction on the master.
You can run GraXpert in three ways:
- Manual batch, multi-select frames in FILES, click the op button, tune the sliders, hit Start.
- Auto during capture, tick "Auto-extract gradient with GraXpert (per frame)" in the AUTORUN End Events panel. Every saved light fires a fire-and-forget BGE in the background. Designed for heavy-light-pollution sites where each frame has its own gradient.
- Combined with Siril, in the STUDIO Siril modal, tick
"Inject GraXpert BGE per-frame before stacking" to chain the
two: GraXpert cleans each light first, then Siril stacks the
_bgeoutputs. Slower but produces a much cleaner master.
Decon + Denoise are manual-only on integrated masters, running them per-frame degrades SNR. See docs/graxpert-setup.md.
Outputs land in dedicated per-tool folders so you can tell what
came from where: {rig}/siril/{target}/, {rig}/bge/{target}/,
and {rig}/decon|denoise/{target}/.
When Siril / GraXpert isn't installed, the built-in C# pipeline (MasterFrameService + CalibrationService + BatchStackingService) remains available as a fallback so users without the externals still get a working stacking workflow.
GraXpert ships its background extraction, deconvolution, and denoise models as ONNX files. Polaris hosts them and runs them directly in the client browser via WebGPU + ONNX Runtime Web, not on the Pi. That changes the perf math completely:
- Server (Pi 4 / 5) load: zero CPU during inference. Polaris
just serves the
.onnxfiles (cached locally per-client in IndexedDB) and the raw FITS pixels. - Where the GPU work happens: your laptop, desktop, tablet, or phone that's connected to the Polaris UI. Modern integrated graphics (Intel Iris Xe, Apple M-series, AMD RDNA, NVIDIA) all expose WebGPU and crunch the GraXpert tile-based pipeline in a fraction of the time the Pi 5 CPU would take.
- Fallback: when WebGPU is unavailable (older browsers, no
GPU surfaced), ONNX Runtime Web auto-falls back to **WASM SIMD
- multi-threading**. Slower than WebGPU but still entirely client-side.
Typical end-to-end timing for a 6000×4000 RGB master, BGE + Denoise + Decon-Stars pass:
| Client | Pipeline time |
|---|---|
| Apple M1 Pro (WebGPU) | 8-12 s |
| Intel Iris Xe / RTX 3060 (WebGPU) | 10-15 s |
| iPad Pro M2 (WebGPU) | 15-20 s |
| Older laptop, WASM SIMD fallback | 60-90 s |
| Pi 5 running GraXpert CLI on the host | 4-8 min |
Same FITS, same output quality (the math is identical, just on different hardware). For users running Polaris on a Pi but opening the UI from a decent laptop, in-browser AI is 30-50x faster than the CLI on the Pi itself.
In the EDITOR tab the AI section has BGE / Denoise / Decon buttons that route to the in-browser pipeline by default. The existing CLI path is still there (Settings toggle) for cases where a strong server CPU + weak client GPU flip the math.
The same EDITOR tab has an ✨ Auto button at the top of the sliders column that computes a Lightroom-style starting point (Exposure, Contrast, Highlights, Shadows, Whites, Blacks + Vibrance on RGB) from the histogram of the loaded master. ↺ Reset sits next to it. Both are non-destructive and undoable. See docs/user-guide/editor.md#auto-adjust for the heuristic details.
WebGPU on LAN requires HTTPS, which Polaris auto-configures via a self-signed cert on port 5000. See docs/user-guide/onnx-inference.md for the full breakdown + browser compatibility matrix, and docs/user-guide/https-setup.md for the one-time per-device cert trust step.
The Files tab is a full server-side file explorer. Browse the device that runs Polaris, including USB sticks and external SSDs, without dropping into an SSH session.
- Roots, Windows drive letters (
C:,D:, …) or Unix mount points (/,/home,/mnt,/media,~). Free-space and volume label shown per root. - Navigation, clickable breadcrumbs, parent shortcut, "show hidden" toggle, persistent cwd across reloads.
- Selection, plain click selects one, ctrl/cmd-click toggles, shift-click selects a range. The header checkbox selects all.
- Operations, new folder, rename, cut, copy, paste, delete. Cut + paste across volumes falls back to copy-then-delete automatically.
- Preview, FITS / XISF render via the same auto-stretch as
Studio (JPEG). PNG / JPG / GIF / BMP / WebP pass through. TIFF
decoded via SkiaSharp.
.txt/.log/.json/.md/.xml/.csvopen in an inline text viewer (first ~32 KB). - Download, single file is a direct browser download with the correct filename; multi-select streams a ZIP archive built on the fly, so dragging 50 × 60 MB FITS onto your laptop doesn't OOM the Raspberry Pi.
- Studio root, select a folder and click ⭐ Set as Studio root. Studio rescans this tree on its next visit. The Settings tab no longer carries the directory input; it just shows the current value and links here.
Safety: every destructive operation prompts with window.confirm
and the server requires confirmed=true on the /delete endpoint,
nothing gets wiped by an accidental double-click. A path blocklist
refuses access to C:\Windows, /proc, /sys, /dev, /etc/shadow,
/etc/ssh, and per-segment matches for .ssh, .aws, .gnupg, and
.config/gh at any depth. Every file operation logs at Information
level with EventId="FileOp".
The Polaris server has no authentication on the LAN. The Files tab exposes the filesystem to anyone who can reach the server address. Polaris assumes a trusted local network, do not expose port 5000 directly to the internet. For remote access use the Relay (which has per-tenant tokens and quotas).
Target list execution with automated imaging:
- Multi-target sequences with per-target exposure, gain, binning, filter, and frame count
- Automatic slew-to-target before capture
- Pause/resume with SemaphoreSlim gate
- Real-time progress tracking via WebSocket (1Hz status broadcast)
- Persisted output in two formats, selectable per profile:
- FITS, extended headers (camera / telescope / focuser / rotator / filter / weather / observer / target, 30+ keywords per the N.I.N.A. manual spec)
- XISF (PixInsight native), UInt16 monochrome with optional LZ4 compression (~3-10× smaller than FITS); native
<Property>elements alongside<FITSKeyword>mirrors so any downstream tool works
- Configurable filename pattern (
{target}_{filter}_{exposure}s_{date}_{seq}etc.) - Optional dithering between frames + automatic meridian flip
- Focal length / focal ratio come from the active equipment rig (see below), not a global setting
Automated flat-field acquisition with a full UI panel inside AUTORUN → Flat Wizard sub-tab (tabstrip "Sequence | Flat Wizard" at the top of the AUTORUN tab). See docs/user-guide/flat-wizard.md for the full walkthrough.
- Binary search on exposure time per filter until median ADU lands within tolerance of target (default 30000 ADU ± 5%)
- Captures N flat frames at the converged exposure, tagged
IMAGETYP=FLAT - Per-(filter, binning) trained exposures persisted to
trained-flats.jsonfor next session - Per-rig settings on
EquipmentProfile.FlatWizard(TargetADU / tolerance / frames per filter / min-max exposure / binning / max iterations / panel brightness) so a cold APO at f/5 and a warm SCT at f/10 each keep their own defaults - Pre-flight panel surfaces camera / filter wheel / flat panel status. Filter chips let you pick which filters to capture (in the order picked). Optional brightness slider auto-applies to the connected flat panel before the wizard runs (0 = sky / T-shirt flats, panel is left alone)
- Polaris Shutter component for start / abort, real-time progress
ring showing
(filtersDone + frames-in-current-filter) / totalFilters, live readout of the binary search ("Searching attempt 4: 2.137s → 28473 ADU") followed by capture counter ("Capturing 8/20 at 2.137s") - Trained exposures table shows cached per-(filter, binning) seeds — next run converges in 1-2 iterations instead of 5-8
Responsive, dark-themed interface inspired by ASIAIR:
- Home, Cold-start landing with a Horsehead/Flame nebula hero. 4 colour-coded status cards (Equipment / Guider / Sequence / Server) react in real time to the rest of the app, plus 6 quick-action tiles that jump straight into the relevant tab (Connect / Plan / Launch PHD2 / Build sequence / Auto-focus / Live view). Live UTC clock in the hero
- Live View, Real-time camera preview with WebGL2 GPU rendering (debayer + MTF stretch on GPU), star annotations overlay, crosshair + 3x3 grid, hover pixel readout (raw ADU or RGB), manual stretch sliders, image-history thumbnail strip, HFR + star-count history chart, detailed statistics panel + histogram, full-resolution zoom viewer (OpenSeadragon)
- Preview, Dedicated snap-shot tab (between Focus and Autorun) with exp/gain/bin/filter controls, single-shot + opt-in loop mode, "💾 Save to disk" toggle that routes captures to
{rig}/snaps/{filter}_{date}/(separate from sequence lights so test snaps don't contaminate the science folder). Filter dropdown only appears when a filter wheel is connected; pre-capture filter swap happens automatically when chosen - RIGS (was "Equipment"), Reorganised by role: connection strip pinned to the top (compact when INDI/Alpaca is connected, expanded with the connect form when not), then a responsive role-based grid, Main Telescope (OTA optics + curated catalogue picker that auto-fills aperture / focal length / f-ratio / required back-focus), Camera (with auto-detected sensor + temperature + cooler power chart + driver dropdown for INDI / Canon EDSDK / Nikon / Sony / Alpaca with install-banner links when a vendor SDK isn't reachable), Mount, Focuser, Filter Wheel, Guidescope (metadata), Guide Camera (read-only, PHD2 owns it). Optional accessories (Rotator, Flat Panel, Dome, Weather) collapsed in a
<details>block below, auto-expanded when at least one is configured. Rig selector + "💾 Save selections" in the header. "Manage rigs…" modal handles rename / activate / duplicate / delete + per-rig filter offsets (the device pickers + optics live on the cards now, no longer duplicated in the modal) - Mount Control, NSEW directional pad, tracking toggle, park/unpark, GoTo via Sky Explorer
- Focus, Manual stepper + full Auto-Focus V-curve panel (start/abort, live progress bar, fitted parabola chart, best-position marker) + live frame preview canvas that renders each AF sample exposure with a HUD chip showing current position / HFR / star count
- Guider, Two-tab layout: Control (existing JSON-RPC UI, connect, profile switcher, exposure, Dec-mode, equipment connect, guiding controls, live RA/Dec error chart, settle parameters, Smart Calibrate button with optional slew-to-equator, algorithm preset pills Default/Reactive/Smooth/Custom + Advanced disclosure for individual algorithm knobs, profile-sync indicator chip) and PHD2 GUI (Linux only, xpra HTML5 client iframe embedding PHD2's native window for Wizard / Brain / Guiding Assistant / dark library access, see docs/phd2-gui-embedding.md). Launch / Shutdown / Auto-start on boot persist as before
- Sky Explorer, stellarium-web-engine WebGL2 iframe (sandboxed sub-app at
/sky/) with Gaia stars to ~mag 16, DSO surveys with image overlays, IAU constellation art, atmosphere/horizon, sun + moon + planets + bright asteroids, and HiPS Milky Way tiles. Fully offline when the bundled skydata is present. Drag-to-frame ASIAIR-style target rectangle + blue mount rectangle (auto cos(δ) correction). Object search, filtered catalog browser, "Tonight's altitude" chart with twilight bands, Stellarium sync, Slew & Center, "Plan mosaic" (panel grid pushed to the engine as yellow tile overlays), Add to Sequence. WebGL2 required. - Tonight, Ranked list of best DSOs / Moon / planets / comets for the current observing window. Cards with NASA / Wikipedia thumbnails (offline-cached), live ephemeris, mini altitude chart, compass widget, FOV-fit badge, and a mount-gated "Go to" that triggers Slew & Center
- Weather, Astronomy-specific 3-day forecast from 7Timer with per-3 h-slot observation score (cloud + seeing + transparency + humidity), tonight's best windows callout, per-slot weather emoji (lunar glyph at night) and per-day sun/moon ephemeris from SunCalc
- Sequence, Target list editor with progress bars, collapsible Meridian Flip + Dithering panels, start/pause/resume/stop
- Adv, Advanced Sequencer tri-pane tree editor (palette / tree / properties) with drag-reorder, live status colouring during runs, load/save/import/export JSON, template management
- Studio, Post-processing tab: SQLite-indexed FITS browser, single-frame viewer with manual stretch + multi-format export, master integration, light calibration with auto-match, batch alignment + stack, debayer + background extraction + noise reduction + sharpening
- Settings, INDI connection, observatory location (with address geocoder + "Use my location" GPS button, accessible any time after first-run), image output (format: FITS or XISF), profile management. Sensor dimensions are auto-read from the connected camera; focal length lives per-rig (Equipment → Manage rigs)
- First-run, Location-setup modal with address geocoder (OpenStreetMap/Nominatim), browser geolocation, or manual lat/lon entry
Activity Bar, App-wide footer (36 px, glass treatment) showing live operation chips (sequence progress, AF run, meridian flip, slew, exposure, filter change, PHD2 calibrating/settling, live stack, Siril/GraXpert jobs) on the left + host CPU% + RAM (green < 60% / amber 60-85% / red > 85%) on the right. Always visible across every tab; collapses chip-row to nothing when idle.
Night Mode, Red-on-black theme that preserves dark adaptation (critical for field use).
Mobile Responsive, Full functionality on phones and tablets with bottom tab navigation.
One physical N.I.N.A. Polaris host frequently serves multiple physical setups, "backyard SCT", "travel APO", "remote site mono camera + AO". Each user profile carries a list of named rigs; switch in one click and every device selector + per-rig default (cooler temperature, focuser step size, focal length, PHD2 host/port, PHD2 algorithm preset) is reapplied automatically.
Per-rig stored data:
- Device names: Camera / Telescope / Focuser / FilterWheel / Rotator / FlatDevice / Dome / Weather (INDI names as returned by getProperties)
- Cooler target temperature, default gain / offset / binning
- Focuser step size + backlash
- Main scope focal length + aperture (drives FOV calculation + FITS FOCALLEN; auto-fillable from the catalogue picker)
- Guide scope focal length + aperture (record-keeping + PHD2 pixel-scale sanity check)
- Telescope brand + model + accessory (auto-resolved from
wwwroot/data/telescopes.json+optical-accessories.jsonvia the Main Telescope card's catalogue picker) - PHD2 endpoint (host + port)
- PHD2 deep-integration fields:
PHD2ProfileIdcache after first name-match,PHD2AlgoPreset(Default / Reactive / Smooth / Custom),PHD2CalibrationStepMsOverride,PHD2AutoSyncOnRigSwitch(default true, triggersPHD2ProfileSyncServiceto swap PHD2 profile + apply preset on every rig activation),PHD2CustomAlgoParams(free-formaxis:name → valuebag) - Per-filter focuser offsets (consumed by
MoveToFilterOffsetInstruction)
CRUD via /api/equipment/rigs/*. UI: dropdown in the RIGS tab header
plus a slim Manage rigs… modal for rename / activate / duplicate / delete
- filter offsets (the device pickers + optics live directly on the RIGS-tab cards, no longer duplicated in the modal).
Existing profiles auto-migrate on first load, the pre-existing
LastCamera / LastTelescope / etc. fields become the rig named "Default".
Curated catalogue of popular astro OTAs + reducers / Barlows / flatteners that drives auto-fill of the rig's optical fields. Pick brand → model → optional accessory and Polaris computes the aperture, native + effective focal length, f-ratio, and the required camera-side back-focus.
- ~80 telescopes spanning Askar (FRA + PHQ + APO lines),
Astro-Physics, Celestron (C-series + EdgeHD + RASA), Explore
Scientific, GSO RC + Newtonian, Meade LX200, Sharpstar EDPH,
Skywatcher (Esprit + Evostar + Quattro + Skymax), SVBony (SV48P
- SV503 + SV550 + SV535 + SV545 + SV555), Takahashi (FSQ + TOA
- Epsilon), Tele Vue, Vixen, William Optics (RedCat + ZenithStar
- GT + FLT).
- ~25 optical accessories, Celestron 0.7× EdgeHD + f/6.3 SCT
reducers, Starizona Hyperstar 8/11, Skywatcher 0.85× Esprit,
Askar / Sharpstar / SVBony / Takahashi dedicated reducers, WO
Flat 6A III flatteners, Tele Vue Powermate 2/2.5/4/5×, generic
1.6/2/3× Barlows.
compatibleScopesfilters the dropdown to entries that fit the picked OTA; empty list means generic. - Effective focal length = native × accessory factor (rounded). Back-focus reminder surfaces in amber, wrong backspacing is the most common cause of elongated stars in the corners of a flatener shot.
- The picker auto-fills, then writes the resolved values into the rig, the catalogue can change later without breaking saved rigs. Off-catalogue scopes still work via the manual focal length / aperture inputs.
- Catalogues live in
wwwroot/data/telescopes.json+wwwroot/data/optical-accessories.json. PRs welcome to extend the lists.
JSON-based settings persistence with multi-profile support:
- Observatory location
- INDI connection settings
- Plate solver configuration (primary, blind fallback, paths, API keys)
- Image output directory, naming pattern, format (FITS / XISF)
- List of equipment rigs (see above)
- Save, load, and rename profiles
- Factory reset (Settings → "Reset everything to factory defaults"): wipes every profile, rig, the app password and login sessions, observer location, and the browser's UI preferences, then reloads into first-run state. Captured images / FITS are left untouched. Use it before imaging a clean SD card for distribution so none of your own (or test) config ships.
For accessing a N.I.N.A. Polaris host on a remote LAN (observatory site, friend's house) without inbound port-forwarding or dynamic DNS, this repo ships a companion NINA.Relay.Server project that acts as a reverse tunnel.
Browser ──HTTPS──► relay.example.com ──reverse WebSocket──► Raspberry Pi
(NINA.Relay.Server) (N.I.N.A. Polaris,
no public IP)
- N.I.N.A. Polaris opens an outbound WebSocket to the relay (firewall-friendly)
- Multiplexed binary protocol: many concurrent HTTP requests on one socket
- Auto-reconnect on the client side with exponential backoff (2s → 60s)
- Subdomain routing (
alice.relay.example.com) OR path-prefix (/t/alice/...) - Multi-tenant: each headless host has its own bearer token
- WebSocket-over-tunnel forwarding, image stream + status broadcasts + any other browser-side WS endpoint now work end-to-end through the relay (browser ↔ relay ↔ tunnel client ↔ local Kestrel WS, bidirectional pump)
- JSON tenant store (
tenants.json), hot-reloaded on file change; no server restart needed when adding or revoking tokens (legacyappsettings.jsonTenants:section still works for trivial setups) - Per-tenant rate limiting, token-bucket on both HTTP requests/sec and
bytes/sec (request + response counted together), with configurable burst.
Exceeding either bucket returns HTTP 429 with a
Retry-Afterheader naming which bucket tripped - Monthly byte quotas + expiring tokens, per-tenant
monthlyBytescap (counter persists across restarts intenant-state.json, auto-resets on the 1st UTC, HTTP 402 when exhausted) andexpiresAttimestamp (auth refused after expiry, useful for trials) - Built-in TLS,
Tls:Mode=letsencrypt(LettuceEncrypt fetches + renews certs from Let's Encrypt automatically) orTls:Mode=pfx(load a static.pfx). No reverse proxy required - Web admin UI at
/admin/, gated byAdmin:Password(HTTP Basic); add/edit/delete tenants, view live tunnels + monthly usage bars, generate cryptographically-random tokens, reset usage counters, browse the audit log with per-tenant filter - Per-request audit log (JSON-lines
audit.log), timestamp, tenant, method, path, status, bytes in/out, duration, source IP, outcome reason. Auto-rotates at 50 MB. In-memory ring buffer surfaced via/_admin/audit?tenant=&limit=and the admin UI - mTLS for tunnel auth, per-tenant
clientCertThumbprintpins the X.509 cert the tunnel client must present. Bearer token alone is the default; mTLS is opt-in per tenant. N.I.N.A. Polaris client points at a.pfxviaRelay:ClientCertPath(+ optional password) - Admin endpoints:
/_health,/_tunnels(with per-tunnel byte counters),/_admin/tenants(full CRUD),/_admin/generate-token,/_admin/usage/{token}/reset,/_admin/audit,/_admin/reload-tenants - Two deployment models, self-host on a $5 VPS, or use a hosted instance
- See
src/NINA.Relay.Server/README.mdfor deployment instructions, Caddy reverse-proxy example, fulltenants.jsonschema, and protocol details
A ? icon at the end of the sidebar opens a HELP tab with four
guided paths:
- First night (5 steps): browser + cert, password wizard, location, WiFi mode, first device connect. The "I just installed Polaris" checklist.
- Capture to export (12 steps): connect → polar align → focus → pick target → slew & center → start guiding → build sequence → live stack → calibrate + integrate → AI cleanup → edit → export. The main end-to-end workflow.
- Specific workflows: LRGB / mono pipeline, planetary lucky imaging, photometric color calibration. Three short steppers for the variant paths.
- Troubleshooting & FAQ: collapsible Q+A for the most common "I can't reach Polaris" / "plate solve fails" / "GraXpert model not found" / etc, each pointing into the deeper docs.
Each step has a hero screenshot slot + 2-4 paragraph body +
optional "Open this tab" button + optional "Read more" link
into the docs. Screenshots live under
wwwroot/screenshots/{topic}/{NN}-{slug}.png; missing ones
render a placeholder card printing the expected path, so the
tutorials work end-to-end before every screenshot is in place.
Tutorial position persists per browser via localStorage, jump
into another tab and come back to resume.
Password gate for the local HTTP API + WebSockets + embedded sub-apps. Default ON, no default password: first browser hit shows a full-screen wizard that forces the operator to pick a password before any other tab loads. Subsequent devices get a Sign in overlay with optional "remember on this device" checkbox.
- PBKDF2-SHA256 (100k iterations, 16-byte random salt) hash stored on the active profile. 32-byte random session tokens with sliding 24h expiration. Per-IP rate limit: 5 failed attempts / minute then exponential backoff capped at 1h.
- Loopback bypass (127.0.0.1 / ::1) for SSH tunnels + local scripts, same convention as Jupyter / Grafana / RStudio.
- Gates
/api/*(except/api/auth/*and/api/system/version),/ws/*,/phd2-gui/*,/indi-web/*,/sky/*. Static assets (login page, JS, CSS, images) stay open so the UI itself can load. - Token transport via
Authorization: Bearer,?token=query string, or HttpOnlypolaris_sessioncookie (browser auto-attaches to same-origin XHR, fetch, WS upgrades, iframe loads). Bearer is primary; the cookie covers iframes + bare fetches without per-site refactors. - Opt-out toggle in Settings → Authentication (requires current password) for closed/trusted LANs.
- Recovery: edit
~/.config/NINA.Polaris/profiles/active.json, clearAuthPasswordHash+AuthPasswordSalt, restart the service; first-run wizard kicks in again.
See docs/user-guide/authentication.md for the full walkthrough.
Every server log, HTTP request, browser exception and toast lands in a single in-memory ring buffer (5000 entries). The LOG badge in the header turns amber on unread warnings and red on unread errors; clicking opens a fullscreen panel with filters by level / source / search, exportable as JSONL or TXT to attach to bug reports. Sensitivity filter strips passwords, tokens, Bearer headers, and session cookies before any entry hits the buffer.
Opt-in disk persistence (Settings → Debug logging) flushes
batched entries to JSONL files under {LocalAppData}/NINA.Polaris/logs/
with 7-day retention -- useful for chasing intermittent issues
across server restarts.
See docs/user-guide/debug-logging.md for screenshots, the exact export format, and how to attach a log file to a bug report.
Built for unreliable field WiFi:
- WebSocket auto-reconnect with exponential backoff + jitter
- Request deduplication for in-flight API calls
- Server reachability detection with automatic recovery
- Toast notifications for connection state changes
- 15-second request timeout with abort controller
- Adaptive bandwidth, server measures actual WebSocket send latency and auto-downgrades raw clients to JPEG when bandwidth degrades, upgrades back when it recovers
- mDNS announcer, host reachable on the LAN (no IP needed). Each device auto-names itself
polaris-app-XXXX, whereXXXXis derived from a stable hardware id (Raspberry Pi serial, else MAC) -- so you can clone one SD-card image onto several Pis without mDNS name collisions; each Pi self-names. Set a human-friendly label in Settings → Device name (advertised in the TXT record + shown in the mobile app's discovery list). Override the instance name entirely viaMdns:InstanceNameinappsettings.jsonif you prefer a fixed name. Note: phones can't resolve.localnames, so the mobile app connects via the IP that discovery returns. - Alpaca (ASCOM HTTP) support, UDP discovery on port 32227 plus base Camera / Telescope wrappers, so you can drive Windows-only ASCOM drivers exposed over the network
ASIAIR-style WiFi management built into the .deb. The Pi comes up
as a hotspot named Polaris-Hotspot (password polaris1234) on
first boot, so you can reach https://polaris-pi.local:5000 from
a phone in the field without ever plugging in a monitor. From the
Settings → Network panel you switch the Pi onto your home WiFi at
home, with a 30 s try-and-revert safety net that pops the hotspot
back if the new credentials fail. Linux + NetworkManager only
(Pi OS Bookworm default). See
docs/user-guide/network-mode.md.
Embedded xterm.js + SSH.NET bridge under SETTINGS → Remote terminal.
Opens an interactive shell against any LAN host (or localhost for the
Polaris host itself), so you can restart indiserver, tail logs, or
sudo systemctl status something on a headless Pi without plugging in a
screen.
- Off by default, set
Terminal:Enabled = trueinappsettings.jsonto expose the/ws/terminalendpoint - No auto-login, credentials are entered per session and never persisted
- 10-minute idle timeout closes abandoned sessions server-side
- Resizes with the panel; supports
vim,htop,tmux, colours, scrollback
See docs/user-guide/remote-terminal.md.
On Linux .deb installs (Raspberry Pi / SBC / PC via apt), Polaris updates
itself from GitHub Releases with one click:
- A green ⬆ Update badge appears in the top status bar when a newer
release exists for the host's architecture (
arm64/armhf/amd64); the check runs at startup and hourly (cached 30 min) - The modal shows release notes + the matched asset; Download & install
fetches the architecture-matched
.deb(URL resolved server-side) and installs it, then the browser polls and reloads on the new version - No sudo password — the install runs via an on-demand systemd unit the
polarisuser is authorized to start through a scoped PolicyKit rule, in its own cgroup so it survives the service restart - Only the first release with the updater needs a manual
sudo apt install ./polaris_<arch>.deb; after that, updates are one click
See docs/user-guide/self-update.md.
The POLAR sidebar tab hosts two complementary alignment workflows:
TPPA (Three-Point Polar Alignment) — the default. Slews the mount to three configurable RA positions, plate-solves at each, computes the mount axis vs the true celestial pole, and reports azimuth + altitude errors in arcminutes. Hemisphere-aware (works in both N + S latitudes).
- Refine mode loops capture + solve in real time so you watch the error vector shrink as you adjust the tripod knobs (SharpCap-style UX, red → amber → green overlay on the live frame)
- Per-rig parameters (slew step, exposure, settle, gain) saved in the active equipment profile
Rudimentary (single-target iterative) — alternative for setups where TPPA isn't viable: balconies / blocked polar regions / manual mounts / quick first-night sanity check. Pick a bright visible target, optionally slew, capture + solve once, see the pointing error, nudge the mount, click Re-capture + solve, repeat until satisfied. Inline sparkline tracks convergence; embedded sky map shows target (green) vs solved (red) markers with the error vector between them. See docs/user-guide/polar-alignment-rudimentary.md.
nina-polaris/
├── src/
│ ├── NINA.Polaris/ ← ASP.NET Core app (Kestrel, Minimal API)
│ │ ├── Program.cs ← Host builder, service registration
│ │ ├── Endpoints/ ← REST API (13 endpoint groups)
│ │ ├── WebSocket/ ← Image stream + status broadcast
│ │ ├── Services/ ← Business logic layer
│ │ └── wwwroot/ ← Web UI (HTML, JS, CSS)
│ │
│ ├── NINA.Core.Portable/ ← Shared enums, models, utilities (net10.0)
│ ├── NINA.Image.Portable/ ← Image processing, FITS I/O, statistics (net10.0)
│ └── NINA.INDI/ ← INDI protocol client (net10.0)
│ ├── Protocol/ ← XML parser/writer
│ ├── Client/ ← TCP client, blob receiver, connection manager
│ └── Devices/ ← 9 device type implementations
│
│ ├── NINA.Relay.Protocol/ ← Shared multiplexed frame format (net10.0)
│ └── NINA.Relay.Server/ ← Standalone reverse-tunnel relay (ASP.NET Core)
│
├── tests/
│ └── NINA.Polaris.Test/ ← 294 unit tests (NUnit)
│
├── deploy/ ← Deployment scripts
│ ├── nina-polaris.service ← systemd unit file
│ ├── install.sh ← Linux installer
│ ├── publish-linux-arm64.sh ← RPi build script
│ ├── publish-win-x64.ps1 ← Windows build script
│ └── docker-build.sh ← Multi-arch Docker buildx
│
├── Dockerfile ← Multi-stage, linux/amd64 + arm64
└── docker-compose.yml ← NINA + optional indiserver sidecar
| Layer | Technology | Purpose |
|---|---|---|
| Web server | Kestrel (standalone) | Native .NET, no nginx/IIS needed |
| API | ASP.NET Core Minimal API | Low overhead, AOT-friendly |
| Real-time (images) | WebSocket (binary) | JPEG or LZ4-compressed raw frames, adaptive |
| Real-time (status) | WebSocket (JSON) | Equipment + sequence + guider + AF + meridian flip at 1Hz |
| Frontend framework | Alpine.js v3 | Reactive UI (~15KB, no build step) |
| UI typeface | Inter (SIL OFL 1.1, self-hosted) | Variable woff2 for every weight + italic, ~740 KB total. No external CDN call, the UI looks the same online and offline |
| Charts | Chart.js v4 | Guiding, focus, HFR, temperature, histogram, altitude |
| Sky map | stellarium-web-engine (AGPLv3, sandboxed in /sky/ iframe) |
WebGL2 sky viewer with Gaia stars, DSO surveys, constellation art, atmosphere, HiPS Milky Way tiles |
| Image viewer | OpenSeadragon | Full-resolution zoom/pan over last frame |
| Image rendering | WebGL2 shaders | GPU debayer + MTF stretch (CPU fallback) |
| Image encoding | SkiaSharp | Cross-platform JPEG / PNG encoding (incl. STUDIO previews + thumbnails) |
| FITS I/O | Custom FITSWriter | Extended headers per N.I.N.A. manual spec |
| XISF I/O | Custom XISFWriter | PixInsight native, LZ4-compressed, FITSKeyword mirrored |
| TIFF export | Custom TiffWriter | Baseline uncompressed 8-bit / 16-bit grayscale (SkiaSharp doesn't ship TIFF) |
| STUDIO frame index | Microsoft.Data.Sqlite | On-disk metadata cache so 2000-frame sessions list in < 50 ms |
| Astronomy ephemeris | CosineKitty.AstronomyEngine | Planet positions for the Tonight's Best panel (MIT, ~150 KB, no native deps) |
| Sun / moon math | SunCalc (BSD-2, vendored) | Sunset / sunrise / twilight / moon phase for the Weather panel |
| Weather forecast | 7Timer ASTRO (HTTP, no key) | Cloud / seeing / transparency, 3-day window in 3 h slots |
| Compression | K4os.Compression.LZ4 | Fast image compression (~2GB/s) |
| Equipment drivers | INDI protocol (TCP/XML) + Alpaca (HTTP) | 400+ Linux drivers + ASCOM over network |
| Plate solving | ASTAP / PlateSolve3 / Astrometry.net (online + local) | Strategy dispatcher with primary + blind fallback |
| Guiding | PHD2 (TCP/JSON-RPC, port 4400), fully managed | Profile switch, equipment connect, process launch/shutdown |
| Remote access | NINA.Relay.Server reverse tunnel | Public access without inbound port-forwarding |
| Discovery | Makaretu.Dns.Multicast | mDNS announcer for nina.local |
| Geocoding | Nominatim (OpenStreetMap, proxied) | Address → coordinates for location setup |
| Stellarium sync | HTTP (Remote Control plugin, port 8090) | Pull selected object as target |
| Logging | Serilog | Structured logging to console + file |
| Target framework | .NET 10.0 | Latest LTS, cross-platform |
Minimum to build + run from source:
- .NET 10 SDK
- Git (with submodules: stellarium-web-engine is pulled at build time)
- On Linux for hardware control:
sudo apt install indi-full - Optional plate-solving: ASTAP + H17/H18 database
For the complete tooling matrix, Windows + Linux ARM (Raspberry Pi) + Linux x64, required vs optional per feature, firewall rules, hardware sizing, see REQUIREMENTS.md.
git clone https://github.com/DanWBR/nina-polaris.git
cd nina-polaris
dotnet build
dotnet run --project src/NINA.PolarisOpen http://localhost:5000 in your browser.
dotnet testThe .deb package (built automatically by GitHub Actions on every
tag push) handles user creation, systemd unit, indi-web venv, apt
dependencies, and self-signed HTTPS cert generation. End-user install:
wget https://github.com/DanWBR/NINA.Polaris/releases/latest/download/polaris_arm64.deb
sudo apt install ./polaris_arm64.deb
# 30 seconds later: Polaris running at https://<hostname>.local:5000The postinst prints the URL, sets up the service, and starts it. Full breakdown in packaging/README.md. Pi- specific end-to-end recipe (hardware checklist, OS flashing, optional SSD mount) in docs/user-guide/raspberry-pi-setup.md.
Manage the service:
sudo systemctl status polaris # Check status
sudo journalctl -u polaris -f # Follow logs
sudo systemctl restart polaris # RestartFor non-Debian distros (Fedora, Arch, etc) or when you prefer no systemd integration:
wget https://github.com/DanWBR/NINA.Polaris/releases/latest/download/polaris-linux-arm64.tar.gz
tar -xzf polaris-linux-arm64.tar.gz
cd polaris-linux-arm64
./NINA.Polaris # foreground; wire your own service unit if neededReplace linux-arm64 with linux-x64 for Intel/AMD 64-bit Linux.
Download the portable zip from GitHub Releases:
# x64 (most desktops/laptops):
Invoke-WebRequest -Uri "https://github.com/DanWBR/NINA.Polaris/releases/latest/download/polaris-win-x64.zip" -OutFile polaris.zip
Expand-Archive polaris.zip
cd polaris-win-x64
.\NINA.Polaris.exe
# ARM64 (Surface Pro X, some Copilot+ PCs):
Invoke-WebRequest -Uri "https://github.com/DanWBR/NINA.Polaris/releases/latest/download/polaris-win-arm64.zip" -OutFile polaris.zip
Expand-Archive polaris.zip
cd polaris-win-arm64
.\NINA.Polaris.exeOpen https://localhost:5000 (accept the self-signed cert once).
For unattended Windows installs, wire your own service via sc.exe,
NSSM, or a scheduled task. Build-from-source path:
.\deploy\publish-win-x64.ps1Multi-stage Dockerfile and docker-compose.yml are checked in. Builds for both linux/amd64 and linux/arm64:
# Single host build (uses your platform)
docker compose up -d --build
# Multi-arch build + push to registry
REGISTRY=ghcr.io/yourname ./deploy/docker-build.sh latestThe default compose file runs in network_mode: host so mDNS and INDI LAN
reach work out of the box. Add --profile indi to also start an
indiserver sidecar with the standard simulators (good for testing with no
hardware).
Persistence:
nina-datavolume → profiles + trained-flat exposures./imagesbind-mount → captured FITS output
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/equipment/devices |
List all discovered INDI devices |
| POST | /api/equipment/connect |
Connect to all selected devices |
| POST | /api/equipment/disconnect |
Disconnect all devices |
| GET | /api/equipment/status |
Aggregated status of every selected device (includes auto-derived sensor dimensions) |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/equipment/rigs |
All rigs + active id |
| GET | /api/equipment/rigs/active |
Active rig (full payload) |
| POST | /api/equipment/rigs |
Create empty rig { name } |
| POST | /api/equipment/rigs/clone |
Duplicate the active rig { newName } |
| PUT | /api/equipment/rigs/{id} |
Update a rig (selections, defaults, focal lengths, PHD2 endpoint) |
| POST | /api/equipment/rigs/{id}/activate |
Switch to this rig |
| DELETE | /api/equipment/rigs/{id} |
Delete a rig (refuses to delete the last one) |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/indi/properties?device= |
Full device → group → property tree (optionally filtered to one device) |
| POST | /api/indi/properties/set |
Set a property { device, property, type, numbers/switches/texts } |
| POST | /api/indi/properties/refresh |
Wipe the device cache and re-issue getProperties |
| POST | /api/indi/properties/config/{save|load|default}?device= |
Drive the driver's CONFIG_PROCESS |
| GET | /api/indi/properties/notes |
Operator's saved help notes (keyed by property name) |
| POST | /api/indi/properties/note |
Set or clear a note { property, text } (empty text clears) |
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/camera/select/{name} |
Select camera by INDI device name |
| POST | /api/camera/connect |
Connect selected camera |
| POST | /api/camera/capture |
Capture an image { exposure, gain, binning, filter } |
| POST | /api/camera/abort |
Abort current exposure |
| POST | /api/camera/cooler |
Set cooler { enabled, targetTemperature } |
| GET | /api/camera/status |
Camera status |
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/telescope/select/{name} |
Select mount |
| POST | /api/telescope/slew |
Slew to coordinates { ra, dec } |
| POST | /api/telescope/move/{direction} |
Manual move (north/south/east/west/stop) |
| POST | /api/telescope/park |
Park mount |
| POST | /api/telescope/unpark |
Unpark mount |
| POST | /api/telescope/tracking |
Toggle tracking { enabled } |
| POST | /api/telescope/abort |
Emergency stop |
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/focuser/select/{name} |
Select focuser |
| POST | /api/focuser/move/relative |
Move relative { steps } |
| POST | /api/focuser/move/absolute |
Move to position { position } |
| POST | /api/focuser/abort |
Abort movement |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/filterwheel/status |
Current filter and position |
| POST | /api/filterwheel/position/{slot} |
Move to slot number |
| POST | /api/filterwheel/filter/{name} |
Move to filter by name |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/image/latest/preview |
Latest image as JPEG |
| GET | /api/image/latest/stats?withStars |
Image dimensions + mean/median/min/max/stddev/MAD (+ optional star detection HFR stats) |
| GET | /api/image/latest/histogram?bins=256 |
Pixel-value histogram |
| GET | /api/image/latest/stars?maxStars&sigma |
Detected star list with (x, y, HFR, flux, peak) |
| GET | /api/image/stream/clients |
Per-client WebSocket diagnostics (mode, latency, streaks) |
| POST | /api/image/stream/adaptive |
Toggle adaptive bandwidth { enabled } |
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/guider/connect |
Connect to PHD2 { host, port } |
| POST | /api/guider/disconnect |
Disconnect |
| GET | /api/guider/status |
App state, RMS, peak, settle, pixel scale, last alert |
| GET | /api/guider/equipment |
Guide camera + mount + aux mount + AO names (get_current_equipment) |
| GET | /api/guider/steps?limit=N |
Recent GuideStep history |
| POST | /api/guider/guide |
Start guiding { settlePixels, settleTime, settleTimeout, recalibrate } |
| POST | /api/guider/dither |
Dither { pixels, raOnly, settle* } |
| POST | /api/guider/stop / /loop / /pause / /resume |
State changes |
| POST | /api/guider/find-star / /clear-calibration / /clear-history |
Maintenance |
| GET | /api/guider/profiles |
List PHD2 profiles + current one |
| POST | /api/guider/profile/{id} |
Switch PHD2 profile (auto-disconnects equipment first) |
| GET | /api/guider/equipment/connected |
Whether PHD2's own equipment is connected |
| POST | /api/guider/equipment/{connect,disconnect} |
Toggle PHD2's own equipment |
| GET | /api/guider/exposure |
Current exposure ms + list of available durations |
| POST | /api/guider/exposure/set/{ms} |
Set guide exposure |
| GET | /api/guider/dec-mode |
Current Dec guide mode |
| POST | /api/guider/dec-mode/{Auto|North|South|Off} |
Set Dec mode |
| GET | /api/guider/process/status |
Is PHD2 running? did we launch it? path configured? |
| POST | /api/guider/process/launch |
Spawn PHD2 (loopback only, polls port 4400 for up to 30s) |
| POST | /api/guider/process/shutdown |
Graceful JSON-RPC shutdown, falls back to kill only if we own it |
| GET | /api/guider/install-info |
Detected install (installed, resolvedPath, downloadUrl, os, searchedPaths), UI uses this to surface "Download PHD2" when missing |
| POST | /api/guider/auto-start/{true|false} |
Persist auto-start-on-boot preference in the user profile |
| POST | /api/guider/profile/sync |
Sync a rig (default: active rig) to its matching PHD2 profile + apply preset. Body: { rigId? } |
| GET | /api/guider/profile/sync/status |
Last sync phase / error / profileMissing flag |
| POST | /api/guider/calibrate/smart |
Start smart calibration job. Body: SmartCalibrateOptions (slewToEquator, exposureMsOverride, calibrationStepMsOverride, timeoutSeconds). Returns { jobId } |
| GET | /api/guider/calibrate/smart/{jobId} |
Poll calibration state (phase + stepMs + pixelScale + calibration + warnings) |
| POST | /api/guider/calibrate/smart/{jobId}/abort |
Abort running calibration |
| GET | /api/guider/algo-presets |
Curated algorithm presets (Default / Reactive / Smooth) with the (axis, name, value) triples each applies |
| POST | /api/guider/algo-preset/{name} |
Apply preset live + persist on the active rig |
| GET | /api/guider/algo-params |
Live values: per axis, every param get_algo_param_names reports |
| PUT | /api/guider/algo-params |
Set a single live knob { axis, name, value } + flip preset to "Custom" |
| GET | /api/guider/gui-session/status |
xpra-hosted PHD2 GUI lifecycle (xpra installed? version? session running? bind port) |
| POST | /api/guider/gui-session/{start,stop,restart} |
Manage the embedded PHD2 GUI session (Linux only; 501 elsewhere) |
| ALL | /phd2-gui/{**} |
Reverse-proxy to xpra HTML5 client (HTTP + WebSocket). Same-origin so iframe sessionStorage works |
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/autofocus/start |
Start V-curve { steps, stepSize, exposureSeconds, minStars, backlashSteps } |
| POST | /api/autofocus/abort |
Abort + restore start position |
| GET | /api/autofocus/status |
Live progress + sampled points |
| GET | /api/autofocus/result |
Most recent completed run + fitted parabola coefficients |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/meridianflip/settings |
Current configuration |
| PUT | /api/meridianflip/settings |
Update settings |
| GET | /api/meridianflip/status |
State + LST + hour angle + minutes-to-meridian |
| POST | /api/meridianflip/trigger |
Manual flip { ra, dec } |
| POST | /api/meridianflip/abort |
Abort in-progress flip |
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/flatwizard/start |
Start automated flat acquisition { filters, framesPerFilter, targetAdu, tolerance, minExposure, maxExposure, binning } |
| POST | /api/flatwizard/abort |
Abort |
| GET | /api/flatwizard/status |
Live progress + per-filter results |
| GET | /api/flatwizard/trained |
Persisted (filter+binning → exposure) dictionary |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/alpaca/discover?timeoutMs=3000 |
UDP-broadcast discovery on port 32227 + per-server /management/v1/configureddevices enrichment |
| GET | /api/alpaca/devices?host=&port= |
Direct device list query (skip discovery) |
| GET | /api/alpaca/camera/info?host=&port=&device= |
Camera probe (sensor, cooler, binning) |
| GET | /api/alpaca/telescope/info?host=&port=&device= |
Telescope probe (pointing, tracking, pier side) |
| POST | /api/alpaca/{camera,telescope}/connect?host=&port=&device=&connect= |
Connect / disconnect |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/stellarium/target?host=&port= |
Pull currently-selected object from Stellarium Remote Control plugin |
| GET | /api/stellarium/view?host=&port= |
Current view direction (alt / az / fov) |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/weather/forecast?lat=&lon= |
7Timer ASTRO 3-day forecast in 3 h slots with computed observationScore (0-100) per slot. Server-cached 15 min |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/sky/tonights-best?lat=&lon=&limit= |
Ranked list of DSOs / Moon / planets / comets observable during tonight's window |
| GET | /api/sky/image?name= |
Resolve thumbnail URL for a celestial object (NASA Image Library → Wikipedia fallback, disk-cached 30 days) |
| POST | /api/sky/image/prefetch |
Walk the full DSO catalog + Moon + planets + comets and pull all thumbnails to disk for offline use |
Frame browser, master integration, calibration, batch stacking, debayer, background extraction, noise reduction, sharpening, and multi-format export.
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/studio/rescan |
Walk ImageOutputDir recursively, header-only FITS scan, upsert SQLite index |
| GET | /api/studio/rescan/status |
Rescan progress |
| GET | /api/studio/frames?type=&filter=&target=&dateFrom=&dateTo=&limit=&offset= |
Paginated frame list |
| GET | /api/studio/frames/{id} |
Full row + FITS keyword dump |
| GET | /api/studio/frames/{id}/thumb |
Auto-stretched 256 px JPEG thumbnail (cached on disk) |
| GET | /api/studio/stats |
Aggregate: total lights, total exposure (h), distinct targets / filters |
| GET | /api/studio/frames/{id}/preview?black=&mid=&white=&max=&format=jpg|png |
Stretched preview (debounced slider re-renders hit this) |
| GET | /api/studio/frames/{id}/autostretch |
Auto-stretch black/mid/white triple to seed UI sliders |
| GET | /api/studio/frames/{id}/stats?stars= |
Full ImageStatistics + StarDetector output + histogram |
| POST | /api/studio/frames/{id}/export?format=tif|png|jpg&stretched=&black=&mid=&white= |
Export to {rig}/processed/{target}/ |
| POST | /api/studio/masters |
Start master-frame integration { frameIds, type: Bias|Dark|Flat|DarkFlat, method: Mean|Median|SigmaClippedMean } → { jobId } |
| GET | /api/studio/masters/{jobId}/status |
Master-integration progress |
| POST | /api/studio/calibrate |
Calibrate lights { lightIds, masterDarkId?, masterFlatId?, masterBiasId? } (null = auto-match per light) → { jobId } |
| GET | /api/studio/calibrate/{jobId}/status |
Calibration progress with succeeded / failed counts |
| POST | /api/studio/integrate |
Batch stack { frameIds, method } (align + integrate) → { jobId } |
| GET | /api/studio/integrate/{jobId}/status |
Stack progress with combined / dropped / total exposure |
| POST | /api/studio/frames/{id}/debayer |
Bilinear demosaic → luminance FITS in {rig}/processed/{target}/ |
| POST | /api/studio/frames/{id}/bgextract?samplesX=&samplesY=&polyDegree= |
Subtract polynomial gradient |
| POST | /api/studio/frames/{id}/nr?radius= |
Gaussian noise reduction |
| POST | /api/studio/frames/{id}/sharpen?amount=&radius=&threshold= |
Unsharp mask sharpening |
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/livestack/start |
Start live stacking |
| POST | /api/livestack/stop |
Stop live stacking |
| POST | /api/livestack/reset |
Reset stack buffer |
| GET | /api/livestack/status |
Stack frame count and state |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/sequence |
Current sequence items and state |
| POST | /api/sequence |
Load sequence [{ name, exposure, gain, ... }] |
| POST | /api/sequence/start |
Start execution |
| POST | /api/sequence/pause |
Pause execution |
| POST | /api/sequence/resume |
Resume from pause |
| POST | /api/sequence/stop |
Stop execution |
| GET | /api/sequence/status |
Detailed progress |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/sequencer/document |
Current SequenceDocument + state + lastError + abortReason |
| POST | /api/sequencer/document |
Load a SequenceDocument (JSON object) |
| GET | /api/sequencer/document/json |
Raw JSON download for "save sequence to file" |
| POST | /api/sequencer/document/json |
Accept raw JSON body, "load sequence from file" |
| POST | /api/sequencer/start |
Validate + run the tree in the background |
| POST | /api/sequencer/stop |
Cancel the run via the engine's CTS |
| POST | /api/sequencer/validate |
Walk Validate() across the tree, return errors |
| GET | /api/sequencer/types |
Palette listing, every known (type, category, kind) |
| GET | /api/sequencer/templates |
List saved templates + their store dir |
| GET | /api/sequencer/templates/{name} |
Load a named template |
| POST | /api/sequencer/templates/{name} |
Save a SequenceDocument as a named template |
| DELETE | /api/sequencer/templates/{name} |
Delete a template |
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/mosaic/plan |
Compute panels + time estimate from MosaicRequest (for the UI overlay preview) |
| POST | /api/mosaic/to-sequence |
Build the plan + lower to a SequenceDocument; optionally load into the engine via loadIntoEngine=true |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/plugins |
List loaded plugins with name / version / author / discriminators they contributed |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/sky/catalog/search?query=M31 |
Search embedded DSO catalog |
| GET | /api/sky/catalog/{name} |
Get object by exact name |
| GET | /api/sky/catalog/types |
Distinct object types (for filter dropdowns) |
| GET | /api/sky/catalog/filter?query&type&minMag&maxMag&minDec&maxDec&limit |
Filtered catalog query |
| GET | /api/sky/altitude?ra&dec&stepMinutes |
Target altitude track across tonight's window + twilight transitions |
| GET | /api/sky/fov |
Current FOV based on optics config |
| GET | /api/sky/solver/status |
Primary + blind solver availability and identity |
| GET | /api/sky/solver/list |
All four plate-solver backends with id / name / available / blind flag |
| POST | /api/sky/slew-and-center |
Start slew & center job { ra, dec, toleranceArcsec } |
| GET | /api/sky/slew-and-center/{id}/status |
Job progress |
| POST | /api/sky/slew-and-center/{id}/cancel |
Cancel job |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/sequence/dither |
Current dither settings |
| PUT | /api/sequence/dither |
Update dither settings { enabled, pixels, everyNFrames, raOnly, settle* } |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/system/status |
System info (CPU, RAM, uptime) |
| GET | /api/system/geocode?query=&limit= |
Address geocoding via Nominatim (rate-limited, User-Agent set) |
| GET | /api/system/relay |
Relay tunnel status (state, hostname, lastError) |
| GET | /api/system/profiles |
List profiles |
| GET | /api/system/profile |
Active profile |
| PUT | /api/system/profile |
Update settings |
| POST | /api/system/profile/save-as |
Save profile as new name |
| POST | /api/system/profile/load/{id} |
Load profile by ID |
| POST | /api/system/factory-reset |
Wipe all profiles / rigs / auth / settings back to first-run (keeps captured images) |
| Endpoint | Type | Description |
|---|---|---|
/ws/image-stream |
Binary | Live image frames (JPEG or raw+LZ4) |
/ws/status |
JSON | Equipment + sequence status at 1Hz |
Image stream negotiation: After connecting, send {"mode":"jpeg"} or {"mode":"raw"} to select format.
Status message format:
{
"type": "status",
"equipment": {
"indi": { "connected": true },
"camera": { "name": "ZWO ASI2600MC", "temperature": -10.0 },
"telescope": { "ra": 0.713, "dec": 41.27, "tracking": true, "slewing": false },
"focuser": { "position": 12500, "temperature": 15.2 },
"filterWheel": { "position": 3, "currentFilter": "Ha", "filters": ["L","R","G","B","Ha","OIII","SII"] }
},
"liveStack": { "isRunning": true, "frameCount": 42 },
"sequence": { "state": "running", "currentItemIndex": 1, "totalFrames": 100, "totalFramesCompleted": 37 }
}{
"Indi": {
"Host": "localhost",
"Port": 7624
},
"Logging": {
"LogLevel": {
"Default": "Information"
}
}
}| Variable | Default | Description |
|---|---|---|
ASPNETCORE_URLS |
http://0.0.0.0:5000 |
Listen address and port |
DOTNET_gcServer |
0 |
Use Workstation GC (saves RAM on RPi) |
Indi__Host |
localhost |
INDI server hostname |
Indi__Port |
7624 |
INDI server port |
PHD2__ExecutablePath |
(auto-detected) | Override the path to phd2.exe / phd2 binary. By default the app walks the standard install paths per OS, only set this for non-standard installs |
PHD2__Host / PHD2__Port |
localhost / 4400 |
PHD2 event server endpoint |
PHD2__InstanceNumber |
1 |
PHD2 -i N instance number |
PHD2__AutoStart |
false |
Fallback for PHD2AutoStart profile flag. UI checkbox in Guider tab is the normal way to set this |
Sequencer__TemplateDir |
sequencer-templates |
Folder where Advanced Sequencer templates are stored (one JSON file per template) |
Plugins__Enabled |
true |
Set to false to skip the plugin scan entirely |
Plugins__Directory |
plugins |
Folder scanned at startup for plugin .dll files |
PlateSolve__PrimarySolver |
astap |
One of astap, platesolve3, astrometry-net-online, astrometry-net-local |
PlateSolve__BlindSolver |
astrometry-net-online |
Fallback when primary fails |
PlateSolve__UseBlindFallback |
true |
Disable to lock to the primary only |
PlateSolve__AstapPath |
(auto) | ASTAP CLI path |
PlateSolve__PlateSolve3Path |
(none) | PlateSolve3.exe path |
PlateSolve__SolveFieldPath |
/usr/bin/solve-field |
Local Astrometry.net binary |
PlateSolve__AstrometryApiKey |
(none) | nova.astrometry.net API key |
Mdns__Enabled / Mdns__InstanceName |
true / nina-<hostname> |
mDNS announcer |
Relay__Enabled |
false |
Enable reverse-tunnel client |
Relay__ServerUrl |
(none) | e.g. wss://relay.example.com/_tunnel |
Relay__Token |
(none) | Bearer token matching a tenant entry on the relay server |
Relay__ClientCertPath |
(none) | Path to a .pfx to present on tunnel TLS handshake (mTLS) |
Relay__ClientCertPassword |
(none) | Password for the .pfx (optional) |
Relay server side (different process, same Relay__* prefix in appsettings.json):
| Key | Default | Purpose |
|---|---|---|
Relay__TenantsFile |
tenants.json |
Path to the JSON tenant store; hot-reloaded on change. Falls back to the legacy Tenants: section if empty/missing |
Relay__UsageStateFile |
tenant-state.json |
Persistent monthly-byte counter file |
Proxy__TimeoutSeconds |
60 |
Per-request timeout (long enough for plate-solving uploads) |
Proxy__HostnameSuffix |
(none) | e.g. .relay.example.com to enable subdomain routing |
Admin__Password |
(empty) | Password for /_admin/* and the /admin/ Web UI. Empty = admin API disabled (returns 503) |
Audit__Enabled |
true |
Set to false to disable the audit log |
Audit__Path |
audit.log |
JSON-lines audit log path |
Audit__MaxFileBytes |
52428800 |
Rotate at this size (default 50 MB) |
Audit__RingBufferSize |
5000 |
In-memory ring for /_admin/audit |
Tls__Mode |
off |
off / pfx / letsencrypt |
Tls__ClientCertificateMode |
request |
none / request / require, Kestrel client-cert behaviour (mTLS) |
Tls__HttpsPort |
443 |
HTTPS bind port when TLS is enabled |
Tls__RedirectHttpToHttps |
false |
308-redirect plain HTTP to HTTPS |
Tls__PfxPath / Tls__PfxPassword |
, | Static cert when Tls:Mode=pfx |
Tls__LetsEncrypt__Domains |
, | string[] of domains for ACME issuance |
Tls__LetsEncrypt__EmailAddress |
, | Contact email for Let's Encrypt |
Tls__LetsEncrypt__UseStaging |
false |
Use Let's Encrypt staging API while testing |
| Metric | Target | Notes |
|---|---|---|
| Memory | < 500 MB | RPi 4 with 2GB RAM |
| Startup | < 5 seconds | RPi 4 |
| Image relay | ~3-10 MB/frame | LZ4 compressed, fits WiFi 5GHz |
| JPEG preview | ~200-400 KB | For mobile/weak clients |
| Frontend bundle | ~580 KB total | Alpine.js + libs, cacheable |
| WASM live-stack bundle | ~12 MB on disk, ~3 MB gzipped | One-time download per browser |
| Status broadcast | 1 Hz | Equipment + sequence state |
Polaris ships with a one-click button to spawn a fake telescope +
camera + focuser + filter wheel. Open Settings → Equipment simulator
→ Launch. The simulated camera renders real stars from the GSC
catalog at whatever RA/Dec the simulated mount is pointing at,
plate solve, auto-focus, live stacking all work end-to-end against
it. Linux/macOS uses INDI simulators (apt install indi-bin);
Windows uses Alpaca Omni Simulator. See
docs/user-guide/simulator-mode.md.
Live stacking can run in your browser via a WebAssembly module
that reuses the same NINA.Image.Portable algorithms the server
runs. On Pi 2 / Pi 3 hosts this is the only way to keep up, the
Pi just orchestrates equipment + relays raw frames, the browser
does StarDetector + alignment + accumulator. Auto-detected on WS
handshake; per-rig override in the LIVE tab toolbar. See
docs/user-guide/client-side-compute.md.
If N.I.N.A. Polaris saves you an evening of fiddling with rigs and you want to chip in for hosting / a coffee / dark-sky travel:
Donations are entirely optional, the project stays free and open-source either way. Bug reports and PRs are just as welcome (see below).
Contributions are welcome! This project follows the same coding standards as the main N.I.N.A. repository.
- Endpoints are in
src/NINA.Polaris/Endpoints/, each is an extension method onWebApplication - Services are in
src/NINA.Polaris/Services/, registered as singletons inProgram.cs - INDI devices follow a consistent pattern in
src/NINA.INDI/Devices/ - Frontend is plain HTML/JS/CSS in
src/NINA.Polaris/wwwroot/, no build step required - Tests go in
tests/NINA.Polaris.Test/using NUnit
When the Photometric Color Calibration (PCC) workflow is enabled,
Polaris uses the AAVSO APASS DR10 star catalog under a CC-BY
4.0 license. The catalog is downloaded by scripts/download-apass.py
to wwwroot/catalogs/apass/apass.db (gitignored). If you publish
images calibrated with PCC, please credit:
Henden, A. A., Levine, S., Terrell, D., Welch, D. L., Munari, U., & Kloppenborg, B. K. (2018). "The APASS Data Release 10." VizieR On-line Data Catalog: II/336. https://www.aavso.org/apass
N.I.N.A. Polaris stands on the shoulders of a large community of astronomy and
open-source projects. Some we derive code from, some we studied as a reference,
and many ship inside the capture and processing stack. The same list is shown
in-app under HELP -> Credits & acknowledgements. Thank you to every author
below. Per-component license details are in ### Third-party licenses,
the licenses/ folder, and the bundled 3rd-party-licenses notice.
Built on
- N.I.N.A. - Nighttime Imaging 'N' Astronomy - Stefan Berg and the N.I.N.A. contributors. Polaris is derived from N.I.N.A. (MPL-2.0).
Guiding & gear simulation
- PHD2 - Open PHD Guiding - Andy Galasso, Bret McKee, Craig Stark and the PHD2 contributors. Managed external guider, and the reference for the native autoguider + gear simulator (BSD-3-Clause).
Image processing & AI
- GraXpert - the GraXpert development team. Background extraction, denoise and deconvolution ONNX models, and the default auto-stretch algorithm.
- Siril - the Free-Astro / Siril team. Optional external pre-processing and stacking.
Plate solving
- ASTAP - Han Kleijn. Default fast offline solver and star database.
- Astrometry.net - Dustin Lang, David W. Hogg and collaborators. Local and online blind solving.
- PlateSolve3 - PlaneWave Instruments. Alternative solver.
Equipment, protocols & camera SDKs
- INDI Library - Jasem Mutlaq and the INDI community. Primary equipment-control protocol.
- ASCOM Initiative & Alpaca - the ASCOM Initiative. Windows COM drivers and the cross-platform Alpaca protocol.
- ZWO ASI SDK - Suzhou ZWO Co., Ltd.
- SVBony SDK - SVBONY.
- Player One SDK - Player One Astronomy.
- ToupTek SDK - ToupTek Astro.
- Nikon SDK - Nikon Corporation.
Sky data, catalogs & astrometry
- OpenNGC - Mattia Verga. Bundled NGC/IC/Messier/Caldwell catalog (CC BY-SA 4.0).
- APASS - AAVSO Photometric All-Sky Survey - the AAVSO. Reference photometry for color calibration.
- Aladin Lite - CDS, Universite de Strasbourg / CNRS. Interactive sky atlas.
- Stellarium Web Engine - Stellarium Labs / Guillaume Chereau and contributors. WebGL planetarium sky view (AGPL-3.0).
- Astronomy Engine - Don Cross. High-precision ephemeris and coordinate math.
In-browser UI
- Alpine.js - Caleb Porzio and contributors.
- Chart.js - the Chart.js contributors.
- OpenSeadragon - the OpenSeadragon contributors.
- noVNC - the noVNC authors (MPL-2.0).
- xterm.js - the xterm.js authors.
- SunCalc - Vladimir Agafonkin.
- SortableJS - the SortableJS contributors.
Server & .NET
- Silk.NET - the .NET Foundation. OpenCL bindings for the SBC GPU compute backend.
- SkiaSharp - Microsoft, wrapping Google's Skia.
- ONNX Runtime - Microsoft. Runs the GraXpert AI models in the browser.
- YARP - Microsoft. Reverse proxy for embedded device web UIs.
- .NET Community Toolkit (MVVM) - the .NET Foundation.
- K4os.Compression.LZ4 - Milosz Krajewski.
- LettuceEncrypt - Nate McMaster.
- Serilog - the Serilog contributors.
- Json.NET - James Newton-King.
- SSH.NET - the SSH.NET contributors.
- net-mdns (Makaretu.Dns) - Richard Schneider.
nina.localdiscovery. - SQLite - D. Richard Hipp and the SQLite team.
- Rockchip RKNN runtime - Rockchip. Optional NPU acceleration on RK35xx boards.
Plus the wider amateur astronomy and free-software communities. If your work is used here and not listed, it is an oversight, not an intent, please let us know.
N.I.N.A. Polaris as a whole is licensed under the GNU Affero General Public
License v3.0 (AGPL-3.0). See LICENSE.txt and
NOTICE.
Portions are derived from N.I.N.A. - Nighttime Imaging 'N' Astronomy,
Copyright (C) Stefan Berg and the N.I.N.A. contributors, under the Mozilla
Public License 2.0 (licenses/MPL-2.0.txt). Those
files keep their MPL header; per MPL-2.0 section 3.3 they are combined into
this AGPL-3.0 Larger Work and a recipient may use those specific files under
either the MPL-2.0 or the AGPL-3.0.
A limited additional permission (linking exception) covers proprietary camera
vendor SDKs and dynamically-loaded plugins - see
licenses/LINKING-EXCEPTION.txt.
Because Polaris is a network-served application, AGPL-3.0 section 13 applies: if you run a modified version as a service, you must offer its source to users. Releases published before the relicensing remain available under the MPL-2.0.
- PHD2 (OpenPHDGuiding) -- BSD-3-Clause. The native autoguider
(
NINA.Guider.Portable) ports PHD2's core guiding math (single-star centroid, calibration, Hysteresis + Resist-Switch algorithms, camera/mount transforms) to C#. Each ported file carries the PHD2 BSD-3 header; the full license text is inlicenses/PHD2-LICENSE.txt. PHD2 is Copyright (c) the Open PHD Guiding development team and the Max Planck Society. See https://openphdguiding.org. - Silk.NET -- MIT. Managed OpenCL bindings used by the optional SBC GPU
compute backend (
NINA.Polaris.Services.OpenCl). Copyright (c) .NET Foundation and Contributors. See https://github.com/dotnet/Silk.NET.