v2.0.0
Loading...
Searching...
No Matches
polhemus_connection.cpp
Go to the documentation of this file.
1//=============================================================================================================
21
22#include "polhemus_connection.h"
23
24#include <QtMath>
25
26using namespace UTILSLIB;
27
28namespace
29{
30constexpr int kMockTickIntervalMs = 100;
31constexpr float kMockSphereRadiusM = 0.10f; // 10 cm (typical adult head)
32constexpr int kMockPointsPerLap = 60;
33constexpr int kStreamPauseMs = 300; // pen-button pause threshold
34}
35
36//=============================================================================================================
37
39: QObject(parent)
40{
41 m_mockTimer.setInterval(kMockTickIntervalMs);
42 connect(&m_mockTimer, &QTimer::timeout, this, &PolhemusConnection::onMockTick);
43
44 m_streamPauseTimer.setSingleShot(true);
45 m_streamPauseTimer.setInterval(kStreamPauseMs);
46 connect(&m_streamPauseTimer, &QTimer::timeout, this, &PolhemusConnection::onStreamPauseTimeout);
47}
48
49//=============================================================================================================
50
55
56//=============================================================================================================
57
58bool PolhemusConnection::open(const QString& portName, const PolhemusSerialConfig& cfg)
59{
60 if (m_isConnected) {
61 return true;
62 }
63
64 if (!portName.isEmpty()) {
65 if (openSerial(portName, cfg)) {
66 m_isConnected = true;
67 emit connectedChanged(true);
68 return true;
69 }
70 // Serial open failed; do NOT silently fall back — surface the error.
71 return false;
72 }
73
74 if (!openMock()) {
75 return false;
76 }
77
78 m_isConnected = true;
79 emit connectedChanged(true);
80 return true;
81}
82
83//=============================================================================================================
84
86{
87 if (!m_isConnected) {
88 return;
89 }
90 if (m_pSerial) {
91 closeSerial();
92 } else {
93 closeMock();
94 }
95 m_isConnected = false;
96 m_backendName.clear();
97 emit connectedChanged(false);
98}
99
100//=============================================================================================================
101
103{
104 return m_isConnected;
105}
106
108{
109 return m_backendName;
110}
111
112//=============================================================================================================
113// Port discovery helpers
114//=============================================================================================================
115
117{
118 QStringList names;
119 const auto ports = QSerialPortInfo::availablePorts();
120 names.reserve(ports.size());
121 for (const auto& info : ports) {
122 names << info.portName();
123 }
124 return names;
125}
126
128{
129 constexpr quint16 kPolhemusVid = 0x0F44;
130 constexpr quint16 kFtdiVid = 0x0403;
131
132 QString ftdiCandidate;
133 for (const auto& info : QSerialPortInfo::availablePorts()) {
134 if (!info.hasVendorIdentifier()) {
135 continue;
136 }
137 const quint16 vid = info.vendorIdentifier();
138 if (vid == kPolhemusVid) {
139 return info.portName();
140 }
141 if (vid == kFtdiVid && ftdiCandidate.isEmpty()) {
142 ftdiCandidate = info.portName();
143 }
144 }
145 return ftdiCandidate;
146}
147
148//=============================================================================================================
149// Mock backend
150//=============================================================================================================
151
152bool PolhemusConnection::openMock()
153{
154 m_backendName = QStringLiteral("mock");
155 m_mockSampleIdx = 0;
156 m_mockTimer.start();
157 return true;
158}
159
160void PolhemusConnection::closeMock()
161{
162 m_mockTimer.stop();
163}
164
165void PolhemusConnection::onMockTick()
166{
167 const double phase = (2.0 * M_PI * (m_mockSampleIdx % kMockPointsPerLap)) / static_cast<double>(kMockPointsPerLap);
168 const double elevation = 0.3 * std::sin(phase * 3.0);
169 const QVector3D pos(
170 kMockSphereRadiusM * static_cast<float>(std::cos(phase)),
171 kMockSphereRadiusM * static_cast<float>(std::sin(phase)),
172 kMockSphereRadiusM * static_cast<float>(elevation));
173 ++m_mockSampleIdx;
174
175 // Station 1 = pen (stylus — matches default penStation=1)
176 const QVector3D penPos(
177 kMockSphereRadiusM * static_cast<float>(std::cos(phase + 0.3)),
178 kMockSphereRadiusM * static_cast<float>(std::sin(phase + 0.3)),
179 kMockSphereRadiusM * static_cast<float>(elevation) + 0.02f);
180 const QQuaternion penOri = QQuaternion::fromAxisAndAngle(
181 QVector3D(0, 0, 1), static_cast<float>(phase * 180.0 / M_PI));
182 emit pointReceived(/*station*/ 1, penPos, penOri);
183
184 // Station 2 = tracker (device pose — matches default trackerStation=2)
185 emit pointReceived(/*station*/ 2, pos, QQuaternion());
186}
187
188//=============================================================================================================
189// Serial / Fastrak backend
190//=============================================================================================================
191
192bool PolhemusConnection::openSerial(const QString& portName, const PolhemusSerialConfig& cfg)
193{
194 auto* port = new QSerialPort(this);
195 port->setPortName(portName);
196 port->setBaudRate(cfg.baudRate);
197 port->setDataBits(QSerialPort::Data8);
198 port->setParity(QSerialPort::NoParity);
199 port->setStopBits(QSerialPort::OneStop);
200 port->setFlowControl(QSerialPort::NoFlowControl);
201
202 if (!port->open(QIODevice::ReadWrite)) {
203 emit errorOccurred(QStringLiteral("Serial open failed (%1): %2")
204 .arg(portName, port->errorString()));
205 port->deleteLater();
206 return false;
207 }
208
209 m_pSerial = port;
210 m_parser.reset();
211 m_parser.setUnits(cfg.units);
212
213 connect(m_pSerial, &QSerialPort::readyRead, this, &PolhemusConnection::onSerialReadyRead);
214 connect(m_pSerial, &QSerialPort::errorOccurred, this, &PolhemusConnection::onSerialError);
215
216 // Ensure all four stations are enabled and output position + Euler angles.
217 // Fastrak command: "l<station>,1\r" — enable station output
218 // Fastrak command: "O<station>,2,4,1\r" — output position (2), Euler angles (4), CRLF (1)
219 // NOTE: item 11 (quaternion) is not supported by all Fastrak firmware versions.
220 // Harmless for stations without a sensor physically connected.
221 for (int st = 1; st <= 4; ++st) {
222 m_pSerial->write(QStringLiteral("l%1,1\r").arg(st).toLatin1());
223 m_pSerial->write(QStringLiteral("O%1,2,4,1\r").arg(st).toLatin1());
224 }
225 m_pSerial->flush();
226
227 // Set tracking hemisphere per station if configured (non-zero vector).
228 // Fastrak command: "H<station>,x,y,z\r" — prevents position jumps
229 // when a sensor crosses the default hemisphere boundary.
230 if (!cfg.hemisphere.isNull()) {
231 const QVector3D& h = cfg.hemisphere;
232 for (int st = 1; st <= 4; ++st) {
233 const QByteArray cmd = QStringLiteral("H%1,%2,%3,%4\r")
234 .arg(st)
235 .arg(h.x(), 0, 'f', 1)
236 .arg(h.y(), 0, 'f', 1)
237 .arg(h.z(), 0, 'f', 1)
238 .toLatin1();
239 m_pSerial->write(cmd);
240 }
241 m_pSerial->flush();
242 qInfo() << "Fastrak: hemisphere set to" << cfg.hemisphere;
243 }
244
245 if (!cfg.streamCommand.isEmpty()) {
246 m_pSerial->write(cfg.streamCommand);
247 }
248
249 m_backendName = QStringLiteral("Fastrak (%1 @ %2 baud)")
250 .arg(portName)
251 .arg(cfg.baudRate);
252 return true;
253}
254
255void PolhemusConnection::closeSerial()
256{
257 if (!m_pSerial) {
258 return;
259 }
260 m_streamPauseTimer.stop();
261 m_pSerial->disconnect(this);
262 if (m_pSerial->isOpen()) {
263 m_pSerial->write(QByteArrayLiteral("P\r"));
264 m_pSerial->flush();
265 m_pSerial->close();
266 }
267 m_pSerial->deleteLater();
268 m_pSerial = nullptr;
269 m_parser.reset();
270}
271
272void PolhemusConnection::onSerialReadyRead()
273{
274 if (!m_pSerial) {
275 return;
276 }
277 m_parser.append(m_pSerial->readAll());
278 drainParser();
279}
280
281void PolhemusConnection::onSerialError(QSerialPort::SerialPortError err)
282{
283 if (err == QSerialPort::NoError) {
284 return;
285 }
286 if (!m_pSerial) {
287 return;
288 }
289 const QString msg = QStringLiteral("Serial error: %1").arg(m_pSerial->errorString());
290 emit errorOccurred(msg);
291
292 if (err == QSerialPort::ResourceError || err == QSerialPort::DeviceNotFoundError) {
293 close();
294 }
295}
296
297void PolhemusConnection::drainParser()
298{
299 FastrakSample s;
300 while (m_parser.nextSample(s)) {
301 m_lastSamples[s.station] = {s.position, s.orientation};
303 }
304 if (m_pSerial) {
305 m_streamPauseTimer.start();
306 }
307}
308
309void PolhemusConnection::onStreamPauseTimeout()
310{
311 if (!m_pSerial || !m_isConnected)
312 return;
313
314 for (auto it = m_lastSamples.constBegin(); it != m_lastSamples.constEnd(); ++it) {
315 emit penButtonPressed(it.key(), it.value().position, it.value().orientation);
316 }
317
318 m_pSerial->write(QByteArrayLiteral("C\r"));
319}
Polhemus digitizer connection abstraction.
#define M_PI
Shared utilities (I/O helpers, spectral analysis, layout management, warp algorithms).
QVector3D position
Position in metres (always normalised to SI).
QQuaternion orientation
Sensor orientation; identity if not provided.
int station
1-based station id reported by the device.
Settings used to open a real Polhemus serial connection.
void connectedChanged(bool isConnected)
void penButtonPressed(int station, const QVector3D &position, const QQuaternion &orientation)
void pointReceived(int station, const QVector3D &position, const QQuaternion &orientation)
PolhemusConnection(QObject *parent=nullptr)
void errorOccurred(const QString &message)
bool open(const QString &portName, const PolhemusSerialConfig &cfg=PolhemusSerialConfig{})
static QString autoDetectPortName()
Best-effort auto-detection of an attached Fastrak/FastSCAN.
static QStringList availablePorts()
Enumerate every serial port currently visible to the OS.