diff --git a/api/src/main/kotlin/nebulosa/api/lightboxes/LightBoxController.kt b/api/src/main/kotlin/nebulosa/api/lightboxes/LightBoxController.kt
index 9105ebdbe..2a0c06962 100644
--- a/api/src/main/kotlin/nebulosa/api/lightboxes/LightBoxController.kt
+++ b/api/src/main/kotlin/nebulosa/api/lightboxes/LightBoxController.kt
@@ -53,4 +53,9 @@ class LightBoxController(
fun brightness(lightBox: LightBox, @RequestParam @Valid @PositiveOrZero intensity: Double) {
lightBoxService.brightness(lightBox, intensity)
}
+
+ @PutMapping("{lightBox}/listen")
+ fun listen(lightBox: LightBox) {
+ lightBoxService.listen(lightBox)
+ }
}
diff --git a/api/src/main/kotlin/nebulosa/api/lightboxes/LightBoxEventHub.kt b/api/src/main/kotlin/nebulosa/api/lightboxes/LightBoxEventHub.kt
index 858282809..85d85fc1a 100644
--- a/api/src/main/kotlin/nebulosa/api/lightboxes/LightBoxEventHub.kt
+++ b/api/src/main/kotlin/nebulosa/api/lightboxes/LightBoxEventHub.kt
@@ -21,7 +21,7 @@ class LightBoxEventHub(
@Subscribe(threadMode = ThreadMode.ASYNC)
override fun handleLightBoxEvent(event: LightBoxEvent) {
- if (event.device.type == DeviceType.ROTATOR) {
+ if (event.device.type == DeviceType.LIGHT_BOX) {
when (event) {
is PropertyChangedEvent -> onNext(event)
is LightBoxAttached -> onAttached(event.device)
diff --git a/api/src/main/kotlin/nebulosa/api/lightboxes/LightBoxService.kt b/api/src/main/kotlin/nebulosa/api/lightboxes/LightBoxService.kt
index ab32dd42d..ee861e4f2 100644
--- a/api/src/main/kotlin/nebulosa/api/lightboxes/LightBoxService.kt
+++ b/api/src/main/kotlin/nebulosa/api/lightboxes/LightBoxService.kt
@@ -4,7 +4,7 @@ import nebulosa.indi.device.lightbox.LightBox
import org.springframework.stereotype.Service
@Service
-class LightBoxService {
+class LightBoxService(private val lightBoxEventHub: LightBoxEventHub) {
fun connect(lightBox: LightBox) {
lightBox.connect()
@@ -25,4 +25,8 @@ class LightBoxService {
fun brightness(lightBox: LightBox, intensity: Double) {
lightBox.brightness(intensity)
}
+
+ fun listen(lightBox: LightBox) {
+ lightBoxEventHub.listen(lightBox)
+ }
}
diff --git a/desktop/src/app/about/about.component.ts b/desktop/src/app/about/about.component.ts
index 27d76b644..49e2c2e64 100644
--- a/desktop/src/app/about/about.component.ts
+++ b/desktop/src/app/about/about.component.ts
@@ -47,6 +47,7 @@ export class AboutComponent {
this.icons.push({ link: `${FLAT_ICON_URL}/stack_3342239`, name: 'Stack', author: 'Pixel perfect - Flaticon' })
this.icons.push({ link: `${FLAT_ICON_URL}/blackhole_6704410`, name: 'Blackhole', author: 'Freepik - Flaticon' })
this.icons.push({ link: `${FLAT_ICON_URL}/calibration_2364169`, name: 'Calibration', author: 'Freepik - Flaticon' })
+ this.icons.push({ link: `${FLAT_ICON_URL}/idea_3351801`, name: 'Bulb', author: 'Good Ware - Flaticon' })
}
private mapDependencies() {
diff --git a/desktop/src/app/app-routing.module.ts b/desktop/src/app/app-routing.module.ts
index 7f637d3fe..b3539ad51 100644
--- a/desktop/src/app/app-routing.module.ts
+++ b/desktop/src/app/app-routing.module.ts
@@ -16,6 +16,7 @@ import { GuiderComponent } from './guider/guider.component'
import { HomeComponent } from './home/home.component'
import { ImageComponent } from './image/image.component'
import { INDIComponent } from './indi/indi.component'
+import { LightBoxComponent } from './lightbox/lightbox.component'
import { MountComponent } from './mount/mount.component'
import { RotatorComponent } from './rotator/rotator.component'
import { SequencerComponent } from './sequencer/sequencer.component'
@@ -52,6 +53,10 @@ const routes: Routes = [
path: 'rotator',
component: RotatorComponent,
},
+ {
+ path: 'light-box',
+ component: LightBoxComponent,
+ },
{
path: 'guider',
component: GuiderComponent,
diff --git a/desktop/src/app/app.module.ts b/desktop/src/app/app.module.ts
index e09df9694..d3f98149f 100644
--- a/desktop/src/app/app.module.ts
+++ b/desktop/src/app/app.module.ts
@@ -25,6 +25,7 @@ import { InplaceModule } from 'primeng/inplace'
import { InputNumberModule } from 'primeng/inputnumber'
import { InputSwitchModule } from 'primeng/inputswitch'
import { InputTextModule } from 'primeng/inputtext'
+import { KnobModule } from 'primeng/knob'
import { ListboxModule } from 'primeng/listbox'
import { MenuModule } from 'primeng/menu'
import { MessageModule } from 'primeng/message'
@@ -95,6 +96,7 @@ import { CrossHairComponent } from './image/crosshair.component'
import { ImageComponent } from './image/image.component'
import { INDIComponent } from './indi/indi.component'
import { INDIPropertyComponent } from './indi/property/indi-property.component'
+import { LightBoxComponent } from './lightbox/lightbox.component'
import { MountComponent } from './mount/mount.component'
import { RotatorComponent } from './rotator/rotator.component'
import { SequencerComponent } from './sequencer/sequencer.component'
@@ -138,6 +140,7 @@ import { StackerComponent } from './stacker/stacker.component'
SpinnableNumberDirective,
INDIComponent,
INDIPropertyComponent,
+ LightBoxComponent,
LocationComponent,
MapComponent,
MenuBarComponent,
@@ -181,6 +184,7 @@ import { StackerComponent } from './stacker/stacker.component'
InputNumberModule,
InputSwitchModule,
InputTextModule,
+ KnobModule,
ListboxModule,
MenuModule,
MessageModule,
diff --git a/desktop/src/app/home/home.component.html b/desktop/src/app/home/home.component.html
index caa679cc9..2316ec03b 100644
--- a/desktop/src/app/home/home.component.html
+++ b/desktop/src/app/home/home.component.html
@@ -181,6 +181,16 @@
Switch
+
+
+
+ Light Box
+
+
0
}
+ get hasLightBox() {
+ return this.lightBoxes.length > 0
+ }
+
get hasGuider() {
return (this.hasCamera && this.hasMount) || this.hasGuideOutput
}
@@ -135,7 +141,7 @@ export class HomeComponent implements AfterContentInit {
}
get hasDevices() {
- return this.hasCamera || this.hasMount || this.hasFocuser || this.hasWheel || this.hasDome || this.hasRotator || this.hasSwitch || this.hasGuideOutput
+ return this.hasCamera || this.hasMount || this.hasFocuser || this.hasWheel || this.hasDome || this.hasRotator || this.hasSwitch || this.hasGuideOutput || this.hasLightBox
}
get hasINDI() {
@@ -253,6 +259,22 @@ export class HomeComponent implements AfterContentInit {
})
})
+ electronService.on('LIGHT_BOX.ATTACHED', (event) => {
+ ngZone.run(() => {
+ this.deviceAdded(event.device)
+ })
+ })
+ electronService.on(`LIGHT_BOX.DETACHED`, (event) => {
+ ngZone.run(() => {
+ this.deviceRemoved(event.device)
+ })
+ })
+ electronService.on(`LIGHT_BOX.UPDATED`, (event) => {
+ ngZone.run(() => {
+ this.deviceUpdated(event.device)
+ })
+ })
+
electronService.on('CONNECTION.CLOSED', async (event) => {
if (this.connection?.id === event.id) {
await ngZone.run(() => {
@@ -274,6 +296,7 @@ export class HomeComponent implements AfterContentInit {
this.wheels = await this.api.wheels()
this.rotators = await this.api.rotators()
this.guideOutputs = await this.api.guideOutputs()
+ this.lightBoxes = await this.api.lightBoxes()
}
void this.checkForNewVersion()
@@ -306,6 +329,8 @@ export class HomeComponent implements AfterContentInit {
this.rotators.push(device)
} else if (isGuideOuptut(device)) {
this.guideOutputs.push(device)
+ } else if (isLightBox(device)) {
+ this.lightBoxes.push(device)
}
}
@@ -328,6 +353,9 @@ export class HomeComponent implements AfterContentInit {
} else if (isGuideOuptut(device)) {
const found = this.guideOutputs.findIndex((e) => e.id === device.id)
this.guideOutputs.splice(found, 1)
+ } else if (isLightBox(device)) {
+ const found = this.lightBoxes.findIndex((e) => e.id === device.id)
+ this.lightBoxes.splice(found, 1)
}
}
@@ -350,6 +378,9 @@ export class HomeComponent implements AfterContentInit {
} else if (isGuideOuptut(device)) {
const found = this.guideOutputs.find((e) => e.id === device.id)
found && Object.assign(found, device)
+ } else if (isLightBox(device)) {
+ const found = this.lightBoxes.find((e) => e.id === device.id)
+ found && Object.assign(found, device)
}
}
@@ -423,10 +454,6 @@ export class HomeComponent implements AfterContentInit {
}
}
- protected findDeviceById(id: string) {
- return this.cameras.find((e) => e.id === id) ?? this.mounts.find((e) => e.id === id) ?? this.wheels.find((e) => e.id === id) ?? this.focusers.find((e) => e.id === id) ?? this.rotators.find((e) => e.id === id)
- }
-
protected deviceConnected(event: DeviceConnectionCommandEvent) {
return DeviceChooserComponent.handleConnectDevice(this.api, event.device, event.item)
}
@@ -444,6 +471,7 @@ export class HomeComponent implements AfterContentInit {
: type === 'FOCUSER' ? this.focusers
: type === 'WHEEL' ? this.wheels
: type === 'ROTATOR' ? this.rotators
+ : type === 'LIGHT_BOX' ? this.lightBoxes
: []
if (devices.length === 0) return
@@ -472,6 +500,9 @@ export class HomeComponent implements AfterContentInit {
case 'ROTATOR':
await this.browserWindowService.openRotator(device as Rotator, { bringToFront: true })
break
+ case 'LIGHT_BOX':
+ await this.browserWindowService.openLightBox(device as LightBox, { bringToFront: true })
+ break
}
}
@@ -493,6 +524,7 @@ export class HomeComponent implements AfterContentInit {
case 'FOCUSER':
case 'WHEEL':
case 'ROTATOR':
+ case 'LIGHT_BOX':
await this.openDevice(type)
break
case 'GUIDER':
@@ -588,6 +620,8 @@ export class HomeComponent implements AfterContentInit {
this.wheels = []
this.domes = []
this.rotators = []
+ this.lightBoxes = []
+ this.guideOutputs = []
this.switches = []
}
}
diff --git a/desktop/src/app/indi/indi.component.ts b/desktop/src/app/indi/indi.component.ts
index 224c32acb..0e195be11 100644
--- a/desktop/src/app/indi/indi.component.ts
+++ b/desktop/src/app/indi/indi.component.ts
@@ -87,6 +87,7 @@ export class INDIComponent implements AfterViewInit, OnDestroy {
const focusers = await this.api.focusers()
const rotators = await this.api.rotators()
const guideOutputs = await this.api.guideOutputs()
+ const lightBoxes = await this.api.lightBoxes()
const devices: Device[] = []
devices.push(...cameras.filter((a) => !devices.find((b) => a.name === b.name)))
@@ -95,6 +96,7 @@ export class INDIComponent implements AfterViewInit, OnDestroy {
devices.push(...focusers.filter((a) => !devices.find((b) => a.name === b.name)))
devices.push(...rotators.filter((a) => !devices.find((b) => a.name === b.name)))
devices.push(...guideOutputs.filter((a) => !devices.find((b) => a.name === b.name)))
+ devices.push(...lightBoxes.filter((a) => !devices.find((b) => a.name === b.name)))
this.devices = devices.sort(deviceComparator)
diff --git a/desktop/src/app/lightbox/lightbox.component.html b/desktop/src/app/lightbox/lightbox.component.html
new file mode 100644
index 000000000..c08423a2b
--- /dev/null
+++ b/desktop/src/app/lightbox/lightbox.component.html
@@ -0,0 +1,46 @@
+
diff --git a/desktop/src/app/lightbox/lightbox.component.ts b/desktop/src/app/lightbox/lightbox.component.ts
new file mode 100644
index 000000000..333f0794c
--- /dev/null
+++ b/desktop/src/app/lightbox/lightbox.component.ts
@@ -0,0 +1,119 @@
+import { AfterViewInit, Component, HostListener, NgZone, OnDestroy } from '@angular/core'
+import { ActivatedRoute } from '@angular/router'
+import { debounceTime, Subject, Subscription } from 'rxjs'
+import { ApiService } from '../../shared/services/api.service'
+import { ElectronService } from '../../shared/services/electron.service'
+import { PreferenceService } from '../../shared/services/preference.service'
+import { Tickable, Ticker } from '../../shared/services/ticker.service'
+import { DEFAULT_LIGHT_BOX, DEFAULT_LIGHT_BOX_PREFERENCE, LightBox } from '../../shared/types/lightbox.types'
+import { AppComponent } from '../app.component'
+
+@Component({
+ selector: 'neb-lightbox',
+ templateUrl: './lightbox.component.html',
+})
+export class LightBoxComponent implements AfterViewInit, OnDestroy, Tickable {
+ protected readonly lightBox = structuredClone(DEFAULT_LIGHT_BOX)
+ protected readonly preference = structuredClone(DEFAULT_LIGHT_BOX_PREFERENCE)
+
+ private readonly brightnessPublisher = new Subject()
+ private readonly brightnessSubscription?: Subscription
+
+ constructor(
+ private readonly app: AppComponent,
+ private readonly api: ApiService,
+ electronService: ElectronService,
+ private readonly preferenceService: PreferenceService,
+ private readonly route: ActivatedRoute,
+ private readonly ticker: Ticker,
+ ngZone: NgZone,
+ ) {
+ app.title = 'Light Box'
+
+ electronService.on('LIGHT_BOX.UPDATED', (event) => {
+ if (event.device.id === this.lightBox.id) {
+ ngZone.run(() => {
+ Object.assign(this.lightBox, event.device)
+ this.update()
+ })
+ }
+ })
+
+ electronService.on('LIGHT_BOX.DETACHED', (event) => {
+ if (event.device.id === this.lightBox.id) {
+ ngZone.run(() => {
+ Object.assign(this.lightBox, DEFAULT_LIGHT_BOX)
+ })
+ }
+ })
+
+ this.brightnessSubscription = this.brightnessPublisher.pipe(debounceTime(500)).subscribe((intensity) => {
+ void this.api.lightBoxBrightness(this.lightBox, intensity)
+ })
+ }
+
+ ngAfterViewInit() {
+ this.route.queryParams.subscribe(async (e) => {
+ const data = JSON.parse(decodeURIComponent(e['data'] as string)) as LightBox
+ await this.lightBoxChanged(data)
+ this.ticker.register(this, 30000)
+ })
+ }
+
+ @HostListener('window:unload')
+ ngOnDestroy() {
+ this.ticker.unregister(this)
+ this.brightnessSubscription?.unsubscribe()
+ }
+
+ async tick() {
+ if (this.lightBox.id) {
+ await this.api.lightBoxListen(this.lightBox)
+ }
+ }
+
+ protected async lightBoxChanged(lightBox?: LightBox) {
+ if (lightBox?.id) {
+ lightBox = await this.api.lightBox(lightBox.id)
+ Object.assign(this.lightBox, lightBox)
+
+ this.loadPreference()
+ this.update()
+ }
+
+ this.app.subTitle = lightBox?.name ?? ''
+ }
+
+ protected connect() {
+ if (this.lightBox.connected) {
+ return this.api.lightBoxDisconnect(this.lightBox)
+ } else {
+ return this.api.lightBoxConnect(this.lightBox)
+ }
+ }
+
+ protected toggleEnable(enabled: boolean) {
+ if (enabled) return this.api.lightBoxEnable(this.lightBox)
+ else return this.api.lightBoxDisable(this.lightBox)
+ }
+
+ protected intensityChanged(intensity: number) {
+ this.brightnessPublisher.next(intensity)
+ this.preference.intensity = intensity
+ this.savePreference()
+ }
+
+ private update() {}
+
+ private loadPreference() {
+ if (this.lightBox.id) {
+ Object.assign(this.preference, this.preferenceService.lightBox(this.lightBox).get())
+ }
+ }
+
+ protected savePreference() {
+ if (this.lightBox.connected) {
+ this.preferenceService.lightBox(this.lightBox).set(this.preference)
+ }
+ }
+}
diff --git a/desktop/src/assets/icons/light.png b/desktop/src/assets/icons/light.png
new file mode 100644
index 000000000..9c92fcce2
Binary files /dev/null and b/desktop/src/assets/icons/light.png differ
diff --git a/desktop/src/shared/directives/spinnable-number.directive.ts b/desktop/src/shared/directives/spinnable-number.directive.ts
index a3b588cd2..58bd0eb5b 100644
--- a/desktop/src/shared/directives/spinnable-number.directive.ts
+++ b/desktop/src/shared/directives/spinnable-number.directive.ts
@@ -1,15 +1,30 @@
-import { Directive, Host, HostListener } from '@angular/core'
+import { Directive, Host, HostListener, Optional } from '@angular/core'
import { InputNumber } from 'primeng/inputnumber'
+import { Knob } from 'primeng/knob'
@Directive({ selector: '[spinnableNumber]' })
export class SpinnableNumberDirective {
- constructor(@Host() private readonly inputNumber: InputNumber) {}
+ constructor(
+ @Host() @Optional() private readonly inputNumber?: InputNumber,
+ @Host() @Optional() private readonly knob?: Knob,
+ ) {}
@HostListener('wheel', ['$event'])
handleEvent(event: WheelEvent) {
- if (!this.inputNumber.disabled && !this.inputNumber.readonly && this.inputNumber.showButtons) {
- this.inputNumber.spin(event, -Math.sign(event.deltaY))
- event.stopImmediatePropagation()
+ if (this.inputNumber) {
+ if (!this.inputNumber.disabled && !this.inputNumber.readonly && this.inputNumber.showButtons) {
+ this.inputNumber.spin(event, -Math.sign(event.deltaY))
+ event.stopImmediatePropagation()
+ }
+ } else if (this.knob) {
+ if (!this.knob.disabled && !this.knob.readonly) {
+ const newValue = this.knob.value - this.knob.step * Math.sign(event.deltaY)
+
+ if (newValue >= this.knob.min && newValue <= this.knob.max) {
+ this.knob.updateModelValue(newValue)
+ event.stopImmediatePropagation()
+ }
+ }
}
}
}
diff --git a/desktop/src/shared/pipes/enum.pipe.ts b/desktop/src/shared/pipes/enum.pipe.ts
index b6fd92010..e2448ad6e 100644
--- a/desktop/src/shared/pipes/enum.pipe.ts
+++ b/desktop/src/shared/pipes/enum.pipe.ts
@@ -3,6 +3,7 @@ import { DARVState, Hemisphere, TPPAState } from '../types/alignment.types'
import { Constellation, MoonPhaseName, SatelliteGroupType, SkyObjectType } from '../types/atlas.types'
import { AutoFocusFittingMode, AutoFocusState, BacklashCompensationMode } from '../types/autofocus.type'
import { CameraCaptureState, ExposureMode, ExposureTimeUnit, FrameType, LiveStackerType } from '../types/camera.types'
+import { DeviceType } from '../types/device.types'
import { FlatWizardState } from '../types/flat-wizard.types'
import { GuideDirection, GuideState, GuiderPlotMode, GuiderYAxisUnit } from '../types/guider.types'
import { Bitpix, ImageChannel, SCNRProtectionMethod } from '../types/image.types'
@@ -45,11 +46,12 @@ export type EnumPipeKey =
| ImageChannel
| MoonPhaseName
| StackerState
+ | DeviceType
| 'ALL'
@Pipe({ name: 'enum' })
export class EnumPipe implements PipeTransform {
- readonly enums: Record> = {
+ private readonly enums: Record> = {
'DX/DY': 'dx/dy',
'RA/DEC': 'RA/DEC',
ABSOLUTE: 'Absolute',
@@ -101,6 +103,7 @@ export class EnumPipe implements PipeTransform {
CAE: 'Caelum',
CALIBRATING: 'Calibrating',
CAM: 'Camelopardalis',
+ CAMERA: 'Camera',
CAP: 'Capricornus',
CAPTURE_FINISHED: undefined,
CAPTURE_STARTED: undefined,
@@ -149,6 +152,7 @@ export class EnumPipe implements PipeTransform {
DENSE_CORE: 'Dense Core',
DITHERING: 'Dithering',
DMC: 'Disaster Monitoring',
+ DOME: 'Dome',
DOR: 'Dorado',
DOUBLE_OR_MULTIPLE_STAR: 'Double or Multiple Star',
DOUBLE: 'Double',
@@ -179,6 +183,7 @@ export class EnumPipe implements PipeTransform {
FIXED: 'Fixed',
FLAT: 'Flat',
FLOAT: 'Float',
+ FOCUSER: 'Focuser',
FOR: 'Fornax',
FORWARD: 'Forward',
FULL_MOON: 'Full Moon',
@@ -202,6 +207,7 @@ export class EnumPipe implements PipeTransform {
GOES: 'GOES',
GORIZONT: 'Gorizont',
GPS_OPS: 'GPS Operational',
+ GPS: 'GPS',
GRAVITATIONAL_LENS_SYSTEM_LENS_IMAGES: 'Gravitational Lens System (lens+images)',
GRAVITATIONAL_LENS: 'Gravitational Lens',
GRAVITATIONAL_SOURCE: 'Gravitational Source',
@@ -213,6 +219,7 @@ export class EnumPipe implements PipeTransform {
GREEN: 'Green',
GROUP_OF_GALAXIES: 'Group of Galaxies',
GRU: 'Grus',
+ GUIDE_OUTPUT: 'Guide Output',
GUIDING: 'Guiding',
HER: 'Hercules',
HERBIG_AE_BE_STAR: 'Herbig Ae/Be Star',
@@ -252,6 +259,7 @@ export class EnumPipe implements PipeTransform {
LEO: 'Leo',
LEP: 'Lepus',
LIB: 'Libra',
+ LIGHT_BOX: 'Light Box',
LIGHT: 'Light',
LINER_TYPE_ACTIVE_GALAXY_NUCLEUS: 'LINER-type Active Galaxy Nucleus',
LMI: 'Leo Minor',
@@ -289,6 +297,7 @@ export class EnumPipe implements PipeTransform {
MOLNIYA: 'Molniya',
MON: 'Monoceros',
MONO: 'Mono',
+ MOUNT: 'Mount',
MOVING_GROUP: 'Moving Group',
MOVING: 'Moving',
MUS: 'Musca',
@@ -357,6 +366,7 @@ export class EnumPipe implements PipeTransform {
RET: 'Reticulum',
RGB: 'RGB',
ROTATING_VARIABLE: 'Rotating Variable',
+ ROTATOR: 'Rotator',
RR_LYRAE_VARIABLE: 'RR Lyrae Variable',
RS_CVN_VARIABLE: 'RS CVn Variable',
RUNNING: 'Running',
@@ -405,6 +415,7 @@ export class EnumPipe implements PipeTransform {
SUPERNOVA_REMNANT: 'SuperNova Remnant',
SUPERNOVA: 'SuperNova',
SWARM: 'Swarm',
+ SWITCH: 'Switch',
SX_PHE_VARIABLE: 'SX Phe Variable',
SYMBIOTIC_STAR: 'Symbiotic Star',
T_TAURI_STAR: 'T Tauri Star',
@@ -433,6 +444,7 @@ export class EnumPipe implements PipeTransform {
WAITING: 'Waiting',
WEATHER: 'Weather',
WEST: 'West',
+ WHEEL: 'Filter Wheel',
WHITE_DWARF: 'White Dwarf',
WOLF_RAYET: 'Wolf-Rayet',
X_COMM: 'Experimental Comm',
diff --git a/desktop/src/shared/services/api.service.ts b/desktop/src/shared/services/api.service.ts
index 1e63e22df..06c9a1d00 100644
--- a/desktop/src/shared/services/api.service.ts
+++ b/desktop/src/shared/services/api.service.ts
@@ -12,6 +12,7 @@ import { HipsSurvey } from '../types/framing.types'
import { GuideDirection, GuideOutput, Guider, GuiderHistoryStep, SettleInfo } from '../types/guider.types'
import { ConnectionStatus, ConnectionType, GitHubRelease } from '../types/home.types'
import { AnnotateImageRequest, CoordinateInterpolation, DetectedStar, FOVCamera, FOVTelescope, ImageAnnotation, ImageInfo, ImageMousePosition, ImageSaveDialog, ImageSolved, ImageTransformation } from '../types/image.types'
+import { LightBox } from '../types/lightbox.types'
import { CelestialLocationType, Mount, MountRemoteControl, MountRemoteControlProtocol, SlewRate, TrackMode } from '../types/mount.types'
import { PlateSolverRequest } from '../types/platesolver.types'
import { Rotator } from '../types/rotator.types'
@@ -338,6 +339,41 @@ export class ApiService {
return this.http.put(`guide-outputs/${guideOutput.id}/listen`)
}
+ // LIGHT BOX
+
+ lightBoxes() {
+ return this.http.get(`light-boxes`)
+ }
+
+ lightBox(id: string) {
+ return this.http.get(`light-boxes/${id}`)
+ }
+
+ lightBoxConnect(lightBox: LightBox) {
+ return this.http.put(`light-boxes/${lightBox.id}/connect`)
+ }
+
+ lightBoxDisconnect(lightBox: LightBox) {
+ return this.http.put(`light-boxes/${lightBox.id}/disconnect`)
+ }
+
+ lightBoxEnable(lightBox: LightBox) {
+ return this.http.put(`light-boxes/${lightBox.id}/enable`)
+ }
+
+ lightBoxDisable(lightBox: LightBox) {
+ return this.http.put(`light-boxes/${lightBox.id}/disable`)
+ }
+
+ lightBoxBrightness(lightBox: LightBox, intensity: number) {
+ const query = this.http.query({ intensity })
+ return this.http.put(`light-boxes/${lightBox.id}/brightness?${query}`)
+ }
+
+ lightBoxListen(lightBox: LightBox) {
+ return this.http.put(`light-boxes/${lightBox.id}/listen`)
+ }
+
// GUIDING
guidingConnect(host: string = 'localhost', port: number = 4400) {
diff --git a/desktop/src/shared/services/browser-window.service.ts b/desktop/src/shared/services/browser-window.service.ts
index f255d28c5..b0b8c31c3 100644
--- a/desktop/src/shared/services/browser-window.service.ts
+++ b/desktop/src/shared/services/browser-window.service.ts
@@ -6,6 +6,7 @@ import { Device } from '../types/device.types'
import { Focuser } from '../types/focuser.types'
import { LoadFraming } from '../types/framing.types'
import { ImageSource, OpenImage } from '../types/image.types'
+import { LightBox } from '../types/lightbox.types'
import { Mount } from '../types/mount.types'
import { Rotator } from '../types/rotator.types'
import { Wheel, WheelDialogInput } from '../types/wheel.types'
@@ -61,6 +62,11 @@ export class BrowserWindowService {
return this.openWindow({ preference, data, id: `rotator.${data.name}`, path: 'rotator' })
}
+ openLightBox(data: LightBox, preference: WindowPreference = {}) {
+ Object.assign(preference, { icon: 'light', width: 290, height: 216 })
+ return this.openWindow({ preference, data, id: `lightbox.${data.name}`, path: 'light-box' })
+ }
+
openWheelDialog(data: WheelDialogInput, preference: WindowPreference = {}) {
Object.assign(preference, { icon: 'filter-wheel', width: 300, height: 217 })
return this.openModal({
diff --git a/desktop/src/shared/services/electron.service.ts b/desktop/src/shared/services/electron.service.ts
index 2e9f842ba..b532a223f 100644
--- a/desktop/src/shared/services/electron.service.ts
+++ b/desktop/src/shared/services/electron.service.ts
@@ -19,6 +19,7 @@ import { Focuser } from '../types/focuser.types'
import { GuideOutput, Guider, GuiderHistoryStep, GuiderMessageEvent } from '../types/guider.types'
import { ConnectionClosed } from '../types/home.types'
import { ROISelected } from '../types/image.types'
+import { LightBox } from '../types/lightbox.types'
import { Mount } from '../types/mount.types'
import { Rotator } from '../types/rotator.types'
import { SequencerEvent } from '../types/sequencer.types'
@@ -38,7 +39,7 @@ export const SAVE_IMAGE_FILE_FILTER: Electron.FileFilter[] = [
{ name: 'Image', extensions: ['png', 'jpg', 'jpeg'] },
]
-export interface EventTypes {
+export interface EventMap {
NOTIFICATION: NotificationEvent
CONFIRMATION: ConfirmationEvent
'DEVICE.PROPERTY_CHANGED': INDIMessageEvent
@@ -63,6 +64,9 @@ export interface EventTypes {
'GUIDE_OUTPUT.UPDATED': DeviceMessageEvent
'GUIDE_OUTPUT.ATTACHED': DeviceMessageEvent
'GUIDE_OUTPUT.DETACHED': DeviceMessageEvent
+ 'LIGHT_BOX.UPDATED': DeviceMessageEvent
+ 'LIGHT_BOX.ATTACHED': DeviceMessageEvent
+ 'LIGHT_BOX.DETACHED': DeviceMessageEvent
'GUIDER.CONNECTED': GuiderMessageEvent
'GUIDER.DISCONNECTED': GuiderMessageEvent
'GUIDER.UPDATED': GuiderMessageEvent
@@ -130,11 +134,11 @@ export class ElectronService {
return !!(window && window.process?.type)
}
- send(channel: K, data?: EventTypes[K]) {
+ send(channel: K, data?: EventMap[K]) {
return this.ipcRenderer.invoke(channel, data)
}
- on(channel: K, listener: (arg: EventTypes[K]) => void) {
+ on(channel: K, listener: (arg: EventMap[K]) => void) {
this.ipcRenderer.on(channel, (_, arg) => {
listener(arg)
})
diff --git a/desktop/src/shared/services/preference.service.ts b/desktop/src/shared/services/preference.service.ts
index ea9586ef5..4acf173eb 100644
--- a/desktop/src/shared/services/preference.service.ts
+++ b/desktop/src/shared/services/preference.service.ts
@@ -10,6 +10,7 @@ import { DEFAULT_FRAMING_PREFERENCE, FramingPreference, framingPreferenceWithDef
import { DEFAULT_GUIDER_PREFERENCE, GuiderPreference, guiderPreferenceWithDefault } from '../types/guider.types'
import { DEFAULT_HOME_PREFERENCE, HomePreference, homePreferenceWithDefault } from '../types/home.types'
import { DEFAULT_IMAGE_PREFERENCE, ImagePreference, imagePreferenceWithDefault } from '../types/image.types'
+import { DEFAULT_LIGHT_BOX_PREFERENCE, LightBox, LightBoxPreference, lightBoxPreferenceWithDefault } from '../types/lightbox.types'
import { DEFAULT_MOUNT_PREFERENCE, Mount, MountPreference, mountPreferenceWithDefault } from '../types/mount.types'
import { DEFAULT_ROTATOR_PREFERENCE, Rotator, RotatorPreference, rotatorPreferenceWithDefault } from '../types/rotator.types'
import { DEFAULT_SEQUENCER_PREFERENCE, SequencerPreference, sequencerPreferenceWithDefault } from '../types/sequencer.types'
@@ -99,6 +100,10 @@ export class PreferenceService {
return this.create(`rotator.${rotator.name}`, () => structuredClone(DEFAULT_ROTATOR_PREFERENCE), rotatorPreferenceWithDefault)
}
+ lightBox(lightBox: LightBox) {
+ return this.create(`lightBox.${lightBox.name}`, () => structuredClone(DEFAULT_LIGHT_BOX_PREFERENCE), lightBoxPreferenceWithDefault)
+ }
+
flatWizard(camera: Camera) {
return this.create(`flatWizard.${camera.name}`, () => structuredClone(DEFAULT_FLAT_WIZARD_PREFERENCE), flatWizardPreferenceWithDefault)
}
diff --git a/desktop/src/shared/types/device.types.ts b/desktop/src/shared/types/device.types.ts
index d9d958318..961c1afa5 100644
--- a/desktop/src/shared/types/device.types.ts
+++ b/desktop/src/shared/types/device.types.ts
@@ -8,7 +8,7 @@ export type INDIPropertyType = 'NUMBER' | 'SWITCH' | 'TEXT'
export type SwitchRule = 'ONE_OF_MANY' | 'AT_MOST_ONE' | 'ANY_OF_MANY'
-export type DeviceType = 'CAMERA' | 'MOUNT' | 'WHEEL' | 'FOCUSER' | 'ROTATOR' | 'GPS' | 'DOME' | 'SWITCH' | 'GUIDE_OUTPUT'
+export type DeviceType = 'CAMERA' | 'MOUNT' | 'WHEEL' | 'FOCUSER' | 'ROTATOR' | 'GPS' | 'DOME' | 'SWITCH' | 'GUIDE_OUTPUT' | 'LIGHT_BOX'
export interface Device {
readonly type: DeviceType
diff --git a/desktop/src/shared/types/lightbox.types.ts b/desktop/src/shared/types/lightbox.types.ts
new file mode 100644
index 000000000..e8343fe2c
--- /dev/null
+++ b/desktop/src/shared/types/lightbox.types.ts
@@ -0,0 +1,40 @@
+import type { Device } from './device.types'
+
+export interface LightBox extends Device {
+ enabled: boolean
+ intensity: number
+ minIntensity: number
+ maxIntensity: number
+}
+
+export interface LightBoxPreference {
+ intensity: number
+}
+
+export const DEFAULT_LIGHT_BOX_PREFERENCE: LightBoxPreference = {
+ intensity: 0,
+}
+
+export const DEFAULT_LIGHT_BOX: LightBox = {
+ enabled: false,
+ intensity: 0,
+ minIntensity: 0,
+ maxIntensity: 0,
+ type: 'LIGHT_BOX',
+ sender: '',
+ id: '',
+ name: '',
+ driverName: '',
+ driverVersion: '',
+ connected: false,
+}
+
+export function isLightBox(device?: Device): device is LightBox {
+ return !!device && device.type === 'LIGHT_BOX'
+}
+
+export function lightBoxPreferenceWithDefault(preference?: Partial, source: LightBoxPreference = DEFAULT_LIGHT_BOX_PREFERENCE) {
+ if (!preference) return structuredClone(source)
+ preference.intensity ??= source.intensity
+ return preference as LightBoxPreference
+}
diff --git a/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/INDIDevice.kt b/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/INDIDevice.kt
index c4887e8a6..083f7fdeb 100644
--- a/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/INDIDevice.kt
+++ b/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/INDIDevice.kt
@@ -202,6 +202,8 @@ internal abstract class INDIDevice : Device {
}
override fun disconnect() {
- sendNewSwitch("CONNECTION", "DISCONNECT" to true)
+ if (connected) {
+ sendNewSwitch("CONNECTION", "DISCONNECT" to true)
+ }
}
}
diff --git a/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/INDIDeviceProtocolHandler.kt b/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/INDIDeviceProtocolHandler.kt
index 2cf5bce73..fef8a88b0 100644
--- a/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/INDIDeviceProtocolHandler.kt
+++ b/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/INDIDeviceProtocolHandler.kt
@@ -164,64 +164,72 @@ abstract class INDIDeviceProtocolHandler : AbstractINDIDeviceProvider(), Message
var registered = false
if (DeviceInterfaceType.isCamera(interfaceType)) {
+ registered = true
+
registerCamera(driverInfo)?.also {
- registered = true
it.handleMessage(message)
takeMessageFromReorderingQueue(it)
}
}
if (DeviceInterfaceType.isMount(interfaceType)) {
+ registered = true
+
registerMount(driverInfo)?.also {
- registered = true
it.handleMessage(message)
takeMessageFromReorderingQueue(it)
}
}
if (DeviceInterfaceType.isFilterWheel(interfaceType)) {
+ registered = true
+
registerFilterWheel(driverInfo)?.also {
- registered = true
it.handleMessage(message)
takeMessageFromReorderingQueue(it)
}
}
if (DeviceInterfaceType.isFocuser(interfaceType)) {
+ registered = true
+
registerFocuser(driverInfo)?.also {
- registered = true
it.handleMessage(message)
takeMessageFromReorderingQueue(it)
}
}
if (DeviceInterfaceType.isRotator(interfaceType)) {
+ registered = true
+
registerRotator(driverInfo)?.also {
- registered = true
it.handleMessage(message)
takeMessageFromReorderingQueue(it)
}
}
if (DeviceInterfaceType.isGPS(interfaceType)) {
+ registered = true
+
registerGPS(driverInfo)?.also {
- registered = true
it.handleMessage(message)
takeMessageFromReorderingQueue(it)
}
}
if (DeviceInterfaceType.isGuider(interfaceType)) {
+ registered = true
+
registerGuideOutput(driverInfo)?.also {
- registered = true
it.handleMessage(message)
takeMessageFromReorderingQueue(it)
}
}
if (DeviceInterfaceType.isLightBox(interfaceType)) {
+ registered = true
+
registerLightBox(driverInfo)?.also {
- registered = true
it.handleMessage(message)
takeMessageFromReorderingQueue(it)
}
diff --git a/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/AbstractINDIDeviceProvider.kt b/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/AbstractINDIDeviceProvider.kt
index ac834280c..c665acff9 100644
--- a/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/AbstractINDIDeviceProvider.kt
+++ b/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/AbstractINDIDeviceProvider.kt
@@ -61,6 +61,7 @@ abstract class AbstractINDIDeviceProvider : INDIDeviceProvider {
rotator(id)?.also(devices::add)
gps(id)?.also(devices::add)
guideOutput(id)?.also(devices::add)
+ lightBox(id)?.also(devices::add)
thermometer(id)?.also(devices::add)
return devices
}
@@ -259,6 +260,7 @@ abstract class AbstractINDIDeviceProvider : INDIDeviceProvider {
rotators().onEach(Device::close).onEach(::unregisterRotator)
gps().onEach(Device::close).onEach(::unregisterGPS)
guideOutputs().onEach(Device::close).onEach(::unregisterGuideOutput)
+ lightBoxes().onEach(Device::close).onEach(::unregisterLightBox)
cameras.clear()
mounts.clear()
@@ -267,6 +269,7 @@ abstract class AbstractINDIDeviceProvider : INDIDeviceProvider {
rotators.clear()
gps.clear()
guideOutputs.clear()
+ lightBoxes.clear()
thermometers.clear()
handlers.clear()
diff --git a/nebulosa-indi-protocol/src/main/kotlin/nebulosa/indi/protocol/parser/INDIProtocolHandler.kt b/nebulosa-indi-protocol/src/main/kotlin/nebulosa/indi/protocol/parser/INDIProtocolHandler.kt
index 1089a8bc1..b82831187 100644
--- a/nebulosa-indi-protocol/src/main/kotlin/nebulosa/indi/protocol/parser/INDIProtocolHandler.kt
+++ b/nebulosa-indi-protocol/src/main/kotlin/nebulosa/indi/protocol/parser/INDIProtocolHandler.kt
@@ -2,7 +2,7 @@ package nebulosa.indi.protocol.parser
import nebulosa.indi.protocol.INDIProtocol
-interface INDIProtocolHandler {
+fun interface INDIProtocolHandler {
fun handleMessage(message: INDIProtocol)
}