Instrument Neutral Distributed Interface INDI 2.2.5
ikunfocuser.cpp
Go to the documentation of this file.
1/*
2 INDI driver for IKunFocuser
3 Copyright (C) 2026 IKunFocuser contributors
4 SPDX-License-Identifier: LGPL-2.1-or-later
5
6 This library is free software; you can redistribute it and/or
7 modify it under the terms of the GNU Lesser General Public
8 License as published by the Free Software Foundation; either
9 version 2.1 of the License, or (at your option) any later version.
10
11 This library is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 Lesser General Public License for more details.
15*/
16
17#include "ikunfocuser.h"
18
21#include "indicom.h"
22
23#include <algorithm>
24#include <cstdio>
25#include <cstring>
26#include <memory>
27#include <string>
28
29#include <termios.h>
30#include <unistd.h>
31
32static std::unique_ptr<IKunFocuser> ikunFocuser(new IKunFocuser());
33
41
43{
44 return "IKun Focuser";
45}
46
48{
50
51 // Speed is fixed at 300 steps/s for reliable torque with 28BYJ-48.
52 m_FixedSpeed = 300;
53
54 FocusMaxPosNP[0].setMin(100);
55 FocusMaxPosNP[0].setMax(9999999);
56 FocusMaxPosNP[0].setStep(1000);
57 FocusMaxPosNP[0].setValue(816000);
58 applyMaximumPosition(816000);
59
60 AccelerationNP[0].fill("ACCELERATION_VALUE", "Steps/s²", "%.f", 1, 10000, 10, 1000);
61 AccelerationNP.fill(getDeviceName(), "FOCUS_ACCELERATION", "Acceleration", MAIN_CONTROL_TAB, IP_RW, 60, IPS_IDLE);
62
63 HoldSP[HOLD_ON].fill("HOLD_ON", "Enabled", ISS_OFF);
64 HoldSP[HOLD_OFF].fill("HOLD_OFF", "Disabled", ISS_ON);
65 HoldSP.fill(getDeviceName(), "FOCUS_MOTOR_HOLD", "Motor Hold", MAIN_CONTROL_TAB, IP_RW, ISR_1OFMANY, 60, IPS_IDLE);
66
67 TemperatureNP[0].fill("TEMPERATURE", "Celsius", "%6.2f", -55, 125, 0, 20);
68 TemperatureNP.fill(getDeviceName(), "FOCUS_TEMPERATURE", "Temperature", MAIN_CONTROL_TAB, IP_RO, 60, IPS_IDLE);
69
70 FirmwareTP[FIRMWARE_VERSION].fill("VERSION", "Firmware", "Unknown");
71 FirmwareTP[CONTROLLER_MODEL].fill("MODEL", "Controller", "Unknown");
72 FirmwareTP.fill(getDeviceName(), "FOCUS_FIRMWARE", "Firmware", MAIN_CONTROL_TAB, IP_RO, 60, IPS_IDLE);
73
75 tcpConnection->setDefaultHost("192.168.4.1");
77
79 return true;
80}
81
83{
85
86 if (isConnected())
87 {
88 defineProperty(AccelerationNP);
89 defineProperty(HoldSP);
90 defineProperty(TemperatureNP);
91 defineProperty(FirmwareTP);
92
93 if (readFullState())
94 {
95 // Apply fixed speed on every connect.
96 SetFocuserSpeed(m_FixedSpeed);
100 AccelerationNP.apply();
101 HoldSP.apply();
102 TemperatureNP.apply();
103 FirmwareTP.apply();
104 LOG_INFO("IKunFocuser parameters updated; focuser is ready.");
105 }
106 else
107 {
108 LOG_WARN("Connected to IKunFocuser, but one or more startup values could not be read.");
109 }
110 }
111 else
112 {
113 deleteProperty(AccelerationNP);
114 deleteProperty(HoldSP);
115 deleteProperty(TemperatureNP);
116 deleteProperty(FirmwareTP);
117 }
118
119 return true;
120}
121
123{
125 {
126 // ESP8266 USB bridges reset the controller when the serial port opens.
127 usleep(2200000);
128 tcflush(PortFD, TCIOFLUSH);
129 }
130
131 char response[RESPONSE_SIZE] = { 0 };
132 if (!sendCommand("#", response, sizeof(response), true) ||
134 {
135 LOG_ERROR("IKunFocuser identification failed. Check the selected port, power, and firmware.");
136 return false;
137 }
138
139 char identity[128] = { 0 };
140 std::snprintf(identity, sizeof(identity), "%s", response);
141
142 if (!sendCommand("V#", response, sizeof(response), true) ||
143 !IKunFocuserProtocol::parseVersion(response, m_FirmwareVersion))
144 {
145 LOG_ERROR("IKunFocuser firmware version query failed.");
146 return false;
147 }
148
149 char versionText[16] = { 0 };
150 std::snprintf(versionText, sizeof(versionText), "%d", m_FirmwareVersion);
151 FirmwareTP[FIRMWARE_VERSION].setText(versionText);
152 FirmwareTP[CONTROLLER_MODEL].setText(IKunFocuserProtocol::modelForVersion(m_FirmwareVersion));
153
154 if (!IKunFocuserProtocol::isSupportedVersion(m_FirmwareVersion))
155 {
156 LOGF_ERROR("Firmware %d is outside the supported release range. Upgrade the IKunFocuser firmware.",
157 m_FirmwareVersion);
158 return false;
159 }
160
161 LOGF_INFO("Connected to %s, firmware %d.", identity, m_FirmwareVersion);
162 return true;
163}
164
165bool IKunFocuser::sendCommand(const char *command, char *response, std::size_t responseSize, bool silent)
166{
167 if (response == nullptr || responseSize < 2)
168 return false;
169
171 tcflush(PortFD, TCIFLUSH);
172
173 LOGF_DEBUG("CMD <%s>", command);
174
175 int bytesWritten = 0;
176 int rc = tty_write_string(PortFD, command, &bytesWritten);
177 if (rc != TTY_OK)
178 {
179 if (!silent)
180 {
181 char errorMessage[MAXRBUF] = { 0 };
182 tty_error_msg(rc, errorMessage, MAXRBUF);
183 LOGF_ERROR("IKunFocuser write failed: %s.", errorMessage);
184 }
185 return false;
186 }
187
188 int bytesRead = 0;
189 rc = tty_nread_section(PortFD, response, static_cast<int>(responseSize - 1), '#',
190 IO_TIMEOUT_SECONDS, &bytesRead);
191 if (rc != TTY_OK)
192 {
193 if (!silent)
194 {
195 char errorMessage[MAXRBUF] = { 0 };
196 tty_error_msg(rc, errorMessage, MAXRBUF);
197 LOGF_ERROR("IKunFocuser read failed: %s.", errorMessage);
198 }
199 return false;
200 }
201
202 response[bytesRead] = '\0';
203 LOGF_DEBUG("RES <%s>", response);
204
206 {
207 if (!silent)
208 LOGF_ERROR("IKunFocuser rejected command <%s> with response <%s>.", command, response);
209 return false;
210 }
211
212 return true;
213}
214
215bool IKunFocuser::readMotionStatus(IKunFocuserProtocol::MotionStatus &status, bool silent)
216{
217 char response[RESPONSE_SIZE] = { 0 };
218 if (!sendCommand("G#", response, sizeof(response), silent))
219 return false;
220
221 if (!IKunFocuserProtocol::parseMotionStatus(response, status))
222 {
223 if (!silent)
224 LOGF_ERROR("Invalid IKunFocuser status response <%s>.", response);
225 return false;
226 }
227
228 return true;
229}
230
231void IKunFocuser::applyMaximumPosition(uint32_t maximum)
232{
233 FocusMaxPosNP[0].setValue(maximum);
234
235 FocusAbsPosNP[0].setMin(0);
236 FocusAbsPosNP[0].setMax(maximum);
237 FocusAbsPosNP[0].setStep(std::max(1.0, maximum / 50.0));
238
239 FocusSyncNP[0].setMin(0);
240 FocusSyncNP[0].setMax(maximum);
241 FocusSyncNP[0].setStep(std::max(1.0, maximum / 50.0));
242
243 FocusRelPosNP[0].setMin(0);
244 FocusRelPosNP[0].setMax(maximum / 2.0);
245 FocusRelPosNP[0].setStep(std::max(1.0, maximum / 100.0));
246
250 SyncPresets(maximum);
251}
252
253bool IKunFocuser::readFullState()
254{
255 char response[RESPONSE_SIZE] = { 0 };
256 if (!sendCommand("I#", response, sizeof(response)))
257 return false;
258
259 const std::string json(response);
260 bool foundAny = false;
261 int64_t integerValue = 0;
262 double numberValue = 0;
263 bool booleanValue = false;
264
265 if (IKunFocuserProtocol::parseJsonInteger(json, "maxSteps", integerValue) &&
266 integerValue >= 100 && integerValue <= 9999999)
267 {
268 applyMaximumPosition(static_cast<uint32_t>(integerValue));
269 foundAny = true;
270 }
271
272 if (IKunFocuserProtocol::parseJsonInteger(json, "positionSteps", integerValue) && integerValue >= 0)
273 {
274 FocusAbsPosNP[0].setValue(integerValue);
275 m_LastPosition = static_cast<uint32_t>(integerValue);
276 foundAny = true;
277 }
278
279 if (IKunFocuserProtocol::parseJsonInteger(json, "acceleration", integerValue) &&
280 integerValue >= 1 && integerValue <= 10000)
281 {
282 AccelerationNP[0].setValue(integerValue);
283 foundAny = true;
284 }
285
286 if (IKunFocuserProtocol::parseJsonBoolean(json, "reversed", booleanValue))
287 {
291 foundAny = true;
292 }
293
294 if (IKunFocuserProtocol::parseJsonBoolean(json, "hold", booleanValue))
295 {
296 HoldSP.reset();
297 HoldSP[HOLD_ON].setState(booleanValue ? ISS_ON : ISS_OFF);
298 HoldSP[HOLD_OFF].setState(booleanValue ? ISS_OFF : ISS_ON);
299 foundAny = true;
300 }
301
302 if (IKunFocuserProtocol::parseJsonNumber(json, "lastTemp", numberValue) &&
303 numberValue >= -55 && numberValue <= 125)
304 {
305 TemperatureNP[0].setValue(numberValue);
306 foundAny = true;
307 }
308
309 if (IKunFocuserProtocol::parseJsonBoolean(json, "tempSensorPresent", booleanValue))
310 {
311 TemperatureNP.setState(booleanValue ? IPS_OK : IPS_IDLE);
312 foundAny = true;
313 }
314
315 return foundAny;
316}
317
318void IKunFocuser::applyMotionStatus(const IKunFocuserProtocol::MotionStatus &status)
319{
320 const auto position = static_cast<uint32_t>(status.position);
321 if (position != m_LastPosition)
322 {
323 FocusAbsPosNP[0].setValue(position);
325 m_LastPosition = position;
326 }
327
328 if (status.moving)
329 {
331 {
334 }
335 }
336 else
337 {
339 {
344 LOG_INFO("IKunFocuser reached the requested position.");
345 }
346 }
347}
348
350{
351 char command[64] = { 0 };
352 char response[RESPONSE_SIZE] = { 0 };
353 std::snprintf(command, sizeof(command), "M %u#", targetTicks);
354
355 if (!sendCommand(command, response, sizeof(response)))
356 return IPS_ALERT;
357
359 if (!IKunFocuserProtocol::parseMotionStatus(response, status))
360 return IPS_ALERT;
361
362 m_CommunicationFailures = 0;
363 return status.moving ? IPS_BUSY : IPS_OK;
364}
365
367{
368 const uint32_t current = static_cast<uint32_t>(FocusAbsPosNP[0].getValue());
369 const uint32_t maximum = static_cast<uint32_t>(FocusMaxPosNP[0].getValue());
370 uint32_t target = current;
371
372 if (direction == FOCUS_INWARD)
373 target = ticks > current ? 0 : current - ticks;
374 else
375 target = ticks > maximum - current ? maximum : current + ticks;
376
377 return MoveAbsFocuser(target);
378}
379
381{
382 char response[RESPONSE_SIZE] = { 0 };
383 return sendCommand("S#", response, sizeof(response));
384}
385
386bool IKunFocuser::SyncFocuser(uint32_t ticks)
387{
388 char command[64] = { 0 };
389 char response[RESPONSE_SIZE] = { 0 };
390 std::snprintf(command, sizeof(command), "P %u#", ticks);
391 return sendCommand(command, response, sizeof(response));
392}
393
395{
396 char response[RESPONSE_SIZE] = { 0 };
397 return sendCommand(enabled ? "R 1#" : "R 0#", response, sizeof(response));
398}
399
401{
402 char command[64] = { 0 };
403 char response[RESPONSE_SIZE] = { 0 };
404 std::snprintf(command, sizeof(command), "D %u#", ticks);
405 if (!sendCommand(command, response, sizeof(response)))
406 return false;
407
408 applyMaximumPosition(ticks);
409 return true;
410}
411
413{
414 char command[64] = { 0 };
415 char response[RESPONSE_SIZE] = { 0 };
416 std::snprintf(command, sizeof(command), "X %d#", speed);
417 return sendCommand(command, response, sizeof(response));
418}
419
420bool IKunFocuser::setAcceleration(uint32_t acceleration)
421{
422 char command[64] = { 0 };
423 char response[RESPONSE_SIZE] = { 0 };
424 std::snprintf(command, sizeof(command), "A %u#", acceleration);
425 return sendCommand(command, response, sizeof(response));
426}
427
428bool IKunFocuser::setHold(bool enabled)
429{
430 char response[RESPONSE_SIZE] = { 0 };
431 return sendCommand(enabled ? "C 1#" : "C 0#", response, sizeof(response));
432}
433
434bool IKunFocuser::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n)
435{
436 if (dev != nullptr && std::strcmp(dev, getDeviceName()) == 0 && AccelerationNP.isNameMatch(name))
437 {
438 const double previous = AccelerationNP[0].getValue();
439 AccelerationNP.update(values, names, n);
440 const auto requested = static_cast<uint32_t>(AccelerationNP[0].getValue());
441
442 if (setAcceleration(requested))
443 AccelerationNP.setState(IPS_OK);
444 else
445 {
446 AccelerationNP[0].setValue(previous);
447 AccelerationNP.setState(IPS_ALERT);
448 }
449
450 AccelerationNP.apply();
451 return true;
452 }
453
454 return INDI::Focuser::ISNewNumber(dev, name, values, names, n);
455}
456
457bool IKunFocuser::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n)
458{
459 if (dev != nullptr && std::strcmp(dev, getDeviceName()) == 0 && HoldSP.isNameMatch(name))
460 {
461 const int previous = HoldSP.findOnSwitchIndex();
462 HoldSP.update(states, names, n);
463 const bool requested = HoldSP.findOnSwitchIndex() == HOLD_ON;
464
465 if (setHold(requested))
466 HoldSP.setState(IPS_OK);
467 else
468 {
469 HoldSP.reset();
470 HoldSP[previous >= 0 ? previous : HOLD_OFF].setState(ISS_ON);
471 HoldSP.setState(IPS_ALERT);
472 }
473
474 HoldSP.apply();
475 return true;
476 }
477
478 return INDI::Focuser::ISNewSwitch(dev, name, states, names, n);
479}
480
481void IKunFocuser::markMotionAlert()
482{
484 {
489 }
490}
491
493{
494 if (!isConnected())
495 return;
496
498 if (readMotionStatus(status, true))
499 {
500 m_CommunicationFailures = 0;
501 applyMotionStatus(status);
502 }
503 else if (++m_CommunicationFailures >= MAX_COMMUNICATION_FAILURES)
504 {
505 markMotionAlert();
506 m_CommunicationFailures = 0;
507 LOG_ERROR("Lost communication with IKunFocuser while polling status.");
508 }
509
510 if (++m_TemperaturePollCounter >= 10)
511 {
512 m_TemperaturePollCounter = 0;
513 const double previousMaximum = FocusMaxPosNP[0].getValue();
514 const double previousAcceleration = AccelerationNP[0].getValue();
515 const double previousTemperature = TemperatureNP[0].getValue();
516 const IPState previousTemperatureState = TemperatureNP.getState();
517 const int previousReverse = FocusReverseSP.findOnSwitchIndex();
518 const int previousHold = HoldSP.findOnSwitchIndex();
519
520 if (readFullState())
521 {
522 if (FocusMaxPosNP[0].getValue() != previousMaximum)
524 if (AccelerationNP[0].getValue() != previousAcceleration)
525 AccelerationNP.apply();
526 if (TemperatureNP[0].getValue() != previousTemperature ||
527 TemperatureNP.getState() != previousTemperatureState)
528 TemperatureNP.apply();
529 if (FocusReverseSP.findOnSwitchIndex() != previousReverse)
531 if (HoldSP.findOnSwitchIndex() != previousHold)
532 HoldSP.apply();
533 }
534 }
535
537}
nlohmann::json json
void setDefaultBaudRate(BaudRate newRate)
setDefaultBaudRate Set default baud rate. The default baud rate is 9600 unless otherwise changed by t...
void setDefaultHost(const char *addressHost)
void setDefaultPort(uint32_t addressPort)
const char * getDefaultName() override
IPState MoveRelFocuser(FocusDirection dir, uint32_t ticks) override
MoveFocuser the focuser to an relative position.
bool AbortFocuser() override
AbortFocuser all focus motion.
IPState MoveAbsFocuser(uint32_t targetTicks) override
MoveFocuser the focuser to an absolute position.
bool Handshake() override
Performs a device-specific handshake after a connection is established. This method should be overrid...
bool SetFocuserSpeed(int speed) override
SetFocuserSpeed Set Focuser speed.
bool SyncFocuser(uint32_t ticks) override
SyncFocuser Set current position to ticks without moving the focuser.
bool updateProperties() override
Updates the state and definition of properties based on the current connection status....
bool SetFocuserMaxPosition(uint32_t ticks) override
SetFocuserMaxPosition Update focuser maximum position. It only updates the PresetNP property limits.
bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override
Processes incoming new switch states from the client for this device. Handles updates to FocuserInter...
void TimerHit() override
Callback function to be called once SetTimer duration elapses.
bool ReverseFocuser(bool enabled) override
ReverseFocuser Reverse focuser motion direction.
bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override
Processes incoming new number values from the client for this device. Handles updates to FocuserInter...
bool initProperties() override
Initializes the default properties for the focuser device. Sets up standard INDI properties,...
bool isConnected() const
void setDefaultPollingPeriod(uint32_t msec)
setDefaultPollingPeriod Change the default polling period to call TimerHit() function in the driver.
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)
uint32_t getCurrentPollingPeriod() const
getCurrentPollingPeriod Return the current polling period.
Connection::Interface * getActiveConnection()
int SetTimer(uint32_t ms)
Set a timer to call the function TimerHit after ms milliseconds.
INDI::PropertyNumber FocusRelPosNP
INDI::PropertySwitch FocusReverseSP
void SetCapability(uint32_t cap)
FI::SetCapability sets the focuser capabilities. All capabilities must be initialized.
INDI::PropertyNumber FocusSyncNP
INDI::PropertyNumber FocusMaxPosNP
INDI::PropertyNumber FocusAbsPosNP
virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override
Processes incoming new switch states from the client for this device. Handles updates to FocuserInter...
virtual bool updateProperties() override
Updates the state and definition of properties based on the current connection status....
virtual bool initProperties() override
Initializes the default properties for the focuser device. Sets up standard INDI properties,...
Connection::TCP * tcpConnection
virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override
Processes incoming new number values from the client for this device. Handles updates to FocuserInter...
virtual void SyncPresets(uint32_t ticks)
Synchronizes the range (min, max, step) of the preset number properties (PresetNP) based on the focus...
Connection::Serial * serialConnection
void setSupportedConnections(const uint8_t &value)
void setState(IPState state)
void apply(const char *format,...) const ATTRIBUTE_FORMAT_PRINTF(2
bool isNameMatch(const char *otherName) const
bool update(const double values[], const char *const names[], int n)
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)
a class to store JSON values
const char * MAIN_CONTROL_TAB
MAIN_CONTROL_TAB Where all the primary controls for the device are located.
std::string requested
Definition discovery.cpp:50
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
IPState
Property state.
Definition indiapi.h:158
@ 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
int tty_write_string(int fd, const char *buf, int *nbytes_written)
Writes a null terminated string to fd.
Definition indicom.c:473
void tty_error_msg(int err_code, char *err_msg, int err_msg_len)
Retrieve the tty error message.
Definition indicom.c:1243
int tty_nread_section(int fd, char *buf, int nsize, char stop_char, int timeout, int *nbytes_read)
read buffer from terminal with a delimiter
Definition indicom.c:759
Implementations for common driver routines.
@ TTY_OK
Definition indicom.h:54
#define MAXRBUF
Definition indidriver.c:52
#define LOGF_INFO(fmt,...)
Definition indilogger.h:82
#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
bool parseJsonBoolean(const std::string &json, const std::string &key, bool &value)
const char * modelForVersion(int version)
bool isSupportedIdentity(const std::string &response)
bool isErrorResponse(const std::string &response)
bool isSupportedVersion(int version)
bool parseJsonNumber(const std::string &json, const std::string &key, double &value)
bool parseMotionStatus(const std::string &response, MotionStatus &status)
bool parseVersion(const std::string &response, int &version)
bool parseJsonInteger(const std::string &json, const std::string &key, int64_t &value)
uint16_t type
Definition pwc-ioctl.h:0