Skip to content

Commit

Permalink
[api][desktop]: Support Auto Focus
Browse files Browse the repository at this point in the history
  • Loading branch information
tiagohm committed May 28, 2024
1 parent a2b5000 commit df472e9
Show file tree
Hide file tree
Showing 16 changed files with 323 additions and 133 deletions.
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
package nebulosa.api.autofocus

import nebulosa.api.cameras.CameraStartCaptureRequest
import nebulosa.api.focusers.BacklashCompensation

data class AutoFocusRequest(
@JvmField val fittingMode: AutoFocusFittingMode = AutoFocusFittingMode.HYPERBOLIC,
@JvmField val capture: CameraStartCaptureRequest = CameraStartCaptureRequest.EMPTY,
@JvmField val rSquaredThreshold: Double = 0.7,
@JvmField val backlashCompensationMode: BacklashCompensationMode = BacklashCompensationMode.OVERSHOOT,
@JvmField val backlashIn: Int = 0,
@JvmField val backlashOut: Int = 0,
@JvmField val backlashCompensation: BacklashCompensation = BacklashCompensation.EMPTY,
@JvmField val initialOffsetSteps: Int = 4,
@JvmField val stepSize: Int = 50,
@JvmField val totalNumberOfAttempts: Int = 1,
Expand Down
18 changes: 9 additions & 9 deletions api/src/main/kotlin/nebulosa/api/autofocus/AutoFocusTask.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@ package nebulosa.api.autofocus

import io.reactivex.rxjava3.functions.Consumer
import nebulosa.api.cameras.*
import nebulosa.api.focusers.BacklashCompensationFocuserMoveTask
import nebulosa.api.focusers.BacklashCompensationMode
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
import nebulosa.api.tasks.AbstractTask
Expand Down Expand Up @@ -56,11 +55,12 @@ data class AutoFocusTask(
frameType = FrameType.LIGHT, autoSave = false, autoSubFolderMode = AutoSubFolderMode.OFF
)


private val focusPoints = ArrayList<CurvePoint>()
private val measurements = ArrayList<MeasuredStars>(request.capture.exposureAmount)
private val cameraCaptureTask = CameraCaptureTask(camera, cameraRequest, exposureMaxRepeat = max(1, request.capture.exposureAmount))
private val focuserMoveTask = BacklashCompensationFocuserMoveTask(focuser, 0, request.backlashCompensation)

@Volatile private var focuserMoveTask: FocuserMoveTask? = null
@Volatile private var trendLineCurve: TrendLineFitting.Curve? = null
@Volatile private var parabolicCurve: Lazy<QuadraticFitting.Curve>? = null
@Volatile private var hyperbolicCurve: Lazy<HyperbolicFitting.Curve>? = null
Expand All @@ -76,7 +76,7 @@ data class AutoFocusTask(
}

override fun handleFocuserEvent(event: FocuserEvent) {
focuserMoveTask?.handleFocuserEvent(event)
focuserMoveTask.handleFocuserEvent(event)
}

override fun canUseAsLastEvent(event: MessageEvent) = event is AutoFocusEvent
Expand All @@ -88,7 +88,8 @@ 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
val reverse = request.backlashCompensation.mode == BacklashCompensationMode.OVERSHOOT && request.backlashCompensation.backlashIn > 0 &&
request.backlashCompensation.backlashOut == 0

LOG.info("Auto Focus started. initialHFD={}, reverse={}, camera={}, focuser={}", initialHFD, reverse, camera, focuser)

Expand Down Expand Up @@ -354,10 +355,9 @@ data class AutoFocusTask(
}

private fun moveFocuser(position: Int, cancellationToken: CancellationToken, relative: Boolean): Int {
focuserMoveTask = if (relative) FocuserMoveRelativeTask(focuser, position)
else FocuserMoveAbsoluteTask(focuser, position)
sendEvent(AutoFocusState.MOVING)
focuserMoveTask!!.execute(cancellationToken)
focuserMoveTask.position = if (relative) focuser.position + position else position
focuserMoveTask.execute(cancellationToken)
return focuser.position
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package nebulosa.api.focusers

import nebulosa.common.concurrency.cancel.CancellationListener
import nebulosa.common.concurrency.cancel.CancellationSource
import nebulosa.common.concurrency.cancel.CancellationToken
import nebulosa.common.concurrency.latch.CountUpDownLatch
import nebulosa.indi.device.focuser.FocuserEvent
import nebulosa.indi.device.focuser.FocuserMoveFailed
import nebulosa.indi.device.focuser.FocuserPositionChanged
import nebulosa.log.loggerFor

abstract class AbstractFocuserMoveTask : FocuserMoveTask, CancellationListener {

@JvmField protected val latch = CountUpDownLatch()

@Volatile private var initialPosition = 0

override fun handleFocuserEvent(event: FocuserEvent) {
if (event.device === focuser) {
when (event) {
is FocuserPositionChanged -> if (focuser.position != initialPosition && !focuser.moving) latch.reset()
is FocuserMoveFailed -> latch.reset()
}
}
}

protected abstract fun canMove(): Boolean

protected abstract fun move()

override fun execute(cancellationToken: CancellationToken) {
if (!cancellationToken.isCancelled && focuser.connected && !focuser.moving && canMove()) {
try {
cancellationToken.listen(this)
initialPosition = focuser.position
LOG.info("Focuser move started. focuser={}", focuser)
latch.countUp()
move()
latch.await()
} finally {
cancellationToken.unlisten(this)
LOG.info("Focuser move finished. focuser={}", focuser)
}
}
}

override fun onCancel(source: CancellationSource) {
focuser.abortFocus()
latch.reset()
}

companion object {

@JvmStatic private val LOG = loggerFor<AbstractFocuserMoveTask>()
}
}
13 changes: 13 additions & 0 deletions api/src/main/kotlin/nebulosa/api/focusers/BacklashCompensation.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package nebulosa.api.focusers

data class BacklashCompensation(
@JvmField val mode: BacklashCompensationMode = BacklashCompensationMode.OVERSHOOT,
@JvmField val backlashIn: Int = 0,
@JvmField val backlashOut: Int = 0,
) {

companion object {

@JvmStatic val EMPTY = BacklashCompensation()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package nebulosa.api.focusers

import nebulosa.common.concurrency.cancel.CancellationToken
import nebulosa.indi.device.focuser.Focuser
import nebulosa.indi.device.focuser.FocuserEvent
import nebulosa.log.loggerFor

/**
* This decorator will wrap an absolute backlash [compensation] model around the [focuser].
* On each move an absolute backlash compensation value will be applied, if the focuser changes its moving direction
* The returned position will then accommodate for this backlash and simulating the position without backlash.
*/
data class BacklashCompensationFocuserMoveTask(
override val focuser: Focuser,
@JvmField @Volatile var position: Int,
@JvmField val compensation: BacklashCompensation,
) : FocuserMoveTask {

enum class OvershootDirection {
NONE,
IN,
OUT,
}

@Volatile private var offset = 0
@Volatile private var lastDirection = OvershootDirection.NONE

private val task = FocuserMoveAbsoluteTask(focuser, 0)

/**
* Returns the adjusted position based on the amount of backlash compensation.
*/
val adjustedPosition
get() = focuser.position - offset

override fun handleFocuserEvent(event: FocuserEvent) {
task.handleFocuserEvent(event)
}

override fun execute(cancellationToken: CancellationToken) {
if (!cancellationToken.isCancelled && focuser.connected && !focuser.moving) {
val startPosition = focuser.position

if (compensation.mode == BacklashCompensationMode.ABSOLUTE) {
val adjustedTargetPosition = position + offset

val finalizedTargetPosition = if (adjustedTargetPosition < 0) {
offset = 0
0
} else if (adjustedTargetPosition > focuser.maxPosition) {
offset = 0
focuser.maxPosition
} else {
val backlashCompensation = calculateAbsoluteBacklashCompensation(startPosition, adjustedTargetPosition)
offset += backlashCompensation
adjustedTargetPosition + backlashCompensation
}

moveFocuser(finalizedTargetPosition, cancellationToken)
} else {
val backlashCompensation = calculateOvershootBacklashCompensation(startPosition, position)

if (backlashCompensation != 0) {
val overshoot = position + backlashCompensation

if (overshoot < 0) {
LOG.info("overshooting position is below minimum 0, skipping overshoot")
} else if (overshoot > focuser.maxPosition) {
LOG.info("overshooting position is above maximum ${focuser.maxPosition}, skipping overshoot")
} else {
LOG.info("overshooting from $startPosition to overshoot position $overshoot using a compensation of $backlashCompensation")

moveFocuser(overshoot, cancellationToken)

LOG.info("moving back to position $position")
}
}

moveFocuser(position, cancellationToken)
}
}
}

private fun moveFocuser(position: Int, cancellationToken: CancellationToken) {
if (position > 0 && position <= focuser.maxPosition) {
lastDirection = determineMovingDirection(focuser.position, position)
task.position = position
task.execute(cancellationToken)
}
}

override fun reset() {
task.reset()

offset = 0
lastDirection = OvershootDirection.NONE
}

override fun close() {
task.close()
}

private fun determineMovingDirection(prevPosition: Int, newPosition: Int): OvershootDirection {
return if (newPosition > prevPosition) OvershootDirection.OUT
else if (newPosition < prevPosition) OvershootDirection.IN
else lastDirection
}

private fun calculateAbsoluteBacklashCompensation(lastPosition: Int, newPosition: Int): Int {
val direction = determineMovingDirection(lastPosition, newPosition)

return if (direction == OvershootDirection.IN && lastDirection == OvershootDirection.OUT) {
LOG.info("Focuser is reversing direction from outwards to inwards")
-compensation.backlashIn
} else if (direction == OvershootDirection.OUT && lastDirection === OvershootDirection.IN) {
LOG.info("Focuser is reversing direction from inwards to outwards")
compensation.backlashOut
} else {
0
}
}

private fun calculateOvershootBacklashCompensation(lastPosition: Int, newPosition: Int): Int {
val direction = determineMovingDirection(lastPosition, newPosition)

return if (direction == OvershootDirection.IN && compensation.backlashIn != 0) -compensation.backlashIn
else if (direction == OvershootDirection.OUT && compensation.backlashOut != 0) compensation.backlashOut
else 0
}

companion object {

@JvmStatic private val LOG = loggerFor<BacklashCompensationFocuserMoveTask>()
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package nebulosa.api.autofocus
package nebulosa.api.focusers

enum class BacklashCompensationMode {
ABSOLUTE,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,63 +1,18 @@
package nebulosa.api.focusers

import nebulosa.common.concurrency.cancel.CancellationListener
import nebulosa.common.concurrency.cancel.CancellationSource
import nebulosa.common.concurrency.cancel.CancellationToken
import nebulosa.common.concurrency.latch.CountUpDownLatch
import nebulosa.indi.device.focuser.Focuser
import nebulosa.indi.device.focuser.FocuserEvent
import nebulosa.indi.device.focuser.FocuserMoveFailed
import nebulosa.indi.device.focuser.FocuserPositionChanged
import nebulosa.log.loggerFor
import kotlin.math.abs

data class FocuserMoveAbsoluteTask(
override val focuser: Focuser,
@JvmField val position: Int,
) : FocuserMoveTask, CancellationListener {
@JvmField @Volatile var position: Int,
) : AbstractFocuserMoveTask() {

private val latch = CountUpDownLatch()
override fun canMove() = position != focuser.position && position > 0 && position < focuser.maxPosition

override fun handleFocuserEvent(event: FocuserEvent) {
if (event.device === focuser) {
when (event) {
is FocuserPositionChanged -> if (focuser.position == position) latch.reset()
is FocuserMoveFailed -> latch.reset()
}
}
}

override fun execute(cancellationToken: CancellationToken) {
if (!cancellationToken.isCancelled && focuser.connected
&& !focuser.moving && position != focuser.position
) {
try {
cancellationToken.listen(this)

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))
else focuser.moveFocusOut(abs(focuser.position - position))

latch.await()
} finally {
cancellationToken.unlisten(this)
}

LOG.info("Focuser move finished. position={}, focuser={}", position, focuser)
}
}

override fun onCancel(source: CancellationSource) {
focuser.abortFocus()
latch.reset()
}

companion object {

@JvmStatic private val LOG = loggerFor<FocuserMoveAbsoluteTask>()
override fun move() {
if (focuser.canAbsoluteMove) focuser.moveFocusTo(position)
else if (position < focuser.position) focuser.moveFocusIn(abs(position - focuser.position))
else focuser.moveFocusOut(abs(position - focuser.position))
}
}
Loading

0 comments on commit df472e9

Please sign in to comment.