diff --git a/api/build.gradle.kts b/api/build.gradle.kts index 5703d471c..98de1e50d 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -20,6 +20,7 @@ dependencies { implementation(project(":nebulosa-horizons")) implementation(project(":nebulosa-image")) implementation(project(":nebulosa-indi-client")) + implementation(project(":nebulosa-job-manager")) implementation(project(":nebulosa-log")) implementation(project(":nebulosa-lx200-protocol")) implementation(project(":nebulosa-nova")) @@ -48,7 +49,8 @@ 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.12") + annotationProcessor("org.springframework:spring-context-indexer:6.1.12") + annotationProcessor("org.springframework.boot:spring-boot-configuration-processor") testImplementation(project(":nebulosa-astrobin-api")) testImplementation(project(":nebulosa-skycatalog-stellarium")) testImplementation(project(":nebulosa-test")) diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVEvent.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVEvent.kt index b4cad781b..160f6ef63 100644 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVEvent.kt +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVEvent.kt @@ -1,5 +1,6 @@ package nebulosa.api.alignment.polar.darv +import com.fasterxml.jackson.annotation.JsonIgnoreProperties import nebulosa.api.cameras.CameraCaptureEvent import nebulosa.api.message.MessageEvent import nebulosa.guiding.GuideDirection @@ -7,9 +8,9 @@ import nebulosa.indi.device.camera.Camera data class DARVEvent( @JvmField val camera: Camera, - @JvmField val state: DARVState = DARVState.IDLE, - @JvmField val direction: GuideDirection? = null, - @JvmField val capture: CameraCaptureEvent? = null, + @JvmField var state: DARVState = DARVState.IDLE, + @JvmField var direction: GuideDirection? = null, + @JvmField @field:JsonIgnoreProperties("camera") val capture: CameraCaptureEvent = CameraCaptureEvent(camera), ) : MessageEvent { override val eventName = "DARV.ELAPSED" diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVExecutor.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVExecutor.kt index 53b85c769..586d143c2 100644 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVExecutor.kt +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVExecutor.kt @@ -13,6 +13,7 @@ import org.greenrobot.eventbus.ThreadMode import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor import org.springframework.stereotype.Component import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.Executor /** * @see Reference @@ -22,7 +23,7 @@ import java.util.concurrent.ConcurrentHashMap class DARVExecutor( private val messageService: MessageService, private val threadPoolTaskExecutor: ThreadPoolTaskExecutor, -) : Consumer, CameraEventAware { +) : Consumer, CameraEventAware, Executor by threadPoolTaskExecutor { private val jobs = ConcurrentHashMap.newKeySet(1) @@ -32,31 +33,28 @@ class DARVExecutor( @Subscribe(threadMode = ThreadMode.ASYNC) override fun handleCameraEvent(event: CameraEvent) { - jobs.find { it.task.camera === event.device }?.handleCameraEvent(event) + jobs.find { it.camera === event.device }?.handleCameraEvent(event) } @Synchronized fun execute(camera: Camera, guideOutput: GuideOutput, request: DARVStartRequest) { check(camera.connected) { "${camera.name} Camera is not connected" } check(guideOutput.connected) { "${guideOutput.name} Guide Output is not connected" } - check(jobs.none { it.task.camera === camera }) { "${camera.name} DARV Job is already in progress" } - check(jobs.none { it.task.guideOutput === guideOutput }) { "${camera.name} DARV Job is already in progress" } + check(jobs.none { it.camera === camera }) { "${camera.name} DARV Job is already in progress" } + check(jobs.none { it.guideOutput === guideOutput }) { "${camera.name} DARV Job is already in progress" } - val task = DARVTask(camera, guideOutput, request, threadPoolTaskExecutor) - task.subscribe(this) - - with(DARVJob(task)) { + with(DARVJob(this, camera, guideOutput, request)) { + val completable = runAsync(threadPoolTaskExecutor) jobs.add(this) - whenComplete { _, _ -> jobs.remove(this) } - start() + completable.whenComplete { _, _ -> jobs.remove(this) } } } fun stop(camera: Camera) { - jobs.find { it.task.camera === camera }?.stop() + jobs.find { it.camera === camera }?.stop() } fun status(camera: Camera): DARVEvent? { - return jobs.find { it.task.camera === camera }?.task?.get() as? DARVEvent + return jobs.find { it.camera === camera }?.status } } diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVJob.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVJob.kt index 97d290089..aaaf29e4c 100644 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVJob.kt +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVJob.kt @@ -1,14 +1,119 @@ package nebulosa.api.alignment.polar.darv -import nebulosa.api.cameras.CameraEventAware -import nebulosa.api.tasks.Job +import nebulosa.api.cameras.* +import nebulosa.api.guiding.GuidePulseRequest +import nebulosa.api.guiding.GuidePulseTask +import nebulosa.api.message.MessageEvent +import nebulosa.indi.device.camera.Camera import nebulosa.indi.device.camera.CameraEvent +import nebulosa.indi.device.camera.FrameType +import nebulosa.indi.device.guider.GuideOutput +import nebulosa.job.manager.AbstractJob +import nebulosa.job.manager.SplitTask +import nebulosa.job.manager.Task +import nebulosa.job.manager.delay.DelayEvent +import nebulosa.job.manager.delay.DelayTask +import nebulosa.log.loggerFor +import java.nio.file.Files +import java.time.Duration -data class DARVJob(override val task: DARVTask) : Job(), CameraEventAware { +data class DARVJob( + @JvmField val darvExecutor: DARVExecutor, + @JvmField val camera: Camera, + @JvmField val guideOutput: GuideOutput, + @JvmField val request: DARVStartRequest, +) : AbstractJob(), CameraEventAware { - override val name = "${task.camera.name} DARV Job" + @JvmField val cameraRequest = request.capture.copy( + exposureAmount = 1, + exposureTime = request.capture.exposureTime + request.capture.exposureDelay, + savePath = CAPTURE_SAVE_PATH, exposureDelay = Duration.ZERO, + frameType = FrameType.LIGHT, autoSave = false, + autoSubFolderMode = AutoSubFolderMode.OFF + ) + + private val direction = if (request.reversed) request.direction.reversed else request.direction + private val guidePulseDuration = request.capture.exposureTime.dividedBy(2L) + + private val cameraExposureTask = CameraExposureTask(this, camera, cameraRequest) + private val delayTask = DelayTask(this, request.capture.exposureDelay) + private val forwardGuidePulseTask = GuidePulseTask(this, guideOutput, GuidePulseRequest(direction, guidePulseDuration)) + private val backwardGuidePulseTask = GuidePulseTask(this, guideOutput, GuidePulseRequest(direction.reversed, guidePulseDuration)) + private val delayAndGuidePulseTask = DelayAndGuidePulseTask() + private val task = SplitTask(listOf(cameraExposureTask, delayAndGuidePulseTask), darvExecutor) + + @JvmField val status = DARVEvent(camera) + + inline val savedPath + get() = status.capture.savedPath + + init { + status.capture.exposureAmount = 1 + + add(task) + } override fun handleCameraEvent(event: CameraEvent) { - task.handleCameraEvent(event) + cameraExposureTask.handleCameraEvent(event) + } + + override fun accept(event: Any) { + when (event) { + is DelayEvent -> { + status.state = if (event.task === delayTask) DARVState.INITIAL_PAUSE + else if (event.task === forwardGuidePulseTask.delayTask) DARVState.FORWARD + else DARVState.BACKWARD + + status.capture.handleCameraDelayEvent(event, CameraCaptureState.EXPOSURING) + } + is CameraExposureEvent -> { + status.capture.handleCameraExposureEvent(event) + + if (event is CameraExposureFinished) { + status.capture.send() + } + } + else -> return + } + + status.send() + } + + override fun beforeStart() { + LOG.debug("DARV started. camera={}, guideOutput={}, request={}", camera, guideOutput, request) + + status.capture.handleCameraCaptureStarted(cameraExposureTask.exposureTimeInMicroseconds) + } + + override fun afterFinish() { + status.capture.handleCameraCaptureFinished() + status.state = DARVState.IDLE + status.send() + + LOG.debug("DARV finished. camera={}, guideOutput={}, request={}", camera, guideOutput, request) + } + + @Suppress("NOTHING_TO_INLINE") + private inline fun MessageEvent.send() { + darvExecutor.accept(this) + } + + private inner class DelayAndGuidePulseTask : Task { + + override fun run() { + delayTask.run() + + status.direction = forwardGuidePulseTask.request.direction + forwardGuidePulseTask.run() + + status.direction = backwardGuidePulseTask.request.direction + backwardGuidePulseTask.run() + } + } + + companion object { + + @JvmStatic private val LOG = loggerFor() + @JvmStatic private val CAPTURE_SAVE_PATH = Files.createTempDirectory("darv-") } } diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVTask.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVTask.kt deleted file mode 100644 index fc5cb574b..000000000 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVTask.kt +++ /dev/null @@ -1,129 +0,0 @@ -package nebulosa.api.alignment.polar.darv - -import io.reactivex.rxjava3.functions.Consumer -import nebulosa.api.cameras.* -import nebulosa.api.guiding.GuidePulseEvent -import nebulosa.api.guiding.GuidePulseRequest -import nebulosa.api.guiding.GuidePulseTask -import nebulosa.api.message.MessageEvent -import nebulosa.api.tasks.AbstractTask -import nebulosa.api.tasks.SplitTask -import nebulosa.api.tasks.Task -import nebulosa.api.tasks.delay.DelayEvent -import nebulosa.api.tasks.delay.DelayTask -import nebulosa.guiding.GuideDirection -import nebulosa.indi.device.camera.Camera -import nebulosa.indi.device.camera.CameraEvent -import nebulosa.indi.device.camera.FrameType -import nebulosa.indi.device.guider.GuideOutput -import nebulosa.indi.device.mount.Mount -import nebulosa.log.loggerFor -import nebulosa.util.concurrency.cancellation.CancellationToken -import java.nio.file.Files -import java.time.Duration -import java.util.concurrent.Executor - -data class DARVTask( - @JvmField val camera: Camera, - @JvmField val guideOutput: GuideOutput, - @JvmField val request: DARVStartRequest, - private val executor: Executor, -) : AbstractTask(), Consumer, CameraEventAware { - - @JvmField val cameraRequest = request.capture.copy( - exposureTime = request.capture.exposureTime + request.capture.exposureDelay, - savePath = CAPTURE_SAVE_PATH, - exposureAmount = 1, exposureDelay = Duration.ZERO, - frameType = FrameType.LIGHT, autoSave = false, autoSubFolderMode = AutoSubFolderMode.OFF - ) - - private val cameraCaptureTask = CameraCaptureTask(camera, cameraRequest) - private val delayTask = DelayTask(request.capture.exposureDelay) - private val forwardGuidePulseTask: GuidePulseTask - private val backwardGuidePulseTask: GuidePulseTask - - @Volatile private var state = DARVState.IDLE - @Volatile private var direction: GuideDirection? = null - - init { - val direction = if (request.reversed) request.direction.reversed else request.direction - val guidePulseDuration = request.capture.exposureTime.dividedBy(2L) - - forwardGuidePulseTask = GuidePulseTask(guideOutput, GuidePulseRequest(direction, guidePulseDuration)) - backwardGuidePulseTask = GuidePulseTask(guideOutput, GuidePulseRequest(direction.reversed, guidePulseDuration)) - - cameraCaptureTask.subscribe(this) - delayTask.subscribe(this) - forwardGuidePulseTask.subscribe(this) - backwardGuidePulseTask.subscribe(this) - } - - override fun handleCameraEvent(event: CameraEvent) { - cameraCaptureTask.handleCameraEvent(event) - } - - override fun execute(cancellationToken: CancellationToken) { - LOG.info("DARV started. camera={}, guideOutput={}, request={}", camera, guideOutput, request) - - if (guideOutput is Mount) camera.snoop(camera.snoopedDevices.filter { it !is Mount } + guideOutput) - - val task = SplitTask(listOf(cameraCaptureTask, Task.of(delayTask, forwardGuidePulseTask, backwardGuidePulseTask)), executor) - task.execute(cancellationToken) - - state = DARVState.IDLE - sendEvent() - - LOG.info("DARV finished. camera={}, guideOutput={}, request={}", camera, guideOutput, request) - } - - override fun canUseAsLastEvent(event: MessageEvent) = event is DARVEvent - - override fun accept(event: Any) { - when (event) { - is DelayEvent -> { - state = DARVState.INITIAL_PAUSE - } - is CameraCaptureEvent -> { - if (event.state == CameraCaptureState.EXPOSURE_FINISHED) { - onNext(event) - } - - sendEvent(event) - } - is GuidePulseEvent -> { - direction = event.task.request.direction - state = if (direction == forwardGuidePulseTask.request.direction) DARVState.FORWARD else DARVState.BACKWARD - } - else -> return LOG.warn("unknown event: {}", event) - } - } - - @Suppress("NOTHING_TO_INLINE") - private inline fun sendEvent(capture: CameraCaptureEvent? = null) { - onNext(DARVEvent(camera, state, direction, capture)) - } - - override fun reset() { - state = DARVState.IDLE - direction = null - - cameraCaptureTask.reset() - delayTask.reset() - forwardGuidePulseTask.reset() - backwardGuidePulseTask.reset() - } - - override fun close() { - cameraCaptureTask.close() - delayTask.close() - forwardGuidePulseTask.close() - backwardGuidePulseTask.close() - super.close() - } - - companion object { - - @JvmStatic private val LOG = loggerFor() - @JvmStatic private val CAPTURE_SAVE_PATH = Files.createTempDirectory("darv-") - } -} diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPAEvent.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPAEvent.kt index 67b3d7f51..87e1c5c90 100644 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPAEvent.kt +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPAEvent.kt @@ -1,5 +1,6 @@ package nebulosa.api.alignment.polar.tppa +import com.fasterxml.jackson.annotation.JsonIgnoreProperties import com.fasterxml.jackson.databind.annotation.JsonSerialize import nebulosa.api.beans.converters.angle.DeclinationSerializer import nebulosa.api.beans.converters.angle.RightAscensionSerializer @@ -10,15 +11,15 @@ import nebulosa.math.Angle data class TPPAEvent( @JvmField val camera: Camera, - @JvmField val state: TPPAState = TPPAState.IDLE, - @JvmField @field:JsonSerialize(using = RightAscensionSerializer::class) val rightAscension: Angle = 0.0, - @JvmField @field:JsonSerialize(using = DeclinationSerializer::class) val declination: Angle = 0.0, - @JvmField @field:JsonSerialize(using = DeclinationSerializer::class) val azimuthError: Angle = 0.0, - @JvmField @field:JsonSerialize(using = DeclinationSerializer::class) val altitudeError: Angle = 0.0, - @JvmField @JsonSerialize(using = DeclinationSerializer::class) val totalError: Angle = 0.0, - @JvmField val azimuthErrorDirection: String = "", - @JvmField val altitudeErrorDirection: String = "", - @JvmField val capture: CameraCaptureEvent? = null, + @JvmField var state: TPPAState = TPPAState.IDLE, + @JvmField @field:JsonSerialize(using = RightAscensionSerializer::class) var rightAscension: Angle = 0.0, + @JvmField @field:JsonSerialize(using = DeclinationSerializer::class) var declination: Angle = 0.0, + @JvmField @field:JsonSerialize(using = DeclinationSerializer::class) var azimuthError: Angle = 0.0, + @JvmField @field:JsonSerialize(using = DeclinationSerializer::class) var altitudeError: Angle = 0.0, + @JvmField @JsonSerialize(using = DeclinationSerializer::class) var totalError: Angle = 0.0, + @JvmField var azimuthErrorDirection: String = "", + @JvmField var altitudeErrorDirection: String = "", + @JvmField @field:JsonIgnoreProperties("camera") val capture: CameraCaptureEvent = CameraCaptureEvent(camera), ) : MessageEvent { override val eventName = "TPPA.ELAPSED" 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 1b7d947f6..f7e6119a3 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 @@ -5,12 +5,15 @@ import nebulosa.api.beans.annotations.Subscriber import nebulosa.api.cameras.CameraEventAware import nebulosa.api.message.MessageEvent import nebulosa.api.message.MessageService +import nebulosa.api.mounts.MountEventAware import nebulosa.indi.device.camera.Camera import nebulosa.indi.device.camera.CameraEvent import nebulosa.indi.device.mount.Mount +import nebulosa.indi.device.mount.MountEvent import okhttp3.OkHttpClient import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor import org.springframework.stereotype.Component import java.util.concurrent.ConcurrentHashMap @@ -19,7 +22,8 @@ import java.util.concurrent.ConcurrentHashMap class TPPAExecutor( private val messageService: MessageService, private val httpClient: OkHttpClient, -) : Consumer, CameraEventAware { + private val threadPoolTaskExecutor: ThreadPoolTaskExecutor, +) : Consumer, CameraEventAware, MountEventAware { private val jobs = ConcurrentHashMap.newKeySet(1) @@ -29,40 +33,43 @@ class TPPAExecutor( @Subscribe(threadMode = ThreadMode.ASYNC) override fun handleCameraEvent(event: CameraEvent) { - jobs.find { it.task.camera === event.device }?.handleCameraEvent(event) + jobs.find { it.camera === event.device }?.handleCameraEvent(event) + } + + @Subscribe(threadMode = ThreadMode.ASYNC) + override fun handleMountEvent(event: MountEvent) { + jobs.find { it.mount === event.device }?.handleMountEvent(event) } @Synchronized fun execute(camera: Camera, mount: Mount, request: TPPAStartRequest) { check(camera.connected) { "${camera.name} Camera is not connected" } check(mount.connected) { "${mount.name} Mount is not connected" } - 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" } + check(jobs.none { it.camera === camera }) { "${camera.name} TPPA Job is already in progress" } + check(jobs.none { it.mount === mount }) { "${camera.name} TPPA Job is already in progress" } val solver = request.plateSolver.get(httpClient) - val task = TPPATask(camera, solver, request, mount) - task.subscribe(this) - with(TPPAJob(task)) { + with(TPPAJob(this, camera, solver, request, mount)) { + val completable = runAsync(threadPoolTaskExecutor) jobs.add(this) - whenComplete { _, _ -> jobs.remove(this) } - start() + completable.whenComplete { _, _ -> jobs.remove(this) } } } fun stop(camera: Camera) { - jobs.find { it.task.camera === camera }?.stop() + jobs.find { it.camera === camera }?.stop() } fun pause(camera: Camera) { - jobs.find { it.task.camera === camera }?.pause() + jobs.find { it.camera === camera }?.pause() } fun unpause(camera: Camera) { - jobs.find { it.task.camera === camera }?.unpause() + jobs.find { it.camera === camera }?.unpause() } fun status(camera: Camera): TPPAEvent? { - return jobs.find { it.task.camera === camera }?.task?.get() as? TPPAEvent + return jobs.find { it.camera === camera }?.status } } diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPAJob.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPAJob.kt index e71b4e21b..3adf0b6a0 100644 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPAJob.kt +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPAJob.kt @@ -1,14 +1,254 @@ package nebulosa.api.alignment.polar.tppa -import nebulosa.api.cameras.CameraEventAware -import nebulosa.api.tasks.Job +import nebulosa.alignment.polar.point.three.ThreePointPolarAlignment +import nebulosa.alignment.polar.point.three.ThreePointPolarAlignmentResult +import nebulosa.api.cameras.* +import nebulosa.api.message.MessageEvent +import nebulosa.api.mounts.MountEventAware +import nebulosa.api.mounts.MountMoveRequest +import nebulosa.api.mounts.MountMoveTask +import nebulosa.api.mounts.MountTrackTask +import nebulosa.indi.device.camera.Camera import nebulosa.indi.device.camera.CameraEvent +import nebulosa.indi.device.camera.FrameType +import nebulosa.indi.device.mount.Mount +import nebulosa.indi.device.mount.MountEvent +import nebulosa.job.manager.AbstractJob +import nebulosa.job.manager.Task +import nebulosa.job.manager.delay.DelayEvent +import nebulosa.job.manager.delay.DelayStarted +import nebulosa.job.manager.delay.DelayTask +import nebulosa.log.loggerFor +import nebulosa.math.Angle +import nebulosa.math.formatSignedDMS +import nebulosa.platesolver.PlateSolver +import nebulosa.util.time.Stopwatch +import java.nio.file.Files +import java.time.Duration +import kotlin.math.hypot -data class TPPAJob(override val task: TPPATask) : Job(), CameraEventAware { +data class TPPAJob( + @JvmField val tppaExecutor: TPPAExecutor, + @JvmField val camera: Camera, + @JvmField val solver: PlateSolver, + @JvmField val request: TPPAStartRequest, + @JvmField val mount: Mount, + @JvmField val longitude: Angle = mount.longitude, + @JvmField val latitude: Angle = mount.latitude, +) : AbstractJob(), CameraEventAware, MountEventAware { - override val name = "${task.camera.name} TPPA Job" + @JvmField val mountMoveRequest = MountMoveRequest(request.stepDirection, request.stepDuration, request.stepSpeed) + + @JvmField val cameraRequest = request.capture.copy( + savePath = CAPTURE_SAVE_PATH, + exposureAmount = 0, exposureDelay = Duration.ZERO, + exposureTime = maxOf(request.capture.exposureTime, MIN_EXPOSURE_TIME), + frameType = FrameType.LIGHT, autoSave = false, autoSubFolderMode = AutoSubFolderMode.OFF + ) + + @JvmField val status = TPPAEvent(camera) + + @Volatile @JvmField internal var noSolutionAttempts = 0 + + private val alignment = ThreePointPolarAlignment(solver, longitude, latitude) + private val cameraExposureTask = CameraExposureTask(this, camera, cameraRequest) + private val settleDelayTask = DelayTask(this, SETTLE_TIME) + private val tppaTask = TPPATask(this, alignment) + private val mountTrackTask = MountTrackTask(this, mount, true) + private val mountMoveTask = MountMoveTask(this, mount, mountMoveRequest) + private val mountMoveState = BooleanArray(3) + private val stopwatch = Stopwatch() + + inline val savedPath + get() = status.capture.savedPath + + init { + add(mountTrackTask) + add(mountMoveTask) + add(settleDelayTask) + add(cameraExposureTask) + add(tppaTask) + } override fun handleCameraEvent(event: CameraEvent) { - task.handleCameraEvent(event) + cameraExposureTask.handleCameraEvent(event) + } + + override fun handleMountEvent(event: MountEvent) { + mountTrackTask.handleMountEvent(event) + } + + override fun isLoop(): Boolean { + return true + } + + override fun canRun(prev: Task?, current: Task): Boolean { + if (current === settleDelayTask) { + return prev === mountMoveTask + } else if (current === mountMoveTask) { + return alignment.state.ordinal in 1..2 && !mountMoveState[alignment.state.ordinal] + } + + return super.canRun(prev, current) + } + + override fun beforeTask(task: Task) { + if (task === mountMoveTask) { + status.state = TPPAState.SLEWING + status.send() + } else if (task === cameraExposureTask) { + status.capture.savedPath = null + status.state = TPPAState.EXPOSURING + status.send() + } else if (task === tppaTask) { + status.state = TPPAState.SOLVING + status.send() + } + } + + override fun afterTask(task: Task, exception: Throwable?): Boolean { + if (exception == null) { + if (task === mountMoveTask) { + status.rightAscension = task.mount.rightAscension + status.declination = task.mount.declination + status.state = TPPAState.SLEWED + status.send() + } else if (task === cameraExposureTask) { + return status.capture.savedPath != null + } + } + + return super.afterTask(task, exception) + } + + override fun onPause(paused: Boolean) { + if (paused) { + status.state = TPPAState.PAUSING + status.send() + } + + super.onPause(paused) + } + + override fun beforePause(task: Task) { + status.state = TPPAState.PAUSED + status.send() + } + + override fun accept(event: Any) { + when (event) { + is CameraExposureEvent -> { + if (event is CameraExposureStarted) { + status.capture.captureElapsedTime = stopwatch.elapsedMicroseconds + } + + status.capture.handleCameraExposureEvent(event) + + if (event is CameraExposureFinished) { + status.capture.send() + } + + status.send() + } + is DelayEvent -> { + if (event is DelayStarted) { + status.capture.captureElapsedTime = stopwatch.elapsedMicroseconds + } + + status.capture.handleCameraDelayEvent(event) + + if (event.task === settleDelayTask) { + status.state = TPPAState.SETTLING + } else if (event.task === mountMoveTask.delayTask) { + status.state = TPPAState.SLEWING + } + + status.send() + } + is ThreePointPolarAlignmentResult.NeedMoreMeasurement -> { + noSolutionAttempts = 0 + status.rightAscension = event.rightAscension + status.declination = event.declination + status.state = TPPAState.SOLVED + status.send() + } + is ThreePointPolarAlignmentResult.NoPlateSolution -> { + noSolutionAttempts++ + status.state = TPPAState.FAILED + + status.send() + + if (noSolutionAttempts >= MAX_ATTEMPTS) { + LOG.error("exhausted all attempts to plate solve") + stop() + } + } + is ThreePointPolarAlignmentResult.Measured -> { + noSolutionAttempts = 0 + + status.rightAscension = event.rightAscension + status.declination = event.declination + status.azimuthError = event.azimuth + status.altitudeError = event.altitude + status.totalError = hypot(status.azimuthError, status.altitudeError) + + status.azimuthErrorDirection = when { + status.azimuthError > 0 -> if (latitude > 0) "πŸ ” Move LEFT/WEST" else "πŸ ” Move LEFT/EAST" + status.azimuthError < 0 -> if (latitude > 0) "Move RIGHT/EAST πŸ –" else "Move RIGHT/WEST πŸ –" + else -> "" + } + + status.altitudeErrorDirection = when { + status.altitudeError > 0 -> if (latitude > 0) "πŸ — Move DOWN" else "Move UP πŸ •" + status.altitudeError < 0 -> if (latitude > 0) "Move UP πŸ •" else "πŸ — Move DOWN" + else -> "" + } + + LOG.debug( + "TPPA aligned. azimuthError={}, altitudeError={}", + status.azimuthError.formatSignedDMS(), status.altitudeError.formatSignedDMS() + ) + + status.state = TPPAState.COMPUTED + status.send() + } + } + } + + override fun beforeStart() { + LOG.debug("TPPA started. longitude={}, latitude={}, camera={}, mount={}, request={}", longitude, latitude, camera, mount, request) + + status.rightAscension = mount.rightAscension + status.declination = mount.declination + + stopwatch.start() + } + + override fun afterFinish() { + LOG.debug("TPPA finished. camera={}, mount={}, request={}", camera, mount, request) + + stopwatch.stop() + + if (request.stopTrackingWhenDone) { + mount.tracking(false) + } + + status.state = TPPAState.FINISHED + status.send() + } + + @Suppress("NOTHING_TO_INLINE") + private inline fun MessageEvent.send() { + tppaExecutor.accept(this) + } + + companion object { + + const val MAX_ATTEMPTS = 30 + + @JvmStatic private val MIN_EXPOSURE_TIME = Duration.ofSeconds(1L) + @JvmStatic private val SETTLE_TIME = Duration.ofSeconds(5) + @JvmStatic private val CAPTURE_SAVE_PATH = Files.createTempDirectory("tppa-") + @JvmStatic private val LOG = loggerFor() } } diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPATask.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPATask.kt index 44f265314..a10fa3aed 100644 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPATask.kt +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPATask.kt @@ -1,321 +1,29 @@ package nebulosa.api.alignment.polar.tppa -import io.reactivex.rxjava3.functions.Consumer import nebulosa.alignment.polar.point.three.ThreePointPolarAlignment -import nebulosa.alignment.polar.point.three.ThreePointPolarAlignmentResult -import nebulosa.api.cameras.* -import nebulosa.api.message.MessageEvent -import nebulosa.api.mounts.MountMoveRequest -import nebulosa.api.mounts.MountMoveTask -import nebulosa.api.tasks.AbstractTask -import nebulosa.api.tasks.delay.DelayEvent -import nebulosa.api.tasks.delay.DelayTask -import nebulosa.indi.device.camera.Camera -import nebulosa.indi.device.camera.CameraEvent -import nebulosa.indi.device.camera.FrameType -import nebulosa.indi.device.mount.Mount -import nebulosa.log.loggerFor -import nebulosa.math.Angle -import nebulosa.math.formatHMS -import nebulosa.math.formatSignedDMS -import nebulosa.platesolver.PlateSolver -import nebulosa.util.concurrency.cancellation.CancellationToken -import nebulosa.util.concurrency.latch.PauseListener -import nebulosa.util.time.Stopwatch -import java.nio.file.Files -import java.nio.file.Path -import java.time.Duration -import java.util.concurrent.atomic.AtomicBoolean -import kotlin.math.hypot +import nebulosa.job.manager.Task data class TPPATask( - @JvmField val camera: Camera, - @JvmField val solver: PlateSolver, - @JvmField val request: TPPAStartRequest, - @JvmField val mount: Mount? = null, - @JvmField val longitude: Angle = mount!!.longitude, - @JvmField val latitude: Angle = mount!!.latitude, -) : AbstractTask(), Consumer, PauseListener, CameraEventAware { + @JvmField val job: TPPAJob, + @JvmField val alignment: ThreePointPolarAlignment, +) : Task { - @JvmField val mountMoveRequest = MountMoveRequest(request.stepDirection, request.stepDuration, request.stepSpeed) + private val mount = job.mount + private val request = job.request - @JvmField val cameraRequest = request.capture.copy( - savePath = CAPTURE_SAVE_PATH, - exposureAmount = 0, exposureDelay = Duration.ZERO, - exposureTime = maxOf(request.capture.exposureTime, MIN_EXPOSURE_TIME), - frameType = FrameType.LIGHT, autoSave = false, autoSubFolderMode = AutoSubFolderMode.OFF - ) + override fun run() { + val radius = ATTEMPT_RADIUS * (job.noSolutionAttempts + 1) - private val alignment = ThreePointPolarAlignment(solver, longitude, latitude) - private val cameraCaptureTask = CameraCaptureTask(camera, cameraRequest, mount = mount) - private val settleDelayTask = DelayTask(SETTLE_TIME) - private val mountMoveState = BooleanArray(3) - private val elapsedTime = Stopwatch() - private val pausing = AtomicBoolean() - private val finished = AtomicBoolean() - - @Volatile private var rightAscension: Angle = 0.0 - @Volatile private var declination: Angle = 0.0 - @Volatile private var azimuthError: Angle = 0.0 - @Volatile private var altitudeError: Angle = 0.0 - @Volatile private var totalError: Angle = 0.0 - @Volatile private var azimuthErrorDirection = "" - @Volatile private var altitudeErrorDirection = "" - @Volatile private var savedImage: Path? = null - @Volatile private var noSolutionAttempts = 0 - @Volatile private var captureEvent: CameraCaptureEvent? = null - - init { - cameraCaptureTask.subscribe(this) - settleDelayTask.subscribe(this) - } - - override fun handleCameraEvent(event: CameraEvent) { - if (camera === event.device) { - cameraCaptureTask.handleCameraEvent(event) - } - } - - override fun canUseAsLastEvent(event: MessageEvent) = event is TPPAEvent - - override fun accept(event: Any) { - when (event) { - is CameraCaptureEvent -> { - captureEvent = event - - if (event.state == CameraCaptureState.EXPOSURE_FINISHED) { - savedImage = event.savedPath!! - } - - if (!finished.get()) { - sendEvent(TPPAState.EXPOSURING, event) - } - } - is DelayEvent -> { - sendEvent(TPPAState.SETTLING) - } - } - } - - override fun execute(cancellationToken: CancellationToken) { - LOG.info( - "TPPA started. longitude={}, latitude={}, rightAscension={}, declination={}, camera={}, mount={}, request={}", - longitude.formatSignedDMS(), latitude.formatSignedDMS(), mount?.rightAscension?.formatHMS(), mount?.declination?.formatSignedDMS(), - camera, mount, request - ) - - finished.set(false) - elapsedTime.start() - - rightAscension = mount?.rightAscension ?: 0.0 - declination = mount?.declination ?: 0.0 - - cancellationToken.listenToPause(this) - - cameraCaptureTask.initialize(cancellationToken) - - while (!cancellationToken.isCancelled) { - if (cancellationToken.isPaused) { - pausing.set(false) - sendEvent(TPPAState.PAUSED) - cancellationToken.waitForPause() - } - - if (cancellationToken.isCancelled) break - - mount?.tracking(true) - - // SLEWING. - if (mount != null) { - if (alignment.state.ordinal in 1..2 && !mountMoveState[alignment.state.ordinal]) { - MountMoveTask(mount, mountMoveRequest).use { - sendEvent(TPPAState.SLEWING) - it.execute(cancellationToken) - mountMoveState[alignment.state.ordinal] = true - } - - if (cancellationToken.isCancelled) break - - rightAscension = mount.rightAscension - declination = mount.declination - sendEvent(TPPAState.SLEWED) - - LOG.info("TPPA slewed. rightAscension={}, declination={}", mount.rightAscension.formatHMS(), mount.declination.formatSignedDMS()) - - settleDelayTask.execute(cancellationToken) - } - } - - if (cancellationToken.isCancelled) break - - sendEvent(TPPAState.EXPOSURING) - - // CAPTURE. - cameraCaptureTask.executeOnce(cancellationToken) - - if (cancellationToken.isCancelled || savedImage == null) { - break - } - - sendEvent(TPPAState.SOLVING) - - // ALIGNMENT. - val radius = if (mount == null) 0.0 else ATTEMPT_RADIUS * (noSolutionAttempts + 1) - - val result = try { - alignment.align( - savedImage!!, mount?.rightAscension ?: 0.0, mount?.declination ?: 0.0, radius, - request.compensateRefraction, cancellationToken - ) - } catch (e: Throwable) { - sendEvent(TPPAState.FAILED) - LOG.error("failed to align", e) - break - } - - savedImage = null - - LOG.info("TPPA alignment completed. result=$result") - - if (cancellationToken.isCancelled) break - - when (result) { - is ThreePointPolarAlignmentResult.NeedMoreMeasurement -> { - noSolutionAttempts = 0 - rightAscension = result.rightAscension - declination = result.declination - sendEvent(TPPAState.SOLVED) - continue - } - is ThreePointPolarAlignmentResult.NoPlateSolution -> { - noSolutionAttempts++ - - sendEvent(TPPAState.FAILED) - - if (noSolutionAttempts < MAX_ATTEMPTS) { - continue - } else { - LOG.error("exhausted all attempts to plate solve") - break - } - } - is ThreePointPolarAlignmentResult.Measured -> { - noSolutionAttempts = 0 - - rightAscension = result.rightAscension - declination = result.declination - azimuthError = result.azimuth - altitudeError = result.altitude - totalError = hypot(azimuthError, altitudeError) - - azimuthErrorDirection = when { - azimuthError > 0 -> if (latitude > 0) "πŸ ” Move LEFT/WEST" else "πŸ ” Move LEFT/EAST" - azimuthError < 0 -> if (latitude > 0) "Move RIGHT/EAST πŸ –" else "Move RIGHT/WEST πŸ –" - else -> "" - } - - altitudeErrorDirection = when { - altitudeError > 0 -> if (latitude > 0) "πŸ — Move DOWN" else "Move UP πŸ •" - altitudeError < 0 -> if (latitude > 0) "Move UP πŸ •" else "πŸ — Move DOWN" - else -> "" - } - - LOG.info( - "TPPA alignment computed. rightAscension={}, declination={}, azimuthError={}, altitudeError={}", - result.rightAscension.formatHMS(), result.declination.formatSignedDMS(), - azimuthError.formatSignedDMS(), altitudeError.formatSignedDMS(), - ) - - sendEvent(TPPAState.COMPUTED) - - continue - } - is ThreePointPolarAlignmentResult.Cancelled -> { - break - } - } - } - - cameraCaptureTask.finalize(cancellationToken) - - pausing.set(false) - cancellationToken.unlistenToPause(this) - - finished.set(true) - elapsedTime.stop() - - if (request.stopTrackingWhenDone) { - mount?.tracking(false) - } - - sendEvent(TPPAState.FINISHED) - - LOG.info("TPPA finished. camera={}, mount={}, request={}", camera, mount, request) - } - - @Suppress("NOTHING_TO_INLINE") - private inline fun processCameraCaptureEvent(event: CameraCaptureEvent?): CameraCaptureEvent? { - return event?.copy(captureElapsedTime = elapsedTime.elapsed) - } - - private fun sendEvent(state: TPPAState, capture: CameraCaptureEvent? = captureEvent) { - val event = TPPAEvent( - camera, if (pausing.get()) TPPAState.PAUSING else state, rightAscension, declination, - azimuthError, altitudeError, totalError, - azimuthErrorDirection, altitudeErrorDirection, - processCameraCaptureEvent(capture), + val result = alignment.align( + job.savedPath!!, mount.rightAscension, mount.declination, radius, + request.compensateRefraction // TODO: CANCELLATION TOKEN? ) - onNext(event) - - if (capture?.state == CameraCaptureState.EXPOSURE_FINISHED) { - onNext(capture) - } - } - - override fun reset() { - mountMoveState.fill(false) - azimuthError = 0.0 - altitudeError = 0.0 - totalError = 0.0 - azimuthErrorDirection = "" - altitudeErrorDirection = "" - savedImage = null - noSolutionAttempts = 0 - - pausing.set(false) - finished.set(false) - elapsedTime.reset() - - cameraCaptureTask.reset() - settleDelayTask.reset() - - alignment.reset() - - super.reset() - } - - override fun onPause(paused: Boolean) { - pausing.set(paused) - - if (paused) { - sendEvent(TPPAState.PAUSING) - } - } - - override fun close() { - cameraCaptureTask.close() - super.close() + job.accept(result) } companion object { - @JvmStatic private val MIN_EXPOSURE_TIME = Duration.ofSeconds(1L) - @JvmStatic private val SETTLE_TIME = Duration.ofSeconds(5) - @JvmStatic private val CAPTURE_SAVE_PATH = Files.createTempDirectory("tppa-") - @JvmStatic private val LOG = loggerFor() - - const val MAX_ATTEMPTS = 30 - const val ATTEMPT_RADIUS = ThreePointPolarAlignment.DEFAULT_RADIUS / 2.0 + private const val ATTEMPT_RADIUS = ThreePointPolarAlignment.DEFAULT_RADIUS / 2.0 } } diff --git a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusEvent.kt b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusEvent.kt index 09c4bffc3..16b73960d 100644 --- a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusEvent.kt +++ b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusEvent.kt @@ -1,20 +1,23 @@ package nebulosa.api.autofocus +import com.fasterxml.jackson.annotation.JsonIgnoreProperties import nebulosa.api.cameras.CameraCaptureEvent import nebulosa.api.message.MessageEvent import nebulosa.curve.fitting.CurvePoint import nebulosa.curve.fitting.HyperbolicFitting import nebulosa.curve.fitting.QuadraticFitting import nebulosa.curve.fitting.TrendLineFitting +import nebulosa.indi.device.camera.Camera data class AutoFocusEvent( - @JvmField val state: AutoFocusState = AutoFocusState.IDLE, - @JvmField val focusPoint: CurvePoint? = null, - @JvmField val determinedFocusPoint: CurvePoint? = null, - @JvmField val starCount: Int = 0, - @JvmField val starHFD: Double = 0.0, - @JvmField val chart: Chart? = null, - @JvmField val capture: CameraCaptureEvent? = null, + @JvmField val camera: Camera, + @JvmField var state: AutoFocusState = AutoFocusState.IDLE, + @JvmField var focusPoint: CurvePoint? = null, + @JvmField var determinedFocusPoint: CurvePoint? = null, + @JvmField var starCount: Int = 0, + @JvmField var starHFD: Double = 0.0, + @JvmField var chart: Chart? = null, + @JvmField @field:JsonIgnoreProperties("camera") val capture: CameraCaptureEvent = CameraCaptureEvent(camera), ) : MessageEvent { data class Chart( diff --git a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusExecutor.kt b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusExecutor.kt index 688d89246..332b29ab4 100644 --- a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusExecutor.kt +++ b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusExecutor.kt @@ -12,6 +12,7 @@ import nebulosa.indi.device.focuser.Focuser import nebulosa.indi.device.focuser.FocuserEvent import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor import org.springframework.stereotype.Component import java.util.concurrent.ConcurrentHashMap @@ -19,18 +20,19 @@ import java.util.concurrent.ConcurrentHashMap @Subscriber class AutoFocusExecutor( private val messageService: MessageService, + private val threadPoolTaskExecutor: ThreadPoolTaskExecutor, ) : Consumer, CameraEventAware, FocuserEventAware { private val jobs = ConcurrentHashMap.newKeySet(2) @Subscribe(threadMode = ThreadMode.ASYNC) override fun handleCameraEvent(event: CameraEvent) { - jobs.find { it.task.camera === event.device }?.handleCameraEvent(event) + jobs.find { it.camera === event.device }?.handleCameraEvent(event) } @Subscribe(threadMode = ThreadMode.ASYNC) override fun handleFocuserEvent(event: FocuserEvent) { - jobs.find { it.task.focuser === event.device }?.handleFocuserEvent(event) + jobs.find { it.focuser === event.device }?.handleFocuserEvent(event) } override fun accept(event: MessageEvent) { @@ -41,25 +43,23 @@ class AutoFocusExecutor( fun execute(camera: Camera, focuser: Focuser, request: AutoFocusRequest) { check(camera.connected) { "${camera.name} Camera is not connected" } check(focuser.connected) { "${focuser.name} Camera is not connected" } - 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" } + check(jobs.none { it.camera === camera }) { "${camera.name} Auto Focus is already in progress" } + check(jobs.none { it.focuser === focuser }) { "${camera.name} Auto Focus is already in progress" } val starDetector = request.starDetector.get() - val task = AutoFocusTask(camera, focuser, request, starDetector) - task.subscribe(this) - with(AutoFocusJob(task)) { + with(AutoFocusJob(this, camera, focuser, request, starDetector)) { + val completable = runAsync(threadPoolTaskExecutor) jobs.add(this) - whenComplete { _, _ -> jobs.remove(this) } - start() + completable.whenComplete { _, _ -> jobs.remove(this) } } } fun stop(camera: Camera) { - jobs.find { it.task.camera === camera }?.stop() + jobs.find { it.camera === camera }?.stop() } fun status(camera: Camera): AutoFocusEvent? { - return jobs.find { it.task.camera === camera }?.task?.get() as? AutoFocusEvent + return jobs.find { it.camera === camera }?.status } } diff --git a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusJob.kt b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusJob.kt index f1e807513..24406475b 100644 --- a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusJob.kt +++ b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusJob.kt @@ -1,20 +1,440 @@ package nebulosa.api.autofocus -import nebulosa.api.cameras.CameraEventAware +import nebulosa.api.cameras.* +import nebulosa.api.focusers.BacklashCompensationFocuserMoveTask +import nebulosa.api.focusers.BacklashCompensationMode import nebulosa.api.focusers.FocuserEventAware -import nebulosa.api.tasks.Job +import nebulosa.api.message.MessageEvent +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 nebulosa.indi.device.camera.Camera import nebulosa.indi.device.camera.CameraEvent +import nebulosa.indi.device.camera.FrameType +import nebulosa.indi.device.focuser.Focuser import nebulosa.indi.device.focuser.FocuserEvent +import nebulosa.job.manager.AbstractJob +import nebulosa.job.manager.LoopTask +import nebulosa.job.manager.Task +import nebulosa.log.debug +import nebulosa.log.loggerFor +import nebulosa.stardetector.StarDetector +import nebulosa.stardetector.StarPoint +import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics +import java.nio.file.Files +import java.nio.file.Path +import java.time.Duration +import kotlin.math.max +import kotlin.math.roundToInt -data class AutoFocusJob(override val task: AutoFocusTask) : Job(), CameraEventAware, FocuserEventAware { +data class AutoFocusJob( + @JvmField val autoFocusExecutor: AutoFocusExecutor, + @JvmField val camera: Camera, + @JvmField val focuser: Focuser, + @JvmField val request: AutoFocusRequest, + @JvmField val starDetection: StarDetector, +) : AbstractJob(), CameraEventAware, FocuserEventAware { - override val name = "${task.camera.name} Auto Focus Job" + data class MeasuredStars(@JvmField val hfd: Double, @JvmField val stdDev: Double) { + + companion object { + + @JvmStatic val EMPTY = MeasuredStars(0.0, 0.0) + } + } + + @JvmField val cameraRequest = request.capture.copy( + savePath = CAPTURE_SAVE_PATH, + exposureTime = maxOf(request.capture.exposureTime, MIN_EXPOSURE_TIME), + frameType = FrameType.LIGHT, + autoSave = false, + autoSubFolderMode = AutoSubFolderMode.OFF + ) + + private val cameraExposureTask = CameraExposureTask(this, camera, cameraRequest) + private val backlashCompensationFocuserMoveTask = BacklashCompensationFocuserMoveTask(this, focuser, 0, request.backlashCompensation) + private val initialHFDTask = InitialHFDTask() + private val reverse = request.backlashCompensation.mode == BacklashCompensationMode.OVERSHOOT && request.backlashCompensation.backlashIn > 0 + private val offsetSteps = request.initialOffsetSteps + private val numberOfSteps = offsetSteps + 1 + private val obtainFocusPointsTask = ObtainFocusPointsTask(numberOfSteps, offsetSteps, reverse) + private val morePointsNeededToTheLeftTask = MorePointsNeededToTheLeftTask() + private val morePointsNeededToTheRightTask = MorePointsNeededToTheRightTask() + private val computeTrendLineCountTask = ComputeTrendLineCountTask() + private val computeFinalFocusPointTask = ComputeFinalFocusPointTask() + private val maximumFocusPoints = request.capture.exposureAmount * request.initialOffsetSteps * 10 + private val measurements = ArrayList(request.capture.exposureAmount) + private val focusPoints = ArrayList(maximumFocusPoints) + + @Volatile private var initialFocusPosition = 0 + @Volatile private var initialHFD = MeasuredStars.EMPTY + @Volatile private var measurementResult = MeasuredStars.EMPTY + @Volatile private var numberOfAttempts = 0 + + @Volatile private var trendLineCurve: TrendLineFitting.Curve? = null + @Volatile private var parabolicCurve: QuadraticFitting.Curve? = null + @Volatile private var hyperbolicCurve: HyperbolicFitting.Curve? = null + @Volatile private var leftCount = 0 + @Volatile private var rightCount = 0 + + @JvmField val status = AutoFocusEvent(camera) + + init { + // Get initial position information, as average of multiple exposures, if configured this way. + if (request.rSquaredThreshold <= 0.0) { + add(initialHFDTask) + } + + add(obtainFocusPointsTask) + add(computeTrendLineCountTask) + + LoopTask(this, listOf(morePointsNeededToTheLeftTask, morePointsNeededToTheRightTask, computeTrendLineCountTask)) { _, _ -> + (leftCount > 0 || rightCount > 0) && !isDataPointsEnough + }.also(::add) + + add(computeFinalFocusPointTask) + } + + val isDataPointsEnough + get() = trendLineCurve != null && (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) override fun handleCameraEvent(event: CameraEvent) { - task.handleCameraEvent(event) + cameraExposureTask.handleCameraEvent(event) } override fun handleFocuserEvent(event: FocuserEvent) { - task.handleFocuserEvent(event) + backlashCompensationFocuserMoveTask.handleFocuserEvent(event) + } + + override fun beforeStart() { + initialFocusPosition = focuser.position + + LOG.debug { "Auto Focus started. reverse=$reverse, request=$request, camera=$camera, focuser=$focuser" } + } + + override fun canRun(prev: Task?, current: Task): Boolean { + if (current === initialHFDTask) { + return numberOfAttempts == 0 + } else if (current === morePointsNeededToTheLeftTask) { + return trendLineCurve!!.left.points.size < offsetSteps && + focusPoints.count { it.x < trendLineCurve!!.minimum.x && it.y == 0.0 } < offsetSteps + } else if (current === morePointsNeededToTheRightTask) { + return trendLineCurve!!.right.points.size < offsetSteps && + focusPoints.count { it.x > trendLineCurve!!.minimum.x && it.y == 0.0 } < offsetSteps + } + + return super.canRun(prev, current) + } + + override fun accept(event: Any) { + when (event) { + is CameraExposureEvent -> { + status.capture.handleCameraExposureEvent(event) + + if (event is CameraExposureFinished) { + status.capture.send() + + status.state = AutoFocusState.ANALYSING + status.send() + + val detectedStars = starDetection.detect(event.savedPath) + status.starCount = detectedStars.size + LOG.debug("detected {} stars", status.starCount) + + val measurement = detectedStars.measureDetectedStars() + status.starHFD = measurement.hfd + LOG.debug("HFD measurement: hfd={}, stdDev={}", measurement.hfd, measurement.stdDev) + measurements.add(measurement) + + status.state = AutoFocusState.ANALYSED + } else { + status.state = AutoFocusState.EXPOSURING + } + + status.send() + } + } + } + + private fun evaluateAllMeasurements(): MeasuredStars { + if (measurements.isEmpty()) MeasuredStars.EMPTY + if (measurements.size == 1) return measurements[0] + val descriptiveStatistics = DescriptiveStatistics(measurements.size) + measurements.forEach { descriptiveStatistics.addValue(it.hfd) } + return MeasuredStars(descriptiveStatistics.mean, descriptiveStatistics.standardDeviation) + } + + private fun computeCurveFittings() { + with(focusPoints) { + trendLineCurve = TrendLineFitting.calculate(this) + + if (size >= 3) { + if (request.fittingMode == AutoFocusFittingMode.PARABOLIC || request.fittingMode == AutoFocusFittingMode.TREND_PARABOLIC) { + parabolicCurve = QuadraticFitting.calculate(this) + } else if (request.fittingMode == AutoFocusFittingMode.HYPERBOLIC || request.fittingMode == AutoFocusFittingMode.TREND_HYPERBOLIC) { + hyperbolicCurve = HyperbolicFitting.calculate(this) + } + } + + val predictedFocusPoint = status.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] + status.chart = AutoFocusEvent.Chart(predictedFocusPoint, minX, minY, maxX, maxY, trendLineCurve, parabolicCurve, hyperbolicCurve) + + status.state = AutoFocusState.CURVE_FITTED + status.copy().send() // TODO: Verificar se Γ© necessΓ‘rio o copy por setar null abaixo. + status.chart = null + } + } + + private fun determineFinalFocusPoint(): CurvePoint? { + return when (request.fittingMode) { + AutoFocusFittingMode.TRENDLINES -> trendLineCurve!!.intersection + AutoFocusFittingMode.PARABOLIC -> parabolicCurve?.minimum + AutoFocusFittingMode.TREND_PARABOLIC -> parabolicCurve?.minimum?.midPoint(trendLineCurve!!.intersection) + AutoFocusFittingMode.HYPERBOLIC -> hyperbolicCurve?.minimum + AutoFocusFittingMode.TREND_HYPERBOLIC -> hyperbolicCurve?.minimum?.midPoint(trendLineCurve!!.intersection) + } + } + + private fun validateCalculatedFocusPosition(focusPoint: CurvePoint, initialHFD: Double): Boolean { + val threshold = request.rSquaredThreshold + + LOG.info("validating calculated focus position. threshold={}", threshold) + + if (threshold > 0.0) { + fun isTrendLineBad() = trendLineCurve?.let { it.left.rSquared < threshold || it.right.rSquared < threshold } != false + fun isParabolicBad() = parabolicCurve?.let { it.rSquared < threshold } != false + fun isHyperbolicBad() = hyperbolicCurve?.let { it.rSquared < threshold } != false + + val isBad = when (request.fittingMode) { + AutoFocusFittingMode.TRENDLINES -> isTrendLineBad() + AutoFocusFittingMode.PARABOLIC -> isParabolicBad() + AutoFocusFittingMode.TREND_PARABOLIC -> isParabolicBad() || isTrendLineBad() + AutoFocusFittingMode.HYPERBOLIC -> isHyperbolicBad() + AutoFocusFittingMode.TREND_HYPERBOLIC -> isHyperbolicBad() || isTrendLineBad() + } + + if (isBad) { + LOG.error("coefficient of determination is below threshold") + return false + } + } + + val min = focusPoints.first().x + val max = focusPoints.last().x + + 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 (isCancelled) return false + + MoveFocuserTask(focusPoint.x.roundToInt(), false).run() + + if (threshold <= 0) { + TakeExposureTask().run() + + val hfd = measurementResult.hfd + + if (initialHFD != 0.0 && hfd > initialHFD * 1.15) { + LOG.warn("New focus point HFR $hfd is significantly worse than original HFR $initialHFD") + return false + } + } + + return true + } + + @Suppress("NOTHING_TO_INLINE") + private inline fun MessageEvent.send() { + autoFocusExecutor.accept(this) + } + + private inner class InitialHFDTask : Task { + + private val takeExposureTask = TakeExposureTask() + + override fun run() { + takeExposureTask.run() + initialHFD = measurementResult + } + } + + private inner class TakeExposureTask(private val exposureAmount: Int = cameraRequest.exposureAmount) : Task { + + override fun run() { + measurementResult = if (!isCancelled) { + measurements.clear() + + status.state = AutoFocusState.EXPOSURING + status.send() + + repeat(max(1, exposureAmount)) { + cameraExposureTask.run() + } + + evaluateAllMeasurements() + } else { + MeasuredStars.EMPTY + } + } + } + + private inner class MoveFocuserTask( + private val position: Int, + private val relative: Boolean, + ) : Task { + + override fun run() { + status.state = AutoFocusState.MOVING + status.send() + + backlashCompensationFocuserMoveTask.position = if (relative) focuser.position + position else position + backlashCompensationFocuserMoveTask.run() + } + } + + private inner class ObtainFocusPointsTask( + private val numberOfSteps: Int, + private val offset: Int, + private val reverse: Boolean, + ) : Task { + + private val stepSize = request.stepSize + private val direction = if (reverse) -1 else 1 + + @JvmField val takeExposureTask = TakeExposureTask() + @JvmField val initialOffsetMoveFocuserTask = MoveFocuserTask(direction * offset * stepSize, true) + @JvmField val reversedMoveFocuserTask = MoveFocuserTask(direction * -stepSize, true) + + override fun run() { + LOG.debug { "retrieving focus points. stepSize=$stepSize, numberOfSteps=$numberOfSteps, offset=$offset, reverse=$reverse" } + + var focusPosition = 0 + + if (offset != 0) { + initialOffsetMoveFocuserTask.run() + focusPosition = focuser.position + } + + var remainingSteps = numberOfSteps + + while (!isCancelled && remainingSteps > 0) { + val currentFocusPosition = focusPosition + + takeExposureTask.run() + val measurement = measurementResult + + if (isCancelled) break + + LOG.debug { "HFD measured after exposures. mean=$measurement" } + + if (remainingSteps-- > 1) { + reversedMoveFocuserTask.run() + focusPosition = focuser.position + } + + if (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.hfd == 0.0) { + LOG.warn("No stars detected in step") + } else { + val focusPoint = CurvePoint(currentFocusPosition.toDouble(), measurement.hfd, measurement.stdDev) + status.focusPoint = focusPoint + + focusPoints.add(focusPoint) + focusPoints.sortBy { it.x } + + LOG.debug { "focus point added. remainingSteps=$remainingSteps, point=$focusPoint" } + + computeCurveFittings() + } + } + } + } + + private inner class MorePointsNeededToTheLeftTask : Task { + + private val obtainFocusPointsTask = ObtainFocusPointsTask(1, -1, false) + + override fun run() { + LOG.info("more data points needed to the left of the minimum") + + val firstX = focusPoints.first().x.roundToInt() + + // Move to the leftmost point - this should never be necessary since we're already there, but just in case + if (focuser.position != firstX) { + MoveFocuserTask(firstX, false).run() + } + + // More points needed to the left. + obtainFocusPointsTask.run() + } + } + + private inner class MorePointsNeededToTheRightTask : Task { + + private val obtainFocusPointsTask = ObtainFocusPointsTask(1, 1, false) + + override fun run() { + LOG.info("more data points needed to the right of the minimum") + + val lastX = focusPoints.last().x.roundToInt() + + // More points needed to the right. Let's get to the rightmost point, and keep going right one point at a time. + if (focuser.position != lastX) { + MoveFocuserTask(lastX, false).run() + } + + // More points needed to the right. + obtainFocusPointsTask.run() + } + } + + private inner class ComputeTrendLineCountTask : Task { + + override fun run() { + leftCount = trendLineCurve?.left?.points?.size ?: 0 + rightCount = trendLineCurve?.right?.points?.size ?: 0 + } + } + + private inner class ComputeFinalFocusPointTask : Task { + + override fun run() { + val finalFocusPoint = determineFinalFocusPoint() + + if (finalFocusPoint == null || !validateCalculatedFocusPosition(finalFocusPoint, initialHFD.hfd)) { + LOG.warn("potentially bad auto-focus. Restoring original focus position") + MoveFocuserTask(initialFocusPosition, false).run() + } else { + status.determinedFocusPoint = finalFocusPoint + status.state = AutoFocusState.FINISHED + status.send() + + LOG.info("Auto Focus completed. x={}, y={}", finalFocusPoint.x, finalFocusPoint.y) + } + } + } + + companion object { + + @JvmStatic private val MIN_EXPOSURE_TIME = Duration.ofSeconds(1L) + @JvmStatic private val CAPTURE_SAVE_PATH = Files.createTempDirectory("af-") + @JvmStatic private val LOG = loggerFor() + + @JvmStatic + private fun List.measureDetectedStars(): MeasuredStars { + if (isEmpty()) return MeasuredStars.EMPTY + val descriptiveStatistics = DescriptiveStatistics(size) + forEach { descriptiveStatistics.addValue(it.hfd) } + return MeasuredStars(descriptiveStatistics.mean, descriptiveStatistics.standardDeviation) + } } } diff --git a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt deleted file mode 100644 index bc0891a56..000000000 --- a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt +++ /dev/null @@ -1,419 +0,0 @@ -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.message.MessageEvent -import nebulosa.api.tasks.AbstractTask -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 nebulosa.indi.device.camera.Camera -import nebulosa.indi.device.camera.CameraEvent -import nebulosa.indi.device.camera.FrameType -import nebulosa.indi.device.focuser.Focuser -import nebulosa.indi.device.focuser.FocuserEvent -import nebulosa.log.loggerFor -import nebulosa.stardetector.StarDetector -import nebulosa.stardetector.StarPoint -import nebulosa.util.concurrency.cancellation.CancellationToken -import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics -import java.nio.file.Files -import java.nio.file.Path -import java.time.Duration -import kotlin.math.max -import kotlin.math.roundToInt - -data class AutoFocusTask( - @JvmField val camera: Camera, - @JvmField val focuser: Focuser, - @JvmField val request: AutoFocusRequest, - @JvmField val starDetection: StarDetector, -) : AbstractTask(), Consumer, CameraEventAware, FocuserEventAware { - - data class MeasuredStars(@JvmField val hfd: Double, @JvmField val stdDev: Double) { - - companion object { - - @JvmStatic val EMPTY = MeasuredStars(0.0, 0.0) - } - } - - @JvmField val cameraRequest = request.capture.copy( - exposureAmount = 0, exposureDelay = Duration.ZERO, - savePath = CAPTURE_SAVE_PATH, - exposureTime = maxOf(request.capture.exposureTime, MIN_EXPOSURE_TIME), - 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, focuser = focuser) - private val focuserMoveTask = BacklashCompensationFocuserMoveTask(focuser, 0, request.backlashCompensation) - - @Volatile private var trendLineCurve: TrendLineFitting.Curve? = null - @Volatile private var parabolicCurve: QuadraticFitting.Curve? = null - @Volatile private var hyperbolicCurve: HyperbolicFitting.Curve? = null - - @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) - } - - override fun handleCameraEvent(event: CameraEvent) { - cameraCaptureTask.handleCameraEvent(event) - } - - override fun handleFocuserEvent(event: FocuserEvent) { - focuserMoveTask.handleFocuserEvent(event) - } - - override fun canUseAsLastEvent(event: MessageEvent) = event is AutoFocusEvent - - override fun execute(cancellationToken: CancellationToken) { - reset() - - val initialFocusPosition = focuser.position - - cameraCaptureTask.initialize(cancellationToken) - - // Get initial position information, as average of multiple exposures, if configured this way. - val initialHFD = if (request.rSquaredThreshold <= 0.0) takeExposure(cancellationToken) else MeasuredStars.EMPTY - val reverse = request.backlashCompensation.mode == BacklashCompensationMode.OVERSHOOT && request.backlashCompensation.backlashIn > 0 - - LOG.info("Auto Focus started. initialHFD={}, reverse={}, request={}, camera={}, focuser={}", initialHFD, reverse, request, camera, focuser) - - var exited = false - var numberOfAttempts = 0 - val maximumFocusPoints = request.capture.exposureAmount * request.initialOffsetSteps * 10 - - while (!exited && !cancellationToken.isCancelled) { - numberOfAttempts++ - - val offsetSteps = request.initialOffsetSteps - val numberOfSteps = offsetSteps + 1 - - LOG.info("attempt #{}. offsetSteps={}, numberOfSteps={}", numberOfAttempts, offsetSteps, numberOfSteps) - - obtainFocusPoints(numberOfSteps, offsetSteps, reverse, cancellationToken) - - if (cancellationToken.isCancelled) break - - 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 { - if (leftCount == 0 && rightCount == 0) { - LOG.warn("Not enought spreaded points") - exited = true - break - } - - LOG.info("data points are not sufficient. attempt={}, numberOfSteps={}", numberOfAttempts, numberOfSteps) - - // Let's keep moving in, one step at a time, until we have enough left trend points. - // Then we can think about moving out to fill in the right trend points. - if (trendLineCurve!!.left.points.size < offsetSteps - && focusPoints.count { it.x < trendLineCurve!!.minimum.x && it.y == 0.0 } < offsetSteps - ) { - LOG.info("more data points needed to the left of the minimum") - - // Move to the leftmost point - this should never be necessary since we're already there, but just in case - if (focuser.position != focusPoints.first().x.roundToInt()) { - moveFocuser(focusPoints.first().x.roundToInt(), cancellationToken, false) - } - - // More points needed to the left. - obtainFocusPoints(1, -1, false, cancellationToken) - } else if (trendLineCurve!!.right.points.size < offsetSteps - && focusPoints.count { it.x > trendLineCurve!!.minimum.x && it.y == 0.0 } < offsetSteps - ) { - // Now we can go to the right, if necessary. - LOG.info("more data points needed to the right of the minimum") - - // More points needed to the right. Let's get to the rightmost point, and keep going right one point at a time. - if (focuser.position != focusPoints.last().x.roundToInt()) { - moveFocuser(focusPoints.last().x.roundToInt(), cancellationToken, false) - } - - // More points needed to the right. - obtainFocusPoints(1, 1, false, cancellationToken) - } - - if (cancellationToken.isCancelled) break - - 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).") - break - } - - if (focuser.position <= 0 || focuser.position >= focuser.maxPosition) { - // Break out when the focuser hits the min/max position. It can't continue from there. - LOG.error("failed to complete. position reached ${focuser.position}") - break - } - } 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 || cancellationToken.isCancelled) break - - val finalFocusPoint = determineFinalFocusPoint() - - if (finalFocusPoint == null || !validateCalculatedFocusPosition(finalFocusPoint, initialHFD.hfd, cancellationToken)) { - if (cancellationToken.isCancelled) { - break - } else if (numberOfAttempts < request.totalNumberOfAttempts) { - moveFocuser(initialFocusPosition, cancellationToken, false) - LOG.warn("potentially bad auto-focus. Reattempting") - reset() - continue - } else { - LOG.warn("potentially bad auto-focus. Restoring original focus position") - exited = true - } - } else { - determinedFocusPoint = finalFocusPoint - LOG.info("Auto Focus completed. x={}, y={}", finalFocusPoint.x, finalFocusPoint.y) - break - } - } - - cameraCaptureTask.finalize(cancellationToken) - - if (exited || cancellationToken.isCancelled) { - LOG.warn("Auto Focus did not complete successfully, so restoring the focuser position to $initialFocusPosition") - sendEvent(if (exited) AutoFocusState.FAILED else AutoFocusState.FINISHED) - - if (exited) { - moveFocuser(initialFocusPosition, CancellationToken.NONE, false) - } - } else { - sendEvent(AutoFocusState.FINISHED) - } - - reset() - - LOG.info("Auto Focus finished. camera={}, focuser={}", camera, focuser) - } - - private fun determineFinalFocusPoint(): CurvePoint? { - return when (request.fittingMode) { - AutoFocusFittingMode.TRENDLINES -> trendLineCurve!!.intersection - AutoFocusFittingMode.PARABOLIC -> parabolicCurve?.minimum - AutoFocusFittingMode.TREND_PARABOLIC -> parabolicCurve?.minimum?.midPoint(trendLineCurve!!.intersection) - AutoFocusFittingMode.HYPERBOLIC -> hyperbolicCurve?.minimum - AutoFocusFittingMode.TREND_HYPERBOLIC -> hyperbolicCurve?.minimum?.midPoint(trendLineCurve!!.intersection) - } - } - - private fun evaluateAllMeasurements(): MeasuredStars { - if (measurements.isEmpty()) MeasuredStars.EMPTY - if (measurements.size == 1) return measurements[0] - val descriptiveStatistics = DescriptiveStatistics(measurements.size) - measurements.forEach { descriptiveStatistics.addValue(it.hfd) } - return MeasuredStars(descriptiveStatistics.mean, descriptiveStatistics.standardDeviation) - } - - override fun accept(event: CameraCaptureEvent) { - if (event.state == CameraCaptureState.EXPOSURE_FINISHED) { - sendEvent(AutoFocusState.EXPOSURED, event) - sendEvent(AutoFocusState.ANALYSING) - val detectedStars = starDetection.detect(event.savedPath!!) - starCount = detectedStars.size - LOG.info("detected $starCount stars") - val measurement = detectedStars.measureDetectedStars() - LOG.info("HFD measurement: hfd={}, stdDev={}", measurement.hfd, measurement.stdDev) - measurements.add(measurement) - sendEvent(AutoFocusState.ANALYSED) - onNext(event) - } else { - sendEvent(AutoFocusState.EXPOSURING, event) - } - } - - private fun takeExposure(cancellationToken: CancellationToken): MeasuredStars { - return if (!cancellationToken.isCancelled) { - measurements.clear() - sendEvent(AutoFocusState.EXPOSURING) - cameraCaptureTask.executeUntil(cancellationToken, max(1, request.capture.exposureAmount)) - evaluateAllMeasurements() - } else { - MeasuredStars.EMPTY - } - } - - private fun obtainFocusPoints(numberOfSteps: Int, offset: Int, reverse: Boolean, cancellationToken: CancellationToken) { - val stepSize = request.stepSize - val direction = if (reverse) -1 else 1 - - LOG.info("retrieving focus points. numberOfSteps={}, offset={}, reverse={}", numberOfSteps, offset, reverse) - - var focusPosition = 0 - - if (offset != 0) { - focusPosition = moveFocuser(direction * offset * stepSize, cancellationToken, true) - } - - var remainingSteps = numberOfSteps - - while (!cancellationToken.isCancelled && remainingSteps > 0) { - val currentFocusPosition = focusPosition - - 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.hfd == 0.0) { - LOG.warn("No stars detected in step") - } else { - focusPoint = CurvePoint(currentFocusPosition.toDouble(), measurement.hfd, measurement.stdDev) - focusPoints.add(focusPoint!!) - focusPoints.sortBy { it.x } - - LOG.info("focus point added. remainingSteps={}, point={}", remainingSteps, focusPoint) - - computeCurveFittings() - } - } - } - - private fun computeCurveFittings() { - with(focusPoints) { - trendLineCurve = TrendLineFitting.calculate(this) - - if (size >= 3) { - if (request.fittingMode == AutoFocusFittingMode.PARABOLIC || request.fittingMode == AutoFocusFittingMode.TREND_PARABOLIC) { - parabolicCurve = QuadraticFitting.calculate(this) - } else if (request.fittingMode == AutoFocusFittingMode.HYPERBOLIC || request.fittingMode == AutoFocusFittingMode.TREND_HYPERBOLIC) { - hyperbolicCurve = HyperbolicFitting.calculate(this) - } - } - - sendEvent(AutoFocusState.CURVE_FITTED) - } - } - - private fun validateCalculatedFocusPosition(focusPoint: CurvePoint, initialHFD: Double, cancellationToken: CancellationToken): Boolean { - val threshold = request.rSquaredThreshold - - LOG.info("validating calculated focus position. threshold={}", threshold) - - if (threshold > 0.0) { - fun isTrendLineBad() = trendLineCurve?.let { it.left.rSquared < threshold || it.right.rSquared < threshold } != false - fun isParabolicBad() = parabolicCurve?.let { it.rSquared < threshold } != false - fun isHyperbolicBad() = hyperbolicCurve?.let { it.rSquared < threshold } != false - - val isBad = when (request.fittingMode) { - AutoFocusFittingMode.TRENDLINES -> isTrendLineBad() - AutoFocusFittingMode.PARABOLIC -> isParabolicBad() - AutoFocusFittingMode.TREND_PARABOLIC -> isParabolicBad() || isTrendLineBad() - AutoFocusFittingMode.HYPERBOLIC -> isHyperbolicBad() - AutoFocusFittingMode.TREND_HYPERBOLIC -> isHyperbolicBad() || isTrendLineBad() - } - - if (isBad) { - LOG.error("coefficient of determination is below threshold") - return false - } - } - - val min = focusPoints.first().x - val max = focusPoints.last().x - - 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) - - if (threshold <= 0) { - if (initialHFD != 0.0 && hfd > initialHFD * 1.15) { - LOG.warn("New focus point HFR $hfd is significantly worse than original HFR $initialHFD") - return false - } - } - - return true - } - - private fun moveFocuser(position: Int, cancellationToken: CancellationToken, relative: Boolean): Int { - sendEvent(AutoFocusState.MOVING) - focuserMoveTask.position = if (relative) focuser.position + position else position - focuserMoveTask.execute(cancellationToken) - return focuser.position - } - - private fun sendEvent(state: AutoFocusState, capture: CameraCaptureEvent? = null) { - val chart = when (state) { - 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 - } - - onNext(AutoFocusEvent(state, focusPoint, determinedFocusPoint, starCount, starHFD, chart, capture)) - } - - override fun reset() { - cameraCaptureTask.reset() - focusPoints.clear() - - trendLineCurve = null - parabolicCurve = null - hyperbolicCurve = null - } - - override fun close() { - super.close() - cameraCaptureTask.close() - } - - companion object { - - @JvmStatic private val MIN_EXPOSURE_TIME = Duration.ofSeconds(1L) - @JvmStatic private val CAPTURE_SAVE_PATH = Files.createTempDirectory("af-") - @JvmStatic private val LOG = loggerFor() - - @JvmStatic - private fun List.measureDetectedStars(): MeasuredStars { - if (isEmpty()) return MeasuredStars.EMPTY - val descriptiveStatistics = DescriptiveStatistics(size) - forEach { descriptiveStatistics.addValue(it.hfd) } - return MeasuredStars(descriptiveStatistics.mean, descriptiveStatistics.standardDeviation) - } - } -} 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 27fc414bf..194200113 100644 --- a/api/src/main/kotlin/nebulosa/api/beans/configurations/BeanConfiguration.kt +++ b/api/src/main/kotlin/nebulosa/api/beans/configurations/BeanConfiguration.kt @@ -109,10 +109,10 @@ class BeanConfiguration { @Bean fun alpacaHttpClient(connectionPool: ConnectionPool) = OkHttpClient.Builder() .connectionPool(connectionPool) - .readTimeout(60L, TimeUnit.SECONDS) - .writeTimeout(60L, TimeUnit.SECONDS) - .connectTimeout(60L, TimeUnit.SECONDS) - .callTimeout(60L, TimeUnit.SECONDS) + .readTimeout(90L, TimeUnit.SECONDS) + .writeTimeout(90L, TimeUnit.SECONDS) + .connectTimeout(90L, TimeUnit.SECONDS) + .callTimeout(90L, TimeUnit.SECONDS) .build() @Bean diff --git a/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameEntity.kt b/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameEntity.kt index 4e684e1b4..a684c42da 100644 --- a/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameEntity.kt +++ b/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameEntity.kt @@ -47,7 +47,6 @@ data class CalibrationFrameEntity( else if (temperature < other.temperature) -1 else if (filter != null && other.filter != null) filter!!.compareTo(other.filter!!) else if (filter == null) -1 - else if (other.filter == null) 1 - else 0 + else 1 } } diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureEvent.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureEvent.kt index 4d158ed95..9cb091ee7 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureEvent.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureEvent.kt @@ -3,25 +3,97 @@ package nebulosa.api.cameras import com.fasterxml.jackson.annotation.JsonIgnore import nebulosa.api.message.MessageEvent import nebulosa.indi.device.camera.Camera +import nebulosa.job.manager.TimedTaskEvent +import nebulosa.job.manager.delay.DelayEvent import java.nio.file.Path -import java.time.Duration data class CameraCaptureEvent( - @JvmField @field:JsonIgnore val task: CameraCaptureTask, - @JvmField val camera: Camera = task.camera, - @JvmField val state: CameraCaptureState = CameraCaptureState.IDLE, - @JvmField val exposureAmount: Int = 0, - @JvmField val exposureCount: Int = 0, - @JvmField val captureRemainingTime: Duration = Duration.ZERO, - @JvmField val captureElapsedTime: Duration = Duration.ZERO, - @JvmField val captureProgress: Double = 0.0, - @JvmField val stepRemainingTime: Duration = Duration.ZERO, - @JvmField val stepElapsedTime: Duration = Duration.ZERO, - @JvmField val stepProgress: Double = 0.0, - @JvmField val savedPath: Path? = null, - @JvmField val liveStackedPath: Path? = null, - @JvmField val capture: CameraStartCaptureRequest? = null, + @JvmField val camera: Camera, + @JvmField var state: CameraCaptureState = CameraCaptureState.IDLE, + @JvmField var exposureAmount: Int = 0, + @JvmField var exposureCount: Int = 0, + @JvmField var captureRemainingTime: Long = 0L, + @JvmField var captureElapsedTime: Long = 0L, + @JvmField var captureProgress: Double = 0.0, + @JvmField var stepRemainingTime: Long = 0L, + @JvmField var stepElapsedTime: Long = 0L, + @JvmField var stepProgress: Double = 0.0, + @JvmField var savedPath: Path? = null, + @JvmField var liveStackedPath: Path? = null, ) : MessageEvent { override val eventName = "CAMERA.CAPTURE_ELAPSED" + + @Volatile @field:JsonIgnore private var captureStartElapsedTime = 0L + @Volatile @field:JsonIgnore private var captureStartRemainingTime = 0L + + @Suppress("NOTHING_TO_INLINE") + private inline fun handleTimedTaskEvent(event: TimedTaskEvent) { + stepRemainingTime = event.remainingTime + stepElapsedTime = event.elapsedTime + stepProgress = event.progress + } + + fun handleCameraExposureStarted(event: CameraExposureStarted) { + handleTimedTaskEvent(event) + state = CameraCaptureState.EXPOSURE_STARTED + captureStartElapsedTime = captureElapsedTime + captureStartRemainingTime = captureRemainingTime + exposureCount++ + } + + fun handleCameraExposureFinished(event: CameraExposureFinished) { + handleTimedTaskEvent(event) + state = CameraCaptureState.EXPOSURE_FINISHED + captureElapsedTime = captureStartElapsedTime + event.elapsedTime + captureRemainingTime = captureStartRemainingTime - event.elapsedTime + computeCaptureProgress() + savedPath = event.savedPath + } + + fun handleCameraExposureElapsed(event: CameraExposureElapsed) { + handleTimedTaskEvent(event) + state = CameraCaptureState.EXPOSURING + captureElapsedTime = captureStartElapsedTime + event.elapsedTime + captureRemainingTime = captureStartRemainingTime - event.elapsedTime + } + + fun handleCameraCaptureStarted(estimatedCaptureTime: Long = 0L) { + state = CameraCaptureState.CAPTURE_STARTED + captureRemainingTime = estimatedCaptureTime + captureElapsedTime = 0L + captureProgress = 0.0 + } + + fun handleCameraCaptureFinished() { + state = CameraCaptureState.CAPTURE_FINISHED + captureRemainingTime = 0L + captureProgress = 1.0 + } + + @Suppress("NOTHING_TO_INLINE") + private inline fun computeCaptureProgress() { + if (captureElapsedTime > 0L && captureRemainingTime >= 0L) { + captureProgress = captureElapsedTime.toDouble() / (captureElapsedTime + captureRemainingTime) + } + } + + fun handleCameraDelayEvent(event: DelayEvent, newState: CameraCaptureState = CameraCaptureState.WAITING) { + handleTimedTaskEvent(event) + captureElapsedTime += event.waitTime + captureRemainingTime -= event.waitTime + state = newState + computeCaptureProgress() + } + + fun handleCameraExposureEvent(event: CameraExposureEvent) { + when (event) { + is CameraExposureElapsed -> handleCameraExposureElapsed(event) + is CameraExposureFinished -> handleCameraExposureFinished(event) + is CameraExposureStarted -> handleCameraExposureStarted(event) + is CameraExposureFailed -> handleTimedTaskEvent(event) + } + + computeCaptureProgress() + } } diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureExecutor.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureExecutor.kt index d3958a4e2..5fe43a2d4 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureExecutor.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureExecutor.kt @@ -18,6 +18,7 @@ import org.greenrobot.eventbus.ThreadMode import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor import org.springframework.stereotype.Component import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.Executor @Component @Subscriber @@ -26,18 +27,18 @@ class CameraCaptureExecutor( private val guider: Guider, private val threadPoolTaskExecutor: ThreadPoolTaskExecutor, private val calibrationFrameService: CalibrationFrameService, -) : Consumer, CameraEventAware, WheelEventAware { +) : Consumer, CameraEventAware, WheelEventAware, Executor by threadPoolTaskExecutor { private val jobs = ConcurrentHashMap.newKeySet(2) @Subscribe(threadMode = ThreadMode.ASYNC) override fun handleCameraEvent(event: CameraEvent) { - jobs.find { it.task.camera === event.device }?.handleCameraEvent(event) + jobs.find { it.camera === event.device }?.handleCameraEvent(event) } @Subscribe(threadMode = ThreadMode.ASYNC) override fun handleFilterWheelEvent(event: FilterWheelEvent) { - jobs.find { it.task.wheel === event.device }?.handleFilterWheelEvent(event) + jobs.find { it.wheel === event.device }?.handleFilterWheelEvent(event) } override fun accept(event: CameraCaptureEvent) { @@ -50,36 +51,30 @@ class CameraCaptureExecutor( mount: Mount? = null, wheel: FilterWheel? = null, focuser: Focuser? = null, rotator: Rotator? = null ) { check(camera.connected) { "${camera.name} Camera is not connected" } - check(jobs.none { it.task.camera === camera }) { "${camera.name} Camera Capture is already in progress" } + check(jobs.none { it.camera === camera }) { "${camera.name} Camera Capture is already in progress" } val liveStackingManager = CameraLiveStackingManager(calibrationFrameService) - val task = CameraCaptureTask( - camera, request, guider, false, threadPoolTaskExecutor, - liveStackingManager, mount, wheel, focuser, rotator - ) - task.subscribe(this) - - with(CameraCaptureJob(task)) { + with(CameraCaptureJob(this, camera, request, guider, liveStackingManager, mount, wheel, focuser, rotator)) { + val completable = runAsync(threadPoolTaskExecutor) jobs.add(this) - whenComplete { _, _ -> jobs.remove(this); liveStackingManager.close() } - start() + completable.whenComplete { _, _ -> jobs.remove(this); liveStackingManager.close() } } } fun pause(camera: Camera) { - jobs.find { it.task.camera === camera }?.pause() + jobs.find { it.camera === camera }?.pause() } fun unpause(camera: Camera) { - jobs.find { it.task.camera === camera }?.unpause() + jobs.find { it.camera === camera }?.unpause() } fun stop(camera: Camera) { - jobs.find { it.task.camera === camera }?.stop() + jobs.find { it.camera === camera }?.stop() } fun status(camera: Camera): CameraCaptureEvent? { - return jobs.find { it.task.camera === camera }?.task?.get() + return jobs.find { it.camera === camera }?.status } } diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureJob.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureJob.kt index 6a310180e..9790dc8af 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureJob.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureJob.kt @@ -1,19 +1,174 @@ package nebulosa.api.cameras -import nebulosa.api.tasks.Job +import nebulosa.api.guiding.DitherAfterExposureEvent +import nebulosa.api.guiding.DitherAfterExposureTask +import nebulosa.api.guiding.WaitForSettleTask import nebulosa.api.wheels.WheelEventAware +import nebulosa.api.wheels.WheelMoveTask +import nebulosa.guiding.Guider +import nebulosa.indi.device.camera.Camera import nebulosa.indi.device.camera.CameraEvent +import nebulosa.indi.device.camera.FrameType +import nebulosa.indi.device.filterwheel.FilterWheel import nebulosa.indi.device.filterwheel.FilterWheelEvent +import nebulosa.indi.device.focuser.Focuser +import nebulosa.indi.device.mount.Mount +import nebulosa.indi.device.rotator.Rotator +import nebulosa.job.manager.AbstractJob +import nebulosa.job.manager.SplitTask +import nebulosa.job.manager.Task +import nebulosa.job.manager.delay.DelayEvent +import nebulosa.job.manager.delay.DelayTask +import nebulosa.log.debug +import nebulosa.log.loggerFor +import java.nio.file.Path -data class CameraCaptureJob(override val task: CameraCaptureTask) : Job(), CameraEventAware, WheelEventAware { +data class CameraCaptureJob( + @JvmField val cameraCaptureExecutor: CameraCaptureExecutor, + @JvmField val camera: Camera, + @JvmField val request: CameraStartCaptureRequest, + @JvmField val guider: Guider? = null, + @JvmField val liveStackerManager: CameraLiveStackingManager? = null, + @JvmField val mount: Mount? = null, + @JvmField val wheel: FilterWheel? = null, + @JvmField val focuser: Focuser? = null, + @JvmField val rotator: Rotator? = null, +) : AbstractJob(), CameraEventAware, WheelEventAware { - override val name = "${task.camera.name} Camera Capture Job" + private val delayTask = DelayTask(this, request.exposureDelay) + private val waitForSettleTask = WaitForSettleTask(this, guider) + private val delayAndWaitForSettleSplitTask = SplitTask(listOf(delayTask, waitForSettleTask), cameraCaptureExecutor) + private val cameraExposureTask = CameraExposureTask(this, camera, request) + private val ditherAfterExposureTask = DitherAfterExposureTask(this, guider, request.dither) + private val shutterWheelMoveTask = if (wheel != null && request.shutterPosition > 0) WheelMoveTask(this, wheel, request.shutterPosition) else null + + @JvmField val status = CameraCaptureEvent(camera, exposureAmount = request.exposureAmount) + + init { + shutterWheelMoveTask?.also(::add) + add(delayAndWaitForSettleSplitTask) + add(cameraExposureTask) + add(ditherAfterExposureTask) + } override fun handleCameraEvent(event: CameraEvent) { - task.handleCameraEvent(event) + cameraExposureTask.handleCameraEvent(event) } override fun handleFilterWheelEvent(event: FilterWheelEvent) { - task.handleFilterWheelEvent(event) + shutterWheelMoveTask?.handleFilterWheelEvent(event) + } + + override fun onPause(paused: Boolean) { + if (paused) { + status.state = CameraCaptureState.PAUSING + status.send() + } + + super.onPause(paused) + } + + override fun beforePause(task: Task) { + status.state = CameraCaptureState.PAUSED + status.send() + } + + override fun beforeStart() { + LOG.debug { "Camera Capture started. request=$request, camera=$camera, mount=$mount, wheel=$wheel, focuser=$focuser" } + + camera.snoop(listOf(mount, wheel, focuser, rotator)) + + val estimatedCaptureTime = + (request.exposureTime.toNanos() * request.exposureAmount + request.exposureDelay.toNanos() * (request.exposureAmount - 1)) / 1000L + + status.handleCameraCaptureStarted(estimatedCaptureTime) + status.send() + } + + override fun afterFinish() { + status.handleCameraCaptureFinished() + status.send() + + liveStackerManager?.stop(request) + + LOG.debug { "Camera Capture finished. request=$request, status=$status, mount=$mount, wheel=$wheel, focuser=$focuser" } + } + + override fun isLoop(): Boolean { + return request.isLoop || status.exposureCount < request.exposureAmount + } + + override fun canRun(prev: Task?, current: Task): Boolean { + if (current === ditherAfterExposureTask) { + return !isCancelled && guider != null + && status.exposureCount >= 1 && request.dither.afterExposures > 0 + && status.exposureCount % request.dither.afterExposures == 0 + } else if (current === delayAndWaitForSettleSplitTask) { + return status.exposureCount > 0 + } else if (current === shutterWheelMoveTask) { + return request.frameType == FrameType.DARK + } + + return super.canRun(prev, current) + } + + override fun accept(event: Any) { + val pausing = status.state == CameraCaptureState.PAUSING + + when (event) { + is DelayEvent -> { + if (event.task === delayTask) { + status.handleCameraDelayEvent(event) + if (pausing) status.state = CameraCaptureState.PAUSING + status.send() + } + } + is CameraExposureEvent -> { + status.handleCameraExposureEvent(event) + + if (event is CameraExposureFinished) { + status.liveStackedPath = addFrameToLiveStacker(status.savedPath) + + if (pausing) { + status.copy().send() + } + } + + if (pausing) { + status.state = CameraCaptureState.PAUSING + } + + status.send() + } + is DitherAfterExposureEvent -> { + status.state = CameraCaptureState.DITHERING + status.send() + } + } + } + + private fun addFrameToLiveStacker(path: Path?): Path? { + return if (path != null && liveStackerManager?.start(request, path) == true) { + try { + status.state = CameraCaptureState.STACKING + status.send() + + liveStackerManager.stack(request, path) + } catch (_: Throwable) { + null + } + } else { + null + } + } + + @Suppress("NOTHING_TO_INLINE") + private inline fun CameraCaptureEvent.send() { + cameraCaptureExecutor.accept(this) + } + + companion object { + + @JvmStatic private val LOG = loggerFor() } } diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureTask.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureTask.kt deleted file mode 100644 index 3b627a722..000000000 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureTask.kt +++ /dev/null @@ -1,319 +0,0 @@ -package nebulosa.api.cameras - -import com.fasterxml.jackson.annotation.JsonIgnore -import io.reactivex.rxjava3.functions.Consumer -import nebulosa.api.guiding.DitherAfterExposureEvent -import nebulosa.api.guiding.DitherAfterExposureTask -import nebulosa.api.guiding.WaitForSettleTask -import nebulosa.api.tasks.AbstractTask -import nebulosa.api.tasks.SplitTask -import nebulosa.api.tasks.delay.DelayEvent -import nebulosa.api.tasks.delay.DelayTask -import nebulosa.api.wheels.WheelEventAware -import nebulosa.api.wheels.WheelMoveTask -import nebulosa.guiding.Guider -import nebulosa.indi.device.camera.Camera -import nebulosa.indi.device.camera.CameraEvent -import nebulosa.indi.device.camera.FrameType -import nebulosa.indi.device.filterwheel.FilterWheel -import nebulosa.indi.device.filterwheel.FilterWheelEvent -import nebulosa.indi.device.focuser.Focuser -import nebulosa.indi.device.mount.Mount -import nebulosa.indi.device.rotator.Rotator -import nebulosa.log.loggerFor -import nebulosa.util.concurrency.cancellation.CancellationToken -import nebulosa.util.concurrency.latch.PauseListener -import java.nio.file.Path -import java.time.Duration -import java.util.concurrent.Executor -import java.util.concurrent.atomic.AtomicBoolean - -data class CameraCaptureTask( - @JvmField val camera: Camera, - @JvmField val request: CameraStartCaptureRequest, - @JvmField val guider: Guider? = null, - private val useFirstExposure: Boolean = false, - private val executor: Executor? = null, - private val liveStackerManager: CameraLiveStackingManager? = null, - @JvmField val mount: Mount? = null, - @JvmField val wheel: FilterWheel? = null, - @JvmField val focuser: Focuser? = null, - @JvmField val rotator: Rotator? = null, -) : AbstractTask(), Consumer, PauseListener, CameraEventAware, WheelEventAware { - - private val delayTask = DelayTask(request.exposureDelay) - private val waitForSettleTask = WaitForSettleTask(guider) - private val delayAndWaitForSettleSplitTask = SplitTask(listOf(delayTask, waitForSettleTask), executor) - private val cameraExposureTask = CameraExposureTask(camera, request) - private val ditherAfterExposureTask = DitherAfterExposureTask(guider, request.dither) - private val shutterWheelMoveTask = if (wheel != null && request.shutterPosition > 0) WheelMoveTask(wheel, request.shutterPosition) else null - - @Volatile private var exposureCount = 0 - @Volatile private var captureRemainingTime = Duration.ZERO - @Volatile private var prevCaptureElapsedTime = Duration.ZERO - @Volatile private var captureElapsedTime = Duration.ZERO - @Volatile private var captureProgress = 0.0 - @Volatile private var stepRemainingTime = Duration.ZERO - @Volatile private var stepElapsedTime = Duration.ZERO - @Volatile private var stepProgress = 0.0 - @Volatile private var savedPath: Path? = null - @Volatile private var liveStackedPath: Path? = null - - @JvmField @JsonIgnore val estimatedCaptureTime: Duration = if (request.isLoop) Duration.ZERO - else Duration.ofNanos(request.exposureTime.toNanos() * request.exposureAmount + request.exposureDelay.toNanos() * (request.exposureAmount - if (useFirstExposure) 0 else 1)) - - @Volatile private var exposureRepeatCount = 0 - - private val pausing = AtomicBoolean() - - init { - delayTask.subscribe(this) - cameraExposureTask.subscribe(this) - - if (guider != null) { - // waitForSettleTask.subscribe(this) - ditherAfterExposureTask.subscribe(this) - } - } - - override fun handleCameraEvent(event: CameraEvent) { - cameraExposureTask.handleCameraEvent(event) - } - - override fun handleFilterWheelEvent(event: FilterWheelEvent) { - shutterWheelMoveTask?.handleFilterWheelEvent(event) - } - - override fun onPause(paused: Boolean) { - pausing.set(paused) - - if (paused) { - sendEvent(CameraCaptureState.PAUSING) - } - } - - fun initialize( - cancellationToken: CancellationToken, - hasShutter: Boolean = false, snoopDevices: Boolean = true, - ) { - LOG.info( - "Camera Capture started. request={}, exposureCount={}, camera={}, mount={}, wheel={}, focuser={}", request, exposureCount, - camera, mount, wheel, focuser - ) - - cameraExposureTask.reset() - - pausing.set(false) - cancellationToken.listenToPause(this) - - if (snoopDevices) { - camera.snoop(listOf(mount, wheel, focuser, rotator)) - } - - if (hasShutter && shutterWheelMoveTask != null && request.frameType == FrameType.DARK) { - shutterWheelMoveTask.execute(cancellationToken) - } - } - - fun finalize(cancellationToken: CancellationToken) { - pausing.set(false) - cancellationToken.unlistenToPause(this) - - sendEvent(CameraCaptureState.CAPTURE_FINISHED) - - liveStackerManager?.stop(request) - - LOG.info("Camera Capture finished. camera={}, request={}, exposureCount={}", camera, request, exposureCount) - } - - override fun execute(cancellationToken: CancellationToken) { - try { - initialize(cancellationToken, hasShutter = true) - executeInLoop(cancellationToken) - } finally { - finalize(cancellationToken) - } - } - - fun executeUntil(cancellationToken: CancellationToken, count: Int) { - exposureRepeatCount = 0 - - while (!cancellationToken.isCancelled && !cameraExposureTask.isAborted && exposureRepeatCount < count) { - executeOnce(cancellationToken) - } - } - - fun executeInLoop(cancellationToken: CancellationToken) { - exposureCount = 0 - - while (!cancellationToken.isCancelled && !cameraExposureTask.isAborted && (request.isLoop || exposureCount < request.exposureAmount)) { - executeOnce(cancellationToken) - } - } - - fun executeOnce(cancellationToken: CancellationToken) { - if (cancellationToken.isPaused) { - pausing.set(false) - sendEvent(CameraCaptureState.PAUSED) - cancellationToken.waitForPause() - } - - if (exposureCount == 0) { - sendEvent(CameraCaptureState.CAPTURE_STARTED) - - if (guider != null) { - if (useFirstExposure) { - // DELAY & WAIT FOR SETTLE. - delayAndWaitForSettleSplitTask.execute(cancellationToken) - } else { - // WAIT FOR SETTLE. - waitForSettleTask.execute(cancellationToken) - } - } else if (useFirstExposure) { - // DELAY. - delayTask.execute(cancellationToken) - } - } else if (guider != null) { - // DELAY & WAIT FOR SETTLE. - delayAndWaitForSettleSplitTask.execute(cancellationToken) - } else { - // DELAY. - delayTask.execute(cancellationToken) - } - - // CAPTURE. - cameraExposureTask.execute(cancellationToken) - - // DITHER. - if (!cancellationToken.isCancelled && !cameraExposureTask.isAborted && guider != null - && exposureCount >= 1 && request.dither.afterExposures > 0 && exposureCount % request.dither.afterExposures == 0 - ) { - ditherAfterExposureTask.execute(cancellationToken) - } - - if (cancellationToken.isPaused) { - pausing.set(false) - sendEvent(CameraCaptureState.PAUSED) - } - } - - @Synchronized - override fun accept(event: Any) { - val state = when (event) { - is DelayEvent -> { - captureElapsedTime += event.waitTime - stepElapsedTime = event.task.duration - event.remainingTime - stepRemainingTime = event.remainingTime - stepProgress = event.progress - CameraCaptureState.WAITING - } - is CameraExposureEvent -> { - when (event.state) { - CameraExposureState.STARTED -> { - prevCaptureElapsedTime = captureElapsedTime - exposureCount++ - exposureRepeatCount++ - CameraCaptureState.EXPOSURE_STARTED - } - CameraExposureState.ELAPSED -> { - captureElapsedTime = prevCaptureElapsedTime + event.elapsedTime - stepElapsedTime = event.elapsedTime - stepRemainingTime = event.remainingTime - stepProgress = event.progress - CameraCaptureState.EXPOSURING - } - CameraExposureState.FINISHED -> { - captureElapsedTime = prevCaptureElapsedTime + request.exposureTime - savedPath = event.savedPath - liveStackedPath = addFrameToLiveStacker(savedPath) - CameraCaptureState.EXPOSURE_FINISHED - } - CameraExposureState.IDLE -> { - CameraCaptureState.CAPTURE_FINISHED - } - } - } - is DitherAfterExposureEvent -> { - CameraCaptureState.DITHERING - } - else -> { - return LOG.warn("unknown event: {}", event) - } - } - - sendEvent(state) - } - - private fun sendEvent(state: CameraCaptureState) { - if (state != CameraCaptureState.IDLE && !request.isLoop) { - captureRemainingTime = if (estimatedCaptureTime > captureElapsedTime) estimatedCaptureTime - captureElapsedTime else Duration.ZERO - captureProgress = (estimatedCaptureTime - captureRemainingTime).toNanos().toDouble() / estimatedCaptureTime.toNanos() - } - - val isExposureFinished = state == CameraCaptureState.EXPOSURE_FINISHED - - val event = CameraCaptureEvent( - this, camera, if (pausing.get() && !isExposureFinished) CameraCaptureState.PAUSING else state, - request.exposureAmount, exposureCount, - captureRemainingTime, captureElapsedTime, captureProgress, - stepRemainingTime, stepElapsedTime, stepProgress, - savedPath, liveStackedPath, - if (isExposureFinished) request else null - ) - - onNext(event) - } - - private fun addFrameToLiveStacker(path: Path?): Path? { - if (liveStackerManager == null) return null - - return if (path != null && liveStackerManager.start(request, path)) { - sendEvent(CameraCaptureState.STACKING) - - try { - liveStackerManager.stack(request, path) - } catch (_: Throwable) { - null - } finally { - sendEvent(CameraCaptureState.WAITING) - } - } else { - null - } - } - - override fun close() { - delayTask.close() - waitForSettleTask.close() - delayAndWaitForSettleSplitTask.close() - cameraExposureTask.close() - ditherAfterExposureTask.close() - liveStackerManager?.stop(request) - super.close() - } - - override fun reset() { - exposureCount = 0 - captureRemainingTime = Duration.ZERO - prevCaptureElapsedTime = Duration.ZERO - captureElapsedTime = Duration.ZERO - captureProgress = 0.0 - stepRemainingTime = Duration.ZERO - stepElapsedTime = Duration.ZERO - stepProgress = 0.0 - savedPath = null - liveStackedPath = null - - delayTask.reset() - cameraExposureTask.reset() - ditherAfterExposureTask.reset() - - pausing.set(false) - exposureRepeatCount = 0 - } - - companion object { - - @JvmStatic private val LOG = loggerFor() - } -} diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureElapsed.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureElapsed.kt new file mode 100644 index 000000000..0eaef0149 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureElapsed.kt @@ -0,0 +1,14 @@ +package nebulosa.api.cameras + +import nebulosa.job.manager.Job + +data class CameraExposureElapsed( + override val job: Job, + override val task: CameraExposureTask, + override val elapsedTime: Long, + override val remainingTime: Long, + override val progress: Double, +) : CameraExposureEvent { + + override val savedPath = null +} diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureEvent.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureEvent.kt index 8d721dafb..b7f9a264c 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureEvent.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureEvent.kt @@ -1,13 +1,9 @@ package nebulosa.api.cameras +import nebulosa.job.manager.TimedTaskEvent import java.nio.file.Path -import java.time.Duration -data class CameraExposureEvent( - @JvmField val task: CameraExposureTask, - @JvmField val state: CameraExposureState = CameraExposureState.IDLE, - @JvmField val elapsedTime: Duration = Duration.ZERO, - @JvmField val remainingTime: Duration = Duration.ZERO, - @JvmField val progress: Double = 0.0, - @JvmField val savedPath: Path? = null, -) +sealed interface CameraExposureEvent : TimedTaskEvent { + + val savedPath: Path? +} diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureFailed.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureFailed.kt new file mode 100644 index 000000000..b9b9e1459 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureFailed.kt @@ -0,0 +1,14 @@ +package nebulosa.api.cameras + +import nebulosa.job.manager.Job + +data class CameraExposureFailed( + override val job: Job, + override val task: CameraExposureTask, +) : CameraExposureEvent { + + override val elapsedTime = task.exposureTimeInMicroseconds + override val remainingTime = 0L + override val progress = 1.0 + override val savedPath = null +} diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureFinished.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureFinished.kt new file mode 100644 index 000000000..3232f6039 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureFinished.kt @@ -0,0 +1,15 @@ +package nebulosa.api.cameras + +import nebulosa.job.manager.Job +import java.nio.file.Path + +data class CameraExposureFinished( + override val job: Job, + override val task: CameraExposureTask, + override val savedPath: Path, +) : CameraExposureEvent { + + override val elapsedTime = task.exposureTimeInMicroseconds + override val remainingTime = 0L + override val progress = 1.0 +} diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureStarted.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureStarted.kt new file mode 100644 index 000000000..093393b24 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureStarted.kt @@ -0,0 +1,11 @@ +package nebulosa.api.cameras + +import nebulosa.job.manager.Job + +data class CameraExposureStarted(override val job: Job, override val task: CameraExposureTask) : CameraExposureEvent { + + override val elapsedTime = 0L + override val remainingTime = task.exposureTimeInMicroseconds + override val progress = 0.0 + override val savedPath = null +} diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureState.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureState.kt deleted file mode 100644 index 3c71ba3ea..000000000 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureState.kt +++ /dev/null @@ -1,8 +0,0 @@ -package nebulosa.api.cameras - -enum class CameraExposureState { - IDLE, - STARTED, - ELAPSED, - FINISHED, -} diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureTask.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureTask.kt index e295da42e..2b6a741a1 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureTask.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureTask.kt @@ -1,44 +1,35 @@ package nebulosa.api.cameras -import nebulosa.api.tasks.AbstractTask import nebulosa.fits.fits import nebulosa.image.format.ReadableHeader import nebulosa.indi.device.camera.* import nebulosa.io.transferAndClose +import nebulosa.job.manager.Job +import nebulosa.job.manager.Task import nebulosa.log.loggerFor -import nebulosa.util.concurrency.cancellation.CancellationListener import nebulosa.util.concurrency.cancellation.CancellationSource -import nebulosa.util.concurrency.cancellation.CancellationToken import nebulosa.util.concurrency.latch.CountUpDownLatch import okio.sink import java.nio.file.Files import java.nio.file.Path -import java.time.Duration import java.time.LocalDateTime -import java.util.concurrent.atomic.AtomicBoolean import kotlin.io.path.createParentDirectories import kotlin.io.path.deleteIfExists import kotlin.io.path.moveTo import kotlin.io.path.outputStream +import kotlin.math.min data class CameraExposureTask( + @JvmField val job: Job, @JvmField val camera: Camera, @JvmField val request: CameraStartCaptureRequest, -) : AbstractTask(), CancellationListener, CameraEventAware { +) : Task, CameraEventAware { private val latch = CountUpDownLatch() - private val aborted = AtomicBoolean() - - @Volatile private var elapsedTime = Duration.ZERO - @Volatile private var remainingTime = Duration.ZERO - @Volatile private var progress = 0.0 - @Volatile private var savedPath: Path? = null - private val outputPath = Files.createTempFile(camera.name, ".fits") private val formatter = CameraCaptureNamingFormatter(camera) - val isAborted - get() = aborted.get() + @JvmField val exposureTimeInMicroseconds = request.exposureTime.toNanos() / 1000L override fun handleCameraEvent(event: CameraEvent) { if (event.device === camera) { @@ -47,32 +38,29 @@ data class CameraExposureTask( save(event) } is CameraExposureAborted, - is CameraExposureFailed, + is nebulosa.indi.device.camera.CameraExposureFailed, is CameraDetached -> { - aborted.set(true) latch.reset() + job.accept(CameraExposureFailed(job, this)) } is CameraExposureProgressChanged -> { - val exposureTime = request.exposureTime - // minOf fix possible bug on SVBony exposure time? - remainingTime = minOf(event.device.exposureTime, request.exposureTime) - elapsedTime = exposureTime - remainingTime - progress = elapsedTime.toNanos().toDouble() / exposureTime.toNanos() - sendEvent(CameraExposureState.ELAPSED) + // "min" fix possible bug on SVBony exposure time? + val remainingTime = min(event.device.exposureTime, request.exposureTime.toNanos() / 1000L) + val elapsedTime = exposureTimeInMicroseconds - remainingTime + val progress = elapsedTime.toDouble() / exposureTimeInMicroseconds + job.accept(CameraExposureElapsed(job, this@CameraExposureTask, elapsedTime, remainingTime, progress)) } } } } - override fun execute(cancellationToken: CancellationToken) { - if (camera.connected && !aborted.get()) { - LOG.info("Camera Exposure started. camera={}, request={}", camera, request) - - cancellationToken.waitForPause() + override fun run() { + if (camera.connected) { + LOG.debug("Camera Exposure started. camera={}, request={}", camera, request) latch.countUp() - sendEvent(CameraExposureState.STARTED) + job.accept(CameraExposureStarted(job, this)) with(camera) { enableBlob() @@ -86,37 +74,23 @@ data class CameraExposureTask( bin(request.binX, request.binY) gain(request.gain) offset(request.offset) - startCapture(request.exposureTime) + startCapture(request.exposureTime.toNanos() / 1000L) } - try { - cancellationToken.listen(this) - latch.await() - } finally { - cancellationToken.unlisten(this) - } + latch.await() - LOG.info("Camera Exposure finished. aborted={}, camera={}, request={}", aborted.get(), camera, request) + LOG.debug("Camera Exposure finished. camera={}, request={}", camera, request) } else { - LOG.warn("camera not connected or aborted. aborted={}, camera={}, request={}", aborted.get(), camera, request) + LOG.warn("camera not connected. camera={}, request={}", camera, request) } + + outputPath.deleteIfExists() } override fun onCancel(source: CancellationSource) { camera.abortCapture() } - override fun reset() { - aborted.set(false) - latch.reset() - } - - override fun close() { - onCancel(CancellationSource.Close) - outputPath.deleteIfExists() - super.close() - } - private fun save(event: CameraFrameCaptured) { try { val header = if (event.stream != null) { @@ -134,22 +108,15 @@ data class CameraExposureTask( LOG.info("saving FITS image at {}", this) createParentDirectories() outputPath.moveTo(this, true) - savedPath = this + job.accept(CameraExposureFinished(job, this@CameraExposureTask, this)) } - - sendEvent(CameraExposureState.FINISHED) } catch (e: Throwable) { LOG.error("failed to save FITS image", e) - aborted.set(true) } finally { latch.countDown() } } - private fun sendEvent(state: CameraExposureState) { - onNext(CameraExposureEvent(this, state, elapsedTime, remainingTime, progress, savedPath)) - } - private fun CameraStartCaptureRequest.makeSavePath( autoSave: Boolean = this.autoSave, header: ReadableHeader? = null, diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraLiveStackingManager.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraLiveStackingManager.kt index 697fc3d12..73e22019a 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraLiveStackingManager.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraLiveStackingManager.kt @@ -3,16 +3,7 @@ package nebulosa.api.cameras import nebulosa.api.calibration.CalibrationFrameProvider import nebulosa.api.livestacker.LiveStackingRequest import nebulosa.api.stacker.StackerGroupType -import nebulosa.fits.binX -import nebulosa.fits.binY -import nebulosa.fits.exposureTimeInMicroseconds -import nebulosa.fits.filter -import nebulosa.fits.fits -import nebulosa.fits.gain -import nebulosa.fits.height -import nebulosa.fits.isFits -import nebulosa.fits.temperature -import nebulosa.fits.width +import nebulosa.fits.* import nebulosa.image.format.ImageHdu import nebulosa.livestacker.LiveStacker import nebulosa.log.loggerFor @@ -20,13 +11,8 @@ import nebulosa.xisf.isXisf import nebulosa.xisf.xisf import java.nio.file.Files import java.nio.file.Path -import java.util.EnumMap -import kotlin.io.path.copyTo -import kotlin.io.path.deleteIfExists -import kotlin.io.path.deleteRecursively -import kotlin.io.path.exists -import kotlin.io.path.extension -import kotlin.io.path.isRegularFile +import java.util.* +import kotlin.io.path.* data class CameraLiveStackingManager( private val calibrationFrameProvider: CalibrationFrameProvider? = null, @@ -121,7 +107,7 @@ data class CameraLiveStackingManager( else if (path.isXisf()) path.xisf() else return this - val hdu = image.use { it.firstOrNull { it is ImageHdu } } ?: return this + val hdu = image.use { it.firstOrNull { h -> h is ImageHdu } } ?: return this val header = hdu.header val temperature = header.temperature val binX = header.binX diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraSerializer.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraSerializer.kt index f7aa10c9a..3f94ad139 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraSerializer.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraSerializer.kt @@ -22,10 +22,10 @@ class CameraSerializer(private val capturesPath: Path) : DeviceSerializer 0 && position <= focuser.maxPosition) { lastDirection = determineMovingDirection(focuser.position, position) task.position = position - task.execute(cancellationToken) + task.run() } } - 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 diff --git a/api/src/main/kotlin/nebulosa/api/focusers/FocuserMoveAbsoluteTask.kt b/api/src/main/kotlin/nebulosa/api/focusers/FocuserMoveAbsoluteTask.kt index 85379b053..91692bf03 100644 --- a/api/src/main/kotlin/nebulosa/api/focusers/FocuserMoveAbsoluteTask.kt +++ b/api/src/main/kotlin/nebulosa/api/focusers/FocuserMoveAbsoluteTask.kt @@ -1,9 +1,11 @@ package nebulosa.api.focusers import nebulosa.indi.device.focuser.Focuser +import nebulosa.job.manager.Job import kotlin.math.abs data class FocuserMoveAbsoluteTask( + override val job: Job, override val focuser: Focuser, @JvmField @Volatile var position: Int, ) : AbstractFocuserMoveTask() { diff --git a/api/src/main/kotlin/nebulosa/api/focusers/FocuserMoveRelativeTask.kt b/api/src/main/kotlin/nebulosa/api/focusers/FocuserMoveRelativeTask.kt index ec4671b3a..9d550f1f1 100644 --- a/api/src/main/kotlin/nebulosa/api/focusers/FocuserMoveRelativeTask.kt +++ b/api/src/main/kotlin/nebulosa/api/focusers/FocuserMoveRelativeTask.kt @@ -1,9 +1,11 @@ package nebulosa.api.focusers import nebulosa.indi.device.focuser.Focuser +import nebulosa.job.manager.Job import kotlin.math.abs data class FocuserMoveRelativeTask( + override val job: Job, override val focuser: Focuser, @JvmField val offset: Int, ) : AbstractFocuserMoveTask() { diff --git a/api/src/main/kotlin/nebulosa/api/focusers/FocuserMoveTask.kt b/api/src/main/kotlin/nebulosa/api/focusers/FocuserTask.kt similarity index 53% rename from api/src/main/kotlin/nebulosa/api/focusers/FocuserMoveTask.kt rename to api/src/main/kotlin/nebulosa/api/focusers/FocuserTask.kt index 729ca77af..d5fcd7804 100644 --- a/api/src/main/kotlin/nebulosa/api/focusers/FocuserMoveTask.kt +++ b/api/src/main/kotlin/nebulosa/api/focusers/FocuserTask.kt @@ -1,9 +1,9 @@ package nebulosa.api.focusers -import nebulosa.api.tasks.Task import nebulosa.indi.device.focuser.Focuser +import nebulosa.job.manager.Task -interface FocuserMoveTask : Task, FocuserEventAware { +sealed interface FocuserTask : Task, FocuserEventAware { val focuser: Focuser } diff --git a/api/src/main/kotlin/nebulosa/api/guiding/DitherAfterExposureDithered.kt b/api/src/main/kotlin/nebulosa/api/guiding/DitherAfterExposureDithered.kt new file mode 100644 index 000000000..a50318272 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/guiding/DitherAfterExposureDithered.kt @@ -0,0 +1,10 @@ +package nebulosa.api.guiding + +import nebulosa.job.manager.Job + +data class DitherAfterExposureDithered( + override val job: Job, + override val task: DitherAfterExposureTask, + override val dx: Double, + override val dy: Double, +) : DitherAfterExposureEvent diff --git a/api/src/main/kotlin/nebulosa/api/guiding/DitherAfterExposureEvent.kt b/api/src/main/kotlin/nebulosa/api/guiding/DitherAfterExposureEvent.kt index 34fbd6056..7633362e3 100644 --- a/api/src/main/kotlin/nebulosa/api/guiding/DitherAfterExposureEvent.kt +++ b/api/src/main/kotlin/nebulosa/api/guiding/DitherAfterExposureEvent.kt @@ -1,10 +1,12 @@ package nebulosa.api.guiding -import java.time.Duration - -data class DitherAfterExposureEvent( - @JvmField val task: DitherAfterExposureTask, - @JvmField val state: DitherAfterExposureState = DitherAfterExposureState.IDLE, - @JvmField val dx: Double = 0.0, @JvmField val dy: Double = 0.0, - @JvmField val elapsedTime: Duration = Duration.ZERO, -) +import nebulosa.job.manager.TaskEvent + +sealed interface DitherAfterExposureEvent : TaskEvent { + + override val task: DitherAfterExposureTask + + val dx: Double + + val dy: Double +} diff --git a/api/src/main/kotlin/nebulosa/api/guiding/DitherAfterExposureState.kt b/api/src/main/kotlin/nebulosa/api/guiding/DitherAfterExposureState.kt deleted file mode 100644 index 8c2eebe21..000000000 --- a/api/src/main/kotlin/nebulosa/api/guiding/DitherAfterExposureState.kt +++ /dev/null @@ -1,8 +0,0 @@ -package nebulosa.api.guiding - -enum class DitherAfterExposureState { - IDLE, - STARTED, - DITHERED, - FINISHED, -} diff --git a/api/src/main/kotlin/nebulosa/api/guiding/DitherAfterExposureTask.kt b/api/src/main/kotlin/nebulosa/api/guiding/DitherAfterExposureTask.kt index a4aec197f..5b30f58f1 100644 --- a/api/src/main/kotlin/nebulosa/api/guiding/DitherAfterExposureTask.kt +++ b/api/src/main/kotlin/nebulosa/api/guiding/DitherAfterExposureTask.kt @@ -1,63 +1,44 @@ package nebulosa.api.guiding -import nebulosa.api.tasks.AbstractTask import nebulosa.guiding.GuideState import nebulosa.guiding.Guider import nebulosa.guiding.GuiderListener +import nebulosa.job.manager.Job +import nebulosa.job.manager.Task +import nebulosa.log.debug import nebulosa.log.loggerFor -import nebulosa.util.concurrency.cancellation.CancellationListener +import nebulosa.util.Resettable import nebulosa.util.concurrency.cancellation.CancellationSource -import nebulosa.util.concurrency.cancellation.CancellationToken import nebulosa.util.concurrency.latch.CountUpDownLatch -import java.time.Duration -import kotlin.system.measureTimeMillis data class DitherAfterExposureTask( + @JvmField val job: Job, @JvmField val guider: Guider?, @JvmField val request: DitherAfterExposureRequest, -) : AbstractTask(), GuiderListener, CancellationListener { +) : Task, GuiderListener, Resettable { private val ditherLatch = CountUpDownLatch() - @Volatile private var dx = 0.0 - @Volatile private var dy = 0.0 - @Volatile private var elapsedTime = Duration.ZERO - - override fun execute(cancellationToken: CancellationToken) { + override fun run() { if (guider != null && guider.canDither && request.enabled && guider.state == GuideState.GUIDING - && !cancellationToken.isCancelled + && !job.isCancelled ) { - LOG.info("Dither started. request={}", request) - - try { - cancellationToken.listen(this) - guider.registerGuiderListener(this) - ditherLatch.countUp() - - sendEvent(DitherAfterExposureState.STARTED) - - elapsedTime = Duration.ofMillis(measureTimeMillis { - guider.dither(request.amount, request.raOnly) - ditherLatch.await() - }) - } finally { - sendEvent(DitherAfterExposureState.FINISHED) + LOG.debug { "Dither started. request=$request" } - guider.unregisterGuiderListener(this) - cancellationToken.unlisten(this) + guider.registerGuiderListener(this) + ditherLatch.countUp() + guider.dither(request.amount, request.raOnly) + ditherLatch.await() + guider.unregisterGuiderListener(this) - LOG.info("Dither finished. elapsedTime={}, request={}", elapsedTime, request) - } + LOG.debug { "Dither finished. request=$request" } } } override fun onDithered(dx: Double, dy: Double) { - this.dx = dx - this.dy = dy - - sendEvent(DitherAfterExposureState.DITHERED) - LOG.info("dithered. dx={}, dy={}", dx, dy) + job.accept(DitherAfterExposureDithered(job, this, dx, dy)) + LOG.debug { "dithered. dx=$dx, dy=$dy" } ditherLatch.reset() } @@ -66,13 +47,7 @@ data class DitherAfterExposureTask( } override fun reset() { - dx = 0.0 - dy = 0.0 - elapsedTime = Duration.ZERO - } - - private fun sendEvent(state: DitherAfterExposureState) { - onNext(DitherAfterExposureEvent(this, state, dx, dy, elapsedTime)) + ditherLatch.reset() } companion object { diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseEvent.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseEvent.kt deleted file mode 100644 index c15be48dc..000000000 --- a/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseEvent.kt +++ /dev/null @@ -1,9 +0,0 @@ -package nebulosa.api.guiding - -import java.time.Duration - -data class GuidePulseEvent( - @JvmField val task: GuidePulseTask, - @JvmField val remainingTime: Duration = Duration.ZERO, - @JvmField val progress: Double = 0.0, -) diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseTask.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseTask.kt index 105ac4936..d119fbac7 100644 --- a/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseTask.kt +++ b/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseTask.kt @@ -1,54 +1,41 @@ package nebulosa.api.guiding -import io.reactivex.rxjava3.functions.Consumer -import nebulosa.api.tasks.AbstractTask -import nebulosa.api.tasks.delay.DelayEvent -import nebulosa.api.tasks.delay.DelayTask import nebulosa.guiding.GuideDirection import nebulosa.indi.device.guider.GuideOutput +import nebulosa.job.manager.Job +import nebulosa.job.manager.Task +import nebulosa.job.manager.delay.DelayTask +import nebulosa.log.debug import nebulosa.log.loggerFor -import nebulosa.util.concurrency.cancellation.CancellationListener import nebulosa.util.concurrency.cancellation.CancellationSource -import nebulosa.util.concurrency.cancellation.CancellationToken import java.time.Duration data class GuidePulseTask( + @JvmField val job: Job, @JvmField val guideOutput: GuideOutput, @JvmField val request: GuidePulseRequest, -) : AbstractTask(), CancellationListener, Consumer { +) : Task { - private val delayTask = DelayTask(request.duration) + private val direction = request.direction + private val duration = request.duration.toMillis() - init { - delayTask.subscribe(this) - } - - override fun execute(cancellationToken: CancellationToken) { - if (!cancellationToken.isCancelled && guideOutput.pulseGuide(request.duration, request.direction)) { - LOG.info("Guide Pulse started. guideOutput={}, duration={}, direction={}", guideOutput, request.duration.toMillis(), request.direction) - - try { - cancellationToken.listen(this) - delayTask.execute(cancellationToken) - } finally { - cancellationToken.unlisten(this) - } + @JvmField val delayTask = DelayTask(job, duration) - LOG.info("Guide Pulse finished. guideOutput={}, duration={}, direction={}", guideOutput, request.duration.toMillis(), request.direction) + override fun run() { + if (!job.isCancelled && guideOutput.pulseGuide(request.duration, request.direction)) { + LOG.debug { "Guide Pulse started. guideOutput=$guideOutput, duration=$duration ms, direction=$direction" } + delayTask.run() + LOG.debug { "Guide Pulse finished. guideOutput=$guideOutput, duration=$duration ms, direction=$direction" } } } override fun onCancel(source: CancellationSource) { + delayTask.onCancel(source) guideOutput.pulseGuide(Duration.ZERO, request.direction) } - override fun accept(event: DelayEvent) { - onNext(GuidePulseEvent(this, event.remainingTime, event.progress)) - } - - override fun close() { - delayTask.close() - super.close() + override fun onPause(paused: Boolean) { + delayTask.onPause(paused) } companion object { diff --git a/api/src/main/kotlin/nebulosa/api/guiding/WaitForSettleEvent.kt b/api/src/main/kotlin/nebulosa/api/guiding/WaitForSettleEvent.kt new file mode 100644 index 000000000..ec161a99b --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/guiding/WaitForSettleEvent.kt @@ -0,0 +1,5 @@ +package nebulosa.api.guiding + +import nebulosa.job.manager.TaskEvent + +sealed interface WaitForSettleEvent : TaskEvent diff --git a/api/src/main/kotlin/nebulosa/api/guiding/WaitForSettleFinished.kt b/api/src/main/kotlin/nebulosa/api/guiding/WaitForSettleFinished.kt new file mode 100644 index 000000000..7f4a958f4 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/guiding/WaitForSettleFinished.kt @@ -0,0 +1,5 @@ +package nebulosa.api.guiding + +import nebulosa.job.manager.Job + +data class WaitForSettleFinished(override val job: Job, override val task: WaitForSettleTask) : WaitForSettleEvent diff --git a/api/src/main/kotlin/nebulosa/api/guiding/WaitForSettleStarted.kt b/api/src/main/kotlin/nebulosa/api/guiding/WaitForSettleStarted.kt new file mode 100644 index 000000000..cdd3a1e57 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/guiding/WaitForSettleStarted.kt @@ -0,0 +1,5 @@ +package nebulosa.api.guiding + +import nebulosa.job.manager.Job + +data class WaitForSettleStarted(override val job: Job, override val task: WaitForSettleTask) : WaitForSettleEvent diff --git a/api/src/main/kotlin/nebulosa/api/guiding/WaitForSettleTask.kt b/api/src/main/kotlin/nebulosa/api/guiding/WaitForSettleTask.kt index 69db3628d..b149fc457 100644 --- a/api/src/main/kotlin/nebulosa/api/guiding/WaitForSettleTask.kt +++ b/api/src/main/kotlin/nebulosa/api/guiding/WaitForSettleTask.kt @@ -1,19 +1,23 @@ package nebulosa.api.guiding -import nebulosa.api.tasks.Task import nebulosa.guiding.Guider +import nebulosa.job.manager.Job +import nebulosa.job.manager.Task +import nebulosa.log.debug import nebulosa.log.loggerFor -import nebulosa.util.concurrency.cancellation.CancellationToken data class WaitForSettleTask( + @JvmField val job: Job, @JvmField val guider: Guider?, ) : Task { - override fun execute(cancellationToken: CancellationToken) { - if (guider != null && guider.isSettling && !cancellationToken.isCancelled) { - LOG.info("Wait For Settle started") - guider.waitForSettle(cancellationToken) - LOG.info("Wait For Settle finished") + override fun run() { + if (guider != null && guider.isSettling && !job.isCancelled) { + LOG.debug { "Wait For Settle started" } + job.accept(WaitForSettleStarted(job, this)) + guider.waitForSettle() + job.accept(WaitForSettleFinished(job, this)) + LOG.debug { "Wait For Settle finished" } } } diff --git a/api/src/main/kotlin/nebulosa/api/message/MessageService.kt b/api/src/main/kotlin/nebulosa/api/message/MessageService.kt index 2d3761a2e..714b5a169 100644 --- a/api/src/main/kotlin/nebulosa/api/message/MessageService.kt +++ b/api/src/main/kotlin/nebulosa/api/message/MessageService.kt @@ -1,6 +1,5 @@ package nebulosa.api.message -import nebulosa.log.debug import nebulosa.log.loggerFor import org.springframework.context.event.EventListener import org.springframework.messaging.simp.SimpMessageHeaderAccessor @@ -37,9 +36,10 @@ class MessageService( fun sendMessage(event: MessageEvent) { if (connected.get()) { + LOG.debug("sending message. event={}", event) simpleMessageTemplate.convertAndSend(DESTINATION, event) } else if (event is QueueableEvent) { - LOG.debug { "queueing message. event=$event" } + LOG.debug("queueing message. event={}", event) messageQueue.offer(event) } } diff --git a/api/src/main/kotlin/nebulosa/api/mounts/MountMoveTask.kt b/api/src/main/kotlin/nebulosa/api/mounts/MountMoveTask.kt index 178000c34..a9665d183 100644 --- a/api/src/main/kotlin/nebulosa/api/mounts/MountMoveTask.kt +++ b/api/src/main/kotlin/nebulosa/api/mounts/MountMoveTask.kt @@ -1,64 +1,51 @@ package nebulosa.api.mounts -import io.reactivex.rxjava3.functions.Consumer -import nebulosa.api.tasks.AbstractTask -import nebulosa.api.tasks.delay.DelayEvent -import nebulosa.api.tasks.delay.DelayTask import nebulosa.guiding.GuideDirection import nebulosa.indi.device.mount.Mount +import nebulosa.job.manager.Job +import nebulosa.job.manager.Task +import nebulosa.job.manager.delay.DelayTask +import nebulosa.log.debug import nebulosa.log.loggerFor -import nebulosa.util.concurrency.cancellation.CancellationListener +import nebulosa.util.Startable +import nebulosa.util.Stoppable import nebulosa.util.concurrency.cancellation.CancellationSource -import nebulosa.util.concurrency.cancellation.CancellationToken data class MountMoveTask( + @JvmField val job: Job, @JvmField val mount: Mount, @JvmField val request: MountMoveRequest, -) : AbstractTask(), CancellationListener, Consumer { +) : Task, Startable, Stoppable { - private val delayTask = DelayTask(request.duration) + @JvmField val delayTask = DelayTask(job, request.duration) - init { - delayTask.subscribe(this) - } + override fun run() { + if (!job.isCancelled && delayTask.duration > 0) { + LOG.debug { "Mount Move started. mount=$mount, request=$request" } - override fun execute(cancellationToken: CancellationToken) { - if (!cancellationToken.isCancelled && request.duration.toMillis() > 0) { mount.slewRates.takeIf { !request.speed.isNullOrBlank() } ?.find { it.name == request.speed } ?.also { mount.slewRate(it) } - mount.move(request.direction, true) - - LOG.info("Mount Move started. mount={}, request={}", mount, request) - - try { - cancellationToken.listen(this) - delayTask.execute(cancellationToken) - } finally { - stop() - cancellationToken.unlisten(this) - } + start() + delayTask.run() + stop() - LOG.info("Mount Move finished. mount={}, request={}", mount, request) + LOG.debug { "Mount Move finished. mount=$mount, request=$request" } } } override fun onCancel(source: CancellationSource) { + delayTask.onCancel(source) stop() } - fun stop() { - mount.move(request.direction, false) - } - - override fun accept(event: DelayEvent) { - onNext(MountMoveEvent(this, event.remainingTime, event.progress)) + override fun start() { + mount.move(request.direction, true) } - override fun close() { - delayTask.close() - super.close() + override fun stop() { + mount.move(request.direction, false) } companion object { diff --git a/api/src/main/kotlin/nebulosa/api/mounts/MountSlewTask.kt b/api/src/main/kotlin/nebulosa/api/mounts/MountSlewTask.kt index 6965df07c..52a60b25e 100644 --- a/api/src/main/kotlin/nebulosa/api/mounts/MountSlewTask.kt +++ b/api/src/main/kotlin/nebulosa/api/mounts/MountSlewTask.kt @@ -1,28 +1,31 @@ package nebulosa.api.mounts -import nebulosa.api.tasks.Task -import nebulosa.api.tasks.delay.DelayTask import nebulosa.indi.device.mount.Mount import nebulosa.indi.device.mount.MountEvent import nebulosa.indi.device.mount.MountSlewFailed import nebulosa.indi.device.mount.MountSlewingChanged +import nebulosa.job.manager.Job +import nebulosa.job.manager.Task +import nebulosa.job.manager.delay.DelayTask +import nebulosa.log.debug import nebulosa.log.loggerFor import nebulosa.math.Angle import nebulosa.math.formatHMS import nebulosa.math.formatSignedDMS -import nebulosa.util.concurrency.cancellation.CancellationListener +import nebulosa.util.Resettable +import nebulosa.util.Stoppable import nebulosa.util.concurrency.cancellation.CancellationSource -import nebulosa.util.concurrency.cancellation.CancellationToken import nebulosa.util.concurrency.latch.CountUpDownLatch import java.time.Duration data class MountSlewTask( + @JvmField val job: Job, @JvmField val mount: Mount, @JvmField val rightAscension: Angle, @JvmField val declination: Angle, @JvmField val j2000: Boolean = false, @JvmField val goTo: Boolean = true, -) : Task, CancellationListener, MountEventAware { +) : Task, Stoppable, Resettable, MountEventAware { - private val delayTask = DelayTask(SETTLE_DURATION) + private val delayTask = DelayTask(job, SETTLE_DURATION) private val latch = CountUpDownLatch() @Volatile private var initialRA = mount.rightAscension @@ -41,55 +44,48 @@ data class MountSlewTask( } } - override fun execute(cancellationToken: CancellationToken) { - if (!cancellationToken.isCancelled && + override fun run() { + if (!job.isCancelled && mount.connected && !mount.parked && !mount.parking && !mount.slewing && rightAscension.isFinite() && declination.isFinite() && (mount.rightAscension != rightAscension || mount.declination != declination) ) { - latch.countUp() + LOG.debug { "Mount Slew started. mount=$mount, ra=${rightAscension.formatHMS()}, dec=${declination.formatSignedDMS()}" } - LOG.info("Mount Slew started. mount={}, ra={}, dec={}", mount, rightAscension.formatHMS(), declination.formatSignedDMS()) + latch.countUp() initialRA = mount.rightAscension initialDEC = mount.declination - try { - cancellationToken.listen(this) - - if (j2000) { - if (goTo) mount.goToJ2000(rightAscension, declination) - else mount.slewToJ2000(rightAscension, declination) - } else { - if (goTo) mount.goTo(rightAscension, declination) - else mount.slewTo(rightAscension, declination) - } - - latch.await() - } finally { - cancellationToken.unlisten(this) + if (j2000) { + if (goTo) mount.goToJ2000(rightAscension, declination) + else mount.slewToJ2000(rightAscension, declination) + } else { + if (goTo) mount.goTo(rightAscension, declination) + else mount.slewTo(rightAscension, declination) } - LOG.info("Mount Slew finished. mount={}, ra={}, dec={}", mount, rightAscension.formatHMS(), declination.formatSignedDMS()) + latch.await() + + LOG.debug { "Mount Slew finished. mount=$mount, ra=${rightAscension.formatHMS()}, dec=${declination.formatSignedDMS()}" } - delayTask.execute(cancellationToken) + delayTask.run() } else { LOG.warn("cannot slew mount. mount={}, ra={}, dec={}", mount, rightAscension.formatHMS(), declination.formatSignedDMS()) } } - fun stop() { + override fun stop() { mount.abortMotion() latch.reset() } - override fun onCancel(source: CancellationSource) { - stop() + override fun reset() { + latch.reset() } - override fun close() { - delayTask.close() - super.close() + override fun onCancel(source: CancellationSource) { + stop() } companion object { diff --git a/api/src/main/kotlin/nebulosa/api/mounts/MountTrackTask.kt b/api/src/main/kotlin/nebulosa/api/mounts/MountTrackTask.kt new file mode 100644 index 000000000..9ee25942a --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/mounts/MountTrackTask.kt @@ -0,0 +1,39 @@ +package nebulosa.api.mounts + +import nebulosa.indi.device.mount.Mount +import nebulosa.indi.device.mount.MountEvent +import nebulosa.indi.device.mount.MountTrackingChanged +import nebulosa.job.manager.Job +import nebulosa.job.manager.Task +import nebulosa.log.loggerFor +import nebulosa.util.concurrency.latch.CountUpDownLatch + +data class MountTrackTask( + @JvmField val job: Job, + @JvmField val mount: Mount, + @JvmField val enabled: Boolean, +) : Task, MountEventAware { + + private val trackingLatch = CountUpDownLatch() + + override fun handleMountEvent(event: MountEvent) { + if (event.device === mount && event is MountTrackingChanged && mount.tracking == enabled) { + trackingLatch.reset() + } + } + + override fun run() { + if (mount.connected && mount.tracking != enabled) { + LOG.debug("Mount Track started. mount={}, enabled={}", mount, enabled) + trackingLatch.countUp() + mount.tracking(enabled) + trackingLatch.await() + LOG.debug("Mount Track finished. mount={}, enabled={}", mount, enabled) + } + } + + companion object { + + @JvmStatic private val LOG = loggerFor() + } +} diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/SequencerEvent.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SequencerEvent.kt index f072ab4b8..e34a7a36b 100644 --- a/api/src/main/kotlin/nebulosa/api/sequencer/SequencerEvent.kt +++ b/api/src/main/kotlin/nebulosa/api/sequencer/SequencerEvent.kt @@ -1,16 +1,18 @@ package nebulosa.api.sequencer +import com.fasterxml.jackson.annotation.JsonIgnoreProperties import nebulosa.api.cameras.CameraCaptureEvent import nebulosa.api.message.MessageEvent -import java.time.Duration +import nebulosa.indi.device.camera.Camera data class SequencerEvent( - @JvmField val id: Int = 0, - @JvmField val elapsedTime: Duration = Duration.ZERO, - @JvmField val remainingTime: Duration = Duration.ZERO, - @JvmField val progress: Double = 0.0, - @JvmField val capture: CameraCaptureEvent? = null, - @JvmField val state: SequencerState = SequencerState.IDLE, + @JvmField val camera: Camera, + @JvmField var id: Int = 0, + @JvmField var elapsedTime: Long = 0L, + @JvmField var remainingTime: Long = 0L, + @JvmField var progress: Double = 0.0, + @JvmField var state: SequencerState = SequencerState.IDLE, + @JvmField @field:JsonIgnoreProperties("camera") var capture: CameraCaptureEvent = CameraCaptureEvent(camera), ) : MessageEvent { override val eventName = "SEQUENCER.ELAPSED" diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/SequencerExecutor.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SequencerExecutor.kt index efd19c02c..53cd540c8 100644 --- a/api/src/main/kotlin/nebulosa/api/sequencer/SequencerExecutor.kt +++ b/api/src/main/kotlin/nebulosa/api/sequencer/SequencerExecutor.kt @@ -7,6 +7,7 @@ import nebulosa.api.cameras.CameraEventAware import nebulosa.api.focusers.FocuserEventAware import nebulosa.api.message.MessageEvent import nebulosa.api.message.MessageService +import nebulosa.api.rotators.RotatorEventAware import nebulosa.api.wheels.WheelEventAware import nebulosa.guiding.Guider import nebulosa.indi.device.camera.Camera @@ -17,11 +18,13 @@ import nebulosa.indi.device.focuser.Focuser import nebulosa.indi.device.focuser.FocuserEvent import nebulosa.indi.device.mount.Mount import nebulosa.indi.device.rotator.Rotator +import nebulosa.indi.device.rotator.RotatorEvent import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor import org.springframework.stereotype.Component import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.Executor @Component @Subscriber @@ -30,23 +33,28 @@ class SequencerExecutor( private val guider: Guider, private val threadPoolTaskExecutor: ThreadPoolTaskExecutor, private val calibrationFrameService: CalibrationFrameService, -) : Consumer, CameraEventAware, WheelEventAware, FocuserEventAware { +) : Consumer, CameraEventAware, WheelEventAware, FocuserEventAware, RotatorEventAware, Executor by threadPoolTaskExecutor { private val jobs = ConcurrentHashMap.newKeySet(1) @Subscribe(threadMode = ThreadMode.ASYNC) override fun handleCameraEvent(event: CameraEvent) { - jobs.find { it.task.camera === event.device }?.handleCameraEvent(event) + jobs.find { it.camera === event.device }?.handleCameraEvent(event) } @Subscribe(threadMode = ThreadMode.ASYNC) override fun handleFilterWheelEvent(event: FilterWheelEvent) { - jobs.find { it.task.wheel === event.device }?.handleFilterWheelEvent(event) + jobs.find { it.wheel === event.device }?.handleFilterWheelEvent(event) } @Subscribe(threadMode = ThreadMode.ASYNC) override fun handleFocuserEvent(event: FocuserEvent) { - // jobs.find { it.task.focuser === event.device }?.handleFocuserEvent(event) + jobs.find { it.focuser === event.device }?.handleFocuserEvent(event) + } + + @Subscribe(threadMode = ThreadMode.ASYNC) + override fun handleRotatorEvent(event: RotatorEvent) { + jobs.find { it.rotator === event.device }?.handleRotatorEvent(event) } override fun accept(event: MessageEvent) { @@ -58,43 +66,40 @@ class SequencerExecutor( mount: Mount? = null, wheel: FilterWheel? = null, focuser: Focuser? = null, rotator: Rotator? = null, ) { check(camera.connected) { "${camera.name} Camera is not connected" } - check(jobs.none { it.task.camera === camera }) { "${camera.name} Sequencer Job is already in progress" } + check(jobs.none { it.camera === camera }) { "${camera.name} Sequencer Job is already in progress" } if (wheel != null && wheel.connected) { - check(jobs.none { it.task.wheel === wheel }) { "${camera.name} Sequencer Job is already in progress" } + check(jobs.none { it.wheel === wheel }) { "${camera.name} Sequencer Job is already in progress" } } if (focuser != null && focuser.connected) { - check(jobs.none { it.task.focuser === focuser }) { "${camera.name} Sequencer Job is already in progress" } + check(jobs.none { it.focuser === focuser }) { "${camera.name} Sequencer Job is already in progress" } } if (rotator != null && rotator.connected) { - check(jobs.none { it.task.rotator === rotator }) { "${camera.name} Sequencer Job is already in progress" } + check(jobs.none { it.rotator === rotator }) { "${camera.name} Sequencer Job is already in progress" } } - val task = SequencerTask(camera, request, guider, mount, wheel, focuser, rotator, threadPoolTaskExecutor, calibrationFrameService) - task.subscribe(this) - - with(SequencerJob(task)) { + with(SequencerJob(this, camera, request, guider, mount, wheel, focuser, rotator, calibrationFrameService)) { + val completable = runAsync(threadPoolTaskExecutor) jobs.add(this) - whenComplete { _, _ -> jobs.remove(this) } - start() + completable.whenComplete { _, _ -> jobs.remove(this) } } } fun pause(camera: Camera) { - jobs.find { it.task.camera === camera }?.pause() + jobs.find { it.camera === camera }?.pause() } fun unpause(camera: Camera) { - jobs.find { it.task.camera === camera }?.unpause() + jobs.find { it.camera === camera }?.unpause() } fun stop(camera: Camera) { - jobs.find { it.task.camera === camera }?.stop() + jobs.find { it.camera === camera }?.stop() } fun status(camera: Camera): SequencerEvent? { - return jobs.find { it.task.camera === camera }?.task?.get() as? SequencerEvent + return jobs.find { it.camera === camera }?.status } } diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/SequencerJob.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SequencerJob.kt index 55b2e7d70..1e8fd6109 100644 --- a/api/src/main/kotlin/nebulosa/api/sequencer/SequencerJob.kt +++ b/api/src/main/kotlin/nebulosa/api/sequencer/SequencerJob.kt @@ -1,20 +1,316 @@ package nebulosa.api.sequencer -import nebulosa.api.cameras.CameraEventAware -import nebulosa.api.tasks.Job +import nebulosa.api.calibration.CalibrationFrameProvider +import nebulosa.api.cameras.* +import nebulosa.api.focusers.FocuserEventAware +import nebulosa.api.guiding.DitherAfterExposureEvent +import nebulosa.api.guiding.DitherAfterExposureTask +import nebulosa.api.guiding.WaitForSettleTask +import nebulosa.api.message.MessageEvent +import nebulosa.api.rotators.RotatorEventAware import nebulosa.api.wheels.WheelEventAware +import nebulosa.api.wheels.WheelMoveTask +import nebulosa.guiding.Guider +import nebulosa.indi.device.camera.Camera import nebulosa.indi.device.camera.CameraEvent +import nebulosa.indi.device.camera.FrameType +import nebulosa.indi.device.filterwheel.FilterWheel import nebulosa.indi.device.filterwheel.FilterWheelEvent +import nebulosa.indi.device.focuser.Focuser +import nebulosa.indi.device.focuser.FocuserEvent +import nebulosa.indi.device.mount.Mount +import nebulosa.indi.device.rotator.Rotator +import nebulosa.indi.device.rotator.RotatorEvent +import nebulosa.job.manager.AbstractJob +import nebulosa.job.manager.SplitTask +import nebulosa.job.manager.Task +import nebulosa.job.manager.delay.DelayEvent +import nebulosa.job.manager.delay.DelayTask +import nebulosa.log.debug +import nebulosa.log.loggerFor +import java.nio.file.Path -data class SequencerJob(override val task: SequencerTask) : Job(), CameraEventAware, WheelEventAware { +data class SequencerJob( + @JvmField val sequencerExecutor: SequencerExecutor, + @JvmField val camera: Camera, + @JvmField val plan: SequencerPlanRequest, + @JvmField val guider: Guider? = null, + @JvmField val mount: Mount? = null, + @JvmField val wheel: FilterWheel? = null, + @JvmField val focuser: Focuser? = null, + @JvmField val rotator: Rotator? = null, + private val calibrationFrameProvider: CalibrationFrameProvider? = null, +) : AbstractJob(), CameraEventAware, WheelEventAware, FocuserEventAware, RotatorEventAware { - override val name = "${task.camera.name} Sequencer Job" + private val sequences = plan.sequences.filter { it.enabled } + private val initialDelayTask = DelayTask(this, plan.initialDelay) + private val waitForSettleTask = WaitForSettleTask(this, guider) + private val liveStackingManager = CameraLiveStackingManager(calibrationFrameProvider) + private val cameraCaptureEvents = + Array(plan.sequences.size + 1) { CameraCaptureEvent(camera, exposureAmount = plan.sequences.getOrNull(it - 1)?.exposureAmount ?: 0) } + + @Volatile private var estimatedCaptureTime = initialDelayTask.duration * 1000L + @Volatile private var captureStartElapsedTime = 0L + + @JvmField val status = SequencerEvent(camera) + + init { + require(sequences.isNotEmpty()) { "no entries found" } + + add(initialDelayTask) + + if (plan.captureMode == SequencerCaptureMode.FULLY || sequences.size == 1) { + var first = true + + for (i in sequences.indices) { + val request = sequences[i].map() + + val id = plan.sequences.indexOfFirst { it === sequences[i] } + 1 + + // SEQUENCE ID. + add(SequencerIdTask(id)) + + // FILTER WHEEL. + request.wheelMoveTask()?.also(::add) + + // DELAY. + val delayTask = DelayTask(this, request.exposureDelay) + + // CAPTURE. + val cameraCaptureTask = CameraExposureTask(this, camera, request) + + repeat(request.exposureAmount) { + if (!first) { + add(SplitTask(listOf(delayTask, waitForSettleTask), sequencerExecutor)) + cameraCaptureEvents[id].captureRemainingTime += delayTask.duration * 1000L + } + + add(cameraCaptureTask) + first = false + + cameraCaptureEvents[id].captureRemainingTime += cameraCaptureTask.exposureTimeInMicroseconds + } + + // DITHER. + add(DitherAfterExposureTask(this, guider, request.dither)) + } + } else { + val requests = sequences.map { it.map() } + val sequenceIdTasks = sequences.map { req -> SequencerIdTask(plan.sequences.indexOfFirst { it === req } + 1) } + val wheelMoveTasks = requests.map { it.wheelMoveTask() } + val cameraExposureTasks = requests.map { CameraExposureTask(this, camera, it) } + val delayTasks = requests.map { DelayTask(this, it.exposureDelay) } + val ditherAfterExposureTask = requests.map { DitherAfterExposureTask(this, guider, it.dither) } + val delayAndWaitForSettleSplitTasks = delayTasks.map { SplitTask(listOf(it, waitForSettleTask), sequencerExecutor) } + val count = IntArray(requests.size) { requests[it].exposureAmount } + var first = true + + while (count.sum() > 0) { + for (i in count.indices) { + if (count[i] > 0) { + val id = sequenceIdTasks[i].id + + // SEQUENCE ID. + add(sequenceIdTasks[i]) + + // FILTER WHEEL. + wheelMoveTasks[i]?.also(::add) + + // DELAY. + if (!first) { + add(delayAndWaitForSettleSplitTasks[i]) + cameraCaptureEvents[id].captureRemainingTime += delayTasks[i].duration * 1000L + } + + // CAPTURE. + add(cameraExposureTasks[i]) + cameraCaptureEvents[id].captureRemainingTime += cameraExposureTasks[i].exposureTimeInMicroseconds + + // DITHER. + add(ditherAfterExposureTask[i]) + + count[i]-- + first = false + } + } + } + } + + estimatedCaptureTime += cameraCaptureEvents.sumOf { it.captureRemainingTime } + } override fun handleCameraEvent(event: CameraEvent) { - task.handleCameraEvent(event) + (currentTask as? CameraEventAware)?.handleCameraEvent(event) } override fun handleFilterWheelEvent(event: FilterWheelEvent) { - task.handleFilterWheelEvent(event) + (currentTask as? WheelEventAware)?.handleFilterWheelEvent(event) + } + + override fun handleFocuserEvent(event: FocuserEvent) = Unit + + override fun handleRotatorEvent(event: RotatorEvent) = Unit + + private fun CameraStartCaptureRequest.map() = copy( + savePath = plan.savePath, + autoSave = true, + autoSubFolderMode = plan.autoSubFolderMode, + dither = plan.dither, + liveStacking = plan.liveStacking, + namingFormat = plan.namingFormat, + ) + + private fun CameraStartCaptureRequest.wheelMoveTask(): WheelMoveTask? { + if (wheel != null) { + val filterPosition = if (frameType == FrameType.DARK) shutterPosition else filterPosition + + if (filterPosition in 1..wheel.count) { + return WheelMoveTask(this@SequencerJob, wheel, filterPosition) + } + } + + return null + } + + private fun addFrameToLiveStacker(request: CameraStartCaptureRequest, path: Path?): Path? { + return if (path != null && liveStackingManager.start(request, path)) { + try { + status.capture.state = CameraCaptureState.STACKING + status.send() + + liveStackingManager.stack(request, path) + } catch (_: Throwable) { + null + } + } else { + null + } + } + + override fun onPause(paused: Boolean) { + if (paused) { + status.state = SequencerState.PAUSING + status.send() + } + + super.onPause(paused) + } + + override fun beforePause(task: Task) { + status.state = SequencerState.PAUSED + status.send() + } + + override fun afterPause(task: Task) { + status.state = SequencerState.RUNNING + status.send() + } + + override fun canRun(prev: Task?, current: Task): Boolean { + if (current is DitherAfterExposureTask) { + return !isCancelled && guider != null + && status.capture.exposureCount >= 1 && current.request.afterExposures > 0 + && status.capture.exposureCount % current.request.afterExposures == 0 + } + + return super.canRun(prev, current) + } + + override fun accept(event: Any) { + when (event) { + is DelayEvent -> { + status.capture.handleCameraDelayEvent(event) + status.elapsedTime += event.waitTime + status.computeRemainingTimeAndProgress() + status.send() + } + is CameraExposureEvent -> { + status.capture.handleCameraExposureEvent(event) + + if (event is CameraExposureFailed) { + return stop() + } + + if (event is CameraExposureStarted) { + captureStartElapsedTime = status.elapsedTime + } else { + status.elapsedTime = captureStartElapsedTime + event.elapsedTime + + if (event is CameraExposureFinished) { + if (status.capture.captureRemainingTime <= 0L) { + status.capture.state = CameraCaptureState.IDLE + } + + status.capture.liveStackedPath = addFrameToLiveStacker(event.task.request, status.capture.savedPath) + status.capture.send() + } + } + + status.computeRemainingTimeAndProgress() + status.send() + } + is DitherAfterExposureEvent -> { + status.capture.state = CameraCaptureState.DITHERING + status.send() + } + } + } + + override fun beforeStart() { + LOG.debug("Sequencer started. camera={}, mount={}, wheel={}, focuser={}, rotator={}, plan={}", camera, mount, wheel, focuser, rotator, plan) + + status.state = SequencerState.RUNNING + status.send() + } + + override fun beforeTask(task: Task) { + if (task === initialDelayTask && initialDelayTask.duration > 0L) { + status.state = SequencerState.WAITING + } + } + + override fun afterTask(task: Task, exception: Throwable?): Boolean { + if (exception == null) { + if (task is SequencerIdTask) { + status.capture = cameraCaptureEvents[task.id] + } else if (task === initialDelayTask) { + status.state = SequencerState.RUNNING + } + } + + return super.afterTask(task, exception) + } + + override fun afterFinish() { + liveStackingManager.close() + + status.state = SequencerState.IDLE + status.send() + + LOG.debug("Sequencer finished. camera={}, mount={}, wheel={}, focuser={}, rotator={}, plan={}", camera, mount, wheel, focuser, rotator, plan) + } + + @Suppress("NOTHING_TO_INLINE") + private inline fun SequencerEvent.computeRemainingTimeAndProgress() { + remainingTime = if (estimatedCaptureTime > elapsedTime) estimatedCaptureTime - elapsedTime else 0L + progress = elapsedTime.toDouble() / estimatedCaptureTime + } + + @Suppress("NOTHING_TO_INLINE") + private inline fun MessageEvent.send() { + sequencerExecutor.accept(this) + } + + private inner class SequencerIdTask(@JvmField val id: Int) : Task { + + override fun run() { + LOG.debug { "Sequence in execution. id=$id" } + status.id = id + } + } + + companion object { + + @JvmStatic private val LOG = loggerFor() } } diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/SequencerState.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SequencerState.kt index 4fc5ecca9..8b318bd79 100644 --- a/api/src/main/kotlin/nebulosa/api/sequencer/SequencerState.kt +++ b/api/src/main/kotlin/nebulosa/api/sequencer/SequencerState.kt @@ -2,6 +2,7 @@ package nebulosa.api.sequencer enum class SequencerState { IDLE, + WAITING, RUNNING, PAUSING, PAUSED, diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/SequencerTask.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SequencerTask.kt deleted file mode 100644 index 34029f5b9..000000000 --- a/api/src/main/kotlin/nebulosa/api/sequencer/SequencerTask.kt +++ /dev/null @@ -1,301 +0,0 @@ -package nebulosa.api.sequencer - -import io.reactivex.rxjava3.functions.Consumer -import nebulosa.api.calibration.CalibrationFrameProvider -import nebulosa.api.cameras.* -import nebulosa.api.message.MessageEvent -import nebulosa.api.tasks.AbstractTask -import nebulosa.api.tasks.Task -import nebulosa.api.tasks.delay.DelayEvent -import nebulosa.api.tasks.delay.DelayTask -import nebulosa.api.wheels.WheelEventAware -import nebulosa.api.wheels.WheelMoveTask -import nebulosa.guiding.Guider -import nebulosa.indi.device.camera.Camera -import nebulosa.indi.device.camera.CameraEvent -import nebulosa.indi.device.camera.FrameType -import nebulosa.indi.device.filterwheel.FilterWheel -import nebulosa.indi.device.filterwheel.FilterWheelEvent -import nebulosa.indi.device.focuser.Focuser -import nebulosa.indi.device.mount.Mount -import nebulosa.indi.device.rotator.Rotator -import nebulosa.log.loggerFor -import nebulosa.util.concurrency.cancellation.CancellationToken -import nebulosa.util.concurrency.latch.PauseListener -import java.time.Duration -import java.util.* -import java.util.concurrent.Executor -import java.util.concurrent.atomic.AtomicBoolean -import java.util.concurrent.atomic.AtomicInteger -import java.util.concurrent.atomic.AtomicReference - -// https://cdn.diffractionlimited.com/help/maximdl/Autosave_Sequence.htm -// https://nighttime-imaging.eu/docs/master/site/tabs/sequence/ -// https://nighttime-imaging.eu/docs/master/site/sequencer/advanced/advanced/ - -data class SequencerTask( - @JvmField val camera: Camera, - @JvmField val plan: SequencerPlanRequest, - @JvmField val guider: Guider? = null, - @JvmField val mount: Mount? = null, - @JvmField val wheel: FilterWheel? = null, - @JvmField val focuser: Focuser? = null, - @JvmField val rotator: Rotator? = null, - private val executor: Executor? = null, - private val calibrationFrameProvider: CalibrationFrameProvider? = null, -) : AbstractTask(), Consumer, CameraEventAware, WheelEventAware, PauseListener { - - private val sequences = plan.sequences.filter { it.enabled } - private val initialDelayTask = DelayTask(plan.initialDelay) - private val sequencerId = AtomicInteger() - private val tasks = LinkedList() - private val currentTask = AtomicReference() - private val pausing = AtomicBoolean() - private val paused = AtomicBoolean() - private val liveStackingManager = CameraLiveStackingManager(calibrationFrameProvider) - - @Volatile private var estimatedCaptureTime = initialDelayTask.duration - @Volatile private var elapsedTime = Duration.ZERO - @Volatile private var prevElapsedTime = Duration.ZERO - @Volatile private var remainingTime = Duration.ZERO - @Volatile private var progress = 0.0 - - init { - require(sequences.isNotEmpty()) { "no entries found" } - - initialDelayTask.subscribe(this) - tasks.add(initialDelayTask) - - fun mapRequest(request: CameraStartCaptureRequest) = request.copy( - savePath = plan.savePath, autoSave = true, - autoSubFolderMode = plan.autoSubFolderMode, - dither = plan.dither, - liveStacking = plan.liveStacking, - namingFormat = plan.namingFormat, - ) - - if (plan.captureMode == SequencerCaptureMode.FULLY || sequences.size == 1) { - for (i in sequences.indices) { - val request = mapRequest(sequences[i]) - - // ID. - tasks.add(SequencerIdTask(plan.sequences.indexOfFirst { it === sequences[i] } + 1)) - - // FILTER WHEEL. - request.wheelMoveTask()?.also(tasks::add) - - // CAPTURE. - val cameraCaptureTask = CameraCaptureTask( - camera, request, guider, false, executor, - liveStackingManager, - mount, wheel, focuser, rotator - ) - - cameraCaptureTask.subscribe(this) - estimatedCaptureTime += cameraCaptureTask.estimatedCaptureTime - tasks.add(SequenceCaptureModeCameraCaptureTask(cameraCaptureTask, SequencerCaptureMode.FULLY, i)) - } - } else { - val sequenceIdTasks = sequences.map { req -> SequencerIdTask(plan.sequences.indexOfFirst { it === req } + 1) } - val requests = sequences.map { mapRequest(it) } - val cameraCaptureTasks = requests.mapIndexed { i, req -> - val task = CameraCaptureTask( - camera, req, guider, - i > 0, executor, liveStackingManager, - mount, wheel, focuser, rotator - ) - - SequenceCaptureModeCameraCaptureTask(task, SequencerCaptureMode.INTERLEAVED, i) - } - val wheelMoveTasks = requests.map { it.wheelMoveTask() } - val count = IntArray(requests.size) { sequences[it].exposureAmount } - - for ((cameraCaptureTask) in cameraCaptureTasks) { - cameraCaptureTask.subscribe(this) - estimatedCaptureTime += cameraCaptureTask.estimatedCaptureTime - } - - while (count.sum() > 0) { - for (i in sequences.indices) { - if (count[i] > 0) { - tasks.add(sequenceIdTasks[i]) - wheelMoveTasks[i]?.also(tasks::add) - - val task = cameraCaptureTasks[i] - - if (count[i] == sequences[i].exposureAmount) { - tasks.add(InitializeCameraCaptureTask(task.task)) - } - - tasks.add(task) - - count[i]-- - - if (count[i] == 0) { - tasks.add(FininalizeCameraCaptureTask(task.task)) - } - } - } - } - } - } - - override fun handleCameraEvent(event: CameraEvent) { - when (val task = currentTask.get()) { - is CameraCaptureTask -> task.handleCameraEvent(event) - is SequenceCaptureModeCameraCaptureTask -> task.task.handleCameraEvent(event) - } - } - - override fun handleFilterWheelEvent(event: FilterWheelEvent) { - when (val task = currentTask.get()) { - is WheelMoveTask -> task.handleFilterWheelEvent(event) - is CameraCaptureTask -> task.handleFilterWheelEvent(event) - is SequenceCaptureModeCameraCaptureTask -> task.task.handleFilterWheelEvent(event) - } - } - - override fun onPause(paused: Boolean) { - pausing.set(paused) - - if (paused) { - sendEvent(SequencerState.PAUSING) - } - } - - override fun execute(cancellationToken: CancellationToken) { - LOG.info("Sequencer started. camera={}, mount={}, wheel={}, focuser={}, plan={}", camera, mount, wheel, focuser, plan) - - cancellationToken.listenToPause(this) - - for (task in tasks) { - if (cancellationToken.isCancelled) break - - currentTask.set(task) - task.execute(cancellationToken) - currentTask.set(null) - } - - if (remainingTime.toMillis() > 0L) { - remainingTime = Duration.ZERO - progress = 1.0 - sendEvent(SequencerState.IDLE) - } - - cancellationToken.unlistenToPause(this) - - LOG.info("Sequencer finished. camera={}, mount={}, wheel={}, focuser={}, plan={}", camera, mount, wheel, focuser, plan) - } - - private fun CameraStartCaptureRequest.wheelMoveTask(): WheelMoveTask? { - if (wheel != null) { - val filterPosition = if (frameType == FrameType.DARK) shutterPosition else filterPosition - - if (filterPosition in 1..wheel.count) { - return WheelMoveTask(wheel, filterPosition) - } - } - - return null - } - - override fun canUseAsLastEvent(event: MessageEvent) = event is SequencerEvent - - override fun accept(event: Any) { - when (event) { - is DelayEvent -> { - if (event.task === initialDelayTask) { - elapsedTime += event.waitTime - computeRemainingTimeAndProgress() - sendEvent(SequencerState.RUNNING) - } - } - is CameraCaptureEvent -> { - pausing.set(event.state == CameraCaptureState.PAUSING) - paused.set(event.state == CameraCaptureState.PAUSED) - - when (event.state) { - CameraCaptureState.CAPTURE_STARTED -> { - prevElapsedTime = elapsedTime - } - CameraCaptureState.EXPOSURING, - CameraCaptureState.WAITING -> { - elapsedTime = prevElapsedTime + event.captureElapsedTime - computeRemainingTimeAndProgress() - } - CameraCaptureState.EXPOSURE_FINISHED -> { - onNext(event) - } - else -> Unit - } - - sendEvent(SequencerState.RUNNING, event) - } - } - } - - @Suppress("NOTHING_TO_INLINE") - private inline fun computeRemainingTimeAndProgress() { - remainingTime = if (estimatedCaptureTime > elapsedTime) estimatedCaptureTime - elapsedTime else Duration.ZERO - progress = (estimatedCaptureTime - remainingTime).toNanos().toDouble() / estimatedCaptureTime.toNanos() - } - - @Suppress("NOTHING_TO_INLINE") - private inline fun sendEvent(state: SequencerState, capture: CameraCaptureEvent? = null) { - onNext( - SequencerEvent( - sequencerId.get(), elapsedTime, remainingTime, progress, capture, - if (pausing.get()) SequencerState.PAUSING else if (paused.get()) SequencerState.PAUSED else state - ) - ) - } - - override fun close() { - tasks.forEach { it.close() } - liveStackingManager.close() - } - - private inner class SequencerIdTask(private val id: Int) : Task { - - override fun execute(cancellationToken: CancellationToken) { - LOG.info("Sequence in execution. id={}", id) - sequencerId.set(id) - } - } - - private data class InitializeCameraCaptureTask(@JvmField val task: CameraCaptureTask) : Task { - - override fun execute(cancellationToken: CancellationToken) { - task.initialize(cancellationToken) - } - } - - private data class SequenceCaptureModeCameraCaptureTask( - @JvmField val task: CameraCaptureTask, - @JvmField val mode: SequencerCaptureMode, - @JvmField val index: Int, - ) : Task { - - override fun execute(cancellationToken: CancellationToken) { - if (mode == SequencerCaptureMode.FULLY) { - task.initialize(cancellationToken) - task.executeInLoop(cancellationToken) - task.finalize(cancellationToken) - } else { - task.executeOnce(cancellationToken) - } - } - } - - private data class FininalizeCameraCaptureTask(@JvmField val task: CameraCaptureTask) : Task { - - override fun execute(cancellationToken: CancellationToken) { - task.finalize(cancellationToken) - } - } - - companion object { - - @JvmStatic private val LOG = loggerFor() - } -} diff --git a/api/src/main/kotlin/nebulosa/api/tasks/AbstractTask.kt b/api/src/main/kotlin/nebulosa/api/tasks/AbstractTask.kt deleted file mode 100644 index 676439b12..000000000 --- a/api/src/main/kotlin/nebulosa/api/tasks/AbstractTask.kt +++ /dev/null @@ -1,31 +0,0 @@ -package nebulosa.api.tasks - -import io.reactivex.rxjava3.core.Observable -import io.reactivex.rxjava3.core.Observer -import java.util.concurrent.atomic.AtomicReference - -abstract class AbstractTask : Observable(), ObservableTask { - - private val observers = LinkedHashSet>(1) - private val lastEvent = AtomicReference() - - protected open fun canUseAsLastEvent(event: T) = true - - final override fun get(): T? = lastEvent.get() - - final override fun subscribeActual(observer: Observer) { - observers.add(observer) - } - - protected fun onNext(event: T) { - if (canUseAsLastEvent(event)) { - lastEvent.set(event) - } - - observers.forEach { it.onNext(event) } - } - - override fun close() { - observers.forEach { it.onComplete() } - } -} diff --git a/api/src/main/kotlin/nebulosa/api/tasks/Job.kt b/api/src/main/kotlin/nebulosa/api/tasks/Job.kt deleted file mode 100644 index a001127c0..000000000 --- a/api/src/main/kotlin/nebulosa/api/tasks/Job.kt +++ /dev/null @@ -1,66 +0,0 @@ -package nebulosa.api.tasks - -import nebulosa.util.concurrency.cancellation.CancellationToken -import java.util.concurrent.CompletableFuture -import java.util.concurrent.atomic.AtomicBoolean - -abstract class Job : CompletableFuture(), Runnable { - - abstract val task: Task - - abstract val name: String - - private val cancellationToken = CancellationToken() - private val running = AtomicBoolean() - - @Volatile private var thread: Thread? = null - - val isRunning - get() = running.get() - - final override fun run() { - try { - running.set(true) - task.execute(cancellationToken) - } finally { - running.set(false) - thread = null - cancellationToken.close() - complete(Unit) - task.close() - } - } - - /** - * Runs this Job in a new thread. - */ - @Synchronized - fun start() { - if (thread == null && !running.get()) { - thread = Thread(this, name) - thread!!.isDaemon = true - thread!!.start() - } - } - - /** - * Stops gracefully this Job. - */ - fun stop() { - cancellationToken.cancel() - } - - /** - * Pauses this Job. - */ - fun pause() { - cancellationToken.pause() - } - - /** - * Unpauses this Job. - */ - fun unpause() { - cancellationToken.unpause() - } -} diff --git a/api/src/main/kotlin/nebulosa/api/tasks/ObservableTask.kt b/api/src/main/kotlin/nebulosa/api/tasks/ObservableTask.kt deleted file mode 100644 index e04a941c4..000000000 --- a/api/src/main/kotlin/nebulosa/api/tasks/ObservableTask.kt +++ /dev/null @@ -1,6 +0,0 @@ -package nebulosa.api.tasks - -import io.reactivex.rxjava3.core.ObservableSource -import java.util.function.Supplier - -interface ObservableTask : Task, ObservableSource, Supplier diff --git a/api/src/main/kotlin/nebulosa/api/tasks/SplitTask.kt b/api/src/main/kotlin/nebulosa/api/tasks/SplitTask.kt deleted file mode 100644 index b3377ff27..000000000 --- a/api/src/main/kotlin/nebulosa/api/tasks/SplitTask.kt +++ /dev/null @@ -1,32 +0,0 @@ -package nebulosa.api.tasks - -import nebulosa.util.concurrency.cancellation.CancellationToken -import java.util.concurrent.CompletableFuture -import java.util.concurrent.Executor -import java.util.concurrent.ForkJoinPool - -data class SplitTask( - private val tasks: Collection, - private val executor: Executor? = null, -) : Task { - - override fun execute(cancellationToken: CancellationToken) { - if (tasks.isEmpty()) { - return - } else if (tasks.size == 1) { - tasks.first().execute(cancellationToken) - } else { - val completables = tasks.map { CompletableFuture.runAsync({ it.execute(cancellationToken) }, executor ?: EXECUTOR) } - completables.forEach(CompletableFuture<*>::join) - } - } - - override fun reset() { - tasks.forEach { it.reset() } - } - - companion object { - - @JvmStatic private val EXECUTOR = ForkJoinPool.commonPool() - } -} diff --git a/api/src/main/kotlin/nebulosa/api/tasks/Task.kt b/api/src/main/kotlin/nebulosa/api/tasks/Task.kt deleted file mode 100644 index bec06bc63..000000000 --- a/api/src/main/kotlin/nebulosa/api/tasks/Task.kt +++ /dev/null @@ -1,26 +0,0 @@ -package nebulosa.api.tasks - -import nebulosa.util.Resettable -import nebulosa.util.concurrency.cancellation.CancellationToken - -interface Task : Resettable, AutoCloseable { - - fun execute(cancellationToken: CancellationToken = CancellationToken.NONE) - - override fun reset() = Unit - - override fun close() = Unit - - companion object { - - @JvmStatic - fun of(vararg tasks: Task) = object : Task { - - override fun execute(cancellationToken: CancellationToken) = tasks.forEach { it.execute(cancellationToken) } - - override fun reset() = tasks.forEach { it.reset() } - - override fun close() = tasks.forEach { it.close() } - } - } -} diff --git a/api/src/main/kotlin/nebulosa/api/tasks/delay/DelayEvent.kt b/api/src/main/kotlin/nebulosa/api/tasks/delay/DelayEvent.kt deleted file mode 100644 index f68e8119c..000000000 --- a/api/src/main/kotlin/nebulosa/api/tasks/delay/DelayEvent.kt +++ /dev/null @@ -1,10 +0,0 @@ -package nebulosa.api.tasks.delay - -import java.time.Duration - -data class DelayEvent( - @JvmField val task: DelayTask, - @JvmField val remainingTime: Duration = Duration.ZERO, - @JvmField val waitTime: Duration = Duration.ZERO, - @JvmField val progress: Double = 0.0, -) diff --git a/api/src/main/kotlin/nebulosa/api/tasks/delay/DelayTask.kt b/api/src/main/kotlin/nebulosa/api/tasks/delay/DelayTask.kt deleted file mode 100644 index 8befc7879..000000000 --- a/api/src/main/kotlin/nebulosa/api/tasks/delay/DelayTask.kt +++ /dev/null @@ -1,61 +0,0 @@ -package nebulosa.api.tasks.delay - -import nebulosa.api.tasks.AbstractTask -import nebulosa.log.loggerFor -import nebulosa.util.concurrency.cancellation.CancellationToken -import java.time.Duration - -data class DelayTask( - @JvmField val duration: Duration, -) : AbstractTask() { - - @Volatile private var remainingTime = Duration.ZERO - @Volatile private var waitTime = Duration.ZERO - @Volatile private var progress = 0.0 - - override fun execute(cancellationToken: CancellationToken) { - val durationTime = duration.toMillis() - var remainingTime = durationTime - - if (!cancellationToken.isCancelled && remainingTime > 0L) { - LOG.info("Delay started. duration={}", remainingTime) - - while (!cancellationToken.isCancelled && remainingTime > 0L) { - val waitTime = minOf(remainingTime, DELAY_INTERVAL) - - if (waitTime > 0L) { - progress = (durationTime - remainingTime) / durationTime.toDouble() - this.remainingTime = Duration.ofMillis(remainingTime) - this.waitTime = Duration.ofMillis(waitTime) - sendEvent() - - Thread.sleep(waitTime) - - remainingTime -= waitTime - } - } - - this.remainingTime = Duration.ZERO - this.waitTime = Duration.ZERO - progress = 1.0 - - sendEvent() - } - } - - override fun reset() { - remainingTime = Duration.ZERO - waitTime = Duration.ZERO - progress = 0.0 - } - - private fun sendEvent() { - onNext(DelayEvent(this, remainingTime, waitTime, progress)) - } - - companion object { - - const val DELAY_INTERVAL = 500L - @JvmStatic private val LOG = loggerFor() - } -} diff --git a/api/src/main/kotlin/nebulosa/api/wheels/WheelMoveTask.kt b/api/src/main/kotlin/nebulosa/api/wheels/WheelMoveTask.kt index f269ae2b0..3e7dd539a 100644 --- a/api/src/main/kotlin/nebulosa/api/wheels/WheelMoveTask.kt +++ b/api/src/main/kotlin/nebulosa/api/wheels/WheelMoveTask.kt @@ -1,15 +1,18 @@ package nebulosa.api.wheels -import nebulosa.api.tasks.Task import nebulosa.indi.device.filterwheel.FilterWheel import nebulosa.indi.device.filterwheel.FilterWheelEvent import nebulosa.indi.device.filterwheel.FilterWheelMoveFailed import nebulosa.indi.device.filterwheel.FilterWheelPositionChanged +import nebulosa.job.manager.Job +import nebulosa.job.manager.Task +import nebulosa.log.debug import nebulosa.log.loggerFor -import nebulosa.util.concurrency.cancellation.CancellationToken +import nebulosa.util.concurrency.cancellation.CancellationSource import nebulosa.util.concurrency.latch.CountUpDownLatch data class WheelMoveTask( + @JvmField val job: Job, @JvmField val wheel: FilterWheel, @JvmField val position: Int, ) : Task, WheelEventAware { @@ -29,26 +32,26 @@ data class WheelMoveTask( } } - override fun execute(cancellationToken: CancellationToken) { + override fun run() { if (wheel.connected && position in 1..wheel.count && wheel.position != position) { + LOG.debug { "Wheel Move started. wheel=$wheel, position=$position" } + initialPosition = wheel.position - LOG.info("Filter Wheel Move started. wheel={}, position={}", wheel, position) + latch.countUp() + wheel.moveTo(position) + latch.await() - try { - cancellationToken.listen(latch) - latch.countUp() - wheel.moveTo(position) - latch.await() - } finally { - cancellationToken.unlisten(latch) - LOG.info("Filter Wheel Move finished. wheel={}, position={}", wheel, position) - } + LOG.debug { "Wheel Move finished. wheel=$wheel, position=$position" } } else { LOG.warn("filter wheel not connected or invalid position. position={}, wheel={}", position, wheel) } } + override fun onCancel(source: CancellationSource) { + latch.onCancel(source) + } + companion object { @JvmStatic private val LOG = loggerFor() diff --git a/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardEvent.kt b/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardEvent.kt index a8d13c5a2..44266d4cb 100644 --- a/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardEvent.kt +++ b/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardEvent.kt @@ -1,15 +1,15 @@ package nebulosa.api.wizard.flat +import com.fasterxml.jackson.annotation.JsonIgnoreProperties import nebulosa.api.cameras.CameraCaptureEvent import nebulosa.api.message.MessageEvent -import java.nio.file.Path -import java.time.Duration +import nebulosa.indi.device.camera.Camera data class FlatWizardEvent( - @JvmField val state: FlatWizardState = FlatWizardState.IDLE, - @JvmField val exposureTime: Duration = Duration.ZERO, - @JvmField val capture: CameraCaptureEvent? = null, - @JvmField val savedPath: Path? = null, + @JvmField val camera: Camera, + @JvmField var state: FlatWizardState = FlatWizardState.IDLE, + @JvmField var exposureTime: Long = 0L, + @JvmField @field:JsonIgnoreProperties("camera") val capture: CameraCaptureEvent = CameraCaptureEvent(camera), ) : MessageEvent { override val eventName = "FLAT_WIZARD.ELAPSED" diff --git a/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardExecutor.kt b/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardExecutor.kt index cce2facbc..975a037d5 100644 --- a/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardExecutor.kt +++ b/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardExecutor.kt @@ -9,6 +9,7 @@ import nebulosa.indi.device.camera.Camera import nebulosa.indi.device.camera.CameraEvent import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor import org.springframework.stereotype.Component import java.util.concurrent.ConcurrentHashMap @@ -16,13 +17,14 @@ import java.util.concurrent.ConcurrentHashMap @Subscriber class FlatWizardExecutor( private val messageService: MessageService, + private val threadPoolTaskExecutor: ThreadPoolTaskExecutor, ) : Consumer, CameraEventAware { private val jobs = ConcurrentHashMap.newKeySet(1) @Subscribe(threadMode = ThreadMode.ASYNC) override fun handleCameraEvent(event: CameraEvent) { - jobs.find { it.task.camera === event.device }?.handleCameraEvent(event) + jobs.find { it.camera === event.device }?.handleCameraEvent(event) } override fun accept(event: MessageEvent) { @@ -31,23 +33,20 @@ class FlatWizardExecutor( fun execute(camera: Camera, request: FlatWizardRequest) { check(camera.connected) { "camera is not connected" } - check(jobs.none { it.task.camera === camera }) { "${camera.name} Flat Wizard is already in progress" } + check(jobs.none { it.camera === camera }) { "${camera.name} Flat Wizard is already in progress" } - val task = FlatWizardTask(camera, request) - task.subscribe(this) - - with(FlatWizardJob(task)) { + with(FlatWizardJob(this, camera, request)) { + val completable = runAsync(threadPoolTaskExecutor) jobs.add(this) - whenComplete { _, _ -> jobs.remove(this) } - start() + completable.whenComplete { _, _ -> jobs.remove(this) } } } fun stop(camera: Camera) { - jobs.find { it.task.camera === camera }?.stop() + jobs.find { it.camera === camera }?.stop() } fun status(camera: Camera): FlatWizardEvent? { - return jobs.find { it.task.camera === camera }?.task?.get() as? FlatWizardEvent + return jobs.find { it.camera === camera }?.status } } diff --git a/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardJob.kt b/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardJob.kt index 4dfcb7a3a..dd2ee0e53 100644 --- a/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardJob.kt +++ b/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardJob.kt @@ -1,14 +1,144 @@ package nebulosa.api.wizard.flat -import nebulosa.api.cameras.CameraEventAware -import nebulosa.api.tasks.Job +import nebulosa.api.cameras.* +import nebulosa.api.message.MessageEvent +import nebulosa.fits.fits +import nebulosa.image.Image +import nebulosa.image.algorithms.computation.Statistics +import nebulosa.indi.device.camera.Camera import nebulosa.indi.device.camera.CameraEvent +import nebulosa.indi.device.camera.FrameType +import nebulosa.job.manager.AbstractJob +import nebulosa.job.manager.Task +import nebulosa.log.loggerFor +import nebulosa.util.concurrency.latch.CountUpDownLatch +import java.nio.file.Path +import java.time.Duration -data class FlatWizardJob(override val task: FlatWizardTask) : Job(), CameraEventAware { +data class FlatWizardJob( + @JvmField val flatWizardExecutor: FlatWizardExecutor, + @JvmField val camera: Camera, + @JvmField val request: FlatWizardRequest, +) : AbstractJob(), CameraEventAware { - override val name = "${task.camera.name} Flat Wizard Job" + @JvmField val meanTarget = request.meanTarget / 65535f + @JvmField val meanRange = (meanTarget * request.meanTolerance / 100f).let { (meanTarget - it)..(meanTarget + it) } + + @Volatile private var exposureMin = request.exposureMin.toNanos() + @Volatile private var exposureMax = request.exposureMax.toNanos() + + @JvmField val status = FlatWizardEvent(camera) + + @Volatile private var cameraExposureTask = CameraExposureTask( + this, camera, request.capture.copy( + exposureTime = Duration.ofNanos((exposureMin + exposureMax) / 2), + frameType = FrameType.FLAT, + autoSave = false, autoSubFolderMode = AutoSubFolderMode.OFF, + ) + ) + + private val waitToComputeOptimalExposureTime = CountUpDownLatch() + + inline val savedPath + get() = status.capture.savedPath + + init { + add(cameraExposureTask) + status.exposureTime = cameraExposureTask.request.exposureTime.toNanos() / 1000L + } override fun handleCameraEvent(event: CameraEvent) { - task.handleCameraEvent(event) + cameraExposureTask.handleCameraEvent(event) + } + + override fun accept(event: Any) { + when (event) { + is CameraExposureEvent -> { + status.capture.handleCameraExposureEvent(event) + + if (event is CameraExposureFinished) { + status.capture.send() + + if (!computeOptimalExposureTime(event.savedPath)) { + val exposureTimeInNanos = (exposureMax + exposureMin) / 2L + val request = cameraExposureTask.request.copy(exposureTime = Duration.ofNanos(exposureTimeInNanos)) + status.exposureTime = exposureTimeInNanos / 1000L + add(CameraExposureTask(this, camera, request).also { cameraExposureTask = it }) + } + + waitToComputeOptimalExposureTime.reset() + } + + status.send() + } + } + } + + private fun computeOptimalExposureTime(savedPath: Path): Boolean { + val image = savedPath.fits().use { Image.open(it, false) } + val statistics = STATISTICS.compute(image) + + LOG.debug("flat frame computed. statistics={}", statistics) + + if (statistics.mean in meanRange) { + LOG.debug("found an optimal exposure time. exposureTime={}, path={}", status.exposureTime, savedPath) + status.state = FlatWizardState.CAPTURED + status.capture.state = CameraCaptureState.IDLE + return true + } else if (statistics.mean < meanRange.start) { + exposureMin = cameraExposureTask.request.exposureTime.toNanos() + LOG.debug("captured frame is below mean range. exposureTime={}, path={}", exposureMin, savedPath) + } else { + exposureMax = cameraExposureTask.request.exposureTime.toNanos() + LOG.debug("captured frame is above mean range. exposureTime={}, path={}", exposureMax, savedPath) + } + + val delta = exposureMax - exposureMin + + // 10ms + if (delta < MIN_DELTA_TIME) { + LOG.warn("Failed to find an optimal exposure time. exposureMin={}, exposureMax={}", exposureMin, exposureMax) + status.state = FlatWizardState.FAILED + status.capture.state = CameraCaptureState.IDLE + return true + } + + return false + } + + override fun beforeStart() { + LOG.debug("Flat Wizard started. camera={}, request={}", camera, request) + + status.state = FlatWizardState.EXPOSURING + status.send() + } + + override fun beforeTask(task: Task) { + waitToComputeOptimalExposureTime.countUp() + } + + override fun afterTask(task: Task, exception: Throwable?): Boolean { + if (exception == null) { + waitToComputeOptimalExposureTime.await() + } + + return super.afterTask(task, exception) + } + + override fun afterFinish() { + LOG.debug("Flat Wizard finished. camera={}, request={}, exposureTime={} Β΅s", camera, request, status.exposureTime) + } + + @Suppress("NOTHING_TO_INLINE") + private inline fun MessageEvent.send() { + flatWizardExecutor.accept(this) + } + + companion object { + + private const val MIN_DELTA_TIME = 10000000 // 10ms + + @JvmStatic private val LOG = loggerFor() + @JvmStatic private val STATISTICS = Statistics(noMedian = true, noDeviation = true) } } diff --git a/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardTask.kt b/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardTask.kt deleted file mode 100644 index eba63de24..000000000 --- a/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardTask.kt +++ /dev/null @@ -1,125 +0,0 @@ -package nebulosa.api.wizard.flat - -import nebulosa.api.cameras.* -import nebulosa.api.message.MessageEvent -import nebulosa.api.tasks.AbstractTask -import nebulosa.fits.fits -import nebulosa.image.Image -import nebulosa.image.algorithms.computation.Statistics -import nebulosa.indi.device.camera.Camera -import nebulosa.indi.device.camera.CameraEvent -import nebulosa.indi.device.camera.FrameType -import nebulosa.log.loggerFor -import nebulosa.util.concurrency.cancellation.CancellationToken -import java.nio.file.Path -import java.time.Duration - -data class FlatWizardTask( - @JvmField val camera: Camera, - @JvmField val request: FlatWizardRequest, -) : AbstractTask(), CameraEventAware { - - private val meanTarget = request.meanTarget / 65535f - private val meanRange = (meanTarget * request.meanTolerance / 100f).let { (meanTarget - it)..(meanTarget + it) } - - @Volatile private var cameraCaptureTask: CameraCaptureTask? = null - @Volatile private var exposureMin = request.exposureMin - @Volatile private var exposureMax = request.exposureMax - @Volatile private var exposureTime = Duration.ZERO - - @Volatile private var state = FlatWizardState.IDLE - @Volatile private var capture: CameraCaptureEvent? = null - @Volatile private var savedPath: Path? = null - - override fun handleCameraEvent(event: CameraEvent) { - cameraCaptureTask?.handleCameraEvent(event) - } - - override fun canUseAsLastEvent(event: MessageEvent) = event is FlatWizardEvent - - override fun execute(cancellationToken: CancellationToken) { - while (!cancellationToken.isCancelled) { - val delta = exposureMax.toMillis() - exposureMin.toMillis() - - if (delta < 10) { - LOG.warn("Failed to find an optimal exposure time. exposureMin={}, exposureMax={}", exposureMin, exposureMax) - state = FlatWizardState.FAILED - break - } - - exposureTime = (exposureMax + exposureMin).dividedBy(2L) - - LOG.info("Flat Wizard started. camera={}, request={}, exposureTime={}", camera, request, exposureTime) - - val cameraRequest = request.capture.copy( - exposureTime = exposureTime, frameType = FrameType.FLAT, - autoSave = false, autoSubFolderMode = AutoSubFolderMode.OFF, - ) - - state = FlatWizardState.EXPOSURING - - CameraCaptureTask(camera, cameraRequest).use { - cameraCaptureTask = it - - it.subscribe { event -> - capture = event - - if (event.state == CameraCaptureState.EXPOSURE_FINISHED) { - savedPath = event.savedPath!! - onNext(event) - } - - sendEvent() - } - - it.execute(cancellationToken) - } - - if (cancellationToken.isCancelled) { - state = FlatWizardState.IDLE - break - } else if (savedPath == null) { - state = FlatWizardState.FAILED - break - } - - val image = savedPath!!.fits().use { Image.open(it, false) } - - val statistics = STATISTICS.compute(image) - LOG.info("flat frame captured. exposureTime={}, statistics={}", exposureTime, statistics) - - if (statistics.mean in meanRange) { - state = FlatWizardState.CAPTURED - LOG.info("found an optimal exposure time. exposureTime={}, path={}", exposureTime, savedPath) - break - } else if (statistics.mean < meanRange.start) { - savedPath = null - exposureMin = exposureTime - LOG.info("captured frame is below mean range. exposureTime={}, path={}", exposureTime, savedPath) - } else { - savedPath = null - exposureMax = exposureTime - LOG.info("captured frame is above mean range. exposureTime={}, path={}", exposureTime, savedPath) - } - } - - if (state != FlatWizardState.FAILED && cancellationToken.isCancelled) { - state = FlatWizardState.IDLE - } - - sendEvent() - - LOG.info("Flat Wizard finished. camera={}, request={}, exposureTime={}", camera, request, exposureTime) - } - - @Suppress("NOTHING_TO_INLINE") - private inline fun sendEvent() { - onNext(FlatWizardEvent(state, exposureTime, capture, savedPath)) - } - - companion object { - - @JvmStatic private val LOG = loggerFor() - @JvmStatic private val STATISTICS = Statistics(noMedian = true, noDeviation = true) - } -} diff --git a/api/src/test/kotlin/CameraCaptureNamingFormatterTest.kt b/api/src/test/kotlin/CameraCaptureNamingFormatterTest.kt index ba9b8e1c6..edb8365b8 100644 --- a/api/src/test/kotlin/CameraCaptureNamingFormatterTest.kt +++ b/api/src/test/kotlin/CameraCaptureNamingFormatterTest.kt @@ -263,10 +263,10 @@ class CameraCaptureNamingFormatterTest { override val cfaOffsetX = 0 override val cfaOffsetY = 0 override val cfaType = CfaPattern.RGGB - override val exposureMin: Duration = Duration.ofNanos(1000) - override val exposureMax: Duration = Duration.ofMinutes(10) + override val exposureMin = 1L + override val exposureMax = 1000000L * 60 * 10 override val exposureState = PropertyState.IDLE - override val exposureTime: Duration = Duration.ofSeconds(1) + override val exposureTime = 1000000L override val hasCooler = true override val canSetTemperature = true override val canSubFrame = true @@ -319,7 +319,7 @@ class CameraCaptureNamingFormatterTest { override fun offset(value: Int) = Unit - override fun startCapture(exposureTime: Duration) = Unit + override fun startCapture(exposureTime: Long) = Unit override fun abortCapture() = Unit diff --git a/desktop/src/app/alignment/alignment.component.html b/desktop/src/app/alignment/alignment.component.html index 57dd89ef2..051e404f8 100644 --- a/desktop/src/app/alignment/alignment.component.html +++ b/desktop/src/app/alignment/alignment.component.html @@ -56,7 +56,7 @@ -
+
@@ -262,19 +262,46 @@ [text]="true" />
-
-
- 1. Locate a star near the south meridian and close to declination 0. - 2. Start DARV and wait for routine to complete. - 3. If you see V shaped track, adjust the Azimuth and repeat the step 2 till you get a line. - 4. Locate a star in the eastern or western horizon and close to declination 0. - 5. Start DARV and wait for routine to complete. - 6. If you see V shaped track, adjust the Altitude and repeat the step 5 till you get a line. - 7. Increase the drift time and repeat the step 1 to refine the alignment. -
-
+ + + +
+
+ 1. Choose step duration and speed so that the step size is at least 30 arcmin. + 2. Start TPPA and wait for the azimuth/altitude errors to be displayed. + 3. Repeatedly adjust the Azimuth/Altitude until their values get close to 0. +
+
+
+ +
+
+ 1. Locate a star near the south meridian and close to declination 0. + 2. Start DARV and wait for routine to complete. + 3. If you see V shaped track, adjust the Azimuth and repeat the step 2 till you get a line. + 4. Locate a star in the eastern or western horizon and close to declination 0. + 5. Start DARV and wait for routine to complete. + 6. If you see V shaped track, adjust the Altitude and repeat the step 5 till you get a line. + 7. Increase the drift time and repeat the step 1 to refine the alignment. +
+
+
diff --git a/desktop/src/app/alignment/alignment.component.ts b/desktop/src/app/alignment/alignment.component.ts index e6f45d791..780632253 100644 --- a/desktop/src/app/alignment/alignment.component.ts +++ b/desktop/src/app/alignment/alignment.component.ts @@ -178,7 +178,7 @@ export class AlignmentComponent implements AfterViewInit, OnDestroy, Tickable { this.tppaResult.failed = true } - if (event.capture && event.capture.state !== 'CAPTURE_FINISHED') { + if (event.capture.state !== 'CAPTURE_FINISHED') { this.cameraExposure.handleCameraCaptureEvent(event.capture, true) } }) @@ -274,7 +274,6 @@ export class AlignmentComponent implements AfterViewInit, OnDestroy, Tickable { this.method = 'DARV' this.darvRequest.direction = direction this.darvRequest.reversed = this.preference.darvHemisphere === 'SOUTHERN' - Object.assign(this.tppaRequest.plateSolver, this.preferenceService.settings.get().plateSolver[this.tppaRequest.plateSolver.type]) await this.openCameraImage() await this.api.darvStart(this.camera, this.guideOutput, this.darvRequest) @@ -290,6 +289,7 @@ export class AlignmentComponent implements AfterViewInit, OnDestroy, Tickable { protected async tppaStart() { if (this.camera?.id && this.mount?.id) { this.method = 'TPPA' + Object.assign(this.tppaRequest.plateSolver, this.preferenceService.settings.get().plateSolver[this.tppaRequest.plateSolver.type]) await this.openCameraImage() await this.api.tppaStart(this.camera, this.mount, this.tppaRequest) diff --git a/desktop/src/app/flat-wizard/flat-wizard.component.ts b/desktop/src/app/flat-wizard/flat-wizard.component.ts index 9be7b2260..0c822aadf 100644 --- a/desktop/src/app/flat-wizard/flat-wizard.component.ts +++ b/desktop/src/app/flat-wizard/flat-wizard.component.ts @@ -57,14 +57,16 @@ export class FlatWizardComponent implements AfterViewInit, OnDestroy, Tickable { electronService.on('FLAT_WIZARD.ELAPSED', (event) => { ngZone.run(() => { - if (event.state === 'EXPOSURING' && event.capture && event.capture.camera.id === this.camera?.id) { + if (event.state === 'EXPOSURING' && event.capture && event.camera.id === this.camera?.id) { this.running = true this.cameraExposure.handleCameraCaptureEvent(event.capture, true) } else if (event.state === 'CAPTURED') { this.running = false + this.cameraExposure.reset() this.angularService.message('Flat frame captured') } else if (event.state === 'FAILED') { this.running = false + this.cameraExposure.reset() this.angularService.message('Failed to find an optimal exposure time from given parameters', 'error') } }) diff --git a/desktop/src/app/image/image.component.ts b/desktop/src/app/image/image.component.ts index 9542ca963..74b37d3b9 100644 --- a/desktop/src/app/image/image.component.ts +++ b/desktop/src/app/image/image.component.ts @@ -449,7 +449,6 @@ export class ImageComponent implements AfterViewInit, OnDestroy { await this.closeImage() this.imageData.path = event.savedPath - this.imageData.capture = event.capture this.imageData.exposureCount = event.exposureCount this.liveStacking.path = event.liveStackedPath diff --git a/desktop/src/app/sequencer/sequencer.component.ts b/desktop/src/app/sequencer/sequencer.component.ts index 805b2b4e7..d783390b3 100644 --- a/desktop/src/app/sequencer/sequencer.component.ts +++ b/desktop/src/app/sequencer/sequencer.component.ts @@ -262,12 +262,12 @@ export class SequencerComponent implements AfterContentInit, OnDestroy, Tickable electronService.on('SEQUENCER.ELAPSED', (event) => { ngZone.run(() => { - if (this.running !== event.remainingTime > 0) { - this.enableOrDisableTopbarMenu(event.remainingTime <= 0) + if (this.running !== (event.state !== 'IDLE')) { + this.enableOrDisableTopbarMenu(this.running) } this.event = event - this.running = event.remainingTime > 0 + this.running = event.state !== 'IDLE' const captureEvent = event.capture diff --git a/desktop/src/shared/components/camera-exposure/camera-exposure.component.ts b/desktop/src/shared/components/camera-exposure/camera-exposure.component.ts index 1c51b1a50..31ee71c2c 100644 --- a/desktop/src/shared/components/camera-exposure/camera-exposure.component.ts +++ b/desktop/src/shared/components/camera-exposure/camera-exposure.component.ts @@ -25,11 +25,12 @@ export class CameraExposureComponent { return this.state } - handleCameraCaptureEvent(event: CameraCaptureEvent, looping: boolean = false) { + handleCameraCaptureEvent(event: Omit, looping: boolean = false) { this.capture.elapsedTime = event.captureElapsedTime this.capture.remainingTime = event.captureRemainingTime this.capture.progress = event.captureProgress this.capture.count = event.exposureCount + this.capture.amount = event.exposureAmount if (looping) this.capture.looping = looping this.step.elapsedTime = event.stepElapsedTime this.step.remainingTime = event.stepRemainingTime diff --git a/desktop/src/shared/types/alignment.types.ts b/desktop/src/shared/types/alignment.types.ts index 9bb245529..e8af9ab96 100644 --- a/desktop/src/shared/types/alignment.types.ts +++ b/desktop/src/shared/types/alignment.types.ts @@ -27,7 +27,7 @@ export interface DARVEvent extends MessageEvent { camera: Camera state: DARVState direction?: GuideDirection - capture: CameraCaptureEvent + capture: Omit } export interface TPPAStart { @@ -62,7 +62,7 @@ export interface TPPAEvent extends MessageEvent { totalError: Angle azimuthErrorDirection: string altitudeErrorDirection: string - capture?: CameraCaptureEvent + capture: Omit } export interface DARVResult { diff --git a/desktop/src/shared/types/camera.types.ts b/desktop/src/shared/types/camera.types.ts index cb8eb75bd..95c1ccb72 100644 --- a/desktop/src/shared/types/camera.types.ts +++ b/desktop/src/shared/types/camera.types.ts @@ -131,7 +131,6 @@ export interface CameraCaptureEvent extends MessageEvent { savedPath?: string liveStackedPath?: string state: CameraCaptureState - capture?: CameraStartCapture } export interface CameraDialogInput { diff --git a/desktop/src/shared/types/flat-wizard.types.ts b/desktop/src/shared/types/flat-wizard.types.ts index d7565dfe5..dbb0dd14b 100644 --- a/desktop/src/shared/types/flat-wizard.types.ts +++ b/desktop/src/shared/types/flat-wizard.types.ts @@ -1,4 +1,4 @@ -import { cameraStartCaptureWithDefault, DEFAULT_CAMERA_START_CAPTURE, type CameraCaptureEvent, type CameraStartCapture } from './camera.types' +import { cameraStartCaptureWithDefault, DEFAULT_CAMERA_START_CAPTURE, type Camera, type CameraCaptureEvent, type CameraStartCapture } from './camera.types' export type FlatWizardState = 'EXPOSURING' | 'CAPTURED' | 'FAILED' @@ -15,9 +15,10 @@ export interface FlatWizardRequest { } export interface FlatWizardEvent { + camera: Camera state: FlatWizardState exposureTime: number - capture?: CameraCaptureEvent + capture?: Omit savedPath?: string } diff --git a/desktop/src/shared/types/sequencer.types.ts b/desktop/src/shared/types/sequencer.types.ts index e1b1118ee..207ff3f81 100644 --- a/desktop/src/shared/types/sequencer.types.ts +++ b/desktop/src/shared/types/sequencer.types.ts @@ -23,7 +23,7 @@ export type Sequence = CameraStartCapture export type SequencerCaptureMode = 'FULLY' | 'INTERLEAVED' -export type SequencerState = 'IDLE' | 'PAUSING' | 'PAUSED' | 'RUNNING' +export type SequencerState = 'IDLE' | 'WAITING' | 'PAUSING' | 'PAUSED' | 'RUNNING' export type SequenceProperty = 'EXPOSURE_TIME' | 'EXPOSURE_AMOUNT' | 'EXPOSURE_DELAY' | 'FRAME_TYPE' | 'X' | 'Y' | 'WIDTH' | 'HEIGHT' | 'BIN' | 'FRAME_FORMAT' | 'GAIN' | 'OFFSET' | 'STACKING_GROUP' | 'CALIBRATION_GROUP' @@ -61,11 +61,12 @@ export interface SequencerPlan { } export interface SequencerEvent extends MessageEvent { + camera: Camera id: number elapsedTime: number remainingTime: number progress: number - capture?: CameraCaptureEvent + capture?: Omit state: SequencerState } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 2c3521197..a4b76b953 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 66cd5a0e4..79eb9d003 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/nebulosa-adql/src/main/kotlin/nebulosa/adql/And.kt b/nebulosa-adql/src/main/kotlin/nebulosa/adql/And.kt index df6dbce6c..ceed8e670 100644 --- a/nebulosa-adql/src/main/kotlin/nebulosa/adql/And.kt +++ b/nebulosa-adql/src/main/kotlin/nebulosa/adql/And.kt @@ -3,7 +3,7 @@ package nebulosa.adql import adql.query.constraint.ADQLConstraint import adql.query.constraint.NotConstraint -data class And internal constructor(override val constraint: ADQLConstraint) : WhereConstraint { +data class And(override val constraint: ADQLConstraint) : WhereConstraint { constructor(vararg contraints: WhereConstraint) : this(LogicalConstraintsGroup("AND")) { val logicalConstraint = constraint as LogicalConstraintsGroup diff --git a/nebulosa-adql/src/main/kotlin/nebulosa/adql/Area.kt b/nebulosa-adql/src/main/kotlin/nebulosa/adql/Area.kt index 80f29a592..60a065119 100644 --- a/nebulosa-adql/src/main/kotlin/nebulosa/adql/Area.kt +++ b/nebulosa-adql/src/main/kotlin/nebulosa/adql/Area.kt @@ -3,7 +3,7 @@ package nebulosa.adql import adql.query.operand.function.geometry.AreaFunction import adql.query.operand.function.geometry.GeometryFunction -data class Area internal constructor(override val operand: GeometryFunction) : Operand { +data class Area(override val operand: GeometryFunction) : Operand { constructor(region: Region) : this(AreaFunction(GeometryFunction.GeometryValue(region.operand))) } diff --git a/nebulosa-adql/src/main/kotlin/nebulosa/adql/Between.kt b/nebulosa-adql/src/main/kotlin/nebulosa/adql/Between.kt index 793a2f5cd..76cb419e3 100644 --- a/nebulosa-adql/src/main/kotlin/nebulosa/adql/Between.kt +++ b/nebulosa-adql/src/main/kotlin/nebulosa/adql/Between.kt @@ -3,7 +3,7 @@ package nebulosa.adql import adql.query.constraint.ADQLConstraint import adql.query.constraint.Between as ADQLBetween -data class Between internal constructor(override val constraint: ADQLConstraint) : WhereConstraint { +data class Between(override val constraint: ADQLConstraint) : WhereConstraint { constructor(operand: Operand<*>, min: Operand<*>, max: Operand<*>) : this(ADQLBetween(operand.operand, min.operand, max.operand)) diff --git a/nebulosa-adql/src/main/kotlin/nebulosa/adql/Box.kt b/nebulosa-adql/src/main/kotlin/nebulosa/adql/Box.kt index f1bc9913a..826b2414c 100644 --- a/nebulosa-adql/src/main/kotlin/nebulosa/adql/Box.kt +++ b/nebulosa-adql/src/main/kotlin/nebulosa/adql/Box.kt @@ -5,7 +5,7 @@ import adql.query.operand.function.geometry.BoxFunction import nebulosa.math.Angle import nebulosa.math.toDegrees -data class Box internal constructor(override val operand: BoxFunction) : Region { +data class Box(override val operand: BoxFunction) : Region { constructor(x: Operand<*>, y: Operand<*>, width: Operand<*>, height: Operand<*>) : this(BoxFunction(StringConstant("ICRS"), x.operand, y.operand, width.operand, height.operand)) diff --git a/nebulosa-adql/src/main/kotlin/nebulosa/adql/Circle.kt b/nebulosa-adql/src/main/kotlin/nebulosa/adql/Circle.kt index 37fd52468..a9fea9c5b 100644 --- a/nebulosa-adql/src/main/kotlin/nebulosa/adql/Circle.kt +++ b/nebulosa-adql/src/main/kotlin/nebulosa/adql/Circle.kt @@ -5,7 +5,7 @@ import adql.query.operand.function.geometry.CircleFunction import nebulosa.math.Angle import nebulosa.math.toDegrees -data class Circle internal constructor(override val operand: CircleFunction) : Region { +data class Circle(override val operand: CircleFunction) : Region { constructor(x: Operand<*>, y: Operand<*>, radius: Operand<*>) : this(CircleFunction(StringConstant("ICRS"), x.operand, y.operand, radius.operand)) diff --git a/nebulosa-adql/src/main/kotlin/nebulosa/adql/Column.kt b/nebulosa-adql/src/main/kotlin/nebulosa/adql/Column.kt index 07391d8aa..223ae06d3 100644 --- a/nebulosa-adql/src/main/kotlin/nebulosa/adql/Column.kt +++ b/nebulosa-adql/src/main/kotlin/nebulosa/adql/Column.kt @@ -2,7 +2,7 @@ package nebulosa.adql import adql.query.operand.ADQLColumn -data class Column internal constructor(override val operand: ADQLColumn) : Operand { +data class Column(override val operand: ADQLColumn) : Operand { constructor(columnRef: String) : this(ADQLColumn(columnRef)) diff --git a/nebulosa-adql/src/main/kotlin/nebulosa/adql/Contains.kt b/nebulosa-adql/src/main/kotlin/nebulosa/adql/Contains.kt index 6ff47a3d2..604019ead 100644 --- a/nebulosa-adql/src/main/kotlin/nebulosa/adql/Contains.kt +++ b/nebulosa-adql/src/main/kotlin/nebulosa/adql/Contains.kt @@ -7,7 +7,7 @@ import adql.query.operand.NumericConstant import adql.query.operand.function.geometry.ContainsFunction import adql.query.operand.function.geometry.GeometryFunction -data class Contains internal constructor(override val constraint: ADQLConstraint) : WhereConstraint { +data class Contains(override val constraint: ADQLConstraint) : WhereConstraint { private constructor(left: GeometryFunction.GeometryValue, right: GeometryFunction.GeometryValue) : this( Comparison(ContainsFunction(left, right), ComparisonOperator.EQUAL, NumericConstant(1L)) diff --git a/nebulosa-adql/src/main/kotlin/nebulosa/adql/CrossJoin.kt b/nebulosa-adql/src/main/kotlin/nebulosa/adql/CrossJoin.kt index 3a0eed106..a95cfd63e 100644 --- a/nebulosa-adql/src/main/kotlin/nebulosa/adql/CrossJoin.kt +++ b/nebulosa-adql/src/main/kotlin/nebulosa/adql/CrossJoin.kt @@ -3,7 +3,7 @@ package nebulosa.adql import adql.query.from.ADQLJoin import adql.query.from.CrossJoin as ADQLCrossJoin -data class CrossJoin internal constructor(override val table: ADQLJoin) : Table { +data class CrossJoin(override val table: ADQLJoin) : Table { constructor(left: Table, right: Table) : this(ADQLCrossJoin(left.table, right.table)) } diff --git a/nebulosa-adql/src/main/kotlin/nebulosa/adql/Distance.kt b/nebulosa-adql/src/main/kotlin/nebulosa/adql/Distance.kt index 9940d1174..76fffddb5 100644 --- a/nebulosa-adql/src/main/kotlin/nebulosa/adql/Distance.kt +++ b/nebulosa-adql/src/main/kotlin/nebulosa/adql/Distance.kt @@ -3,7 +3,7 @@ package nebulosa.adql import adql.query.operand.function.geometry.DistanceFunction import adql.query.operand.function.geometry.GeometryFunction -data class Distance internal constructor(override val operand: GeometryFunction) : Operand { +data class Distance(override val operand: GeometryFunction) : Operand { constructor(a: SkyPoint, b: SkyPoint) : this( DistanceFunction( diff --git a/nebulosa-adql/src/main/kotlin/nebulosa/adql/Equal.kt b/nebulosa-adql/src/main/kotlin/nebulosa/adql/Equal.kt index db4e12a2a..3deec3a22 100644 --- a/nebulosa-adql/src/main/kotlin/nebulosa/adql/Equal.kt +++ b/nebulosa-adql/src/main/kotlin/nebulosa/adql/Equal.kt @@ -4,7 +4,7 @@ import adql.query.constraint.ADQLConstraint import adql.query.constraint.Comparison import adql.query.constraint.ComparisonOperator -data class Equal internal constructor(override val constraint: ADQLConstraint) : WhereConstraint { +data class Equal(override val constraint: ADQLConstraint) : WhereConstraint { constructor(left: Operand<*>, right: Operand<*>) : this(Comparison(left.operand, ComparisonOperator.EQUAL, right.operand)) diff --git a/nebulosa-adql/src/main/kotlin/nebulosa/adql/Exists.kt b/nebulosa-adql/src/main/kotlin/nebulosa/adql/Exists.kt index 2dd827da5..ce632ffd7 100644 --- a/nebulosa-adql/src/main/kotlin/nebulosa/adql/Exists.kt +++ b/nebulosa-adql/src/main/kotlin/nebulosa/adql/Exists.kt @@ -4,7 +4,7 @@ import adql.query.constraint.ADQLConstraint import adql.query.constraint.Exists import adql.query.constraint.NotConstraint -data class Exists internal constructor(override val constraint: ADQLConstraint) : WhereConstraint { +data class Exists(override val constraint: ADQLConstraint) : WhereConstraint { constructor(query: Query) : this(Exists(query.query)) diff --git a/nebulosa-adql/src/main/kotlin/nebulosa/adql/From.kt b/nebulosa-adql/src/main/kotlin/nebulosa/adql/From.kt index 0421f945c..68a45919f 100644 --- a/nebulosa-adql/src/main/kotlin/nebulosa/adql/From.kt +++ b/nebulosa-adql/src/main/kotlin/nebulosa/adql/From.kt @@ -2,7 +2,7 @@ package nebulosa.adql import adql.query.from.ADQLTable -data class From internal constructor(override val table: ADQLTable) : Table { +data class From(override val table: ADQLTable) : Table { constructor(table: String) : this(ADQLTable(table)) diff --git a/nebulosa-adql/src/main/kotlin/nebulosa/adql/FullJoin.kt b/nebulosa-adql/src/main/kotlin/nebulosa/adql/FullJoin.kt index b394b7cac..1b3201de0 100644 --- a/nebulosa-adql/src/main/kotlin/nebulosa/adql/FullJoin.kt +++ b/nebulosa-adql/src/main/kotlin/nebulosa/adql/FullJoin.kt @@ -5,7 +5,7 @@ import adql.query.from.ADQLJoin import adql.query.from.OuterJoin import adql.query.from.OuterJoin as ADQLOuterJoin -data class FullJoin internal constructor(override val table: ADQLJoin) : Table { +data class FullJoin(override val table: ADQLJoin) : Table { constructor(left: Table, right: Table) : this(ADQLOuterJoin(left.table, right.table, OuterJoin.OuterType.FULL)) diff --git a/nebulosa-adql/src/main/kotlin/nebulosa/adql/GreaterOrEqual.kt b/nebulosa-adql/src/main/kotlin/nebulosa/adql/GreaterOrEqual.kt index 29b8ab046..29df2bdf4 100644 --- a/nebulosa-adql/src/main/kotlin/nebulosa/adql/GreaterOrEqual.kt +++ b/nebulosa-adql/src/main/kotlin/nebulosa/adql/GreaterOrEqual.kt @@ -4,7 +4,7 @@ import adql.query.constraint.ADQLConstraint import adql.query.constraint.Comparison import adql.query.constraint.ComparisonOperator -data class GreaterOrEqual internal constructor(override val constraint: ADQLConstraint) : WhereConstraint { +data class GreaterOrEqual(override val constraint: ADQLConstraint) : WhereConstraint { constructor(left: Operand<*>, right: Operand<*>) : this(Comparison(left.operand, ComparisonOperator.GREATER_OR_EQUAL, right.operand)) diff --git a/nebulosa-adql/src/main/kotlin/nebulosa/adql/GreaterThan.kt b/nebulosa-adql/src/main/kotlin/nebulosa/adql/GreaterThan.kt index 3478550b0..d6097fd4e 100644 --- a/nebulosa-adql/src/main/kotlin/nebulosa/adql/GreaterThan.kt +++ b/nebulosa-adql/src/main/kotlin/nebulosa/adql/GreaterThan.kt @@ -4,7 +4,7 @@ import adql.query.constraint.ADQLConstraint import adql.query.constraint.Comparison import adql.query.constraint.ComparisonOperator -data class GreaterThan internal constructor(override val constraint: ADQLConstraint) : WhereConstraint { +data class GreaterThan(override val constraint: ADQLConstraint) : WhereConstraint { constructor(left: Operand<*>, right: Operand<*>) : this(Comparison(left.operand, ComparisonOperator.GREATER_THAN, right.operand)) diff --git a/nebulosa-adql/src/main/kotlin/nebulosa/adql/In.kt b/nebulosa-adql/src/main/kotlin/nebulosa/adql/In.kt index 8a5e26368..069aadd53 100644 --- a/nebulosa-adql/src/main/kotlin/nebulosa/adql/In.kt +++ b/nebulosa-adql/src/main/kotlin/nebulosa/adql/In.kt @@ -5,7 +5,7 @@ import adql.query.constraint.ADQLConstraint import adql.query.operand.ADQLOperand import adql.query.constraint.In as ADQLIn -data class In internal constructor(override val constraint: ADQLConstraint) : WhereConstraint { +data class In(override val constraint: ADQLConstraint) : WhereConstraint { constructor(operand: Operand<*>, values: Array>) : this(ADQLIn(operand.operand, values.list())) diff --git a/nebulosa-adql/src/main/kotlin/nebulosa/adql/InnerJoin.kt b/nebulosa-adql/src/main/kotlin/nebulosa/adql/InnerJoin.kt index ada4021df..d95ad5e86 100644 --- a/nebulosa-adql/src/main/kotlin/nebulosa/adql/InnerJoin.kt +++ b/nebulosa-adql/src/main/kotlin/nebulosa/adql/InnerJoin.kt @@ -4,7 +4,7 @@ import adql.query.ClauseConstraints import adql.query.from.ADQLJoin import adql.query.from.InnerJoin as ADQLInnerJoin -data class InnerJoin internal constructor(override val table: ADQLJoin) : Table { +data class InnerJoin(override val table: ADQLJoin) : Table { constructor(left: Table, right: Table) : this(ADQLInnerJoin(left.table, right.table)) diff --git a/nebulosa-adql/src/main/kotlin/nebulosa/adql/IsNotNull.kt b/nebulosa-adql/src/main/kotlin/nebulosa/adql/IsNotNull.kt index 2300ac598..90f9fc40a 100644 --- a/nebulosa-adql/src/main/kotlin/nebulosa/adql/IsNotNull.kt +++ b/nebulosa-adql/src/main/kotlin/nebulosa/adql/IsNotNull.kt @@ -4,7 +4,7 @@ import adql.query.constraint.ADQLConstraint import adql.query.operand.ADQLColumn import adql.query.constraint.IsNull as ADQLIsNull -data class IsNotNull internal constructor(override val constraint: ADQLConstraint) : WhereConstraint { +data class IsNotNull(override val constraint: ADQLConstraint) : WhereConstraint { constructor(operand: Operand) : this(ADQLIsNull(operand.operand, true)) diff --git a/nebulosa-adql/src/main/kotlin/nebulosa/adql/IsNull.kt b/nebulosa-adql/src/main/kotlin/nebulosa/adql/IsNull.kt index 69e937000..c2af9678f 100644 --- a/nebulosa-adql/src/main/kotlin/nebulosa/adql/IsNull.kt +++ b/nebulosa-adql/src/main/kotlin/nebulosa/adql/IsNull.kt @@ -4,7 +4,7 @@ import adql.query.constraint.ADQLConstraint import adql.query.operand.ADQLColumn import adql.query.constraint.IsNull as ADQLIsNull -data class IsNull internal constructor(override val constraint: ADQLConstraint) : WhereConstraint { +data class IsNull(override val constraint: ADQLConstraint) : WhereConstraint { constructor(operand: Operand) : this(ADQLIsNull(operand.operand)) diff --git a/nebulosa-adql/src/main/kotlin/nebulosa/adql/LeftJoin.kt b/nebulosa-adql/src/main/kotlin/nebulosa/adql/LeftJoin.kt index f36fbcea7..124af6c6d 100644 --- a/nebulosa-adql/src/main/kotlin/nebulosa/adql/LeftJoin.kt +++ b/nebulosa-adql/src/main/kotlin/nebulosa/adql/LeftJoin.kt @@ -5,7 +5,7 @@ import adql.query.from.ADQLJoin import adql.query.from.OuterJoin import adql.query.from.OuterJoin as ADQLOuterJoin -data class LeftJoin internal constructor(override val table: ADQLJoin) : Table { +data class LeftJoin(override val table: ADQLJoin) : Table { constructor(left: Table, right: Table) : this(ADQLOuterJoin(left.table, right.table, OuterJoin.OuterType.LEFT)) diff --git a/nebulosa-adql/src/main/kotlin/nebulosa/adql/LessOrEqual.kt b/nebulosa-adql/src/main/kotlin/nebulosa/adql/LessOrEqual.kt index fa0eeeff0..6452d4f11 100644 --- a/nebulosa-adql/src/main/kotlin/nebulosa/adql/LessOrEqual.kt +++ b/nebulosa-adql/src/main/kotlin/nebulosa/adql/LessOrEqual.kt @@ -4,7 +4,7 @@ import adql.query.constraint.ADQLConstraint import adql.query.constraint.Comparison import adql.query.constraint.ComparisonOperator -data class LessOrEqual internal constructor(override val constraint: ADQLConstraint) : WhereConstraint { +data class LessOrEqual(override val constraint: ADQLConstraint) : WhereConstraint { constructor(left: Operand<*>, right: Operand<*>) : this(Comparison(left.operand, ComparisonOperator.LESS_OR_EQUAL, right.operand)) diff --git a/nebulosa-adql/src/main/kotlin/nebulosa/adql/LessThan.kt b/nebulosa-adql/src/main/kotlin/nebulosa/adql/LessThan.kt index 46d5c3d34..fbfff2bcf 100644 --- a/nebulosa-adql/src/main/kotlin/nebulosa/adql/LessThan.kt +++ b/nebulosa-adql/src/main/kotlin/nebulosa/adql/LessThan.kt @@ -4,7 +4,7 @@ import adql.query.constraint.ADQLConstraint import adql.query.constraint.Comparison import adql.query.constraint.ComparisonOperator -data class LessThan internal constructor(override val constraint: ADQLConstraint) : WhereConstraint { +data class LessThan(override val constraint: ADQLConstraint) : WhereConstraint { constructor(left: Operand<*>, right: Operand<*>) : this(Comparison(left.operand, ComparisonOperator.LESS_THAN, right.operand)) diff --git a/nebulosa-adql/src/main/kotlin/nebulosa/adql/Like.kt b/nebulosa-adql/src/main/kotlin/nebulosa/adql/Like.kt index 6eeb76501..1a21c5d3e 100644 --- a/nebulosa-adql/src/main/kotlin/nebulosa/adql/Like.kt +++ b/nebulosa-adql/src/main/kotlin/nebulosa/adql/Like.kt @@ -4,7 +4,7 @@ import adql.query.constraint.ADQLConstraint import adql.query.constraint.Comparison import adql.query.constraint.ComparisonOperator -data class Like internal constructor(override val constraint: ADQLConstraint) : WhereConstraint { +data class Like(override val constraint: ADQLConstraint) : WhereConstraint { constructor(left: Operand<*>, right: Operand<*>) : this(Comparison(left.operand, ComparisonOperator.LIKE, right.operand)) diff --git a/nebulosa-adql/src/main/kotlin/nebulosa/adql/Literal.kt b/nebulosa-adql/src/main/kotlin/nebulosa/adql/Literal.kt index 5d6626f0f..600679418 100644 --- a/nebulosa-adql/src/main/kotlin/nebulosa/adql/Literal.kt +++ b/nebulosa-adql/src/main/kotlin/nebulosa/adql/Literal.kt @@ -5,7 +5,7 @@ import adql.query.operand.NegativeOperand import adql.query.operand.NumericConstant import adql.query.operand.StringConstant -data class Literal internal constructor(override val operand: ADQLOperand) : Operand { +data class Literal(override val operand: ADQLOperand) : Operand { constructor(value: String) : this(StringConstant(value)) diff --git a/nebulosa-adql/src/main/kotlin/nebulosa/adql/NotAnd.kt b/nebulosa-adql/src/main/kotlin/nebulosa/adql/NotAnd.kt index 540e28f51..8493c9b36 100644 --- a/nebulosa-adql/src/main/kotlin/nebulosa/adql/NotAnd.kt +++ b/nebulosa-adql/src/main/kotlin/nebulosa/adql/NotAnd.kt @@ -3,7 +3,7 @@ package nebulosa.adql import adql.query.constraint.ADQLConstraint import adql.query.constraint.NotConstraint -data class NotAnd internal constructor(override val constraint: ADQLConstraint) : WhereConstraint { +data class NotAnd(override val constraint: ADQLConstraint) : WhereConstraint { constructor(vararg contraints: WhereConstraint) : this(NotConstraint(LogicalConstraintsGroup("AND"))) { val logicalConstraint = constraint as LogicalConstraintsGroup diff --git a/nebulosa-adql/src/main/kotlin/nebulosa/adql/NotBetween.kt b/nebulosa-adql/src/main/kotlin/nebulosa/adql/NotBetween.kt index 94637fdf5..9aa123e9e 100644 --- a/nebulosa-adql/src/main/kotlin/nebulosa/adql/NotBetween.kt +++ b/nebulosa-adql/src/main/kotlin/nebulosa/adql/NotBetween.kt @@ -3,7 +3,7 @@ package nebulosa.adql import adql.query.constraint.ADQLConstraint import adql.query.constraint.Between as ADQLBetween -data class NotBetween internal constructor(override val constraint: ADQLConstraint) : WhereConstraint { +data class NotBetween(override val constraint: ADQLConstraint) : WhereConstraint { constructor(operand: Operand<*>, min: Operand<*>, max: Operand<*>) : this(ADQLBetween(operand.operand, min.operand, max.operand, true)) diff --git a/nebulosa-adql/src/main/kotlin/nebulosa/adql/NotContains.kt b/nebulosa-adql/src/main/kotlin/nebulosa/adql/NotContains.kt index 19a8ec188..e28b82e56 100644 --- a/nebulosa-adql/src/main/kotlin/nebulosa/adql/NotContains.kt +++ b/nebulosa-adql/src/main/kotlin/nebulosa/adql/NotContains.kt @@ -7,7 +7,7 @@ import adql.query.operand.NumericConstant import adql.query.operand.function.geometry.ContainsFunction import adql.query.operand.function.geometry.GeometryFunction -data class NotContains internal constructor(override val constraint: ADQLConstraint) : WhereConstraint { +data class NotContains(override val constraint: ADQLConstraint) : WhereConstraint { private constructor(left: GeometryFunction.GeometryValue, right: GeometryFunction.GeometryValue) : this( Comparison(ContainsFunction(left, right), ComparisonOperator.EQUAL, NumericConstant(0L)) diff --git a/nebulosa-adql/src/main/kotlin/nebulosa/adql/NotEqual.kt b/nebulosa-adql/src/main/kotlin/nebulosa/adql/NotEqual.kt index 47aed92f5..cf235855a 100644 --- a/nebulosa-adql/src/main/kotlin/nebulosa/adql/NotEqual.kt +++ b/nebulosa-adql/src/main/kotlin/nebulosa/adql/NotEqual.kt @@ -4,7 +4,7 @@ import adql.query.constraint.ADQLConstraint import adql.query.constraint.Comparison import adql.query.constraint.ComparisonOperator -data class NotEqual internal constructor(override val constraint: ADQLConstraint) : WhereConstraint { +data class NotEqual(override val constraint: ADQLConstraint) : WhereConstraint { constructor(left: Operand<*>, right: Operand<*>) : this(Comparison(left.operand, ComparisonOperator.NOT_EQUAL, right.operand)) diff --git a/nebulosa-adql/src/main/kotlin/nebulosa/adql/NotExists.kt b/nebulosa-adql/src/main/kotlin/nebulosa/adql/NotExists.kt index d2ef96890..5389aa6d1 100644 --- a/nebulosa-adql/src/main/kotlin/nebulosa/adql/NotExists.kt +++ b/nebulosa-adql/src/main/kotlin/nebulosa/adql/NotExists.kt @@ -4,7 +4,7 @@ import adql.query.constraint.ADQLConstraint import adql.query.constraint.NotConstraint import adql.query.constraint.Exists as ADQLExists -data class NotExists internal constructor(override val constraint: ADQLConstraint) : WhereConstraint { +data class NotExists(override val constraint: ADQLConstraint) : WhereConstraint { constructor(query: Query) : this(NotConstraint(ADQLExists(query.query))) diff --git a/nebulosa-adql/src/main/kotlin/nebulosa/adql/NotIn.kt b/nebulosa-adql/src/main/kotlin/nebulosa/adql/NotIn.kt index 85e536ddf..59b0da06c 100644 --- a/nebulosa-adql/src/main/kotlin/nebulosa/adql/NotIn.kt +++ b/nebulosa-adql/src/main/kotlin/nebulosa/adql/NotIn.kt @@ -5,7 +5,7 @@ import adql.query.constraint.ADQLConstraint import adql.query.operand.ADQLOperand import adql.query.constraint.In as ADQLIn -data class NotIn internal constructor(override val constraint: ADQLConstraint) : WhereConstraint { +data class NotIn(override val constraint: ADQLConstraint) : WhereConstraint { constructor(operand: Operand<*>, values: Array>) : this(ADQLIn(operand.operand, values.list(), true)) diff --git a/nebulosa-adql/src/main/kotlin/nebulosa/adql/NotLike.kt b/nebulosa-adql/src/main/kotlin/nebulosa/adql/NotLike.kt index 2adb29559..aecb3b2ee 100644 --- a/nebulosa-adql/src/main/kotlin/nebulosa/adql/NotLike.kt +++ b/nebulosa-adql/src/main/kotlin/nebulosa/adql/NotLike.kt @@ -4,7 +4,7 @@ import adql.query.constraint.ADQLConstraint import adql.query.constraint.Comparison import adql.query.constraint.ComparisonOperator -data class NotLike internal constructor(override val constraint: ADQLConstraint) : WhereConstraint { +data class NotLike(override val constraint: ADQLConstraint) : WhereConstraint { constructor(left: Operand<*>, right: Operand<*>) : this(Comparison(left.operand, ComparisonOperator.NOTLIKE, right.operand)) diff --git a/nebulosa-adql/src/main/kotlin/nebulosa/adql/NotOr.kt b/nebulosa-adql/src/main/kotlin/nebulosa/adql/NotOr.kt index f03957d70..637b7ad3b 100644 --- a/nebulosa-adql/src/main/kotlin/nebulosa/adql/NotOr.kt +++ b/nebulosa-adql/src/main/kotlin/nebulosa/adql/NotOr.kt @@ -3,7 +3,7 @@ package nebulosa.adql import adql.query.constraint.ADQLConstraint import adql.query.constraint.NotConstraint -data class NotOr internal constructor(override val constraint: ADQLConstraint) : WhereConstraint { +data class NotOr(override val constraint: ADQLConstraint) : WhereConstraint { constructor(vararg contraints: WhereConstraint) : this(NotConstraint(LogicalConstraintsGroup("OR"))) { val logicalConstraint = constraint as LogicalConstraintsGroup diff --git a/nebulosa-adql/src/main/kotlin/nebulosa/adql/Or.kt b/nebulosa-adql/src/main/kotlin/nebulosa/adql/Or.kt index bdf974e8e..b77fe1e9c 100644 --- a/nebulosa-adql/src/main/kotlin/nebulosa/adql/Or.kt +++ b/nebulosa-adql/src/main/kotlin/nebulosa/adql/Or.kt @@ -3,7 +3,7 @@ package nebulosa.adql import adql.query.constraint.ADQLConstraint import adql.query.constraint.NotConstraint -data class Or internal constructor(override val constraint: ADQLConstraint) : WhereConstraint { +data class Or(override val constraint: ADQLConstraint) : WhereConstraint { constructor(vararg contraints: WhereConstraint) : this(LogicalConstraintsGroup("OR")) { val logicalConstraint = constraint as LogicalConstraintsGroup diff --git a/nebulosa-adql/src/main/kotlin/nebulosa/adql/Polygon.kt b/nebulosa-adql/src/main/kotlin/nebulosa/adql/Polygon.kt index 58aa163cf..bf03d6f4e 100644 --- a/nebulosa-adql/src/main/kotlin/nebulosa/adql/Polygon.kt +++ b/nebulosa-adql/src/main/kotlin/nebulosa/adql/Polygon.kt @@ -4,7 +4,7 @@ import adql.query.operand.NumericConstant import adql.query.operand.function.geometry.PolygonFunction import nebulosa.math.toDegrees -data class Polygon internal constructor(override val operand: PolygonFunction) : Region { +data class Polygon(override val operand: PolygonFunction) : Region { constructor(points: DoubleArray) : this(PolygonFunction(Region.ICRS, points.map { NumericConstant(it.toDegrees) })) } diff --git a/nebulosa-adql/src/main/kotlin/nebulosa/adql/Query.kt b/nebulosa-adql/src/main/kotlin/nebulosa/adql/Query.kt index 7504c91d5..69f871429 100644 --- a/nebulosa-adql/src/main/kotlin/nebulosa/adql/Query.kt +++ b/nebulosa-adql/src/main/kotlin/nebulosa/adql/Query.kt @@ -2,7 +2,7 @@ package nebulosa.adql import adql.query.ADQLQuery -data class Query internal constructor(@JvmField internal val query: ADQLQuery) : CharSequence { +data class Query(@JvmField internal val query: ADQLQuery) : CharSequence { private val queryText: String by lazy(query::toADQL) diff --git a/nebulosa-adql/src/main/kotlin/nebulosa/adql/RightJoin.kt b/nebulosa-adql/src/main/kotlin/nebulosa/adql/RightJoin.kt index f4f39c6e1..788adce96 100644 --- a/nebulosa-adql/src/main/kotlin/nebulosa/adql/RightJoin.kt +++ b/nebulosa-adql/src/main/kotlin/nebulosa/adql/RightJoin.kt @@ -5,7 +5,7 @@ import adql.query.from.ADQLJoin import adql.query.from.OuterJoin import adql.query.from.OuterJoin as ADQLOuterJoin -data class RightJoin internal constructor(override val table: ADQLJoin) : Table { +data class RightJoin(override val table: ADQLJoin) : Table { constructor(left: Table, right: Table) : this(ADQLOuterJoin(left.table, right.table, OuterJoin.OuterType.RIGHT)) diff --git a/nebulosa-adql/src/main/kotlin/nebulosa/adql/SkyPoint.kt b/nebulosa-adql/src/main/kotlin/nebulosa/adql/SkyPoint.kt index b8e9b554d..18f0eebe9 100644 --- a/nebulosa-adql/src/main/kotlin/nebulosa/adql/SkyPoint.kt +++ b/nebulosa-adql/src/main/kotlin/nebulosa/adql/SkyPoint.kt @@ -5,7 +5,7 @@ import adql.query.operand.function.geometry.PointFunction import nebulosa.math.Angle import nebulosa.math.toDegrees -data class SkyPoint internal constructor(override val operand: PointFunction) : Region { +data class SkyPoint(override val operand: PointFunction) : Region { constructor(x: Operand<*>, y: Operand<*>) : this(PointFunction(StringConstant("ICRS"), x.operand, y.operand)) diff --git a/nebulosa-adql/src/main/kotlin/nebulosa/adql/SortBy.kt b/nebulosa-adql/src/main/kotlin/nebulosa/adql/SortBy.kt index ec9b1a424..3c1bd1929 100644 --- a/nebulosa-adql/src/main/kotlin/nebulosa/adql/SortBy.kt +++ b/nebulosa-adql/src/main/kotlin/nebulosa/adql/SortBy.kt @@ -2,7 +2,7 @@ package nebulosa.adql import adql.query.ADQLOrder -data class SortBy internal constructor(val order: ADQLOrder) : QueryClause { +data class SortBy(val order: ADQLOrder) : QueryClause { constructor(column: Column, direction: SortDirection = SortDirection.ASCENDING) : this(ADQLOrder(column.operand.columnName, direction == SortDirection.DESCENDING)) diff --git a/nebulosa-alignment/src/main/kotlin/nebulosa/alignment/polar/point/three/ThreePointPolarAlignment.kt b/nebulosa-alignment/src/main/kotlin/nebulosa/alignment/polar/point/three/ThreePointPolarAlignment.kt index e221cde71..8ecd32ee2 100644 --- a/nebulosa-alignment/src/main/kotlin/nebulosa/alignment/polar/point/three/ThreePointPolarAlignment.kt +++ b/nebulosa-alignment/src/main/kotlin/nebulosa/alignment/polar/point/three/ThreePointPolarAlignment.kt @@ -56,19 +56,13 @@ data class ThreePointPolarAlignment( compensateRefraction: Boolean = false, cancellationToken: CancellationToken = CancellationToken.NONE, ): ThreePointPolarAlignmentResult { - if (cancellationToken.isCancelled) { - return Cancelled - } - val solution = try { solver.solve(path, null, rightAscension, declination, radius, cancellationToken = cancellationToken) } catch (e: PlateSolverException) { return NoPlateSolution(e) } - if (cancellationToken.isCancelled) { - return Cancelled - } else if (!solution.solved) { + if (!solution.solved) { return NoPlateSolution(null) } else { val time = UTC.now() diff --git a/nebulosa-alignment/src/main/kotlin/nebulosa/alignment/polar/point/three/ThreePointPolarAlignmentResult.kt b/nebulosa-alignment/src/main/kotlin/nebulosa/alignment/polar/point/three/ThreePointPolarAlignmentResult.kt index bfa8cfc28..a3c28152c 100644 --- a/nebulosa-alignment/src/main/kotlin/nebulosa/alignment/polar/point/three/ThreePointPolarAlignmentResult.kt +++ b/nebulosa-alignment/src/main/kotlin/nebulosa/alignment/polar/point/three/ThreePointPolarAlignmentResult.kt @@ -13,6 +13,4 @@ sealed interface ThreePointPolarAlignmentResult { ) : ThreePointPolarAlignmentResult data class NoPlateSolution(@JvmField val exception: PlateSolverException?) : ThreePointPolarAlignmentResult - - data object Cancelled : ThreePointPolarAlignmentResult } diff --git a/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/DateTimeResponse.kt b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/DateTimeResponse.kt index 168e49293..b29639569 100644 --- a/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/DateTimeResponse.kt +++ b/nebulosa-alpaca-api/src/main/kotlin/nebulosa/alpaca/api/DateTimeResponse.kt @@ -2,13 +2,13 @@ package nebulosa.alpaca.api import com.fasterxml.jackson.annotation.JsonFormat import com.fasterxml.jackson.annotation.JsonProperty -import java.time.LocalDateTime +import java.time.OffsetDateTime data class DateTimeResponse( @field:JsonProperty("ClientTransactionID") override val clientTransactionID: Int = 0, @field:JsonProperty("ServerTransactionID") override val serverTransactionID: Int = 0, @field:JsonProperty("ErrorNumber") override val errorNumber: Int = 0, @field:JsonProperty("ErrorMessage") override val errorMessage: String = "", - @field:JsonProperty("Value") @field:JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss[.SSS]") - override val value: LocalDateTime = LocalDateTime.now(), -) : AlpacaResponse + @field:JsonProperty("Value") @field:JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss[.SSS][XXX]") + override val value: OffsetDateTime = OffsetDateTime.MIN, +) : AlpacaResponse 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 6aec88d40..0f01c88ff 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 @@ -5,8 +5,8 @@ import nebulosa.alpaca.api.AlpacaResponse import nebulosa.alpaca.api.ConfiguredDevice import nebulosa.alpaca.indi.client.AlpacaClient import nebulosa.indi.device.* -import nebulosa.log.debug import nebulosa.log.loggerFor +import nebulosa.time.SystemClock import nebulosa.util.Resettable import nebulosa.util.time.Stopwatch import retrofit2.Call @@ -90,20 +90,19 @@ abstract class ASCOMDevice : Device, Resettable { val body = response.body() return if (body == null) { - LOG.debug { "response has no body. device=%s, request=%s %s, response=%s".format(name, request.method, request.url, response) } + LOG.debug("response has no body. device={}, request={} {}, response={}", name, request.method, request.url, response) null } else if (body.errorNumber != 0) { val message = body.errorMessage if (message.isNotEmpty()) { - addMessageAndFireEvent("[%s]: %s".format(LocalDateTime.now(), message)) + addMessageAndFireEvent("[%s]: %s".format(LocalDateTime.now(SystemClock), message)) } - LOG.debug { - "unsuccessful response. device=%s, request=%s %s, errorNumber=%s, message=%s".format( - name, request.method, request.url, body.errorNumber, body.errorMessage - ) - } + LOG.debug( + "unsuccessful response. device={}, request={} {}, errorNumber={}, message={}", + name, request.method, request.url, body.errorNumber, body.errorMessage + ) null } else { 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 9659ca949..ce85b786f 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 @@ -13,7 +13,6 @@ import nebulosa.image.format.FloatImageData 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.guider.GuideOutputPulsingChanged @@ -58,10 +57,10 @@ data class ASCOMCamera( @Volatile final override var cfaOffsetX = 0 @Volatile final override var cfaOffsetY = 0 @Volatile final override var cfaType = CfaPattern.RGGB - @Volatile final override var exposureMin: Duration = Duration.ZERO - @Volatile final override var exposureMax: Duration = Duration.ZERO + @Volatile final override var exposureMin = 0L + @Volatile final override var exposureMax = 0L @Volatile final override var exposureState = PropertyState.IDLE - @Volatile final override var exposureTime: Duration = Duration.ZERO + @Volatile final override var exposureTime = 0L @Volatile final override var hasCooler = false @Volatile final override var canSetTemperature = false @Volatile final override var canSubFrame = true @@ -157,11 +156,11 @@ data class ASCOMCamera( service.offset(device.number, value).doRequest() } - override fun startCapture(exposureTime: Duration) { + override fun startCapture(exposureTime: Long) { if (!exposuring) { this.exposureTime = exposureTime - service.startExposure(device.number, exposureTime.toNanos() / NANO_TO_SECONDS, frameType == FrameType.LIGHT).doRequest { + service.startExposure(device.number, exposureTime.toDouble() / MICROS_TO_SECONDS, frameType == FrameType.LIGHT).doRequest { imageReadyWaiter.captureStarted(exposureTime) } } @@ -242,10 +241,10 @@ data class ASCOMCamera( cfaOffsetX = 0 cfaOffsetY = 0 cfaType = CfaPattern.RGGB - exposureMin = Duration.ZERO - exposureMax = Duration.ZERO + exposureMin = 0L + exposureMax = 0L exposureState = PropertyState.IDLE - exposureTime = Duration.ZERO + exposureTime = 0L hasCooler = false canSetTemperature = false canSubFrame = true @@ -356,9 +355,8 @@ data class ASCOMCamera( if (exposuring) { service.percentCompleted(device.number).doRequest { - val exposureTimeInNanoseconds = imageReadyWaiter.exposureTime.toNanos() - val progressedExposureTime = (exposureTimeInNanoseconds * it.value) / 100 - exposureTime = Duration.ofNanos(exposureTimeInNanoseconds - progressedExposureTime) + val progressedExposureTime = (imageReadyWaiter.exposureTime * it.value) / 100 + this.exposureTime = imageReadyWaiter.exposureTime - progressedExposureTime sender.fireOnEventReceived(CameraExposureProgressChanged(this)) } @@ -536,8 +534,8 @@ data class ASCOMCamera( private fun processExposureMinMax() { service.exposureMin(device.number).doRequest { min -> service.exposureMax(device.number).doRequest { max -> - exposureMin = Duration.ofNanos((min.value * NANO_TO_SECONDS).toLong()) - exposureMax = Duration.ofNanos((max.value * NANO_TO_SECONDS).toLong()) + exposureMin = (min.value * MICROS_TO_SECONDS).toLong() + exposureMax = (max.value * MICROS_TO_SECONDS).toLong() sender.fireOnEventReceived(CameraExposureMinMaxChanged(this)) } @@ -601,7 +599,7 @@ data class ASCOMCamera( } } - private fun readImage(exposureTime: Duration) { + private fun readImage(exposureTime: Long) { service.imageArray(device.number).execute().body()?.use { body -> val stream = body.byteStream() val metadata = ImageMetadata.from(stream.readNBytes(44)) @@ -646,7 +644,7 @@ data class ASCOMCamera( if (numberOfChannels == 3) header.add(FitsKeyword.NAXIS3, numberOfChannels) header.add(FitsKeyword.EXTEND, true) header.add(FitsKeyword.INSTRUME, name) - val exposureTimeInSeconds = exposureTime.toNanos() / NANO_TO_SECONDS + val exposureTimeInSeconds = exposureTime.toDouble() / MICROS_TO_SECONDS header.add(FitsKeyword.EXPTIME, exposureTimeInSeconds) header.add(FitsKeyword.EXPOSURE, exposureTimeInSeconds) if (hasThermometer) header.add(FitsKeyword.CCD_TEMP, temperature) @@ -753,13 +751,13 @@ data class ASCOMCamera( private val latch = CountUpDownLatch(1) private val aborted = AtomicBoolean() - @Volatile @JvmField var exposureTime: Duration = Duration.ZERO + @Volatile @JvmField var exposureTime = 0L init { isDaemon = true } - fun captureStarted(exposureTime: Duration) { + fun captureStarted(exposureTime: Long) { this.exposureTime = exposureTime aborted.set(false) latch.reset() @@ -816,6 +814,8 @@ data class ASCOMCamera( companion object { + private const val MICROS_TO_SECONDS = 1_000_000L + @JvmStatic private val LOG = loggerFor() @JvmStatic private val DATE_OBS_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS") @JvmStatic private val RA_FORMAT = AngleFormatter.HMS.newBuilder().secondsDecimalPlaces(3).separators(" ").build() diff --git a/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/device/mounts/ASCOMMount.kt b/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/device/mounts/ASCOMMount.kt index 9a14a6d6e..7f28824b8 100644 --- a/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/device/mounts/ASCOMMount.kt +++ b/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/device/mounts/ASCOMMount.kt @@ -425,7 +425,7 @@ data class ASCOMMount( private fun processDateTime() { service.utcDate(device.number).doRequest { - dateTime = it.value.atOffset(ZoneOffset.UTC) + dateTime = it.value sender.fireOnEventReceived(MountTimeChanged(this)) } } diff --git a/nebulosa-astrometrynet/src/main/kotlin/nebulosa/astrometrynet/nova/NovaAstrometryNetService.kt b/nebulosa-astrometrynet/src/main/kotlin/nebulosa/astrometrynet/nova/NovaAstrometryNetService.kt index b3cf6590e..9162d0ff7 100644 --- a/nebulosa-astrometrynet/src/main/kotlin/nebulosa/astrometrynet/nova/NovaAstrometryNetService.kt +++ b/nebulosa-astrometrynet/src/main/kotlin/nebulosa/astrometrynet/nova/NovaAstrometryNetService.kt @@ -26,20 +26,20 @@ class NovaAstrometryNetService( fun login(apiKey: String): Call { return FormBody.Builder() - .add("request-json", jsonMapper.writeValueAsString(mapOf("apikey" to apiKey))) + .add("request-json", objectMapper.writeValueAsString(mapOf("apikey" to apiKey))) .build() .let(service::login) } fun uploadFromUrl(upload: Upload): Call { return FormBody.Builder() - .add("request-json", jsonMapper.writeValueAsString(upload)) + .add("request-json", objectMapper.writeValueAsString(upload)) .build() .let(service::uploadFromUrl) } fun uploadFromFile(path: Path, upload: Upload): Call { - val requestJsonBody = jsonMapper.writeValueAsBytes(upload).toRequestBody(TEXT_PLAIN_MEDIA_TYPE) + val requestJsonBody = objectMapper.writeValueAsBytes(upload).toRequestBody(TEXT_PLAIN_MEDIA_TYPE) val fileName = "%s.%s".format(UUID.randomUUID(), path.extension) val fileBody = path.toFile().asRequestBody(OCTET_STREAM_MEDIA_TYPE) @@ -54,7 +54,7 @@ class NovaAstrometryNetService( } fun uploadFromImage(image: Image, upload: Upload): Call { - val requestJsonBody = jsonMapper.writeValueAsBytes(upload).toRequestBody(TEXT_PLAIN_MEDIA_TYPE) + val requestJsonBody = objectMapper.writeValueAsBytes(upload).toRequestBody(TEXT_PLAIN_MEDIA_TYPE) val fileName = "%s.fits".format(UUID.randomUUID()) val fileBody = object : RequestBody() { diff --git a/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/AxisStats.kt b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/AxisStats.kt index 516b64d9a..e0a9f4173 100644 --- a/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/AxisStats.kt +++ b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/AxisStats.kt @@ -75,7 +75,7 @@ internal open class AxisStats { get() = guidingEntries.size val lastEntry - get() = if (count > 0) guidingEntries.last!! else StarDisplacement.ZERO + get() = if (count > 0) guidingEntries.last() else StarDisplacement.ZERO /** * Adds a guiding info element of relative time, diff --git a/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/GuideGraph.kt b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/GuideGraph.kt index f040cf0d4..af916c4af 100644 --- a/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/GuideGraph.kt +++ b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/GuideGraph.kt @@ -42,7 +42,7 @@ internal class GuideGraph( // Update counter for osc index. if (nr >= 1) { - if (offset.mount.x * last.ra > 0.0) { + if (offset.mount.x * last().ra > 0.0) { raSameSides++ } diff --git a/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/WindowedAxisStats.kt b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/WindowedAxisStats.kt index 2b75c6010..7d369424f 100644 --- a/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/WindowedAxisStats.kt +++ b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/WindowedAxisStats.kt @@ -19,7 +19,7 @@ internal class WindowedAxisStats(private val autoWindowSize: Int = 0) : AxisStat fun removeOldestEntry() { if (count <= 0) return - val target = guidingEntries.first + val target = guidingEntries.first() val value = target.starPos val deltaTime = target.deltaTime @@ -41,7 +41,7 @@ internal class WindowedAxisStats(private val autoWindowSize: Int = 0) : AxisStat } private fun adjustMinMaxValues() { - val target = guidingEntries.first + val target = guidingEntries.first() var recalNeeded = false var prev = target.starPos diff --git a/nebulosa-guiding-phd2/src/main/kotlin/nebulosa/guiding/phd2/PHD2Guider.kt b/nebulosa-guiding-phd2/src/main/kotlin/nebulosa/guiding/phd2/PHD2Guider.kt index 71c6a5e8a..c206a11ad 100644 --- a/nebulosa-guiding-phd2/src/main/kotlin/nebulosa/guiding/phd2/PHD2Guider.kt +++ b/nebulosa-guiding-phd2/src/main/kotlin/nebulosa/guiding/phd2/PHD2Guider.kt @@ -9,7 +9,6 @@ import nebulosa.phd2.client.PHD2Client import nebulosa.phd2.client.PHD2EventListener import nebulosa.phd2.client.commands.* import nebulosa.phd2.client.events.* -import nebulosa.util.concurrency.cancellation.CancellationToken import nebulosa.util.concurrency.latch.CountUpDownLatch class PHD2Guider(private val client: PHD2Client) : Guider, PHD2EventListener { @@ -193,7 +192,7 @@ class PHD2Guider(private val client: PHD2Client) : Guider, PHD2EventListener { repeat(5) { try { return client.sendCommandSync(GetLockPosition, 5) - } catch (ignored: Throwable) { + } catch (_: Throwable) { Thread.sleep(5000) } } @@ -208,7 +207,7 @@ class PHD2Guider(private val client: PHD2Client) : Guider, PHD2EventListener { client.sendCommandSync(command) refreshShiftLockParams() true - } catch (e: Throwable) { + } catch (_: Throwable) { false } } @@ -234,16 +233,14 @@ class PHD2Guider(private val client: PHD2Client) : Guider, PHD2EventListener { } } - override fun waitForSettle(cancellationToken: CancellationToken) { + override fun waitForSettle() { try { - cancellationToken.listen(settling) settling.await(settleTimeout) - } catch (e: InterruptedException) { + } catch (_: InterruptedException) { LOG.warn("PHD2 did not send SettleDone message in expected time") } catch (e: Throwable) { LOG.warn("an error occurrs while waiting for settle done", e) } finally { - cancellationToken.unlisten(settling) settling.reset() } } diff --git a/nebulosa-guiding/build.gradle.kts b/nebulosa-guiding/build.gradle.kts index ff10a4474..9ad896cec 100644 --- a/nebulosa-guiding/build.gradle.kts +++ b/nebulosa-guiding/build.gradle.kts @@ -6,7 +6,6 @@ plugins { dependencies { api(project(":nebulosa-math")) api(project(":nebulosa-indi-device")) - api(project(":nebulosa-util")) testImplementation(project(":nebulosa-test")) } diff --git a/nebulosa-guiding/src/main/kotlin/nebulosa/guiding/Guider.kt b/nebulosa-guiding/src/main/kotlin/nebulosa/guiding/Guider.kt index cd22a3e07..98ad9dcf5 100644 --- a/nebulosa-guiding/src/main/kotlin/nebulosa/guiding/Guider.kt +++ b/nebulosa-guiding/src/main/kotlin/nebulosa/guiding/Guider.kt @@ -1,6 +1,5 @@ package nebulosa.guiding -import nebulosa.util.concurrency.cancellation.CancellationToken import java.time.Duration interface Guider : AutoCloseable { @@ -35,7 +34,7 @@ interface Guider : AutoCloseable { fun dither(amount: Double, raOnly: Boolean = false) - fun waitForSettle(cancellationToken: CancellationToken = CancellationToken.NONE) + fun waitForSettle() companion object { diff --git a/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/camera/INDICamera.kt b/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/camera/INDICamera.kt index 9cc506feb..ee2073494 100644 --- a/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/camera/INDICamera.kt +++ b/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/camera/INDICamera.kt @@ -9,7 +9,6 @@ import nebulosa.indi.client.device.INDIDevice import nebulosa.indi.client.device.handler.INDIGuideOutputHandler 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.mount.Mount @@ -38,10 +37,10 @@ internal open class INDICamera( @Volatile final override var cfaOffsetX = 0 @Volatile final override var cfaOffsetY = 0 @Volatile final override var cfaType = CfaPattern.RGGB - @Volatile final override var exposureMin: Duration = Duration.ZERO - @Volatile final override var exposureMax: Duration = Duration.ZERO + @Volatile final override var exposureMin = 0L + @Volatile final override var exposureMax = 0L @Volatile final override var exposureState = PropertyState.IDLE - @Volatile final override var exposureTime: Duration = Duration.ZERO + @Volatile final override var exposureTime = 0L @Volatile final override var hasCooler = false @Volatile final override var canSetTemperature = false @Volatile final override var canSubFrame = false @@ -143,8 +142,9 @@ internal open class INDICamera( val element = message[if (isGuider) "GUIDER_EXPOSURE_VALUE" else "CCD_EXPOSURE_VALUE"]!! if (element is DefNumber) { - exposureMin = Duration.ofNanos((element.min * NANO_TO_SECONDS).toLong()) - exposureMax = Duration.ofNanos((element.max * NANO_TO_SECONDS).toLong()) + exposureMin = (element.min * MICROS_TO_SECONDS).toLong() + exposureMax = (element.max * MICROS_TO_SECONDS).toLong() + sender.fireOnEventReceived(CameraExposureMinMaxChanged(this)) } @@ -152,7 +152,7 @@ internal open class INDICamera( exposureState = message.state if (exposureState == PropertyState.BUSY || exposureState == PropertyState.OK) { - exposureTime = Duration.ofNanos((element.value * NANO_TO_SECONDS).toLong()) + exposureTime = (element.value * MICROS_TO_SECONDS).toLong() sender.fireOnEventReceived(CameraExposureProgressChanged(this)) } @@ -321,7 +321,7 @@ internal open class INDICamera( override fun offset(value: Int) = Unit - override fun startCapture(exposureTime: Duration) { + override fun startCapture(exposureTime: Long) { sendNewSwitch("CCD_TRANSFER_FORMAT", "FORMAT_FITS" to true) if (exposureState != PropertyState.IDLE) { @@ -329,7 +329,7 @@ internal open class INDICamera( sender.fireOnEventReceived(CameraExposureStateChanged(this)) } - val exposureInSeconds = exposureTime.toNanos() / NANO_TO_SECONDS + val exposureInSeconds = exposureTime.toDouble() / MICROS_TO_SECONDS if (this is GuideHead) { sendNewNumber("GUIDER_EXPOSURE", "GUIDER_EXPOSURE_VALUE" to exposureInSeconds) @@ -495,7 +495,8 @@ internal open class INDICamera( companion object { - const val GUIDE_HEAD_SUFFIX = "(Guide Head)" + private const val GUIDE_HEAD_SUFFIX = "(Guide Head)" + private const val MICROS_TO_SECONDS = 1_000_000L @JvmStatic private val COMPRESSION_FORMATS = arrayOf(".fz", ".gz") @JvmStatic private val LOG = loggerFor() diff --git a/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/mount/INDIMount.kt b/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/mount/INDIMount.kt index 8c12ec2e9..7f18ec324 100644 --- a/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/mount/INDIMount.kt +++ b/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/mount/INDIMount.kt @@ -13,6 +13,7 @@ import nebulosa.indi.protocol.DefVector.Companion.isNotReadOnly import nebulosa.indi.protocol.Vector.Companion.isBusy import nebulosa.math.* import nebulosa.nova.position.ICRF +import nebulosa.time.SystemClock import java.time.Duration import java.time.OffsetDateTime import java.time.ZoneOffset @@ -56,7 +57,7 @@ internal open class INDIMount( @Volatile final override var longitude = 0.0 @Volatile final override var latitude = 0.0 @Volatile final override var elevation = 0.0 - @Volatile final override var dateTime = OffsetDateTime.now()!! + @Volatile final override var dateTime = OffsetDateTime.now(SystemClock)!! override fun handleMessage(message: INDIProtocol) { when (message) { diff --git a/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/camera/Camera.kt b/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/camera/Camera.kt index cbd7a7fbb..4c0baa3cc 100644 --- a/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/camera/Camera.kt +++ b/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/camera/Camera.kt @@ -6,7 +6,6 @@ import nebulosa.indi.device.DeviceType import nebulosa.indi.device.guider.GuideOutput import nebulosa.indi.device.thermometer.Thermometer import nebulosa.indi.protocol.PropertyState -import java.time.Duration interface Camera : GuideOutput, Thermometer { @@ -35,13 +34,13 @@ interface Camera : GuideOutput, Thermometer { val cfaType: CfaPattern - val exposureMin: Duration + val exposureMin: Long - val exposureMax: Duration + val exposureMax: Long val exposureState: PropertyState - val exposureTime: Duration + val exposureTime: Long val hasCooler: Boolean @@ -119,14 +118,9 @@ interface Camera : GuideOutput, Thermometer { fun offset(value: Int) - fun startCapture(exposureTime: Duration) + fun startCapture(exposureTime: Long) fun abortCapture() fun fitsKeywords(vararg cards: HeaderCard) - - companion object { - - const val NANO_TO_SECONDS = 1_000_000_000.0 - } } diff --git a/nebulosa-indi-protocol/src/main/kotlin/nebulosa/indi/protocol/parser/INDIXmlInputStream.kt b/nebulosa-indi-protocol/src/main/kotlin/nebulosa/indi/protocol/parser/INDIXmlInputStream.kt index 2536c377a..d3aa8a0af 100644 --- a/nebulosa-indi-protocol/src/main/kotlin/nebulosa/indi/protocol/parser/INDIXmlInputStream.kt +++ b/nebulosa-indi-protocol/src/main/kotlin/nebulosa/indi/protocol/parser/INDIXmlInputStream.kt @@ -209,7 +209,7 @@ class INDIXmlInputStream(source: InputStream) : INDIInputStream { private fun parseDefSwitch(): DefSwitch { val defSwitch = DefSwitch() defSwitch.parseDefElement() - defSwitch.value = reader.elementText.trim().equals("On", true) + defSwitch.value = reader.elementText.contains("On", true) return defSwitch } diff --git a/nebulosa-job-manager/build.gradle.kts b/nebulosa-job-manager/build.gradle.kts new file mode 100644 index 000000000..1ed9169cc --- /dev/null +++ b/nebulosa-job-manager/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + kotlin("jvm") + id("maven-publish") +} + +dependencies { + api(project(":nebulosa-util")) + implementation(project(":nebulosa-log")) + testImplementation(project(":nebulosa-test")) +} + +publishing { + publications { + create("pluginMaven") { + from(components["java"]) + } + } +} diff --git a/nebulosa-job-manager/src/main/kotlin/nebulosa/job/manager/AbstractJob.kt b/nebulosa-job-manager/src/main/kotlin/nebulosa/job/manager/AbstractJob.kt new file mode 100644 index 000000000..93aab4cd4 --- /dev/null +++ b/nebulosa-job-manager/src/main/kotlin/nebulosa/job/manager/AbstractJob.kt @@ -0,0 +1,237 @@ +package nebulosa.job.manager + +import nebulosa.log.loggerFor +import nebulosa.util.concurrency.cancellation.CancellationListener +import nebulosa.util.concurrency.cancellation.CancellationSource +import nebulosa.util.concurrency.latch.CountUpDownLatch +import nebulosa.util.concurrency.latch.PauseListener +import java.util.* +import java.util.concurrent.CompletableFuture +import java.util.concurrent.Executor +import java.util.concurrent.ForkJoinPool +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicInteger + +abstract class AbstractJob : JobTask, CancellationListener, PauseListener { + + private val tasks = Collections.synchronizedList(ArrayList(128)) + + private val running = AtomicBoolean() + private val cancelled = AtomicBoolean() + private val pauseLatch = CountUpDownLatch() + private val current = AtomicInteger(-1) + + @Volatile final override var loopCount = 0 + private set + + @Volatile final override var taskCount = 0 + private set + + override val size + get() = tasks.size + + override val isRunning + get() = running.get() + + override val isCancelled + get() = cancelled.get() + + override val isPaused + get() = !pauseLatch.get() + + override val currentTask + get() = tasks.getOrNull(current.get()) + + protected open fun beforeStart() = Unit + + protected open fun beforeTask(task: Task) = Unit + + protected open fun afterTask(task: Task, exception: Throwable? = null) = exception == null + + protected open fun afterFinish() = Unit + + protected open fun beforePause(task: Task) = Unit + + protected open fun afterPause(task: Task) = Unit + + protected open fun isLoop() = false + + protected open fun canRun(prev: Task?, current: Task) = true + + protected open fun canPause(task: Task) = true + + override fun onCancel(source: CancellationSource) { + cancelled.set(true) + currentTask?.onCancel(source) + } + + override fun onPause(paused: Boolean) { + if (paused) { + pauseLatch.countUp() + } else { + pauseLatch.reset() + } + + currentTask?.onPause(paused) + } + + override fun runTask(task: Task, prev: Task?): TaskExecutionState { + return try { + checkIfPaused(task) + + if (isCancelled) { + TaskExecutionState.BREAK + } else if (canRun(prev, task)) { + beforeTask(task) + + var exception: Throwable? = null + + try { + taskCount++ + task.run() + } catch (e: Throwable) { + LOG.error("task execution failed", e) + exception = e + } + + if (!afterTask(task, exception) || isCancelled) { + TaskExecutionState.BREAK + } else { + checkIfPaused(task) + TaskExecutionState.OK + } + } else { + TaskExecutionState.CONTINUE + } + } catch (e: Throwable) { + LOG.error("task execution failed", e) + TaskExecutionState.CONTINUE + } + } + + final override fun run() { + if (current.compareAndSet(-1, 0)) { + running.set(true) + + beforeStart() + + var prev: Task? = null + + while (isRunning && !isCancelled) { + val index = current.get() + val task = tasks.getOrNull(index) ?: break + val state = runTask(task, prev) + + if (state == TaskExecutionState.OK) { + prev = task + } else if (state == TaskExecutionState.BREAK) { + break + } + + val next = tasks.getOrNull(index + 1) + + if (next != null) { + current.set(index + 1) + } else if (isLoop()) { + loopCount++ + current.set(0) + } else { + break + } + } + + afterFinish() + running.set(false) + + current.set(-1) + } + } + + fun runAsync(executor: Executor = EXECUTOR): CompletableFuture { + return CompletableFuture.runAsync(this, executor) + } + + private fun checkIfPaused(task: Task) { + if (isPaused && canPause(task)) { + beforePause(task) + pauseLatch.await() + afterPause(task) + } + } + + final override fun iterator() = object : MutableIterator { + + @Volatile private var index = 0 + + override fun hasNext(): Boolean { + return index < size + } + + override fun next(): Task { + return tasks.getOrNull(index++) ?: throw NoSuchElementException() + } + + override fun remove() { + TODO("Not yet implemented") + } + } + + fun addFirst(task: Task) { + tasks.add(0, task) + } + + fun addLast(task: Task) { + tasks.add(task) + } + + fun removeFirst(): Task? { + return tasks.removeFirstOrNull() + } + + fun removeLast(): Task? { + return tasks.removeLastOrNull() + } + + final override fun add(element: Task): Boolean { + addLast(element) + return true + } + + final override fun contains(element: Task): Boolean { + return element in tasks + } + + final override fun clear() { + tasks.clear() + } + + final override fun removeAll(elements: Collection): Boolean { + return tasks.removeAll(elements) + } + + final override fun retainAll(elements: Collection): Boolean { + return tasks.retainAll(elements) + } + + final override fun isEmpty(): Boolean { + return size == 0 + } + + final override fun containsAll(elements: Collection): Boolean { + return tasks.containsAll(elements) + } + + override fun addAll(elements: Collection): Boolean { + return tasks.addAll(elements) + } + + override fun remove(element: Task): Boolean { + return tasks.remove(element) + } + + companion object { + + @JvmStatic private val EXECUTOR = ForkJoinPool.commonPool() + @JvmStatic private val LOG = loggerFor() + } +} diff --git a/nebulosa-job-manager/src/main/kotlin/nebulosa/job/manager/Job.kt b/nebulosa-job-manager/src/main/kotlin/nebulosa/job/manager/Job.kt new file mode 100644 index 000000000..0f54aff9f --- /dev/null +++ b/nebulosa-job-manager/src/main/kotlin/nebulosa/job/manager/Job.kt @@ -0,0 +1,20 @@ +package nebulosa.job.manager + +import nebulosa.util.Stoppable +import nebulosa.util.concurrency.latch.Pauseable +import java.util.function.Consumer + +interface Job : MutableCollection, Runnable, Pauseable, Stoppable, Consumer { + + val loopCount: Int + + val taskCount: Int + + val isRunning: Boolean + + val isCancelled: Boolean + + val currentTask: Task? + + fun runTask(task: Task, prev: Task?): TaskExecutionState +} diff --git a/nebulosa-job-manager/src/main/kotlin/nebulosa/job/manager/JobTask.kt b/nebulosa-job-manager/src/main/kotlin/nebulosa/job/manager/JobTask.kt new file mode 100644 index 000000000..0bdec58d4 --- /dev/null +++ b/nebulosa-job-manager/src/main/kotlin/nebulosa/job/manager/JobTask.kt @@ -0,0 +1,18 @@ +package nebulosa.job.manager + +import nebulosa.util.concurrency.cancellation.CancellationSource + +interface JobTask : Job, Task { + + override fun pause() { + onPause(true) + } + + override fun unpause() { + onPause(false) + } + + override fun stop() { + onCancel(CancellationSource.DEFAULT) + } +} diff --git a/nebulosa-job-manager/src/main/kotlin/nebulosa/job/manager/LoopTask.kt b/nebulosa-job-manager/src/main/kotlin/nebulosa/job/manager/LoopTask.kt new file mode 100644 index 000000000..5468179b1 --- /dev/null +++ b/nebulosa-job-manager/src/main/kotlin/nebulosa/job/manager/LoopTask.kt @@ -0,0 +1,44 @@ +package nebulosa.job.manager + +data class LoopTask( + private val job: Job, + private val tasks: List, + private val condition: (Task, Int) -> Boolean = INFINITY_LOOP, +) : Task { + + @Volatile private var index = 0 + @Volatile private var count = 0 + + @Volatile var state = TaskExecutionState.OK + private set + + override fun run() { + var prev: Task? = null + + while (job.isRunning && !job.isCancelled) { + val task = tasks[index] + + if (!condition(task, count)) { + break + } + + state = job.runTask(task, prev) + + if (state == TaskExecutionState.OK) { + prev = task + } else if (state == TaskExecutionState.BREAK) { + break + } + + if (++index >= tasks.size) { + index = 0 + count++ + } + } + } + + companion object { + + @JvmField val INFINITY_LOOP: (Task, Int) -> Boolean = { _, _ -> true } + } +} diff --git a/nebulosa-job-manager/src/main/kotlin/nebulosa/job/manager/SplitTask.kt b/nebulosa-job-manager/src/main/kotlin/nebulosa/job/manager/SplitTask.kt new file mode 100644 index 000000000..293c2ebea --- /dev/null +++ b/nebulosa-job-manager/src/main/kotlin/nebulosa/job/manager/SplitTask.kt @@ -0,0 +1,36 @@ +package nebulosa.job.manager + +import nebulosa.util.concurrency.cancellation.CancellationSource +import java.util.concurrent.CompletableFuture +import java.util.concurrent.Executor +import java.util.concurrent.ForkJoinPool + +data class SplitTask( + private val tasks: List, + private val executor: Executor = EXECUTOR, +) : Task { + + override fun onPause(paused: Boolean) { + tasks.forEach { it.onPause(paused) } + } + + override fun onCancel(source: CancellationSource) { + tasks.forEach { it.onCancel(source) } + } + + override fun run() { + if (tasks.isEmpty()) { + return + } else if (tasks.size == 1) { + tasks[0].run() + } else { + val completables = Array(tasks.size) { CompletableFuture.runAsync(tasks[it], executor) } + CompletableFuture.allOf(*completables).join() + } + } + + companion object { + + @JvmStatic private val EXECUTOR = ForkJoinPool.commonPool() + } +} diff --git a/nebulosa-job-manager/src/main/kotlin/nebulosa/job/manager/Task.kt b/nebulosa-job-manager/src/main/kotlin/nebulosa/job/manager/Task.kt new file mode 100644 index 000000000..7015f0bc7 --- /dev/null +++ b/nebulosa-job-manager/src/main/kotlin/nebulosa/job/manager/Task.kt @@ -0,0 +1,12 @@ +package nebulosa.job.manager + +import nebulosa.util.concurrency.cancellation.CancellationListener +import nebulosa.util.concurrency.cancellation.CancellationSource +import nebulosa.util.concurrency.latch.PauseListener + +fun interface Task : CancellationListener, PauseListener, Runnable { + + override fun onCancel(source: CancellationSource) = Unit + + override fun onPause(paused: Boolean) = Unit +} diff --git a/nebulosa-job-manager/src/main/kotlin/nebulosa/job/manager/TaskEvent.kt b/nebulosa-job-manager/src/main/kotlin/nebulosa/job/manager/TaskEvent.kt new file mode 100644 index 000000000..66c88b0dc --- /dev/null +++ b/nebulosa-job-manager/src/main/kotlin/nebulosa/job/manager/TaskEvent.kt @@ -0,0 +1,8 @@ +package nebulosa.job.manager + +interface TaskEvent { + + val job: Job + + val task: Task +} diff --git a/nebulosa-job-manager/src/main/kotlin/nebulosa/job/manager/TaskExecutionState.kt b/nebulosa-job-manager/src/main/kotlin/nebulosa/job/manager/TaskExecutionState.kt new file mode 100644 index 000000000..b7f971133 --- /dev/null +++ b/nebulosa-job-manager/src/main/kotlin/nebulosa/job/manager/TaskExecutionState.kt @@ -0,0 +1,7 @@ +package nebulosa.job.manager + +enum class TaskExecutionState { + OK, + CONTINUE, + BREAK, +} diff --git a/nebulosa-job-manager/src/main/kotlin/nebulosa/job/manager/TimedTaskEvent.kt b/nebulosa-job-manager/src/main/kotlin/nebulosa/job/manager/TimedTaskEvent.kt new file mode 100644 index 000000000..94d8388b1 --- /dev/null +++ b/nebulosa-job-manager/src/main/kotlin/nebulosa/job/manager/TimedTaskEvent.kt @@ -0,0 +1,10 @@ +package nebulosa.job.manager + +interface TimedTaskEvent : TaskEvent { + + val elapsedTime: Long + + val remainingTime: Long + + val progress: Double +} diff --git a/nebulosa-job-manager/src/main/kotlin/nebulosa/job/manager/delay/DelayElapsed.kt b/nebulosa-job-manager/src/main/kotlin/nebulosa/job/manager/delay/DelayElapsed.kt new file mode 100644 index 000000000..764999fe1 --- /dev/null +++ b/nebulosa-job-manager/src/main/kotlin/nebulosa/job/manager/delay/DelayElapsed.kt @@ -0,0 +1,11 @@ +package nebulosa.job.manager.delay + +import nebulosa.job.manager.Job + +data class DelayElapsed( + override val job: Job, override val task: DelayTask, + override val remainingTime: Long, + override val elapsedTime: Long, + override val waitTime: Long, + override val progress: Double, +) : DelayEvent diff --git a/nebulosa-job-manager/src/main/kotlin/nebulosa/job/manager/delay/DelayEvent.kt b/nebulosa-job-manager/src/main/kotlin/nebulosa/job/manager/delay/DelayEvent.kt new file mode 100644 index 000000000..cbb34414b --- /dev/null +++ b/nebulosa-job-manager/src/main/kotlin/nebulosa/job/manager/delay/DelayEvent.kt @@ -0,0 +1,10 @@ +package nebulosa.job.manager.delay + +import nebulosa.job.manager.TimedTaskEvent + +sealed interface DelayEvent : TimedTaskEvent { + + override val task: DelayTask + + val waitTime: Long +} diff --git a/nebulosa-job-manager/src/main/kotlin/nebulosa/job/manager/delay/DelayFinished.kt b/nebulosa-job-manager/src/main/kotlin/nebulosa/job/manager/delay/DelayFinished.kt new file mode 100644 index 000000000..9a76c48b0 --- /dev/null +++ b/nebulosa-job-manager/src/main/kotlin/nebulosa/job/manager/delay/DelayFinished.kt @@ -0,0 +1,11 @@ +package nebulosa.job.manager.delay + +import nebulosa.job.manager.Job + +data class DelayFinished(override val job: Job, override val task: DelayTask) : DelayEvent { + + override val remainingTime = 0L + override val elapsedTime = task.duration * 1000L + override val waitTime = 0L + override val progress = 1.0 +} diff --git a/nebulosa-job-manager/src/main/kotlin/nebulosa/job/manager/delay/DelayStarted.kt b/nebulosa-job-manager/src/main/kotlin/nebulosa/job/manager/delay/DelayStarted.kt new file mode 100644 index 000000000..f305b5782 --- /dev/null +++ b/nebulosa-job-manager/src/main/kotlin/nebulosa/job/manager/delay/DelayStarted.kt @@ -0,0 +1,11 @@ +package nebulosa.job.manager.delay + +import nebulosa.job.manager.Job + +data class DelayStarted(override val job: Job, override val task: DelayTask) : DelayEvent { + + override val remainingTime = task.duration * 1000L + override val elapsedTime = 0L + override val waitTime = 0L + override val progress = 0.0 +} diff --git a/nebulosa-job-manager/src/main/kotlin/nebulosa/job/manager/delay/DelayTask.kt b/nebulosa-job-manager/src/main/kotlin/nebulosa/job/manager/delay/DelayTask.kt new file mode 100644 index 000000000..e1d6e507a --- /dev/null +++ b/nebulosa-job-manager/src/main/kotlin/nebulosa/job/manager/delay/DelayTask.kt @@ -0,0 +1,49 @@ +package nebulosa.job.manager.delay + +import nebulosa.job.manager.Job +import nebulosa.job.manager.Task +import nebulosa.log.debug +import nebulosa.log.loggerFor +import java.time.Duration + +data class DelayTask( + @JvmField val job: Job, + @JvmField val duration: Long, +) : Task { + + constructor(job: Job, duration: Duration) : this(job, duration.toMillis()) + + override fun run() { + var remainingTime = duration + + if (!job.isCancelled && remainingTime > 0L) { + LOG.debug { "Delay started. duration=$duration ms" } + + job.accept(DelayStarted(job, this)) + + while (!job.isCancelled && remainingTime > 0L) { + val waitTime = minOf(remainingTime, DELAY_INTERVAL) + + if (waitTime > 0L) { + val progress = (duration - remainingTime) / duration.toDouble() + job.accept(DelayElapsed(job, this, remainingTime * 1000L, (duration - remainingTime) * 1000L, waitTime * 1000L, progress)) + + Thread.sleep(waitTime) + + remainingTime -= waitTime + } + } + + job.accept(DelayFinished(job, this)) + + LOG.debug { "Delay finished. duration=$duration ms" } + } + } + + companion object { + + const val DELAY_INTERVAL = 500L + + @JvmStatic private val LOG = loggerFor() + } +} diff --git a/nebulosa-math/src/main/kotlin/nebulosa/math/AngleFormatter.kt b/nebulosa-math/src/main/kotlin/nebulosa/math/AngleFormatter.kt index cc8fed929..eb05d3f08 100644 --- a/nebulosa-math/src/main/kotlin/nebulosa/math/AngleFormatter.kt +++ b/nebulosa-math/src/main/kotlin/nebulosa/math/AngleFormatter.kt @@ -5,7 +5,7 @@ import java.math.RoundingMode import java.util.* import kotlin.math.abs -data class AngleFormatter internal constructor( +data class AngleFormatter( private val isHours: Boolean = false, private val hasSign: Boolean = true, private val hoursFormat: String = "%02d", diff --git a/nebulosa-retrofit/src/main/kotlin/nebulosa/retrofit/RetrofitService.kt b/nebulosa-retrofit/src/main/kotlin/nebulosa/retrofit/RetrofitService.kt index 4d0565dc3..ab25fa033 100644 --- a/nebulosa-retrofit/src/main/kotlin/nebulosa/retrofit/RetrofitService.kt +++ b/nebulosa-retrofit/src/main/kotlin/nebulosa/retrofit/RetrofitService.kt @@ -4,8 +4,8 @@ import com.fasterxml.jackson.annotation.JsonInclude import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.MapperFeature import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.json.JsonMapper import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule -import com.fasterxml.jackson.module.kotlin.jsonMapper import nebulosa.json.PathModule import okhttp3.ConnectionPool import okhttp3.OkHttpClient @@ -21,7 +21,7 @@ abstract class RetrofitService( mapper: ObjectMapper? = null, ) { - protected val jsonMapper: ObjectMapper by lazy { mapper ?: DEFAULT_MAPPER.copy() } + protected val objectMapper: ObjectMapper by lazy { mapper ?: DEFAULT_MAPPER.also(::withJsonMapper).build() } protected open val converterFactory: Iterable get() = emptyList() @@ -29,23 +29,22 @@ abstract class RetrofitService( protected open val callAdaptorFactory: CallAdapter.Factory? get() = null - protected open fun withOkHttpClientBuilder(builder: OkHttpClient.Builder) = Unit + protected open fun withOkHttpClient(builder: OkHttpClient.Builder) = Unit - protected open fun withObjectMapper(mapper: ObjectMapper) = Unit + protected open fun withJsonMapper(mapper: JsonMapper.Builder) = Unit protected open val retrofit by lazy { val builder = Retrofit.Builder() builder.baseUrl(url.trim().let { if (it.endsWith("/")) it else "$it/" }) builder.addConverterFactory(RawAsStringConverterFactory) builder.addConverterFactory(RawAsByteArrayConverterFactory) - builder.addConverterFactory(EnumToStringConverterFactory(jsonMapper)) + builder.addConverterFactory(EnumToStringConverterFactory(objectMapper)) converterFactory.forEach { builder.addConverterFactory(it) } - withObjectMapper(jsonMapper) - builder.addConverterFactory(JacksonConverterFactory.create(jsonMapper)) + builder.addConverterFactory(JacksonConverterFactory.create(objectMapper)) callAdaptorFactory?.also(builder::addCallAdapterFactory) with((httpClient ?: HTTP_CLIENT).newBuilder()) { - withOkHttpClientBuilder(this) + withOkHttpClient(this) builder.client(build()) } @@ -64,9 +63,9 @@ abstract class RetrofitService( .callTimeout(60L, TimeUnit.SECONDS) .build() - @JvmStatic private val DEFAULT_MAPPER = jsonMapper { + @JvmStatic private val DEFAULT_MAPPER = JsonMapper.builder().apply { + addModule(JavaTimeModule()) // Why needs to be registered first? Fix "java.time.LocalDateTime not supported by default" addModule(PathModule()) - addModule(JavaTimeModule()) enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS) disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) serializationInclusion(JsonInclude.Include.NON_NULL) diff --git a/nebulosa-sbd/src/main/kotlin/nebulosa/sbd/SmallBodyDatabaseService.kt b/nebulosa-sbd/src/main/kotlin/nebulosa/sbd/SmallBodyDatabaseService.kt index 42e9f01ad..4147bd740 100644 --- a/nebulosa-sbd/src/main/kotlin/nebulosa/sbd/SmallBodyDatabaseService.kt +++ b/nebulosa-sbd/src/main/kotlin/nebulosa/sbd/SmallBodyDatabaseService.kt @@ -16,7 +16,7 @@ class SmallBodyDatabaseService( private val service by lazy { retrofit.create() } - override fun withOkHttpClientBuilder(builder: OkHttpClient.Builder) { + override fun withOkHttpClient(builder: OkHttpClient.Builder) { builder.readTimeout(5L, TimeUnit.MINUTES) .writeTimeout(5L, TimeUnit.MINUTES) .connectTimeout(5L, TimeUnit.MINUTES) diff --git a/nebulosa-util/src/main/kotlin/nebulosa/util/Startable.kt b/nebulosa-util/src/main/kotlin/nebulosa/util/Startable.kt new file mode 100644 index 000000000..41e9dc38d --- /dev/null +++ b/nebulosa-util/src/main/kotlin/nebulosa/util/Startable.kt @@ -0,0 +1,6 @@ +package nebulosa.util + +fun interface Startable { + + fun start() +} diff --git a/nebulosa-util/src/main/kotlin/nebulosa/util/Stoppable.kt b/nebulosa-util/src/main/kotlin/nebulosa/util/Stoppable.kt new file mode 100644 index 000000000..d058daf4d --- /dev/null +++ b/nebulosa-util/src/main/kotlin/nebulosa/util/Stoppable.kt @@ -0,0 +1,6 @@ +package nebulosa.util + +fun interface Stoppable { + + fun stop() +} diff --git a/nebulosa-util/src/main/kotlin/nebulosa/util/concurrency/cancellation/Cancellable.kt b/nebulosa-util/src/main/kotlin/nebulosa/util/concurrency/cancellation/Cancellable.kt new file mode 100644 index 000000000..a1a8bdb0c --- /dev/null +++ b/nebulosa-util/src/main/kotlin/nebulosa/util/concurrency/cancellation/Cancellable.kt @@ -0,0 +1,6 @@ +package nebulosa.util.concurrency.cancellation + +interface Cancellable { + + fun cancel(source: CancellationSource = CancellationSource.DEFAULT): Boolean +} diff --git a/nebulosa-util/src/main/kotlin/nebulosa/util/concurrency/cancellation/CancellationSource.kt b/nebulosa-util/src/main/kotlin/nebulosa/util/concurrency/cancellation/CancellationSource.kt index de69a22c6..f2122252e 100644 --- a/nebulosa-util/src/main/kotlin/nebulosa/util/concurrency/cancellation/CancellationSource.kt +++ b/nebulosa-util/src/main/kotlin/nebulosa/util/concurrency/cancellation/CancellationSource.kt @@ -11,4 +11,9 @@ interface CancellationSource { data object Close : CancellationSource data class Exceptionally(val exception: Throwable) : CancellationSource + + companion object { + + @JvmStatic val DEFAULT = Cancel(true) + } } diff --git a/nebulosa-util/src/main/kotlin/nebulosa/util/concurrency/cancellation/CancellationToken.kt b/nebulosa-util/src/main/kotlin/nebulosa/util/concurrency/cancellation/CancellationToken.kt index 0b9b1b49a..29432026e 100644 --- a/nebulosa-util/src/main/kotlin/nebulosa/util/concurrency/cancellation/CancellationToken.kt +++ b/nebulosa-util/src/main/kotlin/nebulosa/util/concurrency/cancellation/CancellationToken.kt @@ -7,7 +7,7 @@ import java.util.concurrent.Future import java.util.concurrent.TimeUnit class CancellationToken private constructor(private val completable: CompletableFuture?) : - Pauser(), AutoCloseable, Future { + Pauser(), Cancellable, Future, AutoCloseable { constructor() : this(CompletableFuture()) @@ -48,16 +48,12 @@ class CancellationToken private constructor(private val completable: Completable listeners.clear() } - fun cancel() { - cancel(true) - } - override fun cancel(mayInterruptIfRunning: Boolean): Boolean { return cancel(CancellationSource.Cancel(mayInterruptIfRunning)) } @Synchronized - fun cancel(source: CancellationSource): Boolean { + override fun cancel(source: CancellationSource): Boolean { unpause() completable?.complete(source) ?: return false return true @@ -80,7 +76,9 @@ class CancellationToken private constructor(private val completable: Completable } fun throwIfCancelled() { - if (isCancelled) throw CancellationException() + if (isCancelled) { + throw CancellationException() + } } override fun close() { diff --git a/nebulosa-util/src/main/kotlin/nebulosa/util/exec/CommandLine.kt b/nebulosa-util/src/main/kotlin/nebulosa/util/exec/CommandLine.kt index 46cf18545..61e843acd 100644 --- a/nebulosa-util/src/main/kotlin/nebulosa/util/exec/CommandLine.kt +++ b/nebulosa-util/src/main/kotlin/nebulosa/util/exec/CommandLine.kt @@ -16,7 +16,7 @@ inline fun commandLine(action: CommandLine.Builder.() -> Unit): CommandLine { return CommandLine.Builder().also(action).get() } -data class CommandLine internal constructor( +class CommandLine internal constructor( private val builder: ProcessBuilder, private val listeners: LinkedHashSet, ) : CompletableFuture(), CancellationListener { diff --git a/nebulosa-util/src/test/kotlin/CancellationTokenTest.kt b/nebulosa-util/src/test/kotlin/CancellationTokenTest.kt index 0c1a6cd23..e288aa785 100644 --- a/nebulosa-util/src/test/kotlin/CancellationTokenTest.kt +++ b/nebulosa-util/src/test/kotlin/CancellationTokenTest.kt @@ -27,7 +27,7 @@ class CancellationTokenTest { token.listen { source = it } token.cancel() token.get() shouldBe source - source shouldBe CancellationSource.Cancel(true) + source shouldBe CancellationSource.DEFAULT token.isCancelled.shouldBeTrue() token.isDone.shouldBeTrue() } @@ -50,7 +50,7 @@ class CancellationTokenTest { val token = CancellationToken() token.cancel() token.listen { source = it } - token.get() shouldBe CancellationSource.Cancel(true) + token.get() shouldBe CancellationSource.DEFAULT source shouldBe CancellationSource.Listen token.isCancelled.shouldBeTrue() token.isDone.shouldBeTrue() diff --git a/settings.gradle.kts b/settings.gradle.kts index c3f04f092..2ed6a15eb 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -68,6 +68,7 @@ include(":nebulosa-indi-client") include(":nebulosa-indi-device") include(":nebulosa-indi-protocol") include(":nebulosa-io") +include(":nebulosa-job-manager") include(":nebulosa-json") include(":nebulosa-livestacker") include(":nebulosa-log")