From 67e0cf4ea7fb174ad22fd01f218494c53bb0c724 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Sat, 25 May 2024 21:59:23 -0300 Subject: [PATCH] [api]: Support Auto Focus --- .../api/alignment/polar/tppa/TPPATask.kt | 12 +- .../nebulosa/api/autofocus/AutoFocusTask.kt | 118 +++++++------ .../nebulosa/api/cameras/CameraCaptureTask.kt | 4 +- .../api/focusers/FocuserMoveAbsoluteTask.kt | 8 +- .../api/focusers/FocuserMoveRelativeTask.kt | 10 +- .../api/guiding/DitherAfterExposureTask.kt | 2 +- .../nebulosa/api/guiding/GuidePulseTask.kt | 2 +- .../nebulosa/api/guiding/WaitForSettleTask.kt | 2 +- .../kotlin/nebulosa/api/image/ImageBucket.kt | 26 ++- .../kotlin/nebulosa/api/image/ImageService.kt | 7 +- .../nebulosa/api/mounts/MountMoveTask.kt | 2 +- .../nebulosa/api/mounts/MountService.kt | 2 +- .../nebulosa/api/mounts/MountSlewTask.kt | 2 +- .../nebulosa/api/sequencer/SequencerTask.kt | 2 +- .../nebulosa/api/tasks/delay/DelayTask.kt | 4 +- .../api/wizard/flat/FlatWizardTask.kt | 6 +- api/src/test/kotlin/APITest.kt | 158 +++++++++++++++--- .../point/three/ThreePointPolarAlignment.kt | 4 +- .../src/test/kotlin/CancellationTokenTest.kt | 10 +- 19 files changed, 266 insertions(+), 115 deletions(-) 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 1b8f1e1be..1f4504d6b 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 @@ -114,14 +114,14 @@ data class TPPATask( cancellationToken.listenToPause(this) - while (!cancellationToken.isDone) { + while (!cancellationToken.isCancelled) { if (cancellationToken.isPaused) { pausing.set(false) sendEvent(TPPAState.PAUSED) cancellationToken.waitForPause() } - if (cancellationToken.isDone) break + if (cancellationToken.isCancelled) break mount?.tracking(true) @@ -134,7 +134,7 @@ data class TPPATask( mountMoveState[alignment.state.ordinal] = true } - if (cancellationToken.isDone) break + if (cancellationToken.isCancelled) break rightAscension = mount.rightAscension declination = mount.declination @@ -146,14 +146,14 @@ data class TPPATask( } } - if (cancellationToken.isDone) break + if (cancellationToken.isCancelled) break sendEvent(TPPAState.EXPOSURING) // CAPTURE. cameraCaptureTask.execute(cancellationToken) - if (cancellationToken.isDone || savedImage == null) { + if (cancellationToken.isCancelled || savedImage == null) { break } @@ -177,7 +177,7 @@ data class TPPATask( LOG.info("TPPA alignment completed. result=$result") - if (cancellationToken.isDone) break + if (cancellationToken.isCancelled) break when (result) { is ThreePointPolarAlignmentResult.NeedMoreMeasurement -> { diff --git a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt index 09dec9fdf..7325c85c1 100644 --- a/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt +++ b/api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt @@ -4,6 +4,7 @@ import io.reactivex.rxjava3.functions.Consumer import nebulosa.api.cameras.* import nebulosa.api.focusers.FocuserEventAware import nebulosa.api.focusers.FocuserMoveAbsoluteTask +import nebulosa.api.focusers.FocuserMoveRelativeTask import nebulosa.api.focusers.FocuserMoveTask import nebulosa.api.image.ImageBucket import nebulosa.api.messages.MessageEvent @@ -40,7 +41,13 @@ data class AutoFocusTask( data class MeasuredStars( @JvmField val averageHFD: Double = 0.0, @JvmField var hfdStandardDeviation: Double = 0.0, - ) + ) { + + companion object { + + @JvmStatic val ZERO = MeasuredStars() + } + } @JvmField val cameraRequest = request.capture.copy( exposureAmount = 0, exposureDelay = Duration.ZERO, @@ -54,7 +61,7 @@ data class AutoFocusTask( private val cameraCaptureTask = CameraCaptureTask(camera, cameraRequest, exposureMaxRepeat = request.capture.exposureAmount) @Volatile private var focuserMoveTask: FocuserMoveTask? = null - @Volatile private var trendLineCurve: Lazy? = null + @Volatile private var trendLineCurve: TrendLineFitting.Curve? = null @Volatile private var parabolicCurve: Lazy? = null @Volatile private var hyperbolicCurve: Lazy? = null @@ -77,69 +84,71 @@ data class AutoFocusTask( // Get initial position information, as average of multiple exposures, if configured this way. val initialHFD = if (request.rSquaredThreshold <= 0.0) takeExposure(cancellationToken).averageHFD else Double.NaN - val reverse = request.backlashCompensationMode == BacklashCompensationMode.OVERSHOOT && request.backlashIn > 0 && request.backlashOut == 0 + LOG.info("Auto Focus started. initialHFD={}, reverse={}, camera={}, focuser={}", initialHFD, reverse, camera, focuser) + + var exited = false var numberOfAttempts = 0 - var reattempt: Boolean val maximumFocusPoints = request.capture.exposureAmount * request.initialOffsetSteps * 10 - do { - reattempt = false + while (!exited && !cancellationToken.isCancelled) { numberOfAttempts++ val offsetSteps = request.initialOffsetSteps val numberOfSteps = offsetSteps + 1 + LOG.info("attempt #{}. offsetSteps={}, numberOfSteps={}", numberOfAttempts, offsetSteps, numberOfSteps) + obtainFocusPoints(numberOfSteps, offsetSteps, reverse, cancellationToken) - var leftCount = trendLineCurve!!.value.left.points.size - var rightCount = trendLineCurve!!.value.right.points.size + var leftCount = trendLineCurve!!.left.points.size + var rightCount = trendLineCurve!!.right.points.size - // When datapoints are not sufficient analyze and take more. + // When data points are not sufficient analyze and take more. do { if (leftCount == 0 && rightCount == 0) { - // TODO: ERROR NotEnoughtSpreadedPoints - // Reattempting in this situation is very likely meaningless - just move back to initial focus position and call it a day. - moveFocuser(initialFocusPosition, cancellationToken) - return + LOG.warn("Not enought spreaded points") + exited = true + break } + LOG.info("data points are not sufficient. attempt={}, numberOfSteps={}", numberOfAttempts, numberOfSteps) + // Let's keep moving in, one step at a time, until we have enough left trend points. // Then we can think about moving out to fill in the right trend points. - if (trendLineCurve!!.value.left.points.size < offsetSteps - && focusPoints.count { it.x < trendLineCurve!!.value.minimum.x && it.y == 0.0 } < offsetSteps + if (trendLineCurve!!.left.points.size < offsetSteps + && focusPoints.count { it.x < trendLineCurve!!.minimum.x && it.y == 0.0 } < offsetSteps ) { LOG.info("more data points needed to the left of the minimum") // Move to the leftmost point - this should never be necessary since we're already there, but just in case if (focuser.position != focusPoints.first().x.roundToInt()) { - moveFocuser(focusPoints.first().x.roundToInt(), cancellationToken) + moveFocuser(focusPoints.first().x.roundToInt(), cancellationToken, false) } // More points needed to the left. obtainFocusPoints(1, -1, false, cancellationToken) - } else if (trendLineCurve!!.value.right.points.size < offsetSteps - && focusPoints.count { it.x > trendLineCurve!!.value.minimum.x && it.y == 0.0 } < offsetSteps + } else if (trendLineCurve!!.right.points.size < offsetSteps + && focusPoints.count { it.x > trendLineCurve!!.minimum.x && it.y == 0.0 } < offsetSteps ) { // Now we can go to the right, if necessary. LOG.info("more data points needed to the right of the minimum") // More points needed to the right. Let's get to the rightmost point, and keep going right one point at a time. if (focuser.position != focusPoints.last().x.roundToInt()) { - moveFocuser(focusPoints.last().x.roundToInt(), cancellationToken) + moveFocuser(focusPoints.last().x.roundToInt(), cancellationToken, false) } // More points needed to the right. obtainFocusPoints(1, 1, false, cancellationToken) } - leftCount = trendLineCurve!!.value.left.points.size - rightCount = trendLineCurve!!.value.right.points.size + leftCount = trendLineCurve!!.left.points.size + rightCount = trendLineCurve!!.right.points.size if (maximumFocusPoints < focusPoints.size) { // Break out when the maximum limit of focus points is reached - // TODO: ERROR LOG.error("failed to complete. Maximum number of focus points exceeded ($maximumFocusPoints).") break } @@ -149,26 +158,35 @@ data class AutoFocusTask( LOG.error("failed to complete. position reached 0") break } - } while (rightCount + focusPoints.count { it.x > trendLineCurve!!.value.minimum.x && it.y == 0.0 } < offsetSteps || leftCount + focusPoints.count { it.x < trendLineCurve!!.value.minimum.x && it.y == 0.0 } < offsetSteps) + } while (!cancellationToken.isCancelled && (rightCount + focusPoints.count { it.x > trendLineCurve!!.minimum.x && it.y == 0.0 } < offsetSteps || leftCount + focusPoints.count { it.x < trendLineCurve!!.minimum.x && it.y == 0.0 } < offsetSteps)) + + if (exited) break val finalFocusPoint = determineFinalFocusPoint() val goodAutoFocus = validateCalculatedFocusPosition(finalFocusPoint, initialHFD, cancellationToken) if (!goodAutoFocus) { if (numberOfAttempts < request.totalNumberOfAttempts) { - moveFocuser(initialFocusPosition, cancellationToken) + moveFocuser(initialFocusPosition, cancellationToken, false) LOG.warn("potentially bad auto-focus. reattempting") reset() - reattempt = true + continue } else { LOG.warn("potentially bad auto-focus. Restoring original focus position") - reattempt = false - moveFocuser(initialFocusPosition, cancellationToken) + moveFocuser(initialFocusPosition, cancellationToken, false) + break } } - } while (reattempt) + } + + if (exited || cancellationToken.isCancelled) { + LOG.warn("did not complete successfully, so restoring the focuser position to $initialFocusPosition") + moveFocuser(initialFocusPosition, CancellationToken.NONE, false) + } reset() + + LOG.info("Auto Focus finished. camera={}, focuser={}", camera, focuser) } private fun determineFinalFocusPoint(): CurvePoint { @@ -201,8 +219,11 @@ data class AutoFocusTask( if (event.state == CameraCaptureState.EXPOSURE_FINISHED) { val image = imageBucket.open(event.savePath!!) val detectedStars = starDetection.detect(image) + LOG.info("detected ${detectedStars.size} stars") val measure = detectedStars.measureDetectedStars() + LOG.info("HFD measurement. mean={}, stdDev={}", measure.averageHFD, measure.hfdStandardDeviation) measurements.add(measure) + onNext(event) } } @@ -216,23 +237,21 @@ data class AutoFocusTask( val stepSize = request.stepSize val direction = if (reverse) -1 else 1 + LOG.info("retrieving focus points. numberOfSteps={}, offset={}, reverse={}", numberOfSteps, offset, reverse) + var focusPosition = 0 if (offset != 0) { - focuserMoveTask = FocuserMoveAbsoluteTask(focuser, direction * offset * stepSize) - focuserMoveTask!!.execute(cancellationToken) - focusPosition = focuser.position + focusPosition = moveFocuser(direction * offset * stepSize, cancellationToken, true) } var remainingSteps = numberOfSteps - while (!cancellationToken.isDone && remainingSteps > 0) { + while (!cancellationToken.isCancelled && remainingSteps > 0) { val currentFocusPosition = focusPosition if (remainingSteps > 1) { - focuserMoveTask = FocuserMoveAbsoluteTask(focuser, direction * -stepSize) - focuserMoveTask!!.execute(cancellationToken) - focusPosition = focuser.position + focusPosition = moveFocuser(direction * -stepSize, cancellationToken, true) } val measurement = takeExposure(cancellationToken) @@ -246,18 +265,21 @@ data class AutoFocusTask( } val weight = max(0.001, measurement.hfdStandardDeviation) - focusPoints.add(CurvePoint(currentFocusPosition.toDouble(), measurement.averageHFD, weight)) + val point = CurvePoint(currentFocusPosition.toDouble(), measurement.averageHFD, weight) + focusPoints.add(point) focusPoints.sortBy { it.x } - computeCurveFittings() - remainingSteps-- + + LOG.info("focus point added. remainingSteps={}, x={}, y={}, weight={}", remainingSteps, point.x, point.y, point.weight) + + computeCurveFittings() } } private fun computeCurveFittings() { with(focusPoints.toList()) { - trendLineCurve = lazy { TrendLineFitting.calculate(this) } + trendLineCurve = TrendLineFitting.calculate(this) if (size >= 3) { if (request.fittingMode == AutoFocusFittingMode.PARABOLIC || request.fittingMode == AutoFocusFittingMode.TREND_PARABOLIC) { @@ -273,14 +295,12 @@ data class AutoFocusTask( private fun validateCalculatedFocusPosition(focusPoint: CurvePoint, initialHFD: Double, cancellationToken: CancellationToken): Boolean { val threshold = request.rSquaredThreshold - fun isTrendLineBad() = trendLineCurve?.value?.let { it.left.rSquared < threshold || it.right.rSquared < threshold } ?: false - + fun isTrendLineBad() = trendLineCurve?.let { it.left.rSquared < threshold || it.right.rSquared < threshold } ?: false fun isParabolicBad() = parabolicCurve?.value?.let { it.rSquared < threshold } ?: false - fun isHyperbolicBad() = hyperbolicCurve?.value?.let { it.rSquared < threshold } ?: false if (threshold > 0.0) { - val bad = when (request.fittingMode) { + val isBad = when (request.fittingMode) { AutoFocusFittingMode.TRENDLINES -> isTrendLineBad() AutoFocusFittingMode.PARABOLIC -> isParabolicBad() AutoFocusFittingMode.TREND_PARABOLIC -> isParabolicBad() || isTrendLineBad() @@ -288,7 +308,7 @@ data class AutoFocusTask( AutoFocusFittingMode.TREND_HYPERBOLIC -> isHyperbolicBad() || isTrendLineBad() } - if (bad) { + if (isBad) { LOG.error("coefficient of determination is below threshold") return false } @@ -302,7 +322,7 @@ data class AutoFocusTask( return false } - moveFocuser(focusPoint.x.roundToInt(), cancellationToken) + moveFocuser(focusPoint.x.roundToInt(), cancellationToken, false) val hfd = takeExposure(cancellationToken).averageHFD if (threshold <= 0) { @@ -315,9 +335,11 @@ data class AutoFocusTask( return true } - private fun moveFocuser(position: Int, cancellationToken: CancellationToken) { - focuserMoveTask = FocuserMoveAbsoluteTask(focuser, position) + private fun moveFocuser(position: Int, cancellationToken: CancellationToken, relative: Boolean): Int { + focuserMoveTask = if (relative) FocuserMoveRelativeTask(focuser, position) + else FocuserMoveAbsoluteTask(focuser, position) focuserMoveTask!!.execute(cancellationToken) + return focuser.position } override fun reset() { @@ -341,6 +363,8 @@ data class AutoFocusTask( @JvmStatic private fun List.measureDetectedStars(): MeasuredStars { + if (isEmpty()) return MeasuredStars.ZERO + val mean = sumOf { it.hfd } / size var stdDev = 0.0 diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureTask.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureTask.kt index 83a83251b..6d6ade6f0 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureTask.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureTask.kt @@ -67,7 +67,7 @@ data class CameraCaptureTask( cameraExposureTask.reset() - while (!cancellationToken.isDone && + while (!cancellationToken.isCancelled && !cameraExposureTask.isAborted && ((exposureMaxRepeat > 0 && exposureRepeatCount < exposureMaxRepeat) || (exposureMaxRepeat <= 0 && (request.isLoop || exposureCount < request.exposureAmount))) @@ -100,7 +100,7 @@ data class CameraCaptureTask( cameraExposureTask.execute(cancellationToken) // DITHER. - if (!cancellationToken.isDone && !cameraExposureTask.isAborted && guider != null + if (!cancellationToken.isCancelled && !cameraExposureTask.isAborted && guider != null && exposureCount >= 1 && exposureCount % request.dither.afterExposures == 0 ) { ditherAfterExposureTask.execute(cancellationToken) diff --git a/api/src/main/kotlin/nebulosa/api/focusers/FocuserMoveAbsoluteTask.kt b/api/src/main/kotlin/nebulosa/api/focusers/FocuserMoveAbsoluteTask.kt index d51ffd0da..b154ee55e 100644 --- a/api/src/main/kotlin/nebulosa/api/focusers/FocuserMoveAbsoluteTask.kt +++ b/api/src/main/kotlin/nebulosa/api/focusers/FocuserMoveAbsoluteTask.kt @@ -28,13 +28,15 @@ data class FocuserMoveAbsoluteTask( } override fun execute(cancellationToken: CancellationToken) { - if (!cancellationToken.isDone && focuser.connected + if (!cancellationToken.isCancelled && focuser.connected && !focuser.moving && position != focuser.position ) { try { cancellationToken.listen(this) - LOG.info("Focuser move started. focuser={}, position={}", focuser, position) + LOG.info("Focuser move started. position={}, focuser={}", position, focuser) + + latch.countUp() if (focuser.canAbsoluteMove) focuser.moveFocusTo(position) else if (focuser.position - position < 0) focuser.moveFocusIn(abs(focuser.position - position)) @@ -45,7 +47,7 @@ data class FocuserMoveAbsoluteTask( cancellationToken.unlisten(this) } - LOG.info("Focuser move finished. focuser={}, position={}", focuser, position) + LOG.info("Focuser move finished. position={}, focuser={}", position, focuser) } } diff --git a/api/src/main/kotlin/nebulosa/api/focusers/FocuserMoveRelativeTask.kt b/api/src/main/kotlin/nebulosa/api/focusers/FocuserMoveRelativeTask.kt index 3ad2bae08..531dafce0 100644 --- a/api/src/main/kotlin/nebulosa/api/focusers/FocuserMoveRelativeTask.kt +++ b/api/src/main/kotlin/nebulosa/api/focusers/FocuserMoveRelativeTask.kt @@ -30,24 +30,26 @@ data class FocuserMoveRelativeTask( } override fun execute(cancellationToken: CancellationToken) { - if (!cancellationToken.isDone && focuser.connected && !focuser.moving && offset != 0) { + if (!cancellationToken.isCancelled && focuser.connected && !focuser.moving && offset != 0) { try { cancellationToken.listen(this) initialPosition = focuser.position - LOG.info("Focuser move started. focuser={}, offset={}", focuser, offset) + LOG.info("Focuser move started. offset={}, focuser={}", offset, focuser) + + latch.countUp() if (!focuser.canRelativeMove) focuser.moveFocusTo(focuser.position + offset) else if (offset > 0) focuser.moveFocusOut(offset) - else focuser.moveFocusIn(offset) + else focuser.moveFocusIn(abs(offset)) latch.await() } finally { cancellationToken.unlisten(this) } - LOG.info("Focuser move finished. focuser={}, offset={}", focuser, offset) + LOG.info("Focuser move finished. offset={}, focuser={}", offset, focuser) } } diff --git a/api/src/main/kotlin/nebulosa/api/guiding/DitherAfterExposureTask.kt b/api/src/main/kotlin/nebulosa/api/guiding/DitherAfterExposureTask.kt index fb2de4a50..aa59f6828 100644 --- a/api/src/main/kotlin/nebulosa/api/guiding/DitherAfterExposureTask.kt +++ b/api/src/main/kotlin/nebulosa/api/guiding/DitherAfterExposureTask.kt @@ -27,7 +27,7 @@ data class DitherAfterExposureTask( override fun execute(cancellationToken: CancellationToken) { if (guider != null && guider.canDither && request.enabled && guider.state == GuideState.GUIDING - && !cancellationToken.isDone + && !cancellationToken.isCancelled ) { LOG.info("Dither started. request={}", request) diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseTask.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseTask.kt index 787757139..5bc3f2079 100644 --- a/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseTask.kt +++ b/api/src/main/kotlin/nebulosa/api/guiding/GuidePulseTask.kt @@ -24,7 +24,7 @@ data class GuidePulseTask( } override fun execute(cancellationToken: CancellationToken) { - if (!cancellationToken.isDone && guideOutput.pulseGuide(request.duration, request.direction)) { + if (!cancellationToken.isCancelled && guideOutput.pulseGuide(request.duration, request.direction)) { LOG.info("Guide Pulse started. guideOutput={}, duration={}, direction={}", guideOutput, request.duration.toMillis(), request.direction) try { diff --git a/api/src/main/kotlin/nebulosa/api/guiding/WaitForSettleTask.kt b/api/src/main/kotlin/nebulosa/api/guiding/WaitForSettleTask.kt index 1f4aa2435..87006cb6e 100644 --- a/api/src/main/kotlin/nebulosa/api/guiding/WaitForSettleTask.kt +++ b/api/src/main/kotlin/nebulosa/api/guiding/WaitForSettleTask.kt @@ -10,7 +10,7 @@ data class WaitForSettleTask( ) : Task { override fun execute(cancellationToken: CancellationToken) { - if (guider != null && guider.isSettling && !cancellationToken.isDone) { + if (guider != null && guider.isSettling && !cancellationToken.isCancelled) { LOG.info("Wait For Settle started") guider.waitForSettle(cancellationToken) LOG.info("Wait For Settle finished") diff --git a/api/src/main/kotlin/nebulosa/api/image/ImageBucket.kt b/api/src/main/kotlin/nebulosa/api/image/ImageBucket.kt index a9119ede1..7d51daeb3 100644 --- a/api/src/main/kotlin/nebulosa/api/image/ImageBucket.kt +++ b/api/src/main/kotlin/nebulosa/api/image/ImageBucket.kt @@ -11,17 +11,23 @@ import kotlin.io.path.extension @Component class ImageBucket { - private val bucket = HashMap>(256) + data class OpenedImage( + @JvmField val image: Image, + @JvmField var solution: PlateSolution? = null, + @JvmField val debayer: Boolean = true, + ) + + private val bucket = HashMap(256) @Synchronized - fun put(path: Path, image: Image, solution: PlateSolution? = null) { - bucket[path] = image to (solution ?: PlateSolution.from(image.header)) + fun put(path: Path, image: Image, solution: PlateSolution? = null, debayer: Boolean = true) { + bucket[path] = OpenedImage(image, solution ?: PlateSolution.from(image.header), debayer) } @Synchronized fun put(path: Path, solution: PlateSolution): Boolean { val item = bucket[path] ?: return false - bucket[path] = item.first to solution + item.solution = solution return true } @@ -29,7 +35,9 @@ class ImageBucket { fun open(path: Path, debayer: Boolean = true, solution: PlateSolution? = null, force: Boolean = false): Image { val openedImage = this[path] - if (openedImage != null && !force) return openedImage.first + if (openedImage != null && !force && debayer == openedImage.debayer) { + return openedImage.image + } val representation = when (path.extension.lowercase()) { "fit", "fits" -> path.fits() @@ -38,7 +46,7 @@ class ImageBucket { } val image = representation.use { Image.open(it, debayer) } - put(path, image, solution) + put(path, image, solution, debayer) return image } @@ -47,7 +55,7 @@ class ImageBucket { bucket.remove(path) } - operator fun get(path: Path): Pair? { + operator fun get(path: Path): OpenedImage? { return bucket[path] } @@ -56,10 +64,10 @@ class ImageBucket { } operator fun contains(image: Image): Boolean { - return bucket.any { it.value.first === image } + return bucket.any { it.value.image === image } } operator fun contains(solution: PlateSolution): Boolean { - return bucket.any { it.value.second === solution } + return bucket.any { it.value.solution === solution } } } diff --git a/api/src/main/kotlin/nebulosa/api/image/ImageService.kt b/api/src/main/kotlin/nebulosa/api/image/ImageService.kt index 59d7ce7da..4b9303a4b 100644 --- a/api/src/main/kotlin/nebulosa/api/image/ImageService.kt +++ b/api/src/main/kotlin/nebulosa/api/image/ImageService.kt @@ -94,7 +94,7 @@ class ImageService( stretchParams!!.shadow, stretchParams.highlight, stretchParams.midtone, transformedImage.header.rightAscension.takeIf { it.isFinite() }, transformedImage.header.declination.takeIf { it.isFinite() }, - imageBucket[path]?.second?.let(::ImageSolved), + imageBucket[path]?.solution?.let(::ImageSolved), transformedImage.header.mapNotNull { if (it.isCommentStyle) null else ImageHeaderItem(it.key, it.value) }, transformedImage.header.bitpix, instrument, statistics, ) @@ -271,7 +271,7 @@ class ImageService( } fun saveImageAs(inputPath: Path, save: SaveImage, camera: Camera?) { - val (image) = imageBucket[inputPath]?.first?.transform(save.shouldBeTransformed, save.transformation, ImageOperation.SAVE) + val (image) = imageBucket[inputPath]?.image?.transform(save.shouldBeTransformed, save.transformation, ImageOperation.SAVE) ?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Image not found") require(save.path != null) @@ -292,8 +292,7 @@ class ImageService( width: Int, height: Int, fov: Angle, rotation: Angle = 0.0, id: String = "CDS/P/DSS2/COLOR", ): Path { - val (image, calibration, path) = framingService - .frame(rightAscension, declination, width, height, fov, rotation, id)!! + val (image, calibration, path) = framingService.frame(rightAscension, declination, width, height, fov, rotation, id)!! imageBucket.put(path, image, calibration) return path } diff --git a/api/src/main/kotlin/nebulosa/api/mounts/MountMoveTask.kt b/api/src/main/kotlin/nebulosa/api/mounts/MountMoveTask.kt index 9279f7400..db1546b52 100644 --- a/api/src/main/kotlin/nebulosa/api/mounts/MountMoveTask.kt +++ b/api/src/main/kotlin/nebulosa/api/mounts/MountMoveTask.kt @@ -23,7 +23,7 @@ data class MountMoveTask( } override fun execute(cancellationToken: CancellationToken) { - if (!cancellationToken.isDone && request.duration.toMillis() > 0) { + if (!cancellationToken.isCancelled && request.duration.toMillis() > 0) { mount.slewRates.takeIf { !request.speed.isNullOrBlank() } ?.find { it.name == request.speed } ?.also { mount.slewRate(it) } diff --git a/api/src/main/kotlin/nebulosa/api/mounts/MountService.kt b/api/src/main/kotlin/nebulosa/api/mounts/MountService.kt index 3ac78f22b..b21b85a2c 100644 --- a/api/src/main/kotlin/nebulosa/api/mounts/MountService.kt +++ b/api/src/main/kotlin/nebulosa/api/mounts/MountService.kt @@ -220,7 +220,7 @@ class MountService(private val imageBucket: ImageBucket) { } fun pointMountHere(mount: Mount, path: Path, x: Double, y: Double) { - val calibration = imageBucket[path]?.second ?: return + val calibration = imageBucket[path]?.solution ?: return if (calibration.isNotEmpty() && calibration.solved) { val wcs = WCS(calibration) diff --git a/api/src/main/kotlin/nebulosa/api/mounts/MountSlewTask.kt b/api/src/main/kotlin/nebulosa/api/mounts/MountSlewTask.kt index e6eecd2ae..9c057a224 100644 --- a/api/src/main/kotlin/nebulosa/api/mounts/MountSlewTask.kt +++ b/api/src/main/kotlin/nebulosa/api/mounts/MountSlewTask.kt @@ -42,7 +42,7 @@ data class MountSlewTask( } override fun execute(cancellationToken: CancellationToken) { - if (!cancellationToken.isDone && + if (!cancellationToken.isCancelled && mount.connected && !mount.parked && !mount.parking && !mount.slewing && rightAscension.isFinite() && declination.isFinite() && (mount.rightAscension != rightAscension || mount.declination != declination) diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/SequencerTask.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SequencerTask.kt index ed3fdb6be..4efde90bf 100644 --- a/api/src/main/kotlin/nebulosa/api/sequencer/SequencerTask.kt +++ b/api/src/main/kotlin/nebulosa/api/sequencer/SequencerTask.kt @@ -128,7 +128,7 @@ data class SequencerTask( camera.snoop(listOf(mount, wheel, focuser)) for (task in tasks) { - if (cancellationToken.isDone) break + if (cancellationToken.isCancelled) break currentTask.set(task) task.execute(cancellationToken) currentTask.set(null) diff --git a/api/src/main/kotlin/nebulosa/api/tasks/delay/DelayTask.kt b/api/src/main/kotlin/nebulosa/api/tasks/delay/DelayTask.kt index af20bc2cf..6d3dbe5e4 100644 --- a/api/src/main/kotlin/nebulosa/api/tasks/delay/DelayTask.kt +++ b/api/src/main/kotlin/nebulosa/api/tasks/delay/DelayTask.kt @@ -17,10 +17,10 @@ data class DelayTask( val durationTime = duration.toMillis() var remainingTime = durationTime - if (!cancellationToken.isDone && remainingTime > 0L) { + if (!cancellationToken.isCancelled && remainingTime > 0L) { LOG.info("Delay started. duration={}", remainingTime) - while (!cancellationToken.isDone && remainingTime > 0L) { + while (!cancellationToken.isCancelled && remainingTime > 0L) { val waitTime = minOf(remainingTime, DELAY_INTERVAL) if (waitTime > 0L) { 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 06f8c848b..8a0a0ec3d 100644 --- a/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardTask.kt +++ b/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardTask.kt @@ -38,7 +38,7 @@ data class FlatWizardTask( override fun canUseAsLastEvent(event: MessageEvent) = event is FlatWizardEvent override fun execute(cancellationToken: CancellationToken) { - while (!cancellationToken.isDone) { + while (!cancellationToken.isCancelled) { val delta = exposureMax.toMillis() - exposureMin.toMillis() if (delta < 10) { @@ -75,7 +75,7 @@ data class FlatWizardTask( it.execute(cancellationToken) } - if (cancellationToken.isDone) { + if (cancellationToken.isCancelled) { state = FlatWizardState.IDLE break } else if (savedPath == null) { @@ -103,7 +103,7 @@ data class FlatWizardTask( } } - if (state != FlatWizardState.FAILED && cancellationToken.isDone) { + if (state != FlatWizardState.FAILED && cancellationToken.isCancelled) { state = FlatWizardState.IDLE } diff --git a/api/src/test/kotlin/APITest.kt b/api/src/test/kotlin/APITest.kt index 673abd1cb..8d89f6d8d 100644 --- a/api/src/test/kotlin/APITest.kt +++ b/api/src/test/kotlin/APITest.kt @@ -4,6 +4,9 @@ import com.fasterxml.jackson.module.kotlin.kotlinModule import io.kotest.core.annotation.EnabledIf import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.booleans.shouldBeTrue +import kotlinx.coroutines.delay +import nebulosa.api.autofocus.AutoFocusRequest +import nebulosa.api.beans.converters.time.DurationSerializer import nebulosa.api.cameras.CameraStartCaptureRequest import nebulosa.common.json.PathSerializer import nebulosa.test.NonGitHubOnlyCondition @@ -15,26 +18,133 @@ import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.logging.HttpLoggingInterceptor import java.nio.file.Path import java.time.Duration +import java.time.temporal.ChronoUnit @EnabledIf(NonGitHubOnlyCondition::class) class APITest : StringSpec() { init { - "Connect" { put("connection?host=localhost&port=7624") } - "Cameras" { get("cameras") } - "Camera Connect" { put("cameras/$CAMERA_NAME/connect") } - "Camera" { get("cameras/$CAMERA_NAME") } - "Camera Capture Start" { putJson("cameras/$CAMERA_NAME/capture/start", CAMERA_START_CAPTURE_REQUEST) } - "Camera Capture Stop" { put("cameras/$CAMERA_NAME/capture/abort") } - "Camera Disconnect" { put("cameras/$CAMERA_NAME/disconnect") } - "Mounts" { get("mounts") } - "Mount Connect" { put("mounts/$MOUNT_NAME/connect") } - "Mount" { get("mounts/$MOUNT_NAME") } - "Mount Telescope Control Start" { put("mounts/$MOUNT_NAME/remote-control/start?type=LX200&host=0.0.0.0&port=10001") } - "Mount Telescope Control List" { get("mounts/$MOUNT_NAME/remote-control") } - "Mount Telescope Control Stop" { put("mounts/$MOUNT_NAME/remote-control/stop?type=LX200") } - "Mount Disconnect" { put("mounts/$MOUNT_NAME/disconnect") } - "Disconnect" { delete("connection") } + // GENERAL. + + "Connect" { connect() } + "Disconnect" { disconnect() } + + // CAMERA. + + "Cameras" { cameras() } + "Camera Connect" { cameraConnect() } + "Camera" { camera() } + "Camera Capture Start" { cameraStartCapture() } + "Camera Capture Stop" { cameraStopCapture() } + "Camera Disconnect" { cameraDisconnect() } + + // MOUNT. + + "Mounts" { mounts() } + "Mount Connect" { mountConnect() } + "Mount" { mount() } + "Mount Remote Control Start" { mountRemoteControlStart() } + "Mount Remote Control List" { mountRemoteControlList() } + "Mount Remote Control Stop" { mountRemoteControlStop() } + "Mount Disconnect" { mountDisconnect() } + + // FOCUSER. + + "Focusers" { focusers() } + "Focuser Connect" { focuserConnect() } + "Focuser" { focuser() } + "Focuser Disconnect" { focuserDisconnect() } + + // AUTO FOCUS. + + "Auto Focus Start" { + connect() + delay(2000) + cameraConnect() + focuserConnect() + delay(1000) + autoFocusStart() + } + } + + private fun connect(host: String = "0.0.0.0", port: Int = 7624) { + put("connection?host=$host&port=$port") + } + + private fun disconnect() { + delete("connection") + } + + private fun cameras() { + get("cameras") + } + + private fun cameraConnect(camera: String = CAMERA_NAME) { + put("cameras/$camera/connect") + } + + private fun cameraDisconnect(camera: String = CAMERA_NAME) { + put("cameras/$camera/disconnect") + } + + private fun camera(camera: String = CAMERA_NAME) { + get("cameras/$camera") + } + + private fun cameraStartCapture(camera: String = CAMERA_NAME) { + putJson("cameras/$camera/capture/start", CAMERA_START_CAPTURE_REQUEST) + } + + private fun cameraStopCapture(camera: String = CAMERA_NAME) { + put("cameras/$camera/capture/abort") + } + + private fun mounts() { + get("mounts") + } + + private fun mountConnect(mount: String = MOUNT_NAME) { + put("mounts/$mount/connect") + } + + private fun mountDisconnect(mount: String = MOUNT_NAME) { + put("mounts/$mount/disconnect") + } + + private fun mount(mount: String = MOUNT_NAME) { + get("mounts/$mount") + } + + private fun mountRemoteControlStart(mount: String = MOUNT_NAME, host: String = "0.0.0.0", port: Int = 10001) { + put("mounts/$mount/remote-control/start?type=LX200&host=$host&port=$port") + } + + private fun mountRemoteControlList(mount: String = MOUNT_NAME) { + get("mounts/$mount/remote-control") + } + + private fun mountRemoteControlStop(mount: String = MOUNT_NAME) { + put("mounts/$mount/remote-control/stop?type=LX200") + } + + private fun focusers() { + get("focusers") + } + + private fun focuserConnect(focuser: String = FOCUSER_NAME) { + put("focusers/$focuser/connect") + } + + private fun focuserDisconnect(focuser: String = FOCUSER_NAME) { + put("focusers/$focuser/disconnect") + } + + private fun focuser(focuser: String = FOCUSER_NAME) { + get("focusers/$focuser") + } + + private fun autoFocusStart(camera: String = CAMERA_NAME, focuser: String = FOCUSER_NAME) { + putJson("auto-focus/$camera/$focuser/start", AUTO_FOCUS_REQUEST) } companion object { @@ -42,18 +152,25 @@ class APITest : StringSpec() { private const val BASE_URL = "http://localhost:7000" private const val CAMERA_NAME = "CCD Simulator" private const val MOUNT_NAME = "Telescope Simulator" + private const val FOCUSER_NAME = "Focuser Simulator" @JvmStatic private val EXPOSURE_TIME = Duration.ofSeconds(5) @JvmStatic private val CAPTURES_PATH = Path.of("/home/tiagohm/Git/nebulosa/data/captures") - @JvmStatic private val CAMERA_START_CAPTURE_REQUEST = - CameraStartCaptureRequest(exposureTime = EXPOSURE_TIME, width = 1280, height = 1024, frameFormat = "INDI_MONO", savePath = CAPTURES_PATH) - .copy(exposureAmount = 2) + + @JvmStatic private val CAMERA_START_CAPTURE_REQUEST = CameraStartCaptureRequest( + exposureTime = EXPOSURE_TIME, width = 1280, height = 1024, frameFormat = "INDI_MONO", + savePath = CAPTURES_PATH, exposureAmount = 2 + ) + + @JvmStatic private val AUTO_FOCUS_REQUEST = AutoFocusRequest(capture = CAMERA_START_CAPTURE_REQUEST) @JvmStatic private val CLIENT = OkHttpClient.Builder() .addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY)) .build() - @JvmStatic private val KOTLIN_MODULE = kotlinModule().addSerializer(PathSerializer) + @JvmStatic private val KOTLIN_MODULE = kotlinModule() + .addSerializer(PathSerializer) + .addSerializer(DurationSerializer()) @JvmStatic private val OBJECT_MAPPER = ObjectMapper() .registerModule(JavaTimeModule()) @@ -78,8 +195,7 @@ class APITest : StringSpec() { private fun putJson(path: String, data: Any) { val bytes = OBJECT_MAPPER.writeValueAsBytes(data) val body = bytes.toRequestBody(APPLICATION_JSON) - val request = Request.Builder().put(body).url("$BASE_URL/$path").build() - CLIENT.newCall(request).execute().use { it.isSuccessful.shouldBeTrue() } + put(path, body) } @JvmStatic diff --git a/nebulosa-alignment/src/main/kotlin/nebulosa/alignment/polar/point/three/ThreePointPolarAlignment.kt b/nebulosa-alignment/src/main/kotlin/nebulosa/alignment/polar/point/three/ThreePointPolarAlignment.kt index 3f0f5c33c..b85a3385b 100644 --- a/nebulosa-alignment/src/main/kotlin/nebulosa/alignment/polar/point/three/ThreePointPolarAlignment.kt +++ b/nebulosa-alignment/src/main/kotlin/nebulosa/alignment/polar/point/three/ThreePointPolarAlignment.kt @@ -56,7 +56,7 @@ data class ThreePointPolarAlignment( compensateRefraction: Boolean = false, cancellationToken: CancellationToken = CancellationToken.NONE, ): ThreePointPolarAlignmentResult { - if (cancellationToken.isDone) { + if (cancellationToken.isCancelled) { return Cancelled } @@ -66,7 +66,7 @@ data class ThreePointPolarAlignment( return NoPlateSolution(e) } - if (cancellationToken.isDone) { + if (cancellationToken.isCancelled) { return Cancelled } else if (!solution.solved) { return NoPlateSolution(null) diff --git a/nebulosa-common/src/test/kotlin/CancellationTokenTest.kt b/nebulosa-common/src/test/kotlin/CancellationTokenTest.kt index 16f6425a0..169eef2dc 100644 --- a/nebulosa-common/src/test/kotlin/CancellationTokenTest.kt +++ b/nebulosa-common/src/test/kotlin/CancellationTokenTest.kt @@ -15,7 +15,7 @@ class CancellationTokenTest : StringSpec() { token.cancel(false) token.get() shouldBe source source shouldBe CancellationSource.Cancel(false) - token.isDone.shouldBeTrue() + token.isCancelled.shouldBeTrue() } "cancel may interrupt if running" { var source: CancellationSource? = null @@ -24,7 +24,7 @@ class CancellationTokenTest : StringSpec() { token.cancel() token.get() shouldBe source source shouldBe CancellationSource.Cancel(true) - token.isDone.shouldBeTrue() + token.isCancelled.shouldBeTrue() } "close" { var source: CancellationSource? = null @@ -33,7 +33,7 @@ class CancellationTokenTest : StringSpec() { token.close() token.get() shouldBe source source shouldBe CancellationSource.Close - token.isDone.shouldBeTrue() + token.isCancelled.shouldBeTrue() } "listen" { var source: CancellationSource? = null @@ -42,12 +42,12 @@ class CancellationTokenTest : StringSpec() { token.listen { source = it } token.get() shouldBe CancellationSource.Cancel(true) source shouldBe CancellationSource.Listen - token.isDone.shouldBeTrue() + token.isCancelled.shouldBeTrue() } "none" { var source: CancellationSource? = null val token = CancellationToken.NONE - token.isDone.shouldBeTrue() + token.isCancelled.shouldBeTrue() token.listen { source = it } token.cancel() token.get() shouldBe CancellationSource.None