File indexing completed on 2024-05-05 15:55:06
0001 /* 0002 SPDX-FileCopyrightText: 2023 John Evans <john.e.evans.email@googlemail.com> 0003 0004 SPDX-License-Identifier: GPL-2.0-or-later 0005 */ 0006 0007 #include "adaptivefocus.h" 0008 #include <kstars_debug.h> 0009 #include "kstars.h" 0010 #include "Options.h" 0011 0012 namespace Ekos 0013 { 0014 0015 AdaptiveFocus::AdaptiveFocus(Focus *_focus) : m_focus(_focus) 0016 { 0017 if (m_focus == nullptr) 0018 qCDebug(KSTARS_EKOS_FOCUS) << "AdaptiveFocus constructed with null focus ptr"; 0019 } 0020 0021 AdaptiveFocus::~AdaptiveFocus() 0022 { 0023 } 0024 0025 // Start an Adaptive Focus run 0026 void AdaptiveFocus::runAdaptiveFocus(const int currentPosition, const QString &filter) 0027 { 0028 if (!m_focus) 0029 { 0030 qCDebug(KSTARS_EKOS_FOCUS) << "runAdaptiveFocus called but focus ptr is null. Ignoring."; 0031 adaptiveFocusAdmin(currentPosition, false, false); 0032 return; 0033 } 0034 0035 if (!m_focus->m_FilterManager) 0036 { 0037 qCDebug(KSTARS_EKOS_FOCUS) << "runAdaptiveFocus called but focus filterManager is null. Ignoring."; 0038 adaptiveFocusAdmin(currentPosition, false, false); 0039 return; 0040 } 0041 0042 if (!m_focus->m_OpsFocusSettings->focusAdaptive->isChecked()) 0043 { 0044 qCDebug(KSTARS_EKOS_FOCUS) << "runAdaptiveFocus called but focusAdaptive->isChecked is false. Ignoring."; 0045 adaptiveFocusAdmin(currentPosition, false, false); 0046 return; 0047 } 0048 0049 if (inAdaptiveFocus()) 0050 { 0051 qCDebug(KSTARS_EKOS_FOCUS) << "runAdaptiveFocus called whilst already inAdaptiveFocus. Ignoring."; 0052 adaptiveFocusAdmin(currentPosition, false, false); 0053 return; 0054 } 0055 0056 if (m_focus->inAutoFocus || m_focus->inFocusLoop || m_focus->inAdjustFocus || m_focus->inBuildOffsets) 0057 { 0058 qCDebug(KSTARS_EKOS_FOCUS) << "adaptiveFocus called whilst other focus activity in progress. Ignoring."; 0059 adaptiveFocusAdmin(currentPosition, false, false); 0060 return; 0061 } 0062 0063 setInAdaptiveFocus(true); 0064 m_ThisAdaptiveFocusStartPos = currentPosition; 0065 0066 // Get the reference data to be used by Adaptive Focus 0067 int refPosition; 0068 QString adaptiveFilter = getAdaptiveFilter(filter); 0069 0070 // If the filter has been changed since the last Adaptive Focus iteration, reset 0071 if (filter != m_LastAdaptiveFilter) 0072 { 0073 resetAdaptiveFocusCounters(); 0074 m_LastAdaptiveFilter = filter; 0075 } 0076 if (!m_focus->m_FilterManager->getFilterAbsoluteFocusDetails(adaptiveFilter, refPosition, 0077 m_focus->m_LastSourceAutofocusTemperature, 0078 m_focus->m_LastSourceAutofocusAlt)) 0079 { 0080 qCDebug(KSTARS_EKOS_FOCUS) << "runAdaptiveFocus unable to get last Autofocus details. Ignoring."; 0081 adaptiveFocusAdmin(currentPosition, false, false); 0082 return; 0083 } 0084 0085 // If we are using a lock filter then adjust the reference position by the offset 0086 refPosition += getAdaptiveFilterOffset(filter, adaptiveFilter); 0087 0088 // Find out if there is anything to do for temperature, firstly do we have a valid temperature source 0089 double currentTemp = INVALID_VALUE; 0090 double tempTicksDelta = 0.0, tempTicksDeltaLast = 0.0; 0091 if (m_focus->currentTemperatureSourceElement && m_focus->m_LastSourceAutofocusTemperature != INVALID_VALUE) 0092 { 0093 if (m_LastAdaptiveFocusTemperature == INVALID_VALUE) 0094 // 1st Adaptive Focus run so no previous results 0095 m_LastAdaptiveFocusTemperature = m_focus->m_LastSourceAutofocusTemperature; 0096 0097 currentTemp = m_focus->currentTemperatureSourceElement->value; 0098 0099 // Calculate the deltas since the last Autofocus and Last Adaptive Focus 0100 const double tempDelta = currentTemp - m_focus->m_LastSourceAutofocusTemperature; 0101 const double tempDeltaLast = m_LastAdaptiveFocusTemperature - m_focus->m_LastSourceAutofocusTemperature; 0102 0103 // Scale the temperature delta to number of ticks 0104 const double ticksPerTemp = m_focus->m_FilterManager->getFilterTicksPerTemp(adaptiveFilter); 0105 tempTicksDelta = ticksPerTemp * tempDelta; 0106 tempTicksDeltaLast = ticksPerTemp * tempDeltaLast; 0107 } 0108 0109 // Now check for altitude 0110 double currentAlt = INVALID_VALUE; 0111 double altTicksDelta = 0.0, altTicksDeltaLast = 0.0; 0112 bool altDimension = false; 0113 if (m_focus->m_LastSourceAutofocusAlt != INVALID_VALUE) 0114 { 0115 if (m_LastAdaptiveFocusAlt == INVALID_VALUE) 0116 // 1st Adaptive Focus run so no previous results 0117 m_LastAdaptiveFocusAlt = m_focus->m_LastSourceAutofocusAlt; 0118 0119 currentAlt = m_focus->mountAlt; 0120 0121 const double altDelta = currentAlt - m_focus->m_LastSourceAutofocusAlt; 0122 const double altDeltaLast = m_LastAdaptiveFocusAlt - m_focus->m_LastSourceAutofocusAlt; 0123 0124 // Scale the altitude delta to number of ticks 0125 const double ticksPerAlt = m_focus->m_FilterManager->getFilterTicksPerAlt(adaptiveFilter); 0126 altDimension = (abs(ticksPerAlt) > 0.001); 0127 altTicksDelta = ticksPerAlt * altDelta; 0128 altTicksDeltaLast = ticksPerAlt * altDeltaLast; 0129 } 0130 0131 // proposedPosition is where focuser should move; proposedPositionLast is where focuser should have moved in last Adaptive iterative 0132 int proposedPosition = refPosition + static_cast<int>(round(tempTicksDelta + altTicksDelta)); 0133 int proposedPositionLast = refPosition + static_cast<int>(round(tempTicksDeltaLast + altTicksDeltaLast)); 0134 int proposedMove = proposedPosition - currentPosition; 0135 0136 // We have the total movement, now work out the split by Temp, Alt and Pos Error 0137 m_ThisAdaptiveFocusTempTicks = tempTicksDelta - tempTicksDeltaLast; 0138 m_ThisAdaptiveFocusAltTicks = altTicksDelta - altTicksDeltaLast; 0139 0140 // If everything is going to plan the currentPosition will equal proposedPositionLast. If the focuser hasn't moved exactly to the 0141 // requested position, e.g. its 1 or 2 ticks away then we need to account for this Positioning Error. It will have been reported 0142 // on the last Adaptive run but we now need to reverse that positioning error out in the accounting for this run 0143 m_LastAdaptiveFocusPosErrorReversal = proposedPositionLast - currentPosition; 0144 0145 // There could be a rounding error, where, for example, a small change in temp/alt (e.g. 0.1 ticks) results in a 1 tick move 0146 m_ThisAdaptiveFocusRoundingError = proposedMove - m_LastAdaptiveFocusPosErrorReversal - 0147 static_cast<int>(round(m_ThisAdaptiveFocusTempTicks + m_ThisAdaptiveFocusAltTicks)); 0148 0149 // Check movement is above user defined minimum 0150 if (abs(proposedMove) < m_focus->m_OpsFocusSettings->focusAdaptiveMinMove->value()) 0151 { 0152 m_focus->appendLogText(i18n("Adaptive Focus: No movement (below threshold)")); 0153 adaptiveFocusAdmin(currentPosition, true, false); 0154 } 0155 else 0156 { 0157 // Now do some checks that the movement is permitted 0158 if (abs(m_focus->initialFocuserAbsPosition - proposedPosition) > m_focus->m_OpsFocusMechanics->focusMaxTravel->value()) 0159 { 0160 // We are about to move the focuser beyond focus max travel so don't 0161 // Suspend adaptive focusing, user can always re-enable, if required 0162 m_focus->m_OpsFocusSettings->focusAdaptive->setChecked(false); 0163 m_focus->appendLogText(i18n("Adaptive Focus suspended. Total movement would exceed Max Travel limit")); 0164 adaptiveFocusAdmin(currentPosition, false, false); 0165 } 0166 else if (abs(m_AdaptiveTotalMove + proposedMove) > m_focus->m_OpsFocusSettings->focusAdaptiveMaxMove->value()) 0167 { 0168 // We are about to move the focuser beyond adaptive focus max move so don't 0169 // Suspend adaptive focusing. User can always re-enable, if required 0170 m_focus->m_OpsFocusSettings->focusAdaptive->setChecked(false); 0171 m_focus->appendLogText(i18n("Adaptive Focus suspended. Total movement would exceed adaptive limit")); 0172 adaptiveFocusAdmin(currentPosition, false, false); 0173 } 0174 else 0175 { 0176 // For most folks, most of the time there won't be alt data or positioning errors, so don't continually report 0 0177 QString tempStr = QString("%1").arg(m_ThisAdaptiveFocusTempTicks, 0, 'f', 1); 0178 QString altStr = QString("%1").arg(m_ThisAdaptiveFocusAltTicks, 0, 'f', 1); 0179 QString text = i18n("Adaptive Focus: Moving from %1 to %2 (TempΔ %3", currentPosition, proposedPosition, tempStr); 0180 text = (!altDimension) ? text + i18n("") : text + i18n("; AltΔ %1", altStr); 0181 text = (m_LastAdaptiveFocusPosErrorReversal == 0) ? text + i18n(")") : text + i18n("; Pos Error %1)", 0182 m_LastAdaptiveFocusPosErrorReversal); 0183 m_focus->appendLogText(text); 0184 0185 // Go ahead and try to move the focuser. Admin tasks will be completed when the focuser move completes 0186 if (m_focus->changeFocus(proposedMove)) 0187 { 0188 // All good so update variables used after focuser move completes (see adaptiveFocusAdmin()) 0189 m_ThisAdaptiveFocusTemperature = currentTemp; 0190 m_ThisAdaptiveFocusAlt = currentAlt; 0191 m_AdaptiveFocusPositionReq = proposedPosition; 0192 m_AdaptiveTotalMove += proposedMove; 0193 } 0194 else 0195 { 0196 // Problem moving the focuser 0197 m_focus->appendLogText(i18n("Adaptive Focus unable to move focuser")); 0198 adaptiveFocusAdmin(currentPosition, false, false); 0199 } 0200 } 0201 } 0202 } 0203 0204 // When adaptiveFocus succeeds the focuser is moved and admin tasks are performed to inform other modules that adaptiveFocus is complete 0205 void AdaptiveFocus::adaptiveFocusAdmin(const int currentPosition, const bool success, const bool focuserMoved) 0206 { 0207 // Reset member variable 0208 setInAdaptiveFocus(false); 0209 0210 int thisPosError = 0; 0211 if (focuserMoved) 0212 { 0213 // Signal Capture that we are done - honour the focuser settle time after movement. 0214 QTimer::singleShot(m_focus->m_OpsFocusMechanics->focusSettleTime->value() * 1000, m_focus, [ &, success]() 0215 { 0216 emit m_focus->focusAdaptiveComplete(success); 0217 }); 0218 0219 // Check whether the focuser moved to the requested position or whether we have a positioning error (1 or 2 ticks for example) 0220 // If there is a positioning error update the total ticks to include the error. 0221 if (m_AdaptiveFocusPositionReq != INVALID_VALUE) 0222 { 0223 thisPosError = currentPosition - m_AdaptiveFocusPositionReq; 0224 m_AdaptiveTotalMove += thisPosError; 0225 } 0226 } 0227 else 0228 emit m_focus->focusAdaptiveComplete(success); 0229 0230 // Signal Analyze if success both for focuser moves and zero moves 0231 bool check = true; 0232 if (success) 0233 { 0234 // Note we send Analyze the active filter, not the Adaptive Filter (i.e. not the lock filter) 0235 int totalTicks = static_cast<int>(round(m_ThisAdaptiveFocusTempTicks + m_ThisAdaptiveFocusAltTicks)) + 0236 m_LastAdaptiveFocusPosErrorReversal + m_ThisAdaptiveFocusRoundingError + thisPosError; 0237 0238 emit m_focus->adaptiveFocusComplete(m_focus->filter(), m_ThisAdaptiveFocusTemperature, m_ThisAdaptiveFocusTempTicks, 0239 m_ThisAdaptiveFocusAlt, m_ThisAdaptiveFocusAltTicks, m_LastAdaptiveFocusPosErrorReversal, 0240 thisPosError, totalTicks, currentPosition, focuserMoved); 0241 0242 // Check that totalTicks movement is above minimum 0243 if (totalTicks < m_focus->m_OpsFocusSettings->focusAdaptiveMinMove->value()) 0244 totalTicks = 0; 0245 // Perform an accounting check that the numbers add up 0246 check = (m_ThisAdaptiveFocusStartPos + totalTicks == currentPosition); 0247 } 0248 0249 if (success && focuserMoved) 0250 { 0251 // Reset member variables in prep for the next Adaptive Focus run 0252 m_LastAdaptiveFocusTemperature = m_ThisAdaptiveFocusTemperature; 0253 m_LastAdaptiveFocusAlt = m_ThisAdaptiveFocusAlt; 0254 } 0255 0256 // Log a debug for each Adaptive Focus. 0257 qCDebug(KSTARS_EKOS_FOCUS) << "Adaptive Focus Stats: Filter:" << m_LastAdaptiveFilter 0258 << ", Adaptive Filter:" << getAdaptiveFilter(m_LastAdaptiveFilter) 0259 << ", Min Move:" << m_focus->m_OpsFocusSettings->focusAdaptiveMinMove->value() 0260 << ", success:" << (success ? "Yes" : "No") 0261 << ", focuserMoved:" << (focuserMoved ? "Yes" : "No") 0262 << ", Temp:" << m_ThisAdaptiveFocusTemperature 0263 << ", Temp ticks:" << m_ThisAdaptiveFocusTempTicks 0264 << ", Alt:" << m_ThisAdaptiveFocusAlt 0265 << ", Alt ticks:" << m_ThisAdaptiveFocusAltTicks 0266 << ", Last Pos Error reversal:" << m_LastAdaptiveFocusPosErrorReversal 0267 << ", Rounding Error:" << m_ThisAdaptiveFocusRoundingError 0268 << ", New Pos Error:" << thisPosError 0269 << ", Starting Pos:" << m_ThisAdaptiveFocusStartPos 0270 << ", Current Position:" << currentPosition 0271 << ", Accounting Check:" << (check ? "Passed" : "Failed"); 0272 } 0273 0274 // Get the filter to use for Adaptive Focus 0275 QString AdaptiveFocus::getAdaptiveFilter(const QString filter) 0276 { 0277 // If the active filter has a lock filter use that 0278 QString adaptiveFilter = filter; 0279 QString lockFilter = m_focus->m_FilterManager->getFilterLock(filter); 0280 if (lockFilter != NULL_FILTER) 0281 adaptiveFilter = lockFilter; 0282 0283 return adaptiveFilter; 0284 } 0285 0286 // Calc the filter offset between the active filter and the adaptive filter (either active filter or lock filter) 0287 int AdaptiveFocus::getAdaptiveFilterOffset(const QString &activeFilter, const QString &adaptiveFilter) 0288 { 0289 int offset = 0; 0290 if (activeFilter != adaptiveFilter) 0291 { 0292 int activeOffset = m_focus->m_FilterManager->getFilterOffset(activeFilter); 0293 int adaptiveOffset = m_focus->m_FilterManager->getFilterOffset(adaptiveFilter); 0294 if (activeOffset != INVALID_VALUE && adaptiveOffset != INVALID_VALUE) 0295 offset = activeOffset - adaptiveOffset; 0296 else 0297 qCDebug(KSTARS_EKOS_FOCUS) << "getAdaptiveFilterOffset unable to calculate filter offsets"; 0298 } 0299 return offset; 0300 } 0301 0302 // Reset the variables used by Adaptive Focus 0303 void AdaptiveFocus::resetAdaptiveFocusCounters() 0304 { 0305 m_LastAdaptiveFilter = NULL_FILTER; 0306 m_LastAdaptiveFocusTemperature = INVALID_VALUE; 0307 m_LastAdaptiveFocusAlt = INVALID_VALUE; 0308 m_AdaptiveTotalMove = 0; 0309 } 0310 0311 // Function to set inAdaptiveFocus 0312 void AdaptiveFocus::setInAdaptiveFocus(bool value) 0313 { 0314 m_inAdaptiveFocus = value; 0315 } 0316 0317 // Change the start position of an autofocus run based Adaptive Focus settings 0318 // The start position uses the last successful AF run for the active filter and adapts that position 0319 // based on the temperature and altitude delta between now and when the last successful AF run happened 0320 int AdaptiveFocus::adaptStartPosition(int currentPosition, QString &AFfilter) 0321 { 0322 // If the active filter has no lock then the AF run will happen on this filter so get the start point 0323 // Otherwise get the lock filter on which the AF run will happen and get the start point of this filter 0324 // An exception is when the BuildOffsets utility is being used as this ignores the lock filter 0325 if (!m_focus->m_FilterManager) 0326 return currentPosition; 0327 0328 QString filterText; 0329 QString lockFilter = m_focus->m_FilterManager->getFilterLock(AFfilter); 0330 if (m_focus->inBuildOffsets || lockFilter == NULL_FILTER || lockFilter == AFfilter) 0331 filterText = AFfilter; 0332 else 0333 { 0334 filterText = AFfilter + " locked to " + lockFilter; 0335 AFfilter = lockFilter; 0336 } 0337 0338 if (!m_focus->m_OpsFocusSettings->focusAdaptStart->isChecked()) 0339 // Adapt start option disabled 0340 return currentPosition; 0341 0342 if (m_focus->m_FocusAlgorithm != Focus::FOCUS_LINEAR1PASS) 0343 // Only enabled for LINEAR 1 PASS 0344 return currentPosition; 0345 0346 // Start with the last AF run result for the active filter 0347 int lastPos; 0348 double lastTemp, lastAlt; 0349 if(!m_focus->m_FilterManager->getFilterAbsoluteFocusDetails(AFfilter, lastPos, lastTemp, lastAlt)) 0350 // Unable to get the last AF run information for the filter so just use the currentPosition 0351 return currentPosition; 0352 0353 // Only proceed if we have a sensible lastPos 0354 if (lastPos <= 0) 0355 return currentPosition; 0356 0357 // Do some checks on the lastPos 0358 int minTravelLimit = qMax(0.0, currentPosition - m_focus->m_OpsFocusMechanics->focusMaxTravel->value()); 0359 int maxTravelLimit = qMin(m_focus->absMotionMax, currentPosition + m_focus->m_OpsFocusMechanics->focusMaxTravel->value()); 0360 if (lastPos < minTravelLimit || lastPos > maxTravelLimit) 0361 { 0362 // Looks like there is a potentially dodgy lastPos so just use currentPosition 0363 m_focus->appendLogText(i18n("Adaptive start point, last AF solution outside Max Travel, ignoring")); 0364 return currentPosition; 0365 } 0366 0367 // Adjust temperature 0368 double ticksTemp = 0.0; 0369 double tempDelta = 0.0; 0370 if (!m_focus->currentTemperatureSourceElement) 0371 m_focus->appendLogText(i18n("Adaptive start point, no temperature source available")); 0372 else if (lastTemp == INVALID_VALUE) 0373 m_focus->appendLogText(i18n("Adaptive start point, no temperature for last AF solution")); 0374 else 0375 { 0376 double currentTemp = m_focus->currentTemperatureSourceElement->value; 0377 tempDelta = currentTemp - lastTemp; 0378 if (abs(tempDelta) > 30) 0379 // Sanity check on the temperature delta 0380 m_focus->appendLogText(i18n("Adaptive start point, very large temperature delta, ignoring")); 0381 else 0382 ticksTemp = tempDelta * m_focus->m_FilterManager->getFilterTicksPerTemp(AFfilter); 0383 } 0384 0385 // Adjust altitude 0386 double ticksAlt = 0.0; 0387 double currentAlt = m_focus->mountAlt; 0388 double altDelta = currentAlt - lastAlt; 0389 0390 // Sanity check on the altitude delta 0391 if (lastAlt == INVALID_VALUE) 0392 m_focus->appendLogText(i18n("Adaptive start point, no alt recorded for last AF solution")); 0393 else if (abs(altDelta) > 90.0) 0394 m_focus->appendLogText(i18n("Adaptive start point, very large altitude delta, ignoring")); 0395 else 0396 ticksAlt = altDelta * m_focus->m_FilterManager->getFilterTicksPerAlt(AFfilter); 0397 0398 // We have all the elements to adjust the AF start position so final checks before the move 0399 const int ticksTotal = static_cast<int> (round(ticksTemp + ticksAlt)); 0400 int targetPos = lastPos + ticksTotal; 0401 if (targetPos < minTravelLimit || targetPos > maxTravelLimit) 0402 { 0403 // targetPos is outside Max Travel 0404 m_focus->appendLogText(i18n("Adaptive start point, target position is outside Max Travel, ignoring")); 0405 return currentPosition; 0406 } 0407 0408 if (abs(targetPos - currentPosition) > m_focus->m_OpsFocusSettings->focusAdaptiveMaxMove->value()) 0409 { 0410 // Disallow excessive movement. 0411 // No need to check minimum movement 0412 m_focus->appendLogText(i18n("Adaptive start point [%1] excessive move disallowed", filterText)); 0413 qCDebug(KSTARS_EKOS_FOCUS) << "Adaptive start point: " << filterText 0414 << " startPosition: " << currentPosition 0415 << " Last filter position: " << lastPos 0416 << " Temp delta: " << tempDelta << " Temp ticks: " << ticksTemp 0417 << " Alt delta: " << altDelta << " Alt ticks: " << ticksAlt 0418 << " Target position: " << targetPos 0419 << " Exceeds max allowed move: " << m_focus->m_OpsFocusSettings->focusAdaptiveMaxMove->value() 0420 << " Using startPosition."; 0421 return currentPosition; 0422 } 0423 else 0424 { 0425 // All good so report the move 0426 m_focus->appendLogText(i18n("Adapting start point [%1] from %2 to %3", filterText, currentPosition, targetPos)); 0427 qCDebug(KSTARS_EKOS_FOCUS) << "Adaptive start point: " << filterText 0428 << " startPosition: " << currentPosition 0429 << " Last filter position: " << lastPos 0430 << " Temp delta: " << tempDelta << " Temp ticks: " << ticksTemp 0431 << " Alt delta: " << altDelta << " Alt ticks: " << ticksAlt 0432 << " Target position: " << targetPos; 0433 return targetPos; 0434 } 0435 } 0436 0437 }