diff options
Diffstat (limited to 'xbmc/peripherals/devices')
25 files changed, 4769 insertions, 0 deletions
diff --git a/xbmc/peripherals/devices/CMakeLists.txt b/xbmc/peripherals/devices/CMakeLists.txt new file mode 100644 index 0000000..57795f5 --- /dev/null +++ b/xbmc/peripherals/devices/CMakeLists.txt @@ -0,0 +1,30 @@ +set(SOURCES Peripheral.cpp + PeripheralBluetooth.cpp + PeripheralDisk.cpp + PeripheralHID.cpp + PeripheralImon.cpp + PeripheralJoystick.cpp + PeripheralKeyboard.cpp + PeripheralMouse.cpp + PeripheralNIC.cpp + PeripheralNyxboard.cpp + PeripheralTuner.cpp) + +set(HEADERS Peripheral.h + PeripheralBluetooth.h + PeripheralDisk.h + PeripheralHID.h + PeripheralImon.h + PeripheralJoystick.h + PeripheralKeyboard.h + PeripheralMouse.h + PeripheralNIC.h + PeripheralNyxboard.h + PeripheralTuner.h) + +if(CEC_FOUND) + list(APPEND SOURCES PeripheralCecAdapter.cpp) + list(APPEND HEADERS PeripheralCecAdapter.h) +endif() + +core_add_library(peripherals_devices) diff --git a/xbmc/peripherals/devices/Peripheral.cpp b/xbmc/peripherals/devices/Peripheral.cpp new file mode 100644 index 0000000..6c3f779 --- /dev/null +++ b/xbmc/peripherals/devices/Peripheral.cpp @@ -0,0 +1,771 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "Peripheral.h" + +#include "Util.h" +#include "XBDateTime.h" +#include "games/controllers/Controller.h" +#include "games/controllers/ControllerLayout.h" +#include "guilib/LocalizeStrings.h" +#include "input/joysticks/interfaces/IInputHandler.h" +#include "peripherals/Peripherals.h" +#include "peripherals/addons/AddonButtonMapping.h" +#include "peripherals/addons/AddonInputHandling.h" +#include "peripherals/addons/PeripheralAddon.h" +#include "peripherals/bus/PeripheralBus.h" +#include "peripherals/bus/virtual/PeripheralBusAddon.h" +#include "settings/SettingAddon.h" +#include "settings/lib/Setting.h" +#include "utils/FileUtils.h" +#include "utils/StringUtils.h" +#include "utils/XBMCTinyXML.h" +#include "utils/XMLUtils.h" +#include "utils/log.h" + +#include <utility> + +using namespace KODI; +using namespace JOYSTICK; +using namespace PERIPHERALS; + +struct SortBySettingsOrder +{ + bool operator()(const PeripheralDeviceSetting& left, const PeripheralDeviceSetting& right) + { + return left.m_order < right.m_order; + } +}; + +CPeripheral::CPeripheral(CPeripherals& manager, + const PeripheralScanResult& scanResult, + CPeripheralBus* bus) + : m_manager(manager), + m_type(scanResult.m_mappedType), + m_busType(scanResult.m_busType), + m_mappedBusType(scanResult.m_mappedBusType), + m_strLocation(scanResult.m_strLocation), + m_strDeviceName(scanResult.m_strDeviceName), + m_iVendorId(scanResult.m_iVendorId), + m_iProductId(scanResult.m_iProductId), + m_strVersionInfo(g_localizeStrings.Get(13205)), // "unknown" + m_bInitialised(false), + m_bHidden(false), + m_bError(false), + m_bus(bus) +{ + PeripheralTypeTranslator::FormatHexString(scanResult.m_iVendorId, m_strVendorId); + PeripheralTypeTranslator::FormatHexString(scanResult.m_iProductId, m_strProductId); + if (scanResult.m_iSequence > 0) + { + m_strFileLocation = + StringUtils::Format("peripherals://{}/{}_{}.dev", + PeripheralTypeTranslator::BusTypeToString(scanResult.m_busType), + scanResult.m_strLocation, scanResult.m_iSequence); + } + else + { + m_strFileLocation = StringUtils::Format( + "peripherals://{}/{}.dev", PeripheralTypeTranslator::BusTypeToString(scanResult.m_busType), + scanResult.m_strLocation); + } +} + +CPeripheral::~CPeripheral(void) +{ + PersistSettings(true); + + m_subDevices.clear(); + + ClearSettings(); +} + +bool CPeripheral::operator==(const CPeripheral& right) const +{ + return m_type == right.m_type && m_strLocation == right.m_strLocation && + m_iVendorId == right.m_iVendorId && m_iProductId == right.m_iProductId; +} + +bool CPeripheral::operator!=(const CPeripheral& right) const +{ + return !(*this == right); +} + +bool CPeripheral::HasFeature(const PeripheralFeature feature) const +{ + bool bReturn(false); + + for (unsigned int iFeaturePtr = 0; iFeaturePtr < m_features.size(); iFeaturePtr++) + { + if (m_features.at(iFeaturePtr) == feature) + { + bReturn = true; + break; + } + } + + if (!bReturn) + { + for (unsigned int iSubdevicePtr = 0; iSubdevicePtr < m_subDevices.size(); iSubdevicePtr++) + { + if (m_subDevices.at(iSubdevicePtr)->HasFeature(feature)) + { + bReturn = true; + break; + } + } + } + + return bReturn; +} + +void CPeripheral::GetFeatures(std::vector<PeripheralFeature>& features) const +{ + for (unsigned int iFeaturePtr = 0; iFeaturePtr < m_features.size(); iFeaturePtr++) + features.push_back(m_features.at(iFeaturePtr)); + + for (unsigned int iSubdevicePtr = 0; iSubdevicePtr < m_subDevices.size(); iSubdevicePtr++) + m_subDevices.at(iSubdevicePtr)->GetFeatures(features); +} + +bool CPeripheral::Initialise(void) +{ + bool bReturn(false); + + if (m_bError) + return bReturn; + + bReturn = true; + if (m_bInitialised) + return bReturn; + + m_manager.GetSettingsFromMapping(*this); + + std::string safeDeviceName = m_strDeviceName; + StringUtils::Replace(safeDeviceName, ' ', '_'); + + if (m_iVendorId == 0x0000 && m_iProductId == 0x0000) + { + m_strSettingsFile = + StringUtils::Format("special://profile/peripheral_data/{}_{}.xml", + PeripheralTypeTranslator::BusTypeToString(m_mappedBusType), + CUtil::MakeLegalFileName(safeDeviceName, LEGAL_WIN32_COMPAT)); + } + else + { + // Backwards compatibility - old settings files didn't include the device name + m_strSettingsFile = StringUtils::Format( + "special://profile/peripheral_data/{}_{}_{}.xml", + PeripheralTypeTranslator::BusTypeToString(m_mappedBusType), m_strVendorId, m_strProductId); + + if (!CFileUtils::Exists(m_strSettingsFile)) + m_strSettingsFile = StringUtils::Format( + "special://profile/peripheral_data/{}_{}_{}_{}.xml", + PeripheralTypeTranslator::BusTypeToString(m_mappedBusType), m_strVendorId, m_strProductId, + CUtil::MakeLegalFileName(safeDeviceName, LEGAL_WIN32_COMPAT)); + } + + LoadPersistedSettings(); + + for (unsigned int iFeaturePtr = 0; iFeaturePtr < m_features.size(); iFeaturePtr++) + { + PeripheralFeature feature = m_features.at(iFeaturePtr); + bReturn &= InitialiseFeature(feature); + } + + for (unsigned int iSubdevicePtr = 0; iSubdevicePtr < m_subDevices.size(); iSubdevicePtr++) + bReturn &= m_subDevices.at(iSubdevicePtr)->Initialise(); + + if (bReturn) + { + CLog::Log(LOGDEBUG, "{} - initialised peripheral on '{}' with {} features and {} sub devices", + __FUNCTION__, m_strLocation, (int)m_features.size(), (int)m_subDevices.size()); + m_bInitialised = true; + } + + return bReturn; +} + +void CPeripheral::GetSubdevices(PeripheralVector& subDevices) const +{ + subDevices = m_subDevices; +} + +bool CPeripheral::IsMultiFunctional(void) const +{ + return m_subDevices.size() > 0; +} + +std::vector<std::shared_ptr<CSetting>> CPeripheral::GetSettings(void) const +{ + std::vector<PeripheralDeviceSetting> tmpSettings; + tmpSettings.reserve(m_settings.size()); + for (const auto& it : m_settings) + tmpSettings.push_back(it.second); + sort(tmpSettings.begin(), tmpSettings.end(), SortBySettingsOrder()); + + std::vector<std::shared_ptr<CSetting>> settings; + settings.reserve(tmpSettings.size()); + for (const auto& it : tmpSettings) + settings.push_back(it.m_setting); + return settings; +} + +void CPeripheral::AddSetting(const std::string& strKey, const SettingConstPtr& setting, int order) +{ + if (!setting) + { + CLog::Log(LOGERROR, "{} - invalid setting", __FUNCTION__); + return; + } + + if (!HasSetting(strKey)) + { + PeripheralDeviceSetting deviceSetting = {NULL, order}; + switch (setting->GetType()) + { + case SettingType::Boolean: + { + std::shared_ptr<const CSettingBool> mappedSetting = + std::static_pointer_cast<const CSettingBool>(setting); + std::shared_ptr<CSettingBool> boolSetting = + std::make_shared<CSettingBool>(strKey, *mappedSetting); + if (boolSetting) + { + boolSetting->SetVisible(mappedSetting->IsVisible()); + deviceSetting.m_setting = boolSetting; + } + } + break; + case SettingType::Integer: + { + std::shared_ptr<const CSettingInt> mappedSetting = + std::static_pointer_cast<const CSettingInt>(setting); + std::shared_ptr<CSettingInt> intSetting = + std::make_shared<CSettingInt>(strKey, *mappedSetting); + if (intSetting) + { + intSetting->SetVisible(mappedSetting->IsVisible()); + deviceSetting.m_setting = intSetting; + } + } + break; + case SettingType::Number: + { + std::shared_ptr<const CSettingNumber> mappedSetting = + std::static_pointer_cast<const CSettingNumber>(setting); + std::shared_ptr<CSettingNumber> floatSetting = + std::make_shared<CSettingNumber>(strKey, *mappedSetting); + if (floatSetting) + { + floatSetting->SetVisible(mappedSetting->IsVisible()); + deviceSetting.m_setting = floatSetting; + } + } + break; + case SettingType::String: + { + if (std::dynamic_pointer_cast<const CSettingAddon>(setting)) + { + std::shared_ptr<const CSettingAddon> mappedSetting = + std::static_pointer_cast<const CSettingAddon>(setting); + std::shared_ptr<CSettingAddon> addonSetting = + std::make_shared<CSettingAddon>(strKey, *mappedSetting); + addonSetting->SetVisible(mappedSetting->IsVisible()); + deviceSetting.m_setting = addonSetting; + } + else + { + std::shared_ptr<const CSettingString> mappedSetting = + std::static_pointer_cast<const CSettingString>(setting); + std::shared_ptr<CSettingString> stringSetting = + std::make_shared<CSettingString>(strKey, *mappedSetting); + stringSetting->SetVisible(mappedSetting->IsVisible()); + deviceSetting.m_setting = stringSetting; + } + } + break; + default: + //! @todo add more types if needed + break; + } + + if (deviceSetting.m_setting != NULL) + m_settings.insert(make_pair(strKey, deviceSetting)); + } +} + +bool CPeripheral::HasSetting(const std::string& strKey) const +{ + std::map<std::string, PeripheralDeviceSetting>::const_iterator it = m_settings.find(strKey); + return it != m_settings.end(); +} + +bool CPeripheral::HasSettings(void) const +{ + return !m_settings.empty(); +} + +bool CPeripheral::HasConfigurableSettings(void) const +{ + bool bReturn(false); + std::map<std::string, PeripheralDeviceSetting>::const_iterator it = m_settings.begin(); + while (it != m_settings.end() && !bReturn) + { + if ((*it).second.m_setting->IsVisible()) + { + bReturn = true; + break; + } + + ++it; + } + + return bReturn; +} + +bool CPeripheral::GetSettingBool(const std::string& strKey) const +{ + std::map<std::string, PeripheralDeviceSetting>::const_iterator it = m_settings.find(strKey); + if (it != m_settings.end() && (*it).second.m_setting->GetType() == SettingType::Boolean) + { + std::shared_ptr<CSettingBool> boolSetting = + std::static_pointer_cast<CSettingBool>((*it).second.m_setting); + if (boolSetting) + return boolSetting->GetValue(); + } + + return false; +} + +int CPeripheral::GetSettingInt(const std::string& strKey) const +{ + std::map<std::string, PeripheralDeviceSetting>::const_iterator it = m_settings.find(strKey); + if (it != m_settings.end() && (*it).second.m_setting->GetType() == SettingType::Integer) + { + std::shared_ptr<CSettingInt> intSetting = + std::static_pointer_cast<CSettingInt>((*it).second.m_setting); + if (intSetting) + return intSetting->GetValue(); + } + + return 0; +} + +float CPeripheral::GetSettingFloat(const std::string& strKey) const +{ + std::map<std::string, PeripheralDeviceSetting>::const_iterator it = m_settings.find(strKey); + if (it != m_settings.end() && (*it).second.m_setting->GetType() == SettingType::Number) + { + std::shared_ptr<CSettingNumber> floatSetting = + std::static_pointer_cast<CSettingNumber>((*it).second.m_setting); + if (floatSetting) + return (float)floatSetting->GetValue(); + } + + return 0; +} + +const std::string CPeripheral::GetSettingString(const std::string& strKey) const +{ + std::map<std::string, PeripheralDeviceSetting>::const_iterator it = m_settings.find(strKey); + if (it != m_settings.end() && (*it).second.m_setting->GetType() == SettingType::String) + { + std::shared_ptr<CSettingString> stringSetting = + std::static_pointer_cast<CSettingString>((*it).second.m_setting); + if (stringSetting) + return stringSetting->GetValue(); + } + + return ""; +} + +bool CPeripheral::SetSetting(const std::string& strKey, bool bValue) +{ + bool bChanged(false); + std::map<std::string, PeripheralDeviceSetting>::iterator it = m_settings.find(strKey); + if (it != m_settings.end() && (*it).second.m_setting->GetType() == SettingType::Boolean) + { + std::shared_ptr<CSettingBool> boolSetting = + std::static_pointer_cast<CSettingBool>((*it).second.m_setting); + if (boolSetting) + { + bChanged = boolSetting->GetValue() != bValue; + boolSetting->SetValue(bValue); + if (bChanged && m_bInitialised) + m_changedSettings.insert(strKey); + } + } + return bChanged; +} + +bool CPeripheral::SetSetting(const std::string& strKey, int iValue) +{ + bool bChanged(false); + std::map<std::string, PeripheralDeviceSetting>::iterator it = m_settings.find(strKey); + if (it != m_settings.end() && (*it).second.m_setting->GetType() == SettingType::Integer) + { + std::shared_ptr<CSettingInt> intSetting = + std::static_pointer_cast<CSettingInt>((*it).second.m_setting); + if (intSetting) + { + bChanged = intSetting->GetValue() != iValue; + intSetting->SetValue(iValue); + if (bChanged && m_bInitialised) + m_changedSettings.insert(strKey); + } + } + return bChanged; +} + +bool CPeripheral::SetSetting(const std::string& strKey, float fValue) +{ + bool bChanged(false); + std::map<std::string, PeripheralDeviceSetting>::iterator it = m_settings.find(strKey); + if (it != m_settings.end() && (*it).second.m_setting->GetType() == SettingType::Number) + { + std::shared_ptr<CSettingNumber> floatSetting = + std::static_pointer_cast<CSettingNumber>((*it).second.m_setting); + if (floatSetting) + { + bChanged = floatSetting->GetValue() != static_cast<double>(fValue); + floatSetting->SetValue(static_cast<double>(fValue)); + if (bChanged && m_bInitialised) + m_changedSettings.insert(strKey); + } + } + return bChanged; +} + +void CPeripheral::SetSettingVisible(const std::string& strKey, bool bSetTo) +{ + std::map<std::string, PeripheralDeviceSetting>::iterator it = m_settings.find(strKey); + if (it != m_settings.end()) + (*it).second.m_setting->SetVisible(bSetTo); +} + +bool CPeripheral::IsSettingVisible(const std::string& strKey) const +{ + std::map<std::string, PeripheralDeviceSetting>::const_iterator it = m_settings.find(strKey); + if (it != m_settings.end()) + return (*it).second.m_setting->IsVisible(); + return false; +} + +bool CPeripheral::SetSetting(const std::string& strKey, const std::string& strValue) +{ + bool bChanged(false); + std::map<std::string, PeripheralDeviceSetting>::iterator it = m_settings.find(strKey); + if (it != m_settings.end()) + { + if ((*it).second.m_setting->GetType() == SettingType::String) + { + std::shared_ptr<CSettingString> stringSetting = + std::static_pointer_cast<CSettingString>((*it).second.m_setting); + if (stringSetting) + { + bChanged = !StringUtils::EqualsNoCase(stringSetting->GetValue(), strValue); + stringSetting->SetValue(strValue); + if (bChanged && m_bInitialised) + m_changedSettings.insert(strKey); + } + } + else if ((*it).second.m_setting->GetType() == SettingType::Integer) + bChanged = SetSetting(strKey, strValue.empty() ? 0 : atoi(strValue.c_str())); + else if ((*it).second.m_setting->GetType() == SettingType::Number) + bChanged = SetSetting(strKey, (float)(strValue.empty() ? 0 : atof(strValue.c_str()))); + else if ((*it).second.m_setting->GetType() == SettingType::Boolean) + bChanged = SetSetting(strKey, strValue == "1"); + } + return bChanged; +} + +void CPeripheral::PersistSettings(bool bExiting /* = false */) +{ + CXBMCTinyXML doc; + TiXmlElement node("settings"); + doc.InsertEndChild(node); + for (const auto& itr : m_settings) + { + TiXmlElement nodeSetting("setting"); + nodeSetting.SetAttribute("id", itr.first.c_str()); + std::string strValue; + switch (itr.second.m_setting->GetType()) + { + case SettingType::String: + { + std::shared_ptr<CSettingString> stringSetting = + std::static_pointer_cast<CSettingString>(itr.second.m_setting); + if (stringSetting) + strValue = stringSetting->GetValue(); + } + break; + case SettingType::Integer: + { + std::shared_ptr<CSettingInt> intSetting = + std::static_pointer_cast<CSettingInt>(itr.second.m_setting); + if (intSetting) + strValue = std::to_string(intSetting->GetValue()); + } + break; + case SettingType::Number: + { + std::shared_ptr<CSettingNumber> floatSetting = + std::static_pointer_cast<CSettingNumber>(itr.second.m_setting); + if (floatSetting) + strValue = StringUtils::Format("{:.2f}", floatSetting->GetValue()); + } + break; + case SettingType::Boolean: + { + std::shared_ptr<CSettingBool> boolSetting = + std::static_pointer_cast<CSettingBool>(itr.second.m_setting); + if (boolSetting) + strValue = std::to_string(boolSetting->GetValue() ? 1 : 0); + } + break; + default: + break; + } + nodeSetting.SetAttribute("value", strValue.c_str()); + doc.RootElement()->InsertEndChild(nodeSetting); + } + + doc.SaveFile(m_strSettingsFile); + + if (!bExiting) + { + for (const auto& it : m_changedSettings) + OnSettingChanged(it); + } + m_changedSettings.clear(); +} + +void CPeripheral::LoadPersistedSettings(void) +{ + CXBMCTinyXML doc; + if (doc.LoadFile(m_strSettingsFile)) + { + const TiXmlElement* setting = doc.RootElement()->FirstChildElement("setting"); + while (setting) + { + std::string strId = XMLUtils::GetAttribute(setting, "id"); + std::string strValue = XMLUtils::GetAttribute(setting, "value"); + SetSetting(strId, strValue); + + setting = setting->NextSiblingElement("setting"); + } + } +} + +void CPeripheral::ResetDefaultSettings(void) +{ + ClearSettings(); + m_manager.GetSettingsFromMapping(*this); + + std::map<std::string, PeripheralDeviceSetting>::iterator it = m_settings.begin(); + while (it != m_settings.end()) + { + m_changedSettings.insert((*it).first); + ++it; + } + + PersistSettings(); +} + +void CPeripheral::ClearSettings(void) +{ + m_settings.clear(); +} + +void CPeripheral::RegisterInputHandler(IInputHandler* handler, bool bPromiscuous) +{ + auto it = m_inputHandlers.find(handler); + if (it == m_inputHandlers.end()) + { + PeripheralAddonPtr addon = m_manager.GetAddonWithButtonMap(this); + if (addon) + { + std::unique_ptr<CAddonInputHandling> addonInput = std::make_unique<CAddonInputHandling>( + m_manager, this, std::move(addon), handler, GetDriverReceiver()); + if (addonInput->Load()) + { + RegisterJoystickDriverHandler(addonInput.get(), bPromiscuous); + m_inputHandlers[handler] = std::move(addonInput); + } + } + else + { + CLog::Log(LOGDEBUG, "Failed to locate add-on for \"{}\"", m_strLocation); + } + } +} + +void CPeripheral::UnregisterInputHandler(IInputHandler* handler) +{ + handler->ResetInputReceiver(); + + auto it = m_inputHandlers.find(handler); + if (it != m_inputHandlers.end()) + { + UnregisterJoystickDriverHandler(it->second.get()); + m_inputHandlers.erase(it); + } +} + +void CPeripheral::RegisterKeyboardHandler(KEYBOARD::IKeyboardInputHandler* handler, + bool bPromiscuous) +{ + auto it = m_keyboardHandlers.find(handler); + if (it == m_keyboardHandlers.end()) + { + std::unique_ptr<KODI::KEYBOARD::IKeyboardDriverHandler> keyboardDriverHandler; + + PeripheralAddonPtr addon = m_manager.GetAddonWithButtonMap(this); + if (addon) + { + std::unique_ptr<CAddonInputHandling> addonInput = + std::make_unique<CAddonInputHandling>(m_manager, this, std::move(addon), handler); + if (addonInput->Load()) + keyboardDriverHandler = std::move(addonInput); + } + else + { + CLog::Log(LOGDEBUG, "Failed to locate add-on for \"{}\"", m_strLocation); + } + + if (keyboardDriverHandler) + { + RegisterKeyboardDriverHandler(keyboardDriverHandler.get(), bPromiscuous); + m_keyboardHandlers[handler] = std::move(keyboardDriverHandler); + } + } +} + +void CPeripheral::UnregisterKeyboardHandler(KEYBOARD::IKeyboardInputHandler* handler) +{ + auto it = m_keyboardHandlers.find(handler); + if (it != m_keyboardHandlers.end()) + { + UnregisterKeyboardDriverHandler(it->second.get()); + m_keyboardHandlers.erase(it); + } +} + +void CPeripheral::RegisterMouseHandler(MOUSE::IMouseInputHandler* handler, bool bPromiscuous) +{ + auto it = m_mouseHandlers.find(handler); + if (it == m_mouseHandlers.end()) + { + std::unique_ptr<KODI::MOUSE::IMouseDriverHandler> mouseDriverHandler; + + PeripheralAddonPtr addon = m_manager.GetAddonWithButtonMap(this); + if (addon) + { + std::unique_ptr<CAddonInputHandling> addonInput = + std::make_unique<CAddonInputHandling>(m_manager, this, std::move(addon), handler); + if (addonInput->Load()) + mouseDriverHandler = std::move(addonInput); + } + else + { + CLog::Log(LOGDEBUG, "Failed to locate add-on for \"{}\"", m_strLocation); + } + + if (mouseDriverHandler) + { + RegisterMouseDriverHandler(mouseDriverHandler.get(), bPromiscuous); + m_mouseHandlers[handler] = std::move(mouseDriverHandler); + } + } +} + +void CPeripheral::UnregisterMouseHandler(MOUSE::IMouseInputHandler* handler) +{ + auto it = m_mouseHandlers.find(handler); + if (it != m_mouseHandlers.end()) + { + UnregisterMouseDriverHandler(it->second.get()); + m_mouseHandlers.erase(it); + } +} + +void CPeripheral::RegisterJoystickButtonMapper(IButtonMapper* mapper) +{ + auto it = m_buttonMappers.find(mapper); + if (it == m_buttonMappers.end()) + { + std::unique_ptr<CAddonButtonMapping> addonMapping( + new CAddonButtonMapping(m_manager, this, mapper)); + + RegisterJoystickDriverHandler(addonMapping.get(), false); + RegisterKeyboardDriverHandler(addonMapping.get(), false); + RegisterMouseDriverHandler(addonMapping.get(), false); + + m_buttonMappers[mapper] = std::move(addonMapping); + } +} + +void CPeripheral::UnregisterJoystickButtonMapper(IButtonMapper* mapper) +{ + auto it = m_buttonMappers.find(mapper); + if (it != m_buttonMappers.end()) + { + UnregisterMouseDriverHandler(it->second.get()); + UnregisterKeyboardDriverHandler(it->second.get()); + UnregisterJoystickDriverHandler(it->second.get()); + + m_buttonMappers.erase(it); + } +} + +std::string CPeripheral::GetIcon() const +{ + std::string icon; + + // Try controller profile + const GAME::ControllerPtr controller = ControllerProfile(); + if (controller) + icon = controller->Layout().ImagePath(); + + // Try add-on + if (icon.empty() && m_busType == PERIPHERAL_BUS_ADDON) + { + CPeripheralBusAddon* bus = static_cast<CPeripheralBusAddon*>(m_bus); + + PeripheralAddonPtr addon; + unsigned int index; + if (bus->SplitLocation(m_strLocation, addon, index)) + { + std::string addonIcon = addon->Icon(); + if (!addonIcon.empty()) + icon = std::move(addonIcon); + } + } + + // Fallback + if (icon.empty()) + icon = "DefaultAddon.png"; + + return icon; +} + +bool CPeripheral::operator==(const PeripheralScanResult& right) const +{ + return StringUtils::EqualsNoCase(m_strLocation, right.m_strLocation); +} + +bool CPeripheral::operator!=(const PeripheralScanResult& right) const +{ + return !(*this == right); +} + +CDateTime CPeripheral::LastActive() +{ + return CDateTime(); +} diff --git a/xbmc/peripherals/devices/Peripheral.h b/xbmc/peripherals/devices/Peripheral.h new file mode 100644 index 0000000..8c4049b --- /dev/null +++ b/xbmc/peripherals/devices/Peripheral.h @@ -0,0 +1,306 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "games/controllers/ControllerTypes.h" +#include "input/joysticks/interfaces/IInputProvider.h" +#include "input/keyboard/interfaces/IKeyboardInputProvider.h" +#include "input/mouse/interfaces/IMouseInputProvider.h" +#include "peripherals/PeripheralTypes.h" + +#include <map> +#include <set> +#include <string> +#include <vector> + +class TiXmlDocument; +class CDateTime; +class CSetting; +class IKeymap; + +namespace KODI +{ +namespace JOYSTICK +{ +class IButtonMapper; +class IDriverHandler; +class IDriverReceiver; +class IInputHandler; +} // namespace JOYSTICK + +namespace KEYBOARD +{ +class IKeyboardDriverHandler; +} + +namespace MOUSE +{ +class IMouseDriverHandler; +} +} // namespace KODI + +namespace PERIPHERALS +{ +class CAddonButtonMapping; +class CGUIDialogPeripheralSettings; +class CPeripheralBus; +class CPeripherals; + +typedef enum +{ + STATE_SWITCH_TOGGLE, + STATE_ACTIVATE_SOURCE, + STATE_STANDBY +} CecStateChange; + +class CPeripheral : public KODI::JOYSTICK::IInputProvider, + public KODI::KEYBOARD::IKeyboardInputProvider, + public KODI::MOUSE::IMouseInputProvider +{ + friend class CGUIDialogPeripheralSettings; + +public: + CPeripheral(CPeripherals& manager, const PeripheralScanResult& scanResult, CPeripheralBus* bus); + ~CPeripheral(void) override; + + bool operator==(const CPeripheral& right) const; + bool operator!=(const CPeripheral& right) const; + bool operator==(const PeripheralScanResult& right) const; + bool operator!=(const PeripheralScanResult& right) const; + + const std::string& FileLocation(void) const { return m_strFileLocation; } + const std::string& Location(void) const { return m_strLocation; } + int VendorId(void) const { return m_iVendorId; } + const char* VendorIdAsString(void) const { return m_strVendorId.c_str(); } + int ProductId(void) const { return m_iProductId; } + const char* ProductIdAsString(void) const { return m_strProductId.c_str(); } + PeripheralType Type(void) const { return m_type; } + PeripheralBusType GetBusType(void) const { return m_busType; } + const std::string& DeviceName(void) const { return m_strDeviceName; } + bool IsHidden(void) const { return m_bHidden; } + void SetHidden(bool bSetTo = true) { m_bHidden = bSetTo; } + const std::string& GetVersionInfo(void) const { return m_strVersionInfo; } + + /*! + * @brief Get an icon for this peripheral + * @return Path to an icon, or skin icon file name + */ + virtual std::string GetIcon() const; + + /*! + * @brief Check whether this device has the given feature. + * @param feature The feature to check for. + * @return True when the device has the feature, false otherwise. + */ + bool HasFeature(const PeripheralFeature feature) const; + + /*! + * @brief Get all features that are supported by this device. + * @param features The features. + */ + void GetFeatures(std::vector<PeripheralFeature>& features) const; + + /*! + * @brief Initialises the peripheral. + * @return True when the peripheral has been initialised successfully, false otherwise. + */ + bool Initialise(void); + + /*! + * @brief Initialise one of the features of this peripheral. + * @param feature The feature to initialise. + * @return True when the feature has been initialised successfully, false otherwise. + */ + virtual bool InitialiseFeature(const PeripheralFeature feature) { return true; } + + /*! + * @brief Briefly activate a feature to notify the user + */ + virtual void OnUserNotification() {} + + /*! + * @brief Briefly test one of the features of this peripheral. + * @param feature The feature to test. + * @return True if the test succeeded, false otherwise. + */ + virtual bool TestFeature(PeripheralFeature feature) { return false; } + + /*! + * @brief Called when a setting changed. + * @param strChangedSetting The changed setting. + */ + virtual void OnSettingChanged(const std::string& strChangedSetting) {} + + /*! + * @brief Called when this device is removed, before calling the destructor. + */ + virtual void OnDeviceRemoved(void) {} + + /*! + * @brief Get all subdevices if this device is multifunctional. + * @param subDevices The subdevices. + */ + virtual void GetSubdevices(PeripheralVector& subDevices) const; + + /*! + * @return True when this device is multifunctional, false otherwise. + */ + virtual bool IsMultiFunctional(void) const; + + /*! + * @brief Add a setting to this peripheral. This will overwrite a previous setting with the same + * key. + * @param strKey The key of the setting. + * @param setting The setting. + */ + virtual void AddSetting(const std::string& strKey, + const std::shared_ptr<const CSetting>& setting, + int order); + + /*! + * @brief Check whether a setting is known with the given key. + * @param strKey The key to search. + * @return True when found, false otherwise. + */ + virtual bool HasSetting(const std::string& strKey) const; + + /*! + * @return True when this device has any settings, false otherwise. + */ + virtual bool HasSettings(void) const; + + /*! + * @return True when this device has any configurable settings, false otherwise. + */ + virtual bool HasConfigurableSettings(void) const; + + /*! + * @brief Get the value of a setting. + * @param strKey The key to search. + * @return The value or an empty string if it wasn't found. + */ + virtual const std::string GetSettingString(const std::string& strKey) const; + virtual bool SetSetting(const std::string& strKey, const std::string& strValue); + virtual void SetSettingVisible(const std::string& strKey, bool bSetTo); + virtual bool IsSettingVisible(const std::string& strKey) const; + + virtual int GetSettingInt(const std::string& strKey) const; + virtual bool SetSetting(const std::string& strKey, int iValue); + + virtual bool GetSettingBool(const std::string& strKey) const; + virtual bool SetSetting(const std::string& strKey, bool bValue); + + virtual float GetSettingFloat(const std::string& strKey) const; + virtual bool SetSetting(const std::string& strKey, float fValue); + + virtual void PersistSettings(bool bExiting = false); + virtual void LoadPersistedSettings(void); + virtual void ResetDefaultSettings(void); + + virtual std::vector<std::shared_ptr<CSetting>> GetSettings(void) const; + + virtual bool ErrorOccured(void) const { return m_bError; } + + virtual void RegisterJoystickDriverHandler(KODI::JOYSTICK::IDriverHandler* handler, + bool bPromiscuous) + { + } + virtual void UnregisterJoystickDriverHandler(KODI::JOYSTICK::IDriverHandler* handler) {} + + virtual void RegisterKeyboardDriverHandler(KODI::KEYBOARD::IKeyboardDriverHandler* handler, + bool bPromiscuous) + { + } + virtual void UnregisterKeyboardDriverHandler(KODI::KEYBOARD::IKeyboardDriverHandler* handler) {} + + virtual void RegisterMouseDriverHandler(KODI::MOUSE::IMouseDriverHandler* handler, + bool bPromiscuous) + { + } + virtual void UnregisterMouseDriverHandler(KODI::MOUSE::IMouseDriverHandler* handler) {} + + // implementation of IInputProvider + void RegisterInputHandler(KODI::JOYSTICK::IInputHandler* handler, bool bPromiscuous) override; + void UnregisterInputHandler(KODI::JOYSTICK::IInputHandler* handler) override; + + // implementation of IKeyboardInputProvider + void RegisterKeyboardHandler(KODI::KEYBOARD::IKeyboardInputHandler* handler, + bool bPromiscuous) override; + void UnregisterKeyboardHandler(KODI::KEYBOARD::IKeyboardInputHandler* handler) override; + + // implementation of IMouseInputProvider + void RegisterMouseHandler(KODI::MOUSE::IMouseInputHandler* handler, bool bPromiscuous) override; + void UnregisterMouseHandler(KODI::MOUSE::IMouseInputHandler* handler) override; + + virtual void RegisterJoystickButtonMapper(KODI::JOYSTICK::IButtonMapper* mapper); + virtual void UnregisterJoystickButtonMapper(KODI::JOYSTICK::IButtonMapper* mapper); + + virtual KODI::JOYSTICK::IDriverReceiver* GetDriverReceiver() { return nullptr; } + + virtual IKeymap* GetKeymap(const std::string& controllerId) { return nullptr; } + + /*! + * \brief Return the last time this peripheral was active + * + * \return The time of last activation, or invalid if unknown/never active + */ + virtual CDateTime LastActive(); + + /*! + * \brief Get the controller profile that best represents this peripheral + * + * \return The controller profile, or empty if unknown + */ + virtual KODI::GAME::ControllerPtr ControllerProfile() const { return m_controllerProfile; } + + /*! + * \brief Set the controller profile for this peripheral + * + * \param controller The new controller profile + */ + virtual void SetControllerProfile(const KODI::GAME::ControllerPtr& controller) + { + m_controllerProfile = controller; + } + +protected: + virtual void ClearSettings(void); + + CPeripherals& m_manager; + PeripheralType m_type; + PeripheralBusType m_busType; + PeripheralBusType m_mappedBusType; + std::string m_strLocation; + std::string m_strDeviceName; + std::string m_strSettingsFile; + std::string m_strFileLocation; + int m_iVendorId; + std::string m_strVendorId; + int m_iProductId; + std::string m_strProductId; + std::string m_strVersionInfo; + bool m_bInitialised; + bool m_bHidden; + bool m_bError; + std::vector<PeripheralFeature> m_features; + PeripheralVector m_subDevices; + std::map<std::string, PeripheralDeviceSetting> m_settings; + std::set<std::string> m_changedSettings; + CPeripheralBus* m_bus; + std::map<KODI::JOYSTICK::IInputHandler*, std::unique_ptr<KODI::JOYSTICK::IDriverHandler>> + m_inputHandlers; + std::map<KODI::KEYBOARD::IKeyboardInputHandler*, + std::unique_ptr<KODI::KEYBOARD::IKeyboardDriverHandler>> + m_keyboardHandlers; + std::map<KODI::MOUSE::IMouseInputHandler*, std::unique_ptr<KODI::MOUSE::IMouseDriverHandler>> + m_mouseHandlers; + std::map<KODI::JOYSTICK::IButtonMapper*, std::unique_ptr<CAddonButtonMapping>> m_buttonMappers; + KODI::GAME::ControllerPtr m_controllerProfile; +}; +} // namespace PERIPHERALS diff --git a/xbmc/peripherals/devices/PeripheralBluetooth.cpp b/xbmc/peripherals/devices/PeripheralBluetooth.cpp new file mode 100644 index 0000000..6f3802d --- /dev/null +++ b/xbmc/peripherals/devices/PeripheralBluetooth.cpp @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PeripheralBluetooth.h" + +using namespace PERIPHERALS; + +CPeripheralBluetooth::CPeripheralBluetooth(CPeripherals& manager, + const PeripheralScanResult& scanResult, + CPeripheralBus* bus) + : CPeripheral(manager, scanResult, bus) +{ + m_features.push_back(FEATURE_BLUETOOTH); +} diff --git a/xbmc/peripherals/devices/PeripheralBluetooth.h b/xbmc/peripherals/devices/PeripheralBluetooth.h new file mode 100644 index 0000000..11fa7ca --- /dev/null +++ b/xbmc/peripherals/devices/PeripheralBluetooth.h @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "Peripheral.h" + +namespace PERIPHERALS +{ +class CPeripheralBluetooth : public CPeripheral +{ +public: + CPeripheralBluetooth(CPeripherals& manager, + const PeripheralScanResult& scanResult, + CPeripheralBus* bus); + ~CPeripheralBluetooth(void) override = default; +}; +} // namespace PERIPHERALS diff --git a/xbmc/peripherals/devices/PeripheralCecAdapter.cpp b/xbmc/peripherals/devices/PeripheralCecAdapter.cpp new file mode 100644 index 0000000..bc62bea --- /dev/null +++ b/xbmc/peripherals/devices/PeripheralCecAdapter.cpp @@ -0,0 +1,1861 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PeripheralCecAdapter.h" + +#include "ServiceBroker.h" +#include "application/ApplicationComponents.h" +#include "application/ApplicationEnums.h" +#include "application/ApplicationPlayer.h" +#include "application/ApplicationPowerHandling.h" +#include "application/ApplicationVolumeHandling.h" +#include "dialogs/GUIDialogKaiToast.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/LocalizeStrings.h" +#include "input/actions/Action.h" +#include "input/actions/ActionIDs.h" +#include "input/remote/IRRemote.h" +#include "messaging/ApplicationMessenger.h" +#include "pictures/GUIWindowSlideShow.h" +#include "utils/JobManager.h" +#include "utils/Variant.h" +#include "utils/log.h" +#include "xbmc/interfaces/AnnouncementManager.h" + +#include <mutex> + +#include <libcec/cec.h> + +using namespace PERIPHERALS; +using namespace CEC; +using namespace ANNOUNCEMENT; +using namespace std::chrono_literals; + +#define CEC_LIB_SUPPORTED_VERSION LIBCEC_VERSION_TO_UINT(4, 0, 0) + +/* time in seconds to ignore standby commands from devices after the screensaver has been activated + */ +#define SCREENSAVER_TIMEOUT 20 +#define VOLUME_CHANGE_TIMEOUT 250 +#define VOLUME_REFRESH_TIMEOUT 100 + +#define LOCALISED_ID_TV 36037 +#define LOCALISED_ID_AVR 36038 +#define LOCALISED_ID_TV_AVR 36039 +#define LOCALISED_ID_STOP 36044 +#define LOCALISED_ID_PAUSE 36045 +#define LOCALISED_ID_POWEROFF 13005 +#define LOCALISED_ID_SUSPEND 13011 +#define LOCALISED_ID_HIBERNATE 13010 +#define LOCALISED_ID_QUIT 13009 +#define LOCALISED_ID_IGNORE 36028 +#define LOCALISED_ID_RECORDING_DEVICE 36051 +#define LOCALISED_ID_PLAYBACK_DEVICE 36052 +#define LOCALISED_ID_TUNER_DEVICE 36053 + +#define LOCALISED_ID_NONE 231 + +/* time in seconds to suppress source activation after receiving OnStop */ +#define CEC_SUPPRESS_ACTIVATE_SOURCE_AFTER_ON_STOP 2 + +CPeripheralCecAdapter::CPeripheralCecAdapter(CPeripherals& manager, + const PeripheralScanResult& scanResult, + CPeripheralBus* bus) + : CPeripheralHID(manager, scanResult, bus), CThread("CECAdapter"), m_cecAdapter(NULL) +{ + ResetMembers(); + m_features.push_back(FEATURE_CEC); + m_strComPort = scanResult.m_strLocation; +} + +CPeripheralCecAdapter::~CPeripheralCecAdapter(void) +{ + { + std::unique_lock<CCriticalSection> lock(m_critSection); + CServiceBroker::GetAnnouncementManager()->RemoveAnnouncer(this); + m_bStop = true; + } + + StopThread(true); + delete m_queryThread; + + if (m_cecAdapter) + { + CECDestroy(m_cecAdapter); + m_cecAdapter = NULL; + } +} + +void CPeripheralCecAdapter::ResetMembers(void) +{ + if (m_cecAdapter) + CECDestroy(m_cecAdapter); + m_cecAdapter = NULL; + m_bStarted = false; + m_bHasButton = false; + m_bIsReady = false; + m_bHasConnectedAudioSystem = false; + m_strMenuLanguage = "???"; + m_lastKeypress = {}; + m_lastChange = VOLUME_CHANGE_NONE; + m_iExitCode = EXITCODE_QUIT; + + //! @todo fetch the correct initial value when system audiostatus is + //! implemented in libCEC + m_bIsMuted = false; + + m_bGoingToStandby = false; + m_bIsRunning = false; + m_bDeviceRemoved = false; + m_bActiveSourcePending = false; + m_bStandbyPending = false; + m_bActiveSourceBeforeStandby = false; + m_bOnPlayReceived = false; + m_bPlaybackPaused = false; + m_queryThread = NULL; + m_bPowerOnScreensaver = false; + m_bUseTVMenuLanguage = false; + m_bSendInactiveSource = false; + m_bPowerOffScreensaver = false; + m_bShutdownOnStandby = false; + + m_currentButton.iButton = 0; + m_currentButton.iDuration = 0; + m_standbySent.SetValid(false); + m_configuration.Clear(); +} + +void CPeripheralCecAdapter::Announce(ANNOUNCEMENT::AnnouncementFlag flag, + const std::string& sender, + const std::string& message, + const CVariant& data) +{ + if (flag == ANNOUNCEMENT::System && sender == CAnnouncementManager::ANNOUNCEMENT_SENDER && + message == "OnQuit" && m_bIsReady) + { + std::unique_lock<CCriticalSection> lock(m_critSection); + m_iExitCode = static_cast<int>(data["exitcode"].asInteger(EXITCODE_QUIT)); + CServiceBroker::GetAnnouncementManager()->RemoveAnnouncer(this); + StopThread(false); + } + else if (flag == ANNOUNCEMENT::GUI && sender == CAnnouncementManager::ANNOUNCEMENT_SENDER && + message == "OnScreensaverDeactivated" && m_bIsReady) + { + bool bIgnoreDeactivate(false); + if (data["shuttingdown"].isBoolean()) + { + // don't respond to the deactivation if we are just going to suspend/shutdown anyway + // the tv will not have time to switch on before being told to standby and + // may not action the standby command. + bIgnoreDeactivate = data["shuttingdown"].asBoolean(); + if (bIgnoreDeactivate) + CLog::Log(LOGDEBUG, "{} - ignoring OnScreensaverDeactivated for power action", + __FUNCTION__); + } + if (m_bPowerOnScreensaver && !bIgnoreDeactivate && m_configuration.bActivateSource) + { + ActivateSource(); + } + } + else if (flag == ANNOUNCEMENT::GUI && sender == CAnnouncementManager::ANNOUNCEMENT_SENDER && + message == "OnScreensaverActivated" && m_bIsReady) + { + // Don't put devices to standby if application is currently playing + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + if (!appPlayer->IsPlaying() && m_bPowerOffScreensaver) + { + // only power off when we're the active source + if (m_cecAdapter->IsLibCECActiveSource()) + StandbyDevices(); + } + } + else if (flag == ANNOUNCEMENT::System && sender == CAnnouncementManager::ANNOUNCEMENT_SENDER && + message == "OnSleep") + { + // this will also power off devices when we're the active source + { + std::unique_lock<CCriticalSection> lock(m_critSection); + m_bGoingToStandby = true; + } + StopThread(); + } + else if (flag == ANNOUNCEMENT::System && sender == CAnnouncementManager::ANNOUNCEMENT_SENDER && + message == "OnWake") + { + CLog::Log(LOGDEBUG, "{} - reconnecting to the CEC adapter after standby mode", __FUNCTION__); + if (ReopenConnection()) + { + bool bActivate(false); + { + std::unique_lock<CCriticalSection> lock(m_critSection); + bActivate = m_bActiveSourceBeforeStandby; + m_bActiveSourceBeforeStandby = false; + } + if (bActivate) + ActivateSource(); + } + } + else if (flag == ANNOUNCEMENT::Player && sender == CAnnouncementManager::ANNOUNCEMENT_SENDER && + message == "OnStop") + { + std::unique_lock<CCriticalSection> lock(m_critSection); + m_preventActivateSourceOnPlay = CDateTime::GetCurrentDateTime(); + m_bOnPlayReceived = false; + } + else if (flag == ANNOUNCEMENT::Player && sender == CAnnouncementManager::ANNOUNCEMENT_SENDER && + (message == "OnPlay" || message == "OnResume")) + { + // activate the source when playback started, and the option is enabled + bool bActivateSource(false); + { + std::unique_lock<CCriticalSection> lock(m_critSection); + bActivateSource = (m_configuration.bActivateSource && !m_bOnPlayReceived && + !m_cecAdapter->IsLibCECActiveSource() && + (!m_preventActivateSourceOnPlay.IsValid() || + CDateTime::GetCurrentDateTime() - m_preventActivateSourceOnPlay > + CDateTimeSpan(0, 0, 0, CEC_SUPPRESS_ACTIVATE_SOURCE_AFTER_ON_STOP))); + m_bOnPlayReceived = true; + } + if (bActivateSource) + ActivateSource(); + } +} + +bool CPeripheralCecAdapter::InitialiseFeature(const PeripheralFeature feature) +{ + if (feature == FEATURE_CEC && !m_bStarted && GetSettingBool("enabled")) + { + // hide settings that have an override set + if (!GetSettingString("wake_devices_advanced").empty()) + SetSettingVisible("wake_devices", false); + if (!GetSettingString("standby_devices_advanced").empty()) + SetSettingVisible("standby_devices", false); + + SetConfigurationFromSettings(); + m_callbacks.Clear(); + m_callbacks.logMessage = &CecLogMessage; + m_callbacks.keyPress = &CecKeyPress; + m_callbacks.commandReceived = &CecCommand; + m_callbacks.configurationChanged = &CecConfiguration; + m_callbacks.alert = &CecAlert; + m_callbacks.sourceActivated = &CecSourceActivated; + m_configuration.callbackParam = this; + m_configuration.callbacks = &m_callbacks; + + m_cecAdapter = CECInitialise(&m_configuration); + + if (m_configuration.serverVersion < CEC_LIB_SUPPORTED_VERSION) + { + /* unsupported libcec version */ + CLog::Log( + LOGERROR, + "Detected version of libCEC interface ({0:x}) is lower than the supported version {1:x}", + m_cecAdapter ? m_configuration.serverVersion : -1, CEC_LIB_SUPPORTED_VERSION); + + // display warning: incompatible libCEC + std::string strMessage = StringUtils::Format( + g_localizeStrings.Get(36040), m_cecAdapter ? m_configuration.serverVersion : -1, + CEC_LIB_SUPPORTED_VERSION); + CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Error, g_localizeStrings.Get(36000), + strMessage); + m_bError = true; + if (m_cecAdapter) + CECDestroy(m_cecAdapter); + m_cecAdapter = NULL; + + m_features.clear(); + return false; + } + else + { + CLog::Log(LOGDEBUG, "{} - using libCEC v{}", __FUNCTION__, + m_cecAdapter->VersionToString(m_configuration.serverVersion)); + SetVersionInfo(m_configuration); + } + + m_bStarted = true; + Create(); + } + + return CPeripheral::InitialiseFeature(feature); +} + +void CPeripheralCecAdapter::SetVersionInfo(const libcec_configuration& configuration) +{ + m_strVersionInfo = StringUtils::Format("libCEC {} - firmware v{}", + m_cecAdapter->VersionToString(configuration.serverVersion), + configuration.iFirmwareVersion); + + // append firmware build date + if (configuration.iFirmwareBuildDate != CEC_FW_BUILD_UNKNOWN) + { + CDateTime dt((time_t)configuration.iFirmwareBuildDate); + m_strVersionInfo += StringUtils::Format(" ({})", dt.GetAsDBDate()); + } +} + +bool CPeripheralCecAdapter::OpenConnection(void) +{ + bool bIsOpen(false); + + if (!GetSettingBool("enabled")) + { + CLog::Log(LOGDEBUG, "{} - CEC adapter is disabled in peripheral settings", __FUNCTION__); + m_bStarted = false; + return bIsOpen; + } + + // open the CEC adapter + CLog::Log(LOGDEBUG, "{} - opening a connection to the CEC adapter: {}", __FUNCTION__, + m_strComPort); + + // scanning the CEC bus takes about 5 seconds, so display a notification to inform users that + // we're busy + std::string strMessage = + StringUtils::Format(g_localizeStrings.Get(21336), g_localizeStrings.Get(36000)); + CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info, g_localizeStrings.Get(36000), + strMessage); + + bool bConnectionFailedDisplayed(false); + + while (!m_bStop && !bIsOpen) + { + if ((bIsOpen = m_cecAdapter->Open(m_strComPort.c_str(), 10000)) == false) + { + // display warning: couldn't initialise libCEC + CLog::Log(LOGERROR, "{} - could not opening a connection to the CEC adapter", __FUNCTION__); + if (!bConnectionFailedDisplayed) + CGUIDialogKaiToast::QueueNotification( + CGUIDialogKaiToast::Error, g_localizeStrings.Get(36000), g_localizeStrings.Get(36012)); + bConnectionFailedDisplayed = true; + + CThread::Sleep(10000ms); + } + } + + if (bIsOpen) + { + CLog::Log(LOGDEBUG, "{} - connection to the CEC adapter opened", __FUNCTION__); + + // read the configuration + libcec_configuration config; + if (m_cecAdapter->GetCurrentConfiguration(&config)) + { + // update the local configuration + std::unique_lock<CCriticalSection> lock(m_critSection); + SetConfigurationFromLibCEC(config); + } + } + + return bIsOpen; +} + +void CPeripheralCecAdapter::Process(void) +{ + if (!OpenConnection()) + return; + + { + std::unique_lock<CCriticalSection> lock(m_critSection); + m_iExitCode = EXITCODE_QUIT; + m_bGoingToStandby = false; + m_bIsRunning = true; + m_bActiveSourceBeforeStandby = false; + } + + CServiceBroker::GetAnnouncementManager()->AddAnnouncer(this); + + m_queryThread = new CPeripheralCecAdapterUpdateThread(this, &m_configuration); + m_queryThread->Create(false); + + while (!m_bStop) + { + if (!m_bStop) + ProcessVolumeChange(); + + if (!m_bStop) + ProcessActivateSource(); + + if (!m_bStop) + ProcessStandbyDevices(); + + if (!m_bStop) + CThread::Sleep(5ms); + } + + m_queryThread->StopThread(true); + + bool bSendStandbyCommands(false); + { + std::unique_lock<CCriticalSection> lock(m_critSection); + bSendStandbyCommands = m_iExitCode != EXITCODE_REBOOT && m_iExitCode != EXITCODE_RESTARTAPP && + !m_bDeviceRemoved && + (!m_bGoingToStandby || GetSettingBool("standby_tv_on_pc_standby")) && + GetSettingBool("enabled"); + + if (m_bGoingToStandby) + m_bActiveSourceBeforeStandby = m_cecAdapter->IsLibCECActiveSource(); + } + + if (bSendStandbyCommands) + { + if (m_cecAdapter->IsLibCECActiveSource()) + { + if (!m_configuration.powerOffDevices.IsEmpty()) + { + CLog::Log(LOGDEBUG, "{} - sending standby commands", __FUNCTION__); + m_standbySent = CDateTime::GetCurrentDateTime(); + m_cecAdapter->StandbyDevices(); + } + else if (m_bSendInactiveSource) + { + CLog::Log(LOGDEBUG, "{} - sending inactive source commands", __FUNCTION__); + m_cecAdapter->SetInactiveView(); + } + } + else + { + CLog::Log(LOGDEBUG, "{} - XBMC is not the active source, not sending any standby commands", + __FUNCTION__); + } + } + + m_cecAdapter->Close(); + + CLog::Log(LOGDEBUG, "{} - CEC adapter processor thread ended", __FUNCTION__); + + { + std::unique_lock<CCriticalSection> lock(m_critSection); + m_bStarted = false; + m_bIsRunning = false; + } +} + +bool CPeripheralCecAdapter::HasAudioControl(void) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_bHasConnectedAudioSystem; +} + +void CPeripheralCecAdapter::SetAudioSystemConnected(bool bSetTo) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_bHasConnectedAudioSystem = bSetTo; +} + +void CPeripheralCecAdapter::ProcessVolumeChange(void) +{ + bool bSendRelease(false); + CecVolumeChange pendingVolumeChange = VOLUME_CHANGE_NONE; + { + std::unique_lock<CCriticalSection> lock(m_critSection); + auto now = std::chrono::steady_clock::now(); + auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(now - m_lastKeypress); + if (!m_volumeChangeQueue.empty()) + { + /* get the first change from the queue */ + pendingVolumeChange = m_volumeChangeQueue.front(); + m_volumeChangeQueue.pop(); + + /* remove all dupe entries */ + while (!m_volumeChangeQueue.empty() && m_volumeChangeQueue.front() == pendingVolumeChange) + m_volumeChangeQueue.pop(); + + /* send another keypress after VOLUME_REFRESH_TIMEOUT ms */ + + bool bRefresh(duration.count() > VOLUME_REFRESH_TIMEOUT); + + /* only send the keypress when it hasn't been sent yet */ + if (pendingVolumeChange != m_lastChange) + { + m_lastKeypress = std::chrono::steady_clock::now(); + m_lastChange = pendingVolumeChange; + } + else if (bRefresh) + { + m_lastKeypress = std::chrono::steady_clock::now(); + pendingVolumeChange = m_lastChange; + } + else + pendingVolumeChange = VOLUME_CHANGE_NONE; + } + else if (m_lastKeypress.time_since_epoch().count() > 0 && + duration.count() > VOLUME_CHANGE_TIMEOUT) + { + /* send a key release */ + m_lastKeypress = {}; + bSendRelease = true; + m_lastChange = VOLUME_CHANGE_NONE; + } + } + + switch (pendingVolumeChange) + { + case VOLUME_CHANGE_UP: + m_cecAdapter->SendKeypress(CECDEVICE_AUDIOSYSTEM, CEC_USER_CONTROL_CODE_VOLUME_UP, false); + break; + case VOLUME_CHANGE_DOWN: + m_cecAdapter->SendKeypress(CECDEVICE_AUDIOSYSTEM, CEC_USER_CONTROL_CODE_VOLUME_DOWN, false); + break; + case VOLUME_CHANGE_MUTE: + m_cecAdapter->SendKeypress(CECDEVICE_AUDIOSYSTEM, CEC_USER_CONTROL_CODE_MUTE, false); + { + std::unique_lock<CCriticalSection> lock(m_critSection); + m_bIsMuted = !m_bIsMuted; + } + break; + case VOLUME_CHANGE_NONE: + if (bSendRelease) + m_cecAdapter->SendKeyRelease(CECDEVICE_AUDIOSYSTEM, false); + break; + } +} + +void CPeripheralCecAdapter::VolumeUp(void) +{ + if (HasAudioControl()) + { + std::unique_lock<CCriticalSection> lock(m_critSection); + m_volumeChangeQueue.push(VOLUME_CHANGE_UP); + } +} + +void CPeripheralCecAdapter::VolumeDown(void) +{ + if (HasAudioControl()) + { + std::unique_lock<CCriticalSection> lock(m_critSection); + m_volumeChangeQueue.push(VOLUME_CHANGE_DOWN); + } +} + +void CPeripheralCecAdapter::ToggleMute(void) +{ + if (HasAudioControl()) + { + std::unique_lock<CCriticalSection> lock(m_critSection); + m_volumeChangeQueue.push(VOLUME_CHANGE_MUTE); + } +} + +bool CPeripheralCecAdapter::IsMuted(void) +{ + if (HasAudioControl()) + { + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_bIsMuted; + } + return false; +} + +void CPeripheralCecAdapter::SetMenuLanguage(const char* strLanguage) +{ + if (StringUtils::EqualsNoCase(m_strMenuLanguage, strLanguage)) + return; + + std::string strGuiLanguage; + + if (!strcmp(strLanguage, "bul")) + strGuiLanguage = "bg_bg"; + else if (!strcmp(strLanguage, "hrv")) + strGuiLanguage = "hr_hr"; + else if (!strcmp(strLanguage, "cze")) + strGuiLanguage = "cs_cz"; + else if (!strcmp(strLanguage, "dan")) + strGuiLanguage = "da_dk"; + else if (!strcmp(strLanguage, "deu")) + strGuiLanguage = "de_de"; + else if (!strcmp(strLanguage, "dut")) + strGuiLanguage = "nl_nl"; + else if (!strcmp(strLanguage, "eng")) + strGuiLanguage = "en_gb"; + else if (!strcmp(strLanguage, "fin")) + strGuiLanguage = "fi_fi"; + else if (!strcmp(strLanguage, "fre")) + strGuiLanguage = "fr_fr"; + else if (!strcmp(strLanguage, "ger")) + strGuiLanguage = "de_de"; + else if (!strcmp(strLanguage, "gre")) + strGuiLanguage = "el_gr"; + else if (!strcmp(strLanguage, "hun")) + strGuiLanguage = "hu_hu"; + else if (!strcmp(strLanguage, "ita")) + strGuiLanguage = "it_it"; + else if (!strcmp(strLanguage, "nor")) + strGuiLanguage = "nb_no"; + else if (!strcmp(strLanguage, "pol")) + strGuiLanguage = "pl_pl"; + else if (!strcmp(strLanguage, "por")) + strGuiLanguage = "pt_pt"; + else if (!strcmp(strLanguage, "rum")) + strGuiLanguage = "ro_ro"; + else if (!strcmp(strLanguage, "rus")) + strGuiLanguage = "ru_ru"; + else if (!strcmp(strLanguage, "srp")) + strGuiLanguage = "sr_rs@latin"; + else if (!strcmp(strLanguage, "slo")) + strGuiLanguage = "sk_sk"; + else if (!strcmp(strLanguage, "slv")) + strGuiLanguage = "sl_si"; + else if (!strcmp(strLanguage, "spa")) + strGuiLanguage = "es_es"; + else if (!strcmp(strLanguage, "swe")) + strGuiLanguage = "sv_se"; + else if (!strcmp(strLanguage, "tur")) + strGuiLanguage = "tr_tr"; + + if (!strGuiLanguage.empty()) + { + strGuiLanguage = "resource.language." + strGuiLanguage; + CServiceBroker::GetAppMessenger()->PostMsg(TMSG_SETLANGUAGE, -1, -1, nullptr, strGuiLanguage); + CLog::Log(LOGDEBUG, "{} - language set to '{}'", __FUNCTION__, strGuiLanguage); + } + else + CLog::Log(LOGWARNING, "{} - TV menu language set to unknown value '{}'", __FUNCTION__, + strLanguage); +} + +void CPeripheralCecAdapter::OnTvStandby(void) +{ + int iActionOnTvStandby = GetSettingInt("standby_pc_on_tv_standby"); + switch (iActionOnTvStandby) + { + case LOCALISED_ID_POWEROFF: + m_bStarted = false; + CServiceBroker::GetAppMessenger()->PostMsg(TMSG_SYSTEM_POWERDOWN, TMSG_SHUTDOWN); + break; + case LOCALISED_ID_SUSPEND: + m_bStarted = false; + CServiceBroker::GetAppMessenger()->PostMsg(TMSG_SYSTEM_POWERDOWN, TMSG_SUSPEND); + break; + case LOCALISED_ID_HIBERNATE: + m_bStarted = false; + CServiceBroker::GetAppMessenger()->PostMsg(TMSG_SYSTEM_POWERDOWN, TMSG_HIBERNATE); + break; + case LOCALISED_ID_QUIT: + m_bStarted = false; + CServiceBroker::GetAppMessenger()->PostMsg(TMSG_QUIT); + break; + case LOCALISED_ID_PAUSE: + CServiceBroker::GetAppMessenger()->PostMsg(TMSG_MEDIA_PAUSE_IF_PLAYING); + break; + case LOCALISED_ID_STOP: + { + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + if (appPlayer->IsPlaying()) + CServiceBroker::GetAppMessenger()->PostMsg(TMSG_MEDIA_STOP); + break; + } + case LOCALISED_ID_IGNORE: + break; + default: + CLog::Log(LOGERROR, "{} - Unexpected [standby_pc_on_tv_standby] setting value", __FUNCTION__); + break; + } +} + +void CPeripheralCecAdapter::CecCommand(void* cbParam, const cec_command* command) +{ + CPeripheralCecAdapter* adapter = static_cast<CPeripheralCecAdapter*>(cbParam); + if (!adapter) + return; + + if (adapter->m_bIsReady) + { + switch (command->opcode) + { + case CEC_OPCODE_STANDBY: + if (command->initiator == CECDEVICE_TV && + (!adapter->m_standbySent.IsValid() || + CDateTime::GetCurrentDateTime() - adapter->m_standbySent > + CDateTimeSpan(0, 0, 0, SCREENSAVER_TIMEOUT))) + { + adapter->OnTvStandby(); + } + break; + case CEC_OPCODE_SET_MENU_LANGUAGE: + if (adapter->m_bUseTVMenuLanguage == 1 && command->initiator == CECDEVICE_TV && + command->parameters.size == 3) + { + char strNewLanguage[4]; + for (int iPtr = 0; iPtr < 3; iPtr++) + strNewLanguage[iPtr] = command->parameters[iPtr]; + strNewLanguage[3] = 0; + adapter->SetMenuLanguage(strNewLanguage); + } + break; + case CEC_OPCODE_DECK_CONTROL: + if (command->initiator == CECDEVICE_TV && command->parameters.size == 1 && + command->parameters[0] == CEC_DECK_CONTROL_MODE_STOP) + { + cec_keypress key; + key.duration = 500; + key.keycode = CEC_USER_CONTROL_CODE_STOP; + adapter->PushCecKeypress(key); + } + break; + case CEC_OPCODE_PLAY: + if (command->initiator == CECDEVICE_TV && command->parameters.size == 1) + { + if (command->parameters[0] == CEC_PLAY_MODE_PLAY_FORWARD) + { + cec_keypress key; + key.duration = 500; + key.keycode = CEC_USER_CONTROL_CODE_PLAY; + adapter->PushCecKeypress(key); + } + else if (command->parameters[0] == CEC_PLAY_MODE_PLAY_STILL) + { + cec_keypress key; + key.duration = 500; + key.keycode = CEC_USER_CONTROL_CODE_PAUSE; + adapter->PushCecKeypress(key); + } + } + break; + default: + break; + } + } +} + +void CPeripheralCecAdapter::CecConfiguration(void* cbParam, const libcec_configuration* config) +{ + CPeripheralCecAdapter* adapter = static_cast<CPeripheralCecAdapter*>(cbParam); + if (!adapter) + return; + + std::unique_lock<CCriticalSection> lock(adapter->m_critSection); + adapter->SetConfigurationFromLibCEC(*config); +} + +void CPeripheralCecAdapter::CecAlert(void* cbParam, + const libcec_alert alert, + const libcec_parameter data) +{ + CPeripheralCecAdapter* adapter = static_cast<CPeripheralCecAdapter*>(cbParam); + if (!adapter) + return; + + bool bReopenConnection(false); + int iAlertString(0); + switch (alert) + { + case CEC_ALERT_SERVICE_DEVICE: + iAlertString = 36027; + break; + case CEC_ALERT_CONNECTION_LOST: + bReopenConnection = true; + iAlertString = 36030; + break; +#if defined(CEC_ALERT_PERMISSION_ERROR) + case CEC_ALERT_PERMISSION_ERROR: + bReopenConnection = true; + iAlertString = 36031; + break; + case CEC_ALERT_PORT_BUSY: + bReopenConnection = true; + iAlertString = 36032; + break; +#endif + default: + break; + } + + // display the alert + if (iAlertString) + { + std::string strLog(g_localizeStrings.Get(iAlertString)); + if (data.paramType == CEC_PARAMETER_TYPE_STRING && data.paramData) + strLog += StringUtils::Format(" - {}", (const char*)data.paramData); + CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info, g_localizeStrings.Get(36000), + strLog); + } + + if (bReopenConnection) + { + // Reopen the connection asynchronously. Otherwise a deadlock may occur. + // Reconnect means destruction and recreation of our libcec instance, but libcec + // calls this callback function synchronously and must not be destroyed meanwhile. + adapter->ReopenConnection(true); + } +} + +void CPeripheralCecAdapter::CecKeyPress(void* cbParam, const cec_keypress* key) +{ + CPeripheralCecAdapter* adapter = static_cast<CPeripheralCecAdapter*>(cbParam); + if (!!adapter) + adapter->PushCecKeypress(*key); +} + +void CPeripheralCecAdapter::GetNextKey(void) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_bHasButton = false; + if (m_bIsReady) + { + std::vector<CecButtonPress>::iterator it = m_buttonQueue.begin(); + if (it != m_buttonQueue.end()) + { + m_currentButton = (*it); + m_buttonQueue.erase(it); + m_bHasButton = true; + } + } +} + +void CPeripheralCecAdapter::PushCecKeypress(const CecButtonPress& key) +{ + CLog::Log(LOGDEBUG, "{} - received key {:2x} duration {}", __FUNCTION__, key.iButton, + key.iDuration); + + std::unique_lock<CCriticalSection> lock(m_critSection); + // avoid the queue getting too long + if (m_configuration.iButtonRepeatRateMs && m_buttonQueue.size() > 5) + return; + if (m_configuration.iButtonRepeatRateMs == 0 && key.iDuration > 0) + { + if (m_currentButton.iButton == key.iButton && m_currentButton.iDuration == 0) + { + // update the duration + if (m_bHasButton) + m_currentButton.iDuration = key.iDuration; + // ignore this one, since it's already been handled by xbmc + return; + } + // if we received a keypress with a duration set, try to find the same one without a duration + // set, and replace it + for (std::vector<CecButtonPress>::reverse_iterator it = m_buttonQueue.rbegin(); + it != m_buttonQueue.rend(); ++it) + { + if ((*it).iButton == key.iButton) + { + if ((*it).iDuration == 0) + { + // replace this entry + (*it).iDuration = key.iDuration; + return; + } + // add a new entry + break; + } + } + } + + m_buttonQueue.push_back(key); +} + +void CPeripheralCecAdapter::PushCecKeypress(const cec_keypress& key) +{ + CecButtonPress xbmcKey; + xbmcKey.iDuration = key.duration; + + switch (key.keycode) + { + case CEC_USER_CONTROL_CODE_SELECT: + xbmcKey.iButton = XINPUT_IR_REMOTE_SELECT; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_UP: + xbmcKey.iButton = XINPUT_IR_REMOTE_UP; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_DOWN: + xbmcKey.iButton = XINPUT_IR_REMOTE_DOWN; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_LEFT: + xbmcKey.iButton = XINPUT_IR_REMOTE_LEFT; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_LEFT_UP: + xbmcKey.iButton = XINPUT_IR_REMOTE_LEFT; + PushCecKeypress(xbmcKey); + xbmcKey.iButton = XINPUT_IR_REMOTE_UP; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_LEFT_DOWN: + xbmcKey.iButton = XINPUT_IR_REMOTE_LEFT; + PushCecKeypress(xbmcKey); + xbmcKey.iButton = XINPUT_IR_REMOTE_DOWN; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_RIGHT: + xbmcKey.iButton = XINPUT_IR_REMOTE_RIGHT; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_RIGHT_UP: + xbmcKey.iButton = XINPUT_IR_REMOTE_RIGHT; + PushCecKeypress(xbmcKey); + xbmcKey.iButton = XINPUT_IR_REMOTE_UP; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_RIGHT_DOWN: + xbmcKey.iButton = XINPUT_IR_REMOTE_RIGHT; + PushCecKeypress(xbmcKey); + xbmcKey.iButton = XINPUT_IR_REMOTE_DOWN; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_SETUP_MENU: + xbmcKey.iButton = XINPUT_IR_REMOTE_TITLE; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_CONTENTS_MENU: + xbmcKey.iButton = XINPUT_IR_REMOTE_CONTENTS_MENU; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_ROOT_MENU: + xbmcKey.iButton = XINPUT_IR_REMOTE_ROOT_MENU; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_TOP_MENU: + xbmcKey.iButton = XINPUT_IR_REMOTE_TOP_MENU; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_DVD_MENU: + xbmcKey.iButton = XINPUT_IR_REMOTE_DVD_MENU; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_FAVORITE_MENU: + xbmcKey.iButton = XINPUT_IR_REMOTE_MENU; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_EXIT: + xbmcKey.iButton = XINPUT_IR_REMOTE_BACK; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_ENTER: + xbmcKey.iButton = XINPUT_IR_REMOTE_ENTER; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_CHANNEL_DOWN: + xbmcKey.iButton = XINPUT_IR_REMOTE_CHANNEL_MINUS; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_CHANNEL_UP: + xbmcKey.iButton = XINPUT_IR_REMOTE_CHANNEL_PLUS; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_PREVIOUS_CHANNEL: + xbmcKey.iButton = XINPUT_IR_REMOTE_TELETEXT; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_SOUND_SELECT: + xbmcKey.iButton = XINPUT_IR_REMOTE_LANGUAGE; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_POWER: + case CEC_USER_CONTROL_CODE_POWER_TOGGLE_FUNCTION: + case CEC_USER_CONTROL_CODE_POWER_OFF_FUNCTION: + xbmcKey.iButton = XINPUT_IR_REMOTE_POWER; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_VOLUME_UP: + xbmcKey.iButton = XINPUT_IR_REMOTE_VOLUME_PLUS; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_VOLUME_DOWN: + xbmcKey.iButton = XINPUT_IR_REMOTE_VOLUME_MINUS; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_MUTE: + case CEC_USER_CONTROL_CODE_MUTE_FUNCTION: + case CEC_USER_CONTROL_CODE_RESTORE_VOLUME_FUNCTION: + xbmcKey.iButton = XINPUT_IR_REMOTE_MUTE; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_PLAY: + xbmcKey.iButton = XINPUT_IR_REMOTE_PLAY; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_STOP: + xbmcKey.iButton = XINPUT_IR_REMOTE_STOP; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_PAUSE: + xbmcKey.iButton = XINPUT_IR_REMOTE_PAUSE; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_REWIND: + xbmcKey.iButton = XINPUT_IR_REMOTE_REVERSE; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_FAST_FORWARD: + xbmcKey.iButton = XINPUT_IR_REMOTE_FORWARD; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_NUMBER0: + xbmcKey.iButton = XINPUT_IR_REMOTE_0; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_NUMBER1: + xbmcKey.iButton = XINPUT_IR_REMOTE_1; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_NUMBER2: + xbmcKey.iButton = XINPUT_IR_REMOTE_2; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_NUMBER3: + xbmcKey.iButton = XINPUT_IR_REMOTE_3; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_NUMBER4: + xbmcKey.iButton = XINPUT_IR_REMOTE_4; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_NUMBER5: + xbmcKey.iButton = XINPUT_IR_REMOTE_5; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_NUMBER6: + xbmcKey.iButton = XINPUT_IR_REMOTE_6; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_NUMBER7: + xbmcKey.iButton = XINPUT_IR_REMOTE_7; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_NUMBER8: + xbmcKey.iButton = XINPUT_IR_REMOTE_8; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_NUMBER9: + xbmcKey.iButton = XINPUT_IR_REMOTE_9; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_RECORD: + xbmcKey.iButton = XINPUT_IR_REMOTE_RECORD; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_CLEAR: + xbmcKey.iButton = XINPUT_IR_REMOTE_CLEAR; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_DISPLAY_INFORMATION: + xbmcKey.iButton = XINPUT_IR_REMOTE_INFO; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_PAGE_UP: + xbmcKey.iButton = XINPUT_IR_REMOTE_CHANNEL_PLUS; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_PAGE_DOWN: + xbmcKey.iButton = XINPUT_IR_REMOTE_CHANNEL_MINUS; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_FORWARD: + xbmcKey.iButton = XINPUT_IR_REMOTE_SKIP_PLUS; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_BACKWARD: + xbmcKey.iButton = XINPUT_IR_REMOTE_SKIP_MINUS; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_F1_BLUE: + xbmcKey.iButton = XINPUT_IR_REMOTE_BLUE; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_F2_RED: + xbmcKey.iButton = XINPUT_IR_REMOTE_RED; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_F3_GREEN: + xbmcKey.iButton = XINPUT_IR_REMOTE_GREEN; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_F4_YELLOW: + xbmcKey.iButton = XINPUT_IR_REMOTE_YELLOW; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_ELECTRONIC_PROGRAM_GUIDE: + xbmcKey.iButton = XINPUT_IR_REMOTE_GUIDE; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_AN_CHANNELS_LIST: + xbmcKey.iButton = XINPUT_IR_REMOTE_LIVE_TV; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_NEXT_FAVORITE: + case CEC_USER_CONTROL_CODE_DOT: + case CEC_USER_CONTROL_CODE_AN_RETURN: + xbmcKey.iButton = XINPUT_IR_REMOTE_TITLE; // context menu + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_DATA: + xbmcKey.iButton = XINPUT_IR_REMOTE_TELETEXT; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_SUB_PICTURE: + xbmcKey.iButton = XINPUT_IR_REMOTE_SUBTITLE; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_EJECT: + xbmcKey.iButton = XINPUT_IR_REMOTE_EJECT; + PushCecKeypress(xbmcKey); + break; + case CEC_USER_CONTROL_CODE_POWER_ON_FUNCTION: + case CEC_USER_CONTROL_CODE_INPUT_SELECT: + case CEC_USER_CONTROL_CODE_INITIAL_CONFIGURATION: + case CEC_USER_CONTROL_CODE_HELP: + case CEC_USER_CONTROL_CODE_STOP_RECORD: + case CEC_USER_CONTROL_CODE_PAUSE_RECORD: + case CEC_USER_CONTROL_CODE_ANGLE: + case CEC_USER_CONTROL_CODE_VIDEO_ON_DEMAND: + case CEC_USER_CONTROL_CODE_TIMER_PROGRAMMING: + case CEC_USER_CONTROL_CODE_PLAY_FUNCTION: + case CEC_USER_CONTROL_CODE_PAUSE_PLAY_FUNCTION: + case CEC_USER_CONTROL_CODE_RECORD_FUNCTION: + case CEC_USER_CONTROL_CODE_PAUSE_RECORD_FUNCTION: + case CEC_USER_CONTROL_CODE_STOP_FUNCTION: + case CEC_USER_CONTROL_CODE_TUNE_FUNCTION: + case CEC_USER_CONTROL_CODE_SELECT_MEDIA_FUNCTION: + case CEC_USER_CONTROL_CODE_SELECT_AV_INPUT_FUNCTION: + case CEC_USER_CONTROL_CODE_SELECT_AUDIO_INPUT_FUNCTION: + case CEC_USER_CONTROL_CODE_F5: + case CEC_USER_CONTROL_CODE_NUMBER_ENTRY_MODE: + case CEC_USER_CONTROL_CODE_NUMBER11: + case CEC_USER_CONTROL_CODE_NUMBER12: + case CEC_USER_CONTROL_CODE_SELECT_BROADCAST_TYPE: + case CEC_USER_CONTROL_CODE_SELECT_SOUND_PRESENTATION: + case CEC_USER_CONTROL_CODE_UNKNOWN: + default: + break; + } +} + +int CPeripheralCecAdapter::GetButton(void) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + if (!m_bHasButton) + GetNextKey(); + + return m_bHasButton ? m_currentButton.iButton : 0; +} + +unsigned int CPeripheralCecAdapter::GetHoldTime(void) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + if (!m_bHasButton) + GetNextKey(); + + return m_bHasButton ? m_currentButton.iDuration : 0; +} + +void CPeripheralCecAdapter::ResetButton(void) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_bHasButton = false; + + // wait for the key release if the duration isn't 0 + if (m_currentButton.iDuration > 0) + { + m_currentButton.iButton = 0; + m_currentButton.iDuration = 0; + } +} + +void CPeripheralCecAdapter::OnSettingChanged(const std::string& strChangedSetting) +{ + if (StringUtils::EqualsNoCase(strChangedSetting, "enabled")) + { + bool bEnabled(GetSettingBool("enabled")); + if (!bEnabled && IsRunning()) + { + CLog::Log(LOGDEBUG, "{} - closing the CEC connection", __FUNCTION__); + StopThread(true); + } + else if (bEnabled && !IsRunning()) + { + CLog::Log(LOGDEBUG, "{} - starting the CEC connection", __FUNCTION__); + SetConfigurationFromSettings(); + InitialiseFeature(FEATURE_CEC); + } + } + else if (IsRunning()) + { + if (m_queryThread->IsRunning()) + { + CLog::Log(LOGDEBUG, "{} - sending the updated configuration to libCEC", __FUNCTION__); + SetConfigurationFromSettings(); + m_queryThread->UpdateConfiguration(&m_configuration); + } + } + else + { + CLog::Log(LOGDEBUG, "{} - restarting the CEC connection", __FUNCTION__); + SetConfigurationFromSettings(); + InitialiseFeature(FEATURE_CEC); + } +} + +void CPeripheralCecAdapter::CecSourceActivated(void* cbParam, + const CEC::cec_logical_address address, + const uint8_t activated) +{ + CPeripheralCecAdapter* adapter = static_cast<CPeripheralCecAdapter*>(cbParam); + if (!adapter) + return; + + // wake up the screensaver, so the user doesn't switch to a black screen + if (activated == 1) + { + auto& components = CServiceBroker::GetAppComponents(); + const auto appPower = components.GetComponent<CApplicationPowerHandling>(); + appPower->WakeUpScreenSaverAndDPMS(); + } + + if (adapter->GetSettingInt("pause_or_stop_playback_on_deactivate") != LOCALISED_ID_NONE) + { + bool bShowingSlideshow = + (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_SLIDESHOW); + CGUIWindowSlideShow* pSlideShow = + bShowingSlideshow + ? CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIWindowSlideShow>( + WINDOW_SLIDESHOW) + : NULL; + + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + + bool bPlayingAndDeactivated = activated == 0 && ((pSlideShow && pSlideShow->IsPlaying()) || + !appPlayer->IsPausedPlayback()); + bool bPausedAndActivated = + activated == 1 && adapter->m_bPlaybackPaused && + ((pSlideShow && pSlideShow->IsPaused()) || (appPlayer && appPlayer->IsPausedPlayback())); + if (bPlayingAndDeactivated) + adapter->m_bPlaybackPaused = true; + else if (bPausedAndActivated) + adapter->m_bPlaybackPaused = false; + + if ((bPlayingAndDeactivated || bPausedAndActivated) && + adapter->GetSettingInt("pause_or_stop_playback_on_deactivate") == LOCALISED_ID_PAUSE) + { + if (pSlideShow) + // pause/resume slideshow + pSlideShow->OnAction(CAction(ACTION_PAUSE)); + else + // pause/resume player + CServiceBroker::GetAppMessenger()->SendMsg(TMSG_MEDIA_PAUSE); + } + else if (bPlayingAndDeactivated && + adapter->GetSettingInt("pause_or_stop_playback_on_deactivate") == LOCALISED_ID_STOP) + { + if (pSlideShow) + pSlideShow->OnAction(CAction(ACTION_STOP)); + else + CServiceBroker::GetAppMessenger()->SendMsg(TMSG_MEDIA_STOP); + } + } +} + +void CPeripheralCecAdapter::CecLogMessage(void* cbParam, const cec_log_message* message) +{ + CPeripheralCecAdapter* adapter = static_cast<CPeripheralCecAdapter*>(cbParam); + if (!adapter) + return; + + int iLevel = -1; + switch (message->level) + { + case CEC_LOG_ERROR: + iLevel = LOGERROR; + break; + case CEC_LOG_WARNING: + iLevel = LOGWARNING; + break; + case CEC_LOG_NOTICE: + iLevel = LOGDEBUG; + break; + case CEC_LOG_TRAFFIC: + case CEC_LOG_DEBUG: + iLevel = LOGDEBUG; + break; + default: + break; + } + + if (iLevel >= CEC_LOG_NOTICE || + (iLevel >= 0 && CServiceBroker::GetLogging().IsLogLevelLogged(LOGDEBUG))) + CLog::Log(iLevel, LOGCEC, "{} - {}", __FUNCTION__, message->message); +} + +void CPeripheralCecAdapter::SetConfigurationFromLibCEC(const CEC::libcec_configuration& config) +{ + bool bChanged(false); + + // set the primary device type + m_configuration.deviceTypes.Clear(); + m_configuration.deviceTypes.Add(config.deviceTypes[0]); + + // hide the "connected device" and "hdmi port number" settings when the PA was autodetected + bool bPAAutoDetected(config.bAutodetectAddress == 1); + + SetSettingVisible("connected_device", !bPAAutoDetected); + SetSettingVisible("cec_hdmi_port", !bPAAutoDetected); + + // set the connected device + m_configuration.baseDevice = config.baseDevice; + bChanged |= + SetSetting("connected_device", + config.baseDevice == CECDEVICE_AUDIOSYSTEM ? LOCALISED_ID_AVR : LOCALISED_ID_TV); + + // set the HDMI port number + m_configuration.iHDMIPort = config.iHDMIPort; + bChanged |= SetSetting("cec_hdmi_port", config.iHDMIPort); + + // set the physical address, when baseDevice or iHDMIPort are not set + std::string strPhysicalAddress("0"); + if (!bPAAutoDetected && (m_configuration.baseDevice == CECDEVICE_UNKNOWN || + m_configuration.iHDMIPort < CEC_MIN_HDMI_PORTNUMBER || + m_configuration.iHDMIPort > CEC_MAX_HDMI_PORTNUMBER)) + { + m_configuration.iPhysicalAddress = config.iPhysicalAddress; + strPhysicalAddress = StringUtils::Format("{:x}", config.iPhysicalAddress); + } + bChanged |= SetSetting("physical_address", strPhysicalAddress); + + // set the devices to wake when starting + m_configuration.wakeDevices = config.wakeDevices; + bChanged |= WriteLogicalAddresses(config.wakeDevices, "wake_devices", "wake_devices_advanced"); + + // set the devices to power off when stopping + m_configuration.powerOffDevices = config.powerOffDevices; + bChanged |= + WriteLogicalAddresses(config.powerOffDevices, "standby_devices", "standby_devices_advanced"); + + // set the boolean settings + m_configuration.bActivateSource = config.bActivateSource; + bChanged |= SetSetting("activate_source", m_configuration.bActivateSource == 1); + + m_configuration.iDoubleTapTimeoutMs = config.iDoubleTapTimeoutMs; + bChanged |= SetSetting("double_tap_timeout_ms", (int)m_configuration.iDoubleTapTimeoutMs); + + m_configuration.iButtonRepeatRateMs = config.iButtonRepeatRateMs; + bChanged |= SetSetting("button_repeat_rate_ms", (int)m_configuration.iButtonRepeatRateMs); + + m_configuration.iButtonReleaseDelayMs = config.iButtonReleaseDelayMs; + bChanged |= SetSetting("button_release_delay_ms", (int)m_configuration.iButtonReleaseDelayMs); + + m_configuration.bPowerOffOnStandby = config.bPowerOffOnStandby; + + m_configuration.iFirmwareVersion = config.iFirmwareVersion; + + memcpy(m_configuration.strDeviceLanguage, config.strDeviceLanguage, 3); + m_configuration.iFirmwareBuildDate = config.iFirmwareBuildDate; + + SetVersionInfo(m_configuration); + + if (bChanged) + CLog::Log(LOGDEBUG, "SetConfigurationFromLibCEC - settings updated by libCEC"); +} + +void CPeripheralCecAdapter::SetConfigurationFromSettings(void) +{ + // client version matches the version of libCEC that we originally used the API from + m_configuration.clientVersion = LIBCEC_VERSION_TO_UINT(4, 0, 0); + + // device name 'XBMC' + snprintf(m_configuration.strDeviceName, 13, "%s", GetSettingString("device_name").c_str()); + + // set the primary device type + m_configuration.deviceTypes.Clear(); + switch (GetSettingInt("device_type")) + { + case LOCALISED_ID_PLAYBACK_DEVICE: + m_configuration.deviceTypes.Add(CEC_DEVICE_TYPE_PLAYBACK_DEVICE); + break; + case LOCALISED_ID_TUNER_DEVICE: + m_configuration.deviceTypes.Add(CEC_DEVICE_TYPE_TUNER); + break; + case LOCALISED_ID_RECORDING_DEVICE: + default: + m_configuration.deviceTypes.Add(CEC_DEVICE_TYPE_RECORDING_DEVICE); + break; + } + + // always try to autodetect the address. + // when the firmware supports this, it will override the physical address, connected device and + // hdmi port settings + m_configuration.bAutodetectAddress = CEC_DEFAULT_SETTING_AUTODETECT_ADDRESS; + + // set the physical address + // when set, it will override the connected device and hdmi port settings + std::string strPhysicalAddress = GetSettingString("physical_address"); + int iPhysicalAddress; + if (sscanf(strPhysicalAddress.c_str(), "%x", &iPhysicalAddress) && + iPhysicalAddress >= CEC_PHYSICAL_ADDRESS_TV && iPhysicalAddress <= CEC_MAX_PHYSICAL_ADDRESS) + m_configuration.iPhysicalAddress = iPhysicalAddress; + else + m_configuration.iPhysicalAddress = CEC_PHYSICAL_ADDRESS_TV; + + // set the connected device + int iConnectedDevice = GetSettingInt("connected_device"); + if (iConnectedDevice == LOCALISED_ID_AVR) + m_configuration.baseDevice = CECDEVICE_AUDIOSYSTEM; + else if (iConnectedDevice == LOCALISED_ID_TV) + m_configuration.baseDevice = CECDEVICE_TV; + + // set the HDMI port number + int iHDMIPort = GetSettingInt("cec_hdmi_port"); + if (iHDMIPort >= CEC_MIN_HDMI_PORTNUMBER && iHDMIPort <= CEC_MAX_HDMI_PORTNUMBER) + m_configuration.iHDMIPort = iHDMIPort; + + // set the tv vendor override + int iVendor = GetSettingInt("tv_vendor"); + if (iVendor >= CEC_MIN_VENDORID && iVendor <= CEC_MAX_VENDORID) + m_configuration.tvVendor = iVendor; + + // read the devices to wake when starting + std::string strWakeDevices = GetSettingString("wake_devices_advanced"); + StringUtils::Trim(strWakeDevices); + m_configuration.wakeDevices.Clear(); + if (!strWakeDevices.empty()) + ReadLogicalAddresses(strWakeDevices, m_configuration.wakeDevices); + else + ReadLogicalAddresses(GetSettingInt("wake_devices"), m_configuration.wakeDevices); + + // read the devices to power off when stopping + std::string strStandbyDevices = GetSettingString("standby_devices_advanced"); + StringUtils::Trim(strStandbyDevices); + m_configuration.powerOffDevices.Clear(); + if (!strStandbyDevices.empty()) + ReadLogicalAddresses(strStandbyDevices, m_configuration.powerOffDevices); + else + ReadLogicalAddresses(GetSettingInt("standby_devices"), m_configuration.powerOffDevices); + + // read the boolean settings + m_bUseTVMenuLanguage = GetSettingBool("use_tv_menu_language"); + m_configuration.bActivateSource = GetSettingBool("activate_source") ? 1 : 0; + m_bPowerOffScreensaver = GetSettingBool("cec_standby_screensaver"); + m_bPowerOnScreensaver = GetSettingBool("cec_wake_screensaver"); + m_bSendInactiveSource = GetSettingBool("send_inactive_source"); + m_configuration.bAutoWakeAVR = GetSettingBool("power_avr_on_as") ? 1 : 0; + + // read the mutually exclusive boolean settings + int iStandbyAction(GetSettingInt("standby_pc_on_tv_standby")); + m_configuration.bPowerOffOnStandby = + (iStandbyAction == LOCALISED_ID_SUSPEND || iStandbyAction == LOCALISED_ID_HIBERNATE) ? 1 : 0; + m_bShutdownOnStandby = iStandbyAction == LOCALISED_ID_POWEROFF; + + // double tap prevention timeout in ms + m_configuration.iDoubleTapTimeoutMs = GetSettingInt("double_tap_timeout_ms"); + m_configuration.iButtonRepeatRateMs = GetSettingInt("button_repeat_rate_ms"); + m_configuration.iButtonReleaseDelayMs = GetSettingInt("button_release_delay_ms"); + + if (GetSettingBool("pause_playback_on_deactivate")) + { + SetSetting("pause_or_stop_playback_on_deactivate", LOCALISED_ID_PAUSE); + SetSetting("pause_playback_on_deactivate", false); + } +} + +void CPeripheralCecAdapter::ReadLogicalAddresses(const std::string& strString, + cec_logical_addresses& addresses) +{ + for (size_t iPtr = 0; iPtr < strString.size(); iPtr++) + { + std::string strDevice = strString.substr(iPtr, 1); + StringUtils::Trim(strDevice); + if (!strDevice.empty()) + { + int iDevice(0); + if (sscanf(strDevice.c_str(), "%x", &iDevice) == 1 && iDevice >= 0 && iDevice <= 0xF) + addresses.Set((cec_logical_address)iDevice); + } + } +} + +void CPeripheralCecAdapter::ReadLogicalAddresses(int iLocalisedId, cec_logical_addresses& addresses) +{ + addresses.Clear(); + switch (iLocalisedId) + { + case LOCALISED_ID_TV: + addresses.Set(CECDEVICE_TV); + break; + case LOCALISED_ID_AVR: + addresses.Set(CECDEVICE_AUDIOSYSTEM); + break; + case LOCALISED_ID_TV_AVR: + addresses.Set(CECDEVICE_TV); + addresses.Set(CECDEVICE_AUDIOSYSTEM); + break; + case LOCALISED_ID_NONE: + default: + break; + } +} + +bool CPeripheralCecAdapter::WriteLogicalAddresses(const cec_logical_addresses& addresses, + const std::string& strSettingName, + const std::string& strAdvancedSettingName) +{ + bool bChanged(false); + + // only update the advanced setting if it was set by the user + if (!GetSettingString(strAdvancedSettingName).empty()) + { + std::string strPowerOffDevices; + for (unsigned int iPtr = CECDEVICE_TV; iPtr <= CECDEVICE_BROADCAST; iPtr++) + if (addresses[iPtr]) + strPowerOffDevices += StringUtils::Format(" {:X}", iPtr); + StringUtils::Trim(strPowerOffDevices); + bChanged = SetSetting(strAdvancedSettingName, strPowerOffDevices); + } + + int iSettingPowerOffDevices = LOCALISED_ID_NONE; + if (addresses[CECDEVICE_TV] && addresses[CECDEVICE_AUDIOSYSTEM]) + iSettingPowerOffDevices = LOCALISED_ID_TV_AVR; + else if (addresses[CECDEVICE_TV]) + iSettingPowerOffDevices = LOCALISED_ID_TV; + else if (addresses[CECDEVICE_AUDIOSYSTEM]) + iSettingPowerOffDevices = LOCALISED_ID_AVR; + return SetSetting(strSettingName, iSettingPowerOffDevices) || bChanged; +} + +CPeripheralCecAdapterUpdateThread::CPeripheralCecAdapterUpdateThread( + CPeripheralCecAdapter* adapter, libcec_configuration* configuration) + : CThread("CECAdapterUpdate"), + m_adapter(adapter), + m_configuration(*configuration), + m_bNextConfigurationScheduled(false), + m_bIsUpdating(true) +{ + m_nextConfiguration.Clear(); + m_event.Reset(); +} + +CPeripheralCecAdapterUpdateThread::~CPeripheralCecAdapterUpdateThread(void) +{ + StopThread(false); + m_event.Set(); + StopThread(true); +} + +void CPeripheralCecAdapterUpdateThread::Signal(void) +{ + m_event.Set(); +} + +bool CPeripheralCecAdapterUpdateThread::UpdateConfiguration(libcec_configuration* configuration) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + if (!configuration) + return false; + + if (m_bIsUpdating) + { + m_bNextConfigurationScheduled = true; + m_nextConfiguration = *configuration; + } + else + { + m_configuration = *configuration; + m_event.Set(); + } + return true; +} + +bool CPeripheralCecAdapterUpdateThread::WaitReady(void) +{ + // don't wait if we're not powering up anything + if (m_configuration.wakeDevices.IsEmpty() && m_configuration.bActivateSource == 0) + return true; + + // wait for the TV if we're configured to become the active source. + // wait for the first device in the wake list otherwise. + cec_logical_address waitFor = + (m_configuration.bActivateSource == 1) ? CECDEVICE_TV : m_configuration.wakeDevices.primary; + + cec_power_status powerStatus(CEC_POWER_STATUS_UNKNOWN); + bool bContinue(true); + while (bContinue && !m_adapter->m_bStop && !m_bStop && powerStatus != CEC_POWER_STATUS_ON) + { + powerStatus = m_adapter->m_cecAdapter->GetDevicePowerStatus(waitFor); + if (powerStatus != CEC_POWER_STATUS_ON) + bContinue = !m_event.Wait(1000ms); + } + + return powerStatus == CEC_POWER_STATUS_ON; +} + +void CPeripheralCecAdapterUpdateThread::UpdateMenuLanguage(void) +{ + // request the menu language of the TV + if (m_adapter->m_bUseTVMenuLanguage == 1) + { + CLog::Log(LOGDEBUG, "{} - requesting the menu language of the TV", __FUNCTION__); + std::string language(m_adapter->m_cecAdapter->GetDeviceMenuLanguage(CECDEVICE_TV)); + m_adapter->SetMenuLanguage(language.c_str()); + } + else + { + CLog::Log(LOGDEBUG, "{} - using TV menu language is disabled", __FUNCTION__); + } +} + +std::string CPeripheralCecAdapterUpdateThread::UpdateAudioSystemStatus(void) +{ + std::string strAmpName; + + /* disable the mute setting when an amp is found, because the amp handles the mute setting and + set PCM output to 100% */ + if (m_adapter->m_cecAdapter->IsActiveDeviceType(CEC_DEVICE_TYPE_AUDIO_SYSTEM)) + { + // request the OSD name of the amp + std::string ampName(m_adapter->m_cecAdapter->GetDeviceOSDName(CECDEVICE_AUDIOSYSTEM)); + CLog::Log(LOGDEBUG, + "{} - CEC capable amplifier found ({}). volume will be controlled on the amp", + __FUNCTION__, ampName); + strAmpName += ampName; + + // set amp present + m_adapter->SetAudioSystemConnected(true); + auto& components = CServiceBroker::GetAppComponents(); + const auto appVolume = components.GetComponent<CApplicationVolumeHandling>(); + appVolume->SetMute(false); + appVolume->SetVolume(CApplicationVolumeHandling::VOLUME_MAXIMUM, false); + } + else + { + // set amp present + CLog::Log(LOGDEBUG, "{} - no CEC capable amplifier found", __FUNCTION__); + m_adapter->SetAudioSystemConnected(false); + } + + return strAmpName; +} + +bool CPeripheralCecAdapterUpdateThread::SetInitialConfiguration(void) +{ + // the option to make XBMC the active source is set + if (m_configuration.bActivateSource == 1) + m_adapter->m_cecAdapter->SetActiveSource(); + + // devices to wake are set + cec_logical_addresses tvOnly; + tvOnly.Clear(); + tvOnly.Set(CECDEVICE_TV); + if (!m_configuration.wakeDevices.IsEmpty() && + (m_configuration.wakeDevices != tvOnly || m_configuration.bActivateSource == 0)) + m_adapter->m_cecAdapter->PowerOnDevices(CECDEVICE_BROADCAST); + + // wait until devices are powered up + if (!WaitReady()) + return false; + + UpdateMenuLanguage(); + + // request the OSD name of the TV + std::string strNotification; + std::string tvName(m_adapter->m_cecAdapter->GetDeviceOSDName(CECDEVICE_TV)); + strNotification = StringUtils::Format("{}: {}", g_localizeStrings.Get(36016), tvName); + + std::string strAmpName = UpdateAudioSystemStatus(); + if (!strAmpName.empty()) + strNotification += StringUtils::Format("- {}", strAmpName); + + m_adapter->m_bIsReady = true; + + // and let the gui know that we're done + CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info, g_localizeStrings.Get(36000), + strNotification); + + std::unique_lock<CCriticalSection> lock(m_critSection); + m_bIsUpdating = false; + return true; +} + +bool CPeripheralCecAdapter::IsRunning(void) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_bIsRunning; +} + +void CPeripheralCecAdapterUpdateThread::Process(void) +{ + // set the initial configuration + if (!SetInitialConfiguration()) + return; + + // and wait for updates + bool bUpdate(false); + while (!m_bStop) + { + // update received + if (bUpdate || m_event.Wait(500ms)) + { + if (m_bStop) + return; + // set the new configuration + libcec_configuration configuration; + { + std::unique_lock<CCriticalSection> lock(m_critSection); + configuration = m_configuration; + m_bIsUpdating = false; + } + + CLog::Log(LOGDEBUG, "{} - updating the configuration", __FUNCTION__); + bool bConfigSet(m_adapter->m_cecAdapter->SetConfiguration(&configuration)); + // display message: config updated / failed to update + if (!bConfigSet) + CLog::Log(LOGERROR, "{} - libCEC couldn't set the new configuration", __FUNCTION__); + else + { + UpdateMenuLanguage(); + UpdateAudioSystemStatus(); + } + + CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info, g_localizeStrings.Get(36000), + g_localizeStrings.Get(bConfigSet ? 36023 : 36024)); + + { + std::unique_lock<CCriticalSection> lock(m_critSection); + if ((bUpdate = m_bNextConfigurationScheduled) == true) + { + // another update is scheduled + m_bNextConfigurationScheduled = false; + m_configuration = m_nextConfiguration; + } + else + { + // nothing left to do, wait for updates + m_bIsUpdating = false; + m_event.Reset(); + } + } + } + } +} + +void CPeripheralCecAdapter::OnDeviceRemoved(void) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_bDeviceRemoved = true; +} + +namespace PERIPHERALS +{ + +class CPeripheralCecAdapterReopenJob : public CJob +{ +public: + CPeripheralCecAdapterReopenJob(CPeripheralCecAdapter* adapter) : m_adapter(adapter) {} + ~CPeripheralCecAdapterReopenJob() override = default; + + bool DoWork(void) override { return m_adapter->ReopenConnection(false); } + +private: + CPeripheralCecAdapter* m_adapter; +}; + +}; // namespace PERIPHERALS + +bool CPeripheralCecAdapter::ReopenConnection(bool bAsync /* = false */) +{ + if (bAsync) + { + CServiceBroker::GetJobManager()->AddJob(new CPeripheralCecAdapterReopenJob(this), nullptr, + CJob::PRIORITY_NORMAL); + return true; + } + + // stop running thread + { + std::unique_lock<CCriticalSection> lock(m_critSection); + m_iExitCode = EXITCODE_RESTARTAPP; + CServiceBroker::GetAnnouncementManager()->RemoveAnnouncer(this); + StopThread(false); + } + StopThread(); + + // reset all members to their defaults + ResetMembers(); + + // reopen the connection + return InitialiseFeature(FEATURE_CEC); +} + +void CPeripheralCecAdapter::ActivateSource(void) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_bActiveSourcePending = true; +} + +void CPeripheralCecAdapter::ProcessActivateSource(void) +{ + bool bActivate(false); + + { + std::unique_lock<CCriticalSection> lock(m_critSection); + bActivate = m_bActiveSourcePending; + m_bActiveSourcePending = false; + } + + if (bActivate) + m_cecAdapter->SetActiveSource(); +} + +void CPeripheralCecAdapter::StandbyDevices(void) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_bStandbyPending = true; +} + +void CPeripheralCecAdapter::ProcessStandbyDevices(void) +{ + bool bStandby(false); + + { + std::unique_lock<CCriticalSection> lock(m_critSection); + bStandby = m_bStandbyPending; + m_bStandbyPending = false; + if (bStandby) + m_bGoingToStandby = true; + } + + if (bStandby) + { + if (!m_configuration.powerOffDevices.IsEmpty()) + { + m_standbySent = CDateTime::GetCurrentDateTime(); + m_cecAdapter->StandbyDevices(CECDEVICE_BROADCAST); + } + else if (m_bSendInactiveSource == 1) + { + CLog::Log(LOGDEBUG, "{} - sending inactive source commands", __FUNCTION__); + m_cecAdapter->SetInactiveView(); + } + } +} + +bool CPeripheralCecAdapter::ToggleDeviceState(CecStateChange mode /*= STATE_SWITCH_TOGGLE */, + bool forceType /*= false */) +{ + if (!IsRunning()) + return false; + if (m_cecAdapter->IsLibCECActiveSource() && + (mode == STATE_SWITCH_TOGGLE || mode == STATE_STANDBY)) + { + CLog::Log(LOGDEBUG, "{} - putting CEC device on standby...", __FUNCTION__); + StandbyDevices(); + return false; + } + else if (mode == STATE_SWITCH_TOGGLE || mode == STATE_ACTIVATE_SOURCE) + { + CLog::Log(LOGDEBUG, "{} - waking up CEC device...", __FUNCTION__); + ActivateSource(); + return true; + } + + return false; +} diff --git a/xbmc/peripherals/devices/PeripheralCecAdapter.h b/xbmc/peripherals/devices/PeripheralCecAdapter.h new file mode 100644 index 0000000..b3755b3 --- /dev/null +++ b/xbmc/peripherals/devices/PeripheralCecAdapter.h @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#if !defined(HAVE_LIBCEC) +#include "Peripheral.h" + +// an empty implementation, so CPeripherals can be compiled without a bunch of #ifdef's when libCEC +// is not available +namespace PERIPHERALS +{ +class CPeripheralCecAdapter : public CPeripheral +{ +public: + bool HasAudioControl(void) { return false; } + void VolumeUp(void) {} + void VolumeDown(void) {} + bool IsMuted(void) { return false; } + void ToggleMute(void) {} + bool ToggleDeviceState(CecStateChange mode = STATE_SWITCH_TOGGLE, bool forceType = false) + { + return false; + } + + int GetButton(void) { return 0; } + unsigned int GetHoldTime(void) { return 0; } + void ResetButton(void) {} +}; +} // namespace PERIPHERALS + +#else + +#include "PeripheralHID.h" +#include "XBDateTime.h" +#include "interfaces/AnnouncementManager.h" +#include "threads/CriticalSection.h" +#include "threads/Thread.h" + +#include <chrono> +#include <queue> +#include <vector> + +// undefine macro isset, it collides with function in cectypes.h +#ifdef isset +#undef isset +#endif +#include <libcec/cectypes.h> + +class CVariant; + +namespace CEC +{ +class ICECAdapter; +}; + +namespace PERIPHERALS +{ +class CPeripheralCecAdapterUpdateThread; +class CPeripheralCecAdapterReopenJob; + +typedef struct +{ + int iButton; + unsigned int iDuration; +} CecButtonPress; + +typedef enum +{ + VOLUME_CHANGE_NONE, + VOLUME_CHANGE_UP, + VOLUME_CHANGE_DOWN, + VOLUME_CHANGE_MUTE +} CecVolumeChange; + +class CPeripheralCecAdapter : public CPeripheralHID, + public ANNOUNCEMENT::IAnnouncer, + private CThread +{ + friend class CPeripheralCecAdapterUpdateThread; + friend class CPeripheralCecAdapterReopenJob; + +public: + CPeripheralCecAdapter(CPeripherals& manager, + const PeripheralScanResult& scanResult, + CPeripheralBus* bus); + ~CPeripheralCecAdapter(void) override; + + void Announce(ANNOUNCEMENT::AnnouncementFlag flag, + const std::string& sender, + const std::string& message, + const CVariant& data) override; + + // audio control + bool HasAudioControl(void); + void VolumeUp(void); + void VolumeDown(void); + void ToggleMute(void); + bool IsMuted(void); + + // CPeripheral callbacks + void OnSettingChanged(const std::string& strChangedSetting) override; + void OnDeviceRemoved(void) override; + + // input + int GetButton(void); + unsigned int GetHoldTime(void); + void ResetButton(void); + + // public CEC methods + void ActivateSource(void); + void StandbyDevices(void); + bool ToggleDeviceState(CecStateChange mode = STATE_SWITCH_TOGGLE, bool forceType = false); + +private: + bool InitialiseFeature(const PeripheralFeature feature) override; + void ResetMembers(void); + void Process(void) override; + bool IsRunning(void) const; + + bool OpenConnection(void); + bool ReopenConnection(bool bAsync = false); + + void SetConfigurationFromSettings(void); + void SetConfigurationFromLibCEC(const CEC::libcec_configuration& config); + void SetVersionInfo(const CEC::libcec_configuration& configuration); + + static void ReadLogicalAddresses(const std::string& strString, + CEC::cec_logical_addresses& addresses); + static void ReadLogicalAddresses(int iLocalisedId, CEC::cec_logical_addresses& addresses); + bool WriteLogicalAddresses(const CEC::cec_logical_addresses& addresses, + const std::string& strSettingName, + const std::string& strAdvancedSettingName); + + void ProcessActivateSource(void); + void ProcessStandbyDevices(void); + void ProcessVolumeChange(void); + + void PushCecKeypress(const CEC::cec_keypress& key); + void PushCecKeypress(const CecButtonPress& key); + void GetNextKey(void); + + void SetAudioSystemConnected(bool bSetTo); + void SetMenuLanguage(const char* strLanguage); + void OnTvStandby(void); + + // callbacks from libCEC + static void CecLogMessage(void* cbParam, const CEC::cec_log_message* message); + static void CecCommand(void* cbParam, const CEC::cec_command* command); + static void CecConfiguration(void* cbParam, const CEC::libcec_configuration* config); + static void CecAlert(void* cbParam, + const CEC::libcec_alert alert, + const CEC::libcec_parameter data); + static void CecSourceActivated(void* param, + const CEC::cec_logical_address address, + const uint8_t activated); + static void CecKeyPress(void* cbParam, const CEC::cec_keypress* key); + + CEC::ICECAdapter* m_cecAdapter; + bool m_bStarted; + bool m_bHasButton; + bool m_bIsReady; + bool m_bHasConnectedAudioSystem; + std::string m_strMenuLanguage; + CDateTime m_standbySent; + std::vector<CecButtonPress> m_buttonQueue; + CecButtonPress m_currentButton; + std::queue<CecVolumeChange> m_volumeChangeQueue; + std::chrono::time_point<std::chrono::steady_clock> m_lastKeypress; + CecVolumeChange m_lastChange; + int m_iExitCode; + bool m_bIsMuted; + bool m_bGoingToStandby; + bool m_bIsRunning; + bool m_bDeviceRemoved; + CPeripheralCecAdapterUpdateThread* m_queryThread; + CEC::ICECCallbacks m_callbacks; + mutable CCriticalSection m_critSection; + CEC::libcec_configuration m_configuration; + bool m_bActiveSourcePending; + bool m_bStandbyPending; + CDateTime m_preventActivateSourceOnPlay; + bool m_bActiveSourceBeforeStandby; + bool m_bOnPlayReceived; + bool m_bPlaybackPaused; + std::string m_strComPort; + bool m_bPowerOnScreensaver; + bool m_bUseTVMenuLanguage; + bool m_bSendInactiveSource; + bool m_bPowerOffScreensaver; + bool m_bShutdownOnStandby; +}; + +class CPeripheralCecAdapterUpdateThread : public CThread +{ +public: + CPeripheralCecAdapterUpdateThread(CPeripheralCecAdapter* adapter, + CEC::libcec_configuration* configuration); + ~CPeripheralCecAdapterUpdateThread(void) override; + + void Signal(void); + bool UpdateConfiguration(CEC::libcec_configuration* configuration); + +protected: + void UpdateMenuLanguage(void); + std::string UpdateAudioSystemStatus(void); + bool WaitReady(void); + bool SetInitialConfiguration(void); + void Process(void) override; + + CPeripheralCecAdapter* m_adapter; + CEvent m_event; + CCriticalSection m_critSection; + CEC::libcec_configuration m_configuration; + CEC::libcec_configuration m_nextConfiguration; + bool m_bNextConfigurationScheduled; + bool m_bIsUpdating; +}; +} // namespace PERIPHERALS + +#endif diff --git a/xbmc/peripherals/devices/PeripheralDisk.cpp b/xbmc/peripherals/devices/PeripheralDisk.cpp new file mode 100644 index 0000000..9b0682c --- /dev/null +++ b/xbmc/peripherals/devices/PeripheralDisk.cpp @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PeripheralDisk.h" + +#include "guilib/LocalizeStrings.h" + +using namespace PERIPHERALS; + +CPeripheralDisk::CPeripheralDisk(CPeripherals& manager, + const PeripheralScanResult& scanResult, + CPeripheralBus* bus) + : CPeripheral(manager, scanResult, bus) +{ + m_strDeviceName = scanResult.m_strDeviceName.empty() ? g_localizeStrings.Get(35003) + : scanResult.m_strDeviceName; + m_features.push_back(FEATURE_DISK); +} diff --git a/xbmc/peripherals/devices/PeripheralDisk.h b/xbmc/peripherals/devices/PeripheralDisk.h new file mode 100644 index 0000000..f81b31e --- /dev/null +++ b/xbmc/peripherals/devices/PeripheralDisk.h @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "Peripheral.h" + +namespace PERIPHERALS +{ +class CPeripheralDisk : public CPeripheral +{ +public: + CPeripheralDisk(CPeripherals& manager, + const PeripheralScanResult& scanResult, + CPeripheralBus* bus); + ~CPeripheralDisk(void) override = default; +}; +} // namespace PERIPHERALS diff --git a/xbmc/peripherals/devices/PeripheralHID.cpp b/xbmc/peripherals/devices/PeripheralHID.cpp new file mode 100644 index 0000000..e3d061b --- /dev/null +++ b/xbmc/peripherals/devices/PeripheralHID.cpp @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PeripheralHID.h" + +#include "guilib/LocalizeStrings.h" +#include "input/InputManager.h" +#include "peripherals/Peripherals.h" +#include "utils/log.h" + +using namespace PERIPHERALS; + +CPeripheralHID::CPeripheralHID(CPeripherals& manager, + const PeripheralScanResult& scanResult, + CPeripheralBus* bus) + : CPeripheral(manager, scanResult, bus) +{ + m_strDeviceName = scanResult.m_strDeviceName.empty() ? g_localizeStrings.Get(35001) + : scanResult.m_strDeviceName; + m_features.push_back(FEATURE_HID); +} + +CPeripheralHID::~CPeripheralHID(void) +{ + if (!m_strKeymap.empty() && !GetSettingBool("do_not_use_custom_keymap")) + { + CLog::Log(LOGDEBUG, "{} - switching active keymapping to: default", __FUNCTION__); + m_manager.GetInputManager().RemoveKeymap(m_strKeymap); + } +} + +bool CPeripheralHID::InitialiseFeature(const PeripheralFeature feature) +{ + if (feature == FEATURE_HID && !m_bInitialised) + { + m_bInitialised = true; + + if (HasSetting("keymap")) + m_strKeymap = GetSettingString("keymap"); + + if (m_strKeymap.empty()) + { + m_strKeymap = StringUtils::Format("v{}p{}", VendorIdAsString(), ProductIdAsString()); + SetSetting("keymap", m_strKeymap); + } + + if (!IsSettingVisible("keymap")) + SetSettingVisible("do_not_use_custom_keymap", false); + + if (!m_strKeymap.empty()) + { + bool bKeymapEnabled(!GetSettingBool("do_not_use_custom_keymap")); + if (bKeymapEnabled) + { + CLog::Log(LOGDEBUG, "{} - adding keymapping for: {}", __FUNCTION__, m_strKeymap); + m_manager.GetInputManager().AddKeymap(m_strKeymap); + } + else + { + CLog::Log(LOGDEBUG, "{} - removing keymapping for: {}", __FUNCTION__, m_strKeymap); + m_manager.GetInputManager().RemoveKeymap(m_strKeymap); + } + } + + CLog::Log(LOGDEBUG, "{} - initialised HID device ({}:{})", __FUNCTION__, m_strVendorId, + m_strProductId); + } + + return CPeripheral::InitialiseFeature(feature); +} + +void CPeripheralHID::OnSettingChanged(const std::string& strChangedSetting) +{ + if (m_bInitialised && ((StringUtils::EqualsNoCase(strChangedSetting, "keymap") && + !GetSettingBool("do_not_use_custom_keymap")) || + StringUtils::EqualsNoCase(strChangedSetting, "keymap_enabled"))) + { + m_bInitialised = false; + InitialiseFeature(FEATURE_HID); + } +} diff --git a/xbmc/peripherals/devices/PeripheralHID.h b/xbmc/peripherals/devices/PeripheralHID.h new file mode 100644 index 0000000..3b35dfd --- /dev/null +++ b/xbmc/peripherals/devices/PeripheralHID.h @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "Peripheral.h" +#include "input/XBMC_keyboard.h" + +namespace PERIPHERALS +{ +class CPeripheralHID : public CPeripheral +{ +public: + CPeripheralHID(CPeripherals& manager, + const PeripheralScanResult& scanResult, + CPeripheralBus* bus); + ~CPeripheralHID(void) override; + bool InitialiseFeature(const PeripheralFeature feature) override; + virtual bool LookupSymAndUnicode(XBMC_keysym& keysym, uint8_t* key, char* unicode) + { + return false; + } + void OnSettingChanged(const std::string& strChangedSetting) override; + +protected: + std::string m_strKeymap; +}; +} // namespace PERIPHERALS diff --git a/xbmc/peripherals/devices/PeripheralImon.cpp b/xbmc/peripherals/devices/PeripheralImon.cpp new file mode 100644 index 0000000..6683f8b --- /dev/null +++ b/xbmc/peripherals/devices/PeripheralImon.cpp @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2012-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PeripheralImon.h" + +#include "input/InputManager.h" +#include "settings/Settings.h" +#include "utils/log.h" + +using namespace PERIPHERALS; + +std::atomic<long> CPeripheralImon::m_lCountOfImonsConflictWithDInput(0L); + +CPeripheralImon::CPeripheralImon(CPeripherals& manager, + const PeripheralScanResult& scanResult, + CPeripheralBus* bus) + : CPeripheralHID(manager, scanResult, bus) +{ + m_features.push_back(FEATURE_IMON); + m_bImonConflictsWithDInput = false; +} + +void CPeripheralImon::OnDeviceRemoved() +{ + if (m_bImonConflictsWithDInput) + { + if (--m_lCountOfImonsConflictWithDInput == 0) + ActionOnImonConflict(false); + } +} + +bool CPeripheralImon::InitialiseFeature(const PeripheralFeature feature) +{ + if (feature == FEATURE_IMON) + { +#if defined(TARGET_WINDOWS) + if (HasSetting("disable_winjoystick") && GetSettingBool("disable_winjoystick")) + m_bImonConflictsWithDInput = true; + else +#endif // TARGET_WINDOWS + m_bImonConflictsWithDInput = false; + + if (m_bImonConflictsWithDInput) + { + ++m_lCountOfImonsConflictWithDInput; + ActionOnImonConflict(true); + } + return CPeripheral::InitialiseFeature(feature); + } + + return CPeripheralHID::InitialiseFeature(feature); +} + +void CPeripheralImon::AddSetting(const std::string& strKey, + const std::shared_ptr<const CSetting>& setting, + int order) +{ +#if !defined(TARGET_WINDOWS) + if (strKey.compare("disable_winjoystick") != 0) +#endif // !TARGET_WINDOWS + CPeripheralHID::AddSetting(strKey, setting, order); +} + +void CPeripheralImon::OnSettingChanged(const std::string& strChangedSetting) +{ + if (strChangedSetting.compare("disable_winjoystick") == 0) + { + if (m_bImonConflictsWithDInput && !GetSettingBool("disable_winjoystick")) + { + m_bImonConflictsWithDInput = false; + if (--m_lCountOfImonsConflictWithDInput == 0) + ActionOnImonConflict(false); + } + else if (!m_bImonConflictsWithDInput && GetSettingBool("disable_winjoystick")) + { + m_bImonConflictsWithDInput = true; + ++m_lCountOfImonsConflictWithDInput; + ActionOnImonConflict(true); + } + } +} + +void CPeripheralImon::ActionOnImonConflict(bool deviceInserted /*= true*/) +{ +} diff --git a/xbmc/peripherals/devices/PeripheralImon.h b/xbmc/peripherals/devices/PeripheralImon.h new file mode 100644 index 0000000..612d059 --- /dev/null +++ b/xbmc/peripherals/devices/PeripheralImon.h @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2012-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "PeripheralHID.h" + +#include <atomic> + +class CSetting; + +namespace PERIPHERALS +{ +class CPeripheralImon : public CPeripheralHID +{ +public: + CPeripheralImon(CPeripherals& manager, + const PeripheralScanResult& scanResult, + CPeripheralBus* bus); + ~CPeripheralImon(void) override = default; + bool InitialiseFeature(const PeripheralFeature feature) override; + void OnSettingChanged(const std::string& strChangedSetting) override; + void OnDeviceRemoved() override; + void AddSetting(const std::string& strKey, + const std::shared_ptr<const CSetting>& setting, + int order) override; + inline bool IsImonConflictsWithDInput() { return m_bImonConflictsWithDInput; } + static inline long GetCountOfImonsConflictWithDInput() + { + return m_lCountOfImonsConflictWithDInput; + } + static void ActionOnImonConflict(bool deviceInserted = true); + +private: + bool m_bImonConflictsWithDInput; + static std::atomic<long> m_lCountOfImonsConflictWithDInput; +}; +} // namespace PERIPHERALS diff --git a/xbmc/peripherals/devices/PeripheralJoystick.cpp b/xbmc/peripherals/devices/PeripheralJoystick.cpp new file mode 100644 index 0000000..bccad91 --- /dev/null +++ b/xbmc/peripherals/devices/PeripheralJoystick.cpp @@ -0,0 +1,538 @@ +/* + * Copyright (C) 2014-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PeripheralJoystick.h" + +#include "ServiceBroker.h" +#include "addons/AddonInstaller.h" +#include "addons/AddonManager.h" +#include "application/Application.h" +#include "games/GameServices.h" +#include "games/controllers/Controller.h" +#include "games/controllers/ControllerIDs.h" +#include "games/controllers/ControllerManager.h" +#include "input/InputManager.h" +#include "input/joysticks/DeadzoneFilter.h" +#include "input/joysticks/JoystickMonitor.h" +#include "input/joysticks/JoystickTranslator.h" +#include "input/joysticks/RumbleGenerator.h" +#include "input/joysticks/interfaces/IDriverHandler.h" +#include "input/joysticks/keymaps/KeymapHandling.h" +#include "peripherals/Peripherals.h" +#include "peripherals/addons/AddonButtonMap.h" +#include "peripherals/bus/virtual/PeripheralBusAddon.h" +#include "utils/log.h" + +#include <algorithm> +#include <mutex> + +using namespace KODI; +using namespace JOYSTICK; +using namespace PERIPHERALS; + +CPeripheralJoystick::CPeripheralJoystick(CPeripherals& manager, + const PeripheralScanResult& scanResult, + CPeripheralBus* bus) + : CPeripheral(manager, scanResult, bus), + m_requestedPort(JOYSTICK_PORT_UNKNOWN), + m_buttonCount(0), + m_hatCount(0), + m_axisCount(0), + m_motorCount(0), + m_supportsPowerOff(false), + m_rumbleGenerator(new CRumbleGenerator) +{ + m_features.push_back(FEATURE_JOYSTICK); + // FEATURE_RUMBLE conditionally added via SetMotorCount() +} + +CPeripheralJoystick::~CPeripheralJoystick(void) +{ + if (m_rumbleGenerator) + { + m_rumbleGenerator->AbortRumble(); + m_rumbleGenerator.reset(); + } + + if (m_joystickMonitor) + { + UnregisterInputHandler(m_joystickMonitor.get()); + m_joystickMonitor.reset(); + } + + m_appInput.reset(); + m_deadzoneFilter.reset(); + m_buttonMap.reset(); + + // Wait for remaining install tasks + for (std::future<void>& task : m_installTasks) + task.wait(); +} + +bool CPeripheralJoystick::InitialiseFeature(const PeripheralFeature feature) +{ + bool bSuccess = false; + + if (CPeripheral::InitialiseFeature(feature)) + { + if (feature == FEATURE_JOYSTICK) + { + // Ensure an add-on is present to translate input + PeripheralAddonPtr addon = m_manager.GetAddonWithButtonMap(this); + if (!addon) + { + CLog::Log(LOGERROR, "CPeripheralJoystick: No button mapping add-on for {}", m_strLocation); + } + else + { + if (m_bus->InitializeProperties(*this)) + bSuccess = true; + else + CLog::Log(LOGERROR, "CPeripheralJoystick: Invalid location ({})", m_strLocation); + } + + if (bSuccess) + { + m_buttonMap = + std::make_unique<CAddonButtonMap>(this, addon, DEFAULT_CONTROLLER_ID, m_manager); + if (m_buttonMap->Load()) + { + InitializeDeadzoneFiltering(*m_buttonMap); + InitializeControllerProfile(*m_buttonMap); + } + else + { + CLog::Log(LOGERROR, "CPeripheralJoystick: Failed to load button map for {}", + m_strLocation); + m_buttonMap.reset(); + } + + // Give joystick monitor priority over default controller + m_appInput.reset( + new CKeymapHandling(this, false, m_manager.GetInputManager().KeymapEnvironment())); + m_joystickMonitor.reset(new CJoystickMonitor); + RegisterInputHandler(m_joystickMonitor.get(), false); + } + } + else if (feature == FEATURE_RUMBLE) + { + bSuccess = true; // Nothing to do + } + else if (feature == FEATURE_POWER_OFF) + { + bSuccess = true; // Nothing to do + } + } + + return bSuccess; +} + +void CPeripheralJoystick::InitializeDeadzoneFiltering(IButtonMap& buttonMap) +{ + m_deadzoneFilter.reset(new CDeadzoneFilter(&buttonMap, this)); +} + +void CPeripheralJoystick::InitializeControllerProfile(IButtonMap& buttonMap) +{ + const std::string controllerId = buttonMap.GetAppearance(); + if (controllerId.empty()) + return; + + auto controller = m_manager.GetControllerProfiles().GetController(controllerId); + if (controller) + CPeripheral::SetControllerProfile(controller); + else + { + std::unique_lock<CCriticalSection> lock(m_controllerInstallMutex); + + // Deposit controller into queue + m_controllersToInstall.emplace(controllerId); + + // Clean up finished install tasks + m_installTasks.erase(std::remove_if(m_installTasks.begin(), m_installTasks.end(), + [](std::future<void>& task) { + return task.wait_for(std::chrono::seconds(0)) == + std::future_status::ready; + }), + m_installTasks.end()); + + // Install controller off-thread + std::future<void> installTask = std::async(std::launch::async, [this]() { + // Withdraw controller from queue + std::string controllerToInstall; + { + std::unique_lock<CCriticalSection> lock(m_controllerInstallMutex); + if (!m_controllersToInstall.empty()) + { + controllerToInstall = m_controllersToInstall.front(); + m_controllersToInstall.pop(); + } + } + + // Do the install + GAME::ControllerPtr controller = InstallAsync(controllerToInstall); + if (controller) + CPeripheral::SetControllerProfile(controller); + }); + + // Hold the task to prevent the destructor from completing during an install + m_installTasks.emplace_back(std::move(installTask)); + } +} + +void CPeripheralJoystick::OnUserNotification() +{ + IInputReceiver* inputReceiver = m_appInput->GetInputReceiver(m_rumbleGenerator->ControllerID()); + m_rumbleGenerator->NotifyUser(inputReceiver); +} + +bool CPeripheralJoystick::TestFeature(PeripheralFeature feature) +{ + bool bSuccess = false; + + switch (feature) + { + case FEATURE_RUMBLE: + { + IInputReceiver* inputReceiver = + m_appInput->GetInputReceiver(m_rumbleGenerator->ControllerID()); + bSuccess = m_rumbleGenerator->DoTest(inputReceiver); + break; + } + case FEATURE_POWER_OFF: + if (m_supportsPowerOff) + { + PowerOff(); + bSuccess = true; + } + break; + default: + break; + } + + return bSuccess; +} + +void CPeripheralJoystick::PowerOff() +{ + m_bus->PowerOff(m_strLocation); +} + +void CPeripheralJoystick::RegisterJoystickDriverHandler(IDriverHandler* handler, bool bPromiscuous) +{ + std::unique_lock<CCriticalSection> lock(m_handlerMutex); + + DriverHandler driverHandler = {handler, bPromiscuous}; + m_driverHandlers.insert(m_driverHandlers.begin(), driverHandler); +} + +void CPeripheralJoystick::UnregisterJoystickDriverHandler(IDriverHandler* handler) +{ + std::unique_lock<CCriticalSection> lock(m_handlerMutex); + + m_driverHandlers.erase(std::remove_if(m_driverHandlers.begin(), m_driverHandlers.end(), + [handler](const DriverHandler& driverHandler) { + return driverHandler.handler == handler; + }), + m_driverHandlers.end()); +} + +IKeymap* CPeripheralJoystick::GetKeymap(const std::string& controllerId) +{ + return m_appInput->GetKeymap(controllerId); +} + +GAME::ControllerPtr CPeripheralJoystick::ControllerProfile() const +{ + // Button map has the freshest state + if (m_buttonMap) + { + const std::string controllerId = m_buttonMap->GetAppearance(); + if (!controllerId.empty()) + { + auto controller = m_manager.GetControllerProfiles().GetController(controllerId); + if (controller) + return controller; + } + } + + // Fall back to last set controller profile + if (m_controllerProfile) + return m_controllerProfile; + + // Fall back to default controller + return m_manager.GetControllerProfiles().GetDefaultController(); +} + +void CPeripheralJoystick::SetControllerProfile(const KODI::GAME::ControllerPtr& controller) +{ + CPeripheral::SetControllerProfile(controller); + + // Save preference to buttonmap + if (m_buttonMap) + { + if (m_buttonMap->SetAppearance(controller->ID())) + m_buttonMap->SaveButtonMap(); + } +} + +bool CPeripheralJoystick::OnButtonMotion(unsigned int buttonIndex, bool bPressed) +{ + // Silence debug log if controllers are not enabled + if (m_manager.GetInputManager().IsControllerEnabled()) + { + CLog::Log(LOGDEBUG, "BUTTON [ {} ] on \"{}\" {}", buttonIndex, DeviceName(), + bPressed ? "pressed" : "released"); + } + + // Avoid sending activated input if the app is in the background + if (bPressed && !g_application.IsAppFocused()) + return false; + + m_lastActive = CDateTime::GetCurrentDateTime(); + + std::unique_lock<CCriticalSection> lock(m_handlerMutex); + + // Check GUI setting and send button release if controllers are disabled + if (!m_manager.GetInputManager().IsControllerEnabled()) + { + for (std::vector<DriverHandler>::iterator it = m_driverHandlers.begin(); + it != m_driverHandlers.end(); ++it) + it->handler->OnButtonMotion(buttonIndex, false); + return true; + } + + // Process promiscuous handlers + for (auto& it : m_driverHandlers) + { + if (it.bPromiscuous) + it.handler->OnButtonMotion(buttonIndex, bPressed); + } + + bool bHandled = false; + + // Process regular handlers until one is handled + for (auto& it : m_driverHandlers) + { + if (!it.bPromiscuous) + { + bHandled |= it.handler->OnButtonMotion(buttonIndex, bPressed); + + // If button is released, force bHandled to false to notify all handlers. + // This avoids "sticking". + if (!bPressed) + bHandled = false; + + // Once a button is handled, we're done + if (bHandled) + break; + } + } + + return bHandled; +} + +bool CPeripheralJoystick::OnHatMotion(unsigned int hatIndex, HAT_STATE state) +{ + // Silence debug log if controllers are not enabled + if (m_manager.GetInputManager().IsControllerEnabled()) + { + CLog::Log(LOGDEBUG, "HAT [ {} ] on \"{}\" {}", hatIndex, DeviceName(), + CJoystickTranslator::HatStateToString(state)); + } + + // Avoid sending activated input if the app is in the background + if (state != HAT_STATE::NONE && !g_application.IsAppFocused()) + return false; + + m_lastActive = CDateTime::GetCurrentDateTime(); + + std::unique_lock<CCriticalSection> lock(m_handlerMutex); + + // Check GUI setting and send hat unpressed if controllers are disabled + if (!m_manager.GetInputManager().IsControllerEnabled()) + { + for (std::vector<DriverHandler>::iterator it = m_driverHandlers.begin(); + it != m_driverHandlers.end(); ++it) + it->handler->OnHatMotion(hatIndex, HAT_STATE::NONE); + return true; + } + + // Process promiscuous handlers + for (auto& it : m_driverHandlers) + { + if (it.bPromiscuous) + it.handler->OnHatMotion(hatIndex, state); + } + + bool bHandled = false; + + // Process regular handlers until one is handled + for (auto& it : m_driverHandlers) + { + if (!it.bPromiscuous) + { + bHandled |= it.handler->OnHatMotion(hatIndex, state); + + // If hat is centered, force bHandled to false to notify all handlers. + // This avoids "sticking". + if (state == HAT_STATE::NONE) + bHandled = false; + + // Once a hat is handled, we're done + if (bHandled) + break; + } + } + + return bHandled; +} + +bool CPeripheralJoystick::OnAxisMotion(unsigned int axisIndex, float position) +{ + // Get axis properties + int center = 0; + unsigned int range = 1; + if (m_buttonMap) + m_buttonMap->GetAxisProperties(axisIndex, center, range); + + // Apply deadzone filtering + if (center == 0 && m_deadzoneFilter) + position = m_deadzoneFilter->FilterAxis(axisIndex, position); + + // Avoid sending activated input if the app is in the background + if (position != static_cast<float>(center) && !g_application.IsAppFocused()) + return false; + + std::unique_lock<CCriticalSection> lock(m_handlerMutex); + + // Check GUI setting and send analog axis centered if controllers are disabled + if (!m_manager.GetInputManager().IsControllerEnabled()) + { + for (std::vector<DriverHandler>::iterator it = m_driverHandlers.begin(); + it != m_driverHandlers.end(); ++it) + it->handler->OnAxisMotion(axisIndex, static_cast<float>(center), center, range); + return true; + } + + // Process promiscuous handlers + for (auto& it : m_driverHandlers) + { + if (it.bPromiscuous) + it.handler->OnAxisMotion(axisIndex, position, center, range); + } + + bool bHandled = false; + + // Process regular handlers until one is handled + for (auto& it : m_driverHandlers) + { + if (!it.bPromiscuous) + { + bHandled |= it.handler->OnAxisMotion(axisIndex, position, center, range); + + // If axis is centered, force bHandled to false to notify all handlers. + // This avoids "sticking". + if (position == static_cast<float>(center)) + bHandled = false; + + // Once an axis is handled, we're done + if (bHandled) + break; + } + } + + if (bHandled) + m_lastActive = CDateTime::GetCurrentDateTime(); + + return bHandled; +} + +void CPeripheralJoystick::OnInputFrame(void) +{ + std::unique_lock<CCriticalSection> lock(m_handlerMutex); + + for (auto& it : m_driverHandlers) + it.handler->OnInputFrame(); +} + +bool CPeripheralJoystick::SetMotorState(unsigned int motorIndex, float magnitude) +{ + bool bHandled = false; + + if (m_mappedBusType == PERIPHERAL_BUS_ADDON) + { + CPeripheralBusAddon* addonBus = static_cast<CPeripheralBusAddon*>(m_bus); + if (addonBus) + { + bHandled = addonBus->SendRumbleEvent(m_strLocation, motorIndex, magnitude); + } + } + return bHandled; +} + +void CPeripheralJoystick::SetMotorCount(unsigned int motorCount) +{ + m_motorCount = motorCount; + + if (m_motorCount == 0) + m_features.erase(std::remove(m_features.begin(), m_features.end(), FEATURE_RUMBLE), + m_features.end()); + else if (std::find(m_features.begin(), m_features.end(), FEATURE_RUMBLE) == m_features.end()) + m_features.push_back(FEATURE_RUMBLE); +} + +void CPeripheralJoystick::SetSupportsPowerOff(bool bSupportsPowerOff) +{ + m_supportsPowerOff = bSupportsPowerOff; + + if (!m_supportsPowerOff) + m_features.erase(std::remove(m_features.begin(), m_features.end(), FEATURE_POWER_OFF), + m_features.end()); + else if (std::find(m_features.begin(), m_features.end(), FEATURE_POWER_OFF) == m_features.end()) + m_features.push_back(FEATURE_POWER_OFF); +} + +GAME::ControllerPtr CPeripheralJoystick::InstallAsync(const std::string& controllerId) +{ + GAME::ControllerPtr controller; + + // Only 1 install at a time. Remaining installs will wake when this one + // is done. + std::unique_lock<CCriticalSection> lockInstall(m_manager.GetAddonInstallMutex()); + + CLog::LogF(LOGDEBUG, "Installing {}", controllerId); + + if (InstallSync(controllerId)) + controller = m_manager.GetControllerProfiles().GetController(controllerId); + else + CLog::LogF(LOGERROR, "Failed to install {}", controllerId); + + return controller; +} + +bool CPeripheralJoystick::InstallSync(const std::string& controllerId) +{ + // If the addon isn't installed we need to install it + bool installed = CServiceBroker::GetAddonMgr().IsAddonInstalled(controllerId); + if (!installed) + { + ADDON::AddonPtr installedAddon; + installed = ADDON::CAddonInstaller::GetInstance().InstallModal( + controllerId, installedAddon, ADDON::InstallModalPrompt::CHOICE_NO); + } + + if (installed) + { + // Make sure add-on is enabled + if (CServiceBroker::GetAddonMgr().IsAddonDisabled(controllerId)) + CServiceBroker::GetAddonMgr().EnableAddon(controllerId); + } + + return installed; +} diff --git a/xbmc/peripherals/devices/PeripheralJoystick.h b/xbmc/peripherals/devices/PeripheralJoystick.h new file mode 100644 index 0000000..aa55ad7 --- /dev/null +++ b/xbmc/peripherals/devices/PeripheralJoystick.h @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2014-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "Peripheral.h" +#include "XBDateTime.h" +#include "games/controllers/ControllerTypes.h" +#include "input/joysticks/JoystickTypes.h" +#include "input/joysticks/interfaces/IDriverReceiver.h" +#include "threads/CriticalSection.h" + +#include <future> +#include <memory> +#include <queue> +#include <string> +#include <vector> + +#define JOYSTICK_PORT_UNKNOWN (-1) + +namespace KODI +{ +namespace JOYSTICK +{ +class CDeadzoneFilter; +class CKeymapHandling; +class CRumbleGenerator; +class IButtonMap; +class IDriverHandler; +class IInputHandler; +} // namespace JOYSTICK +} // namespace KODI + +namespace PERIPHERALS +{ +class CPeripherals; + +class CPeripheralJoystick : public CPeripheral, //! @todo extend CPeripheralHID + public KODI::JOYSTICK::IDriverReceiver +{ +public: + CPeripheralJoystick(CPeripherals& manager, + const PeripheralScanResult& scanResult, + CPeripheralBus* bus); + + ~CPeripheralJoystick(void) override; + + // implementation of CPeripheral + bool InitialiseFeature(const PeripheralFeature feature) override; + void OnUserNotification() override; + bool TestFeature(PeripheralFeature feature) override; + void RegisterJoystickDriverHandler(KODI::JOYSTICK::IDriverHandler* handler, + bool bPromiscuous) override; + void UnregisterJoystickDriverHandler(KODI::JOYSTICK::IDriverHandler* handler) override; + KODI::JOYSTICK::IDriverReceiver* GetDriverReceiver() override { return this; } + IKeymap* GetKeymap(const std::string& controllerId) override; + CDateTime LastActive() override { return m_lastActive; } + KODI::GAME::ControllerPtr ControllerProfile() const override; + void SetControllerProfile(const KODI::GAME::ControllerPtr& controller) override; + + bool OnButtonMotion(unsigned int buttonIndex, bool bPressed); + bool OnHatMotion(unsigned int hatIndex, KODI::JOYSTICK::HAT_STATE state); + bool OnAxisMotion(unsigned int axisIndex, float position); + void OnInputFrame(void); + + // implementation of IDriverReceiver + bool SetMotorState(unsigned int motorIndex, float magnitude) override; + + /*! + * \brief Get the name of the driver or API providing this joystick + */ + const std::string& Provider(void) const { return m_strProvider; } + + /*! + * \brief Get the specific port number requested by this joystick + * + * This could indicate that the joystick is connected to a hardware port + * with a number label; some controllers, such as the Xbox 360 controller, + * also have LEDs that indicate the controller is on a specific port. + * + * \return The 0-indexed port number, or JOYSTICK_PORT_UNKNOWN if no port is requested + */ + int RequestedPort(void) const { return m_requestedPort; } + + /*! + * \brief Get the number of elements reported by the driver + */ + unsigned int ButtonCount(void) const { return m_buttonCount; } + unsigned int HatCount(void) const { return m_hatCount; } + unsigned int AxisCount(void) const { return m_axisCount; } + unsigned int MotorCount(void) const { return m_motorCount; } + bool SupportsPowerOff(void) const { return m_supportsPowerOff; } + + /*! + * \brief Set joystick properties + */ + void SetProvider(const std::string& provider) { m_strProvider = provider; } + void SetRequestedPort(int port) { m_requestedPort = port; } + void SetButtonCount(unsigned int buttonCount) { m_buttonCount = buttonCount; } + void SetHatCount(unsigned int hatCount) { m_hatCount = hatCount; } + void SetAxisCount(unsigned int axisCount) { m_axisCount = axisCount; } + void SetMotorCount(unsigned int motorCount); // specialized to update m_features + void SetSupportsPowerOff(bool bSupportsPowerOff); // specialized to update m_features + +protected: + void InitializeDeadzoneFiltering(KODI::JOYSTICK::IButtonMap& buttonMap); + void InitializeControllerProfile(KODI::JOYSTICK::IButtonMap& buttonMap); + + void PowerOff(); + + // Helper functions + KODI::GAME::ControllerPtr InstallAsync(const std::string& controllerId); + static bool InstallSync(const std::string& controllerId); + + struct DriverHandler + { + KODI::JOYSTICK::IDriverHandler* handler; + bool bPromiscuous; + }; + + // State parameters + std::string m_strProvider; + int m_requestedPort; + unsigned int m_buttonCount; + unsigned int m_hatCount; + unsigned int m_axisCount; + unsigned int m_motorCount; + bool m_supportsPowerOff; + CDateTime m_lastActive; + std::queue<std::string> m_controllersToInstall; + std::vector<std::future<void>> m_installTasks; + + // Input clients + std::unique_ptr<KODI::JOYSTICK::CKeymapHandling> m_appInput; + std::unique_ptr<KODI::JOYSTICK::CRumbleGenerator> m_rumbleGenerator; + std::unique_ptr<KODI::JOYSTICK::IInputHandler> m_joystickMonitor; + std::unique_ptr<KODI::JOYSTICK::IButtonMap> m_buttonMap; + std::unique_ptr<KODI::JOYSTICK::CDeadzoneFilter> m_deadzoneFilter; + std::vector<DriverHandler> m_driverHandlers; + + // Synchronization parameters + CCriticalSection m_handlerMutex; + CCriticalSection m_controllerInstallMutex; +}; +} // namespace PERIPHERALS diff --git a/xbmc/peripherals/devices/PeripheralKeyboard.cpp b/xbmc/peripherals/devices/PeripheralKeyboard.cpp new file mode 100644 index 0000000..271d8b1 --- /dev/null +++ b/xbmc/peripherals/devices/PeripheralKeyboard.cpp @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2017-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PeripheralKeyboard.h" + +#include "games/GameServices.h" +#include "games/controllers/Controller.h" +#include "games/controllers/ControllerManager.h" +#include "input/InputManager.h" +#include "peripherals/Peripherals.h" + +#include <mutex> +#include <sstream> + +using namespace KODI; +using namespace PERIPHERALS; + +CPeripheralKeyboard::CPeripheralKeyboard(CPeripherals& manager, + const PeripheralScanResult& scanResult, + CPeripheralBus* bus) + : CPeripheral(manager, scanResult, bus) +{ + // Initialize CPeripheral + m_features.push_back(FEATURE_KEYBOARD); +} + +CPeripheralKeyboard::~CPeripheralKeyboard(void) +{ + m_manager.GetInputManager().UnregisterKeyboardDriverHandler(this); +} + +bool CPeripheralKeyboard::InitialiseFeature(const PeripheralFeature feature) +{ + bool bSuccess = false; + + if (CPeripheral::InitialiseFeature(feature)) + { + switch (feature) + { + case FEATURE_KEYBOARD: + { + m_manager.GetInputManager().RegisterKeyboardDriverHandler(this); + break; + } + default: + break; + } + + bSuccess = true; + } + + return bSuccess; +} + +void CPeripheralKeyboard::RegisterKeyboardDriverHandler( + KODI::KEYBOARD::IKeyboardDriverHandler* handler, bool bPromiscuous) +{ + std::unique_lock<CCriticalSection> lock(m_mutex); + + KeyboardHandle handle{handler, bPromiscuous}; + m_keyboardHandlers.insert(m_keyboardHandlers.begin(), handle); +} + +void CPeripheralKeyboard::UnregisterKeyboardDriverHandler( + KODI::KEYBOARD::IKeyboardDriverHandler* handler) +{ + std::unique_lock<CCriticalSection> lock(m_mutex); + + auto it = + std::find_if(m_keyboardHandlers.begin(), m_keyboardHandlers.end(), + [handler](const KeyboardHandle& handle) { return handle.handler == handler; }); + + if (it != m_keyboardHandlers.end()) + m_keyboardHandlers.erase(it); +} + +GAME::ControllerPtr CPeripheralKeyboard::ControllerProfile() const +{ + if (m_controllerProfile) + return m_controllerProfile; + + return m_manager.GetControllerProfiles().GetDefaultKeyboard(); +} + +bool CPeripheralKeyboard::OnKeyPress(const CKey& key) +{ + m_lastActive = CDateTime::GetCurrentDateTime(); + + std::unique_lock<CCriticalSection> lock(m_mutex); + + bool bHandled = false; + + // Process promiscuous handlers + for (const KeyboardHandle& handle : m_keyboardHandlers) + { + if (handle.bPromiscuous) + handle.handler->OnKeyPress(key); + } + + // Process handlers until one is handled + for (const KeyboardHandle& handle : m_keyboardHandlers) + { + if (!handle.bPromiscuous) + { + bHandled = handle.handler->OnKeyPress(key); + if (bHandled) + break; + } + } + + return bHandled; +} + +void CPeripheralKeyboard::OnKeyRelease(const CKey& key) +{ + std::unique_lock<CCriticalSection> lock(m_mutex); + + for (const KeyboardHandle& handle : m_keyboardHandlers) + handle.handler->OnKeyRelease(key); +} diff --git a/xbmc/peripherals/devices/PeripheralKeyboard.h b/xbmc/peripherals/devices/PeripheralKeyboard.h new file mode 100644 index 0000000..2cf2934 --- /dev/null +++ b/xbmc/peripherals/devices/PeripheralKeyboard.h @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2017-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "Peripheral.h" +#include "XBDateTime.h" +#include "input/keyboard/interfaces/IKeyboardDriverHandler.h" +#include "threads/CriticalSection.h" + +#include <vector> + +namespace PERIPHERALS +{ +class CPeripheralKeyboard : public CPeripheral, public KODI::KEYBOARD::IKeyboardDriverHandler +{ +public: + CPeripheralKeyboard(CPeripherals& manager, + const PeripheralScanResult& scanResult, + CPeripheralBus* bus); + + ~CPeripheralKeyboard(void) override; + + // implementation of CPeripheral + bool InitialiseFeature(const PeripheralFeature feature) override; + void RegisterKeyboardDriverHandler(KODI::KEYBOARD::IKeyboardDriverHandler* handler, + bool bPromiscuous) override; + void UnregisterKeyboardDriverHandler(KODI::KEYBOARD::IKeyboardDriverHandler* handler) override; + CDateTime LastActive() override { return m_lastActive; } + KODI::GAME::ControllerPtr ControllerProfile() const override; + + // implementation of IKeyboardDriverHandler + bool OnKeyPress(const CKey& key) override; + void OnKeyRelease(const CKey& key) override; + +private: + struct KeyboardHandle + { + KODI::KEYBOARD::IKeyboardDriverHandler* handler; + bool bPromiscuous; + }; + + std::vector<KeyboardHandle> m_keyboardHandlers; + CCriticalSection m_mutex; + CDateTime m_lastActive; +}; +} // namespace PERIPHERALS diff --git a/xbmc/peripherals/devices/PeripheralMouse.cpp b/xbmc/peripherals/devices/PeripheralMouse.cpp new file mode 100644 index 0000000..4f26236 --- /dev/null +++ b/xbmc/peripherals/devices/PeripheralMouse.cpp @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2017-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PeripheralMouse.h" + +#include "games/GameServices.h" +#include "games/controllers/Controller.h" +#include "games/controllers/ControllerManager.h" +#include "input/InputManager.h" +#include "peripherals/Peripherals.h" + +#include <mutex> +#include <sstream> + +using namespace KODI; +using namespace PERIPHERALS; + +CPeripheralMouse::CPeripheralMouse(CPeripherals& manager, + const PeripheralScanResult& scanResult, + CPeripheralBus* bus) + : CPeripheral(manager, scanResult, bus) +{ + // Initialize CPeripheral + m_features.push_back(FEATURE_MOUSE); +} + +CPeripheralMouse::~CPeripheralMouse(void) +{ + m_manager.GetInputManager().UnregisterMouseDriverHandler(this); +} + +bool CPeripheralMouse::InitialiseFeature(const PeripheralFeature feature) +{ + bool bSuccess = false; + + if (CPeripheral::InitialiseFeature(feature)) + { + if (feature == FEATURE_MOUSE) + { + m_manager.GetInputManager().RegisterMouseDriverHandler(this); + } + + bSuccess = true; + } + + return bSuccess; +} + +void CPeripheralMouse::RegisterMouseDriverHandler(MOUSE::IMouseDriverHandler* handler, + bool bPromiscuous) +{ + using namespace KEYBOARD; + + std::unique_lock<CCriticalSection> lock(m_mutex); + + MouseHandle handle{handler, bPromiscuous}; + m_mouseHandlers.insert(m_mouseHandlers.begin(), handle); +} + +void CPeripheralMouse::UnregisterMouseDriverHandler(MOUSE::IMouseDriverHandler* handler) +{ + std::unique_lock<CCriticalSection> lock(m_mutex); + + auto it = + std::find_if(m_mouseHandlers.begin(), m_mouseHandlers.end(), + [handler](const MouseHandle& handle) { return handle.handler == handler; }); + + if (it != m_mouseHandlers.end()) + m_mouseHandlers.erase(it); +} + +GAME::ControllerPtr CPeripheralMouse::ControllerProfile() const +{ + if (m_controllerProfile) + return m_controllerProfile; + + return m_manager.GetControllerProfiles().GetDefaultMouse(); +} + +bool CPeripheralMouse::OnPosition(int x, int y) +{ + std::unique_lock<CCriticalSection> lock(m_mutex); + + bool bHandled = false; + + // Process promiscuous handlers + for (const MouseHandle& handle : m_mouseHandlers) + { + if (handle.bPromiscuous) + handle.handler->OnPosition(x, y); + } + + // Process handlers until one is handled + for (const MouseHandle& handle : m_mouseHandlers) + { + if (!handle.bPromiscuous) + { + bHandled = handle.handler->OnPosition(x, y); + if (bHandled) + break; + } + } + + if (bHandled) + m_lastActive = CDateTime::GetCurrentDateTime(); + + return bHandled; +} + +bool CPeripheralMouse::OnButtonPress(MOUSE::BUTTON_ID button) +{ + m_lastActive = CDateTime::GetCurrentDateTime(); + + std::unique_lock<CCriticalSection> lock(m_mutex); + + bool bHandled = false; + + // Process promiscuous handlers + for (const MouseHandle& handle : m_mouseHandlers) + { + if (handle.bPromiscuous) + handle.handler->OnButtonPress(button); + } + + // Process handlers until one is handled + for (const MouseHandle& handle : m_mouseHandlers) + { + if (!handle.bPromiscuous) + { + bHandled = handle.handler->OnButtonPress(button); + if (bHandled) + break; + } + } + + return bHandled; +} + +void CPeripheralMouse::OnButtonRelease(MOUSE::BUTTON_ID button) +{ + std::unique_lock<CCriticalSection> lock(m_mutex); + + for (const MouseHandle& handle : m_mouseHandlers) + handle.handler->OnButtonRelease(button); +} diff --git a/xbmc/peripherals/devices/PeripheralMouse.h b/xbmc/peripherals/devices/PeripheralMouse.h new file mode 100644 index 0000000..b46776d --- /dev/null +++ b/xbmc/peripherals/devices/PeripheralMouse.h @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2017-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "Peripheral.h" +#include "XBDateTime.h" +#include "input/mouse/interfaces/IMouseDriverHandler.h" +#include "threads/CriticalSection.h" + +#include <vector> + +namespace PERIPHERALS +{ +class CPeripheralMouse : public CPeripheral, public KODI::MOUSE::IMouseDriverHandler +{ +public: + CPeripheralMouse(CPeripherals& manager, + const PeripheralScanResult& scanResult, + CPeripheralBus* bus); + + ~CPeripheralMouse(void) override; + + // implementation of CPeripheral + bool InitialiseFeature(const PeripheralFeature feature) override; + void RegisterMouseDriverHandler(KODI::MOUSE::IMouseDriverHandler* handler, + bool bPromiscuous) override; + void UnregisterMouseDriverHandler(KODI::MOUSE::IMouseDriverHandler* handler) override; + CDateTime LastActive() override { return m_lastActive; } + KODI::GAME::ControllerPtr ControllerProfile() const override; + + // implementation of IMouseDriverHandler + bool OnPosition(int x, int y) override; + bool OnButtonPress(KODI::MOUSE::BUTTON_ID button) override; + void OnButtonRelease(KODI::MOUSE::BUTTON_ID button) override; + +private: + struct MouseHandle + { + KODI::MOUSE::IMouseDriverHandler* handler; + bool bPromiscuous; + }; + + std::vector<MouseHandle> m_mouseHandlers; + CCriticalSection m_mutex; + CDateTime m_lastActive; +}; +} // namespace PERIPHERALS diff --git a/xbmc/peripherals/devices/PeripheralNIC.cpp b/xbmc/peripherals/devices/PeripheralNIC.cpp new file mode 100644 index 0000000..a5b5c32 --- /dev/null +++ b/xbmc/peripherals/devices/PeripheralNIC.cpp @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PeripheralNIC.h" + +#include "guilib/LocalizeStrings.h" + +using namespace PERIPHERALS; + +CPeripheralNIC::CPeripheralNIC(CPeripherals& manager, + const PeripheralScanResult& scanResult, + CPeripheralBus* bus) + : CPeripheral(manager, scanResult, bus) +{ + m_strDeviceName = scanResult.m_strDeviceName.empty() ? g_localizeStrings.Get(35002) + : scanResult.m_strDeviceName; + m_features.push_back(FEATURE_NIC); +} diff --git a/xbmc/peripherals/devices/PeripheralNIC.h b/xbmc/peripherals/devices/PeripheralNIC.h new file mode 100644 index 0000000..01fe5f3 --- /dev/null +++ b/xbmc/peripherals/devices/PeripheralNIC.h @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "Peripheral.h" + +namespace PERIPHERALS +{ +class CPeripheralNIC : public CPeripheral +{ +public: + CPeripheralNIC(CPeripherals& manager, + const PeripheralScanResult& scanResult, + CPeripheralBus* bus); + ~CPeripheralNIC(void) override = default; +}; +} // namespace PERIPHERALS diff --git a/xbmc/peripherals/devices/PeripheralNyxboard.cpp b/xbmc/peripherals/devices/PeripheralNyxboard.cpp new file mode 100644 index 0000000..548d8cf --- /dev/null +++ b/xbmc/peripherals/devices/PeripheralNyxboard.cpp @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PeripheralNyxboard.h" + +#include "PeripheralHID.h" +#include "application/Application.h" +#include "utils/log.h" + +using namespace PERIPHERALS; + +CPeripheralNyxboard::CPeripheralNyxboard(CPeripherals& manager, + const PeripheralScanResult& scanResult, + CPeripheralBus* bus) + : CPeripheralHID(manager, scanResult, bus) +{ + m_features.push_back(FEATURE_NYXBOARD); +} + +bool CPeripheralNyxboard::LookupSymAndUnicode(XBMC_keysym& keysym, uint8_t* key, char* unicode) +{ + std::string strCommand; + if (keysym.sym == XBMCK_F7 && keysym.mod == XBMCKMOD_NONE && + GetSettingBool("enable_flip_commands")) + { + /* switched to keyboard side */ + CLog::Log(LOGDEBUG, "{} - switched to keyboard side", __FUNCTION__); + strCommand = GetSettingString("flip_keyboard"); + } + else if (keysym.sym == XBMCK_F7 && keysym.mod == XBMCKMOD_LCTRL && + GetSettingBool("enable_flip_commands")) + { + /* switched to remote side */ + CLog::Log(LOGDEBUG, "{} - switched to remote side", __FUNCTION__); + strCommand = GetSettingString("flip_remote"); + } + + if (!strCommand.empty()) + { + CLog::Log(LOGDEBUG, "{} - executing command '{}'", __FUNCTION__, strCommand); + if (g_application.ExecuteXBMCAction(strCommand)) + { + *key = 0; + *unicode = (char)0; + return true; + } + } + + return false; +} diff --git a/xbmc/peripherals/devices/PeripheralNyxboard.h b/xbmc/peripherals/devices/PeripheralNyxboard.h new file mode 100644 index 0000000..f81c3ef --- /dev/null +++ b/xbmc/peripherals/devices/PeripheralNyxboard.h @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "PeripheralHID.h" + +namespace PERIPHERALS +{ +class CPeripheralNyxboard : public CPeripheralHID +{ +public: + CPeripheralNyxboard(CPeripherals& manager, + const PeripheralScanResult& scanResult, + CPeripheralBus* bus); + ~CPeripheralNyxboard(void) override = default; + bool LookupSymAndUnicode(XBMC_keysym& keysym, uint8_t* key, char* unicode) override; +}; +} // namespace PERIPHERALS diff --git a/xbmc/peripherals/devices/PeripheralTuner.cpp b/xbmc/peripherals/devices/PeripheralTuner.cpp new file mode 100644 index 0000000..770656d --- /dev/null +++ b/xbmc/peripherals/devices/PeripheralTuner.cpp @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PeripheralTuner.h" + +using namespace PERIPHERALS; + +CPeripheralTuner::CPeripheralTuner(CPeripherals& manager, + const PeripheralScanResult& scanResult, + CPeripheralBus* bus) + : CPeripheral(manager, scanResult, bus) +{ + m_features.push_back(FEATURE_TUNER); +} diff --git a/xbmc/peripherals/devices/PeripheralTuner.h b/xbmc/peripherals/devices/PeripheralTuner.h new file mode 100644 index 0000000..aedfc12 --- /dev/null +++ b/xbmc/peripherals/devices/PeripheralTuner.h @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "Peripheral.h" + +namespace PERIPHERALS +{ +class CPeripheralTuner : public CPeripheral +{ +public: + CPeripheralTuner(CPeripherals& manager, + const PeripheralScanResult& scanResult, + CPeripheralBus* bus); + ~CPeripheralTuner(void) override = default; +}; +} // namespace PERIPHERALS |