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 @@ +
+
+
+
+
+ + + + +
+
+ + +
+
+
+
+
+
+ + + + +
+
+ + +
+
+
+
+ + {{ darvStatus }} + + + {{ darvDirection }} + + + {{ darvCaptureEvent.captureRemainingTime | exposureTime }} + + + {{ darvCaptureEvent.captureProgress | percent:'1.1-1' }} + + +
+
+
+
+ + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+
+
+ + + +
+
+
+
+
+
+
\ 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" />
- - + + + 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" />
+ 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" /> + severity="sucsess" pTooltip="Apply" tooltipPosition="bottom" />
@@ -99,7 +100,7 @@
@@ -110,7 +111,8 @@
- @@ -119,13 +121,15 @@
Exposure Mode -
@@ -133,7 +137,8 @@
@@ -143,7 +148,7 @@
@@ -151,7 +156,7 @@
@@ -159,7 +164,7 @@
@@ -167,7 +172,7 @@
@@ -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()" />
+ [step]="1.0" [min]="1" [max]="4" styleClass="p-inputtext-sm border-0" [allowEmpty]="false" + (ngModelChange)="savePreference()" />
@@ -200,7 +207,8 @@
- @@ -208,7 +216,7 @@
@@ -216,7 +224,7 @@
@@ -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" />
+ size="small" severity="danger" /> + size="small" severity="info" />
@@ -20,15 +20,16 @@
-
+
- +
-
+
-
@@ -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" />
+ size="small" severity="danger" pTooltip="Disconnect" tooltipPosition="bottom" /> + size="small" severity="info" pTooltip="Connect" tooltipPosition="bottom" />
@@ -21,42 +21,42 @@
- +
- +
- +
+ [text]="true" size="small" pTooltip="Move In" tooltipPosition="bottom" /> - + [text]="true" size="small" pTooltip="Move Out" tooltipPosition="bottom" />
- + + [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 @@
- +
- +
-
-
-
- @@ -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 @@
-
- - - - - - - -
-
- - - - - - - -
-
- - - - - - - -
- - - - - + +
+
+ + + + +
+
+ + + + +
+
+ + +
+
+
+
+ + {{ phdState | enum | lowercase }} + {{ phdMessage }} +
+
+ + +
+
+ + + + +
+
+ Dither in RA only + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+
+ +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+
+
+ +
+ North + East +
+
+
+
+ + + +
+
+
- - - - + +
+ + + + + + +
+
+
+
+ + + + +
+
+
+ + + + +
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+ + + + +
+
+
+ + + + +
+
+
-
-
- - - -
-
- - -
-
\ 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" />
+ size="small" severity="danger" pTooltip="Disconnect" tooltipPosition="bottom" /> + size="small" severity="info" pTooltip="Connect" tooltipPosition="bottom" />
@@ -98,15 +98,15 @@
-
- +
@@ -140,8 +140,7 @@ icon="mdi mdi-arrow-left-thick" class="p-button-text">
- +
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 @@ - + - + \ No newline at end of file diff --git a/desktop/src/shared/constants.ts b/desktop/src/shared/constants.ts new file mode 100644 index 000000000..152499212 --- /dev/null +++ b/desktop/src/shared/constants.ts @@ -0,0 +1 @@ +export const EVERY_MINUTE_CRON_TIME = '0 */1 * * * *' diff --git a/desktop/src/shared/dialogs/location/location.dialog.html b/desktop/src/shared/dialogs/location/location.dialog.html index f6eed1fa8..f09e5689b 100644 --- a/desktop/src/shared/dialogs/location/location.dialog.html +++ b/desktop/src/shared/dialogs/location/location.dialog.html @@ -38,5 +38,5 @@
\ No newline at end of file diff --git a/desktop/src/shared/pipes/enum.pipe.ts b/desktop/src/shared/pipes/enum.pipe.ts index 78c83d7b4..01f15e32a 100644 --- a/desktop/src/shared/pipes/enum.pipe.ts +++ b/desktop/src/shared/pipes/enum.pipe.ts @@ -307,6 +307,13 @@ export class EnumPipe implements PipeTransform { 'RADAR': 'Radar Calibration', 'CUBESAT': 'CubeSats', 'OTHER': 'Other', + 'STOPPED': 'Stopped', + 'SELECTED': 'Selected', + 'CALIBRATING': 'Calibrating', + 'GUIDING': 'Guiding', + 'LOST_LOCK': 'Lost Lock', + 'PAUSED': 'Paused', + 'LOOPING': 'Looping', } transform(value: string) { diff --git a/desktop/src/shared/services/api.service.ts b/desktop/src/shared/services/api.service.ts index 949d0b726..7772811ad 100644 --- a/desktop/src/shared/services/api.service.ts +++ b/desktop/src/shared/services/api.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@angular/core' import moment from 'moment' import { Angle, BodyPosition, Camera, CameraStartCapture, ComputedLocation, Constellation, DeepSkyObject, Device, - FilterWheel, Focuser, GuideDirection, GuideOutput, GuidingChart, GuidingStar, HipsSurvey, + FilterWheel, Focuser, GuideDirection, GuideOutput, GuiderStatus, HipsSurvey, HistoryStep, INDIProperty, INDISendProperty, ImageAnnotation, ImageCalibrated, ImageChannel, ImageInfo, ListeningEventType, Location, MinorPlanet, Mount, PlateSolverType, SCNRProtectionMethod, Satellite, SatelliteGroupType, @@ -64,8 +64,8 @@ export class ApiService { return this.http.put(`cameras/${camera.name}/temperature/setpoint?temperature=${temperature}`) } - cameraStartCapture(camera: Camera, value: CameraStartCapture) { - return this.http.put(`cameras/${camera.name}/capture/start`, value) + cameraStartCapture(camera: Camera, data: CameraStartCapture) { + return this.http.put(`cameras/${camera.name}/capture/start`, data) } cameraAbortCapture(camera: Camera) { @@ -99,9 +99,9 @@ export class ApiService { return this.http.put(`mounts/${mount.name}/sync?${query}`) } - mountSlewTo(mount: Mount, rightAscension: Angle, declination: Angle, j2000: boolean) { + mountSlew(mount: Mount, rightAscension: Angle, declination: Angle, j2000: boolean) { const query = this.http.query({ rightAscension, declination, j2000 }) - return this.http.put(`mounts/${mount.name}/slew-to?${query}`) + return this.http.put(`mounts/${mount.name}/slew?${query}`) } mountGoTo(mount: Mount, rightAscension: Angle, declination: Angle, j2000: boolean) { @@ -162,7 +162,7 @@ export class ApiService { pointMountHere(mount: Mount, path: string, x: number, y: number, synchronized: boolean = true) { const query = this.http.query({ path, x, y, synchronized }) - return this.http.post(`mounts/${mount.name}/point-here${query}`) + return this.http.post(`mounts/${mount.name}/point-here?${query}`) } // FOCUSER @@ -229,52 +229,78 @@ export class ApiService { return this.http.put(`wheels/${wheel.name}/sync?names=${names.join(',')}`) } - attachedGuideOutputs() { - return this.http.get(`attachedGuideOutputs`) + // GUIDE OUTPUT + + guideOutputs() { + return this.http.get(`guide-outputs`) } guideOutput(name: string) { - return this.http.get(`guideOutput?name=${name}`) + return this.http.get(`guide-outputs/${name}`) } guideOutputConnect(guideOutput: GuideOutput) { - return this.http.post(`guideOutputConnect?name=${guideOutput.name}`) + return this.http.put(`guide-outputs/${guideOutput.name}/connect`) } guideOutputDisconnect(guideOutput: GuideOutput) { - return this.http.post(`guideOutputDisconnect?name=${guideOutput.name}`) + return this.http.put(`guide-outputs/${guideOutput.name}/disconnect`) + } + + guideOutputPulse(guideOutput: GuideOutput, direction: GuideDirection, duration: number) { + const query = this.http.query({ direction, duration }) + return this.http.put(`guide-outputs/${guideOutput.name}/pulse?${query}`) + } + + // GUIDING + + guidingConnect(host: string = 'localhost', port: number = 4400) { + const query = this.http.query({ host, port }) + return this.http.put(`guiding/connect?${query}`) + } + + guidingDisconnect() { + return this.http.delete(`guiding/disconnect`) + } + + guidingStatus() { + return this.http.get(`guiding/status`) } - startGuideLooping(camera: Camera, mount: Mount, guideOutput: GuideOutput) { - return this.http.post(`startGuideLooping?camera=${camera.name}&mount=${mount.name}&guideOutput=${guideOutput.name}`) + guidingHistory() { + return this.http.get(`guiding/history`) } - stopGuideLooping() { - return this.http.post(`stopGuideLooping`) + guidingLatestHistory() { + return this.http.get(`guiding/history/latest`) } - startGuiding(forceCalibration: boolean = false) { - return this.http.post(`startGuiding?forceCalibration=${forceCalibration}`) + guidingClearHistory() { + return this.http.put(`guiding/history/clear`) } - stopGuiding() { - return this.http.post(`stopGuiding`) + guidingLoop(autoSelectGuideStar: boolean = true) { + const query = this.http.query({ autoSelectGuideStar }) + return this.http.put(`guiding/loop?${query}`) } - guidingChart() { - return this.http.get(`guidingChart`) + guidingStart(forceCalibration: boolean = false) { + const query = this.http.query({ forceCalibration }) + return this.http.put(`guiding/start?${query}`) } - guidingStar() { - return this.http.get(`guidingStar`) + guidingDither(amount: number, raOnly: boolean = false) { + const query = this.http.query({ amount, raOnly }) + return this.http.put(`guiding/dither?${query}`) } - selectGuideStar(x: number, y: number) { - return this.http.post(`selectGuideStar?x=${x}&y=${y}`) + guidingSettle(amount: number, time: number, timeout: number) { + const query = this.http.query({ amount, time, timeout }) + return this.http.put(`guiding/settle?${query}`) } - deselectGuideStar() { - return this.http.post(`deselectGuideStar`) + guidingStop() { + return this.http.put(`guiding/stop`) } // IMAGE @@ -463,7 +489,7 @@ export class ApiService { solveImage( path: string, type: PlateSolverType, blind: boolean, - centerRA: string | number, centerDEC: string | number, radius: string | number, + centerRA: Angle, centerDEC: Angle, radius: Angle, downsampleFactor: number, pathOrUrl: string, apiKey: string, ) { @@ -485,4 +511,16 @@ export class ApiService { const query = this.http.query({ rightAscension, declination, width, height, fov, rotation, hipsSurvey: hipsSurvey.type }) return this.http.put(`framing?${query}`) } + + // DARV + + darvStart(camera: Camera, guideOutput: GuideOutput, + exposureInSeconds: number, initialPauseInSeconds: number, direction: GuideDirection, reversed: boolean = false) { + const data = { exposureInSeconds, initialPauseInSeconds, direction, reversed } + return this.http.put(`polar-alignment/darv/${camera.name}/${guideOutput.name}/start`, data) + } + + darvStop(camera: Camera, guideOutput: GuideOutput) { + return this.http.put(`polar-alignment/darv/${camera.name}/${guideOutput.name}/stop`) + } } diff --git a/desktop/src/shared/services/browser-window.service.ts b/desktop/src/shared/services/browser-window.service.ts index b9ed3d57c..8bd407bee 100644 --- a/desktop/src/shared/services/browser-window.service.ts +++ b/desktop/src/shared/services/browser-window.service.ts @@ -19,7 +19,7 @@ export class BrowserWindowService { this.openWindow({ ...options, id: 'mount', path: 'mount', icon: options.icon || 'telescope', - width: options.width || 400, height: options.height || 477, + width: options.width || 400, height: options.height || 469, }) } @@ -27,7 +27,7 @@ export class BrowserWindowService { this.openWindow({ ...options, id: 'camera', path: 'camera', icon: options.icon || 'camera', - width: options.width || 400, height: options.height || 502, + width: options.width || 400, height: options.height || 478, }) } @@ -35,7 +35,7 @@ export class BrowserWindowService { this.openWindow({ ...options, id: 'focuser', path: 'focuser', icon: options.icon || 'focus', - width: options.width || 360, height: options.height || 218, + width: options.width || 360, height: options.height || 203, }) } @@ -43,7 +43,7 @@ export class BrowserWindowService { this.openWindow({ ...options, id: 'wheel', path: 'wheel', icon: options.icon || 'filter-wheel', - width: options.width || 320, height: options.height || 171, + width: options.width || 280, height: options.height || 201, }) } @@ -51,7 +51,7 @@ export class BrowserWindowService { this.openWindow({ ...options, id: 'guider', path: 'guider', icon: options.icon || 'guider', - width: options.width || 720, height: options.height || 360, + width: options.width || 425, height: options.height || 440, }) } @@ -93,7 +93,15 @@ export class BrowserWindowService { this.openWindow({ ...options, id: 'framing', path: 'framing', icon: options.icon || 'framing', - width: options.width || 280, height: options.height || 321, params, + width: options.width || 280, height: options.height || 310, params, + }) + } + + openAlignment(options: OpenWindowOptions = {}) { + this.openWindow({ + ...options, + id: 'alignment', path: 'alignment', icon: options.icon || 'star', + width: options.width || 470, height: options.height || 280, }) } diff --git a/desktop/src/shared/services/electron.service.ts b/desktop/src/shared/services/electron.service.ts index 5632df893..6ba18510d 100644 --- a/desktop/src/shared/services/electron.service.ts +++ b/desktop/src/shared/services/electron.service.ts @@ -67,4 +67,12 @@ export class ElectronService { if (!mount) return undefined return this.api.mount(mount.name) } + + registerCron(cronTime: string) { + return this.sendSync('REGISTER_CRON', cronTime) + } + + unregisterCron() { + return this.sendSync('UNREGISTER_CRON') + } } diff --git a/desktop/src/shared/types.ts b/desktop/src/shared/types.ts index 2a05a2498..4b270ac96 100644 --- a/desktop/src/shared/types.ts +++ b/desktop/src/shared/types.ts @@ -1,4 +1,3 @@ -import { Point } from 'electron' export type Angle = string | number @@ -17,6 +16,53 @@ export interface GuideOutput extends Device { pulseGuiding: boolean } +export interface GuiderStatus { + connected: boolean + state: GuideState + settling: boolean + pixelScale: number +} + +export interface GuidePoint { + x: number + y: number +} + +export interface GuideStep { + frame: number + starMass: number + snr: number + hfd: number + dx: number + dy: number + raDistance: number + decDistance: number + raDistanceGuide: number + decDistanceGuide: number + raDuration: number + raDirection: GuideDirection + decDuration: number + decDirection: GuideDirection + averageDistance: number +} + +export interface HistoryStep { + id: number + rmsRA: number + rmsDEC: number + rmsTotal: number + guideStep?: GuideStep + ditherX: number + ditherY: number +} + +export interface GuideStar { + lockPosition: GuidePoint + starPosition: GuidePoint + image: string + guideStep: GuideStep +} + export interface Camera extends GuideOutput, Thermometer { exposuring: boolean hasCoolerControl: boolean @@ -390,72 +436,27 @@ export interface ComputedLocation extends EquatorialCoordinate, EquatorialCoordi lst: string } -export interface ImageStarSelected { - camera: Camera - x: number - y: number -} - -export interface GuideStats { - timestamp: number - dx: number - dy: number - ra: number - dec: number - // starSNR: number - // starMass: number - raDuration: number - decDuration: number - raDirection?: GuideDirection - decDirection?: GuideDirection - rmsRA: number - rmsDEC: number - peakRA: number - peakDEC: number -} - -export interface GuidingChart { - chart: GuideStats[] - rmsRA: number - rmsDEC: number - rmsTotal: number -} - -export interface GuidingStar { - image: string - lockPositionX: number - lockPositionY: number - primaryStarX: number - primaryStarY: number - peak: number - fwhm: number - hfd: number - snr: number -} - -export interface GuideStar extends Point { - valid: boolean +export interface Satellite { + id: number + name: string + tle: string + groups: SatelliteGroupType[] } -export interface Guider { - lockPosition: GuideStar - primaryStar: GuideStar - searchRegion: number - looping: boolean - calibrating: boolean - guiding: boolean +export interface DARVPolarAlignmentEvent { + camera: Camera + guideOutput: GuideOutput + remainingTime: number + progress: number } -export interface GuideTrackingBox { - camera: Camera - guider: Guider +export interface DARVPolarAlignmentInitialPauseElapsed extends DARVPolarAlignmentEvent { + pauseTime: number } -export interface Satellite { - id: number - name: string - tle: string - groups: SatelliteGroupType[] +export interface DARVPolarAlignmentGuidePulseElapsed extends DARVPolarAlignmentEvent { + forward: boolean + direction: GuideDirection } export enum ExposureTimeUnit { @@ -643,29 +644,36 @@ export const INDI_EVENT_TYPES = [ 'CAMERA_UPDATED', 'CAMERA_ATTACHED', 'CAMERA_DETACHED', 'CAMERA_CAPTURE_STARTED', 'CAMERA_CAPTURE_FINISHED', 'CAMERA_EXPOSURE_UPDATED', 'CAMERA_EXPOSURE_STARTED', 'CAMERA_EXPOSURE_FINISHED', + // Mount. 'MOUNT_UPDATED', 'MOUNT_ATTACHED', 'MOUNT_DETACHED', + // Focuser. 'FOCUSER_UPDATED', 'FOCUSER_ATTACHED', 'FOCUSER_DETACHED', + // Filter Wheel. 'WHEEL_UPDATED', 'WHEEL_ATTACHED', 'WHEEL_DETACHED', + // Guide Output. 'GUIDE_OUTPUT_ATTACHED', 'GUIDE_OUTPUT_DETACHED', 'GUIDE_OUTPUT_UPDATED', - 'GUIDE_EXPOSURE_FINISHED', 'GUIDE_LOCK_POSITION_CHANGED', - 'GUIDE_STAR_LOST', 'GUIDE_LOCK_POSITION_LOST', + // Guider. + 'GUIDER_CONNECTED', 'GUIDER_DISCONNECTED', 'GUIDER_UPDATED', 'GUIDER_STEPPED', + 'GUIDER_MESSAGE_RECEIVED', + // Polar Alignment. + 'DARV_POLAR_ALIGNMENT_STARTED', 'DARV_POLAR_ALIGNMENT_FINISHED', + 'DARV_POLAR_ALIGNMENT_INITIAL_PAUSE_ELAPSED', 'DARV_POLAR_ALIGNMENT_GUIDE_PULSE_ELAPSED', ] as const export type INDIEventType = (typeof INDI_EVENT_TYPES)[number] export const WINDOW_EVENT_TYPES = [ 'SAVE_FITS_AS', 'OPEN_FITS', 'OPEN_WINDOW', 'OPEN_DIRECTORY', 'CLOSE_WINDOW', - 'PIN_WINDOW', 'UNPIN_WINDOW', 'MINIMIZE_WINDOW', 'MAXIMIZE_WINDOW' + 'PIN_WINDOW', 'UNPIN_WINDOW', 'MINIMIZE_WINDOW', 'MAXIMIZE_WINDOW', + 'REGISTER_CRON', 'UNREGISTER_CRON', ] as const export type WindowEventType = (typeof WINDOW_EVENT_TYPES)[number] export const INTERNAL_EVENT_TYPES = [ - 'SELECTED_CAMERA', 'SELECTED_FOCUSER', 'SELECTED_WHEEL', - 'SELECTED_MOUNT', + 'SELECTED_CAMERA', 'SELECTED_FOCUSER', 'SELECTED_WHEEL', 'SELECTED_MOUNT', 'CAMERA_CHANGED', 'FOCUSER_CHANGED', 'MOUNT_CHANGED', 'WHEEL_CHANGED', - 'WHEEL_RENAMED', 'IMAGE_STAR_SELECTED', 'GUIDE_OUTPUT_CHANGED', - 'DRAW_GUIDE_TRACKING_BOX', + 'WHEEL_RENAMED', 'GUIDE_OUTPUT_CHANGED', 'CRON_TICKED', ] as const export type InternalEventType = (typeof INTERNAL_EVENT_TYPES)[number] @@ -709,12 +717,21 @@ export type TargetCoordinateType = 'J2000' | 'JNOW' export type TrackMode = 'SIDEREAL' | ' LUNAR' | 'SOLAR' | 'KING' | 'CUSTOM' -export type GuideDirection = 'UP_NORTH' | // DEC+ - 'DOWN_SOUTH' | // DEC- - 'LEFT_WEST' | // RA+ - 'RIGHT_EAST' // RA- +export type GuideDirection = 'NORTH' | // DEC+ + 'SOUTH' | // DEC- + 'WEST' | // RA+ + 'EAST' // RA- -export const SATELLITE_GROUP_TYPES = [ +export function reverseGuideDirection(direction: GuideDirection): GuideDirection { + switch (direction) { + case 'NORTH': return 'SOUTH' + case 'SOUTH': return 'NORTH' + case 'WEST': return 'EAST' + case 'EAST': return 'WEST' + } +} + +export const SATELLITE_GROUPS = [ 'LAST_30_DAYS', 'STATIONS', 'VISUAL', 'ACTIVE', 'ANALYST', 'COSMOS_1408_DEBRIS', 'FENGYUN_1C_DEBRIS', 'IRIDIUM_33_DEBRIS', @@ -733,6 +750,20 @@ export const SATELLITE_GROUP_TYPES = [ 'RADAR', 'CUBESAT', 'OTHER', ] as const -export type SatelliteGroupType = (typeof SATELLITE_GROUP_TYPES)[number] +export type SatelliteGroupType = (typeof SATELLITE_GROUPS)[number] export type ListeningEventType = 'INDI' | 'GUIDING' | 'CAMERA' | 'MOUNT' + +export const GUIDER_TYPES = ['PHD2'] as const + +export type GuiderType = (typeof GUIDER_TYPES)[number] + +export const GUIDE_STATES = [ + 'STOPPED', 'SELECTED', 'CALIBRATING', + 'GUIDING', 'LOST_LOCK', 'PAUSED', + 'LOOPING', +] as const + +export type GuideState = (typeof GUIDE_STATES)[number] + +export type Hemisphere = 'NORTHERN' | 'SOUTHERN' diff --git a/desktop/src/styles.scss b/desktop/src/styles.scss index 4eacf4a04..224432313 100644 --- a/desktop/src/styles.scss +++ b/desktop/src/styles.scss @@ -136,10 +136,18 @@ i.mdi { } } +.p-tabview .p-tabview-nav { + border: 0; +} + .draggable-region { -webkit-app-region: drag; } +.pixelated { + image-rendering: pixelated; +} + ::-webkit-scrollbar { width: 6px; } diff --git a/nebulosa-alignment/src/main/kotlin/nebulosa/alignment/StarAlignment.kt b/nebulosa-alignment/src/main/kotlin/nebulosa/alignment/StarAlignment.kt index d27a01c29..095664c82 100644 --- a/nebulosa-alignment/src/main/kotlin/nebulosa/alignment/StarAlignment.kt +++ b/nebulosa-alignment/src/main/kotlin/nebulosa/alignment/StarAlignment.kt @@ -1,4 +1,3 @@ package nebulosa.alignment -interface StarAlignment { -} +interface StarAlignment diff --git a/nebulosa-alignment/src/main/kotlin/nebulosa/alignment/algorithms/ThreePointPolarAlignment.kt b/nebulosa-alignment/src/main/kotlin/nebulosa/alignment/algorithms/ThreePointPolarAlignment.kt index 4404f8520..48f433b55 100644 --- a/nebulosa-alignment/src/main/kotlin/nebulosa/alignment/algorithms/ThreePointPolarAlignment.kt +++ b/nebulosa-alignment/src/main/kotlin/nebulosa/alignment/algorithms/ThreePointPolarAlignment.kt @@ -2,5 +2,4 @@ package nebulosa.alignment.algorithms import nebulosa.alignment.StarAlignment -class ThreePointPolarAlignment : StarAlignment { -} +class ThreePointPolarAlignment : StarAlignment diff --git a/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/CountUpDownLatch.kt b/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/CountUpDownLatch.kt index 66aeeed83..884b96376 100644 --- a/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/CountUpDownLatch.kt +++ b/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/CountUpDownLatch.kt @@ -1,5 +1,6 @@ package nebulosa.common.concurrency +import java.time.Duration import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.locks.AbstractQueuedSynchronizer @@ -48,6 +49,10 @@ class CountUpDownLatch(initialCount: Int = 0) : AtomicBoolean(true) { return n >= 0 && sync.tryAcquireSharedNanos(n, unit.toNanos(timeout)) } + fun await(timeout: Duration, n: Int = 0): Boolean { + return n >= 0 && sync.tryAcquireSharedNanos(n, timeout.toNanos()) + } + private class Sync(private val latch: AtomicBoolean) : AbstractQueuedSynchronizer() { var count diff --git a/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/AxisStats.kt b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/AxisStats.kt index e76926d0e..516b64d9a 100644 --- a/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/AxisStats.kt +++ b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/AxisStats.kt @@ -169,8 +169,8 @@ internal open class AxisStats { var currentVariance = 0.0 // Apply the linear fit to the data points and compute their resultant sigma. - for (entry in guidingEntries) { - val newValue = entry.starPos - (entry.deltaTime * slope + intercept) + for ((deltaTime, starPos) in guidingEntries) { + val newValue = starPos - (deltaTime * slope + intercept) if (currentMean.isNaN()) { currentMean = newValue diff --git a/nebulosa-guiding/src/main/kotlin/nebulosa/guiding/CalibrationState.kt b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/CalibrationState.kt similarity index 81% rename from nebulosa-guiding/src/main/kotlin/nebulosa/guiding/CalibrationState.kt rename to nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/CalibrationState.kt index 2ed9c95c1..bcab3ea9d 100644 --- a/nebulosa-guiding/src/main/kotlin/nebulosa/guiding/CalibrationState.kt +++ b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/CalibrationState.kt @@ -1,4 +1,4 @@ -package nebulosa.guiding +package nebulosa.guiding.internal enum class CalibrationState { CLEARED, diff --git a/nebulosa-guiding/src/main/kotlin/nebulosa/guiding/DeclinationGuideMode.kt b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/DeclinationGuideMode.kt similarity index 69% rename from nebulosa-guiding/src/main/kotlin/nebulosa/guiding/DeclinationGuideMode.kt rename to nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/DeclinationGuideMode.kt index 010181335..e4d494fc4 100644 --- a/nebulosa-guiding/src/main/kotlin/nebulosa/guiding/DeclinationGuideMode.kt +++ b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/DeclinationGuideMode.kt @@ -1,4 +1,4 @@ -package nebulosa.guiding +package nebulosa.guiding.internal enum class DeclinationGuideMode { NONE, diff --git a/nebulosa-guiding/src/main/kotlin/nebulosa/guiding/Dither.kt b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/Dither.kt similarity index 75% rename from nebulosa-guiding/src/main/kotlin/nebulosa/guiding/Dither.kt rename to nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/Dither.kt index 00a9b7b7c..aea9711a0 100644 --- a/nebulosa-guiding/src/main/kotlin/nebulosa/guiding/Dither.kt +++ b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/Dither.kt @@ -1,4 +1,4 @@ -package nebulosa.guiding +package nebulosa.guiding.internal interface Dither { diff --git a/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/FWHM.kt b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/FWHM.kt index 650a1f225..a1db07266 100644 --- a/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/FWHM.kt +++ b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/FWHM.kt @@ -1,6 +1,5 @@ package nebulosa.guiding.internal -import nebulosa.guiding.StarPoint import nebulosa.imaging.Image import nebulosa.imaging.algorithms.ComputationAlgorithm import nebulosa.imaging.algorithms.Draw diff --git a/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/GuideAlgorithm.kt b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/GuideAlgorithm.kt index 56a612ecc..bab0dfcac 100644 --- a/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/GuideAlgorithm.kt +++ b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/GuideAlgorithm.kt @@ -1,7 +1,5 @@ package nebulosa.guiding.internal -import nebulosa.guiding.GuideAxis - interface GuideAlgorithm { val axis: GuideAxis diff --git a/nebulosa-guiding/src/main/kotlin/nebulosa/guiding/GuideAxis.kt b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/GuideAxis.kt similarity index 58% rename from nebulosa-guiding/src/main/kotlin/nebulosa/guiding/GuideAxis.kt rename to nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/GuideAxis.kt index 5b1b74f76..f60b8ee51 100644 --- a/nebulosa-guiding/src/main/kotlin/nebulosa/guiding/GuideAxis.kt +++ b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/GuideAxis.kt @@ -1,4 +1,4 @@ -package nebulosa.guiding +package nebulosa.guiding.internal enum class GuideAxis { RA_X, diff --git a/nebulosa-guiding/src/main/kotlin/nebulosa/guiding/GuideCalibration.kt b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/GuideCalibration.kt similarity index 93% rename from nebulosa-guiding/src/main/kotlin/nebulosa/guiding/GuideCalibration.kt rename to nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/GuideCalibration.kt index 4a5c2d6be..604a9602c 100644 --- a/nebulosa-guiding/src/main/kotlin/nebulosa/guiding/GuideCalibration.kt +++ b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/GuideCalibration.kt @@ -1,4 +1,4 @@ -package nebulosa.guiding +package nebulosa.guiding.internal import nebulosa.math.Angle diff --git a/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/GuideCalibrator.kt b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/GuideCalibrator.kt index ebfce2205..c856d7e58 100644 --- a/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/GuideCalibrator.kt +++ b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/GuideCalibrator.kt @@ -2,7 +2,7 @@ package nebulosa.guiding.internal import nebulosa.constants.PI import nebulosa.constants.PIOVERTWO -import nebulosa.guiding.* +import nebulosa.guiding.GuideDirection import nebulosa.log.loggerFor import nebulosa.math.* import kotlin.math.* @@ -191,9 +191,9 @@ internal class GuideCalibrator(private val guider: MultiStarGuider) { LOG.info("West step {}, dist={}", calibrationSteps, dist) - calibrationStatus(GuideDirection.LEFT_WEST) + calibrationStatus(GuideDirection.WEST) - guideDirection(GuideDirection.LEFT_WEST, guider.calibrationStep) + guideDirection(GuideDirection.WEST, guider.calibrationStep) break } @@ -249,13 +249,13 @@ internal class GuideCalibrator(private val guider: MultiStarGuider) { LOG.info("East step {}, dist={}", calibrationSteps, dist) - calibrationStatus(GuideDirection.RIGHT_EAST) + calibrationStatus(GuideDirection.EAST) recenterRemaining -= duration calibrationSteps-- lastLocation.set(currentLocation) - guideDirection(GuideDirection.RIGHT_EAST, duration) + guideDirection(GuideDirection.EAST, duration) break } @@ -310,9 +310,9 @@ internal class GuideCalibrator(private val guider: MultiStarGuider) { if (calibrationSteps == 0) { // Get things moving with the first clearing pulse. LOG.info("starting north clearing using pulse width of {}", guider.calibrationStep) - guideDirection(GuideDirection.UP_NORTH, guider.calibrationStep) + guideDirection(GuideDirection.NORTH, guider.calibrationStep) calibrationSteps = 1 - calibrationStatus(GuideDirection.UP_NORTH) + calibrationStatus(GuideDirection.NORTH) break } @@ -337,7 +337,7 @@ internal class GuideCalibrator(private val guider: MultiStarGuider) { if (blAcceptedMoves < BL_MIN_COUNT) { if (calibrationSteps < blMaxClearingPulses && blCumDelta < distCrit) { // Still have attempts left, haven't moved the star by 25 px yet. - guideDirection(GuideDirection.UP_NORTH, guider.calibrationStep) + guideDirection(GuideDirection.NORTH, guider.calibrationStep) calibrationSteps++ @@ -345,7 +345,7 @@ internal class GuideCalibrator(private val guider: MultiStarGuider) { calibrationStartingCoords.set(guider.mount.rightAscension.toHours, guider.mount.declination.toDegrees) blLastCumDistance = blCumDelta - calibrationStatus(GuideDirection.UP_NORTH) + calibrationStatus(GuideDirection.NORTH) LOG.info("last delta = {} px, cum distance = {}", blDelta, blCumDelta) @@ -399,9 +399,9 @@ internal class GuideCalibrator(private val guider: MultiStarGuider) { LOG.info("North step {}, dist={}", calibrationSteps, dist) - calibrationStatus(GuideDirection.UP_NORTH) + calibrationStatus(GuideDirection.NORTH) - guideDirection(GuideDirection.UP_NORTH, guider.calibrationStep) + guideDirection(GuideDirection.NORTH, guider.calibrationStep) break } @@ -475,12 +475,12 @@ internal class GuideCalibrator(private val guider: MultiStarGuider) { LOG.info("South step {}, dist={}", calibrationSteps, dist) - calibrationStatus(GuideDirection.DOWN_SOUTH) + calibrationStatus(GuideDirection.SOUTH) recenterRemaining -= duration calibrationSteps-- - guideDirection(GuideDirection.DOWN_SOUTH, duration) + guideDirection(GuideDirection.SOUTH, duration) break } @@ -535,9 +535,9 @@ internal class GuideCalibrator(private val guider: MultiStarGuider) { calibrationSteps++ - calibrationStatus(GuideDirection.DOWN_SOUTH) + calibrationStatus(GuideDirection.SOUTH) - guideDirection(GuideDirection.DOWN_SOUTH, pulseAmt) + guideDirection(GuideDirection.SOUTH, pulseAmt) break } diff --git a/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/GuideGraph.kt b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/GuideGraph.kt index 217219dbc..f040cf0d4 100644 --- a/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/GuideGraph.kt +++ b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/GuideGraph.kt @@ -1,7 +1,6 @@ package nebulosa.guiding.internal import nebulosa.guiding.GuideDirection -import nebulosa.guiding.GuideStats import java.util.* import kotlin.math.max import kotlin.math.sqrt diff --git a/nebulosa-guiding/src/main/kotlin/nebulosa/guiding/GuideParity.kt b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/GuideParity.kt similarity index 91% rename from nebulosa-guiding/src/main/kotlin/nebulosa/guiding/GuideParity.kt rename to nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/GuideParity.kt index 3267664f0..cffd99f26 100644 --- a/nebulosa-guiding/src/main/kotlin/nebulosa/guiding/GuideParity.kt +++ b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/GuideParity.kt @@ -1,4 +1,4 @@ -package nebulosa.guiding +package nebulosa.guiding.internal enum class GuideParity { EVEN, // Guide(NORTH) moves scope north. diff --git a/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/GuidePoint.kt b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/GuidePoint.kt new file mode 100644 index 000000000..ec6b5d870 --- /dev/null +++ b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/GuidePoint.kt @@ -0,0 +1,25 @@ +package nebulosa.guiding.internal + +import nebulosa.math.Angle +import kotlin.math.hypot + +interface GuidePoint { + + val x: Double + + val y: Double + + val valid: Boolean + + fun dX(point: GuidePoint): Double + + fun dY(point: GuidePoint): Double + + val distance: Double + + fun distance(point: GuidePoint) = hypot(dX(point), dY(point)) + + val angle: Angle + + fun angle(point: GuidePoint): Angle +} diff --git a/nebulosa-guiding/src/main/kotlin/nebulosa/guiding/GuideStats.kt b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/GuideStats.kt similarity index 88% rename from nebulosa-guiding/src/main/kotlin/nebulosa/guiding/GuideStats.kt rename to nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/GuideStats.kt index 3e08cbeb4..af680df8d 100644 --- a/nebulosa-guiding/src/main/kotlin/nebulosa/guiding/GuideStats.kt +++ b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/GuideStats.kt @@ -1,4 +1,6 @@ -package nebulosa.guiding +package nebulosa.guiding.internal + +import nebulosa.guiding.GuideDirection data class GuideStats( val timestamp: Long = 0L, diff --git a/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/GuiderListener.kt b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/GuiderListener.kt new file mode 100644 index 000000000..2e57db51b --- /dev/null +++ b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/GuiderListener.kt @@ -0,0 +1,40 @@ +package nebulosa.guiding.internal + +import nebulosa.guiding.GuideDirection +import nebulosa.imaging.Image + +interface GuiderListener { + + fun onLockPositionChanged(position: GuidePoint) + + fun onStarSelected(star: StarPoint) + + fun onGuidingDithered(dx: Double, dy: Double) + + fun onGuidingStopped() + + fun onLockShiftLimitReached() + + fun onLooping(image: Image, number: Int, star: StarPoint?) + + fun onStarLost() + + fun onLockPositionLost() + + fun onStartCalibration() + + fun onCalibrationStep( + calibrationState: CalibrationState, + direction: GuideDirection, stepNumber: Int, + dx: Double, dy: Double, posX: Double, posY: Double, + distance: Double, + ) + + fun onCalibrationCompleted(calibration: GuideCalibration) + + fun onCalibrationFailed() + + fun onGuideStep(stats: GuideStats) + + fun onNotifyDirectMove(mount: GuidePoint) +} diff --git a/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/HysteresisGuideAlgorithm.kt b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/HysteresisGuideAlgorithm.kt index be8bcef4e..d686244dc 100644 --- a/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/HysteresisGuideAlgorithm.kt +++ b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/HysteresisGuideAlgorithm.kt @@ -1,6 +1,5 @@ package nebulosa.guiding.internal -import nebulosa.guiding.GuideAxis import kotlin.math.abs data class HysteresisGuideAlgorithm( diff --git a/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/IdentityGuideAlgorithm.kt b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/IdentityGuideAlgorithm.kt index cb561c5d4..61e7d8a0c 100644 --- a/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/IdentityGuideAlgorithm.kt +++ b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/IdentityGuideAlgorithm.kt @@ -1,7 +1,5 @@ package nebulosa.guiding.internal -import nebulosa.guiding.GuideAxis - class IdentityGuideAlgorithm(override val axis: GuideAxis) : GuideAlgorithm { override var minMove = -1.0 diff --git a/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/InternalGuider.kt b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/InternalGuider.kt index ddc7b545b..a3d845357 100644 --- a/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/InternalGuider.kt +++ b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/InternalGuider.kt @@ -1,8 +1,79 @@ package nebulosa.guiding.internal -import nebulosa.guiding.Guider +import nebulosa.imaging.Image -interface InternalGuider : Guider { +interface InternalGuider : Iterable { + + val primaryStar: StarPoint + + val lockPosition: GuidePoint + + var searchRegion: Double + + var dither: Dither + + var ditherAmount: Double + + var ditherRAOnly: Boolean + + var calibrationFlipRequiresDecFlip: Boolean + + var assumeDECOrthogonalToRA: Boolean + + var calibrationStep: Int + + var calibrationDistance: Int + + var useDECCompensation: Boolean + + var declinationGuideMode: DeclinationGuideMode + + var maxDECDuration: Int + + var maxRADuration: Int + + val isGuidingRAOnly + get() = declinationGuideMode == DeclinationGuideMode.NONE + + var noiseReductionMethod: NoiseReductionMethod + + var isGuidingEnabled: Boolean + + fun processImage(image: Image) + + val stats: List + + fun autoSelect(): Boolean + + fun selectGuideStar(x: Double, y: Double): Boolean + + fun deselectGuideStar() + + val isGuiding: Boolean + + fun startGuiding() + + fun stopGuiding() + + fun reset(fullReset: Boolean) + + val isCalibrating: Boolean + + fun clearCalibration() + + fun loadCalibration(calibration: GuideCalibration) + + fun dither() + + fun registerListener(listener: GuiderListener) + + fun unregisterListener(listener: GuiderListener) + + var isMultiStar: Boolean + + val isLockPositionShiftEnabled: Boolean + + val isSettling: Boolean var xGuideAlgorithm: GuideAlgorithm? diff --git a/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/LowPassGuideAlgorithm.kt b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/LowPassGuideAlgorithm.kt index 1aaa03f2a..d12701724 100644 --- a/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/LowPassGuideAlgorithm.kt +++ b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/LowPassGuideAlgorithm.kt @@ -1,6 +1,5 @@ package nebulosa.guiding.internal -import nebulosa.guiding.GuideAxis import kotlin.math.abs data class LowPassGuideAlgorithm( @@ -9,7 +8,7 @@ data class LowPassGuideAlgorithm( var slopeWeight: Double = DEFAULT_SLOPE_WEIGHT, ) : GuideAlgorithm { - private val axisStats = WindowedAxisStats(0) + private val axisStats = WindowedAxisStats() private var timeBase = 0 init { diff --git a/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/MultiStarGuider.kt b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/MultiStarGuider.kt index 561b766c5..15db9b460 100644 --- a/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/MultiStarGuider.kt +++ b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/MultiStarGuider.kt @@ -1,7 +1,7 @@ package nebulosa.guiding.internal import nebulosa.constants.PIOVERTWO -import nebulosa.guiding.* +import nebulosa.guiding.GuideDirection import nebulosa.imaging.Image import nebulosa.imaging.algorithms.Mean import nebulosa.imaging.algorithms.star.hfd.FindMode @@ -1018,10 +1018,10 @@ class MultiStarGuider : InternalGuider { yDistance = yGuideAlgorithm?.compute(yDistance) ?: 0.0 } - val xDirection = if (xDistance > 0.0) GuideDirection.LEFT_WEST else GuideDirection.RIGHT_EAST + val xDirection = if (xDistance > 0.0) GuideDirection.WEST else GuideDirection.EAST val requestedXAmount = abs(xDistance / guideCalibrator.xRate).roundToInt() - val yDirection = if (yDistance > 0.0) GuideDirection.DOWN_SOUTH else GuideDirection.UP_NORTH + val yDirection = if (yDistance > 0.0) GuideDirection.SOUTH else GuideDirection.NORTH val requestedYAmount = abs(yDistance / guideCalibrator.yRate).roundToInt() val xResult = moveAxis(xDirection, requestedXAmount, moveOptions) @@ -1055,15 +1055,15 @@ class MultiStarGuider : InternalGuider { var newDuration = duration when (direction) { - GuideDirection.UP_NORTH, - GuideDirection.DOWN_SOUTH -> { + GuideDirection.NORTH, + GuideDirection.SOUTH -> { // Enforce DEC guide mode and max duration for guide step (or deduced step) moves. if (MountMoveOption.ALGORITHM_RESULT in moveOptions || MountMoveOption.ALGORITHM_DEDUCE in moveOptions ) { if ((declinationGuideMode == DeclinationGuideMode.NONE) || - (direction == GuideDirection.DOWN_SOUTH && declinationGuideMode == DeclinationGuideMode.NORTH) || - (direction == GuideDirection.UP_NORTH && declinationGuideMode == DeclinationGuideMode.SOUTH) + (direction == GuideDirection.SOUTH && declinationGuideMode == DeclinationGuideMode.NORTH) || + (direction == GuideDirection.NORTH && declinationGuideMode == DeclinationGuideMode.SOUTH) ) { newDuration = 0 LOG.info("duration set to 0. mode={}", declinationGuideMode) @@ -1076,8 +1076,8 @@ class MultiStarGuider : InternalGuider { } } } - GuideDirection.LEFT_WEST, - GuideDirection.RIGHT_EAST -> { + GuideDirection.WEST, + GuideDirection.EAST -> { // Enforce RA guide mode and max duration for guide step (or deduced step) moves. if (MountMoveOption.ALGORITHM_RESULT in moveOptions || MountMoveOption.ALGORITHM_DEDUCE in moveOptions @@ -1101,10 +1101,10 @@ class MultiStarGuider : InternalGuider { } internal fun guideDirection(direction: GuideDirection, duration: Int) = when (direction) { - GuideDirection.UP_NORTH -> pulse.guideNorth(duration) - GuideDirection.DOWN_SOUTH -> pulse.guideSouth(duration) - GuideDirection.LEFT_WEST -> pulse.guideWest(duration) - GuideDirection.RIGHT_EAST -> pulse.guideEast(duration) + GuideDirection.NORTH -> pulse.guideNorth(duration) + GuideDirection.SOUTH -> pulse.guideSouth(duration) + GuideDirection.WEST -> pulse.guideWest(duration) + GuideDirection.EAST -> pulse.guideEast(duration) else -> false } diff --git a/nebulosa-guiding/src/main/kotlin/nebulosa/guiding/NoiseReductionMethod.kt b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/NoiseReductionMethod.kt similarity index 62% rename from nebulosa-guiding/src/main/kotlin/nebulosa/guiding/NoiseReductionMethod.kt rename to nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/NoiseReductionMethod.kt index 057433651..0fb1f7c11 100644 --- a/nebulosa-guiding/src/main/kotlin/nebulosa/guiding/NoiseReductionMethod.kt +++ b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/NoiseReductionMethod.kt @@ -1,4 +1,4 @@ -package nebulosa.guiding +package nebulosa.guiding.internal enum class NoiseReductionMethod { NONE, diff --git a/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/Point.kt b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/Point.kt index 48c477e75..546e5750a 100644 --- a/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/Point.kt +++ b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/Point.kt @@ -1,6 +1,5 @@ package nebulosa.guiding.internal -import nebulosa.guiding.GuidePoint import nebulosa.math.Angle import nebulosa.math.rad import kotlin.math.atan2 diff --git a/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/RandomDither.kt b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/RandomDither.kt index 7799eac6f..c24877604 100644 --- a/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/RandomDither.kt +++ b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/RandomDither.kt @@ -1,6 +1,5 @@ package nebulosa.guiding.internal -import nebulosa.guiding.Dither import kotlin.random.Random class RandomDither(private val random: Random = Random.Default) : Dither { diff --git a/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/ResistSwitchGuideAlgorithm.kt b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/ResistSwitchGuideAlgorithm.kt index 703c4e5a3..4b1520996 100644 --- a/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/ResistSwitchGuideAlgorithm.kt +++ b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/ResistSwitchGuideAlgorithm.kt @@ -1,6 +1,5 @@ package nebulosa.guiding.internal -import nebulosa.guiding.GuideAxis import nebulosa.log.loggerFor import kotlin.math.abs import kotlin.math.sign diff --git a/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/SpiralDither.kt b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/SpiralDither.kt index 50805b544..27456a626 100644 --- a/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/SpiralDither.kt +++ b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/SpiralDither.kt @@ -1,7 +1,5 @@ package nebulosa.guiding.internal -import nebulosa.guiding.Dither - class SpiralDither : Dither { private var prevRaOnly = false diff --git a/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/Star.kt b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/Star.kt index 6792c9e14..1ac667197 100644 --- a/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/Star.kt +++ b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/Star.kt @@ -1,6 +1,5 @@ package nebulosa.guiding.internal -import nebulosa.guiding.StarPoint import nebulosa.imaging.Image import nebulosa.imaging.algorithms.star.hfd.FindMode import nebulosa.imaging.algorithms.star.hfd.FindResult diff --git a/nebulosa-guiding/src/main/kotlin/nebulosa/guiding/StarPoint.kt b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/StarPoint.kt similarity index 81% rename from nebulosa-guiding/src/main/kotlin/nebulosa/guiding/StarPoint.kt rename to nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/StarPoint.kt index e4ce6c19e..fab3be6db 100644 --- a/nebulosa-guiding/src/main/kotlin/nebulosa/guiding/StarPoint.kt +++ b/nebulosa-guiding-internal/src/main/kotlin/nebulosa/guiding/internal/StarPoint.kt @@ -1,4 +1,4 @@ -package nebulosa.guiding +package nebulosa.guiding.internal interface StarPoint : GuidePoint { diff --git a/nebulosa-guiding-phd2/build.gradle.kts b/nebulosa-guiding-phd2/build.gradle.kts index 9787bf0e8..a2e9a4713 100644 --- a/nebulosa-guiding-phd2/build.gradle.kts +++ b/nebulosa-guiding-phd2/build.gradle.kts @@ -5,9 +5,10 @@ plugins { dependencies { api(project(":nebulosa-io")) + api(project(":nebulosa-common")) api(project(":nebulosa-guiding")) api(project(":nebulosa-phd2-client")) - implementation(libs.jackson) + api(project(":nebulosa-json")) implementation(project(":nebulosa-log")) testImplementation(project(":nebulosa-test")) } diff --git a/nebulosa-guiding-phd2/src/main/kotlin/nebulosa/guiding/phd2/PHD2Guider.kt b/nebulosa-guiding-phd2/src/main/kotlin/nebulosa/guiding/phd2/PHD2Guider.kt index 2489f4954..c677082f1 100644 --- a/nebulosa-guiding-phd2/src/main/kotlin/nebulosa/guiding/phd2/PHD2Guider.kt +++ b/nebulosa-guiding-phd2/src/main/kotlin/nebulosa/guiding/phd2/PHD2Guider.kt @@ -1,27 +1,376 @@ package nebulosa.guiding.phd2 +import nebulosa.common.concurrency.CountUpDownLatch +import nebulosa.guiding.* +import nebulosa.log.loggerFor +import nebulosa.math.arcsec +import nebulosa.math.toArcsec import nebulosa.phd2.client.PHD2Client +import nebulosa.phd2.client.PHD2EventListener +import nebulosa.phd2.client.commands.* +import nebulosa.phd2.client.events.* +import java.util.concurrent.TimeUnit -class PHD2Guider( - host: String, - port: Int = 4400, -) { +class PHD2Guider(private val client: PHD2Client) : Guider, PHD2EventListener { - private val client = PHD2Client(host, port) + private val dither = DoubleArray(2) + private val settling = CountUpDownLatch() + @Volatile private var shiftRate = SiderealShiftTrackingRate.DISABLED + @Volatile private var shiftEnabled = false + @Volatile private var shiftRateAxis = ShiftAxesType.RADEC + @Volatile private var lockPosition = GuidePoint.ZERO + @Volatile private var starPosition = GuidePoint.ZERO + private val listeners = hashSetOf() - // override fun connect() { - // client.registerListener(this) - // client.connect() - // } + override var pixelScale = 1.0 + private set - // override fun onEvent(client: PHD2Client, event: PHD2Event) { - // if (client === this.client) { - // println(event) - // } - // } + override var state = GuideState.STOPPED + private set(value) { + if (value != field) { + field = value + listeners.forEach { it.onStateChanged(value, pixelScale) } + } + } - // override fun close() { - // client.unregisterListener(this) - // client.close() - // } + override val isSettling + get() = settling.get() + + init { + client.registerListener(this) + } + + override var settleAmount = Guider.DEFAULT_SETTLE_AMOUNT + override var settleTime = Guider.DEFAULT_SETTLE_TIME + override var settleTimeout = Guider.DEFAULT_SETTLE_TIMEOUT + + override fun registerGuiderListener(listener: GuiderListener) { + listeners.add(listener) + } + + override fun unregisterGuiderListener(listener: GuiderListener) { + listeners.remove(listener) + } + + private inline fun fireMessage(lazyMessage: () -> String) { + if (listeners.isNotEmpty()) { + val message = lazyMessage() + listeners.forEach { it.onMessageReceived(message) } + } + } + + override fun startLooping(autoSelectGuideStar: Boolean) { + val state = client.sendCommandSync(GetAppState) + + if (state == GuideState.STOPPED) { + if (autoSelectGuideStar) { + autoSelectGuideStar() + } else { + client.sendCommandSync(Loop) + } + } else if (state == GuideState.LOOPING) { + if (autoSelectGuideStar) { + autoSelectGuideStar() + } + } + } + + override fun startGuiding(forceCalibration: Boolean, waitForSettle: Boolean) { + val state = client.sendCommandSync(GetAppState) + + if (state == GuideState.GUIDING) { + LOG.info("app is already guiding. skipping start guiding") + return + } + + if (state == GuideState.LOST_LOCK) { + LOG.info("app lost guide star and needs to stop before starting guiding again") + stopGuiding() + } + + if (state == GuideState.CALIBRATING) { + LOG.info("app is already calibrating. waiting for calibration to finish") + waitForCalibrationFinished() + } + + val isCalibrated = forceCalibration || client.sendCommandSync(GetCalibrated) + + startGuide(forceCalibration) + val starSelected = waitForStarSelected() + + if (starSelected != null) { + if (!isCalibrated) { + waitForCalibrationFinished() + } + + waitForGuidingStarted() + + if (waitForSettle) { + waitForSettle() + } + + return + } else { + fireMessage { "failed to select star" } + } + + stopGuiding() + } + + override fun stopGuiding(force: Boolean) { + if (!force) { + val state = client.sendCommandSync(GetAppState) + + if (state != GuideState.GUIDING && state != GuideState.CALIBRATING && state != GuideState.LOST_LOCK) { + fireMessage { "stop guiding skipped, as the app is already in state $state" } + return + } + } + + client.sendCommandSync(StopCapture) + } + + override fun autoSelectGuideStar() { + val state = client.sendCommandSync(GetAppState) + + if (state != GuideState.LOOPING) { + client.sendCommandSync(Loop) + waitForState(GuideState.LOOPING) + } + + // Wait for at least one exposure to finish. + val exposureTime = client.sendCommandSync(GetExposure) + Thread.sleep(exposureTime + 1000) + + client.sendCommandSync(FindStar()) + } + + override fun clearCalibration() { + client.sendCommandSync(ClearCalibration(WhichMount.BOTH)) + Thread.sleep(100) + } + + override fun dither(amount: Double, raOnly: Boolean) { + val state = client.sendCommandSync(GetAppState) + + if (state == GuideState.GUIDING) { + waitForSettle() + + val dither = Dither(amount, raOnly, settleAmount, settleTime, settleTimeout) + client.sendCommandSync(dither) + + settling.countUp() + waitForSettle() + } + } + + private fun waitForCalibrationFinished() { + while (true) { + val state = client.sendCommandSync(GetAppState) + if (state != GuideState.CALIBRATING) break + Thread.sleep(1000) + } + } + + private fun waitForGuidingStarted() { + waitForState(GuideState.GUIDING) + settling.countUp() + } + + private fun waitForState(state: GuideState) { + while (true) { + if (client.sendCommandSync(GetAppState) == state) break + Thread.sleep(1000) + } + } + + private fun waitForStarSelected(): IntArray? { + repeat(5) { + try { + return client.sendCommandSync(GetLockPosition, 5) + } catch (ignored: Throwable) { + Thread.sleep(5000) + } + } + + return null + } + + private fun startGuide(forceCalibration: Boolean): Boolean { + return try { + waitForSettle() + val command = Guide(settleAmount, settleTime, settleTimeout, forceCalibration) + client.sendCommandSync(command) + refreshShiftLockParams() + true + } catch (e: Throwable) { + false + } + } + + private fun refreshShiftLockParams() { + val shiftParams = client.sendCommandSync(GetLockShiftParams) + + if (shiftParams.enabled) { + shiftRate = if (shiftParams.units == RateUnit.PIXELS_HOUR) { + val (raShiftRate, decShiftRate) = shiftParams.rate + SiderealShiftTrackingRate((raShiftRate * pixelScale).arcsec, (decShiftRate * pixelScale).arcsec) + } else { + val (raShiftRate, decShiftRate) = shiftParams.rate + SiderealShiftTrackingRate(raShiftRate.arcsec, decShiftRate.arcsec) + } + + shiftRateAxis = shiftParams.axes + shiftEnabled = true + + LOG.info("shift lock params refreshed. rate={}, axes={}", shiftRate, shiftRateAxis) + } else { + shiftEnabled = false + } + } + + override fun waitForSettle() { + try { + settling.await(settleTimeout.inWholeNanoseconds, TimeUnit.NANOSECONDS) + } catch (e: InterruptedException) { + LOG.warn("PHD2 did not send SettleDone message in expected time") + } catch (e: Throwable) { + LOG.warn("an error occurrs while waiting for settle done", e) + } finally { + settling.reset() + } + } + + private fun restartForLostShiftLock() { + stopGuiding() + // Don't wait for settling when restarting due + // to lost lock shift, which should minimize downtime. + startGuiding(waitForSettle = false) + updateShiftRate(shiftRate) + } + + private fun updateShiftRate(shiftTrackingRate: SiderealShiftTrackingRate) { + if (!shiftTrackingRate.enabled) { + return stopShifting() + } + + shiftRate = shiftTrackingRate + val raArcsecPerHour = shiftTrackingRate.raPerHour.toArcsec + val decArcsecPerHour = shiftTrackingRate.decPerHour.toArcsec + + LOG.info("setting shift rate. ra={}, dec={}", raArcsecPerHour, decArcsecPerHour) + + val command = SetLockShiftParams(raArcsecPerHour, decArcsecPerHour, ShiftAxesType.RADEC, RateUnit.ARCSEC_HOUR) + client.sendCommandSync(command) + client.sendCommandSync(SetLockShiftEnabled(true)) + + refreshShiftLockParams() + } + + private fun stopShifting() { + if (!shiftEnabled) return + client.sendCommandSync(SetLockShiftEnabled(false)) + refreshShiftLockParams() + } + + override fun onEventReceived(event: PHD2Event) { + LOG.info("event received: {}", event) + + when (event) { + is AlertEvent -> Unit + is AppStateEvent -> state = event.state + is CalibratingEvent -> { + fireMessage { "${event.state}. step=${event.step} ${event.direction} dx=${event.dx} dy=${event.dy}" } + } + is CalibrationCompleteEvent -> { + client.sendCommand(GetPixelScale) + fireMessage { "calibration completed" } + } + is CalibrationDataFlippedEvent -> Unit + is CalibrationFailedEvent -> { + fireMessage { "calibration failed. ${event.reason}" } + } + ConfigurationChangeEvent -> client.sendCommand(GetPixelScale) + is GuideParamChangeEvent -> LOG.info("guide param changed: ${event.name} = ${event.value}") + is GuideStepEvent -> { + state = GuideState.GUIDING + + fireMessage { "frame=${event.frame} RA=${event.raDuration} ms ${event.raDirection} DEC=${event.decDuration} ms ${event.decDirection}" } + + if (listeners.isNotEmpty()) { + val guideStar = GuideStar(lockPosition, starPosition, "", event) + listeners.forEach { it.onGuideStepped(guideStar) } + } + } + is GuidingDitheredEvent -> { + dither[0] = event.dx + dither[1] = event.dy + fireMessage { "dithered. dx=${event.dx} dy=${event.dy}" } + } + GuidingStoppedEvent -> fireMessage { "guiding stopped" } + LockPositionLostEvent -> { + state = GuideState.LOST_LOCK + fireMessage { "lock position lost" } + } + is LockPositionSetEvent -> { + lockPosition = event + fireMessage { "lock position set. x=${event.x} y=${event.y}" } + } + LockPositionShiftLimitReachedEvent -> restartForLostShiftLock() + is LoopingExposuresEvent -> { + state = GuideState.LOOPING + fireMessage { "frame: ${event.frame}" } + } + LoopingExposuresStoppedEvent -> state = GuideState.STOPPED + PausedEvent -> state = GuideState.PAUSED + ResumedEvent -> Unit + SettleBeginEvent -> fireMessage { "settling started" } + is SettleDoneEvent -> { + settling.reset() + if (event.error.isEmpty()) fireMessage { "settling done" } + else fireMessage { event.error } + } + is SettlingEvent -> { + settling.countUp() + fireMessage { "settling. distance=${event.distance}" } + } + is StarLostEvent -> { + state = GuideState.LOST_LOCK + fireMessage { "star lost. status=${event.status}" } + } + is StarSelectedEvent -> { + starPosition = event + fireMessage { "star selected. x=${event.x} y=${event.y}" } + } + is StartCalibrationEvent -> { + state = GuideState.CALIBRATING + fireMessage { "calibration started" } + } + StartGuidingEvent -> fireMessage { "guiding started" } + is VersionEvent -> Unit + } + } + + override fun onCommandProcessed(command: PHD2Command, result: T?, error: String?) { + if (result != null) { + if (command is GetPixelScale) { + pixelScale = result as Double + LOG.info("pixel scale = {}", pixelScale) + listeners.forEach { it.onStateChanged(state, pixelScale) } + } + } else if (error != null) { + LOG.error("command error. command={}, message={}", command.methodName, error) + } + } + + override fun close() { + listeners.clear() + client.unregisterListener(this) + client.close() + } + + companion object { + + @JvmStatic private val LOG = loggerFor() + } } diff --git a/nebulosa-guiding-phd2/src/main/kotlin/nebulosa/guiding/phd2/SiderealShiftTrackingRate.kt b/nebulosa-guiding-phd2/src/main/kotlin/nebulosa/guiding/phd2/SiderealShiftTrackingRate.kt new file mode 100644 index 000000000..dd1836290 --- /dev/null +++ b/nebulosa-guiding-phd2/src/main/kotlin/nebulosa/guiding/phd2/SiderealShiftTrackingRate.kt @@ -0,0 +1,21 @@ +package nebulosa.guiding.phd2 + +import nebulosa.math.Angle +import nebulosa.math.PairOfAngle +import kotlin.time.Duration + +data class SiderealShiftTrackingRate( + val raPerHour: Angle, val decPerHour: Angle, + val enabled: Boolean = true, +) { + + constructor(start: PairOfAngle, end: PairOfAngle, between: Duration) : this( + (end.first - start.first) / between.inWholeSeconds / 3600.0, + (end.second - start.second) / between.inWholeSeconds / 3600.0, + ) + + companion object { + + @JvmStatic val DISABLED = SiderealShiftTrackingRate(0.0, 0.0, false) + } +} diff --git a/nebulosa-guiding-phd2/src/test/kotlin/PHD2ClientTest.kt b/nebulosa-guiding-phd2/src/test/kotlin/PHD2ClientTest.kt deleted file mode 100644 index 8de8b9959..000000000 --- a/nebulosa-guiding-phd2/src/test/kotlin/PHD2ClientTest.kt +++ /dev/null @@ -1,26 +0,0 @@ -import io.kotest.core.annotation.Ignored -import io.kotest.core.spec.style.StringSpec -import nebulosa.phd2.client.PHD2Client -import nebulosa.phd2.client.PHD2EventListener -import nebulosa.phd2.client.event.PHD2Event - -@Ignored -@Suppress("BlockingMethodInNonBlockingContext") -class PHD2ClientTest : StringSpec(), PHD2EventListener { - - init { - "start" { - val client = PHD2Client("pi.local") - client.registerListener(this@PHD2ClientTest) - client.connect() - - Thread.sleep(70000) - - client.close() - } - } - - override fun onEvent(client: PHD2Client, event: PHD2Event) { - println(event) - } -} diff --git a/nebulosa-guiding/src/main/kotlin/nebulosa/guiding/GuideDirection.kt b/nebulosa-guiding/src/main/kotlin/nebulosa/guiding/GuideDirection.kt index fa40f090f..e9fdaf49f 100644 --- a/nebulosa-guiding/src/main/kotlin/nebulosa/guiding/GuideDirection.kt +++ b/nebulosa-guiding/src/main/kotlin/nebulosa/guiding/GuideDirection.kt @@ -1,16 +1,16 @@ package nebulosa.guiding enum class GuideDirection { - UP_NORTH, // DEC+ - DOWN_SOUTH, // DEC- - LEFT_WEST, // RA+ - RIGHT_EAST; // RA- + NORTH, // UP, DEC+ + SOUTH, // DOWN, DEC- + WEST, // LEFT, RA+ + EAST; // RIGHT, RA- val reversed get() = when (this) { - UP_NORTH -> DOWN_SOUTH - DOWN_SOUTH -> UP_NORTH - LEFT_WEST -> RIGHT_EAST - RIGHT_EAST -> LEFT_WEST + NORTH -> SOUTH + SOUTH -> NORTH + WEST -> EAST + EAST -> WEST } } diff --git a/nebulosa-guiding/src/main/kotlin/nebulosa/guiding/GuidePoint.kt b/nebulosa-guiding/src/main/kotlin/nebulosa/guiding/GuidePoint.kt index e67be8a9f..7e54b7e41 100644 --- a/nebulosa-guiding/src/main/kotlin/nebulosa/guiding/GuidePoint.kt +++ b/nebulosa-guiding/src/main/kotlin/nebulosa/guiding/GuidePoint.kt @@ -1,25 +1,16 @@ package nebulosa.guiding -import nebulosa.math.Angle -import kotlin.math.hypot - interface GuidePoint { - val x: Double - - val y: Double - - val valid: Boolean - - fun dX(point: GuidePoint): Double - - fun dY(point: GuidePoint): Double - - val distance: Double + val x: Int - fun distance(point: GuidePoint) = hypot(dX(point), dY(point)) + val y: Int - val angle: Angle + companion object { - fun angle(point: GuidePoint): Angle + @JvmStatic val ZERO = object : GuidePoint { + override val x = 0 + override val y = 0 + } + } } diff --git a/nebulosa-guiding/src/main/kotlin/nebulosa/guiding/GuideStar.kt b/nebulosa-guiding/src/main/kotlin/nebulosa/guiding/GuideStar.kt new file mode 100644 index 000000000..85fe12746 --- /dev/null +++ b/nebulosa-guiding/src/main/kotlin/nebulosa/guiding/GuideStar.kt @@ -0,0 +1,8 @@ +package nebulosa.guiding + +data class GuideStar( + val lockPosition: GuidePoint = GuidePoint.ZERO, + val starPosition: GuidePoint = GuidePoint.ZERO, + val image: String = "", + val guideStep: GuideStep? = null, +) diff --git a/nebulosa-guiding/src/main/kotlin/nebulosa/guiding/GuideState.kt b/nebulosa-guiding/src/main/kotlin/nebulosa/guiding/GuideState.kt new file mode 100644 index 000000000..bcb7632d1 --- /dev/null +++ b/nebulosa-guiding/src/main/kotlin/nebulosa/guiding/GuideState.kt @@ -0,0 +1,11 @@ +package nebulosa.guiding + +enum class GuideState { + STOPPED, + SELECTED, + CALIBRATING, + GUIDING, + LOST_LOCK, + PAUSED, + LOOPING, +} diff --git a/nebulosa-guiding/src/main/kotlin/nebulosa/guiding/GuideStep.kt b/nebulosa-guiding/src/main/kotlin/nebulosa/guiding/GuideStep.kt new file mode 100644 index 000000000..e6ea8740b --- /dev/null +++ b/nebulosa-guiding/src/main/kotlin/nebulosa/guiding/GuideStep.kt @@ -0,0 +1,30 @@ +package nebulosa.guiding + +interface GuideStep { + + val frame: Int + + val starMass: Double + + val snr: Double + + val hfd: Double + + val dx: Double + + val dy: Double + + val raDistance: Double + + val decDistance: Double + + val raDuration: Long + + val raDirection: GuideDirection + + val decDuration: Long + + val decDirection: GuideDirection + + val averageDistance: Double +} diff --git a/nebulosa-guiding/src/main/kotlin/nebulosa/guiding/Guider.kt b/nebulosa-guiding/src/main/kotlin/nebulosa/guiding/Guider.kt index 5abeb8306..2f9db6495 100644 --- a/nebulosa-guiding/src/main/kotlin/nebulosa/guiding/Guider.kt +++ b/nebulosa-guiding/src/main/kotlin/nebulosa/guiding/Guider.kt @@ -1,77 +1,45 @@ package nebulosa.guiding -import nebulosa.imaging.Image +import java.io.Closeable +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds -interface Guider : Iterable { +interface Guider : Closeable { - val primaryStar: StarPoint + val state: GuideState - val lockPosition: GuidePoint + val pixelScale: Double - var searchRegion: Double - - var dither: Dither - - var ditherAmount: Double - - var ditherRAOnly: Boolean - - var calibrationFlipRequiresDecFlip: Boolean - - var assumeDECOrthogonalToRA: Boolean - - var calibrationStep: Int - - var calibrationDistance: Int - - var useDECCompensation: Boolean - - var declinationGuideMode: DeclinationGuideMode - - var maxDECDuration: Int - - var maxRADuration: Int - - val isGuidingRAOnly - get() = declinationGuideMode == DeclinationGuideMode.NONE - - var noiseReductionMethod: NoiseReductionMethod - - var isGuidingEnabled: Boolean - - fun processImage(image: Image) + val isSettling: Boolean - val stats: List + var settleAmount: Double - fun autoSelect(): Boolean + var settleTime: Duration - fun selectGuideStar(x: Double, y: Double): Boolean + var settleTimeout: Duration - fun deselectGuideStar() + fun registerGuiderListener(listener: GuiderListener) - val isGuiding: Boolean + fun unregisterGuiderListener(listener: GuiderListener) - fun startGuiding() + fun autoSelectGuideStar() - fun stopGuiding() + fun startLooping(autoSelectGuideStar: Boolean = true) - fun reset(fullReset: Boolean) + fun startGuiding(forceCalibration: Boolean = false, waitForSettle: Boolean = true) - val isCalibrating: Boolean + fun stopGuiding(force: Boolean = false) fun clearCalibration() - fun loadCalibration(calibration: GuideCalibration) - - fun dither() - - fun registerListener(listener: GuiderListener) + fun dither(amount: Double, raOnly: Boolean = false) - fun unregisterListener(listener: GuiderListener) + fun waitForSettle() - var isMultiStar: Boolean + companion object { - val isLockPositionShiftEnabled: Boolean - - val isSettling: Boolean + @JvmStatic val DEFAULT_SETTLE_AMOUNT = 1.5 + @JvmStatic val DEFAULT_SETTLE_TIME = 10.seconds + @JvmStatic val DEFAULT_SETTLE_TIMEOUT = 30.seconds + } } diff --git a/nebulosa-guiding/src/main/kotlin/nebulosa/guiding/GuiderListener.kt b/nebulosa-guiding/src/main/kotlin/nebulosa/guiding/GuiderListener.kt index 72d5085be..f58744e9d 100644 --- a/nebulosa-guiding/src/main/kotlin/nebulosa/guiding/GuiderListener.kt +++ b/nebulosa-guiding/src/main/kotlin/nebulosa/guiding/GuiderListener.kt @@ -1,39 +1,12 @@ package nebulosa.guiding -import nebulosa.imaging.Image - interface GuiderListener { - fun onLockPositionChanged(position: GuidePoint) - - fun onStarSelected(star: StarPoint) - - fun onGuidingDithered(dx: Double, dy: Double) - - fun onGuidingStopped() - - fun onLockShiftLimitReached() - - fun onLooping(image: Image, number: Int, star: StarPoint?) - - fun onStarLost() - - fun onLockPositionLost() - - fun onStartCalibration() - - fun onCalibrationStep( - calibrationState: CalibrationState, - direction: GuideDirection, stepNumber: Int, - dx: Double, dy: Double, posX: Double, posY: Double, - distance: Double, - ) - - fun onCalibrationCompleted(calibration: GuideCalibration) + fun onStateChanged(state: GuideState, pixelScale: Double) = Unit - fun onCalibrationFailed() + fun onGuideStepped(guideStar: GuideStar) = Unit - fun onGuideStep(stats: GuideStats) + fun onDithered(dx: Double, dy: Double) = Unit - fun onNotifyDirectMove(mount: GuidePoint) + fun onMessageReceived(message: String) = Unit } diff --git a/nebulosa-horizons/src/main/kotlin/nebulosa/horizons/HorizonsQuantity.kt b/nebulosa-horizons/src/main/kotlin/nebulosa/horizons/HorizonsQuantity.kt index a143e5537..681dd2109 100644 --- a/nebulosa-horizons/src/main/kotlin/nebulosa/horizons/HorizonsQuantity.kt +++ b/nebulosa-horizons/src/main/kotlin/nebulosa/horizons/HorizonsQuantity.kt @@ -1,4 +1,4 @@ -package nebulosa.horizons; +package nebulosa.horizons enum class HorizonsQuantity( @JvmField internal val code: Int, diff --git a/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/DeviceProtocolHandler.kt b/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/DeviceProtocolHandler.kt index 3e78e8d35..5214c3cfe 100644 --- a/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/DeviceProtocolHandler.kt +++ b/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/DeviceProtocolHandler.kt @@ -79,6 +79,18 @@ abstract class DeviceProtocolHandler : MessageSender, INDIProtocolParser { handlers.forEach { it.onEventReceived(event) } } + internal fun registerGPS(device: GPS) { + gps[device.name] = device + fireOnEventReceived(GPSAttached(device)) + } + + internal fun unregisterGPS(device: GPS) { + if (device.name in gps) { + guideOutputs.remove(device.name) + fireOnEventReceived(GPSDetached(device)) + } + } + internal fun registerGuideOutput(device: GuideOutput) { guideOutputs[device.name] = device fireOnEventReceived(GuideOutputAttached(device)) diff --git a/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/mount/MountDevice.kt b/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/mount/MountDevice.kt index 16e110c5a..f45fce657 100644 --- a/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/mount/MountDevice.kt +++ b/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/mount/MountDevice.kt @@ -5,9 +5,6 @@ import nebulosa.indi.client.device.DeviceProtocolHandler import nebulosa.indi.device.firstOnSwitch import nebulosa.indi.device.firstOnSwitchOrNull import nebulosa.indi.device.gps.GPS -import nebulosa.indi.device.gps.GPSDetached -import nebulosa.indi.device.guide.GuideOutputAttached -import nebulosa.indi.device.guide.GuideOutputDetached import nebulosa.indi.device.guide.GuideOutputPulsingChanged import nebulosa.indi.device.mount.* import nebulosa.indi.protocol.* @@ -155,7 +152,7 @@ internal open class MountDevice( if (!canPulseGuide && message is DefNumberVector) { canPulseGuide = true - handler.fireOnEventReceived(GuideOutputAttached(this)) + handler.registerGuideOutput(this) LOG.info("guide output attached: {}", name) } @@ -317,13 +314,13 @@ internal open class MountDevice( override fun close() { if (canPulseGuide) { canPulseGuide = false - handler.fireOnEventReceived(GuideOutputDetached(this)) + handler.unregisterGuideOutput(this) LOG.info("guide output detached: {}", name) } if (hasGPS) { hasGPS = false - handler.fireOnEventReceived(GPSDetached(this)) + handler.unregisterGPS(this) LOG.info("GPS detached: {}", name) } } diff --git a/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/Device.kt b/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/Device.kt index 686be2859..59c0a6910 100644 --- a/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/Device.kt +++ b/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/Device.kt @@ -49,10 +49,10 @@ interface Device : INDIProtocolHandler, Closeable { vector.device = this.name vector.name = name - for (element in elements) { + for ((first, second) in elements) { val switch = OneSwitch() - switch.name = element.first - switch.value = element.second + switch.name = first + switch.value = second vector.elements.add(switch) } @@ -74,10 +74,10 @@ interface Device : INDIProtocolHandler, Closeable { vector.device = this.name vector.name = name - for (element in elements) { + for ((first, second) in elements) { val switch = OneNumber() - switch.name = element.first - switch.value = element.second + switch.name = first + switch.value = second vector.elements.add(switch) } @@ -99,10 +99,10 @@ interface Device : INDIProtocolHandler, Closeable { vector.device = this.name vector.name = name - for (element in elements) { + for ((first, second) in elements) { val switch = OneText() - switch.name = element.first - switch.value = element.second + switch.name = first + switch.value = second vector.elements.add(switch) } diff --git a/nebulosa-json/build.gradle.kts b/nebulosa-json/build.gradle.kts index b44e829f4..7ab28945d 100644 --- a/nebulosa-json/build.gradle.kts +++ b/nebulosa-json/build.gradle.kts @@ -4,7 +4,7 @@ plugins { } dependencies { - api(libs.jackson) + api(libs.bundles.jackson) testImplementation(project(":nebulosa-test")) } diff --git a/nebulosa-json/src/main/kotlin/nebulosa/json/modules/FromJson.kt b/nebulosa-json/src/main/kotlin/nebulosa/json/FromJson.kt similarity index 88% rename from nebulosa-json/src/main/kotlin/nebulosa/json/modules/FromJson.kt rename to nebulosa-json/src/main/kotlin/nebulosa/json/FromJson.kt index 68a3b2687..20fc294da 100644 --- a/nebulosa-json/src/main/kotlin/nebulosa/json/modules/FromJson.kt +++ b/nebulosa-json/src/main/kotlin/nebulosa/json/FromJson.kt @@ -1,4 +1,4 @@ -package nebulosa.json.modules +package nebulosa.json import com.fasterxml.jackson.core.JsonParser import com.fasterxml.jackson.databind.DeserializationContext diff --git a/nebulosa-json/src/main/kotlin/nebulosa/json/Module.kt b/nebulosa-json/src/main/kotlin/nebulosa/json/Module.kt new file mode 100644 index 000000000..464d7eac5 --- /dev/null +++ b/nebulosa-json/src/main/kotlin/nebulosa/json/Module.kt @@ -0,0 +1,40 @@ +package nebulosa.json + +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.SerializerProvider +import com.fasterxml.jackson.databind.deser.std.StdDeserializer +import com.fasterxml.jackson.databind.module.SimpleModule +import com.fasterxml.jackson.databind.ser.std.StdSerializer + +data class ToJsonSerializer(private val serializer: ToJson) : StdSerializer(serializer.type) { + + override fun serialize(value: T?, gen: JsonGenerator, provider: SerializerProvider) { + if (value != null) serializer.serialize(value, gen, provider) + else gen.writeNull() + } +} + +data class FromJsonDeserializer(private val deserializer: FromJson) : StdDeserializer(deserializer.type) { + + override fun deserialize(p: JsonParser, ctxt: DeserializationContext): T? { + return deserializer.deserialize(p, ctxt) + } +} + +@Suppress("NOTHING_TO_INLINE") +inline fun SimpleModule.addSerializer(serializer: ToJson) = apply { + addSerializer(serializer.type, ToJsonSerializer(serializer)) +} + +@Suppress("NOTHING_TO_INLINE") +inline fun SimpleModule.addDeserializer(deserializer: FromJson) = apply { + addDeserializer(deserializer.type, FromJsonDeserializer(deserializer)) +} + +@Suppress("NOTHING_TO_INLINE") +inline fun SimpleModule.addConverter(converter: T) where T : FromJson<*>, T : ToJson<*> = apply { + addSerializer(converter) + addDeserializer(converter) +} diff --git a/nebulosa-json/src/main/kotlin/nebulosa/json/modules/ToJson.kt b/nebulosa-json/src/main/kotlin/nebulosa/json/ToJson.kt similarity index 88% rename from nebulosa-json/src/main/kotlin/nebulosa/json/modules/ToJson.kt rename to nebulosa-json/src/main/kotlin/nebulosa/json/ToJson.kt index 0511ae882..be70b51d0 100644 --- a/nebulosa-json/src/main/kotlin/nebulosa/json/modules/ToJson.kt +++ b/nebulosa-json/src/main/kotlin/nebulosa/json/ToJson.kt @@ -1,4 +1,4 @@ -package nebulosa.json.modules +package nebulosa.json import com.fasterxml.jackson.core.JsonGenerator import com.fasterxml.jackson.databind.SerializerProvider diff --git a/api/src/main/kotlin/nebulosa/api/beans/converters/PathConverter.kt b/nebulosa-json/src/main/kotlin/nebulosa/json/converters/PathConverter.kt similarity index 71% rename from api/src/main/kotlin/nebulosa/api/beans/converters/PathConverter.kt rename to nebulosa-json/src/main/kotlin/nebulosa/json/converters/PathConverter.kt index 85a165cd3..356531347 100644 --- a/api/src/main/kotlin/nebulosa/api/beans/converters/PathConverter.kt +++ b/nebulosa-json/src/main/kotlin/nebulosa/json/converters/PathConverter.kt @@ -1,16 +1,14 @@ -package nebulosa.api.beans.converters +package nebulosa.json.converters import com.fasterxml.jackson.core.JsonGenerator import com.fasterxml.jackson.core.JsonParser import com.fasterxml.jackson.databind.DeserializationContext import com.fasterxml.jackson.databind.SerializerProvider -import nebulosa.json.modules.FromJson -import nebulosa.json.modules.ToJson -import org.springframework.stereotype.Component +import nebulosa.json.FromJson +import nebulosa.json.ToJson import java.nio.file.Path -@Component -class PathConverter : ToJson, FromJson { +object PathConverter : FromJson, ToJson { override val type = Path::class.java diff --git a/nebulosa-json/src/main/kotlin/nebulosa/json/modules/JsonModule.kt b/nebulosa-json/src/main/kotlin/nebulosa/json/modules/JsonModule.kt deleted file mode 100644 index ecdf3ab13..000000000 --- a/nebulosa-json/src/main/kotlin/nebulosa/json/modules/JsonModule.kt +++ /dev/null @@ -1,48 +0,0 @@ -package nebulosa.json.modules - -import com.fasterxml.jackson.core.JsonGenerator -import com.fasterxml.jackson.core.JsonParser -import com.fasterxml.jackson.databind.DeserializationContext -import com.fasterxml.jackson.databind.SerializerProvider -import com.fasterxml.jackson.databind.deser.std.StdDeserializer -import com.fasterxml.jackson.databind.module.SimpleModule -import com.fasterxml.jackson.databind.ser.std.StdSerializer - -data class JsonModule( - private val serializers: Iterable>, - private val deserializers: Iterable>, -) : SimpleModule() { - - init { - for (serializer in serializers) { - addToJson(serializer) - } - - for (deserializer in deserializers) { - addFromJson(deserializer) - } - } - - fun addToJson(toJson: ToJson) { - addSerializer(toJson.type, ToJsonSerializer(toJson)) - } - - fun addFromJson(fromJson: FromJson) { - addDeserializer(fromJson.type, FromJsonDeserializer(fromJson)) - } - - private data class ToJsonSerializer(private val serializer: ToJson) : StdSerializer(serializer.type) { - - override fun serialize(value: T?, gen: JsonGenerator, provider: SerializerProvider) { - if (value != null) serializer.serialize(value, gen, provider) - else gen.writeNull() - } - } - - private data class FromJsonDeserializer(private val deserializer: FromJson) : StdDeserializer(deserializer.type) { - - override fun deserialize(p: JsonParser, ctxt: DeserializationContext): T? { - return deserializer.deserialize(p, ctxt) - } - } -} diff --git a/nebulosa-lx200-protocol/src/main/kotlin/nebulosa/lx200/protocol/LX200ProtocolMessage.kt b/nebulosa-lx200-protocol/src/main/kotlin/nebulosa/lx200/protocol/LX200ProtocolMessage.kt index 0976ef530..7b3f1798e 100644 --- a/nebulosa-lx200-protocol/src/main/kotlin/nebulosa/lx200/protocol/LX200ProtocolMessage.kt +++ b/nebulosa-lx200-protocol/src/main/kotlin/nebulosa/lx200/protocol/LX200ProtocolMessage.kt @@ -6,11 +6,11 @@ import java.time.LocalTime sealed interface LX200ProtocolMessage { - object Ack : LX200ProtocolMessage + data object Ack : LX200ProtocolMessage - object Ok : LX200ProtocolMessage + data object Ok : LX200ProtocolMessage - object Zero : LX200ProtocolMessage + data object Zero : LX200ProtocolMessage data class Text(val text: String) : LX200ProtocolMessage diff --git a/nebulosa-math/src/main/kotlin/nebulosa/math/Angle.kt b/nebulosa-math/src/main/kotlin/nebulosa/math/Angle.kt index f7d341353..c78a1106e 100644 --- a/nebulosa-math/src/main/kotlin/nebulosa/math/Angle.kt +++ b/nebulosa-math/src/main/kotlin/nebulosa/math/Angle.kt @@ -177,4 +177,4 @@ inline val String?.hours get() = Angle(this, true) inline val String?.deg - get() = Angle(this, false) + get() = Angle(this) diff --git a/nebulosa-nasa/src/main/kotlin/nebulosa/nasa/pck/PckSegment.kt b/nebulosa-nasa/src/main/kotlin/nebulosa/nasa/pck/PckSegment.kt index b101a1253..fc220897b 100644 --- a/nebulosa-nasa/src/main/kotlin/nebulosa/nasa/pck/PckSegment.kt +++ b/nebulosa-nasa/src/main/kotlin/nebulosa/nasa/pck/PckSegment.kt @@ -1,4 +1,4 @@ -package nebulosa.nasa.pck; +package nebulosa.nasa.pck import nebulosa.erfa.PositionAndVelocity import nebulosa.time.InstantOfTime diff --git a/nebulosa-nasa/src/main/kotlin/nebulosa/nasa/spk/Type21Segment.kt b/nebulosa-nasa/src/main/kotlin/nebulosa/nasa/spk/Type21Segment.kt index 946c4eaaa..3f90fd3df 100644 --- a/nebulosa-nasa/src/main/kotlin/nebulosa/nasa/spk/Type21Segment.kt +++ b/nebulosa-nasa/src/main/kotlin/nebulosa/nasa/spk/Type21Segment.kt @@ -96,6 +96,7 @@ internal data class Type21Segment( } private fun computeCoefficient(recordIndex: Int): Boolean { + if (recordIndex < 0) return false if (recordIndex in coefficients) return true val mdaRecord = spk.daf.read( diff --git a/nebulosa-netty/src/main/kotlin/nebulosa/netty/NettyClient.kt b/nebulosa-netty/src/main/kotlin/nebulosa/netty/NettyClient.kt index 214ab044a..57c215986 100644 --- a/nebulosa-netty/src/main/kotlin/nebulosa/netty/NettyClient.kt +++ b/nebulosa-netty/src/main/kotlin/nebulosa/netty/NettyClient.kt @@ -11,21 +11,17 @@ import nebulosa.log.loggerFor import java.io.Closeable import java.util.concurrent.atomic.AtomicReference -abstract class NettyClient : Runnable, Closeable { +abstract class NettyClient : Closeable { protected val channel = AtomicReference() - abstract val host: String - - abstract val port: Int - protected abstract val channelInitialzer: ChannelInitializer - val running + val isOpen get() = channel.get() != null - final override fun run() { - require(!running) { "the server has already been started" } + fun open(host: String, port: Int) { + require(!isOpen) { "the server has already been started" } val masterGroup = NioEventLoopGroup() @@ -34,7 +30,6 @@ abstract class NettyClient : Runnable, Closeable { b.group(masterGroup) .channel(NioSocketChannel::class.java) .handler(channelInitialzer) - .option(ChannelOption.SO_BACKLOG, 128) .option(ChannelOption.TCP_NODELAY, true) val future = b.connect(host, port).sync() diff --git a/nebulosa-phd2-client/build.gradle.kts b/nebulosa-phd2-client/build.gradle.kts index f201809d1..4e837f030 100644 --- a/nebulosa-phd2-client/build.gradle.kts +++ b/nebulosa-phd2-client/build.gradle.kts @@ -4,8 +4,9 @@ plugins { } dependencies { - api(project(":nebulosa-io")) - implementation(libs.jackson) + api(project(":nebulosa-netty")) + api(project(":nebulosa-json")) + api(project(":nebulosa-guiding")) implementation(project(":nebulosa-log")) testImplementation(project(":nebulosa-test")) } diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/PHD2Client.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/PHD2Client.kt index 786bb4625..cf4e34146 100644 --- a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/PHD2Client.kt +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/PHD2Client.kt @@ -1,27 +1,40 @@ package nebulosa.phd2.client +import com.fasterxml.jackson.annotation.JsonInclude import com.fasterxml.jackson.databind.DeserializationFeature -import com.fasterxml.jackson.databind.ObjectMapper -import nebulosa.log.debug +import com.fasterxml.jackson.databind.MapperFeature +import com.fasterxml.jackson.databind.module.SimpleModule +import com.fasterxml.jackson.module.kotlin.jsonMapper +import io.netty.channel.ChannelInitializer +import io.netty.channel.socket.SocketChannel +import nebulosa.json.addConverter +import nebulosa.json.addDeserializer +import nebulosa.json.converters.PathConverter import nebulosa.log.loggerFor -import nebulosa.phd2.client.event.* -import okio.buffer -import okio.source -import java.io.Closeable -import java.io.InputStream -import java.net.InetSocketAddress -import java.net.Socket - -class PHD2Client( - val host: String, - val port: Int = 4400, -) : Closeable { - - private val listeners = hashSetOf() - - @Volatile private var socket: Socket? = null - @Volatile private var input: InputStream? = null - @Volatile private var thread: Thread? = null +import nebulosa.netty.NettyClient +import nebulosa.phd2.client.commands.CompletableCommand +import nebulosa.phd2.client.commands.PHD2Command +import nebulosa.phd2.client.events.GuideStateConverter +import java.util.* +import java.util.concurrent.CompletableFuture +import java.util.concurrent.TimeUnit +import kotlin.math.max + +class PHD2Client : NettyClient() { + + @JvmField internal val listeners = hashSetOf() + @JvmField internal val commands = hashMapOf>() + + override val channelInitialzer = object : ChannelInitializer() { + + override fun initChannel(ch: SocketChannel) { + ch.pipeline().addLast( + PHD2ProtocolDecoder(this@PHD2Client, JSON_MAPPER), + PHD2ProtocolEncoder(JSON_MAPPER), + PHD2ProtocolHandler(this@PHD2Client), + ) + } + } fun registerListener(listener: PHD2EventListener) { listeners.add(listener) @@ -31,87 +44,44 @@ class PHD2Client( listeners.remove(listener) } - fun connect() { - require(socket == null) { "socket is already open" } + @Synchronized + fun sendCommand(command: PHD2Command, timeout: Long = 30): CompletableFuture { + val task = CompletableFuture() + val id = UUID.randomUUID().toString() - val socket = Socket() - socket.connect(InetSocketAddress(host, port)) - input = socket.getInputStream() - this.socket = socket + val completableCommand = CompletableCommand(command, task, id) + commands[id] = completableCommand + channel.get()?.channel()?.writeAndFlush(completableCommand) - thread = Thread(::readEvent) - thread!!.isDaemon = true - thread!!.start() + return task.orTimeout(max(1L, timeout), TimeUnit.SECONDS) + .whenComplete { _, e -> + if (e != null) LOG.error("Command error: $command", e) + commands.remove(id) + } } - override fun close() { - thread?.interrupt() - thread = null - - input = null - - socket?.close() - socket = null - - listeners.clear() + fun sendCommandSync(command: PHD2Command, timeout: Long = 30): T { + return sendCommand(command, timeout).get() } - private fun readEvent() { - val buffer = input!!.source().buffer() - - try { - while (true) { - val eventText = buffer.readUtf8Line() ?: break - val eventName = EVENT_NAME_REGEX.matchEntire(eventText)?.groupValues?.get(1) ?: continue + companion object { - LOG.debug { "event received. event=$eventText" } + const val DEFAULT_PORT = 4400 - val type = EVENT_TYPES[eventName] ?: continue - val event = type.second ?: OBJECT_MAPPER.readValue(eventText, type.first) - listeners.forEach { it.onEvent(this, event) } - } - } catch (_: InterruptedException) { - } catch (e: Throwable) { - LOG.error("event read error", e) - } - } + @JvmStatic private val LOG = loggerFor() - companion object { + private val MODULE = SimpleModule() - @JvmStatic private val LOG = loggerFor() + init { + MODULE.addDeserializer(PathConverter) + MODULE.addConverter(GuideStateConverter) + } - @JvmStatic private val OBJECT_MAPPER = ObjectMapper() - .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) - - @JvmStatic private val EVENT_NAME_REGEX = Regex(".*\"Event\":\"(.*?)\".*") - - @JvmStatic private val EVENT_TYPES = mapOf( - "Alert" to (Alert::class.java to null), - "AppState" to (AppState::class.java to null), - "Calibrating" to (Calibrating::class.java to null), - "CalibrationComplete" to (CalibrationComplete::class.java to null), - "CalibrationDataFlipped" to (CalibrationDataFlipped::class.java to null), - "CalibrationFailed" to (CalibrationFailed::class.java to null), - "ConfigurationChange" to (ConfigurationChange::class.java to ConfigurationChange), - "GuideParamChange" to (GuideParamChange::class.java to null), - "GuideStep" to (GuideStep::class.java to null), - "GuidingDithered" to (GuidingDithered::class.java to null), - "GuidingStopped" to (GuidingStopped::class.java to GuidingStopped), - "LockPositionLost" to (LockPositionLost::class.java to LockPositionLost), - "LockPositionSet" to (LockPositionSet::class.java to null), - "LockPositionShiftLimitReached" to (LockPositionShiftLimitReached::class.java to LockPositionShiftLimitReached), - "LoopingExposures" to (LoopingExposures::class.java to null), - "LoopingExposuresStopped" to (LoopingExposuresStopped::class.java to LoopingExposuresStopped), - "Paused" to (Paused::class.java to Paused), - "Resumed" to (Resumed::class.java to Resumed), - "SettleBegin" to (SettleBegin::class.java to SettleBegin), - "SettleDone" to (SettleDone::class.java to null), - "Settling" to (Settling::class.java to null), - "StarLost" to (StarLost::class.java to null), - "StarSelected" to (StarSelected::class.java to null), - "StartCalibration" to (StartCalibration::class.java to null), - "StartGuiding" to (StartGuiding::class.java to StartGuiding), - "Version" to (Version::class.java to null), - ) + private val JSON_MAPPER = jsonMapper { + disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS) + serializationInclusion(JsonInclude.Include.NON_NULL) + addModule(MODULE) + } } } diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/PHD2EventListener.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/PHD2EventListener.kt index c555f08f9..ff8939ff6 100644 --- a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/PHD2EventListener.kt +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/PHD2EventListener.kt @@ -1,8 +1,11 @@ package nebulosa.phd2.client -import nebulosa.phd2.client.event.PHD2Event +import nebulosa.phd2.client.commands.PHD2Command +import nebulosa.phd2.client.events.PHD2Event -fun interface PHD2EventListener { +interface PHD2EventListener { - fun onEvent(client: PHD2Client, event: PHD2Event) + fun onEventReceived(event: PHD2Event) + + fun onCommandProcessed(command: PHD2Command, result: T?, error: String?) } diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/PHD2ProtocolDecoder.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/PHD2ProtocolDecoder.kt new file mode 100644 index 000000000..d2c0d02c5 --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/PHD2ProtocolDecoder.kt @@ -0,0 +1,118 @@ +package nebulosa.phd2.client + +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.ObjectMapper +import io.netty.buffer.ByteBuf +import io.netty.channel.ChannelHandlerContext +import io.netty.handler.codec.ByteToMessageDecoder +import nebulosa.log.debug +import nebulosa.log.loggerFor +import nebulosa.phd2.client.commands.CompletableCommand +import nebulosa.phd2.client.commands.PHD2CommandFailedException +import nebulosa.phd2.client.events.* +import java.io.ByteArrayOutputStream + +class PHD2ProtocolDecoder( + private val client: PHD2Client, + private val mapper: ObjectMapper, +) : ByteToMessageDecoder() { + + private val data = object : ByteArrayOutputStream(32 * 1024) { + + fun readTree() = mapper.readTree(buf, 0, count) + } + + override fun decode(ctx: ChannelHandlerContext, buf: ByteBuf, out: MutableList) { + for (i in 0 until buf.readableBytes()) { + val b = buf.readByte() + + data.write(b.toInt()) + + if (b == 0x0A.toByte()) { + try { + val eventTree = data.readTree() + + if (eventTree.has("jsonrpc")) { + processJsonRPC(eventTree) + } else { + processEvent(eventTree, out) + } + } catch (e: Throwable) { + LOG.error("failed to process PHD2 message", e) + } finally { + data.reset() + } + + return + } + } + } + + @Suppress("UNCHECKED_CAST") + private fun processJsonRPC(node: JsonNode) { + LOG.debug { "$node" } + + val id = node.get("id").textValue() + val command = client.commands.remove(id) as? CompletableCommand + + if (command == null) { + LOG.error("command not found. id={}", id) + } else if (node.has("error")) { + val message = node.get("error").get("message").textValue() + client.listeners.forEach { it.onCommandProcessed(command.command, null, message) } + command.task.completeExceptionally(PHD2CommandFailedException(command.methodName, message)) + } else if (command.responseType != null) { + val result = mapper.treeToValue(node.get("result"), command.responseType) + client.listeners.forEach { it.onCommandProcessed(command.command, result, null) } + command.task.complete(result) + } else { + client.listeners.forEach { it.onCommandProcessed(command.command, null, null) } + command.task.complete(null) + } + } + + private fun processEvent(node: JsonNode, out: MutableList): Boolean { + val eventName = node.get("Event").textValue() + val (type, value) = EVENT_TYPES[eventName] ?: return false + + return if (value != null) { + out.add(value) + } else { + out.add(mapper.treeToValue(node, type)) + } + } + + companion object { + + @JvmStatic private val LOG = loggerFor() + + @JvmStatic private val EVENT_TYPES = mapOf( + "Alert" to (AlertEvent::class.java to null), + "AppState" to (AppStateEvent::class.java to null), + "Calibrating" to (CalibratingEvent::class.java to null), + "CalibrationComplete" to (CalibrationCompleteEvent::class.java to null), + "CalibrationDataFlipped" to (CalibrationDataFlippedEvent::class.java to null), + "CalibrationFailed" to (CalibrationFailedEvent::class.java to null), + "ConfigurationChange" to (ConfigurationChangeEvent::class.java to ConfigurationChangeEvent), + "GuideParamChange" to (GuideParamChangeEvent::class.java to null), + "GuideStep" to (GuideStepEvent::class.java to null), + "GuidingDithered" to (GuidingDitheredEvent::class.java to null), + "GuidingStopped" to (GuidingStoppedEvent::class.java to GuidingStoppedEvent), + "LockPositionLost" to (LockPositionLostEvent::class.java to LockPositionLostEvent), + "LockPositionSet" to (LockPositionSetEvent::class.java to null), + "LockPositionShiftLimitReached" to (LockPositionShiftLimitReachedEvent::class.java to LockPositionShiftLimitReachedEvent), + "LoopingExposures" to (LoopingExposuresEvent::class.java to null), + "LoopingExposuresStopped" to (LoopingExposuresStoppedEvent::class.java to LoopingExposuresStoppedEvent), + "Paused" to (PausedEvent::class.java to PausedEvent), + "Resumed" to (ResumedEvent::class.java to ResumedEvent), + "SettleBegin" to (SettleBeginEvent::class.java to SettleBeginEvent), + "SettleDone" to (SettleDoneEvent::class.java to null), + "Settling" to (SettlingEvent::class.java to null), + "StarLost" to (StarLostEvent::class.java to null), + "StarSelected" to (StarSelectedEvent::class.java to null), + "StartCalibration" to (StartCalibrationEvent::class.java to null), + "StartGuiding" to (StartGuidingEvent::class.java to StartGuidingEvent), + "Version" to (VersionEvent::class.java to null), + ) + } +} diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/PHD2ProtocolEncoder.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/PHD2ProtocolEncoder.kt new file mode 100644 index 000000000..63e09efc2 --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/PHD2ProtocolEncoder.kt @@ -0,0 +1,29 @@ +package nebulosa.phd2.client + +import com.fasterxml.jackson.databind.ObjectMapper +import io.netty.buffer.ByteBuf +import io.netty.channel.ChannelHandlerContext +import io.netty.handler.codec.MessageToByteEncoder +import nebulosa.log.debug +import nebulosa.log.loggerFor +import nebulosa.phd2.client.commands.CompletableCommand +import nebulosa.phd2.client.commands.PHD2Command +import java.util.* + +class PHD2ProtocolEncoder(private val mapper: ObjectMapper) : MessageToByteEncoder>() { + + override fun encode(ctx: ChannelHandlerContext, msg: PHD2Command<*>, out: ByteBuf) { + val id = if (msg is CompletableCommand) msg.id else UUID.randomUUID().toString() + val data = mapOf("method" to msg.methodName, "params" to msg.params, "id" to id) + val bytes = mapper.writeValueAsBytes(data) + LOG.debug { bytes.decodeToString() } + out.writeBytes(bytes) + out.writeByte(13) + out.writeByte(10) + } + + companion object { + + @JvmStatic private val LOG = loggerFor() + } +} diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/PHD2ProtocolHandler.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/PHD2ProtocolHandler.kt new file mode 100644 index 000000000..3b415bfca --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/PHD2ProtocolHandler.kt @@ -0,0 +1,19 @@ +package nebulosa.phd2.client + +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.ChannelInboundHandlerAdapter +import nebulosa.phd2.client.events.PHD2Event + +class PHD2ProtocolHandler(private val client: PHD2Client) : ChannelInboundHandlerAdapter() { + + override fun channelRead(ctx: ChannelHandlerContext, msg: Any) { + val event = msg as PHD2Event + client.listeners.forEach { it.onEventReceived(event) } + } + + @Suppress("OVERRIDE_DEPRECATION") + override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable?) { + cause?.printStackTrace() + ctx.close() + } +} diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/CalibrationData.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/CalibrationData.kt new file mode 100644 index 000000000..d30efa26c --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/CalibrationData.kt @@ -0,0 +1,13 @@ +package nebulosa.phd2.client.commands + +import com.fasterxml.jackson.annotation.JsonProperty + +data class CalibrationData( + @field:JsonProperty("calibrated") val calibrated: Boolean = false, + @field:JsonProperty("xAngle") val xAngle: Double = 0.0, + @field:JsonProperty("xRate") val xRate: Double = 0.0, + @field:JsonProperty("xParity") val xParity: String = "+", + @field:JsonProperty("yAngle") val yAngle: Double = 0.0, + @field:JsonProperty("yRate") val yRate: Double = 0.0, + @field:JsonProperty("yParity") val yParity: String = "+", +) diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/CaptureSingleFrame.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/CaptureSingleFrame.kt new file mode 100644 index 000000000..7c2ff4970 --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/CaptureSingleFrame.kt @@ -0,0 +1,22 @@ +package nebulosa.phd2.client.commands + +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds + +data class CaptureSingleFrame( + val exposure: Duration = 1.seconds, + val x: Int = 0, + val y: Int = 0, + val width: Int = 0, + val height: Int = 0, +) : PHD2Command { + + override val methodName = "capture_single_frame" + + override val params = mapOf( + "exposure" to exposure.inWholeMilliseconds, + "subframe" to if (width > 0 && height > 0) listOf(x, y, width, height) else null, + ) + + override val responseType = Int::class.java +} diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/ClearCalibration.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/ClearCalibration.kt new file mode 100644 index 000000000..de3f5653f --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/ClearCalibration.kt @@ -0,0 +1,10 @@ +package nebulosa.phd2.client.commands + +data class ClearCalibration(val which: WhichMount = WhichMount.MOUNT) : PHD2Command { + + override val methodName = "clear_calibration" + + override val params = listOf(which) + + override val responseType = Int::class.java +} diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/CompletableCommand.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/CompletableCommand.kt new file mode 100644 index 000000000..20d388ddc --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/CompletableCommand.kt @@ -0,0 +1,10 @@ +package nebulosa.phd2.client.commands + +import java.util.concurrent.CompletableFuture +import java.util.concurrent.Future + +internal data class CompletableCommand( + @JvmField internal val command: PHD2Command, + @JvmField internal val task: CompletableFuture, + @JvmField internal val id: String, +) : PHD2Command by command, Future by task diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/DeclinationGuideMode.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/DeclinationGuideMode.kt new file mode 100644 index 000000000..2c26e3b35 --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/DeclinationGuideMode.kt @@ -0,0 +1,8 @@ +package nebulosa.phd2.client.commands + +enum class DeclinationGuideMode { + OFF, + AUTO, + NORTH, + SOUTH, +} diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/DeselectStar.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/DeselectStar.kt new file mode 100644 index 000000000..ce42eeb8a --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/DeselectStar.kt @@ -0,0 +1,10 @@ +package nebulosa.phd2.client.commands + +data object DeselectStar : PHD2Command { + + override val methodName = "deselect_star" + + override val params = null + + override val responseType = Int::class.java +} diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/Dither.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/Dither.kt new file mode 100644 index 000000000..b712c004f --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/Dither.kt @@ -0,0 +1,25 @@ +package nebulosa.phd2.client.commands + +import nebulosa.guiding.Guider +import kotlin.time.Duration + +data class Dither( + val amount: Double, + val raOnly: Boolean = false, + val settleAmount: Double = Guider.DEFAULT_SETTLE_AMOUNT, + val settleTime: Duration = Guider.DEFAULT_SETTLE_TIME, + val settleTimeout: Duration = Guider.DEFAULT_SETTLE_TIMEOUT, +) : PHD2Command { + + override val methodName = "dither" + + override val params = mapOf( + "amount" to amount, "raOnly" to raOnly, + "settle" to mapOf( + "pixels" to settleAmount, "time" to settleTime.inWholeSeconds, + "timeout" to settleTimeout.inWholeSeconds, + ) + ) + + override val responseType = Int::class.java +} diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/Equipment.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/Equipment.kt new file mode 100644 index 000000000..bb25fc0b4 --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/Equipment.kt @@ -0,0 +1,11 @@ +package nebulosa.phd2.client.commands + +import com.fasterxml.jackson.annotation.JsonProperty + +data class Equipment( + @field:JsonProperty("camera") val camera: EquipmentDevice? = null, + @field:JsonProperty("mount") val mount: EquipmentDevice? = null, + @field:JsonProperty("aux_mount") val auxMount: EquipmentDevice? = null, + @field:JsonProperty("AO") val ao: EquipmentDevice? = null, + @field:JsonProperty("rotator") val rotator: EquipmentDevice? = null, +) diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/EquipmentDevice.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/EquipmentDevice.kt new file mode 100644 index 000000000..1ed965e31 --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/EquipmentDevice.kt @@ -0,0 +1,3 @@ +package nebulosa.phd2.client.commands + +data class EquipmentDevice(val name: String = "", val connected: Boolean = false) diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/FindStar.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/FindStar.kt new file mode 100644 index 000000000..5762c5bbf --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/FindStar.kt @@ -0,0 +1,15 @@ +package nebulosa.phd2.client.commands + +data class FindStar( + val x: Int = 0, + val y: Int = 0, + val width: Int = 0, + val height: Int = 0, +) : PHD2Command { + + override val methodName = "find_star" + + override val params = mapOf("roi" to if (width > 0 && height > 0) listOf(x, y, width, height) else null) + + override val responseType = IntArray::class.java +} diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/FlipCalibration.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/FlipCalibration.kt new file mode 100644 index 000000000..59b09bf3f --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/FlipCalibration.kt @@ -0,0 +1,10 @@ +package nebulosa.phd2.client.commands + +data object FlipCalibration : PHD2Command { + + override val methodName = "flip_calibration" + + override val params = null + + override val responseType = Int::class.java +} diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetAlgorithmParam.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetAlgorithmParam.kt new file mode 100644 index 000000000..90c6515dc --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetAlgorithmParam.kt @@ -0,0 +1,10 @@ +package nebulosa.phd2.client.commands + +data class GetAlgorithmParam(val axis: GuideAxis, val name: String) : PHD2Command { + + override val methodName = "get_algo_param" + + override val params = mapOf("axis" to axis, "name" to name) + + override val responseType = Any::class.java +} diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetAlgorithmParamNames.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetAlgorithmParamNames.kt new file mode 100644 index 000000000..b292dd486 --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetAlgorithmParamNames.kt @@ -0,0 +1,10 @@ +package nebulosa.phd2.client.commands + +data class GetAlgorithmParamNames(val axis: GuideAxis) : PHD2Command> { + + override val methodName = "get_algo_param_names" + + override val params = mapOf("axis" to axis) + + override val responseType = Array::class.java +} diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetAppState.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetAppState.kt new file mode 100644 index 000000000..3a74d8cef --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetAppState.kt @@ -0,0 +1,12 @@ +package nebulosa.phd2.client.commands + +import nebulosa.guiding.GuideState + +data object GetAppState : PHD2Command { + + override val methodName = "get_app_state" + + override val params = null + + override val responseType = GuideState::class.java +} diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetCalibrated.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetCalibrated.kt new file mode 100644 index 000000000..96b7224da --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetCalibrated.kt @@ -0,0 +1,10 @@ +package nebulosa.phd2.client.commands + +data object GetCalibrated : PHD2Command { + + override val methodName = "get_calibrated" + + override val params = null + + override val responseType = Boolean::class.java +} diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetCalibrationData.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetCalibrationData.kt new file mode 100644 index 000000000..c37c55e7d --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetCalibrationData.kt @@ -0,0 +1,10 @@ +package nebulosa.phd2.client.commands + +data class GetCalibrationData(val which: WhichMount = WhichMount.MOUNT) : PHD2Command { + + override val methodName = "get_calibration_data" + + override val params = listOf(which) + + override val responseType = CalibrationData::class.java +} diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetCameraBinning.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetCameraBinning.kt new file mode 100644 index 000000000..13d60635f --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetCameraBinning.kt @@ -0,0 +1,10 @@ +package nebulosa.phd2.client.commands + +data object GetCameraBinning : PHD2Command { + + override val methodName = "get_camera_binning" + + override val params = null + + override val responseType = Int::class.java +} diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetCameraFrameSize.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetCameraFrameSize.kt new file mode 100644 index 000000000..5d4b79ba9 --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetCameraFrameSize.kt @@ -0,0 +1,10 @@ +package nebulosa.phd2.client.commands + +data object GetCameraFrameSize : PHD2Command { + + override val methodName = "get_camera_frame_size" + + override val params = null + + override val responseType = IntArray::class.java +} diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetConnected.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetConnected.kt new file mode 100644 index 000000000..3d6e01293 --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetConnected.kt @@ -0,0 +1,10 @@ +package nebulosa.phd2.client.commands + +data object GetConnected : PHD2Command { + + override val methodName = "get_connected" + + override val params = null + + override val responseType = Boolean::class.java +} diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetCurrentEquipment.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetCurrentEquipment.kt new file mode 100644 index 000000000..8b21dacb4 --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetCurrentEquipment.kt @@ -0,0 +1,10 @@ +package nebulosa.phd2.client.commands + +data object GetCurrentEquipment : PHD2Command { + + override val methodName = "get_current_equipment" + + override val params = null + + override val responseType = Equipment::class.java +} diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetDeclinationGuideMode.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetDeclinationGuideMode.kt new file mode 100644 index 000000000..fe3a316c4 --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetDeclinationGuideMode.kt @@ -0,0 +1,10 @@ +package nebulosa.phd2.client.commands + +data object GetDeclinationGuideMode : PHD2Command { + + override val methodName = "get_dec_guide_mode" + + override val params = null + + override val responseType = DeclinationGuideMode::class.java +} diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetExposure.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetExposure.kt new file mode 100644 index 000000000..264fcb1fe --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetExposure.kt @@ -0,0 +1,10 @@ +package nebulosa.phd2.client.commands + +data object GetExposure : PHD2Command { + + override val methodName = "get_exposure" + + override val params = null + + override val responseType = Long::class.java +} diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetExposureDurations.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetExposureDurations.kt new file mode 100644 index 000000000..9ef69f36d --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetExposureDurations.kt @@ -0,0 +1,10 @@ +package nebulosa.phd2.client.commands + +data object GetExposureDurations : PHD2Command { + + override val methodName = "get_exposure_durations" + + override val params = null + + override val responseType = LongArray::class.java +} diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetGuideOutputEnabled.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetGuideOutputEnabled.kt new file mode 100644 index 000000000..7b45b464d --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetGuideOutputEnabled.kt @@ -0,0 +1,10 @@ +package nebulosa.phd2.client.commands + +data object GetGuideOutputEnabled : PHD2Command { + + override val methodName = "get_guide_output_enabled" + + override val params = null + + override val responseType = Boolean::class.java +} diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetLockPosition.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetLockPosition.kt new file mode 100644 index 000000000..11c6d4116 --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetLockPosition.kt @@ -0,0 +1,10 @@ +package nebulosa.phd2.client.commands + +data object GetLockPosition : PHD2Command { + + override val methodName = "get_lock_position" + + override val params = null + + override val responseType = IntArray::class.java +} diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetLockShiftEnabled.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetLockShiftEnabled.kt new file mode 100644 index 000000000..4699cae51 --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetLockShiftEnabled.kt @@ -0,0 +1,10 @@ +package nebulosa.phd2.client.commands + +data object GetLockShiftEnabled : PHD2Command { + + override val methodName = "get_lock_shift_enabled" + + override val params = null + + override val responseType = Boolean::class.java +} diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetLockShiftParams.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetLockShiftParams.kt new file mode 100644 index 000000000..36fea3f76 --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetLockShiftParams.kt @@ -0,0 +1,10 @@ +package nebulosa.phd2.client.commands + +data object GetLockShiftParams : PHD2Command { + + override val methodName = "get_lock_shift_params" + + override val params = null + + override val responseType = LockShiftParams::class.java +} diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetPaused.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetPaused.kt new file mode 100644 index 000000000..9b9d7b631 --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetPaused.kt @@ -0,0 +1,10 @@ +package nebulosa.phd2.client.commands + +data object GetPaused : PHD2Command { + + override val methodName = "get_paused" + + override val params = null + + override val responseType = Boolean::class.java +} diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetPixelScale.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetPixelScale.kt new file mode 100644 index 000000000..5c6d75d4d --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetPixelScale.kt @@ -0,0 +1,10 @@ +package nebulosa.phd2.client.commands + +data object GetPixelScale : PHD2Command { + + override val methodName = "get_pixel_scale" + + override val params = null + + override val responseType = Double::class.java +} diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetProfile.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetProfile.kt new file mode 100644 index 000000000..79b782200 --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetProfile.kt @@ -0,0 +1,10 @@ +package nebulosa.phd2.client.commands + +data object GetProfile : PHD2Command { + + override val methodName = "get_profile" + + override val params = null + + override val responseType = Profile::class.java +} diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetProfiles.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetProfiles.kt new file mode 100644 index 000000000..73536a011 --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetProfiles.kt @@ -0,0 +1,10 @@ +package nebulosa.phd2.client.commands + +data object GetProfiles : PHD2Command> { + + override val methodName = "get_profiles" + + override val params = null + + override val responseType = Array::class.java +} diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetSearchRegion.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetSearchRegion.kt new file mode 100644 index 000000000..abc5e0180 --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetSearchRegion.kt @@ -0,0 +1,10 @@ +package nebulosa.phd2.client.commands + +data object GetSearchRegion : PHD2Command { + + override val methodName = "get_search_region" + + override val params = null + + override val responseType = Int::class.java +} diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetSettling.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetSettling.kt new file mode 100644 index 000000000..f4438ca20 --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetSettling.kt @@ -0,0 +1,10 @@ +package nebulosa.phd2.client.commands + +data object GetSettling : PHD2Command { + + override val methodName = "get_settling" + + override val params = null + + override val responseType = Boolean::class.java +} diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetStarImage.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetStarImage.kt new file mode 100644 index 000000000..3bab81c4b --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetStarImage.kt @@ -0,0 +1,10 @@ +package nebulosa.phd2.client.commands + +data class GetStarImage(val size: Int = 15) : PHD2Command { + + override val methodName = "get_star_image" + + override val params = listOf(size) + + override val responseType = StarImage::class.java +} diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetUseSubframes.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetUseSubframes.kt new file mode 100644 index 000000000..60ffd7d54 --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GetUseSubframes.kt @@ -0,0 +1,10 @@ +package nebulosa.phd2.client.commands + +data object GetUseSubframes : PHD2Command { + + override val methodName = "get_use_subframes" + + override val params = null + + override val responseType = Boolean::class.java +} diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/Guide.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/Guide.kt new file mode 100644 index 000000000..fa654667e --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/Guide.kt @@ -0,0 +1,29 @@ +package nebulosa.phd2.client.commands + +import nebulosa.guiding.Guider +import kotlin.time.Duration + +data class Guide( + val settleAmount: Double = Guider.DEFAULT_SETTLE_AMOUNT, + val settleTime: Duration = Guider.DEFAULT_SETTLE_TIME, + val settleTimeout: Duration = Guider.DEFAULT_SETTLE_TIMEOUT, + val recalibrate: Boolean = false, + val x: Int = 0, + val y: Int = 0, + val width: Int = 0, + val height: Int = 0, +) : PHD2Command { + + override val methodName = "guide" + + override val params = mapOf( + "recalibrate" to recalibrate, + "roi" to if (width > 0 && height > 0) listOf(x, y, width, height) else null, + "settle" to mapOf( + "pixels" to settleAmount, "time" to settleTime.inWholeSeconds, + "timeout" to settleTimeout.inWholeSeconds, + ) + ) + + override val responseType = Int::class.java +} diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GuideAxis.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GuideAxis.kt new file mode 100644 index 000000000..eb56ae78f --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GuideAxis.kt @@ -0,0 +1,6 @@ +package nebulosa.phd2.client.commands + +enum class GuideAxis { + RA, + DEC, +} diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GuidePulse.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GuidePulse.kt new file mode 100644 index 000000000..55d9a7bd2 --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/GuidePulse.kt @@ -0,0 +1,16 @@ +package nebulosa.phd2.client.commands + +import nebulosa.guiding.GuideDirection + +data class GuidePulse( + val amount: Int, // ms or step count + val direction: GuideDirection, + val which: WhichMount = WhichMount.MOUNT, +) : PHD2Command { + + override val methodName = "guide_pulse" + + override val params = listOf(amount, direction, which) + + override val responseType = Int::class.java +} diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/LockShiftParams.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/LockShiftParams.kt new file mode 100644 index 000000000..3e056a959 --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/LockShiftParams.kt @@ -0,0 +1,11 @@ +package nebulosa.phd2.client.commands + +import com.fasterxml.jackson.annotation.JsonProperty + +@Suppress("ArrayInDataClass") +data class LockShiftParams( + @field:JsonProperty("enabled") val enabled: Boolean = false, + @field:JsonProperty("rate") val rate: IntArray = IntArray(2), + @field:JsonProperty("units") val units: RateUnit = RateUnit.ARCSEC_HOUR, + @field:JsonProperty("axes") val axes: ShiftAxesType = ShiftAxesType.RADEC, +) diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/Loop.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/Loop.kt new file mode 100644 index 000000000..bd4aa74fc --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/Loop.kt @@ -0,0 +1,13 @@ +package nebulosa.phd2.client.commands + +/** + * Starts capturing, or, if guiding, stops guiding but continue capturing. + */ +data object Loop : PHD2Command { + + override val methodName = "loop" + + override val params = null + + override val responseType = Int::class.java +} diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/PHD2Command.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/PHD2Command.kt new file mode 100644 index 000000000..21193a335 --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/PHD2Command.kt @@ -0,0 +1,13 @@ +package nebulosa.phd2.client.commands + +/** + * @see Reference + */ +sealed interface PHD2Command { + + val methodName: String + + val params: Any? + + val responseType: Class? +} diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/PHD2CommandFailedException.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/PHD2CommandFailedException.kt new file mode 100644 index 000000000..49662666f --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/PHD2CommandFailedException.kt @@ -0,0 +1,3 @@ +package nebulosa.phd2.client.commands + +class PHD2CommandFailedException(methodName: String, message: String) : Exception("[$methodName]: $message") diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/Profile.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/Profile.kt new file mode 100644 index 000000000..326fb5dbf --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/Profile.kt @@ -0,0 +1,3 @@ +package nebulosa.phd2.client.commands + +data class Profile(val id: Int = 0, val name: String = "") diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/RateUnit.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/RateUnit.kt new file mode 100644 index 000000000..7305aada4 --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/RateUnit.kt @@ -0,0 +1,8 @@ +package nebulosa.phd2.client.commands + +import com.fasterxml.jackson.annotation.JsonValue + +enum class RateUnit(@JsonValue val rate: String) { + ARCSEC_HOUR("arcsec/hr"), + PIXELS_HOUR("pixels/hr"), +} diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/SaveImage.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/SaveImage.kt new file mode 100644 index 000000000..4441a2914 --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/SaveImage.kt @@ -0,0 +1,10 @@ +package nebulosa.phd2.client.commands + +data object SaveImage : PHD2Command { + + override val methodName = "save_image" + + override val params = null + + override val responseType = SavedImage::class.java +} diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/SavedImage.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/SavedImage.kt new file mode 100644 index 000000000..14955ae61 --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/SavedImage.kt @@ -0,0 +1,6 @@ +package nebulosa.phd2.client.commands + +import com.fasterxml.jackson.annotation.JsonProperty +import java.nio.file.Path + +data class SavedImage(@field:JsonProperty("filename") val path: Path) diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/SetAlgorithmParam.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/SetAlgorithmParam.kt new file mode 100644 index 000000000..4d1764206 --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/SetAlgorithmParam.kt @@ -0,0 +1,13 @@ +package nebulosa.phd2.client.commands + +data class SetAlgorithmParam( + val axis: String, + val name: String, val value: Any, +) : PHD2Command { + + override val methodName = "set_algo_param" + + override val params = listOf(axis, name, value) + + override val responseType = Int::class.java +} diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/SetConnected.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/SetConnected.kt new file mode 100644 index 000000000..2cdbc6904 --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/SetConnected.kt @@ -0,0 +1,13 @@ +package nebulosa.phd2.client.commands + +/** + * Connects or disconnects all equipment. + */ +data class SetConnected(val connected: Boolean) : PHD2Command { + + override val methodName = "set_connected" + + override val params = listOf(connected) + + override val responseType = Int::class.java +} diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/SetDeclinationGuideMode.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/SetDeclinationGuideMode.kt new file mode 100644 index 000000000..2de32c6ef --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/SetDeclinationGuideMode.kt @@ -0,0 +1,10 @@ +package nebulosa.phd2.client.commands + +data class SetDeclinationGuideMode(val mode: DeclinationGuideMode) : PHD2Command { + + override val methodName = "set_dec_guide_mode" + + override val params = listOf(mode) + + override val responseType = Int::class.java +} diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/SetExposure.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/SetExposure.kt new file mode 100644 index 000000000..370dd5cdd --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/SetExposure.kt @@ -0,0 +1,12 @@ +package nebulosa.phd2.client.commands + +import kotlin.time.Duration + +data class SetExposure(val duration: Duration) : PHD2Command { + + override val methodName = "set_exposure" + + override val params = listOf(duration.inWholeMilliseconds) + + override val responseType = Int::class.java +} diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/SetGuideOutputEnabled.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/SetGuideOutputEnabled.kt new file mode 100644 index 000000000..445bd13dc --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/SetGuideOutputEnabled.kt @@ -0,0 +1,10 @@ +package nebulosa.phd2.client.commands + +data class SetGuideOutputEnabled(val enabled: Boolean) : PHD2Command { + + override val methodName = "set_guide_output_enabled" + + override val params = listOf(enabled) + + override val responseType = Int::class.java +} diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/SetLockPosition.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/SetLockPosition.kt new file mode 100644 index 000000000..df6b906f9 --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/SetLockPosition.kt @@ -0,0 +1,15 @@ +package nebulosa.phd2.client.commands + +/** + * When [exact] is true, the lock position is moved to the exact given coordinates ([x], [y]). + * When false, the current position is moved to the given coordinates and if + * a guide star is in range, the lock position is set to the coordinates of the guide star. + */ +data class SetLockPosition(val x: Double, val y: Double, val exact: Boolean = true) : PHD2Command { + + override val methodName = "set_lock_position" + + override val params = listOf(x, y, exact) + + override val responseType = Int::class.java +} diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/SetLockShiftEnabled.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/SetLockShiftEnabled.kt new file mode 100644 index 000000000..dcf542dff --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/SetLockShiftEnabled.kt @@ -0,0 +1,10 @@ +package nebulosa.phd2.client.commands + +data class SetLockShiftEnabled(val enabled: Boolean) : PHD2Command { + + override val methodName = "set_lock_shift_enabled" + + override val params = listOf(enabled) + + override val responseType = Int::class.java +} diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/SetLockShiftParams.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/SetLockShiftParams.kt new file mode 100644 index 000000000..53b1ec397 --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/SetLockShiftParams.kt @@ -0,0 +1,14 @@ +package nebulosa.phd2.client.commands + +data class SetLockShiftParams( + val xRate: Double, val yRate: Double, + val shiftAxes: ShiftAxesType, + val shiftUnit: RateUnit = shiftAxes.rateUnit, +) : PHD2Command { + + override val methodName = "set_lock_shift_params" + + override val params = mapOf("rate" to doubleArrayOf(xRate, yRate), "units" to shiftUnit, "axes" to shiftAxes) + + override val responseType = Int::class.java +} diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/SetPaused.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/SetPaused.kt new file mode 100644 index 000000000..8e9b36a2d --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/SetPaused.kt @@ -0,0 +1,15 @@ +package nebulosa.phd2.client.commands + +/** + * When setting paused to true, an optional second parameter with value "FULL" can be provided + * to fully pause PHD2, including pausing looping exposures. + * Otherwise, exposures continue to loop, and only guide output is paused. + */ +data class SetPaused(val paused: Boolean, val full: Boolean = false) : PHD2Command { + + override val methodName = "set_paused" + + override val params = listOf(paused, if (full) "full" else null) + + override val responseType = Int::class.java +} diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/SetProfile.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/SetProfile.kt new file mode 100644 index 000000000..fccee1687 --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/SetProfile.kt @@ -0,0 +1,13 @@ +package nebulosa.phd2.client.commands + +/** + * Select an equipment profile. All equipment must be disconnected before switching profiles. + */ +data class SetProfile(val profile: Profile) : PHD2Command { + + override val methodName = "set_profile" + + override val params = listOf(profile.id) + + override val responseType = Int::class.java +} diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/ShiftAxesType.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/ShiftAxesType.kt new file mode 100644 index 000000000..24974c129 --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/ShiftAxesType.kt @@ -0,0 +1,11 @@ +package nebulosa.phd2.client.commands + +import com.fasterxml.jackson.annotation.JsonValue + +enum class ShiftAxesType( + @JsonValue val axes: String, + val rateUnit: RateUnit, +) { + RADEC("RA/Dec", RateUnit.ARCSEC_HOUR), + XY("X/Y", RateUnit.PIXELS_HOUR), +} diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/Shutdown.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/Shutdown.kt new file mode 100644 index 000000000..8c241859d --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/Shutdown.kt @@ -0,0 +1,13 @@ +package nebulosa.phd2.client.commands + +/** + * Closes PHD2. + */ +data object Shutdown : PHD2Command { + + override val methodName = "shutdown" + + override val params = null + + override val responseType = Int::class.java +} diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/StarImage.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/StarImage.kt new file mode 100644 index 000000000..bdde2a6df --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/StarImage.kt @@ -0,0 +1,25 @@ +package nebulosa.phd2.client.commands + +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.databind.annotation.JsonDeserialize +import nebulosa.phd2.client.events.StarPosition +import java.awt.image.BufferedImage +import java.awt.image.DataBufferByte +import kotlin.io.encoding.Base64 + +data class StarImage( + val frame: Int = 0, + val width: Int = 0, val height: Int = 0, + @field:JsonDeserialize(using = StarPosition.Deserializer::class) + @field:JsonProperty("star_pos") val starPosition: StarPosition = StarPosition.ZERO, + val pixels: String = "", +) { + + fun decodeImage(): BufferedImage { + val pixels = Base64.decode(pixels) + val image = BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY) + val bytes = (image.raster.dataBuffer as DataBufferByte).data + for (i in pixels.indices step 2) bytes[i / 2] = pixels[i] + return image + } +} diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/StopCapture.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/StopCapture.kt new file mode 100644 index 000000000..a2de6e3da --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/StopCapture.kt @@ -0,0 +1,13 @@ +package nebulosa.phd2.client.commands + +/** + * Stop capturing (and stop guiding). + */ +data object StopCapture : PHD2Command { + + override val methodName = "stop_capture" + + override val params = null + + override val responseType = Int::class.java +} diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/WhichMount.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/WhichMount.kt new file mode 100644 index 000000000..d9a27df61 --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/commands/WhichMount.kt @@ -0,0 +1,7 @@ +package nebulosa.phd2.client.commands + +enum class WhichMount { + MOUNT, + AO, + BOTH, +} diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/Alert.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/Alert.kt deleted file mode 100644 index 9a6ffe512..000000000 --- a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/Alert.kt +++ /dev/null @@ -1,8 +0,0 @@ -package nebulosa.phd2.client.event - -import com.fasterxml.jackson.annotation.JsonProperty - -data class Alert( - @field:JsonProperty("Msg") var message: String = "", - @field:JsonProperty("Type") var type: String = "", -) : PHD2Event diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/AppState.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/AppState.kt deleted file mode 100644 index fe2363639..000000000 --- a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/AppState.kt +++ /dev/null @@ -1,7 +0,0 @@ -package nebulosa.phd2.client.event - -import com.fasterxml.jackson.annotation.JsonProperty - -data class AppState( - @field:JsonProperty("State") var state: String = "", -) : PHD2Event diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/Calibrating.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/Calibrating.kt deleted file mode 100644 index 08056c787..000000000 --- a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/Calibrating.kt +++ /dev/null @@ -1,16 +0,0 @@ -package nebulosa.phd2.client.event - -import com.fasterxml.jackson.annotation.JsonProperty -import com.fasterxml.jackson.databind.annotation.JsonDeserialize - -data class Calibrating( - @field:JsonProperty("Mount") var mount: String = "", - @field:JsonProperty("dir") var direction: String = "", - @field:JsonProperty("dist") var distance: Double = 0.0, - @field:JsonProperty("dx") var dx: Double = 0.0, - @field:JsonProperty("dy") var dy: Double = 0.0, - @field:JsonDeserialize(using = StarCoordinate.Deserializer::class) - @field:JsonProperty("pos") var position: StarCoordinate = StarCoordinate.EMPTY, - @field:JsonProperty("step") var step: Int = 0, - @field:JsonProperty("State") var state: String = "", -) : PHD2Event diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/CalibrationComplete.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/CalibrationComplete.kt deleted file mode 100644 index 74bfe7c1b..000000000 --- a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/CalibrationComplete.kt +++ /dev/null @@ -1,7 +0,0 @@ -package nebulosa.phd2.client.event - -import com.fasterxml.jackson.annotation.JsonProperty - -data class CalibrationComplete( - @field:JsonProperty("Mount") var mount: String = "", -) : PHD2Event diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/CalibrationDataFlipped.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/CalibrationDataFlipped.kt deleted file mode 100644 index 117cad524..000000000 --- a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/CalibrationDataFlipped.kt +++ /dev/null @@ -1,7 +0,0 @@ -package nebulosa.phd2.client.event - -import com.fasterxml.jackson.annotation.JsonProperty - -data class CalibrationDataFlipped( - @field:JsonProperty("Mount") var mount: String = "", -) : PHD2Event diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/CalibrationFailed.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/CalibrationFailed.kt deleted file mode 100644 index e142b5f83..000000000 --- a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/CalibrationFailed.kt +++ /dev/null @@ -1,7 +0,0 @@ -package nebulosa.phd2.client.event - -import com.fasterxml.jackson.annotation.JsonProperty - -data class CalibrationFailed( - @field:JsonProperty("Reason") var reason: String = "", -) : PHD2Event diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/ConfigurationChange.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/ConfigurationChange.kt deleted file mode 100644 index 64260a098..000000000 --- a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/ConfigurationChange.kt +++ /dev/null @@ -1,3 +0,0 @@ -package nebulosa.phd2.client.event - -object ConfigurationChange : PHD2Event diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/GuideParamChange.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/GuideParamChange.kt deleted file mode 100644 index 4be68c72d..000000000 --- a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/GuideParamChange.kt +++ /dev/null @@ -1,8 +0,0 @@ -package nebulosa.phd2.client.event - -import com.fasterxml.jackson.annotation.JsonProperty - -data class GuideParamChange( - @field:JsonProperty("Name") var name: String = "", - @field:JsonProperty("Value") var value: Any? = null, -) : PHD2Event diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/GuideStep.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/GuideStep.kt deleted file mode 100644 index da718c750..000000000 --- a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/GuideStep.kt +++ /dev/null @@ -1,26 +0,0 @@ -package nebulosa.phd2.client.event - -import com.fasterxml.jackson.annotation.JsonProperty - -data class GuideStep( - @field:JsonProperty("Frame") var frame: Int = 0, - @field:JsonProperty("Time") var time: Double = 0.0, - @field:JsonProperty("Mount") var mount: String = "", - @field:JsonProperty("dx") var dx: Double = 0.0, - @field:JsonProperty("dy") var dy: Double = 0.0, - @field:JsonProperty("RADistanceRaw") var raDistanceRaw: Double = 0.0, - @field:JsonProperty("DECDistanceRaw") var decDistanceRaw: Double = 0.0, - @field:JsonProperty("RADistanceGuide") var raDistanceGuide: Double = 0.0, - @field:JsonProperty("DECDistanceGuide") var decDistanceGuide: Double = 0.0, - @field:JsonProperty("RADuration") var raDuration: Long = 0L, - @field:JsonProperty("RADirection") var raDirection: String = "", - @field:JsonProperty("DECDuration") var decDuration: Long = 0L, - @field:JsonProperty("DECDirection") var decDirection: String = "", - @field:JsonProperty("StarMass") var starMass: Double = 0.0, - @field:JsonProperty("SNR") var snr: Double = 0.0, - @field:JsonProperty("HFD") var hfd: Double = 0.0, - @field:JsonProperty("AvgDist") var averageDistance: Double = 0.0, - @field:JsonProperty("RALimited") var raLimited: Boolean = false, - @field:JsonProperty("DecLimited") var decLimited: Boolean = false, - @field:JsonProperty("ErrorCode") var errorCode: Int = 0, -) : PHD2Event diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/GuidingDithered.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/GuidingDithered.kt deleted file mode 100644 index 58b977260..000000000 --- a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/GuidingDithered.kt +++ /dev/null @@ -1,8 +0,0 @@ -package nebulosa.phd2.client.event - -import com.fasterxml.jackson.annotation.JsonProperty - -data class GuidingDithered( - @field:JsonProperty("dx") var dx: Double = 0.0, - @field:JsonProperty("dy") var dy: Double = 0.0, -) : PHD2Event diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/GuidingStopped.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/GuidingStopped.kt deleted file mode 100644 index c4d3d8005..000000000 --- a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/GuidingStopped.kt +++ /dev/null @@ -1,3 +0,0 @@ -package nebulosa.phd2.client.event - -object GuidingStopped : PHD2Event diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/LockPositionLost.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/LockPositionLost.kt deleted file mode 100644 index f52ad4736..000000000 --- a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/LockPositionLost.kt +++ /dev/null @@ -1,3 +0,0 @@ -package nebulosa.phd2.client.event - -object LockPositionLost : PHD2Event diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/LockPositionSet.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/LockPositionSet.kt deleted file mode 100644 index 5ca9c17c1..000000000 --- a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/LockPositionSet.kt +++ /dev/null @@ -1,8 +0,0 @@ -package nebulosa.phd2.client.event - -import com.fasterxml.jackson.annotation.JsonProperty - -data class LockPositionSet( - @field:JsonProperty("X") var x: Int = 0, - @field:JsonProperty("Y") var y: Int = 0, -) : PHD2Event diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/LockPositionShiftLimitReached.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/LockPositionShiftLimitReached.kt deleted file mode 100644 index eaf96dd7b..000000000 --- a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/LockPositionShiftLimitReached.kt +++ /dev/null @@ -1,3 +0,0 @@ -package nebulosa.phd2.client.event - -object LockPositionShiftLimitReached : PHD2Event diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/LoopingExposures.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/LoopingExposures.kt deleted file mode 100644 index 12621fdc2..000000000 --- a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/LoopingExposures.kt +++ /dev/null @@ -1,7 +0,0 @@ -package nebulosa.phd2.client.event - -import com.fasterxml.jackson.annotation.JsonProperty - -data class LoopingExposures( - @field:JsonProperty("Frame") var frame: Int = 0, -) : PHD2Event diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/LoopingExposuresStopped.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/LoopingExposuresStopped.kt deleted file mode 100644 index dde2ae394..000000000 --- a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/LoopingExposuresStopped.kt +++ /dev/null @@ -1,3 +0,0 @@ -package nebulosa.phd2.client.event - -object LoopingExposuresStopped : PHD2Event diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/Paused.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/Paused.kt deleted file mode 100644 index 2e8fff88b..000000000 --- a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/Paused.kt +++ /dev/null @@ -1,3 +0,0 @@ -package nebulosa.phd2.client.event - -object Paused : PHD2Event diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/Resumed.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/Resumed.kt deleted file mode 100644 index 4cffa03b7..000000000 --- a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/Resumed.kt +++ /dev/null @@ -1,3 +0,0 @@ -package nebulosa.phd2.client.event - -object Resumed : PHD2Event diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/SettleBegin.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/SettleBegin.kt deleted file mode 100644 index a491c81eb..000000000 --- a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/SettleBegin.kt +++ /dev/null @@ -1,3 +0,0 @@ -package nebulosa.phd2.client.event - -object SettleBegin : PHD2Event diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/SettleDone.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/SettleDone.kt deleted file mode 100644 index 8681710fe..000000000 --- a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/SettleDone.kt +++ /dev/null @@ -1,10 +0,0 @@ -package nebulosa.phd2.client.event - -import com.fasterxml.jackson.annotation.JsonProperty - -data class SettleDone( - @field:JsonProperty("Status") var status: Int = 0, - @field:JsonProperty("TotalFrames") var totalFrames: Int = 0, - @field:JsonProperty("DroppedFrames") var droppedFrames: Int = 0, - @field:JsonProperty("Error") var error: String = "", -) : PHD2Event diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/Settling.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/Settling.kt deleted file mode 100644 index 4fcf6ece0..000000000 --- a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/Settling.kt +++ /dev/null @@ -1,10 +0,0 @@ -package nebulosa.phd2.client.event - -import com.fasterxml.jackson.annotation.JsonProperty - -data class Settling( - @field:JsonProperty("Distance") var distance: Double = 0.0, - @field:JsonProperty("Time") var time: Double = 0.0, - @field:JsonProperty("SettleTime") var settleTime: Double = 0.0, - @field:JsonProperty("StarLocked") var starLocked: Boolean = false, -) : PHD2Event diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/StarCoordinate.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/StarCoordinate.kt deleted file mode 100644 index 2c18dbffb..000000000 --- a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/StarCoordinate.kt +++ /dev/null @@ -1,30 +0,0 @@ -package nebulosa.phd2.client.event - -import com.fasterxml.jackson.core.JsonParser -import com.fasterxml.jackson.databind.DeserializationContext -import com.fasterxml.jackson.databind.JsonNode -import com.fasterxml.jackson.databind.deser.std.StdDeserializer -import com.fasterxml.jackson.databind.node.ArrayNode - -data class StarCoordinate( - val x: Double, val y: Double, -) { - - class Deserializer : StdDeserializer(Any::class.java) { - - override fun deserialize( - p: JsonParser, - ctxt: DeserializationContext, - ): StarCoordinate { - val node = p.codec.readTree(p) - val pos = node.get("pos") as? ArrayNode - return if (pos == null) EMPTY - else StarCoordinate(pos[0].asDouble(), pos[1].asDouble()) - } - } - - companion object { - - @JvmStatic val EMPTY = StarCoordinate(Double.NaN, Double.NaN) - } -} diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/StarLost.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/StarLost.kt deleted file mode 100644 index c2a1060d1..000000000 --- a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/StarLost.kt +++ /dev/null @@ -1,13 +0,0 @@ -package nebulosa.phd2.client.event - -import com.fasterxml.jackson.annotation.JsonProperty - -data class StarLost( - @field:JsonProperty("Frame") var frame: Int = 0, - @field:JsonProperty("Time") var time: Double = 0.0, - @field:JsonProperty("StarMass") var starMass: Double = 0.0, - @field:JsonProperty("SNR") var snr: Double = 0.0, - @field:JsonProperty("AvgDist") var averageDistance: Double = 0.0, - @field:JsonProperty("ErrorCode") var errorCode: Int = 0, - @field:JsonProperty("Status") var status: String = "", -) : PHD2Event diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/StarSelected.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/StarSelected.kt deleted file mode 100644 index c6534569a..000000000 --- a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/StarSelected.kt +++ /dev/null @@ -1,8 +0,0 @@ -package nebulosa.phd2.client.event - -import com.fasterxml.jackson.annotation.JsonProperty - -data class StarSelected( - @field:JsonProperty("X") var x: Int = 0, - @field:JsonProperty("Y") var y: Int = 0, -) : PHD2Event diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/StartCalibration.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/StartCalibration.kt deleted file mode 100644 index f705967f5..000000000 --- a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/StartCalibration.kt +++ /dev/null @@ -1,7 +0,0 @@ -package nebulosa.phd2.client.event - -import com.fasterxml.jackson.annotation.JsonProperty - -data class StartCalibration( - @field:JsonProperty("Mount") var mount: String = "", -) : PHD2Event diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/StartGuiding.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/StartGuiding.kt deleted file mode 100644 index 2ca3f7e07..000000000 --- a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/StartGuiding.kt +++ /dev/null @@ -1,3 +0,0 @@ -package nebulosa.phd2.client.event - -object StartGuiding : PHD2Event diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/Version.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/Version.kt deleted file mode 100644 index 6c9a868f1..000000000 --- a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/Version.kt +++ /dev/null @@ -1,8 +0,0 @@ -package nebulosa.phd2.client.event - -import com.fasterxml.jackson.annotation.JsonProperty - -data class Version( - @field:JsonProperty("PHDVersion") var version: String = "", - @field:JsonProperty("PHDSubver") var subVersion: String = "", -) : PHD2Event diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/AlertEvent.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/AlertEvent.kt new file mode 100644 index 000000000..65e3c6793 --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/AlertEvent.kt @@ -0,0 +1,8 @@ +package nebulosa.phd2.client.events + +import com.fasterxml.jackson.annotation.JsonAlias + +data class AlertEvent( + @field:JsonAlias("Msg") val message: String = "", + @field:JsonAlias("Type") val type: AlertType = AlertType.INFO, +) : PHD2Event diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/AlertType.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/AlertType.kt new file mode 100644 index 000000000..037f4daa9 --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/AlertType.kt @@ -0,0 +1,8 @@ +package nebulosa.phd2.client.events + +enum class AlertType { + INFO, + QUESTION, + WARNING, + ERROR, +} diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/AppStateEvent.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/AppStateEvent.kt new file mode 100644 index 000000000..9307cb927 --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/AppStateEvent.kt @@ -0,0 +1,8 @@ +package nebulosa.phd2.client.events + +import com.fasterxml.jackson.annotation.JsonAlias +import nebulosa.guiding.GuideState + +data class AppStateEvent( + @field:JsonAlias("State") val state: GuideState = GuideState.STOPPED, +) : PHD2Event diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/CalibratingEvent.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/CalibratingEvent.kt new file mode 100644 index 000000000..be1868b8e --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/CalibratingEvent.kt @@ -0,0 +1,17 @@ +package nebulosa.phd2.client.events + +import com.fasterxml.jackson.annotation.JsonAlias +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.databind.annotation.JsonDeserialize + +data class CalibratingEvent( + @field:JsonAlias("Mount") val mount: String = "", + @field:JsonAlias("dir") val direction: String = "", + @field:JsonAlias("dist") val distance: Double = 0.0, + @field:JsonAlias("dx") val dx: Double = 0.0, + @field:JsonAlias("dy") val dy: Double = 0.0, + @field:JsonDeserialize(using = StarPosition.Deserializer::class) + @field:JsonAlias("pos") val position: StarPosition = StarPosition.ZERO, + @field:JsonProperty("step") val step: Int = 0, + @field:JsonAlias("State") val state: String = "", +) : PHD2Event diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/CalibrationCompleteEvent.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/CalibrationCompleteEvent.kt new file mode 100644 index 000000000..cb4ccd811 --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/CalibrationCompleteEvent.kt @@ -0,0 +1,7 @@ +package nebulosa.phd2.client.events + +import com.fasterxml.jackson.annotation.JsonAlias + +data class CalibrationCompleteEvent( + @field:JsonAlias("Mount") val mount: String = "", +) : PHD2Event diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/CalibrationDataFlippedEvent.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/CalibrationDataFlippedEvent.kt new file mode 100644 index 000000000..ed8555cb8 --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/CalibrationDataFlippedEvent.kt @@ -0,0 +1,7 @@ +package nebulosa.phd2.client.events + +import com.fasterxml.jackson.annotation.JsonAlias + +data class CalibrationDataFlippedEvent( + @field:JsonAlias("Mount") val mount: String = "", +) : PHD2Event diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/CalibrationFailedEvent.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/CalibrationFailedEvent.kt new file mode 100644 index 000000000..420384ef5 --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/CalibrationFailedEvent.kt @@ -0,0 +1,7 @@ +package nebulosa.phd2.client.events + +import com.fasterxml.jackson.annotation.JsonAlias + +data class CalibrationFailedEvent( + @field:JsonAlias("Reason") val reason: String = "", +) : PHD2Event diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/ConfigurationChangeEvent.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/ConfigurationChangeEvent.kt new file mode 100644 index 000000000..17bd698e9 --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/ConfigurationChangeEvent.kt @@ -0,0 +1,3 @@ +package nebulosa.phd2.client.events + +data object ConfigurationChangeEvent : PHD2Event diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/GuideParamChangeEvent.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/GuideParamChangeEvent.kt new file mode 100644 index 000000000..dd90ac897 --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/GuideParamChangeEvent.kt @@ -0,0 +1,8 @@ +package nebulosa.phd2.client.events + +import com.fasterxml.jackson.annotation.JsonAlias + +data class GuideParamChangeEvent( + @field:JsonAlias("Name") val name: String = "", + @field:JsonAlias("Value") val value: Any? = null, +) : PHD2Event diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/GuideStateConverter.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/GuideStateConverter.kt new file mode 100644 index 000000000..454fa8729 --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/GuideStateConverter.kt @@ -0,0 +1,35 @@ +package nebulosa.phd2.client.events + +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.SerializerProvider +import nebulosa.guiding.GuideState +import nebulosa.json.FromJson +import nebulosa.json.ToJson +import java.util.* + +object GuideStateConverter : ToJson, FromJson { + + override val type = GuideState::class.java + + override fun serialize(value: GuideState, gen: JsonGenerator, provider: SerializerProvider) { + gen.writeString(GUIDE_STATES[value]) + } + + override fun deserialize(p: JsonParser, ctxt: DeserializationContext): GuideState? { + return p.valueAsString?.let(GUIDE_STATE_NAMES::get) + } + + @JvmStatic private val GUIDE_STATES = EnumMap(GuideState::class.java).also { + it[GuideState.STOPPED] = "Stopped" + it[GuideState.SELECTED] = "Selected" + it[GuideState.CALIBRATING] = "Calibrating" + it[GuideState.GUIDING] = "Guiding" + it[GuideState.LOST_LOCK] = "LostLock" + it[GuideState.PAUSED] = "Paused" + it[GuideState.LOOPING] = "Looping" + } + + @JvmStatic private val GUIDE_STATE_NAMES = GUIDE_STATES.map { it.value to it.key }.toMap() +} diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/GuideStepEvent.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/GuideStepEvent.kt new file mode 100644 index 000000000..cfac9c82b --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/GuideStepEvent.kt @@ -0,0 +1,27 @@ +package nebulosa.phd2.client.events + +import com.fasterxml.jackson.annotation.JsonAlias +import com.fasterxml.jackson.annotation.JsonProperty +import nebulosa.guiding.GuideDirection +import nebulosa.guiding.GuideStep + +data class GuideStepEvent( + @field:JsonAlias("Frame") override val frame: Int = 0, + @field:JsonAlias("Time") val time: Double = 0.0, + @field:JsonAlias("Mount") val mount: String = "", + @field:JsonProperty("dx") override val dx: Double = 0.0, + @field:JsonProperty("dy") override val dy: Double = 0.0, + @field:JsonAlias("RADistanceRaw") override val raDistance: Double = 0.0, + @field:JsonAlias("DECDistanceRaw") override val decDistance: Double = 0.0, + @field:JsonAlias("RADuration") override val raDuration: Long = 0L, + @field:JsonAlias("RADirection") override val raDirection: GuideDirection = GuideDirection.WEST, + @field:JsonAlias("DECDuration") override val decDuration: Long = 0L, + @field:JsonAlias("DECDirection") override val decDirection: GuideDirection = GuideDirection.NORTH, + @field:JsonAlias("StarMass") override val starMass: Double = 0.0, + @field:JsonAlias("SNR") override val snr: Double = 0.0, + @field:JsonAlias("HFD") override val hfd: Double = 0.0, + @field:JsonAlias("AvgDist") override val averageDistance: Double = 0.0, + @field:JsonAlias("RALimited") val raLimited: Boolean = false, + @field:JsonAlias("DecLimited") val decLimited: Boolean = false, + @field:JsonAlias("ErrorCode") val errorCode: Int = 0, +) : PHD2Event, GuideStep diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/GuidingDitheredEvent.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/GuidingDitheredEvent.kt new file mode 100644 index 000000000..181899146 --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/GuidingDitheredEvent.kt @@ -0,0 +1,8 @@ +package nebulosa.phd2.client.events + +import com.fasterxml.jackson.annotation.JsonProperty + +data class GuidingDitheredEvent( + @field:JsonProperty("dx") val dx: Double = 0.0, + @field:JsonProperty("dy") val dy: Double = 0.0, +) : PHD2Event diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/GuidingStoppedEvent.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/GuidingStoppedEvent.kt new file mode 100644 index 000000000..8ede8630c --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/GuidingStoppedEvent.kt @@ -0,0 +1,3 @@ +package nebulosa.phd2.client.events + +data object GuidingStoppedEvent : PHD2Event diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/LockPositionLostEvent.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/LockPositionLostEvent.kt new file mode 100644 index 000000000..5d60140c5 --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/LockPositionLostEvent.kt @@ -0,0 +1,3 @@ +package nebulosa.phd2.client.events + +data object LockPositionLostEvent : PHD2Event diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/LockPositionSetEvent.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/LockPositionSetEvent.kt new file mode 100644 index 000000000..329d82156 --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/LockPositionSetEvent.kt @@ -0,0 +1,9 @@ +package nebulosa.phd2.client.events + +import com.fasterxml.jackson.annotation.JsonAlias +import nebulosa.guiding.GuidePoint + +data class LockPositionSetEvent( + @field:JsonAlias("X") override val x: Int = 0, + @field:JsonAlias("Y") override val y: Int = 0, +) : PHD2Event, GuidePoint diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/LockPositionShiftLimitReachedEvent.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/LockPositionShiftLimitReachedEvent.kt new file mode 100644 index 000000000..a63f8f004 --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/LockPositionShiftLimitReachedEvent.kt @@ -0,0 +1,3 @@ +package nebulosa.phd2.client.events + +data object LockPositionShiftLimitReachedEvent : PHD2Event diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/LoopingExposuresEvent.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/LoopingExposuresEvent.kt new file mode 100644 index 000000000..2e0c090e6 --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/LoopingExposuresEvent.kt @@ -0,0 +1,7 @@ +package nebulosa.phd2.client.events + +import com.fasterxml.jackson.annotation.JsonAlias + +data class LoopingExposuresEvent( + @field:JsonAlias("Frame") val frame: Int = 0, +) : PHD2Event diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/LoopingExposuresStoppedEvent.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/LoopingExposuresStoppedEvent.kt new file mode 100644 index 000000000..763dc4ddc --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/LoopingExposuresStoppedEvent.kt @@ -0,0 +1,3 @@ +package nebulosa.phd2.client.events + +data object LoopingExposuresStoppedEvent : PHD2Event diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/PHD2Event.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/PHD2Event.kt similarity index 78% rename from nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/PHD2Event.kt rename to nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/PHD2Event.kt index 94f7bb7a4..1fb98a5bd 100644 --- a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/event/PHD2Event.kt +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/PHD2Event.kt @@ -1,4 +1,4 @@ -package nebulosa.phd2.client.event +package nebulosa.phd2.client.events /** * @see Reference diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/PausedEvent.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/PausedEvent.kt new file mode 100644 index 000000000..6b82b8349 --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/PausedEvent.kt @@ -0,0 +1,3 @@ +package nebulosa.phd2.client.events + +data object PausedEvent : PHD2Event diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/ResumedEvent.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/ResumedEvent.kt new file mode 100644 index 000000000..51da0c870 --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/ResumedEvent.kt @@ -0,0 +1,3 @@ +package nebulosa.phd2.client.events + +data object ResumedEvent : PHD2Event diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/SettleBeginEvent.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/SettleBeginEvent.kt new file mode 100644 index 000000000..3cb6920a6 --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/SettleBeginEvent.kt @@ -0,0 +1,3 @@ +package nebulosa.phd2.client.events + +data object SettleBeginEvent : PHD2Event diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/SettleDoneEvent.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/SettleDoneEvent.kt new file mode 100644 index 000000000..a0eeb3531 --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/SettleDoneEvent.kt @@ -0,0 +1,10 @@ +package nebulosa.phd2.client.events + +import com.fasterxml.jackson.annotation.JsonAlias + +data class SettleDoneEvent( + @field:JsonAlias("Status") val status: Int = 0, + @field:JsonAlias("TotalFrames") val totalFrames: Int = 0, + @field:JsonAlias("DroppedFrames") val droppedFrames: Int = 0, + @field:JsonAlias("Error") val error: String = "", +) : PHD2Event diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/SettlingEvent.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/SettlingEvent.kt new file mode 100644 index 000000000..0e567eb72 --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/SettlingEvent.kt @@ -0,0 +1,10 @@ +package nebulosa.phd2.client.events + +import com.fasterxml.jackson.annotation.JsonAlias + +data class SettlingEvent( + @field:JsonAlias("Distance") val distance: Double = 0.0, + @field:JsonAlias("Time") val time: Double = 0.0, + @field:JsonAlias("SettleTime") val settleTime: Double = 0.0, + @field:JsonAlias("StarLocked") val starLocked: Boolean = false, +) : PHD2Event diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/StarLostEvent.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/StarLostEvent.kt new file mode 100644 index 000000000..5b20f0678 --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/StarLostEvent.kt @@ -0,0 +1,13 @@ +package nebulosa.phd2.client.events + +import com.fasterxml.jackson.annotation.JsonAlias + +data class StarLostEvent( + @field:JsonAlias("Frame") val frame: Int = 0, + @field:JsonAlias("Time") val time: Double = 0.0, + @field:JsonAlias("StarMass") val starMass: Double = 0.0, + @field:JsonAlias("SNR") val snr: Double = 0.0, + @field:JsonAlias("AvgDist") val averageDistance: Double = 0.0, + @field:JsonAlias("ErrorCode") val errorCode: Int = 0, + @field:JsonAlias("Status") val status: String = "", +) : PHD2Event diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/StarPosition.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/StarPosition.kt new file mode 100644 index 000000000..6da88915d --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/StarPosition.kt @@ -0,0 +1,35 @@ +package nebulosa.phd2.client.events + +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.deser.std.StdDeserializer +import com.fasterxml.jackson.databind.node.ArrayNode +import nebulosa.guiding.GuidePoint + +data class StarPosition(override val x: Int = 0, override val y: Int = 0) : GuidePoint { + + class Deserializer : StdDeserializer(Any::class.java) { + + override fun deserialize( + p: JsonParser, + ctxt: DeserializationContext, + ): StarPosition { + val node = p.codec.readTree(p) + + return if (node is ArrayNode) { + StarPosition(node[0].asInt(), node[1].asInt()) + } else if (node.has("pos")) { + val pos = node.get("pos") as ArrayNode + StarPosition(pos[0].asInt(), pos[1].asInt()) + } else { + ZERO + } + } + } + + companion object { + + @JvmStatic val ZERO = StarPosition() + } +} diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/StarSelectedEvent.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/StarSelectedEvent.kt new file mode 100644 index 000000000..52c619534 --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/StarSelectedEvent.kt @@ -0,0 +1,9 @@ +package nebulosa.phd2.client.events + +import com.fasterxml.jackson.annotation.JsonAlias +import nebulosa.guiding.GuidePoint + +data class StarSelectedEvent( + @field:JsonAlias("X") override val x: Int = 0, + @field:JsonAlias("Y") override val y: Int = 0, +) : PHD2Event, GuidePoint diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/StartCalibrationEvent.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/StartCalibrationEvent.kt new file mode 100644 index 000000000..3cc4275ef --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/StartCalibrationEvent.kt @@ -0,0 +1,7 @@ +package nebulosa.phd2.client.events + +import com.fasterxml.jackson.annotation.JsonAlias + +data class StartCalibrationEvent( + @field:JsonAlias("Mount") val mount: String = "", +) : PHD2Event diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/StartGuidingEvent.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/StartGuidingEvent.kt new file mode 100644 index 000000000..b9594befd --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/StartGuidingEvent.kt @@ -0,0 +1,3 @@ +package nebulosa.phd2.client.events + +data object StartGuidingEvent : PHD2Event diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/VersionEvent.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/VersionEvent.kt new file mode 100644 index 000000000..0617c1125 --- /dev/null +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/events/VersionEvent.kt @@ -0,0 +1,9 @@ +package nebulosa.phd2.client.events + +import com.fasterxml.jackson.annotation.JsonAlias + +data class VersionEvent( + @field:JsonAlias("PHDVersion") val version: String = "", + @field:JsonAlias("PHDSubver") val subVersion: String = "", + @field:JsonAlias("OverlapSupport") val overlapSupport: Boolean = false, +) : PHD2Event diff --git a/nebulosa-phd2-client/src/test/kotlin/PHD2ClientTest.kt b/nebulosa-phd2-client/src/test/kotlin/PHD2ClientTest.kt new file mode 100644 index 000000000..1095e557b --- /dev/null +++ b/nebulosa-phd2-client/src/test/kotlin/PHD2ClientTest.kt @@ -0,0 +1,43 @@ +import io.kotest.core.annotation.EnabledIf +import io.kotest.core.spec.style.StringSpec +import kotlinx.coroutines.delay +import nebulosa.phd2.client.PHD2Client +import nebulosa.phd2.client.PHD2EventListener +import nebulosa.phd2.client.commands.PHD2Command +import nebulosa.phd2.client.events.PHD2Event + +@EnabledIf(NonGitHubOnlyCondition::class) +class PHD2ClientTest : StringSpec(), PHD2EventListener { + + init { + "start" { + val client = PHD2Client() + client.registerListener(this@PHD2ClientTest) + client.open("localhost", PHD2Client.DEFAULT_PORT) + + delay(1000) + + client.close() + } + } + + override fun onEventReceived(event: PHD2Event) { + println(event) + } + + override fun onCommandProcessed(command: PHD2Command, result: T?, error: String?) { + // println("$command, $result, $error") + } + + private fun PHD2Client.sendCommandAndGetResult(command: PHD2Command): T? { + return try { + val result = sendCommandSync(command) + if (result is Array<*>) println(result.contentToString()) + else println(result) + result + } catch (e: Exception) { + e.printStackTrace() + null + } + } +} diff --git a/nebulosa-projection/build.gradle.kts b/nebulosa-projection/build.gradle.kts deleted file mode 100644 index 133ae63ca..000000000 --- a/nebulosa-projection/build.gradle.kts +++ /dev/null @@ -1,18 +0,0 @@ -plugins { - kotlin("jvm") - id("maven-publish") -} - -dependencies { - api(project(":nebulosa-math")) - implementation(project(":nebulosa-log")) - testImplementation(project(":nebulosa-test")) -} - -publishing { - publications { - create("pluginMaven") { - from(components["java"]) - } - } -} diff --git a/nebulosa-projection/src/main/kotlin/nebulosa/projection/Point.kt b/nebulosa-projection/src/main/kotlin/nebulosa/projection/Point.kt deleted file mode 100644 index 7e132c21f..000000000 --- a/nebulosa-projection/src/main/kotlin/nebulosa/projection/Point.kt +++ /dev/null @@ -1,3 +0,0 @@ -package nebulosa.projection - -data class Point(val x: Double, val y: Double) diff --git a/nebulosa-projection/src/main/kotlin/nebulosa/projection/Projection.kt b/nebulosa-projection/src/main/kotlin/nebulosa/projection/Projection.kt deleted file mode 100644 index e0b037e72..000000000 --- a/nebulosa-projection/src/main/kotlin/nebulosa/projection/Projection.kt +++ /dev/null @@ -1,8 +0,0 @@ -package nebulosa.projection - -import nebulosa.math.Vector3D - -interface Projection { - - fun project(position: Vector3D): Point -} diff --git a/nebulosa-projection/src/main/kotlin/nebulosa/projection/Stereographic.kt b/nebulosa-projection/src/main/kotlin/nebulosa/projection/Stereographic.kt deleted file mode 100644 index de81bdd64..000000000 --- a/nebulosa-projection/src/main/kotlin/nebulosa/projection/Stereographic.kt +++ /dev/null @@ -1,28 +0,0 @@ -package nebulosa.projection - -import nebulosa.math.Vector3D -import kotlin.math.hypot -import kotlin.math.sqrt - -class Stereographic(center: Vector3D) : Projection { - - private val normalized = center.normalized - private val cX = normalized.x - private val cY = normalized.y - private val cZ = normalized.z - private val t0 = 1.0 / hypot(cX, cY) - private val t2 = sqrt(-(cZ * cZ) + 1.0) - private val t3 = t0 * t2 - private val t6 = t0 * cZ - - override fun project(position: Vector3D): Point { - val u = position.normalized - val (x, y, z) = u - - val t1 = x * cX - val t4 = y * cY - val t5 = 1 / (t1 * t3 + t3 * t4 + z * cZ + 1.0) - - return Point(t0 * t5 * (x * cY - cX * y), -t5 * (t1 * t6 - t2 * z + t4 * t6)) - } -} diff --git a/nebulosa-retrofit/build.gradle.kts b/nebulosa-retrofit/build.gradle.kts index afe6c9358..d68a28ef0 100644 --- a/nebulosa-retrofit/build.gradle.kts +++ b/nebulosa-retrofit/build.gradle.kts @@ -4,9 +4,9 @@ plugins { } dependencies { + api(project(":nebulosa-json")) api(libs.retrofit) api(libs.retrofit.jackson) - api(libs.jackson) api(libs.okhttp) api(libs.okhttp.logging) compileOnly(libs.csv) diff --git a/nebulosa-simbad/src/main/kotlin/nebulosa/simbad/SimbadCatalogType.kt b/nebulosa-simbad/src/main/kotlin/nebulosa/simbad/SimbadCatalogType.kt new file mode 100644 index 000000000..1576726d1 --- /dev/null +++ b/nebulosa-simbad/src/main/kotlin/nebulosa/simbad/SimbadCatalogType.kt @@ -0,0 +1,43 @@ +package nebulosa.simbad + +typealias SimbadCatalogNameProvider = MatchResult.() -> String + +enum class SimbadCatalogType( + val regex: Regex, + val isStar: Boolean, val isDSO: Boolean = !isStar, + private val provider: SimbadCatalogNameProvider, +) { + NAMED("NAME\\s+(.*)", true, true, { groupValues[1].trim() }), + STAR("\\*\\s+(.*)", true, false, { groupValues[1].trim() }), + HD("HD\\s+(\\w*)", true, false, { "HD " + groupValues[1] }), + HR("HR\\s+(\\w*)", true, false, { "HR " + groupValues[1] }), + HIP("HIP\\s+(\\w*)", true, false, { "HIP " + groupValues[1] }), + NGC("NGC\\s+(\\w{1,5})", true, true, { "NGC " + groupValues[1] }), + IC("IC\\s+(\\w{1,5})", true, true, { "IC " + groupValues[1] }), + GUM("GUM\\s+(\\d{1,4})", false, true, { "Gum " + groupValues[1] }), + M("M\\s+(\\d{1,3})", false, true, { "M " + groupValues[1] }), + BARNARD("Barnard\\s+(\\d{1,3})", false, true, { "Barnard " + groupValues[1] }), + LBN("LBN\\s+(\\d{1,4})", false, true, { "LBN " + groupValues[1] }), + LDN("LDN\\s+(\\d{1,4})", false, true, { "LDN " + groupValues[1] }), + RCW("RCW\\s+(\\d{1,4})", false, true, { "RCW " + groupValues[1] }), + SH("SH\\s+2-(\\d{1,3})", false, true, { "Sh2-" + groupValues[1] }), + CED("Ced\\s+(\\d{1,3})", false, true, { "Ced " + groupValues[1] }), + UGC("UGC\\s+(\\d{1,5})", false, true, { "UGC " + groupValues[1] }), + APG("APG\\s+(\\d{1,3})", false, true, { "Arp " + groupValues[1] }), + HCG("HCG\\s+(\\d{1,3})", false, true, { "HCG " + groupValues[1] }), + VV("VV\\s+(\\d{1,4})", false, true, { "VV " + groupValues[1] }), + VDBH("VdBH\\s+(\\d{1,2})", false, true, { "VdBH " + groupValues[1] }), + DWB("DWB\\s+(\\d{1,3})", false, true, { "DWB " + groupValues[1] }), + LEDA("LEDA\\s+(\\d{1,7})", false, true, { "LEDA " + groupValues[1] }), + CL("Cl\\s+([\\w-]+)\\s+(\\d{1,5})", false, true, { groupValues[1] + " " + groupValues[2] }); + + constructor( + regex: String, + isStar: Boolean, isDSO: Boolean = !isStar, + provider: MatchResult.() -> String, + ) : this(regex.toRegex(), isStar, isDSO, provider) + + fun matches(text: String) = regex.matches(text) + + fun match(text: String) = regex.matchEntire(text)?.let { provider(it) } +} diff --git a/nebulosa-simbad/src/main/kotlin/nebulosa/simbad/SimbadEntry.kt b/nebulosa-simbad/src/main/kotlin/nebulosa/simbad/SimbadEntry.kt index 985caa5fe..9801ead0b 100644 --- a/nebulosa-simbad/src/main/kotlin/nebulosa/simbad/SimbadEntry.kt +++ b/nebulosa-simbad/src/main/kotlin/nebulosa/simbad/SimbadEntry.kt @@ -1,4 +1,4 @@ -package nebulosa.simbad; +package nebulosa.simbad import nebulosa.math.Angle import nebulosa.math.Distance diff --git a/nebulosa-skycatalog/src/main/kotlin/nebulosa/skycatalog/GeodesicGrid.kt b/nebulosa-skycatalog/src/main/kotlin/nebulosa/skycatalog/GeodesicGrid.kt index 46c521d04..0c6c1fa08 100644 --- a/nebulosa-skycatalog/src/main/kotlin/nebulosa/skycatalog/GeodesicGrid.kt +++ b/nebulosa-skycatalog/src/main/kotlin/nebulosa/skycatalog/GeodesicGrid.kt @@ -7,7 +7,7 @@ class GeodesicGrid(val maxLevel: Int) { fun interface Traverser { - fun traverse(level: Int, index: Int, c0: Vector3D, c1: Vector3D, c2: Vector3D): Unit + fun traverse(level: Int, index: Int, c0: Vector3D, c1: Vector3D, c2: Vector3D) } val triangles: Array> @@ -45,7 +45,7 @@ class GeodesicGrid(val maxLevel: Int) { ICOSAHEDRON_CORNERS[corners[1]], ICOSAHEDRON_CORNERS[corners[2]], maxVisitLevel, traverser, - ); + ) } } } diff --git a/nebulosa-time/src/main/kotlin/nebulosa/time/DeltaTime.kt b/nebulosa-time/src/main/kotlin/nebulosa/time/DeltaTime.kt index f965b21d0..123b9209c 100644 --- a/nebulosa-time/src/main/kotlin/nebulosa/time/DeltaTime.kt +++ b/nebulosa-time/src/main/kotlin/nebulosa/time/DeltaTime.kt @@ -107,8 +107,4 @@ interface DeltaTime { override fun delta(time: InstantOfTime) = spline.compute((time.value - 1721045.0) / DAYSPERJY) } - - companion object { - - } } diff --git a/nebulosa-time/src/main/kotlin/nebulosa/time/IERSA.kt b/nebulosa-time/src/main/kotlin/nebulosa/time/IERSA.kt index 3ffb62389..294f1d895 100644 --- a/nebulosa-time/src/main/kotlin/nebulosa/time/IERSA.kt +++ b/nebulosa-time/src/main/kotlin/nebulosa/time/IERSA.kt @@ -54,7 +54,7 @@ class IERSA : IERS() { override lateinit var dut1: DoubleArray private set - override val columns = Column.values().toList() + override val columns = Column.entries override fun canUseThisLine(line: String) = line.trim().length > 17 && line[16] == 'I' diff --git a/settings.gradle.kts b/settings.gradle.kts index f5b471271..87d91809d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -12,19 +12,20 @@ buildCache { dependencyResolutionManagement { versionCatalogs { create("libs") { - library("okio", "com.squareup.okio:okio:3.5.0") + library("okio", "com.squareup.okio:okio:3.6.0") library("okhttp", "com.squareup.okhttp3:okhttp:5.0.0-alpha.11") library("okhttp-logging", "com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.11") library("fits", "gov.nasa.gsfc.heasarc:nom-tam-fits:1.18.1") - library("jackson", "com.fasterxml.jackson.core:jackson-databind:2.15.2") - library("jackson-jsr310", "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.15.2") + library("jackson-core", "com.fasterxml.jackson.core:jackson-databind:2.15.3") + library("jackson-jsr310", "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.15.3") + library("jackson-kt", "com.fasterxml.jackson.module:jackson-module-kotlin:2.15.3") library("retrofit", "com.squareup.retrofit2:retrofit:2.9.0") library("retrofit-jackson", "com.squareup.retrofit2:converter-jackson:2.9.0") - library("rx", "io.reactivex.rxjava3:rxjava:3.1.7") + library("rx", "io.reactivex.rxjava3:rxjava:3.1.8") library("logback", "ch.qos.logback:logback-classic:1.4.11") library("eventbus", "org.greenrobot:eventbus-java:3.3.1") - library("netty-transport", "io.netty:netty-transport:4.1.99.Final") - library("netty-codec", "io.netty:netty-codec:4.1.99.Final") + library("netty-transport", "io.netty:netty-transport:4.1.100.Final") + library("netty-codec", "io.netty:netty-codec:4.1.100.Final") library("xml", "com.fasterxml:aalto-xml:1.3.2") library("csv", "de.siegmar:fastcsv:2.2.2") library("apache-lang3", "org.apache.commons:commons-lang3:3.13.0") @@ -33,12 +34,13 @@ dependencyResolutionManagement { library("oshi", "com.github.oshi:oshi-core:6.4.6") library("timeshape", "net.iakovlev:timeshape:2022g.17") library("sqlite", "org.xerial:sqlite-jdbc:3.43.0.0") - library("flyway", "org.flywaydb:flyway-core:9.22.1") + library("flyway", "org.flywaydb:flyway-core:9.22.3") library("jna", "net.java.dev.jna:jna:5.13.0") library("kotest-assertions-core", "io.kotest:kotest-assertions-core:5.7.2") library("kotest-runner-junit5", "io.kotest:kotest-runner-junit5:5.7.2") bundle("kotest", listOf("kotest-assertions-core", "kotest-runner-junit5")) bundle("netty", listOf("netty-transport", "netty-codec")) + bundle("jackson", listOf("jackson-core", "jackson-jsr310", "jackson-kt")) } } } @@ -75,7 +77,6 @@ include(":nebulosa-platesolving") include(":nebulosa-platesolving-astap") include(":nebulosa-platesolving-astrometrynet") include(":nebulosa-platesolving-watney") -include(":nebulosa-projection") include(":nebulosa-retrofit") include(":nebulosa-sbd") include(":nebulosa-simbad")