#include "../../include/dv-sdk/data/event.hpp"
#include "../../include/dv-sdk/data/frame.hpp"
#include "../../include/dv-sdk/data/imu.hpp"
#include "../../include/dv-sdk/data/trigger.hpp"

#include "../../src/log.hpp"
#include "common.hpp"

#include <dv-processing/io/camera/davis.hpp>

void *dvModuleGetHooks(enum dvModuleHooks hook) {
	// Support only device discovery.
	if (hook != DV_HOOK_DEVICE_DISCOVERY) {
		return nullptr;
	}

	// Get current device status.
	const auto devices = dv::io::camera::DAVIS::findDevices();

	// Add devices to config tree.
	auto devicesNode = dv::Cfg::GLOBAL.getNode("/system/_devices/");

	for (const auto &dev : devices) {
		const auto nodeName
			= fmt::format("davis_{:d}-{:d}/", static_cast<int>(dev.busNumber), static_cast<int>(dev.devAddress));

		auto devNode = devicesNode.getRelativeNode(nodeName);

		devNode.create<dv::CfgType::STRING>("OpenWithModule", "dv_davis", {1, 32},
			dv::CfgFlags::READ_ONLY | dv::CfgFlags::NO_EXPORT, "Open device with specified module.");

		devNode.create<dv::CfgType::INT>("USBBusNumber", dev.busNumber, {0, 255},
			dv::CfgFlags::READ_ONLY | dv::CfgFlags::NO_EXPORT, "USB bus number.");
		devNode.create<dv::CfgType::INT>("USBDeviceAddress", dev.devAddress, {0, 255},
			dv::CfgFlags::READ_ONLY | dv::CfgFlags::NO_EXPORT, "USB device address.");

		devNode.create<dv::CfgType::STRING>("SerialNumber", dev.serialNumber, {0, 8},
			dv::CfgFlags::READ_ONLY | dv::CfgFlags::NO_EXPORT, "USB device serial number.");
		devNode.create<dv::CfgType::INT>("FirmwareVersion", dev.firmwareVersion, {0, INT16_MAX},
			dv::CfgFlags::READ_ONLY | dv::CfgFlags::NO_EXPORT, "Version of device firmware.");
	}

	// Nothing to return, no further steps needed.
	return nullptr;
}

namespace dv_runtime {

class DAVIS final : public dv::ModuleBase {
private:
	dv::io::camera::DAVIS mDevice;
	dv::io::camera::parser::DAVIS::SensorModel mSensorModel;
	std::mutex mConfigLock{};
	bool mBAFilterPresent{false};
	bool mSkipFilterPresent{false};
	std::atomic_bool mInConfigEvents{false};
	std::atomic_bool mInConfigFrames{false};
	std::atomic_bool mSyncOffsetUpdated{false};

public:
	static void initOutputs(dv::OutputDefinitionList &out) {
		out.addEventOutput("events");
		out.addFrameOutput("frames");
		out.addIMUOutput("imu");
		out.addTriggerOutput("triggers");
	}

	static const char *initDescription() {
		return "iniVation DAVIS camera support.";
	}

	static void initConfigOptions(dv::RuntimeConfig &config) {
		config.add("serialNumber", dv::ConfigOption::stringOption("USB serial number restriction.", ""));

		config.add("syncWith",
			dv::ConfigOption::stringOption(
				"Module name(s) of all the secondary cameras to sync timestamps with (comma-separated).", ""));
		config.add("syncTime",
			dv::ConfigOption::buttonOption("Synchronize timestamps with secondary cameras.", "Synchronize timestamps"));

		configEventsCreate(config);
		configFramesCreate(config);
		configIMUCreate(config);
		configTriggersCreate(config);
		configSystemCreate(config);
	}

	DAVIS() :
		mDevice(
			config.getString("serialNumber"),
			[this](const dv::io::camera::USBDevice::LogLevel level, const std::string_view deviceName,
				const std::string_view message) {
				switch (level) {
					case dv::io::camera::USBDevice::LogLevel::LVL_ERROR:
						log.error(fmt::format("{}: {}", deviceName, message));
						break;

					case dv::io::camera::USBDevice::LogLevel::LVL_WARNING:
						log.warning(fmt::format("{}: {}", deviceName, message));
						break;

					case dv::io::camera::USBDevice::LogLevel::LVL_INFO:
						log.info(fmt::format("{}: {}", deviceName, message));
						break;

					default:
					case dv::io::camera::USBDevice::LogLevel::LVL_DEBUG:
						log.debug(fmt::format("{}: {}", deviceName, message));
						break;
				}
			},
			deviceLogLevelConvert(dv::LoggerInternal::logLevelNameToEnum(config.getString("logLevel")))) {
		// Setup outputs.
		outputs.getEventOutput("events").setup(
			mDevice.getEventResolution()->width, mDevice.getEventResolution()->height, mDevice.getCameraName());
		outputs.getFrameOutput("frames").setup(
			mDevice.getFrameResolution()->width, mDevice.getFrameResolution()->height, mDevice.getCameraName());
		outputs.getIMUOutput("imu").setup(mDevice.getCameraName());
		outputs.getTriggerOutput("triggers").setup(mDevice.getCameraName());

		mSensorModel = mDevice.getSensorModel();

		TimeSync::checkConfigFields(moduleNode, mDevice);

		auto sourceInfoNode = moduleNode.getRelativeNode("sourceInfo/");

		sourceInfoNode.create<dv::CfgType::STRING>("serialNumber", mDevice.getSerialNumber(), {0, 8},
			dv::CfgFlags::READ_ONLY | dv::CfgFlags::NO_EXPORT, "Device serial number.");
		sourceInfoNode.create<dv::CfgType::INT>("usbBusNumber", mDevice.getDeviceDescriptor().busNumber, {0, 255},
			dv::CfgFlags::READ_ONLY | dv::CfgFlags::NO_EXPORT, "Device USB bus number.");
		sourceInfoNode.create<dv::CfgType::INT>("usbDeviceAddress", mDevice.getDeviceDescriptor().devAddress, {0, 255},
			dv::CfgFlags::READ_ONLY | dv::CfgFlags::NO_EXPORT, "Device USB device address.");

		sourceInfoNode.create<dv::CfgType::INT>("firmwareVersion", mDevice.getFirmwareVersion(),
			{mDevice.getFirmwareVersion(), mDevice.getFirmwareVersion()},
			dv::CfgFlags::READ_ONLY | dv::CfgFlags::NO_EXPORT, "Device USB firmware version.");
		sourceInfoNode.create<dv::CfgType::INT>("logicVersion", static_cast<int32_t>(mDevice.getLogicVersion()),
			{static_cast<int32_t>(mDevice.getLogicVersion()), static_cast<int32_t>(mDevice.getLogicVersion())},
			dv::CfgFlags::READ_ONLY | dv::CfgFlags::NO_EXPORT, "Device FPGA logic version.");
		sourceInfoNode.create<dv::CfgType::INT>("logicPatchLevel", static_cast<int32_t>(mDevice.getLogicPatchLevel()),
			{static_cast<int32_t>(mDevice.getLogicPatchLevel()), static_cast<int32_t>(mDevice.getLogicPatchLevel())},
			dv::CfgFlags::READ_ONLY | dv::CfgFlags::NO_EXPORT, "Device FPGA logic patch level.");

		sourceInfoNode.create<dv::CfgType::STRING>("source", mDevice.getCameraName(),
			{static_cast<int32_t>(mDevice.getCameraName().length()),
				static_cast<int32_t>(mDevice.getCameraName().length())},
			dv::CfgFlags::READ_ONLY | dv::CfgFlags::NO_EXPORT, "Device source information.");

		// Default time sync state after startup.
		sourceInfoNode.create<dv::CfgType::STRING>("syncStatus", "NO_SYNC", {1, 10},
			dv::CfgFlags::READ_ONLY | dv::CfgFlags::NO_EXPORT, "Time synchronization status.");

		sourceInfoNode.create<dv::CfgType::LONG>("tsOffset", mDevice.getTimestampOffset().count(), {0, INT64_MAX},
			dv::CfgFlags::READ_ONLY | dv::CfgFlags::NO_EXPORT,
			"Time offset of data stream starting point to Unix time in µs.");

		sourceInfoNode.create<dv::CfgType::BOOL>("deviceIsMaster", mDevice.isMaster(), {},
			dv::CfgFlags::READ_ONLY | dv::CfgFlags::NO_EXPORT,
			"Timestamp synchronization support: device master status.");

		// Frame color filter information.
		sourceInfoNode.create<dv::CfgType::INT>("framesPixelArrangement",
			dv::EnumAsInteger(mDevice.getPixelArrangementFrames()), {0, 4},
			dv::CfgFlags::READ_ONLY | dv::CfgFlags::NO_EXPORT, "Frame sensor initial color-filter pattern.");

		// Add color filter information to event output too.
		outputs.getEventOutput("events").infoNode().create<dv::CfgType::INT>("eventsPixelArrangement",
			dv::EnumAsInteger(mDevice.getPixelArrangementEvents()), {0, 4},
			dv::CfgFlags::READ_ONLY | dv::CfgFlags::NO_EXPORT, "Event sensor initial color-filter pattern.");

		// Detect filters presence.
		try {
			(void) mDevice.getBackgroundActivityFilter();
			mBAFilterPresent = true;
		}
		catch (const std::exception &) {
			// Ignore.
		}

		try {
			(void) mDevice.getSkipFilter();
			mSkipFilterPresent = true;
		}
		catch (const std::exception &) {
			// Ignore.
		}

		// Create default device-dependant settings.
		configEventsCreateDynamic();
		configFramesCreateDynamic();
		configBiasCreateDynamic();

		// DVS240 supports only either Events or Frames.
		if ((mSensorModel == dv::io::camera::parser::DAVIS::SensorModel::DAVIS240C)
			&& (mDevice.getDeviceDescriptor().deviceType == dv::io::camera::USBDeviceType::FX3_RED)) {
			if (config.getBool("events/runEvents")) {
				config.setBool("frames/runFrames", false);
			}
		}

		// Initial configuration, since we don't use configUpdate() here.
		configSystem();
		moduleNode.getRelativeNode("system/").addAttributeListener(this, &configSystemTrampoline);

		configBias();
		moduleNode.getRelativeNode("bias/").addAttributeListener(this, &configBiasTrampoline);

		configEvents();
		moduleNode.getRelativeNode("events/").addAttributeListener(this, &configEventsTrampoline);

		configFrames();
		moduleNode.getRelativeNode("frames/").addAttributeListener(this, &configFramesTrampoline);

		configIMU();
		moduleNode.getRelativeNode("imu/").addAttributeListener(this, &configIMUTrampoline);

		configTriggers();
		moduleNode.getRelativeNode("triggers/").addAttributeListener(this, &configTriggersTrampoline);

		moduleNode.getRelativeNode("frames/").attributeUpdaterAdd(
			"exposureDuration", dv::CfgType::INT, &configFramesExposureUpdater, this);

		moduleNode.getRelativeNode("sourceInfo/").addAttributeListener(&mSyncOffsetUpdated, &TimeSync::offsetNotify);
	}

	~DAVIS() override {
		// Ensure Exposure value is coherent with device.
		moduleNode.getRelativeNode("frames/").attributeUpdaterRemoveAll();
		try {
			config.setInt("frames/exposureDuration", static_cast<int32_t>(mDevice.getExposureDuration().count()));
		}
		catch (const std::exception &) {
			// Catch communication failures and ignore them.
		}

		moduleNode.getRelativeNode("system/").removeAttributeListener(this, &configSystemTrampoline);

		moduleNode.getRelativeNode("bias/").removeAttributeListener(this, &configBiasTrampoline);

		moduleNode.getRelativeNode("events/").removeAttributeListener(this, &configEventsTrampoline);

		moduleNode.getRelativeNode("frames/").removeAttributeListener(this, &configFramesTrampoline);

		moduleNode.getRelativeNode("imu/").removeAttributeListener(this, &configIMUTrampoline);

		moduleNode.getRelativeNode("triggers/").removeAttributeListener(this, &configTriggersTrampoline);

		moduleNode.getRelativeNode("sourceInfo/").removeAttributeListener(&mSyncOffsetUpdated, &TimeSync::offsetNotify);

		// Data capture stopped already in respective destructors.
		// Clear sourceInfo node.
		auto sourceInfoNode = moduleNode.getRelativeNode("sourceInfo/");
		sourceInfoNode.removeAllAttributes();

		TimeSync::resetConfigFields(moduleNode);
	}

	void run() override {
		if (!mDevice.isRunning()) {
			// Device gone, stop module.
			config.setBool("running", false);
			return;
		}

		bool gotData{false};

		auto events = mDevice.getNextEventPacket();

		if (events) {
			gotData = true;

			// Send data out.
			auto eventsOutput              = outputs.getEventOutput("events").events();
			*eventsOutput.getBasePointer() = std::move(*events);
			eventsOutput.commit();
		}

		auto frame = mDevice.getNextFrame();

		if (frame) {
			gotData = true;

			// Send data out.
			auto framesOutput              = outputs.getFrameOutput("frames").frame();
			*framesOutput.getBasePointer() = std::move(*frame);
			framesOutput.commit();
		}

		auto imu = mDevice.getNextImuBatch();

		if (imu) {
			gotData = true;

			// Send data out.
			auto imuOutput                       = outputs.getIMUOutput("imu").data();
			imuOutput.getBasePointer()->elements = std::move(*imu);
			imuOutput.commit();
		}

		auto triggers = mDevice.getNextTriggerBatch();

		if (triggers) {
			gotData = true;

			// Send data out.
			auto triggersOutput                       = outputs.getTriggerOutput("triggers").data();
			triggersOutput.getBasePointer()->elements = std::move(*triggers);
			triggersOutput.commit();
		}

		// No data at all ready during this pass, sleep a bit to reduce load.
		if (!gotData) {
			std::this_thread::sleep_for(std::chrono::microseconds{500});
		}
	}

	void configUpdate() override {
		std::scoped_lock lock(mConfigLock);

		mDevice.setLogLevel(
			deviceLogLevelConvert(dv::LoggerInternal::logLevelNameToEnum(config.getString("logLevel"))));

		// Last update position.
		TimeSync::execute(moduleNode, mDevice, mSyncOffsetUpdated, log);
	}

private:
	static void configEventsCreate(dv::RuntimeConfig &config) {
		config.add("events/runEvents", dv::ConfigOption::boolOption("Enable events output.", true));
		config.add("events/externalAERControl",
			dv::ConfigOption::boolOption("Enable external AER (for DAVIS with exposed AER connector).", false));

		config.add("events/flatten",
			dv::ConfigOption::listOption("Flatten events control.", 0, {"none", "all ON", "ON only", "OFF only"}));

		config.add("events/flipHorizontal",
			dv::ConfigOption::boolOption("Flip event coordinates along horizontal X axis.", false));
		config.add("events/flipVertical",
			dv::ConfigOption::boolOption("Flip event coordinates along vertical Y axis.", false));

		config.setPriorityOptions({"events/runEvents"});
	}

	void configEventsCreateDynamic() {
		if (mBAFilterPresent) {
			config.add("events/backgroundActivityFilter",
				dv::ConfigOption::intOption(
					"Filter background noise. Maximum time difference for events to be considered "
					"correlated and not be filtered out (in 250µs units). 0 to disable.",
					8, 0, 4095));
			config.add("events/refractoryPeriodFilter",
				dv::ConfigOption::intOption(
					"Limit pixel firing rate. Minimum time between events to not be filtered out "
					"(in 250µs units). 0 to disable.",
					1, 0, 4095));
		}

		if (mSkipFilterPresent) {
			config.add("events/skipFilter",
				dv::ConfigOption::intOption(
					"Skip one event every N. Number of events to let through before skipping one. 0 to disable.", 0, 0,
					255));
		}

		config.add("events/cropStartX", dv::ConfigOption::intOption("Horizontal (X axis) start of ROI.", 0, 0,
											mDevice.getEventResolution()->width - 1));
		config.add("events/cropStartY", dv::ConfigOption::intOption("Vertical (Y axis) start of ROI.", 0, 0,
											mDevice.getEventResolution()->height - 1));
		config.add("events/cropWidth", dv::ConfigOption::intOption("ROI width.", mDevice.getEventResolution()->width, 1,
										   mDevice.getEventResolution()->width));
		config.add("events/cropHeight", dv::ConfigOption::intOption("ROI height.", mDevice.getEventResolution()->height,
											1, mDevice.getEventResolution()->height));
	}

	static dv::io::camera::CameraInputBase::Flatten mapFlatten(const std::string &strVal) {
		if (strVal == "all ON") {
			return dv::io::camera::CameraInputBase::Flatten::FLATTEN_ON;
		}
		else if (strVal == "ON only") {
			return dv::io::camera::CameraInputBase::Flatten::ON_ONLY;
		}
		else if (strVal == "OFF only") {
			return dv::io::camera::CameraInputBase::Flatten::OFF_ONLY;
		}
		else {
			// Default: no flattening.
			return dv::io::camera::CameraInputBase::Flatten::NONE;
		}
	}

	static void configEventsTrampoline(dvConfigNode node, void *userData, enum dvConfigAttributeEvents event,
		const char *changeKey, enum dvConfigAttributeType changeType, union dvConfigAttributeValue changeValue) {
		auto *obj = static_cast<DAVIS *>(userData);

		if (obj->mInConfigEvents) {
			return;
		}

		obj->configEvents();
	}

	void configEvents() {
		std::scoped_lock lock(mConfigLock);
		mInConfigEvents = true;

		config.update();

		const auto externalAERControl = config.getBool("events/externalAERControl");
		mDevice.setExternalAERControl(externalAERControl);
		if (externalAERControl) {
			config.setBool("events/runEvents", false);
		}
		else {
			mDevice.setEventsRunning(config.getBool("events/runEvents"));
		}

		if (mBAFilterPresent) {
			mDevice.setBackgroundActivityFilter(config.getInt("events/backgroundActivityFilter"));
			mDevice.setRefractoryPeriodFilter(config.getInt("events/refractoryPeriodFilter"));
		}

		if (mSkipFilterPresent) {
			mDevice.setSkipFilter(config.getInt("events/skipFilter"));
		}

		const auto flatten = config.getString("events/flatten");
		mDevice.setFlatten(mapFlatten(flatten));

		mDevice.setFlipHorizontal(config.getBool("events/flipHorizontal"));
		mDevice.setFlipVertical(config.getBool("events/flipVertical"));

		try {
			mDevice.setCropArea(cv::Rect{config.getInt("events/cropStartX"), config.getInt("events/cropStartY"),
				config.getInt("events/cropWidth"), config.getInt("events/cropHeight")});
		}
		catch (const std::exception &) {
			log.warning("Invalid events crop area setting, verify that (cropStartX + cropWidth) is smaller than sensor "
						"width and that (cropStartY + cropHeight) is smaller than sensor height.");
		}

		mInConfigEvents = false;
	}

	static void configFramesCreate(dv::RuntimeConfig &config) {
		config.add("frames/runFrames", dv::ConfigOption::boolOption("Enable frames output.", true));

		config.add("frames/colorMode",
			dv::ConfigOption::listOption("Color mode.", 0, {"Default", "Grayscale", "Original", "Split Original"}));

		config.add("frames/autoExposure", dv::ConfigOption::boolOption("Automatic exposure control.", true));
		config.add("frames/exposureDuration",
			dv::ConfigOption::intOption("Frame exposure duration. Auto-exposure must be disabled for manual control.",
				20'000, 1, ((1 << 22) - 1)));
		config.add("frames/frameInterval",
			dv::ConfigOption::intOption(
				"The time interval between two consecutive frame captures.", 0, 0, ((1 << 23) - 1)));

		config.add(
			"frames/flipHorizontal", dv::ConfigOption::boolOption("Flip frames along horizontal X axis.", false));
		config.add("frames/flipVertical", dv::ConfigOption::boolOption("Flip frames along vertical Y axis.", false));

		config.setPriorityOptions({"frames/runFrames", "frames/colorMode", "frames/autoExposure",
			"frames/exposureDuration", "frames/frameInterval"});
	}

	void configFramesCreateDynamic() {
		config.add("frames/cropStartX", dv::ConfigOption::intOption("Horizontal (X axis) start of ROI.", 0, 0,
											mDevice.getFrameResolution()->width - 1));
		config.add("frames/cropStartY", dv::ConfigOption::intOption("Vertical (Y axis) start of ROI.", 0, 0,
											mDevice.getFrameResolution()->height - 1));
		config.add("frames/cropWidth", dv::ConfigOption::intOption("ROI width.", mDevice.getFrameResolution()->width, 1,
										   mDevice.getFrameResolution()->width));
		config.add("frames/cropHeight", dv::ConfigOption::intOption("ROI height.", mDevice.getFrameResolution()->height,
											1, mDevice.getFrameResolution()->height));
	}

	static dv::io::camera::parser::DAVIS::ColorMode mapColorMode(const std::string &strVal) {
		if (strVal == "Grayscale") {
			return dv::io::camera::parser::DAVIS::ColorMode::GRAYSCALE;
		}
		else if (strVal == "Original") {
			return dv::io::camera::parser::DAVIS::ColorMode::ORIGINAL;
		}
		else if (strVal == "Split Original") {
			return dv::io::camera::parser::DAVIS::ColorMode::ORIGINAL_SPLIT;
		}
		else {
			// Default.
			return dv::io::camera::parser::DAVIS::ColorMode::DEFAULT;
		}
	}

	static void configFramesTrampoline(dvConfigNode node, void *userData, enum dvConfigAttributeEvents event,
		const char *changeKey, enum dvConfigAttributeType changeType, union dvConfigAttributeValue changeValue) {
		auto *obj = static_cast<DAVIS *>(userData);

		if (obj->mInConfigFrames) {
			return;
		}

		obj->configFrames();
	}

	void configFrames() {
		std::scoped_lock lock(mConfigLock);
		mInConfigFrames = true;

		config.update();

		mDevice.setFramesRunning(config.getBool("frames/runFrames"));
		mDevice.setColorMode(mapColorMode(config.getString("frames/colorMode")));
		mDevice.setFrameInterval(std::chrono::microseconds{config.getInt("frames/frameInterval")});

		const auto autoExposureEnabled = config.getBool("frames/autoExposure");
		mDevice.setAutoExposure(autoExposureEnabled);
		configFramesSetupExposure(moduleNode.getRelativeNode("frames/"), autoExposureEnabled);
		if (!autoExposureEnabled) {
			// Can only set exposure if auto-exposure is disabled.
			mDevice.setExposureDuration(std::chrono::microseconds{config.getInt("frames/exposureDuration")});
		}

		mDevice.setFlipHorizontalFrames(config.getBool("frames/flipHorizontal"));
		mDevice.setFlipVerticalFrames(config.getBool("frames/flipVertical"));

		try {
			mDevice.setCropAreaFrames(cv::Rect{config.getInt("frames/cropStartX"), config.getInt("frames/cropStartY"),
				config.getInt("frames/cropWidth"), config.getInt("frames/cropHeight")});
		}
		catch (const std::exception &) {
			log.warning("Invalid frames crop area setting, verify that (cropStartX + cropWidth) is smaller than sensor "
						"width and that (cropStartY + cropHeight) is smaller than sensor height.");
		}

		mInConfigFrames = false;
	}

	static void configFramesSetupExposure(dv::Config::Node framesConfigNode, const bool autoExposureEnabled) {
		if (autoExposureEnabled) {
			framesConfigNode.createAttribute<dv::CfgType::INT>("exposureDuration",
				framesConfigNode.get<dv::CfgType::INT>("exposureDuration"),
				framesConfigNode.getAttributeRanges<dv::CfgType::INT>("exposureDuration"),
				dv::CfgFlags::READ_ONLY | dv::CfgFlags::NO_EXPORT,
				framesConfigNode.getAttributeDescription<dv::CfgType::INT>("exposureDuration"));
		}
		else {
			framesConfigNode.createAttribute<dv::CfgType::INT>("exposureDuration",
				framesConfigNode.get<dv::CfgType::INT>("exposureDuration"),
				framesConfigNode.getAttributeRanges<dv::CfgType::INT>("exposureDuration"), dv::CfgFlags::NORMAL,
				framesConfigNode.getAttributeDescription<dv::CfgType::INT>("exposureDuration"));
		}
	}

	static union dvConfigAttributeValue configFramesExposureUpdater(
		void *userData, const char *key, enum dvConfigAttributeType type) {
		auto *obj = static_cast<DAVIS *>(userData);

		union dvConfigAttributeValue currentExposureValue = {.iint = 0};

		try {
			currentExposureValue.iint = static_cast<int32_t>(obj->mDevice.getExposureDuration().count());
		}
		catch (const std::exception &) {
			// Catch communication failures and ignore them.
		}

		return currentExposureValue;
	}

	static void configIMUCreate(dv::RuntimeConfig &config) {
		config.add("imu/runAccelerometer", dv::ConfigOption::boolOption("Enable accelerometer.", true));
		config.add("imu/runGyroscope", dv::ConfigOption::boolOption("Enable gyroscope.", true));
		config.add("imu/runTemperature", dv::ConfigOption::boolOption("Enable temperature sensor.", true));

		config.add("imu/accelRange",
			dv::ConfigOption::listOption("Accelerometer range configuration.", "±4G", {"±2G", "±4G", "±8G", "±16G"}));
		config.add("imu/gyroRange", dv::ConfigOption::listOption("Gyroscope range configuration.", "±500°/s",
										{"±250°/s", "±500°/s", "±1000°/s", "±2000°/s"}));

		config.setPriorityOptions({"imu/runAccelerometer", "imu/runGyroscope"});
	}

	static dv::io::camera::imu::InvensenseAccelRange mapAccelRange(const std::string &strVal) {
		if (strVal == "±2G") {
			return dv::io::camera::imu::InvensenseAccelRange::RANGE_2G;
		}
		else if (strVal == "±4G") {
			return dv::io::camera::imu::InvensenseAccelRange::RANGE_4G;
		}
		else if (strVal == "±8G") {
			return dv::io::camera::imu::InvensenseAccelRange::RANGE_8G;
		}
		else {
			return dv::io::camera::imu::InvensenseAccelRange::RANGE_16G;
		}
	}

	static dv::io::camera::imu::InvensenseGyroRange mapGyroRange(const std::string &strVal) {
		if (strVal == "±250°/s") {
			return dv::io::camera::imu::InvensenseGyroRange::RANGE_250DPS;
		}
		else if (strVal == "±500°/s") {
			return dv::io::camera::imu::InvensenseGyroRange::RANGE_500DPS;
		}
		else if (strVal == "±1000°/s") {
			return dv::io::camera::imu::InvensenseGyroRange::RANGE_1000DPS;
		}
		else {
			return dv::io::camera::imu::InvensenseGyroRange::RANGE_2000DPS;
		}
	}

	static void configIMUTrampoline(dvConfigNode node, void *userData, enum dvConfigAttributeEvents event,
		const char *changeKey, enum dvConfigAttributeType changeType, union dvConfigAttributeValue changeValue) {
		auto *obj = static_cast<DAVIS *>(userData);

		obj->configIMU();
	}

	void configIMU() {
		std::scoped_lock lock(mConfigLock);
		config.update();

		mDevice.setImuAccelRange(mapAccelRange(config.getString("imu/accelRange")));
		mDevice.setImuGyroRange(mapGyroRange(config.getString("imu/gyroRange")));

		mDevice.setImuRunningAccelerometer(config.getBool("imu/runAccelerometer"));
		mDevice.setImuRunningGyroscope(config.getBool("imu/runGyroscope"));
		mDevice.setImuRunningTemperature(config.getBool("imu/runTemperature"));
	}

	static void configTriggersCreate(dv::RuntimeConfig &config) {
		config.add("triggers/detectorRun", dv::ConfigOption::boolOption("Enable signal detector."));
		config.add("triggers/detectorRisingEdges",
			dv::ConfigOption::boolOption("Emit trigger event if a rising edge is detected."));
		config.add("triggers/detectorFallingEdges",
			dv::ConfigOption::boolOption("Emit trigger event if a falling edge is detected."));

		config.add("triggers/generatorRun", dv::ConfigOption::boolOption("Enable signal generator (PWM-like)."));
		config.add("triggers/generatorHighTime",
			dv::ConfigOption::intOption("PWM output high time (in µs).", 500, 1, ((1 << 20) - 1)));
		config.add("triggers/generatorLowTime",
			dv::ConfigOption::intOption("PWM output low time (in µs).", 500, 1, ((1 << 20) - 1)));
		config.add("triggers/generatorInjectTriggerOnRisingEdge",
			dv::ConfigOption::boolOption("Emit a trigger event when a rising edge is generated."));
		config.add("triggers/generatorInjectTriggerOnFallingEdge",
			dv::ConfigOption::boolOption("Emit a trigger event when a falling edge is generated."));

		config.setPriorityOptions({"triggers/"});
	}

	static void configTriggersTrampoline(dvConfigNode node, void *userData, enum dvConfigAttributeEvents event,
		const char *changeKey, enum dvConfigAttributeType changeType, union dvConfigAttributeValue changeValue) {
		auto *obj = static_cast<DAVIS *>(userData);

		obj->configTriggers();
	}

	void configTriggers() {
		std::scoped_lock lock(mConfigLock);
		config.update();

		mDevice.setDetectorRunning(config.getBool("triggers/detectorRun"));
		mDevice.setDetectorRisingEdges(config.getBool("triggers/detectorRisingEdges"));
		mDevice.setDetectorFallingEdges(config.getBool("triggers/detectorFallingEdges"));

		mDevice.setGeneratorRunning(config.getBool("triggers/generatorRun"));
		mDevice.setGeneratorHighTime(std::chrono::microseconds{config.getInt("triggers/generatorHighTime")});
		mDevice.setGeneratorLowTime(std::chrono::microseconds{config.getInt("triggers/generatorLowTime")});
		mDevice.setGeneratorInjectTriggerOnRisingEdge(config.getBool("triggers/generatorInjectTriggerOnRisingEdge"));
		mDevice.setGeneratorInjectTriggerOnFallingEdge(config.getBool("triggers/generatorInjectTriggerOnFallingEdge"));
	}

	void configBiasCreateCoarseFineDAVIS240(
		const std::string_view name, const dv::io::camera::DAVIS::Davis240BiasCF bias) {
		const auto coarseFineDefault = mDevice.getDavis240BiasCoarseFine(bias);

		config.add(fmt::format("bias/{}Coarse", name),
			dv::ConfigOption::intOption(fmt::format("{} bias coarse value.", name), coarseFineDefault.first, 0, 7));
		config.add(fmt::format("bias/{}Fine", name),
			dv::ConfigOption::intOption(fmt::format("{} bias fine value.", name), coarseFineDefault.second, 0, 255));
	}

	void configBiasCreateCoarseFineDAVIS346(
		const std::string_view name, const dv::io::camera::DAVIS::Davis346BiasCF bias) {
		const auto coarseFineDefault = mDevice.getDavis346BiasCoarseFine(bias);

		config.add(fmt::format("bias/{}Coarse", name),
			dv::ConfigOption::intOption(fmt::format("{} bias coarse value.", name), coarseFineDefault.first, 0, 7));
		config.add(fmt::format("bias/{}Fine", name),
			dv::ConfigOption::intOption(fmt::format("{} bias fine value.", name), coarseFineDefault.second, 0, 255));
	}

	void configBiasCreateCoarseFineCDAVIS(const std::string_view name, const dv::io::camera::DAVIS::CDavisBiasCF bias) {
		const auto coarseFineDefault = mDevice.getCDavisBiasCoarseFine(bias);

		config.add(fmt::format("bias/{}Coarse", name),
			dv::ConfigOption::intOption(fmt::format("{} bias coarse value.", name), coarseFineDefault.first, 0, 7));
		config.add(fmt::format("bias/{}Fine", name),
			dv::ConfigOption::intOption(fmt::format("{} bias fine value.", name), coarseFineDefault.second, 0, 255));
	}

	void configBiasCreateDynamic() {
		if ((mSensorModel == dv::io::camera::parser::DAVIS::SensorModel::DAVIS240A)
			|| (mSensorModel == dv::io::camera::parser::DAVIS::SensorModel::DAVIS240B)
			|| (mSensorModel == dv::io::camera::parser::DAVIS::SensorModel::DAVIS240C)) {
			configBiasCreateCoarseFineDAVIS240("Diff", dv::io::camera::DAVIS::Davis240BiasCF::Diff);
			configBiasCreateCoarseFineDAVIS240("On", dv::io::camera::DAVIS::Davis240BiasCF::On);
			configBiasCreateCoarseFineDAVIS240("Off", dv::io::camera::DAVIS::Davis240BiasCF::Off);
			configBiasCreateCoarseFineDAVIS240("ApsCascode", dv::io::camera::DAVIS::Davis240BiasCF::ApsCascode);
			configBiasCreateCoarseFineDAVIS240("DiffCascode", dv::io::camera::DAVIS::Davis240BiasCF::DiffCascode);
			configBiasCreateCoarseFineDAVIS240(
				"ApsReadoutSourceFollower", dv::io::camera::DAVIS::Davis240BiasCF::ApsReadoutSourceFollower);
			configBiasCreateCoarseFineDAVIS240("LocalBuffer", dv::io::camera::DAVIS::Davis240BiasCF::LocalBuffer);
			configBiasCreateCoarseFineDAVIS240("PixelInverter", dv::io::camera::DAVIS::Davis240BiasCF::PixelInverter);
			configBiasCreateCoarseFineDAVIS240("Photoreceptor", dv::io::camera::DAVIS::Davis240BiasCF::Photoreceptor);
			configBiasCreateCoarseFineDAVIS240(
				"PhotoreceptorSourceFollower", dv::io::camera::DAVIS::Davis240BiasCF::PhotoreceptorSourceFollower);
			configBiasCreateCoarseFineDAVIS240("Refractory", dv::io::camera::DAVIS::Davis240BiasCF::Refractory);
			configBiasCreateCoarseFineDAVIS240("AERPullDown", dv::io::camera::DAVIS::Davis240BiasCF::AERPullDown);
			configBiasCreateCoarseFineDAVIS240("LCOLTimeout", dv::io::camera::DAVIS::Davis240BiasCF::LCOLTimeout);
			configBiasCreateCoarseFineDAVIS240("AERPullUpX", dv::io::camera::DAVIS::Davis240BiasCF::AERPullUpX);
			configBiasCreateCoarseFineDAVIS240("AERPullUpY", dv::io::camera::DAVIS::Davis240BiasCF::AERPullUpY);
			configBiasCreateCoarseFineDAVIS240("PadFollower", dv::io::camera::DAVIS::Davis240BiasCF::PadFollower);
			configBiasCreateCoarseFineDAVIS240(
				"ApsOverflowLevel", dv::io::camera::DAVIS::Davis240BiasCF::ApsOverflowLevel);
			configBiasCreateCoarseFineDAVIS240("BiasBuffer", dv::io::camera::DAVIS::Davis240BiasCF::BiasBuffer);
		}

		if ((mSensorModel == dv::io::camera::parser::DAVIS::SensorModel::DAVIS346)
			|| (mSensorModel == dv::io::camera::parser::DAVIS::SensorModel::DAVIS640)) {
			config.add("bias/ApsOverflowLevel", dv::ConfigOption::intOption("ApsOverflowLevel bias.",
													static_cast<int32_t>(mDevice.getDavis346BiasVoltage(
														dv::io::camera::DAVIS::Davis346BiasVDAC::ApsOverflowLevel)),
													0, 63));
			config.add("bias/ApsCascode", dv::ConfigOption::intOption("ApsCascode bias.",
											  static_cast<int32_t>(mDevice.getDavis346BiasVoltage(
												  dv::io::camera::DAVIS::Davis346BiasVDAC::ApsCascode)),
											  0, 63));
			config.add("bias/ADCReferenceHigh", dv::ConfigOption::intOption("ADCReferenceHigh bias.",
													static_cast<int32_t>(mDevice.getDavis346BiasVoltage(
														dv::io::camera::DAVIS::Davis346BiasVDAC::ADCReferenceHigh)),
													0, 63));
			config.add("bias/ADCReferenceLow", dv::ConfigOption::intOption("ADCReferenceLow bias.",
												   static_cast<int32_t>(mDevice.getDavis346BiasVoltage(
													   dv::io::camera::DAVIS::Davis346BiasVDAC::ADCReferenceLow)),
												   0, 63));
			config.add("bias/ADCTestVoltage", dv::ConfigOption::intOption("ADCTestVoltage bias.",
												  static_cast<int32_t>(mDevice.getDavis346BiasVoltage(
													  dv::io::camera::DAVIS::Davis346BiasVDAC::ADCTestVoltage)),
												  0, 63));

			configBiasCreateCoarseFineDAVIS346("LocalBuffer", dv::io::camera::DAVIS::Davis346BiasCF::LocalBuffer);
			configBiasCreateCoarseFineDAVIS346("PadFollower", dv::io::camera::DAVIS::Davis346BiasCF::PadFollower);
			configBiasCreateCoarseFineDAVIS346("Diff", dv::io::camera::DAVIS::Davis346BiasCF::Diff);
			configBiasCreateCoarseFineDAVIS346("On", dv::io::camera::DAVIS::Davis346BiasCF::On);
			configBiasCreateCoarseFineDAVIS346("Off", dv::io::camera::DAVIS::Davis346BiasCF::Off);
			configBiasCreateCoarseFineDAVIS346("PixelInverter", dv::io::camera::DAVIS::Davis346BiasCF::PixelInverter);
			configBiasCreateCoarseFineDAVIS346("Photoreceptor", dv::io::camera::DAVIS::Davis346BiasCF::Photoreceptor);
			configBiasCreateCoarseFineDAVIS346(
				"PhotoreceptorSourceFollower", dv::io::camera::DAVIS::Davis346BiasCF::PhotoreceptorSourceFollower);
			configBiasCreateCoarseFineDAVIS346("Refractory", dv::io::camera::DAVIS::Davis346BiasCF::Refractory);
			configBiasCreateCoarseFineDAVIS346("ReadoutBuffer", dv::io::camera::DAVIS::Davis346BiasCF::ReadoutBuffer);
			configBiasCreateCoarseFineDAVIS346(
				"ApsReadoutSourceFollower", dv::io::camera::DAVIS::Davis346BiasCF::ApsReadoutSourceFollower);
			configBiasCreateCoarseFineDAVIS346("ADCComparator", dv::io::camera::DAVIS::Davis346BiasCF::ADCComparator);
			configBiasCreateCoarseFineDAVIS346("COLSelectLow", dv::io::camera::DAVIS::Davis346BiasCF::COLSelectLow);
			configBiasCreateCoarseFineDAVIS346("DACBuffer", dv::io::camera::DAVIS::Davis346BiasCF::DACBuffer);
			configBiasCreateCoarseFineDAVIS346("LCOLTimeout", dv::io::camera::DAVIS::Davis346BiasCF::LCOLTimeout);
			configBiasCreateCoarseFineDAVIS346("AERPullDown", dv::io::camera::DAVIS::Davis346BiasCF::AERPullDown);
			configBiasCreateCoarseFineDAVIS346("AERPullUpX", dv::io::camera::DAVIS::Davis346BiasCF::AERPullUpX);
			configBiasCreateCoarseFineDAVIS346("AERPullUpY", dv::io::camera::DAVIS::Davis346BiasCF::AERPullUpY);
			configBiasCreateCoarseFineDAVIS346("BiasBuffer", dv::io::camera::DAVIS::Davis346BiasCF::BiasBuffer);
		}

		if (mSensorModel == dv::io::camera::parser::DAVIS::SensorModel::CDAVIS) {
			config.add("bias/ApsCascode", dv::ConfigOption::intOption("ApsCascode bias.",
											  static_cast<int32_t>(mDevice.getCDavisBiasVoltage(
												  dv::io::camera::DAVIS::CDavisBiasVDAC::ApsCascode)),
											  0, 63));
			config.add("bias/OVG1Low",
				dv::ConfigOption::intOption("OVG1Low bias.",
					static_cast<int32_t>(mDevice.getCDavisBiasVoltage(dv::io::camera::DAVIS::CDavisBiasVDAC::OVG1Low)),
					0, 63));
			config.add("bias/OVG2Low",
				dv::ConfigOption::intOption("OVG2Low bias.",
					static_cast<int32_t>(mDevice.getCDavisBiasVoltage(dv::io::camera::DAVIS::CDavisBiasVDAC::OVG2Low)),
					0, 63));
			config.add("bias/TX2OVG2High", dv::ConfigOption::intOption("TX2OVG2High bias.",
											   static_cast<int32_t>(mDevice.getCDavisBiasVoltage(
												   dv::io::camera::DAVIS::CDavisBiasVDAC::TX2OVG2High)),
											   0, 63));
			config.add("bias/Gnd07",
				dv::ConfigOption::intOption("Gnd07 bias.",
					static_cast<int32_t>(mDevice.getCDavisBiasVoltage(dv::io::camera::DAVIS::CDavisBiasVDAC::Gnd07)), 0,
					63));
			config.add("bias/ADCTestVoltage", dv::ConfigOption::intOption("ADCTestVoltage bias.",
												  static_cast<int32_t>(mDevice.getCDavisBiasVoltage(
													  dv::io::camera::DAVIS::CDavisBiasVDAC::ADCTestVoltage)),
												  0, 63));
			config.add("bias/ADCReferenceHigh", dv::ConfigOption::intOption("ADCReferenceHigh bias.",
													static_cast<int32_t>(mDevice.getCDavisBiasVoltage(
														dv::io::camera::DAVIS::CDavisBiasVDAC::ADCReferenceHigh)),
													0, 63));
			config.add("bias/ADCReferenceLow", dv::ConfigOption::intOption("ADCReferenceLow bias.",
												   static_cast<int32_t>(mDevice.getCDavisBiasVoltage(
													   dv::io::camera::DAVIS::CDavisBiasVDAC::ADCReferenceLow)),
												   0, 63));

			configBiasCreateCoarseFineCDAVIS("LocalBuffer", dv::io::camera::DAVIS::CDavisBiasCF::LocalBuffer);
			configBiasCreateCoarseFineCDAVIS("PadFollower", dv::io::camera::DAVIS::CDavisBiasCF::PadFollower);
			configBiasCreateCoarseFineCDAVIS("PixelInverter", dv::io::camera::DAVIS::CDavisBiasCF::PixelInverter);
			configBiasCreateCoarseFineCDAVIS("Diff", dv::io::camera::DAVIS::CDavisBiasCF::Diff);
			configBiasCreateCoarseFineCDAVIS("On", dv::io::camera::DAVIS::CDavisBiasCF::On);
			configBiasCreateCoarseFineCDAVIS("Off", dv::io::camera::DAVIS::CDavisBiasCF::Off);
			configBiasCreateCoarseFineCDAVIS("Photoreceptor", dv::io::camera::DAVIS::CDavisBiasCF::Photoreceptor);
			configBiasCreateCoarseFineCDAVIS(
				"PhotoreceptorSourceFollower", dv::io::camera::DAVIS::CDavisBiasCF::PhotoreceptorSourceFollower);
			configBiasCreateCoarseFineCDAVIS("Refractory", dv::io::camera::DAVIS::CDavisBiasCF::Refractory);
			configBiasCreateCoarseFineCDAVIS("ArrayBiasBuffer", dv::io::camera::DAVIS::CDavisBiasCF::ArrayBiasBuffer);
			configBiasCreateCoarseFineCDAVIS("ArrayLogicBuffer", dv::io::camera::DAVIS::CDavisBiasCF::ArrayLogicBuffer);
			configBiasCreateCoarseFineCDAVIS("FallTime", dv::io::camera::DAVIS::CDavisBiasCF::FallTime);
			configBiasCreateCoarseFineCDAVIS("RiseTime", dv::io::camera::DAVIS::CDavisBiasCF::RiseTime);
			configBiasCreateCoarseFineCDAVIS("ReadoutBuffer", dv::io::camera::DAVIS::CDavisBiasCF::ReadoutBuffer);
			configBiasCreateCoarseFineCDAVIS(
				"ApsReadoutSourceFollower", dv::io::camera::DAVIS::CDavisBiasCF::ApsReadoutSourceFollower);
			configBiasCreateCoarseFineCDAVIS("ADCComparator", dv::io::camera::DAVIS::CDavisBiasCF::ADCComparator);
			configBiasCreateCoarseFineCDAVIS("DACBuffer", dv::io::camera::DAVIS::CDavisBiasCF::DACBuffer);
			configBiasCreateCoarseFineCDAVIS("LCOLTimeout", dv::io::camera::DAVIS::CDavisBiasCF::LCOLTimeout);
			configBiasCreateCoarseFineCDAVIS("AERPullDown", dv::io::camera::DAVIS::CDavisBiasCF::AERPullDown);
			configBiasCreateCoarseFineCDAVIS("AERPullUpX", dv::io::camera::DAVIS::CDavisBiasCF::AERPullUpX);
			configBiasCreateCoarseFineCDAVIS("AERPullUpY", dv::io::camera::DAVIS::CDavisBiasCF::AERPullUpY);
			configBiasCreateCoarseFineCDAVIS("BiasBuffer", dv::io::camera::DAVIS::CDavisBiasCF::BiasBuffer);

			config.add("bias/AdjustOVG1Low",
				dv::ConfigOption::boolOption("AdjustOVG1Low bias setting.", mDevice.getCDavisAdjustOVG1Low()));
			config.add("bias/AdjustOVG2Low",
				dv::ConfigOption::boolOption("AdjustOVG2Low bias setting.", mDevice.getCDavisAdjustOVG2Low()));
			config.add("bias/AdjustTX2OVGHigh",
				dv::ConfigOption::boolOption("AdjustTX2OVGHigh bias setting.", mDevice.getCDavisAdjustTX2OVGHigh()));
		}

		config.setPriorityOptions({"bias/"});
	}

	static void configBiasTrampoline(dvConfigNode node, void *userData, enum dvConfigAttributeEvents event,
		const char *changeKey, enum dvConfigAttributeType changeType, union dvConfigAttributeValue changeValue) {
		auto *obj = static_cast<DAVIS *>(userData);

		obj->configBias();
	}

	void configBiasSendCoarseFineDAVIS240(
		const std::string_view name, const dv::io::camera::DAVIS::Davis240BiasCF bias) {
		mDevice.setDavis240BiasCoarseFine(bias, static_cast<uint8_t>(config.getInt(fmt::format("bias/{}Coarse", name))),
			static_cast<uint8_t>(config.getInt(fmt::format("bias/{}Fine", name))));
	}

	void configBiasSendCoarseFineDAVIS346(
		const std::string_view name, const dv::io::camera::DAVIS::Davis346BiasCF bias) {
		mDevice.setDavis346BiasCoarseFine(bias, static_cast<uint8_t>(config.getInt(fmt::format("bias/{}Coarse", name))),
			static_cast<uint8_t>(config.getInt(fmt::format("bias/{}Fine", name))));
	}

	void configBiasSendCoarseFineCDAVIS(const std::string_view name, const dv::io::camera::DAVIS::CDavisBiasCF bias) {
		mDevice.setCDavisBiasCoarseFine(bias, static_cast<uint8_t>(config.getInt(fmt::format("bias/{}Coarse", name))),
			static_cast<uint8_t>(config.getInt(fmt::format("bias/{}Fine", name))));
	}

	void configBias() {
		std::scoped_lock lock(mConfigLock);
		config.update();

		if ((mSensorModel == dv::io::camera::parser::DAVIS::SensorModel::DAVIS240A)
			|| (mSensorModel == dv::io::camera::parser::DAVIS::SensorModel::DAVIS240B)
			|| (mSensorModel == dv::io::camera::parser::DAVIS::SensorModel::DAVIS240C)) {
			configBiasSendCoarseFineDAVIS240("Diff", dv::io::camera::DAVIS::Davis240BiasCF::Diff);
			configBiasSendCoarseFineDAVIS240("On", dv::io::camera::DAVIS::Davis240BiasCF::On);
			configBiasSendCoarseFineDAVIS240("Off", dv::io::camera::DAVIS::Davis240BiasCF::Off);
			configBiasSendCoarseFineDAVIS240("ApsCascode", dv::io::camera::DAVIS::Davis240BiasCF::ApsCascode);
			configBiasSendCoarseFineDAVIS240("DiffCascode", dv::io::camera::DAVIS::Davis240BiasCF::DiffCascode);
			configBiasSendCoarseFineDAVIS240(
				"ApsReadoutSourceFollower", dv::io::camera::DAVIS::Davis240BiasCF::ApsReadoutSourceFollower);
			configBiasSendCoarseFineDAVIS240("LocalBuffer", dv::io::camera::DAVIS::Davis240BiasCF::LocalBuffer);
			configBiasSendCoarseFineDAVIS240("PixelInverter", dv::io::camera::DAVIS::Davis240BiasCF::PixelInverter);
			configBiasSendCoarseFineDAVIS240("Photoreceptor", dv::io::camera::DAVIS::Davis240BiasCF::Photoreceptor);
			configBiasSendCoarseFineDAVIS240(
				"PhotoreceptorSourceFollower", dv::io::camera::DAVIS::Davis240BiasCF::PhotoreceptorSourceFollower);
			configBiasSendCoarseFineDAVIS240("Refractory", dv::io::camera::DAVIS::Davis240BiasCF::Refractory);
			configBiasSendCoarseFineDAVIS240("AERPullDown", dv::io::camera::DAVIS::Davis240BiasCF::AERPullDown);
			configBiasSendCoarseFineDAVIS240("LCOLTimeout", dv::io::camera::DAVIS::Davis240BiasCF::LCOLTimeout);
			configBiasSendCoarseFineDAVIS240("AERPullUpX", dv::io::camera::DAVIS::Davis240BiasCF::AERPullUpX);
			configBiasSendCoarseFineDAVIS240("AERPullUpY", dv::io::camera::DAVIS::Davis240BiasCF::AERPullUpY);
			configBiasSendCoarseFineDAVIS240("PadFollower", dv::io::camera::DAVIS::Davis240BiasCF::PadFollower);
			configBiasSendCoarseFineDAVIS240(
				"ApsOverflowLevel", dv::io::camera::DAVIS::Davis240BiasCF::ApsOverflowLevel);
			configBiasSendCoarseFineDAVIS240("BiasBuffer", dv::io::camera::DAVIS::Davis240BiasCF::BiasBuffer);
		}

		if ((mSensorModel == dv::io::camera::parser::DAVIS::SensorModel::DAVIS346)
			|| (mSensorModel == dv::io::camera::parser::DAVIS::SensorModel::DAVIS640)) {
			mDevice.setDavis346BiasVoltage(
				dv::io::camera::DAVIS::Davis346BiasVDAC::ApsOverflowLevel, config.getInt("bias/ApsOverflowLevel"));
			mDevice.setDavis346BiasVoltage(
				dv::io::camera::DAVIS::Davis346BiasVDAC::ApsCascode, config.getInt("bias/ApsCascode"));
			mDevice.setDavis346BiasVoltage(
				dv::io::camera::DAVIS::Davis346BiasVDAC::ADCReferenceHigh, config.getInt("bias/ADCReferenceHigh"));
			mDevice.setDavis346BiasVoltage(
				dv::io::camera::DAVIS::Davis346BiasVDAC::ADCReferenceLow, config.getInt("bias/ADCReferenceLow"));
			mDevice.setDavis346BiasVoltage(
				dv::io::camera::DAVIS::Davis346BiasVDAC::ADCTestVoltage, config.getInt("bias/ADCTestVoltage"));

			configBiasSendCoarseFineDAVIS346("LocalBuffer", dv::io::camera::DAVIS::Davis346BiasCF::LocalBuffer);
			configBiasSendCoarseFineDAVIS346("PadFollower", dv::io::camera::DAVIS::Davis346BiasCF::PadFollower);
			configBiasSendCoarseFineDAVIS346("Diff", dv::io::camera::DAVIS::Davis346BiasCF::Diff);
			configBiasSendCoarseFineDAVIS346("On", dv::io::camera::DAVIS::Davis346BiasCF::On);
			configBiasSendCoarseFineDAVIS346("Off", dv::io::camera::DAVIS::Davis346BiasCF::Off);
			configBiasSendCoarseFineDAVIS346("PixelInverter", dv::io::camera::DAVIS::Davis346BiasCF::PixelInverter);
			configBiasSendCoarseFineDAVIS346("Photoreceptor", dv::io::camera::DAVIS::Davis346BiasCF::Photoreceptor);
			configBiasSendCoarseFineDAVIS346(
				"PhotoreceptorSourceFollower", dv::io::camera::DAVIS::Davis346BiasCF::PhotoreceptorSourceFollower);
			configBiasSendCoarseFineDAVIS346("Refractory", dv::io::camera::DAVIS::Davis346BiasCF::Refractory);
			configBiasSendCoarseFineDAVIS346("ReadoutBuffer", dv::io::camera::DAVIS::Davis346BiasCF::ReadoutBuffer);
			configBiasSendCoarseFineDAVIS346(
				"ApsReadoutSourceFollower", dv::io::camera::DAVIS::Davis346BiasCF::ApsReadoutSourceFollower);
			configBiasSendCoarseFineDAVIS346("ADCComparator", dv::io::camera::DAVIS::Davis346BiasCF::ADCComparator);
			configBiasSendCoarseFineDAVIS346("COLSelectLow", dv::io::camera::DAVIS::Davis346BiasCF::COLSelectLow);
			configBiasSendCoarseFineDAVIS346("DACBuffer", dv::io::camera::DAVIS::Davis346BiasCF::DACBuffer);
			configBiasSendCoarseFineDAVIS346("LCOLTimeout", dv::io::camera::DAVIS::Davis346BiasCF::LCOLTimeout);
			configBiasSendCoarseFineDAVIS346("AERPullDown", dv::io::camera::DAVIS::Davis346BiasCF::AERPullDown);
			configBiasSendCoarseFineDAVIS346("AERPullUpX", dv::io::camera::DAVIS::Davis346BiasCF::AERPullUpX);
			configBiasSendCoarseFineDAVIS346("AERPullUpY", dv::io::camera::DAVIS::Davis346BiasCF::AERPullUpY);
			configBiasSendCoarseFineDAVIS346("BiasBuffer", dv::io::camera::DAVIS::Davis346BiasCF::BiasBuffer);
		}

		if (mSensorModel == dv::io::camera::parser::DAVIS::SensorModel::CDAVIS) {
			mDevice.setCDavisBiasVoltage(
				dv::io::camera::DAVIS::CDavisBiasVDAC::ApsCascode, config.getInt("bias/ApsCascode"));
			mDevice.setCDavisBiasVoltage(dv::io::camera::DAVIS::CDavisBiasVDAC::OVG1Low, config.getInt("bias/OVG1Low"));
			mDevice.setCDavisBiasVoltage(dv::io::camera::DAVIS::CDavisBiasVDAC::OVG2Low, config.getInt("bias/OVG2Low"));
			mDevice.setCDavisBiasVoltage(
				dv::io::camera::DAVIS::CDavisBiasVDAC::TX2OVG2High, config.getInt("bias/TX2OVG2High"));
			mDevice.setCDavisBiasVoltage(dv::io::camera::DAVIS::CDavisBiasVDAC::Gnd07, config.getInt("bias/Gnd07"));
			mDevice.setCDavisBiasVoltage(
				dv::io::camera::DAVIS::CDavisBiasVDAC::ADCTestVoltage, config.getInt("bias/ADCTestVoltage"));
			mDevice.setCDavisBiasVoltage(
				dv::io::camera::DAVIS::CDavisBiasVDAC::ADCReferenceHigh, config.getInt("bias/ADCReferenceHigh"));
			mDevice.setCDavisBiasVoltage(
				dv::io::camera::DAVIS::CDavisBiasVDAC::ADCReferenceLow, config.getInt("bias/ADCReferenceLow"));

			configBiasSendCoarseFineCDAVIS("LocalBuffer", dv::io::camera::DAVIS::CDavisBiasCF::LocalBuffer);
			configBiasSendCoarseFineCDAVIS("PadFollower", dv::io::camera::DAVIS::CDavisBiasCF::PadFollower);
			configBiasSendCoarseFineCDAVIS("PixelInverter", dv::io::camera::DAVIS::CDavisBiasCF::PixelInverter);
			configBiasSendCoarseFineCDAVIS("Diff", dv::io::camera::DAVIS::CDavisBiasCF::Diff);
			configBiasSendCoarseFineCDAVIS("On", dv::io::camera::DAVIS::CDavisBiasCF::On);
			configBiasSendCoarseFineCDAVIS("Off", dv::io::camera::DAVIS::CDavisBiasCF::Off);
			configBiasSendCoarseFineCDAVIS("Photoreceptor", dv::io::camera::DAVIS::CDavisBiasCF::Photoreceptor);
			configBiasSendCoarseFineCDAVIS(
				"PhotoreceptorSourceFollower", dv::io::camera::DAVIS::CDavisBiasCF::PhotoreceptorSourceFollower);
			configBiasSendCoarseFineCDAVIS("Refractory", dv::io::camera::DAVIS::CDavisBiasCF::Refractory);
			configBiasSendCoarseFineCDAVIS("ArrayBiasBuffer", dv::io::camera::DAVIS::CDavisBiasCF::ArrayBiasBuffer);
			configBiasSendCoarseFineCDAVIS("ArrayLogicBuffer", dv::io::camera::DAVIS::CDavisBiasCF::ArrayLogicBuffer);
			configBiasSendCoarseFineCDAVIS("FallTime", dv::io::camera::DAVIS::CDavisBiasCF::FallTime);
			configBiasSendCoarseFineCDAVIS("RiseTime", dv::io::camera::DAVIS::CDavisBiasCF::RiseTime);
			configBiasSendCoarseFineCDAVIS("ReadoutBuffer", dv::io::camera::DAVIS::CDavisBiasCF::ReadoutBuffer);
			configBiasSendCoarseFineCDAVIS(
				"ApsReadoutSourceFollower", dv::io::camera::DAVIS::CDavisBiasCF::ApsReadoutSourceFollower);
			configBiasSendCoarseFineCDAVIS("ADCComparator", dv::io::camera::DAVIS::CDavisBiasCF::ADCComparator);
			configBiasSendCoarseFineCDAVIS("DACBuffer", dv::io::camera::DAVIS::CDavisBiasCF::DACBuffer);
			configBiasSendCoarseFineCDAVIS("LCOLTimeout", dv::io::camera::DAVIS::CDavisBiasCF::LCOLTimeout);
			configBiasSendCoarseFineCDAVIS("AERPullDown", dv::io::camera::DAVIS::CDavisBiasCF::AERPullDown);
			configBiasSendCoarseFineCDAVIS("AERPullUpX", dv::io::camera::DAVIS::CDavisBiasCF::AERPullUpX);
			configBiasSendCoarseFineCDAVIS("AERPullUpY", dv::io::camera::DAVIS::CDavisBiasCF::AERPullUpY);
			configBiasSendCoarseFineCDAVIS("BiasBuffer", dv::io::camera::DAVIS::CDavisBiasCF::BiasBuffer);

			mDevice.setCDavisAdjustOVG1Low(config.getBool("bias/AdjustOVG1Low"));
			mDevice.setCDavisAdjustOVG2Low(config.getBool("bias/AdjustOVG2Low"));
			mDevice.setCDavisAdjustTX2OVGHigh(config.getBool("bias/AdjustTX2OVGHigh"));
		}
	}

	static void configSystemCreate(dv::RuntimeConfig &config) {
		// USB buffer settings.
		config.add("system/usbTransfersNumber", dv::ConfigOption::intOption("Number of USB transfers.", 32, 2, 128));
		config.add("system/usbTransfersSize",
			dv::ConfigOption::intOption("Size in bytes of data buffers for USB transfers.", 8 * 1024, 512, 64 * 1024));

		config.add(
			"system/timeInterval", dv::ConfigOption::intOption("Time interval in µs for data parsing and forwarding.",
									   10'000, 1, 120 * 1000 * 1000));

		config.add("system/usbEarlyPacketDelay",
			dv::ConfigOption::intOption("Send early USB packets if this timeout is reached.", 1000, 125, 1'000'000));

		config.setPriorityOptions({"system/"});
	}

	static void configSystemTrampoline(dvConfigNode node, void *userData, enum dvConfigAttributeEvents event,
		const char *changeKey, enum dvConfigAttributeType changeType, union dvConfigAttributeValue changeValue) {
		auto *obj = static_cast<DAVIS *>(userData);

		obj->configSystem();
	}

	void configSystem() {
		std::scoped_lock lock(mConfigLock);
		config.update();

		mDevice.setTransfersNumber(config.getInt("system/usbTransfersNumber"));
		mDevice.setTransfersSize(config.getInt("system/usbTransfersSize"));

		mDevice.setTimeInterval(std::chrono::microseconds{config.getInt("system/timeInterval")});

		mDevice.setUSBEarlyPacketDelay(std::chrono::microseconds{config.getInt("system/usbEarlyPacketDelay")});
	}
};

} // namespace dv_runtime

registerModuleClass(dv_runtime::DAVIS)
