From c083a5ec0f7bca33e39d029555aeb789d2cacf45 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Wed, 6 Dec 2023 09:56:09 -0300 Subject: [PATCH 01/87] [desktop]: Restore last directory when open a file --- desktop/app/main.ts | 9 +++++---- .../app/calibration/calibration.component.ts | 19 +++++++++++++------ desktop/src/app/home/home.component.ts | 9 ++++++--- .../src/shared/services/electron.service.ts | 10 +++++++--- desktop/src/shared/types.ts | 6 +++++- 5 files changed, 36 insertions(+), 17 deletions(-) diff --git a/desktop/app/main.ts b/desktop/app/main.ts index d6ae79640..b715a727a 100644 --- a/desktop/app/main.ts +++ b/desktop/app/main.ts @@ -2,7 +2,7 @@ import { Client } from '@stomp/stompjs' import { BrowserWindow, Menu, Notification, app, dialog, ipcMain, screen, shell } from 'electron' import { ChildProcessWithoutNullStreams, spawn } from 'node:child_process' import * as path from 'path' -import { InternalEventType, MessageEvent, NotificationEvent, OpenDirectory, OpenWindow } from './types' +import { InternalEventType, MessageEvent, NotificationEvent, OpenDirectory, OpenFile, OpenWindow } from './types' import { WebSocket } from 'ws' Object.assign(global, { WebSocket }) @@ -280,12 +280,13 @@ try { }) }) - ipcMain.handle('OPEN_FITS', async (event) => { + ipcMain.handle('OPEN_FILE', async (event, data?: OpenFile) => { const ownerWindow = findWindowById(event.sender.id) const value = await dialog.showOpenDialog(ownerWindow!, { - filters: [{ name: 'FITS files', extensions: ['fits', 'fit'] }], + filters: data?.filters, properties: ['openFile'], + defaultPath: data?.defaultPath || undefined, }) return !value.canceled && value.filePaths[0] @@ -308,7 +309,7 @@ try { const ownerWindow = findWindowById(event.sender.id) const value = await dialog.showOpenDialog(ownerWindow!, { properties: ['openDirectory'], - defaultPath: data?.defaultPath, + defaultPath: data?.defaultPath || undefined, }) return !value.canceled && value.filePaths[0] diff --git a/desktop/src/app/calibration/calibration.component.ts b/desktop/src/app/calibration/calibration.component.ts index b1da8bbf2..9d80947ac 100644 --- a/desktop/src/app/calibration/calibration.component.ts +++ b/desktop/src/app/calibration/calibration.component.ts @@ -1,8 +1,10 @@ import { AfterViewInit, Component, HostListener, OnDestroy } from '@angular/core' import { ActivatedRoute } from '@angular/router' +import path from 'path' import { CheckboxChangeEvent } from 'primeng/checkbox' import { ApiService } from '../../shared/services/api.service' import { ElectronService } from '../../shared/services/electron.service' +import { LocalStorageService } from '../../shared/services/local-storage.service' import { CalibrationFrame, CalibrationFrameGroup, Camera } from '../../shared/types' import { AppComponent } from '../app.component' @@ -28,6 +30,7 @@ export class CalibrationComponent implements AfterViewInit, OnDestroy { private api: ApiService, electron: ElectronService, private route: ActivatedRoute, + private storage: LocalStorageService, ) { app.title = 'Calibration' @@ -35,10 +38,12 @@ export class CalibrationComponent implements AfterViewInit, OnDestroy { icon: 'mdi mdi-image-plus', tooltip: 'Add file', command: async () => { - const path = await electron.openFITS() + const defaultPath = this.storage.get('calibration.directory', '') + const fitsPath = await electron.openFITS({ defaultPath }) - if (path) { - this.upload(path) + if (fitsPath) { + this.storage.set('calibration.directory', path.dirname(fitsPath)) + this.upload(fitsPath) } }, }) @@ -47,10 +52,12 @@ export class CalibrationComponent implements AfterViewInit, OnDestroy { icon: 'mdi mdi-folder-plus', tooltip: 'Add folder', command: async () => { - const path = await electron.openDirectory() + const defaultPath = this.storage.get('calibration.directory', '') + const dirPath = await electron.openDirectory({ defaultPath }) - if (path) { - this.upload(path) + if (dirPath) { + this.storage.set('calibration.directory', dirPath) + this.upload(dirPath) } }, }) diff --git a/desktop/src/app/home/home.component.ts b/desktop/src/app/home/home.component.ts index 41f75c781..42ae0d7ed 100644 --- a/desktop/src/app/home/home.component.ts +++ b/desktop/src/app/home/home.component.ts @@ -8,6 +8,7 @@ import { ElectronService } from '../../shared/services/electron.service' import { LocalStorageService } from '../../shared/services/local-storage.service' import { Camera, Device, FilterWheel, Focuser, HomeWindowType, Mount } from '../../shared/types' import { AppComponent } from '../app.component' +import path from 'path' type MappedDevice = { 'CAMERA': Camera @@ -259,10 +260,12 @@ export class HomeComponent implements AfterContentInit, OnDestroy { private async openImage(force: boolean = false) { if (force || this.cameras.length === 0) { - const path = await this.electron.openFITS() + const defaultPath = this.storage.get('home.image.directory', '') + const fitsPath = await this.electron.openFITS({ defaultPath }) - if (path) { - this.browserWindow.openImage({ path, source: 'PATH' }) + if (fitsPath) { + this.storage.set('home.image.directory', path.dirname(fitsPath)) + this.browserWindow.openImage({ path: fitsPath, source: 'PATH' }) } } else { const camera = await this.imageMenu.show(this.cameras) diff --git a/desktop/src/shared/services/electron.service.ts b/desktop/src/shared/services/electron.service.ts index d1d3c70ed..91a90c5a0 100644 --- a/desktop/src/shared/services/electron.service.ts +++ b/desktop/src/shared/services/electron.service.ts @@ -10,7 +10,7 @@ import { ApiEventType, Camera, CameraCaptureElapsed, CameraCaptureFinished, CameraCaptureIsWaiting, CameraCaptureStarted, CameraExposureElapsed, CameraExposureFinished, CameraExposureStarted, DARVPolarAlignmentEvent, DARVPolarAlignmentGuidePulseElapsed, DARVPolarAlignmentInitialPauseElapsed, DeviceMessageEvent, FilterWheel, Focuser, GuideOutput, Guider, - GuiderMessageEvent, HistoryStep, INDIMessageEvent, InternalEventType, Location, Mount, NotificationEvent, NotificationEventType, OpenDirectory + GuiderMessageEvent, HistoryStep, INDIMessageEvent, InternalEventType, Location, Mount, NotificationEvent, NotificationEventType, OpenDirectory, OpenFile } from '../types' import { ApiService } from './api.service' @@ -98,8 +98,12 @@ export class ElectronService { this.ipcRenderer.on(channel, (_, arg) => listener(arg)) } - openFITS(): Promise { - return this.send('OPEN_FITS') + openFile(data?: OpenFile): Promise { + return this.send('OPEN_FILE', data) + } + + openFITS(data?: OpenFile): Promise { + return this.openFile({ ...data, filters: [{ name: 'FITS files', extensions: ['fits', 'fit'] }] }) } openDirectory(data?: OpenDirectory): Promise { diff --git a/desktop/src/shared/types.ts b/desktop/src/shared/types.ts index 26446afb1..289d1cae6 100644 --- a/desktop/src/shared/types.ts +++ b/desktop/src/shared/types.ts @@ -269,6 +269,10 @@ export interface OpenDirectory { defaultPath?: string } +export interface OpenFile extends OpenDirectory { + filters?: Electron.FileFilter[] +} + export interface GuideCaptureEvent { camera: Camera } @@ -785,7 +789,7 @@ export const API_EVENT_TYPES = [ export type ApiEventType = (typeof API_EVENT_TYPES)[number] export const INTERNAL_EVENT_TYPES = [ - 'SAVE_FITS_AS', 'OPEN_FITS', 'OPEN_WINDOW', 'OPEN_DIRECTORY', 'CLOSE_WINDOW', + 'SAVE_FITS_AS', 'OPEN_FILE', 'OPEN_WINDOW', 'OPEN_DIRECTORY', 'CLOSE_WINDOW', 'PIN_WINDOW', 'UNPIN_WINDOW', 'MINIMIZE_WINDOW', 'MAXIMIZE_WINDOW', 'WHEEL_RENAMED', 'LOCATION_CHANGED', ] as const From 148ea918007f70ae01e99a9569ce72d06a674a62 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Thu, 7 Dec 2023 14:32:26 -0300 Subject: [PATCH 02/87] [api]: Fix Duration parse in query params and body --- .../converters/StringToDurationConverter.kt | 18 ++++++++++++++++++ .../api/cameras/CameraStartCaptureRequest.kt | 6 ++++-- .../api/guiding/GuideOutputController.kt | 7 +++++-- 3 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 api/src/main/kotlin/nebulosa/api/beans/converters/StringToDurationConverter.kt diff --git a/api/src/main/kotlin/nebulosa/api/beans/converters/StringToDurationConverter.kt b/api/src/main/kotlin/nebulosa/api/beans/converters/StringToDurationConverter.kt new file mode 100644 index 000000000..a95e14c30 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/beans/converters/StringToDurationConverter.kt @@ -0,0 +1,18 @@ +package nebulosa.api.beans.converters + +import org.springframework.boot.convert.DurationStyle +import org.springframework.core.convert.converter.Converter +import org.springframework.stereotype.Component +import java.time.Duration +import java.time.temporal.ChronoUnit + +@Component +class StringToDurationConverter : Converter { + + override fun convert(source: String): Duration? { + val text = source.ifBlank { null } ?: return null + + return text.toLongOrNull()?.let { Duration.ofNanos(it * 1000L) } + ?: DurationStyle.SIMPLE.parse(text, ChronoUnit.MICROS) + } +} diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraStartCaptureRequest.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraStartCaptureRequest.kt index 4eefe4b60..fd7277ad5 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraStartCaptureRequest.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraStartCaptureRequest.kt @@ -8,14 +8,16 @@ import nebulosa.api.guiding.DitherAfterExposureRequest import nebulosa.indi.device.camera.Camera import nebulosa.indi.device.camera.FrameType import org.hibernate.validator.constraints.Range +import org.hibernate.validator.constraints.time.DurationMax +import org.hibernate.validator.constraints.time.DurationMin import java.nio.file.Path import java.time.Duration data class CameraStartCaptureRequest( @JsonIgnore val camera: Camera? = null, - @field:Positive val exposureTime: Duration = Duration.ZERO, + @field:DurationMin(nanos = 1000L) @field:DurationMax(minutes = 60L) val exposureTime: Duration = Duration.ZERO, @field:Range(min = 0L, max = 1000L) val exposureAmount: Int = 1, // 0 = looping - @field:Range(min = 0L, max = 60L) val exposureDelay: Duration = Duration.ZERO, + @field:DurationMin(nanos = 0L) @field:DurationMax(seconds = 60L) val exposureDelay: Duration = Duration.ZERO, @field:PositiveOrZero val x: Int = 0, @field:PositiveOrZero val y: Int = 0, @field:PositiveOrZero val width: Int = 0, diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputController.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputController.kt index ec75d6c4c..12ab9ccf0 100644 --- a/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputController.kt +++ b/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputController.kt @@ -4,6 +4,8 @@ import nebulosa.api.beans.annotations.EntityBy import nebulosa.api.connection.ConnectionService import nebulosa.guiding.GuideDirection import nebulosa.indi.device.guide.GuideOutput +import org.hibernate.validator.constraints.time.DurationMax +import org.hibernate.validator.constraints.time.DurationMin import org.springframework.web.bind.annotation.* import java.time.Duration @@ -37,8 +39,9 @@ class GuideOutputController( @PutMapping("{guideOutput}/pulse") fun pulse( @EntityBy guideOutput: GuideOutput, - @RequestParam direction: GuideDirection, @RequestParam duration: Long, + @RequestParam direction: GuideDirection, + @RequestParam @DurationMin(nanos = 0L) @DurationMax(seconds = 60L) duration: Duration, ) { - guideOutputService.pulse(guideOutput, direction, Duration.ofNanos(duration * 1000L)) + guideOutputService.pulse(guideOutput, direction, duration) } } From e9f376232aea1ab5f6b7f7c43491dc3d33dc657a Mon Sep 17 00:00:00 2001 From: tiagohm Date: Thu, 7 Dec 2023 22:36:31 -0300 Subject: [PATCH 03/87] [api]: Fix Transactional annotation usage; Fix DARV job executor --- .../polar/darv/DARVPolarAlignmentExecutor.kt | 21 ++----------------- .../api/atlas/DeepSkyObjectRepository.kt | 5 ----- .../nebulosa/api/atlas/SatelliteRepository.kt | 4 ---- .../nebulosa/api/atlas/SkyAtlasService.kt | 6 +++++- .../api/atlas/SkyAtlasUpdateFinished.kt | 8 ------- .../nebulosa/api/atlas/SkyAtlasUpdater.kt | 10 +++++++-- .../nebulosa/api/atlas/StarRepository.kt | 5 ----- .../calibration/CalibrationFrameRepository.kt | 6 ------ .../api/cameras/CameraCaptureExecutor.kt | 4 ---- .../nebulosa/api/guiding/GuidingService.kt | 7 +------ .../api/locations/LocationRepository.kt | 3 --- .../nebulosa/api/locations/LocationService.kt | 2 ++ .../api/preferences/PreferenceRepository.kt | 4 ---- .../api/sequencer/SequenceJobFactory.kt | 19 +++++++++++++++++ .../sequencer/tasklets/delay/DelayTasklet.kt | 2 -- .../resources/db/migration/beforeMigrate.sql | 1 + build.gradle.kts | 4 ++-- gradle.properties | 2 -- 18 files changed, 40 insertions(+), 73 deletions(-) delete mode 100644 api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasUpdateFinished.kt 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 index 730aba73e..eb722e5d6 100644 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentExecutor.kt +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentExecutor.kt @@ -10,20 +10,14 @@ import nebulosa.api.guiding.GuidePulseRequest import nebulosa.api.sequencer.* import nebulosa.api.sequencer.tasklets.delay.DelayElapsed import nebulosa.api.services.MessageService -import nebulosa.common.concurrency.Incrementer import nebulosa.indi.device.camera.Camera import nebulosa.indi.device.guide.GuideOutput import nebulosa.log.loggerFor import org.springframework.batch.core.JobExecution import org.springframework.batch.core.JobExecutionListener import org.springframework.batch.core.JobParameters -import org.springframework.batch.core.configuration.JobRegistry -import org.springframework.batch.core.configuration.support.ReferenceJobFactory -import org.springframework.batch.core.job.builder.JobBuilder import org.springframework.batch.core.launch.JobLauncher import org.springframework.batch.core.launch.JobOperator -import org.springframework.batch.core.repository.JobRepository -import org.springframework.core.task.SimpleAsyncTaskExecutor import org.springframework.stereotype.Component import java.nio.file.Path import java.util.* @@ -33,16 +27,13 @@ import java.util.* */ @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, + private val sequenceJobFactory: SequenceJobFactory, ) : SequenceJobExecutor, Consumer, JobExecutionListener { private val runningSequenceJobs = LinkedList() @@ -84,20 +75,12 @@ class DARVPolarAlignmentExecutor( 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() + val darvJob = sequenceJobFactory.darvPolarAlignment(cameraExposureFlow, guidePulseFlow, this, cameraExposureTasklet) return jobLauncher .run(darvJob, JobParameters()) .let { DARVSequenceJob(camera, guideOutput, request, darvJob, it) } .also(runningSequenceJobs::add) - .also { jobRegistry.register(ReferenceJobFactory(darvJob)) } } @Synchronized diff --git a/api/src/main/kotlin/nebulosa/api/atlas/DeepSkyObjectRepository.kt b/api/src/main/kotlin/nebulosa/api/atlas/DeepSkyObjectRepository.kt index 96ceb780e..ab73ec990 100644 --- a/api/src/main/kotlin/nebulosa/api/atlas/DeepSkyObjectRepository.kt +++ b/api/src/main/kotlin/nebulosa/api/atlas/DeepSkyObjectRepository.kt @@ -8,11 +8,8 @@ import org.springframework.data.domain.Pageable import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.Query import org.springframework.stereotype.Repository -import org.springframework.transaction.annotation.Isolation -import org.springframework.transaction.annotation.Transactional @Repository -@Transactional(readOnly = false, isolation = Isolation.SERIALIZABLE) interface DeepSkyObjectRepository : JpaRepository { @Query( @@ -24,7 +21,6 @@ interface DeepSkyObjectRepository : JpaRepository { "(:radius <= 0.0 OR acos(sin(dso.declinationJ2000) * sin(:declinationJ2000) + cos(dso.declinationJ2000) * cos(:declinationJ2000) * cos(dso.rightAscensionJ2000 - :rightAscensionJ2000)) <= :radius) " + "ORDER BY dso.magnitude ASC" ) - @Transactional(readOnly = true, isolation = Isolation.READ_UNCOMMITTED) fun search( text: String? = null, rightAscensionJ2000: Angle = 0.0, declinationJ2000: Angle = 0.0, radius: Angle = 0.0, @@ -35,6 +31,5 @@ interface DeepSkyObjectRepository : JpaRepository { ): List @Query("SELECT DISTINCT dso.type FROM DeepSkyObjectEntity dso") - @Transactional(readOnly = true, isolation = Isolation.READ_UNCOMMITTED) fun types(): List } diff --git a/api/src/main/kotlin/nebulosa/api/atlas/SatelliteRepository.kt b/api/src/main/kotlin/nebulosa/api/atlas/SatelliteRepository.kt index fdb0f847a..6c8d2da4f 100644 --- a/api/src/main/kotlin/nebulosa/api/atlas/SatelliteRepository.kt +++ b/api/src/main/kotlin/nebulosa/api/atlas/SatelliteRepository.kt @@ -4,11 +4,8 @@ import org.springframework.data.domain.Pageable import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.Query import org.springframework.stereotype.Repository -import org.springframework.transaction.annotation.Isolation -import org.springframework.transaction.annotation.Transactional @Repository -@Transactional(readOnly = false, isolation = Isolation.SERIALIZABLE) interface SatelliteRepository : JpaRepository { @Query( @@ -17,7 +14,6 @@ interface SatelliteRepository : JpaRepository { " (:groupType = 0 OR s.group_type & :groupType != 0)", nativeQuery = true, ) - @Transactional(readOnly = true, isolation = Isolation.READ_UNCOMMITTED) fun search(text: String? = null, groupType: Long = 0L, page: Pageable): List fun search(text: String? = null, groups: List, page: Pageable): List { diff --git a/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasService.kt b/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasService.kt index 8471ff9f7..c38d2dfaa 100644 --- a/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasService.kt +++ b/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasService.kt @@ -25,6 +25,7 @@ import org.springframework.http.HttpStatus import org.springframework.scheduling.annotation.EnableScheduling import org.springframework.scheduling.annotation.Scheduled import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional import org.springframework.web.server.ResponseStatusException import java.awt.image.BufferedImage import java.io.ByteArrayOutputStream @@ -112,6 +113,7 @@ class SkyAtlasService( else horizonsEphemerisProvider.compute(target, position, dateTime, zoneId) } + @Transactional(readOnly = true) fun searchSatellites(text: String, groups: List): List { return satelliteRepository.search(text.ifBlank { null }, groups, Pageable.ofSize(1000)) } @@ -203,6 +205,7 @@ class SkyAtlasService( ?.let(MinorPlanet::of) ?: MinorPlanet.EMPTY + @Transactional(readOnly = true) fun searchStar( text: String, rightAscension: Angle = 0.0, declination: Angle = 0.0, radius: Angle = 0.0, @@ -217,6 +220,7 @@ class SkyAtlasService( Pageable.ofSize(5000), ) + @Transactional(readOnly = true) fun searchDSO( text: String, rightAscension: Angle = 0.0, declination: Angle = 0.0, radius: Angle = 0.0, @@ -241,7 +245,7 @@ class SkyAtlasService( .search(SimbadSearch(0, text, rightAscension, declination, radius, type?.let(::listOf), magnitudeMin, magnitudeMax, constellation, 5000)) @Scheduled(fixedDelay = 15, timeUnit = TimeUnit.MINUTES) - private fun refreshImageOfSun() { + protected fun refreshImageOfSun() { val request = Request.Builder() .url(SUN_IMAGE_URL) .build() diff --git a/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasUpdateFinished.kt b/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasUpdateFinished.kt deleted file mode 100644 index a351c304b..000000000 --- a/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasUpdateFinished.kt +++ /dev/null @@ -1,8 +0,0 @@ -package nebulosa.api.atlas - -import nebulosa.api.notification.NotificationEvent - -data class SkyAtlasUpdateFinished(override val body: String) : NotificationEvent { - - override val type = "SKY_ATLAS_UPDATE_FINISHED" -} diff --git a/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasUpdater.kt b/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasUpdater.kt index e194de02c..9fe78c2c1 100644 --- a/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasUpdater.kt +++ b/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasUpdater.kt @@ -3,6 +3,7 @@ package nebulosa.api.atlas import com.fasterxml.jackson.core.type.TypeReference import com.fasterxml.jackson.databind.ObjectMapper import nebulosa.api.beans.annotations.ThreadedTask +import nebulosa.api.notification.NotificationEvent import nebulosa.api.preferences.PreferenceService import nebulosa.api.services.MessageService import nebulosa.log.loggerFor @@ -28,6 +29,11 @@ class SkyAtlasUpdater( private val messageService: MessageService, ) : Runnable { + data class Finished(override val body: String) : NotificationEvent { + + override val type = "SKY_ATLAS_UPDATE_FINISHED" + } + override fun run() { satelliteUpdater.run() @@ -36,7 +42,7 @@ class SkyAtlasUpdater( if (version != DATABASE_VERSION) { LOG.info("Star/DSO database is out of date. currentVersion={}, newVersion={}", version, DATABASE_VERSION) - messageService.sendMessage(SkyAtlasUpdateFinished("Star/DSO database is being updated.")) + messageService.sendMessage(Finished("Star/DSO database is being updated.")) starsRepository.deleteAllInBatch() deepSkyObjectRepository.deleteAllInBatch() @@ -46,7 +52,7 @@ class SkyAtlasUpdater( preferenceService.skyAtlasVersion = DATABASE_VERSION - messageService.sendMessage(SkyAtlasUpdateFinished("Sky Atlas database was updated to version $DATABASE_VERSION.")) + messageService.sendMessage(Finished("Sky Atlas database was updated to version $DATABASE_VERSION.")) } else { LOG.info("Star/DSO database is up to date") } diff --git a/api/src/main/kotlin/nebulosa/api/atlas/StarRepository.kt b/api/src/main/kotlin/nebulosa/api/atlas/StarRepository.kt index 5b6160a59..d32dcbb59 100644 --- a/api/src/main/kotlin/nebulosa/api/atlas/StarRepository.kt +++ b/api/src/main/kotlin/nebulosa/api/atlas/StarRepository.kt @@ -8,11 +8,8 @@ import org.springframework.data.domain.Pageable import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.Query import org.springframework.stereotype.Repository -import org.springframework.transaction.annotation.Isolation -import org.springframework.transaction.annotation.Transactional @Repository -@Transactional(readOnly = false, isolation = Isolation.SERIALIZABLE) interface StarRepository : JpaRepository { @Query( @@ -24,7 +21,6 @@ interface StarRepository : JpaRepository { "(:radius <= 0.0 OR acos(sin(star.declinationJ2000) * sin(:declinationJ2000) + cos(star.declinationJ2000) * cos(:declinationJ2000) * cos(star.rightAscensionJ2000 - :rightAscensionJ2000)) <= :radius) " + "ORDER BY star.magnitude ASC" ) - @Transactional(readOnly = true, isolation = Isolation.READ_UNCOMMITTED) fun search( text: String? = null, rightAscensionJ2000: Angle = 0.0, declinationJ2000: Angle = 0.0, radius: Angle = 0.0, @@ -35,6 +31,5 @@ interface StarRepository : JpaRepository { ): List @Query("SELECT DISTINCT star.type FROM StarEntity star") - @Transactional(readOnly = true, isolation = Isolation.READ_UNCOMMITTED) fun types(): List } diff --git a/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameRepository.kt b/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameRepository.kt index 86c6455ba..f4a04594c 100644 --- a/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameRepository.kt +++ b/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameRepository.kt @@ -5,11 +5,8 @@ import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.Modifying import org.springframework.data.jpa.repository.Query import org.springframework.stereotype.Repository -import org.springframework.transaction.annotation.Isolation -import org.springframework.transaction.annotation.Transactional @Repository -@Transactional(readOnly = false, isolation = Isolation.SERIALIZABLE) interface CalibrationFrameRepository : JpaRepository { @Query("SELECT frame FROM CalibrationFrameEntity frame WHERE frame.camera = :#{#camera.name}") @@ -28,7 +25,6 @@ interface CalibrationFrameRepository : JpaRepository @Query( @@ -38,7 +34,6 @@ interface CalibrationFrameRepository : JpaRepository @Query( @@ -47,6 +42,5 @@ interface CalibrationFrameRepository : JpaRepository } diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureExecutor.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureExecutor.kt index e4828afda..7c1cf3dc7 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureExecutor.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureExecutor.kt @@ -8,8 +8,6 @@ import nebulosa.indi.device.camera.Camera import nebulosa.log.loggerFor import org.springframework.batch.core.JobExecution import org.springframework.batch.core.JobParameters -import org.springframework.batch.core.configuration.JobRegistry -import org.springframework.batch.core.configuration.support.ReferenceJobFactory import org.springframework.batch.core.launch.JobLauncher import org.springframework.batch.core.launch.JobOperator import org.springframework.stereotype.Component @@ -19,7 +17,6 @@ import java.util.* class CameraCaptureExecutor( private val jobOperator: JobOperator, private val asyncJobLauncher: JobLauncher, - private val jobRegistry: JobRegistry, private val messageService: MessageService, private val sequenceJobFactory: SequenceJobFactory, ) : SequenceJobExecutor, Consumer { @@ -45,7 +42,6 @@ class CameraCaptureExecutor( .run(cameraCaptureJob, JobParameters()) .let { CameraSequenceJob(camera, request, cameraCaptureJob, it) } .also(runningSequenceJobs::add) - .also { jobRegistry.register(ReferenceJobFactory(cameraCaptureJob)) } } fun stop(camera: Camera) { diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuidingService.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuidingService.kt index 78bd71846..3a62244ec 100644 --- a/api/src/main/kotlin/nebulosa/api/guiding/GuidingService.kt +++ b/api/src/main/kotlin/nebulosa/api/guiding/GuidingService.kt @@ -1,6 +1,5 @@ package nebulosa.api.guiding -import jakarta.annotation.PostConstruct import jakarta.annotation.PreDestroy import nebulosa.api.preferences.PreferenceService import nebulosa.api.services.MessageService @@ -33,17 +32,13 @@ class GuidingService( val settleTimeout get() = guider.settleTimeout - @PostConstruct - private fun initialize() { - settle(preferenceService.getJSON("GUIDING.SETTLE") ?: SettleInfo.EMPTY) - } - @Synchronized fun connect(host: String, port: Int) { check(!phd2Client.isOpen) phd2Client.open(host, port) guider.registerGuiderListener(this) + settle(preferenceService.getJSON("GUIDING.SETTLE") ?: SettleInfo.EMPTY) messageService.sendMessage(GuiderMessageEvent(GUIDER_CONNECTED)) } diff --git a/api/src/main/kotlin/nebulosa/api/locations/LocationRepository.kt b/api/src/main/kotlin/nebulosa/api/locations/LocationRepository.kt index ebffc80aa..4b78cc76e 100644 --- a/api/src/main/kotlin/nebulosa/api/locations/LocationRepository.kt +++ b/api/src/main/kotlin/nebulosa/api/locations/LocationRepository.kt @@ -4,11 +4,8 @@ import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.Modifying import org.springframework.data.jpa.repository.Query import org.springframework.stereotype.Repository -import org.springframework.transaction.annotation.Isolation -import org.springframework.transaction.annotation.Transactional @Repository -@Transactional(readOnly = false, isolation = Isolation.SERIALIZABLE) interface LocationRepository : JpaRepository { fun findFirstByOrderById(): LocationEntity? diff --git a/api/src/main/kotlin/nebulosa/api/locations/LocationService.kt b/api/src/main/kotlin/nebulosa/api/locations/LocationService.kt index 83314a4d1..6d6b0fcc7 100644 --- a/api/src/main/kotlin/nebulosa/api/locations/LocationService.kt +++ b/api/src/main/kotlin/nebulosa/api/locations/LocationService.kt @@ -1,6 +1,7 @@ package nebulosa.api.locations import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional @Service class LocationService( @@ -27,6 +28,7 @@ class LocationService( } @Synchronized + @Transactional fun delete(id: Long) { if (id > 0L && locationRepository.count() > 1) { var location = location(id) diff --git a/api/src/main/kotlin/nebulosa/api/preferences/PreferenceRepository.kt b/api/src/main/kotlin/nebulosa/api/preferences/PreferenceRepository.kt index d9641be71..8f9bb7837 100644 --- a/api/src/main/kotlin/nebulosa/api/preferences/PreferenceRepository.kt +++ b/api/src/main/kotlin/nebulosa/api/preferences/PreferenceRepository.kt @@ -3,12 +3,8 @@ package nebulosa.api.preferences import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.Query import org.springframework.stereotype.Repository -import org.springframework.transaction.annotation.Isolation -import org.springframework.transaction.annotation.Propagation -import org.springframework.transaction.annotation.Transactional @Repository -@Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW, isolation = Isolation.SERIALIZABLE) interface PreferenceRepository : JpaRepository { @Query("SELECT p.key FROM PreferenceEntity p") diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/SequenceJobFactory.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SequenceJobFactory.kt index 549a6d8fd..c0e0e848c 100644 --- a/api/src/main/kotlin/nebulosa/api/sequencer/SequenceJobFactory.kt +++ b/api/src/main/kotlin/nebulosa/api/sequencer/SequenceJobFactory.kt @@ -5,12 +5,15 @@ import nebulosa.api.cameras.CameraCaptureEvent import nebulosa.api.cameras.CameraStartCaptureRequest import nebulosa.common.concurrency.Incrementer import org.springframework.batch.core.Job +import org.springframework.batch.core.JobExecutionListener import org.springframework.batch.core.job.builder.JobBuilder +import org.springframework.batch.core.job.flow.Flow import org.springframework.batch.core.repository.JobRepository import org.springframework.beans.factory.config.ConfigurableBeanFactory import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Scope +import org.springframework.core.task.SimpleAsyncTaskExecutor @Configuration class SequenceJobFactory( @@ -19,6 +22,7 @@ class SequenceJobFactory( private val sequenceStepFactory: SequenceStepFactory, private val sequenceTaskletFactory: SequenceTaskletFactory, private val jobIncrementer: Incrementer, + private val simpleAsyncTaskExecutor: SimpleAsyncTaskExecutor, ) { @Bean(name = ["cameraLoopCaptureJob"], autowireCandidate = false) @@ -68,4 +72,19 @@ class SequenceJobFactory( .listener(cameraDelayTasklet) .build() } + + @Bean(name = ["darvJob"], autowireCandidate = false) + @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) + fun darvPolarAlignment( + cameraExposureFlow: Flow, guidePulseFlow: Flow, + vararg listeners: JobExecutionListener, + ): Job { + return JobBuilder("DARVPolarAlignment.Job.${jobIncrementer.increment()}", jobRepository) + .start(cameraExposureFlow) + .split(simpleAsyncTaskExecutor) + .add(guidePulseFlow) + .end() + .also { listeners.forEach(it::listener) } + .build() + } } diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/tasklets/delay/DelayTasklet.kt b/api/src/main/kotlin/nebulosa/api/sequencer/tasklets/delay/DelayTasklet.kt index 3d35c60a3..48770a73e 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 @@ -18,8 +18,6 @@ data class DelayTasklet(val duration: Duration) : PublishSequenceTasklet Duration.ZERO) { - aborted.set(false) - while (!aborted.get() && remainingTime > Duration.ZERO) { val waitTime = minOf(remainingTime, DELAY_INTERVAL) diff --git a/api/src/main/resources/db/migration/beforeMigrate.sql b/api/src/main/resources/db/migration/beforeMigrate.sql index a11aaf1f6..e237c8ab0 100644 --- a/api/src/main/resources/db/migration/beforeMigrate.sql +++ b/api/src/main/resources/db/migration/beforeMigrate.sql @@ -3,6 +3,7 @@ DROP TABLE IF EXISTS BATCH_JOB_EXECUTION_PARAMS; DROP TABLE IF EXISTS BATCH_JOB_EXECUTION; DROP TABLE IF EXISTS BATCH_JOB_EXECUTION_SEQ; DROP TABLE IF EXISTS BATCH_JOB_INSTANCE; +DROP TABLE IF EXISTS BATCH_JOB_SEQ; DROP TABLE IF EXISTS BATCH_STEP_EXECUTION_CONTEXT; DROP TABLE IF EXISTS BATCH_STEP_EXECUTION; DROP TABLE IF EXISTS BATCH_STEP_EXECUTION_SEQ; diff --git a/build.gradle.kts b/build.gradle.kts index e0d5c7a60..992473caf 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,8 +5,8 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile buildscript { dependencies { - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.21") - classpath("org.jetbrains.kotlin:kotlin-allopen:1.9.21") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.0.0-Beta1") + classpath("org.jetbrains.kotlin:kotlin-allopen:2.0.0-Beta1") classpath("com.adarshr:gradle-test-logger-plugin:4.0.0") } diff --git a/gradle.properties b/gradle.properties index 417ed3209..4eb978215 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,5 +2,3 @@ org.gradle.caching=true org.gradle.parallel=true org.gradle.jvmargs=-XX:MaxMetaspaceSize=1024m -Xmx2048m version.code=0.1.0 -kotlin.experimental.tryK2=true -kapt.use.k2=true From c985e453f4dfd1b9436012b3bf3436e4fc0d6ce7 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Fri, 8 Dec 2023 00:12:56 -0300 Subject: [PATCH 04/87] [api]: Use SQLite in-memory database for batch processing --- api/build.gradle.kts | 1 + .../configurations/BatchConfiguration.kt | 52 +++++++++++ .../beans/configurations/BeanConfiguration.kt | 10 -- .../configurations/DataSourceConfiguration.kt | 92 +++++++++++++++++++ .../api/cameras/CameraCaptureExecutor.kt | 4 +- api/src/main/resources/application.yml | 2 - settings.gradle.kts | 1 + 7 files changed, 148 insertions(+), 14 deletions(-) create mode 100644 api/src/main/kotlin/nebulosa/api/beans/configurations/BatchConfiguration.kt create mode 100644 api/src/main/kotlin/nebulosa/api/beans/configurations/DataSourceConfiguration.kt diff --git a/api/build.gradle.kts b/api/build.gradle.kts index 7b3e29349..52377a141 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -33,6 +33,7 @@ dependencies { implementation(libs.oshi) implementation(libs.rx) implementation(libs.sqlite) + implementation(libs.hikari) implementation("org.springframework.boot:spring-boot-starter") implementation("org.springframework.boot:spring-boot-starter-web") { exclude(module = "spring-boot-starter-tomcat") diff --git a/api/src/main/kotlin/nebulosa/api/beans/configurations/BatchConfiguration.kt b/api/src/main/kotlin/nebulosa/api/beans/configurations/BatchConfiguration.kt new file mode 100644 index 000000000..c60ed06a6 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/beans/configurations/BatchConfiguration.kt @@ -0,0 +1,52 @@ +package nebulosa.api.beans.configurations + +import nebulosa.common.concurrency.DaemonThreadFactory +import nebulosa.log.loggerFor +import org.springframework.batch.core.configuration.support.DefaultBatchConfiguration +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.boot.autoconfigure.batch.BatchDataSourceScriptDatabaseInitializer +import org.springframework.boot.autoconfigure.batch.BatchProperties +import org.springframework.boot.context.properties.ConfigurationProperties +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.core.task.SimpleAsyncTaskExecutor +import org.springframework.core.task.TaskExecutor +import org.springframework.transaction.PlatformTransactionManager +import javax.sql.DataSource + +@Configuration +class BatchConfiguration( + @Qualifier("batchDataSource") private val batchDataSource: DataSource, + @Qualifier("batchTransactionManager") private val batchTransactionManager: PlatformTransactionManager, +) : DefaultBatchConfiguration() { + + override fun getDataSource(): DataSource { + return batchDataSource + } + + override fun getTransactionManager(): PlatformTransactionManager { + return batchTransactionManager + } + + override fun getTaskExecutor(): TaskExecutor { + return SimpleAsyncTaskExecutor(DaemonThreadFactory) + } + + @Bean + @ConfigurationProperties(prefix = "spring.batch") + fun batchProperties(): BatchProperties { + return BatchProperties() + } + + @Bean + fun batchDataSourceScriptDatabaseInitializer(batchProperties: BatchProperties): BatchDataSourceScriptDatabaseInitializer { + val initializer = BatchDataSourceScriptDatabaseInitializer(batchDataSource, batchProperties.jdbc) + LOG.info("batch database initialized: {}", initializer.initializeDatabase()) + return initializer + } + + companion object { + + @JvmStatic private val LOG = loggerFor() + } +} diff --git a/api/src/main/kotlin/nebulosa/api/beans/configurations/BeanConfiguration.kt b/api/src/main/kotlin/nebulosa/api/beans/configurations/BeanConfiguration.kt index b5afd2a0b..7fb822294 100644 --- a/api/src/main/kotlin/nebulosa/api/beans/configurations/BeanConfiguration.kt +++ b/api/src/main/kotlin/nebulosa/api/beans/configurations/BeanConfiguration.kt @@ -120,16 +120,6 @@ class BeanConfiguration { .executorService(systemExecutorService) .installDefaultEventBus()!! - @Bean - @Primary - fun asyncJobLauncher(jobRepository: JobRepository): JobLauncher { - val jobLauncher = TaskExecutorJobLauncher() - jobLauncher.setJobRepository(jobRepository) - jobLauncher.setTaskExecutor(SimpleAsyncTaskExecutor(DaemonThreadFactory)) - jobLauncher.afterPropertiesSet() - return jobLauncher - } - @Bean fun flowIncrementer() = Incrementer() diff --git a/api/src/main/kotlin/nebulosa/api/beans/configurations/DataSourceConfiguration.kt b/api/src/main/kotlin/nebulosa/api/beans/configurations/DataSourceConfiguration.kt new file mode 100644 index 000000000..ec2141ecb --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/beans/configurations/DataSourceConfiguration.kt @@ -0,0 +1,92 @@ +package nebulosa.api.beans.configurations + +import com.zaxxer.hikari.HikariConfig +import com.zaxxer.hikari.HikariDataSource +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.boot.context.properties.ConfigurationProperties +import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Primary +import org.springframework.data.jpa.repository.config.EnableJpaRepositories +import org.springframework.jdbc.datasource.DataSourceTransactionManager +import org.springframework.jdbc.datasource.DriverManagerDataSource +import org.springframework.transaction.PlatformTransactionManager +import javax.sql.DataSource + +@Configuration +class DataSourceConfiguration { + + @Bean + @Primary + @ConfigurationProperties(prefix = "spring.datasource") + fun dataSource(): DataSource { + return DriverManagerDataSource() + } + + @Bean("batchDataSource") + fun batchDataSource(): DataSource { + val config = HikariConfig() + config.jdbcUrl = JDBC_MEMORY_URL + config.driverClassName = DRIVER_CLASS_NAME + config.maximumPoolSize = 1 + config.minimumIdle = 1 + return HikariDataSource(config) + } + + @Configuration + @EnableJpaRepositories( + basePackages = ["nebulosa.api"], + entityManagerFactoryRef = "entityManagerFactory", + transactionManagerRef = "transactionManager" + ) + class Main { + + @Primary + @Bean(name = ["entityManagerFactory"]) + fun entityManagerFactory( + builder: EntityManagerFactoryBuilder, + dataSource: DataSource, + ) = builder + .dataSource(dataSource) + .packages("nebulosa.api") + .persistenceUnit("mainPersistenceUnit") + .build()!! + + @Bean + @Primary + fun transactionManager(dataSource: DataSource): PlatformTransactionManager { + return DataSourceTransactionManager(dataSource) + } + } + + @Configuration + @EnableJpaRepositories( + basePackages = ["org.springframework.batch.core.migration"], + entityManagerFactoryRef = "batchEntityManagerFactory", + transactionManagerRef = "batchTransactionManager" + ) + class Batch { + + @Bean(name = ["batchEntityManagerFactory"]) + fun batchEntityManagerFactory( + builder: EntityManagerFactoryBuilder, + @Qualifier("batchDataSource") dataSource: DataSource, + ) = builder + .dataSource(dataSource) + .packages("org.springframework.batch.core.migration") + .persistenceUnit("batchPersistenceUnit") + .build()!! + + @Bean + fun batchTransactionManager(@Qualifier("batchDataSource") dataSource: DataSource): PlatformTransactionManager { + return DataSourceTransactionManager(dataSource) + } + } + + companion object { + + const val DRIVER_CLASS_NAME = "org.sqlite.JDBC" + const val JDBC_MEMORY_URL = "jdbc:sqlite:file:nebulosa.db?mode=memory" + } +} diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureExecutor.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureExecutor.kt index 7c1cf3dc7..ceca9b7ad 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureExecutor.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureExecutor.kt @@ -16,7 +16,7 @@ import java.util.* @Component class CameraCaptureExecutor( private val jobOperator: JobOperator, - private val asyncJobLauncher: JobLauncher, + private val jobLauncher: JobLauncher, private val messageService: MessageService, private val sequenceJobFactory: SequenceJobFactory, ) : SequenceJobExecutor, Consumer { @@ -38,7 +38,7 @@ class CameraCaptureExecutor( sequenceJobFactory.cameraCapture(request, this) } - return asyncJobLauncher + return jobLauncher .run(cameraCaptureJob, JobParameters()) .let { CameraSequenceJob(camera, request, cameraCaptureJob, it) } .also(runningSequenceJobs::add) diff --git a/api/src/main/resources/application.yml b/api/src/main/resources/application.yml index efe1ceb95..3a354b379 100644 --- a/api/src/main/resources/application.yml +++ b/api/src/main/resources/application.yml @@ -13,8 +13,6 @@ spring: datasource: url: jdbc:sqlite:file:${DATA_PATH}/nebulosa.db - username: - password: driverClassName: org.sqlite.JDBC jpa: diff --git a/settings.gradle.kts b/settings.gradle.kts index 1678b7403..5d28e1652 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -35,6 +35,7 @@ dependencyResolutionManagement { library("sqlite", "org.xerial:sqlite-jdbc:3.44.1.0") library("flyway", "org.flywaydb:flyway-core:9.22.3") library("jna", "net.java.dev.jna:jna:5.13.0") + library("hikari", "com.zaxxer:HikariCP:5.1.0") library("kotest-assertions-core", "io.kotest:kotest-assertions-core:5.8.0") library("kotest-runner-junit5", "io.kotest:kotest-runner-junit5:5.8.0") bundle("kotest", listOf("kotest-assertions-core", "kotest-runner-junit5")) From e7ccc1010a91d76653e972ce380e94547445bcb0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 Dec 2023 03:52:03 +0000 Subject: [PATCH 05/87] [api]: Bump ch.qos.logback:logback-classic from 1.4.13 to 1.4.14 Bumps [ch.qos.logback:logback-classic](https://github.com/qos-ch/logback) from 1.4.13 to 1.4.14. - [Commits](https://github.com/qos-ch/logback/compare/v_1.4.13...v_1.4.14) --- updated-dependencies: - dependency-name: ch.qos.logback:logback-classic dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- settings.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.gradle.kts b/settings.gradle.kts index 1678b7403..6962eac11 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -21,7 +21,7 @@ dependencyResolutionManagement { 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.8") - library("logback", "ch.qos.logback:logback-classic:1.4.13") + library("logback", "ch.qos.logback:logback-classic:1.4.14") library("eventbus", "org.greenrobot:eventbus-java:3.3.1") library("netty-transport", "io.netty:netty-transport:4.1.101.Final") library("netty-codec", "io.netty:netty-codec:4.1.101.Final") From 6bfaccaf105239d8cbb78aaf1d27b059c65fd5c8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 Dec 2023 04:02:58 +0000 Subject: [PATCH 06/87] [desktop]: Bump chart.js from 4.4.0 to 4.4.1 in /desktop Bumps [chart.js](https://github.com/chartjs/Chart.js) from 4.4.0 to 4.4.1. - [Release notes](https://github.com/chartjs/Chart.js/releases) - [Commits](https://github.com/chartjs/Chart.js/compare/v4.4.0...v4.4.1) --- updated-dependencies: - dependency-name: chart.js dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- desktop/package-lock.json | 8 ++++---- desktop/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/desktop/package-lock.json b/desktop/package-lock.json index 23895af40..6232d6448 100644 --- a/desktop/package-lock.json +++ b/desktop/package-lock.json @@ -22,7 +22,7 @@ "@angular/router": "17.0.5", "@fontsource/roboto": "5.0.8", "@mdi/font": "7.3.67", - "chart.js": "4.4.0", + "chart.js": "4.4.1", "chartjs-plugin-zoom": "2.0.1", "interactjs": "1.10.23", "leaflet": "1.9.4", @@ -5928,9 +5928,9 @@ "dev": true }, "node_modules/chart.js": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.0.tgz", - "integrity": "sha512-vQEj6d+z0dcsKLlQvbKIMYFHd3t8W/7L2vfJIbYcfyPcRx92CsHqECpueN8qVGNlKyDcr5wBrYAYKnfu/9Q1hQ==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.1.tgz", + "integrity": "sha512-C74QN1bxwV1v2PEujhmKjOZ7iUM4w6BWs23Md/6aOZZSlwMzeCIDGuZay++rBgChYru7/+QFeoQW0fQoP534Dg==", "dependencies": { "@kurkle/color": "^0.3.0" }, diff --git a/desktop/package.json b/desktop/package.json index 98712052e..90304abbb 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -42,7 +42,7 @@ "@angular/router": "17.0.5", "@fontsource/roboto": "5.0.8", "@mdi/font": "7.3.67", - "chart.js": "4.4.0", + "chart.js": "4.4.1", "chartjs-plugin-zoom": "2.0.1", "interactjs": "1.10.23", "leaflet": "1.9.4", From 702c4bb638a57df58c3e603b9cf0ecdbda96a59c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 Dec 2023 04:01:52 +0000 Subject: [PATCH 07/87] [desktop]: Bump interactjs from 1.10.23 to 1.10.26 in /desktop Bumps [interactjs](https://github.com/taye/interact.js) from 1.10.23 to 1.10.26. - [Changelog](https://github.com/taye/interact.js/blob/main/CHANGELOG.md) - [Commits](https://github.com/taye/interact.js/compare/v1.10.23...v1.10.26) --- updated-dependencies: - dependency-name: interactjs dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- desktop/package-lock.json | 16 ++++++++-------- desktop/package.json | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/desktop/package-lock.json b/desktop/package-lock.json index 6232d6448..b23151a68 100644 --- a/desktop/package-lock.json +++ b/desktop/package-lock.json @@ -24,7 +24,7 @@ "@mdi/font": "7.3.67", "chart.js": "4.4.1", "chartjs-plugin-zoom": "2.0.1", - "interactjs": "1.10.23", + "interactjs": "1.10.26", "leaflet": "1.9.4", "moment": "2.29.4", "panzoom": "9.4.3", @@ -3001,9 +3001,9 @@ } }, "node_modules/@interactjs/types": { - "version": "1.10.23", - "resolved": "https://registry.npmjs.org/@interactjs/types/-/types-1.10.23.tgz", - "integrity": "sha512-8/s1gFVNW60SqFLiFQDsvJuuzICthzyOu52bu8MhLFsxFnhVfng1xzjxi2+UokQULsp0WgBsctIS9bF7se9nJQ==" + "version": "1.10.26", + "resolved": "https://registry.npmjs.org/@interactjs/types/-/types-1.10.26.tgz", + "integrity": "sha512-DekYpdkMV3XJVd/0k3f4pJluZAsCiG86yEtVXvGLK0lS/Fj0+OzYEv7HoMpcBZSkQ8s7//yaeEBgnxy2tV81lA==" }, "node_modules/@isaacs/cliui": { "version": "8.0.2", @@ -9855,11 +9855,11 @@ } }, "node_modules/interactjs": { - "version": "1.10.23", - "resolved": "https://registry.npmjs.org/interactjs/-/interactjs-1.10.23.tgz", - "integrity": "sha512-ZnxfYh4QBnWnnCXVOVHEU4r2w01EQMTsLCd71n0mpsItFhV7S/jXycvzgsNvf5I99trBRRwP8RJXU8oy4hRFEw==", + "version": "1.10.26", + "resolved": "https://registry.npmjs.org/interactjs/-/interactjs-1.10.26.tgz", + "integrity": "sha512-5gNTNDTfEHp2EifqtWGi5VkD3CMZVJSTGmtK/IsVRd+rkOk3E63iVs5Z+IeD5K1Lr0qZpU2754VHAwf5i+Z9xg==", "dependencies": { - "@interactjs/types": "1.10.23" + "@interactjs/types": "1.10.26" } }, "node_modules/internal-slot": { diff --git a/desktop/package.json b/desktop/package.json index 90304abbb..a892dae11 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -44,7 +44,7 @@ "@mdi/font": "7.3.67", "chart.js": "4.4.1", "chartjs-plugin-zoom": "2.0.1", - "interactjs": "1.10.23", + "interactjs": "1.10.26", "leaflet": "1.9.4", "moment": "2.29.4", "panzoom": "9.4.3", From 764b4ed58ace8eb1a59fe89d8f95c497c655729e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 Dec 2023 04:02:17 +0000 Subject: [PATCH 08/87] [desktop]: Bump @types/node from 20.10.1 to 20.10.4 in /desktop Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 20.10.1 to 20.10.4. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) --- updated-dependencies: - dependency-name: "@types/node" dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- desktop/package-lock.json | 8 ++++---- desktop/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/desktop/package-lock.json b/desktop/package-lock.json index b23151a68..288bd8d85 100644 --- a/desktop/package-lock.json +++ b/desktop/package-lock.json @@ -42,7 +42,7 @@ "@angular/cli": "17.0.5", "@angular/compiler-cli": "17.0.5", "@types/leaflet": "1.9.8", - "@types/node": "20.10.1", + "@types/node": "20.10.4", "@types/uuid": "9.0.7", "electron": "27.1.3", "electron-builder": "24.9.1", @@ -3943,9 +3943,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.10.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.1.tgz", - "integrity": "sha512-T2qwhjWwGH81vUEx4EXmBKsTJRXFXNZTL4v0gi01+zyBmCwzE6TyHszqX01m+QHTEq+EZNo13NeJIdEqf+Myrg==", + "version": "20.10.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.4.tgz", + "integrity": "sha512-D08YG6rr8X90YB56tSIuBaddy/UXAA9RKJoFvrsnogAum/0pmjkgi4+2nx96A330FmioegBWmEYQ+syqCFaveg==", "dev": true, "dependencies": { "undici-types": "~5.26.4" diff --git a/desktop/package.json b/desktop/package.json index a892dae11..290f6ad5a 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -62,7 +62,7 @@ "@angular/cli": "17.0.5", "@angular/compiler-cli": "17.0.5", "@types/leaflet": "1.9.8", - "@types/node": "20.10.1", + "@types/node": "20.10.4", "@types/uuid": "9.0.7", "electron": "27.1.3", "electron-builder": "24.9.1", From b715d510163a7f872c52328ea82da17a7cf106cf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 Dec 2023 04:02:04 +0000 Subject: [PATCH 09/87] [desktop]: Bump primeng from 17.0.0-rc.1 to 17.0.0 in /desktop Bumps [primeng](https://github.com/primefaces/primeng) from 17.0.0-rc.1 to 17.0.0. - [Release notes](https://github.com/primefaces/primeng/releases) - [Changelog](https://github.com/primefaces/primeng/blob/master/CHANGELOG.md) - [Commits](https://github.com/primefaces/primeng/compare/17.0.0-rc.1...17.0.0) --- updated-dependencies: - dependency-name: primeng dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- desktop/package-lock.json | 8 ++++---- desktop/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/desktop/package-lock.json b/desktop/package-lock.json index 288bd8d85..4dae09cea 100644 --- a/desktop/package-lock.json +++ b/desktop/package-lock.json @@ -30,7 +30,7 @@ "panzoom": "9.4.3", "primeflex": "3.3.1", "primeicons": "6.0.1", - "primeng": "17.0.0-rc.1", + "primeng": "17.0.0", "rxjs": "7.8.1", "tslib": "2.6.2", "uuid": "9.0.1", @@ -13273,9 +13273,9 @@ "integrity": "sha512-KDeO94CbWI4pKsPnYpA1FPjo79EsY9I+M8ywoPBSf9XMXoe/0crjbUK7jcQEDHuc0ZMRIZsxH3TYLv4TUtHmAA==" }, "node_modules/primeng": { - "version": "17.0.0-rc.1", - "resolved": "https://registry.npmjs.org/primeng/-/primeng-17.0.0-rc.1.tgz", - "integrity": "sha512-S1z7DnP2p3Ia6KmBICHXsrxAlSVIiIyu6jSEKnuQTrUiTWMsaJnZX8IMHp+d21f8Q35HsS65etxyqxt8wn/MVQ==", + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/primeng/-/primeng-17.0.0.tgz", + "integrity": "sha512-6ja5koKWaENKr2C1o8N5xqRII/yA0Byy9AHeb25f4vQ9gEivkRit8O9tqoiaG9fncZUL8gLVjbImUtZj2kw4gQ==", "dependencies": { "tslib": "^2.3.0" }, diff --git a/desktop/package.json b/desktop/package.json index 290f6ad5a..6aaed2f5b 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -50,7 +50,7 @@ "panzoom": "9.4.3", "primeflex": "3.3.1", "primeicons": "6.0.1", - "primeng": "17.0.0-rc.1", + "primeng": "17.0.0", "rxjs": "7.8.1", "tslib": "2.6.2", "uuid": "9.0.1", From fa9ac97fed27cf0bbaf1b608079d0aeb0004656d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 Dec 2023 12:11:26 +0000 Subject: [PATCH 10/87] [desktop]: Bump electron from 27.1.3 to 28.0.0 in /desktop Bumps [electron](https://github.com/electron/electron) from 27.1.3 to 28.0.0. - [Release notes](https://github.com/electron/electron/releases) - [Changelog](https://github.com/electron/electron/blob/main/docs/breaking-changes.md) - [Commits](https://github.com/electron/electron/compare/v27.1.3...v28.0.0) --- updated-dependencies: - dependency-name: electron dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- desktop/package-lock.json | 8 ++++---- desktop/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/desktop/package-lock.json b/desktop/package-lock.json index 4dae09cea..583a03eb9 100644 --- a/desktop/package-lock.json +++ b/desktop/package-lock.json @@ -44,7 +44,7 @@ "@types/leaflet": "1.9.8", "@types/node": "20.10.4", "@types/uuid": "9.0.7", - "electron": "27.1.3", + "electron": "28.0.0", "electron-builder": "24.9.1", "electron-debug": "3.2.0", "electron-reloader": "1.2.3", @@ -7385,9 +7385,9 @@ } }, "node_modules/electron": { - "version": "27.1.3", - "resolved": "https://registry.npmjs.org/electron/-/electron-27.1.3.tgz", - "integrity": "sha512-7eD8VMhhlL5J531OOawn00eMthUkX1e3qN5Nqd7eMK8bg5HxQBrn8bdPlvUEnCano9KhrVwaDnGeuzWoDOGpjQ==", + "version": "28.0.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-28.0.0.tgz", + "integrity": "sha512-eDhnCFBvG0PGFVEpNIEdBvyuGUBsFdlokd+CtuCe2ER3P+17qxaRfWRxMmksCOKgDHb5Wif5UxqOkZSlA4snlw==", "dev": true, "hasInstallScript": true, "dependencies": { diff --git a/desktop/package.json b/desktop/package.json index 6aaed2f5b..69507d692 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -64,7 +64,7 @@ "@types/leaflet": "1.9.8", "@types/node": "20.10.4", "@types/uuid": "9.0.7", - "electron": "27.1.3", + "electron": "28.0.0", "electron-builder": "24.9.1", "electron-debug": "3.2.0", "electron-reloader": "1.2.3", From fa57b9f0ba0e993e42496e7b71557b343a73b54e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 Dec 2023 04:03:10 +0000 Subject: [PATCH 11/87] [desktop]: Bump node-polyfill-webpack-plugin in /desktop Bumps [node-polyfill-webpack-plugin](https://github.com/Richienb/node-polyfill-webpack-plugin) from 2.0.1 to 3.0.0. - [Release notes](https://github.com/Richienb/node-polyfill-webpack-plugin/releases) - [Commits](https://github.com/Richienb/node-polyfill-webpack-plugin/compare/v2.0.1...v3.0.0) --- updated-dependencies: - dependency-name: node-polyfill-webpack-plugin dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- desktop/package-lock.json | 40 +++++++++++++++------------------------ desktop/package.json | 2 +- 2 files changed, 16 insertions(+), 26 deletions(-) diff --git a/desktop/package-lock.json b/desktop/package-lock.json index 583a03eb9..dfbc67f4e 100644 --- a/desktop/package-lock.json +++ b/desktop/package-lock.json @@ -48,7 +48,7 @@ "electron-builder": "24.9.1", "electron-debug": "3.2.0", "electron-reloader": "1.2.3", - "node-polyfill-webpack-plugin": "2.0.1", + "node-polyfill-webpack-plugin": "3.0.0", "npm-run-all": "4.1.5", "ts-node": "10.9.1", "typescript": "5.2.2", @@ -8703,15 +8703,6 @@ "node": ">=8" } }, - "node_modules/filter-obj": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-2.0.2.tgz", - "integrity": "sha512-lO3ttPjHZRfjMcxWKb1j1eDhTFsu4meeR3lnMcnBFhk6RuLhvEiuALu2TlfL310ph4lCYYwgF/ElIjdP739tdg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/finalhandler": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz", @@ -11915,12 +11906,12 @@ } }, "node_modules/node-polyfill-webpack-plugin": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/node-polyfill-webpack-plugin/-/node-polyfill-webpack-plugin-2.0.1.tgz", - "integrity": "sha512-ZUMiCnZkP1LF0Th2caY6J/eKKoA0TefpoVa68m/LQU1I/mE8rGt4fNYGgNuCcK+aG8P8P43nbeJ2RqJMOL/Y1A==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/node-polyfill-webpack-plugin/-/node-polyfill-webpack-plugin-3.0.0.tgz", + "integrity": "sha512-QpG496dDBiaelQZu9wDcVvpLbtk7h9Ctz693RaUMZBgl8DUoFToO90ZTLKq57gP7rwKqYtGbMBXkcEgLSag2jQ==", "dev": true, "dependencies": { - "assert": "^2.0.0", + "assert": "^2.1.0", "browserify-zlib": "^0.2.0", "buffer": "^6.0.3", "console-browserify": "^1.2.0", @@ -11928,26 +11919,25 @@ "crypto-browserify": "^3.12.0", "domain-browser": "^4.22.0", "events": "^3.3.0", - "filter-obj": "^2.0.2", "https-browserify": "^1.0.0", "os-browserify": "^0.3.0", "path-browserify": "^1.0.1", "process": "^0.11.10", - "punycode": "^2.1.1", + "punycode": "^2.3.0", "querystring-es3": "^0.2.1", - "readable-stream": "^4.0.0", + "readable-stream": "^4.4.2", "stream-browserify": "^3.0.0", "stream-http": "^3.2.0", "string_decoder": "^1.3.0", "timers-browserify": "^2.0.12", "tty-browserify": "^0.0.1", - "type-fest": "^2.14.0", - "url": "^0.11.0", - "util": "^0.12.4", + "type-fest": "^4.4.0", + "url": "^0.11.3", + "util": "^0.12.5", "vm-browserify": "^1.1.2" }, "engines": { - "node": ">=12" + "node": ">=14" }, "peerDependencies": { "webpack": ">=5" @@ -11978,12 +11968,12 @@ } }, "node_modules/node-polyfill-webpack-plugin/node_modules/type-fest": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", - "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.8.3.tgz", + "integrity": "sha512-//BaTm14Q/gHBn09xlnKNqfI8t6bmdzx2DXYfPBNofN0WUybCEUDcbCWcTa0oF09lzLjZgPphXAsvRiMK0V6Bw==", "dev": true, "engines": { - "node": ">=12.20" + "node": ">=16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" diff --git a/desktop/package.json b/desktop/package.json index 69507d692..817093043 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -68,7 +68,7 @@ "electron-builder": "24.9.1", "electron-debug": "3.2.0", "electron-reloader": "1.2.3", - "node-polyfill-webpack-plugin": "2.0.1", + "node-polyfill-webpack-plugin": "3.0.0", "npm-run-all": "4.1.5", "ts-node": "10.9.1", "typescript": "5.2.2", From bcd840bfe6b8866a7b0e2d94e5117326cb584c1c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 Dec 2023 12:17:11 +0000 Subject: [PATCH 12/87] [desktop]: Bump the angular group in /desktop with 13 updates Bumps the angular group in /desktop with 13 updates: | Package | From | To | | --- | --- | --- | | [@angular/animations](https://github.com/angular/angular/tree/HEAD/packages/animations) | `17.0.5` | `17.0.6` | | [@angular/cdk](https://github.com/angular/components) | `17.0.1` | `17.0.3` | | [@angular/common](https://github.com/angular/angular/tree/HEAD/packages/common) | `17.0.5` | `17.0.6` | | [@angular/compiler](https://github.com/angular/angular/tree/HEAD/packages/compiler) | `17.0.5` | `17.0.6` | | [@angular/core](https://github.com/angular/angular/tree/HEAD/packages/core) | `17.0.5` | `17.0.6` | | [@angular/forms](https://github.com/angular/angular/tree/HEAD/packages/forms) | `17.0.5` | `17.0.6` | | [@angular/language-service](https://github.com/angular/angular/tree/HEAD/packages/language-service) | `17.0.5` | `17.0.6` | | [@angular/platform-browser](https://github.com/angular/angular/tree/HEAD/packages/platform-browser) | `17.0.5` | `17.0.6` | | [@angular/platform-browser-dynamic](https://github.com/angular/angular/tree/HEAD/packages/platform-browser-dynamic) | `17.0.5` | `17.0.6` | | [@angular/router](https://github.com/angular/angular/tree/HEAD/packages/router) | `17.0.5` | `17.0.6` | | [@angular-devkit/build-angular](https://github.com/angular/angular-cli) | `17.0.5` | `17.0.6` | | [@angular/cli](https://github.com/angular/angular-cli) | `17.0.5` | `17.0.6` | | [@angular/compiler-cli](https://github.com/angular/angular/tree/HEAD/packages/compiler-cli) | `17.0.5` | `17.0.6` | Updates `@angular/animations` from 17.0.5 to 17.0.6 - [Release notes](https://github.com/angular/angular/releases) - [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md) - [Commits](https://github.com/angular/angular/commits/17.0.6/packages/animations) Updates `@angular/cdk` from 17.0.1 to 17.0.3 - [Release notes](https://github.com/angular/components/releases) - [Changelog](https://github.com/angular/components/blob/main/CHANGELOG.md) - [Commits](https://github.com/angular/components/compare/17.0.1...17.0.3) Updates `@angular/common` from 17.0.5 to 17.0.6 - [Release notes](https://github.com/angular/angular/releases) - [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md) - [Commits](https://github.com/angular/angular/commits/17.0.6/packages/common) Updates `@angular/compiler` from 17.0.5 to 17.0.6 - [Release notes](https://github.com/angular/angular/releases) - [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md) - [Commits](https://github.com/angular/angular/commits/17.0.6/packages/compiler) Updates `@angular/core` from 17.0.5 to 17.0.6 - [Release notes](https://github.com/angular/angular/releases) - [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md) - [Commits](https://github.com/angular/angular/commits/17.0.6/packages/core) Updates `@angular/forms` from 17.0.5 to 17.0.6 - [Release notes](https://github.com/angular/angular/releases) - [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md) - [Commits](https://github.com/angular/angular/commits/17.0.6/packages/forms) Updates `@angular/language-service` from 17.0.5 to 17.0.6 - [Release notes](https://github.com/angular/angular/releases) - [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md) - [Commits](https://github.com/angular/angular/commits/17.0.6/packages/language-service) Updates `@angular/platform-browser` from 17.0.5 to 17.0.6 - [Release notes](https://github.com/angular/angular/releases) - [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md) - [Commits](https://github.com/angular/angular/commits/17.0.6/packages/platform-browser) Updates `@angular/platform-browser-dynamic` from 17.0.5 to 17.0.6 - [Release notes](https://github.com/angular/angular/releases) - [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md) - [Commits](https://github.com/angular/angular/commits/17.0.6/packages/platform-browser-dynamic) Updates `@angular/router` from 17.0.5 to 17.0.6 - [Release notes](https://github.com/angular/angular/releases) - [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md) - [Commits](https://github.com/angular/angular/commits/17.0.6/packages/router) Updates `@angular-devkit/build-angular` from 17.0.5 to 17.0.6 - [Release notes](https://github.com/angular/angular-cli/releases) - [Changelog](https://github.com/angular/angular-cli/blob/main/CHANGELOG.md) - [Commits](https://github.com/angular/angular-cli/compare/17.0.5...17.0.6) Updates `@angular/cli` from 17.0.5 to 17.0.6 - [Release notes](https://github.com/angular/angular-cli/releases) - [Changelog](https://github.com/angular/angular-cli/blob/main/CHANGELOG.md) - [Commits](https://github.com/angular/angular-cli/compare/17.0.5...17.0.6) Updates `@angular/compiler-cli` from 17.0.5 to 17.0.6 - [Release notes](https://github.com/angular/angular/releases) - [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md) - [Commits](https://github.com/angular/angular/commits/17.0.6/packages/compiler-cli) --- updated-dependencies: - dependency-name: "@angular/animations" dependency-type: direct:production update-type: version-update:semver-patch dependency-group: angular - dependency-name: "@angular/cdk" dependency-type: direct:production update-type: version-update:semver-patch dependency-group: angular - dependency-name: "@angular/common" dependency-type: direct:production update-type: version-update:semver-patch dependency-group: angular - dependency-name: "@angular/compiler" dependency-type: direct:production update-type: version-update:semver-patch dependency-group: angular - dependency-name: "@angular/core" dependency-type: direct:production update-type: version-update:semver-patch dependency-group: angular - dependency-name: "@angular/forms" dependency-type: direct:production update-type: version-update:semver-patch dependency-group: angular - dependency-name: "@angular/language-service" dependency-type: direct:production update-type: version-update:semver-patch dependency-group: angular - dependency-name: "@angular/platform-browser" dependency-type: direct:production update-type: version-update:semver-patch dependency-group: angular - dependency-name: "@angular/platform-browser-dynamic" dependency-type: direct:production update-type: version-update:semver-patch dependency-group: angular - dependency-name: "@angular/router" dependency-type: direct:production update-type: version-update:semver-patch dependency-group: angular - dependency-name: "@angular-devkit/build-angular" dependency-type: direct:development update-type: version-update:semver-patch dependency-group: angular - dependency-name: "@angular/cli" dependency-type: direct:development update-type: version-update:semver-patch dependency-group: angular - dependency-name: "@angular/compiler-cli" dependency-type: direct:development update-type: version-update:semver-patch dependency-group: angular ... Signed-off-by: dependabot[bot] --- desktop/package-lock.json | 228 +++++++++++++++++++------------------- desktop/package.json | 26 ++--- 2 files changed, 127 insertions(+), 127 deletions(-) diff --git a/desktop/package-lock.json b/desktop/package-lock.json index dfbc67f4e..a9c68faf6 100644 --- a/desktop/package-lock.json +++ b/desktop/package-lock.json @@ -10,16 +10,16 @@ "hasInstallScript": true, "license": "MIT", "dependencies": { - "@angular/animations": "17.0.5", - "@angular/cdk": "17.0.1", - "@angular/common": "17.0.5", - "@angular/compiler": "17.0.5", - "@angular/core": "17.0.5", - "@angular/forms": "17.0.5", - "@angular/language-service": "17.0.5", - "@angular/platform-browser": "17.0.5", - "@angular/platform-browser-dynamic": "17.0.5", - "@angular/router": "17.0.5", + "@angular/animations": "17.0.6", + "@angular/cdk": "17.0.3", + "@angular/common": "17.0.6", + "@angular/compiler": "17.0.6", + "@angular/core": "17.0.6", + "@angular/forms": "17.0.6", + "@angular/language-service": "17.0.6", + "@angular/platform-browser": "17.0.6", + "@angular/platform-browser-dynamic": "17.0.6", + "@angular/router": "17.0.6", "@fontsource/roboto": "5.0.8", "@mdi/font": "7.3.67", "chart.js": "4.4.1", @@ -38,9 +38,9 @@ }, "devDependencies": { "@angular-builders/custom-webpack": "17.0.0", - "@angular-devkit/build-angular": "17.0.5", - "@angular/cli": "17.0.5", - "@angular/compiler-cli": "17.0.5", + "@angular-devkit/build-angular": "17.0.6", + "@angular/cli": "17.0.6", + "@angular/compiler-cli": "17.0.6", "@types/leaflet": "1.9.8", "@types/node": "20.10.4", "@types/uuid": "9.0.7", @@ -93,12 +93,12 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.1700.5", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1700.5.tgz", - "integrity": "sha512-kPGiPzystxyLDj79Wy+wCZs5vzx6iUy6fjZ9dKFNS3M9T9UXoo8CZLJS0dWrgO/97M25MSgufyIEDmi+HvwZ7w==", + "version": "0.1700.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1700.6.tgz", + "integrity": "sha512-zVpz736cBZHXcv0v2bRLfJLcykanUyEMVQXkGwZp2eygjNK1Ls9s/74o1dXd6nGdvjh6AnkzOU/vouj2dqA41g==", "dev": true, "dependencies": { - "@angular-devkit/core": "17.0.5", + "@angular-devkit/core": "17.0.6", "rxjs": "7.8.1" }, "engines": { @@ -108,15 +108,15 @@ } }, "node_modules/@angular-devkit/build-angular": { - "version": "17.0.5", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-17.0.5.tgz", - "integrity": "sha512-45+DTM3F8OFlMFRxQRgTBXnfndysgiZXiqItiKmFFau7wENZiTijUuFMFjOIHlLXFDI1qs130hYE4YkPNFffxg==", + "version": "17.0.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-17.0.6.tgz", + "integrity": "sha512-gYxmbvq5/nk7aVJ6JxIIW0//RM7859kMPJGPKekcCGSWkkObjqG6P5cDgJJNAjMl/IfCsG7B+xGYjr4zN8QV9g==", "dev": true, "dependencies": { "@ampproject/remapping": "2.2.1", - "@angular-devkit/architect": "0.1700.5", - "@angular-devkit/build-webpack": "0.1700.5", - "@angular-devkit/core": "17.0.5", + "@angular-devkit/architect": "0.1700.6", + "@angular-devkit/build-webpack": "0.1700.6", + "@angular-devkit/core": "17.0.6", "@babel/core": "7.23.2", "@babel/generator": "7.23.0", "@babel/helper-annotate-as-pure": "7.22.5", @@ -127,7 +127,7 @@ "@babel/preset-env": "7.23.2", "@babel/runtime": "7.23.2", "@discoveryjs/json-ext": "0.5.7", - "@ngtools/webpack": "17.0.5", + "@ngtools/webpack": "17.0.6", "@vitejs/plugin-basic-ssl": "1.0.1", "ansi-colors": "4.1.3", "autoprefixer": "10.4.16", @@ -172,7 +172,7 @@ "tree-kill": "1.2.2", "tslib": "2.6.2", "undici": "5.27.2", - "vite": "4.5.0", + "vite": "4.5.1", "webpack": "5.89.0", "webpack-dev-middleware": "6.1.1", "webpack-dev-server": "4.15.1", @@ -231,12 +231,12 @@ } }, "node_modules/@angular-devkit/build-webpack": { - "version": "0.1700.5", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1700.5.tgz", - "integrity": "sha512-rLtDIK6je7JxhWG76aM8smfX13XHv+LlepwdK4lQqPEnz5BnkTfNFBnqwIWHA2eNUNTnVgeS356PxckZI3YL1g==", + "version": "0.1700.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1700.6.tgz", + "integrity": "sha512-xT5LL92rScVjvGZO7but/YbTQ12PNilosyjDouephl+HIf2V6rwDovTsEfpLYgcrqgodh+R0X0ZCOk95+MmSBA==", "dev": true, "dependencies": { - "@angular-devkit/architect": "0.1700.5", + "@angular-devkit/architect": "0.1700.6", "rxjs": "7.8.1" }, "engines": { @@ -250,9 +250,9 @@ } }, "node_modules/@angular-devkit/core": { - "version": "17.0.5", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.0.5.tgz", - "integrity": "sha512-e1evgRabAfOZBnmFCe8E0oufcu+FzBe5hBzS94Dm42GlxdX965/M4yVKQxIMpjivQTmjl+AWb6cF1ltBdSGZeQ==", + "version": "17.0.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.0.6.tgz", + "integrity": "sha512-+h9VnFHof7rKzBJ5FWrbPXWzbag31QKbUGJ/mV5BYgj39vjzPNUXBW8AaScZAlATi8+tElYXjRMvM49GnuyRLg==", "dev": true, "dependencies": { "ajv": "8.12.0", @@ -277,12 +277,12 @@ } }, "node_modules/@angular-devkit/schematics": { - "version": "17.0.5", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.0.5.tgz", - "integrity": "sha512-KYPku0qTb8B+TtRbFqXGYpJOPg1k6d5bNHV6n8jTc35mlEUUghOd7HkovdfkQ3cgGNQM56a74D1CvSeruZEGsA==", + "version": "17.0.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.0.6.tgz", + "integrity": "sha512-2g769MpazA1aOzJOm2MNGosra3kxw8CbdIQQOKkvycIzroRNgN06yHcRTDC03GADgP/CkDJ6kxwJQNG+wNFL2A==", "dev": true, "dependencies": { - "@angular-devkit/core": "17.0.5", + "@angular-devkit/core": "17.0.6", "jsonc-parser": "3.2.0", "magic-string": "0.30.5", "ora": "5.4.1", @@ -295,9 +295,9 @@ } }, "node_modules/@angular/animations": { - "version": "17.0.5", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-17.0.5.tgz", - "integrity": "sha512-NZ9Y3QWqrn0THypVNwsztMV9rnjxNMRIf6to8aZv+ehIUOvskqcA/lW5qAdcMr1uNoyloB9vahJrDniWWEKT5A==", + "version": "17.0.6", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-17.0.6.tgz", + "integrity": "sha512-fic61LjLHry79c5H9UGM8Ff311MJnf9an7EukLj2aLJ3J0uadL/H9de7dDp8PaIT10DX9g+aRTIKOmF3PmmXIQ==", "dependencies": { "tslib": "^2.3.0" }, @@ -305,13 +305,13 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/core": "17.0.5" + "@angular/core": "17.0.6" } }, "node_modules/@angular/cdk": { - "version": "17.0.1", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-17.0.1.tgz", - "integrity": "sha512-0hrXm2D0s0/vUtDoLFRWTs75k5WY/hQmfnsaJXHeqinbE3eKOxmQxL1i7ymnMSQthEWzgRAhzS3Nfs7Alw3dQA==", + "version": "17.0.3", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-17.0.3.tgz", + "integrity": "sha512-Qd5uvC09B3+uk2uX1JxmiWrD7wueMHSxNBoCbDEmnrsdDVUta0wN/jj/CtATljxUM8ZqvEvkqgxJCig1od9oyQ==", "dependencies": { "tslib": "^2.3.0" }, @@ -325,15 +325,15 @@ } }, "node_modules/@angular/cli": { - "version": "17.0.5", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-17.0.5.tgz", - "integrity": "sha512-IWtepjO1yTVGblbpTI7vtdxX5EjOYSL4BGa+3g85XuY6U2H38Bc9ZVBAYteAvRX1ZA2yvwJw068YY52ITlnr4A==", + "version": "17.0.6", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-17.0.6.tgz", + "integrity": "sha512-BLA2wDeqZManC/7MI6WvRRV+VhrwjxxB7FawLyp4xYlo0CTSOFOfeKPVRMLEnA/Ou4R5d47B+BqJTlep62pHwg==", "dev": true, "dependencies": { - "@angular-devkit/architect": "0.1700.5", - "@angular-devkit/core": "17.0.5", - "@angular-devkit/schematics": "17.0.5", - "@schematics/angular": "17.0.5", + "@angular-devkit/architect": "0.1700.6", + "@angular-devkit/core": "17.0.6", + "@angular-devkit/schematics": "17.0.6", + "@schematics/angular": "17.0.6", "@yarnpkg/lockfile": "1.1.0", "ansi-colors": "4.1.3", "ini": "4.1.1", @@ -359,9 +359,9 @@ } }, "node_modules/@angular/common": { - "version": "17.0.5", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-17.0.5.tgz", - "integrity": "sha512-1vFZ7nd8xyAYh/DwFtRuSieP8Dy/6QuOxl914/TOUr26F1a4e+7ywCyMLVjmYjx+WkZe7uu/Hgpr2raBaVTnQw==", + "version": "17.0.6", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-17.0.6.tgz", + "integrity": "sha512-FZtf8ol8W2V21ZDgFtcxmJ6JJKUO97QZ+wr/bosyYEryWMmn6VGrbOARhfW7BlrEgn14NdFkLb72KKtqoqRjrg==", "dependencies": { "tslib": "^2.3.0" }, @@ -369,14 +369,14 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/core": "17.0.5", + "@angular/core": "17.0.6", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/compiler": { - "version": "17.0.5", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-17.0.5.tgz", - "integrity": "sha512-V6LnX/B2YXpzXeNWavtX/XPNUnWrVUFpiOniKqHYhAxXnibhyXL9DRsyVs8QbKgIcPPcQeJMHdAjklCWJsePvg==", + "version": "17.0.6", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-17.0.6.tgz", + "integrity": "sha512-PaCNnlPcL0rvByKCBUUyLWkKJYXOrcfKlYYvcacjOzEUgZeEpekG81hMRb9u/Pz+A+M4HJSTmdgzwGP35zo8qw==", "dependencies": { "tslib": "^2.3.0" }, @@ -384,7 +384,7 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/core": "17.0.5" + "@angular/core": "17.0.6" }, "peerDependenciesMeta": { "@angular/core": { @@ -393,9 +393,9 @@ } }, "node_modules/@angular/compiler-cli": { - "version": "17.0.5", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-17.0.5.tgz", - "integrity": "sha512-Nb99iKz8LMoc5HC9iu5rbWblXb68sHHI6bcN8sdqvc2g+PohkGNbtRjVZFhP+WKMaNFYDSvLWcHFFYItLRkT4g==", + "version": "17.0.6", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-17.0.6.tgz", + "integrity": "sha512-C1Gfh9kbjYZezEMOwxnvUTHuPXa+6pk7mAfSj8e5oAO6E+wfo2dTxv1J5zxa3KYzxPYMNfF8OFvLuMKsw7lXjA==", "dev": true, "dependencies": { "@babel/core": "7.23.2", @@ -416,14 +416,14 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/compiler": "17.0.5", + "@angular/compiler": "17.0.6", "typescript": ">=5.2 <5.3" } }, "node_modules/@angular/core": { - "version": "17.0.5", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-17.0.5.tgz", - "integrity": "sha512-siWUrdBWgTAqMnRF+qxGZznj5AdR/x3+8l0/bj4CkSZzwZGL/CHy40ec71bbgiPkYob1v4v40voXu2aSSeCLPg==", + "version": "17.0.6", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-17.0.6.tgz", + "integrity": "sha512-QzfKRTDNgGOY9D5VxenUUz20cvPVC+uVw9xiqkDuHgGfLYVFlCAK9ymFYkdUCLTcVzJPxckP+spMpPX8nc4Aqw==", "dependencies": { "tslib": "^2.3.0" }, @@ -436,9 +436,9 @@ } }, "node_modules/@angular/forms": { - "version": "17.0.5", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-17.0.5.tgz", - "integrity": "sha512-d91Rre/NK+SgamF1OJmDJUx+Zs8M7qFmrKu7c+hNsXPe8J/fkMNoWFikne/WSsegwY929E1xpeqvu/KXQt90ug==", + "version": "17.0.6", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-17.0.6.tgz", + "integrity": "sha512-n/trsMtQHUBGiWz5lFaggMcMOuw0gH+96TCtHxQiUYJOdrbOemkFdGrNh3B4fGHmogWuOYJVF5FAm97WRES2XA==", "dependencies": { "tslib": "^2.3.0" }, @@ -446,24 +446,24 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/common": "17.0.5", - "@angular/core": "17.0.5", - "@angular/platform-browser": "17.0.5", + "@angular/common": "17.0.6", + "@angular/core": "17.0.6", + "@angular/platform-browser": "17.0.6", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/language-service": { - "version": "17.0.5", - "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-17.0.5.tgz", - "integrity": "sha512-tVXYamdjkAYYv4YCiMKxCYqLgvI/g2y2Ny6fUUVPti9xFqiF88q8V7j3N8FeLdSNvgok1LSdfFjJAgQonJ4Sxw==", + "version": "17.0.6", + "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-17.0.6.tgz", + "integrity": "sha512-HTJmnZeXFZoAJD8wvMN7QHuGd9KHsEQTdA7DeEDxqDneGM63bPVdRN6gSaai6abU1/8gfBNtSTfiwhHnCRTh0Q==", "engines": { "node": "^18.13.0 || >=20.9.0" } }, "node_modules/@angular/platform-browser": { - "version": "17.0.5", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-17.0.5.tgz", - "integrity": "sha512-VJQ6bVS40xJLNGNcX59/QFPrZesIm2zETOqAc6K04onuWF1EnJqvcDog9eYJsm0sLWhQeCdWVmAFRenTkDoqng==", + "version": "17.0.6", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-17.0.6.tgz", + "integrity": "sha512-nBhWH1MKT2WswgRNIoMnmNAt0n5/fG59BanJtodW71//Aj5aIE+BuVoFgK3wmO8IMoeP4i4GXRInBXs6lUMOJw==", "dependencies": { "tslib": "^2.3.0" }, @@ -471,9 +471,9 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/animations": "17.0.5", - "@angular/common": "17.0.5", - "@angular/core": "17.0.5" + "@angular/animations": "17.0.6", + "@angular/common": "17.0.6", + "@angular/core": "17.0.6" }, "peerDependenciesMeta": { "@angular/animations": { @@ -482,9 +482,9 @@ } }, "node_modules/@angular/platform-browser-dynamic": { - "version": "17.0.5", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-17.0.5.tgz", - "integrity": "sha512-Ki+0B3/S+Rv3O4jf+tbDBPs0m+VUMoS6VVCCLviaurYGPLPtGblhCzRv49Zoyo5gEVoEOgnxS6CI91Tv6My9ug==", + "version": "17.0.6", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-17.0.6.tgz", + "integrity": "sha512-5ZEmBtBkqamTaWjUXCls7G1f3xyK/ykXE7hnUV9CgGqXKrNkxblmbtOhoWdsbuIYjjdxQcAk1qtg/Rg21wcc4w==", "dependencies": { "tslib": "^2.3.0" }, @@ -492,16 +492,16 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/common": "17.0.5", - "@angular/compiler": "17.0.5", - "@angular/core": "17.0.5", - "@angular/platform-browser": "17.0.5" + "@angular/common": "17.0.6", + "@angular/compiler": "17.0.6", + "@angular/core": "17.0.6", + "@angular/platform-browser": "17.0.6" } }, "node_modules/@angular/router": { - "version": "17.0.5", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-17.0.5.tgz", - "integrity": "sha512-9e5MQJzDdfhXKSYrduIDmDf73GBRcjx6qE+k5CliGY4sFza10wdbrM4LkiuA3Z2Ja+2AKkotrGG3ZMCtAsFY1g==", + "version": "17.0.6", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-17.0.6.tgz", + "integrity": "sha512-xW6yDxREpBOB9MoODSfIw5HwkwLK+OgK34Q6sGYs0ft9UryMoFwft+pHGAaDz2nzhA72n+Ht9B2eai78UE9jGQ==", "dependencies": { "tslib": "^2.3.0" }, @@ -509,9 +509,9 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/common": "17.0.5", - "@angular/core": "17.0.5", - "@angular/platform-browser": "17.0.5", + "@angular/common": "17.0.6", + "@angular/core": "17.0.6", + "@angular/platform-browser": "17.0.6", "rxjs": "^6.5.3 || ^7.4.0" } }, @@ -3286,9 +3286,9 @@ "integrity": "sha512-SWxvzRbUQRfewlIV+OF4/YF4DkeTjMWoT8Hh9yeU/5UBVdJZj9Uf4a9+cXjknSIhIaMxZ/4N1O/s7ojApOOGjg==" }, "node_modules/@ngtools/webpack": { - "version": "17.0.5", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-17.0.5.tgz", - "integrity": "sha512-r82k7mxErJHtd6dzq0PKHQNhOuEjUZn95f2adJpO5mP/R/ms8LUk1ILvP3EocxkisYU8ET2EeGj3wQZC2g3RcA==", + "version": "17.0.6", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-17.0.6.tgz", + "integrity": "sha512-9Us20rqGhi8PmQBwQu6Qtww3WVV/gf2s2DbzcLclsiDtSBobzT64Z6F6E9OpAYD+c5PxlUaOghL6NXdnSNdByA==", "dev": true, "engines": { "node": "^18.13.0 || >=20.9.0", @@ -3562,13 +3562,13 @@ } }, "node_modules/@schematics/angular": { - "version": "17.0.5", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-17.0.5.tgz", - "integrity": "sha512-sOc1UG4NiV+7cGwrbWPnyW71O+NgsKaFb2agSrVduRL7o4neMDeqF04ik4Kv1jKA7sZOQfPV+3cn6XI49Mumrw==", + "version": "17.0.6", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-17.0.6.tgz", + "integrity": "sha512-AyC7Bk3Omy6PfADThhq5ci+zzdTTi2N1oZI35gw4tMK5ZxVwIACx2Zyhaz399m5c2RCDi9Hz4A2BOFq9f0j/dg==", "dev": true, "dependencies": { - "@angular-devkit/core": "17.0.5", - "@angular-devkit/schematics": "17.0.5", + "@angular-devkit/core": "17.0.6", + "@angular-devkit/schematics": "17.0.6", "jsonc-parser": "3.2.0" }, "engines": { @@ -5468,9 +5468,9 @@ } }, "node_modules/browserslist": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", - "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz", + "integrity": "sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==", "dev": true, "funding": [ { @@ -5487,9 +5487,9 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001541", - "electron-to-chromium": "^1.4.535", - "node-releases": "^2.0.13", + "caniuse-lite": "^1.0.30001565", + "electron-to-chromium": "^1.4.601", + "node-releases": "^2.0.14", "update-browserslist-db": "^1.0.13" }, "bin": { @@ -6471,12 +6471,12 @@ } }, "node_modules/core-js-compat": { - "version": "3.33.3", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.33.3.tgz", - "integrity": "sha512-cNzGqFsh3Ot+529GIXacjTJ7kegdt5fPXxCBVS1G0iaZpuo/tBz399ymceLJveQhFFZ8qThHiP3fzuoQjKN2ow==", + "version": "3.34.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.34.0.tgz", + "integrity": "sha512-4ZIyeNbW/Cn1wkMMDy+mvrRUxrwFNjKwbhCfQpDd+eLgYipDqp8oGFGtLmhh18EDPKA0g3VUBYOxQGGwvWLVpA==", "dev": true, "dependencies": { - "browserslist": "^4.22.1" + "browserslist": "^4.22.2" }, "funding": { "type": "opencollective", @@ -16211,9 +16211,9 @@ } }, "node_modules/vite": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz", - "integrity": "sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==", + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.1.tgz", + "integrity": "sha512-AXXFaAJ8yebyqzoNB9fu2pHoo/nWX+xZlaRwoeYUxEqBO+Zj4msE5G+BhGBll9lYEKv9Hfks52PAF2X7qDYXQA==", "dev": true, "dependencies": { "esbuild": "^0.18.10", diff --git a/desktop/package.json b/desktop/package.json index 817093043..fd9992c5b 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -30,16 +30,16 @@ "lint": "ng lint" }, "dependencies": { - "@angular/animations": "17.0.5", - "@angular/cdk": "17.0.1", - "@angular/common": "17.0.5", - "@angular/compiler": "17.0.5", - "@angular/core": "17.0.5", - "@angular/forms": "17.0.5", - "@angular/language-service": "17.0.5", - "@angular/platform-browser": "17.0.5", - "@angular/platform-browser-dynamic": "17.0.5", - "@angular/router": "17.0.5", + "@angular/animations": "17.0.6", + "@angular/cdk": "17.0.3", + "@angular/common": "17.0.6", + "@angular/compiler": "17.0.6", + "@angular/core": "17.0.6", + "@angular/forms": "17.0.6", + "@angular/language-service": "17.0.6", + "@angular/platform-browser": "17.0.6", + "@angular/platform-browser-dynamic": "17.0.6", + "@angular/router": "17.0.6", "@fontsource/roboto": "5.0.8", "@mdi/font": "7.3.67", "chart.js": "4.4.1", @@ -58,9 +58,9 @@ }, "devDependencies": { "@angular-builders/custom-webpack": "17.0.0", - "@angular-devkit/build-angular": "17.0.5", - "@angular/cli": "17.0.5", - "@angular/compiler-cli": "17.0.5", + "@angular-devkit/build-angular": "17.0.6", + "@angular/cli": "17.0.6", + "@angular/compiler-cli": "17.0.6", "@types/leaflet": "1.9.8", "@types/node": "20.10.4", "@types/uuid": "9.0.7", From 7f30a792ac4ac91124bc9b99b81920eb112f87a8 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Fri, 8 Dec 2023 21:30:42 -0300 Subject: [PATCH 13/87] [api]: Fix possible error "no transaction is in progress" --- .../api/beans/configurations/DataSourceConfiguration.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/api/src/main/kotlin/nebulosa/api/beans/configurations/DataSourceConfiguration.kt b/api/src/main/kotlin/nebulosa/api/beans/configurations/DataSourceConfiguration.kt index ec2141ecb..b18c9b55a 100644 --- a/api/src/main/kotlin/nebulosa/api/beans/configurations/DataSourceConfiguration.kt +++ b/api/src/main/kotlin/nebulosa/api/beans/configurations/DataSourceConfiguration.kt @@ -2,6 +2,7 @@ package nebulosa.api.beans.configurations import com.zaxxer.hikari.HikariConfig import com.zaxxer.hikari.HikariDataSource +import jakarta.persistence.EntityManagerFactory import org.springframework.beans.factory.annotation.Qualifier import org.springframework.boot.context.properties.ConfigurationProperties import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder @@ -11,6 +12,7 @@ import org.springframework.context.annotation.Primary import org.springframework.data.jpa.repository.config.EnableJpaRepositories import org.springframework.jdbc.datasource.DataSourceTransactionManager import org.springframework.jdbc.datasource.DriverManagerDataSource +import org.springframework.orm.jpa.JpaTransactionManager import org.springframework.transaction.PlatformTransactionManager import javax.sql.DataSource @@ -55,8 +57,9 @@ class DataSourceConfiguration { @Bean @Primary - fun transactionManager(dataSource: DataSource): PlatformTransactionManager { - return DataSourceTransactionManager(dataSource) + fun transactionManager(entityManagerFactory: EntityManagerFactory): PlatformTransactionManager { + // Fix "no transactions is in progress": https://stackoverflow.com/a/33397173 + return JpaTransactionManager(entityManagerFactory) } } From 5e036457f6899e10634e079fb884af4cecb47e3d Mon Sep 17 00:00:00 2001 From: tiagohm Date: Fri, 8 Dec 2023 21:32:15 -0300 Subject: [PATCH 14/87] [api]: Use ThreadPoolTaskExecutor and Scheduled to run tasks on startup --- .../nebulosa/api/atlas/IERSUpdateTask.kt | 72 +++++++++++++++++++ .../kotlin/nebulosa/api/atlas/IERSUpdater.kt | 54 -------------- ...lliteUpdater.kt => SatelliteUpdateTask.kt} | 9 +-- ...yAtlasUpdater.kt => SkyAtlasUpdateTask.kt} | 20 +++--- .../beans/ThreadedTaskBeanPostProcessor.kt | 29 -------- .../api/beans/annotations/ThreadedTask.kt | 8 --- .../beans/configurations/BeanConfiguration.kt | 20 +++--- .../kotlin/nebulosa/api/image/ImageService.kt | 20 +++--- ...tionInitializer.kt => LocationSeedTask.kt} | 7 +- .../api/preferences/PreferenceService.kt | 14 ---- .../nebulosa/api/services/MessageService.kt | 3 +- .../main/kotlin/nebulosa/fits/FitsHelper.kt | 4 +- 12 files changed, 117 insertions(+), 143 deletions(-) create mode 100644 api/src/main/kotlin/nebulosa/api/atlas/IERSUpdateTask.kt delete mode 100644 api/src/main/kotlin/nebulosa/api/atlas/IERSUpdater.kt rename api/src/main/kotlin/nebulosa/api/atlas/{SatelliteUpdater.kt => SatelliteUpdateTask.kt} (90%) rename api/src/main/kotlin/nebulosa/api/atlas/{SkyAtlasUpdater.kt => SkyAtlasUpdateTask.kt} (84%) delete mode 100644 api/src/main/kotlin/nebulosa/api/beans/ThreadedTaskBeanPostProcessor.kt delete mode 100644 api/src/main/kotlin/nebulosa/api/beans/annotations/ThreadedTask.kt rename api/src/main/kotlin/nebulosa/api/locations/{LocationInitializer.kt => LocationSeedTask.kt} (54%) diff --git a/api/src/main/kotlin/nebulosa/api/atlas/IERSUpdateTask.kt b/api/src/main/kotlin/nebulosa/api/atlas/IERSUpdateTask.kt new file mode 100644 index 000000000..54a4c2c0b --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/atlas/IERSUpdateTask.kt @@ -0,0 +1,72 @@ +package nebulosa.api.atlas + +import nebulosa.api.preferences.PreferenceService +import nebulosa.io.transferAndClose +import nebulosa.log.loggerFor +import nebulosa.time.IERS +import nebulosa.time.IERSA +import okhttp3.OkHttpClient +import okhttp3.Request +import org.springframework.http.HttpHeaders +import org.springframework.scheduling.annotation.Scheduled +import org.springframework.stereotype.Component +import java.nio.file.Path +import java.util.concurrent.TimeUnit +import kotlin.io.path.inputStream +import kotlin.io.path.outputStream + +@Component +class IERSUpdateTask( + private val dataPath: Path, + private val preferenceService: PreferenceService, + private val httpClient: OkHttpClient, +) : Runnable { + + @Scheduled(initialDelay = 5L, fixedDelay = Long.MAX_VALUE, timeUnit = TimeUnit.SECONDS) + override fun run() { + val finals2000A = Path.of("$dataPath", "finals2000A.all") + + finals2000A.download() + + val iersa = IERSA() + finals2000A.inputStream().use(iersa::load) + IERS.attach(iersa) + } + + private fun Path.download() { + try { + var request = Request.Builder() + .head() + .url(IERSA.URL) + .build() + + var modifiedAt = httpClient.newCall(request).execute() + .use { it.headers.getDate(HttpHeaders.LAST_MODIFIED) } + + if (modifiedAt != null && "$modifiedAt" == preferenceService.getText(IERS_UPDATED_AT)) { + LOG.info("finals2000A.all is up to date. modifiedAt={}", modifiedAt) + return + } + + request = request.newBuilder().get().build() + + LOG.info("downloading finals2000A.all") + + httpClient.newCall(request).execute().use { + it.body!!.byteStream().transferAndClose(outputStream()) + modifiedAt = it.headers.getDate(HttpHeaders.LAST_MODIFIED) + preferenceService.putText(IERS_UPDATED_AT, "$modifiedAt") + LOG.info("finals2000A.all downloaded. modifiedAt={}", modifiedAt) + } + } catch (e: Throwable) { + LOG.error("failed to download finals2000A.all", e) + } + } + + companion object { + + const val IERS_UPDATED_AT = "IERS_UPDATED_AT" + + @JvmStatic private val LOG = loggerFor() + } +} diff --git a/api/src/main/kotlin/nebulosa/api/atlas/IERSUpdater.kt b/api/src/main/kotlin/nebulosa/api/atlas/IERSUpdater.kt deleted file mode 100644 index f607859d0..000000000 --- a/api/src/main/kotlin/nebulosa/api/atlas/IERSUpdater.kt +++ /dev/null @@ -1,54 +0,0 @@ -package nebulosa.api.atlas - -import nebulosa.api.beans.annotations.ThreadedTask -import nebulosa.io.transferAndClose -import nebulosa.log.loggerFor -import nebulosa.time.IERS -import nebulosa.time.IERSA -import okhttp3.OkHttpClient -import okhttp3.Request -import org.springframework.stereotype.Component -import java.nio.file.Path -import kotlin.io.path.inputStream -import kotlin.io.path.outputStream - -@Component -@ThreadedTask -class IERSUpdater( - private val dataPath: Path, - private val httpClient: OkHttpClient, -) : Runnable { - - override fun run() { - val finals2000A = Path.of("$dataPath", "finals2000A.all") - - finals2000A.download() - - val iersa = IERSA() - finals2000A.inputStream().use(iersa::load) - IERS.attach(iersa) - } - - private fun Path.download() { - val request = Request.Builder() - .get() - .url(IERSA.URL) - .build() - - try { - LOG.info("downloading finals2000A.all") - - httpClient.newCall(request).execute().use { - it.body!!.byteStream().transferAndClose(outputStream()) - LOG.info("finals2000A.all loaded") - } - } catch (e: Throwable) { - LOG.error("failed to download finals2000A.all", e) - } - } - - companion object { - - @JvmStatic private val LOG = loggerFor() - } -} diff --git a/api/src/main/kotlin/nebulosa/api/atlas/SatelliteUpdater.kt b/api/src/main/kotlin/nebulosa/api/atlas/SatelliteUpdateTask.kt similarity index 90% rename from api/src/main/kotlin/nebulosa/api/atlas/SatelliteUpdater.kt rename to api/src/main/kotlin/nebulosa/api/atlas/SatelliteUpdateTask.kt index f80c548ee..fe9e3024a 100644 --- a/api/src/main/kotlin/nebulosa/api/atlas/SatelliteUpdater.kt +++ b/api/src/main/kotlin/nebulosa/api/atlas/SatelliteUpdateTask.kt @@ -8,7 +8,7 @@ import org.springframework.stereotype.Component import java.util.concurrent.CompletableFuture @Component -class SatelliteUpdater( +class SatelliteUpdateTask( private val httpClient: OkHttpClient, private val preferenceService: PreferenceService, private val satelliteRepository: SatelliteRepository, @@ -19,7 +19,7 @@ class SatelliteUpdater( } private fun isOutOfDate(): Boolean { - val updatedAt = preferenceService.satellitesUpdatedAt + val updatedAt = preferenceService.getLong(SATELLITES_UPDATED_AT) ?: 0L return System.currentTimeMillis() - updatedAt >= UPDATE_INTERVAL } @@ -28,7 +28,7 @@ class SatelliteUpdater( LOG.info("satellites is out of date") if (updateTLEs()) { - preferenceService.satellitesUpdatedAt = System.currentTimeMillis() + preferenceService.putLong(SATELLITES_UPDATED_AT, System.currentTimeMillis()) } else { LOG.warn("no satellites was updated") } @@ -100,7 +100,8 @@ class SatelliteUpdater( companion object { const val UPDATE_INTERVAL = 1000L * 60 * 60 * 24 // 1 day + const val SATELLITES_UPDATED_AT = "SATELLITES_UPDATED_AT" - @JvmStatic private val LOG = loggerFor() + @JvmStatic private val LOG = loggerFor() } } diff --git a/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasUpdater.kt b/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasUpdateTask.kt similarity index 84% rename from api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasUpdater.kt rename to api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasUpdateTask.kt index 9fe78c2c1..6f68d214c 100644 --- a/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasUpdater.kt +++ b/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasUpdateTask.kt @@ -2,30 +2,30 @@ package nebulosa.api.atlas import com.fasterxml.jackson.core.type.TypeReference import com.fasterxml.jackson.databind.ObjectMapper -import nebulosa.api.beans.annotations.ThreadedTask import nebulosa.api.notification.NotificationEvent import nebulosa.api.preferences.PreferenceService import nebulosa.api.services.MessageService import nebulosa.log.loggerFor import okhttp3.OkHttpClient import okhttp3.Request +import org.springframework.scheduling.annotation.Scheduled import org.springframework.stereotype.Component import java.io.InputStream import java.nio.file.Path +import java.util.concurrent.TimeUnit import java.util.zip.GZIPInputStream import kotlin.io.path.exists import kotlin.io.path.inputStream @Component -@ThreadedTask -class SkyAtlasUpdater( +class SkyAtlasUpdateTask( private val objectMapper: ObjectMapper, private val preferenceService: PreferenceService, private val starsRepository: StarRepository, private val deepSkyObjectRepository: DeepSkyObjectRepository, private val httpClient: OkHttpClient, private val dataPath: Path, - private val satelliteUpdater: SatelliteUpdater, + private val satelliteUpdateTask: SatelliteUpdateTask, private val messageService: MessageService, ) : Runnable { @@ -34,10 +34,11 @@ class SkyAtlasUpdater( override val type = "SKY_ATLAS_UPDATE_FINISHED" } + @Scheduled(initialDelay = 1L, fixedDelay = Long.MAX_VALUE, timeUnit = TimeUnit.SECONDS) override fun run() { - satelliteUpdater.run() + satelliteUpdateTask.run() - val version = preferenceService.skyAtlasVersion + val version = preferenceService.getText(SKY_ATLAS_VERSION) if (version != DATABASE_VERSION) { LOG.info("Star/DSO database is out of date. currentVersion={}, newVersion={}", version, DATABASE_VERSION) @@ -50,11 +51,11 @@ class SkyAtlasUpdater( readStarsAndLoad() readDSOsAndLoad() - preferenceService.skyAtlasVersion = DATABASE_VERSION + preferenceService.putText(SKY_ATLAS_VERSION, DATABASE_VERSION) messageService.sendMessage(Finished("Sky Atlas database was updated to version $DATABASE_VERSION.")) } else { - LOG.info("Star/DSO database is up to date") + LOG.info("Star/DSO database is up to date. version={}", version) } } @@ -115,7 +116,8 @@ class SkyAtlasUpdater( companion object { const val DATABASE_VERSION = "2023.10.18" + const val SKY_ATLAS_VERSION = "SKY_ATLAS_VERSION" - @JvmStatic private val LOG = loggerFor() + @JvmStatic private val LOG = loggerFor() } } diff --git a/api/src/main/kotlin/nebulosa/api/beans/ThreadedTaskBeanPostProcessor.kt b/api/src/main/kotlin/nebulosa/api/beans/ThreadedTaskBeanPostProcessor.kt deleted file mode 100644 index ac50ea599..000000000 --- a/api/src/main/kotlin/nebulosa/api/beans/ThreadedTaskBeanPostProcessor.kt +++ /dev/null @@ -1,29 +0,0 @@ -package nebulosa.api.beans - -import nebulosa.api.beans.annotations.ThreadedTask -import nebulosa.log.loggerFor -import org.springframework.beans.factory.config.BeanPostProcessor -import org.springframework.stereotype.Component -import java.util.concurrent.CompletableFuture -import java.util.concurrent.ExecutorService - -@Component -class ThreadedTaskBeanPostProcessor(private val systemExecutorService: ExecutorService) : BeanPostProcessor { - - override fun postProcessAfterInitialization(bean: Any, beanName: String): Any { - if (bean is Runnable && bean::class.java.isAnnotationPresent(ThreadedTask::class.java)) { - LOG.info("threaded task scheduled. name={}", beanName) - - CompletableFuture - .runAsync(bean, systemExecutorService) - .whenComplete { _, e -> e?.printStackTrace() ?: LOG.info("threaded task finished. name={}", beanName) } - } - - return bean - } - - companion object { - - @JvmStatic private val LOG = loggerFor() - } -} diff --git a/api/src/main/kotlin/nebulosa/api/beans/annotations/ThreadedTask.kt b/api/src/main/kotlin/nebulosa/api/beans/annotations/ThreadedTask.kt deleted file mode 100644 index 4cfbb9144..000000000 --- a/api/src/main/kotlin/nebulosa/api/beans/annotations/ThreadedTask.kt +++ /dev/null @@ -1,8 +0,0 @@ -package nebulosa.api.beans.annotations - -import org.springframework.context.annotation.Lazy - -@Retention -@Lazy(false) -@Target(AnnotationTarget.CLASS) -annotation class ThreadedTask diff --git a/api/src/main/kotlin/nebulosa/api/beans/configurations/BeanConfiguration.kt b/api/src/main/kotlin/nebulosa/api/beans/configurations/BeanConfiguration.kt index 7fb822294..d4f62364c 100644 --- a/api/src/main/kotlin/nebulosa/api/beans/configurations/BeanConfiguration.kt +++ b/api/src/main/kotlin/nebulosa/api/beans/configurations/BeanConfiguration.kt @@ -23,22 +23,17 @@ import okhttp3.ConnectionPool import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import org.greenrobot.eventbus.EventBus -import org.springframework.batch.core.launch.JobLauncher -import org.springframework.batch.core.launch.support.TaskExecutorJobLauncher -import org.springframework.batch.core.repository.JobRepository import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration -import org.springframework.context.annotation.Primary import org.springframework.core.task.SimpleAsyncTaskExecutor import org.springframework.http.converter.HttpMessageConverter import org.springframework.http.converter.StringHttpMessageConverter +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor import org.springframework.web.method.support.HandlerMethodArgumentResolver import org.springframework.web.servlet.config.annotation.CorsRegistry import org.springframework.web.servlet.config.annotation.WebMvcConfigurer import java.nio.file.Path -import java.util.concurrent.ExecutorService -import java.util.concurrent.Executors import java.util.concurrent.TimeUnit import kotlin.io.path.createDirectories @@ -107,17 +102,22 @@ class BeanConfiguration { fun hips2FitsService(httpClient: OkHttpClient) = Hips2FitsService(httpClient = httpClient) @Bean - fun systemExecutorService(): ExecutorService = - Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors(), DaemonThreadFactory) + fun threadPoolTaskExecutor(): ThreadPoolTaskExecutor { + val taskExecutor = ThreadPoolTaskExecutor() + taskExecutor.corePoolSize = 8 + taskExecutor.maxPoolSize = 32 + taskExecutor.initialize() + return taskExecutor + } @Bean - fun eventBus(systemExecutorService: ExecutorService) = EventBus.builder() + fun eventBus(threadPoolTaskExecutor: ThreadPoolTaskExecutor) = EventBus.builder() .sendNoSubscriberEvent(false) .sendSubscriberExceptionEvent(false) .throwSubscriberException(false) .logNoSubscriberMessages(false) .logSubscriberExceptions(false) - .executorService(systemExecutorService) + .executorService(threadPoolTaskExecutor.threadPoolExecutor) .installDefaultEventBus()!! @Bean diff --git a/api/src/main/kotlin/nebulosa/api/image/ImageService.kt b/api/src/main/kotlin/nebulosa/api/image/ImageService.kt index c839caab1..8c9ece745 100644 --- a/api/src/main/kotlin/nebulosa/api/image/ImageService.kt +++ b/api/src/main/kotlin/nebulosa/api/image/ImageService.kt @@ -22,12 +22,12 @@ import nebulosa.watney.star.detection.WatneyStarDetector import nebulosa.wcs.WCSException import nebulosa.wcs.WCSTransform import org.springframework.http.HttpStatus +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor import org.springframework.stereotype.Service import org.springframework.web.server.ResponseStatusException import java.nio.file.Path import java.util.* import java.util.concurrent.CompletableFuture -import java.util.concurrent.ExecutorService import javax.imageio.ImageIO import kotlin.io.path.extension import kotlin.io.path.inputStream @@ -41,7 +41,7 @@ class ImageService( private val smallBodyDatabaseService: SmallBodyDatabaseService, private val simbadService: SimbadService, private val imageBucket: ImageBucket, - private val systemExecutorService: ExecutorService, + private val threadPoolTaskExecutor: ThreadPoolTaskExecutor, ) { @Synchronized @@ -141,9 +141,9 @@ class ImageService( val dateTime = image.header.observationDate if (minorPlanets && dateTime != null) { - CompletableFuture.runAsync({ - val latitude = image.header.latitude.let { if (it.isFinite()) it else 0.0 } - val longitude = image.header.longitude.let { if (it.isFinite()) it else 0.0 } + threadPoolTaskExecutor.submitCompletable { + val latitude = image.header.latitude ?: 0.0 + val longitude = image.header.longitude ?: 0.0 LOG.info( "finding minor planet annotations. dateTime={}, latitude={}, longitude={}, calibration={}", @@ -154,7 +154,7 @@ class ImageService( dateTime, latitude, longitude, 0.0, calibration.rightAscension, calibration.declination, calibration.radius, minorPlanetMagLimit, - ).execute().body() ?: return@runAsync + ).execute().body() ?: return@submitCompletable val radiusInSeconds = calibration.radius.toArcsec var count = 0 @@ -174,13 +174,14 @@ class ImageService( } LOG.info("Found {} minor planets", count) - }, systemExecutorService).whenComplete { _, e -> e?.printStackTrace() }.also(tasks::add) + }.whenComplete { _, e -> e?.printStackTrace() } + .also(tasks::add) } // val barycentric = VSOP87E.EARTH.at(UTC(TimeYMDHMS(dateTime))) if (stars || dsos) { - CompletableFuture.runAsync({ + threadPoolTaskExecutor.submitCompletable { LOG.info("finding star annotations. dateTime={}, calibration={}", dateTime, calibration) val types = ArrayList(4) @@ -227,7 +228,8 @@ class ImageService( } LOG.info("Found {} stars/DSOs", count) - }, systemExecutorService).whenComplete { _, e -> e?.printStackTrace() }.also(tasks::add) + }.whenComplete { _, e -> e?.printStackTrace() } + .also(tasks::add) } CompletableFuture.allOf(*tasks.toTypedArray()).join() diff --git a/api/src/main/kotlin/nebulosa/api/locations/LocationInitializer.kt b/api/src/main/kotlin/nebulosa/api/locations/LocationSeedTask.kt similarity index 54% rename from api/src/main/kotlin/nebulosa/api/locations/LocationInitializer.kt rename to api/src/main/kotlin/nebulosa/api/locations/LocationSeedTask.kt index 9b8ad74ef..f440692ef 100644 --- a/api/src/main/kotlin/nebulosa/api/locations/LocationInitializer.kt +++ b/api/src/main/kotlin/nebulosa/api/locations/LocationSeedTask.kt @@ -1,12 +1,13 @@ package nebulosa.api.locations -import nebulosa.api.beans.annotations.ThreadedTask +import org.springframework.scheduling.annotation.Scheduled import org.springframework.stereotype.Component +import java.util.concurrent.TimeUnit @Component -@ThreadedTask -class LocationInitializer(private val locationRepository: LocationRepository) : Runnable { +class LocationSeedTask(private val locationRepository: LocationRepository) : Runnable { + @Scheduled(initialDelay = 1L, fixedDelay = Long.MAX_VALUE, timeUnit = TimeUnit.SECONDS) override fun run() { if (locationRepository.count() <= 0) { val location = LocationEntity(1, "Null Island", 0.0, 0.0, 0.0, selected = true) diff --git a/api/src/main/kotlin/nebulosa/api/preferences/PreferenceService.kt b/api/src/main/kotlin/nebulosa/api/preferences/PreferenceService.kt index 418eb4965..ed6422a4b 100644 --- a/api/src/main/kotlin/nebulosa/api/preferences/PreferenceService.kt +++ b/api/src/main/kotlin/nebulosa/api/preferences/PreferenceService.kt @@ -54,18 +54,4 @@ class PreferenceService( @Synchronized fun delete(key: String) = preferenceRepository.deleteById(key) - - final inline var skyAtlasVersion - get() = getText(SKY_ATLAS_VERSION) - set(value) = putText(SKY_ATLAS_VERSION, value) - - final inline var satellitesUpdatedAt - get() = getLong(SATELLITES_UPDATED_AT) ?: 0L - set(value) = putLong(SATELLITES_UPDATED_AT, value) - - companion object { - - const val SKY_ATLAS_VERSION = "SKY_ATLAS_VERSION" - const val SATELLITES_UPDATED_AT = "SATELLITES_UPDATED_AT" - } } diff --git a/api/src/main/kotlin/nebulosa/api/services/MessageService.kt b/api/src/main/kotlin/nebulosa/api/services/MessageService.kt index 7389be237..8e9d254b8 100644 --- a/api/src/main/kotlin/nebulosa/api/services/MessageService.kt +++ b/api/src/main/kotlin/nebulosa/api/services/MessageService.kt @@ -1,5 +1,6 @@ package nebulosa.api.services +import nebulosa.log.debug import nebulosa.log.loggerFor import org.springframework.context.event.EventListener import org.springframework.messaging.simp.SimpMessageHeaderAccessor @@ -38,7 +39,7 @@ class MessageService( if (connected.get()) { simpleMessageTemplate.convertAndSend(EVENT_NAME, event) } else { - LOG.warn("queueing message. event={}", event) + LOG.debug { "queueing message. event=$event" } messageQueue.offer(event) } } diff --git a/nebulosa-fits/src/main/kotlin/nebulosa/fits/FitsHelper.kt b/nebulosa-fits/src/main/kotlin/nebulosa/fits/FitsHelper.kt index 16e70168d..d212bb858 100644 --- a/nebulosa-fits/src/main/kotlin/nebulosa/fits/FitsHelper.kt +++ b/nebulosa-fits/src/main/kotlin/nebulosa/fits/FitsHelper.kt @@ -55,10 +55,10 @@ inline val Header.gain get() = getDouble(NOAOExt.GAIN, 0.0) val Header.latitude - get() = (getDoubleOrNull(SBFitsExt.SITELAT) ?: getDouble("LAT-OBS", Double.NaN)).deg + get() = (getDoubleOrNull(SBFitsExt.SITELAT) ?: getDoubleOrNull("LAT-OBS"))?.deg val Header.longitude - get() = (getDoubleOrNull(SBFitsExt.SITELONG) ?: getDouble("LONG-OBS", Double.NaN)).deg + get() = (getDoubleOrNull(SBFitsExt.SITELONG) ?: getDoubleOrNull("LONG-OBS"))?.deg val Header.observationDate get() = getStringOrNull(Standard.DATE_OBS)?.let(LocalDateTime::parse) From 188e6f9e2904d0e5b91418afd23e9d9a427950ed Mon Sep 17 00:00:00 2001 From: tiagohm Date: Fri, 8 Dec 2023 22:45:19 -0300 Subject: [PATCH 15/87] [api]: Use Hikari for main DataSource --- .../nebulosa/api/atlas/SkyAtlasService.kt | 4 -- .../configurations/DataSourceConfiguration.kt | 39 +++++++++++-------- .../nebulosa/api/locations/LocationService.kt | 2 - api/src/main/resources/application.yml | 5 ++- .../resources/db/migration/beforeMigrate.sql | 9 ----- desktop/.vscode/settings.json | 2 +- 6 files changed, 26 insertions(+), 35 deletions(-) delete mode 100644 api/src/main/resources/db/migration/beforeMigrate.sql diff --git a/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasService.kt b/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasService.kt index c38d2dfaa..aef72d3cb 100644 --- a/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasService.kt +++ b/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasService.kt @@ -25,7 +25,6 @@ import org.springframework.http.HttpStatus import org.springframework.scheduling.annotation.EnableScheduling import org.springframework.scheduling.annotation.Scheduled import org.springframework.stereotype.Service -import org.springframework.transaction.annotation.Transactional import org.springframework.web.server.ResponseStatusException import java.awt.image.BufferedImage import java.io.ByteArrayOutputStream @@ -113,7 +112,6 @@ class SkyAtlasService( else horizonsEphemerisProvider.compute(target, position, dateTime, zoneId) } - @Transactional(readOnly = true) fun searchSatellites(text: String, groups: List): List { return satelliteRepository.search(text.ifBlank { null }, groups, Pageable.ofSize(1000)) } @@ -205,7 +203,6 @@ class SkyAtlasService( ?.let(MinorPlanet::of) ?: MinorPlanet.EMPTY - @Transactional(readOnly = true) fun searchStar( text: String, rightAscension: Angle = 0.0, declination: Angle = 0.0, radius: Angle = 0.0, @@ -220,7 +217,6 @@ class SkyAtlasService( Pageable.ofSize(5000), ) - @Transactional(readOnly = true) fun searchDSO( text: String, rightAscension: Angle = 0.0, declination: Angle = 0.0, radius: Angle = 0.0, diff --git a/api/src/main/kotlin/nebulosa/api/beans/configurations/DataSourceConfiguration.kt b/api/src/main/kotlin/nebulosa/api/beans/configurations/DataSourceConfiguration.kt index b18c9b55a..890cd560c 100644 --- a/api/src/main/kotlin/nebulosa/api/beans/configurations/DataSourceConfiguration.kt +++ b/api/src/main/kotlin/nebulosa/api/beans/configurations/DataSourceConfiguration.kt @@ -4,14 +4,13 @@ import com.zaxxer.hikari.HikariConfig import com.zaxxer.hikari.HikariDataSource import jakarta.persistence.EntityManagerFactory import org.springframework.beans.factory.annotation.Qualifier -import org.springframework.boot.context.properties.ConfigurationProperties +import org.springframework.beans.factory.annotation.Value import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Primary import org.springframework.data.jpa.repository.config.EnableJpaRepositories import org.springframework.jdbc.datasource.DataSourceTransactionManager -import org.springframework.jdbc.datasource.DriverManagerDataSource import org.springframework.orm.jpa.JpaTransactionManager import org.springframework.transaction.PlatformTransactionManager import javax.sql.DataSource @@ -19,17 +18,24 @@ import javax.sql.DataSource @Configuration class DataSourceConfiguration { - @Bean + @Value("\${spring.datasource.url}") private lateinit var mainDataSourceUrl: String + @Value("\${spring.batch.datasource.url}") private lateinit var batchDataSourceUrl: String + @Primary - @ConfigurationProperties(prefix = "spring.datasource") - fun dataSource(): DataSource { - return DriverManagerDataSource() + @Bean("mainDataSource") + fun mainDataSource(): DataSource { + val config = HikariConfig() + config.jdbcUrl = mainDataSourceUrl + config.driverClassName = DRIVER_CLASS_NAME + config.maximumPoolSize = 8 + config.minimumIdle = 1 + return HikariDataSource(config) } @Bean("batchDataSource") fun batchDataSource(): DataSource { val config = HikariConfig() - config.jdbcUrl = JDBC_MEMORY_URL + config.jdbcUrl = batchDataSourceUrl config.driverClassName = DRIVER_CLASS_NAME config.maximumPoolSize = 1 config.minimumIdle = 1 @@ -39,14 +45,14 @@ class DataSourceConfiguration { @Configuration @EnableJpaRepositories( basePackages = ["nebulosa.api"], - entityManagerFactoryRef = "entityManagerFactory", - transactionManagerRef = "transactionManager" + entityManagerFactoryRef = "mainEntityManagerFactory", + transactionManagerRef = "mainTransactionManager" ) class Main { @Primary - @Bean(name = ["entityManagerFactory"]) - fun entityManagerFactory( + @Bean("mainEntityManagerFactory") + fun mainEntityManagerFactory( builder: EntityManagerFactoryBuilder, dataSource: DataSource, ) = builder @@ -55,11 +61,11 @@ class DataSourceConfiguration { .persistenceUnit("mainPersistenceUnit") .build()!! - @Bean @Primary - fun transactionManager(entityManagerFactory: EntityManagerFactory): PlatformTransactionManager { + @Bean("mainTransactionManager") + fun mainTransactionManager(mainEntityManagerFactory: EntityManagerFactory): PlatformTransactionManager { // Fix "no transactions is in progress": https://stackoverflow.com/a/33397173 - return JpaTransactionManager(entityManagerFactory) + return JpaTransactionManager(mainEntityManagerFactory) } } @@ -71,7 +77,7 @@ class DataSourceConfiguration { ) class Batch { - @Bean(name = ["batchEntityManagerFactory"]) + @Bean("batchEntityManagerFactory") fun batchEntityManagerFactory( builder: EntityManagerFactoryBuilder, @Qualifier("batchDataSource") dataSource: DataSource, @@ -81,7 +87,7 @@ class DataSourceConfiguration { .persistenceUnit("batchPersistenceUnit") .build()!! - @Bean + @Bean("batchTransactionManager") fun batchTransactionManager(@Qualifier("batchDataSource") dataSource: DataSource): PlatformTransactionManager { return DataSourceTransactionManager(dataSource) } @@ -90,6 +96,5 @@ class DataSourceConfiguration { companion object { const val DRIVER_CLASS_NAME = "org.sqlite.JDBC" - const val JDBC_MEMORY_URL = "jdbc:sqlite:file:nebulosa.db?mode=memory" } } diff --git a/api/src/main/kotlin/nebulosa/api/locations/LocationService.kt b/api/src/main/kotlin/nebulosa/api/locations/LocationService.kt index 6d6b0fcc7..83314a4d1 100644 --- a/api/src/main/kotlin/nebulosa/api/locations/LocationService.kt +++ b/api/src/main/kotlin/nebulosa/api/locations/LocationService.kt @@ -1,7 +1,6 @@ package nebulosa.api.locations import org.springframework.stereotype.Service -import org.springframework.transaction.annotation.Transactional @Service class LocationService( @@ -28,7 +27,6 @@ class LocationService( } @Synchronized - @Transactional fun delete(id: Long) { if (id > 0L && locationRepository.count() > 1) { var location = location(id) diff --git a/api/src/main/resources/application.yml b/api/src/main/resources/application.yml index 3a354b379..c763c2860 100644 --- a/api/src/main/resources/application.yml +++ b/api/src/main/resources/application.yml @@ -12,8 +12,7 @@ spring: lazy-initialization: true datasource: - url: jdbc:sqlite:file:${DATA_PATH}/nebulosa.db - driverClassName: org.sqlite.JDBC + url: 'jdbc:sqlite:file:${DATA_PATH}/nebulosa.db' jpa: database-platform: org.hibernate.community.dialect.SQLiteDialect @@ -30,5 +29,7 @@ spring: batch: job: enabled: false + datasource: + url: 'jdbc:sqlite:file:nebulosa.db?mode=memory' jdbc: initialize-schema: always diff --git a/api/src/main/resources/db/migration/beforeMigrate.sql b/api/src/main/resources/db/migration/beforeMigrate.sql deleted file mode 100644 index e237c8ab0..000000000 --- a/api/src/main/resources/db/migration/beforeMigrate.sql +++ /dev/null @@ -1,9 +0,0 @@ -DROP TABLE IF EXISTS BATCH_JOB_EXECUTION_CONTEXT; -DROP TABLE IF EXISTS BATCH_JOB_EXECUTION_PARAMS; -DROP TABLE IF EXISTS BATCH_JOB_EXECUTION; -DROP TABLE IF EXISTS BATCH_JOB_EXECUTION_SEQ; -DROP TABLE IF EXISTS BATCH_JOB_INSTANCE; -DROP TABLE IF EXISTS BATCH_JOB_SEQ; -DROP TABLE IF EXISTS BATCH_STEP_EXECUTION_CONTEXT; -DROP TABLE IF EXISTS BATCH_STEP_EXECUTION; -DROP TABLE IF EXISTS BATCH_STEP_EXECUTION_SEQ; diff --git a/desktop/.vscode/settings.json b/desktop/.vscode/settings.json index cc6d56677..224f2841b 100644 --- a/desktop/.vscode/settings.json +++ b/desktop/.vscode/settings.json @@ -5,7 +5,7 @@ "typescript.preferences.quoteStyle": "single", "editor.formatOnSave": true, "editor.codeActionsOnSave": { - "source.organizeImports": true + "source.organizeImports": "explicit" }, "html.format.wrapAttributes": "preserve-aligned", "html.format.wrapLineLength": 150, From c286b3cbf9ad7e6ae608982677576bb718caa6aa Mon Sep 17 00:00:00 2001 From: tiagohm Date: Fri, 8 Dec 2023 22:50:52 -0300 Subject: [PATCH 16/87] [desktop]: Update NPM dependencies --- desktop/package-lock.json | 55 ++++++++++++++++----------------------- 1 file changed, 22 insertions(+), 33 deletions(-) diff --git a/desktop/package-lock.json b/desktop/package-lock.json index a9c68faf6..6990258dd 100644 --- a/desktop/package-lock.json +++ b/desktop/package-lock.json @@ -4323,9 +4323,9 @@ } }, "node_modules/acorn-walk": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.0.tgz", - "integrity": "sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA==", + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.1.tgz", + "integrity": "sha512-TgUZgYvqZprrl7YldZNoa9OciCAyZR+Ejm9eXzKCmjsF5IKp/wgQ7Z/ZpjpGTIUPwrHQIcYeI8qDh4PsEwxMbw==", "dev": true, "engines": { "node": ">=0.4.0" @@ -5888,9 +5888,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001565", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001565.tgz", - "integrity": "sha512-xrE//a3O7TP0vaJ8ikzkD2c2NgcVUvsEe2IvFTntV4Yd1Z9FVzh+gW+enX96L0psrbaFMcVcH2l90xNuGDWc8w==", + "version": "1.0.30001566", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001566.tgz", + "integrity": "sha512-ggIhCsTxmITBAMmK8yZjEhCO5/47jKXPu6Dha/wuCS4JePVL+3uiDEBuhu2aIoT+bqTOR8L76Ip1ARL9xYsEJA==", "dev": true, "funding": [ { @@ -7838,15 +7838,15 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.601", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.601.tgz", - "integrity": "sha512-SpwUMDWe9tQu8JX5QCO1+p/hChAi9AE9UpoC3rcHVc+gdCGlbT3SGb5I1klgb952HRIyvt9wZhSz9bNBYz9swA==", + "version": "1.4.609", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.609.tgz", + "integrity": "sha512-ihiCP7PJmjoGNuLpl7TjNA8pCQWu09vGyjlPYw1Rqww4gvNuCcmvl+44G+2QyJ6S2K4o+wbTS++Xz0YN8Q9ERw==", "dev": true }, "node_modules/electron/node_modules/@types/node": { - "version": "18.19.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.0.tgz", - "integrity": "sha512-667KNhaD7U29mT5wf+TZUnrzPrlL2GNQ5N0BMjO2oNULhBxX0/FKCkm6JMu0Jh7Z+1LwUlR21ekd7KhIboNFNw==", + "version": "18.19.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.3.tgz", + "integrity": "sha512-k5fggr14DwAytoA/t8rPrIz++lXK7/DqckthCmoZOKNsEbJkId4Z//BqgApXBUGrGddrigYa1oqheo/7YmW4rg==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -11717,13 +11717,12 @@ } }, "node_modules/needle": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/needle/-/needle-3.2.0.tgz", - "integrity": "sha512-oUvzXnyLiVyVGoianLijF9O/RecZUf7TkBfimjGrLM4eQhXyeJwM6GeAWccwfQ9aa4gMCZKqhAOuLaMIcQxajQ==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/needle/-/needle-3.3.0.tgz", + "integrity": "sha512-Kaq820952NOrLY/LVbIhPZeXtCGDBAPVgT0BYnoT3p/Nr3nkGXdvWXXA3zgy7wpAgqRULu9p/NvKiFo6f/12fw==", "dev": true, "optional": true, "dependencies": { - "debug": "^3.2.6", "iconv-lite": "^0.6.3", "sax": "^1.2.4" }, @@ -11734,16 +11733,6 @@ "node": ">= 4.4.x" } }, - "node_modules/needle/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "optional": true, - "dependencies": { - "ms": "^2.1.1" - } - }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -12136,12 +12125,12 @@ } }, "node_modules/npm-packlist": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-8.0.0.tgz", - "integrity": "sha512-ErAGFB5kJUciPy1mmx/C2YFbvxoJ0QJ9uwkCZOeR6CqLLISPZBOiFModAbSXnjjlwW5lOhuhXva+fURsSGJqyw==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-8.0.1.tgz", + "integrity": "sha512-MQpL27ZrsJQ2kiAuQPpZb5LtJwydNRnI15QWXsf3WHERu4rzjRj6Zju/My2fov7tLuu3Gle/uoIX/DDZ3u4O4Q==", "dev": true, "dependencies": { - "ignore-walk": "^6.0.0" + "ignore-walk": "^6.0.4" }, "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" @@ -13759,9 +13748,9 @@ } }, "node_modules/reflect-metadata": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", - "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==", + "version": "0.1.14", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.14.tgz", + "integrity": "sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==", "dev": true }, "node_modules/regenerate": { From 82108a42597bbb5ff638daa9cf3f99b6f5dbeae6 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Fri, 8 Dec 2023 23:39:43 -0300 Subject: [PATCH 17/87] [api]: Fix: "TransactionRequiredException: Executing an update/delete query" --- .../kotlin/nebulosa/api/atlas/DeepSkyObjectRepository.kt | 5 +++++ .../main/kotlin/nebulosa/api/atlas/SatelliteRepository.kt | 5 +++++ api/src/main/kotlin/nebulosa/api/atlas/StarRepository.kt | 5 +++++ .../api/beans/configurations/DataSourceConfiguration.kt | 2 +- .../api/calibration/CalibrationFrameRepository.kt | 8 ++++++++ .../kotlin/nebulosa/api/locations/LocationRepository.kt | 4 ++++ .../nebulosa/api/preferences/PreferenceRepository.kt | 4 ++++ 7 files changed, 32 insertions(+), 1 deletion(-) diff --git a/api/src/main/kotlin/nebulosa/api/atlas/DeepSkyObjectRepository.kt b/api/src/main/kotlin/nebulosa/api/atlas/DeepSkyObjectRepository.kt index ab73ec990..714ca0f8e 100644 --- a/api/src/main/kotlin/nebulosa/api/atlas/DeepSkyObjectRepository.kt +++ b/api/src/main/kotlin/nebulosa/api/atlas/DeepSkyObjectRepository.kt @@ -8,8 +8,12 @@ import org.springframework.data.domain.Pageable import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.Query import org.springframework.stereotype.Repository +import org.springframework.transaction.annotation.Isolation +import org.springframework.transaction.annotation.Propagation +import org.springframework.transaction.annotation.Transactional @Repository +@Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW, isolation = Isolation.SERIALIZABLE) interface DeepSkyObjectRepository : JpaRepository { @Query( @@ -21,6 +25,7 @@ interface DeepSkyObjectRepository : JpaRepository { "(:radius <= 0.0 OR acos(sin(dso.declinationJ2000) * sin(:declinationJ2000) + cos(dso.declinationJ2000) * cos(:declinationJ2000) * cos(dso.rightAscensionJ2000 - :rightAscensionJ2000)) <= :radius) " + "ORDER BY dso.magnitude ASC" ) + @Transactional(readOnly = true) fun search( text: String? = null, rightAscensionJ2000: Angle = 0.0, declinationJ2000: Angle = 0.0, radius: Angle = 0.0, diff --git a/api/src/main/kotlin/nebulosa/api/atlas/SatelliteRepository.kt b/api/src/main/kotlin/nebulosa/api/atlas/SatelliteRepository.kt index 6c8d2da4f..a3cf1b87d 100644 --- a/api/src/main/kotlin/nebulosa/api/atlas/SatelliteRepository.kt +++ b/api/src/main/kotlin/nebulosa/api/atlas/SatelliteRepository.kt @@ -4,8 +4,12 @@ import org.springframework.data.domain.Pageable import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.Query import org.springframework.stereotype.Repository +import org.springframework.transaction.annotation.Isolation +import org.springframework.transaction.annotation.Propagation +import org.springframework.transaction.annotation.Transactional @Repository +@Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW, isolation = Isolation.SERIALIZABLE) interface SatelliteRepository : JpaRepository { @Query( @@ -14,6 +18,7 @@ interface SatelliteRepository : JpaRepository { " (:groupType = 0 OR s.group_type & :groupType != 0)", nativeQuery = true, ) + @Transactional(readOnly = true) fun search(text: String? = null, groupType: Long = 0L, page: Pageable): List fun search(text: String? = null, groups: List, page: Pageable): List { diff --git a/api/src/main/kotlin/nebulosa/api/atlas/StarRepository.kt b/api/src/main/kotlin/nebulosa/api/atlas/StarRepository.kt index d32dcbb59..25f813b5b 100644 --- a/api/src/main/kotlin/nebulosa/api/atlas/StarRepository.kt +++ b/api/src/main/kotlin/nebulosa/api/atlas/StarRepository.kt @@ -8,8 +8,12 @@ import org.springframework.data.domain.Pageable import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.Query import org.springframework.stereotype.Repository +import org.springframework.transaction.annotation.Isolation +import org.springframework.transaction.annotation.Propagation +import org.springframework.transaction.annotation.Transactional @Repository +@Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW, isolation = Isolation.SERIALIZABLE) interface StarRepository : JpaRepository { @Query( @@ -21,6 +25,7 @@ interface StarRepository : JpaRepository { "(:radius <= 0.0 OR acos(sin(star.declinationJ2000) * sin(:declinationJ2000) + cos(star.declinationJ2000) * cos(:declinationJ2000) * cos(star.rightAscensionJ2000 - :rightAscensionJ2000)) <= :radius) " + "ORDER BY star.magnitude ASC" ) + @Transactional(readOnly = true) fun search( text: String? = null, rightAscensionJ2000: Angle = 0.0, declinationJ2000: Angle = 0.0, radius: Angle = 0.0, diff --git a/api/src/main/kotlin/nebulosa/api/beans/configurations/DataSourceConfiguration.kt b/api/src/main/kotlin/nebulosa/api/beans/configurations/DataSourceConfiguration.kt index 890cd560c..25eba0293 100644 --- a/api/src/main/kotlin/nebulosa/api/beans/configurations/DataSourceConfiguration.kt +++ b/api/src/main/kotlin/nebulosa/api/beans/configurations/DataSourceConfiguration.kt @@ -27,7 +27,7 @@ class DataSourceConfiguration { val config = HikariConfig() config.jdbcUrl = mainDataSourceUrl config.driverClassName = DRIVER_CLASS_NAME - config.maximumPoolSize = 8 + config.maximumPoolSize = 1 config.minimumIdle = 1 return HikariDataSource(config) } diff --git a/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameRepository.kt b/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameRepository.kt index f4a04594c..77bc8eed5 100644 --- a/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameRepository.kt +++ b/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameRepository.kt @@ -5,10 +5,15 @@ import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.Modifying import org.springframework.data.jpa.repository.Query import org.springframework.stereotype.Repository +import org.springframework.transaction.annotation.Isolation +import org.springframework.transaction.annotation.Propagation +import org.springframework.transaction.annotation.Transactional @Repository +@Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW, isolation = Isolation.SERIALIZABLE) interface CalibrationFrameRepository : JpaRepository { + @Transactional(readOnly = true) @Query("SELECT frame FROM CalibrationFrameEntity frame WHERE frame.camera = :#{#camera.name}") fun findAll(camera: Camera): List @@ -25,6 +30,7 @@ interface CalibrationFrameRepository : JpaRepository @Query( @@ -34,6 +40,7 @@ interface CalibrationFrameRepository : JpaRepository @Query( @@ -42,5 +49,6 @@ interface CalibrationFrameRepository : JpaRepository } diff --git a/api/src/main/kotlin/nebulosa/api/locations/LocationRepository.kt b/api/src/main/kotlin/nebulosa/api/locations/LocationRepository.kt index 4b78cc76e..e5050aa66 100644 --- a/api/src/main/kotlin/nebulosa/api/locations/LocationRepository.kt +++ b/api/src/main/kotlin/nebulosa/api/locations/LocationRepository.kt @@ -4,8 +4,12 @@ import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.Modifying import org.springframework.data.jpa.repository.Query import org.springframework.stereotype.Repository +import org.springframework.transaction.annotation.Isolation +import org.springframework.transaction.annotation.Propagation +import org.springframework.transaction.annotation.Transactional @Repository +@Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW, isolation = Isolation.SERIALIZABLE) interface LocationRepository : JpaRepository { fun findFirstByOrderById(): LocationEntity? diff --git a/api/src/main/kotlin/nebulosa/api/preferences/PreferenceRepository.kt b/api/src/main/kotlin/nebulosa/api/preferences/PreferenceRepository.kt index 8f9bb7837..d9641be71 100644 --- a/api/src/main/kotlin/nebulosa/api/preferences/PreferenceRepository.kt +++ b/api/src/main/kotlin/nebulosa/api/preferences/PreferenceRepository.kt @@ -3,8 +3,12 @@ package nebulosa.api.preferences import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.Query import org.springframework.stereotype.Repository +import org.springframework.transaction.annotation.Isolation +import org.springframework.transaction.annotation.Propagation +import org.springframework.transaction.annotation.Transactional @Repository +@Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW, isolation = Isolation.SERIALIZABLE) interface PreferenceRepository : JpaRepository { @Query("SELECT p.key FROM PreferenceEntity p") From 3826aa205c1a62b948574810f880d540fa170442 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Dec 2023 03:43:09 +0000 Subject: [PATCH 18/87] [api]: Bump org.flywaydb:flyway-core from 9.22.3 to 10.1.0 Bumps [org.flywaydb:flyway-core](https://github.com/flyway/flyway) from 9.22.3 to 10.1.0. - [Release notes](https://github.com/flyway/flyway/releases) - [Commits](https://github.com/flyway/flyway/compare/flyway-9.22.3...flyway-10.1.0) --- updated-dependencies: - dependency-name: org.flywaydb:flyway-core dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- settings.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.gradle.kts b/settings.gradle.kts index 6962eac11..7e69c9d6b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -33,7 +33,7 @@ dependencyResolutionManagement { library("oshi", "com.github.oshi:oshi-core:6.4.8") library("timeshape", "net.iakovlev:timeshape:2022g.17") library("sqlite", "org.xerial:sqlite-jdbc:3.44.1.0") - library("flyway", "org.flywaydb:flyway-core:9.22.3") + library("flyway", "org.flywaydb:flyway-core:10.1.0") library("jna", "net.java.dev.jna:jna:5.13.0") library("kotest-assertions-core", "io.kotest:kotest-assertions-core:5.8.0") library("kotest-runner-junit5", "io.kotest:kotest-runner-junit5:5.8.0") From dc07618dfed3d8a539151cddf8eb65fd8391c6f0 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Sat, 9 Dec 2023 11:15:38 -0300 Subject: [PATCH 19/87] [desktop]: Bump axios * Fix Axios Cross-Site Request Forgery Vulnerability --- desktop/package-lock.json | 21 ++++++--------------- desktop/package.json | 3 +++ 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/desktop/package-lock.json b/desktop/package-lock.json index 6990258dd..c6020964c 100644 --- a/desktop/package-lock.json +++ b/desktop/package-lock.json @@ -4811,12 +4811,14 @@ } }, "node_modules/axios": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", - "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", + "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==", "dev": true, "dependencies": { - "follow-redirects": "^1.14.0" + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" } }, "node_modules/babel-loader": { @@ -16668,17 +16670,6 @@ "node": ">=12.0.0" } }, - "node_modules/wait-on/node_modules/axios": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", - "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==", - "dev": true, - "dependencies": { - "follow-redirects": "^1.15.0", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, "node_modules/watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", diff --git a/desktop/package.json b/desktop/package.json index fd9992c5b..5bea9e851 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -74,6 +74,9 @@ "typescript": "5.2.2", "wait-on": "7.2.0" }, + "overrides": { + "axios": "1.6.2" + }, "engines": { "node": ">= 20.9.0" }, From 5fba99526808dc9ed45722144e1957b2b52a4541 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Sat, 9 Dec 2023 18:33:15 -0300 Subject: [PATCH 20/87] [github-actions]: Update dependabot.yml --- .github/dependabot.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index d03b43554..5f1f1ea80 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -10,7 +10,6 @@ updates: directory: "/" schedule: interval: "monthly" - day: "friday" open-pull-requests-limit: 16 target-branch: "dev" commit-message: @@ -28,6 +27,9 @@ updates: okhttp: patterns: - "com.squareup.okhttp3*" + rx: + patterns: + - "io.reactivex.rxjava3*" jackson: patterns: - "com.fasterxml.jackson*" @@ -36,7 +38,6 @@ updates: directory: "/desktop" schedule: interval: "monthly" - day: "friday" open-pull-requests-limit: 64 target-branch: "dev" commit-message: @@ -45,12 +46,14 @@ updates: angular: patterns: - "@angular*" + types: + patterns: + - "@types*" - package-ecosystem: "npm" directory: "/desktop/app" schedule: interval: "monthly" - day: "friday" target-branch: "dev" commit-message: prefix: "[desktop]" From ab7e600b5b1208e2360aeac0fd3465fab913fbfd Mon Sep 17 00:00:00 2001 From: tiagohm Date: Sat, 9 Dec 2023 18:42:27 -0300 Subject: [PATCH 21/87] [desktop]: Don't use script to copy files --- desktop/app/main.ts | 2 +- desktop/copyFiles.js | 6 ------ desktop/package.json | 11 ++--------- desktop/tsconfig.serve.json | 2 +- 4 files changed, 4 insertions(+), 17 deletions(-) delete mode 100644 desktop/copyFiles.js diff --git a/desktop/app/main.ts b/desktop/app/main.ts index b715a727a..42c727e7d 100644 --- a/desktop/app/main.ts +++ b/desktop/app/main.ts @@ -2,7 +2,7 @@ import { Client } from '@stomp/stompjs' import { BrowserWindow, Menu, Notification, app, dialog, ipcMain, screen, shell } from 'electron' import { ChildProcessWithoutNullStreams, spawn } from 'node:child_process' import * as path from 'path' -import { InternalEventType, MessageEvent, NotificationEvent, OpenDirectory, OpenFile, OpenWindow } from './types' +import { InternalEventType, MessageEvent, NotificationEvent, OpenDirectory, OpenFile, OpenWindow } from '../src/shared/types' import { WebSocket } from 'ws' Object.assign(global, { WebSocket }) diff --git a/desktop/copyFiles.js b/desktop/copyFiles.js deleted file mode 100644 index 34480147a..000000000 --- a/desktop/copyFiles.js +++ /dev/null @@ -1,6 +0,0 @@ -const fs = require('fs') -const { copyFiles } = require('./package.json') - -for (const file of copyFiles) { - fs.copyFile(file.from, file.to, () => null) -} diff --git a/desktop/package.json b/desktop/package.json index 5bea9e851..c6e56157a 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -13,10 +13,9 @@ "scripts": { "postinstall": "electron-builder install-app-deps", "ng": "ng", - "copy:files": "node copyFiles.js", - "start": "npm run copy:files && npm-run-all -p electron:serve ng:serve", + "start": "npm-run-all -p electron:serve ng:serve", "ng:serve": "ng serve -c web", - "build": "npm run copy:files && npm run electron:serve-tsc && ng build --base-href ./", + "build": "npm run electron:serve-tsc && ng build --base-href ./", "build:dev": "npm run build -- -c dev", "build:prod": "npm run build -- -c production", "web:build": "npm run build -- -c web-production", @@ -82,11 +81,5 @@ }, "browserslist": [ "chrome 114" - ], - "copyFiles": [ - { - "from": "src/shared/types.ts", - "to": "app/types.ts" - } ] } diff --git a/desktop/tsconfig.serve.json b/desktop/tsconfig.serve.json index 1f579c034..40e15a9a4 100644 --- a/desktop/tsconfig.serve.json +++ b/desktop/tsconfig.serve.json @@ -22,7 +22,7 @@ }, "files": [ "app/main.ts", - "app/types.ts" + "src/shared/types.ts" ], "exclude": [ "node_modules", From 8e4cacbdf8eda7c52870fbfb395dbf5f4612c5f6 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Sun, 10 Dec 2023 23:19:13 -0300 Subject: [PATCH 22/87] [api]: Add angle param resolver; Improve param resolvers --- .../polar/PolarAlignmentController.kt | 6 +- .../nebulosa/api/atlas/SkyAtlasController.kt | 76 ++++++++-------- .../DateAndTimeMethodArgumentResolver.kt | 58 ------------- .../beans/EntityByMethodArgumentResolver.kt | 86 ------------------- .../api/beans/annotations/AngleParam.kt | 10 +++ .../{DateAndTime.kt => DateAndTimeParam.kt} | 2 +- .../{EntityBy.kt => EntityParam.kt} | 2 +- .../beans/configurations/BeanConfiguration.kt | 12 +-- .../api/beans/converters/AngleDeserializer.kt | 17 ++++ .../api/beans/converters/ConverterHelper.kt | 24 ++++++ .../converters/DeclinationDeserializer.kt | 3 + .../converters/RightAscensionDeserializer.kt | 3 + .../AngleParamMethodArgumentResolver.kt | 33 +++++++ .../DateAndTimeParamMethodArgumentResolver.kt | 47 ++++++++++ .../EntityParamMethodArgumentResolver.kt | 74 ++++++++++++++++ .../calibration/CalibrationFrameController.kt | 6 +- .../nebulosa/api/cameras/CameraController.kt | 18 ++-- .../api/focusers/FocuserController.kt | 18 ++-- .../api/guiding/GuideOutputController.kt | 10 +-- .../nebulosa/api/image/ImageController.kt | 4 +- .../nebulosa/api/mounts/MountController.kt | 50 +++++------ .../api/solver/PlateSolverController.kt | 12 +-- .../nebulosa/api/wheels/WheelController.kt | 12 +-- .../quad/QuadDatabaseCellFileDescriptor.kt | 1 - .../watney/star/detection/StarPixelBin.kt | 1 - 25 files changed, 325 insertions(+), 260 deletions(-) delete mode 100644 api/src/main/kotlin/nebulosa/api/beans/DateAndTimeMethodArgumentResolver.kt delete mode 100644 api/src/main/kotlin/nebulosa/api/beans/EntityByMethodArgumentResolver.kt create mode 100644 api/src/main/kotlin/nebulosa/api/beans/annotations/AngleParam.kt rename api/src/main/kotlin/nebulosa/api/beans/annotations/{DateAndTime.kt => DateAndTimeParam.kt} (85%) rename api/src/main/kotlin/nebulosa/api/beans/annotations/{EntityBy.kt => EntityParam.kt} (61%) create mode 100644 api/src/main/kotlin/nebulosa/api/beans/converters/AngleDeserializer.kt create mode 100644 api/src/main/kotlin/nebulosa/api/beans/converters/ConverterHelper.kt create mode 100644 api/src/main/kotlin/nebulosa/api/beans/converters/DeclinationDeserializer.kt create mode 100644 api/src/main/kotlin/nebulosa/api/beans/converters/RightAscensionDeserializer.kt create mode 100644 api/src/main/kotlin/nebulosa/api/beans/resolvers/AngleParamMethodArgumentResolver.kt create mode 100644 api/src/main/kotlin/nebulosa/api/beans/resolvers/DateAndTimeParamMethodArgumentResolver.kt create mode 100644 api/src/main/kotlin/nebulosa/api/beans/resolvers/EntityParamMethodArgumentResolver.kt diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/PolarAlignmentController.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/PolarAlignmentController.kt index 9d79844e8..8ca0bb346 100644 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/PolarAlignmentController.kt +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/PolarAlignmentController.kt @@ -1,7 +1,7 @@ package nebulosa.api.alignment.polar import nebulosa.api.alignment.polar.darv.DARVStart -import nebulosa.api.beans.annotations.EntityBy +import nebulosa.api.beans.annotations.EntityParam import nebulosa.indi.device.camera.Camera import nebulosa.indi.device.guide.GuideOutput import org.springframework.web.bind.annotation.PutMapping @@ -17,14 +17,14 @@ class PolarAlignmentController( @PutMapping("darv/{camera}/{guideOutput}/start") fun darvStart( - @EntityBy camera: Camera, @EntityBy guideOutput: GuideOutput, + @EntityParam camera: Camera, @EntityParam guideOutput: GuideOutput, @RequestBody body: DARVStart, ) { polarAlignmentService.darvStart(camera, guideOutput, body) } @PutMapping("darv/{camera}/{guideOutput}/stop") - fun darvStop(@EntityBy camera: Camera, @EntityBy guideOutput: GuideOutput) { + fun darvStop(@EntityParam camera: Camera, @EntityParam guideOutput: GuideOutput) { polarAlignmentService.darvStop(camera, guideOutput) } } diff --git a/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasController.kt b/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasController.kt index 11756d57b..e083648de 100644 --- a/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasController.kt +++ b/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasController.kt @@ -4,8 +4,8 @@ import jakarta.servlet.http.HttpServletResponse import jakarta.validation.Valid import jakarta.validation.constraints.Min import jakarta.validation.constraints.NotBlank -import nebulosa.api.beans.annotations.DateAndTime -import nebulosa.api.beans.annotations.EntityBy +import nebulosa.api.beans.annotations.DateAndTimeParam +import nebulosa.api.beans.annotations.EntityParam import nebulosa.api.locations.LocationEntity import nebulosa.math.deg import nebulosa.math.hours @@ -27,42 +27,42 @@ class SkyAtlasController( @GetMapping("sun/position") fun positionOfSun( - @EntityBy location: LocationEntity, - @DateAndTime dateTime: LocalDateTime, + @EntityParam location: LocationEntity, + @DateAndTimeParam dateTime: LocalDateTime, ) = skyAtlasService.positionOfSun(location, dateTime) @GetMapping("sun/altitude-points") fun altitudePointsOfSun( - @EntityBy location: LocationEntity, - @DateAndTime dateTime: LocalDateTime, + @EntityParam location: LocationEntity, + @DateAndTimeParam dateTime: LocalDateTime, @RequestParam(required = false, defaultValue = "1") @Valid @Min(1) stepSize: Int, ) = skyAtlasService.altitudePointsOfSun(location, dateTime.toLocalDate(), stepSize) @GetMapping("moon/position") fun positionOfMoon( - @EntityBy location: LocationEntity, - @DateAndTime dateTime: LocalDateTime, + @EntityParam location: LocationEntity, + @DateAndTimeParam dateTime: LocalDateTime, ) = skyAtlasService.positionOfMoon(location, dateTime) @GetMapping("moon/altitude-points") fun altitudePointsOfMoon( - @EntityBy location: LocationEntity, - @DateAndTime dateTime: LocalDateTime, + @EntityParam location: LocationEntity, + @DateAndTimeParam dateTime: LocalDateTime, @RequestParam(required = false, defaultValue = "1") stepSize: Int, ) = skyAtlasService.altitudePointsOfMoon(location, dateTime.toLocalDate(), stepSize) @GetMapping("planets/{code}/position") fun positionOfPlanet( @PathVariable code: String, - @EntityBy location: LocationEntity, - @DateAndTime dateTime: LocalDateTime, + @EntityParam location: LocationEntity, + @DateAndTimeParam dateTime: LocalDateTime, ) = skyAtlasService.positionOfPlanet(location, code, dateTime) @GetMapping("planets/{code}/altitude-points") fun altitudePointsOfPlanet( @PathVariable code: String, - @EntityBy location: LocationEntity, - @DateAndTime dateTime: LocalDateTime, + @EntityParam location: LocationEntity, + @DateAndTimeParam dateTime: LocalDateTime, @RequestParam(required = false, defaultValue = "1") stepSize: Int, ) = skyAtlasService.altitudePointsOfPlanet(location, code, dateTime.toLocalDate(), stepSize) @@ -71,16 +71,16 @@ class SkyAtlasController( @GetMapping("stars/{star}/position") fun positionOfStar( - @EntityBy star: StarEntity, - @EntityBy location: LocationEntity, - @DateAndTime dateTime: LocalDateTime, + @EntityParam star: StarEntity, + @EntityParam location: LocationEntity, + @DateAndTimeParam dateTime: LocalDateTime, ) = skyAtlasService.positionOfStar(location, star, dateTime) @GetMapping("stars/{star}/altitude-points") fun altitudePointsOfStar( - @EntityBy star: StarEntity, - @EntityBy location: LocationEntity, - @DateAndTime dateTime: LocalDateTime, + @EntityParam star: StarEntity, + @EntityParam location: LocationEntity, + @DateAndTimeParam dateTime: LocalDateTime, @RequestParam(required = false, defaultValue = "1") stepSize: Int, ) = skyAtlasService.altitudePointsOfStar(location, star, dateTime.toLocalDate(), stepSize) @@ -104,16 +104,16 @@ class SkyAtlasController( @GetMapping("dsos/{dso}/position") fun positionOfDSO( - @EntityBy dso: DeepSkyObjectEntity, - @EntityBy location: LocationEntity, - @DateAndTime dateTime: LocalDateTime, + @EntityParam dso: DeepSkyObjectEntity, + @EntityParam location: LocationEntity, + @DateAndTimeParam dateTime: LocalDateTime, ) = skyAtlasService.positionOfDSO(location, dso, dateTime) @GetMapping("dsos/{dso}/altitude-points") fun altitudePointsOfDSO( - @EntityBy dso: DeepSkyObjectEntity, - @EntityBy location: LocationEntity, - @DateAndTime dateTime: LocalDateTime, + @EntityParam dso: DeepSkyObjectEntity, + @EntityParam location: LocationEntity, + @DateAndTimeParam dateTime: LocalDateTime, @RequestParam(required = false, defaultValue = "1") stepSize: Int, ) = skyAtlasService.altitudePointsOfDSO(location, dso, dateTime.toLocalDate(), stepSize) @@ -138,15 +138,15 @@ class SkyAtlasController( @GetMapping("simbad/{id}/position") fun positionOfSimbad( @PathVariable id: Long, - @EntityBy location: LocationEntity, - @DateAndTime dateTime: LocalDateTime, + @EntityParam location: LocationEntity, + @DateAndTimeParam dateTime: LocalDateTime, ) = skyAtlasService.positionOfSimbad(location, id, dateTime) @GetMapping("simbad/{id}/altitude-points") fun altitudePointsOfSimbad( @PathVariable id: Long, - @EntityBy location: LocationEntity, - @DateAndTime dateTime: LocalDateTime, + @EntityParam location: LocationEntity, + @DateAndTimeParam dateTime: LocalDateTime, @RequestParam(required = false, defaultValue = "1") stepSize: Int, ) = skyAtlasService.altitudePointsOfSimbad(location, id, dateTime.toLocalDate(), stepSize) @@ -170,16 +170,16 @@ class SkyAtlasController( @GetMapping("satellites/{satellite}/position") fun positionOfSatellite( - @EntityBy satellite: SatelliteEntity, - @EntityBy location: LocationEntity, - @DateAndTime dateTime: LocalDateTime, + @EntityParam satellite: SatelliteEntity, + @EntityParam location: LocationEntity, + @DateAndTimeParam dateTime: LocalDateTime, ) = skyAtlasService.positionOfSatellite(location, satellite, dateTime) @GetMapping("satellites/{satellite}/altitude-points") fun altitudePointsOfSatellite( - @EntityBy satellite: SatelliteEntity, - @EntityBy location: LocationEntity, - @DateAndTime dateTime: LocalDateTime, + @EntityParam satellite: SatelliteEntity, + @EntityParam location: LocationEntity, + @DateAndTimeParam dateTime: LocalDateTime, @RequestParam(required = false, defaultValue = "1") stepSize: Int, ) = skyAtlasService.altitudePointsOfSatellite(location, satellite, dateTime.toLocalDate(), stepSize) @@ -191,7 +191,7 @@ class SkyAtlasController( @GetMapping("twilight") fun twilight( - @EntityBy location: LocationEntity, - @DateAndTime dateTime: LocalDateTime, + @EntityParam location: LocationEntity, + @DateAndTimeParam dateTime: LocalDateTime, ) = skyAtlasService.twilight(location, dateTime.toLocalDate()) } diff --git a/api/src/main/kotlin/nebulosa/api/beans/DateAndTimeMethodArgumentResolver.kt b/api/src/main/kotlin/nebulosa/api/beans/DateAndTimeMethodArgumentResolver.kt deleted file mode 100644 index 28aa879c1..000000000 --- a/api/src/main/kotlin/nebulosa/api/beans/DateAndTimeMethodArgumentResolver.kt +++ /dev/null @@ -1,58 +0,0 @@ -package nebulosa.api.beans - -import jakarta.servlet.http.HttpServletRequest -import nebulosa.api.beans.annotations.DateAndTime -import org.springframework.core.MethodParameter -import org.springframework.stereotype.Component -import org.springframework.web.bind.support.WebDataBinderFactory -import org.springframework.web.context.request.NativeWebRequest -import org.springframework.web.method.support.HandlerMethodArgumentResolver -import org.springframework.web.method.support.ModelAndViewContainer -import org.springframework.web.servlet.HandlerMapping -import java.time.LocalDate -import java.time.LocalDateTime -import java.time.LocalTime -import java.time.format.DateTimeFormatter - -@Component -class DateAndTimeMethodArgumentResolver : HandlerMethodArgumentResolver { - - override fun supportsParameter(parameter: MethodParameter): Boolean { - return parameter.hasParameterAnnotation(DateAndTime::class.java) - } - - override fun resolveArgument( - parameter: MethodParameter, - mavContainer: ModelAndViewContainer?, - webRequest: NativeWebRequest, - binderFactory: WebDataBinderFactory? - ): Any? { - val dateAndTime = parameter.getParameterAnnotation(DateAndTime::class.java)!! - - val dateValue = webRequest.pathVariables()["date"] - ?: webRequest.getParameter("date") - val timeValue = webRequest.pathVariables()["time"] - ?: webRequest.getParameter("time") - - val date = dateValue?.ifBlank { null } - ?.let { LocalDate.parse(it, DateTimeFormatter.ofPattern(dateAndTime.datePattern)) } - ?: LocalDate.now() - - val time = timeValue?.ifBlank { null } - ?.let { LocalTime.parse(it, DateTimeFormatter.ofPattern(dateAndTime.timePattern)) } - ?: LocalTime.now() - - return LocalDateTime.of(date, time) - .let { if (dateAndTime.noSeconds) it.withSecond(0).withNano(0) else it } - } - - companion object { - - @JvmStatic - @Suppress("UNCHECKED_CAST") - private fun NativeWebRequest.pathVariables(): Map { - val httpServletRequest = getNativeRequest(HttpServletRequest::class.java)!! - return httpServletRequest.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE) as Map - } - } -} diff --git a/api/src/main/kotlin/nebulosa/api/beans/EntityByMethodArgumentResolver.kt b/api/src/main/kotlin/nebulosa/api/beans/EntityByMethodArgumentResolver.kt deleted file mode 100644 index 31f06c603..000000000 --- a/api/src/main/kotlin/nebulosa/api/beans/EntityByMethodArgumentResolver.kt +++ /dev/null @@ -1,86 +0,0 @@ -package nebulosa.api.beans - -import jakarta.servlet.http.HttpServletRequest -import nebulosa.api.atlas.* -import nebulosa.api.beans.annotations.EntityBy -import nebulosa.api.connection.ConnectionService -import nebulosa.api.locations.LocationEntity -import nebulosa.api.locations.LocationRepository -import nebulosa.indi.device.camera.Camera -import nebulosa.indi.device.filterwheel.FilterWheel -import nebulosa.indi.device.focuser.Focuser -import nebulosa.indi.device.guide.GuideOutput -import nebulosa.indi.device.mount.Mount -import org.springframework.core.MethodParameter -import org.springframework.data.repository.findByIdOrNull -import org.springframework.http.HttpStatus -import org.springframework.stereotype.Component -import org.springframework.web.bind.support.WebDataBinderFactory -import org.springframework.web.context.request.NativeWebRequest -import org.springframework.web.method.support.HandlerMethodArgumentResolver -import org.springframework.web.method.support.ModelAndViewContainer -import org.springframework.web.server.ResponseStatusException -import org.springframework.web.servlet.HandlerMapping - -@Component -class EntityByMethodArgumentResolver( - private val locationRepository: LocationRepository, - private val starRepository: StarRepository, - private val deepSkyObjectRepository: DeepSkyObjectRepository, - private val satelliteRepository: SatelliteRepository, - private val connectionService: ConnectionService, -) : HandlerMethodArgumentResolver { - - override fun supportsParameter(parameter: MethodParameter): Boolean { - return parameter.hasParameterAnnotation(EntityBy::class.java) - } - - override fun resolveArgument( - parameter: MethodParameter, - mavContainer: ModelAndViewContainer?, - webRequest: NativeWebRequest, - binderFactory: WebDataBinderFactory? - ): Any? { - val entityBy = parameter.getParameterAnnotation(EntityBy::class.java)!! - val parameterType = parameter.parameterType - val parameterName = parameter.parameterName ?: "id" - val parameterValue = webRequest.pathVariables()[parameterName] - ?: webRequest.getParameter(parameterName) - - val entity = entityByParameterValue(parameterType, parameterValue) - - if (entityBy.required && entity == null) { - val message = "Cannot found a ${parameterType.simpleName} entity with name [$parameterValue]" - throw ResponseStatusException(HttpStatus.NOT_FOUND, message) - } - - return entity - } - - private fun entityByParameterValue(parameterType: Class<*>, parameterValue: String?): Any? { - if (parameterValue.isNullOrBlank()) return null - - return when (parameterType) { - LocationEntity::class.java -> locationRepository.findByIdOrNull(parameterValue.toLong()) - StarEntity::class.java -> starRepository.findByIdOrNull(parameterValue.toLong()) - DeepSkyObjectEntity::class.java -> deepSkyObjectRepository.findByIdOrNull(parameterValue.toLong()) - SatelliteEntity::class.java -> satelliteRepository.findByIdOrNull(parameterValue.toLong()) - Camera::class.java -> connectionService.camera(parameterValue) - Mount::class.java -> connectionService.mount(parameterValue) - Focuser::class.java -> connectionService.focuser(parameterValue) - FilterWheel::class.java -> connectionService.wheel(parameterValue) - GuideOutput::class.java -> connectionService.guideOutput(parameterValue) - else -> null - } - } - - companion object { - - @JvmStatic - @Suppress("UNCHECKED_CAST") - private fun NativeWebRequest.pathVariables(): Map { - val httpServletRequest = getNativeRequest(HttpServletRequest::class.java)!! - return httpServletRequest.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE) as Map - } - } -} diff --git a/api/src/main/kotlin/nebulosa/api/beans/annotations/AngleParam.kt b/api/src/main/kotlin/nebulosa/api/beans/annotations/AngleParam.kt new file mode 100644 index 000000000..a8d1266cb --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/beans/annotations/AngleParam.kt @@ -0,0 +1,10 @@ +package nebulosa.api.beans.annotations + +@Retention +@Target(AnnotationTarget.VALUE_PARAMETER) +annotation class AngleParam( + val name: String = "", + val required: Boolean = true, + val isHours: Boolean = false, + val defaultValue: String = "", +) diff --git a/api/src/main/kotlin/nebulosa/api/beans/annotations/DateAndTime.kt b/api/src/main/kotlin/nebulosa/api/beans/annotations/DateAndTimeParam.kt similarity index 85% rename from api/src/main/kotlin/nebulosa/api/beans/annotations/DateAndTime.kt rename to api/src/main/kotlin/nebulosa/api/beans/annotations/DateAndTimeParam.kt index 67ec80781..2dcdfe264 100644 --- a/api/src/main/kotlin/nebulosa/api/beans/annotations/DateAndTime.kt +++ b/api/src/main/kotlin/nebulosa/api/beans/annotations/DateAndTimeParam.kt @@ -2,7 +2,7 @@ package nebulosa.api.beans.annotations @Retention @Target(AnnotationTarget.VALUE_PARAMETER) -annotation class DateAndTime( +annotation class DateAndTimeParam( val datePattern: String = "yyyy-MM-dd", val timePattern: String = "HH:mm", val noSeconds: Boolean = true, diff --git a/api/src/main/kotlin/nebulosa/api/beans/annotations/EntityBy.kt b/api/src/main/kotlin/nebulosa/api/beans/annotations/EntityParam.kt similarity index 61% rename from api/src/main/kotlin/nebulosa/api/beans/annotations/EntityBy.kt rename to api/src/main/kotlin/nebulosa/api/beans/annotations/EntityParam.kt index ed3846b4a..6f99c36fb 100644 --- a/api/src/main/kotlin/nebulosa/api/beans/annotations/EntityBy.kt +++ b/api/src/main/kotlin/nebulosa/api/beans/annotations/EntityParam.kt @@ -2,4 +2,4 @@ package nebulosa.api.beans.annotations @Retention @Target(AnnotationTarget.VALUE_PARAMETER) -annotation class EntityBy(val required: Boolean = true) +annotation class EntityParam(val required: Boolean = true) diff --git a/api/src/main/kotlin/nebulosa/api/beans/configurations/BeanConfiguration.kt b/api/src/main/kotlin/nebulosa/api/beans/configurations/BeanConfiguration.kt index d4f62364c..47f1130fa 100644 --- a/api/src/main/kotlin/nebulosa/api/beans/configurations/BeanConfiguration.kt +++ b/api/src/main/kotlin/nebulosa/api/beans/configurations/BeanConfiguration.kt @@ -5,8 +5,6 @@ import com.fasterxml.jackson.databind.deser.std.StdDeserializer import com.fasterxml.jackson.databind.module.SimpleModule import com.fasterxml.jackson.databind.ser.std.StdSerializer import com.fasterxml.jackson.module.kotlin.kotlinModule -import nebulosa.api.beans.DateAndTimeMethodArgumentResolver -import nebulosa.api.beans.EntityByMethodArgumentResolver import nebulosa.common.concurrency.DaemonThreadFactory import nebulosa.common.concurrency.Incrementer import nebulosa.common.json.PathDeserializer @@ -140,8 +138,9 @@ class BeanConfiguration { @Bean fun webMvcConfigurer( - entityByMethodArgumentResolver: EntityByMethodArgumentResolver, - dateAndTimeMethodArgumentResolver: DateAndTimeMethodArgumentResolver, + entityParamMethodArgumentResolver: HandlerMethodArgumentResolver, + dateAndTimeParamMethodArgumentResolver: HandlerMethodArgumentResolver, + angleParamMethodArgumentResolver: HandlerMethodArgumentResolver, ) = object : WebMvcConfigurer { override fun extendMessageConverters(converters: MutableList>) { @@ -158,8 +157,9 @@ class BeanConfiguration { } override fun addArgumentResolvers(resolvers: MutableList) { - resolvers.add(entityByMethodArgumentResolver) - resolvers.add(dateAndTimeMethodArgumentResolver) + resolvers.add(entityParamMethodArgumentResolver) + resolvers.add(dateAndTimeParamMethodArgumentResolver) + resolvers.add(angleParamMethodArgumentResolver) } } diff --git a/api/src/main/kotlin/nebulosa/api/beans/converters/AngleDeserializer.kt b/api/src/main/kotlin/nebulosa/api/beans/converters/AngleDeserializer.kt new file mode 100644 index 000000000..3cd2409c2 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/beans/converters/AngleDeserializer.kt @@ -0,0 +1,17 @@ +package nebulosa.api.beans.converters + +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.deser.std.StdDeserializer +import nebulosa.math.Angle + +abstract class AngleDeserializer( + private val isHours: Boolean = false, + private val decimalIsHours: Boolean = isHours, + private val defaultValue: Angle = Double.NaN, +) : StdDeserializer(Double::class.java) { + + override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Double { + return Angle(p.text, isHours, decimalIsHours, defaultValue) + } +} diff --git a/api/src/main/kotlin/nebulosa/api/beans/converters/ConverterHelper.kt b/api/src/main/kotlin/nebulosa/api/beans/converters/ConverterHelper.kt new file mode 100644 index 000000000..870f7c4ab --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/beans/converters/ConverterHelper.kt @@ -0,0 +1,24 @@ +package nebulosa.api.beans.converters + +import jakarta.servlet.http.HttpServletRequest +import org.springframework.core.MethodParameter +import org.springframework.web.context.request.NativeWebRequest +import org.springframework.web.servlet.HandlerMapping + +@Suppress("UNCHECKED_CAST") +val NativeWebRequest.pathVariables + get() = getNativeRequest(HttpServletRequest::class.java)!! + .getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE) as Map + +fun NativeWebRequest.parameter(name: String) = + pathVariables[name]?.ifBlank { null } + ?: getParameter(name)?.ifBlank { null } + ?: getNativeRequest(HttpServletRequest::class.java)!!.getParameter(name)?.ifBlank { null } + +inline fun MethodParameter.hasAnnotation(): Boolean { + return hasParameterAnnotation(T::class.java) +} + +inline fun MethodParameter.annotation(): T? { + return getParameterAnnotation(T::class.java) +} diff --git a/api/src/main/kotlin/nebulosa/api/beans/converters/DeclinationDeserializer.kt b/api/src/main/kotlin/nebulosa/api/beans/converters/DeclinationDeserializer.kt new file mode 100644 index 000000000..e0951b8c0 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/beans/converters/DeclinationDeserializer.kt @@ -0,0 +1,3 @@ +package nebulosa.api.beans.converters + +class DeclinationDeserializer : AngleDeserializer(true) diff --git a/api/src/main/kotlin/nebulosa/api/beans/converters/RightAscensionDeserializer.kt b/api/src/main/kotlin/nebulosa/api/beans/converters/RightAscensionDeserializer.kt new file mode 100644 index 000000000..a5276730e --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/beans/converters/RightAscensionDeserializer.kt @@ -0,0 +1,3 @@ +package nebulosa.api.beans.converters + +class RightAscensionDeserializer : AngleDeserializer(true) diff --git a/api/src/main/kotlin/nebulosa/api/beans/resolvers/AngleParamMethodArgumentResolver.kt b/api/src/main/kotlin/nebulosa/api/beans/resolvers/AngleParamMethodArgumentResolver.kt new file mode 100644 index 000000000..f1e649332 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/beans/resolvers/AngleParamMethodArgumentResolver.kt @@ -0,0 +1,33 @@ +package nebulosa.api.beans.resolvers + +import nebulosa.api.beans.annotations.AngleParam +import nebulosa.api.beans.converters.annotation +import nebulosa.api.beans.converters.hasAnnotation +import nebulosa.api.beans.converters.parameter +import nebulosa.math.Angle +import org.springframework.core.MethodParameter +import org.springframework.stereotype.Component +import org.springframework.web.bind.support.WebDataBinderFactory +import org.springframework.web.context.request.NativeWebRequest +import org.springframework.web.method.support.HandlerMethodArgumentResolver +import org.springframework.web.method.support.ModelAndViewContainer + +@Component +class AngleParamMethodArgumentResolver : HandlerMethodArgumentResolver { + + override fun supportsParameter(parameter: MethodParameter): Boolean { + return parameter.hasAnnotation() + } + + override fun resolveArgument( + parameter: MethodParameter, + mavContainer: ModelAndViewContainer?, + webRequest: NativeWebRequest, + binderFactory: WebDataBinderFactory?, + ): Any? { + val param = parameter.annotation()!! + val parameterName = param.name.ifBlank { null } ?: parameter.parameterName!! + val parameterValue = webRequest.parameter(parameterName) ?: param.defaultValue + return Angle(parameterValue, param.isHours) + } +} diff --git a/api/src/main/kotlin/nebulosa/api/beans/resolvers/DateAndTimeParamMethodArgumentResolver.kt b/api/src/main/kotlin/nebulosa/api/beans/resolvers/DateAndTimeParamMethodArgumentResolver.kt new file mode 100644 index 000000000..dad1a9c01 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/beans/resolvers/DateAndTimeParamMethodArgumentResolver.kt @@ -0,0 +1,47 @@ +package nebulosa.api.beans.resolvers + +import nebulosa.api.beans.annotations.DateAndTimeParam +import nebulosa.api.beans.converters.annotation +import nebulosa.api.beans.converters.hasAnnotation +import nebulosa.api.beans.converters.parameter +import org.springframework.core.MethodParameter +import org.springframework.stereotype.Component +import org.springframework.web.bind.support.WebDataBinderFactory +import org.springframework.web.context.request.NativeWebRequest +import org.springframework.web.method.support.HandlerMethodArgumentResolver +import org.springframework.web.method.support.ModelAndViewContainer +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.LocalTime +import java.time.format.DateTimeFormatter + +@Component +class DateAndTimeParamMethodArgumentResolver : HandlerMethodArgumentResolver { + + override fun supportsParameter(parameter: MethodParameter): Boolean { + return parameter.hasAnnotation() + } + + override fun resolveArgument( + parameter: MethodParameter, + mavContainer: ModelAndViewContainer?, + webRequest: NativeWebRequest, + binderFactory: WebDataBinderFactory?, + ): Any? { + val dateAndTimeParam = parameter.annotation()!! + + val dateValue = webRequest.parameter("date") + val timeValue = webRequest.parameter("time") + + val date = dateValue?.ifBlank { null } + ?.let { LocalDate.parse(it, DateTimeFormatter.ofPattern(dateAndTimeParam.datePattern)) } + ?: LocalDate.now() + + val time = timeValue?.ifBlank { null } + ?.let { LocalTime.parse(it, DateTimeFormatter.ofPattern(dateAndTimeParam.timePattern)) } + ?: LocalTime.now() + + return LocalDateTime.of(date, time) + .let { if (dateAndTimeParam.noSeconds) it.withSecond(0).withNano(0) else it } + } +} diff --git a/api/src/main/kotlin/nebulosa/api/beans/resolvers/EntityParamMethodArgumentResolver.kt b/api/src/main/kotlin/nebulosa/api/beans/resolvers/EntityParamMethodArgumentResolver.kt new file mode 100644 index 000000000..6f2ff6efe --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/beans/resolvers/EntityParamMethodArgumentResolver.kt @@ -0,0 +1,74 @@ +package nebulosa.api.beans.resolvers + +import nebulosa.api.atlas.* +import nebulosa.api.beans.converters.annotation +import nebulosa.api.beans.converters.parameter +import nebulosa.api.connection.ConnectionService +import nebulosa.api.locations.LocationEntity +import nebulosa.api.locations.LocationRepository +import nebulosa.indi.device.camera.Camera +import nebulosa.indi.device.filterwheel.FilterWheel +import nebulosa.indi.device.focuser.Focuser +import nebulosa.indi.device.guide.GuideOutput +import nebulosa.indi.device.mount.Mount +import org.springframework.core.MethodParameter +import org.springframework.data.repository.findByIdOrNull +import org.springframework.http.HttpStatus +import org.springframework.stereotype.Component +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.support.WebDataBinderFactory +import org.springframework.web.context.request.NativeWebRequest +import org.springframework.web.method.support.HandlerMethodArgumentResolver +import org.springframework.web.method.support.ModelAndViewContainer +import org.springframework.web.server.ResponseStatusException + +@Component +class EntityParamMethodArgumentResolver( + private val locationRepository: LocationRepository, + private val starRepository: StarRepository, + private val deepSkyObjectRepository: DeepSkyObjectRepository, + private val satelliteRepository: SatelliteRepository, + private val connectionService: ConnectionService, +) : HandlerMethodArgumentResolver { + + private val entityResolvers = mapOf, (String) -> Any?>( + LocationEntity::class.java to { locationRepository.findByIdOrNull(it.toLong()) }, + StarEntity::class.java to { starRepository.findByIdOrNull(it.toLong()) }, + DeepSkyObjectEntity::class.java to { deepSkyObjectRepository.findByIdOrNull(it.toLong()) }, + SatelliteEntity::class.java to { satelliteRepository.findByIdOrNull(it.toLong()) }, + Camera::class.java to { connectionService.camera(it) }, + Mount::class.java to { connectionService.mount(it) }, + Focuser::class.java to { connectionService.focuser(it) }, + FilterWheel::class.java to { connectionService.wheel(it) }, + GuideOutput::class.java to { connectionService.guideOutput(it) }, + ) + + override fun supportsParameter(parameter: MethodParameter): Boolean { + return parameter.parameterType in entityResolvers + } + + override fun resolveArgument( + parameter: MethodParameter, + mavContainer: ModelAndViewContainer?, + webRequest: NativeWebRequest, + binderFactory: WebDataBinderFactory?, + ): Any? { + val requestParam = parameter.annotation() + val parameterName = requestParam?.name?.ifBlank { null } ?: parameter.parameterName ?: "id" + val parameterValue = webRequest.parameter(parameterName) ?: requestParam?.defaultValue + + val entity = entityByParameterValue(parameter.parameterType, parameterValue) + + if (requestParam != null && requestParam.required && entity == null) { + val message = "Cannot found a ${parameter.parameterType.simpleName} entity with name [$parameterValue]" + throw ResponseStatusException(HttpStatus.NOT_FOUND, message) + } + + return entity + } + + private fun entityByParameterValue(parameterType: Class<*>, parameterValue: String?): Any? { + if (parameterValue.isNullOrBlank()) return null + return entityResolvers[parameterType]?.invoke(parameterValue) + } +} diff --git a/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameController.kt b/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameController.kt index a694a7bce..048bf9a89 100644 --- a/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameController.kt +++ b/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameController.kt @@ -1,6 +1,6 @@ package nebulosa.api.calibration -import nebulosa.api.beans.annotations.EntityBy +import nebulosa.api.beans.annotations.EntityParam import nebulosa.indi.device.camera.Camera import org.springframework.web.bind.annotation.* import java.nio.file.Path @@ -12,14 +12,14 @@ class CalibrationFrameController( ) { @GetMapping("{camera}") - fun groups(@EntityBy camera: Camera): List { + fun groups(@EntityParam camera: Camera): List { var id = 0 val groupedFrames = calibrationFrameService.groupedCalibrationFrames(camera) return groupedFrames.map { CalibrationFrameGroup(id++, it.key, it.value) } } @PutMapping("{camera}") - fun upload(@EntityBy camera: Camera, @RequestParam path: Path): List { + fun upload(@EntityParam camera: Camera, @RequestParam path: Path): List { return calibrationFrameService.upload(camera, path) } diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraController.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraController.kt index 67cbe040e..94dccbe20 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraController.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraController.kt @@ -1,7 +1,7 @@ package nebulosa.api.cameras import jakarta.validation.Valid -import nebulosa.api.beans.annotations.EntityBy +import nebulosa.api.beans.annotations.EntityParam import nebulosa.api.connection.ConnectionService import nebulosa.indi.device.camera.Camera import org.hibernate.validator.constraints.Range @@ -20,28 +20,28 @@ class CameraController( } @GetMapping("{camera}") - fun camera(@EntityBy camera: Camera): Camera { + fun camera(@EntityParam camera: Camera): Camera { return camera } @PutMapping("{camera}/connect") - fun connect(@EntityBy camera: Camera) { + fun connect(@EntityParam camera: Camera) { cameraService.connect(camera) } @PutMapping("{camera}/disconnect") - fun disconnect(@EntityBy camera: Camera) { + fun disconnect(@EntityParam camera: Camera) { cameraService.disconnect(camera) } @GetMapping("{camera}/capturing") - fun isCapturing(@EntityBy camera: Camera): Boolean { + fun isCapturing(@EntityParam camera: Camera): Boolean { return cameraService.isCapturing(camera) } @PutMapping("{camera}/cooler") fun cooler( - @EntityBy camera: Camera, + @EntityParam camera: Camera, @RequestParam enabled: Boolean, ) { cameraService.cooler(camera, enabled) @@ -49,7 +49,7 @@ class CameraController( @PutMapping("{camera}/temperature/setpoint") fun setpointTemperature( - @EntityBy camera: Camera, + @EntityParam camera: Camera, @RequestParam @Valid @Range(min = -50, max = 50) temperature: Double, ) { cameraService.setpointTemperature(camera, temperature) @@ -57,14 +57,14 @@ class CameraController( @PutMapping("{camera}/capture/start") fun startCapture( - @EntityBy camera: Camera, + @EntityParam camera: Camera, @RequestBody body: CameraStartCaptureRequest, ) { cameraService.startCapture(camera, body) } @PutMapping("{camera}/capture/abort") - fun abortCapture(@EntityBy camera: Camera) { + fun abortCapture(@EntityParam camera: Camera) { cameraService.abortCapture(camera) } } diff --git a/api/src/main/kotlin/nebulosa/api/focusers/FocuserController.kt b/api/src/main/kotlin/nebulosa/api/focusers/FocuserController.kt index 531054312..40b95db8e 100644 --- a/api/src/main/kotlin/nebulosa/api/focusers/FocuserController.kt +++ b/api/src/main/kotlin/nebulosa/api/focusers/FocuserController.kt @@ -2,7 +2,7 @@ package nebulosa.api.focusers import jakarta.validation.Valid import jakarta.validation.constraints.PositiveOrZero -import nebulosa.api.beans.annotations.EntityBy +import nebulosa.api.beans.annotations.EntityParam import nebulosa.api.connection.ConnectionService import nebulosa.indi.device.focuser.Focuser import org.springframework.web.bind.annotation.* @@ -20,23 +20,23 @@ class FocuserController( } @GetMapping("{focuser}") - fun focuser(@EntityBy focuser: Focuser): Focuser { + fun focuser(@EntityParam focuser: Focuser): Focuser { return focuser } @PutMapping("{focuser}/connect") - fun connect(@EntityBy focuser: Focuser) { + fun connect(@EntityParam focuser: Focuser) { focuserService.connect(focuser) } @PutMapping("{focuser}/disconnect") - fun disconnect(@EntityBy focuser: Focuser) { + fun disconnect(@EntityParam focuser: Focuser) { focuserService.disconnect(focuser) } @PutMapping("{focuser}/move-in") fun moveIn( - @EntityBy focuser: Focuser, + @EntityParam focuser: Focuser, @RequestParam @Valid @PositiveOrZero steps: Int, ) { focuserService.moveIn(focuser, steps) @@ -44,7 +44,7 @@ class FocuserController( @PutMapping("{focuser}/move-out") fun moveOut( - @EntityBy focuser: Focuser, + @EntityParam focuser: Focuser, @RequestParam @Valid @PositiveOrZero steps: Int, ) { focuserService.moveOut(focuser, steps) @@ -52,20 +52,20 @@ class FocuserController( @PutMapping("{focuser}/move-to") fun moveTo( - @EntityBy focuser: Focuser, + @EntityParam focuser: Focuser, @RequestParam @Valid @PositiveOrZero steps: Int, ) { focuserService.moveTo(focuser, steps) } @PutMapping("{focuser}/abort") - fun abort(@EntityBy focuser: Focuser) { + fun abort(@EntityParam focuser: Focuser) { focuserService.abort(focuser) } @PutMapping("{focuser}/sync") fun sync( - @EntityBy focuser: Focuser, + @EntityParam focuser: Focuser, @RequestParam @Valid @PositiveOrZero steps: Int, ) { focuserService.sync(focuser, steps) diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputController.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputController.kt index 12ab9ccf0..b77ba1597 100644 --- a/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputController.kt +++ b/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputController.kt @@ -1,6 +1,6 @@ package nebulosa.api.guiding -import nebulosa.api.beans.annotations.EntityBy +import nebulosa.api.beans.annotations.EntityParam import nebulosa.api.connection.ConnectionService import nebulosa.guiding.GuideDirection import nebulosa.indi.device.guide.GuideOutput @@ -22,23 +22,23 @@ class GuideOutputController( } @GetMapping("{guideOutput}") - fun guideOutput(@EntityBy guideOutput: GuideOutput): GuideOutput { + fun guideOutput(@EntityParam guideOutput: GuideOutput): GuideOutput { return guideOutput } @PutMapping("{guideOutput}/connect") - fun connect(@EntityBy guideOutput: GuideOutput) { + fun connect(@EntityParam guideOutput: GuideOutput) { guideOutputService.connect(guideOutput) } @PutMapping("{guideOutput}/disconnect") - fun disconnect(@EntityBy guideOutput: GuideOutput) { + fun disconnect(@EntityParam guideOutput: GuideOutput) { guideOutputService.disconnect(guideOutput) } @PutMapping("{guideOutput}/pulse") fun pulse( - @EntityBy guideOutput: GuideOutput, + @EntityParam guideOutput: GuideOutput, @RequestParam direction: GuideDirection, @RequestParam @DurationMin(nanos = 0L) @DurationMax(seconds = 60L) duration: Duration, ) { diff --git a/api/src/main/kotlin/nebulosa/api/image/ImageController.kt b/api/src/main/kotlin/nebulosa/api/image/ImageController.kt index 793e4117d..d9aebd13a 100644 --- a/api/src/main/kotlin/nebulosa/api/image/ImageController.kt +++ b/api/src/main/kotlin/nebulosa/api/image/ImageController.kt @@ -1,7 +1,7 @@ package nebulosa.api.image import jakarta.servlet.http.HttpServletResponse -import nebulosa.api.beans.annotations.EntityBy +import nebulosa.api.beans.annotations.EntityParam import nebulosa.imaging.ImageChannel import nebulosa.imaging.algorithms.transformation.ProtectionMethod import nebulosa.indi.device.camera.Camera @@ -18,7 +18,7 @@ class ImageController( @GetMapping fun openImage( @RequestParam path: Path, - @EntityBy(required = false) camera: Camera?, + @EntityParam(required = false) camera: Camera?, @RequestParam(required = false, defaultValue = "true") debayer: Boolean, @RequestParam(required = false, defaultValue = "false") calibrate: Boolean, @RequestParam(required = false, defaultValue = "false") autoStretch: Boolean, diff --git a/api/src/main/kotlin/nebulosa/api/mounts/MountController.kt b/api/src/main/kotlin/nebulosa/api/mounts/MountController.kt index 7bec6fca0..6e07dad88 100644 --- a/api/src/main/kotlin/nebulosa/api/mounts/MountController.kt +++ b/api/src/main/kotlin/nebulosa/api/mounts/MountController.kt @@ -3,8 +3,8 @@ package nebulosa.api.mounts import jakarta.validation.Valid import jakarta.validation.constraints.NotBlank import jakarta.validation.constraints.PositiveOrZero -import nebulosa.api.beans.annotations.DateAndTime -import nebulosa.api.beans.annotations.EntityBy +import nebulosa.api.beans.annotations.DateAndTimeParam +import nebulosa.api.beans.annotations.EntityParam import nebulosa.api.connection.ConnectionService import nebulosa.guiding.GuideDirection import nebulosa.indi.device.mount.Mount @@ -32,23 +32,23 @@ class MountController( } @GetMapping("{mount}") - fun mount(@EntityBy mount: Mount): Mount { + fun mount(@EntityParam mount: Mount): Mount { return mount } @PutMapping("{mount}/connect") - fun connect(@EntityBy mount: Mount) { + fun connect(@EntityParam mount: Mount) { mountService.connect(mount) } @PutMapping("{mount}/disconnect") - fun disconnect(@EntityBy mount: Mount) { + fun disconnect(@EntityParam mount: Mount) { mountService.disconnect(mount) } @PutMapping("{mount}/tracking") fun tracking( - @EntityBy mount: Mount, + @EntityParam mount: Mount, @RequestParam enabled: Boolean, ) { mountService.tracking(mount, enabled) @@ -56,7 +56,7 @@ class MountController( @PutMapping("{mount}/sync") fun sync( - @EntityBy mount: Mount, + @EntityParam mount: Mount, @RequestParam @Valid @NotBlank rightAscension: String, @RequestParam @Valid @NotBlank declination: String, @RequestParam(required = false, defaultValue = "false") j2000: Boolean, @@ -66,7 +66,7 @@ class MountController( @PutMapping("{mount}/slew") fun slew( - @EntityBy mount: Mount, + @EntityParam mount: Mount, @RequestParam @Valid @NotBlank rightAscension: String, @RequestParam @Valid @NotBlank declination: String, @RequestParam(required = false, defaultValue = "false") j2000: Boolean, @@ -76,7 +76,7 @@ class MountController( @PutMapping("{mount}/goto") fun goTo( - @EntityBy mount: Mount, + @EntityParam mount: Mount, @RequestParam @Valid @NotBlank rightAscension: String, @RequestParam @Valid @NotBlank declination: String, @RequestParam(required = false, defaultValue = "false") j2000: Boolean, @@ -85,18 +85,18 @@ class MountController( } @PutMapping("{mount}/home") - fun home(@EntityBy mount: Mount) { + fun home(@EntityParam mount: Mount) { mountService.home(mount) } @PutMapping("{mount}/abort") - fun abort(@EntityBy mount: Mount) { + fun abort(@EntityParam mount: Mount) { mountService.abort(mount) } @PutMapping("{mount}/track-mode") fun trackMode( - @EntityBy mount: Mount, + @EntityParam mount: Mount, @RequestParam mode: TrackMode, ) { mountService.trackMode(mount, mode) @@ -104,7 +104,7 @@ class MountController( @PutMapping("{mount}/slew-rate") fun slewRate( - @EntityBy mount: Mount, + @EntityParam mount: Mount, @RequestParam @Valid @NotBlank rate: String, ) { mountService.slewRate(mount, mount.slewRates.first { it.name == rate }) @@ -112,7 +112,7 @@ class MountController( @PutMapping("{mount}/move") fun move( - @EntityBy mount: Mount, + @EntityParam mount: Mount, @RequestParam direction: GuideDirection, @RequestParam enabled: Boolean, ) { @@ -120,18 +120,18 @@ class MountController( } @PutMapping("{mount}/park") - fun park(@EntityBy mount: Mount) { + fun park(@EntityParam mount: Mount) { mountService.park(mount) } @PutMapping("{mount}/unpark") - fun unpark(@EntityBy mount: Mount) { + fun unpark(@EntityParam mount: Mount) { mountService.unpark(mount) } @PutMapping("{mount}/coordinates") fun coordinates( - @EntityBy mount: Mount, + @EntityParam mount: Mount, @RequestParam @Valid @NotBlank longitude: String, @RequestParam @Valid @NotBlank latitude: String, @RequestParam(required = false, defaultValue = "0.0") elevation: Double, @@ -141,36 +141,36 @@ class MountController( @PutMapping("{mount}/datetime") fun dateTime( - @EntityBy mount: Mount, - @DateAndTime dateTime: LocalDateTime, + @EntityParam mount: Mount, + @DateAndTimeParam dateTime: LocalDateTime, @RequestParam @Valid @Range(min = -720, max = 720) offsetInMinutes: Int, ) { mountService.dateTime(mount, OffsetDateTime.of(dateTime, ZoneOffset.ofTotalSeconds(offsetInMinutes * 60))) } @GetMapping("{mount}/location/zenith") - fun zenithLocation(@EntityBy mount: Mount): ComputedLocation { + fun zenithLocation(@EntityParam mount: Mount): ComputedLocation { return mountService.computeZenithLocation(mount) } @GetMapping("{mount}/location/celestial-pole/north") - fun northCelestialPoleLocation(@EntityBy mount: Mount): ComputedLocation { + fun northCelestialPoleLocation(@EntityParam mount: Mount): ComputedLocation { return mountService.computeNorthCelestialPoleLocation(mount) } @GetMapping("{mount}/location/celestial-pole/south") - fun southCelestialPoleLocation(@EntityBy mount: Mount): ComputedLocation { + fun southCelestialPoleLocation(@EntityParam mount: Mount): ComputedLocation { return mountService.computeSouthCelestialPoleLocation(mount) } @GetMapping("{mount}/location/galactic-center") - fun galacticCenterLocation(@EntityBy mount: Mount): ComputedLocation { + fun galacticCenterLocation(@EntityParam mount: Mount): ComputedLocation { return mountService.computeGalacticCenterLocation(mount) } @GetMapping("{mount}/location") fun location( - @EntityBy mount: Mount, + @EntityParam mount: Mount, @RequestParam rightAscension: String, @RequestParam declination: String, @RequestParam(required = false, defaultValue = "false") j2000: Boolean, @@ -186,7 +186,7 @@ class MountController( @PutMapping("{mount}/point-here") fun pointMountHere( - @EntityBy mount: Mount, + @EntityParam mount: Mount, @RequestParam path: Path, @RequestParam @Valid @PositiveOrZero x: Double, @RequestParam @Valid @PositiveOrZero y: Double, diff --git a/api/src/main/kotlin/nebulosa/api/solver/PlateSolverController.kt b/api/src/main/kotlin/nebulosa/api/solver/PlateSolverController.kt index e8dc46de1..1b42e9d83 100644 --- a/api/src/main/kotlin/nebulosa/api/solver/PlateSolverController.kt +++ b/api/src/main/kotlin/nebulosa/api/solver/PlateSolverController.kt @@ -1,8 +1,8 @@ package nebulosa.api.solver import jakarta.validation.Valid -import nebulosa.math.deg -import nebulosa.math.hours +import nebulosa.api.beans.annotations.AngleParam +import nebulosa.math.Angle import org.springframework.web.bind.annotation.* import java.nio.file.Path @@ -16,10 +16,10 @@ class PlateSolverController( fun solveImage( @RequestParam path: Path, @RequestParam(required = false, defaultValue = "true") blind: Boolean, - @RequestParam(required = false, defaultValue = "0.0") centerRA: String, - @RequestParam(required = false, defaultValue = "0.0") centerDEC: String, - @RequestParam(required = false, defaultValue = "8.0") radius: String, - ) = plateSolverService.solveImage(path, centerRA.hours, centerDEC.deg, if (blind) 0.0 else radius.deg) + @AngleParam(required = false, isHours = true, defaultValue = "0.0") centerRA: Angle, + @AngleParam(required = false, defaultValue = "0.0") centerDEC: Angle, + @AngleParam(required = false, defaultValue = "4.0") radius: Angle, + ) = plateSolverService.solveImage(path, centerRA, centerDEC, if (blind) 0.0 else radius) @PutMapping("settings") fun settings(@RequestBody @Valid body: PlateSolverOptions) { diff --git a/api/src/main/kotlin/nebulosa/api/wheels/WheelController.kt b/api/src/main/kotlin/nebulosa/api/wheels/WheelController.kt index 84980b45f..cb1836102 100644 --- a/api/src/main/kotlin/nebulosa/api/wheels/WheelController.kt +++ b/api/src/main/kotlin/nebulosa/api/wheels/WheelController.kt @@ -2,7 +2,7 @@ package nebulosa.api.wheels import jakarta.validation.Valid import jakarta.validation.constraints.PositiveOrZero -import nebulosa.api.beans.annotations.EntityBy +import nebulosa.api.beans.annotations.EntityParam import nebulosa.api.connection.ConnectionService import nebulosa.indi.device.filterwheel.FilterWheel import org.springframework.web.bind.annotation.* @@ -20,23 +20,23 @@ class WheelController( } @GetMapping("{wheel}") - fun wheel(@EntityBy wheel: FilterWheel): FilterWheel { + fun wheel(@EntityParam wheel: FilterWheel): FilterWheel { return wheel } @PutMapping("{wheel}/connect") - fun connect(@EntityBy wheel: FilterWheel) { + fun connect(@EntityParam wheel: FilterWheel) { wheelService.connect(wheel) } @PutMapping("{wheel}/disconnect") - fun disconnect(@EntityBy wheel: FilterWheel) { + fun disconnect(@EntityParam wheel: FilterWheel) { wheelService.disconnect(wheel) } @PutMapping("{wheel}/move-to") fun moveTo( - @EntityBy wheel: FilterWheel, + @EntityParam wheel: FilterWheel, @RequestParam @Valid @PositiveOrZero position: Int, ) { wheelService.moveTo(wheel, position) @@ -44,7 +44,7 @@ class WheelController( @PutMapping("{wheel}/sync") fun sync( - @EntityBy wheel: FilterWheel, + @EntityParam wheel: FilterWheel, @RequestParam @Valid @PositiveOrZero names: String, ) { wheelService.sync(wheel, names.split(",")) diff --git a/nebulosa-watney/src/main/kotlin/nebulosa/watney/plate/solving/quad/QuadDatabaseCellFileDescriptor.kt b/nebulosa-watney/src/main/kotlin/nebulosa/watney/plate/solving/quad/QuadDatabaseCellFileDescriptor.kt index a41d0fa28..3fe249eed 100644 --- a/nebulosa-watney/src/main/kotlin/nebulosa/watney/plate/solving/quad/QuadDatabaseCellFileDescriptor.kt +++ b/nebulosa-watney/src/main/kotlin/nebulosa/watney/plate/solving/quad/QuadDatabaseCellFileDescriptor.kt @@ -4,7 +4,6 @@ import nebulosa.erfa.SphericalCoordinate import nebulosa.io.ByteOrder import nebulosa.io.readFloat import nebulosa.io.readInt -import nebulosa.log.debug import nebulosa.log.loggerFor import nebulosa.math.Angle import nebulosa.math.deg diff --git a/nebulosa-watney/src/main/kotlin/nebulosa/watney/star/detection/StarPixelBin.kt b/nebulosa-watney/src/main/kotlin/nebulosa/watney/star/detection/StarPixelBin.kt index d0c1beaa1..c8bb00988 100644 --- a/nebulosa-watney/src/main/kotlin/nebulosa/watney/star/detection/StarPixelBin.kt +++ b/nebulosa-watney/src/main/kotlin/nebulosa/watney/star/detection/StarPixelBin.kt @@ -1,7 +1,6 @@ package nebulosa.watney.star.detection import kotlin.math.hypot -import kotlin.math.roundToInt /** * A class representing a "bin" of star pixels, i.e. the list of a single star's pixels From c2b8ad35ebd4445f2da4e65285837f9304634836 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Mon, 11 Dec 2023 00:52:09 -0300 Subject: [PATCH 23/87] [api][desktop]: Fix INDI protocol handler --- .../EntityParamMethodArgumentResolver.kt | 2 + .../nebulosa/api/indi/INDIController.kt | 38 ++++++++++++------- desktop/src/app/camera/camera.component.ts | 3 -- desktop/src/app/guider/guider.component.ts | 6 +-- desktop/src/app/indi/indi.component.html | 2 +- desktop/src/app/indi/indi.component.scss | 12 +++++- desktop/src/app/indi/indi.component.ts | 16 +++++--- desktop/src/app/mount/mount.component.ts | 4 -- desktop/src/shared/services/api.service.ts | 23 +++++------ desktop/src/shared/types.ts | 2 - .../client/device/DeviceProtocolHandler.kt | 30 +++++++++------ .../protocol/parser/INDIProtocolReader.kt | 3 ++ 12 files changed, 84 insertions(+), 57 deletions(-) diff --git a/api/src/main/kotlin/nebulosa/api/beans/resolvers/EntityParamMethodArgumentResolver.kt b/api/src/main/kotlin/nebulosa/api/beans/resolvers/EntityParamMethodArgumentResolver.kt index 6f2ff6efe..9438c7c42 100644 --- a/api/src/main/kotlin/nebulosa/api/beans/resolvers/EntityParamMethodArgumentResolver.kt +++ b/api/src/main/kotlin/nebulosa/api/beans/resolvers/EntityParamMethodArgumentResolver.kt @@ -6,6 +6,7 @@ import nebulosa.api.beans.converters.parameter import nebulosa.api.connection.ConnectionService import nebulosa.api.locations.LocationEntity import nebulosa.api.locations.LocationRepository +import nebulosa.indi.device.Device import nebulosa.indi.device.camera.Camera import nebulosa.indi.device.filterwheel.FilterWheel import nebulosa.indi.device.focuser.Focuser @@ -36,6 +37,7 @@ class EntityParamMethodArgumentResolver( StarEntity::class.java to { starRepository.findByIdOrNull(it.toLong()) }, DeepSkyObjectEntity::class.java to { deepSkyObjectRepository.findByIdOrNull(it.toLong()) }, SatelliteEntity::class.java to { satelliteRepository.findByIdOrNull(it.toLong()) }, + Device::class.java to { connectionService.device(it) }, Camera::class.java to { connectionService.camera(it) }, Mount::class.java to { connectionService.mount(it) }, Focuser::class.java to { connectionService.focuser(it) }, diff --git a/api/src/main/kotlin/nebulosa/api/indi/INDIController.kt b/api/src/main/kotlin/nebulosa/api/indi/INDIController.kt index 04670baff..8dcc2a98c 100644 --- a/api/src/main/kotlin/nebulosa/api/indi/INDIController.kt +++ b/api/src/main/kotlin/nebulosa/api/indi/INDIController.kt @@ -1,36 +1,48 @@ package nebulosa.api.indi import jakarta.validation.Valid -import jakarta.validation.constraints.NotBlank -import nebulosa.api.connection.ConnectionService +import nebulosa.api.beans.annotations.EntityParam +import nebulosa.indi.device.Device import nebulosa.indi.device.PropertyVector import org.springframework.web.bind.annotation.* @RestController +@RequestMapping("indi") class INDIController( - private val connectionService: ConnectionService, private val indiService: INDIService, + private val indiEventHandler: INDIEventHandler, ) { - @GetMapping("indiProperties") - fun properties(@RequestParam @Valid @NotBlank name: String): Collection> { - val device = requireNotNull(connectionService.device(name)) + @GetMapping("{device}/properties") + fun properties(@EntityParam device: Device): Collection> { return indiService.properties(device) } - @PostMapping("sendIndiProperty") + @PutMapping("{device}/send") fun sendProperty( - @RequestParam @Valid @NotBlank name: String, + @EntityParam device: Device, @RequestBody @Valid body: INDISendProperty, ) { - val device = requireNotNull(connectionService.device(name)) return indiService.sendProperty(device, body) } - @GetMapping("indiLog") - fun indiLog(@RequestParam(required = false) name: String?): List { - if (name.isNullOrBlank()) return indiService.messages() - val device = connectionService.device(name) ?: return emptyList() + @GetMapping("{device}/log") + fun log(@EntityParam device: Device): List { return device.messages } + + @GetMapping("log") + fun log(): List { + return indiService.messages() + } + + @PutMapping("listener/start") + fun startListening() { + indiEventHandler.canSendEvents = true + } + + @PutMapping("listener/stop") + fun stopListening() { + indiEventHandler.canSendEvents = false + } } diff --git a/desktop/src/app/camera/camera.component.ts b/desktop/src/app/camera/camera.component.ts index 1632366d9..b499bc21c 100644 --- a/desktop/src/app/camera/camera.component.ts +++ b/desktop/src/app/camera/camera.component.ts @@ -229,8 +229,6 @@ export class CameraComponent implements AfterContentInit, OnDestroy { ) { app.title = 'Camera' - api.startListening('CAMERA') - electron.on('CAMERA_UPDATED', event => { if (event.device.name === this.camera?.name) { ngZone.run(() => { @@ -326,7 +324,6 @@ export class CameraComponent implements AfterContentInit, OnDestroy { @HostListener('window:unload') ngOnDestroy() { - this.api.stopListening('CAMERA') this.abortCapture() } diff --git a/desktop/src/app/guider/guider.component.ts b/desktop/src/app/guider/guider.component.ts index e0d3b88a6..0f00c93a2 100644 --- a/desktop/src/app/guider/guider.component.ts +++ b/desktop/src/app/guider/guider.component.ts @@ -220,8 +220,6 @@ export class GuiderComponent implements AfterViewInit, OnDestroy { ) { title.setTitle('Guider') - api.startListening('GUIDING') - electron.on('GUIDE_OUTPUT_UPDATED', event => { if (event.device.name === this.guideOutput?.name) { ngZone.run(() => { @@ -304,9 +302,7 @@ export class GuiderComponent implements AfterViewInit, OnDestroy { } @HostListener('window:unload') - ngOnDestroy() { - this.api.stopListening('GUIDING') - } + ngOnDestroy() { } private processGuiderStatus(event: Guider) { this.connected = event.connected diff --git a/desktop/src/app/indi/indi.component.html b/desktop/src/app/indi/indi.component.html index 8d27baf9c..5d435408d 100644 --- a/desktop/src/app/indi/indi.component.html +++ b/desktop/src/app/indi/indi.component.html @@ -18,7 +18,7 @@
- +
diff --git a/desktop/src/app/indi/indi.component.scss b/desktop/src/app/indi/indi.component.scss index 232c4e990..7746ea400 100644 --- a/desktop/src/app/indi/indi.component.scss +++ b/desktop/src/app/indi/indi.component.scss @@ -1,8 +1,16 @@ +:host { + ::ng-deep { + .p-listbox-list-wrapper { + max-height: calc(100vh - 175px) !important; + } + } +} + .properties { - height: calc(100vh - 80px); + height: calc(100vh - 100px); overflow-y: auto; } .properties::-webkit-scrollbar { display: none; -} +} \ No newline at end of file diff --git a/desktop/src/app/indi/indi.component.ts b/desktop/src/app/indi/indi.component.ts index 057e361b1..84fc49604 100644 --- a/desktop/src/app/indi/indi.component.ts +++ b/desktop/src/app/indi/indi.component.ts @@ -31,7 +31,7 @@ export class INDIComponent implements AfterViewInit, OnDestroy { ) { app.title = 'INDI' - this.api.startListening('INDI') + this.api.indiStartListening() electron.on('DEVICE_PROPERTY_CHANGED', event => { ngZone.run(() => { @@ -62,7 +62,11 @@ export class INDIComponent implements AfterViewInit, OnDestroy { async ngAfterViewInit() { this.route.queryParams.subscribe(e => { - this.device = JSON.parse(decodeURIComponent(e.data)) as Device + const device = JSON.parse(decodeURIComponent(e.data)) + + if ("name" in device && device.name) { + this.device = device + } }) this.devices = [ @@ -70,12 +74,14 @@ export class INDIComponent implements AfterViewInit, OnDestroy { ...await this.api.mounts(), ...await this.api.focusers(), ...await this.api.wheels(), - ] + ].sort((a, b) => a.name.localeCompare(b.name)) + + this.device = this.devices[0] } @HostListener('window:unload') ngOnDestroy() { - this.api.stopListening('INDI') + this.api.indiStopListening() } async deviceChanged() { @@ -89,7 +95,7 @@ export class INDIComponent implements AfterViewInit, OnDestroy { } send(property: INDISendProperty) { - this.api.sendIndiProperty(this.device!, property) + this.api.indiSendProperty(this.device!, property) } private updateGroups() { diff --git a/desktop/src/app/mount/mount.component.ts b/desktop/src/app/mount/mount.component.ts index 8b3f56339..d89e8c1ee 100644 --- a/desktop/src/app/mount/mount.component.ts +++ b/desktop/src/app/mount/mount.component.ts @@ -156,8 +156,6 @@ export class MountComponent implements AfterContentInit, OnDestroy { ) { app.title = 'Mount' - api.startListening('MOUNT') - electron.on('MOUNT_UPDATED', event => { if (event.device.name === this.mount?.name) { ngZone.run(() => { @@ -195,8 +193,6 @@ export class MountComponent implements AfterContentInit, OnDestroy { this.computeCoordinateSubscriptions .forEach(e => e.unsubscribe()) - - this.api.stopListening('MOUNT') } async mountChanged(mount?: Mount) { diff --git a/desktop/src/shared/services/api.service.ts b/desktop/src/shared/services/api.service.ts index 36710016a..b0ca41694 100644 --- a/desktop/src/shared/services/api.service.ts +++ b/desktop/src/shared/services/api.service.ts @@ -3,9 +3,8 @@ import moment from 'moment' import { Angle, BodyPosition, CalibrationFrame, CalibrationFrameGroup, Camera, CameraStartCapture, ComputedLocation, Constellation, CoordinateInterpolation, DeepSkyObject, DetectedStar, Device, FilterWheel, Focuser, GuideDirection, GuideOutput, Guider, HipsSurvey, HistoryStep, - INDIProperty, INDISendProperty, ImageAnnotation, ImageCalibrated, - ImageChannel, ImageInfo, ListeningEventType, Location, MinorPlanet, - Mount, PlateSolverOptions, SCNRProtectionMethod, Satellite, SatelliteGroupType, + INDIProperty, INDISendProperty, ImageAnnotation, ImageCalibrated, ImageChannel, ImageInfo, + Location, MinorPlanet, Mount, PlateSolverOptions, SCNRProtectionMethod, Satellite, SatelliteGroupType, SettleInfo, SkyObjectType, SlewRate, Star, TrackMode, Twilight } from '../types' import { HttpService } from './http.service' @@ -339,24 +338,26 @@ export class ApiService { return this.http.delete(`image?${query}`) } + // INDI + indiProperties(device: Device) { - return this.http.get[]>(`indiProperties?name=${device.name}`) + return this.http.get[]>(`indi/${device.name}/properties`) } - sendIndiProperty(device: Device, property: INDISendProperty) { - return this.http.post(`sendIndiProperty?name=${device.name}`, property) + indiSendProperty(device: Device, property: INDISendProperty) { + return this.http.put(`indi/${device.name}/send`, property) } - startListening(eventType: ListeningEventType) { - return this.http.post(`startListening?eventType=${eventType}`) + indiStartListening() { + return this.http.put(`indi/listener/start`) } - stopListening(eventType: ListeningEventType) { - return this.http.post(`stopListening?eventType=${eventType}`) + indiStopListening() { + return this.http.put(`indi/listener/stop`) } indiLog(device: Device) { - return this.http.get(`indiLog?name=${device.name}`) + return this.http.get(`indi/${device.name}/log`) } // LOCATION diff --git a/desktop/src/shared/types.ts b/desktop/src/shared/types.ts index 289d1cae6..d98b65597 100644 --- a/desktop/src/shared/types.ts +++ b/desktop/src/shared/types.ts @@ -876,8 +876,6 @@ export const SATELLITE_GROUPS = [ export type SatelliteGroupType = (typeof SATELLITE_GROUPS)[number] -export type ListeningEventType = 'INDI' | 'GUIDING' | 'CAMERA' | 'MOUNT' - export const GUIDER_TYPES = ['PHD2'] as const export type GuiderType = (typeof GUIDER_TYPES)[number] diff --git a/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/DeviceProtocolHandler.kt b/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/DeviceProtocolHandler.kt index e1f18e3f1..b0b683e50 100644 --- a/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/DeviceProtocolHandler.kt +++ b/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/DeviceProtocolHandler.kt @@ -33,7 +33,6 @@ import nebulosa.indi.protocol.DefTextVector import nebulosa.indi.protocol.DelProperty import nebulosa.indi.protocol.INDIProtocol import nebulosa.indi.protocol.Message -import nebulosa.indi.protocol.io.INDIInputStream import nebulosa.indi.protocol.parser.INDIProtocolParser import nebulosa.indi.protocol.parser.INDIProtocolReader import nebulosa.log.debug @@ -55,16 +54,6 @@ abstract class DeviceProtocolHandler : MessageSender, INDIProtocolParser { private val messageQueueCounter = HashMap(2048) private val handlers = ArrayList() - override val input = object : INDIInputStream { - - override fun readINDIProtocol(): INDIProtocol { - Thread.sleep(1) - return messageReorderingQueue.take() - } - - override fun close() = Unit - } - val isRunning get() = protocolReader != null @@ -190,6 +179,20 @@ abstract class DeviceProtocolHandler : MessageSender, INDIProtocolParser { var registered = false + fun takeMessageFromReorderingQueue(device: Device) { + if (messageReorderingQueue.isNotEmpty()) { + repeat(messageReorderingQueue.size) { + val queuedMessage = messageReorderingQueue.take() + + if (queuedMessage.device == device.name) { + handleMessage(queuedMessage) + } else { + messageReorderingQueue.offer(message) + } + } + } + } + if (executable in Camera.DRIVERS) { registered = true @@ -197,6 +200,7 @@ abstract class DeviceProtocolHandler : MessageSender, INDIProtocolParser { val device = CAMERAS[executable]?.create(this, message.device) ?: CameraDevice(this, message.device) cameras[message.device] = device + takeMessageFromReorderingQueue(device) LOG.info("camera attached: {}", device.name) fireOnEventReceived(CameraAttached(device)) } @@ -209,6 +213,7 @@ abstract class DeviceProtocolHandler : MessageSender, INDIProtocolParser { val device = MOUNTS[executable]?.create(this, message.device) ?: MountDevice(this, message.device) mounts[message.device] = device + takeMessageFromReorderingQueue(device) LOG.info("mount attached: {}", device.name) fireOnEventReceived(MountAttached(device)) } @@ -220,6 +225,7 @@ abstract class DeviceProtocolHandler : MessageSender, INDIProtocolParser { if (message.device !in wheels) { val device = FilterWheelDevice(this, message.device) wheels[message.device] = device + takeMessageFromReorderingQueue(device) LOG.info("filter wheel attached: {}", device.name) fireOnEventReceived(FilterWheelAttached(device)) } @@ -231,6 +237,7 @@ abstract class DeviceProtocolHandler : MessageSender, INDIProtocolParser { if (message.device !in focusers) { val device = FocuserDevice(this, message.device) focusers[message.device] = device + takeMessageFromReorderingQueue(device) LOG.info("focuser attached: {}", device.name) fireOnEventReceived(FocuserAttached(device)) } @@ -242,6 +249,7 @@ abstract class DeviceProtocolHandler : MessageSender, INDIProtocolParser { if (message.device !in gps) { val device = GPSDevice(this, message.device) gps[message.device] = device + takeMessageFromReorderingQueue(device) LOG.info("gps attached: {}", device.name) fireOnEventReceived(GPSAttached(device)) } diff --git a/nebulosa-indi-protocol/src/main/kotlin/nebulosa/indi/protocol/parser/INDIProtocolReader.kt b/nebulosa-indi-protocol/src/main/kotlin/nebulosa/indi/protocol/parser/INDIProtocolReader.kt index a26f132cf..0d9051d3e 100644 --- a/nebulosa-indi-protocol/src/main/kotlin/nebulosa/indi/protocol/parser/INDIProtocolReader.kt +++ b/nebulosa-indi-protocol/src/main/kotlin/nebulosa/indi/protocol/parser/INDIProtocolReader.kt @@ -27,7 +27,10 @@ class INDIProtocolReader( val message = input.readINDIProtocol() ?: break parser.handleMessage(message) } + + LOG.info("protocol parser finished") } catch (_: InterruptedException) { + LOG.info("protocol parser interrupted") } catch (e: Throwable) { LOG.error("protocol parser error", e) parser.close() From f4f69ed986abf18343704b064aa4069fe6fd5c2c Mon Sep 17 00:00:00 2001 From: tiagohm Date: Mon, 11 Dec 2023 10:59:38 -0300 Subject: [PATCH 24/87] [api][desktop]: Improve INDI by listening only for selected device --- .../kotlin/nebulosa/api/indi/INDIController.kt | 14 ++++++++------ .../nebulosa/api/indi/INDIEventHandler.kt | 9 ++++----- desktop/src/app/indi/indi.component.html | 2 +- desktop/src/app/indi/indi.component.ts | 17 ++++++++++++----- desktop/src/shared/services/api.service.ts | 8 ++++---- 5 files changed, 29 insertions(+), 21 deletions(-) diff --git a/api/src/main/kotlin/nebulosa/api/indi/INDIController.kt b/api/src/main/kotlin/nebulosa/api/indi/INDIController.kt index 8dcc2a98c..49a973654 100644 --- a/api/src/main/kotlin/nebulosa/api/indi/INDIController.kt +++ b/api/src/main/kotlin/nebulosa/api/indi/INDIController.kt @@ -36,13 +36,15 @@ class INDIController( return indiService.messages() } - @PutMapping("listener/start") - fun startListening() { - indiEventHandler.canSendEvents = true + @Synchronized + @PutMapping("listener/{device}/start") + fun startListening(device: Device) { + indiEventHandler.canSendEvents.add(device) } - @PutMapping("listener/stop") - fun stopListening() { - indiEventHandler.canSendEvents = false + @Synchronized + @PutMapping("listener/{device}/stop") + fun stopListening(device: Device) { + indiEventHandler.canSendEvents.remove(device) } } diff --git a/api/src/main/kotlin/nebulosa/api/indi/INDIEventHandler.kt b/api/src/main/kotlin/nebulosa/api/indi/INDIEventHandler.kt index 60c285424..aff9bd9cc 100644 --- a/api/src/main/kotlin/nebulosa/api/indi/INDIEventHandler.kt +++ b/api/src/main/kotlin/nebulosa/api/indi/INDIEventHandler.kt @@ -14,8 +14,7 @@ class INDIEventHandler( private val messageService: MessageService, ) : LinkedList() { - var canSendEvents = false - internal set + val canSendEvents = HashSet() @Subscribe(threadMode = ThreadMode.ASYNC) fun onDeviceEvent(event: DeviceEvent<*>) { @@ -33,19 +32,19 @@ class INDIEventHandler( } fun sendINDIPropertyChanged(event: DevicePropertyEvent) { - if (canSendEvents) { + if (event.device in canSendEvents) { messageService.sendMessage(INDIMessageEvent(DEVICE_PROPERTY_CHANGED, event)) } } fun sendINDIPropertyDeleted(event: DevicePropertyEvent) { - if (canSendEvents) { + if (event.device in canSendEvents) { messageService.sendMessage(INDIMessageEvent(DEVICE_PROPERTY_DELETED, event)) } } fun sendINDIMessageReceived(event: DeviceMessageReceived) { - if (canSendEvents) { + if (event.device in canSendEvents) { messageService.sendMessage(INDIMessageEvent(DEVICE_MESSAGE_RECEIVED, event)) } } diff --git a/desktop/src/app/indi/indi.component.html b/desktop/src/app/indi/indi.component.html index 5d435408d..0bfb1d811 100644 --- a/desktop/src/app/indi/indi.component.html +++ b/desktop/src/app/indi/indi.component.html @@ -2,7 +2,7 @@
- diff --git a/desktop/src/app/indi/indi.component.ts b/desktop/src/app/indi/indi.component.ts index 84fc49604..e2855f1f2 100644 --- a/desktop/src/app/indi/indi.component.ts +++ b/desktop/src/app/indi/indi.component.ts @@ -31,8 +31,6 @@ export class INDIComponent implements AfterViewInit, OnDestroy { ) { app.title = 'INDI' - this.api.indiStartListening() - electron.on('DEVICE_PROPERTY_CHANGED', event => { ngZone.run(() => { this.addOrUpdateProperty(event.property!) @@ -81,12 +79,21 @@ export class INDIComponent implements AfterViewInit, OnDestroy { @HostListener('window:unload') ngOnDestroy() { - this.api.indiStopListening() + if (this.device) { + this.api.indiStopListening(this.device) + } } - async deviceChanged() { + async deviceChanged(device: Device) { + if (this.device) { + this.api.indiStopListening(this.device) + } + + this.device = device + this.updateProperties() - this.messages = await this.api.indiLog(this.device!) + this.api.indiStartListening(device) + this.messages = await this.api.indiLog(device) } changeGroup(group: string) { diff --git a/desktop/src/shared/services/api.service.ts b/desktop/src/shared/services/api.service.ts index b0ca41694..ba2d0827d 100644 --- a/desktop/src/shared/services/api.service.ts +++ b/desktop/src/shared/services/api.service.ts @@ -348,12 +348,12 @@ export class ApiService { return this.http.put(`indi/${device.name}/send`, property) } - indiStartListening() { - return this.http.put(`indi/listener/start`) + indiStartListening(device: Device) { + return this.http.put(`indi/listener/${device.name}/start`) } - indiStopListening() { - return this.http.put(`indi/listener/stop`) + indiStopListening(device: Device) { + return this.http.put(`indi/listener/${device.name}/stop`) } indiLog(device: Device) { From ab4c6ec0429c3c655c234df536d6cea9b4087ecd Mon Sep 17 00:00:00 2001 From: tiagohm Date: Mon, 11 Dec 2023 13:33:52 -0300 Subject: [PATCH 25/87] [desktop]: Sort devices by name --- desktop/src/app/home/home.component.ts | 5 +++-- desktop/src/app/indi/indi.component.ts | 5 +++-- desktop/src/shared/utils/comparators.ts | 9 +++++++++ 3 files changed, 15 insertions(+), 4 deletions(-) create mode 100644 desktop/src/shared/utils/comparators.ts diff --git a/desktop/src/app/home/home.component.ts b/desktop/src/app/home/home.component.ts index 42ae0d7ed..4246317df 100644 --- a/desktop/src/app/home/home.component.ts +++ b/desktop/src/app/home/home.component.ts @@ -1,4 +1,5 @@ import { AfterContentInit, Component, HostListener, NgZone, OnDestroy, ViewChild } from '@angular/core' +import path from 'path' import { MenuItem, MessageService } from 'primeng/api' import { DeviceMenuComponent } from '../../shared/components/devicemenu/devicemenu.component' import { DialogMenuComponent } from '../../shared/components/dialogmenu/dialogmenu.component' @@ -7,8 +8,8 @@ import { BrowserWindowService } from '../../shared/services/browser-window.servi import { ElectronService } from '../../shared/services/electron.service' import { LocalStorageService } from '../../shared/services/local-storage.service' import { Camera, Device, FilterWheel, Focuser, HomeWindowType, Mount } from '../../shared/types' +import { compareDevice } from '../../shared/utils/comparators' import { AppComponent } from '../app.component' -import path from 'path' type MappedDevice = { 'CAMERA': Camera @@ -228,7 +229,7 @@ export class HomeComponent implements AfterContentInit, OnDestroy { if (devices.length === 0) return if (devices.length === 1) return this.openDeviceWindow(type, devices[0] as any) - for (const device of devices) { + for (const device of [...devices].sort(compareDevice)) { this.deviceModel.push({ icon: 'mdi mdi-connection', label: device.name, diff --git a/desktop/src/app/indi/indi.component.ts b/desktop/src/app/indi/indi.component.ts index e2855f1f2..cd38566f0 100644 --- a/desktop/src/app/indi/indi.component.ts +++ b/desktop/src/app/indi/indi.component.ts @@ -4,6 +4,7 @@ import { MenuItem } from 'primeng/api' import { ApiService } from '../../shared/services/api.service' import { ElectronService } from '../../shared/services/electron.service' import { Device, INDIProperty, INDIPropertyItem, INDISendProperty } from '../../shared/types' +import { compareDevice, compareText } from '../../shared/utils/comparators' import { AppComponent } from '../app.component' @Component({ @@ -72,7 +73,7 @@ export class INDIComponent implements AfterViewInit, OnDestroy { ...await this.api.mounts(), ...await this.api.focusers(), ...await this.api.wheels(), - ].sort((a, b) => a.name.localeCompare(b.name)) + ].sort(compareDevice) this.device = this.devices[0] } @@ -129,7 +130,7 @@ export class INDIComponent implements AfterViewInit, OnDestroy { if (this.groups.length === 0 || groupsChanged) { this.groups = Array.from(groups) - .sort((a, b) => a.localeCompare(b)) + .sort(compareText) .map(e => { icon: 'mdi mdi-sitemap', label: e, diff --git a/desktop/src/shared/utils/comparators.ts b/desktop/src/shared/utils/comparators.ts new file mode 100644 index 000000000..9074a23ad --- /dev/null +++ b/desktop/src/shared/utils/comparators.ts @@ -0,0 +1,9 @@ +import { Device } from '../types' + +export function compareText(a: string, b: string) { + return a.localeCompare(b) +} + +export function compareDevice(a: Device, b: Device) { + return compareText(a.name, b.name) +} \ No newline at end of file From c37cf013e9abc71538117262e73a8bcda789b065 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Mon, 11 Dec 2023 19:40:13 -0300 Subject: [PATCH 26/87] [api]: Implement Batch Processing --- nebulosa-batch-processing/build.gradle.kts | 18 +++ .../batch/processing/AsyncJobLauncher.kt | 138 ++++++++++++++++++ .../nebulosa/batch/processing/BatchStatus.kt | 11 ++ .../batch/processing/ExecutionContext.kt | 10 ++ .../nebulosa/batch/processing/FlowStep.kt | 8 + .../kotlin/nebulosa/batch/processing/Job.kt | 14 ++ .../nebulosa/batch/processing/JobExecution.kt | 60 ++++++++ .../nebulosa/batch/processing/JobLauncher.kt | 6 + .../nebulosa/batch/processing/JobListener.kt | 8 + .../nebulosa/batch/processing/RepeatStatus.kt | 6 + .../batch/processing/SimpleFlowStep.kt | 18 +++ .../nebulosa/batch/processing/SimpleJob.kt | 22 +++ .../kotlin/nebulosa/batch/processing/Step.kt | 8 + .../nebulosa/batch/processing/StepChain.kt | 10 ++ .../batch/processing/StepInterceptor.kt | 6 + .../batch/processing/StepInterceptorChain.kt | 15 ++ .../nebulosa/batch/processing/StepListener.kt | 8 + .../nebulosa/batch/processing/StepResult.kt | 23 +++ .../nebulosa/batch/processing/Stoppable.kt | 6 + .../src/test/kotlin/BatchProcessingTest.kt | 119 +++++++++++++++ settings.gradle.kts | 1 + 21 files changed, 515 insertions(+) create mode 100644 nebulosa-batch-processing/build.gradle.kts create mode 100644 nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/AsyncJobLauncher.kt create mode 100644 nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/BatchStatus.kt create mode 100644 nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/ExecutionContext.kt create mode 100644 nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/FlowStep.kt create mode 100644 nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/Job.kt create mode 100644 nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobExecution.kt create mode 100644 nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobLauncher.kt create mode 100644 nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobListener.kt create mode 100644 nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/RepeatStatus.kt create mode 100644 nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/SimpleFlowStep.kt create mode 100644 nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/SimpleJob.kt create mode 100644 nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/Step.kt create mode 100644 nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepChain.kt create mode 100644 nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepInterceptor.kt create mode 100644 nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepInterceptorChain.kt create mode 100644 nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepListener.kt create mode 100644 nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepResult.kt create mode 100644 nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/Stoppable.kt create mode 100644 nebulosa-batch-processing/src/test/kotlin/BatchProcessingTest.kt diff --git a/nebulosa-batch-processing/build.gradle.kts b/nebulosa-batch-processing/build.gradle.kts new file mode 100644 index 000000000..055c5463b --- /dev/null +++ b/nebulosa-batch-processing/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + kotlin("jvm") + id("maven-publish") +} + +dependencies { + implementation(project(":nebulosa-log")) + testImplementation(project(":nebulosa-common")) + testImplementation(project(":nebulosa-test")) +} + +publishing { + publications { + create("pluginMaven") { + from(components["java"]) + } + } +} diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/AsyncJobLauncher.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/AsyncJobLauncher.kt new file mode 100644 index 000000000..921608234 --- /dev/null +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/AsyncJobLauncher.kt @@ -0,0 +1,138 @@ +package nebulosa.batch.processing + +import java.time.LocalDateTime +import java.util.concurrent.ExecutorService + +open class AsyncJobLauncher(private val executor: ExecutorService) : JobLauncher, StepInterceptor { + + private val jobListeners = HashSet() + private val stepListeners = HashSet() + private val stepInterceptors = HashSet() + private val jobs = LinkedHashMap() + + fun registerJobListener(listener: JobListener) { + jobListeners.add(listener) + } + + fun unregisterJobListener(listener: JobListener) { + jobListeners.remove(listener) + } + + fun registerStepListener(listener: StepListener) { + stepListeners.add(listener) + } + + fun unregisterStepListener(listener: StepListener) { + stepListeners.remove(listener) + } + + fun registerStepInterceptor(interceptor: StepInterceptor) { + stepInterceptors.add(interceptor) + } + + fun unregisterStepInterceptor(interceptor: StepInterceptor) { + stepInterceptors.remove(interceptor) + } + + override val size + get() = jobs.size + + override fun contains(element: JobExecution): Boolean { + return jobs.containsValue(element) + } + + override fun containsAll(elements: Collection): Boolean { + return elements.all { it in this } + } + + override fun isEmpty(): Boolean { + return jobs.isEmpty() + } + + override fun iterator(): Iterator { + return jobs.values.iterator() + } + + @Synchronized + override fun launch(job: Job): JobExecution { + var jobExecution = jobs[job.id] + + if (jobExecution != null) { + if (!jobExecution.isDone) { + return jobExecution + } + } + + val context = ExecutionContext() + jobExecution = JobExecution(job, context) + + jobs[job.id] = jobExecution + + executor.submit { + jobExecution.status = BatchStatus.STARTED + + job.beforeJob(jobExecution) + jobListeners.forEach { it.beforeJob(jobExecution) } + + val interceptors = ArrayList(stepInterceptors) + interceptors.add(this) + + try { + while (jobExecution.canContinue && job.hasNext(jobExecution)) { + val step = job.next(jobExecution) + + if (step is FlowStep) { + val flow = object : Step { + + override fun execute(jobExecution: JobExecution): StepResult { + step.toList().parallelStream().forEach { execute(interceptors, it, jobExecution) } + return step.execute(jobExecution) + } + + override fun stop(mayInterruptIfRunning: Boolean) { + step.stop(mayInterruptIfRunning) + } + } + + execute(interceptors, flow, jobExecution) + } else { + execute(interceptors, step, jobExecution) + } + } + + jobExecution.status = if (jobExecution.isStopping) BatchStatus.STOPPED else BatchStatus.COMPLETED + jobExecution.complete() + } catch (e: Throwable) { + jobExecution.status = BatchStatus.FAILED + jobExecution.completeExceptionally(e) + } finally { + jobExecution.finishedAt = LocalDateTime.now() + } + + job.afterJob(jobExecution) + jobListeners.forEach { it.afterJob(jobExecution) } + } + + return jobExecution + } + + override fun stop(mayInterruptIfRunning: Boolean) { + jobs.forEach { it.value.stop(mayInterruptIfRunning) } + } + + override fun intercept(chain: StepChain): StepResult { + return chain.step.execute(chain.jobExecution) + } + + private fun execute(interceptors: List, step: Step, jobExecution: JobExecution) { + val chain = StepInterceptorChain(interceptors, jobExecution, step) + var status: RepeatStatus + + do { + stepListeners.forEach { it.beforeStep(step, jobExecution) } + val result = chain.proceed() + stepListeners.forEach { it.afterStep(step, jobExecution) } + status = result.get() + } while (status == RepeatStatus.CONTINUABLE) + } +} diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/BatchStatus.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/BatchStatus.kt new file mode 100644 index 000000000..5825b5a57 --- /dev/null +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/BatchStatus.kt @@ -0,0 +1,11 @@ +package nebulosa.batch.processing + +enum class BatchStatus { + STARTING, + STARTED, + STOPPING, + STOPPED, + FAILED, + COMPLETED, + ABANDONED, +} diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/ExecutionContext.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/ExecutionContext.kt new file mode 100644 index 000000000..0525cbad9 --- /dev/null +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/ExecutionContext.kt @@ -0,0 +1,10 @@ +package nebulosa.batch.processing + +import java.util.concurrent.ConcurrentHashMap + +open class ExecutionContext : ConcurrentHashMap { + + constructor(initialCapacity: Int = 64) : super(initialCapacity) + + constructor(context: ExecutionContext) : super(context) +} diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/FlowStep.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/FlowStep.kt new file mode 100644 index 000000000..eeebd8d2a --- /dev/null +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/FlowStep.kt @@ -0,0 +1,8 @@ +package nebulosa.batch.processing + +interface FlowStep : Step, Iterable { + + override fun stop(mayInterruptIfRunning: Boolean) { + forEach { it.stop(mayInterruptIfRunning) } + } +} diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/Job.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/Job.kt new file mode 100644 index 000000000..54be67d84 --- /dev/null +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/Job.kt @@ -0,0 +1,14 @@ +package nebulosa.batch.processing + +interface Job : JobListener, Stoppable { + + val id: String + + fun hasNext(jobExecution: JobExecution): Boolean + + fun next(jobExecution: JobExecution): Step + + override fun beforeJob(jobExecution: JobExecution) = Unit + + override fun afterJob(jobExecution: JobExecution) = Unit +} diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobExecution.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobExecution.kt new file mode 100644 index 000000000..1874f139c --- /dev/null +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobExecution.kt @@ -0,0 +1,60 @@ +package nebulosa.batch.processing + +import java.time.LocalDateTime +import java.util.concurrent.CompletableFuture +import java.util.concurrent.ExecutionException +import java.util.concurrent.TimeUnit + +data class JobExecution( + val job: Job, + val context: ExecutionContext, + val startedAt: LocalDateTime = LocalDateTime.now(), + var status: BatchStatus = BatchStatus.STARTING, + var finishedAt: LocalDateTime? = null, +) : Stoppable { + + private val completable = CompletableFuture() + + val canContinue + get() = status == BatchStatus.STARTED + + val isStopping + get() = status == BatchStatus.STOPPING + + val isStopped + get() = status == BatchStatus.STOPPED + + val isCompleted + get() = status == BatchStatus.COMPLETED + + val isFailed + get() = status == BatchStatus.FAILED + + val isDone + get() = isCompleted || isFailed || isStopped + + fun waitForCompletion(timeout: Long = 0L, unit: TimeUnit = TimeUnit.MILLISECONDS): Boolean { + try { + if (timeout <= 0L) completable.get() + else return completable.get(timeout, unit) + return true + } catch (e: ExecutionException) { + throw e.cause ?: e + } + } + + internal fun complete() { + completable.complete(true) + } + + internal fun completeExceptionally(e: Throwable) { + completable.completeExceptionally(e) + } + + override fun stop(mayInterruptIfRunning: Boolean) { + if (!isDone) { + status = BatchStatus.STOPPING + job.stop(mayInterruptIfRunning) + } + } +} diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobLauncher.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobLauncher.kt new file mode 100644 index 000000000..ff54dc2c8 --- /dev/null +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobLauncher.kt @@ -0,0 +1,6 @@ +package nebulosa.batch.processing + +interface JobLauncher : Collection, Stoppable { + + fun launch(job: Job): JobExecution +} diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobListener.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobListener.kt new file mode 100644 index 000000000..3fe925cf0 --- /dev/null +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobListener.kt @@ -0,0 +1,8 @@ +package nebulosa.batch.processing + +interface JobListener { + + fun beforeJob(jobExecution: JobExecution) + + fun afterJob(jobExecution: JobExecution) +} diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/RepeatStatus.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/RepeatStatus.kt new file mode 100644 index 000000000..aa45a61ba --- /dev/null +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/RepeatStatus.kt @@ -0,0 +1,6 @@ +package nebulosa.batch.processing + +enum class RepeatStatus { + CONTINUABLE, + FINISHED, +} diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/SimpleFlowStep.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/SimpleFlowStep.kt new file mode 100644 index 000000000..799ff8b09 --- /dev/null +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/SimpleFlowStep.kt @@ -0,0 +1,18 @@ +package nebulosa.batch.processing + +abstract class SimpleFlowStep : FlowStep { + + protected abstract val steps: Collection + + override fun execute(jobExecution: JobExecution): StepResult { + return StepResult.FINISHED + } + + final override fun iterator(): Iterator { + return steps.iterator() + } + + final override fun stop(mayInterruptIfRunning: Boolean) { + super.stop(mayInterruptIfRunning) + } +} diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/SimpleJob.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/SimpleJob.kt new file mode 100644 index 000000000..8464a2081 --- /dev/null +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/SimpleJob.kt @@ -0,0 +1,22 @@ +package nebulosa.batch.processing + +abstract class SimpleJob : Job { + + @Volatile private var position = 0 + + protected abstract val steps: List + + override fun hasNext(jobExecution: JobExecution): Boolean { + return position < steps.size + } + + override fun next(jobExecution: JobExecution): Step { + return steps[position++] + } + + override fun stop(mayInterruptIfRunning: Boolean) { + if (position in steps.indices) { + steps[position].stop(mayInterruptIfRunning) + } + } +} diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/Step.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/Step.kt new file mode 100644 index 000000000..02ab9ce54 --- /dev/null +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/Step.kt @@ -0,0 +1,8 @@ +package nebulosa.batch.processing + +interface Step : Stoppable { + + fun execute(jobExecution: JobExecution): StepResult + + override fun stop(mayInterruptIfRunning: Boolean) = Unit +} diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepChain.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepChain.kt new file mode 100644 index 000000000..3622e2745 --- /dev/null +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepChain.kt @@ -0,0 +1,10 @@ +package nebulosa.batch.processing + +interface StepChain { + + val step: Step + + val jobExecution: JobExecution + + fun proceed(): StepResult +} diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepInterceptor.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepInterceptor.kt new file mode 100644 index 000000000..80759da03 --- /dev/null +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepInterceptor.kt @@ -0,0 +1,6 @@ +package nebulosa.batch.processing + +interface StepInterceptor { + + fun intercept(chain: StepChain): StepResult +} diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepInterceptorChain.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepInterceptorChain.kt new file mode 100644 index 000000000..3217af44e --- /dev/null +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepInterceptorChain.kt @@ -0,0 +1,15 @@ +package nebulosa.batch.processing + +data class StepInterceptorChain( + private val interceptors: List, + override val jobExecution: JobExecution, + override val step: Step, + private val index: Int = 0, +) : StepChain { + + override fun proceed(): StepResult { + val next = StepInterceptorChain(interceptors, jobExecution, step, index + 1) + val interceptor = interceptors[index] + return interceptor.intercept(next) + } +} diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepListener.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepListener.kt new file mode 100644 index 000000000..55f3991a9 --- /dev/null +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepListener.kt @@ -0,0 +1,8 @@ +package nebulosa.batch.processing + +interface StepListener { + + fun beforeStep(step: Step, jobExecution: JobExecution) + + fun afterStep(step: Step, jobExecution: JobExecution) +} diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepResult.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepResult.kt new file mode 100644 index 000000000..e3d044cd8 --- /dev/null +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepResult.kt @@ -0,0 +1,23 @@ +package nebulosa.batch.processing + +import java.util.concurrent.CompletableFuture +import java.util.concurrent.Future + +data class StepResult(@JvmField internal val completable: CompletableFuture) : Future by completable { + + constructor() : this(CompletableFuture()) + + fun complete(status: RepeatStatus): Boolean { + return completable.complete(status) + } + + fun completeExceptionally(e: Throwable): Boolean { + return completable.completeExceptionally(e) + } + + companion object { + + @JvmStatic val CONTINUABLE = StepResult(CompletableFuture.completedFuture(RepeatStatus.CONTINUABLE)) + @JvmStatic val FINISHED = StepResult(CompletableFuture.completedFuture(RepeatStatus.FINISHED)) + } +} diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/Stoppable.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/Stoppable.kt new file mode 100644 index 000000000..c7c7f48a2 --- /dev/null +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/Stoppable.kt @@ -0,0 +1,6 @@ +package nebulosa.batch.processing + +interface Stoppable { + + fun stop(mayInterruptIfRunning: Boolean = true) +} diff --git a/nebulosa-batch-processing/src/test/kotlin/BatchProcessingTest.kt b/nebulosa-batch-processing/src/test/kotlin/BatchProcessingTest.kt new file mode 100644 index 000000000..1e90a01ca --- /dev/null +++ b/nebulosa-batch-processing/src/test/kotlin/BatchProcessingTest.kt @@ -0,0 +1,119 @@ +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.booleans.shouldBeTrue +import io.kotest.matchers.longs.shouldBeInRange +import io.kotest.matchers.shouldBe +import nebulosa.batch.processing.* +import nebulosa.common.concurrency.DaemonThreadFactory +import nebulosa.log.loggerFor +import java.util.concurrent.Executors +import kotlin.concurrent.thread + +class BatchProcessingTest : StringSpec() { + + init { + val launcher = AsyncJobLauncher(Executors.newSingleThreadExecutor(DaemonThreadFactory)) + + "single" { + val startedAt = System.currentTimeMillis() + val jobExecution = launcher.launch(MathJob(listOf(SumStep()))) + jobExecution.waitForCompletion() + jobExecution.context["VALUE"] shouldBe 1.0 + (System.currentTimeMillis() - startedAt) shouldBeInRange (1000L..2000L) + } + "multiple" { + val startedAt = System.currentTimeMillis() + val jobExecution = launcher.launch(MathJob(listOf(SumStep(), SumStep()))) + jobExecution.waitForCompletion() + jobExecution.context["VALUE"] shouldBe 2.0 + (System.currentTimeMillis() - startedAt) shouldBeInRange (2000L..3000L) + } + "flow" { + val startedAt = System.currentTimeMillis() + val jobExecution = launcher.launch(MathJob(listOf(MultipleSumStep()))) + jobExecution.waitForCompletion() + jobExecution.context["VALUE"] shouldBe NUMBER_OF_PROCESSORS.toDouble() + (System.currentTimeMillis() - startedAt) shouldBeInRange (1000L..2000L) + } + "stop" { + val startedAt = System.currentTimeMillis() + val jobExecution = launcher.launch(MathJob((0..7).map { SumStep() })) + thread { Thread.sleep(4000); jobExecution.stop() } + jobExecution.waitForCompletion() + jobExecution.context["VALUE"] shouldBe 4.0 + jobExecution.isStopped.shouldBeTrue() + (System.currentTimeMillis() - startedAt) shouldBeInRange (4000L..5000L) + } + "repeatable" { + val startedAt = System.currentTimeMillis() + val jobExecution = launcher.launch(MathJob(listOf(SumStep()), 10.0)) + jobExecution.waitForCompletion() + jobExecution.context["VALUE"] shouldBe 20.0 + (System.currentTimeMillis() - startedAt) shouldBeInRange (10000L..11000L) + } + } + + private data class MathJob( + override val steps: List, + private val initialValue: Double = 0.0, + ) : SimpleJob() { + + override val id = "Job.Math" + + override fun beforeJob(jobExecution: JobExecution) { + jobExecution.context["VALUE"] = initialValue + } + } + + private abstract class MathStep : Step { + + @Volatile private var running = false + + protected abstract fun compute(value: Double): Double + + final override fun execute(jobExecution: JobExecution): StepResult { + var sleepCount = 0 + + running = jobExecution.canContinue + + while (running && sleepCount++ < 100) { + Thread.sleep(10) + } + + if (running) { + synchronized(jobExecution) { + val value = jobExecution.context["VALUE"]!! as Double + LOG.info("executing ${javaClass.simpleName}: $value") + jobExecution.context["VALUE"] = compute(value) + + if (value >= 10.0 && value < 19.0) { + return StepResult.CONTINUABLE + } + } + } + + return StepResult.FINISHED + } + + override fun stop(mayInterruptIfRunning: Boolean) { + running = false + } + } + + private class SumStep : MathStep() { + + override fun compute(value: Double): Double { + return value + 1.0 + } + } + + private class MultipleSumStep : SimpleFlowStep() { + + override val steps = (0 until NUMBER_OF_PROCESSORS).map { SumStep() } + } + + companion object { + + @JvmStatic private val LOG = loggerFor() + @JvmStatic private val NUMBER_OF_PROCESSORS = Runtime.getRuntime().availableProcessors() + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 925045edd..bc2c3728e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -53,6 +53,7 @@ include(":nebulosa-alpaca-discovery-protocol") include(":nebulosa-astap") include(":nebulosa-astrometrynet") include(":nebulosa-astrometrynet-jna") +include(":nebulosa-batch-processing") include(":nebulosa-common") include(":nebulosa-constants") include(":nebulosa-erfa") From 803010c4c793cdb41b216e09f81dc908616147e7 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Tue, 12 Dec 2023 00:47:48 -0300 Subject: [PATCH 27/87] [api]: Remove Spring Batch in favor of Batch Processing --- api/build.gradle.kts | 2 +- .../polar/darv/DARVPolarAlignmentEvent.kt | 3 +- .../polar/darv/DARVPolarAlignmentExecutor.kt | 97 ++++++++--------- .../polar/darv/DARVPolarAlignmentFinished.kt | 2 - .../polar/darv/DARVPolarAlignmentJob.kt | 35 ++++++ .../alignment/polar/darv/DARVSequenceJob.kt | 18 ---- .../configurations/BatchConfiguration.kt | 52 --------- .../beans/configurations/BeanConfiguration.kt | 15 +-- .../configurations/DataSourceConfiguration.kt | 37 ------- .../api/cameras/CameraCaptureElapsed.kt | 14 +-- .../api/cameras/CameraCaptureEvent.kt | 7 +- .../api/cameras/CameraCaptureExecutor.kt | 70 ++++++------ .../api/cameras/CameraCaptureFinished.kt | 8 +- .../api/cameras/CameraCaptureIsWaiting.kt | 13 +-- .../nebulosa/api/cameras/CameraCaptureJob.kt | 54 ++++++++++ .../api/cameras/CameraCaptureListener.kt | 17 +++ .../api/cameras/CameraCaptureStarted.kt | 17 +-- .../api/cameras/CameraExposureElapsed.kt | 10 +- .../api/cameras/CameraExposureEvent.kt | 12 +-- .../api/cameras/CameraExposureFinished.kt | 14 +-- .../api/cameras/CameraExposureStarted.kt | 12 +-- ...posureTasklet.kt => CameraExposureStep.kt} | 101 +++++++++++------- .../api/cameras/CameraLoopExposureStep.kt | 45 ++++++++ .../api/cameras/CameraLoopExposureTasklet.kt | 42 -------- .../nebulosa/api/cameras/CameraSequenceJob.kt | 16 --- .../api/cameras/CameraStartCaptureStep.kt | 13 +++ .../api/cameras/CameraStartCaptureTasklet.kt | 8 -- ...eTasklet.kt => DitherAfterExposureStep.kt} | 18 ++-- .../nebulosa/api/guiding/GuidePulseElapsed.kt | 14 --- .../nebulosa/api/guiding/GuidePulseEvent.kt | 9 -- .../api/guiding/GuidePulseFinished.kt | 12 --- .../api/guiding/GuidePulseListener.kt | 8 ++ .../nebulosa/api/guiding/GuidePulseStarted.kt | 12 --- .../nebulosa/api/guiding/GuidePulseStep.kt | 68 ++++++++++++ .../nebulosa/api/guiding/GuidePulseTasklet.kt | 64 ----------- .../nebulosa/api/guiding/WaitForSettleStep.kt | 17 +++ .../api/guiding/WaitForSettleTasklet.kt | 24 ----- .../api/sequencer/PublishSequenceTasklet.kt | 55 ---------- .../api/sequencer/SequenceFlowFactory.kt | 66 ------------ .../api/sequencer/SequenceFlowStepFactory.kt | 28 ----- .../nebulosa/api/sequencer/SequenceJob.kt | 26 ----- .../api/sequencer/SequenceJobEvent.kt | 10 -- .../api/sequencer/SequenceJobExecutor.kt | 26 ----- .../api/sequencer/SequenceJobFactory.kt | 90 ---------------- .../api/sequencer/SequenceJobSerializer.kt | 20 ---- .../api/sequencer/SequenceStepEvent.kt | 12 --- .../api/sequencer/SequenceStepFactory.kt | 64 ----------- .../nebulosa/api/sequencer/SequenceTasklet.kt | 12 --- .../api/sequencer/SequenceTaskletEvent.kt | 10 -- .../api/sequencer/SequenceTaskletFactory.kt | 52 --------- .../sequencer/tasklets/delay/DelayElapsed.kt | 17 --- .../sequencer/tasklets/delay/DelayEvent.kt | 21 ---- .../sequencer/tasklets/delay/DelayTasklet.kt | 53 --------- api/src/main/resources/application.yml | 8 -- nebulosa-batch-processing/build.gradle.kts | 1 - .../batch/processing/AsyncJobLauncher.kt | 43 +++++--- .../nebulosa/batch/processing/FlowStep.kt | 4 + .../kotlin/nebulosa/batch/processing/Job.kt | 2 +- .../batch/processing/JobExecutionListener.kt | 8 ++ .../nebulosa/batch/processing/JobListener.kt | 8 -- .../batch/processing/SimpleFlowStep.kt | 10 +- .../kotlin/nebulosa/batch/processing/Step.kt | 2 +- .../nebulosa/batch/processing/StepChain.kt | 4 +- .../batch/processing/StepExecution.kt | 10 ++ .../batch/processing/StepExecutionListener.kt | 8 ++ .../batch/processing/StepInterceptorChain.kt | 5 +- .../nebulosa/batch/processing/StepListener.kt | 8 -- .../batch/processing/delay/DelayListener.kt | 8 ++ .../batch/processing/delay/DelayStep.kt | 62 +++++++++++ .../src/test/kotlin/BatchProcessingTest.kt | 6 +- 70 files changed, 569 insertions(+), 1170 deletions(-) create mode 100644 api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentJob.kt delete mode 100644 api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVSequenceJob.kt delete mode 100644 api/src/main/kotlin/nebulosa/api/beans/configurations/BatchConfiguration.kt create mode 100644 api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureJob.kt create mode 100644 api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureListener.kt rename api/src/main/kotlin/nebulosa/api/cameras/{CameraExposureTasklet.kt => CameraExposureStep.kt} (56%) create mode 100644 api/src/main/kotlin/nebulosa/api/cameras/CameraLoopExposureStep.kt delete mode 100644 api/src/main/kotlin/nebulosa/api/cameras/CameraLoopExposureTasklet.kt delete mode 100644 api/src/main/kotlin/nebulosa/api/cameras/CameraSequenceJob.kt create mode 100644 api/src/main/kotlin/nebulosa/api/cameras/CameraStartCaptureStep.kt delete mode 100644 api/src/main/kotlin/nebulosa/api/cameras/CameraStartCaptureTasklet.kt rename api/src/main/kotlin/nebulosa/api/guiding/{DitherAfterExposureTasklet.kt => DitherAfterExposureStep.kt} (66%) delete mode 100644 api/src/main/kotlin/nebulosa/api/guiding/GuidePulseElapsed.kt delete mode 100644 api/src/main/kotlin/nebulosa/api/guiding/GuidePulseEvent.kt delete mode 100644 api/src/main/kotlin/nebulosa/api/guiding/GuidePulseFinished.kt create mode 100644 api/src/main/kotlin/nebulosa/api/guiding/GuidePulseListener.kt delete mode 100644 api/src/main/kotlin/nebulosa/api/guiding/GuidePulseStarted.kt create mode 100644 api/src/main/kotlin/nebulosa/api/guiding/GuidePulseStep.kt delete mode 100644 api/src/main/kotlin/nebulosa/api/guiding/GuidePulseTasklet.kt create mode 100644 api/src/main/kotlin/nebulosa/api/guiding/WaitForSettleStep.kt delete mode 100644 api/src/main/kotlin/nebulosa/api/guiding/WaitForSettleTasklet.kt delete mode 100644 api/src/main/kotlin/nebulosa/api/sequencer/PublishSequenceTasklet.kt delete mode 100644 api/src/main/kotlin/nebulosa/api/sequencer/SequenceFlowFactory.kt delete mode 100644 api/src/main/kotlin/nebulosa/api/sequencer/SequenceFlowStepFactory.kt delete mode 100644 api/src/main/kotlin/nebulosa/api/sequencer/SequenceJob.kt delete mode 100644 api/src/main/kotlin/nebulosa/api/sequencer/SequenceJobEvent.kt delete mode 100644 api/src/main/kotlin/nebulosa/api/sequencer/SequenceJobExecutor.kt delete mode 100644 api/src/main/kotlin/nebulosa/api/sequencer/SequenceJobFactory.kt delete mode 100644 api/src/main/kotlin/nebulosa/api/sequencer/SequenceJobSerializer.kt delete mode 100644 api/src/main/kotlin/nebulosa/api/sequencer/SequenceStepEvent.kt delete mode 100644 api/src/main/kotlin/nebulosa/api/sequencer/SequenceStepFactory.kt delete mode 100644 api/src/main/kotlin/nebulosa/api/sequencer/SequenceTasklet.kt delete mode 100644 api/src/main/kotlin/nebulosa/api/sequencer/SequenceTaskletEvent.kt delete mode 100644 api/src/main/kotlin/nebulosa/api/sequencer/SequenceTaskletFactory.kt delete mode 100644 api/src/main/kotlin/nebulosa/api/sequencer/tasklets/delay/DelayElapsed.kt delete mode 100644 api/src/main/kotlin/nebulosa/api/sequencer/tasklets/delay/DelayEvent.kt delete mode 100644 api/src/main/kotlin/nebulosa/api/sequencer/tasklets/delay/DelayTasklet.kt create mode 100644 nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobExecutionListener.kt delete mode 100644 nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobListener.kt create mode 100644 nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepExecution.kt create mode 100644 nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepExecutionListener.kt delete mode 100644 nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepListener.kt create mode 100644 nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/delay/DelayListener.kt create mode 100644 nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/delay/DelayStep.kt diff --git a/api/build.gradle.kts b/api/build.gradle.kts index 52377a141..c358ae730 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -11,6 +11,7 @@ plugins { dependencies { implementation(project(":nebulosa-astap")) implementation(project(":nebulosa-astrometrynet")) + implementation(project(":nebulosa-batch-processing")) implementation(project(":nebulosa-common")) implementation(project(":nebulosa-guiding-phd2")) implementation(project(":nebulosa-hips2fits")) @@ -42,7 +43,6 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-websocket") { exclude(module = "spring-boot-starter-tomcat") } - implementation("org.springframework.boot:spring-boot-starter-batch") implementation("org.springframework.boot:spring-boot-starter-data-jpa") implementation("org.springframework.boot:spring-boot-starter-undertow") implementation("org.hibernate.orm:hibernate-community-dialects") diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentEvent.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentEvent.kt index 340487f0b..411c4abb7 100644 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentEvent.kt +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentEvent.kt @@ -1,10 +1,9 @@ package nebulosa.api.alignment.polar.darv -import nebulosa.api.sequencer.SequenceJobEvent import nebulosa.indi.device.camera.Camera import nebulosa.indi.device.guide.GuideOutput -sealed interface DARVPolarAlignmentEvent : SequenceJobEvent { +sealed interface DARVPolarAlignmentEvent { val camera: Camera 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 index eb722e5d6..47285361a 100644 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentExecutor.kt +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentExecutor.kt @@ -1,23 +1,22 @@ package nebulosa.api.alignment.polar.darv -import io.reactivex.rxjava3.functions.Consumer import nebulosa.api.alignment.polar.darv.DARVPolarAlignmentState.BACKWARD import nebulosa.api.alignment.polar.darv.DARVPolarAlignmentState.FORWARD import nebulosa.api.cameras.CameraCaptureEvent +import nebulosa.api.cameras.CameraCaptureListener +import nebulosa.api.cameras.CameraExposureStep import nebulosa.api.cameras.CameraStartCaptureRequest -import nebulosa.api.guiding.GuidePulseEvent -import nebulosa.api.guiding.GuidePulseRequest +import nebulosa.api.guiding.GuidePulseListener import nebulosa.api.sequencer.* -import nebulosa.api.sequencer.tasklets.delay.DelayElapsed import nebulosa.api.services.MessageService +import nebulosa.batch.processing.JobExecution +import nebulosa.batch.processing.JobExecutionListener +import nebulosa.batch.processing.JobLauncher +import nebulosa.batch.processing.StepExecution +import nebulosa.batch.processing.delay.DelayListener import nebulosa.indi.device.camera.Camera import nebulosa.indi.device.guide.GuideOutput import nebulosa.log.loggerFor -import org.springframework.batch.core.JobExecution -import org.springframework.batch.core.JobExecutionListener -import org.springframework.batch.core.JobParameters -import org.springframework.batch.core.launch.JobLauncher -import org.springframework.batch.core.launch.JobOperator import org.springframework.stereotype.Component import java.nio.file.Path import java.util.* @@ -27,19 +26,15 @@ import java.util.* */ @Component class DARVPolarAlignmentExecutor( - private val jobOperator: JobOperator, private val jobLauncher: JobLauncher, private val messageService: MessageService, private val capturesPath: Path, - private val sequenceFlowFactory: SequenceFlowFactory, - private val sequenceTaskletFactory: SequenceTaskletFactory, - private val sequenceJobFactory: SequenceJobFactory, -) : SequenceJobExecutor, Consumer, JobExecutionListener { +) : JobExecutionListener, CameraCaptureListener, GuidePulseListener, DelayListener { - private val runningSequenceJobs = LinkedList() + private val jobExecutions = HashMap, JobExecution>(1) @Synchronized - override fun execute(request: DARVStart): DARVSequenceJob { + fun execute(request: DARVStart) { val camera = requireNotNull(request.camera) val guideOutput = requireNotNull(request.guideOutput) @@ -55,47 +50,15 @@ class DARVPolarAlignmentExecutor( savePath = Path.of("$capturesPath", "${camera.name}-DARV.fits") ) - val cameraExposureTasklet = sequenceTaskletFactory.cameraExposure(cameraRequest) - cameraExposureTasklet.subscribe(this) - val cameraExposureFlow = sequenceFlowFactory.cameraExposure(cameraExposureTasklet) - - val guidePulseDuration = request.exposureTime.dividedBy(2L) - val initialPauseDelayTasklet = sequenceTaskletFactory.delay(request.initialPause) - initialPauseDelayTasklet.subscribe(this) - - val direction = if (request.reversed) request.direction.reversed else request.direction - - val forwardGuidePulseRequest = GuidePulseRequest(guideOutput, direction, guidePulseDuration) - val forwardGuidePulseTasklet = sequenceTaskletFactory.guidePulse(forwardGuidePulseRequest) - forwardGuidePulseTasklet.subscribe(this) - - val backwardGuidePulseRequest = GuidePulseRequest(guideOutput, direction.reversed, guidePulseDuration) - val backwardGuidePulseTasklet = sequenceTaskletFactory.guidePulse(backwardGuidePulseRequest) - backwardGuidePulseTasklet.subscribe(this) - - val guidePulseFlow = sequenceFlowFactory.guidePulse(initialPauseDelayTasklet, forwardGuidePulseTasklet, backwardGuidePulseTasklet) - - val darvJob = sequenceJobFactory.darvPolarAlignment(cameraExposureFlow, guidePulseFlow, this, cameraExposureTasklet) - - return jobLauncher - .run(darvJob, JobParameters()) - .let { DARVSequenceJob(camera, guideOutput, request, darvJob, it) } - .also(runningSequenceJobs::add) + val darvJob = DARVPolarAlignmentJob(request, cameraRequest) + val jobExecution = jobLauncher.launch(darvJob) + jobExecutions[camera to guideOutput] = jobExecution } @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 + val jobExecution = jobExecutions[camera to guideOutput] ?: return + jobExecution.stop() } override fun accept(event: SequenceTaskletEvent) { @@ -133,8 +96,32 @@ class DARVPolarAlignmentExecutor( messageService.sendMessage(DARVPolarAlignmentFinished(camera, guideOutput, jobExecution)) } - override fun iterator(): Iterator { - return runningSequenceJobs.iterator() + override fun onCaptureStarted(step: CameraExposureStep, jobExecution: JobExecution) { + TODO("Not yet implemented") + } + + override fun onExposureStarted(stepExecution: StepExecution) { + TODO("Not yet implemented") + } + + override fun onExposureElapsed(stepExecution: StepExecution) { + TODO("Not yet implemented") + } + + override fun onExposureFinished(stepExecution: StepExecution) { + TODO("Not yet implemented") + } + + override fun onCaptureFinished(step: CameraExposureStep, jobExecution: JobExecution) { + TODO("Not yet implemented") + } + + override fun onGuidePulseElapsed(stepExecution: StepExecution) { + TODO("Not yet implemented") + } + + override fun onDelayElapsed(stepExecution: StepExecution) { + TODO("Not yet implemented") } companion object { 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 index 016a753ed..3a3009469 100644 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentFinished.kt +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentFinished.kt @@ -12,8 +12,6 @@ data class DARVPolarAlignmentFinished( @JsonIgnore override val jobExecution: JobExecution, ) : MessageEvent, DARVPolarAlignmentEvent { - override val progress = 1.0 - override val state = DARVPolarAlignmentState.IDLE override val eventName = "DARV_POLAR_ALIGNMENT_FINISHED" diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentJob.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentJob.kt new file mode 100644 index 000000000..270256bd7 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentJob.kt @@ -0,0 +1,35 @@ +package nebulosa.api.alignment.polar.darv + +import nebulosa.api.cameras.CameraExposureStep +import nebulosa.api.cameras.CameraStartCaptureRequest +import nebulosa.api.guiding.GuidePulseRequest +import nebulosa.api.guiding.GuidePulseStep +import nebulosa.batch.processing.SimpleJob +import nebulosa.batch.processing.delay.DelayStep + +data class DARVPolarAlignmentJob( + val request: DARVStart, + val cameraRequest: CameraStartCaptureRequest, +) : SimpleJob() { + + init { + val guideOutput = requireNotNull(request.guideOutput) + + val cameraExposureStep = CameraExposureStep(cameraRequest) + cameraExposureStep.registerListener(this) + + val guidePulseDuration = request.exposureTime.dividedBy(2L) + val initialPauseDelayStep = DelayStep(request.initialPause) + initialPauseDelayStep.registerListener(this) + + val direction = if (request.reversed) request.direction.reversed else request.direction + + val forwardGuidePulseRequest = GuidePulseRequest(guideOutput, direction, guidePulseDuration) + val forwardGuidePulseStep = GuidePulseStep(forwardGuidePulseRequest) + forwardGuidePulseStep.registerListener(this) + + val backwardGuidePulseRequest = GuidePulseRequest(guideOutput, direction.reversed, guidePulseDuration) + val backwardGuidePulseStep = GuidePulseStep(backwardGuidePulseRequest) + backwardGuidePulseStep.registerListener(this) + } +} diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVSequenceJob.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVSequenceJob.kt deleted file mode 100644 index 6ea79d91a..000000000 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVSequenceJob.kt +++ /dev/null @@ -1,18 +0,0 @@ -package nebulosa.api.alignment.polar.darv - -import nebulosa.api.sequencer.SequenceJob -import nebulosa.indi.device.camera.Camera -import nebulosa.indi.device.guide.GuideOutput -import org.springframework.batch.core.Job -import org.springframework.batch.core.JobExecution - -data class DARVSequenceJob( - val camera: Camera, - val guideOutput: GuideOutput, - val data: DARVStart, - override val job: Job, - override val jobExecution: JobExecution, -) : SequenceJob { - - override val devices = listOf(camera, guideOutput) -} diff --git a/api/src/main/kotlin/nebulosa/api/beans/configurations/BatchConfiguration.kt b/api/src/main/kotlin/nebulosa/api/beans/configurations/BatchConfiguration.kt deleted file mode 100644 index c60ed06a6..000000000 --- a/api/src/main/kotlin/nebulosa/api/beans/configurations/BatchConfiguration.kt +++ /dev/null @@ -1,52 +0,0 @@ -package nebulosa.api.beans.configurations - -import nebulosa.common.concurrency.DaemonThreadFactory -import nebulosa.log.loggerFor -import org.springframework.batch.core.configuration.support.DefaultBatchConfiguration -import org.springframework.beans.factory.annotation.Qualifier -import org.springframework.boot.autoconfigure.batch.BatchDataSourceScriptDatabaseInitializer -import org.springframework.boot.autoconfigure.batch.BatchProperties -import org.springframework.boot.context.properties.ConfigurationProperties -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration -import org.springframework.core.task.SimpleAsyncTaskExecutor -import org.springframework.core.task.TaskExecutor -import org.springframework.transaction.PlatformTransactionManager -import javax.sql.DataSource - -@Configuration -class BatchConfiguration( - @Qualifier("batchDataSource") private val batchDataSource: DataSource, - @Qualifier("batchTransactionManager") private val batchTransactionManager: PlatformTransactionManager, -) : DefaultBatchConfiguration() { - - override fun getDataSource(): DataSource { - return batchDataSource - } - - override fun getTransactionManager(): PlatformTransactionManager { - return batchTransactionManager - } - - override fun getTaskExecutor(): TaskExecutor { - return SimpleAsyncTaskExecutor(DaemonThreadFactory) - } - - @Bean - @ConfigurationProperties(prefix = "spring.batch") - fun batchProperties(): BatchProperties { - return BatchProperties() - } - - @Bean - fun batchDataSourceScriptDatabaseInitializer(batchProperties: BatchProperties): BatchDataSourceScriptDatabaseInitializer { - val initializer = BatchDataSourceScriptDatabaseInitializer(batchDataSource, batchProperties.jdbc) - LOG.info("batch database initialized: {}", initializer.initializeDatabase()) - return initializer - } - - companion object { - - @JvmStatic private val LOG = loggerFor() - } -} diff --git a/api/src/main/kotlin/nebulosa/api/beans/configurations/BeanConfiguration.kt b/api/src/main/kotlin/nebulosa/api/beans/configurations/BeanConfiguration.kt index 47f1130fa..d426ed229 100644 --- a/api/src/main/kotlin/nebulosa/api/beans/configurations/BeanConfiguration.kt +++ b/api/src/main/kotlin/nebulosa/api/beans/configurations/BeanConfiguration.kt @@ -5,8 +5,8 @@ import com.fasterxml.jackson.databind.deser.std.StdDeserializer import com.fasterxml.jackson.databind.module.SimpleModule import com.fasterxml.jackson.databind.ser.std.StdSerializer import com.fasterxml.jackson.module.kotlin.kotlinModule +import nebulosa.batch.processing.AsyncJobLauncher import nebulosa.common.concurrency.DaemonThreadFactory -import nebulosa.common.concurrency.Incrementer import nebulosa.common.json.PathDeserializer import nebulosa.common.json.PathSerializer import nebulosa.guiding.Guider @@ -24,7 +24,6 @@ import org.greenrobot.eventbus.EventBus import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration -import org.springframework.core.task.SimpleAsyncTaskExecutor import org.springframework.http.converter.HttpMessageConverter import org.springframework.http.converter.StringHttpMessageConverter import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor @@ -32,6 +31,7 @@ import org.springframework.web.method.support.HandlerMethodArgumentResolver import org.springframework.web.servlet.config.annotation.CorsRegistry import org.springframework.web.servlet.config.annotation.WebMvcConfigurer import java.nio.file.Path +import java.util.concurrent.Executors import java.util.concurrent.TimeUnit import kotlin.io.path.createDirectories @@ -118,15 +118,6 @@ class BeanConfiguration { .executorService(threadPoolTaskExecutor.threadPoolExecutor) .installDefaultEventBus()!! - @Bean - fun flowIncrementer() = Incrementer() - - @Bean - fun stepIncrementer() = Incrementer() - - @Bean - fun jobIncrementer() = Incrementer() - @Bean fun phd2Client() = PHD2Client() @@ -134,7 +125,7 @@ class BeanConfiguration { fun phd2Guider(phd2Client: PHD2Client): Guider = PHD2Guider(phd2Client) @Bean - fun simpleAsyncTaskExecutor() = SimpleAsyncTaskExecutor(DaemonThreadFactory) + fun asyncJobLauncher() = AsyncJobLauncher(Executors.newCachedThreadPool(DaemonThreadFactory)) @Bean fun webMvcConfigurer( diff --git a/api/src/main/kotlin/nebulosa/api/beans/configurations/DataSourceConfiguration.kt b/api/src/main/kotlin/nebulosa/api/beans/configurations/DataSourceConfiguration.kt index 25eba0293..04aa77e5d 100644 --- a/api/src/main/kotlin/nebulosa/api/beans/configurations/DataSourceConfiguration.kt +++ b/api/src/main/kotlin/nebulosa/api/beans/configurations/DataSourceConfiguration.kt @@ -3,14 +3,12 @@ package nebulosa.api.beans.configurations import com.zaxxer.hikari.HikariConfig import com.zaxxer.hikari.HikariDataSource import jakarta.persistence.EntityManagerFactory -import org.springframework.beans.factory.annotation.Qualifier import org.springframework.beans.factory.annotation.Value import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Primary import org.springframework.data.jpa.repository.config.EnableJpaRepositories -import org.springframework.jdbc.datasource.DataSourceTransactionManager import org.springframework.orm.jpa.JpaTransactionManager import org.springframework.transaction.PlatformTransactionManager import javax.sql.DataSource @@ -19,7 +17,6 @@ import javax.sql.DataSource class DataSourceConfiguration { @Value("\${spring.datasource.url}") private lateinit var mainDataSourceUrl: String - @Value("\${spring.batch.datasource.url}") private lateinit var batchDataSourceUrl: String @Primary @Bean("mainDataSource") @@ -32,16 +29,6 @@ class DataSourceConfiguration { return HikariDataSource(config) } - @Bean("batchDataSource") - fun batchDataSource(): DataSource { - val config = HikariConfig() - config.jdbcUrl = batchDataSourceUrl - config.driverClassName = DRIVER_CLASS_NAME - config.maximumPoolSize = 1 - config.minimumIdle = 1 - return HikariDataSource(config) - } - @Configuration @EnableJpaRepositories( basePackages = ["nebulosa.api"], @@ -69,30 +56,6 @@ class DataSourceConfiguration { } } - @Configuration - @EnableJpaRepositories( - basePackages = ["org.springframework.batch.core.migration"], - entityManagerFactoryRef = "batchEntityManagerFactory", - transactionManagerRef = "batchTransactionManager" - ) - class Batch { - - @Bean("batchEntityManagerFactory") - fun batchEntityManagerFactory( - builder: EntityManagerFactoryBuilder, - @Qualifier("batchDataSource") dataSource: DataSource, - ) = builder - .dataSource(dataSource) - .packages("org.springframework.batch.core.migration") - .persistenceUnit("batchPersistenceUnit") - .build()!! - - @Bean("batchTransactionManager") - fun batchTransactionManager(@Qualifier("batchDataSource") dataSource: DataSource): PlatformTransactionManager { - return DataSourceTransactionManager(dataSource) - } - } - companion object { const val DRIVER_CLASS_NAME = "org.sqlite.JDBC" diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureElapsed.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureElapsed.kt index eae7f59d5..099fdcc31 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureElapsed.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureElapsed.kt @@ -1,20 +1,12 @@ package nebulosa.api.cameras -import com.fasterxml.jackson.annotation.JsonIgnore -import nebulosa.api.sequencer.SequenceStepEvent +import nebulosa.batch.processing.JobExecution import nebulosa.indi.device.camera.Camera -import org.springframework.batch.core.StepExecution -import java.time.Duration data class CameraCaptureElapsed( override val camera: Camera, - val exposureCount: Int, - val remainingTime: Duration, - override val progress: Double, - val elapsedTime: Duration, - @JsonIgnore override val stepExecution: StepExecution, - @JsonIgnore override val tasklet: CameraExposureTasklet, -) : CameraCaptureEvent, SequenceStepEvent { + override val jobExecution: JobExecution, +) : CameraCaptureEvent { override val eventName = "CAMERA_CAPTURE_ELAPSED" } diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureEvent.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureEvent.kt index 61e3d1e48..d9f205957 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureEvent.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureEvent.kt @@ -1,11 +1,12 @@ package nebulosa.api.cameras -import nebulosa.api.sequencer.SequenceJobEvent -import nebulosa.api.sequencer.SequenceTaskletEvent import nebulosa.api.services.MessageEvent +import nebulosa.batch.processing.JobExecution import nebulosa.indi.device.camera.Camera -sealed interface CameraCaptureEvent : MessageEvent, SequenceTaskletEvent, SequenceJobEvent { +sealed interface CameraCaptureEvent : MessageEvent { val camera: Camera + + val jobExecution: JobExecution } diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureExecutor.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureExecutor.kt index ceca9b7ad..99b94aad1 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureExecutor.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureExecutor.kt @@ -1,30 +1,25 @@ package nebulosa.api.cameras -import io.reactivex.rxjava3.functions.Consumer -import nebulosa.api.sequencer.SequenceJobExecutor -import nebulosa.api.sequencer.SequenceJobFactory import nebulosa.api.services.MessageService +import nebulosa.batch.processing.JobExecution +import nebulosa.batch.processing.JobLauncher +import nebulosa.batch.processing.StepExecution +import nebulosa.guiding.Guider import nebulosa.indi.device.camera.Camera import nebulosa.log.loggerFor -import org.springframework.batch.core.JobExecution -import org.springframework.batch.core.JobParameters -import org.springframework.batch.core.launch.JobLauncher -import org.springframework.batch.core.launch.JobOperator import org.springframework.stereotype.Component -import java.util.* @Component class CameraCaptureExecutor( - private val jobOperator: JobOperator, - private val jobLauncher: JobLauncher, private val messageService: MessageService, - private val sequenceJobFactory: SequenceJobFactory, -) : SequenceJobExecutor, Consumer { + private val guider: Guider, + private val asyncJobLauncher: JobLauncher, +) : CameraCaptureListener { - private val runningSequenceJobs = LinkedList() + private val jobExecutions = HashMap(4) @Synchronized - override fun execute(request: CameraStartCaptureRequest): CameraSequenceJob { + fun execute(request: CameraStartCaptureRequest) { val camera = requireNotNull(request.camera) check(!isCapturing(camera)) { "job is already running for camera: [${camera.name}]" } @@ -32,40 +27,47 @@ class CameraCaptureExecutor( LOG.info("starting camera capture. data={}", request) - val cameraCaptureJob = if (request.isLoop) { - sequenceJobFactory.cameraLoopCapture(request, this) - } else { - sequenceJobFactory.cameraCapture(request, this) - } - - return jobLauncher - .run(cameraCaptureJob, JobParameters()) - .let { CameraSequenceJob(camera, request, cameraCaptureJob, it) } - .also(runningSequenceJobs::add) + val cameraCaptureJob = CameraCaptureJob(request, guider) + cameraCaptureJob.registerListener(this) + val jobExecution = asyncJobLauncher.launch(cameraCaptureJob) + jobExecutions[camera] = jobExecution } fun stop(camera: Camera) { - val jobExecution = jobExecutionFor(camera) ?: return - jobOperator.stop(jobExecution.id) + val jobExecution = jobExecutions[camera] ?: return + jobExecution.stop() } fun isCapturing(camera: Camera): Boolean { - return sequenceJobFor(camera)?.jobExecution?.isRunning ?: false + val jobExecution = jobExecutions[camera] ?: return false + return !jobExecution.isDone + } + + override fun onCaptureStarted(step: CameraExposureStep, jobExecution: JobExecution) { + messageService.sendMessage(CameraCaptureStarted(step.request.camera!!, jobExecution)) } - @Suppress("NOTHING_TO_INLINE") - private inline fun jobExecutionFor(camera: Camera): JobExecution? { - return sequenceJobFor(camera)?.jobExecution + override fun onExposureStarted(stepExecution: StepExecution) { + val step = stepExecution.step as CameraExposureStep + messageService.sendMessage(CameraExposureStarted(step.request.camera!!, stepExecution)) } - override fun accept(event: CameraCaptureEvent) { - messageService.sendMessage(event) + override fun onExposureElapsed(stepExecution: StepExecution) { + val step = stepExecution.step as CameraExposureStep + messageService.sendMessage(CameraExposureElapsed(step.request.camera!!, stepExecution)) } - override fun iterator(): Iterator { - return runningSequenceJobs.iterator() + override fun onExposureFinished(stepExecution: StepExecution) { + val step = stepExecution.step as CameraExposureStep + messageService.sendMessage(CameraExposureFinished(step.request.camera!!, stepExecution)) } + override fun onCaptureFinished(step: CameraExposureStep, jobExecution: JobExecution) { + messageService.sendMessage(CameraCaptureFinished(step.request.camera!!, jobExecution)) + } + + // TODO: CameraCaptureIsWaiting + companion object { @JvmStatic private val LOG = loggerFor() diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureFinished.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureFinished.kt index deeb6425a..b8626ae72 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureFinished.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureFinished.kt @@ -1,16 +1,12 @@ package nebulosa.api.cameras -import com.fasterxml.jackson.annotation.JsonIgnore +import nebulosa.batch.processing.JobExecution import nebulosa.indi.device.camera.Camera -import org.springframework.batch.core.JobExecution data class CameraCaptureFinished( override val camera: Camera, - @JsonIgnore override val jobExecution: JobExecution, - @JsonIgnore override val tasklet: CameraExposureTasklet, + override val jobExecution: JobExecution, ) : CameraCaptureEvent { - override val progress = 1.0 - override val eventName = "CAMERA_CAPTURE_FINISHED" } diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureIsWaiting.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureIsWaiting.kt index 4af353c99..1bd01c0bd 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureIsWaiting.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureIsWaiting.kt @@ -1,19 +1,12 @@ package nebulosa.api.cameras -import com.fasterxml.jackson.annotation.JsonIgnore -import nebulosa.api.sequencer.SequenceStepEvent +import nebulosa.batch.processing.JobExecution import nebulosa.indi.device.camera.Camera -import org.springframework.batch.core.StepExecution -import java.time.Duration data class CameraCaptureIsWaiting( override val camera: Camera, - val waitDuration: Duration, - val remainingTime: Duration, - override val progress: Double, - @JsonIgnore override val stepExecution: StepExecution, - @JsonIgnore override val tasklet: CameraExposureTasklet, -) : CameraCaptureEvent, SequenceStepEvent { + override val jobExecution: JobExecution, +) : CameraCaptureEvent { override val eventName = "CAMERA_CAPTURE_WAITING" } diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureJob.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureJob.kt new file mode 100644 index 000000000..ecbd625b3 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureJob.kt @@ -0,0 +1,54 @@ +package nebulosa.api.cameras + +import nebulosa.api.guiding.DitherAfterExposureStep +import nebulosa.api.guiding.WaitForSettleStep +import nebulosa.batch.processing.SimpleFlowStep +import nebulosa.batch.processing.SimpleJob +import nebulosa.batch.processing.Step +import nebulosa.batch.processing.delay.DelayStep +import nebulosa.guiding.Guider + +data class CameraCaptureJob( + private val request: CameraStartCaptureRequest, + private val guider: Guider, +) : SimpleJob() { + + private val cameraExposureStep = if (request.isLoop) CameraLoopExposureStep(request) + else CameraExposureStep(request) + + override val id = "CameraCapture.Job.${System.currentTimeMillis()}" + + override val steps = ArrayList() + + init { + if (cameraExposureStep is CameraExposureStep) { + val waitForSettleStep = WaitForSettleStep(guider) + val ditherStep = DitherAfterExposureStep(request.dither) + val cameraDelayStep = DelayStep(request.exposureDelay) + val delayAndWaitForSettleStep = DelayAndWaitForSettleStep(listOf(cameraDelayStep, waitForSettleStep)) + + cameraDelayStep.registerListener(cameraExposureStep) + + steps.add(waitForSettleStep) + steps.add(cameraExposureStep) + + repeat(request.exposureAmount - 1) { + steps.add(delayAndWaitForSettleStep) + steps.add(cameraExposureStep) + steps.add(ditherStep) + } + } else { + steps.add(cameraExposureStep) + } + } + + fun registerListener(listener: CameraCaptureListener) { + cameraExposureStep.registerListener(listener) + } + + fun unregisterListener(listener: CameraCaptureListener) { + cameraExposureStep.unregisterListener(listener) + } + + data class DelayAndWaitForSettleStep(override val steps: Collection) : SimpleFlowStep() +} diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureListener.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureListener.kt new file mode 100644 index 000000000..523d559bc --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureListener.kt @@ -0,0 +1,17 @@ +package nebulosa.api.cameras + +import nebulosa.batch.processing.JobExecution +import nebulosa.batch.processing.StepExecution + +interface CameraCaptureListener { + + fun onCaptureStarted(step: CameraExposureStep, jobExecution: JobExecution) + + fun onExposureStarted(stepExecution: StepExecution) + + fun onExposureElapsed(stepExecution: StepExecution) + + fun onExposureFinished(stepExecution: StepExecution) + + fun onCaptureFinished(step: CameraExposureStep, jobExecution: JobExecution) +} diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureStarted.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureStarted.kt index 370f38639..bfc2791de 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureStarted.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureStarted.kt @@ -1,25 +1,12 @@ package nebulosa.api.cameras -import com.fasterxml.jackson.annotation.JsonIgnore +import nebulosa.batch.processing.JobExecution import nebulosa.indi.device.camera.Camera -import org.springframework.batch.core.JobExecution -import java.time.Duration data class CameraCaptureStarted( override val camera: Camera, - val looping: Boolean, - val estimatedTime: Duration, - @JsonIgnore override val jobExecution: JobExecution, - @JsonIgnore override val tasklet: CameraExposureTasklet, + override val jobExecution: JobExecution, ) : CameraCaptureEvent { - val exposureAmount - get() = tasklet.request.exposureAmount - - val exposureTime - get() = tasklet.request.exposureTime - - override val progress = 0.0 - override val eventName = "CAMERA_CAPTURE_STARTED" } diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureElapsed.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureElapsed.kt index d34418b39..0fc98bfb5 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureElapsed.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureElapsed.kt @@ -1,17 +1,11 @@ package nebulosa.api.cameras -import com.fasterxml.jackson.annotation.JsonIgnore +import nebulosa.batch.processing.StepExecution import nebulosa.indi.device.camera.Camera -import org.springframework.batch.core.StepExecution -import java.time.Duration data class CameraExposureElapsed( override val camera: Camera, - override val exposureCount: Int, - override val remainingTime: Duration, - override val progress: Double, - @JsonIgnore override val stepExecution: StepExecution, - @JsonIgnore override val tasklet: CameraExposureTasklet, + override val stepExecution: StepExecution, ) : CameraExposureEvent { override val eventName = "CAMERA_EXPOSURE_ELAPSED" diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureEvent.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureEvent.kt index aae3da0a4..650de147c 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureEvent.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureEvent.kt @@ -1,13 +1,11 @@ package nebulosa.api.cameras -import nebulosa.api.sequencer.SequenceStepEvent -import java.time.Duration +import nebulosa.batch.processing.StepExecution -sealed interface CameraExposureEvent : CameraCaptureEvent, SequenceStepEvent { +sealed interface CameraExposureEvent : CameraCaptureEvent { - override val tasklet: CameraStartCaptureTasklet + val stepExecution: StepExecution - val exposureCount: Int - - val remainingTime: Duration + override val jobExecution + get() = stepExecution.jobExecution } diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureFinished.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureFinished.kt index 9e4b82d7b..8daf1298e 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureFinished.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureFinished.kt @@ -1,22 +1,12 @@ package nebulosa.api.cameras -import com.fasterxml.jackson.annotation.JsonIgnore +import nebulosa.batch.processing.StepExecution import nebulosa.indi.device.camera.Camera -import org.springframework.batch.core.StepExecution -import java.nio.file.Path -import java.time.Duration data class CameraExposureFinished( override val camera: Camera, - override val exposureCount: Int, - @JsonIgnore override val stepExecution: StepExecution, - @JsonIgnore override val tasklet: CameraExposureTasklet, - val savePath: Path?, + override val stepExecution: StepExecution, ) : CameraExposureEvent { - override val remainingTime = Duration.ZERO!! - - override val progress = 1.0 - 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 94166488e..e9024cd69 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureStarted.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureStarted.kt @@ -1,20 +1,12 @@ package nebulosa.api.cameras -import com.fasterxml.jackson.annotation.JsonIgnore +import nebulosa.batch.processing.StepExecution import nebulosa.indi.device.camera.Camera -import org.springframework.batch.core.StepExecution data class CameraExposureStarted( override val camera: Camera, - override val exposureCount: Int, - @JsonIgnore override val stepExecution: StepExecution, - @JsonIgnore override val tasklet: CameraExposureTasklet, + override val stepExecution: StepExecution, ) : CameraExposureEvent { - override val remainingTime - get() = tasklet.request.exposureTime - - override val progress = 0.0 - 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/CameraExposureStep.kt similarity index 56% rename from api/src/main/kotlin/nebulosa/api/cameras/CameraExposureTasklet.kt rename to api/src/main/kotlin/nebulosa/api/cameras/CameraExposureStep.kt index 2222b5538..a3aaabc28 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureTasklet.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureStep.kt @@ -1,8 +1,10 @@ package nebulosa.api.cameras -import io.reactivex.rxjava3.functions.Consumer -import nebulosa.api.sequencer.PublishSequenceTasklet -import nebulosa.api.sequencer.tasklets.delay.DelayEvent +import nebulosa.batch.processing.JobExecution +import nebulosa.batch.processing.StepExecution +import nebulosa.batch.processing.StepResult +import nebulosa.batch.processing.delay.DelayListener +import nebulosa.batch.processing.delay.DelayStep import nebulosa.common.concurrency.CountUpDownLatch import nebulosa.indi.device.camera.* import nebulosa.io.transferAndClose @@ -10,30 +12,23 @@ import nebulosa.log.loggerFor import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode -import org.springframework.batch.core.JobExecution -import org.springframework.batch.core.JobExecutionListener -import org.springframework.batch.core.StepContribution -import org.springframework.batch.core.StepExecution -import org.springframework.batch.core.scope.context.ChunkContext -import org.springframework.batch.repeat.RepeatStatus import java.io.InputStream import java.nio.file.Path import java.time.Duration import java.time.LocalDateTime import java.time.format.DateTimeFormatter -import java.util.concurrent.atomic.AtomicBoolean import kotlin.io.path.createParentDirectories import kotlin.io.path.outputStream -data class CameraExposureTasklet(override val request: CameraStartCaptureRequest) : - PublishSequenceTasklet(), CameraStartCaptureTasklet, JobExecutionListener, Consumer { +data class CameraExposureStep(override val request: CameraStartCaptureRequest) : CameraStartCaptureStep, DelayListener { private val latch = CountUpDownLatch() - private val aborted = AtomicBoolean() + private val listeners = HashSet() + @Volatile private var aborted = false @Volatile private var exposureCount = 0 @Volatile private var captureElapsedTime = Duration.ZERO!! - @Volatile private var stepExecution: StepExecution? = null + private lateinit var stepExecution: StepExecution private val camera = requireNotNull(request.camera) private val exposureTime = request.exposureTime @@ -42,19 +37,27 @@ data class CameraExposureTasklet(override val request: CameraStartCaptureRequest private val estimatedTime = if (request.isLoop) Duration.ZERO else Duration.ofNanos(exposureTime.toNanos() * request.exposureAmount + exposureDelay.toNanos() * (request.exposureAmount - 1)) + override fun registerListener(listener: CameraCaptureListener) { + listeners.add(listener) + } + + override fun unregisterListener(listener: CameraCaptureListener) { + listeners.remove(listener) + } + @Subscribe(threadMode = ThreadMode.ASYNC) fun onCameraEvent(event: CameraEvent) { if (event.device === camera) { when (event) { is CameraFrameCaptured -> { - save(event.fits, stepExecution!!) + save(event.fits) latch.countDown() } is CameraExposureAborted, is CameraExposureFailed, is CameraDetached -> { latch.reset() - aborted.set(true) + aborted = true } is CameraExposureProgressChanged -> { val exposureRemainingTime = event.device.exposureTime @@ -69,46 +72,45 @@ data class CameraExposureTasklet(override val request: CameraStartCaptureRequest override fun beforeJob(jobExecution: JobExecution) { camera.enableBlob() EventBus.getDefault().register(this) - onNext(CameraCaptureStarted(camera, request.isLoop, estimatedTime, jobExecution, this)) + listeners.forEach { it.onCaptureStarted(this, jobExecution) } captureElapsedTime = Duration.ZERO } override fun afterJob(jobExecution: JobExecution) { camera.disableBlob() EventBus.getDefault().unregister(this) - onNext(CameraCaptureFinished(camera, jobExecution, this)) - close() + listeners.forEach { it.onCaptureFinished(this, jobExecution) } } - override fun execute(contribution: StepContribution, chunkContext: ChunkContext): RepeatStatus { - executeCapture(contribution) - return RepeatStatus.FINISHED + override fun execute(stepExecution: StepExecution): StepResult { + this.stepExecution = stepExecution + LOG.info("starting exposure. camera=${camera.name}") + executeCapture(stepExecution) + return StepResult.FINISHED } - override fun stop() { + override fun stop(mayInterruptIfRunning: Boolean) { LOG.info("stopping exposure. camera=${camera.name}") camera.abortCapture() camera.disableBlob() - aborted.set(true) + aborted = true latch.reset() } - override fun accept(event: DelayEvent) { - captureElapsedTime += event.waitDuration - onNext(CameraCaptureIsWaiting(camera, event.waitDuration, event.remainingTime, event.progress, event.stepExecution, this)) + override fun onDelayElapsed(stepExecution: StepExecution) { + val waitTime = stepExecution.jobExecution.context[DelayStep.WAIT_TIME] as Duration + captureElapsedTime += waitTime onCameraExposureElapsed(Duration.ZERO, Duration.ZERO, 1.0) } - private fun executeCapture(contribution: StepContribution) { - stepExecution = contribution.stepExecution - - if (camera.connected && !aborted.get()) { + private fun executeCapture(stepExecution: StepExecution) { + if (camera.connected && !aborted) { synchronized(camera) { latch.countUp() - exposureCount++ + stepExecution.jobExecution.context[EXPOSURE_AMOUNT] = ++exposureCount - onNext(CameraExposureStarted(camera, exposureCount, stepExecution!!, this)) + listeners.forEach { it.onExposureStarted(stepExecution) } if (request.width > 0 && request.height > 0) { camera.frame(request.x, request.y, request.width, request.height) @@ -130,7 +132,7 @@ data class CameraExposureTasklet(override val request: CameraStartCaptureRequest } } - private fun save(stream: InputStream, stepExecution: StepExecution) { + private fun save(stream: InputStream) { val savePath = if (request.autoSave) { val now = LocalDateTime.now() val fileName = "%s-%s.fits".format(now.format(DATE_TIME_FORMAT), request.frameType) @@ -146,30 +148,47 @@ data class CameraExposureTasklet(override val request: CameraStartCaptureRequest savePath.createParentDirectories() stream.transferAndClose(savePath.outputStream()) - onNext(CameraExposureFinished(camera, exposureCount, stepExecution, this, savePath)) + stepExecution.jobExecution.context[SAVE_PATH] = savePath + + listeners.forEach { it.onExposureFinished(stepExecution) } } catch (e: Throwable) { LOG.error("failed to save FITS", e) - aborted.set(true) + aborted = true } } private fun onCameraExposureElapsed(elapsedTime: Duration, remainingTime: Duration, progress: Double) { - val totalElapsedTime = captureElapsedTime + elapsedTime + val captureElapsedTime = captureElapsedTime + elapsedTime var captureRemainingTime = Duration.ZERO var captureProgress = 0.0 if (!request.isLoop) { - captureRemainingTime = if (estimatedTime > totalElapsedTime) estimatedTime - totalElapsedTime else Duration.ZERO + captureRemainingTime = if (estimatedTime > captureElapsedTime) estimatedTime - captureElapsedTime else Duration.ZERO captureProgress = (estimatedTime - captureRemainingTime).toNanos().toDouble() / estimatedTime.toNanos() } - onNext(CameraExposureElapsed(camera, exposureCount, remainingTime, progress, stepExecution!!, this)) - onNext(CameraCaptureElapsed(camera, exposureCount, captureRemainingTime, captureProgress, totalElapsedTime, stepExecution!!, this)) + stepExecution.jobExecution.context[EXPOSURE_ELAPSED_TIME] = elapsedTime + stepExecution.jobExecution.context[EXPOSURE_REMAINING_TIME] = remainingTime + stepExecution.jobExecution.context[EXPOSURE_PROGRESS] = progress + stepExecution.jobExecution.context[CAPTURE_ELAPSED_TIME] = captureElapsedTime + stepExecution.jobExecution.context[CAPTURE_REMAINING_TIME] = captureRemainingTime + stepExecution.jobExecution.context[CAPTURE_PROGRESS] = captureProgress + + listeners.forEach { it.onExposureElapsed(stepExecution) } } companion object { - @JvmStatic private val LOG = loggerFor() + const val EXPOSURE_AMOUNT = "CAMERA_EXPOSURE.EXPOSURE_AMOUNT" + const val SAVE_PATH = "CAMERA_EXPOSURE.SAVE_PATH" + const val EXPOSURE_ELAPSED_TIME = "CAMERA_EXPOSURE.EXPOSURE_ELAPSED_TIME" + const val EXPOSURE_REMAINING_TIME = "CAMERA_EXPOSURE.EXPOSURE_REMAINING_TIME" + const val EXPOSURE_PROGRESS = "CAMERA_EXPOSURE.EXPOSURE_PROGRESS" + const val CAPTURE_ELAPSED_TIME = "CAMERA_EXPOSURE.CAPTURE_ELAPSED_TIME" + const val CAPTURE_REMAINING_TIME = "CAMERA_EXPOSURE.CAPTURE_REMAINING_TIME" + const val CAPTURE_PROGRESS = "CAMERA_EXPOSURE.CAPTURE_PROGRESS" + + @JvmStatic private val LOG = loggerFor() @JvmStatic private val DATE_TIME_FORMAT = DateTimeFormatter.ofPattern("yyyyMMdd.HHmmssSSS") } } diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraLoopExposureStep.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraLoopExposureStep.kt new file mode 100644 index 000000000..37105d0e3 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraLoopExposureStep.kt @@ -0,0 +1,45 @@ +package nebulosa.api.cameras + +import nebulosa.batch.processing.JobExecution +import nebulosa.batch.processing.StepExecution +import nebulosa.batch.processing.StepResult +import nebulosa.batch.processing.delay.DelayStep + +data class CameraLoopExposureStep( + override val request: CameraStartCaptureRequest, +) : CameraStartCaptureStep { + + private val cameraExposureStep = CameraExposureStep(request) + private val delayStep = DelayStep(request.exposureDelay) + + init { + delayStep.registerListener(cameraExposureStep) + } + + override fun registerListener(listener: CameraCaptureListener) { + cameraExposureStep.registerListener(listener) + } + + override fun unregisterListener(listener: CameraCaptureListener) { + cameraExposureStep.unregisterListener(listener) + } + + override fun execute(stepExecution: StepExecution): StepResult { + cameraExposureStep.execute(stepExecution) + delayStep.execute(stepExecution) + return StepResult.CONTINUABLE + } + + override fun stop(mayInterruptIfRunning: Boolean) { + cameraExposureStep.stop() + delayStep.stop() + } + + override fun beforeJob(jobExecution: JobExecution) { + cameraExposureStep.beforeJob(jobExecution) + } + + override fun afterJob(jobExecution: JobExecution) { + cameraExposureStep.afterJob(jobExecution) + } +} diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraLoopExposureTasklet.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraLoopExposureTasklet.kt deleted file mode 100644 index 69e95472a..000000000 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraLoopExposureTasklet.kt +++ /dev/null @@ -1,42 +0,0 @@ -package nebulosa.api.cameras - -import nebulosa.api.sequencer.PublishSequenceTasklet -import nebulosa.api.sequencer.tasklets.delay.DelayTasklet -import org.springframework.batch.core.JobExecution -import org.springframework.batch.core.JobExecutionListener -import org.springframework.batch.core.StepContribution -import org.springframework.batch.core.scope.context.ChunkContext -import org.springframework.batch.repeat.RepeatStatus - -data class CameraLoopExposureTasklet(override val request: CameraStartCaptureRequest) : - PublishSequenceTasklet(), CameraStartCaptureTasklet, JobExecutionListener { - - private val exposureTasklet = CameraExposureTasklet(request) - private val delayTasklet = DelayTasklet(request.exposureDelay) - - init { - exposureTasklet.subscribe(this) - delayTasklet.subscribe(exposureTasklet) - } - - override fun execute(contribution: StepContribution, chunkContext: ChunkContext): RepeatStatus { - exposureTasklet.execute(contribution, chunkContext) - delayTasklet.execute(contribution, chunkContext) - return RepeatStatus.CONTINUABLE - } - - override fun stop() { - exposureTasklet.stop() - delayTasklet.stop() - } - - override fun beforeJob(jobExecution: JobExecution) { - exposureTasklet.beforeJob(jobExecution) - delayTasklet.beforeJob(jobExecution) - } - - override fun afterJob(jobExecution: JobExecution) { - exposureTasklet.afterJob(jobExecution) - delayTasklet.afterJob(jobExecution) - } -} diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraSequenceJob.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraSequenceJob.kt deleted file mode 100644 index d16338932..000000000 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraSequenceJob.kt +++ /dev/null @@ -1,16 +0,0 @@ -package nebulosa.api.cameras - -import nebulosa.api.sequencer.SequenceJob -import nebulosa.indi.device.camera.Camera -import org.springframework.batch.core.Job -import org.springframework.batch.core.JobExecution - -data class CameraSequenceJob( - val camera: Camera, - val data: CameraStartCaptureRequest, - override val job: Job, - override val jobExecution: JobExecution, -) : SequenceJob { - - override val devices = listOf(camera) -} diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraStartCaptureStep.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraStartCaptureStep.kt new file mode 100644 index 000000000..fb5ecff0a --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraStartCaptureStep.kt @@ -0,0 +1,13 @@ +package nebulosa.api.cameras + +import nebulosa.batch.processing.JobExecutionListener +import nebulosa.batch.processing.Step + +sealed interface CameraStartCaptureStep : Step, JobExecutionListener { + + val request: CameraStartCaptureRequest + + fun registerListener(listener: CameraCaptureListener) + + fun unregisterListener(listener: CameraCaptureListener) +} diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraStartCaptureTasklet.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraStartCaptureTasklet.kt deleted file mode 100644 index dffc38539..000000000 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraStartCaptureTasklet.kt +++ /dev/null @@ -1,8 +0,0 @@ -package nebulosa.api.cameras - -import nebulosa.api.sequencer.SequenceTasklet - -sealed interface CameraStartCaptureTasklet : SequenceTasklet { - - val request: CameraStartCaptureRequest -} diff --git a/api/src/main/kotlin/nebulosa/api/guiding/DitherAfterExposureTasklet.kt b/api/src/main/kotlin/nebulosa/api/guiding/DitherAfterExposureStep.kt similarity index 66% rename from api/src/main/kotlin/nebulosa/api/guiding/DitherAfterExposureTasklet.kt rename to api/src/main/kotlin/nebulosa/api/guiding/DitherAfterExposureStep.kt index 059aca432..8f8b21492 100644 --- a/api/src/main/kotlin/nebulosa/api/guiding/DitherAfterExposureTasklet.kt +++ b/api/src/main/kotlin/nebulosa/api/guiding/DitherAfterExposureStep.kt @@ -1,24 +1,23 @@ package nebulosa.api.guiding +import nebulosa.batch.processing.Step +import nebulosa.batch.processing.StepExecution +import nebulosa.batch.processing.StepResult import nebulosa.common.concurrency.CountUpDownLatch import nebulosa.guiding.GuideState import nebulosa.guiding.Guider import nebulosa.guiding.GuiderListener -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 { +data class DitherAfterExposureStep(@JvmField val request: DitherAfterExposureRequest) : Step, GuiderListener { @Autowired private lateinit var guider: Guider private val ditherLatch = CountUpDownLatch() private val exposureCount = AtomicInteger() - override fun execute(contribution: StepContribution, chunkContext: ChunkContext): RepeatStatus { + override fun execute(stepExecution: StepExecution): StepResult { if (guider.canDither && request.enabled && guider.state == GuideState.GUIDING) { if (exposureCount.get() < request.afterExposures) { try { @@ -36,12 +35,11 @@ data class DitherAfterExposureTasklet(val request: DitherAfterExposureRequest) : } } - return RepeatStatus.FINISHED + return StepResult.FINISHED } - override fun stop() { - ditherLatch.reset(0) - guider.unregisterGuiderListener(this) + override fun stop(mayInterruptIfRunning: Boolean) { + ditherLatch.reset() } override fun onDithered(dx: Double, dy: Double) { diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseElapsed.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseElapsed.kt deleted file mode 100644 index 9e0728c62..000000000 --- a/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseElapsed.kt +++ /dev/null @@ -1,14 +0,0 @@ -package nebulosa.api.guiding - -import com.fasterxml.jackson.annotation.JsonIgnore -import nebulosa.guiding.GuideDirection -import org.springframework.batch.core.StepExecution -import java.time.Duration - -data class GuidePulseElapsed( - val remainingTime: Duration, - override val progress: Double, - val direction: GuideDirection, - @JsonIgnore override val stepExecution: StepExecution, - @JsonIgnore override val tasklet: GuidePulseTasklet, -) : GuidePulseEvent diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseEvent.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseEvent.kt deleted file mode 100644 index 57aaad25b..000000000 --- a/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseEvent.kt +++ /dev/null @@ -1,9 +0,0 @@ -package nebulosa.api.guiding - -import nebulosa.api.sequencer.SequenceStepEvent -import nebulosa.api.sequencer.SequenceTaskletEvent - -sealed interface GuidePulseEvent : SequenceTaskletEvent, SequenceStepEvent { - - override val tasklet: GuidePulseTasklet -} diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseFinished.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseFinished.kt deleted file mode 100644 index c76953e06..000000000 --- a/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseFinished.kt +++ /dev/null @@ -1,12 +0,0 @@ -package nebulosa.api.guiding - -import com.fasterxml.jackson.annotation.JsonIgnore -import org.springframework.batch.core.StepExecution - -data class GuidePulseFinished( - @JsonIgnore override val stepExecution: StepExecution, - @JsonIgnore override val tasklet: GuidePulseTasklet, -) : GuidePulseEvent { - - override val progress = 1.0 -} diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseListener.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseListener.kt new file mode 100644 index 000000000..7efad28e6 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseListener.kt @@ -0,0 +1,8 @@ +package nebulosa.api.guiding + +import nebulosa.batch.processing.StepExecution + +fun interface GuidePulseListener { + + fun onGuidePulseElapsed(stepExecution: StepExecution) +} diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseStarted.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseStarted.kt deleted file mode 100644 index a275bcc08..000000000 --- a/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseStarted.kt +++ /dev/null @@ -1,12 +0,0 @@ -package nebulosa.api.guiding - -import com.fasterxml.jackson.annotation.JsonIgnore -import org.springframework.batch.core.StepExecution - -data class GuidePulseStarted( - @JsonIgnore override val stepExecution: StepExecution, - @JsonIgnore override val tasklet: GuidePulseTasklet, -) : GuidePulseEvent { - - override val progress = 0.0 -} diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseStep.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseStep.kt new file mode 100644 index 000000000..55381229c --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseStep.kt @@ -0,0 +1,68 @@ +package nebulosa.api.guiding + +import nebulosa.batch.processing.Step +import nebulosa.batch.processing.StepExecution +import nebulosa.batch.processing.StepResult +import nebulosa.batch.processing.delay.DelayListener +import nebulosa.batch.processing.delay.DelayStep +import nebulosa.guiding.GuideDirection +import nebulosa.indi.device.guide.GuideOutput +import java.time.Duration + +data class GuidePulseStep(val request: GuidePulseRequest) : Step, DelayListener { + + private val listeners = HashSet() + private val delayStep = DelayStep(request.duration) + + init { + delayStep.registerListener(this) + } + + fun registerListener(listener: GuidePulseListener) { + listeners.add(listener) + } + + fun unregisterListener(listener: GuidePulseListener) { + listeners.remove(listener) + } + + override fun execute(stepExecution: StepExecution): StepResult { + val guideOutput = requireNotNull(request.guideOutput) + + // Force stop in reversed direction. + guideOutput.pulseGuide(Duration.ZERO, request.direction.reversed) + + if (guideOutput.pulseGuide(request.duration, request.direction)) { + delayStep.execute(stepExecution) + } + + return StepResult.FINISHED + } + + override fun stop(mayInterruptIfRunning: Boolean) { + request.guideOutput?.pulseGuide(Duration.ZERO, request.direction) + delayStep.stop() + } + + @Suppress("NAME_SHADOWING") + override fun onDelayElapsed(stepExecution: StepExecution) { + val stepExecution = stepExecution.copy(step = this) + listeners.forEach { it.onGuidePulseElapsed(stepExecution) } + } + + companion object { + + @JvmStatic + private fun GuideOutput.pulseGuide(duration: Duration, direction: GuideDirection): Boolean { + when (direction) { + GuideDirection.NORTH -> guideNorth(duration) + GuideDirection.SOUTH -> guideSouth(duration) + GuideDirection.WEST -> guideWest(duration) + GuideDirection.EAST -> guideEast(duration) + else -> return false + } + + return true + } + } +} diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseTasklet.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseTasklet.kt deleted file mode 100644 index 40717cba0..000000000 --- a/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseTasklet.kt +++ /dev/null @@ -1,64 +0,0 @@ -package nebulosa.api.guiding - -import io.reactivex.rxjava3.functions.Consumer -import nebulosa.api.sequencer.PublishSequenceTasklet -import nebulosa.api.sequencer.tasklets.delay.DelayEvent -import nebulosa.api.sequencer.tasklets.delay.DelayTasklet -import nebulosa.guiding.GuideDirection -import nebulosa.indi.device.guide.GuideOutput -import org.springframework.batch.core.StepContribution -import org.springframework.batch.core.scope.context.ChunkContext -import org.springframework.batch.repeat.RepeatStatus -import java.time.Duration - -data class GuidePulseTasklet(val request: GuidePulseRequest) : PublishSequenceTasklet(), Consumer { - - private val delayTasklet = DelayTasklet(request.duration) - - init { - delayTasklet.subscribe(this) - } - - override fun execute(contribution: StepContribution, chunkContext: ChunkContext): RepeatStatus { - val guideOutput = requireNotNull(request.guideOutput) - val durationInMilliseconds = request.duration - - // Force stop in reversed direction. - guideOutput.pulseGuide(Duration.ZERO, request.direction.reversed) - - if (guideOutput.pulseGuide(durationInMilliseconds, request.direction)) { - delayTasklet.execute(contribution, chunkContext) - } - - return RepeatStatus.FINISHED - } - - override fun stop() { - request.guideOutput?.pulseGuide(Duration.ZERO, request.direction) - delayTasklet.stop() - } - - override fun accept(event: DelayEvent) { - val guidePulseEvent = if (event.isStarted) GuidePulseStarted(event.stepExecution, this) - else if (event.isFinished) GuidePulseFinished(event.stepExecution, this) - else GuidePulseElapsed(event.remainingTime, event.progress, request.direction, event.stepExecution, this) - - onNext(guidePulseEvent) - } - - companion object { - - @JvmStatic - private fun GuideOutput.pulseGuide(duration: Duration, direction: GuideDirection): Boolean { - when (direction) { - GuideDirection.NORTH -> guideNorth(duration) - GuideDirection.SOUTH -> guideSouth(duration) - GuideDirection.WEST -> guideWest(duration) - GuideDirection.EAST -> guideEast(duration) - else -> return false - } - - return true - } - } -} diff --git a/api/src/main/kotlin/nebulosa/api/guiding/WaitForSettleStep.kt b/api/src/main/kotlin/nebulosa/api/guiding/WaitForSettleStep.kt new file mode 100644 index 000000000..39908b614 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/guiding/WaitForSettleStep.kt @@ -0,0 +1,17 @@ +package nebulosa.api.guiding + +import nebulosa.batch.processing.JobExecution +import nebulosa.batch.processing.Step +import nebulosa.batch.processing.StepResult +import nebulosa.guiding.Guider + +data class WaitForSettleStep(private val guider: Guider) : Step { + + override fun execute(jobExecution: JobExecution): StepResult { + if (guider.isSettling) { + guider.waitForSettle() + } + + return StepResult.FINISHED + } +} diff --git a/api/src/main/kotlin/nebulosa/api/guiding/WaitForSettleTasklet.kt b/api/src/main/kotlin/nebulosa/api/guiding/WaitForSettleTasklet.kt deleted file mode 100644 index af47a6808..000000000 --- a/api/src/main/kotlin/nebulosa/api/guiding/WaitForSettleTasklet.kt +++ /dev/null @@ -1,24 +0,0 @@ -package nebulosa.api.guiding - -import nebulosa.guiding.Guider -import org.springframework.batch.core.StepContribution -import org.springframework.batch.core.scope.context.ChunkContext -import org.springframework.batch.core.step.tasklet.StoppableTasklet -import org.springframework.batch.repeat.RepeatStatus -import org.springframework.beans.factory.annotation.Autowired - -class WaitForSettleTasklet : StoppableTasklet { - - @Autowired private lateinit var guider: Guider - - override fun execute(contribution: StepContribution, chunkContext: ChunkContext): RepeatStatus { - if (guider.isSettling) { - guider.waitForSettle() - } - - return RepeatStatus.FINISHED - } - - override fun stop() { - } -} diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/PublishSequenceTasklet.kt b/api/src/main/kotlin/nebulosa/api/sequencer/PublishSequenceTasklet.kt deleted file mode 100644 index f6b963240..000000000 --- a/api/src/main/kotlin/nebulosa/api/sequencer/PublishSequenceTasklet.kt +++ /dev/null @@ -1,55 +0,0 @@ -package nebulosa.api.sequencer - -import io.reactivex.rxjava3.core.Observable -import io.reactivex.rxjava3.core.Observer -import io.reactivex.rxjava3.disposables.Disposable -import io.reactivex.rxjava3.functions.Consumer -import io.reactivex.rxjava3.subjects.PublishSubject -import io.reactivex.rxjava3.subjects.Subject -import nebulosa.log.debug -import nebulosa.log.loggerFor -import java.io.Closeable - -abstract class PublishSequenceTasklet(@JvmField protected val subject: Subject) : SequenceTasklet, Closeable { - - constructor() : this(PublishSubject.create()) - - protected open fun Observable.transform() = this - - final override fun subscribe(onNext: Consumer): Disposable { - return subject.transform().subscribe(onNext) - } - - final override fun subscribe(observer: Observer) { - return subject.transform().subscribe(observer) - } - - final override fun onSubscribe(disposable: Disposable) { - subject.onSubscribe(disposable) - } - - @Synchronized - final override fun onNext(event: T) { - LOG.debug { "$event" } - subject.onNext(event) - } - - @Synchronized - final override fun onError(e: Throwable) { - subject.onError(e) - } - - @Synchronized - final override fun onComplete() { - subject.onComplete() - } - - final override fun close() { - onComplete() - } - - companion object { - - @JvmStatic private val LOG = loggerFor>() - } -} diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/SequenceFlowFactory.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SequenceFlowFactory.kt deleted file mode 100644 index 6eebacd37..000000000 --- a/api/src/main/kotlin/nebulosa/api/sequencer/SequenceFlowFactory.kt +++ /dev/null @@ -1,66 +0,0 @@ -package nebulosa.api.sequencer - -import nebulosa.api.cameras.CameraExposureTasklet -import nebulosa.api.guiding.GuidePulseTasklet -import nebulosa.api.guiding.WaitForSettleTasklet -import nebulosa.api.sequencer.tasklets.delay.DelayTasklet -import nebulosa.common.concurrency.Incrementer -import org.springframework.batch.core.job.builder.FlowBuilder -import org.springframework.batch.core.job.flow.support.SimpleFlow -import org.springframework.beans.factory.config.ConfigurableBeanFactory -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration -import org.springframework.context.annotation.Scope -import org.springframework.core.task.SimpleAsyncTaskExecutor - -@Configuration -class SequenceFlowFactory( - private val flowIncrementer: Incrementer, - private val sequenceStepFactory: SequenceStepFactory, - private val simpleAsyncTaskExecutor: SimpleAsyncTaskExecutor, -) { - - @Bean(name = ["cameraExposureFlow"], autowireCandidate = false) - @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) - fun cameraExposure(cameraExposureTasklet: CameraExposureTasklet): SimpleFlow { - val step = sequenceStepFactory.cameraExposure(cameraExposureTasklet) - return FlowBuilder("Flow.CameraExposure.${flowIncrementer.increment()}").start(step).end() - } - - @Bean(name = ["delayFlow"], autowireCandidate = false) - @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) - fun delay(delayTasklet: DelayTasklet): SimpleFlow { - val step = sequenceStepFactory.delay(delayTasklet) - return FlowBuilder("Flow.Delay.${flowIncrementer.increment()}").start(step).end() - } - - @Bean(name = ["waitForSettleFlow"], autowireCandidate = false) - @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) - fun waitForSettle(waitForSettleTasklet: WaitForSettleTasklet): SimpleFlow { - val step = sequenceStepFactory.waitForSettle(waitForSettleTasklet) - return FlowBuilder("Flow.WaitForSettle.${flowIncrementer.increment()}").start(step).end() - } - - @Bean(name = ["guidePulseFlow"], autowireCandidate = false) - @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) - fun guidePulse( - initialPauseDelayTasklet: DelayTasklet, - forwardGuidePulseTasklet: GuidePulseTasklet, backwardGuidePulseTasklet: GuidePulseTasklet - ): SimpleFlow { - return FlowBuilder("Flow.GuidePulse.${flowIncrementer.increment()}") - .start(sequenceStepFactory.delay(initialPauseDelayTasklet)) - .next(sequenceStepFactory.guidePulse(forwardGuidePulseTasklet)) - .next(sequenceStepFactory.guidePulse(backwardGuidePulseTasklet)) - .end() - } - - @Bean(name = ["delayAndWaitForSettleFlow"], autowireCandidate = false) - @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) - fun delayAndWaitForSettle(cameraDelayTasklet: DelayTasklet, waitForSettleTasklet: WaitForSettleTasklet): SimpleFlow { - return FlowBuilder("Flow.DelayAndWaitForSettle.${flowIncrementer.increment()}") - .start(delay(cameraDelayTasklet)) - .split(simpleAsyncTaskExecutor) - .add(waitForSettle(waitForSettleTasklet)) - .end() - } -} diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/SequenceFlowStepFactory.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SequenceFlowStepFactory.kt deleted file mode 100644 index 441e85cd2..000000000 --- a/api/src/main/kotlin/nebulosa/api/sequencer/SequenceFlowStepFactory.kt +++ /dev/null @@ -1,28 +0,0 @@ -package nebulosa.api.sequencer - -import nebulosa.api.guiding.WaitForSettleTasklet -import nebulosa.api.sequencer.tasklets.delay.DelayTasklet -import nebulosa.common.concurrency.Incrementer -import org.springframework.batch.core.Step -import org.springframework.batch.core.repository.JobRepository -import org.springframework.batch.core.step.builder.StepBuilder -import org.springframework.beans.factory.config.ConfigurableBeanFactory -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration -import org.springframework.context.annotation.Scope - -@Configuration -class SequenceFlowStepFactory( - private val jobRepository: JobRepository, - private val stepIncrementer: Incrementer, - private val sequenceFlowFactory: SequenceFlowFactory, -) { - - @Bean(name = ["delayAndWaitForSettleFlowStep"], autowireCandidate = false) - @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) - fun delayAndWaitForSettle(cameraDelayTasklet: DelayTasklet, waitForSettleTasklet: WaitForSettleTasklet): Step { - return StepBuilder("FlowStep.DelayAndWaitForSettle.${stepIncrementer.increment()}", jobRepository) - .flow(sequenceFlowFactory.delayAndWaitForSettle(cameraDelayTasklet, waitForSettleTasklet)) - .build() - } -} diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/SequenceJob.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SequenceJob.kt deleted file mode 100644 index b9fd3d3fd..000000000 --- a/api/src/main/kotlin/nebulosa/api/sequencer/SequenceJob.kt +++ /dev/null @@ -1,26 +0,0 @@ -package nebulosa.api.sequencer - -import nebulosa.indi.device.Device -import org.springframework.batch.core.Job -import org.springframework.batch.core.JobExecution - -interface SequenceJob { - - val devices: List - - val job: Job - - val jobExecution: JobExecution - - val jobId - get() = jobExecution.jobId - - val startTime - get() = jobExecution.startTime - - val endTime - get() = jobExecution.endTime - - val isRunning - get() = jobExecution.isRunning -} diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/SequenceJobEvent.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SequenceJobEvent.kt deleted file mode 100644 index b54d8a40b..000000000 --- a/api/src/main/kotlin/nebulosa/api/sequencer/SequenceJobEvent.kt +++ /dev/null @@ -1,10 +0,0 @@ -package nebulosa.api.sequencer - -import org.springframework.batch.core.JobExecution - -interface SequenceJobEvent { - - val jobExecution: JobExecution - - val progress: Double -} diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/SequenceJobExecutor.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SequenceJobExecutor.kt deleted file mode 100644 index 7ee1e65a2..000000000 --- a/api/src/main/kotlin/nebulosa/api/sequencer/SequenceJobExecutor.kt +++ /dev/null @@ -1,26 +0,0 @@ -package nebulosa.api.sequencer - -import nebulosa.indi.device.Device - -interface SequenceJobExecutor : Iterable { - - fun execute(request: T): J - - fun sequenceJobFor(vararg devices: Device): J? { - fun find(task: J): Boolean { - for (i in devices.indices) { - if (i >= task.devices.size || task.devices[i].name != devices[i].name) { - return false - } - } - - return true - } - - return findLast(::find) - } - - fun sequenceJobWithId(jobId: Long): J? { - return find { it.jobId == jobId } - } -} diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/SequenceJobFactory.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SequenceJobFactory.kt deleted file mode 100644 index c0e0e848c..000000000 --- a/api/src/main/kotlin/nebulosa/api/sequencer/SequenceJobFactory.kt +++ /dev/null @@ -1,90 +0,0 @@ -package nebulosa.api.sequencer - -import io.reactivex.rxjava3.functions.Consumer -import nebulosa.api.cameras.CameraCaptureEvent -import nebulosa.api.cameras.CameraStartCaptureRequest -import nebulosa.common.concurrency.Incrementer -import org.springframework.batch.core.Job -import org.springframework.batch.core.JobExecutionListener -import org.springframework.batch.core.job.builder.JobBuilder -import org.springframework.batch.core.job.flow.Flow -import org.springframework.batch.core.repository.JobRepository -import org.springframework.beans.factory.config.ConfigurableBeanFactory -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration -import org.springframework.context.annotation.Scope -import org.springframework.core.task.SimpleAsyncTaskExecutor - -@Configuration -class SequenceJobFactory( - private val jobRepository: JobRepository, - private val sequenceFlowStepFactory: SequenceFlowStepFactory, - private val sequenceStepFactory: SequenceStepFactory, - private val sequenceTaskletFactory: SequenceTaskletFactory, - private val jobIncrementer: Incrementer, - private val simpleAsyncTaskExecutor: SimpleAsyncTaskExecutor, -) { - - @Bean(name = ["cameraLoopCaptureJob"], autowireCandidate = false) - @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) - fun cameraLoopCapture( - request: CameraStartCaptureRequest, - cameraCaptureListener: Consumer, - ): Job { - val cameraExposureTasklet = sequenceTaskletFactory.cameraLoopExposure(request) - cameraExposureTasklet.subscribe(cameraCaptureListener) - - val cameraExposureStep = sequenceStepFactory.cameraExposure(cameraExposureTasklet) - - return JobBuilder("CameraCapture.Job.${jobIncrementer.increment()}", jobRepository) - .start(cameraExposureStep) - .listener(cameraExposureTasklet) - .build() - } - - @Bean(name = ["cameraCaptureJob"], autowireCandidate = false) - @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) - fun cameraCapture( - request: CameraStartCaptureRequest, - cameraCaptureListener: Consumer, - ): Job { - val cameraExposureTasklet = sequenceTaskletFactory.cameraExposure(request) - cameraExposureTasklet.subscribe(cameraCaptureListener) - - val cameraDelayTasklet = sequenceTaskletFactory.delay(request.exposureDelay) - cameraDelayTasklet.subscribe(cameraExposureTasklet) - - val ditherTasklet = sequenceTaskletFactory.ditherAfterExposure(request.dither) - val waitForSettleTasklet = sequenceTaskletFactory.waitForSettle() - - val jobBuilder = JobBuilder("CameraCapture.Job.${jobIncrementer.increment()}", jobRepository) - .start(sequenceStepFactory.waitForSettle(waitForSettleTasklet)) - .next(sequenceStepFactory.cameraExposure(cameraExposureTasklet)) - - repeat(request.exposureAmount - 1) { - jobBuilder.next(sequenceFlowStepFactory.delayAndWaitForSettle(cameraDelayTasklet, waitForSettleTasklet)) - .next(sequenceStepFactory.cameraExposure(cameraExposureTasklet)) - .next(sequenceStepFactory.dither(ditherTasklet)) - } - - return jobBuilder - .listener(cameraExposureTasklet) - .listener(cameraDelayTasklet) - .build() - } - - @Bean(name = ["darvJob"], autowireCandidate = false) - @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) - fun darvPolarAlignment( - cameraExposureFlow: Flow, guidePulseFlow: Flow, - vararg listeners: JobExecutionListener, - ): Job { - return JobBuilder("DARVPolarAlignment.Job.${jobIncrementer.increment()}", jobRepository) - .start(cameraExposureFlow) - .split(simpleAsyncTaskExecutor) - .add(guidePulseFlow) - .end() - .also { listeners.forEach(it::listener) } - .build() - } -} diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/SequenceJobSerializer.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SequenceJobSerializer.kt deleted file mode 100644 index 88a0ce260..000000000 --- a/api/src/main/kotlin/nebulosa/api/sequencer/SequenceJobSerializer.kt +++ /dev/null @@ -1,20 +0,0 @@ -package nebulosa.api.sequencer - -import com.fasterxml.jackson.core.JsonGenerator -import com.fasterxml.jackson.databind.SerializerProvider -import com.fasterxml.jackson.databind.ser.std.StdSerializer -import org.springframework.stereotype.Component -import java.time.ZoneOffset - -@Component -class SequenceJobSerializer : StdSerializer(SequenceJob::class.java) { - - override fun serialize(value: SequenceJob, gen: JsonGenerator, provider: SerializerProvider) { - gen.writeStartObject() - gen.writeObjectField("devices", value.devices) - gen.writeNumberField("jobId", value.jobId) - gen.writeNumberField("startTime", value.startTime?.atZone(ZoneOffset.UTC)?.toInstant()?.toEpochMilli() ?: 0L) - gen.writeNumberField("endTime", value.endTime?.atZone(ZoneOffset.UTC)?.toInstant()?.toEpochMilli() ?: 0L) - gen.writeEndObject() - } -} diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/SequenceStepEvent.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SequenceStepEvent.kt deleted file mode 100644 index eb81f7aea..000000000 --- a/api/src/main/kotlin/nebulosa/api/sequencer/SequenceStepEvent.kt +++ /dev/null @@ -1,12 +0,0 @@ -package nebulosa.api.sequencer - -import com.fasterxml.jackson.annotation.JsonIgnore -import org.springframework.batch.core.StepExecution - -interface SequenceStepEvent : SequenceJobEvent { - - val stepExecution: StepExecution - - override val jobExecution - @JsonIgnore get() = stepExecution.jobExecution -} diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/SequenceStepFactory.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SequenceStepFactory.kt deleted file mode 100644 index a7dcd2931..000000000 --- a/api/src/main/kotlin/nebulosa/api/sequencer/SequenceStepFactory.kt +++ /dev/null @@ -1,64 +0,0 @@ -package nebulosa.api.sequencer - -import nebulosa.api.cameras.CameraStartCaptureTasklet -import nebulosa.api.guiding.DitherAfterExposureTasklet -import nebulosa.api.guiding.GuidePulseTasklet -import nebulosa.api.guiding.WaitForSettleTasklet -import nebulosa.api.sequencer.tasklets.delay.DelayTasklet -import nebulosa.common.concurrency.Incrementer -import org.springframework.batch.core.repository.JobRepository -import org.springframework.batch.core.step.builder.StepBuilder -import org.springframework.batch.core.step.tasklet.TaskletStep -import org.springframework.beans.factory.config.ConfigurableBeanFactory -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration -import org.springframework.context.annotation.Scope -import org.springframework.transaction.PlatformTransactionManager - -@Configuration -class SequenceStepFactory( - private val jobRepository: JobRepository, - private val platformTransactionManager: PlatformTransactionManager, - private val stepIncrementer: Incrementer, -) { - - @Bean(name = ["delayStep"], autowireCandidate = false) - @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) - fun delay(delayTasklet: DelayTasklet): TaskletStep { - return StepBuilder("Step.Delay.${stepIncrementer.increment()}", jobRepository) - .tasklet(delayTasklet, platformTransactionManager) - .build() - } - - @Bean(name = ["cameraExposureStep"], autowireCandidate = false) - @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) - fun cameraExposure(cameraExposureTasklet: CameraStartCaptureTasklet): TaskletStep { - return StepBuilder("Step.Exposure.${stepIncrementer.increment()}", jobRepository) - .tasklet(cameraExposureTasklet, platformTransactionManager) - .build() - } - - @Bean(name = ["guidePulseStep"], autowireCandidate = false) - @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) - fun guidePulse(guidePulseTasklet: GuidePulseTasklet): TaskletStep { - return StepBuilder("Step.GuidePulse.${stepIncrementer.increment()}", jobRepository) - .tasklet(guidePulseTasklet, platformTransactionManager) - .build() - } - - @Bean(name = ["ditherStep"], autowireCandidate = false) - @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) - fun dither(ditherAfterExposureTasklet: DitherAfterExposureTasklet): TaskletStep { - return StepBuilder("Step.DitherAfterExposure.${stepIncrementer.increment()}", jobRepository) - .tasklet(ditherAfterExposureTasklet, platformTransactionManager) - .build() - } - - @Bean(name = ["waitForSettleStep"], autowireCandidate = false) - @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) - fun waitForSettle(waitForSettleTasklet: WaitForSettleTasklet): TaskletStep { - return StepBuilder("Step.WaitForSettle.${stepIncrementer.increment()}", jobRepository) - .tasklet(waitForSettleTasklet, platformTransactionManager) - .build() - } -} diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/SequenceTasklet.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SequenceTasklet.kt deleted file mode 100644 index 029acce13..000000000 --- a/api/src/main/kotlin/nebulosa/api/sequencer/SequenceTasklet.kt +++ /dev/null @@ -1,12 +0,0 @@ -package nebulosa.api.sequencer - -import io.reactivex.rxjava3.core.ObservableSource -import io.reactivex.rxjava3.core.Observer -import io.reactivex.rxjava3.disposables.Disposable -import io.reactivex.rxjava3.functions.Consumer -import org.springframework.batch.core.step.tasklet.StoppableTasklet - -interface SequenceTasklet : StoppableTasklet, ObservableSource, Observer { - - fun subscribe(onNext: Consumer): Disposable -} diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/SequenceTaskletEvent.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SequenceTaskletEvent.kt deleted file mode 100644 index 3bba9015c..000000000 --- a/api/src/main/kotlin/nebulosa/api/sequencer/SequenceTaskletEvent.kt +++ /dev/null @@ -1,10 +0,0 @@ -package nebulosa.api.sequencer - -import org.springframework.batch.core.step.tasklet.Tasklet - -interface SequenceTaskletEvent { - - val tasklet: Tasklet - - val progress: Double -} diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/SequenceTaskletFactory.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SequenceTaskletFactory.kt deleted file mode 100644 index 68c097233..000000000 --- a/api/src/main/kotlin/nebulosa/api/sequencer/SequenceTaskletFactory.kt +++ /dev/null @@ -1,52 +0,0 @@ -package nebulosa.api.sequencer - -import nebulosa.api.cameras.CameraExposureTasklet -import nebulosa.api.cameras.CameraLoopExposureTasklet -import nebulosa.api.cameras.CameraStartCaptureRequest -import nebulosa.api.guiding.* -import nebulosa.api.sequencer.tasklets.delay.DelayTasklet -import org.springframework.beans.factory.config.ConfigurableBeanFactory -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration -import org.springframework.context.annotation.Scope -import java.time.Duration - -@Configuration -class SequenceTaskletFactory { - - @Bean(name = ["delayTasklet"], autowireCandidate = false) - @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) - fun delay(duration: Duration): DelayTasklet { - return DelayTasklet(duration) - } - - @Bean(name = ["cameraExposureTasklet"], autowireCandidate = false) - @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) - fun cameraExposure(request: CameraStartCaptureRequest): CameraExposureTasklet { - return CameraExposureTasklet(request) - } - - @Bean(name = ["cameraLoopExposureTasklet"], autowireCandidate = false) - @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) - fun cameraLoopExposure(request: CameraStartCaptureRequest): CameraLoopExposureTasklet { - return CameraLoopExposureTasklet(request) - } - - @Bean(name = ["guidePulseTasklet"], autowireCandidate = false) - @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) - fun guidePulse(request: GuidePulseRequest): GuidePulseTasklet { - return GuidePulseTasklet(request) - } - - @Bean(name = ["ditherTasklet"], autowireCandidate = false) - @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) - fun ditherAfterExposure(request: DitherAfterExposureRequest): DitherAfterExposureTasklet { - return DitherAfterExposureTasklet(request) - } - - @Bean(name = ["waitForSettleTasklet"], autowireCandidate = false) - @Scope(value = ConfigurableBeanFactory.SCOPE_SINGLETON) - fun waitForSettle(): WaitForSettleTasklet { - return WaitForSettleTasklet() - } -} diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/tasklets/delay/DelayElapsed.kt b/api/src/main/kotlin/nebulosa/api/sequencer/tasklets/delay/DelayElapsed.kt deleted file mode 100644 index af1aa9450..000000000 --- a/api/src/main/kotlin/nebulosa/api/sequencer/tasklets/delay/DelayElapsed.kt +++ /dev/null @@ -1,17 +0,0 @@ -package nebulosa.api.sequencer.tasklets.delay - -import com.fasterxml.jackson.annotation.JsonIgnore -import org.springframework.batch.core.StepExecution -import java.time.Duration - -data class DelayElapsed( - override val remainingTime: Duration, - override val waitDuration: Duration, - @JsonIgnore override val stepExecution: StepExecution, - @JsonIgnore override val tasklet: DelayTasklet, -) : DelayEvent { - - override val progress - get() = if (remainingTime > Duration.ZERO) (tasklet.duration.toNanos() - remainingTime.toNanos()) / tasklet.duration.toNanos().toDouble() - else 1.0 -} diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/tasklets/delay/DelayEvent.kt b/api/src/main/kotlin/nebulosa/api/sequencer/tasklets/delay/DelayEvent.kt deleted file mode 100644 index cf0dad2c5..000000000 --- a/api/src/main/kotlin/nebulosa/api/sequencer/tasklets/delay/DelayEvent.kt +++ /dev/null @@ -1,21 +0,0 @@ -package nebulosa.api.sequencer.tasklets.delay - -import com.fasterxml.jackson.annotation.JsonIgnore -import nebulosa.api.sequencer.SequenceStepEvent -import nebulosa.api.sequencer.SequenceTaskletEvent -import java.time.Duration - -sealed interface DelayEvent : SequenceStepEvent, SequenceTaskletEvent { - - override val tasklet: DelayTasklet - - val remainingTime: Duration - - val waitDuration: Duration - - val isStarted - @JsonIgnore get() = remainingTime == tasklet.duration - - val isFinished - @JsonIgnore get() = remainingTime == Duration.ZERO -} diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/tasklets/delay/DelayTasklet.kt b/api/src/main/kotlin/nebulosa/api/sequencer/tasklets/delay/DelayTasklet.kt deleted file mode 100644 index 48770a73e..000000000 --- a/api/src/main/kotlin/nebulosa/api/sequencer/tasklets/delay/DelayTasklet.kt +++ /dev/null @@ -1,53 +0,0 @@ -package nebulosa.api.sequencer.tasklets.delay - -import nebulosa.api.sequencer.PublishSequenceTasklet -import org.springframework.batch.core.JobExecution -import org.springframework.batch.core.JobExecutionListener -import org.springframework.batch.core.StepContribution -import org.springframework.batch.core.scope.context.ChunkContext -import org.springframework.batch.repeat.RepeatStatus -import java.time.Duration -import java.util.concurrent.atomic.AtomicBoolean - -data class DelayTasklet(val duration: Duration) : PublishSequenceTasklet(), JobExecutionListener { - - private val aborted = AtomicBoolean() - - override fun execute(contribution: StepContribution, chunkContext: ChunkContext): RepeatStatus { - val stepExecution = contribution.stepExecution - var remainingTime = duration - - if (remainingTime > Duration.ZERO) { - while (!aborted.get() && remainingTime > Duration.ZERO) { - val waitTime = minOf(remainingTime, DELAY_INTERVAL) - - if (waitTime > Duration.ZERO) { - onNext(DelayElapsed(remainingTime, waitTime, stepExecution, this)) - Thread.sleep(waitTime.toMillis()) - remainingTime -= waitTime - } - } - - onNext(DelayElapsed(Duration.ZERO, Duration.ZERO, stepExecution, this)) - } - - return RepeatStatus.FINISHED - } - - override fun afterJob(jobExecution: JobExecution) { - close() - } - - override fun stop() { - aborted.set(true) - } - - fun wasAborted(): Boolean { - return aborted.get() - } - - companion object { - - @JvmField val DELAY_INTERVAL = Duration.ofMillis(500)!! - } -} diff --git a/api/src/main/resources/application.yml b/api/src/main/resources/application.yml index c763c2860..453e17745 100644 --- a/api/src/main/resources/application.yml +++ b/api/src/main/resources/application.yml @@ -25,11 +25,3 @@ spring: baseline-on-migrate: true baseline-version: 0 table: migrations - - batch: - job: - enabled: false - datasource: - url: 'jdbc:sqlite:file:nebulosa.db?mode=memory' - jdbc: - initialize-schema: always diff --git a/nebulosa-batch-processing/build.gradle.kts b/nebulosa-batch-processing/build.gradle.kts index 055c5463b..24ca9524d 100644 --- a/nebulosa-batch-processing/build.gradle.kts +++ b/nebulosa-batch-processing/build.gradle.kts @@ -5,7 +5,6 @@ plugins { dependencies { implementation(project(":nebulosa-log")) - testImplementation(project(":nebulosa-common")) testImplementation(project(":nebulosa-test")) } diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/AsyncJobLauncher.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/AsyncJobLauncher.kt index 921608234..3ee4f3322 100644 --- a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/AsyncJobLauncher.kt +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/AsyncJobLauncher.kt @@ -5,24 +5,24 @@ import java.util.concurrent.ExecutorService open class AsyncJobLauncher(private val executor: ExecutorService) : JobLauncher, StepInterceptor { - private val jobListeners = HashSet() - private val stepListeners = HashSet() + private val jobListeners = HashSet() + private val stepListeners = HashSet() private val stepInterceptors = HashSet() private val jobs = LinkedHashMap() - fun registerJobListener(listener: JobListener) { + fun registerJobListener(listener: JobExecutionListener) { jobListeners.add(listener) } - fun unregisterJobListener(listener: JobListener) { + fun unregisterJobListener(listener: JobExecutionListener) { jobListeners.remove(listener) } - fun registerStepListener(listener: StepListener) { + fun registerStepListener(listener: StepExecutionListener) { stepListeners.add(listener) } - fun unregisterStepListener(listener: StepListener) { + fun unregisterStepListener(listener: StepExecutionListener) { stepListeners.remove(listener) } @@ -77,16 +77,24 @@ open class AsyncJobLauncher(private val executor: ExecutorService) : JobLauncher val interceptors = ArrayList(stepInterceptors) interceptors.add(this) + val stepJobListeners = HashSet() + try { while (jobExecution.canContinue && job.hasNext(jobExecution)) { val step = job.next(jobExecution) + if (step is JobExecutionListener) { + if (stepJobListeners.add(step)) { + step.beforeJob(jobExecution) + } + } + if (step is FlowStep) { val flow = object : Step { - override fun execute(jobExecution: JobExecution): StepResult { - step.toList().parallelStream().forEach { execute(interceptors, it, jobExecution) } - return step.execute(jobExecution) + override fun execute(stepExecution: StepExecution): StepResult { + step.toList().parallelStream().forEach { execute(interceptors, stepExecution) } + return step.execute(stepExecution) } override fun stop(mayInterruptIfRunning: Boolean) { @@ -94,9 +102,9 @@ open class AsyncJobLauncher(private val executor: ExecutorService) : JobLauncher } } - execute(interceptors, flow, jobExecution) + execute(interceptors, StepExecution(flow, jobExecution)) } else { - execute(interceptors, step, jobExecution) + execute(interceptors, StepExecution(step, jobExecution)) } } @@ -111,6 +119,7 @@ open class AsyncJobLauncher(private val executor: ExecutorService) : JobLauncher job.afterJob(jobExecution) jobListeners.forEach { it.afterJob(jobExecution) } + stepJobListeners.forEach { it.afterJob(jobExecution) } } return jobExecution @@ -121,18 +130,20 @@ open class AsyncJobLauncher(private val executor: ExecutorService) : JobLauncher } override fun intercept(chain: StepChain): StepResult { - return chain.step.execute(chain.jobExecution) + return chain.stepExecution.step.execute(chain.stepExecution) } - private fun execute(interceptors: List, step: Step, jobExecution: JobExecution) { - val chain = StepInterceptorChain(interceptors, jobExecution, step) + private fun execute(interceptors: List, stepExecution: StepExecution) { + val chain = StepInterceptorChain(interceptors, stepExecution) var status: RepeatStatus do { - stepListeners.forEach { it.beforeStep(step, jobExecution) } + stepListeners.forEach { it.beforeStep(stepExecution) } val result = chain.proceed() - stepListeners.forEach { it.afterStep(step, jobExecution) } + stepListeners.forEach { it.afterStep(stepExecution) } status = result.get() } while (status == RepeatStatus.CONTINUABLE) + + stepExecution.finishedAt = LocalDateTime.now() } } diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/FlowStep.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/FlowStep.kt index eeebd8d2a..5c62620bf 100644 --- a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/FlowStep.kt +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/FlowStep.kt @@ -2,6 +2,10 @@ package nebulosa.batch.processing interface FlowStep : Step, Iterable { + override fun execute(stepExecution: StepExecution): StepResult { + return StepResult.FINISHED + } + override fun stop(mayInterruptIfRunning: Boolean) { forEach { it.stop(mayInterruptIfRunning) } } diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/Job.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/Job.kt index 54be67d84..11dcf486a 100644 --- a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/Job.kt +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/Job.kt @@ -1,6 +1,6 @@ package nebulosa.batch.processing -interface Job : JobListener, Stoppable { +interface Job : JobExecutionListener, Stoppable { val id: String diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobExecutionListener.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobExecutionListener.kt new file mode 100644 index 000000000..70508ddc9 --- /dev/null +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobExecutionListener.kt @@ -0,0 +1,8 @@ +package nebulosa.batch.processing + +interface JobExecutionListener { + + fun beforeJob(jobExecution: JobExecution) = Unit + + fun afterJob(jobExecution: JobExecution) = Unit +} diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobListener.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobListener.kt deleted file mode 100644 index 3fe925cf0..000000000 --- a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobListener.kt +++ /dev/null @@ -1,8 +0,0 @@ -package nebulosa.batch.processing - -interface JobListener { - - fun beforeJob(jobExecution: JobExecution) - - fun afterJob(jobExecution: JobExecution) -} diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/SimpleFlowStep.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/SimpleFlowStep.kt index 799ff8b09..ff2db4de0 100644 --- a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/SimpleFlowStep.kt +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/SimpleFlowStep.kt @@ -4,15 +4,7 @@ abstract class SimpleFlowStep : FlowStep { protected abstract val steps: Collection - override fun execute(jobExecution: JobExecution): StepResult { - return StepResult.FINISHED - } - - final override fun iterator(): Iterator { + override fun iterator(): Iterator { return steps.iterator() } - - final override fun stop(mayInterruptIfRunning: Boolean) { - super.stop(mayInterruptIfRunning) - } } diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/Step.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/Step.kt index 02ab9ce54..e9a5d7ce3 100644 --- a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/Step.kt +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/Step.kt @@ -2,7 +2,7 @@ package nebulosa.batch.processing interface Step : Stoppable { - fun execute(jobExecution: JobExecution): StepResult + fun execute(stepExecution: StepExecution): StepResult override fun stop(mayInterruptIfRunning: Boolean) = Unit } diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepChain.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepChain.kt index 3622e2745..1eb7052de 100644 --- a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepChain.kt +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepChain.kt @@ -2,9 +2,7 @@ package nebulosa.batch.processing interface StepChain { - val step: Step - - val jobExecution: JobExecution + val stepExecution: StepExecution fun proceed(): StepResult } diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepExecution.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepExecution.kt new file mode 100644 index 000000000..bb625716b --- /dev/null +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepExecution.kt @@ -0,0 +1,10 @@ +package nebulosa.batch.processing + +import java.time.LocalDateTime + +data class StepExecution( + val step: Step, + val jobExecution: JobExecution, + val startedAt: LocalDateTime = LocalDateTime.now(), + var finishedAt: LocalDateTime? = null, +) diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepExecutionListener.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepExecutionListener.kt new file mode 100644 index 000000000..009528698 --- /dev/null +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepExecutionListener.kt @@ -0,0 +1,8 @@ +package nebulosa.batch.processing + +interface StepExecutionListener { + + fun beforeStep(stepExecution: StepExecution) = Unit + + fun afterStep(stepExecution: StepExecution) = Unit +} diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepInterceptorChain.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepInterceptorChain.kt index 3217af44e..b5eef3542 100644 --- a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepInterceptorChain.kt +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepInterceptorChain.kt @@ -2,13 +2,12 @@ package nebulosa.batch.processing data class StepInterceptorChain( private val interceptors: List, - override val jobExecution: JobExecution, - override val step: Step, + override val stepExecution: StepExecution, private val index: Int = 0, ) : StepChain { override fun proceed(): StepResult { - val next = StepInterceptorChain(interceptors, jobExecution, step, index + 1) + val next = StepInterceptorChain(interceptors, stepExecution, index + 1) val interceptor = interceptors[index] return interceptor.intercept(next) } diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepListener.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepListener.kt deleted file mode 100644 index 55f3991a9..000000000 --- a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepListener.kt +++ /dev/null @@ -1,8 +0,0 @@ -package nebulosa.batch.processing - -interface StepListener { - - fun beforeStep(step: Step, jobExecution: JobExecution) - - fun afterStep(step: Step, jobExecution: JobExecution) -} diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/delay/DelayListener.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/delay/DelayListener.kt new file mode 100644 index 000000000..7123841c3 --- /dev/null +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/delay/DelayListener.kt @@ -0,0 +1,8 @@ +package nebulosa.batch.processing.delay + +import nebulosa.batch.processing.StepExecution + +fun interface DelayListener { + + fun onDelayElapsed(stepExecution: StepExecution) +} diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/delay/DelayStep.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/delay/DelayStep.kt new file mode 100644 index 000000000..aae7c0c7f --- /dev/null +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/delay/DelayStep.kt @@ -0,0 +1,62 @@ +package nebulosa.batch.processing.delay + +import nebulosa.batch.processing.Step +import nebulosa.batch.processing.StepExecution +import nebulosa.batch.processing.StepResult +import java.time.Duration + +data class DelayStep(@JvmField val duration: Duration) : Step { + + @Volatile private var aborted = false + private val listeners = HashSet() + + fun registerListener(listener: DelayListener) { + listeners.add(listener) + } + + fun unregisterListener(listener: DelayListener) { + listeners.remove(listener) + } + + override fun execute(stepExecution: StepExecution): StepResult { + var remainingTime = duration + + if (remainingTime > Duration.ZERO) { + while (!aborted && remainingTime > Duration.ZERO) { + val waitTime = minOf(remainingTime, DELAY_INTERVAL) + + if (waitTime > Duration.ZERO) { + stepExecution.jobExecution.context[REMAINING_TIME] = remainingTime + stepExecution.jobExecution.context[WAIT_TIME] = waitTime + + val progress = (duration.toNanos() - remainingTime.toNanos()) / duration.toNanos().toDouble() + stepExecution.jobExecution.context[PROGRESS] = progress + + listeners.forEach { it.onDelayElapsed(stepExecution) } + Thread.sleep(waitTime.toMillis()) + remainingTime -= waitTime + } + } + + stepExecution.jobExecution.context[REMAINING_TIME] = Duration.ZERO + stepExecution.jobExecution.context[WAIT_TIME] = Duration.ZERO + + listeners.forEach { it.onDelayElapsed(stepExecution) } + } + + return StepResult.FINISHED + } + + override fun stop(mayInterruptIfRunning: Boolean) { + aborted = true + } + + companion object { + + const val REMAINING_TIME = "DELAY.REMAINING_TIME" + const val WAIT_TIME = "DELAY.WAIT_TIME" + const val PROGRESS = "DELAY.PROGRESS" + + @JvmField val DELAY_INTERVAL = Duration.ofMillis(500)!! + } +} diff --git a/nebulosa-batch-processing/src/test/kotlin/BatchProcessingTest.kt b/nebulosa-batch-processing/src/test/kotlin/BatchProcessingTest.kt index 1e90a01ca..aa56aca4a 100644 --- a/nebulosa-batch-processing/src/test/kotlin/BatchProcessingTest.kt +++ b/nebulosa-batch-processing/src/test/kotlin/BatchProcessingTest.kt @@ -3,7 +3,6 @@ import io.kotest.matchers.booleans.shouldBeTrue import io.kotest.matchers.longs.shouldBeInRange import io.kotest.matchers.shouldBe import nebulosa.batch.processing.* -import nebulosa.common.concurrency.DaemonThreadFactory import nebulosa.log.loggerFor import java.util.concurrent.Executors import kotlin.concurrent.thread @@ -11,7 +10,7 @@ import kotlin.concurrent.thread class BatchProcessingTest : StringSpec() { init { - val launcher = AsyncJobLauncher(Executors.newSingleThreadExecutor(DaemonThreadFactory)) + val launcher = AsyncJobLauncher(Executors.newSingleThreadExecutor()) "single" { val startedAt = System.currentTimeMillis() @@ -70,9 +69,10 @@ class BatchProcessingTest : StringSpec() { protected abstract fun compute(value: Double): Double - final override fun execute(jobExecution: JobExecution): StepResult { + final override fun execute(stepExecution: StepExecution): StepResult { var sleepCount = 0 + val jobExecution = stepExecution.jobExecution running = jobExecution.canContinue while (running && sleepCount++ < 100) { From 8ce5a20beab2c42ba48605f3b68a0f56e887fc2c Mon Sep 17 00:00:00 2001 From: tiagohm Date: Wed, 13 Dec 2023 00:27:49 -0300 Subject: [PATCH 28/87] [api]: Improve Batch Processing; Reimplement existing steps --- .../polar/darv/DARVPolarAlignmentEvent.kt | 10 +-- .../polar/darv/DARVPolarAlignmentExecutor.kt | 78 ++----------------- .../polar/darv/DARVPolarAlignmentFinished.kt | 12 +-- .../DARVPolarAlignmentGuidePulseElapsed.kt | 14 +--- .../DARVPolarAlignmentInitialPauseElapsed.kt | 22 +----- .../polar/darv/DARVPolarAlignmentJob.kt | 60 +++++++++++++- .../polar/darv/DARVPolarAlignmentStarted.kt | 14 +--- .../configurations/DataSourceConfiguration.kt | 24 +++--- .../nebulosa/api/cameras/CameraCaptureJob.kt | 21 ++--- .../nebulosa/api/guiding/WaitForSettleStep.kt | 4 +- .../nebulosa/api/sequencer/ObservableJob.kt | 45 +++++++++++ .../batch/processing/AsyncJobLauncher.kt | 77 +++++++----------- .../batch/processing/DefaultStepHandler.kt | 46 +++++++++++ .../nebulosa/batch/processing/FlowStep.kt | 4 +- .../nebulosa/batch/processing/JobExecution.kt | 1 + .../nebulosa/batch/processing/JobLauncher.kt | 22 +++++- .../batch/processing/SimpleFlowStep.kt | 10 +-- .../nebulosa/batch/processing/SimpleJob.kt | 18 +++-- .../batch/processing/SimpleSplitStep.kt | 10 +++ .../nebulosa/batch/processing/SplitStep.kt | 3 + .../nebulosa/batch/processing/StepChain.kt | 2 + .../nebulosa/batch/processing/StepHandler.kt | 6 ++ .../batch/processing/StepInterceptorChain.kt | 3 +- .../src/test/kotlin/BatchProcessingTest.kt | 45 ++++++++--- 24 files changed, 315 insertions(+), 236 deletions(-) create mode 100644 api/src/main/kotlin/nebulosa/api/sequencer/ObservableJob.kt create mode 100644 nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/DefaultStepHandler.kt create mode 100644 nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/SimpleSplitStep.kt create mode 100644 nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/SplitStep.kt create mode 100644 nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepHandler.kt 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 index 411c4abb7..1080be6e3 100644 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentEvent.kt +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentEvent.kt @@ -1,13 +1,11 @@ package nebulosa.api.alignment.polar.darv -import nebulosa.indi.device.camera.Camera -import nebulosa.indi.device.guide.GuideOutput +import nebulosa.api.services.MessageEvent +import nebulosa.batch.processing.JobExecution -sealed interface DARVPolarAlignmentEvent { +sealed interface DARVPolarAlignmentEvent : MessageEvent { - val camera: Camera - - val guideOutput: GuideOutput + val jobExecution: JobExecution val state: DARVPolarAlignmentState } diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentExecutor.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentExecutor.kt index 47285361a..2d0f2b03f 100644 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentExecutor.kt +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentExecutor.kt @@ -1,25 +1,15 @@ package nebulosa.api.alignment.polar.darv -import nebulosa.api.alignment.polar.darv.DARVPolarAlignmentState.BACKWARD -import nebulosa.api.alignment.polar.darv.DARVPolarAlignmentState.FORWARD -import nebulosa.api.cameras.CameraCaptureEvent -import nebulosa.api.cameras.CameraCaptureListener -import nebulosa.api.cameras.CameraExposureStep +import io.reactivex.rxjava3.functions.Consumer import nebulosa.api.cameras.CameraStartCaptureRequest -import nebulosa.api.guiding.GuidePulseListener -import nebulosa.api.sequencer.* import nebulosa.api.services.MessageService import nebulosa.batch.processing.JobExecution -import nebulosa.batch.processing.JobExecutionListener import nebulosa.batch.processing.JobLauncher -import nebulosa.batch.processing.StepExecution -import nebulosa.batch.processing.delay.DelayListener import nebulosa.indi.device.camera.Camera import nebulosa.indi.device.guide.GuideOutput import nebulosa.log.loggerFor import org.springframework.stereotype.Component import java.nio.file.Path -import java.util.* /** * @see Reference @@ -29,7 +19,7 @@ class DARVPolarAlignmentExecutor( private val jobLauncher: JobLauncher, private val messageService: MessageService, private val capturesPath: Path, -) : JobExecutionListener, CameraCaptureListener, GuidePulseListener, DelayListener { +) : Consumer { private val jobExecutions = HashMap, JobExecution>(1) @@ -61,67 +51,13 @@ class DARVPolarAlignmentExecutor( jobExecution.stop() } - override fun accept(event: SequenceTaskletEvent) { - if (event !is SequenceJobEvent) { - LOG.warn("unaccepted sequence task event: {}", event) - return - } - - val (camera, guideOutput, data) = sequenceJobWithId(event.jobExecution.jobId) ?: return - - val messageEvent = when (event) { - // Initial pulse event. - is DelayElapsed -> DARVPolarAlignmentInitialPauseElapsed(camera, guideOutput, event) - // Forward & backward guide pulse event. - is GuidePulseEvent -> { - val direction = event.tasklet.request.direction - val duration = event.tasklet.request.duration - val state = if ((direction == data.direction) != data.reversed) FORWARD else BACKWARD - DARVPolarAlignmentGuidePulseElapsed(camera, guideOutput, state, direction, duration, event.progress, event.jobExecution) - } - is CameraCaptureEvent -> event - else -> return - } - - messageService.sendMessage(messageEvent) - } - - override fun beforeJob(jobExecution: JobExecution) { - val (camera, guideOutput) = sequenceJobWithId(jobExecution.jobId) ?: return - messageService.sendMessage(DARVPolarAlignmentStarted(camera, guideOutput, jobExecution)) - } - - override fun afterJob(jobExecution: JobExecution) { - val (camera, guideOutput) = sequenceJobWithId(jobExecution.jobId) ?: return - messageService.sendMessage(DARVPolarAlignmentFinished(camera, guideOutput, jobExecution)) - } - - override fun onCaptureStarted(step: CameraExposureStep, jobExecution: JobExecution) { - TODO("Not yet implemented") - } - - override fun onExposureStarted(stepExecution: StepExecution) { - TODO("Not yet implemented") - } - - override fun onExposureElapsed(stepExecution: StepExecution) { - TODO("Not yet implemented") - } - - override fun onExposureFinished(stepExecution: StepExecution) { - TODO("Not yet implemented") - } - - override fun onCaptureFinished(step: CameraExposureStep, jobExecution: JobExecution) { - TODO("Not yet implemented") - } - - override fun onGuidePulseElapsed(stepExecution: StepExecution) { - TODO("Not yet implemented") + fun isRunning(camera: Camera, guideOutput: GuideOutput): Boolean { + val jobExecution = jobExecutions[camera to guideOutput] ?: return false + return !jobExecution.isDone } - override fun onDelayElapsed(stepExecution: StepExecution) { - TODO("Not yet implemented") + override fun accept(event: DARVPolarAlignmentEvent) { + messageService.sendMessage(event) } companion object { 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 index 3a3009469..bf8d21aba 100644 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentFinished.kt +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentFinished.kt @@ -1,16 +1,10 @@ package nebulosa.api.alignment.polar.darv -import com.fasterxml.jackson.annotation.JsonIgnore -import nebulosa.api.services.MessageEvent -import nebulosa.indi.device.camera.Camera -import nebulosa.indi.device.guide.GuideOutput -import org.springframework.batch.core.JobExecution +import nebulosa.batch.processing.JobExecution data class DARVPolarAlignmentFinished( - override val camera: Camera, - override val guideOutput: GuideOutput, - @JsonIgnore override val jobExecution: JobExecution, -) : MessageEvent, DARVPolarAlignmentEvent { + override val jobExecution: JobExecution, +) : DARVPolarAlignmentEvent { override val state = DARVPolarAlignmentState.IDLE 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 index b06cc2d0c..77cc58b4a 100644 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentGuidePulseElapsed.kt +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentGuidePulseElapsed.kt @@ -1,21 +1,11 @@ package nebulosa.api.alignment.polar.darv -import com.fasterxml.jackson.annotation.JsonIgnore import nebulosa.api.services.MessageEvent -import nebulosa.guiding.GuideDirection -import nebulosa.indi.device.camera.Camera -import nebulosa.indi.device.guide.GuideOutput -import org.springframework.batch.core.JobExecution -import java.time.Duration +import nebulosa.batch.processing.JobExecution data class DARVPolarAlignmentGuidePulseElapsed( - override val camera: Camera, - override val guideOutput: GuideOutput, + override val jobExecution: JobExecution, override val state: DARVPolarAlignmentState, - val direction: GuideDirection, - val remainingTime: Duration, - override val progress: Double, - @JsonIgnore override val jobExecution: JobExecution, ) : MessageEvent, DARVPolarAlignmentEvent { override val eventName = "DARV_POLAR_ALIGNMENT_UPDATED" diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentInitialPauseElapsed.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentInitialPauseElapsed.kt index bebe3d285..1ef851989 100644 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentInitialPauseElapsed.kt +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentInitialPauseElapsed.kt @@ -1,26 +1,8 @@ package nebulosa.api.alignment.polar.darv -import com.fasterxml.jackson.annotation.JsonIgnore -import nebulosa.api.sequencer.tasklets.delay.DelayEvent -import nebulosa.api.services.MessageEvent -import nebulosa.indi.device.camera.Camera -import nebulosa.indi.device.guide.GuideOutput -import org.springframework.batch.core.JobExecution -import java.time.Duration +import nebulosa.batch.processing.JobExecution -data class DARVPolarAlignmentInitialPauseElapsed( - override val camera: Camera, - override val guideOutput: GuideOutput, - val pauseTime: Duration, - val remainingTime: Duration, - override val progress: Double, - @JsonIgnore override val jobExecution: JobExecution, -) : MessageEvent, DARVPolarAlignmentEvent { - - constructor(camera: Camera, guideOutput: GuideOutput, delay: DelayEvent) : this( - camera, guideOutput, delay.tasklet.duration, - delay.remainingTime, delay.progress, delay.jobExecution - ) +data class DARVPolarAlignmentInitialPauseElapsed(override val jobExecution: JobExecution) : DARVPolarAlignmentEvent { override val state = DARVPolarAlignmentState.INITIAL_PAUSE diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentJob.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentJob.kt index 270256bd7..936f29db2 100644 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentJob.kt +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentJob.kt @@ -1,20 +1,30 @@ package nebulosa.api.alignment.polar.darv +import nebulosa.api.cameras.CameraCaptureListener import nebulosa.api.cameras.CameraExposureStep import nebulosa.api.cameras.CameraStartCaptureRequest +import nebulosa.api.guiding.GuidePulseListener import nebulosa.api.guiding.GuidePulseRequest import nebulosa.api.guiding.GuidePulseStep -import nebulosa.batch.processing.SimpleJob +import nebulosa.api.sequencer.ObservableJob +import nebulosa.batch.processing.JobExecution +import nebulosa.batch.processing.SimpleFlowStep +import nebulosa.batch.processing.SimpleSplitStep +import nebulosa.batch.processing.StepExecution +import nebulosa.batch.processing.delay.DelayListener import nebulosa.batch.processing.delay.DelayStep data class DARVPolarAlignmentJob( val request: DARVStart, val cameraRequest: CameraStartCaptureRequest, -) : SimpleJob() { +) : ObservableJob(), CameraCaptureListener, GuidePulseListener, DelayListener { - init { - val guideOutput = requireNotNull(request.guideOutput) + @JvmField val camera = requireNotNull(request.camera) + @JvmField val guideOutput = requireNotNull(request.guideOutput) + + override val id = "DARVPolarAlignment.Job.${System.currentTimeMillis()}" + init { val cameraExposureStep = CameraExposureStep(cameraRequest) cameraExposureStep.registerListener(this) @@ -31,5 +41,47 @@ data class DARVPolarAlignmentJob( val backwardGuidePulseRequest = GuidePulseRequest(guideOutput, direction.reversed, guidePulseDuration) val backwardGuidePulseStep = GuidePulseStep(backwardGuidePulseRequest) backwardGuidePulseStep.registerListener(this) + + val guideFlow = SimpleFlowStep(initialPauseDelayStep, forwardGuidePulseStep, backwardGuidePulseStep) + add(SimpleSplitStep(cameraExposureStep, guideFlow)) + } + + override fun beforeJob(jobExecution: JobExecution) { + onNext(DARVPolarAlignmentStarted(jobExecution)) + } + + override fun afterJob(jobExecution: JobExecution) { + onNext(DARVPolarAlignmentFinished(jobExecution)) + } + + override fun onCaptureStarted(step: CameraExposureStep, jobExecution: JobExecution) { + TODO("Not yet implemented") + } + + override fun onExposureStarted(stepExecution: StepExecution) { + TODO("Not yet implemented") + } + + override fun onExposureElapsed(stepExecution: StepExecution) { + TODO("Not yet implemented") + } + + override fun onExposureFinished(stepExecution: StepExecution) { + TODO("Not yet implemented") + } + + override fun onCaptureFinished(step: CameraExposureStep, jobExecution: JobExecution) { + TODO("Not yet implemented") + } + + override fun onGuidePulseElapsed(stepExecution: StepExecution) { + // val direction = event.tasklet.request.direction + // val duration = event.tasklet.request.duration + // val state = if ((direction == data.direction) != data.reversed) FORWARD else BACKWARD + onNext(DARVPolarAlignmentGuidePulseElapsed(stepExecution.jobExecution, DARVPolarAlignmentState.FORWARD)) + } + + override fun onDelayElapsed(stepExecution: StepExecution) { + onNext(DARVPolarAlignmentInitialPauseElapsed(stepExecution.jobExecution)) } } 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 index 0ba1998f5..6772ac8c2 100644 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentStarted.kt +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentStarted.kt @@ -1,18 +1,10 @@ package nebulosa.api.alignment.polar.darv -import com.fasterxml.jackson.annotation.JsonIgnore -import nebulosa.api.services.MessageEvent -import nebulosa.indi.device.camera.Camera -import nebulosa.indi.device.guide.GuideOutput -import org.springframework.batch.core.JobExecution +import nebulosa.batch.processing.JobExecution data class DARVPolarAlignmentStarted( - override val camera: Camera, - override val guideOutput: GuideOutput, - @JsonIgnore override val jobExecution: JobExecution, -) : MessageEvent, DARVPolarAlignmentEvent { - - override val progress = 0.0 + override val jobExecution: JobExecution, +) : DARVPolarAlignmentEvent { override val state = DARVPolarAlignmentState.INITIAL_PAUSE diff --git a/api/src/main/kotlin/nebulosa/api/beans/configurations/DataSourceConfiguration.kt b/api/src/main/kotlin/nebulosa/api/beans/configurations/DataSourceConfiguration.kt index 04aa77e5d..8d314a907 100644 --- a/api/src/main/kotlin/nebulosa/api/beans/configurations/DataSourceConfiguration.kt +++ b/api/src/main/kotlin/nebulosa/api/beans/configurations/DataSourceConfiguration.kt @@ -16,13 +16,13 @@ import javax.sql.DataSource @Configuration class DataSourceConfiguration { - @Value("\${spring.datasource.url}") private lateinit var mainDataSourceUrl: String + @Value("\${spring.datasource.url}") private lateinit var dataSourceUrl: String + @Bean @Primary - @Bean("mainDataSource") - fun mainDataSource(): DataSource { + fun dataSource(): DataSource { val config = HikariConfig() - config.jdbcUrl = mainDataSourceUrl + config.jdbcUrl = dataSourceUrl config.driverClassName = DRIVER_CLASS_NAME config.maximumPoolSize = 1 config.minimumIdle = 1 @@ -32,27 +32,27 @@ class DataSourceConfiguration { @Configuration @EnableJpaRepositories( basePackages = ["nebulosa.api"], - entityManagerFactoryRef = "mainEntityManagerFactory", - transactionManagerRef = "mainTransactionManager" + entityManagerFactoryRef = "entityManagerFactory", + transactionManagerRef = "transactionManager" ) class Main { + @Bean @Primary - @Bean("mainEntityManagerFactory") - fun mainEntityManagerFactory( + fun entityManagerFactory( builder: EntityManagerFactoryBuilder, dataSource: DataSource, ) = builder .dataSource(dataSource) .packages("nebulosa.api") - .persistenceUnit("mainPersistenceUnit") + .persistenceUnit("persistenceUnit") .build()!! + @Bean @Primary - @Bean("mainTransactionManager") - fun mainTransactionManager(mainEntityManagerFactory: EntityManagerFactory): PlatformTransactionManager { + fun transactionManager(entityManagerFactory: EntityManagerFactory): PlatformTransactionManager { // Fix "no transactions is in progress": https://stackoverflow.com/a/33397173 - return JpaTransactionManager(mainEntityManagerFactory) + return JpaTransactionManager(entityManagerFactory) } } diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureJob.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureJob.kt index ecbd625b3..ece56d059 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureJob.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureJob.kt @@ -2,9 +2,8 @@ package nebulosa.api.cameras import nebulosa.api.guiding.DitherAfterExposureStep import nebulosa.api.guiding.WaitForSettleStep -import nebulosa.batch.processing.SimpleFlowStep import nebulosa.batch.processing.SimpleJob -import nebulosa.batch.processing.Step +import nebulosa.batch.processing.SimpleSplitStep import nebulosa.batch.processing.delay.DelayStep import nebulosa.guiding.Guider @@ -18,27 +17,25 @@ data class CameraCaptureJob( override val id = "CameraCapture.Job.${System.currentTimeMillis()}" - override val steps = ArrayList() - init { if (cameraExposureStep is CameraExposureStep) { val waitForSettleStep = WaitForSettleStep(guider) val ditherStep = DitherAfterExposureStep(request.dither) val cameraDelayStep = DelayStep(request.exposureDelay) - val delayAndWaitForSettleStep = DelayAndWaitForSettleStep(listOf(cameraDelayStep, waitForSettleStep)) + val delayAndWaitForSettleStep = SimpleSplitStep(cameraDelayStep, waitForSettleStep) cameraDelayStep.registerListener(cameraExposureStep) - steps.add(waitForSettleStep) - steps.add(cameraExposureStep) + add(waitForSettleStep) + add(cameraExposureStep) repeat(request.exposureAmount - 1) { - steps.add(delayAndWaitForSettleStep) - steps.add(cameraExposureStep) - steps.add(ditherStep) + add(delayAndWaitForSettleStep) + add(cameraExposureStep) + add(ditherStep) } } else { - steps.add(cameraExposureStep) + add(cameraExposureStep) } } @@ -49,6 +46,4 @@ data class CameraCaptureJob( fun unregisterListener(listener: CameraCaptureListener) { cameraExposureStep.unregisterListener(listener) } - - data class DelayAndWaitForSettleStep(override val steps: Collection) : SimpleFlowStep() } diff --git a/api/src/main/kotlin/nebulosa/api/guiding/WaitForSettleStep.kt b/api/src/main/kotlin/nebulosa/api/guiding/WaitForSettleStep.kt index 39908b614..67aea4cca 100644 --- a/api/src/main/kotlin/nebulosa/api/guiding/WaitForSettleStep.kt +++ b/api/src/main/kotlin/nebulosa/api/guiding/WaitForSettleStep.kt @@ -1,13 +1,13 @@ package nebulosa.api.guiding -import nebulosa.batch.processing.JobExecution import nebulosa.batch.processing.Step +import nebulosa.batch.processing.StepExecution import nebulosa.batch.processing.StepResult import nebulosa.guiding.Guider data class WaitForSettleStep(private val guider: Guider) : Step { - override fun execute(jobExecution: JobExecution): StepResult { + override fun execute(stepExecution: StepExecution): StepResult { if (guider.isSettling) { guider.waitForSettle() } diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/ObservableJob.kt b/api/src/main/kotlin/nebulosa/api/sequencer/ObservableJob.kt new file mode 100644 index 000000000..3dd7e3976 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/sequencer/ObservableJob.kt @@ -0,0 +1,45 @@ +package nebulosa.api.sequencer + +import io.reactivex.rxjava3.core.Observable +import io.reactivex.rxjava3.core.ObservableSource +import io.reactivex.rxjava3.core.Observer +import io.reactivex.rxjava3.disposables.Disposable +import io.reactivex.rxjava3.functions.Consumer +import io.reactivex.rxjava3.subjects.PublishSubject +import nebulosa.batch.processing.SimpleJob +import java.io.Closeable + +abstract class ObservableJob : SimpleJob(), ObservableSource, Observer, Closeable { + + @JvmField protected val subject = PublishSubject.create() + + protected open fun Observable.transform() = this + + fun subscribe(onNext: Consumer): Disposable { + return subject.transform().subscribe(onNext) + } + + final override fun subscribe(observer: Observer) { + return subject.transform().subscribe(observer) + } + + final override fun onSubscribe(disposable: Disposable) { + subject.onSubscribe(disposable) + } + + final override fun onNext(event: T) { + subject.onNext(event) + } + + final override fun onError(e: Throwable) { + subject.onError(e) + } + + final override fun onComplete() { + subject.onComplete() + } + + final override fun close() { + onComplete() + } +} diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/AsyncJobLauncher.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/AsyncJobLauncher.kt index 3ee4f3322..69b40a221 100644 --- a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/AsyncJobLauncher.kt +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/AsyncJobLauncher.kt @@ -3,35 +3,48 @@ package nebulosa.batch.processing import java.time.LocalDateTime import java.util.concurrent.ExecutorService -open class AsyncJobLauncher(private val executor: ExecutorService) : JobLauncher, StepInterceptor { +open class AsyncJobLauncher(private val executor: ExecutorService) : JobLauncher { private val jobListeners = HashSet() private val stepListeners = HashSet() - private val stepInterceptors = HashSet() + private val mStepInterceptors = HashSet() private val jobs = LinkedHashMap() - fun registerJobListener(listener: JobExecutionListener) { + override val stepHandler: StepHandler = DefaultStepHandler + + override val stepInterceptors: Collection + get() = mStepInterceptors + + override fun registerJobListener(listener: JobExecutionListener) { jobListeners.add(listener) } - fun unregisterJobListener(listener: JobExecutionListener) { + override fun unregisterJobListener(listener: JobExecutionListener) { jobListeners.remove(listener) } - fun registerStepListener(listener: StepExecutionListener) { + override fun registerStepListener(listener: StepExecutionListener) { stepListeners.add(listener) } - fun unregisterStepListener(listener: StepExecutionListener) { + override fun unregisterStepListener(listener: StepExecutionListener) { stepListeners.remove(listener) } - fun registerStepInterceptor(interceptor: StepInterceptor) { - stepInterceptors.add(interceptor) + override fun registerStepInterceptor(interceptor: StepInterceptor) { + mStepInterceptors.add(interceptor) } - fun unregisterStepInterceptor(interceptor: StepInterceptor) { - stepInterceptors.remove(interceptor) + override fun unregisterStepInterceptor(interceptor: StepInterceptor) { + mStepInterceptors.remove(interceptor) + } + + override fun fireBeforeStep(stepExecution: StepExecution) { + stepListeners.forEach { it.beforeStep(stepExecution) } + } + + override fun fireAfterStep(stepExecution: StepExecution) { + stepListeners.forEach { it.afterStep(stepExecution) } } override val size @@ -54,7 +67,7 @@ open class AsyncJobLauncher(private val executor: ExecutorService) : JobLauncher } @Synchronized - override fun launch(job: Job): JobExecution { + override fun launch(job: Job, executionContext: ExecutionContext?): JobExecution { var jobExecution = jobs[job.id] if (jobExecution != null) { @@ -63,8 +76,7 @@ open class AsyncJobLauncher(private val executor: ExecutorService) : JobLauncher } } - val context = ExecutionContext() - jobExecution = JobExecution(job, context) + jobExecution = JobExecution(job, executionContext ?: ExecutionContext(), this) jobs[job.id] = jobExecution @@ -74,9 +86,6 @@ open class AsyncJobLauncher(private val executor: ExecutorService) : JobLauncher job.beforeJob(jobExecution) jobListeners.forEach { it.beforeJob(jobExecution) } - val interceptors = ArrayList(stepInterceptors) - interceptors.add(this) - val stepJobListeners = HashSet() try { @@ -89,23 +98,7 @@ open class AsyncJobLauncher(private val executor: ExecutorService) : JobLauncher } } - if (step is FlowStep) { - val flow = object : Step { - - override fun execute(stepExecution: StepExecution): StepResult { - step.toList().parallelStream().forEach { execute(interceptors, stepExecution) } - return step.execute(stepExecution) - } - - override fun stop(mayInterruptIfRunning: Boolean) { - step.stop(mayInterruptIfRunning) - } - } - - execute(interceptors, StepExecution(flow, jobExecution)) - } else { - execute(interceptors, StepExecution(step, jobExecution)) - } + stepHandler.handle(step, StepExecution(step, jobExecution)) } jobExecution.status = if (jobExecution.isStopping) BatchStatus.STOPPED else BatchStatus.COMPLETED @@ -128,22 +121,4 @@ open class AsyncJobLauncher(private val executor: ExecutorService) : JobLauncher override fun stop(mayInterruptIfRunning: Boolean) { jobs.forEach { it.value.stop(mayInterruptIfRunning) } } - - override fun intercept(chain: StepChain): StepResult { - return chain.stepExecution.step.execute(chain.stepExecution) - } - - private fun execute(interceptors: List, stepExecution: StepExecution) { - val chain = StepInterceptorChain(interceptors, stepExecution) - var status: RepeatStatus - - do { - stepListeners.forEach { it.beforeStep(stepExecution) } - val result = chain.proceed() - stepListeners.forEach { it.afterStep(stepExecution) } - status = result.get() - } while (status == RepeatStatus.CONTINUABLE) - - stepExecution.finishedAt = LocalDateTime.now() - } } diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/DefaultStepHandler.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/DefaultStepHandler.kt new file mode 100644 index 000000000..315155b66 --- /dev/null +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/DefaultStepHandler.kt @@ -0,0 +1,46 @@ +package nebulosa.batch.processing + +import java.time.LocalDateTime + +object DefaultStepHandler : StepHandler, StepInterceptor { + + override fun handle(step: Step, stepExecution: StepExecution): StepResult { + val jobLauncher = stepExecution.jobExecution.jobLauncher + + when (step) { + is SplitStep -> { + step.beforeStep(stepExecution) + step.parallelStream().forEach { jobLauncher.stepHandler.handle(it, stepExecution) } + step.afterStep(stepExecution) + } + is FlowStep -> { + step.beforeStep(stepExecution) + step.forEach { jobLauncher.stepHandler.handle(it, stepExecution) } + step.afterStep(stepExecution) + } + else -> { + val interceptors = ArrayList(jobLauncher.stepInterceptors.size + 1) + interceptors.addAll(jobLauncher.stepInterceptors) + interceptors.add(this) + + val chain = StepInterceptorChain(interceptors, step, stepExecution) + var status: RepeatStatus + + do { + jobLauncher.fireBeforeStep(stepExecution) + val result = chain.proceed() + jobLauncher.fireAfterStep(stepExecution) + status = result.get() + } while (status == RepeatStatus.CONTINUABLE) + + stepExecution.finishedAt = LocalDateTime.now() + } + } + + return StepResult.FINISHED + } + + override fun intercept(chain: StepChain): StepResult { + return chain.step.execute(chain.stepExecution) + } +} diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/FlowStep.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/FlowStep.kt index 5c62620bf..ae3f1a49f 100644 --- a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/FlowStep.kt +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/FlowStep.kt @@ -1,9 +1,9 @@ package nebulosa.batch.processing -interface FlowStep : Step, Iterable { +interface FlowStep : Step, StepExecutionListener, Collection { override fun execute(stepExecution: StepExecution): StepResult { - return StepResult.FINISHED + return stepExecution.jobExecution.jobLauncher.stepHandler.handle(this, stepExecution) } override fun stop(mayInterruptIfRunning: Boolean) { diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobExecution.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobExecution.kt index 1874f139c..50fd261fe 100644 --- a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobExecution.kt +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobExecution.kt @@ -8,6 +8,7 @@ import java.util.concurrent.TimeUnit data class JobExecution( val job: Job, val context: ExecutionContext, + val jobLauncher: JobLauncher, val startedAt: LocalDateTime = LocalDateTime.now(), var status: BatchStatus = BatchStatus.STARTING, var finishedAt: LocalDateTime? = null, diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobLauncher.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobLauncher.kt index ff54dc2c8..e71243239 100644 --- a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobLauncher.kt +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobLauncher.kt @@ -2,5 +2,25 @@ package nebulosa.batch.processing interface JobLauncher : Collection, Stoppable { - fun launch(job: Job): JobExecution + val stepHandler: StepHandler + + val stepInterceptors: Collection + + fun registerJobListener(listener: JobExecutionListener) + + fun unregisterJobListener(listener: JobExecutionListener) + + fun registerStepListener(listener: StepExecutionListener) + + fun unregisterStepListener(listener: StepExecutionListener) + + fun registerStepInterceptor(interceptor: StepInterceptor) + + fun unregisterStepInterceptor(interceptor: StepInterceptor) + + fun fireBeforeStep(stepExecution: StepExecution) + + fun fireAfterStep(stepExecution: StepExecution) + + fun launch(job: Job, executionContext: ExecutionContext? = null): JobExecution } diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/SimpleFlowStep.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/SimpleFlowStep.kt index ff2db4de0..7f45b613f 100644 --- a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/SimpleFlowStep.kt +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/SimpleFlowStep.kt @@ -1,10 +1,10 @@ package nebulosa.batch.processing -abstract class SimpleFlowStep : FlowStep { +open class SimpleFlowStep : FlowStep, ArrayList { - protected abstract val steps: Collection + constructor(initialCapacity: Int = 4) : super(initialCapacity) - override fun iterator(): Iterator { - return steps.iterator() - } + constructor(steps: Collection) : super(steps) + + constructor(vararg steps: Step) : this(steps.toList()) } diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/SimpleJob.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/SimpleJob.kt index 8464a2081..c3df9287d 100644 --- a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/SimpleJob.kt +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/SimpleJob.kt @@ -1,22 +1,26 @@ package nebulosa.batch.processing -abstract class SimpleJob : Job { +abstract class SimpleJob : Job, ArrayList { - @Volatile private var position = 0 + constructor(initialCapacity: Int = 4) : super(initialCapacity) + + constructor(steps: Collection) : super(steps) - protected abstract val steps: List + constructor(vararg steps: Step) : this(steps.toList()) + + @Volatile private var position = 0 override fun hasNext(jobExecution: JobExecution): Boolean { - return position < steps.size + return position < size } override fun next(jobExecution: JobExecution): Step { - return steps[position++] + return this[position++] } override fun stop(mayInterruptIfRunning: Boolean) { - if (position in steps.indices) { - steps[position].stop(mayInterruptIfRunning) + if (position in indices) { + this[position].stop(mayInterruptIfRunning) } } } diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/SimpleSplitStep.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/SimpleSplitStep.kt new file mode 100644 index 000000000..2b4dbd98a --- /dev/null +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/SimpleSplitStep.kt @@ -0,0 +1,10 @@ +package nebulosa.batch.processing + +open class SimpleSplitStep : SplitStep, ArrayList { + + constructor(initialCapacity: Int = 4) : super(initialCapacity) + + constructor(steps: Collection) : super(steps) + + constructor(vararg steps: Step) : this(steps.toList()) +} diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/SplitStep.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/SplitStep.kt new file mode 100644 index 000000000..a91bc8fa1 --- /dev/null +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/SplitStep.kt @@ -0,0 +1,3 @@ +package nebulosa.batch.processing + +interface SplitStep : FlowStep diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepChain.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepChain.kt index 1eb7052de..4e4d97cb4 100644 --- a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepChain.kt +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepChain.kt @@ -2,6 +2,8 @@ package nebulosa.batch.processing interface StepChain { + val step: Step + val stepExecution: StepExecution fun proceed(): StepResult diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepHandler.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepHandler.kt new file mode 100644 index 000000000..0e6ad8bd0 --- /dev/null +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepHandler.kt @@ -0,0 +1,6 @@ +package nebulosa.batch.processing + +interface StepHandler { + + fun handle(step: Step, stepExecution: StepExecution): StepResult +} diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepInterceptorChain.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepInterceptorChain.kt index b5eef3542..d1ed9a905 100644 --- a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepInterceptorChain.kt +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepInterceptorChain.kt @@ -2,12 +2,13 @@ package nebulosa.batch.processing data class StepInterceptorChain( private val interceptors: List, + override val step: Step, override val stepExecution: StepExecution, private val index: Int = 0, ) : StepChain { override fun proceed(): StepResult { - val next = StepInterceptorChain(interceptors, stepExecution, index + 1) + val next = StepInterceptorChain(interceptors, step, stepExecution, index + 1) val interceptor = interceptors[index] return interceptor.intercept(next) } diff --git a/nebulosa-batch-processing/src/test/kotlin/BatchProcessingTest.kt b/nebulosa-batch-processing/src/test/kotlin/BatchProcessingTest.kt index aa56aca4a..03808f381 100644 --- a/nebulosa-batch-processing/src/test/kotlin/BatchProcessingTest.kt +++ b/nebulosa-batch-processing/src/test/kotlin/BatchProcessingTest.kt @@ -26,13 +26,27 @@ class BatchProcessingTest : StringSpec() { jobExecution.context["VALUE"] shouldBe 2.0 (System.currentTimeMillis() - startedAt) shouldBeInRange (2000L..3000L) } - "flow" { + "split" { val startedAt = System.currentTimeMillis() - val jobExecution = launcher.launch(MathJob(listOf(MultipleSumStep()))) + val jobExecution = launcher.launch(MathJob(listOf(SplitSumStep()))) jobExecution.waitForCompletion() - jobExecution.context["VALUE"] shouldBe NUMBER_OF_PROCESSORS.toDouble() + jobExecution.context["VALUE"] shouldBe N.toDouble() (System.currentTimeMillis() - startedAt) shouldBeInRange (1000L..2000L) } + "flow" { + val startedAt = System.currentTimeMillis() + val jobExecution = launcher.launch(MathJob(listOf(FlowSumStep()))) + jobExecution.waitForCompletion() + jobExecution.context["VALUE"] shouldBe N.toDouble() + (System.currentTimeMillis() - startedAt) shouldBeInRange (N * 1000L..(N + 1) * 1000L) + } + "split flow" { + val startedAt = System.currentTimeMillis() + val jobExecution = launcher.launch(MathJob(listOf(SimpleSplitStep(FlowSumStep(), FlowSumStep())))) + jobExecution.waitForCompletion() + jobExecution.context["VALUE"] shouldBe (N * 2).toDouble() + (System.currentTimeMillis() - startedAt) shouldBeInRange (N * 1000L..(N + 1) * 1000L) + } "stop" { val startedAt = System.currentTimeMillis() val jobExecution = launcher.launch(MathJob((0..7).map { SumStep() })) @@ -51,10 +65,10 @@ class BatchProcessingTest : StringSpec() { } } - private data class MathJob( - override val steps: List, + private class MathJob( + steps: List, private val initialValue: Double = 0.0, - ) : SimpleJob() { + ) : SimpleJob(steps) { override val id = "Job.Math" @@ -106,14 +120,27 @@ class BatchProcessingTest : StringSpec() { } } - private class MultipleSumStep : SimpleFlowStep() { + private class SplitSumStep : SimpleSplitStep() { - override val steps = (0 until NUMBER_OF_PROCESSORS).map { SumStep() } + init { + repeat(N) { + add(SumStep()) + } + } + } + + private class FlowSumStep : SimpleFlowStep() { + + init { + repeat(N) { + add(SumStep()) + } + } } companion object { @JvmStatic private val LOG = loggerFor() - @JvmStatic private val NUMBER_OF_PROCESSORS = Runtime.getRuntime().availableProcessors() + @JvmStatic private val N = Runtime.getRuntime().availableProcessors() } } From f62174ced506277e272345d131770509103329cc Mon Sep 17 00:00:00 2001 From: tiagohm Date: Thu, 14 Dec 2023 00:56:14 -0300 Subject: [PATCH 29/87] [api][desktop]: Reimplement DARV using Batch Processing --- api/build.gradle.kts | 1 - .../polar/PolarAlignmentController.kt | 4 +- .../alignment/polar/PolarAlignmentService.kt | 12 +-- .../api/alignment/polar/darv/DARVEvent.kt | 22 +++++ .../api/alignment/polar/darv/DARVExecutor.kt | 73 ++++++++++++++ .../api/alignment/polar/darv/DARVFinished.kt | 18 ++++ .../polar/darv/DARVGuidePulseElapsed.kt | 19 ++++ .../polar/darv/DARVInitialPauseElapsed.kt | 18 ++++ .../api/alignment/polar/darv/DARVJob.kt | 98 +++++++++++++++++++ .../polar/darv/DARVPolarAlignmentEvent.kt | 11 --- .../polar/darv/DARVPolarAlignmentExecutor.kt | 67 ------------- .../polar/darv/DARVPolarAlignmentFinished.kt | 12 --- .../DARVPolarAlignmentGuidePulseElapsed.kt | 12 --- .../DARVPolarAlignmentInitialPauseElapsed.kt | 10 -- .../polar/darv/DARVPolarAlignmentJob.kt | 87 ---------------- .../polar/darv/DARVPolarAlignmentStarted.kt | 12 --- .../{DARVStart.kt => DARVStartRequest.kt} | 2 +- .../api/alignment/polar/darv/DARVStarted.kt | 19 ++++ ...ARVPolarAlignmentState.kt => DARVState.kt} | 2 +- .../beans/configurations/BeanConfiguration.kt | 7 +- .../api/cameras/CameraCaptureElapsed.kt | 3 +- .../api/cameras/CameraCaptureEvent.kt | 3 +- .../api/cameras/CameraCaptureExecutor.kt | 19 ++-- .../api/cameras/CameraCaptureFinished.kt | 4 +- .../api/cameras/CameraCaptureIsWaiting.kt | 3 +- .../nebulosa/api/cameras/CameraCaptureJob.kt | 6 +- .../api/cameras/CameraCaptureListener.kt | 10 +- .../api/cameras/CameraCaptureStarted.kt | 4 +- .../api/cameras/CameraExposureElapsed.kt | 3 +- .../api/cameras/CameraExposureEvent.kt | 10 +- .../api/cameras/CameraExposureFinished.kt | 5 +- .../api/cameras/CameraExposureStarted.kt | 3 +- .../api/cameras/CameraExposureStep.kt | 44 +++++---- .../api/cameras/CameraLoopExposureStep.kt | 10 +- .../api/cameras/CameraStartCaptureStep.kt | 4 +- .../api/guiding/GuidePulseListener.kt | 2 +- .../nebulosa/api/guiding/GuidePulseStep.kt | 16 ++- .../nebulosa/api/sequencer/ObservableJob.kt | 45 --------- .../app/alignment/alignment.component.html | 4 +- .../src/app/alignment/alignment.component.ts | 45 ++------- .../src/shared/services/electron.service.ts | 10 +- desktop/src/shared/types.ts | 16 ++- nebulosa-batch-processing/build.gradle.kts | 1 + .../batch/processing/AsyncJobLauncher.kt | 76 +++++++------- .../batch/processing/DefaultStepHandler.kt | 31 +++--- .../nebulosa/batch/processing/JobExecution.kt | 28 +++--- .../nebulosa/batch/processing/JobLauncher.kt | 18 ++-- .../{BatchStatus.kt => JobStatus.kt} | 2 +- .../batch/processing/PublishSubscribe.kt | 44 +++++++++ .../batch/processing/StepExecution.kt | 10 +- .../batch/processing/delay/DelayListener.kt | 8 -- .../batch/processing/delay/DelayStep.kt | 33 ++++--- .../processing/delay/DelayStepListener.kt | 8 ++ 53 files changed, 514 insertions(+), 520 deletions(-) create mode 100644 api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVEvent.kt create mode 100644 api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVExecutor.kt create mode 100644 api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVFinished.kt create mode 100644 api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVGuidePulseElapsed.kt create mode 100644 api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVInitialPauseElapsed.kt create mode 100644 api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVJob.kt delete mode 100644 api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentEvent.kt delete mode 100644 api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentExecutor.kt delete mode 100644 api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentFinished.kt delete mode 100644 api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentGuidePulseElapsed.kt delete mode 100644 api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentInitialPauseElapsed.kt delete mode 100644 api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentJob.kt delete mode 100644 api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentStarted.kt rename api/src/main/kotlin/nebulosa/api/alignment/polar/darv/{DARVStart.kt => DARVStartRequest.kt} (96%) create mode 100644 api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVStarted.kt rename api/src/main/kotlin/nebulosa/api/alignment/polar/darv/{DARVPolarAlignmentState.kt => DARVState.kt} (73%) delete mode 100644 api/src/main/kotlin/nebulosa/api/sequencer/ObservableJob.kt rename nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/{BatchStatus.kt => JobStatus.kt} (84%) create mode 100644 nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/PublishSubscribe.kt delete mode 100644 nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/delay/DelayListener.kt create mode 100644 nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/delay/DelayStepListener.kt diff --git a/api/build.gradle.kts b/api/build.gradle.kts index c358ae730..a3ea18d04 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -32,7 +32,6 @@ dependencies { implementation(libs.flyway) implementation(libs.okhttp) implementation(libs.oshi) - implementation(libs.rx) implementation(libs.sqlite) implementation(libs.hikari) implementation("org.springframework.boot:spring-boot-starter") diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/PolarAlignmentController.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/PolarAlignmentController.kt index 8ca0bb346..15eddb11b 100644 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/PolarAlignmentController.kt +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/PolarAlignmentController.kt @@ -1,6 +1,6 @@ package nebulosa.api.alignment.polar -import nebulosa.api.alignment.polar.darv.DARVStart +import nebulosa.api.alignment.polar.darv.DARVStartRequest import nebulosa.api.beans.annotations.EntityParam import nebulosa.indi.device.camera.Camera import nebulosa.indi.device.guide.GuideOutput @@ -18,7 +18,7 @@ class PolarAlignmentController( @PutMapping("darv/{camera}/{guideOutput}/start") fun darvStart( @EntityParam camera: Camera, @EntityParam guideOutput: GuideOutput, - @RequestBody body: DARVStart, + @RequestBody body: DARVStartRequest, ) { polarAlignmentService.darvStart(camera, guideOutput, body) } diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/PolarAlignmentService.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/PolarAlignmentService.kt index 7265a3d76..44e703b19 100644 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/PolarAlignmentService.kt +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/PolarAlignmentService.kt @@ -1,23 +1,23 @@ package nebulosa.api.alignment.polar -import nebulosa.api.alignment.polar.darv.DARVPolarAlignmentExecutor -import nebulosa.api.alignment.polar.darv.DARVStart +import nebulosa.api.alignment.polar.darv.DARVExecutor +import nebulosa.api.alignment.polar.darv.DARVStartRequest import nebulosa.indi.device.camera.Camera import nebulosa.indi.device.guide.GuideOutput import org.springframework.stereotype.Service @Service class PolarAlignmentService( - private val darvPolarAlignmentExecutor: DARVPolarAlignmentExecutor, + private val darvExecutor: DARVExecutor, ) { - fun darvStart(camera: Camera, guideOutput: GuideOutput, darvStart: DARVStart) { + fun darvStart(camera: Camera, guideOutput: GuideOutput, darvStartRequest: DARVStartRequest) { check(camera.connected) { "camera not connected" } check(guideOutput.connected) { "guide output not connected" } - darvPolarAlignmentExecutor.execute(darvStart.copy(camera = camera, guideOutput = guideOutput)) + darvExecutor.execute(darvStartRequest.copy(camera = camera, guideOutput = guideOutput)) } fun darvStop(camera: Camera, guideOutput: GuideOutput) { - darvPolarAlignmentExecutor.stop(camera, guideOutput) + darvExecutor.stop(camera, guideOutput) } } diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVEvent.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVEvent.kt new file mode 100644 index 000000000..a19ad880a --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVEvent.kt @@ -0,0 +1,22 @@ +package nebulosa.api.alignment.polar.darv + +import nebulosa.api.services.MessageEvent +import nebulosa.guiding.GuideDirection +import nebulosa.indi.device.camera.Camera +import nebulosa.indi.device.guide.GuideOutput +import java.time.Duration + +sealed interface DARVEvent : MessageEvent { + + val camera: Camera + + val guideOutput: GuideOutput + + val remainingTime: Duration + + val progress: Double + + val direction: GuideDirection? + + val state: DARVState +} diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVExecutor.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVExecutor.kt new file mode 100644 index 000000000..def1bc8cb --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVExecutor.kt @@ -0,0 +1,73 @@ +package nebulosa.api.alignment.polar.darv + +import io.reactivex.rxjava3.functions.Consumer +import nebulosa.api.services.MessageEvent +import nebulosa.api.services.MessageService +import nebulosa.batch.processing.JobExecution +import nebulosa.batch.processing.JobLauncher +import nebulosa.indi.device.camera.Camera +import nebulosa.indi.device.guide.GuideOutput +import nebulosa.log.loggerFor +import org.springframework.stereotype.Component +import java.util.* + +/** + * @see Reference + */ +@Component +class DARVExecutor( + private val jobLauncher: JobLauncher, + private val messageService: MessageService, +) : Consumer { + + private val jobExecutions = LinkedList() + + @Synchronized + fun execute(request: DARVStartRequest) { + val camera = requireNotNull(request.camera) + val guideOutput = requireNotNull(request.guideOutput) + + check(!isRunning(camera, guideOutput)) { "DARV job is already running" } + + LOG.info("starting DARV job. data={}", request) + + with(DARVJob(request)) { + subscribe(this@DARVExecutor) + val jobExecution = jobLauncher.launch(this) + jobExecutions.add(jobExecution) + } + } + + fun findJobExecution(camera: Camera, guideOutput: GuideOutput): JobExecution? { + for (i in jobExecutions.indices.reversed()) { + val jobExecution = jobExecutions[i] + val job = jobExecution.job as DARVJob + + if (!jobExecution.isDone && job.camera === camera && job.guideOutput === guideOutput) { + return jobExecution + } + } + + return null + } + + @Synchronized + fun stop(camera: Camera, guideOutput: GuideOutput) { + val jobExecution = findJobExecution(camera, guideOutput) ?: return + jobExecution.stop() + } + + fun isRunning(camera: Camera, guideOutput: GuideOutput): Boolean { + val jobExecution = findJobExecution(camera, guideOutput) ?: return false + return !jobExecution.isDone + } + + override fun accept(event: MessageEvent) { + messageService.sendMessage(event) + } + + companion object { + + @JvmStatic private val LOG = loggerFor() + } +} diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVFinished.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVFinished.kt new file mode 100644 index 000000000..d92ce9f15 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVFinished.kt @@ -0,0 +1,18 @@ +package nebulosa.api.alignment.polar.darv + +import nebulosa.indi.device.camera.Camera +import nebulosa.indi.device.guide.GuideOutput +import java.time.Duration + +data class DARVFinished( + override val camera: Camera, + override val guideOutput: GuideOutput, +) : DARVEvent { + + override val remainingTime = Duration.ZERO!! + override val progress = 0.0 + override val state = DARVState.IDLE + override val direction = null + + override val eventName = "DARV_POLAR_ALIGNMENT_ELAPSED" +} diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVGuidePulseElapsed.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVGuidePulseElapsed.kt new file mode 100644 index 000000000..f583371cf --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVGuidePulseElapsed.kt @@ -0,0 +1,19 @@ +package nebulosa.api.alignment.polar.darv + +import nebulosa.api.services.MessageEvent +import nebulosa.guiding.GuideDirection +import nebulosa.indi.device.camera.Camera +import nebulosa.indi.device.guide.GuideOutput +import java.time.Duration + +data class DARVGuidePulseElapsed( + override val camera: Camera, + override val guideOutput: GuideOutput, + override val remainingTime: Duration, + override val progress: Double, + override val direction: GuideDirection, + override val state: DARVState, +) : MessageEvent, DARVEvent { + + override val eventName = "DARV_POLAR_ALIGNMENT_ELAPSED" +} diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVInitialPauseElapsed.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVInitialPauseElapsed.kt new file mode 100644 index 000000000..da1a1ecc0 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVInitialPauseElapsed.kt @@ -0,0 +1,18 @@ +package nebulosa.api.alignment.polar.darv + +import nebulosa.indi.device.camera.Camera +import nebulosa.indi.device.guide.GuideOutput +import java.time.Duration + +data class DARVInitialPauseElapsed( + override val camera: Camera, + override val guideOutput: GuideOutput, + override val remainingTime: Duration, + override val progress: Double, +) : DARVEvent { + + override val state = DARVState.INITIAL_PAUSE + override val direction = null + + override val eventName = "DARV_POLAR_ALIGNMENT_ELAPSED" +} diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVJob.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVJob.kt new file mode 100644 index 000000000..44f55e476 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVJob.kt @@ -0,0 +1,98 @@ +package nebulosa.api.alignment.polar.darv + +import io.reactivex.rxjava3.subjects.PublishSubject +import nebulosa.api.cameras.CameraCaptureListener +import nebulosa.api.cameras.CameraExposureFinished +import nebulosa.api.cameras.CameraExposureStep +import nebulosa.api.cameras.CameraStartCaptureRequest +import nebulosa.api.guiding.GuidePulseListener +import nebulosa.api.guiding.GuidePulseRequest +import nebulosa.api.guiding.GuidePulseStep +import nebulosa.api.services.MessageEvent +import nebulosa.batch.processing.* +import nebulosa.batch.processing.delay.DelayStep +import nebulosa.batch.processing.delay.DelayStepListener +import nebulosa.common.concurrency.Incrementer +import java.nio.file.Files +import java.nio.file.Path +import java.time.Duration + +data class DARVJob( + val request: DARVStartRequest, +) : SimpleJob(), PublishSubscribe, CameraCaptureListener, GuidePulseListener, DelayStepListener { + + @JvmField val camera = requireNotNull(request.camera) + @JvmField val guideOutput = requireNotNull(request.guideOutput) + @JvmField val direction = if (request.reversed) request.direction.reversed else request.direction + + @JvmField val cameraRequest = CameraStartCaptureRequest( + camera = camera, + exposureTime = request.exposureTime + request.initialPause, + savePath = Files.createTempDirectory("darv"), + ) + + override val id = "DARV.Job.${ID.increment()}" + + override val subject = PublishSubject.create() + + init { + val cameraExposureStep = CameraExposureStep(cameraRequest) + cameraExposureStep.registerCameraCaptureListener(this) + + val initialPauseDelayStep = DelayStep(request.initialPause) + initialPauseDelayStep.registerDelayStepListener(this) + + val guidePulseDuration = request.exposureTime.dividedBy(2L) + val forwardGuidePulseRequest = GuidePulseRequest(guideOutput, direction, guidePulseDuration) + val forwardGuidePulseStep = GuidePulseStep(forwardGuidePulseRequest) + forwardGuidePulseStep.registerGuidePulseListener(this) + + val backwardGuidePulseRequest = GuidePulseRequest(guideOutput, direction.reversed, guidePulseDuration) + val backwardGuidePulseStep = GuidePulseStep(backwardGuidePulseRequest) + backwardGuidePulseStep.registerGuidePulseListener(this) + + val guideFlow = SimpleFlowStep(initialPauseDelayStep, forwardGuidePulseStep, backwardGuidePulseStep) + add(SimpleSplitStep(cameraExposureStep, guideFlow)) + } + + override fun beforeJob(jobExecution: JobExecution) { + val job = jobExecution.job as DARVJob + onNext(DARVStarted(job.camera, job.guideOutput, job.request.initialPause, job.direction)) + } + + override fun afterJob(jobExecution: JobExecution) { + val job = jobExecution.job as DARVJob + onNext(DARVFinished(job.camera, job.guideOutput)) + } + + override fun onExposureFinished(step: CameraExposureStep, stepExecution: StepExecution) { + val savePath = stepExecution.context[CameraExposureStep.SAVE_PATH] as Path + onNext(CameraExposureFinished(step.camera, 1.0, savePath)) + } + + override fun onGuidePulseElapsed(step: GuidePulseStep, stepExecution: StepExecution) { + val job = stepExecution.jobExecution.job as DARVJob + val direction = step.request.direction + val remainingTime = stepExecution.context[DelayStep.REMAINING_TIME] as Duration + val progress = stepExecution.context[DelayStep.PROGRESS] as Double + val state = if (direction == job.direction) DARVState.FORWARD else DARVState.BACKWARD + onNext(DARVGuidePulseElapsed(job.camera, job.guideOutput, remainingTime, progress, direction, state)) + } + + override fun onDelayElapsed(step: DelayStep, stepExecution: StepExecution) { + val job = stepExecution.jobExecution.job as DARVJob + val remainingTime = stepExecution.context[DelayStep.REMAINING_TIME] as Duration + val progress = stepExecution.context[DelayStep.PROGRESS] as Double + onNext(DARVInitialPauseElapsed(job.camera, job.guideOutput, remainingTime, progress)) + } + + override fun stop(mayInterruptIfRunning: Boolean) { + super.stop(mayInterruptIfRunning) + close() + } + + companion object { + + @JvmStatic private val ID = Incrementer() + } +} diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentEvent.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentEvent.kt deleted file mode 100644 index 1080be6e3..000000000 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentEvent.kt +++ /dev/null @@ -1,11 +0,0 @@ -package nebulosa.api.alignment.polar.darv - -import nebulosa.api.services.MessageEvent -import nebulosa.batch.processing.JobExecution - -sealed interface DARVPolarAlignmentEvent : MessageEvent { - - val jobExecution: JobExecution - - val state: DARVPolarAlignmentState -} diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentExecutor.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentExecutor.kt deleted file mode 100644 index 2d0f2b03f..000000000 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentExecutor.kt +++ /dev/null @@ -1,67 +0,0 @@ -package nebulosa.api.alignment.polar.darv - -import io.reactivex.rxjava3.functions.Consumer -import nebulosa.api.cameras.CameraStartCaptureRequest -import nebulosa.api.services.MessageService -import nebulosa.batch.processing.JobExecution -import nebulosa.batch.processing.JobLauncher -import nebulosa.indi.device.camera.Camera -import nebulosa.indi.device.guide.GuideOutput -import nebulosa.log.loggerFor -import org.springframework.stereotype.Component -import java.nio.file.Path - -/** - * @see Reference - */ -@Component -class DARVPolarAlignmentExecutor( - private val jobLauncher: JobLauncher, - private val messageService: MessageService, - private val capturesPath: Path, -) : Consumer { - - private val jobExecutions = HashMap, JobExecution>(1) - - @Synchronized - fun execute(request: DARVStart) { - val camera = requireNotNull(request.camera) - val guideOutput = requireNotNull(request.guideOutput) - - if (isRunning(camera, guideOutput)) { - throw IllegalStateException("DARV Polar Alignment job is already running") - } - - LOG.info("starting DARV polar alignment. data={}", request) - - val cameraRequest = CameraStartCaptureRequest( - camera = camera, - exposureTime = request.exposureTime + request.initialPause, - savePath = Path.of("$capturesPath", "${camera.name}-DARV.fits") - ) - - val darvJob = DARVPolarAlignmentJob(request, cameraRequest) - val jobExecution = jobLauncher.launch(darvJob) - jobExecutions[camera to guideOutput] = jobExecution - } - - @Synchronized - fun stop(camera: Camera, guideOutput: GuideOutput) { - val jobExecution = jobExecutions[camera to guideOutput] ?: return - jobExecution.stop() - } - - fun isRunning(camera: Camera, guideOutput: GuideOutput): Boolean { - val jobExecution = jobExecutions[camera to guideOutput] ?: return false - return !jobExecution.isDone - } - - override fun accept(event: DARVPolarAlignmentEvent) { - messageService.sendMessage(event) - } - - companion object { - - @JvmStatic private val LOG = loggerFor() - } -} diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentFinished.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentFinished.kt deleted file mode 100644 index bf8d21aba..000000000 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentFinished.kt +++ /dev/null @@ -1,12 +0,0 @@ -package nebulosa.api.alignment.polar.darv - -import nebulosa.batch.processing.JobExecution - -data class DARVPolarAlignmentFinished( - override val jobExecution: JobExecution, -) : DARVPolarAlignmentEvent { - - override val state = DARVPolarAlignmentState.IDLE - - override val eventName = "DARV_POLAR_ALIGNMENT_FINISHED" -} diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentGuidePulseElapsed.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentGuidePulseElapsed.kt deleted file mode 100644 index 77cc58b4a..000000000 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentGuidePulseElapsed.kt +++ /dev/null @@ -1,12 +0,0 @@ -package nebulosa.api.alignment.polar.darv - -import nebulosa.api.services.MessageEvent -import nebulosa.batch.processing.JobExecution - -data class DARVPolarAlignmentGuidePulseElapsed( - override val jobExecution: JobExecution, - override val state: DARVPolarAlignmentState, -) : MessageEvent, DARVPolarAlignmentEvent { - - override val eventName = "DARV_POLAR_ALIGNMENT_UPDATED" -} diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentInitialPauseElapsed.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentInitialPauseElapsed.kt deleted file mode 100644 index 1ef851989..000000000 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentInitialPauseElapsed.kt +++ /dev/null @@ -1,10 +0,0 @@ -package nebulosa.api.alignment.polar.darv - -import nebulosa.batch.processing.JobExecution - -data class DARVPolarAlignmentInitialPauseElapsed(override val jobExecution: JobExecution) : DARVPolarAlignmentEvent { - - override val state = DARVPolarAlignmentState.INITIAL_PAUSE - - override val eventName = "DARV_POLAR_ALIGNMENT_UPDATED" -} diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentJob.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentJob.kt deleted file mode 100644 index 936f29db2..000000000 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentJob.kt +++ /dev/null @@ -1,87 +0,0 @@ -package nebulosa.api.alignment.polar.darv - -import nebulosa.api.cameras.CameraCaptureListener -import nebulosa.api.cameras.CameraExposureStep -import nebulosa.api.cameras.CameraStartCaptureRequest -import nebulosa.api.guiding.GuidePulseListener -import nebulosa.api.guiding.GuidePulseRequest -import nebulosa.api.guiding.GuidePulseStep -import nebulosa.api.sequencer.ObservableJob -import nebulosa.batch.processing.JobExecution -import nebulosa.batch.processing.SimpleFlowStep -import nebulosa.batch.processing.SimpleSplitStep -import nebulosa.batch.processing.StepExecution -import nebulosa.batch.processing.delay.DelayListener -import nebulosa.batch.processing.delay.DelayStep - -data class DARVPolarAlignmentJob( - val request: DARVStart, - val cameraRequest: CameraStartCaptureRequest, -) : ObservableJob(), CameraCaptureListener, GuidePulseListener, DelayListener { - - @JvmField val camera = requireNotNull(request.camera) - @JvmField val guideOutput = requireNotNull(request.guideOutput) - - override val id = "DARVPolarAlignment.Job.${System.currentTimeMillis()}" - - init { - val cameraExposureStep = CameraExposureStep(cameraRequest) - cameraExposureStep.registerListener(this) - - val guidePulseDuration = request.exposureTime.dividedBy(2L) - val initialPauseDelayStep = DelayStep(request.initialPause) - initialPauseDelayStep.registerListener(this) - - val direction = if (request.reversed) request.direction.reversed else request.direction - - val forwardGuidePulseRequest = GuidePulseRequest(guideOutput, direction, guidePulseDuration) - val forwardGuidePulseStep = GuidePulseStep(forwardGuidePulseRequest) - forwardGuidePulseStep.registerListener(this) - - val backwardGuidePulseRequest = GuidePulseRequest(guideOutput, direction.reversed, guidePulseDuration) - val backwardGuidePulseStep = GuidePulseStep(backwardGuidePulseRequest) - backwardGuidePulseStep.registerListener(this) - - val guideFlow = SimpleFlowStep(initialPauseDelayStep, forwardGuidePulseStep, backwardGuidePulseStep) - add(SimpleSplitStep(cameraExposureStep, guideFlow)) - } - - override fun beforeJob(jobExecution: JobExecution) { - onNext(DARVPolarAlignmentStarted(jobExecution)) - } - - override fun afterJob(jobExecution: JobExecution) { - onNext(DARVPolarAlignmentFinished(jobExecution)) - } - - override fun onCaptureStarted(step: CameraExposureStep, jobExecution: JobExecution) { - TODO("Not yet implemented") - } - - override fun onExposureStarted(stepExecution: StepExecution) { - TODO("Not yet implemented") - } - - override fun onExposureElapsed(stepExecution: StepExecution) { - TODO("Not yet implemented") - } - - override fun onExposureFinished(stepExecution: StepExecution) { - TODO("Not yet implemented") - } - - override fun onCaptureFinished(step: CameraExposureStep, jobExecution: JobExecution) { - TODO("Not yet implemented") - } - - override fun onGuidePulseElapsed(stepExecution: StepExecution) { - // val direction = event.tasklet.request.direction - // val duration = event.tasklet.request.duration - // val state = if ((direction == data.direction) != data.reversed) FORWARD else BACKWARD - onNext(DARVPolarAlignmentGuidePulseElapsed(stepExecution.jobExecution, DARVPolarAlignmentState.FORWARD)) - } - - override fun onDelayElapsed(stepExecution: StepExecution) { - onNext(DARVPolarAlignmentInitialPauseElapsed(stepExecution.jobExecution)) - } -} diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentStarted.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentStarted.kt deleted file mode 100644 index 6772ac8c2..000000000 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentStarted.kt +++ /dev/null @@ -1,12 +0,0 @@ -package nebulosa.api.alignment.polar.darv - -import nebulosa.batch.processing.JobExecution - -data class DARVPolarAlignmentStarted( - override val jobExecution: JobExecution, -) : DARVPolarAlignmentEvent { - - override val state = DARVPolarAlignmentState.INITIAL_PAUSE - - override val eventName = "DARV_POLAR_ALIGNMENT_STARTED" -} diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVStart.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVStartRequest.kt similarity index 96% rename from api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVStart.kt rename to api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVStartRequest.kt index cb5580384..eea8091ab 100644 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVStart.kt +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVStartRequest.kt @@ -8,7 +8,7 @@ import org.hibernate.validator.constraints.time.DurationMax import org.hibernate.validator.constraints.time.DurationMin import java.time.Duration -data class DARVStart( +data class DARVStartRequest( @JsonIgnore val camera: Camera? = null, @JsonIgnore val guideOutput: GuideOutput? = null, @field:DurationMin(seconds = 1) @field:DurationMax(seconds = 600) val exposureTime: Duration = Duration.ZERO, diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVStarted.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVStarted.kt new file mode 100644 index 000000000..feaab9da0 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVStarted.kt @@ -0,0 +1,19 @@ +package nebulosa.api.alignment.polar.darv + +import nebulosa.guiding.GuideDirection +import nebulosa.indi.device.camera.Camera +import nebulosa.indi.device.guide.GuideOutput +import java.time.Duration + +data class DARVStarted( + override val camera: Camera, + override val guideOutput: GuideOutput, + override val remainingTime: Duration, + override val direction: GuideDirection, +) : DARVEvent { + + override val progress = 0.0 + override val state = DARVState.INITIAL_PAUSE + + override val eventName = "DARV_POLAR_ALIGNMENT_ELAPSED" +} diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentState.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVState.kt similarity index 73% rename from api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentState.kt rename to api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVState.kt index 61ed4e749..d8721cc45 100644 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVPolarAlignmentState.kt +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVState.kt @@ -1,6 +1,6 @@ package nebulosa.api.alignment.polar.darv -enum class DARVPolarAlignmentState { +enum class DARVState { IDLE, INITIAL_PAUSE, FORWARD, diff --git a/api/src/main/kotlin/nebulosa/api/beans/configurations/BeanConfiguration.kt b/api/src/main/kotlin/nebulosa/api/beans/configurations/BeanConfiguration.kt index d426ed229..81108f9ee 100644 --- a/api/src/main/kotlin/nebulosa/api/beans/configurations/BeanConfiguration.kt +++ b/api/src/main/kotlin/nebulosa/api/beans/configurations/BeanConfiguration.kt @@ -6,7 +6,6 @@ import com.fasterxml.jackson.databind.module.SimpleModule import com.fasterxml.jackson.databind.ser.std.StdSerializer import com.fasterxml.jackson.module.kotlin.kotlinModule import nebulosa.batch.processing.AsyncJobLauncher -import nebulosa.common.concurrency.DaemonThreadFactory import nebulosa.common.json.PathDeserializer import nebulosa.common.json.PathSerializer import nebulosa.guiding.Guider @@ -31,7 +30,6 @@ import org.springframework.web.method.support.HandlerMethodArgumentResolver import org.springframework.web.servlet.config.annotation.CorsRegistry import org.springframework.web.servlet.config.annotation.WebMvcConfigurer import java.nio.file.Path -import java.util.concurrent.Executors import java.util.concurrent.TimeUnit import kotlin.io.path.createDirectories @@ -102,8 +100,7 @@ class BeanConfiguration { @Bean fun threadPoolTaskExecutor(): ThreadPoolTaskExecutor { val taskExecutor = ThreadPoolTaskExecutor() - taskExecutor.corePoolSize = 8 - taskExecutor.maxPoolSize = 32 + taskExecutor.corePoolSize = 32 taskExecutor.initialize() return taskExecutor } @@ -125,7 +122,7 @@ class BeanConfiguration { fun phd2Guider(phd2Client: PHD2Client): Guider = PHD2Guider(phd2Client) @Bean - fun asyncJobLauncher() = AsyncJobLauncher(Executors.newCachedThreadPool(DaemonThreadFactory)) + fun asyncJobLauncher(threadPoolTaskExecutor: ThreadPoolTaskExecutor) = AsyncJobLauncher(threadPoolTaskExecutor) @Bean fun webMvcConfigurer( diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureElapsed.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureElapsed.kt index 099fdcc31..b5d2fc679 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureElapsed.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureElapsed.kt @@ -1,11 +1,10 @@ package nebulosa.api.cameras -import nebulosa.batch.processing.JobExecution import nebulosa.indi.device.camera.Camera data class CameraCaptureElapsed( override val camera: Camera, - override val jobExecution: JobExecution, + override val progress: Double, ) : CameraCaptureEvent { override val eventName = "CAMERA_CAPTURE_ELAPSED" diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureEvent.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureEvent.kt index d9f205957..a03905c7d 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureEvent.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureEvent.kt @@ -1,12 +1,11 @@ package nebulosa.api.cameras import nebulosa.api.services.MessageEvent -import nebulosa.batch.processing.JobExecution import nebulosa.indi.device.camera.Camera sealed interface CameraCaptureEvent : MessageEvent { val camera: Camera - val jobExecution: JobExecution + val progress: Double } diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureExecutor.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureExecutor.kt index 99b94aad1..db4d050a8 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureExecutor.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureExecutor.kt @@ -44,26 +44,23 @@ class CameraCaptureExecutor( } override fun onCaptureStarted(step: CameraExposureStep, jobExecution: JobExecution) { - messageService.sendMessage(CameraCaptureStarted(step.request.camera!!, jobExecution)) + // TODO: messageService.sendMessage(CameraCaptureStarted(step.request.camera!!, jobExecution)) } - override fun onExposureStarted(stepExecution: StepExecution) { - val step = stepExecution.step as CameraExposureStep - messageService.sendMessage(CameraExposureStarted(step.request.camera!!, stepExecution)) + override fun onExposureStarted(step: CameraExposureStep, stepExecution: StepExecution) { + // TODO: messageService.sendMessage(CameraExposureStarted(step.request.camera!!, stepExecution)) } - override fun onExposureElapsed(stepExecution: StepExecution) { - val step = stepExecution.step as CameraExposureStep - messageService.sendMessage(CameraExposureElapsed(step.request.camera!!, stepExecution)) + override fun onExposureElapsed(step: CameraExposureStep, stepExecution: StepExecution) { + // TODO: messageService.sendMessage(CameraExposureElapsed(step.request.camera!!, stepExecution)) } - override fun onExposureFinished(stepExecution: StepExecution) { - val step = stepExecution.step as CameraExposureStep - messageService.sendMessage(CameraExposureFinished(step.request.camera!!, stepExecution)) + override fun onExposureFinished(step: CameraExposureStep, stepExecution: StepExecution) { + // TODO: messageService.sendMessage(CameraExposureFinished(step.request.camera!!, stepExecution)) } override fun onCaptureFinished(step: CameraExposureStep, jobExecution: JobExecution) { - messageService.sendMessage(CameraCaptureFinished(step.request.camera!!, jobExecution)) + // TODO: messageService.sendMessage(CameraCaptureFinished(step.request.camera!!, jobExecution)) } // TODO: CameraCaptureIsWaiting diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureFinished.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureFinished.kt index b8626ae72..658e8ab3c 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureFinished.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureFinished.kt @@ -1,12 +1,12 @@ package nebulosa.api.cameras -import nebulosa.batch.processing.JobExecution import nebulosa.indi.device.camera.Camera data class CameraCaptureFinished( override val camera: Camera, - override val jobExecution: JobExecution, ) : CameraCaptureEvent { + override val progress = 1.0 + override val eventName = "CAMERA_CAPTURE_FINISHED" } diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureIsWaiting.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureIsWaiting.kt index 1bd01c0bd..c7f6f700a 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureIsWaiting.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureIsWaiting.kt @@ -1,11 +1,10 @@ package nebulosa.api.cameras -import nebulosa.batch.processing.JobExecution import nebulosa.indi.device.camera.Camera data class CameraCaptureIsWaiting( override val camera: Camera, - override val jobExecution: JobExecution, + override val progress: Double, ) : CameraCaptureEvent { override val eventName = "CAMERA_CAPTURE_WAITING" diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureJob.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureJob.kt index ece56d059..b21defcd1 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureJob.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureJob.kt @@ -24,7 +24,7 @@ data class CameraCaptureJob( val cameraDelayStep = DelayStep(request.exposureDelay) val delayAndWaitForSettleStep = SimpleSplitStep(cameraDelayStep, waitForSettleStep) - cameraDelayStep.registerListener(cameraExposureStep) + cameraDelayStep.registerDelayStepListener(cameraExposureStep) add(waitForSettleStep) add(cameraExposureStep) @@ -40,10 +40,10 @@ data class CameraCaptureJob( } fun registerListener(listener: CameraCaptureListener) { - cameraExposureStep.registerListener(listener) + cameraExposureStep.registerCameraCaptureListener(listener) } fun unregisterListener(listener: CameraCaptureListener) { - cameraExposureStep.unregisterListener(listener) + cameraExposureStep.unregisterCameraCaptureListener(listener) } } diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureListener.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureListener.kt index 523d559bc..3b685d6b4 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureListener.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureListener.kt @@ -5,13 +5,13 @@ import nebulosa.batch.processing.StepExecution interface CameraCaptureListener { - fun onCaptureStarted(step: CameraExposureStep, jobExecution: JobExecution) + fun onCaptureStarted(step: CameraExposureStep, jobExecution: JobExecution) = Unit - fun onExposureStarted(stepExecution: StepExecution) + fun onExposureStarted(step: CameraExposureStep, stepExecution: StepExecution) = Unit - fun onExposureElapsed(stepExecution: StepExecution) + fun onExposureElapsed(step: CameraExposureStep, stepExecution: StepExecution) = Unit - fun onExposureFinished(stepExecution: StepExecution) + fun onExposureFinished(step: CameraExposureStep, stepExecution: StepExecution) = Unit - fun onCaptureFinished(step: CameraExposureStep, jobExecution: JobExecution) + fun onCaptureFinished(step: CameraExposureStep, jobExecution: JobExecution) = Unit } diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureStarted.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureStarted.kt index bfc2791de..bb95a9af8 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureStarted.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureStarted.kt @@ -1,12 +1,12 @@ package nebulosa.api.cameras -import nebulosa.batch.processing.JobExecution import nebulosa.indi.device.camera.Camera data class CameraCaptureStarted( override val camera: Camera, - override val jobExecution: JobExecution, ) : CameraCaptureEvent { + override val progress = 0.0 + override val eventName = "CAMERA_CAPTURE_STARTED" } diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureElapsed.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureElapsed.kt index 0fc98bfb5..30ba54c9e 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureElapsed.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureElapsed.kt @@ -1,11 +1,10 @@ package nebulosa.api.cameras -import nebulosa.batch.processing.StepExecution import nebulosa.indi.device.camera.Camera data class CameraExposureElapsed( override val camera: Camera, - override val stepExecution: StepExecution, + override val progress: Double, ) : CameraExposureEvent { override val eventName = "CAMERA_EXPOSURE_ELAPSED" diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureEvent.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureEvent.kt index 650de147c..b2b004111 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureEvent.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureEvent.kt @@ -1,11 +1,3 @@ package nebulosa.api.cameras -import nebulosa.batch.processing.StepExecution - -sealed interface CameraExposureEvent : CameraCaptureEvent { - - val stepExecution: StepExecution - - override val jobExecution - get() = stepExecution.jobExecution -} +sealed interface CameraExposureEvent : CameraCaptureEvent diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureFinished.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureFinished.kt index 8daf1298e..793bb4dd8 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureFinished.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureFinished.kt @@ -1,11 +1,12 @@ package nebulosa.api.cameras -import nebulosa.batch.processing.StepExecution import nebulosa.indi.device.camera.Camera +import java.nio.file.Path data class CameraExposureFinished( override val camera: Camera, - override val stepExecution: StepExecution, + override val progress: Double, + val savePath: Path, ) : CameraExposureEvent { 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 e9024cd69..fd70ed728 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureStarted.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureStarted.kt @@ -1,11 +1,10 @@ package nebulosa.api.cameras -import nebulosa.batch.processing.StepExecution import nebulosa.indi.device.camera.Camera data class CameraExposureStarted( override val camera: Camera, - override val stepExecution: StepExecution, + override val progress: Double, ) : CameraExposureEvent { override val eventName = "CAMERA_EXPOSURE_STARTED" diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureStep.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureStep.kt index a3aaabc28..785de82bc 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureStep.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureStep.kt @@ -3,8 +3,8 @@ package nebulosa.api.cameras import nebulosa.batch.processing.JobExecution import nebulosa.batch.processing.StepExecution import nebulosa.batch.processing.StepResult -import nebulosa.batch.processing.delay.DelayListener import nebulosa.batch.processing.delay.DelayStep +import nebulosa.batch.processing.delay.DelayStepListener import nebulosa.common.concurrency.CountUpDownLatch import nebulosa.indi.device.camera.* import nebulosa.io.transferAndClose @@ -20,7 +20,9 @@ import java.time.format.DateTimeFormatter import kotlin.io.path.createParentDirectories import kotlin.io.path.outputStream -data class CameraExposureStep(override val request: CameraStartCaptureRequest) : CameraStartCaptureStep, DelayListener { +data class CameraExposureStep(override val request: CameraStartCaptureRequest) : CameraStartCaptureStep, DelayStepListener { + + @JvmField val camera = requireNotNull(request.camera) private val latch = CountUpDownLatch() private val listeners = HashSet() @@ -28,21 +30,21 @@ data class CameraExposureStep(override val request: CameraStartCaptureRequest) : @Volatile private var aborted = false @Volatile private var exposureCount = 0 @Volatile private var captureElapsedTime = Duration.ZERO!! - private lateinit var stepExecution: StepExecution - private val camera = requireNotNull(request.camera) private val exposureTime = request.exposureTime private val exposureDelay = request.exposureDelay private val estimatedTime = if (request.isLoop) Duration.ZERO else Duration.ofNanos(exposureTime.toNanos() * request.exposureAmount + exposureDelay.toNanos() * (request.exposureAmount - 1)) - override fun registerListener(listener: CameraCaptureListener) { - listeners.add(listener) + private lateinit var stepExecution: StepExecution + + override fun registerCameraCaptureListener(listener: CameraCaptureListener): Boolean { + return listeners.add(listener) } - override fun unregisterListener(listener: CameraCaptureListener) { - listeners.remove(listener) + override fun unregisterCameraCaptureListener(listener: CameraCaptureListener): Boolean { + return listeners.remove(listener) } @Subscribe(threadMode = ThreadMode.ASYNC) @@ -97,8 +99,8 @@ data class CameraExposureStep(override val request: CameraStartCaptureRequest) : latch.reset() } - override fun onDelayElapsed(stepExecution: StepExecution) { - val waitTime = stepExecution.jobExecution.context[DelayStep.WAIT_TIME] as Duration + override fun onDelayElapsed(step: DelayStep, stepExecution: StepExecution) { + val waitTime = stepExecution.context[DelayStep.WAIT_TIME] as Duration captureElapsedTime += waitTime onCameraExposureElapsed(Duration.ZERO, Duration.ZERO, 1.0) } @@ -108,9 +110,9 @@ data class CameraExposureStep(override val request: CameraStartCaptureRequest) : synchronized(camera) { latch.countUp() - stepExecution.jobExecution.context[EXPOSURE_AMOUNT] = ++exposureCount + stepExecution.context[EXPOSURE_AMOUNT] = ++exposureCount - listeners.forEach { it.onExposureStarted(stepExecution) } + listeners.forEach { it.onExposureStarted(this, stepExecution) } if (request.width > 0 && request.height > 0) { camera.frame(request.x, request.y, request.width, request.height) @@ -148,9 +150,9 @@ data class CameraExposureStep(override val request: CameraStartCaptureRequest) : savePath.createParentDirectories() stream.transferAndClose(savePath.outputStream()) - stepExecution.jobExecution.context[SAVE_PATH] = savePath + stepExecution.context[SAVE_PATH] = savePath - listeners.forEach { it.onExposureFinished(stepExecution) } + listeners.forEach { it.onExposureFinished(this, stepExecution) } } catch (e: Throwable) { LOG.error("failed to save FITS", e) aborted = true @@ -167,14 +169,14 @@ data class CameraExposureStep(override val request: CameraStartCaptureRequest) : captureProgress = (estimatedTime - captureRemainingTime).toNanos().toDouble() / estimatedTime.toNanos() } - stepExecution.jobExecution.context[EXPOSURE_ELAPSED_TIME] = elapsedTime - stepExecution.jobExecution.context[EXPOSURE_REMAINING_TIME] = remainingTime - stepExecution.jobExecution.context[EXPOSURE_PROGRESS] = progress - stepExecution.jobExecution.context[CAPTURE_ELAPSED_TIME] = captureElapsedTime - stepExecution.jobExecution.context[CAPTURE_REMAINING_TIME] = captureRemainingTime - stepExecution.jobExecution.context[CAPTURE_PROGRESS] = captureProgress + stepExecution.context[EXPOSURE_ELAPSED_TIME] = elapsedTime + stepExecution.context[EXPOSURE_REMAINING_TIME] = remainingTime + stepExecution.context[EXPOSURE_PROGRESS] = progress + stepExecution.context[CAPTURE_ELAPSED_TIME] = captureElapsedTime + stepExecution.context[CAPTURE_REMAINING_TIME] = captureRemainingTime + stepExecution.context[CAPTURE_PROGRESS] = captureProgress - listeners.forEach { it.onExposureElapsed(stepExecution) } + listeners.forEach { it.onExposureElapsed(this, stepExecution) } } companion object { diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraLoopExposureStep.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraLoopExposureStep.kt index 37105d0e3..2ff9cb865 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraLoopExposureStep.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraLoopExposureStep.kt @@ -13,15 +13,15 @@ data class CameraLoopExposureStep( private val delayStep = DelayStep(request.exposureDelay) init { - delayStep.registerListener(cameraExposureStep) + delayStep.registerDelayStepListener(cameraExposureStep) } - override fun registerListener(listener: CameraCaptureListener) { - cameraExposureStep.registerListener(listener) + override fun registerCameraCaptureListener(listener: CameraCaptureListener): Boolean { + return cameraExposureStep.registerCameraCaptureListener(listener) } - override fun unregisterListener(listener: CameraCaptureListener) { - cameraExposureStep.unregisterListener(listener) + override fun unregisterCameraCaptureListener(listener: CameraCaptureListener): Boolean { + return cameraExposureStep.unregisterCameraCaptureListener(listener) } override fun execute(stepExecution: StepExecution): StepResult { diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraStartCaptureStep.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraStartCaptureStep.kt index fb5ecff0a..8a57e813a 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraStartCaptureStep.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraStartCaptureStep.kt @@ -7,7 +7,7 @@ sealed interface CameraStartCaptureStep : Step, JobExecutionListener { val request: CameraStartCaptureRequest - fun registerListener(listener: CameraCaptureListener) + fun registerCameraCaptureListener(listener: CameraCaptureListener): Boolean - fun unregisterListener(listener: CameraCaptureListener) + fun unregisterCameraCaptureListener(listener: CameraCaptureListener): Boolean } diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseListener.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseListener.kt index 7efad28e6..555969c0f 100644 --- a/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseListener.kt +++ b/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseListener.kt @@ -4,5 +4,5 @@ import nebulosa.batch.processing.StepExecution fun interface GuidePulseListener { - fun onGuidePulseElapsed(stepExecution: StepExecution) + fun onGuidePulseElapsed(step: GuidePulseStep, stepExecution: StepExecution) } diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseStep.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseStep.kt index 55381229c..23ddb701a 100644 --- a/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseStep.kt +++ b/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseStep.kt @@ -3,26 +3,26 @@ package nebulosa.api.guiding import nebulosa.batch.processing.Step import nebulosa.batch.processing.StepExecution import nebulosa.batch.processing.StepResult -import nebulosa.batch.processing.delay.DelayListener import nebulosa.batch.processing.delay.DelayStep +import nebulosa.batch.processing.delay.DelayStepListener import nebulosa.guiding.GuideDirection import nebulosa.indi.device.guide.GuideOutput import java.time.Duration -data class GuidePulseStep(val request: GuidePulseRequest) : Step, DelayListener { +data class GuidePulseStep(@JvmField val request: GuidePulseRequest) : Step, DelayStepListener { private val listeners = HashSet() private val delayStep = DelayStep(request.duration) init { - delayStep.registerListener(this) + delayStep.registerDelayStepListener(this) } - fun registerListener(listener: GuidePulseListener) { + fun registerGuidePulseListener(listener: GuidePulseListener) { listeners.add(listener) } - fun unregisterListener(listener: GuidePulseListener) { + fun unregisterGuidePulseListener(listener: GuidePulseListener) { listeners.remove(listener) } @@ -44,10 +44,8 @@ data class GuidePulseStep(val request: GuidePulseRequest) : Step, DelayListener delayStep.stop() } - @Suppress("NAME_SHADOWING") - override fun onDelayElapsed(stepExecution: StepExecution) { - val stepExecution = stepExecution.copy(step = this) - listeners.forEach { it.onGuidePulseElapsed(stepExecution) } + override fun onDelayElapsed(step: DelayStep, stepExecution: StepExecution) { + listeners.forEach { it.onGuidePulseElapsed(this, stepExecution) } } companion object { diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/ObservableJob.kt b/api/src/main/kotlin/nebulosa/api/sequencer/ObservableJob.kt deleted file mode 100644 index 3dd7e3976..000000000 --- a/api/src/main/kotlin/nebulosa/api/sequencer/ObservableJob.kt +++ /dev/null @@ -1,45 +0,0 @@ -package nebulosa.api.sequencer - -import io.reactivex.rxjava3.core.Observable -import io.reactivex.rxjava3.core.ObservableSource -import io.reactivex.rxjava3.core.Observer -import io.reactivex.rxjava3.disposables.Disposable -import io.reactivex.rxjava3.functions.Consumer -import io.reactivex.rxjava3.subjects.PublishSubject -import nebulosa.batch.processing.SimpleJob -import java.io.Closeable - -abstract class ObservableJob : SimpleJob(), ObservableSource, Observer, Closeable { - - @JvmField protected val subject = PublishSubject.create() - - protected open fun Observable.transform() = this - - fun subscribe(onNext: Consumer): Disposable { - return subject.transform().subscribe(onNext) - } - - final override fun subscribe(observer: Observer) { - return subject.transform().subscribe(observer) - } - - final override fun onSubscribe(disposable: Disposable) { - subject.onSubscribe(disposable) - } - - final override fun onNext(event: T) { - subject.onNext(event) - } - - final override fun onError(e: Throwable) { - subject.onError(e) - } - - final override fun onComplete() { - subject.onComplete() - } - - final override fun close() { - onComplete() - } -} diff --git a/desktop/src/app/alignment/alignment.component.html b/desktop/src/app/alignment/alignment.component.html index a3dc17a88..ae76dc6b2 100644 --- a/desktop/src/app/alignment/alignment.component.html +++ b/desktop/src/app/alignment/alignment.component.html @@ -43,10 +43,10 @@ {{ darvDirection }} - {{ darvCapture.remainingTime | exposureTime }} + {{ darvRemainingTime | exposureTime }} - {{ darvCapture.progress | percent:'1.1-1' }} + {{ darvProgress | percent:'1.1-1' }}
diff --git a/desktop/src/app/alignment/alignment.component.ts b/desktop/src/app/alignment/alignment.component.ts index 9714c7eba..83592eb9b 100644 --- a/desktop/src/app/alignment/alignment.component.ts +++ b/desktop/src/app/alignment/alignment.component.ts @@ -2,7 +2,7 @@ import { AfterViewInit, Component, HostListener, NgZone, OnDestroy } from '@angu import { ApiService } from '../../shared/services/api.service' import { BrowserWindowService } from '../../shared/services/browser-window.service' import { ElectronService } from '../../shared/services/electron.service' -import { Camera, DARVPolarAlignmentState, GuideDirection, GuideOutput, Hemisphere, Union } from '../../shared/types' +import { Camera, DARVState, GuideDirection, GuideOutput, Hemisphere, Union } from '../../shared/types' import { AppComponent } from '../app.component' @Component({ @@ -26,12 +26,9 @@ export class AlignmentComponent implements AfterViewInit, OnDestroy { readonly darvHemispheres: Hemisphere[] = ['NORTHERN', 'SOUTHERN'] darvHemisphere: Hemisphere = 'NORTHERN' darvDirection?: GuideDirection - darvStatus: Union = 'IDLE' - - readonly darvCapture = { - remainingTime: 0, - progress: 0, - } + darvStatus: Union = 'IDLE' + darvRemainingTime = 0 + darvProgress = 0 constructor( app: AppComponent, @@ -73,31 +70,14 @@ export class AlignmentComponent implements AfterViewInit, OnDestroy { } }) - electron.on('DARV_POLAR_ALIGNMENT_STARTED', event => { - if (event.camera.name === this.camera?.name && - event.guideOutput.name === this.guideOutput?.name) { - ngZone.run(() => { - this.darvInProgress = true - }) - } - }) - - electron.on('DARV_POLAR_ALIGNMENT_FINISHED', event => { - if (event.camera.name === this.camera?.name && - event.guideOutput.name === this.guideOutput?.name) { - ngZone.run(() => { - this.darvInProgress = false - this.darvStatus = 'IDLE' - this.darvDirection = undefined - }) - } - }) - - electron.on('DARV_POLAR_ALIGNMENT_UPDATED', event => { + electron.on('DARV_POLAR_ALIGNMENT_ELAPSED', event => { if (event.camera.name === this.camera?.name && event.guideOutput.name === this.guideOutput?.name) { ngZone.run(() => { this.darvStatus = event.state + this.darvRemainingTime = event.remainingTime + this.darvProgress = event.progress + this.darvInProgress = event.remainingTime > 0 if (event.state !== 'INITIAL_PAUSE') { this.darvDirection = event.direction @@ -105,15 +85,6 @@ export class AlignmentComponent implements AfterViewInit, OnDestroy { }) } }) - - electron.on('CAMERA_EXPOSURE_ELAPSED', event => { - if (event.camera.name === this.camera?.name) { - ngZone.run(() => { - this.darvCapture.remainingTime = event.remainingTime - this.darvCapture.progress = event.progress - }) - } - }) } async ngAfterViewInit() { diff --git a/desktop/src/shared/services/electron.service.ts b/desktop/src/shared/services/electron.service.ts index 91a90c5a0..df6f76b15 100644 --- a/desktop/src/shared/services/electron.service.ts +++ b/desktop/src/shared/services/electron.service.ts @@ -8,9 +8,9 @@ import { ipcRenderer, webFrame } from 'electron' import * as fs from 'fs' import { ApiEventType, Camera, CameraCaptureElapsed, CameraCaptureFinished, CameraCaptureIsWaiting, CameraCaptureStarted, - CameraExposureElapsed, CameraExposureFinished, CameraExposureStarted, DARVPolarAlignmentEvent, DARVPolarAlignmentGuidePulseElapsed, - DARVPolarAlignmentInitialPauseElapsed, DeviceMessageEvent, FilterWheel, Focuser, GuideOutput, Guider, - GuiderMessageEvent, HistoryStep, INDIMessageEvent, InternalEventType, Location, Mount, NotificationEvent, NotificationEventType, OpenDirectory, OpenFile + CameraExposureElapsed, CameraExposureFinished, CameraExposureStarted, DARVEvent, DeviceMessageEvent, FilterWheel, + Focuser, GuideOutput, Guider, GuiderMessageEvent, HistoryStep, INDIMessageEvent, InternalEventType, Location, Mount, NotificationEvent, + NotificationEventType, OpenDirectory, OpenFile } from '../types' import { ApiService } from './api.service' @@ -45,9 +45,7 @@ type EventMappedType = { 'GUIDER_UPDATED': GuiderMessageEvent 'GUIDER_STEPPED': GuiderMessageEvent 'GUIDER_MESSAGE_RECEIVED': GuiderMessageEvent - 'DARV_POLAR_ALIGNMENT_STARTED': DARVPolarAlignmentEvent - 'DARV_POLAR_ALIGNMENT_FINISHED': DARVPolarAlignmentEvent - 'DARV_POLAR_ALIGNMENT_UPDATED': DARVPolarAlignmentInitialPauseElapsed | DARVPolarAlignmentGuidePulseElapsed + 'DARV_POLAR_ALIGNMENT_ELAPSED': DARVEvent 'DATA_CHANGED': any 'LOCATION_CHANGED': Location 'SKY_ATLAS_UPDATE_FINISHED': NotificationEvent diff --git a/desktop/src/shared/types.ts b/desktop/src/shared/types.ts index d98b65597..271e05b66 100644 --- a/desktop/src/shared/types.ts +++ b/desktop/src/shared/types.ts @@ -487,24 +487,20 @@ export interface Satellite { groups: SatelliteGroupType[] } -export interface DARVPolarAlignmentEvent extends MessageEvent { +export interface DARVEvent extends MessageEvent { camera: Camera guideOutput: GuideOutput remainingTime: number progress: number - state: DARVPolarAlignmentState + state: DARVState + direction?: GuideDirection } -export interface DARVPolarAlignmentInitialPauseElapsed extends DARVPolarAlignmentEvent { +export interface DARVInitialPauseElapsed extends DARVEvent { pauseTime: number state: 'INITIAL_PAUSE' } -export interface DARVPolarAlignmentGuidePulseElapsed extends DARVPolarAlignmentEvent { - direction: GuideDirection - state: 'FORWARD' | 'BACKWARD' -} - export interface CoordinateInterpolation { ma: number[] md: number[] @@ -783,7 +779,7 @@ export const API_EVENT_TYPES = [ 'GUIDER_CONNECTED', 'GUIDER_DISCONNECTED', 'GUIDER_UPDATED', 'GUIDER_STEPPED', 'GUIDER_MESSAGE_RECEIVED', // Polar Alignment. - 'DARV_POLAR_ALIGNMENT_STARTED', 'DARV_POLAR_ALIGNMENT_FINISHED', 'DARV_POLAR_ALIGNMENT_UPDATED', + 'DARV_POLAR_ALIGNMENT_ELAPSED', ] as const export type ApiEventType = (typeof API_EVENT_TYPES)[number] @@ -890,7 +886,7 @@ export type GuideState = (typeof GUIDE_STATES)[number] export type Hemisphere = 'NORTHERN' | 'SOUTHERN' -export type DARVPolarAlignmentState = 'IDLE' | 'INITIAL_PAUSE' | 'FORWARD' | 'BACKWARD' +export type DARVState = 'IDLE' | 'INITIAL_PAUSE' | 'FORWARD' | 'BACKWARD' export type GuiderPlotMode = 'RA/DEC' | 'DX/DY' diff --git a/nebulosa-batch-processing/build.gradle.kts b/nebulosa-batch-processing/build.gradle.kts index 24ca9524d..73f4f81c5 100644 --- a/nebulosa-batch-processing/build.gradle.kts +++ b/nebulosa-batch-processing/build.gradle.kts @@ -4,6 +4,7 @@ plugins { } dependencies { + api(libs.rx) implementation(project(":nebulosa-log")) testImplementation(project(":nebulosa-test")) } diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/AsyncJobLauncher.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/AsyncJobLauncher.kt index 69b40a221..69b02f24a 100644 --- a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/AsyncJobLauncher.kt +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/AsyncJobLauncher.kt @@ -1,50 +1,39 @@ package nebulosa.batch.processing import java.time.LocalDateTime -import java.util.concurrent.ExecutorService +import java.util.concurrent.Executor -open class AsyncJobLauncher(private val executor: ExecutorService) : JobLauncher { +open class AsyncJobLauncher(private val executor: Executor) : JobLauncher, StepInterceptor { private val jobListeners = HashSet() private val stepListeners = HashSet() - private val mStepInterceptors = HashSet() + private val stepInterceptors = HashSet() private val jobs = LinkedHashMap() - override val stepHandler: StepHandler = DefaultStepHandler + override var stepHandler: StepHandler = DefaultStepHandler - override val stepInterceptors: Collection - get() = mStepInterceptors - - override fun registerJobListener(listener: JobExecutionListener) { - jobListeners.add(listener) - } - - override fun unregisterJobListener(listener: JobExecutionListener) { - jobListeners.remove(listener) + override fun registerJobExecutionListener(listener: JobExecutionListener): Boolean { + return jobListeners.add(listener) } - override fun registerStepListener(listener: StepExecutionListener) { - stepListeners.add(listener) + override fun unregisterJobExecutionListener(listener: JobExecutionListener): Boolean { + return jobListeners.remove(listener) } - override fun unregisterStepListener(listener: StepExecutionListener) { - stepListeners.remove(listener) + override fun registerStepExecutionListener(listener: StepExecutionListener): Boolean { + return stepListeners.add(listener) } - override fun registerStepInterceptor(interceptor: StepInterceptor) { - mStepInterceptors.add(interceptor) + override fun unregisterStepExecutionListener(listener: StepExecutionListener): Boolean { + return stepListeners.remove(listener) } - override fun unregisterStepInterceptor(interceptor: StepInterceptor) { - mStepInterceptors.remove(interceptor) + override fun registerStepInterceptor(interceptor: StepInterceptor): Boolean { + return stepInterceptors.add(interceptor) } - override fun fireBeforeStep(stepExecution: StepExecution) { - stepListeners.forEach { it.beforeStep(stepExecution) } - } - - override fun fireAfterStep(stepExecution: StepExecution) { - stepListeners.forEach { it.afterStep(stepExecution) } + override fun unregisterStepInterceptor(interceptor: StepInterceptor): Boolean { + return stepInterceptors.remove(interceptor) } override val size @@ -76,35 +65,30 @@ open class AsyncJobLauncher(private val executor: ExecutorService) : JobLauncher } } - jobExecution = JobExecution(job, executionContext ?: ExecutionContext(), this) + val interceptors = ArrayList(stepInterceptors.size + 1) + interceptors.addAll(stepInterceptors) + interceptors.add(this) + + jobExecution = JobExecution(job, executionContext ?: ExecutionContext(), this, interceptors) jobs[job.id] = jobExecution - executor.submit { - jobExecution.status = BatchStatus.STARTED + executor.execute { + jobExecution.status = JobStatus.STARTED job.beforeJob(jobExecution) jobListeners.forEach { it.beforeJob(jobExecution) } - val stepJobListeners = HashSet() - try { while (jobExecution.canContinue && job.hasNext(jobExecution)) { val step = job.next(jobExecution) - - if (step is JobExecutionListener) { - if (stepJobListeners.add(step)) { - step.beforeJob(jobExecution) - } - } - stepHandler.handle(step, StepExecution(step, jobExecution)) } - jobExecution.status = if (jobExecution.isStopping) BatchStatus.STOPPED else BatchStatus.COMPLETED + jobExecution.status = if (jobExecution.isStopping) JobStatus.STOPPED else JobStatus.COMPLETED jobExecution.complete() } catch (e: Throwable) { - jobExecution.status = BatchStatus.FAILED + jobExecution.status = JobStatus.FAILED jobExecution.completeExceptionally(e) } finally { jobExecution.finishedAt = LocalDateTime.now() @@ -112,7 +96,6 @@ open class AsyncJobLauncher(private val executor: ExecutorService) : JobLauncher job.afterJob(jobExecution) jobListeners.forEach { it.afterJob(jobExecution) } - stepJobListeners.forEach { it.afterJob(jobExecution) } } return jobExecution @@ -121,4 +104,13 @@ open class AsyncJobLauncher(private val executor: ExecutorService) : JobLauncher override fun stop(mayInterruptIfRunning: Boolean) { jobs.forEach { it.value.stop(mayInterruptIfRunning) } } + + override fun intercept(chain: StepChain): StepResult { + stepListeners.forEach { it.beforeStep(chain.stepExecution) } + val result = chain.step.execute(chain.stepExecution) + stepListeners.forEach { it.afterStep(chain.stepExecution) } + return result + } + + override fun toString() = "AsyncJobLauncher" } diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/DefaultStepHandler.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/DefaultStepHandler.kt index 315155b66..6118f797f 100644 --- a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/DefaultStepHandler.kt +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/DefaultStepHandler.kt @@ -1,12 +1,20 @@ package nebulosa.batch.processing -import java.time.LocalDateTime +import nebulosa.log.loggerFor -object DefaultStepHandler : StepHandler, StepInterceptor { +object DefaultStepHandler : StepHandler { + + @JvmStatic private val LOG = loggerFor() override fun handle(step: Step, stepExecution: StepExecution): StepResult { val jobLauncher = stepExecution.jobExecution.jobLauncher + if (step is JobExecutionListener) { + if (jobLauncher.registerJobExecutionListener(step)) { + step.beforeJob(stepExecution.jobExecution) + } + } + when (step) { is SplitStep -> { step.beforeStep(stepExecution) @@ -19,28 +27,19 @@ object DefaultStepHandler : StepHandler, StepInterceptor { step.afterStep(stepExecution) } else -> { - val interceptors = ArrayList(jobLauncher.stepInterceptors.size + 1) - interceptors.addAll(jobLauncher.stepInterceptors) - interceptors.add(this) - - val chain = StepInterceptorChain(interceptors, step, stepExecution) + val chain = StepInterceptorChain(stepExecution.jobExecution.stepInterceptors, step, stepExecution) var status: RepeatStatus + LOG.info("step started. step={}, context={}", step, stepExecution.context) + do { - jobLauncher.fireBeforeStep(stepExecution) - val result = chain.proceed() - jobLauncher.fireAfterStep(stepExecution) - status = result.get() + status = chain.proceed().get() } while (status == RepeatStatus.CONTINUABLE) - stepExecution.finishedAt = LocalDateTime.now() + LOG.info("step finished. step={}, context={}", step, stepExecution.context) } } return StepResult.FINISHED } - - override fun intercept(chain: StepChain): StepResult { - return chain.step.execute(chain.stepExecution) - } } diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobExecution.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobExecution.kt index 50fd261fe..9eaeca7e0 100644 --- a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobExecution.kt +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobExecution.kt @@ -9,27 +9,31 @@ data class JobExecution( val job: Job, val context: ExecutionContext, val jobLauncher: JobLauncher, + val stepInterceptors: List, val startedAt: LocalDateTime = LocalDateTime.now(), - var status: BatchStatus = BatchStatus.STARTING, + var status: JobStatus = JobStatus.STARTING, var finishedAt: LocalDateTime? = null, ) : Stoppable { private val completable = CompletableFuture() - val canContinue - get() = status == BatchStatus.STARTED + inline val jobId + get() = job.id - val isStopping - get() = status == BatchStatus.STOPPING + inline val canContinue + get() = status == JobStatus.STARTED - val isStopped - get() = status == BatchStatus.STOPPED + inline val isStopping + get() = status == JobStatus.STOPPING - val isCompleted - get() = status == BatchStatus.COMPLETED + inline val isStopped + get() = status == JobStatus.STOPPED - val isFailed - get() = status == BatchStatus.FAILED + inline val isCompleted + get() = status == JobStatus.COMPLETED + + inline val isFailed + get() = status == JobStatus.FAILED val isDone get() = isCompleted || isFailed || isStopped @@ -54,7 +58,7 @@ data class JobExecution( override fun stop(mayInterruptIfRunning: Boolean) { if (!isDone) { - status = BatchStatus.STOPPING + status = JobStatus.STOPPING job.stop(mayInterruptIfRunning) } } diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobLauncher.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobLauncher.kt index e71243239..4612e234e 100644 --- a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobLauncher.kt +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobLauncher.kt @@ -4,23 +4,17 @@ interface JobLauncher : Collection, Stoppable { val stepHandler: StepHandler - val stepInterceptors: Collection + fun registerJobExecutionListener(listener: JobExecutionListener): Boolean - fun registerJobListener(listener: JobExecutionListener) + fun unregisterJobExecutionListener(listener: JobExecutionListener): Boolean - fun unregisterJobListener(listener: JobExecutionListener) + fun registerStepExecutionListener(listener: StepExecutionListener): Boolean - fun registerStepListener(listener: StepExecutionListener) + fun unregisterStepExecutionListener(listener: StepExecutionListener): Boolean - fun unregisterStepListener(listener: StepExecutionListener) + fun registerStepInterceptor(interceptor: StepInterceptor): Boolean - fun registerStepInterceptor(interceptor: StepInterceptor) - - fun unregisterStepInterceptor(interceptor: StepInterceptor) - - fun fireBeforeStep(stepExecution: StepExecution) - - fun fireAfterStep(stepExecution: StepExecution) + fun unregisterStepInterceptor(interceptor: StepInterceptor): Boolean fun launch(job: Job, executionContext: ExecutionContext? = null): JobExecution } diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/BatchStatus.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobStatus.kt similarity index 84% rename from nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/BatchStatus.kt rename to nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobStatus.kt index 5825b5a57..005aa08e2 100644 --- a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/BatchStatus.kt +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobStatus.kt @@ -1,6 +1,6 @@ package nebulosa.batch.processing -enum class BatchStatus { +enum class JobStatus { STARTING, STARTED, STOPPING, diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/PublishSubscribe.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/PublishSubscribe.kt new file mode 100644 index 000000000..a2a26aa00 --- /dev/null +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/PublishSubscribe.kt @@ -0,0 +1,44 @@ +package nebulosa.batch.processing + +import io.reactivex.rxjava3.core.Observable +import io.reactivex.rxjava3.core.ObservableSource +import io.reactivex.rxjava3.core.Observer +import io.reactivex.rxjava3.disposables.Disposable +import io.reactivex.rxjava3.functions.Consumer +import io.reactivex.rxjava3.subjects.Subject +import java.io.Closeable + +interface PublishSubscribe : ObservableSource, Observer, Closeable { + + val subject: Subject + + fun Observable.transform() = this + + fun subscribe(onNext: Consumer): Disposable { + return subject.transform().subscribe(onNext) + } + + override fun subscribe(observer: Observer) { + return subject.transform().subscribe(observer) + } + + override fun onSubscribe(disposable: Disposable) { + subject.onSubscribe(disposable) + } + + override fun onNext(event: T) { + subject.onNext(event) + } + + override fun onError(e: Throwable) { + subject.onError(e) + } + + override fun onComplete() { + subject.onComplete() + } + + override fun close() { + onComplete() + } +} diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepExecution.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepExecution.kt index bb625716b..56538f1b7 100644 --- a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepExecution.kt +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/StepExecution.kt @@ -1,10 +1,10 @@ package nebulosa.batch.processing -import java.time.LocalDateTime - data class StepExecution( val step: Step, val jobExecution: JobExecution, - val startedAt: LocalDateTime = LocalDateTime.now(), - var finishedAt: LocalDateTime? = null, -) +) { + + inline val context + get() = jobExecution.context +} diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/delay/DelayListener.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/delay/DelayListener.kt deleted file mode 100644 index 7123841c3..000000000 --- a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/delay/DelayListener.kt +++ /dev/null @@ -1,8 +0,0 @@ -package nebulosa.batch.processing.delay - -import nebulosa.batch.processing.StepExecution - -fun interface DelayListener { - - fun onDelayElapsed(stepExecution: StepExecution) -} diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/delay/DelayStep.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/delay/DelayStep.kt index aae7c0c7f..be164f58d 100644 --- a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/delay/DelayStep.kt +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/delay/DelayStep.kt @@ -1,47 +1,46 @@ package nebulosa.batch.processing.delay -import nebulosa.batch.processing.Step -import nebulosa.batch.processing.StepExecution -import nebulosa.batch.processing.StepResult +import nebulosa.batch.processing.* import java.time.Duration -data class DelayStep(@JvmField val duration: Duration) : Step { +data class DelayStep(@JvmField val duration: Duration) : Step, JobExecutionListener { + + private val listeners = HashSet() @Volatile private var aborted = false - private val listeners = HashSet() - fun registerListener(listener: DelayListener) { + fun registerDelayStepListener(listener: DelayStepListener) { listeners.add(listener) } - fun unregisterListener(listener: DelayListener) { + fun unregisterDelayStepListener(listener: DelayStepListener) { listeners.remove(listener) } override fun execute(stepExecution: StepExecution): StepResult { var remainingTime = duration - if (remainingTime > Duration.ZERO) { + if (!aborted && remainingTime > Duration.ZERO) { while (!aborted && remainingTime > Duration.ZERO) { val waitTime = minOf(remainingTime, DELAY_INTERVAL) if (waitTime > Duration.ZERO) { - stepExecution.jobExecution.context[REMAINING_TIME] = remainingTime - stepExecution.jobExecution.context[WAIT_TIME] = waitTime + stepExecution.context[REMAINING_TIME] = remainingTime + stepExecution.context[WAIT_TIME] = waitTime val progress = (duration.toNanos() - remainingTime.toNanos()) / duration.toNanos().toDouble() - stepExecution.jobExecution.context[PROGRESS] = progress + stepExecution.context[PROGRESS] = progress - listeners.forEach { it.onDelayElapsed(stepExecution) } + listeners.forEach { it.onDelayElapsed(this, stepExecution) } Thread.sleep(waitTime.toMillis()) remainingTime -= waitTime } } - stepExecution.jobExecution.context[REMAINING_TIME] = Duration.ZERO - stepExecution.jobExecution.context[WAIT_TIME] = Duration.ZERO + stepExecution.context[REMAINING_TIME] = Duration.ZERO + stepExecution.context[WAIT_TIME] = Duration.ZERO - listeners.forEach { it.onDelayElapsed(stepExecution) } + listeners.forEach { it.onDelayElapsed(this, stepExecution) } } return StepResult.FINISHED @@ -51,6 +50,10 @@ data class DelayStep(@JvmField val duration: Duration) : Step { aborted = true } + override fun afterJob(jobExecution: JobExecution) { + listeners.clear() + } + companion object { const val REMAINING_TIME = "DELAY.REMAINING_TIME" diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/delay/DelayStepListener.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/delay/DelayStepListener.kt new file mode 100644 index 000000000..04847c947 --- /dev/null +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/delay/DelayStepListener.kt @@ -0,0 +1,8 @@ +package nebulosa.batch.processing.delay + +import nebulosa.batch.processing.StepExecution + +fun interface DelayStepListener { + + fun onDelayElapsed(step: DelayStep, stepExecution: StepExecution) +} From d83802d3e9460fba3475977dadbf5d8921e370fe Mon Sep 17 00:00:00 2001 From: tiagohm Date: Thu, 14 Dec 2023 09:10:12 -0300 Subject: [PATCH 30/87] [api]: Move message classes to another package --- .../kotlin/nebulosa/api/alignment/polar/darv/DARVEvent.kt | 2 +- .../kotlin/nebulosa/api/alignment/polar/darv/DARVExecutor.kt | 4 ++-- .../api/alignment/polar/darv/DARVGuidePulseElapsed.kt | 2 +- .../main/kotlin/nebulosa/api/alignment/polar/darv/DARVJob.kt | 2 +- api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasUpdateTask.kt | 4 ++-- .../main/kotlin/nebulosa/api/cameras/CameraCaptureEvent.kt | 2 +- .../kotlin/nebulosa/api/cameras/CameraCaptureExecutor.kt | 2 +- .../main/kotlin/nebulosa/api/cameras/CameraEventHandler.kt | 2 +- .../main/kotlin/nebulosa/api/cameras/CameraMessageEvent.kt | 2 +- .../main/kotlin/nebulosa/api/focusers/FocuserEventHandler.kt | 2 +- .../main/kotlin/nebulosa/api/focusers/FocuserMessageEvent.kt | 2 +- .../kotlin/nebulosa/api/guiding/GuideOutputEventHandler.kt | 2 +- .../kotlin/nebulosa/api/guiding/GuideOutputMessageEvent.kt | 2 +- .../main/kotlin/nebulosa/api/guiding/GuiderMessageEvent.kt | 2 +- api/src/main/kotlin/nebulosa/api/guiding/GuidingService.kt | 2 +- api/src/main/kotlin/nebulosa/api/indi/INDIEventHandler.kt | 2 +- api/src/main/kotlin/nebulosa/api/indi/INDIMessageEvent.kt | 2 +- .../api/{services => messages}/DeviceMessageEvent.kt | 2 +- .../nebulosa/api/{services => messages}/MessageEvent.kt | 2 +- .../nebulosa/api/{services => messages}/MessageService.kt | 2 +- api/src/main/kotlin/nebulosa/api/mounts/MountEventHandler.kt | 2 +- api/src/main/kotlin/nebulosa/api/mounts/MountMessageEvent.kt | 2 +- .../api/{notification => notifications}/NotificationEvent.kt | 4 ++-- api/src/main/kotlin/nebulosa/api/wheels/WheelEventHandler.kt | 2 +- api/src/main/kotlin/nebulosa/api/wheels/WheelMessageEvent.kt | 2 +- desktop/src/shared/types.ts | 5 ----- .../main/kotlin/nebulosa/batch/processing/SimpleSplitStep.kt | 2 +- 27 files changed, 29 insertions(+), 34 deletions(-) rename api/src/main/kotlin/nebulosa/api/{services => messages}/DeviceMessageEvent.kt (79%) rename api/src/main/kotlin/nebulosa/api/{services => messages}/MessageEvent.kt (64%) rename api/src/main/kotlin/nebulosa/api/{services => messages}/MessageService.kt (98%) rename api/src/main/kotlin/nebulosa/api/{notification => notifications}/NotificationEvent.kt (83%) diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVEvent.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVEvent.kt index a19ad880a..eb4102f31 100644 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVEvent.kt +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVEvent.kt @@ -1,6 +1,6 @@ package nebulosa.api.alignment.polar.darv -import nebulosa.api.services.MessageEvent +import nebulosa.api.messages.MessageEvent import nebulosa.guiding.GuideDirection import nebulosa.indi.device.camera.Camera import nebulosa.indi.device.guide.GuideOutput diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVExecutor.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVExecutor.kt index def1bc8cb..8fa46af6c 100644 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVExecutor.kt +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVExecutor.kt @@ -1,8 +1,8 @@ package nebulosa.api.alignment.polar.darv import io.reactivex.rxjava3.functions.Consumer -import nebulosa.api.services.MessageEvent -import nebulosa.api.services.MessageService +import nebulosa.api.messages.MessageEvent +import nebulosa.api.messages.MessageService import nebulosa.batch.processing.JobExecution import nebulosa.batch.processing.JobLauncher import nebulosa.indi.device.camera.Camera diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVGuidePulseElapsed.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVGuidePulseElapsed.kt index f583371cf..ce9d5e6f9 100644 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVGuidePulseElapsed.kt +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVGuidePulseElapsed.kt @@ -1,6 +1,6 @@ package nebulosa.api.alignment.polar.darv -import nebulosa.api.services.MessageEvent +import nebulosa.api.messages.MessageEvent import nebulosa.guiding.GuideDirection import nebulosa.indi.device.camera.Camera import nebulosa.indi.device.guide.GuideOutput diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVJob.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVJob.kt index 44f55e476..69a360274 100644 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVJob.kt +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVJob.kt @@ -8,7 +8,7 @@ import nebulosa.api.cameras.CameraStartCaptureRequest import nebulosa.api.guiding.GuidePulseListener import nebulosa.api.guiding.GuidePulseRequest import nebulosa.api.guiding.GuidePulseStep -import nebulosa.api.services.MessageEvent +import nebulosa.api.messages.MessageEvent import nebulosa.batch.processing.* import nebulosa.batch.processing.delay.DelayStep import nebulosa.batch.processing.delay.DelayStepListener diff --git a/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasUpdateTask.kt b/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasUpdateTask.kt index 6f68d214c..20d490cae 100644 --- a/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasUpdateTask.kt +++ b/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasUpdateTask.kt @@ -2,9 +2,9 @@ package nebulosa.api.atlas import com.fasterxml.jackson.core.type.TypeReference import com.fasterxml.jackson.databind.ObjectMapper -import nebulosa.api.notification.NotificationEvent +import nebulosa.api.messages.MessageService +import nebulosa.api.notifications.NotificationEvent import nebulosa.api.preferences.PreferenceService -import nebulosa.api.services.MessageService import nebulosa.log.loggerFor import okhttp3.OkHttpClient import okhttp3.Request diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureEvent.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureEvent.kt index a03905c7d..f727ecd72 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureEvent.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureEvent.kt @@ -1,6 +1,6 @@ package nebulosa.api.cameras -import nebulosa.api.services.MessageEvent +import nebulosa.api.messages.MessageEvent import nebulosa.indi.device.camera.Camera sealed interface CameraCaptureEvent : MessageEvent { diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureExecutor.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureExecutor.kt index db4d050a8..43593c5a6 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureExecutor.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureExecutor.kt @@ -1,6 +1,6 @@ package nebulosa.api.cameras -import nebulosa.api.services.MessageService +import nebulosa.api.messages.MessageService import nebulosa.batch.processing.JobExecution import nebulosa.batch.processing.JobLauncher import nebulosa.batch.processing.StepExecution diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraEventHandler.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraEventHandler.kt index 98778f5c9..28e49ed67 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraEventHandler.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraEventHandler.kt @@ -3,7 +3,7 @@ package nebulosa.api.cameras import io.reactivex.rxjava3.subjects.PublishSubject import jakarta.annotation.PostConstruct import nebulosa.api.beans.annotations.Subscriber -import nebulosa.api.services.MessageService +import nebulosa.api.messages.MessageService import nebulosa.indi.device.PropertyChangedEvent import nebulosa.indi.device.camera.Camera import nebulosa.indi.device.camera.CameraAttached diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraMessageEvent.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraMessageEvent.kt index 911a1fd85..4e69fe521 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraMessageEvent.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraMessageEvent.kt @@ -1,6 +1,6 @@ package nebulosa.api.cameras -import nebulosa.api.services.DeviceMessageEvent +import nebulosa.api.messages.DeviceMessageEvent import nebulosa.indi.device.camera.Camera data class CameraMessageEvent( diff --git a/api/src/main/kotlin/nebulosa/api/focusers/FocuserEventHandler.kt b/api/src/main/kotlin/nebulosa/api/focusers/FocuserEventHandler.kt index 2b218cde3..e77752446 100644 --- a/api/src/main/kotlin/nebulosa/api/focusers/FocuserEventHandler.kt +++ b/api/src/main/kotlin/nebulosa/api/focusers/FocuserEventHandler.kt @@ -3,7 +3,7 @@ package nebulosa.api.focusers import io.reactivex.rxjava3.subjects.PublishSubject import jakarta.annotation.PostConstruct import nebulosa.api.beans.annotations.Subscriber -import nebulosa.api.services.MessageService +import nebulosa.api.messages.MessageService import nebulosa.indi.device.PropertyChangedEvent import nebulosa.indi.device.focuser.Focuser import nebulosa.indi.device.focuser.FocuserAttached diff --git a/api/src/main/kotlin/nebulosa/api/focusers/FocuserMessageEvent.kt b/api/src/main/kotlin/nebulosa/api/focusers/FocuserMessageEvent.kt index d75cbcdbc..cae010f3f 100644 --- a/api/src/main/kotlin/nebulosa/api/focusers/FocuserMessageEvent.kt +++ b/api/src/main/kotlin/nebulosa/api/focusers/FocuserMessageEvent.kt @@ -1,6 +1,6 @@ package nebulosa.api.focusers -import nebulosa.api.services.DeviceMessageEvent +import nebulosa.api.messages.DeviceMessageEvent import nebulosa.indi.device.focuser.Focuser data class FocuserMessageEvent( diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputEventHandler.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputEventHandler.kt index 081d041f3..9baaa26f8 100644 --- a/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputEventHandler.kt +++ b/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputEventHandler.kt @@ -3,7 +3,7 @@ package nebulosa.api.guiding import io.reactivex.rxjava3.subjects.PublishSubject import jakarta.annotation.PostConstruct import nebulosa.api.beans.annotations.Subscriber -import nebulosa.api.services.MessageService +import nebulosa.api.messages.MessageService import nebulosa.indi.device.DeviceEvent import nebulosa.indi.device.PropertyChangedEvent import nebulosa.indi.device.guide.GuideOutput diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputMessageEvent.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputMessageEvent.kt index 9ea9e3673..b19092080 100644 --- a/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputMessageEvent.kt +++ b/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputMessageEvent.kt @@ -1,6 +1,6 @@ package nebulosa.api.guiding -import nebulosa.api.services.DeviceMessageEvent +import nebulosa.api.messages.DeviceMessageEvent import nebulosa.indi.device.guide.GuideOutput data class GuideOutputMessageEvent( diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuiderMessageEvent.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuiderMessageEvent.kt index 6b13b0ca2..5cd58eadd 100644 --- a/api/src/main/kotlin/nebulosa/api/guiding/GuiderMessageEvent.kt +++ b/api/src/main/kotlin/nebulosa/api/guiding/GuiderMessageEvent.kt @@ -1,6 +1,6 @@ package nebulosa.api.guiding -import nebulosa.api.services.MessageEvent +import nebulosa.api.messages.MessageEvent data class GuiderMessageEvent( override val eventName: String, diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuidingService.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuidingService.kt index 3a62244ec..eee542c18 100644 --- a/api/src/main/kotlin/nebulosa/api/guiding/GuidingService.kt +++ b/api/src/main/kotlin/nebulosa/api/guiding/GuidingService.kt @@ -1,8 +1,8 @@ package nebulosa.api.guiding import jakarta.annotation.PreDestroy +import nebulosa.api.messages.MessageService import nebulosa.api.preferences.PreferenceService -import nebulosa.api.services.MessageService import nebulosa.guiding.GuideStar import nebulosa.guiding.GuideState import nebulosa.guiding.Guider diff --git a/api/src/main/kotlin/nebulosa/api/indi/INDIEventHandler.kt b/api/src/main/kotlin/nebulosa/api/indi/INDIEventHandler.kt index aff9bd9cc..b8710dd05 100644 --- a/api/src/main/kotlin/nebulosa/api/indi/INDIEventHandler.kt +++ b/api/src/main/kotlin/nebulosa/api/indi/INDIEventHandler.kt @@ -1,7 +1,7 @@ package nebulosa.api.indi import nebulosa.api.beans.annotations.Subscriber -import nebulosa.api.services.MessageService +import nebulosa.api.messages.MessageService import nebulosa.indi.device.* import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode diff --git a/api/src/main/kotlin/nebulosa/api/indi/INDIMessageEvent.kt b/api/src/main/kotlin/nebulosa/api/indi/INDIMessageEvent.kt index 767fd7850..c689d81ab 100644 --- a/api/src/main/kotlin/nebulosa/api/indi/INDIMessageEvent.kt +++ b/api/src/main/kotlin/nebulosa/api/indi/INDIMessageEvent.kt @@ -1,6 +1,6 @@ package nebulosa.api.indi -import nebulosa.api.services.DeviceMessageEvent +import nebulosa.api.messages.DeviceMessageEvent import nebulosa.indi.device.Device import nebulosa.indi.device.DeviceMessageReceived import nebulosa.indi.device.DevicePropertyEvent diff --git a/api/src/main/kotlin/nebulosa/api/services/DeviceMessageEvent.kt b/api/src/main/kotlin/nebulosa/api/messages/DeviceMessageEvent.kt similarity index 79% rename from api/src/main/kotlin/nebulosa/api/services/DeviceMessageEvent.kt rename to api/src/main/kotlin/nebulosa/api/messages/DeviceMessageEvent.kt index 1968a78af..248354ff7 100644 --- a/api/src/main/kotlin/nebulosa/api/services/DeviceMessageEvent.kt +++ b/api/src/main/kotlin/nebulosa/api/messages/DeviceMessageEvent.kt @@ -1,4 +1,4 @@ -package nebulosa.api.services +package nebulosa.api.messages import nebulosa.indi.device.Device diff --git a/api/src/main/kotlin/nebulosa/api/services/MessageEvent.kt b/api/src/main/kotlin/nebulosa/api/messages/MessageEvent.kt similarity index 64% rename from api/src/main/kotlin/nebulosa/api/services/MessageEvent.kt rename to api/src/main/kotlin/nebulosa/api/messages/MessageEvent.kt index 57052c612..1d6aac26d 100644 --- a/api/src/main/kotlin/nebulosa/api/services/MessageEvent.kt +++ b/api/src/main/kotlin/nebulosa/api/messages/MessageEvent.kt @@ -1,4 +1,4 @@ -package nebulosa.api.services +package nebulosa.api.messages interface MessageEvent { diff --git a/api/src/main/kotlin/nebulosa/api/services/MessageService.kt b/api/src/main/kotlin/nebulosa/api/messages/MessageService.kt similarity index 98% rename from api/src/main/kotlin/nebulosa/api/services/MessageService.kt rename to api/src/main/kotlin/nebulosa/api/messages/MessageService.kt index 8e9d254b8..c5ea68a1c 100644 --- a/api/src/main/kotlin/nebulosa/api/services/MessageService.kt +++ b/api/src/main/kotlin/nebulosa/api/messages/MessageService.kt @@ -1,4 +1,4 @@ -package nebulosa.api.services +package nebulosa.api.messages import nebulosa.log.debug import nebulosa.log.loggerFor diff --git a/api/src/main/kotlin/nebulosa/api/mounts/MountEventHandler.kt b/api/src/main/kotlin/nebulosa/api/mounts/MountEventHandler.kt index 4a276b5eb..1a9787d12 100644 --- a/api/src/main/kotlin/nebulosa/api/mounts/MountEventHandler.kt +++ b/api/src/main/kotlin/nebulosa/api/mounts/MountEventHandler.kt @@ -3,7 +3,7 @@ package nebulosa.api.mounts import io.reactivex.rxjava3.subjects.PublishSubject import jakarta.annotation.PostConstruct import nebulosa.api.beans.annotations.Subscriber -import nebulosa.api.services.MessageService +import nebulosa.api.messages.MessageService import nebulosa.indi.device.PropertyChangedEvent import nebulosa.indi.device.mount.Mount import nebulosa.indi.device.mount.MountAttached diff --git a/api/src/main/kotlin/nebulosa/api/mounts/MountMessageEvent.kt b/api/src/main/kotlin/nebulosa/api/mounts/MountMessageEvent.kt index f72989ea4..dc8f68048 100644 --- a/api/src/main/kotlin/nebulosa/api/mounts/MountMessageEvent.kt +++ b/api/src/main/kotlin/nebulosa/api/mounts/MountMessageEvent.kt @@ -1,6 +1,6 @@ package nebulosa.api.mounts -import nebulosa.api.services.DeviceMessageEvent +import nebulosa.api.messages.DeviceMessageEvent import nebulosa.indi.device.mount.Mount data class MountMessageEvent( diff --git a/api/src/main/kotlin/nebulosa/api/notification/NotificationEvent.kt b/api/src/main/kotlin/nebulosa/api/notifications/NotificationEvent.kt similarity index 83% rename from api/src/main/kotlin/nebulosa/api/notification/NotificationEvent.kt rename to api/src/main/kotlin/nebulosa/api/notifications/NotificationEvent.kt index b63a79609..0971bc731 100644 --- a/api/src/main/kotlin/nebulosa/api/notification/NotificationEvent.kt +++ b/api/src/main/kotlin/nebulosa/api/notifications/NotificationEvent.kt @@ -1,6 +1,6 @@ -package nebulosa.api.notification +package nebulosa.api.notifications -import nebulosa.api.services.MessageEvent +import nebulosa.api.messages.MessageEvent interface NotificationEvent : MessageEvent { diff --git a/api/src/main/kotlin/nebulosa/api/wheels/WheelEventHandler.kt b/api/src/main/kotlin/nebulosa/api/wheels/WheelEventHandler.kt index 88a5bb660..f9d9988e5 100644 --- a/api/src/main/kotlin/nebulosa/api/wheels/WheelEventHandler.kt +++ b/api/src/main/kotlin/nebulosa/api/wheels/WheelEventHandler.kt @@ -3,7 +3,7 @@ package nebulosa.api.wheels import io.reactivex.rxjava3.subjects.PublishSubject import jakarta.annotation.PostConstruct import nebulosa.api.beans.annotations.Subscriber -import nebulosa.api.services.MessageService +import nebulosa.api.messages.MessageService import nebulosa.indi.device.PropertyChangedEvent import nebulosa.indi.device.filterwheel.FilterWheel import nebulosa.indi.device.filterwheel.FilterWheelAttached diff --git a/api/src/main/kotlin/nebulosa/api/wheels/WheelMessageEvent.kt b/api/src/main/kotlin/nebulosa/api/wheels/WheelMessageEvent.kt index 9dce40456..dad365e72 100644 --- a/api/src/main/kotlin/nebulosa/api/wheels/WheelMessageEvent.kt +++ b/api/src/main/kotlin/nebulosa/api/wheels/WheelMessageEvent.kt @@ -1,6 +1,6 @@ package nebulosa.api.wheels -import nebulosa.api.services.DeviceMessageEvent +import nebulosa.api.messages.DeviceMessageEvent import nebulosa.indi.device.filterwheel.FilterWheel data class WheelMessageEvent( diff --git a/desktop/src/shared/types.ts b/desktop/src/shared/types.ts index 271e05b66..c23fdf4b6 100644 --- a/desktop/src/shared/types.ts +++ b/desktop/src/shared/types.ts @@ -496,11 +496,6 @@ export interface DARVEvent extends MessageEvent { direction?: GuideDirection } -export interface DARVInitialPauseElapsed extends DARVEvent { - pauseTime: number - state: 'INITIAL_PAUSE' -} - export interface CoordinateInterpolation { ma: number[] md: number[] diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/SimpleSplitStep.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/SimpleSplitStep.kt index 2b4dbd98a..ba17430ae 100644 --- a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/SimpleSplitStep.kt +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/SimpleSplitStep.kt @@ -1,6 +1,6 @@ package nebulosa.batch.processing -open class SimpleSplitStep : SplitStep, ArrayList { +open class SimpleSplitStep : SimpleFlowStep, SplitStep { constructor(initialCapacity: Int = 4) : super(initialCapacity) From 28c64ab54c3764ef8282ebb4ca67ac7df3b3af99 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Fri, 15 Dec 2023 00:10:38 -0300 Subject: [PATCH 31/87] [api][desktop]: Reimplement Camera Capture using Batch Processing --- .../api/alignment/polar/darv/DARVEvent.kt | 3 + .../api/alignment/polar/darv/DARVExecutor.kt | 5 +- .../api/alignment/polar/darv/DARVFinished.kt | 2 - .../polar/darv/DARVGuidePulseElapsed.kt | 5 +- .../polar/darv/DARVInitialPauseElapsed.kt | 2 - .../api/alignment/polar/darv/DARVJob.kt | 16 +-- .../api/alignment/polar/darv/DARVStarted.kt | 2 - .../api/cameras/CameraCaptureElapsed.kt | 11 -- .../api/cameras/CameraCaptureEvent.kt | 27 ++++- .../api/cameras/CameraCaptureExecutor.kt | 59 +++++----- .../api/cameras/CameraCaptureFinished.kt | 15 ++- .../api/cameras/CameraCaptureIsSettling.kt | 21 ++++ .../api/cameras/CameraCaptureIsWaiting.kt | 14 ++- .../nebulosa/api/cameras/CameraCaptureJob.kt | 107 ++++++++++++++++-- .../api/cameras/CameraCaptureStarted.kt | 15 ++- .../api/cameras/CameraCaptureState.kt | 11 ++ .../api/cameras/CameraExposureElapsed.kt | 14 ++- .../api/cameras/CameraExposureFinished.kt | 16 ++- .../api/cameras/CameraExposureStarted.kt | 14 ++- .../api/cameras/CameraExposureStep.kt | 50 +++++--- .../api/cameras/CameraLoopExposureStep.kt | 6 +- .../api/cameras/DelayAndWaitForSettleStep.kt | 54 +++++++++ .../api/guiding/DitherAfterExposureStep.kt | 17 ++- .../nebulosa/api/guiding/GuidePulseStep.kt | 2 +- .../api/guiding/WaitForSettleListener.kt | 10 ++ .../nebulosa/api/guiding/WaitForSettleStep.kt | 32 +++++- .../src/app/alignment/alignment.component.ts | 4 +- desktop/src/app/camera/camera.component.html | 16 +-- desktop/src/app/camera/camera.component.ts | 89 ++++----------- desktop/src/app/home/home.component.ts | 2 + desktop/src/app/image/image.component.ts | 4 +- .../src/shared/services/electron.service.ts | 13 +-- desktop/src/shared/types.ts | 80 +++++++------ .../batch/processing/AsyncJobLauncher.kt | 35 +++++- .../batch/processing/DefaultStepHandler.kt | 14 +-- .../nebulosa/batch/processing/JobExecution.kt | 11 +- .../nebulosa/batch/processing/JobLauncher.kt | 2 + .../nebulosa/batch/processing/SimpleJob.kt | 16 ++- .../batch/processing/delay/DelayStep.kt | 2 +- .../src/test/kotlin/BatchProcessingTest.kt | 4 +- .../common/concurrency/CancellationToken.kt | 60 ++++++++++ .../common/concurrency/CountUpDownLatch.kt | 8 +- nebulosa-guiding-phd2/build.gradle.kts | 1 - .../nebulosa/guiding/phd2/PHD2Guider.kt | 7 +- nebulosa-guiding/build.gradle.kts | 1 + .../main/kotlin/nebulosa/guiding/Guider.kt | 3 +- .../kotlin/nebulosa/phd2/client/PHD2Client.kt | 2 +- .../protocol/StellariumProtocolServer.kt | 2 +- 48 files changed, 607 insertions(+), 299 deletions(-) delete mode 100644 api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureElapsed.kt create mode 100644 api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureIsSettling.kt create mode 100644 api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureState.kt create mode 100644 api/src/main/kotlin/nebulosa/api/cameras/DelayAndWaitForSettleStep.kt create mode 100644 api/src/main/kotlin/nebulosa/api/guiding/WaitForSettleListener.kt create mode 100644 nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/CancellationToken.kt diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVEvent.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVEvent.kt index eb4102f31..7174f8cc8 100644 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVEvent.kt +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVEvent.kt @@ -19,4 +19,7 @@ sealed interface DARVEvent : MessageEvent { val direction: GuideDirection? val state: DARVState + + override val eventName + get() = "DARV_POLAR_ALIGNMENT_ELAPSED" } diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVExecutor.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVExecutor.kt index 8fa46af6c..359228dc9 100644 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVExecutor.kt +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVExecutor.kt @@ -54,12 +54,11 @@ class DARVExecutor( @Synchronized fun stop(camera: Camera, guideOutput: GuideOutput) { val jobExecution = findJobExecution(camera, guideOutput) ?: return - jobExecution.stop() + jobLauncher.stop(jobExecution) } fun isRunning(camera: Camera, guideOutput: GuideOutput): Boolean { - val jobExecution = findJobExecution(camera, guideOutput) ?: return false - return !jobExecution.isDone + return findJobExecution(camera, guideOutput) != null } override fun accept(event: MessageEvent) { diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVFinished.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVFinished.kt index d92ce9f15..0748956e8 100644 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVFinished.kt +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVFinished.kt @@ -13,6 +13,4 @@ data class DARVFinished( override val progress = 0.0 override val state = DARVState.IDLE override val direction = null - - override val eventName = "DARV_POLAR_ALIGNMENT_ELAPSED" } diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVGuidePulseElapsed.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVGuidePulseElapsed.kt index ce9d5e6f9..580cb0afc 100644 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVGuidePulseElapsed.kt +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVGuidePulseElapsed.kt @@ -13,7 +13,4 @@ data class DARVGuidePulseElapsed( override val progress: Double, override val direction: GuideDirection, override val state: DARVState, -) : MessageEvent, DARVEvent { - - override val eventName = "DARV_POLAR_ALIGNMENT_ELAPSED" -} +) : MessageEvent, DARVEvent diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVInitialPauseElapsed.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVInitialPauseElapsed.kt index da1a1ecc0..cc17a87a1 100644 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVInitialPauseElapsed.kt +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVInitialPauseElapsed.kt @@ -13,6 +13,4 @@ data class DARVInitialPauseElapsed( override val state = DARVState.INITIAL_PAUSE override val direction = null - - override val eventName = "DARV_POLAR_ALIGNMENT_ELAPSED" } diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVJob.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVJob.kt index 69a360274..16f6551fd 100644 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVJob.kt +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVJob.kt @@ -56,34 +56,30 @@ data class DARVJob( } override fun beforeJob(jobExecution: JobExecution) { - val job = jobExecution.job as DARVJob - onNext(DARVStarted(job.camera, job.guideOutput, job.request.initialPause, job.direction)) + onNext(DARVStarted(camera, guideOutput, request.initialPause, direction)) } override fun afterJob(jobExecution: JobExecution) { - val job = jobExecution.job as DARVJob - onNext(DARVFinished(job.camera, job.guideOutput)) + onNext(DARVFinished(camera, guideOutput)) } override fun onExposureFinished(step: CameraExposureStep, stepExecution: StepExecution) { val savePath = stepExecution.context[CameraExposureStep.SAVE_PATH] as Path - onNext(CameraExposureFinished(step.camera, 1.0, savePath)) + onNext(CameraExposureFinished(step.camera, 1, 1, Duration.ZERO, 1.0, Duration.ZERO, savePath)) } override fun onGuidePulseElapsed(step: GuidePulseStep, stepExecution: StepExecution) { - val job = stepExecution.jobExecution.job as DARVJob val direction = step.request.direction val remainingTime = stepExecution.context[DelayStep.REMAINING_TIME] as Duration val progress = stepExecution.context[DelayStep.PROGRESS] as Double - val state = if (direction == job.direction) DARVState.FORWARD else DARVState.BACKWARD - onNext(DARVGuidePulseElapsed(job.camera, job.guideOutput, remainingTime, progress, direction, state)) + val state = if (direction == this.direction) DARVState.FORWARD else DARVState.BACKWARD + onNext(DARVGuidePulseElapsed(camera, guideOutput, remainingTime, progress, direction, state)) } override fun onDelayElapsed(step: DelayStep, stepExecution: StepExecution) { - val job = stepExecution.jobExecution.job as DARVJob val remainingTime = stepExecution.context[DelayStep.REMAINING_TIME] as Duration val progress = stepExecution.context[DelayStep.PROGRESS] as Double - onNext(DARVInitialPauseElapsed(job.camera, job.guideOutput, remainingTime, progress)) + onNext(DARVInitialPauseElapsed(camera, guideOutput, remainingTime, progress)) } override fun stop(mayInterruptIfRunning: Boolean) { diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVStarted.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVStarted.kt index feaab9da0..066e6f704 100644 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVStarted.kt +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVStarted.kt @@ -14,6 +14,4 @@ data class DARVStarted( override val progress = 0.0 override val state = DARVState.INITIAL_PAUSE - - override val eventName = "DARV_POLAR_ALIGNMENT_ELAPSED" } diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureElapsed.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureElapsed.kt deleted file mode 100644 index b5d2fc679..000000000 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureElapsed.kt +++ /dev/null @@ -1,11 +0,0 @@ -package nebulosa.api.cameras - -import nebulosa.indi.device.camera.Camera - -data class CameraCaptureElapsed( - override val camera: Camera, - override val progress: Double, -) : CameraCaptureEvent { - - override val eventName = "CAMERA_CAPTURE_ELAPSED" -} diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureEvent.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureEvent.kt index f727ecd72..99d8d98b2 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureEvent.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureEvent.kt @@ -2,10 +2,35 @@ package nebulosa.api.cameras import nebulosa.api.messages.MessageEvent import nebulosa.indi.device.camera.Camera +import java.nio.file.Path +import java.time.Duration sealed interface CameraCaptureEvent : MessageEvent { val camera: Camera - val progress: Double + val state: CameraCaptureState + + val exposureAmount: Int + + val exposureCount: Int + + val captureElapsedTime: Duration + + val captureProgress: Double + + val captureRemainingTime: Duration + + val exposureProgress: Double + + val exposureRemainingTime: Duration + + val waitRemainingTime: Duration + + val waitProgress: Double + + val savePath: Path? + + override val eventName + get() = "CAMERA_CAPTURE_ELAPSED" } diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureExecutor.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureExecutor.kt index 43593c5a6..fcfd67088 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureExecutor.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureExecutor.kt @@ -1,70 +1,67 @@ package nebulosa.api.cameras +import io.reactivex.rxjava3.functions.Consumer +import nebulosa.api.messages.MessageEvent import nebulosa.api.messages.MessageService import nebulosa.batch.processing.JobExecution import nebulosa.batch.processing.JobLauncher -import nebulosa.batch.processing.StepExecution import nebulosa.guiding.Guider import nebulosa.indi.device.camera.Camera import nebulosa.log.loggerFor import org.springframework.stereotype.Component +import java.util.* @Component class CameraCaptureExecutor( private val messageService: MessageService, private val guider: Guider, - private val asyncJobLauncher: JobLauncher, -) : CameraCaptureListener { + private val jobLauncher: JobLauncher, +) : Consumer { - private val jobExecutions = HashMap(4) + private val jobExecutions = LinkedList() @Synchronized fun execute(request: CameraStartCaptureRequest) { val camera = requireNotNull(request.camera) - check(!isCapturing(camera)) { "job is already running for camera: [${camera.name}]" } check(camera.connected) { "camera is not connected" } + check(!isCapturing(camera)) { "job is already running for camera: [${camera.name}]" } - LOG.info("starting camera capture. data={}", request) + LOG.info("starting camera capture. request={}", request) val cameraCaptureJob = CameraCaptureJob(request, guider) - cameraCaptureJob.registerListener(this) - val jobExecution = asyncJobLauncher.launch(cameraCaptureJob) - jobExecutions[camera] = jobExecution - } - - fun stop(camera: Camera) { - val jobExecution = jobExecutions[camera] ?: return - jobExecution.stop() + cameraCaptureJob.subscribe(this) + val jobExecution = jobLauncher.launch(cameraCaptureJob) + jobExecutions.add(jobExecution) } - fun isCapturing(camera: Camera): Boolean { - val jobExecution = jobExecutions[camera] ?: return false - return !jobExecution.isDone - } + fun findJobExecution(camera: Camera): JobExecution? { + for (i in jobExecutions.indices.reversed()) { + val jobExecution = jobExecutions[i] + val job = jobExecution.job as CameraCaptureJob - override fun onCaptureStarted(step: CameraExposureStep, jobExecution: JobExecution) { - // TODO: messageService.sendMessage(CameraCaptureStarted(step.request.camera!!, jobExecution)) - } + if (!jobExecution.isDone && job.camera === camera) { + return jobExecution + } + } - override fun onExposureStarted(step: CameraExposureStep, stepExecution: StepExecution) { - // TODO: messageService.sendMessage(CameraExposureStarted(step.request.camera!!, stepExecution)) + return null } - override fun onExposureElapsed(step: CameraExposureStep, stepExecution: StepExecution) { - // TODO: messageService.sendMessage(CameraExposureElapsed(step.request.camera!!, stepExecution)) + @Synchronized + fun stop(camera: Camera) { + val jobExecution = findJobExecution(camera) ?: return + jobLauncher.stop(jobExecution) } - override fun onExposureFinished(step: CameraExposureStep, stepExecution: StepExecution) { - // TODO: messageService.sendMessage(CameraExposureFinished(step.request.camera!!, stepExecution)) + fun isCapturing(camera: Camera): Boolean { + return findJobExecution(camera) != null } - override fun onCaptureFinished(step: CameraExposureStep, jobExecution: JobExecution) { - // TODO: messageService.sendMessage(CameraCaptureFinished(step.request.camera!!, jobExecution)) + override fun accept(event: MessageEvent) { + messageService.sendMessage(event) } - // TODO: CameraCaptureIsWaiting - companion object { @JvmStatic private val LOG = loggerFor() diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureFinished.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureFinished.kt index 658e8ab3c..5ed7a736a 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureFinished.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureFinished.kt @@ -1,12 +1,21 @@ package nebulosa.api.cameras import nebulosa.indi.device.camera.Camera +import java.time.Duration data class CameraCaptureFinished( override val camera: Camera, + override val exposureAmount: Int, + override val captureElapsedTime: Duration, ) : CameraCaptureEvent { - override val progress = 1.0 - - override val eventName = "CAMERA_CAPTURE_FINISHED" + override val exposureCount = exposureAmount + override val captureProgress = 1.0 + override val captureRemainingTime = Duration.ZERO!! + override val exposureProgress = 1.0 + override val exposureRemainingTime = Duration.ZERO!! + override val state = CameraCaptureState.CAPTURE_FINISHED + override val waitRemainingTime = Duration.ZERO!! + override val waitProgress = 0.0 + override val savePath = null } diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureIsSettling.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureIsSettling.kt new file mode 100644 index 000000000..1595cb5c9 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureIsSettling.kt @@ -0,0 +1,21 @@ +package nebulosa.api.cameras + +import nebulosa.indi.device.camera.Camera +import java.time.Duration + +data class CameraCaptureIsSettling( + override val camera: Camera, + override val exposureAmount: Int, + override val exposureCount: Int, + override val captureElapsedTime: Duration, + override val captureProgress: Double, + override val captureRemainingTime: Duration, +) : CameraCaptureEvent { + + override val state = CameraCaptureState.WAITING + override val exposureProgress = 1.0 + override val exposureRemainingTime = Duration.ZERO!! + override val savePath = null + override val waitProgress = 0.0 + override val waitRemainingTime = Duration.ZERO!! +} diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureIsWaiting.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureIsWaiting.kt index c7f6f700a..0da124e81 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureIsWaiting.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureIsWaiting.kt @@ -1,11 +1,21 @@ package nebulosa.api.cameras import nebulosa.indi.device.camera.Camera +import java.time.Duration data class CameraCaptureIsWaiting( override val camera: Camera, - override val progress: Double, + override val exposureAmount: Int, + override val exposureCount: Int, + override val captureElapsedTime: Duration, + override val captureProgress: Double, + override val captureRemainingTime: Duration, + override val waitProgress: Double, + override val waitRemainingTime: Duration, ) : CameraCaptureEvent { - override val eventName = "CAMERA_CAPTURE_WAITING" + override val state = CameraCaptureState.WAITING + override val exposureProgress = 1.0 + override val exposureRemainingTime = Duration.ZERO!! + override val savePath = null } diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureJob.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureJob.kt index b21defcd1..112f433fb 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureJob.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureJob.kt @@ -1,30 +1,42 @@ package nebulosa.api.cameras +import io.reactivex.rxjava3.subjects.PublishSubject import nebulosa.api.guiding.DitherAfterExposureStep import nebulosa.api.guiding.WaitForSettleStep +import nebulosa.api.messages.MessageEvent +import nebulosa.batch.processing.JobExecution +import nebulosa.batch.processing.PublishSubscribe import nebulosa.batch.processing.SimpleJob -import nebulosa.batch.processing.SimpleSplitStep +import nebulosa.batch.processing.StepExecution import nebulosa.batch.processing.delay.DelayStep +import nebulosa.common.concurrency.Incrementer import nebulosa.guiding.Guider +import java.nio.file.Path +import java.time.Duration data class CameraCaptureJob( private val request: CameraStartCaptureRequest, private val guider: Guider, -) : SimpleJob() { +) : SimpleJob(), PublishSubscribe, CameraCaptureListener { + + @JvmField val camera = requireNotNull(request.camera) private val cameraExposureStep = if (request.isLoop) CameraLoopExposureStep(request) else CameraExposureStep(request) - override val id = "CameraCapture.Job.${System.currentTimeMillis()}" + override val id = "CameraCapture.Job.${ID.increment()}" + + override val subject = PublishSubject.create() init { if (cameraExposureStep is CameraExposureStep) { val waitForSettleStep = WaitForSettleStep(guider) - val ditherStep = DitherAfterExposureStep(request.dither) + val ditherStep = DitherAfterExposureStep(request.dither, guider) val cameraDelayStep = DelayStep(request.exposureDelay) - val delayAndWaitForSettleStep = SimpleSplitStep(cameraDelayStep, waitForSettleStep) + val delayAndWaitForSettleStep = DelayAndWaitForSettleStep(camera, cameraDelayStep, waitForSettleStep) cameraDelayStep.registerDelayStepListener(cameraExposureStep) + delayAndWaitForSettleStep.subscribe(this) add(waitForSettleStep) add(cameraExposureStep) @@ -37,13 +49,90 @@ data class CameraCaptureJob( } else { add(cameraExposureStep) } + + cameraExposureStep.registerCameraCaptureListener(this) + } + + override fun onCaptureStarted(step: CameraExposureStep, jobExecution: JobExecution) { + onNext(CameraCaptureStarted(step.camera, step.exposureAmount, step.estimatedCaptureTime, step.exposureTime)) + } + + override fun onExposureStarted(step: CameraExposureStep, stepExecution: StepExecution) { + sendCameraExposureEvent(step, stepExecution, CameraCaptureState.EXPOSURE_STARTED) + } + + override fun onExposureElapsed(step: CameraExposureStep, stepExecution: StepExecution) { + val waiting = stepExecution.context[CameraExposureStep.CAPTURE_WAITING] as Boolean + val state = if (waiting) CameraCaptureState.WAITING else CameraCaptureState.EXPOSURING + sendCameraExposureEvent(step, stepExecution, state) + } + + override fun onExposureFinished(step: CameraExposureStep, stepExecution: StepExecution) { + sendCameraExposureEvent(step, stepExecution, CameraCaptureState.EXPOSURE_FINISHED) + } + + override fun onCaptureFinished(step: CameraExposureStep, jobExecution: JobExecution) { + val captureElapsedTime = jobExecution.context[CameraExposureStep.CAPTURE_ELAPSED_TIME] as Duration + onNext(CameraCaptureFinished(step.camera, step.exposureAmount, captureElapsedTime)) } - fun registerListener(listener: CameraCaptureListener) { - cameraExposureStep.registerCameraCaptureListener(listener) + fun sendCameraExposureEvent(step: CameraExposureStep, stepExecution: StepExecution, state: CameraCaptureState) { + val exposureCount = stepExecution.context[CameraExposureStep.EXPOSURE_COUNT] as Int + val captureElapsedTime = stepExecution.context[CameraExposureStep.CAPTURE_ELAPSED_TIME] as Duration + val captureProgress = stepExecution.context[CameraExposureStep.CAPTURE_PROGRESS] as Double + val captureRemainingTime = stepExecution.context[CameraExposureStep.CAPTURE_REMAINING_TIME] as Duration + + val event = when (state) { + CameraCaptureState.WAITING -> { + val waitProgress = stepExecution.context[DelayStep.PROGRESS] as Double + val waitRemainingTime = stepExecution.context[DelayStep.REMAINING_TIME] as Duration + + CameraCaptureIsWaiting( + step.camera, + step.exposureAmount, exposureCount, captureElapsedTime, captureProgress, captureRemainingTime, + waitProgress, waitRemainingTime + ) + } + CameraCaptureState.SETTLING -> { + CameraCaptureIsSettling(step.camera, step.exposureAmount, exposureCount, captureElapsedTime, captureProgress, captureRemainingTime) + } + CameraCaptureState.EXPOSURING -> { + val exposureProgress = stepExecution.context[CameraExposureStep.EXPOSURE_PROGRESS] as Double + val exposureRemainingTime = stepExecution.context[CameraExposureStep.EXPOSURE_REMAINING_TIME] as Duration + + CameraExposureElapsed( + step.camera, + step.exposureAmount, exposureCount, captureElapsedTime, captureProgress, captureRemainingTime, + exposureProgress, exposureRemainingTime + ) + } + CameraCaptureState.EXPOSURE_STARTED -> { + val exposureRemainingTime = stepExecution.context[CameraExposureStep.EXPOSURE_REMAINING_TIME] as Duration + + CameraExposureStarted( + step.camera, + step.exposureAmount, exposureCount, captureElapsedTime, + captureProgress, captureRemainingTime, exposureRemainingTime + ) + } + CameraCaptureState.EXPOSURE_FINISHED -> { + val savePath = stepExecution.context[CameraExposureStep.SAVE_PATH] as Path + + CameraExposureFinished( + step.camera, + step.exposureAmount, exposureCount, + captureElapsedTime, captureProgress, captureRemainingTime, + savePath + ) + } + else -> return + } + + onNext(event) } - fun unregisterListener(listener: CameraCaptureListener) { - cameraExposureStep.unregisterCameraCaptureListener(listener) + companion object { + + @JvmStatic private val ID = Incrementer() } } diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureStarted.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureStarted.kt index bb95a9af8..8976d5f54 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureStarted.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureStarted.kt @@ -1,12 +1,21 @@ package nebulosa.api.cameras import nebulosa.indi.device.camera.Camera +import java.time.Duration data class CameraCaptureStarted( override val camera: Camera, + override val exposureAmount: Int, + override val captureRemainingTime: Duration, + override val exposureRemainingTime: Duration, ) : CameraCaptureEvent { - override val progress = 0.0 - - override val eventName = "CAMERA_CAPTURE_STARTED" + override val exposureCount = 1 + override val captureElapsedTime = Duration.ZERO!! + override val captureProgress = 0.0 + override val exposureProgress = 0.0 + override val state = CameraCaptureState.CAPTURE_STARTED + override val waitRemainingTime = Duration.ZERO!! + override val waitProgress = 0.0 + override val savePath = null } diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureState.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureState.kt new file mode 100644 index 000000000..b64506b33 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureState.kt @@ -0,0 +1,11 @@ +package nebulosa.api.cameras + +enum class CameraCaptureState { + CAPTURE_STARTED, + EXPOSURE_STARTED, + EXPOSURING, + WAITING, + SETTLING, + EXPOSURE_FINISHED, + CAPTURE_FINISHED, +} diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureElapsed.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureElapsed.kt index 30ba54c9e..1b0810160 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureElapsed.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureElapsed.kt @@ -1,11 +1,21 @@ package nebulosa.api.cameras import nebulosa.indi.device.camera.Camera +import java.time.Duration data class CameraExposureElapsed( override val camera: Camera, - override val progress: Double, + override val exposureAmount: Int, + override val exposureCount: Int, + override val captureElapsedTime: Duration, + override val captureProgress: Double, + override val captureRemainingTime: Duration, + override val exposureProgress: Double, + override val exposureRemainingTime: Duration, ) : CameraExposureEvent { - override val eventName = "CAMERA_EXPOSURE_ELAPSED" + override val state = CameraCaptureState.EXPOSURING + override val waitProgress = 0.0 + override val waitRemainingTime = Duration.ZERO!! + override val savePath = null } diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureFinished.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureFinished.kt index 793bb4dd8..c666488f7 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureFinished.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureFinished.kt @@ -2,12 +2,22 @@ package nebulosa.api.cameras import nebulosa.indi.device.camera.Camera import java.nio.file.Path +import java.time.Duration data class CameraExposureFinished( override val camera: Camera, - override val progress: Double, - val savePath: Path, + override val exposureAmount: Int, + override val exposureCount: Int, + override val captureElapsedTime: Duration, + override val captureProgress: Double, + override val captureRemainingTime: Duration, + override val savePath: Path, ) : CameraExposureEvent { - override val eventName = "CAMERA_EXPOSURE_FINISHED" + override val state = CameraCaptureState.EXPOSURE_FINISHED + override val exposureProgress = 0.0 + override val exposureRemainingTime = Duration.ZERO!! + override val waitProgress = 0.0 + override val waitRemainingTime = Duration.ZERO!! } + diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureStarted.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureStarted.kt index fd70ed728..8647f5ac7 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureStarted.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureStarted.kt @@ -1,11 +1,21 @@ package nebulosa.api.cameras import nebulosa.indi.device.camera.Camera +import java.time.Duration data class CameraExposureStarted( override val camera: Camera, - override val progress: Double, + override val exposureAmount: Int, + override val exposureCount: Int, + override val captureElapsedTime: Duration, + override val captureProgress: Double, + override val captureRemainingTime: Duration, + override val exposureRemainingTime: Duration, ) : CameraExposureEvent { - override val eventName = "CAMERA_EXPOSURE_STARTED" + override val state = CameraCaptureState.EXPOSURE_STARTED + override val exposureProgress = 0.0 + override val waitProgress = 0.0 + override val waitRemainingTime = Duration.ZERO!! + override val savePath = null } diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureStep.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureStep.kt index 785de82bc..7c1746df6 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureStep.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureStep.kt @@ -24,19 +24,20 @@ data class CameraExposureStep(override val request: CameraStartCaptureRequest) : @JvmField val camera = requireNotNull(request.camera) + @JvmField val exposureTime = request.exposureTime + @JvmField val exposureAmount = request.exposureAmount + @JvmField val exposureDelay = request.exposureDelay + + @JvmField val estimatedCaptureTime: Duration = if (request.isLoop) Duration.ZERO + else Duration.ofNanos(exposureTime.toNanos() * exposureAmount + exposureDelay.toNanos() * (exposureAmount - 1)) + private val latch = CountUpDownLatch() - private val listeners = HashSet() + private val listeners = LinkedHashSet() @Volatile private var aborted = false @Volatile private var exposureCount = 0 @Volatile private var captureElapsedTime = Duration.ZERO!! - private val exposureTime = request.exposureTime - private val exposureDelay = request.exposureDelay - - private val estimatedTime = if (request.isLoop) Duration.ZERO - else Duration.ofNanos(exposureTime.toNanos() * request.exposureAmount + exposureDelay.toNanos() * (request.exposureAmount - 1)) - private lateinit var stepExecution: StepExecution override fun registerCameraCaptureListener(listener: CameraCaptureListener): Boolean { @@ -53,7 +54,6 @@ data class CameraExposureStep(override val request: CameraStartCaptureRequest) : when (event) { is CameraFrameCaptured -> { save(event.fits) - latch.countDown() } is CameraExposureAborted, is CameraExposureFailed, @@ -74,25 +74,34 @@ data class CameraExposureStep(override val request: CameraStartCaptureRequest) : override fun beforeJob(jobExecution: JobExecution) { camera.enableBlob() EventBus.getDefault().register(this) - listeners.forEach { it.onCaptureStarted(this, jobExecution) } + captureElapsedTime = Duration.ZERO + + jobExecution.context[CAPTURE_ELAPSED_TIME] = Duration.ZERO + jobExecution.context[CAPTURE_PROGRESS] = 0.0 + jobExecution.context[CAPTURE_REMAINING_TIME] = exposureTime + jobExecution.context[EXPOSURE_ELAPSED_TIME] = Duration.ZERO + jobExecution.context[EXPOSURE_REMAINING_TIME] = estimatedCaptureTime + jobExecution.context[EXPOSURE_PROGRESS] = 0.0 + + listeners.forEach { it.onCaptureStarted(this, jobExecution) } } override fun afterJob(jobExecution: JobExecution) { camera.disableBlob() EventBus.getDefault().unregister(this) listeners.forEach { it.onCaptureFinished(this, jobExecution) } + listeners.clear() } override fun execute(stepExecution: StepExecution): StepResult { this.stepExecution = stepExecution - LOG.info("starting exposure. camera=${camera.name}") executeCapture(stepExecution) return StepResult.FINISHED } override fun stop(mayInterruptIfRunning: Boolean) { - LOG.info("stopping exposure. camera=${camera.name}") + LOG.info("stopping camera exposure. camera={}", camera) camera.abortCapture() camera.disableBlob() aborted = true @@ -100,6 +109,7 @@ data class CameraExposureStep(override val request: CameraStartCaptureRequest) : } override fun onDelayElapsed(step: DelayStep, stepExecution: StepExecution) { + stepExecution.context[CAPTURE_WAITING] = true val waitTime = stepExecution.context[DelayStep.WAIT_TIME] as Duration captureElapsedTime += waitTime onCameraExposureElapsed(Duration.ZERO, Duration.ZERO, 1.0) @@ -108,9 +118,12 @@ data class CameraExposureStep(override val request: CameraStartCaptureRequest) : private fun executeCapture(stepExecution: StepExecution) { if (camera.connected && !aborted) { synchronized(camera) { + LOG.info("starting camera exposure. camera={}", camera) + latch.countUp() - stepExecution.context[EXPOSURE_AMOUNT] = ++exposureCount + stepExecution.context[CAPTURE_WAITING] = false + stepExecution.context[EXPOSURE_COUNT] = ++exposureCount listeners.forEach { it.onExposureStarted(this, stepExecution) } @@ -129,7 +142,7 @@ data class CameraExposureStep(override val request: CameraStartCaptureRequest) : captureElapsedTime += exposureTime - LOG.info("camera exposure finished") + LOG.info("camera exposure finished. aborted={}, camera={}", aborted, camera) } } } @@ -145,7 +158,7 @@ data class CameraExposureStep(override val request: CameraStartCaptureRequest) : } try { - LOG.info("saving FITS at $savePath...") + LOG.info("saving FITS. path={}", savePath) savePath.createParentDirectories() stream.transferAndClose(savePath.outputStream()) @@ -156,6 +169,8 @@ data class CameraExposureStep(override val request: CameraStartCaptureRequest) : } catch (e: Throwable) { LOG.error("failed to save FITS", e) aborted = true + } finally { + latch.countDown() } } @@ -165,8 +180,8 @@ data class CameraExposureStep(override val request: CameraStartCaptureRequest) : var captureProgress = 0.0 if (!request.isLoop) { - captureRemainingTime = if (estimatedTime > captureElapsedTime) estimatedTime - captureElapsedTime else Duration.ZERO - captureProgress = (estimatedTime - captureRemainingTime).toNanos().toDouble() / estimatedTime.toNanos() + captureRemainingTime = if (estimatedCaptureTime > captureElapsedTime) estimatedCaptureTime - captureElapsedTime else Duration.ZERO + captureProgress = (estimatedCaptureTime - captureRemainingTime).toNanos().toDouble() / estimatedCaptureTime.toNanos() } stepExecution.context[EXPOSURE_ELAPSED_TIME] = elapsedTime @@ -181,7 +196,7 @@ data class CameraExposureStep(override val request: CameraStartCaptureRequest) : companion object { - const val EXPOSURE_AMOUNT = "CAMERA_EXPOSURE.EXPOSURE_AMOUNT" + const val EXPOSURE_COUNT = "CAMERA_EXPOSURE.EXPOSURE_COUNT" const val SAVE_PATH = "CAMERA_EXPOSURE.SAVE_PATH" const val EXPOSURE_ELAPSED_TIME = "CAMERA_EXPOSURE.EXPOSURE_ELAPSED_TIME" const val EXPOSURE_REMAINING_TIME = "CAMERA_EXPOSURE.EXPOSURE_REMAINING_TIME" @@ -189,6 +204,7 @@ data class CameraExposureStep(override val request: CameraStartCaptureRequest) : const val CAPTURE_ELAPSED_TIME = "CAMERA_EXPOSURE.CAPTURE_ELAPSED_TIME" const val CAPTURE_REMAINING_TIME = "CAMERA_EXPOSURE.CAPTURE_REMAINING_TIME" const val CAPTURE_PROGRESS = "CAMERA_EXPOSURE.CAPTURE_PROGRESS" + const val CAPTURE_WAITING = "CAMERA_EXPOSURE.WAITING" @JvmStatic private val LOG = loggerFor() @JvmStatic private val DATE_TIME_FORMAT = DateTimeFormatter.ofPattern("yyyyMMdd.HHmmssSSS") diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraLoopExposureStep.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraLoopExposureStep.kt index 2ff9cb865..92cf7bd23 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraLoopExposureStep.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraLoopExposureStep.kt @@ -31,15 +31,17 @@ data class CameraLoopExposureStep( } override fun stop(mayInterruptIfRunning: Boolean) { - cameraExposureStep.stop() - delayStep.stop() + cameraExposureStep.stop(mayInterruptIfRunning) + delayStep.stop(mayInterruptIfRunning) } override fun beforeJob(jobExecution: JobExecution) { cameraExposureStep.beforeJob(jobExecution) + delayStep.beforeJob(jobExecution) } override fun afterJob(jobExecution: JobExecution) { cameraExposureStep.afterJob(jobExecution) + delayStep.afterJob(jobExecution) } } diff --git a/api/src/main/kotlin/nebulosa/api/cameras/DelayAndWaitForSettleStep.kt b/api/src/main/kotlin/nebulosa/api/cameras/DelayAndWaitForSettleStep.kt new file mode 100644 index 000000000..6dd8e714b --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/cameras/DelayAndWaitForSettleStep.kt @@ -0,0 +1,54 @@ +package nebulosa.api.cameras + +import io.reactivex.rxjava3.subjects.PublishSubject +import nebulosa.api.guiding.WaitForSettleListener +import nebulosa.api.guiding.WaitForSettleStep +import nebulosa.batch.processing.PublishSubscribe +import nebulosa.batch.processing.SimpleSplitStep +import nebulosa.batch.processing.StepExecution +import nebulosa.batch.processing.delay.DelayStep +import nebulosa.batch.processing.delay.DelayStepListener +import nebulosa.indi.device.camera.Camera +import java.time.Duration + +data class DelayAndWaitForSettleStep( + @JvmField val camera: Camera, + @JvmField val cameraDelayStep: DelayStep, + @JvmField val waitForSettleStep: WaitForSettleStep, +) : SimpleSplitStep(cameraDelayStep, waitForSettleStep), PublishSubscribe, DelayStepListener, WaitForSettleListener { + + @Volatile private var settling = false + + override val subject = PublishSubject.create() + + override fun beforeStep(stepExecution: StepExecution) { + cameraDelayStep.registerDelayStepListener(this) + waitForSettleStep.registerWaitForSettleListener(this) + } + + override fun afterStep(stepExecution: StepExecution) { + cameraDelayStep.unregisterDelayStepListener(this) + waitForSettleStep.unregisterWaitForSettleListener(this) + } + + override fun onSettleStarted(step: WaitForSettleStep, stepExecution: StepExecution) { + settling = true + } + + override fun onSettleFinished(step: WaitForSettleStep, stepExecution: StepExecution) { + settling = false + } + + override fun onDelayElapsed(step: DelayStep, stepExecution: StepExecution) { + val send = settling && (stepExecution.context[DelayStep.PROGRESS] as Double) < 1.0 + + if (send) { + val exposureCount = stepExecution.context[CameraExposureStep.EXPOSURE_COUNT] as Int + val captureElapsedTime = stepExecution.context[CameraExposureStep.CAPTURE_ELAPSED_TIME] as Duration + val captureProgress = stepExecution.context[CameraExposureStep.CAPTURE_PROGRESS] as Double + val captureRemainingTime = stepExecution.context[CameraExposureStep.CAPTURE_REMAINING_TIME] as Duration + + onNext(CameraCaptureIsSettling(camera, 0, exposureCount, captureElapsedTime, captureProgress, captureRemainingTime)) + } + } +} diff --git a/api/src/main/kotlin/nebulosa/api/guiding/DitherAfterExposureStep.kt b/api/src/main/kotlin/nebulosa/api/guiding/DitherAfterExposureStep.kt index 8f8b21492..073e66c72 100644 --- a/api/src/main/kotlin/nebulosa/api/guiding/DitherAfterExposureStep.kt +++ b/api/src/main/kotlin/nebulosa/api/guiding/DitherAfterExposureStep.kt @@ -7,19 +7,18 @@ import nebulosa.common.concurrency.CountUpDownLatch import nebulosa.guiding.GuideState import nebulosa.guiding.Guider import nebulosa.guiding.GuiderListener -import org.springframework.beans.factory.annotation.Autowired -import java.util.concurrent.atomic.AtomicInteger -data class DitherAfterExposureStep(@JvmField val request: DitherAfterExposureRequest) : Step, GuiderListener { - - @Autowired private lateinit var guider: Guider +data class DitherAfterExposureStep( + @JvmField val request: DitherAfterExposureRequest, + @JvmField val guider: Guider, +) : Step, GuiderListener { private val ditherLatch = CountUpDownLatch() - private val exposureCount = AtomicInteger() + @Volatile private var exposureCount = 0 override fun execute(stepExecution: StepExecution): StepResult { if (guider.canDither && request.enabled && guider.state == GuideState.GUIDING) { - if (exposureCount.get() < request.afterExposures) { + if (exposureCount < request.afterExposures) { try { guider.registerGuiderListener(this) ditherLatch.countUp() @@ -30,8 +29,8 @@ data class DitherAfterExposureStep(@JvmField val request: DitherAfterExposureReq } } - if (exposureCount.incrementAndGet() >= request.afterExposures) { - exposureCount.set(0) + if (++exposureCount >= request.afterExposures) { + exposureCount = 0 } } diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseStep.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseStep.kt index 23ddb701a..f501b6a1c 100644 --- a/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseStep.kt +++ b/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseStep.kt @@ -11,7 +11,7 @@ import java.time.Duration data class GuidePulseStep(@JvmField val request: GuidePulseRequest) : Step, DelayStepListener { - private val listeners = HashSet() + private val listeners = LinkedHashSet() private val delayStep = DelayStep(request.duration) init { diff --git a/api/src/main/kotlin/nebulosa/api/guiding/WaitForSettleListener.kt b/api/src/main/kotlin/nebulosa/api/guiding/WaitForSettleListener.kt new file mode 100644 index 000000000..fb289c0b1 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/guiding/WaitForSettleListener.kt @@ -0,0 +1,10 @@ +package nebulosa.api.guiding + +import nebulosa.batch.processing.StepExecution + +interface WaitForSettleListener { + + fun onSettleStarted(step: WaitForSettleStep, stepExecution: StepExecution) + + fun onSettleFinished(step: WaitForSettleStep, stepExecution: StepExecution) +} diff --git a/api/src/main/kotlin/nebulosa/api/guiding/WaitForSettleStep.kt b/api/src/main/kotlin/nebulosa/api/guiding/WaitForSettleStep.kt index 67aea4cca..3765bee65 100644 --- a/api/src/main/kotlin/nebulosa/api/guiding/WaitForSettleStep.kt +++ b/api/src/main/kotlin/nebulosa/api/guiding/WaitForSettleStep.kt @@ -1,17 +1,37 @@ package nebulosa.api.guiding -import nebulosa.batch.processing.Step -import nebulosa.batch.processing.StepExecution -import nebulosa.batch.processing.StepResult +import nebulosa.batch.processing.* +import nebulosa.common.concurrency.CancellationToken import nebulosa.guiding.Guider -data class WaitForSettleStep(private val guider: Guider) : Step { +data class WaitForSettleStep(@JvmField val guider: Guider) : Step, JobExecutionListener { + + private val cancellationToken = CancellationToken() + private val listeners = LinkedHashSet() + + fun registerWaitForSettleListener(listener: WaitForSettleListener) { + listeners.add(listener) + } + + fun unregisterWaitForSettleListener(listener: WaitForSettleListener) { + listeners.remove(listener) + } override fun execute(stepExecution: StepExecution): StepResult { - if (guider.isSettling) { - guider.waitForSettle() + if (guider.isSettling && !cancellationToken.isCancelled) { + listeners.forEach { it.onSettleStarted(this, stepExecution) } + guider.waitForSettle(cancellationToken) + listeners.forEach { it.onSettleFinished(this, stepExecution) } } return StepResult.FINISHED } + + override fun stop(mayInterruptIfRunning: Boolean) { + cancellationToken.cancel() + } + + override fun afterJob(jobExecution: JobExecution) { + listeners.clear() + } } diff --git a/desktop/src/app/alignment/alignment.component.ts b/desktop/src/app/alignment/alignment.component.ts index 83592eb9b..038f5e398 100644 --- a/desktop/src/app/alignment/alignment.component.ts +++ b/desktop/src/app/alignment/alignment.component.ts @@ -79,8 +79,10 @@ export class AlignmentComponent implements AfterViewInit, OnDestroy { this.darvProgress = event.progress this.darvInProgress = event.remainingTime > 0 - if (event.state !== 'INITIAL_PAUSE') { + if (event.state === 'FORWARD' || event.state === 'BACKWARD') { this.darvDirection = event.direction + } else { + this.darvDirection = undefined } }) } diff --git a/desktop/src/app/camera/camera.component.html b/desktop/src/app/camera/camera.component.html index eedad2166..5cfaac891 100644 --- a/desktop/src/app/camera/camera.component.html +++ b/desktop/src/app/camera/camera.component.html @@ -38,16 +38,12 @@ {{ waiting ? 'waiting' : capturing ? 'capturing' : 'idle' }} - - - {{ exposure.count }} of {{ capture.amount }} - - - - - {{ exposure.count }} - - + + {{ exposure.count }} + of {{ capture.amount }} + + + {{ exposure.remainingTime | exposureTime }} diff --git a/desktop/src/app/camera/camera.component.ts b/desktop/src/app/camera/camera.component.ts index b499bc21c..adda0da04 100644 --- a/desktop/src/app/camera/camera.component.ts +++ b/desktop/src/app/camera/camera.component.ts @@ -179,7 +179,6 @@ export class CameraComponent implements AfterContentInit, OnDestroy { } readonly wait = { - duration: 0, remainingTime: 0, progress: 0, } @@ -238,78 +237,30 @@ export class CameraComponent implements AfterContentInit, OnDestroy { } }) - electron.on('CAMERA_CAPTURE_STARTED', event => { - if (event.camera.name === this.camera?.name) { - ngZone.run(() => { - this.capture.looping = event.looping - this.capture.amount = event.exposureAmount - this.capture.elapsedTime = 0 - this.capture.remainingTime = event.estimatedTime - this.capture.progress = event.progress - this.capturing = true - this.waiting = false - }) - } - }) - electron.on('CAMERA_CAPTURE_ELAPSED', event => { if (event.camera.name === this.camera?.name) { ngZone.run(() => { - this.capture.elapsedTime = event.elapsedTime - this.capture.remainingTime = event.remainingTime - this.capture.progress = event.progress - }) - } - }) - - electron.on('CAMERA_CAPTURE_WAITING', event => { - if (event.camera.name === this.camera?.name) { - ngZone.run(() => { - this.wait.duration = event.waitDuration - this.wait.remainingTime = event.remainingTime - this.wait.progress = event.progress - this.capturing = false - this.waiting = true - }) - } - }) - - electron.on('CAMERA_CAPTURE_FINISHED', event => { - if (event.camera.name === this.camera?.name) { - ngZone.run(() => { - this.capturing = false - this.waiting = false - }) - } - }) - - electron.on('CAMERA_EXPOSURE_STARTED', event => { - if (event.camera.name === this.camera?.name) { - ngZone.run(() => { - this.exposure.remainingTime = event.remainingTime - this.exposure.progress = event.progress - this.exposure.count = event.exposureCount - this.capturing = true - this.waiting = false - }) - } - }) - - electron.on('CAMERA_EXPOSURE_ELAPSED', event => { - if (event.camera.name === this.camera?.name) { - ngZone.run(() => { - this.exposure.remainingTime = event.remainingTime - this.exposure.progress = event.progress + this.capture.elapsedTime = event.captureElapsedTime + this.capture.remainingTime = event.captureRemainingTime + this.capture.progress = event.captureProgress + this.exposure.remainingTime = event.exposureRemainingTime + this.exposure.progress = event.exposureProgress this.exposure.count = event.exposureCount - }) - } - }) - electron.on('CAMERA_EXPOSURE_FINISHED', event => { - if (event.camera.name === this.camera?.name) { - ngZone.run(() => { - this.exposure.remainingTime = event.remainingTime - this.exposure.progress = event.progress + if (event.state === 'WAITING') { + this.wait.remainingTime = event.waitRemainingTime + this.wait.progress = event.waitProgress + this.waiting = true + } else if (event.state === 'CAPTURE_STARTED') { + this.capture.looping = event.exposureAmount <= 0 + this.capture.amount = event.exposureAmount + this.capturing = true + } else if (event.state === 'CAPTURE_FINISHED') { + this.capturing = false + this.waiting = false + } else if (event.state === 'EXPOSURE_STARTED') { + this.waiting = false + } }) } }) @@ -336,7 +287,7 @@ export class CameraComponent implements AfterContentInit, OnDestroy { const camera = await this.api.camera(this.camera.name) Object.assign(this.camera, camera) - await this.loadPreference() + this.loadPreference() this.update() } else { this.app.subTitle = '' diff --git a/desktop/src/app/home/home.component.ts b/desktop/src/app/home/home.component.ts index 4246317df..67f3d3aaa 100644 --- a/desktop/src/app/home/home.component.ts +++ b/desktop/src/app/home/home.component.ts @@ -317,7 +317,9 @@ export class HomeComponent implements AfterContentInit, OnDestroy { this.connected = await this.api.connectionStatus() } catch { this.connected = false + } + if (!this.connected) { this.cameras = [] this.mounts = [] this.focusers = [] diff --git a/desktop/src/app/image/image.component.ts b/desktop/src/app/image/image.component.ts index 95649657b..db7c88d8a 100644 --- a/desktop/src/app/image/image.component.ts +++ b/desktop/src/app/image/image.component.ts @@ -357,8 +357,8 @@ export class ImageComponent implements AfterViewInit, OnDestroy { ) { app.title = 'Image' - electron.on('CAMERA_EXPOSURE_FINISHED', async (event) => { - if (event.camera.name === this.imageData.camera?.name) { + electron.on('CAMERA_CAPTURE_ELAPSED', async (event) => { + if (event.state === 'EXPOSURE_FINISHED' && event.camera.name === this.imageData.camera?.name) { await this.closeImage() ngZone.run(() => { diff --git a/desktop/src/shared/services/electron.service.ts b/desktop/src/shared/services/electron.service.ts index df6f76b15..898781b3d 100644 --- a/desktop/src/shared/services/electron.service.ts +++ b/desktop/src/shared/services/electron.service.ts @@ -7,9 +7,8 @@ import * as childProcess from 'child_process' import { ipcRenderer, webFrame } from 'electron' import * as fs from 'fs' import { - ApiEventType, Camera, CameraCaptureElapsed, CameraCaptureFinished, CameraCaptureIsWaiting, CameraCaptureStarted, - CameraExposureElapsed, CameraExposureFinished, CameraExposureStarted, DARVEvent, DeviceMessageEvent, FilterWheel, - Focuser, GuideOutput, Guider, GuiderMessageEvent, HistoryStep, INDIMessageEvent, InternalEventType, Location, Mount, NotificationEvent, + ApiEventType, Camera, CameraCaptureEvent, DARVEvent, DeviceMessageEvent, FilterWheel, Focuser, + GuideOutput, Guider, GuiderMessageEvent, HistoryStep, INDIMessageEvent, InternalEventType, Location, Mount, NotificationEvent, NotificationEventType, OpenDirectory, OpenFile } from '../types' import { ApiService } from './api.service' @@ -21,13 +20,7 @@ type EventMappedType = { 'CAMERA_UPDATED': DeviceMessageEvent 'CAMERA_ATTACHED': DeviceMessageEvent 'CAMERA_DETACHED': DeviceMessageEvent - 'CAMERA_CAPTURE_STARTED': CameraCaptureStarted - 'CAMERA_CAPTURE_FINISHED': CameraCaptureFinished - 'CAMERA_CAPTURE_ELAPSED': CameraCaptureElapsed - 'CAMERA_CAPTURE_WAITING': CameraCaptureIsWaiting - 'CAMERA_EXPOSURE_ELAPSED': CameraExposureElapsed - 'CAMERA_EXPOSURE_STARTED': CameraExposureStarted - 'CAMERA_EXPOSURE_FINISHED': CameraExposureFinished + 'CAMERA_CAPTURE_ELAPSED': CameraCaptureEvent 'MOUNT_UPDATED': DeviceMessageEvent 'MOUNT_ATTACHED': DeviceMessageEvent 'MOUNT_DETACHED': DeviceMessageEvent diff --git a/desktop/src/shared/types.ts b/desktop/src/shared/types.ts index c23fdf4b6..3a8506e1e 100644 --- a/desktop/src/shared/types.ts +++ b/desktop/src/shared/types.ts @@ -213,41 +213,16 @@ export interface CameraStartCapture { export interface CameraCaptureEvent extends MessageEvent { camera: Camera - progress: number -} - -export interface CameraCaptureStarted extends CameraCaptureEvent { - looping: boolean - exposureAmount: number - exposureTime: number - estimatedTime: number -} - -export interface CameraCaptureFinished extends CameraCaptureEvent { } - -export interface CameraCaptureElapsed extends CameraCaptureEvent { - exposureCount: number - remainingTime: number - elapsedTime: number -} - -export interface CameraCaptureIsWaiting extends CameraCaptureEvent { - waitDuration: number - remainingTime: number -} - -export interface CameraExposureEvent extends CameraCaptureEvent { + state: CameraCaptureState exposureAmount: number exposureCount: number - exposureTime: number - remainingTime: number -} - -export interface CameraExposureStarted extends CameraExposureEvent { } - -export interface CameraExposureElapsed extends CameraExposureEvent { } - -export interface CameraExposureFinished extends CameraExposureEvent { + captureElapsedTime: number + captureProgress: number + captureRemainingTime: number + exposureProgress: number + exposureRemainingTime: number + waitRemainingTime: number + waitProgress: number savePath?: string } @@ -793,7 +768,9 @@ export const NOTIFICATION_EVENT_TYPE = [ export type NotificationEventType = (typeof NOTIFICATION_EVENT_TYPE)[number] -export type ImageSource = 'FRAMING' | 'PATH' | 'CAMERA' +export type ImageSource = 'FRAMING' | + 'PATH' | + 'CAMERA' export const HIPS_SURVEY_TYPES = [ 'CDS_P_DSS2_NIR', @@ -826,11 +803,18 @@ export const HIPS_SURVEY_TYPES = [ export type HipsSurveyType = (typeof HIPS_SURVEY_TYPES)[number] -export type PierSide = 'EAST' | 'WEST' | 'NEITHER' +export type PierSide = 'EAST' | + 'WEST' | + 'NEITHER' -export type TargetCoordinateType = 'J2000' | 'JNOW' +export type TargetCoordinateType = 'J2000' | + 'JNOW' -export type TrackMode = 'SIDEREAL' | ' LUNAR' | 'SOLAR' | 'KING' | 'CUSTOM' +export type TrackMode = 'SIDEREAL' | + ' LUNAR' | + 'SOLAR' | + 'KING' | + 'CUSTOM' export type GuideDirection = 'NORTH' | // DEC+ 'SOUTH' | // DEC- @@ -879,10 +863,24 @@ export const GUIDE_STATES = [ export type GuideState = (typeof GUIDE_STATES)[number] -export type Hemisphere = 'NORTHERN' | 'SOUTHERN' +export type Hemisphere = 'NORTHERN' | + 'SOUTHERN' + +export type DARVState = 'IDLE' | + 'INITIAL_PAUSE' | + 'FORWARD' | + 'BACKWARD' -export type DARVState = 'IDLE' | 'INITIAL_PAUSE' | 'FORWARD' | 'BACKWARD' +export type GuiderPlotMode = 'RA/DEC' | + 'DX/DY' -export type GuiderPlotMode = 'RA/DEC' | 'DX/DY' +export type GuiderYAxisUnit = 'ARCSEC' | + 'PIXEL' -export type GuiderYAxisUnit = 'ARCSEC' | 'PIXEL' +export type CameraCaptureState = 'CAPTURE_STARTED' | + 'EXPOSURE_STARTED' | + 'EXPOSURING' | + 'WAITING' | + 'SETTLING' | + 'EXPOSURE_FINISHED' | + 'CAPTURE_FINISHED' diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/AsyncJobLauncher.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/AsyncJobLauncher.kt index 69b02f24a..221b17265 100644 --- a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/AsyncJobLauncher.kt +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/AsyncJobLauncher.kt @@ -1,13 +1,14 @@ package nebulosa.batch.processing +import nebulosa.log.loggerFor import java.time.LocalDateTime import java.util.concurrent.Executor open class AsyncJobLauncher(private val executor: Executor) : JobLauncher, StepInterceptor { - private val jobListeners = HashSet() - private val stepListeners = HashSet() - private val stepInterceptors = HashSet() + private val jobListeners = LinkedHashSet() + private val stepListeners = LinkedHashSet() + private val stepInterceptors = LinkedHashSet() private val jobs = LinkedHashMap() override var stepHandler: StepHandler = DefaultStepHandler @@ -79,15 +80,26 @@ open class AsyncJobLauncher(private val executor: Executor) : JobLauncher, StepI job.beforeJob(jobExecution) jobListeners.forEach { it.beforeJob(jobExecution) } + val stepJobListeners = LinkedHashSet() + try { while (jobExecution.canContinue && job.hasNext(jobExecution)) { val step = job.next(jobExecution) - stepHandler.handle(step, StepExecution(step, jobExecution)) + + if (step is JobExecutionListener) { + if (stepJobListeners.add(step)) { + step.beforeJob(jobExecution) + } + } + + val result = stepHandler.handle(step, StepExecution(step, jobExecution)) + result.get() } jobExecution.status = if (jobExecution.isStopping) JobStatus.STOPPED else JobStatus.COMPLETED jobExecution.complete() } catch (e: Throwable) { + LOG.error("job failed. job=$job, jobExecution=$jobExecution", e) jobExecution.status = JobStatus.FAILED jobExecution.completeExceptionally(e) } finally { @@ -96,13 +108,21 @@ open class AsyncJobLauncher(private val executor: Executor) : JobLauncher, StepI job.afterJob(jobExecution) jobListeners.forEach { it.afterJob(jobExecution) } + stepJobListeners.forEach { it.afterJob(jobExecution) } } return jobExecution } override fun stop(mayInterruptIfRunning: Boolean) { - jobs.forEach { it.value.stop(mayInterruptIfRunning) } + jobs.forEach { stop(it.value, mayInterruptIfRunning) } + } + + override fun stop(jobExecution: JobExecution, mayInterruptIfRunning: Boolean) { + if (!jobExecution.isDone && !jobExecution.isStopping) { + jobExecution.status = JobStatus.STOPPING + jobExecution.job.stop(mayInterruptIfRunning) + } } override fun intercept(chain: StepChain): StepResult { @@ -113,4 +133,9 @@ open class AsyncJobLauncher(private val executor: Executor) : JobLauncher, StepI } override fun toString() = "AsyncJobLauncher" + + companion object { + + @JvmStatic private val LOG = loggerFor() + } } diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/DefaultStepHandler.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/DefaultStepHandler.kt index 6118f797f..248aea50f 100644 --- a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/DefaultStepHandler.kt +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/DefaultStepHandler.kt @@ -9,12 +9,6 @@ object DefaultStepHandler : StepHandler { override fun handle(step: Step, stepExecution: StepExecution): StepResult { val jobLauncher = stepExecution.jobExecution.jobLauncher - if (step is JobExecutionListener) { - if (jobLauncher.registerJobExecutionListener(step)) { - step.beforeJob(stepExecution.jobExecution) - } - } - when (step) { is SplitStep -> { step.beforeStep(stepExecution) @@ -28,13 +22,13 @@ object DefaultStepHandler : StepHandler { } else -> { val chain = StepInterceptorChain(stepExecution.jobExecution.stepInterceptors, step, stepExecution) - var status: RepeatStatus LOG.info("step started. step={}, context={}", step, stepExecution.context) - do { - status = chain.proceed().get() - } while (status == RepeatStatus.CONTINUABLE) + while (stepExecution.jobExecution.canContinue) { + val status = chain.proceed().get() + if (status != RepeatStatus.CONTINUABLE) break + } LOG.info("step finished. step={}, context={}", step, stepExecution.context) } diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobExecution.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobExecution.kt index 9eaeca7e0..7dfed6d38 100644 --- a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobExecution.kt +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobExecution.kt @@ -13,9 +13,9 @@ data class JobExecution( val startedAt: LocalDateTime = LocalDateTime.now(), var status: JobStatus = JobStatus.STARTING, var finishedAt: LocalDateTime? = null, -) : Stoppable { +) { - private val completable = CompletableFuture() + @JvmField internal val completable = CompletableFuture() inline val jobId get() = job.id @@ -55,11 +55,4 @@ data class JobExecution( internal fun completeExceptionally(e: Throwable) { completable.completeExceptionally(e) } - - override fun stop(mayInterruptIfRunning: Boolean) { - if (!isDone) { - status = JobStatus.STOPPING - job.stop(mayInterruptIfRunning) - } - } } diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobLauncher.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobLauncher.kt index 4612e234e..fa00440a7 100644 --- a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobLauncher.kt +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobLauncher.kt @@ -17,4 +17,6 @@ interface JobLauncher : Collection, Stoppable { fun unregisterStepInterceptor(interceptor: StepInterceptor): Boolean fun launch(job: Job, executionContext: ExecutionContext? = null): JobExecution + + fun stop(jobExecution: JobExecution, mayInterruptIfRunning: Boolean = true) } diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/SimpleJob.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/SimpleJob.kt index c3df9287d..9b062638a 100644 --- a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/SimpleJob.kt +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/SimpleJob.kt @@ -9,9 +9,10 @@ abstract class SimpleJob : Job, ArrayList { constructor(vararg steps: Step) : this(steps.toList()) @Volatile private var position = 0 + @Volatile private var stopped = false override fun hasNext(jobExecution: JobExecution): Boolean { - return position < size + return !stopped && position < size } override fun next(jobExecution: JobExecution): Step { @@ -19,8 +20,17 @@ abstract class SimpleJob : Job, ArrayList { } override fun stop(mayInterruptIfRunning: Boolean) { - if (position in indices) { - this[position].stop(mayInterruptIfRunning) + if (stopped) return + + stopped = true + + if (position in 1..size) { + this[position - 1].stop(mayInterruptIfRunning) } } + + fun reset() { + stopped = false + position = 0 + } } diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/delay/DelayStep.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/delay/DelayStep.kt index be164f58d..894f28ad7 100644 --- a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/delay/DelayStep.kt +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/delay/DelayStep.kt @@ -5,7 +5,7 @@ import java.time.Duration data class DelayStep(@JvmField val duration: Duration) : Step, JobExecutionListener { - private val listeners = HashSet() + private val listeners = LinkedHashSet() @Volatile private var aborted = false diff --git a/nebulosa-batch-processing/src/test/kotlin/BatchProcessingTest.kt b/nebulosa-batch-processing/src/test/kotlin/BatchProcessingTest.kt index 03808f381..d9cb0d00b 100644 --- a/nebulosa-batch-processing/src/test/kotlin/BatchProcessingTest.kt +++ b/nebulosa-batch-processing/src/test/kotlin/BatchProcessingTest.kt @@ -50,9 +50,9 @@ class BatchProcessingTest : StringSpec() { "stop" { val startedAt = System.currentTimeMillis() val jobExecution = launcher.launch(MathJob((0..7).map { SumStep() })) - thread { Thread.sleep(4000); jobExecution.stop() } + thread { Thread.sleep(4000); launcher.stop(jobExecution) } jobExecution.waitForCompletion() - jobExecution.context["VALUE"] shouldBe 4.0 + jobExecution.context["VALUE"] shouldBe 3.0 jobExecution.isStopped.shouldBeTrue() (System.currentTimeMillis() - startedAt) shouldBeInRange (4000L..5000L) } diff --git a/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/CancellationToken.kt b/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/CancellationToken.kt new file mode 100644 index 000000000..00d954d68 --- /dev/null +++ b/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/CancellationToken.kt @@ -0,0 +1,60 @@ +package nebulosa.common.concurrency + +import java.io.Closeable +import java.util.concurrent.Future +import java.util.concurrent.TimeUnit + +class CancellationToken : Closeable, Future { + + private val latch = CountUpDownLatch(1) + private val listeners = LinkedHashSet() + + fun listen(action: Runnable): Boolean { + return if (isDone) { + action.run() + false + } else { + listeners.add(action) + } + } + + fun cancel() { + cancel(true) + } + + @Synchronized + override fun cancel(mayInterruptIfRunning: Boolean): Boolean { + if (latch.count <= 0) return false + latch.reset() + listeners.forEach(Runnable::run) + listeners.clear() + return true + } + + override fun isCancelled(): Boolean { + return latch.get() + } + + override fun isDone(): Boolean { + return latch.get() + } + + override fun get(): Boolean { + latch.await() + return true + } + + override fun get(timeout: Long, unit: TimeUnit): Boolean { + return latch.await(timeout, unit) + } + + fun reset() { + latch.countUp(1 - latch.count) + listeners.clear() + } + + override fun close() { + latch.reset() + listeners.clear() + } +} diff --git a/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/CountUpDownLatch.kt b/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/CountUpDownLatch.kt index 884b96376..3b373dddf 100644 --- a/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/CountUpDownLatch.kt +++ b/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/CountUpDownLatch.kt @@ -6,15 +6,13 @@ import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.locks.AbstractQueuedSynchronizer import kotlin.math.max -class CountUpDownLatch(initialCount: Int = 0) : AtomicBoolean(true) { +class CountUpDownLatch(initialCount: Int = 0) : AtomicBoolean(initialCount == 0) { private val sync = Sync(this) init { - if (initialCount > 0) { - sync.count = initialCount - set(false) - } + require(initialCount >= 0) { "initialCount < 0: $initialCount" } + sync.count = initialCount } val count diff --git a/nebulosa-guiding-phd2/build.gradle.kts b/nebulosa-guiding-phd2/build.gradle.kts index 38ee1e398..7c9baa997 100644 --- a/nebulosa-guiding-phd2/build.gradle.kts +++ b/nebulosa-guiding-phd2/build.gradle.kts @@ -5,7 +5,6 @@ plugins { dependencies { api(project(":nebulosa-io")) - api(project(":nebulosa-common")) api(project(":nebulosa-guiding")) api(project(":nebulosa-phd2-client")) implementation(project(":nebulosa-log")) diff --git a/nebulosa-guiding-phd2/src/main/kotlin/nebulosa/guiding/phd2/PHD2Guider.kt b/nebulosa-guiding-phd2/src/main/kotlin/nebulosa/guiding/phd2/PHD2Guider.kt index 766c38366..daaa58bcc 100644 --- a/nebulosa-guiding-phd2/src/main/kotlin/nebulosa/guiding/phd2/PHD2Guider.kt +++ b/nebulosa-guiding-phd2/src/main/kotlin/nebulosa/guiding/phd2/PHD2Guider.kt @@ -1,5 +1,6 @@ package nebulosa.guiding.phd2 +import nebulosa.common.concurrency.CancellationToken import nebulosa.common.concurrency.CountUpDownLatch import nebulosa.guiding.* import nebulosa.log.loggerFor @@ -19,7 +20,7 @@ class PHD2Guider(private val client: PHD2Client) : Guider, PHD2EventListener { @Volatile private var shiftRateAxis = ShiftAxesType.RADEC @Volatile private var lockPosition = GuidePoint.ZERO @Volatile private var starPosition = GuidePoint.ZERO - private val listeners = hashSetOf() + private val listeners = LinkedHashSet() override var pixelScale = 1.0 private set @@ -232,14 +233,16 @@ class PHD2Guider(private val client: PHD2Client) : Guider, PHD2EventListener { } } - override fun waitForSettle() { + override fun waitForSettle(cancellationToken: CancellationToken?) { try { + cancellationToken?.listen(settling::reset) settling.await(settleTimeout) } catch (e: InterruptedException) { LOG.warn("PHD2 did not send SettleDone message in expected time") } catch (e: Throwable) { LOG.warn("an error occurrs while waiting for settle done", e) } finally { + cancellationToken?.close() settling.reset() } } diff --git a/nebulosa-guiding/build.gradle.kts b/nebulosa-guiding/build.gradle.kts index 9ad896cec..f891151a0 100644 --- a/nebulosa-guiding/build.gradle.kts +++ b/nebulosa-guiding/build.gradle.kts @@ -5,6 +5,7 @@ plugins { dependencies { api(project(":nebulosa-math")) + api(project(":nebulosa-common")) api(project(":nebulosa-indi-device")) testImplementation(project(":nebulosa-test")) } diff --git a/nebulosa-guiding/src/main/kotlin/nebulosa/guiding/Guider.kt b/nebulosa-guiding/src/main/kotlin/nebulosa/guiding/Guider.kt index 4e37d4626..178a039c3 100644 --- a/nebulosa-guiding/src/main/kotlin/nebulosa/guiding/Guider.kt +++ b/nebulosa-guiding/src/main/kotlin/nebulosa/guiding/Guider.kt @@ -1,5 +1,6 @@ package nebulosa.guiding +import nebulosa.common.concurrency.CancellationToken import java.io.Closeable import java.time.Duration @@ -35,7 +36,7 @@ interface Guider : Closeable { fun dither(amount: Double, raOnly: Boolean = false) - fun waitForSettle() + fun waitForSettle(cancellationToken: CancellationToken? = null) companion object { diff --git a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/PHD2Client.kt b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/PHD2Client.kt index f192dfcac..9d3789923 100644 --- a/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/PHD2Client.kt +++ b/nebulosa-phd2-client/src/main/kotlin/nebulosa/phd2/client/PHD2Client.kt @@ -23,7 +23,7 @@ import kotlin.math.max class PHD2Client : NettyClient() { - @JvmField internal val listeners = hashSetOf() + @JvmField internal val listeners = LinkedHashSet() @JvmField internal val commands = hashMapOf>() override val channelInitialzer = object : ChannelInitializer() { diff --git a/nebulosa-stellarium-protocol/src/main/kotlin/nebulosa/stellarium/protocol/StellariumProtocolServer.kt b/nebulosa-stellarium-protocol/src/main/kotlin/nebulosa/stellarium/protocol/StellariumProtocolServer.kt index 046a8cded..b815be467 100644 --- a/nebulosa-stellarium-protocol/src/main/kotlin/nebulosa/stellarium/protocol/StellariumProtocolServer.kt +++ b/nebulosa-stellarium-protocol/src/main/kotlin/nebulosa/stellarium/protocol/StellariumProtocolServer.kt @@ -71,7 +71,7 @@ class StellariumProtocolServer( ) : NettyServer(), CurrentPositionHandler { private val stellariumMountHandler = AtomicReference() - private val currentPositionHandlers = hashSetOf() + private val currentPositionHandlers = LinkedHashSet() val rightAscension: Angle? get() = stellariumMountHandler.get()?.rightAscension From a3be27c6bcc641aa86f3ddef5d79d88e3476260b Mon Sep 17 00:00:00 2001 From: tiagohm Date: Fri, 15 Dec 2023 00:59:11 -0300 Subject: [PATCH 32/87] [api][desktop]: Fix event dispatching and handling; Fix Camera exposure update --- .../api/guiding/GuideOutputEventHandler.kt | 21 +++++------ .../src/app/alignment/alignment.component.ts | 37 +++++++++++++++++-- desktop/src/app/camera/camera.component.html | 2 +- desktop/src/app/camera/camera.component.ts | 30 ++++++++++----- .../app/filterwheel/filterwheel.component.ts | 10 ++++- desktop/src/app/focuser/focuser.component.ts | 10 ++++- desktop/src/app/mount/mount.component.ts | 10 ++++- .../indi/client/device/camera/CameraDevice.kt | 4 +- 8 files changed, 94 insertions(+), 30 deletions(-) diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputEventHandler.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputEventHandler.kt index 9baaa26f8..eea722189 100644 --- a/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputEventHandler.kt +++ b/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputEventHandler.kt @@ -33,17 +33,16 @@ class GuideOutputEventHandler( fun onGuideOutputEvent(event: DeviceEvent) { val device = event.device ?: return - if (device.canPulseGuide) { - when (event) { - is PropertyChangedEvent -> { - throttler.onNext(event) - } - is GuideOutputAttached -> { - messageService.sendMessage(GuideOutputMessageEvent(GUIDE_OUTPUT_ATTACHED, event.device)) - } - is GuideOutputDetached -> { - messageService.sendMessage(GuideOutputMessageEvent(GUIDE_OUTPUT_DETACHED, event.device)) - } + if (device.canPulseGuide && event is PropertyChangedEvent) { + throttler.onNext(event) + } + + when (event) { + is GuideOutputAttached -> { + messageService.sendMessage(GuideOutputMessageEvent(GUIDE_OUTPUT_ATTACHED, event.device)) + } + is GuideOutputDetached -> { + messageService.sendMessage(GuideOutputMessageEvent(GUIDE_OUTPUT_DETACHED, event.device)) } } } diff --git a/desktop/src/app/alignment/alignment.component.ts b/desktop/src/app/alignment/alignment.component.ts index 038f5e398..00647774d 100644 --- a/desktop/src/app/alignment/alignment.component.ts +++ b/desktop/src/app/alignment/alignment.component.ts @@ -48,6 +48,28 @@ export class AlignmentComponent implements AfterViewInit, OnDestroy { } }) + electron.on('CAMERA_ATTACHED', event => { + ngZone.run(() => { + this.cameras.push(event.device) + }) + }) + + electron.on('CAMERA_DETACHED', event => { + ngZone.run(() => { + const index = this.cameras.findIndex(e => e.name === event.device.name) + + if (index >= 0) { + if (this.cameras[index] === this.camera) { + this.camera = undefined + this.cameraConnected = false + } + + this.cameras.splice(index, 1) + + } + }) + }) + electron.on('GUIDE_OUTPUT_ATTACHED', event => { ngZone.run(() => { this.guideOutputs.push(event.device) @@ -57,7 +79,16 @@ export class AlignmentComponent implements AfterViewInit, OnDestroy { electron.on('GUIDE_OUTPUT_DETACHED', event => { ngZone.run(() => { const index = this.guideOutputs.findIndex(e => e.name === event.device.name) - if (index >= 0) this.guideOutputs.splice(index, 1) + + if (index >= 0) { + if (this.guideOutputs[index] === this.guideOutput) { + this.guideOutput = undefined + this.guideOutputConnected = false + } + + this.guideOutputs.splice(index, 1) + + } }) }) @@ -156,7 +187,7 @@ export class AlignmentComponent implements AfterViewInit, OnDestroy { return this.browserWindow.openCameraImage(this.camera!) } - private async updateCamera() { + private updateCamera() { if (!this.camera) { return } @@ -164,7 +195,7 @@ export class AlignmentComponent implements AfterViewInit, OnDestroy { this.cameraConnected = this.camera.connected } - private async updateGuideOutput() { + private updateGuideOutput() { if (!this.guideOutput) { return } diff --git a/desktop/src/app/camera/camera.component.html b/desktop/src/app/camera/camera.component.html index 5cfaac891..7a45c2b1b 100644 --- a/desktop/src/app/camera/camera.component.html +++ b/desktop/src/app/camera/camera.component.html @@ -116,7 +116,7 @@
diff --git a/desktop/src/app/camera/camera.component.ts b/desktop/src/app/camera/camera.component.ts index adda0da04..6638ffa5f 100644 --- a/desktop/src/app/camera/camera.component.ts +++ b/desktop/src/app/camera/camera.component.ts @@ -237,6 +237,14 @@ export class CameraComponent implements AfterContentInit, OnDestroy { } }) + electron.on('CAMERA_DETACHED', event => { + if (event.device.name === this.camera?.name) { + ngZone.run(() => { + this.connected = false + }) + } + }) + electron.on('CAMERA_CAPTURE_ELAPSED', event => { if (event.camera.name === this.camera?.name) { ngZone.run(() => { @@ -373,18 +381,20 @@ export class CameraComponent implements AfterContentInit, OnDestroy { } private updateExposureUnit(unit: ExposureTimeUnit) { - const a = CameraComponent.exposureUnitFactor(this.exposureTimeUnit) - const b = CameraComponent.exposureUnitFactor(unit) - const exposureTime = Math.trunc(this.exposureTime * b / a) - const exposureTimeMin = Math.trunc(this.camera!.exposureMin * b / 60000000) - const exposureTimeMax = Math.trunc(this.camera!.exposureMax * b / 60000000) - this.exposureTimeMax = Math.max(1, exposureTimeMax) - this.exposureTimeMin = Math.max(1, exposureTimeMin) - this.exposureTime = Math.max(this.exposureTimeMin, Math.min(exposureTime, this.exposureTimeMax)) - this.exposureTimeUnit = unit + if (this.camera!.exposureMax) { + const a = CameraComponent.exposureUnitFactor(this.exposureTimeUnit) + const b = CameraComponent.exposureUnitFactor(unit) + const exposureTime = Math.trunc(this.exposureTime * b / a) + const exposureTimeMin = Math.trunc(this.camera!.exposureMin * b / 60000000) + const exposureTimeMax = Math.trunc(this.camera!.exposureMax * b / 60000000) + this.exposureTimeMax = Math.max(1, exposureTimeMax) + this.exposureTimeMin = Math.max(1, exposureTimeMin) + this.exposureTime = Math.max(this.exposureTimeMin, Math.min(exposureTime, this.exposureTimeMax)) + this.exposureTimeUnit = unit + } } - private async update() { + private update() { if (this.camera) { this.connected = this.camera.connected diff --git a/desktop/src/app/filterwheel/filterwheel.component.ts b/desktop/src/app/filterwheel/filterwheel.component.ts index 1ec686822..c49045abd 100644 --- a/desktop/src/app/filterwheel/filterwheel.component.ts +++ b/desktop/src/app/filterwheel/filterwheel.component.ts @@ -64,6 +64,14 @@ export class FilterWheelComponent implements AfterContentInit, OnDestroy { } }) + electron.on('WHEEL_DETACHED', event => { + if (event.device.name === this.wheel?.name) { + ngZone.run(() => { + this.connected = false + }) + } + }) + this.subscription = this.filterChangedPublisher .pipe(throttleTime(1500)) .subscribe((filter) => { @@ -127,7 +135,7 @@ export class FilterWheelComponent implements AfterContentInit, OnDestroy { this.filterChangedPublisher.next(filter) } - private async update() { + private update() { if (!this.wheel) { return } diff --git a/desktop/src/app/focuser/focuser.component.ts b/desktop/src/app/focuser/focuser.component.ts index 666b8fd6a..0f59124ce 100644 --- a/desktop/src/app/focuser/focuser.component.ts +++ b/desktop/src/app/focuser/focuser.component.ts @@ -55,6 +55,14 @@ export class FocuserComponent implements AfterViewInit, OnDestroy { }) } }) + + electron.on('FOCUSER_DETACHED', event => { + if (event.device.name === this.focuser?.name) { + ngZone.run(() => { + this.connected = false + }) + } + }) } async ngAfterViewInit() { @@ -120,7 +128,7 @@ export class FocuserComponent implements AfterViewInit, OnDestroy { this.api.focuserAbort(this.focuser!) } - private async update() { + private update() { if (!this.focuser) { return } diff --git a/desktop/src/app/mount/mount.component.ts b/desktop/src/app/mount/mount.component.ts index d89e8c1ee..fa4b049da 100644 --- a/desktop/src/app/mount/mount.component.ts +++ b/desktop/src/app/mount/mount.component.ts @@ -165,6 +165,14 @@ export class MountComponent implements AfterContentInit, OnDestroy { } }) + electron.on('MOUNT_DETACHED', event => { + if (event.device.name === this.mount?.name) { + ngZone.run(() => { + this.connected = false + }) + } + }) + this.computeCoordinateSubscriptions[0] = this.computeCoordinatePublisher .pipe(throttleTime(5000)) .subscribe(() => this.computeCoordinates()) @@ -322,7 +330,7 @@ export class MountComponent implements AfterContentInit, OnDestroy { } } - private async update() { + private update() { if (this.mount) { this.connected = this.mount.connected this.slewing = this.mount.slewing diff --git a/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/camera/CameraDevice.kt b/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/camera/CameraDevice.kt index 5e87324cc..5d96adf67 100644 --- a/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/camera/CameraDevice.kt +++ b/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/camera/CameraDevice.kt @@ -339,14 +339,14 @@ internal open class CameraDevice( override fun close() { if (hasThermometer) { - hasThermometer = false handler.unregisterThermometer(this) + hasThermometer = false LOG.info("thermometer detached: {}", name) } if (canPulseGuide) { - canPulseGuide = false handler.unregisterGuideOutput(this) + canPulseGuide = false LOG.info("guide output detached: {}", name) } } From 8bcb8e85d02da89406c486b0b4d290081ff94dc9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 Dec 2023 03:28:59 +0000 Subject: [PATCH 33/87] [api]: Bump the netty group with 2 updates Bumps the netty group with 2 updates: [io.netty:netty-transport](https://github.com/netty/netty) and [io.netty:netty-codec](https://github.com/netty/netty). Updates `io.netty:netty-transport` from 4.1.101.Final to 4.1.103.Final - [Commits](https://github.com/netty/netty/compare/netty-4.1.101.Final...netty-4.1.103.Final) Updates `io.netty:netty-codec` from 4.1.101.Final to 4.1.103.Final - [Commits](https://github.com/netty/netty/compare/netty-4.1.101.Final...netty-4.1.103.Final) --- updated-dependencies: - dependency-name: io.netty:netty-transport dependency-type: direct:production update-type: version-update:semver-patch dependency-group: netty - dependency-name: io.netty:netty-codec dependency-type: direct:production update-type: version-update:semver-patch dependency-group: netty ... Signed-off-by: dependabot[bot] --- settings.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/settings.gradle.kts b/settings.gradle.kts index 7e69c9d6b..ee7b1a19e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -23,8 +23,8 @@ dependencyResolutionManagement { library("rx", "io.reactivex.rxjava3:rxjava:3.1.8") library("logback", "ch.qos.logback:logback-classic:1.4.14") library("eventbus", "org.greenrobot:eventbus-java:3.3.1") - library("netty-transport", "io.netty:netty-transport:4.1.101.Final") - library("netty-codec", "io.netty:netty-codec:4.1.101.Final") + library("netty-transport", "io.netty:netty-transport:4.1.103.Final") + library("netty-codec", "io.netty:netty-codec:4.1.103.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.14.0") From 9503c8a330b6a9da05405923fd7dcc1075ff3146 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 Dec 2023 03:29:10 +0000 Subject: [PATCH 34/87] [api]: Bump org.flywaydb:flyway-core from 10.1.0 to 10.3.0 Bumps [org.flywaydb:flyway-core](https://github.com/flyway/flyway) from 10.1.0 to 10.3.0. - [Release notes](https://github.com/flyway/flyway/releases) - [Commits](https://github.com/flyway/flyway/compare/flyway-10.1.0...flyway-10.3.0) --- updated-dependencies: - dependency-name: org.flywaydb:flyway-core dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- settings.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.gradle.kts b/settings.gradle.kts index ee7b1a19e..5ed946c30 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -33,7 +33,7 @@ dependencyResolutionManagement { library("oshi", "com.github.oshi:oshi-core:6.4.8") library("timeshape", "net.iakovlev:timeshape:2022g.17") library("sqlite", "org.xerial:sqlite-jdbc:3.44.1.0") - library("flyway", "org.flywaydb:flyway-core:10.1.0") + library("flyway", "org.flywaydb:flyway-core:10.3.0") library("jna", "net.java.dev.jna:jna:5.13.0") library("kotest-assertions-core", "io.kotest:kotest-assertions-core:5.8.0") library("kotest-runner-junit5", "io.kotest:kotest-runner-junit5:5.8.0") From c631ca5754ffb94f13220a406652bd6b1e335cf3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 Dec 2023 03:29:29 +0000 Subject: [PATCH 35/87] [api]: Bump com.github.oshi:oshi-core from 6.4.8 to 6.4.9 Bumps [com.github.oshi:oshi-core](https://github.com/oshi/oshi) from 6.4.8 to 6.4.9. - [Release notes](https://github.com/oshi/oshi/releases) - [Changelog](https://github.com/oshi/oshi/blob/master/CHANGELOG.md) - [Commits](https://github.com/oshi/oshi/compare/oshi-parent-6.4.8...oshi-parent-6.4.9) --- updated-dependencies: - dependency-name: com.github.oshi:oshi-core dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- settings.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.gradle.kts b/settings.gradle.kts index 5ed946c30..fb3dc9354 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -30,7 +30,7 @@ dependencyResolutionManagement { library("apache-lang3", "org.apache.commons:commons-lang3:3.14.0") library("apache-codec", "commons-codec:commons-codec:1.16.0") library("apache-collections", "org.apache.commons:commons-collections4:4.4") - library("oshi", "com.github.oshi:oshi-core:6.4.8") + library("oshi", "com.github.oshi:oshi-core:6.4.9") library("timeshape", "net.iakovlev:timeshape:2022g.17") library("sqlite", "org.xerial:sqlite-jdbc:3.44.1.0") library("flyway", "org.flywaydb:flyway-core:10.3.0") From 58b7c35b71a451df3a0a4aacb5558ef5acc7e41d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 Dec 2023 03:29:23 +0000 Subject: [PATCH 36/87] [api]: Bump org.springframework:spring-context-indexer Bumps [org.springframework:spring-context-indexer](https://github.com/spring-projects/spring-framework) from 6.1.1 to 6.1.2. - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.1.1...v6.1.2) --- updated-dependencies: - dependency-name: org.springframework:spring-context-indexer dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- api/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/build.gradle.kts b/api/build.gradle.kts index 7b3e29349..52b7ecc12 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -47,7 +47,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.1.1") + kapt("org.springframework:spring-context-indexer:6.1.2") testImplementation(project(":nebulosa-test")) } From cd924a0dc547150d8af3a948e1ecef08a660ea7e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 Dec 2023 04:00:57 +0000 Subject: [PATCH 37/87] [api]: Bump net.java.dev.jna:jna from 5.13.0 to 5.14.0 Bumps [net.java.dev.jna:jna](https://github.com/java-native-access/jna) from 5.13.0 to 5.14.0. - [Changelog](https://github.com/java-native-access/jna/blob/master/CHANGES.md) - [Commits](https://github.com/java-native-access/jna/compare/5.13.0...5.14.0) --- updated-dependencies: - dependency-name: net.java.dev.jna:jna dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- settings.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.gradle.kts b/settings.gradle.kts index fb3dc9354..e25877a97 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -34,7 +34,7 @@ dependencyResolutionManagement { library("timeshape", "net.iakovlev:timeshape:2022g.17") library("sqlite", "org.xerial:sqlite-jdbc:3.44.1.0") library("flyway", "org.flywaydb:flyway-core:10.3.0") - library("jna", "net.java.dev.jna:jna:5.13.0") + library("jna", "net.java.dev.jna:jna:5.14.0") library("kotest-assertions-core", "io.kotest:kotest-assertions-core:5.8.0") library("kotest-runner-junit5", "io.kotest:kotest-runner-junit5:5.8.0") bundle("kotest", listOf("kotest-assertions-core", "kotest-runner-junit5")) From 71ff89b71d63da48db84e4b75f0994210c9f4ec9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 Dec 2023 03:31:43 +0000 Subject: [PATCH 38/87] [desktop]: Bump ws from 8.14.2 to 8.15.1 in /desktop/app Bumps [ws](https://github.com/websockets/ws) from 8.14.2 to 8.15.1. - [Release notes](https://github.com/websockets/ws/releases) - [Commits](https://github.com/websockets/ws/compare/8.14.2...8.15.1) --- updated-dependencies: - dependency-name: ws dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- desktop/app/package-lock.json | 8 ++++---- desktop/app/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/desktop/app/package-lock.json b/desktop/app/package-lock.json index e2f4afadf..b6af85251 100644 --- a/desktop/app/package-lock.json +++ b/desktop/app/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "dependencies": { "@stomp/stompjs": "7.0.0", - "ws": "8.14.2" + "ws": "8.15.1" } }, "node_modules/@stomp/stompjs": { @@ -19,9 +19,9 @@ "integrity": "sha512-fGdq4wPDnSV/KyOsjq4P+zLc8MFWC3lMmP5FBgLWKPJTYcuCbAIrnRGjB7q2jHZdYCOD5vxLuFoKIYLy5/u8Pw==" }, "node_modules/ws": { - "version": "8.14.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", - "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", + "version": "8.15.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.15.1.tgz", + "integrity": "sha512-W5OZiCjXEmk0yZ66ZN82beM5Sz7l7coYxpRkzS+p9PP+ToQry8szKh+61eNktr7EA9DOwvFGhfC605jDHbP6QQ==", "engines": { "node": ">=10.0.0" }, diff --git a/desktop/app/package.json b/desktop/app/package.json index e350a816c..b3df9ce07 100644 --- a/desktop/app/package.json +++ b/desktop/app/package.json @@ -12,6 +12,6 @@ "private": true, "dependencies": { "@stomp/stompjs": "7.0.0", - "ws": "8.14.2" + "ws": "8.15.1" } } From 3216289d77567cb9b1719571eeda1e7ecde6ad0a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 Dec 2023 03:55:28 +0000 Subject: [PATCH 39/87] [desktop]: Bump ts-node from 10.9.1 to 10.9.2 in /desktop Bumps [ts-node](https://github.com/TypeStrong/ts-node) from 10.9.1 to 10.9.2. - [Release notes](https://github.com/TypeStrong/ts-node/releases) - [Changelog](https://github.com/TypeStrong/ts-node/blob/main/development-docs/release-template.md) - [Commits](https://github.com/TypeStrong/ts-node/compare/v10.9.1...v10.9.2) --- updated-dependencies: - dependency-name: ts-node dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- desktop/package-lock.json | 8 ++++---- desktop/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/desktop/package-lock.json b/desktop/package-lock.json index c6020964c..e1f06b524 100644 --- a/desktop/package-lock.json +++ b/desktop/package-lock.json @@ -50,7 +50,7 @@ "electron-reloader": "1.2.3", "node-polyfill-webpack-plugin": "3.0.0", "npm-run-all": "4.1.5", - "ts-node": "10.9.1", + "ts-node": "10.9.2", "typescript": "5.2.2", "wait-on": "7.2.0" }, @@ -15720,9 +15720,9 @@ } }, "node_modules/ts-node": { - "version": "10.9.1", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", - "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", diff --git a/desktop/package.json b/desktop/package.json index 5bea9e851..fe73a2f44 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -70,7 +70,7 @@ "electron-reloader": "1.2.3", "node-polyfill-webpack-plugin": "3.0.0", "npm-run-all": "4.1.5", - "ts-node": "10.9.1", + "ts-node": "10.9.2", "typescript": "5.2.2", "wait-on": "7.2.0" }, From c3c4fdbca9143973bd92983438a8162410324a64 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 Dec 2023 03:54:21 +0000 Subject: [PATCH 40/87] [desktop]: Bump primeng from 17.0.0 to 17.1.0 in /desktop Bumps [primeng](https://github.com/primefaces/primeng) from 17.0.0 to 17.1.0. - [Release notes](https://github.com/primefaces/primeng/releases) - [Changelog](https://github.com/primefaces/primeng/blob/master/CHANGELOG.md) - [Commits](https://github.com/primefaces/primeng/compare/17.0.0...17.1.0) --- updated-dependencies: - dependency-name: primeng dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- desktop/package-lock.json | 8 ++++---- desktop/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/desktop/package-lock.json b/desktop/package-lock.json index e1f06b524..52fcff3c4 100644 --- a/desktop/package-lock.json +++ b/desktop/package-lock.json @@ -30,7 +30,7 @@ "panzoom": "9.4.3", "primeflex": "3.3.1", "primeicons": "6.0.1", - "primeng": "17.0.0", + "primeng": "17.1.0", "rxjs": "7.8.1", "tslib": "2.6.2", "uuid": "9.0.1", @@ -13254,9 +13254,9 @@ "integrity": "sha512-KDeO94CbWI4pKsPnYpA1FPjo79EsY9I+M8ywoPBSf9XMXoe/0crjbUK7jcQEDHuc0ZMRIZsxH3TYLv4TUtHmAA==" }, "node_modules/primeng": { - "version": "17.0.0", - "resolved": "https://registry.npmjs.org/primeng/-/primeng-17.0.0.tgz", - "integrity": "sha512-6ja5koKWaENKr2C1o8N5xqRII/yA0Byy9AHeb25f4vQ9gEivkRit8O9tqoiaG9fncZUL8gLVjbImUtZj2kw4gQ==", + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/primeng/-/primeng-17.1.0.tgz", + "integrity": "sha512-LpilMBPiTXLiaNADLLlhHTinGNzfD4FjQZQ/uEdE6ATQz/JeSzVGjvspFkQjknTrVbQxPfFMc846aILuS0enDg==", "dependencies": { "tslib": "^2.3.0" }, diff --git a/desktop/package.json b/desktop/package.json index fe73a2f44..4dd79e565 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -50,7 +50,7 @@ "panzoom": "9.4.3", "primeflex": "3.3.1", "primeicons": "6.0.1", - "primeng": "17.0.0", + "primeng": "17.1.0", "rxjs": "7.8.1", "tslib": "2.6.2", "uuid": "9.0.1", From e361b7e7a5666abfe0f79a8b90f3b7a324673bf0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 Dec 2023 03:53:41 +0000 Subject: [PATCH 41/87] [desktop]: Bump the angular group in /desktop with 13 updates Bumps the angular group in /desktop with 13 updates: | Package | From | To | | --- | --- | --- | | [@angular/animations](https://github.com/angular/angular/tree/HEAD/packages/animations) | `17.0.6` | `17.0.7` | | [@angular/cdk](https://github.com/angular/components) | `17.0.3` | `17.0.4` | | [@angular/common](https://github.com/angular/angular/tree/HEAD/packages/common) | `17.0.6` | `17.0.7` | | [@angular/compiler](https://github.com/angular/angular/tree/HEAD/packages/compiler) | `17.0.6` | `17.0.7` | | [@angular/core](https://github.com/angular/angular/tree/HEAD/packages/core) | `17.0.6` | `17.0.7` | | [@angular/forms](https://github.com/angular/angular/tree/HEAD/packages/forms) | `17.0.6` | `17.0.7` | | [@angular/language-service](https://github.com/angular/angular/tree/HEAD/packages/language-service) | `17.0.6` | `17.0.7` | | [@angular/platform-browser](https://github.com/angular/angular/tree/HEAD/packages/platform-browser) | `17.0.6` | `17.0.7` | | [@angular/platform-browser-dynamic](https://github.com/angular/angular/tree/HEAD/packages/platform-browser-dynamic) | `17.0.6` | `17.0.7` | | [@angular/router](https://github.com/angular/angular/tree/HEAD/packages/router) | `17.0.6` | `17.0.7` | | [@angular-devkit/build-angular](https://github.com/angular/angular-cli) | `17.0.6` | `17.0.7` | | [@angular/cli](https://github.com/angular/angular-cli) | `17.0.6` | `17.0.7` | | [@angular/compiler-cli](https://github.com/angular/angular/tree/HEAD/packages/compiler-cli) | `17.0.6` | `17.0.7` | Updates `@angular/animations` from 17.0.6 to 17.0.7 - [Release notes](https://github.com/angular/angular/releases) - [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md) - [Commits](https://github.com/angular/angular/commits/17.0.7/packages/animations) Updates `@angular/cdk` from 17.0.3 to 17.0.4 - [Release notes](https://github.com/angular/components/releases) - [Changelog](https://github.com/angular/components/blob/main/CHANGELOG.md) - [Commits](https://github.com/angular/components/compare/17.0.3...17.0.4) Updates `@angular/common` from 17.0.6 to 17.0.7 - [Release notes](https://github.com/angular/angular/releases) - [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md) - [Commits](https://github.com/angular/angular/commits/17.0.7/packages/common) Updates `@angular/compiler` from 17.0.6 to 17.0.7 - [Release notes](https://github.com/angular/angular/releases) - [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md) - [Commits](https://github.com/angular/angular/commits/17.0.7/packages/compiler) Updates `@angular/core` from 17.0.6 to 17.0.7 - [Release notes](https://github.com/angular/angular/releases) - [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md) - [Commits](https://github.com/angular/angular/commits/17.0.7/packages/core) Updates `@angular/forms` from 17.0.6 to 17.0.7 - [Release notes](https://github.com/angular/angular/releases) - [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md) - [Commits](https://github.com/angular/angular/commits/17.0.7/packages/forms) Updates `@angular/language-service` from 17.0.6 to 17.0.7 - [Release notes](https://github.com/angular/angular/releases) - [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md) - [Commits](https://github.com/angular/angular/commits/17.0.7/packages/language-service) Updates `@angular/platform-browser` from 17.0.6 to 17.0.7 - [Release notes](https://github.com/angular/angular/releases) - [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md) - [Commits](https://github.com/angular/angular/commits/17.0.7/packages/platform-browser) Updates `@angular/platform-browser-dynamic` from 17.0.6 to 17.0.7 - [Release notes](https://github.com/angular/angular/releases) - [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md) - [Commits](https://github.com/angular/angular/commits/17.0.7/packages/platform-browser-dynamic) Updates `@angular/router` from 17.0.6 to 17.0.7 - [Release notes](https://github.com/angular/angular/releases) - [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md) - [Commits](https://github.com/angular/angular/commits/17.0.7/packages/router) Updates `@angular-devkit/build-angular` from 17.0.6 to 17.0.7 - [Release notes](https://github.com/angular/angular-cli/releases) - [Changelog](https://github.com/angular/angular-cli/blob/main/CHANGELOG.md) - [Commits](https://github.com/angular/angular-cli/compare/17.0.6...17.0.7) Updates `@angular/cli` from 17.0.6 to 17.0.7 - [Release notes](https://github.com/angular/angular-cli/releases) - [Changelog](https://github.com/angular/angular-cli/blob/main/CHANGELOG.md) - [Commits](https://github.com/angular/angular-cli/compare/17.0.6...17.0.7) Updates `@angular/compiler-cli` from 17.0.6 to 17.0.7 - [Release notes](https://github.com/angular/angular/releases) - [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md) - [Commits](https://github.com/angular/angular/commits/17.0.7/packages/compiler-cli) --- updated-dependencies: - dependency-name: "@angular/animations" dependency-type: direct:production update-type: version-update:semver-patch dependency-group: angular - dependency-name: "@angular/cdk" dependency-type: direct:production update-type: version-update:semver-patch dependency-group: angular - dependency-name: "@angular/common" dependency-type: direct:production update-type: version-update:semver-patch dependency-group: angular - dependency-name: "@angular/compiler" dependency-type: direct:production update-type: version-update:semver-patch dependency-group: angular - dependency-name: "@angular/core" dependency-type: direct:production update-type: version-update:semver-patch dependency-group: angular - dependency-name: "@angular/forms" dependency-type: direct:production update-type: version-update:semver-patch dependency-group: angular - dependency-name: "@angular/language-service" dependency-type: direct:production update-type: version-update:semver-patch dependency-group: angular - dependency-name: "@angular/platform-browser" dependency-type: direct:production update-type: version-update:semver-patch dependency-group: angular - dependency-name: "@angular/platform-browser-dynamic" dependency-type: direct:production update-type: version-update:semver-patch dependency-group: angular - dependency-name: "@angular/router" dependency-type: direct:production update-type: version-update:semver-patch dependency-group: angular - dependency-name: "@angular-devkit/build-angular" dependency-type: direct:development update-type: version-update:semver-patch dependency-group: angular - dependency-name: "@angular/cli" dependency-type: direct:development update-type: version-update:semver-patch dependency-group: angular - dependency-name: "@angular/compiler-cli" dependency-type: direct:development update-type: version-update:semver-patch dependency-group: angular ... Signed-off-by: dependabot[bot] --- desktop/package-lock.json | 257 +++++++++++++++++++------------------- desktop/package.json | 26 ++-- 2 files changed, 142 insertions(+), 141 deletions(-) diff --git a/desktop/package-lock.json b/desktop/package-lock.json index 52fcff3c4..63fe84afc 100644 --- a/desktop/package-lock.json +++ b/desktop/package-lock.json @@ -10,16 +10,16 @@ "hasInstallScript": true, "license": "MIT", "dependencies": { - "@angular/animations": "17.0.6", - "@angular/cdk": "17.0.3", - "@angular/common": "17.0.6", - "@angular/compiler": "17.0.6", - "@angular/core": "17.0.6", - "@angular/forms": "17.0.6", - "@angular/language-service": "17.0.6", - "@angular/platform-browser": "17.0.6", - "@angular/platform-browser-dynamic": "17.0.6", - "@angular/router": "17.0.6", + "@angular/animations": "17.0.7", + "@angular/cdk": "17.0.4", + "@angular/common": "17.0.7", + "@angular/compiler": "17.0.7", + "@angular/core": "17.0.7", + "@angular/forms": "17.0.7", + "@angular/language-service": "17.0.7", + "@angular/platform-browser": "17.0.7", + "@angular/platform-browser-dynamic": "17.0.7", + "@angular/router": "17.0.7", "@fontsource/roboto": "5.0.8", "@mdi/font": "7.3.67", "chart.js": "4.4.1", @@ -38,9 +38,9 @@ }, "devDependencies": { "@angular-builders/custom-webpack": "17.0.0", - "@angular-devkit/build-angular": "17.0.6", - "@angular/cli": "17.0.6", - "@angular/compiler-cli": "17.0.6", + "@angular-devkit/build-angular": "17.0.7", + "@angular/cli": "17.0.7", + "@angular/compiler-cli": "17.0.7", "@types/leaflet": "1.9.8", "@types/node": "20.10.4", "@types/uuid": "9.0.7", @@ -93,12 +93,12 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.1700.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1700.6.tgz", - "integrity": "sha512-zVpz736cBZHXcv0v2bRLfJLcykanUyEMVQXkGwZp2eygjNK1Ls9s/74o1dXd6nGdvjh6AnkzOU/vouj2dqA41g==", + "version": "0.1700.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1700.7.tgz", + "integrity": "sha512-32uitQKsYLGXAKoXBsmOnPsTt9pS+b9cnFI9ZvBFVhJ31I2EOM7vGcMFalhTxdB/DkVHk4TyO78efV0V26DwCA==", "dev": true, "dependencies": { - "@angular-devkit/core": "17.0.6", + "@angular-devkit/core": "17.0.7", "rxjs": "7.8.1" }, "engines": { @@ -108,15 +108,15 @@ } }, "node_modules/@angular-devkit/build-angular": { - "version": "17.0.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-17.0.6.tgz", - "integrity": "sha512-gYxmbvq5/nk7aVJ6JxIIW0//RM7859kMPJGPKekcCGSWkkObjqG6P5cDgJJNAjMl/IfCsG7B+xGYjr4zN8QV9g==", + "version": "17.0.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-17.0.7.tgz", + "integrity": "sha512-AtEzLk6n6BXqQzk0Bsupe6GV0IgUe7RbpBfqROi+NZqMA7OUAHCX3xA6M68Qu+5KxBtW7T5lHeZZ7iP/y39wtQ==", "dev": true, "dependencies": { "@ampproject/remapping": "2.2.1", - "@angular-devkit/architect": "0.1700.6", - "@angular-devkit/build-webpack": "0.1700.6", - "@angular-devkit/core": "17.0.6", + "@angular-devkit/architect": "0.1700.7", + "@angular-devkit/build-webpack": "0.1700.7", + "@angular-devkit/core": "17.0.7", "@babel/core": "7.23.2", "@babel/generator": "7.23.0", "@babel/helper-annotate-as-pure": "7.22.5", @@ -127,7 +127,7 @@ "@babel/preset-env": "7.23.2", "@babel/runtime": "7.23.2", "@discoveryjs/json-ext": "0.5.7", - "@ngtools/webpack": "17.0.6", + "@ngtools/webpack": "17.0.7", "@vitejs/plugin-basic-ssl": "1.0.1", "ansi-colors": "4.1.3", "autoprefixer": "10.4.16", @@ -231,12 +231,12 @@ } }, "node_modules/@angular-devkit/build-webpack": { - "version": "0.1700.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1700.6.tgz", - "integrity": "sha512-xT5LL92rScVjvGZO7but/YbTQ12PNilosyjDouephl+HIf2V6rwDovTsEfpLYgcrqgodh+R0X0ZCOk95+MmSBA==", + "version": "0.1700.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1700.7.tgz", + "integrity": "sha512-B9Mg/qYDpE5my8PJ3VPQyRSUV0Oq1bFUzU8s0ZpqEZl1URKc04pm0LtLmebrMIcUZgDiGk0RHaD+O1E9IV/bdQ==", "dev": true, "dependencies": { - "@angular-devkit/architect": "0.1700.6", + "@angular-devkit/architect": "0.1700.7", "rxjs": "7.8.1" }, "engines": { @@ -250,9 +250,9 @@ } }, "node_modules/@angular-devkit/core": { - "version": "17.0.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.0.6.tgz", - "integrity": "sha512-+h9VnFHof7rKzBJ5FWrbPXWzbag31QKbUGJ/mV5BYgj39vjzPNUXBW8AaScZAlATi8+tElYXjRMvM49GnuyRLg==", + "version": "17.0.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.0.7.tgz", + "integrity": "sha512-vATobHo5O5tJba424hJfQWLb40GzvZPNsI74dcgSUTgrDph8ksmk5xB9OvEvf0INorQZ2IMphj/VIWj4/+JqSA==", "dev": true, "dependencies": { "ajv": "8.12.0", @@ -277,12 +277,12 @@ } }, "node_modules/@angular-devkit/schematics": { - "version": "17.0.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.0.6.tgz", - "integrity": "sha512-2g769MpazA1aOzJOm2MNGosra3kxw8CbdIQQOKkvycIzroRNgN06yHcRTDC03GADgP/CkDJ6kxwJQNG+wNFL2A==", + "version": "17.0.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.0.7.tgz", + "integrity": "sha512-BY11OkJkM3xyXcvyD7x5kGY/c8Ufd4AfPvI0D9imhVxbns45Q48b1DlvCQvSnCJ/s+OwnkrYb/Efa70ZiaGu8A==", "dev": true, "dependencies": { - "@angular-devkit/core": "17.0.6", + "@angular-devkit/core": "17.0.7", "jsonc-parser": "3.2.0", "magic-string": "0.30.5", "ora": "5.4.1", @@ -295,9 +295,9 @@ } }, "node_modules/@angular/animations": { - "version": "17.0.6", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-17.0.6.tgz", - "integrity": "sha512-fic61LjLHry79c5H9UGM8Ff311MJnf9an7EukLj2aLJ3J0uadL/H9de7dDp8PaIT10DX9g+aRTIKOmF3PmmXIQ==", + "version": "17.0.7", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-17.0.7.tgz", + "integrity": "sha512-IjZjPGMxvi2a9o7fzjwNO44FvhTZlVSgcPtqM6Glq0+WVeQcnZxf1Onj68M/FGx2AunS8elRbrgPxTexVeSo7A==", "dependencies": { "tslib": "^2.3.0" }, @@ -305,13 +305,13 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/core": "17.0.6" + "@angular/core": "17.0.7" } }, "node_modules/@angular/cdk": { - "version": "17.0.3", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-17.0.3.tgz", - "integrity": "sha512-Qd5uvC09B3+uk2uX1JxmiWrD7wueMHSxNBoCbDEmnrsdDVUta0wN/jj/CtATljxUM8ZqvEvkqgxJCig1od9oyQ==", + "version": "17.0.4", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-17.0.4.tgz", + "integrity": "sha512-mh/EuIR0NPfpNqAXBSZWuJeBMXUvUDYdKhiFWZet5NLO1bDgFe1MGLBjtW4us95k4BZsMLbCKNxJgc+4JqwUvg==", "dependencies": { "tslib": "^2.3.0" }, @@ -325,15 +325,15 @@ } }, "node_modules/@angular/cli": { - "version": "17.0.6", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-17.0.6.tgz", - "integrity": "sha512-BLA2wDeqZManC/7MI6WvRRV+VhrwjxxB7FawLyp4xYlo0CTSOFOfeKPVRMLEnA/Ou4R5d47B+BqJTlep62pHwg==", + "version": "17.0.7", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-17.0.7.tgz", + "integrity": "sha512-oSa0GVAQNA7wFbLJYeaO3kV4iUcbKEqXDLxcIE8s1GfHddBOlXH2P1T4fXonCBl5qvV+joP0G0+fs7I0w2utZQ==", "dev": true, "dependencies": { - "@angular-devkit/architect": "0.1700.6", - "@angular-devkit/core": "17.0.6", - "@angular-devkit/schematics": "17.0.6", - "@schematics/angular": "17.0.6", + "@angular-devkit/architect": "0.1700.7", + "@angular-devkit/core": "17.0.7", + "@angular-devkit/schematics": "17.0.7", + "@schematics/angular": "17.0.7", "@yarnpkg/lockfile": "1.1.0", "ansi-colors": "4.1.3", "ini": "4.1.1", @@ -359,9 +359,9 @@ } }, "node_modules/@angular/common": { - "version": "17.0.6", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-17.0.6.tgz", - "integrity": "sha512-FZtf8ol8W2V21ZDgFtcxmJ6JJKUO97QZ+wr/bosyYEryWMmn6VGrbOARhfW7BlrEgn14NdFkLb72KKtqoqRjrg==", + "version": "17.0.7", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-17.0.7.tgz", + "integrity": "sha512-bPPL6x0KOAOTxKSE2j4EWmEUOnqZYzOYiHzroa5b9UEyA9NvGkd9bm3zIxw8xcndRj1Ehcmvpi6KBLcYBBbWfg==", "dependencies": { "tslib": "^2.3.0" }, @@ -369,14 +369,14 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/core": "17.0.6", + "@angular/core": "17.0.7", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/compiler": { - "version": "17.0.6", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-17.0.6.tgz", - "integrity": "sha512-PaCNnlPcL0rvByKCBUUyLWkKJYXOrcfKlYYvcacjOzEUgZeEpekG81hMRb9u/Pz+A+M4HJSTmdgzwGP35zo8qw==", + "version": "17.0.7", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-17.0.7.tgz", + "integrity": "sha512-QHPuLti2c2tGZmOGZ0cfCHo4LxiHUkC27I0aZFDyQSSQqEI5obQGVlEREHysw0nsS3sYIcLvqcwcKcRtXlXtxQ==", "dependencies": { "tslib": "^2.3.0" }, @@ -384,7 +384,7 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/core": "17.0.6" + "@angular/core": "17.0.7" }, "peerDependenciesMeta": { "@angular/core": { @@ -393,9 +393,9 @@ } }, "node_modules/@angular/compiler-cli": { - "version": "17.0.6", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-17.0.6.tgz", - "integrity": "sha512-C1Gfh9kbjYZezEMOwxnvUTHuPXa+6pk7mAfSj8e5oAO6E+wfo2dTxv1J5zxa3KYzxPYMNfF8OFvLuMKsw7lXjA==", + "version": "17.0.7", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-17.0.7.tgz", + "integrity": "sha512-YnL38idjIYtl3BXYpv+sVJKWGbUjHT6eyQSQVAfO/1AwWqVa21K9hnE+Q37VmUKEcKFMnQembeuErA+KVsGI6A==", "dev": true, "dependencies": { "@babel/core": "7.23.2", @@ -416,14 +416,14 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/compiler": "17.0.6", + "@angular/compiler": "17.0.7", "typescript": ">=5.2 <5.3" } }, "node_modules/@angular/core": { - "version": "17.0.6", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-17.0.6.tgz", - "integrity": "sha512-QzfKRTDNgGOY9D5VxenUUz20cvPVC+uVw9xiqkDuHgGfLYVFlCAK9ymFYkdUCLTcVzJPxckP+spMpPX8nc4Aqw==", + "version": "17.0.7", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-17.0.7.tgz", + "integrity": "sha512-mEkelXkzEi6+A9GjdKOSGGzQAfo1iAjVTn6YsplNUeGE5JgDZYZ7sXGQqs0Lin7dzJxnPAgGjCOl7SpWLXIPSQ==", "dependencies": { "tslib": "^2.3.0" }, @@ -436,9 +436,9 @@ } }, "node_modules/@angular/forms": { - "version": "17.0.6", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-17.0.6.tgz", - "integrity": "sha512-n/trsMtQHUBGiWz5lFaggMcMOuw0gH+96TCtHxQiUYJOdrbOemkFdGrNh3B4fGHmogWuOYJVF5FAm97WRES2XA==", + "version": "17.0.7", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-17.0.7.tgz", + "integrity": "sha512-28BxRxEmgZIofGwVp6s2v3ri/kuWW+/EY/ZXhavlWKJEh4ATJl72k0RkRWNcQi4wnvn0Qb8tFdnVJnvRZvvKEw==", "dependencies": { "tslib": "^2.3.0" }, @@ -446,24 +446,24 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/common": "17.0.6", - "@angular/core": "17.0.6", - "@angular/platform-browser": "17.0.6", + "@angular/common": "17.0.7", + "@angular/core": "17.0.7", + "@angular/platform-browser": "17.0.7", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/language-service": { - "version": "17.0.6", - "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-17.0.6.tgz", - "integrity": "sha512-HTJmnZeXFZoAJD8wvMN7QHuGd9KHsEQTdA7DeEDxqDneGM63bPVdRN6gSaai6abU1/8gfBNtSTfiwhHnCRTh0Q==", + "version": "17.0.7", + "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-17.0.7.tgz", + "integrity": "sha512-03EXeBZgyGNnEDLiABwEw0lpAcqLxSgl+bXQahOO4OnBSYQWGEiqfs3ymNbNj0chUfQVadG4FWghwGTGbj77Iw==", "engines": { "node": "^18.13.0 || >=20.9.0" } }, "node_modules/@angular/platform-browser": { - "version": "17.0.6", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-17.0.6.tgz", - "integrity": "sha512-nBhWH1MKT2WswgRNIoMnmNAt0n5/fG59BanJtodW71//Aj5aIE+BuVoFgK3wmO8IMoeP4i4GXRInBXs6lUMOJw==", + "version": "17.0.7", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-17.0.7.tgz", + "integrity": "sha512-bm9/wt51nc/MPjft/FlRNIgFSeLjDtfJOT7M32Rt6kOHhNKSK7ZTPWdMe9ahuHSbAhLzd0G/4NsT5sKrWSeVZg==", "dependencies": { "tslib": "^2.3.0" }, @@ -471,9 +471,9 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/animations": "17.0.6", - "@angular/common": "17.0.6", - "@angular/core": "17.0.6" + "@angular/animations": "17.0.7", + "@angular/common": "17.0.7", + "@angular/core": "17.0.7" }, "peerDependenciesMeta": { "@angular/animations": { @@ -482,9 +482,9 @@ } }, "node_modules/@angular/platform-browser-dynamic": { - "version": "17.0.6", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-17.0.6.tgz", - "integrity": "sha512-5ZEmBtBkqamTaWjUXCls7G1f3xyK/ykXE7hnUV9CgGqXKrNkxblmbtOhoWdsbuIYjjdxQcAk1qtg/Rg21wcc4w==", + "version": "17.0.7", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-17.0.7.tgz", + "integrity": "sha512-OquwUX9fLWA2JUZW5Jm6atk0CPt0sA7Tg24eGLsr6g1XfTS7jRZprlGaa72NgPLnQVV6m84o/ZiNYS6yPmq1Gg==", "dependencies": { "tslib": "^2.3.0" }, @@ -492,16 +492,16 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/common": "17.0.6", - "@angular/compiler": "17.0.6", - "@angular/core": "17.0.6", - "@angular/platform-browser": "17.0.6" + "@angular/common": "17.0.7", + "@angular/compiler": "17.0.7", + "@angular/core": "17.0.7", + "@angular/platform-browser": "17.0.7" } }, "node_modules/@angular/router": { - "version": "17.0.6", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-17.0.6.tgz", - "integrity": "sha512-xW6yDxREpBOB9MoODSfIw5HwkwLK+OgK34Q6sGYs0ft9UryMoFwft+pHGAaDz2nzhA72n+Ht9B2eai78UE9jGQ==", + "version": "17.0.7", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-17.0.7.tgz", + "integrity": "sha512-rUFPe1uDlYYw6+3Gq68czW7WxBH7zT/D3UsT1otqwUV4RnQQsVze4fIit9FqJh7tuP4y3WpB4XBNf7p7Oi6TJw==", "dependencies": { "tslib": "^2.3.0" }, @@ -509,9 +509,9 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/common": "17.0.6", - "@angular/core": "17.0.6", - "@angular/platform-browser": "17.0.6", + "@angular/common": "17.0.7", + "@angular/core": "17.0.7", + "@angular/platform-browser": "17.0.7", "rxjs": "^6.5.3 || ^7.4.0" } }, @@ -653,9 +653,9 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.23.5.tgz", - "integrity": "sha512-QELlRWxSpgdwdJzSJn4WAhKC+hvw/AtHbbrIoncKHkhKKR/luAlKkgBDcri1EzWAo8f8VvYVryEHN4tax/V67A==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.23.6.tgz", + "integrity": "sha512-cBXU1vZni/CpGF29iTu4YRbOZt3Wat6zCoMDxRF1MayiEc4URxOj31tT65HUM0CRpMowA3HCJaAOVOUnMf96cw==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", @@ -711,9 +711,9 @@ } }, "node_modules/@babel/helper-define-polyfill-provider": { - "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==", + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.4.tgz", + "integrity": "sha512-QcJMILQCu2jm5TFPGA3lCpJJTeEP+mqeXooG/NZbg/h5FTFi6V0+99ahlRsW8/kRLyb24LZVCCiclDedhLKcBA==", "dev": true, "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", @@ -1501,12 +1501,13 @@ } }, "node_modules/@babel/plugin-transform-for-of": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.23.3.tgz", - "integrity": "sha512-X8jSm8X1CMwxmK878qsUGJRmbysKNbdpTv/O1/v0LuY/ZkZrng5WYiekYSdg9m09OTmDDUWeEDsTE+17WYbAZw==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.23.6.tgz", + "integrity": "sha512-aYH4ytZ0qSuBbpfhuofbg/e96oQ7U2w1Aw/UQmKT+1l39uEhUPoFS3fHevDc1G0OvewyDudfMKY1OulczHzWIw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -3286,9 +3287,9 @@ "integrity": "sha512-SWxvzRbUQRfewlIV+OF4/YF4DkeTjMWoT8Hh9yeU/5UBVdJZj9Uf4a9+cXjknSIhIaMxZ/4N1O/s7ojApOOGjg==" }, "node_modules/@ngtools/webpack": { - "version": "17.0.6", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-17.0.6.tgz", - "integrity": "sha512-9Us20rqGhi8PmQBwQu6Qtww3WVV/gf2s2DbzcLclsiDtSBobzT64Z6F6E9OpAYD+c5PxlUaOghL6NXdnSNdByA==", + "version": "17.0.7", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-17.0.7.tgz", + "integrity": "sha512-gwhUhpwXn0trwwKdSu9WlJbEcLt+s/2fPwoD9lZ0y3wXfrOogsfcNBJKeO5BZf1h+A3AWt7ePmgrZXSJM+865Q==", "dev": true, "engines": { "node": "^18.13.0 || >=20.9.0", @@ -3562,13 +3563,13 @@ } }, "node_modules/@schematics/angular": { - "version": "17.0.6", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-17.0.6.tgz", - "integrity": "sha512-AyC7Bk3Omy6PfADThhq5ci+zzdTTi2N1oZI35gw4tMK5ZxVwIACx2Zyhaz399m5c2RCDi9Hz4A2BOFq9f0j/dg==", + "version": "17.0.7", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-17.0.7.tgz", + "integrity": "sha512-d7QKmcKrM4owb/2bR7Ipf23roiNbvbD/x7reNhQAtKAPLSHJ3Ulkf1+Yv+dj+9f+K7y9SBviEUSrD27BQ9WaxQ==", "dev": true, "dependencies": { - "@angular-devkit/core": "17.0.6", - "@angular-devkit/schematics": "17.0.6", + "@angular-devkit/core": "17.0.7", + "@angular-devkit/schematics": "17.0.7", "jsonc-parser": "3.2.0" }, "engines": { @@ -4855,13 +4856,13 @@ } }, "node_modules/babel-plugin-polyfill-corejs2": { - "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==", + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.7.tgz", + "integrity": "sha512-LidDk/tEGDfuHW2DWh/Hgo4rmnw3cduK6ZkOI1NPFceSK3n/yAGeOsNT7FLnSGHkXj3RHGSEVkN3FsCTY6w2CQ==", "dev": true, "dependencies": { "@babel/compat-data": "^7.22.6", - "@babel/helper-define-polyfill-provider": "^0.4.3", + "@babel/helper-define-polyfill-provider": "^0.4.4", "semver": "^6.3.1" }, "peerDependencies": { @@ -4878,12 +4879,12 @@ } }, "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.8.6", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.6.tgz", - "integrity": "sha512-leDIc4l4tUgU7str5BWLS2h8q2N4Nf6lGZP6UrNDxdtfF2g69eJ5L0H7S8A5Ln/arfFAfHor5InAdZuIOwZdgQ==", + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.7.tgz", + "integrity": "sha512-KyDvZYxAzkC0Aj2dAPyDzi2Ym15e5JKZSK+maI7NAwSqofvuFglbSsxE7wUOvTg9oFVnHMzVzBKcqEb4PJgtOA==", "dev": true, "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.4.3", + "@babel/helper-define-polyfill-provider": "^0.4.4", "core-js-compat": "^3.33.1" }, "peerDependencies": { @@ -4891,12 +4892,12 @@ } }, "node_modules/babel-plugin-polyfill-regenerator": { - "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==", + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.4.tgz", + "integrity": "sha512-S/x2iOCvDaCASLYsOOgWOq4bCfKYVqvO/uxjkaYyZ3rVsVE3CeAI/c84NpyuBBymEgNvHgjEot3a9/Z/kXvqsg==", "dev": true, "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.4.3" + "@babel/helper-define-polyfill-provider": "^0.4.4" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -11719,9 +11720,9 @@ } }, "node_modules/needle": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/needle/-/needle-3.3.0.tgz", - "integrity": "sha512-Kaq820952NOrLY/LVbIhPZeXtCGDBAPVgT0BYnoT3p/Nr3nkGXdvWXXA3zgy7wpAgqRULu9p/NvKiFo6f/12fw==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/needle/-/needle-3.3.1.tgz", + "integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==", "dev": true, "optional": true, "dependencies": { @@ -16868,9 +16869,9 @@ } }, "node_modules/webpack-dev-server/node_modules/ws": { - "version": "8.14.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", - "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", + "version": "8.15.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.15.1.tgz", + "integrity": "sha512-W5OZiCjXEmk0yZ66ZN82beM5Sz7l7coYxpRkzS+p9PP+ToQry8szKh+61eNktr7EA9DOwvFGhfC605jDHbP6QQ==", "dev": true, "engines": { "node": ">=10.0.0" diff --git a/desktop/package.json b/desktop/package.json index 4dd79e565..f194737ac 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -30,16 +30,16 @@ "lint": "ng lint" }, "dependencies": { - "@angular/animations": "17.0.6", - "@angular/cdk": "17.0.3", - "@angular/common": "17.0.6", - "@angular/compiler": "17.0.6", - "@angular/core": "17.0.6", - "@angular/forms": "17.0.6", - "@angular/language-service": "17.0.6", - "@angular/platform-browser": "17.0.6", - "@angular/platform-browser-dynamic": "17.0.6", - "@angular/router": "17.0.6", + "@angular/animations": "17.0.7", + "@angular/cdk": "17.0.4", + "@angular/common": "17.0.7", + "@angular/compiler": "17.0.7", + "@angular/core": "17.0.7", + "@angular/forms": "17.0.7", + "@angular/language-service": "17.0.7", + "@angular/platform-browser": "17.0.7", + "@angular/platform-browser-dynamic": "17.0.7", + "@angular/router": "17.0.7", "@fontsource/roboto": "5.0.8", "@mdi/font": "7.3.67", "chart.js": "4.4.1", @@ -58,9 +58,9 @@ }, "devDependencies": { "@angular-builders/custom-webpack": "17.0.0", - "@angular-devkit/build-angular": "17.0.6", - "@angular/cli": "17.0.6", - "@angular/compiler-cli": "17.0.6", + "@angular-devkit/build-angular": "17.0.7", + "@angular/cli": "17.0.7", + "@angular/compiler-cli": "17.0.7", "@types/leaflet": "1.9.8", "@types/node": "20.10.4", "@types/uuid": "9.0.7", From 8adef81b39b23e1b2814d199aaa9f1717f562538 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Fri, 15 Dec 2023 01:08:21 -0300 Subject: [PATCH 42/87] [desktop]: Update NPM dependencies --- desktop/package-lock.json | 74 +++++++++++++++++++-------------------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/desktop/package-lock.json b/desktop/package-lock.json index 63fe84afc..8f245024d 100644 --- a/desktop/package-lock.json +++ b/desktop/package-lock.json @@ -628,14 +628,14 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", - "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", + "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.22.9", - "@babel/helper-validator-option": "^7.22.15", - "browserslist": "^4.21.9", + "@babel/compat-data": "^7.23.5", + "@babel/helper-validator-option": "^7.23.5", + "browserslist": "^4.22.2", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -936,14 +936,14 @@ } }, "node_modules/@babel/helpers": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.5.tgz", - "integrity": "sha512-oO7us8FzTEsG3U6ag9MfdF1iA/7Z6dz+MtFhifZk8C8o453rGJFFWUP1t+ULM9TUIAzC9uxXEiXjOiVMyd7QPg==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.6.tgz", + "integrity": "sha512-wCfsbN4nBidDRhpDhvcKlzHWCTlgJYUUdSJfzXb2NuBssDSIjc3xcb+znA7l+zYsFljAcGM0aFkN40cR3lXiGA==", "dev": true, "dependencies": { "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.5", - "@babel/types": "^7.23.5" + "@babel/traverse": "^7.23.6", + "@babel/types": "^7.23.6" }, "engines": { "node": ">=6.9.0" @@ -964,9 +964,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.5.tgz", - "integrity": "sha512-hOOqoiNXrmGdFbhgCzu6GiURxUgM27Xwd/aPuu8RfHEZPBzL1Z54okAHAQjXfcQNwvrlkAmAp4SlRTZ45vlthQ==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz", + "integrity": "sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -2206,20 +2206,20 @@ } }, "node_modules/@babel/traverse": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.5.tgz", - "integrity": "sha512-czx7Xy5a6sapWWRx61m1Ke1Ra4vczu1mCTtJam5zRTBOonfdJ+S/B6HYmGYu3fJtr8GGET3si6IhgWVBhJ/m8w==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.6.tgz", + "integrity": "sha512-czastdK1e8YByZqezMPFiZ8ahwVMh/ESl9vPgvgdB9AmFMGP5jfpFax74AQgl5zj4XHzqeYAg2l8PuUeRS1MgQ==", "dev": true, "dependencies": { "@babel/code-frame": "^7.23.5", - "@babel/generator": "^7.23.5", + "@babel/generator": "^7.23.6", "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-function-name": "^7.23.0", "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.23.5", - "@babel/types": "^7.23.5", - "debug": "^4.1.0", + "@babel/parser": "^7.23.6", + "@babel/types": "^7.23.6", + "debug": "^4.3.1", "globals": "^11.1.0" }, "engines": { @@ -2227,12 +2227,12 @@ } }, "node_modules/@babel/traverse/node_modules/@babel/generator": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.5.tgz", - "integrity": "sha512-BPssCHrBD+0YrxviOa3QzpqwhNIXKEtOa2jQrm4FlmkC2apYgRnQcmPWiGZDlGxiNtltnUFolMe8497Esry+jA==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", + "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", "dev": true, "dependencies": { - "@babel/types": "^7.23.5", + "@babel/types": "^7.23.6", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -2242,9 +2242,9 @@ } }, "node_modules/@babel/types": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.5.tgz", - "integrity": "sha512-ON5kSOJwVO6xXVRTvOI0eOnWe7VdUcIpsovGo9U/Br4Ie4UVFQTboO2cYnDhAGU6Fp+UxSiT+pMft0SMHfuq6w==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", + "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.23.4", @@ -3822,9 +3822,9 @@ } }, "node_modules/@types/eslint": { - "version": "8.44.8", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.8.tgz", - "integrity": "sha512-4K8GavROwhrYl2QXDXm0Rv9epkA8GBFu0EI+XrrnnuCl7u8CWBRusX7fXJfanhZTDWSAL24gDI/UqXyUM0Injw==", + "version": "8.44.9", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.9.tgz", + "integrity": "sha512-6yBxcvwnnYoYT1Uk2d+jvIfsuP4mb2EdIxFnrPABj5a/838qe5bGkNLFOiipX4ULQ7XVQvTxOh7jO+BTAiqsEw==", "dev": true, "dependencies": { "@types/estree": "*", @@ -5891,9 +5891,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001566", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001566.tgz", - "integrity": "sha512-ggIhCsTxmITBAMmK8yZjEhCO5/47jKXPu6Dha/wuCS4JePVL+3uiDEBuhu2aIoT+bqTOR8L76Ip1ARL9xYsEJA==", + "version": "1.0.30001570", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001570.tgz", + "integrity": "sha512-+3e0ASu4sw1SWaoCtvPeyXp+5PsjigkSt8OXZbF9StH5pQWbxEjLAZE3n8Aup5udop1uRiKA7a4utUk/uoSpUw==", "dev": true, "funding": [ { @@ -7841,9 +7841,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.609", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.609.tgz", - "integrity": "sha512-ihiCP7PJmjoGNuLpl7TjNA8pCQWu09vGyjlPYw1Rqww4gvNuCcmvl+44G+2QyJ6S2K4o+wbTS++Xz0YN8Q9ERw==", + "version": "1.4.613", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.613.tgz", + "integrity": "sha512-r4x5+FowKG6q+/Wj0W9nidx7QO31BJwmR2uEo+Qh3YLGQ8SbBAFuDFpTxzly/I2gsbrFwBuIjrMp423L3O5U3w==", "dev": true }, "node_modules/electron/node_modules/@types/node": { From c31948d445bb067ce0791a12b08241d223d7ae69 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Fri, 15 Dec 2023 14:18:17 -0300 Subject: [PATCH 43/87] [api]: Fix possible memory leak on PublishSubject; Improve CancellationToken --- .../api/alignment/polar/darv/DARVJob.kt | 5 -- .../api/cameras/CameraEventHandler.kt | 32 +++++----- .../api/cameras/DelayAndWaitForSettleStep.kt | 15 +++-- .../api/focusers/FocuserEventHandler.kt | 30 +++++---- .../api/guiding/GuideOutputEventHandler.kt | 30 +++++---- .../nebulosa/api/guiding/WaitForSettleStep.kt | 10 +-- .../nebulosa/api/mounts/MountEventHandler.kt | 30 +++++---- .../nebulosa/api/wheels/WheelEventHandler.kt | 32 +++++----- nebulosa-batch-processing/build.gradle.kts | 1 + .../batch/processing/AsyncJobLauncher.kt | 26 +++++++- .../nebulosa/batch/processing/JobExecution.kt | 41 +++++++++++- .../concurrency/CancellationListener.kt | 5 ++ .../common/concurrency/CancellationSource.kt | 12 ++++ .../common/concurrency/CancellationToken.kt | 64 ++++++++++++------- .../common/concurrency/CountUpDownLatch.kt | 8 ++- .../src/test/kotlin/CancellationTokenTest.kt | 57 +++++++++++++++++ .../nebulosa/guiding/phd2/PHD2Guider.kt | 6 +- .../main/kotlin/nebulosa/guiding/Guider.kt | 2 +- 18 files changed, 281 insertions(+), 125 deletions(-) create mode 100644 nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/CancellationListener.kt create mode 100644 nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/CancellationSource.kt create mode 100644 nebulosa-common/src/test/kotlin/CancellationTokenTest.kt diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVJob.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVJob.kt index 16f6551fd..177dffdf7 100644 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVJob.kt +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVJob.kt @@ -82,11 +82,6 @@ data class DARVJob( onNext(DARVInitialPauseElapsed(camera, guideOutput, remainingTime, progress)) } - override fun stop(mayInterruptIfRunning: Boolean) { - super.stop(mayInterruptIfRunning) - close() - } - companion object { @JvmStatic private val ID = Incrementer() diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraEventHandler.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraEventHandler.kt index 28e49ed67..df4dae057 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraEventHandler.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraEventHandler.kt @@ -1,7 +1,6 @@ package nebulosa.api.cameras import io.reactivex.rxjava3.subjects.PublishSubject -import jakarta.annotation.PostConstruct import nebulosa.api.beans.annotations.Subscriber import nebulosa.api.messages.MessageService import nebulosa.indi.device.PropertyChangedEvent @@ -12,40 +11,43 @@ import nebulosa.indi.device.camera.CameraEvent import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode import org.springframework.stereotype.Component +import java.io.Closeable import java.util.concurrent.TimeUnit @Component @Subscriber class CameraEventHandler( private val messageService: MessageService, -) { +) : Closeable { private val throttler = PublishSubject.create() - @PostConstruct - private fun initialize() { + init { throttler .throttleLast(1000, TimeUnit.MILLISECONDS) .subscribe { sendUpdate(it.device!!) } } @Subscribe(threadMode = ThreadMode.ASYNC) - fun onCameraEvent(event: CameraEvent) { + fun onFocuserEvent(event: CameraEvent) { when (event) { - is PropertyChangedEvent -> { - throttler.onNext(event) - } - is CameraAttached -> { - messageService.sendMessage(CameraMessageEvent(CAMERA_ATTACHED, event.device)) - } - is CameraDetached -> { - messageService.sendMessage(CameraMessageEvent(CAMERA_DETACHED, event.device)) - } + is PropertyChangedEvent -> throttler.onNext(event) + is CameraAttached -> sendMessage(CAMERA_ATTACHED, event.device) + is CameraDetached -> sendMessage(CAMERA_DETACHED, event.device) } } + @Suppress("NOTHING_TO_INLINE") + private inline fun sendMessage(eventName: String, device: Camera) { + messageService.sendMessage(CameraMessageEvent(eventName, device)) + } + fun sendUpdate(device: Camera) { - messageService.sendMessage(CameraMessageEvent(CAMERA_UPDATED, device)) + sendMessage(CAMERA_UPDATED, device) + } + + override fun close() { + throttler.onComplete() } companion object { diff --git a/api/src/main/kotlin/nebulosa/api/cameras/DelayAndWaitForSettleStep.kt b/api/src/main/kotlin/nebulosa/api/cameras/DelayAndWaitForSettleStep.kt index 6dd8e714b..9c1978ac3 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/DelayAndWaitForSettleStep.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/DelayAndWaitForSettleStep.kt @@ -3,9 +3,7 @@ package nebulosa.api.cameras import io.reactivex.rxjava3.subjects.PublishSubject import nebulosa.api.guiding.WaitForSettleListener import nebulosa.api.guiding.WaitForSettleStep -import nebulosa.batch.processing.PublishSubscribe -import nebulosa.batch.processing.SimpleSplitStep -import nebulosa.batch.processing.StepExecution +import nebulosa.batch.processing.* import nebulosa.batch.processing.delay.DelayStep import nebulosa.batch.processing.delay.DelayStepListener import nebulosa.indi.device.camera.Camera @@ -15,7 +13,8 @@ data class DelayAndWaitForSettleStep( @JvmField val camera: Camera, @JvmField val cameraDelayStep: DelayStep, @JvmField val waitForSettleStep: WaitForSettleStep, -) : SimpleSplitStep(cameraDelayStep, waitForSettleStep), PublishSubscribe, DelayStepListener, WaitForSettleListener { +) : SimpleSplitStep(cameraDelayStep, waitForSettleStep), PublishSubscribe, + JobExecutionListener, DelayStepListener, WaitForSettleListener { @Volatile private var settling = false @@ -31,6 +30,10 @@ data class DelayAndWaitForSettleStep( waitForSettleStep.unregisterWaitForSettleListener(this) } + override fun afterJob(jobExecution: JobExecution) { + close() + } + override fun onSettleStarted(step: WaitForSettleStep, stepExecution: StepExecution) { settling = true } @@ -40,9 +43,9 @@ data class DelayAndWaitForSettleStep( } override fun onDelayElapsed(step: DelayStep, stepExecution: StepExecution) { - val send = settling && (stepExecution.context[DelayStep.PROGRESS] as Double) < 1.0 + val canSendEvent = settling && (stepExecution.context[DelayStep.PROGRESS] as Double) < 1.0 - if (send) { + if (canSendEvent) { val exposureCount = stepExecution.context[CameraExposureStep.EXPOSURE_COUNT] as Int val captureElapsedTime = stepExecution.context[CameraExposureStep.CAPTURE_ELAPSED_TIME] as Duration val captureProgress = stepExecution.context[CameraExposureStep.CAPTURE_PROGRESS] as Double diff --git a/api/src/main/kotlin/nebulosa/api/focusers/FocuserEventHandler.kt b/api/src/main/kotlin/nebulosa/api/focusers/FocuserEventHandler.kt index e77752446..b5a5a0d9b 100644 --- a/api/src/main/kotlin/nebulosa/api/focusers/FocuserEventHandler.kt +++ b/api/src/main/kotlin/nebulosa/api/focusers/FocuserEventHandler.kt @@ -1,7 +1,6 @@ package nebulosa.api.focusers import io.reactivex.rxjava3.subjects.PublishSubject -import jakarta.annotation.PostConstruct import nebulosa.api.beans.annotations.Subscriber import nebulosa.api.messages.MessageService import nebulosa.indi.device.PropertyChangedEvent @@ -12,18 +11,18 @@ import nebulosa.indi.device.focuser.FocuserEvent import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode import org.springframework.stereotype.Component +import java.io.Closeable import java.util.concurrent.TimeUnit @Component @Subscriber class FocuserEventHandler( private val messageService: MessageService, -) { +) : Closeable { private val throttler = PublishSubject.create() - @PostConstruct - private fun initialize() { + init { throttler .throttleLast(1000, TimeUnit.MILLISECONDS) .subscribe { sendUpdate(it.device!!) } @@ -32,20 +31,23 @@ class FocuserEventHandler( @Subscribe(threadMode = ThreadMode.ASYNC) fun onFocuserEvent(event: FocuserEvent) { when (event) { - is PropertyChangedEvent -> { - throttler.onNext(event) - } - is FocuserAttached -> { - messageService.sendMessage(FocuserMessageEvent(FOCUSER_ATTACHED, event.device)) - } - is FocuserDetached -> { - messageService.sendMessage(FocuserMessageEvent(FOCUSER_DETACHED, event.device)) - } + is PropertyChangedEvent -> throttler.onNext(event) + is FocuserAttached -> sendMessage(FOCUSER_ATTACHED, event.device) + is FocuserDetached -> sendMessage(FOCUSER_DETACHED, event.device) } } + @Suppress("NOTHING_TO_INLINE") + private inline fun sendMessage(eventName: String, device: Focuser) { + messageService.sendMessage(FocuserMessageEvent(eventName, device)) + } + fun sendUpdate(device: Focuser) { - messageService.sendMessage(FocuserMessageEvent(FOCUSER_UPDATED, device)) + sendMessage(FOCUSER_UPDATED, device) + } + + override fun close() { + throttler.onComplete() } companion object { diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputEventHandler.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputEventHandler.kt index eea722189..0e2df478a 100644 --- a/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputEventHandler.kt +++ b/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputEventHandler.kt @@ -1,7 +1,6 @@ package nebulosa.api.guiding import io.reactivex.rxjava3.subjects.PublishSubject -import jakarta.annotation.PostConstruct import nebulosa.api.beans.annotations.Subscriber import nebulosa.api.messages.MessageService import nebulosa.indi.device.DeviceEvent @@ -12,18 +11,18 @@ import nebulosa.indi.device.guide.GuideOutputDetached import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode import org.springframework.stereotype.Component +import java.io.Closeable import java.util.concurrent.TimeUnit @Component @Subscriber class GuideOutputEventHandler( private val messageService: MessageService, -) { +) : Closeable { private val throttler = PublishSubject.create>() - @PostConstruct - private fun initialize() { + init { throttler .throttleLast(1000, TimeUnit.MILLISECONDS) .subscribe { sendUpdate(it.device!!) } @@ -31,24 +30,27 @@ class GuideOutputEventHandler( @Subscribe(threadMode = ThreadMode.ASYNC) fun onGuideOutputEvent(event: DeviceEvent) { - val device = event.device ?: return - - if (device.canPulseGuide && event is PropertyChangedEvent) { + if (event.device!!.canPulseGuide && event is PropertyChangedEvent) { throttler.onNext(event) } when (event) { - is GuideOutputAttached -> { - messageService.sendMessage(GuideOutputMessageEvent(GUIDE_OUTPUT_ATTACHED, event.device)) - } - is GuideOutputDetached -> { - messageService.sendMessage(GuideOutputMessageEvent(GUIDE_OUTPUT_DETACHED, event.device)) - } + is GuideOutputAttached -> sendMessage(GUIDE_OUTPUT_ATTACHED, event.device) + is GuideOutputDetached -> sendMessage(GUIDE_OUTPUT_DETACHED, event.device) } } + @Suppress("NOTHING_TO_INLINE") + private inline fun sendMessage(eventName: String, device: GuideOutput) { + messageService.sendMessage(GuideOutputMessageEvent(eventName, device)) + } + fun sendUpdate(device: GuideOutput) { - messageService.sendMessage(GuideOutputMessageEvent(GUIDE_OUTPUT_UPDATED, device)) + sendMessage(GUIDE_OUTPUT_UPDATED, device) + } + + override fun close() { + throttler.onComplete() } companion object { diff --git a/api/src/main/kotlin/nebulosa/api/guiding/WaitForSettleStep.kt b/api/src/main/kotlin/nebulosa/api/guiding/WaitForSettleStep.kt index 3765bee65..c5e314bec 100644 --- a/api/src/main/kotlin/nebulosa/api/guiding/WaitForSettleStep.kt +++ b/api/src/main/kotlin/nebulosa/api/guiding/WaitForSettleStep.kt @@ -1,12 +1,10 @@ package nebulosa.api.guiding import nebulosa.batch.processing.* -import nebulosa.common.concurrency.CancellationToken import nebulosa.guiding.Guider data class WaitForSettleStep(@JvmField val guider: Guider) : Step, JobExecutionListener { - private val cancellationToken = CancellationToken() private val listeners = LinkedHashSet() fun registerWaitForSettleListener(listener: WaitForSettleListener) { @@ -18,19 +16,15 @@ data class WaitForSettleStep(@JvmField val guider: Guider) : Step, JobExecutionL } override fun execute(stepExecution: StepExecution): StepResult { - if (guider.isSettling && !cancellationToken.isCancelled) { + if (guider.isSettling && !stepExecution.jobExecution.cancellationToken.isDone) { listeners.forEach { it.onSettleStarted(this, stepExecution) } - guider.waitForSettle(cancellationToken) + guider.waitForSettle(stepExecution.jobExecution.cancellationToken) listeners.forEach { it.onSettleFinished(this, stepExecution) } } return StepResult.FINISHED } - override fun stop(mayInterruptIfRunning: Boolean) { - cancellationToken.cancel() - } - override fun afterJob(jobExecution: JobExecution) { listeners.clear() } diff --git a/api/src/main/kotlin/nebulosa/api/mounts/MountEventHandler.kt b/api/src/main/kotlin/nebulosa/api/mounts/MountEventHandler.kt index 1a9787d12..3efd7d30d 100644 --- a/api/src/main/kotlin/nebulosa/api/mounts/MountEventHandler.kt +++ b/api/src/main/kotlin/nebulosa/api/mounts/MountEventHandler.kt @@ -1,7 +1,6 @@ package nebulosa.api.mounts import io.reactivex.rxjava3.subjects.PublishSubject -import jakarta.annotation.PostConstruct import nebulosa.api.beans.annotations.Subscriber import nebulosa.api.messages.MessageService import nebulosa.indi.device.PropertyChangedEvent @@ -12,18 +11,18 @@ import nebulosa.indi.device.mount.MountEvent import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode import org.springframework.stereotype.Component +import java.io.Closeable import java.util.concurrent.TimeUnit @Component @Subscriber class MountEventHandler( private val messageService: MessageService, -) { +) : Closeable { private val throttler = PublishSubject.create() - @PostConstruct - private fun initialize() { + init { throttler .throttleLast(1000, TimeUnit.MILLISECONDS) .subscribe { sendUpdate(it.device!!) } @@ -32,20 +31,23 @@ class MountEventHandler( @Subscribe(threadMode = ThreadMode.ASYNC) fun onMountEvent(event: MountEvent) { when (event) { - is PropertyChangedEvent -> { - throttler.onNext(event) - } - is MountAttached -> { - messageService.sendMessage(MountMessageEvent(MOUNT_ATTACHED, event.device)) - } - is MountDetached -> { - messageService.sendMessage(MountMessageEvent(MOUNT_DETACHED, event.device)) - } + is PropertyChangedEvent -> throttler.onNext(event) + is MountAttached -> sendMessage(MOUNT_ATTACHED, event.device) + is MountDetached -> sendMessage(MOUNT_DETACHED, event.device) } } + @Suppress("NOTHING_TO_INLINE") + private inline fun sendMessage(eventName: String, device: Mount) { + messageService.sendMessage(MountMessageEvent(eventName, device)) + } + fun sendUpdate(device: Mount) { - messageService.sendMessage(MountMessageEvent(MOUNT_UPDATED, device)) + sendMessage(MOUNT_UPDATED, device) + } + + override fun close() { + throttler.onComplete() } companion object { diff --git a/api/src/main/kotlin/nebulosa/api/wheels/WheelEventHandler.kt b/api/src/main/kotlin/nebulosa/api/wheels/WheelEventHandler.kt index f9d9988e5..4fec38e56 100644 --- a/api/src/main/kotlin/nebulosa/api/wheels/WheelEventHandler.kt +++ b/api/src/main/kotlin/nebulosa/api/wheels/WheelEventHandler.kt @@ -1,7 +1,6 @@ package nebulosa.api.wheels import io.reactivex.rxjava3.subjects.PublishSubject -import jakarta.annotation.PostConstruct import nebulosa.api.beans.annotations.Subscriber import nebulosa.api.messages.MessageService import nebulosa.indi.device.PropertyChangedEvent @@ -12,40 +11,43 @@ import nebulosa.indi.device.filterwheel.FilterWheelEvent import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode import org.springframework.stereotype.Component +import java.io.Closeable import java.util.concurrent.TimeUnit @Component @Subscriber class WheelEventHandler( private val messageService: MessageService, -) { +) : Closeable { private val throttler = PublishSubject.create() - @PostConstruct - private fun initialize() { + init { throttler .throttleLast(1000, TimeUnit.MILLISECONDS) .subscribe { sendUpdate(it.device!!) } } @Subscribe(threadMode = ThreadMode.ASYNC) - fun onFilterWheelEvent(event: FilterWheelEvent) { + fun onFocuserEvent(event: FilterWheelEvent) { when (event) { - is PropertyChangedEvent -> { - throttler.onNext(event) - } - is FilterWheelAttached -> { - messageService.sendMessage(WheelMessageEvent(WHEEL_ATTACHED, event.device)) - } - is FilterWheelDetached -> { - messageService.sendMessage(WheelMessageEvent(WHEEL_DETACHED, event.device)) - } + is PropertyChangedEvent -> throttler.onNext(event) + is FilterWheelAttached -> sendMessage(WHEEL_ATTACHED, event.device) + is FilterWheelDetached -> sendMessage(WHEEL_DETACHED, event.device) } } + @Suppress("NOTHING_TO_INLINE") + private inline fun sendMessage(eventName: String, device: FilterWheel) { + messageService.sendMessage(WheelMessageEvent(eventName, device)) + } + fun sendUpdate(device: FilterWheel) { - messageService.sendMessage(WheelMessageEvent(WHEEL_UPDATED, device)) + sendMessage(WHEEL_UPDATED, device) + } + + override fun close() { + throttler.onComplete() } companion object { diff --git a/nebulosa-batch-processing/build.gradle.kts b/nebulosa-batch-processing/build.gradle.kts index 73f4f81c5..2381c9353 100644 --- a/nebulosa-batch-processing/build.gradle.kts +++ b/nebulosa-batch-processing/build.gradle.kts @@ -4,6 +4,7 @@ plugins { } dependencies { + api(project(":nebulosa-common")) api(libs.rx) implementation(project(":nebulosa-log")) testImplementation(project(":nebulosa-test")) diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/AsyncJobLauncher.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/AsyncJobLauncher.kt index 221b17265..57e262674 100644 --- a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/AsyncJobLauncher.kt +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/AsyncJobLauncher.kt @@ -1,10 +1,13 @@ package nebulosa.batch.processing +import nebulosa.common.concurrency.CancellationListener +import nebulosa.common.concurrency.CancellationSource import nebulosa.log.loggerFor +import java.io.Closeable import java.time.LocalDateTime import java.util.concurrent.Executor -open class AsyncJobLauncher(private val executor: Executor) : JobLauncher, StepInterceptor { +open class AsyncJobLauncher(private val executor: Executor) : JobLauncher, StepInterceptor, CancellationListener { private val jobListeners = LinkedHashSet() private val stepListeners = LinkedHashSet() @@ -61,8 +64,10 @@ open class AsyncJobLauncher(private val executor: Executor) : JobLauncher, StepI var jobExecution = jobs[job.id] if (jobExecution != null) { - if (!jobExecution.isDone) { + if (!jobExecution.isDone || jobExecution.isStopping) { return jobExecution + } else { + throw IllegalStateException("job cannot start again") } } @@ -74,6 +79,8 @@ open class AsyncJobLauncher(private val executor: Executor) : JobLauncher, StepI jobs[job.id] = jobExecution + jobExecution.cancellationToken.listen(this) + executor.execute { jobExecution.status = JobStatus.STARTED @@ -104,11 +111,16 @@ open class AsyncJobLauncher(private val executor: Executor) : JobLauncher, StepI jobExecution.completeExceptionally(e) } finally { jobExecution.finishedAt = LocalDateTime.now() + jobExecution.cancellationToken.unlisten(this) } job.afterJob(jobExecution) jobListeners.forEach { it.afterJob(jobExecution) } stepJobListeners.forEach { it.afterJob(jobExecution) } + + if (job is Closeable) { + job.close() + } } return jobExecution @@ -122,6 +134,10 @@ open class AsyncJobLauncher(private val executor: Executor) : JobLauncher, StepI if (!jobExecution.isDone && !jobExecution.isStopping) { jobExecution.status = JobStatus.STOPPING jobExecution.job.stop(mayInterruptIfRunning) + + if (!jobExecution.cancellationToken.isDone) { + jobExecution.cancellationToken.cancel(mayInterruptIfRunning) + } } } @@ -132,6 +148,12 @@ open class AsyncJobLauncher(private val executor: Executor) : JobLauncher, StepI return result } + override fun accept(source: CancellationSource) { + if (source is CancellationSource.Cancel) { + stop(source.mayInterruptIfRunning) + } + } + override fun toString() = "AsyncJobLauncher" companion object { diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobExecution.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobExecution.kt index 7dfed6d38..02ebba1ff 100644 --- a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobExecution.kt +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobExecution.kt @@ -1,21 +1,27 @@ package nebulosa.batch.processing +import nebulosa.common.concurrency.CancellationToken import java.time.LocalDateTime import java.util.concurrent.CompletableFuture import java.util.concurrent.ExecutionException import java.util.concurrent.TimeUnit -data class JobExecution( +class JobExecution( val job: Job, val context: ExecutionContext, val jobLauncher: JobLauncher, val stepInterceptors: List, val startedAt: LocalDateTime = LocalDateTime.now(), - var status: JobStatus = JobStatus.STARTING, - var finishedAt: LocalDateTime? = null, ) { + var status = JobStatus.STARTING + internal set + + var finishedAt: LocalDateTime? = null + internal set + @JvmField internal val completable = CompletableFuture() + @JvmField val cancellationToken = CancellationToken() inline val jobId get() = job.id @@ -55,4 +61,33 @@ data class JobExecution( internal fun completeExceptionally(e: Throwable) { completable.completeExceptionally(e) } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is JobExecution) return false + + if (job != other.job) return false + if (context != other.context) return false + if (jobLauncher != other.jobLauncher) return false + if (stepInterceptors != other.stepInterceptors) return false + if (startedAt != other.startedAt) return false + if (status != other.status) return false + if (finishedAt != other.finishedAt) return false + + return true + } + + override fun hashCode(): Int { + var result = job.hashCode() + result = 31 * result + context.hashCode() + result = 31 * result + jobLauncher.hashCode() + result = 31 * result + stepInterceptors.hashCode() + result = 31 * result + startedAt.hashCode() + result = 31 * result + status.hashCode() + result = 31 * result + (finishedAt?.hashCode() ?: 0) + return result + } + + override fun toString() = "JobExecution(job=$job, context=$context, startedAt=$startedAt," + + " status=$status, finishedAt=$finishedAt)" } diff --git a/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/CancellationListener.kt b/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/CancellationListener.kt new file mode 100644 index 000000000..c75194c88 --- /dev/null +++ b/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/CancellationListener.kt @@ -0,0 +1,5 @@ +package nebulosa.common.concurrency + +import java.util.function.Consumer + +fun interface CancellationListener : Consumer diff --git a/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/CancellationSource.kt b/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/CancellationSource.kt new file mode 100644 index 000000000..4f7adc76e --- /dev/null +++ b/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/CancellationSource.kt @@ -0,0 +1,12 @@ +package nebulosa.common.concurrency + +sealed interface CancellationSource { + + data object None : CancellationSource + + data object Listen : CancellationSource + + data class Cancel(val mayInterruptIfRunning: Boolean) : CancellationSource + + data object Close : CancellationSource +} diff --git a/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/CancellationToken.kt b/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/CancellationToken.kt index 00d954d68..d749cd9d5 100644 --- a/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/CancellationToken.kt +++ b/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/CancellationToken.kt @@ -1,60 +1,76 @@ package nebulosa.common.concurrency import java.io.Closeable +import java.util.concurrent.CompletableFuture import java.util.concurrent.Future import java.util.concurrent.TimeUnit -class CancellationToken : Closeable, Future { +class CancellationToken private constructor(private val completable: CompletableFuture?) : Closeable, Future { - private val latch = CountUpDownLatch(1) - private val listeners = LinkedHashSet() + constructor() : this(CompletableFuture()) - fun listen(action: Runnable): Boolean { - return if (isDone) { - action.run() + private val listeners = LinkedHashSet() + + init { + completable + ?.whenCompleteAsync { source, _ -> + if (source != null) { + listeners.forEach { it.accept(source) } + } + + listeners.clear() + } + } + + fun listen(listener: CancellationListener): Boolean { + return if (completable == null) { + false + } else if (isDone) { + listener.accept(CancellationSource.Listen) false } else { - listeners.add(action) + listeners.add(listener) } } + fun unlisten(listener: CancellationListener): Boolean { + return listeners.remove(listener) + } + fun cancel() { cancel(true) } @Synchronized override fun cancel(mayInterruptIfRunning: Boolean): Boolean { - if (latch.count <= 0) return false - latch.reset() - listeners.forEach(Runnable::run) - listeners.clear() + completable?.complete(CancellationSource.Cancel(mayInterruptIfRunning)) ?: return false return true } override fun isCancelled(): Boolean { - return latch.get() + return isDone } override fun isDone(): Boolean { - return latch.get() + return completable?.isDone ?: true } - override fun get(): Boolean { - latch.await() - return true + override fun get(): CancellationSource { + return completable?.get() ?: CancellationSource.None } - override fun get(timeout: Long, unit: TimeUnit): Boolean { - return latch.await(timeout, unit) + override fun get(timeout: Long, unit: TimeUnit): CancellationSource { + return completable?.get(timeout, unit) ?: CancellationSource.None } - fun reset() { - latch.countUp(1 - latch.count) - listeners.clear() + override fun close() { + if (!isDone) { + completable?.complete(CancellationSource.Close) + } } - override fun close() { - latch.reset() - listeners.clear() + companion object { + + @JvmStatic val NONE = CancellationToken(null) } } 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 3b373dddf..1c3ea045a 100644 --- a/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/CountUpDownLatch.kt +++ b/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/CountUpDownLatch.kt @@ -6,7 +6,7 @@ import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.locks.AbstractQueuedSynchronizer import kotlin.math.max -class CountUpDownLatch(initialCount: Int = 0) : AtomicBoolean(initialCount == 0) { +class CountUpDownLatch(initialCount: Int = 0) : AtomicBoolean(initialCount == 0), CancellationListener { private val sync = Sync(this) @@ -51,6 +51,12 @@ class CountUpDownLatch(initialCount: Int = 0) : AtomicBoolean(initialCount == 0) return n >= 0 && sync.tryAcquireSharedNanos(n, timeout.toNanos()) } + override fun accept(source: CancellationSource) { + if (source !== CancellationSource.None) { + reset() + } + } + private class Sync(private val latch: AtomicBoolean) : AbstractQueuedSynchronizer() { var count diff --git a/nebulosa-common/src/test/kotlin/CancellationTokenTest.kt b/nebulosa-common/src/test/kotlin/CancellationTokenTest.kt new file mode 100644 index 000000000..428f5d8de --- /dev/null +++ b/nebulosa-common/src/test/kotlin/CancellationTokenTest.kt @@ -0,0 +1,57 @@ +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.booleans.shouldBeTrue +import io.kotest.matchers.nulls.shouldBeNull +import io.kotest.matchers.shouldBe +import nebulosa.common.concurrency.CancellationSource +import nebulosa.common.concurrency.CancellationToken + +class CancellationTokenTest : StringSpec() { + + init { + "cancel" { + var source: CancellationSource? = null + val token = CancellationToken() + token.listen { source = it } + token.cancel(false) + token.get() shouldBe source + source shouldBe CancellationSource.Cancel(false) + token.isDone.shouldBeTrue() + } + "cancel may interrupt if running" { + var source: CancellationSource? = null + val token = CancellationToken() + token.listen { source = it } + token.cancel() + token.get() shouldBe source + source shouldBe CancellationSource.Cancel(true) + token.isDone.shouldBeTrue() + } + "close" { + var source: CancellationSource? = null + val token = CancellationToken() + token.listen { source = it } + token.close() + token.get() shouldBe source + source shouldBe CancellationSource.Close + token.isDone.shouldBeTrue() + } + "listen" { + var source: CancellationSource? = null + val token = CancellationToken() + token.cancel() + token.listen { source = it } + token.get() shouldBe CancellationSource.Cancel(true) + source shouldBe CancellationSource.Listen + token.isDone.shouldBeTrue() + } + "none" { + var source: CancellationSource? = null + val token = CancellationToken.NONE + token.isDone.shouldBeTrue() + token.listen { source = it } + token.cancel() + token.get() shouldBe CancellationSource.None + source.shouldBeNull() + } + } +} 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 daaa58bcc..95ba77531 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 @@ -233,16 +233,16 @@ class PHD2Guider(private val client: PHD2Client) : Guider, PHD2EventListener { } } - override fun waitForSettle(cancellationToken: CancellationToken?) { + override fun waitForSettle(cancellationToken: CancellationToken) { try { - cancellationToken?.listen(settling::reset) + cancellationToken.listen(settling) settling.await(settleTimeout) } catch (e: InterruptedException) { LOG.warn("PHD2 did not send SettleDone message in expected time") } catch (e: Throwable) { LOG.warn("an error occurrs while waiting for settle done", e) } finally { - cancellationToken?.close() + cancellationToken.unlisten(settling) settling.reset() } } diff --git a/nebulosa-guiding/src/main/kotlin/nebulosa/guiding/Guider.kt b/nebulosa-guiding/src/main/kotlin/nebulosa/guiding/Guider.kt index 178a039c3..6d8ccc073 100644 --- a/nebulosa-guiding/src/main/kotlin/nebulosa/guiding/Guider.kt +++ b/nebulosa-guiding/src/main/kotlin/nebulosa/guiding/Guider.kt @@ -36,7 +36,7 @@ interface Guider : Closeable { fun dither(amount: Double, raOnly: Boolean = false) - fun waitForSettle(cancellationToken: CancellationToken? = null) + fun waitForSettle(cancellationToken: CancellationToken = CancellationToken.NONE) companion object { From 85bcefc80cf999463c55de3b010dd0dd6f400a66 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Fri, 15 Dec 2023 14:18:35 -0300 Subject: [PATCH 44/87] [desktop]: Improve Calibration layout --- .../src/app/calibration/calibration.component.html | 14 ++++++++++++-- .../src/shared/services/browser-window.service.ts | 2 +- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/desktop/src/app/calibration/calibration.component.html b/desktop/src/app/calibration/calibration.component.html index cffb48474..016a0a634 100644 --- a/desktop/src/app/calibration/calibration.component.html +++ b/desktop/src/app/calibration/calibration.component.html @@ -1,13 +1,17 @@
+
+

Groups

+
Type + # Filter Duration Size @@ -19,6 +23,7 @@ {{ item.key.type }} + {{ item.frames.length }} {{ item.key.filter ?? '' }} {{ item.key.exposureTime | exposureTime }} {{ item.key.width }}x{{ item.key.height }} @@ -29,12 +34,15 @@
+
+

Frames

+
@@ -67,6 +75,8 @@
+
+
diff --git a/desktop/src/shared/services/browser-window.service.ts b/desktop/src/shared/services/browser-window.service.ts index 4b6ef0a42..7e5adf381 100644 --- a/desktop/src/shared/services/browser-window.service.ts +++ b/desktop/src/shared/services/browser-window.service.ts @@ -102,7 +102,7 @@ export class BrowserWindowService { openCalibration(options: OpenWindowOptions) { options.icon ||= 'stack' options.width ||= 510 - options.height ||= 526 + options.height ||= 508 this.openWindow({ ...options, id: 'calibration', path: 'calibration' }) } From 0fdbcf8eb5e7bdee24b3932810ac1aa49d5a0f7c Mon Sep 17 00:00:00 2001 From: tiagohm Date: Sat, 16 Dec 2023 12:43:51 -0300 Subject: [PATCH 45/87] [api][desktop]: Implement Sequencer --- api/Main.run.xml | 2 +- .../api/alignment/polar/darv/DARVExecutor.kt | 3 +- .../api/alignment/polar/darv/DARVJob.kt | 24 ++-- .../beans/configurations/BeanConfiguration.kt | 3 + .../beans/converters/DeviceDeserializer.kt | 36 ++++++ .../api/cameras/CameraCaptureEvent.kt | 3 +- .../api/cameras/CameraCaptureEventHandler.kt | 95 ++++++++++++++ .../api/cameras/CameraCaptureExecutor.kt | 6 +- .../api/cameras/CameraCaptureFinished.kt | 3 + .../api/cameras/CameraCaptureIsSettling.kt | 21 ---- .../api/cameras/CameraCaptureIsWaiting.kt | 5 +- .../nebulosa/api/cameras/CameraCaptureJob.kt | 116 +++--------------- .../api/cameras/CameraCaptureStarted.kt | 3 + .../api/cameras/CameraDeserializer.kt | 18 +-- .../api/cameras/CameraExposureElapsed.kt | 3 + .../api/cameras/CameraExposureFinished.kt | 3 + .../api/cameras/CameraExposureStarted.kt | 3 + .../api/cameras/CameraExposureStep.kt | 53 ++++---- .../api/cameras/CameraStartCaptureRequest.kt | 13 +- .../api/cameras/DelayAndWaitForSettleStep.kt | 57 --------- .../api/focusers/FocuserDeserializer.kt | 18 +++ .../nebulosa/api/guiding/WaitForSettleStep.kt | 7 ++ .../nebulosa/api/mounts/MountDeserializer.kt | 18 +++ .../api/sequencer/AutoFocusAfterConditions.kt | 21 ++++ .../api/sequencer/JobExecutionEvent.kt | 8 ++ .../api/sequencer/SequenceCaptureMode.kt | 10 ++ .../api/sequencer/SequencePlanRequest.kt | 20 +++ .../api/sequencer/SequencerController.kt | 23 ++++ .../nebulosa/api/sequencer/SequencerEvent.kt | 12 ++ .../api/sequencer/SequencerExecutor.kt | 57 +++++++++ .../nebulosa/api/sequencer/SequencerJob.kt | 116 ++++++++++++++++++ .../api/sequencer/SequencerService.kt | 29 +++++ .../nebulosa/api/wheels/WheelDeserializer.kt | 18 +++ api/src/main/resources/application.yml | 4 + desktop/src/app/camera/camera.component.html | 2 +- desktop/src/app/camera/camera.component.ts | 28 +++-- .../batch/processing/AsyncJobLauncher.kt | 25 ++-- .../batch/processing/DefaultStepHandler.kt | 5 +- .../batch/processing/ExecutionContext.kt | 53 ++++++++ .../kotlin/nebulosa/batch/processing/Job.kt | 2 - .../nebulosa/batch/processing/JobExecution.kt | 3 - .../batch/processing/delay/DelayStep.kt | 4 + .../src/test/kotlin/BatchProcessingTest.kt | 2 - 43 files changed, 690 insertions(+), 265 deletions(-) create mode 100644 api/src/main/kotlin/nebulosa/api/beans/converters/DeviceDeserializer.kt create mode 100644 api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureEventHandler.kt delete mode 100644 api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureIsSettling.kt delete mode 100644 api/src/main/kotlin/nebulosa/api/cameras/DelayAndWaitForSettleStep.kt create mode 100644 api/src/main/kotlin/nebulosa/api/focusers/FocuserDeserializer.kt create mode 100644 api/src/main/kotlin/nebulosa/api/mounts/MountDeserializer.kt create mode 100644 api/src/main/kotlin/nebulosa/api/sequencer/AutoFocusAfterConditions.kt create mode 100644 api/src/main/kotlin/nebulosa/api/sequencer/JobExecutionEvent.kt create mode 100644 api/src/main/kotlin/nebulosa/api/sequencer/SequenceCaptureMode.kt create mode 100644 api/src/main/kotlin/nebulosa/api/sequencer/SequencePlanRequest.kt create mode 100644 api/src/main/kotlin/nebulosa/api/sequencer/SequencerController.kt create mode 100644 api/src/main/kotlin/nebulosa/api/sequencer/SequencerEvent.kt create mode 100644 api/src/main/kotlin/nebulosa/api/sequencer/SequencerExecutor.kt create mode 100644 api/src/main/kotlin/nebulosa/api/sequencer/SequencerJob.kt create mode 100644 api/src/main/kotlin/nebulosa/api/sequencer/SequencerService.kt create mode 100644 api/src/main/kotlin/nebulosa/api/wheels/WheelDeserializer.kt diff --git a/api/Main.run.xml b/api/Main.run.xml index 026badb9c..69d9c73ea 100644 --- a/api/Main.run.xml +++ b/api/Main.run.xml @@ -6,7 +6,7 @@ value="nebulosa.api.MainKt"/>
- {{ waiting ? 'waiting' : capturing ? 'capturing' : 'idle' }} + {{ settling ? 'settling' : waiting ? 'waiting' : capturing ? 'capturing' : 'idle' }} {{ exposure.count }} diff --git a/desktop/src/app/camera/camera.component.ts b/desktop/src/app/camera/camera.component.ts index 6638ffa5f..864985e1f 100644 --- a/desktop/src/app/camera/camera.component.ts +++ b/desktop/src/app/camera/camera.component.ts @@ -5,7 +5,7 @@ import { ApiService } from '../../shared/services/api.service' import { BrowserWindowService } from '../../shared/services/browser-window.service' import { ElectronService } from '../../shared/services/electron.service' import { LocalStorageService } from '../../shared/services/local-storage.service' -import { AutoSubFolderMode, Camera, CameraStartCapture, Dither, ExposureMode, ExposureTimeUnit, FilterWheel, FrameType } from '../../shared/types' +import { AutoSubFolderMode, Camera, CameraCaptureState, CameraStartCapture, Dither, ExposureMode, ExposureTimeUnit, FilterWheel, FrameType } from '../../shared/types' import { AppComponent } from '../app.component' export interface CameraPreference { @@ -161,8 +161,19 @@ export class CameraComponent implements AfterContentInit, OnDestroy { offsetMin = 0 offsetMax = 0 - capturing = false - waiting = false + state?: CameraCaptureState + + get capturing() { + return this.state === 'EXPOSURING' + } + + get waiting() { + return this.state === 'WAITING' + } + + get settling() { + return this.state === 'SETTLING' + } readonly exposure = { count: 0, @@ -258,16 +269,17 @@ export class CameraComponent implements AfterContentInit, OnDestroy { if (event.state === 'WAITING') { this.wait.remainingTime = event.waitRemainingTime this.wait.progress = event.waitProgress - this.waiting = true + this.state = event.state + } else if (event.state === 'SETTLING') { + this.state = event.state } else if (event.state === 'CAPTURE_STARTED') { this.capture.looping = event.exposureAmount <= 0 this.capture.amount = event.exposureAmount - this.capturing = true + this.state = 'EXPOSURING' } else if (event.state === 'CAPTURE_FINISHED') { - this.capturing = false - this.waiting = false + this.state = undefined } else if (event.state === 'EXPOSURE_STARTED') { - this.waiting = false + this.state = 'EXPOSURING' } }) } diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/AsyncJobLauncher.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/AsyncJobLauncher.kt index 57e262674..09dbad010 100644 --- a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/AsyncJobLauncher.kt +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/AsyncJobLauncher.kt @@ -2,6 +2,7 @@ package nebulosa.batch.processing import nebulosa.common.concurrency.CancellationListener import nebulosa.common.concurrency.CancellationSource +import nebulosa.log.debug import nebulosa.log.loggerFor import java.io.Closeable import java.time.LocalDateTime @@ -12,7 +13,7 @@ open class AsyncJobLauncher(private val executor: Executor) : JobLauncher, StepI private val jobListeners = LinkedHashSet() private val stepListeners = LinkedHashSet() private val stepInterceptors = LinkedHashSet() - private val jobs = LinkedHashMap() + private val jobs = LinkedHashSet() override var stepHandler: StepHandler = DefaultStepHandler @@ -44,7 +45,7 @@ open class AsyncJobLauncher(private val executor: Executor) : JobLauncher, StepI get() = jobs.size override fun contains(element: JobExecution): Boolean { - return jobs.containsValue(element) + return element in jobs } override fun containsAll(elements: Collection): Boolean { @@ -56,18 +57,17 @@ open class AsyncJobLauncher(private val executor: Executor) : JobLauncher, StepI } override fun iterator(): Iterator { - return jobs.values.iterator() + return jobs.iterator() } @Synchronized override fun launch(job: Job, executionContext: ExecutionContext?): JobExecution { - var jobExecution = jobs[job.id] + var jobExecution = jobs.find { it.job === job } if (jobExecution != null) { if (!jobExecution.isDone || jobExecution.isStopping) { + LOG.warn("unable to launch new job {}, because it is running or stopping.", job::class.simpleName) return jobExecution - } else { - throw IllegalStateException("job cannot start again") } } @@ -75,15 +75,17 @@ open class AsyncJobLauncher(private val executor: Executor) : JobLauncher, StepI interceptors.addAll(stepInterceptors) interceptors.add(this) - jobExecution = JobExecution(job, executionContext ?: ExecutionContext(), this, interceptors) + jobExecution = JobExecution(job, executionContext ?: jobExecution?.context ?: ExecutionContext(), this, interceptors) - jobs[job.id] = jobExecution + jobs.add(jobExecution) jobExecution.cancellationToken.listen(this) executor.execute { jobExecution.status = JobStatus.STARTED + LOG.debug { "job started. job={}".format(job) } + job.beforeJob(jobExecution) jobListeners.forEach { it.beforeJob(jobExecution) } @@ -105,6 +107,8 @@ open class AsyncJobLauncher(private val executor: Executor) : JobLauncher, StepI jobExecution.status = if (jobExecution.isStopping) JobStatus.STOPPED else JobStatus.COMPLETED jobExecution.complete() + + LOG.debug { "job finished. job={}".format(job) } } catch (e: Throwable) { LOG.error("job failed. job=$job, jobExecution=$jobExecution", e) jobExecution.status = JobStatus.FAILED @@ -121,13 +125,16 @@ open class AsyncJobLauncher(private val executor: Executor) : JobLauncher, StepI if (job is Closeable) { job.close() } + if (job is MutableCollection<*>) { + job.clear() + } } return jobExecution } override fun stop(mayInterruptIfRunning: Boolean) { - jobs.forEach { stop(it.value, mayInterruptIfRunning) } + jobs.forEach { stop(it, mayInterruptIfRunning) } } override fun stop(jobExecution: JobExecution, mayInterruptIfRunning: Boolean) { diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/DefaultStepHandler.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/DefaultStepHandler.kt index 248aea50f..86ad96e04 100644 --- a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/DefaultStepHandler.kt +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/DefaultStepHandler.kt @@ -1,5 +1,6 @@ package nebulosa.batch.processing +import nebulosa.log.debug import nebulosa.log.loggerFor object DefaultStepHandler : StepHandler { @@ -23,14 +24,14 @@ object DefaultStepHandler : StepHandler { else -> { val chain = StepInterceptorChain(stepExecution.jobExecution.stepInterceptors, step, stepExecution) - LOG.info("step started. step={}, context={}", step, stepExecution.context) + LOG.debug { "step started. step=%s, context=%s".format(step, stepExecution.context) } while (stepExecution.jobExecution.canContinue) { val status = chain.proceed().get() if (status != RepeatStatus.CONTINUABLE) break } - LOG.info("step finished. step={}, context={}", step, stepExecution.context) + LOG.debug { "step finished. step=%s, context=%s".format(step, stepExecution.context) } } } diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/ExecutionContext.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/ExecutionContext.kt index 0525cbad9..f1290ba0c 100644 --- a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/ExecutionContext.kt +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/ExecutionContext.kt @@ -1,5 +1,7 @@ package nebulosa.batch.processing +import java.nio.file.Path +import java.time.Duration import java.util.concurrent.ConcurrentHashMap open class ExecutionContext : ConcurrentHashMap { @@ -7,4 +9,55 @@ open class ExecutionContext : ConcurrentHashMap { constructor(initialCapacity: Int = 64) : super(initialCapacity) constructor(context: ExecutionContext) : super(context) + + companion object { + + @JvmStatic + fun ExecutionContext.getOrNull(key: String, type: Class) = if (containsKey(key)) type.cast(this[key]) + else null + + @JvmStatic + fun ExecutionContext.getBoolean(key: String, value: Boolean = false) = if (containsKey(key)) this[key] as Boolean + else value + + @JvmStatic + fun ExecutionContext.getBooleanOrNull(key: String) = if (containsKey(key)) this[key] as? Boolean + else null + + @JvmStatic + fun ExecutionContext.getInt(key: String, value: Int = 0) = if (containsKey(key)) this[key] as Int + else value + + @JvmStatic + fun ExecutionContext.getIntOrNull(key: String) = if (containsKey(key)) this[key] as? Int + else null + + @JvmStatic + fun ExecutionContext.getDouble(key: String, value: Double = 0.0) = if (containsKey(key)) this[key] as Double + else value + + @JvmStatic + fun ExecutionContext.getDoubleOrNull(key: String) = if (containsKey(key)) this[key] as? Double + else null + + @JvmStatic + fun ExecutionContext.getText(key: String, value: String = "") = if (containsKey(key)) this[key] as String + else value + + @JvmStatic + fun ExecutionContext.getTextOrNull(key: String) = if (containsKey(key)) this[key] as? String + else null + + @JvmStatic + fun ExecutionContext.getDuration(key: String, value: Duration = Duration.ZERO) = if (containsKey(key)) this[key] as Duration + else value + + @JvmStatic + fun ExecutionContext.getDurationOrNull(key: String) = if (containsKey(key)) this[key] as? Duration + else null + + @JvmStatic + fun ExecutionContext.getPath(key: String) = if (containsKey(key)) this[key] as? Path + else null + } } diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/Job.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/Job.kt index 11dcf486a..60c35e8a4 100644 --- a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/Job.kt +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/Job.kt @@ -2,8 +2,6 @@ package nebulosa.batch.processing interface Job : JobExecutionListener, Stoppable { - val id: String - fun hasNext(jobExecution: JobExecution): Boolean fun next(jobExecution: JobExecution): Step diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobExecution.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobExecution.kt index 02ebba1ff..b703352be 100644 --- a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobExecution.kt +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/JobExecution.kt @@ -23,9 +23,6 @@ class JobExecution( @JvmField internal val completable = CompletableFuture() @JvmField val cancellationToken = CancellationToken() - inline val jobId - get() = job.id - inline val canContinue get() = status == JobStatus.STARTED diff --git a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/delay/DelayStep.kt b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/delay/DelayStep.kt index 894f28ad7..e1a643f4a 100644 --- a/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/delay/DelayStep.kt +++ b/nebulosa-batch-processing/src/main/kotlin/nebulosa/batch/processing/delay/DelayStep.kt @@ -27,6 +27,7 @@ data class DelayStep(@JvmField val duration: Duration) : Step, JobExecutionListe if (waitTime > Duration.ZERO) { stepExecution.context[REMAINING_TIME] = remainingTime stepExecution.context[WAIT_TIME] = waitTime + stepExecution.context[WAITING] = true val progress = (duration.toNanos() - remainingTime.toNanos()) / duration.toNanos().toDouble() stepExecution.context[PROGRESS] = progress @@ -39,6 +40,8 @@ data class DelayStep(@JvmField val duration: Duration) : Step, JobExecutionListe stepExecution.context[REMAINING_TIME] = Duration.ZERO stepExecution.context[WAIT_TIME] = Duration.ZERO + stepExecution.context[PROGRESS] = 1.0 + stepExecution.context[WAITING] = false listeners.forEach { it.onDelayElapsed(this, stepExecution) } } @@ -59,6 +62,7 @@ data class DelayStep(@JvmField val duration: Duration) : Step, JobExecutionListe const val REMAINING_TIME = "DELAY.REMAINING_TIME" const val WAIT_TIME = "DELAY.WAIT_TIME" const val PROGRESS = "DELAY.PROGRESS" + const val WAITING = "DELAY.WAITING" @JvmField val DELAY_INTERVAL = Duration.ofMillis(500)!! } diff --git a/nebulosa-batch-processing/src/test/kotlin/BatchProcessingTest.kt b/nebulosa-batch-processing/src/test/kotlin/BatchProcessingTest.kt index d9cb0d00b..9e4ec2202 100644 --- a/nebulosa-batch-processing/src/test/kotlin/BatchProcessingTest.kt +++ b/nebulosa-batch-processing/src/test/kotlin/BatchProcessingTest.kt @@ -70,8 +70,6 @@ class BatchProcessingTest : StringSpec() { private val initialValue: Double = 0.0, ) : SimpleJob(steps) { - override val id = "Job.Math" - override fun beforeJob(jobExecution: JobExecution) { jobExecution.context["VALUE"] = initialValue } From bc3770e9a918dbf1b1dc924fc57c95ddfb6bc200 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Sat, 16 Dec 2023 16:44:50 -0300 Subject: [PATCH 46/87] [api][desktop]: Implement Sequencer window --- .../beans/converters/DeviceDeserializer.kt | 26 ++--- .../api/cameras/CameraDeserializer.kt | 4 +- .../api/cameras/CameraEventHandler.kt | 2 +- .../api/focusers/FocuserDeserializer.kt | 4 +- .../nebulosa/api/mounts/MountDeserializer.kt | 4 +- .../nebulosa/api/wheels/WheelDeserializer.kt | 4 +- .../nebulosa/api/wheels/WheelEventHandler.kt | 2 +- desktop/src/app/app-routing.module.ts | 5 + desktop/src/app/app.module.ts | 100 ++++++++++-------- desktop/src/app/home/home.component.html | 20 ++-- desktop/src/app/home/home.component.ts | 3 + .../app/sequencer/sequencer.component.html | 67 ++++++++++++ .../app/sequencer/sequencer.component.scss | 0 .../src/app/sequencer/sequencer.component.ts | 74 +++++++++++++ desktop/src/assets/icons/CREDITS.md | 2 +- desktop/src/assets/icons/schedule.png | Bin 3678 -> 0 bytes desktop/src/assets/icons/workflow.png | Bin 0 -> 2929 bytes .../shared/services/browser-window.service.ts | 8 ++ desktop/src/shared/types.ts | 31 +++++- 19 files changed, 264 insertions(+), 92 deletions(-) create mode 100644 desktop/src/app/sequencer/sequencer.component.html create mode 100644 desktop/src/app/sequencer/sequencer.component.scss create mode 100644 desktop/src/app/sequencer/sequencer.component.ts delete mode 100644 desktop/src/assets/icons/schedule.png create mode 100644 desktop/src/assets/icons/workflow.png diff --git a/api/src/main/kotlin/nebulosa/api/beans/converters/DeviceDeserializer.kt b/api/src/main/kotlin/nebulosa/api/beans/converters/DeviceDeserializer.kt index 49d28a92c..4822e062e 100644 --- a/api/src/main/kotlin/nebulosa/api/beans/converters/DeviceDeserializer.kt +++ b/api/src/main/kotlin/nebulosa/api/beans/converters/DeviceDeserializer.kt @@ -8,29 +8,17 @@ import com.fasterxml.jackson.databind.node.TextNode abstract class DeviceDeserializer(type: Class) : StdDeserializer(type) { - protected abstract val names: Iterable - - protected abstract fun device(name: String): T? + protected abstract fun deviceFor(name: String): T? final override fun deserialize(p: JsonParser, ctxt: DeserializationContext): T? { val node = p.codec.readTree(p) - if (node is TextNode) { - return device(node.asText()) + return if (node is TextNode) { + deviceFor(node.asText()) + } else if (node.has("name") && node.get("name") is TextNode) { + deviceFor(node.get("name").asText()) + } else { + null } - - for (name in names) { - if (node.has(name)) { - val deviceNode = node.get(name) - - if (deviceNode is TextNode) { - return device(deviceNode.asText()) ?: continue - } else if (deviceNode.has("name")) { - return device(deviceNode.get("name").asText()) ?: continue - } - } - } - - return null } } diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraDeserializer.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraDeserializer.kt index 965126535..c4fb8bc43 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraDeserializer.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraDeserializer.kt @@ -12,7 +12,5 @@ class CameraDeserializer : DeviceDeserializer(Camera::class.java) { @Autowired @Lazy private lateinit var connectionService: ConnectionService - override val names = listOf("camera", "device") - - override fun device(name: String) = connectionService.camera(name) + override fun deviceFor(name: String) = connectionService.camera(name) } diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraEventHandler.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraEventHandler.kt index df4dae057..407fc4ad8 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraEventHandler.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraEventHandler.kt @@ -29,7 +29,7 @@ class CameraEventHandler( } @Subscribe(threadMode = ThreadMode.ASYNC) - fun onFocuserEvent(event: CameraEvent) { + fun onCameraEvent(event: CameraEvent) { when (event) { is PropertyChangedEvent -> throttler.onNext(event) is CameraAttached -> sendMessage(CAMERA_ATTACHED, event.device) diff --git a/api/src/main/kotlin/nebulosa/api/focusers/FocuserDeserializer.kt b/api/src/main/kotlin/nebulosa/api/focusers/FocuserDeserializer.kt index d731689d1..356af1332 100644 --- a/api/src/main/kotlin/nebulosa/api/focusers/FocuserDeserializer.kt +++ b/api/src/main/kotlin/nebulosa/api/focusers/FocuserDeserializer.kt @@ -12,7 +12,5 @@ class FocuserDeserializer : DeviceDeserializer(Focuser::class.java) { @Autowired @Lazy private lateinit var connectionService: ConnectionService - override val names = listOf("focuser", "device") - - override fun device(name: String) = connectionService.focuser(name) + override fun deviceFor(name: String) = connectionService.focuser(name) } diff --git a/api/src/main/kotlin/nebulosa/api/mounts/MountDeserializer.kt b/api/src/main/kotlin/nebulosa/api/mounts/MountDeserializer.kt index 3f0cd4f75..b01ba42d4 100644 --- a/api/src/main/kotlin/nebulosa/api/mounts/MountDeserializer.kt +++ b/api/src/main/kotlin/nebulosa/api/mounts/MountDeserializer.kt @@ -12,7 +12,5 @@ class MountDeserializer : DeviceDeserializer(Mount::class.java) { @Autowired @Lazy private lateinit var connectionService: ConnectionService - override val names = listOf("mount", "device") - - override fun device(name: String) = connectionService.mount(name) + override fun deviceFor(name: String) = connectionService.mount(name) } diff --git a/api/src/main/kotlin/nebulosa/api/wheels/WheelDeserializer.kt b/api/src/main/kotlin/nebulosa/api/wheels/WheelDeserializer.kt index 11fb00fe5..7539757c4 100644 --- a/api/src/main/kotlin/nebulosa/api/wheels/WheelDeserializer.kt +++ b/api/src/main/kotlin/nebulosa/api/wheels/WheelDeserializer.kt @@ -12,7 +12,5 @@ class WheelDeserializer : DeviceDeserializer(FilterWheel::class.jav @Autowired @Lazy private lateinit var connectionService: ConnectionService - override val names = listOf("wheel", "device") - - override fun device(name: String) = connectionService.wheel(name) + override fun deviceFor(name: String) = connectionService.wheel(name) } diff --git a/api/src/main/kotlin/nebulosa/api/wheels/WheelEventHandler.kt b/api/src/main/kotlin/nebulosa/api/wheels/WheelEventHandler.kt index 4fec38e56..c92d926f0 100644 --- a/api/src/main/kotlin/nebulosa/api/wheels/WheelEventHandler.kt +++ b/api/src/main/kotlin/nebulosa/api/wheels/WheelEventHandler.kt @@ -29,7 +29,7 @@ class WheelEventHandler( } @Subscribe(threadMode = ThreadMode.ASYNC) - fun onFocuserEvent(event: FilterWheelEvent) { + fun onFilterWheelEvent(event: FilterWheelEvent) { when (event) { is PropertyChangedEvent -> throttler.onNext(event) is FilterWheelAttached -> sendMessage(WHEEL_ATTACHED, event.device) diff --git a/desktop/src/app/app-routing.module.ts b/desktop/src/app/app-routing.module.ts index 6dcf848d8..13239417a 100644 --- a/desktop/src/app/app-routing.module.ts +++ b/desktop/src/app/app-routing.module.ts @@ -14,6 +14,7 @@ import { HomeComponent } from './home/home.component' import { ImageComponent } from './image/image.component' import { INDIComponent } from './indi/indi.component' import { MountComponent } from './mount/mount.component' +import { SequencerComponent } from './sequencer/sequencer.component' import { SettingsComponent } from './settings/settings.component' const routes: Routes = [ @@ -66,6 +67,10 @@ const routes: Routes = [ path: 'alignment', component: AlignmentComponent, }, + { + path: 'sequencer', + component: SequencerComponent, + }, { path: 'calibration', component: CalibrationComponent, diff --git a/desktop/src/app/app.module.ts b/desktop/src/app/app.module.ts index 1bf5c14b8..9a96bc1c2 100644 --- a/desktop/src/app/app.module.ts +++ b/desktop/src/app/app.module.ts @@ -5,8 +5,10 @@ import { FormsModule } from '@angular/forms' import { BrowserModule } from '@angular/platform-browser' import { BrowserAnimationsModule } from '@angular/platform-browser/animations' import { ConfirmationService, MessageService } from 'primeng/api' +import { BadgeModule } from 'primeng/badge' import { ButtonModule } from 'primeng/button' import { CalendarModule } from 'primeng/calendar' +import { CardModule } from 'primeng/card' import { ChartModule } from 'primeng/chart' import { CheckboxModule } from 'primeng/checkbox' import { ConfirmDialogModule } from 'primeng/confirmdialog' @@ -62,86 +64,90 @@ import { ImageComponent } from './image/image.component' import { INDIComponent } from './indi/indi.component' import { INDIPropertyComponent } from './indi/property/indi-property.component' import { MountComponent } from './mount/mount.component' +import { SequencerComponent } from './sequencer/sequencer.component' import { SettingsComponent } from './settings/settings.component' @NgModule({ declarations: [ + AboutComponent, + AlignmentComponent, + AnglePipe, AppComponent, - HomeComponent, + AtlasComponent, + CalibrationComponent, CameraComponent, + DeviceMenuComponent, + DialogMenuComponent, + EnumPipe, + EnvPipe, + ExposureTimePipe, + FilterWheelComponent, + FocuserComponent, + FramingComponent, + GuiderComponent, + HomeComponent, ImageComponent, INDIComponent, INDIPropertyComponent, - AtlasComponent, - OpenStreetMapComponent, + LocationDialog, + MenuItemComponent, MoonComponent, - FramingComponent, - AboutComponent, - FocuserComponent, - FilterWheelComponent, MountComponent, - GuiderComponent, - DialogMenuComponent, - AlignmentComponent, + NoDropdownDirective, + OpenStreetMapComponent, + SequencerComponent, SettingsComponent, - LocationDialog, - MenuItemComponent, - DeviceMenuComponent, - CalibrationComponent, - EnvPipe, - WinPipe, - EnumPipe, - ExposureTimePipe, - AnglePipe, SkyObjectPipe, StopPropagationDirective, - NoDropdownDirective, + WinPipe, ], imports: [ - BrowserModule, + AppRoutingModule, + BadgeModule, BrowserAnimationsModule, + BrowserModule, + ButtonModule, + CalendarModule, + CardModule, + ChartModule, + CheckboxModule, + CommonModule, + ConfirmDialogModule, + ContextMenuModule, + DialogModule, + DropdownModule, + DynamicDialogModule, FormsModule, HttpClientModule, - CommonModule, - AppRoutingModule, - ButtonModule, + InplaceModule, InputNumberModule, - InputTextModule, - DropdownModule, InputSwitchModule, + InputTextModule, + ListboxModule, MenuModule, + OverlayPanelModule, + ScrollPanelModule, SelectButtonModule, + SlideMenuModule, + SliderModule, SplitButtonModule, - TabViewModule, - ChartModule, TableModule, + TabViewModule, TagModule, - DialogModule, - ListboxModule, - TooltipModule, - CalendarModule, - CheckboxModule, - ContextMenuModule, - SliderModule, - ToastModule, TieredMenuModule, - InplaceModule, - SlideMenuModule, - DynamicDialogModule, - ScrollPanelModule, - ConfirmDialogModule, - OverlayPanelModule, + ToastModule, + TooltipModule, ], providers: [ - MessageService, - DialogService, + AnglePipe, ConfirmationService, - EnvPipe, - WinPipe, + DialogService, EnumPipe, + EnvPipe, ExposureTimePipe, - AnglePipe, + MessageService, SkyObjectPipe, + WinPipe, { provide: LOCALE_ID, useValue: 'en-US', diff --git a/desktop/src/app/home/home.component.html b/desktop/src/app/home/home.component.html index 34efe5ad5..69222280f 100644 --- a/desktop/src/app/home/home.component.html +++ b/desktop/src/app/home/home.component.html @@ -15,8 +15,8 @@
- - + +
@@ -55,18 +55,18 @@
Focuser
-
- - -
Dome
-
-
Rotator
+
+ + +
Dome
+
+
@@ -87,8 +87,8 @@
- - + +
Sequencer
diff --git a/desktop/src/app/home/home.component.ts b/desktop/src/app/home/home.component.ts index 67f3d3aaa..40d3780f4 100644 --- a/desktop/src/app/home/home.component.ts +++ b/desktop/src/app/home/home.component.ts @@ -297,6 +297,9 @@ export class HomeComponent implements AfterContentInit, OnDestroy { case 'ALIGNMENT': this.browserWindow.openAlignment({ bringToFront: true }) break + case 'SEQUENCER': + this.browserWindow.openSequencer({ bringToFront: true }) + break case 'INDI': this.browserWindow.openINDI({ data: undefined, bringToFront: true }) break diff --git a/desktop/src/app/sequencer/sequencer.component.html b/desktop/src/app/sequencer/sequencer.component.html new file mode 100644 index 000000000..36b46f353 --- /dev/null +++ b/desktop/src/app/sequencer/sequencer.component.html @@ -0,0 +1,67 @@ +
+
+ + +
+
+
+ +
+
+
+ +
+
+
+
+
+
+ +
+ + +
+ + + + + + + + + + + + + + +
+
+
+
+
+ + + + + + + + +
+
+
+ +
+
+
+
+
+
+
\ No newline at end of file diff --git a/desktop/src/app/sequencer/sequencer.component.scss b/desktop/src/app/sequencer/sequencer.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/desktop/src/app/sequencer/sequencer.component.ts b/desktop/src/app/sequencer/sequencer.component.ts new file mode 100644 index 000000000..3031c4472 --- /dev/null +++ b/desktop/src/app/sequencer/sequencer.component.ts @@ -0,0 +1,74 @@ +import { AfterContentInit, Component, HostListener, NgZone, OnDestroy } from '@angular/core' +import { ActivatedRoute } from '@angular/router' +import { ApiService } from '../../shared/services/api.service' +import { BrowserWindowService } from '../../shared/services/browser-window.service' +import { ElectronService } from '../../shared/services/electron.service' +import { LocalStorageService } from '../../shared/services/local-storage.service' +import { Camera, CameraStartCapture, FilterWheel, Focuser } from '../../shared/types' +import { AppComponent } from '../app.component' + +@Component({ + selector: 'app-sequencer', + templateUrl: './sequencer.component.html', + styleUrls: ['./sequencer.component.scss'], +}) +export class SequencerComponent implements AfterContentInit, OnDestroy { + + cameras: Camera[] = [] + wheels: FilterWheel[] = [] + focusers: Focuser[] = [] + + slots: CameraStartCapture[] = [] + + sequenceInProgress = false + + constructor( + private app: AppComponent, + private api: ApiService, + private browserWindow: BrowserWindowService, + private electron: ElectronService, + private storage: LocalStorageService, + private route: ActivatedRoute, + ngZone: NgZone, + ) { + app.title = 'Sequencer' + } + + async ngAfterContentInit() { + this.cameras = await this.api.cameras() + this.wheels = await this.api.wheels() + this.focusers = await this.api.focusers() + + for (let i = 0; i < 32; i++) { + const camera = this.cameras[0] + const wheel = this.wheels[0] + const focuser = this.focusers[0] + + this.slots.push({ + enabled: false, + camera, + exposureTime: 1000000, + exposureAmount: 1, + exposureDelay: 0, + x: camera?.minX ?? 0, + y: camera?.minY ?? 0, + width: camera?.maxWidth ?? 0, + height: camera?.maxHeight ?? 0, + frameType: 'LIGHT', + binX: 1, + binY: 1, + gain: 0, + offset: 0, + autoSave: true, + autoSubFolderMode: 'OFF', + wheel, + focuser, + }) + } + + this.route.queryParams.subscribe(e => { }) + } + + @HostListener('window:unload') + ngOnDestroy() { } +} diff --git a/desktop/src/assets/icons/CREDITS.md b/desktop/src/assets/icons/CREDITS.md index 9640a1559..c9b465176 100644 --- a/desktop/src/assets/icons/CREDITS.md +++ b/desktop/src/assets/icons/CREDITS.md @@ -26,7 +26,7 @@ * https://www.flaticon.com/free-icon/stars_3266390 * https://www.flaticon.com/free-icon/milky-way_1086076 * https://www.flaticon.com/free-icon/satellite_1086093 -* https://www.flaticon.com/free-icon/schedule_3652191 * https://www.flaticon.com/free-icon/search_10770011 * https://www.flaticon.com/free-icon/stack_3342239 +* https://www.flaticon.com/free-icon/workflow_7400612 * https://thenounproject.com/icon/random-dither-4259782 diff --git a/desktop/src/assets/icons/schedule.png b/desktop/src/assets/icons/schedule.png deleted file mode 100644 index 5b879191a76ffc5b32b73b6086e7abf7100dd1c5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3678 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 wD{iTjPZuW)O;+NwYUh>EQZ`xYpKwIonLf~qAo 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, 'data'> = {}) { + options.icon ||= 'workflow' + options.width ||= 760 + options.height ||= 570 + options.resizable = true + this.openWindow({ ...options, id: 'sequencer', path: 'sequencer', data: undefined }) + } + openSettings(options: Omit, 'data'> = {}) { options.icon ||= 'settings' options.width ||= 580 diff --git a/desktop/src/shared/types.ts b/desktop/src/shared/types.ts index 3a8506e1e..6c679ceed 100644 --- a/desktop/src/shared/types.ts +++ b/desktop/src/shared/types.ts @@ -192,6 +192,8 @@ export interface Dither { } export interface CameraStartCapture { + enabled?: boolean + camera?: Camera exposureTime: number exposureAmount: number exposureDelay: number @@ -209,6 +211,11 @@ export interface CameraStartCapture { savePath?: string autoSubFolderMode: AutoSubFolderMode dither?: Dither + wheel?: FilterWheel + wheelPosition?: number + shutterPosition?: number + focuser?: Focuser + focusOffset?: number } export interface CameraCaptureEvent extends MessageEvent { @@ -377,7 +384,8 @@ export interface Twilight { civilDawn: number[] } -export type MinorPlanetKind = 'ASTEROID' | 'COMET' +export type MinorPlanetKind = 'ASTEROID' | + 'COMET' export interface MinorPlanet { found: boolean @@ -553,6 +561,27 @@ export interface SettleInfo { timeout: number } +export type SequenceCaptureMode = 'FULLY' | + 'INTERLEAVED' + +export interface AutoFocusAfterConditions { + onStart: boolean + onFilterChange: boolean + afterElapsedTime: number + afterExposures: number + afterTemperatureChange: number + afterHFDIncrease: number +} + +export interface SequencePlan { + initialDelay: number + captureMode: SequenceCaptureMode + savePath?: string + slots: CameraStartCapture[] + dither?: Dither + autoFocus?: AutoFocusAfterConditions +} + export enum ExposureTimeUnit { MINUTE = 'm', SECOND = 's', From 1f9fe33a962db3206d0c695a9fdd9712e3bb7b1d Mon Sep 17 00:00:00 2001 From: tiagohm Date: Sat, 16 Dec 2023 21:56:22 -0300 Subject: [PATCH 47/87] [desktop]: Improve layouts; Adjust Camera to use CameraStartCapture data --- desktop/src/app/about/about.component.html | 6 +- .../app/alignment/alignment.component.html | 4 +- desktop/src/app/camera/camera.component.html | 137 ++++----- desktop/src/app/camera/camera.component.ts | 268 +++++++----------- .../app/filterwheel/filterwheel.component.ts | 6 +- .../src/app/framing/framing.component.html | 4 +- desktop/src/app/guider/guider.component.html | 4 +- desktop/src/app/home/home.component.html | 2 +- desktop/src/app/home/home.component.scss | 2 + desktop/src/app/indi/indi.component.html | 5 +- .../property/indi-property.component.html | 78 +++-- .../src/app/settings/settings.component.html | 6 +- .../shared/services/browser-window.service.ts | 6 +- desktop/src/shared/types.ts | 54 ++++ desktop/src/styles.scss | 24 ++ 15 files changed, 309 insertions(+), 297 deletions(-) diff --git a/desktop/src/app/about/about.component.html b/desktop/src/app/about/about.component.html index 98b2c5860..fc4b667d4 100644 --- a/desktop/src/app/about/about.component.html +++ b/desktop/src/app/about/about.component.html @@ -1,12 +1,12 @@
-
+
-
+

Nebulosa v0.1.0

Ceres

© 2023 Tiago Melo 🇧🇷

-

This program comes with absolutely no warranty and is provided on an 'as is' basis.

+

This software is WIP, comes with absolutely no warranty and the copyright holder is not liable or responsible for anything.

diff --git a/desktop/src/app/alignment/alignment.component.html b/desktop/src/app/alignment/alignment.component.html index ae76dc6b2..7e9f30ed5 100644 --- a/desktop/src/app/alignment/alignment.component.html +++ b/desktop/src/app/alignment/alignment.component.html @@ -35,7 +35,7 @@
-
+
{{ darvStatus | enum | lowercase }} @@ -70,7 +70,7 @@
-
+
diff --git a/desktop/src/app/camera/camera.component.html b/desktop/src/app/camera/camera.component.html index 20a4dfa40..9bd107182 100644 --- a/desktop/src/app/camera/camera.component.html +++ b/desktop/src/app/camera/camera.component.html @@ -2,14 +2,14 @@
- +
- -
@@ -17,16 +17,16 @@ pTooltip="Image viewer" tooltipPosition="bottom" /> -
- - - - + + + - @@ -36,7 +36,9 @@
- {{ settling ? 'settling' : waiting ? 'waiting' : capturing ? 'capturing' : 'idle' }} + + {{ settling ? 'settling' : waiting ? 'waiting' : capturing ? 'capturing' : 'idle' }} + {{ exposure.count }} @@ -78,24 +80,24 @@
- Cooler ({{ coolerPower | number: '1.1-1' }}%) - + Cooler ({{ camera.coolerPower | number: '1.1-1' }}%) +
Dew heater - +
- - + + - +
@@ -103,19 +105,19 @@
- -
- @@ -123,16 +125,16 @@
-
+
Exposure Mode -
- @@ -140,9 +142,9 @@
- @@ -151,33 +153,33 @@
- +
- +
- +
- +
@@ -185,15 +187,15 @@
Subframe - +
-
- @@ -201,7 +203,7 @@
- @@ -211,34 +213,33 @@
- +
-
- +
- -
@@ -246,30 +247,30 @@ - +
-
+
Enabled - +
-
+
- +
-
+
Dither in RA only - +
- + [(ngModel)]="request.dither!.afterExposures" [step]="1" (ngModelChange)="savePreference()" />
diff --git a/desktop/src/app/camera/camera.component.ts b/desktop/src/app/camera/camera.component.ts index 864985e1f..650f4bfa8 100644 --- a/desktop/src/app/camera/camera.component.ts +++ b/desktop/src/app/camera/camera.component.ts @@ -5,31 +5,14 @@ import { ApiService } from '../../shared/services/api.service' import { BrowserWindowService } from '../../shared/services/browser-window.service' import { ElectronService } from '../../shared/services/electron.service' import { LocalStorageService } from '../../shared/services/local-storage.service' -import { AutoSubFolderMode, Camera, CameraCaptureState, CameraStartCapture, Dither, ExposureMode, ExposureTimeUnit, FilterWheel, FrameType } from '../../shared/types' +import { Camera, CameraCaptureState, CameraStartCapture, EMPTY_CAMERA, ExposureMode, ExposureTimeUnit, FilterWheel, FrameType } from '../../shared/types' import { AppComponent } from '../app.component' -export interface CameraPreference { - autoSave?: boolean - savePath?: string - autoSubFolderMode?: AutoSubFolderMode +export interface CameraPreference extends Partial { setpointTemperature?: number - exposureTime?: number exposureTimeUnit?: ExposureTimeUnit exposureMode?: ExposureMode - exposureDelay?: number - exposureCount?: number - x?: number - y?: number - width?: number - height?: number subFrame?: boolean - binX?: number - binY?: number - frameType?: FrameType - gain?: number - offset?: number - frameFormat?: string - dithering?: Dither } @Component({ @@ -39,30 +22,21 @@ export interface CameraPreference { }) export class CameraComponent implements AfterContentInit, OnDestroy { - camera?: Camera - connected = false + readonly camera = Object.assign({}, EMPTY_CAMERA) - autoSave = false savePath = '' capturesPath = '' - autoSubFolderMode: AutoSubFolderMode = 'OFF' wheel?: FilterWheel - showDitheringDialog = false - readonly dithering: Dither = { - enabled: false, - afterExposures: 1, - amount: 1.5, - raOnly: false, - } + showDitherDialog = false readonly cameraModel: MenuItem[] = [ { icon: 'mdi mdi-content-save', label: 'Auto save all exposures', command: () => { - this.autoSave = !this.autoSave + this.request.autoSave = !this.request.autoSave this.savePreference() }, }, @@ -87,7 +61,7 @@ export class CameraComponent implements AfterContentInit, OnDestroy { icon: 'mdi mdi-folder-off', label: 'None', command: () => { - this.autoSubFolderMode = 'OFF' + this.request.autoSubFolderMode = 'OFF' this.savePreference() }, }, @@ -95,7 +69,7 @@ export class CameraComponent implements AfterContentInit, OnDestroy { icon: 'mdi mdi-weather-sunny', label: 'Noon', command: () => { - this.autoSubFolderMode = 'NOON' + this.request.autoSubFolderMode = 'NOON' this.savePreference() }, }, @@ -103,7 +77,7 @@ export class CameraComponent implements AfterContentInit, OnDestroy { icon: 'mdi mdi-weather-night', label: 'Midnight', command: () => { - this.autoSubFolderMode = 'MIDNIGHT' + this.request.autoSubFolderMode = 'MIDNIGHT' this.savePreference() }, }, @@ -114,52 +88,43 @@ export class CameraComponent implements AfterContentInit, OnDestroy { }, { icon: 'icomoon random-dither', - label: 'Dithering', + label: 'Dither', command: () => { - this.showDitheringDialog = true + this.showDitherDialog = true }, }, ] - cooler = false - hasCooler = false - coolerPower = 0.0 - dewHeater = false hasDewHeater = false - temperature = 0.0 - canSetTemperature = false setpointTemperature = 0.0 - exposureTime = 1 exposureTimeMin = 1 exposureTimeMax = 1 exposureTimeUnit = ExposureTimeUnit.MICROSECOND exposureMode: ExposureMode = 'SINGLE' - exposureDelay = 0 - exposureCount = 1 - x = 0 - minX = 0 - maxX = 0 - y = 0 - minY = 0 - maxY = 0 - width = 1023 - minWidth = 1023 - maxWidth = 1023 - height = 1280 - minHeight = 1280 - maxHeight = 1280 subFrame = false - binX = 1 - binY = 1 - frameType: FrameType = 'LIGHT' - frameFormats: string[] = [] - frameFormat = '' - gain = 0 - gainMin = 0 - gainMax = 0 - offset = 0 - offsetMin = 0 - offsetMax = 0 + + readonly request: CameraStartCapture = { + exposureTime: 1, + exposureAmount: 1, + exposureDelay: 0, + x: 0, + y: 0, + width: 0, + height: 0, + frameType: 'LIGHT', + binX: 1, + binY: 1, + gain: 0, + offset: 0, + autoSave: false, + autoSubFolderMode: 'OFF', + dither: { + enabled: false, + afterExposures: 1, + amount: 1.5, + raOnly: false, + } + } state?: CameraCaptureState @@ -251,7 +216,7 @@ export class CameraComponent implements AfterContentInit, OnDestroy { electron.on('CAMERA_DETACHED', event => { if (event.device.name === this.camera?.name) { ngZone.run(() => { - this.connected = false + Object.assign(this.camera!, event.device) }) } }) @@ -299,12 +264,10 @@ export class CameraComponent implements AfterContentInit, OnDestroy { } async cameraChanged(camera?: Camera) { - this.camera = camera - - if (this.camera) { - this.app.subTitle = this.camera.name + if (camera) { + this.app.subTitle = camera.name - const camera = await this.api.camera(this.camera.name) + camera = await this.api.camera(camera.name) Object.assign(this.camera, camera) this.loadPreference() @@ -315,7 +278,7 @@ export class CameraComponent implements AfterContentInit, OnDestroy { } connect() { - if (this.connected) { + if (this.camera!.connected) { this.api.cameraDisconnect(this.camera!) } else { this.api.cameraConnect(this.camera!) @@ -328,15 +291,15 @@ export class CameraComponent implements AfterContentInit, OnDestroy { } toggleCooler() { - this.api.cameraCooler(this.camera!, this.cooler) + this.api.cameraCooler(this.camera!, this.camera!.cooler) } fullsize() { if (this.camera) { - this.x = this.camera.minX - this.y = this.camera.minY - this.width = this.camera.maxWidth - this.height = this.camera.maxHeight + this.request.x = this.camera.minX + this.request.y = this.camera.minY + this.request.width = this.camera.maxWidth + this.request.height = this.camera.maxHeight this.savePreference() } } @@ -349,34 +312,26 @@ export class CameraComponent implements AfterContentInit, OnDestroy { return this.browserWindow.openCalibration({ data: this.camera! }) } - async startCapture() { - const x = this.subFrame ? this.x : this.camera!.minX - const y = this.subFrame ? this.y : this.camera!.minY - const width = this.subFrame ? this.width : this.camera!.maxWidth - const height = this.subFrame ? this.height : this.camera!.maxHeight + private makeCameraStartCapture(): CameraStartCapture { + const x = this.subFrame ? this.request.x : this.camera!.minX + const y = this.subFrame ? this.request.y : this.camera!.minY + const width = this.subFrame ? this.request.width : this.camera!.maxWidth + const height = this.subFrame ? this.request.height : this.camera!.maxHeight const exposureFactor = CameraComponent.exposureUnitFactor(this.exposureTimeUnit) - const exposureTime = Math.trunc(this.exposureTime * 60000000 / exposureFactor) - const exposureAmount = this.exposureMode === 'LOOP' ? 0 : (this.exposureMode === 'FIXED' ? this.exposureCount : 1) + const exposureTime = Math.trunc(this.request.exposureTime * 60000000 / exposureFactor) + const exposureAmount = this.exposureMode === 'LOOP' ? 0 : (this.exposureMode === 'FIXED' ? this.request.exposureAmount : 1) - const data: CameraStartCapture = { - exposureTime, exposureAmount, - exposureDelay: this.exposureDelay * 1000000, + return { + ...this.request, + exposureDelay: this.request.exposureDelay * 1000000, x, y, width, height, - frameFormat: this.frameFormat, - frameType: this.frameType, - binX: this.binX, - binY: this.binY, - gain: this.gain, - offset: this.offset, - autoSave: this.autoSave, - savePath: this.savePath, - autoSubFolderMode: this.autoSubFolderMode, - dither: this.dithering, + exposureTime, exposureAmount, } + } + async startCapture() { await this.openCameraImage() - - this.api.cameraStartCapture(this.camera!, data) + this.api.cameraStartCapture(this.camera!, this.makeCameraStartCapture()) } abortCapture() { @@ -396,45 +351,24 @@ export class CameraComponent implements AfterContentInit, OnDestroy { if (this.camera!.exposureMax) { const a = CameraComponent.exposureUnitFactor(this.exposureTimeUnit) const b = CameraComponent.exposureUnitFactor(unit) - const exposureTime = Math.trunc(this.exposureTime * b / a) + const exposureTime = Math.trunc(this.request.exposureTime * b / a) const exposureTimeMin = Math.trunc(this.camera!.exposureMin * b / 60000000) const exposureTimeMax = Math.trunc(this.camera!.exposureMax * b / 60000000) this.exposureTimeMax = Math.max(1, exposureTimeMax) this.exposureTimeMin = Math.max(1, exposureTimeMin) - this.exposureTime = Math.max(this.exposureTimeMin, Math.min(exposureTime, this.exposureTimeMax)) + this.request.exposureTime = Math.max(this.exposureTimeMin, Math.min(exposureTime, this.exposureTimeMax)) this.exposureTimeUnit = unit } } private update() { if (this.camera) { - this.connected = this.camera.connected - - if (this.connected) { - this.cooler = this.camera.cooler - this.hasCooler = this.camera.hasCooler - this.coolerPower = this.camera.coolerPower - this.dewHeater = this.camera.dewHeater - this.temperature = this.camera.temperature - this.canSetTemperature = this.camera.canSetTemperature - this.minX = this.camera.minX - this.maxX = this.camera.maxX - this.x = Math.max(this.minX, Math.min(this.x, this.maxX)) - this.minY = this.camera.minY - this.maxY = this.camera.maxY - this.y = Math.max(this.minY, Math.min(this.y, this.maxY)) - this.minWidth = this.camera.minWidth - this.maxWidth = this.camera.maxWidth - this.width = Math.max(this.minWidth, Math.min(this.width < 8 ? this.maxWidth : this.width, this.maxWidth)) - this.minHeight = this.camera.minHeight - this.maxHeight = this.camera.maxHeight - this.height = Math.max(this.minHeight, Math.min(this.height < 8 ? this.maxHeight : this.width, this.maxHeight)) - this.frameFormats = this.camera.frameFormats - if (!this.frameFormat) this.frameFormat = this.frameFormats[0] - this.gainMin = this.camera.gainMin - this.gainMax = this.camera.gainMax - this.offsetMin = this.camera.offsetMin - this.offsetMax = this.camera.offsetMax + if (this.camera.connected) { + this.request.x = Math.max(this.camera.minX, Math.min(this.request.x, this.camera.maxX)) + this.request.y = Math.max(this.camera.minY, Math.min(this.request.y, this.camera.maxY)) + this.request.width = Math.max(this.camera.minWidth, Math.min(this.request.width < 8 ? this.camera.maxWidth : this.request.width, this.camera.maxWidth)) + this.request.height = Math.max(this.camera.minHeight, Math.min(this.request.height < 8 ? this.camera.maxHeight : this.request.width, this.camera.maxHeight)) + if (!this.request.frameFormat) this.request.frameFormat = this.camera.frameFormats[0] this.updateExposureUnit(this.exposureTimeUnit) } @@ -451,58 +385,58 @@ export class CameraComponent implements AfterContentInit, OnDestroy { private loadPreference() { if (this.camera) { const preference = this.storage.get(`camera.${this.camera.name}`, {}) - this.autoSave = preference.autoSave ?? false + this.request.autoSave = preference.autoSave ?? false this.savePath = preference.savePath ?? '' - this.autoSubFolderMode = preference.autoSubFolderMode ?? 'OFF' + this.request.autoSubFolderMode = preference.autoSubFolderMode ?? 'OFF' this.setpointTemperature = preference.setpointTemperature ?? 0 - this.exposureTime = preference.exposureTime ?? this.camera.exposureMin + this.request.exposureTime = preference.exposureTime ?? this.camera.exposureMin this.exposureTimeUnit = preference.exposureTimeUnit ?? ExposureTimeUnit.MICROSECOND this.exposureMode = preference.exposureMode ?? 'SINGLE' - this.exposureDelay = preference.exposureDelay ?? 0 - this.exposureCount = preference.exposureCount ?? 1 - this.x = preference.x ?? this.camera.minX - this.y = preference.y ?? this.camera.minY - this.width = preference.width ?? this.camera.maxWidth - this.height = preference.height ?? this.camera.maxHeight + this.request.exposureDelay = preference.exposureDelay ?? 0 + this.request.exposureAmount = preference.exposureAmount ?? 1 + this.request.x = preference.x ?? this.camera.minX + this.request.y = preference.y ?? this.camera.minY + this.request.width = preference.width ?? this.camera.maxWidth + this.request.height = preference.height ?? this.camera.maxHeight this.subFrame = preference.subFrame ?? false - this.binX = preference.binX ?? 1 - this.binY = preference.binY ?? 1 - this.frameType = preference.frameType ?? 'LIGHT' - this.gain = preference.gain ?? 0 - this.offset = preference.offset ?? 0 - this.frameFormat = preference.frameFormat ?? (this.camera.frameFormats[0] || '') - - this.dithering.enabled = preference.dithering?.enabled ?? false - this.dithering.raOnly = preference.dithering?.raOnly ?? false - this.dithering.amount = preference.dithering?.amount ?? 1.5 - this.dithering.afterExposures = preference.dithering?.afterExposures ?? 1 + this.request.binX = preference.binX ?? 1 + this.request.binY = preference.binY ?? 1 + this.request.frameType = preference.frameType ?? 'LIGHT' + this.request.gain = preference.gain ?? 0 + this.request.offset = preference.offset ?? 0 + this.request.frameFormat = preference.frameFormat ?? (this.camera.frameFormats[0] || '') + + this.request.dither!.enabled = preference.dither?.enabled ?? false + this.request.dither!.raOnly = preference.dither?.raOnly ?? false + this.request.dither!.amount = preference.dither?.amount ?? 1.5 + this.request.dither!.afterExposures = preference.dither?.afterExposures ?? 1 } } savePreference() { if (this.camera && this.camera.connected) { const preference: CameraPreference = { - autoSave: this.autoSave, + autoSave: this.request.autoSave, savePath: this.savePath, - autoSubFolderMode: this.autoSubFolderMode, + autoSubFolderMode: this.request.autoSubFolderMode, setpointTemperature: this.setpointTemperature, - exposureTime: this.exposureTime, + exposureTime: this.request.exposureTime, exposureTimeUnit: this.exposureTimeUnit, exposureMode: this.exposureMode, - exposureDelay: this.exposureDelay, - exposureCount: this.exposureCount, - x: this.x, - y: this.y, - width: this.width, - height: this.height, + exposureDelay: this.request.exposureDelay, + exposureAmount: this.request.exposureAmount, + x: this.request.x, + y: this.request.y, + width: this.request.width, + height: this.request.height, subFrame: this.subFrame, - binX: this.binX, - binY: this.binY, - frameType: this.frameType, - gain: this.gain, - offset: this.offset, - frameFormat: this.frameFormat, - dithering: this.dithering, + binX: this.request.binX, + binY: this.request.binY, + frameType: this.request.frameType, + gain: this.request.gain, + offset: this.request.offset, + frameFormat: this.request.frameFormat, + dither: this.request.dither, } this.storage.set(`camera.${this.camera.name}`, preference) diff --git a/desktop/src/app/filterwheel/filterwheel.component.ts b/desktop/src/app/filterwheel/filterwheel.component.ts index c49045abd..d386550b2 100644 --- a/desktop/src/app/filterwheel/filterwheel.component.ts +++ b/desktop/src/app/filterwheel/filterwheel.component.ts @@ -122,17 +122,17 @@ export class FilterWheelComponent implements AfterContentInit, OnDestroy { shutterToggled(filter: Filter, event: InputSwitchChangeEvent) { this.filters.forEach(e => e.dark = event.checked && e === filter) - this.filterChangedPublisher.next(filter) + this.filterChangedPublisher.next(Object.assign({}, filter)) } filterNameChanged(filter: Filter) { if (filter.name) { - this.filterChangedPublisher.next(filter) + this.filterChangedPublisher.next(Object.assign({}, filter)) } } focusOffsetChanged(filter: Filter) { - this.filterChangedPublisher.next(filter) + this.filterChangedPublisher.next(Object.assign({}, filter)) } private update() { diff --git a/desktop/src/app/framing/framing.component.html b/desktop/src/app/framing/framing.component.html index c3dc665b3..7d086d316 100644 --- a/desktop/src/app/framing/framing.component.html +++ b/desktop/src/app/framing/framing.component.html @@ -58,10 +58,10 @@
-
+
-
+
Made use of hips2fits, a service provided by CDS. diff --git a/desktop/src/app/guider/guider.component.html b/desktop/src/app/guider/guider.component.html index a5767177b..5dc034e14 100644 --- a/desktop/src/app/guider/guider.component.html +++ b/desktop/src/app/guider/guider.component.html @@ -125,8 +125,8 @@
-
-
+
+
Port
-
+
diff --git a/desktop/src/app/home/home.component.scss b/desktop/src/app/home/home.component.scss index 3952ec4ba..ab73ace58 100644 --- a/desktop/src/app/home/home.component.scss +++ b/desktop/src/app/home/home.component.scss @@ -1,4 +1,6 @@ p-button ::ng-deep { + display: contents; + &.open { min-height: 56px; max-height: 56px; diff --git a/desktop/src/app/indi/indi.component.html b/desktop/src/app/indi/indi.component.html index 0bfb1d811..9a0c7baab 100644 --- a/desktop/src/app/indi/indi.component.html +++ b/desktop/src/app/indi/indi.component.html @@ -8,12 +8,13 @@
- +
-
+
diff --git a/desktop/src/app/indi/property/indi-property.component.html b/desktop/src/app/indi/property/indi-property.component.html index d179d22e3..6016f8293 100644 --- a/desktop/src/app/indi/property/indi-property.component.html +++ b/desktop/src/app/indi/property/indi-property.component.html @@ -20,53 +20,49 @@
-
-
-
-
-
- - - - -
-
- - - - -
+
+
+
+
+ + + + +
+
+ + + +
-
- -
+
+
+
-
-
-
-
-
- - - - -
-
- - - - -
+
+
+
+
+ + + + +
+
+ + + +
-
- -
+
+
+
diff --git a/desktop/src/app/settings/settings.component.html b/desktop/src/app/settings/settings.component.html index 16cf282ba..dcb9f1551 100644 --- a/desktop/src/app/settings/settings.component.html +++ b/desktop/src/app/settings/settings.component.html @@ -19,7 +19,7 @@
-
+
-
+
-
+
diff --git a/desktop/src/shared/services/browser-window.service.ts b/desktop/src/shared/services/browser-window.service.ts index ab449d57b..5c5f259b6 100644 --- a/desktop/src/shared/services/browser-window.service.ts +++ b/desktop/src/shared/services/browser-window.service.ts @@ -45,7 +45,7 @@ export class BrowserWindowService { openGuider(options: Omit, 'data'> = {}) { options.icon ||= 'guider' options.width ||= 425 - options.height ||= 440 + options.height ||= 450 this.openWindow({ ...options, id: 'guider', path: 'guider', data: undefined }) } @@ -87,7 +87,7 @@ export class BrowserWindowService { openAlignment(options: Omit, 'data'> = {}) { options.icon ||= 'star' - options.width ||= 470 + options.width ||= 400 options.height ||= 280 this.openWindow({ ...options, id: 'alignment', path: 'alignment', data: undefined }) } @@ -115,6 +115,6 @@ export class BrowserWindowService { } openAbout() { - this.openWindow({ id: 'about', path: 'about', icon: 'about', width: 340, height: 243, bringToFront: true, data: undefined }) + this.openWindow({ id: 'about', path: 'about', icon: 'about', width: 480, height: 252, bringToFront: true, data: undefined }) } } diff --git a/desktop/src/shared/types.ts b/desktop/src/shared/types.ts index 6c679ceed..99ef672b5 100644 --- a/desktop/src/shared/types.ts +++ b/desktop/src/shared/types.ts @@ -113,6 +113,60 @@ export interface Camera extends GuideOutput, Thermometer { capturesPath: string } +export const EMPTY_CAMERA: Camera = { + exposuring: false, + hasCoolerControl: false, + coolerPower: 0, + cooler: false, + hasDewHeater: false, + dewHeater: false, + frameFormats: [], + canAbort: false, + cfaOffsetX: 0, + cfaOffsetY: 0, + cfaType: 'RGGB', + exposureMin: 0, + exposureMax: 1, + exposureState: 'IDLE', + exposureTime: 1, + hasCooler: false, + canSetTemperature: false, + canSubFrame: false, + x: 0, + minX: 0, + maxX: 0, + y: 0, + minY: 0, + maxY: 0, + width: 1023, + minWidth: 1023, + maxWidth: 1023, + height: 1280, + minHeight: 1280, + maxHeight: 1280, + canBin: false, + maxBinX: 1, + maxBinY: 1, + binX: 1, + binY: 1, + gain: 0, + gainMin: 0, + gainMax: 0, + offset: 0, + offsetMin: 0, + offsetMax: 0, + hasGuiderHead: false, + pixelSizeX: 0, + pixelSizeY: 0, + capturesPath: '', + canPulseGuide: false, + pulseGuiding: false, + name: '', + connected: false, + hasThermometer: false, + temperature: 0 +} + export interface Parkable { canPark: boolean parking: boolean diff --git a/desktop/src/styles.scss b/desktop/src/styles.scss index bdb921b29..b7064aaa0 100644 --- a/desktop/src/styles.scss +++ b/desktop/src/styles.scss @@ -82,6 +82,30 @@ p-dropdown, color: rgba(200, 200, 200, 0.87); } +.p-checkbox { + margin-left: auto; + margin-right: auto; +} + +.grid { + min-width: 100%; +} + +.grid>.col, +.grid>[class*=col] { + display: inline-flex; + + >p-selectbutton, + >p-splitbutton, + >p-table, + >p-tabview, + >p-listbox, + >p-chart, + >.p-float-label { + width: 100%; + } +} + .border-0, .p-selectbutton.border-0 .p-button, .p-inputnumber.border-0 .p-inputtext { From e1acb32b162f11b35c44b1b8674bff55860cd322 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Sat, 16 Dec 2023 22:02:16 -0300 Subject: [PATCH 48/87] [api][desktop]: Fix sync filter names --- api/src/main/kotlin/nebulosa/api/wheels/WheelController.kt | 3 ++- desktop/src/app/filterwheel/filterwheel.component.ts | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/api/src/main/kotlin/nebulosa/api/wheels/WheelController.kt b/api/src/main/kotlin/nebulosa/api/wheels/WheelController.kt index cb1836102..04f20422f 100644 --- a/api/src/main/kotlin/nebulosa/api/wheels/WheelController.kt +++ b/api/src/main/kotlin/nebulosa/api/wheels/WheelController.kt @@ -1,6 +1,7 @@ package nebulosa.api.wheels import jakarta.validation.Valid +import jakarta.validation.constraints.NotEmpty import jakarta.validation.constraints.PositiveOrZero import nebulosa.api.beans.annotations.EntityParam import nebulosa.api.connection.ConnectionService @@ -45,7 +46,7 @@ class WheelController( @PutMapping("{wheel}/sync") fun sync( @EntityParam wheel: FilterWheel, - @RequestParam @Valid @PositiveOrZero names: String, + @RequestParam @Valid @NotEmpty names: String, ) { wheelService.sync(wheel, names.split(",")) } diff --git a/desktop/src/app/filterwheel/filterwheel.component.ts b/desktop/src/app/filterwheel/filterwheel.component.ts index d386550b2..143025a01 100644 --- a/desktop/src/app/filterwheel/filterwheel.component.ts +++ b/desktop/src/app/filterwheel/filterwheel.component.ts @@ -1,7 +1,7 @@ import { AfterContentInit, Component, HostListener, NgZone, OnDestroy } from '@angular/core' import { ActivatedRoute } from '@angular/router' import { InputSwitchChangeEvent } from 'primeng/inputswitch' -import { Subject, Subscription, throttleTime } from 'rxjs' +import { Subject, Subscription, debounceTime } from 'rxjs' import { ApiService } from '../../shared/services/api.service' import { ElectronService } from '../../shared/services/electron.service' import { LocalStorageService } from '../../shared/services/local-storage.service' @@ -73,7 +73,7 @@ export class FilterWheelComponent implements AfterContentInit, OnDestroy { }) this.subscription = this.filterChangedPublisher - .pipe(throttleTime(1500)) + .pipe(debounceTime(1500)) .subscribe((filter) => { this.savePreference() this.electron.send('WHEEL_RENAMED', { wheel: this.wheel!, filter }) From 415ea4431f7ee9b139933251f08c5e8221d5d2d1 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Sat, 16 Dec 2023 22:20:40 -0300 Subject: [PATCH 49/87] [desktop]: Improve Sky Atlas layout --- desktop/src/app/atlas/atlas.component.html | 34 ++++++++++++---------- desktop/src/app/atlas/atlas.component.scss | 8 ----- desktop/src/styles.scss | 5 ++++ 3 files changed, 24 insertions(+), 23 deletions(-) diff --git a/desktop/src/app/atlas/atlas.component.html b/desktop/src/app/atlas/atlas.component.html index 878590527..2fd4cb004 100644 --- a/desktop/src/app/atlas/atlas.component.html +++ b/desktop/src/app/atlas/atlas.component.html @@ -329,13 +329,13 @@
-
+
-
+
-
- - -
- - -
-
-
+
+
+ +
+ Time +
+ + +
+
+
diff --git a/desktop/src/app/atlas/atlas.component.scss b/desktop/src/app/atlas/atlas.component.scss index b3054b2a5..109869538 100644 --- a/desktop/src/app/atlas/atlas.component.scss +++ b/desktop/src/app/atlas/atlas.component.scss @@ -65,11 +65,3 @@ .p-input-icon-right p-checkbox { margin-top: -0.745rem; } - -::ng-deep { - - .p-overlaypanel:after, - .p-overlaypanel:before { - left: calc(var(--overlayArrowLeft, 0) + 2.1rem) !important; - } -} \ No newline at end of file diff --git a/desktop/src/styles.scss b/desktop/src/styles.scss index b7064aaa0..825de429e 100644 --- a/desktop/src/styles.scss +++ b/desktop/src/styles.scss @@ -175,6 +175,11 @@ i.mdi { border: 0; } +.p-overlaypanel:before, +.p-overlaypanel:after { + display: none; +} + .draggable-region { -webkit-app-region: drag; } From 061aa2bf265c7f9c5bfead9f7252404e388a0178 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Sat, 16 Dec 2023 23:27:40 -0300 Subject: [PATCH 50/87] [desktop]: Add GitHub button on About; Improve About layout --- desktop/src/app/about/about.component.html | 21 +++++++++++++++------ desktop/src/assets/icons/brazil.png | Bin 0 -> 261 bytes 2 files changed, 15 insertions(+), 6 deletions(-) create mode 100644 desktop/src/assets/icons/brazil.png diff --git a/desktop/src/app/about/about.component.html b/desktop/src/app/about/about.component.html index fc4b667d4..003247b46 100644 --- a/desktop/src/app/about/about.component.html +++ b/desktop/src/app/about/about.component.html @@ -3,12 +3,21 @@
-

Nebulosa v0.1.0

-

Ceres

-

© 2023 Tiago Melo 🇧🇷

+

+ Nebulosa + + +

+

© 2022-2024 Tiago Melo All rights reserved

This software is WIP, comes with absolutely no warranty and the copyright holder is not liable or responsible for anything.

- - - + +
\ No newline at end of file diff --git a/desktop/src/assets/icons/brazil.png b/desktop/src/assets/icons/brazil.png new file mode 100644 index 0000000000000000000000000000000000000000..e6ec11e47b4b33b9a1e7c4c6ee990881bf9d5584 GIT binary patch literal 261 zcmV+g0s8)lP)!rmBwfPgz%qnW2xY=gR;tkM7f|IXFukDZ1TrFXW-E~4j1Xwc+y@l^X>n^yYfI~fyTwUKHLa~J zqeV#t+1iY>B82N1m0Z)((v<{|o$g+u Date: Sun, 17 Dec 2023 21:21:42 -0300 Subject: [PATCH 51/87] [desktop]: Add "Buy me a coffee" button --- desktop/src/app/about/about.component.html | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/desktop/src/app/about/about.component.html b/desktop/src/app/about/about.component.html index 003247b46..162bee3b6 100644 --- a/desktop/src/app/about/about.component.html +++ b/desktop/src/app/about/about.component.html @@ -1,22 +1,25 @@
-
+

Nebulosa - - + +

© 2022-2024 Tiago Melo All rights reserved

This software is WIP, comes with absolutely no warranty and the copyright holder is not liable or responsible for anything.

- From b508a1e24b35289cc7aeb840d0a05f5aaa678974 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Mon, 18 Dec 2023 12:50:34 -0300 Subject: [PATCH 52/87] [desktop]: Fix overlays being removed always image is loaded --- desktop/src/app/image/image.component.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/desktop/src/app/image/image.component.ts b/desktop/src/app/image/image.component.ts index db7c88d8a..d66ff8421 100644 --- a/desktop/src/app/image/image.component.ts +++ b/desktop/src/app/image/image.component.ts @@ -363,6 +363,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { ngZone.run(() => { this.imageData.path = event.savePath + this.clearOverlay() this.loadImage() }) } @@ -462,6 +463,8 @@ export class ImageComponent implements AfterViewInit, OnDestroy { this.disableCalibrate() } + this.clearOverlay() + this.loadImage() } @@ -477,8 +480,6 @@ export class ImageComponent implements AfterViewInit, OnDestroy { private async loadImage() { if (this.imageData.path) { - this.clearOverlay() - await this.loadImageFromPath(this.imageData.path) } From eb4e606f713c8cfe97a335e84603ba2e8b263b59 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Mon, 18 Dec 2023 13:06:30 -0300 Subject: [PATCH 53/87] [desktop]: Fix Image layout --- desktop/app/main.ts | 4 ++-- desktop/src/app/image/image.component.html | 16 +++++++++------- desktop/src/styles.scss | 1 + 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/desktop/app/main.ts b/desktop/app/main.ts index 42c727e7d..c6a89978c 100644 --- a/desktop/app/main.ts +++ b/desktop/app/main.ts @@ -49,8 +49,8 @@ function createMainWindow() { onWebSocketClose: () => { console.warn('Web Socket closed') }, - onWebSocketError: (e) => { - console.error('Web Socket error', e) + onWebSocketError: () => { + console.error('Web Socket error') }, }) diff --git a/desktop/src/app/image/image.component.html b/desktop/src/app/image/image.component.html index adb915555..d15a1e129 100644 --- a/desktop/src/app/image/image.component.html +++ b/desktop/src/app/image/image.component.html @@ -318,13 +318,15 @@
-
-
X: {{ mouseCoordinate.x }}
-
Y: {{ mouseCoordinate.y }}
-
α: {{ mouseCoordinate.alpha }}
-
δ: {{ mouseCoordinate.delta }}
-
l: {{ mouseCoordinate.l }}
-
b: {{ mouseCoordinate.b }}
+
+
+
X: {{ mouseCoordinate.x }}
+
Y: {{ mouseCoordinate.y }}
+
α: {{ mouseCoordinate.alpha }}
+
δ: {{ mouseCoordinate.delta }}
+
l: {{ mouseCoordinate.l }}
+
b: {{ mouseCoordinate.b }}
+
\ No newline at end of file diff --git a/desktop/src/styles.scss b/desktop/src/styles.scss index 825de429e..c6aaf255d 100644 --- a/desktop/src/styles.scss +++ b/desktop/src/styles.scss @@ -85,6 +85,7 @@ p-dropdown, .p-checkbox { margin-left: auto; margin-right: auto; + display: contents !important; } .grid { From 701acb64fcb84f645d80662e0f8877de84967ce7 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Mon, 18 Dec 2023 13:32:07 -0300 Subject: [PATCH 54/87] [api]: Fix rightAscension and declination extension method --- .../src/main/kotlin/nebulosa/fits/FitsHelper.kt | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/nebulosa-fits/src/main/kotlin/nebulosa/fits/FitsHelper.kt b/nebulosa-fits/src/main/kotlin/nebulosa/fits/FitsHelper.kt index d212bb858..aed5ef895 100644 --- a/nebulosa-fits/src/main/kotlin/nebulosa/fits/FitsHelper.kt +++ b/nebulosa-fits/src/main/kotlin/nebulosa/fits/FitsHelper.kt @@ -21,17 +21,13 @@ inline val Header.height get() = naxis(2) val Header.rightAscension - get() = Angle(getStringOrNull(Standard.RA), isHours = true, decimalIsHours = false) - .takeIf { it.isFinite() } - ?.let { Angle(getStringOrNull(SBFitsExt.OBJCTRA), true) } - ?.takeIf { it.isFinite() } + get() = Angle(getStringOrNull(Standard.RA), isHours = true, decimalIsHours = false).takeIf { it.isFinite() } + ?: Angle(getStringOrNull(SBFitsExt.OBJCTRA), true).takeIf { it.isFinite() } ?: getDouble(NOAOExt.CRVAL1, Double.NaN).deg val Header.declination - get() = Angle(getStringOrNull(Standard.DEC)) - .takeIf { it.isFinite() } - ?.let { Angle(getStringOrNull(SBFitsExt.OBJCTDEC)) } - ?.takeIf { it.isFinite() } + get() = Angle(getStringOrNull(Standard.DEC)).takeIf { it.isFinite() } + ?: Angle(getStringOrNull(SBFitsExt.OBJCTDEC)).takeIf { it.isFinite() } ?: getDouble(NOAOExt.CRVAL2, Double.NaN).deg inline val Header.binX From 195899accdd141439b967b292c08ecaee5bbc905 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Tue, 19 Dec 2023 19:10:48 -0300 Subject: [PATCH 55/87] [desktop]: Implement Sequencer --- desktop/app/main.ts | 53 +++++- desktop/src/app/app.component.html | 4 +- desktop/src/app/app.component.ts | 2 +- desktop/src/app/app.module.ts | 2 + desktop/src/app/atlas/atlas.component.ts | 4 +- .../app/calibration/calibration.component.ts | 4 +- desktop/src/app/camera/camera.component.html | 19 +- desktop/src/app/camera/camera.component.ts | 43 +++-- desktop/src/app/image/image.component.ts | 2 +- .../app/sequencer/sequencer.component.html | 108 +++++++---- .../app/sequencer/sequencer.component.scss | 21 ++ .../src/app/sequencer/sequencer.component.ts | 180 +++++++++++++++--- .../src/app/settings/settings.component.ts | 2 +- .../dialogs/location/location.dialog.ts | 2 +- .../shared/services/browser-window.service.ts | 2 +- .../src/shared/services/electron.service.ts | 12 +- desktop/src/shared/services/prime.service.ts | 10 +- desktop/src/shared/types.ts | 13 +- 18 files changed, 376 insertions(+), 107 deletions(-) diff --git a/desktop/app/main.ts b/desktop/app/main.ts index c6a89978c..6159e6b1a 100644 --- a/desktop/app/main.ts +++ b/desktop/app/main.ts @@ -1,8 +1,9 @@ import { Client } from '@stomp/stompjs' import { BrowserWindow, Menu, Notification, app, dialog, ipcMain, screen, shell } from 'electron' +import * as fs from 'fs' import { ChildProcessWithoutNullStreams, spawn } from 'node:child_process' import * as path from 'path' -import { InternalEventType, MessageEvent, NotificationEvent, OpenDirectory, OpenFile, OpenWindow } from '../src/shared/types' +import { InternalEventType, JsonFile, MessageEvent, NotificationEvent, OpenDirectory, OpenFile, OpenWindow, SaveJson } from '../src/shared/types' import { WebSocket } from 'ws' Object.assign(global, { WebSocket }) @@ -292,7 +293,7 @@ try { return !value.canceled && value.filePaths[0] }) - ipcMain.handle('SAVE_FITS_AS', async (event) => { + ipcMain.handle('SAVE_FITS', async (event) => { const ownerWindow = findWindowById(event.sender.id) const value = await dialog.showSaveDialog(ownerWindow!, { filters: [ @@ -305,6 +306,54 @@ try { return !value.canceled && value.filePath }) + ipcMain.handle('SAVE_JSON', async (event, data: SaveJson) => { + const ownerWindow = findWindowById(event.sender.id) + + function writeFile(path: string) { + const json = JSON.stringify(data.json) + fs.writeFileSync(path, json) + return { path, json: data.json } + } + + if (data.path) { + return writeFile(data.path) + } else { + const value = await dialog.showSaveDialog(ownerWindow!, { + filters: [ + ...data.filters ?? [], + { name: 'JSON files', extensions: ['json'] }, + ], + defaultPath: data.defaultPath, + properties: ['createDirectory', 'showOverwriteConfirmation'], + }) + + if (!value.canceled) { + return writeFile(value.filePath!) + } + } + + return false + }) + + ipcMain.handle('OPEN_JSON', async (event, data?: OpenFile) => { + const ownerWindow = findWindowById(event.sender.id) + const value = await dialog.showOpenDialog(ownerWindow!, { + filters: [ + ...data?.filters ?? [], + { name: 'JSON files', extensions: ['json'] }, + ], + properties: ['openFile'], + defaultPath: data?.defaultPath || undefined, + }) + + if (!value.canceled) { + const buffer = fs.readFileSync(value.filePaths[0]) + return { path: value.filePaths[0], json: JSON.parse(buffer.toString('utf-8')) } + } else { + return false + } + }) + ipcMain.handle('OPEN_DIRECTORY', async (event, data?: OpenDirectory) => { const ownerWindow = findWindowById(event.sender.id) const value = await dialog.showOpenDialog(ownerWindow!, { diff --git a/desktop/src/app/app.component.html b/desktop/src/app/app.component.html index aaf3509f6..c97b0ef2a 100644 --- a/desktop/src/app/app.component.html +++ b/desktop/src/app/app.component.html @@ -6,9 +6,9 @@ + (onClick)="e.command?.({originalEvent: $event, item: e, index: i})" size="small" *ngFor="let e of topMenu; let i = index" /> - + diff --git a/desktop/src/app/app.component.ts b/desktop/src/app/app.component.ts index 6f97c42db..0abc98b60 100644 --- a/desktop/src/app/app.component.ts +++ b/desktop/src/app/app.component.ts @@ -16,7 +16,7 @@ export class AppComponent implements AfterViewInit { maximizable = false subTitle = '' backgroundColor = '#212121' - extra: MenuItem[] = [] + topMenu: MenuItem[] = [] get title() { return this.windowTitle.getTitle() diff --git a/desktop/src/app/app.module.ts b/desktop/src/app/app.module.ts index 9a96bc1c2..f19de018c 100644 --- a/desktop/src/app/app.module.ts +++ b/desktop/src/app/app.module.ts @@ -1,3 +1,4 @@ +import { DragDropModule } from '@angular/cdk/drag-drop' import { CommonModule } from '@angular/common' import { HttpClientModule } from '@angular/common/http' import { LOCALE_ID, NgModule } from '@angular/core' @@ -115,6 +116,7 @@ import { SettingsComponent } from './settings/settings.component' ConfirmDialogModule, ContextMenuModule, DialogModule, + DragDropModule, DropdownModule, DynamicDialogModule, FormsModule, diff --git a/desktop/src/app/atlas/atlas.component.ts b/desktop/src/app/atlas/atlas.component.ts index 346b0ff14..cab6cd982 100644 --- a/desktop/src/app/atlas/atlas.component.ts +++ b/desktop/src/app/atlas/atlas.component.ts @@ -171,7 +171,7 @@ export class AtlasComponent implements OnInit, AfterContentInit, AfterViewInit, showSatelliteFilterDialog = false readonly satelliteSearchGroup = new Map() - name?= 'Sun' + name? = 'Sun' tags: { title: string, severity: string }[] = [] @ViewChild('imageOfSun') @@ -482,7 +482,7 @@ export class AtlasComponent implements OnInit, AfterContentInit, AfterViewInit, ) { app.title = 'Sky Atlas' - app.extra.push({ + app.topMenu.push({ icon: 'mdi mdi-calendar', tooltip: 'Date & time', command: (e) => { diff --git a/desktop/src/app/calibration/calibration.component.ts b/desktop/src/app/calibration/calibration.component.ts index 9d80947ac..063bf08ee 100644 --- a/desktop/src/app/calibration/calibration.component.ts +++ b/desktop/src/app/calibration/calibration.component.ts @@ -34,7 +34,7 @@ export class CalibrationComponent implements AfterViewInit, OnDestroy { ) { app.title = 'Calibration' - app.extra.push({ + app.topMenu.push({ icon: 'mdi mdi-image-plus', tooltip: 'Add file', command: async () => { @@ -48,7 +48,7 @@ export class CalibrationComponent implements AfterViewInit, OnDestroy { }, }) - app.extra.push({ + app.topMenu.push({ icon: 'mdi mdi-folder-plus', tooltip: 'Add folder', command: async () => { diff --git a/desktop/src/app/camera/camera.component.html b/desktop/src/app/camera/camera.component.html index 9bd107182..45c6e5424 100644 --- a/desktop/src/app/camera/camera.component.html +++ b/desktop/src/app/camera/camera.component.html @@ -17,10 +17,10 @@ pTooltip="Image viewer" tooltipPosition="bottom" /> - +
-
+
@@ -34,7 +34,8 @@
-
+
{{ settling ? 'settling' : waiting ? 'waiting' : capturing ? 'capturing' : 'idle' }} @@ -110,7 +111,7 @@ [allowEmpty]="false" (ngModelChange)="savePreference()" /> - +
@@ -127,7 +128,7 @@
Exposure Mode -
@@ -237,10 +238,12 @@
- - +
diff --git a/desktop/src/app/camera/camera.component.ts b/desktop/src/app/camera/camera.component.ts index 650f4bfa8..8db2274ef 100644 --- a/desktop/src/app/camera/camera.component.ts +++ b/desktop/src/app/camera/camera.component.ts @@ -1,6 +1,7 @@ -import { AfterContentInit, Component, HostListener, NgZone, OnDestroy } from '@angular/core' +import { AfterContentInit, Component, HostListener, NgZone, OnDestroy, Optional } from '@angular/core' import { ActivatedRoute } from '@angular/router' import { MenuItem } from 'primeng/api' +import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog' import { ApiService } from '../../shared/services/api.service' import { BrowserWindowService } from '../../shared/services/browser-window.service' import { ElectronService } from '../../shared/services/electron.service' @@ -30,6 +31,7 @@ export class CameraComponent implements AfterContentInit, OnDestroy { wheel?: FilterWheel showDitherDialog = false + dialogMode = false readonly cameraModel: MenuItem[] = [ { @@ -194,15 +196,17 @@ export class CameraComponent implements AfterContentInit, OnDestroy { ] constructor( - private app: AppComponent, private api: ApiService, private browserWindow: BrowserWindowService, private electron: ElectronService, private storage: LocalStorageService, private route: ActivatedRoute, ngZone: NgZone, + @Optional() private app?: AppComponent, + @Optional() private dialogRef?: DynamicDialogRef, + @Optional() config?: DynamicDialogConfig, ) { - app.title = 'Camera' + if (app) app.title = 'Camera' electron.on('CAMERA_UPDATED', event => { if (event.device.name === this.camera?.name) { @@ -249,6 +253,12 @@ export class CameraComponent implements AfterContentInit, OnDestroy { }) } }) + + if (config) { + Object.assign(this.request, config.data) + this.dialogMode = true + this.cameraChanged(this.request.camera) + } } async ngAfterContentInit() { @@ -260,20 +270,22 @@ export class CameraComponent implements AfterContentInit, OnDestroy { @HostListener('window:unload') ngOnDestroy() { - this.abortCapture() + if (!this.dialogMode) { + this.abortCapture() + } } async cameraChanged(camera?: Camera) { - if (camera) { - this.app.subTitle = camera.name - + if (camera && camera.name) { camera = await this.api.camera(camera.name) Object.assign(this.camera, camera) this.loadPreference() this.update() - } else { - this.app.subTitle = '' + } + + if (this.app) { + this.app.subTitle = camera?.name ?? '' } } @@ -382,16 +394,22 @@ export class CameraComponent implements AfterContentInit, OnDestroy { this.savePreference() } + apply() { + this.dialogRef?.close(this.makeCameraStartCapture()) + } + private loadPreference() { if (this.camera) { - const preference = this.storage.get(`camera.${this.camera.name}`, {}) + const mode = this.dialogMode ? '.dialog' : '' + const preference = this.storage.get(`camera.${this.camera.name}${mode}`, {}) + this.request.autoSave = preference.autoSave ?? false this.savePath = preference.savePath ?? '' this.request.autoSubFolderMode = preference.autoSubFolderMode ?? 'OFF' this.setpointTemperature = preference.setpointTemperature ?? 0 this.request.exposureTime = preference.exposureTime ?? this.camera.exposureMin this.exposureTimeUnit = preference.exposureTimeUnit ?? ExposureTimeUnit.MICROSECOND - this.exposureMode = preference.exposureMode ?? 'SINGLE' + this.exposureMode = this.dialogMode ? 'FIXED' : (preference.exposureMode ?? 'SINGLE') this.request.exposureDelay = preference.exposureDelay ?? 0 this.request.exposureAmount = preference.exposureAmount ?? 1 this.request.x = preference.x ?? this.camera.minX @@ -439,7 +457,8 @@ export class CameraComponent implements AfterContentInit, OnDestroy { dither: this.request.dither, } - this.storage.set(`camera.${this.camera.name}`, preference) + const mode = this.dialogMode ? '.dialog' : '' + this.storage.set(`camera.${this.camera.name}${mode}`, preference) } } } diff --git a/desktop/src/app/image/image.component.ts b/desktop/src/app/image/image.component.ts index d66ff8421..fa293e16e 100644 --- a/desktop/src/app/image/image.component.ts +++ b/desktop/src/app/image/image.component.ts @@ -115,7 +115,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { label: 'Save as...', icon: 'mdi mdi-content-save', command: async () => { - const path = await this.electron.send('SAVE_FITS_AS') + const path = await this.electron.send('SAVE_FITS') if (path) this.api.saveImageAs(this.imageData.path!, path) }, } diff --git a/desktop/src/app/sequencer/sequencer.component.html b/desktop/src/app/sequencer/sequencer.component.html index 36b46f353..7d52e5128 100644 --- a/desktop/src/app/sequencer/sequencer.component.html +++ b/desktop/src/app/sequencer/sequencer.component.html @@ -1,8 +1,30 @@
- +
+
+ + + + +
+
+ + + + +
+
+ + + + +
@@ -16,52 +38,66 @@
- -
+ +
-
- - - - - - - - - - - - - - +
+
+ + +
+
+
+
+ + + + + +
+
+ + + + +
+
+ + + + +
+
+
-
+
- - - - - - - - + + + + + + +
-
- -
+ +
\ No newline at end of file diff --git a/desktop/src/app/sequencer/sequencer.component.scss b/desktop/src/app/sequencer/sequencer.component.scss index e69de29bb..1e70fa960 100644 --- a/desktop/src/app/sequencer/sequencer.component.scss +++ b/desktop/src/app/sequencer/sequencer.component.scss @@ -0,0 +1,21 @@ +:host { + ::ng-deep { + .p-card { + .p-card-body { + padding: 0px; + padding-left: 1rem !important; + padding-right: 1rem !important; + } + + .p-card-content { + padding: 0px; + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; + } + } + + .p-orderlist-controls { + display: none; + } + } +} \ No newline at end of file diff --git a/desktop/src/app/sequencer/sequencer.component.ts b/desktop/src/app/sequencer/sequencer.component.ts index 3031c4472..e4ca98484 100644 --- a/desktop/src/app/sequencer/sequencer.component.ts +++ b/desktop/src/app/sequencer/sequencer.component.ts @@ -1,11 +1,14 @@ +import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop' import { AfterContentInit, Component, HostListener, NgZone, OnDestroy } from '@angular/core' import { ActivatedRoute } from '@angular/router' import { ApiService } from '../../shared/services/api.service' import { BrowserWindowService } from '../../shared/services/browser-window.service' import { ElectronService } from '../../shared/services/electron.service' import { LocalStorageService } from '../../shared/services/local-storage.service' -import { Camera, CameraStartCapture, FilterWheel, Focuser } from '../../shared/types' +import { PrimeService } from '../../shared/services/prime.service' +import { Camera, CameraStartCapture, FilterWheel, Focuser, SequenceCaptureMode, SequencePlan } from '../../shared/types' import { AppComponent } from '../app.component' +import { CameraComponent } from '../camera/camera.component' @Component({ selector: 'app-sequencer', @@ -18,20 +21,71 @@ export class SequencerComponent implements AfterContentInit, OnDestroy { wheels: FilterWheel[] = [] focusers: Focuser[] = [] - slots: CameraStartCapture[] = [] + readonly captureModes: SequenceCaptureMode[] = ['FULLY', 'INTERLEAVED'] + readonly plan: SequencePlan = { + initialDelay: 0, + captureMode: 'FULLY', + entries: [], + } sequenceInProgress = false + savedPath?: string constructor( - private app: AppComponent, + app: AppComponent, private api: ApiService, private browserWindow: BrowserWindowService, private electron: ElectronService, private storage: LocalStorageService, private route: ActivatedRoute, + private prime: PrimeService, ngZone: NgZone, ) { app.title = 'Sequencer' + + app.topMenu.push({ + icon: 'mdi mdi-content-save', + label: 'Save', + command: async () => { + const file = await electron.saveJson({ path: this.savedPath, json: this.plan }) + + if (file !== false) { + this.savedPath = file.path + app.subTitle = this.savedPath! + } + }, + }) + app.topMenu.push({ + icon: 'mdi mdi-content-save-edit', + label: 'Save as', + command: async () => { + const file = await electron.saveJson({ json: this.plan }) + + if (file !== false) { + this.savedPath = file.path + app.subTitle = this.savedPath! + } + }, + }) + app.topMenu.push({ + icon: 'mdi mdi-folder-open', + label: 'Load', + command: async () => { + const file = await electron.openJson() + + if (file !== false) { + this.savedPath = file.path + this.loadPlan(file.json) + app.subTitle = this.savedPath! + } + }, + }) + + electron.on('CAMERA_UPDATED', event => { + for (const entry of this.plan.entries) { + this.updateEntryFromCamera(entry, event.device) + } + }) } async ngAfterContentInit() { @@ -39,36 +93,104 @@ export class SequencerComponent implements AfterContentInit, OnDestroy { this.wheels = await this.api.wheels() this.focusers = await this.api.focusers() - for (let i = 0; i < 32; i++) { - const camera = this.cameras[0] - const wheel = this.wheels[0] - const focuser = this.focusers[0] - - this.slots.push({ - enabled: false, - camera, - exposureTime: 1000000, - exposureAmount: 1, - exposureDelay: 0, - x: camera?.minX ?? 0, - y: camera?.minY ?? 0, - width: camera?.maxWidth ?? 0, - height: camera?.maxHeight ?? 0, - frameType: 'LIGHT', - binX: 1, - binY: 1, - gain: 0, - offset: 0, - autoSave: true, - autoSubFolderMode: 'OFF', - wheel, - focuser, - }) + if (!this.loadPlan()) { + this.add() } - this.route.queryParams.subscribe(e => { }) + // this.route.queryParams.subscribe(e => { }) } @HostListener('window:unload') ngOnDestroy() { } + + add() { + const camera = this.cameras[0] + const wheel = this.wheels[0] + const focuser = this.focusers[0] + + this.plan.entries.push({ + enabled: true, + camera, + exposureTime: 1000000, + exposureAmount: 1, + exposureDelay: 0, + x: camera?.minX ?? 0, + y: camera?.minY ?? 0, + width: camera?.maxWidth ?? 0, + height: camera?.maxHeight ?? 0, + frameType: 'LIGHT', + binX: 1, + binY: 1, + gain: 0, + offset: 0, + autoSave: true, + autoSubFolderMode: 'OFF', + wheel, + focuser, + }) + } + + drop(event: CdkDragDrop) { + moveItemInArray(this.plan.entries, event.previousIndex, event.currentIndex) + } + + updateEntryFromCamera(entry: CameraStartCapture, camera?: Camera) { + if (camera && camera.connected) { + if (entry.camera && entry.camera.name === camera.name) { + if (camera.maxX) entry.x = Math.max(camera.minX, Math.min(entry.x, camera.maxX)) + if (camera.maxY) entry.y = Math.max(camera.minY, Math.min(entry.y, camera.maxY)) + + if (camera.maxWidth && (entry.width <= 0 || entry.width > camera.maxWidth)) entry.width = camera.maxWidth + if (camera.maxHeight && (entry.height <= 0 || entry.height > camera.maxHeight)) entry.height = camera.maxHeight + + if (camera.maxBinX) entry.binX = Math.max(1, Math.min(entry.binX, camera.maxBinX)) + if (camera.maxBinY) entry.binY = Math.max(1, Math.min(entry.binY, camera.maxBinY)) + if (camera.gainMax) entry.gain = Math.max(camera.gainMin, Math.min(entry.gain, camera.gainMax)) + if (camera.offsetMax) entry.offset = Math.max(camera.offsetMin, Math.min(entry.offset, camera.offsetMax)) + + this.savePlan() + } + } + } + + private loadPlan(plan?: SequencePlan) { + plan ??= this.storage.get('sequencer.plan', this.plan) ?? [] + + for (const entry of plan.entries) { + if (entry.camera) { + entry.camera = this.cameras.find(e => e.name === entry.camera?.name) ?? this.cameras[0] + } else { + entry.camera = this.cameras[0] + } + + if (entry.focuser) { + entry.focuser = this.focusers.find(e => e.name === entry.focuser?.name) ?? this.focusers[0] + } else { + entry.focuser = this.focusers[0] + } + + if (entry.wheel) { + entry.wheel = this.wheels.find(e => e.name === entry.wheel?.name) ?? this.wheels[0] + } else { + entry.wheel = this.wheels[0] + } + } + + Object.assign(this.plan, plan) + + return plan.entries.length + } + + async showCameraDialog(entry: CameraStartCapture) { + const result = await this.prime.open(CameraComponent, { header: 'Camera', width: 'calc(400px + 2.5rem)', data: Object.assign({}, entry) }) + + if (result) { + Object.assign(entry, result) + this.savePlan() + } + } + + savePlan() { + this.storage.set('sequencer.plan', this.plan) + } } diff --git a/desktop/src/app/settings/settings.component.ts b/desktop/src/app/settings/settings.component.ts index 6ceb802ae..33c9e1e18 100644 --- a/desktop/src/app/settings/settings.component.ts +++ b/desktop/src/app/settings/settings.component.ts @@ -44,7 +44,7 @@ export class SettingsComponent implements AfterViewInit, OnDestroy { ) { app.title = 'Settings' - app.extra.push({ + app.topMenu.push({ icon: 'mdi mdi-content-save', tooltip: 'Save changes', command: () => { diff --git a/desktop/src/shared/dialogs/location/location.dialog.ts b/desktop/src/shared/dialogs/location/location.dialog.ts index 56e5180a4..09fcb36fc 100644 --- a/desktop/src/shared/dialogs/location/location.dialog.ts +++ b/desktop/src/shared/dialogs/location/location.dialog.ts @@ -16,7 +16,7 @@ export class LocationDialog implements AfterViewInit { constructor( private dialogRef: DynamicDialogRef, - config: DynamicDialogConfig, + config: DynamicDialogConfig, ) { this.location = config.data! } diff --git a/desktop/src/shared/services/browser-window.service.ts b/desktop/src/shared/services/browser-window.service.ts index 5c5f259b6..784004eb7 100644 --- a/desktop/src/shared/services/browser-window.service.ts +++ b/desktop/src/shared/services/browser-window.service.ts @@ -94,7 +94,7 @@ export class BrowserWindowService { openSequencer(options: Omit, 'data'> = {}) { options.icon ||= 'workflow' - options.width ||= 760 + options.width ||= 630 options.height ||= 570 options.resizable = true this.openWindow({ ...options, id: 'sequencer', path: 'sequencer', data: undefined }) diff --git a/desktop/src/shared/services/electron.service.ts b/desktop/src/shared/services/electron.service.ts index 898781b3d..3c41a3e27 100644 --- a/desktop/src/shared/services/electron.service.ts +++ b/desktop/src/shared/services/electron.service.ts @@ -8,8 +8,8 @@ import { ipcRenderer, webFrame } from 'electron' import * as fs from 'fs' import { ApiEventType, Camera, CameraCaptureEvent, DARVEvent, DeviceMessageEvent, FilterWheel, Focuser, - GuideOutput, Guider, GuiderMessageEvent, HistoryStep, INDIMessageEvent, InternalEventType, Location, Mount, NotificationEvent, - NotificationEventType, OpenDirectory, OpenFile + GuideOutput, Guider, GuiderMessageEvent, HistoryStep, INDIMessageEvent, InternalEventType, JsonFile, Location, Mount, NotificationEvent, + NotificationEventType, OpenDirectory, OpenFile, SaveJson } from '../types' import { ApiService } from './api.service' @@ -100,4 +100,12 @@ export class ElectronService { openDirectory(data?: OpenDirectory): Promise { return this.send('OPEN_DIRECTORY', data) } + + saveJson(data: SaveJson): Promise | false> { + return this.send('SAVE_JSON', data) + } + + openJson(data?: OpenFile): Promise | false> { + return this.send('OPEN_JSON', data) + } } diff --git a/desktop/src/shared/services/prime.service.ts b/desktop/src/shared/services/prime.service.ts index 3c51325d9..323819a90 100644 --- a/desktop/src/shared/services/prime.service.ts +++ b/desktop/src/shared/services/prime.service.ts @@ -1,6 +1,6 @@ import { Injectable, Type } from '@angular/core' import { ConfirmEventType, ConfirmationService } from 'primeng/api' -import { DialogService } from 'primeng/dynamicdialog' +import { DialogService, DynamicDialogConfig } from 'primeng/dynamicdialog' @Injectable({ providedIn: 'root' }) export class PrimeService { @@ -10,16 +10,18 @@ export class PrimeService { private confirmation: ConfirmationService, ) { } - open(componentType: Type, config: { header: string, draggable?: boolean, data?: T }) { + open(componentType: Type, config: DynamicDialogConfig) { const ref = this.dialog.open(componentType, { ...config, draggable: config.draggable ?? true, resizable: false, - width: '80vw', + width: config.width || '80vw', style: { - 'max-width': '360px', + ...config.style, + 'max-width': '480px', }, contentStyle: { + ...config.contentStyle, 'overflow-y': 'hidden', }, }) diff --git a/desktop/src/shared/types.ts b/desktop/src/shared/types.ts index 99ef672b5..54c3a8c5b 100644 --- a/desktop/src/shared/types.ts +++ b/desktop/src/shared/types.ts @@ -309,6 +309,13 @@ export interface OpenFile extends OpenDirectory { filters?: Electron.FileFilter[] } +export interface JsonFile { + path?: string + json: T +} + +export interface SaveJson extends OpenFile, JsonFile { } + export interface GuideCaptureEvent { camera: Camera } @@ -631,7 +638,7 @@ export interface SequencePlan { initialDelay: number captureMode: SequenceCaptureMode savePath?: string - slots: CameraStartCapture[] + entries: CameraStartCapture[] dither?: Dither autoFocus?: AutoFocusAfterConditions } @@ -838,9 +845,9 @@ export const API_EVENT_TYPES = [ export type ApiEventType = (typeof API_EVENT_TYPES)[number] export const INTERNAL_EVENT_TYPES = [ - 'SAVE_FITS_AS', 'OPEN_FILE', 'OPEN_WINDOW', 'OPEN_DIRECTORY', 'CLOSE_WINDOW', + 'SAVE_FITS', 'OPEN_FILE', 'OPEN_WINDOW', 'OPEN_DIRECTORY', 'CLOSE_WINDOW', 'PIN_WINDOW', 'UNPIN_WINDOW', 'MINIMIZE_WINDOW', 'MAXIMIZE_WINDOW', - 'WHEEL_RENAMED', 'LOCATION_CHANGED', + 'WHEEL_RENAMED', 'LOCATION_CHANGED', 'SAVE_JSON', 'OPEN_JSON' ] as const export type InternalEventType = (typeof INTERNAL_EVENT_TYPES)[number] From cf7e8edadea5e4a67f885d4bd32d01f098e23870 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Tue, 19 Dec 2023 22:17:55 -0300 Subject: [PATCH 56/87] [api]: Move test data files to /data directory --- api/src/test/kotlin/SkyDatabaseGenerator.kt | 6 +++--- {api/data => data}/.gitignore | 0 .../src/main/resources => data}/1 Ceres.bsp | Bin .../main/resources => data}/65803 Didymos.bsp | Bin {api/data => data}/IAU-CSN.txt | 0 .../src/main/resources => data}/catalog.dat | Bin {api/data => data}/dsos.json.gz | Bin .../main/resources => data}/finals2000A.all | 0 .../fits/NGC3344.Color.16.fits | 0 .../fits/NGC3344.Color.32.fits | 0 .../fits/NGC3344.Color.8.fits | 0 .../fits/NGC3344.Color.F32.fits | Bin .../fits/NGC3344.Color.F64.fits | 0 .../fits/NGC3344.Mono.16.fits | Bin .../fits/NGC3344.Mono.32.fits | 0 .../fits/NGC3344.Mono.8.fits | 0 .../fits/NGC3344.Mono.F32.fits | Bin .../fits/NGC3344.Mono.F64.fits | 0 .../resources => data}/fits/STAR_FOCUS_1.fits | Bin .../fits/STAR_FOCUS_10.fits | 0 .../fits/STAR_FOCUS_11.fits | 0 .../fits/STAR_FOCUS_12.fits | Bin .../fits/STAR_FOCUS_13.fits | 0 .../fits/STAR_FOCUS_14.fits | Bin .../fits/STAR_FOCUS_15.fits | 0 .../fits/STAR_FOCUS_16.fits | 0 .../fits/STAR_FOCUS_17.fits | Bin .../resources => data}/fits/STAR_FOCUS_2.fits | Bin .../resources => data}/fits/STAR_FOCUS_3.fits | Bin .../resources => data}/fits/STAR_FOCUS_4.fits | 0 .../resources => data}/fits/STAR_FOCUS_5.fits | 0 .../resources => data}/fits/STAR_FOCUS_6.fits | Bin .../resources => data}/fits/STAR_FOCUS_7.fits | 0 .../resources => data}/fits/STAR_FOCUS_8.fits | Bin .../resources => data}/fits/STAR_FOCUS_9.fits | Bin .../resources => data}/ldn673s_block1123.jpg | Bin .../src/main/resources => data}/names.dat | 0 {api/data => data}/stars.json.gz | Bin .../test/kotlin/AstrometryNetServiceTest.kt | 8 ++------ .../NovaAstrometryNetPlateSolverTest.kt | 8 ++------ .../common/concurrency/CancellationToken.kt | 13 ++++++------- nebulosa-nasa/src/test/kotlin/SpkTest.kt | 6 +++--- .../src/test/kotlin/AstrometryTest.kt | 6 +++--- .../src/test/kotlin/FixedStarTest.kt | 5 +++-- .../src/test/kotlin/GeographicPositionTest.kt | 5 +++-- nebulosa-nova/src/test/kotlin/ICRFTest.kt | 5 +++-- .../src/test/kotlin/NebulaTest.kt | 6 +++--- .../kotlin/nebulosa/test/FitsStringSpec.kt | 2 +- .../resources/MOON_PA_DE421_1900-2050.bpc | Bin 1770496 -> 0 bytes .../src/test/kotlin/StandardDeltaTimeTest.kt | 4 +++- nebulosa-time/src/test/kotlin/TimeTest.kt | 5 +++-- 51 files changed, 38 insertions(+), 41 deletions(-) rename {api/data => data}/.gitignore (100%) rename {nebulosa-test/src/main/resources => data}/1 Ceres.bsp (100%) rename {nebulosa-test/src/main/resources => data}/65803 Didymos.bsp (100%) rename {api/data => data}/IAU-CSN.txt (100%) rename {nebulosa-test/src/main/resources => data}/catalog.dat (100%) rename {api/data => data}/dsos.json.gz (100%) rename {nebulosa-test/src/main/resources => data}/finals2000A.all (100%) rename {nebulosa-test/src/main/resources => data}/fits/NGC3344.Color.16.fits (100%) rename {nebulosa-test/src/main/resources => data}/fits/NGC3344.Color.32.fits (100%) rename {nebulosa-test/src/main/resources => data}/fits/NGC3344.Color.8.fits (100%) rename {nebulosa-test/src/main/resources => data}/fits/NGC3344.Color.F32.fits (100%) rename {nebulosa-test/src/main/resources => data}/fits/NGC3344.Color.F64.fits (100%) rename {nebulosa-test/src/main/resources => data}/fits/NGC3344.Mono.16.fits (100%) rename {nebulosa-test/src/main/resources => data}/fits/NGC3344.Mono.32.fits (100%) rename {nebulosa-test/src/main/resources => data}/fits/NGC3344.Mono.8.fits (100%) rename {nebulosa-test/src/main/resources => data}/fits/NGC3344.Mono.F32.fits (100%) rename {nebulosa-test/src/main/resources => data}/fits/NGC3344.Mono.F64.fits (100%) rename {nebulosa-test/src/main/resources => data}/fits/STAR_FOCUS_1.fits (100%) rename {nebulosa-test/src/main/resources => data}/fits/STAR_FOCUS_10.fits (100%) rename {nebulosa-test/src/main/resources => data}/fits/STAR_FOCUS_11.fits (100%) rename {nebulosa-test/src/main/resources => data}/fits/STAR_FOCUS_12.fits (100%) rename {nebulosa-test/src/main/resources => data}/fits/STAR_FOCUS_13.fits (100%) rename {nebulosa-test/src/main/resources => data}/fits/STAR_FOCUS_14.fits (100%) rename {nebulosa-test/src/main/resources => data}/fits/STAR_FOCUS_15.fits (100%) rename {nebulosa-test/src/main/resources => data}/fits/STAR_FOCUS_16.fits (100%) rename {nebulosa-test/src/main/resources => data}/fits/STAR_FOCUS_17.fits (100%) rename {nebulosa-test/src/main/resources => data}/fits/STAR_FOCUS_2.fits (100%) rename {nebulosa-test/src/main/resources => data}/fits/STAR_FOCUS_3.fits (100%) rename {nebulosa-test/src/main/resources => data}/fits/STAR_FOCUS_4.fits (100%) rename {nebulosa-test/src/main/resources => data}/fits/STAR_FOCUS_5.fits (100%) rename {nebulosa-test/src/main/resources => data}/fits/STAR_FOCUS_6.fits (100%) rename {nebulosa-test/src/main/resources => data}/fits/STAR_FOCUS_7.fits (100%) rename {nebulosa-test/src/main/resources => data}/fits/STAR_FOCUS_8.fits (100%) rename {nebulosa-test/src/main/resources => data}/fits/STAR_FOCUS_9.fits (100%) rename {nebulosa-test/src/main/resources => data}/ldn673s_block1123.jpg (100%) rename {nebulosa-test/src/main/resources => data}/names.dat (100%) rename {api/data => data}/stars.json.gz (100%) delete mode 100644 nebulosa-test/src/main/resources/MOON_PA_DE421_1900-2050.bpc diff --git a/api/src/test/kotlin/SkyDatabaseGenerator.kt b/api/src/test/kotlin/SkyDatabaseGenerator.kt index 915d14cbf..08832e8d5 100644 --- a/api/src/test/kotlin/SkyDatabaseGenerator.kt +++ b/api/src/test/kotlin/SkyDatabaseGenerator.kt @@ -30,9 +30,9 @@ typealias CatalogNameProvider = Pair String?> object SkyDatabaseGenerator { - @JvmStatic private val STAR_DATABASE_PATH = Path.of("api/data/stars.json.gz") - @JvmStatic private val DSO_DATABASE_PATH = Path.of("api/data/dsos.json.gz") - @JvmStatic private val IAU_CSN_PATH = Path.of("api/data/IAU-CSN.txt") + @JvmStatic private val STAR_DATABASE_PATH = Path.of("../data/stars.json.gz") + @JvmStatic private val DSO_DATABASE_PATH = Path.of("../data/dsos.json.gz") + @JvmStatic private val IAU_CSN_PATH = Path.of("../data/IAU-CSN.txt") @JvmStatic private val LOG = loggerFor() diff --git a/api/data/.gitignore b/data/.gitignore similarity index 100% rename from api/data/.gitignore rename to data/.gitignore diff --git a/nebulosa-test/src/main/resources/1 Ceres.bsp b/data/1 Ceres.bsp similarity index 100% rename from nebulosa-test/src/main/resources/1 Ceres.bsp rename to data/1 Ceres.bsp diff --git a/nebulosa-test/src/main/resources/65803 Didymos.bsp b/data/65803 Didymos.bsp similarity index 100% rename from nebulosa-test/src/main/resources/65803 Didymos.bsp rename to data/65803 Didymos.bsp diff --git a/api/data/IAU-CSN.txt b/data/IAU-CSN.txt similarity index 100% rename from api/data/IAU-CSN.txt rename to data/IAU-CSN.txt diff --git a/nebulosa-test/src/main/resources/catalog.dat b/data/catalog.dat similarity index 100% rename from nebulosa-test/src/main/resources/catalog.dat rename to data/catalog.dat diff --git a/api/data/dsos.json.gz b/data/dsos.json.gz similarity index 100% rename from api/data/dsos.json.gz rename to data/dsos.json.gz diff --git a/nebulosa-test/src/main/resources/finals2000A.all b/data/finals2000A.all similarity index 100% rename from nebulosa-test/src/main/resources/finals2000A.all rename to data/finals2000A.all diff --git a/nebulosa-test/src/main/resources/fits/NGC3344.Color.16.fits b/data/fits/NGC3344.Color.16.fits similarity index 100% rename from nebulosa-test/src/main/resources/fits/NGC3344.Color.16.fits rename to data/fits/NGC3344.Color.16.fits diff --git a/nebulosa-test/src/main/resources/fits/NGC3344.Color.32.fits b/data/fits/NGC3344.Color.32.fits similarity index 100% rename from nebulosa-test/src/main/resources/fits/NGC3344.Color.32.fits rename to data/fits/NGC3344.Color.32.fits diff --git a/nebulosa-test/src/main/resources/fits/NGC3344.Color.8.fits b/data/fits/NGC3344.Color.8.fits similarity index 100% rename from nebulosa-test/src/main/resources/fits/NGC3344.Color.8.fits rename to data/fits/NGC3344.Color.8.fits diff --git a/nebulosa-test/src/main/resources/fits/NGC3344.Color.F32.fits b/data/fits/NGC3344.Color.F32.fits similarity index 100% rename from nebulosa-test/src/main/resources/fits/NGC3344.Color.F32.fits rename to data/fits/NGC3344.Color.F32.fits diff --git a/nebulosa-test/src/main/resources/fits/NGC3344.Color.F64.fits b/data/fits/NGC3344.Color.F64.fits similarity index 100% rename from nebulosa-test/src/main/resources/fits/NGC3344.Color.F64.fits rename to data/fits/NGC3344.Color.F64.fits diff --git a/nebulosa-test/src/main/resources/fits/NGC3344.Mono.16.fits b/data/fits/NGC3344.Mono.16.fits similarity index 100% rename from nebulosa-test/src/main/resources/fits/NGC3344.Mono.16.fits rename to data/fits/NGC3344.Mono.16.fits diff --git a/nebulosa-test/src/main/resources/fits/NGC3344.Mono.32.fits b/data/fits/NGC3344.Mono.32.fits similarity index 100% rename from nebulosa-test/src/main/resources/fits/NGC3344.Mono.32.fits rename to data/fits/NGC3344.Mono.32.fits diff --git a/nebulosa-test/src/main/resources/fits/NGC3344.Mono.8.fits b/data/fits/NGC3344.Mono.8.fits similarity index 100% rename from nebulosa-test/src/main/resources/fits/NGC3344.Mono.8.fits rename to data/fits/NGC3344.Mono.8.fits diff --git a/nebulosa-test/src/main/resources/fits/NGC3344.Mono.F32.fits b/data/fits/NGC3344.Mono.F32.fits similarity index 100% rename from nebulosa-test/src/main/resources/fits/NGC3344.Mono.F32.fits rename to data/fits/NGC3344.Mono.F32.fits diff --git a/nebulosa-test/src/main/resources/fits/NGC3344.Mono.F64.fits b/data/fits/NGC3344.Mono.F64.fits similarity index 100% rename from nebulosa-test/src/main/resources/fits/NGC3344.Mono.F64.fits rename to data/fits/NGC3344.Mono.F64.fits diff --git a/nebulosa-test/src/main/resources/fits/STAR_FOCUS_1.fits b/data/fits/STAR_FOCUS_1.fits similarity index 100% rename from nebulosa-test/src/main/resources/fits/STAR_FOCUS_1.fits rename to data/fits/STAR_FOCUS_1.fits diff --git a/nebulosa-test/src/main/resources/fits/STAR_FOCUS_10.fits b/data/fits/STAR_FOCUS_10.fits similarity index 100% rename from nebulosa-test/src/main/resources/fits/STAR_FOCUS_10.fits rename to data/fits/STAR_FOCUS_10.fits diff --git a/nebulosa-test/src/main/resources/fits/STAR_FOCUS_11.fits b/data/fits/STAR_FOCUS_11.fits similarity index 100% rename from nebulosa-test/src/main/resources/fits/STAR_FOCUS_11.fits rename to data/fits/STAR_FOCUS_11.fits diff --git a/nebulosa-test/src/main/resources/fits/STAR_FOCUS_12.fits b/data/fits/STAR_FOCUS_12.fits similarity index 100% rename from nebulosa-test/src/main/resources/fits/STAR_FOCUS_12.fits rename to data/fits/STAR_FOCUS_12.fits diff --git a/nebulosa-test/src/main/resources/fits/STAR_FOCUS_13.fits b/data/fits/STAR_FOCUS_13.fits similarity index 100% rename from nebulosa-test/src/main/resources/fits/STAR_FOCUS_13.fits rename to data/fits/STAR_FOCUS_13.fits diff --git a/nebulosa-test/src/main/resources/fits/STAR_FOCUS_14.fits b/data/fits/STAR_FOCUS_14.fits similarity index 100% rename from nebulosa-test/src/main/resources/fits/STAR_FOCUS_14.fits rename to data/fits/STAR_FOCUS_14.fits diff --git a/nebulosa-test/src/main/resources/fits/STAR_FOCUS_15.fits b/data/fits/STAR_FOCUS_15.fits similarity index 100% rename from nebulosa-test/src/main/resources/fits/STAR_FOCUS_15.fits rename to data/fits/STAR_FOCUS_15.fits diff --git a/nebulosa-test/src/main/resources/fits/STAR_FOCUS_16.fits b/data/fits/STAR_FOCUS_16.fits similarity index 100% rename from nebulosa-test/src/main/resources/fits/STAR_FOCUS_16.fits rename to data/fits/STAR_FOCUS_16.fits diff --git a/nebulosa-test/src/main/resources/fits/STAR_FOCUS_17.fits b/data/fits/STAR_FOCUS_17.fits similarity index 100% rename from nebulosa-test/src/main/resources/fits/STAR_FOCUS_17.fits rename to data/fits/STAR_FOCUS_17.fits diff --git a/nebulosa-test/src/main/resources/fits/STAR_FOCUS_2.fits b/data/fits/STAR_FOCUS_2.fits similarity index 100% rename from nebulosa-test/src/main/resources/fits/STAR_FOCUS_2.fits rename to data/fits/STAR_FOCUS_2.fits diff --git a/nebulosa-test/src/main/resources/fits/STAR_FOCUS_3.fits b/data/fits/STAR_FOCUS_3.fits similarity index 100% rename from nebulosa-test/src/main/resources/fits/STAR_FOCUS_3.fits rename to data/fits/STAR_FOCUS_3.fits diff --git a/nebulosa-test/src/main/resources/fits/STAR_FOCUS_4.fits b/data/fits/STAR_FOCUS_4.fits similarity index 100% rename from nebulosa-test/src/main/resources/fits/STAR_FOCUS_4.fits rename to data/fits/STAR_FOCUS_4.fits diff --git a/nebulosa-test/src/main/resources/fits/STAR_FOCUS_5.fits b/data/fits/STAR_FOCUS_5.fits similarity index 100% rename from nebulosa-test/src/main/resources/fits/STAR_FOCUS_5.fits rename to data/fits/STAR_FOCUS_5.fits diff --git a/nebulosa-test/src/main/resources/fits/STAR_FOCUS_6.fits b/data/fits/STAR_FOCUS_6.fits similarity index 100% rename from nebulosa-test/src/main/resources/fits/STAR_FOCUS_6.fits rename to data/fits/STAR_FOCUS_6.fits diff --git a/nebulosa-test/src/main/resources/fits/STAR_FOCUS_7.fits b/data/fits/STAR_FOCUS_7.fits similarity index 100% rename from nebulosa-test/src/main/resources/fits/STAR_FOCUS_7.fits rename to data/fits/STAR_FOCUS_7.fits diff --git a/nebulosa-test/src/main/resources/fits/STAR_FOCUS_8.fits b/data/fits/STAR_FOCUS_8.fits similarity index 100% rename from nebulosa-test/src/main/resources/fits/STAR_FOCUS_8.fits rename to data/fits/STAR_FOCUS_8.fits diff --git a/nebulosa-test/src/main/resources/fits/STAR_FOCUS_9.fits b/data/fits/STAR_FOCUS_9.fits similarity index 100% rename from nebulosa-test/src/main/resources/fits/STAR_FOCUS_9.fits rename to data/fits/STAR_FOCUS_9.fits diff --git a/nebulosa-test/src/main/resources/ldn673s_block1123.jpg b/data/ldn673s_block1123.jpg similarity index 100% rename from nebulosa-test/src/main/resources/ldn673s_block1123.jpg rename to data/ldn673s_block1123.jpg diff --git a/nebulosa-test/src/main/resources/names.dat b/data/names.dat similarity index 100% rename from nebulosa-test/src/main/resources/names.dat rename to data/names.dat diff --git a/api/data/stars.json.gz b/data/stars.json.gz similarity index 100% rename from api/data/stars.json.gz rename to data/stars.json.gz diff --git a/nebulosa-astrometrynet/src/test/kotlin/AstrometryNetServiceTest.kt b/nebulosa-astrometrynet/src/test/kotlin/AstrometryNetServiceTest.kt index 4212b58fc..ae6cc6bd4 100644 --- a/nebulosa-astrometrynet/src/test/kotlin/AstrometryNetServiceTest.kt +++ b/nebulosa-astrometrynet/src/test/kotlin/AstrometryNetServiceTest.kt @@ -10,16 +10,12 @@ import io.kotest.matchers.string.shouldNotBeBlank import nebulosa.astrometrynet.nova.NovaAstrometryNetService import nebulosa.astrometrynet.nova.Parity import nebulosa.astrometrynet.nova.Upload -import nebulosa.io.resource -import nebulosa.io.transferAndClose -import java.nio.file.Files -import kotlin.io.path.outputStream +import java.nio.file.Path class AstrometryNetServiceTest : StringSpec() { init { - val file = Files.createTempFile("nova", ".jpg") - .also { resource("ldn673s_block1123.jpg")!!.transferAndClose(it.outputStream()) } + val file = Path.of("../data/ldn673s_block1123.jpg") val service = NovaAstrometryNetService() diff --git a/nebulosa-astrometrynet/src/test/kotlin/NovaAstrometryNetPlateSolverTest.kt b/nebulosa-astrometrynet/src/test/kotlin/NovaAstrometryNetPlateSolverTest.kt index 8b1d735a3..fdc0160d0 100644 --- a/nebulosa-astrometrynet/src/test/kotlin/NovaAstrometryNetPlateSolverTest.kt +++ b/nebulosa-astrometrynet/src/test/kotlin/NovaAstrometryNetPlateSolverTest.kt @@ -3,20 +3,16 @@ import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.doubles.shouldBeExactly import nebulosa.astrometrynet.nova.NovaAstrometryNetService import nebulosa.astrometrynet.plate.solving.NovaAstrometryNetPlateSolver -import nebulosa.io.resource -import nebulosa.io.transferAndClose import nebulosa.math.deg import nebulosa.math.toArcsec import nebulosa.math.toDegrees -import java.nio.file.Files -import kotlin.io.path.outputStream +import java.nio.file.Path @Ignored class NovaAstrometryNetPlateSolverTest : StringSpec() { init { - val file = Files.createTempFile("nova", ".jpg") - .also { resource("ldn673s_block1123.jpg")!!.transferAndClose(it.outputStream()) } + val file = Path.of("../data/ldn673s_block1123.jpg") "solve" { val service = NovaAstrometryNetService() diff --git a/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/CancellationToken.kt b/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/CancellationToken.kt index d749cd9d5..b8ba730ee 100644 --- a/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/CancellationToken.kt +++ b/nebulosa-common/src/main/kotlin/nebulosa/common/concurrency/CancellationToken.kt @@ -12,14 +12,13 @@ class CancellationToken private constructor(private val completable: Completable private val listeners = LinkedHashSet() init { - completable - ?.whenCompleteAsync { source, _ -> - if (source != null) { - listeners.forEach { it.accept(source) } - } - - listeners.clear() + completable?.whenComplete { source, _ -> + if (source != null) { + listeners.forEach { it.accept(source) } } + + listeners.clear() + } } fun listen(listener: CancellationListener): Boolean { diff --git a/nebulosa-nasa/src/test/kotlin/SpkTest.kt b/nebulosa-nasa/src/test/kotlin/SpkTest.kt index 0942cca9a..8f74a9b85 100644 --- a/nebulosa-nasa/src/test/kotlin/SpkTest.kt +++ b/nebulosa-nasa/src/test/kotlin/SpkTest.kt @@ -1,13 +1,13 @@ import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.doubles.plusOrMinus import io.kotest.matchers.shouldBe -import nebulosa.io.resource -import nebulosa.io.source +import nebulosa.io.seekableSource import nebulosa.nasa.daf.RemoteDaf import nebulosa.nasa.daf.SourceDaf import nebulosa.nasa.spk.Spk import nebulosa.time.TimeYMDHMS import nebulosa.time.UTC +import java.nio.file.Path class SpkTest : StringSpec() { @@ -35,7 +35,7 @@ class SpkTest : StringSpec() { v[2] shouldBe (1.580159029289274E-03 plusOrMinus 1e-6) } "65803 Didymos (Type 21)" { - val spk = Spk(SourceDaf(resource("65803 Didymos.bsp")!!.readBytes().source())) + val spk = Spk(SourceDaf(Path.of("../data/65803 Didymos.bsp").seekableSource())) val (p, v) = spk[10, 2065803]!!.compute(UTC(TimeYMDHMS(2022, 12, 8, 20, 7, 15.0))) p[0] shouldBe (1.231026319338612E-01 plusOrMinus 1e-2) p[1] shouldBe (1.022833989843715E+00 plusOrMinus 1e-2) diff --git a/nebulosa-nova/src/test/kotlin/AstrometryTest.kt b/nebulosa-nova/src/test/kotlin/AstrometryTest.kt index 3f9bd7662..77571fc4c 100644 --- a/nebulosa-nova/src/test/kotlin/AstrometryTest.kt +++ b/nebulosa-nova/src/test/kotlin/AstrometryTest.kt @@ -1,8 +1,7 @@ import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.doubles.plusOrMinus import io.kotest.matchers.shouldBe -import nebulosa.io.resource -import nebulosa.io.source +import nebulosa.io.seekableSource import nebulosa.math.au import nebulosa.math.deg import nebulosa.math.normalized @@ -15,6 +14,7 @@ import nebulosa.nova.position.Barycentric import nebulosa.time.TDB import nebulosa.time.TimeYMDHMS import nebulosa.time.UTC +import java.nio.file.Path class AstrometryTest : StringSpec() { @@ -22,7 +22,7 @@ class AstrometryTest : StringSpec() { val de441 = Spk(RemoteDaf("https://ssd.jpl.nasa.gov/ftp/eph/planets/bsp/de441.bsp")) val mar097 = Spk(RemoteDaf("https://naif.jpl.nasa.gov/pub/naif/generic_kernels/spk/satellites/mar097.bsp")) val ura111 = Spk(RemoteDaf("https://naif.jpl.nasa.gov/pub/naif/generic_kernels/spk/satellites/ura111.bsp")) - val ceresSpk = Spk(SourceDaf(resource("1 Ceres.bsp")!!.readBytes().source())) + val ceresSpk = Spk(SourceDaf(Path.of("../data/1 Ceres.bsp").seekableSource())) val kernel = SpiceKernel(de441, mar097, ura111, ceresSpk) val sun = kernel[10] val moon = kernel[301] diff --git a/nebulosa-nova/src/test/kotlin/FixedStarTest.kt b/nebulosa-nova/src/test/kotlin/FixedStarTest.kt index d52061a4c..1cecd0eb3 100644 --- a/nebulosa-nova/src/test/kotlin/FixedStarTest.kt +++ b/nebulosa-nova/src/test/kotlin/FixedStarTest.kt @@ -1,7 +1,6 @@ import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.doubles.plusOrMinus import io.kotest.matchers.shouldBe -import nebulosa.io.resource import nebulosa.math.* import nebulosa.nasa.daf.RemoteDaf import nebulosa.nasa.spk.Spk @@ -12,12 +11,14 @@ import nebulosa.time.IERS import nebulosa.time.IERSA import nebulosa.time.TimeYMDHMS import nebulosa.time.UTC +import java.nio.file.Path +import kotlin.io.path.inputStream class FixedStarTest : StringSpec() { init { val iersa = IERSA() - iersa.load(resource("finals2000A.all")!!) + iersa.load(Path.of("../data/finals2000A.all").inputStream()) IERS.attach(iersa) "polaris" { diff --git a/nebulosa-nova/src/test/kotlin/GeographicPositionTest.kt b/nebulosa-nova/src/test/kotlin/GeographicPositionTest.kt index 374e55d62..f46466ffd 100644 --- a/nebulosa-nova/src/test/kotlin/GeographicPositionTest.kt +++ b/nebulosa-nova/src/test/kotlin/GeographicPositionTest.kt @@ -2,7 +2,6 @@ import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.doubles.plusOrMinus import io.kotest.matchers.ints.shouldBeExactly import io.kotest.matchers.shouldBe -import nebulosa.io.resource import nebulosa.math.deg import nebulosa.math.hms import nebulosa.math.m @@ -11,12 +10,14 @@ import nebulosa.time.IERS import nebulosa.time.IERSA import nebulosa.time.TimeYMDHMS import nebulosa.time.UT1 +import java.nio.file.Path +import kotlin.io.path.inputStream class GeographicPositionTest : StringSpec() { init { val iersa = IERSA() - iersa.load(resource("finals2000A.all")!!) + iersa.load(Path.of("../data/finals2000A.all").inputStream()) IERS.attach(iersa) "lst" { diff --git a/nebulosa-nova/src/test/kotlin/ICRFTest.kt b/nebulosa-nova/src/test/kotlin/ICRFTest.kt index 7ae8fe2c1..054b77af5 100644 --- a/nebulosa-nova/src/test/kotlin/ICRFTest.kt +++ b/nebulosa-nova/src/test/kotlin/ICRFTest.kt @@ -1,7 +1,6 @@ import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.doubles.plusOrMinus import io.kotest.matchers.shouldBe -import nebulosa.io.resource import nebulosa.math.* import nebulosa.nova.position.Geoid import nebulosa.nova.position.ICRF @@ -9,12 +8,14 @@ import nebulosa.time.IERS import nebulosa.time.IERSA import nebulosa.time.TimeJD import nebulosa.time.TimeYMDHMS +import java.nio.file.Path +import kotlin.io.path.inputStream class ICRFTest : StringSpec() { init { val iersa = IERSA() - iersa.load(resource("finals2000A.all")!!) + iersa.load(Path.of("../data/finals2000A.all").inputStream()) IERS.attach(iersa) "equatorial at date to equatorial J2000" { diff --git a/nebulosa-skycatalog-stellarium/src/test/kotlin/NebulaTest.kt b/nebulosa-skycatalog-stellarium/src/test/kotlin/NebulaTest.kt index cbc860462..4840439ac 100644 --- a/nebulosa-skycatalog-stellarium/src/test/kotlin/NebulaTest.kt +++ b/nebulosa-skycatalog-stellarium/src/test/kotlin/NebulaTest.kt @@ -1,21 +1,21 @@ import io.kotest.core.annotation.EnabledIf import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.ints.shouldBeExactly -import nebulosa.io.resource import nebulosa.math.deg import nebulosa.math.hours import nebulosa.skycatalog.stellarium.Nebula import nebulosa.test.NonGitHubOnlyCondition import okio.gzip import okio.source +import java.nio.file.Path @EnabledIf(NonGitHubOnlyCondition::class) class NebulaTest : StringSpec() { init { val nebula = Nebula() - val catalog = resource("catalog.dat")!!.source().gzip() - val names = resource("names.dat")!!.source() + val catalog = Path.of("../data/catalog.dat").source().gzip() + val names = Path.of("../data/names.dat").source() nebula.load(catalog, names) "load" { diff --git a/nebulosa-test/src/main/kotlin/nebulosa/test/FitsStringSpec.kt b/nebulosa-test/src/main/kotlin/nebulosa/test/FitsStringSpec.kt index 372ef01bd..cfab2b895 100644 --- a/nebulosa-test/src/main/kotlin/nebulosa/test/FitsStringSpec.kt +++ b/nebulosa-test/src/main/kotlin/nebulosa/test/FitsStringSpec.kt @@ -15,7 +15,7 @@ import kotlin.io.path.* @Suppress("PropertyName") abstract class FitsStringSpec : StringSpec() { - protected val FITS_DIR = "../nebulosa-test/src/main/resources/fits" + protected val FITS_DIR = "../data/fits" protected val NGC3344_COLOR_8 by lazy { Fits("$FITS_DIR/NGC3344.Color.8.fits").also(Fits::read) } protected val NGC3344_COLOR_16 by lazy { Fits("$FITS_DIR/NGC3344.Color.16.fits").also(Fits::read) } diff --git a/nebulosa-test/src/main/resources/MOON_PA_DE421_1900-2050.bpc b/nebulosa-test/src/main/resources/MOON_PA_DE421_1900-2050.bpc deleted file mode 100644 index 10e35ab7694a103bb1265ec591a96bf4772e6be2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1770496 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 Q*@B*=q0#nk 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⪻djtYA^pYF*bpdV76aS##GtjE(aJ#H!2cuI+_E9&G@cyk1 zsS195i<=u-P;4_#t7AUUNJCl5~|P2(MH3>aAqHpqN z?%F$$%vXzl6#fx(U(4Gv`}Pp7ZP+kweba%2-aod~MW z5{vMJro-{a`#2Ca@ACxHrU$Zo>ip_F-5{DOb7EJqA4|m~m^<~05;XbI2M=WQVNRFH z1*8%e;g;@u8I2J9FQ`ktuDlREkNRyc?>0^{qhSNiE+ z0*SI4i|tnoL6C;DVwDpK*!ppopUZgqU;H2DJK}FcYIIp$<5252C;4}KuOas$mJ=SH z#lz#6JN6Z>r0C|Y%J~oNgn134o@zU%L^xOzV^-iF4c-mqqJDVTeT< z3s#*7K2;ld^N^r_y|l#iW(4w}y{%7fjH+^><3H}LO0;}|roXnjc|X1ZXKxHlngqTC z;WM<>y#r3bGW!|dOCL>mll{}=BW89eMQ*zF^7~iJ8B@FDh5bSt=T@a1Lg@b(7PP5a zFdc-*ZTpuUX(Y($+x1EoLtjA5AkC}cTM0ntb#N(9Xe1;aNq;YW)*MEAo@QVSJq
  • 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

    ATgUDYz+IRWSQWJq(|7ASFrcL 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

    th`jov{ZY5x#;=N?pw7W#!@~yecuNRSY{h` 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