diff --git a/api/build.gradle.kts b/api/build.gradle.kts index 52377a141..a3ea18d04 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -11,6 +11,7 @@ plugins { dependencies { implementation(project(":nebulosa-astap")) implementation(project(":nebulosa-astrometrynet")) + implementation(project(":nebulosa-batch-processing")) implementation(project(":nebulosa-common")) implementation(project(":nebulosa-guiding-phd2")) implementation(project(":nebulosa-hips2fits")) @@ -31,7 +32,6 @@ dependencies { implementation(libs.flyway) implementation(libs.okhttp) implementation(libs.oshi) - implementation(libs.rx) implementation(libs.sqlite) implementation(libs.hikari) implementation("org.springframework.boot:spring-boot-starter") @@ -42,7 +42,6 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-websocket") { exclude(module = "spring-boot-starter-tomcat") } - implementation("org.springframework.boot:spring-boot-starter-batch") implementation("org.springframework.boot:spring-boot-starter-data-jpa") implementation("org.springframework.boot:spring-boot-starter-undertow") implementation("org.hibernate.orm:hibernate-community-dialects") diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/PolarAlignmentController.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/PolarAlignmentController.kt index 8ca0bb346..15eddb11b 100644 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/PolarAlignmentController.kt +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/PolarAlignmentController.kt @@ -1,6 +1,6 @@ package nebulosa.api.alignment.polar -import nebulosa.api.alignment.polar.darv.DARVStart +import nebulosa.api.alignment.polar.darv.DARVStartRequest import nebulosa.api.beans.annotations.EntityParam import nebulosa.indi.device.camera.Camera import nebulosa.indi.device.guide.GuideOutput @@ -18,7 +18,7 @@ class PolarAlignmentController( @PutMapping("darv/{camera}/{guideOutput}/start") fun darvStart( @EntityParam camera: Camera, @EntityParam guideOutput: GuideOutput, - @RequestBody body: DARVStart, + @RequestBody body: DARVStartRequest, ) { polarAlignmentService.darvStart(camera, guideOutput, body) } diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/PolarAlignmentService.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/PolarAlignmentService.kt index 7265a3d76..44e703b19 100644 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/PolarAlignmentService.kt +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/PolarAlignmentService.kt @@ -1,23 +1,23 @@ package nebulosa.api.alignment.polar -import nebulosa.api.alignment.polar.darv.DARVPolarAlignmentExecutor -import nebulosa.api.alignment.polar.darv.DARVStart +import nebulosa.api.alignment.polar.darv.DARVExecutor +import nebulosa.api.alignment.polar.darv.DARVStartRequest import nebulosa.indi.device.camera.Camera import nebulosa.indi.device.guide.GuideOutput import org.springframework.stereotype.Service @Service class PolarAlignmentService( - private val darvPolarAlignmentExecutor: DARVPolarAlignmentExecutor, + private val darvExecutor: DARVExecutor, ) { - fun darvStart(camera: Camera, guideOutput: GuideOutput, darvStart: DARVStart) { + fun darvStart(camera: Camera, guideOutput: GuideOutput, darvStartRequest: DARVStartRequest) { check(camera.connected) { "camera not connected" } check(guideOutput.connected) { "guide output not connected" } - darvPolarAlignmentExecutor.execute(darvStart.copy(camera = camera, guideOutput = guideOutput)) + darvExecutor.execute(darvStartRequest.copy(camera = camera, guideOutput = guideOutput)) } fun darvStop(camera: Camera, guideOutput: GuideOutput) { - darvPolarAlignmentExecutor.stop(camera, guideOutput) + darvExecutor.stop(camera, guideOutput) } } 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 new file mode 100644 index 000000000..7174f8cc8 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVEvent.kt @@ -0,0 +1,25 @@ +package nebulosa.api.alignment.polar.darv + +import nebulosa.api.messages.MessageEvent +import nebulosa.guiding.GuideDirection +import nebulosa.indi.device.camera.Camera +import nebulosa.indi.device.guide.GuideOutput +import java.time.Duration + +sealed interface DARVEvent : MessageEvent { + + val camera: Camera + + val guideOutput: GuideOutput + + val remainingTime: Duration + + val progress: Double + + val direction: GuideDirection? + + val state: DARVState + + override val eventName + get() = "DARV_POLAR_ALIGNMENT_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 new file mode 100644 index 000000000..359228dc9 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVExecutor.kt @@ -0,0 +1,72 @@ +package nebulosa.api.alignment.polar.darv + +import io.reactivex.rxjava3.functions.Consumer +import nebulosa.api.messages.MessageEvent +import nebulosa.api.messages.MessageService +import nebulosa.batch.processing.JobExecution +import nebulosa.batch.processing.JobLauncher +import nebulosa.indi.device.camera.Camera +import nebulosa.indi.device.guide.GuideOutput +import nebulosa.log.loggerFor +import org.springframework.stereotype.Component +import java.util.* + +/** + * @see Reference + */ +@Component +class DARVExecutor( + private val jobLauncher: JobLauncher, + private val messageService: MessageService, +) : Consumer { + + private val jobExecutions = LinkedList() + + @Synchronized + fun execute(request: DARVStartRequest) { + val camera = requireNotNull(request.camera) + val guideOutput = requireNotNull(request.guideOutput) + + check(!isRunning(camera, guideOutput)) { "DARV job is already running" } + + LOG.info("starting DARV job. data={}", request) + + with(DARVJob(request)) { + subscribe(this@DARVExecutor) + val jobExecution = jobLauncher.launch(this) + jobExecutions.add(jobExecution) + } + } + + fun findJobExecution(camera: Camera, guideOutput: GuideOutput): JobExecution? { + for (i in jobExecutions.indices.reversed()) { + val jobExecution = jobExecutions[i] + val job = jobExecution.job as DARVJob + + if (!jobExecution.isDone && job.camera === camera && job.guideOutput === guideOutput) { + return jobExecution + } + } + + return null + } + + @Synchronized + fun stop(camera: Camera, guideOutput: GuideOutput) { + val jobExecution = findJobExecution(camera, guideOutput) ?: return + jobLauncher.stop(jobExecution) + } + + fun isRunning(camera: Camera, guideOutput: GuideOutput): Boolean { + return findJobExecution(camera, guideOutput) != null + } + + override fun accept(event: MessageEvent) { + messageService.sendMessage(event) + } + + companion object { + + @JvmStatic private val LOG = loggerFor() + } +} diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVFinished.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVFinished.kt new file mode 100644 index 000000000..0748956e8 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVFinished.kt @@ -0,0 +1,16 @@ +package nebulosa.api.alignment.polar.darv + +import nebulosa.indi.device.camera.Camera +import nebulosa.indi.device.guide.GuideOutput +import java.time.Duration + +data class DARVFinished( + override val camera: Camera, + override val guideOutput: GuideOutput, +) : DARVEvent { + + override val remainingTime = Duration.ZERO!! + override val progress = 0.0 + override val state = DARVState.IDLE + override val direction = null +} diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVGuidePulseElapsed.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVGuidePulseElapsed.kt new file mode 100644 index 000000000..580cb0afc --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVGuidePulseElapsed.kt @@ -0,0 +1,16 @@ +package nebulosa.api.alignment.polar.darv + +import nebulosa.api.messages.MessageEvent +import nebulosa.guiding.GuideDirection +import nebulosa.indi.device.camera.Camera +import nebulosa.indi.device.guide.GuideOutput +import java.time.Duration + +data class DARVGuidePulseElapsed( + override val camera: Camera, + override val guideOutput: GuideOutput, + override val remainingTime: Duration, + override val progress: Double, + override val direction: GuideDirection, + override val state: DARVState, +) : MessageEvent, DARVEvent diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVInitialPauseElapsed.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVInitialPauseElapsed.kt new file mode 100644 index 000000000..cc17a87a1 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVInitialPauseElapsed.kt @@ -0,0 +1,16 @@ +package nebulosa.api.alignment.polar.darv + +import nebulosa.indi.device.camera.Camera +import nebulosa.indi.device.guide.GuideOutput +import java.time.Duration + +data class DARVInitialPauseElapsed( + override val camera: Camera, + override val guideOutput: GuideOutput, + override val remainingTime: Duration, + override val progress: Double, +) : DARVEvent { + + override val state = DARVState.INITIAL_PAUSE + override val direction = null +} 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 new file mode 100644 index 000000000..16f6551fd --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVJob.kt @@ -0,0 +1,94 @@ +package nebulosa.api.alignment.polar.darv + +import io.reactivex.rxjava3.subjects.PublishSubject +import nebulosa.api.cameras.CameraCaptureListener +import nebulosa.api.cameras.CameraExposureFinished +import nebulosa.api.cameras.CameraExposureStep +import nebulosa.api.cameras.CameraStartCaptureRequest +import nebulosa.api.guiding.GuidePulseListener +import nebulosa.api.guiding.GuidePulseRequest +import nebulosa.api.guiding.GuidePulseStep +import nebulosa.api.messages.MessageEvent +import nebulosa.batch.processing.* +import nebulosa.batch.processing.delay.DelayStep +import nebulosa.batch.processing.delay.DelayStepListener +import nebulosa.common.concurrency.Incrementer +import java.nio.file.Files +import java.nio.file.Path +import java.time.Duration + +data class DARVJob( + val request: DARVStartRequest, +) : SimpleJob(), PublishSubscribe, CameraCaptureListener, GuidePulseListener, DelayStepListener { + + @JvmField val camera = requireNotNull(request.camera) + @JvmField val guideOutput = requireNotNull(request.guideOutput) + @JvmField val direction = if (request.reversed) request.direction.reversed else request.direction + + @JvmField val cameraRequest = CameraStartCaptureRequest( + camera = camera, + exposureTime = request.exposureTime + request.initialPause, + savePath = Files.createTempDirectory("darv"), + ) + + override val id = "DARV.Job.${ID.increment()}" + + override val subject = PublishSubject.create() + + init { + val cameraExposureStep = CameraExposureStep(cameraRequest) + cameraExposureStep.registerCameraCaptureListener(this) + + val initialPauseDelayStep = DelayStep(request.initialPause) + initialPauseDelayStep.registerDelayStepListener(this) + + val guidePulseDuration = request.exposureTime.dividedBy(2L) + val forwardGuidePulseRequest = GuidePulseRequest(guideOutput, direction, guidePulseDuration) + val forwardGuidePulseStep = GuidePulseStep(forwardGuidePulseRequest) + forwardGuidePulseStep.registerGuidePulseListener(this) + + val backwardGuidePulseRequest = GuidePulseRequest(guideOutput, direction.reversed, guidePulseDuration) + val backwardGuidePulseStep = GuidePulseStep(backwardGuidePulseRequest) + backwardGuidePulseStep.registerGuidePulseListener(this) + + val guideFlow = SimpleFlowStep(initialPauseDelayStep, forwardGuidePulseStep, backwardGuidePulseStep) + add(SimpleSplitStep(cameraExposureStep, guideFlow)) + } + + override fun beforeJob(jobExecution: JobExecution) { + onNext(DARVStarted(camera, guideOutput, request.initialPause, direction)) + } + + override fun afterJob(jobExecution: JobExecution) { + onNext(DARVFinished(camera, guideOutput)) + } + + override fun onExposureFinished(step: CameraExposureStep, stepExecution: StepExecution) { + val savePath = stepExecution.context[CameraExposureStep.SAVE_PATH] as Path + onNext(CameraExposureFinished(step.camera, 1, 1, Duration.ZERO, 1.0, Duration.ZERO, savePath)) + } + + override fun onGuidePulseElapsed(step: GuidePulseStep, stepExecution: StepExecution) { + val direction = step.request.direction + val remainingTime = stepExecution.context[DelayStep.REMAINING_TIME] as Duration + val progress = stepExecution.context[DelayStep.PROGRESS] as Double + val state = if (direction == this.direction) DARVState.FORWARD else DARVState.BACKWARD + onNext(DARVGuidePulseElapsed(camera, guideOutput, remainingTime, progress, direction, state)) + } + + override fun onDelayElapsed(step: DelayStep, stepExecution: StepExecution) { + val remainingTime = stepExecution.context[DelayStep.REMAINING_TIME] as Duration + val progress = stepExecution.context[DelayStep.PROGRESS] as Double + onNext(DARVInitialPauseElapsed(camera, guideOutput, remainingTime, progress)) + } + + override fun stop(mayInterruptIfRunning: Boolean) { + super.stop(mayInterruptIfRunning) + close() + } + + companion object { + + @JvmStatic private val ID = Incrementer() + } +} diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentEvent.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentEvent.kt deleted file mode 100644 index 340487f0b..000000000 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentEvent.kt +++ /dev/null @@ -1,14 +0,0 @@ -package nebulosa.api.alignment.polar.darv - -import nebulosa.api.sequencer.SequenceJobEvent -import nebulosa.indi.device.camera.Camera -import nebulosa.indi.device.guide.GuideOutput - -sealed interface DARVPolarAlignmentEvent : SequenceJobEvent { - - val camera: Camera - - val guideOutput: GuideOutput - - val state: DARVPolarAlignmentState -} diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentExecutor.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentExecutor.kt deleted file mode 100644 index eb722e5d6..000000000 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentExecutor.kt +++ /dev/null @@ -1,144 +0,0 @@ -package nebulosa.api.alignment.polar.darv - -import io.reactivex.rxjava3.functions.Consumer -import nebulosa.api.alignment.polar.darv.DARVPolarAlignmentState.BACKWARD -import nebulosa.api.alignment.polar.darv.DARVPolarAlignmentState.FORWARD -import nebulosa.api.cameras.CameraCaptureEvent -import nebulosa.api.cameras.CameraStartCaptureRequest -import nebulosa.api.guiding.GuidePulseEvent -import nebulosa.api.guiding.GuidePulseRequest -import nebulosa.api.sequencer.* -import nebulosa.api.sequencer.tasklets.delay.DelayElapsed -import nebulosa.api.services.MessageService -import nebulosa.indi.device.camera.Camera -import nebulosa.indi.device.guide.GuideOutput -import nebulosa.log.loggerFor -import org.springframework.batch.core.JobExecution -import org.springframework.batch.core.JobExecutionListener -import org.springframework.batch.core.JobParameters -import org.springframework.batch.core.launch.JobLauncher -import org.springframework.batch.core.launch.JobOperator -import org.springframework.stereotype.Component -import java.nio.file.Path -import java.util.* - -/** - * @see Reference - */ -@Component -class DARVPolarAlignmentExecutor( - private val jobOperator: JobOperator, - private val jobLauncher: JobLauncher, - private val messageService: MessageService, - private val capturesPath: Path, - private val sequenceFlowFactory: SequenceFlowFactory, - private val sequenceTaskletFactory: SequenceTaskletFactory, - private val sequenceJobFactory: SequenceJobFactory, -) : SequenceJobExecutor, Consumer, JobExecutionListener { - - private val runningSequenceJobs = LinkedList() - - @Synchronized - override fun execute(request: DARVStart): DARVSequenceJob { - val camera = requireNotNull(request.camera) - val guideOutput = requireNotNull(request.guideOutput) - - if (isRunning(camera, guideOutput)) { - throw IllegalStateException("DARV Polar Alignment job is already running") - } - - LOG.info("starting DARV polar alignment. data={}", request) - - val cameraRequest = CameraStartCaptureRequest( - camera = camera, - exposureTime = request.exposureTime + request.initialPause, - savePath = Path.of("$capturesPath", "${camera.name}-DARV.fits") - ) - - val cameraExposureTasklet = sequenceTaskletFactory.cameraExposure(cameraRequest) - cameraExposureTasklet.subscribe(this) - val cameraExposureFlow = sequenceFlowFactory.cameraExposure(cameraExposureTasklet) - - val guidePulseDuration = request.exposureTime.dividedBy(2L) - val initialPauseDelayTasklet = sequenceTaskletFactory.delay(request.initialPause) - initialPauseDelayTasklet.subscribe(this) - - val direction = if (request.reversed) request.direction.reversed else request.direction - - val forwardGuidePulseRequest = GuidePulseRequest(guideOutput, direction, guidePulseDuration) - val forwardGuidePulseTasklet = sequenceTaskletFactory.guidePulse(forwardGuidePulseRequest) - forwardGuidePulseTasklet.subscribe(this) - - val backwardGuidePulseRequest = GuidePulseRequest(guideOutput, direction.reversed, guidePulseDuration) - val backwardGuidePulseTasklet = sequenceTaskletFactory.guidePulse(backwardGuidePulseRequest) - backwardGuidePulseTasklet.subscribe(this) - - val guidePulseFlow = sequenceFlowFactory.guidePulse(initialPauseDelayTasklet, forwardGuidePulseTasklet, backwardGuidePulseTasklet) - - val darvJob = sequenceJobFactory.darvPolarAlignment(cameraExposureFlow, guidePulseFlow, this, cameraExposureTasklet) - - return jobLauncher - .run(darvJob, JobParameters()) - .let { DARVSequenceJob(camera, guideOutput, request, darvJob, it) } - .also(runningSequenceJobs::add) - } - - @Synchronized - fun stop(camera: Camera, guideOutput: GuideOutput) { - val jobExecution = jobExecutionFor(camera, guideOutput) ?: return - jobOperator.stop(jobExecution.id) - } - - @Suppress("NOTHING_TO_INLINE") - private inline fun jobExecutionFor(camera: Camera, guideOutput: GuideOutput): JobExecution? { - return sequenceJobFor(camera, guideOutput)?.jobExecution - } - - fun isRunning(camera: Camera, guideOutput: GuideOutput): Boolean { - return sequenceJobFor(camera, guideOutput)?.jobExecution?.isRunning ?: false - } - - override fun accept(event: SequenceTaskletEvent) { - if (event !is SequenceJobEvent) { - LOG.warn("unaccepted sequence task event: {}", event) - return - } - - val (camera, guideOutput, data) = sequenceJobWithId(event.jobExecution.jobId) ?: return - - val messageEvent = when (event) { - // Initial pulse event. - is DelayElapsed -> DARVPolarAlignmentInitialPauseElapsed(camera, guideOutput, event) - // Forward & backward guide pulse event. - is GuidePulseEvent -> { - val direction = event.tasklet.request.direction - val duration = event.tasklet.request.duration - val state = if ((direction == data.direction) != data.reversed) FORWARD else BACKWARD - DARVPolarAlignmentGuidePulseElapsed(camera, guideOutput, state, direction, duration, event.progress, event.jobExecution) - } - is CameraCaptureEvent -> event - else -> return - } - - messageService.sendMessage(messageEvent) - } - - override fun beforeJob(jobExecution: JobExecution) { - val (camera, guideOutput) = sequenceJobWithId(jobExecution.jobId) ?: return - messageService.sendMessage(DARVPolarAlignmentStarted(camera, guideOutput, jobExecution)) - } - - override fun afterJob(jobExecution: JobExecution) { - val (camera, guideOutput) = sequenceJobWithId(jobExecution.jobId) ?: return - messageService.sendMessage(DARVPolarAlignmentFinished(camera, guideOutput, jobExecution)) - } - - override fun iterator(): Iterator { - return runningSequenceJobs.iterator() - } - - companion object { - - @JvmStatic private val LOG = loggerFor() - } -} diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentFinished.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentFinished.kt deleted file mode 100644 index 016a753ed..000000000 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentFinished.kt +++ /dev/null @@ -1,20 +0,0 @@ -package nebulosa.api.alignment.polar.darv - -import com.fasterxml.jackson.annotation.JsonIgnore -import nebulosa.api.services.MessageEvent -import nebulosa.indi.device.camera.Camera -import nebulosa.indi.device.guide.GuideOutput -import org.springframework.batch.core.JobExecution - -data class DARVPolarAlignmentFinished( - override val camera: Camera, - override val guideOutput: GuideOutput, - @JsonIgnore override val jobExecution: JobExecution, -) : MessageEvent, DARVPolarAlignmentEvent { - - override val progress = 1.0 - - override val state = DARVPolarAlignmentState.IDLE - - override val eventName = "DARV_POLAR_ALIGNMENT_FINISHED" -} diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentGuidePulseElapsed.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentGuidePulseElapsed.kt deleted file mode 100644 index b06cc2d0c..000000000 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentGuidePulseElapsed.kt +++ /dev/null @@ -1,22 +0,0 @@ -package nebulosa.api.alignment.polar.darv - -import com.fasterxml.jackson.annotation.JsonIgnore -import nebulosa.api.services.MessageEvent -import nebulosa.guiding.GuideDirection -import nebulosa.indi.device.camera.Camera -import nebulosa.indi.device.guide.GuideOutput -import org.springframework.batch.core.JobExecution -import java.time.Duration - -data class DARVPolarAlignmentGuidePulseElapsed( - override val camera: Camera, - override val guideOutput: GuideOutput, - override val state: DARVPolarAlignmentState, - val direction: GuideDirection, - val remainingTime: Duration, - override val progress: Double, - @JsonIgnore override val jobExecution: JobExecution, -) : MessageEvent, DARVPolarAlignmentEvent { - - override val eventName = "DARV_POLAR_ALIGNMENT_UPDATED" -} diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentInitialPauseElapsed.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentInitialPauseElapsed.kt deleted file mode 100644 index bebe3d285..000000000 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentInitialPauseElapsed.kt +++ /dev/null @@ -1,28 +0,0 @@ -package nebulosa.api.alignment.polar.darv - -import com.fasterxml.jackson.annotation.JsonIgnore -import nebulosa.api.sequencer.tasklets.delay.DelayEvent -import nebulosa.api.services.MessageEvent -import nebulosa.indi.device.camera.Camera -import nebulosa.indi.device.guide.GuideOutput -import org.springframework.batch.core.JobExecution -import java.time.Duration - -data class DARVPolarAlignmentInitialPauseElapsed( - override val camera: Camera, - override val guideOutput: GuideOutput, - val pauseTime: Duration, - val remainingTime: Duration, - override val progress: Double, - @JsonIgnore override val jobExecution: JobExecution, -) : MessageEvent, DARVPolarAlignmentEvent { - - constructor(camera: Camera, guideOutput: GuideOutput, delay: DelayEvent) : this( - camera, guideOutput, delay.tasklet.duration, - delay.remainingTime, delay.progress, delay.jobExecution - ) - - override val state = DARVPolarAlignmentState.INITIAL_PAUSE - - override val eventName = "DARV_POLAR_ALIGNMENT_UPDATED" -} diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentStarted.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentStarted.kt deleted file mode 100644 index 0ba1998f5..000000000 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentStarted.kt +++ /dev/null @@ -1,20 +0,0 @@ -package nebulosa.api.alignment.polar.darv - -import com.fasterxml.jackson.annotation.JsonIgnore -import nebulosa.api.services.MessageEvent -import nebulosa.indi.device.camera.Camera -import nebulosa.indi.device.guide.GuideOutput -import org.springframework.batch.core.JobExecution - -data class DARVPolarAlignmentStarted( - override val camera: Camera, - override val guideOutput: GuideOutput, - @JsonIgnore override val jobExecution: JobExecution, -) : MessageEvent, DARVPolarAlignmentEvent { - - override val progress = 0.0 - - override val state = DARVPolarAlignmentState.INITIAL_PAUSE - - override val eventName = "DARV_POLAR_ALIGNMENT_STARTED" -} diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVSequenceJob.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVSequenceJob.kt deleted file mode 100644 index 6ea79d91a..000000000 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVSequenceJob.kt +++ /dev/null @@ -1,18 +0,0 @@ -package nebulosa.api.alignment.polar.darv - -import nebulosa.api.sequencer.SequenceJob -import nebulosa.indi.device.camera.Camera -import nebulosa.indi.device.guide.GuideOutput -import org.springframework.batch.core.Job -import org.springframework.batch.core.JobExecution - -data class DARVSequenceJob( - val camera: Camera, - val guideOutput: GuideOutput, - val data: DARVStart, - override val job: Job, - override val jobExecution: JobExecution, -) : SequenceJob { - - override val devices = listOf(camera, guideOutput) -} diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVStart.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVStartRequest.kt similarity index 96% rename from api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVStart.kt rename to api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVStartRequest.kt index cb5580384..eea8091ab 100644 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVStart.kt +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVStartRequest.kt @@ -8,7 +8,7 @@ import org.hibernate.validator.constraints.time.DurationMax import org.hibernate.validator.constraints.time.DurationMin import java.time.Duration -data class DARVStart( +data class DARVStartRequest( @JsonIgnore val camera: Camera? = null, @JsonIgnore val guideOutput: GuideOutput? = null, @field:DurationMin(seconds = 1) @field:DurationMax(seconds = 600) val exposureTime: Duration = Duration.ZERO, diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVStarted.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVStarted.kt new file mode 100644 index 000000000..066e6f704 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVStarted.kt @@ -0,0 +1,17 @@ +package nebulosa.api.alignment.polar.darv + +import nebulosa.guiding.GuideDirection +import nebulosa.indi.device.camera.Camera +import nebulosa.indi.device.guide.GuideOutput +import java.time.Duration + +data class DARVStarted( + override val camera: Camera, + override val guideOutput: GuideOutput, + override val remainingTime: Duration, + override val direction: GuideDirection, +) : DARVEvent { + + override val progress = 0.0 + override val state = DARVState.INITIAL_PAUSE +} diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentState.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVState.kt similarity index 73% rename from api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentState.kt rename to api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVState.kt index 61ed4e749..d8721cc45 100644 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentState.kt +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVState.kt @@ -1,6 +1,6 @@ package nebulosa.api.alignment.polar.darv -enum class DARVPolarAlignmentState { +enum class DARVState { IDLE, INITIAL_PAUSE, FORWARD, diff --git a/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasUpdateTask.kt b/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasUpdateTask.kt index 6f68d214c..20d490cae 100644 --- a/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasUpdateTask.kt +++ b/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasUpdateTask.kt @@ -2,9 +2,9 @@ package nebulosa.api.atlas import com.fasterxml.jackson.core.type.TypeReference import com.fasterxml.jackson.databind.ObjectMapper -import nebulosa.api.notification.NotificationEvent +import nebulosa.api.messages.MessageService +import nebulosa.api.notifications.NotificationEvent import nebulosa.api.preferences.PreferenceService -import nebulosa.api.services.MessageService import nebulosa.log.loggerFor import okhttp3.OkHttpClient import okhttp3.Request diff --git a/api/src/main/kotlin/nebulosa/api/beans/configurations/BatchConfiguration.kt b/api/src/main/kotlin/nebulosa/api/beans/configurations/BatchConfiguration.kt deleted file mode 100644 index c60ed06a6..000000000 --- a/api/src/main/kotlin/nebulosa/api/beans/configurations/BatchConfiguration.kt +++ /dev/null @@ -1,52 +0,0 @@ -package nebulosa.api.beans.configurations - -import nebulosa.common.concurrency.DaemonThreadFactory -import nebulosa.log.loggerFor -import org.springframework.batch.core.configuration.support.DefaultBatchConfiguration -import org.springframework.beans.factory.annotation.Qualifier -import org.springframework.boot.autoconfigure.batch.BatchDataSourceScriptDatabaseInitializer -import org.springframework.boot.autoconfigure.batch.BatchProperties -import org.springframework.boot.context.properties.ConfigurationProperties -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration -import org.springframework.core.task.SimpleAsyncTaskExecutor -import org.springframework.core.task.TaskExecutor -import org.springframework.transaction.PlatformTransactionManager -import javax.sql.DataSource - -@Configuration -class BatchConfiguration( - @Qualifier("batchDataSource") private val batchDataSource: DataSource, - @Qualifier("batchTransactionManager") private val batchTransactionManager: PlatformTransactionManager, -) : DefaultBatchConfiguration() { - - override fun getDataSource(): DataSource { - return batchDataSource - } - - override fun getTransactionManager(): PlatformTransactionManager { - return batchTransactionManager - } - - override fun getTaskExecutor(): TaskExecutor { - return SimpleAsyncTaskExecutor(DaemonThreadFactory) - } - - @Bean - @ConfigurationProperties(prefix = "spring.batch") - fun batchProperties(): BatchProperties { - return BatchProperties() - } - - @Bean - fun batchDataSourceScriptDatabaseInitializer(batchProperties: BatchProperties): BatchDataSourceScriptDatabaseInitializer { - val initializer = BatchDataSourceScriptDatabaseInitializer(batchDataSource, batchProperties.jdbc) - LOG.info("batch database initialized: {}", initializer.initializeDatabase()) - return initializer - } - - companion object { - - @JvmStatic private val LOG = loggerFor() - } -} 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 47f1130fa..81108f9ee 100644 --- a/api/src/main/kotlin/nebulosa/api/beans/configurations/BeanConfiguration.kt +++ b/api/src/main/kotlin/nebulosa/api/beans/configurations/BeanConfiguration.kt @@ -5,8 +5,7 @@ import com.fasterxml.jackson.databind.deser.std.StdDeserializer import com.fasterxml.jackson.databind.module.SimpleModule import com.fasterxml.jackson.databind.ser.std.StdSerializer import com.fasterxml.jackson.module.kotlin.kotlinModule -import nebulosa.common.concurrency.DaemonThreadFactory -import nebulosa.common.concurrency.Incrementer +import nebulosa.batch.processing.AsyncJobLauncher import nebulosa.common.json.PathDeserializer import nebulosa.common.json.PathSerializer import nebulosa.guiding.Guider @@ -24,7 +23,6 @@ import org.greenrobot.eventbus.EventBus import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration -import org.springframework.core.task.SimpleAsyncTaskExecutor import org.springframework.http.converter.HttpMessageConverter import org.springframework.http.converter.StringHttpMessageConverter import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor @@ -102,8 +100,7 @@ class BeanConfiguration { @Bean fun threadPoolTaskExecutor(): ThreadPoolTaskExecutor { val taskExecutor = ThreadPoolTaskExecutor() - taskExecutor.corePoolSize = 8 - taskExecutor.maxPoolSize = 32 + taskExecutor.corePoolSize = 32 taskExecutor.initialize() return taskExecutor } @@ -118,15 +115,6 @@ class BeanConfiguration { .executorService(threadPoolTaskExecutor.threadPoolExecutor) .installDefaultEventBus()!! - @Bean - fun flowIncrementer() = Incrementer() - - @Bean - fun stepIncrementer() = Incrementer() - - @Bean - fun jobIncrementer() = Incrementer() - @Bean fun phd2Client() = PHD2Client() @@ -134,7 +122,7 @@ class BeanConfiguration { fun phd2Guider(phd2Client: PHD2Client): Guider = PHD2Guider(phd2Client) @Bean - fun simpleAsyncTaskExecutor() = SimpleAsyncTaskExecutor(DaemonThreadFactory) + fun asyncJobLauncher(threadPoolTaskExecutor: ThreadPoolTaskExecutor) = AsyncJobLauncher(threadPoolTaskExecutor) @Bean fun webMvcConfigurer( diff --git a/api/src/main/kotlin/nebulosa/api/beans/configurations/DataSourceConfiguration.kt b/api/src/main/kotlin/nebulosa/api/beans/configurations/DataSourceConfiguration.kt index 25eba0293..8d314a907 100644 --- a/api/src/main/kotlin/nebulosa/api/beans/configurations/DataSourceConfiguration.kt +++ b/api/src/main/kotlin/nebulosa/api/beans/configurations/DataSourceConfiguration.kt @@ -3,14 +3,12 @@ package nebulosa.api.beans.configurations import com.zaxxer.hikari.HikariConfig import com.zaxxer.hikari.HikariDataSource import jakarta.persistence.EntityManagerFactory -import org.springframework.beans.factory.annotation.Qualifier import org.springframework.beans.factory.annotation.Value import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Primary import org.springframework.data.jpa.repository.config.EnableJpaRepositories -import org.springframework.jdbc.datasource.DataSourceTransactionManager import org.springframework.orm.jpa.JpaTransactionManager import org.springframework.transaction.PlatformTransactionManager import javax.sql.DataSource @@ -18,24 +16,13 @@ import javax.sql.DataSource @Configuration class DataSourceConfiguration { - @Value("\${spring.datasource.url}") private lateinit var mainDataSourceUrl: String - @Value("\${spring.batch.datasource.url}") private lateinit var batchDataSourceUrl: String + @Value("\${spring.datasource.url}") private lateinit var dataSourceUrl: String + @Bean @Primary - @Bean("mainDataSource") - fun mainDataSource(): DataSource { + fun dataSource(): DataSource { val config = HikariConfig() - config.jdbcUrl = mainDataSourceUrl - config.driverClassName = DRIVER_CLASS_NAME - config.maximumPoolSize = 1 - config.minimumIdle = 1 - return HikariDataSource(config) - } - - @Bean("batchDataSource") - fun batchDataSource(): DataSource { - val config = HikariConfig() - config.jdbcUrl = batchDataSourceUrl + config.jdbcUrl = dataSourceUrl config.driverClassName = DRIVER_CLASS_NAME config.maximumPoolSize = 1 config.minimumIdle = 1 @@ -45,51 +32,27 @@ class DataSourceConfiguration { @Configuration @EnableJpaRepositories( basePackages = ["nebulosa.api"], - entityManagerFactoryRef = "mainEntityManagerFactory", - transactionManagerRef = "mainTransactionManager" + entityManagerFactoryRef = "entityManagerFactory", + transactionManagerRef = "transactionManager" ) class Main { + @Bean @Primary - @Bean("mainEntityManagerFactory") - fun mainEntityManagerFactory( + fun entityManagerFactory( builder: EntityManagerFactoryBuilder, dataSource: DataSource, ) = builder .dataSource(dataSource) .packages("nebulosa.api") - .persistenceUnit("mainPersistenceUnit") + .persistenceUnit("persistenceUnit") .build()!! + @Bean @Primary - @Bean("mainTransactionManager") - fun mainTransactionManager(mainEntityManagerFactory: EntityManagerFactory): PlatformTransactionManager { + fun transactionManager(entityManagerFactory: EntityManagerFactory): PlatformTransactionManager { // Fix "no transactions is in progress": https://stackoverflow.com/a/33397173 - return JpaTransactionManager(mainEntityManagerFactory) - } - } - - @Configuration - @EnableJpaRepositories( - basePackages = ["org.springframework.batch.core.migration"], - entityManagerFactoryRef = "batchEntityManagerFactory", - transactionManagerRef = "batchTransactionManager" - ) - class Batch { - - @Bean("batchEntityManagerFactory") - fun batchEntityManagerFactory( - builder: EntityManagerFactoryBuilder, - @Qualifier("batchDataSource") dataSource: DataSource, - ) = builder - .dataSource(dataSource) - .packages("org.springframework.batch.core.migration") - .persistenceUnit("batchPersistenceUnit") - .build()!! - - @Bean("batchTransactionManager") - fun batchTransactionManager(@Qualifier("batchDataSource") dataSource: DataSource): PlatformTransactionManager { - return DataSourceTransactionManager(dataSource) + return JpaTransactionManager(entityManagerFactory) } } diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureElapsed.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureElapsed.kt deleted file mode 100644 index eae7f59d5..000000000 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureElapsed.kt +++ /dev/null @@ -1,20 +0,0 @@ -package nebulosa.api.cameras - -import com.fasterxml.jackson.annotation.JsonIgnore -import nebulosa.api.sequencer.SequenceStepEvent -import nebulosa.indi.device.camera.Camera -import org.springframework.batch.core.StepExecution -import java.time.Duration - -data class CameraCaptureElapsed( - override val camera: Camera, - val exposureCount: Int, - val remainingTime: Duration, - override val progress: Double, - val elapsedTime: Duration, - @JsonIgnore override val stepExecution: StepExecution, - @JsonIgnore override val tasklet: CameraExposureTasklet, -) : CameraCaptureEvent, SequenceStepEvent { - - override val eventName = "CAMERA_CAPTURE_ELAPSED" -} diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureEvent.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureEvent.kt index 61e3d1e48..99d8d98b2 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureEvent.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureEvent.kt @@ -1,11 +1,36 @@ package nebulosa.api.cameras -import nebulosa.api.sequencer.SequenceJobEvent -import nebulosa.api.sequencer.SequenceTaskletEvent -import nebulosa.api.services.MessageEvent +import nebulosa.api.messages.MessageEvent import nebulosa.indi.device.camera.Camera +import java.nio.file.Path +import java.time.Duration -sealed interface CameraCaptureEvent : MessageEvent, SequenceTaskletEvent, SequenceJobEvent { +sealed interface CameraCaptureEvent : MessageEvent { val camera: Camera + + val state: CameraCaptureState + + val exposureAmount: Int + + val exposureCount: Int + + val captureElapsedTime: Duration + + val captureProgress: Double + + val captureRemainingTime: Duration + + val exposureProgress: Double + + val exposureRemainingTime: Duration + + val waitRemainingTime: Duration + + val waitProgress: Double + + val savePath: Path? + + override val eventName + get() = "CAMERA_CAPTURE_ELAPSED" } diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureExecutor.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureExecutor.kt index ceca9b7ad..fcfd67088 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureExecutor.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureExecutor.kt @@ -1,71 +1,67 @@ package nebulosa.api.cameras import io.reactivex.rxjava3.functions.Consumer -import nebulosa.api.sequencer.SequenceJobExecutor -import nebulosa.api.sequencer.SequenceJobFactory -import nebulosa.api.services.MessageService +import nebulosa.api.messages.MessageEvent +import nebulosa.api.messages.MessageService +import nebulosa.batch.processing.JobExecution +import nebulosa.batch.processing.JobLauncher +import nebulosa.guiding.Guider import nebulosa.indi.device.camera.Camera import nebulosa.log.loggerFor -import org.springframework.batch.core.JobExecution -import org.springframework.batch.core.JobParameters -import org.springframework.batch.core.launch.JobLauncher -import org.springframework.batch.core.launch.JobOperator import org.springframework.stereotype.Component import java.util.* @Component class CameraCaptureExecutor( - private val jobOperator: JobOperator, - private val jobLauncher: JobLauncher, private val messageService: MessageService, - private val sequenceJobFactory: SequenceJobFactory, -) : SequenceJobExecutor, Consumer { + private val guider: Guider, + private val jobLauncher: JobLauncher, +) : Consumer { - private val runningSequenceJobs = LinkedList() + private val jobExecutions = LinkedList() @Synchronized - override fun execute(request: CameraStartCaptureRequest): CameraSequenceJob { + fun execute(request: CameraStartCaptureRequest) { val camera = requireNotNull(request.camera) - check(!isCapturing(camera)) { "job is already running for camera: [${camera.name}]" } check(camera.connected) { "camera is not connected" } + check(!isCapturing(camera)) { "job is already running for camera: [${camera.name}]" } + + LOG.info("starting camera capture. request={}", request) + + val cameraCaptureJob = CameraCaptureJob(request, guider) + cameraCaptureJob.subscribe(this) + val jobExecution = jobLauncher.launch(cameraCaptureJob) + jobExecutions.add(jobExecution) + } - LOG.info("starting camera capture. data={}", request) + fun findJobExecution(camera: Camera): JobExecution? { + for (i in jobExecutions.indices.reversed()) { + val jobExecution = jobExecutions[i] + val job = jobExecution.job as CameraCaptureJob - val cameraCaptureJob = if (request.isLoop) { - sequenceJobFactory.cameraLoopCapture(request, this) - } else { - sequenceJobFactory.cameraCapture(request, this) + if (!jobExecution.isDone && job.camera === camera) { + return jobExecution + } } - return jobLauncher - .run(cameraCaptureJob, JobParameters()) - .let { CameraSequenceJob(camera, request, cameraCaptureJob, it) } - .also(runningSequenceJobs::add) + return null } + @Synchronized fun stop(camera: Camera) { - val jobExecution = jobExecutionFor(camera) ?: return - jobOperator.stop(jobExecution.id) + val jobExecution = findJobExecution(camera) ?: return + jobLauncher.stop(jobExecution) } fun isCapturing(camera: Camera): Boolean { - return sequenceJobFor(camera)?.jobExecution?.isRunning ?: false - } - - @Suppress("NOTHING_TO_INLINE") - private inline fun jobExecutionFor(camera: Camera): JobExecution? { - return sequenceJobFor(camera)?.jobExecution + return findJobExecution(camera) != null } - override fun accept(event: CameraCaptureEvent) { + override fun accept(event: MessageEvent) { messageService.sendMessage(event) } - override fun iterator(): Iterator { - return runningSequenceJobs.iterator() - } - companion object { @JvmStatic private val LOG = loggerFor() diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureFinished.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureFinished.kt index deeb6425a..5ed7a736a 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureFinished.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureFinished.kt @@ -1,16 +1,21 @@ package nebulosa.api.cameras -import com.fasterxml.jackson.annotation.JsonIgnore import nebulosa.indi.device.camera.Camera -import org.springframework.batch.core.JobExecution +import java.time.Duration data class CameraCaptureFinished( override val camera: Camera, - @JsonIgnore override val jobExecution: JobExecution, - @JsonIgnore override val tasklet: CameraExposureTasklet, + override val exposureAmount: Int, + override val captureElapsedTime: Duration, ) : CameraCaptureEvent { - override val progress = 1.0 - - override val eventName = "CAMERA_CAPTURE_FINISHED" + override val exposureCount = exposureAmount + override val captureProgress = 1.0 + override val captureRemainingTime = Duration.ZERO!! + override val exposureProgress = 1.0 + override val exposureRemainingTime = Duration.ZERO!! + override val state = CameraCaptureState.CAPTURE_FINISHED + override val waitRemainingTime = Duration.ZERO!! + override val waitProgress = 0.0 + override val savePath = null } diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureIsSettling.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureIsSettling.kt new file mode 100644 index 000000000..1595cb5c9 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureIsSettling.kt @@ -0,0 +1,21 @@ +package nebulosa.api.cameras + +import nebulosa.indi.device.camera.Camera +import java.time.Duration + +data class CameraCaptureIsSettling( + override val camera: Camera, + override val exposureAmount: Int, + override val exposureCount: Int, + override val captureElapsedTime: Duration, + override val captureProgress: Double, + override val captureRemainingTime: Duration, +) : CameraCaptureEvent { + + override val state = CameraCaptureState.WAITING + override val exposureProgress = 1.0 + override val exposureRemainingTime = Duration.ZERO!! + override val savePath = null + override val waitProgress = 0.0 + override val waitRemainingTime = Duration.ZERO!! +} diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureIsWaiting.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureIsWaiting.kt index 4af353c99..0da124e81 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureIsWaiting.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureIsWaiting.kt @@ -1,19 +1,21 @@ package nebulosa.api.cameras -import com.fasterxml.jackson.annotation.JsonIgnore -import nebulosa.api.sequencer.SequenceStepEvent import nebulosa.indi.device.camera.Camera -import org.springframework.batch.core.StepExecution import java.time.Duration data class CameraCaptureIsWaiting( override val camera: Camera, - val waitDuration: Duration, - val remainingTime: Duration, - override val progress: Double, - @JsonIgnore override val stepExecution: StepExecution, - @JsonIgnore override val tasklet: CameraExposureTasklet, -) : CameraCaptureEvent, SequenceStepEvent { + override val exposureAmount: Int, + override val exposureCount: Int, + override val captureElapsedTime: Duration, + override val captureProgress: Double, + override val captureRemainingTime: Duration, + override val waitProgress: Double, + override val waitRemainingTime: Duration, +) : CameraCaptureEvent { - override val eventName = "CAMERA_CAPTURE_WAITING" + override val state = CameraCaptureState.WAITING + override val exposureProgress = 1.0 + override val exposureRemainingTime = Duration.ZERO!! + override val savePath = null } diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureJob.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureJob.kt new file mode 100644 index 000000000..112f433fb --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureJob.kt @@ -0,0 +1,138 @@ +package nebulosa.api.cameras + +import io.reactivex.rxjava3.subjects.PublishSubject +import nebulosa.api.guiding.DitherAfterExposureStep +import nebulosa.api.guiding.WaitForSettleStep +import nebulosa.api.messages.MessageEvent +import nebulosa.batch.processing.JobExecution +import nebulosa.batch.processing.PublishSubscribe +import nebulosa.batch.processing.SimpleJob +import nebulosa.batch.processing.StepExecution +import nebulosa.batch.processing.delay.DelayStep +import nebulosa.common.concurrency.Incrementer +import nebulosa.guiding.Guider +import java.nio.file.Path +import java.time.Duration + +data class CameraCaptureJob( + private val request: CameraStartCaptureRequest, + private val guider: Guider, +) : SimpleJob(), PublishSubscribe, CameraCaptureListener { + + @JvmField val camera = requireNotNull(request.camera) + + private val cameraExposureStep = if (request.isLoop) CameraLoopExposureStep(request) + else CameraExposureStep(request) + + override val id = "CameraCapture.Job.${ID.increment()}" + + override val subject = PublishSubject.create() + + init { + if (cameraExposureStep is CameraExposureStep) { + val waitForSettleStep = WaitForSettleStep(guider) + val ditherStep = DitherAfterExposureStep(request.dither, guider) + val cameraDelayStep = DelayStep(request.exposureDelay) + val delayAndWaitForSettleStep = DelayAndWaitForSettleStep(camera, cameraDelayStep, waitForSettleStep) + + cameraDelayStep.registerDelayStepListener(cameraExposureStep) + delayAndWaitForSettleStep.subscribe(this) + + add(waitForSettleStep) + add(cameraExposureStep) + + repeat(request.exposureAmount - 1) { + add(delayAndWaitForSettleStep) + add(cameraExposureStep) + add(ditherStep) + } + } else { + add(cameraExposureStep) + } + + cameraExposureStep.registerCameraCaptureListener(this) + } + + override fun onCaptureStarted(step: CameraExposureStep, jobExecution: JobExecution) { + onNext(CameraCaptureStarted(step.camera, step.exposureAmount, step.estimatedCaptureTime, step.exposureTime)) + } + + override fun onExposureStarted(step: CameraExposureStep, stepExecution: StepExecution) { + sendCameraExposureEvent(step, stepExecution, CameraCaptureState.EXPOSURE_STARTED) + } + + override fun onExposureElapsed(step: CameraExposureStep, stepExecution: StepExecution) { + val waiting = stepExecution.context[CameraExposureStep.CAPTURE_WAITING] as Boolean + val state = if (waiting) CameraCaptureState.WAITING else CameraCaptureState.EXPOSURING + sendCameraExposureEvent(step, stepExecution, state) + } + + override fun onExposureFinished(step: CameraExposureStep, stepExecution: StepExecution) { + sendCameraExposureEvent(step, stepExecution, CameraCaptureState.EXPOSURE_FINISHED) + } + + override fun onCaptureFinished(step: CameraExposureStep, jobExecution: JobExecution) { + val captureElapsedTime = jobExecution.context[CameraExposureStep.CAPTURE_ELAPSED_TIME] as Duration + onNext(CameraCaptureFinished(step.camera, step.exposureAmount, captureElapsedTime)) + } + + fun sendCameraExposureEvent(step: CameraExposureStep, stepExecution: StepExecution, state: CameraCaptureState) { + val exposureCount = stepExecution.context[CameraExposureStep.EXPOSURE_COUNT] as Int + val captureElapsedTime = stepExecution.context[CameraExposureStep.CAPTURE_ELAPSED_TIME] as Duration + val captureProgress = stepExecution.context[CameraExposureStep.CAPTURE_PROGRESS] as Double + val captureRemainingTime = stepExecution.context[CameraExposureStep.CAPTURE_REMAINING_TIME] as Duration + + val event = when (state) { + CameraCaptureState.WAITING -> { + val waitProgress = stepExecution.context[DelayStep.PROGRESS] as Double + val waitRemainingTime = stepExecution.context[DelayStep.REMAINING_TIME] as Duration + + CameraCaptureIsWaiting( + step.camera, + step.exposureAmount, exposureCount, captureElapsedTime, captureProgress, captureRemainingTime, + waitProgress, waitRemainingTime + ) + } + CameraCaptureState.SETTLING -> { + CameraCaptureIsSettling(step.camera, step.exposureAmount, exposureCount, captureElapsedTime, captureProgress, captureRemainingTime) + } + CameraCaptureState.EXPOSURING -> { + val exposureProgress = stepExecution.context[CameraExposureStep.EXPOSURE_PROGRESS] as Double + val exposureRemainingTime = stepExecution.context[CameraExposureStep.EXPOSURE_REMAINING_TIME] as Duration + + CameraExposureElapsed( + step.camera, + step.exposureAmount, exposureCount, captureElapsedTime, captureProgress, captureRemainingTime, + exposureProgress, exposureRemainingTime + ) + } + CameraCaptureState.EXPOSURE_STARTED -> { + val exposureRemainingTime = stepExecution.context[CameraExposureStep.EXPOSURE_REMAINING_TIME] as Duration + + CameraExposureStarted( + step.camera, + step.exposureAmount, exposureCount, captureElapsedTime, + captureProgress, captureRemainingTime, exposureRemainingTime + ) + } + CameraCaptureState.EXPOSURE_FINISHED -> { + val savePath = stepExecution.context[CameraExposureStep.SAVE_PATH] as Path + + CameraExposureFinished( + step.camera, + step.exposureAmount, exposureCount, + captureElapsedTime, captureProgress, captureRemainingTime, + savePath + ) + } + else -> return + } + + onNext(event) + } + + companion object { + + @JvmStatic private val ID = Incrementer() + } +} diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureListener.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureListener.kt new file mode 100644 index 000000000..3b685d6b4 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureListener.kt @@ -0,0 +1,17 @@ +package nebulosa.api.cameras + +import nebulosa.batch.processing.JobExecution +import nebulosa.batch.processing.StepExecution + +interface CameraCaptureListener { + + fun onCaptureStarted(step: CameraExposureStep, jobExecution: JobExecution) = Unit + + fun onExposureStarted(step: CameraExposureStep, stepExecution: StepExecution) = Unit + + fun onExposureElapsed(step: CameraExposureStep, stepExecution: StepExecution) = Unit + + fun onExposureFinished(step: CameraExposureStep, stepExecution: StepExecution) = Unit + + fun onCaptureFinished(step: CameraExposureStep, jobExecution: JobExecution) = Unit +} diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureStarted.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureStarted.kt index 370f38639..8976d5f54 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureStarted.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureStarted.kt @@ -1,25 +1,21 @@ package nebulosa.api.cameras -import com.fasterxml.jackson.annotation.JsonIgnore import nebulosa.indi.device.camera.Camera -import org.springframework.batch.core.JobExecution import java.time.Duration data class CameraCaptureStarted( override val camera: Camera, - val looping: Boolean, - val estimatedTime: Duration, - @JsonIgnore override val jobExecution: JobExecution, - @JsonIgnore override val tasklet: CameraExposureTasklet, + override val exposureAmount: Int, + override val captureRemainingTime: Duration, + override val exposureRemainingTime: Duration, ) : CameraCaptureEvent { - val exposureAmount - get() = tasklet.request.exposureAmount - - val exposureTime - get() = tasklet.request.exposureTime - - override val progress = 0.0 - - override val eventName = "CAMERA_CAPTURE_STARTED" + override val exposureCount = 1 + override val captureElapsedTime = Duration.ZERO!! + override val captureProgress = 0.0 + override val exposureProgress = 0.0 + override val state = CameraCaptureState.CAPTURE_STARTED + override val waitRemainingTime = Duration.ZERO!! + override val waitProgress = 0.0 + override val savePath = null } diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureState.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureState.kt new file mode 100644 index 000000000..b64506b33 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureState.kt @@ -0,0 +1,11 @@ +package nebulosa.api.cameras + +enum class CameraCaptureState { + CAPTURE_STARTED, + EXPOSURE_STARTED, + EXPOSURING, + WAITING, + SETTLING, + EXPOSURE_FINISHED, + CAPTURE_FINISHED, +} diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraEventHandler.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraEventHandler.kt index 98778f5c9..28e49ed67 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraEventHandler.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraEventHandler.kt @@ -3,7 +3,7 @@ package nebulosa.api.cameras import io.reactivex.rxjava3.subjects.PublishSubject import jakarta.annotation.PostConstruct import nebulosa.api.beans.annotations.Subscriber -import nebulosa.api.services.MessageService +import nebulosa.api.messages.MessageService import nebulosa.indi.device.PropertyChangedEvent import nebulosa.indi.device.camera.Camera import nebulosa.indi.device.camera.CameraAttached diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureElapsed.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureElapsed.kt index d34418b39..1b0810160 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureElapsed.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureElapsed.kt @@ -1,18 +1,21 @@ package nebulosa.api.cameras -import com.fasterxml.jackson.annotation.JsonIgnore import nebulosa.indi.device.camera.Camera -import org.springframework.batch.core.StepExecution import java.time.Duration data class CameraExposureElapsed( override val camera: Camera, + override val exposureAmount: Int, override val exposureCount: Int, - override val remainingTime: Duration, - override val progress: Double, - @JsonIgnore override val stepExecution: StepExecution, - @JsonIgnore override val tasklet: CameraExposureTasklet, + override val captureElapsedTime: Duration, + override val captureProgress: Double, + override val captureRemainingTime: Duration, + override val exposureProgress: Double, + override val exposureRemainingTime: Duration, ) : CameraExposureEvent { - override val eventName = "CAMERA_EXPOSURE_ELAPSED" + override val state = CameraCaptureState.EXPOSURING + override val waitProgress = 0.0 + override val waitRemainingTime = Duration.ZERO!! + override val savePath = null } diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureEvent.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureEvent.kt index aae3da0a4..b2b004111 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureEvent.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureEvent.kt @@ -1,13 +1,3 @@ package nebulosa.api.cameras -import nebulosa.api.sequencer.SequenceStepEvent -import java.time.Duration - -sealed interface CameraExposureEvent : CameraCaptureEvent, SequenceStepEvent { - - override val tasklet: CameraStartCaptureTasklet - - val exposureCount: Int - - val remainingTime: Duration -} +sealed interface CameraExposureEvent : CameraCaptureEvent diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureFinished.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureFinished.kt index 9e4b82d7b..c666488f7 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureFinished.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureFinished.kt @@ -1,22 +1,23 @@ package nebulosa.api.cameras -import com.fasterxml.jackson.annotation.JsonIgnore import nebulosa.indi.device.camera.Camera -import org.springframework.batch.core.StepExecution import java.nio.file.Path import java.time.Duration data class CameraExposureFinished( override val camera: Camera, + override val exposureAmount: Int, override val exposureCount: Int, - @JsonIgnore override val stepExecution: StepExecution, - @JsonIgnore override val tasklet: CameraExposureTasklet, - val savePath: Path?, + override val captureElapsedTime: Duration, + override val captureProgress: Double, + override val captureRemainingTime: Duration, + override val savePath: Path, ) : CameraExposureEvent { - override val remainingTime = Duration.ZERO!! - - override val progress = 1.0 - - override val eventName = "CAMERA_EXPOSURE_FINISHED" + override val state = CameraCaptureState.EXPOSURE_FINISHED + override val exposureProgress = 0.0 + override val exposureRemainingTime = Duration.ZERO!! + override val waitProgress = 0.0 + override val waitRemainingTime = Duration.ZERO!! } + diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureStarted.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureStarted.kt index 94166488e..8647f5ac7 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureStarted.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureStarted.kt @@ -1,20 +1,21 @@ package nebulosa.api.cameras -import com.fasterxml.jackson.annotation.JsonIgnore import nebulosa.indi.device.camera.Camera -import org.springframework.batch.core.StepExecution +import java.time.Duration data class CameraExposureStarted( override val camera: Camera, + override val exposureAmount: Int, override val exposureCount: Int, - @JsonIgnore override val stepExecution: StepExecution, - @JsonIgnore override val tasklet: CameraExposureTasklet, + override val captureElapsedTime: Duration, + override val captureProgress: Double, + override val captureRemainingTime: Duration, + override val exposureRemainingTime: Duration, ) : CameraExposureEvent { - override val remainingTime - get() = tasklet.request.exposureTime - - override val progress = 0.0 - - override val eventName = "CAMERA_EXPOSURE_STARTED" + override val state = CameraCaptureState.EXPOSURE_STARTED + override val exposureProgress = 0.0 + override val waitProgress = 0.0 + override val waitRemainingTime = Duration.ZERO!! + override val savePath = null } diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureStep.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureStep.kt new file mode 100644 index 000000000..7c1746df6 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureStep.kt @@ -0,0 +1,212 @@ +package nebulosa.api.cameras + +import nebulosa.batch.processing.JobExecution +import nebulosa.batch.processing.StepExecution +import nebulosa.batch.processing.StepResult +import nebulosa.batch.processing.delay.DelayStep +import nebulosa.batch.processing.delay.DelayStepListener +import nebulosa.common.concurrency.CountUpDownLatch +import nebulosa.indi.device.camera.* +import nebulosa.io.transferAndClose +import nebulosa.log.loggerFor +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import java.io.InputStream +import java.nio.file.Path +import java.time.Duration +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import kotlin.io.path.createParentDirectories +import kotlin.io.path.outputStream + +data class CameraExposureStep(override val request: CameraStartCaptureRequest) : CameraStartCaptureStep, DelayStepListener { + + @JvmField val camera = requireNotNull(request.camera) + + @JvmField val exposureTime = request.exposureTime + @JvmField val exposureAmount = request.exposureAmount + @JvmField val exposureDelay = request.exposureDelay + + @JvmField val estimatedCaptureTime: Duration = if (request.isLoop) Duration.ZERO + else Duration.ofNanos(exposureTime.toNanos() * exposureAmount + exposureDelay.toNanos() * (exposureAmount - 1)) + + private val latch = CountUpDownLatch() + private val listeners = LinkedHashSet() + + @Volatile private var aborted = false + @Volatile private var exposureCount = 0 + @Volatile private var captureElapsedTime = Duration.ZERO!! + + private lateinit var stepExecution: StepExecution + + override fun registerCameraCaptureListener(listener: CameraCaptureListener): Boolean { + return listeners.add(listener) + } + + override fun unregisterCameraCaptureListener(listener: CameraCaptureListener): Boolean { + return listeners.remove(listener) + } + + @Subscribe(threadMode = ThreadMode.ASYNC) + fun onCameraEvent(event: CameraEvent) { + if (event.device === camera) { + when (event) { + is CameraFrameCaptured -> { + save(event.fits) + } + is CameraExposureAborted, + is CameraExposureFailed, + is CameraDetached -> { + latch.reset() + aborted = true + } + is CameraExposureProgressChanged -> { + val exposureRemainingTime = event.device.exposureTime + val exposureElapsedTime = exposureTime - exposureRemainingTime + val exposureProgress = exposureElapsedTime.toNanos().toDouble() / exposureTime.toNanos() + onCameraExposureElapsed(exposureElapsedTime, exposureRemainingTime, exposureProgress) + } + } + } + } + + override fun beforeJob(jobExecution: JobExecution) { + camera.enableBlob() + EventBus.getDefault().register(this) + + captureElapsedTime = Duration.ZERO + + jobExecution.context[CAPTURE_ELAPSED_TIME] = Duration.ZERO + jobExecution.context[CAPTURE_PROGRESS] = 0.0 + jobExecution.context[CAPTURE_REMAINING_TIME] = exposureTime + jobExecution.context[EXPOSURE_ELAPSED_TIME] = Duration.ZERO + jobExecution.context[EXPOSURE_REMAINING_TIME] = estimatedCaptureTime + jobExecution.context[EXPOSURE_PROGRESS] = 0.0 + + listeners.forEach { it.onCaptureStarted(this, jobExecution) } + } + + override fun afterJob(jobExecution: JobExecution) { + camera.disableBlob() + EventBus.getDefault().unregister(this) + listeners.forEach { it.onCaptureFinished(this, jobExecution) } + listeners.clear() + } + + override fun execute(stepExecution: StepExecution): StepResult { + this.stepExecution = stepExecution + executeCapture(stepExecution) + return StepResult.FINISHED + } + + override fun stop(mayInterruptIfRunning: Boolean) { + LOG.info("stopping camera exposure. camera={}", camera) + camera.abortCapture() + camera.disableBlob() + aborted = true + latch.reset() + } + + override fun onDelayElapsed(step: DelayStep, stepExecution: StepExecution) { + stepExecution.context[CAPTURE_WAITING] = true + val waitTime = stepExecution.context[DelayStep.WAIT_TIME] as Duration + captureElapsedTime += waitTime + onCameraExposureElapsed(Duration.ZERO, Duration.ZERO, 1.0) + } + + private fun executeCapture(stepExecution: StepExecution) { + if (camera.connected && !aborted) { + synchronized(camera) { + LOG.info("starting camera exposure. camera={}", camera) + + latch.countUp() + + stepExecution.context[CAPTURE_WAITING] = false + stepExecution.context[EXPOSURE_COUNT] = ++exposureCount + + listeners.forEach { it.onExposureStarted(this, stepExecution) } + + if (request.width > 0 && request.height > 0) { + camera.frame(request.x, request.y, request.width, request.height) + } + + camera.frameType(request.frameType) + camera.frameFormat(request.frameFormat) + camera.bin(request.binX, request.binY) + camera.gain(request.gain) + camera.offset(request.offset) + camera.startCapture(exposureTime) + + latch.await() + + captureElapsedTime += exposureTime + + LOG.info("camera exposure finished. aborted={}, camera={}", aborted, camera) + } + } + } + + private fun save(stream: InputStream) { + val savePath = if (request.autoSave) { + val now = LocalDateTime.now() + val fileName = "%s-%s.fits".format(now.format(DATE_TIME_FORMAT), request.frameType) + Path.of("${request.savePath}", fileName) + } else { + val fileName = "%s.fits".format(camera.name) + Path.of("${request.savePath}", fileName) + } + + try { + LOG.info("saving FITS. path={}", savePath) + + savePath.createParentDirectories() + stream.transferAndClose(savePath.outputStream()) + + stepExecution.context[SAVE_PATH] = savePath + + listeners.forEach { it.onExposureFinished(this, stepExecution) } + } catch (e: Throwable) { + LOG.error("failed to save FITS", e) + aborted = true + } finally { + latch.countDown() + } + } + + private fun onCameraExposureElapsed(elapsedTime: Duration, remainingTime: Duration, progress: Double) { + val captureElapsedTime = captureElapsedTime + elapsedTime + var captureRemainingTime = Duration.ZERO + var captureProgress = 0.0 + + if (!request.isLoop) { + captureRemainingTime = if (estimatedCaptureTime > captureElapsedTime) estimatedCaptureTime - captureElapsedTime else Duration.ZERO + captureProgress = (estimatedCaptureTime - captureRemainingTime).toNanos().toDouble() / estimatedCaptureTime.toNanos() + } + + stepExecution.context[EXPOSURE_ELAPSED_TIME] = elapsedTime + stepExecution.context[EXPOSURE_REMAINING_TIME] = remainingTime + stepExecution.context[EXPOSURE_PROGRESS] = progress + stepExecution.context[CAPTURE_ELAPSED_TIME] = captureElapsedTime + stepExecution.context[CAPTURE_REMAINING_TIME] = captureRemainingTime + stepExecution.context[CAPTURE_PROGRESS] = captureProgress + + listeners.forEach { it.onExposureElapsed(this, stepExecution) } + } + + companion object { + + const val EXPOSURE_COUNT = "CAMERA_EXPOSURE.EXPOSURE_COUNT" + const val SAVE_PATH = "CAMERA_EXPOSURE.SAVE_PATH" + const val EXPOSURE_ELAPSED_TIME = "CAMERA_EXPOSURE.EXPOSURE_ELAPSED_TIME" + const val EXPOSURE_REMAINING_TIME = "CAMERA_EXPOSURE.EXPOSURE_REMAINING_TIME" + const val EXPOSURE_PROGRESS = "CAMERA_EXPOSURE.EXPOSURE_PROGRESS" + const val CAPTURE_ELAPSED_TIME = "CAMERA_EXPOSURE.CAPTURE_ELAPSED_TIME" + const val CAPTURE_REMAINING_TIME = "CAMERA_EXPOSURE.CAPTURE_REMAINING_TIME" + const val CAPTURE_PROGRESS = "CAMERA_EXPOSURE.CAPTURE_PROGRESS" + const val CAPTURE_WAITING = "CAMERA_EXPOSURE.WAITING" + + @JvmStatic private val LOG = loggerFor() + @JvmStatic private val DATE_TIME_FORMAT = DateTimeFormatter.ofPattern("yyyyMMdd.HHmmssSSS") + } +} diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureTasklet.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureTasklet.kt deleted file mode 100644 index 2222b5538..000000000 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureTasklet.kt +++ /dev/null @@ -1,175 +0,0 @@ -package nebulosa.api.cameras - -import io.reactivex.rxjava3.functions.Consumer -import nebulosa.api.sequencer.PublishSequenceTasklet -import nebulosa.api.sequencer.tasklets.delay.DelayEvent -import nebulosa.common.concurrency.CountUpDownLatch -import nebulosa.indi.device.camera.* -import nebulosa.io.transferAndClose -import nebulosa.log.loggerFor -import org.greenrobot.eventbus.EventBus -import org.greenrobot.eventbus.Subscribe -import org.greenrobot.eventbus.ThreadMode -import org.springframework.batch.core.JobExecution -import org.springframework.batch.core.JobExecutionListener -import org.springframework.batch.core.StepContribution -import org.springframework.batch.core.StepExecution -import org.springframework.batch.core.scope.context.ChunkContext -import org.springframework.batch.repeat.RepeatStatus -import java.io.InputStream -import java.nio.file.Path -import java.time.Duration -import java.time.LocalDateTime -import java.time.format.DateTimeFormatter -import java.util.concurrent.atomic.AtomicBoolean -import kotlin.io.path.createParentDirectories -import kotlin.io.path.outputStream - -data class CameraExposureTasklet(override val request: CameraStartCaptureRequest) : - PublishSequenceTasklet(), CameraStartCaptureTasklet, JobExecutionListener, Consumer { - - private val latch = CountUpDownLatch() - private val aborted = AtomicBoolean() - - @Volatile private var exposureCount = 0 - @Volatile private var captureElapsedTime = Duration.ZERO!! - @Volatile private var stepExecution: StepExecution? = null - - private val camera = requireNotNull(request.camera) - private val exposureTime = request.exposureTime - private val exposureDelay = request.exposureDelay - - private val estimatedTime = if (request.isLoop) Duration.ZERO - else Duration.ofNanos(exposureTime.toNanos() * request.exposureAmount + exposureDelay.toNanos() * (request.exposureAmount - 1)) - - @Subscribe(threadMode = ThreadMode.ASYNC) - fun onCameraEvent(event: CameraEvent) { - if (event.device === camera) { - when (event) { - is CameraFrameCaptured -> { - save(event.fits, stepExecution!!) - latch.countDown() - } - is CameraExposureAborted, - is CameraExposureFailed, - is CameraDetached -> { - latch.reset() - aborted.set(true) - } - is CameraExposureProgressChanged -> { - val exposureRemainingTime = event.device.exposureTime - val exposureElapsedTime = exposureTime - exposureRemainingTime - val exposureProgress = exposureElapsedTime.toNanos().toDouble() / exposureTime.toNanos() - onCameraExposureElapsed(exposureElapsedTime, exposureRemainingTime, exposureProgress) - } - } - } - } - - override fun beforeJob(jobExecution: JobExecution) { - camera.enableBlob() - EventBus.getDefault().register(this) - onNext(CameraCaptureStarted(camera, request.isLoop, estimatedTime, jobExecution, this)) - captureElapsedTime = Duration.ZERO - } - - override fun afterJob(jobExecution: JobExecution) { - camera.disableBlob() - EventBus.getDefault().unregister(this) - onNext(CameraCaptureFinished(camera, jobExecution, this)) - close() - } - - override fun execute(contribution: StepContribution, chunkContext: ChunkContext): RepeatStatus { - executeCapture(contribution) - return RepeatStatus.FINISHED - } - - override fun stop() { - LOG.info("stopping exposure. camera=${camera.name}") - camera.abortCapture() - camera.disableBlob() - aborted.set(true) - latch.reset() - } - - override fun accept(event: DelayEvent) { - captureElapsedTime += event.waitDuration - onNext(CameraCaptureIsWaiting(camera, event.waitDuration, event.remainingTime, event.progress, event.stepExecution, this)) - onCameraExposureElapsed(Duration.ZERO, Duration.ZERO, 1.0) - } - - private fun executeCapture(contribution: StepContribution) { - stepExecution = contribution.stepExecution - - if (camera.connected && !aborted.get()) { - synchronized(camera) { - latch.countUp() - - exposureCount++ - - onNext(CameraExposureStarted(camera, exposureCount, stepExecution!!, this)) - - if (request.width > 0 && request.height > 0) { - camera.frame(request.x, request.y, request.width, request.height) - } - - camera.frameType(request.frameType) - camera.frameFormat(request.frameFormat) - camera.bin(request.binX, request.binY) - camera.gain(request.gain) - camera.offset(request.offset) - camera.startCapture(exposureTime) - - latch.await() - - captureElapsedTime += exposureTime - - LOG.info("camera exposure finished") - } - } - } - - private fun save(stream: InputStream, stepExecution: StepExecution) { - val savePath = if (request.autoSave) { - val now = LocalDateTime.now() - val fileName = "%s-%s.fits".format(now.format(DATE_TIME_FORMAT), request.frameType) - Path.of("${request.savePath}", fileName) - } else { - val fileName = "%s.fits".format(camera.name) - Path.of("${request.savePath}", fileName) - } - - try { - LOG.info("saving FITS at $savePath...") - - savePath.createParentDirectories() - stream.transferAndClose(savePath.outputStream()) - - onNext(CameraExposureFinished(camera, exposureCount, stepExecution, this, savePath)) - } catch (e: Throwable) { - LOG.error("failed to save FITS", e) - aborted.set(true) - } - } - - private fun onCameraExposureElapsed(elapsedTime: Duration, remainingTime: Duration, progress: Double) { - val totalElapsedTime = captureElapsedTime + elapsedTime - var captureRemainingTime = Duration.ZERO - var captureProgress = 0.0 - - if (!request.isLoop) { - captureRemainingTime = if (estimatedTime > totalElapsedTime) estimatedTime - totalElapsedTime else Duration.ZERO - captureProgress = (estimatedTime - captureRemainingTime).toNanos().toDouble() / estimatedTime.toNanos() - } - - onNext(CameraExposureElapsed(camera, exposureCount, remainingTime, progress, stepExecution!!, this)) - onNext(CameraCaptureElapsed(camera, exposureCount, captureRemainingTime, captureProgress, totalElapsedTime, stepExecution!!, this)) - } - - companion object { - - @JvmStatic private val LOG = loggerFor() - @JvmStatic private val DATE_TIME_FORMAT = DateTimeFormatter.ofPattern("yyyyMMdd.HHmmssSSS") - } -} diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraLoopExposureStep.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraLoopExposureStep.kt new file mode 100644 index 000000000..92cf7bd23 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraLoopExposureStep.kt @@ -0,0 +1,47 @@ +package nebulosa.api.cameras + +import nebulosa.batch.processing.JobExecution +import nebulosa.batch.processing.StepExecution +import nebulosa.batch.processing.StepResult +import nebulosa.batch.processing.delay.DelayStep + +data class CameraLoopExposureStep( + override val request: CameraStartCaptureRequest, +) : CameraStartCaptureStep { + + private val cameraExposureStep = CameraExposureStep(request) + private val delayStep = DelayStep(request.exposureDelay) + + init { + delayStep.registerDelayStepListener(cameraExposureStep) + } + + override fun registerCameraCaptureListener(listener: CameraCaptureListener): Boolean { + return cameraExposureStep.registerCameraCaptureListener(listener) + } + + override fun unregisterCameraCaptureListener(listener: CameraCaptureListener): Boolean { + return cameraExposureStep.unregisterCameraCaptureListener(listener) + } + + override fun execute(stepExecution: StepExecution): StepResult { + cameraExposureStep.execute(stepExecution) + delayStep.execute(stepExecution) + return StepResult.CONTINUABLE + } + + override fun stop(mayInterruptIfRunning: Boolean) { + cameraExposureStep.stop(mayInterruptIfRunning) + delayStep.stop(mayInterruptIfRunning) + } + + override fun beforeJob(jobExecution: JobExecution) { + cameraExposureStep.beforeJob(jobExecution) + delayStep.beforeJob(jobExecution) + } + + override fun afterJob(jobExecution: JobExecution) { + cameraExposureStep.afterJob(jobExecution) + delayStep.afterJob(jobExecution) + } +} diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraLoopExposureTasklet.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraLoopExposureTasklet.kt deleted file mode 100644 index 69e95472a..000000000 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraLoopExposureTasklet.kt +++ /dev/null @@ -1,42 +0,0 @@ -package nebulosa.api.cameras - -import nebulosa.api.sequencer.PublishSequenceTasklet -import nebulosa.api.sequencer.tasklets.delay.DelayTasklet -import org.springframework.batch.core.JobExecution -import org.springframework.batch.core.JobExecutionListener -import org.springframework.batch.core.StepContribution -import org.springframework.batch.core.scope.context.ChunkContext -import org.springframework.batch.repeat.RepeatStatus - -data class CameraLoopExposureTasklet(override val request: CameraStartCaptureRequest) : - PublishSequenceTasklet(), CameraStartCaptureTasklet, JobExecutionListener { - - private val exposureTasklet = CameraExposureTasklet(request) - private val delayTasklet = DelayTasklet(request.exposureDelay) - - init { - exposureTasklet.subscribe(this) - delayTasklet.subscribe(exposureTasklet) - } - - override fun execute(contribution: StepContribution, chunkContext: ChunkContext): RepeatStatus { - exposureTasklet.execute(contribution, chunkContext) - delayTasklet.execute(contribution, chunkContext) - return RepeatStatus.CONTINUABLE - } - - override fun stop() { - exposureTasklet.stop() - delayTasklet.stop() - } - - override fun beforeJob(jobExecution: JobExecution) { - exposureTasklet.beforeJob(jobExecution) - delayTasklet.beforeJob(jobExecution) - } - - override fun afterJob(jobExecution: JobExecution) { - exposureTasklet.afterJob(jobExecution) - delayTasklet.afterJob(jobExecution) - } -} diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraMessageEvent.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraMessageEvent.kt index 911a1fd85..4e69fe521 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraMessageEvent.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraMessageEvent.kt @@ -1,6 +1,6 @@ package nebulosa.api.cameras -import nebulosa.api.services.DeviceMessageEvent +import nebulosa.api.messages.DeviceMessageEvent import nebulosa.indi.device.camera.Camera data class CameraMessageEvent( diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraSequenceJob.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraSequenceJob.kt deleted file mode 100644 index d16338932..000000000 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraSequenceJob.kt +++ /dev/null @@ -1,16 +0,0 @@ -package nebulosa.api.cameras - -import nebulosa.api.sequencer.SequenceJob -import nebulosa.indi.device.camera.Camera -import org.springframework.batch.core.Job -import org.springframework.batch.core.JobExecution - -data class CameraSequenceJob( - val camera: Camera, - val data: CameraStartCaptureRequest, - override val job: Job, - override val jobExecution: JobExecution, -) : SequenceJob { - - override val devices = listOf(camera) -} diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraStartCaptureStep.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraStartCaptureStep.kt new file mode 100644 index 000000000..8a57e813a --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraStartCaptureStep.kt @@ -0,0 +1,13 @@ +package nebulosa.api.cameras + +import nebulosa.batch.processing.JobExecutionListener +import nebulosa.batch.processing.Step + +sealed interface CameraStartCaptureStep : Step, JobExecutionListener { + + val request: CameraStartCaptureRequest + + fun registerCameraCaptureListener(listener: CameraCaptureListener): Boolean + + fun unregisterCameraCaptureListener(listener: CameraCaptureListener): Boolean +} diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraStartCaptureTasklet.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraStartCaptureTasklet.kt deleted file mode 100644 index dffc38539..000000000 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraStartCaptureTasklet.kt +++ /dev/null @@ -1,8 +0,0 @@ -package nebulosa.api.cameras - -import nebulosa.api.sequencer.SequenceTasklet - -sealed interface CameraStartCaptureTasklet : SequenceTasklet { - - val request: CameraStartCaptureRequest -} diff --git a/api/src/main/kotlin/nebulosa/api/cameras/DelayAndWaitForSettleStep.kt b/api/src/main/kotlin/nebulosa/api/cameras/DelayAndWaitForSettleStep.kt new file mode 100644 index 000000000..6dd8e714b --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/cameras/DelayAndWaitForSettleStep.kt @@ -0,0 +1,54 @@ +package nebulosa.api.cameras + +import io.reactivex.rxjava3.subjects.PublishSubject +import nebulosa.api.guiding.WaitForSettleListener +import nebulosa.api.guiding.WaitForSettleStep +import nebulosa.batch.processing.PublishSubscribe +import nebulosa.batch.processing.SimpleSplitStep +import nebulosa.batch.processing.StepExecution +import nebulosa.batch.processing.delay.DelayStep +import nebulosa.batch.processing.delay.DelayStepListener +import nebulosa.indi.device.camera.Camera +import java.time.Duration + +data class DelayAndWaitForSettleStep( + @JvmField val camera: Camera, + @JvmField val cameraDelayStep: DelayStep, + @JvmField val waitForSettleStep: WaitForSettleStep, +) : SimpleSplitStep(cameraDelayStep, waitForSettleStep), PublishSubscribe, DelayStepListener, WaitForSettleListener { + + @Volatile private var settling = false + + override val subject = PublishSubject.create() + + override fun beforeStep(stepExecution: StepExecution) { + cameraDelayStep.registerDelayStepListener(this) + waitForSettleStep.registerWaitForSettleListener(this) + } + + override fun afterStep(stepExecution: StepExecution) { + cameraDelayStep.unregisterDelayStepListener(this) + waitForSettleStep.unregisterWaitForSettleListener(this) + } + + override fun onSettleStarted(step: WaitForSettleStep, stepExecution: StepExecution) { + settling = true + } + + override fun onSettleFinished(step: WaitForSettleStep, stepExecution: StepExecution) { + settling = false + } + + override fun onDelayElapsed(step: DelayStep, stepExecution: StepExecution) { + val send = settling && (stepExecution.context[DelayStep.PROGRESS] as Double) < 1.0 + + if (send) { + val exposureCount = stepExecution.context[CameraExposureStep.EXPOSURE_COUNT] as Int + val captureElapsedTime = stepExecution.context[CameraExposureStep.CAPTURE_ELAPSED_TIME] as Duration + val captureProgress = stepExecution.context[CameraExposureStep.CAPTURE_PROGRESS] as Double + val captureRemainingTime = stepExecution.context[CameraExposureStep.CAPTURE_REMAINING_TIME] as Duration + + onNext(CameraCaptureIsSettling(camera, 0, exposureCount, captureElapsedTime, captureProgress, captureRemainingTime)) + } + } +} diff --git a/api/src/main/kotlin/nebulosa/api/focusers/FocuserEventHandler.kt b/api/src/main/kotlin/nebulosa/api/focusers/FocuserEventHandler.kt index 2b218cde3..e77752446 100644 --- a/api/src/main/kotlin/nebulosa/api/focusers/FocuserEventHandler.kt +++ b/api/src/main/kotlin/nebulosa/api/focusers/FocuserEventHandler.kt @@ -3,7 +3,7 @@ package nebulosa.api.focusers import io.reactivex.rxjava3.subjects.PublishSubject import jakarta.annotation.PostConstruct import nebulosa.api.beans.annotations.Subscriber -import nebulosa.api.services.MessageService +import nebulosa.api.messages.MessageService import nebulosa.indi.device.PropertyChangedEvent import nebulosa.indi.device.focuser.Focuser import nebulosa.indi.device.focuser.FocuserAttached diff --git a/api/src/main/kotlin/nebulosa/api/focusers/FocuserMessageEvent.kt b/api/src/main/kotlin/nebulosa/api/focusers/FocuserMessageEvent.kt index d75cbcdbc..cae010f3f 100644 --- a/api/src/main/kotlin/nebulosa/api/focusers/FocuserMessageEvent.kt +++ b/api/src/main/kotlin/nebulosa/api/focusers/FocuserMessageEvent.kt @@ -1,6 +1,6 @@ package nebulosa.api.focusers -import nebulosa.api.services.DeviceMessageEvent +import nebulosa.api.messages.DeviceMessageEvent import nebulosa.indi.device.focuser.Focuser data class FocuserMessageEvent( diff --git a/api/src/main/kotlin/nebulosa/api/guiding/DitherAfterExposureStep.kt b/api/src/main/kotlin/nebulosa/api/guiding/DitherAfterExposureStep.kt new file mode 100644 index 000000000..073e66c72 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/guiding/DitherAfterExposureStep.kt @@ -0,0 +1,47 @@ +package nebulosa.api.guiding + +import nebulosa.batch.processing.Step +import nebulosa.batch.processing.StepExecution +import nebulosa.batch.processing.StepResult +import nebulosa.common.concurrency.CountUpDownLatch +import nebulosa.guiding.GuideState +import nebulosa.guiding.Guider +import nebulosa.guiding.GuiderListener + +data class DitherAfterExposureStep( + @JvmField val request: DitherAfterExposureRequest, + @JvmField val guider: Guider, +) : Step, GuiderListener { + + private val ditherLatch = CountUpDownLatch() + @Volatile private var exposureCount = 0 + + override fun execute(stepExecution: StepExecution): StepResult { + if (guider.canDither && request.enabled && guider.state == GuideState.GUIDING) { + if (exposureCount < request.afterExposures) { + try { + guider.registerGuiderListener(this) + ditherLatch.countUp() + guider.dither(request.amount, request.raOnly) + ditherLatch.await() + } finally { + guider.unregisterGuiderListener(this) + } + } + + if (++exposureCount >= request.afterExposures) { + exposureCount = 0 + } + } + + return StepResult.FINISHED + } + + override fun stop(mayInterruptIfRunning: Boolean) { + ditherLatch.reset() + } + + override fun onDithered(dx: Double, dy: Double) { + ditherLatch.reset() + } +} diff --git a/api/src/main/kotlin/nebulosa/api/guiding/DitherAfterExposureTasklet.kt b/api/src/main/kotlin/nebulosa/api/guiding/DitherAfterExposureTasklet.kt deleted file mode 100644 index 059aca432..000000000 --- a/api/src/main/kotlin/nebulosa/api/guiding/DitherAfterExposureTasklet.kt +++ /dev/null @@ -1,50 +0,0 @@ -package nebulosa.api.guiding - -import nebulosa.common.concurrency.CountUpDownLatch -import nebulosa.guiding.GuideState -import nebulosa.guiding.Guider -import nebulosa.guiding.GuiderListener -import org.springframework.batch.core.StepContribution -import org.springframework.batch.core.scope.context.ChunkContext -import org.springframework.batch.core.step.tasklet.StoppableTasklet -import org.springframework.batch.repeat.RepeatStatus -import org.springframework.beans.factory.annotation.Autowired -import java.util.concurrent.atomic.AtomicInteger - -data class DitherAfterExposureTasklet(val request: DitherAfterExposureRequest) : StoppableTasklet, GuiderListener { - - @Autowired private lateinit var guider: Guider - - private val ditherLatch = CountUpDownLatch() - private val exposureCount = AtomicInteger() - - override fun execute(contribution: StepContribution, chunkContext: ChunkContext): RepeatStatus { - if (guider.canDither && request.enabled && guider.state == GuideState.GUIDING) { - if (exposureCount.get() < request.afterExposures) { - try { - guider.registerGuiderListener(this) - ditherLatch.countUp() - guider.dither(request.amount, request.raOnly) - ditherLatch.await() - } finally { - guider.unregisterGuiderListener(this) - } - } - - if (exposureCount.incrementAndGet() >= request.afterExposures) { - exposureCount.set(0) - } - } - - return RepeatStatus.FINISHED - } - - override fun stop() { - ditherLatch.reset(0) - guider.unregisterGuiderListener(this) - } - - override fun onDithered(dx: Double, dy: Double) { - ditherLatch.reset() - } -} diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputEventHandler.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputEventHandler.kt index 081d041f3..eea722189 100644 --- a/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputEventHandler.kt +++ b/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputEventHandler.kt @@ -3,7 +3,7 @@ package nebulosa.api.guiding import io.reactivex.rxjava3.subjects.PublishSubject import jakarta.annotation.PostConstruct import nebulosa.api.beans.annotations.Subscriber -import nebulosa.api.services.MessageService +import nebulosa.api.messages.MessageService import nebulosa.indi.device.DeviceEvent import nebulosa.indi.device.PropertyChangedEvent import nebulosa.indi.device.guide.GuideOutput @@ -33,17 +33,16 @@ class GuideOutputEventHandler( fun onGuideOutputEvent(event: DeviceEvent) { val device = event.device ?: return - if (device.canPulseGuide) { - when (event) { - is PropertyChangedEvent -> { - throttler.onNext(event) - } - is GuideOutputAttached -> { - messageService.sendMessage(GuideOutputMessageEvent(GUIDE_OUTPUT_ATTACHED, event.device)) - } - is GuideOutputDetached -> { - messageService.sendMessage(GuideOutputMessageEvent(GUIDE_OUTPUT_DETACHED, event.device)) - } + if (device.canPulseGuide && event is PropertyChangedEvent) { + throttler.onNext(event) + } + + when (event) { + is GuideOutputAttached -> { + messageService.sendMessage(GuideOutputMessageEvent(GUIDE_OUTPUT_ATTACHED, event.device)) + } + is GuideOutputDetached -> { + messageService.sendMessage(GuideOutputMessageEvent(GUIDE_OUTPUT_DETACHED, event.device)) } } } diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputMessageEvent.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputMessageEvent.kt index 9ea9e3673..b19092080 100644 --- a/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputMessageEvent.kt +++ b/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputMessageEvent.kt @@ -1,6 +1,6 @@ package nebulosa.api.guiding -import nebulosa.api.services.DeviceMessageEvent +import nebulosa.api.messages.DeviceMessageEvent import nebulosa.indi.device.guide.GuideOutput data class GuideOutputMessageEvent( diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseElapsed.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseElapsed.kt deleted file mode 100644 index 9e0728c62..000000000 --- a/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseElapsed.kt +++ /dev/null @@ -1,14 +0,0 @@ -package nebulosa.api.guiding - -import com.fasterxml.jackson.annotation.JsonIgnore -import nebulosa.guiding.GuideDirection -import org.springframework.batch.core.StepExecution -import java.time.Duration - -data class GuidePulseElapsed( - val remainingTime: Duration, - override val progress: Double, - val direction: GuideDirection, - @JsonIgnore override val stepExecution: StepExecution, - @JsonIgnore override val tasklet: GuidePulseTasklet, -) : GuidePulseEvent 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 57aaad25b..000000000 --- a/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseEvent.kt +++ /dev/null @@ -1,9 +0,0 @@ -package nebulosa.api.guiding - -import nebulosa.api.sequencer.SequenceStepEvent -import nebulosa.api.sequencer.SequenceTaskletEvent - -sealed interface GuidePulseEvent : SequenceTaskletEvent, SequenceStepEvent { - - override val tasklet: GuidePulseTasklet -} diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseFinished.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseFinished.kt deleted file mode 100644 index c76953e06..000000000 --- a/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseFinished.kt +++ /dev/null @@ -1,12 +0,0 @@ -package nebulosa.api.guiding - -import com.fasterxml.jackson.annotation.JsonIgnore -import org.springframework.batch.core.StepExecution - -data class GuidePulseFinished( - @JsonIgnore override val stepExecution: StepExecution, - @JsonIgnore override val tasklet: GuidePulseTasklet, -) : GuidePulseEvent { - - override val progress = 1.0 -} diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseListener.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseListener.kt new file mode 100644 index 000000000..555969c0f --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseListener.kt @@ -0,0 +1,8 @@ +package nebulosa.api.guiding + +import nebulosa.batch.processing.StepExecution + +fun interface GuidePulseListener { + + fun onGuidePulseElapsed(step: GuidePulseStep, stepExecution: StepExecution) +} diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseStarted.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseStarted.kt deleted file mode 100644 index a275bcc08..000000000 --- a/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseStarted.kt +++ /dev/null @@ -1,12 +0,0 @@ -package nebulosa.api.guiding - -import com.fasterxml.jackson.annotation.JsonIgnore -import org.springframework.batch.core.StepExecution - -data class GuidePulseStarted( - @JsonIgnore override val stepExecution: StepExecution, - @JsonIgnore override val tasklet: GuidePulseTasklet, -) : GuidePulseEvent { - - override val progress = 0.0 -} diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseStep.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseStep.kt new file mode 100644 index 000000000..f501b6a1c --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseStep.kt @@ -0,0 +1,66 @@ +package nebulosa.api.guiding + +import nebulosa.batch.processing.Step +import nebulosa.batch.processing.StepExecution +import nebulosa.batch.processing.StepResult +import nebulosa.batch.processing.delay.DelayStep +import nebulosa.batch.processing.delay.DelayStepListener +import nebulosa.guiding.GuideDirection +import nebulosa.indi.device.guide.GuideOutput +import java.time.Duration + +data class GuidePulseStep(@JvmField val request: GuidePulseRequest) : Step, DelayStepListener { + + private val listeners = LinkedHashSet() + private val delayStep = DelayStep(request.duration) + + init { + delayStep.registerDelayStepListener(this) + } + + fun registerGuidePulseListener(listener: GuidePulseListener) { + listeners.add(listener) + } + + fun unregisterGuidePulseListener(listener: GuidePulseListener) { + listeners.remove(listener) + } + + override fun execute(stepExecution: StepExecution): StepResult { + val guideOutput = requireNotNull(request.guideOutput) + + // Force stop in reversed direction. + guideOutput.pulseGuide(Duration.ZERO, request.direction.reversed) + + if (guideOutput.pulseGuide(request.duration, request.direction)) { + delayStep.execute(stepExecution) + } + + return StepResult.FINISHED + } + + override fun stop(mayInterruptIfRunning: Boolean) { + request.guideOutput?.pulseGuide(Duration.ZERO, request.direction) + delayStep.stop() + } + + override fun onDelayElapsed(step: DelayStep, stepExecution: StepExecution) { + listeners.forEach { it.onGuidePulseElapsed(this, stepExecution) } + } + + companion object { + + @JvmStatic + private fun GuideOutput.pulseGuide(duration: Duration, direction: GuideDirection): Boolean { + when (direction) { + GuideDirection.NORTH -> guideNorth(duration) + GuideDirection.SOUTH -> guideSouth(duration) + GuideDirection.WEST -> guideWest(duration) + GuideDirection.EAST -> guideEast(duration) + else -> return false + } + + return true + } + } +} diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseTasklet.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseTasklet.kt deleted file mode 100644 index 40717cba0..000000000 --- a/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseTasklet.kt +++ /dev/null @@ -1,64 +0,0 @@ -package nebulosa.api.guiding - -import io.reactivex.rxjava3.functions.Consumer -import nebulosa.api.sequencer.PublishSequenceTasklet -import nebulosa.api.sequencer.tasklets.delay.DelayEvent -import nebulosa.api.sequencer.tasklets.delay.DelayTasklet -import nebulosa.guiding.GuideDirection -import nebulosa.indi.device.guide.GuideOutput -import org.springframework.batch.core.StepContribution -import org.springframework.batch.core.scope.context.ChunkContext -import org.springframework.batch.repeat.RepeatStatus -import java.time.Duration - -data class GuidePulseTasklet(val request: GuidePulseRequest) : PublishSequenceTasklet(), Consumer { - - private val delayTasklet = DelayTasklet(request.duration) - - init { - delayTasklet.subscribe(this) - } - - override fun execute(contribution: StepContribution, chunkContext: ChunkContext): RepeatStatus { - val guideOutput = requireNotNull(request.guideOutput) - val durationInMilliseconds = request.duration - - // Force stop in reversed direction. - guideOutput.pulseGuide(Duration.ZERO, request.direction.reversed) - - if (guideOutput.pulseGuide(durationInMilliseconds, request.direction)) { - delayTasklet.execute(contribution, chunkContext) - } - - return RepeatStatus.FINISHED - } - - override fun stop() { - request.guideOutput?.pulseGuide(Duration.ZERO, request.direction) - delayTasklet.stop() - } - - override fun accept(event: DelayEvent) { - val guidePulseEvent = if (event.isStarted) GuidePulseStarted(event.stepExecution, this) - else if (event.isFinished) GuidePulseFinished(event.stepExecution, this) - else GuidePulseElapsed(event.remainingTime, event.progress, request.direction, event.stepExecution, this) - - onNext(guidePulseEvent) - } - - companion object { - - @JvmStatic - private fun GuideOutput.pulseGuide(duration: Duration, direction: GuideDirection): Boolean { - when (direction) { - GuideDirection.NORTH -> guideNorth(duration) - GuideDirection.SOUTH -> guideSouth(duration) - GuideDirection.WEST -> guideWest(duration) - GuideDirection.EAST -> guideEast(duration) - else -> return false - } - - return true - } - } -} diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuiderMessageEvent.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuiderMessageEvent.kt index 6b13b0ca2..5cd58eadd 100644 --- a/api/src/main/kotlin/nebulosa/api/guiding/GuiderMessageEvent.kt +++ b/api/src/main/kotlin/nebulosa/api/guiding/GuiderMessageEvent.kt @@ -1,6 +1,6 @@ package nebulosa.api.guiding -import nebulosa.api.services.MessageEvent +import nebulosa.api.messages.MessageEvent data class GuiderMessageEvent( override val eventName: String, diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuidingService.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuidingService.kt index 3a62244ec..eee542c18 100644 --- a/api/src/main/kotlin/nebulosa/api/guiding/GuidingService.kt +++ b/api/src/main/kotlin/nebulosa/api/guiding/GuidingService.kt @@ -1,8 +1,8 @@ package nebulosa.api.guiding import jakarta.annotation.PreDestroy +import nebulosa.api.messages.MessageService import nebulosa.api.preferences.PreferenceService -import nebulosa.api.services.MessageService import nebulosa.guiding.GuideStar import nebulosa.guiding.GuideState import nebulosa.guiding.Guider diff --git a/api/src/main/kotlin/nebulosa/api/guiding/WaitForSettleListener.kt b/api/src/main/kotlin/nebulosa/api/guiding/WaitForSettleListener.kt new file mode 100644 index 000000000..fb289c0b1 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/guiding/WaitForSettleListener.kt @@ -0,0 +1,10 @@ +package nebulosa.api.guiding + +import nebulosa.batch.processing.StepExecution + +interface WaitForSettleListener { + + fun onSettleStarted(step: WaitForSettleStep, stepExecution: StepExecution) + + fun onSettleFinished(step: WaitForSettleStep, stepExecution: StepExecution) +} diff --git a/api/src/main/kotlin/nebulosa/api/guiding/WaitForSettleStep.kt b/api/src/main/kotlin/nebulosa/api/guiding/WaitForSettleStep.kt new file mode 100644 index 000000000..3765bee65 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/guiding/WaitForSettleStep.kt @@ -0,0 +1,37 @@ +package nebulosa.api.guiding + +import nebulosa.batch.processing.* +import nebulosa.common.concurrency.CancellationToken +import nebulosa.guiding.Guider + +data class WaitForSettleStep(@JvmField val guider: Guider) : Step, JobExecutionListener { + + private val cancellationToken = CancellationToken() + private val listeners = LinkedHashSet() + + fun registerWaitForSettleListener(listener: WaitForSettleListener) { + listeners.add(listener) + } + + fun unregisterWaitForSettleListener(listener: WaitForSettleListener) { + listeners.remove(listener) + } + + override fun execute(stepExecution: StepExecution): StepResult { + if (guider.isSettling && !cancellationToken.isCancelled) { + listeners.forEach { it.onSettleStarted(this, stepExecution) } + guider.waitForSettle(cancellationToken) + listeners.forEach { it.onSettleFinished(this, stepExecution) } + } + + return StepResult.FINISHED + } + + override fun stop(mayInterruptIfRunning: Boolean) { + cancellationToken.cancel() + } + + override fun afterJob(jobExecution: JobExecution) { + listeners.clear() + } +} diff --git a/api/src/main/kotlin/nebulosa/api/guiding/WaitForSettleTasklet.kt b/api/src/main/kotlin/nebulosa/api/guiding/WaitForSettleTasklet.kt deleted file mode 100644 index af47a6808..000000000 --- a/api/src/main/kotlin/nebulosa/api/guiding/WaitForSettleTasklet.kt +++ /dev/null @@ -1,24 +0,0 @@ -package nebulosa.api.guiding - -import nebulosa.guiding.Guider -import org.springframework.batch.core.StepContribution -import org.springframework.batch.core.scope.context.ChunkContext -import org.springframework.batch.core.step.tasklet.StoppableTasklet -import org.springframework.batch.repeat.RepeatStatus -import org.springframework.beans.factory.annotation.Autowired - -class WaitForSettleTasklet : StoppableTasklet { - - @Autowired private lateinit var guider: Guider - - override fun execute(contribution: StepContribution, chunkContext: ChunkContext): RepeatStatus { - if (guider.isSettling) { - guider.waitForSettle() - } - - return RepeatStatus.FINISHED - } - - override fun stop() { - } -} diff --git a/api/src/main/kotlin/nebulosa/api/indi/INDIEventHandler.kt b/api/src/main/kotlin/nebulosa/api/indi/INDIEventHandler.kt index aff9bd9cc..b8710dd05 100644 --- a/api/src/main/kotlin/nebulosa/api/indi/INDIEventHandler.kt +++ b/api/src/main/kotlin/nebulosa/api/indi/INDIEventHandler.kt @@ -1,7 +1,7 @@ package nebulosa.api.indi import nebulosa.api.beans.annotations.Subscriber -import nebulosa.api.services.MessageService +import nebulosa.api.messages.MessageService import nebulosa.indi.device.* import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode diff --git a/api/src/main/kotlin/nebulosa/api/indi/INDIMessageEvent.kt b/api/src/main/kotlin/nebulosa/api/indi/INDIMessageEvent.kt index 767fd7850..c689d81ab 100644 --- a/api/src/main/kotlin/nebulosa/api/indi/INDIMessageEvent.kt +++ b/api/src/main/kotlin/nebulosa/api/indi/INDIMessageEvent.kt @@ -1,6 +1,6 @@ package nebulosa.api.indi -import nebulosa.api.services.DeviceMessageEvent +import nebulosa.api.messages.DeviceMessageEvent import nebulosa.indi.device.Device import nebulosa.indi.device.DeviceMessageReceived import nebulosa.indi.device.DevicePropertyEvent diff --git a/api/src/main/kotlin/nebulosa/api/services/DeviceMessageEvent.kt b/api/src/main/kotlin/nebulosa/api/messages/DeviceMessageEvent.kt similarity index 79% rename from api/src/main/kotlin/nebulosa/api/services/DeviceMessageEvent.kt rename to api/src/main/kotlin/nebulosa/api/messages/DeviceMessageEvent.kt index 1968a78af..248354ff7 100644 --- a/api/src/main/kotlin/nebulosa/api/services/DeviceMessageEvent.kt +++ b/api/src/main/kotlin/nebulosa/api/messages/DeviceMessageEvent.kt @@ -1,4 +1,4 @@ -package nebulosa.api.services +package nebulosa.api.messages import nebulosa.indi.device.Device diff --git a/api/src/main/kotlin/nebulosa/api/services/MessageEvent.kt b/api/src/main/kotlin/nebulosa/api/messages/MessageEvent.kt similarity index 64% rename from api/src/main/kotlin/nebulosa/api/services/MessageEvent.kt rename to api/src/main/kotlin/nebulosa/api/messages/MessageEvent.kt index 57052c612..1d6aac26d 100644 --- a/api/src/main/kotlin/nebulosa/api/services/MessageEvent.kt +++ b/api/src/main/kotlin/nebulosa/api/messages/MessageEvent.kt @@ -1,4 +1,4 @@ -package nebulosa.api.services +package nebulosa.api.messages interface MessageEvent { diff --git a/api/src/main/kotlin/nebulosa/api/services/MessageService.kt b/api/src/main/kotlin/nebulosa/api/messages/MessageService.kt similarity index 98% rename from api/src/main/kotlin/nebulosa/api/services/MessageService.kt rename to api/src/main/kotlin/nebulosa/api/messages/MessageService.kt index 8e9d254b8..c5ea68a1c 100644 --- a/api/src/main/kotlin/nebulosa/api/services/MessageService.kt +++ b/api/src/main/kotlin/nebulosa/api/messages/MessageService.kt @@ -1,4 +1,4 @@ -package nebulosa.api.services +package nebulosa.api.messages import nebulosa.log.debug import nebulosa.log.loggerFor diff --git a/api/src/main/kotlin/nebulosa/api/mounts/MountEventHandler.kt b/api/src/main/kotlin/nebulosa/api/mounts/MountEventHandler.kt index 4a276b5eb..1a9787d12 100644 --- a/api/src/main/kotlin/nebulosa/api/mounts/MountEventHandler.kt +++ b/api/src/main/kotlin/nebulosa/api/mounts/MountEventHandler.kt @@ -3,7 +3,7 @@ package nebulosa.api.mounts import io.reactivex.rxjava3.subjects.PublishSubject import jakarta.annotation.PostConstruct import nebulosa.api.beans.annotations.Subscriber -import nebulosa.api.services.MessageService +import nebulosa.api.messages.MessageService import nebulosa.indi.device.PropertyChangedEvent import nebulosa.indi.device.mount.Mount import nebulosa.indi.device.mount.MountAttached diff --git a/api/src/main/kotlin/nebulosa/api/mounts/MountMessageEvent.kt b/api/src/main/kotlin/nebulosa/api/mounts/MountMessageEvent.kt index f72989ea4..dc8f68048 100644 --- a/api/src/main/kotlin/nebulosa/api/mounts/MountMessageEvent.kt +++ b/api/src/main/kotlin/nebulosa/api/mounts/MountMessageEvent.kt @@ -1,6 +1,6 @@ package nebulosa.api.mounts -import nebulosa.api.services.DeviceMessageEvent +import nebulosa.api.messages.DeviceMessageEvent import nebulosa.indi.device.mount.Mount data class MountMessageEvent( diff --git a/api/src/main/kotlin/nebulosa/api/notification/NotificationEvent.kt b/api/src/main/kotlin/nebulosa/api/notifications/NotificationEvent.kt similarity index 83% rename from api/src/main/kotlin/nebulosa/api/notification/NotificationEvent.kt rename to api/src/main/kotlin/nebulosa/api/notifications/NotificationEvent.kt index b63a79609..0971bc731 100644 --- a/api/src/main/kotlin/nebulosa/api/notification/NotificationEvent.kt +++ b/api/src/main/kotlin/nebulosa/api/notifications/NotificationEvent.kt @@ -1,6 +1,6 @@ -package nebulosa.api.notification +package nebulosa.api.notifications -import nebulosa.api.services.MessageEvent +import nebulosa.api.messages.MessageEvent interface NotificationEvent : MessageEvent { diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/PublishSequenceTasklet.kt b/api/src/main/kotlin/nebulosa/api/sequencer/PublishSequenceTasklet.kt deleted file mode 100644 index f6b963240..000000000 --- a/api/src/main/kotlin/nebulosa/api/sequencer/PublishSequenceTasklet.kt +++ /dev/null @@ -1,55 +0,0 @@ -package nebulosa.api.sequencer - -import io.reactivex.rxjava3.core.Observable -import io.reactivex.rxjava3.core.Observer -import io.reactivex.rxjava3.disposables.Disposable -import io.reactivex.rxjava3.functions.Consumer -import io.reactivex.rxjava3.subjects.PublishSubject -import io.reactivex.rxjava3.subjects.Subject -import nebulosa.log.debug -import nebulosa.log.loggerFor -import java.io.Closeable - -abstract class PublishSequenceTasklet(@JvmField protected val subject: Subject) : SequenceTasklet, Closeable { - - constructor() : this(PublishSubject.create()) - - protected open fun Observable.transform() = this - - final override fun subscribe(onNext: Consumer): Disposable { - return subject.transform().subscribe(onNext) - } - - final override fun subscribe(observer: Observer) { - return subject.transform().subscribe(observer) - } - - final override fun onSubscribe(disposable: Disposable) { - subject.onSubscribe(disposable) - } - - @Synchronized - final override fun onNext(event: T) { - LOG.debug { "$event" } - subject.onNext(event) - } - - @Synchronized - final override fun onError(e: Throwable) { - subject.onError(e) - } - - @Synchronized - final override fun onComplete() { - subject.onComplete() - } - - final override fun close() { - onComplete() - } - - companion object { - - @JvmStatic private val LOG = loggerFor>() - } -} diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/SequenceFlowFactory.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SequenceFlowFactory.kt deleted file mode 100644 index 6eebacd37..000000000 --- a/api/src/main/kotlin/nebulosa/api/sequencer/SequenceFlowFactory.kt +++ /dev/null @@ -1,66 +0,0 @@ -package nebulosa.api.sequencer - -import nebulosa.api.cameras.CameraExposureTasklet -import nebulosa.api.guiding.GuidePulseTasklet -import nebulosa.api.guiding.WaitForSettleTasklet -import nebulosa.api.sequencer.tasklets.delay.DelayTasklet -import nebulosa.common.concurrency.Incrementer -import org.springframework.batch.core.job.builder.FlowBuilder -import org.springframework.batch.core.job.flow.support.SimpleFlow -import org.springframework.beans.factory.config.ConfigurableBeanFactory -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration -import org.springframework.context.annotation.Scope -import org.springframework.core.task.SimpleAsyncTaskExecutor - -@Configuration -class SequenceFlowFactory( - private val flowIncrementer: Incrementer, - private val sequenceStepFactory: SequenceStepFactory, - private val simpleAsyncTaskExecutor: SimpleAsyncTaskExecutor, -) { - - @Bean(name = ["cameraExposureFlow"], autowireCandidate = false) - @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) - fun cameraExposure(cameraExposureTasklet: CameraExposureTasklet): SimpleFlow { - val step = sequenceStepFactory.cameraExposure(cameraExposureTasklet) - return FlowBuilder("Flow.CameraExposure.${flowIncrementer.increment()}").start(step).end() - } - - @Bean(name = ["delayFlow"], autowireCandidate = false) - @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) - fun delay(delayTasklet: DelayTasklet): SimpleFlow { - val step = sequenceStepFactory.delay(delayTasklet) - return FlowBuilder("Flow.Delay.${flowIncrementer.increment()}").start(step).end() - } - - @Bean(name = ["waitForSettleFlow"], autowireCandidate = false) - @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) - fun waitForSettle(waitForSettleTasklet: WaitForSettleTasklet): SimpleFlow { - val step = sequenceStepFactory.waitForSettle(waitForSettleTasklet) - return FlowBuilder("Flow.WaitForSettle.${flowIncrementer.increment()}").start(step).end() - } - - @Bean(name = ["guidePulseFlow"], autowireCandidate = false) - @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) - fun guidePulse( - initialPauseDelayTasklet: DelayTasklet, - forwardGuidePulseTasklet: GuidePulseTasklet, backwardGuidePulseTasklet: GuidePulseTasklet - ): SimpleFlow { - return FlowBuilder("Flow.GuidePulse.${flowIncrementer.increment()}") - .start(sequenceStepFactory.delay(initialPauseDelayTasklet)) - .next(sequenceStepFactory.guidePulse(forwardGuidePulseTasklet)) - .next(sequenceStepFactory.guidePulse(backwardGuidePulseTasklet)) - .end() - } - - @Bean(name = ["delayAndWaitForSettleFlow"], autowireCandidate = false) - @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) - fun delayAndWaitForSettle(cameraDelayTasklet: DelayTasklet, waitForSettleTasklet: WaitForSettleTasklet): SimpleFlow { - return FlowBuilder("Flow.DelayAndWaitForSettle.${flowIncrementer.increment()}") - .start(delay(cameraDelayTasklet)) - .split(simpleAsyncTaskExecutor) - .add(waitForSettle(waitForSettleTasklet)) - .end() - } -} diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/SequenceFlowStepFactory.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SequenceFlowStepFactory.kt deleted file mode 100644 index 441e85cd2..000000000 --- a/api/src/main/kotlin/nebulosa/api/sequencer/SequenceFlowStepFactory.kt +++ /dev/null @@ -1,28 +0,0 @@ -package nebulosa.api.sequencer - -import nebulosa.api.guiding.WaitForSettleTasklet -import nebulosa.api.sequencer.tasklets.delay.DelayTasklet -import nebulosa.common.concurrency.Incrementer -import org.springframework.batch.core.Step -import org.springframework.batch.core.repository.JobRepository -import org.springframework.batch.core.step.builder.StepBuilder -import org.springframework.beans.factory.config.ConfigurableBeanFactory -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration -import org.springframework.context.annotation.Scope - -@Configuration -class SequenceFlowStepFactory( - private val jobRepository: JobRepository, - private val stepIncrementer: Incrementer, - private val sequenceFlowFactory: SequenceFlowFactory, -) { - - @Bean(name = ["delayAndWaitForSettleFlowStep"], autowireCandidate = false) - @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) - fun delayAndWaitForSettle(cameraDelayTasklet: DelayTasklet, waitForSettleTasklet: WaitForSettleTasklet): Step { - return StepBuilder("FlowStep.DelayAndWaitForSettle.${stepIncrementer.increment()}", jobRepository) - .flow(sequenceFlowFactory.delayAndWaitForSettle(cameraDelayTasklet, waitForSettleTasklet)) - .build() - } -} diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/SequenceJob.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SequenceJob.kt deleted file mode 100644 index b9fd3d3fd..000000000 --- a/api/src/main/kotlin/nebulosa/api/sequencer/SequenceJob.kt +++ /dev/null @@ -1,26 +0,0 @@ -package nebulosa.api.sequencer - -import nebulosa.indi.device.Device -import org.springframework.batch.core.Job -import org.springframework.batch.core.JobExecution - -interface SequenceJob { - - val devices: List - - val job: Job - - val jobExecution: JobExecution - - val jobId - get() = jobExecution.jobId - - val startTime - get() = jobExecution.startTime - - val endTime - get() = jobExecution.endTime - - val isRunning - get() = jobExecution.isRunning -} diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/SequenceJobEvent.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SequenceJobEvent.kt deleted file mode 100644 index b54d8a40b..000000000 --- a/api/src/main/kotlin/nebulosa/api/sequencer/SequenceJobEvent.kt +++ /dev/null @@ -1,10 +0,0 @@ -package nebulosa.api.sequencer - -import org.springframework.batch.core.JobExecution - -interface SequenceJobEvent { - - val jobExecution: JobExecution - - val progress: Double -} diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/SequenceJobExecutor.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SequenceJobExecutor.kt deleted file mode 100644 index 7ee1e65a2..000000000 --- a/api/src/main/kotlin/nebulosa/api/sequencer/SequenceJobExecutor.kt +++ /dev/null @@ -1,26 +0,0 @@ -package nebulosa.api.sequencer - -import nebulosa.indi.device.Device - -interface SequenceJobExecutor : Iterable { - - fun execute(request: T): J - - fun sequenceJobFor(vararg devices: Device): J? { - fun find(task: J): Boolean { - for (i in devices.indices) { - if (i >= task.devices.size || task.devices[i].name != devices[i].name) { - return false - } - } - - return true - } - - return findLast(::find) - } - - fun sequenceJobWithId(jobId: Long): J? { - return find { it.jobId == jobId } - } -} diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/SequenceJobFactory.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SequenceJobFactory.kt deleted file mode 100644 index c0e0e848c..000000000 --- a/api/src/main/kotlin/nebulosa/api/sequencer/SequenceJobFactory.kt +++ /dev/null @@ -1,90 +0,0 @@ -package nebulosa.api.sequencer - -import io.reactivex.rxjava3.functions.Consumer -import nebulosa.api.cameras.CameraCaptureEvent -import nebulosa.api.cameras.CameraStartCaptureRequest -import nebulosa.common.concurrency.Incrementer -import org.springframework.batch.core.Job -import org.springframework.batch.core.JobExecutionListener -import org.springframework.batch.core.job.builder.JobBuilder -import org.springframework.batch.core.job.flow.Flow -import org.springframework.batch.core.repository.JobRepository -import org.springframework.beans.factory.config.ConfigurableBeanFactory -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration -import org.springframework.context.annotation.Scope -import org.springframework.core.task.SimpleAsyncTaskExecutor - -@Configuration -class SequenceJobFactory( - private val jobRepository: JobRepository, - private val sequenceFlowStepFactory: SequenceFlowStepFactory, - private val sequenceStepFactory: SequenceStepFactory, - private val sequenceTaskletFactory: SequenceTaskletFactory, - private val jobIncrementer: Incrementer, - private val simpleAsyncTaskExecutor: SimpleAsyncTaskExecutor, -) { - - @Bean(name = ["cameraLoopCaptureJob"], autowireCandidate = false) - @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) - fun cameraLoopCapture( - request: CameraStartCaptureRequest, - cameraCaptureListener: Consumer, - ): Job { - val cameraExposureTasklet = sequenceTaskletFactory.cameraLoopExposure(request) - cameraExposureTasklet.subscribe(cameraCaptureListener) - - val cameraExposureStep = sequenceStepFactory.cameraExposure(cameraExposureTasklet) - - return JobBuilder("CameraCapture.Job.${jobIncrementer.increment()}", jobRepository) - .start(cameraExposureStep) - .listener(cameraExposureTasklet) - .build() - } - - @Bean(name = ["cameraCaptureJob"], autowireCandidate = false) - @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) - fun cameraCapture( - request: CameraStartCaptureRequest, - cameraCaptureListener: Consumer, - ): Job { - val cameraExposureTasklet = sequenceTaskletFactory.cameraExposure(request) - cameraExposureTasklet.subscribe(cameraCaptureListener) - - val cameraDelayTasklet = sequenceTaskletFactory.delay(request.exposureDelay) - cameraDelayTasklet.subscribe(cameraExposureTasklet) - - val ditherTasklet = sequenceTaskletFactory.ditherAfterExposure(request.dither) - val waitForSettleTasklet = sequenceTaskletFactory.waitForSettle() - - val jobBuilder = JobBuilder("CameraCapture.Job.${jobIncrementer.increment()}", jobRepository) - .start(sequenceStepFactory.waitForSettle(waitForSettleTasklet)) - .next(sequenceStepFactory.cameraExposure(cameraExposureTasklet)) - - repeat(request.exposureAmount - 1) { - jobBuilder.next(sequenceFlowStepFactory.delayAndWaitForSettle(cameraDelayTasklet, waitForSettleTasklet)) - .next(sequenceStepFactory.cameraExposure(cameraExposureTasklet)) - .next(sequenceStepFactory.dither(ditherTasklet)) - } - - return jobBuilder - .listener(cameraExposureTasklet) - .listener(cameraDelayTasklet) - .build() - } - - @Bean(name = ["darvJob"], autowireCandidate = false) - @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) - fun darvPolarAlignment( - cameraExposureFlow: Flow, guidePulseFlow: Flow, - vararg listeners: JobExecutionListener, - ): Job { - return JobBuilder("DARVPolarAlignment.Job.${jobIncrementer.increment()}", jobRepository) - .start(cameraExposureFlow) - .split(simpleAsyncTaskExecutor) - .add(guidePulseFlow) - .end() - .also { listeners.forEach(it::listener) } - .build() - } -} diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/SequenceJobSerializer.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SequenceJobSerializer.kt deleted file mode 100644 index 88a0ce260..000000000 --- a/api/src/main/kotlin/nebulosa/api/sequencer/SequenceJobSerializer.kt +++ /dev/null @@ -1,20 +0,0 @@ -package nebulosa.api.sequencer - -import com.fasterxml.jackson.core.JsonGenerator -import com.fasterxml.jackson.databind.SerializerProvider -import com.fasterxml.jackson.databind.ser.std.StdSerializer -import org.springframework.stereotype.Component -import java.time.ZoneOffset - -@Component -class SequenceJobSerializer : StdSerializer(SequenceJob::class.java) { - - override fun serialize(value: SequenceJob, gen: JsonGenerator, provider: SerializerProvider) { - gen.writeStartObject() - gen.writeObjectField("devices", value.devices) - gen.writeNumberField("jobId", value.jobId) - gen.writeNumberField("startTime", value.startTime?.atZone(ZoneOffset.UTC)?.toInstant()?.toEpochMilli() ?: 0L) - gen.writeNumberField("endTime", value.endTime?.atZone(ZoneOffset.UTC)?.toInstant()?.toEpochMilli() ?: 0L) - gen.writeEndObject() - } -} diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/SequenceStepEvent.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SequenceStepEvent.kt deleted file mode 100644 index eb81f7aea..000000000 --- a/api/src/main/kotlin/nebulosa/api/sequencer/SequenceStepEvent.kt +++ /dev/null @@ -1,12 +0,0 @@ -package nebulosa.api.sequencer - -import com.fasterxml.jackson.annotation.JsonIgnore -import org.springframework.batch.core.StepExecution - -interface SequenceStepEvent : SequenceJobEvent { - - val stepExecution: StepExecution - - override val jobExecution - @JsonIgnore get() = stepExecution.jobExecution -} diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/SequenceStepFactory.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SequenceStepFactory.kt deleted file mode 100644 index a7dcd2931..000000000 --- a/api/src/main/kotlin/nebulosa/api/sequencer/SequenceStepFactory.kt +++ /dev/null @@ -1,64 +0,0 @@ -package nebulosa.api.sequencer - -import nebulosa.api.cameras.CameraStartCaptureTasklet -import nebulosa.api.guiding.DitherAfterExposureTasklet -import nebulosa.api.guiding.GuidePulseTasklet -import nebulosa.api.guiding.WaitForSettleTasklet -import nebulosa.api.sequencer.tasklets.delay.DelayTasklet -import nebulosa.common.concurrency.Incrementer -import org.springframework.batch.core.repository.JobRepository -import org.springframework.batch.core.step.builder.StepBuilder -import org.springframework.batch.core.step.tasklet.TaskletStep -import org.springframework.beans.factory.config.ConfigurableBeanFactory -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration -import org.springframework.context.annotation.Scope -import org.springframework.transaction.PlatformTransactionManager - -@Configuration -class SequenceStepFactory( - private val jobRepository: JobRepository, - private val platformTransactionManager: PlatformTransactionManager, - private val stepIncrementer: Incrementer, -) { - - @Bean(name = ["delayStep"], autowireCandidate = false) - @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) - fun delay(delayTasklet: DelayTasklet): TaskletStep { - return StepBuilder("Step.Delay.${stepIncrementer.increment()}", jobRepository) - .tasklet(delayTasklet, platformTransactionManager) - .build() - } - - @Bean(name = ["cameraExposureStep"], autowireCandidate = false) - @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) - fun cameraExposure(cameraExposureTasklet: CameraStartCaptureTasklet): TaskletStep { - return StepBuilder("Step.Exposure.${stepIncrementer.increment()}", jobRepository) - .tasklet(cameraExposureTasklet, platformTransactionManager) - .build() - } - - @Bean(name = ["guidePulseStep"], autowireCandidate = false) - @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) - fun guidePulse(guidePulseTasklet: GuidePulseTasklet): TaskletStep { - return StepBuilder("Step.GuidePulse.${stepIncrementer.increment()}", jobRepository) - .tasklet(guidePulseTasklet, platformTransactionManager) - .build() - } - - @Bean(name = ["ditherStep"], autowireCandidate = false) - @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) - fun dither(ditherAfterExposureTasklet: DitherAfterExposureTasklet): TaskletStep { - return StepBuilder("Step.DitherAfterExposure.${stepIncrementer.increment()}", jobRepository) - .tasklet(ditherAfterExposureTasklet, platformTransactionManager) - .build() - } - - @Bean(name = ["waitForSettleStep"], autowireCandidate = false) - @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) - fun waitForSettle(waitForSettleTasklet: WaitForSettleTasklet): TaskletStep { - return StepBuilder("Step.WaitForSettle.${stepIncrementer.increment()}", jobRepository) - .tasklet(waitForSettleTasklet, platformTransactionManager) - .build() - } -} diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/SequenceTasklet.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SequenceTasklet.kt deleted file mode 100644 index 029acce13..000000000 --- a/api/src/main/kotlin/nebulosa/api/sequencer/SequenceTasklet.kt +++ /dev/null @@ -1,12 +0,0 @@ -package nebulosa.api.sequencer - -import io.reactivex.rxjava3.core.ObservableSource -import io.reactivex.rxjava3.core.Observer -import io.reactivex.rxjava3.disposables.Disposable -import io.reactivex.rxjava3.functions.Consumer -import org.springframework.batch.core.step.tasklet.StoppableTasklet - -interface SequenceTasklet : StoppableTasklet, ObservableSource, Observer { - - fun subscribe(onNext: Consumer): Disposable -} diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/SequenceTaskletEvent.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SequenceTaskletEvent.kt deleted file mode 100644 index 3bba9015c..000000000 --- a/api/src/main/kotlin/nebulosa/api/sequencer/SequenceTaskletEvent.kt +++ /dev/null @@ -1,10 +0,0 @@ -package nebulosa.api.sequencer - -import org.springframework.batch.core.step.tasklet.Tasklet - -interface SequenceTaskletEvent { - - val tasklet: Tasklet - - val progress: Double -} diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/SequenceTaskletFactory.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SequenceTaskletFactory.kt deleted file mode 100644 index 68c097233..000000000 --- a/api/src/main/kotlin/nebulosa/api/sequencer/SequenceTaskletFactory.kt +++ /dev/null @@ -1,52 +0,0 @@ -package nebulosa.api.sequencer - -import nebulosa.api.cameras.CameraExposureTasklet -import nebulosa.api.cameras.CameraLoopExposureTasklet -import nebulosa.api.cameras.CameraStartCaptureRequest -import nebulosa.api.guiding.* -import nebulosa.api.sequencer.tasklets.delay.DelayTasklet -import org.springframework.beans.factory.config.ConfigurableBeanFactory -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration -import org.springframework.context.annotation.Scope -import java.time.Duration - -@Configuration -class SequenceTaskletFactory { - - @Bean(name = ["delayTasklet"], autowireCandidate = false) - @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) - fun delay(duration: Duration): DelayTasklet { - return DelayTasklet(duration) - } - - @Bean(name = ["cameraExposureTasklet"], autowireCandidate = false) - @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) - fun cameraExposure(request: CameraStartCaptureRequest): CameraExposureTasklet { - return CameraExposureTasklet(request) - } - - @Bean(name = ["cameraLoopExposureTasklet"], autowireCandidate = false) - @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) - fun cameraLoopExposure(request: CameraStartCaptureRequest): CameraLoopExposureTasklet { - return CameraLoopExposureTasklet(request) - } - - @Bean(name = ["guidePulseTasklet"], autowireCandidate = false) - @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) - fun guidePulse(request: GuidePulseRequest): GuidePulseTasklet { - return GuidePulseTasklet(request) - } - - @Bean(name = ["ditherTasklet"], autowireCandidate = false) - @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) - fun ditherAfterExposure(request: DitherAfterExposureRequest): DitherAfterExposureTasklet { - return DitherAfterExposureTasklet(request) - } - - @Bean(name = ["waitForSettleTasklet"], autowireCandidate = false) - @Scope(value = ConfigurableBeanFactory.SCOPE_SINGLETON) - fun waitForSettle(): WaitForSettleTasklet { - return WaitForSettleTasklet() - } -} diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/tasklets/delay/DelayElapsed.kt b/api/src/main/kotlin/nebulosa/api/sequencer/tasklets/delay/DelayElapsed.kt deleted file mode 100644 index af1aa9450..000000000 --- a/api/src/main/kotlin/nebulosa/api/sequencer/tasklets/delay/DelayElapsed.kt +++ /dev/null @@ -1,17 +0,0 @@ -package nebulosa.api.sequencer.tasklets.delay - -import com.fasterxml.jackson.annotation.JsonIgnore -import org.springframework.batch.core.StepExecution -import java.time.Duration - -data class DelayElapsed( - override val remainingTime: Duration, - override val waitDuration: Duration, - @JsonIgnore override val stepExecution: StepExecution, - @JsonIgnore override val tasklet: DelayTasklet, -) : DelayEvent { - - override val progress - get() = if (remainingTime > Duration.ZERO) (tasklet.duration.toNanos() - remainingTime.toNanos()) / tasklet.duration.toNanos().toDouble() - else 1.0 -} diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/tasklets/delay/DelayEvent.kt b/api/src/main/kotlin/nebulosa/api/sequencer/tasklets/delay/DelayEvent.kt deleted file mode 100644 index cf0dad2c5..000000000 --- a/api/src/main/kotlin/nebulosa/api/sequencer/tasklets/delay/DelayEvent.kt +++ /dev/null @@ -1,21 +0,0 @@ -package nebulosa.api.sequencer.tasklets.delay - -import com.fasterxml.jackson.annotation.JsonIgnore -import nebulosa.api.sequencer.SequenceStepEvent -import nebulosa.api.sequencer.SequenceTaskletEvent -import java.time.Duration - -sealed interface DelayEvent : SequenceStepEvent, SequenceTaskletEvent { - - override val tasklet: DelayTasklet - - val remainingTime: Duration - - val waitDuration: Duration - - val isStarted - @JsonIgnore get() = remainingTime == tasklet.duration - - val isFinished - @JsonIgnore get() = remainingTime == Duration.ZERO -} diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/tasklets/delay/DelayTasklet.kt b/api/src/main/kotlin/nebulosa/api/sequencer/tasklets/delay/DelayTasklet.kt deleted file mode 100644 index 48770a73e..000000000 --- a/api/src/main/kotlin/nebulosa/api/sequencer/tasklets/delay/DelayTasklet.kt +++ /dev/null @@ -1,53 +0,0 @@ -package nebulosa.api.sequencer.tasklets.delay - -import nebulosa.api.sequencer.PublishSequenceTasklet -import org.springframework.batch.core.JobExecution -import org.springframework.batch.core.JobExecutionListener -import org.springframework.batch.core.StepContribution -import org.springframework.batch.core.scope.context.ChunkContext -import org.springframework.batch.repeat.RepeatStatus -import java.time.Duration -import java.util.concurrent.atomic.AtomicBoolean - -data class DelayTasklet(val duration: Duration) : PublishSequenceTasklet(), JobExecutionListener { - - private val aborted = AtomicBoolean() - - override fun execute(contribution: StepContribution, chunkContext: ChunkContext): RepeatStatus { - val stepExecution = contribution.stepExecution - var remainingTime = duration - - if (remainingTime > Duration.ZERO) { - while (!aborted.get() && remainingTime > Duration.ZERO) { - val waitTime = minOf(remainingTime, DELAY_INTERVAL) - - if (waitTime > Duration.ZERO) { - onNext(DelayElapsed(remainingTime, waitTime, stepExecution, this)) - Thread.sleep(waitTime.toMillis()) - remainingTime -= waitTime - } - } - - onNext(DelayElapsed(Duration.ZERO, Duration.ZERO, stepExecution, this)) - } - - return RepeatStatus.FINISHED - } - - override fun afterJob(jobExecution: JobExecution) { - close() - } - - override fun stop() { - aborted.set(true) - } - - fun wasAborted(): Boolean { - return aborted.get() - } - - companion object { - - @JvmField val DELAY_INTERVAL = Duration.ofMillis(500)!! - } -} diff --git a/api/src/main/kotlin/nebulosa/api/wheels/WheelEventHandler.kt b/api/src/main/kotlin/nebulosa/api/wheels/WheelEventHandler.kt index 88a5bb660..f9d9988e5 100644 --- a/api/src/main/kotlin/nebulosa/api/wheels/WheelEventHandler.kt +++ b/api/src/main/kotlin/nebulosa/api/wheels/WheelEventHandler.kt @@ -3,7 +3,7 @@ package nebulosa.api.wheels import io.reactivex.rxjava3.subjects.PublishSubject import jakarta.annotation.PostConstruct import nebulosa.api.beans.annotations.Subscriber -import nebulosa.api.services.MessageService +import nebulosa.api.messages.MessageService import nebulosa.indi.device.PropertyChangedEvent import nebulosa.indi.device.filterwheel.FilterWheel import nebulosa.indi.device.filterwheel.FilterWheelAttached diff --git a/api/src/main/kotlin/nebulosa/api/wheels/WheelMessageEvent.kt b/api/src/main/kotlin/nebulosa/api/wheels/WheelMessageEvent.kt index 9dce40456..dad365e72 100644 --- a/api/src/main/kotlin/nebulosa/api/wheels/WheelMessageEvent.kt +++ b/api/src/main/kotlin/nebulosa/api/wheels/WheelMessageEvent.kt @@ -1,6 +1,6 @@ package nebulosa.api.wheels -import nebulosa.api.services.DeviceMessageEvent +import nebulosa.api.messages.DeviceMessageEvent import nebulosa.indi.device.filterwheel.FilterWheel data class WheelMessageEvent( diff --git a/api/src/main/resources/application.yml b/api/src/main/resources/application.yml index c763c2860..453e17745 100644 --- a/api/src/main/resources/application.yml +++ b/api/src/main/resources/application.yml @@ -25,11 +25,3 @@ spring: baseline-on-migrate: true baseline-version: 0 table: migrations - - batch: - job: - enabled: false - datasource: - url: 'jdbc:sqlite:file:nebulosa.db?mode=memory' - jdbc: - initialize-schema: always diff --git a/desktop/src/app/alignment/alignment.component.html b/desktop/src/app/alignment/alignment.component.html index a3dc17a88..ae76dc6b2 100644 --- a/desktop/src/app/alignment/alignment.component.html +++ b/desktop/src/app/alignment/alignment.component.html @@ -43,10 +43,10 @@ {{ darvDirection }} - {{ darvCapture.remainingTime | exposureTime }} + {{ darvRemainingTime | exposureTime }} - {{ darvCapture.progress | percent:'1.1-1' }} + {{ darvProgress | percent:'1.1-1' }} diff --git a/desktop/src/app/alignment/alignment.component.ts b/desktop/src/app/alignment/alignment.component.ts index 9714c7eba..00647774d 100644 --- a/desktop/src/app/alignment/alignment.component.ts +++ b/desktop/src/app/alignment/alignment.component.ts @@ -2,7 +2,7 @@ import { AfterViewInit, Component, HostListener, NgZone, OnDestroy } from '@angu import { ApiService } from '../../shared/services/api.service' import { BrowserWindowService } from '../../shared/services/browser-window.service' import { ElectronService } from '../../shared/services/electron.service' -import { Camera, DARVPolarAlignmentState, GuideDirection, GuideOutput, Hemisphere, Union } from '../../shared/types' +import { Camera, DARVState, GuideDirection, GuideOutput, Hemisphere, Union } from '../../shared/types' import { AppComponent } from '../app.component' @Component({ @@ -26,12 +26,9 @@ export class AlignmentComponent implements AfterViewInit, OnDestroy { readonly darvHemispheres: Hemisphere[] = ['NORTHERN', 'SOUTHERN'] darvHemisphere: Hemisphere = 'NORTHERN' darvDirection?: GuideDirection - darvStatus: Union = 'IDLE' - - readonly darvCapture = { - remainingTime: 0, - progress: 0, - } + darvStatus: Union = 'IDLE' + darvRemainingTime = 0 + darvProgress = 0 constructor( app: AppComponent, @@ -51,6 +48,28 @@ export class AlignmentComponent implements AfterViewInit, OnDestroy { } }) + electron.on('CAMERA_ATTACHED', event => { + ngZone.run(() => { + this.cameras.push(event.device) + }) + }) + + electron.on('CAMERA_DETACHED', event => { + ngZone.run(() => { + const index = this.cameras.findIndex(e => e.name === event.device.name) + + if (index >= 0) { + if (this.cameras[index] === this.camera) { + this.camera = undefined + this.cameraConnected = false + } + + this.cameras.splice(index, 1) + + } + }) + }) + electron.on('GUIDE_OUTPUT_ATTACHED', event => { ngZone.run(() => { this.guideOutputs.push(event.device) @@ -60,7 +79,16 @@ export class AlignmentComponent implements AfterViewInit, OnDestroy { electron.on('GUIDE_OUTPUT_DETACHED', event => { ngZone.run(() => { const index = this.guideOutputs.findIndex(e => e.name === event.device.name) - if (index >= 0) this.guideOutputs.splice(index, 1) + + if (index >= 0) { + if (this.guideOutputs[index] === this.guideOutput) { + this.guideOutput = undefined + this.guideOutputConnected = false + } + + this.guideOutputs.splice(index, 1) + + } }) }) @@ -73,47 +101,23 @@ export class AlignmentComponent implements AfterViewInit, OnDestroy { } }) - electron.on('DARV_POLAR_ALIGNMENT_STARTED', event => { - if (event.camera.name === this.camera?.name && - event.guideOutput.name === this.guideOutput?.name) { - ngZone.run(() => { - this.darvInProgress = true - }) - } - }) - - electron.on('DARV_POLAR_ALIGNMENT_FINISHED', event => { - if (event.camera.name === this.camera?.name && - event.guideOutput.name === this.guideOutput?.name) { - ngZone.run(() => { - this.darvInProgress = false - this.darvStatus = 'IDLE' - this.darvDirection = undefined - }) - } - }) - - electron.on('DARV_POLAR_ALIGNMENT_UPDATED', event => { + electron.on('DARV_POLAR_ALIGNMENT_ELAPSED', event => { if (event.camera.name === this.camera?.name && event.guideOutput.name === this.guideOutput?.name) { ngZone.run(() => { this.darvStatus = event.state + this.darvRemainingTime = event.remainingTime + this.darvProgress = event.progress + this.darvInProgress = event.remainingTime > 0 - if (event.state !== 'INITIAL_PAUSE') { + if (event.state === 'FORWARD' || event.state === 'BACKWARD') { this.darvDirection = event.direction + } else { + this.darvDirection = undefined } }) } }) - - electron.on('CAMERA_EXPOSURE_ELAPSED', event => { - if (event.camera.name === this.camera?.name) { - ngZone.run(() => { - this.darvCapture.remainingTime = event.remainingTime - this.darvCapture.progress = event.progress - }) - } - }) } async ngAfterViewInit() { @@ -183,7 +187,7 @@ export class AlignmentComponent implements AfterViewInit, OnDestroy { return this.browserWindow.openCameraImage(this.camera!) } - private async updateCamera() { + private updateCamera() { if (!this.camera) { return } @@ -191,7 +195,7 @@ export class AlignmentComponent implements AfterViewInit, OnDestroy { this.cameraConnected = this.camera.connected } - private async updateGuideOutput() { + private updateGuideOutput() { if (!this.guideOutput) { return } diff --git a/desktop/src/app/camera/camera.component.html b/desktop/src/app/camera/camera.component.html index eedad2166..7a45c2b1b 100644 --- a/desktop/src/app/camera/camera.component.html +++ b/desktop/src/app/camera/camera.component.html @@ -38,16 +38,12 @@ {{ waiting ? 'waiting' : capturing ? 'capturing' : 'idle' }} - - - {{ exposure.count }} of {{ capture.amount }} - - - - - {{ exposure.count }} - - + + {{ exposure.count }} + of {{ capture.amount }} + + + {{ exposure.remainingTime | exposureTime }} @@ -120,7 +116,7 @@
diff --git a/desktop/src/app/camera/camera.component.ts b/desktop/src/app/camera/camera.component.ts index b499bc21c..6638ffa5f 100644 --- a/desktop/src/app/camera/camera.component.ts +++ b/desktop/src/app/camera/camera.component.ts @@ -179,7 +179,6 @@ export class CameraComponent implements AfterContentInit, OnDestroy { } readonly wait = { - duration: 0, remainingTime: 0, progress: 0, } @@ -238,16 +237,10 @@ export class CameraComponent implements AfterContentInit, OnDestroy { } }) - electron.on('CAMERA_CAPTURE_STARTED', event => { - if (event.camera.name === this.camera?.name) { + electron.on('CAMERA_DETACHED', event => { + if (event.device.name === this.camera?.name) { ngZone.run(() => { - this.capture.looping = event.looping - this.capture.amount = event.exposureAmount - this.capture.elapsedTime = 0 - this.capture.remainingTime = event.estimatedTime - this.capture.progress = event.progress - this.capturing = true - this.waiting = false + this.connected = false }) } }) @@ -255,61 +248,27 @@ export class CameraComponent implements AfterContentInit, OnDestroy { electron.on('CAMERA_CAPTURE_ELAPSED', event => { if (event.camera.name === this.camera?.name) { ngZone.run(() => { - this.capture.elapsedTime = event.elapsedTime - this.capture.remainingTime = event.remainingTime - this.capture.progress = event.progress - }) - } - }) - - electron.on('CAMERA_CAPTURE_WAITING', event => { - if (event.camera.name === this.camera?.name) { - ngZone.run(() => { - this.wait.duration = event.waitDuration - this.wait.remainingTime = event.remainingTime - this.wait.progress = event.progress - this.capturing = false - this.waiting = true - }) - } - }) - - electron.on('CAMERA_CAPTURE_FINISHED', event => { - if (event.camera.name === this.camera?.name) { - ngZone.run(() => { - this.capturing = false - this.waiting = false - }) - } - }) - - electron.on('CAMERA_EXPOSURE_STARTED', event => { - if (event.camera.name === this.camera?.name) { - ngZone.run(() => { - this.exposure.remainingTime = event.remainingTime - this.exposure.progress = event.progress - this.exposure.count = event.exposureCount - this.capturing = true - this.waiting = false - }) - } - }) - - electron.on('CAMERA_EXPOSURE_ELAPSED', event => { - if (event.camera.name === this.camera?.name) { - ngZone.run(() => { - this.exposure.remainingTime = event.remainingTime - this.exposure.progress = event.progress + this.capture.elapsedTime = event.captureElapsedTime + this.capture.remainingTime = event.captureRemainingTime + this.capture.progress = event.captureProgress + this.exposure.remainingTime = event.exposureRemainingTime + this.exposure.progress = event.exposureProgress this.exposure.count = event.exposureCount - }) - } - }) - electron.on('CAMERA_EXPOSURE_FINISHED', event => { - if (event.camera.name === this.camera?.name) { - ngZone.run(() => { - this.exposure.remainingTime = event.remainingTime - this.exposure.progress = event.progress + if (event.state === 'WAITING') { + this.wait.remainingTime = event.waitRemainingTime + this.wait.progress = event.waitProgress + this.waiting = true + } else if (event.state === 'CAPTURE_STARTED') { + this.capture.looping = event.exposureAmount <= 0 + this.capture.amount = event.exposureAmount + this.capturing = true + } else if (event.state === 'CAPTURE_FINISHED') { + this.capturing = false + this.waiting = false + } else if (event.state === 'EXPOSURE_STARTED') { + this.waiting = false + } }) } }) @@ -336,7 +295,7 @@ export class CameraComponent implements AfterContentInit, OnDestroy { const camera = await this.api.camera(this.camera.name) Object.assign(this.camera, camera) - await this.loadPreference() + this.loadPreference() this.update() } else { this.app.subTitle = '' @@ -422,18 +381,20 @@ export class CameraComponent implements AfterContentInit, OnDestroy { } private updateExposureUnit(unit: ExposureTimeUnit) { - const a = CameraComponent.exposureUnitFactor(this.exposureTimeUnit) - const b = CameraComponent.exposureUnitFactor(unit) - const exposureTime = Math.trunc(this.exposureTime * b / a) - const exposureTimeMin = Math.trunc(this.camera!.exposureMin * b / 60000000) - const exposureTimeMax = Math.trunc(this.camera!.exposureMax * b / 60000000) - this.exposureTimeMax = Math.max(1, exposureTimeMax) - this.exposureTimeMin = Math.max(1, exposureTimeMin) - this.exposureTime = Math.max(this.exposureTimeMin, Math.min(exposureTime, this.exposureTimeMax)) - this.exposureTimeUnit = unit + if (this.camera!.exposureMax) { + const a = CameraComponent.exposureUnitFactor(this.exposureTimeUnit) + const b = CameraComponent.exposureUnitFactor(unit) + const exposureTime = Math.trunc(this.exposureTime * b / a) + const exposureTimeMin = Math.trunc(this.camera!.exposureMin * b / 60000000) + const exposureTimeMax = Math.trunc(this.camera!.exposureMax * b / 60000000) + this.exposureTimeMax = Math.max(1, exposureTimeMax) + this.exposureTimeMin = Math.max(1, exposureTimeMin) + this.exposureTime = Math.max(this.exposureTimeMin, Math.min(exposureTime, this.exposureTimeMax)) + this.exposureTimeUnit = unit + } } - private async update() { + private update() { if (this.camera) { this.connected = this.camera.connected diff --git a/desktop/src/app/filterwheel/filterwheel.component.ts b/desktop/src/app/filterwheel/filterwheel.component.ts index 1ec686822..c49045abd 100644 --- a/desktop/src/app/filterwheel/filterwheel.component.ts +++ b/desktop/src/app/filterwheel/filterwheel.component.ts @@ -64,6 +64,14 @@ export class FilterWheelComponent implements AfterContentInit, OnDestroy { } }) + electron.on('WHEEL_DETACHED', event => { + if (event.device.name === this.wheel?.name) { + ngZone.run(() => { + this.connected = false + }) + } + }) + this.subscription = this.filterChangedPublisher .pipe(throttleTime(1500)) .subscribe((filter) => { @@ -127,7 +135,7 @@ export class FilterWheelComponent implements AfterContentInit, OnDestroy { this.filterChangedPublisher.next(filter) } - private async update() { + private update() { if (!this.wheel) { return } diff --git a/desktop/src/app/focuser/focuser.component.ts b/desktop/src/app/focuser/focuser.component.ts index 666b8fd6a..0f59124ce 100644 --- a/desktop/src/app/focuser/focuser.component.ts +++ b/desktop/src/app/focuser/focuser.component.ts @@ -55,6 +55,14 @@ export class FocuserComponent implements AfterViewInit, OnDestroy { }) } }) + + electron.on('FOCUSER_DETACHED', event => { + if (event.device.name === this.focuser?.name) { + ngZone.run(() => { + this.connected = false + }) + } + }) } async ngAfterViewInit() { @@ -120,7 +128,7 @@ export class FocuserComponent implements AfterViewInit, OnDestroy { this.api.focuserAbort(this.focuser!) } - private async update() { + private update() { if (!this.focuser) { return } diff --git a/desktop/src/app/home/home.component.ts b/desktop/src/app/home/home.component.ts index 4246317df..67f3d3aaa 100644 --- a/desktop/src/app/home/home.component.ts +++ b/desktop/src/app/home/home.component.ts @@ -317,7 +317,9 @@ export class HomeComponent implements AfterContentInit, OnDestroy { this.connected = await this.api.connectionStatus() } catch { this.connected = false + } + if (!this.connected) { this.cameras = [] this.mounts = [] this.focusers = [] diff --git a/desktop/src/app/image/image.component.ts b/desktop/src/app/image/image.component.ts index 95649657b..db7c88d8a 100644 --- a/desktop/src/app/image/image.component.ts +++ b/desktop/src/app/image/image.component.ts @@ -357,8 +357,8 @@ export class ImageComponent implements AfterViewInit, OnDestroy { ) { app.title = 'Image' - electron.on('CAMERA_EXPOSURE_FINISHED', async (event) => { - if (event.camera.name === this.imageData.camera?.name) { + electron.on('CAMERA_CAPTURE_ELAPSED', async (event) => { + if (event.state === 'EXPOSURE_FINISHED' && event.camera.name === this.imageData.camera?.name) { await this.closeImage() ngZone.run(() => { diff --git a/desktop/src/app/mount/mount.component.ts b/desktop/src/app/mount/mount.component.ts index d89e8c1ee..fa4b049da 100644 --- a/desktop/src/app/mount/mount.component.ts +++ b/desktop/src/app/mount/mount.component.ts @@ -165,6 +165,14 @@ export class MountComponent implements AfterContentInit, OnDestroy { } }) + electron.on('MOUNT_DETACHED', event => { + if (event.device.name === this.mount?.name) { + ngZone.run(() => { + this.connected = false + }) + } + }) + this.computeCoordinateSubscriptions[0] = this.computeCoordinatePublisher .pipe(throttleTime(5000)) .subscribe(() => this.computeCoordinates()) @@ -322,7 +330,7 @@ export class MountComponent implements AfterContentInit, OnDestroy { } } - private async update() { + private update() { if (this.mount) { this.connected = this.mount.connected this.slewing = this.mount.slewing diff --git a/desktop/src/shared/services/electron.service.ts b/desktop/src/shared/services/electron.service.ts index 91a90c5a0..898781b3d 100644 --- a/desktop/src/shared/services/electron.service.ts +++ b/desktop/src/shared/services/electron.service.ts @@ -7,10 +7,9 @@ import * as childProcess from 'child_process' import { ipcRenderer, webFrame } from 'electron' import * as fs from 'fs' import { - ApiEventType, Camera, CameraCaptureElapsed, CameraCaptureFinished, CameraCaptureIsWaiting, CameraCaptureStarted, - CameraExposureElapsed, CameraExposureFinished, CameraExposureStarted, DARVPolarAlignmentEvent, DARVPolarAlignmentGuidePulseElapsed, - DARVPolarAlignmentInitialPauseElapsed, DeviceMessageEvent, FilterWheel, Focuser, GuideOutput, Guider, - GuiderMessageEvent, HistoryStep, INDIMessageEvent, InternalEventType, Location, Mount, NotificationEvent, NotificationEventType, OpenDirectory, OpenFile + ApiEventType, Camera, CameraCaptureEvent, DARVEvent, DeviceMessageEvent, FilterWheel, Focuser, + GuideOutput, Guider, GuiderMessageEvent, HistoryStep, INDIMessageEvent, InternalEventType, Location, Mount, NotificationEvent, + NotificationEventType, OpenDirectory, OpenFile } from '../types' import { ApiService } from './api.service' @@ -21,13 +20,7 @@ type EventMappedType = { 'CAMERA_UPDATED': DeviceMessageEvent 'CAMERA_ATTACHED': DeviceMessageEvent 'CAMERA_DETACHED': DeviceMessageEvent - 'CAMERA_CAPTURE_STARTED': CameraCaptureStarted - 'CAMERA_CAPTURE_FINISHED': CameraCaptureFinished - 'CAMERA_CAPTURE_ELAPSED': CameraCaptureElapsed - 'CAMERA_CAPTURE_WAITING': CameraCaptureIsWaiting - 'CAMERA_EXPOSURE_ELAPSED': CameraExposureElapsed - 'CAMERA_EXPOSURE_STARTED': CameraExposureStarted - 'CAMERA_EXPOSURE_FINISHED': CameraExposureFinished + 'CAMERA_CAPTURE_ELAPSED': CameraCaptureEvent 'MOUNT_UPDATED': DeviceMessageEvent 'MOUNT_ATTACHED': DeviceMessageEvent 'MOUNT_DETACHED': DeviceMessageEvent @@ -45,9 +38,7 @@ type EventMappedType = { 'GUIDER_UPDATED': GuiderMessageEvent 'GUIDER_STEPPED': GuiderMessageEvent 'GUIDER_MESSAGE_RECEIVED': GuiderMessageEvent - 'DARV_POLAR_ALIGNMENT_STARTED': DARVPolarAlignmentEvent - 'DARV_POLAR_ALIGNMENT_FINISHED': DARVPolarAlignmentEvent - 'DARV_POLAR_ALIGNMENT_UPDATED': DARVPolarAlignmentInitialPauseElapsed | DARVPolarAlignmentGuidePulseElapsed + 'DARV_POLAR_ALIGNMENT_ELAPSED': DARVEvent 'DATA_CHANGED': any 'LOCATION_CHANGED': Location 'SKY_ATLAS_UPDATE_FINISHED': NotificationEvent diff --git a/desktop/src/shared/types.ts b/desktop/src/shared/types.ts index d98b65597..3a8506e1e 100644 --- a/desktop/src/shared/types.ts +++ b/desktop/src/shared/types.ts @@ -213,41 +213,16 @@ export interface CameraStartCapture { export interface CameraCaptureEvent extends MessageEvent { camera: Camera - progress: number -} - -export interface CameraCaptureStarted extends CameraCaptureEvent { - looping: boolean - exposureAmount: number - exposureTime: number - estimatedTime: number -} - -export interface CameraCaptureFinished extends CameraCaptureEvent { } - -export interface CameraCaptureElapsed extends CameraCaptureEvent { - exposureCount: number - remainingTime: number - elapsedTime: number -} - -export interface CameraCaptureIsWaiting extends CameraCaptureEvent { - waitDuration: number - remainingTime: number -} - -export interface CameraExposureEvent extends CameraCaptureEvent { + state: CameraCaptureState exposureAmount: number exposureCount: number - exposureTime: number - remainingTime: number -} - -export interface CameraExposureStarted extends CameraExposureEvent { } - -export interface CameraExposureElapsed extends CameraExposureEvent { } - -export interface CameraExposureFinished extends CameraExposureEvent { + captureElapsedTime: number + captureProgress: number + captureRemainingTime: number + exposureProgress: number + exposureRemainingTime: number + waitRemainingTime: number + waitProgress: number savePath?: string } @@ -487,22 +462,13 @@ export interface Satellite { groups: SatelliteGroupType[] } -export interface DARVPolarAlignmentEvent extends MessageEvent { +export interface DARVEvent extends MessageEvent { camera: Camera guideOutput: GuideOutput remainingTime: number progress: number - state: DARVPolarAlignmentState -} - -export interface DARVPolarAlignmentInitialPauseElapsed extends DARVPolarAlignmentEvent { - pauseTime: number - state: 'INITIAL_PAUSE' -} - -export interface DARVPolarAlignmentGuidePulseElapsed extends DARVPolarAlignmentEvent { - direction: GuideDirection - state: 'FORWARD' | 'BACKWARD' + state: DARVState + direction?: GuideDirection } export interface CoordinateInterpolation { @@ -783,7 +749,7 @@ export const API_EVENT_TYPES = [ 'GUIDER_CONNECTED', 'GUIDER_DISCONNECTED', 'GUIDER_UPDATED', 'GUIDER_STEPPED', 'GUIDER_MESSAGE_RECEIVED', // Polar Alignment. - 'DARV_POLAR_ALIGNMENT_STARTED', 'DARV_POLAR_ALIGNMENT_FINISHED', 'DARV_POLAR_ALIGNMENT_UPDATED', + 'DARV_POLAR_ALIGNMENT_ELAPSED', ] as const export type ApiEventType = (typeof API_EVENT_TYPES)[number] @@ -802,7 +768,9 @@ export const NOTIFICATION_EVENT_TYPE = [ export type NotificationEventType = (typeof NOTIFICATION_EVENT_TYPE)[number] -export type ImageSource = 'FRAMING' | 'PATH' | 'CAMERA' +export type ImageSource = 'FRAMING' | + 'PATH' | + 'CAMERA' export const HIPS_SURVEY_TYPES = [ 'CDS_P_DSS2_NIR', @@ -835,11 +803,18 @@ export const HIPS_SURVEY_TYPES = [ export type HipsSurveyType = (typeof HIPS_SURVEY_TYPES)[number] -export type PierSide = 'EAST' | 'WEST' | 'NEITHER' +export type PierSide = 'EAST' | + 'WEST' | + 'NEITHER' -export type TargetCoordinateType = 'J2000' | 'JNOW' +export type TargetCoordinateType = 'J2000' | + 'JNOW' -export type TrackMode = 'SIDEREAL' | ' LUNAR' | 'SOLAR' | 'KING' | 'CUSTOM' +export type TrackMode = 'SIDEREAL' | + ' LUNAR' | + 'SOLAR' | + 'KING' | + 'CUSTOM' export type GuideDirection = 'NORTH' | // DEC+ 'SOUTH' | // DEC- @@ -888,10 +863,24 @@ export const GUIDE_STATES = [ export type GuideState = (typeof GUIDE_STATES)[number] -export type Hemisphere = 'NORTHERN' | 'SOUTHERN' +export type Hemisphere = 'NORTHERN' | + 'SOUTHERN' + +export type DARVState = 'IDLE' | + 'INITIAL_PAUSE' | + 'FORWARD' | + 'BACKWARD' -export type DARVPolarAlignmentState = 'IDLE' | 'INITIAL_PAUSE' | 'FORWARD' | 'BACKWARD' +export type GuiderPlotMode = 'RA/DEC' | + 'DX/DY' -export type GuiderPlotMode = 'RA/DEC' | 'DX/DY' +export type GuiderYAxisUnit = 'ARCSEC' | + 'PIXEL' -export type GuiderYAxisUnit = 'ARCSEC' | 'PIXEL' +export type CameraCaptureState = 'CAPTURE_STARTED' | + 'EXPOSURE_STARTED' | + 'EXPOSURING' | + 'WAITING' | + 'SETTLING' | + 'EXPOSURE_FINISHED' | + 'CAPTURE_FINISHED' diff --git a/nebulosa-batch-processing/build.gradle.kts b/nebulosa-batch-processing/build.gradle.kts new file mode 100644 index 000000000..73f4f81c5 --- /dev/null +++ b/nebulosa-batch-processing/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + kotlin("jvm") + id("maven-publish") +} + +dependencies { + api(libs.rx) + implementation(project(":nebulosa-log")) + testImplementation(project(":nebulosa-test")) +} + +publishing { + publications { + create("pluginMaven") { + from(components["java"]) + } + } +} diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/AsyncJobLauncher.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/AsyncJobLauncher.kt new file mode 100644 index 000000000..221b17265 --- /dev/null +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/AsyncJobLauncher.kt @@ -0,0 +1,141 @@ +package nebulosa.batch.processing + +import nebulosa.log.loggerFor +import java.time.LocalDateTime +import java.util.concurrent.Executor + +open class AsyncJobLauncher(private val executor: Executor) : JobLauncher, StepInterceptor { + + private val jobListeners = LinkedHashSet() + private val stepListeners = LinkedHashSet() + private val stepInterceptors = LinkedHashSet() + private val jobs = LinkedHashMap() + + override var stepHandler: StepHandler = DefaultStepHandler + + override fun registerJobExecutionListener(listener: JobExecutionListener): Boolean { + return jobListeners.add(listener) + } + + override fun unregisterJobExecutionListener(listener: JobExecutionListener): Boolean { + return jobListeners.remove(listener) + } + + override fun registerStepExecutionListener(listener: StepExecutionListener): Boolean { + return stepListeners.add(listener) + } + + override fun unregisterStepExecutionListener(listener: StepExecutionListener): Boolean { + return stepListeners.remove(listener) + } + + override fun registerStepInterceptor(interceptor: StepInterceptor): Boolean { + return stepInterceptors.add(interceptor) + } + + override fun unregisterStepInterceptor(interceptor: StepInterceptor): Boolean { + return stepInterceptors.remove(interceptor) + } + + override val size + get() = jobs.size + + override fun contains(element: JobExecution): Boolean { + return jobs.containsValue(element) + } + + override fun containsAll(elements: Collection): Boolean { + return elements.all { it in this } + } + + override fun isEmpty(): Boolean { + return jobs.isEmpty() + } + + override fun iterator(): Iterator { + return jobs.values.iterator() + } + + @Synchronized + override fun launch(job: Job, executionContext: ExecutionContext?): JobExecution { + var jobExecution = jobs[job.id] + + if (jobExecution != null) { + if (!jobExecution.isDone) { + return jobExecution + } + } + + val interceptors = ArrayList(stepInterceptors.size + 1) + interceptors.addAll(stepInterceptors) + interceptors.add(this) + + jobExecution = JobExecution(job, executionContext ?: ExecutionContext(), this, interceptors) + + jobs[job.id] = jobExecution + + executor.execute { + jobExecution.status = JobStatus.STARTED + + job.beforeJob(jobExecution) + jobListeners.forEach { it.beforeJob(jobExecution) } + + val stepJobListeners = LinkedHashSet() + + try { + while (jobExecution.canContinue && job.hasNext(jobExecution)) { + val step = job.next(jobExecution) + + if (step is JobExecutionListener) { + if (stepJobListeners.add(step)) { + step.beforeJob(jobExecution) + } + } + + val result = stepHandler.handle(step, StepExecution(step, jobExecution)) + result.get() + } + + jobExecution.status = if (jobExecution.isStopping) JobStatus.STOPPED else JobStatus.COMPLETED + jobExecution.complete() + } catch (e: Throwable) { + LOG.error("job failed. job=$job, jobExecution=$jobExecution", e) + jobExecution.status = JobStatus.FAILED + jobExecution.completeExceptionally(e) + } finally { + jobExecution.finishedAt = LocalDateTime.now() + } + + job.afterJob(jobExecution) + jobListeners.forEach { it.afterJob(jobExecution) } + stepJobListeners.forEach { it.afterJob(jobExecution) } + } + + return jobExecution + } + + override fun stop(mayInterruptIfRunning: Boolean) { + jobs.forEach { stop(it.value, mayInterruptIfRunning) } + } + + override fun stop(jobExecution: JobExecution, mayInterruptIfRunning: Boolean) { + if (!jobExecution.isDone && !jobExecution.isStopping) { + jobExecution.status = JobStatus.STOPPING + jobExecution.job.stop(mayInterruptIfRunning) + } + } + + override fun intercept(chain: StepChain): StepResult { + stepListeners.forEach { it.beforeStep(chain.stepExecution) } + val result = chain.step.execute(chain.stepExecution) + stepListeners.forEach { it.afterStep(chain.stepExecution) } + return result + } + + override fun toString() = "AsyncJobLauncher" + + companion object { + + @JvmStatic private val LOG = loggerFor() + } +} diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/DefaultStepHandler.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/DefaultStepHandler.kt new file mode 100644 index 000000000..248aea50f --- /dev/null +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/DefaultStepHandler.kt @@ -0,0 +1,39 @@ +package nebulosa.batch.processing + +import nebulosa.log.loggerFor + +object DefaultStepHandler : StepHandler { + + @JvmStatic private val LOG = loggerFor() + + override fun handle(step: Step, stepExecution: StepExecution): StepResult { + val jobLauncher = stepExecution.jobExecution.jobLauncher + + when (step) { + is SplitStep -> { + step.beforeStep(stepExecution) + step.parallelStream().forEach { jobLauncher.stepHandler.handle(it, stepExecution) } + step.afterStep(stepExecution) + } + is FlowStep -> { + step.beforeStep(stepExecution) + step.forEach { jobLauncher.stepHandler.handle(it, stepExecution) } + step.afterStep(stepExecution) + } + else -> { + val chain = StepInterceptorChain(stepExecution.jobExecution.stepInterceptors, step, stepExecution) + + LOG.info("step started. step={}, context={}", step, stepExecution.context) + + while (stepExecution.jobExecution.canContinue) { + val status = chain.proceed().get() + if (status != RepeatStatus.CONTINUABLE) break + } + + LOG.info("step finished. step={}, context={}", step, stepExecution.context) + } + } + + return StepResult.FINISHED + } +} diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/ExecutionContext.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/ExecutionContext.kt new file mode 100644 index 000000000..0525cbad9 --- /dev/null +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/ExecutionContext.kt @@ -0,0 +1,10 @@ +package nebulosa.batch.processing + +import java.util.concurrent.ConcurrentHashMap + +open class ExecutionContext : ConcurrentHashMap { + + constructor(initialCapacity: Int = 64) : super(initialCapacity) + + constructor(context: ExecutionContext) : super(context) +} diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/FlowStep.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/FlowStep.kt new file mode 100644 index 000000000..ae3f1a49f --- /dev/null +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/FlowStep.kt @@ -0,0 +1,12 @@ +package nebulosa.batch.processing + +interface FlowStep : Step, StepExecutionListener, Collection { + + override fun execute(stepExecution: StepExecution): StepResult { + return stepExecution.jobExecution.jobLauncher.stepHandler.handle(this, stepExecution) + } + + override fun stop(mayInterruptIfRunning: Boolean) { + forEach { it.stop(mayInterruptIfRunning) } + } +} diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/Job.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/Job.kt new file mode 100644 index 000000000..11dcf486a --- /dev/null +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/Job.kt @@ -0,0 +1,14 @@ +package nebulosa.batch.processing + +interface Job : JobExecutionListener, Stoppable { + + val id: String + + fun hasNext(jobExecution: JobExecution): Boolean + + fun next(jobExecution: JobExecution): Step + + override fun beforeJob(jobExecution: JobExecution) = Unit + + override fun afterJob(jobExecution: JobExecution) = Unit +} diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobExecution.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobExecution.kt new file mode 100644 index 000000000..7dfed6d38 --- /dev/null +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobExecution.kt @@ -0,0 +1,58 @@ +package nebulosa.batch.processing + +import java.time.LocalDateTime +import java.util.concurrent.CompletableFuture +import java.util.concurrent.ExecutionException +import java.util.concurrent.TimeUnit + +data class JobExecution( + val job: Job, + val context: ExecutionContext, + val jobLauncher: JobLauncher, + val stepInterceptors: List, + val startedAt: LocalDateTime = LocalDateTime.now(), + var status: JobStatus = JobStatus.STARTING, + var finishedAt: LocalDateTime? = null, +) { + + @JvmField internal val completable = CompletableFuture() + + inline val jobId + get() = job.id + + inline val canContinue + get() = status == JobStatus.STARTED + + inline val isStopping + get() = status == JobStatus.STOPPING + + inline val isStopped + get() = status == JobStatus.STOPPED + + inline val isCompleted + get() = status == JobStatus.COMPLETED + + inline val isFailed + get() = status == JobStatus.FAILED + + val isDone + get() = isCompleted || isFailed || isStopped + + fun waitForCompletion(timeout: Long = 0L, unit: TimeUnit = TimeUnit.MILLISECONDS): Boolean { + try { + if (timeout <= 0L) completable.get() + else return completable.get(timeout, unit) + return true + } catch (e: ExecutionException) { + throw e.cause ?: e + } + } + + internal fun complete() { + completable.complete(true) + } + + internal fun completeExceptionally(e: Throwable) { + completable.completeExceptionally(e) + } +} diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobExecutionListener.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobExecutionListener.kt new file mode 100644 index 000000000..70508ddc9 --- /dev/null +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobExecutionListener.kt @@ -0,0 +1,8 @@ +package nebulosa.batch.processing + +interface JobExecutionListener { + + fun beforeJob(jobExecution: JobExecution) = Unit + + fun afterJob(jobExecution: JobExecution) = Unit +} diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobLauncher.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobLauncher.kt new file mode 100644 index 000000000..fa00440a7 --- /dev/null +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobLauncher.kt @@ -0,0 +1,22 @@ +package nebulosa.batch.processing + +interface JobLauncher : Collection, Stoppable { + + val stepHandler: StepHandler + + fun registerJobExecutionListener(listener: JobExecutionListener): Boolean + + fun unregisterJobExecutionListener(listener: JobExecutionListener): Boolean + + fun registerStepExecutionListener(listener: StepExecutionListener): Boolean + + fun unregisterStepExecutionListener(listener: StepExecutionListener): Boolean + + fun registerStepInterceptor(interceptor: StepInterceptor): Boolean + + fun unregisterStepInterceptor(interceptor: StepInterceptor): Boolean + + fun launch(job: Job, executionContext: ExecutionContext? = null): JobExecution + + fun stop(jobExecution: JobExecution, mayInterruptIfRunning: Boolean = true) +} diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobStatus.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobStatus.kt new file mode 100644 index 000000000..005aa08e2 --- /dev/null +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobStatus.kt @@ -0,0 +1,11 @@ +package nebulosa.batch.processing + +enum class JobStatus { + STARTING, + STARTED, + STOPPING, + STOPPED, + FAILED, + COMPLETED, + ABANDONED, +} diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/PublishSubscribe.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/PublishSubscribe.kt new file mode 100644 index 000000000..a2a26aa00 --- /dev/null +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/PublishSubscribe.kt @@ -0,0 +1,44 @@ +package nebulosa.batch.processing + +import io.reactivex.rxjava3.core.Observable +import io.reactivex.rxjava3.core.ObservableSource +import io.reactivex.rxjava3.core.Observer +import io.reactivex.rxjava3.disposables.Disposable +import io.reactivex.rxjava3.functions.Consumer +import io.reactivex.rxjava3.subjects.Subject +import java.io.Closeable + +interface PublishSubscribe : ObservableSource, Observer, Closeable { + + val subject: Subject + + fun Observable.transform() = this + + fun subscribe(onNext: Consumer): Disposable { + return subject.transform().subscribe(onNext) + } + + override fun subscribe(observer: Observer) { + return subject.transform().subscribe(observer) + } + + override fun onSubscribe(disposable: Disposable) { + subject.onSubscribe(disposable) + } + + override fun onNext(event: T) { + subject.onNext(event) + } + + override fun onError(e: Throwable) { + subject.onError(e) + } + + override fun onComplete() { + subject.onComplete() + } + + override fun close() { + onComplete() + } +} diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/RepeatStatus.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/RepeatStatus.kt new file mode 100644 index 000000000..aa45a61ba --- /dev/null +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/RepeatStatus.kt @@ -0,0 +1,6 @@ +package nebulosa.batch.processing + +enum class RepeatStatus { + CONTINUABLE, + FINISHED, +} diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/SimpleFlowStep.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/SimpleFlowStep.kt new file mode 100644 index 000000000..7f45b613f --- /dev/null +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/SimpleFlowStep.kt @@ -0,0 +1,10 @@ +package nebulosa.batch.processing + +open class SimpleFlowStep : FlowStep, ArrayList { + + constructor(initialCapacity: Int = 4) : super(initialCapacity) + + constructor(steps: Collection) : super(steps) + + constructor(vararg steps: Step) : this(steps.toList()) +} diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/SimpleJob.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/SimpleJob.kt new file mode 100644 index 000000000..9b062638a --- /dev/null +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/SimpleJob.kt @@ -0,0 +1,36 @@ +package nebulosa.batch.processing + +abstract class SimpleJob : Job, ArrayList { + + constructor(initialCapacity: Int = 4) : super(initialCapacity) + + constructor(steps: Collection) : super(steps) + + constructor(vararg steps: Step) : this(steps.toList()) + + @Volatile private var position = 0 + @Volatile private var stopped = false + + override fun hasNext(jobExecution: JobExecution): Boolean { + return !stopped && position < size + } + + override fun next(jobExecution: JobExecution): Step { + return this[position++] + } + + override fun stop(mayInterruptIfRunning: Boolean) { + if (stopped) return + + stopped = true + + if (position in 1..size) { + this[position - 1].stop(mayInterruptIfRunning) + } + } + + fun reset() { + stopped = false + position = 0 + } +} diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/SimpleSplitStep.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/SimpleSplitStep.kt new file mode 100644 index 000000000..ba17430ae --- /dev/null +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/SimpleSplitStep.kt @@ -0,0 +1,10 @@ +package nebulosa.batch.processing + +open class SimpleSplitStep : SimpleFlowStep, SplitStep { + + constructor(initialCapacity: Int = 4) : super(initialCapacity) + + constructor(steps: Collection) : super(steps) + + constructor(vararg steps: Step) : this(steps.toList()) +} diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/SplitStep.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/SplitStep.kt new file mode 100644 index 000000000..a91bc8fa1 --- /dev/null +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/SplitStep.kt @@ -0,0 +1,3 @@ +package nebulosa.batch.processing + +interface SplitStep : FlowStep diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/Step.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/Step.kt new file mode 100644 index 000000000..e9a5d7ce3 --- /dev/null +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/Step.kt @@ -0,0 +1,8 @@ +package nebulosa.batch.processing + +interface Step : Stoppable { + + fun execute(stepExecution: StepExecution): StepResult + + override fun stop(mayInterruptIfRunning: Boolean) = Unit +} diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepChain.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepChain.kt new file mode 100644 index 000000000..4e4d97cb4 --- /dev/null +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepChain.kt @@ -0,0 +1,10 @@ +package nebulosa.batch.processing + +interface StepChain { + + val step: Step + + val stepExecution: StepExecution + + fun proceed(): StepResult +} diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepExecution.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepExecution.kt new file mode 100644 index 000000000..56538f1b7 --- /dev/null +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepExecution.kt @@ -0,0 +1,10 @@ +package nebulosa.batch.processing + +data class StepExecution( + val step: Step, + val jobExecution: JobExecution, +) { + + inline val context + get() = jobExecution.context +} diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepExecutionListener.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepExecutionListener.kt new file mode 100644 index 000000000..009528698 --- /dev/null +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepExecutionListener.kt @@ -0,0 +1,8 @@ +package nebulosa.batch.processing + +interface StepExecutionListener { + + fun beforeStep(stepExecution: StepExecution) = Unit + + fun afterStep(stepExecution: StepExecution) = Unit +} diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepHandler.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepHandler.kt new file mode 100644 index 000000000..0e6ad8bd0 --- /dev/null +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepHandler.kt @@ -0,0 +1,6 @@ +package nebulosa.batch.processing + +interface StepHandler { + + fun handle(step: Step, stepExecution: StepExecution): StepResult +} diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepInterceptor.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepInterceptor.kt new file mode 100644 index 000000000..80759da03 --- /dev/null +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepInterceptor.kt @@ -0,0 +1,6 @@ +package nebulosa.batch.processing + +interface StepInterceptor { + + fun intercept(chain: StepChain): StepResult +} diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepInterceptorChain.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepInterceptorChain.kt new file mode 100644 index 000000000..d1ed9a905 --- /dev/null +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepInterceptorChain.kt @@ -0,0 +1,15 @@ +package nebulosa.batch.processing + +data class StepInterceptorChain( + private val interceptors: List, + override val step: Step, + override val stepExecution: StepExecution, + private val index: Int = 0, +) : StepChain { + + override fun proceed(): StepResult { + val next = StepInterceptorChain(interceptors, step, stepExecution, index + 1) + val interceptor = interceptors[index] + return interceptor.intercept(next) + } +} diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepResult.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepResult.kt new file mode 100644 index 000000000..e3d044cd8 --- /dev/null +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepResult.kt @@ -0,0 +1,23 @@ +package nebulosa.batch.processing + +import java.util.concurrent.CompletableFuture +import java.util.concurrent.Future + +data class StepResult(@JvmField internal val completable: CompletableFuture) : Future by completable { + + constructor() : this(CompletableFuture()) + + fun complete(status: RepeatStatus): Boolean { + return completable.complete(status) + } + + fun completeExceptionally(e: Throwable): Boolean { + return completable.completeExceptionally(e) + } + + companion object { + + @JvmStatic val CONTINUABLE = StepResult(CompletableFuture.completedFuture(RepeatStatus.CONTINUABLE)) + @JvmStatic val FINISHED = StepResult(CompletableFuture.completedFuture(RepeatStatus.FINISHED)) + } +} diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/Stoppable.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/Stoppable.kt new file mode 100644 index 000000000..c7c7f48a2 --- /dev/null +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/Stoppable.kt @@ -0,0 +1,6 @@ +package nebulosa.batch.processing + +interface Stoppable { + + fun stop(mayInterruptIfRunning: Boolean = true) +} diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/delay/DelayStep.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/delay/DelayStep.kt new file mode 100644 index 000000000..894f28ad7 --- /dev/null +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/delay/DelayStep.kt @@ -0,0 +1,65 @@ +package nebulosa.batch.processing.delay + +import nebulosa.batch.processing.* +import java.time.Duration + +data class DelayStep(@JvmField val duration: Duration) : Step, JobExecutionListener { + + private val listeners = LinkedHashSet() + + @Volatile private var aborted = false + + fun registerDelayStepListener(listener: DelayStepListener) { + listeners.add(listener) + } + + fun unregisterDelayStepListener(listener: DelayStepListener) { + listeners.remove(listener) + } + + override fun execute(stepExecution: StepExecution): StepResult { + var remainingTime = duration + + if (!aborted && remainingTime > Duration.ZERO) { + while (!aborted && remainingTime > Duration.ZERO) { + val waitTime = minOf(remainingTime, DELAY_INTERVAL) + + if (waitTime > Duration.ZERO) { + stepExecution.context[REMAINING_TIME] = remainingTime + stepExecution.context[WAIT_TIME] = waitTime + + val progress = (duration.toNanos() - remainingTime.toNanos()) / duration.toNanos().toDouble() + stepExecution.context[PROGRESS] = progress + + listeners.forEach { it.onDelayElapsed(this, stepExecution) } + Thread.sleep(waitTime.toMillis()) + remainingTime -= waitTime + } + } + + stepExecution.context[REMAINING_TIME] = Duration.ZERO + stepExecution.context[WAIT_TIME] = Duration.ZERO + + listeners.forEach { it.onDelayElapsed(this, stepExecution) } + } + + return StepResult.FINISHED + } + + override fun stop(mayInterruptIfRunning: Boolean) { + aborted = true + } + + override fun afterJob(jobExecution: JobExecution) { + listeners.clear() + } + + companion object { + + const val REMAINING_TIME = "DELAY.REMAINING_TIME" + const val WAIT_TIME = "DELAY.WAIT_TIME" + const val PROGRESS = "DELAY.PROGRESS" + + @JvmField val DELAY_INTERVAL = Duration.ofMillis(500)!! + } +} diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/delay/DelayStepListener.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/delay/DelayStepListener.kt new file mode 100644 index 000000000..04847c947 --- /dev/null +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/delay/DelayStepListener.kt @@ -0,0 +1,8 @@ +package nebulosa.batch.processing.delay + +import nebulosa.batch.processing.StepExecution + +fun interface DelayStepListener { + + fun onDelayElapsed(step: DelayStep, stepExecution: StepExecution) +} diff --git a/nebulosa-batch-processing/src/test/kotlin/BatchProcessingTest.kt b/nebulosa-batch-processing/src/test/kotlin/BatchProcessingTest.kt new file mode 100644 index 000000000..d9cb0d00b --- /dev/null +++ b/nebulosa-batch-processing/src/test/kotlin/BatchProcessingTest.kt @@ -0,0 +1,146 @@ +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.booleans.shouldBeTrue +import io.kotest.matchers.longs.shouldBeInRange +import io.kotest.matchers.shouldBe +import nebulosa.batch.processing.* +import nebulosa.log.loggerFor +import java.util.concurrent.Executors +import kotlin.concurrent.thread + +class BatchProcessingTest : StringSpec() { + + init { + val launcher = AsyncJobLauncher(Executors.newSingleThreadExecutor()) + + "single" { + val startedAt = System.currentTimeMillis() + val jobExecution = launcher.launch(MathJob(listOf(SumStep()))) + jobExecution.waitForCompletion() + jobExecution.context["VALUE"] shouldBe 1.0 + (System.currentTimeMillis() - startedAt) shouldBeInRange (1000L..2000L) + } + "multiple" { + val startedAt = System.currentTimeMillis() + val jobExecution = launcher.launch(MathJob(listOf(SumStep(), SumStep()))) + jobExecution.waitForCompletion() + jobExecution.context["VALUE"] shouldBe 2.0 + (System.currentTimeMillis() - startedAt) shouldBeInRange (2000L..3000L) + } + "split" { + val startedAt = System.currentTimeMillis() + val jobExecution = launcher.launch(MathJob(listOf(SplitSumStep()))) + jobExecution.waitForCompletion() + jobExecution.context["VALUE"] shouldBe N.toDouble() + (System.currentTimeMillis() - startedAt) shouldBeInRange (1000L..2000L) + } + "flow" { + val startedAt = System.currentTimeMillis() + val jobExecution = launcher.launch(MathJob(listOf(FlowSumStep()))) + jobExecution.waitForCompletion() + jobExecution.context["VALUE"] shouldBe N.toDouble() + (System.currentTimeMillis() - startedAt) shouldBeInRange (N * 1000L..(N + 1) * 1000L) + } + "split flow" { + val startedAt = System.currentTimeMillis() + val jobExecution = launcher.launch(MathJob(listOf(SimpleSplitStep(FlowSumStep(), FlowSumStep())))) + jobExecution.waitForCompletion() + jobExecution.context["VALUE"] shouldBe (N * 2).toDouble() + (System.currentTimeMillis() - startedAt) shouldBeInRange (N * 1000L..(N + 1) * 1000L) + } + "stop" { + val startedAt = System.currentTimeMillis() + val jobExecution = launcher.launch(MathJob((0..7).map { SumStep() })) + thread { Thread.sleep(4000); launcher.stop(jobExecution) } + jobExecution.waitForCompletion() + jobExecution.context["VALUE"] shouldBe 3.0 + jobExecution.isStopped.shouldBeTrue() + (System.currentTimeMillis() - startedAt) shouldBeInRange (4000L..5000L) + } + "repeatable" { + val startedAt = System.currentTimeMillis() + val jobExecution = launcher.launch(MathJob(listOf(SumStep()), 10.0)) + jobExecution.waitForCompletion() + jobExecution.context["VALUE"] shouldBe 20.0 + (System.currentTimeMillis() - startedAt) shouldBeInRange (10000L..11000L) + } + } + + private class MathJob( + steps: List, + private val initialValue: Double = 0.0, + ) : SimpleJob(steps) { + + override val id = "Job.Math" + + override fun beforeJob(jobExecution: JobExecution) { + jobExecution.context["VALUE"] = initialValue + } + } + + private abstract class MathStep : Step { + + @Volatile private var running = false + + protected abstract fun compute(value: Double): Double + + final override fun execute(stepExecution: StepExecution): StepResult { + var sleepCount = 0 + + val jobExecution = stepExecution.jobExecution + running = jobExecution.canContinue + + while (running && sleepCount++ < 100) { + Thread.sleep(10) + } + + if (running) { + synchronized(jobExecution) { + val value = jobExecution.context["VALUE"]!! as Double + LOG.info("executing ${javaClass.simpleName}: $value") + jobExecution.context["VALUE"] = compute(value) + + if (value >= 10.0 && value < 19.0) { + return StepResult.CONTINUABLE + } + } + } + + return StepResult.FINISHED + } + + override fun stop(mayInterruptIfRunning: Boolean) { + running = false + } + } + + private class SumStep : MathStep() { + + override fun compute(value: Double): Double { + return value + 1.0 + } + } + + private class SplitSumStep : SimpleSplitStep() { + + init { + repeat(N) { + add(SumStep()) + } + } + } + + private class FlowSumStep : SimpleFlowStep() { + + init { + repeat(N) { + add(SumStep()) + } + } + } + + companion object { + + @JvmStatic private val LOG = loggerFor() + @JvmStatic private val N = Runtime.getRuntime().availableProcessors() + } +} diff --git a/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/CancellationToken.kt b/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/CancellationToken.kt new file mode 100644 index 000000000..00d954d68 --- /dev/null +++ b/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/CancellationToken.kt @@ -0,0 +1,60 @@ +package nebulosa.common.concurrency + +import java.io.Closeable +import java.util.concurrent.Future +import java.util.concurrent.TimeUnit + +class CancellationToken : Closeable, Future { + + private val latch = CountUpDownLatch(1) + private val listeners = LinkedHashSet() + + fun listen(action: Runnable): Boolean { + return if (isDone) { + action.run() + false + } else { + listeners.add(action) + } + } + + fun cancel() { + cancel(true) + } + + @Synchronized + override fun cancel(mayInterruptIfRunning: Boolean): Boolean { + if (latch.count <= 0) return false + latch.reset() + listeners.forEach(Runnable::run) + listeners.clear() + return true + } + + override fun isCancelled(): Boolean { + return latch.get() + } + + override fun isDone(): Boolean { + return latch.get() + } + + override fun get(): Boolean { + latch.await() + return true + } + + override fun get(timeout: Long, unit: TimeUnit): Boolean { + return latch.await(timeout, unit) + } + + fun reset() { + latch.countUp(1 - latch.count) + listeners.clear() + } + + override fun close() { + latch.reset() + listeners.clear() + } +} diff --git a/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/CountUpDownLatch.kt b/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/CountUpDownLatch.kt index 884b96376..3b373dddf 100644 --- a/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/CountUpDownLatch.kt +++ b/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/CountUpDownLatch.kt @@ -6,15 +6,13 @@ import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.locks.AbstractQueuedSynchronizer import kotlin.math.max -class CountUpDownLatch(initialCount: Int = 0) : AtomicBoolean(true) { +class CountUpDownLatch(initialCount: Int = 0) : AtomicBoolean(initialCount == 0) { private val sync = Sync(this) init { - if (initialCount > 0) { - sync.count = initialCount - set(false) - } + require(initialCount >= 0) { "initialCount < 0: $initialCount" } + sync.count = initialCount } val count diff --git a/nebulosa-guiding-phd2/build.gradle.kts b/nebulosa-guiding-phd2/build.gradle.kts index 38ee1e398..7c9baa997 100644 --- a/nebulosa-guiding-phd2/build.gradle.kts +++ b/nebulosa-guiding-phd2/build.gradle.kts @@ -5,7 +5,6 @@ plugins { dependencies { api(project(":nebulosa-io")) - api(project(":nebulosa-common")) api(project(":nebulosa-guiding")) api(project(":nebulosa-phd2-client")) implementation(project(":nebulosa-log")) 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 766c38366..daaa58bcc 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 @@ -1,5 +1,6 @@ package nebulosa.guiding.phd2 +import nebulosa.common.concurrency.CancellationToken import nebulosa.common.concurrency.CountUpDownLatch import nebulosa.guiding.* import nebulosa.log.loggerFor @@ -19,7 +20,7 @@ class PHD2Guider(private val client: PHD2Client) : Guider, PHD2EventListener { @Volatile private var shiftRateAxis = ShiftAxesType.RADEC @Volatile private var lockPosition = GuidePoint.ZERO @Volatile private var starPosition = GuidePoint.ZERO - private val listeners = hashSetOf() + private val listeners = LinkedHashSet() override var pixelScale = 1.0 private set @@ -232,14 +233,16 @@ class PHD2Guider(private val client: PHD2Client) : Guider, PHD2EventListener { } } - override fun waitForSettle() { + override fun waitForSettle(cancellationToken: CancellationToken?) { try { + cancellationToken?.listen(settling::reset) settling.await(settleTimeout) } catch (e: 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?.close() settling.reset() } } diff --git a/nebulosa-guiding/build.gradle.kts b/nebulosa-guiding/build.gradle.kts index 9ad896cec..f891151a0 100644 --- a/nebulosa-guiding/build.gradle.kts +++ b/nebulosa-guiding/build.gradle.kts @@ -5,6 +5,7 @@ plugins { dependencies { api(project(":nebulosa-math")) + api(project(":nebulosa-common")) api(project(":nebulosa-indi-device")) 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 4e37d4626..178a039c3 100644 --- a/nebulosa-guiding/src/main/kotlin/nebulosa/guiding/Guider.kt +++ b/nebulosa-guiding/src/main/kotlin/nebulosa/guiding/Guider.kt @@ -1,5 +1,6 @@ package nebulosa.guiding +import nebulosa.common.concurrency.CancellationToken import java.io.Closeable import java.time.Duration @@ -35,7 +36,7 @@ interface Guider : Closeable { fun dither(amount: Double, raOnly: Boolean = false) - fun waitForSettle() + fun waitForSettle(cancellationToken: CancellationToken? = null) companion object { diff --git a/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/camera/CameraDevice.kt b/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/camera/CameraDevice.kt index 5e87324cc..5d96adf67 100644 --- a/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/camera/CameraDevice.kt +++ b/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/camera/CameraDevice.kt @@ -339,14 +339,14 @@ internal open class CameraDevice( override fun close() { if (hasThermometer) { - hasThermometer = false handler.unregisterThermometer(this) + hasThermometer = false LOG.info("thermometer detached: {}", name) } if (canPulseGuide) { - canPulseGuide = false handler.unregisterGuideOutput(this) + canPulseGuide = false LOG.info("guide output detached: {}", name) } } diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/PHD2Client.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/PHD2Client.kt index f192dfcac..9d3789923 100644 --- a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/PHD2Client.kt +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/PHD2Client.kt @@ -23,7 +23,7 @@ import kotlin.math.max class PHD2Client : NettyClient() { - @JvmField internal val listeners = hashSetOf() + @JvmField internal val listeners = LinkedHashSet() @JvmField internal val commands = hashMapOf>() override val channelInitialzer = object : ChannelInitializer() { diff --git a/nebulosa-stellarium-protocol/src/main/kotlin/nebulosa/stellarium/protocol/StellariumProtocolServer.kt b/nebulosa-stellarium-protocol/src/main/kotlin/nebulosa/stellarium/protocol/StellariumProtocolServer.kt index 046a8cded..b815be467 100644 --- a/nebulosa-stellarium-protocol/src/main/kotlin/nebulosa/stellarium/protocol/StellariumProtocolServer.kt +++ b/nebulosa-stellarium-protocol/src/main/kotlin/nebulosa/stellarium/protocol/StellariumProtocolServer.kt @@ -71,7 +71,7 @@ class StellariumProtocolServer( ) : NettyServer(), CurrentPositionHandler { private val stellariumMountHandler = AtomicReference() - private val currentPositionHandlers = hashSetOf() + private val currentPositionHandlers = LinkedHashSet() val rightAscension: Angle? get() = stellariumMountHandler.get()?.rightAscension diff --git a/settings.gradle.kts b/settings.gradle.kts index 925045edd..bc2c3728e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -53,6 +53,7 @@ include(":nebulosa-alpaca-discovery-protocol") include(":nebulosa-astap") include(":nebulosa-astrometrynet") include(":nebulosa-astrometrynet-jna") +include(":nebulosa-batch-processing") include(":nebulosa-common") include(":nebulosa-constants") include(":nebulosa-erfa")