#pragma once

#include "imu_support.hpp"
#include "parsers/davis_parser.hpp"
#include "sync_camera_input_base.hpp"
#include "usb_device.hpp"

namespace dv::io::camera {
class DAVIS final : public USBDevice, public SyncCameraInputBase {
public:
	using AutoExposureCallback = std::function<dv::Duration(const dv::Frame &frame)>;

	enum class Davis240BiasCF {
		Diff                        = 0,
		On                          = 1,
		Off                         = 2,
		ApsCascode                  = 3,
		DiffCascode                 = 4,
		ApsReadoutSourceFollower    = 5,
		LocalBuffer                 = 6,
		PixelInverter               = 7,
		Photoreceptor               = 8,
		PhotoreceptorSourceFollower = 9,
		Refractory                  = 10,
		AERPullDown                 = 11,
		LCOLTimeout                 = 12,
		AERPullUpX                  = 13,
		AERPullUpY                  = 14,
		PadFollower                 = 17,
		ApsOverflowLevel            = 18,
		BiasBuffer                  = 19,
	};

	enum class Davis346BiasVDAC {
		ApsOverflowLevel = 0,
		ApsCascode       = 1,
		ADCReferenceHigh = 2,
		ADCReferenceLow  = 3,
		ADCTestVoltage   = 4,
	};

	enum class Davis346BiasCF {
		LocalBuffer                 = 8,
		PadFollower                 = 9,
		Diff                        = 10,
		On                          = 11,
		Off                         = 12,
		PixelInverter               = 13,
		Photoreceptor               = 14,
		PhotoreceptorSourceFollower = 15,
		Refractory                  = 16,
		ReadoutBuffer               = 17,
		ApsReadoutSourceFollower    = 18,
		ADCComparator               = 19,
		COLSelectLow                = 20,
		DACBuffer                   = 21,
		LCOLTimeout                 = 22,
		AERPullDown                 = 23,
		AERPullUpX                  = 24,
		AERPullUpY                  = 25,
		BiasBuffer                  = 34,
	};

	enum class CDavisBiasVDAC {
		ApsCascode       = 0,
		OVG1Low          = 1,
		OVG2Low          = 2,
		TX2OVG2High      = 3,
		Gnd07            = 4,
		ADCTestVoltage   = 5,
		ADCReferenceHigh = 6,
		ADCReferenceLow  = 7,
	};

	enum class CDavisBiasCF {
		LocalBuffer                 = 10,
		PadFollower                 = 11,
		PixelInverter               = 13,
		Diff                        = 14,
		On                          = 15,
		Off                         = 16,
		Photoreceptor               = 17,
		PhotoreceptorSourceFollower = 18,
		Refractory                  = 19,
		ArrayBiasBuffer             = 20,
		ArrayLogicBuffer            = 22,
		FallTime                    = 23,
		RiseTime                    = 24,
		ReadoutBuffer               = 25,
		ApsReadoutSourceFollower    = 26,
		ADCComparator               = 27,
		DACBuffer                   = 28,
		LCOLTimeout                 = 30,
		AERPullDown                 = 31,
		AERPullUpX                  = 32,
		AERPullUpY                  = 33,
		BiasBuffer                  = 34,
	};

	/**
	 * Find connected DAVIS cameras.
	 * @param filterBySerialNumber only search for devices with this serial number
	 * @param logger discovery logging callback, must be thread-safe
	 * @return a descriptor structure describing a compatible device
	 */
	[[nodiscard]] static auto findDevices(
		const std::string_view filterBySerialNumber = {}, loggerCallbackType logger = {}) {
		const auto found = findCompatibleDevices(COMPATIBLE_CAMERA, std::move(logger));

		return filterCompatibleDevices(found, filterBySerialNumber);
	}

	/**
	 * Open the first DAVIS camera that can be found.
	 * Throws if device cannot be opened.
	 * @param deviceLogger per-device logging callback, must be thread-safe
	 * @param deviceLogLevel initial log-level
	 */
	explicit DAVIS(loggerCallbackType deviceLogger = {}, const LogLevel deviceLogLevel = LogLevel::LVL_WARNING) :
		DAVIS(findDevices().at(0), std::move(deviceLogger), deviceLogLevel) {
	}

	/**
	 * Open the DAVIS camera with the specified serial number.
	 * Throws if device cannot be opened.
	 * @param filterBySerialNumber serial number to search for
	 * @param deviceLogger per-device logging callback, must be thread-safe
	 * @param deviceLogLevel initial log-level
	 */
	explicit DAVIS(const std::string_view filterBySerialNumber, loggerCallbackType deviceLogger = {},
		const LogLevel deviceLogLevel = LogLevel::LVL_WARNING) :
		DAVIS(findDevices(filterBySerialNumber).at(0), std::move(deviceLogger), deviceLogLevel) {
	}

	/**
	 * Open the DAVIS camera corresponding to the specified descriptor.
	 * Throws if device cannot be opened.
	 * @param deviceToOpen device descriptor structure
	 * @param deviceLogger per-device logging callback, must be thread-safe
	 * @param deviceLogLevel initial log-level
	 */
	explicit DAVIS(const DeviceDescriptor &deviceToOpen, loggerCallbackType deviceLogger = {},
		const LogLevel deviceLogLevel = LogLevel::LVL_WARNING) :
		USBDevice(deviceToOpen, std::move(deviceLogger), deviceLogLevel, "davis", "DAVIS", COMPATIBLE_CAMERA,
			(deviceToOpen.pid == PID_DAVIS_FX2) ? (FX2_FIRMWARE_REQUIRED_VERSION) : (FX3_FIRMWARE_REQUIRED_VERSION)) {
		const auto requiredLogicVersion
			= (deviceToOpen.pid == PID_DAVIS_FX2) ? (FX2_LOGIC_REQUIRED_VERSION) : (FX3_LOGIC_REQUIRED_VERSION);
		const auto minimumLogicPatch
			= (deviceToOpen.pid == PID_DAVIS_FX2) ? (FX2_LOGIC_MINIMUM_PATCH) : (FX3_LOGIC_MINIMUM_PATCH);

		// Verify device logic version.
		mLogicVersion = spiConfigReceive(MODULE_SYSINFO, SYSINFO_LOGIC_VERSION);

		if (mLogicVersion != requiredLogicVersion) {
			throw dv::exceptions::RuntimeError(
				fmt::format("Device logic version incorrect. You have version {}; but version {} is required. Please "
							"update by following the Flashy documentation at "
							"'https://docs.inivation.com/hardware/hardware-advanced-usage/firmware-update.html'.",
					mLogicVersion, requiredLogicVersion));
		}

		// Verify device logic patch release.
		mLogicPatch = spiConfigReceive(MODULE_SYSINFO, SYSINFO_LOGIC_PATCH);

		if (mLogicPatch < minimumLogicPatch) {
			throw dv::exceptions::RuntimeError(
				fmt::format("Device logic patch version incorrect. You have patch version {}; but at least version {} "
							"is required. Please update by following the Flashy documentation at "
							"'https://docs.inivation.com/hardware/hardware-advanced-usage/firmware-update.html'.",
					mLogicPatch, minimumLogicPatch));
		}

		// Gather device information.
		const auto chipId = spiConfigReceive(MODULE_SYSINFO, SYSINFO_CHIP_IDENTIFIER);

		if ((chipId != dv::EnumAsInteger(parser::DAVIS::SensorModel::DAVIS240A))
			&& (chipId != dv::EnumAsInteger(parser::DAVIS::SensorModel::DAVIS240B))
			&& (chipId != dv::EnumAsInteger(parser::DAVIS::SensorModel::DAVIS240C))
			&& (chipId != dv::EnumAsInteger(parser::DAVIS::SensorModel::DAVIS346))
			&& (chipId != dv::EnumAsInteger(parser::DAVIS::SensorModel::DAVIS640))
			&& (chipId != dv::EnumAsInteger(parser::DAVIS::SensorModel::CDAVIS))) {
			throw dv::exceptions::RuntimeError("Unsupported DAVIS model.");
		}

		mSensorModel = dv::IntegerAsEnum<parser::DAVIS::SensorModel>(chipId);

		resetCameraName(fmt::format("{}", mSensorModel));

		mImuModel = dv::IntegerAsEnum<imu::ImuModel>(spiConfigReceive(MODULE_IMU, IMU_TYPE));

		mTimestampMaster = spiConfigReceive(MODULE_SYSINFO, SYSINFO_DEVICE_IS_MASTER);

		// Gather device clock information.
		const auto logicClock = static_cast<double>(spiConfigReceive(MODULE_SYSINFO, SYSINFO_LOGIC_CLOCK));
		const auto usbClock   = static_cast<double>(spiConfigReceive(MODULE_SYSINFO, SYSINFO_USB_CLOCK));
		const auto apsClock   = static_cast<double>(spiConfigReceive(MODULE_SYSINFO, SYSINFO_ADC_CLOCK));
		const auto clockDeviationFactor
			= static_cast<double>(spiConfigReceive(MODULE_SYSINFO, SYSINFO_CLOCK_DEVIATION));

		// Calculate actual clock frequencies.
		mLogicClockActual = static_cast<float>(logicClock * (clockDeviationFactor / 1000.0));
		mUSBClockActual   = static_cast<float>(usbClock * (clockDeviationFactor / 1000.0));
		mADCClockActual   = static_cast<float>(apsClock * (clockDeviationFactor / 1000.0));

		log(LogLevel::LVL_DEBUG, fmt::format("Device clocks - Logic: {}, USB: {}, ADC: {}", mLogicClockActual,
									 mUSBClockActual, mADCClockActual));

		mInfo.dvsHasPixelFilter              = spiConfigReceive(MODULE_DVS, DVS_HAS_PIXEL_FILTER);
		mInfo.dvsHasBackgroundActivityFilter = spiConfigReceive(MODULE_DVS, DVS_HAS_BACKGROUND_ACTIVITY_FILTER);
		mInfo.dvsHasROIFilter                = spiConfigReceive(MODULE_DVS, DVS_HAS_ROI_FILTER);
		mInfo.dvsHasSkipFilter               = spiConfigReceive(MODULE_DVS, DVS_HAS_SKIP_FILTER);
		mInfo.dvsHasPolarityFilter           = spiConfigReceive(MODULE_DVS, DVS_HAS_POLARITY_FILTER);
		mInfo.dvsHasStatistics               = spiConfigReceive(MODULE_DVS, DVS_HAS_STATISTICS);

		const auto colorFilter = spiConfigReceive(MODULE_APS, APS_COLOR_FILTER);
		if (colorFilter > dv::EnumAsInteger(dv::PixelArrangement::BGRG)) {
			throw dv::exceptions::RuntimeError("Unsupported color filter configuration.");
		}

		const auto apsColorFilter = dv::IntegerAsEnum<dv::PixelArrangement>(colorFilter);
		mDvsColorFilter
			= (mSensorModel == parser::DAVIS::SensorModel::CDAVIS) ? (dv::PixelArrangement::MONO) : (apsColorFilter);

		mInfo.apsHasGlobalShutter = spiConfigReceive(MODULE_APS, APS_HAS_GLOBAL_SHUTTER);

		mInfo.extInputHasGenerator = spiConfigReceive(MODULE_EXTERNAL_INPUT, EXTINPUT_HAS_GENERATOR);

		mInfo.muxHasStatistics = spiConfigReceive(MODULE_MULTIPLEXER, MUX_HAS_STATISTICS);

		log(LogLevel::LVL_DEBUG,
			fmt::format("Device info - DVS Pixel Filter: {}, DVS Noise Filter: {}, DVS color filter: {}, Frame "
						"color filter: {}, Frame Global Shutter: {}, External Signal Generator: {}",
				mInfo.dvsHasPixelFilter, mInfo.dvsHasBackgroundActivityFilter, mDvsColorFilter.load(), apsColorFilter,
				mInfo.apsHasGlobalShutter, mInfo.extInputHasGenerator));

		mEventResolution = cv::Size{static_cast<int>(spiConfigReceive(MODULE_DVS, DVS_SIZE_COLUMNS)),
			static_cast<int>(spiConfigReceive(MODULE_DVS, DVS_SIZE_ROWS))};

		mFrameResolution = cv::Size{static_cast<int>(spiConfigReceive(MODULE_APS, APS_SIZE_COLUMNS)),
			static_cast<int>(spiConfigReceive(MODULE_APS, APS_SIZE_ROWS))};

		log(LogLevel::LVL_DEBUG,
			fmt::format("Device resolution - Events: {}, Frames: {}", mEventResolution, mFrameResolution));

		// Configure parser regarding orientation of sensors.
		const auto dvsOrientation = spiConfigReceive(MODULE_DVS, DVS_ORIENTATION_INFO);

		log(LogLevel::LVL_DEBUG,
			fmt::format("Internal events orientation - Invert Axes: {}, Flip Horizontal: {}, Flip Vertical: {}",
				static_cast<bool>(dvsOrientation & 0x04), static_cast<bool>(dvsOrientation & 0x02),
				static_cast<bool>(dvsOrientation & 0x01)));

		const auto apsOrientation = spiConfigReceive(MODULE_APS, APS_ORIENTATION_INFO);

		log(LogLevel::LVL_DEBUG,
			fmt::format("Internal frames orientation - Invert Axes: {}, Flip Horizontal: {}, Flip Vertical: {}",
				static_cast<bool>(apsOrientation & 0x04), static_cast<bool>(apsOrientation & 0x02),
				static_cast<bool>(apsOrientation & 0x01)));

		const auto imuOrientation = spiConfigReceive(MODULE_IMU, IMU_ORIENTATION_INFO);

		log(LogLevel::LVL_DEBUG, fmt::format("Internal IMU orientation - Flip X: {}, Flip Y: {}, Flip Z: {}",
									 static_cast<bool>(imuOrientation & 0x04), static_cast<bool>(imuOrientation & 0x02),
									 static_cast<bool>(imuOrientation & 0x01)));

		mParser = std::make_unique<parser::DAVIS::Parser>(mSensorModel, mEventResolution, mFrameResolution, mImuModel,
			dvsOrientation, apsOrientation, imuOrientation, apsColorFilter);

		if (dvsOrientation & 0x04) {
			std::swap(mEventResolution.width, mEventResolution.height);
		}

		if (apsOrientation & 0x04) {
			std::swap(mFrameResolution.width, mFrameResolution.height);
		}

		log(LogLevel::LVL_INFO, fmt::format("Events: {}, Frames: {}, IMU: {}, Master: {}", mEventResolution,
									mFrameResolution, mImuModel, mTimestampMaster));

		// Shut down device as if calling the destructor, this ensures an improperly shutdown device (program crash)
		// goes back into a known state that's not generating any data.
		setEventsRunning(false);
		setFramesRunning(false);
		setImuRunningAccelerometer(false);
		setImuRunningGyroscope(false);
		setImuRunningTemperature(false);
		setDetectorRunning(false);
		setGeneratorRunning(false);

		spiConfigSend(MODULE_MULTIPLEXER, MUX_RUN, false);
		spiConfigSend(MODULE_MULTIPLEXER, MUX_TIMESTAMP_RUN, false);
		spiConfigSend(MODULE_USB, USB_RUN, false);

		spiConfigSend(MODULE_MULTIPLEXER, MUX_RUN_CHIP, false); // Put DVS in reset.

		// Wait 100ms for device to clear buffers.
		std::this_thread::sleep_for(std::chrono::milliseconds(100));

		// Cleanup stale data, needs to empty all buffers on data endpoint.
		cleanupBuffers();

		// DAVIS sensor default configuration.
		spiConfigSend(MODULE_DVS, DVS_WAIT_ON_TRANSFER_STALL, false);
		setExternalAERControl(false);
		if (mInfo.dvsHasBackgroundActivityFilter) {
			setBackgroundActivityFilter(0);
			setRefractoryPeriodFilter(0);
		}
		if (mInfo.dvsHasSkipFilter) {
			setSkipFilter(0);
		}
		if (mInfo.dvsHasPolarityFilter) {
			spiConfigSend(MODULE_DVS, DVS_FILTER_POLARITY_FLATTEN, false);
			setFlatten(Flatten::NONE);
		}
		if (mInfo.dvsHasROIFilter) {
			setCropAreaEvents({
				{0, 0},
                mEventResolution
            });
		}

		spiConfigSend(MODULE_APS, APS_WAIT_ON_TRANSFER_STALL, true);
		if (mInfo.apsHasGlobalShutter) {
			// If global-shutter is available (all newer DAVIS models), we want to use it.
			spiConfigSend(MODULE_APS, APS_GLOBAL_SHUTTER, true);

			if (mSensorModel != parser::DAVIS::SensorModel::CDAVIS) {
				spiConfigSend(MODULE_CHIP, DAVIS346_CHIP_GLOBAL_SHUTTER, true);
			}
		}

		setColorMode(dv::io::camera::parser::DAVIS::ColorMode::DEFAULT);
		setCropAreaFrames({
			{0, 0},
            mFrameResolution
        });

		setExposureDuration(std::chrono::milliseconds{20});
		setFrameInterval(std::chrono::milliseconds{0});

		// Use simple internal auto-exposure algorithm.
		setAutoExposureCallback(std::bind_front(&DAVIS::computeAutomaticExposure, this));
		setAutoExposure(true);

		if (mSensorModel == parser::DAVIS::SensorModel::CDAVIS) {
			// in cycles @ ADCClock
			spiConfigSend(MODULE_APS, APS_TRANSFER, 1200);
			spiConfigSend(MODULE_APS, APS_RSFDSETTLE, 100);
			spiConfigSend(MODULE_APS, APS_GSPDRESET, 100);
			spiConfigSend(MODULE_APS, APS_GSRESETFALL, 100);
			spiConfigSend(MODULE_APS, APS_GSTXFALL, 100);
			spiConfigSend(MODULE_APS, APS_GSFDRESET, 300);
		}

		// Default bias configuration.
		if ((mSensorModel == parser::DAVIS::SensorModel::DAVIS240A)
			|| (mSensorModel == parser::DAVIS::SensorModel::DAVIS240B)
			|| (mSensorModel == parser::DAVIS::SensorModel::DAVIS240C)) {
			mCoarseFineBiases[dv::EnumAsInteger(Davis240BiasCF::Diff)] = {4, 39, CoarseFineBiasSex::N_TYPE};
			mCoarseFineBiases[dv::EnumAsInteger(Davis240BiasCF::On)]   = {5, 255, CoarseFineBiasSex::N_TYPE};
			mCoarseFineBiases[dv::EnumAsInteger(Davis240BiasCF::Off)]  = {4, 0, CoarseFineBiasSex::N_TYPE};
			mCoarseFineBiases[dv::EnumAsInteger(Davis240BiasCF::ApsCascode)]
				= {5, 185, CoarseFineBiasSex::N_TYPE, CoarseFineBiasType::CASCODE};
			mCoarseFineBiases[dv::EnumAsInteger(Davis240BiasCF::DiffCascode)]
				= {5, 115, CoarseFineBiasSex::N_TYPE, CoarseFineBiasType::CASCODE};
			mCoarseFineBiases[dv::EnumAsInteger(Davis240BiasCF::ApsReadoutSourceFollower)]
				= {6, 219, CoarseFineBiasSex::N_TYPE};
			mCoarseFineBiases[dv::EnumAsInteger(Davis240BiasCF::LocalBuffer)]   = {5, 164, CoarseFineBiasSex::N_TYPE};
			mCoarseFineBiases[dv::EnumAsInteger(Davis240BiasCF::PixelInverter)] = {6, 144, CoarseFineBiasSex::N_TYPE};
			mCoarseFineBiases[dv::EnumAsInteger(Davis240BiasCF::Photoreceptor)] = {2, 58, CoarseFineBiasSex::P_TYPE};
			mCoarseFineBiases[dv::EnumAsInteger(Davis240BiasCF::PhotoreceptorSourceFollower)]
				= {1, 16, CoarseFineBiasSex::P_TYPE};
			mCoarseFineBiases[dv::EnumAsInteger(Davis240BiasCF::Refractory)]  = {4, 25, CoarseFineBiasSex::P_TYPE};
			mCoarseFineBiases[dv::EnumAsInteger(Davis240BiasCF::AERPullDown)] = {6, 91, CoarseFineBiasSex::N_TYPE};
			mCoarseFineBiases[dv::EnumAsInteger(Davis240BiasCF::LCOLTimeout)] = {5, 49, CoarseFineBiasSex::N_TYPE};
			mCoarseFineBiases[dv::EnumAsInteger(Davis240BiasCF::AERPullUpX)]  = {4, 80, CoarseFineBiasSex::P_TYPE};
			mCoarseFineBiases[dv::EnumAsInteger(Davis240BiasCF::AERPullUpY)]  = {7, 152, CoarseFineBiasSex::P_TYPE};
			mCoarseFineBiases[DAVIS240_BIAS_IFTHRBN]                          = {CoarseFineBiasSex::N_TYPE};
			mCoarseFineBiases[DAVIS240_BIAS_IFREFRBN]                         = {CoarseFineBiasSex::N_TYPE};
			mCoarseFineBiases[dv::EnumAsInteger(Davis240BiasCF::PadFollower)] = {7, 215, CoarseFineBiasSex::N_TYPE};
			mCoarseFineBiases[dv::EnumAsInteger(Davis240BiasCF::ApsOverflowLevel)]
				= {6, 253, CoarseFineBiasSex::N_TYPE};
			mCoarseFineBiases[dv::EnumAsInteger(Davis240BiasCF::BiasBuffer)] = {5, 254, CoarseFineBiasSex::N_TYPE};

			sendCoarseFineBias(dv::EnumAsInteger(Davis240BiasCF::Diff));
			sendCoarseFineBias(dv::EnumAsInteger(Davis240BiasCF::On));
			sendCoarseFineBias(dv::EnumAsInteger(Davis240BiasCF::Off));
			sendCoarseFineBias(dv::EnumAsInteger(Davis240BiasCF::ApsCascode));
			sendCoarseFineBias(dv::EnumAsInteger(Davis240BiasCF::DiffCascode));
			sendCoarseFineBias(dv::EnumAsInteger(Davis240BiasCF::ApsReadoutSourceFollower));
			sendCoarseFineBias(dv::EnumAsInteger(Davis240BiasCF::LocalBuffer));
			sendCoarseFineBias(dv::EnumAsInteger(Davis240BiasCF::PixelInverter));
			sendCoarseFineBias(dv::EnumAsInteger(Davis240BiasCF::Photoreceptor));
			sendCoarseFineBias(dv::EnumAsInteger(Davis240BiasCF::PhotoreceptorSourceFollower));
			sendCoarseFineBias(dv::EnumAsInteger(Davis240BiasCF::Refractory));
			sendCoarseFineBias(dv::EnumAsInteger(Davis240BiasCF::AERPullDown));
			sendCoarseFineBias(dv::EnumAsInteger(Davis240BiasCF::LCOLTimeout));
			sendCoarseFineBias(dv::EnumAsInteger(Davis240BiasCF::AERPullUpX));
			sendCoarseFineBias(dv::EnumAsInteger(Davis240BiasCF::AERPullUpY));
			sendCoarseFineBias(DAVIS240_BIAS_IFTHRBN);
			sendCoarseFineBias(DAVIS240_BIAS_IFREFRBN);
			sendCoarseFineBias(dv::EnumAsInteger(Davis240BiasCF::PadFollower));
			sendCoarseFineBias(dv::EnumAsInteger(Davis240BiasCF::ApsOverflowLevel));
			sendCoarseFineBias(dv::EnumAsInteger(Davis240BiasCF::BiasBuffer));

			mShiftedSourcePBias = {1, 33};
			mShiftedSourceNBias = {1, 33};

			sendShiftedSourceBias(DAVIS240_BIAS_SSP);
			sendShiftedSourceBias(DAVIS240_BIAS_SSN);
		}

		if ((mSensorModel == parser::DAVIS::SensorModel::DAVIS346)
			|| (mSensorModel == parser::DAVIS::SensorModel::DAVIS640)) {
			mVDACBiases[dv::EnumAsInteger(Davis346BiasVDAC::ApsOverflowLevel)] = {27, 6};
			mVDACBiases[dv::EnumAsInteger(Davis346BiasVDAC::ApsCascode)]       = {21, 6};
			mVDACBiases[dv::EnumAsInteger(Davis346BiasVDAC::ADCReferenceHigh)] = {32, 7};
			mVDACBiases[dv::EnumAsInteger(Davis346BiasVDAC::ADCReferenceLow)]  = {1, 7};
			mVDACBiases[dv::EnumAsInteger(Davis346BiasVDAC::ADCTestVoltage)]   = {21, 7};

			sendVDACBias(dv::EnumAsInteger(Davis346BiasVDAC::ApsOverflowLevel));
			sendVDACBias(dv::EnumAsInteger(Davis346BiasVDAC::ApsCascode));
			sendVDACBias(dv::EnumAsInteger(Davis346BiasVDAC::ADCReferenceHigh));
			sendVDACBias(dv::EnumAsInteger(Davis346BiasVDAC::ADCReferenceLow));
			sendVDACBias(dv::EnumAsInteger(Davis346BiasVDAC::ADCTestVoltage));

			mCoarseFineBiases[dv::EnumAsInteger(Davis346BiasCF::LocalBuffer)]   = {5, 164, CoarseFineBiasSex::N_TYPE};
			mCoarseFineBiases[dv::EnumAsInteger(Davis346BiasCF::PadFollower)]   = {CoarseFineBiasSex::N_TYPE};
			mCoarseFineBiases[dv::EnumAsInteger(Davis346BiasCF::Diff)]          = {4, 39, CoarseFineBiasSex::N_TYPE};
			mCoarseFineBiases[dv::EnumAsInteger(Davis346BiasCF::On)]            = {5, 255, CoarseFineBiasSex::N_TYPE};
			mCoarseFineBiases[dv::EnumAsInteger(Davis346BiasCF::Off)]           = {4, 1, CoarseFineBiasSex::N_TYPE};
			mCoarseFineBiases[dv::EnumAsInteger(Davis346BiasCF::PixelInverter)] = {6, 144, CoarseFineBiasSex::N_TYPE};
			mCoarseFineBiases[dv::EnumAsInteger(Davis346BiasCF::Photoreceptor)] = {2, 58, CoarseFineBiasSex::P_TYPE};
			mCoarseFineBiases[dv::EnumAsInteger(Davis346BiasCF::PhotoreceptorSourceFollower)]
				= {1, 16, CoarseFineBiasSex::P_TYPE};
			mCoarseFineBiases[dv::EnumAsInteger(Davis346BiasCF::Refractory)]    = {4, 25, CoarseFineBiasSex::P_TYPE};
			mCoarseFineBiases[dv::EnumAsInteger(Davis346BiasCF::ReadoutBuffer)] = {6, 20, CoarseFineBiasSex::P_TYPE};
			mCoarseFineBiases[dv::EnumAsInteger(Davis346BiasCF::ApsReadoutSourceFollower)]
				= {6, 219, CoarseFineBiasSex::N_TYPE};
			mCoarseFineBiases[dv::EnumAsInteger(Davis346BiasCF::ADCComparator)] = {5, 20, CoarseFineBiasSex::P_TYPE};
			mCoarseFineBiases[dv::EnumAsInteger(Davis346BiasCF::COLSelectLow)]  = {0, 1, CoarseFineBiasSex::N_TYPE};
			mCoarseFineBiases[dv::EnumAsInteger(Davis346BiasCF::DACBuffer)]     = {6, 60, CoarseFineBiasSex::P_TYPE};
			mCoarseFineBiases[dv::EnumAsInteger(Davis346BiasCF::LCOLTimeout)]   = {5, 49, CoarseFineBiasSex::N_TYPE};
			mCoarseFineBiases[dv::EnumAsInteger(Davis346BiasCF::AERPullDown)]   = {6, 91, CoarseFineBiasSex::N_TYPE};
			mCoarseFineBiases[dv::EnumAsInteger(Davis346BiasCF::AERPullUpX)]    = {4, 80, CoarseFineBiasSex::P_TYPE};
			mCoarseFineBiases[dv::EnumAsInteger(Davis346BiasCF::AERPullUpY)]    = {7, 152, CoarseFineBiasSex::P_TYPE};
			mCoarseFineBiases[DAVIS346_BIAS_IFREFRBN]                           = {CoarseFineBiasSex::N_TYPE};
			mCoarseFineBiases[DAVIS346_BIAS_IFTHRBN]                            = {CoarseFineBiasSex::N_TYPE};
			mCoarseFineBiases[dv::EnumAsInteger(Davis346BiasCF::BiasBuffer)]    = {5, 254, CoarseFineBiasSex::N_TYPE};

			// This comes last here so it overrides previous settings for 640 only.
			if (mSensorModel == parser::DAVIS::SensorModel::DAVIS640) {
				// Slow down pixels for big 640x480 array, to avoid overwhelming the AER bus.
				mCoarseFineBiases[dv::EnumAsInteger(Davis346BiasCF::On)]  = {5, 155, CoarseFineBiasSex::N_TYPE};
				mCoarseFineBiases[dv::EnumAsInteger(Davis346BiasCF::Off)] = {1, 4, CoarseFineBiasSex::N_TYPE};
				mCoarseFineBiases[dv::EnumAsInteger(Davis346BiasCF::Photoreceptor)] = {2, 3, CoarseFineBiasSex::P_TYPE};
				mCoarseFineBiases[dv::EnumAsInteger(Davis346BiasCF::PhotoreceptorSourceFollower)]
					= {1, 1, CoarseFineBiasSex::P_TYPE};
				mCoarseFineBiases[dv::EnumAsInteger(Davis346BiasCF::BiasBuffer)] = {6, 125, CoarseFineBiasSex::N_TYPE};
			}

			sendCoarseFineBias(dv::EnumAsInteger(Davis346BiasCF::LocalBuffer));
			sendCoarseFineBias(dv::EnumAsInteger(Davis346BiasCF::PadFollower));
			sendCoarseFineBias(dv::EnumAsInteger(Davis346BiasCF::Diff));
			sendCoarseFineBias(dv::EnumAsInteger(Davis346BiasCF::On));
			sendCoarseFineBias(dv::EnumAsInteger(Davis346BiasCF::Off));
			sendCoarseFineBias(dv::EnumAsInteger(Davis346BiasCF::PixelInverter));
			sendCoarseFineBias(dv::EnumAsInteger(Davis346BiasCF::Photoreceptor));
			sendCoarseFineBias(dv::EnumAsInteger(Davis346BiasCF::PhotoreceptorSourceFollower));
			sendCoarseFineBias(dv::EnumAsInteger(Davis346BiasCF::Refractory));
			sendCoarseFineBias(dv::EnumAsInteger(Davis346BiasCF::ReadoutBuffer));
			sendCoarseFineBias(dv::EnumAsInteger(Davis346BiasCF::ApsReadoutSourceFollower));
			sendCoarseFineBias(dv::EnumAsInteger(Davis346BiasCF::ADCComparator));
			sendCoarseFineBias(dv::EnumAsInteger(Davis346BiasCF::COLSelectLow));
			sendCoarseFineBias(dv::EnumAsInteger(Davis346BiasCF::DACBuffer));
			sendCoarseFineBias(dv::EnumAsInteger(Davis346BiasCF::LCOLTimeout));
			sendCoarseFineBias(dv::EnumAsInteger(Davis346BiasCF::AERPullDown));
			sendCoarseFineBias(dv::EnumAsInteger(Davis346BiasCF::AERPullUpX));
			sendCoarseFineBias(dv::EnumAsInteger(Davis346BiasCF::AERPullUpY));
			sendCoarseFineBias(DAVIS346_BIAS_IFREFRBN);
			sendCoarseFineBias(DAVIS346_BIAS_IFTHRBN);
			sendCoarseFineBias(dv::EnumAsInteger(Davis346BiasCF::BiasBuffer));

			mShiftedSourcePBias = {1, 33};
			mShiftedSourceNBias = {1, 33};

			sendShiftedSourceBias(DAVIS346_BIAS_SSP);
			sendShiftedSourceBias(DAVIS346_BIAS_SSN);
		}

		if (mSensorModel == parser::DAVIS::SensorModel::CDAVIS) {
			mVDACBiases[dv::EnumAsInteger(CDavisBiasVDAC::ApsCascode)]       = {32, 5};
			mVDACBiases[dv::EnumAsInteger(CDavisBiasVDAC::OVG1Low)]          = {63, 7};
			mVDACBiases[dv::EnumAsInteger(CDavisBiasVDAC::OVG2Low)]          = {0, 0};
			mVDACBiases[dv::EnumAsInteger(CDavisBiasVDAC::TX2OVG2High)]      = {0, 0};
			mVDACBiases[dv::EnumAsInteger(CDavisBiasVDAC::Gnd07)]            = {10, 6};
			mVDACBiases[dv::EnumAsInteger(CDavisBiasVDAC::ADCTestVoltage)]   = {27, 0};
			mVDACBiases[dv::EnumAsInteger(CDavisBiasVDAC::ADCReferenceHigh)] = {46, 7};
			mVDACBiases[dv::EnumAsInteger(CDavisBiasVDAC::ADCReferenceLow)]  = {8, 7};

			sendVDACBias(dv::EnumAsInteger(CDavisBiasVDAC::ApsCascode));
			sendVDACBias(dv::EnumAsInteger(CDavisBiasVDAC::OVG1Low));
			sendVDACBias(dv::EnumAsInteger(CDavisBiasVDAC::OVG2Low));
			sendVDACBias(dv::EnumAsInteger(CDavisBiasVDAC::TX2OVG2High));
			sendVDACBias(dv::EnumAsInteger(CDavisBiasVDAC::Gnd07));
			sendVDACBias(dv::EnumAsInteger(CDavisBiasVDAC::ADCTestVoltage));
			sendVDACBias(dv::EnumAsInteger(CDavisBiasVDAC::ADCReferenceHigh));
			sendVDACBias(dv::EnumAsInteger(CDavisBiasVDAC::ADCReferenceLow));

			mCoarseFineBiases[CDAVIS_BIAS_IFREFRBN]                           = {CoarseFineBiasSex::N_TYPE};
			mCoarseFineBiases[CDAVIS_BIAS_IFTHRBN]                            = {CoarseFineBiasSex::N_TYPE};
			mCoarseFineBiases[dv::EnumAsInteger(CDavisBiasCF::LocalBuffer)]   = {CoarseFineBiasSex::N_TYPE};
			mCoarseFineBiases[dv::EnumAsInteger(CDavisBiasCF::PadFollower)]   = {CoarseFineBiasSex::N_TYPE};
			mCoarseFineBiases[dv::EnumAsInteger(CDavisBiasCF::PixelInverter)] = {5, 164, CoarseFineBiasSex::N_TYPE};
			mCoarseFineBiases[dv::EnumAsInteger(CDavisBiasCF::Diff)]          = {3, 135, CoarseFineBiasSex::N_TYPE};
			mCoarseFineBiases[dv::EnumAsInteger(CDavisBiasCF::On)]            = {6, 84, CoarseFineBiasSex::N_TYPE};
			mCoarseFineBiases[dv::EnumAsInteger(CDavisBiasCF::Off)]           = {2, 20, CoarseFineBiasSex::N_TYPE};
			mCoarseFineBiases[dv::EnumAsInteger(CDavisBiasCF::Photoreceptor)] = {3, 88, CoarseFineBiasSex::P_TYPE};
			mCoarseFineBiases[dv::EnumAsInteger(CDavisBiasCF::PhotoreceptorSourceFollower)]
				= {2, 45, CoarseFineBiasSex::P_TYPE};
			mCoarseFineBiases[dv::EnumAsInteger(CDavisBiasCF::Refractory)]       = {4, 25, CoarseFineBiasSex::P_TYPE};
			mCoarseFineBiases[dv::EnumAsInteger(CDavisBiasCF::ArrayBiasBuffer)]  = {3, 128, CoarseFineBiasSex::N_TYPE};
			mCoarseFineBiases[dv::EnumAsInteger(CDavisBiasCF::ArrayLogicBuffer)] = {6, 255, CoarseFineBiasSex::N_TYPE};
			mCoarseFineBiases[dv::EnumAsInteger(CDavisBiasCF::FallTime)]         = {7, 41, CoarseFineBiasSex::N_TYPE};
			mCoarseFineBiases[dv::EnumAsInteger(CDavisBiasCF::RiseTime)]         = {6, 162, CoarseFineBiasSex::P_TYPE};
			mCoarseFineBiases[dv::EnumAsInteger(CDavisBiasCF::ReadoutBuffer)]    = {CoarseFineBiasSex::P_TYPE};
			mCoarseFineBiases[dv::EnumAsInteger(CDavisBiasCF::ApsReadoutSourceFollower)]
				= {7, 102, CoarseFineBiasSex::N_TYPE};
			mCoarseFineBiases[dv::EnumAsInteger(CDavisBiasCF::ADCComparator)] = {4, 159, CoarseFineBiasSex::P_TYPE};
			mCoarseFineBiases[dv::EnumAsInteger(CDavisBiasCF::DACBuffer)]     = {6, 194, CoarseFineBiasSex::P_TYPE};
			mCoarseFineBiases[dv::EnumAsInteger(CDavisBiasCF::LCOLTimeout)]   = {5, 49, CoarseFineBiasSex::N_TYPE};
			mCoarseFineBiases[dv::EnumAsInteger(CDavisBiasCF::AERPullDown)]   = {6, 91, CoarseFineBiasSex::N_TYPE};
			mCoarseFineBiases[dv::EnumAsInteger(CDavisBiasCF::AERPullUpX)]    = {5, 80, CoarseFineBiasSex::P_TYPE};
			mCoarseFineBiases[dv::EnumAsInteger(CDavisBiasCF::AERPullUpY)]    = {7, 152, CoarseFineBiasSex::P_TYPE};
			mCoarseFineBiases[dv::EnumAsInteger(CDavisBiasCF::BiasBuffer)]    = {6, 251, CoarseFineBiasSex::N_TYPE};

			sendCoarseFineBias(CDAVIS_BIAS_IFREFRBN);
			sendCoarseFineBias(CDAVIS_BIAS_IFTHRBN);
			sendCoarseFineBias(dv::EnumAsInteger(CDavisBiasCF::LocalBuffer));
			sendCoarseFineBias(dv::EnumAsInteger(CDavisBiasCF::PadFollower));
			sendCoarseFineBias(dv::EnumAsInteger(CDavisBiasCF::PixelInverter));
			sendCoarseFineBias(dv::EnumAsInteger(CDavisBiasCF::Diff));
			sendCoarseFineBias(dv::EnumAsInteger(CDavisBiasCF::On));
			sendCoarseFineBias(dv::EnumAsInteger(CDavisBiasCF::Off));
			sendCoarseFineBias(dv::EnumAsInteger(CDavisBiasCF::Photoreceptor));
			sendCoarseFineBias(dv::EnumAsInteger(CDavisBiasCF::PhotoreceptorSourceFollower));
			sendCoarseFineBias(dv::EnumAsInteger(CDavisBiasCF::Refractory));
			sendCoarseFineBias(dv::EnumAsInteger(CDavisBiasCF::ArrayBiasBuffer));
			sendCoarseFineBias(dv::EnumAsInteger(CDavisBiasCF::ArrayLogicBuffer));
			sendCoarseFineBias(dv::EnumAsInteger(CDavisBiasCF::FallTime));
			sendCoarseFineBias(dv::EnumAsInteger(CDavisBiasCF::RiseTime));
			sendCoarseFineBias(dv::EnumAsInteger(CDavisBiasCF::ReadoutBuffer));
			sendCoarseFineBias(dv::EnumAsInteger(CDavisBiasCF::ApsReadoutSourceFollower));
			sendCoarseFineBias(dv::EnumAsInteger(CDavisBiasCF::ADCComparator));
			sendCoarseFineBias(dv::EnumAsInteger(CDavisBiasCF::DACBuffer));
			sendCoarseFineBias(dv::EnumAsInteger(CDavisBiasCF::LCOLTimeout));
			sendCoarseFineBias(dv::EnumAsInteger(CDavisBiasCF::AERPullDown));
			sendCoarseFineBias(dv::EnumAsInteger(CDavisBiasCF::AERPullUpX));
			sendCoarseFineBias(dv::EnumAsInteger(CDavisBiasCF::AERPullUpY));
			sendCoarseFineBias(dv::EnumAsInteger(CDavisBiasCF::BiasBuffer));

			mShiftedSourcePBias = {1, 33, ShiftedSourceBiasOperatingMode::TIED_TO_RAIL};
			mShiftedSourceNBias = {2, 33, ShiftedSourceBiasOperatingMode::SHIFTED_SOURCE};

			sendShiftedSourceBias(CDAVIS_BIAS_SSP);
			sendShiftedSourceBias(CDAVIS_BIAS_SSN);
		}

		// Default chip configuration.
		spiConfigSend(MODULE_CHIP, DAVIS346_CHIP_DIGITALMUX0, 0);
		spiConfigSend(MODULE_CHIP, DAVIS346_CHIP_DIGITALMUX1, 0);
		spiConfigSend(MODULE_CHIP, DAVIS346_CHIP_DIGITALMUX2, 0);
		spiConfigSend(MODULE_CHIP, DAVIS346_CHIP_DIGITALMUX3, 0);
		spiConfigSend(MODULE_CHIP, DAVIS346_CHIP_ANALOGMUX0, 0);
		spiConfigSend(MODULE_CHIP, DAVIS346_CHIP_ANALOGMUX1, 0);
		spiConfigSend(MODULE_CHIP, DAVIS346_CHIP_ANALOGMUX2, 0);
		spiConfigSend(MODULE_CHIP, DAVIS346_CHIP_BIASMUX0, 0);
		spiConfigSend(MODULE_CHIP, DAVIS346_CHIP_RESETCALIBNEURON, true);
		spiConfigSend(MODULE_CHIP, DAVIS346_CHIP_TYPENCALIBNEURON, false);
		spiConfigSend(MODULE_CHIP, DAVIS346_CHIP_RESETTESTPIXEL, true);
		spiConfigSend(MODULE_CHIP, DAVIS346_CHIP_AERNAROW, false); // Use nArow in the AER state machine.
		spiConfigSend(MODULE_CHIP, DAVIS346_CHIP_USEAOUT, false);  // Enable analog pads for aMUX output.

		// No GlobalShutter flag set here, we already set it above for the APS GS flag,
		// and that is automatically propagated to the chip config shift-register in
		// configSet() and kept in sync.

		if ((mSensorModel == parser::DAVIS::SensorModel::DAVIS240A)
			|| (mSensorModel == parser::DAVIS::SensorModel::DAVIS240B)) {
			// Special extra pixels control for DAVIS240 A/B.
			spiConfigSend(MODULE_CHIP, DAVIS240_CHIP_SPECIALPIXELCONTROL, false);
		}

		if ((mSensorModel == parser::DAVIS::SensorModel::DAVIS346)
			|| (mSensorModel == parser::DAVIS::SensorModel::DAVIS640)
			|| (mSensorModel == parser::DAVIS::SensorModel::CDAVIS)) {
			// Select which gray counter to use with the internal ADC: '0' means the external gray counter is used,
			// which has to be supplied off-chip. '1' means the on-chip gray counter is used instead.
			spiConfigSend(MODULE_CHIP, DAVIS346_CHIP_SELECTGRAYCOUNTER, 1);

			// Test ADC functionality: if true, the ADC takes its input voltage not from the pixel, but from the
			// VDAC 'AdcTestVoltage'. If false, the voltage comes from the pixels.
			spiConfigSend(MODULE_CHIP, DAVIS346_CHIP_TESTADC, false);
		}

		if (mSensorModel == parser::DAVIS::SensorModel::CDAVIS) {
			setCDavisAdjustOVG1Low(true);
			setCDavisAdjustOVG2Low(false);
			setCDavisAdjustTX2OVGHigh(false);
		}

		// Default configuration of IMU, External Input.
		spiConfigSend(MODULE_MULTIPLEXER, MUX_DROP_EXTINPUT_ON_TRANSFER_STALL, true);
		spiConfigSend(MODULE_MULTIPLEXER, MUX_DROP_DVS_ON_TRANSFER_STALL, true);

		spiConfigSend(MODULE_IMU, IMU_SAMPLE_RATE_DIVIDER, 0); // Sampling rate: 1KHz.
		spiConfigSend(MODULE_IMU, IMU_ACCEL_DLPF, 1);          // FS: 1KHz.
		if (mImuModel == imu::ImuModel::IMU_INVENSENSE_6500_9250) {
			spiConfigSend(MODULE_IMU, IMU_GYRO_DLPF, 1); // FS: 1KHz.
		}
		setImuAccelRange(imu::InvensenseAccelRange::RANGE_4G);
		setImuGyroRange(imu::InvensenseGyroRange::RANGE_500DPS);

		setDetectorRisingEdges(false);
		setDetectorFallingEdges(false);

		setGeneratorHighTime(std::chrono::microseconds{5});
		setGeneratorLowTime(std::chrono::microseconds{5});
		setGeneratorInjectTriggerOnRisingEdge(false);
		setGeneratorInjectTriggerOnFallingEdge(false);

		setUSBEarlyPacketDelay(std::chrono::milliseconds{1});

		// USB and parser setup.
		setDataCallback(std::bind_front(&DAVIS::usbDataCallback, this));
		setShutdownCallback(std::bind_front(&DAVIS::shutdownCallback, this));
		// No debug callback required, log messages are handled in USBDevice class.

		mParser->setLogger([this](const bool debug, const std::string_view message) {
			log((debug) ? (LogLevel::LVL_DEBUG) : (LogLevel::LVL_ERROR), message);
		});

		mParser->setDataCommitCallback(std::bind_front(&DAVIS::dataParserCallback, this));
		mParser->setTimeInitCallback(std::bind_front(&DAVIS::timeInitCallback, this));

		if (getDeviceDescriptor().pid != PID_DAVIS_FX2) {
			usbDebugTransfersStart();
		}

		usbDataTransfersStart();

		try {
			// Enable data transfer on USB end-point 2.
			spiConfigSend(MODULE_MULTIPLEXER, MUX_RUN_CHIP, true);

			spiConfigSend(MODULE_USB, USB_RUN, true);
			spiConfigSend(MODULE_MULTIPLEXER, MUX_TIMESTAMP_RUN, true);
			spiConfigSend(MODULE_MULTIPLEXER, MUX_RUN, true);

			// Wait 10 ms for data transfer to be ready.
			std::this_thread::sleep_for(std::chrono::milliseconds(10));

			// Enable data producers.
			setEventsRunning(true);
			setFramesRunning(true);
			setImuRunningAccelerometer(true);
			setImuRunningGyroscope(true);
			setImuRunningTemperature(true);
			setDetectorRunning(true);
			setGeneratorRunning(false);

			// Timestamp reset to sync up time.
			sendTimestampReset();
			waitForTimestampReset();
		}
		catch (const dv::exceptions::RuntimeError &) {
			if (getDeviceDescriptor().pid != PID_DAVIS_FX2) {
				usbDebugTransfersStop();
			}

			usbDataTransfersStop();

			throw;
		}
	}

	~DAVIS() override {
		try {
			setEventsRunning(false);
			setFramesRunning(false);
			setImuRunningAccelerometer(false);
			setImuRunningGyroscope(false);
			setImuRunningTemperature(false);
			setDetectorRunning(false);
			setGeneratorRunning(false);

			spiConfigSend(MODULE_MULTIPLEXER, MUX_RUN, false);
			spiConfigSend(MODULE_MULTIPLEXER, MUX_TIMESTAMP_RUN, false);
			spiConfigSend(MODULE_USB, USB_RUN, false);

			// Keep chip running if external AER is enabled, as something external might still be using it.
			if (!getExternalAERControl()) {
				spiConfigSend(MODULE_MULTIPLEXER, MUX_RUN_CHIP, false); // Put DVS in reset.
			}
		}
		catch (const dv::exceptions::RuntimeError &) {
			// Ignore error, usually because device is gone. Always try to shut-down, as the device might not have gone
			// away fully and can still receive the command.
		}

		std::this_thread::sleep_for(std::chrono::milliseconds(100));

		if (getDeviceDescriptor().pid != PID_DAVIS_FX2) {
			usbDebugTransfersStop();
		}

		usbDataTransfersStop();
	}

	[[nodiscard]] std::string getCameraName() const override {
		return getDeviceName();
	}

	/**
	 * Get camera FPGA logic version.
	 * @return camera FPGA logic version
	 */
	[[nodiscard]] uint32_t getLogicVersion() const {
		return mLogicVersion;
	}

	/**
	 * Get camera FPGA logic patch level.
	 * @return camera FPGA logic patch level
	 */
	[[nodiscard]] uint32_t getLogicPatchLevel() const {
		return mLogicPatch;
	}

	/**
	 * Get the exact model of the DAVIS sensor chip mounted in this camera.
	 * @return camera DAVIS sensor chip model
	 */
	[[nodiscard]] parser::DAVIS::SensorModel getSensorModel() const {
		return mSensorModel;
	}

	[[nodiscard]] std::optional<cv::Size> getEventResolution() const override {
		return mEventResolution;
	}

	[[nodiscard]] std::optional<cv::Size> getFrameResolution() const override {
		return mFrameResolution;
	}

	[[nodiscard]] imu::ImuModel getImuModel() const override {
		return mImuModel;
	}

	[[nodiscard]] float getPixelPitch() const override {
		return 18.5e-6f;
	}

	[[nodiscard]] bool isMaster() const override {
		return mTimestampMaster;
	}

	[[nodiscard]] bool isEventStreamAvailable() const override {
		// All supported cameras have event output right now
		return true;
	}

	[[nodiscard]] bool isFrameStreamAvailable() const override {
		return true;
	}

	[[nodiscard]] bool isImuStreamAvailable() const override {
		return true;
	}

	[[nodiscard]] bool isTriggerStreamAvailable() const override {
		return true;
	}

	[[nodiscard]] bool isRunning() const override {
		return mIsRunning;
	}

	[[nodiscard]] bool isRunning(const std::string_view streamName) const override {
		if (streamName == "events") {
			return isRunning() && isEventsRunning();
		}
		if (streamName == "frames") {
			return isRunning() && isFramesRunning();
		}
		if (streamName == "imu") {
			return isRunning() && (isImuRunningAccelerometer() || isImuRunningGyroscope() || isImuRunningTemperature());
		}
		if (streamName == "triggers") {
			return isRunning() && isDetectorRunning();
		}

		throw dv::exceptions::InvalidArgument<std::string>(
			"Stream with a given name is not available", std::string{streamName});
	}

	[[nodiscard]] bool isRunningAny() const override {
		return isRunning("events") || isRunning("frames") || isRunning("imu") || isRunning("triggers");
	}

	[[nodiscard]] std::chrono::microseconds getTimeInterval() const override {
		return mParser->getTimeInterval();
	}

	void setTimeInterval(const std::chrono::microseconds timeInterval) override {
		mParser->setTimeInterval(timeInterval);
	}

	[[nodiscard]] std::chrono::microseconds getTimestampOffset() const override {
		return mParser->getSystemOffset();
	}

	/**
	 * Get pixel color filter arrangement for event readout on color DAVIS.
	 * Takes flips into account.
	 * @return current color filter
	 */
	[[nodiscard]] dv::PixelArrangement getPixelArrangementEvents() const {
		return mDvsColorFilter;
	}

	/**
	 * Get pixel color filter arrangement for frame readout on color DAVIS.
	 * Takes flips into account.
	 * @return current color filter
	 */
	[[nodiscard]] dv::PixelArrangement getPixelArrangementFrames() const {
		return mParser->getFrameColorFilter();
	}

	/**
	 * Determine the color of the Bayer color filter for a
	 * specific event, based on its pixel address.
	 * WHITE means White/No Filter.
	 * Please take into account that there are
	 * usually twice as many green pixels as
	 * there are red or blue ones.
	 *
	 * @param event event pixel to get color for.
	 *
	 * @return event color filter value.
	 */
	dv::EventColor getEventColor(const dv::Event &event) const {
		return dv::colorForEvent(event, mDvsColorFilter.load(std::memory_order_relaxed));
	}

	/**
	 * Report if the event output is running.
	 * @return true if events active, false otherwise.
	 */
	[[nodiscard]] bool isEventsRunning() const {
		std::scoped_lock lock{mConfigLock};

		return spiConfigReceive(MODULE_DVS, DVS_RUN);
	}

	/**
	 * Enable or disable the event output.
	 * @param run whether to enable the event output or not.
	 */
	void setEventsRunning(const bool run) {
		std::scoped_lock lock{mConfigLock};

		return spiConfigSend(MODULE_DVS, DVS_RUN, run);
	}

	[[nodiscard]] bool getFlipHorizontal() const override {
		return getFlipHorizontalEvents();
	}

	void setFlipHorizontal(const bool flipHorizontalEvents) override {
		setFlipHorizontalEvents(flipHorizontalEvents);
	}

	[[nodiscard]] bool getFlipVertical() const override {
		return getFlipVerticalEvents();
	}

	void setFlipVertical(const bool flipVerticalEvents) override {
		setFlipVerticalEvents(flipVerticalEvents);
	}

	/**
	 * Status of horizontal events flip.
	 * @return status of horizontal events flip.
	 */
	[[nodiscard]] bool getFlipHorizontalEvents() const {
		return mParser->getDVSFlipHorizontal();
	}

	/**
	 * Flip events horizontally.
	 * @param flipHorizontalEvents flip events horizontally.
	 */
	void setFlipHorizontalEvents(const bool flipHorizontalEvents) {
		std::scoped_lock lock{mConfigLock};

		const auto prev = getFlipHorizontalEvents();

		mParser->setDVSFlipHorizontal(flipHorizontalEvents);

		setCropAreaEventsInternal(mCropAreaEvents);

		// Update color filter to take flips into account, as those mirror
		// the 2x2 pixel groups within the Bayer filter. This is in effect
		// equivalent to switching to another Bayer color arrangement.
		if (mDvsColorFilter != dv::PixelArrangement::MONO) {
			if ((!prev && flipHorizontalEvents) || (prev && !flipHorizontalEvents)) {
				switch (mDvsColorFilter) {
					case dv::PixelArrangement::RGBG:
						mDvsColorFilter = dv::PixelArrangement::GRGB;
						break;

					case dv::PixelArrangement::GRGB:
						mDvsColorFilter = dv::PixelArrangement::RGBG;
						break;

					case dv::PixelArrangement::GBGR:
						mDvsColorFilter = dv::PixelArrangement::BGRG;
						break;

					case dv::PixelArrangement::BGRG:
						mDvsColorFilter = dv::PixelArrangement::GBGR;
						break;

					default:
					case dv::PixelArrangement::MONO:
						// Impossible.
						break;
				}
			}
		}
	}

	/**
	 * Status of vertical events flip.
	 * @return status of vertical events flip.
	 */
	[[nodiscard]] bool getFlipVerticalEvents() const {
		return mParser->getDVSFlipVertical();
	}

	/**
	 * Flip events vertically.
	 * @param flipVerticalEvents flip events vertically.
	 */
	void setFlipVerticalEvents(const bool flipVerticalEvents) {
		std::scoped_lock lock{mConfigLock};

		const auto prev = getFlipVerticalEvents();

		mParser->setDVSFlipVertical(flipVerticalEvents);

		setCropAreaEventsInternal(mCropAreaEvents);

		// Update color filter to take flips into account, as those mirror
		// the 2x2 pixel groups within the Bayer filter. This is in effect
		// equivalent to switching to another Bayer color arrangement.
		if (mDvsColorFilter != dv::PixelArrangement::MONO) {
			if ((!prev && flipVerticalEvents) || (prev && !flipVerticalEvents)) {
				switch (mDvsColorFilter) {
					case dv::PixelArrangement::RGBG:
						mDvsColorFilter = dv::PixelArrangement::GBGR;
						break;

					case dv::PixelArrangement::GRGB:
						mDvsColorFilter = dv::PixelArrangement::BGRG;
						break;

					case dv::PixelArrangement::GBGR:
						mDvsColorFilter = dv::PixelArrangement::RGBG;
						break;

					case dv::PixelArrangement::BGRG:
						mDvsColorFilter = dv::PixelArrangement::GRGB;
						break;

					default:
					case dv::PixelArrangement::MONO:
						// Impossible.
						break;
				}
			}
		}
	}

	/**
	 * Get external AER control status.
	 * If true, the AER handshake with the sensor chip is not done by our camera's FPGA,
	 * but by some external system provided by the customer.
	 * @return external AER control status
	 */
	[[nodiscard]] bool getExternalAERControl() const {
		return spiConfigReceive(MODULE_DVS, DVS_EXTERNAL_AER_CONTROL);
	}

	/**
	 * Enable or disable external AER control feature.
	 * If true, the AER handshake with the sensor chip is not done by our camera's FPGA,
	 * but by some external system provided by the customer.
	 * This also disables the normal event output (isEventsRunning() returns false).
	 * Disabling this will not re-enable the normal event output automatically!
	 * @param externalAERControl true to enable external AER control, false to disable.
	 */
	void setExternalAERControl(const bool externalAERControl) {
		spiConfigSend(MODULE_DVS, DVS_EXTERNAL_AER_CONTROL, externalAERControl);

		// DVS state machine must also be stopped for this to work.
		if (externalAERControl) {
			setEventsRunning(false);
		}
	}

	/**
	 * Get background activity noise filter time delta value.
	 * If 0, filtering is disabled.
	 * @return time delta value in 250µs multiples
	 */
	[[nodiscard]] uint16_t getBackgroundActivityFilter() const {
		if (!mInfo.dvsHasBackgroundActivityFilter) {
			throw dv::exceptions::RuntimeError("Background Activity filter feature not available.");
		}

		return spiConfigReceive(MODULE_DVS, DVS_FILTER_BACKGROUND_ACTIVITY_TIME);
	}

	/**
	 * Set background activity noise filter time delta value.
	 * If 0, filtering is disabled.
	 * @param timeIn250us time delta value in 250µs multiples
	 */
	void setBackgroundActivityFilter(const uint16_t timeIn250us) {
		if (!mInfo.dvsHasBackgroundActivityFilter) {
			throw dv::exceptions::RuntimeError("Background Activity filter feature not available.");
		}

		if (timeIn250us > 4095) {
			throw dv::exceptions::RuntimeError("Background Activity value invalid..");
		}

		std::array<SPIConfigurationParameters, 3> filterConfig{};
		filterConfig[0].moduleAddress    = MODULE_DVS;
		filterConfig[0].parameterAddress = DVS_FILTER_BACKGROUND_ACTIVITY;
		filterConfig[0].parameterValue   = false;
		filterConfig[1].moduleAddress    = MODULE_DVS;
		filterConfig[1].parameterAddress = DVS_FILTER_BACKGROUND_ACTIVITY_TIME;
		filterConfig[1].parameterValue   = timeIn250us;
		filterConfig[2].moduleAddress    = MODULE_DVS;
		filterConfig[2].parameterAddress = DVS_FILTER_BACKGROUND_ACTIVITY;
		filterConfig[2].parameterValue   = (timeIn250us != 0);

		spiConfigSendMultiple(filterConfig);
	}

	/**
	 * Get refractory period filter time delta value.
	 * If 0, filtering is disabled.
	 * @return time delta value in 250µs multiples
	 */
	[[nodiscard]] uint16_t getRefractoryPeriodFilter() const {
		if (!mInfo.dvsHasBackgroundActivityFilter) {
			throw dv::exceptions::RuntimeError("Refractory Period filter feature not available.");
		}

		return spiConfigReceive(MODULE_DVS, DVS_FILTER_REFRACTORY_PERIOD_TIME);
	}

	/**
	 * Set refractory period filter time delta value.
	 * If 0, filtering is disabled.
	 * @param timeIn250us time delta value in 250µs multiples
	 */
	void setRefractoryPeriodFilter(const uint16_t timeIn250us) {
		if (!mInfo.dvsHasBackgroundActivityFilter) {
			throw dv::exceptions::RuntimeError("Refractory Period filter feature not available.");
		}

		if (timeIn250us > 4095) {
			throw dv::exceptions::RuntimeError("Refractory Period value invalid..");
		}

		std::array<SPIConfigurationParameters, 3> filterConfig{};
		filterConfig[0].moduleAddress    = MODULE_DVS;
		filterConfig[0].parameterAddress = DVS_FILTER_REFRACTORY_PERIOD;
		filterConfig[0].parameterValue   = false;
		filterConfig[1].moduleAddress    = MODULE_DVS;
		filterConfig[1].parameterAddress = DVS_FILTER_REFRACTORY_PERIOD_TIME;
		filterConfig[1].parameterValue   = timeIn250us;
		filterConfig[2].moduleAddress    = MODULE_DVS;
		filterConfig[2].parameterAddress = DVS_FILTER_REFRACTORY_PERIOD;
		filterConfig[2].parameterValue   = (timeIn250us != 0);

		spiConfigSendMultiple(filterConfig);
	}

	/**
	 * Get event skip filter skip value.
	 * If 0, filtering is disabled.
	 * @return drop every Nth event
	 */
	[[nodiscard]] uint8_t getSkipFilter() const {
		if (!mInfo.dvsHasSkipFilter) {
			throw dv::exceptions::RuntimeError("Skip filter feature not available.");
		}

		return spiConfigReceive(MODULE_DVS, DVS_FILTER_SKIP_EVENTS_EVERY);
	}

	/**
	 * Set event skip filter skip value. Drops every Nth event.
	 * If 0, filtering is disabled.
	 * @param skipEvery drop every Nth event
	 */
	void setSkipFilter(const uint8_t skipEvery) {
		if (!mInfo.dvsHasSkipFilter) {
			throw dv::exceptions::RuntimeError("Skip filter feature not available.");
		}

		std::array<SPIConfigurationParameters, 3> filterConfig{};
		filterConfig[0].moduleAddress    = MODULE_DVS;
		filterConfig[0].parameterAddress = DVS_FILTER_SKIP_EVENTS;
		filterConfig[0].parameterValue   = false;
		filterConfig[1].moduleAddress    = MODULE_DVS;
		filterConfig[1].parameterAddress = DVS_FILTER_SKIP_EVENTS_EVERY;
		filterConfig[1].parameterValue   = skipEvery;
		filterConfig[2].moduleAddress    = MODULE_DVS;
		filterConfig[2].parameterAddress = DVS_FILTER_SKIP_EVENTS;
		filterConfig[2].parameterValue   = (skipEvery != 0);

		spiConfigSendMultiple(filterConfig);
	}

	[[nodiscard]] Flatten getFlatten() const override {
		if (!mInfo.dvsHasPolarityFilter) {
			throw dv::exceptions::RuntimeError("Polarity filter feature not available.");
		}

		std::scoped_lock lock{mConfigLock};

		const auto flatten      = mParser->getDVSConvertAllOn();
		const auto suppress     = spiConfigReceive(MODULE_DVS, DVS_FILTER_POLARITY_SUPPRESS);
		const auto suppressType = spiConfigReceive(MODULE_DVS, DVS_FILTER_POLARITY_SUPPRESS_TYPE);

		if (flatten) {
			return Flatten::FLATTEN_ON;
		}
		else if (suppress) {
			return (suppressType) ? (Flatten::OFF_ONLY) : (Flatten::ON_ONLY);
		}
		else {
			return Flatten::NONE;
		}
	}

	void setFlatten(const Flatten flatten) override {
		if (!mInfo.dvsHasPolarityFilter) {
			throw dv::exceptions::RuntimeError("Polarity filter feature not available.");
		}

		std::scoped_lock lock{mConfigLock};

		switch (flatten) {
			case Flatten::NONE:
				mParser->setDVSConvertAllOn(false);
				spiConfigSend(MODULE_DVS, DVS_FILTER_POLARITY_SUPPRESS, false);
				spiConfigSend(MODULE_DVS, DVS_FILTER_POLARITY_SUPPRESS_TYPE, false);
				break;

			case Flatten::FLATTEN_ON:
				mParser->setDVSConvertAllOn(true);
				spiConfigSend(MODULE_DVS, DVS_FILTER_POLARITY_SUPPRESS, false);
				spiConfigSend(MODULE_DVS, DVS_FILTER_POLARITY_SUPPRESS_TYPE, false);
				break;

			case Flatten::ON_ONLY:
				mParser->setDVSConvertAllOn(false);
				spiConfigSend(MODULE_DVS, DVS_FILTER_POLARITY_SUPPRESS, true);
				spiConfigSend(MODULE_DVS, DVS_FILTER_POLARITY_SUPPRESS_TYPE, false);
				break;

			case Flatten::OFF_ONLY:
				mParser->setDVSConvertAllOn(false);
				spiConfigSend(MODULE_DVS, DVS_FILTER_POLARITY_SUPPRESS, true);
				spiConfigSend(MODULE_DVS, DVS_FILTER_POLARITY_SUPPRESS_TYPE, true);
				break;
		}
	}

	[[nodiscard]] cv::Rect getCropArea() const override {
		return getCropAreaEvents();
	}

	void setCropArea(const cv::Rect cropAreaEvents) override {
		setCropAreaEvents(cropAreaEvents);
	}

	/**
	 * Get events Region of Interest (ROI).
	 * @return get events Region of Interest (ROI).
	 */
	[[nodiscard]] cv::Rect getCropAreaEvents() const {
		if (!mInfo.dvsHasROIFilter) {
			throw dv::exceptions::RuntimeError("ROI filter feature not available.");
		}

		std::scoped_lock lock{mConfigLock};

		return mCropAreaEvents;
	}

	/**
	 * Set events Region of Interest (ROI). Usually hardware accelerated.
	 * @param cropAreaEvents region of interest (ROI) position and size.
	 */
	void setCropAreaEvents(cv::Rect cropAreaEvents) {
		if (!mInfo.dvsHasROIFilter) {
			throw dv::exceptions::RuntimeError("ROI filter feature not available.");
		}

		if ((cropAreaEvents.x >= mEventResolution.width)
			|| ((cropAreaEvents.x + cropAreaEvents.width - 1) >= mEventResolution.width)
			|| (cropAreaEvents.y >= mEventResolution.height)
			|| ((cropAreaEvents.y + cropAreaEvents.height - 1) >= mEventResolution.height)) {
			throw dv::exceptions::InvalidArgument<cv::Rect>("The events crop area is invalid.", cropAreaEvents);
		}

		std::scoped_lock lock{mConfigLock};

		mCropAreaEvents = cropAreaEvents;

		setCropAreaEventsInternal(mCropAreaEvents);
	}

	/**
	 * Report if the frame output is running.
	 * @return true if frames active, false otherwise.
	 */
	[[nodiscard]] bool isFramesRunning() const {
		std::scoped_lock lock{mConfigLock};

		return spiConfigReceive(MODULE_APS, APS_RUN);
	}

	/**
	 * Enable or disable the frame output.
	 * @param run whether to enable the frame output or not.
	 */
	void setFramesRunning(const bool run) {
		std::scoped_lock lock{mConfigLock};

		return spiConfigSend(MODULE_APS, APS_RUN, run);
	}

	/**
	 * Takes a snapshot (one frame), like a photo-camera.
	 * More efficient implementation that just toggling
	 * setFramesRunning() on and off.
	 * The frame output should not be running prior to calling this,
	 * as it only makes sense if frames are not being generated
	 * at the time. Also, setFrameInterval() should
	 * be set to zero if only doing snapshots, to ensure a
	 * quicker readiness for the next one, since the delay is
	 * always observed after taking a frame.
	 */
	void snapshot() {
		std::scoped_lock lock{mConfigLock};

		std::array<SPIConfigurationParameters, 2> snapshot{};
		snapshot[0].moduleAddress    = MODULE_APS;
		snapshot[0].parameterAddress = APS_RUN;
		snapshot[0].parameterValue   = true;
		snapshot[1].moduleAddress    = MODULE_APS;
		snapshot[1].parameterAddress = APS_RUN;
		snapshot[1].parameterValue   = false;

		spiConfigSendMultiple(snapshot);
	}

	/**
	 * Get frames color mode.
	 * @return frame color mode
	 */
	[[nodiscard]] dv::io::camera::parser::DAVIS::ColorMode getColorMode() const {
		return mParser->getFrameColorMode();
	}

	/**
	 * Set frames color mode. For monochrome sensors, all modes are equivalent.
	 * For color sensors, DEFAULT will output an RGB frame after debayering, GRAYSCALE will debayer to one channel, and
	 * ORIGINAL will return the unchanged readout from the pixels, in case you want to do debayering yourself.
	 * @param colorMode frame color mode
	 */
	void setColorMode(const dv::io::camera::parser::DAVIS::ColorMode colorMode) {
		mParser->setFrameColorMode(colorMode);
	}

	/**
	 * Report if auto-exposure feature is enabled.
	 * @return true if auto-exposure enabled, false otherwise.
	 */
	[[nodiscard]] bool getAutoExposure() const {
		std::scoped_lock lock{mConfigLock, mCallbackConfigLock};

		return mAutoExposureEnabled;
	}

	/**
	 * Enable auto-exposure feature.
	 * Automatic exposure control, tries to set the exposure value
	 * automatically to an appropriate value to maximize information
	 * in the scene and minimize under- and over-exposure.
	 * @param enabled true to enable auto-exposure, false to disable.
	 */
	void setAutoExposure(const bool enabled) {
		std::scoped_lock lock{mConfigLock, mCallbackConfigLock};

		mAutoExposureEnabled = enabled;
	}

	/**
	 * Set custom callback function to compute new exposure value based on current frame output.
	 * @param callback
	 */
	void setAutoExposureCallback(AutoExposureCallback callback) {
		std::scoped_lock lock{mConfigLock, mCallbackConfigLock};

		mAutoExposureCallback = std::move(callback);
	}

	/**
	 * Get current frame exposure duration.
	 * Also works with auto-exposure, reporting last automatically set value.
	 * @return current exposure duration in microseconds.
	 */
	[[nodiscard]] std::chrono::microseconds getExposureDuration() const {
		std::scoped_lock lock{mConfigLock};

		// Times are in µs on host, but in cycles @ ADC_CLOCK_FREQ on FPGA, so we adjust for that.
		const auto lengthInTicks = spiConfigReceive(MODULE_APS, APS_EXPOSURE);

		return std::chrono::microseconds{std::llround(static_cast<float>(lengthInTicks) / mADCClockActual)};
	}

	/**
	 * Set frame exposure duration.
	 * Auto-exposure must be disabled.
	 * @param exposureDurationUs exposure duration in microseconds.
	 */
	void setExposureDuration(const std::chrono::microseconds exposureDurationUs) {
		if ((exposureDurationUs.count() < 1) || (exposureDurationUs.count() > EXPOSURE_MAX)) {
			throw dv::exceptions::InvalidArgument<std::chrono::microseconds>(
				"The exposure duration is invalid.", exposureDurationUs);
		}

		std::scoped_lock lock{mConfigLock};

		{
			std::scoped_lock cbLock{mCallbackConfigLock};

			if (mAutoExposureEnabled) {
				throw dv::exceptions::RuntimeError("Cannot set exposure manually when auto-exposure enabled.");
			}

			mAutoExposureLast = exposureDurationUs;
		}

		// Times are in µs on host, but in cycles @ ADC_CLOCK_FREQ on FPGA, so we adjust for that.
		auto lengthInTicks
			= static_cast<uint32_t>(std::lround(static_cast<float>(exposureDurationUs.count()) * mADCClockActual));

		lengthInTicks = std::clamp<uint32_t>(lengthInTicks, 1, static_cast<uint32_t>(EXPOSURE_MAX * mADCClockActual));

		spiConfigSend(MODULE_APS, APS_EXPOSURE, lengthInTicks);
	}

	/**
	 * Get frame interval, the time between two consecutive frames.
	 * @return frame interval in microseconds.
	 */
	[[nodiscard]] std::chrono::microseconds getFrameInterval() const {
		// Times are in µs on host, but in cycles @ ADC_CLOCK_FREQ on FPGA, so we adjust for that.
		const auto lengthInTicks = spiConfigReceive(MODULE_APS, APS_FRAME_INTERVAL);

		return std::chrono::microseconds{std::llround(static_cast<float>(lengthInTicks) / mADCClockActual)};
	}

	/**
	 * Set frame interval, the time between two consecutive frames.
	 * @param frameIntervalUs frame interval in microseconds.
	 */
	void setFrameInterval(const std::chrono::microseconds frameIntervalUs) {
		if ((frameIntervalUs.count() < 0) || (frameIntervalUs.count() > FRAME_INTERVAL_MAX)) {
			throw dv::exceptions::InvalidArgument<std::chrono::microseconds>(
				"The frame interval duration is invalid.", frameIntervalUs);
		}

		// Times are in µs on host, but in cycles @ ADC_CLOCK_FREQ on FPGA, so we adjust for that.
		auto lengthInTicks
			= static_cast<uint32_t>(std::lround(static_cast<float>(frameIntervalUs.count()) * mADCClockActual));

		lengthInTicks
			= std::clamp<uint32_t>(lengthInTicks, 0, static_cast<uint32_t>(FRAME_INTERVAL_MAX * mADCClockActual));

		spiConfigSend(MODULE_APS, APS_FRAME_INTERVAL, lengthInTicks);
	}

	/**
	 * Status of horizontal frames flip.
	 * @return status of horizontal frames flip.
	 */
	[[nodiscard]] bool getFlipHorizontalFrames() const {
		return mParser->getFrameFlipHorizontal();
	}

	/**
	 * Flip frames horizontally.
	 * @param flipHorizontalFrames flip frames horizontally.
	 */
	void setFlipHorizontalFrames(const bool flipHorizontalFrames) {
		std::scoped_lock lock{mConfigLock};

		mParser->setFrameFlipHorizontal(flipHorizontalFrames);

		setCropAreaFramesInternal(mCropAreaFrames);
	}

	/**
	 * Status of vertical frames flip.
	 * @return status of vertical frames flip.
	 */
	[[nodiscard]] bool getFlipVerticalFrames() const {
		return mParser->getFrameFlipVertical();
	}

	/**
	 * Flip frames vertically.
	 * @param flipVerticalFrames flip frames vertically.
	 */
	void setFlipVerticalFrames(const bool flipVerticalFrames) {
		std::scoped_lock lock{mConfigLock};

		mParser->setFrameFlipVertical(flipVerticalFrames);

		setCropAreaFramesInternal(mCropAreaFrames);
	}

	/**
	 * Get frames Region of Interest (ROI).
	 * @return get frames Region of Interest (ROI).
	 */
	[[nodiscard]] cv::Rect getCropAreaFrames() const {
		std::scoped_lock lock{mConfigLock};

		return mCropAreaFrames;
	}

	/**
	 * Set frames Region of Interest (ROI). Usually hardware accelerated.
	 * @param cropAreaFrames region of interest (ROI) position and size.
	 */
	void setCropAreaFrames(cv::Rect cropAreaFrames) {
		if ((cropAreaFrames.x >= mFrameResolution.width)
			|| ((cropAreaFrames.x + cropAreaFrames.width - 1) >= mFrameResolution.width)
			|| (cropAreaFrames.y >= mFrameResolution.height)
			|| ((cropAreaFrames.y + cropAreaFrames.height - 1) >= mFrameResolution.height)) {
			throw dv::exceptions::InvalidArgument<cv::Rect>("The frames crop area is invalid.", cropAreaFrames);
		}

		// Color sensors and CDAVIS work in groups of four pixels, so ROI must be adjusted to
		// have an even length, possibly a bit bigger than the user-specified ROI.
		if ((mSensorModel == parser::DAVIS::SensorModel::CDAVIS)
			|| (mParser->getFrameColorFilter() != dv::PixelArrangement::MONO)) {
			if (cropAreaFrames.width & 0x01) {
				cropAreaFrames.width++;
			}

			if (cropAreaFrames.height & 0x01) {
				cropAreaFrames.height++;
			}
		}

		std::scoped_lock lock{mConfigLock};

		mCropAreaFrames = cropAreaFrames;

		setCropAreaFramesInternal(mCropAreaFrames);
	}

	/**
	 * Status of IMU accelerometer.
	 * @return true if enabled, false otherwise.
	 */
	[[nodiscard]] bool isImuRunningAccelerometer() const {
		return spiConfigReceive(MODULE_IMU, IMU_RUN_ACCELEROMETER);
	}

	/**
	 * Enable or disable IMU accelerometer.
	 * @param run true to enable, false to disable.
	 */
	void setImuRunningAccelerometer(const bool run) {
		spiConfigSend(MODULE_IMU, IMU_RUN_ACCELEROMETER, run);
	}

	/**
	 * Status of IMU gyroscope.
	 * @return true if enabled, false otherwise.
	 */
	[[nodiscard]] bool isImuRunningGyroscope() const {
		return spiConfigReceive(MODULE_IMU, IMU_RUN_GYROSCOPE);
	}

	/**
	 * Enable or disable IMU gyroscope.
	 * @param run true to enable, false to disable.
	 */
	void setImuRunningGyroscope(const bool run) {
		spiConfigSend(MODULE_IMU, IMU_RUN_GYROSCOPE, run);
	}

	/**
	 * Status of IMU temperature measurement.
	 * @return true if enabled, false otherwise.
	 */
	[[nodiscard]] bool isImuRunningTemperature() const {
		return spiConfigReceive(MODULE_IMU, IMU_RUN_TEMPERATURE);
	}

	/**
	 * Enable or disable IMU temperature measurement.
	 * @param run true to enable, false to disable.
	 */
	void setImuRunningTemperature(const bool run) {
		spiConfigSend(MODULE_IMU, IMU_RUN_TEMPERATURE, run);
	}

	/**
	 * Status of IMU X axis flipping.
	 * @return true if enabled, false otherwise.
	 */
	[[nodiscard]] bool getIMUFlipX() const {
		return mParser->getIMUFlipX();
	}

	/**
	 * Enable or disable IMU X axis flipping.
	 * Will negate (flip) all returned X axis values.
	 * @param flipX true to enable, false to disable.
	 */
	void setIMUFlipX(const bool flipX) {
		mParser->setIMUFlipX(flipX);
	}

	/**
	 * Status of IMU Y axis flipping.
	 * @return true if enabled, false otherwise.
	 */
	[[nodiscard]] bool getIMUFlipY() const {
		return mParser->getIMUFlipY();
	}

	/**
	 * Enable or disable IMU Y axis flipping.
	 * Will negate (flip) all returned Y axis values.
	 * @param flipY true to enable, false to disable.
	 */
	void setIMUFlipY(const bool flipY) {
		mParser->setIMUFlipY(flipY);
	}

	/**
	 * Status of IMU Z axis flipping.
	 * @return true if enabled, false otherwise.
	 */
	[[nodiscard]] bool getIMUFlipZ() const {
		return mParser->getIMUFlipZ();
	}

	/**
	 * Enable or disable IMU Z axis flipping.
	 * Will negate (flip) all returned Z axis values.
	 * @param flipZ true to enable, false to disable.
	 */
	void setIMUFlipZ(const bool flipZ) {
		mParser->setIMUFlipZ(flipZ);
	}

	/**
	 * Get current IMU accelerometer range.
	 * @return accelerometer range.
	 */
	[[nodiscard]] imu::InvensenseAccelRange getImuAccelRange() const {
		return dv::IntegerAsEnum<imu::InvensenseAccelRange>(spiConfigReceive(MODULE_IMU, IMU_ACCEL_FULL_SCALE));
	}

	/**
	 * Set IMU accelerometer range.
	 * @param range accelerometer range.
	 */
	void setImuAccelRange(const imu::InvensenseAccelRange range) {
		spiConfigSend(MODULE_IMU, IMU_ACCEL_FULL_SCALE, dv::EnumAsInteger(range));
	}

	/**
	 * Get current IMU gyroscope range.
	 * @return gyroscope range.
	 */
	[[nodiscard]] imu::InvensenseGyroRange getImuGyroRange() const {
		return dv::IntegerAsEnum<imu::InvensenseGyroRange>(spiConfigReceive(MODULE_IMU, IMU_GYRO_FULL_SCALE));
	}

	/**
	 * Set IMU gyroscope range.
	 * @param range gyroscope range.
	 */
	void setImuGyroRange(const imu::InvensenseGyroRange range) {
		spiConfigSend(MODULE_IMU, IMU_GYRO_FULL_SCALE, dv::EnumAsInteger(range));
	}

	/**
	 * Report status of external signal detector.
	 * @return true if running, false otherwise.
	 */
	[[nodiscard]] bool isDetectorRunning() const {
		return spiConfigReceive(MODULE_EXTERNAL_INPUT, EXTINPUT_RUN_DETECTOR);
	}

	/**
	 * Enable or disable external signal detector.
	 * @param run true to enable, false to disable.
	 */
	void setDetectorRunning(const bool run) {
		spiConfigSend(MODULE_EXTERNAL_INPUT, EXTINPUT_RUN_DETECTOR, run);
	}

	/**
	 * Report status of rising edge detection on the SIGNAL_IN line.
	 * @return true if enabled, false otherwise.
	 */
	[[nodiscard]] bool getDetectorRisingEdges() const {
		return spiConfigReceive(MODULE_EXTERNAL_INPUT, EXTINPUT_DETECT_RISING_EDGES);
	}

	/**
	 * Detect rising edges (low to high transitions) on the SIGNAL_IN line.
	 * @param detectRising true to enable, false to disable.
	 */
	void setDetectorRisingEdges(const bool detectRising) {
		spiConfigSend(MODULE_EXTERNAL_INPUT, EXTINPUT_DETECT_RISING_EDGES, detectRising);
	}

	/**
	 * Report status of falling edge detection on the SIGNAL_IN line.
	 * @return true if enabled, false otherwise.
	 */
	[[nodiscard]] bool getDetectorFallingEdges() const {
		return spiConfigReceive(MODULE_EXTERNAL_INPUT, EXTINPUT_DETECT_FALLING_EDGES);
	}

	/**
	 * Detect falling edges (high to low transitions) on the SIGNAL_IN line.
	 * @param detectFalling true to enable, false to disable.
	 */
	void setDetectorFallingEdges(const bool detectFalling) {
		spiConfigSend(MODULE_EXTERNAL_INPUT, EXTINPUT_DETECT_FALLING_EDGES, detectFalling);
	}

	/**
	 * Report status of external signal generator.
	 * @return true if running, false otherwise.
	 */
	[[nodiscard]] bool isGeneratorRunning() const {
		return spiConfigReceive(MODULE_EXTERNAL_INPUT, EXTINPUT_RUN_GENERATOR);
	}

	/**
	 * Enable or disable external signal generator (PWM-like output).
	 * @param run true to enable, false to disable.
	 */
	void setGeneratorRunning(const bool run) {
		spiConfigSend(MODULE_EXTERNAL_INPUT, EXTINPUT_RUN_GENERATOR, run);
	}

	/**
	 * Get current PWM low time.
	 * @return low time in microseconds.
	 */
	[[nodiscard]] std::chrono::microseconds getGeneratorLowTime() const {
		std::scoped_lock lock{mConfigLock};

		return getGeneratorLowTimeInternal() - getGeneratorHighTimeInternal();
	}

	/**
	 * Set PWM low time for external signal generator.
	 * @param lowTimeUs low time in microseconds.
	 */
	void setGeneratorLowTime(const std::chrono::microseconds lowTimeUs) {
		if ((lowTimeUs.count() < 1) || (lowTimeUs.count() > EXT_INPUT_TIME_MAX)) {
			throw dv::exceptions::InvalidArgument<std::chrono::microseconds>(
				"The generator low time duration is invalid.", lowTimeUs);
		}

		std::scoped_lock lock{mConfigLock};

		const auto intervalTimeUs = lowTimeUs + getGeneratorHighTimeInternal();

		// Times are in µs on host, but in cycles @ LOGIC_CLOCK_FREQ on FPGA, so we adjust for that.
		auto intervalInTicks
			= static_cast<uint32_t>(std::lround(static_cast<float>(intervalTimeUs.count()) * mLogicClockActual));

		intervalInTicks
			= std::clamp<uint32_t>(intervalInTicks, 0, static_cast<uint32_t>(EXT_INPUT_TIME_MAX * mLogicClockActual));

		spiConfigSend(MODULE_EXTERNAL_INPUT, EXTINPUT_GENERATE_PULSE_INTERVAL, intervalInTicks);
	}

	/**
	 * Get current PWM high time.
	 * @return high time in microseconds.
	 */
	[[nodiscard]] std::chrono::microseconds getGeneratorHighTime() const {
		std::scoped_lock lock{mConfigLock};

		return getGeneratorHighTimeInternal();
	}

	/**
	 * Set PWM high time for external signal generator.
	 * @param highTimeUs high time in microseconds.
	 */
	void setGeneratorHighTime(const std::chrono::microseconds highTimeUs) {
		if ((highTimeUs.count() < 1) || (highTimeUs.count() > EXT_INPUT_TIME_MAX)) {
			throw dv::exceptions::InvalidArgument<std::chrono::microseconds>(
				"The generator low time duration is invalid.", highTimeUs);
		}

		std::scoped_lock lock{mConfigLock};

		// Times are in µs on host, but in cycles @ LOGIC_CLOCK_FREQ on FPGA, so we adjust for that.
		auto lengthInTicks
			= static_cast<uint32_t>(std::lround(static_cast<float>(highTimeUs.count()) * mLogicClockActual));

		lengthInTicks
			= std::clamp<uint32_t>(lengthInTicks, 0, static_cast<uint32_t>(EXT_INPUT_TIME_MAX * mLogicClockActual));

		spiConfigSend(MODULE_EXTERNAL_INPUT, EXTINPUT_GENERATE_PULSE_LENGTH, lengthInTicks);
	}

	/**
	 * Report status of trigger event injection feature for external signal generator rising edges.
	 * @return true if enabled, false otherwise.
	 */
	[[nodiscard]] bool getGeneratorInjectTriggerOnRisingEdge() const {
		return spiConfigReceive(MODULE_EXTERNAL_INPUT, EXTINPUT_GENERATE_INJECT_ON_RISING_EDGE);
	}

	/**
	 * Inject a trigger event of type EXTERNAL_GENERATOR_RISING_EDGE into the event stream from the device,
	 * every time a rising edge is generated by the PWM-like output of the external signal generator.
	 *
	 * @param injectRising true to inject trigger event, false to disable.
	 */
	void setGeneratorInjectTriggerOnRisingEdge(const bool injectRising) {
		spiConfigSend(MODULE_EXTERNAL_INPUT, EXTINPUT_GENERATE_INJECT_ON_RISING_EDGE, injectRising);
	}

	/**
	 * Report status of trigger event injection feature for external signal generator falling edges.
	 * @return true if enabled, false otherwise.
	 */
	[[nodiscard]] bool getGeneratorInjectTriggerOnFallingEdge() const {
		return spiConfigReceive(MODULE_EXTERNAL_INPUT, EXTINPUT_GENERATE_INJECT_ON_FALLING_EDGE);
	}

	/**
	 * Inject a trigger event of type EXTERNAL_GENERATOR_FALLING_EDGE into the event stream from the device,
	 * every time a falling edge is generated by the PWM-like output of the external signal generator.
	 *
	 * @param injectFalling true to inject trigger event, false to disable.
	 */
	void setGeneratorInjectTriggerOnFallingEdge(const bool injectFalling) {
		spiConfigSend(MODULE_EXTERNAL_INPUT, EXTINPUT_GENERATE_INJECT_ON_FALLING_EDGE, injectFalling);
	}

	/**
	 * Get low-level coarse/fine values for a specific bias.
	 * @param bias bias to query.
	 * @return coarse/fine values.
	 */
	[[nodiscard]] std::pair<uint8_t, uint8_t> getDavis240BiasCoarseFine(const Davis240BiasCF bias) const {
		if ((mSensorModel != parser::DAVIS::SensorModel::DAVIS240A)
			&& (mSensorModel != parser::DAVIS::SensorModel::DAVIS240B)
			&& (mSensorModel != parser::DAVIS::SensorModel::DAVIS240C)) {
			throw dv::exceptions::RuntimeError("Sensor is not a DAVIS240.");
		}

		std::scoped_lock lock{mConfigLock};

		const auto coarseFine = mCoarseFineBiases[dv::EnumAsInteger(bias)];

		return {coarseFine.coarseValue, coarseFine.fineValue};
	}

	/**
	 * Set specific bias to new low-level coarse/fine values.
	 * @param bias bias to configure.
	 * @param coarse coarse bias value.
	 * @param fine fine bias value.
	 */
	void setDavis240BiasCoarseFine(const Davis240BiasCF bias, const uint8_t coarse, const uint8_t fine) {
		if ((mSensorModel != parser::DAVIS::SensorModel::DAVIS240A)
			&& (mSensorModel != parser::DAVIS::SensorModel::DAVIS240B)
			&& (mSensorModel != parser::DAVIS::SensorModel::DAVIS240C)) {
			throw dv::exceptions::RuntimeError("Sensor is not a DAVIS240.");
		}

		if (coarse > 7) {
			throw dv::exceptions::InvalidArgument<uint8_t>("Invalid coarse value.", coarse);
		}

		std::scoped_lock lock{mConfigLock};

		mCoarseFineBiases[dv::EnumAsInteger(bias)].coarseValue = coarse;
		mCoarseFineBiases[dv::EnumAsInteger(bias)].fineValue   = fine;

		sendCoarseFineBias(dv::EnumAsInteger(bias));
	}

	/**
	 * Get voltage value in 52.4 milliVolt increments for a specific *voltage* bias.
	 * @param bias voltage bias to query.
	 * @return voltage value in 52.4 milliVolt increments, from 0 to 63.
	 */
	[[nodiscard]] uint8_t getDavis346BiasVoltage(const Davis346BiasVDAC bias) const {
		if ((mSensorModel != parser::DAVIS::SensorModel::DAVIS346)
			&& (mSensorModel != parser::DAVIS::SensorModel::DAVIS640)) {
			throw dv::exceptions::RuntimeError("Sensor is not a DAVIS346 or DAVIS640.");
		}

		std::scoped_lock lock{mConfigLock};

		return mVDACBiases[dv::EnumAsInteger(bias)].voltageValue;
	}

	/**
	 * Set specific *voltage* bias to a new value in 52.4 milliVolt increments.
	 * @param bias voltage bias to configure.
	 * @param voltageValue voltage value in 52.4 milliVolt increments, from 0 to 63.
	 */
	void setDavis346BiasVoltage(const Davis346BiasVDAC bias, const uint8_t voltageValue) {
		if ((mSensorModel != parser::DAVIS::SensorModel::DAVIS346)
			&& (mSensorModel != parser::DAVIS::SensorModel::DAVIS640)) {
			throw dv::exceptions::RuntimeError("Sensor is not a DAVIS346 or DAVIS640.");
		}

		if (voltageValue > 63) {
			throw dv::exceptions::InvalidArgument<uint8_t>("Invalid voltage value.", voltageValue);
		}

		std::scoped_lock lock{mConfigLock};

		mVDACBiases[dv::EnumAsInteger(bias)].voltageValue = voltageValue;

		sendVDACBias(dv::EnumAsInteger(bias));
	}

	/**
	 * Get low-level coarse/fine values for a specific bias.
	 * @param bias bias to query.
	 * @return coarse/fine values.
	 */
	[[nodiscard]] std::pair<uint8_t, uint8_t> getDavis346BiasCoarseFine(const Davis346BiasCF bias) const {
		if ((mSensorModel != parser::DAVIS::SensorModel::DAVIS346)
			&& (mSensorModel != parser::DAVIS::SensorModel::DAVIS640)) {
			throw dv::exceptions::RuntimeError("Sensor is not a DAVIS346 or DAVIS640.");
		}

		std::scoped_lock lock{mConfigLock};

		const auto coarseFine = mCoarseFineBiases[dv::EnumAsInteger(bias)];

		return {coarseFine.coarseValue, coarseFine.fineValue};
	}

	/**
	 * Set specific bias to new low-level coarse/fine values.
	 * @param bias bias to configure.
	 * @param coarse coarse bias value.
	 * @param fine fine bias value.
	 */
	void setDavis346BiasCoarseFine(const Davis346BiasCF bias, const uint8_t coarse, const uint8_t fine) {
		if ((mSensorModel != parser::DAVIS::SensorModel::DAVIS346)
			&& (mSensorModel != parser::DAVIS::SensorModel::DAVIS640)) {
			throw dv::exceptions::RuntimeError("Sensor is not a DAVIS346 or DAVIS640.");
		}

		if (coarse > 7) {
			throw dv::exceptions::InvalidArgument<uint8_t>("Invalid coarse value.", coarse);
		}

		std::scoped_lock lock{mConfigLock};

		mCoarseFineBiases[dv::EnumAsInteger(bias)].coarseValue = coarse;
		mCoarseFineBiases[dv::EnumAsInteger(bias)].fineValue   = fine;

		sendCoarseFineBias(dv::EnumAsInteger(bias));
	}

	/**
	 * Get voltage value in 52.4 milliVolt increments for a specific *voltage* bias.
	 * @param bias voltage bias to query.
	 * @return voltage value in 52.4 milliVolt increments, from 0 to 63.
	 */
	[[nodiscard]] uint8_t getCDavisBiasVoltage(const CDavisBiasVDAC bias) const {
		if (mSensorModel != parser::DAVIS::SensorModel::CDAVIS) {
			throw dv::exceptions::RuntimeError("Sensor is not a CDAVIS.");
		}

		std::scoped_lock lock{mConfigLock};

		return mVDACBiases[dv::EnumAsInteger(bias)].voltageValue;
	}

	/**
	 * Set specific *voltage* bias to a new value in 52.4 milliVolt increments.
	 * @param bias voltage bias to configure.
	 * @param voltageValue voltage value in 52.4 milliVolt increments, from 0 to 63.
	 */
	void setCDavisBiasVoltage(const CDavisBiasVDAC bias, const uint8_t voltageValue) {
		if (mSensorModel != parser::DAVIS::SensorModel::CDAVIS) {
			throw dv::exceptions::RuntimeError("Sensor is not a CDAVIS.");
		}

		if (voltageValue > 63) {
			throw dv::exceptions::InvalidArgument<uint8_t>("Invalid voltage value.", voltageValue);
		}

		std::scoped_lock lock{mConfigLock};

		mVDACBiases[dv::EnumAsInteger(bias)].voltageValue = voltageValue;

		sendVDACBias(dv::EnumAsInteger(bias));
	}

	/**
	 * Get low-level coarse/fine values for a specific bias.
	 * @param bias bias to query.
	 * @return coarse/fine values.
	 */
	[[nodiscard]] std::pair<uint8_t, uint8_t> getCDavisBiasCoarseFine(const CDavisBiasCF bias) const {
		if (mSensorModel != parser::DAVIS::SensorModel::CDAVIS) {
			throw dv::exceptions::RuntimeError("Sensor is not a CDAVIS.");
		}

		std::scoped_lock lock{mConfigLock};

		const auto coarseFine = mCoarseFineBiases[dv::EnumAsInteger(bias)];

		return {coarseFine.coarseValue, coarseFine.fineValue};
	}

	/**
	 * Set specific bias to new low-level coarse/fine values.
	 * @param bias bias to configure.
	 * @param coarse coarse bias value.
	 * @param fine fine bias value.
	 */
	void setCDavisBiasCoarseFine(const CDavisBiasCF bias, const uint8_t coarse, const uint8_t fine) {
		if (mSensorModel != parser::DAVIS::SensorModel::CDAVIS) {
			throw dv::exceptions::RuntimeError("Sensor is not a CDAVIS.");
		}

		if (coarse > 7) {
			throw dv::exceptions::InvalidArgument<uint8_t>("Invalid coarse value.", coarse);
		}

		std::scoped_lock lock{mConfigLock};

		mCoarseFineBiases[dv::EnumAsInteger(bias)].coarseValue = coarse;
		mCoarseFineBiases[dv::EnumAsInteger(bias)].fineValue   = fine;

		sendCoarseFineBias(dv::EnumAsInteger(bias));
	}

	[[nodiscard]] bool getCDavisAdjustOVG1Low() const {
		if (mSensorModel != parser::DAVIS::SensorModel::CDAVIS) {
			throw dv::exceptions::RuntimeError("Sensor is not a CDAVIS.");
		}

		return spiConfigReceive(MODULE_CHIP, CDAVIS_CHIP_ADJUSTOVG1LO);
	}

	void setCDavisAdjustOVG1Low(const bool enable) {
		if (mSensorModel != parser::DAVIS::SensorModel::CDAVIS) {
			throw dv::exceptions::RuntimeError("Sensor is not a CDAVIS.");
		}

		spiConfigSend(MODULE_CHIP, CDAVIS_CHIP_ADJUSTOVG1LO, enable);
	}

	[[nodiscard]] bool getCDavisAdjustOVG2Low() const {
		if (mSensorModel != parser::DAVIS::SensorModel::CDAVIS) {
			throw dv::exceptions::RuntimeError("Sensor is not a CDAVIS.");
		}

		return spiConfigReceive(MODULE_CHIP, CDAVIS_CHIP_ADJUSTOVG2LO);
	}

	void setCDavisAdjustOVG2Low(const bool enable) {
		if (mSensorModel != parser::DAVIS::SensorModel::CDAVIS) {
			throw dv::exceptions::RuntimeError("Sensor is not a CDAVIS.");
		}

		spiConfigSend(MODULE_CHIP, CDAVIS_CHIP_ADJUSTOVG2LO, enable);
	}

	[[nodiscard]] bool getCDavisAdjustTX2OVGHigh() const {
		if (mSensorModel != parser::DAVIS::SensorModel::CDAVIS) {
			throw dv::exceptions::RuntimeError("Sensor is not a CDAVIS.");
		}

		return spiConfigReceive(MODULE_CHIP, CDAVIS_CHIP_ADJUSTTX2OVG2HI);
	}

	void setCDavisAdjustTX2OVGHigh(const bool enable) {
		if (mSensorModel != parser::DAVIS::SensorModel::CDAVIS) {
			throw dv::exceptions::RuntimeError("Sensor is not a CDAVIS.");
		}

		spiConfigSend(MODULE_CHIP, CDAVIS_CHIP_ADJUSTTX2OVG2HI, enable);
	}

	/**
	 * Get value of USB early packet timeout.
	 * @return timeout in microseconds.
	 */
	[[nodiscard]] std::chrono::microseconds getUSBEarlyPacketDelay() const {
		// Times are in µs on host, but in cycles @ USB_CLOCK_FREQ on FPGA, so we adjust for that.
		const auto delayInTicks = spiConfigReceive(MODULE_USB, USB_EARLY_PACKET_DELAY);

		return std::chrono::microseconds{std::llround(static_cast<float>(delayInTicks) / mUSBClockActual)};
	}

	/**
	 * Send data over USB early if this timeout is reached, instead of waiting on buffers being full.
	 * Timeout on device is in 125µs time-slices.
	 * @param earlyPacketDelayUs timeout in microseconds.
	 */
	void setUSBEarlyPacketDelay(const std::chrono::microseconds earlyPacketDelayUs) {
		if ((earlyPacketDelayUs.count() < 125) || (earlyPacketDelayUs.count() > USB_EARLY_PACKET_DELAY_MAX)) {
			throw dv::exceptions::InvalidArgument<std::chrono::microseconds>(
				"The USB Early Packet delay is invalid.", earlyPacketDelayUs);
		}

		// Times are in µs on host, but in cycles @ USB_CLOCK_FREQ on FPGA, so we adjust for that.
		auto delayInTicks
			= static_cast<uint32_t>(std::lround(static_cast<float>(earlyPacketDelayUs.count()) * mUSBClockActual));

		delayInTicks = std::clamp<uint32_t>(
			delayInTicks, 0, static_cast<uint32_t>(USB_EARLY_PACKET_DELAY_MAX * mUSBClockActual));

		spiConfigSend(MODULE_USB, USB_EARLY_PACKET_DELAY, delayInTicks);
	}

protected:
	void sendTimestampReset() override {
		mSyncState = SyncState::WAIT_RESET;

		std::array<SPIConfigurationParameters, 2> tsReset{};
		tsReset[0].moduleAddress    = MODULE_MULTIPLEXER;
		tsReset[0].parameterAddress = MUX_TIMESTAMP_RESET;
		tsReset[0].parameterValue   = true;
		tsReset[1].moduleAddress    = MODULE_MULTIPLEXER;
		tsReset[1].parameterAddress = MUX_TIMESTAMP_RESET;
		tsReset[1].parameterValue   = false;

		spiConfigSendMultiple(tsReset);
	}

	void setTimestampOffset(const std::chrono::microseconds timestampOffset) override {
		mParser->setSystemOffset(timestampOffset);
		mBuffers.clearBuffers();
	}

	[[nodiscard]] std::chrono::microseconds getGeneratorHighTimeInternal() const {
		// Times are in µs on host, but in cycles @ LOGIC_CLOCK_FREQ on FPGA, so we adjust for that.
		const auto lengthInTicks = spiConfigReceive(MODULE_EXTERNAL_INPUT, EXTINPUT_GENERATE_PULSE_LENGTH);

		return std::chrono::microseconds{std::llround(static_cast<float>(lengthInTicks) / mLogicClockActual)};
	}

	[[nodiscard]] std::chrono::microseconds getGeneratorLowTimeInternal() const {
		// Times are in µs on host, but in cycles @ LOGIC_CLOCK_FREQ on FPGA, so we adjust for that.
		const auto intervalInTicks = spiConfigReceive(MODULE_EXTERNAL_INPUT, EXTINPUT_GENERATE_PULSE_INTERVAL);

		const auto intervalTimeUs
			= std::chrono::microseconds{std::llround(static_cast<float>(intervalInTicks) / mLogicClockActual)};

		return intervalTimeUs - getGeneratorHighTime();
	}

	void setCropAreaEventsInternal(cv::Rect cropAreaEvents) {
		// Adjust for horizontal and vertical flip.
		if (getFlipHorizontalEvents()) {
			cropAreaEvents.x = mEventResolution.width - cropAreaEvents.x - cropAreaEvents.width;
		}

		if (getFlipVerticalEvents()) {
			cropAreaEvents.y = mEventResolution.height - cropAreaEvents.y - cropAreaEvents.height;
		}

		const bool runState = spiConfigReceive(MODULE_DVS, DVS_RUN);

		std::array<SPIConfigurationParameters, 6> roiConfig{};
		roiConfig[0].moduleAddress    = MODULE_DVS;
		roiConfig[0].parameterAddress = DVS_RUN;
		roiConfig[0].parameterValue   = false;
		roiConfig[1].moduleAddress    = MODULE_DVS;
		roiConfig[1].parameterAddress = DVS_FILTER_ROI_START_COLUMN;
		roiConfig[1].parameterValue   = cropAreaEvents.x;
		roiConfig[2].moduleAddress    = MODULE_DVS;
		roiConfig[2].parameterAddress = DVS_FILTER_ROI_END_COLUMN;
		roiConfig[2].parameterValue   = (cropAreaEvents.x + cropAreaEvents.width - 1);
		roiConfig[3].moduleAddress    = MODULE_DVS;
		roiConfig[3].parameterAddress = DVS_FILTER_ROI_START_ROW;
		roiConfig[3].parameterValue   = cropAreaEvents.y;
		roiConfig[4].moduleAddress    = MODULE_DVS;
		roiConfig[4].parameterAddress = DVS_FILTER_ROI_END_ROW;
		roiConfig[4].parameterValue   = (cropAreaEvents.y + cropAreaEvents.height - 1);
		roiConfig[5].moduleAddress    = MODULE_DVS;
		roiConfig[5].parameterAddress = DVS_RUN;
		roiConfig[5].parameterValue   = runState;

		spiConfigSendMultiple(roiConfig);
	}

	void setCropAreaFramesInternal(cv::Rect cropAreaFrames) {
		// Adjust for horizontal and vertical flip.
		if (getFlipHorizontalFrames()) {
			cropAreaFrames.x = mFrameResolution.width - cropAreaFrames.x - cropAreaFrames.width;
		}

		if (getFlipVerticalFrames()) {
			cropAreaFrames.y = mFrameResolution.height - cropAreaFrames.y - cropAreaFrames.height;
		}

		const bool runState = spiConfigReceive(MODULE_APS, APS_RUN);

		std::array<SPIConfigurationParameters, 6> roiConfig{};
		roiConfig[0].moduleAddress    = MODULE_APS;
		roiConfig[0].parameterAddress = APS_RUN;
		roiConfig[0].parameterValue   = false;
		roiConfig[1].moduleAddress    = MODULE_APS;
		roiConfig[1].parameterAddress = APS_START_COLUMN_0;
		roiConfig[1].parameterValue   = cropAreaFrames.x;
		roiConfig[2].moduleAddress    = MODULE_APS;
		roiConfig[2].parameterAddress = APS_START_ROW_0;
		roiConfig[2].parameterValue   = cropAreaFrames.y;
		roiConfig[3].moduleAddress    = MODULE_APS;
		roiConfig[3].parameterAddress = APS_END_COLUMN_0;
		roiConfig[3].parameterValue   = cropAreaFrames.x + cropAreaFrames.width - 1;
		roiConfig[4].moduleAddress    = MODULE_APS;
		roiConfig[4].parameterAddress = APS_END_ROW_0;
		roiConfig[4].parameterValue   = cropAreaFrames.y + cropAreaFrames.height - 1;
		roiConfig[5].moduleAddress    = MODULE_APS;
		roiConfig[5].parameterAddress = APS_RUN;
		roiConfig[5].parameterValue   = runState;

		spiConfigSendMultiple(roiConfig);
	}

private:
	/**
	 * Module address: device-side Multiplexer configuration.
	 * The Multiplexer is responsible for mixing, timestamping and outputting
	 * (via USB) the various event types generated by the device. It is also
	 * responsible for timestamp generation and synchronization.
	 */
	static constexpr uint16_t MODULE_MULTIPLEXER{0};
	/**
	 * Module address: device-side DVS configuration.
	 * The DVS state machine handshakes with the chip's AER bus and gets the
	 * polarity events from it. It supports various configurable delays, as
	 * well as advanced filtering capabilities on the polarity events.
	 */
	static constexpr uint16_t MODULE_DVS{1};
	/**
	 * Module address: device-side APS (Frame) configuration.
	 * The APS (Active-Pixel-Sensor) is responsible for getting the normal,
	 * synchronous frame from the camera chip. It supports various options
	 * for very precise timing control, as well as Region of Interest imaging.
	 */
	static constexpr uint16_t MODULE_APS{2};
	/**
	 * Module address: device-side IMU (Inertial Measurement Unit) configuration.
	 * The IMU module connects to the external IMU chip and sends data on the
	 * device's movement in space. It can configure various options on the external
	 * chip, such as accelerometer range or gyroscope refresh rate.
	 */
	static constexpr uint16_t MODULE_IMU{3};
	/**
	 * Module address: device-side External Input (signal detector/generator) configuration.
	 * The External Input module is used to detect external signals on the external input
	 * jack and inject an event into the event stream when this happens. It can detect pulses
	 * of a specific length or rising and falling edges.
	 * On some systems, a signal generator module is also present, which can generate
	 * PWM-like pulsed signals with configurable timing.
	 */
	static constexpr uint16_t MODULE_EXTERNAL_INPUT{4};
	/**
	 * Module address: device-side chip bias configuration.
	 * Shared with DAVIS_CONFIG_CHIP.
	 * This state machine is responsible for configuring the chip's bias generator.
	 */
	static constexpr uint16_t MODULE_BIAS{5};
	/**
	 * Module address: device-side chip control configuration.
	 * Shared with DAVIS_CONFIG_BIAS.
	 * This state machine is responsible for configuring the chip's internal
	 * control shift registers, to set special options.
	 */
	static constexpr uint16_t MODULE_CHIP{5};
	/**
	 * Module address: device-side system information.
	 * The system information module provides various details on the device, such
	 * as currently installed logic revision or clock speeds.
	 * All its parameters are read-only.
	 * This is reserved for internal use and should not be used by
	 * anything other than libcaer. Please see the 'struct caer_davis_info'
	 * documentation for more details on what information is available.
	 */
	static constexpr uint16_t MODULE_SYSINFO{6};
	/**
	 * Module address: device-side USB output configuration.
	 * The USB output module forwards the data from the device and the FPGA/CPLD to
	 * the USB chip, usually a Cypress FX2 or FX3.
	 */
	static constexpr uint16_t MODULE_USB{9};

	/**
	 * Parameter address for module DAVIS_CONFIG_MUX:
	 * run the Multiplexer state machine, which is responsible for
	 * mixing the various event types at the device level, timestamping
	 * them and outputting them via USB or other connectors.
	 */
	static constexpr uint16_t MUX_RUN{0};
	/**
	 * Parameter address for module DAVIS_CONFIG_MUX:
	 * run the Timestamp Generator inside the Multiplexer state machine,
	 * which will provide microsecond accurate timestamps to the
	 * events passing through.
	 */
	static constexpr uint16_t MUX_TIMESTAMP_RUN{1};
	/**
	 * Parameter address for module DAVIS_CONFIG_MUX:
	 * reset the Timestamp Generator to zero. This also sends a reset
	 * pulse to all connected slave devices, resetting their timestamp too.
	 */
	static constexpr uint16_t MUX_TIMESTAMP_RESET{2};
	/**
	 * Parameter address for module DAVIS_CONFIG_MUX:
	 * power up the chip's bias generator, enabling the chip to work.
	 */
	static constexpr uint16_t MUX_RUN_CHIP{3};
	/**
	 * Parameter address for module DAVIS_CONFIG_MUX:
	 * drop External Input events if the USB output FIFO is full, instead of having
	 * them pile up at the input FIFOs.
	 */
	static constexpr uint16_t MUX_DROP_EXTINPUT_ON_TRANSFER_STALL{4};
	/**
	 * Parameter address for module DAVIS_CONFIG_MUX:
	 * drop DVS events if the USB output FIFO is full, instead of having
	 * them pile up at the input FIFOs.
	 */
	static constexpr uint16_t MUX_DROP_DVS_ON_TRANSFER_STALL{5};
	/**
	 * Parameter address for module DAVIS_CONFIG_MUX:
	 * read-only parameter, information about the presence of the
	 * statistics feature.
	 * This is reserved for internal use and should not be used by
	 * anything other than libcaer. Please see the 'struct caer_davis_info'
	 * documentation to get this information.
	 */
	static constexpr uint16_t MUX_HAS_STATISTICS{80};
	/**
	 * Parameter address for module DAVIS_CONFIG_MUX:
	 * read-only parameter, representing the number of dropped
	 * External Input events on the device due to full USB buffers.
	 * This is a 64bit value, and should always be read using the
	 * function: caerDeviceConfigGet64().
	 */
	static constexpr uint16_t MUX_STATISTICS_EXTINPUT_DROPPED{81};
	/**
	 * Parameter address for module DAVIS_CONFIG_MUX:
	 * read-only parameter, representing the number of dropped
	 * DVS events on the device due to full USB buffers.
	 * This is a 64bit value, and should always be read using the
	 * function: caerDeviceConfigGet64().
	 */
	static constexpr uint16_t MUX_STATISTICS_DVS_DROPPED{83};

	/**
	 * Parameter address for module DAVIS_CONFIG_DVS:
	 * read-only parameter, contains the X axis resolution of the
	 * DVS events returned by the camera.
	 * This is reserved for internal use and should not be used by
	 * anything other than libcaer. Please see the 'struct caer_davis_info'
	 * documentation to get proper size information that already
	 * considers the rotation and orientation settings.
	 */
	static constexpr uint16_t DVS_SIZE_COLUMNS{0};
	/**
	 * Parameter address for module DAVIS_CONFIG_DVS:
	 * read-only parameter, contains the Y axis resolution of the
	 * DVS events returned by the camera.
	 * This is reserved for internal use and should not be used by
	 * anything other than libcaer. Please see the 'struct caer_davis_info'
	 * documentation to get proper size information that already
	 * considers the rotation and orientation settings.
	 */
	static constexpr uint16_t DVS_SIZE_ROWS{1};
	/**
	 * Parameter address for module DAVIS_CONFIG_DVS:
	 * read-only parameter, contains information on the orientation
	 * of the X/Y axes, whether they should be inverted or not on
	 * the host when parsing incoming events.
	 * Bit 2: dvsInvertXY
	 * Bit 1: reserved
	 * Bit 0: reserved
	 * This is reserved for internal use and should not be used by
	 * anything other than libcaer. Please see the 'struct caer_davis_info'
	 * documentation to get proper size information that already
	 * considers the rotation and orientation settings.
	 */
	static constexpr uint16_t DVS_ORIENTATION_INFO{2};
	/**
	 * Parameter address for module DAVIS_CONFIG_DVS:
	 * run the DVS state machine and get polarity events from the chip by
	 * handshaking with its AER bus.
	 */
	static constexpr uint16_t DVS_RUN{3};
	/**
	 * Parameter address for module DAVIS_CONFIG_DVS:
	 * if the output FIFO for this module is full, stall the AER handshake with
	 * the chip and wait until it's free again, instead of just continuing
	 * the handshake and dropping the resulting events.
	 */
	static constexpr uint16_t DVS_WAIT_ON_TRANSFER_STALL{4};
	/**
	 * Parameter address for module DAVIS_CONFIG_DVS:
	 * enable external AER control. This ensures the chip and the DVS pixel
	 * array are running, but doesn't do the handshake and leaves the ACK
	 * pin in high-impedance, to allow for an external system to take
	 * over the AER communication with the chip.
	 * DAVIS_CONFIG_DVS_RUN has to be turned off for this to work.
	 */
	static constexpr uint16_t DVS_EXTERNAL_AER_CONTROL{5};
	/**
	 * Parameter address for module DAVIS_CONFIG_DVS:
	 * read-only parameter, information about the presence of the
	 * pixel filter feature.
	 * This is reserved for internal use and should not be used by
	 * anything other than libcaer. Please see the 'struct caer_davis_info'
	 * documentation to get this information.
	 */
	static constexpr uint16_t DVS_HAS_PIXEL_FILTER{10};
	/**
	 * Parameter address for module DAVIS_CONFIG_DVS:
	 * the pixel filter completely suppresses up to eight pixels in the
	 * DVS array, filtering out all events produced by them.
	 * This is the pixel 0, Y axis setting.
	 */
	static constexpr uint16_t DVS_FILTER_PIXEL_0_ROW{11};
	/**
	 * Parameter address for module DAVIS_CONFIG_DVS:
	 * the pixel filter completely suppresses up to eight pixels in the
	 * DVS array, filtering out all events produced by them.
	 * This is the pixel 0, X axis setting.
	 */
	static constexpr uint16_t DVS_FILTER_PIXEL_0_COLUMN{12};
	/**
	 * Parameter address for module DAVIS_CONFIG_DVS:
	 * the pixel filter completely suppresses up to eight pixels in the
	 * DVS array, filtering out all events produced by them.
	 * This is the pixel 1, Y axis setting.
	 */
	static constexpr uint16_t DVS_FILTER_PIXEL_1_ROW{13};
	/**
	 * Parameter address for module DAVIS_CONFIG_DVS:
	 * the pixel filter completely suppresses up to eight pixels in the
	 * DVS array, filtering out all events produced by them.
	 * This is the pixel 1, X axis setting.
	 */
	static constexpr uint16_t DVS_FILTER_PIXEL_1_COLUMN{14};
	/**
	 * Parameter address for module DAVIS_CONFIG_DVS:
	 * the pixel filter completely suppresses up to eight pixels in the
	 * DVS array, filtering out all events produced by them.
	 * This is the pixel 2, Y axis setting.
	 */
	static constexpr uint16_t DVS_FILTER_PIXEL_2_ROW{15};
	/**
	 * Parameter address for module DAVIS_CONFIG_DVS:
	 * the pixel filter completely suppresses up to eight pixels in the
	 * DVS array, filtering out all events produced by them.
	 * This is the pixel 2, X axis setting.
	 */
	static constexpr uint16_t DVS_FILTER_PIXEL_2_COLUMN{16};
	/**
	 * Parameter address for module DAVIS_CONFIG_DVS:
	 * the pixel filter completely suppresses up to eight pixels in the
	 * DVS array, filtering out all events produced by them.
	 * This is the pixel 3, Y axis setting.
	 */
	static constexpr uint16_t DVS_FILTER_PIXEL_3_ROW{17};
	/**
	 * Parameter address for module DAVIS_CONFIG_DVS:
	 * the pixel filter completely suppresses up to eight pixels in the
	 * DVS array, filtering out all events produced by them.
	 * This is the pixel 3, X axis setting.
	 */
	static constexpr uint16_t DVS_FILTER_PIXEL_3_COLUMN{18};
	/**
	 * Parameter address for module DAVIS_CONFIG_DVS:
	 * the pixel filter completely suppresses up to eight pixels in the
	 * DVS array, filtering out all events produced by them.
	 * This is the pixel 4, Y axis setting.
	 */
	static constexpr uint16_t DVS_FILTER_PIXEL_4_ROW{19};
	/**
	 * Parameter address for module DAVIS_CONFIG_DVS:
	 * the pixel filter completely suppresses up to eight pixels in the
	 * DVS array, filtering out all events produced by them.
	 * This is the pixel 4, X axis setting.
	 */
	static constexpr uint16_t DVS_FILTER_PIXEL_4_COLUMN{20};
	/**
	 * Parameter address for module DAVIS_CONFIG_DVS:
	 * the pixel filter completely suppresses up to eight pixels in the
	 * DVS array, filtering out all events produced by them.
	 * This is the pixel 5, Y axis setting.
	 */
	static constexpr uint16_t DVS_FILTER_PIXEL_5_ROW{21};
	/**
	 * Parameter address for module DAVIS_CONFIG_DVS:
	 * the pixel filter completely suppresses up to eight pixels in the
	 * DVS array, filtering out all events produced by them.
	 * This is the pixel 5, X axis setting.
	 */
	static constexpr uint16_t DVS_FILTER_PIXEL_5_COLUMN{22};
	/**
	 * Parameter address for module DAVIS_CONFIG_DVS:
	 * the pixel filter completely suppresses up to eight pixels in the
	 * DVS array, filtering out all events produced by them.
	 * This is the pixel 6, Y axis setting.
	 */
	static constexpr uint16_t DVS_FILTER_PIXEL_6_ROW{23};
	/**
	 * Parameter address for module DAVIS_CONFIG_DVS:
	 * the pixel filter completely suppresses up to eight pixels in the
	 * DVS array, filtering out all events produced by them.
	 * This is the pixel 6, X axis setting.
	 */
	static constexpr uint16_t DVS_FILTER_PIXEL_6_COLUMN{24};
	/**
	 * Parameter address for module DAVIS_CONFIG_DVS:
	 * the pixel filter completely suppresses up to eight pixels in the
	 * DVS array, filtering out all events produced by them.
	 * This is the pixel 7, Y axis setting.
	 */
	static constexpr uint16_t DVS_FILTER_PIXEL_7_ROW{25};
	/**
	 * Parameter address for module DAVIS_CONFIG_DVS:
	 * the pixel filter completely suppresses up to eight pixels in the
	 * DVS array, filtering out all events produced by them.
	 * This is the pixel 7, X axis setting.
	 */
	static constexpr uint16_t DVS_FILTER_PIXEL_7_COLUMN{26};
	/**
	 * Parameter address for module DAVIS_CONFIG_DVS:
	 * read-only parameter, information about the presence of the
	 * background-activity filter feature.
	 * This is reserved for internal use and should not be used by
	 * anything other than libcaer. Please see the 'struct caer_davis_info'
	 * documentation to get this information.
	 */
	static constexpr uint16_t DVS_HAS_BACKGROUND_ACTIVITY_FILTER{30};
	/**
	 * Parameter address for module DAVIS_CONFIG_DVS:
	 * enable the background-activity filter, which tries to remove events
	 * caused by transistor leakage, by rejecting uncorrelated events.
	 */
	static constexpr uint16_t DVS_FILTER_BACKGROUND_ACTIVITY{31};
	/**
	 * Parameter address for module DAVIS_CONFIG_DVS:
	 * specify the time difference constant for the background-activity filter.
	 * Range: 0 - 4095, in 250µs units.
	 * Events that are correlated within this time-frame are let through,
	 * while others are filtered out.
	 */
	static constexpr uint16_t DVS_FILTER_BACKGROUND_ACTIVITY_TIME{32};
	/**
	 * Parameter address for module DAVIS_CONFIG_DVS:
	 * enable the refractory period filter, which limits the firing rate
	 * of pixels. This is supported together with the background-activity
	 * filter.
	 */
	static constexpr uint16_t DVS_FILTER_REFRACTORY_PERIOD{33};
	/**
	 * Parameter address for module DAVIS_CONFIG_DVS:
	 * specify the time constant for the refractory period filter.
	 * Range: 0 - 4095, in 250µs units.
	 * Pixels will be inhibited from generating new events during this
	 * time after the last even has fired.
	 */
	static constexpr uint16_t DVS_FILTER_REFRACTORY_PERIOD_TIME{34};
	/**
	 * Parameter address for module DAVIS_CONFIG_DVS:
	 * read-only parameter, information about the presence of the
	 * ROI filter feature.
	 * This is reserved for internal use and should not be used by
	 * anything other than libcaer. Please see the 'struct caer_davis_info'
	 * documentation to get this information.
	 */
	static constexpr uint16_t DVS_HAS_ROI_FILTER{40};
	/**
	 * Parameter address for module DAVIS_CONFIG_DVS:
	 * start position on the X axis for Region of Interest.
	 * Must be between 0 and DVS_SIZE_X-1, and be smaller
	 * or equal to DAVIS_CONFIG_DVS_FILTER_ROI_END_COLUMN.
	 */
	static constexpr uint16_t DVS_FILTER_ROI_START_COLUMN{41};
	/**
	 * Parameter address for module DAVIS_CONFIG_DVS:
	 * start position on the Y axis for Region of Interest.
	 * Must be between 0 and DVS_SIZE_Y-1, and be smaller
	 * or equal to DAVIS_CONFIG_DVS_FILTER_ROI_END_ROW.
	 */
	static constexpr uint16_t DVS_FILTER_ROI_START_ROW{42};
	/**
	 * Parameter address for module DAVIS_CONFIG_DVS:
	 * end position on the X axis for Region of Interest.
	 * Must be between 0 and DVS_SIZE_X-1, and be greater
	 * or equal to DAVIS_CONFIG_DVS_FILTER_ROI_START_COLUMN.
	 */
	static constexpr uint16_t DVS_FILTER_ROI_END_COLUMN{43};
	/**
	 * Parameter address for module DAVIS_CONFIG_DVS:
	 * end position on the Y axis for Region of Interest.
	 * Must be between 0 and DVS_SIZE_Y-1, and be greater
	 * or equal to DAVIS_CONFIG_DVS_FILTER_ROI_START_ROW.
	 */
	static constexpr uint16_t DVS_FILTER_ROI_END_ROW{44};
	/**
	 * Parameter address for module DAVIS_CONFIG_DVS:
	 * read-only parameter, information about the presence of the
	 * event skip filter feature.
	 * This is reserved for internal use and should not be used by
	 * anything other than libcaer. Please see the 'struct caer_davis_info'
	 * documentation to get this information.
	 */
	static constexpr uint16_t DVS_HAS_SKIP_FILTER{50};
	/**
	 * Parameter address for module DAVIS_CONFIG_DVS:
	 * enable the event skip filter, which simply throws away one
	 * event every N events (decimation filter).
	 */
	static constexpr uint16_t DVS_FILTER_SKIP_EVENTS{51};
	/**
	 * Parameter address for module DAVIS_CONFIG_DVS:
	 * number of events to let through before skipping one.
	 * Range: 0 - 255 events.
	 */
	static constexpr uint16_t DVS_FILTER_SKIP_EVENTS_EVERY{52};
	/**
	 * Parameter address for module DAVIS_CONFIG_DVS:
	 * read-only parameter, information about the presence of the
	 * polarity suppression filter feature.
	 * This is reserved for internal use and should not be used by
	 * anything other than libcaer. Please see the 'struct caer_davis_info'
	 * documentation to get this information.
	 */
	static constexpr uint16_t DVS_HAS_POLARITY_FILTER{60};
	/**
	 * Parameter address for module DAVIS_CONFIG_DVS:
	 * flatten all polarities to OFF (0).
	 */
	static constexpr uint16_t DVS_FILTER_POLARITY_FLATTEN{61};
	/**
	 * Parameter address for module DAVIS_CONFIG_DVS:
	 * suppress one of the two ON/OFF polarities completely.
	 * Use DAVIS_CONFIG_DVS_FILTER_POLARITY_IGNORE to select which.
	 */
	static constexpr uint16_t DVS_FILTER_POLARITY_SUPPRESS{62};
	/**
	 * Parameter address for module DAVIS_CONFIG_DVS:
	 * polarity to suppress (0=OFF, 1=ON). Use
	 * DAVIS_CONFIG_DVS_FILTER_POLARITY_IGNORE to enable.
	 */
	static constexpr uint16_t DVS_FILTER_POLARITY_SUPPRESS_TYPE{63};
	/**
	 * Parameter address for module DAVIS_CONFIG_DVS:
	 * read-only parameter, information about the presence of the
	 * statistics feature.
	 * This is reserved for internal use and should not be used by
	 * anything other than libcaer. Please see the 'struct caer_davis_info'
	 * documentation to get this information.
	 */
	static constexpr uint16_t DVS_HAS_STATISTICS{80};
	/**
	 * Parameter address for module DAVIS_CONFIG_DVS:
	 * read-only parameter, representing the number of row event
	 * transactions completed on the device.
	 * This is a 64bit value, and should always be read using the
	 * function: caerDeviceConfigGet64().
	 */
	static constexpr uint16_t DVS_STATISTICS_EVENTS_ROW{81};
	/**
	 * Parameter address for module DAVIS_CONFIG_DVS:
	 * read-only parameter, representing the number of column event
	 * transactions completed on the device.
	 * This is a 64bit value, and should always be read using the
	 * function: caerDeviceConfigGet64().
	 */
	static constexpr uint16_t DVS_STATISTICS_EVENTS_COLUMN{83};
	/**
	 * Parameter address for module DAVIS_CONFIG_DVS:
	 * read-only parameter, representing the number of dropped
	 * transaction sequences on the device due to full buffers.
	 * This is a 64bit value, and should always be read using the
	 * function: caerDeviceConfigGet64().
	 */
	static constexpr uint16_t DVS_STATISTICS_EVENTS_DROPPED{85};
	/**
	 * Parameter address for module DAVIS_CONFIG_DVS:
	 * read-only parameter, representing the number of dropped
	 * events due to the pixel filter.
	 * This is a 64bit value, and should always be read using the
	 * function: caerDeviceConfigGet64().
	 */
	static constexpr uint16_t DVS_STATISTICS_FILTERED_PIXELS{87};
	/**
	 * Parameter address for module DAVIS_CONFIG_DVS:
	 * read-only parameter, representing the number of dropped
	 * events due to the background-activity filter.
	 * This is a 64bit value, and should always be read using the
	 * function: caerDeviceConfigGet64().
	 */
	static constexpr uint16_t DVS_STATISTICS_FILTERED_BACKGROUND_ACTIVITY{89};
	/**
	 * Parameter address for module DAVIS_CONFIG_DVS:
	 * read-only parameter, representing the number of dropped
	 * events due to the refractory period filter.
	 * This is a 64bit value, and should always be read using the
	 * function: caerDeviceConfigGet64().
	 */
	static constexpr uint16_t DVS_STATISTICS_FILTERED_REFRACTORY_PERIOD{91};

	/**
	 * Parameter address for module DAVIS_CONFIG_APS:
	 * read-only parameter, contains the X axis resolution of the
	 * APS frames returned by the camera.
	 * This is reserved for internal use and should not be used by
	 * anything other than libcaer. Please see the 'struct caer_davis_info'
	 * documentation to get proper size information that already
	 * considers the rotation and orientation settings.
	 */
	static constexpr uint16_t APS_SIZE_COLUMNS{0};
	/**
	 * Parameter address for module DAVIS_CONFIG_APS:
	 * read-only parameter, contains the Y axis resolution of the
	 * APS frames returned by the camera.
	 * This is reserved for internal use and should not be used by
	 * anything other than libcaer. Please see the 'struct caer_davis_info'
	 * documentation to get proper size information that already
	 * considers the rotation and orientation settings.
	 */
	static constexpr uint16_t APS_SIZE_ROWS{1};
	/**
	 * Parameter address for module DAVIS_CONFIG_APS:
	 * read-only parameter, contains information on the orientation
	 * of the X/Y axes, whether they should be inverted or not on
	 * the host when parsing incoming pixels, as well as if the X
	 * or Y axes need to be flipped when reading the pixels.
	 * Bit 2: apsInvertXY
	 * Bit 1: apsFlipX
	 * Bit 0: apsFlipY
	 * This is reserved for internal use and should not be used by
	 * anything other than libcaer. Please see the 'struct caer_davis_info'
	 * documentation to get proper size information that already
	 * considers the rotation and orientation settings.
	 */
	static constexpr uint16_t APS_ORIENTATION_INFO{2};
	/**
	 * Parameter address for module DAVIS_CONFIG_APS:
	 * read-only parameter, contains information on the type of
	 * color filter present on the device.
	 * This is reserved for internal use and should not be used by
	 * anything other than libcaer. Please see the 'struct caer_davis_info'
	 * documentation to get proper color filter information.
	 */
	static constexpr uint16_t APS_COLOR_FILTER{3};
	/**
	 * Parameter address for module DAVIS_CONFIG_APS:
	 * enable the APS module and take intensity images of the scene.
	 * While this parameter is enabled, frames will be taken continuously.
	 * To slow down the frame-rate, see DAVIS_CONFIG_APS_FRAME_DELAY.
	 * To only take snapshots, see DAVIS_CONFIG_APS_SNAPSHOT.
	 */
	static constexpr uint16_t APS_RUN{4};
	/**
	 * Parameter address for module DAVIS_CONFIG_APS:
	 * if the output FIFO for this module is full, stall the APS state machine
	 * and wait until it's free again, instead of just dropping the pixels as
	 * they are being read out. This guarantees a complete frame readout, at the
	 * possible cost of slight timing differences between pixels. If disabled,
	 * incomplete frames may be transmitted and will then be dropped on the host,
	 * resulting in lower frame-rates, especially during high DVS traffic.
	 */
	static constexpr uint16_t APS_WAIT_ON_TRANSFER_STALL{5};
	/**
	 * Parameter address for module DAVIS_CONFIG_APS:
	 * read-only parameter, information about the presence of the
	 * global shutter feature.
	 * This is reserved for internal use and should not be used by
	 * anything other than libcaer. Please see the 'struct caer_davis_info'
	 * documentation to get this information.
	 */
	static constexpr uint16_t APS_HAS_GLOBAL_SHUTTER{6};
	/**
	 * Parameter address for module DAVIS_CONFIG_APS:
	 * enable Global Shutter mode instead of Rolling Shutter.
	 * The Global Shutter eliminates motion artifacts, but
	 * is noisier than the Rolling Shutter (worse quality).
	 */
	static constexpr uint16_t APS_GLOBAL_SHUTTER{7};
	/**
	 * Parameter address for module DAVIS_CONFIG_APS:
	 * start position on the X axis for Region of Interest 0.
	 * Must be between 0 and APS_SIZE_X-1, and be smaller
	 * or equal to DAVIS_CONFIG_APS_END_COLUMN_0.
	 */
	static constexpr uint16_t APS_START_COLUMN_0{8};
	/**
	 * Parameter address for module DAVIS_CONFIG_APS:
	 * start position on the Y axis for Region of Interest 0.
	 * Must be between 0 and APS_SIZE_Y-1, and be smaller
	 * or equal to DAVIS_CONFIG_APS_END_ROW_0.
	 */
	static constexpr uint16_t APS_START_ROW_0{9};
	/**
	 * Parameter address for module DAVIS_CONFIG_APS:
	 * end position on the X axis for Region of Interest 0.
	 * Must be between 0 and APS_SIZE_X-1, and be greater
	 * or equal to DAVIS_CONFIG_APS_START_COLUMN_0.
	 */
	static constexpr uint16_t APS_END_COLUMN_0{10};
	/**
	 * Parameter address for module DAVIS_CONFIG_APS:
	 * end position on the Y axis for Region of Interest 0.
	 * Must be between 0 and APS_SIZE_Y-1, and be greater
	 * or equal to DAVIS_CONFIG_APS_START_ROW_0.
	 */
	static constexpr uint16_t APS_END_ROW_0{11};
	/**
	 * Parameter address for module DAVIS_CONFIG_APS:
	 * frame exposure time.
	 * Range: 0-4194303, in microseconds (maximum ~4s).
	 * Very precise for Global Shutter, slightly less exact for
	 * Rolling Shutter due to column-based timing constraints.
	 */
	static constexpr uint16_t APS_EXPOSURE{12};
	/**
	 * Parameter address for module DAVIS_CONFIG_APS:
	 * time between consecutive frames.
	 * Range: 0-8388607, in microseconds (maximum ~8s).
	 * This can be used to set a frame-rate. Please note the
	 * frame-rate is best-effort, and may not be met if readout
	 * and exposure times exceed this value.
	 */
	static constexpr uint16_t APS_FRAME_INTERVAL{13};

	// Extra timing settings for CDAVIS APS module.
	/**
	 * Parameter address for module DAVIS_CONFIG_APS (only for CDAVIS chip):
	 * charge transfer time in ADCClock cycles.
	 */
	static constexpr uint16_t APS_TRANSFER{14};
	/**
	 * Parameter address for module DAVIS_CONFIG_APS (only for CDAVIS chip):
	 * Rolling Shutter FD settle time in ADCClock cycles.
	 */
	static constexpr uint16_t APS_RSFDSETTLE{15};
	/**
	 * Parameter address for module DAVIS_CONFIG_APS (only for CDAVIS chip):
	 * Global Shutter PD reset time in ADCClock cycles.
	 */
	static constexpr uint16_t APS_GSPDRESET{16};
	/**
	 * Parameter address for module DAVIS_CONFIG_APS (only for CDAVIS chip):
	 * Global Shutter Reset Fall time in ADCClock cycles.
	 */
	static constexpr uint16_t APS_GSRESETFALL{17};
	/**
	 * Parameter address for module DAVIS_CONFIG_APS (only for CDAVIS chip):
	 * Global Shutter Transfer Fall time in ADCClock cycles.
	 */
	static constexpr uint16_t APS_GSTXFALL{18};
	/**
	 * Parameter address for module DAVIS_CONFIG_APS (only for CDAVIS chip):
	 * Global Shutter FD reset time in ADCClock cycles.
	 */
	static constexpr uint16_t APS_GSFDRESET{19};

	/**
	 * Parameter address for module DAVIS_CONFIG_IMU:
	 * read-only parameter, contains information on the type of IMU
	 * chip being used in this device:
	 * 0 - no IMU present
	 * 1 - InvenSense MPU 6050/6150
	 * 2 - InvenSense MPU 9250
	 * This is reserved for internal use and should not be used by
	 * anything other than libcaer.
	 */
	static constexpr uint16_t IMU_TYPE{0};
	/**
	 * Parameter address for module DAVIS_CONFIG_IMU:
	 * read-only parameter, contains information on the orientation
	 * of the X/Y/Z axes, whether they should be flipped or not on
	 * the host when parsing incoming IMU data samples.
	 * Bit 2: imuFlipX
	 * Bit 1: imuFlipY
	 * Bit 0: imuFlipZ
	 * This is reserved for internal use and should not be used by
	 * anything other than libcaer. Generated IMU events are already
	 * properly flipped when returned to the user.
	 */
	static constexpr uint16_t IMU_ORIENTATION_INFO{1};
	/**
	 * Parameter address for module DAVIS_CONFIG_IMU:
	 * enable the IMU's accelerometer. This takes the
	 * IMU chip out of sleep.
	 */
	static constexpr uint16_t IMU_RUN_ACCELEROMETER{2};
	/**
	 * Parameter address for module DAVIS_CONFIG_IMU:
	 * enable the IMU's gyroscope. This takes the
	 * IMU chip out of sleep.
	 */
	static constexpr uint16_t IMU_RUN_GYROSCOPE{3};
	/**
	 * Parameter address for module DAVIS_CONFIG_IMU:
	 * enable the IMU's temperature sensor. This takes the
	 * IMU chip out of sleep.
	 */
	static constexpr uint16_t IMU_RUN_TEMPERATURE{4};
	/**
	 * Parameter address for module DAVIS_CONFIG_IMU:
	 * this specifies the divider from the Gyroscope Output Rate used
	 * to generate the Sample Rate for the IMU.
	 * Valid values are from 0 to 255.
	 * The Sample Rate is generated like this:
	 * Sample Rate = Gyroscope Output Rate / (1 + DAVIS_CONFIG_IMU_SAMPLE_RATE_DIVIDER)
	 * where Gyroscope Output Rate = 8 kHz when DAVIS_CONFIG_IMU_DIGITAL_LOW_PASS_FILTER
	 * is disabled (set to 0 or 7), and 1 kHz when enabled.
	 * Note: the accelerometer output rate is 1 kHz. This means that for a Sample Rate
	 * greater than 1 kHz, the same accelerometer sample may be output multiple times.
	 */
	static constexpr uint16_t IMU_SAMPLE_RATE_DIVIDER{5};
	/**
	 * Parameter address for module DAVIS_CONFIG_IMU:
	 * this configures the digital low-pass filter for both the
	 * accelerometer and the gyroscope on InvenSense MPU 6050/6150
	 * IMU devices, or for the accelerometer only on InvenSense MPU 9250.
	 * Valid values are from 0 to 7 and have the following meaning:
	 *
	 * On InvenSense MPU 6050/6150:
	 * 0 - Accel: BW=260Hz, Delay=0ms,    FS=1kHz - Gyro: BW=256Hz, Delay=0.98ms, FS=8kHz
	 * 1 - Accel: BW=184Hz, Delay=2.0ms,  FS=1kHz - Gyro: BW=188Hz, Delay=1.9ms,  FS=1kHz
	 * 2 - Accel: BW=94Hz,  Delay=3.0ms,  FS=1kHz - Gyro: BW=98Hz,  Delay=2.8ms,  FS=1kHz
	 * 3 - Accel: BW=44Hz,  Delay=4.9ms,  FS=1kHz - Gyro: BW=42Hz,  Delay=4.8ms,  FS=1kHz
	 * 4 - Accel: BW=21Hz,  Delay=8.5ms,  FS=1kHz - Gyro: BW=20Hz,  Delay=8.3ms,  FS=1kHz
	 * 5 - Accel: BW=10Hz,  Delay=13.8ms, FS=1kHz - Gyro: BW=10Hz,  Delay=13.4ms, FS=1kHz
	 * 6 - Accel: BW=5Hz,   Delay=19.0ms, FS=1kHz - Gyro: BW=5Hz,   Delay=18.6ms, FS=1kHz
	 * 7 - Accel: RESERVED,               FS=1kHz - Gyro: RESERVED,               FS=8kHz
	 *
	 * On InvenSense MPU 9250:
	 * 0 - Accel: BW=218.1Hz, Delay=1.88ms,  FS=1kHz
	 * 1 - Accel: BW=218.1Hz, Delay=1.88ms,  FS=1kHz
	 * 2 - Accel: BW=99Hz,    Delay=2.88ms,  FS=1kHz
	 * 3 - Accel: BW=44.8Hz,  Delay=4.88ms,  FS=1kHz
	 * 4 - Accel: BW=21.2Hz,  Delay=8.87ms,  FS=1kHz
	 * 5 - Accel: BW=10.2Hz,  Delay=16.83ms, FS=1kHz
	 * 6 - Accel: BW=5.05Hz,  Delay=32.48ms, FS=1kHz
	 * 7 - Accel: BW=420Hz,   Delay=1.38ms,  FS=1kHz
	 */
	static constexpr uint16_t IMU_ACCEL_DLPF{6};

	/**
	 * Parameter address for module DAVIS_CONFIG_IMU:
	 * select the full scale range of the accelerometer outputs.
	 * Valid values are:
	 * 0 - +- 2 g
	 * 1 - +- 4 g
	 * 2 - +- 8 g
	 * 3 - +- 16 g
	 */
	static constexpr uint16_t IMU_ACCEL_FULL_SCALE{7};
	/**
	 * Parameter address for module DAVIS_CONFIG_IMU:
	 * this configures the digital low-pass filter for the gyroscope
	 * on devices using the InvenSense MPU 9250.
	 * Valid values are from 0 to 7 and have the following meaning:
	 *
	 * 0 - Gyro: BW=250Hz,  Delay=0.97ms,  FS=8kHz
	 * 1 - Gyro: BW=184Hz,  Delay=2.9ms,   FS=1kHz
	 * 2 - Gyro: BW=92Hz,   Delay=3.9ms,   FS=1kHz
	 * 3 - Gyro: BW=41Hz,   Delay=5.9ms,   FS=1kHz
	 * 4 - Gyro: BW=20Hz,   Delay=9.9ms,   FS=1kHz
	 * 5 - Gyro: BW=10Hz,   Delay=17.85ms, FS=1kHz
	 * 6 - Gyro: BW=5Hz,    Delay=33.48ms, FS=1kHz
	 * 7 - Gyro: BW=3600Hz, Delay=0.17ms,  FS=8kHz
	 */
	static constexpr uint16_t IMU_GYRO_DLPF{9};
	/**
	 * Parameter address for module DAVIS_CONFIG_IMU:
	 * select the full scale range of the gyroscope outputs.
	 * Valid values are:
	 * 0 - +- 250 °/s
	 * 1 - +- 500 °/s
	 * 2 - +- 1000 °/s
	 * 3 - +- 2000 °/s
	 */
	static constexpr uint16_t IMU_GYRO_FULL_SCALE{10};

	/**
	 * Parameter address for module DAVIS_CONFIG_EXTINPUT:
	 * enable the signal detector module. It generates events
	 * when it sees certain types of signals, such as edges or
	 * pulses of a defined length, on the IN JACK signal.
	 * This can be useful to inject events into the event
	 * stream in response to external stimuli or controls,
	 * such as turning on a LED lamp.
	 */
	static constexpr uint16_t EXTINPUT_RUN_DETECTOR{0};
	/**
	 * Parameter address for module DAVIS_CONFIG_EXTINPUT:
	 * send a special EXTERNAL_INPUT_RISING_EDGE event when a
	 * rising edge is detected (transition from low voltage to high).
	 */
	static constexpr uint16_t EXTINPUT_DETECT_RISING_EDGES{1};
	/**
	 * Parameter address for module DAVIS_CONFIG_EXTINPUT:
	 * send a special EXTERNAL_INPUT_FALLING_EDGE event when a
	 * falling edge is detected (transition from high voltage to low).
	 */
	static constexpr uint16_t EXTINPUT_DETECT_FALLING_EDGES{2};
	/**
	 * Parameter address for module DAVIS_CONFIG_EXTINPUT:
	 * send a special EXTERNAL_INPUT_PULSE event when a pulse, of
	 * a specified, configurable polarity and length, is detected.
	 * See DAVIS_CONFIG_EXTINPUT_DETECT_PULSE_POLARITY and
	 * DAVIS_CONFIG_EXTINPUT_DETECT_PULSE_LENGTH for more details.
	 */
	static constexpr uint16_t EXTINPUT_DETECT_PULSES{3};
	/**
	 * Parameter address for module DAVIS_CONFIG_EXTINPUT:
	 * the polarity the pulse must exhibit to be detected as such.
	 * '1' means active high; a pulse will start when the signal
	 * goes from low to high and will continue to be seen as the
	 * same pulse as long as it stays high.
	 * '0' means active low; a pulse will start when the signal
	 * goes from high to low and will continue to be seen as the
	 * same pulse as long as it stays low.
	 */
	static constexpr uint16_t EXTINPUT_DETECT_PULSE_POLARITY{4};
	/**
	 * Parameter address for module DAVIS_CONFIG_EXTINPUT:
	 * the minimal length that a pulse must have to trigger the
	 * sending of a special event.
	 * Range: 1-1048575, in microseconds.
	 */
	static constexpr uint16_t EXTINPUT_DETECT_PULSE_LENGTH{5};
	/**
	 * Parameter address for module DAVIS_CONFIG_EXTINPUT:
	 * read-only parameter, information about the presence of the
	 * signal generator feature.
	 * This is reserved for internal use and should not be used by
	 * anything other than libcaer. Please see the 'struct caer_davis_info'
	 * documentation to get this information.
	 */
	static constexpr uint16_t EXTINPUT_HAS_GENERATOR{10};
	/**
	 * Parameter address for module DAVIS_CONFIG_EXTINPUT:
	 * enable the signal generator module. It generates a
	 * PWM-like signal based on configurable parameters and
	 * outputs it on the OUT JACK signal.
	 */
	static constexpr uint16_t EXTINPUT_RUN_GENERATOR{11};
	/**
	 * Parameter address for module DAVIS_CONFIG_EXTINPUT:
	 * polarity of the PWM-like signal to be generated.
	 * '1' means active high, '0' means active low.
	 */
	static constexpr uint16_t EXTINPUT_GENERATE_PULSE_POLARITY{12};
	/**
	 * Parameter address for module DAVIS_CONFIG_EXTINPUT:
	 * the interval between the start of two consecutive pulses.
	 * Range: 1-1048575, in microseconds.
	 * This must be bigger or equal to DAVIS_CONFIG_EXTINPUT_GENERATE_PULSE_LENGTH.
	 * To generate a signal with 50% duty cycle, this would
	 * have to be exactly double of DAVIS_CONFIG_EXTINPUT_GENERATE_PULSE_LENGTH.
	 */
	static constexpr uint16_t EXTINPUT_GENERATE_PULSE_INTERVAL{13};
	/**
	 * Parameter address for module DAVIS_CONFIG_EXTINPUT:
	 * the length a pulse stays active.
	 * Range: 1-1048575, in microseconds.
	 * This must be smaller or equal to DAVIS_CONFIG_EXTINPUT_GENERATE_PULSE_INTERVAL.
	 * To generate a signal with 50% duty cycle, this would
	 * have to be exactly half of DAVIS_CONFIG_EXTINPUT_GENERATE_PULSE_INTERVAL.
	 */
	static constexpr uint16_t EXTINPUT_GENERATE_PULSE_LENGTH{14};
	/**
	 * Parameter address for module DAVIS_CONFIG_EXTINPUT:
	 * enables event injection when a rising edge occurs in the
	 * generated signal; a special event EXTERNAL_GENERATOR_RISING_EDGE
	 * is emitted into the event stream.
	 */
	static constexpr uint16_t EXTINPUT_GENERATE_INJECT_ON_RISING_EDGE{15};
	/**
	 * Parameter address for module DAVIS_CONFIG_EXTINPUT:
	 * enables event injection when a falling edge occurs in the
	 * generated signal; a special event EXTERNAL_GENERATOR_FALLING_EDGE
	 * is emitted into the event stream.
	 */
	static constexpr uint16_t EXTINPUT_GENERATE_INJECT_ON_FALLING_EDGE{16};

	/**
	 * Parameter address for module DAVIS_CONFIG_SYSINFO:
	 * read-only parameter, the version of the logic currently
	 * running on the device's FPGA/CPLD.
	 * This is reserved for internal use and should not be used by
	 * anything other than libcaer. Please see the 'struct caer_davis_info'
	 * documentation to get this information.
	 */
	static constexpr uint16_t SYSINFO_LOGIC_VERSION{0};
	/**
	 * Parameter address for module DAVIS_CONFIG_SYSINFO:
	 * read-only parameter, an integer used to identify the different
	 * types of sensor chips used on the device.
	 * This is reserved for internal use and should not be used by
	 * anything other than libcaer. Please see the 'struct caer_davis_info'
	 * documentation to get this information.
	 */
	static constexpr uint16_t SYSINFO_CHIP_IDENTIFIER{1};
	/**
	 * Parameter address for module DAVIS_CONFIG_SYSINFO:
	 * read-only parameter, whether the device is currently a timestamp
	 * master or slave when synchronizing multiple devices together.
	 * This is reserved for internal use and should not be used by
	 * anything other than libcaer. Please see the 'struct caer_davis_info'
	 * documentation to get this information.
	 */
	static constexpr uint16_t SYSINFO_DEVICE_IS_MASTER{2};
	/**
	 * Parameter address for module DAVIS_CONFIG_SYSINFO:
	 * read-only parameter, the frequency in MHz at which the main
	 * FPGA/CPLD logic is running.
	 * This is reserved for internal use and should not be used by
	 * anything other than libcaer. Please see the 'struct caer_davis_info'
	 * documentation to get this information.
	 */
	static constexpr uint16_t SYSINFO_LOGIC_CLOCK{3};
	/**
	 * Parameter address for module DAVIS_CONFIG_SYSINFO:
	 * read-only parameter, the frequency in MHz at which the FPGA/CPLD
	 * logic related to APS frame grabbing is running.
	 * This is reserved for internal use and should not be used by
	 * anything other than libcaer. Please see the 'struct caer_davis_info'
	 * documentation to get this information.
	 */
	static constexpr uint16_t SYSINFO_ADC_CLOCK{4};
	/**
	 * Parameter address for module DAVIS_CONFIG_SYSINFO:
	 * read-only parameter, the frequency in MHz at which the FPGA/CPLD
	 * logic related to USB data transmission is running.
	 * This is reserved for internal use and should not be used by
	 * anything other than libcaer.
	 */
	static constexpr uint16_t SYSINFO_USB_CLOCK{5};
	/**
	 * Parameter address for module DAVIS_CONFIG_SYSINFO:
	 * read-only parameter, the deviation factor for the clocks.
	 * Due to how FX3 generates the clocks, which are then used by
	 * FPGA/CPLD, they are not integers but have a fractional part.
	 * This is reserved for internal use and should not be used by
	 * anything other than libcaer.
	 */
	static constexpr uint16_t SYSINFO_CLOCK_DEVIATION{6};
	/**
	 * Parameter address for module DAVIS_CONFIG_SYSINFO:
	 * read-only parameter, the patch version of the logic currently
	 * running on the device's FPGA/CPLD.
	 * This is reserved for internal use and should not be used by
	 * anything other than libcaer.
	 */
	static constexpr uint16_t SYSINFO_LOGIC_PATCH{7};

	/**
	 * Parameter address for module DAVIS_CONFIG_USB:
	 * enable the USB FIFO module, which transfers the data from the
	 * FPGA/CPLD to the USB chip, to be then sent to the host.
	 * Turning this off will suppress any USB data communication!
	 */
	static constexpr uint16_t USB_RUN{0};
	/**
	 * Parameter address for module DAVIS_CONFIG_USB:
	 * the time delay after which a packet of data is committed to
	 * USB, even if it is not full yet (short USB packet).
	 * The value is in 125µs time-slices, corresponding to how
	 * USB schedules its operations (a value of 4 for example
	 * would mean waiting at most 0.5ms until sending a short
	 * USB packet to the host).
	 */
	static constexpr uint16_t USB_EARLY_PACKET_DELAY{1};

	//@{
	/**
	 * Parameter address for module DAVIS240_CONFIG_CHIP:
	 * DAVIS240 chip configuration.
	 * These are for expert control and should never be used
	 * or changed unless for advanced debugging purposes.
	 * To change the Global Shutter configuration, please use
	 * DAVIS_CONFIG_APS_GLOBAL_SHUTTER instead.
	 * On DAVIS240B cameras, DAVIS240_CONFIG_CHIP_SPECIALPIXELCONTROL
	 * can be used to enable the test pixel array.
	 */
	static constexpr uint16_t DAVIS240_CHIP_DIGITALMUX0{128};
	static constexpr uint16_t DAVIS240_CHIP_DIGITALMUX1{129};
	static constexpr uint16_t DAVIS240_CHIP_DIGITALMUX2{130};
	static constexpr uint16_t DAVIS240_CHIP_DIGITALMUX3{131};
	static constexpr uint16_t DAVIS240_CHIP_ANALOGMUX0{132};
	static constexpr uint16_t DAVIS240_CHIP_ANALOGMUX1{133};
	static constexpr uint16_t DAVIS240_CHIP_ANALOGMUX2{134};
	static constexpr uint16_t DAVIS240_CHIP_BIASMUX0{135};
	static constexpr uint16_t DAVIS240_CHIP_RESETCALIBNEURON{136};
	static constexpr uint16_t DAVIS240_CHIP_TYPENCALIBNEURON{137};
	static constexpr uint16_t DAVIS240_CHIP_RESETTESTPIXEL{138};
	static constexpr uint16_t DAVIS240_CHIP_SPECIALPIXELCONTROL{139};
	static constexpr uint16_t DAVIS240_CHIP_AERNAROW{140};
	static constexpr uint16_t DAVIS240_CHIP_USEAOUT{141};
	static constexpr uint16_t DAVIS240_CHIP_GLOBAL_SHUTTER{142};
	//@}

	//@{
	/**
	 * Parameter address for module DAVIS346_CONFIG_CHIP:
	 * DAVIS346 chip configuration.
	 * These are for expert control and should never be used
	 * or changed unless for advanced debugging purposes.
	 * To change the Global Shutter configuration, please use
	 * DAVIS_CONFIG_APS_GLOBAL_SHUTTER instead.
	 */
	static constexpr uint16_t DAVIS346_CHIP_DIGITALMUX0{128};
	static constexpr uint16_t DAVIS346_CHIP_DIGITALMUX1{129};
	static constexpr uint16_t DAVIS346_CHIP_DIGITALMUX2{130};
	static constexpr uint16_t DAVIS346_CHIP_DIGITALMUX3{131};
	static constexpr uint16_t DAVIS346_CHIP_ANALOGMUX0{132};
	static constexpr uint16_t DAVIS346_CHIP_ANALOGMUX1{133};
	static constexpr uint16_t DAVIS346_CHIP_ANALOGMUX2{134};
	static constexpr uint16_t DAVIS346_CHIP_BIASMUX0{135};
	static constexpr uint16_t DAVIS346_CHIP_RESETCALIBNEURON{136};
	static constexpr uint16_t DAVIS346_CHIP_TYPENCALIBNEURON{137};
	static constexpr uint16_t DAVIS346_CHIP_RESETTESTPIXEL{138};
	static constexpr uint16_t DAVIS346_CHIP_AERNAROW{140};
	static constexpr uint16_t DAVIS346_CHIP_USEAOUT{141};
	static constexpr uint16_t DAVIS346_CHIP_GLOBAL_SHUTTER{142};
	static constexpr uint16_t DAVIS346_CHIP_SELECTGRAYCOUNTER{143};
	static constexpr uint16_t DAVIS346_CHIP_TESTADC{144};
	//@}

	//@{
	/**
	 * Parameter address for module CDAVIS_CONFIG_CHIP:
	 * CDAVIS chip configuration.
	 * These are for expert control and should never be used
	 * or changed unless for advanced debugging purposes.
	 * To change the Global Shutter configuration, please use
	 * DAVIS_CONFIG_APS_GLOBAL_SHUTTER instead.
	 */
	static constexpr uint16_t CDAVIS_CHIP_DIGITALMUX0{128};
	static constexpr uint16_t CDAVIS_CHIP_DIGITALMUX1{129};
	static constexpr uint16_t CDAVIS_CHIP_DIGITALMUX2{130};
	static constexpr uint16_t CDAVIS_CHIP_DIGITALMUX3{131};
	static constexpr uint16_t CDAVIS_CHIP_ANALOGMUX0{132};
	static constexpr uint16_t CDAVIS_CHIP_ANALOGMUX1{133};
	static constexpr uint16_t CDAVIS_CHIP_ANALOGMUX2{134};
	static constexpr uint16_t CDAVIS_CHIP_BIASMUX0{135};
	static constexpr uint16_t CDAVIS_CHIP_RESETCALIBNEURON{136};
	static constexpr uint16_t CDAVIS_CHIP_TYPENCALIBNEURON{137};
	static constexpr uint16_t CDAVIS_CHIP_RESETTESTPIXEL{138};
	static constexpr uint16_t CDAVIS_CHIP_AERNAROW{140};
	static constexpr uint16_t CDAVIS_CHIP_USEAOUT{141};
	static constexpr uint16_t CDAVIS_CHIP_SELECTGRAYCOUNTER{143};
	static constexpr uint16_t CDAVIS_CHIP_TESTADC{144};
	static constexpr uint16_t CDAVIS_CHIP_ADJUSTOVG1LO{145};
	static constexpr uint16_t CDAVIS_CHIP_ADJUSTOVG2LO{146};
	static constexpr uint16_t CDAVIS_CHIP_ADJUSTTX2OVG2HI{147};

	//@}

	/**
	 * On-chip voltage digital-to-analog converter configuration.
	 * See 'https://docs.inivation.com/hardware/hardware-advanced-usage/biasing.html' for more details.
	 */
	struct VDACBias {
		/// Voltage, between 0 and 63, as a fraction of 1/64th of VDD=3.3V.
		uint8_t voltageValue{0};
		/// Current, between 0 and 7, that drives the voltage.
		uint8_t currentValue{0};

		constexpr VDACBias() = default;

		constexpr VDACBias(const uint8_t voltage, const uint8_t current = 7) :
			voltageValue(voltage),
			currentValue(current) {
			if (voltageValue > 63) {
				throw dv::exceptions::InvalidArgument<uint8_t>("Invalid voltage value.", voltageValue);
			}

			if (currentValue > 7) {
				throw dv::exceptions::InvalidArgument<uint8_t>("Invalid current value.", currentValue);
			}
		}
	};

	/**
	 * Coarse-fine bias sex: true for 'N' type, false for 'P' type.
	 */
	enum class CoarseFineBiasSex {
		N_TYPE = true,
		P_TYPE = false,
	};

	/**
	 * Coarse-fine bias type: true for 'Normal', false for 'Cascode'.
	 */
	enum class CoarseFineBiasType {
		NORMAL  = true,
		CASCODE = false,
	};

	/**
	 * Coarse-fine bias current level: true for 'Normal, false for 'Low'.
	 */
	enum class CoarseFineBiasCurrentLevel {
		NORMAL = true,
		LOW    = false,
	};

	/**
	 * On-chip coarse-fine bias current configuration.
	 * See 'https://docs.inivation.com/hardware/hardware-advanced-usage/biasing.html' for more details.
	 */
	struct CoarseFineBias {
		/// Coarse current, from 0 to 7, creates big variations in output current.
		uint8_t coarseValue{0};
		/// Fine current, from 0 to 255, creates small variations in output current.
		uint8_t fineValue{0};
		/// Whether this bias is enabled or not.
		bool enabled{false};
		/// Bias sex: true for 'N' type, false for 'P' type.
		CoarseFineBiasSex sex{CoarseFineBiasSex::N_TYPE};
		/// Bias type: true for 'Normal', false for 'Cascode'.
		CoarseFineBiasType type{CoarseFineBiasType::NORMAL};
		/// Bias current level: true for 'Normal, false for 'Low'.
		CoarseFineBiasCurrentLevel currentLevel{CoarseFineBiasCurrentLevel::NORMAL};

		constexpr CoarseFineBias() = default;

		constexpr CoarseFineBias(const CoarseFineBiasSex sex) : sex(sex) {
		}

		constexpr CoarseFineBias(const uint8_t coarse, const uint8_t fine, const CoarseFineBiasSex sex,
			const CoarseFineBiasType type = CoarseFineBiasType::NORMAL, const bool enable = true) :
			coarseValue(coarse),
			fineValue(fine),
			enabled(enable),
			sex(sex),
			type(type) {
			if (coarseValue > 7) {
				throw dv::exceptions::InvalidArgument<uint8_t>("Invalid coarse value.", coarseValue);
			}
		}
	};

	/**
	 * Shifted-source bias operating mode.
	 */
	enum class ShiftedSourceBiasOperatingMode {
		/// Standard mode.
		SHIFTED_SOURCE = 0,
		/// High impedance (driven from outside).
		HI_Z = 1,
		/// Tied to ground (SSN) or VDD (SSP).
		TIED_TO_RAIL = 2,
	};

	/**
	 * Shifted-source bias voltage level.
	 */
	enum class ShiftedSourceBiasVoltageLevel {
		/// Standard mode (200-400mV).
		SPLIT_GATE = 0,
		/// Higher shifted-source voltage (one cascode).
		SINGLE_DIODE = 1,
		/// Even higher shifted-source voltage (two cascodes).
		DOUBLE_DIODE = 2,
	};

	/**
	 * On-chip shifted-source bias current configuration.
	 * See 'https://docs.inivation.com/hardware/hardware-advanced-usage/biasing.html' for more details.
	 */
	struct ShiftedSourceBias {
		/// Shifted-source bias level, from 0 to 63.
		uint8_t refValue{0};
		/// Shifted-source bias current for buffer amplifier, from 0 to 63.
		uint8_t regValue{0};
		/// Shifted-source operating mode (see ShiftedSourceBiasOperatingMode).
		ShiftedSourceBiasOperatingMode operatingMode{ShiftedSourceBiasOperatingMode::SHIFTED_SOURCE};
		/// Shifted-source voltage level (see ShiftedSourceBiasVoltageLevel).
		ShiftedSourceBiasVoltageLevel voltageLevel{ShiftedSourceBiasVoltageLevel::SPLIT_GATE};

		constexpr ShiftedSourceBias() = default;

		constexpr ShiftedSourceBias(const uint8_t ref, const uint8_t reg,
			const ShiftedSourceBiasOperatingMode mode = ShiftedSourceBiasOperatingMode::SHIFTED_SOURCE) :
			refValue(ref),
			regValue(reg),
			operatingMode(mode) {
			if (refValue > 63) {
				throw dv::exceptions::InvalidArgument<uint8_t>("Invalid ref value.", refValue);
			}

			if (regValue > 63) {
				throw dv::exceptions::InvalidArgument<uint8_t>("Invalid reg value.", regValue);
			}
		}
	};

	// Biases not exposed to user control.
	static constexpr uint16_t DAVIS240_BIAS_IFTHRBN{15};
	static constexpr uint16_t DAVIS240_BIAS_IFREFRBN{16};
	static constexpr uint16_t DAVIS240_BIAS_SSP{20};
	static constexpr uint16_t DAVIS240_BIAS_SSN{21};
	static constexpr uint16_t DAVIS346_BIAS_IFTHRBN{27};
	static constexpr uint16_t DAVIS346_BIAS_IFREFRBN{26};
	static constexpr uint16_t DAVIS346_BIAS_SSP{35};
	static constexpr uint16_t DAVIS346_BIAS_SSN{36};
	static constexpr uint16_t CDAVIS_BIAS_IFTHRBN{9};
	static constexpr uint16_t CDAVIS_BIAS_IFREFRBN{8};
	static constexpr uint16_t CDAVIS_BIAS_SSP{35};
	static constexpr uint16_t CDAVIS_BIAS_SSN{36};

	static constexpr uint16_t PID_DAVIS_FX2{0x841B};
	static constexpr uint8_t FX2_FIRMWARE_REQUIRED_VERSION{4};
	static constexpr uint8_t FX2_LOGIC_REQUIRED_VERSION{18};
	static constexpr uint8_t FX2_LOGIC_MINIMUM_PATCH{1};

	static constexpr uint16_t PID_DAVIS_FX3{0x841A};
	static constexpr uint8_t FX3_FIRMWARE_REQUIRED_VERSION{6};
	static constexpr uint8_t FX3_LOGIC_REQUIRED_VERSION{18};
	static constexpr uint8_t FX3_LOGIC_MINIMUM_PATCH{1};

	static constexpr size_t PIXEL_FILTER_MAX_SIZE{8};

	static constexpr uint32_t EXPOSURE_MAX{(1 << 22) - 1};
	static constexpr uint32_t FRAME_INTERVAL_MAX{(1 << 23) - 1};
	static constexpr uint32_t USB_EARLY_PACKET_DELAY_MAX{(1 << 20) - 1};
	static constexpr uint32_t EXT_INPUT_TIME_MAX{(1 << 20) - 1};

	static constexpr auto COMPATIBLE_CAMERA
		= [](const uint16_t vid, const uint16_t pid,
			  [[maybe_unused]] const USBDeviceType deviceType) -> std::optional<CameraModel> {
		if ((vid == VID_INIVATION) && ((pid == PID_DAVIS_FX2) || (pid == PID_DAVIS_FX3))
			&& (deviceType != USBDeviceType::FX3_GEN2)) {
			return CameraModel::DAVIS;
		}

		return std::nullopt;
	};

	// Set during construction.
	uint32_t mLogicVersion;
	uint32_t mLogicPatch;
	cv::Size mEventResolution;
	cv::Size mFrameResolution;
	parser::DAVIS::SensorModel mSensorModel;
	imu::ImuModel mImuModel;
	float mLogicClockActual;
	float mUSBClockActual;
	float mADCClockActual;
	std::unique_ptr<parser::DAVIS::Parser> mParser;
	std::atomic<dv::PixelArrangement> mDvsColorFilter;
	// Multi-part configuration needing locking.
	mutable std::mutex mConfigLock;
	cv::Rect mCropAreaEvents;
	cv::Rect mCropAreaFrames;
	std::array<VDACBias, 8> mVDACBiases{};
	std::array<CoarseFineBias, 35> mCoarseFineBiases{};
	ShiftedSourceBias mShiftedSourceNBias{};
	ShiftedSourceBias mShiftedSourcePBias{};
	bool mAutoExposureEnabled{false};
	AutoExposureCallback mAutoExposureCallback{};
	std::chrono::microseconds mAutoExposureLast{1};
	// Separate lock for USB callback.
	mutable std::mutex mCallbackConfigLock;
	// Atomic due to USB callback use.
	std::atomic<bool> mIsRunning{true};
	std::atomic<bool> mTimestampMaster{true};

	struct {
		/// Feature test: Multiplexer statistics support (event drops).
		bool muxHasStatistics;
		/// Feature test: DVS pixel-level filtering.
		bool dvsHasPixelFilter;
		/// Feature test: DVS Background Activity filter (and Refractory Period filter).
		bool dvsHasBackgroundActivityFilter;
		/// Feature test: DVS ROI filter.
		bool dvsHasROIFilter;
		/// Feature test: DVS event skip filter.
		bool dvsHasSkipFilter;
		/// Feature test: DVS polarity suppression filter.
		bool dvsHasPolarityFilter;
		/// Feature test: DVS statistics support.
		bool dvsHasStatistics;
		/// Feature test: APS supports Global Shutter.
		bool apsHasGlobalShutter;
		/// Feature test: External Input module supports Signal-Generation.
		bool extInputHasGenerator;
	} mInfo;

	void shutdownCallback() {
		mIsRunning = false;
	}

	void usbDataCallback(const std::span<const uint8_t> data) {
		// Return right away if not running anymore. This prevents useless work.
		if (!mIsRunning || !isUSBDataTransferRunning()) {
			return;
		}

		try {
			mParser->parseData(data, false);
		}
		catch (const dv::exceptions::RuntimeError &ex) {
			// Critical parsing failure (eg. timestamp error). Disable camera by setting mIsRunning=false.
			// Since we are inside the USB callback, we cannot stop the USB transfers from here. They'll continue
			// running but not be actually parsed since we are in an unrecoverable state only fit for destruction.
			mIsRunning = false;
			log(LogLevel::LVL_ERROR, ex.what());
		}
	}

	void dataParserCallback(parser::ParsedData data) {
		if (!data.frames.empty()) {
			// Exposure is in ticks, convert to actual microseconds.
			// Times are in µs on host, but in cycles @ ADC_CLOCK_FREQ on FPGA, so we adjust for that.
			for (auto &frame : data.frames) {
				frame.exposure
					= dv::Duration{std::llround(static_cast<float>(frame.exposure.count()) / mADCClockActual)};
			}

			std::scoped_lock lock{mCallbackConfigLock};

			if (mAutoExposureEnabled && mAutoExposureCallback) {
				const auto &newestFrame = data.frames.back();

				// Only run if the frame corresponds to the last set value.
				if (newestFrame.exposure == mAutoExposureLast) {
					auto newExposureTimeUs = mAutoExposureCallback(newestFrame);

					newExposureTimeUs = std::clamp(
						newExposureTimeUs, std::chrono::microseconds{1}, std::chrono::microseconds{EXPOSURE_MAX});

					mAutoExposureLast = newExposureTimeUs;

					// Times are in µs on host, but in cycles @ ADC_CLOCK_FREQ on FPGA, so we adjust for that.
					auto newExposureTimeInTicks = static_cast<uint32_t>(
						std::lround(static_cast<float>(newExposureTimeUs.count()) * mADCClockActual));

					newExposureTimeInTicks = std::clamp<uint32_t>(
						newExposureTimeInTicks, 1, static_cast<uint32_t>(EXPOSURE_MAX * mADCClockActual));

					// Update exposure with newly computed value.
					// Async call to not deadlock here.
					spiConfigSendAsync(MODULE_APS, APS_EXPOSURE, newExposureTimeInTicks, {});
				}
			}
		}

		mBuffers.acceptData(std::move(data));
	}

	void timeInitCallback() {
		mSyncState = SyncState::GOT_RESET;

		mBuffers.clearBuffers();

		// Update Master/Slave status on incoming TS resets.
		// Async call to not deadlock here.
		spiConfigReceiveAsync(MODULE_SYSINFO, SYSINFO_DEVICE_IS_MASTER,
			[this](const libusb_transfer_status status, const uint32_t param) {
				// If any USB error happened, discard.
				if (status != LIBUSB_TRANSFER_COMPLETED) {
					return;
				}

				mTimestampMaster = param;
			});
	}

	/**
	 * Transform VDAC bias structure into internal integer representation,
	 * suited for sending directly to the device via caerDeviceConfigSet().
	 *
	 * @param vdacBias VDAC bias structure.
	 *
	 * @return internal integer representation for device configuration.
	 */
	[[nodiscard]] static constexpr uint16_t caerBiasVDACGenerate(const VDACBias vdacBias) {
		uint16_t biasValue{0};

		// Build up bias value from all its components.
		biasValue |= vdacBias.voltageValue & 0x3F;
		biasValue |= (vdacBias.currentValue & 0x07) << 6;

		return biasValue;
	}

	/**
	 * Transform internal integer representation, as received by calls to
	 * caerDeviceConfigGet(), into a VDAC bias structure, for easier
	 * handling and understanding of the various parameters.
	 *
	 * @param vdacBias internal integer representation from device.
	 *
	 * @return VDAC bias structure.
	 */
	[[nodiscard]] static constexpr VDACBias caerBiasVDACParse(const uint16_t vdacBias) {
		VDACBias biasValue{};

		// Decompose bias integer into its parts.
		biasValue.voltageValue = vdacBias & 0x3F;
		biasValue.currentValue = (vdacBias >> 6) & 0x07;

		return biasValue;
	}

	/**
	 * Transform coarse-fine bias structure into internal integer representation,
	 * suited for sending directly to the device via caerDeviceConfigSet().
	 *
	 * @param coarseFineBias coarse-fine bias structure.
	 *
	 * @return internal integer representation for device configuration.
	 */
	[[nodiscard]] static constexpr uint16_t caerBiasCoarseFineGenerate(const CoarseFineBias coarseFineBias) {
		uint16_t biasValue{0};

		// Build up bias value from all its components.
		if (coarseFineBias.enabled) {
			biasValue |= 0x01;
		}
		if (coarseFineBias.sex == CoarseFineBiasSex::N_TYPE) {
			biasValue |= 0x02;
		}
		if (coarseFineBias.type == CoarseFineBiasType::NORMAL) {
			biasValue |= 0x04;
		}
		if (coarseFineBias.currentLevel == CoarseFineBiasCurrentLevel::NORMAL) {
			biasValue |= 0x08;
		}

		biasValue |= (coarseFineBias.fineValue & 0xFF) << 4;
		biasValue |= (coarseFineBias.coarseValue & 0x07) << 12;

		return biasValue;
	}

	/**
	 * Transform internal integer representation, as received by calls to
	 * caerDeviceConfigGet(), into a coarse-fine bias structure, for easier
	 * handling and understanding of the various parameters.
	 *
	 * @param coarseFineBias internal integer representation from device.
	 *
	 * @return coarse-fine bias structure.
	 */
	[[nodiscard]] static constexpr CoarseFineBias caerBiasCoarseFineParse(const uint16_t coarseFineBias) {
		CoarseFineBias biasValue{};

		// Decompose bias integer into its parts.
		biasValue.enabled      = coarseFineBias & 0x01;
		biasValue.sex          = dv::IntegerAsEnum<CoarseFineBiasSex>((coarseFineBias & 0x02) != 0);
		biasValue.type         = dv::IntegerAsEnum<CoarseFineBiasType>((coarseFineBias & 0x04) != 0);
		biasValue.currentLevel = dv::IntegerAsEnum<CoarseFineBiasCurrentLevel>((coarseFineBias & 0x08) != 0);
		biasValue.fineValue    = (coarseFineBias >> 4) & 0xFF;
		biasValue.coarseValue  = (coarseFineBias >> 12) & 0x07;

		return biasValue;
	}

	/**
	 * Transform shifted-source bias structure into internal integer representation,
	 * suited for sending directly to the device via caerDeviceConfigSet().
	 *
	 * @param shiftedSourceBias shifted-source bias structure.
	 *
	 * @return internal integer representation for device configuration.
	 */
	[[nodiscard]] static constexpr uint16_t caerBiasShiftedSourceGenerate(const ShiftedSourceBias shiftedSourceBias) {
		uint16_t biasValue{0};

		if (shiftedSourceBias.operatingMode == ShiftedSourceBiasOperatingMode::HI_Z) {
			biasValue |= 0x01;
		}
		else if (shiftedSourceBias.operatingMode == ShiftedSourceBiasOperatingMode::TIED_TO_RAIL) {
			biasValue |= 0x02;
		}

		if (shiftedSourceBias.voltageLevel == ShiftedSourceBiasVoltageLevel::SINGLE_DIODE) {
			biasValue |= 0x04;
		}
		else if (shiftedSourceBias.voltageLevel == ShiftedSourceBiasVoltageLevel::DOUBLE_DIODE) {
			biasValue |= 0x08;
		}

		biasValue |= (shiftedSourceBias.refValue & 0x3F) << 4;
		biasValue |= (shiftedSourceBias.regValue & 0x3F) << 10;

		return biasValue;
	}

	/**
	 * Transform internal integer representation, as received by calls to
	 * caerDeviceConfigGet(), into a shifted-source bias structure, for easier
	 * handling and understanding of the various parameters.
	 *
	 * @param shiftedSourceBias internal integer representation from device.
	 *
	 * @return shifted-source bias structure.
	 */
	[[nodiscard]] static constexpr ShiftedSourceBias caerBiasShiftedSourceParse(const uint16_t shiftedSourceBias) {
		ShiftedSourceBias biasValue{};

		// Decompose bias integer into its parts.
		if (shiftedSourceBias & 0x01) {
			biasValue.operatingMode = ShiftedSourceBiasOperatingMode::HI_Z;
		}
		else if (shiftedSourceBias & 0x02) {
			biasValue.operatingMode = ShiftedSourceBiasOperatingMode::TIED_TO_RAIL;
		}
		else {
			biasValue.operatingMode = ShiftedSourceBiasOperatingMode::SHIFTED_SOURCE;
		}

		if (shiftedSourceBias & 0x04) {
			biasValue.voltageLevel = ShiftedSourceBiasVoltageLevel::SINGLE_DIODE;
		}
		else if (shiftedSourceBias & 0x08) {
			biasValue.voltageLevel = ShiftedSourceBiasVoltageLevel::DOUBLE_DIODE;
		}
		else {
			biasValue.voltageLevel = ShiftedSourceBiasVoltageLevel::SPLIT_GATE;
		}

		biasValue.refValue = (shiftedSourceBias >> 4) & 0x3F;
		biasValue.regValue = (shiftedSourceBias >> 10) & 0x3F;

		return biasValue;
	}

	void sendVDACBias(const int address) {
		spiConfigSend(MODULE_BIAS, address, caerBiasVDACGenerate(mVDACBiases[address]));
	}

	void sendCoarseFineBias(const int address) {
		spiConfigSend(MODULE_BIAS, address, caerBiasCoarseFineGenerate(mCoarseFineBiases[address]));
	}

	void sendShiftedSourceBias(const int address) {
		if ((address == DAVIS240_BIAS_SSN) || (address == DAVIS346_BIAS_SSN)) {
			spiConfigSend(MODULE_BIAS, address, caerBiasShiftedSourceGenerate(mShiftedSourceNBias));
		}
		else {
			spiConfigSend(MODULE_BIAS, address, caerBiasShiftedSourceGenerate(mShiftedSourcePBias));
		}
	}

	static constexpr int AUTOEXPOSURE_HISTOGRAM_SIZE{256};
	static constexpr int AUTOEXPOSURE_HISTOGRAM_MSV_SIZE{5};
	static constexpr std::array<float, 2> AUTOEXPOSURE_HISTOGRAM_RANGE{0, UINT8_MAX + 1};
	static constexpr std::array<const float *, 1> AUTOEXPOSURE_HISTOGRAM_RANGES{AUTOEXPOSURE_HISTOGRAM_RANGE.data()};
	static constexpr float AUTOEXPOSURE_LOW_BOUNDARY{0.10f};
	static constexpr float AUTOEXPOSURE_HIGH_BOUNDARY{0.90f};
	static constexpr float AUTOEXPOSURE_UNDEROVER_FRAC{0.33f};
	static constexpr float AUTOEXPOSURE_UNDEROVER_CORRECTION{14000.0f};
	static constexpr float AUTOEXPOSURE_MSV_CORRECTION{100.0f};
	static constexpr int AUTOEXPOSURE_HISTOGRAM_BINS_LOW{
		static_cast<int>(AUTOEXPOSURE_LOW_BOUNDARY * static_cast<float>(AUTOEXPOSURE_HISTOGRAM_SIZE))};
	static constexpr int AUTOEXPOSURE_HISTOGRAM_BINS_HIGH{
		static_cast<int>(AUTOEXPOSURE_HIGH_BOUNDARY * static_cast<float>(AUTOEXPOSURE_HISTOGRAM_SIZE))};

	struct {
		cv::Mat pixelHistogram;
		cv::Mat msvHistogram;
	} mAutoExposure;

	dv::Duration computeAutomaticExposure(const dv::Frame &frame) {
		// We only do auto-exposure on grayscale images.
		cv::Mat grayscale;
		if (frame.image.channels() == 1) {
			grayscale = frame.image;
		}
		else if (frame.image.channels() == 3) {
			cv::cvtColor(frame.image, grayscale, cv::COLOR_BGR2GRAY);
		}
		else {
			cv::cvtColor(frame.image, grayscale, cv::COLOR_BGRA2GRAY);
		}

		// Fill histograms: 256 regions for pixel values; 5 regions for MSV.
		cv::calcHist(&grayscale, 1, nullptr, cv::Mat{}, mAutoExposure.pixelHistogram, 1, &AUTOEXPOSURE_HISTOGRAM_SIZE,
			const_cast<const float **>(AUTOEXPOSURE_HISTOGRAM_RANGES.data()), true, false);
		cv::calcHist(&grayscale, 1, nullptr, cv::Mat{}, mAutoExposure.msvHistogram, 1, &AUTOEXPOSURE_HISTOGRAM_MSV_SIZE,
			const_cast<const float **>(AUTOEXPOSURE_HISTOGRAM_RANGES.data()), true, false);

		// Calculate statistics on pixel histogram. Sum of histogram is always equal
		// to the number of pixels in the camera.
		const auto pixelsSum = static_cast<float>(grayscale.cols * grayscale.rows);

		float pixelsSumLow  = 0;
		float pixelsSumHigh = 0;

		for (int i = 0; i < AUTOEXPOSURE_HISTOGRAM_BINS_LOW; i++) {
			pixelsSumLow += mAutoExposure.pixelHistogram.at<float>(i);
		}

		for (int i = AUTOEXPOSURE_HISTOGRAM_BINS_HIGH; i < AUTOEXPOSURE_HISTOGRAM_SIZE; i++) {
			pixelsSumHigh += mAutoExposure.pixelHistogram.at<float>(i);
		}

		const auto pixelsFracLow  = pixelsSumLow / pixelsSum;
		const auto pixelsFracHigh = pixelsSumHigh / pixelsSum;

		const auto fracLowError  = pixelsFracLow - AUTOEXPOSURE_UNDEROVER_FRAC;
		const auto fracHighError = pixelsFracHigh - AUTOEXPOSURE_UNDEROVER_FRAC;

		// Exposure okay by default.
		auto newExposure = frame.exposure.count();

		if ((pixelsFracLow >= AUTOEXPOSURE_UNDEROVER_FRAC) && (pixelsFracHigh < AUTOEXPOSURE_UNDEROVER_FRAC)) {
			// Underexposed but not overexposed.
			newExposure = frame.exposure.count()
						+ std::llround(AUTOEXPOSURE_UNDEROVER_CORRECTION * std::pow(fracLowError, 1.65F));

			// Ensure increase.
			if (newExposure == frame.exposure.count()) {
				newExposure++;
			}
		}
		else if ((pixelsFracHigh >= AUTOEXPOSURE_UNDEROVER_FRAC) && (pixelsFracLow < AUTOEXPOSURE_UNDEROVER_FRAC)) {
			// Overexposed but not underexposed.
			newExposure = frame.exposure.count()
						- std::llround(AUTOEXPOSURE_UNDEROVER_CORRECTION * std::pow(fracHighError, 1.65F));

			// Ensure decrease.
			if (newExposure == frame.exposure.count()) {
				newExposure--;
			}
		}
		else {
			// Calculate mean sample value from histogram.
			float meanSampleValueNum   = 0;
			float meanSampleValueDenom = 0;

			for (int i = 0; i < AUTOEXPOSURE_HISTOGRAM_MSV_SIZE; i++) {
				meanSampleValueNum   += (static_cast<float>(i) + 1.0F) * mAutoExposure.msvHistogram.at<float>(i);
				meanSampleValueDenom += mAutoExposure.msvHistogram.at<float>(i);
			}

			// Prevent division by zero.
			if (meanSampleValueDenom < 1.0F) {
				meanSampleValueDenom = 1.0F;
			}

			const auto meanSampleValue      = meanSampleValueNum / meanSampleValueDenom;
			const auto meanSampleValueError = (AUTOEXPOSURE_HISTOGRAM_MSV_SIZE / 2.0F) - meanSampleValue;

			// If we're close to the under/over limits, we make the magnitude of changes smaller
			// to avoid back&forth oscillations.
			float divisor = 1;
			if ((std::fabs(fracLowError) < 0.1F) || (std::fabs(fracHighError) < 0.1F)) {
				divisor = 5;
			}
			if ((std::fabs(fracLowError) < 0.05F) || (std::fabs(fracHighError) < 0.05F)) {
				divisor = 10;
			}

			// If we're not too underexposed or overexposed, use MSV to optimize.
			if (meanSampleValueError > 0.1F) {
				// Underexposed.
				newExposure
					= frame.exposure.count()
					+ std::llround(AUTOEXPOSURE_MSV_CORRECTION * std::pow(meanSampleValueError, 2.0F) / divisor);

				// Ensure increase.
				if (newExposure == frame.exposure.count()) {
					newExposure++;
				}
			}
			else if (meanSampleValueError < -0.1F) {
				// Overexposed.
				newExposure
					= frame.exposure.count()
					- std::llround(AUTOEXPOSURE_MSV_CORRECTION * std::pow(meanSampleValueError, 2.0F) / divisor);

				// Ensure decrease.
				if (newExposure == frame.exposure.count()) {
					newExposure--;
				}
			}
		}

		return dv::Duration{newExposure};
	}
};
} // namespace dv::io::camera
