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 @@ +
+
+
+ + + +
+
+
+
+
+ Enabled + +
+
+
+ +
+
+
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) }