7 #include "PlusConfigure.h" 15 #include <vtkMatrix4x4.h> 16 #include <vtkTransform.h> 19 #include <SensorsApi.h> 21 #include <PortableDeviceTypes.h> 22 #include <stringapiset.h> 55 using UniquePtrType = std::unique_ptr<T, DefaultDeletor<T>>;
58 auto createSafePointer(T* ptr)
60 UniquePtrType<T> safePtr;
68 struct SafeComInitializer
72 CoInitialize(
nullptr);
91 struct SensorStreamConfig
94 std::wstring FriendlyName;
95 std::wstring SerialNumber;
96 std::wstring Manufacturer;
97 std::wstring Description;
98 bool TrackerTimeToSystemTimeComputed{
false};
99 double TrackerTimeToSystemTimeSec{};
100 UniquePtrType<ISensor> SensorHandle{
nullptr};
101 IStream* UpdateThreadSensorMarshallingStream{
nullptr};
102 UniquePtrType<ISensor> UpdateThreadSensorRef{
nullptr};
106 struct SensorUserConfig
109 std::string SerialNumber;
116 std::string GetLastErrorAsString()
119 DWORD errorMessageID = ::GetLastError();
120 if (errorMessageID == 0)
122 return std::string(
"No error message available");
125 LPSTR messageBuffer{
nullptr};
126 auto size = FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
127 NULL, errorMessageID, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPSTR)&messageBuffer, 0, NULL);
129 std::string
message(messageBuffer, size);
130 LocalFree(messageBuffer);
135 uint64_t GetSystemTimeInMs(SYSTEMTIME* systemTime)
138 SystemTimeToFileTime(systemTime, &filetime);
140 return (static_cast<uint64_t>(filetime.dwLowDateTime) + (static_cast<uint64_t>(filetime.dwHighDateTime) << 32LL)) * 100 / 1000000;
143 std::string ConvertWideStr2Str(
const std::wstring& wstr)
145 auto size = WideCharToMultiByte(CP_ACP, 0, wstr.c_str(), -1,
nullptr, 0,
nullptr,
nullptr);
146 std::vector<char> buf(size);
147 WideCharToMultiByte(CP_ACP, 0, wstr.c_str(), -1, &buf[0], size,
nullptr,
nullptr);
148 return std::string(&buf[0]);
155 REFSENSOR_TYPE_ID ConvertCustomTypeToWindowsType(
SensorType type)
159 case SensorType::Accelerometer:
160 return SENSOR_TYPE_ACCELEROMETER_3D;
161 case SensorType::Gyrometer:
162 return SENSOR_TYPE_GYROMETER_3D;
163 case SensorType::Magnetometer:
164 return SENSOR_TYPE_COMPASS_3D;
169 return SENSOR_TYPE_UNKNOWN;
176 case SensorType::Accelerometer:
177 return "Accelerometer";
178 case SensorType::Gyrometer:
180 case SensorType::Magnetometer:
181 return "Magnetometer";
189 std::string ToString(SensorState
state)
193 case SensorState::SENSOR_STATE_READY:
194 return "Ready to send sensor data";
195 case SensorState::SENSOR_STATE_NOT_AVAILABLE:
196 return "The sensor is not available for use";
197 case SensorState::SENSOR_STATE_NO_DATA:
198 return "The sensor is available but does not have data";
199 case SensorState::SENSOR_STATE_INITIALIZING:
200 return "The sensor is available, but performing initialization. Try again later.";
201 case SensorState::SENSOR_STATE_ACCESS_DENIED:
202 return "The sensor is available, but the user account does not have permission to access the sensor data";
203 case SensorState::SENSOR_STATE_ERROR:
204 return "The sensor has raised an error";
214 class vtkPlusGenericSensorTracker::vtkInternal
218 UniquePtrType<ISensorManager> SensorManager{
nullptr};
219 std::vector<SensorStreamConfig> SensorStreams;
220 std::vector<SensorUserConfig> SensorUserConfigs;
221 bool UseReportedTimestamp{
false};
228 vtkInternal::~vtkInternal()
230 this->ReleaseStreams();
231 this->ReleaseManager();
239 CoInitialize(
nullptr);
241 ISensorManager* sensorManager{
nullptr};
242 auto hr = CoCreateInstance(CLSID_SensorManager,
243 nullptr, CLSCTX_INPROC_SERVER,
244 IID_PPV_ARGS(&sensorManager));
246 if (hr == HRESULT_FROM_WIN32(ERROR_ACCESS_DISABLED_BY_POLICY))
250 LOG_ERROR(
"Group policiy settings prevent access to sensor manager: " << GetLastErrorAsString());
256 LOG_ERROR(
"Failed to retrieve sensor manager: " << GetLastErrorAsString());
260 this->SensorManager.reset(sensorManager);
266 std::set<SensorType> sensorTypes = {SensorType::Accelerometer, SensorType::Gyrometer};
275 SensorStreamConfig config;
278 this->SensorStreams.emplace_back(std::move(config));
282 if (this->SensorStreams.empty())
284 LOG_ERROR(
"No data source available: required at least one Accelerometer or Gyrometer source");
291 PlusStatus InitializeStream(SensorStreamConfig& sensorStream)
293 ISensorCollection* collection{
nullptr};
294 auto hr = this->SensorManager->GetSensorsByType(ConvertCustomTypeToWindowsType(sensorStream.Type), &collection);
296 LOG_INFO(
"Initializing stream for sensor of type: " << ToString(sensorStream.Type));
298 if (hr == HRESULT_FROM_WIN32(ERROR_NOT_FOUND))
300 LOG_ERROR(
"No sensor found");
306 LOG_ERROR(
"Failed to retrieve sensor collection: " << GetLastErrorAsString());
310 auto safeCollection = createSafePointer(collection);
312 ULONG collectionSize{};
313 hr = safeCollection->GetCount(&collectionSize);
314 if (FAILED(hr) || collectionSize == 0)
316 LOG_ERROR(
"Failed to retrieve sensor: " << GetLastErrorAsString());
320 auto userConfig = std::find_if(this->SensorUserConfigs.cbegin(), this->SensorUserConfigs.cend(), [&sensorStream](
const SensorUserConfig & userConfig)
322 auto referencedToolId = sensorStream.Tool->GetId();
323 auto refIdx = referencedToolId.find(
"To");
324 if (refIdx != std::string::npos)
326 referencedToolId = referencedToolId.substr(0, refIdx);
328 return userConfig.ToolId == referencedToolId;
331 if (collectionSize > 1 && userConfig == this->SensorUserConfigs.cend())
333 LOG_WARNING(
"Found more than one sensor of this type. Connecting to the first one.");
335 else if (userConfig != this->SensorUserConfigs.cend())
337 LOG_INFO(
"Searching for device with serial number: " << userConfig->SerialNumber);
341 for (
ULONG i = 0;
i < collectionSize;
i++)
343 ISensor* sensor{
nullptr};
344 auto hr = safeCollection->GetAt(
i, &sensor);
347 LOG_ERROR(
"Failed to retrieve sensor: " << GetLastErrorAsString());
350 sensorStream.SensorHandle.reset(sensor);
351 InitializeProperties(sensorStream);
353 if ((userConfig == this->SensorUserConfigs.cend()) || ConvertWideStr2Str(sensorStream.SerialNumber) == userConfig->SerialNumber)
362 LOG_ERROR(
"Failed to find a matching device");
368 IStream* sensorMarshallingStream{
nullptr};
369 hr = CoMarshalInterThreadInterfaceInStream(IID_ISensor, sensorStream.SensorHandle.get(), &sensorMarshallingStream);
373 LOG_ERROR(
"Failed to marshal interface for multithreading: " << GetLastErrorAsString());
377 sensorStream.UpdateThreadSensorMarshallingStream = sensorMarshallingStream;
378 sensorStream.SensorHandle->SetEventSink(
nullptr);
379 return CheckForSupportedData(sensorStream);
382 void InitializeProperties(SensorStreamConfig& sensorStream)
384 const PROPERTYKEY SensorProperties[] =
386 SENSOR_PROPERTY_MANUFACTURER,
387 SENSOR_PROPERTY_SERIAL_NUMBER,
388 SENSOR_PROPERTY_DESCRIPTION,
389 SENSOR_PROPERTY_FRIENDLY_NAME
392 sensorStream.Manufacturer = L
"";
393 sensorStream.SerialNumber = L
"";
394 sensorStream.FriendlyName = L
"";
395 sensorStream.Description = L
"";
397 IPortableDeviceKeyCollection* keys{
nullptr};
399 LOG_DEBUG(
"Retrieving metadata for sensor of type: " << ToString(sensorStream.Type));
401 auto hr = CoCreateInstance(CLSID_PortableDeviceKeyCollection,
403 CLSCTX_INPROC_SERVER,
404 IID_PPV_ARGS(&keys));
408 LOG_WARNING(
"Failed to retrieve metadata: " << GetLastErrorAsString());
412 auto safeKeys = createSafePointer(keys);
414 for (
DWORD dwIndex = 0; dwIndex < ARRAYSIZE(SensorProperties); dwIndex++)
416 safeKeys->Add(SensorProperties[dwIndex]);
419 IPortableDeviceValues* values{
nullptr};
420 hr = sensorStream.SensorHandle->GetProperties(safeKeys.get(), &values);
424 LOG_WARNING(
"Failed to retrieve metadata: " << GetLastErrorAsString());
428 auto safeValues = createSafePointer(values);
431 safeValues->GetCount(&valsCount);
435 for (
DWORD i = 0;
i < valsCount;
i++)
438 hr = safeValues->GetAt(
i, &pk, &pv);
443 if (IsEqualPropertyKey(pk, SENSOR_PROPERTY_MANUFACTURER))
445 sensorStream.Manufacturer = pv.pwszVal;
447 else if (IsEqualPropertyKey(pk, SENSOR_PROPERTY_SERIAL_NUMBER))
449 sensorStream.SerialNumber = pv.pwszVal;
451 else if (IsEqualPropertyKey(pk, SENSOR_PROPERTY_FRIENDLY_NAME))
453 sensorStream.FriendlyName = pv.pwszVal;
455 else if (IsEqualPropertyKey(pk, SENSOR_PROPERTY_DESCRIPTION))
457 sensorStream.Description = pv.pwszVal;
461 PropVariantClear(&pv);
464 LOG_DEBUG(
"Manufacturer: " << ConvertWideStr2Str(sensorStream.Manufacturer));
465 LOG_DEBUG(
"Serial Number: " << ConvertWideStr2Str(sensorStream.SerialNumber));
466 LOG_DEBUG(
"FriendlyName: " << ConvertWideStr2Str(sensorStream.FriendlyName));
467 LOG_DEBUG(
"Description: " << ConvertWideStr2Str(sensorStream.Description));
471 CheckForSupportedData(SensorStreamConfig& sensorStream)
473 LOG_INFO(
"Checking for supported data for sensor of type: " << ToString(sensorStream.Type));
475 IPortableDeviceKeyCollection* keys{
nullptr};
476 auto hr = CoCreateInstance(CLSID_PortableDeviceKeyCollection,
478 CLSCTX_INPROC_SERVER,
479 IID_PPV_ARGS(&keys));
483 LOG_ERROR(
"Failed to retrieve supported data: " << GetLastErrorAsString());
487 hr = sensorStream.SensorHandle->GetSupportedDataFields(&keys);
490 LOG_ERROR(
"Failed to retrieve supported data: " << GetLastErrorAsString());
494 auto safeKeys = createSafePointer(keys);
497 hr = safeKeys->GetCount(&keysCount);
500 LOG_ERROR(
"Failed to retrieve supported data: " << GetLastErrorAsString());
504 if (sensorStream.Type == SensorType::Accelerometer && !CheckForSupportedDataAcc(safeKeys.get(), keysCount))
506 LOG_ERROR(
"The accelerometer sensor cannot provide the expected data");
509 else if (sensorStream.Type == SensorType::Gyrometer && !CheckForSupportedDataGyr(safeKeys.get(), keysCount))
511 LOG_ERROR(
"The gyrometer sensor cannot provide the expected data");
518 void ReleaseStreams()
520 for (
auto& sensorStream : this->SensorStreams)
522 if (sensorStream.UpdateThreadSensorRef)
524 sensorStream.UpdateThreadSensorRef.reset(
nullptr);
526 else if (sensorStream.UpdateThreadSensorMarshallingStream)
528 sensorStream.UpdateThreadSensorMarshallingStream->Release();
530 sensorStream.SensorHandle.reset(
nullptr);
534 void ReleaseManager()
536 this->SensorManager.reset(
nullptr);
543 auto hr = sensorHandle->GetState(&
state);
545 LOG_TRACE(
"Retrieving state for sensor of type: " << ToString(
sensorType));
549 LOG_ERROR(
"Failed to retrieve state for sensor: " << GetLastErrorAsString());
553 if (
state == SensorState::SENSOR_STATE_READY ||
state == SensorState::SENSOR_STATE_NO_DATA)
555 LOG_TRACE(
"Sensor state: " << ToString(
state));
559 LOG_ERROR(
"Sensor is not ready: " << ToString(
state));
569 bool CheckForSupportedDataAcc(IPortableDeviceKeyCollection* keys,
DWORD keysCount)
572 std::bitset<3> supported = 0b000;
574 for (
DWORD i = 0;
i < keysCount;
i++)
576 auto hr = keys->GetAt(
i, &pk);
583 if (IsEqualPropertyKey(pk, SENSOR_DATA_TYPE_ACCELERATION_X_G))
587 else if (IsEqualPropertyKey(pk, SENSOR_DATA_TYPE_ACCELERATION_Y_G))
591 else if (IsEqualPropertyKey(pk, SENSOR_DATA_TYPE_ACCELERATION_Z_G))
597 return supported.all();
600 static PlusStatus RetrieveAccData(ISensorDataReport* report, vtkSmartPointer<vtkMatrix4x4> matrix)
602 std::array<const PROPERTYKEY, 3> fields = {SENSOR_DATA_TYPE_ACCELERATION_X_G, SENSOR_DATA_TYPE_ACCELERATION_Y_G, SENSOR_DATA_TYPE_ACCELERATION_Z_G};
604 for (
int i = 0;
i < 3; ++
i)
606 PROPVARIANT var = {};
607 auto hr = report->GetSensorValue(fields[
i], &var);
609 if (!SUCCEEDED(hr) || var.vt != VT_R8)
611 PropVariantClear(&var);
612 LOG_ERROR(
"Failed to retrieve acceleration in " << ((
i == 0) ?
"x" : ((
i == 1) ?
"y" :
"z")));
616 matrix->SetElement(
i, 3, var.dblVal);
617 LOG_TRACE(
"Retrieve acceleration component " << ((
i == 0) ?
"x" : ((
i == 1) ?
"y" :
"z")) <<
": " << var.dblVal);
618 PropVariantClear(&var);
628 bool CheckForSupportedDataGyr(IPortableDeviceKeyCollection* keys,
DWORD keysCount)
631 std::bitset<3> supported = 0b000;
633 for (
DWORD i = 0;
i < keysCount;
i++)
635 auto hr = keys->GetAt(
i, &pk);
642 if (IsEqualPropertyKey(pk, SENSOR_DATA_TYPE_ANGULAR_VELOCITY_X_DEGREES_PER_SECOND))
646 else if (IsEqualPropertyKey(pk, SENSOR_DATA_TYPE_ANGULAR_VELOCITY_Y_DEGREES_PER_SECOND))
650 else if (IsEqualPropertyKey(pk, SENSOR_DATA_TYPE_ANGULAR_VELOCITY_Z_DEGREES_PER_SECOND))
656 return supported.all();
659 static PlusStatus RetrieveGyrData(ISensorDataReport* report, vtkSmartPointer<vtkMatrix4x4> matrix)
661 std::array<const PROPERTYKEY, 3> fields = {SENSOR_DATA_TYPE_ANGULAR_VELOCITY_X_DEGREES_PER_SECOND, SENSOR_DATA_TYPE_ANGULAR_VELOCITY_Y_DEGREES_PER_SECOND, SENSOR_DATA_TYPE_ANGULAR_VELOCITY_Z_DEGREES_PER_SECOND};
663 for (
int i = 0;
i < 3; ++
i)
665 PROPVARIANT var = {};
666 auto hr = report->GetSensorValue(fields[
i], &var);
668 if (!SUCCEEDED(hr) || var.vt != VT_R8)
670 PropVariantClear(&var);
671 LOG_ERROR(
"Failed to retrieve angular velocity in " << ((
i == 0) ?
"x" : ((
i == 1) ?
"y" :
"z")));
675 matrix->SetElement(
i, 3, var.dblVal);
676 LOG_TRACE(
"Retrieve angular velocity component " << ((
i == 0) ?
"x" : ((
i == 1) ?
"y" :
"z")) <<
": " << var.dblVal);
677 PropVariantClear(&var);
690 : Internal(new vtkInternal(this))
698 delete this->Internal;
699 this->Internal =
nullptr;
707 os << indent <<
"Generic Sensor Configuration:" << std::endl;
708 os << indent <<
"UseReportedTimestamp: " << this->Internal->UseReportedTimestamp << std::endl;
709 for (
auto& sensorStream : this->Internal->SensorStreams)
711 os <<
"Sensor type: " << ToString(sensorStream.Type);
712 os << indent <<
"Serial Number: " << ConvertWideStr2Str(sensorStream.SerialNumber);
713 os << indent <<
"Serial Manufaturer: " << ConvertWideStr2Str(sensorStream.Manufacturer);
714 os << indent <<
"Serial Friendly Name: " << ConvertWideStr2Str(sensorStream.FriendlyName);
715 os << indent <<
"Serial Description: " << ConvertWideStr2Str(sensorStream.Description);
723 XML_READ_BOOL_ATTRIBUTE_NONMEMBER_OPTIONAL(UseReportedTimestamp, this->Internal->UseReportedTimestamp, deviceConfig);
724 XML_FIND_NESTED_ELEMENT_REQUIRED(dataSourcesElement, deviceConfig,
"DataSources");
726 LOG_TRACE(
"Reading custom configuration fields in " << dataSourcesElement->GetNumberOfNestedElements() <<
" nested elements");
727 for (
int nestedElementIndex = 0; nestedElementIndex < dataSourcesElement->GetNumberOfNestedElements(); ++nestedElementIndex)
729 vtkXMLDataElement* dataElement = dataSourcesElement->GetNestedElement(nestedElementIndex);
730 if (STRCASECMP(dataElement->GetName(),
"DataSource") != 0)
735 LOG_TRACE(
"Found a new data source");
737 if (dataElement->GetAttribute(
"Type") != NULL && STRCASECMP(dataElement->GetAttribute(
"Type"),
"Tool") == 0)
739 const char* toolId = dataElement->GetAttribute(
"Id");
740 if (toolId ==
nullptr)
743 LOG_ERROR(
"Failed to initialize tool: Id is missing");
747 LOG_TRACE(
"Data source name: " << toolId);
748 SensorUserConfig config;
749 config.ToolId = toolId;
750 XML_READ_STRING_ATTRIBUTE_NONMEMBER_OPTIONAL(SerialNumber, config.SerialNumber, dataElement);
752 if (!config.SerialNumber.empty())
754 this->Internal->SensorUserConfigs.emplace_back(config);
759 LOG_ERROR(
"DataSource with unknown Type.");
769 if (this->Internal->PopulateStreams() !=
PLUS_SUCCESS ||
770 this->Internal->RetrieveSensorManager() !=
PLUS_SUCCESS)
781 this->Internal->ReleaseManager();
788 for (
const auto& sensorStream : this->Internal->SensorStreams)
790 if (this->Internal->CheckSensorState(sensorStream.Type, sensorStream.SensorHandle.get()) !=
PLUS_SUCCESS)
801 for (
auto& sensorStream : this->Internal->SensorStreams)
803 if (this->Internal->InitializeStream(sensorStream) !=
PLUS_SUCCESS)
809 this->FrameNumber = 0;
816 this->Internal->ReleaseStreams();
823 static SafeComInitializer safeComInitializer;
824 for (
auto& sensorStream : this->Internal->SensorStreams)
826 LOG_TRACE(
"Retrieving sensor data for sensor of type: " << ToString(sensorStream.Type));
828 if (!sensorStream.UpdateThreadSensorRef)
830 ISensor* sensorRef{
nullptr};
831 auto hr = CoGetInterfaceAndReleaseStream(sensorStream.UpdateThreadSensorMarshallingStream, IID_ISensor, (LPVOID*)&sensorRef);
835 LOG_ERROR(
"Failed to retrieve sensor handle: " << GetLastErrorAsString());
839 sensorStream.UpdateThreadSensorRef.reset(sensorRef);
840 sensorStream.UpdateThreadSensorMarshallingStream =
nullptr;
843 if (Internal->CheckSensorState(sensorStream.Type, sensorStream.UpdateThreadSensorRef.get()) !=
PLUS_SUCCESS)
845 LOG_ERROR(
"Sensor state not valid");
849 ISensorDataReport* report{
nullptr};
850 auto hr = sensorStream.UpdateThreadSensorRef->GetData(&report);
852 if (hr == HRESULT_FROM_WIN32(ERROR_NO_DATA))
854 LOG_ERROR(
"No data is available yet");
860 LOG_ERROR(
"Failed to retrieve data: " << GetLastErrorAsString() << hr);
864 auto safeReport = createSafePointer(report);
866 vtkNew<vtkMatrix4x4> transform;
867 if (sensorStream.Type == SensorType::Accelerometer)
869 if (vtkInternal::RetrieveAccData(safeReport.get(), transform) !=
PLUS_SUCCESS)
871 LOG_ERROR(
"Failed to get accelerometer data: " << GetLastErrorAsString());
875 else if (sensorStream.Type == SensorType::Gyrometer)
877 if (vtkInternal::RetrieveGyrData(safeReport.get(), transform) !=
PLUS_SUCCESS)
879 LOG_ERROR(
"Failed to get gyrometer data: " << GetLastErrorAsString());
884 if (this->Internal->UseReportedTimestamp)
887 hr = safeReport->GetTimestamp(&sysTime);
892 LOG_TRACE(
"Failed to get timestamp. Falling back on current system time.");
893 timestamp = vtkIGSIOAccurateTimer::GetSystemTime();
897 auto sensorTimestampMs = GetSystemTimeInMs(&sysTime);
898 LOG_TRACE(
"Sensor reported timestamp is ms: " << sensorTimestampMs);
899 if (!sensorStream.TrackerTimeToSystemTimeComputed)
901 const double timeSystemSec = vtkIGSIOAccurateTimer::GetSystemTime();
902 LOG_TRACE(
"System time in s: " << timeSystemSec);
903 sensorStream.TrackerTimeToSystemTimeSec = timeSystemSec - sensorTimestampMs / 1000.0;
904 sensorStream.TrackerTimeToSystemTimeComputed =
true;
906 LOG_TRACE(
"Timestamp offset for this sensor: " << sensorStream.TrackerTimeToSystemTimeSec);
909 timestamp = sensorTimestampMs / 1000.0 + sensorStream.TrackerTimeToSystemTimeSec;
912 LOG_TRACE(
"Final timestamp in s: " <<
timestamp);
917 ToolTimeStampedUpdate(sensorStream.Tool->GetId(), transform, TOOL_OK, this->FrameNumber, vtkIGSIOAccurateTimer::GetSystemTime());
virtual void PrintSelf(ostream &os, vtkIndent indent) VTK_OVERRIDE
PlusStatus InternalConnect() override
Interface class to collect sensor data in a generic wayFor now, the following sensor types are suppor...
virtual PlusStatus ToolTimeStampedUpdateWithoutFiltering(const std::string &aToolSourceId, vtkMatrix4x4 *matrix, ToolStatus status, double unfilteredtimestamp, double filteredtimestamp, const igsioFieldMapType *customFields=NULL)
virtual PlusStatus ToolTimeStampedUpdate(const std::string &aToolSourceId, vtkMatrix4x4 *matrix, ToolStatus status, unsigned long frameNumber, double unfilteredtimestamp, const igsioFieldMapType *customFields=NULL)
void PrintSelf(ostream &os, vtkIndent indent) override
PlusStatus InternalStopRecording() override
~vtkPlusGenericSensorTracker()
PlusStatus Probe() override
PlusStatus GetToolByPortName(const char *aPortName, vtkPlusDataSource *&aSource)
vtkPlusGenericSensorTracker()
#define XML_FIND_DEVICE_ELEMENT_REQUIRED_FOR_READING(deviceConfig, rootConfigElement)
PlusStatus InternalStartRecording() override
PlusStatus InternalUpdate() override
PlusStatus InternalDisconnect() override
bool StartThreadForInternalUpdates
vtkStandardNewMacro(vtkPlusGenericSensorTracker)
PlusStatus ReadConfiguration(vtkXMLDataElement *config) override
PhidgetVoltageRatioInput_SensorType sensorType
Interface to a 3D positioning tool, video source, or generalized data stream.