diff --git a/.github/dependabot.yml b/.github/dependabot.yml index d03b43554..5f1f1ea80 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -10,7 +10,6 @@ updates: directory: "/" schedule: interval: "monthly" - day: "friday" open-pull-requests-limit: 16 target-branch: "dev" commit-message: @@ -28,6 +27,9 @@ updates: okhttp: patterns: - "com.squareup.okhttp3*" + rx: + patterns: + - "io.reactivex.rxjava3*" jackson: patterns: - "com.fasterxml.jackson*" @@ -36,7 +38,6 @@ updates: directory: "/desktop" schedule: interval: "monthly" - day: "friday" open-pull-requests-limit: 64 target-branch: "dev" commit-message: @@ -45,12 +46,14 @@ updates: angular: patterns: - "@angular*" + types: + patterns: + - "@types*" - package-ecosystem: "npm" directory: "/desktop/app" schedule: interval: "monthly" - day: "friday" target-branch: "dev" commit-message: prefix: "[desktop]" diff --git a/api/build.gradle.kts b/api/build.gradle.kts index 52b7ecc12..ad7cb9aca 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,8 +32,8 @@ 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") implementation("org.springframework.boot:spring-boot-starter-web") { exclude(module = "spring-boot-starter-tomcat") @@ -41,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 9d79844e8..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,7 +1,7 @@ package nebulosa.api.alignment.polar -import nebulosa.api.alignment.polar.darv.DARVStart -import nebulosa.api.beans.annotations.EntityBy +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 import org.springframework.web.bind.annotation.PutMapping @@ -17,14 +17,14 @@ class PolarAlignmentController( @PutMapping("darv/{camera}/{guideOutput}/start") fun darvStart( - @EntityBy camera: Camera, @EntityBy guideOutput: GuideOutput, - @RequestBody body: DARVStart, + @EntityParam camera: Camera, @EntityParam guideOutput: GuideOutput, + @RequestBody body: DARVStartRequest, ) { polarAlignmentService.darvStart(camera, guideOutput, body) } @PutMapping("darv/{camera}/{guideOutput}/stop") - fun darvStop(@EntityBy camera: Camera, @EntityBy guideOutput: GuideOutput) { + fun darvStop(@EntityParam camera: Camera, @EntityParam guideOutput: GuideOutput) { polarAlignmentService.darvStop(camera, guideOutput) } } 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 730aba73e..000000000 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentExecutor.kt +++ /dev/null @@ -1,161 +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.common.concurrency.Incrementer -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.configuration.JobRegistry -import org.springframework.batch.core.configuration.support.ReferenceJobFactory -import org.springframework.batch.core.job.builder.JobBuilder -import org.springframework.batch.core.launch.JobLauncher -import org.springframework.batch.core.launch.JobOperator -import org.springframework.batch.core.repository.JobRepository -import org.springframework.core.task.SimpleAsyncTaskExecutor -import org.springframework.stereotype.Component -import java.nio.file.Path -import java.util.* - -/** - * @see Reference - */ -@Component -class DARVPolarAlignmentExecutor( - private val jobRepository: JobRepository, - private val jobOperator: JobOperator, - private val jobLauncher: JobLauncher, - private val jobRegistry: JobRegistry, - private val messageService: MessageService, - private val jobIncrementer: Incrementer, - private val capturesPath: Path, - private val sequenceFlowFactory: SequenceFlowFactory, - private val sequenceTaskletFactory: SequenceTaskletFactory, - private val simpleAsyncTaskExecutor: SimpleAsyncTaskExecutor, -) : 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 = JobBuilder("DARVPolarAlignment.Job.${jobIncrementer.increment()}", jobRepository) - .start(cameraExposureFlow) - .split(simpleAsyncTaskExecutor) - .add(guidePulseFlow) - .end() - .listener(this) - .listener(cameraExposureTasklet) - .build() - - return jobLauncher - .run(darvJob, JobParameters()) - .let { DARVSequenceJob(camera, guideOutput, request, darvJob, it) } - .also(runningSequenceJobs::add) - .also { jobRegistry.register(ReferenceJobFactory(darvJob)) } - } - - @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/DeepSkyObjectRepository.kt b/api/src/main/kotlin/nebulosa/api/atlas/DeepSkyObjectRepository.kt index 96ceb780e..714ca0f8e 100644 --- a/api/src/main/kotlin/nebulosa/api/atlas/DeepSkyObjectRepository.kt +++ b/api/src/main/kotlin/nebulosa/api/atlas/DeepSkyObjectRepository.kt @@ -9,10 +9,11 @@ import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.Query import org.springframework.stereotype.Repository import org.springframework.transaction.annotation.Isolation +import org.springframework.transaction.annotation.Propagation import org.springframework.transaction.annotation.Transactional @Repository -@Transactional(readOnly = false, isolation = Isolation.SERIALIZABLE) +@Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW, isolation = Isolation.SERIALIZABLE) interface DeepSkyObjectRepository : JpaRepository { @Query( @@ -24,7 +25,7 @@ interface DeepSkyObjectRepository : JpaRepository { "(:radius <= 0.0 OR acos(sin(dso.declinationJ2000) * sin(:declinationJ2000) + cos(dso.declinationJ2000) * cos(:declinationJ2000) * cos(dso.rightAscensionJ2000 - :rightAscensionJ2000)) <= :radius) " + "ORDER BY dso.magnitude ASC" ) - @Transactional(readOnly = true, isolation = Isolation.READ_UNCOMMITTED) + @Transactional(readOnly = true) fun search( text: String? = null, rightAscensionJ2000: Angle = 0.0, declinationJ2000: Angle = 0.0, radius: Angle = 0.0, @@ -35,6 +36,5 @@ interface DeepSkyObjectRepository : JpaRepository { ): List @Query("SELECT DISTINCT dso.type FROM DeepSkyObjectEntity dso") - @Transactional(readOnly = true, isolation = Isolation.READ_UNCOMMITTED) fun types(): List } diff --git a/api/src/main/kotlin/nebulosa/api/atlas/IERSUpdateTask.kt b/api/src/main/kotlin/nebulosa/api/atlas/IERSUpdateTask.kt new file mode 100644 index 000000000..54a4c2c0b --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/atlas/IERSUpdateTask.kt @@ -0,0 +1,72 @@ +package nebulosa.api.atlas + +import nebulosa.api.preferences.PreferenceService +import nebulosa.io.transferAndClose +import nebulosa.log.loggerFor +import nebulosa.time.IERS +import nebulosa.time.IERSA +import okhttp3.OkHttpClient +import okhttp3.Request +import org.springframework.http.HttpHeaders +import org.springframework.scheduling.annotation.Scheduled +import org.springframework.stereotype.Component +import java.nio.file.Path +import java.util.concurrent.TimeUnit +import kotlin.io.path.inputStream +import kotlin.io.path.outputStream + +@Component +class IERSUpdateTask( + private val dataPath: Path, + private val preferenceService: PreferenceService, + private val httpClient: OkHttpClient, +) : Runnable { + + @Scheduled(initialDelay = 5L, fixedDelay = Long.MAX_VALUE, timeUnit = TimeUnit.SECONDS) + override fun run() { + val finals2000A = Path.of("$dataPath", "finals2000A.all") + + finals2000A.download() + + val iersa = IERSA() + finals2000A.inputStream().use(iersa::load) + IERS.attach(iersa) + } + + private fun Path.download() { + try { + var request = Request.Builder() + .head() + .url(IERSA.URL) + .build() + + var modifiedAt = httpClient.newCall(request).execute() + .use { it.headers.getDate(HttpHeaders.LAST_MODIFIED) } + + if (modifiedAt != null && "$modifiedAt" == preferenceService.getText(IERS_UPDATED_AT)) { + LOG.info("finals2000A.all is up to date. modifiedAt={}", modifiedAt) + return + } + + request = request.newBuilder().get().build() + + LOG.info("downloading finals2000A.all") + + httpClient.newCall(request).execute().use { + it.body!!.byteStream().transferAndClose(outputStream()) + modifiedAt = it.headers.getDate(HttpHeaders.LAST_MODIFIED) + preferenceService.putText(IERS_UPDATED_AT, "$modifiedAt") + LOG.info("finals2000A.all downloaded. modifiedAt={}", modifiedAt) + } + } catch (e: Throwable) { + LOG.error("failed to download finals2000A.all", e) + } + } + + companion object { + + const val IERS_UPDATED_AT = "IERS_UPDATED_AT" + + @JvmStatic private val LOG = loggerFor() + } +} diff --git a/api/src/main/kotlin/nebulosa/api/atlas/IERSUpdater.kt b/api/src/main/kotlin/nebulosa/api/atlas/IERSUpdater.kt deleted file mode 100644 index f607859d0..000000000 --- a/api/src/main/kotlin/nebulosa/api/atlas/IERSUpdater.kt +++ /dev/null @@ -1,54 +0,0 @@ -package nebulosa.api.atlas - -import nebulosa.api.beans.annotations.ThreadedTask -import nebulosa.io.transferAndClose -import nebulosa.log.loggerFor -import nebulosa.time.IERS -import nebulosa.time.IERSA -import okhttp3.OkHttpClient -import okhttp3.Request -import org.springframework.stereotype.Component -import java.nio.file.Path -import kotlin.io.path.inputStream -import kotlin.io.path.outputStream - -@Component -@ThreadedTask -class IERSUpdater( - private val dataPath: Path, - private val httpClient: OkHttpClient, -) : Runnable { - - override fun run() { - val finals2000A = Path.of("$dataPath", "finals2000A.all") - - finals2000A.download() - - val iersa = IERSA() - finals2000A.inputStream().use(iersa::load) - IERS.attach(iersa) - } - - private fun Path.download() { - val request = Request.Builder() - .get() - .url(IERSA.URL) - .build() - - try { - LOG.info("downloading finals2000A.all") - - httpClient.newCall(request).execute().use { - it.body!!.byteStream().transferAndClose(outputStream()) - LOG.info("finals2000A.all loaded") - } - } catch (e: Throwable) { - LOG.error("failed to download finals2000A.all", e) - } - } - - companion object { - - @JvmStatic private val LOG = loggerFor() - } -} diff --git a/api/src/main/kotlin/nebulosa/api/atlas/SatelliteRepository.kt b/api/src/main/kotlin/nebulosa/api/atlas/SatelliteRepository.kt index fdb0f847a..a3cf1b87d 100644 --- a/api/src/main/kotlin/nebulosa/api/atlas/SatelliteRepository.kt +++ b/api/src/main/kotlin/nebulosa/api/atlas/SatelliteRepository.kt @@ -5,10 +5,11 @@ import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.Query import org.springframework.stereotype.Repository import org.springframework.transaction.annotation.Isolation +import org.springframework.transaction.annotation.Propagation import org.springframework.transaction.annotation.Transactional @Repository -@Transactional(readOnly = false, isolation = Isolation.SERIALIZABLE) +@Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW, isolation = Isolation.SERIALIZABLE) interface SatelliteRepository : JpaRepository { @Query( @@ -17,7 +18,7 @@ interface SatelliteRepository : JpaRepository { " (:groupType = 0 OR s.group_type & :groupType != 0)", nativeQuery = true, ) - @Transactional(readOnly = true, isolation = Isolation.READ_UNCOMMITTED) + @Transactional(readOnly = true) fun search(text: String? = null, groupType: Long = 0L, page: Pageable): List fun search(text: String? = null, groups: List, page: Pageable): List { diff --git a/api/src/main/kotlin/nebulosa/api/atlas/SatelliteUpdater.kt b/api/src/main/kotlin/nebulosa/api/atlas/SatelliteUpdateTask.kt similarity index 90% rename from api/src/main/kotlin/nebulosa/api/atlas/SatelliteUpdater.kt rename to api/src/main/kotlin/nebulosa/api/atlas/SatelliteUpdateTask.kt index f80c548ee..fe9e3024a 100644 --- a/api/src/main/kotlin/nebulosa/api/atlas/SatelliteUpdater.kt +++ b/api/src/main/kotlin/nebulosa/api/atlas/SatelliteUpdateTask.kt @@ -8,7 +8,7 @@ import org.springframework.stereotype.Component import java.util.concurrent.CompletableFuture @Component -class SatelliteUpdater( +class SatelliteUpdateTask( private val httpClient: OkHttpClient, private val preferenceService: PreferenceService, private val satelliteRepository: SatelliteRepository, @@ -19,7 +19,7 @@ class SatelliteUpdater( } private fun isOutOfDate(): Boolean { - val updatedAt = preferenceService.satellitesUpdatedAt + val updatedAt = preferenceService.getLong(SATELLITES_UPDATED_AT) ?: 0L return System.currentTimeMillis() - updatedAt >= UPDATE_INTERVAL } @@ -28,7 +28,7 @@ class SatelliteUpdater( LOG.info("satellites is out of date") if (updateTLEs()) { - preferenceService.satellitesUpdatedAt = System.currentTimeMillis() + preferenceService.putLong(SATELLITES_UPDATED_AT, System.currentTimeMillis()) } else { LOG.warn("no satellites was updated") } @@ -100,7 +100,8 @@ class SatelliteUpdater( companion object { const val UPDATE_INTERVAL = 1000L * 60 * 60 * 24 // 1 day + const val SATELLITES_UPDATED_AT = "SATELLITES_UPDATED_AT" - @JvmStatic private val LOG = loggerFor() + @JvmStatic private val LOG = loggerFor() } } diff --git a/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasController.kt b/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasController.kt index 11756d57b..e083648de 100644 --- a/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasController.kt +++ b/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasController.kt @@ -4,8 +4,8 @@ import jakarta.servlet.http.HttpServletResponse import jakarta.validation.Valid import jakarta.validation.constraints.Min import jakarta.validation.constraints.NotBlank -import nebulosa.api.beans.annotations.DateAndTime -import nebulosa.api.beans.annotations.EntityBy +import nebulosa.api.beans.annotations.DateAndTimeParam +import nebulosa.api.beans.annotations.EntityParam import nebulosa.api.locations.LocationEntity import nebulosa.math.deg import nebulosa.math.hours @@ -27,42 +27,42 @@ class SkyAtlasController( @GetMapping("sun/position") fun positionOfSun( - @EntityBy location: LocationEntity, - @DateAndTime dateTime: LocalDateTime, + @EntityParam location: LocationEntity, + @DateAndTimeParam dateTime: LocalDateTime, ) = skyAtlasService.positionOfSun(location, dateTime) @GetMapping("sun/altitude-points") fun altitudePointsOfSun( - @EntityBy location: LocationEntity, - @DateAndTime dateTime: LocalDateTime, + @EntityParam location: LocationEntity, + @DateAndTimeParam dateTime: LocalDateTime, @RequestParam(required = false, defaultValue = "1") @Valid @Min(1) stepSize: Int, ) = skyAtlasService.altitudePointsOfSun(location, dateTime.toLocalDate(), stepSize) @GetMapping("moon/position") fun positionOfMoon( - @EntityBy location: LocationEntity, - @DateAndTime dateTime: LocalDateTime, + @EntityParam location: LocationEntity, + @DateAndTimeParam dateTime: LocalDateTime, ) = skyAtlasService.positionOfMoon(location, dateTime) @GetMapping("moon/altitude-points") fun altitudePointsOfMoon( - @EntityBy location: LocationEntity, - @DateAndTime dateTime: LocalDateTime, + @EntityParam location: LocationEntity, + @DateAndTimeParam dateTime: LocalDateTime, @RequestParam(required = false, defaultValue = "1") stepSize: Int, ) = skyAtlasService.altitudePointsOfMoon(location, dateTime.toLocalDate(), stepSize) @GetMapping("planets/{code}/position") fun positionOfPlanet( @PathVariable code: String, - @EntityBy location: LocationEntity, - @DateAndTime dateTime: LocalDateTime, + @EntityParam location: LocationEntity, + @DateAndTimeParam dateTime: LocalDateTime, ) = skyAtlasService.positionOfPlanet(location, code, dateTime) @GetMapping("planets/{code}/altitude-points") fun altitudePointsOfPlanet( @PathVariable code: String, - @EntityBy location: LocationEntity, - @DateAndTime dateTime: LocalDateTime, + @EntityParam location: LocationEntity, + @DateAndTimeParam dateTime: LocalDateTime, @RequestParam(required = false, defaultValue = "1") stepSize: Int, ) = skyAtlasService.altitudePointsOfPlanet(location, code, dateTime.toLocalDate(), stepSize) @@ -71,16 +71,16 @@ class SkyAtlasController( @GetMapping("stars/{star}/position") fun positionOfStar( - @EntityBy star: StarEntity, - @EntityBy location: LocationEntity, - @DateAndTime dateTime: LocalDateTime, + @EntityParam star: StarEntity, + @EntityParam location: LocationEntity, + @DateAndTimeParam dateTime: LocalDateTime, ) = skyAtlasService.positionOfStar(location, star, dateTime) @GetMapping("stars/{star}/altitude-points") fun altitudePointsOfStar( - @EntityBy star: StarEntity, - @EntityBy location: LocationEntity, - @DateAndTime dateTime: LocalDateTime, + @EntityParam star: StarEntity, + @EntityParam location: LocationEntity, + @DateAndTimeParam dateTime: LocalDateTime, @RequestParam(required = false, defaultValue = "1") stepSize: Int, ) = skyAtlasService.altitudePointsOfStar(location, star, dateTime.toLocalDate(), stepSize) @@ -104,16 +104,16 @@ class SkyAtlasController( @GetMapping("dsos/{dso}/position") fun positionOfDSO( - @EntityBy dso: DeepSkyObjectEntity, - @EntityBy location: LocationEntity, - @DateAndTime dateTime: LocalDateTime, + @EntityParam dso: DeepSkyObjectEntity, + @EntityParam location: LocationEntity, + @DateAndTimeParam dateTime: LocalDateTime, ) = skyAtlasService.positionOfDSO(location, dso, dateTime) @GetMapping("dsos/{dso}/altitude-points") fun altitudePointsOfDSO( - @EntityBy dso: DeepSkyObjectEntity, - @EntityBy location: LocationEntity, - @DateAndTime dateTime: LocalDateTime, + @EntityParam dso: DeepSkyObjectEntity, + @EntityParam location: LocationEntity, + @DateAndTimeParam dateTime: LocalDateTime, @RequestParam(required = false, defaultValue = "1") stepSize: Int, ) = skyAtlasService.altitudePointsOfDSO(location, dso, dateTime.toLocalDate(), stepSize) @@ -138,15 +138,15 @@ class SkyAtlasController( @GetMapping("simbad/{id}/position") fun positionOfSimbad( @PathVariable id: Long, - @EntityBy location: LocationEntity, - @DateAndTime dateTime: LocalDateTime, + @EntityParam location: LocationEntity, + @DateAndTimeParam dateTime: LocalDateTime, ) = skyAtlasService.positionOfSimbad(location, id, dateTime) @GetMapping("simbad/{id}/altitude-points") fun altitudePointsOfSimbad( @PathVariable id: Long, - @EntityBy location: LocationEntity, - @DateAndTime dateTime: LocalDateTime, + @EntityParam location: LocationEntity, + @DateAndTimeParam dateTime: LocalDateTime, @RequestParam(required = false, defaultValue = "1") stepSize: Int, ) = skyAtlasService.altitudePointsOfSimbad(location, id, dateTime.toLocalDate(), stepSize) @@ -170,16 +170,16 @@ class SkyAtlasController( @GetMapping("satellites/{satellite}/position") fun positionOfSatellite( - @EntityBy satellite: SatelliteEntity, - @EntityBy location: LocationEntity, - @DateAndTime dateTime: LocalDateTime, + @EntityParam satellite: SatelliteEntity, + @EntityParam location: LocationEntity, + @DateAndTimeParam dateTime: LocalDateTime, ) = skyAtlasService.positionOfSatellite(location, satellite, dateTime) @GetMapping("satellites/{satellite}/altitude-points") fun altitudePointsOfSatellite( - @EntityBy satellite: SatelliteEntity, - @EntityBy location: LocationEntity, - @DateAndTime dateTime: LocalDateTime, + @EntityParam satellite: SatelliteEntity, + @EntityParam location: LocationEntity, + @DateAndTimeParam dateTime: LocalDateTime, @RequestParam(required = false, defaultValue = "1") stepSize: Int, ) = skyAtlasService.altitudePointsOfSatellite(location, satellite, dateTime.toLocalDate(), stepSize) @@ -191,7 +191,7 @@ class SkyAtlasController( @GetMapping("twilight") fun twilight( - @EntityBy location: LocationEntity, - @DateAndTime dateTime: LocalDateTime, + @EntityParam location: LocationEntity, + @DateAndTimeParam dateTime: LocalDateTime, ) = skyAtlasService.twilight(location, dateTime.toLocalDate()) } diff --git a/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasService.kt b/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasService.kt index 8471ff9f7..aef72d3cb 100644 --- a/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasService.kt +++ b/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasService.kt @@ -241,7 +241,7 @@ class SkyAtlasService( .search(SimbadSearch(0, text, rightAscension, declination, radius, type?.let(::listOf), magnitudeMin, magnitudeMax, constellation, 5000)) @Scheduled(fixedDelay = 15, timeUnit = TimeUnit.MINUTES) - private fun refreshImageOfSun() { + protected fun refreshImageOfSun() { val request = Request.Builder() .url(SUN_IMAGE_URL) .build() diff --git a/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasUpdateFinished.kt b/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasUpdateFinished.kt deleted file mode 100644 index a351c304b..000000000 --- a/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasUpdateFinished.kt +++ /dev/null @@ -1,8 +0,0 @@ -package nebulosa.api.atlas - -import nebulosa.api.notification.NotificationEvent - -data class SkyAtlasUpdateFinished(override val body: String) : NotificationEvent { - - override val type = "SKY_ATLAS_UPDATE_FINISHED" -} diff --git a/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasUpdater.kt b/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasUpdateTask.kt similarity index 74% rename from api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasUpdater.kt rename to api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasUpdateTask.kt index e194de02c..20d490cae 100644 --- a/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasUpdater.kt +++ b/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasUpdateTask.kt @@ -2,41 +2,48 @@ package nebulosa.api.atlas import com.fasterxml.jackson.core.type.TypeReference import com.fasterxml.jackson.databind.ObjectMapper -import nebulosa.api.beans.annotations.ThreadedTask +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 +import org.springframework.scheduling.annotation.Scheduled import org.springframework.stereotype.Component import java.io.InputStream import java.nio.file.Path +import java.util.concurrent.TimeUnit import java.util.zip.GZIPInputStream import kotlin.io.path.exists import kotlin.io.path.inputStream @Component -@ThreadedTask -class SkyAtlasUpdater( +class SkyAtlasUpdateTask( private val objectMapper: ObjectMapper, private val preferenceService: PreferenceService, private val starsRepository: StarRepository, private val deepSkyObjectRepository: DeepSkyObjectRepository, private val httpClient: OkHttpClient, private val dataPath: Path, - private val satelliteUpdater: SatelliteUpdater, + private val satelliteUpdateTask: SatelliteUpdateTask, private val messageService: MessageService, ) : Runnable { + data class Finished(override val body: String) : NotificationEvent { + + override val type = "SKY_ATLAS_UPDATE_FINISHED" + } + + @Scheduled(initialDelay = 1L, fixedDelay = Long.MAX_VALUE, timeUnit = TimeUnit.SECONDS) override fun run() { - satelliteUpdater.run() + satelliteUpdateTask.run() - val version = preferenceService.skyAtlasVersion + val version = preferenceService.getText(SKY_ATLAS_VERSION) if (version != DATABASE_VERSION) { LOG.info("Star/DSO database is out of date. currentVersion={}, newVersion={}", version, DATABASE_VERSION) - messageService.sendMessage(SkyAtlasUpdateFinished("Star/DSO database is being updated.")) + messageService.sendMessage(Finished("Star/DSO database is being updated.")) starsRepository.deleteAllInBatch() deepSkyObjectRepository.deleteAllInBatch() @@ -44,11 +51,11 @@ class SkyAtlasUpdater( readStarsAndLoad() readDSOsAndLoad() - preferenceService.skyAtlasVersion = DATABASE_VERSION + preferenceService.putText(SKY_ATLAS_VERSION, DATABASE_VERSION) - messageService.sendMessage(SkyAtlasUpdateFinished("Sky Atlas database was updated to version $DATABASE_VERSION.")) + messageService.sendMessage(Finished("Sky Atlas database was updated to version $DATABASE_VERSION.")) } else { - LOG.info("Star/DSO database is up to date") + LOG.info("Star/DSO database is up to date. version={}", version) } } @@ -109,7 +116,8 @@ class SkyAtlasUpdater( companion object { const val DATABASE_VERSION = "2023.10.18" + const val SKY_ATLAS_VERSION = "SKY_ATLAS_VERSION" - @JvmStatic private val LOG = loggerFor() + @JvmStatic private val LOG = loggerFor() } } diff --git a/api/src/main/kotlin/nebulosa/api/atlas/StarRepository.kt b/api/src/main/kotlin/nebulosa/api/atlas/StarRepository.kt index 5b6160a59..25f813b5b 100644 --- a/api/src/main/kotlin/nebulosa/api/atlas/StarRepository.kt +++ b/api/src/main/kotlin/nebulosa/api/atlas/StarRepository.kt @@ -9,10 +9,11 @@ import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.Query import org.springframework.stereotype.Repository import org.springframework.transaction.annotation.Isolation +import org.springframework.transaction.annotation.Propagation import org.springframework.transaction.annotation.Transactional @Repository -@Transactional(readOnly = false, isolation = Isolation.SERIALIZABLE) +@Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW, isolation = Isolation.SERIALIZABLE) interface StarRepository : JpaRepository { @Query( @@ -24,7 +25,7 @@ interface StarRepository : JpaRepository { "(:radius <= 0.0 OR acos(sin(star.declinationJ2000) * sin(:declinationJ2000) + cos(star.declinationJ2000) * cos(:declinationJ2000) * cos(star.rightAscensionJ2000 - :rightAscensionJ2000)) <= :radius) " + "ORDER BY star.magnitude ASC" ) - @Transactional(readOnly = true, isolation = Isolation.READ_UNCOMMITTED) + @Transactional(readOnly = true) fun search( text: String? = null, rightAscensionJ2000: Angle = 0.0, declinationJ2000: Angle = 0.0, radius: Angle = 0.0, @@ -35,6 +36,5 @@ interface StarRepository : JpaRepository { ): List @Query("SELECT DISTINCT star.type FROM StarEntity star") - @Transactional(readOnly = true, isolation = Isolation.READ_UNCOMMITTED) fun types(): List } diff --git a/api/src/main/kotlin/nebulosa/api/beans/DateAndTimeMethodArgumentResolver.kt b/api/src/main/kotlin/nebulosa/api/beans/DateAndTimeMethodArgumentResolver.kt deleted file mode 100644 index 28aa879c1..000000000 --- a/api/src/main/kotlin/nebulosa/api/beans/DateAndTimeMethodArgumentResolver.kt +++ /dev/null @@ -1,58 +0,0 @@ -package nebulosa.api.beans - -import jakarta.servlet.http.HttpServletRequest -import nebulosa.api.beans.annotations.DateAndTime -import org.springframework.core.MethodParameter -import org.springframework.stereotype.Component -import org.springframework.web.bind.support.WebDataBinderFactory -import org.springframework.web.context.request.NativeWebRequest -import org.springframework.web.method.support.HandlerMethodArgumentResolver -import org.springframework.web.method.support.ModelAndViewContainer -import org.springframework.web.servlet.HandlerMapping -import java.time.LocalDate -import java.time.LocalDateTime -import java.time.LocalTime -import java.time.format.DateTimeFormatter - -@Component -class DateAndTimeMethodArgumentResolver : HandlerMethodArgumentResolver { - - override fun supportsParameter(parameter: MethodParameter): Boolean { - return parameter.hasParameterAnnotation(DateAndTime::class.java) - } - - override fun resolveArgument( - parameter: MethodParameter, - mavContainer: ModelAndViewContainer?, - webRequest: NativeWebRequest, - binderFactory: WebDataBinderFactory? - ): Any? { - val dateAndTime = parameter.getParameterAnnotation(DateAndTime::class.java)!! - - val dateValue = webRequest.pathVariables()["date"] - ?: webRequest.getParameter("date") - val timeValue = webRequest.pathVariables()["time"] - ?: webRequest.getParameter("time") - - val date = dateValue?.ifBlank { null } - ?.let { LocalDate.parse(it, DateTimeFormatter.ofPattern(dateAndTime.datePattern)) } - ?: LocalDate.now() - - val time = timeValue?.ifBlank { null } - ?.let { LocalTime.parse(it, DateTimeFormatter.ofPattern(dateAndTime.timePattern)) } - ?: LocalTime.now() - - return LocalDateTime.of(date, time) - .let { if (dateAndTime.noSeconds) it.withSecond(0).withNano(0) else it } - } - - companion object { - - @JvmStatic - @Suppress("UNCHECKED_CAST") - private fun NativeWebRequest.pathVariables(): Map { - val httpServletRequest = getNativeRequest(HttpServletRequest::class.java)!! - return httpServletRequest.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE) as Map - } - } -} diff --git a/api/src/main/kotlin/nebulosa/api/beans/EntityByMethodArgumentResolver.kt b/api/src/main/kotlin/nebulosa/api/beans/EntityByMethodArgumentResolver.kt deleted file mode 100644 index 31f06c603..000000000 --- a/api/src/main/kotlin/nebulosa/api/beans/EntityByMethodArgumentResolver.kt +++ /dev/null @@ -1,86 +0,0 @@ -package nebulosa.api.beans - -import jakarta.servlet.http.HttpServletRequest -import nebulosa.api.atlas.* -import nebulosa.api.beans.annotations.EntityBy -import nebulosa.api.connection.ConnectionService -import nebulosa.api.locations.LocationEntity -import nebulosa.api.locations.LocationRepository -import nebulosa.indi.device.camera.Camera -import nebulosa.indi.device.filterwheel.FilterWheel -import nebulosa.indi.device.focuser.Focuser -import nebulosa.indi.device.guide.GuideOutput -import nebulosa.indi.device.mount.Mount -import org.springframework.core.MethodParameter -import org.springframework.data.repository.findByIdOrNull -import org.springframework.http.HttpStatus -import org.springframework.stereotype.Component -import org.springframework.web.bind.support.WebDataBinderFactory -import org.springframework.web.context.request.NativeWebRequest -import org.springframework.web.method.support.HandlerMethodArgumentResolver -import org.springframework.web.method.support.ModelAndViewContainer -import org.springframework.web.server.ResponseStatusException -import org.springframework.web.servlet.HandlerMapping - -@Component -class EntityByMethodArgumentResolver( - private val locationRepository: LocationRepository, - private val starRepository: StarRepository, - private val deepSkyObjectRepository: DeepSkyObjectRepository, - private val satelliteRepository: SatelliteRepository, - private val connectionService: ConnectionService, -) : HandlerMethodArgumentResolver { - - override fun supportsParameter(parameter: MethodParameter): Boolean { - return parameter.hasParameterAnnotation(EntityBy::class.java) - } - - override fun resolveArgument( - parameter: MethodParameter, - mavContainer: ModelAndViewContainer?, - webRequest: NativeWebRequest, - binderFactory: WebDataBinderFactory? - ): Any? { - val entityBy = parameter.getParameterAnnotation(EntityBy::class.java)!! - val parameterType = parameter.parameterType - val parameterName = parameter.parameterName ?: "id" - val parameterValue = webRequest.pathVariables()[parameterName] - ?: webRequest.getParameter(parameterName) - - val entity = entityByParameterValue(parameterType, parameterValue) - - if (entityBy.required && entity == null) { - val message = "Cannot found a ${parameterType.simpleName} entity with name [$parameterValue]" - throw ResponseStatusException(HttpStatus.NOT_FOUND, message) - } - - return entity - } - - private fun entityByParameterValue(parameterType: Class<*>, parameterValue: String?): Any? { - if (parameterValue.isNullOrBlank()) return null - - return when (parameterType) { - LocationEntity::class.java -> locationRepository.findByIdOrNull(parameterValue.toLong()) - StarEntity::class.java -> starRepository.findByIdOrNull(parameterValue.toLong()) - DeepSkyObjectEntity::class.java -> deepSkyObjectRepository.findByIdOrNull(parameterValue.toLong()) - SatelliteEntity::class.java -> satelliteRepository.findByIdOrNull(parameterValue.toLong()) - Camera::class.java -> connectionService.camera(parameterValue) - Mount::class.java -> connectionService.mount(parameterValue) - Focuser::class.java -> connectionService.focuser(parameterValue) - FilterWheel::class.java -> connectionService.wheel(parameterValue) - GuideOutput::class.java -> connectionService.guideOutput(parameterValue) - else -> null - } - } - - companion object { - - @JvmStatic - @Suppress("UNCHECKED_CAST") - private fun NativeWebRequest.pathVariables(): Map { - val httpServletRequest = getNativeRequest(HttpServletRequest::class.java)!! - return httpServletRequest.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE) as Map - } - } -} diff --git a/api/src/main/kotlin/nebulosa/api/beans/ThreadedTaskBeanPostProcessor.kt b/api/src/main/kotlin/nebulosa/api/beans/ThreadedTaskBeanPostProcessor.kt deleted file mode 100644 index ac50ea599..000000000 --- a/api/src/main/kotlin/nebulosa/api/beans/ThreadedTaskBeanPostProcessor.kt +++ /dev/null @@ -1,29 +0,0 @@ -package nebulosa.api.beans - -import nebulosa.api.beans.annotations.ThreadedTask -import nebulosa.log.loggerFor -import org.springframework.beans.factory.config.BeanPostProcessor -import org.springframework.stereotype.Component -import java.util.concurrent.CompletableFuture -import java.util.concurrent.ExecutorService - -@Component -class ThreadedTaskBeanPostProcessor(private val systemExecutorService: ExecutorService) : BeanPostProcessor { - - override fun postProcessAfterInitialization(bean: Any, beanName: String): Any { - if (bean is Runnable && bean::class.java.isAnnotationPresent(ThreadedTask::class.java)) { - LOG.info("threaded task scheduled. name={}", beanName) - - CompletableFuture - .runAsync(bean, systemExecutorService) - .whenComplete { _, e -> e?.printStackTrace() ?: LOG.info("threaded task finished. name={}", beanName) } - } - - return bean - } - - companion object { - - @JvmStatic private val LOG = loggerFor() - } -} diff --git a/api/src/main/kotlin/nebulosa/api/beans/annotations/AngleParam.kt b/api/src/main/kotlin/nebulosa/api/beans/annotations/AngleParam.kt new file mode 100644 index 000000000..a8d1266cb --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/beans/annotations/AngleParam.kt @@ -0,0 +1,10 @@ +package nebulosa.api.beans.annotations + +@Retention +@Target(AnnotationTarget.VALUE_PARAMETER) +annotation class AngleParam( + val name: String = "", + val required: Boolean = true, + val isHours: Boolean = false, + val defaultValue: String = "", +) diff --git a/api/src/main/kotlin/nebulosa/api/beans/annotations/DateAndTime.kt b/api/src/main/kotlin/nebulosa/api/beans/annotations/DateAndTimeParam.kt similarity index 85% rename from api/src/main/kotlin/nebulosa/api/beans/annotations/DateAndTime.kt rename to api/src/main/kotlin/nebulosa/api/beans/annotations/DateAndTimeParam.kt index 67ec80781..2dcdfe264 100644 --- a/api/src/main/kotlin/nebulosa/api/beans/annotations/DateAndTime.kt +++ b/api/src/main/kotlin/nebulosa/api/beans/annotations/DateAndTimeParam.kt @@ -2,7 +2,7 @@ package nebulosa.api.beans.annotations @Retention @Target(AnnotationTarget.VALUE_PARAMETER) -annotation class DateAndTime( +annotation class DateAndTimeParam( val datePattern: String = "yyyy-MM-dd", val timePattern: String = "HH:mm", val noSeconds: Boolean = true, diff --git a/api/src/main/kotlin/nebulosa/api/beans/annotations/EntityBy.kt b/api/src/main/kotlin/nebulosa/api/beans/annotations/EntityParam.kt similarity index 61% rename from api/src/main/kotlin/nebulosa/api/beans/annotations/EntityBy.kt rename to api/src/main/kotlin/nebulosa/api/beans/annotations/EntityParam.kt index ed3846b4a..6f99c36fb 100644 --- a/api/src/main/kotlin/nebulosa/api/beans/annotations/EntityBy.kt +++ b/api/src/main/kotlin/nebulosa/api/beans/annotations/EntityParam.kt @@ -2,4 +2,4 @@ package nebulosa.api.beans.annotations @Retention @Target(AnnotationTarget.VALUE_PARAMETER) -annotation class EntityBy(val required: Boolean = true) +annotation class EntityParam(val required: Boolean = true) diff --git a/api/src/main/kotlin/nebulosa/api/beans/annotations/ThreadedTask.kt b/api/src/main/kotlin/nebulosa/api/beans/annotations/ThreadedTask.kt deleted file mode 100644 index 4cfbb9144..000000000 --- a/api/src/main/kotlin/nebulosa/api/beans/annotations/ThreadedTask.kt +++ /dev/null @@ -1,8 +0,0 @@ -package nebulosa.api.beans.annotations - -import org.springframework.context.annotation.Lazy - -@Retention -@Lazy(false) -@Target(AnnotationTarget.CLASS) -annotation class ThreadedTask 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 b5afd2a0b..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,10 +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.api.beans.DateAndTimeMethodArgumentResolver -import nebulosa.api.beans.EntityByMethodArgumentResolver -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 @@ -23,22 +20,16 @@ import okhttp3.ConnectionPool import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import org.greenrobot.eventbus.EventBus -import org.springframework.batch.core.launch.JobLauncher -import org.springframework.batch.core.launch.support.TaskExecutorJobLauncher -import org.springframework.batch.core.repository.JobRepository import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration -import org.springframework.context.annotation.Primary -import org.springframework.core.task.SimpleAsyncTaskExecutor import org.springframework.http.converter.HttpMessageConverter import org.springframework.http.converter.StringHttpMessageConverter +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor import org.springframework.web.method.support.HandlerMethodArgumentResolver import org.springframework.web.servlet.config.annotation.CorsRegistry import org.springframework.web.servlet.config.annotation.WebMvcConfigurer import java.nio.file.Path -import java.util.concurrent.ExecutorService -import java.util.concurrent.Executors import java.util.concurrent.TimeUnit import kotlin.io.path.createDirectories @@ -107,38 +98,23 @@ class BeanConfiguration { fun hips2FitsService(httpClient: OkHttpClient) = Hips2FitsService(httpClient = httpClient) @Bean - fun systemExecutorService(): ExecutorService = - Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors(), DaemonThreadFactory) + fun threadPoolTaskExecutor(): ThreadPoolTaskExecutor { + val taskExecutor = ThreadPoolTaskExecutor() + taskExecutor.corePoolSize = 32 + taskExecutor.initialize() + return taskExecutor + } @Bean - fun eventBus(systemExecutorService: ExecutorService) = EventBus.builder() + fun eventBus(threadPoolTaskExecutor: ThreadPoolTaskExecutor) = EventBus.builder() .sendNoSubscriberEvent(false) .sendSubscriberExceptionEvent(false) .throwSubscriberException(false) .logNoSubscriberMessages(false) .logSubscriberExceptions(false) - .executorService(systemExecutorService) + .executorService(threadPoolTaskExecutor.threadPoolExecutor) .installDefaultEventBus()!! - @Bean - @Primary - fun asyncJobLauncher(jobRepository: JobRepository): JobLauncher { - val jobLauncher = TaskExecutorJobLauncher() - jobLauncher.setJobRepository(jobRepository) - jobLauncher.setTaskExecutor(SimpleAsyncTaskExecutor(DaemonThreadFactory)) - jobLauncher.afterPropertiesSet() - return jobLauncher - } - - @Bean - fun flowIncrementer() = Incrementer() - - @Bean - fun stepIncrementer() = Incrementer() - - @Bean - fun jobIncrementer() = Incrementer() - @Bean fun phd2Client() = PHD2Client() @@ -146,12 +122,13 @@ class BeanConfiguration { fun phd2Guider(phd2Client: PHD2Client): Guider = PHD2Guider(phd2Client) @Bean - fun simpleAsyncTaskExecutor() = SimpleAsyncTaskExecutor(DaemonThreadFactory) + fun asyncJobLauncher(threadPoolTaskExecutor: ThreadPoolTaskExecutor) = AsyncJobLauncher(threadPoolTaskExecutor) @Bean fun webMvcConfigurer( - entityByMethodArgumentResolver: EntityByMethodArgumentResolver, - dateAndTimeMethodArgumentResolver: DateAndTimeMethodArgumentResolver, + entityParamMethodArgumentResolver: HandlerMethodArgumentResolver, + dateAndTimeParamMethodArgumentResolver: HandlerMethodArgumentResolver, + angleParamMethodArgumentResolver: HandlerMethodArgumentResolver, ) = object : WebMvcConfigurer { override fun extendMessageConverters(converters: MutableList>) { @@ -168,8 +145,9 @@ class BeanConfiguration { } override fun addArgumentResolvers(resolvers: MutableList) { - resolvers.add(entityByMethodArgumentResolver) - resolvers.add(dateAndTimeMethodArgumentResolver) + resolvers.add(entityParamMethodArgumentResolver) + resolvers.add(dateAndTimeParamMethodArgumentResolver) + resolvers.add(angleParamMethodArgumentResolver) } } diff --git a/api/src/main/kotlin/nebulosa/api/beans/configurations/DataSourceConfiguration.kt b/api/src/main/kotlin/nebulosa/api/beans/configurations/DataSourceConfiguration.kt new file mode 100644 index 000000000..8d314a907 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/beans/configurations/DataSourceConfiguration.kt @@ -0,0 +1,63 @@ +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.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.orm.jpa.JpaTransactionManager +import org.springframework.transaction.PlatformTransactionManager +import javax.sql.DataSource + +@Configuration +class DataSourceConfiguration { + + @Value("\${spring.datasource.url}") private lateinit var dataSourceUrl: String + + @Bean + @Primary + fun dataSource(): DataSource { + val config = HikariConfig() + config.jdbcUrl = dataSourceUrl + config.driverClassName = DRIVER_CLASS_NAME + config.maximumPoolSize = 1 + config.minimumIdle = 1 + return HikariDataSource(config) + } + + @Configuration + @EnableJpaRepositories( + basePackages = ["nebulosa.api"], + entityManagerFactoryRef = "entityManagerFactory", + transactionManagerRef = "transactionManager" + ) + class Main { + + @Bean + @Primary + fun entityManagerFactory( + builder: EntityManagerFactoryBuilder, + dataSource: DataSource, + ) = builder + .dataSource(dataSource) + .packages("nebulosa.api") + .persistenceUnit("persistenceUnit") + .build()!! + + @Bean + @Primary + fun transactionManager(entityManagerFactory: EntityManagerFactory): PlatformTransactionManager { + // Fix "no transactions is in progress": https://stackoverflow.com/a/33397173 + return JpaTransactionManager(entityManagerFactory) + } + } + + companion object { + + const val DRIVER_CLASS_NAME = "org.sqlite.JDBC" + } +} diff --git a/api/src/main/kotlin/nebulosa/api/beans/converters/AngleDeserializer.kt b/api/src/main/kotlin/nebulosa/api/beans/converters/AngleDeserializer.kt new file mode 100644 index 000000000..3cd2409c2 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/beans/converters/AngleDeserializer.kt @@ -0,0 +1,17 @@ +package nebulosa.api.beans.converters + +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.deser.std.StdDeserializer +import nebulosa.math.Angle + +abstract class AngleDeserializer( + private val isHours: Boolean = false, + private val decimalIsHours: Boolean = isHours, + private val defaultValue: Angle = Double.NaN, +) : StdDeserializer(Double::class.java) { + + override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Double { + return Angle(p.text, isHours, decimalIsHours, defaultValue) + } +} diff --git a/api/src/main/kotlin/nebulosa/api/beans/converters/ConverterHelper.kt b/api/src/main/kotlin/nebulosa/api/beans/converters/ConverterHelper.kt new file mode 100644 index 000000000..870f7c4ab --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/beans/converters/ConverterHelper.kt @@ -0,0 +1,24 @@ +package nebulosa.api.beans.converters + +import jakarta.servlet.http.HttpServletRequest +import org.springframework.core.MethodParameter +import org.springframework.web.context.request.NativeWebRequest +import org.springframework.web.servlet.HandlerMapping + +@Suppress("UNCHECKED_CAST") +val NativeWebRequest.pathVariables + get() = getNativeRequest(HttpServletRequest::class.java)!! + .getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE) as Map + +fun NativeWebRequest.parameter(name: String) = + pathVariables[name]?.ifBlank { null } + ?: getParameter(name)?.ifBlank { null } + ?: getNativeRequest(HttpServletRequest::class.java)!!.getParameter(name)?.ifBlank { null } + +inline fun MethodParameter.hasAnnotation(): Boolean { + return hasParameterAnnotation(T::class.java) +} + +inline fun MethodParameter.annotation(): T? { + return getParameterAnnotation(T::class.java) +} diff --git a/api/src/main/kotlin/nebulosa/api/beans/converters/DeclinationDeserializer.kt b/api/src/main/kotlin/nebulosa/api/beans/converters/DeclinationDeserializer.kt new file mode 100644 index 000000000..e0951b8c0 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/beans/converters/DeclinationDeserializer.kt @@ -0,0 +1,3 @@ +package nebulosa.api.beans.converters + +class DeclinationDeserializer : AngleDeserializer(true) diff --git a/api/src/main/kotlin/nebulosa/api/beans/converters/RightAscensionDeserializer.kt b/api/src/main/kotlin/nebulosa/api/beans/converters/RightAscensionDeserializer.kt new file mode 100644 index 000000000..a5276730e --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/beans/converters/RightAscensionDeserializer.kt @@ -0,0 +1,3 @@ +package nebulosa.api.beans.converters + +class RightAscensionDeserializer : AngleDeserializer(true) diff --git a/api/src/main/kotlin/nebulosa/api/beans/converters/StringToDurationConverter.kt b/api/src/main/kotlin/nebulosa/api/beans/converters/StringToDurationConverter.kt new file mode 100644 index 000000000..a95e14c30 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/beans/converters/StringToDurationConverter.kt @@ -0,0 +1,18 @@ +package nebulosa.api.beans.converters + +import org.springframework.boot.convert.DurationStyle +import org.springframework.core.convert.converter.Converter +import org.springframework.stereotype.Component +import java.time.Duration +import java.time.temporal.ChronoUnit + +@Component +class StringToDurationConverter : Converter { + + override fun convert(source: String): Duration? { + val text = source.ifBlank { null } ?: return null + + return text.toLongOrNull()?.let { Duration.ofNanos(it * 1000L) } + ?: DurationStyle.SIMPLE.parse(text, ChronoUnit.MICROS) + } +} diff --git a/api/src/main/kotlin/nebulosa/api/beans/resolvers/AngleParamMethodArgumentResolver.kt b/api/src/main/kotlin/nebulosa/api/beans/resolvers/AngleParamMethodArgumentResolver.kt new file mode 100644 index 000000000..f1e649332 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/beans/resolvers/AngleParamMethodArgumentResolver.kt @@ -0,0 +1,33 @@ +package nebulosa.api.beans.resolvers + +import nebulosa.api.beans.annotations.AngleParam +import nebulosa.api.beans.converters.annotation +import nebulosa.api.beans.converters.hasAnnotation +import nebulosa.api.beans.converters.parameter +import nebulosa.math.Angle +import org.springframework.core.MethodParameter +import org.springframework.stereotype.Component +import org.springframework.web.bind.support.WebDataBinderFactory +import org.springframework.web.context.request.NativeWebRequest +import org.springframework.web.method.support.HandlerMethodArgumentResolver +import org.springframework.web.method.support.ModelAndViewContainer + +@Component +class AngleParamMethodArgumentResolver : HandlerMethodArgumentResolver { + + override fun supportsParameter(parameter: MethodParameter): Boolean { + return parameter.hasAnnotation() + } + + override fun resolveArgument( + parameter: MethodParameter, + mavContainer: ModelAndViewContainer?, + webRequest: NativeWebRequest, + binderFactory: WebDataBinderFactory?, + ): Any? { + val param = parameter.annotation()!! + val parameterName = param.name.ifBlank { null } ?: parameter.parameterName!! + val parameterValue = webRequest.parameter(parameterName) ?: param.defaultValue + return Angle(parameterValue, param.isHours) + } +} diff --git a/api/src/main/kotlin/nebulosa/api/beans/resolvers/DateAndTimeParamMethodArgumentResolver.kt b/api/src/main/kotlin/nebulosa/api/beans/resolvers/DateAndTimeParamMethodArgumentResolver.kt new file mode 100644 index 000000000..dad1a9c01 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/beans/resolvers/DateAndTimeParamMethodArgumentResolver.kt @@ -0,0 +1,47 @@ +package nebulosa.api.beans.resolvers + +import nebulosa.api.beans.annotations.DateAndTimeParam +import nebulosa.api.beans.converters.annotation +import nebulosa.api.beans.converters.hasAnnotation +import nebulosa.api.beans.converters.parameter +import org.springframework.core.MethodParameter +import org.springframework.stereotype.Component +import org.springframework.web.bind.support.WebDataBinderFactory +import org.springframework.web.context.request.NativeWebRequest +import org.springframework.web.method.support.HandlerMethodArgumentResolver +import org.springframework.web.method.support.ModelAndViewContainer +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.LocalTime +import java.time.format.DateTimeFormatter + +@Component +class DateAndTimeParamMethodArgumentResolver : HandlerMethodArgumentResolver { + + override fun supportsParameter(parameter: MethodParameter): Boolean { + return parameter.hasAnnotation() + } + + override fun resolveArgument( + parameter: MethodParameter, + mavContainer: ModelAndViewContainer?, + webRequest: NativeWebRequest, + binderFactory: WebDataBinderFactory?, + ): Any? { + val dateAndTimeParam = parameter.annotation()!! + + val dateValue = webRequest.parameter("date") + val timeValue = webRequest.parameter("time") + + val date = dateValue?.ifBlank { null } + ?.let { LocalDate.parse(it, DateTimeFormatter.ofPattern(dateAndTimeParam.datePattern)) } + ?: LocalDate.now() + + val time = timeValue?.ifBlank { null } + ?.let { LocalTime.parse(it, DateTimeFormatter.ofPattern(dateAndTimeParam.timePattern)) } + ?: LocalTime.now() + + return LocalDateTime.of(date, time) + .let { if (dateAndTimeParam.noSeconds) it.withSecond(0).withNano(0) else it } + } +} diff --git a/api/src/main/kotlin/nebulosa/api/beans/resolvers/EntityParamMethodArgumentResolver.kt b/api/src/main/kotlin/nebulosa/api/beans/resolvers/EntityParamMethodArgumentResolver.kt new file mode 100644 index 000000000..9438c7c42 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/beans/resolvers/EntityParamMethodArgumentResolver.kt @@ -0,0 +1,76 @@ +package nebulosa.api.beans.resolvers + +import nebulosa.api.atlas.* +import nebulosa.api.beans.converters.annotation +import nebulosa.api.beans.converters.parameter +import nebulosa.api.connection.ConnectionService +import nebulosa.api.locations.LocationEntity +import nebulosa.api.locations.LocationRepository +import nebulosa.indi.device.Device +import nebulosa.indi.device.camera.Camera +import nebulosa.indi.device.filterwheel.FilterWheel +import nebulosa.indi.device.focuser.Focuser +import nebulosa.indi.device.guide.GuideOutput +import nebulosa.indi.device.mount.Mount +import org.springframework.core.MethodParameter +import org.springframework.data.repository.findByIdOrNull +import org.springframework.http.HttpStatus +import org.springframework.stereotype.Component +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.support.WebDataBinderFactory +import org.springframework.web.context.request.NativeWebRequest +import org.springframework.web.method.support.HandlerMethodArgumentResolver +import org.springframework.web.method.support.ModelAndViewContainer +import org.springframework.web.server.ResponseStatusException + +@Component +class EntityParamMethodArgumentResolver( + private val locationRepository: LocationRepository, + private val starRepository: StarRepository, + private val deepSkyObjectRepository: DeepSkyObjectRepository, + private val satelliteRepository: SatelliteRepository, + private val connectionService: ConnectionService, +) : HandlerMethodArgumentResolver { + + private val entityResolvers = mapOf, (String) -> Any?>( + LocationEntity::class.java to { locationRepository.findByIdOrNull(it.toLong()) }, + StarEntity::class.java to { starRepository.findByIdOrNull(it.toLong()) }, + DeepSkyObjectEntity::class.java to { deepSkyObjectRepository.findByIdOrNull(it.toLong()) }, + SatelliteEntity::class.java to { satelliteRepository.findByIdOrNull(it.toLong()) }, + Device::class.java to { connectionService.device(it) }, + Camera::class.java to { connectionService.camera(it) }, + Mount::class.java to { connectionService.mount(it) }, + Focuser::class.java to { connectionService.focuser(it) }, + FilterWheel::class.java to { connectionService.wheel(it) }, + GuideOutput::class.java to { connectionService.guideOutput(it) }, + ) + + override fun supportsParameter(parameter: MethodParameter): Boolean { + return parameter.parameterType in entityResolvers + } + + override fun resolveArgument( + parameter: MethodParameter, + mavContainer: ModelAndViewContainer?, + webRequest: NativeWebRequest, + binderFactory: WebDataBinderFactory?, + ): Any? { + val requestParam = parameter.annotation() + val parameterName = requestParam?.name?.ifBlank { null } ?: parameter.parameterName ?: "id" + val parameterValue = webRequest.parameter(parameterName) ?: requestParam?.defaultValue + + val entity = entityByParameterValue(parameter.parameterType, parameterValue) + + if (requestParam != null && requestParam.required && entity == null) { + val message = "Cannot found a ${parameter.parameterType.simpleName} entity with name [$parameterValue]" + throw ResponseStatusException(HttpStatus.NOT_FOUND, message) + } + + return entity + } + + private fun entityByParameterValue(parameterType: Class<*>, parameterValue: String?): Any? { + if (parameterValue.isNullOrBlank()) return null + return entityResolvers[parameterType]?.invoke(parameterValue) + } +} diff --git a/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameController.kt b/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameController.kt index a694a7bce..048bf9a89 100644 --- a/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameController.kt +++ b/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameController.kt @@ -1,6 +1,6 @@ package nebulosa.api.calibration -import nebulosa.api.beans.annotations.EntityBy +import nebulosa.api.beans.annotations.EntityParam import nebulosa.indi.device.camera.Camera import org.springframework.web.bind.annotation.* import java.nio.file.Path @@ -12,14 +12,14 @@ class CalibrationFrameController( ) { @GetMapping("{camera}") - fun groups(@EntityBy camera: Camera): List { + fun groups(@EntityParam camera: Camera): List { var id = 0 val groupedFrames = calibrationFrameService.groupedCalibrationFrames(camera) return groupedFrames.map { CalibrationFrameGroup(id++, it.key, it.value) } } @PutMapping("{camera}") - fun upload(@EntityBy camera: Camera, @RequestParam path: Path): List { + fun upload(@EntityParam camera: Camera, @RequestParam path: Path): List { return calibrationFrameService.upload(camera, path) } diff --git a/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameRepository.kt b/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameRepository.kt index 86c6455ba..77bc8eed5 100644 --- a/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameRepository.kt +++ b/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameRepository.kt @@ -6,12 +6,14 @@ import org.springframework.data.jpa.repository.Modifying import org.springframework.data.jpa.repository.Query import org.springframework.stereotype.Repository import org.springframework.transaction.annotation.Isolation +import org.springframework.transaction.annotation.Propagation import org.springframework.transaction.annotation.Transactional @Repository -@Transactional(readOnly = false, isolation = Isolation.SERIALIZABLE) +@Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW, isolation = Isolation.SERIALIZABLE) interface CalibrationFrameRepository : JpaRepository { + @Transactional(readOnly = true) @Query("SELECT frame FROM CalibrationFrameEntity frame WHERE frame.camera = :#{#camera.name}") fun findAll(camera: Camera): List @@ -28,7 +30,7 @@ interface CalibrationFrameRepository : JpaRepository @Query( @@ -38,7 +40,7 @@ interface CalibrationFrameRepository : JpaRepository @Query( @@ -47,6 +49,6 @@ interface CalibrationFrameRepository : JpaRepository } 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 e4828afda..fcfd67088 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureExecutor.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureExecutor.kt @@ -1,75 +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.configuration.JobRegistry -import org.springframework.batch.core.configuration.support.ReferenceJobFactory -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 asyncJobLauncher: JobLauncher, - private val jobRegistry: JobRegistry, 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 asyncJobLauncher - .run(cameraCaptureJob, JobParameters()) - .let { CameraSequenceJob(camera, request, cameraCaptureJob, it) } - .also(runningSequenceJobs::add) - .also { jobRegistry.register(ReferenceJobFactory(cameraCaptureJob)) } + 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/CameraController.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraController.kt index 67cbe040e..94dccbe20 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraController.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraController.kt @@ -1,7 +1,7 @@ package nebulosa.api.cameras import jakarta.validation.Valid -import nebulosa.api.beans.annotations.EntityBy +import nebulosa.api.beans.annotations.EntityParam import nebulosa.api.connection.ConnectionService import nebulosa.indi.device.camera.Camera import org.hibernate.validator.constraints.Range @@ -20,28 +20,28 @@ class CameraController( } @GetMapping("{camera}") - fun camera(@EntityBy camera: Camera): Camera { + fun camera(@EntityParam camera: Camera): Camera { return camera } @PutMapping("{camera}/connect") - fun connect(@EntityBy camera: Camera) { + fun connect(@EntityParam camera: Camera) { cameraService.connect(camera) } @PutMapping("{camera}/disconnect") - fun disconnect(@EntityBy camera: Camera) { + fun disconnect(@EntityParam camera: Camera) { cameraService.disconnect(camera) } @GetMapping("{camera}/capturing") - fun isCapturing(@EntityBy camera: Camera): Boolean { + fun isCapturing(@EntityParam camera: Camera): Boolean { return cameraService.isCapturing(camera) } @PutMapping("{camera}/cooler") fun cooler( - @EntityBy camera: Camera, + @EntityParam camera: Camera, @RequestParam enabled: Boolean, ) { cameraService.cooler(camera, enabled) @@ -49,7 +49,7 @@ class CameraController( @PutMapping("{camera}/temperature/setpoint") fun setpointTemperature( - @EntityBy camera: Camera, + @EntityParam camera: Camera, @RequestParam @Valid @Range(min = -50, max = 50) temperature: Double, ) { cameraService.setpointTemperature(camera, temperature) @@ -57,14 +57,14 @@ class CameraController( @PutMapping("{camera}/capture/start") fun startCapture( - @EntityBy camera: Camera, + @EntityParam camera: Camera, @RequestBody body: CameraStartCaptureRequest, ) { cameraService.startCapture(camera, body) } @PutMapping("{camera}/capture/abort") - fun abortCapture(@EntityBy camera: Camera) { + fun abortCapture(@EntityParam camera: Camera) { cameraService.abortCapture(camera) } } 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/CameraStartCaptureRequest.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraStartCaptureRequest.kt index 4eefe4b60..fd7277ad5 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraStartCaptureRequest.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraStartCaptureRequest.kt @@ -8,14 +8,16 @@ import nebulosa.api.guiding.DitherAfterExposureRequest import nebulosa.indi.device.camera.Camera import nebulosa.indi.device.camera.FrameType import org.hibernate.validator.constraints.Range +import org.hibernate.validator.constraints.time.DurationMax +import org.hibernate.validator.constraints.time.DurationMin import java.nio.file.Path import java.time.Duration data class CameraStartCaptureRequest( @JsonIgnore val camera: Camera? = null, - @field:Positive val exposureTime: Duration = Duration.ZERO, + @field:DurationMin(nanos = 1000L) @field:DurationMax(minutes = 60L) val exposureTime: Duration = Duration.ZERO, @field:Range(min = 0L, max = 1000L) val exposureAmount: Int = 1, // 0 = looping - @field:Range(min = 0L, max = 60L) val exposureDelay: Duration = Duration.ZERO, + @field:DurationMin(nanos = 0L) @field:DurationMax(seconds = 60L) val exposureDelay: Duration = Duration.ZERO, @field:PositiveOrZero val x: Int = 0, @field:PositiveOrZero val y: Int = 0, @field:PositiveOrZero val width: Int = 0, 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/FocuserController.kt b/api/src/main/kotlin/nebulosa/api/focusers/FocuserController.kt index 531054312..40b95db8e 100644 --- a/api/src/main/kotlin/nebulosa/api/focusers/FocuserController.kt +++ b/api/src/main/kotlin/nebulosa/api/focusers/FocuserController.kt @@ -2,7 +2,7 @@ package nebulosa.api.focusers import jakarta.validation.Valid import jakarta.validation.constraints.PositiveOrZero -import nebulosa.api.beans.annotations.EntityBy +import nebulosa.api.beans.annotations.EntityParam import nebulosa.api.connection.ConnectionService import nebulosa.indi.device.focuser.Focuser import org.springframework.web.bind.annotation.* @@ -20,23 +20,23 @@ class FocuserController( } @GetMapping("{focuser}") - fun focuser(@EntityBy focuser: Focuser): Focuser { + fun focuser(@EntityParam focuser: Focuser): Focuser { return focuser } @PutMapping("{focuser}/connect") - fun connect(@EntityBy focuser: Focuser) { + fun connect(@EntityParam focuser: Focuser) { focuserService.connect(focuser) } @PutMapping("{focuser}/disconnect") - fun disconnect(@EntityBy focuser: Focuser) { + fun disconnect(@EntityParam focuser: Focuser) { focuserService.disconnect(focuser) } @PutMapping("{focuser}/move-in") fun moveIn( - @EntityBy focuser: Focuser, + @EntityParam focuser: Focuser, @RequestParam @Valid @PositiveOrZero steps: Int, ) { focuserService.moveIn(focuser, steps) @@ -44,7 +44,7 @@ class FocuserController( @PutMapping("{focuser}/move-out") fun moveOut( - @EntityBy focuser: Focuser, + @EntityParam focuser: Focuser, @RequestParam @Valid @PositiveOrZero steps: Int, ) { focuserService.moveOut(focuser, steps) @@ -52,20 +52,20 @@ class FocuserController( @PutMapping("{focuser}/move-to") fun moveTo( - @EntityBy focuser: Focuser, + @EntityParam focuser: Focuser, @RequestParam @Valid @PositiveOrZero steps: Int, ) { focuserService.moveTo(focuser, steps) } @PutMapping("{focuser}/abort") - fun abort(@EntityBy focuser: Focuser) { + fun abort(@EntityParam focuser: Focuser) { focuserService.abort(focuser) } @PutMapping("{focuser}/sync") fun sync( - @EntityBy focuser: Focuser, + @EntityParam focuser: Focuser, @RequestParam @Valid @PositiveOrZero steps: Int, ) { focuserService.sync(focuser, steps) 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/GuideOutputController.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputController.kt index ec75d6c4c..b77ba1597 100644 --- a/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputController.kt +++ b/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputController.kt @@ -1,9 +1,11 @@ package nebulosa.api.guiding -import nebulosa.api.beans.annotations.EntityBy +import nebulosa.api.beans.annotations.EntityParam import nebulosa.api.connection.ConnectionService import nebulosa.guiding.GuideDirection import nebulosa.indi.device.guide.GuideOutput +import org.hibernate.validator.constraints.time.DurationMax +import org.hibernate.validator.constraints.time.DurationMin import org.springframework.web.bind.annotation.* import java.time.Duration @@ -20,25 +22,26 @@ class GuideOutputController( } @GetMapping("{guideOutput}") - fun guideOutput(@EntityBy guideOutput: GuideOutput): GuideOutput { + fun guideOutput(@EntityParam guideOutput: GuideOutput): GuideOutput { return guideOutput } @PutMapping("{guideOutput}/connect") - fun connect(@EntityBy guideOutput: GuideOutput) { + fun connect(@EntityParam guideOutput: GuideOutput) { guideOutputService.connect(guideOutput) } @PutMapping("{guideOutput}/disconnect") - fun disconnect(@EntityBy guideOutput: GuideOutput) { + fun disconnect(@EntityParam guideOutput: GuideOutput) { guideOutputService.disconnect(guideOutput) } @PutMapping("{guideOutput}/pulse") fun pulse( - @EntityBy guideOutput: GuideOutput, - @RequestParam direction: GuideDirection, @RequestParam duration: Long, + @EntityParam guideOutput: GuideOutput, + @RequestParam direction: GuideDirection, + @RequestParam @DurationMin(nanos = 0L) @DurationMax(seconds = 60L) duration: Duration, ) { - guideOutputService.pulse(guideOutput, direction, Duration.ofNanos(duration * 1000L)) + guideOutputService.pulse(guideOutput, direction, duration) } } 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 78bd71846..eee542c18 100644 --- a/api/src/main/kotlin/nebulosa/api/guiding/GuidingService.kt +++ b/api/src/main/kotlin/nebulosa/api/guiding/GuidingService.kt @@ -1,9 +1,8 @@ package nebulosa.api.guiding -import jakarta.annotation.PostConstruct 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 @@ -33,17 +32,13 @@ class GuidingService( val settleTimeout get() = guider.settleTimeout - @PostConstruct - private fun initialize() { - settle(preferenceService.getJSON("GUIDING.SETTLE") ?: SettleInfo.EMPTY) - } - @Synchronized fun connect(host: String, port: Int) { check(!phd2Client.isOpen) phd2Client.open(host, port) guider.registerGuiderListener(this) + settle(preferenceService.getJSON("GUIDING.SETTLE") ?: SettleInfo.EMPTY) messageService.sendMessage(GuiderMessageEvent(GUIDER_CONNECTED)) } 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/image/ImageController.kt b/api/src/main/kotlin/nebulosa/api/image/ImageController.kt index 793e4117d..d9aebd13a 100644 --- a/api/src/main/kotlin/nebulosa/api/image/ImageController.kt +++ b/api/src/main/kotlin/nebulosa/api/image/ImageController.kt @@ -1,7 +1,7 @@ package nebulosa.api.image import jakarta.servlet.http.HttpServletResponse -import nebulosa.api.beans.annotations.EntityBy +import nebulosa.api.beans.annotations.EntityParam import nebulosa.imaging.ImageChannel import nebulosa.imaging.algorithms.transformation.ProtectionMethod import nebulosa.indi.device.camera.Camera @@ -18,7 +18,7 @@ class ImageController( @GetMapping fun openImage( @RequestParam path: Path, - @EntityBy(required = false) camera: Camera?, + @EntityParam(required = false) camera: Camera?, @RequestParam(required = false, defaultValue = "true") debayer: Boolean, @RequestParam(required = false, defaultValue = "false") calibrate: Boolean, @RequestParam(required = false, defaultValue = "false") autoStretch: Boolean, diff --git a/api/src/main/kotlin/nebulosa/api/image/ImageService.kt b/api/src/main/kotlin/nebulosa/api/image/ImageService.kt index c839caab1..8c9ece745 100644 --- a/api/src/main/kotlin/nebulosa/api/image/ImageService.kt +++ b/api/src/main/kotlin/nebulosa/api/image/ImageService.kt @@ -22,12 +22,12 @@ import nebulosa.watney.star.detection.WatneyStarDetector import nebulosa.wcs.WCSException import nebulosa.wcs.WCSTransform import org.springframework.http.HttpStatus +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor import org.springframework.stereotype.Service import org.springframework.web.server.ResponseStatusException import java.nio.file.Path import java.util.* import java.util.concurrent.CompletableFuture -import java.util.concurrent.ExecutorService import javax.imageio.ImageIO import kotlin.io.path.extension import kotlin.io.path.inputStream @@ -41,7 +41,7 @@ class ImageService( private val smallBodyDatabaseService: SmallBodyDatabaseService, private val simbadService: SimbadService, private val imageBucket: ImageBucket, - private val systemExecutorService: ExecutorService, + private val threadPoolTaskExecutor: ThreadPoolTaskExecutor, ) { @Synchronized @@ -141,9 +141,9 @@ class ImageService( val dateTime = image.header.observationDate if (minorPlanets && dateTime != null) { - CompletableFuture.runAsync({ - val latitude = image.header.latitude.let { if (it.isFinite()) it else 0.0 } - val longitude = image.header.longitude.let { if (it.isFinite()) it else 0.0 } + threadPoolTaskExecutor.submitCompletable { + val latitude = image.header.latitude ?: 0.0 + val longitude = image.header.longitude ?: 0.0 LOG.info( "finding minor planet annotations. dateTime={}, latitude={}, longitude={}, calibration={}", @@ -154,7 +154,7 @@ class ImageService( dateTime, latitude, longitude, 0.0, calibration.rightAscension, calibration.declination, calibration.radius, minorPlanetMagLimit, - ).execute().body() ?: return@runAsync + ).execute().body() ?: return@submitCompletable val radiusInSeconds = calibration.radius.toArcsec var count = 0 @@ -174,13 +174,14 @@ class ImageService( } LOG.info("Found {} minor planets", count) - }, systemExecutorService).whenComplete { _, e -> e?.printStackTrace() }.also(tasks::add) + }.whenComplete { _, e -> e?.printStackTrace() } + .also(tasks::add) } // val barycentric = VSOP87E.EARTH.at(UTC(TimeYMDHMS(dateTime))) if (stars || dsos) { - CompletableFuture.runAsync({ + threadPoolTaskExecutor.submitCompletable { LOG.info("finding star annotations. dateTime={}, calibration={}", dateTime, calibration) val types = ArrayList(4) @@ -227,7 +228,8 @@ class ImageService( } LOG.info("Found {} stars/DSOs", count) - }, systemExecutorService).whenComplete { _, e -> e?.printStackTrace() }.also(tasks::add) + }.whenComplete { _, e -> e?.printStackTrace() } + .also(tasks::add) } CompletableFuture.allOf(*tasks.toTypedArray()).join() diff --git a/api/src/main/kotlin/nebulosa/api/indi/INDIController.kt b/api/src/main/kotlin/nebulosa/api/indi/INDIController.kt index 04670baff..49a973654 100644 --- a/api/src/main/kotlin/nebulosa/api/indi/INDIController.kt +++ b/api/src/main/kotlin/nebulosa/api/indi/INDIController.kt @@ -1,36 +1,50 @@ package nebulosa.api.indi import jakarta.validation.Valid -import jakarta.validation.constraints.NotBlank -import nebulosa.api.connection.ConnectionService +import nebulosa.api.beans.annotations.EntityParam +import nebulosa.indi.device.Device import nebulosa.indi.device.PropertyVector import org.springframework.web.bind.annotation.* @RestController +@RequestMapping("indi") class INDIController( - private val connectionService: ConnectionService, private val indiService: INDIService, + private val indiEventHandler: INDIEventHandler, ) { - @GetMapping("indiProperties") - fun properties(@RequestParam @Valid @NotBlank name: String): Collection> { - val device = requireNotNull(connectionService.device(name)) + @GetMapping("{device}/properties") + fun properties(@EntityParam device: Device): Collection> { return indiService.properties(device) } - @PostMapping("sendIndiProperty") + @PutMapping("{device}/send") fun sendProperty( - @RequestParam @Valid @NotBlank name: String, + @EntityParam device: Device, @RequestBody @Valid body: INDISendProperty, ) { - val device = requireNotNull(connectionService.device(name)) return indiService.sendProperty(device, body) } - @GetMapping("indiLog") - fun indiLog(@RequestParam(required = false) name: String?): List { - if (name.isNullOrBlank()) return indiService.messages() - val device = connectionService.device(name) ?: return emptyList() + @GetMapping("{device}/log") + fun log(@EntityParam device: Device): List { return device.messages } + + @GetMapping("log") + fun log(): List { + return indiService.messages() + } + + @Synchronized + @PutMapping("listener/{device}/start") + fun startListening(device: Device) { + indiEventHandler.canSendEvents.add(device) + } + + @Synchronized + @PutMapping("listener/{device}/stop") + fun stopListening(device: Device) { + indiEventHandler.canSendEvents.remove(device) + } } diff --git a/api/src/main/kotlin/nebulosa/api/indi/INDIEventHandler.kt b/api/src/main/kotlin/nebulosa/api/indi/INDIEventHandler.kt index 60c285424..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 @@ -14,8 +14,7 @@ class INDIEventHandler( private val messageService: MessageService, ) : LinkedList() { - var canSendEvents = false - internal set + val canSendEvents = HashSet() @Subscribe(threadMode = ThreadMode.ASYNC) fun onDeviceEvent(event: DeviceEvent<*>) { @@ -33,19 +32,19 @@ class INDIEventHandler( } fun sendINDIPropertyChanged(event: DevicePropertyEvent) { - if (canSendEvents) { + if (event.device in canSendEvents) { messageService.sendMessage(INDIMessageEvent(DEVICE_PROPERTY_CHANGED, event)) } } fun sendINDIPropertyDeleted(event: DevicePropertyEvent) { - if (canSendEvents) { + if (event.device in canSendEvents) { messageService.sendMessage(INDIMessageEvent(DEVICE_PROPERTY_DELETED, event)) } } fun sendINDIMessageReceived(event: DeviceMessageReceived) { - if (canSendEvents) { + if (event.device in canSendEvents) { messageService.sendMessage(INDIMessageEvent(DEVICE_MESSAGE_RECEIVED, event)) } } 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/locations/LocationRepository.kt b/api/src/main/kotlin/nebulosa/api/locations/LocationRepository.kt index ebffc80aa..e5050aa66 100644 --- a/api/src/main/kotlin/nebulosa/api/locations/LocationRepository.kt +++ b/api/src/main/kotlin/nebulosa/api/locations/LocationRepository.kt @@ -5,10 +5,11 @@ import org.springframework.data.jpa.repository.Modifying import org.springframework.data.jpa.repository.Query import org.springframework.stereotype.Repository import org.springframework.transaction.annotation.Isolation +import org.springframework.transaction.annotation.Propagation import org.springframework.transaction.annotation.Transactional @Repository -@Transactional(readOnly = false, isolation = Isolation.SERIALIZABLE) +@Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW, isolation = Isolation.SERIALIZABLE) interface LocationRepository : JpaRepository { fun findFirstByOrderById(): LocationEntity? diff --git a/api/src/main/kotlin/nebulosa/api/locations/LocationInitializer.kt b/api/src/main/kotlin/nebulosa/api/locations/LocationSeedTask.kt similarity index 54% rename from api/src/main/kotlin/nebulosa/api/locations/LocationInitializer.kt rename to api/src/main/kotlin/nebulosa/api/locations/LocationSeedTask.kt index 9b8ad74ef..f440692ef 100644 --- a/api/src/main/kotlin/nebulosa/api/locations/LocationInitializer.kt +++ b/api/src/main/kotlin/nebulosa/api/locations/LocationSeedTask.kt @@ -1,12 +1,13 @@ package nebulosa.api.locations -import nebulosa.api.beans.annotations.ThreadedTask +import org.springframework.scheduling.annotation.Scheduled import org.springframework.stereotype.Component +import java.util.concurrent.TimeUnit @Component -@ThreadedTask -class LocationInitializer(private val locationRepository: LocationRepository) : Runnable { +class LocationSeedTask(private val locationRepository: LocationRepository) : Runnable { + @Scheduled(initialDelay = 1L, fixedDelay = Long.MAX_VALUE, timeUnit = TimeUnit.SECONDS) override fun run() { if (locationRepository.count() <= 0) { val location = LocationEntity(1, "Null Island", 0.0, 0.0, 0.0, selected = true) 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 93% rename from api/src/main/kotlin/nebulosa/api/services/MessageService.kt rename to api/src/main/kotlin/nebulosa/api/messages/MessageService.kt index 7389be237..c5ea68a1c 100644 --- a/api/src/main/kotlin/nebulosa/api/services/MessageService.kt +++ b/api/src/main/kotlin/nebulosa/api/messages/MessageService.kt @@ -1,5 +1,6 @@ -package nebulosa.api.services +package nebulosa.api.messages +import nebulosa.log.debug import nebulosa.log.loggerFor import org.springframework.context.event.EventListener import org.springframework.messaging.simp.SimpMessageHeaderAccessor @@ -38,7 +39,7 @@ class MessageService( if (connected.get()) { simpleMessageTemplate.convertAndSend(EVENT_NAME, event) } else { - LOG.warn("queueing message. event={}", event) + LOG.debug { "queueing message. event=$event" } messageQueue.offer(event) } } diff --git a/api/src/main/kotlin/nebulosa/api/mounts/MountController.kt b/api/src/main/kotlin/nebulosa/api/mounts/MountController.kt index 7bec6fca0..6e07dad88 100644 --- a/api/src/main/kotlin/nebulosa/api/mounts/MountController.kt +++ b/api/src/main/kotlin/nebulosa/api/mounts/MountController.kt @@ -3,8 +3,8 @@ package nebulosa.api.mounts import jakarta.validation.Valid import jakarta.validation.constraints.NotBlank import jakarta.validation.constraints.PositiveOrZero -import nebulosa.api.beans.annotations.DateAndTime -import nebulosa.api.beans.annotations.EntityBy +import nebulosa.api.beans.annotations.DateAndTimeParam +import nebulosa.api.beans.annotations.EntityParam import nebulosa.api.connection.ConnectionService import nebulosa.guiding.GuideDirection import nebulosa.indi.device.mount.Mount @@ -32,23 +32,23 @@ class MountController( } @GetMapping("{mount}") - fun mount(@EntityBy mount: Mount): Mount { + fun mount(@EntityParam mount: Mount): Mount { return mount } @PutMapping("{mount}/connect") - fun connect(@EntityBy mount: Mount) { + fun connect(@EntityParam mount: Mount) { mountService.connect(mount) } @PutMapping("{mount}/disconnect") - fun disconnect(@EntityBy mount: Mount) { + fun disconnect(@EntityParam mount: Mount) { mountService.disconnect(mount) } @PutMapping("{mount}/tracking") fun tracking( - @EntityBy mount: Mount, + @EntityParam mount: Mount, @RequestParam enabled: Boolean, ) { mountService.tracking(mount, enabled) @@ -56,7 +56,7 @@ class MountController( @PutMapping("{mount}/sync") fun sync( - @EntityBy mount: Mount, + @EntityParam mount: Mount, @RequestParam @Valid @NotBlank rightAscension: String, @RequestParam @Valid @NotBlank declination: String, @RequestParam(required = false, defaultValue = "false") j2000: Boolean, @@ -66,7 +66,7 @@ class MountController( @PutMapping("{mount}/slew") fun slew( - @EntityBy mount: Mount, + @EntityParam mount: Mount, @RequestParam @Valid @NotBlank rightAscension: String, @RequestParam @Valid @NotBlank declination: String, @RequestParam(required = false, defaultValue = "false") j2000: Boolean, @@ -76,7 +76,7 @@ class MountController( @PutMapping("{mount}/goto") fun goTo( - @EntityBy mount: Mount, + @EntityParam mount: Mount, @RequestParam @Valid @NotBlank rightAscension: String, @RequestParam @Valid @NotBlank declination: String, @RequestParam(required = false, defaultValue = "false") j2000: Boolean, @@ -85,18 +85,18 @@ class MountController( } @PutMapping("{mount}/home") - fun home(@EntityBy mount: Mount) { + fun home(@EntityParam mount: Mount) { mountService.home(mount) } @PutMapping("{mount}/abort") - fun abort(@EntityBy mount: Mount) { + fun abort(@EntityParam mount: Mount) { mountService.abort(mount) } @PutMapping("{mount}/track-mode") fun trackMode( - @EntityBy mount: Mount, + @EntityParam mount: Mount, @RequestParam mode: TrackMode, ) { mountService.trackMode(mount, mode) @@ -104,7 +104,7 @@ class MountController( @PutMapping("{mount}/slew-rate") fun slewRate( - @EntityBy mount: Mount, + @EntityParam mount: Mount, @RequestParam @Valid @NotBlank rate: String, ) { mountService.slewRate(mount, mount.slewRates.first { it.name == rate }) @@ -112,7 +112,7 @@ class MountController( @PutMapping("{mount}/move") fun move( - @EntityBy mount: Mount, + @EntityParam mount: Mount, @RequestParam direction: GuideDirection, @RequestParam enabled: Boolean, ) { @@ -120,18 +120,18 @@ class MountController( } @PutMapping("{mount}/park") - fun park(@EntityBy mount: Mount) { + fun park(@EntityParam mount: Mount) { mountService.park(mount) } @PutMapping("{mount}/unpark") - fun unpark(@EntityBy mount: Mount) { + fun unpark(@EntityParam mount: Mount) { mountService.unpark(mount) } @PutMapping("{mount}/coordinates") fun coordinates( - @EntityBy mount: Mount, + @EntityParam mount: Mount, @RequestParam @Valid @NotBlank longitude: String, @RequestParam @Valid @NotBlank latitude: String, @RequestParam(required = false, defaultValue = "0.0") elevation: Double, @@ -141,36 +141,36 @@ class MountController( @PutMapping("{mount}/datetime") fun dateTime( - @EntityBy mount: Mount, - @DateAndTime dateTime: LocalDateTime, + @EntityParam mount: Mount, + @DateAndTimeParam dateTime: LocalDateTime, @RequestParam @Valid @Range(min = -720, max = 720) offsetInMinutes: Int, ) { mountService.dateTime(mount, OffsetDateTime.of(dateTime, ZoneOffset.ofTotalSeconds(offsetInMinutes * 60))) } @GetMapping("{mount}/location/zenith") - fun zenithLocation(@EntityBy mount: Mount): ComputedLocation { + fun zenithLocation(@EntityParam mount: Mount): ComputedLocation { return mountService.computeZenithLocation(mount) } @GetMapping("{mount}/location/celestial-pole/north") - fun northCelestialPoleLocation(@EntityBy mount: Mount): ComputedLocation { + fun northCelestialPoleLocation(@EntityParam mount: Mount): ComputedLocation { return mountService.computeNorthCelestialPoleLocation(mount) } @GetMapping("{mount}/location/celestial-pole/south") - fun southCelestialPoleLocation(@EntityBy mount: Mount): ComputedLocation { + fun southCelestialPoleLocation(@EntityParam mount: Mount): ComputedLocation { return mountService.computeSouthCelestialPoleLocation(mount) } @GetMapping("{mount}/location/galactic-center") - fun galacticCenterLocation(@EntityBy mount: Mount): ComputedLocation { + fun galacticCenterLocation(@EntityParam mount: Mount): ComputedLocation { return mountService.computeGalacticCenterLocation(mount) } @GetMapping("{mount}/location") fun location( - @EntityBy mount: Mount, + @EntityParam mount: Mount, @RequestParam rightAscension: String, @RequestParam declination: String, @RequestParam(required = false, defaultValue = "false") j2000: Boolean, @@ -186,7 +186,7 @@ class MountController( @PutMapping("{mount}/point-here") fun pointMountHere( - @EntityBy mount: Mount, + @EntityParam mount: Mount, @RequestParam path: Path, @RequestParam @Valid @PositiveOrZero x: Double, @RequestParam @Valid @PositiveOrZero y: Double, 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/preferences/PreferenceService.kt b/api/src/main/kotlin/nebulosa/api/preferences/PreferenceService.kt index 418eb4965..ed6422a4b 100644 --- a/api/src/main/kotlin/nebulosa/api/preferences/PreferenceService.kt +++ b/api/src/main/kotlin/nebulosa/api/preferences/PreferenceService.kt @@ -54,18 +54,4 @@ class PreferenceService( @Synchronized fun delete(key: String) = preferenceRepository.deleteById(key) - - final inline var skyAtlasVersion - get() = getText(SKY_ATLAS_VERSION) - set(value) = putText(SKY_ATLAS_VERSION, value) - - final inline var satellitesUpdatedAt - get() = getLong(SATELLITES_UPDATED_AT) ?: 0L - set(value) = putLong(SATELLITES_UPDATED_AT, value) - - companion object { - - const val SKY_ATLAS_VERSION = "SKY_ATLAS_VERSION" - const val SATELLITES_UPDATED_AT = "SATELLITES_UPDATED_AT" - } } 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 549a6d8fd..000000000 --- a/api/src/main/kotlin/nebulosa/api/sequencer/SequenceJobFactory.kt +++ /dev/null @@ -1,71 +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.job.builder.JobBuilder -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 - -@Configuration -class SequenceJobFactory( - private val jobRepository: JobRepository, - private val sequenceFlowStepFactory: SequenceFlowStepFactory, - private val sequenceStepFactory: SequenceStepFactory, - private val sequenceTaskletFactory: SequenceTaskletFactory, - private val jobIncrementer: Incrementer, -) { - - @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() - } -} 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 3d35c60a3..000000000 --- a/api/src/main/kotlin/nebulosa/api/sequencer/tasklets/delay/DelayTasklet.kt +++ /dev/null @@ -1,55 +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) { - aborted.set(false) - - 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/solver/PlateSolverController.kt b/api/src/main/kotlin/nebulosa/api/solver/PlateSolverController.kt index e8dc46de1..1b42e9d83 100644 --- a/api/src/main/kotlin/nebulosa/api/solver/PlateSolverController.kt +++ b/api/src/main/kotlin/nebulosa/api/solver/PlateSolverController.kt @@ -1,8 +1,8 @@ package nebulosa.api.solver import jakarta.validation.Valid -import nebulosa.math.deg -import nebulosa.math.hours +import nebulosa.api.beans.annotations.AngleParam +import nebulosa.math.Angle import org.springframework.web.bind.annotation.* import java.nio.file.Path @@ -16,10 +16,10 @@ class PlateSolverController( fun solveImage( @RequestParam path: Path, @RequestParam(required = false, defaultValue = "true") blind: Boolean, - @RequestParam(required = false, defaultValue = "0.0") centerRA: String, - @RequestParam(required = false, defaultValue = "0.0") centerDEC: String, - @RequestParam(required = false, defaultValue = "8.0") radius: String, - ) = plateSolverService.solveImage(path, centerRA.hours, centerDEC.deg, if (blind) 0.0 else radius.deg) + @AngleParam(required = false, isHours = true, defaultValue = "0.0") centerRA: Angle, + @AngleParam(required = false, defaultValue = "0.0") centerDEC: Angle, + @AngleParam(required = false, defaultValue = "4.0") radius: Angle, + ) = plateSolverService.solveImage(path, centerRA, centerDEC, if (blind) 0.0 else radius) @PutMapping("settings") fun settings(@RequestBody @Valid body: PlateSolverOptions) { diff --git a/api/src/main/kotlin/nebulosa/api/wheels/WheelController.kt b/api/src/main/kotlin/nebulosa/api/wheels/WheelController.kt index 84980b45f..cb1836102 100644 --- a/api/src/main/kotlin/nebulosa/api/wheels/WheelController.kt +++ b/api/src/main/kotlin/nebulosa/api/wheels/WheelController.kt @@ -2,7 +2,7 @@ package nebulosa.api.wheels import jakarta.validation.Valid import jakarta.validation.constraints.PositiveOrZero -import nebulosa.api.beans.annotations.EntityBy +import nebulosa.api.beans.annotations.EntityParam import nebulosa.api.connection.ConnectionService import nebulosa.indi.device.filterwheel.FilterWheel import org.springframework.web.bind.annotation.* @@ -20,23 +20,23 @@ class WheelController( } @GetMapping("{wheel}") - fun wheel(@EntityBy wheel: FilterWheel): FilterWheel { + fun wheel(@EntityParam wheel: FilterWheel): FilterWheel { return wheel } @PutMapping("{wheel}/connect") - fun connect(@EntityBy wheel: FilterWheel) { + fun connect(@EntityParam wheel: FilterWheel) { wheelService.connect(wheel) } @PutMapping("{wheel}/disconnect") - fun disconnect(@EntityBy wheel: FilterWheel) { + fun disconnect(@EntityParam wheel: FilterWheel) { wheelService.disconnect(wheel) } @PutMapping("{wheel}/move-to") fun moveTo( - @EntityBy wheel: FilterWheel, + @EntityParam wheel: FilterWheel, @RequestParam @Valid @PositiveOrZero position: Int, ) { wheelService.moveTo(wheel, position) @@ -44,7 +44,7 @@ class WheelController( @PutMapping("{wheel}/sync") fun sync( - @EntityBy wheel: FilterWheel, + @EntityParam wheel: FilterWheel, @RequestParam @Valid @PositiveOrZero names: String, ) { wheelService.sync(wheel, names.split(",")) 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 efe1ceb95..453e17745 100644 --- a/api/src/main/resources/application.yml +++ b/api/src/main/resources/application.yml @@ -12,10 +12,7 @@ spring: lazy-initialization: true datasource: - url: jdbc:sqlite:file:${DATA_PATH}/nebulosa.db - username: - password: - driverClassName: org.sqlite.JDBC + url: 'jdbc:sqlite:file:${DATA_PATH}/nebulosa.db' jpa: database-platform: org.hibernate.community.dialect.SQLiteDialect @@ -28,9 +25,3 @@ spring: baseline-on-migrate: true baseline-version: 0 table: migrations - - batch: - job: - enabled: false - jdbc: - initialize-schema: always diff --git a/api/src/main/resources/db/migration/beforeMigrate.sql b/api/src/main/resources/db/migration/beforeMigrate.sql deleted file mode 100644 index a11aaf1f6..000000000 --- a/api/src/main/resources/db/migration/beforeMigrate.sql +++ /dev/null @@ -1,8 +0,0 @@ -DROP TABLE IF EXISTS BATCH_JOB_EXECUTION_CONTEXT; -DROP TABLE IF EXISTS BATCH_JOB_EXECUTION_PARAMS; -DROP TABLE IF EXISTS BATCH_JOB_EXECUTION; -DROP TABLE IF EXISTS BATCH_JOB_EXECUTION_SEQ; -DROP TABLE IF EXISTS BATCH_JOB_INSTANCE; -DROP TABLE IF EXISTS BATCH_STEP_EXECUTION_CONTEXT; -DROP TABLE IF EXISTS BATCH_STEP_EXECUTION; -DROP TABLE IF EXISTS BATCH_STEP_EXECUTION_SEQ; diff --git a/build.gradle.kts b/build.gradle.kts index e0d5c7a60..992473caf 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,8 +5,8 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile buildscript { dependencies { - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.21") - classpath("org.jetbrains.kotlin:kotlin-allopen:1.9.21") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.0.0-Beta1") + classpath("org.jetbrains.kotlin:kotlin-allopen:2.0.0-Beta1") classpath("com.adarshr:gradle-test-logger-plugin:4.0.0") } diff --git a/desktop/.vscode/settings.json b/desktop/.vscode/settings.json index cc6d56677..224f2841b 100644 --- a/desktop/.vscode/settings.json +++ b/desktop/.vscode/settings.json @@ -5,7 +5,7 @@ "typescript.preferences.quoteStyle": "single", "editor.formatOnSave": true, "editor.codeActionsOnSave": { - "source.organizeImports": true + "source.organizeImports": "explicit" }, "html.format.wrapAttributes": "preserve-aligned", "html.format.wrapLineLength": 150, diff --git a/desktop/app/main.ts b/desktop/app/main.ts index d6ae79640..42c727e7d 100644 --- a/desktop/app/main.ts +++ b/desktop/app/main.ts @@ -2,7 +2,7 @@ import { Client } from '@stomp/stompjs' import { BrowserWindow, Menu, Notification, app, dialog, ipcMain, screen, shell } from 'electron' import { ChildProcessWithoutNullStreams, spawn } from 'node:child_process' import * as path from 'path' -import { InternalEventType, MessageEvent, NotificationEvent, OpenDirectory, OpenWindow } from './types' +import { InternalEventType, MessageEvent, NotificationEvent, OpenDirectory, OpenFile, OpenWindow } from '../src/shared/types' import { WebSocket } from 'ws' Object.assign(global, { WebSocket }) @@ -280,12 +280,13 @@ try { }) }) - ipcMain.handle('OPEN_FITS', async (event) => { + ipcMain.handle('OPEN_FILE', async (event, data?: OpenFile) => { const ownerWindow = findWindowById(event.sender.id) const value = await dialog.showOpenDialog(ownerWindow!, { - filters: [{ name: 'FITS files', extensions: ['fits', 'fit'] }], + filters: data?.filters, properties: ['openFile'], + defaultPath: data?.defaultPath || undefined, }) return !value.canceled && value.filePaths[0] @@ -308,7 +309,7 @@ try { const ownerWindow = findWindowById(event.sender.id) const value = await dialog.showOpenDialog(ownerWindow!, { properties: ['openDirectory'], - defaultPath: data?.defaultPath, + defaultPath: data?.defaultPath || undefined, }) return !value.canceled && value.filePaths[0] diff --git a/desktop/copyFiles.js b/desktop/copyFiles.js deleted file mode 100644 index 34480147a..000000000 --- a/desktop/copyFiles.js +++ /dev/null @@ -1,6 +0,0 @@ -const fs = require('fs') -const { copyFiles } = require('./package.json') - -for (const file of copyFiles) { - fs.copyFile(file.from, file.to, () => null) -} diff --git a/desktop/package.json b/desktop/package.json index f194737ac..886447c65 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -13,10 +13,9 @@ "scripts": { "postinstall": "electron-builder install-app-deps", "ng": "ng", - "copy:files": "node copyFiles.js", - "start": "npm run copy:files && npm-run-all -p electron:serve ng:serve", + "start": "npm-run-all -p electron:serve ng:serve", "ng:serve": "ng serve -c web", - "build": "npm run copy:files && npm run electron:serve-tsc && ng build --base-href ./", + "build": "npm run electron:serve-tsc && ng build --base-href ./", "build:dev": "npm run build -- -c dev", "build:prod": "npm run build -- -c production", "web:build": "npm run build -- -c web-production", @@ -82,11 +81,5 @@ }, "browserslist": [ "chrome 114" - ], - "copyFiles": [ - { - "from": "src/shared/types.ts", - "to": "app/types.ts" - } ] } 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/calibration/calibration.component.ts b/desktop/src/app/calibration/calibration.component.ts index b1da8bbf2..9d80947ac 100644 --- a/desktop/src/app/calibration/calibration.component.ts +++ b/desktop/src/app/calibration/calibration.component.ts @@ -1,8 +1,10 @@ import { AfterViewInit, Component, HostListener, OnDestroy } from '@angular/core' import { ActivatedRoute } from '@angular/router' +import path from 'path' import { CheckboxChangeEvent } from 'primeng/checkbox' import { ApiService } from '../../shared/services/api.service' import { ElectronService } from '../../shared/services/electron.service' +import { LocalStorageService } from '../../shared/services/local-storage.service' import { CalibrationFrame, CalibrationFrameGroup, Camera } from '../../shared/types' import { AppComponent } from '../app.component' @@ -28,6 +30,7 @@ export class CalibrationComponent implements AfterViewInit, OnDestroy { private api: ApiService, electron: ElectronService, private route: ActivatedRoute, + private storage: LocalStorageService, ) { app.title = 'Calibration' @@ -35,10 +38,12 @@ export class CalibrationComponent implements AfterViewInit, OnDestroy { icon: 'mdi mdi-image-plus', tooltip: 'Add file', command: async () => { - const path = await electron.openFITS() + const defaultPath = this.storage.get('calibration.directory', '') + const fitsPath = await electron.openFITS({ defaultPath }) - if (path) { - this.upload(path) + if (fitsPath) { + this.storage.set('calibration.directory', path.dirname(fitsPath)) + this.upload(fitsPath) } }, }) @@ -47,10 +52,12 @@ export class CalibrationComponent implements AfterViewInit, OnDestroy { icon: 'mdi mdi-folder-plus', tooltip: 'Add folder', command: async () => { - const path = await electron.openDirectory() + const defaultPath = this.storage.get('calibration.directory', '') + const dirPath = await electron.openDirectory({ defaultPath }) - if (path) { - this.upload(path) + if (dirPath) { + this.storage.set('calibration.directory', dirPath) + this.upload(dirPath) } }, }) 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 1632366d9..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, } @@ -229,8 +228,6 @@ export class CameraComponent implements AfterContentInit, OnDestroy { ) { app.title = 'Camera' - api.startListening('CAMERA') - electron.on('CAMERA_UPDATED', event => { if (event.device.name === this.camera?.name) { ngZone.run(() => { @@ -240,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 }) } }) @@ -257,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.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 - 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.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 + } }) } }) @@ -326,7 +283,6 @@ export class CameraComponent implements AfterContentInit, OnDestroy { @HostListener('window:unload') ngOnDestroy() { - this.api.stopListening('CAMERA') this.abortCapture() } @@ -339,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 = '' @@ -425,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/guider/guider.component.ts b/desktop/src/app/guider/guider.component.ts index e0d3b88a6..0f00c93a2 100644 --- a/desktop/src/app/guider/guider.component.ts +++ b/desktop/src/app/guider/guider.component.ts @@ -220,8 +220,6 @@ export class GuiderComponent implements AfterViewInit, OnDestroy { ) { title.setTitle('Guider') - api.startListening('GUIDING') - electron.on('GUIDE_OUTPUT_UPDATED', event => { if (event.device.name === this.guideOutput?.name) { ngZone.run(() => { @@ -304,9 +302,7 @@ export class GuiderComponent implements AfterViewInit, OnDestroy { } @HostListener('window:unload') - ngOnDestroy() { - this.api.stopListening('GUIDING') - } + ngOnDestroy() { } private processGuiderStatus(event: Guider) { this.connected = event.connected diff --git a/desktop/src/app/home/home.component.ts b/desktop/src/app/home/home.component.ts index 41f75c781..67f3d3aaa 100644 --- a/desktop/src/app/home/home.component.ts +++ b/desktop/src/app/home/home.component.ts @@ -1,4 +1,5 @@ import { AfterContentInit, Component, HostListener, NgZone, OnDestroy, ViewChild } from '@angular/core' +import path from 'path' import { MenuItem, MessageService } from 'primeng/api' import { DeviceMenuComponent } from '../../shared/components/devicemenu/devicemenu.component' import { DialogMenuComponent } from '../../shared/components/dialogmenu/dialogmenu.component' @@ -7,6 +8,7 @@ import { BrowserWindowService } from '../../shared/services/browser-window.servi import { ElectronService } from '../../shared/services/electron.service' import { LocalStorageService } from '../../shared/services/local-storage.service' import { Camera, Device, FilterWheel, Focuser, HomeWindowType, Mount } from '../../shared/types' +import { compareDevice } from '../../shared/utils/comparators' import { AppComponent } from '../app.component' type MappedDevice = { @@ -227,7 +229,7 @@ export class HomeComponent implements AfterContentInit, OnDestroy { if (devices.length === 0) return if (devices.length === 1) return this.openDeviceWindow(type, devices[0] as any) - for (const device of devices) { + for (const device of [...devices].sort(compareDevice)) { this.deviceModel.push({ icon: 'mdi mdi-connection', label: device.name, @@ -259,10 +261,12 @@ export class HomeComponent implements AfterContentInit, OnDestroy { private async openImage(force: boolean = false) { if (force || this.cameras.length === 0) { - const path = await this.electron.openFITS() + const defaultPath = this.storage.get('home.image.directory', '') + const fitsPath = await this.electron.openFITS({ defaultPath }) - if (path) { - this.browserWindow.openImage({ path, source: 'PATH' }) + if (fitsPath) { + this.storage.set('home.image.directory', path.dirname(fitsPath)) + this.browserWindow.openImage({ path: fitsPath, source: 'PATH' }) } } else { const camera = await this.imageMenu.show(this.cameras) @@ -313,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/indi/indi.component.html b/desktop/src/app/indi/indi.component.html index 8d27baf9c..0bfb1d811 100644 --- a/desktop/src/app/indi/indi.component.html +++ b/desktop/src/app/indi/indi.component.html @@ -2,7 +2,7 @@
- @@ -18,7 +18,7 @@
- +
diff --git a/desktop/src/app/indi/indi.component.scss b/desktop/src/app/indi/indi.component.scss index 232c4e990..7746ea400 100644 --- a/desktop/src/app/indi/indi.component.scss +++ b/desktop/src/app/indi/indi.component.scss @@ -1,8 +1,16 @@ +:host { + ::ng-deep { + .p-listbox-list-wrapper { + max-height: calc(100vh - 175px) !important; + } + } +} + .properties { - height: calc(100vh - 80px); + height: calc(100vh - 100px); overflow-y: auto; } .properties::-webkit-scrollbar { display: none; -} +} \ No newline at end of file diff --git a/desktop/src/app/indi/indi.component.ts b/desktop/src/app/indi/indi.component.ts index 057e361b1..cd38566f0 100644 --- a/desktop/src/app/indi/indi.component.ts +++ b/desktop/src/app/indi/indi.component.ts @@ -4,6 +4,7 @@ import { MenuItem } from 'primeng/api' import { ApiService } from '../../shared/services/api.service' import { ElectronService } from '../../shared/services/electron.service' import { Device, INDIProperty, INDIPropertyItem, INDISendProperty } from '../../shared/types' +import { compareDevice, compareText } from '../../shared/utils/comparators' import { AppComponent } from '../app.component' @Component({ @@ -31,8 +32,6 @@ export class INDIComponent implements AfterViewInit, OnDestroy { ) { app.title = 'INDI' - this.api.startListening('INDI') - electron.on('DEVICE_PROPERTY_CHANGED', event => { ngZone.run(() => { this.addOrUpdateProperty(event.property!) @@ -62,7 +61,11 @@ export class INDIComponent implements AfterViewInit, OnDestroy { async ngAfterViewInit() { this.route.queryParams.subscribe(e => { - this.device = JSON.parse(decodeURIComponent(e.data)) as Device + const device = JSON.parse(decodeURIComponent(e.data)) + + if ("name" in device && device.name) { + this.device = device + } }) this.devices = [ @@ -70,17 +73,28 @@ export class INDIComponent implements AfterViewInit, OnDestroy { ...await this.api.mounts(), ...await this.api.focusers(), ...await this.api.wheels(), - ] + ].sort(compareDevice) + + this.device = this.devices[0] } @HostListener('window:unload') ngOnDestroy() { - this.api.stopListening('INDI') + if (this.device) { + this.api.indiStopListening(this.device) + } } - async deviceChanged() { + async deviceChanged(device: Device) { + if (this.device) { + this.api.indiStopListening(this.device) + } + + this.device = device + this.updateProperties() - this.messages = await this.api.indiLog(this.device!) + this.api.indiStartListening(device) + this.messages = await this.api.indiLog(device) } changeGroup(group: string) { @@ -89,7 +103,7 @@ export class INDIComponent implements AfterViewInit, OnDestroy { } send(property: INDISendProperty) { - this.api.sendIndiProperty(this.device!, property) + this.api.indiSendProperty(this.device!, property) } private updateGroups() { @@ -116,7 +130,7 @@ export class INDIComponent implements AfterViewInit, OnDestroy { if (this.groups.length === 0 || groupsChanged) { this.groups = Array.from(groups) - .sort((a, b) => a.localeCompare(b)) + .sort(compareText) .map(e => { icon: 'mdi mdi-sitemap', label: e, diff --git a/desktop/src/app/mount/mount.component.ts b/desktop/src/app/mount/mount.component.ts index 8b3f56339..fa4b049da 100644 --- a/desktop/src/app/mount/mount.component.ts +++ b/desktop/src/app/mount/mount.component.ts @@ -156,8 +156,6 @@ export class MountComponent implements AfterContentInit, OnDestroy { ) { app.title = 'Mount' - api.startListening('MOUNT') - electron.on('MOUNT_UPDATED', event => { if (event.device.name === this.mount?.name) { ngZone.run(() => { @@ -167,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()) @@ -195,8 +201,6 @@ export class MountComponent implements AfterContentInit, OnDestroy { this.computeCoordinateSubscriptions .forEach(e => e.unsubscribe()) - - this.api.stopListening('MOUNT') } async mountChanged(mount?: Mount) { @@ -326,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/api.service.ts b/desktop/src/shared/services/api.service.ts index 36710016a..ba2d0827d 100644 --- a/desktop/src/shared/services/api.service.ts +++ b/desktop/src/shared/services/api.service.ts @@ -3,9 +3,8 @@ import moment from 'moment' import { Angle, BodyPosition, CalibrationFrame, CalibrationFrameGroup, Camera, CameraStartCapture, ComputedLocation, Constellation, CoordinateInterpolation, DeepSkyObject, DetectedStar, Device, FilterWheel, Focuser, GuideDirection, GuideOutput, Guider, HipsSurvey, HistoryStep, - INDIProperty, INDISendProperty, ImageAnnotation, ImageCalibrated, - ImageChannel, ImageInfo, ListeningEventType, Location, MinorPlanet, - Mount, PlateSolverOptions, SCNRProtectionMethod, Satellite, SatelliteGroupType, + INDIProperty, INDISendProperty, ImageAnnotation, ImageCalibrated, ImageChannel, ImageInfo, + Location, MinorPlanet, Mount, PlateSolverOptions, SCNRProtectionMethod, Satellite, SatelliteGroupType, SettleInfo, SkyObjectType, SlewRate, Star, TrackMode, Twilight } from '../types' import { HttpService } from './http.service' @@ -339,24 +338,26 @@ export class ApiService { return this.http.delete(`image?${query}`) } + // INDI + indiProperties(device: Device) { - return this.http.get[]>(`indiProperties?name=${device.name}`) + return this.http.get[]>(`indi/${device.name}/properties`) } - sendIndiProperty(device: Device, property: INDISendProperty) { - return this.http.post(`sendIndiProperty?name=${device.name}`, property) + indiSendProperty(device: Device, property: INDISendProperty) { + return this.http.put(`indi/${device.name}/send`, property) } - startListening(eventType: ListeningEventType) { - return this.http.post(`startListening?eventType=${eventType}`) + indiStartListening(device: Device) { + return this.http.put(`indi/listener/${device.name}/start`) } - stopListening(eventType: ListeningEventType) { - return this.http.post(`stopListening?eventType=${eventType}`) + indiStopListening(device: Device) { + return this.http.put(`indi/listener/${device.name}/stop`) } indiLog(device: Device) { - return this.http.get(`indiLog?name=${device.name}`) + return this.http.get(`indi/${device.name}/log`) } // LOCATION diff --git a/desktop/src/shared/services/electron.service.ts b/desktop/src/shared/services/electron.service.ts index d1d3c70ed..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 + 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 @@ -98,8 +89,12 @@ export class ElectronService { this.ipcRenderer.on(channel, (_, arg) => listener(arg)) } - openFITS(): Promise { - return this.send('OPEN_FITS') + openFile(data?: OpenFile): Promise { + return this.send('OPEN_FILE', data) + } + + openFITS(data?: OpenFile): Promise { + return this.openFile({ ...data, filters: [{ name: 'FITS files', extensions: ['fits', 'fit'] }] }) } openDirectory(data?: OpenDirectory): Promise { diff --git a/desktop/src/shared/types.ts b/desktop/src/shared/types.ts index 26446afb1..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 } @@ -269,6 +244,10 @@ export interface OpenDirectory { defaultPath?: string } +export interface OpenFile extends OpenDirectory { + filters?: Electron.FileFilter[] +} + export interface GuideCaptureEvent { camera: Camera } @@ -483,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 { @@ -779,13 +749,13 @@ 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] export const INTERNAL_EVENT_TYPES = [ - 'SAVE_FITS_AS', 'OPEN_FITS', 'OPEN_WINDOW', 'OPEN_DIRECTORY', 'CLOSE_WINDOW', + 'SAVE_FITS_AS', 'OPEN_FILE', 'OPEN_WINDOW', 'OPEN_DIRECTORY', 'CLOSE_WINDOW', 'PIN_WINDOW', 'UNPIN_WINDOW', 'MINIMIZE_WINDOW', 'MAXIMIZE_WINDOW', 'WHEEL_RENAMED', 'LOCATION_CHANGED', ] as const @@ -798,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', @@ -831,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- @@ -872,8 +851,6 @@ export const SATELLITE_GROUPS = [ export type SatelliteGroupType = (typeof SATELLITE_GROUPS)[number] -export type ListeningEventType = 'INDI' | 'GUIDING' | 'CAMERA' | 'MOUNT' - export const GUIDER_TYPES = ['PHD2'] as const export type GuiderType = (typeof GUIDER_TYPES)[number] @@ -886,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/desktop/src/shared/utils/comparators.ts b/desktop/src/shared/utils/comparators.ts new file mode 100644 index 000000000..9074a23ad --- /dev/null +++ b/desktop/src/shared/utils/comparators.ts @@ -0,0 +1,9 @@ +import { Device } from '../types' + +export function compareText(a: string, b: string) { + return a.localeCompare(b) +} + +export function compareDevice(a: Device, b: Device) { + return compareText(a.name, b.name) +} \ No newline at end of file diff --git a/desktop/tsconfig.serve.json b/desktop/tsconfig.serve.json index 1f579c034..40e15a9a4 100644 --- a/desktop/tsconfig.serve.json +++ b/desktop/tsconfig.serve.json @@ -22,7 +22,7 @@ }, "files": [ "app/main.ts", - "app/types.ts" + "src/shared/types.ts" ], "exclude": [ "node_modules", diff --git a/gradle.properties b/gradle.properties index 417ed3209..4eb978215 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,5 +2,3 @@ org.gradle.caching=true org.gradle.parallel=true org.gradle.jvmargs=-XX:MaxMetaspaceSize=1024m -Xmx2048m version.code=0.1.0 -kotlin.experimental.tryK2=true -kapt.use.k2=true 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-fits/src/main/kotlin/nebulosa/fits/FitsHelper.kt b/nebulosa-fits/src/main/kotlin/nebulosa/fits/FitsHelper.kt index 16e70168d..d212bb858 100644 --- a/nebulosa-fits/src/main/kotlin/nebulosa/fits/FitsHelper.kt +++ b/nebulosa-fits/src/main/kotlin/nebulosa/fits/FitsHelper.kt @@ -55,10 +55,10 @@ inline val Header.gain get() = getDouble(NOAOExt.GAIN, 0.0) val Header.latitude - get() = (getDoubleOrNull(SBFitsExt.SITELAT) ?: getDouble("LAT-OBS", Double.NaN)).deg + get() = (getDoubleOrNull(SBFitsExt.SITELAT) ?: getDoubleOrNull("LAT-OBS"))?.deg val Header.longitude - get() = (getDoubleOrNull(SBFitsExt.SITELONG) ?: getDouble("LONG-OBS", Double.NaN)).deg + get() = (getDoubleOrNull(SBFitsExt.SITELONG) ?: getDoubleOrNull("LONG-OBS"))?.deg val Header.observationDate get() = getStringOrNull(Standard.DATE_OBS)?.let(LocalDateTime::parse) 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/DeviceProtocolHandler.kt b/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/DeviceProtocolHandler.kt index e1f18e3f1..b0b683e50 100644 --- a/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/DeviceProtocolHandler.kt +++ b/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/DeviceProtocolHandler.kt @@ -33,7 +33,6 @@ import nebulosa.indi.protocol.DefTextVector import nebulosa.indi.protocol.DelProperty import nebulosa.indi.protocol.INDIProtocol import nebulosa.indi.protocol.Message -import nebulosa.indi.protocol.io.INDIInputStream import nebulosa.indi.protocol.parser.INDIProtocolParser import nebulosa.indi.protocol.parser.INDIProtocolReader import nebulosa.log.debug @@ -55,16 +54,6 @@ abstract class DeviceProtocolHandler : MessageSender, INDIProtocolParser { private val messageQueueCounter = HashMap(2048) private val handlers = ArrayList() - override val input = object : INDIInputStream { - - override fun readINDIProtocol(): INDIProtocol { - Thread.sleep(1) - return messageReorderingQueue.take() - } - - override fun close() = Unit - } - val isRunning get() = protocolReader != null @@ -190,6 +179,20 @@ abstract class DeviceProtocolHandler : MessageSender, INDIProtocolParser { var registered = false + fun takeMessageFromReorderingQueue(device: Device) { + if (messageReorderingQueue.isNotEmpty()) { + repeat(messageReorderingQueue.size) { + val queuedMessage = messageReorderingQueue.take() + + if (queuedMessage.device == device.name) { + handleMessage(queuedMessage) + } else { + messageReorderingQueue.offer(message) + } + } + } + } + if (executable in Camera.DRIVERS) { registered = true @@ -197,6 +200,7 @@ abstract class DeviceProtocolHandler : MessageSender, INDIProtocolParser { val device = CAMERAS[executable]?.create(this, message.device) ?: CameraDevice(this, message.device) cameras[message.device] = device + takeMessageFromReorderingQueue(device) LOG.info("camera attached: {}", device.name) fireOnEventReceived(CameraAttached(device)) } @@ -209,6 +213,7 @@ abstract class DeviceProtocolHandler : MessageSender, INDIProtocolParser { val device = MOUNTS[executable]?.create(this, message.device) ?: MountDevice(this, message.device) mounts[message.device] = device + takeMessageFromReorderingQueue(device) LOG.info("mount attached: {}", device.name) fireOnEventReceived(MountAttached(device)) } @@ -220,6 +225,7 @@ abstract class DeviceProtocolHandler : MessageSender, INDIProtocolParser { if (message.device !in wheels) { val device = FilterWheelDevice(this, message.device) wheels[message.device] = device + takeMessageFromReorderingQueue(device) LOG.info("filter wheel attached: {}", device.name) fireOnEventReceived(FilterWheelAttached(device)) } @@ -231,6 +237,7 @@ abstract class DeviceProtocolHandler : MessageSender, INDIProtocolParser { if (message.device !in focusers) { val device = FocuserDevice(this, message.device) focusers[message.device] = device + takeMessageFromReorderingQueue(device) LOG.info("focuser attached: {}", device.name) fireOnEventReceived(FocuserAttached(device)) } @@ -242,6 +249,7 @@ abstract class DeviceProtocolHandler : MessageSender, INDIProtocolParser { if (message.device !in gps) { val device = GPSDevice(this, message.device) gps[message.device] = device + takeMessageFromReorderingQueue(device) LOG.info("gps attached: {}", device.name) fireOnEventReceived(GPSAttached(device)) } 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-indi-protocol/src/main/kotlin/nebulosa/indi/protocol/parser/INDIProtocolReader.kt b/nebulosa-indi-protocol/src/main/kotlin/nebulosa/indi/protocol/parser/INDIProtocolReader.kt index a26f132cf..0d9051d3e 100644 --- a/nebulosa-indi-protocol/src/main/kotlin/nebulosa/indi/protocol/parser/INDIProtocolReader.kt +++ b/nebulosa-indi-protocol/src/main/kotlin/nebulosa/indi/protocol/parser/INDIProtocolReader.kt @@ -27,7 +27,10 @@ class INDIProtocolReader( val message = input.readINDIProtocol() ?: break parser.handleMessage(message) } + + LOG.info("protocol parser finished") } catch (_: InterruptedException) { + LOG.info("protocol parser interrupted") } catch (e: Throwable) { LOG.error("protocol parser error", e) parser.close() 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/nebulosa-watney/src/main/kotlin/nebulosa/watney/plate/solving/quad/QuadDatabaseCellFileDescriptor.kt b/nebulosa-watney/src/main/kotlin/nebulosa/watney/plate/solving/quad/QuadDatabaseCellFileDescriptor.kt index a41d0fa28..3fe249eed 100644 --- a/nebulosa-watney/src/main/kotlin/nebulosa/watney/plate/solving/quad/QuadDatabaseCellFileDescriptor.kt +++ b/nebulosa-watney/src/main/kotlin/nebulosa/watney/plate/solving/quad/QuadDatabaseCellFileDescriptor.kt @@ -4,7 +4,6 @@ import nebulosa.erfa.SphericalCoordinate import nebulosa.io.ByteOrder import nebulosa.io.readFloat import nebulosa.io.readInt -import nebulosa.log.debug import nebulosa.log.loggerFor import nebulosa.math.Angle import nebulosa.math.deg diff --git a/nebulosa-watney/src/main/kotlin/nebulosa/watney/star/detection/StarPixelBin.kt b/nebulosa-watney/src/main/kotlin/nebulosa/watney/star/detection/StarPixelBin.kt index d0c1beaa1..c8bb00988 100644 --- a/nebulosa-watney/src/main/kotlin/nebulosa/watney/star/detection/StarPixelBin.kt +++ b/nebulosa-watney/src/main/kotlin/nebulosa/watney/star/detection/StarPixelBin.kt @@ -1,7 +1,6 @@ package nebulosa.watney.star.detection import kotlin.math.hypot -import kotlin.math.roundToInt /** * A class representing a "bin" of star pixels, i.e. the list of a single star's pixels diff --git a/settings.gradle.kts b/settings.gradle.kts index e25877a97..3ab35d12c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -35,6 +35,7 @@ dependencyResolutionManagement { library("sqlite", "org.xerial:sqlite-jdbc:3.44.1.0") library("flyway", "org.flywaydb:flyway-core:10.3.0") library("jna", "net.java.dev.jna:jna:5.14.0") + library("hikari", "com.zaxxer:HikariCP:5.1.0") library("kotest-assertions-core", "io.kotest:kotest-assertions-core:5.8.0") library("kotest-runner-junit5", "io.kotest:kotest-runner-junit5:5.8.0") bundle("kotest", listOf("kotest-assertions-core", "kotest-runner-junit5")) @@ -52,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")