diff --git a/api/build.gradle.kts b/api/build.gradle.kts index 2384c62a6..0dd711222 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -10,7 +10,6 @@ plugins { dependencies { implementation(project(":nebulosa-common")) - implementation(project(":nebulosa-guiding-internal")) implementation(project(":nebulosa-guiding-phd2")) implementation(project(":nebulosa-hips2fits")) implementation(project(":nebulosa-horizons")) @@ -28,7 +27,6 @@ dependencies { implementation(project(":nebulosa-wcs")) implementation(project(":nebulosa-log")) implementation(libs.csv) - implementation(libs.jackson) implementation(libs.okhttp) implementation(libs.oshi) implementation(libs.eventbus) @@ -46,7 +44,7 @@ dependencies { implementation("org.hibernate.orm:hibernate-community-dialects") implementation("org.jetbrains.kotlin:kotlin-reflect") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") - kapt("org.springframework:spring-context-indexer:6.0.12") + kapt("org.springframework:spring-context-indexer:6.0.13") testImplementation(project(":nebulosa-skycatalog-hyg")) testImplementation(project(":nebulosa-skycatalog-stellarium")) testImplementation(project(":nebulosa-test")) diff --git a/api/data/dsos.json.gz b/api/data/dsos.json.gz index b61cb5b95..14f1f6397 100644 Binary files a/api/data/dsos.json.gz and b/api/data/dsos.json.gz differ diff --git a/api/data/stars.json.gz b/api/data/stars.json.gz index e7df78a23..37148d73d 100644 Binary files a/api/data/stars.json.gz and b/api/data/stars.json.gz differ diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/PolarAlignmentController.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/PolarAlignmentController.kt new file mode 100644 index 000000000..9d79844e8 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/PolarAlignmentController.kt @@ -0,0 +1,30 @@ +package nebulosa.api.alignment.polar + +import nebulosa.api.alignment.polar.darv.DARVStart +import nebulosa.api.beans.annotations.EntityBy +import nebulosa.indi.device.camera.Camera +import nebulosa.indi.device.guide.GuideOutput +import org.springframework.web.bind.annotation.PutMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("polar-alignment") +class PolarAlignmentController( + private val polarAlignmentService: PolarAlignmentService, +) { + + @PutMapping("darv/{camera}/{guideOutput}/start") + fun darvStart( + @EntityBy camera: Camera, @EntityBy guideOutput: GuideOutput, + @RequestBody body: DARVStart, + ) { + polarAlignmentService.darvStart(camera, guideOutput, body) + } + + @PutMapping("darv/{camera}/{guideOutput}/stop") + fun darvStop(@EntityBy camera: Camera, @EntityBy 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 new file mode 100644 index 000000000..7265a3d76 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/PolarAlignmentService.kt @@ -0,0 +1,23 @@ +package nebulosa.api.alignment.polar + +import nebulosa.api.alignment.polar.darv.DARVPolarAlignmentExecutor +import nebulosa.api.alignment.polar.darv.DARVStart +import nebulosa.indi.device.camera.Camera +import nebulosa.indi.device.guide.GuideOutput +import org.springframework.stereotype.Service + +@Service +class PolarAlignmentService( + private val darvPolarAlignmentExecutor: DARVPolarAlignmentExecutor, +) { + + fun darvStart(camera: Camera, guideOutput: GuideOutput, darvStart: DARVStart) { + check(camera.connected) { "camera not connected" } + check(guideOutput.connected) { "guide output not connected" } + darvPolarAlignmentExecutor.execute(darvStart.copy(camera = camera, guideOutput = guideOutput)) + } + + fun darvStop(camera: Camera, guideOutput: GuideOutput) { + darvPolarAlignmentExecutor.stop(camera, guideOutput) + } +} 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 new file mode 100644 index 000000000..6c5e48f42 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentEvent.kt @@ -0,0 +1,11 @@ +package nebulosa.api.alignment.polar.darv + +import nebulosa.indi.device.camera.Camera +import nebulosa.indi.device.guide.GuideOutput + +sealed interface DARVPolarAlignmentEvent { + + val camera: Camera + + val guideOutput: GuideOutput +} 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 new file mode 100644 index 000000000..62433be0d --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentExecutor.kt @@ -0,0 +1,172 @@ +package nebulosa.api.alignment.polar.darv + +import io.reactivex.rxjava3.functions.Consumer +import nebulosa.api.cameras.CameraCaptureEvent +import nebulosa.api.cameras.CameraStartCaptureRequest +import nebulosa.api.guiding.* +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.* +import kotlin.time.Duration.Companion.seconds + +/** + * @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, + exposureInMicroseconds = (request.exposureInSeconds + request.initialPauseInSeconds).seconds.inWholeMicroseconds, + savePath = Path.of("$capturesPath", "${camera.name}-DARV.fits") + ) + + val cameraExposureTasklet = sequenceTaskletFactory.cameraExposure(cameraRequest) + cameraExposureTasklet.subscribe(this) + val cameraExposureFlow = sequenceFlowFactory.cameraExposure(cameraExposureTasklet) + + val guidePulseDuration = (request.exposureInSeconds / 2.0).seconds.inWholeMilliseconds + val initialPauseDelayTasklet = sequenceTaskletFactory.delay(request.initialPauseInSeconds.seconds) + 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.durationInMilliseconds + val forward = (direction == data.direction) != data.reversed + + when (event) { + is GuidePulseStarted -> { + DARVPolarAlignmentGuidePulseElapsed(camera, guideOutput, forward, direction, duration, 0.0) + } + is GuidePulseElapsed -> { + DARVPolarAlignmentGuidePulseElapsed(camera, guideOutput, forward, direction, event.remainingTime, event.progress) + } + is GuidePulseFinished -> { + DARVPolarAlignmentGuidePulseElapsed(camera, guideOutput, forward, direction, 0L, 1.0) + } + } + } + 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)) + } + + override fun afterJob(jobExecution: JobExecution) { + val (camera, guideOutput) = sequenceJobWithId(jobExecution.jobId) ?: return + messageService.sendMessage(DARVPolarAlignmentFinished(camera, guideOutput)) + } + + 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 new file mode 100644 index 000000000..5e9f2d353 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentFinished.kt @@ -0,0 +1,14 @@ +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 + +data class DARVPolarAlignmentFinished( + override val camera: Camera, + override val guideOutput: GuideOutput, +) : MessageEvent, DARVPolarAlignmentEvent { + + @JsonIgnore 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 new file mode 100644 index 000000000..6f13686a3 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentGuidePulseElapsed.kt @@ -0,0 +1,19 @@ +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 + +data class DARVPolarAlignmentGuidePulseElapsed( + override val camera: Camera, + override val guideOutput: GuideOutput, + val forward: Boolean, + val direction: GuideDirection, + val remainingTime: Long, + val progress: Double, +) : MessageEvent, DARVPolarAlignmentEvent { + + @JsonIgnore override val eventName = "DARV_POLAR_ALIGNMENT_GUIDE_PULSE_ELAPSED" +} 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 new file mode 100644 index 000000000..c8aff9dea --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentInitialPauseElapsed.kt @@ -0,0 +1,23 @@ +package nebulosa.api.alignment.polar.darv + +import com.fasterxml.jackson.annotation.JsonIgnore +import nebulosa.api.sequencer.DelayEvent +import nebulosa.api.services.MessageEvent +import nebulosa.indi.device.camera.Camera +import nebulosa.indi.device.guide.GuideOutput + +data class DARVPolarAlignmentInitialPauseElapsed( + override val camera: Camera, + override val guideOutput: GuideOutput, + val pauseTime: Long, + val remainingTime: Long, + val progress: Double, +) : MessageEvent, DARVPolarAlignmentEvent { + + constructor(camera: Camera, guideOutput: GuideOutput, delay: DelayEvent) : this( + camera, guideOutput, delay.waitTime.inWholeMicroseconds, + delay.remainingTime.inWholeMicroseconds, delay.progress + ) + + @JsonIgnore override val eventName = "DARV_POLAR_ALIGNMENT_INITIAL_PAUSE_ELAPSED" +} 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 new file mode 100644 index 000000000..25d53a927 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentStarted.kt @@ -0,0 +1,14 @@ +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 + +data class DARVPolarAlignmentStarted( + override val camera: Camera, + override val guideOutput: GuideOutput, +) : MessageEvent, DARVPolarAlignmentEvent { + + @JsonIgnore 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 new file mode 100644 index 000000000..6ea79d91a --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVSequenceJob.kt @@ -0,0 +1,18 @@ +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/DARVStart.kt new file mode 100644 index 000000000..6d7bbc23d --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVStart.kt @@ -0,0 +1,16 @@ +package nebulosa.api.alignment.polar.darv + +import com.fasterxml.jackson.annotation.JsonIgnore +import nebulosa.guiding.GuideDirection +import nebulosa.indi.device.camera.Camera +import nebulosa.indi.device.guide.GuideOutput +import org.hibernate.validator.constraints.Range + +data class DARVStart( + @JsonIgnore val camera: Camera? = null, + @JsonIgnore val guideOutput: GuideOutput? = null, + @Range(min = 1, max = 600) val exposureInSeconds: Long = 0L, + @Range(min = 1, max = 60) val initialPauseInSeconds: Long = 0L, + val direction: GuideDirection = GuideDirection.NORTH, + val reversed: Boolean = false, +) diff --git a/api/src/main/kotlin/nebulosa/api/atlas/AtlasDatabaseThreadedTask.kt b/api/src/main/kotlin/nebulosa/api/atlas/AtlasDatabaseThreadedTask.kt index e0f1802ea..253dcc8de 100644 --- a/api/src/main/kotlin/nebulosa/api/atlas/AtlasDatabaseThreadedTask.kt +++ b/api/src/main/kotlin/nebulosa/api/atlas/AtlasDatabaseThreadedTask.kt @@ -97,7 +97,7 @@ class AtlasDatabaseThreadedTask( companion object { - const val DATABASE_VERSION = "2023.09.29" + const val DATABASE_VERSION = "2023.10.05" const val DATABASE_VERSION_KEY = "DATABASE_VERSION" @JvmStatic private val LOG = loggerFor() diff --git a/api/src/main/kotlin/nebulosa/api/atlas/ephemeris/CachedEphemerisProvider.kt b/api/src/main/kotlin/nebulosa/api/atlas/ephemeris/CachedEphemerisProvider.kt index fbfd3be50..432b46a05 100644 --- a/api/src/main/kotlin/nebulosa/api/atlas/ephemeris/CachedEphemerisProvider.kt +++ b/api/src/main/kotlin/nebulosa/api/atlas/ephemeris/CachedEphemerisProvider.kt @@ -3,7 +3,6 @@ package nebulosa.api.atlas.ephemeris import nebulosa.horizons.HorizonsElement import nebulosa.log.loggerFor import nebulosa.nova.position.GeographicPosition -import java.time.LocalDate import java.time.LocalDateTime import java.time.LocalTime import java.time.ZoneOffset diff --git a/api/src/main/kotlin/nebulosa/api/beans/EntityByMethodArgumentResolver.kt b/api/src/main/kotlin/nebulosa/api/beans/EntityByMethodArgumentResolver.kt index 10baa2c44..2bd183e30 100644 --- a/api/src/main/kotlin/nebulosa/api/beans/EntityByMethodArgumentResolver.kt +++ b/api/src/main/kotlin/nebulosa/api/beans/EntityByMethodArgumentResolver.kt @@ -13,11 +13,13 @@ 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 @@ -39,12 +41,27 @@ class EntityByMethodArgumentResolver( 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) - ?: return null + ?: throw throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Parameter $parameterName is not mapped") - return when (parameter.parameterType) { + val entity = entityByParameterValue(parameterType, parameterValue) + + if (entityBy.required && entity == null) { + val message = "Cannot found a ${parameterType.simpleName} entity with name [$parameterValue]" + throw 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()) diff --git a/api/src/main/kotlin/nebulosa/api/beans/annotations/EntityBy.kt b/api/src/main/kotlin/nebulosa/api/beans/annotations/EntityBy.kt index 11a05146c..ed3846b4a 100644 --- a/api/src/main/kotlin/nebulosa/api/beans/annotations/EntityBy.kt +++ b/api/src/main/kotlin/nebulosa/api/beans/annotations/EntityBy.kt @@ -2,4 +2,4 @@ package nebulosa.api.beans.annotations @Retention @Target(AnnotationTarget.VALUE_PARAMETER) -annotation class EntityBy +annotation class EntityBy(val required: Boolean = true) 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 32fbdf525..f639f230b 100644 --- a/api/src/main/kotlin/nebulosa/api/beans/configurations/BeanConfiguration.kt +++ b/api/src/main/kotlin/nebulosa/api/beans/configurations/BeanConfiguration.kt @@ -1,16 +1,18 @@ package nebulosa.api.beans.configurations import com.fasterxml.jackson.databind.DeserializationFeature -import com.fasterxml.jackson.databind.ObjectMapper +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.guiding.Guider +import nebulosa.guiding.phd2.PHD2Guider import nebulosa.hips2fits.Hips2FitsService import nebulosa.horizons.HorizonsService -import nebulosa.json.modules.FromJson -import nebulosa.json.modules.JsonModule -import nebulosa.json.modules.ToJson +import nebulosa.json.* +import nebulosa.json.converters.PathConverter +import nebulosa.phd2.client.PHD2Client import nebulosa.sbd.SmallBodyDatabaseService import nebulosa.simbad.SimbadService import okhttp3.Cache @@ -21,6 +23,7 @@ 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 @@ -55,14 +58,19 @@ class BeanConfiguration { fun cachePath(appPath: Path): Path = Path.of("$appPath", "cache").createDirectories() @Bean - @Primary - fun objectMapper( + fun kotlinModule( serializers: List>, deserializers: List>, - ) = ObjectMapper() - .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) - .enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY) - .registerModule(JsonModule(serializers, deserializers))!! + ) = kotlinModule() + .apply { serializers.forEach { addSerializer(it) } } + .apply { deserializers.forEach { addDeserializer(it) } } + .addConverter(PathConverter) + + @Bean + fun jackson2ObjectMapperBuilderCustomizer() = Jackson2ObjectMapperBuilderCustomizer { + it.featuresToDisable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + it.featuresToEnable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY) + } @Bean fun connectionPool() = ConnectionPool(32, 5L, TimeUnit.MINUTES) @@ -121,7 +129,22 @@ class BeanConfiguration { } @Bean - fun executionIncrementer() = Incrementer() + fun flowIncrementer() = Incrementer() + + @Bean + fun stepIncrementer() = Incrementer() + + @Bean + fun jobIncrementer() = Incrementer() + + @Bean + fun phd2Client() = PHD2Client() + + @Bean + fun phd2Guider(phd2Client: PHD2Client): Guider = PHD2Guider(phd2Client) + + @Bean + fun simpleAsyncTaskExecutor() = SimpleAsyncTaskExecutor(DaemonThreadFactory) @Bean fun webMvcConfigurer( diff --git a/api/src/main/kotlin/nebulosa/api/beans/converters/INDIPropertyConverter.kt b/api/src/main/kotlin/nebulosa/api/beans/converters/INDIPropertyConverter.kt index f1f30dd74..c3eb47f23 100644 --- a/api/src/main/kotlin/nebulosa/api/beans/converters/INDIPropertyConverter.kt +++ b/api/src/main/kotlin/nebulosa/api/beans/converters/INDIPropertyConverter.kt @@ -3,7 +3,7 @@ package nebulosa.api.beans.converters import com.fasterxml.jackson.core.JsonGenerator import com.fasterxml.jackson.databind.SerializerProvider import nebulosa.indi.device.Property -import nebulosa.json.modules.ToJson +import nebulosa.json.ToJson import org.springframework.stereotype.Component @Component diff --git a/api/src/main/kotlin/nebulosa/api/beans/converters/INDIPropertyVectorConverter.kt b/api/src/main/kotlin/nebulosa/api/beans/converters/INDIPropertyVectorConverter.kt index 1b62bc0e4..30a516fc9 100644 --- a/api/src/main/kotlin/nebulosa/api/beans/converters/INDIPropertyVectorConverter.kt +++ b/api/src/main/kotlin/nebulosa/api/beans/converters/INDIPropertyVectorConverter.kt @@ -4,7 +4,7 @@ import com.fasterxml.jackson.core.JsonGenerator import com.fasterxml.jackson.databind.SerializerProvider import nebulosa.indi.device.PropertyVector import nebulosa.indi.device.SwitchPropertyVector -import nebulosa.json.modules.ToJson +import nebulosa.json.ToJson import org.springframework.stereotype.Component @Component diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureEvent.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureEvent.kt index cfd443696..6de16c81f 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureEvent.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureEvent.kt @@ -1,9 +1,10 @@ package nebulosa.api.cameras +import nebulosa.api.sequencer.SequenceTaskletEvent import nebulosa.api.services.MessageEvent import nebulosa.indi.device.camera.Camera -sealed interface CameraCaptureEvent : MessageEvent { +sealed interface CameraCaptureEvent : MessageEvent, SequenceTaskletEvent { val camera: Camera diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureEventConverter.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureEventConverter.kt index 43c08e9be..2fc8c9c22 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureEventConverter.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureEventConverter.kt @@ -18,7 +18,7 @@ import nebulosa.api.cameras.CameraCaptureEvent.Companion.WAIT_REMAINING_TIME import nebulosa.api.cameras.CameraCaptureEvent.Companion.WAIT_TIME import nebulosa.api.sequencer.SequenceJobEvent import nebulosa.api.sequencer.SequenceStepEvent -import nebulosa.json.modules.ToJson +import nebulosa.json.ToJson import org.springframework.stereotype.Component @Component diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureExecutor.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureExecutor.kt index eb5a55ea4..e4828afda 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureExecutor.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureExecutor.kt @@ -1,122 +1,72 @@ package nebulosa.api.cameras import io.reactivex.rxjava3.functions.Consumer -import nebulosa.api.sequencer.SequenceJob import nebulosa.api.sequencer.SequenceJobExecutor -import nebulosa.api.sequencer.tasklets.delay.DelayTasklet +import nebulosa.api.sequencer.SequenceJobFactory import nebulosa.api.services.MessageService -import nebulosa.common.concurrency.Incrementer 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.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.batch.core.step.builder.StepBuilder -import org.springframework.batch.core.step.tasklet.Tasklet import org.springframework.stereotype.Component -import org.springframework.transaction.PlatformTransactionManager import java.util.* -import kotlin.time.Duration.Companion.seconds @Component class CameraCaptureExecutor( - private val jobRepository: JobRepository, private val jobOperator: JobOperator, private val asyncJobLauncher: JobLauncher, - private val platformTransactionManager: PlatformTransactionManager, private val jobRegistry: JobRegistry, private val messageService: MessageService, - private val executionIncrementer: Incrementer, -) : SequenceJobExecutor, Consumer { + private val sequenceJobFactory: SequenceJobFactory, +) : SequenceJobExecutor, Consumer { - private val runningSequenceJobs = LinkedList() + private val runningSequenceJobs = LinkedList() @Synchronized - override fun execute(data: CameraStartCapture): SequenceJob { - val camera = requireNotNull(data.camera) + override fun execute(request: CameraStartCaptureRequest): CameraSequenceJob { + val camera = requireNotNull(request.camera) - if (isCapturing(camera)) { - throw IllegalStateException("A Camera Exposure job is already running. camera=${camera.name}") - } - - LOG.info("starting camera capture. data={}", data) + check(!isCapturing(camera)) { "job is already running for camera: [${camera.name}]" } + check(camera.connected) { "camera is not connected" } - val cameraCaptureJob = if (data.isLoop) { - val cameraExposureTasklet = CameraLoopExposureTasklet(data) - cameraExposureTasklet.subscribe(this) + LOG.info("starting camera capture. data={}", request) - JobBuilder("CameraCapture.Job.${executionIncrementer.increment()}", jobRepository) - .start(cameraExposureStep(cameraExposureTasklet)) - .listener(cameraExposureTasklet) - .build() + val cameraCaptureJob = if (request.isLoop) { + sequenceJobFactory.cameraLoopCapture(request, this) } else { - val cameraExposureTasklet = CameraExposureTasklet(data) - cameraExposureTasklet.subscribe(this) - - val jobBuilder = JobBuilder("CameraCapture.Job.${executionIncrementer.increment()}", jobRepository) - .start(cameraExposureStep(cameraExposureTasklet)) - - val hasDelay = data.exposureDelayInSeconds in 1L..60L - val cameraDelayTasklet = DelayTasklet(data.exposureDelayInSeconds.seconds) - cameraDelayTasklet.subscribe(cameraExposureTasklet) - - repeat(data.exposureAmount - 1) { - if (hasDelay) { - val cameraDelayStep = cameraDelayStep(cameraDelayTasklet) - jobBuilder.next(cameraDelayStep) - } - - val cameraExposureStep = cameraExposureStep(cameraExposureTasklet) - jobBuilder.next(cameraExposureStep) - } - - jobBuilder - .listener(cameraExposureTasklet) - .listener(cameraDelayTasklet) - .build() + sequenceJobFactory.cameraCapture(request, this) } return asyncJobLauncher .run(cameraCaptureJob, JobParameters()) - .let { SequenceJob(listOf(camera), cameraCaptureJob, it) } + .let { CameraSequenceJob(camera, request, cameraCaptureJob, it) } .also(runningSequenceJobs::add) .also { jobRegistry.register(ReferenceJobFactory(cameraCaptureJob)) } } - private fun cameraDelayStep(tasklet: Tasklet) = - StepBuilder("CameraCapture.Step.Delay.${executionIncrementer.increment()}", jobRepository) - .tasklet(tasklet, platformTransactionManager) - .build() - - private fun cameraExposureStep(tasklet: Tasklet) = - StepBuilder("CameraCapture.Step.Exposure.${executionIncrementer.increment()}", jobRepository) - .tasklet(tasklet, platformTransactionManager) - .build() - fun stop(camera: Camera) { val jobExecution = jobExecutionFor(camera) ?: return - jobOperator.stop(jobExecution.jobId) + jobOperator.stop(jobExecution.id) } fun isCapturing(camera: Camera): Boolean { - return sequenceTaskFor(camera)?.jobExecution?.isRunning ?: false + return sequenceJobFor(camera)?.jobExecution?.isRunning ?: false } @Suppress("NOTHING_TO_INLINE") private inline fun jobExecutionFor(camera: Camera): JobExecution? { - return sequenceTaskFor(camera)?.jobExecution + return sequenceJobFor(camera)?.jobExecution } override fun accept(event: CameraCaptureEvent) { messageService.sendMessage(event) } - override fun iterator(): Iterator { + override fun iterator(): Iterator { return runningSequenceJobs.iterator() } diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureFinished.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureFinished.kt index 4549b4151..7bf488c1a 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureFinished.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureFinished.kt @@ -1,13 +1,15 @@ package nebulosa.api.cameras +import com.fasterxml.jackson.annotation.JsonIgnore import nebulosa.api.sequencer.SequenceJobEvent import nebulosa.indi.device.camera.Camera import org.springframework.batch.core.JobExecution data class CameraCaptureFinished( override val camera: Camera, - override val jobExecution: JobExecution, + @JsonIgnore override val jobExecution: JobExecution, + @JsonIgnore override val tasklet: CameraExposureTasklet, ) : CameraCaptureEvent, SequenceJobEvent { - override val eventName = "CAMERA_CAPTURE_FINISHED" + @JsonIgnore override val eventName = "CAMERA_CAPTURE_FINISHED" } diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureStarted.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureStarted.kt index 74839cff0..db6d16f17 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureStarted.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureStarted.kt @@ -1,13 +1,15 @@ package nebulosa.api.cameras +import com.fasterxml.jackson.annotation.JsonIgnore import nebulosa.api.sequencer.SequenceJobEvent import nebulosa.indi.device.camera.Camera import org.springframework.batch.core.JobExecution data class CameraCaptureStarted( override val camera: Camera, - override val jobExecution: JobExecution, + @JsonIgnore override val jobExecution: JobExecution, + @JsonIgnore override val tasklet: CameraExposureTasklet, ) : CameraCaptureEvent, SequenceJobEvent { - override val eventName = "CAMERA_CAPTURE_STARTED" + @JsonIgnore override val eventName = "CAMERA_CAPTURE_STARTED" } diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraController.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraController.kt index f64dfb1b8..67cbe040e 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraController.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraController.kt @@ -58,7 +58,7 @@ class CameraController( @PutMapping("{camera}/capture/start") fun startCapture( @EntityBy camera: Camera, - @RequestBody body: CameraStartCapture, + @RequestBody body: CameraStartCaptureRequest, ) { cameraService.startCapture(camera, body) } diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraConverter.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraConverter.kt index bb0a7cd52..1078a59b3 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraConverter.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraConverter.kt @@ -1,14 +1,23 @@ package nebulosa.api.cameras import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.SerializerProvider +import nebulosa.api.connection.ConnectionService import nebulosa.indi.device.camera.Camera -import nebulosa.json.modules.ToJson +import nebulosa.json.FromJson +import nebulosa.json.ToJson +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Lazy import org.springframework.stereotype.Component import java.nio.file.Path @Component -class CameraConverter(private val capturesPath: Path) : ToJson { +class CameraConverter(private val capturesPath: Path) : ToJson, FromJson { + + @Autowired @Lazy private lateinit var connectionService: ConnectionService override val type = Camera::class.java @@ -67,4 +76,15 @@ class CameraConverter(private val capturesPath: Path) : ToJson { gen.writeObjectField("capturesPath", Path.of("$capturesPath", value.name)) gen.writeEndObject() } + + override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Camera? { + val node = p.codec.readTree(p) + + val name = if (node.has("camera")) node.get("camera").asText() + else if (node.has("device")) node.get("device").asText() + else return null + + return if (name.isNullOrBlank()) null + else connectionService.camera(name) + } } diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureFinished.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureFinished.kt index fe51472c4..520213259 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureFinished.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureFinished.kt @@ -1,5 +1,6 @@ package nebulosa.api.cameras +import com.fasterxml.jackson.annotation.JsonIgnore import nebulosa.api.sequencer.SequenceStepEvent import nebulosa.imaging.Image import nebulosa.indi.device.camera.Camera @@ -8,10 +9,10 @@ import java.nio.file.Path data class CameraExposureFinished( override val camera: Camera, - override val stepExecution: StepExecution, - val image: Image?, - val savePath: Path?, + @JsonIgnore override val stepExecution: StepExecution, + @JsonIgnore override val tasklet: CameraExposureTasklet, + val image: Image?, val savePath: Path?, ) : CameraCaptureEvent, SequenceStepEvent { - override val eventName = "CAMERA_EXPOSURE_FINISHED" + @JsonIgnore override val eventName = "CAMERA_EXPOSURE_FINISHED" } diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureStarted.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureStarted.kt index aa85e3cdb..c18489ff5 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureStarted.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureStarted.kt @@ -1,13 +1,15 @@ 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 data class CameraExposureStarted( override val camera: Camera, - override val stepExecution: StepExecution, + @JsonIgnore override val stepExecution: StepExecution, + @JsonIgnore override val tasklet: CameraExposureTasklet, ) : CameraCaptureEvent, SequenceStepEvent { - override val eventName = "CAMERA_EXPOSURE_STARTED" + @JsonIgnore override val eventName = "CAMERA_EXPOSURE_STARTED" } diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureTasklet.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureTasklet.kt index d9ad401de..ade56d53a 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureTasklet.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureTasklet.kt @@ -42,8 +42,8 @@ import kotlin.time.Duration import kotlin.time.Duration.Companion.microseconds import kotlin.time.Duration.Companion.seconds -data class CameraExposureTasklet(private val request: CameraStartCapture) : - SubjectSequenceTasklet(), JobExecutionListener, Consumer { +data class CameraExposureTasklet(override val request: CameraStartCaptureRequest) : + SubjectSequenceTasklet(), CameraStartCaptureTasklet, JobExecutionListener, Consumer { private val latch = CountUpDownLatch() private val aborted = AtomicBoolean() @@ -89,14 +89,14 @@ data class CameraExposureTasklet(private val request: CameraStartCapture) : camera.enableBlob() EventBus.getDefault().register(this) jobExecution.executionContext.put(CAPTURE_IN_LOOP, request.isLoop) - onNext(CameraCaptureStarted(camera, jobExecution)) + onNext(CameraCaptureStarted(camera, jobExecution, this)) captureElapsedTime = 0L } override fun afterJob(jobExecution: JobExecution) { camera.disableBlob() EventBus.getDefault().unregister(this) - onNext(CameraCaptureFinished(camera, jobExecution)) + onNext(CameraCaptureFinished(camera, jobExecution, this)) close() } @@ -115,10 +115,9 @@ data class CameraExposureTasklet(private val request: CameraStartCapture) : override fun accept(event: DelayElapsed) { captureElapsedTime += event.waitTime.inWholeMicroseconds - val waitProgress = if (event.remainingTime > Duration.ZERO) 1.0 - event.delayTime / event.remainingTime else 1.0 with(stepExecution!!.executionContext) { - putDouble(WAIT_PROGRESS, waitProgress) + putDouble(WAIT_PROGRESS, event.progress) putLong(WAIT_REMAINING_TIME, event.remainingTime.inWholeMicroseconds) putLong(WAIT_TIME, event.waitTime.inWholeMicroseconds) put(CAPTURE_IS_WAITING, true) @@ -143,9 +142,12 @@ data class CameraExposureTasklet(private val request: CameraStartCapture) : put(CAPTURE_IS_WAITING, false) } - onNext(CameraExposureStarted(camera, stepExecution!!)) + onNext(CameraExposureStarted(camera, stepExecution!!, this)) + + if (request.width > 0 && request.height > 0) { + camera.frame(request.x, request.y, request.width, request.height) + } - camera.frame(request.x, request.y, request.width, request.height) camera.frameType(request.frameType) camera.frameFormat(request.frameFormat) camera.bin(request.binX, request.binY) @@ -180,14 +182,14 @@ data class CameraExposureTasklet(private val request: CameraStartCapture) : try { if (request.saveInMemory) { val image = Image.openFITS(inputStream) - onNext(CameraExposureFinished(camera, stepExecution, image, savePath)) + onNext(CameraExposureFinished(camera, stepExecution, this, image, savePath)) } else { LOG.info("saving FITS at $savePath...") savePath!!.createParentDirectories() inputStream.transferAndClose(savePath.outputStream()) - onNext(CameraExposureFinished(camera, stepExecution, null, savePath)) + onNext(CameraExposureFinished(camera, stepExecution, this, null, savePath)) } } catch (e: Throwable) { LOG.error("failed to save FITS", e) @@ -215,7 +217,7 @@ data class CameraExposureTasklet(private val request: CameraStartCapture) : putLong(CAPTURE_ELAPSED_TIME, elapsedTime.inWholeMicroseconds) } - onNext(CameraExposureUpdated(camera, stepExecution!!)) + onNext(CameraExposureUpdated(camera, stepExecution!!, this)) } companion object { diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureUpdated.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureUpdated.kt index b60373d37..0ca8933c3 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureUpdated.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureUpdated.kt @@ -1,13 +1,15 @@ 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 data class CameraExposureUpdated( override val camera: Camera, - override val stepExecution: StepExecution, + @JsonIgnore override val stepExecution: StepExecution, + @JsonIgnore override val tasklet: CameraExposureTasklet, ) : CameraCaptureEvent, SequenceStepEvent { - override val eventName = "CAMERA_EXPOSURE_UPDATED" + @JsonIgnore override val eventName = "CAMERA_EXPOSURE_UPDATED" } diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraLoopExposureTasklet.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraLoopExposureTasklet.kt index b67797fa3..f282bf592 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraLoopExposureTasklet.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraLoopExposureTasklet.kt @@ -9,8 +9,8 @@ import org.springframework.batch.core.scope.context.ChunkContext import org.springframework.batch.repeat.RepeatStatus import kotlin.time.Duration.Companion.seconds -data class CameraLoopExposureTasklet(private val request: CameraStartCapture) : - SubjectSequenceTasklet(), JobExecutionListener { +data class CameraLoopExposureTasklet(override val request: CameraStartCaptureRequest) : + SubjectSequenceTasklet(), CameraStartCaptureTasklet, JobExecutionListener { private val exposureTasklet = CameraExposureTasklet(request) private val delayTasklet = DelayTasklet(request.exposureDelayInSeconds.seconds) diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraSequenceJob.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraSequenceJob.kt new file mode 100644 index 000000000..d16338932 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraSequenceJob.kt @@ -0,0 +1,16 @@ +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/CameraService.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraService.kt index b84e24199..8d0442dd8 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraService.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraService.kt @@ -33,7 +33,7 @@ class CameraService( } @Synchronized - fun startCapture(camera: Camera, request: CameraStartCapture) { + fun startCapture(camera: Camera, request: CameraStartCaptureRequest) { if (isCapturing(camera)) return val savePath = request.savePath diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraStartCapture.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraStartCaptureRequest.kt similarity index 81% rename from api/src/main/kotlin/nebulosa/api/cameras/CameraStartCapture.kt rename to api/src/main/kotlin/nebulosa/api/cameras/CameraStartCaptureRequest.kt index fb1438cf6..fe1da96d8 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraStartCapture.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraStartCaptureRequest.kt @@ -1,14 +1,16 @@ package nebulosa.api.cameras import com.fasterxml.jackson.annotation.JsonIgnore +import jakarta.validation.Valid import jakarta.validation.constraints.Positive import jakarta.validation.constraints.PositiveOrZero +import nebulosa.api.guiding.DitherAfterExposureRequest import nebulosa.indi.device.camera.Camera import nebulosa.indi.device.camera.FrameType import org.hibernate.validator.constraints.Range import java.nio.file.Path -data class CameraStartCapture( +data class CameraStartCaptureRequest( @JsonIgnore val camera: Camera? = null, @field:Positive val exposureInMicroseconds: Long = 0L, @field:Range(min = 0L, max = 1000L) val exposureAmount: Int = 1, // 0 = looping @@ -26,7 +28,8 @@ data class CameraStartCapture( val autoSave: Boolean = false, val savePath: Path? = null, val autoSubFolderMode: AutoSubFolderMode = AutoSubFolderMode.OFF, - @JsonIgnore val saveInMemory: Boolean = false, + @JsonIgnore val saveInMemory: Boolean = savePath == null, + @field:Valid val dither: DitherAfterExposureRequest = DitherAfterExposureRequest.DISABLED, ) { inline val isLoop diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraStartCaptureTasklet.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraStartCaptureTasklet.kt new file mode 100644 index 000000000..dffc38539 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraStartCaptureTasklet.kt @@ -0,0 +1,8 @@ +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/data/responses/GuidingChartResponse.kt b/api/src/main/kotlin/nebulosa/api/data/responses/GuidingChartResponse.kt deleted file mode 100644 index 3d561300f..000000000 --- a/api/src/main/kotlin/nebulosa/api/data/responses/GuidingChartResponse.kt +++ /dev/null @@ -1,10 +0,0 @@ -package nebulosa.api.data.responses - -import nebulosa.guiding.GuideStats - -data class GuidingChartResponse( - val chart: List, - val rmsRA: Double, - val rmsDEC: Double, - val rmsTotal: Double, -) diff --git a/api/src/main/kotlin/nebulosa/api/data/responses/GuidingStarResponse.kt b/api/src/main/kotlin/nebulosa/api/data/responses/GuidingStarResponse.kt deleted file mode 100644 index d4319d37e..000000000 --- a/api/src/main/kotlin/nebulosa/api/data/responses/GuidingStarResponse.kt +++ /dev/null @@ -1,13 +0,0 @@ -package nebulosa.api.data.responses - -data class GuidingStarResponse( - val image: String, - val lockPositionX: Double, - val lockPositionY: Double, - val primaryStarX: Double, - val primaryStarY: Double, - val peak: Double, - val fwhm: Float, - val hfd: Double, - val snr: Double, -) diff --git a/api/src/main/kotlin/nebulosa/api/focusers/FocuserConverter.kt b/api/src/main/kotlin/nebulosa/api/focusers/FocuserConverter.kt index d5890d800..5f9bc8a89 100644 --- a/api/src/main/kotlin/nebulosa/api/focusers/FocuserConverter.kt +++ b/api/src/main/kotlin/nebulosa/api/focusers/FocuserConverter.kt @@ -3,7 +3,7 @@ package nebulosa.api.focusers import com.fasterxml.jackson.core.JsonGenerator import com.fasterxml.jackson.databind.SerializerProvider import nebulosa.indi.device.focuser.Focuser -import nebulosa.json.modules.ToJson +import nebulosa.json.ToJson import org.springframework.stereotype.Component @Component diff --git a/api/src/main/kotlin/nebulosa/api/guiding/DitherAfterExposureRequest.kt b/api/src/main/kotlin/nebulosa/api/guiding/DitherAfterExposureRequest.kt new file mode 100644 index 000000000..c5a0b4dfe --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/guiding/DitherAfterExposureRequest.kt @@ -0,0 +1,16 @@ +package nebulosa.api.guiding + +import jakarta.validation.constraints.Positive + +data class DitherAfterExposureRequest( + val enabled: Boolean = true, + @field:Positive val amount: Double = 1.5, + val raOnly: Boolean = false, + @field:Positive val afterExposures: Int = 1, +) { + + companion object { + + @JvmStatic val DISABLED = DitherAfterExposureRequest(false) + } +} diff --git a/api/src/main/kotlin/nebulosa/api/guiding/DitherAfterExposureTasklet.kt b/api/src/main/kotlin/nebulosa/api/guiding/DitherAfterExposureTasklet.kt new file mode 100644 index 000000000..0624bb52c --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/guiding/DitherAfterExposureTasklet.kt @@ -0,0 +1,49 @@ +package nebulosa.api.guiding + +import nebulosa.common.concurrency.CountUpDownLatch +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 (request.enabled) { + 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/DitherMode.kt b/api/src/main/kotlin/nebulosa/api/guiding/DitherMode.kt deleted file mode 100644 index 52f0a9c46..000000000 --- a/api/src/main/kotlin/nebulosa/api/guiding/DitherMode.kt +++ /dev/null @@ -1,6 +0,0 @@ -package nebulosa.api.guiding - -enum class DitherMode { - RANDOM, - SPIRAL, -} diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuideAlgorithmType.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuideAlgorithmType.kt deleted file mode 100644 index 121b37515..000000000 --- a/api/src/main/kotlin/nebulosa/api/guiding/GuideAlgorithmType.kt +++ /dev/null @@ -1,9 +0,0 @@ -package nebulosa.api.guiding - -enum class GuideAlgorithmType { - IDENTITY, - HYSTERESIS, - LOW_PASS, - LOW_PASS_2, - RESIST_SWITCH, -} diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuideCalibrationEntity.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuideCalibrationEntity.kt deleted file mode 100644 index 5249c54ae..000000000 --- a/api/src/main/kotlin/nebulosa/api/guiding/GuideCalibrationEntity.kt +++ /dev/null @@ -1,52 +0,0 @@ -package nebulosa.api.guiding - -import jakarta.persistence.* -import nebulosa.guiding.GuideCalibration -import nebulosa.guiding.GuideParity -import nebulosa.indi.device.camera.Camera -import nebulosa.indi.device.guide.GuideOutput -import nebulosa.indi.device.mount.Mount -import nebulosa.math.rad - -@Entity -@Table(name = "guide_calibrations") -data class GuideCalibrationEntity( - @Id @Column(name = "id", columnDefinition = "INT8") var id: Long = 0L, - @Column(name = "camera", columnDefinition = "TEXT") var camera: String = "", - @Column(name = "mount", columnDefinition = "TEXT") var mount: String = "", - @Column(name = "guide_output", columnDefinition = "TEXT") var guideOutput: String = "", - @Column(name = "saved_at", columnDefinition = "INT8") var savedAt: Long = 0L, - @Column(name = "x_rate", columnDefinition = "REAL") var xRate: Double = 0.0, - @Column(name = "y_rate", columnDefinition = "REAL") var yRate: Double = 0.0, - @Column(name = "x_angle", columnDefinition = "REAL") var xAngle: Double = 0.0, // rad - @Column(name = "y_angle", columnDefinition = "REAL") var yAngle: Double = 0.0, // rad - @Column(name = "declination", columnDefinition = "REAL") var declination: Double = 0.0, // rad - @Column(name = "rotator_angle", columnDefinition = "REAL") var rotatorAngle: Double = 0.0, // rad - @Column(name = "binning", columnDefinition = "INT1") var binning: Int = 1, - @Column(name = "pier_side_at_east", columnDefinition = "INT1") var pierSideAtEast: Boolean = false, - @Column(name = "ra_guide_parity", columnDefinition = "INT1") @Enumerated(EnumType.ORDINAL) var raGuideParity: GuideParity = GuideParity.UNKNOWN, - @Column(name = "dec_guide_parity", columnDefinition = "INT1") @Enumerated(EnumType.ORDINAL) var decGuideParity: GuideParity = GuideParity.UNKNOWN, -) { - - fun toGuideCalibration() = GuideCalibration( - xRate, yRate, - xAngle.rad, yAngle.rad, declination.rad, rotatorAngle.rad, - binning, pierSideAtEast, raGuideParity, decGuideParity - ) - - companion object { - - @JvmStatic - fun from( - camera: Camera, mount: Mount, guideOutput: GuideOutput, - calibration: GuideCalibration, - ) = GuideCalibrationEntity( - 0L, camera.name, mount.name, guideOutput.name, System.currentTimeMillis(), - calibration.xRate, calibration.yRate, - calibration.xAngle, calibration.yAngle, - calibration.declination, calibration.rotatorAngle, - calibration.binning, calibration.pierSideAtEast, - calibration.raGuideParity, calibration.decGuideParity, - ) - } -} diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuideCalibrationRepository.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuideCalibrationRepository.kt deleted file mode 100644 index d8e17b268..000000000 --- a/api/src/main/kotlin/nebulosa/api/guiding/GuideCalibrationRepository.kt +++ /dev/null @@ -1,23 +0,0 @@ -package nebulosa.api.guiding - -import nebulosa.indi.device.camera.Camera -import nebulosa.indi.device.guide.GuideOutput -import nebulosa.indi.device.mount.Mount -import org.springframework.data.jpa.repository.JpaRepository -import org.springframework.data.jpa.repository.Query -import org.springframework.stereotype.Repository - -@Repository -interface GuideCalibrationRepository : JpaRepository { - - @Query( - "SELECT gc.* FROM guide_calibrations gc WHERE" + - " gc.camera = :#{#camera.name} AND" + - " gc.mount = :#{#mount.name} AND" + - " gc.guide_output = :#{#guideOutput.name}" + - " ORDER BY gc.saved_at DESC" + - " LIMIT 1", - nativeQuery = true - ) - fun get(camera: Camera, mount: Mount, guideOutput: GuideOutput): GuideCalibrationEntity? -} diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuideCalibrationThreadedTask.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuideCalibrationThreadedTask.kt deleted file mode 100644 index ca1ef538c..000000000 --- a/api/src/main/kotlin/nebulosa/api/guiding/GuideCalibrationThreadedTask.kt +++ /dev/null @@ -1,14 +0,0 @@ -package nebulosa.api.guiding - -import nebulosa.api.beans.annotations.ThreadedTask -import org.springframework.stereotype.Component - -@Component -@ThreadedTask -class GuideCalibrationThreadedTask( - private val guideCalibrationRepository: GuideCalibrationRepository, -) : Runnable { - - override fun run() { - } -} diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputController.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputController.kt new file mode 100644 index 000000000..6c4ddbfa3 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputController.kt @@ -0,0 +1,43 @@ +package nebulosa.api.guiding + +import nebulosa.api.beans.annotations.EntityBy +import nebulosa.api.connection.ConnectionService +import nebulosa.guiding.GuideDirection +import nebulosa.indi.device.guide.GuideOutput +import org.springframework.web.bind.annotation.* + +@RestController +@RequestMapping("guide-outputs") +class GuideOutputController( + private val connectionService: ConnectionService, + private val guideOutputService: GuideOutputService, +) { + + @GetMapping + fun guideOutputs(): List { + return connectionService.guideOutputs() + } + + @GetMapping("{guideOutput}") + fun guideOutput(@EntityBy guideOutput: GuideOutput): GuideOutput { + return guideOutput + } + + @PutMapping("{guideOutput}/connect") + fun connect(@EntityBy guideOutput: GuideOutput) { + guideOutputService.connect(guideOutput) + } + + @PutMapping("{guideOutput}/disconnect") + fun disconnect(@EntityBy guideOutput: GuideOutput) { + guideOutputService.disconnect(guideOutput) + } + + @PutMapping("{guideOutput}/pulse") + fun pulse( + @EntityBy guideOutput: GuideOutput, + @RequestParam direction: GuideDirection, @RequestParam duration: Int, + ) { + guideOutputService.pulse(guideOutput, direction, duration) + } +} diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputService.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputService.kt new file mode 100644 index 000000000..4d0d39822 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputService.kt @@ -0,0 +1,28 @@ +package nebulosa.api.guiding + +import nebulosa.guiding.GuideDirection +import nebulosa.indi.device.guide.GuideOutput +import org.springframework.stereotype.Service + +@Service +class GuideOutputService { + + fun connect(guideOutput: GuideOutput) { + guideOutput.connect() + } + + fun disconnect(guideOutput: GuideOutput) { + guideOutput.disconnect() + } + + fun pulse(guideOutput: GuideOutput, direction: GuideDirection, duration: Int) { + if (guideOutput.canPulseGuide) { + when (direction) { + GuideDirection.NORTH -> guideOutput.guideNorth(duration) + GuideDirection.SOUTH -> guideOutput.guideSouth(duration) + GuideDirection.WEST -> guideOutput.guideWest(duration) + GuideDirection.EAST -> guideOutput.guideEast(duration) + } + } + } +} diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseElapsed.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseElapsed.kt new file mode 100644 index 000000000..26468f580 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseElapsed.kt @@ -0,0 +1,17 @@ +package nebulosa.api.guiding + +import com.fasterxml.jackson.annotation.JsonIgnore +import nebulosa.api.sequencer.SequenceStepEvent +import nebulosa.guiding.GuideDirection +import org.springframework.batch.core.StepExecution + +data class GuidePulseElapsed( + val remainingTime: Long, + val progress: Double, + val direction: GuideDirection, + @JsonIgnore override val stepExecution: StepExecution, + @JsonIgnore override val tasklet: GuidePulseTasklet, +) : GuidePulseEvent, SequenceStepEvent { + + @JsonIgnore override val eventName = "GUIDE_PULSE_ELAPSED" +} diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseEvent.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseEvent.kt new file mode 100644 index 000000000..583009260 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseEvent.kt @@ -0,0 +1,10 @@ +package nebulosa.api.guiding + +import nebulosa.api.sequencer.SequenceStepEvent +import nebulosa.api.sequencer.SequenceTaskletEvent +import nebulosa.api.services.MessageEvent + +sealed interface GuidePulseEvent : MessageEvent, 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 new file mode 100644 index 000000000..48d0913a0 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseFinished.kt @@ -0,0 +1,13 @@ +package nebulosa.api.guiding + +import com.fasterxml.jackson.annotation.JsonIgnore +import nebulosa.api.sequencer.SequenceStepEvent +import org.springframework.batch.core.StepExecution + +data class GuidePulseFinished( + @JsonIgnore override val stepExecution: StepExecution, + @JsonIgnore override val tasklet: GuidePulseTasklet, +) : GuidePulseEvent, SequenceStepEvent { + + @JsonIgnore override val eventName = "GUIDE_PULSE_FINISHED" +} diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseListener.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseListener.kt deleted file mode 100644 index 0cf3abf16..000000000 --- a/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseListener.kt +++ /dev/null @@ -1,12 +0,0 @@ -package nebulosa.api.guiding - -import nebulosa.guiding.GuideDirection -import nebulosa.indi.device.guide.GuideOutput -import kotlin.time.Duration - -interface GuidePulseListener { - - fun onGuidePulseStarted(guideOutput: GuideOutput, direction: GuideDirection, duration: Duration) {} - - fun onGuidePulseFinished(guideOutput: GuideOutput, direction: GuideDirection, duration: Duration) {} -} diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseRequest.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseRequest.kt new file mode 100644 index 000000000..c0e6a8e3a --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseRequest.kt @@ -0,0 +1,12 @@ +package nebulosa.api.guiding + +import com.fasterxml.jackson.annotation.JsonIgnore +import jakarta.validation.constraints.Positive +import nebulosa.guiding.GuideDirection +import nebulosa.indi.device.guide.GuideOutput + +data class GuidePulseRequest( + @JsonIgnore val guideOutput: GuideOutput? = null, + val direction: GuideDirection, + @field:Positive val durationInMilliseconds: Long, +) diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseStarted.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseStarted.kt new file mode 100644 index 000000000..0037f1104 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseStarted.kt @@ -0,0 +1,12 @@ +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 { + + @JsonIgnore override val eventName = "GUIDE_PULSE_STARTED" +} diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseTasklet.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseTasklet.kt index 83ef49e47..abd052e5a 100644 --- a/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseTasklet.kt +++ b/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseTasklet.kt @@ -1,44 +1,65 @@ package nebulosa.api.guiding +import io.reactivex.rxjava3.functions.Consumer +import nebulosa.api.sequencer.SubjectSequenceTasklet +import nebulosa.api.sequencer.tasklets.delay.DelayElapsed +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.core.step.tasklet.StoppableTasklet import org.springframework.batch.repeat.RepeatStatus -import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds -data class GuidePulseTasklet( - private val guideOutput: GuideOutput, - private val direction: GuideDirection, private val duration: Duration, - private val listener: GuidePulseListener, -) : StoppableTasklet { +data class GuidePulseTasklet(val request: GuidePulseRequest) : SubjectSequenceTasklet(), Consumer { + + private val delayTasklet = DelayTasklet(request.durationInMilliseconds.milliseconds) + + init { + delayTasklet.subscribe(this) + } override fun execute(contribution: StepContribution, chunkContext: ChunkContext): RepeatStatus { - val durationInMilliseconds = duration.inWholeMilliseconds.toInt() + val guideOutput = requireNotNull(request.guideOutput) + val durationInMilliseconds = request.durationInMilliseconds + + // Force stop in reversed direction. + guideOutput.pulseGuide(0, request.direction.reversed) - if (guideTo(durationInMilliseconds)) { - listener.onGuidePulseStarted(guideOutput, direction, duration) - Thread.sleep(durationInMilliseconds.toLong()) - listener.onGuidePulseFinished(guideOutput, direction, duration) + if (guideOutput.pulseGuide(durationInMilliseconds.toInt(), request.direction)) { + delayTasklet.execute(contribution, chunkContext) } return RepeatStatus.FINISHED } override fun stop() { - guideTo(0) + request.guideOutput?.pulseGuide(0, request.direction) + delayTasklet.stop() } - private fun guideTo(durationInMilliseconds: Int): Boolean { - when (direction) { - GuideDirection.UP_NORTH -> guideOutput.guideNorth(durationInMilliseconds) - GuideDirection.DOWN_SOUTH -> guideOutput.guideSouth(durationInMilliseconds) - GuideDirection.LEFT_WEST -> guideOutput.guideWest(durationInMilliseconds) - GuideDirection.RIGHT_EAST -> guideOutput.guideEast(durationInMilliseconds) - else -> return false + override fun accept(event: DelayElapsed) { + if (event.isStarted) onNext(GuidePulseStarted(event.stepExecution, this)) + else if (event.isFinished) onNext(GuidePulseFinished(event.stepExecution, this)) + else { + val remainingTime = event.remainingTime.inWholeMicroseconds + onNext(GuidePulseElapsed(remainingTime, event.progress, request.direction, event.stepExecution, this)) } + } + + companion object { - return true + @JvmStatic + private fun GuideOutput.pulseGuide(durationInMilliseconds: Int, direction: GuideDirection): Boolean { + when (direction) { + GuideDirection.NORTH -> guideNorth(durationInMilliseconds) + GuideDirection.SOUTH -> guideSouth(durationInMilliseconds) + GuideDirection.WEST -> guideWest(durationInMilliseconds) + GuideDirection.EAST -> guideEast(durationInMilliseconds) + else -> return false + } + + return true + } } } diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuideStartLooping.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuideStartLooping.kt deleted file mode 100644 index d1aa23641..000000000 --- a/api/src/main/kotlin/nebulosa/api/guiding/GuideStartLooping.kt +++ /dev/null @@ -1,26 +0,0 @@ -package nebulosa.api.guiding - -import nebulosa.guiding.DeclinationGuideMode -import nebulosa.guiding.NoiseReductionMethod - -data class GuideStartLooping( - var searchRegion: Double = 15.0, - var ditherMode: DitherMode = DitherMode.RANDOM, - var ditherAmount: Double = 5.0, - var ditherRAOnly: Boolean = false, - var calibrationFlipRequiresDecFlip: Boolean = false, - var assumeDECOrthogonalToRA: Boolean = false, - var calibrationStep: Int = 1000, - var calibrationDistance: Int = 25, - var useDECCompensation: Boolean = true, - var declinationGuideMode: DeclinationGuideMode = DeclinationGuideMode.AUTO, - var maxDECDuration: Int = 2500, - var maxRADuration: Int = 2500, - var noiseReductionMethod: NoiseReductionMethod = NoiseReductionMethod.NONE, - var xGuideAlgorithm: GuideAlgorithmType = GuideAlgorithmType.HYSTERESIS, - var yGuideAlgorithm: GuideAlgorithmType = GuideAlgorithmType.HYSTERESIS, - // min: 0.1, max: 10 (px) - var minimumStarHFD: Double = 1.5, - // min: 0.1, max: 10 (px) - var maximumStarHFD: Double = 1.5, -) diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuideStepHistory.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuideStepHistory.kt new file mode 100644 index 000000000..3f22a9547 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/guiding/GuideStepHistory.kt @@ -0,0 +1,56 @@ +package nebulosa.api.guiding + +import nebulosa.common.concurrency.Incrementer +import nebulosa.guiding.GuideStep +import java.util.* +import kotlin.math.max +import kotlin.math.min + +class GuideStepHistory private constructor(private val data: LinkedList) : List by data { + + constructor() : this(LinkedList()) + + private val id = Incrementer() + private val rms = RMS() + + var maxHistorySize = 100 + set(value) { + field = max(100, min(value, 1000)) + } + + private fun add(step: HistoryStep) { + while (data.size >= maxHistorySize) { + data.pop() + } + + data.add(step) + } + + @Synchronized + fun addGuideStep(guideStep: GuideStep): HistoryStep { + if (rms.size == maxHistorySize) { + while (isNotEmpty()) { + val removedStep = data.pop() + removedStep.guideStep ?: continue + rms.removeDataPoint(removedStep.guideStep.raDistance, removedStep.guideStep.decDistance) + break + } + } + + rms.addDataPoint(guideStep.raDistance, guideStep.decDistance) + + return HistoryStep(id.increment(), rms.rightAscension, rms.declination, rms.total, guideStep) + .also(::add) + } + + @Synchronized + fun addDither(dx: Double, dy: Double) { + add(HistoryStep(id = id.increment(), ditherX = dx, ditherY = dy)) + } + + @Synchronized + fun clear() { + data.clear() + rms.clear() + } +} diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuiderConverter.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuiderConverter.kt deleted file mode 100644 index 765418e91..000000000 --- a/api/src/main/kotlin/nebulosa/api/guiding/GuiderConverter.kt +++ /dev/null @@ -1,39 +0,0 @@ -package nebulosa.api.guiding - -import com.fasterxml.jackson.core.JsonGenerator -import com.fasterxml.jackson.databind.SerializerProvider -import nebulosa.guiding.GuidePoint -import nebulosa.guiding.Guider -import nebulosa.json.modules.ToJson -import org.springframework.stereotype.Component - -@Component -class GuiderConverter : ToJson { - - override val type = Guider::class.java - - override fun serialize(value: Guider, gen: JsonGenerator, provider: SerializerProvider) { - gen.writeStartObject() - gen.writeFieldName("lockPosition") - gen.writeGuidePoint(value.lockPosition) - gen.writeFieldName("primaryStar") - gen.writeGuidePoint(value.primaryStar) - gen.writeNumberField("searchRegion", value.searchRegion) - // TODO: gen.writeBooleanField("looping", value.isLooping) - gen.writeBooleanField("calibrating", value.isCalibrating) - gen.writeBooleanField("guiding", value.isGuiding) - gen.writeEndObject() - } - - companion object { - - @JvmStatic - private fun JsonGenerator.writeGuidePoint(point: GuidePoint) { - writeStartObject() - writeNumberField("x", point.x) - writeNumberField("y", point.y) - writeBooleanField("valid", point.valid) - writeEndObject() - } - } -} diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuiderStatus.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuiderStatus.kt new file mode 100644 index 000000000..d4cfc2739 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/guiding/GuiderStatus.kt @@ -0,0 +1,16 @@ +package nebulosa.api.guiding + +import nebulosa.guiding.GuideState + +data class GuiderStatus( + val connected: Boolean = false, + val state: GuideState = GuideState.STOPPED, + val settling: Boolean = false, + val pixelScale: Double = 1.0, +) { + + companion object { + + @JvmStatic val DISCONNECTED = GuiderStatus() + } +} diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuidingController.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuidingController.kt index e06138240..352072792 100644 --- a/api/src/main/kotlin/nebulosa/api/guiding/GuidingController.kt +++ b/api/src/main/kotlin/nebulosa/api/guiding/GuidingController.kt @@ -1,91 +1,77 @@ package nebulosa.api.guiding import jakarta.validation.Valid -import jakarta.validation.constraints.NotBlank -import jakarta.validation.constraints.PositiveOrZero -import nebulosa.api.connection.ConnectionService -import nebulosa.api.data.responses.GuidingChartResponse -import nebulosa.api.data.responses.GuidingStarResponse -import nebulosa.indi.device.guide.GuideOutput -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.PostMapping -import org.springframework.web.bind.annotation.RequestBody -import org.springframework.web.bind.annotation.RequestParam -import org.springframework.web.bind.annotation.RestController +import org.hibernate.validator.constraints.Range +import org.springframework.web.bind.annotation.* +import kotlin.time.Duration.Companion.seconds @RestController -class GuidingController( - private val connectionService: ConnectionService, - private val guidingService: GuidingService, -) { +@RequestMapping("guiding") +class GuidingController(private val guidingService: GuidingService) { - @GetMapping("attachedGuideOutputs") - fun guideOutputs(): List { - return connectionService.guideOutputs() + @PutMapping("connect") + fun connect( + @RequestParam(required = false, defaultValue = "localhost") host: String, + @RequestParam(required = false, defaultValue = "4400") port: Int, + ) { + guidingService.connect(host, port) } - @GetMapping("guideOutput") - fun guideOutput(@RequestParam @Valid @NotBlank name: String): GuideOutput { - return requireNotNull(connectionService.guideOutput(name)) + @DeleteMapping("disconnect") + fun disconnect() { + guidingService.disconnect() } - @PostMapping("guideOutputConnect") - fun connect(@RequestParam @Valid @NotBlank name: String) { - val guideOutput = requireNotNull(connectionService.guideOutput(name)) - guidingService.connect(guideOutput) + @GetMapping("status") + fun status(): GuiderStatus { + return guidingService.status() } - @PostMapping("guideOutputDisconnect") - fun disconnect(@RequestParam @Valid @NotBlank name: String) { - val guideOutput = requireNotNull(connectionService.guideOutput(name)) - guidingService.disconnect(guideOutput) + @GetMapping("history") + fun history(): List { + return guidingService.history() } - @PostMapping("guideLoopingStart") - fun startLooping( - @RequestParam("camera") @Valid @NotBlank cameraName: String, - @RequestParam("mount") @Valid @NotBlank mountName: String, - @RequestParam("guideOutput") @Valid @NotBlank guideOutputName: String, - @RequestBody @Valid body: GuideStartLooping, - ) { - val camera = requireNotNull(connectionService.camera(cameraName)) - val mount = requireNotNull(connectionService.mount(mountName)) - val guideOutput = requireNotNull(connectionService.guideOutput(guideOutputName)) - guidingService.startLooping(camera, mount, guideOutput, body) + @GetMapping("history/latest") + fun latestHistory(): HistoryStep? { + return guidingService.latestHistory() } - @PostMapping("guidingStart") - fun startGuiding( - @RequestParam(required = false, defaultValue = "false") forceCalibration: Boolean, - ) { - guidingService.startGuiding(forceCalibration) + + @PutMapping("history/clear") + fun clearHistory() { + return guidingService.clearHistory() } - @PostMapping("guidingStop") - fun stopGuiding() { - guidingService.stop() + @PutMapping("loop") + fun loop(@RequestParam(required = false, defaultValue = "true") autoSelectGuideStar: Boolean) { + guidingService.loop(autoSelectGuideStar) } - @GetMapping("guidingChart") - fun guidingChart(): GuidingChartResponse { - return guidingService.guidingChart() + @PutMapping("start") + fun start(@RequestParam(required = false, defaultValue = "false") forceCalibration: Boolean) { + guidingService.start(forceCalibration) } - @GetMapping("guidingStar") - fun guidingStar(): GuidingStarResponse? { - return guidingService.guidingStar() + @PutMapping("settle") + fun settle( + @RequestParam(required = false) @Valid @Range(min = 1, max = 25) amount: Double?, + @RequestParam(required = false) @Valid @Range(min = 1, max = 60) time: Long?, + @RequestParam(required = false) @Valid @Range(min = 1, max = 60) timeout: Long?, + ) { + guidingService.settle(amount, time?.seconds, timeout?.seconds) } - @PostMapping("selectGuideStar") - fun selectGuideStar( - @RequestParam @Valid @PositiveOrZero x: Double, - @RequestParam @Valid @PositiveOrZero y: Double, + @PutMapping("dither") + fun dither( + @RequestParam amount: Double, + @RequestParam(required = false, defaultValue = "false") raOnly: Boolean, ) { - guidingService.selectGuideStar(x, y) + return guidingService.dither(amount, raOnly) } - @PostMapping("deselectGuideStar") - fun deselectGuideStar() { - guidingService.deselectGuideStar() + @PutMapping("stop") + fun stop() { + guidingService.stop() } } diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuidingExecutor.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuidingExecutor.kt deleted file mode 100644 index b638d287a..000000000 --- a/api/src/main/kotlin/nebulosa/api/guiding/GuidingExecutor.kt +++ /dev/null @@ -1,285 +0,0 @@ -package nebulosa.api.guiding - -import jakarta.annotation.PostConstruct -import nebulosa.api.services.MessageService -import nebulosa.guiding.* -import nebulosa.guiding.internal.* -import nebulosa.imaging.Image -import nebulosa.indi.device.camera.Camera -import nebulosa.indi.device.guide.GuideOutput -import nebulosa.indi.device.mount.Mount -import nebulosa.indi.device.mount.PierSide -import nebulosa.log.loggerFor -import nebulosa.math.Angle -import org.springframework.batch.core.Job -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.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.batch.core.step.builder.StepBuilder -import org.springframework.stereotype.Component -import org.springframework.transaction.PlatformTransactionManager -import java.util.* -import java.util.concurrent.atomic.AtomicInteger -import java.util.concurrent.atomic.AtomicReference - -// TODO: Implement Guide Rate property on mount device. - -@Component -class GuidingExecutor( - private val jobRepository: JobRepository, - private val jobOperator: JobOperator, - private val asyncJobLauncher: JobLauncher, - private val platformTransactionManager: PlatformTransactionManager, - private val jobRegistry: JobRegistry, - private val guideCalibrationRepository: GuideCalibrationRepository, - private val messageService: MessageService, -) : GuidingCamera, GuidingMount, GuidingRotator, GuidingPulse, GuiderListener { - - private val guider = MultiStarGuider() - private val guideCamera = AtomicReference() - private val guideMount = AtomicReference() - private val guideOutput = AtomicReference() - private val jobExecutionCounter = AtomicInteger(1) - private val stepExecutionCounter = AtomicInteger(1) - private val runningJob = AtomicReference>() - private val randomDither = RandomDither() - private val spiralDither = SpiralDither() - - private val xGuideAlgorithms = EnumMap(GuideAlgorithmType::class.java) - private val yGuideAlgorithms = EnumMap(GuideAlgorithmType::class.java) - - init { - xGuideAlgorithms[GuideAlgorithmType.HYSTERESIS] = HysteresisGuideAlgorithm(GuideAxis.RA_X) - xGuideAlgorithms[GuideAlgorithmType.LOW_PASS] = LowPassGuideAlgorithm(GuideAxis.RA_X) - xGuideAlgorithms[GuideAlgorithmType.RESIST_SWITCH] = ResistSwitchGuideAlgorithm(GuideAxis.RA_X) - - yGuideAlgorithms[GuideAlgorithmType.HYSTERESIS] = HysteresisGuideAlgorithm(GuideAxis.DEC_Y) - yGuideAlgorithms[GuideAlgorithmType.LOW_PASS] = LowPassGuideAlgorithm(GuideAxis.DEC_Y) - yGuideAlgorithms[GuideAlgorithmType.RESIST_SWITCH] = ResistSwitchGuideAlgorithm(GuideAxis.DEC_Y) - } - - @PostConstruct - private fun initialize() { - guider.camera = this - guider.mount = this - guider.pulse = this - guider.registerListener(this) - } - - val stats - get() = guider.stats - - val lockPosition - get() = guider.lockPosition - - val primaryStar - get() = guider.primaryStar - - override val binning - get() = guideCamera.get()?.binX ?: 1 - - override val pixelScale - get() = guideCamera.get()?.pixelSizeX ?: 1.0 - - override val exposureTime: Long - get() = TODO("Not yet implemented") - - override val isBusy: Boolean - get() = TODO("Not yet implemented") - - override val rightAscension - get() = guideMount.get()?.rightAscension ?: 0.0 - - override val declination - get() = guideMount.get()?.declination ?: 0.0 - - override val rightAscensionGuideRate: Double - get() = TODO("Not yet implemented") - - override val declinationGuideRate: Double - get() = TODO("Not yet implemented") - - override val isPierSideAtEast - get() = guideMount.get()?.pierSide == PierSide.EAST - - override fun guideNorth(duration: Int): Boolean { - val guideOutput = guideOutput.get() ?: return false - guideOutput.guideNorth(duration) - LOG.info("guiding north. device={}, duration={} ms", guideOutput.name, duration) - return true - } - - override fun guideSouth(duration: Int): Boolean { - val guideOutput = guideOutput.get() ?: return false - guideOutput.guideSouth(duration) - LOG.info("guiding south. device={}, duration={} ms", guideOutput.name, duration) - return true - } - - override fun guideWest(duration: Int): Boolean { - val guideOutput = guideOutput.get() ?: return false - guideOutput.guideWest(duration) - LOG.info("guiding west. device={}, duration={} ms", guideOutput.name, duration) - return true - } - - override fun guideEast(duration: Int): Boolean { - val guideOutput = guideOutput.get() ?: return false - guideOutput.guideEast(duration) - LOG.info("guiding east. device={}, duration={} ms", guideOutput.name, duration) - return true - } - - // TODO: Ajustar quando implementar o Rotator. - override val angle = 0.0 - - @Synchronized - fun startLooping( - camera: Camera, mount: Mount, guideOutput: GuideOutput, - guideStartLooping: GuideStartLooping, - ) { - if (isLooping()) return - if (!camera.connected) return - - guideCamera.set(camera) - guideMount.set(mount) - this.guideOutput.set(guideOutput) - - with(guider) { - searchRegion = guideStartLooping.searchRegion - dither = if (guideStartLooping.ditherMode == DitherMode.RANDOM) randomDither else spiralDither - ditherAmount = guideStartLooping.ditherAmount - ditherRAOnly = guideStartLooping.ditherRAOnly - calibrationFlipRequiresDecFlip = guideStartLooping.calibrationFlipRequiresDecFlip - assumeDECOrthogonalToRA = guideStartLooping.assumeDECOrthogonalToRA - calibrationStep = guideStartLooping.calibrationStep - calibrationDistance = guideStartLooping.calibrationDistance - useDECCompensation = guideStartLooping.useDECCompensation - declinationGuideMode = guideStartLooping.declinationGuideMode - maxDECDuration = guideStartLooping.maxDECDuration - maxRADuration = guideStartLooping.maxRADuration - noiseReductionMethod = guideStartLooping.noiseReductionMethod - xGuideAlgorithm = xGuideAlgorithms[guideStartLooping.xGuideAlgorithm] - yGuideAlgorithm = yGuideAlgorithms[guideStartLooping.yGuideAlgorithm] - } - - camera.enableBlob() - - val guidingTasklet = GuidingTasklet(camera, guider, guideStartLooping) - - val guidingStep = StepBuilder("GuidingStep.${camera.name}.${stepExecutionCounter.getAndIncrement()}", jobRepository) - .tasklet(guidingTasklet, platformTransactionManager) - // .listener(this) - .build() - - val job = JobBuilder("GuidingJob.${jobExecutionCounter.getAndIncrement()}", jobRepository) - .start(guidingStep) - // .listener(this) - .listener(guidingTasklet) - .build() - - asyncJobLauncher.run(job, JobParameters()) - .also { runningJob.set(job to it) } - .also { jobRegistry.register(ReferenceJobFactory(job)) } - } - - fun isLooping(): Boolean { - return runningJob.get()?.second?.isRunning ?: false - } - - fun isGuiding(): Boolean { - return guider.isGuiding - } - - fun selectGuideStar(x: Double, y: Double) { - guider.selectGuideStar(x, y) - } - - fun deselectGuideStar() { - guider.deselectGuideStar() - } - - @Synchronized - fun startGuiding(forceCalibration: Boolean = false) { - if (guider.isGuiding) return - if (!guideMount.get().connected || !guideOutput.get().connected) return - - val calibration = guideCalibrationRepository - .get(guideCamera.get(), guideMount.get(), guideOutput.get()) - ?.toGuideCalibration() - - if (forceCalibration || calibration == null) { - LOG.info("starting guiding with force calibration") - guider.clearCalibration() - } else { - LOG.info("calibration restored. calibration={}", calibration) - guider.loadCalibration(calibration) - } - - guider.startGuiding() - } - - @Synchronized - fun stop() { - val jobExecution = runningJob.get()?.second ?: return - jobOperator.stop(jobExecution.jobId) - } - - override fun onLockPositionChanged(position: GuidePoint) { - messageService.sendMessage(GUIDE_LOCK_POSITION_CHANGED, guider) - } - - override fun onStarSelected(star: StarPoint) {} - - override fun onGuidingDithered(dx: Double, dy: Double) {} - - override fun onGuidingStopped() {} - - override fun onLockShiftLimitReached() {} - - override fun onLooping(image: Image, number: Int, star: StarPoint?) {} - - override fun onStarLost() { - messageService.sendMessage(GUIDE_STAR_LOST, guider) - } - - override fun onLockPositionLost() { - messageService.sendMessage(GUIDE_LOCK_POSITION_LOST, guider) - } - - override fun onStartCalibration() {} - - override fun onCalibrationStep( - calibrationState: CalibrationState, direction: GuideDirection, stepNumber: Int, - dx: Double, dy: Double, - posX: Double, posY: Double, - distance: Double, - ) { - } - - override fun onCalibrationCompleted(calibration: GuideCalibration) { - guideCalibrationRepository - .save(GuideCalibrationEntity.from(guideCamera.get(), guideMount.get(), guideOutput.get(), calibration)) - } - - override fun onCalibrationFailed() {} - - override fun onGuideStep(stats: GuideStats) {} - - override fun onNotifyDirectMove(mount: GuidePoint) {} - - companion object { - - @JvmStatic private val LOG = loggerFor() - - const val GUIDE_EXPOSURE_FINISHED = "GUIDE_EXPOSURE_FINISHED" - const val GUIDE_LOCK_POSITION_CHANGED = "GUIDE_LOCK_POSITION_CHANGED" - const val GUIDE_STAR_LOST = "GUIDE_STAR_LOST" - const val GUIDE_LOCK_POSITION_LOST = "GUIDE_LOCK_POSITION_LOST" - } -} diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuidingService.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuidingService.kt index 58cb685c1..b9fd6763d 100644 --- a/api/src/main/kotlin/nebulosa/api/guiding/GuidingService.kt +++ b/api/src/main/kotlin/nebulosa/api/guiding/GuidingService.kt @@ -1,102 +1,120 @@ package nebulosa.api.guiding -import nebulosa.api.data.responses.GuidingChartResponse -import nebulosa.api.data.responses.GuidingStarResponse -import nebulosa.api.image.ImageService +import jakarta.annotation.PreDestroy import nebulosa.api.services.MessageService -import nebulosa.indi.device.camera.Camera -import nebulosa.indi.device.guide.GuideOutput -import nebulosa.indi.device.mount.Mount +import nebulosa.guiding.GuideStar +import nebulosa.guiding.GuideState +import nebulosa.guiding.Guider +import nebulosa.guiding.GuiderListener +import nebulosa.phd2.client.PHD2Client +import nebulosa.phd2.client.PHD2EventListener +import nebulosa.phd2.client.commands.PHD2Command +import nebulosa.phd2.client.events.PHD2Event import org.springframework.stereotype.Service -import kotlin.math.hypot +import kotlin.time.Duration @Service class GuidingService( private val messageService: MessageService, - private val imageService: ImageService, - private val guidingExecutor: GuidingExecutor, -) { + private val phd2Client: PHD2Client, + private val guider: Guider, +) : PHD2EventListener, GuiderListener { - // fun onGuideExposureFinished(event: GuideExposureFinished) { - // imageService.load(event.task.token, event.image) - // guideImage.set(event.image) - // sendGuideExposureFinished(event) - // } + private val guideHistory = GuideStepHistory() - fun connect(guideOutput: GuideOutput) { - guideOutput.connect() + @Synchronized + fun connect(host: String, port: Int) { + check(!phd2Client.isOpen) + + phd2Client.open(host, port) + phd2Client.registerListener(this) + guider.registerGuiderListener(this) + messageService.sendMessage(GUIDER_CONNECTED) + } + + @PreDestroy + @Synchronized + fun disconnect() { + runCatching { guider.close() } + phd2Client.unregisterListener(this) + messageService.sendMessage(GUIDER_DISCONNECTED) + } + + fun status(): GuiderStatus { + return if (!phd2Client.isOpen) GuiderStatus.DISCONNECTED + else GuiderStatus(phd2Client.isOpen, guider.state, guider.isSettling, guider.pixelScale) + } + + fun history(): List { + return guideHistory + } + + fun latestHistory(): HistoryStep? { + return guideHistory.lastOrNull() + } + + fun clearHistory() { + return guideHistory.clear() + } + + fun loop(autoSelectGuideStar: Boolean = true) { + if (phd2Client.isOpen) { + guider.startLooping(autoSelectGuideStar) + } } - fun disconnect(guideOutput: GuideOutput) { - guideOutput.disconnect() + fun start(forceCalibration: Boolean = false) { + if (phd2Client.isOpen) { + guider.startGuiding(forceCalibration) + } } - fun startLooping( - camera: Camera, mount: Mount, guideOutput: GuideOutput, - guideStartLooping: GuideStartLooping, - ) { - guidingExecutor.startLooping(camera, mount, guideOutput, guideStartLooping) + fun settle(settleAmount: Double?, settleTime: Duration?, settleTimeout: Duration?) { + if (settleAmount != null) guider.settleAmount = settleAmount + if (settleTime != null) guider.settleTime = settleTime + if (settleTimeout != null) guider.settleTimeout = settleTimeout + } + + fun dither(amount: Double, raOnly: Boolean = false) { + if (phd2Client.isOpen) { + guider.dither(amount, raOnly) + } } fun stop() { - guidingExecutor.stop() - } - - fun startGuiding(forceCalibration: Boolean) { - guidingExecutor.startGuiding(forceCalibration) - } - - fun guidingChart(): GuidingChartResponse { - val chart = guidingExecutor.stats - val stats = chart.lastOrNull() - val rmsTotal = if (stats == null) 0.0 else hypot(stats.rmsRA, stats.rmsDEC) - return GuidingChartResponse(chart, stats?.rmsRA ?: 0.0, stats?.rmsDEC ?: 0.0, rmsTotal) - } - - fun guidingStar(): GuidingStarResponse? { -// val image = guideImage.get() ?: return null -// val lockPosition = guidingExecutor.lockPosition -// val trackBoxSize = guidingExecutor.searchRegion * 2.0 -// -// return if (lockPosition.valid) { -// val size = min(trackBoxSize, 64.0) -// -// val centerX = (lockPosition.x - size / 2).toInt() -// val centerY = (lockPosition.y - size / 2).toInt() -// val transformedImage = image.transform(SubFrame(centerX, centerY, size.toInt(), size.toInt()), AutoScreenTransformFunction) -// -// val fwhm = FWHM(guidingExecutor.primaryStar) -// val computedFWHM = fwhm.compute(transformedImage) -// -// val output = Base64OutputStream(128) -// ImageIO.write(transformedImage.transform(fwhm), "PNG", output) -// -// GuidingStarResponse( -// "data:image/png;base64," + output.base64(), -// guidingExecutor.lockPosition.x, guidingExecutor.lockPosition.y, -// guidingExecutor.primaryStar.x, guidingExecutor.primaryStar.y, -// guidingExecutor.primaryStar.peak, -// computedFWHM, -// guidingExecutor.primaryStar.hfd, -// guidingExecutor.primaryStar.snr, -// ) -// } else { -// null -// } - - return null - } - - fun selectGuideStar(x: Double, y: Double) { - guidingExecutor.selectGuideStar(x, y) - } - - fun deselectGuideStar() { - guidingExecutor.deselectGuideStar() + if (phd2Client.isOpen) { + guider.stopGuiding(true) + } + } + + override fun onStateChanged(state: GuideState, pixelScale: Double) { + val status = GuiderStatus(phd2Client.isOpen, state, guider.isSettling, pixelScale) + messageService.sendMessage(GUIDER_UPDATED, status) + } + + override fun onGuideStepped(guideStar: GuideStar) { + val payload = guideStar.guideStep?.let(guideHistory::addGuideStep) ?: guideStar + messageService.sendMessage(GUIDER_STEPPED, payload) } + override fun onDithered(dx: Double, dy: Double) { + guideHistory.addDither(dx, dy) + } + + override fun onMessageReceived(message: String) { + messageService.sendMessage(GUIDER_MESSAGE_RECEIVED, "message" to message) + } + + override fun onEventReceived(event: PHD2Event) {} + + override fun onCommandProcessed(command: PHD2Command, result: T?, error: String?) {} + companion object { - const val GUIDE_EXPOSURE_FINISHED = "GUIDE_EXPOSURE_FINISHED" + const val GUIDER_CONNECTED = "GUIDER_CONNECTED" + const val GUIDER_DISCONNECTED = "GUIDER_DISCONNECTED" + const val GUIDER_UPDATED = "GUIDER_UPDATED" + const val GUIDER_STEPPED = "GUIDER_STEPPED" + const val GUIDER_MESSAGE_RECEIVED = "GUIDER_MESSAGE_RECEIVED" } } diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuidingTasklet.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuidingTasklet.kt deleted file mode 100644 index 37e9d548a..000000000 --- a/api/src/main/kotlin/nebulosa/api/guiding/GuidingTasklet.kt +++ /dev/null @@ -1,78 +0,0 @@ -package nebulosa.api.guiding - -import nebulosa.api.cameras.CameraStartCapture -import nebulosa.api.cameras.CameraExposureTasklet -import nebulosa.api.sequencer.tasklets.delay.DelayTasklet -import nebulosa.guiding.Guider -import nebulosa.imaging.Image -import nebulosa.indi.device.camera.Camera -import nom.tam.fits.Header -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.core.step.tasklet.StoppableTasklet -import org.springframework.batch.repeat.RepeatStatus -import java.nio.file.Path -import java.util.concurrent.LinkedBlockingQueue -import kotlin.system.measureTimeMillis -import kotlin.time.Duration - -data class GuidingTasklet( - private val camera: Camera, - private val guider: Guider, - private val startLooping: GuideStartLooping, -) : StoppableTasklet, JobExecutionListener { - - private val startCapture = CameraStartCapture(savePath = Path.of("@guiding"), saveInMemory = true) - - private val cameraExposureTasklet = CameraExposureTasklet(startCapture) - private val delayTasklet = DelayTasklet(Duration.ZERO) - private val guideImage = LinkedBlockingQueue() - - override fun execute(contribution: StepContribution, chunkContext: ChunkContext): RepeatStatus { - cameraExposureTasklet.execute(contribution, chunkContext) - - val image = guideImage.take() - - return if (image === DUMMY_IMAGE) { - RepeatStatus.FINISHED - } else { - val elapsedTime = measureTimeMillis { guider.processImage(image) } - val waitTime = startCapture.exposureDelayInSeconds * 1000L - elapsedTime // TODO: FIX ME - - if (waitTime in 100L..60000L) { - contribution.stepExecution.executionContext.putLong(DelayTasklet.DELAY_TIME_NAME, waitTime) - delayTasklet.execute(contribution, chunkContext) - } - - RepeatStatus.CONTINUABLE - } - } - - override fun stop() { - guider.stopGuiding() - guideImage.offer(DUMMY_IMAGE) - cameraExposureTasklet.stop() - } - - override fun beforeJob(jobExecution: JobExecution) { - cameraExposureTasklet.beforeJob(jobExecution) - } - - override fun afterJob(jobExecution: JobExecution) { - cameraExposureTasklet.afterJob(jobExecution) - } - -// override fun onCameraExposureFinished(camera: Camera, image: Image?, path: Path?) { -// if (image != null) { -// guideImage.offer(image) -// listener?.onCameraExposureFinished(camera, image, path) -// } -// } - - companion object { - - @JvmStatic private val DUMMY_IMAGE = Image(1, 1, Header(), true) - } -} diff --git a/api/src/main/kotlin/nebulosa/api/guiding/HistoryStep.kt b/api/src/main/kotlin/nebulosa/api/guiding/HistoryStep.kt new file mode 100644 index 000000000..a4f178d70 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/guiding/HistoryStep.kt @@ -0,0 +1,13 @@ +package nebulosa.api.guiding + +import nebulosa.guiding.GuideStep + +data class HistoryStep( + val id: Long = 0L, + val rmsRA: Double = 0.0, + val rmsDEC: Double = 0.0, + val rmsTotal: Double = 0.0, + val guideStep: GuideStep? = null, + val ditherX: Double = 0.0, + val ditherY: Double = 0.0, +) diff --git a/api/src/main/kotlin/nebulosa/api/guiding/RMS.kt b/api/src/main/kotlin/nebulosa/api/guiding/RMS.kt new file mode 100644 index 000000000..573973cdd --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/guiding/RMS.kt @@ -0,0 +1,76 @@ +package nebulosa.api.guiding + +import kotlin.math.abs +import kotlin.math.hypot +import kotlin.math.max +import kotlin.math.sqrt + +class RMS { + + private var sumRA = 0.0 + private var sumRASquared = 0.0 + private var sumDEC = 0.0 + private var sumDECSquared = 0.0 + + var size = 0 + private set + + var rightAscension = 0.0 + private set + + var declination = 0.0 + private set + + var total = 0.0 + private set + + var peakRA = 0.0 + private set + + var peakDEC = 0.0 + private set + + fun addDataPoint(raDistance: Double, decDistance: Double) { + size++ + + sumRA += raDistance + sumRASquared += raDistance * raDistance + sumDEC += decDistance + sumDECSquared += decDistance * decDistance + + peakRA = max(peakRA, abs(raDistance)) + peakDEC = max(peakDEC, abs(decDistance)) + + computeRMS() + } + + fun removeDataPoint(raDistance: Double, decDistance: Double) { + size-- + + sumRA -= raDistance + sumRASquared -= raDistance * raDistance + sumDEC -= decDistance + sumDECSquared -= decDistance * decDistance + + computeRMS() + } + + private fun computeRMS() { + rightAscension = sqrt(size * sumRASquared - sumRA * sumRA) / size + declination = sqrt(size * sumDECSquared - sumDEC * sumDEC) / size + total = hypot(rightAscension, declination) + } + + fun clear() { + size = 0 + sumRA = 0.0 + sumRASquared = 0.0 + sumDEC = 0.0 + sumDECSquared = 0.0 + rightAscension = 0.0 + declination = 0.0 + total = 0.0 + peakRA = 0.0 + peakDEC = 0.0 + } +} diff --git a/api/src/main/kotlin/nebulosa/api/guiding/WaitForSettleTasklet.kt b/api/src/main/kotlin/nebulosa/api/guiding/WaitForSettleTasklet.kt new file mode 100644 index 000000000..af47a6808 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/guiding/WaitForSettleTasklet.kt @@ -0,0 +1,24 @@ +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/ImageService.kt b/api/src/main/kotlin/nebulosa/api/image/ImageService.kt index 8f8797ac0..9e92faded 100644 --- a/api/src/main/kotlin/nebulosa/api/image/ImageService.kt +++ b/api/src/main/kotlin/nebulosa/api/image/ImageService.kt @@ -18,6 +18,7 @@ import nebulosa.platesolving.astrometrynet.LocalAstrometryNetPlateSolver import nebulosa.platesolving.astrometrynet.NovaAstrometryNetPlateSolver import nebulosa.platesolving.watney.WatneyPlateSolver import nebulosa.sbd.SmallBodyDatabaseService +import nebulosa.simbad.SimbadCatalogType import nebulosa.simbad.SimbadService import nebulosa.simbad.SimbadSkyCatalog import nebulosa.skycatalog.ClassificationType @@ -196,6 +197,10 @@ class ImageService( catalog.search(calibration.rightAscension, calibration.declination, calibration.radius, types) for (entry in catalog) { + if (SimbadCatalogType.entries.none { it.matches(entry.name) }) { + continue + } + val (x, y) = wcs.skyToPix(entry.rightAscensionJ2000, entry.declinationJ2000) val annotation = if (entry.type.classification == ClassificationType.STAR) ImageAnnotation(x, y, star = entry) else ImageAnnotation(x, y, dso = entry) diff --git a/api/src/main/kotlin/nebulosa/api/mounts/MountController.kt b/api/src/main/kotlin/nebulosa/api/mounts/MountController.kt index 4fc7460af..7bec6fca0 100644 --- a/api/src/main/kotlin/nebulosa/api/mounts/MountController.kt +++ b/api/src/main/kotlin/nebulosa/api/mounts/MountController.kt @@ -64,8 +64,8 @@ class MountController( mountService.sync(mount, rightAscension.hours, declination.deg, j2000) } - @PutMapping("{mount}/slew-to") - fun slewTo( + @PutMapping("{mount}/slew") + fun slew( @EntityBy mount: Mount, @RequestParam @Valid @NotBlank rightAscension: String, @RequestParam @Valid @NotBlank declination: String, diff --git a/api/src/main/kotlin/nebulosa/api/mounts/MountConverter.kt b/api/src/main/kotlin/nebulosa/api/mounts/MountConverter.kt index a557a30c8..e62ae99ad 100644 --- a/api/src/main/kotlin/nebulosa/api/mounts/MountConverter.kt +++ b/api/src/main/kotlin/nebulosa/api/mounts/MountConverter.kt @@ -3,7 +3,7 @@ package nebulosa.api.mounts import com.fasterxml.jackson.core.JsonGenerator import com.fasterxml.jackson.databind.SerializerProvider import nebulosa.indi.device.mount.Mount -import nebulosa.json.modules.ToJson +import nebulosa.json.ToJson import nebulosa.math.AngleFormatter import nebulosa.math.format import nebulosa.math.toDegrees diff --git a/api/src/main/kotlin/nebulosa/api/mounts/MountService.kt b/api/src/main/kotlin/nebulosa/api/mounts/MountService.kt index 4bf0b7cba..b273424d1 100644 --- a/api/src/main/kotlin/nebulosa/api/mounts/MountService.kt +++ b/api/src/main/kotlin/nebulosa/api/mounts/MountService.kt @@ -96,10 +96,10 @@ class MountService(private val imageBucket: ImageBucket) { fun move(mount: Mount, direction: GuideDirection, enabled: Boolean) { when (direction) { - GuideDirection.UP_NORTH -> moveNorth(mount, enabled) - GuideDirection.DOWN_SOUTH -> moveSouth(mount, enabled) - GuideDirection.LEFT_WEST -> moveWest(mount, enabled) - GuideDirection.RIGHT_EAST -> moveEast(mount, enabled) + GuideDirection.NORTH -> moveNorth(mount, enabled) + GuideDirection.SOUTH -> moveSouth(mount, enabled) + GuideDirection.WEST -> moveWest(mount, enabled) + GuideDirection.EAST -> moveEast(mount, enabled) } } diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/DelayEvent.kt b/api/src/main/kotlin/nebulosa/api/sequencer/DelayEvent.kt new file mode 100644 index 000000000..792fea382 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/sequencer/DelayEvent.kt @@ -0,0 +1,14 @@ +package nebulosa.api.sequencer + +import kotlin.time.Duration + +interface DelayEvent { + + val remainingTime: Duration + + val delayTime: Duration + + val waitTime: Duration + + val progress: Double +} diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/SequenceFlowFactory.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SequenceFlowFactory.kt new file mode 100644 index 000000000..6eebacd37 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/sequencer/SequenceFlowFactory.kt @@ -0,0 +1,66 @@ +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 new file mode 100644 index 000000000..441e85cd2 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/sequencer/SequenceFlowStepFactory.kt @@ -0,0 +1,28 @@ +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 index cbf1ea9bd..b9fd3d3fd 100644 --- a/api/src/main/kotlin/nebulosa/api/sequencer/SequenceJob.kt +++ b/api/src/main/kotlin/nebulosa/api/sequencer/SequenceJob.kt @@ -4,11 +4,13 @@ import nebulosa.indi.device.Device import org.springframework.batch.core.Job import org.springframework.batch.core.JobExecution -data class SequenceJob( - val devices: List, - val job: Job, - val jobExecution: JobExecution, -) { +interface SequenceJob { + + val devices: List + + val job: Job + + val jobExecution: JobExecution val jobId get() = jobExecution.jobId diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/SequenceJobConverter.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SequenceJobConverter.kt index 2d4715bb4..e6676fdf8 100644 --- a/api/src/main/kotlin/nebulosa/api/sequencer/SequenceJobConverter.kt +++ b/api/src/main/kotlin/nebulosa/api/sequencer/SequenceJobConverter.kt @@ -2,7 +2,7 @@ package nebulosa.api.sequencer import com.fasterxml.jackson.core.JsonGenerator import com.fasterxml.jackson.databind.SerializerProvider -import nebulosa.json.modules.ToJson +import nebulosa.json.ToJson import org.springframework.stereotype.Component import java.time.ZoneOffset diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/SequenceJobExecutor.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SequenceJobExecutor.kt index 1e49d397e..7ee1e65a2 100644 --- a/api/src/main/kotlin/nebulosa/api/sequencer/SequenceJobExecutor.kt +++ b/api/src/main/kotlin/nebulosa/api/sequencer/SequenceJobExecutor.kt @@ -2,12 +2,12 @@ package nebulosa.api.sequencer import nebulosa.indi.device.Device -interface SequenceJobExecutor : Iterable { +interface SequenceJobExecutor : Iterable { - fun execute(data: T): SequenceJob + fun execute(request: T): J - fun sequenceTaskFor(vararg devices: Device): SequenceJob? { - fun find(task: SequenceJob): Boolean { + 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 @@ -19,4 +19,8 @@ interface SequenceJobExecutor : Iterable { 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 new file mode 100644 index 000000000..b388516ce --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/sequencer/SequenceJobFactory.kt @@ -0,0 +1,72 @@ +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 +import kotlin.time.Duration.Companion.seconds + +@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.exposureDelayInSeconds.seconds) + 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/SequenceStepFactory.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SequenceStepFactory.kt new file mode 100644 index 000000000..a7dcd2931 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/sequencer/SequenceStepFactory.kt @@ -0,0 +1,64 @@ +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/SequenceTaskletEvent.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SequenceTaskletEvent.kt new file mode 100644 index 000000000..919b377e6 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/sequencer/SequenceTaskletEvent.kt @@ -0,0 +1,8 @@ +package nebulosa.api.sequencer + +import org.springframework.batch.core.step.tasklet.Tasklet + +interface SequenceTaskletEvent { + + val tasklet: Tasklet +} diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/SequenceTaskletFactory.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SequenceTaskletFactory.kt new file mode 100644 index 000000000..0ba49fdf5 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/sequencer/SequenceTaskletFactory.kt @@ -0,0 +1,52 @@ +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 kotlin.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/SubjectSequenceTasklet.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SubjectSequenceTasklet.kt index 963470679..1bc664f23 100644 --- a/api/src/main/kotlin/nebulosa/api/sequencer/SubjectSequenceTasklet.kt +++ b/api/src/main/kotlin/nebulosa/api/sequencer/SubjectSequenceTasklet.kt @@ -5,9 +5,11 @@ 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 SubjectSequenceTasklet(@JvmField protected val subject: Subject) : SequenceTasklet, Closeable { +abstract class SubjectSequenceTasklet(@JvmField protected val subject: Subject) : SequenceTasklet, Closeable { constructor() : this(PublishSubject.create()) @@ -25,6 +27,7 @@ abstract class SubjectSequenceTasklet(@JvmField protected val subject: @Synchronized final override fun onNext(event: T) { + LOG.debug { "$event" } subject.onNext(event) } @@ -41,4 +44,9 @@ abstract class SubjectSequenceTasklet(@JvmField protected val subject: final override fun close() { onComplete() } + + companion object { + + @JvmStatic private val LOG = loggerFor>() + } } 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 index 7e274c70c..81a4c2ed7 100644 --- a/api/src/main/kotlin/nebulosa/api/sequencer/tasklets/delay/DelayElapsed.kt +++ b/api/src/main/kotlin/nebulosa/api/sequencer/tasklets/delay/DelayElapsed.kt @@ -1,9 +1,26 @@ package nebulosa.api.sequencer.tasklets.delay +import com.fasterxml.jackson.annotation.JsonIgnore +import nebulosa.api.sequencer.DelayEvent +import nebulosa.api.sequencer.SequenceStepEvent +import nebulosa.api.sequencer.SequenceTaskletEvent +import org.springframework.batch.core.StepExecution import kotlin.time.Duration data class DelayElapsed( - val remainingTime: Duration, - val delayTime: Duration, - val waitTime: Duration, -) + override val remainingTime: Duration, + override val delayTime: Duration, + override val waitTime: Duration, + @JsonIgnore override val stepExecution: StepExecution, + @JsonIgnore override val tasklet: DelayTasklet, +) : SequenceStepEvent, SequenceTaskletEvent, DelayEvent { + + override val progress + get() = if (remainingTime > Duration.ZERO) 1.0 - delayTime / remainingTime else 1.0 + + inline val isStarted + get() = remainingTime == delayTime + + inline val isFinished + 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 index e6faece33..dfe95d9cf 100644 --- a/api/src/main/kotlin/nebulosa/api/sequencer/tasklets/delay/DelayTasklet.kt +++ b/api/src/main/kotlin/nebulosa/api/sequencer/tasklets/delay/DelayTasklet.kt @@ -16,7 +16,8 @@ data class DelayTasklet(private val duration: Duration) : SubjectSequenceTasklet private val aborted = AtomicBoolean() override fun execute(contribution: StepContribution, chunkContext: ChunkContext): RepeatStatus { - val delayTimeInMilliseconds = contribution.stepExecution.executionContext + val stepExecution = contribution.stepExecution + val delayTimeInMilliseconds = stepExecution.executionContext .getLong(DELAY_TIME_NAME, duration.inWholeMilliseconds) val delayTime = delayTimeInMilliseconds.milliseconds @@ -27,13 +28,13 @@ data class DelayTasklet(private val duration: Duration) : SubjectSequenceTasklet val waitTime = min(remainingTime, DELAY_INTERVAL) if (waitTime > 0) { - onNext(DelayElapsed(remainingTime.milliseconds, delayTime, waitTime.milliseconds)) + onNext(DelayElapsed(remainingTime.milliseconds, delayTime, waitTime.milliseconds, stepExecution, this)) Thread.sleep(waitTime) remainingTime -= waitTime } } - onNext(DelayElapsed(Duration.ZERO, delayTime, Duration.ZERO)) + onNext(DelayElapsed(Duration.ZERO, delayTime, Duration.ZERO, stepExecution, this)) } return RepeatStatus.FINISHED diff --git a/api/src/main/kotlin/nebulosa/api/services/MessageService.kt b/api/src/main/kotlin/nebulosa/api/services/MessageService.kt index bd07585d4..cc54974b1 100644 --- a/api/src/main/kotlin/nebulosa/api/services/MessageService.kt +++ b/api/src/main/kotlin/nebulosa/api/services/MessageService.kt @@ -1,20 +1,34 @@ package nebulosa.api.services +import com.fasterxml.jackson.databind.ObjectMapper +import nebulosa.log.debug +import nebulosa.log.loggerFor import org.springframework.messaging.simp.SimpMessagingTemplate import org.springframework.stereotype.Service @Service -class MessageService(private val simpleMessageTemplate: SimpMessagingTemplate) { +class MessageService( + private val simpleMessageTemplate: SimpMessagingTemplate, + private val objectMapper: ObjectMapper, +) { fun sendMessage(eventName: String, payload: Any) { + LOG.debug { "$eventName: $payload" } simpleMessageTemplate.convertAndSend(eventName, payload) } - fun sendMessage(eventName: String, vararg attributes: Pair) { - sendMessage(eventName, mapOf(*attributes)) + fun sendMessage(eventName: String, vararg attributes: Pair) { + val payload = objectMapper.createObjectNode() + attributes.forEach { payload.putPOJO(it.first, it.second) } + sendMessage(eventName, payload) } fun sendMessage(event: MessageEvent) { sendMessage(event.eventName, event) } + + companion object { + + @JvmStatic private val LOG = loggerFor() + } } diff --git a/api/src/main/kotlin/nebulosa/api/wheels/WheelConverter.kt b/api/src/main/kotlin/nebulosa/api/wheels/WheelConverter.kt index 3dfc2f7b4..c0327b77c 100644 --- a/api/src/main/kotlin/nebulosa/api/wheels/WheelConverter.kt +++ b/api/src/main/kotlin/nebulosa/api/wheels/WheelConverter.kt @@ -3,7 +3,7 @@ package nebulosa.api.wheels import com.fasterxml.jackson.core.JsonGenerator import com.fasterxml.jackson.databind.SerializerProvider import nebulosa.indi.device.filterwheel.FilterWheel -import nebulosa.json.modules.ToJson +import nebulosa.json.ToJson import org.springframework.stereotype.Component @Component diff --git a/api/src/test/kotlin/SkyDatabaseGenerator.kt b/api/src/test/kotlin/SkyDatabaseGenerator.kt index eadd817ca..daadf77f9 100644 --- a/api/src/test/kotlin/SkyDatabaseGenerator.kt +++ b/api/src/test/kotlin/SkyDatabaseGenerator.kt @@ -1,10 +1,13 @@ import com.fasterxml.jackson.databind.ObjectMapper +import de.siegmar.fastcsv.reader.NamedCsvReader import de.siegmar.fastcsv.reader.NamedCsvRow import nebulosa.api.atlas.DeepSkyObjectEntity import nebulosa.api.atlas.StarEntity import nebulosa.common.concurrency.CountUpDownLatch +import nebulosa.io.resource import nebulosa.log.loggerFor import nebulosa.math.* +import nebulosa.simbad.SimbadCatalogType import nebulosa.simbad.SimbadService import nebulosa.skycatalog.ClassificationType import nebulosa.skycatalog.SkyObject @@ -12,6 +15,7 @@ import nebulosa.skycatalog.SkyObjectType import nebulosa.time.TimeYMDHMS import nebulosa.time.UTC import okhttp3.OkHttpClient +import java.io.InputStreamReader import java.nio.file.Path import java.util.concurrent.Executors import java.util.concurrent.TimeUnit @@ -20,11 +24,9 @@ import kotlin.io.path.bufferedReader import kotlin.io.path.outputStream import kotlin.math.min -typealias CatalogNameProvider = Pair String> +typealias CatalogNameProvider = Pair String?> -// TODO: Caldwell Catalog // TODO: Herschel Catalog -// TODO: Bennett Catalog: https://www.docdb.net/tutorials/bennett_catalogue.php // TODO: Dunlop Catalog: https://www.docdb.net/tutorials/dunlop_catalogue.php object SkyDatabaseGenerator { @@ -45,62 +47,41 @@ object SkyDatabaseGenerator { @JvmStatic private val SIMBAD_SERVICE = SimbadService(httpClient = HTTP_CLIENT) @JvmStatic private val MAPPER = ObjectMapper() - @JvmStatic private val STAR_CATALOG_TYPES = listOf( - "NAME\\s+(.*)".toRegex() to { groupValues[1].trim() }, - "\\*\\s+(.*)".toRegex() to { groupValues[1].trim() }, - "HD\\s+(\\w*)".toRegex() to { "HD " + groupValues[1].uppercase() }, - "HR\\s+(\\w*)".toRegex() to { "HR " + groupValues[1].uppercase() }, - "HIP\\s+(\\w*)".toRegex() to { "HIP " + groupValues[1].uppercase() }, - "NGC\\s+(\\w{1,5})".toRegex() to { "NGC " + groupValues[1].uppercase() }, - "IC\\s+(\\w{1,5})".toRegex() to { "IC " + groupValues[1].uppercase() }, - ) - - @JvmStatic private val DSO_CATALOG_TYPES = listOf( - STAR_CATALOG_TYPES[0], - "NGC\\s+(\\d{1,4})".toRegex() to { "NGC " + groupValues[1] }, - "IC\\s+(\\d{1,4})".toRegex() to { "IC " + groupValues[1] }, - "GUM\\s+(\\d{1,4})".toRegex() to { "GUM " + groupValues[1] }, - "M\\s+(\\d{1,3})".toRegex() to { "M " + groupValues[1] }, - "Barnard\\s+(\\d{1,3})".toRegex() to { "Barnard " + groupValues[1] }, - "LBN\\s+(\\d{1,4})".toRegex() to { "LBN " + groupValues[1] }, - "LDN\\s+(\\d{1,4})".toRegex() to { "LDN " + groupValues[1] }, - "RCW\\s+(\\d{1,4})".toRegex() to { "RCW " + groupValues[1] }, - "SH\\s+2-(\\d{1,3})".toRegex() to { "SH 2-" + groupValues[1] }, - "Ced\\s+(\\d{1,3})".toRegex() to { "Ced " + groupValues[1] }, - "UGC\\s+(\\d{1,5})".toRegex() to { "UGC " + groupValues[1] }, - "APG\\s+(\\d{1,3})".toRegex() to { "Arp " + groupValues[1] }, - "HCG\\s+(\\d{1,3})".toRegex() to { "HCG " + groupValues[1] }, - "VV\\s+(\\d{1,4})".toRegex() to { "VV " + groupValues[1] }, - "VdBH\\s+(\\d{1,2})".toRegex() to { "VdBH " + groupValues[1] }, - "DWB\\s+(\\d{1,3})".toRegex() to { "DWB " + groupValues[1] }, - "LEDA\\s+(\\d{1,7})".toRegex() to { "LEDA " + groupValues[1] }, - "Cl\\s+([\\w-]+)\\s+(\\d{1,5})".toRegex() to { groupValues[1] + " " + groupValues[2] }, - ) - - @JvmStatic private val DSO_CATALOG_TYPES_LIKE = listOf( - "M %" to Double.NaN, - "NGC %" to Double.NaN, - "IC %" to Double.NaN, - "Cl %" to Double.NaN, - "Gum %" to Double.NaN, - "Barnard %" to Double.NaN, - "LBN %" to Double.NaN, - "LDN %" to Double.NaN, - "RCW %" to Double.NaN, - "SH %" to Double.NaN, - "Ced %" to Double.NaN, - "UGC %" to 16.0, - "APG %" to Double.NaN, - "HCG %" to 16.0, - "VV %" to 16.0, - "VdBH %" to Double.NaN, - "DWB %" to Double.NaN, - "NAME %" to Double.NaN, - ) + @JvmStatic private val STAR_CATALOG_TYPES: List = SimbadCatalogType.entries + .filter { it.isStar } + .map { it.regex to it::match } + + @JvmStatic private val DSO_CATALOG_TYPES: List = SimbadCatalogType.entries + .filter { it.isDSO } + .map { it.regex to it::match } @JvmStatic private val NUMBER_OF_CPUS = Runtime.getRuntime().availableProcessors() @JvmStatic private val EXECUTOR_SERVICE = Executors.newFixedThreadPool(NUMBER_OF_CPUS) + @JvmStatic private val CSV_READER = NamedCsvReader.builder() + .fieldSeparator(',') + .quoteCharacter('"') + .commentCharacter('#') + .skipComments(true) + + @JvmStatic private val CALDWELL = resource("Caldwell.csv")!! + .use { stream -> + CSV_READER.build(InputStreamReader(stream, Charsets.UTF_8)) + .associate { it.getField("NGC number").ifEmpty { it.getField("Common name") } to it.getField("Caldwell number") } + } + + @JvmStatic private val BENNETT = resource("Bennett.csv")!! + .use { stream -> + CSV_READER.build(InputStreamReader(stream, Charsets.UTF_8)) + .associate { it.getField("NGC") to it.getField("Bennett") } + } + + @JvmStatic private val DUNLOP = resource("Dunlop.csv")!! + .use { stream -> + CSV_READER.build(InputStreamReader(stream, Charsets.UTF_8)) + .associate { it.getField("NGC") to it.getField("Dunlop") } + } + @JvmStatic fun main(args: Array) { val names = LinkedHashSet(8) @@ -137,14 +118,24 @@ object SkyDatabaseGenerator { val namesIterator = splittedNames.iterator() while (namesIterator.hasNext()) { - val m = type.first.matchEntire(namesIterator.next()) ?: continue - val name = type.second(m) - names.add(name) + val name = type.second(namesIterator.next()) ?: continue namesIterator.remove() - if (useIAU && type === STAR_CATALOG_TYPES[0] && name in iauNames) { - iauNames.remove(name) - magnitude = iauNamesMagnitude[name]!! + if (names.add(name)) { + if (name in CALDWELL) { + names.add("Caldwell ${CALDWELL[name]}") + } + if (name in BENNETT) { + names.add("Bennett ${BENNETT[name]}") + } + if (name in DUNLOP) { + names.add("Dunlop ${DUNLOP[name]}") + } + + if (useIAU && type === STAR_CATALOG_TYPES[0] && name in iauNames) { + iauNames.remove(name) + magnitude = iauNamesMagnitude[name]!! + } } } } @@ -152,7 +143,7 @@ object SkyDatabaseGenerator { return magnitude } - val currentTime = UTC(TimeYMDHMS(2023, 9, 29, 12)) + val currentTime = UTC(TimeYMDHMS(2023, 10, 5, 12)) val data = HashMap(32000) val skyObjectTypes = HashSet(SkyObjectType.entries.size) @@ -299,7 +290,7 @@ object SkyDatabaseGenerator { .use { MAPPER.writeValue(it, data.values) } } - // DSOS. ~28201 objects. + // DSOS. ~30263 objects. if (fetchDSOs) { data.clear() @@ -307,8 +298,20 @@ object SkyDatabaseGenerator { val latch = CountUpDownLatch() + val catalogTypes = listOf( + "M %" to Double.NaN, "NGC %" to Double.NaN, + "IC %" to Double.NaN, "Cl %" to Double.NaN, + "Gum %" to Double.NaN, "Barnard %" to Double.NaN, + "LBN %" to Double.NaN, "LDN %" to Double.NaN, + "RCW %" to Double.NaN, "SH %" to Double.NaN, + "Ced %" to Double.NaN, "UGC %" to 18.0, + "APG %" to Double.NaN, "HCG %" to 18.0, + "VV %" to 16.0, "VdBH %" to Double.NaN, + "DWB %" to Double.NaN, "NAME %" to Double.NaN, + ) + for (i in 0 until maxOID step stepSize) { - for (catalogType in DSO_CATALOG_TYPES_LIKE) { + for (catalogType in catalogTypes) { latch.countUp() EXECUTOR_SERVICE.submit { @@ -330,7 +333,7 @@ object SkyDatabaseGenerator { continue } - val isStarDSO = catalogType !== DSO_CATALOG_TYPES_LIKE.last() + val isStarDSO = catalogType !== catalogTypes.last() synchronized(data) { for (row in rows) { diff --git a/api/src/test/resources/Bennett.csv b/api/src/test/resources/Bennett.csv new file mode 100644 index 000000000..364d80f3a --- /dev/null +++ b/api/src/test/resources/Bennett.csv @@ -0,0 +1,153 @@ +Bennett,NGC +1,NGC 55 +2,NGC 104 +3,NGC 247 +4,NGC 253 +5,NGC 288 +6,NGC 300 +7,NGC 362 +8,NGC 613 +9,NGC 1068 +10,NGC 1097 +10A,NGC 1232 +11,NGC 1261 +12,NGC 1291 +13,NGC 1313 +14,NGC 1316 +14A,NGC 1350 +15,NGC 1360 +16,NGC 1365 +17,NGC 1380 +18,NGC 1387 +19,NGC 1399 +19A,NGC 1398 +20,NGC 1404 +21,NGC 1433 +21A,NGC 1512 +22,NGC 1535 +23,NGC 1549 +24,NGC 1553 +25,NGC 1566 +25A,NGC 1617 +26,NGC 1672 +27,NGC 1763 +28,NGC 1783 +29,NGC 1792 +30,NGC 1818 +31,NGC 1808 +32,NGC 1851 +33,NGC 1866 +34,NGC 1904 +35,NGC 2070 +36,NGC 2214 +36A,NGC 2243 +37,NGC 2298 +37A,NGC 2467 +38,NGC 2489 +39,NGC 2506 +40,NGC 2627 +40A,NGC 2671 +41,NGC 2808 +41A,NGC 2972 +41B,NGC 2997 +42,NGC 3115 +43,NGC 3132 +44,NGC 3201 +45,NGC 3242 +46,NGC 3621 +47,Melotte 105 +48,NGC 3960 +49,NGC 3923 +50,NGC 4372 +51,NGC 4590 +52,NGC 4594 +53,NGC 4697 +54,NGC 4699 +55,NGC 4753 +56,NGC 4833 +57,NGC 4945 +58,NGC 4976 +59,NGC 5061 +59A,NGC 5068 +60,NGC 5128 +61,NGC 5139 +62,NGC 5189 +63,NGC 5236 +63A,NGC 5253 +64,NGC 5286 +65,NGC 5617 +66,NGC 5634 +67,NGC 5824 +68,NGC 5897 +69,NGC 5927 +70,NGC 5986 +71,NGC 5999 +72,NGC 6005 +72A,Trumpler 23 +73,NGC 6093 +74,NGC 6101 +75,NGC 6121 +76,NGC 6134 +77,NGC 6144 +78,NGC 6139 +79,NGC 6171 +79A,NGC 6167 +79B,NGC 6192 +80,NGC 6218 +81,NGC 6216 +82,NGC 6235 +83,NGC 6254 +84,NGC 6253 +85,NGC 6266 +86,NGC 6273 +87,NGC 6284 +88,NGC 6287 +89,NGC 6293 +90,NGC 6304 +91,NGC 6316 +91A,NGC 6318 +92,NGC 6333 +93,NGC 6356 +94,NGC 6352 +95,NGC 6362 +96,NGC 6388 +97,NGC 6402 +98,NGC 6397 +98A,NGC 6440 +98B,NGC 6445 +99,NGC 6441 +100,NGC 6496 +101,NGC 6522 +102,NGC 6528 +103,NGC 6544 +104,NGC 6541 +105,NGC 6553 +106,NGC 6569 +107,NGC 6584 +107A,NGC 6603 +108,NGC 6618 +109,NGC 6624 +110,NGC 6626 +111,NGC 6638 +112,NGC 6637 +112A,NGC 6642 +113,NGC 6652 +114,NGC 6656 +115,NGC 6681 +116,NGC 6705 +117,NGC 6712 +118,NGC 6715 +119,NGC 6723 +120,NGC 6744 +121,NGC 6752 +122,NGC 6809 +123,NGC 6818 +124,NGC 6864 +125,NGC 6981 +126,NGC 7009 +127,NGC 7089 +128,NGC 7099 +129,NGC 7293 +129A,NGC 7410 +129B,IC 1459 +130,NGC 7793 diff --git a/api/src/test/resources/Caldwell.csv b/api/src/test/resources/Caldwell.csv new file mode 100644 index 000000000..59c962c23 --- /dev/null +++ b/api/src/test/resources/Caldwell.csv @@ -0,0 +1,111 @@ +Caldwell number,NGC number,Common name,Type,Magnitude +1,NGC 188,,Open Cluster,8.1 +2,NGC 40,Bow-Tie Nebula,Planetary Nebula,11 +3,NGC 4236,,Barred Spiral Galaxy,9.7 +4,NGC 7023,Iris Nebula,Open Cluster and Nebula,7 +5,IC 342,Hidden Galaxy,Spiral Galaxy,9 +6,NGC 6543,Cat's Eye Nebula,Planetary Nebula,9 +7,NGC 2403,,Spiral Galaxy,8.4 +8,NGC 559,,Open Cluster,9.5 +9,Sh2-155,Cave Nebula,Nebula,7.7 +10,NGC 663,,Open Cluster,7.1 +11,NGC 7635,Bubble Nebula,Nebula,10 +12,NGC 6946,Fireworks Galaxy,Spiral Galaxy,8.9 +13,NGC 457,"Owl Cluster, E.T. Cluster",Open Cluster,6.4 +14,NGC 869,"Double Cluster",Open Cluster,4 +14,NGC 884,"Double Cluster",Open Cluster,4 +15,NGC 6826,Blinking Planetary,Planetary Nebula,10 +16,NGC 7243,,Open Cluster,6.4 +17,NGC 147,,Dwarf Spheroidal Galaxy,9.3 +18,NGC 185,,Dwarf Spheroidal Galaxy,9.2 +19,IC 5146,Cocoon Nebula,Open Cluster and Nebula,7.2 +20,NGC 7000,North America Nebula,Nebula,4 +21,NGC 4449,,Irregular galaxy,9.4 +22,NGC 7662,Blue Snowball,Planetary Nebula,9 +23,NGC 891,Silver Sliver Galaxy,Spiral Galaxy,10 +24,NGC 1275,Perseus A,Supergiant Elliptical Galaxy,11.6 +25,NGC 2419,,Globular Cluster,10.4 +26,NGC 4244,,Spiral Galaxy,10.2 +27,NGC 6888,Crescent Nebula,Nebula,7.4 +28,NGC 752,,Open Cluster,5.7 +29,NGC 5005,,Spiral Galaxy,9.8 +30,NGC 7331,,Spiral Galaxy,9.5 +31,IC 405,Flaming Star Nebula,Nebula,13 +32,NGC 4631,Whale Galaxy,Barred Spiral Galaxy,9.3 +33,NGC 6992,East Veil Nebula,Supernova Remnant,7 +34,NGC 6960,West Veil Nebula,Supernova Remnant,7 +35,NGC 4889,Coma B,Supergiant Elliptical Galaxy,11.4 +36,NGC 4559,,Spiral Galaxy,9.9 +37,NGC 6885,,Open Cluster,6 +38,NGC 4565,Needle Galaxy,Spiral Galaxy,9.6 +39,NGC 2392,Eskimo Nebula/Clown Face Nebula,Planetary Nebula,10 +40,NGC 3626,,Lenticular Galaxy,10.9 +41,Melotte 25,Hyades,Open Cluster,0.5 +42,NGC 7006,,Globular Cluster,10.6 +43,NGC 7814,,Spiral Galaxy,10.5 +44,NGC 7479,,Barred Spiral Galaxy,11 +45,NGC 5248,,Spiral Galaxy,10.2 +46,NGC 2261,Hubble's Variable Nebula,Nebula,- +47,NGC 6934,,Globular Cluster,8.9 +48,NGC 2775,,Spiral Galaxy,10.3 +49,NGC 2237,Rosette Nebula,Nebula,9.0 +50,NGC 2244,Satellite Cluster,Open Cluster,4.8 +51,IC 1613,,Irregular galaxy,9.3 +52,NGC 4697,,Elliptical galaxy,9.3 +53,NGC 3115,Spindle Galaxy,Lenticular Galaxy,9.2 +54,NGC 2506,,Open Cluster,7.6 +55,NGC 7009,Saturn Nebula,Planetary Nebula,8 +56,NGC 246,Skull Nebula,Planetary Nebula,8 +57,NGC 6822,Barnard's Galaxy,Barred irregular galaxy,9 +58,NGC 2360,Caroline's Cluster,Open Cluster,7.2 +59,NGC 3242,Ghost of Jupiter,Planetary Nebula,9 +60,NGC 4038,Antennae Galaxies,Interacting galaxy,10.7 +61,NGC 4039,Antennae Galaxies,Interacting galaxy,13 +62,NGC 247,,Spiral Galaxy,8.9 +63,NGC 7293,Helix Nebula,Planetary Nebula,7.3 +64,NGC 2362,Tau Canis Majoris Cluster,Open Cluster and Nebula,4.1 +65,NGC 253,Sculptor Galaxy,Spiral Galaxy,7.1 +66,NGC 5694,,Globular Cluster,10.2 +67,NGC 1097,,Barred Spiral Galaxy,9.3 +68,NGC 6729,R CrA Nebula,Nebula,- +69,NGC 6302,Bug Nebula,Planetary Nebula,13 +70,NGC 300,,Spiral Galaxy,9 +71,NGC 2477,,Open Cluster,5.8 +72,NGC 55,,Barred Spiral Galaxy,8 +73,NGC 1851,,Globular Cluster,7.3 +74,NGC 3132,Eight Burst Nebula,Planetary Nebula,8 +75,NGC 6124,,Open Cluster,5.8 +76,NGC 6231,,Open Cluster and Nebula,2.6 +77,NGC 5128,Centaurus A,Elliptical or Lenticular Galaxy,7 +78,NGC 6541,,Globular Cluster,6.6 +79,NGC 3201,,Globular Cluster,6.8 +80,NGC 5139,Omega Centauri,Globular Cluster,3.7 +81,NGC 6352,,Globular Cluster,8.2 +82,NGC 6193,,Open Cluster,5.2 +83,NGC 4945,,Barred Spiral Galaxy,9 +84,NGC 5286,,Globular Cluster,7.6 +85,IC 2391,Omicron Velorum Cluster,Open Cluster,2.5 +86,NGC 6397,,Globular Cluster,5.7 +87,NGC 1261,,Globular Cluster,8.4 +88,NGC 5823,,Open Cluster,7.9 +89,NGC 6087,S Normae Cluster,Open Cluster,5.4 +90,NGC 2867,,Planetary Nebula,10 +91,NGC 3532,Wishing Well Cluster,Open Cluster,3 +92,NGC 3372,Eta Carinae Nebula,Nebula,3 +93,NGC 6752,Great Peacock Globular,Globular Cluster,5.4 +94,NGC 4755,Jewel Box,Open Cluster,4.2 +95,NGC 6025,,Open Cluster,5.1 +96,NGC 2516,Southern Beehive Cluster,Open Cluster,3.8 +97,NGC 3766,Pearl Cluster,Open Cluster,5.3 +98,NGC 4609,,Open Cluster,6.9 +99,,Coalsack Nebula,Dark Nebula,- +100,IC 2944,Lambda Centauri Nebula,Open Cluster and Nebula,4.5 +101,NGC 6744,,Spiral Galaxy,9 +102,IC 2602,Theta Car Cluster,Open Cluster,1.9 +103,NGC 2070,Tarantula Nebula,Open Cluster and Nebula,8.2 +104,NGC 362,,Globular Cluster,6.6 +105,NGC 4833,,Globular Cluster,7.4 +106,NGC 104,47 Tucanae,Globular Cluster,4 +107,NGC 6101,,Globular Cluster,9.3 +108,NGC 4372,,Globular Cluster,7.8 +109,NGC 3195,,Planetary Nebula,11.6 diff --git a/api/src/test/resources/Dunlop.csv b/api/src/test/resources/Dunlop.csv new file mode 100644 index 000000000..9b551cf93 --- /dev/null +++ b/api/src/test/resources/Dunlop.csv @@ -0,0 +1,144 @@ +Dunlop,NGC +1,NGC 7590 +2,NGC 7599 +18,NGC 104 +23,NGC 330 +25,NGC 346 +62,NGC 362 +68,NGC 6101 +81,NGC 1795 +90,NGC 1943 +98,NGC 2019 +102,NGC 2058 +106,NGC 2122 +114,NGC 1743 +129,NGC 1910 +131,NGC 1928 +136,NGC 1966 +142,NGC 2070 +143,NGC 2069 +160,NGC 2136 +164,NGC 4833 +167,NGC 1755 +169,NGC 1770 +175,NGC 1936 +193,NGC 2159 +194,NGC 2164 +196,NGC 2156 +201,NGC 2214 +206,NGC 1313 +210,NGC 1869 +211,NGC 1955 +213,NGC 1974 +215,NGC 2004 +218,NGC 2121 +220,NGC 2035 +225,NGC 6362 +235,NGC 1810 +236,NGC 1818 +240,NGC 2029 +241,NGC 2027 +246,NGC 1831 +262,NGC 6744 +265,NGC 2808 +272,NGC 4609 +273,NGC 5281 +282,NGC 5316 +289,NGC 3766 +291,NGC 4103 +292,NGC 4349 +295,NGC 6752 +297,NGC 3114 +301,NGC 4755 +302,NGC 5617 +304,NGC 6025 +309,NGC 3372 +311,NGC 4852 +313,NGC 5606 +323,NGC 3532 +326,NGC 6087 +333,NGC 5715 +334,NGC 6005 +337,NGC 1261 +342,NGC 5662 +343,NGC 5999 +348,NGC 1515 +349,NGC 3960 +355,NGC 3330 +356,NGC 5749 +357,NGC 5925 +359,NGC 6031 +360,NGC 6067 +364,NGC 6208 +366,NGC 6397 +376,NGC 6584 +386,NGC 3228 +388,NGC 5286 +389,NGC 5927 +397,NGC 2972 +400,NGC 6167 +406,NGC 7049 +410,NGC 2547 +411,NGC 4945 +412,NGC 6134 +413,NGC 6193 +417,NGC 6352 +425,NGC 6861 +426,NGC 1433 +431,NGC 5460 +438,NGC 1493 +440,NGC 5139 +442,NGC 6204 +445,NGC 3201 +454,NGC 6216 +456,NGC 6259 +457,NGC 6388 +466,NGC 1512 +469,NGC 5643 +473,NGC 6541 +479,NGC 625 +480,NGC 1487 +481,NGC 3680 +482,NGC 5128 +483,NGC 6192 +487,NGC 1291 +499,NGC 6231 +507,NGC 55 +508,NGC 1851 +511,NGC 4709 +514,NGC 6124 +518,NGC 7410 +520,NGC 6242 +521,NGC 6268 +522,NGC 6318 +535,NGC 2477 +536,NGC 6139 +547,NGC 1317 +548,NGC 1316 +549,NGC 1808 +552,NGC 5986 +556,NGC 6281 +557,NGC 6441 +562,NGC 1436 +563,NGC 2546 +564,NGC 2818 +568,NGC 6400 +573,NGC 6723 +574,NGC 1380 +578,NGC 2298 +591,NGC 1350 +594,NGC 2090 +600,NGC 1532 +607,NGC 6652 +609,NGC 2658 +612,NGC 6416 +613,NGC 6637 +614,NGC 6681 +617,NGC 3621 +619,NGC 6569 +620,NGC 6809 +623,NGC 5253 +624,NGC 6715 +626,NGC 2489 +627,NGC 6266 +628,NGC 5236 diff --git a/build.gradle.kts b/build.gradle.kts index 7355cca07..d95ae7dfe 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,7 +7,7 @@ buildscript { dependencies { classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.10") classpath("org.jetbrains.kotlin:kotlin-allopen:1.9.10") - classpath("com.adarshr:gradle-test-logger-plugin:3.2.0") + classpath("com.adarshr:gradle-test-logger-plugin:4.0.0") } repositories { diff --git a/desktop/README.md b/desktop/README.md index 80017637e..611f8aa58 100644 --- a/desktop/README.md +++ b/desktop/README.md @@ -23,6 +23,10 @@ The complete integrated solution for all of your astronomical imaging needs. ![](filter-wheel.png) +## Guiding + +![](guiding.png) + ## Sky Atlas ![](atlas.1.png) @@ -43,6 +47,10 @@ The complete integrated solution for all of your astronomical imaging needs. ![](framing.png) +## Alignment + +![](alignment.darv.png) + ## INDI ![](indi.png) diff --git a/desktop/alignment.darv.png b/desktop/alignment.darv.png new file mode 100644 index 000000000..c565d344b Binary files /dev/null and b/desktop/alignment.darv.png differ diff --git a/desktop/app/main.ts b/desktop/app/main.ts index 06fcf6308..7951aecd6 100644 --- a/desktop/app/main.ts +++ b/desktop/app/main.ts @@ -4,16 +4,16 @@ import { ChildProcessWithoutNullStreams, spawn } from 'node:child_process' import * as path from 'path' import { Camera, FilterWheel, Focuser, INDI_EVENT_TYPES, INTERNAL_EVENT_TYPES, Mount, OpenWindow } from './types' +import { CronJob } from 'cron' import { WebSocket } from 'ws' import { OpenDirectory } from '../src/shared/types' Object.assign(global, { WebSocket }) -let homeWindow: BrowserWindow | null = null -const secondaryWindows = new Map() +const browserWindows = new Map() +const cronedWindows = new Map[]>() let api: ChildProcessWithoutNullStreams | null = null let apiPort = 7000 let wsClient: Client -let splash: BrowserWindow | null = null let selectedCamera: Camera let selectedMount: Mount @@ -26,8 +26,9 @@ const serve = args.some(e => e === '--serve') app.commandLine.appendSwitch('disable-http-cache') function createMainWindow() { - splash?.close() - splash = null + const splashWindow = browserWindows.get('splash') + splashWindow?.close() + browserWindows.delete('splash') createWindow({ id: 'home', path: 'home' }) @@ -55,16 +56,15 @@ function createMainWindow() { } function createWindow(data: OpenWindow) { - if (secondaryWindows.has(data.id)) { - const window = secondaryWindows.get(data.id)! + let window = browserWindows.get(data.id) + if (window) { if (data.params) { + console.info('params changed. id=%s, params=%s', data.id, data.params) window.webContents.send('PARAMS_CHANGED', data.params) } return window - } else if (data.id === 'home' && homeWindow) { - return homeWindow } const size = screen.getPrimaryDisplay().workAreaSize @@ -99,7 +99,7 @@ function createWindow(data: OpenWindow) { const icon = data.icon ?? 'nebulosa' const params = encodeURIComponent(JSON.stringify(data.params || {})) - const window = new BrowserWindow({ + window = new BrowserWindow({ title: 'Nebulosa', frame: false, width, height, @@ -135,36 +135,38 @@ function createWindow(data: OpenWindow) { }) window.on('close', () => { + const homeWindow = browserWindows.get('home') + if (window === homeWindow) { - for (const [_, value] of secondaryWindows) { + browserWindows.delete('home') + + for (const [_, value] of browserWindows) { value.close() } - homeWindow = null + browserWindows.clear() api?.kill(0) } else { - for (const [key, value] of secondaryWindows) { + for (const [key, value] of browserWindows) { if (value === window) { - secondaryWindows.delete(key) + browserWindows.delete(key) break } } } }) - if (data.id === 'home') { - homeWindow = window - } else { - secondaryWindows.set(data.id, window) - } + browserWindows.set(data.id, window) return window } function createSplashScreen() { - if (!serve && splash === null) { - splash = new BrowserWindow({ + let splashWindow = browserWindows.get('splash') + + if (!serve && splashWindow === null) { + splashWindow = new BrowserWindow({ width: 512, height: 512, transparent: true, @@ -174,16 +176,17 @@ function createSplashScreen() { }) const url = new URL(path.join('file:', __dirname, 'assets', 'images', 'splash.png')) - splash.loadURL(url.href) + splashWindow.loadURL(url.href) + + splashWindow.show() + splashWindow.center() - splash.show() - splash.center() + browserWindows.set('splash', splashWindow) } } function findWindowById(id: number) { - if (homeWindow?.id === id) return homeWindow - for (const [_, window] of secondaryWindows) if (window.id === id) return window + for (const [_, window] of browserWindows) if (window.id === id) return window return undefined } @@ -236,13 +239,15 @@ try { }) app.on('activate', () => { - if (homeWindow === null) { + const homeWindow = browserWindows.get('home') + + if (!homeWindow) { startApp() } }) ipcMain.handle('OPEN_WINDOW', async (_, data: OpenWindow) => { - const newWindow = !secondaryWindows.has(data.id) + const newWindow = !browserWindows.has(data.id) const window = createWindow(data) @@ -264,7 +269,8 @@ try { }) ipcMain.on('OPEN_FITS', async (event) => { - const value = await dialog.showOpenDialog(homeWindow!, { + const ownerWindow = findWindowById(event.sender.id) + const value = await dialog.showOpenDialog(ownerWindow!, { filters: [{ name: 'FITS files', extensions: ['fits', 'fit'] }], properties: ['openFile'], }) @@ -273,7 +279,8 @@ try { }) ipcMain.on('SAVE_FITS_AS', async (event) => { - const value = await dialog.showSaveDialog(homeWindow!, { + const ownerWindow = findWindowById(event.sender.id) + const value = await dialog.showSaveDialog(ownerWindow!, { filters: [ { name: 'FITS files', extensions: ['fits', 'fit'] }, { name: 'Image files', extensions: ['png', 'jpe?g'] }, @@ -285,7 +292,8 @@ try { }) ipcMain.on('OPEN_DIRECTORY', async (event, data?: OpenDirectory) => { - const value = await dialog.showOpenDialog(homeWindow!, { + const ownerWindow = findWindowById(event.sender.id) + const value = await dialog.showOpenDialog(ownerWindow!, { properties: ['openDirectory'], defaultPath: data?.defaultPath, }) @@ -322,7 +330,7 @@ try { ipcMain.on('CLOSE_WINDOW', (event, id?: string) => { if (id) { - for (const [key, value] of secondaryWindows) { + for (const [key, value] of browserWindows) { if (key === id) { value.close() event.returnValue = true @@ -338,6 +346,32 @@ try { } }) + ipcMain.on('REGISTER_CRON', async (event, cronTime: string) => { + const window = findWindowById(event.sender.id) + + if (!window) return + + const cronJobs = cronedWindows.get(window) ?? [] + cronJobs.forEach(e => e.stop()) + const cronJob = new CronJob(cronTime, () => window.webContents.send('CRON_TICKED', cronTime)) + cronJobs.push(cronJob) + cronedWindows.set(window, cronJobs) + + event.returnValue = true + }) + + ipcMain.on('UNREGISTER_CRON', async (event) => { + const window = findWindowById(event.sender.id) + + if (!window) return + + const cronJobs = cronedWindows.get(window) + cronJobs?.forEach(e => e.stop()) + cronedWindows.delete(window) + + event.returnValue = true + }) + for (const item of INTERNAL_EVENT_TYPES) { ipcMain.on(item, (event, data) => { switch (item) { @@ -379,12 +413,12 @@ try { } function sendToAllWindows(channel: string, data: any, home: boolean = true) { - for (const [_, value] of secondaryWindows) { - value.webContents.send(channel, data) - } + const homeWindow = browserWindows.get('home') - if (home) { - homeWindow?.webContents?.send(channel, data) + for (const [_, window] of browserWindows) { + if (window !== homeWindow || home) { + window.webContents.send(channel, data) + } } if (serve) { diff --git a/desktop/app/package-lock.json b/desktop/app/package-lock.json index e2f4afadf..8bb2cb383 100644 --- a/desktop/app/package-lock.json +++ b/desktop/app/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "dependencies": { "@stomp/stompjs": "7.0.0", + "cron": "3.1.0", "ws": "8.14.2" } }, @@ -18,6 +19,28 @@ "resolved": "https://registry.npmjs.org/@stomp/stompjs/-/stompjs-7.0.0.tgz", "integrity": "sha512-fGdq4wPDnSV/KyOsjq4P+zLc8MFWC3lMmP5FBgLWKPJTYcuCbAIrnRGjB7q2jHZdYCOD5vxLuFoKIYLy5/u8Pw==" }, + "node_modules/@types/luxon": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.3.2.tgz", + "integrity": "sha512-l5cpE57br4BIjK+9BSkFBOsWtwv6J9bJpC7gdXIzZyI0vuKvNTk0wZZrkQxMGsUAuGW9+WMNWF2IJMD7br2yeQ==" + }, + "node_modules/cron": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cron/-/cron-3.1.0.tgz", + "integrity": "sha512-u6Z89TV7zhG7aW7MX7aLQhK5PYjTzFpzjFgiSX5r7qC1vjPvRt1FVfarHRaN/5IokEXM1DRJcXnwXI0e9G0awA==", + "dependencies": { + "@types/luxon": "~3.3.0", + "luxon": "~3.3.0" + } + }, + "node_modules/luxon": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.3.0.tgz", + "integrity": "sha512-An0UCfG/rSiqtAIiBPO0Y9/zAnHUZxAMiCpTd5h2smgsj7GGmcenvrvww2cqNA8/4A5ZrD1gJpHN2mIHZQF+Mg==", + "engines": { + "node": ">=12" + } + }, "node_modules/ws": { "version": "8.14.2", "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", diff --git a/desktop/app/package.json b/desktop/app/package.json index e350a816c..b4d99a53f 100644 --- a/desktop/app/package.json +++ b/desktop/app/package.json @@ -12,6 +12,7 @@ "private": true, "dependencies": { "@stomp/stompjs": "7.0.0", - "ws": "8.14.2" + "ws": "8.14.2", + "cron": "3.1.0" } } diff --git a/desktop/guiding.png b/desktop/guiding.png new file mode 100644 index 000000000..2fbe8f214 Binary files /dev/null and b/desktop/guiding.png differ diff --git a/desktop/package-lock.json b/desktop/package-lock.json index 9e200508c..8f0903c6f 100644 --- a/desktop/package-lock.json +++ b/desktop/package-lock.json @@ -10,26 +10,25 @@ "hasInstallScript": true, "license": "MIT", "dependencies": { - "@angular/animations": "16.2.7", - "@angular/cdk": "16.2.6", - "@angular/common": "16.2.7", - "@angular/compiler": "16.2.7", - "@angular/core": "16.2.7", - "@angular/forms": "16.2.7", - "@angular/language-service": "16.2.7", - "@angular/platform-browser": "16.2.7", - "@angular/platform-browser-dynamic": "16.2.7", - "@angular/router": "16.2.7", + "@angular/animations": "16.2.9", + "@angular/cdk": "16.2.8", + "@angular/common": "16.2.9", + "@angular/compiler": "16.2.9", + "@angular/core": "16.2.9", + "@angular/forms": "16.2.9", + "@angular/language-service": "16.2.9", + "@angular/platform-browser": "16.2.9", + "@angular/platform-browser-dynamic": "16.2.9", + "@angular/router": "16.2.9", "chart.js": "4.4.0", "chartjs-plugin-zoom": "2.0.1", - "cron": "2.4.4", "interactjs": "1.10.19", "leaflet": "1.9.4", "moment": "2.29.4", "panzoom": "9.4.3", "primeflex": "3.3.1", "primeicons": "6.0.1", - "primeng": "16.4.1", + "primeng": "16.5.0", "rxjs": "7.8.1", "tslib": "2.6.2", "uuid": "9.0.1", @@ -37,13 +36,13 @@ }, "devDependencies": { "@angular-builders/custom-webpack": "16.0.1", - "@angular-devkit/build-angular": "16.2.4", - "@angular/cli": "16.2.4", - "@angular/compiler-cli": "16.2.7", + "@angular-devkit/build-angular": "16.2.6", + "@angular/cli": "16.2.6", + "@angular/compiler-cli": "16.2.9", "@types/leaflet": "1.9.6", - "@types/node": "20.7.1", - "@types/uuid": "9.0.4", - "electron": "26.2.4", + "@types/node": "20.8.5", + "@types/uuid": "9.0.5", + "electron": "27.0.0", "electron-builder": "24.6.4", "electron-debug": "3.2.0", "electron-reloader": "1.2.3", @@ -92,12 +91,12 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.1602.4", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1602.4.tgz", - "integrity": "sha512-SQr/FZ8wEOGC6EM+7V5rWyb/qpK0LFND/WbES5l+Yvwv+TEyPihsh5QCPmvPxi45eFbaHPrXkIZnvxnkxRDN/A==", + "version": "0.1602.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1602.6.tgz", + "integrity": "sha512-b1NNV3yNg6Rt86ms20bJIroWUI8ihaEwv5k+EoijEXLoMs4eNs5PhqL+QE8rTj+q9pa1gSrWf2blXor2JGwf1g==", "dev": true, "dependencies": { - "@angular-devkit/core": "16.2.4", + "@angular-devkit/core": "16.2.6", "rxjs": "7.8.1" }, "engines": { @@ -107,15 +106,15 @@ } }, "node_modules/@angular-devkit/build-angular": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-16.2.4.tgz", - "integrity": "sha512-qWWjw321+qKzQ3U+arPJ5fdqxZ/aeT5HuxAtA7xqNu/cqnqvRZ8RVbbnugFx4U1R271tABT+N+N1kkIep/vlDg==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-16.2.6.tgz", + "integrity": "sha512-QdU/q77K1P8CPEEZGxw1QqLcnA9ofboDWS7vcLRBmFmk2zydtLTApbK0P8GNDRbnmROOKkoaLo+xUTDJz9gvPA==", "dev": true, "dependencies": { "@ampproject/remapping": "2.2.1", - "@angular-devkit/architect": "0.1602.4", - "@angular-devkit/build-webpack": "0.1602.4", - "@angular-devkit/core": "16.2.4", + "@angular-devkit/architect": "0.1602.6", + "@angular-devkit/build-webpack": "0.1602.6", + "@angular-devkit/core": "16.2.6", "@babel/core": "7.22.9", "@babel/generator": "7.22.9", "@babel/helper-annotate-as-pure": "7.22.5", @@ -127,7 +126,7 @@ "@babel/runtime": "7.22.6", "@babel/template": "7.22.5", "@discoveryjs/json-ext": "0.5.7", - "@ngtools/webpack": "16.2.4", + "@ngtools/webpack": "16.2.6", "@vitejs/plugin-basic-ssl": "1.0.1", "ansi-colors": "4.1.3", "autoprefixer": "10.4.14", @@ -157,7 +156,7 @@ "parse5-html-rewriting-stream": "7.0.0", "picomatch": "2.3.1", "piscina": "4.0.0", - "postcss": "8.4.27", + "postcss": "8.4.31", "postcss-loader": "7.3.3", "resolve-url-loader": "5.0.0", "rxjs": "7.8.1", @@ -228,19 +227,115 @@ } } }, + "node_modules/@angular-devkit/build-angular/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/@angular-devkit/build-angular/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/@angular-devkit/build-angular/node_modules/tslib": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.1.tgz", "integrity": "sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==", "dev": true }, + "node_modules/@angular-devkit/build-angular/node_modules/webpack": { + "version": "5.88.2", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.88.2.tgz", + "integrity": "sha512-JmcgNZ1iKj+aiR0OvTYtWQqJwq37Pf683dY9bVORwVbUrDhLhdn/PlO2sHsFHPkj7sHNQF3JwaAkp49V+Sq1tQ==", + "dev": true, + "dependencies": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^1.0.0", + "@webassemblyjs/ast": "^1.11.5", + "@webassemblyjs/wasm-edit": "^1.11.5", + "@webassemblyjs/wasm-parser": "^1.11.5", + "acorn": "^8.7.1", + "acorn-import-assertions": "^1.9.0", + "browserslist": "^4.14.5", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.15.0", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.9", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.7", + "watchpack": "^2.4.0", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, "node_modules/@angular-devkit/build-webpack": { - "version": "0.1602.4", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1602.4.tgz", - "integrity": "sha512-QOnMfAOFrAQKOw+odgymragqzv6Ts5/Ni7/SJ1iLwlQcH6TajT6373fSCDFdKV40ntF53yjnexIsLx81/dK+Cg==", + "version": "0.1602.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1602.6.tgz", + "integrity": "sha512-BJPR6xdq7gRJ6bVWnZ81xHyH75j7lyLbegCXbvUNaM8TWVBkwWsSdqr2NQ717dNLLn5umg58SFpU/pWMq6CxMQ==", "dev": true, "dependencies": { - "@angular-devkit/architect": "0.1602.4", + "@angular-devkit/architect": "0.1602.6", "rxjs": "7.8.1" }, "engines": { @@ -254,9 +349,9 @@ } }, "node_modules/@angular-devkit/core": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-16.2.4.tgz", - "integrity": "sha512-VCZ1z1lDbFkbYkQ6ZMEFfmNzkMEOCBKSzAhWutRyd7oM02by4/5SvDSXd5BMvMxWhPJ/567DdSPOfhhnXQkkDg==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-16.2.6.tgz", + "integrity": "sha512-iez/8NYXQT6fqVQLlKmZUIRkFUEZ88ACKbTwD4lBmk0+hXW+bQBxI7JOnE3C4zkcM2YeuTXIYsC5SebTKYiR4Q==", "dev": true, "dependencies": { "ajv": "8.12.0", @@ -281,12 +376,12 @@ } }, "node_modules/@angular-devkit/schematics": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-16.2.4.tgz", - "integrity": "sha512-TsSflKJlaHzKgcU/taQg5regmBP/ggvwVtAbJRBWmCaeQJzobFo68+rtwfYfvuQXKAR6KsbSJc97mqmq6zmTwQ==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-16.2.6.tgz", + "integrity": "sha512-PhpRYHCJ3WvZXmng6Qk8TXeQf83jeBMAf7AIzI8h0fgeBocOl97Xf7bZpLg6GymiU+rVn15igQ4Rz9rKAay8bQ==", "dev": true, "dependencies": { - "@angular-devkit/core": "16.2.4", + "@angular-devkit/core": "16.2.6", "jsonc-parser": "3.2.0", "magic-string": "0.30.1", "ora": "5.4.1", @@ -299,9 +394,9 @@ } }, "node_modules/@angular/animations": { - "version": "16.2.7", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-16.2.7.tgz", - "integrity": "sha512-6GM4xFprTjDN71nRF6a2Nq3xS/b69tk2mOpcXZeTvxl6b/hqUo1l0y1eY1XK211cwm36GtSjq2cHJAIRBT3CiA==", + "version": "16.2.9", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-16.2.9.tgz", + "integrity": "sha512-J+nsc2x/ZQuh+YwwTzxXUrV+7SBpJq6DDStfTFkZls9PWGRj9fjqQeRCWrfNLllpxopAEjhFkoyK06oSjcwqAw==", "dependencies": { "tslib": "^2.3.0" }, @@ -309,13 +404,13 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/core": "16.2.7" + "@angular/core": "16.2.9" } }, "node_modules/@angular/cdk": { - "version": "16.2.6", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-16.2.6.tgz", - "integrity": "sha512-vSaPs69xutbxc6IbZz4I5fMzZhlypsMg5JKKNAufmyYNNHQYgSQytpUd1/RxHhPF/JoEvj/J8QjauRriZFN+SA==", + "version": "16.2.8", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-16.2.8.tgz", + "integrity": "sha512-DvqxH909mgSSxWbc5xM5xKLjDMPXY3pzzSVAllngvc9KGPFw240WCs3tSpPaVJI50Esbzdu5O0CyTBfu9jUy4g==", "dependencies": { "tslib": "^2.3.0" }, @@ -329,15 +424,15 @@ } }, "node_modules/@angular/cli": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-16.2.4.tgz", - "integrity": "sha512-OjnlQ2wzhkc1q3iDbWtLeaXoPzS0BtevazT7vmB/MiNVgjDcF3bPFQTcBBvtWAF0wN9jgPC712X8ucwdEAOMlg==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-16.2.6.tgz", + "integrity": "sha512-9poPvUEmlufOAW1Cjk+aA5e2x3mInLtbYYSL/EYviDN2ugmavsSIvxAE/WLnxq6cPWqhNDbHDaqvcmqkcFM3Cw==", "dev": true, "dependencies": { - "@angular-devkit/architect": "0.1602.4", - "@angular-devkit/core": "16.2.4", - "@angular-devkit/schematics": "16.2.4", - "@schematics/angular": "16.2.4", + "@angular-devkit/architect": "0.1602.6", + "@angular-devkit/core": "16.2.6", + "@angular-devkit/schematics": "16.2.6", + "@schematics/angular": "16.2.6", "@yarnpkg/lockfile": "1.1.0", "ansi-colors": "4.1.3", "ini": "4.1.1", @@ -363,9 +458,9 @@ } }, "node_modules/@angular/common": { - "version": "16.2.7", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-16.2.7.tgz", - "integrity": "sha512-vcKbbtDXNmJ8dj1GF52saJRT5U3P+phnIwnv+hQ2c+VVj/S2alWlBkT12iM+KlvnWdxsa0q4yW0G4WvpPJPaMQ==", + "version": "16.2.9", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-16.2.9.tgz", + "integrity": "sha512-5Lh5KsxCkaoBDeSAghKNF5lCi0083ug4X2X7wnafsSd6Z3xt/rDjH9hDOP5SF5IDLtCVjJgHfs3cCLSTjRuNwg==", "dependencies": { "tslib": "^2.3.0" }, @@ -373,14 +468,14 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/core": "16.2.7", + "@angular/core": "16.2.9", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/compiler": { - "version": "16.2.7", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-16.2.7.tgz", - "integrity": "sha512-Sp+QjHFYjBMhjag/YbIV5skqr/UrpBjCPo1WFBBhj5DKkvgWC7T00yYJn+aBj0DU5ZuMmO/P8Vb7bRIHIRNL4w==", + "version": "16.2.9", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-16.2.9.tgz", + "integrity": "sha512-lh799pnbdvzTVShJHOY1JC6c1pwBsZC4UIgB3Itklo9dskGybQma/gP+lE6RhqM4FblNfaaBXGlCMUuY8HkmEQ==", "dependencies": { "tslib": "^2.3.0" }, @@ -388,7 +483,7 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/core": "16.2.7" + "@angular/core": "16.2.9" }, "peerDependenciesMeta": { "@angular/core": { @@ -397,9 +492,9 @@ } }, "node_modules/@angular/compiler-cli": { - "version": "16.2.7", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-16.2.7.tgz", - "integrity": "sha512-aMAmSyurmvdKIcRpATfJPyTa0RYOylmXb7TI5TyDico9pUR7RAlreuW/1NUeIPWfZdPrPyoGOYGqukSuSnyrNA==", + "version": "16.2.9", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-16.2.9.tgz", + "integrity": "sha512-ecH2oOlijJdDqioD9IfgdqJGoRRHI6hAx5rwBxIaYk01ywj13KzvXWPrXbCIupeWtV/XUZUlbwf47nlmL5gxZg==", "dev": true, "dependencies": { "@babel/core": "7.22.5", @@ -420,7 +515,7 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/compiler": "16.2.7", + "@angular/compiler": "16.2.9", "typescript": ">=4.9.3 <5.2" } }, @@ -464,9 +559,9 @@ } }, "node_modules/@angular/core": { - "version": "16.2.7", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-16.2.7.tgz", - "integrity": "sha512-JQOxo+Ja9ThQjUa4vdOMLZfIK2dhR3cnPbqB1tV2WuTmIv49QASbFHsae8zZsS4Au5/TafBaW3KkK9aRU8G5gg==", + "version": "16.2.9", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-16.2.9.tgz", + "integrity": "sha512-chvPX29ZBcMDuh7rLIgb0Cru6oJ/0FaqRzfOI3wT4W2F9W1HOlCtipovzmPYaUAmXBWfVP4EBO9TOWnpog0S0w==", "dependencies": { "tslib": "^2.3.0" }, @@ -479,9 +574,9 @@ } }, "node_modules/@angular/forms": { - "version": "16.2.7", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-16.2.7.tgz", - "integrity": "sha512-zUEcYwoAiRmKBJd3NAnksbqTXm60L/nLmhv8OAS9MvV5tXNvEjavpy3eG16H7H2IPQ2ZkUICB0bssmmAVOCbmQ==", + "version": "16.2.9", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-16.2.9.tgz", + "integrity": "sha512-rxlg2iNJNBH/uc7b5YqybfYc8BkLzzPv1d/nMsQUlY0O2UV2zwNRpcIiWbWd7+ZaKjcyPynVe9FsXC8wgWIABw==", "dependencies": { "tslib": "^2.3.0" }, @@ -489,24 +584,24 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/common": "16.2.7", - "@angular/core": "16.2.7", - "@angular/platform-browser": "16.2.7", + "@angular/common": "16.2.9", + "@angular/core": "16.2.9", + "@angular/platform-browser": "16.2.9", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/language-service": { - "version": "16.2.7", - "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-16.2.7.tgz", - "integrity": "sha512-J5Y5tdiHTyRzVb4rEQDUBvFzaPSZyj+tsq463UlbJECwIfDmPb2G+6y1WasQaH+UOWEanBxOZ2supk10KNS3+A==", + "version": "16.2.9", + "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-16.2.9.tgz", + "integrity": "sha512-yYfe6TRiPZ5cPs8a/PRBjzIULzPwnGWp9b+DuVZXja3wkE1PhckXEH9o8qsHRnzuJFq9cqZbo+CSIaJrLQctVA==", "engines": { "node": "^16.14.0 || >=18.10.0" } }, "node_modules/@angular/platform-browser": { - "version": "16.2.7", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-16.2.7.tgz", - "integrity": "sha512-yQ/4FB33Jc1Xs+slWfddZpbKdkCHdhCh39Mfjxa1wTen6YJZKmvjBbMNCkvnvNbLqc2IFWRwTQdG8s0n1jfl3A==", + "version": "16.2.9", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-16.2.9.tgz", + "integrity": "sha512-9Je7+Jmx0AOyRzBBumraVJG3M0R6YbT4c9jTUbLGJCcPxwDI3/u2ZzvW3rBqpmrDaqLxN5f1LcZeTZx287QeqQ==", "dependencies": { "tslib": "^2.3.0" }, @@ -514,9 +609,9 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/animations": "16.2.7", - "@angular/common": "16.2.7", - "@angular/core": "16.2.7" + "@angular/animations": "16.2.9", + "@angular/common": "16.2.9", + "@angular/core": "16.2.9" }, "peerDependenciesMeta": { "@angular/animations": { @@ -525,9 +620,9 @@ } }, "node_modules/@angular/platform-browser-dynamic": { - "version": "16.2.7", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-16.2.7.tgz", - "integrity": "sha512-raeuYEQfByHByLnA5YRR7fYD/5u6hMjONH77p08IjmtdmLb0XYP18l/C4YqsIOQG6kZLNCVWknEHZu3kuvAwtQ==", + "version": "16.2.9", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-16.2.9.tgz", + "integrity": "sha512-ztpo0939vTZ/5CWVSvo41Yl6YPoTZ0If+yTrs7dk1ce0vFgaZXMlc+y5ZwjJIiMM5CvHbhL48Uk+HJNIojP98A==", "dependencies": { "tslib": "^2.3.0" }, @@ -535,16 +630,16 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/common": "16.2.7", - "@angular/compiler": "16.2.7", - "@angular/core": "16.2.7", - "@angular/platform-browser": "16.2.7" + "@angular/common": "16.2.9", + "@angular/compiler": "16.2.9", + "@angular/core": "16.2.9", + "@angular/platform-browser": "16.2.9" } }, "node_modules/@angular/router": { - "version": "16.2.7", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-16.2.7.tgz", - "integrity": "sha512-CYhbhOqmBIraWjSzpiIZXV0JEx2fNAtRphQ5L/xdzU7G644+4v73SSQddoeX6l0FBkw2gqTisxr9w8/A6s2eCw==", + "version": "16.2.9", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-16.2.9.tgz", + "integrity": "sha512-5vrJNMblTDx3WC3dtaqLddWNtR0P9iwpqffeZL1uobBIwP4hbJx+8Dos3TwxGR4hnopFKahoDQ5nC0NOQslyog==", "dependencies": { "tslib": "^2.3.0" }, @@ -552,9 +647,9 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/common": "16.2.7", - "@angular/core": "16.2.7", - "@angular/platform-browser": "16.2.7", + "@angular/common": "16.2.9", + "@angular/core": "16.2.9", + "@angular/platform-browser": "16.2.9", "rxjs": "^6.5.3 || ^7.4.0" } }, @@ -578,9 +673,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.20.tgz", - "integrity": "sha512-BQYjKbpXjoXwFW5jGqiizJQQT/aC7pFm9Ok1OWssonuguICi264lbgMzRp2ZMmRSlfkX6DsWDDcsrctK8Rwfiw==", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.2.tgz", + "integrity": "sha512-0S9TQMmDHlqAZ2ITT95irXKfxN9bncq8ZCoJhun3nHL/lLUxd2NKBJYoNGWH7S0hz6fRQwWlAWn/ILM0C70KZQ==", "dev": true, "engines": { "node": ">=6.9.0" @@ -748,9 +843,9 @@ } }, "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.2.tgz", - "integrity": "sha512-k0qnnOqHn5dK9pZpfD5XXZ9SojAITdCKRn2Lp6rnDGzIbaP0rHyMPk/4wsSxVBVz4RfN0q6VpXWP2pDGIoQ7hw==", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.3.tgz", + "integrity": "sha512-WBrLmuPP47n7PNwsZ57pqam6G/RGo1vw/87b0Blc53tZNGZ4x7YvZ6HgQe2vo1W/FR20OgjeZuGXzudPiXHFug==", "dev": true, "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", @@ -1001,13 +1096,13 @@ } }, "node_modules/@babel/helpers": { - "version": "7.23.1", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.1.tgz", - "integrity": "sha512-chNpneuK18yW5Oxsr+t553UZzzAs3aZnFm4bxhebsNTeshrC95yA7l5yl7GBAG+JG1rF0F7zzD2EixK9mWSDoA==", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.2.tgz", + "integrity": "sha512-lzchcp8SjTSVe/fPmLwtWVBFC7+Tbn8LGHDVfDp9JGxpAY5opSaEFgt8UQvrnECWOTdji2mOWMz1rOhkHscmGQ==", "dev": true, "dependencies": { "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.0", + "@babel/traverse": "^7.23.2", "@babel/types": "^7.23.0" }, "engines": { @@ -1385,14 +1480,14 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.22.15.tgz", - "integrity": "sha512-jBm1Es25Y+tVoTi5rfd5t1KLmL8ogLKpXszboWOTTtGFGz2RKnQe2yn7HbZ+kb/B8N0FVSGQo874NSlOU1T4+w==", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.2.tgz", + "integrity": "sha512-BBYVGxbDVHfoeXbOwcagAkOQAm9NxoTdMGfTqghu1GrvadSaw6iW3Je6IcL5PNOw8VwjxqBECXy50/iCQSY/lQ==", "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-remap-async-to-generator": "^7.22.9", + "@babel/helper-remap-async-to-generator": "^7.22.20", "@babel/plugin-syntax-async-generators": "^7.8.4" }, "engines": { @@ -2322,9 +2417,9 @@ } }, "node_modules/@babel/traverse": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.0.tgz", - "integrity": "sha512-t/QaEvyIoIkwzpiZ7aoSKK8kObQYeF7T2v+dazAYCb8SXtp58zEVkWW7zAnju8FNKNdr4ScAOEDmMItbyOmEYw==", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", "dev": true, "dependencies": { "@babel/code-frame": "^7.22.13", @@ -3349,9 +3444,9 @@ } }, "node_modules/@ngtools/webpack": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-16.2.4.tgz", - "integrity": "sha512-ILri2xJ6vMUaFxHJABGF/H7/pYoBkuXTFlHCeFee9pHA+EHkxoiwezLf8baiFT3IGOmdG6GOUlfh/4QicGLdTQ==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-16.2.6.tgz", + "integrity": "sha512-d8ZlZL6dOtWmHdjG9PTGBkdiJMcsXD2tp6WeFRVvTEuvCI3XvKsUXBvJDE+mZOhzn5pUEYt+1TR5DHjDZbME3w==", "dev": true, "engines": { "node": "^16.14.0 || >=18.10.0", @@ -3548,13 +3643,13 @@ } }, "node_modules/@schematics/angular": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-16.2.4.tgz", - "integrity": "sha512-ZFPxn0yihdNcg5UpJvnfxIpv4GuW6nYDkgeIlYb5k/a0dKSW8wE8Akcl1JhJtdKJ0RVcn1OwZDmx028JCbZJLA==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-16.2.6.tgz", + "integrity": "sha512-fM09WPqST+nhVGV5Q3fhG7WKo96kgSVMsbz3wGS0DmTn4zge7ZWnrW3VvbxnMapmGoKa9DFPqdqNln4ADcdIMQ==", "dev": true, "dependencies": { - "@angular-devkit/core": "16.2.4", - "@angular-devkit/schematics": "16.2.4", + "@angular-devkit/core": "16.2.6", + "@angular-devkit/schematics": "16.2.6", "jsonc-parser": "3.2.0" }, "engines": { @@ -3786,9 +3881,9 @@ } }, "node_modules/@types/eslint": { - "version": "8.44.3", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.3.tgz", - "integrity": "sha512-iM/WfkwAhwmPff3wZuPLYiHX18HI24jU8k1ZSH7P8FHwxTjZ2P6CoX2wnF43oprR+YXJM6UUxATkNvyv/JHd+g==", + "version": "8.44.4", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.4.tgz", + "integrity": "sha512-lOzjyfY/D9QR4hY9oblZ76B90MYTB3RrQ4z2vBIJKj9ROCRqdkYl2gSUx1x1a4IWPjKJZLL4Aw1Zfay7eMnmnA==", "dev": true, "dependencies": { "@types/estree": "*", @@ -3812,9 +3907,9 @@ "dev": true }, "node_modules/@types/express": { - "version": "4.17.18", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.18.tgz", - "integrity": "sha512-Sxv8BSLLgsBYmcnGdGjjEjqET2U+AKAdCRODmMiq02FgjwuV75Ut85DRpvFjyw/Mk0vgUOliGRU0UUmuuZHByQ==", + "version": "4.17.19", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.19.tgz", + "integrity": "sha512-UtOfBtzN9OvpZPPbnnYunfjM7XCI4jyk1NvnFhTVz5krYAnW4o5DCoIekvms+8ApqhB4+9wSge1kBijdfTSmfg==", "dev": true, "dependencies": { "@types/body-parser": "*", @@ -3895,11 +3990,6 @@ "@types/geojson": "*" } }, - "node_modules/@types/luxon": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.3.2.tgz", - "integrity": "sha512-l5cpE57br4BIjK+9BSkFBOsWtwv6J9bJpC7gdXIzZyI0vuKvNTk0wZZrkQxMGsUAuGW9+WMNWF2IJMD7br2yeQ==" - }, "node_modules/@types/mime": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.3.tgz", @@ -3913,10 +4003,13 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.7.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.7.1.tgz", - "integrity": "sha512-LT+OIXpp2kj4E2S/p91BMe+VgGX2+lfO+XTpfXhh+bCk2LkQtHZSub8ewFBMGP5ClysPjTDFa4sMI8Q3n4T0wg==", - "dev": true + "version": "20.8.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.5.tgz", + "integrity": "sha512-SPlobFgbidfIeOYlzXiEjSYeIJiOCthv+9tSQVpvk4PAdIIc+2SmjNVzWXk9t0Y7dl73Zdf+OgXKHX9XtkqUpw==", + "dev": true, + "dependencies": { + "undici-types": "~5.25.1" + } }, "node_modules/@types/plist": { "version": "3.0.3", @@ -3996,9 +4089,9 @@ } }, "node_modules/@types/uuid": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.4.tgz", - "integrity": "sha512-zAuJWQflfx6dYJM62vna+Sn5aeSWhh3OB+wfUEACNcqUSc0AGc5JKl+ycL1vrH7frGTXhJchYjE1Hak8L819dA==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.5.tgz", + "integrity": "sha512-xfHdwa1FMJ082prjSJpoEI57GZITiQz10r3vEJCHa2khEFQjKy91aWKz6+zybzssCvXUwE1LQWgWVwZ4nYUvHQ==", "dev": true }, "node_modules/@types/verror": { @@ -4009,9 +4102,9 @@ "optional": true }, "node_modules/@types/ws": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.6.tgz", - "integrity": "sha512-8B5EO9jLVCy+B58PLHvLDuOD8DRVMgQzq8d55SjLCOn9kqGyqOvy27exVaTio1q1nX5zLu8/6N0n2ThSxOM6tg==", + "version": "8.5.7", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.7.tgz", + "integrity": "sha512-6UrLjiDUvn40CMrAubXuIVtj2PEfKDffJS7ychvnPU44j+KVeXmdHHTgqcM/dxLUTHxlXHiFM8Skmb8ozGdTnQ==", "dev": true, "dependencies": { "@types/node": "*" @@ -4943,13 +5036,13 @@ } }, "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.5.tgz", - "integrity": "sha512-19hwUH5FKl49JEsvyTcoHakh6BE0wgXLLptIyKZ3PijHc/Ci521wygORCUCCred+E/twuqRyAkE02BAWPmsHOg==", + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.6.tgz", + "integrity": "sha512-jhHiWVZIlnPbEUKSSNb9YoWcQGdlTLq7z1GHL4AjFxaoOUMuuEVJ+Y4pAaQUGOGk93YsVCKPbqbfw3m0SM6H8Q==", "dev": true, "dependencies": { "@babel/compat-data": "^7.22.6", - "@babel/helper-define-polyfill-provider": "^0.4.2", + "@babel/helper-define-polyfill-provider": "^0.4.3", "semver": "^6.3.1" }, "peerDependencies": { @@ -4966,12 +5059,12 @@ } }, "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.8.4", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.4.tgz", - "integrity": "sha512-9l//BZZsPR+5XjyJMPtZSK4jv0BsTO1zDac2GC6ygx9WLGlcsnRd1Co0B2zT5fF5Ic6BZy+9m3HNZ3QcOeDKfg==", + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.5.tgz", + "integrity": "sha512-Q6CdATeAvbScWPNLB8lzSO7fgUVBkQt6zLgNlfyeCr/EQaEQR+bWiBYYPYAFyE528BMjRhL+1QBMOI4jc/c5TA==", "dev": true, "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.4.2", + "@babel/helper-define-polyfill-provider": "^0.4.3", "core-js-compat": "^3.32.2" }, "peerDependencies": { @@ -4979,12 +5072,12 @@ } }, "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.2.tgz", - "integrity": "sha512-tAlOptU0Xj34V1Y2PNTL4Y0FOJMDB6bZmoW39FeCQIhigGLkqu3Fj6uiXpxIf6Ij274ENdYx64y6Au+ZKlb1IA==", + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.3.tgz", + "integrity": "sha512-8sHeDOmXC8csczMrYEOf0UTNa4yE2SxV5JGeT/LP1n0OYVDUUFPxG9vdk2AlDlIit4t+Kf0xCtpgXPBwnn/9pw==", "dev": true, "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.4.2" + "@babel/helper-define-polyfill-provider": "^0.4.3" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -5721,9 +5814,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001541", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001541.tgz", - "integrity": "sha512-bLOsqxDgTqUBkzxbNlSBt8annkDpQB9NdzdTbO2ooJ+eC/IQcvDspDc058g84ejCelF7vHUx57KIOjEecOHXaw==", + "version": "1.0.30001549", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001549.tgz", + "integrity": "sha512-qRp48dPYSCYaP+KurZLhDYdVE+yEyht/3NlmcJgVQ2VMGt6JL36ndQ/7rgspdZsJuxDPFIo/OzBT2+GmIJ53BA==", "dev": true, "funding": [ { @@ -5834,9 +5927,9 @@ "dev": true }, "node_modules/ci-info": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", - "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", "dev": true, "funding": [ { @@ -6230,12 +6323,12 @@ } }, "node_modules/core-js-compat": { - "version": "3.32.2", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.32.2.tgz", - "integrity": "sha512-+GjlguTDINOijtVRUxrQOv3kfu9rl+qPNdX2LTbJ/ZyVTuxK+ksVSAGX1nHstu4hrv1En/uPTtWgq2gI5wt4AQ==", + "version": "3.33.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.33.0.tgz", + "integrity": "sha512-0w4LcLXsVEuNkIqwjjf9rjCoPhK8uqA4tMRh4Ge26vfLtUutshn+aRJU21I9LCJlh2QQHfisNToLjw1XEJLTWw==", "dev": true, "dependencies": { - "browserslist": "^4.21.10" + "browserslist": "^4.22.1" }, "funding": { "type": "opencollective", @@ -6436,15 +6529,6 @@ "node": ">=8" } }, - "node_modules/cron": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/cron/-/cron-2.4.4.tgz", - "integrity": "sha512-MHlPImXJj3K7x7lyUHjtKEOl69CSlTOWxS89jiFgNkzXfvhVjhMz/nc7/EIfN9vgooZp8XTtXJ1FREdmbyXOiQ==", - "dependencies": { - "@types/luxon": "~3.3.0", - "luxon": "~3.3.0" - } - }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -6681,9 +6765,9 @@ } }, "node_modules/define-data-property": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.0.tgz", - "integrity": "sha512-UzGwzcjyv3OtAvolTj1GoyNYzfFR+iqbGjcnBEENZVCpM4/Ng1yhGNvS3lR/xDS74Tb2wGG9WzNSNIOS9UVb2g==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", "dev": true, "dependencies": { "get-intrinsic": "^1.2.1", @@ -7109,9 +7193,9 @@ } }, "node_modules/electron": { - "version": "26.2.4", - "resolved": "https://registry.npmjs.org/electron/-/electron-26.2.4.tgz", - "integrity": "sha512-weMUSMyDho5E0DPQ3breba3D96IxwNvtYHjMd/4/wNN3BdI5s3+0orNnPVGJFcLhSvKoxuKUqdVonUocBPwlQA==", + "version": "27.0.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-27.0.0.tgz", + "integrity": "sha512-mr3Zoy82l8XKK/TgguE5FeNeHZ9KHXIGIpUMjbjZWIREfAv+X2Q3vdX6RG0Pmi1K23AFAxANXQezIHBA2Eypwg==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -7562,15 +7646,15 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.536", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.536.tgz", - "integrity": "sha512-L4VgC/76m6y8WVCgnw5kJy/xs7hXrViCFdNKVG8Y7B2isfwrFryFyJzumh3ugxhd/oB1uEaEEvRdmeLrnd7OFA==", + "version": "1.4.554", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.554.tgz", + "integrity": "sha512-Q0umzPJjfBrrj8unkONTgbKQXzXRrH7sVV7D9ea2yBV3Oaogz991yhbpfvo2LMNkJItmruXTEzVpP9cp7vaIiQ==", "dev": true }, "node_modules/electron/node_modules/@types/node": { - "version": "18.18.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.0.tgz", - "integrity": "sha512-3xA4X31gHT1F1l38ATDIL9GpRLdwVhnEFC8Uikv5ZLlXATwrCYyPq7ZWHxzxc3J/30SUiwiYT+bQe0/XvKlWbw==", + "version": "18.18.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.5.tgz", + "integrity": "sha512-4slmbtwV59ZxitY4ixUZdy1uRLf9eSIvBWPQxNjhHYWEtn0FryfKpyS2cvADYXTayWdKEIsJengncrVvkI4I6A==", "dev": true }, "node_modules/elliptic": { @@ -8435,9 +8519,9 @@ } }, "node_modules/fraction.js": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.6.tgz", - "integrity": "sha512-n2aZ9tNfYDwaHhvFTkhFErqOMIb8uyzSQ+vGJBjZyanAKZVbGUQ1sngfk9FdkBw7G26O7AgNjLcecLffD1c7eg==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", "dev": true, "engines": { "node": "*" @@ -8518,10 +8602,13 @@ } }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/function.prototype.name": { "version": "1.1.6", @@ -8833,13 +8920,10 @@ "dev": true }, "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz", + "integrity": "sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==", "dev": true, - "dependencies": { - "function-bind": "^1.1.1" - }, "engines": { "node": ">= 0.4.0" } @@ -10217,9 +10301,9 @@ } }, "node_modules/joi": { - "version": "17.10.2", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.10.2.tgz", - "integrity": "sha512-hcVhjBxRNW/is3nNLdGLIjkgXetkeGc2wyhydhz8KumG23Aerk4HPjU5zaPAMRqXQFc0xNqXTC7+zQjxr0GlKA==", + "version": "17.11.0", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.11.0.tgz", + "integrity": "sha512-NgB+lZLNoqISVy1rZocE9PZI36bL/77ie924Ri43yEvi9GUUMPeyVIr8KdFTMUlby1p0PBYMk9spIxEUQYqrJQ==", "dev": true, "dependencies": { "@hapi/hoek": "^9.0.0", @@ -10438,9 +10522,9 @@ "dev": true }, "node_modules/keyv": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", - "integrity": "sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==", + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, "dependencies": { "json-buffer": "3.0.1" @@ -10465,13 +10549,13 @@ } }, "node_modules/launch-editor": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.6.0.tgz", - "integrity": "sha512-JpDCcQnyAAzZZaZ7vEiSqL690w7dAEyLao+KC96zBplnYbJS7TYNjvM3M7y3dGz+v7aIsJk3hllWuc0kWAjyRQ==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.6.1.tgz", + "integrity": "sha512-eB/uXmFVpY4zezmGp5XtU21kwo7GBbKB+EQ+UZeWtGb9yAM5xt/Evk+lYH3eRNAtId+ej4u7TYPFZ07w4s7rRw==", "dev": true, "dependencies": { "picocolors": "^1.0.0", - "shell-quote": "^1.7.3" + "shell-quote": "^1.8.1" } }, "node_modules/lazy-val": { @@ -10760,14 +10844,6 @@ "yallist": "^3.0.2" } }, - "node_modules/luxon": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.3.0.tgz", - "integrity": "sha512-An0UCfG/rSiqtAIiBPO0Y9/zAnHUZxAMiCpTd5h2smgsj7GGmcenvrvww2cqNA8/4A5ZrD1gJpHN2mIHZQF+Mg==", - "engines": { - "node": ">=12" - } - }, "node_modules/magic-string": { "version": "0.30.1", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.1.tgz", @@ -11667,9 +11743,9 @@ } }, "node_modules/npm-install-checks": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-6.2.0.tgz", - "integrity": "sha512-744wat5wAAHsxa4590mWO0tJ8PKxR8ORZsH9wGpQc3nWTzozMAgBN/XyqYw7mg3yqLM8dLwEnwSfKMmXAjF69g==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-6.3.0.tgz", + "integrity": "sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw==", "dev": true, "dependencies": { "semver": "^7.1.1" @@ -12660,9 +12736,9 @@ } }, "node_modules/postcss": { - "version": "8.4.27", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.27.tgz", - "integrity": "sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "dev": true, "funding": [ { @@ -12810,9 +12886,9 @@ "integrity": "sha512-KDeO94CbWI4pKsPnYpA1FPjo79EsY9I+M8ywoPBSf9XMXoe/0crjbUK7jcQEDHuc0ZMRIZsxH3TYLv4TUtHmAA==" }, "node_modules/primeng": { - "version": "16.4.1", - "resolved": "https://registry.npmjs.org/primeng/-/primeng-16.4.1.tgz", - "integrity": "sha512-IKOkl74gLDYyrtommqQ9T1isczlLV78qL0uTk5kjPJhyt4gdgEaYP0TAp4GrI2rwsW68BDo65xuP3vgD5LMmMw==", + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/primeng/-/primeng-16.5.0.tgz", + "integrity": "sha512-fgtTJZ76YODexoZctqCEg0on4BG4uCcJy1l4iaCoaHhMFzcgP89U4gdQ5debz0oEF4TekmBLLKvfEzGtF5fEgg==", "dependencies": { "tslib": "^2.3.0" }, @@ -14401,9 +14477,9 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.15.tgz", - "integrity": "sha512-lpT8hSQp9jAKp9mhtBU4Xjon8LPGBvLIuBiSVhMEtmLecTh2mO0tlqrAMp47tBXzMr13NJMQ2lf7RpQGLJ3HsQ==", + "version": "3.0.16", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz", + "integrity": "sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==", "dev": true }, "node_modules/spdy": { @@ -15354,6 +15430,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici-types": { + "version": "5.25.3", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.25.3.tgz", + "integrity": "sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA==", + "dev": true + }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", @@ -15742,10 +15824,11 @@ } }, "node_modules/webpack": { - "version": "5.88.2", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.88.2.tgz", - "integrity": "sha512-JmcgNZ1iKj+aiR0OvTYtWQqJwq37Pf683dY9bVORwVbUrDhLhdn/PlO2sHsFHPkj7sHNQF3JwaAkp49V+Sq1tQ==", + "version": "5.89.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.89.0.tgz", + "integrity": "sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw==", "dev": true, + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.3", "@types/estree": "^1.0.0", @@ -15967,6 +16050,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -15983,6 +16067,7 @@ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", "dev": true, + "peer": true, "peerDependencies": { "ajv": "^6.9.1" } @@ -15991,13 +16076,15 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "dev": true, + "peer": true }, "node_modules/webpack/node_modules/schema-utils": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", "dev": true, + "peer": true, "dependencies": { "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", diff --git a/desktop/package.json b/desktop/package.json index 2e48add7e..4143a6a8d 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -30,26 +30,25 @@ "lint": "ng lint" }, "dependencies": { - "@angular/animations": "16.2.7", - "@angular/cdk": "16.2.6", - "@angular/common": "16.2.7", - "@angular/compiler": "16.2.7", - "@angular/core": "16.2.7", - "@angular/forms": "16.2.7", - "@angular/language-service": "16.2.7", - "@angular/platform-browser": "16.2.7", - "@angular/platform-browser-dynamic": "16.2.7", - "@angular/router": "16.2.7", + "@angular/animations": "16.2.9", + "@angular/cdk": "16.2.8", + "@angular/common": "16.2.9", + "@angular/compiler": "16.2.9", + "@angular/core": "16.2.9", + "@angular/forms": "16.2.9", + "@angular/language-service": "16.2.9", + "@angular/platform-browser": "16.2.9", + "@angular/platform-browser-dynamic": "16.2.9", + "@angular/router": "16.2.9", "chart.js": "4.4.0", "chartjs-plugin-zoom": "2.0.1", - "cron": "2.4.4", "interactjs": "1.10.19", "leaflet": "1.9.4", "moment": "2.29.4", "panzoom": "9.4.3", "primeflex": "3.3.1", "primeicons": "6.0.1", - "primeng": "16.4.1", + "primeng": "16.5.0", "rxjs": "7.8.1", "tslib": "2.6.2", "uuid": "9.0.1", @@ -57,13 +56,13 @@ }, "devDependencies": { "@angular-builders/custom-webpack": "16.0.1", - "@angular-devkit/build-angular": "16.2.4", - "@angular/cli": "16.2.4", - "@angular/compiler-cli": "16.2.7", + "@angular-devkit/build-angular": "16.2.6", + "@angular/cli": "16.2.6", + "@angular/compiler-cli": "16.2.9", "@types/leaflet": "1.9.6", - "@types/node": "20.7.1", - "@types/uuid": "9.0.4", - "electron": "26.2.4", + "@types/node": "20.8.5", + "@types/uuid": "9.0.5", + "electron": "27.0.0", "electron-builder": "24.6.4", "electron-debug": "3.2.0", "electron-reloader": "1.2.3", diff --git a/desktop/src/app/alignment/alignment.component.html b/desktop/src/app/alignment/alignment.component.html new file mode 100644 index 000000000..9f2b978bf --- /dev/null +++ b/desktop/src/app/alignment/alignment.component.html @@ -0,0 +1,95 @@ + + + + + + + + Camera + + + + + + + + + + + + + + Guide Output + + + + + + + + + + + {{ darvStatus }} + + + {{ darvDirection }} + + + {{ darvCaptureEvent.captureRemainingTime | exposureTime }} + + + {{ darvCaptureEvent.captureProgress | percent:'1.1-1' }} + + + + + + + + + + + + + Initial pause (s) + + + + + + Drift for (s) + + + + + + Hemisphere + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/desktop/src/app/alignment/alignment.component.scss b/desktop/src/app/alignment/alignment.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/desktop/src/app/alignment/alignment.component.ts b/desktop/src/app/alignment/alignment.component.ts new file mode 100644 index 000000000..340bdffa2 --- /dev/null +++ b/desktop/src/app/alignment/alignment.component.ts @@ -0,0 +1,187 @@ +import { AfterViewInit, Component, HostListener, NgZone, OnDestroy } from '@angular/core' +import { ApiService } from '../../shared/services/api.service' +import { BrowserWindowService } from '../../shared/services/browser-window.service' +import { ElectronService } from '../../shared/services/electron.service' +import { PreferenceService } from '../../shared/services/preference.service' +import { Camera, CameraCaptureEvent, DARVPolarAlignmentEvent, DARVPolarAlignmentGuidePulseElapsed, DARVPolarAlignmentInitialPauseElapsed, GuideDirection, GuideOutput, Hemisphere } from '../../shared/types' +import { AppComponent } from '../app.component' + +@Component({ + selector: 'app-alignment', + templateUrl: './alignment.component.html', + styleUrls: ['./alignment.component.scss'], +}) +export class AlignmentComponent implements AfterViewInit, OnDestroy { + + cameras: Camera[] = [] + camera?: Camera + cameraConnected = false + + guideOutputs: GuideOutput[] = [] + guideOutput?: GuideOutput + guideOutputConnected = false + + darvInitialPause = 5 + darvDrift = 30 + darvInProgress = false + readonly darvHemispheres: Hemisphere[] = ['NORTHERN', 'SOUTHERN'] + darvHemisphere: Hemisphere = 'NORTHERN' + darvCaptureEvent?: CameraCaptureEvent + darvDirection?: GuideDirection + darvStatus = 'idle' + + constructor( + app: AppComponent, + private api: ApiService, + private browserWindow: BrowserWindowService, + private electron: ElectronService, + private preference: PreferenceService, + ngZone: NgZone, + ) { + app.title = 'Alignment' + + electron.on('CAMERA_UPDATED', (_, event: Camera) => { + if (event.name === this.camera?.name) { + ngZone.run(() => { + Object.assign(this.camera!, event) + this.updateCamera() + }) + } + }) + + electron.on('GUIDE_OUTPUT_UPDATED', (_, event: GuideOutput) => { + if (event.name === this.guideOutput?.name) { + ngZone.run(() => { + Object.assign(this.guideOutput!, event) + this.updateGuideOutput() + }) + } + }) + + electron.on('DARV_POLAR_ALIGNMENT_STARTED', (_, event: DARVPolarAlignmentEvent) => { + 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: DARVPolarAlignmentEvent) => { + 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_INITIAL_PAUSE_ELAPSED', (_, event: DARVPolarAlignmentInitialPauseElapsed) => { + if (event.camera.name === this.camera?.name && + event.guideOutput.name === this.guideOutput?.name) { + ngZone.run(() => { + this.darvStatus = 'initial pause' + }) + } + }) + + electron.on('DARV_POLAR_ALIGNMENT_GUIDE_PULSE_ELAPSED', (_, event: DARVPolarAlignmentGuidePulseElapsed) => { + if (event.camera.name === this.camera?.name && + event.guideOutput.name === this.guideOutput?.name) { + ngZone.run(() => { + this.darvDirection = event.direction + this.darvStatus = event.forward ? 'forwarding' : 'backwarding' + }) + } + }) + + electron.on('CAMERA_EXPOSURE_UPDATED', (_, event: CameraCaptureEvent) => { + if (event.camera.name === this.camera?.name) { + ngZone.run(() => { + this.darvCaptureEvent = event + }) + } + }) + } + + async ngAfterViewInit() { + this.cameras = await this.api.cameras() + this.guideOutputs = await this.api.guideOutputs() + } + + @HostListener('window:unload') + ngOnDestroy() { + this.darvStop() + } + + async cameraChanged() { + if (this.camera) { + const camera = await this.api.camera(this.camera.name) + Object.assign(this.camera, camera) + + this.updateCamera() + } + } + + async guideOutputChanged() { + if (this.guideOutput) { + const guideOutput = await this.api.guideOutput(this.guideOutput.name) + Object.assign(this.guideOutput, guideOutput) + + this.updateGuideOutput() + } + } + + cameraConnect() { + if (this.cameraConnected) { + this.api.cameraDisconnect(this.camera!) + } else { + this.api.cameraConnect(this.camera!) + } + } + + guideOutputConnect() { + if (this.guideOutputConnected) { + this.api.guideOutputDisconnect(this.guideOutput!) + } else { + this.api.guideOutputConnect(this.guideOutput!) + } + } + + private async darvStart(direction: GuideDirection) { + // TODO: Horizonte leste e oeste tem um impacto no "reversed"? + const reversed = this.darvHemisphere === 'SOUTHERN' + await this.browserWindow.openCameraImage(this.camera!) + await this.api.darvStart(this.camera!, this.guideOutput!, this.darvDrift, this.darvInitialPause, direction, reversed) + } + + darvAzimuth() { + this.darvStart('EAST') + } + + darvAltitude() { + this.darvStart('EAST') // TODO: NORTH não é usado? + } + + darvStop() { + this.api.darvStop(this.camera!, this.guideOutput!) + } + + private async updateCamera() { + if (!this.camera) { + return + } + + this.cameraConnected = this.camera.connected + } + + private async updateGuideOutput() { + if (!this.guideOutput) { + return + } + + this.guideOutputConnected = this.guideOutput.connected + } +} \ No newline at end of file diff --git a/desktop/src/app/app-routing.module.ts b/desktop/src/app/app-routing.module.ts index d2c144b9b..dc63f7892 100644 --- a/desktop/src/app/app-routing.module.ts +++ b/desktop/src/app/app-routing.module.ts @@ -2,6 +2,7 @@ import { NgModule } from '@angular/core' import { RouterModule, Routes } from '@angular/router' import { APP_CONFIG } from '../environments/environment' import { AboutComponent } from './about/about.component' +import { AlignmentComponent } from './alignment/alignment.component' import { AtlasComponent } from './atlas/atlas.component' import { CameraComponent } from './camera/camera.component' import { FilterWheelComponent } from './filterwheel/filterwheel.component' @@ -59,6 +60,10 @@ const routes: Routes = [ path: 'framing', component: FramingComponent, }, + { + path: 'alignment', + component: AlignmentComponent, + }, { path: 'about', component: AboutComponent, diff --git a/desktop/src/app/app.component.html b/desktop/src/app/app.component.html index 8d6ffc830..3ce2113b1 100644 --- a/desktop/src/app/app.component.html +++ b/desktop/src/app/app.component.html @@ -1,10 +1,16 @@ - + {{ title }} - - - - - + + + + + diff --git a/desktop/src/app/app.component.scss b/desktop/src/app/app.component.scss index 89a6d1f22..e69de29bb 100644 --- a/desktop/src/app/app.component.scss +++ b/desktop/src/app/app.component.scss @@ -1,3 +0,0 @@ -:host { - -} \ No newline at end of file diff --git a/desktop/src/app/app.module.ts b/desktop/src/app/app.module.ts index 907cd712f..dad16113a 100644 --- a/desktop/src/app/app.module.ts +++ b/desktop/src/app/app.module.ts @@ -40,6 +40,7 @@ import { EnvPipe } from '../shared/pipes/env.pipe' import { ExposureTimePipe } from '../shared/pipes/exposureTime.pipe' import { WinPipe } from '../shared/pipes/win.pipe' import { AboutComponent } from './about/about.component' +import { AlignmentComponent } from './alignment/alignment.component' import { AppRoutingModule } from './app-routing.module' import { AppComponent } from './app.component' import { AtlasComponent } from './atlas/atlas.component' @@ -72,6 +73,7 @@ import { MountComponent } from './mount/mount.component' MountComponent, GuiderComponent, DialogMenuComponent, + AlignmentComponent, LocationDialog, EnvPipe, WinPipe, diff --git a/desktop/src/app/atlas/atlas.component.html b/desktop/src/app/atlas/atlas.component.html index 731dbfb79..adfe9bb91 100644 --- a/desktop/src/app/atlas/atlas.component.html +++ b/desktop/src/app/atlas/atlas.component.html @@ -56,7 +56,7 @@ + severity="success" size="small" /> @@ -90,8 +90,8 @@ - - + + @@ -129,8 +129,8 @@ - - + + @@ -169,9 +169,9 @@ + size="small" severity="info" /> + size="small" /> @@ -207,15 +207,15 @@ + styleClass="p-inputtext-sm border-0" emptyMessage="No location found" /> Location - - + + + size="small" severity="danger" /> @@ -302,13 +302,12 @@ - - - - + + + + @@ -402,7 +401,7 @@ - + @@ -468,7 +467,7 @@ - + @@ -482,8 +481,7 @@ - - + + \ No newline at end of file diff --git a/desktop/src/app/atlas/atlas.component.ts b/desktop/src/app/atlas/atlas.component.ts index a8c316e3b..162636b83 100644 --- a/desktop/src/app/atlas/atlas.component.ts +++ b/desktop/src/app/atlas/atlas.component.ts @@ -1,10 +1,10 @@ import { AfterContentInit, Component, ElementRef, HostListener, OnDestroy, OnInit, ViewChild } from '@angular/core' import { Chart, ChartData, ChartOptions } from 'chart.js' import zoomPlugin from 'chartjs-plugin-zoom' -import { CronJob } from 'cron' import { UIChart } from 'primeng/chart' import { DialogService } from 'primeng/dynamicdialog' import { ListboxChangeEvent } from 'primeng/listbox' +import { EVERY_MINUTE_CRON_TIME } from '../../shared/constants' import { LocationDialog } from '../../shared/dialogs/location/location.dialog' import { oneDecimalPlaceFormatter, twoDigitsFormatter } from '../../shared/formatters' import { ApiService } from '../../shared/services/api.service' @@ -13,7 +13,7 @@ import { ElectronService } from '../../shared/services/electron.service' import { PreferenceService } from '../../shared/services/preference.service' import { CONSTELLATIONS, Constellation, DeepSkyObject, EMPTY_BODY_POSITION, EMPTY_LOCATION, Location, - MinorPlanet, SATELLITE_GROUP_TYPES, Satellite, SatelliteGroupType, SkyObjectType, Star, Union + MinorPlanet, SATELLITE_GROUPS, Satellite, SatelliteGroupType, SkyObjectType, Star, Union } from '../../shared/types' import { AppComponent } from '../app.component' @@ -345,8 +345,6 @@ export class AtlasComponent implements OnInit, AfterContentInit, OnDestroy { readonly altitudeOptions: ChartOptions = { responsive: true, - aspectRatio: 1.8, - maintainAspectRatio: false, plugins: { legend: { display: false, @@ -406,8 +404,8 @@ export class AtlasComponent implements OnInit, AfterContentInit, OnDestroy { scales: { y: { beginAtZero: true, - suggestedMin: 0, - suggestedMax: 90, + min: 0, + max: 90, ticks: { autoSkip: false, count: 10, @@ -454,12 +452,6 @@ export class AtlasComponent implements OnInit, AfterContentInit, OnDestroy { } } - private readonly cronJob = new CronJob('0 */1 * * * *', () => { - if (!this.useManualDateTime) { - this.refreshTab() - } - }, null, false) - private static readonly DEFAULT_SATELLITE_FILTERS: SatelliteGroupType[] = [ 'AMATEUR', 'BEIDOU', 'GALILEO', 'GLO_OPS', 'GNSS', 'GPS_OPS', 'ONEWEB', 'SCIENCE', 'STARLINK', 'STATIONS', 'VISUAL' @@ -475,16 +467,22 @@ export class AtlasComponent implements OnInit, AfterContentInit, OnDestroy { ) { app.title = 'Sky Atlas' - for (const item of SATELLITE_GROUP_TYPES) { + for (const item of SATELLITE_GROUPS) { const enabled = preference.get(`atlas.satellite.filter.${item}`, AtlasComponent.DEFAULT_SATELLITE_FILTERS.includes(item)) this.satelliteSearchGroup.set(item, enabled) } + electron.on('CRON_TICKED', () => { + if (!this.useManualDateTime) { + this.refreshTab() + } + }) + // TODO: Refresh graph and twilight if hours past 12 (noon) } ngOnInit() { - this.cronJob.start() + this.electron.registerCron(EVERY_MINUTE_CRON_TIME) } async ngAfterContentInit() { @@ -511,7 +509,7 @@ export class AtlasComponent implements OnInit, AfterContentInit, OnDestroy { @HostListener('window:unload') ngOnDestroy() { - this.cronJob.stop() + this.electron.unregisterCron() } tabChanged() { @@ -604,11 +602,11 @@ export class AtlasComponent implements OnInit, AfterContentInit, OnDestroy { this.refreshing = true try { - for (const item of SATELLITE_GROUP_TYPES) { + for (const item of SATELLITE_GROUPS) { this.preference.set(`atlas.satellite.filter.${item}`, this.satelliteSearchGroup.get(item)) } - const groups = SATELLITE_GROUP_TYPES.filter(e => this.satelliteSearchGroup.get(e)) + const groups = SATELLITE_GROUPS.filter(e => this.satelliteSearchGroup.get(e)) this.satelliteItems = await this.api.searchSatellites(this.satelliteSearchText, groups) } finally { this.refreshing = false @@ -616,7 +614,7 @@ export class AtlasComponent implements OnInit, AfterContentInit, OnDestroy { } resetSatelliteFilter() { - for (const item of SATELLITE_GROUP_TYPES) { + for (const item of SATELLITE_GROUPS) { const enabled = AtlasComponent.DEFAULT_SATELLITE_FILTERS.includes(item) this.preference.set(`atlas.satellite.filter.${item}`, enabled) this.satelliteSearchGroup.set(item, enabled) @@ -681,7 +679,7 @@ export class AtlasComponent implements OnInit, AfterContentInit, OnDestroy { async mountSlew() { const mount = await this.electron.selectedMount() if (!mount?.connected) return - this.api.mountSlewTo(mount, this.bodyPosition.rightAscension, this.bodyPosition.declination, false) + this.api.mountSlew(mount, this.bodyPosition.rightAscension, this.bodyPosition.declination, false) } async mountSync() { diff --git a/desktop/src/app/camera/camera.component.html b/desktop/src/app/camera/camera.component.html index cf1213594..1fd6f26a7 100644 --- a/desktop/src/app/camera/camera.component.html +++ b/desktop/src/app/camera/camera.component.html @@ -3,19 +3,19 @@ + styleClass="p-inputtext-sm border-0" emptyMessage="No camera found" /> Camera + size="small" severity="danger" pTooltip="Disconnect" tooltipPosition="bottom" /> + size="small" severity="info" pTooltip="Connect" tooltipPosition="bottom" /> - + @@ -86,11 +86,12 @@ + [step]="0.1" suffix="℃" [min]="-50" [max]="50" locale="en" styleClass="p-inputtext-sm border-0" + [allowEmpty]="false" /> Temperature ({{ temperature | number: '1.1-1' }}°C) + severity="sucsess" pTooltip="Apply" tooltipPosition="bottom" /> @@ -99,7 +100,7 @@ Exposure Time @@ -110,7 +111,8 @@ - Frame Type @@ -119,13 +121,15 @@ Exposure Mode - Delay (s) @@ -133,7 +137,8 @@ Count @@ -143,7 +148,7 @@ X @@ -151,7 +156,7 @@ Y @@ -159,7 +164,7 @@ Width @@ -167,7 +172,7 @@ Height @@ -180,19 +185,21 @@ + severity="info" pTooltip="Full size" tooltipPosition="bottom" /> + [step]="1.0" [min]="1" [max]="4" styleClass="p-inputtext-sm border-0" [allowEmpty]="false" + (ngModelChange)="savePreference()" /> Bin X + [step]="1.0" [min]="1" [max]="4" styleClass="p-inputtext-sm border-0" [allowEmpty]="false" + (ngModelChange)="savePreference()" /> Bin Y @@ -200,7 +207,8 @@ - Frame Format @@ -208,7 +216,7 @@ Gain @@ -216,7 +224,7 @@ Offset @@ -225,9 +233,9 @@ + severity="sucsess" /> + severity="danger" /> diff --git a/desktop/src/app/camera/camera.component.ts b/desktop/src/app/camera/camera.component.ts index 234d41b93..b64f99dc0 100644 --- a/desktop/src/app/camera/camera.component.ts +++ b/desktop/src/app/camera/camera.component.ts @@ -179,10 +179,10 @@ export class CameraComponent implements AfterContentInit, OnDestroy { api.startListening('CAMERA') - electron.on('CAMERA_UPDATED', (_, camera: Camera) => { - if (camera.name === this.camera?.name) { + electron.on('CAMERA_UPDATED', (_, event: Camera) => { + if (event.name === this.camera?.name) { ngZone.run(() => { - Object.assign(this.camera!, camera) + Object.assign(this.camera!, event) this.update() }) } @@ -216,9 +216,9 @@ export class CameraComponent implements AfterContentInit, OnDestroy { } }) - electron.on('WHEEL_CHANGED', (_, wheel?: FilterWheel) => { + electron.on('WHEEL_CHANGED', (_, event?: FilterWheel) => { ngZone.run(() => { - this.wheel = wheel + this.wheel = event }) }) } diff --git a/desktop/src/app/filterwheel/filterwheel.component.html b/desktop/src/app/filterwheel/filterwheel.component.html index 9aaae4709..9c02bd622 100644 --- a/desktop/src/app/filterwheel/filterwheel.component.html +++ b/desktop/src/app/filterwheel/filterwheel.component.html @@ -4,15 +4,15 @@ + styleClass="p-inputtext-sm border-0" emptyMessage="No filter wheel found" /> Filter Wheel + size="small" severity="danger" /> + size="small" severity="info" /> @@ -20,15 +20,16 @@ - + - + Position - + - @@ -47,11 +48,11 @@ - - + + diff --git a/desktop/src/app/filterwheel/filterwheel.component.ts b/desktop/src/app/filterwheel/filterwheel.component.ts index dfe9e3ec4..b09fb4e70 100644 --- a/desktop/src/app/filterwheel/filterwheel.component.ts +++ b/desktop/src/app/filterwheel/filterwheel.component.ts @@ -47,10 +47,10 @@ export class FilterWheelComponent implements AfterContentInit, OnDestroy { ) { app.title = 'Filter Wheel' - electron.on('WHEEL_UPDATED', (_, wheel: FilterWheel) => { - if (wheel.name === this.wheel?.name) { + electron.on('WHEEL_UPDATED', (_, event: FilterWheel) => { + if (event.name === this.wheel?.name) { ngZone.run(() => { - Object.assign(this.wheel!, wheel) + Object.assign(this.wheel!, event) this.update() }) } @@ -81,11 +81,11 @@ export class FilterWheelComponent implements AfterContentInit, OnDestroy { this.electron.send('WHEEL_CHANGED', this.wheel) } - async connect() { + connect() { if (this.connected) { - await this.api.wheelDisconnect(this.wheel!) + this.api.wheelDisconnect(this.wheel!) } else { - await this.api.wheelConnect(this.wheel!) + this.api.wheelConnect(this.wheel!) } } diff --git a/desktop/src/app/focuser/focuser.component.html b/desktop/src/app/focuser/focuser.component.html index 2d6bb7955..c42d97b56 100644 --- a/desktop/src/app/focuser/focuser.component.html +++ b/desktop/src/app/focuser/focuser.component.html @@ -3,15 +3,15 @@ + styleClass="p-inputtext-sm border-0" emptyMessage="No focuser found" /> Focuser + size="small" severity="danger" pTooltip="Disconnect" tooltipPosition="bottom" /> + size="small" severity="info" pTooltip="Connect" tooltipPosition="bottom" /> @@ -21,42 +21,42 @@ - + Position - + Temperature (°C) - + + [text]="true" size="small" pTooltip="Move In" tooltipPosition="bottom" /> - Relative + [text]="true" size="small" pTooltip="Move Out" tooltipPosition="bottom" /> - + Absolute + [text]="true" severity="success" size="small" pTooltip="Move To" tooltipPosition="bottom" /> + [text]="true" severity="info" size="small" pTooltip="Sync" tooltipPosition="bottom" /> \ No newline at end of file diff --git a/desktop/src/app/focuser/focuser.component.ts b/desktop/src/app/focuser/focuser.component.ts index b7fdcb6ba..3365fb6ca 100644 --- a/desktop/src/app/focuser/focuser.component.ts +++ b/desktop/src/app/focuser/focuser.component.ts @@ -41,10 +41,10 @@ export class FocuserComponent implements AfterViewInit, OnDestroy { ) { app.title = 'Focuser' - electron.on('FOCUSER_UPDATED', (_, focuser: Focuser) => { - if (focuser.name === this.focuser?.name) { + electron.on('FOCUSER_UPDATED', (_, event: Focuser) => { + if (event.name === this.focuser?.name) { ngZone.run(() => { - Object.assign(this.focuser!, focuser) + Object.assign(this.focuser!, event) this.update() }) } @@ -75,11 +75,11 @@ export class FocuserComponent implements AfterViewInit, OnDestroy { this.electron.send('FOCUSER_CHANGED', this.focuser) } - async connect() { + connect() { if (this.connected) { - await this.api.focuserDisconnect(this.focuser!) + this.api.focuserDisconnect(this.focuser!) } else { - await this.api.focuserConnect(this.focuser!) + this.api.focuserConnect(this.focuser!) } } diff --git a/desktop/src/app/framing/framing.component.html b/desktop/src/app/framing/framing.component.html index 597776558..5f5242f5e 100644 --- a/desktop/src/app/framing/framing.component.html +++ b/desktop/src/app/framing/framing.component.html @@ -1,40 +1,40 @@ - + RA (J2000) - + DEC (J2000) - Width - Height - FOV (°) - Rotation (°) @@ -59,7 +59,7 @@ - + Made use of { - ngZone.run(() => this.frameFromParams(data)) + electron.on('PARAMS_CHANGED', (_, event: FramingParams) => { + ngZone.run(() => this.frameFromParams(event)) }) } diff --git a/desktop/src/app/guider/guider.component.html b/desktop/src/app/guider/guider.component.html index 362259738..ccd0f094b 100644 --- a/desktop/src/app/guider/guider.component.html +++ b/desktop/src/app/guider/guider.component.html @@ -1,66 +1,243 @@ - - - - Camera - - - - - - - - - Mount - - - - - - - - - Guide Output - - - - - - - - - - + + + + + + Host + + + + + + Port + + + + + + + + + + + {{ phdState | enum | lowercase }} + {{ phdMessage }} + + + + + + + + + Dither (px) + + + + Dither in RA only + + + + + + Settle tolerance (px) + + + + + + Minimum settle time (s) + + + + + + Settle timeout (s) + + + + + + + + + + RMS RA + + + + + + RMS DEC + + + + + + RMS Total + + + + + + Star mass + + + + + + HFD + + + + + + SNR + + + + + + Plot Mode + + + + + + Unit + + + + + + + + North + East + + + + + + + + + + - - - - + + + + + Guide Output + + + + + + + + + + North (ms) + + + + + + + West (ms) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + East (ms) + + + + + + + South (ms) + + + + - - - - - - - - - - - \ No newline at end of file diff --git a/desktop/src/app/guider/guider.component.ts b/desktop/src/app/guider/guider.component.ts index f780a8cde..ebd3023be 100644 --- a/desktop/src/app/guider/guider.component.ts +++ b/desktop/src/app/guider/guider.component.ts @@ -1,10 +1,16 @@ -import { AfterViewInit, Component, HostListener, NgZone, OnDestroy } from '@angular/core' +import { AfterViewInit, Component, HostListener, NgZone, OnDestroy, ViewChild } from '@angular/core' import { Title } from '@angular/platform-browser' +import { ChartData, ChartOptions } from 'chart.js' +import { UIChart } from 'primeng/chart' import { ApiService } from '../../shared/services/api.service' import { BrowserWindowService } from '../../shared/services/browser-window.service' import { ElectronService } from '../../shared/services/electron.service' import { PreferenceService } from '../../shared/services/preference.service' -import { Camera, GuideOutput, GuideTrackingBox, Guider, ImageStarSelected, Mount } from '../../shared/types' +import { GuideDirection, GuideOutput, GuideStar, GuideState, GuideStep, GuiderStatus, HistoryStep } from '../../shared/types' + +export type PlotMode = 'RA/DEC' | 'DX/DY' + +export type YAxisUnit = 'ARCSEC' | 'PIXEL' @Component({ selector: 'app-guider', @@ -13,22 +19,199 @@ import { Camera, GuideOutput, GuideTrackingBox, Guider, ImageStarSelected, Mount }) export class GuiderComponent implements AfterViewInit, OnDestroy { - cameras: Camera[] = [] - camera?: Camera - cameraConnected = false - - mounts: Mount[] = [] - mount?: Mount - mountConnected = false - guideOutputs: GuideOutput[] = [] guideOutput?: GuideOutput guideOutputConnected = false + pulseGuiding = false + + guideNorthDuration = 1000 + guideSouthDuration = 1000 + guideWestDuration = 1000 + guideEastDuration = 1000 + + phdConnected = false + phdHost = 'localhost' + phdPort = 4400 + phdState: GuideState = 'STOPPED' + phdGuideStep?: GuideStep + phdMessage = '' + + phdDitherPixels = 5 + phdDitherRAOnly = false + phdSettleAmount = 1.5 + phdSettleTime = 10 + phdSettleTimeout = 30 + readonly phdGuideHistory: HistoryStep[] = [] + private phdDurationScale = 1.0 + + phdPixelScale = 1.0 + phdRmsRA = 0.0 + phdRmsDEC = 0.0 + phdRmsTotal = 0.0 + + readonly plotModes: PlotMode[] = ['RA/DEC', 'DX/DY'] + plotMode: PlotMode = 'RA/DEC' + readonly yAxisUnits: YAxisUnit[] = ['ARCSEC', 'PIXEL'] + yAxisUnit: YAxisUnit = 'ARCSEC' + + @ViewChild('phdChart') + private readonly phdChart!: UIChart + + get stopped() { + return this.phdState === 'STOPPED' + } - private guider?: Guider - looping = false - guiding = false - calibrating = false + get looping() { + return this.phdState === 'LOOPING' + } + + get guiding() { + return this.phdState === 'GUIDING' + } + + readonly phdChartData: ChartData = { + labels: Array.from({ length: 100 }, (_, i) => `${i}`), + datasets: [ + // RA. + { + type: 'line', + fill: false, + borderColor: '#F44336', + borderWidth: 2, + data: [], + pointRadius: 0, + pointHitRadius: 0, + }, + // DEC. + { + type: 'line', + fill: false, + borderColor: '#03A9F4', + borderWidth: 2, + data: [], + pointRadius: 0, + pointHitRadius: 0, + }, + // RA. + { + type: 'bar', + backgroundColor: '#F4433630', + data: [], + }, + // DEC. + { + type: 'bar', + backgroundColor: '#03A9F430', + data: [], + }, + ] + } + + readonly phdChartOptions: ChartOptions = { + responsive: true, + plugins: { + legend: { + display: false, + }, + tooltip: { + displayColors: false, + intersect: false, + filter: (item) => { + return Math.abs(item.parsed.y) - 0.01 > 0.0 + }, + callbacks: { + title: () => { + return '' + }, + label: (context) => { + console.log(context) + const barType = context.dataset.type === 'bar' + const raType = context.datasetIndex === 0 || context.datasetIndex === 2 + const scale = barType ? this.phdDurationScale : 1.0 + const y = context.parsed.y * scale + const prefix = raType ? 'RA: ' : 'DEC: ' + const barSuffix = ' ms' + const lineSuffix = this.yAxisUnit === 'ARCSEC' ? '"' : 'px' + const formattedY = prefix + (barType ? y.toFixed(0) + barSuffix : y.toFixed(2) + lineSuffix) + return formattedY + } + } + }, + zoom: { + zoom: { + wheel: { + enabled: true, + }, + pinch: { + enabled: false, + }, + mode: 'x', + scaleMode: 'xy', + }, + pan: { + enabled: true, + mode: 'xy', + }, + limits: { + x: { + min: 0, + max: 100, + }, + y: { + min: -16, + max: 16, + }, + } + }, + }, + scales: { + y: { + stacked: true, + beginAtZero: false, + min: -16, + max: 16, + ticks: { + autoSkip: false, + count: 7, + callback: (value) => { + return (value as number).toFixed(1).padStart(5, ' ') + } + }, + border: { + display: true, + dash: [2, 4], + }, + grid: { + display: true, + drawTicks: false, + drawOnChartArea: true, + color: '#212121', + } + }, + x: { + stacked: true, + min: 0, + max: 100, + border: { + display: true, + dash: [2, 4], + }, + ticks: { + stepSize: 5.0, + maxRotation: 0, + minRotation: 0, + callback: (value) => { + return (value as number).toFixed(0) + } + }, + grid: { + display: true, + drawTicks: false, + color: '#212121', + } + } + } + } constructor( title: Title, @@ -41,115 +224,150 @@ export class GuiderComponent implements AfterViewInit, OnDestroy { title.setTitle('Guider') api.startListening('GUIDING') - api.startListening('CAMERA') - api.startListening('MOUNT') - electron.on('CAMERA_UPDATED', (_, camera: Camera) => { - if (camera.name === this.camera?.name) { + electron.on('GUIDE_OUTPUT_UPDATED', (_, event: GuideOutput) => { + if (event.name === this.guideOutput?.name) { ngZone.run(() => { - Object.assign(this.camera!, camera) + Object.assign(this.guideOutput!, event) this.update() }) } }) - electron.on('MOUNT_UPDATED', (_, mount: Mount) => { - if (mount.name === this.mount?.name) { - ngZone.run(() => { - Object.assign(this.mount!, mount) - this.update() - }) - } + electron.on('GUIDE_OUTPUT_ATTACHED', (_, event: GuideOutput) => { + ngZone.run(() => { + this.guideOutputs.push(event) + }) }) - electron.on('GUIDE_OUTPUT_UPDATED', (_, guideOutput: GuideOutput) => { - if (guideOutput.name === this.guideOutput?.name) { - ngZone.run(() => { - Object.assign(this.guideOutput!, guideOutput) - this.update() - }) - } + electron.on('GUIDE_OUTPUT_DETACHED', (_, event: GuideOutput) => { + ngZone.run(() => { + const index = this.guideOutputs.findIndex(e => e.name === event.name) + if (index) this.guideOutputs.splice(index, 1) + }) }) - electron.on('GUIDE_OUTPUT_ATTACHED', (_, guideOutput: GuideOutput) => { + electron.on('GUIDER_CONNECTED', () => { ngZone.run(() => { - this.guideOutputs.push(guideOutput) + this.phdConnected = true }) }) - electron.on('GUIDE_OUTPUT_DETACHED', (_, guideOutput: GuideOutput) => { + electron.on('GUIDER_DISCONNECTED', () => { ngZone.run(() => { - const index = this.guideOutputs.findIndex(e => e.name === guideOutput.name) - if (index) this.guideOutputs.splice(index, 1) + this.phdConnected = false }) }) - electron.on('IMAGE_STAR_SELECTED', async (_, star: ImageStarSelected) => { - if (!this.guiding && star.camera.name === this.camera?.name) { - await this.api.selectGuideStar(star.x, star.y) - } + electron.on('GUIDER_UPDATED', (_, event: GuiderStatus) => { + ngZone.run(() => { + this.processGuiderStatus(event) + }) }) - electron.on('GUIDE_LOCK_POSITION_CHANGED', (_, guider: Guider) => { - this.guider = guider - + electron.on('GUIDER_STEPPED', (_, event: HistoryStep | GuideStar) => { ngZone.run(() => { - this.updateGuideState() + if ("id" in event) { + if (this.phdGuideHistory.length >= 100) { + this.phdGuideHistory.splice(0, 1) + } + + this.phdGuideHistory.push(event) + this.updateGuideHistoryChart() + } + + if ("guideStep" in event && event.guideStep) { + this.phdGuideStep = event.guideStep + } }) + }) - this.drawTrackingBox() + electron.on('GUIDER_MESSAGE_RECEIVED', (_, event: { message: string }) => { + ngZone.run(() => { + this.phdMessage = event.message + }) }) } async ngAfterViewInit() { - this.cameras = await this.api.cameras() - this.mounts = await this.api.mounts() - this.guideOutputs = await this.api.attachedGuideOutputs() + this.phdSettleAmount = this.preference.get('guiding.settleAmount', 1.5) + this.phdSettleTime = this.preference.get('guiding.settleTime', 10) + this.phdSettleTimeout = this.preference.get('guiding.settleTimeout', 30) + + this.guideOutputs = await this.api.guideOutputs() + + const status = await this.api.guidingStatus() + this.processGuiderStatus(status) + + const history = await this.api.guidingHistory() + this.phdGuideHistory.push(...history) + this.updateGuideHistoryChart() } @HostListener('window:unload') ngOnDestroy() { this.api.stopListening('GUIDING') - this.api.stopListening('CAMERA') - this.api.stopListening('MOUNT') } - async cameraChanged() { - if (this.camera) { - const camera = await this.api.camera(this.camera.name) - Object.assign(this.camera, camera) + private processGuiderStatus(event: GuiderStatus) { + this.phdConnected = event.connected + this.phdState = event.state + this.phdPixelScale = event.pixelScale + } - this.update() - } + plotModeChanged() { + this.updateGuideHistoryChart() + } - // this.electron.send('GUIDE_CAMERA_CHANGED', this.camera) + yAxisUnitChanged() { + this.updateGuideHistoryChart() } - connectCamera() { - if (this.cameraConnected) { - this.api.cameraDisconnect(this.camera!) + private updateGuideHistoryChart() { + if (this.phdGuideHistory.length > 0) { + const history = this.phdGuideHistory[this.phdGuideHistory.length - 1] + this.phdRmsTotal = history.rmsTotal + this.phdRmsDEC = history.rmsDEC + this.phdRmsRA = history.rmsRA } else { - this.api.cameraConnect(this.camera!) + return } - } - async mountChanged() { - if (this.mount) { - const mount = await this.api.mount(this.mount.name) - Object.assign(this.mount, mount) + const startId = this.phdGuideHistory[0].id + const guideSteps = this.phdGuideHistory.filter(e => e.guideStep) + const scale = this.yAxisUnit === 'ARCSEC' ? this.phdPixelScale : 1.0 - this.update() + let maxDuration = 0 + + for (const step of guideSteps) { + maxDuration = Math.max(maxDuration, Math.abs(step.guideStep!.raDuration)) + maxDuration = Math.max(maxDuration, Math.abs(step.guideStep!.decDuration)) } - // this.electron.send('GUIDE_MOUNT_CHANGED', this.mount) - } + this.phdDurationScale = maxDuration / 16.0 - connectMount() { - if (this.mountConnected) { - this.api.mountDisconnect(this.mount!) + if (this.plotMode === 'RA/DEC') { + this.phdChartData.datasets[0].data = guideSteps + .map(e => [e.id - startId, -e.guideStep!.raDistance * scale]) + this.phdChartData.datasets[1].data = guideSteps + .map(e => [e.id - startId, e.guideStep!.decDistance * scale]) } else { - this.api.mountConnect(this.mount!) + this.phdChartData.datasets[0].data = guideSteps + .map(e => [e.id - startId, -e.guideStep!.dx * scale]) + this.phdChartData.datasets[1].data = guideSteps + .map(e => [e.id - startId, e.guideStep!.dy * scale]) + } + + const durationScale = (direction?: GuideDirection) => { + return !direction || direction === 'NORTH' || direction === 'WEST' ? this.phdDurationScale : -this.phdDurationScale } + + this.phdChartData.datasets[2].data = this.phdGuideHistory + .map(e => (e.guideStep?.raDuration ?? 0) / durationScale(e.guideStep?.raDirection)) + this.phdChartData.datasets[3].data = this.phdGuideHistory + .map(e => (e.guideStep?.decDuration ?? 0) / durationScale(e.guideStep?.decDirection)) + + this.phdChart?.refresh() } async guideOutputChanged() { @@ -171,40 +389,65 @@ export class GuiderComponent implements AfterViewInit, OnDestroy { } } - async openCameraImage() { - await this.browserWindow.openCameraImage(this.camera!) + guidePulseStart(...directions: GuideDirection[]) { + for (const direction of directions) { + switch (direction) { + case 'NORTH': + this.api.guideOutputPulse(this.guideOutput!, direction, this.guideNorthDuration) + break + case 'SOUTH': + this.api.guideOutputPulse(this.guideOutput!, direction, this.guideSouthDuration) + break + case 'WEST': + this.api.guideOutputPulse(this.guideOutput!, direction, this.guideWestDuration) + break + case 'EAST': + this.api.guideOutputPulse(this.guideOutput!, direction, this.guideEastDuration) + break + } + } } - async startLooping() { - await this.openCameraImage() - - this.api.startGuideLooping(this.camera!, this.mount!, this.guideOutput!) + guidePulseStop() { + this.api.guideOutputPulse(this.guideOutput!, 'NORTH', 0) + this.api.guideOutputPulse(this.guideOutput!, 'SOUTH', 0) + this.api.guideOutputPulse(this.guideOutput!, 'WEST', 0) + this.api.guideOutputPulse(this.guideOutput!, 'EAST', 0) } - private update() { - if (this.camera) { - this.cameraConnected = this.camera.connected + guidingConnect() { + if (this.phdConnected) { + this.api.guidingDisconnect() + } else { + this.api.guidingConnect(this.phdHost, this.phdPort) } + } - if (this.mount) { - this.mountConnected = this.mount.connected - } + async guidingStart(event: MouseEvent) { + await this.api.guidingLoop(true) + await this.api.guidingStart(event.shiftKey) + } - if (this.guideOutput) { - this.guideOutputConnected = this.guideOutput.connected - } + async guidingSettleChanged() { + await this.api.guidingSettle(this.phdSettleAmount, this.phdSettleTime, this.phdSettleTimeout) + this.preference.set('guiding.settleAmount', this.phdSettleAmount) + this.preference.set('guiding.settleTime', this.phdSettleTime) + this.preference.set('guiding.settleTimeout', this.phdSettleTimeout) } - private updateGuideState() { - if (this.guider) { - this.looping = this.guider.looping - this.calibrating = this.guider.calibrating - this.guiding = this.guider.guiding - } + guidingClearHistory() { + this.phdGuideHistory.length = 0 + this.api.guidingClearHistory() + } + + guidingStop() { + this.api.guidingStop() } - private drawTrackingBox() { - const trackingBox = { camera: this.camera!, guider: this.guider! } - this.electron.send('DRAW_GUIDE_TRACKING_BOX', trackingBox) + private update() { + if (this.guideOutput) { + this.guideOutputConnected = this.guideOutput.connected + this.pulseGuiding = this.guideOutput.pulseGuiding + } } } \ No newline at end of file diff --git a/desktop/src/app/home/home.component.html b/desktop/src/app/home/home.component.html index ca7bb3055..a6c96703d 100644 --- a/desktop/src/app/home/home.component.html +++ b/desktop/src/app/home/home.component.html @@ -15,8 +15,8 @@ - - + + @@ -78,7 +78,8 @@ - + Alignment diff --git a/desktop/src/app/home/home.component.ts b/desktop/src/app/home/home.component.ts index 1ecb054a9..75f547a74 100644 --- a/desktop/src/app/home/home.component.ts +++ b/desktop/src/app/home/home.component.ts @@ -218,6 +218,9 @@ export class HomeComponent implements AfterContentInit, OnDestroy { case 'FRAMING': this.browserWindow.openFraming(undefined, { bringToFront: true }) break + case 'ALIGNMENT': + this.browserWindow.openAlignment({ bringToFront: true }) + break case 'INDI': this.browserWindow.openINDI(undefined, { bringToFront: true }) break diff --git a/desktop/src/app/image/image.component.html b/desktop/src/app/image/image.component.html index c61c850e1..d84511e59 100644 --- a/desktop/src/app/image/image.component.html +++ b/desktop/src/app/image/image.component.html @@ -1,5 +1,6 @@ - + @@ -18,15 +19,6 @@ - - - - - - X: {{ roiX }} Y: {{ roiY }} W: {{ roiWidth }} H: {{ roiHeight }} @@ -60,7 +52,7 @@ - + @@ -119,9 +111,9 @@ - - - + + + @@ -221,17 +213,17 @@ + [text]="true" sevirity="info" /> + [text]="true" sevirity="success" /> + [text]="true" sevirity="success" /> + [text]="true" /> - + @@ -266,8 +258,8 @@ - - + + @@ -305,7 +297,7 @@ - + diff --git a/desktop/src/app/image/image.component.scss b/desktop/src/app/image/image.component.scss index 6f1b206bf..e7e030efb 100644 --- a/desktop/src/app/image/image.component.scss +++ b/desktop/src/app/image/image.component.scss @@ -4,10 +4,6 @@ display: block; } -img { - image-rendering: pixelated; -} - .roi { width: 128px; height: 128px; diff --git a/desktop/src/app/image/image.component.ts b/desktop/src/app/image/image.component.ts index 663454650..b7876f4dd 100644 --- a/desktop/src/app/image/image.component.ts +++ b/desktop/src/app/image/image.component.ts @@ -12,9 +12,9 @@ import { ElectronService } from '../../shared/services/electron.service' import { PreferenceService } from '../../shared/services/preference.service' import { AstronomicalObject, - Camera, CameraCaptureEvent, DeepSkyObject, EquatorialCoordinateJ2000, FITSHeaderItem, GuideExposureFinished, GuideTrackingBox, + Camera, CameraCaptureEvent, DeepSkyObject, EquatorialCoordinateJ2000, FITSHeaderItem, ImageAnnotation, ImageCalibrated, ImageChannel, ImageInfo, ImageSource, - ImageStarSelected, PlateSolverType, SCNRProtectionMethod, SCNR_PROTECTION_METHODS, Star + PlateSolverType, SCNRProtectionMethod, SCNR_PROTECTION_METHODS, Star } from '../../shared/types' import { AppComponent } from '../app.component' @@ -102,9 +102,6 @@ export class ImageComponent implements AfterViewInit, OnDestroy { roiHeight = 128 roiInteractable?: Interactable - guiding = false - guideTrackingBox?: GuideTrackingBox - private readonly scnrMenuItem: MenuItem = { label: 'SCNR', icon: 'mdi mdi-palette', @@ -287,44 +284,22 @@ export class ImageComponent implements AfterViewInit, OnDestroy { ) { app.title = 'Image' - electron.on('CAMERA_EXPOSURE_FINISHED', async (_, data: CameraCaptureEvent) => { - if (data.camera.name === this.imageParams.camera?.name) { + electron.on('CAMERA_EXPOSURE_FINISHED', async (_, event: CameraCaptureEvent) => { + if (event.camera.name === this.imageParams.camera?.name) { await this.closeImage() ngZone.run(() => { - this.guiding = false this.annotations = [] - this.imageParams.path = data.savePath + this.imageParams.path = event.savePath this.loadImage() }) } }) - electron.on('GUIDE_EXPOSURE_FINISHED', async (_, data: GuideExposureFinished) => { - if (data.camera.name === this.imageParams.camera?.name) { - await this.closeImage() - - ngZone.run(() => { - this.guiding = true - this.annotations = [] - this.imageParams.path = data.path - this.loadImage() - }) - } - }) - - electron.on('PARAMS_CHANGED', async (_, data: ImageParams) => { + electron.on('PARAMS_CHANGED', async (_, event: ImageParams) => { await this.closeImage() - this.loadImageFromParams(data) - }) - - electron.on('DRAW_GUIDE_TRACKING_BOX', (_, data: GuideTrackingBox) => { - if (data.camera.name === this.imageParams.camera?.name) { - ngZone.run(() => { - this.guideTrackingBox = data - }) - } + this.loadImageFromParams(event) }) this.solverPathOrUrl = this.preference.get('image.solver.pathOrUrl', '') @@ -401,6 +376,8 @@ export class ImageComponent implements AfterViewInit, OnDestroy { } private loadImageFromParams(params: ImageParams) { + console.info('loading image from params: %s', params) + this.imageParams = params if (params.source === 'FRAMING') { @@ -462,9 +439,6 @@ export class ImageComponent implements AfterViewInit, OnDestroy { if (menu) { this.menu.show(event) - } else if (this.imageParams.camera) { - const event: ImageStarSelected = { camera: this.imageParams.camera, x: this.imageMouseX, y: this.imageMouseY } - this.electron.send('IMAGE_STAR_SELECTED', event) } } @@ -546,7 +520,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { async mountSlew(coordinate: EquatorialCoordinateJ2000) { const mount = await this.electron.selectedMount() if (!mount?.connected) return - this.api.mountSlewTo(mount, coordinate.rightAscensionJ2000, coordinate.declinationJ2000, true) + this.api.mountSlew(mount, coordinate.rightAscensionJ2000, coordinate.declinationJ2000, true) } frame(coordinate: EquatorialCoordinateJ2000) { diff --git a/desktop/src/app/indi/indi.component.html b/desktop/src/app/indi/indi.component.html index 4f0683cd9..8d27baf9c 100644 --- a/desktop/src/app/indi/indi.component.html +++ b/desktop/src/app/indi/indi.component.html @@ -9,8 +9,7 @@ - + diff --git a/desktop/src/app/indi/indi.component.ts b/desktop/src/app/indi/indi.component.ts index 5d33d3357..6ee8abe63 100644 --- a/desktop/src/app/indi/indi.component.ts +++ b/desktop/src/app/indi/indi.component.ts @@ -37,15 +37,15 @@ export class INDIComponent implements AfterViewInit, OnDestroy { this.api.startListening('INDI') - electron.on('DEVICE_PROPERTY_CHANGED', (_, data: INDIProperty) => { + electron.on('DEVICE_PROPERTY_CHANGED', (_, event: INDIProperty) => { ngZone.run(() => { - this.addOrUpdateProperty(data) + this.addOrUpdateProperty(event) this.updateGroups() }) }) - electron.on('DEVICE_PROPERTY_DELETED', (_, data: INDIProperty) => { - const index = this.properties.findIndex((e) => e.name === data.name) + electron.on('DEVICE_PROPERTY_DELETED', (_, event: INDIProperty) => { + const index = this.properties.findIndex((e) => e.name === event.name) if (index >= 0) { ngZone.run(() => { @@ -55,10 +55,10 @@ export class INDIComponent implements AfterViewInit, OnDestroy { } }) - electron.on('DEVICE_MESSAGE_RECEIVED', (_, data: INDIDeviceMessage) => { - if (this.device && data.device?.name === this.device.name) { + electron.on('DEVICE_MESSAGE_RECEIVED', (_, event: INDIDeviceMessage) => { + if (this.device && event.device?.name === this.device.name) { ngZone.run(() => { - this.messages.splice(0, 0, data.message) + this.messages.splice(0, 0, event.message) }) } }) diff --git a/desktop/src/app/indi/property/indi-property.component.html b/desktop/src/app/indi/property/indi-property.component.html index 3ac4d7218..d179d22e3 100644 --- a/desktop/src/app/indi/property/indi-property.component.html +++ b/desktop/src/app/indi/property/indi-property.component.html @@ -6,8 +6,7 @@ + (onClick)="sendSwitch(item)" icon="pi" [severity]="item.value ? 'success' : 'danger'"> @@ -17,8 +16,7 @@ + (onClick)="sendSwitch(item)" icon="pi" [severity]="item.value ? 'success' : 'danger'"> diff --git a/desktop/src/app/mount/mount.component.html b/desktop/src/app/mount/mount.component.html index 006b2543e..de4850af3 100644 --- a/desktop/src/app/mount/mount.component.html +++ b/desktop/src/app/mount/mount.component.html @@ -3,15 +3,15 @@ + styleClass="p-inputtext-sm border-0" emptyMessage="No mount found" /> Mount + size="small" severity="danger" pTooltip="Disconnect" tooltipPosition="bottom" /> + size="small" severity="info" pTooltip="Connect" tooltipPosition="bottom" /> @@ -98,15 +98,15 @@ - RA (h) - + DEC (°) @@ -140,8 +140,7 @@ icon="mdi mdi-arrow-left-thick" class="p-button-text"> - + + pTooltip="Park" tooltipPosition="bottom" severity="danger" [text]="true" /> + tooltipPosition="bottom" severity="success" [text]="true" /> + tooltipPosition="bottom" severity="info" [text]="true" /> + styleClass="p-inputtext-sm border-0" emptyMessage="No mode found" /> Tracking mode @@ -191,7 +190,7 @@ + styleClass="p-inputtext-sm border-0" emptyMessage="No rate found" /> Slew rate diff --git a/desktop/src/app/mount/mount.component.ts b/desktop/src/app/mount/mount.component.ts index 2ccd45cf5..1fec2b390 100644 --- a/desktop/src/app/mount/mount.component.ts +++ b/desktop/src/app/mount/mount.component.ts @@ -150,10 +150,10 @@ export class MountComponent implements AfterContentInit, OnDestroy { api.startListening('MOUNT') - electron.on('MOUNT_UPDATED', (_, mount: Mount) => { - if (mount.name === this.mount?.name) { + electron.on('MOUNT_UPDATED', (_, event: Mount) => { + if (event.name === this.mount?.name) { ngZone.run(() => { - Object.assign(this.mount!, mount) + Object.assign(this.mount!, event) this.update() }) } @@ -217,7 +217,7 @@ export class MountComponent implements AfterContentInit, OnDestroy { } async slewTo() { - await this.api.mountSlewTo(this.mount!, this.targetRightAscension, this.targetDeclination, this.targetCoordinateType === 'J2000') + await this.api.mountSlew(this.mount!, this.targetRightAscension, this.targetDeclination, this.targetCoordinateType === 'J2000') this.savePreference() } @@ -241,16 +241,16 @@ export class MountComponent implements AfterContentInit, OnDestroy { if (this.moveToDirection[0] !== pressed) { switch (direction[0]) { case 'N': - this.api.mountMove(this.mount!, 'UP_NORTH', pressed) + this.api.mountMove(this.mount!, 'NORTH', pressed) break case 'S': - this.api.mountMove(this.mount!, 'DOWN_SOUTH', pressed) + this.api.mountMove(this.mount!, 'SOUTH', pressed) break case 'W': - this.api.mountMove(this.mount!, 'LEFT_WEST', pressed) + this.api.mountMove(this.mount!, 'WEST', pressed) break case 'E': - this.api.mountMove(this.mount!, 'RIGHT_EAST', pressed) + this.api.mountMove(this.mount!, 'EAST', pressed) break } @@ -260,10 +260,10 @@ export class MountComponent implements AfterContentInit, OnDestroy { if (this.moveToDirection[1] !== pressed) { switch (direction[1]) { case 'W': - this.api.mountMove(this.mount!, 'LEFT_WEST', pressed) + this.api.mountMove(this.mount!, 'WEST', pressed) break case 'E': - this.api.mountMove(this.mount!, 'RIGHT_EAST', pressed) + this.api.mountMove(this.mount!, 'EAST', pressed) break default: return @@ -274,6 +274,10 @@ export class MountComponent implements AfterContentInit, OnDestroy { } } + abort() { + this.api.mountAbort(this.mount!) + } + trackingToggled() { if (this.connected) { this.api.mountTracking(this.mount!, this.tracking) diff --git a/desktop/src/index.html b/desktop/src/index.html index 02a473d30..fae0e97f3 100644 --- a/desktop/src/index.html +++ b/desktop/src/index.html @@ -9,11 +9,11 @@ - + - +