Fix TelescopeSimulator IsSlewing/AtPark wedging (slew-state thread-safety + GEM hour-angle-limit crossing)#46
Open
ivonnyssen wants to merge 2 commits into
Conversation
TelescopeHardware shares its slew state (mountAxes, targetAxes, slewing, SlewState, currentRaDec, AtPark) across Kestrel request threads and the s_wTimer tick with no synchronization. StartSlewAxes' writes are not ordered against DoSlew's reads, so on weak memory (AArch64) or under JIT register caching the tick can observe SlewState==SlewRaDec while slewing is still stale-false. DoSlew bails, MoveAxes' tail switch has no SlewRaDec case, and the slew never advances: IsSlewing stays true indefinitely. The same race on the unsynchronized AtPark accessor lets a client polling AtPark after Park() miss the tick's park-completion write and spin until its own deadline. Init() compounds this. It runs on every Telescope (re)construction and allocated a fresh AutoReset System.Timers.Timer each call without stopping/disposing the previous one, so the runtime kept every prior timer alive and firing M_wTimer_Tick on the shared static slew state - one leaked tick source per reconstruction, racing the slew engine. It also reset the slew-engine state outside any lock and never cleared the slewing flag. Fixes: - Add one hardwareLock, taken on both sides: the timer tick (M_wTimer_Tick -> MoveAxes/DoSlew), StartSlewAxes, AbortSlew, SyncToTarget, SyncToAltAzm, and the IsSlewing / RightAscension / Declination / AtPark / SlewState accessors. This orders the writes against the timer's reads and gives the Slewing/RA/Dec/AtPark pollers a consistent view. Monitor is reentrant, so the tick writing these while already holding the lock is safe. - In Init(), stop/unsubscribe/Dispose the existing timer before allocating a new one, so at most one timer ever drives MoveAxes, and reset the slew-engine state (incl. slewing=false, rateMoveAxes=0) under hardwareLock so it is ordered against any in-flight tick. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…limit On a German equatorial mount, DoSlew rotates the primary axis the shortest way (delta wrapped to (-180, 180]). When a goto's target sits ~180 deg away in axis space - e.g. a sync onto one pier side followed by a slew whose natural side is the other - that shortest rotation can drive the primary axis through the hour-angle software limit (180 + hourAngleLimit). CheckAxisLimits then undoes the move every 100 ms tick, so `finished` is never reached, `slewing` stays true, and IsSlewing is stuck true forever; the client polls until it times out. Fix, in two parts: - DoSlew: when the shortest-path primary rotation would leave the GEM mechanical range [-hourAngleLimit, 180 + hourAngleLimit] but the SAME target is reachable the other way round, take the longer rotation. It reaches the identical target axes, so the pier side is unchanged (no SideOfPier / conformance impact), and it is a no-op for any slew whose short path stays in range. - A no-progress guard in MoveAxes: if a slew makes < slewSpeedSlow/2 of progress for 0.5 s it is stopped (Slewing -> false) instead of reporting IsSlewing forever - a backstop for genuinely unreachable targets. Tracking drift is far below the threshold, so healthy slews never trip it. Verified: the former-wedge geometry now converges (~10 s to target, on the natural pier side) instead of hanging; ConformU telescope conformance reports "SideOfPier Write OK" and the guard never fires. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
TelescopeSimulatorcan leaveIsSlewing(and, relatedly,AtPark) stucktrueforever, so a client that polls after aSlewToCoordinates/Parkwaits until its own timeout. There are two independent causes, fixed in the two commits here. Both are inTelescopeSimulator/TelescopeHardware.cs; no public behaviour changes and no ConformU regressions.Cause 1 — unsynchronized shared slew state (thread races)
TelescopeHardwareshares its slew state (mountAxes,targetAxes,slewing,SlewState,currentRaDec,AtPark) between the Kestrel HTTP request threads (StartSlewAxes/SyncTo*/AbortSlew/ theSlewing/RA/Dec/AtParkaccessors) and thes_wTimertick thread (MoveAxes→DoSlew), with no synchronization.StartSlewAxes' writes aren't ordered against the timer thread's reads, so on weak memory (AArch64) or under JIT register caching the tick can observeSlewState == SlewRaDecwhile still seeingslewing == false.DoSlewbails,MoveAxes' tailswitchhas noSlewRaDeccase, and the slew never advances —IsSlewingstaystrueindefinitely.AtParkaccessor lets a client pollingAtParkafterPark()miss the tick's park-completion write and spin until its deadline.Init()(run on everyTelescope(re)construction) allocated a freshAutoReset System.Timers.Timereach call without stopping/disposing the previous one, so each reconstruction leaked a live tick source firingM_wTimer_Tickon the shared static state. It also reset the slew-engine state outside any lock and never clearedslewing.Fix (
77a32a2): introduce onehardwareLock, taken on both sides — the timer tick,StartSlewAxes,AbortSlew,SyncToTarget,SyncToAltAzm, and theIsSlewing/RightAscension/Declination/AtPark/SlewStateaccessors (Monitoris reentrant, so the tick writing while holding the lock is safe). InInit(), stop/unsubscribe/Disposethe existing timer before allocating a new one, and reset the slew-engine state (incl.slewing = false) under the lock.Cause 2 — GEM goto crossing the hour-angle limit
On a German equatorial mount,
DoSlewrotates the primary axis the shortest way (delta wrapped to(-180, 180]). When a goto's target sits ~180° away in axis space — e.g. a sync onto one pier side followed by a slew whose natural side is the other — that shortest rotation can drive the primary axis through the hour-angle software limit (180 + hourAngleLimit).CheckAxisLimitsthen undoes the move every 100 ms tick, sofinishedis never reached,slewingstaystrue, andIsSlewingis stucktrueforever.Fix (
bfb473e):DoSlew, when the shortest-path primary rotation would leave the GEM mechanical range[-hourAngleLimit, 180 + hourAngleLimit]but the same target is reachable the other way round, take the longer rotation. It reaches the identical target axes, so the pier side is unchanged (noSideOfPier/conformance impact), and it's a no-op for any slew whose short path stays in range.MoveAxes: a slew making< slewSpeedSlow/2progress for 0.5 s is stopped (Slewing → false) instead of reportingIsSlewingforever — a backstop for genuinely unreachable targets. Tracking drift (~0.0004°/tick) is far below the threshold, so healthy slews never trip it.Verification
SideOfPier Write OK; the no-progress guard never fires in normal operation.Scope / risk
TelescopeSimulator/TelescopeHardware.cs), +196/−33, comments included.