diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVExecutor.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVExecutor.kt index 7c6771183..bc2191834 100644 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVExecutor.kt +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/darv/DARVExecutor.kt @@ -35,8 +35,8 @@ class DARVExecutor( fun execute(camera: Camera, guideOutput: GuideOutput, request: DARVStartRequest) { check(camera.connected) { "${camera.name} Camera is not connected" } check(guideOutput.connected) { "${guideOutput.name} Guide Output is not connected" } - check(jobs.any { it.task.camera === camera }) { "${camera.name} DARV Job is already in progress" } - check(jobs.any { it.task.guideOutput === guideOutput }) { "${camera.name} DARV Job is already in progress" } + check(jobs.none { it.task.camera === camera }) { "${camera.name} DARV Job is already in progress" } + check(jobs.none { it.task.guideOutput === guideOutput }) { "${camera.name} DARV Job is already in progress" } val task = DARVTask(camera, guideOutput, request) task.subscribe(this) diff --git a/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPAExecutor.kt b/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPAExecutor.kt index 30abd7a14..51c020aab 100644 --- a/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPAExecutor.kt +++ b/api/src/main/kotlin/nebulosa/api/alignment/polar/tppa/TPPAExecutor.kt @@ -40,8 +40,8 @@ class TPPAExecutor( fun execute(camera: Camera, mount: Mount, request: TPPAStartRequest) { check(camera.connected) { "${camera.name} Camera is not connected" } check(mount.connected) { "${mount.name} Mount is not connected" } - check(jobs.any { it.task.camera === camera }) { "${camera.name} TPPA Job is already in progress" } - check(jobs.any { it.task.mount === mount }) { "${camera.name} TPPA Job is already in progress" } + check(jobs.none { it.task.camera === camera }) { "${camera.name} TPPA Job is already in progress" } + check(jobs.none { it.task.mount === mount }) { "${camera.name} TPPA Job is already in progress" } val solver = plateSolverService.solverFor(request.plateSolver) val task = TPPATask(camera, solver, request, mount) diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureEvent.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureEvent.kt index 7c5348259..4bbdbba2b 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureEvent.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureEvent.kt @@ -7,7 +7,8 @@ import java.time.Duration data class CameraCaptureEvent( @JvmField val camera: Camera, - @JvmField val state: CameraCaptureState, + @JvmField val state: CameraCaptureState = CameraCaptureState.IDLE, + @JvmField val exposureAmount: Int = 0, @JvmField val exposureCount: Int = 0, @JvmField val captureRemainingTime: Duration = Duration.ZERO, @JvmField val captureElapsedTime: Duration = Duration.ZERO, @@ -19,5 +20,4 @@ data class CameraCaptureEvent( ) : MessageEvent { override val eventName = "CAMERA.CAPTURE_ELAPSED" - } diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureExecutor.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureExecutor.kt index b28bf7f5b..7ecf34dcb 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureExecutor.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureExecutor.kt @@ -32,7 +32,7 @@ class CameraCaptureExecutor( @Synchronized fun execute(camera: Camera, request: CameraStartCaptureRequest) { check(camera.connected) { "${camera.name} Camera is not connected" } - check(jobs.any { it.task.camera === camera }) { "${camera.name} Camera Capture is already in progress" } + check(jobs.none { it.task.camera === camera }) { "${camera.name} Camera Capture is already in progress" } val task = CameraCaptureTask(camera, request, guider) task.subscribe(this) diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureTask.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureTask.kt index 2695e84dc..111c30685 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureTask.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureTask.kt @@ -20,7 +20,8 @@ data class CameraCaptureTask( @JvmField val camera: Camera, @JvmField val request: CameraStartCaptureRequest, @JvmField val guider: Guider? = null, - @JvmField val delayOnFirstExposure: Boolean = false, + private val useFirstExposure: Boolean = false, + private val exposureAmount: Int = request.exposureAmount, ) : Task(), Consumer { private val delayTask = DelayTask(request.exposureDelay) @@ -41,7 +42,7 @@ data class CameraCaptureTask( @Volatile private var savePath: 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 (delayOnFirstExposure) 0 else 1)) + else Duration.ofNanos(request.exposureTime.toNanos() * request.exposureAmount + request.exposureDelay.toNanos() * (request.exposureAmount - if (useFirstExposure) 0 else 1)) init { delayTask.subscribe(this) @@ -58,21 +59,24 @@ data class CameraCaptureTask( } override fun execute(cancellationToken: CancellationToken) { - LOG.info("camera capture started. camera={}, request={}, exposureCount={}", camera, request, exposureCount) + LOG.info("Camera Capture started. camera={}, request={}, exposureCount={}", camera, request, exposureCount) while (!cancellationToken.isDone && - (request.isLoop || exposureCount < request.exposureAmount) + (request.isLoop || exposureCount < exposureAmount) ) { if (exposureCount == 0) { + state = CameraCaptureState.CAPTURE_STARTED + sendEvent() + if (guider != null) { - if (delayOnFirstExposure) { + if (useFirstExposure) { // DELAY & WAIT FOR SETTLE. delayAndWaitForSettleSplitTask.execute(cancellationToken) } else { // WAIT FOR SETTLE. waitForSettleTask.execute(cancellationToken) } - } else if (delayOnFirstExposure) { + } else if (useFirstExposure) { // DELAY. delayTask.execute(cancellationToken) } @@ -88,12 +92,17 @@ data class CameraCaptureTask( cameraExposureTask.execute(cancellationToken) // DITHER. - if (guider != null && exposureCount >= 1 && exposureCount % request.dither.afterExposures == 0) { + if (!cancellationToken.isDone && guider != null && exposureCount >= 1 && exposureCount % request.dither.afterExposures == 0) { ditherAfterExposureTask.execute(cancellationToken) } } - LOG.info("camera capture finished. camera={}, request={}, exposureCount={}", camera, request, exposureCount) + if (state != CameraCaptureState.CAPTURE_FINISHED) { + state = CameraCaptureState.CAPTURE_FINISHED + sendEvent() + } + + LOG.info("Camera Capture finished. camera={}, request={}, exposureCount={}", camera, request, exposureCount) } @Synchronized @@ -125,30 +134,32 @@ data class CameraCaptureTask( captureElapsedTime = prevCaptureElapsedTime + request.exposureTime savePath = event.savedPath } - CameraExposureState.IDLE, - CameraExposureState.ABORTED -> { - state = CameraCaptureState.IDLE + CameraExposureState.IDLE -> { + state = CameraCaptureState.CAPTURE_FINISHED } } } - is DitherAfterExposureEvent -> { - } + is DitherAfterExposureEvent -> return else -> return LOG.warn("unknown event: {}", event) } + sendEvent() + } + + private fun sendEvent() { if (state != CameraCaptureState.IDLE && !request.isLoop) { captureRemainingTime = if (estimatedCaptureTime > captureElapsedTime) estimatedCaptureTime - captureElapsedTime else Duration.ZERO captureProgress = (estimatedCaptureTime - captureRemainingTime).toNanos().toDouble() / estimatedCaptureTime.toNanos() } - val cameraCaptureEvent = CameraCaptureEvent( - camera, state, exposureCount, + val event = CameraCaptureEvent( + camera, state, request.exposureAmount, exposureCount, captureRemainingTime, captureElapsedTime, captureProgress, stepRemainingTime, stepElapsedTime, stepProgress, savePath ) - onNext(cameraCaptureEvent) + onNext(event) } override fun close() { diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureState.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureState.kt index 6d32dbb82..3c71ba3ea 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureState.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureState.kt @@ -5,5 +5,4 @@ enum class CameraExposureState { STARTED, ELAPSED, FINISHED, - ABORTED, } diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureTask.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureTask.kt index 9c03e1389..03bc40ba3 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureTask.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraExposureTask.kt @@ -40,7 +40,8 @@ data class CameraExposureTask( is CameraExposureAborted, is CameraExposureFailed, is CameraDetached -> { - onCancelled(CancellationSource.Close) + aborted.set(true) + latch.reset() } is CameraExposureProgressChanged -> { val exposureTime = request.exposureTime @@ -57,7 +58,7 @@ data class CameraExposureTask( override fun execute(cancellationToken: CancellationToken) { if (camera.connected && !aborted.get()) { - LOG.info("camera exposure started. camera={}, request={}", camera, request) + LOG.info("Camera Exposure started. camera={}, request={}", camera, request) latch.countUp() @@ -65,6 +66,8 @@ data class CameraExposureTask( sendEvent() with(camera) { + enableBlob() + if (request.width > 0 && request.height > 0) { frame(request.x, request.y, request.width, request.height) } @@ -74,25 +77,24 @@ data class CameraExposureTask( bin(request.binX, request.binY) gain(request.gain) offset(request.offset) - startCapture(exposureTime) + startCapture(request.exposureTime) } - latch.await() - - if (aborted.get()) { - state = CameraExposureState.ABORTED - sendEvent() + try { + cancellationToken.listen(this) + latch.await() + } finally { + cancellationToken.unlisten(this) } - LOG.info("camera exposure finished. camera={}, request={}", camera, request) + LOG.info("Camera Exposure finished. aborted={}, camera={}, request={}", aborted.get(), camera, request) } else { LOG.warn("camera not connected or aborted. aborted={}, camera={}, request={}", aborted.get(), camera, request) } } override fun onCancelled(source: CancellationSource) { - aborted.set(true) - latch.reset() + camera.abortCapture() } override fun reset() { diff --git a/api/src/main/kotlin/nebulosa/api/cameras/CameraStartCaptureRequest.kt b/api/src/main/kotlin/nebulosa/api/cameras/CameraStartCaptureRequest.kt index a1d8c45e8..2633caada 100644 --- a/api/src/main/kotlin/nebulosa/api/cameras/CameraStartCaptureRequest.kt +++ b/api/src/main/kotlin/nebulosa/api/cameras/CameraStartCaptureRequest.kt @@ -1,10 +1,12 @@ package nebulosa.api.cameras import com.fasterxml.jackson.databind.annotation.JsonDeserialize +import com.fasterxml.jackson.databind.annotation.JsonSerialize import jakarta.validation.Valid import jakarta.validation.constraints.Positive import jakarta.validation.constraints.PositiveOrZero import nebulosa.api.beans.converters.time.DurationDeserializer +import nebulosa.api.beans.converters.time.DurationInMicrosecondsSerializer import nebulosa.api.guiding.DitherAfterExposureRequest import nebulosa.indi.device.camera.FrameType import org.hibernate.validator.constraints.Range @@ -16,32 +18,34 @@ import java.time.Duration import java.time.temporal.ChronoUnit data class CameraStartCaptureRequest( - val enabled: Boolean = true, + @JvmField val enabled: Boolean = true, // Capture. - @field:DurationMin(nanos = 1000L) @field:DurationMax(minutes = 60L) val exposureTime: Duration = Duration.ZERO, - @field:Range(min = 0L, max = 1000L) val exposureAmount: Int = 1, // 0 = looping + @field:DurationMin(nanos = 1000L) @field:DurationMax(minutes = 60L) @field:JsonSerialize(using = DurationInMicrosecondsSerializer::class) + @JvmField val exposureTime: Duration = Duration.ZERO, + @field:Range(min = 0L, max = 1000L) @JvmField val exposureAmount: Int = 1, // 0 = looping @field:JsonDeserialize(using = DurationDeserializer::class) @field:DurationUnit(ChronoUnit.SECONDS) - @field:DurationMin(nanos = 0L) @field:DurationMax(seconds = 60L) val exposureDelay: Duration = Duration.ZERO, - @field:PositiveOrZero val x: Int = 0, - @field:PositiveOrZero val y: Int = 0, - @field:PositiveOrZero val width: Int = 0, - @field:PositiveOrZero val height: Int = 0, - val frameFormat: String? = null, - val frameType: FrameType = FrameType.LIGHT, - @field:Positive val binX: Int = 1, - @field:Positive val binY: Int = 1, - @field:PositiveOrZero val gain: Int = 0, - @field:PositiveOrZero val offset: Int = 0, - val autoSave: Boolean = false, - val savePath: Path? = null, - val autoSubFolderMode: AutoSubFolderMode = AutoSubFolderMode.OFF, - @field:Valid val dither: DitherAfterExposureRequest = DitherAfterExposureRequest.DISABLED, - val calibrationGroup: String? = null, + @field:DurationMin(nanos = 0L) @field:DurationMax(seconds = 60L) @field:JsonSerialize(using = DurationInMicrosecondsSerializer::class) + @JvmField val exposureDelay: Duration = Duration.ZERO, + @field:PositiveOrZero @JvmField val x: Int = 0, + @field:PositiveOrZero @JvmField val y: Int = 0, + @field:PositiveOrZero @JvmField val width: Int = 0, + @field:PositiveOrZero @JvmField val height: Int = 0, + @JvmField val frameFormat: String? = null, + @JvmField val frameType: FrameType = FrameType.LIGHT, + @field:Positive @JvmField val binX: Int = 1, + @field:Positive @JvmField val binY: Int = 1, + @field:PositiveOrZero @JvmField val gain: Int = 0, + @field:PositiveOrZero @JvmField val offset: Int = 0, + @JvmField val autoSave: Boolean = false, + @JvmField val savePath: Path? = null, + @JvmField val autoSubFolderMode: AutoSubFolderMode = AutoSubFolderMode.OFF, + @field:Valid @JvmField val dither: DitherAfterExposureRequest = DitherAfterExposureRequest.DISABLED, + @JvmField val calibrationGroup: String? = null, // Filter Wheel. - val filterPosition: Int = 0, - val shutterPosition: Int = 0, + @JvmField val filterPosition: Int = 0, + @JvmField val shutterPosition: Int = 0, // Focuser. - val focusOffset: Int = 0, + @JvmField val focusOffset: Int = 0, ) { inline val isLoop diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/SequencerExecutor.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SequencerExecutor.kt index 86afc0e7b..0cc511f54 100644 --- a/api/src/main/kotlin/nebulosa/api/sequencer/SequencerExecutor.kt +++ b/api/src/main/kotlin/nebulosa/api/sequencer/SequencerExecutor.kt @@ -48,16 +48,16 @@ class SequencerExecutor( wheel: FilterWheel? = null, focuser: Focuser? = null, ) { check(camera.connected) { "${camera.name} Camera is not connected" } - check(jobs.any { it.task.camera === camera }) { "${camera.name} Sequencer Job is already in progress" } + check(jobs.none { it.task.camera === camera }) { "${camera.name} Sequencer Job is already in progress" } if (wheel != null) { check(wheel.connected) { "${wheel.name} Wheel is not connected" } - check(jobs.any { it.task.wheel === wheel }) { "${camera.name} Sequencer Job is already in progress" } + check(jobs.none { it.task.wheel === wheel }) { "${camera.name} Sequencer Job is already in progress" } } if (focuser != null) { check(focuser.connected) { "${focuser.name} Focuser is not connected" } - check(jobs.any { it.task.focuser === focuser }) { "${camera.name} Sequencer Job is already in progress" } + check(jobs.none { it.task.focuser === focuser }) { "${camera.name} Sequencer Job is already in progress" } } val task = SequencerTask(camera, request, guider) diff --git a/api/src/main/kotlin/nebulosa/api/sequencer/SequencerTask.kt b/api/src/main/kotlin/nebulosa/api/sequencer/SequencerTask.kt index 494af1b99..daf796613 100644 --- a/api/src/main/kotlin/nebulosa/api/sequencer/SequencerTask.kt +++ b/api/src/main/kotlin/nebulosa/api/sequencer/SequencerTask.kt @@ -49,11 +49,8 @@ data class SequencerTask( initialDelayTask.subscribe(this) - fun mapRequest(request: CameraStartCaptureRequest, exposureAmount: Int = request.exposureAmount): CameraStartCaptureRequest { - return request.copy( - savePath = plan.savePath, autoSave = true, autoSubFolderMode = plan.autoSubFolderMode, - exposureAmount = exposureAmount - ) + fun mapRequest(request: CameraStartCaptureRequest): CameraStartCaptureRequest { + return request.copy(savePath = plan.savePath, autoSave = true, autoSubFolderMode = plan.autoSubFolderMode) } if (plan.captureMode == SequenceCaptureMode.FULLY || usedEntries.size == 1) { @@ -74,8 +71,8 @@ data class SequencerTask( } } else { val sequenceIdTasks = usedEntries.map { SequencerIdTask(plan.entries.indexOf(it) + 1) } - val requests = usedEntries.map { mapRequest(it, 1) } - val cameraCaptureTasks = requests.mapIndexed { i, req -> CameraCaptureTask(camera, req, guider, i > 0) } + val requests = usedEntries.map { mapRequest(it) } + val cameraCaptureTasks = requests.mapIndexed { i, req -> CameraCaptureTask(camera, req, guider, i > 0, 1) } val wheelMoveTasks = requests.map { it.wheelMoveTask() } val count = IntArray(requests.size) { usedEntries[it].exposureAmount } diff --git a/api/src/main/kotlin/nebulosa/api/tasks/Job.kt b/api/src/main/kotlin/nebulosa/api/tasks/Job.kt index 0665c9426..3207eb97d 100644 --- a/api/src/main/kotlin/nebulosa/api/tasks/Job.kt +++ b/api/src/main/kotlin/nebulosa/api/tasks/Job.kt @@ -2,7 +2,7 @@ package nebulosa.api.tasks import nebulosa.common.concurrency.cancel.CancellationToken import java.util.concurrent.CompletableFuture -import java.util.concurrent.Executor +import java.util.concurrent.atomic.AtomicBoolean abstract class Job : CompletableFuture(), Runnable { @@ -11,13 +11,19 @@ abstract class Job : CompletableFuture(), Runnable { abstract val name: String private val cancellationToken = CancellationToken() + private val running = AtomicBoolean() @Volatile private var thread: Thread? = null + val isRunning + get() = running.get() + final override fun run() { try { + running.set(true) task.execute(cancellationToken) } finally { + running.set(false) thread = null cancellationToken.close() complete(Unit) @@ -28,25 +34,20 @@ abstract class Job : CompletableFuture(), Runnable { /** * Runs this Job in a new thread. */ + @Synchronized fun start() { - thread = Thread(this, name) - thread!!.isDaemon = false - thread!!.start() - } - - /** - * Runs this Job using the [executor]. - */ - fun start(executor: Executor) { - executor.execute(this) + if (thread == null && !running.get()) { + thread = Thread(this, name) + thread!!.isDaemon = false + thread!!.start() + } } /** - * Stops immediately this Job. + * Stops gracefully this Job. */ fun stop() { cancellationToken.cancel() - task.close() } /** diff --git a/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardExecutor.kt b/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardExecutor.kt index fb06044f6..55b55076c 100644 --- a/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardExecutor.kt +++ b/api/src/main/kotlin/nebulosa/api/wizard/flat/FlatWizardExecutor.kt @@ -29,7 +29,7 @@ class FlatWizardExecutor( fun execute(camera: Camera, request: FlatWizardRequest) { check(camera.connected) { "camera is not connected" } - check(jobs.any { it.task.camera === camera }) { "${camera.name} Flat Wizard is already in progress" } + check(jobs.none { it.task.camera === camera }) { "${camera.name} Flat Wizard is already in progress" } val task = FlatWizardTask(camera, request) task.subscribe(this) diff --git a/api/src/test/kotlin/APITest.kt b/api/src/test/kotlin/APITest.kt new file mode 100644 index 000000000..945491de6 --- /dev/null +++ b/api/src/test/kotlin/APITest.kt @@ -0,0 +1,83 @@ +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +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 nebulosa.api.cameras.CameraStartCaptureRequest +import nebulosa.common.json.PathSerializer +import nebulosa.test.NonGitHubOnlyCondition +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.logging.HttpLoggingInterceptor +import java.nio.file.Path +import java.time.Duration + +@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") } + "Disconnect" { delete("connection") } + } + + companion object { + + private const val BASE_URL = "http://localhost:7000" + private const val CAMERA_NAME = "CCD 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 CLIENT = OkHttpClient.Builder() + .addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY)) + .build() + + @JvmStatic private val KOTLIN_MODULE = kotlinModule().addSerializer(PathSerializer) + + @JvmStatic private val OBJECT_MAPPER = ObjectMapper() + .registerModule(JavaTimeModule()) + .registerModule(KOTLIN_MODULE) + + @JvmStatic private val APPLICATION_JSON = "application/json".toMediaType() + @JvmStatic private val EMPTY_BODY = ByteArray(0).toRequestBody(APPLICATION_JSON) + + @JvmStatic + private fun get(path: String) { + val request = Request.Builder().get().url("$BASE_URL/$path").build() + CLIENT.newCall(request).execute().use { it.isSuccessful.shouldBeTrue() } + } + + @JvmStatic + private fun put(path: String, body: RequestBody = EMPTY_BODY) { + val request = Request.Builder().put(body).url("$BASE_URL/$path").build() + CLIENT.newCall(request).execute().use { it.isSuccessful.shouldBeTrue() } + } + + @JvmStatic + 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() } + } + + @JvmStatic + private fun delete(path: String) { + val request = Request.Builder().delete().url("$BASE_URL/$path").build() + CLIENT.newCall(request).execute().use { it.isSuccessful.shouldBeTrue() } + } + } +} diff --git a/data/.gitignore b/data/.gitignore index 6642278fe..b39285bd2 100644 --- a/data/.gitignore +++ b/data/.gitignore @@ -1,3 +1,4 @@ simbad/ astrobin/ test/ +captures/ diff --git a/desktop/src/shared/components/camera-exposure/camera-exposure.component.html b/desktop/src/shared/components/camera-exposure/camera-exposure.component.html index 00c2f6c98..e796bf397 100644 --- a/desktop/src/shared/components/camera-exposure/camera-exposure.component.html +++ b/desktop/src/shared/components/camera-exposure/camera-exposure.component.html @@ -7,9 +7,11 @@ {{ capture.count }} / {{ capture.amount }} + @if (!capture.looping) { {{ capture.progress * 100 | number:'1.1-1' }} + } @if (capture.looping) { {{ capture.elapsedTime | exposureTime }} @@ -17,16 +19,24 @@ } @else { - {{ capture.remainingTime | exposureTime }} - {{ capture.elapsedTime | exposureTime }} + + {{ capture.remainingTime | exposureTime }} + + + {{ capture.elapsedTime | exposureTime }} + } @if (state === 'EXPOSURING' || state === 'WAITING') { - {{ step.remainingTime | exposureTime }} - {{ step.elapsedTime | exposureTime }} + + {{ step.remainingTime | exposureTime }} + + + {{ step.elapsedTime | exposureTime }} + {{ step.progress * 100 | number:'1.1-1' }} diff --git a/desktop/src/shared/components/camera-exposure/camera-exposure.component.ts b/desktop/src/shared/components/camera-exposure/camera-exposure.component.ts index 265890f4e..801980f7a 100644 --- a/desktop/src/shared/components/camera-exposure/camera-exposure.component.ts +++ b/desktop/src/shared/components/camera-exposure/camera-exposure.component.ts @@ -42,14 +42,13 @@ export class CameraExposureComponent { this.state = 'EXPOSURING' } else if (event.state === 'EXPOSURE_STARTED') { this.state = 'EXPOSURING' - } else if (event.state === 'IDLE' || - (!looping && event.state === 'CAPTURE_FINISHED') || - (!this.capture.looping && !this.capture.remainingTime)) { - this.state = 'IDLE' + } else if (event.state === 'IDLE' || event.state === 'CAPTURE_FINISHED') { + this.reset() } - return this.state !== undefined && this.state !== 'CAPTURE_FINISHED' - && this.state !== 'IDLE' && !event.aborted + return this.state !== undefined + && this.state !== 'CAPTURE_FINISHED' + && this.state !== 'IDLE' } reset() { diff --git a/desktop/src/shared/types/camera.types.ts b/desktop/src/shared/types/camera.types.ts index 33f2066e1..9d3cc2d40 100644 --- a/desktop/src/shared/types/camera.types.ts +++ b/desktop/src/shared/types/camera.types.ts @@ -212,7 +212,6 @@ export interface CameraCaptureElapsed extends MessageEvent { stepRemainingTime: number savePath?: string state: CameraCaptureState - aborted?: boolean } export type CameraCaptureState = 'IDLE' | 'CAPTURE_STARTED' | 'EXPOSURE_STARTED' | 'EXPOSURING' | 'WAITING' | 'SETTLING' | 'EXPOSURE_FINISHED' | 'CAPTURE_FINISHED' diff --git a/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/device/cameras/ASCOMCamera.kt b/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/device/cameras/ASCOMCamera.kt index 7a3e0a3cd..9901e9ade 100644 --- a/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/device/cameras/ASCOMCamera.kt +++ b/nebulosa-alpaca-indi/src/main/kotlin/nebulosa/alpaca/indi/device/cameras/ASCOMCamera.kt @@ -348,7 +348,7 @@ data class ASCOMCamera( } if (prevExposuring != exposuring) sender.fireOnEventReceived(CameraExposuringChanged(this)) - if (prevExposureState != exposureState) sender.fireOnEventReceived(CameraExposureStateChanged(this, prevExposureState)) + if (prevExposureState != exposureState) sender.fireOnEventReceived(CameraExposureStateChanged(this)) if (exposuring) { service.percentCompleted(device.number).doRequest { diff --git a/nebulosa-guiding-phd2/src/main/kotlin/nebulosa/guiding/phd2/PHD2Guider.kt b/nebulosa-guiding-phd2/src/main/kotlin/nebulosa/guiding/phd2/PHD2Guider.kt index 2cb26cef0..81c33ab44 100644 --- a/nebulosa-guiding-phd2/src/main/kotlin/nebulosa/guiding/phd2/PHD2Guider.kt +++ b/nebulosa-guiding-phd2/src/main/kotlin/nebulosa/guiding/phd2/PHD2Guider.kt @@ -34,7 +34,7 @@ class PHD2Guider(private val client: PHD2Client) : Guider, PHD2EventListener { } override val isSettling - get() = settling.get() + get() = !settling.get() init { client.registerListener(this) diff --git a/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/cameras/INDICamera.kt b/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/cameras/INDICamera.kt index cd3de6e97..357793833 100644 --- a/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/cameras/INDICamera.kt +++ b/nebulosa-indi-client/src/main/kotlin/nebulosa/indi/client/device/cameras/INDICamera.kt @@ -161,12 +161,12 @@ internal open class INDICamera( sender.fireOnEventReceived(CameraExposureAborted(this)) } else if (exposureState == PropertyState.OK && prevExposureState == PropertyState.BUSY) { sender.fireOnEventReceived(CameraExposureFinished(this)) - } else if (exposureState == PropertyState.ALERT && prevExposureState != PropertyState.ALERT) { + } else if (exposureState == PropertyState.ALERT) { sender.fireOnEventReceived(CameraExposureFailed(this)) } if (prevExposureState != exposureState) { - sender.fireOnEventReceived(CameraExposureStateChanged(this, prevExposureState)) + sender.fireOnEventReceived(CameraExposureStateChanged(this)) } } } @@ -331,6 +331,11 @@ internal open class INDICamera( override fun startCapture(exposureTime: Duration) { sendNewSwitch("CCD_TRANSFER_FORMAT", "FORMAT_FITS" to true) + if (exposureState != PropertyState.IDLE) { + exposureState = PropertyState.IDLE + sender.fireOnEventReceived(CameraExposureStateChanged(this)) + } + val exposureInSeconds = exposureTime.toNanos() / NANO_TO_SECONDS if (this is GuideHead) { diff --git a/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/camera/CameraExposureStateChanged.kt b/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/camera/CameraExposureStateChanged.kt index d9ac8b8b0..1eacc1dc5 100644 --- a/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/camera/CameraExposureStateChanged.kt +++ b/nebulosa-indi-device/src/main/kotlin/nebulosa/indi/device/camera/CameraExposureStateChanged.kt @@ -1,9 +1,5 @@ package nebulosa.indi.device.camera import nebulosa.indi.device.PropertyChangedEvent -import nebulosa.indi.protocol.PropertyState -data class CameraExposureStateChanged( - override val device: Camera, - val previousState: PropertyState, -) : CameraEvent, PropertyChangedEvent +data class CameraExposureStateChanged(override val device: Camera) : CameraEvent, PropertyChangedEvent