Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D2#rZZK~#8N?VD?C
z9Mu)a@15DT9Xm0EBtjt}X>@S%0{sW{$sq<)Bm3JF0-$P!X3)Gz&vs(vUE
zC{%!i5OyJ?1Qp5ldYwe5C_p0=lF&Bg>6nB(93_rZ%Zv9tGyR{Lv&-(@yWW}2ZdeQd
zq|rU+?CYL0_n!NhyJDGTmia*=xeUaC1I~)}_Uyf;Y2E>~ootRHR!Av3NG@O)&iTGR
z^C0XFn*!lDj*P{uK74*7n_OT|ODR&UW-QA(o5{3I?%XL0HsAOTKrH60mePI{KmQ1v
z-3pW6&kjc-p~qMb#Q1oA7{9fP)ml)HKSr?jIgasqBqIOknHR^G6R}u+H;QL*t#u8-
za6%z-?L!aAYpna?;6dk>eBPR6H7GL(-0!2&&|g`viovB8G<6fszX)*%S3S-X>GpP!
zWZ5_K^KKJQds<-Dp^FTo?k|uJY}2x606IIZy(k7ftuDxk!EM`QU`MDw!wLHBdV@eb
z1s|SgSxeO}fd2Own$c^lmXgIaiu`m0x$O*Ecn0Mh-ph$DOm{m&QdgIluE+UO-h0pK
zSXdCZv5JJ7>m&iek*LGXkYJN{Au;yC7R5_#^m-|%+wN-JK^p){ctTiTL9A%@@}!i;
z!~Ok|UTZlfCY+vp-rkQJazC32%m`uj=n|v03wYUHbv~IidRx_bqOVVWY1>9GTE-`A
zDuA5e`lL`L2RM(gP#{Zp9{uuL)?1bxJ7gjhG9HFVU9||h$ZuFILkR#)a?odf%4&hp
zR=)F 8Y01)jjl^i`H!loo(Vgc2pwZ1dFxYz&tun&LbWHVtap
z9XZ>2hfP&6&9G8uoo~7v5aO~3T11}3#c{mZx25C4aB||-^goc5Eq>+tsN7G0
z{Mz>Ip+h8>q6C08Fp(+uU`Dv6tINCv@-V;D6mrHYVM%w1FKpR{cI&HG`l7!-Gz1@3
zlP{1T*KN-F-PrWSrK?Hz2fn%%78tP>e0kD^B5IcptScZb$&$jK@VTr%omIzVRl_GFkJ;=kwwZe|knd
z^Zc`7=&k*1Z5~Yzz-Loa;`F(5td*0=WyRm$EuYc;aqn|13Gv0$r1pchKjb
zd*vn88c)*#5dYvqk@$9jwb~4i(mG1dd?F!64o$GuSeg=mib|*b`
zQRQZ&EC6Cq!HYiumPhkPJkVzK-X`Nl65Q4ILd
zcC4MpStU}mEcUY}P{;u%(<4~gHfHjYOso-~pYCIg?wBKfymsd2zkXoDhiuY9cb=Sw
zSUk6cuG!b$AEy0|B1#Lun)z-P3#`CGuI!M~jpWMZ9+v2CS>jgv%(`066YcF{1B$Xk
zTaIJUW4|(dB|*aoHd?GU?iA7q;|g8(W!v5!Ef&n#JGv*gJUP&F)iu<>Z=IDl#bVYV
zVnyeIN-#G^BJy)qs&Z*2IfHMk_bic?iT`HB`9z1$x1!H~%&H6g+ST75dYx4lr3L67Dx&Eq4;5P-U7az(RD9DV
zLFJ($g5s+Sv=X|9yg>Dh%TnAUZlxfaH6KN_8ipa2r6syR?Eqlbq}jiccd@W!(9CCm
z^$iO}lmO7FYZMi``pz#{STbPR+sk4Bdtsri^0A*90Dh!Ynehkc5F*Ph&)ArAABu>l
z6*;auPOr-WK)%63fi^mUIWpq3d@i6mN@>%6df6y|F0qfrGPD7p^Y$oaJk0}>i2Du0
zKGMo9f$qF%+V8`IpRlRG4Ev-|b-t{h5V>SAo{k}+NAN>maZ-l|Tl
zXSM2KY1HGFFjKSF%~>9eL_(FV+GWuN1D&@=@sy|41^u;Uke<@A9tc{`
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/desktop/src/shared/components/device-list-button/device-list-button.component.ts b/desktop/src/shared/components/device-list-button/device-list-button.component.ts
index 17c101c42..ce96deefc 100644
--- a/desktop/src/shared/components/device-list-button/device-list-button.component.ts
+++ b/desktop/src/shared/components/device-list-button/device-list-button.component.ts
@@ -7,7 +7,7 @@ import { DeviceListMenuComponent } from '../device-list-menu/device-list-menu.co
templateUrl: './device-list-button.component.html',
styleUrls: ['./device-list-button.component.scss'],
})
-export class DeviceListButtonComponent {
+export class DeviceListButtonComponent {
@Input({ required: true })
readonly title!: string
@@ -19,16 +19,22 @@ export class DeviceListButtonComponent {
readonly icon!: string
@Input({ required: true })
- readonly devices!: Device[]
+ readonly devices!: T[]
@Input()
readonly hasNone: boolean = false
@Input()
- device?: Device
+ device?: T
@Output()
- readonly deviceChange = new EventEmitter()
+ readonly deviceChange = new EventEmitter()
+
+ @Output()
+ readonly deviceConnect = new EventEmitter()
+
+ @Output()
+ readonly deviceDisconnect = new EventEmitter()
@ViewChild('deviceMenu')
private readonly deviceMenu!: DeviceListMenuComponent
diff --git a/desktop/src/shared/components/device-list-menu/device-list-menu.component.ts b/desktop/src/shared/components/device-list-menu/device-list-menu.component.ts
index de139677f..f60d66c5b 100644
--- a/desktop/src/shared/components/device-list-menu/device-list-menu.component.ts
+++ b/desktop/src/shared/components/device-list-menu/device-list-menu.component.ts
@@ -1,4 +1,4 @@
-import { Component, Input, ViewChild } from '@angular/core'
+import { Component, EventEmitter, Input, Output, ViewChild } from '@angular/core'
import { SEPARATOR_MENU_ITEM } from '../../constants'
import { PrimeService } from '../../services/prime.service'
import { Device } from '../../types/device.types'
@@ -28,6 +28,12 @@ export class DeviceListMenuComponent {
@Input()
readonly hasNone: boolean = false
+ @Output()
+ readonly deviceConnect = new EventEmitter()
+
+ @Output()
+ readonly deviceDisconnect = new EventEmitter()
+
@ViewChild('menu')
private readonly menu!: DialogMenuComponent
@@ -71,6 +77,16 @@ export class DeviceListMenuComponent {
label: device.name,
checked: selected === device,
disabled: this.disableIfDeviceIsNotConnected && !device.connected,
+ toolbarMenu: [
+ {
+ icon: 'mdi ' + (device.connected ? 'mdi-close text-red-500' : 'mdi-connection text-blue-500'),
+ label: device.connected ? 'Disconnect' : 'Connect',
+ command: event => {
+ if (device.connected) this.deviceDisconnect.emit(device)
+ else this.deviceConnect.emit(device)
+ }
+ }
+ ],
command: () => {
resolve(device)
},
diff --git a/desktop/src/shared/components/menu-item/menu-item.component.html b/desktop/src/shared/components/menu-item/menu-item.component.html
index 03f61f537..6ad336a57 100644
--- a/desktop/src/shared/components/menu-item/menu-item.component.html
+++ b/desktop/src/shared/components/menu-item/menu-item.component.html
@@ -5,15 +5,13 @@
@if (item.toolbarMenu?.length) {
@for (m of item.toolbarMenu; track i; let i = $index) {
-
}
}
- @if (item.checked) {
-
- } @else if(item.toggleable) {
+ @if(item.toggleable) {
}
@if (item.items?.length || item.menu?.length) {
diff --git a/desktop/src/shared/services/api.service.ts b/desktop/src/shared/services/api.service.ts
index 614cbdbfd..fb1eb0b5a 100644
--- a/desktop/src/shared/services/api.service.ts
+++ b/desktop/src/shared/services/api.service.ts
@@ -2,6 +2,7 @@ import { Injectable } from '@angular/core'
import moment from 'moment'
import { DARVStart, TPPAStart } from '../types/alignment.types'
import { Angle, BodyPosition, CloseApproach, ComputedLocation, Constellation, DeepSkyObject, MinorPlanet, Satellite, SatelliteGroupType, SkyObjectType, Twilight } from '../types/atlas.types'
+import { AutoFocusRequest } from '../types/autofocus.type'
import { CalibrationFrame, CalibrationFrameGroup } from '../types/calibration.types'
import { Camera, CameraStartCapture } from '../types/camera.types'
import { Device, INDIProperty, INDISendProperty } from '../types/device.types'
@@ -635,6 +636,16 @@ export class ApiService {
return this.http.put(`plate-solver?${query}`)
}
+ // AUTO FOCUS
+
+ autoFocusStart(camera: Camera, focuser: Focuser, request: AutoFocusRequest) {
+ return this.http.put(`auto-focus/${camera.name}/${focuser.name}/start`, request)
+ }
+
+ autoFocusStop(camera: Camera) {
+ return this.http.put(`auto-focus/${camera.name}/stop`)
+ }
+
// PREFERENCE
clearPreferences() {
diff --git a/desktop/src/shared/services/browser-window.service.ts b/desktop/src/shared/services/browser-window.service.ts
index 5b2e9d4f5..ac77d57fd 100644
--- a/desktop/src/shared/services/browser-window.service.ts
+++ b/desktop/src/shared/services/browser-window.service.ts
@@ -104,6 +104,11 @@ export class BrowserWindowService {
this.openWindow({ ...options, id: 'sequencer', path: 'sequencer', data: undefined })
}
+ openAutoFocus(options: OpenWindowOptions = {}) {
+ Object.assign(options, { icon: 'auto-focus', width: 385, height: 370 })
+ this.openWindow({ ...options, id: 'auto-focus', path: 'auto-focus', data: undefined })
+ }
+
openFlatWizard(options: OpenWindowOptions = {}) {
Object.assign(options, { icon: 'star', width: 385, height: 370 })
this.openWindow({ ...options, id: 'flat-wizard', path: 'flat-wizard', data: undefined })
diff --git a/desktop/src/shared/services/preference.service.ts b/desktop/src/shared/services/preference.service.ts
index 98b902672..46eec8892 100644
--- a/desktop/src/shared/services/preference.service.ts
+++ b/desktop/src/shared/services/preference.service.ts
@@ -2,6 +2,7 @@ import { Injectable } from '@angular/core'
import { SkyAtlasPreference } from '../../app/atlas/atlas.component'
import { AlignmentPreference, EMPTY_ALIGNMENT_PREFERENCE } from '../types/alignment.types'
import { EMPTY_LOCATION, Location } from '../types/atlas.types'
+import { AutoFocusPreference, EMPTY_AUTO_FOCUS_PREFERENCE } from '../types/autofocus.type'
import { CalibrationPreference } from '../types/calibration.types'
import { Camera, CameraPreference, CameraStartCapture, EMPTY_CAMERA_PREFERENCE } from '../types/camera.types'
import { Device } from '../types/device.types'
@@ -63,6 +64,10 @@ export class PreferenceService {
return new PreferenceData(this.storage, `camera.${camera.name}.tppa`, () => this.cameraPreference(camera).get())
}
+ cameraStartCaptureForAutoFocus(camera: Camera) {
+ return new PreferenceData(this.storage, `camera.${camera.name}.autoFocus`, () => this.cameraPreference(camera).get())
+ }
+
plateSolverPreference(type: PlateSolverType) {
return new PreferenceData(this.storage, `plateSolver.${type}`, () => { ...EMPTY_PLATE_SOLVER_PREFERENCE, type })
}
@@ -92,4 +97,5 @@ export class PreferenceService {
readonly alignmentPreference = new PreferenceData(this.storage, 'alignment', () => structuredClone(EMPTY_ALIGNMENT_PREFERENCE))
readonly imageFOVs = new PreferenceData(this.storage, 'image.fovs', () => [])
readonly calibrationPreference = new PreferenceData(this.storage, 'calibration', () => {})
-}
\ No newline at end of file
+ readonly autoFocusPreference = new PreferenceData(this.storage, 'autoFocus', () => structuredClone(EMPTY_AUTO_FOCUS_PREFERENCE))
+}
diff --git a/desktop/src/shared/types/autofocus.type.ts b/desktop/src/shared/types/autofocus.type.ts
new file mode 100644
index 000000000..48f3847b6
--- /dev/null
+++ b/desktop/src/shared/types/autofocus.type.ts
@@ -0,0 +1,13 @@
+import { CameraStartCapture } from './camera.types'
+
+export interface AutoFocusRequest {
+ capture: CameraStartCapture
+}
+
+export interface AutoFocusPreference {
+
+}
+
+export const EMPTY_AUTO_FOCUS_PREFERENCE: AutoFocusPreference = {
+
+}
diff --git a/desktop/src/shared/types/camera.types.ts b/desktop/src/shared/types/camera.types.ts
index e97040128..056ccc482 100644
--- a/desktop/src/shared/types/camera.types.ts
+++ b/desktop/src/shared/types/camera.types.ts
@@ -3,7 +3,7 @@ import { Thermometer } from './auxiliary.types'
import { CompanionDevice, Device, PropertyState } from './device.types'
import { GuideOutput } from './guider.types'
-export type CameraDialogMode = 'CAPTURE' | 'SEQUENCER' | 'FLAT_WIZARD' | 'TPPA' | 'DARV'
+export type CameraDialogMode = 'CAPTURE' | 'SEQUENCER' | 'FLAT_WIZARD' | 'TPPA' | 'DARV' | 'AUTO_FOCUS'
export type FrameType = 'LIGHT' | 'DARK' | 'FLAT' | 'BIAS'
diff --git a/desktop/src/shared/types/home.types.ts b/desktop/src/shared/types/home.types.ts
index fd22c33fc..0885999c1 100644
--- a/desktop/src/shared/types/home.types.ts
+++ b/desktop/src/shared/types/home.types.ts
@@ -5,7 +5,8 @@ import { Rotator } from './rotator.types'
import { FilterWheel } from './wheel.types'
export type HomeWindowType = 'CAMERA' | 'MOUNT' | 'GUIDER' | 'WHEEL' | 'FOCUSER' | 'DOME' | 'ROTATOR' | 'SWITCH' |
- 'SKY_ATLAS' | 'ALIGNMENT' | 'SEQUENCER' | 'IMAGE' | 'FRAMING' | 'INDI' | 'SETTINGS' | 'CALCULATOR' | 'ABOUT' | 'FLAT_WIZARD'
+ 'SKY_ATLAS' | 'ALIGNMENT' | 'SEQUENCER' | 'IMAGE' | 'FRAMING' | 'INDI' | 'SETTINGS' | 'CALCULATOR' | 'ABOUT' | 'FLAT_WIZARD' |
+ 'AUTO_FOCUS'
export const CONNECTION_TYPES = ['INDI', 'ALPACA'] as const
From 908d7b801951650e986d587590aeaf0bbaf2be93 Mon Sep 17 00:00:00 2001
From: tiagohm
Date: Sun, 26 May 2024 21:34:48 -0300
Subject: [PATCH 24/45] [ci]: Remove wcstools
---
.github/workflows/ci.yml | 3 ---
1 file changed, 3 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 5bc372b73..dc6b32b0e 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -26,9 +26,6 @@ jobs:
distribution: 'zulu'
cache: gradle
- - name: Install wcstools
- run: sudo apt install -y wcstools
-
- name: Grant execute permission for gradlew
run: chmod +x gradlew
From a2b5000a04c47c23d52a353680a898fedde906f7 Mon Sep 17 00:00:00 2001
From: tiagohm
Date: Mon, 27 May 2024 01:11:41 -0300
Subject: [PATCH 25/45] [api][desktop]: Device Chooser
---
.../nebulosa/api/indi/INDIController.kt | 15 +++
.../kotlin/nebulosa/api/indi/INDIService.kt | 8 ++
.../app/alignment/alignment.component.html | 18 +--
.../src/app/alignment/alignment.component.ts | 23 ----
desktop/src/app/app.component.ts | 8 +-
desktop/src/app/app.module.ts | 4 +-
desktop/src/app/atlas/atlas.component.ts | 4 +-
.../app/autofocus/autofocus.component.html | 9 +-
.../src/app/autofocus/autofocus.component.ts | 82 +++++++++----
desktop/src/app/camera/camera.component.ts | 37 +++---
.../filterwheel/filterwheel.component.html | 4 +-
.../flat-wizard/flat-wizard.component.html | 13 +-
.../app/flat-wizard/flat-wizard.component.ts | 10 --
desktop/src/app/guider/guider.component.html | 6 +-
desktop/src/app/guider/guider.component.ts | 8 --
desktop/src/app/home/home.component.html | 3 +-
desktop/src/app/home/home.component.ts | 98 ++++++++++-----
desktop/src/app/image/image.component.ts | 43 ++++---
desktop/src/app/mount/mount.component.ts | 4 +-
.../app/sequencer/sequencer.component.html | 10 +-
.../device-chooser.component.html} | 2 +-
.../device-chooser.component.scss} | 0
.../device-chooser.component.ts | 114 ++++++++++++++++++
.../device-list-button.component.ts | 54 ---------
.../device-list-menu.component.scss | 5 +
.../device-list-menu.component.ts | 20 ++-
.../dialog-menu/dialog-menu.component.ts | 6 +-
.../menu-item/menu-item.component.html | 5 +-
.../menu-item/menu-item.component.ts | 30 +++--
.../slide-menu/slide-menu.component.html | 2 +-
.../slide-menu/slide-menu.component.ts | 12 +-
desktop/src/shared/constants.ts | 4 +-
desktop/src/shared/services/api.service.ts | 13 +-
desktop/src/shared/types/app.types.ts | 13 +-
desktop/src/shared/types/auxiliary.types.ts | 4 +
desktop/src/shared/types/camera.types.ts | 10 +-
desktop/src/shared/types/device.types.ts | 6 +
desktop/src/shared/types/focuser.types.ts | 4 +
desktop/src/shared/types/home.types.ts | 6 +-
desktop/src/shared/types/mount.types.ts | 5 +
desktop/src/shared/types/rotator.types.ts | 4 +
desktop/src/shared/types/wheel.types.ts | 6 +-
desktop/src/styles.scss | 2 +
43 files changed, 445 insertions(+), 289 deletions(-)
rename desktop/src/shared/components/{device-list-button/device-list-button.component.html => device-chooser/device-chooser.component.html} (84%)
rename desktop/src/shared/components/{device-list-button/device-list-button.component.scss => device-chooser/device-chooser.component.scss} (100%)
create mode 100644 desktop/src/shared/components/device-chooser/device-chooser.component.ts
delete mode 100644 desktop/src/shared/components/device-list-button/device-list-button.component.ts
diff --git a/api/src/main/kotlin/nebulosa/api/indi/INDIController.kt b/api/src/main/kotlin/nebulosa/api/indi/INDIController.kt
index f66afda59..ca889fd60 100644
--- a/api/src/main/kotlin/nebulosa/api/indi/INDIController.kt
+++ b/api/src/main/kotlin/nebulosa/api/indi/INDIController.kt
@@ -11,6 +11,21 @@ class INDIController(
private val indiService: INDIService,
) {
+ @GetMapping("{device}")
+ fun device(device: Device): Device {
+ return device
+ }
+
+ @PutMapping("{device}/connect")
+ fun connect(device: Device) {
+ indiService.connect(device)
+ }
+
+ @PutMapping("{device}/disconnect")
+ fun disconnect(device: Device) {
+ indiService.disconnect(device)
+ }
+
@GetMapping("{device}/properties")
fun properties(device: Device): Collection> {
return indiService.properties(device)
diff --git a/api/src/main/kotlin/nebulosa/api/indi/INDIService.kt b/api/src/main/kotlin/nebulosa/api/indi/INDIService.kt
index 0fe78fc8c..91f4c5d92 100644
--- a/api/src/main/kotlin/nebulosa/api/indi/INDIService.kt
+++ b/api/src/main/kotlin/nebulosa/api/indi/INDIService.kt
@@ -18,6 +18,14 @@ class INDIService(
indiEventHandler.unregisterDevice(device)
}
+ fun connect(device: Device) {
+ device.connect()
+ }
+
+ fun disconnect(device: Device) {
+ device.disconnect()
+ }
+
fun messages(): List {
return indiEventHandler.messages()
}
diff --git a/desktop/src/app/alignment/alignment.component.html b/desktop/src/app/alignment/alignment.component.html
index d57ed21c1..d42354c7b 100644
--- a/desktop/src/app/alignment/alignment.component.html
+++ b/desktop/src/app/alignment/alignment.component.html
@@ -1,22 +1,14 @@
-
+
@if (tab === 0) {
-
-
-
+
} @else {
-
-
-
+
}
diff --git a/desktop/src/app/alignment/alignment.component.ts b/desktop/src/app/alignment/alignment.component.ts
index fe628cd54..d00c70296 100644
--- a/desktop/src/app/alignment/alignment.component.ts
+++ b/desktop/src/app/alignment/alignment.component.ts
@@ -109,7 +109,6 @@ export class AlignmentComponent implements AfterViewInit, OnDestroy {
}
this.cameras.splice(index, 1)
- this.cameras.sort(deviceComparator)
}
})
})
@@ -139,7 +138,6 @@ export class AlignmentComponent implements AfterViewInit, OnDestroy {
}
this.mounts.splice(index, 1)
- this.mounts.sort(deviceComparator)
}
})
})
@@ -169,7 +167,6 @@ export class AlignmentComponent implements AfterViewInit, OnDestroy {
}
this.guideOutputs.splice(index, 1)
- this.guideOutputs.sort(deviceComparator)
}
})
})
@@ -265,26 +262,6 @@ export class AlignmentComponent implements AfterViewInit, OnDestroy {
}
}
- mountConnect() {
- if (this.mount.id) {
- if (this.mount.connected) {
- this.api.mountDisconnect(this.mount)
- } else {
- this.api.mountConnect(this.mount)
- }
- }
- }
-
- guideOutputConnect() {
- if (this.guideOutput.id) {
- if (this.guideOutput.connected) {
- this.api.guideOutputDisconnect(this.guideOutput)
- } else {
- this.api.guideOutputConnect(this.guideOutput)
- }
- }
- }
-
async showCameraDialog() {
if (this.camera.id) {
if (this.tab === 0) {
diff --git a/desktop/src/app/app.component.ts b/desktop/src/app/app.component.ts
index a9058300f..ba2f53ea7 100644
--- a/desktop/src/app/app.component.ts
+++ b/desktop/src/app/app.component.ts
@@ -1,14 +1,10 @@
import { AfterViewInit, Component } from '@angular/core'
import { Title } from '@angular/platform-browser'
import { ActivatedRoute } from '@angular/router'
-import { MenuItem } from 'primeng/api'
import { APP_CONFIG } from '../environments/environment'
+import { MenuItem } from '../shared/components/menu-item/menu-item.component'
import { ElectronService } from '../shared/services/electron.service'
-export interface ExtendedMenuItem extends MenuItem {
- badgeSeverity?: 'success' | 'info' | 'warning' | 'danger'
-}
-
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
@@ -21,7 +17,7 @@ export class AppComponent implements AfterViewInit {
readonly modal = window.options.modal ?? false
subTitle? = ''
backgroundColor = '#212121'
- topMenu: ExtendedMenuItem[] = []
+ topMenu: MenuItem[] = []
showTopBar = true
get title() {
diff --git a/desktop/src/app/app.module.ts b/desktop/src/app/app.module.ts
index 13a469da0..dd8e85990 100644
--- a/desktop/src/app/app.module.ts
+++ b/desktop/src/app/app.module.ts
@@ -42,7 +42,7 @@ import { ToastModule } from 'primeng/toast'
import { TooltipModule } from 'primeng/tooltip'
import { TreeModule } from 'primeng/tree'
import { CameraExposureComponent } from '../shared/components/camera-exposure/camera-exposure.component'
-import { DeviceListButtonComponent } from '../shared/components/device-list-button/device-list-button.component'
+import { DeviceChooserComponent } from '../shared/components/device-chooser/device-chooser.component'
import { DeviceListMenuComponent } from '../shared/components/device-list-menu/device-list-menu.component'
import { DialogMenuComponent } from '../shared/components/dialog-menu/dialog-menu.component'
import { HistogramComponent } from '../shared/components/histogram/histogram.component'
@@ -97,7 +97,7 @@ import { SettingsComponent } from './settings/settings.component'
CalibrationComponent,
CameraComponent,
CameraExposureComponent,
- DeviceListButtonComponent,
+ DeviceChooserComponent,
DeviceListMenuComponent,
DialogMenuComponent,
EnumPipe,
diff --git a/desktop/src/app/atlas/atlas.component.ts b/desktop/src/app/atlas/atlas.component.ts
index 4e95d9c85..2397b406d 100644
--- a/desktop/src/app/atlas/atlas.component.ts
+++ b/desktop/src/app/atlas/atlas.component.ts
@@ -8,7 +8,7 @@ import { ListboxChangeEvent } from 'primeng/listbox'
import { OverlayPanel } from 'primeng/overlaypanel'
import { Subscription, timer } from 'rxjs'
import { DeviceListMenuComponent } from '../../shared/components/device-list-menu/device-list-menu.component'
-import { ExtendedMenuItem } from '../../shared/components/menu-item/menu-item.component'
+import { MenuItem } from '../../shared/components/menu-item/menu-item.component'
import { ONE_DECIMAL_PLACE_FORMATTER, TWO_DIGITS_FORMATTER } from '../../shared/constants'
import { SkyObjectPipe } from '../../shared/pipes/skyObject.pipe'
import { ApiService } from '../../shared/services/api.service'
@@ -406,7 +406,7 @@ export class AtlasComponent implements OnInit, AfterContentInit, AfterViewInit,
'ONEWEB', 'SCIENCE', 'STARLINK', 'STATIONS', 'VISUAL'
]
- readonly ephemerisModel: ExtendedMenuItem[] = [
+ readonly ephemerisModel: MenuItem[] = [
{
icon: 'mdi mdi-magnify',
label: 'Find sky objects around this object',
diff --git a/desktop/src/app/autofocus/autofocus.component.html b/desktop/src/app/autofocus/autofocus.component.html
index 33e4e855a..7edd35cf2 100644
--- a/desktop/src/app/autofocus/autofocus.component.html
+++ b/desktop/src/app/autofocus/autofocus.component.html
@@ -1,10 +1,9 @@
\ No newline at end of file
diff --git a/desktop/src/app/autofocus/autofocus.component.ts b/desktop/src/app/autofocus/autofocus.component.ts
index 5ccb2ac50..ec576a103 100644
--- a/desktop/src/app/autofocus/autofocus.component.ts
+++ b/desktop/src/app/autofocus/autofocus.component.ts
@@ -39,6 +39,64 @@ export class AutoFocusComponent implements AfterViewInit, OnDestroy {
) {
app.title = 'Auto Focus'
+ electron.on('CAMERA.UPDATED', event => {
+ if (event.device.id === this.camera.id) {
+ ngZone.run(() => {
+ Object.assign(this.camera, event.device)
+ })
+ }
+ })
+
+ electron.on('CAMERA.ATTACHED', event => {
+ ngZone.run(() => {
+ this.cameras.push(event.device)
+ this.cameras.sort(deviceComparator)
+ })
+ })
+
+ electron.on('CAMERA.DETACHED', event => {
+ ngZone.run(() => {
+ const index = this.cameras.findIndex(e => e.id === event.device.id)
+
+ if (index >= 0) {
+ if (this.cameras[index] === this.camera) {
+ Object.assign(this.camera, this.cameras[0] ?? EMPTY_CAMERA)
+ }
+
+ this.cameras.splice(index, 1)
+ }
+ })
+ })
+
+ electron.on('FOCUSER.UPDATED', event => {
+ if (event.device.id === this.focuser.id) {
+ ngZone.run(() => {
+ Object.assign(this.focuser, event.device)
+ })
+ }
+ })
+
+ electron.on('FOCUSER.ATTACHED', event => {
+ ngZone.run(() => {
+ this.focusers.push(event.device)
+ this.focusers.sort(deviceComparator)
+ })
+ })
+
+ electron.on('FOCUSER.DETACHED', event => {
+ ngZone.run(() => {
+ const index = this.focusers.findIndex(e => e.id === event.device.id)
+
+ if (index >= 0) {
+ if (this.focusers[index] === this.focuser) {
+ Object.assign(this.focuser, this.focusers[0] ?? EMPTY_FOCUSER)
+ }
+
+ this.focusers.splice(index, 1)
+ }
+ })
+ })
+
this.loadPreference()
}
@@ -59,18 +117,6 @@ export class AutoFocusComponent implements AfterViewInit, OnDestroy {
}
}
- cameraConnect(camera?: Camera) {
- camera ??= this.camera
-
- if (camera.id) {
- if (camera.connected) {
- this.api.cameraDisconnect(camera)
- } else {
- this.api.cameraConnect(camera)
- }
- }
- }
-
async focuserChanged() {
if (this.focuser.id) {
const focuser = await this.api.focuser(this.focuser.id)
@@ -78,18 +124,6 @@ export class AutoFocusComponent implements AfterViewInit, OnDestroy {
}
}
- focuserConnect(focuser?: Focuser) {
- focuser ??= this.focuser
-
- if (focuser.id) {
- if (focuser.connected) {
- this.api.focuserDisconnect(focuser)
- } else {
- this.api.focuserConnect(focuser)
- }
- }
- }
-
async showCameraDialog() {
if (this.camera.id) {
if (await CameraComponent.showAsDialog(this.browserWindow, 'AUTO_FOCUS', this.camera, this.request.capture)) {
diff --git a/desktop/src/app/camera/camera.component.ts b/desktop/src/app/camera/camera.component.ts
index e056f9565..0df5f366f 100644
--- a/desktop/src/app/camera/camera.component.ts
+++ b/desktop/src/app/camera/camera.component.ts
@@ -2,7 +2,6 @@ import { AfterContentInit, Component, HostListener, NgZone, OnDestroy, ViewChild
import { ActivatedRoute } from '@angular/router'
import { MenuItem } from 'primeng/api'
import { CameraExposureComponent } from '../../shared/components/camera-exposure/camera-exposure.component'
-import { ExtendedMenuItem } from '../../shared/components/menu-item/menu-item.component'
import { SlideMenuItem, SlideMenuItemCommandEvent } from '../../shared/components/slide-menu/slide-menu.component'
import { SEPARATOR_MENU_ITEM } from '../../shared/constants'
import { ApiService } from '../../shared/services/api.service'
@@ -76,9 +75,9 @@ export class CameraComponent implements AfterContentInit, OnDestroy {
showDitherDialog = false
- calibrationModel: ExtendedMenuItem[] = []
+ calibrationModel: MenuItem[] = []
- readonly cameraModel: ExtendedMenuItem[] = [
+ readonly cameraModel: MenuItem[] = [
{
icon: 'icomoon random-dither',
label: 'Dither',
@@ -89,26 +88,26 @@ export class CameraComponent implements AfterContentInit, OnDestroy {
{
icon: 'mdi mdi-connection',
label: 'Snoop Devices',
- menu: [
+ subMenu: [
{
icon: 'mdi mdi-telescope',
label: 'Mount',
- menu: [],
+ subMenu: [],
},
{
icon: 'mdi mdi-palette',
label: 'Filter Wheel',
- menu: [],
+ subMenu: [],
},
{
icon: 'mdi mdi-image-filter-center-focus',
label: 'Focuser',
- menu: [],
+ subMenu: [],
},
{
icon: 'mdi mdi-rotate-right',
label: 'Rotator',
- menu: [],
+ subMenu: [],
},
]
},
@@ -324,7 +323,7 @@ export class CameraComponent implements AfterContentInit, OnDestroy {
}
const makeItem = (checked: boolean, command: () => void, device?: Device) => {
- return {
+ return
-
+
diff --git a/desktop/src/app/flat-wizard/flat-wizard.component.ts b/desktop/src/app/flat-wizard/flat-wizard.component.ts
index 65ef1982e..c4edc631f 100644
--- a/desktop/src/app/flat-wizard/flat-wizard.component.ts
+++ b/desktop/src/app/flat-wizard/flat-wizard.component.ts
@@ -108,7 +108,6 @@ export class FlatWizardComponent implements AfterViewInit, OnDestroy {
}
this.cameras.splice(index, 1)
- this.cameras.sort(deviceComparator)
}
})
})
@@ -139,7 +138,6 @@ export class FlatWizardComponent implements AfterViewInit, OnDestroy {
}
this.wheels.splice(index, 1)
- this.wheels.sort(deviceComparator)
}
})
})
@@ -167,14 +165,6 @@ export class FlatWizardComponent implements AfterViewInit, OnDestroy {
}
}
- wheelConnect() {
- if (this.wheel.connected) {
- this.api.wheelDisconnect(this.wheel)
- } else {
- this.api.wheelConnect(this.wheel)
- }
- }
-
private updateEntryFromCamera(camera?: Camera) {
if (camera && camera.connected) {
updateCameraStartCaptureFromCamera(this.request.capture, camera)
diff --git a/desktop/src/app/guider/guider.component.html b/desktop/src/app/guider/guider.component.html
index 067d5df5c..9202e47e7 100644
--- a/desktop/src/app/guider/guider.component.html
+++ b/desktop/src/app/guider/guider.component.html
@@ -139,11 +139,7 @@
diff --git a/desktop/src/app/guider/guider.component.ts b/desktop/src/app/guider/guider.component.ts
index a3c79fd0c..14f72e44f 100644
--- a/desktop/src/app/guider/guider.component.ts
+++ b/desktop/src/app/guider/guider.component.ts
@@ -374,14 +374,6 @@ export class GuiderComponent implements AfterViewInit, OnDestroy {
}
}
- connectGuideOutput() {
- if (this.guideOutputConnected) {
- this.api.guideOutputDisconnect(this.guideOutput!)
- } else {
- this.api.guideOutputConnect(this.guideOutput!)
- }
- }
-
guidePulseStart(...directions: GuideDirection[]) {
for (const direction of directions) {
switch (direction) {
diff --git a/desktop/src/app/home/home.component.html b/desktop/src/app/home/home.component.html
index b2dfa5193..2fa0bf5cc 100644
--- a/desktop/src/app/home/home.component.html
+++ b/desktop/src/app/home/home.component.html
@@ -204,5 +204,6 @@
-
+
\ No newline at end of file
diff --git a/desktop/src/app/home/home.component.ts b/desktop/src/app/home/home.component.ts
index 1dba47feb..d9dd33d22 100644
--- a/desktop/src/app/home/home.component.ts
+++ b/desktop/src/app/home/home.component.ts
@@ -1,7 +1,7 @@
import { AfterContentInit, Component, HostListener, NgZone, OnDestroy, ViewChild } from '@angular/core'
import { dirname } from 'path'
-import { DeviceListMenuComponent } from '../../shared/components/device-list-menu/device-list-menu.component'
-import { DialogMenuComponent } from '../../shared/components/dialog-menu/dialog-menu.component'
+import { DeviceChooserComponent } from '../../shared/components/device-chooser/device-chooser.component'
+import { DeviceConnectionCommandEvent, DeviceListMenuComponent } from '../../shared/components/device-list-menu/device-list-menu.component'
import { SlideMenuItem } from '../../shared/components/slide-menu/slide-menu.component'
import { ApiService } from '../../shared/services/api.service'
import { BrowserWindowService } from '../../shared/services/browser-window.service'
@@ -15,7 +15,6 @@ import { CONNECTION_TYPES, ConnectionDetails, EMPTY_CONNECTION_DETAILS, HomeWind
import { Mount } from '../../shared/types/mount.types'
import { Rotator } from '../../shared/types/rotator.types'
import { FilterWheel } from '../../shared/types/wheel.types'
-import { deviceComparator } from '../../shared/utils/comparators'
import { AppComponent } from '../app.component'
type MappedDevice = {
@@ -34,7 +33,7 @@ type MappedDevice = {
export class HomeComponent implements AfterContentInit, OnDestroy {
@ViewChild('deviceMenu')
- private readonly deviceMenu!: DialogMenuComponent
+ private readonly deviceMenu!: DeviceListMenuComponent
@ViewChild('imageMenu')
private readonly imageMenu!: DeviceListMenuComponent
@@ -137,6 +136,7 @@ export class HomeComponent implements AfterContentInit, OnDestroy {
type: K,
onAdd: (device: MappedDevice[K]) => number,
onRemove: (device: MappedDevice[K]) => number,
+ onUpdate: (device: MappedDevice[K]) => void,
) {
this.electron.on(`${type}.ATTACHED`, event => {
this.ngZone.run(() => {
@@ -149,6 +149,12 @@ export class HomeComponent implements AfterContentInit, OnDestroy {
onRemove(event.device as never)
})
})
+
+ this.electron.on(`${type}.UPDATED`, event => {
+ this.ngZone.run(() => {
+ onUpdate(event.device as never)
+ })
+ })
}
constructor(
@@ -163,53 +169,78 @@ export class HomeComponent implements AfterContentInit, OnDestroy {
app.title = 'Nebulosa'
this.startListening('CAMERA',
- (device) => {
+ device => {
return this.cameras.push(device)
},
- (device) => {
+ device => {
this.cameras.splice(this.cameras.findIndex(e => e.id === device.id), 1)
return this.cameras.length
},
+ device => {
+ const found = this.cameras.find(e => e.id === device.id)
+ if (!found) return
+ Object.assign(found, device)
+ }
)
this.startListening('MOUNT',
- (device) => {
+ device => {
return this.mounts.push(device)
},
- (device) => {
+ device => {
this.mounts.splice(this.mounts.findIndex(e => e.id === device.id), 1)
return this.mounts.length
},
+ device => {
+ const found = this.mounts.find(e => e.id === device.id)
+ if (!found) return
+ Object.assign(found, device)
+ }
)
this.startListening('FOCUSER',
- (device) => {
+ device => {
return this.focusers.push(device)
},
- (device) => {
+ device => {
this.focusers.splice(this.focusers.findIndex(e => e.id === device.id), 1)
return this.focusers.length
},
+ device => {
+ const found = this.focusers.find(e => e.id === device.id)
+ if (!found) return
+ Object.assign(found, device)
+ }
)
this.startListening('WHEEL',
- (device) => {
+ device => {
return this.wheels.push(device)
},
- (device) => {
+ device => {
this.wheels.splice(this.wheels.findIndex(e => e.id === device.id), 1)
return this.wheels.length
},
+ device => {
+ const found = this.wheels.find(e => e.id === device.id)
+ if (!found) return
+ Object.assign(found, device)
+ }
)
this.startListening('ROTATOR',
- (device) => {
+ device => {
return this.rotators.push(device)
},
- (device) => {
+ device => {
this.rotators.splice(this.rotators.findIndex(e => e.id === device.id), 1)
return this.rotators.length
},
+ device => {
+ const found = this.rotators.find(e => e.id === device.id)
+ if (!found) return
+ Object.assign(found, device)
+ }
)
electron.on('CONNECTION.CLOSED', event => {
@@ -323,7 +354,23 @@ export class HomeComponent implements AfterContentInit, OnDestroy {
}
}
- private openDevice (type: K, header: string) {
+ 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 async deviceConnected(event: DeviceConnectionCommandEvent) {
+ DeviceChooserComponent.handleConnectDevice(this.api, event.device, event.item)
+ }
+
+ protected async deviceDisconnected(event: DeviceConnectionCommandEvent) {
+ DeviceChooserComponent.handleDisconnectDevice(this.api, event.device, event.item)
+ }
+
+ private async openDevice(type: K) {
this.deviceModel.length = 0
const devices: Device[] = type === 'CAMERA' ? this.cameras
@@ -334,20 +381,13 @@ export class HomeComponent implements AfterContentInit, OnDestroy {
: []
if (devices.length === 0) return
- if (devices.length === 1) return this.openDeviceWindow(type, devices[0] as any)
-
- for (const device of [...devices].sort(deviceComparator)) {
- this.deviceModel.push({
- icon: 'mdi mdi-connection',
- label: device.name,
- command: () => {
- this.openDeviceWindow(type, device as any)
- }
- })
- }
- this.deviceMenu.header = header
- this.deviceMenu.show()
+ this.deviceMenu.header = type
+ const device = await this.deviceMenu.show(devices)
+
+ if (device && device !== 'NONE') {
+ this.openDeviceWindow(type, device as any)
+ }
}
private openDeviceWindow(type: K, device: MappedDevice[K]) {
@@ -396,7 +436,7 @@ export class HomeComponent implements AfterContentInit, OnDestroy {
case 'FOCUSER':
case 'WHEEL':
case 'ROTATOR':
- this.openDevice(type, type)
+ this.openDevice(type)
break
case 'GUIDER':
this.browserWindow.openGuider({ bringToFront: true })
diff --git a/desktop/src/app/image/image.component.ts b/desktop/src/app/image/image.component.ts
index 5866d9700..74d19f19b 100644
--- a/desktop/src/app/image/image.component.ts
+++ b/desktop/src/app/image/image.component.ts
@@ -8,7 +8,7 @@ import { basename, dirname, extname } from 'path'
import { ContextMenu } from 'primeng/contextmenu'
import { DeviceListMenuComponent } from '../../shared/components/device-list-menu/device-list-menu.component'
import { HistogramComponent } from '../../shared/components/histogram/histogram.component'
-import { ExtendedMenuItem } from '../../shared/components/menu-item/menu-item.component'
+import { MenuItem } from '../../shared/components/menu-item/menu-item.component'
import { SlideMenuItem } from '../../shared/components/slide-menu/slide-menu.component'
import { SEPARATOR_MENU_ITEM } from '../../shared/constants'
import { ApiService } from '../../shared/services/api.service'
@@ -16,7 +16,6 @@ import { BrowserWindowService } from '../../shared/services/browser-window.servi
import { ElectronService } from '../../shared/services/electron.service'
import { PreferenceService } from '../../shared/services/preference.service'
import { PrimeService } from '../../shared/services/prime.service'
-import { CheckableMenuItem, ToggleableMenuItem } from '../../shared/types/app.types'
import { Angle, AstronomicalObject, DeepSkyObject, EquatorialCoordinateJ2000, Star } from '../../shared/types/atlas.types'
import { Camera } from '../../shared/types/camera.types'
import { DEFAULT_FOV, EMPTY_IMAGE_SOLVED, FOV, IMAGE_STATISTICS_BIT_OPTIONS, ImageAnnotation, ImageAnnotationDialog, ImageChannel, ImageData, ImageDetectStars, ImageFITSHeadersDialog, ImageFOVDialog, ImageInfo, ImageROI, ImageSCNRDialog, ImageSaveDialog, ImageSolved, ImageSolverDialog, ImageStatisticsBitOption, ImageStretchDialog, ImageTransformation, SCNR_PROTECTION_METHODS } from '../../shared/types/image.types'
@@ -169,7 +168,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy {
transformation: this.transformation
}
- private readonly saveAsMenuItem: ExtendedMenuItem = {
+ private readonly saveAsMenuItem: MenuItem = {
label: 'Save as...',
icon: 'mdi mdi-content-save',
command: async () => {
@@ -191,7 +190,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy {
},
}
- private readonly plateSolveMenuItem: ExtendedMenuItem = {
+ private readonly plateSolveMenuItem: MenuItem = {
label: 'Plate Solve',
icon: 'mdi mdi-sigma',
command: () => {
@@ -199,7 +198,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy {
},
}
- private readonly stretchMenuItem: ExtendedMenuItem = {
+ private readonly stretchMenuItem: MenuItem = {
label: 'Stretch',
icon: 'mdi mdi-chart-histogram',
command: () => {
@@ -207,7 +206,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy {
},
}
- private readonly autoStretchMenuItem: CheckableMenuItem = {
+ private readonly autoStretchMenuItem: MenuItem = {
id: 'auto-stretch-menuitem',
label: 'Auto stretch',
icon: 'mdi mdi-auto-fix',
@@ -217,7 +216,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy {
},
}
- private readonly scnrMenuItem: ExtendedMenuItem = {
+ private readonly scnrMenuItem: MenuItem = {
label: 'SCNR',
icon: 'mdi mdi-palette',
disabled: true,
@@ -226,7 +225,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy {
},
}
- private readonly horizontalMirrorMenuItem: CheckableMenuItem = {
+ private readonly horizontalMirrorMenuItem: MenuItem = {
label: 'Horizontal mirror',
icon: 'mdi mdi-flip-horizontal',
checked: false,
@@ -237,7 +236,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy {
},
}
- private readonly verticalMirrorMenuItem: CheckableMenuItem = {
+ private readonly verticalMirrorMenuItem: MenuItem = {
label: 'Vertical mirror',
icon: 'mdi mdi-flip-vertical',
checked: false,
@@ -248,7 +247,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy {
},
}
- private readonly invertMenuItem: CheckableMenuItem = {
+ private readonly invertMenuItem: MenuItem = {
label: 'Invert',
icon: 'mdi mdi-invert-colors',
checked: false,
@@ -257,13 +256,13 @@ export class ImageComponent implements AfterViewInit, OnDestroy {
},
}
- private readonly calibrationMenuItem: ExtendedMenuItem = {
+ private readonly calibrationMenuItem: MenuItem = {
label: 'Calibration',
icon: 'mdi mdi-wrench',
items: [],
}
- private readonly statisticsMenuItem: ExtendedMenuItem = {
+ private readonly statisticsMenuItem: MenuItem = {
icon: 'mdi mdi-chart-histogram',
label: 'Statistics',
command: () => {
@@ -272,7 +271,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy {
},
}
- private readonly fitsHeaderMenuItem: ExtendedMenuItem = {
+ private readonly fitsHeaderMenuItem: MenuItem = {
icon: 'mdi mdi-list-box',
label: 'FITS Header',
command: () => {
@@ -280,7 +279,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy {
},
}
- private readonly pointMountHereMenuItem: ExtendedMenuItem = {
+ private readonly pointMountHereMenuItem: MenuItem = {
label: 'Point mount here',
icon: 'mdi mdi-telescope',
disabled: true,
@@ -291,7 +290,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy {
},
}
- private readonly frameAtThisCoordinateMenuItem: ExtendedMenuItem = {
+ private readonly frameAtThisCoordinateMenuItem: MenuItem = {
label: 'Frame at this coordinate',
icon: 'mdi mdi-image',
disabled: true,
@@ -304,7 +303,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy {
},
}
- private readonly crosshairMenuItem: CheckableMenuItem = {
+ private readonly crosshairMenuItem: MenuItem = {
label: 'Crosshair',
icon: 'mdi mdi-bullseye',
checked: false,
@@ -313,7 +312,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy {
},
}
- private readonly annotationMenuItem: ToggleableMenuItem = {
+ private readonly annotationMenuItem: MenuItem = {
label: 'Annotate',
icon: 'mdi mdi-marker',
disabled: true,
@@ -328,7 +327,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy {
},
}
- private readonly detectStarsMenuItem: ToggleableMenuItem = {
+ private readonly detectStarsMenuItem: MenuItem = {
label: 'Detect stars',
icon: 'mdi mdi-creation',
disabled: false,
@@ -346,7 +345,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy {
},
}
- private readonly roiMenuItem: CheckableMenuItem = {
+ private readonly roiMenuItem: MenuItem = {
label: 'ROI',
icon: 'mdi mdi-select',
checked: false,
@@ -386,7 +385,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy {
},
}
- private readonly fovMenuItem: ExtendedMenuItem = {
+ private readonly fovMenuItem: MenuItem = {
label: 'Field of View',
icon: 'mdi mdi-camera-metering-spot',
command: () => {
@@ -398,7 +397,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy {
},
}
- private readonly overlayMenuItem: ExtendedMenuItem = {
+ private readonly overlayMenuItem: MenuItem = {
label: 'Overlay',
icon: 'mdi mdi-layers',
items: [
@@ -573,7 +572,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy {
const label = name ?? 'None'
const icon = name ? 'mdi mdi-wrench' : 'mdi mdi-close'
- return {
+ return {
label, icon,
checked: this.transformation.calibrationGroup === name,
disabled: this.calibrationViaCamera,
diff --git a/desktop/src/app/mount/mount.component.ts b/desktop/src/app/mount/mount.component.ts
index fa90749d8..c853d97cf 100644
--- a/desktop/src/app/mount/mount.component.ts
+++ b/desktop/src/app/mount/mount.component.ts
@@ -124,7 +124,7 @@ export class MountComponent implements AfterContentInit, OnDestroy {
{
icon: 'mdi mdi-crosshairs-gps',
label: 'Locations',
- menu: [
+ subMenu: [
{
icon: 'mdi mdi-crosshairs-gps',
label: 'Current location',
@@ -178,7 +178,7 @@ export class MountComponent implements AfterContentInit, OnDestroy {
{
icon: 'mdi mdi-crosshairs',
label: 'Intersection points',
- menu: [
+ subMenu: [
{
icon: 'mdi mdi-crosshairs-gps',
label: 'Meridian x Equator',
diff --git a/desktop/src/app/sequencer/sequencer.component.html b/desktop/src/app/sequencer/sequencer.component.html
index dfb9eabb2..afbb1ab77 100644
--- a/desktop/src/app/sequencer/sequencer.component.html
+++ b/desktop/src/app/sequencer/sequencer.component.html
@@ -136,11 +136,11 @@
diff --git a/desktop/src/shared/components/device-list-button/device-list-button.component.html b/desktop/src/shared/components/device-chooser/device-chooser.component.html
similarity index 84%
rename from desktop/src/shared/components/device-list-button/device-list-button.component.html
rename to desktop/src/shared/components/device-chooser/device-chooser.component.html
index e7e0351a9..10733a640 100644
--- a/desktop/src/shared/components/device-list-button/device-list-button.component.html
+++ b/desktop/src/shared/components/device-chooser/device-chooser.component.html
@@ -13,4 +13,4 @@
\ No newline at end of file
+ (deviceConnect)="deviceConnected($event)" (deviceDisconnect)="deviceDisconnected($event)" />
\ No newline at end of file
diff --git a/desktop/src/shared/components/device-list-button/device-list-button.component.scss b/desktop/src/shared/components/device-chooser/device-chooser.component.scss
similarity index 100%
rename from desktop/src/shared/components/device-list-button/device-list-button.component.scss
rename to desktop/src/shared/components/device-chooser/device-chooser.component.scss
diff --git a/desktop/src/shared/components/device-chooser/device-chooser.component.ts b/desktop/src/shared/components/device-chooser/device-chooser.component.ts
new file mode 100644
index 000000000..33c6a6087
--- /dev/null
+++ b/desktop/src/shared/components/device-chooser/device-chooser.component.ts
@@ -0,0 +1,114 @@
+import { Component, EventEmitter, Input, Output, ViewChild } from '@angular/core'
+import { ApiService } from '../../services/api.service'
+import { Device } from '../../types/device.types'
+import { DeviceConnectionCommandEvent, DeviceListMenuComponent } from '../device-list-menu/device-list-menu.component'
+import { MenuItem } from '../menu-item/menu-item.component'
+
+@Component({
+ selector: 'neb-device-chooser',
+ templateUrl: './device-chooser.component.html',
+ styleUrls: ['./device-chooser.component.scss'],
+})
+export class DeviceChooserComponent {
+
+ @Input({ required: true })
+ readonly title!: string
+
+ @Input()
+ readonly noDeviceMessage?: string
+
+ @Input({ required: true })
+ readonly icon!: string
+
+ @Input({ required: true })
+ readonly devices!: T[]
+
+ @Input()
+ readonly hasNone: boolean = false
+
+ @Input()
+ device?: T
+
+ @Output()
+ readonly deviceChange = new EventEmitter()
+
+ @Output()
+ readonly deviceConnect = new EventEmitter()
+
+ @Output()
+ readonly deviceDisconnect = new EventEmitter()
+
+ @ViewChild('deviceMenu')
+ private readonly deviceMenu!: DeviceListMenuComponent
+
+ constructor(private api: ApiService) { }
+
+ async show() {
+ const device = await this.deviceMenu.show(this.devices, this.device)
+
+ if (device) {
+ this.device = device === 'NONE' ? undefined : device
+ this.deviceChange.emit(this.device)
+ }
+ }
+
+ hide() {
+ this.deviceMenu.hide()
+ }
+
+ protected async deviceConnected(event: DeviceConnectionCommandEvent) {
+ const newEvent = await DeviceChooserComponent.handleConnectDevice(this.api, event.device, event.item)
+ if (newEvent) this.deviceConnect.emit(newEvent)
+ }
+
+ protected async deviceDisconnected(event: DeviceConnectionCommandEvent) {
+ const newEvent = await DeviceChooserComponent.handleDisconnectDevice(this.api, event.device, event.item)
+ if (newEvent) this.deviceDisconnect.emit(newEvent)
+ }
+
+ static async handleConnectDevice(api: ApiService, device: Device, item: MenuItem) {
+ await api.indiDeviceConnect(device)
+
+ item.disabled = true
+
+ return new Promise((resolve) => {
+ setTimeout(async () => {
+ Object.assign(device, await api.indiDevice(device))
+
+ if (device.connected) {
+ item.icon = 'mdi mdi-close'
+ item.toolbarButtonSeverity = 'danger'
+ item.label = 'Disconnect'
+ resolve({ device, item })
+ } else {
+ resolve(undefined)
+ }
+
+ item.disabled = false
+ }, 1000)
+ })
+ }
+
+ static async handleDisconnectDevice(api: ApiService, device: Device, item: MenuItem) {
+ await api.indiDeviceDisconnect(device)
+
+ item.disabled = true
+
+ return new Promise((resolve) => {
+ setTimeout(async () => {
+ Object.assign(device, await api.indiDevice(device))
+
+ if (!device.connected) {
+ item.icon = 'mdi mdi-connection'
+ item.toolbarButtonSeverity = 'info'
+ item.label = 'Connect'
+ resolve({ device, item })
+ } else {
+ resolve(undefined)
+ }
+
+ item.disabled = false
+ }, 1000)
+ })
+ }
+}
\ No newline at end of file
diff --git a/desktop/src/shared/components/device-list-button/device-list-button.component.ts b/desktop/src/shared/components/device-list-button/device-list-button.component.ts
deleted file mode 100644
index ce96deefc..000000000
--- a/desktop/src/shared/components/device-list-button/device-list-button.component.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-import { Component, EventEmitter, Input, Output, ViewChild } from '@angular/core'
-import { Device } from '../../types/device.types'
-import { DeviceListMenuComponent } from '../device-list-menu/device-list-menu.component'
-
-@Component({
- selector: 'neb-device-list-button',
- templateUrl: './device-list-button.component.html',
- styleUrls: ['./device-list-button.component.scss'],
-})
-export class DeviceListButtonComponent {
-
- @Input({ required: true })
- readonly title!: string
-
- @Input()
- readonly noDeviceMessage?: string
-
- @Input({ required: true })
- readonly icon!: string
-
- @Input({ required: true })
- readonly devices!: T[]
-
- @Input()
- readonly hasNone: boolean = false
-
- @Input()
- device?: T
-
- @Output()
- readonly deviceChange = new EventEmitter()
-
- @Output()
- readonly deviceConnect = new EventEmitter()
-
- @Output()
- readonly deviceDisconnect = new EventEmitter()
-
- @ViewChild('deviceMenu')
- private readonly deviceMenu!: DeviceListMenuComponent
-
- async show() {
- const device = await this.deviceMenu.show(this.devices, this.device)
-
- if (device) {
- this.device = device === 'NONE' ? undefined : device
- this.deviceChange.emit(this.device)
- }
- }
-
- hide() {
- this.deviceMenu.hide()
- }
-}
\ No newline at end of file
diff --git a/desktop/src/shared/components/device-list-menu/device-list-menu.component.scss b/desktop/src/shared/components/device-list-menu/device-list-menu.component.scss
index e69de29bb..1e445858e 100644
--- a/desktop/src/shared/components/device-list-menu/device-list-menu.component.scss
+++ b/desktop/src/shared/components/device-list-menu/device-list-menu.component.scss
@@ -0,0 +1,5 @@
+:host {
+ ::ng-deep .p-menuitem-link {
+ min-height: 43px;
+ }
+}
\ No newline at end of file
diff --git a/desktop/src/shared/components/device-list-menu/device-list-menu.component.ts b/desktop/src/shared/components/device-list-menu/device-list-menu.component.ts
index f60d66c5b..d09d1e0b5 100644
--- a/desktop/src/shared/components/device-list-menu/device-list-menu.component.ts
+++ b/desktop/src/shared/components/device-list-menu/device-list-menu.component.ts
@@ -1,11 +1,18 @@
import { Component, EventEmitter, Input, Output, ViewChild } from '@angular/core'
import { SEPARATOR_MENU_ITEM } from '../../constants'
import { PrimeService } from '../../services/prime.service'
+import { isGuideHead } from '../../types/camera.types'
import { Device } from '../../types/device.types'
import { deviceComparator } from '../../utils/comparators'
import { DialogMenuComponent } from '../dialog-menu/dialog-menu.component'
+import { MenuItem } from '../menu-item/menu-item.component'
import { SlideMenuItem } from '../slide-menu/slide-menu.component'
+export interface DeviceConnectionCommandEvent {
+ device: Device
+ item: MenuItem
+}
+
@Component({
selector: 'neb-device-list-menu',
templateUrl: './device-list-menu.component.html',
@@ -29,10 +36,10 @@ export class DeviceListMenuComponent {
readonly hasNone: boolean = false
@Output()
- readonly deviceConnect = new EventEmitter()
+ readonly deviceConnect = new EventEmitter()
@Output()
- readonly deviceDisconnect = new EventEmitter()
+ readonly deviceDisconnect = new EventEmitter()
@ViewChild('menu')
private readonly menu!: DialogMenuComponent
@@ -73,17 +80,18 @@ export class DeviceListMenuComponent {
for (const device of devices.sort(deviceComparator)) {
model.push({
- icon: 'mdi mdi-circle-medium ' + (device.connected ? 'text-green-500' : 'text-red-500'),
label: device.name,
checked: selected === device,
disabled: this.disableIfDeviceIsNotConnected && !device.connected,
toolbarMenu: [
{
- icon: 'mdi ' + (device.connected ? 'mdi-close text-red-500' : 'mdi-connection text-blue-500'),
+ icon: 'mdi ' + (device.connected ? 'mdi-close' : 'mdi-connection'),
+ toolbarButtonSeverity: device.connected ? 'danger' : 'info',
label: device.connected ? 'Disconnect' : 'Connect',
+ visible: !isGuideHead(device),
command: event => {
- if (device.connected) this.deviceDisconnect.emit(device)
- else this.deviceConnect.emit(device)
+ if (device.connected) this.deviceDisconnect.emit({ device, item: event.item! })
+ else this.deviceConnect.emit({ device, item: event.item! })
}
}
],
diff --git a/desktop/src/shared/components/dialog-menu/dialog-menu.component.ts b/desktop/src/shared/components/dialog-menu/dialog-menu.component.ts
index 7c811cfb5..efdc0dcc2 100644
--- a/desktop/src/shared/components/dialog-menu/dialog-menu.component.ts
+++ b/desktop/src/shared/components/dialog-menu/dialog-menu.component.ts
@@ -1,5 +1,5 @@
import { Component, EventEmitter, Input, Output } from '@angular/core'
-import { ExtendedMenuItem } from '../menu-item/menu-item.component'
+import { MenuItem } from '../menu-item/menu-item.component'
import { SlideMenuItemCommandEvent } from '../slide-menu/slide-menu.component'
@Component({
@@ -16,7 +16,7 @@ export class DialogMenuComponent {
readonly visibleChange = new EventEmitter()
@Input()
- model: ExtendedMenuItem[] = []
+ model: MenuItem[] = []
@Input()
header?: string
@@ -38,7 +38,7 @@ export class DialogMenuComponent {
}
next(event: SlideMenuItemCommandEvent) {
- if (!event.item?.menu?.length) {
+ if (!event.item?.subMenu?.length) {
this.hide()
} else {
this.navigationHeader.push(this.header)
diff --git a/desktop/src/shared/components/menu-item/menu-item.component.html b/desktop/src/shared/components/menu-item/menu-item.component.html
index 6ad336a57..4fd51b180 100644
--- a/desktop/src/shared/components/menu-item/menu-item.component.html
+++ b/desktop/src/shared/components/menu-item/menu-item.component.html
@@ -6,7 +6,8 @@
@for (m of item.toolbarMenu; track i; let i = $index) {
}
@@ -14,7 +15,7 @@
@if(item.toggleable) {
}
- @if (item.items?.length || item.menu?.length) {
+ @if (item.items?.length || item.subMenu?.length) {
}
\ No newline at end of file
diff --git a/desktop/src/shared/components/menu-item/menu-item.component.ts b/desktop/src/shared/components/menu-item/menu-item.component.ts
index 71fbac809..616b6ee1f 100644
--- a/desktop/src/shared/components/menu-item/menu-item.component.ts
+++ b/desktop/src/shared/components/menu-item/menu-item.component.ts
@@ -1,15 +1,27 @@
import { Component, Input } from '@angular/core'
-import { MenuItem, MenuItemCommandEvent } from 'primeng/api'
-import { CheckableMenuItem, ToggleableMenuItem } from '../../types/app.types'
+import { MenuItem as PrimeMenuItem, MenuItemCommandEvent as PrimeMenuItemCommandEvent } from 'primeng/api'
+import { CheckboxChangeEvent } from 'primeng/checkbox'
+import { Severity } from '../../types/app.types'
-export interface ExtendedMenuItemCommandEvent extends MenuItemCommandEvent {
- item?: ExtendedMenuItem
+export interface MenuItemCommandEvent extends PrimeMenuItemCommandEvent {
+ item?: MenuItem
}
-export interface ExtendedMenuItem extends MenuItem, Partial, Partial {
- menu?: ExtendedMenuItem[]
- toolbarMenu?: ExtendedMenuItem[]
- command?: (event: ExtendedMenuItemCommandEvent) => void
+export interface MenuItem extends PrimeMenuItem {
+ badgeSeverity?: Severity
+
+ checked?: boolean
+
+ toggleable?: boolean
+ toggled?: boolean
+
+ subMenu?: MenuItem[]
+
+ toolbarMenu?: MenuItem[]
+ toolbarButtonSeverity?: Severity
+
+ command?: (event: MenuItemCommandEvent) => void
+ toggle?: (event: CheckboxChangeEvent) => void
}
@Component({
@@ -20,5 +32,5 @@ export interface ExtendedMenuItem extends MenuItem, Partial,
export class MenuItemComponent {
@Input({ required: true })
- readonly item!: ExtendedMenuItem
+ readonly item!: MenuItem
}
\ No newline at end of file
diff --git a/desktop/src/shared/components/slide-menu/slide-menu.component.html b/desktop/src/shared/components/slide-menu/slide-menu.component.html
index 3cc80d1a2..27dd182cd 100644
--- a/desktop/src/shared/components/slide-menu/slide-menu.component.html
+++ b/desktop/src/shared/components/slide-menu/slide-menu.component.html
@@ -1,5 +1,5 @@
-
+
diff --git a/desktop/src/shared/components/slide-menu/slide-menu.component.ts b/desktop/src/shared/components/slide-menu/slide-menu.component.ts
index f7c254c71..59cfbd2fa 100644
--- a/desktop/src/shared/components/slide-menu/slide-menu.component.ts
+++ b/desktop/src/shared/components/slide-menu/slide-menu.component.ts
@@ -1,11 +1,11 @@
import { Component, ElementRef, EventEmitter, Input, OnInit, Output, TemplateRef } from '@angular/core'
-import { ExtendedMenuItem, ExtendedMenuItemCommandEvent } from '../menu-item/menu-item.component'
+import { MenuItem, MenuItemCommandEvent } from '../menu-item/menu-item.component'
-export interface SlideMenuItem extends ExtendedMenuItem {
+export interface SlideMenuItem extends MenuItem {
command?: (event: SlideMenuItemCommandEvent) => void
}
-export interface SlideMenuItemCommandEvent extends ExtendedMenuItemCommandEvent {
+export interface SlideMenuItemCommandEvent extends MenuItemCommandEvent {
item?: SlideMenuItem
parent?: SlideMenuItem
level?: number
@@ -51,9 +51,9 @@ export class SlideMenuComponent implements OnInit {
for (const item of menu) {
const command = item.command
- if (item.menu?.length) {
+ if (item.subMenu?.length) {
item.command = (event: SlideMenuItemCommandEvent) => {
- this.menu = item.menu!
+ this.menu = item.subMenu!
this.navigation.push(menu)
event.parent = parent
event.level = level
@@ -61,7 +61,7 @@ export class SlideMenuComponent implements OnInit {
this.onNext.emit(event)
}
- this.processMenu(item.menu, level + 1, item)
+ this.processMenu(item.subMenu, level + 1, item)
} else {
item.command = (event: SlideMenuItemCommandEvent) => {
event.parent = parent
diff --git a/desktop/src/shared/constants.ts b/desktop/src/shared/constants.ts
index 5a2628fdf..bda08b458 100644
--- a/desktop/src/shared/constants.ts
+++ b/desktop/src/shared/constants.ts
@@ -1,4 +1,4 @@
-import { ExtendedMenuItem } from './components/menu-item/menu-item.component'
+import { MenuItem } from './components/menu-item/menu-item.component'
export const EVERY_MINUTE_CRON_TIME = '0 */1 * * * *'
@@ -6,6 +6,6 @@ export const TWO_DIGITS_FORMATTER = new Intl.NumberFormat('en-US', { minimumInte
export const THREE_DIGITS_FORMATTER = new Intl.NumberFormat('en-US', { minimumIntegerDigits: 3, minimumFractionDigits: 0, maximumFractionDigits: 0 })
export const ONE_DECIMAL_PLACE_FORMATTER = new Intl.NumberFormat('en-US', { minimumFractionDigits: 1, maximumFractionDigits: 1 })
-export const SEPARATOR_MENU_ITEM: ExtendedMenuItem = {
+export const SEPARATOR_MENU_ITEM: MenuItem = {
separator: true,
}
diff --git a/desktop/src/shared/services/api.service.ts b/desktop/src/shared/services/api.service.ts
index fb1eb0b5a..223d97d0b 100644
--- a/desktop/src/shared/services/api.service.ts
+++ b/desktop/src/shared/services/api.service.ts
@@ -69,7 +69,6 @@ export class ApiService {
return this.http.get(`cameras/${camera.id}/capturing`)
}
- // TODO: Rotator
cameraSnoop(camera: Camera, equipment: Equipment) {
const { mount, wheel, focuser, rotator } = equipment
const query = this.http.query({ mount: mount?.name, wheel: wheel?.name, focuser: focuser?.name, rotator: rotator?.name })
@@ -383,6 +382,18 @@ export class ApiService {
// INDI
+ indiDevice(device: T) {
+ return this.http.get(`indi/${device.id}`)
+ }
+
+ indiDeviceConnect(device: Device) {
+ return this.http.put(`indi/${device.id}/connect`)
+ }
+
+ indiDeviceDisconnect(device: Device) {
+ return this.http.put(`indi/${device.id}/disconnect`)
+ }
+
indiProperties(device: Device) {
return this.http.get[]>(`indi/${device.id}/properties`)
}
diff --git a/desktop/src/shared/types/app.types.ts b/desktop/src/shared/types/app.types.ts
index 59325b479..633e583b5 100644
--- a/desktop/src/shared/types/app.types.ts
+++ b/desktop/src/shared/types/app.types.ts
@@ -1,17 +1,6 @@
-import { MenuItem } from 'primeng/api'
-import { CheckboxChangeEvent } from 'primeng/checkbox'
import { MessageEvent } from './api.types'
-export interface CheckableMenuItem extends MenuItem {
- checked: boolean
-}
-
-export interface ToggleableMenuItem extends MenuItem {
- toggleable: boolean
- toggled: boolean
-
- toggle: (event: CheckboxChangeEvent) => void
-}
+export type Severity = 'success' | 'info' | 'warning' | 'danger'
export interface NotificationEvent extends MessageEvent {
type: string
diff --git a/desktop/src/shared/types/auxiliary.types.ts b/desktop/src/shared/types/auxiliary.types.ts
index 549799e4f..eca875e7d 100644
--- a/desktop/src/shared/types/auxiliary.types.ts
+++ b/desktop/src/shared/types/auxiliary.types.ts
@@ -4,3 +4,7 @@ export interface Thermometer extends Device {
hasThermometer: boolean
temperature: number
}
+
+export function isThermometer(device?: Device): device is Thermometer {
+ return !!device && 'temperature' in device
+}
diff --git a/desktop/src/shared/types/camera.types.ts b/desktop/src/shared/types/camera.types.ts
index 056ccc482..5d48be72f 100644
--- a/desktop/src/shared/types/camera.types.ts
+++ b/desktop/src/shared/types/camera.types.ts
@@ -1,6 +1,6 @@
import { MessageEvent } from './api.types'
import { Thermometer } from './auxiliary.types'
-import { CompanionDevice, Device, PropertyState } from './device.types'
+import { CompanionDevice, Device, PropertyState, isCompanionDevice } from './device.types'
import { GuideOutput } from './guider.types'
export type CameraDialogMode = 'CAPTURE' | 'SEQUENCER' | 'FLAT_WIZARD' | 'TPPA' | 'DARV' | 'AUTO_FOCUS'
@@ -266,3 +266,11 @@ export const EMPTY_CAMERA_CAPTURE_INFO: CameraCaptureInfo = {
progress: 0,
count: 0,
}
+
+export function isCamera(device?: Device): device is Camera {
+ return !!device && 'exposuring' in device
+}
+
+export function isGuideHead(device?: Device): device is GuideHead {
+ return isCamera(device) && isCompanionDevice(device) && !!device.main
+}
diff --git a/desktop/src/shared/types/device.types.ts b/desktop/src/shared/types/device.types.ts
index 934a9509f..b26f23daa 100644
--- a/desktop/src/shared/types/device.types.ts
+++ b/desktop/src/shared/types/device.types.ts
@@ -8,6 +8,8 @@ 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'
+
export interface Device {
readonly sender: string
readonly id: string
@@ -57,3 +59,7 @@ export interface INDIDeviceMessage {
device?: Device
message: string
}
+
+export function isCompanionDevice(device?: T | CompanionDevice): device is CompanionDevice {
+ return !!device && 'main' in device
+}
diff --git a/desktop/src/shared/types/focuser.types.ts b/desktop/src/shared/types/focuser.types.ts
index 50bf8c5ea..5f05f8302 100644
--- a/desktop/src/shared/types/focuser.types.ts
+++ b/desktop/src/shared/types/focuser.types.ts
@@ -37,3 +37,7 @@ export interface FocuserPreference {
stepsRelative?: number
stepsAbsolute?: number
}
+
+export function isFocuser(device?: Device): device is Focuser {
+ return !!device && 'maxPosition' in device
+}
diff --git a/desktop/src/shared/types/home.types.ts b/desktop/src/shared/types/home.types.ts
index 0885999c1..6c6195728 100644
--- a/desktop/src/shared/types/home.types.ts
+++ b/desktop/src/shared/types/home.types.ts
@@ -1,12 +1,12 @@
import { Camera } from './camera.types'
+import { DeviceType } from './device.types'
import { Focuser } from './focuser.types'
import { Mount } from './mount.types'
import { Rotator } from './rotator.types'
import { FilterWheel } from './wheel.types'
-export type HomeWindowType = 'CAMERA' | 'MOUNT' | 'GUIDER' | 'WHEEL' | 'FOCUSER' | 'DOME' | 'ROTATOR' | 'SWITCH' |
- 'SKY_ATLAS' | 'ALIGNMENT' | 'SEQUENCER' | 'IMAGE' | 'FRAMING' | 'INDI' | 'SETTINGS' | 'CALCULATOR' | 'ABOUT' | 'FLAT_WIZARD' |
- 'AUTO_FOCUS'
+export type HomeWindowType = DeviceType | 'GUIDER' | 'SKY_ATLAS' | 'ALIGNMENT' | 'SEQUENCER' | 'IMAGE' | 'FRAMING' | 'INDI' |
+ 'SETTINGS' | 'CALCULATOR' | 'ABOUT' | 'FLAT_WIZARD' | 'AUTO_FOCUS'
export const CONNECTION_TYPES = ['INDI', 'ALPACA'] as const
diff --git a/desktop/src/shared/types/mount.types.ts b/desktop/src/shared/types/mount.types.ts
index 7fc7611af..afc1c453f 100644
--- a/desktop/src/shared/types/mount.types.ts
+++ b/desktop/src/shared/types/mount.types.ts
@@ -1,4 +1,5 @@
import { Angle, EquatorialCoordinate } from './atlas.types'
+import { Device } from './device.types'
import { GPS } from './gps.types'
import { GuideOutput } from './guider.types'
@@ -95,3 +96,7 @@ export interface MountRemoteControlDialog {
port: number
data: MountRemoteControl[]
}
+
+export function isMount(device?: Device): device is Mount {
+ return !!device && 'tracking' in device
+}
diff --git a/desktop/src/shared/types/rotator.types.ts b/desktop/src/shared/types/rotator.types.ts
index 4348ce342..daebb23a2 100644
--- a/desktop/src/shared/types/rotator.types.ts
+++ b/desktop/src/shared/types/rotator.types.ts
@@ -33,3 +33,7 @@ export const EMPTY_ROTATOR: Rotator = {
export interface RotatorPreference {
angle?: number
}
+
+export function isRotator(device?: Device): device is Rotator {
+ return !!device && 'angle' in device
+}
diff --git a/desktop/src/shared/types/wheel.types.ts b/desktop/src/shared/types/wheel.types.ts
index bd4928393..d60f738c8 100644
--- a/desktop/src/shared/types/wheel.types.ts
+++ b/desktop/src/shared/types/wheel.types.ts
@@ -42,4 +42,8 @@ export interface FilterSlot {
export interface WheelRenamed {
wheel: FilterWheel
filter: FilterSlot
-}
\ No newline at end of file
+}
+
+export function isFilterWheel(device?: Device): device is FilterWheel {
+ return !!device && 'count' in device
+}
diff --git a/desktop/src/styles.scss b/desktop/src/styles.scss
index b1a1660ff..ef9dc1cb9 100644
--- a/desktop/src/styles.scss
+++ b/desktop/src/styles.scss
@@ -182,6 +182,8 @@ i.mdi {
}
.p-menuitem-link {
+ padding: 0.5rem 0.75rem;
+
&.p-menuitem-checked {
background-color: #C5E1A5;
color: #212121 !important;
From df472e92f0ac0a5c691fe878245afca52076fd6c Mon Sep 17 00:00:00 2001
From: tiagohm
Date: Mon, 27 May 2024 22:31:20 -0300
Subject: [PATCH 26/45] [api][desktop]: Support Auto Focus
---
.../api/autofocus/AutoFocusRequest.kt | 5 +-
.../nebulosa/api/autofocus/AutoFocusTask.kt | 18 +--
.../api/focusers/AbstractFocuserMoveTask.kt | 56 ++++++++
.../api/focusers/BacklashCompensation.kt | 13 ++
.../BacklashCompensationFocuserMoveTask.kt | 135 ++++++++++++++++++
.../BacklashCompensationMode.kt | 2 +-
.../api/focusers/FocuserMoveAbsoluteTask.kt | 59 +-------
.../api/focusers/FocuserMoveRelativeTask.kt | 59 +-------
.../app/alignment/alignment.component.html | 4 +-
.../src/app/alignment/alignment.component.ts | 3 +-
.../app/autofocus/autofocus.component.html | 40 ++++++
.../src/app/autofocus/autofocus.component.ts | 22 ++-
desktop/src/app/image/image.component.ts | 3 +-
.../src/app/settings/settings.component.ts | 4 +-
desktop/src/shared/types/autofocus.type.ts | 31 +++-
desktop/src/shared/types/settings.types.ts | 2 -
16 files changed, 323 insertions(+), 133 deletions(-)
create mode 100644 api/src/main/kotlin/nebulosa/api/focusers/AbstractFocuserMoveTask.kt
create mode 100644 api/src/main/kotlin/nebulosa/api/focusers/BacklashCompensation.kt
create mode 100644 api/src/main/kotlin/nebulosa/api/focusers/BacklashCompensationFocuserMoveTask.kt
rename api/src/main/kotlin/nebulosa/api/{autofocus => focusers}/BacklashCompensationMode.kt (69%)
diff --git a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusRequest.kt b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusRequest.kt
index b19e26940..3899a409f 100644
--- a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusRequest.kt
+++ b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusRequest.kt
@@ -1,14 +1,13 @@
package nebulosa.api.autofocus
import nebulosa.api.cameras.CameraStartCaptureRequest
+import nebulosa.api.focusers.BacklashCompensation
data class AutoFocusRequest(
@JvmField val fittingMode: AutoFocusFittingMode = AutoFocusFittingMode.HYPERBOLIC,
@JvmField val capture: CameraStartCaptureRequest = CameraStartCaptureRequest.EMPTY,
@JvmField val rSquaredThreshold: Double = 0.7,
- @JvmField val backlashCompensationMode: BacklashCompensationMode = BacklashCompensationMode.OVERSHOOT,
- @JvmField val backlashIn: Int = 0,
- @JvmField val backlashOut: Int = 0,
+ @JvmField val backlashCompensation: BacklashCompensation = BacklashCompensation.EMPTY,
@JvmField val initialOffsetSteps: Int = 4,
@JvmField val stepSize: Int = 50,
@JvmField val totalNumberOfAttempts: Int = 1,
diff --git a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt
index 44e92e2ed..6c79ad5b1 100644
--- a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt
+++ b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt
@@ -2,10 +2,9 @@ package nebulosa.api.autofocus
import io.reactivex.rxjava3.functions.Consumer
import nebulosa.api.cameras.*
+import nebulosa.api.focusers.BacklashCompensationFocuserMoveTask
+import nebulosa.api.focusers.BacklashCompensationMode
import nebulosa.api.focusers.FocuserEventAware
-import nebulosa.api.focusers.FocuserMoveAbsoluteTask
-import nebulosa.api.focusers.FocuserMoveRelativeTask
-import nebulosa.api.focusers.FocuserMoveTask
import nebulosa.api.image.ImageBucket
import nebulosa.api.messages.MessageEvent
import nebulosa.api.tasks.AbstractTask
@@ -56,11 +55,12 @@ data class AutoFocusTask(
frameType = FrameType.LIGHT, autoSave = false, autoSubFolderMode = AutoSubFolderMode.OFF
)
+
private val focusPoints = ArrayList()
private val measurements = ArrayList(request.capture.exposureAmount)
private val cameraCaptureTask = CameraCaptureTask(camera, cameraRequest, exposureMaxRepeat = max(1, request.capture.exposureAmount))
+ private val focuserMoveTask = BacklashCompensationFocuserMoveTask(focuser, 0, request.backlashCompensation)
- @Volatile private var focuserMoveTask: FocuserMoveTask? = null
@Volatile private var trendLineCurve: TrendLineFitting.Curve? = null
@Volatile private var parabolicCurve: Lazy? = null
@Volatile private var hyperbolicCurve: Lazy? = null
@@ -76,7 +76,7 @@ data class AutoFocusTask(
}
override fun handleFocuserEvent(event: FocuserEvent) {
- focuserMoveTask?.handleFocuserEvent(event)
+ focuserMoveTask.handleFocuserEvent(event)
}
override fun canUseAsLastEvent(event: MessageEvent) = event is AutoFocusEvent
@@ -88,7 +88,8 @@ data class AutoFocusTask(
// Get initial position information, as average of multiple exposures, if configured this way.
val initialHFD = if (request.rSquaredThreshold <= 0.0) takeExposure(cancellationToken).averageHFD else Double.NaN
- val reverse = request.backlashCompensationMode == BacklashCompensationMode.OVERSHOOT && request.backlashIn > 0 && request.backlashOut == 0
+ val reverse = request.backlashCompensation.mode == BacklashCompensationMode.OVERSHOOT && request.backlashCompensation.backlashIn > 0 &&
+ request.backlashCompensation.backlashOut == 0
LOG.info("Auto Focus started. initialHFD={}, reverse={}, camera={}, focuser={}", initialHFD, reverse, camera, focuser)
@@ -354,10 +355,9 @@ data class AutoFocusTask(
}
private fun moveFocuser(position: Int, cancellationToken: CancellationToken, relative: Boolean): Int {
- focuserMoveTask = if (relative) FocuserMoveRelativeTask(focuser, position)
- else FocuserMoveAbsoluteTask(focuser, position)
sendEvent(AutoFocusState.MOVING)
- focuserMoveTask!!.execute(cancellationToken)
+ focuserMoveTask.position = if (relative) focuser.position + position else position
+ focuserMoveTask.execute(cancellationToken)
return focuser.position
}
diff --git a/api/src/main/kotlin/nebulosa/api/focusers/AbstractFocuserMoveTask.kt b/api/src/main/kotlin/nebulosa/api/focusers/AbstractFocuserMoveTask.kt
new file mode 100644
index 000000000..115f8a354
--- /dev/null
+++ b/api/src/main/kotlin/nebulosa/api/focusers/AbstractFocuserMoveTask.kt
@@ -0,0 +1,56 @@
+package nebulosa.api.focusers
+
+import nebulosa.common.concurrency.cancel.CancellationListener
+import nebulosa.common.concurrency.cancel.CancellationSource
+import nebulosa.common.concurrency.cancel.CancellationToken
+import nebulosa.common.concurrency.latch.CountUpDownLatch
+import nebulosa.indi.device.focuser.FocuserEvent
+import nebulosa.indi.device.focuser.FocuserMoveFailed
+import nebulosa.indi.device.focuser.FocuserPositionChanged
+import nebulosa.log.loggerFor
+
+abstract class AbstractFocuserMoveTask : FocuserMoveTask, CancellationListener {
+
+ @JvmField protected val latch = CountUpDownLatch()
+
+ @Volatile private var initialPosition = 0
+
+ override fun handleFocuserEvent(event: FocuserEvent) {
+ if (event.device === focuser) {
+ when (event) {
+ is FocuserPositionChanged -> if (focuser.position != initialPosition && !focuser.moving) latch.reset()
+ is FocuserMoveFailed -> latch.reset()
+ }
+ }
+ }
+
+ protected abstract fun canMove(): Boolean
+
+ protected abstract fun move()
+
+ override fun execute(cancellationToken: CancellationToken) {
+ if (!cancellationToken.isCancelled && focuser.connected && !focuser.moving && canMove()) {
+ try {
+ cancellationToken.listen(this)
+ initialPosition = focuser.position
+ LOG.info("Focuser move started. focuser={}", focuser)
+ latch.countUp()
+ move()
+ latch.await()
+ } finally {
+ cancellationToken.unlisten(this)
+ LOG.info("Focuser move finished. focuser={}", focuser)
+ }
+ }
+ }
+
+ override fun onCancel(source: CancellationSource) {
+ focuser.abortFocus()
+ latch.reset()
+ }
+
+ companion object {
+
+ @JvmStatic private val LOG = loggerFor()
+ }
+}
diff --git a/api/src/main/kotlin/nebulosa/api/focusers/BacklashCompensation.kt b/api/src/main/kotlin/nebulosa/api/focusers/BacklashCompensation.kt
new file mode 100644
index 000000000..e7b4b5982
--- /dev/null
+++ b/api/src/main/kotlin/nebulosa/api/focusers/BacklashCompensation.kt
@@ -0,0 +1,13 @@
+package nebulosa.api.focusers
+
+data class BacklashCompensation(
+ @JvmField val mode: BacklashCompensationMode = BacklashCompensationMode.OVERSHOOT,
+ @JvmField val backlashIn: Int = 0,
+ @JvmField val backlashOut: Int = 0,
+) {
+
+ companion object {
+
+ @JvmStatic val EMPTY = BacklashCompensation()
+ }
+}
diff --git a/api/src/main/kotlin/nebulosa/api/focusers/BacklashCompensationFocuserMoveTask.kt b/api/src/main/kotlin/nebulosa/api/focusers/BacklashCompensationFocuserMoveTask.kt
new file mode 100644
index 000000000..49e4ebc94
--- /dev/null
+++ b/api/src/main/kotlin/nebulosa/api/focusers/BacklashCompensationFocuserMoveTask.kt
@@ -0,0 +1,135 @@
+package nebulosa.api.focusers
+
+import nebulosa.common.concurrency.cancel.CancellationToken
+import nebulosa.indi.device.focuser.Focuser
+import nebulosa.indi.device.focuser.FocuserEvent
+import nebulosa.log.loggerFor
+
+/**
+ * This decorator will wrap an absolute backlash [compensation] model around the [focuser].
+ * On each move an absolute backlash compensation value will be applied, if the focuser changes its moving direction
+ * The returned position will then accommodate for this backlash and simulating the position without backlash.
+ */
+data class BacklashCompensationFocuserMoveTask(
+ override val focuser: Focuser,
+ @JvmField @Volatile var position: Int,
+ @JvmField val compensation: BacklashCompensation,
+) : FocuserMoveTask {
+
+ enum class OvershootDirection {
+ NONE,
+ IN,
+ OUT,
+ }
+
+ @Volatile private var offset = 0
+ @Volatile private var lastDirection = OvershootDirection.NONE
+
+ private val task = FocuserMoveAbsoluteTask(focuser, 0)
+
+ /**
+ * Returns the adjusted position based on the amount of backlash compensation.
+ */
+ val adjustedPosition
+ get() = focuser.position - offset
+
+ override fun handleFocuserEvent(event: FocuserEvent) {
+ task.handleFocuserEvent(event)
+ }
+
+ override fun execute(cancellationToken: CancellationToken) {
+ if (!cancellationToken.isCancelled && focuser.connected && !focuser.moving) {
+ val startPosition = focuser.position
+
+ if (compensation.mode == BacklashCompensationMode.ABSOLUTE) {
+ val adjustedTargetPosition = position + offset
+
+ val finalizedTargetPosition = if (adjustedTargetPosition < 0) {
+ offset = 0
+ 0
+ } else if (adjustedTargetPosition > focuser.maxPosition) {
+ offset = 0
+ focuser.maxPosition
+ } else {
+ val backlashCompensation = calculateAbsoluteBacklashCompensation(startPosition, adjustedTargetPosition)
+ offset += backlashCompensation
+ adjustedTargetPosition + backlashCompensation
+ }
+
+ moveFocuser(finalizedTargetPosition, cancellationToken)
+ } else {
+ val backlashCompensation = calculateOvershootBacklashCompensation(startPosition, position)
+
+ if (backlashCompensation != 0) {
+ val overshoot = position + backlashCompensation
+
+ if (overshoot < 0) {
+ LOG.info("overshooting position is below minimum 0, skipping overshoot")
+ } else if (overshoot > focuser.maxPosition) {
+ LOG.info("overshooting position is above maximum ${focuser.maxPosition}, skipping overshoot")
+ } else {
+ LOG.info("overshooting from $startPosition to overshoot position $overshoot using a compensation of $backlashCompensation")
+
+ moveFocuser(overshoot, cancellationToken)
+
+ LOG.info("moving back to position $position")
+ }
+ }
+
+ moveFocuser(position, cancellationToken)
+ }
+ }
+ }
+
+ private fun moveFocuser(position: Int, cancellationToken: CancellationToken) {
+ if (position > 0 && position <= focuser.maxPosition) {
+ lastDirection = determineMovingDirection(focuser.position, position)
+ task.position = position
+ task.execute(cancellationToken)
+ }
+ }
+
+ override fun reset() {
+ task.reset()
+
+ offset = 0
+ lastDirection = OvershootDirection.NONE
+ }
+
+ override fun close() {
+ task.close()
+ }
+
+ private fun determineMovingDirection(prevPosition: Int, newPosition: Int): OvershootDirection {
+ return if (newPosition > prevPosition) OvershootDirection.OUT
+ else if (newPosition < prevPosition) OvershootDirection.IN
+ else lastDirection
+ }
+
+ private fun calculateAbsoluteBacklashCompensation(lastPosition: Int, newPosition: Int): Int {
+ val direction = determineMovingDirection(lastPosition, newPosition)
+
+ return if (direction == OvershootDirection.IN && lastDirection == OvershootDirection.OUT) {
+ LOG.info("Focuser is reversing direction from outwards to inwards")
+ -compensation.backlashIn
+ } else if (direction == OvershootDirection.OUT && lastDirection === OvershootDirection.IN) {
+ LOG.info("Focuser is reversing direction from inwards to outwards")
+ compensation.backlashOut
+ } else {
+ 0
+ }
+ }
+
+ private fun calculateOvershootBacklashCompensation(lastPosition: Int, newPosition: Int): Int {
+ val direction = determineMovingDirection(lastPosition, newPosition)
+
+ return if (direction == OvershootDirection.IN && compensation.backlashIn != 0) -compensation.backlashIn
+ else if (direction == OvershootDirection.OUT && compensation.backlashOut != 0) compensation.backlashOut
+ else 0
+ }
+
+ companion object {
+
+ @JvmStatic private val LOG = loggerFor()
+ }
+}
diff --git a/api/src/main/kotlin/nebulosa/api/autofocus/BacklashCompensationMode.kt b/api/src/main/kotlin/nebulosa/api/focusers/BacklashCompensationMode.kt
similarity index 69%
rename from api/src/main/kotlin/nebulosa/api/autofocus/BacklashCompensationMode.kt
rename to api/src/main/kotlin/nebulosa/api/focusers/BacklashCompensationMode.kt
index d80ab414b..a81802872 100644
--- a/api/src/main/kotlin/nebulosa/api/autofocus/BacklashCompensationMode.kt
+++ b/api/src/main/kotlin/nebulosa/api/focusers/BacklashCompensationMode.kt
@@ -1,4 +1,4 @@
-package nebulosa.api.autofocus
+package nebulosa.api.focusers
enum class BacklashCompensationMode {
ABSOLUTE,
diff --git a/api/src/main/kotlin/nebulosa/api/focusers/FocuserMoveAbsoluteTask.kt b/api/src/main/kotlin/nebulosa/api/focusers/FocuserMoveAbsoluteTask.kt
index b154ee55e..85379b053 100644
--- a/api/src/main/kotlin/nebulosa/api/focusers/FocuserMoveAbsoluteTask.kt
+++ b/api/src/main/kotlin/nebulosa/api/focusers/FocuserMoveAbsoluteTask.kt
@@ -1,63 +1,18 @@
package nebulosa.api.focusers
-import nebulosa.common.concurrency.cancel.CancellationListener
-import nebulosa.common.concurrency.cancel.CancellationSource
-import nebulosa.common.concurrency.cancel.CancellationToken
-import nebulosa.common.concurrency.latch.CountUpDownLatch
import nebulosa.indi.device.focuser.Focuser
-import nebulosa.indi.device.focuser.FocuserEvent
-import nebulosa.indi.device.focuser.FocuserMoveFailed
-import nebulosa.indi.device.focuser.FocuserPositionChanged
-import nebulosa.log.loggerFor
import kotlin.math.abs
data class FocuserMoveAbsoluteTask(
override val focuser: Focuser,
- @JvmField val position: Int,
-) : FocuserMoveTask, CancellationListener {
+ @JvmField @Volatile var position: Int,
+) : AbstractFocuserMoveTask() {
- private val latch = CountUpDownLatch()
+ override fun canMove() = position != focuser.position && position > 0 && position < focuser.maxPosition
- override fun handleFocuserEvent(event: FocuserEvent) {
- if (event.device === focuser) {
- when (event) {
- is FocuserPositionChanged -> if (focuser.position == position) latch.reset()
- is FocuserMoveFailed -> latch.reset()
- }
- }
- }
-
- override fun execute(cancellationToken: CancellationToken) {
- if (!cancellationToken.isCancelled && focuser.connected
- && !focuser.moving && position != focuser.position
- ) {
- try {
- cancellationToken.listen(this)
-
- LOG.info("Focuser move started. position={}, focuser={}", position, focuser)
-
- latch.countUp()
-
- if (focuser.canAbsoluteMove) focuser.moveFocusTo(position)
- else if (focuser.position - position < 0) focuser.moveFocusIn(abs(focuser.position - position))
- else focuser.moveFocusOut(abs(focuser.position - position))
-
- latch.await()
- } finally {
- cancellationToken.unlisten(this)
- }
-
- LOG.info("Focuser move finished. position={}, focuser={}", position, focuser)
- }
- }
-
- override fun onCancel(source: CancellationSource) {
- focuser.abortFocus()
- latch.reset()
- }
-
- companion object {
-
- @JvmStatic private val LOG = loggerFor()
+ override fun move() {
+ if (focuser.canAbsoluteMove) focuser.moveFocusTo(position)
+ else if (position < focuser.position) focuser.moveFocusIn(abs(position - focuser.position))
+ else focuser.moveFocusOut(abs(position - focuser.position))
}
}
diff --git a/api/src/main/kotlin/nebulosa/api/focusers/FocuserMoveRelativeTask.kt b/api/src/main/kotlin/nebulosa/api/focusers/FocuserMoveRelativeTask.kt
index 531dafce0..ec4671b3a 100644
--- a/api/src/main/kotlin/nebulosa/api/focusers/FocuserMoveRelativeTask.kt
+++ b/api/src/main/kotlin/nebulosa/api/focusers/FocuserMoveRelativeTask.kt
@@ -1,65 +1,18 @@
package nebulosa.api.focusers
-import nebulosa.common.concurrency.cancel.CancellationListener
-import nebulosa.common.concurrency.cancel.CancellationSource
-import nebulosa.common.concurrency.cancel.CancellationToken
-import nebulosa.common.concurrency.latch.CountUpDownLatch
import nebulosa.indi.device.focuser.Focuser
-import nebulosa.indi.device.focuser.FocuserEvent
-import nebulosa.indi.device.focuser.FocuserMoveFailed
-import nebulosa.indi.device.focuser.FocuserPositionChanged
-import nebulosa.log.loggerFor
import kotlin.math.abs
data class FocuserMoveRelativeTask(
override val focuser: Focuser,
@JvmField val offset: Int,
-) : FocuserMoveTask, CancellationListener {
+) : AbstractFocuserMoveTask() {
- private val latch = CountUpDownLatch()
+ override fun canMove() = offset != 0
- @Volatile private var initialPosition = 0
-
- override fun handleFocuserEvent(event: FocuserEvent) {
- if (event.device === focuser) {
- when (event) {
- is FocuserPositionChanged -> if (abs(focuser.position - initialPosition) == abs(offset)) latch.reset()
- is FocuserMoveFailed -> latch.reset()
- }
- }
- }
-
- override fun execute(cancellationToken: CancellationToken) {
- if (!cancellationToken.isCancelled && focuser.connected && !focuser.moving && offset != 0) {
- try {
- cancellationToken.listen(this)
-
- initialPosition = focuser.position
-
- LOG.info("Focuser move started. offset={}, focuser={}", offset, focuser)
-
- latch.countUp()
-
- if (!focuser.canRelativeMove) focuser.moveFocusTo(focuser.position + offset)
- else if (offset > 0) focuser.moveFocusOut(offset)
- else focuser.moveFocusIn(abs(offset))
-
- latch.await()
- } finally {
- cancellationToken.unlisten(this)
- }
-
- LOG.info("Focuser move finished. offset={}, focuser={}", offset, focuser)
- }
- }
-
- override fun onCancel(source: CancellationSource) {
- focuser.abortFocus()
- latch.reset()
- }
-
- companion object {
-
- @JvmStatic private val LOG = loggerFor()
+ override fun move() {
+ if (!focuser.canRelativeMove) focuser.moveFocusTo(focuser.position + offset)
+ else if (offset > 0) focuser.moveFocusOut(offset)
+ else focuser.moveFocusIn(abs(offset))
}
}
diff --git a/desktop/src/app/alignment/alignment.component.html b/desktop/src/app/alignment/alignment.component.html
index d42354c7b..3acb4adf8 100644
--- a/desktop/src/app/alignment/alignment.component.html
+++ b/desktop/src/app/alignment/alignment.component.html
@@ -27,8 +27,8 @@
diff --git a/desktop/src/app/alignment/alignment.component.ts b/desktop/src/app/alignment/alignment.component.ts
index d00c70296..eb4e91581 100644
--- a/desktop/src/app/alignment/alignment.component.ts
+++ b/desktop/src/app/alignment/alignment.component.ts
@@ -9,7 +9,7 @@ import { Angle } from '../../shared/types/atlas.types'
import { Camera, EMPTY_CAMERA, EMPTY_CAMERA_START_CAPTURE, ExposureTimeUnit, updateCameraStartCaptureFromCamera } from '../../shared/types/camera.types'
import { EMPTY_GUIDE_OUTPUT, GuideDirection, GuideOutput } from '../../shared/types/guider.types'
import { EMPTY_MOUNT, Mount } from '../../shared/types/mount.types'
-import { DEFAULT_SOLVER_TYPES, EMPTY_PLATE_SOLVER_PREFERENCE } from '../../shared/types/settings.types'
+import { EMPTY_PLATE_SOLVER_PREFERENCE } from '../../shared/types/settings.types'
import { deviceComparator } from '../../shared/utils/comparators'
import { AppComponent } from '../app.component'
import { CameraComponent } from '../camera/camera.component'
@@ -47,7 +47,6 @@ export class AlignmentComponent implements AfterViewInit, OnDestroy {
stepDuration: 5,
}
- readonly plateSolverTypes = Array.from(DEFAULT_SOLVER_TYPES)
tppaFailed = false
tppaRightAscension: Angle = `00h00m00s`
tppaDeclination: Angle = `00°00'00"`
diff --git a/desktop/src/app/autofocus/autofocus.component.html b/desktop/src/app/autofocus/autofocus.component.html
index 7edd35cf2..9cf121fdb 100644
--- a/desktop/src/app/autofocus/autofocus.component.html
+++ b/desktop/src/app/autofocus/autofocus.component.html
@@ -6,4 +6,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/desktop/src/app/autofocus/autofocus.component.ts b/desktop/src/app/autofocus/autofocus.component.ts
index ec576a103..ee6271829 100644
--- a/desktop/src/app/autofocus/autofocus.component.ts
+++ b/desktop/src/app/autofocus/autofocus.component.ts
@@ -26,7 +26,17 @@ export class AutoFocusComponent implements AfterViewInit, OnDestroy {
running = false
readonly request: AutoFocusRequest = {
- capture: structuredClone(EMPTY_CAMERA_START_CAPTURE)
+ capture: structuredClone(EMPTY_CAMERA_START_CAPTURE),
+ fittingMode: 'HYPERBOLIC',
+ rSquaredThreshold: 0.7,
+ backlashCompensation: {
+ mode: 'OVERSHOOT',
+ backlashIn: 0,
+ backlashOut: 0
+ },
+ initialOffsetSteps: 4,
+ stepSize: 100,
+ totalNumberOfAttempts: 4
}
constructor(
@@ -157,6 +167,16 @@ export class AutoFocusComponent implements AfterViewInit, OnDestroy {
this.preference.cameraStartCaptureForAutoFocus(this.camera).set(this.request.capture)
const preference: AutoFocusPreference = {
+ fittingMode: 'TRENDLINES',
+ rSquaredThreshold: 0,
+ backlashCompensation: {
+ mode: 'OVERSHOOT',
+ backlashIn: 0,
+ backlashOut: 0
+ },
+ initialOffsetSteps: 0,
+ stepSize: 0,
+ totalNumberOfAttempts: 0
}
this.preference.autoFocusPreference.set(preference)
diff --git a/desktop/src/app/image/image.component.ts b/desktop/src/app/image/image.component.ts
index 74d19f19b..956ba9d9e 100644
--- a/desktop/src/app/image/image.component.ts
+++ b/desktop/src/app/image/image.component.ts
@@ -20,7 +20,6 @@ import { Angle, AstronomicalObject, DeepSkyObject, EquatorialCoordinateJ2000, St
import { Camera } from '../../shared/types/camera.types'
import { DEFAULT_FOV, EMPTY_IMAGE_SOLVED, FOV, IMAGE_STATISTICS_BIT_OPTIONS, ImageAnnotation, ImageAnnotationDialog, ImageChannel, ImageData, ImageDetectStars, ImageFITSHeadersDialog, ImageFOVDialog, ImageInfo, ImageROI, ImageSCNRDialog, ImageSaveDialog, ImageSolved, ImageSolverDialog, ImageStatisticsBitOption, ImageStretchDialog, ImageTransformation, SCNR_PROTECTION_METHODS } from '../../shared/types/image.types'
import { Mount } from '../../shared/types/mount.types'
-import { DEFAULT_SOLVER_TYPES } from '../../shared/types/settings.types'
import { CoordinateInterpolator, InterpolatedCoordinate } from '../../shared/utils/coordinate-interpolation'
import { AppComponent } from '../app.component'
@@ -104,7 +103,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy {
centerDEC: '',
radius: 4,
solved: structuredClone(EMPTY_IMAGE_SOLVED),
- types: Array.from(DEFAULT_SOLVER_TYPES),
+ types: ['ASTAP', 'ASTROMETRY_NET_ONLINE'],
type: 'ASTAP'
}
diff --git a/desktop/src/app/settings/settings.component.ts b/desktop/src/app/settings/settings.component.ts
index 4567d72ce..7c288e160 100644
--- a/desktop/src/app/settings/settings.component.ts
+++ b/desktop/src/app/settings/settings.component.ts
@@ -6,7 +6,7 @@ import { ElectronService } from '../../shared/services/electron.service'
import { PreferenceService } from '../../shared/services/preference.service'
import { PrimeService } from '../../shared/services/prime.service'
import { EMPTY_LOCATION, Location } from '../../shared/types/atlas.types'
-import { DEFAULT_SOLVER_TYPES, PlateSolverPreference, PlateSolverType } from '../../shared/types/settings.types'
+import { PlateSolverPreference, PlateSolverType } from '../../shared/types/settings.types'
import { AppComponent } from '../app.component'
@Component({
@@ -19,7 +19,7 @@ export class SettingsComponent implements AfterViewInit, OnDestroy {
readonly locations: Location[]
location: Location
- readonly solverTypes = Array.from(DEFAULT_SOLVER_TYPES)
+ readonly solverTypes: PlateSolverType[] = ['ASTAP', 'ASTROMETRY_NET_ONLINE']
solverType = this.solverTypes[0]
readonly solvers = new Map()
diff --git a/desktop/src/shared/types/autofocus.type.ts b/desktop/src/shared/types/autofocus.type.ts
index 48f3847b6..5a07e32e5 100644
--- a/desktop/src/shared/types/autofocus.type.ts
+++ b/desktop/src/shared/types/autofocus.type.ts
@@ -1,13 +1,36 @@
import { CameraStartCapture } from './camera.types'
+export type AutoFocusFittingMode = 'TRENDLINES' | 'PARABOLIC' | 'TREND_PARABOLIC' | 'HYPERBOLIC' | 'TREND_HYPERBOLIC'
+
+export type BacklashCompensationMode = 'ABSOLUTE' | 'OVERSHOOT'
+
+export interface BacklashCompensation {
+ mode: BacklashCompensationMode
+ backlashIn: number
+ backlashOut: number
+}
+
export interface AutoFocusRequest {
+ fittingMode: AutoFocusFittingMode
capture: CameraStartCapture
+ rSquaredThreshold: number
+ backlashCompensation: BacklashCompensation
+ initialOffsetSteps: number
+ stepSize: number
+ totalNumberOfAttempts: number
}
-export interface AutoFocusPreference {
-
-}
+export interface AutoFocusPreference extends Omit { }
export const EMPTY_AUTO_FOCUS_PREFERENCE: AutoFocusPreference = {
-
+ fittingMode: 'HYPERBOLIC',
+ rSquaredThreshold: 0.7,
+ initialOffsetSteps: 4,
+ stepSize: 100,
+ totalNumberOfAttempts: 4,
+ backlashCompensation: {
+ mode: 'OVERSHOOT',
+ backlashIn: 0,
+ backlashOut: 0
+ }
}
diff --git a/desktop/src/shared/types/settings.types.ts b/desktop/src/shared/types/settings.types.ts
index d3eeacabb..7b26a7f55 100644
--- a/desktop/src/shared/types/settings.types.ts
+++ b/desktop/src/shared/types/settings.types.ts
@@ -1,8 +1,6 @@
export type PlateSolverType = 'ASTROMETRY_NET' | 'ASTROMETRY_NET_ONLINE' | 'ASTAP'
-export const DEFAULT_SOLVER_TYPES: PlateSolverType[] = ['ASTROMETRY_NET_ONLINE', 'ASTAP']
-
export interface PlateSolverPreference {
type: PlateSolverType
executablePath: string
From dddc47bfe65bbf34c5f7f1995574ba70df970853 Mon Sep 17 00:00:00 2001
From: tiagohm
Date: Tue, 28 May 2024 21:48:36 -0300
Subject: [PATCH 27/45] [desktop]: Support Auto Focus
---
.../src/app/alignment/alignment.component.ts | 2 +-
.../app/autofocus/autofocus.component.html | 33 ++++++++++++++++---
.../src/app/autofocus/autofocus.component.ts | 31 +++++++++--------
.../src/app/sequencer/sequencer.component.ts | 2 +-
.../device-list-menu.component.scss | 1 +
desktop/src/shared/types/autofocus.type.ts | 6 ++--
desktop/src/shared/types/image.types.ts | 2 +-
desktop/src/styles.scss | 2 --
8 files changed, 54 insertions(+), 25 deletions(-)
diff --git a/desktop/src/app/alignment/alignment.component.ts b/desktop/src/app/alignment/alignment.component.ts
index eb4e91581..769d1430b 100644
--- a/desktop/src/app/alignment/alignment.component.ts
+++ b/desktop/src/app/alignment/alignment.component.ts
@@ -326,7 +326,7 @@ export class AlignmentComponent implements AfterViewInit, OnDestroy {
}
openCameraImage() {
- return this.browserWindow.openCameraImage(this.camera)
+ return this.browserWindow.openCameraImage(this.camera, 'ALIGNMENT')
}
private loadPreference() {
diff --git a/desktop/src/app/autofocus/autofocus.component.html b/desktop/src/app/autofocus/autofocus.component.html
index 9cf121fdb..610a59a5b 100644
--- a/desktop/src/app/autofocus/autofocus.component.html
+++ b/desktop/src/app/autofocus/autofocus.component.html
@@ -10,7 +10,7 @@
-
@@ -39,11 +39,36 @@
-
-
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/desktop/src/app/autofocus/autofocus.component.ts b/desktop/src/app/autofocus/autofocus.component.ts
index ee6271829..61aec9bbe 100644
--- a/desktop/src/app/autofocus/autofocus.component.ts
+++ b/desktop/src/app/autofocus/autofocus.component.ts
@@ -30,13 +30,13 @@ export class AutoFocusComponent implements AfterViewInit, OnDestroy {
fittingMode: 'HYPERBOLIC',
rSquaredThreshold: 0.7,
backlashCompensation: {
- mode: 'OVERSHOOT',
+ mode: 'NONE',
backlashIn: 0,
backlashOut: 0
},
initialOffsetSteps: 4,
stepSize: 100,
- totalNumberOfAttempts: 4
+ totalNumberOfAttempts: 1
}
constructor(
@@ -142,8 +142,13 @@ export class AutoFocusComponent implements AfterViewInit, OnDestroy {
}
}
+ start() {
+ this.browserWindow.openCameraImage(this.camera, 'AUTO_FOCUS')
+ return this.api.autoFocusStart(this.camera, this.focuser, this.request)
+ }
+
stop() {
- return this.api.tppaStop(this.camera)
+ return this.api.autoFocusStop(this.camera)
}
openCameraImage() {
@@ -153,6 +158,15 @@ export class AutoFocusComponent implements AfterViewInit, OnDestroy {
private loadPreference() {
const preference = this.preference.autoFocusPreference.get()
+ this.request.fittingMode = preference.fittingMode ?? 'HYPERBOLIC'
+ this.request.initialOffsetSteps = preference.initialOffsetSteps ?? 4
+ // this.request.rSquaredThreshold
+ this.request.stepSize = preference.stepSize ?? 100
+ this.request.totalNumberOfAttempts = preference.totalNumberOfAttempts ?? 1
+ this.request.backlashCompensation.mode = preference.backlashCompensation.mode ?? 'NONE'
+ this.request.backlashCompensation.backlashIn = preference.backlashCompensation.backlashIn ?? 0
+ this.request.backlashCompensation.backlashOut = preference.backlashCompensation.backlashOut ?? 0
+
if (this.camera.id) {
const cameraPreference = this.preference.cameraPreference(this.camera).get()
Object.assign(this.request.capture, this.preference.cameraStartCaptureForAutoFocus(this.camera).get(cameraPreference))
@@ -167,16 +181,7 @@ export class AutoFocusComponent implements AfterViewInit, OnDestroy {
this.preference.cameraStartCaptureForAutoFocus(this.camera).set(this.request.capture)
const preference: AutoFocusPreference = {
- fittingMode: 'TRENDLINES',
- rSquaredThreshold: 0,
- backlashCompensation: {
- mode: 'OVERSHOOT',
- backlashIn: 0,
- backlashOut: 0
- },
- initialOffsetSteps: 0,
- stepSize: 0,
- totalNumberOfAttempts: 0
+ ...this.request
}
this.preference.autoFocusPreference.set(preference)
diff --git a/desktop/src/app/sequencer/sequencer.component.ts b/desktop/src/app/sequencer/sequencer.component.ts
index 9833a323f..abd8516e1 100644
--- a/desktop/src/app/sequencer/sequencer.component.ts
+++ b/desktop/src/app/sequencer/sequencer.component.ts
@@ -470,7 +470,7 @@ export class SequencerComponent implements AfterContentInit, OnDestroy {
this.savePlan()
- await this.browserWindow.openCameraImage(this.camera!)
+ await this.browserWindow.openCameraImage(this.camera!, 'SEQUENCER')
this.api.sequencerStart(this.camera!, this.plan)
}
diff --git a/desktop/src/shared/components/device-list-menu/device-list-menu.component.scss b/desktop/src/shared/components/device-list-menu/device-list-menu.component.scss
index 1e445858e..abfb55957 100644
--- a/desktop/src/shared/components/device-list-menu/device-list-menu.component.scss
+++ b/desktop/src/shared/components/device-list-menu/device-list-menu.component.scss
@@ -1,5 +1,6 @@
:host {
::ng-deep .p-menuitem-link {
+ padding: 0.5rem 0.75rem;
min-height: 43px;
}
}
\ No newline at end of file
diff --git a/desktop/src/shared/types/autofocus.type.ts b/desktop/src/shared/types/autofocus.type.ts
index 5a07e32e5..7bb415e5c 100644
--- a/desktop/src/shared/types/autofocus.type.ts
+++ b/desktop/src/shared/types/autofocus.type.ts
@@ -2,7 +2,7 @@ import { CameraStartCapture } from './camera.types'
export type AutoFocusFittingMode = 'TRENDLINES' | 'PARABOLIC' | 'TREND_PARABOLIC' | 'HYPERBOLIC' | 'TREND_HYPERBOLIC'
-export type BacklashCompensationMode = 'ABSOLUTE' | 'OVERSHOOT'
+export type BacklashCompensationMode = 'NONE' | 'ABSOLUTE' | 'OVERSHOOT'
export interface BacklashCompensation {
mode: BacklashCompensationMode
@@ -27,9 +27,9 @@ export const EMPTY_AUTO_FOCUS_PREFERENCE: AutoFocusPreference = {
rSquaredThreshold: 0.7,
initialOffsetSteps: 4,
stepSize: 100,
- totalNumberOfAttempts: 4,
+ totalNumberOfAttempts: 1,
backlashCompensation: {
- mode: 'OVERSHOOT',
+ mode: 'NONE',
backlashIn: 0,
backlashOut: 0
}
diff --git a/desktop/src/shared/types/image.types.ts b/desktop/src/shared/types/image.types.ts
index 37f87cc93..6fb7c9c74 100644
--- a/desktop/src/shared/types/image.types.ts
+++ b/desktop/src/shared/types/image.types.ts
@@ -8,7 +8,7 @@ export type ImageChannel = 'RED' | 'GREEN' | 'BLUE' | 'GRAY'
export const SCNR_PROTECTION_METHODS = ['MAXIMUM_MASK', 'ADDITIVE_MASK', 'AVERAGE_NEUTRAL', 'MAXIMUM_NEUTRAL', 'MINIMUM_NEUTRAL'] as const
export type SCNRProtectionMethod = (typeof SCNR_PROTECTION_METHODS)[number]
-export type ImageSource = 'FRAMING' | 'PATH' | 'CAMERA' | 'FLAT_WIZARD'
+export type ImageSource = 'FRAMING' | 'PATH' | 'CAMERA' | 'FLAT_WIZARD' | 'SEQUENCER' | 'ALIGNMENT' | 'AUTO_FOCUS'
export type ImageFormat = 'FITS' | 'XISF' | 'PNG' | 'JPG'
diff --git a/desktop/src/styles.scss b/desktop/src/styles.scss
index ef9dc1cb9..b1a1660ff 100644
--- a/desktop/src/styles.scss
+++ b/desktop/src/styles.scss
@@ -182,8 +182,6 @@ i.mdi {
}
.p-menuitem-link {
- padding: 0.5rem 0.75rem;
-
&.p-menuitem-checked {
background-color: #C5E1A5;
color: #212121 !important;
From 9384a3c8f830deb478c125df90fe1253437d0ed8 Mon Sep 17 00:00:00 2001
From: tiagohm
Date: Thu, 30 May 2024 11:24:46 -0300
Subject: [PATCH 28/45] [api][desktop]: Support Auto Focus
---
api/build.gradle.kts | 1 -
.../api/alignment/polar/tppa/TPPAExecutor.kt | 6 +-
.../api/autofocus/AutoFocusExecutor.kt | 11 +--
.../api/autofocus/AutoFocusRequest.kt | 2 +
.../nebulosa/api/autofocus/AutoFocusTask.kt | 11 ++-
.../beans/configurations/BeanConfiguration.kt | 7 --
.../nebulosa/api/image/ImageController.kt | 6 --
.../kotlin/nebulosa/api/image/ImageService.kt | 8 ---
.../api/solver/PlateSolverController.kt | 10 ++-
.../nebulosa/api/solver/PlateSolverOptions.kt | 18 +++++
.../nebulosa/api/solver/PlateSolverService.kt | 26 +------
.../stardetection/StarDetectionController.kt | 18 +++++
.../api/stardetection/StarDetectionOptions.kt | 23 ++++++
.../api/stardetection/StarDetectionService.kt | 14 ++++
.../api/stardetection/StarDetectorType.kt | 5 ++
.../src/app/alignment/alignment.component.ts | 6 +-
.../app/autofocus/autofocus.component.html | 71 +++++++++++--------
.../src/app/autofocus/autofocus.component.ts | 11 +--
desktop/src/app/image/image.component.ts | 10 ++-
.../src/app/settings/settings.component.html | 27 ++++++-
.../src/app/settings/settings.component.ts | 37 +++++++---
desktop/src/shared/services/api.service.ts | 12 ++--
.../shared/services/browser-window.service.ts | 2 +-
.../src/shared/services/preference.service.ts | 10 ++-
desktop/src/shared/types/alignment.types.ts | 4 +-
desktop/src/shared/types/autofocus.type.ts | 5 +-
desktop/src/shared/types/settings.types.ts | 19 ++++-
.../astap/star/detection/AstapStarDetector.kt | 2 +-
28 files changed, 241 insertions(+), 141 deletions(-)
create mode 100644 api/src/main/kotlin/nebulosa/api/stardetection/StarDetectionController.kt
create mode 100644 api/src/main/kotlin/nebulosa/api/stardetection/StarDetectionOptions.kt
create mode 100644 api/src/main/kotlin/nebulosa/api/stardetection/StarDetectionService.kt
create mode 100644 api/src/main/kotlin/nebulosa/api/stardetection/StarDetectorType.kt
diff --git a/api/build.gradle.kts b/api/build.gradle.kts
index 601a46919..942f4e926 100644
--- a/api/build.gradle.kts
+++ b/api/build.gradle.kts
@@ -27,7 +27,6 @@ dependencies {
implementation(project(":nebulosa-sbd"))
implementation(project(":nebulosa-simbad"))
implementation(project(":nebulosa-stellarium-protocol"))
- implementation(project(":nebulosa-watney"))
implementation(project(":nebulosa-wcs"))
implementation(project(":nebulosa-xisf"))
implementation(libs.rx)
diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPAExecutor.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPAExecutor.kt
index d4b900519..c7808faaa 100644
--- a/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPAExecutor.kt
+++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPAExecutor.kt
@@ -4,10 +4,10 @@ import io.reactivex.rxjava3.functions.Consumer
import nebulosa.api.beans.annotations.Subscriber
import nebulosa.api.messages.MessageEvent
import nebulosa.api.messages.MessageService
-import nebulosa.api.solver.PlateSolverService
import nebulosa.indi.device.camera.Camera
import nebulosa.indi.device.camera.CameraEvent
import nebulosa.indi.device.mount.Mount
+import okhttp3.OkHttpClient
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import org.springframework.stereotype.Component
@@ -17,7 +17,7 @@ import java.util.concurrent.ConcurrentHashMap
@Subscriber
class TPPAExecutor(
private val messageService: MessageService,
- private val plateSolverService: PlateSolverService,
+ private val httpClient: OkHttpClient,
) : Consumer {
private val jobs = ConcurrentHashMap.newKeySet(1)
@@ -38,7 +38,7 @@ class TPPAExecutor(
check(jobs.none { it.task.camera === camera }) { "${camera.name} TPPA Job is already in progress" }
check(jobs.none { it.task.mount === mount }) { "${camera.name} TPPA Job is already in progress" }
- val solver = plateSolverService.solverFor(request.plateSolver)
+ val solver = request.plateSolver.get(httpClient)
val task = TPPATask(camera, solver, request, mount)
task.subscribe(this)
diff --git a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusExecutor.kt b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusExecutor.kt
index 63d842b1d..bfdd6698d 100644
--- a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusExecutor.kt
+++ b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusExecutor.kt
@@ -2,14 +2,12 @@ package nebulosa.api.autofocus
import io.reactivex.rxjava3.functions.Consumer
import nebulosa.api.beans.annotations.Subscriber
-import nebulosa.api.image.ImageBucket
import nebulosa.api.messages.MessageEvent
import nebulosa.api.messages.MessageService
import nebulosa.indi.device.camera.Camera
import nebulosa.indi.device.camera.CameraEvent
import nebulosa.indi.device.focuser.Focuser
import nebulosa.indi.device.focuser.FocuserEvent
-import nebulosa.watney.star.detection.WatneyStarDetector
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import org.springframework.stereotype.Component
@@ -19,7 +17,6 @@ import java.util.concurrent.ConcurrentHashMap
@Subscriber
class AutoFocusExecutor(
private val messageService: MessageService,
- private val imageBucket: ImageBucket,
) : Consumer {
private val jobs = ConcurrentHashMap.newKeySet(2)
@@ -45,7 +42,8 @@ class AutoFocusExecutor(
check(jobs.none { it.task.camera === camera }) { "${camera.name} Auto Focus is already in progress" }
check(jobs.none { it.task.focuser === focuser }) { "${camera.name} Auto Focus is already in progress" }
- val task = AutoFocusTask(camera, focuser, request, STAR_DETECTOR, imageBucket)
+ val starDetector = request.starDetector.get()
+ val task = AutoFocusTask(camera, focuser, request, starDetector)
task.subscribe(this)
with(AutoFocusJob(task)) {
@@ -62,9 +60,4 @@ class AutoFocusExecutor(
fun status(camera: Camera): AutoFocusEvent? {
return jobs.find { it.task.camera === camera }?.task?.get() as? AutoFocusEvent
}
-
- companion object {
-
- @JvmStatic private val STAR_DETECTOR = WatneyStarDetector(computeHFD = true, minHFD = 0.1f)
- }
}
diff --git a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusRequest.kt b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusRequest.kt
index 3899a409f..815209a3b 100644
--- a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusRequest.kt
+++ b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusRequest.kt
@@ -2,6 +2,7 @@ package nebulosa.api.autofocus
import nebulosa.api.cameras.CameraStartCaptureRequest
import nebulosa.api.focusers.BacklashCompensation
+import nebulosa.api.stardetection.StarDetectionOptions
data class AutoFocusRequest(
@JvmField val fittingMode: AutoFocusFittingMode = AutoFocusFittingMode.HYPERBOLIC,
@@ -11,4 +12,5 @@ data class AutoFocusRequest(
@JvmField val initialOffsetSteps: Int = 4,
@JvmField val stepSize: Int = 50,
@JvmField val totalNumberOfAttempts: Int = 1,
+ @JvmField val starDetector: StarDetectionOptions = StarDetectionOptions.EMPTY,
)
diff --git a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt
index 6c79ad5b1..688e29ba8 100644
--- a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt
+++ b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt
@@ -5,7 +5,6 @@ import nebulosa.api.cameras.*
import nebulosa.api.focusers.BacklashCompensationFocuserMoveTask
import nebulosa.api.focusers.BacklashCompensationMode
import nebulosa.api.focusers.FocuserEventAware
-import nebulosa.api.image.ImageBucket
import nebulosa.api.messages.MessageEvent
import nebulosa.api.tasks.AbstractTask
import nebulosa.common.concurrency.cancel.CancellationToken
@@ -14,7 +13,6 @@ import nebulosa.curve.fitting.CurvePoint.Companion.midPoint
import nebulosa.curve.fitting.HyperbolicFitting
import nebulosa.curve.fitting.QuadraticFitting
import nebulosa.curve.fitting.TrendLineFitting
-import nebulosa.image.Image
import nebulosa.indi.device.camera.Camera
import nebulosa.indi.device.camera.CameraEvent
import nebulosa.indi.device.camera.FrameType
@@ -24,6 +22,7 @@ import nebulosa.log.loggerFor
import nebulosa.star.detection.ImageStar
import nebulosa.star.detection.StarDetector
import java.nio.file.Files
+import java.nio.file.Path
import java.time.Duration
import kotlin.math.max
import kotlin.math.roundToInt
@@ -33,8 +32,7 @@ data class AutoFocusTask(
@JvmField val camera: Camera,
@JvmField val focuser: Focuser,
@JvmField val request: AutoFocusRequest,
- @JvmField val starDetection: StarDetector,
- @JvmField val imageBucket: ImageBucket,
+ @JvmField val starDetection: StarDetector,
) : AbstractTask(), Consumer, CameraEventAware, FocuserEventAware {
data class MeasuredStars(
@@ -97,7 +95,7 @@ data class AutoFocusTask(
var numberOfAttempts = 0
val maximumFocusPoints = request.capture.exposureAmount * request.initialOffsetSteps * 10
- camera.snoop(listOf(focuser))
+ // camera.snoop(listOf(focuser))
while (!exited && !cancellationToken.isCancelled) {
numberOfAttempts++
@@ -229,8 +227,7 @@ data class AutoFocusTask(
override fun accept(event: CameraCaptureEvent) {
if (event.state == CameraCaptureState.EXPOSURE_FINISHED) {
sendEvent(AutoFocusState.COMPUTING, capture = event)
- val image = imageBucket.open(event.savePath!!)
- val detectedStars = starDetection.detect(image)
+ val detectedStars = starDetection.detect(event.savePath!!)
LOG.info("detected ${detectedStars.size} stars")
val measure = detectedStars.measureDetectedStars()
LOG.info("HFD measurement. mean={}, stdDev={}", measure.averageHFD, measure.hfdStandardDeviation)
diff --git a/api/src/main/kotlin/nebulosa/api/beans/configurations/BeanConfiguration.kt b/api/src/main/kotlin/nebulosa/api/beans/configurations/BeanConfiguration.kt
index fe3a7d905..b03a02883 100644
--- a/api/src/main/kotlin/nebulosa/api/beans/configurations/BeanConfiguration.kt
+++ b/api/src/main/kotlin/nebulosa/api/beans/configurations/BeanConfiguration.kt
@@ -18,13 +18,10 @@ import nebulosa.guiding.Guider
import nebulosa.guiding.phd2.PHD2Guider
import nebulosa.hips2fits.Hips2FitsService
import nebulosa.horizons.HorizonsService
-import nebulosa.image.Image
import nebulosa.log.loggerFor
import nebulosa.phd2.client.PHD2Client
import nebulosa.sbd.SmallBodyDatabaseService
import nebulosa.simbad.SimbadService
-import nebulosa.star.detection.StarDetector
-import nebulosa.watney.star.detection.WatneyStarDetector
import okhttp3.Cache
import okhttp3.ConnectionPool
import okhttp3.OkHttpClient
@@ -159,10 +156,6 @@ class BeanConfiguration {
@Bean
fun phd2Guider(phd2Client: PHD2Client): Guider = PHD2Guider(phd2Client)
- @Bean
- @Primary
- fun watneyStarDetector(): StarDetector = WatneyStarDetector(computeHFD = true)
-
@Bean
@Primary
fun boxStore(dataPath: Path) = MyObjectBox.builder()
diff --git a/api/src/main/kotlin/nebulosa/api/image/ImageController.kt b/api/src/main/kotlin/nebulosa/api/image/ImageController.kt
index 63d9a58a6..985da8ff7 100644
--- a/api/src/main/kotlin/nebulosa/api/image/ImageController.kt
+++ b/api/src/main/kotlin/nebulosa/api/image/ImageController.kt
@@ -5,7 +5,6 @@ import jakarta.validation.Valid
import nebulosa.api.atlas.Location
import nebulosa.api.beans.converters.location.LocationParam
import nebulosa.indi.device.camera.Camera
-import nebulosa.star.detection.ImageStar
import org.hibernate.validator.constraints.Range
import org.springframework.http.HttpHeaders
import org.springframework.web.bind.annotation.*
@@ -54,11 +53,6 @@ class ImageController(
return imageService.coordinateInterpolation(path)
}
- @PutMapping("detect-stars")
- fun detectStars(@RequestParam path: Path): List {
- return imageService.detectStars(path)
- }
-
@GetMapping("histogram")
fun histogram(
@RequestParam path: Path,
diff --git a/api/src/main/kotlin/nebulosa/api/image/ImageService.kt b/api/src/main/kotlin/nebulosa/api/image/ImageService.kt
index 4b9303a4b..ff452a3e2 100644
--- a/api/src/main/kotlin/nebulosa/api/image/ImageService.kt
+++ b/api/src/main/kotlin/nebulosa/api/image/ImageService.kt
@@ -24,8 +24,6 @@ import nebulosa.simbad.SimbadSearch
import nebulosa.simbad.SimbadService
import nebulosa.skycatalog.ClassificationType
import nebulosa.skycatalog.SkyObjectType
-import nebulosa.star.detection.ImageStar
-import nebulosa.star.detection.StarDetector
import nebulosa.time.TimeYMDHMS
import nebulosa.time.UTC
import nebulosa.wcs.WCS
@@ -55,7 +53,6 @@ class ImageService(
private val imageBucket: ImageBucket,
private val threadPoolTaskExecutor: ThreadPoolTaskExecutor,
private val connectionService: ConnectionService,
- private val starDetector: StarDetector,
) {
private enum class ImageOperation {
@@ -333,11 +330,6 @@ class ImageService(
return CoordinateInterpolation(ma, md, 0, 0, width, height, delta, image.header.observationDate)
}
- fun detectStars(path: Path): List {
- val (image) = imageBucket[path] ?: return emptyList()
- return starDetector.detect(image)
- }
-
fun histogram(path: Path, bitLength: Int = 16): IntArray {
val (image) = imageBucket[path] ?: return IntArray(0)
return image.compute(Histogram(bitLength = bitLength))
diff --git a/api/src/main/kotlin/nebulosa/api/solver/PlateSolverController.kt b/api/src/main/kotlin/nebulosa/api/solver/PlateSolverController.kt
index 80c9e3d98..f4936f0fa 100644
--- a/api/src/main/kotlin/nebulosa/api/solver/PlateSolverController.kt
+++ b/api/src/main/kotlin/nebulosa/api/solver/PlateSolverController.kt
@@ -1,11 +1,9 @@
package nebulosa.api.solver
+import jakarta.validation.Valid
import nebulosa.api.beans.converters.angle.AngleParam
import nebulosa.math.Angle
-import org.springframework.web.bind.annotation.PutMapping
-import org.springframework.web.bind.annotation.RequestMapping
-import org.springframework.web.bind.annotation.RequestParam
-import org.springframework.web.bind.annotation.RestController
+import org.springframework.web.bind.annotation.*
import java.nio.file.Path
@RestController
@@ -17,10 +15,10 @@ class PlateSolverController(
@PutMapping
fun solveImage(
@RequestParam path: Path,
- options: PlateSolverOptions,
+ @RequestBody @Valid solver: PlateSolverOptions,
@RequestParam(required = false, defaultValue = "true") blind: Boolean,
@AngleParam(required = false, isHours = true, defaultValue = "0.0") centerRA: Angle,
@AngleParam(required = false, defaultValue = "0.0") centerDEC: Angle,
@AngleParam(required = false, defaultValue = "4.0") radius: Angle,
- ) = plateSolverService.solveImage(options, path, centerRA, centerDEC, if (blind) 0.0 else radius)
+ ) = plateSolverService.solveImage(solver, path, centerRA, centerDEC, if (blind) 0.0 else radius)
}
diff --git a/api/src/main/kotlin/nebulosa/api/solver/PlateSolverOptions.kt b/api/src/main/kotlin/nebulosa/api/solver/PlateSolverOptions.kt
index 0e3c8be85..1612c43cc 100644
--- a/api/src/main/kotlin/nebulosa/api/solver/PlateSolverOptions.kt
+++ b/api/src/main/kotlin/nebulosa/api/solver/PlateSolverOptions.kt
@@ -1,5 +1,10 @@
package nebulosa.api.solver
+import nebulosa.astap.plate.solving.AstapPlateSolver
+import nebulosa.astrometrynet.nova.NovaAstrometryNetService
+import nebulosa.astrometrynet.plate.solving.LocalAstrometryNetPlateSolver
+import nebulosa.astrometrynet.plate.solving.NovaAstrometryNetPlateSolver
+import okhttp3.OkHttpClient
import org.hibernate.validator.constraints.time.DurationMax
import org.hibernate.validator.constraints.time.DurationMin
import org.springframework.boot.convert.DurationUnit
@@ -17,8 +22,21 @@ data class PlateSolverOptions(
@JvmField val timeout: Duration = Duration.ZERO,
) {
+ fun get(httpClient: OkHttpClient? = null) = with(this) {
+ when (type) {
+ PlateSolverType.ASTAP -> AstapPlateSolver(executablePath!!)
+ PlateSolverType.ASTROMETRY_NET -> LocalAstrometryNetPlateSolver(executablePath!!)
+ PlateSolverType.ASTROMETRY_NET_ONLINE -> {
+ val key = "$apiUrl@$apiKey"
+ val service = NOVA_ASTROMETRY_NET_CACHE.getOrPut(key) { NovaAstrometryNetService(apiUrl, httpClient) }
+ NovaAstrometryNetPlateSolver(service, apiKey)
+ }
+ }
+ }
+
companion object {
@JvmStatic val EMPTY = PlateSolverOptions()
+ @JvmStatic private val NOVA_ASTROMETRY_NET_CACHE = HashMap()
}
}
diff --git a/api/src/main/kotlin/nebulosa/api/solver/PlateSolverService.kt b/api/src/main/kotlin/nebulosa/api/solver/PlateSolverService.kt
index 89e0ba376..61f0e0790 100644
--- a/api/src/main/kotlin/nebulosa/api/solver/PlateSolverService.kt
+++ b/api/src/main/kotlin/nebulosa/api/solver/PlateSolverService.kt
@@ -2,12 +2,7 @@ package nebulosa.api.solver
import nebulosa.api.image.ImageBucket
import nebulosa.api.image.ImageSolved
-import nebulosa.astap.plate.solving.AstapPlateSolver
-import nebulosa.astrometrynet.nova.NovaAstrometryNetService
-import nebulosa.astrometrynet.plate.solving.LocalAstrometryNetPlateSolver
-import nebulosa.astrometrynet.plate.solving.NovaAstrometryNetPlateSolver
import nebulosa.math.Angle
-import nebulosa.plate.solving.PlateSolver
import okhttp3.OkHttpClient
import org.springframework.stereotype.Service
import java.nio.file.Path
@@ -27,29 +22,10 @@ class PlateSolverService(
return ImageSolved(calibration)
}
- fun solverFor(options: PlateSolverOptions): PlateSolver {
- return with(options) {
- when (type) {
- PlateSolverType.ASTAP -> AstapPlateSolver(executablePath!!)
- PlateSolverType.ASTROMETRY_NET -> LocalAstrometryNetPlateSolver(executablePath!!)
- PlateSolverType.ASTROMETRY_NET_ONLINE -> {
- val key = "$apiUrl@$apiKey"
- val service = NOVA_ASTROMETRY_NET_CACHE.getOrPut(key) { NovaAstrometryNetService(apiUrl, httpClient) }
- NovaAstrometryNetPlateSolver(service, apiKey)
- }
- }
- }
- }
-
@Synchronized
fun solve(
options: PlateSolverOptions, path: Path,
centerRA: Angle = 0.0, centerDEC: Angle = 0.0, radius: Angle = 0.0,
- ) = solverFor(options)
+ ) = options.get(httpClient)
.solve(path, null, centerRA, centerDEC, radius, 1, options.timeout.takeIf { it.toSeconds() > 0 })
-
- companion object {
-
- @JvmStatic private val NOVA_ASTROMETRY_NET_CACHE = HashMap()
- }
}
diff --git a/api/src/main/kotlin/nebulosa/api/stardetection/StarDetectionController.kt b/api/src/main/kotlin/nebulosa/api/stardetection/StarDetectionController.kt
new file mode 100644
index 000000000..810c13811
--- /dev/null
+++ b/api/src/main/kotlin/nebulosa/api/stardetection/StarDetectionController.kt
@@ -0,0 +1,18 @@
+package nebulosa.api.stardetection
+
+import jakarta.validation.Valid
+import org.springframework.validation.annotation.Validated
+import org.springframework.web.bind.annotation.*
+import java.nio.file.Path
+
+@Validated
+@RestController
+@RequestMapping("star-detection")
+class StarDetectionController(private val starDetectionService: StarDetectionService) {
+
+ @PutMapping
+ fun detectStars(
+ @RequestParam path: Path,
+ @RequestBody @Valid body: StarDetectionOptions
+ ) = starDetectionService.detectStars(path, body)
+}
diff --git a/api/src/main/kotlin/nebulosa/api/stardetection/StarDetectionOptions.kt b/api/src/main/kotlin/nebulosa/api/stardetection/StarDetectionOptions.kt
new file mode 100644
index 000000000..5f8aa4940
--- /dev/null
+++ b/api/src/main/kotlin/nebulosa/api/stardetection/StarDetectionOptions.kt
@@ -0,0 +1,23 @@
+package nebulosa.api.stardetection
+
+import nebulosa.astap.star.detection.AstapStarDetector
+import nebulosa.star.detection.StarDetector
+import java.nio.file.Path
+import java.time.Duration
+import java.util.function.Supplier
+
+data class StarDetectionOptions(
+ @JvmField val type: StarDetectorType = StarDetectorType.ASTAP,
+ @JvmField val executablePath: Path? = null,
+ @JvmField val timeout: Duration = Duration.ZERO,
+) : Supplier> {
+
+ override fun get() = when (type) {
+ StarDetectorType.ASTAP -> AstapStarDetector(executablePath!!)
+ }
+
+ companion object {
+
+ @JvmStatic val EMPTY = StarDetectionOptions()
+ }
+}
diff --git a/api/src/main/kotlin/nebulosa/api/stardetection/StarDetectionService.kt b/api/src/main/kotlin/nebulosa/api/stardetection/StarDetectionService.kt
new file mode 100644
index 000000000..243239456
--- /dev/null
+++ b/api/src/main/kotlin/nebulosa/api/stardetection/StarDetectionService.kt
@@ -0,0 +1,14 @@
+package nebulosa.api.stardetection
+
+import nebulosa.star.detection.ImageStar
+import org.springframework.stereotype.Service
+import java.nio.file.Path
+
+@Service
+class StarDetectionService {
+
+ fun detectStars(path: Path, options: StarDetectionOptions): List {
+ val starDetector = options.get()
+ return starDetector.detect(path)
+ }
+}
diff --git a/api/src/main/kotlin/nebulosa/api/stardetection/StarDetectorType.kt b/api/src/main/kotlin/nebulosa/api/stardetection/StarDetectorType.kt
new file mode 100644
index 000000000..31ae2f97c
--- /dev/null
+++ b/api/src/main/kotlin/nebulosa/api/stardetection/StarDetectorType.kt
@@ -0,0 +1,5 @@
+package nebulosa.api.stardetection
+
+enum class StarDetectorType {
+ ASTAP
+}
diff --git a/desktop/src/app/alignment/alignment.component.ts b/desktop/src/app/alignment/alignment.component.ts
index 769d1430b..25bbae873 100644
--- a/desktop/src/app/alignment/alignment.component.ts
+++ b/desktop/src/app/alignment/alignment.component.ts
@@ -9,7 +9,7 @@ import { Angle } from '../../shared/types/atlas.types'
import { Camera, EMPTY_CAMERA, EMPTY_CAMERA_START_CAPTURE, ExposureTimeUnit, updateCameraStartCaptureFromCamera } from '../../shared/types/camera.types'
import { EMPTY_GUIDE_OUTPUT, GuideDirection, GuideOutput } from '../../shared/types/guider.types'
import { EMPTY_MOUNT, Mount } from '../../shared/types/mount.types'
-import { EMPTY_PLATE_SOLVER_PREFERENCE } from '../../shared/types/settings.types'
+import { EMPTY_PLATE_SOLVER_OPTIONS } from '../../shared/types/settings.types'
import { deviceComparator } from '../../shared/utils/comparators'
import { AppComponent } from '../app.component'
import { CameraComponent } from '../camera/camera.component'
@@ -39,7 +39,7 @@ export class AlignmentComponent implements AfterViewInit, OnDestroy {
readonly tppaRequest: TPPAStart = {
capture: structuredClone(EMPTY_CAMERA_START_CAPTURE),
- plateSolver: structuredClone(EMPTY_PLATE_SOLVER_PREFERENCE),
+ plateSolver: structuredClone(EMPTY_PLATE_SOLVER_OPTIONS),
startFromCurrentPosition: true,
stepDirection: 'EAST',
compensateRefraction: true,
@@ -279,7 +279,7 @@ export class AlignmentComponent implements AfterViewInit, OnDestroy {
}
plateSolverChanged() {
- this.tppaRequest.plateSolver = this.preference.plateSolverPreference(this.tppaRequest.plateSolver.type).get()
+ this.tppaRequest.plateSolver = this.preference.plateSolverOptions(this.tppaRequest.plateSolver.type).get()
this.savePreference()
}
diff --git a/desktop/src/app/autofocus/autofocus.component.html b/desktop/src/app/autofocus/autofocus.component.html
index 610a59a5b..41eb277f7 100644
--- a/desktop/src/app/autofocus/autofocus.component.html
+++ b/desktop/src/app/autofocus/autofocus.component.html
@@ -15,51 +15,64 @@
-
-
-
-
-
-
-
+
-
-
+
+
-
+
-
+
-
-
+
-
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
diff --git a/desktop/src/app/autofocus/autofocus.component.ts b/desktop/src/app/autofocus/autofocus.component.ts
index 61aec9bbe..71a861a67 100644
--- a/desktop/src/app/autofocus/autofocus.component.ts
+++ b/desktop/src/app/autofocus/autofocus.component.ts
@@ -6,6 +6,7 @@ import { PreferenceService } from '../../shared/services/preference.service'
import { AutoFocusPreference, AutoFocusRequest } from '../../shared/types/autofocus.type'
import { Camera, EMPTY_CAMERA, EMPTY_CAMERA_START_CAPTURE, updateCameraStartCaptureFromCamera } from '../../shared/types/camera.types'
import { EMPTY_FOCUSER, Focuser } from '../../shared/types/focuser.types'
+import { EMPTY_STAR_DETECTION_OPTIONS } from '../../shared/types/settings.types'
import { deviceComparator } from '../../shared/utils/comparators'
import { AppComponent } from '../app.component'
import { CameraComponent } from '../camera/camera.component'
@@ -36,7 +37,8 @@ export class AutoFocusComponent implements AfterViewInit, OnDestroy {
},
initialOffsetSteps: 4,
stepSize: 100,
- totalNumberOfAttempts: 1
+ totalNumberOfAttempts: 1,
+ starDetector: structuredClone(EMPTY_STAR_DETECTION_OPTIONS),
}
constructor(
@@ -142,8 +144,9 @@ export class AutoFocusComponent implements AfterViewInit, OnDestroy {
}
}
- start() {
- this.browserWindow.openCameraImage(this.camera, 'AUTO_FOCUS')
+ async start() {
+ await this.openCameraImage()
+ this.request.starDetector = this.preference.starDetectionOptions('ASTAP').get()
return this.api.autoFocusStart(this.camera, this.focuser, this.request)
}
@@ -152,7 +155,7 @@ export class AutoFocusComponent implements AfterViewInit, OnDestroy {
}
openCameraImage() {
- return this.browserWindow.openCameraImage(this.camera)
+ return this.browserWindow.openCameraImage(this.camera, 'ALIGNMENT')
}
private loadPreference() {
diff --git a/desktop/src/app/image/image.component.ts b/desktop/src/app/image/image.component.ts
index 956ba9d9e..1f06971c5 100644
--- a/desktop/src/app/image/image.component.ts
+++ b/desktop/src/app/image/image.component.ts
@@ -333,7 +333,8 @@ export class ImageComponent implements AfterViewInit, OnDestroy {
toggleable: false,
toggled: false,
command: async () => {
- this.detectedStars.stars = await this.api.detectStars(this.imageData.path!)
+ const options = this.preference.starDetectionOptions('ASTAP').get()
+ this.detectedStars.stars = await this.api.detectStars(this.imageData.path!, options)
this.detectedStars.visible = this.detectedStars.stars.length > 0
this.detectStarsMenuItem.toggleable = this.detectedStars.visible
this.detectStarsMenuItem.toggled = this.detectedStars.visible
@@ -947,7 +948,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy {
this.solver.solving = true
try {
- const solver = this.preference.plateSolverPreference(this.solver.type).get()
+ const solver = this.preference.plateSolverOptions(this.solver.type).get()
const solved = await this.api.solveImage(solver, this.imageData.path!, this.solver.blind,
this.solver.centerRA, this.solver.centerDEC, this.solver.radius)
@@ -957,7 +958,10 @@ export class ImageComponent implements AfterViewInit, OnDestroy {
this.updateImageSolved(this.imageInfo?.solved)
} finally {
this.solver.solving = false
- this.retrieveCoordinateInterpolation()
+
+ if (this.solver.solved.solved) {
+ this.retrieveCoordinateInterpolation()
+ }
}
}
diff --git a/desktop/src/app/settings/settings.component.html b/desktop/src/app/settings/settings.component.html
index e2084571b..0931108e9 100644
--- a/desktop/src/app/settings/settings.component.html
+++ b/desktop/src/app/settings/settings.component.html
@@ -19,7 +19,7 @@
-
@@ -42,7 +42,7 @@
(ngModelChange)="solvers.get(solverType)!.executablePath = $event; save()" />
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/desktop/src/app/settings/settings.component.ts b/desktop/src/app/settings/settings.component.ts
index 7c288e160..2d8931004 100644
--- a/desktop/src/app/settings/settings.component.ts
+++ b/desktop/src/app/settings/settings.component.ts
@@ -6,7 +6,7 @@ import { ElectronService } from '../../shared/services/electron.service'
import { PreferenceService } from '../../shared/services/preference.service'
import { PrimeService } from '../../shared/services/prime.service'
import { EMPTY_LOCATION, Location } from '../../shared/types/atlas.types'
-import { PlateSolverPreference, PlateSolverType } from '../../shared/types/settings.types'
+import { PlateSolverOptions, PlateSolverType, StarDetectionOptions, StarDetectorType } from '../../shared/types/settings.types'
import { AppComponent } from '../app.component'
@Component({
@@ -19,9 +19,11 @@ export class SettingsComponent implements AfterViewInit, OnDestroy {
readonly locations: Location[]
location: Location
- readonly solverTypes: PlateSolverType[] = ['ASTAP', 'ASTROMETRY_NET_ONLINE']
- solverType = this.solverTypes[0]
- readonly solvers = new Map ()
+ solverType: PlateSolverType = 'ASTAP'
+ readonly solvers = new Map()
+
+ starDetectorType: StarDetectorType = 'ASTAP'
+ readonly starDetectors = new Map()
constructor(
app: AppComponent,
@@ -35,9 +37,10 @@ export class SettingsComponent implements AfterViewInit, OnDestroy {
this.locations = preference.locations.get()
this.location = preference.selectedLocation.get(this.locations[0])
- for (const type of this.solverTypes) {
- this.solvers.set(type, preference.plateSolverPreference(type).get())
- }
+ this.solvers.set('ASTAP', preference.plateSolverOptions('ASTAP').get())
+ this.solvers.set('ASTROMETRY_NET_ONLINE', preference.plateSolverOptions('ASTROMETRY_NET_ONLINE').get())
+
+ this.starDetectors.set('ASTAP', preference.starDetectionOptions('ASTAP').get())
}
async ngAfterViewInit() { }
@@ -99,19 +102,31 @@ export class SettingsComponent implements AfterViewInit, OnDestroy {
this.electron.send('LOCATION.CHANGED', this.location)
}
- async chooseExecutablePath() {
+ async chooseExecutablePathForPlateSolver() {
const options = this.solvers.get(this.solverType)!
+ this.chooseExecutablePath(options)
+ }
+
+ async chooseExecutablePathForStarDetection() {
+ const options = this.solvers.get(this.starDetectorType)!
+ this.chooseExecutablePath(options)
+ }
+
+ private async chooseExecutablePath(options: { executablePath: string }) {
const executablePath = await this.electron.openFile({ defaultPath: path.dirname(options.executablePath) })
if (executablePath) {
options.executablePath = executablePath
this.save()
}
+
+ return executablePath
}
save() {
- for (const type of this.solverTypes) {
- this.preference.plateSolverPreference(type).set(this.solvers.get(type)!)
- }
+ this.preference.plateSolverOptions('ASTAP').set(this.solvers.get('ASTAP')!)
+ this.preference.plateSolverOptions('ASTROMETRY_NET_ONLINE').set(this.solvers.get('ASTROMETRY_NET_ONLINE')!)
+
+ this.preference.starDetectionOptions('ASTAP').set(this.starDetectors.get('ASTAP')!)
}
}
\ No newline at end of file
diff --git a/desktop/src/shared/services/api.service.ts b/desktop/src/shared/services/api.service.ts
index 223d97d0b..56db681fa 100644
--- a/desktop/src/shared/services/api.service.ts
+++ b/desktop/src/shared/services/api.service.ts
@@ -15,7 +15,7 @@ import { CoordinateInterpolation, DetectedStar, FOVCamera, FOVTelescope, ImageAn
import { CelestialLocationType, Mount, MountRemoteControl, MountRemoteControlType, SlewRate, TrackMode } from '../types/mount.types'
import { Rotator } from '../types/rotator.types'
import { SequencePlan } from '../types/sequencer.types'
-import { PlateSolverPreference } from '../types/settings.types'
+import { PlateSolverOptions, StarDetectionOptions } from '../types/settings.types'
import { FilterWheel } from '../types/wheel.types'
import { HttpService } from './http.service'
@@ -531,9 +531,9 @@ export class ApiService {
return this.http.get(`image/coordinate-interpolation?${query}`)
}
- detectStars(path: string) {
+ detectStars(path: string, starDetector: StarDetectionOptions) {
const query = this.http.query({ path })
- return this.http.put(`image/detect-stars?${query}`)
+ return this.http.put(`star-detection?${query}`, starDetector)
}
imageHistogram(path: string, bitLength: number = 16) {
@@ -640,11 +640,11 @@ export class ApiService {
// SOLVER
solveImage(
- solver: PlateSolverPreference, path: string, blind: boolean,
+ solver: PlateSolverOptions, path: string, blind: boolean,
centerRA: Angle, centerDEC: Angle, radius: Angle,
) {
- const query = this.http.query({ ...solver, path, blind, centerRA, centerDEC, radius })
- return this.http.put(`plate-solver?${query}`)
+ const query = this.http.query({ path, blind, centerRA, centerDEC, radius })
+ return this.http.put(`plate-solver?${query}`, solver)
}
// AUTO FOCUS
diff --git a/desktop/src/shared/services/browser-window.service.ts b/desktop/src/shared/services/browser-window.service.ts
index ac77d57fd..041cdc134 100644
--- a/desktop/src/shared/services/browser-window.service.ts
+++ b/desktop/src/shared/services/browser-window.service.ts
@@ -105,7 +105,7 @@ export class BrowserWindowService {
}
openAutoFocus(options: OpenWindowOptions = {}) {
- Object.assign(options, { icon: 'auto-focus', width: 385, height: 370 })
+ Object.assign(options, { icon: 'auto-focus', width: 410, height: 370 })
this.openWindow({ ...options, id: 'auto-focus', path: 'auto-focus', data: undefined })
}
diff --git a/desktop/src/shared/services/preference.service.ts b/desktop/src/shared/services/preference.service.ts
index 46eec8892..92808fee0 100644
--- a/desktop/src/shared/services/preference.service.ts
+++ b/desktop/src/shared/services/preference.service.ts
@@ -10,7 +10,7 @@ import { Focuser, FocuserPreference } from '../types/focuser.types'
import { ConnectionDetails, Equipment, HomePreference } from '../types/home.types'
import { EMPTY_IMAGE_PREFERENCE, FOV, ImagePreference } from '../types/image.types'
import { Rotator, RotatorPreference } from '../types/rotator.types'
-import { EMPTY_PLATE_SOLVER_PREFERENCE, PlateSolverPreference, PlateSolverType } from '../types/settings.types'
+import { EMPTY_PLATE_SOLVER_OPTIONS, EMPTY_STAR_DETECTION_OPTIONS, PlateSolverOptions, PlateSolverType, StarDetectionOptions, StarDetectorType } from '../types/settings.types'
import { FilterWheel, WheelPreference } from '../types/wheel.types'
import { LocalStorageService } from './local-storage.service'
@@ -68,8 +68,12 @@ export class PreferenceService {
return new PreferenceData(this.storage, `camera.${camera.name}.autoFocus`, () => this.cameraPreference(camera).get())
}
- plateSolverPreference(type: PlateSolverType) {
- return new PreferenceData(this.storage, `plateSolver.${type}`, () => { ...EMPTY_PLATE_SOLVER_PREFERENCE, type })
+ plateSolverOptions(type: PlateSolverType) {
+ return new PreferenceData(this.storage, `plateSolver.${type}`, () => { ...EMPTY_PLATE_SOLVER_OPTIONS, type })
+ }
+
+ starDetectionOptions(type: StarDetectorType) {
+ return new PreferenceData(this.storage, `starDetection.${type}`, () => { ...EMPTY_STAR_DETECTION_OPTIONS, type })
}
equipmentForDevice(device: Device) {
diff --git a/desktop/src/shared/types/alignment.types.ts b/desktop/src/shared/types/alignment.types.ts
index 3b48be378..19b4ddfd0 100644
--- a/desktop/src/shared/types/alignment.types.ts
+++ b/desktop/src/shared/types/alignment.types.ts
@@ -1,7 +1,7 @@
import { Angle } from './atlas.types'
import { Camera, CameraCaptureEvent, CameraStartCapture } from './camera.types'
import { GuideDirection } from './guider.types'
-import { PlateSolverPreference, PlateSolverType } from './settings.types'
+import { PlateSolverOptions, PlateSolverType } from './settings.types'
export type Hemisphere = 'NORTHERN' | 'SOUTHERN'
@@ -52,7 +52,7 @@ export interface DARVEvent extends MessageEvent {
export interface TPPAStart {
capture: CameraStartCapture
- plateSolver: PlateSolverPreference
+ plateSolver: PlateSolverOptions
startFromCurrentPosition: boolean
compensateRefraction: boolean
stopTrackingWhenDone: boolean
diff --git a/desktop/src/shared/types/autofocus.type.ts b/desktop/src/shared/types/autofocus.type.ts
index 7bb415e5c..aa24a424b 100644
--- a/desktop/src/shared/types/autofocus.type.ts
+++ b/desktop/src/shared/types/autofocus.type.ts
@@ -1,4 +1,5 @@
import { CameraStartCapture } from './camera.types'
+import { EMPTY_STAR_DETECTION_OPTIONS, StarDetectionOptions } from './settings.types'
export type AutoFocusFittingMode = 'TRENDLINES' | 'PARABOLIC' | 'TREND_PARABOLIC' | 'HYPERBOLIC' | 'TREND_HYPERBOLIC'
@@ -18,6 +19,7 @@ export interface AutoFocusRequest {
initialOffsetSteps: number
stepSize: number
totalNumberOfAttempts: number
+ starDetector: StarDetectionOptions
}
export interface AutoFocusPreference extends Omit { }
@@ -32,5 +34,6 @@ export const EMPTY_AUTO_FOCUS_PREFERENCE: AutoFocusPreference = {
mode: 'NONE',
backlashIn: 0,
backlashOut: 0
- }
+ },
+ starDetector: EMPTY_STAR_DETECTION_OPTIONS,
}
diff --git a/desktop/src/shared/types/settings.types.ts b/desktop/src/shared/types/settings.types.ts
index 7b26a7f55..b427182f6 100644
--- a/desktop/src/shared/types/settings.types.ts
+++ b/desktop/src/shared/types/settings.types.ts
@@ -1,7 +1,6 @@
-
export type PlateSolverType = 'ASTROMETRY_NET' | 'ASTROMETRY_NET_ONLINE' | 'ASTAP'
-export interface PlateSolverPreference {
+export interface PlateSolverOptions {
type: PlateSolverType
executablePath: string
downsampleFactor: number
@@ -10,7 +9,7 @@ export interface PlateSolverPreference {
timeout: number
}
-export const EMPTY_PLATE_SOLVER_PREFERENCE: PlateSolverPreference = {
+export const EMPTY_PLATE_SOLVER_OPTIONS: PlateSolverOptions = {
type: 'ASTAP',
executablePath: '',
downsampleFactor: 0,
@@ -18,3 +17,17 @@ export const EMPTY_PLATE_SOLVER_PREFERENCE: PlateSolverPreference = {
apiKey: '',
timeout: 600,
}
+
+export type StarDetectorType = 'ASTAP'
+
+export interface StarDetectionOptions {
+ type: StarDetectorType
+ executablePath: string
+ timeout: number
+}
+
+export const EMPTY_STAR_DETECTION_OPTIONS: StarDetectionOptions = {
+ type: 'ASTAP',
+ executablePath: '',
+ timeout: 600,
+}
diff --git a/nebulosa-astap/src/main/kotlin/nebulosa/astap/star/detection/AstapStarDetector.kt b/nebulosa-astap/src/main/kotlin/nebulosa/astap/star/detection/AstapStarDetector.kt
index 87306d63d..6c25aa07b 100644
--- a/nebulosa-astap/src/main/kotlin/nebulosa/astap/star/detection/AstapStarDetector.kt
+++ b/nebulosa-astap/src/main/kotlin/nebulosa/astap/star/detection/AstapStarDetector.kt
@@ -21,7 +21,7 @@ class AstapStarDetector(path: Path) : StarDetector {
val arguments = mutableMapOf()
arguments["-f"] = input
- arguments["-z"] = 2
+ arguments["-z"] = 0
arguments["-extract"] = 0
val process = executor.execute(arguments, workingDir = input.parent)
From 32e5c0f337ddcd30421b50a60792b7ca50a6bdba Mon Sep 17 00:00:00 2001
From: tiagohm
Date: Thu, 30 May 2024 17:38:18 -0300
Subject: [PATCH 29/45] [api][desktop]: Support Auto Focus
---
.../api/autofocus/AutoFocusRequest.kt | 2 +-
.../nebulosa/api/autofocus/AutoFocusTask.kt | 128 ++++++++----------
.../api/focusers/AbstractFocuserMoveTask.kt | 8 +-
.../BacklashCompensationFocuserMoveTask.kt | 70 +++++-----
.../api/focusers/BacklashCompensationMode.kt | 1 +
api/src/test/kotlin/APITest.kt | 13 +-
.../src/app/autofocus/autofocus.component.ts | 15 +-
desktop/src/shared/types/autofocus.type.ts | 2 +-
.../alpaca/indi/device/ASCOMDevice.kt | 4 +-
.../indi/device/focusers/ASCOMFocuser.kt | 27 ++--
.../nebulosa/curve/fitting/CurvePoint.kt | 2 +-
.../curve/fitting/TrendLineFitting.kt | 7 +-
.../src/test/kotlin/AutoFocusTest.kt | 64 +++++++++
13 files changed, 201 insertions(+), 142 deletions(-)
create mode 100644 nebulosa-curve-fitting/src/test/kotlin/AutoFocusTest.kt
diff --git a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusRequest.kt b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusRequest.kt
index 815209a3b..5f1c5edd1 100644
--- a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusRequest.kt
+++ b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusRequest.kt
@@ -7,7 +7,7 @@ import nebulosa.api.stardetection.StarDetectionOptions
data class AutoFocusRequest(
@JvmField val fittingMode: AutoFocusFittingMode = AutoFocusFittingMode.HYPERBOLIC,
@JvmField val capture: CameraStartCaptureRequest = CameraStartCaptureRequest.EMPTY,
- @JvmField val rSquaredThreshold: Double = 0.7,
+ @JvmField val rSquaredThreshold: Double = 0.5,
@JvmField val backlashCompensation: BacklashCompensation = BacklashCompensation.EMPTY,
@JvmField val initialOffsetSteps: Int = 4,
@JvmField val stepSize: Int = 50,
diff --git a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt
index 688e29ba8..ef0348e28 100644
--- a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt
+++ b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt
@@ -26,7 +26,6 @@ import java.nio.file.Path
import java.time.Duration
import kotlin.math.max
import kotlin.math.roundToInt
-import kotlin.math.sqrt
data class AutoFocusTask(
@JvmField val camera: Camera,
@@ -35,17 +34,6 @@ data class AutoFocusTask(
@JvmField val starDetection: StarDetector,
) : AbstractTask(), Consumer, CameraEventAware, FocuserEventAware {
- data class MeasuredStars(
- @JvmField val averageHFD: Double = 0.0,
- @JvmField val hfdStandardDeviation: Double = 0.0,
- ) {
-
- companion object {
-
- @JvmStatic val ZERO = MeasuredStars()
- }
- }
-
@JvmField val cameraRequest = request.capture.copy(
exposureAmount = 0, exposureDelay = Duration.ZERO,
savePath = CAPTURE_SAVE_PATH,
@@ -53,9 +41,8 @@ data class AutoFocusTask(
frameType = FrameType.LIGHT, autoSave = false, autoSubFolderMode = AutoSubFolderMode.OFF
)
-
private val focusPoints = ArrayList()
- private val measurements = ArrayList(request.capture.exposureAmount)
+ private val measurements = DoubleArray(request.capture.exposureAmount)
private val cameraCaptureTask = CameraCaptureTask(camera, cameraRequest, exposureMaxRepeat = max(1, request.capture.exposureAmount))
private val focuserMoveTask = BacklashCompensationFocuserMoveTask(focuser, 0, request.backlashCompensation)
@@ -63,6 +50,7 @@ data class AutoFocusTask(
@Volatile private var parabolicCurve: Lazy? = null
@Volatile private var hyperbolicCurve: Lazy? = null
+ @Volatile private var measurementPos = 0
@Volatile private var focusPoint = CurvePoint.ZERO
init {
@@ -85,11 +73,10 @@ data class AutoFocusTask(
val initialFocusPosition = focuser.position
// Get initial position information, as average of multiple exposures, if configured this way.
- val initialHFD = if (request.rSquaredThreshold <= 0.0) takeExposure(cancellationToken).averageHFD else Double.NaN
- val reverse = request.backlashCompensation.mode == BacklashCompensationMode.OVERSHOOT && request.backlashCompensation.backlashIn > 0 &&
- request.backlashCompensation.backlashOut == 0
+ val initialHFD = if (request.rSquaredThreshold <= 0.0) takeExposure(cancellationToken) else 0.0
+ val reverse = request.backlashCompensation.mode == BacklashCompensationMode.OVERSHOOT && request.backlashCompensation.backlashIn > 0
- LOG.info("Auto Focus started. initialHFD={}, reverse={}, camera={}, focuser={}", initialHFD, reverse, camera, focuser)
+ LOG.info("Auto Focus started. initialHFD={}, reverse={}, request={}, camera={}, focuser={}", initialHFD, reverse, request, camera, focuser)
var exited = false
var numberOfAttempts = 0
@@ -107,8 +94,10 @@ data class AutoFocusTask(
obtainFocusPoints(numberOfSteps, offsetSteps, reverse, cancellationToken)
- var leftCount = trendLineCurve!!.left.points.size
- var rightCount = trendLineCurve!!.right.points.size
+ var leftCount = trendLineCurve?.left?.points?.size ?: 0
+ var rightCount = trendLineCurve?.right?.points?.size ?: 0
+
+ LOG.info("trend line computed. left=$leftCount, right=$rightCount")
// When data points are not sufficient analyze and take more.
do {
@@ -152,6 +141,8 @@ data class AutoFocusTask(
leftCount = trendLineCurve!!.left.points.size
rightCount = trendLineCurve!!.right.points.size
+ LOG.info("trend line computed. left=$leftCount, right=$rightCount")
+
if (maximumFocusPoints < focusPoints.size) {
// Break out when the maximum limit of focus points is reached
LOG.error("failed to complete. Maximum number of focus points exceeded ($maximumFocusPoints).")
@@ -173,27 +164,32 @@ data class AutoFocusTask(
if (!goodAutoFocus) {
if (numberOfAttempts < request.totalNumberOfAttempts) {
moveFocuser(initialFocusPosition, cancellationToken, false)
- LOG.warn("potentially bad auto-focus. reattempting")
+ LOG.warn("potentially bad auto-focus. Reattempting")
reset()
continue
} else {
LOG.warn("potentially bad auto-focus. Restoring original focus position")
+ exited = true
}
} else {
LOG.info("Auto Focus completed. x={}, y={}", finalFocusPoint.x, finalFocusPoint.y)
+ moveFocuser(finalFocusPoint.x.roundToInt(), cancellationToken, false)
+ break
}
-
- exited = true
}
- if (exited) {
- sendEvent(AutoFocusState.FAILED)
+ if (exited || cancellationToken.isCancelled) {
LOG.warn("Auto Focus did not complete successfully, so restoring the focuser position to $initialFocusPosition")
- moveFocuser(initialFocusPosition, CancellationToken.NONE, false)
+ sendEvent(if (exited) AutoFocusState.FAILED else AutoFocusState.FINISHED)
+
+ if (exited) {
+ moveFocuser(initialFocusPosition, CancellationToken.NONE, false)
+ }
+ } else {
+ sendEvent(AutoFocusState.FINISHED)
}
reset()
- sendEvent(AutoFocusState.FINISHED)
LOG.info("Auto Focus finished. camera={}, focuser={}", camera, focuser)
}
@@ -212,16 +208,8 @@ data class AutoFocusTask(
}
}
- private fun evaluateAllMeasurements(): MeasuredStars {
- var sumHFD = 0.0
- var sumVariances = 0.0
-
- for ((averageHFD, hfdStandardDeviation) in measurements) {
- sumHFD += averageHFD
- sumVariances += hfdStandardDeviation * hfdStandardDeviation
- }
-
- return MeasuredStars(sumHFD / measurements.size, sqrt(sumVariances / measurements.size))
+ private fun evaluateAllMeasurements(): Double {
+ return if (measurements.isEmpty()) 0.0 else measurements.average()
}
override fun accept(event: CameraCaptureEvent) {
@@ -230,22 +218,22 @@ data class AutoFocusTask(
val detectedStars = starDetection.detect(event.savePath!!)
LOG.info("detected ${detectedStars.size} stars")
val measure = detectedStars.measureDetectedStars()
- LOG.info("HFD measurement. mean={}, stdDev={}", measure.averageHFD, measure.hfdStandardDeviation)
- measurements.add(measure)
+ LOG.info("HFD measurement. mean={}", measure)
+ measurements[measurementPos++] = measure
onNext(event)
} else {
sendEvent(AutoFocusState.EXPOSURING, capture = event)
}
}
- private fun takeExposure(cancellationToken: CancellationToken): MeasuredStars {
+ private fun takeExposure(cancellationToken: CancellationToken): Double {
return if (!cancellationToken.isCancelled) {
- measurements.clear()
+ measurementPos = 0
sendEvent(AutoFocusState.EXPOSURING)
cameraCaptureTask.execute(cancellationToken)
evaluateAllMeasurements()
} else {
- MeasuredStars.ZERO
+ 0.0
}
}
@@ -266,19 +254,20 @@ data class AutoFocusTask(
while (!cancellationToken.isCancelled && remainingSteps > 0) {
val currentFocusPosition = focusPosition
- if (remainingSteps > 1) {
+ val measurement = takeExposure(cancellationToken)
+
+ LOG.info("HFD measured after exposures. mean={}", measurement)
+
+ if (remainingSteps-- > 1) {
focusPosition = moveFocuser(direction * -stepSize, cancellationToken, true)
}
- val measurement = takeExposure(cancellationToken)
-
// If star measurement is 0, we didn't detect any stars or shapes,
// and want this point to be ignored by the fitting as much as possible.
- if (measurement.averageHFD == 0.0) {
+ if (measurement == 0.0) {
LOG.warn("No stars detected in step")
- sendEvent(AutoFocusState.FAILED)
} else {
- focusPoint = CurvePoint(currentFocusPosition.toDouble(), measurement.averageHFD)
+ focusPoint = CurvePoint(currentFocusPosition.toDouble(), measurement)
focusPoints.add(focusPoint)
focusPoints.sortBy { it.x }
@@ -288,8 +277,6 @@ data class AutoFocusTask(
sendEvent(AutoFocusState.FOCUS_POINT_ADDED)
}
-
- remainingSteps--
}
}
@@ -300,8 +287,7 @@ data class AutoFocusTask(
if (size >= 3) {
if (request.fittingMode == AutoFocusFittingMode.PARABOLIC || request.fittingMode == AutoFocusFittingMode.TREND_PARABOLIC) {
parabolicCurve = lazy { QuadraticFitting.calculate(this) }
- }
- if (request.fittingMode == AutoFocusFittingMode.HYPERBOLIC || request.fittingMode == AutoFocusFittingMode.TREND_HYPERBOLIC) {
+ } else if (request.fittingMode == AutoFocusFittingMode.HYPERBOLIC || request.fittingMode == AutoFocusFittingMode.TREND_HYPERBOLIC) {
hyperbolicCurve = lazy { HyperbolicFitting.calculate(this) }
}
}
@@ -311,11 +297,13 @@ data class AutoFocusTask(
private fun validateCalculatedFocusPosition(focusPoint: CurvePoint, initialHFD: Double, cancellationToken: CancellationToken): Boolean {
val threshold = request.rSquaredThreshold
- fun isTrendLineBad() = trendLineCurve?.let { it.left.rSquared < threshold || it.right.rSquared < threshold } ?: true
- fun isParabolicBad() = parabolicCurve?.value?.let { it.rSquared < threshold } ?: true
- fun isHyperbolicBad() = hyperbolicCurve?.value?.let { it.rSquared < threshold } ?: true
+ LOG.info("validating calculated focus position. threshold={}", threshold)
if (threshold > 0.0) {
+ fun isTrendLineBad() = trendLineCurve?.let { it.left.rSquared < threshold || it.right.rSquared < threshold } ?: true
+ fun isParabolicBad() = parabolicCurve?.value?.let { it.rSquared < threshold } ?: true
+ fun isHyperbolicBad() = hyperbolicCurve?.value?.let { it.rSquared < threshold } ?: true
+
val isBad = when (request.fittingMode) {
AutoFocusFittingMode.TRENDLINES -> isTrendLineBad()
AutoFocusFittingMode.PARABOLIC -> isParabolicBad()
@@ -339,7 +327,7 @@ data class AutoFocusTask(
}
moveFocuser(focusPoint.x.roundToInt(), cancellationToken, false)
- val hfd = takeExposure(cancellationToken).averageHFD
+ val hfd = takeExposure(cancellationToken)
if (threshold <= 0) {
if (initialHFD != 0.0 && hfd > initialHFD * 1.15) {
@@ -384,25 +372,17 @@ data class AutoFocusTask(
@JvmStatic private val LOG = loggerFor()
@JvmStatic
- private fun List.measureDetectedStars(): MeasuredStars {
- if (isEmpty()) return MeasuredStars.ZERO
-
- val mean = sumOf { it.hfd } / size
-
- var stdDev = 0.0
-
- if (size > 1) {
- for (star in this) {
- stdDev += (star.hfd - mean).let { it * it }
- }
-
- stdDev /= size - 1
- stdDev = sqrt(stdDev)
- } else {
- stdDev = Double.NaN
- }
+ private fun DoubleArray.median(): Double {
+ return if (size % 2 == 0) (this[size / 2] + this[size / 2 - 1]) / 2.0
+ else this[size / 2]
+ }
- return MeasuredStars(mean, stdDev)
+ @JvmStatic
+ private fun List.measureDetectedStars(): Double {
+ return if (isEmpty()) 0.0
+ else if (size == 1) this[0].hfd
+ else if (size == 2) (this[0].hfd + this[1].hfd) / 2.0
+ else DoubleArray(size) { this[it].hfd }.also { it.sort() }.median()
}
}
}
diff --git a/api/src/main/kotlin/nebulosa/api/focusers/AbstractFocuserMoveTask.kt b/api/src/main/kotlin/nebulosa/api/focusers/AbstractFocuserMoveTask.kt
index 115f8a354..e130bb6c6 100644
--- a/api/src/main/kotlin/nebulosa/api/focusers/AbstractFocuserMoveTask.kt
+++ b/api/src/main/kotlin/nebulosa/api/focusers/AbstractFocuserMoveTask.kt
@@ -6,6 +6,7 @@ import nebulosa.common.concurrency.cancel.CancellationToken
import nebulosa.common.concurrency.latch.CountUpDownLatch
import nebulosa.indi.device.focuser.FocuserEvent
import nebulosa.indi.device.focuser.FocuserMoveFailed
+import nebulosa.indi.device.focuser.FocuserMovingChanged
import nebulosa.indi.device.focuser.FocuserPositionChanged
import nebulosa.log.loggerFor
@@ -13,12 +14,13 @@ abstract class AbstractFocuserMoveTask : FocuserMoveTask, CancellationListener {
@JvmField protected val latch = CountUpDownLatch()
- @Volatile private var initialPosition = 0
+ @Volatile private var moving = false
override fun handleFocuserEvent(event: FocuserEvent) {
if (event.device === focuser) {
when (event) {
- is FocuserPositionChanged -> if (focuser.position != initialPosition && !focuser.moving) latch.reset()
+ is FocuserMovingChanged -> if (event.device.moving) moving = true else latch.reset()
+ is FocuserPositionChanged -> if (moving && !event.device.moving) latch.reset()
is FocuserMoveFailed -> latch.reset()
}
}
@@ -32,12 +34,12 @@ abstract class AbstractFocuserMoveTask : FocuserMoveTask, CancellationListener {
if (!cancellationToken.isCancelled && focuser.connected && !focuser.moving && canMove()) {
try {
cancellationToken.listen(this)
- initialPosition = focuser.position
LOG.info("Focuser move started. focuser={}", focuser)
latch.countUp()
move()
latch.await()
} finally {
+ moving = false
cancellationToken.unlisten(this)
LOG.info("Focuser move finished. focuser={}", focuser)
}
diff --git a/api/src/main/kotlin/nebulosa/api/focusers/BacklashCompensationFocuserMoveTask.kt b/api/src/main/kotlin/nebulosa/api/focusers/BacklashCompensationFocuserMoveTask.kt
index 49e4ebc94..1131b1bd7 100644
--- a/api/src/main/kotlin/nebulosa/api/focusers/BacklashCompensationFocuserMoveTask.kt
+++ b/api/src/main/kotlin/nebulosa/api/focusers/BacklashCompensationFocuserMoveTask.kt
@@ -41,43 +41,49 @@ data class BacklashCompensationFocuserMoveTask(
if (!cancellationToken.isCancelled && focuser.connected && !focuser.moving) {
val startPosition = focuser.position
- if (compensation.mode == BacklashCompensationMode.ABSOLUTE) {
- val adjustedTargetPosition = position + offset
-
- val finalizedTargetPosition = if (adjustedTargetPosition < 0) {
- offset = 0
- 0
- } else if (adjustedTargetPosition > focuser.maxPosition) {
- offset = 0
- focuser.maxPosition
- } else {
- val backlashCompensation = calculateAbsoluteBacklashCompensation(startPosition, adjustedTargetPosition)
- offset += backlashCompensation
- adjustedTargetPosition + backlashCompensation
- }
-
- moveFocuser(finalizedTargetPosition, cancellationToken)
- } else {
- val backlashCompensation = calculateOvershootBacklashCompensation(startPosition, position)
-
- if (backlashCompensation != 0) {
- val overshoot = position + backlashCompensation
-
- if (overshoot < 0) {
- LOG.info("overshooting position is below minimum 0, skipping overshoot")
- } else if (overshoot > focuser.maxPosition) {
- LOG.info("overshooting position is above maximum ${focuser.maxPosition}, skipping overshoot")
+ val newPosition = when (compensation.mode) {
+ BacklashCompensationMode.ABSOLUTE -> {
+ val adjustedTargetPosition = position + offset
+
+ if (adjustedTargetPosition < 0) {
+ offset = 0
+ 0
+ } else if (adjustedTargetPosition > focuser.maxPosition) {
+ offset = 0
+ focuser.maxPosition
} else {
- LOG.info("overshooting from $startPosition to overshoot position $overshoot using a compensation of $backlashCompensation")
-
- moveFocuser(overshoot, cancellationToken)
-
- LOG.info("moving back to position $position")
+ val backlashCompensation = calculateAbsoluteBacklashCompensation(startPosition, adjustedTargetPosition)
+ offset += backlashCompensation
+ adjustedTargetPosition + backlashCompensation
}
}
+ BacklashCompensationMode.OVERSHOOT -> {
+ val backlashCompensation = calculateOvershootBacklashCompensation(startPosition, position)
+
+ if (backlashCompensation != 0) {
+ val overshoot = position + backlashCompensation
+
+ if (overshoot < 0) {
+ LOG.warn("overshooting position is below minimum 0, skipping overshoot")
+ } else if (overshoot > focuser.maxPosition) {
+ LOG.warn("overshooting position is above maximum ${focuser.maxPosition}, skipping overshoot")
+ } else {
+ LOG.info("overshooting from $startPosition to overshoot position $overshoot using a compensation of $backlashCompensation")
+ moveFocuser(overshoot, cancellationToken)
+ LOG.info("moving back to position $position")
+ }
+ }
- moveFocuser(position, cancellationToken)
+ position
+ }
+ else -> {
+ position
+ }
}
+
+ LOG.info("moving to position {} using {} backlash compensation", newPosition, compensation.mode)
+
+ moveFocuser(newPosition, cancellationToken)
}
}
diff --git a/api/src/main/kotlin/nebulosa/api/focusers/BacklashCompensationMode.kt b/api/src/main/kotlin/nebulosa/api/focusers/BacklashCompensationMode.kt
index a81802872..d75206b3b 100644
--- a/api/src/main/kotlin/nebulosa/api/focusers/BacklashCompensationMode.kt
+++ b/api/src/main/kotlin/nebulosa/api/focusers/BacklashCompensationMode.kt
@@ -1,6 +1,7 @@
package nebulosa.api.focusers
enum class BacklashCompensationMode {
+ NONE,
ABSOLUTE,
OVERSHOOT,
}
diff --git a/api/src/test/kotlin/APITest.kt b/api/src/test/kotlin/APITest.kt
index e3683fadb..76197a043 100644
--- a/api/src/test/kotlin/APITest.kt
+++ b/api/src/test/kotlin/APITest.kt
@@ -8,6 +8,8 @@ import kotlinx.coroutines.delay
import nebulosa.api.autofocus.AutoFocusRequest
import nebulosa.api.beans.converters.time.DurationSerializer
import nebulosa.api.cameras.CameraStartCaptureRequest
+import nebulosa.api.connection.ConnectionType
+import nebulosa.api.stardetection.StarDetectionOptions
import nebulosa.common.json.PathSerializer
import nebulosa.test.NonGitHubOnlyCondition
import okhttp3.MediaType.Companion.toMediaType
@@ -69,8 +71,8 @@ class APITest : StringSpec() {
"Auto Focus Stop" { autoFocusStop() }
}
- private fun connect(host: String = "0.0.0.0", port: Int = 7624) {
- put("connection?host=$host&port=$port")
+ private fun connect(host: String = "0.0.0.0", port: Int = 7624, type: ConnectionType = ConnectionType.INDI) {
+ put("connection?host=$host&port=$port&type=$type")
}
private fun disconnect() {
@@ -167,12 +169,17 @@ class APITest : StringSpec() {
@JvmStatic private val EXPOSURE_TIME = Duration.ofSeconds(5)
@JvmStatic private val CAPTURES_PATH = Path.of("/home/tiagohm/Git/nebulosa/data/captures")
+ @JvmStatic private val STAR_DETECTION_OPTIONS = StarDetectionOptions(executablePath = Path.of("astap"))
+
@JvmStatic private val CAMERA_START_CAPTURE_REQUEST = CameraStartCaptureRequest(
exposureTime = EXPOSURE_TIME, width = 1280, height = 1024, frameFormat = "INDI_MONO",
savePath = CAPTURES_PATH, exposureAmount = 1
)
- @JvmStatic private val AUTO_FOCUS_REQUEST = AutoFocusRequest(capture = CAMERA_START_CAPTURE_REQUEST, stepSize = 11000)
+ @JvmStatic private val AUTO_FOCUS_REQUEST = AutoFocusRequest(
+ capture = CAMERA_START_CAPTURE_REQUEST, stepSize = 500,
+ starDetector = STAR_DETECTION_OPTIONS
+ )
@JvmStatic private val CLIENT = OkHttpClient.Builder()
.addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))
diff --git a/desktop/src/app/autofocus/autofocus.component.ts b/desktop/src/app/autofocus/autofocus.component.ts
index 71a861a67..ca7532a3a 100644
--- a/desktop/src/app/autofocus/autofocus.component.ts
+++ b/desktop/src/app/autofocus/autofocus.component.ts
@@ -3,10 +3,9 @@ import { ApiService } from '../../shared/services/api.service'
import { BrowserWindowService } from '../../shared/services/browser-window.service'
import { ElectronService } from '../../shared/services/electron.service'
import { PreferenceService } from '../../shared/services/preference.service'
-import { AutoFocusPreference, AutoFocusRequest } from '../../shared/types/autofocus.type'
+import { AutoFocusPreference, AutoFocusRequest, EMPTY_AUTO_FOCUS_PREFERENCE } from '../../shared/types/autofocus.type'
import { Camera, EMPTY_CAMERA, EMPTY_CAMERA_START_CAPTURE, updateCameraStartCaptureFromCamera } from '../../shared/types/camera.types'
import { EMPTY_FOCUSER, Focuser } from '../../shared/types/focuser.types'
-import { EMPTY_STAR_DETECTION_OPTIONS } from '../../shared/types/settings.types'
import { deviceComparator } from '../../shared/utils/comparators'
import { AppComponent } from '../app.component'
import { CameraComponent } from '../camera/camera.component'
@@ -27,18 +26,8 @@ export class AutoFocusComponent implements AfterViewInit, OnDestroy {
running = false
readonly request: AutoFocusRequest = {
+ ...structuredClone(EMPTY_AUTO_FOCUS_PREFERENCE),
capture: structuredClone(EMPTY_CAMERA_START_CAPTURE),
- fittingMode: 'HYPERBOLIC',
- rSquaredThreshold: 0.7,
- backlashCompensation: {
- mode: 'NONE',
- backlashIn: 0,
- backlashOut: 0
- },
- initialOffsetSteps: 4,
- stepSize: 100,
- totalNumberOfAttempts: 1,
- starDetector: structuredClone(EMPTY_STAR_DETECTION_OPTIONS),
}
constructor(
diff --git a/desktop/src/shared/types/autofocus.type.ts b/desktop/src/shared/types/autofocus.type.ts
index aa24a424b..5f728901a 100644
--- a/desktop/src/shared/types/autofocus.type.ts
+++ b/desktop/src/shared/types/autofocus.type.ts
@@ -26,7 +26,7 @@ export interface AutoFocusPreference extends Omit {
export const EMPTY_AUTO_FOCUS_PREFERENCE: AutoFocusPreference = {
fittingMode: 'HYPERBOLIC',
- rSquaredThreshold: 0.7,
+ rSquaredThreshold: 0.5,
initialOffsetSteps: 4,
stepSize: 100,
totalNumberOfAttempts: 1,
diff --git a/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/device/ASCOMDevice.kt b/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/device/ASCOMDevice.kt
index 6a58bb809..6a044aff5 100644
--- a/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/device/ASCOMDevice.kt
+++ b/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/device/ASCOMDevice.kt
@@ -157,9 +157,9 @@ abstract class ASCOMDevice : Device, Resettable {
refresh(stopwatch.elapsedSeconds)
}
- val delayTime = 2000L - elapsedTime
+ val delayTime = 1500L - elapsedTime
- if (delayTime > 1L) {
+ if (delayTime >= 10L) {
sleep(delayTime)
}
}
diff --git a/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/device/focusers/ASCOMFocuser.kt b/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/device/focusers/ASCOMFocuser.kt
index c07f7990e..14739c573 100644
--- a/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/device/focusers/ASCOMFocuser.kt
+++ b/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/device/focusers/ASCOMFocuser.kt
@@ -29,26 +29,28 @@ data class ASCOMFocuser(
@Volatile final override var hasThermometer = false
@Volatile final override var temperature = 0.0
+ @Volatile private var internalMoving = false
+
override val snoopedDevices = emptyList()
override fun moveFocusIn(steps: Int) {
- if (canAbsoluteMove) {
- service.move(device.number, position + steps).doRequest()
+ internalMoving = if (canAbsoluteMove) {
+ service.move(device.number, position + steps).doRequest { }
} else {
- service.move(device.number, steps).doRequest()
+ service.move(device.number, steps).doRequest { }
}
}
override fun moveFocusOut(steps: Int) {
- if (canAbsoluteMove) {
- service.move(device.number, position - steps).doRequest()
+ internalMoving = if (canAbsoluteMove) {
+ service.move(device.number, position - steps).doRequest { }
} else {
- service.move(device.number, -steps).doRequest()
+ service.move(device.number, -steps).doRequest { }
}
}
override fun moveFocusTo(steps: Int) {
- service.move(device.number, steps).doRequest()
+ internalMoving = service.move(device.number, steps).doRequest { }
}
override fun abortFocus() {
@@ -74,6 +76,7 @@ data class ASCOMFocuser(
super.reset()
moving = false
+ internalMoving = false
position = 0
canAbsoluteMove = false
canRelativeMove = false
@@ -124,9 +127,10 @@ data class ASCOMFocuser(
private fun processMoving() {
service.isMoving(device.number).doRequest {
- if (it.value != moving) {
- moving = it.value
+ val value = it.value || internalMoving
+ if (value != moving) {
+ moving = value
sender.fireOnEventReceived(FocuserMovingChanged(this))
}
}
@@ -136,8 +140,11 @@ data class ASCOMFocuser(
service.position(device.number).doRequest {
if (it.value != position) {
position = it.value
-
sender.fireOnEventReceived(FocuserPositionChanged(this))
+ } else if (internalMoving && moving) {
+ moving = false
+ internalMoving = false
+ sender.fireOnEventReceived(FocuserMovingChanged(this))
}
}
}
diff --git a/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/CurvePoint.kt b/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/CurvePoint.kt
index 9cb84ed17..26a23f980 100644
--- a/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/CurvePoint.kt
+++ b/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/CurvePoint.kt
@@ -19,7 +19,7 @@ class CurvePoint(x: Double, y: Double) : WeightedObservedPoint(1.0, x, y) {
return result
}
- override fun toString() = "CurvePoint(x=$x, y=$y, weight=$weight)"
+ override fun toString() = "CurvePoint(x=$x, y=$y)"
companion object {
diff --git a/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/TrendLineFitting.kt b/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/TrendLineFitting.kt
index e13c68376..205eb3bed 100644
--- a/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/TrendLineFitting.kt
+++ b/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/TrendLineFitting.kt
@@ -20,8 +20,11 @@ data object TrendLineFitting : CurveFitting {
override fun calculate(points: Collection): Curve {
val minimum = points.minBy { it.y }
- val left = TrendLine(points.filter { it.x < minimum.x && it.y > minimum.y + 0.1 })
- val right = TrendLine(points.filter { it.x > minimum.x && it.y > minimum.y + 0.1 })
+ val minX = minimum.x
+ val minY = minimum.y + 0.1
+
+ val left = TrendLine(points.filter { it.x < minX && it.y > minY })
+ val right = TrendLine(points.filter { it.x > minX && it.y > minY })
return Curve(left, right, minimum)
}
diff --git a/nebulosa-curve-fitting/src/test/kotlin/AutoFocusTest.kt b/nebulosa-curve-fitting/src/test/kotlin/AutoFocusTest.kt
new file mode 100644
index 000000000..030123a94
--- /dev/null
+++ b/nebulosa-curve-fitting/src/test/kotlin/AutoFocusTest.kt
@@ -0,0 +1,64 @@
+import io.kotest.core.spec.style.StringSpec
+import io.kotest.matchers.doubles.plusOrMinus
+import io.kotest.matchers.ints.shouldBeExactly
+import io.kotest.matchers.shouldBe
+import nebulosa.curve.fitting.CurvePoint
+import nebulosa.curve.fitting.CurvePoint.Companion.midPoint
+import nebulosa.curve.fitting.HyperbolicFitting
+import nebulosa.curve.fitting.QuadraticFitting
+import nebulosa.curve.fitting.TrendLineFitting
+import kotlin.math.roundToInt
+
+class AutoFocusTest : StringSpec() {
+
+ init {
+ // The best focus is 8000.
+
+ "near:hyperbolic" {
+ val points = focusPointsNearBestFocus()
+ val curve = HyperbolicFitting.calculate(points)
+ curve.minimum.x.roundToInt() shouldBeExactly 8031
+ curve.rSquared shouldBe (0.89 plusOrMinus 1e-2)
+ }
+ "near:parabolic" {
+ val points = focusPointsNearBestFocus()
+ val curve = QuadraticFitting.calculate(points)
+ curve.minimum.x.roundToInt() shouldBeExactly 8051
+ curve.rSquared shouldBe (0.74 plusOrMinus 1e-2)
+ }
+ "near:trendline" {
+ val points = focusPointsNearBestFocus()
+ val line = TrendLineFitting.calculate(points)
+ line.minimum.x.roundToInt() shouldBeExactly 8100
+ line.rSquared shouldBe (0.94 plusOrMinus 1e-2)
+ }
+ "near:hyperbolic + trendline" {
+ val points = focusPointsNearBestFocus()
+ val curve = HyperbolicFitting.calculate(points)
+ val line = TrendLineFitting.calculate(points)
+ (curve.minimum midPoint line.intersection).x.roundToInt() shouldBeExactly 7952
+ }
+ "near:parabolic + trendline" {
+ val points = focusPointsNearBestFocus()
+ val curve = QuadraticFitting.calculate(points)
+ val line = TrendLineFitting.calculate(points)
+ (curve.minimum midPoint line.intersection).x.roundToInt() shouldBeExactly 7962
+ }
+ }
+
+ companion object {
+
+ @JvmStatic
+ private fun focusPointsNearBestFocus() = listOf(
+ CurvePoint(10100.0, 13.892408928571431),
+ CurvePoint(9600.0, 12.879208888888888),
+ CurvePoint(9100.0, 10.640856213017754),
+ CurvePoint(8600.0, 6.891483673469387),
+ CurvePoint(8100.0, 2.9738176470588247),
+ CurvePoint(7600.0, 5.063299489795917),
+ CurvePoint(7100.0, 9.326303846153845),
+ CurvePoint(6600.0, 12.428210576923071),
+ CurvePoint(6100.0, 13.662644615384618),
+ )
+ }
+}
From 2e044107a52206671cd0731b0486448e9a4b091c Mon Sep 17 00:00:00 2001
From: tiagohm
Date: Fri, 31 May 2024 19:21:11 -0300
Subject: [PATCH 30/45] [api][desktop]: Support Auto Focus
Successfully tested using ASCOM Sky Simulator
---
.../nebulosa/api/autofocus/AutoFocusEvent.kt | 65 +++++++++++++-
.../nebulosa/api/autofocus/AutoFocusState.kt | 5 +-
.../nebulosa/api/autofocus/AutoFocusTask.kt | 89 ++++++++++++-------
desktop/app/main.ts | 2 +-
.../app/autofocus/autofocus.component.html | 26 +++++-
.../src/app/autofocus/autofocus.component.ts | 30 ++++++-
desktop/src/app/guider/guider.component.ts | 1 -
desktop/src/shared/pipes/enum.pipe.ts | 13 ++-
.../src/shared/services/electron.service.ts | 2 +
desktop/src/shared/types/api.types.ts | 2 +
desktop/src/shared/types/autofocus.type.ts | 55 +++++++++++-
.../alpaca/indi/device/cameras/ASCOMCamera.kt | 15 +++-
.../nebulosa/curve/fitting/CurvePoint.kt | 4 +
.../curve/fitting/HyperbolicFitting.kt | 8 +-
14 files changed, 267 insertions(+), 50 deletions(-)
diff --git a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusEvent.kt b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusEvent.kt
index 0b3441aeb..c88dce623 100644
--- a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusEvent.kt
+++ b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusEvent.kt
@@ -2,13 +2,74 @@ package nebulosa.api.autofocus
import nebulosa.api.cameras.CameraCaptureEvent
import nebulosa.api.messages.MessageEvent
-import nebulosa.curve.fitting.CurvePoint
+import nebulosa.curve.fitting.*
+import nebulosa.nova.almanac.evenlySpacedNumbers
data class AutoFocusEvent(
@JvmField val state: AutoFocusState = AutoFocusState.IDLE,
- @JvmField val focusPoint: CurvePoint = CurvePoint.ZERO,
+ @JvmField val focusPoint: CurvePoint? = null,
+ @JvmField val determinedFocusPoint: CurvePoint? = null,
+ @JvmField val starCount: Int = 0,
+ @JvmField val starHFD: Double = 0.0,
+ @JvmField val minX: Double = 0.0,
+ @JvmField val minY: Double = 0.0,
+ @JvmField val maxX: Double = 0.0,
+ @JvmField val maxY: Double = 0.0,
+ @JvmField val chart: Chart? = null,
@JvmField val capture: CameraCaptureEvent? = null,
) : MessageEvent {
+ data class Chart(
+ @JvmField val trendLine: Map? = null,
+ @JvmField val parabolic: Map? = null,
+ @JvmField val hyperbolic: Map? = null,
+ )
+
override val eventName = "AUTO_FOCUS.ELAPSED"
+
+ companion object {
+
+ @JvmStatic
+ fun makeChart(
+ points: List,
+ trendLine: TrendLineFitting.Curve?,
+ parabolic: QuadraticFitting.Curve?,
+ hyperbolic: HyperbolicFitting.Curve?
+ ) = with(evenlySpacedNumbers(points.first().x, points.last().x, 100)) {
+ Chart(trendLine?.mapped(this), parabolic?.mapped(this), hyperbolic?.mapped(this))
+ }
+
+ @JvmStatic
+ private fun TrendLineFitting.Curve.mapped(points: DoubleArray) = mapOf(
+ "left" to left.mapped(points),
+ "right" to right.mapped(points),
+ "intersection" to intersection,
+ "minimum" to minimum, "rSquared" to rSquared,
+ )
+
+ @JvmStatic
+ private fun TrendLine.mapped(points: DoubleArray) = mapOf(
+ "slope" to slope, "intercept" to intercept,
+ "rSquared" to rSquared,
+ "points" to makePoints(points)
+ )
+
+ @JvmStatic
+ private fun QuadraticFitting.Curve.mapped(points: DoubleArray) = mapOf(
+ "minimum" to minimum, "rSquared" to rSquared,
+ "points" to makePoints(points)
+ )
+
+ @JvmStatic
+ private fun HyperbolicFitting.Curve.mapped(points: DoubleArray) = mapOf(
+ "a" to a, "b" to b, "p" to p,
+ "minimum" to minimum, "rSquared" to rSquared,
+ "points" to makePoints(points)
+ )
+
+ @Suppress("NOTHING_TO_INLINE")
+ private inline fun Curve.makePoints(points: DoubleArray): List {
+ return points.map { CurvePoint(it, this(it)) }
+ }
+ }
}
diff --git a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusState.kt b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusState.kt
index 80f8f4e80..406775746 100644
--- a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusState.kt
+++ b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusState.kt
@@ -4,8 +4,11 @@ enum class AutoFocusState {
IDLE,
MOVING,
EXPOSURING,
- COMPUTING,
+ EXPOSURED,
+ ANALYSING,
+ ANALYSED,
FOCUS_POINT_ADDED,
+ CURVE_FITTED,
FAILED,
FINISHED,
}
diff --git a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt
index ef0348e28..c9a7da6db 100644
--- a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt
+++ b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt
@@ -47,11 +47,14 @@ data class AutoFocusTask(
private val focuserMoveTask = BacklashCompensationFocuserMoveTask(focuser, 0, request.backlashCompensation)
@Volatile private var trendLineCurve: TrendLineFitting.Curve? = null
- @Volatile private var parabolicCurve: Lazy? = null
- @Volatile private var hyperbolicCurve: Lazy? = null
+ @Volatile private var parabolicCurve: QuadraticFitting.Curve? = null
+ @Volatile private var hyperbolicCurve: HyperbolicFitting.Curve? = null
@Volatile private var measurementPos = 0
- @Volatile private var focusPoint = CurvePoint.ZERO
+ @Volatile private var focusPoint: CurvePoint? = null
+ @Volatile private var starCount = 0
+ @Volatile private var starHFD = 0.0
+ @Volatile private var determinedFocusPoint: CurvePoint? = null
init {
cameraCaptureTask.subscribe(this)
@@ -94,6 +97,8 @@ data class AutoFocusTask(
obtainFocusPoints(numberOfSteps, offsetSteps, reverse, cancellationToken)
+ if (cancellationToken.isCancelled) break
+
var leftCount = trendLineCurve?.left?.points?.size ?: 0
var rightCount = trendLineCurve?.right?.points?.size ?: 0
@@ -138,6 +143,8 @@ data class AutoFocusTask(
obtainFocusPoints(1, 1, false, cancellationToken)
}
+ if (cancellationToken.isCancelled) break
+
leftCount = trendLineCurve!!.left.points.size
rightCount = trendLineCurve!!.right.points.size
@@ -156,13 +163,15 @@ data class AutoFocusTask(
}
} while (!cancellationToken.isCancelled && (rightCount + focusPoints.count { it.x > trendLineCurve!!.minimum.x && it.y == 0.0 } < offsetSteps || leftCount + focusPoints.count { it.x < trendLineCurve!!.minimum.x && it.y == 0.0 } < offsetSteps))
- if (exited) break
+ if (exited || cancellationToken.isCancelled) break
val finalFocusPoint = determineFinalFocusPoint()
val goodAutoFocus = validateCalculatedFocusPosition(finalFocusPoint, initialHFD, cancellationToken)
if (!goodAutoFocus) {
- if (numberOfAttempts < request.totalNumberOfAttempts) {
+ if (cancellationToken.isCancelled) {
+ break
+ } else if (numberOfAttempts < request.totalNumberOfAttempts) {
moveFocuser(initialFocusPosition, cancellationToken, false)
LOG.warn("potentially bad auto-focus. Reattempting")
reset()
@@ -172,8 +181,8 @@ data class AutoFocusTask(
exited = true
}
} else {
+ determinedFocusPoint = finalFocusPoint
LOG.info("Auto Focus completed. x={}, y={}", finalFocusPoint.x, finalFocusPoint.y)
- moveFocuser(finalFocusPoint.x.roundToInt(), cancellationToken, false)
break
}
}
@@ -195,16 +204,12 @@ data class AutoFocusTask(
}
private fun determineFinalFocusPoint(): CurvePoint {
- val trendLine by lazy { TrendLineFitting.calculate(focusPoints) }
- val hyperbolic by lazy { HyperbolicFitting.calculate(focusPoints) }
- val parabolic by lazy { QuadraticFitting.calculate(focusPoints) }
-
return when (request.fittingMode) {
- AutoFocusFittingMode.TRENDLINES -> trendLine.intersection
- AutoFocusFittingMode.PARABOLIC -> parabolic.minimum
- AutoFocusFittingMode.TREND_PARABOLIC -> trendLine.intersection midPoint parabolic.minimum
- AutoFocusFittingMode.HYPERBOLIC -> hyperbolic.minimum
- AutoFocusFittingMode.TREND_HYPERBOLIC -> trendLine.intersection midPoint hyperbolic.minimum
+ AutoFocusFittingMode.TRENDLINES -> trendLineCurve!!.intersection
+ AutoFocusFittingMode.PARABOLIC -> parabolicCurve!!.minimum
+ AutoFocusFittingMode.TREND_PARABOLIC -> trendLineCurve!!.intersection midPoint parabolicCurve!!.minimum
+ AutoFocusFittingMode.HYPERBOLIC -> hyperbolicCurve!!.minimum
+ AutoFocusFittingMode.TREND_HYPERBOLIC -> trendLineCurve!!.intersection midPoint trendLineCurve!!.minimum
}
}
@@ -214,15 +219,18 @@ data class AutoFocusTask(
override fun accept(event: CameraCaptureEvent) {
if (event.state == CameraCaptureState.EXPOSURE_FINISHED) {
- sendEvent(AutoFocusState.COMPUTING, capture = event)
+ sendEvent(AutoFocusState.EXPOSURED, event)
+ sendEvent(AutoFocusState.ANALYSING)
val detectedStars = starDetection.detect(event.savePath!!)
- LOG.info("detected ${detectedStars.size} stars")
- val measure = detectedStars.measureDetectedStars()
- LOG.info("HFD measurement. mean={}", measure)
- measurements[measurementPos++] = measure
+ starCount = detectedStars.size
+ LOG.info("detected $starCount stars")
+ starHFD = detectedStars.measureDetectedStars()
+ LOG.info("HFD measurement. mean={}", starHFD)
+ measurements[measurementPos++] = starHFD
+ sendEvent(AutoFocusState.ANALYSED)
onNext(event)
} else {
- sendEvent(AutoFocusState.EXPOSURING, capture = event)
+ sendEvent(AutoFocusState.EXPOSURING, event)
}
}
@@ -256,19 +264,23 @@ data class AutoFocusTask(
val measurement = takeExposure(cancellationToken)
+ if (cancellationToken.isCancelled) break
+
LOG.info("HFD measured after exposures. mean={}", measurement)
if (remainingSteps-- > 1) {
focusPosition = moveFocuser(direction * -stepSize, cancellationToken, true)
}
+ if (cancellationToken.isCancelled) break
+
// If star measurement is 0, we didn't detect any stars or shapes,
// and want this point to be ignored by the fitting as much as possible.
if (measurement == 0.0) {
LOG.warn("No stars detected in step")
} else {
focusPoint = CurvePoint(currentFocusPosition.toDouble(), measurement)
- focusPoints.add(focusPoint)
+ focusPoints.add(focusPoint!!)
focusPoints.sortBy { it.x }
LOG.info("focus point added. remainingSteps={}, point={}", remainingSteps, focusPoint)
@@ -281,16 +293,18 @@ data class AutoFocusTask(
}
private fun computeCurveFittings() {
- with(focusPoints.toList()) {
+ with(focusPoints) {
trendLineCurve = TrendLineFitting.calculate(this)
if (size >= 3) {
if (request.fittingMode == AutoFocusFittingMode.PARABOLIC || request.fittingMode == AutoFocusFittingMode.TREND_PARABOLIC) {
- parabolicCurve = lazy { QuadraticFitting.calculate(this) }
+ parabolicCurve = QuadraticFitting.calculate(this)
} else if (request.fittingMode == AutoFocusFittingMode.HYPERBOLIC || request.fittingMode == AutoFocusFittingMode.TREND_HYPERBOLIC) {
- hyperbolicCurve = lazy { HyperbolicFitting.calculate(this) }
+ hyperbolicCurve = HyperbolicFitting.calculate(this)
}
}
+
+ sendEvent(AutoFocusState.CURVE_FITTED)
}
}
@@ -301,8 +315,8 @@ data class AutoFocusTask(
if (threshold > 0.0) {
fun isTrendLineBad() = trendLineCurve?.let { it.left.rSquared < threshold || it.right.rSquared < threshold } ?: true
- fun isParabolicBad() = parabolicCurve?.value?.let { it.rSquared < threshold } ?: true
- fun isHyperbolicBad() = hyperbolicCurve?.value?.let { it.rSquared < threshold } ?: true
+ fun isParabolicBad() = parabolicCurve?.let { it.rSquared < threshold } ?: true
+ fun isHyperbolicBad() = hyperbolicCurve?.let { it.rSquared < threshold } ?: true
val isBad = when (request.fittingMode) {
AutoFocusFittingMode.TRENDLINES -> isTrendLineBad()
@@ -318,14 +332,16 @@ data class AutoFocusTask(
}
}
- val min = focusPoints.minOf { it.x }
- val max = focusPoints.maxOf { it.x }
+ val min = focusPoints.first().x
+ val max = focusPoints.last().x
- if (focusPoint.x < min || focusPoint.y > max) {
+ if (focusPoint.x < min || focusPoint.x > max) {
LOG.error("determined focus point position is outside of the overall measurement points of the curve")
return false
}
+ if (cancellationToken.isCancelled) return false
+
moveFocuser(focusPoint.x.roundToInt(), cancellationToken, false)
val hfd = takeExposure(cancellationToken)
@@ -346,9 +362,16 @@ data class AutoFocusTask(
return focuser.position
}
- @Suppress("NOTHING_TO_INLINE")
- private inline fun sendEvent(state: AutoFocusState, capture: CameraCaptureEvent? = null) {
- onNext(AutoFocusEvent(state, focusPoint, capture))
+ private fun sendEvent(state: AutoFocusState, capture: CameraCaptureEvent? = null) {
+ val chart = when (state) {
+ AutoFocusState.FOCUS_POINT_ADDED -> AutoFocusEvent.makeChart(focusPoints, trendLineCurve, parabolicCurve, hyperbolicCurve)
+ else -> null
+ }
+
+ val (minX, minY) = if (focusPoints.isEmpty()) CurvePoint.ZERO else focusPoints[0]
+ val (maxX, maxY) = if (focusPoints.isEmpty()) CurvePoint.ZERO else focusPoints[focusPoints.lastIndex]
+
+ onNext(AutoFocusEvent(state, focusPoint, determinedFocusPoint, starCount, starHFD, minX, minY, maxX, maxY, chart, capture))
}
override fun reset() {
diff --git a/desktop/app/main.ts b/desktop/app/main.ts
index f631e27e1..2dada076b 100644
--- a/desktop/app/main.ts
+++ b/desktop/app/main.ts
@@ -565,6 +565,6 @@ function sendToAllWindows(channel: string, data: any, home: boolean = true) {
}
if (serve) {
- console.info(data)
+ console.info(JSON.stringify(data))
}
}
diff --git a/desktop/src/app/autofocus/autofocus.component.html b/desktop/src/app/autofocus/autofocus.component.html
index 41eb277f7..3934065c0 100644
--- a/desktop/src/app/autofocus/autofocus.component.html
+++ b/desktop/src/app/autofocus/autofocus.component.html
@@ -6,6 +6,29 @@
+
+
+
+
@@ -51,8 +74,7 @@
+ styleClass="p-inputtext-sm border-0" (ngModelChange)="savePreference()" />
diff --git a/desktop/src/app/autofocus/autofocus.component.ts b/desktop/src/app/autofocus/autofocus.component.ts
index ca7532a3a..21ecc08da 100644
--- a/desktop/src/app/autofocus/autofocus.component.ts
+++ b/desktop/src/app/autofocus/autofocus.component.ts
@@ -1,9 +1,10 @@
-import { AfterViewInit, Component, HostListener, NgZone, OnDestroy } from '@angular/core'
+import { AfterViewInit, Component, HostListener, NgZone, OnDestroy, ViewChild } from '@angular/core'
+import { CameraExposureComponent } from '../../shared/components/camera-exposure/camera-exposure.component'
import { ApiService } from '../../shared/services/api.service'
import { BrowserWindowService } from '../../shared/services/browser-window.service'
import { ElectronService } from '../../shared/services/electron.service'
import { PreferenceService } from '../../shared/services/preference.service'
-import { AutoFocusPreference, AutoFocusRequest, EMPTY_AUTO_FOCUS_PREFERENCE } from '../../shared/types/autofocus.type'
+import { AutoFocusPreference, AutoFocusRequest, AutoFocusState, EMPTY_AUTO_FOCUS_PREFERENCE } from '../../shared/types/autofocus.type'
import { Camera, EMPTY_CAMERA, EMPTY_CAMERA_START_CAPTURE, updateCameraStartCaptureFromCamera } from '../../shared/types/camera.types'
import { EMPTY_FOCUSER, Focuser } from '../../shared/types/focuser.types'
import { deviceComparator } from '../../shared/utils/comparators'
@@ -24,12 +25,18 @@ export class AutoFocusComponent implements AfterViewInit, OnDestroy {
focuser = structuredClone(EMPTY_FOCUSER)
running = false
+ status: AutoFocusState = 'IDLE'
+ starCount = 0
+ starHFD = 0
readonly request: AutoFocusRequest = {
...structuredClone(EMPTY_AUTO_FOCUS_PREFERENCE),
capture: structuredClone(EMPTY_CAMERA_START_CAPTURE),
}
+ @ViewChild('cameraExposure')
+ private readonly cameraExposure!: CameraExposureComponent
+
constructor(
app: AppComponent,
private api: ApiService,
@@ -98,6 +105,24 @@ export class AutoFocusComponent implements AfterViewInit, OnDestroy {
})
})
+ electron.on('AUTO_FOCUS.ELAPSED', event => {
+ ngZone.run(() => {
+ this.status = event.state
+ this.running = event.state !== 'FAILED' && event.state !== 'FINISHED'
+
+ if (event.capture) {
+ this.cameraExposure.handleCameraCaptureEvent(event.capture, true)
+ }
+
+ if (event.state === 'FOCUS_POINT_ADDED') {
+ const chart = event.chart!
+ } else if (event.state === 'ANALYSED') {
+ this.starCount = event.starCount
+ this.starHFD = event.starHFD
+ }
+ })
+ })
+
this.loadPreference()
}
@@ -115,6 +140,7 @@ export class AutoFocusComponent implements AfterViewInit, OnDestroy {
if (this.camera.id) {
const camera = await this.api.camera(this.camera.id)
Object.assign(this.camera, camera)
+ this.loadPreference()
}
}
diff --git a/desktop/src/app/guider/guider.component.ts b/desktop/src/app/guider/guider.component.ts
index 14f72e44f..8c0e79507 100644
--- a/desktop/src/app/guider/guider.component.ts
+++ b/desktop/src/app/guider/guider.component.ts
@@ -123,7 +123,6 @@ export class GuiderComponent implements AfterViewInit, OnDestroy {
return ''
},
label: (context) => {
- console.log(context)
const barType = context.dataset.type === 'bar'
const raType = context.datasetIndex === 0 || context.datasetIndex === 2
const scale = barType ? this.phdDurationScale : 1.0
diff --git a/desktop/src/shared/pipes/enum.pipe.ts b/desktop/src/shared/pipes/enum.pipe.ts
index c84525f23..e2d85cfee 100644
--- a/desktop/src/shared/pipes/enum.pipe.ts
+++ b/desktop/src/shared/pipes/enum.pipe.ts
@@ -1,12 +1,14 @@
import { Pipe, PipeTransform } from '@angular/core'
import { DARVState, TPPAState } from '../types/alignment.types'
import { Constellation, SatelliteGroupType, SkyObjectType } from '../types/atlas.types'
+import { AutoFocusState } from '../types/autofocus.type'
import { CameraCaptureState } from '../types/camera.types'
+import { FlatWizardState } from '../types/flat-wizard.types'
import { GuideState } from '../types/guider.types'
import { SCNRProtectionMethod } from '../types/image.types'
export type EnumPipeKey = SCNRProtectionMethod | Constellation | SkyObjectType | SatelliteGroupType |
- DARVState | TPPAState | GuideState | CameraCaptureState | 'ALL' | string
+ DARVState | TPPAState | GuideState | CameraCaptureState | FlatWizardState | AutoFocusState | 'ALL' | string
@Pipe({ name: 'enum' })
export class EnumPipe implements PipeTransform {
@@ -342,7 +344,14 @@ export class EnumPipe implements PipeTransform {
'CAPTURE_STARTED': undefined,
'EXPOSURE_STARTED': undefined,
'EXPOSURE_FINISHED': undefined,
- 'CAPTURE_FINISHED': undefined
+ 'CAPTURE_FINISHED': undefined,
+ // Auto Focus.
+ 'CAPTURED': 'Captured',
+ 'MOVING': 'Moving',
+ 'EXPOSURED': 'Exposured',
+ 'ANALYSING': 'Analysing',
+ 'ANALYSED': 'Analysed',
+ 'FOCUS_POINT_ADDED': 'Focus point added',
}
transform(value: EnumPipeKey) {
diff --git a/desktop/src/shared/services/electron.service.ts b/desktop/src/shared/services/electron.service.ts
index 8c7cbea52..6c343e2d9 100644
--- a/desktop/src/shared/services/electron.service.ts
+++ b/desktop/src/shared/services/electron.service.ts
@@ -22,6 +22,7 @@ import { Mount } from '../types/mount.types'
import { Rotator } from '../types/rotator.types'
import { SequencerEvent } from '../types/sequencer.types'
import { FilterWheel, WheelRenamed } from '../types/wheel.types'
+import { AutoFocusEvent } from '../types/autofocus.type'
type EventMappedType = {
'DEVICE.PROPERTY_CHANGED': INDIMessageEvent
@@ -74,6 +75,7 @@ type EventMappedType = {
'WINDOW.CLOSE': CloseWindow
'WHEEL.RENAMED': WheelRenamed
'ROI.SELECTED': ROISelected
+ 'AUTO_FOCUS.ELAPSED': AutoFocusEvent
}
@Injectable({ providedIn: 'root' })
diff --git a/desktop/src/shared/types/api.types.ts b/desktop/src/shared/types/api.types.ts
index 65733134a..56dd57faa 100644
--- a/desktop/src/shared/types/api.types.ts
+++ b/desktop/src/shared/types/api.types.ts
@@ -29,6 +29,8 @@ export const API_EVENT_TYPES = [
'GUIDER.CONNECTED', 'GUIDER.DISCONNECTED', 'GUIDER.UPDATED', 'GUIDER.STEPPED', 'GUIDER.MESSAGE_RECEIVED',
// Polar Alignment.
'DARV_ALIGNMENT.ELAPSED',
+ // Auto Focus.
+ 'AUTO_FOCUS.ELAPSED',
] as const
export type ApiEventType = (typeof API_EVENT_TYPES)[number]
diff --git a/desktop/src/shared/types/autofocus.type.ts b/desktop/src/shared/types/autofocus.type.ts
index 5f728901a..42111d56b 100644
--- a/desktop/src/shared/types/autofocus.type.ts
+++ b/desktop/src/shared/types/autofocus.type.ts
@@ -1,6 +1,9 @@
-import { CameraStartCapture } from './camera.types'
+import { Point } from 'electron'
+import { CameraCaptureEvent, CameraStartCapture } from './camera.types'
import { EMPTY_STAR_DETECTION_OPTIONS, StarDetectionOptions } from './settings.types'
+export type AutoFocusState = 'IDLE' | 'MOVING' | 'EXPOSURING' | 'EXPOSURED' | 'ANALYSING' | 'ANALYSED' | 'FOCUS_POINT_ADDED' | 'FAILED' | 'FINISHED'
+
export type AutoFocusFittingMode = 'TRENDLINES' | 'PARABOLIC' | 'TREND_PARABOLIC' | 'HYPERBOLIC' | 'TREND_HYPERBOLIC'
export type BacklashCompensationMode = 'NONE' | 'ABSOLUTE' | 'OVERSHOOT'
@@ -37,3 +40,53 @@ export const EMPTY_AUTO_FOCUS_PREFERENCE: AutoFocusPreference = {
},
starDetector: EMPTY_STAR_DETECTION_OPTIONS,
}
+
+export interface Curve {
+ minimum: Point
+ rSquared: number
+}
+
+export interface Plottable {
+ points: Point[]
+}
+
+export interface HyperbolicCurve extends Curve, Plottable {
+ a: number
+ b: number
+ p: number
+}
+
+export interface ParabolicCurve extends Curve, Plottable {
+}
+
+export interface Line extends Plottable {
+ slope: number
+ intercept: number
+ rSquared: number
+}
+
+export interface TrendLineCurve extends Curve {
+ left: Line
+ right: Line
+ intersection: Point
+}
+
+export interface Chart {
+ trendLine?: TrendLineCurve
+ parabolic?: ParabolicCurve
+ hyperbolic?: HyperbolicCurve
+}
+
+export interface AutoFocusEvent {
+ state: AutoFocusState
+ focusPoint?: Point
+ determinedFocusPoint?: Point
+ starCount: number
+ starHFD: number
+ minX: number
+ maxX: number
+ minY: number
+ maxY: number
+ chart?: Chart
+ capture?: CameraCaptureEvent
+}
diff --git a/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/device/cameras/ASCOMCamera.kt b/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/device/cameras/ASCOMCamera.kt
index 14c3fb6a6..84d26fa14 100644
--- a/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/device/cameras/ASCOMCamera.kt
+++ b/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/device/cameras/ASCOMCamera.kt
@@ -15,6 +15,8 @@ import nebulosa.image.format.HeaderCard
import nebulosa.indi.device.Device
import nebulosa.indi.device.camera.*
import nebulosa.indi.device.camera.Camera.Companion.NANO_TO_SECONDS
+import nebulosa.indi.device.filterwheel.FilterWheel
+import nebulosa.indi.device.focuser.Focuser
import nebulosa.indi.device.guide.GuideOutputPulsingChanged
import nebulosa.indi.device.mount.Mount
import nebulosa.indi.protocol.INDIProtocol
@@ -679,10 +681,21 @@ data class ASCOMCamera(
header.add(FitsKeyword.EQUINOX, 2000)
}
+ val focuser = snoopedDevices.firstOrNull { it is Focuser } as? Focuser
+
+ focuser?.also {
+ header.add(FitsKeyword.FOCUSPOS, it.position)
+ }
+
+ val wheel = snoopedDevices.firstOrNull { it is FilterWheel } as? FilterWheel
+
+ wheel?.also {
+ header.add(FitsKeyword.FILTER, it.names.getOrNull(it.position) ?: "Filter #${it.position}")
+ }
+
fitsKeywords.forEach(header::add)
val hdu = BasicImageHdu(width, height, numberOfChannels, header, data)
-
val image = Fits()
image.add(hdu)
diff --git a/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/CurvePoint.kt b/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/CurvePoint.kt
index 26a23f980..54cca627f 100644
--- a/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/CurvePoint.kt
+++ b/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/CurvePoint.kt
@@ -4,6 +4,10 @@ import org.apache.commons.math3.fitting.WeightedObservedPoint
class CurvePoint(x: Double, y: Double) : WeightedObservedPoint(1.0, x, y) {
+ operator fun component1() = x
+
+ operator fun component2() = y
+
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is CurvePoint) return false
diff --git a/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/HyperbolicFitting.kt b/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/HyperbolicFitting.kt
index d845d404e..d711430d1 100644
--- a/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/HyperbolicFitting.kt
+++ b/nebulosa-curve-fitting/src/main/kotlin/nebulosa/curve/fitting/HyperbolicFitting.kt
@@ -7,11 +7,11 @@ import kotlin.math.*
data object HyperbolicFitting : CurveFitting {
data class Curve(
- private val a: Double,
- private val b: Double,
- private val p: Double,
+ @JvmField val a: Double,
+ @JvmField val b: Double,
+ @JvmField val p: Double,
override val minimum: CurvePoint,
- private val points: Collection,
+ @JvmField val points: Collection,
) : FittedCurve {
override val rSquared by lazy { RSquared.calculate(points, this) }
From 64ac7ec334caf28650c7f8c474011b6b020094e2 Mon Sep 17 00:00:00 2001
From: tiagohm
Date: Fri, 31 May 2024 21:39:43 -0300
Subject: [PATCH 31/45] [desktop]: Minor changes
---
desktop/src/app/atlas/atlas.component.html | 30 ++++++-------
.../app/autofocus/autofocus.component.html | 6 +--
.../app/calculator/calculator.component.ts | 10 +++++
.../calculator/formula/formula.component.html | 7 ++-
.../calibration/calibration.component.html | 2 +-
desktop/src/app/camera/camera.component.html | 2 +-
.../filterwheel/filterwheel.component.html | 2 +-
.../flat-wizard/flat-wizard.component.html | 2 +-
.../src/app/focuser/focuser.component.html | 4 +-
desktop/src/app/guider/guider.component.html | 14 +++---
desktop/src/app/image/image.component.html | 44 +++++++++----------
.../property/indi-property.component.html | 11 ++---
desktop/src/app/mount/mount.component.html | 22 +++++-----
.../src/app/rotator/rotator.component.html | 4 +-
.../app/sequencer/sequencer.component.html | 2 +-
desktop/src/shared/types/calculator.types.ts | 1 +
16 files changed, 85 insertions(+), 78 deletions(-)
diff --git a/desktop/src/app/atlas/atlas.component.html b/desktop/src/app/atlas/atlas.component.html
index 6bbcf5f94..db73c7c19 100644
--- a/desktop/src/app/atlas/atlas.component.html
+++ b/desktop/src/app/atlas/atlas.component.html
@@ -59,7 +59,7 @@
-
@@ -140,7 +140,7 @@
-
+
@@ -182,7 +182,7 @@
-
+
@@ -228,74 +228,74 @@
-
+
-
+
-
+
-
+
-
+
-
+
-
-
+
-
+
-
+
-
+
-
+
diff --git a/desktop/src/app/autofocus/autofocus.component.html b/desktop/src/app/autofocus/autofocus.component.html
index 3934065c0..e4d1b67ee 100644
--- a/desktop/src/app/autofocus/autofocus.component.html
+++ b/desktop/src/app/autofocus/autofocus.component.html
@@ -12,19 +12,19 @@
-
+
-
+
-
+
diff --git a/desktop/src/app/calculator/calculator.component.ts b/desktop/src/app/calculator/calculator.component.ts
index d67c42912..aa166ade6 100644
--- a/desktop/src/app/calculator/calculator.component.ts
+++ b/desktop/src/app/calculator/calculator.component.ts
@@ -22,6 +22,7 @@ export class CalculatorComponent {
{
label: 'Aperture',
suffix: 'mm',
+ min: 1,
},
{
label: 'Focal Ratio',
@@ -49,10 +50,12 @@ export class CalculatorComponent {
{
label: 'Focal Length',
suffix: 'mm',
+ min: 1,
},
{
label: 'Aperture',
suffix: 'mm',
+ min: 1,
},
],
result: {
@@ -76,6 +79,7 @@ export class CalculatorComponent {
{
label: 'Aperture',
suffix: 'mm',
+ min: 1,
},
],
result: {
@@ -99,6 +103,7 @@ export class CalculatorComponent {
{
label: 'Aperture',
suffix: 'mm',
+ min: 1,
},
],
result: {
@@ -122,6 +127,7 @@ export class CalculatorComponent {
{
label: 'Aperture',
suffix: 'mm',
+ min: 1,
},
],
result: {
@@ -144,10 +150,12 @@ export class CalculatorComponent {
{
label: 'Larger Aperture',
suffix: 'mm',
+ min: 1,
},
{
label: 'Smaller Aperture',
suffix: 'mm',
+ min: 1,
},
],
result: {
@@ -171,10 +179,12 @@ export class CalculatorComponent {
{
label: 'Pixel Size',
suffix: 'µm',
+ min: 1,
},
{
label: 'Focal Length',
suffix: 'mm',
+ min: 1,
},
],
result: {
diff --git a/desktop/src/app/calculator/formula/formula.component.html b/desktop/src/app/calculator/formula/formula.component.html
index 6d4083c3f..8b47fdc2b 100644
--- a/desktop/src/app/calculator/formula/formula.component.html
+++ b/desktop/src/app/calculator/formula/formula.component.html
@@ -12,7 +12,7 @@
+ [min]="item.min ?? 0" [showButtons]="true" styleClass="border-0 p-inputtext-sm" locale="en" scrollableNumber />
@@ -26,9 +26,8 @@
{{ formula.result.prefix }}
-
+
{{ formula.result.suffix }}
diff --git a/desktop/src/app/calibration/calibration.component.html b/desktop/src/app/calibration/calibration.component.html
index acfded5e3..a9f783024 100644
--- a/desktop/src/app/calibration/calibration.component.html
+++ b/desktop/src/app/calibration/calibration.component.html
@@ -52,7 +52,7 @@
-
+
diff --git a/desktop/src/app/camera/camera.component.html b/desktop/src/app/camera/camera.component.html
index 2127b8875..c082be6b0 100644
--- a/desktop/src/app/camera/camera.component.html
+++ b/desktop/src/app/camera/camera.component.html
@@ -2,7 +2,7 @@
-
+
-
+
-
diff --git a/desktop/src/app/focuser/focuser.component.html b/desktop/src/app/focuser/focuser.component.html
index 385b82a5d..4283b74eb 100644
--- a/desktop/src/app/focuser/focuser.component.html
+++ b/desktop/src/app/focuser/focuser.component.html
@@ -2,7 +2,7 @@
-
+
-
diff --git a/desktop/src/app/guider/guider.component.html b/desktop/src/app/guider/guider.component.html
index 9202e47e7..2d8551e46 100644
--- a/desktop/src/app/guider/guider.component.html
+++ b/desktop/src/app/guider/guider.component.html
@@ -5,7 +5,7 @@
-
@@ -35,40 +35,40 @@
-
-
-
-
+
-
+
-
+
diff --git a/desktop/src/app/image/image.component.html b/desktop/src/app/image/image.component.html
index b9872f8ce..dbb15221c 100644
--- a/desktop/src/app/image/image.component.html
+++ b/desktop/src/app/image/image.component.html
@@ -94,46 +94,46 @@
-
+
-
-
-
-
+
-
+
-
@@ -191,38 +191,38 @@
-
+
-
+
-
+
-
+
-
-
+
@@ -348,55 +348,55 @@
-
+
-
-
-
-
-
-
-
@@ -581,7 +581,7 @@
-
+
diff --git a/desktop/src/app/indi/property/indi-property.component.html b/desktop/src/app/indi/property/indi-property.component.html
index 458bb5a0e..7b5c9c3f6 100644
--- a/desktop/src/app/indi/property/indi-property.component.html
+++ b/desktop/src/app/indi/property/indi-property.component.html
@@ -25,14 +25,13 @@
-
+
-
+
@@ -48,15 +47,13 @@
-
+
-
+
diff --git a/desktop/src/app/mount/mount.component.html b/desktop/src/app/mount/mount.component.html
index 769ee72a2..471ac72fc 100644
--- a/desktop/src/app/mount/mount.component.html
+++ b/desktop/src/app/mount/mount.component.html
@@ -2,7 +2,7 @@
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
diff --git a/desktop/src/app/rotator/rotator.component.html b/desktop/src/app/rotator/rotator.component.html
index 17e1b1e6f..b7cb53b0b 100644
--- a/desktop/src/app/rotator/rotator.component.html
+++ b/desktop/src/app/rotator/rotator.component.html
@@ -2,7 +2,7 @@
-
+
-
diff --git a/desktop/src/app/sequencer/sequencer.component.html b/desktop/src/app/sequencer/sequencer.component.html
index afbb1ab77..556df7628 100644
--- a/desktop/src/app/sequencer/sequencer.component.html
+++ b/desktop/src/app/sequencer/sequencer.component.html
@@ -29,7 +29,7 @@
[positionTop]="8">
-
diff --git a/desktop/src/shared/types/calculator.types.ts b/desktop/src/shared/types/calculator.types.ts
index b82d5da25..ca490510f 100644
--- a/desktop/src/shared/types/calculator.types.ts
+++ b/desktop/src/shared/types/calculator.types.ts
@@ -6,6 +6,7 @@ export interface CalculatorOperand {
value?: number
minFractionDigits?: number
maxFractionDigits?: number
+ min?: number
}
export interface CalculatorFormula {
From b8deb6eb2d0bd2553f2baa19f0fe514aaa450ae1 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sat, 1 Jun 2024 03:12:27 +0000
Subject: [PATCH 32/45] [api]: Bump com.github.oshi:oshi-core from 6.6.0 to
6.6.1
Bumps [com.github.oshi:oshi-core](https://github.com/oshi/oshi) from 6.6.0 to 6.6.1.
- [Release notes](https://github.com/oshi/oshi/releases)
- [Changelog](https://github.com/oshi/oshi/blob/master/CHANGELOG.md)
- [Commits](https://github.com/oshi/oshi/compare/oshi-parent-6.6.0...oshi-parent-6.6.1)
---
updated-dependencies:
- dependency-name: com.github.oshi:oshi-core
dependency-type: direct:production
update-type: version-update:semver-patch
...
Signed-off-by: dependabot[bot]
---
settings.gradle.kts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 44067acf1..a19b5eb89 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -31,7 +31,7 @@ dependencyResolutionManagement {
library("apache-codec", "commons-codec:commons-codec:1.17.0")
library("apache-collections", "org.apache.commons:commons-collections4:4.4")
library("apache-numbers-complex", "org.apache.commons:commons-numbers-complex:1.1")
- library("oshi", "com.github.oshi:oshi-core:6.6.0")
+ library("oshi", "com.github.oshi:oshi-core:6.6.1")
library("jna", "net.java.dev.jna:jna:5.14.0")
library("kotest-assertions-core", "io.kotest:kotest-assertions-core:5.9.0")
library("kotest-runner-junit5", "io.kotest:kotest-runner-junit5:5.9.0")
From 62e47de50f0f221f304b88bcdb0448d746f17dfd Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sat, 1 Jun 2024 03:12:03 +0000
Subject: [PATCH 33/45] [api]: Bump the netty group with 2 updates
Bumps the netty group with 2 updates: [io.netty:netty-transport](https://github.com/netty/netty) and [io.netty:netty-codec](https://github.com/netty/netty).
Updates `io.netty:netty-transport` from 4.1.109.Final to 4.1.110.Final
- [Commits](https://github.com/netty/netty/compare/netty-4.1.109.Final...netty-4.1.110.Final)
Updates `io.netty:netty-codec` from 4.1.109.Final to 4.1.110.Final
- [Commits](https://github.com/netty/netty/compare/netty-4.1.109.Final...netty-4.1.110.Final)
---
updated-dependencies:
- dependency-name: io.netty:netty-transport
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: netty
- dependency-name: io.netty:netty-codec
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: netty
...
Signed-off-by: dependabot[bot]
---
settings.gradle.kts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/settings.gradle.kts b/settings.gradle.kts
index a19b5eb89..daf2ac638 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -23,8 +23,8 @@ dependencyResolutionManagement {
library("rx", "io.reactivex.rxjava3:rxjava:3.1.8")
library("logback", "ch.qos.logback:logback-classic:1.5.6")
library("eventbus", "org.greenrobot:eventbus-java:3.3.1")
- library("netty-transport", "io.netty:netty-transport:4.1.109.Final")
- library("netty-codec", "io.netty:netty-codec:4.1.109.Final")
+ library("netty-transport", "io.netty:netty-transport:4.1.110.Final")
+ library("netty-codec", "io.netty:netty-codec:4.1.110.Final")
library("xml", "com.fasterxml:aalto-xml:1.3.2")
library("csv", "de.siegmar:fastcsv:3.1.0")
library("apache-lang3", "org.apache.commons:commons-lang3:3.14.0")
From 48dd2583a8bc9330c81bbca09c721b7b1ad67e76 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sat, 1 Jun 2024 03:12:15 +0000
Subject: [PATCH 34/45] [api]: Bump org.springframework.boot from 3.2.5 to
3.3.0
Bumps org.springframework.boot from 3.2.5 to 3.3.0.
---
updated-dependencies:
- dependency-name: org.springframework.boot
dependency-type: direct:production
update-type: version-update:semver-minor
...
Signed-off-by: dependabot[bot]
---
api/build.gradle.kts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/api/build.gradle.kts b/api/build.gradle.kts
index 8832af464..a3d6fd6ae 100644
--- a/api/build.gradle.kts
+++ b/api/build.gradle.kts
@@ -2,7 +2,7 @@ import org.springframework.boot.gradle.tasks.bundling.BootJar
plugins {
kotlin("jvm")
- id("org.springframework.boot") version "3.2.5"
+ id("org.springframework.boot") version "3.3.0"
id("io.spring.dependency-management") version "1.1.5"
kotlin("plugin.spring")
kotlin("kapt")
From 54315774e74aaab57b01755fb4a4051e80fbb38c Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sat, 1 Jun 2024 03:12:23 +0000
Subject: [PATCH 35/45] [api]: Bump org.springframework:spring-context-indexer
Bumps [org.springframework:spring-context-indexer](https://github.com/spring-projects/spring-framework) from 6.1.7 to 6.1.8.
- [Release notes](https://github.com/spring-projects/spring-framework/releases)
- [Commits](https://github.com/spring-projects/spring-framework/compare/v6.1.7...v6.1.8)
---
updated-dependencies:
- dependency-name: org.springframework:spring-context-indexer
dependency-type: direct:production
update-type: version-update:semver-patch
...
Signed-off-by: dependabot[bot]
---
api/build.gradle.kts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/api/build.gradle.kts b/api/build.gradle.kts
index a3d6fd6ae..b86cc2805 100644
--- a/api/build.gradle.kts
+++ b/api/build.gradle.kts
@@ -46,7 +46,7 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-undertow")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
- kapt("org.springframework:spring-context-indexer:6.1.7")
+ kapt("org.springframework:spring-context-indexer:6.1.8")
testImplementation(project(":nebulosa-astrobin-api"))
testImplementation(project(":nebulosa-skycatalog-stellarium"))
testImplementation(project(":nebulosa-test"))
From edbd10064412ea734992fa06050afaa73d33aaff Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sat, 1 Jun 2024 04:00:54 +0000
Subject: [PATCH 36/45] [desktop]: Bump @types/node in /desktop in the types
group
Bumps the types group in /desktop with 1 update: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node).
Updates `@types/node` from 20.12.12 to 20.13.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)
---
updated-dependencies:
- dependency-name: "@types/node"
dependency-type: direct:development
update-type: version-update:semver-minor
dependency-group: types
...
Signed-off-by: dependabot[bot]
---
desktop/package-lock.json | 8 ++++----
desktop/package.json | 2 +-
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/desktop/package-lock.json b/desktop/package-lock.json
index 23c11b95c..67d4444ad 100644
--- a/desktop/package-lock.json
+++ b/desktop/package-lock.json
@@ -43,7 +43,7 @@
"@angular/compiler-cli": "17.3.9",
"@angular/language-service": "17.3.9",
"@types/leaflet": "1.9.12",
- "@types/node": "20.12.12",
+ "@types/node": "20.13.0",
"@types/uuid": "9.0.8",
"electron": "30.0.6",
"electron-builder": "24.13.3",
@@ -4476,9 +4476,9 @@
"dev": true
},
"node_modules/@types/node": {
- "version": "20.12.12",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz",
- "integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==",
+ "version": "20.13.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.13.0.tgz",
+ "integrity": "sha512-FM6AOb3khNkNIXPnHFDYaHerSv8uN22C91z098AnGccVu+Pcdhi+pNUFDi0iLmPIsVE0JBD0KVS7mzUYt4nRzQ==",
"dev": true,
"dependencies": {
"undici-types": "~5.26.4"
diff --git a/desktop/package.json b/desktop/package.json
index e97e2dbd6..c9d103b92 100644
--- a/desktop/package.json
+++ b/desktop/package.json
@@ -65,7 +65,7 @@
"@angular/compiler-cli": "17.3.9",
"@angular/language-service": "17.3.9",
"@types/leaflet": "1.9.12",
- "@types/node": "20.12.12",
+ "@types/node": "20.13.0",
"@types/uuid": "9.0.8",
"electron": "30.0.6",
"electron-builder": "24.13.3",
From 6baa05d54f6bf23f0eb083e31de0c311ad625b4a Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sat, 1 Jun 2024 10:44:45 +0000
Subject: [PATCH 37/45] [desktop]: Bump electron from 30.0.6 to 30.0.9 in
/desktop
Bumps [electron](https://github.com/electron/electron) from 30.0.6 to 30.0.9.
- [Release notes](https://github.com/electron/electron/releases)
- [Changelog](https://github.com/electron/electron/blob/main/docs/breaking-changes.md)
- [Commits](https://github.com/electron/electron/compare/v30.0.6...v30.0.9)
---
updated-dependencies:
- dependency-name: electron
dependency-type: direct:development
update-type: version-update:semver-patch
...
Signed-off-by: dependabot[bot]
---
desktop/package-lock.json | 8 ++++----
desktop/package.json | 2 +-
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/desktop/package-lock.json b/desktop/package-lock.json
index 67d4444ad..9953ec849 100644
--- a/desktop/package-lock.json
+++ b/desktop/package-lock.json
@@ -45,7 +45,7 @@
"@types/leaflet": "1.9.12",
"@types/node": "20.13.0",
"@types/uuid": "9.0.8",
- "electron": "30.0.6",
+ "electron": "30.0.9",
"electron-builder": "24.13.3",
"node-polyfill-webpack-plugin": "3.0.0",
"npm-run-all": "4.1.5",
@@ -7738,9 +7738,9 @@
}
},
"node_modules/electron": {
- "version": "30.0.6",
- "resolved": "https://registry.npmjs.org/electron/-/electron-30.0.6.tgz",
- "integrity": "sha512-PkhEPFdpYcTzjAO3gMHZ+map7g2+xCrMDedo/L1i0ir2BRXvAB93IkTJX497U6Srb/09r2cFt+k20VPNVCdw3Q==",
+ "version": "30.0.9",
+ "resolved": "https://registry.npmjs.org/electron/-/electron-30.0.9.tgz",
+ "integrity": "sha512-ArxgdGHVu3o5uaP+Tqj8cJDvU03R6vrGrOqiMs7JXLnvQHMqXJIIxmFKQAIdJW8VoT3ac3hD21tA7cPO10RLow==",
"dev": true,
"hasInstallScript": true,
"dependencies": {
diff --git a/desktop/package.json b/desktop/package.json
index c9d103b92..22fd4d39e 100644
--- a/desktop/package.json
+++ b/desktop/package.json
@@ -67,7 +67,7 @@
"@types/leaflet": "1.9.12",
"@types/node": "20.13.0",
"@types/uuid": "9.0.8",
- "electron": "30.0.6",
+ "electron": "30.0.9",
"electron-builder": "24.13.3",
"node-polyfill-webpack-plugin": "3.0.0",
"npm-run-all": "4.1.5",
From 4be3e5790e876b58b3c1e410c49463a897e9b0f2 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sat, 1 Jun 2024 04:01:19 +0000
Subject: [PATCH 38/45] [desktop]: Bump primeng from 17.17.0 to 17.18.0 in
/desktop
Bumps [primeng](https://github.com/primefaces/primeng) from 17.17.0 to 17.18.0.
- [Release notes](https://github.com/primefaces/primeng/releases)
- [Changelog](https://github.com/primefaces/primeng/blob/master/CHANGELOG.md)
- [Commits](https://github.com/primefaces/primeng/compare/17.17.0...17.18.0)
---
updated-dependencies:
- dependency-name: primeng
dependency-type: direct:production
update-type: version-update:semver-minor
...
Signed-off-by: dependabot[bot]
---
desktop/package-lock.json | 14 +++++++-------
desktop/package.json | 2 +-
2 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/desktop/package-lock.json b/desktop/package-lock.json
index 9953ec849..ebc817120 100644
--- a/desktop/package-lock.json
+++ b/desktop/package-lock.json
@@ -30,7 +30,7 @@
"panzoom": "9.4.3",
"primeflex": "3.3.1",
"primeicons": "7.0.0",
- "primeng": "17.17.0",
+ "primeng": "17.18.0",
"rxjs": "7.8.1",
"tslib": "2.6.2",
"uuid": "9.0.1",
@@ -13228,16 +13228,16 @@
"integrity": "sha512-jK3Et9UzwzTsd6tzl2RmwrVY/b8raJ3QZLzoDACj+oTJ0oX7L9Hy+XnVwgo4QVKlKpnP/Ur13SXV/pVh4LzaDw=="
},
"node_modules/primeng": {
- "version": "17.17.0",
- "resolved": "https://registry.npmjs.org/primeng/-/primeng-17.17.0.tgz",
- "integrity": "sha512-+lIfG2nVve5GJQXGBDi2YeVabg6E9RmG67LDw9Ol8XvMWuHwJXQAGfO+AKPhPPzFSdb1j2v44uJemuNcJLXUiw==",
+ "version": "17.18.0",
+ "resolved": "https://registry.npmjs.org/primeng/-/primeng-17.18.0.tgz",
+ "integrity": "sha512-EcvU/0Ex9QoBR6g6db9fDTCTAmzokW70TV5Oroy2gdvXRr3eqlflnOBoArQsmxTaw1oxSsu68YVj3RvcKYWhTg==",
"dependencies": {
"tslib": "^2.3.0"
},
"peerDependencies": {
- "@angular/common": "^17.0.0",
- "@angular/core": "^17.0.0",
- "@angular/forms": "^17.0.0",
+ "@angular/common": "^17.0.0 || ^18.0.0",
+ "@angular/core": "^17.0.0 || ^18.0.0",
+ "@angular/forms": "^17.0.0 || ^18.0.0",
"rxjs": "^6.0.0 || ^7.8.1",
"zone.js": "~0.14.0"
}
diff --git a/desktop/package.json b/desktop/package.json
index 22fd4d39e..74b1501d0 100644
--- a/desktop/package.json
+++ b/desktop/package.json
@@ -52,7 +52,7 @@
"panzoom": "9.4.3",
"primeflex": "3.3.1",
"primeicons": "7.0.0",
- "primeng": "17.17.0",
+ "primeng": "17.18.0",
"rxjs": "7.8.1",
"tslib": "2.6.2",
"uuid": "9.0.1",
From 45db58de839f2b1128c6d159c2d01b65998f400d Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sat, 1 Jun 2024 10:47:31 +0000
Subject: [PATCH 39/45] [desktop]: Bump node-polyfill-webpack-plugin in
/desktop
Bumps [node-polyfill-webpack-plugin](https://github.com/Richienb/node-polyfill-webpack-plugin) from 3.0.0 to 4.0.0.
- [Release notes](https://github.com/Richienb/node-polyfill-webpack-plugin/releases)
- [Commits](https://github.com/Richienb/node-polyfill-webpack-plugin/compare/v3.0.0...v4.0.0)
---
updated-dependencies:
- dependency-name: node-polyfill-webpack-plugin
dependency-type: direct:development
update-type: version-update:semver-major
...
Signed-off-by: dependabot[bot]
---
desktop/package-lock.json | 24 ++++++++++++------------
desktop/package.json | 2 +-
2 files changed, 13 insertions(+), 13 deletions(-)
diff --git a/desktop/package-lock.json b/desktop/package-lock.json
index ebc817120..1ecffeb10 100644
--- a/desktop/package-lock.json
+++ b/desktop/package-lock.json
@@ -47,7 +47,7 @@
"@types/uuid": "9.0.8",
"electron": "30.0.9",
"electron-builder": "24.13.3",
- "node-polyfill-webpack-plugin": "3.0.0",
+ "node-polyfill-webpack-plugin": "4.0.0",
"npm-run-all": "4.1.5",
"ts-node": "10.9.2",
"typescript": "5.4.5",
@@ -7643,12 +7643,12 @@
}
},
"node_modules/domain-browser": {
- "version": "4.23.0",
- "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-4.23.0.tgz",
- "integrity": "sha512-ArzcM/II1wCCujdCNyQjXrAFwS4mrLh4C7DZWlaI8mdh7h3BfKdNd3bKXITfl2PT9FtfQqaGvhi1vPRQPimjGA==",
+ "version": "5.7.0",
+ "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-5.7.0.tgz",
+ "integrity": "sha512-edTFu0M/7wO1pXY6GDxVNVW086uqwWYIHP98txhcPyV995X21JIH2DtYp33sQJOupYoXKe9RwTw2Ya2vWaquTQ==",
"dev": true,
"engines": {
- "node": ">=10"
+ "node": ">=4"
},
"funding": {
"url": "https://bevry.me/fund"
@@ -11911,9 +11911,9 @@
}
},
"node_modules/node-polyfill-webpack-plugin": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/node-polyfill-webpack-plugin/-/node-polyfill-webpack-plugin-3.0.0.tgz",
- "integrity": "sha512-QpG496dDBiaelQZu9wDcVvpLbtk7h9Ctz693RaUMZBgl8DUoFToO90ZTLKq57gP7rwKqYtGbMBXkcEgLSag2jQ==",
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/node-polyfill-webpack-plugin/-/node-polyfill-webpack-plugin-4.0.0.tgz",
+ "integrity": "sha512-WLk77vLpbcpmTekRj6s6vYxk30XoyaY5MDZ4+9g8OaKoG3Ij+TjOqhpQjVUlfDZBPBgpNATDltaQkzuXSnnkwg==",
"dev": true,
"dependencies": {
"assert": "^2.1.0",
@@ -11922,21 +11922,21 @@
"console-browserify": "^1.2.0",
"constants-browserify": "^1.0.0",
"crypto-browserify": "^3.12.0",
- "domain-browser": "^4.22.0",
+ "domain-browser": "^5.7.0",
"events": "^3.3.0",
"https-browserify": "^1.0.0",
"os-browserify": "^0.3.0",
"path-browserify": "^1.0.1",
"process": "^0.11.10",
- "punycode": "^2.3.0",
+ "punycode": "^2.3.1",
"querystring-es3": "^0.2.1",
- "readable-stream": "^4.4.2",
+ "readable-stream": "^4.5.2",
"stream-browserify": "^3.0.0",
"stream-http": "^3.2.0",
"string_decoder": "^1.3.0",
"timers-browserify": "^2.0.12",
"tty-browserify": "^0.0.1",
- "type-fest": "^4.4.0",
+ "type-fest": "^4.18.2",
"url": "^0.11.3",
"util": "^0.12.5",
"vm-browserify": "^1.1.2"
diff --git a/desktop/package.json b/desktop/package.json
index 74b1501d0..0466564d7 100644
--- a/desktop/package.json
+++ b/desktop/package.json
@@ -69,7 +69,7 @@
"@types/uuid": "9.0.8",
"electron": "30.0.9",
"electron-builder": "24.13.3",
- "node-polyfill-webpack-plugin": "3.0.0",
+ "node-polyfill-webpack-plugin": "4.0.0",
"npm-run-all": "4.1.5",
"ts-node": "10.9.2",
"typescript": "5.4.5",
From de81b7aa31a081aa7ae00e27acdd3c3ecf2a78fb Mon Sep 17 00:00:00 2001
From: tiagohm
Date: Sat, 1 Jun 2024 15:55:10 -0300
Subject: [PATCH 40/45] [api][desktop]: Support Auto Focus
---
.../nebulosa/api/autofocus/AutoFocusEvent.kt | 67 +----
.../AutoFocusEventChartSerializer.kt | 115 +++++++++
.../nebulosa/api/autofocus/AutoFocusState.kt | 1 -
.../nebulosa/api/autofocus/AutoFocusTask.kt | 28 +-
.../api/autofocus/CurvePointSerializer.kt | 21 ++
api/src/test/kotlin/APITest.kt | 8 +-
desktop/README.md | 4 +
desktop/auto-focus.png | Bin 0 -> 33231 bytes
.../app/autofocus/autofocus.component.html | 134 +++++-----
.../src/app/autofocus/autofocus.component.ts | 240 +++++++++++++++++-
desktop/src/app/guider/guider.component.html | 23 +-
desktop/src/app/guider/guider.component.ts | 14 +-
desktop/src/shared/pipes/enum.pipe.ts | 2 +-
.../shared/services/browser-window.service.ts | 4 +-
desktop/src/shared/types/autofocus.type.ts | 15 +-
desktop/src/shared/utils/comparators.ts | 1 +
16 files changed, 514 insertions(+), 163 deletions(-)
create mode 100644 api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusEventChartSerializer.kt
create mode 100644 api/src/main/kotlin/nebulosa/api/autofocus/CurvePointSerializer.kt
create mode 100644 desktop/auto-focus.png
diff --git a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusEvent.kt b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusEvent.kt
index c88dce623..334f44d59 100644
--- a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusEvent.kt
+++ b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusEvent.kt
@@ -2,8 +2,10 @@ package nebulosa.api.autofocus
import nebulosa.api.cameras.CameraCaptureEvent
import nebulosa.api.messages.MessageEvent
-import nebulosa.curve.fitting.*
-import nebulosa.nova.almanac.evenlySpacedNumbers
+import nebulosa.curve.fitting.CurvePoint
+import nebulosa.curve.fitting.HyperbolicFitting
+import nebulosa.curve.fitting.QuadraticFitting
+import nebulosa.curve.fitting.TrendLineFitting
data class AutoFocusEvent(
@JvmField val state: AutoFocusState = AutoFocusState.IDLE,
@@ -11,65 +13,20 @@ data class AutoFocusEvent(
@JvmField val determinedFocusPoint: CurvePoint? = null,
@JvmField val starCount: Int = 0,
@JvmField val starHFD: Double = 0.0,
- @JvmField val minX: Double = 0.0,
- @JvmField val minY: Double = 0.0,
- @JvmField val maxX: Double = 0.0,
- @JvmField val maxY: Double = 0.0,
@JvmField val chart: Chart? = null,
@JvmField val capture: CameraCaptureEvent? = null,
) : MessageEvent {
data class Chart(
- @JvmField val trendLine: Map? = null,
- @JvmField val parabolic: Map? = null,
- @JvmField val hyperbolic: Map? = null,
+ @JvmField val predictedFocusPoint: CurvePoint? = null,
+ @JvmField val minX: Double = 0.0,
+ @JvmField val minY: Double = 0.0,
+ @JvmField val maxX: Double = 0.0,
+ @JvmField val maxY: Double = 0.0,
+ @JvmField val trendLine: TrendLineFitting.Curve? = null,
+ @JvmField val parabolic: QuadraticFitting.Curve? = null,
+ @JvmField val hyperbolic: HyperbolicFitting.Curve? = null,
)
override val eventName = "AUTO_FOCUS.ELAPSED"
-
- companion object {
-
- @JvmStatic
- fun makeChart(
- points: List,
- trendLine: TrendLineFitting.Curve?,
- parabolic: QuadraticFitting.Curve?,
- hyperbolic: HyperbolicFitting.Curve?
- ) = with(evenlySpacedNumbers(points.first().x, points.last().x, 100)) {
- Chart(trendLine?.mapped(this), parabolic?.mapped(this), hyperbolic?.mapped(this))
- }
-
- @JvmStatic
- private fun TrendLineFitting.Curve.mapped(points: DoubleArray) = mapOf(
- "left" to left.mapped(points),
- "right" to right.mapped(points),
- "intersection" to intersection,
- "minimum" to minimum, "rSquared" to rSquared,
- )
-
- @JvmStatic
- private fun TrendLine.mapped(points: DoubleArray) = mapOf(
- "slope" to slope, "intercept" to intercept,
- "rSquared" to rSquared,
- "points" to makePoints(points)
- )
-
- @JvmStatic
- private fun QuadraticFitting.Curve.mapped(points: DoubleArray) = mapOf(
- "minimum" to minimum, "rSquared" to rSquared,
- "points" to makePoints(points)
- )
-
- @JvmStatic
- private fun HyperbolicFitting.Curve.mapped(points: DoubleArray) = mapOf(
- "a" to a, "b" to b, "p" to p,
- "minimum" to minimum, "rSquared" to rSquared,
- "points" to makePoints(points)
- )
-
- @Suppress("NOTHING_TO_INLINE")
- private inline fun Curve.makePoints(points: DoubleArray): List {
- return points.map { CurvePoint(it, this(it)) }
- }
- }
}
diff --git a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusEventChartSerializer.kt b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusEventChartSerializer.kt
new file mode 100644
index 000000000..ff36ce31c
--- /dev/null
+++ b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusEventChartSerializer.kt
@@ -0,0 +1,115 @@
+package nebulosa.api.autofocus
+
+import com.fasterxml.jackson.core.JsonGenerator
+import com.fasterxml.jackson.databind.SerializerProvider
+import com.fasterxml.jackson.databind.ser.std.StdSerializer
+import nebulosa.curve.fitting.*
+import nebulosa.nova.almanac.evenlySpacedNumbers
+import org.springframework.stereotype.Component
+import kotlin.math.max
+import kotlin.math.min
+import kotlin.math.roundToInt
+
+@Component
+class AutoFocusEventChartSerializer : StdSerializer(AutoFocusEvent.Chart::class.java) {
+
+ override fun serialize(chart: AutoFocusEvent.Chart?, gen: JsonGenerator, provider: SerializerProvider) {
+ if (chart == null) {
+ gen.writeNull()
+ } else {
+ gen.writeStartObject()
+
+ gen.writePOJOField("predictedFocusPoint", chart.predictedFocusPoint)
+ gen.writeNumberField("minX", chart.minX)
+ gen.writeNumberField("minY", chart.minY)
+ gen.writeNumberField("maxX", chart.maxX)
+ gen.writeNumberField("maxY", chart.maxY)
+
+ if (chart.trendLine != null || chart.parabolic != null || chart.hyperbolic != null) {
+ val delta = chart.maxX - chart.minX
+ val stepSize = max(3, min((delta / 10.0).roundToInt().let { if (it % 2 == 0) it + 1 else it }, 101))
+ val points = if (delta <= 0.0) doubleArrayOf(chart.minX) else evenlySpacedNumbers(chart.minX, chart.maxX, stepSize)
+ chart.trendLine?.serialize(gen, points)
+ chart.parabolic?.serialize(gen, points)
+ chart.hyperbolic?.serialize(gen, points)
+ }
+
+ gen.writeEndObject()
+ }
+ }
+
+ companion object {
+
+ @Suppress("NOTHING_TO_INLINE")
+ private inline fun Double.isRSquaredValid() = isFinite() && this > 0.0
+
+ private inline fun T?.serializeAsFittedCurve(gen: JsonGenerator, fieldName: String, block: (T) -> Unit = {}) {
+ if (this != null && rSquared.isRSquaredValid()) {
+ gen.writeObjectFieldStart(fieldName)
+ gen.writeNumberField("rSquared", rSquared)
+ gen.writePOJOField("minimum", minimum)
+ block(this)
+ gen.writeEndObject()
+ }
+ }
+
+ @JvmStatic
+ private fun TrendLineFitting.Curve?.serialize(gen: JsonGenerator, points: DoubleArray) {
+ serializeAsFittedCurve(gen, "trendLine") {
+ it.left.serialize(gen, "left", points)
+ it.right.serialize(gen, "right", points)
+ gen.writePOJOField("intersection", it.intersection)
+ }
+ }
+
+ @JvmStatic
+ private fun TrendLine.serialize(gen: JsonGenerator, fieldName: String, points: DoubleArray) {
+ gen.writeObjectFieldStart(fieldName)
+ gen.writeNumberField("slope", slope)
+ gen.writeNumberField("intercept", intercept)
+ gen.writeNumberField("rSquared", rSquared)
+
+ if (rSquared.isRSquaredValid()) {
+ makePoints(gen, points)
+ }
+
+ gen.writeEndObject()
+ }
+
+ @JvmStatic
+ private fun QuadraticFitting.Curve?.serialize(gen: JsonGenerator, points: DoubleArray) {
+ serializeAsFittedCurve(gen, "parabolic") {
+ if (it.rSquared.isRSquaredValid()) {
+ it.makePoints(gen, points)
+ }
+ }
+ }
+
+ @JvmStatic
+ private fun HyperbolicFitting.Curve?.serialize(gen: JsonGenerator, points: DoubleArray) {
+ serializeAsFittedCurve(gen, "hyperbolic") {
+ gen.writeNumberField("a", it.a)
+ gen.writeNumberField("b", it.b)
+ gen.writeNumberField("p", it.p)
+
+ if (it.rSquared.isRSquaredValid()) {
+ it.makePoints(gen, points)
+ }
+ }
+ }
+
+ @JvmStatic
+ private fun Curve.makePoints(gen: JsonGenerator, points: DoubleArray) {
+ gen.writeArrayFieldStart("points")
+
+ for (x in points) {
+ gen.writeStartObject()
+ gen.writeNumberField("x", x)
+ gen.writeNumberField("y", this(x))
+ gen.writeEndObject()
+ }
+
+ gen.writeEndArray()
+ }
+ }
+}
diff --git a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusState.kt b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusState.kt
index 406775746..e9cc57521 100644
--- a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusState.kt
+++ b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusState.kt
@@ -7,7 +7,6 @@ enum class AutoFocusState {
EXPOSURED,
ANALYSING,
ANALYSED,
- FOCUS_POINT_ADDED,
CURVE_FITTED,
FAILED,
FINISHED,
diff --git a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt
index c9a7da6db..249e5c51d 100644
--- a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt
+++ b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt
@@ -166,9 +166,8 @@ data class AutoFocusTask(
if (exited || cancellationToken.isCancelled) break
val finalFocusPoint = determineFinalFocusPoint()
- val goodAutoFocus = validateCalculatedFocusPosition(finalFocusPoint, initialHFD, cancellationToken)
- if (!goodAutoFocus) {
+ if (finalFocusPoint == null || !validateCalculatedFocusPosition(finalFocusPoint, initialHFD, cancellationToken)) {
if (cancellationToken.isCancelled) {
break
} else if (numberOfAttempts < request.totalNumberOfAttempts) {
@@ -203,13 +202,13 @@ data class AutoFocusTask(
LOG.info("Auto Focus finished. camera={}, focuser={}", camera, focuser)
}
- private fun determineFinalFocusPoint(): CurvePoint {
+ private fun determineFinalFocusPoint(): CurvePoint? {
return when (request.fittingMode) {
AutoFocusFittingMode.TRENDLINES -> trendLineCurve!!.intersection
- AutoFocusFittingMode.PARABOLIC -> parabolicCurve!!.minimum
- AutoFocusFittingMode.TREND_PARABOLIC -> trendLineCurve!!.intersection midPoint parabolicCurve!!.minimum
- AutoFocusFittingMode.HYPERBOLIC -> hyperbolicCurve!!.minimum
- AutoFocusFittingMode.TREND_HYPERBOLIC -> trendLineCurve!!.intersection midPoint trendLineCurve!!.minimum
+ AutoFocusFittingMode.PARABOLIC -> parabolicCurve?.minimum
+ AutoFocusFittingMode.TREND_PARABOLIC -> parabolicCurve?.minimum?.midPoint(trendLineCurve!!.intersection)
+ AutoFocusFittingMode.HYPERBOLIC -> hyperbolicCurve?.minimum
+ AutoFocusFittingMode.TREND_HYPERBOLIC -> hyperbolicCurve?.minimum?.midPoint(trendLineCurve!!.intersection)
}
}
@@ -286,8 +285,6 @@ data class AutoFocusTask(
LOG.info("focus point added. remainingSteps={}, point={}", remainingSteps, focusPoint)
computeCurveFittings()
-
- sendEvent(AutoFocusState.FOCUS_POINT_ADDED)
}
}
}
@@ -364,14 +361,17 @@ data class AutoFocusTask(
private fun sendEvent(state: AutoFocusState, capture: CameraCaptureEvent? = null) {
val chart = when (state) {
- AutoFocusState.FOCUS_POINT_ADDED -> AutoFocusEvent.makeChart(focusPoints, trendLineCurve, parabolicCurve, hyperbolicCurve)
+ AutoFocusState.FINISHED,
+ AutoFocusState.CURVE_FITTED -> {
+ val predictedFocusPoint = determinedFocusPoint ?: determineFinalFocusPoint()
+ val (minX, minY) = if (focusPoints.isEmpty()) CurvePoint.ZERO else focusPoints[0]
+ val (maxX, maxY) = if (focusPoints.isEmpty()) CurvePoint.ZERO else focusPoints[focusPoints.lastIndex]
+ AutoFocusEvent.Chart(predictedFocusPoint, minX, minY, maxX, maxY, trendLineCurve, parabolicCurve, hyperbolicCurve)
+ }
else -> null
}
- val (minX, minY) = if (focusPoints.isEmpty()) CurvePoint.ZERO else focusPoints[0]
- val (maxX, maxY) = if (focusPoints.isEmpty()) CurvePoint.ZERO else focusPoints[focusPoints.lastIndex]
-
- onNext(AutoFocusEvent(state, focusPoint, determinedFocusPoint, starCount, starHFD, minX, minY, maxX, maxY, chart, capture))
+ onNext(AutoFocusEvent(state, focusPoint, determinedFocusPoint, starCount, starHFD, chart, capture))
}
override fun reset() {
diff --git a/api/src/main/kotlin/nebulosa/api/autofocus/CurvePointSerializer.kt b/api/src/main/kotlin/nebulosa/api/autofocus/CurvePointSerializer.kt
new file mode 100644
index 000000000..3d5db1549
--- /dev/null
+++ b/api/src/main/kotlin/nebulosa/api/autofocus/CurvePointSerializer.kt
@@ -0,0 +1,21 @@
+package nebulosa.api.autofocus
+
+import com.fasterxml.jackson.core.JsonGenerator
+import com.fasterxml.jackson.databind.SerializerProvider
+import com.fasterxml.jackson.databind.ser.std.StdSerializer
+import nebulosa.curve.fitting.CurvePoint
+import org.springframework.stereotype.Component
+
+@Component
+class CurvePointSerializer : StdSerializer(CurvePoint::class.java) {
+
+ override fun serialize(point: CurvePoint?, gen: JsonGenerator, provider: SerializerProvider) {
+ if (point == null) gen.writeNull()
+ else {
+ gen.writeStartObject()
+ gen.writeNumberField("x", point.x)
+ gen.writeNumberField("y", point.y)
+ gen.writeEndObject()
+ }
+ }
+}
diff --git a/api/src/test/kotlin/APITest.kt b/api/src/test/kotlin/APITest.kt
index 76197a043..a71b9c688 100644
--- a/api/src/test/kotlin/APITest.kt
+++ b/api/src/test/kotlin/APITest.kt
@@ -59,12 +59,12 @@ class APITest : StringSpec() {
// AUTO FOCUS.
"Auto Focus Start" {
- connect()
+ connect("192.168.31.153", 11111, ConnectionType.ALPACA)
delay(1000)
cameraConnect()
focuserConnect()
delay(1000)
- focuserMoveTo(position = 36000)
+ // focuserMoveTo(position = 8100)
delay(2000)
autoFocusStart()
}
@@ -162,9 +162,9 @@ class APITest : StringSpec() {
companion object {
private const val BASE_URL = "http://localhost:7000"
- private const val CAMERA_NAME = "CCD Simulator"
+ private const val CAMERA_NAME = "Sky Simulator"
private const val MOUNT_NAME = "Telescope Simulator"
- private const val FOCUSER_NAME = "Focuser Simulator"
+ private const val FOCUSER_NAME = "ZWO Focuser (1)"
@JvmStatic private val EXPOSURE_TIME = Duration.ofSeconds(5)
@JvmStatic private val CAPTURES_PATH = Path.of("/home/tiagohm/Git/nebulosa/data/captures")
diff --git a/desktop/README.md b/desktop/README.md
index b6880a99a..2f0bae907 100644
--- a/desktop/README.md
+++ b/desktop/README.md
@@ -50,6 +50,10 @@ The complete integrated solution for all of your astronomical imaging needs.
![](flat-wizard.png)
+## Auto Focus
+
+![](auto-focus.png)
+
## Sequencer
![](sequencer.png)
diff --git a/desktop/auto-focus.png b/desktop/auto-focus.png
new file mode 100644
index 0000000000000000000000000000000000000000..cea2e19e30528ab03503de78e02e737b41cad61a
GIT binary patch
literal 33231
zcmb@u1yCJPv?WRsf(8f@2p&R^;O_43?k>UICAho0y99R$!8N$MdvJ$2yf=S-P1Ub?
zGxaA`gxp)VZ+D+Qd+)W@UOT}uQo;yuSa1*!5D20o0&)-#Z;HWB9t;$CC$xIOAN+!}
z;}cbY0e{?K41&OGOnX5UdwDA(dna96LkMF_D+@zPJAGS2LrXgoEBn)T9bDi;#IGOX
zw>8wYH?^|-qF`!a2w}Ib_l1V`%MS<3FLX3?3}0v&*cceu=xK_$0;C}zzCefy@G3Z`
z9W6U)C|swTQWTIeICW_1e0uu1@{Ly(6T^iY9|_NO?9P4kE^Vyoru}r+j%(NMQbSXd5r)9;
zBb!J^J^UBHk9-OOK5-xUVA6?r#lZWcF<*$lo5rG1D10#4HQz*F3H<)5qU3@1_9$TG
z_+SX^6Cl689xwi1A2zr^N!?2C%}7=VPYC7xf>k=IhWQf+WsO)nsvsXHC(7(ckqWlntBx;;ji5WgIV9O?cK
zV|;wv@Wrt$q;R7ho=PBIo-E#cr4FQeK?Z)*;zdWiR(6N2fpCXC3IdD3~LV4aUPo3KACLzi2HU&G3se*zJIQ}+8tVrMDZj@`4{V^!nlh}Hp_&zq5bB~8~4j`;WD+lkg%{1TAp`?
zunJE9o*zz8QBb-95U^{14lXXH;(I+tQ>xeh8)05m81URmQSivKHompS538`;c*s|s
zyBjs=CdE&n=1|m4=$5p*&ALM0x$CJqZ(=Q*zP;Ekls^le?|xZn37V~`q`R8*|K=ZI
zyd7`Lq+=PJj_VDlm;g66i$QOdMEjeZONaYiGUF32`mR>q3(^UN1`XoPF9-L37U&*E
zqg&}s7_4`)feh(5-LJ7{r;^NTcu%(oB`XqNLq?Hp0F@55IR=}(-J8R
z!Sgl77u$*2IWkjPFSV7G)a7b*&H5=D8+z?8&ktB+vft~}M#3=ZRqD+#v6;-*Vx_rs
zpC9kQ=1Pv_D*5-$ZS0%Kv@jmw=eg0KMudl^q^O?g{$vSU4j*Ck%&$#86bD-@8`X;5
zA$g$~vRCfV`0}5GJvBN9DOFX_9gI5*BmX`jiz{gj+)G;;KPuz)y!4jJXO68YNE-*K
z4OCOl*o4Tl+#neI*gdg_wdtD_5&S0&uhO8{Q_=qsuS%M@*jeB`(a?sT0!~Q=S6C|7
zPr^QBI*&`H2#xZX6qG9&9v+D0W(WO|6h?_O7Fq0~4+scm`5f<{5e=3aZJZ?JRa8_q
zdEvxbM2$yMdQuq8RH_X`6+ckXec04
zihp+NQy4gUc4nI_ls3M<9H5wsM-e~a
zLoJ)ymX?;`iH8M_#|z^Xn(P}J8++5Gvlg>OcyYCP2iUp7pPA<{E6jI&{noFd<@~fQDz}4QVy4^8%ha7)SA_2d=f=T720;z49T!B
z>!aE7LFW?5bb{=UOG`^>b!M}4x?PY76skU%nfTl5kRXt*!>Yq{8hNpHU*P28F~Uow+1#BE
zuWb5vF_SAjtm(YxMxgm7J9717b4zjm1VfjWM~Ec%dpoKc0$dNb^~FEwmz0$?TL$kq
zJOVek2a$GtHqGyEDce~b&7$Nsjtl`Dw-Y{|y)GTS%*d9}hTi9V>Ddxg&7qKF##M%bUyf9$Xfm)YJMFXBZPo}nR8{|pUh^SQW3{3qxP#O-0?y!>0A9%Z8n5j}@gdim))!oxWYc@kMQ=utt
zZB0W&L{z6KkZx_}ox<)!1zsI3)P+i=v(eh^NL-&RiyIm3ygL;ho1Rvbf4}{SFDP(y
zqbGE}$&O@ZW~TM&dhs>Z>5WIH9INcRN)$^QZMWerc1Fl+mO4AV^TiWml9KwDn(XkM
zxhIxM{60EWnNAXmiHWJSxiXG%Jx~~rWt1O(oo*h8B}r`2C|9EQxW6P@jC?IyMLY%u
z-@bqU4q-lD{k~2uT#RUz%`C?L=08d$9i-;}?K1k`tM73xTge7&2q?h-{uj$
zR_eh2E0y}cF7R?IoWVt=6&a&p8H_>ooa(wJ1~Q91#ddItKSuGxzUa(LQ3du+JKCHi
zr@f>o&aP4;v2!XbD4@XlMlF1aw(ccX8I2?ouk9r*!hn^+|CUYb!
z3x{VginM2y1Z79R$u&kOIo4Z3WqqkDq-xhbcn2%TR$wuvRHx||D5hCD)~vJ=7(Uf;
z5^W~OCgdroT;4KWSR+pkb(1MpimpjRLsL?3TQp7fl?q3{FC)HNGqi{%J?&+I=&I7~
zn@F(h(0uWy%HhWfHQz}|C@7&?i^)(W%I1GP=ewt$i~K&m#GG)+S|OZp*9pCyD*x~y
z?DLY>kMI-upk@i5xS|HNx;?qdYjOe2Z_6H63Nzz6GgMq61`^1`l$@B4*COuftIq6O
z{#8G^U8UT@44$haRNiWE_ZJd6AzO@Rm@f8iSis42R?T8mlQqIbGOy|->{qs)^P(N7
zUvHi*s9%OY#u)y5pARoFVskPb2zzSZGhb^j+Z8#K+|mlMdaAuWfXJxyo^d6l$uWV0D;FR4sTD$`En{>cvflR29ft`~iG!<#
zd&Ohn*G65YsPy1gdL2E)U~9W4Yj7s_yp+VAVi}eijk~LMGva~F$+NK+XE3#K85EDU
zHhy41eONL*%$Ts+6i+yeCNXabml)f?LW8f!-r7(sDn-y@bs~b*N#1yZuJdP~dlgaw
z%q%XCgo|R_{qcVZt(eh%X%t;8i73^CtRB@xNA;f^FP%&AwPy0E%Q?aZLLrY9XF=?5&G~v5Iaeuv0EB)d_1Wuw$=TND=iN5
zboqkZm#cbrB6O6Fy>T=iWTmAYNmdy}qKz-seP-h6>_m8K;d~bHphC~={iyM2a}|$w
zjvGUT{zh5N-bCr-x%`;&&EA88Q%Zx?V7oSA&3Ssbd-DV3hY#CkC0wyJ+DQ3F+@e8)
z2ro`gV(+5txV~$X#KyqjjSBif3g(>rMTrz2$6=4+kL()}rzZ=mstdgE>dQIVh7!&V
zQkc^*OYs<*k^E(`h*Bx!Ef@Tgzz-s6VX{OTJFRN-9$y!-Coi`E8HzSLt=Yg=nOgtr
z3zzsM4#pB1qZZLsM079#uDiOKHi?$#`o{ie;t_2A+26Q(v^l6&h-%&Iow1j4j>J^w
zY?J7m{x|v=_IQ6I!nP<-^yN+(Bg<+vG}IUi7NZbr54U;YPmT8W58vpRn2Q`zjlB7*
zd=MKO4ec#{*n4|RZMw@^)%AT~V!yJUC87}}W7~?>%Ps`%ukh^D1I_@v%hZ7_)
z-$pdLG?79UVYnD;=W;gQ)XHK&PQW}QuOXo2_(oMXTR=N7K+>EB0>y_vxW-bnW0%7u
z39YBS31Xc?>
zL>7b0+T3tN_nd-UMgBV-FG7{ipE3UpGRvFHK8)U=A*4K#8owYKpUe^AEW0F}{Bi$f
zLz*u5Wm^r4n){yM^@n_QzGy^VC5xDZF!JvoS#Y?Hg_-J|*heHHQ_*SNgCrV9iq$b~
z=!#h74=gtZqwefdR@6B|Qx%Pjt%t;xt2K!t1g{>fhe%>NM>4Jt>BVB&0zRp
zz)Sw1lYV+GJ|?o|D=qX^N6&oJ82`9*8->?Z4*h}oiJvzkgqRX%TzToK!C1_lt&-!K
z7UCG+LNXE=*GfEBd10@3>Uwa}%)mIM-d1ll;Lqdzw58M$>zLVUVuX2ZeT~6^4+#WK
z1Ksq_oy%_~Ppw}F9v&XVqqzRj#p@o&>Kq(-M2f=2HFhwnZRzR1nct%A*e8!?#929_
zv=T*0lavv$loTzsCWKRdVNZ`sZL{A+IzJv#XGK#M=zkVD52>uNcF*B-6UB3SKTO>^4!*_~Xry>bmPXc12-Yui;n)OK@29qQP
zs}I!f*9_yk`c2N)a1c;JVoU4{J#GB*IGV?`chHiqanUvR@^W%MU#)6<$Jw{3z1feL8FG_NxVp_ck@l8
zpIwtbHV_0&7UQ1Pg>C#@e`_8X-tF}ZL58P);tCQ(+;km7WwT0fVkP$BGxZ1G-%tN&
zB#f<+%Xed~8NwzPexg!}zv%zqzmPgUOGX>4Lh_w=Yv<&Y3P3U5>J5^1x1oomdisMD
zD&F$A^us!Cb@^!Sfs^+)_qxeySohW(-66W?dZfQULb2&=GQ%?=o8z7~q2Zv&=uRvw
z`Mqj+OJfry>-V~eW=b|<|KYy~|bkjG{(Kz0|
zHyn3NX>b~MOm3?>a6uD$e-GzzUJZW>!glBVaC@NzSI58aGb5{FoiUm!^J+AP&ny~h
z%v72Ud*3}%Ua+ybUvSOT#fiNb(EE%WSsLeJ&=LoqC51`9lVvY{w7P$|5zx*aDSDzok(=+8lT>3_spq0@@t%D||M$Bf(b|NJ
zaPve_@DxL~W=j^=A)YKfE)4sHI?QnQX5YRIISYb>O-ORs$5
z9Fx7?=&)5?rQ6MC-(=JMAbK}-3^tcU$5U1HdHTbq{CGGSq>WQ={nniZ@2?KD@R?5m
zhl#NeDt9NuROz9w5R4g`Dlae34=M98TI?*
z$3sNeXGigyjA)eP*}LCMM?3HO2SllBsMrhbX@06{^0*4=)g|^k&`N7*&>?ejau&EV
zD6u!{ES37&T$%es7ZXrZXZp3bbB&GSN}i6>y^k$g1R{*4{#*=lW_UOGeu80fHV)hm
ziAAY>bC>lGTKKmcxUKP%m^ugJpM{%PuiFLt(hTRFUL0SJ5VQh)J?K-KoNag9X^clt
zp}#OwhYD&-Z9OqF=~`OKkMH0m`G;fTr7v5X?ecY>i92)ez&JinAk8KQShF
zWhHn3RRcyfCE(^)LPJAdwf=mI4n|>3yf*@;7Pjajs@YwS3C?7|@>7nNn9P?$CSu=e
zn8^swTWJSE-8&|D)~J2#FD2g`@532Z^EFStwDgeeTyon1>pZkv|8yj0|~?TVIa
zg+eFOSXq{g&*{h%X^n6F<$nLnV5#RJ;v8{W&2sutV**_Oj9s*AeBViYB7QXPS97
zU3B=t<%BjzO3Ub9={Nq8EO_yGB^Ms6YBhOk+2aUZEj|q-%EpW$n^Kosp(@8
zioZW3FBA$5eZxxwOV#f<{3QVTiKjCed)Ym_nJ&kPVzD5>GrGWhdFajxiq_Q#nM>1(
zyGOOf7uJ+LfjOi8XVDifO-!NELBu<%2x1ct%X}J6=gyg?t8%-dX_Wvw=lk;H%>(e_2mZS6LCW=#28tm4`v?HS8Q+y5iFIvn+_fSAMMdyGU
zO?r!8X8iEez17I^tKBq`0LF@ncAC_d3;c@nOvTcXm>DXalQ1%K8TlyXQ_=<3mKFCK
zo|Rxt_m**}js#=YBf6ia??PZa@035RWXiCOWp=$K7o>|OvHiDaDU}#W;{En3M)_C0
zTS%;O%~1k>T&SvnB2G@zIAm#abEa@&$?nx{d~_=Z{n1p*t@jni@jqB5Yj3Rc?T#+e
zaYCuTRL+Id)|j(gnsHPeZOJS~J8QI$yYdGwPT7R*p^XK=vNxUR_|Do%PgXiK1*#FT
zxgllM+(1CIs@<%44T%{`{+*mO?zI!Ak-?(-F*h`=>mk(Xj82rHh5ca`jXTLj`bJujT{QdPPf@q7@!i1P)f9Fi`n{07M+WYOF
z5Dex9zQM`Js-~=l&6qETk?{{r)?`rm*>7K7u!G~v-={v`Km$xfiV!F9>`Y;{^E4=L
z_2Ex-Hft!c$DhS%OFS|Q+U6&q`R3oNl7+AbYLu_zlFk~k#)3D#4l<2AlUqt=#r+Jy?
zHnzg~+cs(~Q%Y;aod6k!vlx3%46f(tjX~c8Tgu#GE5vc1dT#
zxqtSRz;YzpMD=KE^KtH9SDWM)#LgDr*J#xcTHZN9l5mgh$r=wBeB?~aRcZL^jXXrM
zSpcWMj13)p;rNaa1B#j=y{WBWZjrHPe&8JfRvCpkt<21GEob=h(f*Y)iJFr`rM-7E
zif4FO9tGQN{hQADT-wEU#0>yz)Ew=J&IK5XXD93A(YFmTbUcR~@Bpf@rN0=2EM84G
z)~&4jj(}6P$5{d&tSpxVN-tGeR&BwpOuaNZX6nSA1Cy+s&f{?5PejRi@+_jnh>7BF&K?qy26xEYD(izjz*RWXC5mTP*6g)|ZbqQ%SD
z$XeysygDiqUYH1SV@K_sbCr|6{(uC~nXf63)c3DV#8g@Igq@tl>+EZQ2?J0%rQk
ziK}n+z8`oi19Li5dz#nY@&xNEV@T9oeCG@93o{L?V(Cvar~5DL=WKL$PthMvOk~;S_-!_s
z75LrEFG7aUP;t|hJ9AcZz}t~FYcg2LN~DA}8B?fWn-lv9jv
z5^(OG7$Ek8lZ$K>osJId%#He{u`eKcg}NvErxDdjuu=5HDpR%C-28`jH)-f-zi~G}
zIOrmmxHAH6wOg>4{&=#`#%#B1y;FXUsXn_eC_>`Jonw;A11nJ}d}z6FVr{ge$&UV_
zw&qu8GryYLRn#IRH^`C3XXrAeVWtlgq!M5GX#uY_3&1C+G~Z3SnY7`
zOUt`hSbqxtMa+7!5R-u}&VgR}r(E!2&8D@I!gCc`PT8^GW57!(8uM@g(KnS)JNLJ>fi>eHFixcA>%uw12-SfG~OxH>jscq<>I1k{5
z_Y4oB>B|$oQW&TJvEB}Pp9)|0U-xYzD)Xv7LcXBFm$dS=yP6{WP(+sQV8p?OCihIk
z=-lV#r)^-`9GDGEXpv^$h5Du~`KLnk4s}usHj>&Y05N;fK
zgA}j3cQgkf(8=h<%^dx;B027}RK&K#EM;A~JFD$?@bX-EPU_6-UIVov&AbJIC{B4?
zt)V^KMf-G(me57)vZabwKRS@uNt~d?Ld1c%=egKS*paz2POEu>mF0I54YW%t-;;MP
zxo#GTIj({lEJBM+Z9fb9VGO9EUd=>slAEuW
zRT&$GpVYK}r~WMJUDC2Oyr0B3ogO$I;$~NPAJ&5Y8)GPQ*M08i4=Va?~wxXp?
zTVNzMAua+|rC#21L@}B#KE>OR-K^#Fjlh3bgztHD_4Vm7c=BVAg`2++vWC`p1?pND
z2basgLleUcx37HFoF6~+hA_|AurgHChuTSlZ*FaV^mIwW#7>l=pu7=%)#6!gGykKr
zP4a)(*)CPvJ(*-?5H_8N%4szan{xS7AWw?+0hbNU*w{EHCugq4SlR(y9O&@4vXqqH
z^NT6N@t!`<(k@zK-V;{n*KhvarzvKwUZg6;%K35SSk170D53`7-sM(+Nv
zI*89|j6EeiG<03eX0yy@Ghv{!Caa+#AuEe4naV`;?HlB~cke)7JTN*MnTd%B^8I^c
zOw57)Xu|mhD|}N^)7M*vhK4>rKZp77K_Fi)TY(Id%bf*uZ2STPQE_m>9JI7hBcKd-w=D%g9
z)%ERdf4Sm3@kA=nGphOl)c&tu_oj+bsi>$}SXf>^VRQRW_ww?xWb4)+cpTDyd}fio
z9(rDASFWY}zLw#`>+a409!*|;zOIpxl98sK-q-f__RE9m(4rzzK7M{$CMIHb_SDNu
zdk9-wTN+y0Y}Fd0q?DA2Y<}p=tE=G1$i9Jr;QoFQ&@uu)e7WNsowV>^^$`{E^3G#N
z^~@IwHm-t=Pt00g5WddO&zEVopffWw11kcH+cgCSwN@F863?Exzpqh5KybD_aX8Z1|EP|(_3m40Jmqe9a4
z!F1{EZJZ-L8w<-CSa5gu*7~8pgM(tt51=~)t>_X(M%BNzu#(u{;)(M&%Ss0>1;w@!
zZ|h(s@x-}fstpDn{?GV$G-z{8<_aT4;B#x~E_glPLG$zT7f7b|X7T!pNJ{@DN2X;O;UiWSq0Q;JV5WyWfs_io~?oN}Zs>DK6=cJ@5}!#OhL
zgo<;TAp#>y^q<`>W5vV0_L97D9=-&wZAq;>Xco1Ef2$8R)P)BZT~Kz_aF=^CWkE0D
zgMuAtZjP142Nx^&1!eARyBQZV#Nk5xCe~;O=@uqimX(@#p8vj5X#tSV)J7@&Z-LMc
zN;s!dfyO)nUs8>ktcL3#)5J#!k4HWy`;Zi$4Z%YBxX*^fte~N*|7caXb<3EDey{F|
zrT)lOeW5Jx^Yfz-vA>Hdcr8{bnwe8b((%WyXy{;=r1-Hj?B)Y2B>PgyJxyXMl6ID<;NaB>ArtU
z=Vv(cwlR-KK8yA8v0=kyn9nCx4EASveSYs5t}s<}7f|1X?$KNd_EK_qlaZu1+C7j<
zkoy&3A4o)nh${Y?;SS6sF}fJaGf!b6)!p1OUpYz~8(z8V&LguNvLloFtxqZLE8U^=
zLrR;nW4)2N3!+2G&{x3_thAZ1_~oa2m3j3n8kvtMI0N-#*EAb-&(8K%p6d6v{g6xU
z8h@4dHrI@VErqm_S&?hxZE5Q0dRu>D1`tEB)Y*PJvlWxXo3OxYIFXvlrgTPdM$FHK
zRSiz8vN-N|gC*`Lt}%t8{o}@)>Eti(
ztvSUrVGoC4rAZM)?)+WTB9+&6WuN1OT>M=>F9%U3^kPk%Tb@?7#N~9(7YL-DXr0e6
z%_5Grwr{`Jx*g5@_YPyO*&5@I*1j-%2T15+V^p2}!!i#Gf9Je?a1`gS3WOGBdyRy)
zjJK6n-ze}^4rIo9GWvuDdTnQF@6hRtF3>G|HIT_|XUinuWo8!>m898M1VFy~*IgKR
zcA46}t^boz(VjryQ;|GxNwgvCJGB?=C|dPGH_A>y!P&d$QPZzWn8tVvjL~Z<+~R)t1+&OPxZCL3B71rkJwT0
z7t^0pv6rh?{Ct+Y)Ro>hkx5X7m)<~}=$%e{uf=9aS85;od)k8!^ZaaRJFzh&S<1ka
ztD>p)+|^1EHex|jW7^%{#BTaGk1=qzU3}`dB%`W3LHD~7eqJ$t*9<(u1@wtpUf3QY
zn&Hxq{P_W8Z#gZk9(YI+^&A5e5uN8yb-g3)WRtH7E6L7&X!zx%sWqx=X |