From 0be1491ef0972604ddcb82cb2df5c3d94d9ed3fc Mon Sep 17 00:00:00 2001 From: Shawn Hoffman Date: Tue, 15 Sep 2020 04:24:25 -0700 Subject: rename ciface::Wiimote to ciface::WiimoteController --- .../ControllerInterface/Wiimote/Wiimote.cpp | 1680 -------------------- .../ControllerInterface/Wiimote/Wiimote.h | 282 ---- .../Wiimote/WiimoteController.cpp | 1680 ++++++++++++++++++++ .../Wiimote/WiimoteController.h | 282 ++++ 4 files changed, 1962 insertions(+), 1962 deletions(-) delete mode 100644 Source/Core/InputCommon/ControllerInterface/Wiimote/Wiimote.cpp delete mode 100644 Source/Core/InputCommon/ControllerInterface/Wiimote/Wiimote.h create mode 100644 Source/Core/InputCommon/ControllerInterface/Wiimote/WiimoteController.cpp create mode 100644 Source/Core/InputCommon/ControllerInterface/Wiimote/WiimoteController.h (limited to 'Source/Core/InputCommon/ControllerInterface') diff --git a/Source/Core/InputCommon/ControllerInterface/Wiimote/Wiimote.cpp b/Source/Core/InputCommon/ControllerInterface/Wiimote/Wiimote.cpp deleted file mode 100644 index 2a11904550..0000000000 --- a/Source/Core/InputCommon/ControllerInterface/Wiimote/Wiimote.cpp +++ /dev/null @@ -1,1680 +0,0 @@ -// Copyright 2020 Dolphin Emulator Project -// Licensed under GPLv2+ -// Refer to the license.txt file included. - -#include "InputCommon/ControllerInterface/Wiimote/Wiimote.h" - -#include "Common/BitUtils.h" -#include "Common/Logging/Log.h" -#include "Common/MathUtil.h" -#include "Core/Config/SYSCONFSettings.h" -#include "Core/HW/WiimoteEmu/ExtensionPort.h" -#include "Core/HW/WiimoteEmu/WiimoteEmu.h" -#include "InputCommon/ControllerEmu/ControllerEmu.h" -#include "InputCommon/ControllerInterface/ControllerInterface.h" - -namespace ciface::Wiimote -{ -static constexpr char SOURCE_NAME[] = "Bluetooth"; - -static constexpr size_t IR_SENSITIVITY_LEVEL_COUNT = 5; - -template -class Button final : public Core::Device::Input -{ -public: - Button(const T* value, std::common_type_t mask, std::string name) - : m_value(*value), m_mask(mask), m_name(std::move(name)) - { - } - - std::string GetName() const override { return m_name; } - - ControlState GetState() const override { return (m_value & m_mask) != 0; } - -private: - const T& m_value; - const T m_mask; - const std::string m_name; -}; - -// GetState returns value divided by supplied "extent". -template -class GenericInput : public Core::Device::Input -{ -public: - GenericInput(const T* value, std::string name, ControlState extent) - : m_value(*value), m_name(std::move(name)), m_extent(extent) - { - } - - bool IsDetectable() const override { return Detectable; } - - std::string GetName() const override { return m_name; } - - ControlState GetState() const final override { return ControlState(m_value) / m_extent; } - -protected: - const T& m_value; - const std::string m_name; - const ControlState m_extent; -}; - -template -using AnalogInput = GenericInput; - -template -using UndetectableAnalogInput = GenericInput; - -// GetName() is appended with '-' or '+' based on sign of "extent" value. -template -class SignedInput final : public GenericInput -{ -public: - using GenericInput::GenericInput; - - std::string GetName() const override { return this->m_name + (this->m_extent < 0 ? '-' : '+'); } -}; - -using SignedAnalogInput = SignedInput; -using UndetectableSignedAnalogInput = SignedInput; - -class Motor final : public Core::Device::Output -{ -public: - Motor(ControlState* value) : m_value(*value) {} - - std::string GetName() const override { return "Motor"; } - - void SetState(ControlState state) override { m_value = state; } - -private: - ControlState& m_value; -}; - -template -void Device::QueueReport(T&& report, std::function ack_callback) -{ - // Maintain proper rumble state. - report.rumble = m_rumble; - - m_wiimote->QueueReport(report.REPORT_ID, &report, sizeof(report)); - - if (ack_callback) - AddReportHandler(MakeAckHandler(report.REPORT_ID, std::move(ack_callback))); -} - -void AddDevice(std::unique_ptr wiimote) -{ - // Our real wiimote class requires an index. - // Within the pool it's only going to be used for logging purposes. - static constexpr int CIFACE_WIIMOTE_INDEX = 55; - - if (!wiimote->Connect(CIFACE_WIIMOTE_INDEX)) - { - WARN_LOG_FMT(WIIMOTE, "WiiRemote: Failed to connect."); - return; - } - - wiimote->Prepare(); - wiimote->EventLinked(); - - g_controller_interface.AddDevice(std::make_shared(std::move(wiimote))); -} - -void ReleaseDevices(std::optional count) -{ - u32 removed_devices = 0; - - // Remove up to "count" remotes (or all of them if nullopt). - // Real wiimotes will be added to the pool. - g_controller_interface.RemoveDevice([&](const Core::Device* device) { - if (device->GetSource() != SOURCE_NAME || count == removed_devices) - return false; - - ++removed_devices; - return true; - }); -} - -Device::Device(std::unique_ptr wiimote) : m_wiimote(std::move(wiimote)) -{ - using EmuWiimote = WiimoteEmu::Wiimote; - - // Buttons. - static constexpr std::pair button_masks[] = { - {EmuWiimote::BUTTON_A, "A"}, {EmuWiimote::BUTTON_B, "B"}, - {EmuWiimote::BUTTON_ONE, "1"}, {EmuWiimote::BUTTON_TWO, "2"}, - {EmuWiimote::BUTTON_MINUS, "-"}, {EmuWiimote::BUTTON_PLUS, "+"}, - {EmuWiimote::BUTTON_HOME, "HOME"}, - }; - - for (auto& button : button_masks) - AddInput(new Button(&m_core_data.hex, button.first, button.second)); - - static constexpr u16 dpad_masks[] = { - EmuWiimote::PAD_UP, - EmuWiimote::PAD_DOWN, - EmuWiimote::PAD_LEFT, - EmuWiimote::PAD_RIGHT, - }; - - // Friendly orientation inputs. - static constexpr const char* const rotation_names[] = {"Pitch", "Roll", "Yaw"}; - for (std::size_t i = 0; i != std::size(rotation_names); ++i) - { - AddInput( - new UndetectableSignedAnalogInput(&m_rotation_inputs.data[i], rotation_names[i], -1.f)); - AddInput(new UndetectableSignedAnalogInput(&m_rotation_inputs.data[i], rotation_names[i], 1.f)); - } - - // Raw accelerometer. - for (std::size_t i = 0; i != std::size(dpad_masks); ++i) - AddInput(new Button(&m_core_data.hex, dpad_masks[i], named_directions[i])); - - static constexpr std::array, 3> accel_names = {{ - {"Accel Left", "Accel Right"}, - {"Accel Backward", "Accel Forward"}, - {"Accel Up", "Accel Down"}, - }}; - - for (std::size_t i = 0; i != m_accel_data.data.size(); ++i) - { - AddInput(new UndetectableAnalogInput(&m_accel_data.data[i], accel_names[i][0], 1)); - AddInput(new UndetectableAnalogInput(&m_accel_data.data[i], accel_names[i][1], -1)); - } - - // IR data. - static constexpr const char* const ir_names[] = {"IR Center X", "IR Center Y"}; - for (std::size_t i = 0; i != std::size(ir_names); ++i) - { - AddInput( - new UndetectableSignedAnalogInput(&m_ir_state.center_position.data[i], ir_names[i], -1.f)); - AddInput( - new UndetectableSignedAnalogInput(&m_ir_state.center_position.data[i], ir_names[i], 1.f)); - } - - AddInput(new UndetectableAnalogInput(&m_ir_state.is_hidden, "IR Hidden", 1)); - - AddInput(new UndetectableAnalogInput(&m_ir_state.distance, "IR Distance", 1)); - - // Raw gyroscope. - static constexpr std::array, 3> gyro_names = {{ - {"Gyro Pitch Down", "Gyro Pitch Up"}, - {"Gyro Roll Left", "Gyro Roll Right"}, - {"Gyro Yaw Left", "Gyro Yaw Right"}, - }}; - - for (std::size_t i = 0; i != m_accel_data.data.size(); ++i) - { - AddInput( - new UndetectableAnalogInput(&m_mplus_state.gyro_data.data[i], gyro_names[i][0], 1)); - AddInput( - new UndetectableAnalogInput(&m_mplus_state.gyro_data.data[i], gyro_names[i][1], -1)); - } - - using WiimoteEmu::Nunchuk; - const std::string nunchuk_prefix = "Nunchuk "; - - // Buttons. - AddInput(new Button(&m_nunchuk_state.buttons, Nunchuk::BUTTON_C, nunchuk_prefix + "C")); - AddInput(new Button(&m_nunchuk_state.buttons, Nunchuk::BUTTON_Z, nunchuk_prefix + "Z")); - - // Stick. - static constexpr const char* const nunchuk_stick_names[] = {"X", "Y"}; - for (std::size_t i = 0; i != std::size(nunchuk_stick_names); ++i) - { - AddInput(new SignedAnalogInput(&m_nunchuk_state.stick.data[i], - nunchuk_prefix + nunchuk_stick_names[i], -1.f)); - AddInput(new SignedAnalogInput(&m_nunchuk_state.stick.data[i], - nunchuk_prefix + nunchuk_stick_names[i], 1.f)); - } - - // Raw accelerometer. - for (std::size_t i = 0; i != m_accel_data.data.size(); ++i) - { - AddInput(new UndetectableAnalogInput(&m_nunchuk_state.accel.data[i], - nunchuk_prefix + accel_names[i][0], 1)); - AddInput(new UndetectableAnalogInput(&m_nunchuk_state.accel.data[i], - nunchuk_prefix + accel_names[i][1], -1)); - } - - using WiimoteEmu::Classic; - const std::string classic_prefix = "Classic "; - - // Buttons. - static constexpr u16 classic_dpad_masks[] = { - Classic::PAD_UP, - Classic::PAD_DOWN, - Classic::PAD_LEFT, - Classic::PAD_RIGHT, - }; - - for (std::size_t i = 0; i != std::size(classic_dpad_masks); ++i) - AddInput(new Button(&m_classic_state.buttons, classic_dpad_masks[i], - classic_prefix + named_directions[i])); - - static constexpr u16 classic_button_masks[] = { - Classic::BUTTON_A, Classic::BUTTON_B, Classic::BUTTON_X, Classic::BUTTON_Y, - Classic::TRIGGER_L, Classic::TRIGGER_R, Classic::BUTTON_ZL, Classic::BUTTON_ZR, - Classic::BUTTON_MINUS, Classic::BUTTON_PLUS, Classic::BUTTON_HOME, - }; - - static constexpr const char* const classic_button_names[] = { - "A", "B", "X", "Y", "L", "R", "ZL", "ZR", "-", "+", "HOME", - }; - - for (std::size_t i = 0; i != std::size(classic_button_masks); ++i) - AddInput(new Button(&m_classic_state.buttons, classic_button_masks[i], - classic_prefix + classic_button_names[i])); - - // Sticks. - static constexpr const char* const classic_stick_names[][2] = {{"Left X", "Left Y"}, - {"Right X", "Right Y"}}; - - for (std::size_t s = 0; s != std::size(m_classic_state.sticks); ++s) - { - for (std::size_t i = 0; i != std::size(m_classic_state.sticks[0].data); ++i) - { - AddInput(new SignedAnalogInput(&m_classic_state.sticks[s].data[i], - classic_prefix + classic_stick_names[s][i], -1.f)); - AddInput(new SignedAnalogInput(&m_classic_state.sticks[s].data[i], - classic_prefix + classic_stick_names[s][i], 1.f)); - } - } - - // Triggers. - AddInput(new AnalogInput(&m_classic_state.triggers[0], classic_prefix + "L-Analog", 1.f)); - AddInput(new AnalogInput(&m_classic_state.triggers[1], classic_prefix + "R-Analog", 1.f)); - - // Specialty inputs: - AddInput(new UndetectableAnalogInput(&m_battery, "Battery", 1.f)); - AddInput(new UndetectableAnalogInput( - &m_extension_number_input, "Attached Extension", WiimoteEmu::ExtensionNumber(1))); - AddInput(new UndetectableAnalogInput(&m_mplus_attached_input, "Attached MotionPlus", 1)); - - AddOutput(new Motor(&m_rumble_level)); -} - -Device::~Device() -{ - if (!m_wiimote->IsConnected()) - return; - - m_wiimote->EmuStop(); - - INFO_LOG_FMT(WIIMOTE, "WiiRemote: Returning remote to pool."); - WiimoteReal::AddWiimoteToPool(std::move(m_wiimote)); -} - -std::string Device::GetName() const -{ - return "Wii Remote"; -} - -std::string Device::GetSource() const -{ - return SOURCE_NAME; -} - -void Device::RunTasks() -{ - if (IsPerformingTask()) - return; - - // Request status. - if (Clock::now() >= m_status_outdated_time) - { - QueueReport(OutputReportRequestStatus()); - - AddReportHandler(std::function( - [this](const InputReportStatus& status) { - DEBUG_LOG_FMT(WIIMOTE, "WiiRemote: Received requested status."); - ProcessStatusReport(status); - })); - - return; - } - - // Set LEDs. - const auto desired_leds = GetDesiredLEDValue(); - if (m_leds != desired_leds) - { - OutputReportLeds rpt = {}; - rpt.ack = 1; - rpt.leds = desired_leds; - QueueReport(rpt, [this, desired_leds](ErrorCode result) { - if (result != ErrorCode::Success) - { - WARN_LOG_FMT(WIIMOTE, "WiiRemote: Failed to set LEDs."); - return; - } - - DEBUG_LOG_FMT(WIIMOTE, "WiiRemote: Set LEDs."); - - m_leds = desired_leds; - }); - - return; - } - - // Set reporting mode to one that supports every component. - static constexpr auto desired_reporting_mode = InputReportID::ReportCoreAccelIR10Ext6; - if (m_reporting_mode != desired_reporting_mode) - { - OutputReportMode mode = {}; - mode.ack = 1; - mode.mode = desired_reporting_mode; - QueueReport(mode, [this](ErrorCode error) { - if (error != ErrorCode::Success) - { - WARN_LOG_FMT(WIIMOTE, "WiiRemote: Failed to set reporting mode."); - return; - } - - m_reporting_mode = desired_reporting_mode; - - DEBUG_LOG_FMT(WIIMOTE, "WiiRemote: Set reporting mode."); - }); - - return; - } - - // Read accelerometer calibration. - if (!m_accel_calibration.has_value()) - { - static constexpr u16 ACCEL_CALIBRATION_ADDR = 0x16; - - ReadData(AddressSpace::EEPROM, 0, ACCEL_CALIBRATION_ADDR, sizeof(AccelCalibrationData), - [this](ReadResponse response) { - if (!response) - { - WARN_LOG_FMT(WIIMOTE, "WiiRemote: Failed to read accelerometer calibration."); - return; - } - - DEBUG_LOG_FMT(WIIMOTE, "WiiRemote: Read accelerometer calibration."); - - auto& calibration_data = *response; - - const AccelCalibrationData accel_calibration = - Common::BitCastPtr(calibration_data.data()); - m_accel_calibration = accel_calibration.GetCalibration(); - - WiimoteEmu::UpdateCalibrationDataChecksum(calibration_data, 1); - - // We could potentially try the second block at 0x26 if the checksum is bad. - if (accel_calibration.checksum != calibration_data.back()) - WARN_LOG_FMT(WIIMOTE, "WiiRemote: Bad accelerometer calibration checksum."); - }); - - return; - } - - if (!m_ir_state.IsFullyConfigured()) - { - ConfigureIRCamera(); - - return; - } - - if (!m_speaker_configured) - { - ConfigureSpeaker(); - - return; - } - - // Perform the following tasks only after M+ is settled. - if (IsWaitingForMotionPlus()) - return; - - // Read the "active" extension ID. (This also gives us the current M+ mode) - // This will fail on an un-intialized other extension. - // But extension initialization is the same as M+ de-activation so we must try this first. - if (m_extension_port == true && - (!IsMotionPlusStateKnown() || (!IsMotionPlusActive() && !m_extension_id.has_value()))) - { - static constexpr u16 ENCRYPTION_ADDR = 0xfb; - static constexpr u8 ENCRYPTION_VALUE = 0x00; - - // First disable encryption. Note this is a no-op when performed on the M+. - WriteData(AddressSpace::I2CBus, WiimoteEmu::ExtensionPort::REPORT_I2C_SLAVE, ENCRYPTION_ADDR, - {ENCRYPTION_VALUE}, [this](ErrorCode error) { - if (error != ErrorCode::Success) - return; - - ReadActiveExtensionID(); - }); - - return; - } - - static constexpr u16 INIT_ADDR = 0xf0; - static constexpr u8 INIT_VALUE = 0x55; - - // Initialize "active" extension if ID was not recognized. - // Note this is done before M+ setup to determine the required passthrough mode. - if (m_extension_id == ExtensionID::Unsupported) - { - // Note that this signal also DE-activates a M+. - WriteData(AddressSpace::I2CBus, WiimoteEmu::ExtensionPort::REPORT_I2C_SLAVE, INIT_ADDR, - {INIT_VALUE}, [this](ErrorCode result) { - DEBUG_LOG_FMT(WIIMOTE, "WiiRemote: Initialized extension: {}.", int(result)); - m_extension_id = std::nullopt; - }); - - return; - } - - // The following tasks require a known M+ state. - if (!IsMotionPlusStateKnown()) - return; - - // We now know the status of the M+. - // Updating it too frequently results off/on flashes on mode change. - m_mplus_attached_input = IsMotionPlusActive(); - - // Extension removal status is known here. Attachment status is updated after the ID is read. - if (m_extension_port != true) - m_extension_number_input = WiimoteEmu::ExtensionNumber::NONE; - - // Periodically try to initialize and activate an inactive M+. - if (!IsMotionPlusActive() && m_mplus_desired_mode.has_value() && - m_mplus_state.current_mode != m_mplus_desired_mode) - { - static constexpr u16 MPLUS_POLL_ADDR = WiimoteEmu::MotionPlus::PASSTHROUGH_MODE_OFFSET; - ReadData(AddressSpace::I2CBus, WiimoteEmu::MotionPlus::INACTIVE_DEVICE_ADDR, MPLUS_POLL_ADDR, 1, - [this](ReadResponse response) { - if (!response) - { - DEBUG_LOG_FMT(WIIMOTE, "WiiRemote: M+ poll failed."); - HandleMotionPlusNonResponse(); - return; - } - - WriteData(AddressSpace::I2CBus, WiimoteEmu::MotionPlus::INACTIVE_DEVICE_ADDR, - INIT_ADDR, {INIT_VALUE}, [this](ErrorCode result) { - DEBUG_LOG_FMT(WIIMOTE, "WiiRemote: M+ initialization: {}.", int(result)); - if (result != ErrorCode::Success) - { - HandleMotionPlusNonResponse(); - return; - } - - TriggerMotionPlusModeChange(); - }); - }); - - return; - } - - // Change active M+ passthrough mode. - if (IsMotionPlusActive() && m_mplus_desired_mode.has_value() && - m_mplus_state.current_mode != m_mplus_desired_mode) - { - TriggerMotionPlusModeChange(); - - return; - } - - // Read passthrough extension ID. - // This will also give us a desired M+ passthrough mode. - if (IsMotionPlusActive() && m_mplus_state.passthrough_port == true && !m_extension_id.has_value()) - { - // The M+ reads the passthrough ext ID and stores it at 0xf6,f8,f9. - static constexpr u16 PASSTHROUGH_EXT_ID_ADDR = 0xf6; - - ReadData(AddressSpace::I2CBus, WiimoteEmu::MotionPlus::ACTIVE_DEVICE_ADDR, - PASSTHROUGH_EXT_ID_ADDR, 4, [this](ReadResponse response) { - if (!response) - return; - - // Port status may have changed since the read was sent. - // In which case this data read would succeed but be useless. - if (m_mplus_state.passthrough_port != true) - return; - - auto& identifier = *response; - - ProcessExtensionID(identifier[2], identifier[0], identifier[3]); - }); - - return; - } - - // The following tasks require M+ configuration to be done. - if (!IsMotionPlusInDesiredMode()) - return; - - // Now that M+ config has settled we can update the extension number. - // Updating it too frequently results off/on flashes on M+ mode change. - UpdateExtensionNumberInput(); - - static constexpr u16 NORMAL_CALIBRATION_ADDR = 0x20; - - // Read M+ calibration. - if (IsMotionPlusActive() && !m_mplus_state.calibration.has_value()) - { - ReadData(AddressSpace::I2CBus, WiimoteEmu::MotionPlus::ACTIVE_DEVICE_ADDR, - NORMAL_CALIBRATION_ADDR, sizeof(WiimoteEmu::MotionPlus::CalibrationData), - [this](ReadResponse response) { - if (!response) - return; - - DEBUG_LOG_FMT(WIIMOTE, "WiiRemote: Read M+ calibration."); - - WiimoteEmu::MotionPlus::CalibrationData calibration = - Common::BitCastPtr(response->data()); - - const auto read_checksum = std::pair(calibration.crc32_lsb, calibration.crc32_msb); - - calibration.UpdateChecksum(); - - m_mplus_state.SetCalibrationData(calibration); - - if (read_checksum != std::pair(calibration.crc32_lsb, calibration.crc32_msb)) - { - // We could potentially try another read or call the M+ unusable. - WARN_LOG_FMT(WIIMOTE, "WiiRemote: Bad M+ calibration checksum."); - } - }); - - return; - } - - // Read normal extension calibration. - if ((m_extension_id == ExtensionID::Nunchuk && !m_nunchuk_state.calibration) || - (m_extension_id == ExtensionID::Classic && !m_classic_state.calibration)) - { - // Extension calibration is normally at 0x20 but M+ reads and stores it at 0x40. - static constexpr u16 PASSTHROUGH_CALIBRATION_ADDR = 0x40; - - const u16 calibration_addr = - IsMotionPlusActive() ? PASSTHROUGH_CALIBRATION_ADDR : NORMAL_CALIBRATION_ADDR; - static constexpr u16 CALIBRATION_SIZE = 0x10; - - ReadData( - AddressSpace::I2CBus, WiimoteEmu::ExtensionPort::REPORT_I2C_SLAVE, calibration_addr, - CALIBRATION_SIZE, [this](ReadResponse response) { - if (!response) - return; - - DEBUG_LOG_FMT(WIIMOTE, "WiiRemote: Read extension calibration."); - - auto& calibration_data = *response; - - const auto read_checksum = std::pair(calibration_data[CALIBRATION_SIZE - 2], - calibration_data[CALIBRATION_SIZE - 1]); - - WiimoteEmu::UpdateCalibrationDataChecksum(calibration_data, 2); - - Checksum checksum = Checksum::Good; - - if (read_checksum != std::pair(calibration_data[CALIBRATION_SIZE - 2], - calibration_data[CALIBRATION_SIZE - 1])) - { - // We could potentially try another block or call the extension unusable. - WARN_LOG_FMT(WIIMOTE, "WiiRemote: Bad extension calibration checksum."); - checksum = Checksum::Bad; - } - - if (m_extension_id == ExtensionID::Nunchuk) - { - m_nunchuk_state.SetCalibrationData( - Common::BitCastPtr(calibration_data.data()), - checksum); - } - else if (m_extension_id == ExtensionID::Classic) - { - m_classic_state.SetCalibrationData( - Common::BitCastPtr(calibration_data.data()), - checksum); - } - }); - - return; - } -} - -void Device::HandleMotionPlusNonResponse() -{ - // No need for additional checks if an extension is attached. - // (not possible for M+ to become attached) - if (m_extension_port == true) - m_mplus_desired_mode = MotionPlusState::PassthroughMode{}; - else - WaitForMotionPlus(); -} - -// Produce LED bitmask for remotes. -// Remotes 1-4 are normal. Additional remotes LED labels will add up to their assigned ID. -u8 Device::GetDesiredLEDValue() const -{ - const auto index = GetId(); - - // Normal LED behavior for remotes 1-4. - if (index < 4) - return 1 << index; - - // Light LED 4 and LEDs 1 through 3 for remotes 5-7. (Add up the numbers on the remote) - if (index < 7) - return 1 << (index - 4) | 8; - - // Light LED 4+3 and LEDs 1 or 2 for remotes 8 or 9. (Add up the numbers on the remote) - if (index < 9) - return 1 << (index - 7) | 8 | 4; - - // For remotes 10 and up just light all LEDs. - return 0xf; -} - -void Device::UpdateExtensionNumberInput() -{ - switch (m_extension_id.value_or(ExtensionID::Unsupported)) - { - case ExtensionID::Nunchuk: - m_extension_number_input = WiimoteEmu::ExtensionNumber::NUNCHUK; - break; - case ExtensionID::Classic: - m_extension_number_input = WiimoteEmu::ExtensionNumber::CLASSIC; - break; - case ExtensionID::Unsupported: - default: - m_extension_number_input = WiimoteEmu::ExtensionNumber::NONE; - break; - } -} - -void Device::ProcessExtensionEvent(bool connected) -{ - // Reset extension state. - m_nunchuk_state = {}; - m_classic_state = {}; - - m_extension_id = std::nullopt; - - // We won't know the desired mode until we get the extension ID. - if (connected) - m_mplus_desired_mode = std::nullopt; -} - -void Device::ProcessExtensionID(u8 id_0, u8 id_4, u8 id_5) -{ - if (id_4 == 0x00 && id_5 == 0x00) - { - INFO_LOG_FMT(WIIMOTE, "WiiRemote: Nunchuk is attached."); - m_extension_id = ExtensionID::Nunchuk; - - m_mplus_desired_mode = MotionPlusState::PassthroughMode::Nunchuk; - } - else if (id_4 == 0x01 && id_5 == 0x01) - { - INFO_LOG_FMT(WIIMOTE, "WiiRemote: Classic Controller is attached."); - m_extension_id = ExtensionID::Classic; - - m_mplus_desired_mode = MotionPlusState::PassthroughMode::Classic; - } - else - { - // This is a normal occurance before extension initialization. - DEBUG_LOG_FMT(WIIMOTE, "WiiRemote: Unknown extension: {} {} {}.", id_0, id_4, id_5); - m_extension_id = ExtensionID::Unsupported; - } -} - -void Device::MotionPlusState::SetCalibrationData( - const WiimoteEmu::MotionPlus::CalibrationData& data) -{ - DEBUG_LOG_FMT(WIIMOTE, "WiiRemote: Set M+ calibration."); - - calibration.emplace(); - - calibration->fast = data.fast; - calibration->slow = data.slow; -} - -Device::NunchukState::Calibration::Calibration() : accel{}, stick{} -{ - accel.zero.data.fill(1 << (accel.BITS_OF_PRECISION - 1)); - // Approximate 1G value per WiiBrew: - accel.max.data.fill(740); - - stick.zero.data.fill(1 << (stick.BITS_OF_PRECISION - 1)); - stick.max.data.fill((1 << stick.BITS_OF_PRECISION) - 1); -} - -void Device::NunchukState::SetCalibrationData(const WiimoteEmu::Nunchuk::CalibrationData& data, - Checksum checksum) -{ - DEBUG_LOG_FMT(WIIMOTE, "WiiRemote: Set Nunchuk calibration."); - - calibration.emplace(); - - if (checksum == Checksum::Bad) - return; - - // Genuine Nunchuks have been observed with "min" and "max" values of zero. - // We catch that here and fall back to "full range" calibration. - const auto stick_calibration = data.GetStick(); - if (stick_calibration.IsSane()) - { - calibration->stick = stick_calibration; - } - else - { - WARN_LOG_FMT(WIIMOTE, - "WiiRemote: Nunchuk stick calibration is not sane. Using fallback values."); - } - - // No known reports of bad accelerometer calibration but we'll handle it just in case. - const auto accel_calibration = data.GetAccel(); - if (accel_calibration.IsSane()) - { - calibration->accel = accel_calibration; - } - else - { - WARN_LOG_FMT(WIIMOTE, - "WiiRemote: Nunchuk accel calibration is not sane. Using fallback values."); - } -} - -Device::ClassicState::Calibration::Calibration() - : left_stick{}, right_stick{}, left_trigger{}, right_trigger{} -{ - left_stick.zero.data.fill(1 << (left_stick.BITS_OF_PRECISION - 1)); - left_stick.max.data.fill((1 << left_stick.BITS_OF_PRECISION) - 1); - - right_stick.zero.data.fill(1 << (right_stick.BITS_OF_PRECISION - 1)); - right_stick.max.data.fill((1 << right_stick.BITS_OF_PRECISION) - 1); - - left_trigger.max = (1 << left_trigger.BITS_OF_PRECISION) - 1; - right_trigger.max = (1 << right_trigger.BITS_OF_PRECISION) - 1; -} - -void Device::ClassicState::SetCalibrationData(const WiimoteEmu::Classic::CalibrationData& data, - Checksum checksum) -{ - DEBUG_LOG_FMT(WIIMOTE, "WiiRemote: Set Classic Controller calibration."); - - calibration.emplace(); - - if (checksum == Checksum::Bad) - return; - - const auto left_stick_calibration = data.GetLeftStick(); - if (left_stick_calibration.IsSane()) - { - calibration->left_stick = left_stick_calibration; - } - else - { - WARN_LOG_FMT(WIIMOTE, - "WiiRemote: CC left stick calibration is not sane. Using fallback values."); - } - - const auto right_stick_calibration = data.GetRightStick(); - if (right_stick_calibration.IsSane()) - { - calibration->right_stick = right_stick_calibration; - } - else - { - WARN_LOG_FMT(WIIMOTE, - "WiiRemote: CC right stick calibration is not sane. Using fallback values."); - } - calibration->left_trigger = data.GetLeftTrigger(); - calibration->right_trigger = data.GetRightTrigger(); -} - -void Device::ReadActiveExtensionID() -{ - static constexpr u16 EXT_ID_ADDR = 0xfa; - static constexpr u16 EXT_ID_SIZE = 6; - - ReadData(AddressSpace::I2CBus, WiimoteEmu::ExtensionPort::REPORT_I2C_SLAVE, EXT_ID_ADDR, - EXT_ID_SIZE, [this](ReadResponse response) { - if (!response) - return; - - auto& identifier = *response; - - // Check for M+ ID. - if (identifier[5] == 0x05) - { - const auto passthrough_mode = MotionPlusState::PassthroughMode(identifier[4]); - - m_mplus_state.current_mode = passthrough_mode; - - INFO_LOG_FMT(WIIMOTE, "WiiRemote: M+ is active in mode: {}.", int(passthrough_mode)); - } - else - { - m_mplus_state.current_mode = MotionPlusState::PassthroughMode{}; - - ProcessExtensionID(identifier[0], identifier[4], identifier[5]); - } - }); -} - -bool Device::IRState::IsFullyConfigured() const -{ - return enabled && mode_set && current_sensitivity == GetDesiredIRSensitivity(); -} - -u32 Device::IRState::GetDesiredIRSensitivity() -{ - // Wii stores values from 1 to 5. (subtract 1) - const u32 configured_level = Config::Get(Config::SYSCONF_SENSOR_BAR_SENSITIVITY) - 1; - - if (configured_level < IR_SENSITIVITY_LEVEL_COUNT) - return configured_level; - - // Default to middle level on bad value. - return 2; -} - -void Device::SetIRSensitivity(u32 level) -{ - struct IRSensitivityConfig - { - std::array block1; - std::array block2; - }; - - // Data for Wii levels 1 to 5. - static constexpr std::array sensitivity_configs = - {{ - {{0x02, 0x00, 0x00, 0x71, 0x01, 0x00, 0x64, 0x00, 0xfe}, {0xfd, 0x05}}, - {{0x02, 0x00, 0x00, 0x71, 0x01, 0x00, 0x96, 0x00, 0xb4}, {0xb3, 0x04}}, - {{0x02, 0x00, 0x00, 0x71, 0x01, 0x00, 0xaa, 0x00, 0x64}, {0x63, 0x03}}, - {{0x02, 0x00, 0x00, 0x71, 0x01, 0x00, 0xc8, 0x00, 0x36}, {0x35, 0x03}}, - {{0x07, 0x00, 0x00, 0x71, 0x01, 0x00, 0x72, 0x00, 0x20}, {0x1f, 0x03}}, - }}; - - static constexpr u16 BLOCK1_ADDR = 0x00; - static constexpr u16 BLOCK2_ADDR = 0x1a; - - DEBUG_LOG_FMT(WIIMOTE, "WiiRemote: Setting IR sensitivity: {}.", level + 1); - - const auto& sensitivity_config = sensitivity_configs[level]; - - WriteData(AddressSpace::I2CBus, WiimoteEmu::CameraLogic::I2C_ADDR, BLOCK1_ADDR, - sensitivity_config.block1, [&sensitivity_config, level, this](ErrorCode block_result) { - if (block_result != ErrorCode::Success) - { - WARN_LOG_FMT(WIIMOTE, "WiiRemote: Failed to write IR block 1."); - return; - } - - WriteData(AddressSpace::I2CBus, WiimoteEmu::CameraLogic::I2C_ADDR, BLOCK2_ADDR, - sensitivity_config.block2, [&, level, this](ErrorCode block2_result) { - if (block2_result != ErrorCode::Success) - { - WARN_LOG_FMT(WIIMOTE, "WiiRemote: Failed to write IR block 2."); - return; - } - - DEBUG_LOG_FMT(WIIMOTE, "WiiRemote: IR sensitivity set."); - - m_ir_state.current_sensitivity = level; - }); - }); -} - -void Device::ConfigureIRCamera() -{ - if (!m_ir_state.enabled) - { - OutputReportIRLogicEnable2 ir_logic2 = {}; - ir_logic2.ack = 1; - ir_logic2.enable = 1; - QueueReport(ir_logic2, [this](ErrorCode result) { - if (result != ErrorCode::Success) - { - WARN_LOG_FMT(WIIMOTE, "WiiRemote: Failed to enable IR."); - return; - } - - OutputReportIRLogicEnable ir_logic = {}; - ir_logic.ack = 1; - ir_logic.enable = 1; - QueueReport(ir_logic, [this](ErrorCode ir_result) { - if (ir_result != ErrorCode::Success) - { - WARN_LOG_FMT(WIIMOTE, "WiiRemote: Failed to enable IR."); - return; - } - - DEBUG_LOG_FMT(WIIMOTE, "WiiRemote: IR enabled."); - - m_ir_state.enabled = true; - }); - }); - - return; - } - - if (const u32 desired_level = IRState::GetDesiredIRSensitivity(); - desired_level != m_ir_state.current_sensitivity) - { - SetIRSensitivity(desired_level); - - return; - } - - if (!m_ir_state.mode_set) - { - static constexpr u16 MODE_ADDR = 0x33; - - // We only support "Basic" mode (it's all that fits in ReportCoreAccelIR10Ext6). - WriteData(AddressSpace::I2CBus, WiimoteEmu::CameraLogic::I2C_ADDR, MODE_ADDR, - {WiimoteEmu::CameraLogic::IR_MODE_BASIC}, [this](ErrorCode mode_result) { - if (mode_result != ErrorCode::Success) - { - WARN_LOG_FMT(WIIMOTE, "WiiRemote: Failed to set IR mode."); - return; - } - - // This seems to enable object tracking. - static constexpr u16 ENABLE_ADDR = 0x30; - static constexpr u8 ENABLE_VALUE = 0x08; - - WriteData(AddressSpace::I2CBus, WiimoteEmu::CameraLogic::I2C_ADDR, ENABLE_ADDR, - {ENABLE_VALUE}, [this](ErrorCode result) { - if (result != ErrorCode::Success) - { - WARN_LOG_FMT(WIIMOTE, "WiiRemote: Failed to enable object tracking."); - return; - } - - DEBUG_LOG_FMT(WIIMOTE, "WiiRemote: IR mode set."); - - m_ir_state.mode_set = true; - }); - }); - } -} - -void Device::ConfigureSpeaker() -{ - OutputReportSpeakerMute mute = {}; - mute.enable = 1; - mute.ack = 1; - QueueReport(mute, [this](ErrorCode mute_result) { - if (mute_result != ErrorCode::Success) - { - WARN_LOG_FMT(WIIMOTE, "WiiRemote: Failed to mute speaker."); - return; - } - - OutputReportSpeakerEnable spkr = {}; - spkr.enable = 0; - spkr.ack = 1; - QueueReport(spkr, [this](ErrorCode enable_result) { - if (enable_result != ErrorCode::Success) - { - WARN_LOG_FMT(WIIMOTE, "WiiRemote: Failed to disable speaker."); - return; - } - - DEBUG_LOG_FMT(WIIMOTE, "WiiRemote: Speaker muted and disabled."); - - m_speaker_configured = true; - }); - }); -} - -void Device::TriggerMotionPlusModeChange() -{ - if (!m_mplus_desired_mode.has_value()) - return; - - const u8 passthrough_mode = u8(*m_mplus_desired_mode); - - const u8 device_addr = IsMotionPlusActive() ? WiimoteEmu::MotionPlus::ACTIVE_DEVICE_ADDR : - WiimoteEmu::MotionPlus::INACTIVE_DEVICE_ADDR; - - WriteData(AddressSpace::I2CBus, device_addr, WiimoteEmu::MotionPlus::PASSTHROUGH_MODE_OFFSET, - {passthrough_mode}, [this](ErrorCode activation_result) { - DEBUG_LOG_FMT(WIIMOTE, "WiiRemote: M+ activation: {}.", int(activation_result)); - - WaitForMotionPlus(); - - // Normally M+ will be seen performing a reset here. (extension port events) - // But sometimes (rarely) M+ activation does not cause an extension port event. - // We'll consider the mode unknown. It will be read back after some time. - m_mplus_state.current_mode = std::nullopt; - }); -} - -void Device::TriggerMotionPlusCalibration() -{ - static constexpr u16 CALIBRATION_TRIGGER_ADDR = 0xf2; - static constexpr u8 CALIBRATION_TRIGGER_VALUE = 0x00; - - // This triggers a hardware "zero" calibration. - // The effect is notiecable but output still strays from calibration data. - // It seems we're better off just manually determining "zero". - WriteData(AddressSpace::I2CBus, WiimoteEmu::MotionPlus::ACTIVE_DEVICE_ADDR, - CALIBRATION_TRIGGER_ADDR, {CALIBRATION_TRIGGER_VALUE}, [](ErrorCode result) { - DEBUG_LOG_FMT(WIIMOTE, "WiiRemote: M+ calibration trigger done: {}.", int(result)); - }); -} - -bool Device::IsMotionPlusStateKnown() const -{ - return m_mplus_state.current_mode.has_value(); -} - -bool Device::IsMotionPlusActive() const -{ - return m_mplus_state.current_mode != MotionPlusState::PassthroughMode{}; -} - -bool Device::IsMotionPlusInDesiredMode() const -{ - return m_mplus_state.current_mode.has_value() && - (m_mplus_state.current_mode == m_mplus_desired_mode); -} - -void Device::ProcessInputReport(WiimoteReal::Report& report) -{ - if (report.size() < WiimoteReal::REPORT_HID_HEADER_SIZE) - { - WARN_LOG_FMT(WIIMOTE, "WiiRemote: Bad report size."); - return; - } - - auto report_id = InputReportID(report[WiimoteReal::REPORT_HID_HEADER_SIZE]); - - for (auto it = m_report_handlers.begin(); true;) - { - if (it == m_report_handlers.end()) - { - if (report_id == InputReportID::Status) - { - if (report.size() - WiimoteReal::REPORT_HID_HEADER_SIZE < - sizeof(TypedInputData)) - { - WARN_LOG_FMT(WIIMOTE, "WiiRemote: Bad report size."); - } - else - { - ProcessStatusReport(Common::BitCastPtr(report.data() + 2)); - } - } - else if (report_id < InputReportID::ReportCore) - { - WARN_LOG_FMT(WIIMOTE, "WiiRemote: Unhandled input report: {}.", - ArrayToString(report.data(), u32(report.size()))); - } - - break; - } - - if (it->IsExpired()) - { - WARN_LOG_FMT(WIIMOTE, "WiiRemote: Removing expired handler."); - it = m_report_handlers.erase(it); - continue; - } - - if (const auto result = it->TryToHandleReport(report); - result == ReportHandler::HandlerResult::Handled) - { - it = m_report_handlers.erase(it); - break; - } - - ++it; - } - - if (report_id < InputReportID::ReportCore) - { - // Normal input reports can be processed as "ReportCore". - report_id = InputReportID::ReportCore; - } - else - { - // We can assume the last received input report is the current reporting mode. - // FYI: This logic fails to properly handle the (never used) "interleaved" reports. - m_reporting_mode = InputReportID(report_id); - } - - auto manipulator = MakeDataReportManipulator( - report_id, report.data() + WiimoteReal::REPORT_HID_HEADER_SIZE + sizeof(InputReportID)); - - if (manipulator->GetDataSize() + WiimoteReal::REPORT_HID_HEADER_SIZE > report.size()) - { - WARN_LOG_FMT(WIIMOTE, "WiiRemote: Bad report size."); - return; - } - - // Read buttons. - manipulator->GetCoreData(&m_core_data); - - // Process accel data. - if (manipulator->HasAccel() && m_accel_calibration.has_value()) - { - // FYI: This logic fails to properly handle the (never used) "interleaved" reports. - AccelData accel_data = {}; - manipulator->GetAccelData(&accel_data); - - m_accel_data = - accel_data.GetNormalizedValue(*m_accel_calibration) * float(MathUtil::GRAVITY_ACCELERATION); - } - - // Process IR data. - if (manipulator->HasIR() && m_ir_state.IsFullyConfigured()) - { - m_ir_state.ProcessData( - Common::BitCastPtr>(manipulator->GetIRDataPtr())); - } - - // Process extension data. - if (IsMotionPlusStateKnown()) - { - const auto ext_data = manipulator->GetExtDataPtr(); - const auto ext_size = manipulator->GetExtDataSize(); - - if (IsMotionPlusActive()) - ProcessMotionPlusExtensionData(ext_data, ext_size); - else - ProcessNormalExtensionData(ext_data, ext_size); - } - - UpdateOrientation(); -} - -void Device::UpdateOrientation() -{ - const auto current_report_time = Clock::now(); - const auto elapsed_time = std::chrono::duration_cast>( - current_report_time - m_last_report_time); - m_last_report_time = current_report_time; - - // Apply M+ gyro data to our orientation. - m_orientation = - WiimoteEmu::GetRotationFromGyroscope(m_mplus_state.gyro_data * -1 * elapsed_time.count()) * - m_orientation; - - // When M+ data is not available give accel/ir data more weight. - // ComplementaryFilter will then just smooth out our data a bit. - const bool is_mplus_active = IsMotionPlusStateKnown() && IsMotionPlusActive(); - - // With non-zero acceleration data we can perform pitch and roll correction. - if (m_accel_data.LengthSquared()) - { - const auto accel_weight = is_mplus_active ? 0.04 : 0.5f; - - m_orientation = WiimoteEmu::ComplementaryFilter(m_orientation, m_accel_data, accel_weight); - } - - // If IR objects are visible we can perform yaw and pitch correction. - if (!m_ir_state.is_hidden) - { - // FYI: We could do some roll correction from multiple IR objects. - - const auto ir_rotation = - Common::Vec3(m_ir_state.center_position.y * WiimoteEmu::CameraLogic::CAMERA_FOV_Y, 0, - m_ir_state.center_position.x * WiimoteEmu::CameraLogic::CAMERA_FOV_X) / - 2; - const auto ir_normal = Common::Vec3(0, 1, 0); - const auto ir_vector = WiimoteEmu::GetRotationFromGyroscope(-ir_rotation) * ir_normal; - - // Pitch correction will be slightly wrong based on sensorbar height. - // Keep weight below accelerometer weight for that reason. - // Correction will only happen near pitch zero when the sensorbar is actually in view. - const auto ir_weight = is_mplus_active ? 0.035 : 0.45f; - - m_orientation = WiimoteEmu::ComplementaryFilter(m_orientation, ir_vector, ir_weight, ir_normal); - } - - // Normalize for floating point inaccuracies. - m_orientation = m_orientation.Normalized(); - - // Update our (pitch, roll, yaw) inputs now that orientation has been adjusted. - m_rotation_inputs = - Common::Vec3{WiimoteEmu::GetPitch(m_orientation), WiimoteEmu::GetRoll(m_orientation), - WiimoteEmu::GetYaw(m_orientation)} / - float(MathUtil::PI); -} - -void Device::IRState::ProcessData(const std::array& data) -{ - // A better implementation might extrapolate points when they fall out of camera view. - // But just averaging visible points actually seems to work very well. - - using IRObject = WiimoteEmu::IRBasic::IRObject; - - MathUtil::RunningVariance points; - - const auto camera_max = IRObject(WiimoteEmu::CameraLogic::CAMERA_RES_X - 1, - WiimoteEmu::CameraLogic::CAMERA_RES_Y - 1); - - const auto add_point = [&](IRObject point) { - // Non-visible points are 0xFF-filled. - if (point.y > camera_max.y) - return; - - points.Push(Common::Vec2(point)); - }; - - for (auto& block : data) - { - add_point(block.GetObject1()); - add_point(block.GetObject2()); - } - - is_hidden = !points.Count(); - - if (points.Count() >= 2) - { - const auto variance = points.PopulationVariance(); - // Adjusts Y coorinate to match horizontal FOV. - const auto separation = - Common::Vec2(std::sqrt(variance.x), std::sqrt(variance.y)) / - Common::Vec2(WiimoteEmu::CameraLogic::CAMERA_RES_X, - WiimoteEmu::CameraLogic::CAMERA_RES_Y * WiimoteEmu::CameraLogic::CAMERA_AR) * - 2; - - distance = WiimoteEmu::CameraLogic::SENSOR_BAR_LED_SEPARATION / separation.Length() / 2 / - std::tan(WiimoteEmu::CameraLogic::CAMERA_FOV_X / 2); - } - - if (points.Count()) - { - center_position = points.Mean() / Common::Vec2(camera_max) * 2.f - Common::Vec2(1, 1); - } - else - { - center_position = {}; - } -} - -void Device::ProcessMotionPlusExtensionData(const u8* ext_data, u32 ext_size) -{ - if (ext_size < sizeof(WiimoteEmu::MotionPlus::DataFormat)) - return; - - const WiimoteEmu::MotionPlus::DataFormat mplus_data = - Common::BitCastPtr(ext_data); - - const bool is_ext_connected = mplus_data.extension_connected; - - // Handle passthrough extension change. - if (is_ext_connected != m_mplus_state.passthrough_port) - { - m_mplus_state.passthrough_port = is_ext_connected; - - DEBUG_LOG_FMT(WIIMOTE, "WiiRemote: M+ passthrough port event: {}.", is_ext_connected); - - // With no passthrough extension we'll be happy with the current mode. - if (!is_ext_connected) - m_mplus_desired_mode = m_mplus_state.current_mode; - - ProcessExtensionEvent(is_ext_connected); - } - - if (mplus_data.is_mp_data) - { - m_mplus_state.ProcessData(mplus_data); - return; - } - - if (!IsMotionPlusInDesiredMode()) - { - DEBUG_LOG_FMT(WIIMOTE, "WiiRemote: Ignoring unwanted passthrough data."); - return; - } - - std::array data; - std::copy_n(ext_data, ext_size, data.begin()); - - // Undo bit-hacks of M+ passthrough. - WiimoteEmu::MotionPlus::ReversePassthroughModifications(*m_mplus_state.current_mode, data.data()); - - ProcessNormalExtensionData(data.data(), u32(data.size())); -} - -void Device::ProcessNormalExtensionData(const u8* ext_data, u32 ext_size) -{ - if (m_extension_id == ExtensionID::Nunchuk) - { - if (ext_size < sizeof(WiimoteEmu::MotionPlus::DataFormat)) - return; - - const WiimoteEmu::Nunchuk::DataFormat nunchuk_data = - Common::BitCastPtr(ext_data); - - m_nunchuk_state.ProcessData(nunchuk_data); - } - else if (m_extension_id == ExtensionID::Classic) - { - if (ext_size < sizeof(WiimoteEmu::Classic::DataFormat)) - return; - - const WiimoteEmu::Classic::DataFormat cc_data = - Common::BitCastPtr(ext_data); - - m_classic_state.ProcessData(cc_data); - } -} - -void Device::UpdateRumble() -{ - static constexpr auto rumble_period = std::chrono::milliseconds(100); - - const auto on_time = std::chrono::duration_cast(rumble_period * m_rumble_level); - const auto off_time = rumble_period - on_time; - - const auto now = Clock::now(); - - if (m_rumble && (now < m_last_rumble_change + on_time || !off_time.count())) - return; - - if (!m_rumble && (now < m_last_rumble_change + off_time || !on_time.count())) - return; - - m_last_rumble_change = now; - m_rumble ^= true; - - // Rumble flag will be set within QueueReport. - QueueReport(OutputReportRumble{}); -} - -void Device::UpdateInput() -{ - if (!m_wiimote->IsConnected()) - { - g_controller_interface.RemoveDevice( - [this](const Core::Device* device) { return device == this; }); - return; - } - - UpdateRumble(); - RunTasks(); - - WiimoteReal::Report report; - while (m_wiimote->GetNextReport(&report)) - { - ProcessInputReport(report); - RunTasks(); - } -} - -void Device::MotionPlusState::ProcessData(const WiimoteEmu::MotionPlus::DataFormat& data) -{ - // We need the calibration block read to know the sensor orientations. - if (!calibration.has_value()) - return; - - gyro_data = data.GetData().GetAngularVelocity(*calibration); -} - -bool Device::IsWaitingForMotionPlus() const -{ - return Clock::now() < m_mplus_wait_time; -} - -void Device::WaitForMotionPlus() -{ - DEBUG_LOG_FMT(WIIMOTE, "WiiRemote: Wait for M+."); - m_mplus_wait_time = Clock::now() + std::chrono::seconds{2}; -} - -void Device::NunchukState::ProcessData(const WiimoteEmu::Nunchuk::DataFormat& data) -{ - buttons = data.GetButtons(); - - // Stick/accel require calibration data. - if (!calibration.has_value()) - return; - - stick = data.GetStick().GetNormalizedValue(calibration->stick); - accel = data.GetAccel().GetNormalizedValue(calibration->accel) * - float(MathUtil::GRAVITY_ACCELERATION); -} - -void Device::ClassicState::ProcessData(const WiimoteEmu::Classic::DataFormat& data) -{ - buttons = data.GetButtons(); - - // Sticks/triggers require calibration data. - if (!calibration.has_value()) - return; - - sticks[0] = data.GetLeftStick().GetNormalizedValue(calibration->left_stick); - sticks[1] = data.GetRightStick().GetNormalizedValue(calibration->right_stick); - triggers[0] = data.GetLeftTrigger().GetNormalizedValue(calibration->left_trigger); - triggers[1] = data.GetRightTrigger().GetNormalizedValue(calibration->right_trigger); -} - -void Device::ReadData(AddressSpace space, u8 slave, u16 address, u16 size, - std::function callback) -{ - OutputReportReadData read_data{}; - read_data.space = u8(space); - read_data.slave_address = slave; - read_data.address[0] = u8(address >> 8); - read_data.address[1] = u8(address); - read_data.size[0] = u8(size >> 8); - read_data.size[1] = u8(size); - QueueReport(read_data); - - AddReadDataReplyHandler(space, slave, address, size, {}, std::move(callback)); -} - -void Device::AddReadDataReplyHandler(AddressSpace space, u8 slave, u16 address, u16 size, - std::vector starting_data, - std::function callback) -{ - // Data read may return a busy ack. - auto ack_handler = MakeAckHandler(OutputReportID::ReadData, [callback](ErrorCode result) { - DEBUG_LOG_FMT(WIIMOTE, "WiiRemote: Read ack error: {}.", int(result)); - callback(ReadResponse{}); - }); - - // Or more normally a "ReadDataReply". - auto read_handler = [this, space, slave, address, size, data = std::move(starting_data), - callback = - std::move(callback)](const InputReportReadDataReply& reply) mutable { - if (Common::swap16(reply.address) != address) - return ReportHandler::HandlerResult::NotHandled; - - if (reply.error != u8(ErrorCode::Success)) - { - DEBUG_LOG_FMT(WIIMOTE, "WiiRemote: Read reply error: {}.", int(reply.error)); - callback(ReadResponse{}); - - return ReportHandler::HandlerResult::Handled; - } - - const auto read_count = reply.size_minus_one + 1; - - data.insert(data.end(), reply.data, reply.data + read_count); - - if (read_count < size) - { - // We have more data to acquire. - AddReadDataReplyHandler(space, slave, address + read_count, size - read_count, - std::move(data), std::move(callback)); - } - else - { - // We have all the data. - callback(std::move(data)); - } - - return ReportHandler::HandlerResult::Handled; - }; - - AddReportHandler( - std::function( - std::move(read_handler)), - std::move(ack_handler)); -} - -template -void Device::WriteData(AddressSpace space, u8 slave, u16 address, T&& data, C&& callback) -{ - OutputReportWriteData write_data = {}; - write_data.space = u8(space); - write_data.slave_address = slave; - write_data.address[0] = u8(address >> 8); - write_data.address[1] = u8(address); - - static constexpr auto MAX_DATA_SIZE = std::size(write_data.data); - write_data.size = u8(std::min(std::size(data), MAX_DATA_SIZE)); - - std::copy_n(std::begin(data), write_data.size, write_data.data); - - // Writes of more than 16 bytes must be split into multiple reports. - if (std::size(data) > MAX_DATA_SIZE) - { - auto next_write = [this, space, slave, address, - additional_data = - std::vector(std::begin(data) + MAX_DATA_SIZE, std::end(data)), - callback = std::forward(callback)](ErrorCode result) mutable { - if (result != ErrorCode::Success) - callback(result); - else - WriteData(space, slave, address + MAX_DATA_SIZE, additional_data, std::move(callback)); - }; - - QueueReport(write_data, std::move(next_write)); - } - else - { - QueueReport(write_data, std::forward(callback)); - } -} - -Device::ReportHandler::ReportHandler(Clock::time_point expired_time) : m_expired_time(expired_time) -{ -} - -template -void Device::AddReportHandler(T&&... callbacks) -{ - auto& handler = m_report_handlers.emplace_back(Clock::now() + std::chrono::seconds{5}); - (handler.AddHandler(std::forward(callbacks)), ...); -} - -template -void Device::ReportHandler::AddHandler(std::function handler) -{ - m_callbacks.emplace_back([handler = std::move(handler)](const WiimoteReal::Report& report) { - if (report[WiimoteReal::REPORT_HID_HEADER_SIZE] != u8(T::REPORT_ID)) - return ReportHandler::HandlerResult::NotHandled; - - T data; - - if (report.size() < sizeof(T) + WiimoteReal::REPORT_HID_HEADER_SIZE + 1) - { - // Off-brand "NEW 2in1" Wii Remote likes to shorten read data replies. - WARN_LOG_FMT(WIIMOTE, "WiiRemote: Bad report size ({}) for report {:#x}. Zero-filling.", - report.size(), int(T::REPORT_ID)); - - data = {}; - std::memcpy(&data, report.data() + WiimoteReal::REPORT_HID_HEADER_SIZE + 1, - report.size() - WiimoteReal::REPORT_HID_HEADER_SIZE + 1); - } - else - { - data = Common::BitCastPtr(report.data() + WiimoteReal::REPORT_HID_HEADER_SIZE + 1); - } - - if constexpr (std::is_same_v) - { - handler(data); - return ReportHandler::HandlerResult::Handled; - } - else - { - return handler(data); - } - }); -} - -auto Device::ReportHandler::TryToHandleReport(const WiimoteReal::Report& report) -> HandlerResult -{ - for (auto& callback : m_callbacks) - { - if (const auto result = callback(report); result != HandlerResult::NotHandled) - return result; - } - - return HandlerResult::NotHandled; -} - -bool Device::ReportHandler::IsExpired() const -{ - return Clock::now() >= m_expired_time; -} - -auto Device::MakeAckHandler(OutputReportID report_id, - std::function callback) - -> AckReportHandler -{ - return [report_id, callback = std::move(callback)](const InputReportAck& reply) { - if (reply.rpt_id != report_id) - return ReportHandler::HandlerResult::NotHandled; - - callback(reply.error_code); - return ReportHandler::HandlerResult::Handled; - }; -} - -bool Device::IsPerformingTask() const -{ - return !m_report_handlers.empty(); -} - -void Device::ProcessStatusReport(const InputReportStatus& status) -{ - // Update status periodically to keep battery level value up to date. - m_status_outdated_time = Clock::now() + std::chrono::seconds(10); - - m_battery = status.GetEstimatedCharge() * BATTERY_INPUT_MAX_VALUE; - m_leds = status.leds; - - if (!status.ir) - m_ir_state = {}; - - const bool is_ext_connected = status.extension; - - // Handle extension port state change. - if (is_ext_connected != m_extension_port) - { - DEBUG_LOG_FMT(WIIMOTE, "WiiRemote: Extension port event: {}.", is_ext_connected); - - m_extension_port = is_ext_connected; - - // Data reporting stops on an extension port event. - m_reporting_mode = InputReportID::ReportDisabled; - - ProcessExtensionEvent(is_ext_connected); - - // The M+ is now in an unknown state. - m_mplus_state = {}; - - if (is_ext_connected) - { - // We can assume the M+ is settled on an attachment event. - m_mplus_wait_time = Clock::now(); - } - else - { - // "Nunchuk" will be the most used mode and also works with no passthrough extension. - m_mplus_desired_mode = MotionPlusState::PassthroughMode::Nunchuk; - - // If an extension is not connected the M+ is either disabled or resetting. - m_mplus_state.current_mode = MotionPlusState::PassthroughMode{}; - } - } -} - -} // namespace ciface::Wiimote diff --git a/Source/Core/InputCommon/ControllerInterface/Wiimote/Wiimote.h b/Source/Core/InputCommon/ControllerInterface/Wiimote/Wiimote.h deleted file mode 100644 index 0dc9082af6..0000000000 --- a/Source/Core/InputCommon/ControllerInterface/Wiimote/Wiimote.h +++ /dev/null @@ -1,282 +0,0 @@ -// Copyright 2020 Dolphin Emulator Project -// Licensed under GPLv2+ -// Refer to the license.txt file included. - -#pragma once - -#include -#include -#include -#include - -#include "Core/HW/WiimoteCommon/DataReport.h" -#include "Core/HW/WiimoteCommon/WiimoteReport.h" -#include "Core/HW/WiimoteEmu/Camera.h" -#include "Core/HW/WiimoteEmu/Extension/Classic.h" -#include "Core/HW/WiimoteEmu/Extension/Nunchuk.h" -#include "Core/HW/WiimoteEmu/MotionPlus.h" -#include "Core/HW/WiimoteReal/WiimoteReal.h" -#include "InputCommon/ControllerInterface/Device.h" - -namespace ciface::Wiimote -{ -using namespace WiimoteCommon; - -void AddDevice(std::unique_ptr); -void ReleaseDevices(std::optional count = std::nullopt); - -class Device final : public Core::Device -{ -public: - Device(std::unique_ptr wiimote); - ~Device(); - - std::string GetName() const override; - std::string GetSource() const override; - - void UpdateInput() override; - -private: - using Clock = std::chrono::steady_clock; - - enum class ExtensionID - { - Nunchuk, - Classic, - Unsupported, - }; - - enum class Checksum - { - Good, - Bad, - }; - - class MotionPlusState - { - public: - void SetCalibrationData(const WiimoteEmu::MotionPlus::CalibrationData&); - void ProcessData(const WiimoteEmu::MotionPlus::DataFormat&); - - using PassthroughMode = WiimoteEmu::MotionPlus::PassthroughMode; - - // State is unknown by default. - std::optional current_mode; - - // The last known state of the passthrough port flag. - // Used to detect passthrough extension port events. - std::optional passthrough_port; - - Common::Vec3 gyro_data = {}; - - std::optional calibration; - }; - - struct NunchukState - { - using CalibrationData = WiimoteEmu::Nunchuk::CalibrationData; - - void SetCalibrationData(const CalibrationData&, Checksum); - void ProcessData(const WiimoteEmu::Nunchuk::DataFormat&); - - Common::Vec2 stick = {}; - Common::Vec3 accel = {}; - - u8 buttons = 0; - - struct Calibration - { - Calibration(); - - CalibrationData::AccelCalibration accel; - CalibrationData::StickCalibration stick; - }; - - std::optional calibration; - }; - - struct ClassicState - { - using CalibrationData = WiimoteEmu::Classic::CalibrationData; - - void SetCalibrationData(const CalibrationData&, Checksum); - void ProcessData(const WiimoteEmu::Classic::DataFormat&); - - std::array sticks = {}; - std::array triggers = {}; - - u16 buttons = 0; - - struct Calibration - { - Calibration(); - - CalibrationData::StickCalibration left_stick; - CalibrationData::StickCalibration right_stick; - - CalibrationData::TriggerCalibration left_trigger; - CalibrationData::TriggerCalibration right_trigger; - }; - - std::optional calibration; - }; - - struct IRState - { - static u32 GetDesiredIRSensitivity(); - - void ProcessData(const std::array&); - bool IsFullyConfigured() const; - - u32 current_sensitivity = u32(-1); - bool enabled = false; - bool mode_set = false; - - // Average of visible IR "objects". - Common::Vec2 center_position = {}; - - float distance = 0; - - bool is_hidden = true; - }; - - class ReportHandler - { - public: - enum class HandlerResult - { - Handled, - NotHandled, - }; - - ReportHandler(Clock::time_point expired_time); - - template - void AddHandler(std::function); - - HandlerResult TryToHandleReport(const WiimoteReal::Report& report); - - bool IsExpired() const; - - private: - const Clock::time_point m_expired_time; - std::vector> m_callbacks; - }; - - using AckReportHandler = std::function; - - static AckReportHandler MakeAckHandler(OutputReportID report_id, - std::function callback); - - // TODO: Make parameter const. (need to modify DataReportManipulator) - void ProcessInputReport(WiimoteReal::Report& report); - void ProcessMotionPlusExtensionData(const u8* data, u32 size); - void ProcessNormalExtensionData(const u8* data, u32 size); - void ProcessExtensionEvent(bool connected); - void ProcessExtensionID(u8 id_0, u8 id_4, u8 id_5); - void ProcessStatusReport(const InputReportStatus&); - - void RunTasks(); - - bool IsPerformingTask() const; - - template - void QueueReport(T&& report, std::function ack_callback = {}); - - template - void AddReportHandler(T&&... callbacks); - - using ReadResponse = std::optional>; - - void ReadData(AddressSpace space, u8 slave, u16 address, u16 size, - std::function callback); - - void AddReadDataReplyHandler(AddressSpace space, u8 slave, u16 address, u16 size, - std::vector starting_data, - std::function callback); - - template , typename C> - void WriteData(AddressSpace space, u8 slave, u16 address, T&& data, C&& callback); - - void ReadActiveExtensionID(); - void SetIRSensitivity(u32 level); - void ConfigureSpeaker(); - void ConfigureIRCamera(); - - u8 GetDesiredLEDValue() const; - - void TriggerMotionPlusModeChange(); - void TriggerMotionPlusCalibration(); - - bool IsMotionPlusStateKnown() const; - bool IsMotionPlusActive() const; - bool IsMotionPlusInDesiredMode() const; - - bool IsWaitingForMotionPlus() const; - void WaitForMotionPlus(); - void HandleMotionPlusNonResponse(); - - void UpdateRumble(); - void UpdateOrientation(); - void UpdateExtensionNumberInput(); - - std::unique_ptr m_wiimote; - - // Buttons. - DataReportManipulator::CoreData m_core_data = {}; - - // Accelerometer. - Common::Vec3 m_accel_data = {}; - std::optional m_accel_calibration; - - // Pitch, Roll, Yaw inputs. - Common::Vec3 m_rotation_inputs = {}; - - MotionPlusState m_mplus_state = {}; - NunchukState m_nunchuk_state = {}; - ClassicState m_classic_state = {}; - IRState m_ir_state = {}; - - // Used to poll for M+ periodically and wait for it to reset. - Clock::time_point m_mplus_wait_time = Clock::now(); - - // The desired mode is set based on the attached normal extension. - std::optional m_mplus_desired_mode; - - // Status report is requested every so often to update the battery level. - Clock::time_point m_status_outdated_time = Clock::now(); - float m_battery = 0; - u8 m_leds = 0; - - bool m_speaker_configured = false; - - // The last known state of the extension port status flag. - // Used to detect extension port events. - std::optional m_extension_port; - - // Note this refers to the passthrough extension when M+ is active. - std::optional m_extension_id; - - // Rumble state must be saved to set the proper flag in every output report. - bool m_rumble = false; - - // For pulse of rumble motor to simulate multiple levels. - ControlState m_rumble_level = 0; - Clock::time_point m_last_rumble_change = Clock::now(); - - // Assume mode is disabled so one gets set. - InputReportID m_reporting_mode = InputReportID::ReportDisabled; - - // Used only to provide a value for a specialty "input". (for attached extension passthrough) - WiimoteEmu::ExtensionNumber m_extension_number_input = WiimoteEmu::ExtensionNumber::NONE; - bool m_mplus_attached_input = false; - - // Holds callbacks for output report replies. - std::list m_report_handlers; - - // World rotation. (used to rotate IR data and provide pitch, roll, yaw inputs) - Common::Quaternion m_orientation = Common::Quaternion::Identity(); - Clock::time_point m_last_report_time = Clock::now(); -}; - -} // namespace ciface::Wiimote diff --git a/Source/Core/InputCommon/ControllerInterface/Wiimote/WiimoteController.cpp b/Source/Core/InputCommon/ControllerInterface/Wiimote/WiimoteController.cpp new file mode 100644 index 0000000000..eaa56ef449 --- /dev/null +++ b/Source/Core/InputCommon/ControllerInterface/Wiimote/WiimoteController.cpp @@ -0,0 +1,1680 @@ +// Copyright 2020 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#include "InputCommon/ControllerInterface/Wiimote/WiimoteController.h" + +#include "Common/BitUtils.h" +#include "Common/Logging/Log.h" +#include "Common/MathUtil.h" +#include "Core/Config/SYSCONFSettings.h" +#include "Core/HW/WiimoteEmu/ExtensionPort.h" +#include "Core/HW/WiimoteEmu/WiimoteEmu.h" +#include "InputCommon/ControllerEmu/ControllerEmu.h" +#include "InputCommon/ControllerInterface/ControllerInterface.h" + +namespace ciface::WiimoteController +{ +static constexpr char SOURCE_NAME[] = "Bluetooth"; + +static constexpr size_t IR_SENSITIVITY_LEVEL_COUNT = 5; + +template +class Button final : public Core::Device::Input +{ +public: + Button(const T* value, std::common_type_t mask, std::string name) + : m_value(*value), m_mask(mask), m_name(std::move(name)) + { + } + + std::string GetName() const override { return m_name; } + + ControlState GetState() const override { return (m_value & m_mask) != 0; } + +private: + const T& m_value; + const T m_mask; + const std::string m_name; +}; + +// GetState returns value divided by supplied "extent". +template +class GenericInput : public Core::Device::Input +{ +public: + GenericInput(const T* value, std::string name, ControlState extent) + : m_value(*value), m_name(std::move(name)), m_extent(extent) + { + } + + bool IsDetectable() const override { return Detectable; } + + std::string GetName() const override { return m_name; } + + ControlState GetState() const final override { return ControlState(m_value) / m_extent; } + +protected: + const T& m_value; + const std::string m_name; + const ControlState m_extent; +}; + +template +using AnalogInput = GenericInput; + +template +using UndetectableAnalogInput = GenericInput; + +// GetName() is appended with '-' or '+' based on sign of "extent" value. +template +class SignedInput final : public GenericInput +{ +public: + using GenericInput::GenericInput; + + std::string GetName() const override { return this->m_name + (this->m_extent < 0 ? '-' : '+'); } +}; + +using SignedAnalogInput = SignedInput; +using UndetectableSignedAnalogInput = SignedInput; + +class Motor final : public Core::Device::Output +{ +public: + Motor(ControlState* value) : m_value(*value) {} + + std::string GetName() const override { return "Motor"; } + + void SetState(ControlState state) override { m_value = state; } + +private: + ControlState& m_value; +}; + +template +void Device::QueueReport(T&& report, std::function ack_callback) +{ + // Maintain proper rumble state. + report.rumble = m_rumble; + + m_wiimote->QueueReport(report.REPORT_ID, &report, sizeof(report)); + + if (ack_callback) + AddReportHandler(MakeAckHandler(report.REPORT_ID, std::move(ack_callback))); +} + +void AddDevice(std::unique_ptr wiimote) +{ + // Our real wiimote class requires an index. + // Within the pool it's only going to be used for logging purposes. + static constexpr int CIFACE_WIIMOTE_INDEX = 55; + + if (!wiimote->Connect(CIFACE_WIIMOTE_INDEX)) + { + WARN_LOG_FMT(WIIMOTE, "WiiRemote: Failed to connect."); + return; + } + + wiimote->Prepare(); + wiimote->EventLinked(); + + g_controller_interface.AddDevice(std::make_shared(std::move(wiimote))); +} + +void ReleaseDevices(std::optional count) +{ + u32 removed_devices = 0; + + // Remove up to "count" remotes (or all of them if nullopt). + // Real wiimotes will be added to the pool. + g_controller_interface.RemoveDevice([&](const Core::Device* device) { + if (device->GetSource() != SOURCE_NAME || count == removed_devices) + return false; + + ++removed_devices; + return true; + }); +} + +Device::Device(std::unique_ptr wiimote) : m_wiimote(std::move(wiimote)) +{ + using EmuWiimote = WiimoteEmu::Wiimote; + + // Buttons. + static constexpr std::pair button_masks[] = { + {EmuWiimote::BUTTON_A, "A"}, {EmuWiimote::BUTTON_B, "B"}, + {EmuWiimote::BUTTON_ONE, "1"}, {EmuWiimote::BUTTON_TWO, "2"}, + {EmuWiimote::BUTTON_MINUS, "-"}, {EmuWiimote::BUTTON_PLUS, "+"}, + {EmuWiimote::BUTTON_HOME, "HOME"}, + }; + + for (auto& button : button_masks) + AddInput(new Button(&m_core_data.hex, button.first, button.second)); + + static constexpr u16 dpad_masks[] = { + EmuWiimote::PAD_UP, + EmuWiimote::PAD_DOWN, + EmuWiimote::PAD_LEFT, + EmuWiimote::PAD_RIGHT, + }; + + // Friendly orientation inputs. + static constexpr const char* const rotation_names[] = {"Pitch", "Roll", "Yaw"}; + for (std::size_t i = 0; i != std::size(rotation_names); ++i) + { + AddInput( + new UndetectableSignedAnalogInput(&m_rotation_inputs.data[i], rotation_names[i], -1.f)); + AddInput(new UndetectableSignedAnalogInput(&m_rotation_inputs.data[i], rotation_names[i], 1.f)); + } + + // Raw accelerometer. + for (std::size_t i = 0; i != std::size(dpad_masks); ++i) + AddInput(new Button(&m_core_data.hex, dpad_masks[i], named_directions[i])); + + static constexpr std::array, 3> accel_names = {{ + {"Accel Left", "Accel Right"}, + {"Accel Backward", "Accel Forward"}, + {"Accel Up", "Accel Down"}, + }}; + + for (std::size_t i = 0; i != m_accel_data.data.size(); ++i) + { + AddInput(new UndetectableAnalogInput(&m_accel_data.data[i], accel_names[i][0], 1)); + AddInput(new UndetectableAnalogInput(&m_accel_data.data[i], accel_names[i][1], -1)); + } + + // IR data. + static constexpr const char* const ir_names[] = {"IR Center X", "IR Center Y"}; + for (std::size_t i = 0; i != std::size(ir_names); ++i) + { + AddInput( + new UndetectableSignedAnalogInput(&m_ir_state.center_position.data[i], ir_names[i], -1.f)); + AddInput( + new UndetectableSignedAnalogInput(&m_ir_state.center_position.data[i], ir_names[i], 1.f)); + } + + AddInput(new UndetectableAnalogInput(&m_ir_state.is_hidden, "IR Hidden", 1)); + + AddInput(new UndetectableAnalogInput(&m_ir_state.distance, "IR Distance", 1)); + + // Raw gyroscope. + static constexpr std::array, 3> gyro_names = {{ + {"Gyro Pitch Down", "Gyro Pitch Up"}, + {"Gyro Roll Left", "Gyro Roll Right"}, + {"Gyro Yaw Left", "Gyro Yaw Right"}, + }}; + + for (std::size_t i = 0; i != m_accel_data.data.size(); ++i) + { + AddInput( + new UndetectableAnalogInput(&m_mplus_state.gyro_data.data[i], gyro_names[i][0], 1)); + AddInput( + new UndetectableAnalogInput(&m_mplus_state.gyro_data.data[i], gyro_names[i][1], -1)); + } + + using WiimoteEmu::Nunchuk; + const std::string nunchuk_prefix = "Nunchuk "; + + // Buttons. + AddInput(new Button(&m_nunchuk_state.buttons, Nunchuk::BUTTON_C, nunchuk_prefix + "C")); + AddInput(new Button(&m_nunchuk_state.buttons, Nunchuk::BUTTON_Z, nunchuk_prefix + "Z")); + + // Stick. + static constexpr const char* const nunchuk_stick_names[] = {"X", "Y"}; + for (std::size_t i = 0; i != std::size(nunchuk_stick_names); ++i) + { + AddInput(new SignedAnalogInput(&m_nunchuk_state.stick.data[i], + nunchuk_prefix + nunchuk_stick_names[i], -1.f)); + AddInput(new SignedAnalogInput(&m_nunchuk_state.stick.data[i], + nunchuk_prefix + nunchuk_stick_names[i], 1.f)); + } + + // Raw accelerometer. + for (std::size_t i = 0; i != m_accel_data.data.size(); ++i) + { + AddInput(new UndetectableAnalogInput(&m_nunchuk_state.accel.data[i], + nunchuk_prefix + accel_names[i][0], 1)); + AddInput(new UndetectableAnalogInput(&m_nunchuk_state.accel.data[i], + nunchuk_prefix + accel_names[i][1], -1)); + } + + using WiimoteEmu::Classic; + const std::string classic_prefix = "Classic "; + + // Buttons. + static constexpr u16 classic_dpad_masks[] = { + Classic::PAD_UP, + Classic::PAD_DOWN, + Classic::PAD_LEFT, + Classic::PAD_RIGHT, + }; + + for (std::size_t i = 0; i != std::size(classic_dpad_masks); ++i) + AddInput(new Button(&m_classic_state.buttons, classic_dpad_masks[i], + classic_prefix + named_directions[i])); + + static constexpr u16 classic_button_masks[] = { + Classic::BUTTON_A, Classic::BUTTON_B, Classic::BUTTON_X, Classic::BUTTON_Y, + Classic::TRIGGER_L, Classic::TRIGGER_R, Classic::BUTTON_ZL, Classic::BUTTON_ZR, + Classic::BUTTON_MINUS, Classic::BUTTON_PLUS, Classic::BUTTON_HOME, + }; + + static constexpr const char* const classic_button_names[] = { + "A", "B", "X", "Y", "L", "R", "ZL", "ZR", "-", "+", "HOME", + }; + + for (std::size_t i = 0; i != std::size(classic_button_masks); ++i) + AddInput(new Button(&m_classic_state.buttons, classic_button_masks[i], + classic_prefix + classic_button_names[i])); + + // Sticks. + static constexpr const char* const classic_stick_names[][2] = {{"Left X", "Left Y"}, + {"Right X", "Right Y"}}; + + for (std::size_t s = 0; s != std::size(m_classic_state.sticks); ++s) + { + for (std::size_t i = 0; i != std::size(m_classic_state.sticks[0].data); ++i) + { + AddInput(new SignedAnalogInput(&m_classic_state.sticks[s].data[i], + classic_prefix + classic_stick_names[s][i], -1.f)); + AddInput(new SignedAnalogInput(&m_classic_state.sticks[s].data[i], + classic_prefix + classic_stick_names[s][i], 1.f)); + } + } + + // Triggers. + AddInput(new AnalogInput(&m_classic_state.triggers[0], classic_prefix + "L-Analog", 1.f)); + AddInput(new AnalogInput(&m_classic_state.triggers[1], classic_prefix + "R-Analog", 1.f)); + + // Specialty inputs: + AddInput(new UndetectableAnalogInput(&m_battery, "Battery", 1.f)); + AddInput(new UndetectableAnalogInput( + &m_extension_number_input, "Attached Extension", WiimoteEmu::ExtensionNumber(1))); + AddInput(new UndetectableAnalogInput(&m_mplus_attached_input, "Attached MotionPlus", 1)); + + AddOutput(new Motor(&m_rumble_level)); +} + +Device::~Device() +{ + if (!m_wiimote->IsConnected()) + return; + + m_wiimote->EmuStop(); + + INFO_LOG_FMT(WIIMOTE, "WiiRemote: Returning remote to pool."); + WiimoteReal::AddWiimoteToPool(std::move(m_wiimote)); +} + +std::string Device::GetName() const +{ + return "Wii Remote"; +} + +std::string Device::GetSource() const +{ + return SOURCE_NAME; +} + +void Device::RunTasks() +{ + if (IsPerformingTask()) + return; + + // Request status. + if (Clock::now() >= m_status_outdated_time) + { + QueueReport(OutputReportRequestStatus()); + + AddReportHandler(std::function( + [this](const InputReportStatus& status) { + DEBUG_LOG_FMT(WIIMOTE, "WiiRemote: Received requested status."); + ProcessStatusReport(status); + })); + + return; + } + + // Set LEDs. + const auto desired_leds = GetDesiredLEDValue(); + if (m_leds != desired_leds) + { + OutputReportLeds rpt = {}; + rpt.ack = 1; + rpt.leds = desired_leds; + QueueReport(rpt, [this, desired_leds](ErrorCode result) { + if (result != ErrorCode::Success) + { + WARN_LOG_FMT(WIIMOTE, "WiiRemote: Failed to set LEDs."); + return; + } + + DEBUG_LOG_FMT(WIIMOTE, "WiiRemote: Set LEDs."); + + m_leds = desired_leds; + }); + + return; + } + + // Set reporting mode to one that supports every component. + static constexpr auto desired_reporting_mode = InputReportID::ReportCoreAccelIR10Ext6; + if (m_reporting_mode != desired_reporting_mode) + { + OutputReportMode mode = {}; + mode.ack = 1; + mode.mode = desired_reporting_mode; + QueueReport(mode, [this](ErrorCode error) { + if (error != ErrorCode::Success) + { + WARN_LOG_FMT(WIIMOTE, "WiiRemote: Failed to set reporting mode."); + return; + } + + m_reporting_mode = desired_reporting_mode; + + DEBUG_LOG_FMT(WIIMOTE, "WiiRemote: Set reporting mode."); + }); + + return; + } + + // Read accelerometer calibration. + if (!m_accel_calibration.has_value()) + { + static constexpr u16 ACCEL_CALIBRATION_ADDR = 0x16; + + ReadData(AddressSpace::EEPROM, 0, ACCEL_CALIBRATION_ADDR, sizeof(AccelCalibrationData), + [this](ReadResponse response) { + if (!response) + { + WARN_LOG_FMT(WIIMOTE, "WiiRemote: Failed to read accelerometer calibration."); + return; + } + + DEBUG_LOG_FMT(WIIMOTE, "WiiRemote: Read accelerometer calibration."); + + auto& calibration_data = *response; + + const AccelCalibrationData accel_calibration = + Common::BitCastPtr(calibration_data.data()); + m_accel_calibration = accel_calibration.GetCalibration(); + + WiimoteEmu::UpdateCalibrationDataChecksum(calibration_data, 1); + + // We could potentially try the second block at 0x26 if the checksum is bad. + if (accel_calibration.checksum != calibration_data.back()) + WARN_LOG_FMT(WIIMOTE, "WiiRemote: Bad accelerometer calibration checksum."); + }); + + return; + } + + if (!m_ir_state.IsFullyConfigured()) + { + ConfigureIRCamera(); + + return; + } + + if (!m_speaker_configured) + { + ConfigureSpeaker(); + + return; + } + + // Perform the following tasks only after M+ is settled. + if (IsWaitingForMotionPlus()) + return; + + // Read the "active" extension ID. (This also gives us the current M+ mode) + // This will fail on an un-intialized other extension. + // But extension initialization is the same as M+ de-activation so we must try this first. + if (m_extension_port == true && + (!IsMotionPlusStateKnown() || (!IsMotionPlusActive() && !m_extension_id.has_value()))) + { + static constexpr u16 ENCRYPTION_ADDR = 0xfb; + static constexpr u8 ENCRYPTION_VALUE = 0x00; + + // First disable encryption. Note this is a no-op when performed on the M+. + WriteData(AddressSpace::I2CBus, WiimoteEmu::ExtensionPort::REPORT_I2C_SLAVE, ENCRYPTION_ADDR, + {ENCRYPTION_VALUE}, [this](ErrorCode error) { + if (error != ErrorCode::Success) + return; + + ReadActiveExtensionID(); + }); + + return; + } + + static constexpr u16 INIT_ADDR = 0xf0; + static constexpr u8 INIT_VALUE = 0x55; + + // Initialize "active" extension if ID was not recognized. + // Note this is done before M+ setup to determine the required passthrough mode. + if (m_extension_id == ExtensionID::Unsupported) + { + // Note that this signal also DE-activates a M+. + WriteData(AddressSpace::I2CBus, WiimoteEmu::ExtensionPort::REPORT_I2C_SLAVE, INIT_ADDR, + {INIT_VALUE}, [this](ErrorCode result) { + DEBUG_LOG_FMT(WIIMOTE, "WiiRemote: Initialized extension: {}.", int(result)); + m_extension_id = std::nullopt; + }); + + return; + } + + // The following tasks require a known M+ state. + if (!IsMotionPlusStateKnown()) + return; + + // We now know the status of the M+. + // Updating it too frequently results off/on flashes on mode change. + m_mplus_attached_input = IsMotionPlusActive(); + + // Extension removal status is known here. Attachment status is updated after the ID is read. + if (m_extension_port != true) + m_extension_number_input = WiimoteEmu::ExtensionNumber::NONE; + + // Periodically try to initialize and activate an inactive M+. + if (!IsMotionPlusActive() && m_mplus_desired_mode.has_value() && + m_mplus_state.current_mode != m_mplus_desired_mode) + { + static constexpr u16 MPLUS_POLL_ADDR = WiimoteEmu::MotionPlus::PASSTHROUGH_MODE_OFFSET; + ReadData(AddressSpace::I2CBus, WiimoteEmu::MotionPlus::INACTIVE_DEVICE_ADDR, MPLUS_POLL_ADDR, 1, + [this](ReadResponse response) { + if (!response) + { + DEBUG_LOG_FMT(WIIMOTE, "WiiRemote: M+ poll failed."); + HandleMotionPlusNonResponse(); + return; + } + + WriteData(AddressSpace::I2CBus, WiimoteEmu::MotionPlus::INACTIVE_DEVICE_ADDR, + INIT_ADDR, {INIT_VALUE}, [this](ErrorCode result) { + DEBUG_LOG_FMT(WIIMOTE, "WiiRemote: M+ initialization: {}.", int(result)); + if (result != ErrorCode::Success) + { + HandleMotionPlusNonResponse(); + return; + } + + TriggerMotionPlusModeChange(); + }); + }); + + return; + } + + // Change active M+ passthrough mode. + if (IsMotionPlusActive() && m_mplus_desired_mode.has_value() && + m_mplus_state.current_mode != m_mplus_desired_mode) + { + TriggerMotionPlusModeChange(); + + return; + } + + // Read passthrough extension ID. + // This will also give us a desired M+ passthrough mode. + if (IsMotionPlusActive() && m_mplus_state.passthrough_port == true && !m_extension_id.has_value()) + { + // The M+ reads the passthrough ext ID and stores it at 0xf6,f8,f9. + static constexpr u16 PASSTHROUGH_EXT_ID_ADDR = 0xf6; + + ReadData(AddressSpace::I2CBus, WiimoteEmu::MotionPlus::ACTIVE_DEVICE_ADDR, + PASSTHROUGH_EXT_ID_ADDR, 4, [this](ReadResponse response) { + if (!response) + return; + + // Port status may have changed since the read was sent. + // In which case this data read would succeed but be useless. + if (m_mplus_state.passthrough_port != true) + return; + + auto& identifier = *response; + + ProcessExtensionID(identifier[2], identifier[0], identifier[3]); + }); + + return; + } + + // The following tasks require M+ configuration to be done. + if (!IsMotionPlusInDesiredMode()) + return; + + // Now that M+ config has settled we can update the extension number. + // Updating it too frequently results off/on flashes on M+ mode change. + UpdateExtensionNumberInput(); + + static constexpr u16 NORMAL_CALIBRATION_ADDR = 0x20; + + // Read M+ calibration. + if (IsMotionPlusActive() && !m_mplus_state.calibration.has_value()) + { + ReadData(AddressSpace::I2CBus, WiimoteEmu::MotionPlus::ACTIVE_DEVICE_ADDR, + NORMAL_CALIBRATION_ADDR, sizeof(WiimoteEmu::MotionPlus::CalibrationData), + [this](ReadResponse response) { + if (!response) + return; + + DEBUG_LOG_FMT(WIIMOTE, "WiiRemote: Read M+ calibration."); + + WiimoteEmu::MotionPlus::CalibrationData calibration = + Common::BitCastPtr(response->data()); + + const auto read_checksum = std::pair(calibration.crc32_lsb, calibration.crc32_msb); + + calibration.UpdateChecksum(); + + m_mplus_state.SetCalibrationData(calibration); + + if (read_checksum != std::pair(calibration.crc32_lsb, calibration.crc32_msb)) + { + // We could potentially try another read or call the M+ unusable. + WARN_LOG_FMT(WIIMOTE, "WiiRemote: Bad M+ calibration checksum."); + } + }); + + return; + } + + // Read normal extension calibration. + if ((m_extension_id == ExtensionID::Nunchuk && !m_nunchuk_state.calibration) || + (m_extension_id == ExtensionID::Classic && !m_classic_state.calibration)) + { + // Extension calibration is normally at 0x20 but M+ reads and stores it at 0x40. + static constexpr u16 PASSTHROUGH_CALIBRATION_ADDR = 0x40; + + const u16 calibration_addr = + IsMotionPlusActive() ? PASSTHROUGH_CALIBRATION_ADDR : NORMAL_CALIBRATION_ADDR; + static constexpr u16 CALIBRATION_SIZE = 0x10; + + ReadData( + AddressSpace::I2CBus, WiimoteEmu::ExtensionPort::REPORT_I2C_SLAVE, calibration_addr, + CALIBRATION_SIZE, [this](ReadResponse response) { + if (!response) + return; + + DEBUG_LOG_FMT(WIIMOTE, "WiiRemote: Read extension calibration."); + + auto& calibration_data = *response; + + const auto read_checksum = std::pair(calibration_data[CALIBRATION_SIZE - 2], + calibration_data[CALIBRATION_SIZE - 1]); + + WiimoteEmu::UpdateCalibrationDataChecksum(calibration_data, 2); + + Checksum checksum = Checksum::Good; + + if (read_checksum != std::pair(calibration_data[CALIBRATION_SIZE - 2], + calibration_data[CALIBRATION_SIZE - 1])) + { + // We could potentially try another block or call the extension unusable. + WARN_LOG_FMT(WIIMOTE, "WiiRemote: Bad extension calibration checksum."); + checksum = Checksum::Bad; + } + + if (m_extension_id == ExtensionID::Nunchuk) + { + m_nunchuk_state.SetCalibrationData( + Common::BitCastPtr(calibration_data.data()), + checksum); + } + else if (m_extension_id == ExtensionID::Classic) + { + m_classic_state.SetCalibrationData( + Common::BitCastPtr(calibration_data.data()), + checksum); + } + }); + + return; + } +} + +void Device::HandleMotionPlusNonResponse() +{ + // No need for additional checks if an extension is attached. + // (not possible for M+ to become attached) + if (m_extension_port == true) + m_mplus_desired_mode = MotionPlusState::PassthroughMode{}; + else + WaitForMotionPlus(); +} + +// Produce LED bitmask for remotes. +// Remotes 1-4 are normal. Additional remotes LED labels will add up to their assigned ID. +u8 Device::GetDesiredLEDValue() const +{ + const auto index = GetId(); + + // Normal LED behavior for remotes 1-4. + if (index < 4) + return 1 << index; + + // Light LED 4 and LEDs 1 through 3 for remotes 5-7. (Add up the numbers on the remote) + if (index < 7) + return 1 << (index - 4) | 8; + + // Light LED 4+3 and LEDs 1 or 2 for remotes 8 or 9. (Add up the numbers on the remote) + if (index < 9) + return 1 << (index - 7) | 8 | 4; + + // For remotes 10 and up just light all LEDs. + return 0xf; +} + +void Device::UpdateExtensionNumberInput() +{ + switch (m_extension_id.value_or(ExtensionID::Unsupported)) + { + case ExtensionID::Nunchuk: + m_extension_number_input = WiimoteEmu::ExtensionNumber::NUNCHUK; + break; + case ExtensionID::Classic: + m_extension_number_input = WiimoteEmu::ExtensionNumber::CLASSIC; + break; + case ExtensionID::Unsupported: + default: + m_extension_number_input = WiimoteEmu::ExtensionNumber::NONE; + break; + } +} + +void Device::ProcessExtensionEvent(bool connected) +{ + // Reset extension state. + m_nunchuk_state = {}; + m_classic_state = {}; + + m_extension_id = std::nullopt; + + // We won't know the desired mode until we get the extension ID. + if (connected) + m_mplus_desired_mode = std::nullopt; +} + +void Device::ProcessExtensionID(u8 id_0, u8 id_4, u8 id_5) +{ + if (id_4 == 0x00 && id_5 == 0x00) + { + INFO_LOG_FMT(WIIMOTE, "WiiRemote: Nunchuk is attached."); + m_extension_id = ExtensionID::Nunchuk; + + m_mplus_desired_mode = MotionPlusState::PassthroughMode::Nunchuk; + } + else if (id_4 == 0x01 && id_5 == 0x01) + { + INFO_LOG_FMT(WIIMOTE, "WiiRemote: Classic Controller is attached."); + m_extension_id = ExtensionID::Classic; + + m_mplus_desired_mode = MotionPlusState::PassthroughMode::Classic; + } + else + { + // This is a normal occurance before extension initialization. + DEBUG_LOG_FMT(WIIMOTE, "WiiRemote: Unknown extension: {} {} {}.", id_0, id_4, id_5); + m_extension_id = ExtensionID::Unsupported; + } +} + +void Device::MotionPlusState::SetCalibrationData( + const WiimoteEmu::MotionPlus::CalibrationData& data) +{ + DEBUG_LOG_FMT(WIIMOTE, "WiiRemote: Set M+ calibration."); + + calibration.emplace(); + + calibration->fast = data.fast; + calibration->slow = data.slow; +} + +Device::NunchukState::Calibration::Calibration() : accel{}, stick{} +{ + accel.zero.data.fill(1 << (accel.BITS_OF_PRECISION - 1)); + // Approximate 1G value per WiiBrew: + accel.max.data.fill(740); + + stick.zero.data.fill(1 << (stick.BITS_OF_PRECISION - 1)); + stick.max.data.fill((1 << stick.BITS_OF_PRECISION) - 1); +} + +void Device::NunchukState::SetCalibrationData(const WiimoteEmu::Nunchuk::CalibrationData& data, + Checksum checksum) +{ + DEBUG_LOG_FMT(WIIMOTE, "WiiRemote: Set Nunchuk calibration."); + + calibration.emplace(); + + if (checksum == Checksum::Bad) + return; + + // Genuine Nunchuks have been observed with "min" and "max" values of zero. + // We catch that here and fall back to "full range" calibration. + const auto stick_calibration = data.GetStick(); + if (stick_calibration.IsSane()) + { + calibration->stick = stick_calibration; + } + else + { + WARN_LOG_FMT(WIIMOTE, + "WiiRemote: Nunchuk stick calibration is not sane. Using fallback values."); + } + + // No known reports of bad accelerometer calibration but we'll handle it just in case. + const auto accel_calibration = data.GetAccel(); + if (accel_calibration.IsSane()) + { + calibration->accel = accel_calibration; + } + else + { + WARN_LOG_FMT(WIIMOTE, + "WiiRemote: Nunchuk accel calibration is not sane. Using fallback values."); + } +} + +Device::ClassicState::Calibration::Calibration() + : left_stick{}, right_stick{}, left_trigger{}, right_trigger{} +{ + left_stick.zero.data.fill(1 << (left_stick.BITS_OF_PRECISION - 1)); + left_stick.max.data.fill((1 << left_stick.BITS_OF_PRECISION) - 1); + + right_stick.zero.data.fill(1 << (right_stick.BITS_OF_PRECISION - 1)); + right_stick.max.data.fill((1 << right_stick.BITS_OF_PRECISION) - 1); + + left_trigger.max = (1 << left_trigger.BITS_OF_PRECISION) - 1; + right_trigger.max = (1 << right_trigger.BITS_OF_PRECISION) - 1; +} + +void Device::ClassicState::SetCalibrationData(const WiimoteEmu::Classic::CalibrationData& data, + Checksum checksum) +{ + DEBUG_LOG_FMT(WIIMOTE, "WiiRemote: Set Classic Controller calibration."); + + calibration.emplace(); + + if (checksum == Checksum::Bad) + return; + + const auto left_stick_calibration = data.GetLeftStick(); + if (left_stick_calibration.IsSane()) + { + calibration->left_stick = left_stick_calibration; + } + else + { + WARN_LOG_FMT(WIIMOTE, + "WiiRemote: CC left stick calibration is not sane. Using fallback values."); + } + + const auto right_stick_calibration = data.GetRightStick(); + if (right_stick_calibration.IsSane()) + { + calibration->right_stick = right_stick_calibration; + } + else + { + WARN_LOG_FMT(WIIMOTE, + "WiiRemote: CC right stick calibration is not sane. Using fallback values."); + } + calibration->left_trigger = data.GetLeftTrigger(); + calibration->right_trigger = data.GetRightTrigger(); +} + +void Device::ReadActiveExtensionID() +{ + static constexpr u16 EXT_ID_ADDR = 0xfa; + static constexpr u16 EXT_ID_SIZE = 6; + + ReadData(AddressSpace::I2CBus, WiimoteEmu::ExtensionPort::REPORT_I2C_SLAVE, EXT_ID_ADDR, + EXT_ID_SIZE, [this](ReadResponse response) { + if (!response) + return; + + auto& identifier = *response; + + // Check for M+ ID. + if (identifier[5] == 0x05) + { + const auto passthrough_mode = MotionPlusState::PassthroughMode(identifier[4]); + + m_mplus_state.current_mode = passthrough_mode; + + INFO_LOG_FMT(WIIMOTE, "WiiRemote: M+ is active in mode: {}.", int(passthrough_mode)); + } + else + { + m_mplus_state.current_mode = MotionPlusState::PassthroughMode{}; + + ProcessExtensionID(identifier[0], identifier[4], identifier[5]); + } + }); +} + +bool Device::IRState::IsFullyConfigured() const +{ + return enabled && mode_set && current_sensitivity == GetDesiredIRSensitivity(); +} + +u32 Device::IRState::GetDesiredIRSensitivity() +{ + // Wii stores values from 1 to 5. (subtract 1) + const u32 configured_level = Config::Get(Config::SYSCONF_SENSOR_BAR_SENSITIVITY) - 1; + + if (configured_level < IR_SENSITIVITY_LEVEL_COUNT) + return configured_level; + + // Default to middle level on bad value. + return 2; +} + +void Device::SetIRSensitivity(u32 level) +{ + struct IRSensitivityConfig + { + std::array block1; + std::array block2; + }; + + // Data for Wii levels 1 to 5. + static constexpr std::array sensitivity_configs = + {{ + {{0x02, 0x00, 0x00, 0x71, 0x01, 0x00, 0x64, 0x00, 0xfe}, {0xfd, 0x05}}, + {{0x02, 0x00, 0x00, 0x71, 0x01, 0x00, 0x96, 0x00, 0xb4}, {0xb3, 0x04}}, + {{0x02, 0x00, 0x00, 0x71, 0x01, 0x00, 0xaa, 0x00, 0x64}, {0x63, 0x03}}, + {{0x02, 0x00, 0x00, 0x71, 0x01, 0x00, 0xc8, 0x00, 0x36}, {0x35, 0x03}}, + {{0x07, 0x00, 0x00, 0x71, 0x01, 0x00, 0x72, 0x00, 0x20}, {0x1f, 0x03}}, + }}; + + static constexpr u16 BLOCK1_ADDR = 0x00; + static constexpr u16 BLOCK2_ADDR = 0x1a; + + DEBUG_LOG_FMT(WIIMOTE, "WiiRemote: Setting IR sensitivity: {}.", level + 1); + + const auto& sensitivity_config = sensitivity_configs[level]; + + WriteData(AddressSpace::I2CBus, WiimoteEmu::CameraLogic::I2C_ADDR, BLOCK1_ADDR, + sensitivity_config.block1, [&sensitivity_config, level, this](ErrorCode block_result) { + if (block_result != ErrorCode::Success) + { + WARN_LOG_FMT(WIIMOTE, "WiiRemote: Failed to write IR block 1."); + return; + } + + WriteData(AddressSpace::I2CBus, WiimoteEmu::CameraLogic::I2C_ADDR, BLOCK2_ADDR, + sensitivity_config.block2, [&, level, this](ErrorCode block2_result) { + if (block2_result != ErrorCode::Success) + { + WARN_LOG_FMT(WIIMOTE, "WiiRemote: Failed to write IR block 2."); + return; + } + + DEBUG_LOG_FMT(WIIMOTE, "WiiRemote: IR sensitivity set."); + + m_ir_state.current_sensitivity = level; + }); + }); +} + +void Device::ConfigureIRCamera() +{ + if (!m_ir_state.enabled) + { + OutputReportIRLogicEnable2 ir_logic2 = {}; + ir_logic2.ack = 1; + ir_logic2.enable = 1; + QueueReport(ir_logic2, [this](ErrorCode result) { + if (result != ErrorCode::Success) + { + WARN_LOG_FMT(WIIMOTE, "WiiRemote: Failed to enable IR."); + return; + } + + OutputReportIRLogicEnable ir_logic = {}; + ir_logic.ack = 1; + ir_logic.enable = 1; + QueueReport(ir_logic, [this](ErrorCode ir_result) { + if (ir_result != ErrorCode::Success) + { + WARN_LOG_FMT(WIIMOTE, "WiiRemote: Failed to enable IR."); + return; + } + + DEBUG_LOG_FMT(WIIMOTE, "WiiRemote: IR enabled."); + + m_ir_state.enabled = true; + }); + }); + + return; + } + + if (const u32 desired_level = IRState::GetDesiredIRSensitivity(); + desired_level != m_ir_state.current_sensitivity) + { + SetIRSensitivity(desired_level); + + return; + } + + if (!m_ir_state.mode_set) + { + static constexpr u16 MODE_ADDR = 0x33; + + // We only support "Basic" mode (it's all that fits in ReportCoreAccelIR10Ext6). + WriteData(AddressSpace::I2CBus, WiimoteEmu::CameraLogic::I2C_ADDR, MODE_ADDR, + {WiimoteEmu::CameraLogic::IR_MODE_BASIC}, [this](ErrorCode mode_result) { + if (mode_result != ErrorCode::Success) + { + WARN_LOG_FMT(WIIMOTE, "WiiRemote: Failed to set IR mode."); + return; + } + + // This seems to enable object tracking. + static constexpr u16 ENABLE_ADDR = 0x30; + static constexpr u8 ENABLE_VALUE = 0x08; + + WriteData(AddressSpace::I2CBus, WiimoteEmu::CameraLogic::I2C_ADDR, ENABLE_ADDR, + {ENABLE_VALUE}, [this](ErrorCode result) { + if (result != ErrorCode::Success) + { + WARN_LOG_FMT(WIIMOTE, "WiiRemote: Failed to enable object tracking."); + return; + } + + DEBUG_LOG_FMT(WIIMOTE, "WiiRemote: IR mode set."); + + m_ir_state.mode_set = true; + }); + }); + } +} + +void Device::ConfigureSpeaker() +{ + OutputReportSpeakerMute mute = {}; + mute.enable = 1; + mute.ack = 1; + QueueReport(mute, [this](ErrorCode mute_result) { + if (mute_result != ErrorCode::Success) + { + WARN_LOG_FMT(WIIMOTE, "WiiRemote: Failed to mute speaker."); + return; + } + + OutputReportSpeakerEnable spkr = {}; + spkr.enable = 0; + spkr.ack = 1; + QueueReport(spkr, [this](ErrorCode enable_result) { + if (enable_result != ErrorCode::Success) + { + WARN_LOG_FMT(WIIMOTE, "WiiRemote: Failed to disable speaker."); + return; + } + + DEBUG_LOG_FMT(WIIMOTE, "WiiRemote: Speaker muted and disabled."); + + m_speaker_configured = true; + }); + }); +} + +void Device::TriggerMotionPlusModeChange() +{ + if (!m_mplus_desired_mode.has_value()) + return; + + const u8 passthrough_mode = u8(*m_mplus_desired_mode); + + const u8 device_addr = IsMotionPlusActive() ? WiimoteEmu::MotionPlus::ACTIVE_DEVICE_ADDR : + WiimoteEmu::MotionPlus::INACTIVE_DEVICE_ADDR; + + WriteData(AddressSpace::I2CBus, device_addr, WiimoteEmu::MotionPlus::PASSTHROUGH_MODE_OFFSET, + {passthrough_mode}, [this](ErrorCode activation_result) { + DEBUG_LOG_FMT(WIIMOTE, "WiiRemote: M+ activation: {}.", int(activation_result)); + + WaitForMotionPlus(); + + // Normally M+ will be seen performing a reset here. (extension port events) + // But sometimes (rarely) M+ activation does not cause an extension port event. + // We'll consider the mode unknown. It will be read back after some time. + m_mplus_state.current_mode = std::nullopt; + }); +} + +void Device::TriggerMotionPlusCalibration() +{ + static constexpr u16 CALIBRATION_TRIGGER_ADDR = 0xf2; + static constexpr u8 CALIBRATION_TRIGGER_VALUE = 0x00; + + // This triggers a hardware "zero" calibration. + // The effect is notiecable but output still strays from calibration data. + // It seems we're better off just manually determining "zero". + WriteData(AddressSpace::I2CBus, WiimoteEmu::MotionPlus::ACTIVE_DEVICE_ADDR, + CALIBRATION_TRIGGER_ADDR, {CALIBRATION_TRIGGER_VALUE}, [](ErrorCode result) { + DEBUG_LOG_FMT(WIIMOTE, "WiiRemote: M+ calibration trigger done: {}.", int(result)); + }); +} + +bool Device::IsMotionPlusStateKnown() const +{ + return m_mplus_state.current_mode.has_value(); +} + +bool Device::IsMotionPlusActive() const +{ + return m_mplus_state.current_mode != MotionPlusState::PassthroughMode{}; +} + +bool Device::IsMotionPlusInDesiredMode() const +{ + return m_mplus_state.current_mode.has_value() && + (m_mplus_state.current_mode == m_mplus_desired_mode); +} + +void Device::ProcessInputReport(WiimoteReal::Report& report) +{ + if (report.size() < WiimoteReal::REPORT_HID_HEADER_SIZE) + { + WARN_LOG_FMT(WIIMOTE, "WiiRemote: Bad report size."); + return; + } + + auto report_id = InputReportID(report[WiimoteReal::REPORT_HID_HEADER_SIZE]); + + for (auto it = m_report_handlers.begin(); true;) + { + if (it == m_report_handlers.end()) + { + if (report_id == InputReportID::Status) + { + if (report.size() - WiimoteReal::REPORT_HID_HEADER_SIZE < + sizeof(TypedInputData)) + { + WARN_LOG_FMT(WIIMOTE, "WiiRemote: Bad report size."); + } + else + { + ProcessStatusReport(Common::BitCastPtr(report.data() + 2)); + } + } + else if (report_id < InputReportID::ReportCore) + { + WARN_LOG_FMT(WIIMOTE, "WiiRemote: Unhandled input report: {}.", + ArrayToString(report.data(), u32(report.size()))); + } + + break; + } + + if (it->IsExpired()) + { + WARN_LOG_FMT(WIIMOTE, "WiiRemote: Removing expired handler."); + it = m_report_handlers.erase(it); + continue; + } + + if (const auto result = it->TryToHandleReport(report); + result == ReportHandler::HandlerResult::Handled) + { + it = m_report_handlers.erase(it); + break; + } + + ++it; + } + + if (report_id < InputReportID::ReportCore) + { + // Normal input reports can be processed as "ReportCore". + report_id = InputReportID::ReportCore; + } + else + { + // We can assume the last received input report is the current reporting mode. + // FYI: This logic fails to properly handle the (never used) "interleaved" reports. + m_reporting_mode = InputReportID(report_id); + } + + auto manipulator = MakeDataReportManipulator( + report_id, report.data() + WiimoteReal::REPORT_HID_HEADER_SIZE + sizeof(InputReportID)); + + if (manipulator->GetDataSize() + WiimoteReal::REPORT_HID_HEADER_SIZE > report.size()) + { + WARN_LOG_FMT(WIIMOTE, "WiiRemote: Bad report size."); + return; + } + + // Read buttons. + manipulator->GetCoreData(&m_core_data); + + // Process accel data. + if (manipulator->HasAccel() && m_accel_calibration.has_value()) + { + // FYI: This logic fails to properly handle the (never used) "interleaved" reports. + AccelData accel_data = {}; + manipulator->GetAccelData(&accel_data); + + m_accel_data = + accel_data.GetNormalizedValue(*m_accel_calibration) * float(MathUtil::GRAVITY_ACCELERATION); + } + + // Process IR data. + if (manipulator->HasIR() && m_ir_state.IsFullyConfigured()) + { + m_ir_state.ProcessData( + Common::BitCastPtr>(manipulator->GetIRDataPtr())); + } + + // Process extension data. + if (IsMotionPlusStateKnown()) + { + const auto ext_data = manipulator->GetExtDataPtr(); + const auto ext_size = manipulator->GetExtDataSize(); + + if (IsMotionPlusActive()) + ProcessMotionPlusExtensionData(ext_data, ext_size); + else + ProcessNormalExtensionData(ext_data, ext_size); + } + + UpdateOrientation(); +} + +void Device::UpdateOrientation() +{ + const auto current_report_time = Clock::now(); + const auto elapsed_time = std::chrono::duration_cast>( + current_report_time - m_last_report_time); + m_last_report_time = current_report_time; + + // Apply M+ gyro data to our orientation. + m_orientation = + WiimoteEmu::GetRotationFromGyroscope(m_mplus_state.gyro_data * -1 * elapsed_time.count()) * + m_orientation; + + // When M+ data is not available give accel/ir data more weight. + // ComplementaryFilter will then just smooth out our data a bit. + const bool is_mplus_active = IsMotionPlusStateKnown() && IsMotionPlusActive(); + + // With non-zero acceleration data we can perform pitch and roll correction. + if (m_accel_data.LengthSquared()) + { + const auto accel_weight = is_mplus_active ? 0.04 : 0.5f; + + m_orientation = WiimoteEmu::ComplementaryFilter(m_orientation, m_accel_data, accel_weight); + } + + // If IR objects are visible we can perform yaw and pitch correction. + if (!m_ir_state.is_hidden) + { + // FYI: We could do some roll correction from multiple IR objects. + + const auto ir_rotation = + Common::Vec3(m_ir_state.center_position.y * WiimoteEmu::CameraLogic::CAMERA_FOV_Y, 0, + m_ir_state.center_position.x * WiimoteEmu::CameraLogic::CAMERA_FOV_X) / + 2; + const auto ir_normal = Common::Vec3(0, 1, 0); + const auto ir_vector = WiimoteEmu::GetRotationFromGyroscope(-ir_rotation) * ir_normal; + + // Pitch correction will be slightly wrong based on sensorbar height. + // Keep weight below accelerometer weight for that reason. + // Correction will only happen near pitch zero when the sensorbar is actually in view. + const auto ir_weight = is_mplus_active ? 0.035 : 0.45f; + + m_orientation = WiimoteEmu::ComplementaryFilter(m_orientation, ir_vector, ir_weight, ir_normal); + } + + // Normalize for floating point inaccuracies. + m_orientation = m_orientation.Normalized(); + + // Update our (pitch, roll, yaw) inputs now that orientation has been adjusted. + m_rotation_inputs = + Common::Vec3{WiimoteEmu::GetPitch(m_orientation), WiimoteEmu::GetRoll(m_orientation), + WiimoteEmu::GetYaw(m_orientation)} / + float(MathUtil::PI); +} + +void Device::IRState::ProcessData(const std::array& data) +{ + // A better implementation might extrapolate points when they fall out of camera view. + // But just averaging visible points actually seems to work very well. + + using IRObject = WiimoteEmu::IRBasic::IRObject; + + MathUtil::RunningVariance points; + + const auto camera_max = IRObject(WiimoteEmu::CameraLogic::CAMERA_RES_X - 1, + WiimoteEmu::CameraLogic::CAMERA_RES_Y - 1); + + const auto add_point = [&](IRObject point) { + // Non-visible points are 0xFF-filled. + if (point.y > camera_max.y) + return; + + points.Push(Common::Vec2(point)); + }; + + for (auto& block : data) + { + add_point(block.GetObject1()); + add_point(block.GetObject2()); + } + + is_hidden = !points.Count(); + + if (points.Count() >= 2) + { + const auto variance = points.PopulationVariance(); + // Adjusts Y coorinate to match horizontal FOV. + const auto separation = + Common::Vec2(std::sqrt(variance.x), std::sqrt(variance.y)) / + Common::Vec2(WiimoteEmu::CameraLogic::CAMERA_RES_X, + WiimoteEmu::CameraLogic::CAMERA_RES_Y * WiimoteEmu::CameraLogic::CAMERA_AR) * + 2; + + distance = WiimoteEmu::CameraLogic::SENSOR_BAR_LED_SEPARATION / separation.Length() / 2 / + std::tan(WiimoteEmu::CameraLogic::CAMERA_FOV_X / 2); + } + + if (points.Count()) + { + center_position = points.Mean() / Common::Vec2(camera_max) * 2.f - Common::Vec2(1, 1); + } + else + { + center_position = {}; + } +} + +void Device::ProcessMotionPlusExtensionData(const u8* ext_data, u32 ext_size) +{ + if (ext_size < sizeof(WiimoteEmu::MotionPlus::DataFormat)) + return; + + const WiimoteEmu::MotionPlus::DataFormat mplus_data = + Common::BitCastPtr(ext_data); + + const bool is_ext_connected = mplus_data.extension_connected; + + // Handle passthrough extension change. + if (is_ext_connected != m_mplus_state.passthrough_port) + { + m_mplus_state.passthrough_port = is_ext_connected; + + DEBUG_LOG_FMT(WIIMOTE, "WiiRemote: M+ passthrough port event: {}.", is_ext_connected); + + // With no passthrough extension we'll be happy with the current mode. + if (!is_ext_connected) + m_mplus_desired_mode = m_mplus_state.current_mode; + + ProcessExtensionEvent(is_ext_connected); + } + + if (mplus_data.is_mp_data) + { + m_mplus_state.ProcessData(mplus_data); + return; + } + + if (!IsMotionPlusInDesiredMode()) + { + DEBUG_LOG_FMT(WIIMOTE, "WiiRemote: Ignoring unwanted passthrough data."); + return; + } + + std::array data; + std::copy_n(ext_data, ext_size, data.begin()); + + // Undo bit-hacks of M+ passthrough. + WiimoteEmu::MotionPlus::ReversePassthroughModifications(*m_mplus_state.current_mode, data.data()); + + ProcessNormalExtensionData(data.data(), u32(data.size())); +} + +void Device::ProcessNormalExtensionData(const u8* ext_data, u32 ext_size) +{ + if (m_extension_id == ExtensionID::Nunchuk) + { + if (ext_size < sizeof(WiimoteEmu::MotionPlus::DataFormat)) + return; + + const WiimoteEmu::Nunchuk::DataFormat nunchuk_data = + Common::BitCastPtr(ext_data); + + m_nunchuk_state.ProcessData(nunchuk_data); + } + else if (m_extension_id == ExtensionID::Classic) + { + if (ext_size < sizeof(WiimoteEmu::Classic::DataFormat)) + return; + + const WiimoteEmu::Classic::DataFormat cc_data = + Common::BitCastPtr(ext_data); + + m_classic_state.ProcessData(cc_data); + } +} + +void Device::UpdateRumble() +{ + static constexpr auto rumble_period = std::chrono::milliseconds(100); + + const auto on_time = std::chrono::duration_cast(rumble_period * m_rumble_level); + const auto off_time = rumble_period - on_time; + + const auto now = Clock::now(); + + if (m_rumble && (now < m_last_rumble_change + on_time || !off_time.count())) + return; + + if (!m_rumble && (now < m_last_rumble_change + off_time || !on_time.count())) + return; + + m_last_rumble_change = now; + m_rumble ^= true; + + // Rumble flag will be set within QueueReport. + QueueReport(OutputReportRumble{}); +} + +void Device::UpdateInput() +{ + if (!m_wiimote->IsConnected()) + { + g_controller_interface.RemoveDevice( + [this](const Core::Device* device) { return device == this; }); + return; + } + + UpdateRumble(); + RunTasks(); + + WiimoteReal::Report report; + while (m_wiimote->GetNextReport(&report)) + { + ProcessInputReport(report); + RunTasks(); + } +} + +void Device::MotionPlusState::ProcessData(const WiimoteEmu::MotionPlus::DataFormat& data) +{ + // We need the calibration block read to know the sensor orientations. + if (!calibration.has_value()) + return; + + gyro_data = data.GetData().GetAngularVelocity(*calibration); +} + +bool Device::IsWaitingForMotionPlus() const +{ + return Clock::now() < m_mplus_wait_time; +} + +void Device::WaitForMotionPlus() +{ + DEBUG_LOG_FMT(WIIMOTE, "WiiRemote: Wait for M+."); + m_mplus_wait_time = Clock::now() + std::chrono::seconds{2}; +} + +void Device::NunchukState::ProcessData(const WiimoteEmu::Nunchuk::DataFormat& data) +{ + buttons = data.GetButtons(); + + // Stick/accel require calibration data. + if (!calibration.has_value()) + return; + + stick = data.GetStick().GetNormalizedValue(calibration->stick); + accel = data.GetAccel().GetNormalizedValue(calibration->accel) * + float(MathUtil::GRAVITY_ACCELERATION); +} + +void Device::ClassicState::ProcessData(const WiimoteEmu::Classic::DataFormat& data) +{ + buttons = data.GetButtons(); + + // Sticks/triggers require calibration data. + if (!calibration.has_value()) + return; + + sticks[0] = data.GetLeftStick().GetNormalizedValue(calibration->left_stick); + sticks[1] = data.GetRightStick().GetNormalizedValue(calibration->right_stick); + triggers[0] = data.GetLeftTrigger().GetNormalizedValue(calibration->left_trigger); + triggers[1] = data.GetRightTrigger().GetNormalizedValue(calibration->right_trigger); +} + +void Device::ReadData(AddressSpace space, u8 slave, u16 address, u16 size, + std::function callback) +{ + OutputReportReadData read_data{}; + read_data.space = u8(space); + read_data.slave_address = slave; + read_data.address[0] = u8(address >> 8); + read_data.address[1] = u8(address); + read_data.size[0] = u8(size >> 8); + read_data.size[1] = u8(size); + QueueReport(read_data); + + AddReadDataReplyHandler(space, slave, address, size, {}, std::move(callback)); +} + +void Device::AddReadDataReplyHandler(AddressSpace space, u8 slave, u16 address, u16 size, + std::vector starting_data, + std::function callback) +{ + // Data read may return a busy ack. + auto ack_handler = MakeAckHandler(OutputReportID::ReadData, [callback](ErrorCode result) { + DEBUG_LOG_FMT(WIIMOTE, "WiiRemote: Read ack error: {}.", int(result)); + callback(ReadResponse{}); + }); + + // Or more normally a "ReadDataReply". + auto read_handler = [this, space, slave, address, size, data = std::move(starting_data), + callback = + std::move(callback)](const InputReportReadDataReply& reply) mutable { + if (Common::swap16(reply.address) != address) + return ReportHandler::HandlerResult::NotHandled; + + if (reply.error != u8(ErrorCode::Success)) + { + DEBUG_LOG_FMT(WIIMOTE, "WiiRemote: Read reply error: {}.", int(reply.error)); + callback(ReadResponse{}); + + return ReportHandler::HandlerResult::Handled; + } + + const auto read_count = reply.size_minus_one + 1; + + data.insert(data.end(), reply.data, reply.data + read_count); + + if (read_count < size) + { + // We have more data to acquire. + AddReadDataReplyHandler(space, slave, address + read_count, size - read_count, + std::move(data), std::move(callback)); + } + else + { + // We have all the data. + callback(std::move(data)); + } + + return ReportHandler::HandlerResult::Handled; + }; + + AddReportHandler( + std::function( + std::move(read_handler)), + std::move(ack_handler)); +} + +template +void Device::WriteData(AddressSpace space, u8 slave, u16 address, T&& data, C&& callback) +{ + OutputReportWriteData write_data = {}; + write_data.space = u8(space); + write_data.slave_address = slave; + write_data.address[0] = u8(address >> 8); + write_data.address[1] = u8(address); + + static constexpr auto MAX_DATA_SIZE = std::size(write_data.data); + write_data.size = u8(std::min(std::size(data), MAX_DATA_SIZE)); + + std::copy_n(std::begin(data), write_data.size, write_data.data); + + // Writes of more than 16 bytes must be split into multiple reports. + if (std::size(data) > MAX_DATA_SIZE) + { + auto next_write = [this, space, slave, address, + additional_data = + std::vector(std::begin(data) + MAX_DATA_SIZE, std::end(data)), + callback = std::forward(callback)](ErrorCode result) mutable { + if (result != ErrorCode::Success) + callback(result); + else + WriteData(space, slave, address + MAX_DATA_SIZE, additional_data, std::move(callback)); + }; + + QueueReport(write_data, std::move(next_write)); + } + else + { + QueueReport(write_data, std::forward(callback)); + } +} + +Device::ReportHandler::ReportHandler(Clock::time_point expired_time) : m_expired_time(expired_time) +{ +} + +template +void Device::AddReportHandler(T&&... callbacks) +{ + auto& handler = m_report_handlers.emplace_back(Clock::now() + std::chrono::seconds{5}); + (handler.AddHandler(std::forward(callbacks)), ...); +} + +template +void Device::ReportHandler::AddHandler(std::function handler) +{ + m_callbacks.emplace_back([handler = std::move(handler)](const WiimoteReal::Report& report) { + if (report[WiimoteReal::REPORT_HID_HEADER_SIZE] != u8(T::REPORT_ID)) + return ReportHandler::HandlerResult::NotHandled; + + T data; + + if (report.size() < sizeof(T) + WiimoteReal::REPORT_HID_HEADER_SIZE + 1) + { + // Off-brand "NEW 2in1" Wii Remote likes to shorten read data replies. + WARN_LOG_FMT(WIIMOTE, "WiiRemote: Bad report size ({}) for report {:#x}. Zero-filling.", + report.size(), int(T::REPORT_ID)); + + data = {}; + std::memcpy(&data, report.data() + WiimoteReal::REPORT_HID_HEADER_SIZE + 1, + report.size() - WiimoteReal::REPORT_HID_HEADER_SIZE + 1); + } + else + { + data = Common::BitCastPtr(report.data() + WiimoteReal::REPORT_HID_HEADER_SIZE + 1); + } + + if constexpr (std::is_same_v) + { + handler(data); + return ReportHandler::HandlerResult::Handled; + } + else + { + return handler(data); + } + }); +} + +auto Device::ReportHandler::TryToHandleReport(const WiimoteReal::Report& report) -> HandlerResult +{ + for (auto& callback : m_callbacks) + { + if (const auto result = callback(report); result != HandlerResult::NotHandled) + return result; + } + + return HandlerResult::NotHandled; +} + +bool Device::ReportHandler::IsExpired() const +{ + return Clock::now() >= m_expired_time; +} + +auto Device::MakeAckHandler(OutputReportID report_id, + std::function callback) + -> AckReportHandler +{ + return [report_id, callback = std::move(callback)](const InputReportAck& reply) { + if (reply.rpt_id != report_id) + return ReportHandler::HandlerResult::NotHandled; + + callback(reply.error_code); + return ReportHandler::HandlerResult::Handled; + }; +} + +bool Device::IsPerformingTask() const +{ + return !m_report_handlers.empty(); +} + +void Device::ProcessStatusReport(const InputReportStatus& status) +{ + // Update status periodically to keep battery level value up to date. + m_status_outdated_time = Clock::now() + std::chrono::seconds(10); + + m_battery = status.GetEstimatedCharge() * BATTERY_INPUT_MAX_VALUE; + m_leds = status.leds; + + if (!status.ir) + m_ir_state = {}; + + const bool is_ext_connected = status.extension; + + // Handle extension port state change. + if (is_ext_connected != m_extension_port) + { + DEBUG_LOG_FMT(WIIMOTE, "WiiRemote: Extension port event: {}.", is_ext_connected); + + m_extension_port = is_ext_connected; + + // Data reporting stops on an extension port event. + m_reporting_mode = InputReportID::ReportDisabled; + + ProcessExtensionEvent(is_ext_connected); + + // The M+ is now in an unknown state. + m_mplus_state = {}; + + if (is_ext_connected) + { + // We can assume the M+ is settled on an attachment event. + m_mplus_wait_time = Clock::now(); + } + else + { + // "Nunchuk" will be the most used mode and also works with no passthrough extension. + m_mplus_desired_mode = MotionPlusState::PassthroughMode::Nunchuk; + + // If an extension is not connected the M+ is either disabled or resetting. + m_mplus_state.current_mode = MotionPlusState::PassthroughMode{}; + } + } +} + +} // namespace ciface::WiimoteController diff --git a/Source/Core/InputCommon/ControllerInterface/Wiimote/WiimoteController.h b/Source/Core/InputCommon/ControllerInterface/Wiimote/WiimoteController.h new file mode 100644 index 0000000000..53abc0bf3f --- /dev/null +++ b/Source/Core/InputCommon/ControllerInterface/Wiimote/WiimoteController.h @@ -0,0 +1,282 @@ +// Copyright 2020 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include + +#include "Core/HW/WiimoteCommon/DataReport.h" +#include "Core/HW/WiimoteCommon/WiimoteReport.h" +#include "Core/HW/WiimoteEmu/Camera.h" +#include "Core/HW/WiimoteEmu/Extension/Classic.h" +#include "Core/HW/WiimoteEmu/Extension/Nunchuk.h" +#include "Core/HW/WiimoteEmu/MotionPlus.h" +#include "Core/HW/WiimoteReal/WiimoteReal.h" +#include "InputCommon/ControllerInterface/Device.h" + +namespace ciface::WiimoteController +{ +using namespace WiimoteCommon; + +void AddDevice(std::unique_ptr); +void ReleaseDevices(std::optional count = std::nullopt); + +class Device final : public Core::Device +{ +public: + Device(std::unique_ptr wiimote); + ~Device(); + + std::string GetName() const override; + std::string GetSource() const override; + + void UpdateInput() override; + +private: + using Clock = std::chrono::steady_clock; + + enum class ExtensionID + { + Nunchuk, + Classic, + Unsupported, + }; + + enum class Checksum + { + Good, + Bad, + }; + + class MotionPlusState + { + public: + void SetCalibrationData(const WiimoteEmu::MotionPlus::CalibrationData&); + void ProcessData(const WiimoteEmu::MotionPlus::DataFormat&); + + using PassthroughMode = WiimoteEmu::MotionPlus::PassthroughMode; + + // State is unknown by default. + std::optional current_mode; + + // The last known state of the passthrough port flag. + // Used to detect passthrough extension port events. + std::optional passthrough_port; + + Common::Vec3 gyro_data = {}; + + std::optional calibration; + }; + + struct NunchukState + { + using CalibrationData = WiimoteEmu::Nunchuk::CalibrationData; + + void SetCalibrationData(const CalibrationData&, Checksum); + void ProcessData(const WiimoteEmu::Nunchuk::DataFormat&); + + Common::Vec2 stick = {}; + Common::Vec3 accel = {}; + + u8 buttons = 0; + + struct Calibration + { + Calibration(); + + CalibrationData::AccelCalibration accel; + CalibrationData::StickCalibration stick; + }; + + std::optional calibration; + }; + + struct ClassicState + { + using CalibrationData = WiimoteEmu::Classic::CalibrationData; + + void SetCalibrationData(const CalibrationData&, Checksum); + void ProcessData(const WiimoteEmu::Classic::DataFormat&); + + std::array sticks = {}; + std::array triggers = {}; + + u16 buttons = 0; + + struct Calibration + { + Calibration(); + + CalibrationData::StickCalibration left_stick; + CalibrationData::StickCalibration right_stick; + + CalibrationData::TriggerCalibration left_trigger; + CalibrationData::TriggerCalibration right_trigger; + }; + + std::optional calibration; + }; + + struct IRState + { + static u32 GetDesiredIRSensitivity(); + + void ProcessData(const std::array&); + bool IsFullyConfigured() const; + + u32 current_sensitivity = u32(-1); + bool enabled = false; + bool mode_set = false; + + // Average of visible IR "objects". + Common::Vec2 center_position = {}; + + float distance = 0; + + bool is_hidden = true; + }; + + class ReportHandler + { + public: + enum class HandlerResult + { + Handled, + NotHandled, + }; + + ReportHandler(Clock::time_point expired_time); + + template + void AddHandler(std::function); + + HandlerResult TryToHandleReport(const WiimoteReal::Report& report); + + bool IsExpired() const; + + private: + const Clock::time_point m_expired_time; + std::vector> m_callbacks; + }; + + using AckReportHandler = std::function; + + static AckReportHandler MakeAckHandler(OutputReportID report_id, + std::function callback); + + // TODO: Make parameter const. (need to modify DataReportManipulator) + void ProcessInputReport(WiimoteReal::Report& report); + void ProcessMotionPlusExtensionData(const u8* data, u32 size); + void ProcessNormalExtensionData(const u8* data, u32 size); + void ProcessExtensionEvent(bool connected); + void ProcessExtensionID(u8 id_0, u8 id_4, u8 id_5); + void ProcessStatusReport(const InputReportStatus&); + + void RunTasks(); + + bool IsPerformingTask() const; + + template + void QueueReport(T&& report, std::function ack_callback = {}); + + template + void AddReportHandler(T&&... callbacks); + + using ReadResponse = std::optional>; + + void ReadData(AddressSpace space, u8 slave, u16 address, u16 size, + std::function callback); + + void AddReadDataReplyHandler(AddressSpace space, u8 slave, u16 address, u16 size, + std::vector starting_data, + std::function callback); + + template , typename C> + void WriteData(AddressSpace space, u8 slave, u16 address, T&& data, C&& callback); + + void ReadActiveExtensionID(); + void SetIRSensitivity(u32 level); + void ConfigureSpeaker(); + void ConfigureIRCamera(); + + u8 GetDesiredLEDValue() const; + + void TriggerMotionPlusModeChange(); + void TriggerMotionPlusCalibration(); + + bool IsMotionPlusStateKnown() const; + bool IsMotionPlusActive() const; + bool IsMotionPlusInDesiredMode() const; + + bool IsWaitingForMotionPlus() const; + void WaitForMotionPlus(); + void HandleMotionPlusNonResponse(); + + void UpdateRumble(); + void UpdateOrientation(); + void UpdateExtensionNumberInput(); + + std::unique_ptr m_wiimote; + + // Buttons. + DataReportManipulator::CoreData m_core_data = {}; + + // Accelerometer. + Common::Vec3 m_accel_data = {}; + std::optional m_accel_calibration; + + // Pitch, Roll, Yaw inputs. + Common::Vec3 m_rotation_inputs = {}; + + MotionPlusState m_mplus_state = {}; + NunchukState m_nunchuk_state = {}; + ClassicState m_classic_state = {}; + IRState m_ir_state = {}; + + // Used to poll for M+ periodically and wait for it to reset. + Clock::time_point m_mplus_wait_time = Clock::now(); + + // The desired mode is set based on the attached normal extension. + std::optional m_mplus_desired_mode; + + // Status report is requested every so often to update the battery level. + Clock::time_point m_status_outdated_time = Clock::now(); + float m_battery = 0; + u8 m_leds = 0; + + bool m_speaker_configured = false; + + // The last known state of the extension port status flag. + // Used to detect extension port events. + std::optional m_extension_port; + + // Note this refers to the passthrough extension when M+ is active. + std::optional m_extension_id; + + // Rumble state must be saved to set the proper flag in every output report. + bool m_rumble = false; + + // For pulse of rumble motor to simulate multiple levels. + ControlState m_rumble_level = 0; + Clock::time_point m_last_rumble_change = Clock::now(); + + // Assume mode is disabled so one gets set. + InputReportID m_reporting_mode = InputReportID::ReportDisabled; + + // Used only to provide a value for a specialty "input". (for attached extension passthrough) + WiimoteEmu::ExtensionNumber m_extension_number_input = WiimoteEmu::ExtensionNumber::NONE; + bool m_mplus_attached_input = false; + + // Holds callbacks for output report replies. + std::list m_report_handlers; + + // World rotation. (used to rotate IR data and provide pitch, roll, yaw inputs) + Common::Quaternion m_orientation = Common::Quaternion::Identity(); + Clock::time_point m_last_report_time = Clock::now(); +}; + +} // namespace ciface::WiimoteController -- cgit v1.2.3 From 7e1df34735454636c25ed082b1e0be3c6bd2530a Mon Sep 17 00:00:00 2001 From: Shawn Hoffman Date: Tue, 15 Sep 2020 04:34:41 -0700 Subject: rename InputCommon/ControllerInterface/Device to CoreDevice --- .../ControllerInterface/ControllerInterface.h | 2 +- .../InputCommon/ControllerInterface/CoreDevice.cpp | 448 +++++++++++++++++++++ .../InputCommon/ControllerInterface/CoreDevice.h | 228 +++++++++++ .../ControllerInterface/DInput/DInputJoystick.h | 2 +- .../DInput/DInputKeyboardMouse.h | 2 +- .../InputCommon/ControllerInterface/Device.cpp | 448 --------------------- .../Core/InputCommon/ControllerInterface/Device.h | 228 ----------- .../ForceFeedback/ForceFeedbackDevice.h | 2 +- .../ControllerInterface/OSX/OSXJoystick.h | 2 +- .../Quartz/QuartzKeyboardAndMouse.h | 2 +- .../Core/InputCommon/ControllerInterface/SDL/SDL.h | 2 +- .../Wiimote/WiimoteController.h | 2 +- 12 files changed, 684 insertions(+), 684 deletions(-) create mode 100644 Source/Core/InputCommon/ControllerInterface/CoreDevice.cpp create mode 100644 Source/Core/InputCommon/ControllerInterface/CoreDevice.h delete mode 100644 Source/Core/InputCommon/ControllerInterface/Device.cpp delete mode 100644 Source/Core/InputCommon/ControllerInterface/Device.h (limited to 'Source/Core/InputCommon/ControllerInterface') diff --git a/Source/Core/InputCommon/ControllerInterface/ControllerInterface.h b/Source/Core/InputCommon/ControllerInterface/ControllerInterface.h index 84209a720b..ffa189437f 100644 --- a/Source/Core/InputCommon/ControllerInterface/ControllerInterface.h +++ b/Source/Core/InputCommon/ControllerInterface/ControllerInterface.h @@ -12,7 +12,7 @@ #include "Common/Matrix.h" #include "Common/WindowSystemInfo.h" -#include "InputCommon/ControllerInterface/Device.h" +#include "InputCommon/ControllerInterface/CoreDevice.h" // enable disable sources #ifdef _WIN32 diff --git a/Source/Core/InputCommon/ControllerInterface/CoreDevice.cpp b/Source/Core/InputCommon/ControllerInterface/CoreDevice.cpp new file mode 100644 index 0000000000..3a396ddf37 --- /dev/null +++ b/Source/Core/InputCommon/ControllerInterface/CoreDevice.cpp @@ -0,0 +1,448 @@ +// Copyright 2013 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#include "InputCommon/ControllerInterface/CoreDevice.h" + +#include +#include +#include +#include +#include +#include + +#include + +#include "Common/MathUtil.h" +#include "Common/Thread.h" + +namespace ciface::Core +{ +// Compared to an input's current state (ideally 1.0) minus abs(initial_state) (ideally 0.0). +// Note: Detect() logic assumes this is greater than 0.5. +constexpr ControlState INPUT_DETECT_THRESHOLD = 0.55; + +class CombinedInput final : public Device::Input +{ +public: + using Inputs = std::pair; + + CombinedInput(std::string name, const Inputs& inputs) : m_name(std::move(name)), m_inputs(inputs) + { + } + ControlState GetState() const override + { + ControlState result = 0; + + if (m_inputs.first) + result = m_inputs.first->GetState(); + + if (m_inputs.second) + result = std::max(result, m_inputs.second->GetState()); + + return result; + } + std::string GetName() const override { return m_name; } + bool IsDetectable() const override { return false; } + bool IsChild(const Input* input) const override + { + return m_inputs.first == input || m_inputs.second == input; + } + +private: + const std::string m_name; + const std::pair m_inputs; +}; + +Device::~Device() +{ + // delete inputs + for (Device::Input* input : m_inputs) + delete input; + + // delete outputs + for (Device::Output* output : m_outputs) + delete output; +} + +std::optional Device::GetPreferredId() const +{ + return {}; +} + +void Device::AddInput(Device::Input* const i) +{ + m_inputs.push_back(i); +} + +void Device::AddOutput(Device::Output* const o) +{ + m_outputs.push_back(o); +} + +std::string Device::GetQualifiedName() const +{ + return fmt::format("{}/{}/{}", GetSource(), GetId(), GetName()); +} + +auto Device::GetParentMostInput(Input* child) const -> Input* +{ + for (auto* input : m_inputs) + { + if (input->IsChild(child)) + { + // Running recursively is currently unnecessary but it doesn't hurt. + return GetParentMostInput(input); + } + } + + return child; +} + +Device::Input* Device::FindInput(std::string_view name) const +{ + for (Input* input : m_inputs) + { + if (input->IsMatchingName(name)) + return input; + } + + return nullptr; +} + +Device::Output* Device::FindOutput(std::string_view name) const +{ + for (Output* output : m_outputs) + { + if (output->IsMatchingName(name)) + return output; + } + + return nullptr; +} + +bool Device::Control::IsMatchingName(std::string_view name) const +{ + return GetName() == name; +} + +ControlState Device::FullAnalogSurface::GetState() const +{ + return (1 + std::max(0.0, m_high.GetState()) - std::max(0.0, m_low.GetState())) / 2; +} + +std::string Device::FullAnalogSurface::GetName() const +{ + // E.g. "Full Axis X+" + return "Full " + m_high.GetName(); +} + +bool Device::FullAnalogSurface::IsMatchingName(std::string_view name) const +{ + if (Control::IsMatchingName(name)) + return true; + + // Old naming scheme was "Axis X-+" which is too visually similar to "Axis X+". + // This has caused countless problems for users with mysterious misconfigurations. + // We match this old name to support old configurations. + const auto old_name = m_low.GetName() + *m_high.GetName().rbegin(); + + return old_name == name; +} + +void Device::AddCombinedInput(std::string name, const std::pair& inputs) +{ + AddInput(new CombinedInput(std::move(name), {FindInput(inputs.first), FindInput(inputs.second)})); +} + +// +// DeviceQualifier :: ToString +// +// Get string from a device qualifier / serialize +// +std::string DeviceQualifier::ToString() const +{ + if (source.empty() && (cid < 0) && name.empty()) + return ""; + + std::ostringstream ss; + ss << source << '/'; + if (cid > -1) + ss << cid; + ss << '/' << name; + + return ss.str(); +} + +// +// DeviceQualifier :: FromString +// +// Set a device qualifier from a string / unserialize +// +void DeviceQualifier::FromString(const std::string& str) +{ + *this = {}; + + std::istringstream ss(str); + + std::getline(ss, source, '/'); + + // silly + std::getline(ss, name, '/'); + std::istringstream(name) >> cid; + + std::getline(ss, name); +} + +// +// DeviceQualifier :: FromDevice +// +// Set a device qualifier from a device +// +void DeviceQualifier::FromDevice(const Device* const dev) +{ + name = dev->GetName(); + cid = dev->GetId(); + source = dev->GetSource(); +} + +bool DeviceQualifier::operator==(const Device* const dev) const +{ + if (dev->GetId() == cid) + if (dev->GetName() == name) + if (dev->GetSource() == source) + return true; + + return false; +} + +bool DeviceQualifier::operator!=(const Device* const dev) const +{ + return !operator==(dev); +} + +bool DeviceQualifier::operator==(const DeviceQualifier& devq) const +{ + return std::tie(cid, name, source) == std::tie(devq.cid, devq.name, devq.source); +} + +bool DeviceQualifier::operator!=(const DeviceQualifier& devq) const +{ + return !operator==(devq); +} + +std::shared_ptr DeviceContainer::FindDevice(const DeviceQualifier& devq) const +{ + std::lock_guard lk(m_devices_mutex); + for (const auto& d : m_devices) + { + if (devq == d.get()) + return d; + } + + return nullptr; +} + +std::vector DeviceContainer::GetAllDeviceStrings() const +{ + std::lock_guard lk(m_devices_mutex); + + std::vector device_strings; + DeviceQualifier device_qualifier; + + for (const auto& d : m_devices) + { + device_qualifier.FromDevice(d.get()); + device_strings.emplace_back(device_qualifier.ToString()); + } + + return device_strings; +} + +std::string DeviceContainer::GetDefaultDeviceString() const +{ + std::lock_guard lk(m_devices_mutex); + if (m_devices.empty()) + return ""; + + DeviceQualifier device_qualifier; + device_qualifier.FromDevice(m_devices[0].get()); + return device_qualifier.ToString(); +} + +Device::Input* DeviceContainer::FindInput(std::string_view name, const Device* def_dev) const +{ + if (def_dev) + { + Device::Input* const inp = def_dev->FindInput(name); + if (inp) + return inp; + } + + std::lock_guard lk(m_devices_mutex); + for (const auto& d : m_devices) + { + Device::Input* const i = d->FindInput(name); + + if (i) + return i; + } + + return nullptr; +} + +Device::Output* DeviceContainer::FindOutput(std::string_view name, const Device* def_dev) const +{ + return def_dev->FindOutput(name); +} + +bool DeviceContainer::HasConnectedDevice(const DeviceQualifier& qualifier) const +{ + const auto device = FindDevice(qualifier); + return device != nullptr && device->IsValid(); +} + +// Wait for inputs on supplied devices. +// Inputs are only considered if they are first seen in a neutral state. +// This is useful for crazy flightsticks that have certain buttons that are always held down +// and also properly handles detection when using "FullAnalogSurface" inputs. +// Multiple detections are returned until the various timeouts have been reached. +auto DeviceContainer::DetectInput(const std::vector& device_strings, + std::chrono::milliseconds initial_wait, + std::chrono::milliseconds confirmation_wait, + std::chrono::milliseconds maximum_wait) const + -> std::vector +{ + struct InputState + { + InputState(ciface::Core::Device::Input* input_) : input{input_} { stats.Push(0.0); } + + ciface::Core::Device::Input* input; + ControlState initial_state = input->GetState(); + ControlState last_state = initial_state; + MathUtil::RunningVariance stats; + + // Prevent multiiple detections until after release. + bool is_ready = true; + + void Update() + { + const auto new_state = input->GetState(); + + if (!is_ready && new_state < (1 - INPUT_DETECT_THRESHOLD)) + { + last_state = new_state; + is_ready = true; + stats.Clear(); + } + + const auto difference = new_state - last_state; + stats.Push(difference); + last_state = new_state; + } + + bool IsPressed() + { + if (!is_ready) + return false; + + // We want an input that was initially 0.0 and currently 1.0. + const auto detection_score = (last_state - std::abs(initial_state)); + return detection_score > INPUT_DETECT_THRESHOLD; + } + }; + + struct DeviceState + { + std::shared_ptr device; + + std::vector input_states; + }; + + // Acquire devices and initial input states. + std::vector device_states; + for (const auto& device_string : device_strings) + { + DeviceQualifier dq; + dq.FromString(device_string); + auto device = FindDevice(dq); + + if (!device) + continue; + + std::vector input_states; + + for (auto* input : device->Inputs()) + { + // Don't detect things like absolute cursor positions, accelerometers, or gyroscopes. + if (!input->IsDetectable()) + continue; + + // Undesirable axes will have negative values here when trying to map a + // "FullAnalogSurface". + input_states.push_back(InputState{input}); + } + + if (!input_states.empty()) + device_states.emplace_back(DeviceState{std::move(device), std::move(input_states)}); + } + + if (device_states.empty()) + return {}; + + std::vector detections; + + const auto start_time = Clock::now(); + while (true) + { + const auto now = Clock::now(); + const auto elapsed_time = now - start_time; + + if (elapsed_time >= maximum_wait || (detections.empty() && elapsed_time >= initial_wait) || + (!detections.empty() && detections.back().release_time.has_value() && + now >= *detections.back().release_time + confirmation_wait)) + { + break; + } + + Common::SleepCurrentThread(10); + + for (auto& device_state : device_states) + { + for (std::size_t i = 0; i != device_state.input_states.size(); ++i) + { + auto& input_state = device_state.input_states[i]; + input_state.Update(); + + if (input_state.IsPressed()) + { + input_state.is_ready = false; + + // Digital presses will evaluate as 1 here. + // Analog presses will evaluate greater than 1. + const auto smoothness = + 1 / std::sqrt(input_state.stats.Variance() / input_state.stats.Mean()); + + InputDetection new_detection; + new_detection.device = device_state.device; + new_detection.input = input_state.input; + new_detection.press_time = Clock::now(); + new_detection.smoothness = smoothness; + + // We found an input. Add it to our detections. + detections.emplace_back(std::move(new_detection)); + } + } + } + + // Check for any releases of our detected inputs. + for (auto& d : detections) + { + if (!d.release_time.has_value() && d.input->GetState() < (1 - INPUT_DETECT_THRESHOLD)) + d.release_time = Clock::now(); + } + } + + return detections; +} +} // namespace ciface::Core diff --git a/Source/Core/InputCommon/ControllerInterface/CoreDevice.h b/Source/Core/InputCommon/ControllerInterface/CoreDevice.h new file mode 100644 index 0000000000..2612f59627 --- /dev/null +++ b/Source/Core/InputCommon/ControllerInterface/CoreDevice.h @@ -0,0 +1,228 @@ +// Copyright 2013 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "Common/CommonTypes.h" + +// idk in case I wanted to change it to double or something, idk what's best +typedef double ControlState; + +namespace ciface +{ +// 100Hz which homebrew docs very roughly imply is within WiiMote normal +// range, used for periodic haptic effects though often ignored by devices +// TODO: Make this configurable. +constexpr int RUMBLE_PERIOD_MS = 10; + +// This needs to be at least as long as the longest rumble that might ever be played. +// Too short and it's going to stop in the middle of a long effect. +// Infinite values are invalid for ramp effects and probably not sensible. +constexpr int RUMBLE_LENGTH_MS = 1000 * 10; + +// All inputs (other than accel/gyro) return 1.0 as their maximum value. +// Battery inputs will almost always be mapped to the "Battery" setting which is a percentage. +// If someone actually wants to map a battery input to a regular control they can divide by 100. +// I think this is better than requiring multiplication by 100 for the most common usage. +constexpr ControlState BATTERY_INPUT_MAX_VALUE = 100.0; + +namespace Core +{ +class Device +{ +public: + class Input; + class Output; + + // + // Control + // + // Control includes inputs and outputs + // + class Control // input or output + { + public: + virtual ~Control() = default; + virtual std::string GetName() const = 0; + virtual Input* ToInput() { return nullptr; } + virtual Output* ToOutput() { return nullptr; } + + // May be overridden to allow multiple valid names. + // Useful for backwards-compatible configurations when names change. + virtual bool IsMatchingName(std::string_view name) const; + }; + + // + // Input + // + // An input on a device + // + class Input : public Control + { + public: + // Things like absolute axes/ absolute mouse position should override this to prevent + // undesirable behavior in our mapping logic. + virtual bool IsDetectable() const { return true; } + + // Implementations should return a value from 0.0 to 1.0 across their normal range. + // One input should be provided for each "direction". (e.g. 2 for each axis) + // If possible, negative values may be returned in situations where an opposing input is + // activated. (e.g. When an underlying axis, X, is currently negative, "Axis X-", will return a + // positive value and "Axis X+" may return a negative value.) + // Doing so is solely to allow our input detection logic to better detect false positives. + // This is necessary when making use of "FullAnalogSurface" as multiple inputs will be seen + // increasing from 0.0 to 1.0 as a user tries to map just one. The negative values provide a + // view of the underlying axis. (Negative values are clamped off before they reach + // expression-parser or controller-emu) + virtual ControlState GetState() const = 0; + + Input* ToInput() override { return this; } + + // Overridden by CombinedInput, + // so hotkey logic knows Ctrl, L_Ctrl, and R_Ctrl are the same, + // and so input detection can return the parent name. + virtual bool IsChild(const Input*) const { return false; } + }; + + // + // Output + // + // An output on a device + // + class Output : public Control + { + public: + virtual ~Output() = default; + virtual void SetState(ControlState state) = 0; + Output* ToOutput() override { return this; } + }; + + virtual ~Device(); + + int GetId() const { return m_id; } + void SetId(int id) { m_id = id; } + virtual std::string GetName() const = 0; + virtual std::string GetSource() const = 0; + std::string GetQualifiedName() const; + virtual void UpdateInput() {} + + // May be overridden to implement hotplug removal. + // Currently handled on a per-backend basis but this could change. + virtual bool IsValid() const { return true; } + + // (e.g. Xbox 360 controllers have controller number LEDs which should match the ID we use.) + virtual std::optional GetPreferredId() const; + + const std::vector& Inputs() const { return m_inputs; } + const std::vector& Outputs() const { return m_outputs; } + + Input* GetParentMostInput(Input* input) const; + + Input* FindInput(std::string_view name) const; + Output* FindOutput(std::string_view name) const; + +protected: + void AddInput(Input* const i); + void AddOutput(Output* const o); + + class FullAnalogSurface final : public Input + { + public: + FullAnalogSurface(Input* low, Input* high) : m_low(*low), m_high(*high) {} + ControlState GetState() const override; + std::string GetName() const override; + bool IsMatchingName(std::string_view name) const override; + + private: + Input& m_low; + Input& m_high; + }; + + void AddAnalogInputs(Input* low, Input* high) + { + AddInput(low); + AddInput(high); + AddInput(new FullAnalogSurface(low, high)); + AddInput(new FullAnalogSurface(high, low)); + } + + void AddCombinedInput(std::string name, const std::pair& inputs); + +private: + int m_id; + std::vector m_inputs; + std::vector m_outputs; +}; + +// +// DeviceQualifier +// +// Device qualifier used to match devices. +// Currently has ( source, id, name ) properties which match a device +// +class DeviceQualifier +{ +public: + DeviceQualifier() : cid(-1) {} + DeviceQualifier(std::string source_, const int id_, std::string name_) + : source(std::move(source_)), cid(id_), name(std::move(name_)) + { + } + void FromDevice(const Device* const dev); + void FromString(const std::string& str); + std::string ToString() const; + + bool operator==(const DeviceQualifier& devq) const; + bool operator!=(const DeviceQualifier& devq) const; + + bool operator==(const Device* dev) const; + bool operator!=(const Device* dev) const; + + std::string source; + int cid; + std::string name; +}; + +class DeviceContainer +{ +public: + using Clock = std::chrono::steady_clock; + + struct InputDetection + { + std::shared_ptr device; + Device::Input* input; + Clock::time_point press_time; + std::optional release_time; + ControlState smoothness; + }; + + Device::Input* FindInput(std::string_view name, const Device* def_dev) const; + Device::Output* FindOutput(std::string_view name, const Device* def_dev) const; + + std::vector GetAllDeviceStrings() const; + std::string GetDefaultDeviceString() const; + std::shared_ptr FindDevice(const DeviceQualifier& devq) const; + + bool HasConnectedDevice(const DeviceQualifier& qualifier) const; + + std::vector DetectInput(const std::vector& device_strings, + std::chrono::milliseconds initial_wait, + std::chrono::milliseconds confirmation_wait, + std::chrono::milliseconds maximum_wait) const; + +protected: + mutable std::recursive_mutex m_devices_mutex; + std::vector> m_devices; +}; +} // namespace Core +} // namespace ciface diff --git a/Source/Core/InputCommon/ControllerInterface/DInput/DInputJoystick.h b/Source/Core/InputCommon/ControllerInterface/DInput/DInputJoystick.h index 70e8af8e6e..fe0f46424f 100644 --- a/Source/Core/InputCommon/ControllerInterface/DInput/DInputJoystick.h +++ b/Source/Core/InputCommon/ControllerInterface/DInput/DInputJoystick.h @@ -4,7 +4,7 @@ #pragma once -#include "InputCommon/ControllerInterface/Device.h" +#include "InputCommon/ControllerInterface/CoreDevice.h" #include "InputCommon/ControllerInterface/ForceFeedback/ForceFeedbackDevice.h" namespace ciface::DInput diff --git a/Source/Core/InputCommon/ControllerInterface/DInput/DInputKeyboardMouse.h b/Source/Core/InputCommon/ControllerInterface/DInput/DInputKeyboardMouse.h index 0ae81fb6be..c3834df528 100644 --- a/Source/Core/InputCommon/ControllerInterface/DInput/DInputKeyboardMouse.h +++ b/Source/Core/InputCommon/ControllerInterface/DInput/DInputKeyboardMouse.h @@ -7,8 +7,8 @@ #include #include "Common/Matrix.h" +#include "InputCommon/ControllerInterface/CoreDevice.h" #include "InputCommon/ControllerInterface/DInput/DInput8.h" -#include "InputCommon/ControllerInterface/Device.h" namespace ciface::DInput { diff --git a/Source/Core/InputCommon/ControllerInterface/Device.cpp b/Source/Core/InputCommon/ControllerInterface/Device.cpp deleted file mode 100644 index 2fd7fb02e7..0000000000 --- a/Source/Core/InputCommon/ControllerInterface/Device.cpp +++ /dev/null @@ -1,448 +0,0 @@ -// Copyright 2013 Dolphin Emulator Project -// Licensed under GPLv2+ -// Refer to the license.txt file included. - -#include "InputCommon/ControllerInterface/Device.h" - -#include -#include -#include -#include -#include -#include - -#include - -#include "Common/MathUtil.h" -#include "Common/Thread.h" - -namespace ciface::Core -{ -// Compared to an input's current state (ideally 1.0) minus abs(initial_state) (ideally 0.0). -// Note: Detect() logic assumes this is greater than 0.5. -constexpr ControlState INPUT_DETECT_THRESHOLD = 0.55; - -class CombinedInput final : public Device::Input -{ -public: - using Inputs = std::pair; - - CombinedInput(std::string name, const Inputs& inputs) : m_name(std::move(name)), m_inputs(inputs) - { - } - ControlState GetState() const override - { - ControlState result = 0; - - if (m_inputs.first) - result = m_inputs.first->GetState(); - - if (m_inputs.second) - result = std::max(result, m_inputs.second->GetState()); - - return result; - } - std::string GetName() const override { return m_name; } - bool IsDetectable() const override { return false; } - bool IsChild(const Input* input) const override - { - return m_inputs.first == input || m_inputs.second == input; - } - -private: - const std::string m_name; - const std::pair m_inputs; -}; - -Device::~Device() -{ - // delete inputs - for (Device::Input* input : m_inputs) - delete input; - - // delete outputs - for (Device::Output* output : m_outputs) - delete output; -} - -std::optional Device::GetPreferredId() const -{ - return {}; -} - -void Device::AddInput(Device::Input* const i) -{ - m_inputs.push_back(i); -} - -void Device::AddOutput(Device::Output* const o) -{ - m_outputs.push_back(o); -} - -std::string Device::GetQualifiedName() const -{ - return fmt::format("{}/{}/{}", GetSource(), GetId(), GetName()); -} - -auto Device::GetParentMostInput(Input* child) const -> Input* -{ - for (auto* input : m_inputs) - { - if (input->IsChild(child)) - { - // Running recursively is currently unnecessary but it doesn't hurt. - return GetParentMostInput(input); - } - } - - return child; -} - -Device::Input* Device::FindInput(std::string_view name) const -{ - for (Input* input : m_inputs) - { - if (input->IsMatchingName(name)) - return input; - } - - return nullptr; -} - -Device::Output* Device::FindOutput(std::string_view name) const -{ - for (Output* output : m_outputs) - { - if (output->IsMatchingName(name)) - return output; - } - - return nullptr; -} - -bool Device::Control::IsMatchingName(std::string_view name) const -{ - return GetName() == name; -} - -ControlState Device::FullAnalogSurface::GetState() const -{ - return (1 + std::max(0.0, m_high.GetState()) - std::max(0.0, m_low.GetState())) / 2; -} - -std::string Device::FullAnalogSurface::GetName() const -{ - // E.g. "Full Axis X+" - return "Full " + m_high.GetName(); -} - -bool Device::FullAnalogSurface::IsMatchingName(std::string_view name) const -{ - if (Control::IsMatchingName(name)) - return true; - - // Old naming scheme was "Axis X-+" which is too visually similar to "Axis X+". - // This has caused countless problems for users with mysterious misconfigurations. - // We match this old name to support old configurations. - const auto old_name = m_low.GetName() + *m_high.GetName().rbegin(); - - return old_name == name; -} - -void Device::AddCombinedInput(std::string name, const std::pair& inputs) -{ - AddInput(new CombinedInput(std::move(name), {FindInput(inputs.first), FindInput(inputs.second)})); -} - -// -// DeviceQualifier :: ToString -// -// Get string from a device qualifier / serialize -// -std::string DeviceQualifier::ToString() const -{ - if (source.empty() && (cid < 0) && name.empty()) - return ""; - - std::ostringstream ss; - ss << source << '/'; - if (cid > -1) - ss << cid; - ss << '/' << name; - - return ss.str(); -} - -// -// DeviceQualifier :: FromString -// -// Set a device qualifier from a string / unserialize -// -void DeviceQualifier::FromString(const std::string& str) -{ - *this = {}; - - std::istringstream ss(str); - - std::getline(ss, source, '/'); - - // silly - std::getline(ss, name, '/'); - std::istringstream(name) >> cid; - - std::getline(ss, name); -} - -// -// DeviceQualifier :: FromDevice -// -// Set a device qualifier from a device -// -void DeviceQualifier::FromDevice(const Device* const dev) -{ - name = dev->GetName(); - cid = dev->GetId(); - source = dev->GetSource(); -} - -bool DeviceQualifier::operator==(const Device* const dev) const -{ - if (dev->GetId() == cid) - if (dev->GetName() == name) - if (dev->GetSource() == source) - return true; - - return false; -} - -bool DeviceQualifier::operator!=(const Device* const dev) const -{ - return !operator==(dev); -} - -bool DeviceQualifier::operator==(const DeviceQualifier& devq) const -{ - return std::tie(cid, name, source) == std::tie(devq.cid, devq.name, devq.source); -} - -bool DeviceQualifier::operator!=(const DeviceQualifier& devq) const -{ - return !operator==(devq); -} - -std::shared_ptr DeviceContainer::FindDevice(const DeviceQualifier& devq) const -{ - std::lock_guard lk(m_devices_mutex); - for (const auto& d : m_devices) - { - if (devq == d.get()) - return d; - } - - return nullptr; -} - -std::vector DeviceContainer::GetAllDeviceStrings() const -{ - std::lock_guard lk(m_devices_mutex); - - std::vector device_strings; - DeviceQualifier device_qualifier; - - for (const auto& d : m_devices) - { - device_qualifier.FromDevice(d.get()); - device_strings.emplace_back(device_qualifier.ToString()); - } - - return device_strings; -} - -std::string DeviceContainer::GetDefaultDeviceString() const -{ - std::lock_guard lk(m_devices_mutex); - if (m_devices.empty()) - return ""; - - DeviceQualifier device_qualifier; - device_qualifier.FromDevice(m_devices[0].get()); - return device_qualifier.ToString(); -} - -Device::Input* DeviceContainer::FindInput(std::string_view name, const Device* def_dev) const -{ - if (def_dev) - { - Device::Input* const inp = def_dev->FindInput(name); - if (inp) - return inp; - } - - std::lock_guard lk(m_devices_mutex); - for (const auto& d : m_devices) - { - Device::Input* const i = d->FindInput(name); - - if (i) - return i; - } - - return nullptr; -} - -Device::Output* DeviceContainer::FindOutput(std::string_view name, const Device* def_dev) const -{ - return def_dev->FindOutput(name); -} - -bool DeviceContainer::HasConnectedDevice(const DeviceQualifier& qualifier) const -{ - const auto device = FindDevice(qualifier); - return device != nullptr && device->IsValid(); -} - -// Wait for inputs on supplied devices. -// Inputs are only considered if they are first seen in a neutral state. -// This is useful for crazy flightsticks that have certain buttons that are always held down -// and also properly handles detection when using "FullAnalogSurface" inputs. -// Multiple detections are returned until the various timeouts have been reached. -auto DeviceContainer::DetectInput(const std::vector& device_strings, - std::chrono::milliseconds initial_wait, - std::chrono::milliseconds confirmation_wait, - std::chrono::milliseconds maximum_wait) const - -> std::vector -{ - struct InputState - { - InputState(ciface::Core::Device::Input* input_) : input{input_} { stats.Push(0.0); } - - ciface::Core::Device::Input* input; - ControlState initial_state = input->GetState(); - ControlState last_state = initial_state; - MathUtil::RunningVariance stats; - - // Prevent multiiple detections until after release. - bool is_ready = true; - - void Update() - { - const auto new_state = input->GetState(); - - if (!is_ready && new_state < (1 - INPUT_DETECT_THRESHOLD)) - { - last_state = new_state; - is_ready = true; - stats.Clear(); - } - - const auto difference = new_state - last_state; - stats.Push(difference); - last_state = new_state; - } - - bool IsPressed() - { - if (!is_ready) - return false; - - // We want an input that was initially 0.0 and currently 1.0. - const auto detection_score = (last_state - std::abs(initial_state)); - return detection_score > INPUT_DETECT_THRESHOLD; - } - }; - - struct DeviceState - { - std::shared_ptr device; - - std::vector input_states; - }; - - // Acquire devices and initial input states. - std::vector device_states; - for (const auto& device_string : device_strings) - { - DeviceQualifier dq; - dq.FromString(device_string); - auto device = FindDevice(dq); - - if (!device) - continue; - - std::vector input_states; - - for (auto* input : device->Inputs()) - { - // Don't detect things like absolute cursor positions, accelerometers, or gyroscopes. - if (!input->IsDetectable()) - continue; - - // Undesirable axes will have negative values here when trying to map a - // "FullAnalogSurface". - input_states.push_back(InputState{input}); - } - - if (!input_states.empty()) - device_states.emplace_back(DeviceState{std::move(device), std::move(input_states)}); - } - - if (device_states.empty()) - return {}; - - std::vector detections; - - const auto start_time = Clock::now(); - while (true) - { - const auto now = Clock::now(); - const auto elapsed_time = now - start_time; - - if (elapsed_time >= maximum_wait || (detections.empty() && elapsed_time >= initial_wait) || - (!detections.empty() && detections.back().release_time.has_value() && - now >= *detections.back().release_time + confirmation_wait)) - { - break; - } - - Common::SleepCurrentThread(10); - - for (auto& device_state : device_states) - { - for (std::size_t i = 0; i != device_state.input_states.size(); ++i) - { - auto& input_state = device_state.input_states[i]; - input_state.Update(); - - if (input_state.IsPressed()) - { - input_state.is_ready = false; - - // Digital presses will evaluate as 1 here. - // Analog presses will evaluate greater than 1. - const auto smoothness = - 1 / std::sqrt(input_state.stats.Variance() / input_state.stats.Mean()); - - InputDetection new_detection; - new_detection.device = device_state.device; - new_detection.input = input_state.input; - new_detection.press_time = Clock::now(); - new_detection.smoothness = smoothness; - - // We found an input. Add it to our detections. - detections.emplace_back(std::move(new_detection)); - } - } - } - - // Check for any releases of our detected inputs. - for (auto& d : detections) - { - if (!d.release_time.has_value() && d.input->GetState() < (1 - INPUT_DETECT_THRESHOLD)) - d.release_time = Clock::now(); - } - } - - return detections; -} -} // namespace ciface::Core diff --git a/Source/Core/InputCommon/ControllerInterface/Device.h b/Source/Core/InputCommon/ControllerInterface/Device.h deleted file mode 100644 index 2612f59627..0000000000 --- a/Source/Core/InputCommon/ControllerInterface/Device.h +++ /dev/null @@ -1,228 +0,0 @@ -// Copyright 2013 Dolphin Emulator Project -// Licensed under GPLv2+ -// Refer to the license.txt file included. - -#pragma once - -#include -#include -#include -#include -#include -#include -#include - -#include "Common/CommonTypes.h" - -// idk in case I wanted to change it to double or something, idk what's best -typedef double ControlState; - -namespace ciface -{ -// 100Hz which homebrew docs very roughly imply is within WiiMote normal -// range, used for periodic haptic effects though often ignored by devices -// TODO: Make this configurable. -constexpr int RUMBLE_PERIOD_MS = 10; - -// This needs to be at least as long as the longest rumble that might ever be played. -// Too short and it's going to stop in the middle of a long effect. -// Infinite values are invalid for ramp effects and probably not sensible. -constexpr int RUMBLE_LENGTH_MS = 1000 * 10; - -// All inputs (other than accel/gyro) return 1.0 as their maximum value. -// Battery inputs will almost always be mapped to the "Battery" setting which is a percentage. -// If someone actually wants to map a battery input to a regular control they can divide by 100. -// I think this is better than requiring multiplication by 100 for the most common usage. -constexpr ControlState BATTERY_INPUT_MAX_VALUE = 100.0; - -namespace Core -{ -class Device -{ -public: - class Input; - class Output; - - // - // Control - // - // Control includes inputs and outputs - // - class Control // input or output - { - public: - virtual ~Control() = default; - virtual std::string GetName() const = 0; - virtual Input* ToInput() { return nullptr; } - virtual Output* ToOutput() { return nullptr; } - - // May be overridden to allow multiple valid names. - // Useful for backwards-compatible configurations when names change. - virtual bool IsMatchingName(std::string_view name) const; - }; - - // - // Input - // - // An input on a device - // - class Input : public Control - { - public: - // Things like absolute axes/ absolute mouse position should override this to prevent - // undesirable behavior in our mapping logic. - virtual bool IsDetectable() const { return true; } - - // Implementations should return a value from 0.0 to 1.0 across their normal range. - // One input should be provided for each "direction". (e.g. 2 for each axis) - // If possible, negative values may be returned in situations where an opposing input is - // activated. (e.g. When an underlying axis, X, is currently negative, "Axis X-", will return a - // positive value and "Axis X+" may return a negative value.) - // Doing so is solely to allow our input detection logic to better detect false positives. - // This is necessary when making use of "FullAnalogSurface" as multiple inputs will be seen - // increasing from 0.0 to 1.0 as a user tries to map just one. The negative values provide a - // view of the underlying axis. (Negative values are clamped off before they reach - // expression-parser or controller-emu) - virtual ControlState GetState() const = 0; - - Input* ToInput() override { return this; } - - // Overridden by CombinedInput, - // so hotkey logic knows Ctrl, L_Ctrl, and R_Ctrl are the same, - // and so input detection can return the parent name. - virtual bool IsChild(const Input*) const { return false; } - }; - - // - // Output - // - // An output on a device - // - class Output : public Control - { - public: - virtual ~Output() = default; - virtual void SetState(ControlState state) = 0; - Output* ToOutput() override { return this; } - }; - - virtual ~Device(); - - int GetId() const { return m_id; } - void SetId(int id) { m_id = id; } - virtual std::string GetName() const = 0; - virtual std::string GetSource() const = 0; - std::string GetQualifiedName() const; - virtual void UpdateInput() {} - - // May be overridden to implement hotplug removal. - // Currently handled on a per-backend basis but this could change. - virtual bool IsValid() const { return true; } - - // (e.g. Xbox 360 controllers have controller number LEDs which should match the ID we use.) - virtual std::optional GetPreferredId() const; - - const std::vector& Inputs() const { return m_inputs; } - const std::vector& Outputs() const { return m_outputs; } - - Input* GetParentMostInput(Input* input) const; - - Input* FindInput(std::string_view name) const; - Output* FindOutput(std::string_view name) const; - -protected: - void AddInput(Input* const i); - void AddOutput(Output* const o); - - class FullAnalogSurface final : public Input - { - public: - FullAnalogSurface(Input* low, Input* high) : m_low(*low), m_high(*high) {} - ControlState GetState() const override; - std::string GetName() const override; - bool IsMatchingName(std::string_view name) const override; - - private: - Input& m_low; - Input& m_high; - }; - - void AddAnalogInputs(Input* low, Input* high) - { - AddInput(low); - AddInput(high); - AddInput(new FullAnalogSurface(low, high)); - AddInput(new FullAnalogSurface(high, low)); - } - - void AddCombinedInput(std::string name, const std::pair& inputs); - -private: - int m_id; - std::vector m_inputs; - std::vector m_outputs; -}; - -// -// DeviceQualifier -// -// Device qualifier used to match devices. -// Currently has ( source, id, name ) properties which match a device -// -class DeviceQualifier -{ -public: - DeviceQualifier() : cid(-1) {} - DeviceQualifier(std::string source_, const int id_, std::string name_) - : source(std::move(source_)), cid(id_), name(std::move(name_)) - { - } - void FromDevice(const Device* const dev); - void FromString(const std::string& str); - std::string ToString() const; - - bool operator==(const DeviceQualifier& devq) const; - bool operator!=(const DeviceQualifier& devq) const; - - bool operator==(const Device* dev) const; - bool operator!=(const Device* dev) const; - - std::string source; - int cid; - std::string name; -}; - -class DeviceContainer -{ -public: - using Clock = std::chrono::steady_clock; - - struct InputDetection - { - std::shared_ptr device; - Device::Input* input; - Clock::time_point press_time; - std::optional release_time; - ControlState smoothness; - }; - - Device::Input* FindInput(std::string_view name, const Device* def_dev) const; - Device::Output* FindOutput(std::string_view name, const Device* def_dev) const; - - std::vector GetAllDeviceStrings() const; - std::string GetDefaultDeviceString() const; - std::shared_ptr FindDevice(const DeviceQualifier& devq) const; - - bool HasConnectedDevice(const DeviceQualifier& qualifier) const; - - std::vector DetectInput(const std::vector& device_strings, - std::chrono::milliseconds initial_wait, - std::chrono::milliseconds confirmation_wait, - std::chrono::milliseconds maximum_wait) const; - -protected: - mutable std::recursive_mutex m_devices_mutex; - std::vector> m_devices; -}; -} // namespace Core -} // namespace ciface diff --git a/Source/Core/InputCommon/ControllerInterface/ForceFeedback/ForceFeedbackDevice.h b/Source/Core/InputCommon/ControllerInterface/ForceFeedback/ForceFeedbackDevice.h index c689cef5a5..8fab93c429 100644 --- a/Source/Core/InputCommon/ControllerInterface/ForceFeedback/ForceFeedbackDevice.h +++ b/Source/Core/InputCommon/ControllerInterface/ForceFeedback/ForceFeedbackDevice.h @@ -10,7 +10,7 @@ #include "Common/Event.h" #include "Common/Flag.h" -#include "InputCommon/ControllerInterface/Device.h" +#include "InputCommon/ControllerInterface/CoreDevice.h" #ifdef _WIN32 #include diff --git a/Source/Core/InputCommon/ControllerInterface/OSX/OSXJoystick.h b/Source/Core/InputCommon/ControllerInterface/OSX/OSXJoystick.h index 377f7d9b29..89552a80de 100644 --- a/Source/Core/InputCommon/ControllerInterface/OSX/OSXJoystick.h +++ b/Source/Core/InputCommon/ControllerInterface/OSX/OSXJoystick.h @@ -8,7 +8,7 @@ #include -#include "InputCommon/ControllerInterface/Device.h" +#include "InputCommon/ControllerInterface/CoreDevice.h" #include "InputCommon/ControllerInterface/ForceFeedback/ForceFeedbackDevice.h" namespace ciface::OSX diff --git a/Source/Core/InputCommon/ControllerInterface/Quartz/QuartzKeyboardAndMouse.h b/Source/Core/InputCommon/ControllerInterface/Quartz/QuartzKeyboardAndMouse.h index 39516035a3..740954082b 100644 --- a/Source/Core/InputCommon/ControllerInterface/Quartz/QuartzKeyboardAndMouse.h +++ b/Source/Core/InputCommon/ControllerInterface/Quartz/QuartzKeyboardAndMouse.h @@ -7,7 +7,7 @@ #include #include "Common/Matrix.h" -#include "InputCommon/ControllerInterface/Device.h" +#include "InputCommon/ControllerInterface/CoreDevice.h" namespace ciface::Quartz { diff --git a/Source/Core/InputCommon/ControllerInterface/SDL/SDL.h b/Source/Core/InputCommon/ControllerInterface/SDL/SDL.h index e9ac021bd9..38b6bf3cfb 100644 --- a/Source/Core/InputCommon/ControllerInterface/SDL/SDL.h +++ b/Source/Core/InputCommon/ControllerInterface/SDL/SDL.h @@ -14,7 +14,7 @@ #include #endif -#include "InputCommon/ControllerInterface/Device.h" +#include "InputCommon/ControllerInterface/CoreDevice.h" namespace ciface::SDL { diff --git a/Source/Core/InputCommon/ControllerInterface/Wiimote/WiimoteController.h b/Source/Core/InputCommon/ControllerInterface/Wiimote/WiimoteController.h index 53abc0bf3f..4864e0abb3 100644 --- a/Source/Core/InputCommon/ControllerInterface/Wiimote/WiimoteController.h +++ b/Source/Core/InputCommon/ControllerInterface/Wiimote/WiimoteController.h @@ -16,7 +16,7 @@ #include "Core/HW/WiimoteEmu/Extension/Nunchuk.h" #include "Core/HW/WiimoteEmu/MotionPlus.h" #include "Core/HW/WiimoteReal/WiimoteReal.h" -#include "InputCommon/ControllerInterface/Device.h" +#include "InputCommon/ControllerInterface/CoreDevice.h" namespace ciface::WiimoteController { -- cgit v1.2.3