Instrument Neutral Distributed Interface INDI 2.2.5
lx200_proxisky.cpp
Go to the documentation of this file.
1/*
2 Proxisky UMi
3
4 Proxisky UMi mounts run OnStep-derived firmware, so the standard protocol is handled
5 entirely by LX200_OnStep. This driver adds the vendor-specific ":P..." extensions that
6 OnStep itself does not implement.
7
8 Copyright (C) 2026 Nico Trost
9
10 This library is free software; you can redistribute it and/or
11 modify it under the terms of the GNU Lesser General Public
12 License as published by the Free Software Foundation; either
13 version 2.1 of the License, or (at your option) any later version.
14
15 This library is distributed in the hope that it will be useful,
16 but WITHOUT ANY WARRANTY; without even the implied warranty of
17 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
18 Lesser General Public License for more details.
19
20 You should have received a copy of the GNU Lesser General Public
21 License along with this library; if not, write to the Free Software
22 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
23
24*/
25
26#include "lx200_proxisky.h"
27
28#include <cerrno>
29#include <cmath>
30#include <cstring>
31#include <ctime>
32#include <limits>
33
34// Mirrors the vendor tool's own tab split (tabSettings / tabLimits / tabACS / tabAdvanced), so
35// anyone moving between the two finds things in the same place. Prefixed because these sit
36// alongside LX200_OnStep's dozen tabs and INDI's standard ones, where a bare "Settings" would
37// be ambiguous.
38#define PROXISKY_SETTINGS_TAB "Proxisky Settings"
39#define PROXISKY_LIMITS_TAB "Proxisky RA/Dec Limits"
40#define PROXISKY_ACS_TAB "Proxisky ACS"
41#define PROXISKY_ADVANCED_TAB "Proxisky Advanced"
42
43// The vendor ":P..." reads use whatever timeout LX200_OnStep set for the transport (100ms serial,
44// 2s TCP); this driver does not touch it, which is why the probes run from updateProperties()
45// rather than getBasicData() - see the note there.
46
47// Poll the ACS collision counters every Nth status cycle rather than every one.
48#define PROXISKY_ACS_POLL_DIVIDER 5
49
50// Wall-clock budget for the ":Pbvg#" retry loop, polled every 250ms. The mount answers it with a
51// bare "0" for roughly a second after connect, so 3s covers that with margin. Bounded by the clock
52// rather than by an attempt count because an unanswered read costs a whole transport timeout, and
53// on TCP that is 2s - a fixed attempt count would overrun badly there.
54// The deadline is tested after each read so that a slow mount still gets one full attempt, which
55// means the real worst case is this budget plus one transport timeout: ~3.1s on serial, ~5s on TCP.
56#define PROXISKY_MODEL_PROBE_SECONDS 3
57
58// Backstop on the same loop, in case the clock rather than the mount is what misbehaves.
59// monotonicSeconds() reports failure as +infinity, which is the fail-closed answer everywhere it
60// is compared against a deadline - but not where it *is* the deadline: one failed clock read while
61// computing the deadline, followed by successful reads inside the loop, leaves every "finite >=
62// infinity" test false and the loop spinning. That needs CLOCK_MONOTONIC to fail and then recover,
63// which on Linux does not happen, so this is purely a guarantee of termination. Sized well above
64// the ~12 attempts the 3s budget allows on serial so it can never fire before the deadline does.
65#define PROXISKY_MODEL_PROBE_MAX_ATTEMPTS 64
66
67// Pause before a capability probe's single retry. The mount answers a healthy vendor read in
68// less than 25ms, so this is not about giving it more time to reply - the transport timeout has
69// already elapsed by then. It is about not re-issuing into whatever disturbed the line in the first
70// place.
71#define PROXISKY_PROBE_RETRY_NSEC 50000000L // 50ms
72
73// Wall-clock budget for the post-commit PID readback. ":Pnas<axis>r#" restarts the axis, which
74// reports all four gains as zero until it is back - measured at well under 100ms, so this is
75// generous. Bounded by the clock because the alternative is reporting a mismatch that never was.
76#define PROXISKY_PID_RESTART_SECONDS 1
77
78// Wall-clock budget for every remaining probe once ":Pbvg#" has answered. A healthy mount finishes
79// the sequence in well under a second. This only bites when the mount identifies itself and then
80// stops answering, where ~19 further reads would block the event loop inside Connect().
81// As above, the budget bounds when a read may *start*, not when it may finish. A probe read is
82// retried once and the budget is re-checked in between, so the last one can begin just under the
83// deadline and then spend two transport timeouts: worst case ~10.2s on serial, ~14s on TCP. Added
84// to the model probe, a mount that identifies itself and then goes silent can hold Connect() for
85// ~19s on TCP. That is in family with LX200_OnStep's own connect, which makes dozens of blocking
86// reads on the same path, and the alternative - deducting a timeout the transport does not expose -
87// would only move the imprecision somewhere less visible.
88#define PROXISKY_PROBE_BUDGET_SECONDS 10
89
90// The vendor grammar is case sensitive and the families are not symmetric:
91// :Ps<Feature><g|s>[arg]# boolean feature, g = get, s = set
92// :Pn<Feature>... numeric parameter
93// :Pb... board / capability query
94// Note :PsSdg# (Dec limit enable) and :PsSdG# (Dec limit values) differ only in case, and the
95// RA travel limits read their enable flag from the :Ps family but their values from :Pn.
96// Keep every command as a literal at its call site rather than composing it from fragments.
97
98// Monotonic seconds since an arbitrary origin. Only ever used for elapsed-time comparisons, so
99// the origin does not matter - but CLOCK_MONOTONIC does, since a wall-clock jump mid-connect
100// (NTP stepping the clock on a field laptop that just got a network) must not shorten or extend
101// a probe budget.
102static double monotonicSeconds()
103{
104 struct timespec now = {0, 0};
105
106 // A failure here would leave now at {0,0}, i.e. "the epoch", making every deadline look
107 // infinitely far away - probeModel() would loop forever. CLOCK_MONOTONIC is always available
108 // on Linux so this is belt and braces, but infinity is the fail-closed answer: every caller
109 // compares against a deadline, and infinity reads as "time is up" in all of them.
110 if (clock_gettime(CLOCK_MONOTONIC, &now) != 0)
111 return std::numeric_limits<double>::infinity();
112
113 return now.tv_sec + now.tv_nsec / 1e9;
114}
115
116// nanosleep() returns early when a signal arrives; INDI drivers run with SIGALRM and friends live,
117// so a bare call can sleep for a fraction of what was asked. Both callers sit in loops bounded by a
118// wall-clock deadline, which makes a short sleep merely wasteful rather than wrong - but a settle
119// delay that does not settle is worth getting right once here.
120static void sleepFor(const struct timespec &duration)
121{
122 struct timespec remaining = duration;
123
124 while (nanosleep(&remaining, &remaining) == -1 && errno == EINTR)
125 continue;
126}
127
128// Repairs an ISR_1OFMANY vector after update() has rejected a write. Nothing is logged here:
129// IUUpdateSwitch has already sent its own IDSetSwitch describing the error, and a second message
130// would only contradict the first. But its two failure paths do not leave the vector in the same
131// shape - a bad on-count resets and then puts the previous selection back, while an unknown element
132// name returns early with the vector already reset and nothing selected at all
133// (libs/indibase/indidriver.c:1348-1352). Only the second case needs fixing, and leaving it unfixed
134// strands a 1-of-many switch showing no selection until the next successful write. Reachable only
135// from a hand-written indi_setprop that misspells an element.
136static void restoreAfterFailedUpdate(INDI::PropertySwitch &sp, bool previous)
137{
138 if (sp.findOnSwitchIndex() >= 0)
139 return;
140
141 sp[previous ? 1 : 0].setState(ISS_ON);
142 sp.apply();
143}
144
145// One answer for "you wrote to a property this mount does not have". Not reachable through a
146// normal client - an unsupported feature is never defined, so it never appears in the panel - but
147// a hand-written indi_setprop can still address it by name. Passing it down to LX200_OnStep, which
148// has never heard of the property, is not an answer; saying so is. The message is passed whole
149// rather than built from a label because half these features are plural ("RA limits are...").
150template <typename PropertyT>
151static bool reportUnavailable(PropertyT &property, const char *message)
152{
153 property.setState(IPS_ALERT);
154 property.apply("%s", message);
155 return true;
156}
157
159{
160 // Everything else - LX200 capabilities, telescope capabilities, focuser/rotator/weather
161 // interfaces - is inherited unchanged from LX200_OnStep.
162 setVersion(1, 0);
163}
164
165// Deliberately no saveConfigItems() override. Every property here mirrors state held in the
166// mount's own non-volatile storage and is read back at connect, so persisting it client-side
167// would create a second source of truth that silently diverges from the hardware.
169{
170 return "Proxisky UMi";
171}
172
174{
176 return false;
177
178 // ============== PROXISKY_SETTINGS_TAB
179 ModelTP[0].fill("MODEL", "Model/Firmware", "Unknown");
180 ModelTP.fill(getDeviceName(), "PROXISKY_MODEL", "Proxisky Model", PROXISKY_SETTINGS_TAB, IP_RO, 0, IPS_IDLE);
181
182 RALimitEnableSP[0].fill("DISABLED", "Disabled", ISS_OFF);
183 RALimitEnableSP[1].fill("ENABLED", "Enabled", ISS_OFF);
184 RALimitEnableSP.fill(getDeviceName(), "PROXISKY_RA_LIMIT_ENABLE", "RA Limits", PROXISKY_LIMITS_TAB, IP_RW,
186
187 // Left/Right and CCW/CW are the vendor's own terms. Nothing in the protocol establishes a
188 // mapping to East/West.
189 RALimitNP[0].fill("LEFT", "Left (deg)", "%.0f", 1, 180, 1, 90);
190 RALimitNP[1].fill("RIGHT", "Right (deg)", "%.0f", 1, 180, 1, 90);
191 RALimitNP.fill(getDeviceName(), "PROXISKY_RA_LIMIT", "RA Limit Values", PROXISKY_LIMITS_TAB, IP_RW, 0, IPS_IDLE);
192
193 DecLimitEnableSP[0].fill("DISABLED", "Disabled", ISS_OFF);
194 DecLimitEnableSP[1].fill("ENABLED", "Enabled", ISS_OFF);
195 DecLimitEnableSP.fill(getDeviceName(), "PROXISKY_DEC_LIMIT_ENABLE", "Dec Limits", PROXISKY_LIMITS_TAB, IP_RW,
197
198 DecLimitNP[0].fill("CCW", "CCW (deg)", "%.0f", 90, 200, 1, 90);
199 DecLimitNP[1].fill("CW", "CW (deg)", "%.0f", 90, 200, 1, 90);
200 DecLimitNP.fill(getDeviceName(), "PROXISKY_DEC_LIMIT", "Dec Limit Values", PROXISKY_LIMITS_TAB, IP_RW, 0, IPS_IDLE);
201
202 LedSP[0].fill("OFF", "Off", ISS_OFF);
203 LedSP[1].fill("ON", "On", ISS_OFF);
204 LedSP.fill(getDeviceName(), "PROXISKY_LED", "Status LED", PROXISKY_SETTINGS_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE);
205
206 AcsEnableSP[0].fill("DISABLED", "Disabled", ISS_OFF);
207 AcsEnableSP[1].fill("ENABLED", "Enabled", ISS_OFF);
208 AcsEnableSP.fill(getDeviceName(), "PROXISKY_ACS_ENABLE", "Anti-Collision", PROXISKY_ACS_TAB, IP_RW, ISR_1OFMANY, 0,
209 IPS_IDLE);
210
211 // Bounds taken from the vendor tool's own client-side validation, not from the firmware - an
212 // in-range value can still be refused, which is what the readback after each write is for.
213 // The asymmetric lower bounds are the tool's, not a typo: RA accepts 10, Dec only 100. The
214 // upper bound is the tool's error *text* (59999) rather than its actual check (v <= 60000),
215 // which is off by one against itself; we follow the text, since that is what the vendor
216 // documents to users.
217 AcsThresholdNP[0].fill("RA", "RA Threshold", "%.0f", 10, 59999, 1, 1000);
218 AcsThresholdNP[1].fill("DEC", "Dec Threshold", "%.0f", 100, 59999, 1, 1000);
219 AcsThresholdNP.fill(getDeviceName(), "PROXISKY_ACS_THRESHOLD", "ACS Sensitivity", PROXISKY_ACS_TAB, IP_RW, 0, IPS_IDLE);
220
221 AcsCollisionNP[0].fill("RA", "RA Collisions", "%.0f", 0, 1e9, 1, 0);
222 AcsCollisionNP[1].fill("DEC", "Dec Collisions", "%.0f", 0, 1e9, 1, 0);
223 AcsCollisionNP.fill(getDeviceName(), "PROXISKY_ACS_COLLISIONS", "Collision Count", PROXISKY_ACS_TAB, IP_RO, 0,
224 IPS_IDLE);
225
226 AcsClearSP[0].fill("CLEAR", "Clear Counters", ISS_OFF);
227 AcsClearSP.fill(getDeviceName(), "PROXISKY_ACS_CLEAR", "Collision Counters", PROXISKY_ACS_TAB, IP_RW, ISR_ATMOST1, 0,
228 IPS_IDLE);
229
230 PowerLossSP[0].fill("DISABLED", "Disabled", ISS_OFF);
231 PowerLossSP[1].fill("ENABLED", "Enabled", ISS_OFF);
232 PowerLossSP.fill(getDeviceName(), "PROXISKY_POWER_LOSS_MEMORY", "Power-loss Memory", PROXISKY_SETTINGS_TAB, IP_RW,
234
235 AsiairSP[0].fill("DISABLED", "Disabled", ISS_OFF);
236 AsiairSP[1].fill("ENABLED", "Enabled", ISS_OFF);
237 AsiairSP.fill(getDeviceName(), "PROXISKY_ASIAIR", "ASIAIR Homing", PROXISKY_SETTINGS_TAB, IP_RW, ISR_1OFMANY, 0,
238 IPS_IDLE);
239
240 SuperHomeSP[0].fill("DISABLED", "Disabled", ISS_OFF);
241 SuperHomeSP[1].fill("ENABLED", "Enabled", ISS_OFF);
242 SuperHomeSP.fill(getDeviceName(), "PROXISKY_SUPER_HOME", "Supervised Home", PROXISKY_SETTINGS_TAB, IP_RW,
244
245 DecSecHomeSP[0].fill("DISABLED", "Disabled", ISS_OFF);
246 DecSecHomeSP[1].fill("ENABLED", "Enabled", ISS_OFF);
247 DecSecHomeSP.fill(getDeviceName(), "PROXISKY_DEC_SEC_HOME", "Dec Second Home", PROXISKY_SETTINGS_TAB, IP_RW,
249
250 AutoTrackSP[0].fill("DISABLED", "Disabled", ISS_OFF);
251 AutoTrackSP[1].fill("ENABLED", "Enabled", ISS_OFF);
252 AutoTrackSP.fill(getDeviceName(), "PROXISKY_AUTO_TRACK", "Auto Tracking", PROXISKY_SETTINGS_TAB, IP_RW,
254
255 SuperGotoSP[0].fill("DISABLED", "Disabled", ISS_OFF);
256 SuperGotoSP[1].fill("ENABLED", "Enabled", ISS_OFF);
257 SuperGotoSP.fill(getDeviceName(), "PROXISKY_SUPER_GOTO", "Supervised GOTO", PROXISKY_ADVANCED_TAB, IP_RW,
259
260 SuperGotoToleranceNP[0].fill("TOLERANCE", "Tolerance (deg)", "%.0f", 5, 30, 1, 10);
261 SuperGotoToleranceNP.fill(getDeviceName(), "PROXISKY_SUPER_GOTO_TOLERANCE", "Supervised GOTO Tolerance",
263
264 // Servo PID gains. Ranges are the vendor tool's own validation: 11..1999 for everything
265 // except speed Ki, which it caps at 80. Out-of-range gains can leave the mount unable to
266 // track, so these are enforced here as well as by the client's min/max.
267 RaPidNP[0].fill("ANGLE_KP", "Angle Kp", "%.0f", 11, 1999, 1, 100);
268 RaPidNP[1].fill("ANGLE_KI", "Angle Ki", "%.0f", 11, 1999, 1, 100);
269 RaPidNP[2].fill("SPEED_KP", "Speed Kp", "%.0f", 11, 1999, 1, 100);
270 RaPidNP[3].fill("SPEED_KI", "Speed Ki", "%.0f", 11, 80, 1, 20);
271 RaPidNP.fill(getDeviceName(), "PROXISKY_RA_PID", "RA/Azm PID", PROXISKY_ADVANCED_TAB, IP_RW, 0, IPS_IDLE);
272
273 DecPidNP[0].fill("ANGLE_KP", "Angle Kp", "%.0f", 11, 1999, 1, 100);
274 DecPidNP[1].fill("ANGLE_KI", "Angle Ki", "%.0f", 11, 1999, 1, 100);
275 DecPidNP[2].fill("SPEED_KP", "Speed Kp", "%.0f", 11, 1999, 1, 100);
276 DecPidNP[3].fill("SPEED_KI", "Speed Ki", "%.0f", 11, 80, 1, 20);
277 DecPidNP.fill(getDeviceName(), "PROXISKY_DEC_PID", "Dec/Alt PID", PROXISKY_ADVANCED_TAB, IP_RW, 0, IPS_IDLE);
278
279 return true;
280}
281
283{
284 // Captured rather than returned straight away: our own teardown has to run even if the parent
285 // reports a problem, otherwise a failed disconnect leaves the vendor properties on screen.
286 const bool parentOk = LX200_OnStep::updateProperties();
287
288 if (!isConnected())
289 {
291 return parentOk;
292 }
293
294 // Probe here rather than from getBasicData(). Both are reached during connect, but
295 // getBasicData() runs from inside LX200Generic::updateProperties(), which the parent calls
296 // *before* it picks the transport timeout - so probing there would use the 100ms default even
297 // on TCP, where the parent is about to ask for 2s. Running after the parent means the vendor
298 // reads use whatever timeout it settled on, with no second copy of its transport detection here.
299 //
300 // Simulation is skipped outright: every vendor property mirrors a value read back from real
301 // hardware, and there is nothing to read. The driver is deliberately indistinguishable from
302 // bare OnStep there - documented, not a bug.
303 if (!isSimulation())
304 {
307 }
308
309 return parentOk;
310}
311
313{
314 // Connect-time only. ReadScopeStatus() re-runs LX200_OnStep::updateProperties() when it first
315 // detects PEC or pier side; that call is explicitly qualified, so it does not reach our
316 // override - but the guard stays, because a second full pass from the event loop would block
317 // the status cadence for as long as a connect does.
318 if (m_ProbesDone || isSimulation())
319 return;
320
321 m_ProbesDone = true;
322
323 // probeModel() absorbs the mount's startup window internally, so by the time it succeeds the
324 // mount is answering vendor commands and the optional probes need no retry of their own.
325 probeModel();
326
327 if (!m_HasModel)
328 return;
329
330 // Start the budget only after the mount has identified itself, so the time probeModel()
331 // spent waiting out the startup window is not charged against the feature probes.
332 m_ProbeDeadline = monotonicSeconds() + PROXISKY_PROBE_BUDGET_SECONDS;
333
336 probeLed();
337 probeAcs();
340
341 if (!probeBudgetExpired())
342 m_HasRaPid = probePidAxis(RaPidNP, ":Pnag0#", "RA/Azm");
343
344 if (!probeBudgetExpired())
345 m_HasDecPid = probePidAxis(DecPidNP, ":Pnag1#", "Dec/Alt");
346
347 // Clearing the deadline takes getVendorString() off the budget for the rest of the session.
348 // Every read from here on is a user action or a status poll, and neither should be refused
349 // because connect-time probing once ran long.
350 m_ProbeDeadline = 0;
351}
352
354{
355 if (m_ProbeDeadline <= 0 || monotonicSeconds() < m_ProbeDeadline)
356 return false;
357
358 if (!m_ProbeBudgetWarned)
359 {
360 m_ProbeBudgetWarned = true;
361 LOGF_WARN("Vendor probing exceeded its %ds budget - the mount identified itself but "
362 "stopped answering. Skipping the remaining feature probes; reconnect to retry.",
364 }
365
366 return true;
367}
368
370{
371 if (!isConnected() || m_PropertiesDefined)
372 return;
373
374 m_PropertiesDefined = true;
375
376 if (m_HasModel)
377 defineProperty(ModelTP);
378
379 if (m_HasRALimits)
380 {
381 defineProperty(RALimitEnableSP);
382 defineProperty(RALimitNP);
383 }
384
385 if (m_HasDecLimits)
386 {
387 defineProperty(DecLimitEnableSP);
388 defineProperty(DecLimitNP);
389 }
390
391 if (m_HasLed)
392 defineProperty(LedSP);
393
394 if (m_HasAcs)
395 {
396 defineProperty(AcsEnableSP);
397 defineProperty(AcsThresholdNP);
398 defineProperty(AcsCollisionNP);
399 defineProperty(AcsClearSP);
400 }
401
402 if (m_HasPowerLoss)
403 defineProperty(PowerLossSP);
404
405 if (m_HasAsiair)
406 defineProperty(AsiairSP);
407
408 if (m_HasSuperHome)
409 defineProperty(SuperHomeSP);
410
411 if (m_HasDecSecHome)
412 defineProperty(DecSecHomeSP);
413
414 if (m_HasAutoTrack)
415 defineProperty(AutoTrackSP);
416
417 if (m_HasSuperGoto)
418 defineProperty(SuperGotoSP);
419
420 if (m_HasSuperGotoTolerance)
421 defineProperty(SuperGotoToleranceNP);
422
423 if (m_HasRaPid)
424 defineProperty(RaPidNP);
425
426 if (m_HasDecPid)
427 defineProperty(DecPidNP);
428}
429
431{
432 deleteProperty(ModelTP);
433 deleteProperty(RALimitEnableSP);
434 deleteProperty(RALimitNP);
435 deleteProperty(DecLimitEnableSP);
436 deleteProperty(DecLimitNP);
437 deleteProperty(LedSP);
438 deleteProperty(AcsEnableSP);
439 deleteProperty(AcsThresholdNP);
440 deleteProperty(AcsCollisionNP);
441 deleteProperty(AcsClearSP);
442 deleteProperty(PowerLossSP);
443 deleteProperty(AsiairSP);
444 deleteProperty(SuperHomeSP);
445 deleteProperty(DecSecHomeSP);
446 deleteProperty(AutoTrackSP);
447 deleteProperty(SuperGotoSP);
448 deleteProperty(SuperGotoToleranceNP);
449 deleteProperty(RaPidNP);
450 deleteProperty(DecPidNP);
451
452 // Clear both the capability and the published state so the next connection re-probes from
453 // scratch - a different mount may be on the other end of the port. Deleting a property that
454 // was never defined is a no-op: DefaultDevice::deleteProperty returns false without emitting
455 // IDDelete, so the unconditional list above is safe.
456 m_HasModel = false;
457 m_ProbesDone = false;
458 m_PropertiesDefined = false;
459 m_ProbeDeadline = 0;
460 m_ProbeBudgetWarned = false;
461 m_HasRALimits = false;
462 m_HasDecLimits = false;
463 m_HasLed = false;
464 m_HasAcs = false;
465 m_HasPowerLoss = false;
466 m_HasAsiair = false;
467 m_HasSuperHome = false;
468 m_HasDecSecHome = false;
469 m_HasAutoTrack = false;
470 m_HasSuperGoto = false;
471 m_HasSuperGotoTolerance = false;
472 m_HasRaPid = false;
473 m_HasDecPid = false;
474 m_AcsEnabled = false;
475 m_AcsCountersGood = true;
476 m_AcsPollDivider = 0;
477}
478
479/***************************************************************************************
480** Vendor command helpers
481***************************************************************************************/
482
483bool LX200_Proxisky::getVendorString(const char *cmd, char *out, size_t outLen, bool requireTerminated, int *rcOut)
484{
485 if (outLen > 0)
486 out[0] = '\0';
487
488 if (rcOut != nullptr)
489 *rcOut = 0;
490
491 // Checked here, at the one place every vendor read passes through, rather than only at the
492 // head of each probe. A probe that issues several reads would otherwise overrun the budget by
493 // a whole transport timeout per read - seconds, on TCP. runVendorProbes() clears the deadline
494 // when it finishes, and probeBudgetExpired() is false whenever the deadline is unset, so this
495 // costs nothing once the driver is running.
496 if (probeBudgetExpired())
497 return false;
498
499 if (readVendorStringOnce(cmd, out, outLen, requireTerminated, rcOut))
500 return true;
501
502 // One retry, and only while the capability probes are running - m_ProbeDeadline is non-zero
503 // exactly then, which is also what keeps this out of the status loop and off every runtime
504 // readback, where a retry would just double the cost of a mount that has stopped talking.
505 //
506 // Worth it here because during probing a dropped reply is not merely a failed read: "did the
507 // mount answer" is the only capability test this half of the protocol offers, so a single lost
508 // byte is indistinguishable from "feature not fitted" and hides a working control until the
509 // user reconnects. Note this does not retry the vendor's "not supported" sentinel - a bare "0"
510 // is a *successful* read here and is rejected later by the parsers - so a mount that genuinely
511 // lacks a feature says so once and costs nothing extra.
512 //
513 // probeModel() is deliberately not covered: it runs before the deadline is set and has its own
514 // retry loop, sized for the mount's ~1s startup window rather than for a transport glitch.
515 if (m_ProbeDeadline <= 0 || probeBudgetExpired())
516 return false;
517
518 const struct timespec retryPause = {0, PROXISKY_PROBE_RETRY_NSEC};
519 sleepFor(retryPause);
520
521 LOGF_DEBUG("Retrying %s once before treating the feature as unsupported.", cmd);
522 return readVendorStringOnce(cmd, out, outLen, requireTerminated, rcOut);
523}
524
525bool LX200_Proxisky::readVendorStringOnce(const char *cmd, char *out, size_t outLen, bool requireTerminated,
526 int *rcOut)
527{
528 char response[RB_MAX_LEN] = {0};
529
531
532 if (rcOut != nullptr)
533 *rcOut = rc;
534
535 // Hand back whatever arrived even on the failure paths below, so a caller that wants to
536 // report what the mount actually said does not have to re-issue the command to find out.
537 snprintf(out, outLen, "%s", response);
538
539 // getCommandSingleCharErrorOrLongResponse() fills and NUL-terminates the buffer (stripping
540 // the '#') before it checks the TTY status, so a reply that arrives without a terminator
541 // comes back as a negative rc with usable data. Only an empty buffer is a real failure.
542 if (rc < 1 && response[0] == '\0')
543 {
544 LOGF_DEBUG("No response to %s (rc=%d)", cmd, rc);
545 return false;
546 }
547
548 if (rc < 1)
549 {
550 if (requireTerminated)
551 {
552 LOGF_DEBUG("Discarding unterminated response to %s: <%s> (rc=%d)", cmd, response, rc);
553 return false;
554 }
555
556 LOGF_DEBUG("Unterminated response to %s: <%s> (rc=%d)", cmd, response, rc);
557 }
558
559 return true;
560}
561
562bool LX200_Proxisky::getVendorBool(const char *cmd, bool &value)
563{
564 char response[RB_MAX_LEN] = {0};
565
566 if (!getVendorString(cmd, response, sizeof(response)))
567 return false;
568
569 if (response[0] != '0' && response[0] != '1')
570 {
571 LOGF_DEBUG("%s: unexpected boolean reply <%s>", cmd, response);
572 return false;
573 }
574
575 value = (response[0] == '1');
576 return true;
577}
578
580{
581 char response[RB_MAX_LEN] = {0};
582
583 // The mount acknowledges every setter command: "1#" accepted,
584 // "0#" refused. Measured on a UMi20S 1.0.6, all 28 setters tested answered in 3-15ms.
585 // Consuming it here is both the success check and what keeps the ack from being read as the
586 // *next* command's reply - a blind write leaves it sitting in the buffer.
587 //
588 // Both failures log at DEBUG, not ERROR: every caller already reports the refusal to the user
589 // in its own words, and two messages for one event just makes the log read like two faults.
590 // What is kept here is the part the caller cannot say - the exact command and the raw reply.
591 if (!getVendorString(cmd, response, sizeof(response)))
592 {
593 LOGF_DEBUG("%s: no acknowledgement from the mount", cmd);
594 return false;
595 }
596
597 if (response[0] != '1')
598 {
599 LOGF_DEBUG("%s: refused, mount replied <%s>", cmd, response);
600 return false;
601 }
602
603 // No settle delay. Once the ack has been read the write has landed.
604 return true;
605}
606
607bool LX200_Proxisky::getVendorPair(const char *cmd, double &first, double &second)
608{
609 char response[RB_MAX_LEN] = {0};
610
611 // Require a properly terminated reply: getVendorString() deliberately accepts a timed-out
612 // partial read, which is fine for a free-form string but would silently truncate a number.
613 if (!getVendorString(cmd, response, sizeof(response), true))
614 return false;
615
616 // A bare "0" means the feature is not supported, and lands here as a failed match.
617 if (sscanf(response, "%lf|%lf", &first, &second) != 2)
618 {
619 LOGF_DEBUG("%s: not a pair reply <%s>, treating feature as unsupported", cmd, response);
620 return false;
621 }
622
623 return true;
624}
625
626bool LX200_Proxisky::getVendorQuad(const char *cmd, double *out)
627{
628 char response[RB_MAX_LEN] = {0};
629
630 if (!getVendorString(cmd, response, sizeof(response), true))
631 return false;
632
633 // At least four fields, not exactly four: a UMi20S answers ":Pnag<axis>#" with seven.
634 if (sscanf(response, "%lf|%lf|%lf|%lf", &out[0], &out[1], &out[2], &out[3]) != 4)
635 {
636 LOGF_DEBUG("%s: fewer than four numeric fields in <%s>, treating feature as unsupported", cmd, response);
637 return false;
638 }
639
640 LOGF_DEBUG("%s: raw reply <%s>", cmd, response);
641 return true;
642}
643
644bool LX200_Proxisky::probeBoolSwitch(INDI::PropertySwitch &sp, const char *readCmd, const char *label)
645{
646 bool value = false;
647
648 if (!getVendorBool(readCmd, value))
649 {
650 // "Did it answer at all" is the only capability test this part of the protocol offers -
651 // an unsupported feature and a dropped reply are indistinguishable here. getVendorString()
652 // has already retried once, so this is a feature that stayed silent twice; probing runs
653 // once per connection, so it stays hidden until the user reconnects.
654 //
655 // WARN rather than INFO, matching probeLed() and probeAcs(). The lower level would imply a
656 // distinction this probe cannot actually draw - "the board says not fitted" reads as
657 // routine, but that is a bare "0", which arrives as a *successful* read and never reaches
658 // this branch. Everything that gets here is silence, and silence is worth a warning.
659 LOGF_WARN("%s did not answer (%s), even after a retry; its control is hidden. "
660 "Reconnect to probe again.", label, readCmd);
661 return false;
662 }
663
664 sp.reset();
665 sp[value ? 1 : 0].setState(ISS_ON);
666 sp.setState(IPS_OK);
667 LOGF_INFO("%s: %s", label, value ? "enabled" : "disabled");
668 return true;
669}
670
671bool LX200_Proxisky::handleBoolSwitch(INDI::PropertySwitch &sp, const char *setOn, const char *setOff,
672 const char *readCmd, const char *label,
673 ISState *states, char *names[], int n, bool *mirror)
674{
675 // Captured before update() overwrites it: on a refusal this is the state the mount is still
676 // running, and it is the only record of it left if the readback also fails.
677 const bool previous = (sp[1].getState() == ISS_ON);
678
679 if (!sp.update(states, names, n))
680 {
681 restoreAfterFailedUpdate(sp, previous);
682 return true;
683 }
684
685 sp.setState(IPS_BUSY);
686 sp.apply();
687
688 const bool wanted = (sp[1].getState() == ISS_ON);
689 LOGF_DEBUG("===CMD==> %s = %s", label, wanted ? "enabled" : "disabled");
690
691 bool actual = false;
692
693 if (!sendVendorSetter(wanted ? setOn : setOff))
694 {
695 // Refused, so the mount is still running the old setting - but sp.update() has already
696 // moved the switch to what the user asked for. Put back what the mount actually holds,
697 // falling back to the pre-write selection when even the readback fails, so the panel and
698 // the mirror below never disagree about what is running.
699 const bool restored = getVendorBool(readCmd, actual) ? actual : previous;
700
701 sp.reset();
702 sp[restored ? 1 : 0].setState(ISS_ON);
704
705 if (mirror != nullptr)
706 *mirror = restored;
707
708 sp.apply("The mount refused the %s change; it is still %s.", label,
709 restored ? "enabled" : "disabled");
710 LOGF_ERROR("The mount refused the %s change.", label);
711 return true;
712 }
713
714 sp.reset();
715
716 if (getVendorBool(readCmd, actual))
717 {
718 sp[actual ? 1 : 0].setState(ISS_ON);
719
720 if (mirror != nullptr)
721 *mirror = actual;
722
723 if (actual == wanted)
724 {
725 sp.setState(IPS_OK);
726 sp.apply("%s %s.", label, actual ? "enabled" : "disabled");
727 LOGF_INFO("%s %s.", label, actual ? "enabled" : "disabled");
728 }
729 else
730 {
732 sp.apply("Mount still reports %s %s after requesting %s.", label,
733 actual ? "enabled" : "disabled", wanted ? "enabled" : "disabled");
734 LOGF_WARN("Mount did not accept the %s change (wanted %s, reads %s).", label,
735 wanted ? "enabled" : "disabled", actual ? "enabled" : "disabled");
736 }
737
738 return true;
739 }
740
741 // Leave a valid selection behind: the reset() above cleared every element.
742 sp[wanted ? 1 : 0].setState(ISS_ON);
744
745 // Assume the write landed. Leaving the mirror stale makes ReadScopeStatus() keep
746 // polling a feature the user just turned off.
747 if (mirror != nullptr)
748 *mirror = wanted;
749
750 sp.apply("Set %s but could not read the state back (%s).", label, readCmd);
751 LOGF_ERROR("Could not read back %s state (%s).", label, readCmd);
752 return true;
753}
754
755bool LX200_Proxisky::handlePendingSwitch(INDI::PropertySwitch &sp, const char *setOn, const char *setOff,
756 const char *readCmd, const char *label,
757 ISState *states, char *names[], int n)
758{
759 // Captured before update() overwrites it: on a refusal this is the setting the mount still
760 // has stored, and it is the only record of it left if the readback also fails.
761 const bool previous = (sp[1].getState() == ISS_ON);
762
763 if (!sp.update(states, names, n))
764 {
765 restoreAfterFailedUpdate(sp, previous);
766 return true;
767 }
768
769 sp.setState(IPS_BUSY);
770 sp.apply();
771
772 const bool wanted = (sp[1].getState() == ISS_ON);
773 LOGF_DEBUG("===CMD==> %s = %s (applies at next power up)", label, wanted ? "enabled" : "disabled");
774
775 // The readback reports the stored setting, which says nothing about what the mount is
776 // currently running, so a mismatch only refines the log message - never the verdict. The
777 // switch stays IPS_BUSY showing the requested state until the next connection, when the
778 // probe reads back whatever the mount actually booted with.
779 bool actual = false;
780
781 if (!sendVendorSetter(wanted ? setOn : setOff))
782 {
783 // Refused, so nothing was stored - but sp.update() has already moved the switch. Put back
784 // what the mount still has, falling back to the pre-write selection if the readback fails.
785 const bool restored = getVendorBool(readCmd, actual) ? actual : previous;
786
787 sp.reset();
788 sp[restored ? 1 : 0].setState(ISS_ON);
790 sp.apply("The mount refused the %s change; it still has %s stored.", label,
791 restored ? "enabled" : "disabled");
792 LOGF_ERROR("The mount refused the %s change.", label);
793 return true;
794 }
795
796 if (!getVendorBool(readCmd, actual))
797 {
798 // No answer is different from a mismatch. Telling the user it "will apply after a power
799 // cycle" here would be a guess they act on - they would cycle the mount and find the
800 // setting unchanged, with nothing but a log line to explain why.
802 sp.apply("Sent the %s change but could not confirm the mount stored it (%s).", label, readCmd);
803 LOGF_WARN("Could not read back %s state (%s) after the write.", label, readCmd);
804 return true;
805 }
806
807 // IPS_OK, not IPS_BUSY. The state describes the write, and the write is finished and verified
808 // - nothing further is in flight. A vector left permanently IPS_BUSY reads to a client as an
809 // operation still running, which is how KStars decides to show a spinner; the fact that the
810 // mount needs a power cycle before it acts on the value belongs in the message, not the state.
811 sp.setState(IPS_OK);
812 sp.apply("%s will be %s after the mount is power cycled.", label, wanted ? "enabled" : "disabled");
813
814 if (actual == wanted)
815 LOGF_INFO("Mount stored %s = %s; power cycle to apply it.", label, wanted ? "enabled" : "disabled");
816 else
817 LOGF_INFO("Mount still reports %s = %s; the new setting applies at next power up.", label,
818 actual ? "enabled" : "disabled");
819
820 return true;
821}
822
823/***************************************************************************************
824** Capability probes
825***************************************************************************************/
826
828{
829 // The mount answers ":Pbvg#" with a bare "0" - its "unsupported" sentinel - for about a
830 // second after connect, and only then reports its model. Absorb that here, during connect,
831 // where a short delay is expected, rather than retrying from the status loop where a slow
832 // read competes with guiding and abort handling.
833 const struct timespec settle = {0, 250000000L}; // 250ms
834 const double deadline = monotonicSeconds() + PROXISKY_MODEL_PROBE_SECONDS;
835
836 for (int attempt = 1; ; attempt++)
837 {
838 char response[RB_MAX_LEN] = {0};
839 int rc = 0;
840
841 // requireTerminated is off: an unterminated reply still carries a usable model string,
842 // and rejecting it would fail a mount that is merely slow. rcOut is what this probe needs
843 // that the others do not - it gates every other vendor property, so when it fails it must
844 // say why without the user having to enable debug logging first.
845 getVendorString(":Pbvg#", response, sizeof(response), false, &rc);
846
847 if (response[0] != '\0' && !(response[0] == '0' && response[1] == '\0'))
848 {
849 // WARN, not INFO: no vendor command has ever been observed to answer unterminated, so
850 // this is an anomaly worth surfacing rather than a normal step being narrated. The
851 // model string is still usable, hence accepting it and carrying on.
852 if (rc < 1)
853 LOGF_WARN("Proxisky :Pbvg# replied without a terminator (rc=%d), accepting '%s'.", rc, response);
854
855 m_HasModel = true;
856 ModelTP[0].setText(response);
857 ModelTP.setState(IPS_OK);
858 LOGF_INFO("Proxisky model/firmware: %s", response);
859 return;
860 }
861
862 // Checked after the read, not before, so a mount that is merely slow still gets one full
863 // attempt. An unanswered read burns a whole transport timeout, so this is what keeps
864 // Connect() from blocking on a mount that has no vendor extensions.
865 // The attempt count is the backstop for a deadline that can never be reached at all - see
866 // PROXISKY_MODEL_PROBE_MAX_ATTEMPTS. In every real run the clock ends the loop first.
867 if (monotonicSeconds() >= deadline || attempt >= PROXISKY_MODEL_PROBE_MAX_ATTEMPTS)
868 {
869 LOGF_WARN("Proxisky vendor extensions not reported by :Pbvg# after %d attempts over %ds "
870 "(rc=%d, response='%s'), vendor properties disabled.",
871 attempt, PROXISKY_MODEL_PROBE_SECONDS, rc, response);
872 return;
873 }
874
875 LOGF_DEBUG("Proxisky :Pbvg# not ready yet (attempt %d, rc=%d, response='%s'), retrying.",
876 attempt, rc, response);
877 sleepFor(settle);
878 }
879}
880
882{
883 if (probeBudgetExpired())
884 return;
885
886 double left = 0, right = 0;
887
888 if (!getVendorPair(":Pnbg#", left, right))
889 {
890 LOG_INFO("RA travel limits not supported by this mount.");
891 return;
892 }
893
894 m_HasRALimits = true;
895 const double raLimits[2] = {left, right};
896 RALimitNP.setState(setReportedValues(RALimitNP, raLimits, 2) ? IPS_OK : IPS_ALERT);
897
898 bool enabled = false;
899 RALimitEnableSP.reset();
900
901 if (getVendorBool(":PsSrg#", enabled))
902 {
903 RALimitEnableSP[enabled ? 1 : 0].setState(ISS_ON);
904 RALimitEnableSP.setState(IPS_OK);
905 LOGF_INFO("RA travel limits: left %.0f deg, right %.0f deg, %s", left, right,
906 enabled ? "enabled" : "disabled");
907 }
908 else
909 {
910 // reset() above cleared both elements; an ISR_1OFMANY vector with nothing ON is invalid
911 // and clients render it as an empty selector. Fall back to a known element and flag it.
912 RALimitEnableSP[0].setState(ISS_ON);
913 RALimitEnableSP.setState(IPS_ALERT);
914 LOG_WARN("Could not read RA limit enable state (:PsSrg#), showing it as disabled.");
915 }
916}
917
919{
920 // Gate on the capability read: if :PsSdG# answers with a valid pair the feature is there.
921 if (probeBudgetExpired())
922 return;
923
924 double ccw = 0, cw = 0;
925
926 if (!getVendorPair(":PsSdG#", ccw, cw))
927 {
928 LOGF_INFO("Dec travel limits not supported by this mount (model '%s').", ModelTP[0].getText());
929 return;
930 }
931
932 m_HasDecLimits = true;
933 const double decLimits[2] = {ccw, cw};
934 DecLimitNP.setState(setReportedValues(DecLimitNP, decLimits, 2) ? IPS_OK : IPS_ALERT);
935
936 bool enabled = false;
937 DecLimitEnableSP.reset();
938
939 if (getVendorBool(":PsSdg#", enabled))
940 {
941 DecLimitEnableSP[enabled ? 1 : 0].setState(ISS_ON);
942 DecLimitEnableSP.setState(IPS_OK);
943 LOGF_INFO("Dec travel limits: CCW %.0f deg, CW %.0f deg, %s", ccw, cw,
944 enabled ? "enabled" : "disabled");
945 }
946 else
947 {
948 DecLimitEnableSP[0].setState(ISS_ON);
949 DecLimitEnableSP.setState(IPS_ALERT);
950 LOG_WARN("Could not read Dec limit enable state (:PsSdg#), showing it as disabled.");
951 }
952}
953
955{
956 if (probeBudgetExpired())
957 return;
958
959 // The LED is the first feature with a capability query distinct from its state query:
960 // :PsLga# asks whether the board has an LED at all, :PsLgb# reads whether it is currently
961 // lit. One character apart - do not conflate them.
962 bool supported = false;
963
964 if (!getVendorBool(":PsLga#", supported))
965 {
966 // Probing is once-per-connection, so this hides the control until the user reconnects.
967 LOG_WARN("Status LED capability could not be read (:PsLga#); its control is hidden.");
968 return;
969 }
970
971 if (!supported)
972 {
973 LOG_INFO("Status LED not fitted to this mount.");
974 return;
975 }
976
977 // The board has an LED, so the control is published whatever the state read does - hiding a
978 // working switch because one read was dropped would cost the user the feature until they
979 // reconnect. Same treatment probeAcs() gives its own enable flag.
980 m_HasLed = true;
981 LedSP.reset();
982
983 bool lit = false;
984
985 if (getVendorBool(":PsLgb#", lit))
986 {
987 LedSP[lit ? 1 : 0].setState(ISS_ON);
988 LedSP.setState(IPS_OK);
989 LOGF_INFO("Status LED: %s", lit ? "on" : "off");
990 }
991 else
992 {
993 LedSP[0].setState(ISS_ON);
994 LedSP.setState(IPS_ALERT);
995 LOG_WARN("Mount reports a status LED (:PsLga#) but its state could not be read (:PsLgb#); "
996 "showing it as off.");
997 }
998}
999
1001{
1002 if (probeBudgetExpired())
1003 return;
1004
1005 // :PbC# is a board capability query, distinct from :PsCg# which reports whether the user
1006 // has the feature switched on.
1007 bool supported = false;
1008
1009 if (!getVendorBool(":PbC#", supported))
1010 {
1011 LOG_WARN("Anti-collision capability could not be read (:PbC#); its controls are hidden.");
1012 return;
1013 }
1014
1015 if (!supported)
1016 {
1017 LOG_INFO("Anti-collision system not fitted to this mount.");
1018 return;
1019 }
1020
1021 double raThreshold = 0, decThreshold = 0;
1022
1023 if (!getVendorPair(":Pncg#", raThreshold, decThreshold))
1024 {
1025 LOG_WARN("Mount reports an anti-collision system (:PbC#) but its thresholds could not be read (:Pncg#).");
1026 return;
1027 }
1028
1029 m_HasAcs = true;
1030 const double thresholds[2] = {raThreshold, decThreshold};
1031 AcsThresholdNP.setState(setReportedValues(AcsThresholdNP, thresholds, 2) ? IPS_OK : IPS_ALERT);
1032
1033 AcsEnableSP.reset();
1034
1035 if (getVendorBool(":PsCg#", m_AcsEnabled))
1036 {
1037 AcsEnableSP[m_AcsEnabled ? 1 : 0].setState(ISS_ON);
1038 AcsEnableSP.setState(IPS_OK);
1039 }
1040 else
1041 {
1042 AcsEnableSP[0].setState(ISS_ON);
1043 AcsEnableSP.setState(IPS_ALERT);
1044 LOG_WARN("Could not read anti-collision enable state (:PsCg#), showing it as disabled.");
1045 }
1046
1047 LOGF_INFO("Anti-collision: RA threshold %.0f, Dec threshold %.0f, %s", raThreshold, decThreshold,
1048 m_AcsEnabled ? "enabled" : "disabled");
1049
1050 refreshAcsCounters(false);
1051}
1052
1054{
1055 if (probeBudgetExpired())
1056 return;
1057
1058 // Power-loss memory is the one here with a board capability query of its own. Lower-case
1059 // :Pbc#
1060 bool fitted = false;
1061
1062 if (!getVendorBool(":Pbc#", fitted))
1063 LOG_WARN("Power loss memory capability could not be read (:Pbc#); its control is hidden.");
1064 else if (fitted)
1065 m_HasPowerLoss = probeBoolSwitch(PowerLossSP, ":Pscg#", "Power loss memory");
1066 else
1067 LOG_INFO("Power loss memory not fitted to this mount.");
1068
1069 if (probeBudgetExpired())
1070 return;
1071
1072 m_HasAsiair = probeBoolSwitch(AsiairSP, ":PsAg#", "ASIAIR homing");
1073
1074 if (probeBudgetExpired())
1075 return;
1076
1077 m_HasSuperHome = probeBoolSwitch(SuperHomeSP, ":PsShg#", "Supervised Home");
1078
1079 if (probeBudgetExpired())
1080 return;
1081
1082 m_HasDecSecHome = probeBoolSwitch(DecSecHomeSP, ":PsDgb#", "Dec second home");
1083
1084 if (probeBudgetExpired())
1085 return;
1086
1087 m_HasAutoTrack = probeBoolSwitch(AutoTrackSP, ":Psag#", "Auto tracking");
1088}
1089
1091{
1092 if (probeBudgetExpired())
1093 return;
1094
1095 // Asymmetric family: the enable flag reads from :Pndgo# but writes to :Pndss0|1#.
1096 if (!probeBoolSwitch(SuperGotoSP, ":Pndgo#", "Supervised GOTO"))
1097 return;
1098
1099 // The switch answered, so the feature exists and its control is published regardless of what
1100 // the tolerance read does below - those are two separate properties and one should not take
1101 // the other down with it.
1102 m_HasSuperGoto = true;
1103
1104 char response[RB_MAX_LEN] = {0};
1105
1106 // requireTerminated: this is a numeric parse, so a timed-out partial read must be rejected
1107 // rather than parsed. A truncated "10#" arriving as "1" would otherwise fail the range check
1108 // below and hide a supported feature for the whole session.
1109 if (!getVendorString(":Pndgv#", response, sizeof(response), true))
1110 {
1111 LOG_WARN("Mount reports Super GOTO (:Pndgo#) but its tolerance could not be read (:Pndgv#); "
1112 "the enable switch is still available, the tolerance field is not.");
1113 return;
1114 }
1115
1116 double tolerance = 0;
1117
1118 // A bare "#" reply decodes to an empty string, which atof() would silently turn into 0 and
1119 // publish as a valid tolerance.
1120 if (sscanf(response, "%lf", &tolerance) != 1)
1121 {
1122 LOGF_WARN("Supervised GOTO tolerance reply was not numeric <%s>; the tolerance field is hidden.", response);
1123 return;
1124 }
1125
1126 // A bare "0" is the vendor's not-supported sentinel here, same as it is for :Pnbg#. The
1127 // pair/quad parsers reject it for free because they require a '|'; a single %lf does not, and
1128 // 0 sits below the element's declared minimum - which is why the bounds are read from the
1129 // element rather than repeated, so widening the range cannot quietly admit the sentinel.
1130 if (tolerance < SuperGotoToleranceNP[0].getMin() || tolerance > SuperGotoToleranceNP[0].getMax())
1131 {
1132 LOGF_INFO("Supervised GOTO tolerance not supported by this mount (reply '%s'); "
1133 "its enable switch is still available.", response);
1134 return;
1135 }
1136
1137 m_HasSuperGotoTolerance = true;
1138 SuperGotoToleranceNP[0].setValue(tolerance);
1139 SuperGotoToleranceNP.setState(IPS_OK);
1140 LOGF_INFO("Supervised GOTO tolerance: %.0f degrees", tolerance);
1141}
1142
1144{
1145 char response[RB_MAX_LEN] = {0};
1146
1147 if (!getVendorString(":Pndgv#", response, sizeof(response), true))
1148 {
1149 SuperGotoToleranceNP.setState(IPS_ALERT);
1150 SuperGotoToleranceNP.apply();
1151 LOG_ERROR("Could not read back Super GOTO tolerance (:Pndgv#).");
1152 return false;
1153 }
1154
1155 double actual = 0;
1156
1157 if (sscanf(response, "%lf", &actual) != 1)
1158 {
1159 SuperGotoToleranceNP.setState(IPS_ALERT);
1160 SuperGotoToleranceNP.apply();
1161 LOGF_ERROR("Supervised GOTO tolerance reply was not numeric <%s>.", response);
1162 return false;
1163 }
1164
1165 // Same range gate probeSuperGoto() applies, and read from the same place. Without it a bare
1166 // "0" - the vendor's not-supported sentinel - or a truncated reply would be published against
1167 // an element it sits outside, leaving a value on screen the client cannot send back.
1168 if (actual < SuperGotoToleranceNP[0].getMin() || actual > SuperGotoToleranceNP[0].getMax())
1169 {
1170 SuperGotoToleranceNP.setState(IPS_ALERT);
1171 SuperGotoToleranceNP.apply();
1172 LOGF_ERROR("Mount reported an out-of-range Super GOTO tolerance <%s>.", response);
1173 return false;
1174 }
1175
1176 SuperGotoToleranceNP[0].setValue(actual);
1177
1178 const bool matched = (std::lround(actual) == std::lround(expected));
1179 SuperGotoToleranceNP.setState(matched ? IPS_OK : IPS_ALERT);
1180
1181 if (matched)
1182 {
1183 SuperGotoToleranceNP.apply("Supervised GOTO tolerance set to %.0f degrees.", actual);
1184 LOGF_INFO("Supervised GOTO tolerance set to %.0f degrees.", actual);
1185 }
1186 else
1187 {
1188 SuperGotoToleranceNP.apply("Mount reports Supervised GOTO tolerance %.0f after requesting %.0f.",
1189 actual, expected);
1190 LOGF_WARN("Mount reports Super GOTO tolerance %.0f after requesting %.0f.", actual, expected);
1191 }
1192
1193 return matched;
1194}
1195
1196bool LX200_Proxisky::rangeCheck(INDI::PropertyNumber &np, int index, double value, const char *what,
1197 const char *unit)
1198{
1199 auto &widget = np[index];
1200
1201 // Non-finite first: the comparisons below are false for a NaN, so without this it would pass
1202 // as "in range" and reach lround(), where it is undefined.
1203 if (std::isfinite(value) && value >= widget.getMin() && value <= widget.getMax())
1204 return true;
1205
1206 np.setState(IPS_ALERT);
1207 np.apply();
1208 LOGF_ERROR("%s must be between %.0f and %.0f%s.", what, widget.getMin(), widget.getMax(), unit);
1209 return false;
1210}
1211
1212bool LX200_Proxisky::setReportedValues(INDI::PropertyNumber &np, const double *values, int count)
1213{
1214 bool allInRange = true;
1215
1216 for (int i = 0; i < count; i++)
1217 {
1218 np[i].setValue(values[i]);
1219
1220 if (!std::isfinite(values[i]) || values[i] < np[i].getMin() || values[i] > np[i].getMax())
1221 {
1222 allInRange = false;
1223 LOGF_WARN("Mount reports %s = %.0f, outside this driver's accepted range %.0f..%.0f. "
1224 "It was most likely set with the vendor tool; writing a new value will correct it.",
1225 np[i].getLabel(), values[i], np[i].getMin(), np[i].getMax());
1226 }
1227 }
1228
1229 return allInRange;
1230}
1231
1232bool LX200_Proxisky::probePidAxis(INDI::PropertyNumber &np, const char *readCmd, const char *label)
1233{
1234 double gains[4] = {0, 0, 0, 0};
1235
1236 if (!getVendorQuad(readCmd, gains))
1237 {
1238 LOGF_INFO("%s PID gains not supported by this mount.", label);
1239 return false;
1240 }
1241
1242 np.setState(setReportedValues(np, gains, 4) ? IPS_OK : IPS_ALERT);
1243 LOGF_INFO("%s PID: angle Kp %.0f, angle Ki %.0f, speed Kp %.0f, speed Ki %.0f", label, gains[0], gains[1], gains[2],
1244 gains[3]);
1245 return true;
1246}
1247
1248bool LX200_Proxisky::handlePidWrite(INDI::PropertyNumber &np, const char *axisPrefix, const char *readCmd,
1249 const char *label, double values[], char *names[], int n)
1250{
1251 // The sequence ends in ":Pnas<axis>r#", which the vendor documents as "Commit/restart axis".
1252 // Measured on an idle UMi20S it answered "1#" in 8ms and disturbed nothing - but that was with
1253 // the axis unenergised, and nothing here would stop a client sending it mid-slew or in the
1254 // middle of an exposure. Refuse unless the mount is standing still. Manual N/S/E/W motion does
1255 // not show in TrackState, so the movement vectors are checked too, exactly as ReadScopeStatus()
1256 // does before its own vendor reads.
1257 const bool moving = (MovementNSSP.getState() == IPS_BUSY) || (MovementWESP.getState() == IPS_BUSY);
1258 const bool stopped = (TrackState == SCOPE_IDLE || TrackState == SCOPE_PARKED) && !moving;
1259
1260 if (!stopped)
1261 {
1262 np.setState(IPS_ALERT);
1263 np.apply("PID gains can only be changed while the mount is stopped - the commit restarts the axis.");
1264 LOGF_WARN("Refusing to write %s PID gains while the mount is not idle.", label);
1265 return true;
1266 }
1267
1268 // Seeded from the property so a partial vector still has something to send, but all four
1269 // arrive in practice: they are one field group behind a single Set button.
1270 double wanted[4];
1271
1272 for (int i = 0; i < 4; i++)
1273 wanted[i] = np[i].getValue();
1274
1275 for (int i = 0; i < n; i++)
1276 {
1277 for (int j = 0; j < 4; j++)
1278 {
1279 if (np[j].isNameMatch(names[i]))
1280 {
1281 wanted[j] = values[i];
1282 LOGF_DEBUG("===CMD==> %s %s= %f", label, np[j].getLabel(), wanted[j]);
1283 }
1284 }
1285 }
1286
1287 // All four gains are written on every commit, so all four are checked. The bounds live on the
1288 // elements - speed Ki has a much tighter ceiling than the other three - and rangeCheck() reads
1289 // them from there, so this loop never needs to know that.
1290 for (int i = 0; i < 4; i++)
1291 {
1292 char field[64] = {0};
1293 snprintf(field, sizeof(field), "%s %s", label, np[i].getLabel());
1294
1295 if (!rangeCheck(np, i, wanted[i], field))
1296 return true;
1297 }
1298
1299 // Order matters: all four gains, then the commit.
1300 static const char *suffix[4] = {"Ap", "Ai", "Sp", "Si"};
1301 char cmd[CMD_MAX_LEN] = {0};
1302
1303 for (int i = 0; i < 4; i++)
1304 {
1305 snprintf(cmd, sizeof(cmd), "%s%s%d#", axisPrefix, suffix[i], static_cast<int>(std::lround(wanted[i])));
1306
1307 if (!sendVendorSetter(cmd))
1308 {
1309 // Abandoning here is safe precisely because the commit below never goes out: without
1310 // it the mount does not retain any of the gains, so it keeps its previous set rather
1311 // than a half-written one.
1312 np.setState(IPS_ALERT);
1313 np.apply("The mount did not accept the %s PID gains.", label);
1314 return true;
1315 }
1316 }
1317
1318 snprintf(cmd, sizeof(cmd), "%sr#", axisPrefix);
1319
1320 if (!sendVendorSetter(cmd))
1321 {
1322 np.setState(IPS_ALERT);
1323 np.apply("The mount did not accept the %s PID gain commit.", label);
1324 return true;
1325 }
1326
1327 // Unlike the LED, the readback here is worth acting on: it confirms the gains were stored,
1328 // and a mismatch means a value did not stick.
1329 //
1330 // But the commit really does restart the axis, and while it is coming back the mount reports
1331 // every gain as zero. Measured on a UMi20S: ":Pnag0#" answered "0|0|0|0|0|0|0#" 68ms after the
1332 // commit acknowledgement and "300|200|200|50|0|0|0#" 7ms after that. A single read lands in
1333 // that window and reports a mismatch that never happened. This is the only readback in the
1334 // driver that retries against a measured transient.
1335 double actual[4] = {0, 0, 0, 0};
1336 const struct timespec axisSettle = {0, 20000000L}; // 20ms
1337 const double restartDeadline = monotonicSeconds() + PROXISKY_PID_RESTART_SECONDS;
1338 bool readOk = false;
1339
1340 while (true)
1341 {
1342 readOk = getVendorQuad(readCmd, actual);
1343
1344 // All four zero is also the vendor's "unsupported" sentinel, but this axis answered its
1345 // gains moments ago, so here it can only mean the restart is still in progress.
1346 const bool restarting = readOk && actual[0] == 0 && actual[1] == 0 && actual[2] == 0 && actual[3] == 0;
1347
1348 if ((readOk && !restarting) || monotonicSeconds() >= restartDeadline)
1349 break;
1350
1351 sleepFor(axisSettle);
1352 }
1353
1354 if (!readOk)
1355 {
1356 np.setState(IPS_ALERT);
1357 np.apply();
1358 LOGF_ERROR("Could not read back %s PID gains (%s) after the write.", label, readCmd);
1359 return true;
1360 }
1361
1362 // Keep the requested values on display. These gains only take effect after a power cycle,
1363 // so the readback returning the still-running values is the expected outcome, not a failed
1364 // write - overwriting the property with them would silently discard what the user asked for
1365 // and invite them to "correct" it back to the old gains.
1366 bool matched = true;
1367
1368 for (int i = 0; i < 4; i++)
1369 {
1370 np[i].setValue(wanted[i]);
1371
1372 if (std::lround(actual[i]) != std::lround(wanted[i]))
1373 matched = false;
1374 }
1375
1376 // IPS_OK for the same reason as handlePendingSwitch: the write is complete and verified, so
1377 // there is nothing left in flight for a IPS_BUSY to represent. The power cycle is in the message.
1378 np.setState(IPS_OK);
1379 np.apply("%s PID gains stored. Power cycle the mount for them to take effect.", label);
1380
1381 if (matched)
1382 LOGF_INFO("Mount stored %s PID gains; power cycle to apply them.", label);
1383 else
1384 LOGF_INFO("Mount still reports the previous %s PID gains (%.0f|%.0f|%.0f|%.0f); "
1385 "the new ones apply at next power up.", label, actual[0], actual[1], actual[2], actual[3]);
1386
1387 return true;
1388}
1389
1390bool LX200_Proxisky::refreshAcsCounters(bool stickyOnFailure)
1391{
1392 double raCount = 0, decCount = 0;
1393
1394 if (!getVendorPair(":Pnccg#", raCount, decCount))
1395 {
1396 // Only the polling path latches the sticky flag. The probe-time call happens during
1397 // connect, in the same window where the mount is still waking up and vendor commands
1398 // are unreliable - letting one boot-time timeout disable the counters for the whole
1399 // session would be wrong, and probeAcs() does not run again to undo it.
1400 AcsCollisionNP.setState(IPS_ALERT);
1401
1402 if (m_PropertiesDefined)
1403 AcsCollisionNP.apply();
1404
1405 if (stickyOnFailure)
1406 {
1407 m_AcsCountersGood = false;
1408 LOG_WARN("Collision counters did not respond (:Pnccg#), disabling further checks this session.");
1409 }
1410 else
1411 {
1412 LOG_WARN("Collision counters did not respond (:Pnccg#) during startup.");
1413 }
1414
1415 return false;
1416 }
1417
1418 AcsCollisionNP[0].setValue(raCount);
1419 AcsCollisionNP[1].setValue(decCount);
1420 AcsCollisionNP.setState(IPS_OK);
1421
1422 // defineVendorProperties() has not run yet when this is called from probeAcs(); applying a
1423 // property the client has never been sent a definition for is dropped and logged as unknown.
1424 if (m_PropertiesDefined)
1425 AcsCollisionNP.apply();
1426
1427 return true;
1428}
1429
1431{
1433 return false;
1434
1435 // Nothing slow happens while the mount is actually moving. TrackState is NOT SCOPE_SLEWING
1436 // during manual N/S/E/W motion - only MovementNSSP/MovementWESP show it - and the parent
1437 // early-returns for that case precisely to keep RA/Dec updates
1438 // fast. A vendor command here would undo that. SCOPE_PARKED and SCOPE_TRACKING both count
1439 // as settled: excluding them would mean a mount parked at connect never gets probed.
1440 const bool moving = (MovementNSSP.getState() == IPS_BUSY) || (MovementWESP.getState() == IPS_BUSY);
1441 const bool settled = (TrackState != SCOPE_SLEWING && TrackState != SCOPE_PARKING) && !moving;
1442
1443 // Poll the collision counters, but sparingly. Vendor commands are slow relative to the
1444 // status cadence, so doing this every cycle would compete with OnStep's own polling for
1445 // the port. Skipped while ACS is switched off.
1446 if (m_HasAcs && m_AcsEnabled && m_AcsCountersGood && settled)
1447 {
1448 if (++m_AcsPollDivider >= PROXISKY_ACS_POLL_DIVIDER)
1449 {
1450 m_AcsPollDivider = 0;
1452 }
1453 }
1454
1455 return true;
1456}
1457
1458/***************************************************************************************
1459** Readback after write
1460***************************************************************************************/
1461
1462bool LX200_Proxisky::refreshRALimits(double expectedLeft, double expectedRight)
1463{
1464 double left = 0, right = 0;
1465
1466 if (!getVendorPair(":Pnbg#", left, right))
1467 {
1468 RALimitNP.setState(IPS_ALERT);
1469 RALimitNP.apply();
1470 LOG_ERROR("Could not read back RA travel limits (:Pnbg#).");
1471 return false;
1472 }
1473
1474 RALimitNP[0].setValue(left);
1475 RALimitNP[1].setValue(right);
1476
1477 const bool matched = (std::lround(left) == std::lround(expectedLeft) &&
1478 std::lround(right) == std::lround(expectedRight));
1479
1480 RALimitNP.setState(matched ? IPS_OK : IPS_ALERT);
1481
1482 if (matched)
1483 {
1484 RALimitNP.apply("RA travel limits set to %.0f / %.0f degrees.", left, right);
1485 LOGF_INFO("RA travel limits set to left %.0f, right %.0f degrees.", left, right);
1486 }
1487 else
1488 {
1489 RALimitNP.apply("Mount reports RA travel limits %.0f / %.0f after requesting %.0f / %.0f.",
1490 left, right, expectedLeft, expectedRight);
1491 LOGF_WARN("Mount reports RA travel limits %.0f|%.0f after requesting %.0f|%.0f.", left, right,
1492 expectedLeft, expectedRight);
1493 }
1494
1495 return matched;
1496}
1497
1498bool LX200_Proxisky::refreshDecLimits(double expectedCCW, double expectedCW)
1499{
1500 double ccw = 0, cw = 0;
1501
1502 if (!getVendorPair(":PsSdG#", ccw, cw))
1503 {
1504 DecLimitNP.setState(IPS_ALERT);
1505 DecLimitNP.apply();
1506 LOG_ERROR("Could not read back Dec travel limits (:PsSdG#).");
1507 return false;
1508 }
1509
1510 DecLimitNP[0].setValue(ccw);
1511 DecLimitNP[1].setValue(cw);
1512
1513 const bool matched = (std::lround(ccw) == std::lround(expectedCCW) &&
1514 std::lround(cw) == std::lround(expectedCW));
1515
1516 DecLimitNP.setState(matched ? IPS_OK : IPS_ALERT);
1517
1518 if (matched)
1519 {
1520 DecLimitNP.apply("Dec travel limits set to %.0f / %.0f degrees.", ccw, cw);
1521 LOGF_INFO("Dec travel limits set to CCW %.0f, CW %.0f degrees.", ccw, cw);
1522 }
1523 else
1524 {
1525 DecLimitNP.apply("Mount reports Dec travel limits %.0f / %.0f after requesting %.0f / %.0f.",
1526 ccw, cw, expectedCCW, expectedCW);
1527 LOGF_WARN("Mount reports Dec travel limits %.0f|%.0f after requesting %.0f|%.0f.", ccw, cw,
1528 expectedCCW, expectedCW);
1529 }
1530
1531 return matched;
1532}
1533
1534bool LX200_Proxisky::refreshAcsThresholds(double expectedRA, double expectedDec)
1535{
1536 double raThreshold = 0, decThreshold = 0;
1537
1538 if (!getVendorPair(":Pncg#", raThreshold, decThreshold))
1539 {
1540 AcsThresholdNP.setState(IPS_ALERT);
1541 AcsThresholdNP.apply();
1542 LOG_ERROR("Could not read back anti-collision thresholds (:Pncg#).");
1543 return false;
1544 }
1545
1546 AcsThresholdNP[0].setValue(raThreshold);
1547 AcsThresholdNP[1].setValue(decThreshold);
1548
1549 const bool matched = (std::lround(raThreshold) == std::lround(expectedRA) &&
1550 std::lround(decThreshold) == std::lround(expectedDec));
1551
1552 AcsThresholdNP.setState(matched ? IPS_OK : IPS_ALERT);
1553
1554 if (matched)
1555 {
1556 AcsThresholdNP.apply("Anti-collision thresholds set to RA %.0f / Dec %.0f.", raThreshold, decThreshold);
1557 LOGF_INFO("Anti-collision thresholds set to RA %.0f, Dec %.0f.", raThreshold, decThreshold);
1558 }
1559 else
1560 {
1561 AcsThresholdNP.apply("Mount reports anti-collision thresholds %.0f / %.0f after requesting %.0f / %.0f.",
1562 raThreshold, decThreshold, expectedRA, expectedDec);
1563 LOGF_WARN("Mount reports anti-collision thresholds %.0f|%.0f after requesting %.0f|%.0f.", raThreshold,
1564 decThreshold, expectedRA, expectedDec);
1565 }
1566
1567 return matched;
1568}
1569
1570/***************************************************************************************
1571** Client writes
1572***************************************************************************************/
1573
1574bool LX200_Proxisky::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n)
1575{
1576 if (dev != nullptr && strcmp(dev, getDeviceName()) == 0)
1577 {
1578 if (RALimitNP.isNameMatch(name))
1579 {
1580 if (!m_HasRALimits)
1581 return reportUnavailable(RALimitNP, "RA limits are not available on this mount.");
1582
1583 // Seeded from the property so a partial vector still has something to send, but in
1584 // practice every element arrives: a number vector is one field group behind a single
1585 // Set button, so the client transmits all of it. The values on screen are what the
1586 // user is looking at and what they mean to write.
1587 double left = RALimitNP[0].getValue();
1588 double right = RALimitNP[1].getValue();
1589
1590 for (int i = 0; i < n; i++)
1591 {
1592 if (RALimitNP[0].isNameMatch(names[i]))
1593 {
1594 left = values[i];
1595 LOGF_DEBUG("===CMD==> RA limit left= %f", left);
1596 }
1597 else if (RALimitNP[1].isNameMatch(names[i]))
1598 {
1599 right = values[i];
1600 LOGF_DEBUG("===CMD==> RA limit right= %f", right);
1601 }
1602 }
1603
1604 // Both values go out on every write, so both are checked - short-circuiting on the
1605 // first bad one, which is also the one worth naming.
1606 if (!rangeCheck(RALimitNP, 0, left, "RA left limit", " degrees") ||
1607 !rangeCheck(RALimitNP, 1, right, "RA right limit", " degrees"))
1608 return true;
1609
1610 char cmd[CMD_MAX_LEN] = {0};
1611 snprintf(cmd, sizeof(cmd), ":Pnbsl%d#", static_cast<int>(std::lround(left)));
1612 const bool sentLeft = sendVendorSetter(cmd);
1613 snprintf(cmd, sizeof(cmd), ":Pnbsr%d#", static_cast<int>(std::lround(right)));
1614 const bool sentRight = sendVendorSetter(cmd);
1615
1616 // Refresh even when a setter was refused. The two limits are written separately, so a
1617 // refusal can leave one landed and the other not; only the readback says which, and
1618 // skipping it would leave the panel showing a pair the mount never took.
1619 if (!sentLeft || !sentRight)
1620 LOGF_ERROR("The mount refused the RA %s travel limit.",
1621 !sentLeft ? (!sentRight ? "left and right" : "left") : "right");
1622
1623 refreshRALimits(left, right);
1624 return true;
1625 }
1626
1627 if (RaPidNP.isNameMatch(name))
1628 {
1629 if (!m_HasRaPid)
1630 return reportUnavailable(RaPidNP, "RA/Azm PID gains are not available on this mount.");
1631
1632 return handlePidWrite(RaPidNP, ":Pnas0", ":Pnag0#", "RA/Azm", values, names, n);
1633 }
1634
1635 if (DecPidNP.isNameMatch(name))
1636 {
1637 if (!m_HasDecPid)
1638 return reportUnavailable(DecPidNP, "Dec/Alt PID gains are not available on this mount.");
1639
1640 return handlePidWrite(DecPidNP, ":Pnas1", ":Pnag1#", "Dec/Alt", values, names, n);
1641 }
1642
1643 if (SuperGotoToleranceNP.isNameMatch(name))
1644 {
1645 if (!m_HasSuperGotoTolerance)
1646 return reportUnavailable(SuperGotoToleranceNP,
1647 "The Supervised GOTO tolerance is not available on this mount.");
1648
1649 double tolerance = SuperGotoToleranceNP[0].getValue();
1650
1651 for (int i = 0; i < n; i++)
1652 {
1653 if (SuperGotoToleranceNP[0].isNameMatch(names[i]))
1654 {
1655 tolerance = values[i];
1656 LOGF_DEBUG("===CMD==> Supervised GOTO tolerance= %f", tolerance);
1657 }
1658 }
1659
1660 if (!rangeCheck(SuperGotoToleranceNP, 0, tolerance, "Supervised GOTO tolerance", " degrees"))
1661 return true;
1662
1663 char cmd[CMD_MAX_LEN] = {0};
1664 snprintf(cmd, sizeof(cmd), ":Pndsv%d#", static_cast<int>(std::lround(tolerance)));
1665
1666 // Refresh regardless - see the RA travel limits above for why a refusal still needs
1667 // the readback.
1668 if (!sendVendorSetter(cmd))
1669 LOG_ERROR("The mount refused the Supervised GOTO tolerance.");
1670
1671 refreshSuperGotoTolerance(tolerance);
1672 return true;
1673 }
1674
1675 if (AcsThresholdNP.isNameMatch(name))
1676 {
1677 if (!m_HasAcs)
1678 return reportUnavailable(AcsThresholdNP, "The anti-collision system is not available on this mount.");
1679
1680 double raThreshold = AcsThresholdNP[0].getValue();
1681 double decThreshold = AcsThresholdNP[1].getValue();
1682
1683 for (int i = 0; i < n; i++)
1684 {
1685 if (AcsThresholdNP[0].isNameMatch(names[i]))
1686 {
1687 raThreshold = values[i];
1688 LOGF_DEBUG("===CMD==> ACS RA threshold= %f", raThreshold);
1689 }
1690 else if (AcsThresholdNP[1].isNameMatch(names[i]))
1691 {
1692 decThreshold = values[i];
1693 LOGF_DEBUG("===CMD==> ACS Dec threshold= %f", decThreshold);
1694 }
1695 }
1696
1697 // Both values go out on every write, so both are checked - see the RA limit handler
1698 // above. The lower bounds are asymmetric (RA accepts 10, Dec only 100), which is
1699 // another reason to read them off the elements rather than repeat them here.
1700 if (!rangeCheck(AcsThresholdNP, 0, raThreshold, "RA collision threshold") ||
1701 !rangeCheck(AcsThresholdNP, 1, decThreshold, "Dec collision threshold"))
1702 return true;
1703
1704 char cmd[CMD_MAX_LEN] = {0};
1705 snprintf(cmd, sizeof(cmd), ":Pncsr%d#", static_cast<int>(std::lround(raThreshold)));
1706 const bool sentRa = sendVendorSetter(cmd);
1707 snprintf(cmd, sizeof(cmd), ":Pncsd%d#", static_cast<int>(std::lround(decThreshold)));
1708 const bool sentDec = sendVendorSetter(cmd);
1709
1710 // Refresh regardless - see the RA travel limits above for why a refusal still needs
1711 // the readback.
1712 if (!sentRa || !sentDec)
1713 LOGF_ERROR("The mount refused the %s anti-collision threshold.",
1714 !sentRa ? (!sentDec ? "RA and Dec" : "RA") : "Dec");
1715
1716 refreshAcsThresholds(raThreshold, decThreshold);
1717 return true;
1718 }
1719
1720 if (DecLimitNP.isNameMatch(name))
1721 {
1722 if (!m_HasDecLimits)
1723 return reportUnavailable(DecLimitNP, "Dec limits are not available on this mount.");
1724
1725 double ccw = DecLimitNP[0].getValue();
1726 double cw = DecLimitNP[1].getValue();
1727
1728 for (int i = 0; i < n; i++)
1729 {
1730 if (DecLimitNP[0].isNameMatch(names[i]))
1731 {
1732 ccw = values[i];
1733 LOGF_DEBUG("===CMD==> Dec limit CCW= %f", ccw);
1734 }
1735 else if (DecLimitNP[1].isNameMatch(names[i]))
1736 {
1737 cw = values[i];
1738 LOGF_DEBUG("===CMD==> Dec limit CW= %f", cw);
1739 }
1740 }
1741
1742 // Both values go out on every write, so both are checked - see the RA limit handler above.
1743 if (!rangeCheck(DecLimitNP, 0, ccw, "Dec CCW limit", " degrees") ||
1744 !rangeCheck(DecLimitNP, 1, cw, "Dec CW limit", " degrees"))
1745 return true;
1746
1747 char cmd[CMD_MAX_LEN] = {0};
1748 snprintf(cmd, sizeof(cmd), ":PsSdl%d#", static_cast<int>(std::lround(ccw)));
1749 const bool sentCCW = sendVendorSetter(cmd);
1750 snprintf(cmd, sizeof(cmd), ":PsSdr%d#", static_cast<int>(std::lround(cw)));
1751 const bool sentCW = sendVendorSetter(cmd);
1752
1753 // Refresh regardless - see the RA travel limits above for why a refusal still needs
1754 // the readback.
1755 if (!sentCCW || !sentCW)
1756 LOGF_ERROR("The mount refused the Dec %s travel limit.",
1757 !sentCCW ? (!sentCW ? "counter-clockwise and clockwise" : "counter-clockwise") : "clockwise");
1758
1759 refreshDecLimits(ccw, cw);
1760 return true;
1761 }
1762 }
1763
1764 return LX200_OnStep::ISNewNumber(dev, name, values, names, n);
1765}
1766
1767bool LX200_Proxisky::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n)
1768{
1769 if (dev != nullptr && strcmp(dev, getDeviceName()) == 0)
1770 {
1771 // Settings that apply immediately.
1772 if (PowerLossSP.isNameMatch(name))
1773 {
1774 if (!m_HasPowerLoss)
1775 return reportUnavailable(PowerLossSP, "Power-loss memory is not available on this mount.");
1776
1777 return handleBoolSwitch(PowerLossSP, ":Pscs1#", ":Pscs0#", ":Pscg#", "power loss memory", states, names, n);
1778 }
1779
1780 if (AsiairSP.isNameMatch(name))
1781 {
1782 if (!m_HasAsiair)
1783 return reportUnavailable(AsiairSP, "ASIAIR homing is not available on this mount.");
1784
1785 return handleBoolSwitch(AsiairSP, ":PsAs1#", ":PsAs0#", ":PsAg#", "ASIAIR homing", states, names, n);
1786 }
1787
1788 if (SuperHomeSP.isNameMatch(name))
1789 {
1790 if (!m_HasSuperHome)
1791 return reportUnavailable(SuperHomeSP, "Supervised Home is not available on this mount.");
1792
1793 return handleBoolSwitch(SuperHomeSP, ":PsShs1#", ":PsShs0#", ":PsShg#", "Supervised Home", states, names, n);
1794 }
1795
1796 // Reads :PsDgb#, writes :PsDs0|1# - the read and write halves are not symmetric.
1797 if (DecSecHomeSP.isNameMatch(name))
1798 {
1799 if (!m_HasDecSecHome)
1800 return reportUnavailable(DecSecHomeSP, "Dec second home is not available on this mount.");
1801
1802 return handleBoolSwitch(DecSecHomeSP, ":PsDs1#", ":PsDs0#", ":PsDgb#", "Dec second home", states, names, n);
1803 }
1804
1805 // Reads :Pndgo#, writes :Pndss0|1# - likewise asymmetric.
1806 if (SuperGotoSP.isNameMatch(name))
1807 {
1808 if (!m_HasSuperGoto)
1809 return reportUnavailable(SuperGotoSP, "Supervised GOTO is not available on this mount.");
1810
1811 return handleBoolSwitch(SuperGotoSP, ":Pndss1#", ":Pndss0#", ":Pndgo#", "Supervised GOTO", states, names, n);
1812 }
1813
1814 if (RALimitEnableSP.isNameMatch(name))
1815 {
1816 if (!m_HasRALimits)
1817 return reportUnavailable(RALimitEnableSP, "RA limits are not available on this mount.");
1818
1819 return handleBoolSwitch(RALimitEnableSP, ":PsSrs1#", ":PsSrs0#", ":PsSrg#", "RA limits", states, names, n);
1820 }
1821
1822 if (DecLimitEnableSP.isNameMatch(name))
1823 {
1824 if (!m_HasDecLimits)
1825 return reportUnavailable(DecLimitEnableSP, "Dec limits are not available on this mount.");
1826
1827 return handleBoolSwitch(DecLimitEnableSP, ":PsSds1#", ":PsSds0#", ":PsSdg#", "Dec limits", states, names, n);
1828 }
1829
1830 // Settings that only take effect after a power cycle.
1831 if (LedSP.isNameMatch(name))
1832 {
1833 if (!m_HasLed)
1834 return reportUnavailable(LedSP, "This mount has no status LED.");
1835
1836 return handlePendingSwitch(LedSP, ":PsLs1#", ":PsLs0#", ":PsLgb#", "Status LED", states, names, n);
1837 }
1838
1839 if (AutoTrackSP.isNameMatch(name))
1840 {
1841 if (!m_HasAutoTrack)
1842 return reportUnavailable(AutoTrackSP, "Auto tracking is not available on this mount.");
1843
1844 return handlePendingSwitch(AutoTrackSP, ":Psas1#", ":Psas0#", ":Psag#", "Auto tracking", states, names, n);
1845 }
1846
1847 if (AcsEnableSP.isNameMatch(name))
1848 {
1849 if (!m_HasAcs)
1850 return reportUnavailable(AcsEnableSP, "The anti-collision system is not available on this mount.");
1851
1852 // mirror keeps m_AcsEnabled in step with what the mount reports, which is what the
1853 // counter polling in ReadScopeStatus() gates on.
1854 handleBoolSwitch(AcsEnableSP, ":PsCs1#", ":PsCs0#", ":PsCg#", "anti-collision", states, names, n,
1855 &m_AcsEnabled);
1856
1857 // Switching the feature on is an explicit user action, so give the collision
1858 // counters another chance even if an earlier read had disabled polling.
1859 if (m_AcsEnabled)
1860 {
1861 m_AcsCountersGood = true;
1862 m_AcsPollDivider = PROXISKY_ACS_POLL_DIVIDER;
1863 }
1864
1865 return true;
1866 }
1867
1868 if (AcsClearSP.isNameMatch(name))
1869 {
1870 if (!m_HasAcs)
1871 return reportUnavailable(AcsClearSP, "The anti-collision system is not available on this mount.");
1872
1873 // update() has already sent its own IDSetSwitch on rejection; adding a second message
1874 // here would only contradict it.
1875 if (!AcsClearSP.update(states, names, n))
1876 return true;
1877
1878 // Clearing the counters is destructive and irreversible, so act only on an explicit
1879 // ON. ISR_ATMOST1 permits a client to send OFF - ":CLEAR=Off" from indi_setprop, or
1880 // an untoggle in a client that mirrors switch state - and that must not fire it.
1881 if (AcsClearSP[0].getState() != ISS_ON)
1882 {
1883 AcsClearSP.reset();
1884 AcsClearSP.setState(IPS_IDLE);
1885 AcsClearSP.apply();
1886 return true;
1887 }
1888
1889 AcsClearSP.setState(IPS_BUSY);
1890 AcsClearSP.apply();
1891
1892 if (!sendVendorSetter(":Pnccc#"))
1893 {
1894 AcsClearSP.reset();
1895 AcsClearSP.setState(IPS_ALERT);
1896 AcsClearSP.apply("The mount did not accept the counter clear.");
1897 return true;
1898 }
1899
1900 // Momentary action - the button never stays latched. Whether the clear worked is
1901 // reported by the counters themselves, so re-read them regardless of the sticky
1902 // polling flag.
1903 AcsClearSP.reset();
1904 m_AcsCountersGood = true;
1905
1906 // Not sticky: the user just asked for this, so a single failure should not disable
1907 // polling for the session (and would undo the line above).
1908 if (refreshAcsCounters(false))
1909 {
1910 AcsClearSP.setState(IPS_OK);
1911 LOG_INFO("Collision counters cleared.");
1912 }
1913 else
1914 {
1915 AcsClearSP.setState(IPS_ALERT);
1916 }
1917
1918 AcsClearSP.apply();
1919 return true;
1920 }
1921
1922 }
1923
1924 return LX200_OnStep::ISNewSwitch(dev, name, states, names, n);
1925}
bool isConnected() const
INDI::PropertyText getText(const char *name) const
void setVersion(uint16_t vMajor, uint16_t vMinor)
Set driver version information to be defined in DRIVER_INFO property as vMajor.vMinor.
virtual bool deleteProperty(const char *propertyName)
Delete a property and unregister it. It will also be deleted from all clients.
void defineProperty(INumberVectorProperty *property)
bool isSimulation() const
void setState(IPState state)
void apply(const char *format,...) const ATTRIBUTE_FORMAT_PRINTF(2
bool isNameMatch(const char *otherName) const
void fill(const char *device, const char *name, const char *label, const char *group, IPerm permission, double timeout, IPState state)
bool update(const ISState states[], const char *const names[], int n)
void fill(const char *device, const char *name, const char *label, const char *group, IPerm permission, ISRule rule, double timeout, IPState state)
void fill(const char *device, const char *name, const char *label, const char *group, IPerm permission, double timeout, IPState state)
TelescopeStatus TrackState
virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override
Process the client newNumber command.
virtual bool initProperties() override
Called to initialize basic properties required all the time.
int getCommandSingleCharErrorOrLongResponse(int fd, char *data, const char *cmd)
virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override
Process the client newSwitch command.
virtual bool updateProperties() override
Called when connected state changes, to add/remove properties.
virtual bool ReadScopeStatus() override
Read telescope status.
bool getVendorQuad(const char *cmd, double *out)
bool refreshRALimits(double expectedLeft, double expectedRight)
bool handlePidWrite(INDI::PropertyNumber &np, const char *axisPrefix, const char *readCmd, const char *label, double values[], char *names[], int n)
bool getVendorBool(const char *cmd, bool &value)
virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override
Process the client newNumber command.
virtual bool updateProperties() override
Called when connected state changes, to add/remove properties.
void deleteVendorProperties()
bool readVendorStringOnce(const char *cmd, char *out, size_t outLen, bool requireTerminated, int *rcOut)
bool getVendorString(const char *cmd, char *out, size_t outLen, bool requireTerminated=false, int *rcOut=nullptr)
bool refreshDecLimits(double expectedCCW, double expectedCW)
bool refreshSuperGotoTolerance(double expected)
bool probePidAxis(INDI::PropertyNumber &np, const char *readCmd, const char *label)
virtual bool ReadScopeStatus() override
Read telescope status.
bool probeBoolSwitch(INDI::PropertySwitch &sp, const char *readCmd, const char *label)
bool rangeCheck(INDI::PropertyNumber &np, int index, double value, const char *what, const char *unit="")
virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override
Process the client newSwitch command.
void defineVendorProperties()
virtual bool initProperties() override
Called to initialize basic properties required all the time.
bool sendVendorSetter(const char *cmd)
bool handleBoolSwitch(INDI::PropertySwitch &sp, const char *setOn, const char *setOff, const char *readCmd, const char *label, ISState *states, char *names[], int n, bool *mirror=nullptr)
virtual const char * getDefaultName() override
bool setReportedValues(INDI::PropertyNumber &np, const double *values, int count)
bool getVendorPair(const char *cmd, double &first, double &second)
bool refreshAcsCounters(bool stickyOnFailure=true)
bool handlePendingSwitch(INDI::PropertySwitch &sp, const char *setOn, const char *setOff, const char *readCmd, const char *label, ISState *states, char *names[], int n)
bool refreshAcsThresholds(double expectedRA, double expectedDec)
int errno
INDI::PropertySwitch MovementNSSP
INDI::PropertySwitch MovementWESP
ISState
Switch state.
Definition indiapi.h:148
@ ISS_OFF
Definition indiapi.h:149
@ ISS_ON
Definition indiapi.h:150
@ IP_RW
Definition indiapi.h:184
@ IP_RO
Definition indiapi.h:182
@ IPS_BUSY
Definition indiapi.h:161
@ IPS_ALERT
Definition indiapi.h:162
@ IPS_IDLE
Definition indiapi.h:159
@ IPS_OK
Definition indiapi.h:160
@ ISR_1OFMANY
Definition indiapi.h:171
@ ISR_ATMOST1
Definition indiapi.h:172
#define LOGF_INFO(fmt,...)
Definition indilogger.h:82
#define LOGF_WARN(fmt,...)
Definition indilogger.h:81
#define LOG_WARN(txt)
Definition indilogger.h:73
#define LOGF_DEBUG(fmt,...)
Definition indilogger.h:83
#define LOG_ERROR(txt)
Shorter logging macros. In order to use these macros, the function (or method) "getDeviceName()" must...
Definition indilogger.h:72
#define LOGF_ERROR(fmt,...)
Definition indilogger.h:80
#define LOG_INFO(txt)
Definition indilogger.h:74
#define getDeviceName
#define RB_MAX_LEN
#define PROXISKY_MODEL_PROBE_SECONDS
#define PROXISKY_LIMITS_TAB
#define PROXISKY_PROBE_BUDGET_SECONDS
#define PROXISKY_ACS_TAB
#define PROXISKY_PROBE_RETRY_NSEC
#define PROXISKY_PID_RESTART_SECONDS
#define PROXISKY_ADVANCED_TAB
#define PROXISKY_MODEL_PROBE_MAX_ATTEMPTS
#define PROXISKY_SETTINGS_TAB
#define PROXISKY_ACS_POLL_DIVIDER
uint8_t cmd[4]
Definition pwc-ioctl.h:2
#define CMD_MAX_LEN