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 {
29constexpr int kMockTickIntervalMs = 100;
30constexpr float kMockSphereRadiusM = 0.10f; // 10 cm (typical adult head)
31constexpr int kMockPointsPerLap = 60;
32constexpr int kStreamPauseMs = 300; // pen-button pause threshold
33}
34
35//=============================================================================================================
36
38 : QObject(parent)
39{
40 m_mockTimer.setInterval(kMockTickIntervalMs);
41 connect(&m_mockTimer, &QTimer::timeout, this, &PolhemusConnection::onMockTick);
42
43 m_streamPauseTimer.setSingleShot(true);
44 m_streamPauseTimer.setInterval(kStreamPauseMs);
45 connect(&m_streamPauseTimer, &QTimer::timeout, this, &PolhemusConnection::onStreamPauseTimeout);
46}
47
48//=============================================================================================================
49
54
55//=============================================================================================================
56
57bool PolhemusConnection::open(const QString& portName, const PolhemusSerialConfig& cfg)
58{
59 if (m_isConnected) {
60 return true;
61 }
62
63 if (!portName.isEmpty()) {
64 if (openSerial(portName, cfg)) {
65 m_isConnected = true;
66 emit connectedChanged(true);
67 return true;
68 }
69 // Serial open failed; do NOT silently fall back — surface the error.
70 return false;
71 }
72
73 if (!openMock()) {
74 return false;
75 }
76
77 m_isConnected = true;
78 emit connectedChanged(true);
79 return true;
80}
81
82//=============================================================================================================
83
85{
86 if (!m_isConnected) {
87 return;
88 }
89 if (m_pSerial) {
90 closeSerial();
91 } else {
92 closeMock();
93 }
94 m_isConnected = false;
95 m_backendName.clear();
96 emit connectedChanged(false);
97}
98
99//=============================================================================================================
100
102{
103 return m_isConnected;
104}
105
107{
108 return m_backendName;
109}
110
111//=============================================================================================================
112// Port discovery helpers
113//=============================================================================================================
114
116{
117 QStringList names;
118 const auto ports = QSerialPortInfo::availablePorts();
119 names.reserve(ports.size());
120 for (const auto& info : ports) {
121 names << info.portName();
122 }
123 return names;
124}
125
127{
128 constexpr quint16 kPolhemusVid = 0x0F44;
129 constexpr quint16 kFtdiVid = 0x0403;
130
131 QString ftdiCandidate;
132 for (const auto& info : QSerialPortInfo::availablePorts()) {
133 if (!info.hasVendorIdentifier()) {
134 continue;
135 }
136 const quint16 vid = info.vendorIdentifier();
137 if (vid == kPolhemusVid) {
138 return info.portName();
139 }
140 if (vid == kFtdiVid && ftdiCandidate.isEmpty()) {
141 ftdiCandidate = info.portName();
142 }
143 }
144 return ftdiCandidate;
145}
146
147//=============================================================================================================
148// Mock backend
149//=============================================================================================================
150
151bool PolhemusConnection::openMock()
152{
153 m_backendName = QStringLiteral("mock");
154 m_mockSampleIdx = 0;
155 m_mockTimer.start();
156 return true;
157}
158
159void PolhemusConnection::closeMock()
160{
161 m_mockTimer.stop();
162}
163
164void PolhemusConnection::onMockTick()
165{
166 const double phase = (2.0 * M_PI * (m_mockSampleIdx % kMockPointsPerLap))
167 / 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).arg(h.y(), 0, 'f', 1).arg(h.z(), 0, 'f', 1)
236 .toLatin1();
237 m_pSerial->write(cmd);
238 }
239 m_pSerial->flush();
240 qInfo() << "Fastrak: hemisphere set to" << cfg.hemisphere;
241 }
242
243 if (!cfg.streamCommand.isEmpty()) {
244 m_pSerial->write(cfg.streamCommand);
245 }
246
247 m_backendName = QStringLiteral("Fastrak (%1 @ %2 baud)")
248 .arg(portName)
249 .arg(cfg.baudRate);
250 return true;
251}
252
253void PolhemusConnection::closeSerial()
254{
255 if (!m_pSerial) {
256 return;
257 }
258 m_streamPauseTimer.stop();
259 m_pSerial->disconnect(this);
260 if (m_pSerial->isOpen()) {
261 m_pSerial->write(QByteArrayLiteral("P\r"));
262 m_pSerial->flush();
263 m_pSerial->close();
264 }
265 m_pSerial->deleteLater();
266 m_pSerial = nullptr;
267 m_parser.reset();
268}
269
270void PolhemusConnection::onSerialReadyRead()
271{
272 if (!m_pSerial) {
273 return;
274 }
275 m_parser.append(m_pSerial->readAll());
276 drainParser();
277}
278
279void PolhemusConnection::onSerialError(QSerialPort::SerialPortError err)
280{
281 if (err == QSerialPort::NoError) {
282 return;
283 }
284 if (!m_pSerial) {
285 return;
286 }
287 const QString msg = QStringLiteral("Serial error: %1").arg(m_pSerial->errorString());
288 emit errorOccurred(msg);
289
290 if (err == QSerialPort::ResourceError || err == QSerialPort::DeviceNotFoundError) {
291 close();
292 }
293}
294
295void PolhemusConnection::drainParser()
296{
297 FastrakSample s;
298 while (m_parser.nextSample(s)) {
299 m_lastSamples[s.station] = {s.position, s.orientation};
301 }
302 if (m_pSerial) {
303 m_streamPauseTimer.start();
304 }
305}
306
307void PolhemusConnection::onStreamPauseTimeout()
308{
309 if (!m_pSerial || !m_isConnected) return;
310
311 for (auto it = m_lastSamples.constBegin(); it != m_lastSamples.constEnd(); ++it) {
312 emit penButtonPressed(it.key(), it.value().position, it.value().orientation);
313 }
314
315 m_pSerial->write(QByteArrayLiteral("C\r"));
316}
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.