/* Copyright 2024 flyinghead This file is part of Flycast. Flycast is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. Flycast is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Flycast. If not, see . */ #include "dreampicoport.h" #ifdef USE_DREAMCASTCONTROLLER #include "hw/maple/maple_devs.h" #include "ui/gui.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if defined(__linux__) || (defined(__APPLE__) && defined(TARGET_OS_MAC)) #include #endif #if defined(_WIN32) #include #include #endif class DreamPicoPortSerialHandler { //! Asynchronous context for serial_handler asio::io_context io_context; //! Output buffer data for serial_handler std::string serial_out_data; //! Handles communication to DreamPicoPort asio::serial_port serial_handler{io_context}; //! Set to true while an async write is in progress with serial_handler bool serial_write_in_progress = false; //! Set to true while an async read is in progress with serial_handler std::atomic serial_read_in_progress = false; //! Signaled when serial_write_in_progress transitions to false std::condition_variable write_cv; //! Mutex for write_cv and serializes access to serial_write_in_progress std::mutex write_cv_mutex; //! Input stream buffer from serial_handler char serial_read_buffer[1024]; //! Holds on to partially parsed line std::string read_line_buffer; //! Thread which runs the io_context std::unique_ptr io_context_thread; //! Contains queue of incoming lines from serial std::list read_queue; //! Signaled when data is in read_queue std::condition_variable read_cv; //! Mutex for read_cv and serializes access to read_queue std::mutex read_cv_mutex; //! When >= 0, parsing binary input and signifies total number parsed in this set //! When < 0, not parsing binary input int32_t num_binary_parsed = -1; //! Number of binary bytes left to parse uint16_t stored_binary_size = 0; //! Number of binary bytes left to parse in current set uint16_t num_binary_left = 0; //! Serializes send calls, making them thread-safe std::mutex send_mutex; public: DreamPicoPortSerialHandler() { // the serial port isn't ready at this point, so we need to sleep briefly // we probably should have a better way to handle this std::this_thread::sleep_for(std::chrono::milliseconds(500)); serial_handler = asio::serial_port(io_context); io_context.reset(); std::string serial_device = ""; // use user-configured serial device if available, fallback to first available serial_device = cfgLoadStr("input", "DreamPicoPortSerialDevice", ""); if (!serial_device.empty()) { NOTICE_LOG(INPUT, "DreamPicoPort connecting to user-configured serial device: %s", serial_device.c_str()); } else { serial_device = getFirstSerialDevice(); NOTICE_LOG(INPUT, "DreamPicoPort connecting to autoselected serial device: %s", serial_device.c_str()); } asio::error_code ec; serial_handler.open(serial_device, ec); if (ec || !serial_handler.is_open()) { WARN_LOG(INPUT, "DreamPicoPort serial connection failed: %s", ec.message().c_str()); disconnect(); } else { NOTICE_LOG(INPUT, "DreamPicoPort serial connection successful!"); } // This must be done before the io_context is run because it will keep io_context from returning immediately startSerialRead(); io_context_thread = std::make_unique([this](){contextThreadEnty();}); } ~DreamPicoPortSerialHandler() { disconnect(); io_context_thread->join(); } bool is_open() const { return serial_handler.is_open(); } asio::error_code sendCmd( const std::string& cmd, std::string& response, std::chrono::milliseconds timeout_ms ) { const std::chrono::steady_clock::time_point expiration = std::chrono::steady_clock::now() + timeout_ms; std::lock_guard lock(send_mutex); // Ensure thread safety for send operations asio::error_code ec = transmit(cmd, true, expiration); if (!ec) { ec = receive(response, expiration); } return ec; } asio::error_code sendCmd( const std::string& cmd, std::chrono::milliseconds timeout_ms ) { const std::chrono::steady_clock::time_point expiration = std::chrono::steady_clock::now() + timeout_ms; std::lock_guard lock(send_mutex); // Ensure thread safety for send operations return transmit(cmd, false, expiration); } asio::error_code sendMsg( const MapleMsg& msg, int hardware_bus, MapleMsg& response, std::chrono::milliseconds timeout_ms) { const std::chrono::steady_clock::time_point expiration = std::chrono::steady_clock::now() + timeout_ms; std::lock_guard lock(send_mutex); // Ensure thread safety for send operations std::string cmd = msgToStr(msg, hardware_bus); asio::error_code ec = transmit(cmd, true, expiration); if (!ec) { ec = receive(response, expiration); } return ec; } asio::error_code sendMsg( const MapleMsg& msg, int hardware_bus, std::chrono::milliseconds timeout_ms) { const std::chrono::steady_clock::time_point expiration = std::chrono::steady_clock::now() + timeout_ms; std::lock_guard lock(send_mutex); // Ensure thread safety for send operations std::string cmd = msgToStr(msg, hardware_bus); return transmit(cmd, false, expiration); } private: void disconnect() { io_context.stop(); if (serial_handler.is_open()) { try { serial_handler.cancel(); } catch(const asio::system_error&) { // Ignore cancel errors } } try { serial_handler.close(); } catch(const asio::system_error&) { // Ignore closing errors } } void contextThreadEnty() { // This context should never exit until disconnect due to read handler automatically rearming io_context.run(); } static std::string getFirstSerialDevice() { // On Windows, we get the first serial device matching our VID/PID #if defined(_WIN32) HDEVINFO deviceInfoSet = SetupDiGetClassDevs(NULL, "USB", NULL, DIGCF_PRESENT | DIGCF_ALLCLASSES); if (deviceInfoSet == INVALID_HANDLE_VALUE) { return ""; } SP_DEVINFO_DATA deviceInfoData; deviceInfoData.cbSize = sizeof(SP_DEVINFO_DATA); for (DWORD i = 0; SetupDiEnumDeviceInfo(deviceInfoSet, i, &deviceInfoData); ++i) { DWORD dataType, bufferSize = 0; SetupDiGetDeviceRegistryProperty(deviceInfoSet, &deviceInfoData, SPDRP_HARDWAREID, &dataType, NULL, 0, &bufferSize); if (bufferSize > 0) { std::vector buffer(bufferSize); if (SetupDiGetDeviceRegistryProperty(deviceInfoSet, &deviceInfoData, SPDRP_HARDWAREID, &dataType, (PBYTE)buffer.data(), bufferSize, NULL)) { std::string hardwareId(buffer.begin(), buffer.end()); if (hardwareId.find("VID_1209") != std::string::npos && hardwareId.find("PID_2F07") != std::string::npos) { HKEY deviceKey = SetupDiOpenDevRegKey(deviceInfoSet, &deviceInfoData, DICS_FLAG_GLOBAL, 0, DIREG_DEV, KEY_READ); if (deviceKey != INVALID_HANDLE_VALUE) { char portName[256]; DWORD portNameSize = sizeof(portName); if (RegQueryValueEx(deviceKey, "PortName", NULL, NULL, (LPBYTE)portName, &portNameSize) == ERROR_SUCCESS) { RegCloseKey(deviceKey); SetupDiDestroyDeviceInfoList(deviceInfoSet); return std::string(portName); } RegCloseKey(deviceKey); } } } } } SetupDiDestroyDeviceInfoList(deviceInfoSet); return ""; #endif #if defined(__linux__) || (defined(__APPLE__) && defined(TARGET_OS_MAC)) // On MacOS/Linux, we get the first serial device matching the device prefix std::string device_prefix = ""; #if defined(__linux__) device_prefix = "ttyACM"; #elif (defined(__APPLE__) && defined(TARGET_OS_MAC)) device_prefix = "tty.usbmodem"; #endif std::string path = "/dev/"; DIR *dir; struct dirent *ent; if ((dir = opendir(path.c_str())) != NULL) { while ((ent = readdir(dir)) != NULL) { std::string device = ent->d_name; if (device.find(device_prefix) != std::string::npos) { closedir(dir); return path + device; } } closedir(dir); } return ""; #endif } asio::error_code transmit( const std::string& cmd, bool receive_expected, const std::chrono::steady_clock::time_point& expiration ) { asio::error_code ec; if (!serial_handler.is_open()) { return asio::error::not_connected; } if (receive_expected && serial_read_in_progress) { // Wait up to 30 ms for read to complete before writing to help ensure expected command order. // Continue regardless of result. std::string rx; std::chrono::steady_clock::time_point rxExpiration = std::chrono::steady_clock::now() + std::chrono::milliseconds(30); if (rxExpiration > expiration) { rxExpiration = expiration; } (void)receive(rx, rxExpiration); } else { // Just clear out the read queue before continuing std::unique_lock lock(read_cv_mutex); read_queue.clear(); } // Wait for last write to complete std::unique_lock lock(write_cv_mutex); if (!write_cv.wait_until(lock, expiration, [this](){return (!serial_write_in_progress || !serial_handler.is_open());})) { return asio::error::timed_out; } // Check again before continuing if (!serial_handler.is_open()) { return asio::error::not_connected; } serial_out_data = cmd; // Clear out the read buffer before writing next command serial_write_in_progress = true; serial_read_in_progress = true; asio::async_write( serial_handler, asio::buffer(serial_out_data), asio::transfer_exactly(serial_out_data.size()), [this](const asio::error_code& error, size_t bytes_transferred) { std::unique_lock lock(write_cv_mutex); if (error) { try { serial_handler.cancel(); } catch(const asio::system_error&) { // Ignore cancel errors } } serial_write_in_progress = false; write_cv.notify_all(); } ); return ec; } asio::error_code receive(std::string& cmd, const std::chrono::steady_clock::time_point& expiration) { asio::error_code ec; // Wait for at least 2 lines to be received (first line is echo back) std::unique_lock lock(read_cv_mutex); if (!read_cv.wait_until(lock, expiration, [this](){return ((read_queue.size() >= 2) || !serial_handler.is_open());})) { // Timeout return asio::error::timed_out; } if (read_queue.size() < 2) { // Connection was closed before data could be received return asio::error::connection_aborted; } // discard the first message as we are interested in the second only which returns the controller configuration cmd = std::move(read_queue.back()); read_queue.clear(); serial_read_in_progress = false; return ec; } asio::error_code receive(MapleMsg& msg, const std::chrono::steady_clock::time_point& expiration) { asio::error_code ec; std::string response; ec = receive(response, expiration); if (ec) { return ec; } std::vector words; bool valid = false; const char* iter = response.c_str(); const char* eol = iter + response.size(); if (*iter == '*') { // Asterisk indicates the write or read operation failed return asio::error::no_data; } else if (*iter == '\5') // binary parsing { // binary ++iter; while (iter < eol) { uint32_t word = 0; uint32_t i = 0; while (i < 4 && iter < eol) { const u8* pu8 = reinterpret_cast(iter++); // Apply value into current word word |= (*pu8 << ((4 - i) * 8 - 8)); ++i; } // Invalid if a partial word was given valid = ((i == 4) || (i == 0)); if (i == 4) { words.push_back(word); } } } else { while (iter < eol) { uint32_t word = 0; uint32_t i = 0; while (i < 8 && iter < eol) { char v = *iter++; uint_fast8_t value = 0; if (v >= '0' && v <= '9') { value = v - '0'; } else if (v >= 'a' && v <= 'f') { value = v - 'a' + 0xa; } else if (v >= 'A' && v <= 'F') { value = v - 'A' + 0xA; } else { // Ignore this character continue; } // Apply value into current word word |= (value << ((8 - i) * 4 - 4)); ++i; } // Invalid if a partial word was given valid = ((i == 8) || (i == 0)); if (i == 8) { words.push_back(word); } } } if (words.size() > 0) { msg.command = (words[0] >> 24) & 0xFF; msg.destAP = (words[0] >> 16) & 0xFF; msg.originAP = (words[0] >> 8) & 0xFF; msg.size = words[0] & 0xFF; for (uint32_t i = 1; i < words.size(); ++i) { uint32_t dat = ntohl(words[i]); memcpy(&msg.data[(i-1)*4], &dat, sizeof(dat)); } } else { return asio::error::message_size; } if (!serial_handler.is_open()) { return asio::error::not_connected; } return ec; } std::string msgToStr(const MapleMsg& msg, int hardware_bus) { // Build serial_out_data string // Need to message the hardware bus instead of the software bus u8 hwDestAP = (hardware_bus << 6) | (msg.destAP & 0x3F); u8 hwOriginAP = (hardware_bus << 6) | (msg.originAP & 0x3F); std::ostringstream s; s << "X "; // 'X' prefix triggers flycast command parser s.fill('0'); s << std::hex << std::uppercase << std::setw(2) << (u32)msg.command << std::setw(2) << (u32)hwDestAP // override dest << std::setw(2) << (u32)hwOriginAP // override origin << std::setw(2) << (u32)msg.size; const u32 sz = msg.getDataSize(); for (u32 i = 0; i < sz; i++) { s << std::setw(2) << (u32)msg.data[i]; } s << "\n"; return s.str(); } void startSerialRead() { serialReadHandler(); // Just to make sure initial data is cleared off of incoming buffer io_context.poll_one(); read_queue.clear(); } void serialReadHandler() { // Arm or rearm the read serial_handler.async_read_some( asio::buffer(serial_read_buffer, sizeof(serial_read_buffer)), [this](const asio::error_code& error, std::size_t size) -> void { std::lock_guard lock(read_cv_mutex); if (error) { try { serial_handler.cancel(); } catch(const asio::system_error&) { // Ignore cancel errors } read_cv.notify_all(); } else { if (size > 0) { // Consume the received data if (consumeReadBuffer(size) > 0) { // New lines available read_cv.notify_all(); } } // Auto reload read - io_context will always have work to do serialReadHandler(); } } ); } int consumeReadBuffer(std::size_t size) { if (size <= 0) { return 0; } int numberOfLines = 0; const char* iter = serial_read_buffer; while (size-- > 0) { char c = *iter++; if (num_binary_parsed >= 0) { ++num_binary_parsed; --num_binary_left; if (num_binary_parsed == 1) { stored_binary_size = (c << 8); } else if (num_binary_parsed == 2) { stored_binary_size |= c; num_binary_left = stored_binary_size; read_line_buffer.reserve(1 + stored_binary_size); } else { read_line_buffer += c; } if (num_binary_left == 0) { num_binary_parsed = -1; } } else if (c == '\5') // binary start character { read_line_buffer += c; num_binary_parsed = 0; stored_binary_size = 0; num_binary_left = 2; // Parse size } else if (c == '\n') { // Remove carriage return if found and add this line to queue if (read_line_buffer.size() > 0 && read_line_buffer[read_line_buffer.size() - 1] == '\r') { read_line_buffer.pop_back(); } read_queue.push_back(read_line_buffer); read_line_buffer.clear(); ++numberOfLines; } else { read_line_buffer += c; } } return numberOfLines; } }; // Define the static instances here std::unique_ptr DreamPicoPort::serial; std::atomic DreamPicoPort::connected_dev_count = 0; DreamPicoPort::DreamPicoPort(int bus, int joystick_idx, SDL_Joystick* sdl_joystick) : software_bus(bus) { #if defined(_WIN32) // Workaround: Getting the instance ID here fixes some sort of L/R trigger bug in Windows dinput for some reason (void)SDL_JoystickGetDeviceInstanceID(joystick_idx); #endif determineHardwareBus(joystick_idx, sdl_joystick); unique_id.clear(); if (!is_hardware_bus_implied && !serial_number.empty()) { // Locking to name, which includes A-D, plus serial number will ensure correct enumeration every time unique_id = std::string("sdl_") + getName("") + std::string("_") + serial_number; } } DreamPicoPort::~DreamPicoPort() { disconnect(); } bool DreamPicoPort::send(const MapleMsg& msg) { if (serial) { asio::error_code ec = serial->sendMsg(msg, hardware_bus, timeout_ms); return !ec; } return false; } bool DreamPicoPort::send(const MapleMsg& txMsg, MapleMsg& rxMsg) { if (serial) { asio::error_code ec = serial->sendMsg(txMsg, hardware_bus, rxMsg, timeout_ms); return !ec; } return false; } inline void DreamPicoPort::gameTermination() { // Need a short delay to wait for last screen draw to complete std::this_thread::sleep_for(std::chrono::milliseconds(10)); // Reset screen to selected port sendPort(); } int DreamPicoPort::getBus() const { return software_bus; } u32 DreamPicoPort::getFunctionCode(int forPort) const { u32 mask = 0; if (peripherals.size() > forPort) { for (const auto& peripheral : peripherals[forPort]) { mask |= peripheral[0]; } } // swap bytes to get the correct function code return SWAP32(mask); } std::array DreamPicoPort::getFunctionDefinitions(int forPort) const { std::array arr{0, 0, 0}; if (peripherals.size() > forPort) { std::size_t idx = 0; for (const auto& peripheral : peripherals[forPort]) { arr[idx++] = SWAP32(peripheral[1]); if (idx >= 3) break; } } return arr; } int DreamPicoPort::getDefaultBus() const { if (!is_hardware_bus_implied && !is_single_device) { return hardware_bus; } else { // Value of -1 means to use enumeration order return -1; } } void DreamPicoPort::setDefaultMapping(const std::shared_ptr& mapping) const { // Since this is a real DC controller, no deadzone adjustment is needed mapping->dead_zone = 0.0f; // Map the things not set by SDL mapping->set_button(DC_BTN_C, 2); mapping->set_button(DC_BTN_Z, 5); mapping->set_button(DC_BTN_D, 10); mapping->set_button(DC_DPAD2_UP, 9); mapping->set_button(DC_DPAD2_DOWN, 8); mapping->set_button(DC_DPAD2_LEFT, 7); mapping->set_button(DC_DPAD2_RIGHT, 6); } const char *DreamPicoPort::getButtonName(u32 code) const { switch (code) { // Coincides with buttons setup in setDefaultMapping case 2: return "C"; case 5: return "Z"; case 10: return "D"; case 9: return "DPad2 Up"; case 8: return "DPad2 Down"; case 7: return "DPad2 Left"; case 6: return "DPad2 Right"; // These buttons are normally not physically accessible, but are mapped on DreamPicoPort case 12: return "VMU1 A"; case 15: return "VMU1 B"; case 16: return "VMU1 Up"; case 17: return "VMU1 Down"; case 18: return "VMU1 Left"; case 19: return "VMU1 Right"; default: return nullptr; // no override } } std::string DreamPicoPort::getUniqueId() const { return unique_id; } void DreamPicoPort::changeBus(int newBus) { software_bus = newBus; } std::string DreamPicoPort::getName() const { return getName(" "); } std::string DreamPicoPort::getName(std::string separator) const { std::string name = "DreamPicoPort"; if (!is_hardware_bus_implied && !is_single_device) { const char portChar = ('A' + hardware_bus); name += separator + std::string(1, portChar); } return name; } void DreamPicoPort::connect() { // Timeout is 1 second while establishing connection timeout_ms = std::chrono::seconds(1); if (connection_established && serial) { if (serial->is_open()) { sendPort(); } else { disconnect(); return; } } ++connected_dev_count; connection_established = true; if (!serial) { serial = std::make_unique(); } if (serial && serial->is_open()) { sendPort(); } else { disconnect(); return; } if (!queryInterfaceVersion()) { disconnect(); return; } if (!queryPeripherals()) { disconnect(); return; } // Timeout is extended to 5 seconds for all other communication after connection timeout_ms = std::chrono::seconds(5); int vmuCount = 0; int vibrationCount = 0; u32 portOneFn = getFunctionCode(1); if (portOneFn & MFID_1_Storage) { config::MapleExpansionDevices[software_bus][0] = MDT_SegaVMU; ++vmuCount; } else { config::MapleExpansionDevices[software_bus][0] = MDT_None; } u32 portTwoFn = getFunctionCode(2); if (portTwoFn & MFID_8_Vibration) { config::MapleExpansionDevices[software_bus][1] = MDT_PurupuruPack; ++vibrationCount; } else if (portTwoFn & MFID_1_Storage) { config::MapleExpansionDevices[software_bus][1] = MDT_SegaVMU; ++vmuCount; } else { config::MapleExpansionDevices[software_bus][1] = MDT_None; } NOTICE_LOG(INPUT, "Connected to DreamcastController[%d]: Type:%s, VMU:%d, Rumble Pack:%d", software_bus, getName().c_str(), vmuCount, vibrationCount); } void DreamPicoPort::disconnect() { if (connection_established) { connection_established = false; if (--connected_dev_count == 0) { // serial is no longer needed serial.reset(); } } } void DreamPicoPort::sendPort() { if (connection_established && serial && software_bus >= 0 && software_bus <= 3 && hardware_bus >=0 && hardware_bus <= 3) { // This will update the displayed port letter on the screen std::ostringstream s; s << "XP "; // XP is flycast "set port" command s << hardware_bus << " " << software_bus << "\n"; serial->sendCmd(s.str(), timeout_ms); } } int DreamPicoPort::hardwareBus() const { return hardware_bus; } bool DreamPicoPort::isHardwareBusImplied() const { return is_hardware_bus_implied; } bool DreamPicoPort::isSingleDevice() const { return is_single_device; } void DreamPicoPort::determineHardwareBus(int joystick_idx, SDL_Joystick* sdl_joystick) { // This function determines what bus index to use when communicating with the hardware. // Set the serial number if found by SDL Joystick const char* joystick_serial = SDL_JoystickGetSerial(sdl_joystick); if (joystick_serial) { serial_number = joystick_serial; } #if defined(_WIN32) // This only works in Windows because the joystick_path is not given in other OSes const char* joystick_name = SDL_JoystickName(sdl_joystick); const char* joystick_path = SDL_JoystickPath(sdl_joystick); struct SDL_hid_device_info* devs = SDL_hid_enumerate(VID, PID); if (devs) { struct SDL_hid_device_info* my_dev = nullptr; if (!devs->next) { // Only single device found, so this is simple (host-1p firmware used) hardware_bus = 0; is_hardware_bus_implied = false; is_single_device = true; my_dev = devs; } else { struct SDL_hid_device_info* it = devs; if (joystick_path) { while (it) { // Note: hex characters will be differing case, so case-insensitive cmp is needed if (it->path && 0 == SDL_strcasecmp(it->path, joystick_path)) { my_dev = it; break; } it = it->next; } } if (my_dev) { it = devs; int count = 0; if (my_dev->serial_number) { while (it) { if (it->serial_number && 0 == wcscmp(it->serial_number, my_dev->serial_number)) { ++count; } it = it->next; } if (count == 1) { // Single device of this serial found is_single_device = true; hardware_bus = 0; is_hardware_bus_implied = false; } else { is_single_device = false; if (my_dev->release_number < 0x0102) { // Interfaces go in decending order hardware_bus = (count - (my_dev->interface_number % 4) - 1); is_hardware_bus_implied = false; } else { // Version 1.02 of interface will make interfaces in ascending order hardware_bus = (my_dev->interface_number % 4); is_hardware_bus_implied = false; } } } } } // Set serial number if found in SDL_hid if (my_dev) { if (serial_number.empty() && my_dev->serial_number) { std::wstring_convert> converter; serial_number = converter.to_bytes(my_dev->serial_number); } } SDL_hid_free_enumeration(devs); } #endif if (hardware_bus < 0) { // The number of buttons gives a clue as to what index the controller is int nbuttons = SDL_JoystickNumButtons(sdl_joystick); if (nbuttons >= 32 || nbuttons <= 27) { // Older version of firmware or single player hardware_bus = 0; is_hardware_bus_implied = true; is_single_device = true; } else { hardware_bus = 31 - nbuttons; is_hardware_bus_implied = false; is_single_device = false; } } } bool DreamPicoPort::queryInterfaceVersion() { std::string buffer; asio::error_code error = serial->sendCmd("XV\n", buffer, timeout_ms); if (error) { WARN_LOG(INPUT, "DreamPicoPort[%d] send(XV) failed: %s", software_bus, error.message().c_str()); return false; } if (0 == strncmp("*failed", buffer.c_str(), 7) || 0 == strncmp("0: failed", buffer.c_str(), 9)) { // Using a version of firmware before "XV" was available interface_version = 0.0; } else { try { interface_version = std::stod(buffer); } catch(const std::exception&) { WARN_LOG(INPUT, "DreamPicoPort[%d] command XV received invalid response: %s", software_bus, buffer.c_str()); return false; } } return true; } bool DreamPicoPort::queryPeripherals() { peripherals.clear(); expansionDevs = 0; MapleMsg msg; msg.command = MDCF_GetCondition; msg.destAP = (hardware_bus << 6) | 0x20; msg.originAP = hardware_bus << 6; msg.setData(MFID_0_Input); asio::error_code error = serial->sendMsg(msg, hardware_bus, msg, timeout_ms); if (error) { WARN_LOG(INPUT, "DreamPicoPort[%d] send(condition) failed: %s", software_bus, error.message().c_str()); return true; // assume simply controller not connected yet } expansionDevs = msg.originAP & 0x1f; if (interface_version >= 1.0) { // Can just use X? std::string buffer; error = serial->sendCmd("X?" + std::to_string(hardware_bus) + "\n", buffer, timeout_ms); if (error) { WARN_LOG(INPUT, "DreamPicoPort[%d] send(X?) failed: %s", software_bus, error.message().c_str()); return false; } { std::istringstream stream(buffer); std::string outerGroup; while (std::getline(stream, outerGroup, ';')) { if (outerGroup.empty() || outerGroup == ",") continue; std::vector> outerList; std::istringstream outerStream(outerGroup.substr(1)); // Skip the leading '{' std::string innerGroup; while (std::getline(outerStream, innerGroup, '}')) { if (innerGroup.empty() || innerGroup == ",") continue; std::array innerList = {{0, 0}}; std::istringstream innerStream(innerGroup.substr(1)); // Skip the leading '{' std::string number; std::size_t idx = 0; while (std::getline(innerStream, number, ',')) { if (!number.empty() && number[0] == '{') { number = number.substr(1); } uint32_t value; std::stringstream ss; ss << std::hex << number; ss >> value; if (idx < 2) { innerList[idx] = value; } ++idx; } outerList.push_back(innerList); } peripherals.push_back(outerList); } } } else { // TODO: probably should just pop up a toast asking user to update firmware // Manually query each sub-peripheral peripherals.push_back({}); // skip controller since it's not used for (u32 i = 0; i < 2; ++i) { std::vector> portPeripherals; u8 port = (1 << i); if (expansionDevs & port) { msg.command = MDC_DeviceRequest; msg.destAP = (hardware_bus << 6) | port; msg.originAP = hardware_bus << 6; msg.size = 0; error = serial->sendMsg(msg, hardware_bus, msg, timeout_ms); if (error) { WARN_LOG(INPUT, "DreamPicoPort[%d] send(query) failed: %s", software_bus, error.message().c_str()); return false; } if (msg.size < 4) { WARN_LOG(INPUT, "DreamPicoPort[%d] read(query) failed: invalid size %d", software_bus, msg.size); return false; } const u32 fnCode = (msg.data[0] << 24) | (msg.data[1] << 16) | (msg.data[2] << 8) | msg.data[3]; u8 fnIdx = 1; u32 mask = 0x80000000; while (mask > 0) { if (fnCode & mask) { u32 i = fnIdx++ * 4; u32 code = (msg.data[i] << 24) | (msg.data[i+1] << 16) | (msg.data[i+2] << 8) | msg.data[i+3]; std::array peripheral = {{mask, code}}; portPeripherals.push_back(std::move(peripheral)); } mask >>= 1; } } peripherals.push_back(portPeripherals); } } return true; } #endif // USE_DREAMCASTCONTROLLER