diff --git a/api/build.gradle.kts b/api/build.gradle.kts index 88cfc8c51..47b11a299 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -26,6 +26,7 @@ dependencies { implementation(project(":nebulosa-nova")) implementation(project(":nebulosa-sbd")) implementation(project(":nebulosa-simbad")) + implementation(project(":nebulosa-siril")) implementation(project(":nebulosa-stellarium-protocol")) implementation(project(":nebulosa-wcs")) implementation(project(":nebulosa-xisf")) diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPAStartRequest.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPAStartRequest.kt index acb655885..066e09151 100644 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPAStartRequest.kt +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPAStartRequest.kt @@ -4,7 +4,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties import jakarta.validation.Valid import jakarta.validation.constraints.NotNull import nebulosa.api.cameras.CameraStartCaptureRequest -import nebulosa.api.solver.PlateSolverOptions +import nebulosa.api.solver.PlateSolverRequest import nebulosa.guiding.GuideDirection import org.hibernate.validator.constraints.time.DurationMin import org.springframework.boot.convert.DurationUnit @@ -13,7 +13,7 @@ import java.time.temporal.ChronoUnit data class TPPAStartRequest( @JsonIgnoreProperties("camera", "focuser", "wheel") @JvmField val capture: CameraStartCaptureRequest = CameraStartCaptureRequest.EMPTY, - @field:NotNull @Valid @JvmField val plateSolver: PlateSolverOptions = PlateSolverOptions.EMPTY, + @field:NotNull @Valid @JvmField val plateSolver: PlateSolverRequest = PlateSolverRequest.EMPTY, @JvmField val startFromCurrentPosition: Boolean = true, @JvmField val compensateRefraction: Boolean = false, @JvmField val stopTrackingWhenDone: Boolean = true, diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPATask.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPATask.kt index e3ae706ff..b3d8e926c 100644 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPATask.kt +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPATask.kt @@ -84,7 +84,7 @@ data class TPPATask( captureEvent = event if (event.state == CameraCaptureState.EXPOSURE_FINISHED) { - savedImage = event.savePath!! + savedImage = event.savedPath!! } if (!finished.get()) { diff --git a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusRequest.kt b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusRequest.kt index 5f1c5edd1..2a30afc6c 100644 --- a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusRequest.kt +++ b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusRequest.kt @@ -2,7 +2,7 @@ package nebulosa.api.autofocus import nebulosa.api.cameras.CameraStartCaptureRequest import nebulosa.api.focusers.BacklashCompensation -import nebulosa.api.stardetection.StarDetectionOptions +import nebulosa.api.stardetection.StarDetectionRequest data class AutoFocusRequest( @JvmField val fittingMode: AutoFocusFittingMode = AutoFocusFittingMode.HYPERBOLIC, @@ -12,5 +12,5 @@ data class AutoFocusRequest( @JvmField val initialOffsetSteps: Int = 4, @JvmField val stepSize: Int = 50, @JvmField val totalNumberOfAttempts: Int = 1, - @JvmField val starDetector: StarDetectionOptions = StarDetectionOptions.EMPTY, + @JvmField val starDetector: StarDetectionRequest = StarDetectionRequest.EMPTY, ) diff --git a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt index 249e5c51d..d1231501b 100644 --- a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt +++ b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt @@ -220,7 +220,7 @@ data class AutoFocusTask( if (event.state == CameraCaptureState.EXPOSURE_FINISHED) { sendEvent(AutoFocusState.EXPOSURED, event) sendEvent(AutoFocusState.ANALYSING) - val detectedStars = starDetection.detect(event.savePath!!) + val detectedStars = starDetection.detect(event.savedPath!!) starCount = detectedStars.size LOG.info("detected $starCount stars") starHFD = detectedStars.measureDetectedStars() diff --git a/api/src/main/kotlin/nebulosa/api/beans/converters/angle/DeclinationDeserializer.kt b/api/src/main/kotlin/nebulosa/api/beans/converters/angle/DeclinationDeserializer.kt index 01e170cfe..0ed5dcbd1 100644 --- a/api/src/main/kotlin/nebulosa/api/beans/converters/angle/DeclinationDeserializer.kt +++ b/api/src/main/kotlin/nebulosa/api/beans/converters/angle/DeclinationDeserializer.kt @@ -1,3 +1,3 @@ package nebulosa.api.beans.converters.angle -class DeclinationDeserializer : AngleDeserializer(true) +class DeclinationDeserializer : AngleDeserializer(false) diff --git a/api/src/main/kotlin/nebulosa/api/beans/converters/angle/DegreesDeserializer.kt b/api/src/main/kotlin/nebulosa/api/beans/converters/angle/DegreesDeserializer.kt new file mode 100644 index 000000000..b97011eec --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/beans/converters/angle/DegreesDeserializer.kt @@ -0,0 +1,3 @@ +package nebulosa.api.beans.converters.angle + +class DegreesDeserializer : AngleDeserializer(false, defaultValue = 0.0) diff --git a/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameProvider.kt b/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameProvider.kt new file mode 100644 index 000000000..0c3ed2a06 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameProvider.kt @@ -0,0 +1,20 @@ +package nebulosa.api.calibration + +interface CalibrationFrameProvider { + + fun findBestDarkFrames( + name: String, temperature: Double, width: Int, height: Int, + binX: Int, binY: Int = binX, exposureTimeInMicroseconds: Long = 0L, + gain: Double = 0.0, + ): List + + fun findBestFlatFrames( + name: String, width: Int, height: Int, + binX: Int, binY: Int = binX, filter: String? = null + ): List + + fun findBestBiasFrames( + name: String, width: Int, height: Int, + binX: Int, binY: Int = binX, gain: Double = 0.0, + ): List +} diff --git a/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameService.kt b/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameService.kt index 8188e1b0d..25ecd51d6 100644 --- a/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameService.kt +++ b/api/src/main/kotlin/nebulosa/api/calibration/CalibrationFrameService.kt @@ -26,7 +26,7 @@ import kotlin.math.roundToInt @Service class CalibrationFrameService( private val calibrationFrameRepository: CalibrationFrameRepository, -) { +) : CalibrationFrameProvider { fun calibrate(name: String, image: Image, createNew: Boolean = false): Image { return synchronized(image) { @@ -146,13 +146,13 @@ class CalibrationFrameService( calibrationFrameRepository.delete(frame) } - // exposureTime, temperature, width, height, binX, binY, gain. - fun findBestDarkFrames(name: String, image: Image): List { - val header = image.header - val temperature = header.temperature - + override fun findBestDarkFrames( + name: String, temperature: Double, width: Int, height: Int, + binX: Int, binY: Int, exposureTimeInMicroseconds: Long, + gain: Double, + ): List { val frames = calibrationFrameRepository - .darkFrames(name, image.width, image.height, header.binX, header.exposureTimeInMicroseconds, header.gain) + .darkFrames(name, width, height, binX, exposureTimeInMicroseconds, gain) if (frames.isEmpty()) return emptyList() @@ -164,20 +164,46 @@ class CalibrationFrameService( return groupedFrames.firstEntry().value } - // filter, width, height, binX, binY. - fun findBestFlatFrames(name: String, image: Image): List { - val filter = image.header.filter + fun findBestDarkFrames(name: String, image: Image): List { + val header = image.header + val temperature = header.temperature + val binX = header.binX + val exposureTime = header.exposureTimeInMicroseconds + return findBestDarkFrames(name, temperature, image.width, image.height, binX, binX, exposureTime, header.gain) + } + + override fun findBestFlatFrames( + name: String, width: Int, height: Int, + binX: Int, binY: Int, filter: String? + ): List { // TODO: Generate master from matched frames. return calibrationFrameRepository - .flatFrames(name, filter, image.width, image.height, image.header.binX) + .flatFrames(name, filter, width, height, binX) } - // width, height, binX, binY, gain. - fun findBestBiasFrames(name: String, image: Image): List { + fun findBestFlatFrames(name: String, image: Image): List { + val header = image.header + val filter = header.filter + val binX = header.binX + + return findBestFlatFrames(name, image.width, image.height, binX, binX, filter) + } + + override fun findBestBiasFrames( + name: String, width: Int, height: Int, + binX: Int, binY: Int, gain: Double, + ): List { // TODO: Generate master from matched frames. return calibrationFrameRepository - .biasFrames(name, image.width, image.height, image.header.binX, image.header.gain) + .biasFrames(name, width, height, binX, gain) + } + + fun findBestBiasFrames(name: String, image: Image): List { + val header = image.header + val binX = header.binX + + return findBestBiasFrames(name, image.width, image.height, binX, binX, image.header.gain) } companion object { diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureEvent.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureEvent.kt index 805551724..7bcd3f6e2 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureEvent.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureEvent.kt @@ -1,12 +1,14 @@ package nebulosa.api.cameras +import com.fasterxml.jackson.annotation.JsonIgnore import nebulosa.api.messages.MessageEvent import nebulosa.indi.device.camera.Camera import java.nio.file.Path import java.time.Duration data class CameraCaptureEvent( - @JvmField val camera: Camera, + @JvmField @field:JsonIgnore val task: CameraCaptureTask, + @JvmField val camera: Camera = task.camera, @JvmField val state: CameraCaptureState = CameraCaptureState.IDLE, @JvmField val exposureAmount: Int = 0, @JvmField val exposureCount: Int = 0, @@ -16,15 +18,10 @@ data class CameraCaptureEvent( @JvmField val stepRemainingTime: Duration = Duration.ZERO, @JvmField val stepElapsedTime: Duration = Duration.ZERO, @JvmField val stepProgress: Double = 0.0, - @JvmField val savePath: Path? = null, + @JvmField val savedPath: Path? = null, + @JvmField val liveStackedPath: Path? = null, + @JvmField val capture: CameraStartCaptureRequest? = null, ) : MessageEvent { override val eventName = "CAMERA.CAPTURE_ELAPSED" - - companion object { - - @JvmStatic - fun exposureFinished(camera: Camera, savePath: Path) = - CameraCaptureEvent(camera, CameraCaptureState.EXPOSURE_FINISHED, savePath = savePath) - } } diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureExecutor.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureExecutor.kt index 0c0a83e91..2f7db5453 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureExecutor.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureExecutor.kt @@ -2,6 +2,7 @@ package nebulosa.api.cameras import io.reactivex.rxjava3.functions.Consumer import nebulosa.api.beans.annotations.Subscriber +import nebulosa.api.calibration.CalibrationFrameService import nebulosa.api.messages.MessageService import nebulosa.guiding.Guider import nebulosa.indi.device.camera.Camera @@ -18,6 +19,7 @@ class CameraCaptureExecutor( private val messageService: MessageService, private val guider: Guider, private val threadPoolTaskExecutor: ThreadPoolTaskExecutor, + private val calibrationFrameService: CalibrationFrameService, ) : Consumer, CameraEventAware { private val jobs = ConcurrentHashMap.newKeySet(2) @@ -36,7 +38,7 @@ class CameraCaptureExecutor( check(camera.connected) { "${camera.name} Camera is not connected" } check(jobs.none { it.task.camera === camera }) { "${camera.name} Camera Capture is already in progress" } - val task = CameraCaptureTask(camera, request, guider, executor = threadPoolTaskExecutor) + val task = CameraCaptureTask(camera, request, guider, executor = threadPoolTaskExecutor, calibrationFrameProvider = calibrationFrameService) task.subscribe(this) with(CameraCaptureJob(task)) { diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureTask.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureTask.kt index 6d6ade6f0..4b78b7e59 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureTask.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureTask.kt @@ -2,8 +2,10 @@ package nebulosa.api.cameras import com.fasterxml.jackson.annotation.JsonIgnore import io.reactivex.rxjava3.functions.Consumer +import nebulosa.api.calibration.CalibrationFrameProvider import nebulosa.api.guiding.DitherAfterExposureTask import nebulosa.api.guiding.WaitForSettleTask +import nebulosa.api.livestacking.LiveStackingRequest import nebulosa.api.tasks.AbstractTask import nebulosa.api.tasks.SplitTask import nebulosa.api.tasks.delay.DelayEvent @@ -12,6 +14,8 @@ import nebulosa.common.concurrency.cancel.CancellationToken import nebulosa.guiding.Guider import nebulosa.indi.device.camera.Camera import nebulosa.indi.device.camera.CameraEvent +import nebulosa.indi.device.filterwheel.FilterWheel +import nebulosa.livestacking.LiveStacker import nebulosa.log.loggerFor import java.nio.file.Path import java.time.Duration @@ -24,6 +28,7 @@ data class CameraCaptureTask( private val useFirstExposure: Boolean = false, private val exposureMaxRepeat: Int = 0, private val executor: Executor? = null, + private val calibrationFrameProvider: CalibrationFrameProvider? = null, ) : AbstractTask(), Consumer, CameraEventAware { private val delayTask = DelayTask(request.exposureDelay) @@ -32,7 +37,6 @@ data class CameraCaptureTask( private val cameraExposureTask = CameraExposureTask(camera, request) private val ditherAfterExposureTask = DitherAfterExposureTask(guider, request.dither) - @Volatile private var state = CameraCaptureState.IDLE @Volatile private var exposureCount = 0 @Volatile private var captureRemainingTime = Duration.ZERO @Volatile private var prevCaptureElapsedTime = Duration.ZERO @@ -41,12 +45,14 @@ data class CameraCaptureTask( @Volatile private var stepRemainingTime = Duration.ZERO @Volatile private var stepElapsedTime = Duration.ZERO @Volatile private var stepProgress = 0.0 - @Volatile private var savePath: Path? = null + @Volatile private var savedPath: Path? = null + @Volatile private var liveStackedPath: Path? = null @JvmField @JsonIgnore val estimatedCaptureTime: Duration = if (request.isLoop) Duration.ZERO else Duration.ofNanos(request.exposureTime.toNanos() * request.exposureAmount + request.exposureDelay.toNanos() * (request.exposureAmount - if (useFirstExposure) 0 else 1)) @Volatile private var exposureRepeatCount = 0 + @Volatile private var liveStacker: LiveStacker? = null init { delayTask.subscribe(this) @@ -62,19 +68,66 @@ data class CameraCaptureTask( cameraExposureTask.handleCameraEvent(event) } + private fun LiveStackingRequest.processCalibrationGroup(): LiveStackingRequest { + return if (calibrationFrameProvider != null && enabled && + !request.calibrationGroup.isNullOrBlank() && (dark == null || flat == null) + ) { + val calibrationGroup = request.calibrationGroup + val temperature = camera.temperature + val binX = request.binX + val binY = request.binY + val width = request.width / binX + val height = request.height / binY + val exposureTime = request.exposureTime.toNanos() / 1000 + val gain = request.gain.toDouble() + + val wheel = camera.snoopedDevices.firstOrNull { it is FilterWheel } as? FilterWheel + val filter = wheel?.let { it.names.getOrNull(it.position - 1) } + + val newDark = dark ?: calibrationFrameProvider + .findBestDarkFrames(calibrationGroup, temperature, width, height, binX, binY, exposureTime, gain) + .firstOrNull() + ?.path + + val newFlat = flat ?: calibrationFrameProvider + .findBestFlatFrames(calibrationGroup, width, height, binX, binY, filter) + .firstOrNull() + ?.path + + LOG.info("live stacking will use dark frame at {} and flat frame at {}", newDark, newFlat) + + copy(dark = newDark, flat = newFlat) + } else { + this + } + } + override fun execute(cancellationToken: CancellationToken) { LOG.info("Camera Capture started. camera={}, request={}, exposureCount={}", camera, request, exposureCount) cameraExposureTask.reset() + if (liveStacker == null && request.liveStacking.enabled && + (request.isLoop || request.exposureAmount > 1 || exposureMaxRepeat > 1) + ) { + try { + liveStacker = request.liveStacking.processCalibrationGroup().get() + liveStacker!!.start() + } catch (e: Throwable) { + LOG.error("failed to start live stacking. request={}", request.liveStacking, e) + + liveStacker?.close() + liveStacker = null + } + } + while (!cancellationToken.isCancelled && !cameraExposureTask.isAborted && ((exposureMaxRepeat > 0 && exposureRepeatCount < exposureMaxRepeat) || (exposureMaxRepeat <= 0 && (request.isLoop || exposureCount < request.exposureAmount))) ) { if (exposureCount == 0) { - state = CameraCaptureState.CAPTURE_STARTED - sendEvent() + sendEvent(CameraCaptureState.CAPTURE_STARTED) if (guider != null) { if (useFirstExposure) { @@ -107,11 +160,9 @@ data class CameraCaptureTask( } } - if (state != CameraCaptureState.CAPTURE_FINISHED) { - state = CameraCaptureState.CAPTURE_FINISHED - sendEvent() - } + sendEvent(CameraCaptureState.CAPTURE_FINISHED) + liveStacker?.close() exposureRepeatCount = 0 LOG.info("Camera Capture finished. camera={}, request={}, exposureCount={}", camera, request, exposureCount) @@ -119,61 +170,67 @@ data class CameraCaptureTask( @Synchronized override fun accept(event: Any) { - when (event) { + val state = when (event) { is DelayEvent -> { - state = CameraCaptureState.WAITING captureElapsedTime += event.waitTime stepElapsedTime = event.task.duration - event.remainingTime stepRemainingTime = event.remainingTime stepProgress = event.progress + CameraCaptureState.WAITING } is CameraExposureEvent -> { when (event.state) { CameraExposureState.STARTED -> { - state = CameraCaptureState.EXPOSURE_STARTED prevCaptureElapsedTime = captureElapsedTime exposureCount++ exposureRepeatCount++ + CameraCaptureState.EXPOSURE_STARTED } CameraExposureState.ELAPSED -> { - state = CameraCaptureState.EXPOSURING captureElapsedTime = prevCaptureElapsedTime + event.elapsedTime stepElapsedTime = event.elapsedTime stepRemainingTime = event.remainingTime stepProgress = event.progress + CameraCaptureState.EXPOSURING } CameraExposureState.FINISHED -> { - state = CameraCaptureState.EXPOSURE_FINISHED captureElapsedTime = prevCaptureElapsedTime + request.exposureTime - savePath = event.savedPath + savedPath = event.savedPath + liveStackedPath = addFrameToLiveStacker(savedPath) + CameraCaptureState.EXPOSURE_FINISHED } CameraExposureState.IDLE -> { - state = CameraCaptureState.CAPTURE_FINISHED + CameraCaptureState.CAPTURE_FINISHED } } } else -> return LOG.warn("unknown event: {}", event) } - sendEvent() + sendEvent(state) } - private fun sendEvent() { + private fun sendEvent(state: CameraCaptureState) { if (state != CameraCaptureState.IDLE && !request.isLoop) { captureRemainingTime = if (estimatedCaptureTime > captureElapsedTime) estimatedCaptureTime - captureElapsedTime else Duration.ZERO captureProgress = (estimatedCaptureTime - captureRemainingTime).toNanos().toDouble() / estimatedCaptureTime.toNanos() } val event = CameraCaptureEvent( - camera, state, request.exposureAmount, exposureCount, + this, camera, state, request.exposureAmount, exposureCount, captureRemainingTime, captureElapsedTime, captureProgress, stepRemainingTime, stepElapsedTime, stepProgress, - savePath + savedPath, liveStackedPath, + if (state == CameraCaptureState.EXPOSURE_FINISHED) request else null ) onNext(event) } + private fun addFrameToLiveStacker(path: Path?): Path? { + return liveStacker?.add(path ?: return null) + } + override fun close() { delayTask.close() waitForSettleTask.close() @@ -184,7 +241,6 @@ data class CameraCaptureTask( } override fun reset() { - state = CameraCaptureState.IDLE exposureCount = 0 captureRemainingTime = Duration.ZERO prevCaptureElapsedTime = Duration.ZERO @@ -193,7 +249,8 @@ data class CameraCaptureTask( stepRemainingTime = Duration.ZERO stepElapsedTime = Duration.ZERO stepProgress = 0.0 - savePath = null + savedPath = null + liveStackedPath = null delayTask.reset() cameraExposureTask.reset() diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraStartCaptureRequest.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraStartCaptureRequest.kt index c39ab7323..0549aa8ea 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraStartCaptureRequest.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraStartCaptureRequest.kt @@ -4,6 +4,7 @@ import jakarta.validation.Valid import jakarta.validation.constraints.Positive import jakarta.validation.constraints.PositiveOrZero import nebulosa.api.guiding.DitherAfterExposureRequest +import nebulosa.api.livestacking.LiveStackingRequest import nebulosa.indi.device.camera.FrameType import org.hibernate.validator.constraints.Range import org.hibernate.validator.constraints.time.DurationMax @@ -35,6 +36,7 @@ data class CameraStartCaptureRequest( @JvmField val autoSubFolderMode: AutoSubFolderMode = AutoSubFolderMode.OFF, @field:Valid @JvmField val dither: DitherAfterExposureRequest = DitherAfterExposureRequest.DISABLED, @JvmField val calibrationGroup: String? = null, + @JvmField val liveStacking: LiveStackingRequest = LiveStackingRequest.EMPTY, // Filter Wheel. @JvmField val filterPosition: Int = 0, @JvmField val shutterPosition: Int = 0, diff --git a/api/src/main/kotlin/nebulosa/api/image/ImageBucket.kt b/api/src/main/kotlin/nebulosa/api/image/ImageBucket.kt index 7d51daeb3..fac91805a 100644 --- a/api/src/main/kotlin/nebulosa/api/image/ImageBucket.kt +++ b/api/src/main/kotlin/nebulosa/api/image/ImageBucket.kt @@ -2,26 +2,33 @@ package nebulosa.api.image import nebulosa.fits.fits import nebulosa.image.Image +import nebulosa.log.loggerFor import nebulosa.plate.solving.PlateSolution import nebulosa.xisf.xisf import org.springframework.stereotype.Component +import java.io.Closeable import java.nio.file.Path +import java.util.* +import java.util.concurrent.ConcurrentHashMap +import kotlin.concurrent.timer import kotlin.io.path.extension @Component -class ImageBucket { +class ImageBucket : Closeable { data class OpenedImage( - @JvmField val image: Image, + @JvmField var image: Image? = null, @JvmField var solution: PlateSolution? = null, @JvmField val debayer: Boolean = true, + @JvmField var openedAt: Long = System.currentTimeMillis(), ) - private val bucket = HashMap(256) + private val bucket = ConcurrentHashMap(8) + private val timer = timer("Image Bucket Timer", true, IMAGES_MAX_TIME, IMAGES_MAX_TIME, ::deleteUnusedImages) @Synchronized - fun put(path: Path, image: Image, solution: PlateSolution? = null, debayer: Boolean = true) { - bucket[path] = OpenedImage(image, solution ?: PlateSolution.from(image.header), debayer) + fun put(path: Path, image: Image, solution: PlateSolution? = null, debayer: Boolean = true): OpenedImage { + return OpenedImage(image, solution ?: PlateSolution.from(image.header), debayer).also { bucket[path] = it } } @Synchronized @@ -32,11 +39,17 @@ class ImageBucket { } @Synchronized - fun open(path: Path, debayer: Boolean = true, solution: PlateSolution? = null, force: Boolean = false): Image { + fun open( + path: Path, debayer: Boolean = this[path]?.debayer ?: true, + solution: PlateSolution? = null, force: Boolean = false + ): OpenedImage { val openedImage = this[path] if (openedImage != null && !force && debayer == openedImage.debayer) { - return openedImage.image + if (openedImage.image != null && solution == null) { + openedImage.openedAt = System.currentTimeMillis() + return openedImage + } } val representation = when (path.extension.lowercase()) { @@ -46,8 +59,7 @@ class ImageBucket { } val image = representation.use { Image.open(it, debayer) } - put(path, image, solution, debayer) - return image + return put(path, image, solution ?: openedImage?.solution, debayer) } @Synchronized @@ -60,7 +72,7 @@ class ImageBucket { } operator fun contains(path: Path): Boolean { - return path in bucket + return bucket.containsKey(path) } operator fun contains(image: Image): Boolean { @@ -70,4 +82,29 @@ class ImageBucket { operator fun contains(solution: PlateSolution): Boolean { return bucket.any { it.value.solution === solution } } + + override fun close() { + timer.cancel() + } + + @Suppress("UNUSED_PARAMETER") + private fun deleteUnusedImages(task: TimerTask) { + val currentTime = System.currentTimeMillis() + + synchronized(this) { + for ((path, image) in bucket) { + if (currentTime - image.openedAt >= IMAGES_MAX_TIME) { + image.image = null + LOG.info("image at {} has been disposed", path) + } + } + } + } + + companion object { + + private const val IMAGES_MAX_TIME = 1000 * 60 * 5L // 5 min + + @JvmStatic private val LOG = loggerFor() + } } diff --git a/api/src/main/kotlin/nebulosa/api/image/ImageService.kt b/api/src/main/kotlin/nebulosa/api/image/ImageService.kt index ff452a3e2..d73785c67 100644 --- a/api/src/main/kotlin/nebulosa/api/image/ImageService.kt +++ b/api/src/main/kotlin/nebulosa/api/image/ImageService.kt @@ -82,8 +82,8 @@ class ImageService( path: Path, camera: Camera?, transformation: ImageTransformation, output: HttpServletResponse, ) { - val image = imageBucket.open(path, transformation.debayer, force = transformation.force) - val (transformedImage, statistics, stretchParams, instrument) = image.transform(true, transformation, ImageOperation.OPEN, camera) + val (image, calibration) = imageBucket.open(path, transformation.debayer, force = transformation.force) + val (transformedImage, statistics, stretchParams, instrument) = image!!.transform(true, transformation, ImageOperation.OPEN, camera) val info = ImageInfo( path, @@ -91,7 +91,7 @@ class ImageService( stretchParams!!.shadow, stretchParams.highlight, stretchParams.midtone, transformedImage.header.rightAscension.takeIf { it.isFinite() }, transformedImage.header.declination.takeIf { it.isFinite() }, - imageBucket[path]?.solution?.let(::ImageSolved), + calibration?.let(::ImageSolved), transformedImage.header.mapNotNull { if (it.isCommentStyle) null else ImageHeaderItem(it.key, it.value) }, transformedImage.header.bitpix, instrument, statistics, ) @@ -170,9 +170,9 @@ class ImageService( minorPlanetMagLimit: Double = 12.0, useSimbad: Boolean = false, location: Location? = null, ): List { - val (image, calibration) = imageBucket[path] ?: return emptyList() + val (image, calibration) = imageBucket.open(path) - if (calibration.isNullOrEmpty() || !calibration.solved) { + if (image == null || calibration.isNullOrEmpty() || !calibration.solved) { return emptyList() } @@ -267,8 +267,8 @@ class ImageService( return annotations } - fun saveImageAs(inputPath: Path, save: SaveImage, camera: Camera?) { - val (image) = imageBucket[inputPath]?.image?.transform(save.shouldBeTransformed, save.transformation, ImageOperation.SAVE) + fun saveImageAs(path: Path, save: SaveImage, camera: Camera?) { + val (image) = imageBucket.open(path).image?.transform(save.shouldBeTransformed, save.transformation, ImageOperation.SAVE) ?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Image not found") require(save.path != null) @@ -295,9 +295,9 @@ class ImageService( } fun coordinateInterpolation(path: Path): CoordinateInterpolation? { - val (image, calibration) = imageBucket[path] ?: return null + val (image, calibration) = imageBucket.open(path) - if (calibration.isNullOrEmpty() || !calibration.solved) { + if (image == null || calibration.isNullOrEmpty() || !calibration.solved) { return null } @@ -331,8 +331,7 @@ class ImageService( } fun histogram(path: Path, bitLength: Int = 16): IntArray { - val (image) = imageBucket[path] ?: return IntArray(0) - return image.compute(Histogram(bitLength = bitLength)) + return imageBucket.open(path).image?.compute(Histogram(bitLength = bitLength)) ?: IntArray(0) } companion object { diff --git a/api/src/main/kotlin/nebulosa/api/livestacking/LiveStackerType.kt b/api/src/main/kotlin/nebulosa/api/livestacking/LiveStackerType.kt new file mode 100644 index 000000000..5e1d6426e --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/livestacking/LiveStackerType.kt @@ -0,0 +1,5 @@ +package nebulosa.api.livestacking + +enum class LiveStackerType { + SIRIL, +} diff --git a/api/src/main/kotlin/nebulosa/api/livestacking/LiveStackingController.kt b/api/src/main/kotlin/nebulosa/api/livestacking/LiveStackingController.kt new file mode 100644 index 000000000..b953b7ee4 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/livestacking/LiveStackingController.kt @@ -0,0 +1,27 @@ +package nebulosa.api.livestacking + +import jakarta.validation.Valid +import jakarta.validation.constraints.NotBlank +import nebulosa.indi.device.camera.Camera +import org.springframework.web.bind.annotation.* +import java.nio.file.Path + +@RestController +@RequestMapping("live-stacking") +class LiveStackingController(private val liveStackingService: LiveStackingService) { + + @PutMapping("{camera}/start") + fun start(camera: Camera, @RequestBody body: LiveStackingRequest) { + liveStackingService.start(camera, body) + } + + @PutMapping("{camera}/add") + fun add(camera: Camera, @RequestParam @Valid @NotBlank path: Path): Path? { + return liveStackingService.add(camera, path) + } + + @PutMapping("{camera}/stop") + fun stop(camera: Camera) { + liveStackingService.stop(camera) + } +} diff --git a/api/src/main/kotlin/nebulosa/api/livestacking/LiveStackingRequest.kt b/api/src/main/kotlin/nebulosa/api/livestacking/LiveStackingRequest.kt new file mode 100644 index 000000000..9f26e144b --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/livestacking/LiveStackingRequest.kt @@ -0,0 +1,34 @@ +package nebulosa.api.livestacking + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize +import nebulosa.api.beans.converters.angle.DegreesDeserializer +import nebulosa.livestacking.LiveStacker +import nebulosa.siril.livestacking.SirilLiveStacker +import org.jetbrains.annotations.NotNull +import java.nio.file.Files +import java.nio.file.Path +import java.util.function.Supplier + +data class LiveStackingRequest( + @JvmField val enabled: Boolean = false, + @JvmField val type: LiveStackerType = LiveStackerType.SIRIL, + @JvmField @field:NotNull val executablePath: Path? = null, + @JvmField val dark: Path? = null, + @JvmField val flat: Path? = null, + @JvmField @field:JsonDeserialize(using = DegreesDeserializer::class) val rotate: Double = 0.0, + @JvmField val use32Bits: Boolean = false, +) : Supplier { + + override fun get(): LiveStacker { + val workingDirectory = Files.createTempDirectory("ls-") + + return when (type) { + LiveStackerType.SIRIL -> SirilLiveStacker(executablePath!!, workingDirectory, dark, flat, rotate, use32Bits) + } + } + + companion object { + + @JvmStatic val EMPTY = LiveStackingRequest() + } +} diff --git a/api/src/main/kotlin/nebulosa/api/livestacking/LiveStackingService.kt b/api/src/main/kotlin/nebulosa/api/livestacking/LiveStackingService.kt new file mode 100644 index 000000000..a749b6361 --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/livestacking/LiveStackingService.kt @@ -0,0 +1,29 @@ +package nebulosa.api.livestacking + +import nebulosa.indi.device.camera.Camera +import nebulosa.livestacking.LiveStacker +import org.springframework.stereotype.Service +import java.nio.file.Path +import java.util.concurrent.ConcurrentHashMap + +@Service +class LiveStackingService { + + private val liveStackers = ConcurrentHashMap(2) + + fun start(camera: Camera, options: LiveStackingRequest) { + stop(camera) + + val liveStacker = options.get() + liveStackers[camera] = liveStacker + liveStacker.start() + } + + fun add(camera: Camera, path: Path): Path? { + return liveStackers[camera]?.add(path) + } + + fun stop(camera: Camera) { + liveStackers.remove(camera)?.stop() + } +} diff --git a/api/src/main/kotlin/nebulosa/api/mounts/MountService.kt b/api/src/main/kotlin/nebulosa/api/mounts/MountService.kt index 08d2e882f..e60a61a74 100644 --- a/api/src/main/kotlin/nebulosa/api/mounts/MountService.kt +++ b/api/src/main/kotlin/nebulosa/api/mounts/MountService.kt @@ -224,11 +224,10 @@ class MountService( } fun pointMountHere(mount: Mount, path: Path, x: Double, y: Double) { - val calibration = imageBucket[path]?.solution ?: return + val calibration = imageBucket.open(path).solution ?: return if (calibration.isNotEmpty() && calibration.solved) { - val wcs = WCS(calibration) - val (rightAscension, declination) = wcs.use { it.pixToSky(x, y) } // J2000 + val (rightAscension, declination) = WCS(calibration).use { it.pixToSky(x, y) } // J2000 val icrf = ICRF.equatorial(calibration.rightAscension, calibration.declination) val (calibratedRA, calibratedDEC) = icrf.equatorialAtDate() diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/SequencerExecutor.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SequencerExecutor.kt index 4992cff28..48b10b2af 100644 --- a/api/src/main/kotlin/nebulosa/api/sequencer/SequencerExecutor.kt +++ b/api/src/main/kotlin/nebulosa/api/sequencer/SequencerExecutor.kt @@ -2,6 +2,7 @@ package nebulosa.api.sequencer import io.reactivex.rxjava3.functions.Consumer import nebulosa.api.beans.annotations.Subscriber +import nebulosa.api.calibration.CalibrationFrameService import nebulosa.api.cameras.CameraEventAware import nebulosa.api.focusers.FocuserEventAware import nebulosa.api.messages.MessageEvent @@ -27,6 +28,7 @@ class SequencerExecutor( private val messageService: MessageService, private val guider: Guider, private val threadPoolTaskExecutor: ThreadPoolTaskExecutor, + private val calibrationFrameService: CalibrationFrameService, ) : Consumer, CameraEventAware, WheelEventAware, FocuserEventAware { private val jobs = ConcurrentHashMap.newKeySet(1) @@ -65,7 +67,7 @@ class SequencerExecutor( check(jobs.none { it.task.focuser === focuser }) { "${camera.name} Sequencer Job is already in progress" } } - val task = SequencerTask(camera, request, guider, mount, wheel, focuser, threadPoolTaskExecutor) + val task = SequencerTask(camera, request, guider, mount, wheel, focuser, threadPoolTaskExecutor, calibrationFrameService) task.subscribe(this) with(SequencerJob(task)) { diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/SequencerTask.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SequencerTask.kt index 4efde90bf..ecd8aca07 100644 --- a/api/src/main/kotlin/nebulosa/api/sequencer/SequencerTask.kt +++ b/api/src/main/kotlin/nebulosa/api/sequencer/SequencerTask.kt @@ -1,6 +1,7 @@ package nebulosa.api.sequencer import io.reactivex.rxjava3.functions.Consumer +import nebulosa.api.calibration.CalibrationFrameProvider import nebulosa.api.cameras.* import nebulosa.api.messages.MessageEvent import nebulosa.api.tasks.AbstractTask @@ -37,6 +38,7 @@ data class SequencerTask( @JvmField val wheel: FilterWheel? = null, @JvmField val focuser: Focuser? = null, private val executor: Executor? = null, + private val calibrationFrameProvider: CalibrationFrameProvider? = null, ) : AbstractTask(), Consumer, CameraEventAware, WheelEventAware { private val usedEntries = plan.entries.filter { it.enabled } @@ -75,7 +77,10 @@ data class SequencerTask( request.wheelMoveTask()?.also(tasks::add) // CAPTURE. - val cameraCaptureTask = CameraCaptureTask(camera, request, guider, executor = executor) + val cameraCaptureTask = CameraCaptureTask( + camera, request, guider, executor = executor, + calibrationFrameProvider = calibrationFrameProvider + ) cameraCaptureTask.subscribe(this) estimatedCaptureTime += cameraCaptureTask.estimatedCaptureTime tasks.add(cameraCaptureTask) @@ -83,7 +88,8 @@ data class SequencerTask( } else { val sequenceIdTasks = usedEntries.map { req -> SequencerIdTask(plan.entries.indexOfFirst { it === req } + 1) } val requests = usedEntries.map { mapRequest(it) } - val cameraCaptureTasks = requests.mapIndexed { i, req -> CameraCaptureTask(camera, req, guider, i > 0, 1, executor) } + val cameraCaptureTasks = requests + .mapIndexed { i, req -> CameraCaptureTask(camera, req, guider, i > 0, 1, executor, calibrationFrameProvider) } val wheelMoveTasks = requests.map { it.wheelMoveTask() } val count = IntArray(requests.size) { usedEntries[it].exposureAmount } diff --git a/api/src/main/kotlin/nebulosa/api/solver/PlateSolverController.kt b/api/src/main/kotlin/nebulosa/api/solver/PlateSolverController.kt index f4936f0fa..cb828bb33 100644 --- a/api/src/main/kotlin/nebulosa/api/solver/PlateSolverController.kt +++ b/api/src/main/kotlin/nebulosa/api/solver/PlateSolverController.kt @@ -15,7 +15,7 @@ class PlateSolverController( @PutMapping fun solveImage( @RequestParam path: Path, - @RequestBody @Valid solver: PlateSolverOptions, + @RequestBody @Valid solver: PlateSolverRequest, @RequestParam(required = false, defaultValue = "true") blind: Boolean, @AngleParam(required = false, isHours = true, defaultValue = "0.0") centerRA: Angle, @AngleParam(required = false, defaultValue = "0.0") centerDEC: Angle, diff --git a/api/src/main/kotlin/nebulosa/api/solver/PlateSolverOptions.kt b/api/src/main/kotlin/nebulosa/api/solver/PlateSolverRequest.kt similarity index 95% rename from api/src/main/kotlin/nebulosa/api/solver/PlateSolverOptions.kt rename to api/src/main/kotlin/nebulosa/api/solver/PlateSolverRequest.kt index 1612c43cc..724fc06ad 100644 --- a/api/src/main/kotlin/nebulosa/api/solver/PlateSolverOptions.kt +++ b/api/src/main/kotlin/nebulosa/api/solver/PlateSolverRequest.kt @@ -12,7 +12,7 @@ import java.nio.file.Path import java.time.Duration import java.time.temporal.ChronoUnit -data class PlateSolverOptions( +data class PlateSolverRequest( @JvmField val type: PlateSolverType = PlateSolverType.ASTROMETRY_NET_ONLINE, @JvmField val executablePath: Path? = null, @JvmField val downsampleFactor: Int = 0, @@ -36,7 +36,7 @@ data class PlateSolverOptions( companion object { - @JvmStatic val EMPTY = PlateSolverOptions() + @JvmStatic val EMPTY = PlateSolverRequest() @JvmStatic private val NOVA_ASTROMETRY_NET_CACHE = HashMap() } } diff --git a/api/src/main/kotlin/nebulosa/api/solver/PlateSolverService.kt b/api/src/main/kotlin/nebulosa/api/solver/PlateSolverService.kt index 61f0e0790..9564562b9 100644 --- a/api/src/main/kotlin/nebulosa/api/solver/PlateSolverService.kt +++ b/api/src/main/kotlin/nebulosa/api/solver/PlateSolverService.kt @@ -14,7 +14,7 @@ class PlateSolverService( ) { fun solveImage( - options: PlateSolverOptions, path: Path, + options: PlateSolverRequest, path: Path, centerRA: Angle, centerDEC: Angle, radius: Angle, ): ImageSolved { val calibration = solve(options, path, centerRA, centerDEC, radius) @@ -24,7 +24,7 @@ class PlateSolverService( @Synchronized fun solve( - options: PlateSolverOptions, path: Path, + options: PlateSolverRequest, path: Path, centerRA: Angle = 0.0, centerDEC: Angle = 0.0, radius: Angle = 0.0, ) = options.get(httpClient) .solve(path, null, centerRA, centerDEC, radius, 1, options.timeout.takeIf { it.toSeconds() > 0 }) diff --git a/api/src/main/kotlin/nebulosa/api/stardetection/StarDetectionController.kt b/api/src/main/kotlin/nebulosa/api/stardetection/StarDetectionController.kt index 810c13811..07868c583 100644 --- a/api/src/main/kotlin/nebulosa/api/stardetection/StarDetectionController.kt +++ b/api/src/main/kotlin/nebulosa/api/stardetection/StarDetectionController.kt @@ -13,6 +13,6 @@ class StarDetectionController(private val starDetectionService: StarDetectionSer @PutMapping fun detectStars( @RequestParam path: Path, - @RequestBody @Valid body: StarDetectionOptions + @RequestBody @Valid body: StarDetectionRequest ) = starDetectionService.detectStars(path, body) } diff --git a/api/src/main/kotlin/nebulosa/api/stardetection/StarDetectionOptions.kt b/api/src/main/kotlin/nebulosa/api/stardetection/StarDetectionRequest.kt similarity index 87% rename from api/src/main/kotlin/nebulosa/api/stardetection/StarDetectionOptions.kt rename to api/src/main/kotlin/nebulosa/api/stardetection/StarDetectionRequest.kt index 9d3711755..dcdd0ab97 100644 --- a/api/src/main/kotlin/nebulosa/api/stardetection/StarDetectionOptions.kt +++ b/api/src/main/kotlin/nebulosa/api/stardetection/StarDetectionRequest.kt @@ -6,7 +6,7 @@ import java.nio.file.Path import java.time.Duration import java.util.function.Supplier -data class StarDetectionOptions( +data class StarDetectionRequest( @JvmField val type: StarDetectorType = StarDetectorType.ASTAP, @JvmField val executablePath: Path? = null, @JvmField val timeout: Duration = Duration.ZERO, @@ -19,6 +19,6 @@ data class StarDetectionOptions( companion object { - @JvmStatic val EMPTY = StarDetectionOptions() + @JvmStatic val EMPTY = StarDetectionRequest() } } diff --git a/api/src/main/kotlin/nebulosa/api/stardetection/StarDetectionService.kt b/api/src/main/kotlin/nebulosa/api/stardetection/StarDetectionService.kt index 243239456..b1ee07583 100644 --- a/api/src/main/kotlin/nebulosa/api/stardetection/StarDetectionService.kt +++ b/api/src/main/kotlin/nebulosa/api/stardetection/StarDetectionService.kt @@ -7,7 +7,7 @@ import java.nio.file.Path @Service class StarDetectionService { - fun detectStars(path: Path, options: StarDetectionOptions): List { + fun detectStars(path: Path, options: StarDetectionRequest): List { val starDetector = options.get() return starDetector.detect(path) } diff --git a/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardTask.kt b/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardTask.kt index 8a0a0ec3d..4dc26907e 100644 --- a/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardTask.kt +++ b/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardTask.kt @@ -65,7 +65,7 @@ data class FlatWizardTask( capture = event if (event.state == CameraCaptureState.EXPOSURE_FINISHED) { - savedPath = event.savePath!! + savedPath = event.savedPath!! onNext(event) } diff --git a/api/src/test/kotlin/APITest.kt b/api/src/test/kotlin/APITest.kt index a71b9c688..21e0d8fee 100644 --- a/api/src/test/kotlin/APITest.kt +++ b/api/src/test/kotlin/APITest.kt @@ -9,7 +9,7 @@ import nebulosa.api.autofocus.AutoFocusRequest import nebulosa.api.beans.converters.time.DurationSerializer import nebulosa.api.cameras.CameraStartCaptureRequest import nebulosa.api.connection.ConnectionType -import nebulosa.api.stardetection.StarDetectionOptions +import nebulosa.api.stardetection.StarDetectionRequest import nebulosa.common.json.PathSerializer import nebulosa.test.NonGitHubOnlyCondition import okhttp3.MediaType.Companion.toMediaType @@ -169,7 +169,7 @@ class APITest : StringSpec() { @JvmStatic private val EXPOSURE_TIME = Duration.ofSeconds(5) @JvmStatic private val CAPTURES_PATH = Path.of("/home/tiagohm/Git/nebulosa/data/captures") - @JvmStatic private val STAR_DETECTION_OPTIONS = StarDetectionOptions(executablePath = Path.of("astap")) + @JvmStatic private val STAR_DETECTION_OPTIONS = StarDetectionRequest(executablePath = Path.of("astap")) @JvmStatic private val CAMERA_START_CAPTURE_REQUEST = CameraStartCaptureRequest( exposureTime = EXPOSURE_TIME, width = 1280, height = 1024, frameFormat = "INDI_MONO", diff --git a/api/src/test/kotlin/SirilLiveStackerTest.kt b/api/src/test/kotlin/SirilLiveStackerTest.kt new file mode 100644 index 000000000..41220e34e --- /dev/null +++ b/api/src/test/kotlin/SirilLiveStackerTest.kt @@ -0,0 +1,28 @@ +import io.kotest.core.annotation.EnabledIf +import io.kotest.core.spec.style.StringSpec +import nebulosa.siril.livestacking.SirilLiveStacker +import nebulosa.test.NonGitHubOnlyCondition +import java.nio.file.Path +import kotlin.io.path.listDirectoryEntries + +@EnabledIf(NonGitHubOnlyCondition::class) +class SirilLiveStackerTest : StringSpec() { + + init { + "live stacking" { + val executablePath = Path.of("siril-cli") + val workingDirectory = Path.of("/home/tiagohm/Git/nebulosa/data/siril") + + val siril = SirilLiveStacker(executablePath, workingDirectory) + siril.start() + + val fitsDir = Path.of("/home/tiagohm/Imagens/Astrophotos/Light/C2023_A3/2024-05-29") + + for (fits in fitsDir.listDirectoryEntries().drop(140).sorted()) { + siril.add(fits) + } + + siril.stop() + } + } +} diff --git a/data/.gitignore b/data/.gitignore index b39285bd2..d48badef8 100644 --- a/data/.gitignore +++ b/data/.gitignore @@ -2,3 +2,4 @@ simbad/ astrobin/ test/ captures/ +siril/ diff --git a/desktop/package.json b/desktop/package.json index 0466564d7..4bd615f44 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -14,7 +14,7 @@ "postinstall": "electron-builder install-app-deps", "ng": "ng", "start": "npm-run-all -p electron:serve ng:serve", - "ng:serve": "ng serve -c web", + "ng:serve": "ng serve -c web --hmr", "build": "npm run electron:serve-tsc && ng build --base-href ./", "build:dev": "npm run build -- -c dev", "build:prod": "npm run build -- -c production", diff --git a/desktop/settings.png b/desktop/settings.png index ee6099d48..80e5fde64 100644 Binary files a/desktop/settings.png and b/desktop/settings.png differ diff --git a/desktop/src/app/alignment/alignment.component.ts b/desktop/src/app/alignment/alignment.component.ts index ad4ceb05f..e0808fd88 100644 --- a/desktop/src/app/alignment/alignment.component.ts +++ b/desktop/src/app/alignment/alignment.component.ts @@ -224,11 +224,11 @@ export class AlignmentComponent implements AfterViewInit, OnDestroy, Pingable { }) this.loadPreference() - - pinger.register(this, 30000) } async ngAfterViewInit() { + this.pinger.register(this, 30000) + this.cameras = (await this.api.cameras()).sort(deviceComparator) this.mounts = (await this.api.mounts()).sort(deviceComparator) this.guideOutputs = (await this.api.guideOutputs()).sort(deviceComparator) @@ -294,7 +294,7 @@ export class AlignmentComponent implements AfterViewInit, OnDestroy, Pingable { } plateSolverChanged() { - this.tppaRequest.plateSolver = this.preference.plateSolverOptions(this.tppaRequest.plateSolver.type).get() + this.tppaRequest.plateSolver = this.preference.plateSolverRequest(this.tppaRequest.plateSolver.type).get() this.savePreference() } diff --git a/desktop/src/app/app.component.html b/desktop/src/app/app.component.html index d6c47560e..37f7d7c2c 100644 --- a/desktop/src/app/app.component.html +++ b/desktop/src/app/app.component.html @@ -6,9 +6,16 @@ + @if (e.toggleable) { +
+ {{ e.label }} + +
+ } @else { + }
diff --git a/desktop/src/app/app.module.ts b/desktop/src/app/app.module.ts index dd8e85990..0826d8f46 100644 --- a/desktop/src/app/app.module.ts +++ b/desktop/src/app/app.module.ts @@ -84,6 +84,7 @@ import { MountComponent } from './mount/mount.component' import { RotatorComponent } from './rotator/rotator.component' import { SequencerComponent } from './sequencer/sequencer.component' import { SettingsComponent } from './settings/settings.component' +import { PathChooserComponent } from '../shared/components/path-chooser/path-chooser.component' @NgModule({ declarations: [ @@ -121,6 +122,7 @@ import { SettingsComponent } from './settings/settings.component' MoonComponent, MountComponent, NoDropdownDirective, + PathChooserComponent, RotatorComponent, SequencerComponent, SettingsComponent, diff --git a/desktop/src/app/autofocus/autofocus.component.ts b/desktop/src/app/autofocus/autofocus.component.ts index cec4ab6ab..b3aa3b750 100644 --- a/desktop/src/app/autofocus/autofocus.component.ts +++ b/desktop/src/app/autofocus/autofocus.component.ts @@ -302,11 +302,11 @@ export class AutoFocusComponent implements AfterViewInit, OnDestroy, Pingable { }) this.loadPreference() - - pinger.register(this, 30000) } async ngAfterViewInit() { + this.pinger.register(this, 30000) + this.cameras = (await this.api.cameras()).sort(deviceComparator) this.focusers = (await this.api.focusers()).sort(deviceComparator) } @@ -355,7 +355,7 @@ export class AutoFocusComponent implements AfterViewInit, OnDestroy, Pingable { this.clearChart() this.stepSizeForScale = this.request.stepSize - this.request.starDetector = this.preference.starDetectionOptions('ASTAP').get() + this.request.starDetector = this.preference.starDetectionRequest('ASTAP').get() return this.api.autoFocusStart(this.camera, this.focuser, this.request) } diff --git a/desktop/src/app/camera/camera.component.html b/desktop/src/app/camera/camera.component.html index c082be6b0..793d9b3b2 100644 --- a/desktop/src/app/camera/camera.component.html +++ b/desktop/src/app/camera/camera.component.html @@ -221,28 +221,66 @@
Enabled - +
RA only -
-
- + [(ngModel)]="request.dither.afterExposures" [step]="1" (ngModelChange)="savePreference()" scrollableNumber />
+ + + +
+
+ Enabled + +
+
+ + + + +
+
+ 32-bits (slower) + +
+
+ + + + +
+
+ +
+
+ +
+
\ No newline at end of file diff --git a/desktop/src/app/camera/camera.component.ts b/desktop/src/app/camera/camera.component.ts index 2d883b3e4..4dd5aa290 100644 --- a/desktop/src/app/camera/camera.component.ts +++ b/desktop/src/app/camera/camera.component.ts @@ -75,43 +75,57 @@ export class CameraComponent implements AfterContentInit, OnDestroy, Pingable { } showDitherDialog = false + showLiveStackingDialog = false calibrationModel: MenuItem[] = [] - readonly cameraModel: MenuItem[] = [ - { - icon: 'icomoon random-dither', - label: 'Dither', - command: () => { - this.showDitherDialog = true - }, + private readonly ditherMenuItem: MenuItem = { + icon: 'icomoon random-dither', + label: 'Dither', + command: () => { + this.showDitherDialog = true }, - { - icon: 'mdi mdi-connection', - label: 'Snoop Devices', - subMenu: [ - { - icon: 'mdi mdi-telescope', - label: 'Mount', - subMenu: [], - }, - { - icon: 'mdi mdi-palette', - label: 'Filter Wheel', - subMenu: [], - }, - { - icon: 'mdi mdi-image-filter-center-focus', - label: 'Focuser', - subMenu: [], - }, - { - icon: 'mdi mdi-rotate-right', - label: 'Rotator', - subMenu: [], - }, - ] + } + + private readonly liveStackingMenuItem: MenuItem = { + icon: 'mdi mdi-image-multiple', + label: 'Live Stacking', + command: () => { + this.showLiveStackingDialog = true }, + } + + private readonly snoopDevicesMenuItem: MenuItem = { + icon: 'mdi mdi-connection', + label: 'Snoop Devices', + subMenu: [ + { + icon: 'mdi mdi-telescope', + label: 'Mount', + subMenu: [], + }, + { + icon: 'mdi mdi-palette', + label: 'Filter Wheel', + subMenu: [], + }, + { + icon: 'mdi mdi-image-filter-center-focus', + label: 'Focuser', + subMenu: [], + }, + { + icon: 'mdi mdi-rotate-right', + label: 'Rotator', + subMenu: [], + }, + ] + } + + readonly cameraModel: MenuItem[] = [ + this.ditherMenuItem, + this.liveStackingMenuItem, + this.snoopDevicesMenuItem, ] hasDewHeater = false @@ -246,27 +260,27 @@ export class CameraComponent implements AfterContentInit, OnDestroy, Pingable { } }) - this.cameraModel[1].visible = !app.modal - - pinger.register(this, 30000) + this.snoopDevicesMenuItem.visible = !app.modal } ngAfterContentInit() { - this.route.queryParams.subscribe(e => { + this.route.queryParams.subscribe(async e => { const decodedData = JSON.parse(decodeURIComponent(e.data)) if (this.app.modal) { - this.loadCameraStartCaptureForDialogMode(decodedData) + await this.loadCameraStartCaptureForDialogMode(decodedData) } else { - this.cameraChanged(decodedData) + await this.cameraChanged(decodedData) } - }) - if (!this.app.modal) { - this.loadEquipment() - } + this.pinger.register(this, 30000) - this.loadCalibrationGroups() + if (!this.app.modal) { + await this.loadEquipment() + } + + await this.loadCalibrationGroups() + }) } @HostListener('window:unload') @@ -355,10 +369,10 @@ export class CameraComponent implements AfterContentInit, OnDestroy, Pingable { return makeItem(this.equipment.mount?.name === mount?.name, () => this.equipment.mount = mount, mount) } - this.cameraModel[1].subMenu![0].subMenu!.push(makeMountItem()) + this.snoopDevicesMenuItem.subMenu![0].subMenu!.push(makeMountItem()) for (const mount of mounts) { - this.cameraModel[1].subMenu![0].subMenu!.push(makeMountItem(mount)) + this.snoopDevicesMenuItem.subMenu![0].subMenu!.push(makeMountItem(mount)) } // FILTER WHEEL @@ -370,10 +384,10 @@ export class CameraComponent implements AfterContentInit, OnDestroy, Pingable { return makeItem(this.equipment.wheel?.name === wheel?.name, () => this.equipment.wheel = wheel, wheel) } - this.cameraModel[1].subMenu![1].subMenu!.push(makeWheelItem()) + this.snoopDevicesMenuItem.subMenu![1].subMenu!.push(makeWheelItem()) for (const wheel of wheels) { - this.cameraModel[1].subMenu![1].subMenu!.push(makeWheelItem(wheel)) + this.snoopDevicesMenuItem.subMenu![1].subMenu!.push(makeWheelItem(wheel)) } // FOCUSER @@ -385,10 +399,10 @@ export class CameraComponent implements AfterContentInit, OnDestroy, Pingable { return makeItem(this.equipment.focuser?.name === focuser?.name, () => this.equipment.focuser = focuser, focuser) } - this.cameraModel[1].subMenu![2].subMenu!.push(makeFocuserItem()) + this.snoopDevicesMenuItem.subMenu![2].subMenu!.push(makeFocuserItem()) for (const focuser of focusers) { - this.cameraModel[1].subMenu![2].subMenu!.push(makeFocuserItem(focuser)) + this.snoopDevicesMenuItem.subMenu![2].subMenu!.push(makeFocuserItem(focuser)) } // ROTATOR @@ -400,10 +414,10 @@ export class CameraComponent implements AfterContentInit, OnDestroy, Pingable { return makeItem(this.equipment.rotator?.name === rotator?.name, () => this.equipment.rotator = rotator, rotator) } - this.cameraModel[1].subMenu![3].subMenu!.push(makeRotatorItem()) + this.snoopDevicesMenuItem.subMenu![3].subMenu!.push(makeRotatorItem()) for (const rotator of rotators) { - this.cameraModel[1].subMenu![3].subMenu!.push(makeRotatorItem(rotator)) + this.snoopDevicesMenuItem.subMenu![3].subMenu!.push(makeRotatorItem(rotator)) } buildStartTooltip() @@ -518,6 +532,8 @@ export class CameraComponent implements AfterContentInit, OnDestroy, Pingable { const exposureAmount = this.exposureMode === 'LOOP' ? 0 : (this.exposureMode === 'FIXED' ? this.request.exposureAmount : 1) const savePath = this.mode !== 'CAPTURE' ? this.request.savePath : this.savePath + this.request.liveStacking.executablePath = this.preference.liveStackingRequest(this.request.liveStacking.type).get().executablePath + return { ...this.request, x, y, width, height, @@ -631,10 +647,13 @@ export class CameraComponent implements AfterContentInit, OnDestroy, Pingable { 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 + if (preference.dither) { + Object.assign(this.request.dither, preference.dither) + } + + if (preference.liveStacking) { + Object.assign(this.request.liveStacking, preference.liveStacking) + } Object.assign(this.equipment, this.preference.equipmentForDevice(this.camera).get()) } diff --git a/desktop/src/app/filterwheel/filterwheel.component.ts b/desktop/src/app/filterwheel/filterwheel.component.ts index 18ee4ab06..9cbdf65e0 100644 --- a/desktop/src/app/filterwheel/filterwheel.component.ts +++ b/desktop/src/app/filterwheel/filterwheel.component.ts @@ -134,22 +134,22 @@ export class FilterWheelComponent implements AfterContentInit, OnDestroy, Pingab hotkeys('7', event => { event.preventDefault(); this.moveToPosition(7) }) hotkeys('8', event => { event.preventDefault(); this.moveToPosition(8) }) hotkeys('9', event => { event.preventDefault(); this.moveToPosition(9) }) - - pinger.register(this, 30000) } async ngAfterContentInit() { - this.route.queryParams.subscribe(e => { + this.route.queryParams.subscribe(async e => { const decodedData = JSON.parse(decodeURIComponent(e.data)) if (this.app.modal) { const request = decodedData as WheelDialogInput Object.assign(this.request, request.request) this.mode = request.mode - this.wheelChanged(request.wheel) + await this.wheelChanged(request.wheel) } else { - this.wheelChanged(decodedData) + await this.wheelChanged(decodedData) } + + this.pinger.register(this, 30000) }) this.focusers = await this.api.focusers() diff --git a/desktop/src/app/flat-wizard/flat-wizard.component.ts b/desktop/src/app/flat-wizard/flat-wizard.component.ts index 41a887d19..fd02d6764 100644 --- a/desktop/src/app/flat-wizard/flat-wizard.component.ts +++ b/desktop/src/app/flat-wizard/flat-wizard.component.ts @@ -143,11 +143,11 @@ export class FlatWizardComponent implements AfterViewInit, OnDestroy, Pingable { } }) }) - - pinger.register(this, 30000) } async ngAfterViewInit() { + this.pinger.register(this, 30000) + this.cameras = (await this.api.cameras()).sort(deviceComparator) this.wheels = (await this.api.wheels()).sort(deviceComparator) } diff --git a/desktop/src/app/focuser/focuser.component.ts b/desktop/src/app/focuser/focuser.component.ts index 1c1221264..c069d991f 100644 --- a/desktop/src/app/focuser/focuser.component.ts +++ b/desktop/src/app/focuser/focuser.component.ts @@ -61,14 +61,13 @@ export class FocuserComponent implements AfterViewInit, OnDestroy, Pingable { hotkeys('down', event => { event.preventDefault(); this.stepsRelative = Math.max(0, this.stepsRelative - 1) }) hotkeys('ctrl+up', event => { event.preventDefault(); this.stepsAbsolute = Math.max(0, this.stepsAbsolute - 1) }) hotkeys('ctrl+down', event => { event.preventDefault(); this.stepsAbsolute = Math.min(this.focuser.maxPosition, this.stepsAbsolute + 1) }) - - pinger.register(this, 30000) } async ngAfterViewInit() { - this.route.queryParams.subscribe(e => { + this.route.queryParams.subscribe(async e => { const focuser = JSON.parse(decodeURIComponent(e.data)) as Focuser - this.focuserChanged(focuser) + await this.focuserChanged(focuser) + this.pinger.register(this, 30000) }) } diff --git a/desktop/src/app/guider/guider.component.ts b/desktop/src/app/guider/guider.component.ts index ac16de440..8b05c6572 100644 --- a/desktop/src/app/guider/guider.component.ts +++ b/desktop/src/app/guider/guider.component.ts @@ -285,11 +285,11 @@ export class GuiderComponent implements AfterViewInit, OnDestroy, Pingable { this.message = event.data }) }) - - pinger.register(this, 30000) } async ngAfterViewInit() { + this.pinger.register(this, 30000) + const settle = await this.api.getGuidingSettle() this.settleAmount = settle.amount ?? 1.5 diff --git a/desktop/src/app/image/image.component.ts b/desktop/src/app/image/image.component.ts index 4a8dbeaff..5a2a2f04f 100644 --- a/desktop/src/app/image/image.component.ts +++ b/desktop/src/app/image/image.component.ts @@ -51,6 +51,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { imageInfo?: ImageInfo private imageURL!: string imageData: ImageData = {} + showLiveStackedImage?: boolean readonly scnrChannels: { name: string, value?: ImageChannel }[] = [ { name: 'None', value: undefined }, @@ -303,7 +304,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { disabled: true, command: () => { this.executeMount(mount => { - this.api.pointMountHere(mount, this.imageData.path!, this.imageMouseX, this.imageMouseY) + this.api.pointMountHere(mount, this.imagePath!, this.imageMouseX, this.imageMouseY) }) }, } @@ -453,6 +454,10 @@ export class ImageComponent implements AfterViewInit, OnDestroy { && !this.transformation.mirrorVertical } + get imagePath() { + return (this.showLiveStackedImage && this.imageData.liveStackedPath) || this.imageData.path + } + constructor( private app: AppComponent, private route: ActivatedRoute, @@ -465,6 +470,18 @@ export class ImageComponent implements AfterViewInit, OnDestroy { ) { app.title = 'Image' + app.topMenu.push({ + label: 'Live Stacking', + toggleable: true, + visible: false, + toggle: (event) => { + if (event.originalEvent) { + this.showLiveStackedImage = !!event.checked + this.loadImage(true) + } + }, + }) + app.topMenu.push({ icon: 'mdi mdi-fullscreen', label: 'Fullscreen', @@ -509,19 +526,30 @@ export class ImageComponent implements AfterViewInit, OnDestroy { electron.on('CAMERA.CAPTURE_ELAPSED', async (event) => { if (event.state === 'EXPOSURE_FINISHED' && event.camera.id === this.imageData.camera?.id) { - await this.closeImage(true) - ngZone.run(() => { - this.imageData.path = event.savePath + if (this.showLiveStackedImage === undefined) { + if (event.liveStackedPath) { + this.showLiveStackedImage = true + this.app.topMenu[0].toggled = true + this.app.topMenu[0].visible = true + } + } else if (!event.liveStackedPath) { + this.showLiveStackedImage = undefined + this.app.topMenu[0].toggled = false + this.app.topMenu[0].visible = false + } + + this.imageData.path = event.savedPath + this.imageData.liveStackedPath = event.liveStackedPath + this.imageData.capture = event.capture + this.clearOverlay() - this.loadImage() + this.loadImage(true) }) } }) electron.on('DATA.CHANGED', async (event: ImageData) => { - await this.closeImage(event.path !== this.imageData.path) - ngZone.run(() => { this.loadImageFromData(event) }) @@ -634,10 +662,11 @@ export class ImageComponent implements AfterViewInit, OnDestroy { } private async closeImage(force: boolean = false) { - if (this.imageData.path) { - if (force) { - await this.api.closeImage(this.imageData.path) - } + if (this.imageData.path && force) { + await this.api.closeImage(this.imageData.path) + } + if (this.imageData.liveStackedPath && force) { + await this.api.closeImage(this.imageData.liveStackedPath) } } @@ -720,7 +749,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { } this.clearOverlay() - this.loadImage() + this.loadImage(true) } private clearOverlay() { @@ -738,7 +767,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { } private async computeHistogram() { - const data = await this.api.imageHistogram(this.imageData.path!, this.statisticsBitLength.bitLength) + const data = await this.api.imageHistogram(this.imagePath!, this.statisticsBitLength.bitLength) this.histogram.update(data) } @@ -747,9 +776,9 @@ export class ImageComponent implements AfterViewInit, OnDestroy { } async detectStars() { - const options = this.preference.starDetectionOptions(this.starDetection.type).get() + const options = this.preference.starDetectionRequest(this.starDetection.type).get() options.minSNR = this.starDetection.minSNR - this.starDetection.stars = await this.api.detectStars(this.imageData.path!, options) + this.starDetection.stars = await this.api.detectStars(this.imagePath!, options) let hfd = 0 let snr = 0 @@ -784,9 +813,13 @@ export class ImageComponent implements AfterViewInit, OnDestroy { ctx?.drawImage(this.image.nativeElement, star.x - 8, star.y - 8, 16, 16, 0, 0, canvas.width, canvas.height) } - private async loadImage() { - if (this.imageData.path) { - await this.loadImageFromPath(this.imageData.path) + private async loadImage(force: boolean = false) { + await this.closeImage(force) + + const path = this.imagePath + + if (path) { + await this.loadImageFromPath(path) } if (this.imageData.title) { @@ -861,14 +894,14 @@ export class ImageComponent implements AfterViewInit, OnDestroy { } async saveImageAs() { - await this.api.saveImageAs(this.imageData!.path!, this.saveAs, this.imageData.camera) + await this.api.saveImageAs(this.imagePath!, this.saveAs, this.imageData.camera) this.saveAs.showDialog = false } async annotateImage() { try { this.annotating = true - this.annotations = await this.api.annotationsOfImage(this.imageData.path!, this.annotation.useStarsAndDSOs, + this.annotations = await this.api.annotationsOfImage(this.imagePath!, this.annotation.useStarsAndDSOs, this.annotation.useMinorPlanets, this.annotation.minorPlanetsMagLimit, this.annotation.useSimbad) this.annotationIsVisible = true this.annotationMenuItem.toggleable = this.annotations.length > 0 @@ -983,7 +1016,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { } private async retrieveCoordinateInterpolation() { - const coordinate = await this.api.coordinateInterpolation(this.imageData.path!) + const coordinate = await this.api.coordinateInterpolation(this.imagePath!) if (coordinate) { const { ma, md, x0, y0, x1, y1, delta } = coordinate @@ -1001,8 +1034,8 @@ export class ImageComponent implements AfterViewInit, OnDestroy { this.solver.solving = true try { - const solver = this.preference.plateSolverOptions(this.solver.type).get() - const solved = await this.api.solveImage(solver, this.imageData.path!, this.solver.blind, + const solver = this.preference.plateSolverRequest(this.solver.type).get() + const solved = await this.api.solveImage(solver, this.imagePath!, this.solver.blind, this.solver.centerRA, this.solver.centerDEC, this.solver.radius) this.savePreference() @@ -1189,7 +1222,7 @@ export class ImageComponent implements AfterViewInit, OnDestroy { this.solver.radius = preference.solverRadius ?? this.solver.radius this.solver.type = preference.solverType ?? this.solver.types[0] this.starDetection.type = preference.starDetectionType ?? this.starDetection.type - this.starDetection.minSNR = this.preference.starDetectionOptions(this.starDetection.type).get().minSNR ?? this.starDetection.type + this.starDetection.minSNR = this.preference.starDetectionRequest(this.starDetection.type).get().minSNR ?? this.starDetection.type this.fov.fovs = this.preference.imageFOVs.get() this.fov.fovs.forEach(e => { e.enabled = false; e.computed = undefined }) diff --git a/desktop/src/app/mount/mount.component.ts b/desktop/src/app/mount/mount.component.ts index 604dd02de..fe4fbad8d 100644 --- a/desktop/src/app/mount/mount.component.ts +++ b/desktop/src/app/mount/mount.component.ts @@ -279,14 +279,13 @@ export class MountComponent implements AfterContentInit, OnDestroy, Pingable { hotkeys('e', { keyup: true }, event => { event.preventDefault(); this.moveTo('NE', event.type === 'keydown') }) hotkeys('z', { keyup: true }, event => { event.preventDefault(); this.moveTo('SW', event.type === 'keydown') }) hotkeys('c', { keyup: true }, event => { event.preventDefault(); this.moveTo('SE', event.type === 'keydown') }) - - this.pinger.register(this, 30000) } async ngAfterContentInit() { - this.route.queryParams.subscribe(e => { + this.route.queryParams.subscribe(async e => { const mount = JSON.parse(decodeURIComponent(e.data)) as Mount - this.mountChanged(mount) + await this.mountChanged(mount) + this.pinger.register(this, 30000) }) } diff --git a/desktop/src/app/rotator/rotator.component.ts b/desktop/src/app/rotator/rotator.component.ts index d9a8215dc..6886e7044 100644 --- a/desktop/src/app/rotator/rotator.component.ts +++ b/desktop/src/app/rotator/rotator.component.ts @@ -47,14 +47,13 @@ export class RotatorComponent implements AfterViewInit, OnDestroy, Pingable { }) } }) - - pinger.register(this, 30000) } async ngAfterViewInit() { - this.route.queryParams.subscribe(e => { + this.route.queryParams.subscribe(async e => { const rotator = JSON.parse(decodeURIComponent(e.data)) as Rotator - this.rotatorChanged(rotator) + await this.rotatorChanged(rotator) + this.pinger.register(this, 30000) }) } diff --git a/desktop/src/app/sequencer/sequencer.component.ts b/desktop/src/app/sequencer/sequencer.component.ts index bde94f520..c17bd92d8 100644 --- a/desktop/src/app/sequencer/sequencer.component.ts +++ b/desktop/src/app/sequencer/sequencer.component.ts @@ -237,11 +237,11 @@ export class SequencerComponent implements AfterContentInit, OnDestroy, Pingable for (const p of SEQUENCE_ENTRY_PROPERTIES) { this.availableEntryPropertiesToApply.set(p, true) } - - pinger.register(this, 30000) } async ngAfterContentInit() { + this.pinger.register(this, 30000) + this.cameras = (await this.api.cameras()).sort(deviceComparator) this.mounts = (await this.api.mounts()).sort(deviceComparator) this.wheels = (await this.api.wheels()).sort(deviceComparator) @@ -290,6 +290,19 @@ export class SequencerComponent implements AfterContentInit, OnDestroy, Pingable frameFormat: camera?.frameFormats[0], autoSave: true, autoSubFolderMode: 'OFF', + dither: { + enabled: false, + amount: 0, + raOnly: false, + afterExposures: 0 + }, + liveStacking: { + enabled: false, + type: 'SIRIL', + executablePath: '', + rotate: 0, + use32Bits: false + }, }) this.savePlan() diff --git a/desktop/src/app/settings/settings.component.html b/desktop/src/app/settings/settings.component.html index e2cdc4107..e7f18020c 100644 --- a/desktop/src/app/settings/settings.component.html +++ b/desktop/src/app/settings/settings.component.html @@ -1,90 +1,126 @@ -
- - -
- +
+
+ + + + +
+
+
+
+
- - - + + +
- - -
- - - +
+
+
+
+
+ + + - +
+
+ - - +
+ + - - - - - - - - - +
+
+ +
+
+ + - - +
+
+ +
- - -
- +
+
+
+
+
+ - + - +
+
+ - +
+
+ - - - - - - +
+
+ +
+
+
+
+
+
+ + +
- - +
+ +
+
+
\ No newline at end of file diff --git a/desktop/src/app/settings/settings.component.ts b/desktop/src/app/settings/settings.component.ts index 2d8931004..dfd22b51c 100644 --- a/desktop/src/app/settings/settings.component.ts +++ b/desktop/src/app/settings/settings.component.ts @@ -1,11 +1,10 @@ import { AfterViewInit, Component, HostListener, OnDestroy } from '@angular/core' -import path from 'path' import { LocationDialog } from '../../shared/dialogs/location/location.dialog' -import { ApiService } from '../../shared/services/api.service' import { ElectronService } from '../../shared/services/electron.service' import { PreferenceService } from '../../shared/services/preference.service' import { PrimeService } from '../../shared/services/prime.service' import { EMPTY_LOCATION, Location } from '../../shared/types/atlas.types' +import { LiveStackerType, LiveStackingRequest } from '../../shared/types/camera.types' import { PlateSolverOptions, PlateSolverType, StarDetectionOptions, StarDetectorType } from '../../shared/types/settings.types' import { AppComponent } from '../app.component' @@ -16,18 +15,40 @@ import { AppComponent } from '../app.component' }) export class SettingsComponent implements AfterViewInit, OnDestroy { + tab = 0 + readonly tabs: { id: number, name: string }[] = [ + { + id: 0, + name: 'Location' + }, + { + id: 1, + name: 'Plate Solver' + }, + { + id: 2, + name: 'Star Detection' + }, + { + id: 3, + name: 'Live Stacking' + }, + ] + readonly locations: Location[] location: Location - solverType: PlateSolverType = 'ASTAP' - readonly solvers = new Map() + plateSolverType: PlateSolverType = 'ASTAP' + readonly plateSolvers = new Map() starDetectorType: StarDetectorType = 'ASTAP' readonly starDetectors = new Map() + liveStackerType: LiveStackerType = 'SIRIL' + readonly liveStackers = new Map() + constructor( app: AppComponent, - private api: ApiService, private preference: PreferenceService, private electron: ElectronService, private prime: PrimeService, @@ -37,10 +58,12 @@ export class SettingsComponent implements AfterViewInit, OnDestroy { this.locations = preference.locations.get() this.location = preference.selectedLocation.get(this.locations[0]) - this.solvers.set('ASTAP', preference.plateSolverOptions('ASTAP').get()) - this.solvers.set('ASTROMETRY_NET_ONLINE', preference.plateSolverOptions('ASTROMETRY_NET_ONLINE').get()) + this.plateSolvers.set('ASTAP', preference.plateSolverRequest('ASTAP').get()) + this.plateSolvers.set('ASTROMETRY_NET_ONLINE', preference.plateSolverRequest('ASTROMETRY_NET_ONLINE').get()) - this.starDetectors.set('ASTAP', preference.starDetectionOptions('ASTAP').get()) + this.starDetectors.set('ASTAP', preference.starDetectionRequest('ASTAP').get()) + + this.liveStackers.set('SIRIL', preference.liveStackingRequest('SIRIL').get()) } async ngAfterViewInit() { } @@ -102,31 +125,12 @@ export class SettingsComponent implements AfterViewInit, OnDestroy { this.electron.send('LOCATION.CHANGED', this.location) } - async chooseExecutablePathForPlateSolver() { - const options = this.solvers.get(this.solverType)! - this.chooseExecutablePath(options) - } - - async chooseExecutablePathForStarDetection() { - const options = this.solvers.get(this.starDetectorType)! - this.chooseExecutablePath(options) - } - - private async chooseExecutablePath(options: { executablePath: string }) { - const executablePath = await this.electron.openFile({ defaultPath: path.dirname(options.executablePath) }) - - if (executablePath) { - options.executablePath = executablePath - this.save() - } - - return executablePath - } - save() { - this.preference.plateSolverOptions('ASTAP').set(this.solvers.get('ASTAP')!) - this.preference.plateSolverOptions('ASTROMETRY_NET_ONLINE').set(this.solvers.get('ASTROMETRY_NET_ONLINE')!) + this.preference.plateSolverRequest('ASTAP').set(this.plateSolvers.get('ASTAP')) + this.preference.plateSolverRequest('ASTROMETRY_NET_ONLINE').set(this.plateSolvers.get('ASTROMETRY_NET_ONLINE')) + + this.preference.starDetectionRequest('ASTAP').set(this.starDetectors.get('ASTAP')) - this.preference.starDetectionOptions('ASTAP').set(this.starDetectors.get('ASTAP')!) + this.preference.liveStackingRequest('SIRIL').set(this.liveStackers.get('SIRIL')) } } \ No newline at end of file diff --git a/desktop/src/shared/components/map/map.component.html b/desktop/src/shared/components/map/map.component.html index e4e6a7ec1..0c20a3260 100644 --- a/desktop/src/shared/components/map/map.component.html +++ b/desktop/src/shared/components/map/map.component.html @@ -1 +1 @@ -
+
diff --git a/desktop/src/shared/components/path-chooser/path-chooser.component.html b/desktop/src/shared/components/path-chooser/path-chooser.component.html new file mode 100644 index 000000000..f650816a8 --- /dev/null +++ b/desktop/src/shared/components/path-chooser/path-chooser.component.html @@ -0,0 +1,9 @@ +
+ + + + + +
\ No newline at end of file diff --git a/desktop/src/shared/components/path-chooser/path-chooser.component.scss b/desktop/src/shared/components/path-chooser/path-chooser.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/desktop/src/shared/components/path-chooser/path-chooser.component.ts b/desktop/src/shared/components/path-chooser/path-chooser.component.ts new file mode 100644 index 000000000..086c5453e --- /dev/null +++ b/desktop/src/shared/components/path-chooser/path-chooser.component.ts @@ -0,0 +1,59 @@ +import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core' +import { dirname } from 'path' +import { ElectronService } from '../../services/electron.service' + +@Component({ + selector: 'neb-path-chooser', + templateUrl: './path-chooser.component.html', + styleUrls: ['./path-chooser.component.scss'], +}) +export class PathChooserComponent implements OnChanges { + + @Input({ required: true }) + readonly key!: string + + @Input() + readonly label?: string + + @Input() + readonly placeholder?: string + + @Input() + readonly disabled: boolean = false + + @Input() + readonly readonly: boolean = false + + @Input({ required: true }) + readonly directory!: boolean + + @Input() + path?: string + + @Output() + readonly pathChange = new EventEmitter() + + constructor(private electron: ElectronService) { } + + ngOnChanges(changes: SimpleChanges) { + if (changes.path) { + this.path = changes.path.currentValue + } + } + + async choosePath() { + const storageKey = `pathChooser.${this.key}.defaultPath` + const defaultPath = localStorage.getItem(storageKey) + const dirName = defaultPath && !this.directory ? dirname(defaultPath) : defaultPath + + const path = await (this.directory + ? this.electron.openDirectory({ defaultPath: dirName || this.path }) + : this.electron.openFile({ defaultPath: dirName || this.path })) + + if (path) { + this.path = path + this.pathChange.emit(path) + localStorage.setItem(storageKey, path) + } + } +} \ No newline at end of file diff --git a/desktop/src/shared/dialogs/location/location.dialog.html b/desktop/src/shared/dialogs/location/location.dialog.html index 7fe83c726..21068ab49 100644 --- a/desktop/src/shared/dialogs/location/location.dialog.html +++ b/desktop/src/shared/dialogs/location/location.dialog.html @@ -1,42 +1,42 @@ -
-
+
+
-
+
-
+
+ + + + +
+
-
+
-
- - - - -
-