Skip to content

Fix TelescopeSimulator IsSlewing/AtPark wedging (slew-state thread-safety + GEM hour-angle-limit crossing)#46

Open
ivonnyssen wants to merge 2 commits into
ASCOMInitiative:mainfrom
ivonnyssen:fix/telescope-isslewing-wedge
Open

Fix TelescopeSimulator IsSlewing/AtPark wedging (slew-state thread-safety + GEM hour-angle-limit crossing)#46
ivonnyssen wants to merge 2 commits into
ASCOMInitiative:mainfrom
ivonnyssen:fix/telescope-isslewing-wedge

Conversation

@ivonnyssen

Copy link
Copy Markdown

Summary

TelescopeSimulator can leave IsSlewing (and, relatedly, AtPark) stuck true forever, so a client that polls after a SlewToCoordinates/Park waits until its own timeout. There are two independent causes, fixed in the two commits here. Both are in TelescopeSimulator/TelescopeHardware.cs; no public behaviour changes and no ConformU regressions.

Cause 1 — unsynchronized shared slew state (thread races)

TelescopeHardware shares its slew state (mountAxes, targetAxes, slewing, SlewState, currentRaDec, AtPark) between the Kestrel HTTP request threads (StartSlewAxes / SyncTo* / AbortSlew / the Slewing/RA/Dec/AtPark accessors) and the s_wTimer tick thread (MoveAxesDoSlew), 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 observe SlewState == SlewRaDec while still seeing slewing == 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 deadline.
  • Init() (run on every Telescope (re)construction) allocated a fresh AutoReset System.Timers.Timer each call without stopping/disposing the previous one, so each reconstruction leaked a live tick source firing M_wTimer_Tick on the shared static state. It also reset the slew-engine state outside any lock and never cleared slewing.

Fix (77a32a2): introduce one hardwareLock, taken on both sides — the timer tick, StartSlewAxes, AbortSlew, SyncToTarget, SyncToAltAzm, and the IsSlewing / RightAscension / Declination / AtPark / SlewState accessors (Monitor is reentrant, so the tick writing while holding the lock is safe). In Init(), stop/unsubscribe/Dispose the 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, DoSlew rotates 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). CheckAxisLimits then undoes the move every 100 ms tick, so finished is never reached, slewing stays true, and IsSlewing is stuck true forever.

Fix (bfb473e):

  • In 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's a no-op for any slew whose short path stays in range.
  • Add a no-progress guard in MoveAxes: a slew making < slewSpeedSlow/2 progress for 0.5 s is stopped (Slewing → false) instead of reporting IsSlewing forever — a backstop for genuinely unreachable targets. Tracking drift (~0.0004°/tick) is far below the threshold, so healthy slews never trip it.

Verification

  • ConformU telescope conformance passes, including SideOfPier Write OK; the no-progress guard never fires in normal operation.
  • The former-wedge geometry now converges (~10 s to target, on the natural pier side) instead of hanging.
  • These fixes have been running in a downstream fork build for several weeks against automated Alpaca integration tests with no recurrence of the hang.

Scope / risk

  • Single file (TelescopeSimulator/TelescopeHardware.cs), +196/−33, comments included.
  • No public API or behaviour changes; the locking is additive and the GEM change preserves the resolved target (and therefore pier side).

ivonnyssen and others added 2 commits June 30, 2026 19:51
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant