From 410db34a79255f473b4da48736ab8d37014dc5c7 Mon Sep 17 00:00:00 2001 From: Lain Musgrove Date: Wed, 25 Jan 2023 03:07:49 -0800 Subject: [PATCH 1/2] Allow `mac_device` overrides in `/etc/joycond.conf`. This allows the use of third-party joycons like the Binbok which misidentify themselves as Switch Pro Controllers but otherwise work just fine. --- .gitmodules | 3 ++ CMakeLists.txt | 5 +- README.md | 7 +-- build_deb.sh | 1 + include/config.h | 23 +++++++++ include/phys_ctlr.h | 4 ++ joycond.conf.example | 2 + moonlight | 1 + src/CMakeLists.txt | 2 +- src/config.cpp | 67 ++++++++++++++++++++++++++ src/ctlr_detector_udev.cpp | 3 +- src/ctlr_mgr.cpp | 2 +- src/phys_ctlr.cpp | 99 ++++++++++++++++++++++++++------------ src/virt_ctlr_combined.cpp | 2 +- src/virt_ctlr_pro.cpp | 2 +- 15 files changed, 181 insertions(+), 42 deletions(-) create mode 100644 .gitmodules create mode 100644 include/config.h create mode 100644 joycond.conf.example create mode 160000 moonlight create mode 100644 src/config.cpp diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..ce5ea1c --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "moonlight"] + path = moonlight + url = git@github.com:/lainproliant/moonlight diff --git a/CMakeLists.txt b/CMakeLists.txt index f9d6e93..a71ab67 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.13) project(joycond) -set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD 20) # Generate compile_commands.json set(CMAKE_EXPORT_COMPILE_COMMANDS 1) @@ -14,6 +14,7 @@ add_executable(joycond "") target_compile_options(joycond PRIVATE -Wall -Werror) include_directories( include/ + moonlight/include/ ${LIBEVDEV_INCLUDE_DIRS} ${LIBUDEV_INCLUDE_DIRS} ) @@ -29,7 +30,7 @@ install(TARGETS joycond DESTINATION /usr/bin/ PERMISSIONS OWNER_WRITE OWNER_READ OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE ) install(FILES udev/89-joycond.rules udev/72-joycond.rules DESTINATION /lib/udev/rules.d/ - PERMISSIONS OWNER_WRITE OWNER_READ GROUP_READ WORLD_READ + PERMISSIONS OWNER_WRITE OWNER_READ GROUP_READ WORLD_READ ) install(FILES systemd/joycond.service DESTINATION /etc/systemd/system PERMISSIONS OWNER_WRITE OWNER_READ GROUP_READ WORLD_READ diff --git a/README.md b/README.md index d928afa..a812a84 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,10 @@ hid-nintendo is currently in review on the linux-input mailing list. The most re # Installation 1. clone the repo 2. Install requirements (`sudo apt install libevdev-dev` or `sudo dnf install libevdev-devel libudev-devel`) -3. `cmake .` -4. `sudo make install` -5. `sudo systemctl enable --now joycond` +3. `git submodule update --init --recursive` +4. `cmake .` +5. `sudo make install` +6. `sudo systemctl enable --now joycond` # Usage When a joy-con or pro controller is connected via bluetooth or USB, the player LEDs should start blinking periodically. This signals that the controller is in pairing mode. diff --git a/build_deb.sh b/build_deb.sh index 3a1d1a4..7f3780f 100755 --- a/build_deb.sh +++ b/build_deb.sh @@ -9,6 +9,7 @@ architecture="arm64" install_root="$(pwd)/build/deb/joycond" package_version="0.4-0" +git submodule update --init --recursive mkdir -p "$install_root" || exit cmake . || exit make DESTDIR="$install_root" install || exit diff --git a/include/config.h b/include/config.h new file mode 100644 index 0000000..dd3cd85 --- /dev/null +++ b/include/config.h @@ -0,0 +1,23 @@ +#ifndef JOYCOND_CONFIG_H +#define JOYCOND_CONFIG_H + +#include "phys_ctlr.h" +#include + +class Config { +public: + Config(const std::string& filename = "/etc/joycond.conf"); + ~Config() { } + + static const Config& get(); + + std::optional get_mac_device_override(const std::string& mac_addr) const; + +private: + void process_config_file(std::istream& infile); + void process_config_line(const std::string& line); + + std::map _mac_model_overrides; +}; + +#endif /* !JOYCOND_CONFIG_H */ diff --git a/include/phys_ctlr.h b/include/phys_ctlr.h index 60ef6cc..17083e1 100644 --- a/include/phys_ctlr.h +++ b/include/phys_ctlr.h @@ -5,6 +5,7 @@ #include #include #include +#include class phys_ctlr { @@ -12,6 +13,9 @@ class phys_ctlr enum class Model { Procon, Snescon, Left_Joycon, Right_Joycon, Unknown }; enum class PairingState { Pairing, Lone, Waiting, Horizontal, Virt_Procon }; + static const std::map& models_by_name(); + static const std::map& names_by_model(); + private: std::string devpath; std::string devname; diff --git a/joycond.conf.example b/joycond.conf.example new file mode 100644 index 0000000..c8d396e --- /dev/null +++ b/joycond.conf.example @@ -0,0 +1,2 @@ +mac_device 98:B6:E9:F8:6F:86 Left_Joycon +mac_device 98:B6:E9:33:EF:5C Right_Joycon diff --git a/moonlight b/moonlight new file mode 160000 index 0000000..c8f04db --- /dev/null +++ b/moonlight @@ -0,0 +1 @@ +Subproject commit c8f04db7e6b25f5c1c22e25e86ef32575044971c diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 780c315..a194ace 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -2,6 +2,7 @@ target_sources( joycond PRIVATE main.cpp + config.cpp phys_ctlr.cpp virt_ctlr.cpp virt_ctlr_passthrough.cpp @@ -12,4 +13,3 @@ target_sources( ctlr_detector_udev.cpp ctlr_mgr.cpp ) - diff --git a/src/config.cpp b/src/config.cpp new file mode 100644 index 0000000..ffdd2f0 --- /dev/null +++ b/src/config.cpp @@ -0,0 +1,67 @@ +#include "config.h" +#include "moonlight/file.h" +#include "moonlight/shlex.h" + +static const Config _config; + +// ctor +Config::Config(const std::string& filename) { + try { + auto infile = moonlight::file::open_r(filename); + process_config_file(infile); + + } catch (const moonlight::core::RuntimeError& e) { + std::cout << "Failed to open config file " << filename << ", skipping." << std::endl; + } +} + +// public static +const Config& Config::get() { + return _config; +} + +// public +std::optional Config::get_mac_device_override(const std::string& mac_addr) const { + auto iter = _mac_model_overrides.find(mac_addr); + if (iter == _mac_model_overrides.end()) { + return {}; + } + return iter->second; +} + +// private +void Config::process_config_file(std::istream& infile) { + std::string line; + while (std::getline(infile, line)) { + process_config_line(line); + } +} + +// private +void Config::process_config_line(const std::string& line) { + auto tokens = moonlight::shlex::split(line); + if (tokens.size() < 1) { + return; + } + + if (tokens[0] == "mac_device") { + if (tokens.size() != 3) { + std::cout << "Malformed `mac_device` directive in config file." << std::endl; + return; + } + + auto mac_address = tokens[1]; + auto device_name = tokens[2]; + + auto iter = phys_ctlr::models_by_name().find(device_name); + if (iter == phys_ctlr::models_by_name().end()) { + std::cout << "Invalid device name in `mac_device` directive: " << device_name << std::endl; + return; + } + + _mac_model_overrides[mac_address] = iter->second; + + } else { + std::cout << "Unknown directive in config file: " << tokens[0] << std::endl; + } +} diff --git a/src/ctlr_detector_udev.cpp b/src/ctlr_detector_udev.cpp index a79403a..04f529d 100644 --- a/src/ctlr_detector_udev.cpp +++ b/src/ctlr_detector_udev.cpp @@ -49,7 +49,7 @@ ctlr_detector_udev::ctlr_detector_udev(ctlr_mgr& ctlr_manager, epoll_mgr& epoll_ udev_mon_fd = udev_monitor_get_fd(mon); subscriber = std::make_shared(std::vector({udev_mon_fd}), - [=](int event_fd){epoll_event_callback(event_fd);}); + [=, this](int event_fd){epoll_event_callback(event_fd);}); epoll_manager.add_subscriber(subscriber); // Detect any existing controllers prior to daemon start @@ -83,4 +83,3 @@ ctlr_detector_udev::~ctlr_detector_udev() { epoll_manager.remove_subscriber(subscriber); } - diff --git a/src/ctlr_mgr.cpp b/src/ctlr_mgr.cpp index 2f6fbc4..b30a671 100644 --- a/src/ctlr_mgr.cpp +++ b/src/ctlr_mgr.cpp @@ -168,7 +168,7 @@ void ctlr_mgr::add_ctlr(const std::string& devpath, const std::string& devname) unpaired_controllers[devpath] = phys; phys->blink_player_leds(); subscribers[devpath] = std::make_shared(std::vector({phys->get_fd()}), - [=](int event_fd){epoll_event_callback(event_fd);}); + [=, this](int event_fd){epoll_event_callback(event_fd);}); epoll_manager.add_subscriber(subscribers[devpath]); } else { std::cerr << "Attempting to add existing phys_ctlr to controller manager\n"; diff --git a/src/phys_ctlr.cpp b/src/phys_ctlr.cpp index 1077119..bea8e43 100644 --- a/src/phys_ctlr.cpp +++ b/src/phys_ctlr.cpp @@ -1,4 +1,6 @@ #include "phys_ctlr.h" +#include "config.h" +#include "moonlight/maps.h" #include #include @@ -9,7 +11,32 @@ #include #include -//private +// public static +const std::map& phys_ctlr::models_by_name() { + static const std::map mapping = { + {"Procon", Model::Procon}, + {"Snescon", Model::Snescon}, + {"Left_Joycon", Model::Left_Joycon}, + {"Right_Joycon", Model::Right_Joycon}, + {"Unknown", Model::Unknown}, + }; + return mapping; +} + +// public static +const std::map& phys_ctlr::names_by_model() { + static const std::map mapping = []() { + std::map mapping; + for (auto iter = models_by_name().begin(); iter != models_by_name().end(); iter++) { + mapping.insert({iter->second, iter->first}); + } + return mapping; + }(); + + return mapping; +} + +// private std::optional phys_ctlr::get_first_glob_path(std::string const &pattern) { glob_t globbuf; @@ -224,8 +251,19 @@ phys_ctlr::phys_ctlr(std::string const &devpath, std::string const &devname) : is_serial(false) { + const Config& config = Config::get(); zero_triggers(); + // Attempt to read MAC address from uniq attribute + mac_addr = ""; +#if defined(ANDROID) || defined(__ANDROID__) + std::ifstream funiq("/sys/" + devpath + "/uniq"); +#else + std::ifstream funiq("/sys/" + devpath + "/../uniq"); +#endif + std::getline(funiq, mac_addr); + std::cout << "MAC: " << mac_addr << std::endl; + int fd = open(devname.c_str(), O_RDWR | O_NONBLOCK); if (fd < 0) { std::cerr << "Failed to open " << devname << " ; errno=" << errno << std::endl; @@ -246,27 +284,35 @@ phys_ctlr::phys_ctlr(std::string const &devpath, std::string const &devname) : product_id = 0x2007; } - switch (product_id) { - case 0x2009: - model = Model::Procon; - std::cout << "Found Pro Controller\n"; - break; - case 0x2006: - model = Model::Left_Joycon; - std::cout << "Found Left Joy-Con\n"; - break; - case 0x2007: - model = Model::Right_Joycon; - std::cout << "Found Right Joy-Con\n"; - break; - case 0x2017: - model = Model::Snescon; - std::cout << "Found SNES Controller\n"; - break; - default: - model = Model::Unknown; - std::cerr << "Unknown product id = " << std::hex << libevdev_get_id_product(evdev) << std::endl; - break; + auto device_override = config.get_mac_device_override(mac_addr); + + if (device_override) { + model = *device_override; + std::cout << "Device type override for mac_addr " << mac_addr << " activated: " << phys_ctlr::names_by_model().at(model) << std::endl; + + } else { + switch (product_id) { + case 0x2009: + model = Model::Procon; + std::cout << "Found Pro Controller\n"; + break; + case 0x2006: + model = Model::Left_Joycon; + std::cout << "Found Left Joy-Con\n"; + break; + case 0x2007: + model = Model::Right_Joycon; + std::cout << "Found Right Joy-Con\n"; + break; + case 0x2017: + model = Model::Snescon; + std::cout << "Found SNES Controller\n"; + break; + default: + model = Model::Unknown; + std::cerr << "Unknown product id = " << std::hex << libevdev_get_id_product(evdev) << std::endl; + break; + } } init_leds(); @@ -292,15 +338,6 @@ phys_ctlr::phys_ctlr(std::string const &devpath, std::string const &devname) : is_serial = true; } - // Attempt to read MAC address from uniq attribute - mac_addr = ""; -#if defined(ANDROID) || defined(__ANDROID__) - std::ifstream funiq("/sys/" + devpath + "/uniq"); -#else - std::ifstream funiq("/sys/" + devpath + "/../uniq"); -#endif - std::getline(funiq, mac_addr); - std::cout << "MAC: " << mac_addr << std::endl; } phys_ctlr::~phys_ctlr() diff --git a/src/virt_ctlr_combined.cpp b/src/virt_ctlr_combined.cpp index ce5ea9a..371ac34 100644 --- a/src/virt_ctlr_combined.cpp +++ b/src/virt_ctlr_combined.cpp @@ -341,7 +341,7 @@ virt_ctlr_combined::virt_ctlr_combined(std::shared_ptr physl, std::sh fcntl(get_uinput_fd(), F_SETFL, flags | O_NONBLOCK); subscriber = std::make_shared(std::vector({get_uinput_fd()}), - [=](int event_fd){handle_events(event_fd);}); + [=, this](int event_fd){handle_events(event_fd);}); epoll_manager.add_subscriber(subscriber); } diff --git a/src/virt_ctlr_pro.cpp b/src/virt_ctlr_pro.cpp index 8000020..ec145cf 100644 --- a/src/virt_ctlr_pro.cpp +++ b/src/virt_ctlr_pro.cpp @@ -261,7 +261,7 @@ virt_ctlr_pro::virt_ctlr_pro(std::shared_ptr phys, epoll_mgr& epoll_m fcntl(get_uinput_fd(), F_SETFL, flags | O_NONBLOCK); subscriber = std::make_shared(std::vector({get_uinput_fd()}), - [=](int event_fd){handle_events(event_fd);}); + [=, this](int event_fd){handle_events(event_fd);}); epoll_manager.add_subscriber(subscriber); } From 4382244353130481571afe8d0055800aeae255c7 Mon Sep 17 00:00:00 2001 From: Lain Musgrove Date: Fri, 27 Jan 2023 13:21:53 -0800 Subject: [PATCH 2/2] Add `xbox_orientation` flag to allow combined joycons to swap a/b x/y. --- include/config.h | 10 ++++++ include/virt_ctlr_combined.h | 2 ++ joycond.conf.example | 1 + src/config.cpp | 64 ++++++++++++++++++++++++++++++++++++ src/virt_ctlr_combined.cpp | 33 ++++++++++++++++++- 5 files changed, 109 insertions(+), 1 deletion(-) diff --git a/include/config.h b/include/config.h index dd3cd85..fe26b7c 100644 --- a/include/config.h +++ b/include/config.h @@ -4,6 +4,12 @@ #include "phys_ctlr.h" #include +typedef std::pair CombinedMACs; + +struct ControllerProps { + bool xbox_orientation = false; +}; + class Config { public: Config(const std::string& filename = "/etc/joycond.conf"); @@ -12,12 +18,16 @@ class Config { static const Config& get(); std::optional get_mac_device_override(const std::string& mac_addr) const; + const ControllerProps& get_combined_joycons_props(const CombinedMACs& macs) const; + void set_combined_joycons_props(const CombinedMACs& macs, const ControllerProps& props); private: void process_config_file(std::istream& infile); void process_config_line(const std::string& line); + void parse_and_set_controller_prop(ControllerProps& props, const std::string& name, const std::string& value) const; std::map _mac_model_overrides; + std::map _combined_joycons_props; }; #endif /* !JOYCOND_CONFIG_H */ diff --git a/include/virt_ctlr_combined.h b/include/virt_ctlr_combined.h index f8c85a3..39130c7 100644 --- a/include/virt_ctlr_combined.h +++ b/include/virt_ctlr_combined.h @@ -4,6 +4,7 @@ #include "virt_ctlr.h" #include "phys_ctlr.h" #include "epoll_mgr.h" +#include "config.h" #include #include @@ -22,6 +23,7 @@ class virt_ctlr_combined : public virt_ctlr std::map> rumble_effects; std::string left_mac; std::string right_mac; + ControllerProps props; void relay_events(std::shared_ptr phys); void handle_uinput_event(); diff --git a/joycond.conf.example b/joycond.conf.example index c8d396e..d6276cd 100644 --- a/joycond.conf.example +++ b/joycond.conf.example @@ -1,2 +1,3 @@ mac_device 98:B6:E9:F8:6F:86 Left_Joycon mac_device 98:B6:E9:33:EF:5C Right_Joycon +prop combined_joycons 98:B6:E9:F8:6F:86 98:B6:E9:33:EF:5C xbox_orientation 1 diff --git a/src/config.cpp b/src/config.cpp index ffdd2f0..84b38af 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -3,6 +3,7 @@ #include "moonlight/shlex.h" static const Config _config; +static const ControllerProps _default_props; // ctor Config::Config(const std::string& filename) { @@ -29,6 +30,19 @@ std::optional Config::get_mac_device_override(const std::strin return iter->second; } +const ControllerProps& Config::get_combined_joycons_props(const CombinedMACs& macs) const { + auto iter = _combined_joycons_props.find(macs); + if (iter == _combined_joycons_props.end()) { + return _default_props; + } + + return iter->second; +} + +void Config::set_combined_joycons_props(const CombinedMACs& macs, const ControllerProps& props) { + _combined_joycons_props.insert({macs, props}); +} + // private void Config::process_config_file(std::istream& infile) { std::string line; @@ -44,6 +58,10 @@ void Config::process_config_line(const std::string& line) { return; } + if (tokens[0].starts_with("#")) { + return; + } + if (tokens[0] == "mac_device") { if (tokens.size() != 3) { std::cout << "Malformed `mac_device` directive in config file." << std::endl; @@ -61,7 +79,53 @@ void Config::process_config_line(const std::string& line) { _mac_model_overrides[mac_address] = iter->second; + + } else if (tokens[0] == "prop") { + if (tokens.size() < 5) { + std::cout << "Malformed `prop` directive in config file." << std::endl; + return; + } + + auto controller_type = tokens[1]; + + if (controller_type == "combined_joycons") { + if (tokens.size() < 6) { + std::cout << "Malformed `prop` directive for `combined_joycons` controller in config file." << std::endl; + return; + } + + auto left_mac = tokens[2]; + auto right_mac = tokens[3]; + auto name = tokens[4]; + auto value = tokens[5]; + + std::cout << "Setting controller prop " << name << " to " << value << " for combined joycons " << left_mac << " and " << right_mac << std::endl; + + auto props = get_combined_joycons_props({left_mac, right_mac}); + parse_and_set_controller_prop(props, name, value); + set_combined_joycons_props({left_mac, right_mac}, props); + + } else { + std::cout << "Unsupported controller type for `prop` directive: " << controller_type << std::endl; + return; + } + } else { std::cout << "Unknown directive in config file: " << tokens[0] << std::endl; } } + +// private +void Config::parse_and_set_controller_prop(ControllerProps& props, const std::string& name, const std::string& value) const { + if (name == "xbox_orientation") { + try { + props.xbox_orientation = std::stoi(value); + + } catch (...) { + std::cout << "Failed to parse value for `xbox_orientation`." << std::endl; + return; + } + } else { + std::cout << "Unsupported property: " << name << std::endl; + } +} diff --git a/src/virt_ctlr_combined.cpp b/src/virt_ctlr_combined.cpp index 371ac34..f063b1c 100644 --- a/src/virt_ctlr_combined.cpp +++ b/src/virt_ctlr_combined.cpp @@ -1,4 +1,5 @@ #include "virt_ctlr_combined.h" +#include "config.h" #include #include @@ -76,7 +77,34 @@ void virt_ctlr_combined::relay_events(std::shared_ptr phys) } } #endif - libevdev_uinput_write_event(uidev, ev.type, ev.code, ev.value); + + // Swap X/Y and A/B if xbox_orientation is enabled for this controller. + if (props.xbox_orientation) { + switch (ev.code) { + case BTN_X: + libevdev_uinput_write_event(uidev, ev.type, BTN_Y, ev.value); + break; + + case BTN_Y: + libevdev_uinput_write_event(uidev, ev.type, BTN_X, ev.value); + break; + + case BTN_A: + libevdev_uinput_write_event(uidev, ev.type, BTN_B, ev.value); + break; + + case BTN_B: + libevdev_uinput_write_event(uidev, ev.type, BTN_A, ev.value); + break; + + default: + libevdev_uinput_write_event(uidev, ev.type, ev.code, ev.value); + break; + } + + } else { + libevdev_uinput_write_event(uidev, ev.type, ev.code, ev.value); + } } ret = libevdev_next_event(evdev, LIBEVDEV_READ_FLAG_NORMAL, &ev); } @@ -235,6 +263,9 @@ virt_ctlr_combined::virt_ctlr_combined(std::shared_ptr physl, std::sh { int ret; + const Config& config = Config::get(); + props = config.get_combined_joycons_props({left_mac, right_mac}); + uifd = open("/dev/uinput", O_RDWR); if (uifd < 0) { std::cerr << "Failed to open uinput; errno=" << errno << std::endl;