Instrument Neutral Distributed Interface INDI 2.2.5
wanderer_snowflake.cpp
Go to the documentation of this file.
1/*******************************************************************************
2 Copyright(c) 2026 Jérémie Klein. All rights reserved.
3 Copyright(c) 2026 WandererAstro. All rights reserved. (modifications)
4
5 Wanderer Snowflake Filter Wheel Driver
6
7 This library is free software; you can redistribute it and/or
8 modify it under the terms of the GNU Library General Public
9 License version 2 as published by the Free Software Foundation.
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 Library General Public License for more details.
15
16 You should have received a copy of the GNU Library General Public License
17 along with this library; see the file COPYING.LIB. If not, write to
18 the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
19 Boston, MA 02110-1301, USA.
20*******************************************************************************/
21
22#include "wanderer_snowflake.h"
23
24#include "indicom.h"
25
27#include <cstring>
28#include <memory>
29#include <strings.h>
30#include <termios.h>
31
32#define WANDERER_TIMEOUT 5
33
34#define WANDERER_STATUS_TIMEOUT 12
35
36#define WANDERER_CALIB_REPLY_TIMEOUT 10
37#define WANDERER_MAX_FILTERS 8
38
39#define WANDERER_CMD_ZERO_DETECT "1002"
40
41static std::unique_ptr<WandererSnowflakeFW> wanderer_snowflake(new WandererSnowflakeFW());
42
48
50{
51 return "Wanderer Snowflake Filter Wheel";
52}
53
55{
57
58 if (serialConnection != nullptr)
60
61 FilterSlotNP[0].setMin(1);
63
64 IUFillSwitch(&CalibrationCmdS[0], "CALIBRATE", "Auto calibrate", ISS_OFF);
65 IUFillSwitch(&CalibrationCmdS[1], "ZERO_DETECT", "Zero detection", ISS_OFF);
66 IUFillSwitchVector(&CalibrationCmdSP, CalibrationCmdS, 2, getDeviceName(), "CALIBRATION_CMDS", "Calibration",
68
69 IUFillNumber(&DeviceIDN[0], "DEVICE_ID", "ID", "%0.0f", 0, 10, 1, 0);
70 IUFillNumberVector(&DeviceIDNP, DeviceIDN, 1, getDeviceName(),
71 "DEVICE_ID", "Device ID", OPTIONS_TAB, IP_RW, 60, IPS_IDLE);
72
73 IUFillText(&DeviceInfoT[0], "MODEL", "Model", "");
74 IUFillText(&DeviceInfoT[1], "FIRMWARE", "Firmware", "");
75 IUFillTextVector(&DeviceInfoTP, DeviceInfoT, 2, getDeviceName(),
76 "DEVICE_INFO", "Device Info", OPTIONS_TAB, IP_RO, 60, IPS_IDLE);
77
79
80 return true;
81}
82
84{
86
87 if (isConnected())
88 {
89 defineProperty(&CalibrationCmdSP);
90 defineProperty(&DeviceIDNP);
91 DeviceIDN[0].value = mDeviceID;
92 DeviceIDNP.s = IPS_OK;
93 IDSetNumber(&DeviceIDNP, nullptr);
94 defineProperty(&DeviceInfoTP);
95 }
96 else
97 {
98 deleteProperty(CalibrationCmdSP.name);
99 deleteProperty(DeviceIDNP.name);
100 deleteProperty(DeviceInfoTP.name);
101 }
102
103 return true;
104}
105
107{
108 if (isSimulation())
109 {
110 sendAutomaticCalibration();
111 return true;
112 }
113
114 tcflush(PortFD, TCIOFLUSH);
115
116 int current = 0;
117 if (!readCurrentFilterFromStatus(current))
118 {
119 LOG_ERROR("Wanderer Snowflake model (WSFW508/WSFW368) not found in status. Wrong port or device.");
120 return false;
121 }
122
123 if (current >= 1 && current <= WANDERER_MAX_FILTERS)
124 {
125 CurrentFilter = current;
126 FilterSlotNP[0].setValue(CurrentFilter);
127 }
128
129 char field[64] = {0};
130 int nbytes = 0;
131
132 // Consume the hardware's per-filter single-character identifier field. These
133 // are an internal hardware concept and are intentionally not exposed as or
134 // synced with the INDI FILTER_NAME property (see GitHub issue #2478).
135 tty_read_section(PortFD, field, 'A', WANDERER_STATUS_TIMEOUT, &nbytes);
136
137 for (int i = 0; i < WANDERER_MAX_FILTERS; i++)
138 tty_read_section(PortFD, field, 'A', WANDERER_STATUS_TIMEOUT, &nbytes);
139
140 if (tty_read_section(PortFD, field, 'A', WANDERER_STATUS_TIMEOUT, &nbytes) == TTY_OK)
141 {
142 while (nbytes > 0 && (field[nbytes - 1] == 'A' || field[nbytes - 1] == '\r' || field[nbytes - 1] == '\n'))
143 {
144 nbytes--;
145 field[nbytes] = '\0';
146 }
147 mDeviceID = std::atoi(field);
148 }
149
150 IUSaveText(&DeviceInfoT[0], mModel);
151 IUSaveText(&DeviceInfoT[1], mFirmware);
152 DeviceInfoTP.s = IPS_OK;
153 IDSetText(&DeviceInfoTP, nullptr);
154
155 usleep(200000);
156
157 if (!sendAutomaticCalibration())
158 LOG_WARN("Automatic calibration (1500002) failed at connect; use Calibration → Auto calibrate.");
159 else
160 LOG_INFO("Automatic calibration (1500002) sent at connection.");
161
162 return true;
163}
164
165bool WandererSnowflakeFW::sendAutomaticCalibration()
166{
167 if (isSimulation())
168 {
169 LOG_INFO("Automatic calibration (1500002, simulation).");
170 return true;
171 }
172
173 if (!sendCommand("1500002", nullptr, 0, WANDERER_TIMEOUT))
174 {
175 LOG_ERROR("Failed to send automatic calibration command (1500002).");
176 return false;
177 }
178 LOG_INFO("Automatic calibration command (1500002) sent.");
179 return true;
180}
181
182bool WandererSnowflakeFW::sendZeroDetection()
183{
184 if (isSimulation())
185 {
186 LOG_INFO("Zero detection (" WANDERER_CMD_ZERO_DETECT ", simulation). CurrentFilter → 1.");
187 CurrentFilter = 1;
188 FilterSlotNP[0].setValue(1);
190 FilterSlotNP.apply("Zero detection complete (simulation).");
191 return true;
192 }
193
194 if (!sendCommand(WANDERER_CMD_ZERO_DETECT, nullptr, 0, WANDERER_TIMEOUT))
195 {
196 LOGF_ERROR("Failed to send zero detection command (%s).", WANDERER_CMD_ZERO_DETECT);
197 return false;
198 }
199 LOGF_INFO("Zero detection command (%s) sent; waiting for filter 1.", WANDERER_CMD_ZERO_DETECT);
200
201 constexpr int maxAttempts = 80;
202 for (int attempts = 0; attempts < maxAttempts; ++attempts)
203 {
204 int current = 0;
205 if (readCurrentFilterFromStatus(current) && current == 1)
206 {
207 CurrentFilter = 1;
208 FilterSlotNP[0].setValue(1);
210 FilterSlotNP.apply("Zero detection complete, at filter 1.");
211 return true;
212 }
213 usleep(500 * 1000);
214 }
215
216 LOG_WARN("Zero detection: timed out waiting for filter 1; wheel may still be moving.");
217 return false;
218}
219
220bool WandererSnowflakeFW::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n)
221{
222 if (dev != nullptr && std::strcmp(dev, getDeviceName()) == 0)
223 {
224 if (std::strcmp(CalibrationCmdSP.name, name) == 0)
225 {
226 IUUpdateSwitch(&CalibrationCmdSP, states, names, n);
227 const bool wantCal = (CalibrationCmdS[0].s == ISS_ON);
228 const bool wantZero = (CalibrationCmdS[1].s == ISS_ON);
229 IUResetSwitch(&CalibrationCmdSP);
230
231 CalibrationCmdSP.s = IPS_BUSY;
232 IDSetSwitch(&CalibrationCmdSP, nullptr);
233
234 bool ok = true;
235 if (wantCal)
236 ok = sendAutomaticCalibration();
237 else if (wantZero)
238 ok = sendZeroDetection();
239
240 CalibrationCmdSP.s = ok ? IPS_OK : IPS_ALERT;
241 IDSetSwitch(&CalibrationCmdSP, nullptr);
242 return true;
243 }
244 }
245
246 return INDI::FilterWheel::ISNewSwitch(dev, name, states, names, n);
247}
248
249bool WandererSnowflakeFW::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n)
250{
251 if (dev != nullptr && std::strcmp(dev, getDeviceName()) == 0)
252 {
253
254 if (std::strcmp(DeviceIDNP.name, name) == 0)
255 {
256 IUUpdateNumber(&DeviceIDNP, values, names, n);
257 int id = static_cast<int>(DeviceIDN[0].value);
258 if (id < 0 || id > 10)
259 {
260 LOG_ERROR("Device ID must be 0-10.");
261 DeviceIDNP.s = IPS_ALERT;
262 IDSetNumber(&DeviceIDNP, nullptr);
263 return true;
264 }
265 char cmd[16] = {0};
266 snprintf(cmd, sizeof(cmd), "%d", 1900000 + id);
267 if (!sendCommand(cmd, nullptr, 0, WANDERER_TIMEOUT))
268 {
269 LOG_ERROR("Failed to set device ID.");
270 DeviceIDNP.s = IPS_ALERT;
271 IDSetNumber(&DeviceIDNP, nullptr);
272 return true;
273 }
274 mDeviceID = id;
275 DeviceIDNP.s = IPS_OK;
276 IDSetNumber(&DeviceIDNP, nullptr);
277 LOGF_INFO("Device ID set to %d.", id);
278 return true;
279 }
280 }
281 return INDI::FilterWheel::ISNewNumber(dev, name, values, names, n);
282}
283
288
290{
291 if (position == CurrentFilter)
292 {
294 return true;
295 }
296
297 if (position < 1 || position > WANDERER_MAX_FILTERS)
298 {
299 LOGF_ERROR("Requested filter position %d is out of range (1-%d).", position, WANDERER_MAX_FILTERS);
300 return false;
301 }
302
303 TargetFilter = position;
304
305 if (isSimulation())
306 {
308 FilterSlotNP[0].setValue(CurrentFilter);
310 FilterSlotNP.apply("Selected filter position reached (simulation).");
312 return true;
313 }
314
315 char cmd[16] = {0};
316 snprintf(cmd, sizeof(cmd), "%d", 2000 + TargetFilter);
317
318 if (!sendCommand(cmd, nullptr, 0, WANDERER_TIMEOUT))
319 {
320 LOG_ERROR("Failed to send move command to Wanderer Snowflake filter wheel.");
321 return false;
322 }
323
324 tcflush(PortFD, TCIFLUSH);
325
326 int attempts = 0;
327 constexpr int maxAttempts = 80;
328
329 while (attempts++ < maxAttempts)
330 {
331 int current = 0;
332 if (readCurrentFilterFromStatus(current) && current == TargetFilter)
333 {
334 CurrentFilter = current;
335 FilterSlotNP[0].setValue(CurrentFilter);
337 FilterSlotNP.apply("Selected filter position reached");
339 return true;
340 }
341 usleep(500 * 1000);
342 }
343
344 LOG_ERROR("Timed out waiting for Wanderer Snowflake filter wheel to reach target position.");
345 return false;
346}
347
348bool WandererSnowflakeFW::sendCommand(const char *command, char *response, int responseLen, int timeoutSeconds)
349{
350 int nbytes_written = 0;
351 int nbytes_read = 0;
352 int rc;
353
354 if (PortFD < 0)
355 return false;
356
357 char line[48];
358 const int linelen = std::snprintf(line, sizeof(line), "%s\r", command);
359 if (linelen <= 0 || linelen >= static_cast<int>(sizeof(line)))
360 {
361 LOG_ERROR("Wanderer Snowflake command too long for transmit buffer.");
362 return false;
363 }
364
365 if (response != nullptr && responseLen > 1)
366 tcflush(PortFD, TCIFLUSH);
367
368 if ((rc = tty_write(PortFD, line, linelen, &nbytes_written)) != TTY_OK)
369 {
370 char errstr[MAXRBUF] = {0};
371 tty_error_msg(rc, errstr, MAXRBUF);
372 LOGF_ERROR("Error sending command '%s' to Wanderer Snowflake filter wheel: %s", command, errstr);
373 return false;
374 }
375
376 if (response == nullptr || responseLen <= 1)
377 return true;
378
379 rc = tty_read_section(PortFD, response, '\r', timeoutSeconds, &nbytes_read);
380 if (rc != TTY_OK)
381 rc = tty_read_section(PortFD, response, '\n', timeoutSeconds, &nbytes_read);
382 if (rc != TTY_OK)
383 {
384 char errstr[MAXRBUF] = {0};
385 tty_error_msg(rc, errstr, MAXRBUF);
386 LOGF_DEBUG("No CR/LF line after command '%s' (status stream may omit it while moving): %s", command, errstr);
387 response[0] = '\0';
388 return true;
389 }
390
391 while (nbytes_read > 0 && (response[nbytes_read - 1] == '\r' || response[nbytes_read - 1] == '\n'))
392 {
393 nbytes_read--;
394 response[nbytes_read] = '\0';
395 }
396 if (nbytes_read >= responseLen)
397 nbytes_read = responseLen - 1;
398 response[nbytes_read] = '\0';
399
400 return true;
401}
402
404{
405
407 if (FilterNameTP.size() > 0)
408 FilterNameTP.save(fp);
409 return true;
410}
411
412namespace
413{
414
415void wandererStripFieldDelimiter(char *field, int &nbytes_read, char delimiter)
416{
417 while (nbytes_read > 0)
418 {
419 char c = field[nbytes_read - 1];
420 if (c == delimiter || c == '\r' || c == '\n')
421 {
422 nbytes_read--;
423 field[nbytes_read] = '\0';
424 }
425 else
426 break;
427 }
428 field[nbytes_read] = '\0';
429}
430
431char *wandererTrimLeft(char *s)
432{
433 char *p = s;
434 while (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n')
435 p++;
436 if (p != s)
437 std::memmove(s, p, std::strlen(p) + 1);
438 return s;
439}
440
441bool wandererFieldLooksLikeModel(char *field, int nbytes_read)
442{
443 if (nbytes_read <= 0)
444 return false;
445 field[nbytes_read] = '\0';
446 wandererTrimLeft(field);
447 return (strncasecmp(field, "WSFW508", 7) == 0 || strncasecmp(field, "WSFW368", 7) == 0);
448}
449}
450
451bool WandererSnowflakeFW::readCurrentFilterFromStatus(int &position)
452{
453 int rc = -1;
454 int nbytes_read = 0;
455 char field[64] = {0};
456
457 if (PortFD < 0)
458 return false;
459
460 constexpr int maxResync = 32;
461 bool haveModel = false;
462
463 for (int skip = 0; skip < maxResync; ++skip)
464 {
465 if ((rc = tty_read_section(PortFD, field, 'A', WANDERER_STATUS_TIMEOUT, &nbytes_read)) != TTY_OK)
466 {
467 char errstr[MAXRBUF] = {0};
468 tty_error_msg(rc, errstr, MAXRBUF);
469 LOGF_DEBUG("Failed to read Wanderer Snowflake status field: %s", errstr);
470 return false;
471 }
472 wandererStripFieldDelimiter(field, nbytes_read, 'A');
473
474 if (wandererFieldLooksLikeModel(field, static_cast<int>(std::strlen(field))))
475 {
476 snprintf(mModel, sizeof(mModel), "%s", field);
477 haveModel = true;
478 break;
479 }
480
481 LOGF_DEBUG("Skipping Wanderer Snowflake status field while resyncing: '%s'", field);
482 }
483
484 if (haveModel)
485 {
486 if ((rc = tty_read_section(PortFD, field, 'A', WANDERER_STATUS_TIMEOUT, &nbytes_read)) != TTY_OK)
487 {
488 char errstr[MAXRBUF] = {0};
489 tty_error_msg(rc, errstr, MAXRBUF);
490 LOGF_DEBUG("Failed to read firmware field from Wanderer Snowflake status: %s", errstr);
491 return false;
492 }
493 wandererStripFieldDelimiter(field, nbytes_read, 'A');
494 snprintf(mFirmware, sizeof(mFirmware), "%s", field);
495
496 double fwVersion = std::atof(field);
497 if (fwVersion < 20260124.0)
498 {
499 LOGF_ERROR("Wanderer Snowflake firmware %.0f is too old (minimum 20260124). "
500 "Please update the firmware in WandererEmpire.", fwVersion);
501 return false;
502 }
503
504 if ((rc = tty_read_section(PortFD, field, 'A', WANDERER_STATUS_TIMEOUT, &nbytes_read)) != TTY_OK)
505 {
506 char errstr[MAXRBUF] = {0};
507 tty_error_msg(rc, errstr, MAXRBUF);
508 LOGF_DEBUG("Failed to read current filter field from Wanderer Snowflake status: %s", errstr);
509 return false;
510 }
511 wandererStripFieldDelimiter(field, nbytes_read, 'A');
512
513 int current = std::atoi(field);
514 if (current >= 1 && current <= WANDERER_MAX_FILTERS)
515 {
516 position = current;
517 return true;
518 }
519 LOGF_DEBUG("Received invalid current filter value '%s' from Wanderer Snowflake status.", field);
520 return false;
521 }
522
523 return false;
524}
525
526
void setDefaultBaudRate(BaudRate newRate)
setDefaultBaudRate Set default baud rate. The default baud rate is 9600 unless otherwise changed by t...
bool isConnected() 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)
virtual bool saveConfigItems(FILE *fp)
saveConfigItems Save specific properties in the provide config file handler. Child class usually over...
bool isSimulation() const
void addAuxControls()
Add Debug, Simulation, and Configuration options to the driver.
INDI::PropertyNumber FilterSlotNP
INDI::PropertyText FilterNameTP
void SelectFilterDone(int newpos)
The child class calls this function when the hardware successfully finished selecting a new filter wh...
virtual bool updateProperties() override
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) override
Process the client newSwitch command.
Connection::Serial * serialConnection
virtual bool initProperties() override
Initialize properties initial state and value. The child class must implement this function.
void setFilterConnection(const uint8_t &value)
setFilterConnection Set Filter connection mode. Child class should call this in the constructor befor...
virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override
Process the client newNumber command.
int PortFD
For Serial & TCP connections.
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 * getDefaultName() override
int QueryFilter() override
Return current filter position.
bool initProperties() override
Initialize properties initial state and value. The child class must implement this function.
bool SelectFilter(int position) override
Select a new filter position.
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....
bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override
Process the client newNumber command.
bool Handshake() override
perform handshake with device to check communication
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_BUSY
Definition indiapi.h:161
@ IPS_ALERT
Definition indiapi.h:162
@ IPS_IDLE
Definition indiapi.h:159
@ IPS_OK
Definition indiapi.h:160
@ ISR_ATMOST1
Definition indiapi.h:172
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(int fd, const char *buf, int nbytes, int *nbytes_written)
Writes a buffer to fd.
Definition indicom.c:418
void tty_error_msg(int err_code, char *err_msg, int err_msg_len)
Retrieve the tty error message.
Definition indicom.c:1243
Implementations for common driver routines.
@ TTY_OK
Definition indicom.h:54
void IUFillNumberVector(INumberVectorProperty *nvp, INumber *np, int nnp, const char *dev, const char *name, const char *label, const char *group, IPerm p, double timeout, IPState s)
Assign attributes for a number vector property. The vector's auxiliary elements will be set to NULL.
Definition indidevapi.c:490
void IUResetSwitch(ISwitchVectorProperty *svp)
Reset all switches in a switch vector property to OFF.
Definition indidevapi.c:366
void IUFillTextVector(ITextVectorProperty *tvp, IText *tp, int ntp, const char *dev, const char *name, const char *label, const char *group, IPerm p, double timeout, IPState s)
Assign attributes for a text vector property. The vector's auxiliary elements will be set to NULL.
Definition indidevapi.c:509
void IUSaveText(IText *tp, const char *newtext)
Function to reliably save new text in a IText.
Definition indidevapi.c:44
void IUFillSwitch(ISwitch *sp, const char *name, const char *label, ISState s)
Assign attributes for a switch property. The switch's auxiliary elements will be set to NULL.
Definition indidevapi.c:376
void IUFillText(IText *tp, const char *name, const char *label, const char *initialText)
Assign attributes for a text property. The text's auxiliary elements will be set to NULL.
Definition indidevapi.c:416
void IUFillNumber(INumber *np, const char *name, const char *label, const char *format, double min, double max, double step, double value)
Assign attributes for a number property. The number's auxiliary elements will be set to NULL.
Definition indidevapi.c:398
void IUFillSwitchVector(ISwitchVectorProperty *svp, ISwitch *sp, int nsp, const char *dev, const char *name, const char *label, const char *group, IPerm p, ISRule r, double timeout, IPState s)
Assign attributes for a switch vector property. The vector's auxiliary elements will be set to NULL.
Definition indidevapi.c:453
int IUUpdateSwitch(ISwitchVectorProperty *svp, ISState *states, char *names[], int n)
Update all switches in a switch vector property.
void IDSetNumber(const INumberVectorProperty *nvp, const char *fmt,...)
void IDSetSwitch(const ISwitchVectorProperty *svp, const char *fmt,...)
#define MAXRBUF
Definition indidriver.c:52
int IUUpdateNumber(INumberVectorProperty *nvp, double values[], char *names[], int n)
Update all numbers in a number vector property.
void IDSetText(const ITextVectorProperty *tvp, const char *fmt,...)
#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
uint8_t cmd[4]
Definition pwc-ioctl.h:2
char name[MAXINDINAME]
Definition indiapi.h:321
char name[MAXINDINAME]
Definition indiapi.h:369
char name[MAXINDINAME]
Definition indiapi.h:248
#define WANDERER_MAX_FILTERS
#define WANDERER_STATUS_TIMEOUT
#define WANDERER_CMD_ZERO_DETECT
#define WANDERER_TIMEOUT