INSTINCT Code Coverage Report


Directory: src/
File: Nodes/DataProcessor/WiFi/WiFiPositioning.cpp
Date: 2025-11-25 23:34:18
Exec Total Coverage
Lines: 18 540 3.3%
Functions: 5 14 35.7%
Branches: 28 1256 2.2%

Line Branch Exec Source
1 // This file is part of INSTINCT, the INS Toolkit for Integrated
2 // Navigation Concepts and Training by the Institute of Navigation of
3 // the University of Stuttgart, Germany.
4 //
5 // This Source Code Form is subject to the terms of the Mozilla Public
6 // License, v. 2.0. If a copy of the MPL was not distributed with this
7 // file, You can obtain one at https://mozilla.org/MPL/2.0/.
8
9 #include "WiFiPositioning.hpp"
10
11 #include <algorithm>
12 #include <ranges>
13 #include <regex>
14 #include <fmt/ranges.h>
15
16 #include "util/Logger.hpp"
17 #include "util/Container/Vector.hpp"
18
19 #include "Navigation/Constants.hpp"
20
21 #include "internal/gui/NodeEditorApplication.hpp"
22 #include "internal/gui/widgets/HelpMarker.hpp"
23 #include "internal/gui/widgets/imgui_ex.hpp"
24 #include "internal/gui/widgets/InputWithUnit.hpp"
25
26 #include "internal/FlowManager.hpp"
27
28 #include "NodeData/WiFi/WiFiObs.hpp"
29 #include "NodeData/WiFi/WiFiPositioningSolution.hpp"
30 #include "Navigation/GNSS/Functions.hpp"
31 #include "Navigation/Transformations/CoordinateFrames.hpp"
32 #include "Navigation/Transformations/Units.hpp"
33
34 #include "Navigation/Math/LeastSquares.hpp"
35
36 114 NAV::WiFiPositioning::WiFiPositioning()
37
12/24
✓ Branch 1 taken 114 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 114 times.
✗ Branch 5 not taken.
✓ Branch 8 taken 114 times.
✗ Branch 9 not taken.
✓ Branch 11 taken 114 times.
✗ Branch 12 not taken.
✓ Branch 14 taken 114 times.
✗ Branch 15 not taken.
✓ Branch 17 taken 114 times.
✗ Branch 18 not taken.
✓ Branch 20 taken 114 times.
✗ Branch 21 not taken.
✓ Branch 23 taken 114 times.
✗ Branch 24 not taken.
✓ Branch 26 taken 114 times.
✗ Branch 27 not taken.
✓ Branch 29 taken 114 times.
✗ Branch 30 not taken.
✓ Branch 33 taken 114 times.
✗ Branch 34 not taken.
✓ Branch 36 taken 114 times.
✗ Branch 37 not taken.
1254 : Node(typeStatic())
38 {
39 LOG_TRACE("{}: called", name);
40
41 114 _hasConfig = true;
42 114 _guiConfigDefaultWindowSize = { 600, 500 };
43
44
1/2
✓ Branch 1 taken 114 times.
✗ Branch 2 not taken.
114 updateNumberOfInputPins();
45
46
5/10
✓ Branch 2 taken 114 times.
✗ Branch 3 not taken.
✓ Branch 6 taken 114 times.
✗ Branch 7 not taken.
✓ Branch 10 taken 114 times.
✗ Branch 11 not taken.
✓ Branch 14 taken 114 times.
✓ Branch 15 taken 114 times.
✗ Branch 19 not taken.
✗ Branch 20 not taken.
456 CreateOutputPin(NAV::WiFiPositioningSolution::type().c_str(), Pin::Type::Flow, { NAV::WiFiPositioningSolution::type() });
47 228 }
48
49 228 NAV::WiFiPositioning::~WiFiPositioning()
50 {
51 LOG_TRACE("{}: called", nameId());
52 228 }
53
54 228 std::string NAV::WiFiPositioning::typeStatic()
55 {
56
1/2
✓ Branch 1 taken 228 times.
✗ Branch 2 not taken.
456 return "WiFiPositioning";
57 }
58
59 ✗ std::string NAV::WiFiPositioning::type() const
60 {
61 ✗ return typeStatic();
62 }
63
64 114 std::string NAV::WiFiPositioning::category()
65 {
66
1/2
✓ Branch 1 taken 114 times.
✗ Branch 2 not taken.
228 return "Data Processor";
67 }
68
69 ✗ void NAV::WiFiPositioning::guiConfig()
70 {
71 ✗ float columnWidth = 140.0F * gui::NodeEditorApplication::windowFontRatio();
72 ✗ float configWidth = 380.0F * gui::NodeEditorApplication::windowFontRatio();
73 ✗ float unitWidth = 150.0F * gui::NodeEditorApplication::windowFontRatio();
74
75 ✗ if (ImGui::Button(fmt::format("Add Input Pin##{}", size_t(id)).c_str()))
76 {
77 ✗ _nWifiInputPins++;
78 ✗ LOG_DEBUG("{}: # Input Pins changed to {}", nameId(), _nWifiInputPins);
79 ✗ flow::ApplyChanges();
80 ✗ updateNumberOfInputPins();
81 }
82 ✗ ImGui::SameLine();
83 ✗ if (ImGui::Button(fmt::format("Delete Input Pin##{}", size_t(id)).c_str()))
84 {
85 ✗ _nWifiInputPins--;
86 ✗ LOG_DEBUG("{}: # Input Pins changed to {}", nameId(), _nWifiInputPins);
87 ✗ flow::ApplyChanges();
88 ✗ updateNumberOfInputPins();
89 }
90
91 ✗ ImGui::SetNextItemWidth(250 * gui::NodeEditorApplication::windowFontRatio());
92
93 // ###########################################################################################################
94 // Frames
95 // ###########################################################################################################
96
97 ✗ if (auto frame = static_cast<int>(_frame);
98 ✗ ImGui::Combo(fmt::format("Frame##{}", size_t(id)).c_str(), &frame, "ECEF\0LLA\0\0"))
99 {
100 ✗ _frame = static_cast<decltype(_frame)>(frame);
101 ✗ switch (_frame)
102 {
103 ✗ case Frame::ECEF:
104 ✗ LOG_DEBUG("{}: Frame changed to ECEF", nameId());
105 ✗ for (auto& devPos : _devicePositions)
106 {
107 ✗ devPos = trafo::lla2ecef_WGS84(Eigen::Vector3d(deg2rad(devPos.x()), deg2rad(devPos.y()), devPos.z()));
108 }
109 ✗ break;
110 ✗ case Frame::LLA:
111 ✗ LOG_DEBUG("{}: Frame changed to LLA", nameId());
112 ✗ for (auto& devPos : _devicePositions)
113 {
114 ✗ devPos = trafo::ecef2lla_WGS84(devPos);
115 ✗ devPos.x() = rad2deg(devPos.x());
116 ✗ devPos.y() = rad2deg(devPos.y());
117 }
118 ✗ break;
119 }
120 ✗ flow::ApplyChanges();
121 }
122
123 ✗ if (ImGui::BeginTable("AccessPointInput", 6, ImGuiTableFlags_Borders | ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_NoHostExtendX, ImVec2(0.0F, 0.0F)))
124 {
125 // Column headers
126 ✗ ImGui::TableSetupColumn("MAC address", ImGuiTableColumnFlags_WidthFixed, columnWidth);
127 ✗ if (_frame == Frame::ECEF)
128 {
129 ✗ ImGui::TableSetupColumn("X", ImGuiTableColumnFlags_WidthFixed, columnWidth);
130 ✗ ImGui::TableSetupColumn("Y", ImGuiTableColumnFlags_WidthFixed, columnWidth);
131 ✗ ImGui::TableSetupColumn("Z", ImGuiTableColumnFlags_WidthFixed, columnWidth);
132 }
133 ✗ else if (_frame == Frame::LLA)
134 {
135 ✗ ImGui::TableSetupColumn("Latitude", ImGuiTableColumnFlags_WidthFixed, columnWidth);
136 ✗ ImGui::TableSetupColumn("Longitude", ImGuiTableColumnFlags_WidthFixed, columnWidth);
137 ✗ ImGui::TableSetupColumn("Altitude", ImGuiTableColumnFlags_WidthFixed, columnWidth);
138 }
139 ✗ ImGui::TableSetupColumn("Bias", ImGuiTableColumnFlags_WidthFixed, columnWidth);
140 ✗ ImGui::TableSetupColumn("Scale", ImGuiTableColumnFlags_WidthFixed, columnWidth);
141
142 // Automatic header row
143 ✗ ImGui::TableHeadersRow();
144
145 ✗ for (size_t rowIndex = 0; rowIndex < _numOfDevices; rowIndex++)
146 {
147 ✗ ImGui::TableNextRow();
148 ✗ ImGui::TableNextColumn();
149 ✗ auto* devPosRow = _devicePositions.at(rowIndex).data();
150
151 // MAC address validation
152 ✗ std::regex macRegex("^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$");
153
154 ✗ ImGui::SetNextItemWidth(columnWidth);
155 ✗ if (ImGui::InputText(fmt::format("##Mac{}", rowIndex).c_str(), &_deviceMacAddresses.at(rowIndex), ImGuiInputTextFlags_None))
156 {
157 ✗ std::transform(_deviceMacAddresses.at(rowIndex).begin(), _deviceMacAddresses.at(rowIndex).end(), _deviceMacAddresses.at(rowIndex).begin(), ::toupper); // Convert to uppercase
158 ✗ if (!std::regex_match(_deviceMacAddresses.at(rowIndex), macRegex))
159 {
160 ✗ _deviceMacAddresses.at(rowIndex) = "00:00:00:00:00:00";
161 ✗ LOG_DEBUG("{}: Invalid MAC address", nameId());
162 }
163 else
164 {
165 ✗ flow::ApplyChanges();
166 }
167 }
168 ✗ if (_frame == Frame::ECEF)
169 {
170 ✗ ImGui::TableNextColumn();
171 ✗ ImGui::SetNextItemWidth(columnWidth);
172 ✗ if (ImGui::InputDouble(fmt::format("##InputX{}", rowIndex).c_str(), &devPosRow[0], 0.0, 0.0, "%.4fm"))
173 {
174 ✗ flow::ApplyChanges();
175 }
176 ✗ ImGui::TableNextColumn();
177 ✗ ImGui::SetNextItemWidth(columnWidth);
178 ✗ if (ImGui::InputDouble(fmt::format("##InputY{}", rowIndex).c_str(), &devPosRow[1], 0.0, 0.0, "%.4fm"))
179 {
180 ✗ flow::ApplyChanges();
181 }
182 ✗ ImGui::TableNextColumn();
183 ✗ ImGui::SetNextItemWidth(columnWidth);
184 ✗ if (ImGui::InputDouble(fmt::format("##InputZ{}", rowIndex).c_str(), &devPosRow[2], 0.0, 0.0, "%.4fm"))
185 {
186 ✗ flow::ApplyChanges();
187 }
188 }
189 ✗ else if (_frame == Frame::LLA)
190 {
191 ✗ ImGui::TableNextColumn();
192 ✗ ImGui::SetNextItemWidth(columnWidth);
193 ✗ if (ImGui::InputDoubleL(fmt::format("##InputLat{}", rowIndex).c_str(), &devPosRow[0], -180, 180, 0.0, 0.0, "%.8f°"))
194 {
195 ✗ flow::ApplyChanges();
196 }
197 ✗ ImGui::TableNextColumn();
198 ✗ ImGui::SetNextItemWidth(columnWidth);
199 ✗ if (ImGui::InputDoubleL(fmt::format("##InputLon{}", rowIndex).c_str(), &devPosRow[1], -180, 180, 0.0, 0.0, "%.8f°"))
200 {
201 ✗ flow::ApplyChanges();
202 }
203 ✗ ImGui::TableNextColumn();
204 ✗ ImGui::SetNextItemWidth(columnWidth);
205 ✗ if (ImGui::InputDouble(fmt::format("##InputHeight{}", rowIndex).c_str(), &devPosRow[2], 0.0, 0.0, "%.4fm"))
206 {
207 ✗ flow::ApplyChanges();
208 }
209 }
210 ✗ ImGui::TableNextColumn();
211 ✗ ImGui::SetNextItemWidth(columnWidth);
212 ✗ if (ImGui::InputDouble(fmt::format("##InputBias{}", rowIndex).c_str(), &_deviceBias.at(rowIndex), 0.0, 0.0, "%.4fm"))
213 {
214 ✗ flow::ApplyChanges();
215 }
216 ✗ ImGui::TableNextColumn();
217 ✗ ImGui::SetNextItemWidth(columnWidth);
218 ✗ if (ImGui::InputDouble(fmt::format("##InputScale{}", rowIndex).c_str(), &_deviceScale.at(rowIndex), 0.0, 0.0, "%.4f"))
219 {
220 ✗ flow::ApplyChanges();
221 }
222 ✗ }
223 ✗ ImGui::EndTable();
224 }
225 ✗ if (ImGui::Button(fmt::format("Add Device##{}", size_t(id)).c_str(), ImVec2(columnWidth * 2.1F, 0)))
226 {
227 ✗ _numOfDevices++;
228 ✗ _deviceMacAddresses.emplace_back("00:00:00:00:00:00");
229 ✗ _devicePositions.emplace_back(Eigen::Vector3d::Zero());
230 ✗ _deviceBias.emplace_back(0.0);
231 ✗ _deviceScale.emplace_back(0.0);
232 ✗ flow::ApplyChanges();
233 }
234 ✗ ImGui::SameLine();
235 ✗ if (ImGui::Button(fmt::format("Delete Device##{}", size_t(id)).c_str(), ImVec2(columnWidth * 2.1F, 0)))
236 {
237 ✗ if (_numOfDevices > 0)
238 {
239 ✗ _numOfDevices--;
240 ✗ _deviceMacAddresses.pop_back();
241 ✗ _devicePositions.pop_back();
242 ✗ _deviceBias.pop_back();
243 ✗ _deviceScale.pop_back();
244 ✗ flow::ApplyChanges();
245 }
246 }
247 ✗ ImGui::Separator();
248 ✗ if (auto solutionMode = static_cast<int>(_solutionMode);
249 ✗ ImGui::Combo(fmt::format("Solution##{}", size_t(id)).c_str(), &solutionMode, "Least squares\0Kalman Filter\0\0"))
250 {
251 ✗ _solutionMode = static_cast<decltype(_solutionMode)>(solutionMode);
252 ✗ switch (_solutionMode)
253 {
254 ✗ case SolutionMode::LSQ:
255 ✗ LOG_DEBUG("{}: Solution changed to Least squares 3D", nameId());
256 ✗ break;
257 ✗ case SolutionMode::KF:
258 ✗ LOG_DEBUG("{}: Solution changed to Kalman Filter", nameId());
259 ✗ break;
260 }
261 ✗ flow::ApplyChanges();
262 }
263
264 // ###########################################################################################################
265 // Least Squares
266 // ###########################################################################################################
267 ✗ if (_solutionMode == SolutionMode::LSQ)
268 {
269 ✗ ImGui::SetNextItemOpen(true, ImGuiCond_FirstUseEver);
270 ✗ if (ImGui::TreeNode(fmt::format("x0 - Initial State##{}", size_t(id)).c_str()))
271 {
272 ✗ Eigen::Vector3d llaPos = trafo::ecef2lla_WGS84(_initialState.e_position);
273 ✗ llaPos.block<2, 1>(0, 0) = rad2deg(llaPos.block<2, 1>(0, 0));
274
275 ✗ ImGui::SetNextItemWidth(configWidth);
276 ✗ if (ImGui::InputDouble3(fmt::format("Position ECEF (m)##{}", "m",
277 ✗ size_t(id))
278 .c_str(),
279 _initialState.e_position.data(), "%.4f", ImGuiInputTextFlags_CharsScientific))
280 {
281 ✗ LOG_DEBUG("{}: e_position changed to {}", nameId(), _initialState.e_position);
282 ✗ flow::ApplyChanges();
283 }
284
285 ✗ ImGui::SetNextItemWidth(configWidth);
286 ✗ if (ImGui::InputDouble3(fmt::format("Position LLA (°,°,m)##{}", "(°,°,m)",
287 ✗ size_t(id))
288 .c_str(),
289 llaPos.data(), "%.8f", ImGuiInputTextFlags_CharsScientific))
290 {
291 ✗ llaPos.block<2, 1>(0, 0) = deg2rad(llaPos.block<2, 1>(0, 0));
292 ✗ _initialState.e_position = trafo::lla2ecef_WGS84(llaPos);
293 ✗ LOG_DEBUG("{}: e_position changed to {}", nameId(), _initialState.e_position);
294 ✗ flow::ApplyChanges();
295 }
296
297 ✗ if (_estimateBias)
298 {
299 ✗ ImGui::SetNextItemWidth(configWidth);
300 ✗ if (ImGui::InputDouble(fmt::format("Bias (m)##{}", "m",
301 ✗ size_t(id))
302 .c_str(),
303 &_initialState.bias, 0, 0, "%.3e", ImGuiInputTextFlags_CharsScientific))
304 {
305 ✗ LOG_DEBUG("{}: bias changed to {}", nameId(), _initialState.bias);
306 ✗ flow::ApplyChanges();
307 }
308 }
309
310 ✗ ImGui::TreePop();
311 }
312 }
313 // ###########################################################################################################
314 // Kalman Filter
315 // ###########################################################################################################
316 ✗ if (_solutionMode == SolutionMode::KF)
317 {
318 // ###########################################################################################################
319 // Measurement Uncertainties 𝐑
320 // ###########################################################################################################
321
322 ✗ ImGui::SetNextItemOpen(true, ImGuiCond_FirstUseEver);
323 ✗ if (ImGui::TreeNode(fmt::format("R - Measurement Noise ##{}", size_t(id)).c_str()))
324 {
325 ✗ if (gui::widgets::InputDoubleWithUnit(fmt::format("({})##{}",
326 ✗ _measurementNoiseUnit == MeasurementNoiseUnit::meter2
327 ✗ ? "Variance σ²"
328 : "Standard deviation σ",
329 ✗ size_t(id))
330 .c_str(),
331 ✗ configWidth, unitWidth, &_measurementNoise, _measurementNoiseUnit, "m^2, m^2, m^2\0"
332 "m, m, m\0\0",
333 0, 0, "%.3e", ImGuiInputTextFlags_CharsScientific))
334 {
335 ✗ LOG_DEBUG("{}: measurementNoise changed to {}", nameId(), _measurementNoise);
336 ✗ LOG_DEBUG("{}: measurementNoiseUnit changed to {}", nameId(), fmt::underlying(_measurementNoiseUnit));
337 ✗ flow::ApplyChanges();
338 }
339 ✗ ImGui::TreePop();
340 }
341
342 // ###########################################################################################################
343 // Process Noise Covariance 𝐐
344 // ###########################################################################################################
345 ✗ ImGui::SetNextItemOpen(true, ImGuiCond_FirstUseEver);
346 ✗ if (ImGui::TreeNode(fmt::format("Q - Process Noise ##{}", size_t(id)).c_str()))
347 {
348 ✗ if (gui::widgets::InputDoubleWithUnit(fmt::format("({})##{}",
349 ✗ _processNoiseUnit == ProcessNoiseUnit::meter2
350 ✗ ? "Variance σ²"
351 : "Standard deviation σ",
352 ✗ size_t(id))
353 .c_str(),
354 ✗ configWidth, unitWidth, &_processNoise, _processNoiseUnit, "m^2, m^2, m^2\0"
355 "m, m, m\0\0",
356 0, 0, "%.3e", ImGuiInputTextFlags_CharsScientific))
357 {
358 ✗ LOG_DEBUG("{}: processNoise changed to {}", nameId(), _processNoise);
359 ✗ LOG_DEBUG("{}: processNoiseUnit changed to {}", nameId(), fmt::underlying(_processNoiseUnit));
360 ✗ flow::ApplyChanges();
361 }
362 ✗ ImGui::TreePop();
363 }
364
365 // ###########################################################################################################
366 // Initial State Estimate 𝐱0
367 // ###########################################################################################################
368 ✗ ImGui::SetNextItemOpen(true, ImGuiCond_FirstUseEver);
369 ✗ if (ImGui::TreeNode(fmt::format("x0 - Initial State##{}", size_t(id)).c_str()))
370 {
371 ✗ Eigen::Vector3d llaPos = trafo::ecef2lla_WGS84(_initialState.e_position);
372 ✗ llaPos.block<2, 1>(0, 0) = rad2deg(llaPos.block<2, 1>(0, 0));
373
374 ✗ ImGui::SetNextItemWidth(configWidth);
375 ✗ if (ImGui::InputDouble3(fmt::format("Position ECEF (m)##{}", "m",
376 ✗ size_t(id))
377 .c_str(),
378 _initialState.e_position.data(), "%.4f", ImGuiInputTextFlags_CharsScientific))
379 {
380 ✗ LOG_DEBUG("{}: e_position changed to {}", nameId(), _initialState.e_position);
381 ✗ flow::ApplyChanges();
382 }
383
384 ✗ ImGui::SetNextItemWidth(configWidth);
385 ✗ if (ImGui::InputDouble3(fmt::format("Position LLA ##{}", "m",
386 ✗ size_t(id))
387 .c_str(),
388 llaPos.data(), "%.8f°", ImGuiInputTextFlags_CharsScientific))
389 {
390 ✗ llaPos.block<2, 1>(0, 0) = deg2rad(llaPos.block<2, 1>(0, 0));
391 ✗ _initialState.e_position = trafo::lla2ecef_WGS84(llaPos);
392 ✗ LOG_DEBUG("{}: e_position changed to {}", nameId(), _initialState.e_position);
393 ✗ flow::ApplyChanges();
394 }
395
396 ✗ ImGui::SetNextItemWidth(configWidth);
397 ✗ if (ImGui::InputDouble3(fmt::format("Velocity (m/s)##{}", "m",
398 ✗ size_t(id))
399 .c_str(),
400 _state.e_velocity.data(), "%.3e", ImGuiInputTextFlags_CharsScientific))
401 {
402 ✗ LOG_DEBUG("{}: e_position changed to {}", nameId(), _state.e_velocity);
403 ✗ flow::ApplyChanges();
404 }
405
406 ✗ if (_estimateBias)
407 {
408 ✗ ImGui::SetNextItemWidth(configWidth);
409 ✗ if (ImGui::InputDouble(fmt::format("Bias (m)##{}", "m",
410 ✗ size_t(id))
411 .c_str(),
412 &_state.bias, 0, 0, "%.3e", ImGuiInputTextFlags_CharsScientific))
413 {
414 ✗ LOG_DEBUG("{}: bias changed to {}", nameId(), _state.bias);
415 ✗ flow::ApplyChanges();
416 }
417 }
418
419 ✗ ImGui::TreePop();
420 }
421
422 // ###########################################################################################################
423 // 𝐏 Error covariance matrix
424 // ###########################################################################################################
425
426 ✗ ImGui::SetNextItemOpen(true, ImGuiCond_FirstUseEver);
427 ✗ if (ImGui::TreeNode(fmt::format("P - Error covariance matrix (init)##{}", size_t(id)).c_str()))
428 {
429 ✗ if (gui::widgets::InputDouble3WithUnit(fmt::format("Position covariance ({})##{}",
430 ✗ _initCovariancePositionUnit == InitCovariancePositionUnit::meter2
431 ✗ ? "Variance σ²"
432 : "Standard deviation σ",
433 ✗ size_t(id))
434 .c_str(),
435 ✗ configWidth, unitWidth, _initCovariancePosition.data(), _initCovariancePositionUnit, "m^2, m^2, m^2\0"
436 "m, m, m\0\0",
437 "%.2e", ImGuiInputTextFlags_CharsScientific))
438 {
439 ✗ LOG_DEBUG("{}: initCovariancePosition changed to {}", nameId(), _initCovariancePosition);
440 ✗ LOG_DEBUG("{}: initCovariancePositionUnit changed to {}", nameId(), fmt::underlying(_initCovariancePositionUnit));
441 ✗ flow::ApplyChanges();
442 }
443
444 ✗ if (gui::widgets::InputDouble3WithUnit(fmt::format("Velocity covariance ({})##{}",
445 ✗ _initCovarianceVelocityUnit == InitCovarianceVelocityUnit::m2_s2
446 ✗ ? "Variance σ²"
447 : "Standard deviation σ",
448 ✗ size_t(id))
449 .c_str(),
450 ✗ configWidth, unitWidth, _initCovarianceVelocity.data(), _initCovarianceVelocityUnit, "m^2/s^2\0"
451 "m/s\0\0",
452 "%.2e", ImGuiInputTextFlags_CharsScientific))
453 {
454 ✗ LOG_DEBUG("{}: initCovarianceVelocity changed to {}", nameId(), _initCovarianceVelocity);
455 ✗ LOG_DEBUG("{}: initCovarianceVelocityUnit changed to {}", nameId(), fmt::underlying(_initCovarianceVelocityUnit));
456 ✗ flow::ApplyChanges();
457 }
458
459 ✗ if (_estimateBias)
460 {
461 ✗ if (gui::widgets::InputDoubleWithUnit(fmt::format("Bias covariance ({})##{}",
462 ✗ _initCovarianceBiasUnit == InitCovarianceBiasUnit::meter2
463 ✗ ? "Variance σ²"
464 : "Standard deviation σ",
465 ✗ size_t(id))
466 .c_str(),
467 ✗ configWidth, unitWidth, &_initCovarianceBias, _initCovarianceBiasUnit, "m^2\0"
468 "m\0\0",
469 0, 0, "%.2e", ImGuiInputTextFlags_CharsScientific))
470 {
471 ✗ LOG_DEBUG("{}: initCovarianceBias changed to {}", nameId(), _initCovarianceBias);
472 ✗ LOG_DEBUG("{}: initCovarianceBiasUnit changed to {}", nameId(), fmt::underlying(_initCovarianceBiasUnit));
473 ✗ flow::ApplyChanges();
474 }
475 }
476
477 ✗ ImGui::TreePop();
478 }
479 }
480 ✗ ImGui::Separator();
481 // ###########################################################################################################
482 // Estimate Bias
483 // ###########################################################################################################
484 ✗ if (ImGui::Checkbox(fmt::format("Estimate Bias##{}", size_t(id)).c_str(), &_estimateBias))
485 {
486 ✗ if (_estimateBias)
487 {
488 ✗ LOG_DEBUG("{}: Estimate Bias changed to Yes", nameId());
489 ✗ _numStates = 7;
490 }
491 else
492 {
493 ✗ LOG_DEBUG("{}: Estimate Bias changed to No", nameId());
494 ✗ _numStates = 6;
495 }
496 ✗ flow::ApplyChanges();
497 }
498 // ###########################################################################################################
499 // Weighted Solution
500 // ###########################################################################################################
501 ✗ if (ImGui::Checkbox(fmt::format("Weighted Solution##{}", size_t(id)).c_str(), &_weightedSolution))
502 {
503 ✗ if (_weightedSolution)
504 {
505 ✗ LOG_DEBUG("{}: Weighted Solution changed to Yes", nameId());
506 }
507 else
508 {
509 ✗ LOG_DEBUG("{}: Weighted Solution changed to No", nameId());
510 }
511 ✗ flow::ApplyChanges();
512 }
513
514 // ###########################################################################################################
515 // Use Initial Values
516 // ###########################################################################################################
517 ✗ if (_solutionMode == SolutionMode::LSQ)
518 {
519 ✗ if (ImGui::Checkbox(fmt::format("Use Initial Values##{}", size_t(id)).c_str(), &_useInitialValues))
520 {
521 ✗ if (_useInitialValues)
522 {
523 ✗ LOG_DEBUG("{}: Use Initial Values changed to Yes", nameId());
524 }
525 else
526 {
527 ✗ LOG_DEBUG("{}: Use Initial Values changed to No", nameId());
528 }
529 ✗ flow::ApplyChanges();
530 }
531 }
532 ✗ }
533
534 ✗ [[nodiscard]] json NAV::WiFiPositioning::save() const
535 {
536 LOG_TRACE("{}: called", nameId());
537
538 ✗ json j;
539
540 ✗ j["nWifiInputPins"] = _nWifiInputPins;
541 ✗ j["numStates"] = _numStates;
542 ✗ j["numMeasurements"] = _numMeasurements;
543 ✗ j["frame"] = _frame;
544 ✗ j["estimateBias"] = _estimateBias;
545 ✗ j["weightedSolution"] = _weightedSolution;
546 ✗ j["useInitialValues"] = _useInitialValues;
547 ✗ j["deviceMacAddresses"] = _deviceMacAddresses;
548 ✗ j["devicePositions"] = _devicePositions;
549 ✗ j["deviceBias"] = _deviceBias;
550 ✗ j["deviceScale"] = _deviceScale;
551 ✗ j["numOfDevices"] = _numOfDevices;
552 ✗ j["solutionMode"] = _solutionMode;
553 ✗ j["e_position"] = _state.e_position;
554 ✗ j["e_velocity"] = _state.e_velocity;
555 ✗ j["bias"] = _state.bias;
556 ✗ j["intialStatePosition"] = _initialState.e_position;
557 ✗ j["initialStateVelocity"] = _initialState.e_velocity;
558 ✗ j["initialStateBias"] = _initialState.bias;
559 ✗ j["initCovariancePosition"] = _initCovariancePosition;
560 ✗ j["initCovariancePositionUnit"] = _initCovariancePositionUnit;
561 ✗ j["initCovarianceVelocity"] = _initCovarianceVelocity;
562 ✗ j["initCovarianceVelocityUnit"] = _initCovarianceVelocityUnit;
563 ✗ j["initCovarianceBias"] = _initCovarianceBias;
564 ✗ j["initCovarianceBiasUnit"] = _initCovarianceBiasUnit;
565 ✗ j["measurementNoise"] = _measurementNoise;
566 ✗ j["measurementNoiseUnit"] = _measurementNoiseUnit;
567 ✗ j["processNoise"] = _processNoise;
568 ✗ j["processNoiseUnit"] = _processNoiseUnit;
569
570 ✗ return j;
571 ✗ }
572
573 ✗ void NAV::WiFiPositioning::restore(json const& j)
574 {
575 LOG_TRACE("{}: called", nameId());
576
577 ✗ if (j.contains("nWifiInputPins"))
578 {
579 ✗ j.at("nWifiInputPins").get_to(_nWifiInputPins);
580 ✗ updateNumberOfInputPins();
581 }
582 ✗ if (j.contains("numStates"))
583 {
584 ✗ j.at("numStates").get_to(_numStates);
585 }
586 ✗ if (j.contains("numMeasurements"))
587 {
588 ✗ j.at("numMeasurements").get_to(_numMeasurements);
589 }
590 ✗ if (j.contains("frame"))
591 {
592 ✗ j.at("frame").get_to(_frame);
593 }
594 ✗ if (j.contains("estimateBias"))
595 {
596 ✗ j.at("estimateBias").get_to(_estimateBias);
597 }
598 ✗ if (j.contains("weightedSolution"))
599 {
600 ✗ j.at("weightedSolution").get_to(_weightedSolution);
601 }
602 ✗ if (j.contains("useInitialValues"))
603 {
604 ✗ j.at("useInitialValues").get_to(_useInitialValues);
605 }
606 ✗ if (j.contains("deviceMacAddresses"))
607 {
608 ✗ j.at("deviceMacAddresses").get_to(_deviceMacAddresses);
609 }
610 ✗ if (j.contains("devicePositions"))
611 {
612 ✗ j.at("devicePositions").get_to(_devicePositions);
613 }
614 ✗ if (j.contains("deviceBias"))
615 {
616 ✗ j.at("deviceBias").get_to(_deviceBias);
617 }
618 ✗ if (j.contains("deviceScale"))
619 {
620 ✗ j.at("deviceScale").get_to(_deviceScale);
621 }
622 ✗ if (j.contains("numOfDevices"))
623 {
624 ✗ j.at("numOfDevices").get_to(_numOfDevices);
625 }
626 ✗ if (j.contains("solutionMode"))
627 {
628 ✗ j.at("solutionMode").get_to(_solutionMode);
629 }
630 ✗ if (j.contains("e_position"))
631 {
632 ✗ j.at("e_position").get_to(_state.e_position);
633 }
634 ✗ if (j.contains("e_velocity"))
635 {
636 ✗ j.at("e_velocity").get_to(_state.e_velocity);
637 }
638 ✗ if (j.contains("bias"))
639 {
640 ✗ j.at("bias").get_to(_state.bias);
641 }
642 ✗ if (j.contains("intialStatePosition"))
643 {
644 ✗ j.at("intialStatePosition").get_to(_initialState.e_position);
645 }
646 ✗ if (j.contains("initialStateVelocity"))
647 {
648 ✗ j.at("initialStateVelocity").get_to(_initialState.e_velocity);
649 }
650 ✗ if (j.contains("initialStateBias"))
651 {
652 ✗ j.at("initialStateBias").get_to(_initialState.bias);
653 }
654 ✗ if (j.contains("initCovariancePosition"))
655 {
656 ✗ j.at("initCovariancePosition").get_to(_initCovariancePosition);
657 }
658 ✗ if (j.contains("initCovariancePositionUnit"))
659 {
660 ✗ j.at("initCovariancePositionUnit").get_to(_initCovariancePositionUnit);
661 }
662 ✗ if (j.contains("initCovarianceVelocity"))
663 {
664 ✗ j.at("initCovarianceVelocity").get_to(_initCovarianceVelocity);
665 }
666 ✗ if (j.contains("initCovarianceVelocityUnit"))
667 {
668 ✗ j.at("initCovarianceVelocityUnit").get_to(_initCovarianceVelocityUnit);
669 }
670 ✗ if (j.contains("initCovarianceBias"))
671 {
672 ✗ j.at("initCovarianceBias").get_to(_initCovarianceBias);
673 }
674 ✗ if (j.contains("initCovarianceBiasUnit"))
675 {
676 ✗ j.at("initCovarianceBiasUnit").get_to(_initCovarianceBiasUnit);
677 }
678 ✗ if (j.contains("measurementNoise"))
679 {
680 ✗ j.at("measurementNoise").get_to(_measurementNoise);
681 }
682 ✗ if (j.contains("measurementNoiseUnit"))
683 {
684 ✗ j.at("measurementNoiseUnit").get_to(_measurementNoiseUnit);
685 }
686 ✗ if (j.contains("processNoise"))
687 {
688 ✗ j.at("processNoise").get_to(_processNoise);
689 }
690 ✗ if (j.contains("processNoiseUnit"))
691 {
692 ✗ j.at("processNoiseUnit").get_to(_processNoiseUnit);
693 }
694 ✗ }
695
696 ✗ bool NAV::WiFiPositioning::initialize()
697 {
698 LOG_TRACE("{}: called", nameId());
699
700 ✗ _kalmanFilter = KalmanFilter{ _numStates, _numMeasurements };
701
702 // Initial state
703 ✗ _state.e_position = _initialState.e_position;
704 ✗ _state.e_velocity = _initialState.e_velocity;
705 ✗ if (_estimateBias)
706 {
707 ✗ _state.bias = _initialState.bias;
708 }
709
710 // Initial Covariance of the velocity in [m²/s²]
711 ✗ Eigen::Vector3d variance_vel = Eigen::Vector3d::Zero();
712 ✗ if (_initCovarianceVelocityUnit == InitCovarianceVelocityUnit::m2_s2)
713 {
714 ✗ variance_vel = _initCovarianceVelocity;
715 }
716 ✗ else if (_initCovarianceVelocityUnit == InitCovarianceVelocityUnit::m_s)
717 {
718 ✗ variance_vel = _initCovarianceVelocity.array().pow(2);
719 }
720
721 // Initial Covariance of the position in [m²]
722 ✗ Eigen::Vector3d variance_pos = Eigen::Vector3d::Zero();
723 ✗ if (_initCovariancePositionUnit == InitCovariancePositionUnit::meter2)
724 {
725 ✗ variance_pos = _initCovariancePosition;
726 }
727 ✗ else if (_initCovariancePositionUnit == InitCovariancePositionUnit::meter)
728 {
729 ✗ variance_pos = _initCovariancePosition.array().pow(2);
730 }
731
732 // Initial Covariance of the bias in [m²]
733 ✗ double variance_bias = 0.0;
734 ✗ if (_initCovarianceBiasUnit == InitCovarianceBiasUnit::meter2)
735 {
736 ✗ variance_bias = _initCovarianceBias;
737 }
738 ✗ else if (_initCovarianceBiasUnit == InitCovarianceBiasUnit::meter)
739 {
740 ✗ variance_bias = std::pow(_initCovarianceBias, 2);
741 }
742 ✗ if (_estimateBias)
743 {
744 ✗ _kalmanFilter.P.diagonal() << variance_pos, variance_vel, variance_bias;
745 }
746 else
747 {
748 ✗ _kalmanFilter.P.diagonal() << variance_pos, variance_vel;
749 }
750 ✗ if (_estimateBias)
751 {
752 ✗ _kalmanFilter.x << _state.e_position, _state.e_velocity, _state.bias;
753 }
754 else
755 {
756 ✗ _kalmanFilter.x << _state.e_position, _state.e_velocity;
757 }
758 ✗ if (_measurementNoiseUnit == MeasurementNoiseUnit::meter2)
759 {
760 ✗ _kalmanFilter.R << _measurementNoise;
761 }
762 ✗ else if (_measurementNoiseUnit == MeasurementNoiseUnit::meter)
763 {
764 ✗ _kalmanFilter.R << std::pow(_measurementNoise, 2);
765 }
766
767 ✗ LOG_DEBUG("WiFiPositioning initialized");
768
769 ✗ return true;
770 }
771
772 ✗ void NAV::WiFiPositioning::deinitialize()
773 {
774 LOG_TRACE("{}: called", nameId());
775 ✗ }
776
777 114 void NAV::WiFiPositioning::updateNumberOfInputPins()
778 {
779
2/2
✓ Branch 1 taken 114 times.
✓ Branch 2 taken 114 times.
228 while (inputPins.size() < _nWifiInputPins)
780 {
781
5/10
✓ Branch 1 taken 114 times.
✗ Branch 2 not taken.
✓ Branch 5 taken 114 times.
✗ Branch 6 not taken.
✓ Branch 9 taken 114 times.
✗ Branch 10 not taken.
✓ Branch 13 taken 114 times.
✓ Branch 14 taken 114 times.
✗ Branch 18 not taken.
✗ Branch 19 not taken.
342 CreateInputPin(NAV::WiFiObs::type().c_str(), Pin::Type::Flow, { NAV::WiFiObs::type() }, &WiFiPositioning::recvWiFiObs);
782 }
783
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 114 times.
114 while (inputPins.size() > _nWifiInputPins)
784 {
785 ✗ DeleteInputPin(inputPins.size() - 1);
786 }
787 228 }
788
789 ✗ void NAV::WiFiPositioning::recvWiFiObs(NAV::InputPin::NodeDataQueue& queue, size_t /* pinIdx */)
790 {
791 ✗ auto obs = std::static_pointer_cast<const WiFiObs>(queue.extract_front());
792 ✗ auto it = std::ranges::find(_deviceMacAddresses, obs->macAddress);
793 ✗ if (it != _deviceMacAddresses.end()) // Check if the MAC address is in the list
794 {
795 // Get the index of the found element
796 ✗ auto index = static_cast<size_t>(std::distance(_deviceMacAddresses.begin(), it));
797
798 // Check if a device with the same position already exists and update the distance
799 ✗ bool deviceExists = false;
800 ✗ for (auto& device : _devices)
801 {
802 ✗ if (_frame == Frame::ECEF)
803 {
804 ✗ if (device.position == _devicePositions.at(index))
805 {
806 ✗ deviceExists = true;
807 ✗ device.distance = obs->distance * _deviceScale.at(index) + _deviceBias.at(index);
808 ✗ device.distanceStd = obs->distanceStd * _deviceScale.at(index);
809 ✗ device.time = obs->insTime;
810 ✗ break;
811 }
812 }
813 ✗ else if (_frame == Frame::LLA)
814 {
815 ✗ Eigen::Vector3d ecefPos = _devicePositions.at(index);
816 ✗ ecefPos.block<2, 1>(0, 0) = deg2rad(ecefPos.block<2, 1>(0, 0));
817 ✗ ecefPos = trafo::lla2ecef_WGS84(ecefPos);
818 ✗ if (device.position == ecefPos)
819 {
820 ✗ deviceExists = true;
821 ✗ device.distance = obs->distance * _deviceScale.at(index) + _deviceBias.at(index);
822 ✗ device.distanceStd = obs->distanceStd * _deviceScale.at(index);
823 ✗ device.time = obs->insTime;
824 ✗ break;
825 }
826 }
827 }
828
829 // If the device does not exist, add it to the list
830 ✗ if (!deviceExists)
831 {
832 ✗ if (_frame == Frame::LLA)
833 {
834 ✗ Eigen::Vector3d llaPos = _devicePositions.at(index);
835 ✗ llaPos.block<2, 1>(0, 0) = deg2rad(llaPos.block<2, 1>(0, 0));
836 ✗ _devices.push_back({ trafo::lla2ecef_WGS84(llaPos), obs->insTime, obs->distance * _deviceScale.at(index) + _deviceBias.at(index), obs->distanceStd * _deviceScale.at(index) });
837 }
838 ✗ else if (_frame == Frame::ECEF)
839 {
840 ✗ _devices.push_back({ _devicePositions.at(index), obs->insTime, obs->distance * _deviceScale.at(index) + _deviceBias.at(index), obs->distanceStd * _deviceScale.at(index) });
841 }
842 }
843
844 // Calculate the solution
845 ✗ auto wifiPositioningSolution = std::make_shared<NAV::WiFiPositioningSolution>();
846 ✗ wifiPositioningSolution->insTime = obs->insTime;
847 // Least Squares
848 ✗ if (_solutionMode == SolutionMode::LSQ)
849 {
850 ✗ if (_devices.size() == _numOfDevices)
851 {
852 ✗ LeastSquaresResult<Eigen::VectorXd, Eigen::MatrixXd> lsqSolution = WiFiPositioning::lsqSolution();
853 ✗ wifiPositioningSolution->setPositionAndCov_e(lsqSolution.solution.block<3, 1>(0, 0),
854 ✗ lsqSolution.variance.block<3, 3>(0, 0));
855 ✗ if (wifiPositioningSolution->lla_position().hasNaN())
856 {
857 ✗ LOG_WARN("{}: WiFi LSQ calculation failed, invalid position", nameId());
858 ✗ return;
859 }
860 ✗ if (_estimateBias)
861 {
862 ✗ wifiPositioningSolution->bias = _state.bias;
863 ✗ wifiPositioningSolution->biasStdev = lsqSolution.variance(3, 3);
864 }
865 ✗ invokeCallbacks(OUTPUT_PORT_INDEX_WIFISOL, wifiPositioningSolution);
866 ✗ }
867 }
868 // Kalman Filter
869 ✗ else if (_solutionMode == SolutionMode::KF)
870 {
871 ✗ WiFiPositioning::kfSolution();
872 ✗ wifiPositioningSolution->setPosVelAndCov_e(_kalmanFilter.x.block<3, 1>(0, 0),
873 ✗ _kalmanFilter.x.block<3, 1>(3, 0),
874 ✗ _kalmanFilter.P.block<6, 6>(0, 0));
875 ✗ if (wifiPositioningSolution->lla_position().hasNaN())
876 {
877 ✗ LOG_WARN("{}: WiFi KF calculation failed, invalid position", nameId());
878 ✗ return;
879 }
880 ✗ if (_estimateBias)
881 {
882 ✗ wifiPositioningSolution->bias = _kalmanFilter.x(6);
883 ✗ wifiPositioningSolution->biasStdev = _kalmanFilter.P(6, 6);
884 }
885 ✗ invokeCallbacks(OUTPUT_PORT_INDEX_WIFISOL, wifiPositioningSolution);
886 }
887
888 LOG_DATA("{}: [{}] Received distance to device {} at position {} with distance {}", nameId(), obs->insTime.toYMDHMS(GPST),
889 obs->macAddress, _devicePositions.at(index).transpose(), obs->distance);
890 ✗ }
891 ✗ }
892
893 ✗ NAV::LeastSquaresResult<Eigen::VectorXd, Eigen::MatrixXd> NAV::WiFiPositioning::lsqSolution()
894 {
895 ✗ LeastSquaresResult<Eigen::VectorXd, Eigen::MatrixXd> lsq;
896 ✗ int n = (_estimateBias) ? 4 : 3; // Number of unknowns
897
898 // Check if the number of devices is sufficient to compute the position
899 ✗ if ((_estimateBias && _devices.size() < 5) || (!_estimateBias && _devices.size() < 4))
900 {
901 ✗ LOG_WARN("{}: Received less than {} observations. Can't compute position", nameId(), (_estimateBias ? 5 : 4));
902 ✗ return lsq;
903 }
904
905 LOG_DATA("{}: Received {} observations", nameId(), _devices.size());
906
907 ✗ Eigen::MatrixXd e_H = Eigen::MatrixXd::Zero(static_cast<int>(_devices.size()), n);
908 ✗ Eigen::MatrixXd W = Eigen::MatrixXd::Identity(static_cast<int>(_devices.size()), static_cast<int>(_devices.size()));
909 ✗ Eigen::VectorXd ddist = Eigen::VectorXd::Zero(static_cast<int>(_devices.size()));
910 ✗ size_t numMeasurements = _devices.size();
911
912 // Check if the initial position is NaN
913 ✗ if (std::isnan(_state.e_position(0)) || std::isnan(_state.e_position(1)) || std::isnan(_state.e_position(2)) || _useInitialValues)
914 {
915 ✗ _state.e_position << _initialState.e_position;
916 ✗ if (_estimateBias)
917 {
918 ✗ _state.bias = _initialState.bias;
919 }
920 }
921
922 // Iteratively solve the linear least squares problem
923 ✗ for (size_t o = 0; o < 15; o++)
924 {
925 LOG_DATA("{}: Iteration {}", nameId(), o);
926 ✗ for (size_t i = 0; i < numMeasurements; i++)
927 {
928 // Calculate the distance between the device and the estimated position
929 ✗ double distEst = (_devices.at(i).position - _state.e_position).norm();
930 ✗ if (_estimateBias)
931 {
932 ✗ distEst += _state.bias;
933 }
934
935 // Calculate the residual vector
936 ✗ ddist(static_cast<int>(i)) = _devices.at(i).distance - distEst;
937
938 ✗ Eigen::Vector3d e_lineOfSightUnitVector = Eigen::Vector3d::Zero();
939 ✗ if ((_state.e_position - _devices.at(i).position).norm() != 0) // Check if it is the same position
940 {
941 ✗ e_lineOfSightUnitVector = e_calcLineOfSightUnitVector(_state.e_position, _devices.at(i).position);
942 }
943
944 // Calculate the design matrix
945 ✗ e_H.block<1, 3>(static_cast<int>(i), 0) = -e_lineOfSightUnitVector;
946 ✗ if (_estimateBias)
947 {
948 ✗ e_H(static_cast<int>(i), 3) = 1;
949 }
950
951 // Calculate the weight matrix
952 ✗ if (_weightedSolution)
953 {
954 ✗ W(static_cast<int>(i), static_cast<int>(i)) = 1 / std::pow(_devices.at(i).distanceStd, 2);
955 }
956 }
957 // Solve the linear least squares problem
958 ✗ lsq = solveWeightedLinearLeastSquaresUncertainties(e_H, W, ddist);
959
960 ✗ if (_estimateBias)
961 {
962 LOG_DATA("{}: [{}] dx (lsq) {}, {}, {}, {}", nameId(), o, lsq.solution(0), lsq.solution(1), lsq.solution(2), lsq.solution(3));
963 LOG_DATA("{}: [{}] stdev_dx (lsq)\n{}", nameId(), o, lsq.variance.cwiseSqrt());
964 }
965 else
966 {
967 LOG_DATA("{}: [{}] dx (lsq) {}, {}, {}", nameId(), o, lsq.solution(0), lsq.solution(1), lsq.solution(2));
968 LOG_DATA("{}: [{}] stdev_dx (lsq)\n{}", nameId(), o, lsq.variance.cwiseSqrt());
969 }
970
971 // Update the estimated position
972 ✗ _state.e_position += lsq.solution.block<3, 1>(0, 0);
973
974 // Update the estimated bias
975 ✗ if (_estimateBias)
976 {
977 ✗ _state.bias += lsq.solution(3);
978 }
979
980 ✗ bool solInaccurate = pow(lsq.solution.norm(), 2) > 1e-3;
981 ✗ if (!solInaccurate) // Solution is accurate enough
982 {
983 ✗ lsq.solution.block<3, 1>(0, 0) = _state.e_position;
984 ✗ break;
985 }
986 ✗ if (o == 14)
987 {
988 LOG_DATA("{}: Solution did not converge", nameId());
989 ✗ lsq.solution.setConstant(std::numeric_limits<double>::quiet_NaN());
990 ✗ lsq.variance.setConstant(std::numeric_limits<double>::quiet_NaN());
991 ✗ if (_estimateBias)
992 {
993 ✗ _state.bias = std::numeric_limits<double>::quiet_NaN();
994 }
995 ✗ _state.e_position = _initialState.e_position;
996 ✗ if (_estimateBias)
997 {
998 ✗ _state.bias = _initialState.bias;
999 }
1000 }
1001 }
1002
1003 ✗ _devices.clear();
1004 LOG_DATA("{}: Position: {}", nameId(), _state.e_position.transpose());
1005
1006 ✗ return lsq;
1007 ✗ }
1008
1009 ✗ void NAV::WiFiPositioning::kfSolution()
1010 {
1011 ✗ double tau_i = !_lastPredictTime.empty()
1012 ✗ ? static_cast<double>((_devices.at(0).time - _lastPredictTime).count())
1013 ✗ : 0.0;
1014
1015 // ###########################################################################################################
1016 // Prediction
1017 // ###########################################################################################################
1018
1019 ✗ _lastPredictTime = _devices.at(0).time;
1020 ✗ if (tau_i > 0)
1021 {
1022 // Transition matrix
1023 ✗ Eigen::MatrixXd F = Eigen::MatrixXd::Zero(_numStates, _numStates);
1024 ✗ F(0, 3) = 1;
1025 ✗ F(1, 4) = 1;
1026 ✗ F(2, 5) = 1;
1027 ✗ _kalmanFilter.Phi = transitionMatrix_Phi_Taylor(F, tau_i, 1);
1028
1029 // Process noise covariance matrix
1030 ✗ _kalmanFilter.Q.block(0, 0, 3, 3) = std::pow(tau_i, 3) / 3.0 * Eigen::Matrix3d::Identity();
1031 ✗ _kalmanFilter.Q.block(3, 0, 3, 3) = std::pow(tau_i, 2) / 2.0 * Eigen::Matrix3d::Identity();
1032 ✗ _kalmanFilter.Q.block(0, 3, 3, 3) = std::pow(tau_i, 2) / 2.0 * Eigen::Matrix3d::Identity();
1033 ✗ _kalmanFilter.Q.block(3, 3, 3, 3) = tau_i * Eigen::Matrix3d::Identity();
1034 ✗ if (_estimateBias)
1035 {
1036 ✗ _kalmanFilter.Q(6, 6) = tau_i;
1037 }
1038 ✗ if (_processNoiseUnit == ProcessNoiseUnit::meter2)
1039 {
1040 ✗ _kalmanFilter.Q *= _processNoise;
1041 }
1042 ✗ else if (_processNoiseUnit == ProcessNoiseUnit::meter)
1043 {
1044 ✗ _kalmanFilter.Q *= std::pow(_processNoise, 2);
1045 }
1046 // Predict
1047 ✗ _kalmanFilter.predict();
1048 ✗ }
1049
1050 // ###########################################################################################################
1051 // Update
1052 // ###########################################################################################################
1053
1054 // Measurement
1055 ✗ double estimatedDistance = (_devices.at(0).position - _kalmanFilter.x.block<3, 1>(0, 0)).norm();
1056 ✗ if (_estimateBias)
1057 {
1058 ✗ estimatedDistance += _kalmanFilter.x(6);
1059 }
1060 ✗ _kalmanFilter.z << _devices.at(0).distance - estimatedDistance;
1061 ✗ if (_weightedSolution)
1062 {
1063 ✗ _kalmanFilter.R << std::pow(_devices.at(0).distanceStd, 2);
1064 }
1065
1066 // Design matrix
1067 ✗ Eigen::MatrixXd H = Eigen::MatrixXd::Zero(1, _numStates);
1068 ✗ H.block<1, 3>(0, 0) = -e_calcLineOfSightUnitVector(_kalmanFilter.x.block<3, 1>(0, 0), _devices.at(0).position);
1069 ✗ if (_estimateBias)
1070 {
1071 ✗ H(0, 6) = 1;
1072 }
1073 ✗ _kalmanFilter.H << H;
1074
1075 // Update
1076 ✗ _kalmanFilter.correctWithMeasurementInnovation();
1077
1078 ✗ _devices.clear();
1079 LOG_DATA("{}: Position: {}", nameId(), _kalmanFilter.x.block<3, 1>(0, 0).transpose());
1080 LOG_DATA("{}: Velocity: {}", nameId(), _kalmanFilter.x.block<3, 1>(3, 0).transpose());
1081 ✗ }
1082