Softfusion cleanup (#424)

* Make SPI work

Move sensor building logic to a separate class and file

Don't use templates for RegisterInterface/I2CImpl/SPIImpl

This started getting ridiculous, now we can actually maintain it.

Make BNO085 work again

Add BNO to automatic detection, remove a bunch of others

Not all IMU types are enabled right now due to code size optimization, but it could be expanded in the future with optimization of Softfusion.

Pick IMU type automatically by asking it

* ESP32 spelling fix (#396)

Fix: ES32 -> ESP32

* (Probably) multiply acceleration by tracker rotation offset

* Remove stome stuff from softfusionsensor

* Missed stuff

* Undo defines changes

* Undo debug.h changes

* Uses32BitSensorData is unneccessary now

* Remove some unnecessary variables

* Cleanup some logging text

* Formatting

* Split SensorBuilder into a header-source file pair

* Fix copyright

* Fix some issues with glove after all the changes

Add definitions for SlimeVR v1.2
Fix typo and add comments to quaternion sandwich function

* Add NO_WIRE definition for SPI devices

* Fix formatting

* Minor fix

* If ICM-45686 not found, ask again nicely

* Fix MCP interface not building

* Remove uneccessary "default" ctor from SPIImpl

* Remove buildSensorReal

* Invert if statement in sensorbuilder+

* Fix formatting

* If ICM-45686 not found, ask again nicely

* Fix MCP interface not building

* Fix formatting

* Various cleanup

* Formattign

* Fix detected logic

* Remove unnecessary Self using

* For some reason this fixes memory corruption issues

* Formatting

* This actually works this time

* Small cleanup

* Remove some unused includes

* Formatting

* Mag support (Attempt 2) (#458)

* WhoAmI check working

* In theory this should be setting up the mag

* Not sure how that happened

* Add magnetometer status to GET TEST and GET INFO

* Formatting

* Formatting 2

---------

Co-authored-by: Eiren Rain <Eirenliel@users.noreply.github.com>
Co-authored-by: Butterscotch! <bscotchvanilla@gmail.com>
This commit is contained in:
gorbit99
2025-06-12 04:44:58 +02:00
committed by GitHub
parent cabceb2067
commit 23632581e7
24 changed files with 869 additions and 355 deletions

View File

@@ -72,10 +72,10 @@
#if USE_RUNTIME_CALIBRATION
#include "sensors/softfusion/runtimecalibration/RuntimeCalibration.h"
#define SFCALIBRATOR SlimeVR::Sensors::RuntimeCalibration::RuntimeCalibrator
#define SFCALIBRATOR RuntimeCalibration::RuntimeCalibrator
#else
#include "sensors/softfusion/SoftfusionCalibration.h"
#define SFCALIBRATOR SlimeVR::Sensor::SoftfusionCalibrator
#define SFCALIBRATOR SoftfusionCalibrator
#endif
#ifdef ESP32

View File

@@ -25,3 +25,27 @@ bool SensorToggleState::getToggle(SensorToggles toggle) const {
}
return false;
}
void SensorToggleState::onToggleChange(
std::function<void(SensorToggles, bool)>&& callback
) {
this->callback = callback;
}
void SensorToggleState::emitToggleChange(SensorToggles toggle, bool state) const {
if (callback) {
(*callback)(toggle, state);
}
}
const char* SensorToggleState::toggleToString(SensorToggles toggle) {
switch (toggle) {
case SensorToggles::MagEnabled:
return "MagEnabled";
case SensorToggles::CalibrationEnabled:
return "CalibrationEnabled";
case SensorToggles::TempGradientCalibrationEnabled:
return "TempGradientCalibrationEnabled";
}
return "Unknown";
}

View File

@@ -24,6 +24,8 @@
#pragma once
#include <cstdint>
#include <functional>
#include <optional>
#include "../debug.h"
@@ -38,7 +40,14 @@ public:
void setToggle(SensorToggles toggle, bool state);
[[nodiscard]] bool getToggle(SensorToggles toggle) const;
void onToggleChange(std::function<void(SensorToggles, bool)>&& callback);
static const char* toggleToString(SensorToggles toggle);
private:
std::optional<std::function<void(SensorToggles, bool)>> callback;
void emitToggleChange(SensorToggles toggle, bool state) const;
bool magEnabled = !USE_6_AXIS;
bool calibrationEnabled = true;
bool tempGradientCalibrationEnabled = true;

View File

@@ -135,6 +135,13 @@ void BNO080Sensor::motionSetup() {
configured = true;
m_tpsCounter.reset();
m_dataCounter.reset();
toggles.onToggleChange([&](SensorToggles toggle, bool) {
if (toggle == SensorToggles::MagEnabled) {
// TODO: maybe handle this more gracefully, I'm sure it's possible
motionSetup();
}
});
}
void BNO080Sensor::motionLoop() {

View File

@@ -91,6 +91,8 @@ void Sensor::resetTemperatureCalibrationState() {
printTemperatureCalibrationUnsupported();
};
const char* Sensor::getAttachedMagnetometer() const { return nullptr; }
SlimeVR::Configuration::SensorConfigBits Sensor::getSensorConfigData() {
return SlimeVR::Configuration::SensorConfigBits{
.magEnabled = toggles.getToggle(SensorToggles::MagEnabled),
@@ -157,12 +159,16 @@ void Sensor::markRestCalibrationComplete(bool completed) {
}
void Sensor::setFlag(SensorToggles toggle, bool state) {
assert(isFlagSupported(toggle));
if (!isFlagSupported(toggle)) {
m_Logger.error(
"Toggle %s isn't supported by this sensor!",
SensorToggleState::toggleToString(toggle)
);
return;
}
toggles.setToggle(toggle, state);
configuration.setSensorToggles(sensorId, toggles);
configuration.save();
motionSetup();
}

View File

@@ -85,6 +85,11 @@ public:
virtual void printDebugTemperatureCalibrationState();
virtual void resetTemperatureCalibrationState();
virtual void saveTemperatureCalibration();
// TODO: currently only for softfusionsensor, bmi160 and others should get
// an overload too
virtual const char* getAttachedMagnetometer() const;
// TODO: realistically each sensor should print its own state instead of
// having 15 getters for things only the serial commands use
bool isWorking() { return working; };
bool getHadData() const { return hadData; };
bool isValid() { return m_hwInterface != nullptr; };

View File

@@ -28,12 +28,13 @@
#include <functional>
#include "configuration/SensorConfig.h"
#include "imuconsts.h"
#include "motionprocessing/types.h"
#include "sensors/SensorFusion.h"
namespace SlimeVR::Sensor {
namespace SlimeVR::Sensors {
template <typename IMU, typename RawSensorT, typename RawVectorT>
template <typename IMU>
class CalibrationBase {
public:
CalibrationBase(
@@ -41,20 +42,17 @@ public:
IMU& sensor,
uint8_t sensorId,
SlimeVR::Logging::Logger& logger,
float TempTs,
float AScale,
float GScale,
SensorToggleState& toggles
)
: fusion{fusion}
, sensor{sensor}
, sensorId{sensorId}
, logger{logger}
, TempTs{TempTs}
, AScale{AScale}
, GScale{GScale}
, toggles{toggles} {}
using Consts = IMUConsts<IMU>;
using RawSensorT = typename Consts::RawSensorT;
static constexpr bool HasMotionlessCalib
= requires(IMU& i) { typename IMU::MotionlessCalibrationData; };
static constexpr size_t MotionlessCalibDataSize() {
@@ -65,15 +63,8 @@ public:
}
}
using EatSamplesFn = std::function<void(const uint32_t)>;
using ReturnLastFn
= std::function<std::tuple<RawVectorT, RawVectorT, int16_t>(const uint32_t)>;
virtual void startCalibration(
int calibrationType,
const EatSamplesFn& eatSamplesForSeconds,
const ReturnLastFn& eatSamplesReturnLast
){};
virtual void checkStartupCalibration() {}
virtual void startCalibration(int calibrationType){};
virtual bool calibrationMatches(
const SlimeVR::Configuration::SensorConfig& sensorCalibration
@@ -116,10 +107,7 @@ protected:
IMU& sensor;
uint8_t sensorId;
SlimeVR::Logging::Logger& logger;
float TempTs;
float AScale;
float GScale;
SensorToggleState& toggles;
};
} // namespace SlimeVR::Sensor
} // namespace SlimeVR::Sensors

View File

@@ -23,48 +23,120 @@
#pragma once
#include <cstdint>
#include <cstring>
#include <vector>
#include "CalibrationBase.h"
#include "GlobalVars.h"
#include "configuration/SensorConfig.h"
#include "logging/Logger.h"
#include "motionprocessing/RestDetection.h"
#include "motionprocessing/types.h"
#include "sensors/SensorFusion.h"
#include "sensors/softfusion/CalibrationBase.h"
namespace SlimeVR::Sensor {
namespace SlimeVR::Sensors {
template <typename IMU, typename RawSensorT, typename RawVectorT>
class SoftfusionCalibrator : public CalibrationBase<IMU, RawSensorT, RawVectorT> {
template <typename IMU>
class SoftfusionCalibrator : public CalibrationBase<IMU> {
public:
static constexpr bool HasUpsideDownCalibration = true;
using Base = CalibrationBase<IMU, RawSensorT, RawVectorT>;
using Base = CalibrationBase<IMU>;
using Consts = typename Base::Consts;
using RawSensorT = typename Consts::RawSensorT;
using RawVectorT = typename Consts::RawVectorT;
SoftfusionCalibrator(
Sensors::SensorFusion& fusion,
IMU& sensor,
uint8_t sensorId,
SlimeVR::Logging::Logger& logger,
float TempTs,
float AScale,
float GScale,
SensorToggleState& toggles
)
: Base{fusion, sensor, sensorId, logger, TempTs, AScale, GScale, toggles} {
calibration.T_Ts = TempTs;
: Base{fusion, sensor, sensorId, logger, toggles} {
calibration.T_Ts = Consts::getDefaultTempTs();
}
void startCalibration(
int calibrationType,
const Base::EatSamplesFn& eatSamplesForSeconds,
const Base::ReturnLastFn& eatSamplesReturnLast
) final {
void eatSamplesForSeconds(const uint32_t seconds) {
const auto targetDelay = millis() + 1000 * seconds;
auto lastSecondsRemaining = seconds;
while (millis() < targetDelay) {
#ifdef ESP8266
ESP.wdtFeed();
#endif
auto currentSecondsRemaining = (targetDelay - millis()) / 1000;
if (currentSecondsRemaining != lastSecondsRemaining) {
logger.info("%d...", currentSecondsRemaining + 1);
lastSecondsRemaining = currentSecondsRemaining;
}
sensor.bulkRead({
[](const RawSensorT xyz[3], const sensor_real_t timeDelta) {},
[](const RawSensorT xyz[3], const sensor_real_t timeDelta) {},
[](const int16_t xyz, const sensor_real_t timeDelta) {},
});
}
}
std::tuple<RawVectorT, RawVectorT, int16_t> eatSamplesReturnLast(
const uint32_t milliseconds
) {
RawVectorT accel = {0};
RawVectorT gyro = {0};
int16_t temp = 0;
const auto targetDelay = millis() + milliseconds;
while (millis() < targetDelay) {
sensor.bulkRead({
[&](const RawSensorT xyz[3], const sensor_real_t timeDelta) {
accel[0] = xyz[0];
accel[1] = xyz[1];
accel[2] = xyz[2];
},
[&](const RawSensorT xyz[3], const sensor_real_t timeDelta) {
gyro[0] = xyz[0];
gyro[1] = xyz[1];
gyro[2] = xyz[2];
},
[&](const int16_t rawTemp, const sensor_real_t timeDelta) {
temp = rawTemp;
},
});
yield();
}
return std::make_tuple(accel, gyro, temp);
}
void checkStartupCalibration() final {
auto lastRawSample = eatSamplesReturnLast(1000);
auto gravity = static_cast<sensor_real_t>(
Consts::AScale * static_cast<sensor_real_t>(std::get<0>(lastRawSample)[2])
);
logger.info("Gravity read: %.1f (need < -7.5 to start calibration)", gravity);
if (gravity > -7.5f) {
return;
}
ledManager.on();
logger.info("Flip front in 5 seconds to start calibration");
lastRawSample = eatSamplesReturnLast(5000);
gravity = static_cast<sensor_real_t>(
Consts::AScale * static_cast<sensor_real_t>(std::get<0>(lastRawSample)[2])
);
if (gravity > 7.5f) {
logger.debug("Starting calibration...");
startCalibration(0);
} else {
logger.info("Flip not detected. Skipping calibration.");
}
ledManager.off();
}
void startCalibration(int calibrationType) final {
if (calibrationType == 0) {
// ALL
calibrateSampleRate(eatSamplesForSeconds);
calibrateSampleRate();
if constexpr (Base::HasMotionlessCalib) {
typename IMU::MotionlessCalibrationData calibData;
sensor.motionlessCalibration(calibData);
@@ -73,14 +145,14 @@ public:
// Gryoscope offset calibration can only happen after any motionless
// gyroscope calibration, otherwise we are calculating the offset based
// on an incorrect starting point
calibrateGyroOffset(eatSamplesReturnLast);
calibrateAccel(eatSamplesForSeconds);
calibrateGyroOffset();
calibrateAccel();
} else if (calibrationType == 1) {
calibrateSampleRate(eatSamplesForSeconds);
calibrateSampleRate();
} else if (calibrationType == 2) {
calibrateGyroOffset(eatSamplesReturnLast);
calibrateGyroOffset();
} else if (calibrationType == 3) {
calibrateAccel(eatSamplesForSeconds);
calibrateAccel();
} else if (calibrationType == 4) {
if constexpr (Base::HasMotionlessCalib) {
typename IMU::MotionlessCalibrationData calibData;
@@ -133,28 +205,28 @@ public:
accelSample[0]
= (calibration.A_Ainv[0][0] * tmp[0] + calibration.A_Ainv[0][1] * tmp[1]
+ calibration.A_Ainv[0][2] * tmp[2])
* AScale;
* Consts::AScale;
accelSample[1]
= (calibration.A_Ainv[1][0] * tmp[0] + calibration.A_Ainv[1][1] * tmp[1]
+ calibration.A_Ainv[1][2] * tmp[2])
* AScale;
* Consts::AScale;
accelSample[2]
= (calibration.A_Ainv[2][0] * tmp[0] + calibration.A_Ainv[2][1] * tmp[1]
+ calibration.A_Ainv[2][2] * tmp[2])
* AScale;
* Consts::AScale;
}
float getAccelTimestep() final { return calibration.A_Ts; }
void scaleGyroSample(sensor_real_t gyroSample[3]) final {
gyroSample[0] = static_cast<sensor_real_t>(
GScale * (gyroSample[0] - calibration.G_off[0])
Consts::GScale * (gyroSample[0] - calibration.G_off[0])
);
gyroSample[1] = static_cast<sensor_real_t>(
GScale * (gyroSample[1] - calibration.G_off[1])
Consts::GScale * (gyroSample[1] - calibration.G_off[1])
);
gyroSample[2] = static_cast<sensor_real_t>(
GScale * (gyroSample[2] - calibration.G_off[2])
Consts::GScale * (gyroSample[2] - calibration.G_off[2])
);
}
@@ -185,15 +257,15 @@ private:
configuration.save();
}
void calibrateGyroOffset(const Base::ReturnLastFn& eatSamplesReturnLast) {
void calibrateGyroOffset() {
if (!toggles.getToggle(SensorToggles::CalibrationEnabled)) {
return;
}
// Wait for sensor to calm down before calibration
logger.info(
"Put down the device and wait for baseline gyro reading calibration (%d "
"seconds)",
"Put down the device and wait for baseline gyro reading calibration "
"(%d seconds)",
GyroCalibDelaySeconds
);
ledManager.on();
@@ -216,7 +288,7 @@ private:
#ifdef ESP8266
ESP.wdtFeed();
#endif
sensor.bulkRead(
sensor.bulkRead({
[](const RawSensorT xyz[3], const sensor_real_t timeDelta) {},
[&sumXYZ,
&sampleCount](const RawSensorT xyz[3], const sensor_real_t timeDelta) {
@@ -225,8 +297,8 @@ private:
sumXYZ[2] += xyz[2];
++sampleCount;
},
[](const int16_t rawTemp, const sensor_real_t timeDelta) {}
);
[](const int16_t rawTemp, const sensor_real_t timeDelta) {},
});
}
ledManager.off();
@@ -244,15 +316,15 @@ private:
);
}
void calibrateAccel(const Base::EatSamplesFn& eatSamplesForSeconds) {
void calibrateAccel() {
if (!toggles.getToggle(SensorToggles::CalibrationEnabled)) {
return;
}
auto magneto = std::make_unique<MagnetoCalibration>();
logger.info(
"Put the device into 6 unique orientations (all sides), leave it still and "
"do not hold/touch for %d seconds each",
"Put the device into 6 unique orientations (all sides), leave it still "
"and do not hold/touch for %d seconds each",
AccelCalibRestSeconds
);
ledManager.on();
@@ -290,17 +362,17 @@ private:
#ifdef ESP8266
ESP.wdtFeed();
#endif
sensor.bulkRead(
sensor.bulkRead({
[&](const RawSensorT xyz[3], const sensor_real_t timeDelta) {
const sensor_real_t scaledData[]
= {static_cast<sensor_real_t>(
AScale * static_cast<sensor_real_t>(xyz[0])
Consts::AScale * static_cast<sensor_real_t>(xyz[0])
),
static_cast<sensor_real_t>(
AScale * static_cast<sensor_real_t>(xyz[1])
Consts::AScale * static_cast<sensor_real_t>(xyz[1])
),
static_cast<sensor_real_t>(
AScale * static_cast<sensor_real_t>(xyz[2])
Consts::AScale * static_cast<sensor_real_t>(xyz[2])
)};
calibrationRestDetection.updateAcc(IMU::AccTs, scaledData);
@@ -347,8 +419,8 @@ private:
}
},
[](const RawSensorT xyz[3], const sensor_real_t timeDelta) {},
[](const int16_t rawTemp, const sensor_real_t timeDelta) {}
);
[](const int16_t rawTemp, const sensor_real_t timeDelta) {},
});
}
ledManager.off();
logger.debug("Calculating accelerometer calibration data...");
@@ -376,7 +448,7 @@ private:
logger.debug("}");
}
void calibrateSampleRate(const Base::EatSamplesFn& eatSamplesForSeconds) {
void calibrateSampleRate() {
logger.debug(
"Calibrating IMU sample rate in %d second(s)...",
SampleRateCalibDelaySeconds
@@ -392,7 +464,7 @@ private:
logger.debug("Counting samples now...");
uint32_t currentTime;
while ((currentTime = millis()) < calibTarget) {
sensor.bulkRead(
sensor.bulkRead({
[&](const RawSensorT xyz[3], const sensor_real_t timeDelta) {
accelSamples++;
},
@@ -401,8 +473,8 @@ private:
},
[&](const int16_t rawTemp, const sensor_real_t timeDelta) {
tempSamples++;
}
);
},
});
yield();
}
@@ -423,7 +495,8 @@ private:
= millisFromStart / (static_cast<float>(tempSamples) * 1000.0f);
logger.debug(
"Gyro frequency %fHz, accel frequency: %fHz, temperature frequency: %fHz",
"Gyro frequency %fHz, accel frequency: %fHz, temperature frequency: "
"%fHz",
1.0 / calibration.G_Ts,
1.0 / calibration.A_Ts,
1.0 / calibration.T_Ts
@@ -453,12 +526,10 @@ private:
};
private:
using Base::AScale;
using Base::GScale;
using Base::logger;
using Base::sensor;
using Base::sensorId;
using Base::toggles;
};
} // namespace SlimeVR::Sensor
} // namespace SlimeVR::Sensors

View File

@@ -31,6 +31,7 @@
#include "../../../sensorinterface/RegisterInterface.h"
#include "bmi270fw.h"
#include "callbacks.h"
#include "vqf.h"
namespace SlimeVR::Sensors::SoftFusion::Drivers {
@@ -433,12 +434,7 @@ struct BMI270 {
return to_ret;
}
template <typename AccelCall, typename GyroCall, typename TempCall>
void bulkRead(
AccelCall&& processAccelSample,
GyroCall&& processGyroSample,
TempCall&& processTempSample
) {
void bulkRead(DriverCallbacks<int16_t>&& callbacks) {
const auto fifo_bytes = m_RegisterInterface.readReg16(Regs::FifoCount);
const auto bytes_to_read = std::min(
@@ -480,7 +476,7 @@ struct BMI270 {
static_cast<int32_t>(ShortLimit::min()),
static_cast<int32_t>(ShortLimit::max())
);
processGyroSample(gyro, GyrTs);
callbacks.processGyroSample(gyro, GyrTs);
}
if (header & Fifo::AccelDataBit) {
@@ -488,7 +484,7 @@ struct BMI270 {
accel[0] = getFromFifo<uint16_t>(i, read_buffer);
accel[1] = getFromFifo<uint16_t>(i, read_buffer);
accel[2] = getFromFifo<uint16_t>(i, read_buffer);
processAccelSample(accel, AccTs);
callbacks.processAccelSample(accel, AccTs);
}
}
}

View File

@@ -0,0 +1,34 @@
/*
SlimeVR Code is placed under the MIT license
Copyright (c) 2025 Gorbit99 & SlimeVR Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
#pragma once
#include <cstdint>
#include <functional>
template <typename SampleType>
struct DriverCallbacks {
std::function<void(const SampleType sample[3], float AccTs)> processAccelSample;
std::function<void(const SampleType sample[3], float GyrTs)> processGyroSample;
std::function<void(int16_t sample, float TempTs)> processTempSample;
};

View File

@@ -27,6 +27,7 @@
#include <array>
#include <cstdint>
#include "callbacks.h"
#include "vqf.h"
namespace SlimeVR::Sensors::SoftFusion::Drivers {
@@ -50,8 +51,6 @@ struct ICM42688 {
static constexpr float GyroSensitivity = 32.8f;
static constexpr float AccelSensitivity = 4096.0f;
static constexpr bool Uses32BitSensorData = true;
static constexpr float TemperatureBias = 25.0f;
static constexpr float TemperatureSensitivity = 2.07f;
@@ -159,12 +158,7 @@ struct ICM42688 {
return true;
}
template <typename AccelCall, typename GyroCall, typename TempCall>
void bulkRead(
AccelCall&& processAccelSample,
GyroCall&& processGyroSample,
TempCall&& processTemperatureSample
) {
void bulkRead(DriverCallbacks<int32_t>&& callbacks) {
const auto fifo_bytes = m_RegisterInterface.readReg16(Regs::FifoCount);
std::array<uint8_t, FullFifoEntrySize * 8> read_buffer; // max 8 readings
@@ -188,7 +182,7 @@ struct ICM42688 {
static_cast<int32_t>(entry.part.gyro[1]) << 4 | (entry.part.ylsb & 0xf),
static_cast<int32_t>(entry.part.gyro[2]) << 4 | (entry.part.zlsb & 0xf),
};
processGyroSample(gyroData, GyrTs);
callbacks.processGyroSample(gyroData, GyrTs);
if (entry.part.accel[0] != -32768) {
const int32_t accelData[3]{
@@ -199,11 +193,14 @@ struct ICM42688 {
static_cast<int32_t>(entry.part.accel[2]) << 4
| (static_cast<int32_t>(entry.part.zlsb) & 0xf0 >> 4),
};
processAccelSample(accelData, AccTs);
callbacks.processAccelSample(accelData, AccTs);
}
if (entry.part.temp != 0x8000) {
processTemperatureSample(static_cast<int16_t>(entry.part.temp), TempTs);
callbacks.processTempSample(
static_cast<int16_t>(entry.part.temp),
TempTs
);
}
}
}

View File

@@ -25,6 +25,8 @@
#include <cstdint>
#include "../../../sensorinterface/RegisterInterface.h"
#include "callbacks.h"
#include "sensors/softfusion/magdriver.h"
namespace SlimeVR::Sensors::SoftFusion::Drivers {
@@ -52,8 +54,6 @@ struct ICM45Base {
static constexpr float TemperatureZROChange = 20.0f;
static constexpr bool Uses32BitSensorData = true;
RegisterInterface& m_RegisterInterface;
SlimeVR::Logging::Logger& m_Logger;
ICM45Base(RegisterInterface& registerInterface, SlimeVR::Logging::Logger& logger)
@@ -105,6 +105,77 @@ struct ICM45Base {
static constexpr uint8_t FifoCount = 0x12;
static constexpr uint8_t FifoData = 0x14;
// Indirect Register Access
static constexpr uint32_t IRegWaitTimeMicros = 4;
enum class Bank : uint8_t {
IMemSram = 0x00,
IPregBar = 0xa0,
IPregSys1 = 0xa4,
IPregSys2 = 0xa5,
IPregTop1 = 0xa2,
};
static constexpr uint8_t IRegAddr = 0x7c;
static constexpr uint8_t IRegData = 0x7e;
// Mag Support
struct IOCPadScenarioAuxOvrd {
static constexpr uint8_t reg = 0x30;
static constexpr uint8_t value = (0b1 << 4) // Enable AUX1 override
| (0b01 << 2) // Enable I2CM master
| (0b1 << 1) // Enable AUX1 enable override
| (0b1 << 0); // Enable AUX1
};
struct I2CMCommand0 {
static constexpr Bank bank = Bank::IPregTop1;
static constexpr uint8_t reg = 0x06;
};
struct I2CMDevProfile0 {
static constexpr Bank bank = Bank::IPregTop1;
static constexpr uint8_t reg = 0x0e;
};
struct I2CMDevProfile1 {
static constexpr Bank bank = Bank::IPregTop1;
static constexpr uint8_t reg = 0x0f;
};
struct I2CMWrData0 {
static constexpr Bank bank = Bank::IPregTop1;
static constexpr uint8_t reg = 0x33;
};
struct I2CMRdData0 {
static constexpr Bank bank = Bank::IPregTop1;
static constexpr uint8_t reg = 0x1b;
};
struct DmpExtSenOdrCfg {
// TODO: todo
};
struct I2CMControl {
static constexpr Bank bank = Bank::IPregTop1;
static constexpr uint8_t reg = 0x16;
};
struct I2CMStatus {
static constexpr Bank bank = Bank::IPregTop1;
static constexpr uint8_t reg = 0x18;
static constexpr uint8_t SDAErr = 0b1 << 5;
static constexpr uint8_t SCLErr = 0b1 << 4;
static constexpr uint8_t SRSTErr = 0b1 << 3;
static constexpr uint8_t TimeoutErr = 0b1 << 2;
static constexpr uint8_t Done = 0b1 << 1;
static constexpr uint8_t Busy = 0b1 << 0;
};
};
#pragma pack(push, 1)
@@ -149,51 +220,50 @@ struct ICM45Base {
BaseRegs::PwrMgmt0::reg,
BaseRegs::PwrMgmt0::value
);
m_RegisterInterface.writeReg(
BaseRegs::IOCPadScenarioAuxOvrd::reg,
BaseRegs::IOCPadScenarioAuxOvrd::value
);
read_buffer.resize(FullFifoEntrySize * MaxReadings);
delay(1);
return true;
}
template <typename AccelCall, typename GyroCall, typename TempCall>
void bulkRead(
AccelCall&& processAccelSample,
GyroCall&& processGyroSample,
TempCall&& processTemperatureSample
) {
// Allocate statically so that it does not take up stack space, which
// can result in stack overflow and panic
constexpr size_t MaxReadings = 8;
static std::array<uint8_t, FullFifoEntrySize * MaxReadings> read_buffer;
static constexpr size_t MaxReadings = 8;
// Allocate on heap so that it does not take up stack space, which can result in
// stack overflow and panic
std::vector<uint8_t> read_buffer;
void bulkRead(DriverCallbacks<int32_t>&& callbacks) {
constexpr int16_t InvalidReading = -32768;
size_t fifo_packets = m_RegisterInterface.readReg16(BaseRegs::FifoCount);
if (fifo_packets >= 1) {
//
// AN-000364
// 2.16 FIFO EMPTY EVENT IN STREAMING MODE CAN CORRUPT FIFO DATA
//
// Description: When in FIFO streaming mode, a FIFO empty event
// (caused by host reading the last byte of the last FIFO frame) can
// cause FIFO data corruption in the first FIFO frame that arrives
// after the FIFO empty condition. Once the issue is triggered, the
// FIFO state is compromised and cannot recover. FIFO must be set in
// bypass mode to flush out the wrong state
//
// When operating in FIFO streaming mode, if FIFO threshold
// interrupt is triggered with M number of FIFO frames accumulated
// in the FIFO buffer, the host should only read the first M-1
// number of FIFO frames. This prevents the FIFO empty event, that
// can cause FIFO data corruption, from happening.
//
--fifo_packets;
}
if (fifo_packets == 0) {
if (fifo_packets <= 1) {
return;
}
// AN-000364
// 2.16 FIFO EMPTY EVENT IN STREAMING MODE CAN CORRUPT FIFO DATA
//
// Description: When in FIFO streaming mode, a FIFO empty event
// (caused by host reading the last byte of the last FIFO frame) can
// cause FIFO data corruption in the first FIFO frame that arrives
// after the FIFO empty condition. Once the issue is triggered, the
// FIFO state is compromised and cannot recover. FIFO must be set in
// bypass mode to flush out the wrong state
//
// When operating in FIFO streaming mode, if FIFO threshold
// interrupt is triggered with M number of FIFO frames accumulated
// in the FIFO buffer, the host should only read the first M-1
// number of FIFO frames. This prevents the FIFO empty event, that
// can cause FIFO data corruption, from happening.
--fifo_packets;
fifo_packets = std::min(fifo_packets, MaxReadings);
size_t bytes_to_read = fifo_packets * FullFifoEntrySize;
@@ -218,7 +288,7 @@ struct ICM45Base {
static_cast<int32_t>(entry.gyro[1]) << 4 | (entry.lsb[1] & 0xf),
static_cast<int32_t>(entry.gyro[2]) << 4 | (entry.lsb[2] & 0xf),
};
processGyroSample(gyroData, GyrTs);
callbacks.processGyroSample(gyroData, GyrTs);
}
if (has_accel && entry.accel[0] != InvalidReading) {
@@ -230,14 +300,138 @@ struct ICM45Base {
static_cast<int32_t>(entry.accel[2]) << 4
| (static_cast<int32_t>((entry.lsb[2]) & 0xf0) >> 4),
};
processAccelSample(accelData, AccTs);
callbacks.processAccelSample(accelData, AccTs);
}
if (entry.temp != 0x8000) {
processTemperatureSample(static_cast<int16_t>(entry.temp), TempTs);
callbacks.processTempSample(static_cast<int16_t>(entry.temp), TempTs);
}
}
}
template <typename Reg>
uint8_t readBankRegister() {
uint8_t buffer;
readBankRegister<Reg>(&buffer, sizeof(buffer));
return buffer;
}
template <typename Reg, typename T>
void readBankRegister(T* buffer, size_t length) {
uint8_t data[] = {
static_cast<uint8_t>(Reg::bank),
Reg::reg,
};
auto* bufferBytes = reinterpret_cast<uint8_t*>(buffer);
m_RegisterInterface.writeBytes(BaseRegs::IRegAddr, sizeof(data), data);
delayMicroseconds(BaseRegs::IRegWaitTimeMicros);
for (size_t i = 0; i < length * sizeof(T); i++) {
bufferBytes[i] = m_RegisterInterface.readReg(BaseRegs::IRegData);
delayMicroseconds(BaseRegs::IRegWaitTimeMicros);
}
}
template <typename Reg>
void writeBankRegister() {
writeBankRegister<Reg>(&Reg::value, sizeof(Reg::value));
}
template <typename Reg, typename T>
void writeBankRegister(T* buffer, size_t length) {
auto* bufferBytes = reinterpret_cast<uint8_t*>(buffer);
uint8_t data[] = {
static_cast<uint8_t>(Reg::bank),
Reg::reg,
bufferBytes[0],
};
m_RegisterInterface.writeBytes(BaseRegs::IRegAddr, sizeof(data), data);
delayMicroseconds(BaseRegs::IRegWaitTimeMicros);
for (size_t i = 1; i < length * sizeof(T); i++) {
m_RegisterInterface.writeReg(BaseRegs::IRegData, bufferBytes[i]);
delayMicroseconds(BaseRegs::IRegWaitTimeMicros);
}
}
template <typename Reg>
void writeBankRegister(uint8_t value) {
writeBankRegister<Reg>(&value, sizeof(value));
}
void setAuxId(uint8_t deviceId) {
writeBankRegister<typename BaseRegs::I2CMDevProfile1>(deviceId);
}
uint8_t readAux(uint8_t address) {
writeBankRegister<typename BaseRegs::I2CMDevProfile0>(address);
writeBankRegister<typename BaseRegs::I2CMCommand0>(
(0b1 << 7) // Last transaction
| (0b0 << 6) // Channel 0
| (0b01 << 4) // Read with register
| (0b0001 << 0) // Read 1 byte
);
writeBankRegister<typename BaseRegs::I2CMControl>(
(0b0 << 6) // No restarts
| (0b0 << 3) // Fast mode
| (0b1 << 0) // Start transaction
);
uint8_t lastStatus;
while ((lastStatus = readBankRegister<typename BaseRegs::I2CMStatus>())
& BaseRegs::I2CMStatus::Busy)
;
if (lastStatus != BaseRegs::I2CMStatus::Done) {
m_Logger.error(
"Aux read from address 0x%02x returned status 0x%02x",
address,
lastStatus
);
}
return readBankRegister<typename BaseRegs::I2CMRdData0>();
}
void writeAux(uint8_t address, uint8_t value) {
writeBankRegister<typename BaseRegs::I2CMDevProfile0>(address);
writeBankRegister<typename BaseRegs::I2CMWrData0>(value);
writeBankRegister<typename BaseRegs::I2CMCommand0>(
(0b1 << 7) // Last transaction
| (0b0 << 6) // Channel 0
| (0b01 << 4) // Read with register
| (0b0001 << 0) // Read 1 byte
);
writeBankRegister<typename BaseRegs::I2CMControl>(
(0b0 << 6) // No restarts
| (0b0 << 3) // Fast mode
| (0b1 << 0) // Start transaction
);
uint8_t lastStatus;
while ((lastStatus = readBankRegister<typename BaseRegs::I2CMStatus>())
& BaseRegs::I2CMStatus::Busy)
;
if (lastStatus != BaseRegs::I2CMStatus::Done) {
m_Logger.error(
"Aux write to address 0x%02x with value 0x%02x returned status 0x%02x",
address,
value,
lastStatus
);
}
}
void startAuxPolling(uint8_t dataReg, MagDataWidth dataWidth) {
// TODO:
}
void stopAuxPolling() {
// TODO:
}
};
}; // namespace SlimeVR::Sensors::SoftFusion::Drivers

View File

@@ -28,6 +28,7 @@
#include <cstdint>
#include "../../../sensorinterface/RegisterInterface.h"
#include "callbacks.h"
namespace SlimeVR::Sensors::SoftFusion::Drivers {
@@ -53,11 +54,9 @@ struct LSM6DSOutputHandler {
static constexpr size_t FullFifoEntrySize = sizeof(FifoEntryAligned) + 1;
template <typename AccelCall, typename GyroCall, typename TempCall, typename Regs>
template <typename Regs>
void bulkRead(
AccelCall& processAccelSample,
GyroCall& processGyroSample,
TempCall& processTempSample,
DriverCallbacks<int16_t>&& callbacks,
float GyrTs,
float AccTs,
float TempTs
@@ -94,13 +93,13 @@ struct LSM6DSOutputHandler {
switch (tag) {
case 0x01: // Gyro NC
processGyroSample(entry.xyz, GyrTs);
callbacks.processGyroSample(entry.xyz, GyrTs);
break;
case 0x02: // Accel NC
processAccelSample(entry.xyz, AccTs);
callbacks.processAccelSample(entry.xyz, AccTs);
break;
case 0x03: // Temperature
processTempSample(entry.xyz[0], TempTs);
callbacks.processTempSample(entry.xyz[0], TempTs);
break;
}
}

View File

@@ -28,6 +28,7 @@
#include <cstdint>
#include "../../../sensorinterface/RegisterInterface.h"
#include "callbacks.h"
#include "vqf.h"
namespace SlimeVR::Sensors::SoftFusion::Drivers {
@@ -123,12 +124,7 @@ struct LSM6DS3TRC {
return true;
}
template <typename AccelCall, typename GyroCall, typename TempCall>
void bulkRead(
AccelCall&& processAccelSample,
GyroCall&& processGyroSample,
TempCall&& processTemperatureSample
) {
void bulkRead(DriverCallbacks<int16_t>&& callbacks) {
const auto read_result = m_RegisterInterface.readReg16(Regs::FifoStatus);
if (read_result & 0x4000) { // overrun!
// disable and re-enable fifo to clear it
@@ -158,14 +154,11 @@ struct LSM6DS3TRC {
);
for (uint16_t i = 0; i < bytes_to_read / sizeof(uint16_t);
i += single_measurement_words) {
processGyroSample(reinterpret_cast<const int16_t*>(&read_buffer[i]), GyrTs);
processAccelSample(
reinterpret_cast<const int16_t*>(&read_buffer[i + 3]),
AccTs
);
processTemperatureSample(read_buffer[i + 9], TempTs);
callbacks.processGyroSample(&read_buffer[i], GyrTs);
callbacks.processAccelSample(&read_buffer[i + 3], AccTs);
callbacks.processTempSample(read_buffer[i + 9], TempTs);
}
}
};
}; // namespace SlimeVR::Sensors::SoftFusion::Drivers
} // namespace SlimeVR::Sensors::SoftFusion::Drivers

View File

@@ -27,6 +27,7 @@
#include <array>
#include <cstdint>
#include "callbacks.h"
#include "lsm6ds-common.h"
#include "vqf.h"
@@ -122,16 +123,9 @@ struct LSM6DSO : LSM6DSOutputHandler {
return true;
}
template <typename AccelCall, typename GyroCall, typename TempCall>
void bulkRead(
AccelCall&& processAccelSample,
GyroCall&& processGyroSample,
TempCall&& processTempSample
) {
LSM6DSOutputHandler::template bulkRead<AccelCall, GyroCall, TempCall, Regs>(
processAccelSample,
processGyroSample,
processTempSample,
void bulkRead(DriverCallbacks<int16_t>&& callbacks) {
LSM6DSOutputHandler::template bulkRead<Regs>(
std::move(callbacks),
GyrTs,
AccTs,
TempTs

View File

@@ -121,16 +121,9 @@ struct LSM6DSR : LSM6DSOutputHandler {
return true;
}
template <typename AccelCall, typename GyroCall, typename TempCall>
void bulkRead(
AccelCall&& processAccelSample,
GyroCall&& processGyroSample,
TempCall&& processTempSample
) {
LSM6DSOutputHandler::template bulkRead<AccelCall, GyroCall, TempCall, Regs>(
processAccelSample,
processGyroSample,
processTempSample,
void bulkRead(DriverCallbacks<int16_t>&& callbacks) {
LSM6DSOutputHandler::template bulkRead<Regs>(
std::move(callbacks),
GyrTs,
AccTs,
TempTs

View File

@@ -137,16 +137,9 @@ struct LSM6DSV : LSM6DSOutputHandler {
return true;
}
template <typename AccelCall, typename GyroCall, typename TempCall>
void bulkRead(
AccelCall&& processAccelSample,
GyroCall&& processGyroSample,
TempCall&& processTempSample
) {
LSM6DSOutputHandler::bulkRead<AccelCall, GyroCall, TempCall, Regs>(
processAccelSample,
processGyroSample,
processTempSample,
void bulkRead(DriverCallbacks<int16_t>&& callbacks) {
LSM6DSOutputHandler::template bulkRead<Regs>(
std::move(callbacks),
GyrTs,
AccTs,
TempTs

View File

@@ -30,6 +30,7 @@
#include <cstdint>
#include "../../../sensorinterface/RegisterInterface.h"
#include "callbacks.h"
#include "vqf.h"
namespace SlimeVR::Sensors::SoftFusion::Drivers {
@@ -181,12 +182,7 @@ struct MPU6050 {
return result;
}
template <typename AccelCall, typename GyroCall, typename TempCall>
void bulkRead(
AccelCall&& processAccelSample,
GyroCall&& processGyroSample,
TempCall&& processTempSample
) {
void bulkRead(DriverCallbacks<int16_t>&& callbacks) {
const auto status = m_RegisterInterface.readReg(Regs::IntStatus);
if (status & (1 << MPU6050_INTERRUPT_FIFO_OFLOW_BIT)) {
@@ -216,14 +212,14 @@ struct MPU6050 {
xyz[0] = MPU6050_FIFO_VALUE(sample, accel_x);
xyz[1] = MPU6050_FIFO_VALUE(sample, accel_y);
xyz[2] = MPU6050_FIFO_VALUE(sample, accel_z);
processAccelSample(xyz, AccTs);
callbacks.processAccelSample(xyz, AccTs);
xyz[0] = MPU6050_FIFO_VALUE(sample, gyro_x);
xyz[1] = MPU6050_FIFO_VALUE(sample, gyro_y);
xyz[2] = MPU6050_FIFO_VALUE(sample, gyro_z);
processGyroSample(xyz, GyrTs);
callbacks.processGyroSample(xyz, GyrTs);
}
}
};
}; // namespace SlimeVR::Sensors::SoftFusion::Drivers
} // namespace SlimeVR::Sensors::SoftFusion::Drivers

View File

@@ -0,0 +1,68 @@
/*
SlimeVR Code is placed under the MIT license
Copyright (c) 2025 Gorbit99 & SlimeVR Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
#pragma once
#include <array>
#include <cstdint>
#include <type_traits>
#include "../../motionprocessing/types.h"
#include "drivers/callbacks.h"
template <typename IMU>
struct IMUConsts {
static constexpr bool Uses32BitSensorData
= requires(IMU& i, DriverCallbacks<int32_t> callbacks) {
i.bulkRead(std::move(callbacks));
};
static constexpr bool DirectTempReadOnly = requires(IMU& i) { i.getDirectTemp(); };
using RawSensorT =
typename std::conditional<Uses32BitSensorData, int32_t, int16_t>::type;
using RawVectorT = std::array<RawSensorT, 3>;
static constexpr float GScale
= ((32768. / IMU::GyroSensitivity) / 32768.) * (PI / 180.0);
static constexpr float AScale = CONST_EARTH_GRAVITY / IMU::AccelSensitivity;
static constexpr float DirectTempReadFreq = 15;
static constexpr float DirectTempReadTs = 1.0f / DirectTempReadFreq;
static constexpr sensor_real_t getDefaultTempTs() {
if constexpr (DirectTempReadOnly) {
return DirectTempReadTs;
} else {
return IMU::TempTs;
}
}
static constexpr bool SupportsMags = requires(IMU& i) { i.readAux(0x00); };
static constexpr bool Supports9ByteMag = []() constexpr {
if constexpr (requires { IMU::Supports9ByteMag; }) {
return IMU::Supports9ByteMag;
} else {
return true;
}
}();
};

View File

@@ -0,0 +1,132 @@
/*
SlimeVR Code is placed under the MIT license
Copyright (c) 2025 Gorbit99 & SlimeVR Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
#include "magdriver.h"
namespace SlimeVR::Sensors::SoftFusion {
std::vector<MagDefinition> MagDriver::supportedMags{
MagDefinition{
.name = "QMC6309",
.deviceId = 0x7c,
.whoAmIReg = 0x00,
.expectedWhoAmI = 0x90,
.dataWidth = MagDataWidth::SixByte,
.dataReg = 0x01,
.setup =
[](MagInterface& interface) {
interface.writeByte(0x0b, 0x80);
interface.writeByte(0x0b, 0x00); // Soft reset
delay(10);
interface.writeByte(0x0b, 0x48); // Set/reset on, 8g full range, 200Hz
interface.writeByte(
0x0a,
0x21
); // LP filter 2, 8x Oversampling, normal mode
return true;
},
},
MagDefinition{
.name = "IST8306",
.deviceId = 0x19,
.whoAmIReg = 0x00,
.expectedWhoAmI = 0x06,
.dataWidth = MagDataWidth::SixByte,
.dataReg = 0x11,
.setup =
[](MagInterface& interface) {
interface.writeByte(0x32, 0x01); // Soft reset
delay(50);
interface.writeByte(0x30, 0x20); // Noise suppression: low
interface.writeByte(0x41, 0x2d); // Oversampling: 32X
interface.writeByte(0x31, 0x02); // Continuous measurement @ 10Hz
return true;
},
},
};
bool MagDriver::init(MagInterface&& interface, bool supports9ByteMags) {
for (auto& mag : supportedMags) {
interface.setDeviceId(mag.deviceId);
logger.info("Trying mag %s!", mag.name);
uint8_t whoAmI = interface.readByte(mag.whoAmIReg);
if (whoAmI != mag.expectedWhoAmI) {
continue;
}
if (!supports9ByteMags && mag.dataWidth == MagDataWidth::NineByte) {
logger.error("The sensor doesn't support this mag!");
return false;
}
logger.info("Found mag %s! Initializing", mag.name);
if (!mag.setup(interface)) {
logger.error("Mag %s failed to initialize!", mag.name);
return false;
}
detectedMag = mag;
break;
}
this->interface = interface;
return detectedMag.has_value();
}
void MagDriver::startPolling() const {
if (!detectedMag) {
return;
}
interface.startPolling(detectedMag->dataReg, detectedMag->dataWidth);
}
void MagDriver::stopPolling() const {
if (!detectedMag) {
return;
}
interface.stopPolling();
}
const char* MagDriver::getAttachedMagName() const {
if (!detectedMag) {
return nullptr;
}
return detectedMag->name;
}
} // namespace SlimeVR::Sensors::SoftFusion

View File

@@ -0,0 +1,77 @@
/*
SlimeVR Code is placed under the MIT license
Copyright (c) 2025 Gorbit99 & SlimeVR Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
#pragma once
#include <functional>
#include <optional>
#include "logging/Logger.h"
#include "sensorinterface/RegisterInterface.h"
namespace SlimeVR::Sensors::SoftFusion {
enum class MagDataWidth {
SixByte,
NineByte,
};
struct MagInterface {
std::function<uint8_t(uint8_t)> readByte;
std::function<void(uint8_t, uint8_t)> writeByte;
std::function<void(uint8_t)> setDeviceId;
std::function<void(uint8_t, MagDataWidth)> startPolling;
std::function<void()> stopPolling;
};
struct MagDefinition {
const char* name;
uint8_t deviceId;
uint8_t whoAmIReg;
uint8_t expectedWhoAmI;
MagDataWidth dataWidth;
uint8_t dataReg;
std::function<bool(MagInterface& interface)> setup;
};
class MagDriver {
public:
bool init(MagInterface&& interface, bool supports9ByteMags);
void startPolling() const;
void stopPolling() const;
[[nodiscard]] const char* getAttachedMagName() const;
private:
std::optional<MagDefinition> detectedMag;
MagInterface interface;
static std::vector<MagDefinition> supportedMags;
Logging::Logger logger{"MagDriver"};
};
} // namespace SlimeVR::Sensors::SoftFusion

View File

@@ -41,27 +41,27 @@
namespace SlimeVR::Sensors::RuntimeCalibration {
template <typename IMU, typename RawSensorT, typename RawVectorT>
class RuntimeCalibrator : public Sensor::CalibrationBase<IMU, RawSensorT, RawVectorT> {
template <typename IMU>
class RuntimeCalibrator : public Sensors::CalibrationBase<IMU> {
public:
static constexpr bool HasUpsideDownCalibration = false;
using Base = Sensor::CalibrationBase<IMU, RawSensorT, RawVectorT>;
using Self = RuntimeCalibrator<IMU, RawSensorT, RawVectorT>;
using Base = Sensors::CalibrationBase<IMU>;
using Self = RuntimeCalibrator<IMU>;
using Consts = typename Base::Consts;
using RawSensorT = typename Consts::RawSensorT;
using RawVectorT = typename Consts::RawVectorT;
RuntimeCalibrator(
SensorFusion& fusion,
IMU& imu,
uint8_t sensorId,
Logging::Logger& logger,
float TempTs,
float AScale,
float GScale,
SensorToggleState& toggles
)
: Base{fusion, imu, sensorId, logger, TempTs, AScale, GScale, toggles} {
calibration.T_Ts = TempTs;
activeCalibration.T_Ts = TempTs;
: Base{fusion, imu, sensorId, logger, toggles} {
calibration.T_Ts = Consts::getDefaultTempTs();
activeCalibration.T_Ts = Consts::getDefaultTempTs();
}
bool calibrationMatches(const Configuration::SensorConfig& sensorCalibration
@@ -152,22 +152,22 @@ public:
}
void scaleAccelSample(sensor_real_t accelSample[3]) final {
accelSample[0] = accelSample[0] * AScale - activeCalibration.A_off[0];
accelSample[1] = accelSample[1] * AScale - activeCalibration.A_off[1];
accelSample[2] = accelSample[2] * AScale - activeCalibration.A_off[2];
accelSample[0] = accelSample[0] * Consts::AScale - activeCalibration.A_off[0];
accelSample[1] = accelSample[1] * Consts::AScale - activeCalibration.A_off[1];
accelSample[2] = accelSample[2] * Consts::AScale - activeCalibration.A_off[2];
}
float getAccelTimestep() final { return activeCalibration.A_Ts; }
void scaleGyroSample(sensor_real_t gyroSample[3]) final {
gyroSample[0] = static_cast<sensor_real_t>(
GScale * (gyroSample[0] - activeCalibration.G_off1[0])
Consts::GScale * (gyroSample[0] - activeCalibration.G_off1[0])
);
gyroSample[1] = static_cast<sensor_real_t>(
GScale * (gyroSample[1] - activeCalibration.G_off1[1])
Consts::GScale * (gyroSample[1] - activeCalibration.G_off1[1])
);
gyroSample[2] = static_cast<sensor_real_t>(
GScale * (gyroSample[2] - activeCalibration.G_off1[2])
Consts::GScale * (gyroSample[2] - activeCalibration.G_off1[2])
);
}
@@ -202,12 +202,12 @@ public:
activeZROChange = IMU::TemperatureZROChange;
}
float diffX
= (activeCalibration.G_off2[0] - activeCalibration.G_off1[0]) * GScale;
float diffY
= (activeCalibration.G_off2[1] - activeCalibration.G_off1[1]) * GScale;
float diffZ
= (activeCalibration.G_off2[2] - activeCalibration.G_off1[2]) * GScale;
float diffX = (activeCalibration.G_off2[0] - activeCalibration.G_off1[0])
* Consts::GScale;
float diffY = (activeCalibration.G_off2[1] - activeCalibration.G_off1[1])
* Consts::GScale;
float diffZ = (activeCalibration.G_off2[2] - activeCalibration.G_off1[2])
* Consts::GScale;
float maxDiff
= std::max(std::max(std::abs(diffX), std::abs(diffY)), std::abs(diffZ));
@@ -402,7 +402,7 @@ private:
GyroBiasCalibrationStep<RawSensorT> gyroBiasCalibrationStep{calibration};
AccelBiasCalibrationStep<RawSensorT> accelBiasCalibrationStep{
calibration,
static_cast<float>(Base::AScale)
static_cast<float>(Consts::AScale)
};
NullCalibrationStep<RawSensorT> nullCalibrationStep{calibration};
@@ -440,9 +440,7 @@ private:
Configuration::RuntimeCalibrationSensorConfig activeCalibration = calibration;
using Base::AScale;
using Base::fusion;
using Base::GScale;
using Base::logger;
using Base::sensor;
using Base::sensorId;

View File

@@ -23,54 +23,31 @@
#pragma once
#include <cstdint>
#include <cstdlib>
#include <tuple>
#include <PinInterface.h>
#include "../../sensorinterface/i2cimpl.h"
#include <cstdint>
#include <cstring>
#include "../../GlobalVars.h"
#include "../../sensorinterface/SensorInterface.h"
#include "../RestCalibrationDetector.h"
#include "../sensor.h"
#include "GlobalVars.h"
#include "PinInterface.h"
#include "TempGradientCalculator.h"
#include "imuconsts.h"
#include "motionprocessing/types.h"
#include "sensors/SensorFusion.h"
#include "sensors/softfusion/TempGradientCalculator.h"
#include "sensors/softfusion/magdriver.h"
namespace SlimeVR::Sensors {
template <
typename SensorType,
template <typename IMU, typename RawSensorT, typename RawVectorT>
typename Calibrator>
template <typename SensorType, template <typename IMU> typename Calibrator>
class SoftFusionSensor : public Sensor {
static constexpr sensor_real_t getDefaultTempTs() {
if constexpr (DirectTempReadOnly) {
return DirectTempReadTs;
} else {
return SensorType::TempTs;
}
}
static constexpr bool Uses32BitSensorData
= requires(SensorType& i) { i.Uses32BitSensorData; };
static constexpr bool DirectTempReadOnly
= requires(SensorType& i) { i.getDirectTemp(); };
using RawSensorT =
typename std::conditional<Uses32BitSensorData, int32_t, int16_t>::type;
using RawVectorT = std::array<RawSensorT, 3>;
static constexpr float GScale
= ((32768. / SensorType::GyroSensitivity) / 32768.) * (PI / 180.0);
static constexpr float AScale = CONST_EARTH_GRAVITY / SensorType::AccelSensitivity;
using Calib = Calibrator<SensorType, RawSensorT, RawVectorT>;
using Consts = IMUConsts<SensorType>;
using RawSensorT = typename Consts::RawSensorT;
using Calib = Calibrator<SensorType>;
static constexpr auto UpsideDownCalibrationInit = Calib::HasUpsideDownCalibration;
static constexpr float DirectTempReadFreq = 15;
static constexpr float DirectTempReadTs = 1.0f / DirectTempReadFreq;
float lastReadTemperature = 0;
uint32_t lastTempPollTime = micros();
@@ -107,7 +84,7 @@ class SoftFusionSensor : public Sensor {
}
}
void sendData() {
void sendData() final {
Sensor::sendData();
sendTempIfNeeded();
}
@@ -155,7 +132,7 @@ class SoftFusionSensor : public Sensor {
void
processTempSample(const int16_t rawTemperature, const sensor_real_t timeDelta) {
if constexpr (!DirectTempReadOnly) {
if constexpr (!Consts::DirectTempReadOnly) {
const float scaledTemperature
= SensorType::TemperatureBias
+ static_cast<float>(rawTemperature)
@@ -173,54 +150,6 @@ class SoftFusionSensor : public Sensor {
}
}
void eatSamplesForSeconds(const uint32_t seconds) {
const auto targetDelay = millis() + 1000 * seconds;
auto lastSecondsRemaining = seconds;
while (millis() < targetDelay) {
#ifdef ESP8266
ESP.wdtFeed();
#endif
auto currentSecondsRemaining = (targetDelay - millis()) / 1000;
if (currentSecondsRemaining != lastSecondsRemaining) {
m_Logger.info("%ld...", currentSecondsRemaining + 1);
lastSecondsRemaining = currentSecondsRemaining;
}
m_sensor.bulkRead(
[](const RawSensorT xyz[3], const sensor_real_t timeDelta) {},
[](const RawSensorT xyz[3], const sensor_real_t timeDelta) {},
[](const int16_t xyz, const sensor_real_t timeDelta) {}
);
}
}
std::tuple<RawVectorT, RawVectorT, int16_t> eatSamplesReturnLast(
const uint32_t milliseconds
) {
RawVectorT accel = {0};
RawVectorT gyro = {0};
int16_t temp = 0;
const auto targetDelay = millis() + milliseconds;
while (millis() < targetDelay) {
m_sensor.bulkRead(
[&](const RawSensorT xyz[3], const sensor_real_t timeDelta) {
accel[0] = xyz[0];
accel[1] = xyz[1];
accel[2] = xyz[2];
},
[&](const RawSensorT xyz[3], const sensor_real_t timeDelta) {
gyro[0] = xyz[0];
gyro[1] = xyz[1];
gyro[2] = xyz[2];
},
[&](const int16_t rawTemp, const sensor_real_t timeDelta) {
temp = rawTemp;
}
);
yield();
}
return std::make_tuple(accel, gyro, temp);
}
public:
static constexpr auto TypeID = SensorType::Type;
static constexpr uint8_t Address = SensorType::Address;
@@ -276,12 +205,13 @@ public:
// read fifo updating fusion
uint32_t now = micros();
if constexpr (DirectTempReadOnly) {
if constexpr (Consts::DirectTempReadOnly) {
uint32_t tempElapsed = now - lastTempPollTime;
if (tempElapsed >= DirectTempReadTs * 1e6) {
if (tempElapsed >= Consts::DirectTempReadTs * 1e6) {
lastTempPollTime
= now
- (tempElapsed - static_cast<uint32_t>(DirectTempReadTs * 1e6));
- (tempElapsed
- static_cast<uint32_t>(Consts::DirectTempReadTs * 1e6));
lastReadTemperature = m_sensor.getDirectTemp();
calibrator.provideTempSample(lastReadTemperature);
@@ -289,7 +219,7 @@ public:
if (toggles.getToggle(SensorToggles::TempGradientCalibrationEnabled)) {
tempGradientCalculator.feedSample(
lastReadTemperature,
DirectTempReadTs
Consts::DirectTempReadTs
);
}
}
@@ -308,20 +238,20 @@ public:
// send new fusion values when time is up
now = micros();
constexpr float maxSendRateHz = 100.0f;
constexpr uint32_t sendInterval = 1.0f / maxSendRateHz * 1e6;
constexpr uint32_t sendInterval = 1.0f / maxSendRateHz * 1e6f;
elapsed = now - m_lastRotationPacketSent;
if (elapsed >= sendInterval) {
m_sensor.bulkRead(
[&](const RawSensorT xyz[3], const sensor_real_t timeDelta) {
processAccelSample(xyz, timeDelta);
m_sensor.bulkRead({
[&](const auto sample[3], float AccTs) {
processAccelSample(sample, AccTs);
},
[&](const RawSensorT xyz[3], const sensor_real_t timeDelta) {
processGyroSample(xyz, timeDelta);
[&](const auto sample[3], float GyrTs) {
processGyroSample(sample, GyrTs);
},
[&](const int16_t rawTemp, const sensor_real_t timeDelta) {
processTempSample(rawTemp, timeDelta);
}
);
[&](int16_t sample, float TempTs) {
processTempSample(sample, TempTs);
},
});
if (!m_fusion.isUpdated()) {
checkSensorTimeout();
return;
@@ -395,43 +325,46 @@ public:
m_status = SensorStatus::SENSOR_OK;
working = true;
[[maybe_unused]] auto lastRawSample = eatSamplesReturnLast(1000);
if constexpr (UpsideDownCalibrationInit) {
auto gravity = static_cast<sensor_real_t>(
AScale * static_cast<sensor_real_t>(std::get<0>(lastRawSample)[2])
);
m_Logger.info(
"Gravity read: %.1f (need < -7.5 to start calibration)",
gravity
);
if (gravity < -7.5f) {
ledManager.on();
m_Logger.info("Flip front in 5 seconds to start calibration");
lastRawSample = eatSamplesReturnLast(5000);
gravity = static_cast<sensor_real_t>(
AScale * static_cast<sensor_real_t>(std::get<0>(lastRawSample)[2])
);
if (gravity > 7.5f) {
m_Logger.debug("Starting calibration...");
startCalibration(0);
} else {
m_Logger.info("Flip not detected. Skipping calibration.");
}
ledManager.off();
calibrator.checkStartupCalibration();
if constexpr (Consts::SupportsMags) {
magDriver.init(
SoftFusion::MagInterface{
.readByte
= [&](uint8_t address) { return m_sensor.readAux(address); },
.writeByte = [&](uint8_t address, uint8_t value) {},
.setDeviceId
= [&](uint8_t deviceId) { m_sensor.setAuxId(deviceId); },
.startPolling
= [&](uint8_t dataReg, SoftFusion::MagDataWidth dataWidth
) { m_sensor.startAuxPolling(dataReg, dataWidth); },
.stopPolling = [&]() { m_sensor.stopAuxPolling(); },
},
Consts::Supports9ByteMag
);
if (toggles.getToggle(SensorToggles::MagEnabled)) {
magDriver.startPolling();
}
}
toggles.onToggleChange([&](SensorToggles toggle, bool value) {
if (toggle == SensorToggles::MagEnabled) {
if (value) {
magDriver.startPolling();
} else {
magDriver.stopPolling();
}
}
});
}
void startCalibration(int calibrationType) final {
calibrator.startCalibration(
calibrationType,
[&](const uint32_t seconds) { eatSamplesForSeconds(seconds); },
[&](const uint32_t millis) { return eatSamplesReturnLast(millis); }
);
calibrator.startCalibration(calibrationType);
}
bool isFlagSupported(SensorToggles toggle) const final {
[[nodiscard]] bool isFlagSupported(SensorToggles toggle) const final {
return toggle == SensorToggles::CalibrationEnabled
|| toggle == SensorToggles::TempGradientCalibrationEnabled;
}
@@ -440,16 +373,7 @@ public:
SensorFusion m_fusion;
SensorType m_sensor;
Calib calibrator{
m_fusion,
m_sensor,
sensorId,
m_Logger,
getDefaultTempTs(),
AScale,
GScale,
toggles
};
Calib calibrator{m_fusion, m_sensor, sensorId, m_Logger, toggles};
SensorStatus m_status = SensorStatus::SENSOR_OFFLINE;
uint32_t m_lastPollTime = micros();
@@ -459,6 +383,8 @@ public:
RestCalibrationDetector calibrationDetector;
SoftFusion::MagDriver magDriver;
static bool checkPresent(const RegisterInterface& imuInterface) {
I2Cdev::readTimeout = 100;
auto value = imuInterface.readReg(SensorType::Regs::WhoAmI::reg);
@@ -477,6 +403,10 @@ public:
return false;
}
}
const char* getAttachedMagnetometer() const final {
return magDriver.getAttachedMagName();
}
};
} // namespace SlimeVR::Sensors

View File

@@ -164,6 +164,10 @@ void printState() {
sensor->isWorking() ? "true" : "false",
sensor->getHadData() ? "true" : "false"
);
const char* mag = sensor->getAttachedMagnetometer();
if (mag) {
logger.info("Sensor[%d] magnetometer: %s", sensor->getSensorId(), mag);
}
}
logger.info(
"Battery voltage: %.3f, level: %.1f%%",
@@ -288,6 +292,12 @@ void cmdGet(CmdParser* parser) {
sensor0->isWorking() ? "true" : "false",
sensor0->getHadData() ? "true" : "false"
);
const char* mag = sensor0->getAttachedMagnetometer();
if (mag) {
logger.info("[TEST] Sensor[0] magnetometer: %s", mag);
}
if (!sensor0->getHadData()) {
logger.error("[TEST] Sensor[0] didn't send any data yet!");
} else {