5#include "wheelly_config.h"
22#include <sys/select.h>
31static const char *TAB_MAIN =
"Main Control";
34static const char *EMPTY_SLOT_NAME =
"Empty";
40static const char *TAB_CALIBRATION =
"Calibration and Diagnostics";
48static const double JOG_STEPS[] = {0, -10, -1, -0.1, 0.1, 1, 10, 0};
49static const char *JOG_NAMES[] = {
"JOG_M_PITCH",
"JOG_M10",
"JOG_M1",
"JOG_M0_1",
50 "JOG_P0_1",
"JOG_P1",
"JOG_P10",
"JOG_P_PITCH"
52static const char *JOG_LABELS[] = {
"-%1$s°",
"-10°",
"-1°",
"-0.1°",
53 "+0.1°",
"+1°",
"+10°",
"+%1$s°"
55static const char *TAB_OPTIONS =
"Options";
57static const int ANSWER_WAIT_MS = 3000;
65static std::string fill_in(
const char *format,
const std::vector<std::string> ¶ms)
67 const std::string text = format;
69 out.reserve(text.size() + 64);
70 for (
size_t i = 0; i < text.size(); i++)
77 if (i + 1 < text.size() && text[i + 1] ==
'%')
85 while (j < text.size() && text[j] >=
'0' && text[j] <=
'9')
87 number = number * 10 + (size_t)(text[j] -
'0');
90 if (number >= 1 && j + 1 < text.size() && text[j] ==
'$' && text[j + 1] ==
's')
92 if (number <= params.size()) out += params[number - 1];
105static std::string name_refusal(
const std::string &reason)
107 if (reason ==
"empty")
return "the name is empty";
108 if (reason ==
"too-long")
return "too long: at most 32 characters";
109 if (reason ==
"bad-edge")
return "it must start and end with a letter or a digit";
110 if (reason ==
"bad-char")
return "there is a character that cannot be used";
111 if (reason ==
"reserved")
112 return "this is a reserved device name on Windows, and the filter name is also used as a folder name";
117static std::string error_message(
int code,
const std::string &expected,
118 const std::string &got,
const std::string &reason)
123 return "The wheel did not understand the command.";
125 return "Wrong arguments for the command.";
127 return fill_in(
"Value out of range: expected %1$s, got %2$s.", {expected, got});
129 return "The position sensor is not answering. Check its wiring, and the VDD5V-VDD3V3 jumper on the module.";
131 return "The sensor works but does not see the magnet.";
133 return "The motor driver is not answering: is the 12 V supply connected? The wheel does not move without it.";
135 return "Not allowed right now: the wheel is busy.";
137 return "The wheel could not save to its memory.";
139 return fill_in(
"Invalid filter name (%1$s).", {name_refusal(reason)});
141 return fill_in(
"The wheel reported error %1$s.", {std::to_string(code)});
157static std::string filter_name_reason(
int outcome,
const char *text)
161 for (
const unsigned char *p = (
const unsigned char *)text; *p !=
'\0'; p++)
164 if (*p ==
' ')
return "there is a space";
165 if (*p >= 0x80)
return "there is an accented or non-English letter";
166 if (*p < 0x20 || *p == 0x7f)
return "there is an invisible character in it";
167 const char one[2] = {(char)*p,
'\0'};
168 return fill_in(
"\"%1$s\" cannot be used", {one});
174 return name_refusal(
"empty");
176 return name_refusal(
"too-long");
178 return name_refusal(
"bad-edge");
180 return name_refusal(
"bad-char");
182 return name_refusal(
"reserved");
184 return std::string();
190static const double RETRY_FIRST_S = 1.0;
191static const double RETRY_MAX_S = 10.0;
199static const double MOVE_CEILING_S = 28.0;
200static const double MOVE_CEILING_MARGIN_S = 3.0;
203static const int SAMPLE_INTERVAL_MS = 100;
209 using namespace std::chrono;
210 return duration_cast<duration<double>>(steady_clock::now().time_since_epoch()).count();
226 if (m_fd < 0)
return;
230 while (
line(rubbish, 50)) { }
235 if (m_fd < 0)
return false;
237 const double deadline = now_s() + wait_ms / 1000.0;
240 const size_t end = m_rest.find(
'\n');
241 if (end != std::string::npos)
243 out = m_rest.substr(0, end);
244 m_rest.erase(0, end + 1);
245 while (!out.empty() && (out.back() ==
'\r' || out.back() ==
' '))
250 const double left = deadline - now_s();
251 if (left <= 0)
return false;
256 struct timeval how_long;
257 how_long.tv_sec = (time_t)left;
258 how_long.tv_usec = (suseconds_t)((left - (
double)how_long.tv_sec) * 1e6);
260 const int ready = select(m_fd + 1, &set,
nullptr,
nullptr, &how_long);
263 if (
errno == EINTR)
continue;
268 if (ready == 0)
return false;
271 const ssize_t got = read(m_fd, chunk,
sizeof(chunk));
274 if (got < 0 && (
errno == EAGAIN ||
errno == EINTR))
continue;
276 m_errno = got < 0 ?
errno : 0;
279 m_rest.append(chunk, (
size_t)got);
290 setVersion(WHEELLY_VERSION_MAJOR, WHEELLY_VERSION_MINOR);
300 return WHEELLY_DEVICE_NAME;
323 PositionNP[0].
fill(
"ANGLE",
"Angle",
"%.2f", 0, 360, 0, 0);
324 PositionNP[1].
fill(
"ERROR",
"Residual error",
"%.2f", -180, 180, 0, 0);
325 PositionNP[2].
fill(
"RETRIES",
"Retries",
"%.0f", 0, 99, 0, 0);
329 SensorNP[0].
fill(
"AGC",
"Gain",
"%.0f", 0, 255, 0, 0);
330 SensorNP[1].
fill(
"MAGNITUDE",
"Magnitude",
"%.0f", 0, 4095, 0, 0);
331 SensorNP[2].
fill(
"MD",
"Magnet detected",
"%.0f", 0, 1, 0, 0);
332 SensorNP[3].
fill(
"ML",
"Field too weak",
"%.0f", 0, 1, 0, 0);
333 SensorNP[4].
fill(
"MH",
"Field too strong",
"%.0f", 0, 1, 0, 0);
349 snprintf(name,
sizeof(name),
"WHEELLY_ANGLE_%d", i + 1);
350 AngleNP.emplace_back(1);
355 AngleNP[i][0].fill(
"ANGLE",
"Calibration angle (°)",
"%.2f", 0,
ANGLE_MAX_DEG, 0.01, 0);
357 AngleNP[i].fill(
getDeviceName(), name, std::to_string(i + 1).c_str(),
364 ActionsSP[0].
fill(
"SAVE",
"Save to the wheel",
ISS_OFF);
367 WheelConfigSP[0].
fill(
"SAVE",
"Save to the wheel",
ISS_OFF);
379 for (
int i = 0; i < JOG_PER_ROW; i++)
381 JogDownSP[i].
fill(JOG_NAMES[i], JOG_LABELS[i],
ISS_OFF);
382 JogUpSP[i].
fill(JOG_NAMES[JOG_PER_ROW + i], JOG_LABELS[JOG_PER_ROW + i],
ISS_OFF);
398 ToleranceNP[2].
fill(
"RETRIES",
"Max retries",
"%.0f", 0,
RETRIES_MAX, 1, 3);
418 HoldNP[0].
fill(
"HOLD_MA",
"Current (mA, 0 = released)",
"%.0f", 0, 800, 5, 0);
424 HoldNP[1].
fill(
"SETTLE_MS",
"Hold after arrival (ms)",
"%.0f", 0,
SETTLE_MS_MAX, 50,
432 DirectionSP[0].
fill(
"DIR_SHORTEST",
"Shortest way",
434 DirectionSP[1].
fill(
"DIR_UP",
"Increasing angles only",
436 DirectionSP[2].
fill(
"DIR_DOWN",
"Decreasing angles only",
451 LedSP[1].
fill(
"LED_PULSE",
"Pulses while moving",
ISS_ON);
453 LedSP[3].
fill(
"LED_TEST",
"Test (blinks WHEELLY)",
ISS_OFF);
466 SweepSP[0].
fill(
"RUN",
"Turn all the way round",
ISS_OFF);
470 FileTP[FILE_LOG].
fill(
"PATH",
"Movement log", log_path().c_str());
471 FileTP[FILE_SWEEP].
fill(
"SWEEP",
"Last sweep",
"not written yet");
475 SweepDirTP[0].
fill(
"DIR",
"Folder", default_folder().c_str());
481 FirmwareTP[0].
fill(
"FW",
"Version",
"");
482 FirmwareTP[1].
fill(
"PROTO",
"Protocol",
"");
483 FirmwareTP[2].
fill(
"SERIAL",
"Serial number",
"");
491 if (FirmwareTP[2].
getText() !=
nullptr)
492 m_expected_serial = FirmwareTP[2].
getText();
528 SlotsNP[0].setValue(m_slots);
556 if (m_serial_to_save)
558 m_serial_to_save =
false;
559 LOGF_INFO(
"%s", fill_in(
"From now on this profile looks for the wheel with serial %1$s.", {m_expected_serial}).c_str());
581 if (LogSP[0].getState() ==
ISS_ON) open_log();
611Wheelly::Fields Wheelly::split_fields(
const std::string &line)
616 while (i < line.size() && line[i] !=
' ') i++;
617 while (i < line.size())
619 while (i < line.size() && line[i] ==
' ') i++;
620 const size_t start = i;
621 while (i < line.size() && line[i] !=
' ') i++;
622 const std::string piece = line.substr(start, i - start);
623 const size_t equals = piece.find(
'=');
624 if (equals != std::string::npos && equals > 0)
625 fields[piece.substr(0, equals)] = piece.substr(equals + 1);
630double Wheelly::number(
const Fields &fields,
const char *key,
double if_missing)
632 const auto found = fields.find(key);
633 if (found == fields.end())
return if_missing;
636 return std::stod(found->second);
644void Wheelly::handle_unsolicited_line(
const std::string &line)
646 if (line.empty())
return;
651 if (m_show_comments)
LOGF_INFO(
"%s", line.c_str() + 1);
663 while (i < line.size() && line[i] ==
' ') i++;
664 const size_t end = line.find(
' ', i);
665 const std::string name = line.substr(i, end == std::string::npos ? std::string::npos : end - i);
673 const Fields c = split_fields(line);
674 const auto reason = c.find(
F_REASON);
675 if (reason != c.end()
677 LOG_WARN(
"The motor driver had lost its settings - the 12 V dropped and came back - and has been set up again before moving.");
680 else LOG_INFO(
"The motor driver got its 12 V after the wheel had started, and has been set up now.");
685 m_magnet_lost =
true;
686 LOG_ERROR(
"The sensor no longer detects the magnet. This is serious: check the sensor wiring before imaging further.");
701 const Fields c = split_fields(line);
703 snprintf(deviation,
sizeof(deviation),
"%.2f", std::fabs(number(c,
F_ERR, 0)));
705 && HoldNP[0].getValue() > 0;
709 snprintf(how_much,
sizeof(how_much),
"%.0f", HoldNP[0].getValue());
711 fill_in(
"The wheel moved %1$s degrees on its own while at rest, past the alarm tolerance, and the holding current is already on at %2$s mA. Raising it may help, but look at the mechanics too: a clutch that lets go at rest is a matter of spring preload.", {deviation, how_much}).c_str());
719 fill_in(
"The wheel moved %1$s degrees on its own while at rest, past the alarm tolerance, and the motor is released at rest, so nothing holds it: turn on the holding current at rest - %2$s mA is the value this project suggests.",
725void Wheelly::collect_unsolicited()
728 while (m_reader.
line(line, 0)) handle_unsolicited_line(line);
731bool Wheelly::command(
const std::string &text, Fields *fields,
732 std::string *error_for_user)
736 if (m_link_lost && !m_reconnecting)
738 LOG_WARN(
"The wheel is not reachable right now: waiting for its USB link to come back.");
741 if (
PortFD < 0)
return false;
745 const std::string to_send = text +
"\n";
751 const int error_number =
errno;
752 if (!m_reconnecting && link_is_gone(error_number))
754 link_lost(strerror(error_number));
759 if (!m_reconnecting)
LOGF_ERROR(
"%s (%s)",
"The wheel is not answering.", explanation);
766 while (m_reader.
line(line, ANSWER_WAIT_MS))
768 if (line.empty())
continue;
771 handle_unsolicited_line(line);
778 if (fields !=
nullptr) *fields = split_fields(line);
783 const Fields c = split_fields(line);
785 sscanf(line.c_str(),
"%*s %d", &code);
786 const auto take = [&c](
const char *k)
788 const auto t = c.find(k);
789 return t == c.end() ? std::string() : t->second;
791 const std::string sentence = error_message(
793 if (error_for_user !=
nullptr) *error_for_user = sentence;
795 if (fields !=
nullptr) *fields = c;
799 LOGF_DEBUG(
"Unexpected line from the wheel: %s", line.c_str());
802 if (m_reconnecting)
return false;
806 :
"the port was closed");
809 LOG_ERROR(
"The wheel is not answering.");
838 m_only_mine = !m_expected_serial.empty();
840 if (!m_only_mine)
return false;
844 fill_in(
"The wheel of this profile (serial %1$s) is not on any port. Connecting to whichever Wheelly is here instead.", {m_expected_serial}).c_str());
852 m_magnet_lost =
false;
860 if (!m_reconnecting)
LOG_ERROR(
"The device on this port did not answer as a Wheelly wheel. Check the port.");
863 if (c[
F_NAME] !=
"wheelly")
865 if (!m_reconnecting)
LOG_ERROR(
"The device on this port did not answer as a Wheelly wheel. Check the port.");
871 LOGF_ERROR(
"%s", fill_in(
"This firmware speaks protocol %1$s, this driver speaks %2$s. Update one of the two.",
880 if (m_only_mine && serial != m_expected_serial)
884 fill_in(
"This is a different Wheelly (serial %1$s) from the one this profile uses (%2$s): looking for yours on the other ports.", {serial, m_expected_serial}).c_str());
892 if (!serial.empty() && serial != m_expected_serial)
894 m_expected_serial = serial;
895 m_serial_to_save =
true;
911 LOGF_WARN(
"%s", fill_in(
"The wheel reports %1$s slots, which this driver cannot handle (from %2$s to %3$s).",
913 std::to_string(count), std::to_string(
MIN_SLOTS),
919 FirmwareTP[0].setText(c[
F_FW].c_str());
920 FirmwareTP[1].setText(c[
F_PROTO].c_str());
921 FirmwareTP[2].setText(c[
F_SERIAL].c_str());
925 LOGF_INFO(
"%s", fill_in(
"Connected to Wheelly, firmware %1$s, protocol %2$s.", {c[
F_FW], c[
F_PROTO]}).c_str());
934bool Wheelly::link_is_gone(
int error_number)
942 switch (error_number)
956void Wheelly::link_lost(
const std::string &why)
958 if (m_link_lost)
return;
965 fill_in(
"The USB link to the wheel was lost (%1$s). The driver stays connected and reconnects by itself as soon as the wheel is back; until then commands cannot reach it.", {why}).c_str());
993 if (m_jog_row !=
nullptr)
999 if (m_sweeping) finish_sweep(
false);
1014 m_retry_pause_s = RETRY_FIRST_S;
1015 m_next_attempt = now_s() + m_retry_pause_s;
1018void Wheelly::remember_alias()
1025 if (m_port_in_use.empty())
return;
1026 char resolved[PATH_MAX];
1027 if (realpath(m_port_in_use.c_str(), resolved) ==
nullptr)
return;
1028 const char *folder = getenv(
"WHEELLY_SERIAL_BY_ID");
1029 const std::string by_id = folder !=
nullptr ? folder :
"/dev/serial/by-id";
1030 DIR *d = opendir(by_id.c_str());
1031 if (d ==
nullptr)
return;
1032 while (
struct dirent *e = readdir(d))
1034 if (e->d_name[0] ==
'.')
continue;
1035 const std::string link = by_id +
"/" + e->d_name;
1036 char target[PATH_MAX];
1037 if (realpath(link.c_str(), target) !=
nullptr && strcmp(target, resolved) == 0)
1044 if (!m_alias.empty())
LOGF_DEBUG(
"Port %s is also %s", m_port_in_use.c_str(), m_alias.c_str());
1047bool Wheelly::try_reconnect()
1055 std::vector<std::string> paths;
1058 if (!m_alias.empty() && std::find(paths.begin(), paths.end(), m_alias) == paths.end())
1059 paths.push_back(m_alias);
1061 const int slots_before = m_slots;
1062 for (
const std::string &path : paths)
1065 if (stat(path.c_str(), &st) != 0)
continue;
1069 m_reconnecting =
true;
1070 m_only_mine = !m_expected_serial.empty();
1072 m_reconnecting =
false;
1076 m_link_lost =
false;
1077 m_port_in_use = path;
1080 if (m_slots != slots_before)
1098void Wheelly::link_found(
const std::string &path)
1100 LOGF_INFO(
"%s", fill_in(
"The wheel is back on %1$s: reconnected.", {path}).c_str());
1118 m_link_lost =
false;
1127void Wheelly::size_slots()
1132 m_angles_stale =
false;
1136void Wheelly::label_slots()
1138 for (
int i = 0; i < m_slots; i++)
1140 const std::string n = std::to_string(i + 1);
1141 const char *key = i + 1 == m_unsaved_slot ?
"▶ %1$s *"
1142 : i + 1 == m_marked_slot ?
"▶ %1$s" :
nullptr;
1143 AngleNP[i].setLabel((key !=
nullptr ? fill_in(key, {n}) : n).c_str());
1147void Wheelly::define_angles()
1150 m_angles_defined = m_slots;
1153void Wheelly::delete_angles()
1155 for (
int i = 0; i < m_angles_defined; i++)
deleteProperty(AngleNP[i]);
1156 m_angles_defined = 0;
1159void Wheelly::mark_current_slot()
1162 if (now != m_marked_slot)
1164 m_marked_slot = now;
1167 if (m_unsaved_slot != 0 && m_unsaved_slot != now) forget_unsaved();
1168 m_angles_stale =
true;
1173void Wheelly::forget_unsaved()
1175 if (m_unsaved_slot == 0)
return;
1177 AngleNP[m_unsaved_slot - 1][0].setValue(m_taught[m_unsaved_slot - 1]);
1179 m_angles_stale =
true;
1182void Wheelly::refresh_angles()
1184 if (!m_angles_stale || m_moving || m_jogging || m_sweeping)
return;
1185 m_angles_stale =
false;
1187 redefine_calibration();
1197void Wheelly::redefine_calibration()
1201 std::addressof(JogDownSP), std::addressof(JogUpSP),
1202 std::addressof(ActionsSP), std::addressof(ToleranceNP),
1203 std::addressof(DiagSP), std::addressof(SweepSP),
1204 std::addressof(SweepDirTP), std::addressof(LogSP), std::addressof(FileTP)
1214void Wheelly::label_jogs()
1217 snprintf(pitch,
sizeof(pitch),
"%.2f", 360.0 / m_slots);
1218 std::string text = pitch;
1219 while (!text.empty() && text.back() ==
'0') text.pop_back();
1220 if (!text.empty() && text.back() ==
'.') text.pop_back();
1221 JogDownSP[0].
setLabel(fill_in(JOG_LABELS[0], {text}).c_str());
1222 JogUpSP[JOG_PER_ROW - 1].
setLabel(fill_in(JOG_LABELS[2 * JOG_PER_ROW - 1], {text}).c_str());
1229bool Wheelly::change_slots(
int count)
1231 if (count == m_slots)
return true;
1234 if (!command(std::string(
CMD_SLOTS) +
" " + std::to_string(count), &c, &error))
1236 LOGF_ERROR(
"%s", fill_in(
"The wheel refused the new number of slots: %1$s", {
error}).c_str());
1239 m_slots = (int)number(c,
F_SLOTS, count);
1247 redefine_calibration();
1250 fill_in(
"The wheel now has %1$s slots. Its calibration starts again from evenly spaced angles and generic names: teach each slot (steps, then Set on its row) and name the filters, then press 'Save to the wheel'. Until then nothing is written: switching the wheel off brings back the previous setup.", {std::to_string(m_slots)}).c_str());
1254bool Wheelly::read_calibration()
1259 for (
int i = 0; i < m_slots; i++)
1261 m_taught[i] = number(c, (
"a" + std::to_string(i + 1)).c_str());
1262 AngleNP[i][0].setValue(i + 1 == m_unsaved_slot ? m_live_angle : m_taught[i]);
1263 AngleNP[i].setState(
IPS_OK);
1271 ToleranceNP[2].setValue(number(c,
F_RETRIES, 3));
1273 ToleranceNP.
apply();
1289 const double ma = number(c,
F_MA, 0);
1290 HoldNP[0].setValue(ma);
1297 read_direction(
true);
1302void Wheelly::take_ceiling(
const Fields &reply,
bool warn)
1304 const double ms = number(reply,
F_CEILING, 0);
1305 if (ms <= 0)
return;
1306 m_wheel_ceiling_s = ms / 1000.0;
1312 char worst[16], ekos[16];
1313 snprintf(worst,
sizeof(worst),
"%.0f", std::ceil(ms / 1000.0));
1316 fill_in(
"With these motor settings the longest filter change can take up to %1$s s, more than the %2$s s after which Ekos gives up on it. Raise the motor speed, shorten the hold after arrival, or choose the shortest way if your wheel allows it.", {worst, ekos}).c_str());
1320void Wheelly::read_direction(
bool warn)
1329 DirectionSP.
reset();
1332 DirectionSP.
apply();
1334 take_ceiling(c, warn);
1337void Wheelly::read_settle()
1345 std::string ignored;
1353 take_ceiling(c,
false);
1356double Wheelly::move_ceiling_s()
const
1358 return m_wheel_ceiling_s > 0 ? m_wheel_ceiling_s + MOVE_CEILING_MARGIN_S : MOVE_CEILING_S;
1364void Wheelly::read_led_mode()
1367 if (!command(
CMD_LED, &c))
return;
1387 for (
int i = 0; i < m_slots; i++)
1389 const std::string key =
"n" + std::to_string(i + 1);
1390 m_names[i] = c.count(key) ? c[key] : (
"Filter" + std::to_string(i + 1));
1392 one.
fill((
"FILTER_SLOT_NAME_" + std::to_string(i + 1)).c_str(),
1393 std::to_string(i + 1).c_str(), m_names[i].c_str());
1412 if (fresh == m_names[i])
continue;
1414 if (!command(std::string(
CMD_NAME) +
" " + std::to_string(i + 1) +
" " + fresh,
1417 LOGF_ERROR(
"%s", fill_in(
"Filter name refused: %1$s", {error}).c_str());
1428 if (!command(std::string(
CMD_GO) +
" " + std::to_string(slot), &c))
1440 if (m_jog_row !=
nullptr)
1452 m_move_start = now_s();
1453 LOGF_DEBUG(
"%s", fill_in(
"Moving to slot %1$s.", {std::to_string(slot)}).c_str());
1462bool Wheelly::read_status(Fields &fields)
1467void Wheelly::update_panels(
const Fields &status)
1469 PositionNP[0].setValue(number(status,
F_ANGLE));
1470 PositionNP[1].setValue(number(status,
F_ERR));
1471 PositionNP[2].setValue(number(status,
F_RETRIES));
1475 const bool magnet = number(status,
F_MD, 1) > 0.5;
1479 if (magnet) m_magnet_lost =
false;
1480 SensorNP[0].setValue(number(status,
F_AGC));
1481 SensorNP[1].setValue(number(status,
F_MAG));
1482 SensorNP[2].setValue(magnet ? 1 : 0);
1483 SensorNP[3].setValue(number(status,
F_ML));
1484 SensorNP[4].setValue(number(status,
F_MH));
1499 if (now_s() >= m_next_attempt)
1501 if (try_reconnect())
1506 m_retry_pause_s = std::min(2.0 * m_retry_pause_s, RETRY_MAX_S);
1507 m_next_attempt = now_s() + m_retry_pause_s;
1513 collect_unsolicited();
1516 if (!read_status(status))
1523 update_panels(status);
1529 if (m_sweeping && m_samples.size() < 4000)
1531 m_samples.push_back({number(status,
F_ANGLE), number(status,
F_MAG)});
1534 const std::string motion = status.count(
F_MOTION) ? status.at(
F_MOTION) :
"";
1536 const int slot = (int)number(status,
F_POS);
1537 const double error = number(status,
F_ERR);
1538 const int retries = (int)number(status,
F_RETRIES);
1543 if (!still && now_s() - m_move_start > move_ceiling_s())
1550 finish_jog(status, outcome);
1558 if (!still && now_s() - m_move_start > move_ceiling_s())
1564 LOGF_ERROR(
"%s", fill_in(
"The wheel did not finish within %1$s seconds. Imaging is stopped.",
1565 {std::to_string((
int)std::ceil(move_ceiling_s()))}).c_str());
1577 LOG_WARN(
"If the motor stalls or the clutch slips, check first that the wheel's detent (its spring click stop) has been removed: the motor cannot climb out of its notches. Assembly guide, chapter 10, The wheel body.");
1580 record_move(status,
"timeout");
1581 if (m_sweeping) finish_sweep(
false);
1586 const std::string which = std::to_string(
TargetFilter);
1588 snprintf(deviation,
sizeof(deviation),
"%.2f", std::fabs(error));
1596 fill_in(
"Slot %1$s NOT reached: off by %2$s deg after %3$s retries. Imaging is stopped so that no frame is taken with the wheel out of place.",
1597 {which, deviation, std::to_string(retries)}).c_str());
1598 LOG_WARN(
"If the motor stalls or the clutch slips, check first that the wheel's detent (its spring click stop) has been removed: the motor cannot climb out of its notches. Assembly guide, chapter 10, The wheel body.");
1607 LOGF_WARN(
"%s", fill_in(
"Slot %1$s reached but off by %2$s deg, beyond the good tolerance. Imaging continues.", {which, deviation}).c_str());
1621 LOGF_WARN(
"Unknown outcome from the wheel: %s", outcome.c_str());
1623 LOGF_INFO(
"%s", fill_in(
"Slot %1$s reached, residual error %2$s deg.", {which, deviation}).c_str());
1627 record_move(status, outcome.c_str());
1634 if (outcome ==
EV_FAILED) finish_sweep(
false);
1659 if (!m_sweeping) mark_current_slot();
1666std::string Wheelly::suggest_name(
const char *text,
int slot)
1677 if (std::strcmp(clean, text) == 0)
return std::string();
1681 if (i + 1 == slot)
continue;
1685 return std::string(clean);
1689 char *names[],
int n)
1694 SweepDirTP.
update(texts, names, n);
1698 const std::string where = expand_home(SweepDirTP[0].
getText());
1699 ::mkdir(where.c_str(), 0755);
1700 const bool usable = (::access(where.c_str(), W_OK) == 0);
1702 LOGF_WARN(
"%s", fill_in(
"The folder %1$s cannot be used right now: %2$s. The sweeps will not be saved until it can.",
1703 {where, std::strerror(
errno)}).c_str());
1713 std::vector<std::string> own;
1714 std::vector<char *> fixed;
1724 auto which_slot = [
this](
const char *element)
1727 if (
FilterNameTP[i].isNameMatch(element))
return (
int)i + 1;
1736 auto refuse = [&](
int slot,
const char *text,
1737 const std::string & why,
1738 const std::string & suggestion)
1740 const std::string sentence = suggestion.empty()
1748 ? fill_in(
"Slot %1$s: \"%2$s\" refused - %3$s.",
1749 {std::to_string(slot), text, why})
1750 : fill_in(
"Slot %1$s: \"%2$s\" refused - %3$s. A name that would be accepted here: \"%4$s\".",
1751 {std::to_string(slot), text, why, suggestion});
1776 const std::string rule =
1777 "Allowed: letters without accents, digits, _ - and . ; 1 to 32 characters; the first and the last must be a letter or a digit.";
1778 const std::string both = sentence +
" " + rule;
1809 own.assign(texts, texts + n);
1811 std::vector<int> given;
1812 for (
int i = 0; i < n; i++)
1814 if (own[i].find_first_not_of(
" \t") == std::string::npos)
1816 const int slot = which_slot(names[i]);
1819 own[i] = std::string(EMPTY_SLOT_NAME) +
"_" + std::to_string(slot);
1823 fixed[i] = &own[i][0];
1825 texts = fixed.data();
1827 for (
int i = 0; i < n; i++)
1830 if (check ==
NAME_OK)
continue;
1832 const int slot = which_slot(names[i]);
1833 refuse(slot, texts[i], filter_name_reason(check, texts[i]),
1834 suggest_name(texts[i], slot));
1839 for (
int i = 0; i < n; i++)
1846 for (
int k = 0; k < n && !clash; k++)
1848 if (!clash)
continue;
1853 refuse(which_slot(names[i]), texts[i],
1854 "another slot already has this name, ignoring upper and lower case", std::string());
1862 LOGF_INFO(
"%s", fill_in(
"Slot %1$s left empty, so it has no filter: it is now called \"%2$s\".",
1863 {std::to_string(which_slot(names[i])), own[i]}).c_str());
1869 char *names[],
int n)
1874 for (
int i = 0; i < m_slots; i++)
1876 if (!AngleNP[i].isNameMatch(name))
continue;
1877 if (n >= 1) set_angle(i + 1, values[0]);
1883 ToleranceNP.
update(values, names, n);
1885 snprintf(line,
sizeof(line),
"%s %.2f %.2f %.0f",
CMD_TOLERANCE,
1886 ToleranceNP[0].getValue(), ToleranceNP[1].getValue(),
1887 ToleranceNP[2].getValue());
1889 const bool ok = command(line, &c);
1892 ToleranceNP[0].setValue(number(c,
F_GOOD, ToleranceNP[0].getValue()));
1893 ToleranceNP[1].setValue(number(c,
F_WARN, ToleranceNP[1].getValue()));
1894 ToleranceNP[2].setValue(number(c,
F_RETRIES, ToleranceNP[2].getValue()));
1897 ToleranceNP.
apply();
1899 if (ok) read_direction(
true);
1905 SlotsNP.
update(values, names, n);
1906 const bool ok = change_slots((
int)SlotsNP[0].getValue());
1907 SlotsNP[0].setValue(m_slots);
1915 MotorNP.
update(values, names, n);
1920 snprintf(line,
sizeof(line),
"%s %.0f %.0f %.0f",
CMD_MOTOR, MotorNP[0].getValue(),
1921 MotorNP[1].getValue(), MotorNP[2].getValue());
1923 const bool ok = command(line, &c);
1926 MotorNP[0].setValue(number(c,
F_MA, MotorNP[0].getValue()));
1927 MotorNP[1].setValue(number(c,
F_SPEED, MotorNP[1].getValue()));
1928 MotorNP[2].setValue(number(c,
F_ACCEL, MotorNP[2].getValue()));
1930 take_ceiling(c,
true);
1939 const double settle_before = HoldNP[1].getValue();
1940 HoldNP.
update(values, names, n);
1942 snprintf(line,
sizeof(line),
"%s %.0f",
CMD_HOLD, HoldNP[0].getValue());
1944 bool ok = command(line, &c);
1947 HoldNP[0].setValue(number(c,
F_MA, HoldNP[0].getValue()));
1950 if (HoldNP[0].getValue() > 0)
1951 LOG_WARN(
"Holding current is on: the motor stays energised at rest. It warms up next to the sensor and the driver sings during exposures. Leave it off unless the wheel drifts at rest.");
1957 if (ok && HoldNP[1].getValue() != settle_before)
1959 snprintf(line,
sizeof(line),
"%s %.0f",
CMD_SETTLE, HoldNP[1].getValue());
1961 ok = command(line, &s);
1964 HoldNP[1].setValue(number(s,
F_MS, HoldNP[1].getValue()));
1965 take_ceiling(s,
true);
1969 HoldNP[1].setValue(settle_before);
1983 char *names[],
int n)
1993 which.
update(states, names, n);
1996 const bool ok = pressed ? save_to_wheel() :
true;
2003 return jog_pressed(JogDownSP, 0, states, names, n);
2005 return jog_pressed(JogUpSP, JOG_PER_ROW, states, names, n);
2009 DirectionSP.
update(states, names, n);
2013 const bool ok = command(std::string(
CMD_DIRECTION) +
" " + argument, &c);
2018 const std::string read = t != c.end() ? t->second : std::string(argument);
2019 DirectionSP.
reset();
2021 LOG_INFO(
"Direction of travel changed. Press \"Save to the wheel\" to keep the choice after power-off.");
2022 take_ceiling(c,
true);
2025 DirectionSP.
apply();
2031 LedSP.
update(states, names, n);
2033 const char *argument = choice == 0 ?
ARG_ON
2037 const bool ok = command(std::string(
CMD_LED) +
" " + argument, &c);
2038 if (ok && choice == 3)
2041 fill_in(
"The LED is blinking WHEELLY in Morse for %1$s seconds. Do this with the cover open or in daylight: it is inside the optical path.",
2048 ?
"The LED stays off, and breathes gently while the wheel goes to another filter. Press \"Save to the wheel\" to keep the choice after power-off."
2049 :
"LED mode changed. Press \"Save to the wheel\" to keep the choice after power-off.");
2071 DiagSP.
update(states, names, n);
2072 m_show_comments =
true;
2073 LOG_INFO(
"Asking the wheel whether it can really talk to its sensor and its motor driver:");
2075 m_show_comments =
false;
2084 SweepSP.
update(states, names, n);
2086 if (m_moving || m_sweeping)
2088 LOG_ERROR(
"Cannot sweep while the wheel is moving: wait for it to stop.");
2101 LogSP.
update(states, names, n);
2102 if (LogSP[0].getState() ==
ISS_ON) open_log();
2124 FirmwareTP.
save(fp);
2125 ToleranceNP.
save(fp);
2126 SweepDirTP.
save(fp);
2145void Wheelly::set_angle(
int slot,
double value)
2148 if (m_moving || m_sweeping || m_jogging)
2150 LOG_ERROR(
"Cannot change the angles while the wheel is moving: wait for it to stop.");
2155 const bool confirm = slot == m_unsaved_slot && std::fabs(value - m_live_angle) < 0.005;
2156 const double before = m_taught[slot - 1];
2157 if (confirm || std::fabs(value - before) >= 0.005)
2160 snprintf(line,
sizeof(line),
"%s %d %.2f",
CMD_ANGLE, slot, confirm ? m_live_angle : value);
2163 if (!command(line, &c, &error))
2165 LOGF_ERROR(
"%s", fill_in(
"Slot %1$s: the angle was not changed. %2$s", {std::to_string(slot),
error}).c_str());
2166 row[0].setValue(slot == m_unsaved_slot ? m_live_angle : before);
2171 const double after = number(c, (
"a" + std::to_string(slot)).c_str(), value);
2172 m_taught[slot - 1] = after;
2173 row[0].setValue(after);
2174 if (slot == m_unsaved_slot)
2177 m_angles_stale =
true;
2179 note_angle_change(slot, before, after, confirm ?
"angle-taught" :
"angle-set");
2181 "Slot %1$s now means the angle the wheel is at right now. Press 'Save to the wheel' to keep it after the wheel is switched off."
2182 :
"The new angles live in the wheel's working memory: press 'Save to the wheel' to keep them after the wheel is switched off.",
2183 {std::to_string(slot)}).c_str());
2208void Wheelly::note_angle_change(
int slot,
double before,
double after,
const char *what)
2210 char was[16], is[16], delta[16];
2211 snprintf(was,
sizeof(was),
"%.2f", before);
2212 snprintf(is,
sizeof(is),
"%.2f", after);
2213 snprintf(delta,
sizeof(delta),
"%.2f", std::remainder(after - before, 360.0));
2217 LOGF_INFO(
"%s", fill_in(
"Slot %1$s: %2$s° → %3$s°.", {std::to_string(slot), was, is}).c_str());
2218 if (m_log ==
nullptr)
return;
2220 const std::time_t now = std::time(
nullptr);
2221 std::strftime(when,
sizeof(when),
"%Y-%m-%dT%H:%M:%S", std::gmtime(&now));
2222 std::fprintf(m_log,
"%s,%d,%s,%s,%s,,%s,,\n", when, slot, is, was, delta, what);
2226bool Wheelly::save_to_wheel()
2234 if (ok)
LOG_INFO(
"Calibration saved in the wheel.");
2241 ISState *states,
char *names[],
int n)
2243 row.
update(states, names, n);
2251 if (m_moving || m_sweeping || m_jogging)
2253 LOG_ERROR(
"Cannot move the wheel by a step while it is moving: wait for it to stop.");
2260 double degrees = JOG_STEPS[first + choice];
2261 if (degrees == 0.0) degrees = (first == 0 ? -360.0 : 360.0) / m_slots;
2263 snprintf(line,
sizeof(line),
"%s %.4f",
CMD_JOG, degrees);
2264 if (!command(line,
nullptr, &error))
2273 m_jog_row = std::addressof(row);
2274 m_move_start = now_s();
2280void Wheelly::finish_jog(
const Fields &status,
const std::string &outcome)
2288 char angle[16], off[16];
2289 snprintf(angle,
sizeof(angle),
"%.2f", number(status,
F_ANGLE));
2290 snprintf(off,
sizeof(off),
"%.2f", std::fabs(number(status,
F_ERR)));
2291 const bool failed = outcome ==
EV_FAILED;
2295 fill_in(
"The wheel did not get where the step asked: it is at %1$s°, %2$s° away. Try again, or turn it by hand and write the angle it reaches in its row.", {angle, off}).c_str());
2296 LOG_WARN(
"If the motor stalls or the clutch slips, check first that the wheel's detent (its spring click stop) has been removed: the motor cannot climb out of its notches. Assembly guide, chapter 10, The wheel body.");
2299 else LOGF_INFO(
"%s", fill_in(
"The wheel is at %1$s°, %2$s° from where the step asked.", {angle, off}).c_str());
2300 if (m_jog_row !=
nullptr)
2313 if (m_unsaved_slot != 0 && m_unsaved_slot != slot) forget_unsaved();
2314 m_unsaved_slot = slot;
2318 m_live_angle = std::round(number(status,
F_ANGLE) * 100.0) / 100.0;
2319 AngleNP[slot - 1][0].setValue(m_live_angle);
2320 m_angles_stale =
true;
2326void Wheelly::start_sweep()
2329 m_samples.reserve(512);
2335 m_sweep_remaining = m_slots;
2343 LOG_INFO(
"Turning all the way round and measuring the magnet at every step. The wheel moves: do this with the cover open, not during a sequence.");
2347void Wheelly::sweep_step()
2349 if (m_sweep_remaining <= 0)
2354 m_sweep_remaining--;
2360 finish_sweep(
false);
2367void Wheelly::finish_sweep(
bool succeeded)
2373 if (!succeeded || m_samples.size() < 4)
2375 LOG_ERROR(
"The sweep stopped before finishing the turn: it would lie about the magnet, so nothing was written.");
2381 double minimum = m_samples[0].magnitude, maximum = minimum;
2382 for (
const Sample &c : m_samples)
2384 minimum = std::min(minimum, c.magnitude);
2385 maximum = std::max(maximum, c.magnitude);
2399 const std::string where = sweep_path();
2400 ensure_folder(where);
2401 bool written =
false;
2407 if (std::FILE *f = std::fopen(where.c_str(),
"w"))
2409 bool ok = std::fprintf(f,
"# Wheelly magnet sweep\n# slots,%d\n# taught_deg", m_slots) > 0;
2410 for (
int i = 0; i < m_slots; i++) ok = ok && std::fprintf(f,
",%.2f", m_taught[i]) > 0;
2411 ok = ok && std::fprintf(f,
"\nangle_deg,magnitude\n") > 0;
2412 for (
const Sample &c : m_samples)
2413 ok = ok &&
std::fprintf(f,
"%.2f,%.0f\n", c.angle, c.magnitude) > 0;
2415 if (!written) why =
errno;
2416 if (std::fclose(f) != 0 && written)
2428 FileTP[FILE_SWEEP].setText(where.c_str());
2432 FileTP[FILE_SWEEP].setText(
"not written yet");
2433 LOGF_WARN(
"%s", fill_in(
"Cannot write the sweep to %1$s: %2$s",
2434 {where, std::strerror(why)}).c_str());
2440 snprintf(deviation,
sizeof(deviation),
"%.1f",
2441 maximum > 0 ? (maximum - minimum) / maximum * 100.0 : 0.0);
2443 fill_in(
"Sweep done: %1$s samples, magnitude from %2$s to %3$s, excursion %4$s counts (%5$s%%). The smaller it is, the better the magnet is centred.",
2445 std::to_string(m_samples.size()),
2446 std::to_string((
long)minimum), std::to_string((
long)maximum),
2447 std::to_string((
long)(maximum - minimum)), deviation
2451 fill_in(
"The sweep is here: %1$s. To see it as a curve, open it with the sweep viewer: https://teoteo.github.io/Wheelly/tools/sweep.html",
2459std::string Wheelly::sweep_path()
const
2473 const std::time_t now = std::time(
nullptr);
2475 localtime_r(&now, &parts);
2476 std::strftime(when,
sizeof(when),
"%Y-%m-%d_%H-%M-%S", &parts);
2478 std::string folder = expand_home(SweepDirTP[0].
getText());
2479 while (folder.size() > 1 && folder.back() ==
'/') folder.pop_back();
2480 if (folder.empty()) folder = default_folder();
2481 return folder +
"/" + when +
"_wheelly_sweep.csv";
2484std::string Wheelly::default_folder()
2486 const char *home = getenv(
"HOME");
2487 return std::string(home ? home :
"/tmp") +
"/Documents";
2490std::string Wheelly::expand_home(
const std::string &path)
2492 if (path.compare(0, 2,
"~/") != 0)
return path;
2493 const char *home = getenv(
"HOME");
2494 return std::string(home ? home :
"/tmp") + path.substr(1);
2497void Wheelly::ensure_folder(
const std::string &file_path)
2499 const size_t cut = file_path.rfind(
'/');
2500 if (cut == std::string::npos || cut == 0)
return;
2501 ::mkdir(file_path.substr(0, cut).c_str(), 0755);
2504std::string Wheelly::log_path()
const
2506 const char *home = getenv(
"HOME");
2509 return std::string(home ? home :
"/tmp") +
"/.indi/wheelly_movements.csv";
2512void Wheelly::open_log()
2514 if (m_log !=
nullptr)
return;
2515 const std::string path = log_path();
2521 const std::string old = path.substr(0, path.rfind(
'/')) +
"/wheelly_movimenti.csv";
2522 if (access(path.c_str(), F_OK) != 0 && access(old.c_str(), F_OK) == 0)
2523 std::rename(old.c_str(), path.c_str());
2525 const bool fresh = (access(path.c_str(), F_OK) != 0);
2526 m_log = std::fopen(path.c_str(),
"a");
2527 if (m_log ==
nullptr)
2529 LOGF_ERROR(
"%s", fill_in(
"Cannot write the log file %1$s: %2$s",
2530 {path, std::strerror(
errno)}).c_str());
2543 "timestamp,slot,target,angle,error,retries,outcome,agc,magnitude\n");
2545 LOGF_INFO(
"%s", fill_in(
"Movement log on: %1$s", {path}).c_str());
2548void Wheelly::close_log()
2550 if (m_log ==
nullptr)
return;
2555void Wheelly::record_move(
const Fields &status,
const char *outcome)
2557 if (m_log ==
nullptr)
return;
2560 const std::time_t now = std::time(
nullptr);
2561 std::strftime(when,
sizeof(when),
"%Y-%m-%dT%H:%M:%S", std::gmtime(&now));
2563 const auto take = [&status](
const char *k)
2565 const auto t = status.find(k);
2566 return t == status.end() ? std::string(
"") : t->second;
2568 std::fprintf(m_log,
"%s,%d,%s,%s,%s,%s,%s,%s,%s\n",
virtual const char * port()
void setPortMatchPattern(const std::string &pattern)
setPortMatchPattern Set a regex pattern used to automatically select a serial port from the discovere...
void setDefaultBaudRate(BaudRate newRate)
setDefaultBaudRate Set default baud rate. The default baud rate is 9600 unless otherwise changed by t...
virtual bool Disconnect() override
Disconnect Disconnect from device.
INDI::PropertyText getText(const char *name) const
void addPollPeriodControl()
Add Polling period control to the driver.
virtual bool saveConfig(bool silent=false, const char *property=nullptr)
Save the current properties in a configuration file.
void addConfigurationControl()
Add Configuration control to the driver.
void addSimulationControl()
Add Simulation control to the driver.
virtual bool Disconnect()
Disconnect from device.
void setDefaultPollingPeriod(uint32_t msec)
setDefaultPollingPeriod Change the default polling period to call TimerHit() function in the driver.
virtual void setConnected(bool status, IPState state=IPS_OK, const char *msg=nullptr)
Set connection switch status in the client.
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.
int SetTimer(uint32_t ms)
Set a timer to call the function TimerHit after ms milliseconds.
virtual bool Connect()
Connect to the device. INDI::DefaultDevice implementation connects to appropriate connection interfac...
void addDebugControl()
Add Debug control 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 GetFilterNames()
Obtains a list of filter names from the hardware and initializes the FilterNameTP property....
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.
virtual bool saveConfigItems(FILE *fp) override
saveConfigItems Save specific properties in the provide config file handler. Child class usually over...
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.
virtual bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) override
Process the client newSwitch command.
void push(WidgetView< T > &&item)
void setState(IPState state)
void apply(const char *format,...) const ATTRIBUTE_FORMAT_PRINTF(2
bool load()
load Attempt to load property values from configuration file.
void save(FILE *f) const
save Save property to configuration file.
void setLabel(const char *label)
const char * getName() const
bool isNameMatch(const char *otherName) const
void setGroupName(const char *name)
void reserve(size_t size)
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)
int findOnSwitchIndex() const
void fill(const char *device, const char *name, const char *label, const char *group, IPerm permission, ISRule rule, double timeout, IPState state)
bool update(const char *const texts[], 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)
Provides generic container for INDI properties.
INDI::PropertyViewText * getText() const
bool channel_down() const
bool line(std::string &out, int wait_ms)
void attach(int descriptor)
bool SelectFilter(int slot) override
Select a new filter position.
int QueryFilter() override
Return current filter position.
bool updateProperties() override
updateProperties is called whenever there is a change in the CONNECTION status of the driver....
bool Connect() override
Connect to the device. INDI::DefaultDevice implementation connects to appropriate connection interfac...
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.
bool saveConfigItems(FILE *fp) override
saveConfigItems Save specific properties in the provide config file handler. Child class usually over...
bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) override
Process the client newSwitch command.
bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override
Process the client newSwitch command.
bool GetFilterNames() override
Obtains a list of filter names from the hardware and initializes the FilterNameTP property....
bool Handshake() override
perform handshake with device to check communication
const char * getDefaultName() override
bool SetFilterNames() override
Set filter names as defined by the client for each filter position. The desired filter names are stor...
bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override
Process the client newNumber command.
bool Disconnect() override
Disconnect from device.
int tty_connect(const char *device, int bit_rate, int word_size, int parity, int stop_bits, int *fd)
Establishes a tty connection to a terminal device.
int tty_disconnect(int fd)
Closes a tty connection and flushes the bus.
int tty_write_string(int fd, const char *buf, int *nbytes_written)
Writes a null terminated string to fd.
void tty_error_msg(int err_code, char *err_msg, int err_msg_len)
Retrieve the tty error message.
Implementations for common driver routines.
#define LOGF_INFO(fmt,...)
#define LOGF_WARN(fmt,...)
#define LOGF_DEBUG(fmt,...)
#define LOG_ERROR(txt)
Shorter logging macros. In order to use these macros, the function (or method) "getDeviceName()" must...
#define LOGF_ERROR(fmt,...)
@ error
throw a parse_error exception in case of a tag
const size_t FILTER_NAME_MAX
bool name_is_allowed(char c)
const uint16_t RECOMMENDED_HOLD_MA
const int PROTOCOL_VERSION
const char CMD_TOLERANCE[]
bool name_equal_nocase(const char *a, const char *b)
const char MOTION_FAILED[]
const char CMD_DIRECTION[]
const size_t WHEELLY_LINE_MAX
const char OUTCOME_NONE[]
const char ARG_SHORTEST[]
const unsigned long MOTOR_ACCEL_MAX
void sanitize_filter_name(const char *in, char *out, size_t out_size)
const unsigned long FACTORY_ACCEL
const unsigned FACTORY_SETTLE_MS
const int TOLERANCE_MAX_DEG
const unsigned long FACTORY_SPEED
const char PREFIX_ERROR[]
const unsigned MOTOR_MA_MAX
NameCheck check_filter_name(const char *s)
const unsigned FACTORY_RUN_MA
const float FACTORY_GOOD_DEG
const char FACTORY_DIRECTION[]
const char PREFIX_COMMENT
const unsigned long MOTOR_SPEED_MAX
const unsigned SETTLE_MS_MAX
const float FACTORY_WARN_DEG
const unsigned long EKOS_FILTER_TIMEOUT_MS
void fill(const char *name, const char *label, const char *initialText)