Instrument Neutral Distributed Interface INDI 2.2.5
startech_hub.cpp
Go to the documentation of this file.
1/*******************************************************************************
2 INDI StarTech Managed USB Hub Driver
3*******************************************************************************/
4
5#include "startech_hub.h"
6
8#include "indicom.h"
9
10#include <algorithm>
11#include <cstring>
12#include <iomanip>
13#include <memory>
14#include <sstream>
15#include <string>
16
17#include <sys/ioctl.h>
18#include <termios.h>
19
20namespace
21{
22std::unique_ptr<StarTechHub> hub(new StarTechHub());
23
24std::string trimCRLF(std::string s)
25{
26 s.erase(std::remove(s.begin(), s.end(), '\r'), s.end());
27 s.erase(std::remove(s.begin(), s.end(), '\n'), s.end());
28 return s;
29}
30} // namespace
31
32void ISGetProperties(const char *dev)
33{
34 hub->ISGetProperties(dev);
35}
36
37void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n)
38{
39 // Call through the public base-class interface to avoid access issues.
40 // Virtual dispatch will still reach StarTechHub::ISNewSwitch().
41 static_cast<INDI::DefaultDevice *>(hub.get())->ISNewSwitch(dev, name, states, names, n);
42}
43
44void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n)
45{
46 static_cast<INDI::DefaultDevice *>(hub.get())->ISNewText(dev, name, texts, names, n);
47}
48
50{
51 hub->ISSnoopDevice(root);
52}
53
58
60{
61 return "StarTech Managed USB Hub";
62}
63
65{
68
69 // -----------------------
70 // Options: Port Aliases
71 // -----------------------
72 PortAliasesTP.fill(getDeviceName(), "USB_PORT_ALIASES", "Port Labels", OPTIONS_TAB, IP_RW, 0, IPS_IDLE);
73 for (uint8_t i = 0; i < USB_PORTS; i++)
74 {
75 char itemName[16];
76 snprintf(itemName, sizeof(itemName), "ALIAS_P%u", i + 1);
77 PortAliasesTP[i].fill(itemName, defaultPortLabel(i + 1).c_str(), "");
78 }
79
80 PortAliasesTP.onUpdate([this]()
81 {
82 syncAliasesFromProperty(true);
83 applyPortLabelsAndRedefineUSBProperties();
84 PortAliasesTP.setState(IPS_OK);
85 PortAliasesTP.apply();
86 return true;
87 });
88
89 // -----------------------
90 // Main Control: Info
91 // -----------------------
92 InfoTP.fill(getDeviceName(), "HUB_INFO", "Info", MAIN_CONTROL_TAB, IP_RO, 0, IPS_IDLE);
93 InfoTP[INFO_IDENTITY].fill("IDENTITY", "Model", "N/A");
94 InfoTP[INFO_MASK].fill("MASK", "Current Mask", "N/A");
95
96 // -----------------------
97 // Main Control: USB Ports
98 // -----------------------
99 for (uint8_t i = 0; i < USB_PORTS; i++)
100 {
101 char propName[20];
102 snprintf(propName, sizeof(propName), "USB_PORT_%u", i + 1);
103
104 // ON must precede OFF
105 USBPortSP[i][SW_ON].fill("ON", "On", ISS_OFF);
106 USBPortSP[i][SW_OFF].fill("OFF", "Off", ISS_ON);
107
108 USBPortSP[i].fill(getDeviceName(), propName, defaultPortLabel(i + 1).c_str(), MAIN_CONTROL_TAB,
110
111 USBPortSP[i].onUpdate([this, i]()
112 {
113 const bool enable = (USBPortSP[i][SW_ON].getState() == ISS_ON);
114 if (setUSBPortEnabled(i + 1, enable))
115 USBPortSP[i].setState(IPS_OK);
116 else
117 USBPortSP[i].setState(IPS_ALERT);
118
119 USBPortSP[i].apply();
120 return true;
121 });
122 }
123
124 // All ports
125 USBAllSP[SW_ON].fill("ON", "On", ISS_OFF);
126 USBAllSP[SW_OFF].fill("OFF", "Off", ISS_ON);
127 USBAllSP.fill(getDeviceName(), "USB_ALL_PORTS", "USB All Ports", MAIN_CONTROL_TAB,
129
130 USBAllSP.onUpdate([this]()
131 {
132 const bool enable = (USBAllSP[SW_ON].getState() == ISS_ON);
133 if (setAllPortsEnabled(enable))
134 USBAllSP.setState(IPS_OK);
135 else
136 USBAllSP.setState(IPS_ALERT);
137
138 USBAllSP.apply();
139 return true;
140 });
141
142 // Serial connection
143 serialConnection = new Connection::Serial(this);
144 serialConnection->registerHandshake([this]() { return Handshake(); });
145 registerConnection(serialConnection);
146
147 return true;
148}
149
151{
153
154 if (isConnected())
155 {
156 // Order requested: Info first, then port switches
157 defineProperty(InfoTP);
158 for (auto &p : USBPortSP)
160 defineProperty(USBAllSP);
161
162 // Options property should appear after Polling, so define after DefaultDevice::updateProperties()
163 defineProperty(PortAliasesTP);
164
165 // Make sure labels reflect current alias texts
166 syncAliasesFromProperty(false);
167 committedAliases = portAliases;
168 applyPortLabelsAndRedefineUSBProperties();
169
170 // Start polling loop
172 }
173 else
174 {
175 deleteProperty(InfoTP.getName());
176 for (auto &p : USBPortSP)
177 deleteProperty(p.getName());
178 deleteProperty(USBAllSP.getName());
179 deleteProperty(PortAliasesTP.getName());
180
181 // Lose unsaved edits on disconnect: revert to last committed state
182 portAliases = committedAliases;
183 for (uint8_t i = 0; i < USB_PORTS; i++)
184 PortAliasesTP[i].setText(portAliases[i].c_str());
185 }
186
187 return true;
188}
189
190bool StarTechHub::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n)
191{
192 // Let DefaultDevice process everything first (including CONFIG_PROCESS)
193 const bool rc = INDI::DefaultDevice::ISNewSwitch(dev, name, states, names, n);
194
195 if (dev != nullptr && strcmp(dev, getDeviceName()) != 0)
196 return rc;
197
198 if (name == nullptr)
199 return rc;
200
201 // React immediately to Load/Default so aliases apply without reconnect
202 if (strcmp(name, "CONFIG_PROCESS") == 0)
203 {
204 bool doLoad = false;
205 bool doDefault = false;
206
207 for (int i = 0; i < n; i++)
208 {
209 if (states[i] != ISS_ON || names[i] == nullptr)
210 continue;
211
212 if (strcmp(names[i], "CONFIG_LOAD") == 0)
213 doLoad = true;
214 else if (strcmp(names[i], "CONFIG_DEFAULT") == 0)
215 doDefault = true;
216 }
217
218 if (doLoad)
219 {
220 // Load saved aliases (if present) and commit them.
221 loadConfig(PortAliasesTP);
222 syncAliasesFromProperty(true);
223 committedAliases = portAliases;
224 applyPortLabelsAndRedefineUSBProperties();
225 PortAliasesTP.apply();
226 }
227 else if (doDefault)
228 {
229 // Reset to defaults (blank aliases) WITHOUT committing.
230 for (uint8_t i = 0; i < USB_PORTS; i++)
231 PortAliasesTP[i].setText("");
232 PortAliasesTP.setState(IPS_OK);
233 PortAliasesTP.apply();
234
235 syncAliasesFromProperty(false);
236 applyPortLabelsAndRedefineUSBProperties();
237 }
238 }
239
240 return rc;
241}
242
244{
246
247 // Only persisted when the user presses Save (or AutoSave is enabled).
248 PortAliasesTP.save(fp);
249 committedAliases = portAliases;
250
251 return true;
252}
253
255{
256 if (!isConnected())
257 {
259 return;
260 }
261
262 uint32_t mask = 0;
263 if (getPortMask(mask))
264 {
265 if (mask != currentMask)
266 {
267 currentMask = mask;
268 updatePortsFromMask(currentMask);
269 }
270 }
271
273}
274
275void StarTechHub::syncAliasesFromProperty(bool rewriteTruncated)
276{
277 bool anyRewrite = false;
278
279 for (uint8_t i = 0; i < USB_PORTS; i++)
280 {
281 bool wasTruncated = false;
282 const std::string sanitized = sanitizeAlias(PortAliasesTP[i].getText(), wasTruncated);
283 portAliases[i] = sanitized;
284
285 if (rewriteTruncated && wasTruncated)
286 {
287 PortAliasesTP[i].setText(sanitized.c_str());
288 anyRewrite = true;
289 }
290 }
291
292 if (anyRewrite)
293 {
294 LOGF_WARN("StarTechHub: one or more aliases were truncated to %zu characters.", ALIAS_MAX_LEN);
295 }
296}
297
298std::string StarTechHub::sanitizeAlias(const std::string &in, bool &wasTruncated) const
299{
300 wasTruncated = false;
301
302 // Keep spaces, but trim leading/trailing whitespace.
303 size_t start = in.find_first_not_of(" \t");
304 if (start == std::string::npos)
305 return "";
306
307 size_t end = in.find_last_not_of(" \t");
308 std::string s = in.substr(start, end - start + 1);
309
310 if (s.size() > ALIAS_MAX_LEN)
311 {
312 s.resize(ALIAS_MAX_LEN);
313 wasTruncated = true;
314 }
315
316 return s;
317}
318
319std::string StarTechHub::defaultPortLabel(uint8_t port) const
320{
321 std::ostringstream oss;
322 oss << "USB Port " << static_cast<unsigned>(port);
323 return oss.str();
324}
325
326void StarTechHub::applyPortLabelsAndRedefineUSBProperties()
327{
328 // Update in-memory labels
329 for (uint8_t i = 0; i < USB_PORTS; i++)
330 {
331 const std::string lbl = portAliases[i].empty() ? defaultPortLabel(i + 1) : portAliases[i];
332 USBPortSP[i].setLabel(lbl.c_str());
333 }
334
335 // Force clients to refresh labels by redefining properties
336 if (!isConnected())
337 return;
338
339 for (auto &p : USBPortSP)
340 {
341 deleteProperty(p.getName());
343 p.apply();
344 }
345
346 deleteProperty(USBAllSP.getName());
347 defineProperty(USBAllSP);
348 USBAllSP.apply();
349}
350
351bool StarTechHub::sendCommand(const std::string &cmd, std::string &response)
352{
353 if (PortFD < 0)
354 return false;
355
356 const std::string fullCmd = cmd + "\r";
357
358 int nbytesWritten = 0;
359 if (tty_write_string(PortFD, fullCmd.c_str(), &nbytesWritten) != TTY_OK)
360 return false;
361
362 char buf[128] = {0};
363 int nbytesRead = 0;
364 if (tty_read_section(PortFD, buf, '\n', STARTECH_TIMEOUT_SEC, &nbytesRead) != TTY_OK)
365 return false;
366
367 buf[std::min(nbytesRead, static_cast<int>(sizeof(buf) - 1))] = '\0';
368 response = trimCRLF(buf);
369 return true;
370}
371
372bool StarTechHub::queryIdentity(std::string &out)
373{
374 std::string rx;
375 if (!sendCommand(CMD_GET_IDENTITY, rx))
376 return false;
377
378 out = rx;
379 return !out.empty();
380}
381
382bool StarTechHub::getPortMask(uint32_t &mask)
383{
384 std::string rx;
385 if (!sendCommand(CMD_GET_MASK, rx))
386 return false;
387
388 if (rx.size() != 8)
389 {
390 LOGF_ERROR("StarTechHub: unexpected mask length '%s'", rx.c_str());
391 return false;
392 }
393
394 char *end = nullptr;
395 const unsigned long v = strtoul(rx.c_str(), &end, 16);
396 if (end == nullptr || *end != '\0')
397 {
398 LOGF_ERROR("StarTechHub: failed parsing mask '%s'", rx.c_str());
399 return false;
400 }
401
402 mask = static_cast<uint32_t>(v);
403 return true;
404}
405
406bool StarTechHub::setPortMask(uint32_t mask)
407{
408 std::ostringstream oss;
409 oss << std::uppercase << std::hex << std::setw(8) << std::setfill('0') << mask;
410 const std::string maskStr = oss.str();
411
412 const std::string cmd = std::string(CMD_SET_MASK) + DEFAULT_PASS8 + maskStr;
413
414 std::string rx;
415 if (!sendCommand(cmd, rx))
416 return false;
417
418 const std::string expected = std::string("G") + maskStr;
419 if (rx != expected)
420 {
421 LOGF_ERROR("StarTechHub: unexpected ACK '%s' (expected '%s')", rx.c_str(), expected.c_str());
422 return false;
423 }
424
425 currentMask = mask;
426 updatePortsFromMask(currentMask);
427 return true;
428}
429
430bool StarTechHub::setUSBPortEnabled(uint8_t port, bool enabled)
431{
432 if (port < 1 || port > USB_PORTS)
433 return false;
434
435 const uint8_t bit = 1u << (port - 1);
436 uint8_t top = static_cast<uint8_t>((currentMask >> 24) & 0xFF);
437
438 if (enabled)
439 top |= bit;
440 else
441 top &= static_cast<uint8_t>(~bit);
442
443 const uint32_t newMask = (currentMask & 0x00FFFFFFu) | (static_cast<uint32_t>(top) << 24);
444 return setPortMask(newMask);
445}
446
447bool StarTechHub::setAllPortsEnabled(bool enabled)
448{
449 const uint32_t newMask = enabled ? (currentMask | 0xFF000000u)
450 : (currentMask & 0x00FFFFFFu);
451 return setPortMask(newMask);
452}
453
454void StarTechHub::updatePortsFromMask(uint32_t mask)
455{
456 // Update Info
457 std::ostringstream oss;
458 oss << std::uppercase << std::hex << std::setw(8) << std::setfill('0') << mask;
459 InfoTP[INFO_MASK].setText(oss.str().c_str());
460 InfoTP.apply();
461
462 const uint8_t top = static_cast<uint8_t>((mask >> 24) & 0xFF);
463
464 // All ports state
465 const uint8_t allBits = 0x7F; // ports 1..7
466 if ((top & allBits) == allBits)
467 {
468 USBAllSP[SW_ON].setState(ISS_ON);
469 USBAllSP[SW_OFF].setState(ISS_OFF);
470 }
471 else if ((top & allBits) == 0)
472 {
473 USBAllSP[SW_ON].setState(ISS_OFF);
474 USBAllSP[SW_OFF].setState(ISS_ON);
475 }
476 else
477 {
478 USBAllSP[SW_ON].setState(ISS_OFF);
479 USBAllSP[SW_OFF].setState(ISS_OFF);
480 }
481
482 USBAllSP.setState(IPS_OK);
483 USBAllSP.apply();
484
485 // Individual ports
486 for (uint8_t i = 0; i < USB_PORTS; i++)
487 {
488 const bool enabled = (top & (1u << i)) != 0;
489 USBPortSP[i][SW_ON].setState(enabled ? ISS_ON : ISS_OFF);
490 USBPortSP[i][SW_OFF].setState(enabled ? ISS_OFF : ISS_ON);
491 USBPortSP[i].setState(IPS_OK);
492 USBPortSP[i].apply();
493 }
494}
495
496bool StarTechHub::Handshake()
497{
498 PortFD = serialConnection->getPortFD();
499 if (PortFD < 0)
500 return false;
501
502 // Assert DTR and RTS.
503 int status = 0;
504 if (ioctl(PortFD, TIOCMGET, &status) == 0)
505 {
506 status |= (TIOCM_DTR | TIOCM_RTS);
507 ioctl(PortFD, TIOCMSET, &status);
508 }
509
510 // Identify
511 std::string ident;
512 if (queryIdentity(ident))
513 InfoTP[INFO_IDENTITY].setText(ident.c_str());
514 else
515 InfoTP[INFO_IDENTITY].setText("NA");
516
517 // Read initial mask
518 uint32_t mask = 0;
519 if (!getPortMask(mask))
520 return false;
521
522 currentMask = mask;
523 updatePortsFromMask(currentMask);
524
525 return true;
526}
void registerHandshake(std::function< bool()> callback)
registerHandshake Register a handshake function to be called once the initial connection to the devic...
The Serial class manages connection with serial devices including Bluetooth. Serial communication is ...
bool isConnected() const
const char * getDeviceName() const
INDI::PropertyText getText(const char *name) const
Class to provide extended functionality for devices in addition to the functionality provided by INDI...
virtual bool updateProperties()
updateProperties is called whenever there is a change in the CONNECTION status of the driver....
virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n)
Process the client newSwitch command.
void registerConnection(Connection::Interface *newConnection)
registerConnection Add new connection plugin to the existing connection pool. The connection type sha...
virtual bool ISSnoopDevice(XMLEle *root)
Process a snoop event from INDI server. This function is called when a snooped property is updated in...
void setDriverInterface(uint32_t value)
setInterface Set driver interface. By default the driver interface is set to GENERAL_DEVICE....
void setVersion(uint16_t vMajor, uint16_t vMinor)
Set driver version information to be defined in DRIVER_INFO property as vMajor.vMinor.
virtual bool loadConfig(bool silent=false, const char *property=nullptr)
Load the last saved configuration file.
virtual bool deleteProperty(const char *propertyName)
Delete a property and unregister it. It will also be deleted from all clients.
uint32_t getPollingPeriod() const
getPollingPeriod Return the polling period.
void defineProperty(INumberVectorProperty *property)
virtual bool saveConfigItems(FILE *fp)
saveConfigItems Save specific properties in the provide config file handler. Child class usually over...
virtual bool initProperties()
Initialize properties initial state and value. The child class must implement this function.
int SetTimer(uint32_t ms)
Set a timer to call the function TimerHit after ms milliseconds.
void setState(IPState state)
void apply(const char *format,...) const ATTRIBUTE_FORMAT_PRINTF(2
void save(FILE *f) const
save Save property to configuration file.
const char * getName() const
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)
void onUpdate(const std::function< void()> &callback)
bool initProperties() override
Initialize properties initial state and value. The child class must implement this function.
void TimerHit() override
Callback function to be called once SetTimer duration elapses.
const char * getDefaultName() override
bool saveConfigItems(FILE *fp) override
saveConfigItems Save specific properties in the provide config file handler. Child class usually over...
bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override
Process the client newSwitch command.
bool updateProperties() override
updateProperties is called whenever there is a change in the CONNECTION status of the driver....
const char * MAIN_CONTROL_TAB
MAIN_CONTROL_TAB Where all the primary controls for the device are located.
const char * OPTIONS_TAB
OPTIONS_TAB Where all the driver's options are located. Those may include auxiliary controls,...
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_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_read_section(int fd, char *buf, char stop_char, int timeout, int *nbytes_read)
read buffer from terminal with a delimiter
Definition indicom.c:653
int tty_write_string(int fd, const char *buf, int *nbytes_written)
Writes a null terminated string to fd.
Definition indicom.c:473
Implementations for common driver routines.
@ TTY_OK
Definition indicom.h:54
#define LOGF_WARN(fmt,...)
Definition indilogger.h:81
#define LOGF_ERROR(fmt,...)
Definition indilogger.h:80
uint8_t cmd[4]
Definition pwc-ioctl.h:2
void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n)
Update the value of an existing switch vector property.
void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n)
Update the value of an existing text vector property.
void ISGetProperties(const char *dev)
Get Device Properties.
void ISSnoopDevice(XMLEle *root)
Function defined by Drivers that is called when another Driver it is snooping (by having previously c...