Skip to content

Commit

Permalink
[api][desktop]: Fix Camera Capture
Browse files Browse the repository at this point in the history
  • Loading branch information
tiagohm committed May 3, 2024
1 parent ba5e62b commit e2a6d31
Show file tree
Hide file tree
Showing 21 changed files with 208 additions and 101 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -19,5 +20,4 @@ data class CameraCaptureEvent(
) : MessageEvent {

override val eventName = "CAMERA.CAPTURE_ELAPSED"

}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
43 changes: 27 additions & 16 deletions api/src/main/kotlin/nebulosa/api/cameras/CameraCaptureTask.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<CameraCaptureEvent>(), Consumer<Any> {

private val delayTask = DelayTask(request.exposureDelay)
Expand All @@ -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)
Expand All @@ -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)
}
Expand All @@ -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
Expand Down Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,4 @@ enum class CameraExposureState {
STARTED,
ELAPSED,
FINISHED,
ABORTED,
}
24 changes: 13 additions & 11 deletions api/src/main/kotlin/nebulosa/api/cameras/CameraExposureTask.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -57,14 +58,16 @@ 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()

state = CameraExposureState.STARTED
sendEvent()

with(camera) {
enableBlob()

if (request.width > 0 && request.height > 0) {
frame(request.x, request.y, request.width, request.height)
}
Expand All @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
11 changes: 4 additions & 7 deletions api/src/main/kotlin/nebulosa/api/sequencer/SequencerTask.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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 }

Expand Down
Loading

0 comments on commit e2a6d31

Please sign in to comment.