diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/StringSetting.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/StringSetting.kt index 8dc9e619c5..f959764564 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/StringSetting.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/StringSetting.kt @@ -29,6 +29,12 @@ enum class StringSetting( "BBA_TAPSERVER_DESTINATION", "/tmp/dolphin-tap" ), + MAIN_MODEM_TAPSERVER_DESTINATION( + Settings.FILE_DOLPHIN, + Settings.SECTION_INI_CORE, + "MODEM_TAPSERVER_DESTINATION", + "/tmp/dolphin-modem-tap" + ), MAIN_CUSTOM_RTC_VALUE( Settings.FILE_DOLPHIN, Settings.SECTION_INI_CORE, diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt index a0de70a723..a3ec018b46 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt @@ -1121,6 +1121,16 @@ class SettingsFragmentPresenter( R.string.bba_builtin_dns_description ) ) + } else if (serialPort1Type == 13) { + // Modem Adapter (tapserver) + sl.add( + InputStringSetting( + context, + StringSetting.MAIN_MODEM_TAPSERVER_DESTINATION, + R.string.modem_tapserver_destination, + R.string.modem_tapserver_destination_description + ) + ) } } diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml index 2e8fc6856e..89ddc6b705 100644 --- a/Source/Android/app/src/main/res/values/strings.xml +++ b/Source/Android/app/src/main/res/values/strings.xml @@ -135,6 +135,8 @@ IP address or hostname of device running the XLink Kai client Tapserver destination Enter the socket path or netloc (address:port) of the tapserver instance + Tapserver destination + Enter the socket path or netloc (address:port) of the tapserver instance DNS Server Use 8.8.8.8 for normal DNS, else enter your custom one diff --git a/Source/Core/Core/CMakeLists.txt b/Source/Core/Core/CMakeLists.txt index 10b60a1a1c..f36235d7d4 100644 --- a/Source/Core/Core/CMakeLists.txt +++ b/Source/Core/Core/CMakeLists.txt @@ -193,6 +193,7 @@ add_library(core HW/EXI/BBA/XLINK_KAI_BBA.cpp HW/EXI/BBA/BuiltIn.cpp HW/EXI/BBA/BuiltIn.h + HW/EXI/Modem/TAPServer.cpp HW/EXI/EXI_Channel.cpp HW/EXI/EXI_Channel.h HW/EXI/EXI_Device.cpp @@ -213,6 +214,8 @@ add_library(core HW/EXI/EXI_DeviceMemoryCard.h HW/EXI/EXI_DeviceMic.cpp HW/EXI/EXI_DeviceMic.h + HW/EXI/EXI_DeviceModem.cpp + HW/EXI/EXI_DeviceModem.h HW/EXI/EXI.cpp HW/EXI/EXI.h HW/GBAPad.cpp diff --git a/Source/Core/Core/Config/MainSettings.cpp b/Source/Core/Core/Config/MainSettings.cpp index cdd852c260..432abbeb24 100644 --- a/Source/Core/Core/Config/MainSettings.cpp +++ b/Source/Core/Core/Config/MainSettings.cpp @@ -139,6 +139,8 @@ const Info MAIN_BBA_BUILTIN_DNS{{System::Main, "Core", "BBA_BUILTIN "3.18.217.27"}; const Info MAIN_BBA_TAPSERVER_DESTINATION{ {System::Main, "Core", "BBA_TAPSERVER_DESTINATION"}, "/tmp/dolphin-tap"}; +const Info MAIN_MODEM_TAPSERVER_DESTINATION{ + {System::Main, "Core", "MODEM_TAPSERVER_DESTINATION"}, "/tmp/dolphin-modem-tap"}; const Info MAIN_BBA_BUILTIN_IP{{System::Main, "Core", "BBA_BUILTIN_IP"}, ""}; const Info& GetInfoForSIDevice(int channel) diff --git a/Source/Core/Core/Config/MainSettings.h b/Source/Core/Core/Config/MainSettings.h index b13615df5d..5c028d9b36 100644 --- a/Source/Core/Core/Config/MainSettings.h +++ b/Source/Core/Core/Config/MainSettings.h @@ -97,6 +97,7 @@ extern const Info MAIN_BBA_XLINK_CHAT_OSD; extern const Info MAIN_BBA_BUILTIN_DNS; extern const Info MAIN_BBA_BUILTIN_IP; extern const Info MAIN_BBA_TAPSERVER_DESTINATION; +extern const Info MAIN_MODEM_TAPSERVER_DESTINATION; const Info& GetInfoForSIDevice(int channel); const Info& GetInfoForAdapterRumble(int channel); const Info& GetInfoForSimulateKonga(int channel); diff --git a/Source/Core/Core/HW/EXI/EXI_Device.cpp b/Source/Core/Core/HW/EXI/EXI_Device.cpp index ad25a94522..0d3ba0f8f9 100644 --- a/Source/Core/Core/HW/EXI/EXI_Device.cpp +++ b/Source/Core/Core/HW/EXI/EXI_Device.cpp @@ -14,6 +14,7 @@ #include "Core/HW/EXI/EXI_DeviceIPL.h" #include "Core/HW/EXI/EXI_DeviceMemoryCard.h" #include "Core/HW/EXI/EXI_DeviceMic.h" +#include "Core/HW/EXI/EXI_DeviceModem.h" #include "Core/HW/Memmap.h" #include "Core/System.h" @@ -149,6 +150,10 @@ std::unique_ptr EXIDevice_Create(Core::System& system, const EXIDevi result = std::make_unique(system, BBADeviceType::BuiltIn); break; + case EXIDeviceType::ModemTapServer: + result = std::make_unique(system, ModemDeviceType::TAPSERVER); + break; + case EXIDeviceType::Gecko: result = std::make_unique(system); break; diff --git a/Source/Core/Core/HW/EXI/EXI_Device.h b/Source/Core/Core/HW/EXI/EXI_Device.h index c927a255d3..f405de167e 100644 --- a/Source/Core/Core/HW/EXI/EXI_Device.h +++ b/Source/Core/Core/HW/EXI/EXI_Device.h @@ -41,6 +41,7 @@ enum class EXIDeviceType : int EthernetXLink, EthernetTapServer, EthernetBuiltIn, + ModemTapServer, None = 0xFF }; @@ -87,7 +88,7 @@ std::unique_ptr EXIDevice_Create(Core::System& system, EXIDeviceType template <> struct fmt::formatter - : EnumFormatter + : EnumFormatter { static constexpr array_type names = { _trans("Dummy"), @@ -104,6 +105,7 @@ struct fmt::formatter _trans("Broadband Adapter (XLink Kai)"), _trans("Broadband Adapter (tapserver)"), _trans("Broadband Adapter (HLE)"), + _trans("Modem Adapter (tapserver)"), }; constexpr formatter() : EnumFormatter(names) {} diff --git a/Source/Core/Core/HW/EXI/EXI_DeviceModem.cpp b/Source/Core/Core/HW/EXI/EXI_DeviceModem.cpp new file mode 100644 index 0000000000..867044ec89 --- /dev/null +++ b/Source/Core/Core/HW/EXI/EXI_DeviceModem.cpp @@ -0,0 +1,382 @@ +// Copyright 2008 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "Core/HW/EXI/EXI_DeviceModem.h" + +#include + +#include +#include +#include +#include + +#include "Common/BitUtils.h" +#include "Common/ChunkFile.h" +#include "Common/CommonTypes.h" +#include "Common/Logging/Log.h" +#include "Common/Network.h" +#include "Common/StringUtil.h" +#include "Core/Config/MainSettings.h" +#include "Core/CoreTiming.h" +#include "Core/HW/EXI/EXI.h" +#include "Core/HW/Memmap.h" +#include "Core/PowerPC/PowerPC.h" +#include "Core/System.h" + +namespace ExpansionInterface +{ + +CEXIModem::CEXIModem(Core::System& system, ModemDeviceType type) : IEXIDevice(system) +{ + switch (type) + { + case ModemDeviceType::TAPSERVER: + m_network_interface = std::make_unique( + this, Config::Get(Config::MAIN_MODEM_TAPSERVER_DESTINATION)); + INFO_LOG_FMT(SP1, "Created tapserver physical network interface."); + break; + } + + for (size_t z = 0; z < m_regs.size(); z++) + { + m_regs[z] = 0; + } + m_regs[Register::DEVICE_TYPE] = 0x02; + m_regs[Register::INTERRUPT_MASK] = 0x02; +} + +CEXIModem::~CEXIModem() +{ + m_network_interface->Deactivate(); +} + +bool CEXIModem::IsPresent() const +{ + return true; +} + +void CEXIModem::SetCS(int cs) +{ + m_transfer_descriptor = INVALID_TRANSFER_DESCRIPTOR; +} + +bool CEXIModem::IsInterruptSet() +{ + return !!(m_regs[Register::INTERRUPT_MASK] & m_regs[Register::PENDING_INTERRUPT_MASK]); +} + +void CEXIModem::ImmWrite(u32 data, u32 size) +{ + if (m_transfer_descriptor == INVALID_TRANSFER_DESCRIPTOR) + { + m_transfer_descriptor = data; + if (m_transfer_descriptor == 0x00008000) + { // Reset + m_network_interface->Deactivate(); + m_transfer_descriptor = INVALID_TRANSFER_DESCRIPTOR; + } + } + else if (!IsWriteTransfer(m_transfer_descriptor)) + { + ERROR_LOG_FMT(SP1, "Received EXI IMM write {:x} ({} bytes) after read command {:x}", data, size, + m_transfer_descriptor); + m_transfer_descriptor = INVALID_TRANSFER_DESCRIPTOR; + } + else if (IsModemTransfer(m_transfer_descriptor)) + { // Write AT command buffer or packet send buffer + u32 be_data = htonl(data); + HandleWriteModemTransfer(&be_data, size); + } + else + { // Write device register + uint8_t reg_num = static_cast((m_transfer_descriptor >> 24) & 0x1F); + bool should_update_interrupts = false; + for (; size; size--) + { + should_update_interrupts |= + ((reg_num == Register::INTERRUPT_MASK) || (reg_num == Register::PENDING_INTERRUPT_MASK)); + m_regs[reg_num++] = (data >> 24); + data <<= 8; + } + if (should_update_interrupts) + { + m_system.GetExpansionInterface().ScheduleUpdateInterrupts(CoreTiming::FromThread::CPU, 0); + } + m_transfer_descriptor = INVALID_TRANSFER_DESCRIPTOR; + } +} + +void CEXIModem::DMAWrite(u32 addr, u32 size) +{ + if (m_transfer_descriptor == INVALID_TRANSFER_DESCRIPTOR) + { + ERROR_LOG_FMT(SP1, "Received EXI DMA write {:x} ({} bytes) after read command {:x}", addr, size, + m_transfer_descriptor); + } + else if (!IsWriteTransfer(m_transfer_descriptor)) + { + ERROR_LOG_FMT(SP1, "Received EXI DMA write {:x} ({} bytes) after read command {:x}", addr, size, + m_transfer_descriptor); + m_transfer_descriptor = INVALID_TRANSFER_DESCRIPTOR; + } + else if (!IsModemTransfer(m_transfer_descriptor)) + { + ERROR_LOG_FMT(SP1, "Received EXI DMA write {:x} ({} bytes) to registers {:x}", addr, size, + m_transfer_descriptor); + m_transfer_descriptor = INVALID_TRANSFER_DESCRIPTOR; + } + else + { + auto& memory = m_system.GetMemory(); + HandleWriteModemTransfer(memory.GetPointer(addr), size); + } +} + +u32 CEXIModem::ImmRead(u32 size) +{ + if (m_transfer_descriptor == INVALID_TRANSFER_DESCRIPTOR) + { + ERROR_LOG_FMT(SP1, "Received EXI IMM read ({} bytes) with no pending transfer", size); + return 0; + } + else if (IsWriteTransfer(m_transfer_descriptor)) + { + ERROR_LOG_FMT(SP1, "Received EXI IMM read ({} bytes) after write command {:x}", size, + m_transfer_descriptor); + m_transfer_descriptor = INVALID_TRANSFER_DESCRIPTOR; + return 0; + } + else if (IsModemTransfer(m_transfer_descriptor)) + { + u32 be_data = 0; + HandleReadModemTransfer(&be_data, size); + return ntohl(be_data); + } + else + { // Read device register + uint8_t reg_num = static_cast((m_transfer_descriptor >> 24) & 0x1F); + if (reg_num == 0) + { + return 0x02020000; // Device ID (modem) + } + u32 ret = 0; + for (size_t z = 0; z < size; z++) + { + ret |= (m_regs[reg_num + z] << ((3 - z) * 8)); + } + m_transfer_descriptor = INVALID_TRANSFER_DESCRIPTOR; + return ret; + } +} + +void CEXIModem::DMARead(u32 addr, u32 size) +{ + if (m_transfer_descriptor == INVALID_TRANSFER_DESCRIPTOR) + { + ERROR_LOG_FMT(SP1, "Received EXI DMA read {:x} ({} bytes) with no pending transfer", addr, + size); + } + else if (IsWriteTransfer(m_transfer_descriptor)) + { + ERROR_LOG_FMT(SP1, "Received EXI DMA read {:x} ({} bytes) after write command {:x}", addr, size, + m_transfer_descriptor); + m_transfer_descriptor = INVALID_TRANSFER_DESCRIPTOR; + } + else if (!IsModemTransfer(m_transfer_descriptor)) + { + ERROR_LOG_FMT(SP1, "Received EXI DMA read {:x} ({} bytes) to registers {:x}", addr, size, + m_transfer_descriptor); + m_transfer_descriptor = INVALID_TRANSFER_DESCRIPTOR; + } + else + { + auto& memory = m_system.GetMemory(); + HandleReadModemTransfer(memory.GetPointer(addr), size); + } +} + +void CEXIModem::HandleReadModemTransfer(void* data, u32 size) +{ + u16 bytes_requested = GetModemTransferSize(m_transfer_descriptor); + if (size > bytes_requested) + { + ERROR_LOG_FMT(SP1, "More bytes requested ({}) than originally requested for transfer {:x}", + size, m_transfer_descriptor); + size = bytes_requested; + } + u16 bytes_requested_after_read = bytes_requested - size; + + if ((m_transfer_descriptor & 0x0F000000) == 0x03000000) + { // AT command buffer + memcpy(data, m_at_reply_data.data(), std::min(size, m_at_reply_data.size())); + m_at_reply_data = m_at_reply_data.substr(size); + m_regs[Register::AT_REPLY_SIZE] = m_at_reply_data.size(); + SetInterruptFlag(Interrupt::AT_REPLY_DATA_AVAILABLE, !m_at_reply_data.empty(), true); + } + else if ((m_transfer_descriptor & 0x0F000000) == 0x08000000) + { // Packet receive buffer + std::lock_guard g(m_receive_buffer_lock); + size_t bytes_to_copy = std::min(size, m_receive_buffer.size()); + memcpy(data, m_receive_buffer.data(), bytes_to_copy); + m_receive_buffer = m_receive_buffer.substr(size); + OnReceiveBufferSizeChangedLocked(true); + } + else + { + ERROR_LOG_FMT(SP1, "Invalid modem read transfer type {:x}", m_transfer_descriptor); + } + + m_transfer_descriptor = + (bytes_requested_after_read == 0) ? + INVALID_TRANSFER_DESCRIPTOR : + SetModemTransferSize(m_transfer_descriptor, bytes_requested_after_read); +} + +void CEXIModem::HandleWriteModemTransfer(const void* data, u32 size) +{ + u16 bytes_expected = GetModemTransferSize(m_transfer_descriptor); + if (size > bytes_expected) + { + ERROR_LOG_FMT(SP1, "More bytes received ({}) than expected for transfer {:x}", size, + m_transfer_descriptor); + return; + } + u16 bytes_expected_after_write = bytes_expected - size; + + if ((m_transfer_descriptor & 0x0F000000) == 0x03000000) + { // AT command buffer + m_at_command_data.append(reinterpret_cast(data), size); + RunAllPendingATCommands(); + m_regs[Register::AT_COMMAND_SIZE] = m_at_command_data.size(); + } + else if ((m_transfer_descriptor & 0x0F000000) == 0x08000000) + { // Packet send buffer + m_send_buffer.append(reinterpret_cast(data), size); + // A more accurate implementation would only set this interrupt if the send + // FIFO has enough space; however, we can clear the send FIFO "instantly" + // from the emulated program's perspective, so we always tell it the send + // FIFO is empty. + SetInterruptFlag(Interrupt::SEND_BUFFER_BELOW_THRESHOLD, true, true); + m_network_interface->SendFrames(); + } + else + { + ERROR_LOG_FMT(SP1, "Invalid modem write transfer type {:x}", m_transfer_descriptor); + } + + m_transfer_descriptor = + (bytes_expected_after_write == 0) ? + INVALID_TRANSFER_DESCRIPTOR : + SetModemTransferSize(m_transfer_descriptor, bytes_expected_after_write); +} + +void CEXIModem::DoState(PointerWrap& p) +{ + // There isn't really any state to save. The registers depend on the state of + // the external connection, which Dolphin doesn't have control over. What + // should happen when the user saves a state during an online session and + // loads it later? The remote server presumably doesn't support point-in-time + // snapshots and reloading thereof. +} + +u16 CEXIModem::GetTxThreshold() const +{ + return (m_regs[Register::TX_THRESHOLD_HIGH] << 8) | m_regs[Register::TX_THRESHOLD_LOW]; +} + +u16 CEXIModem::GetRxThreshold() const +{ + return (m_regs[Register::RX_THRESHOLD_HIGH] << 8) | m_regs[Register::RX_THRESHOLD_LOW]; +} + +void CEXIModem::SetInterruptFlag(uint8_t what, bool enabled, bool from_cpu) +{ + if (enabled) + { + m_regs[Register::PENDING_INTERRUPT_MASK] |= what; + } + else + { + m_regs[Register::PENDING_INTERRUPT_MASK] &= (~what); + } + m_system.GetExpansionInterface().ScheduleUpdateInterrupts( + from_cpu ? CoreTiming::FromThread::CPU : CoreTiming::FromThread::NON_CPU, 0); +} + +void CEXIModem::OnReceiveBufferSizeChangedLocked(bool from_cpu) +{ + // The caller is expected to hold m_receive_buffer_lock when calling this. + uint16_t bytes_available = std::min(m_receive_buffer.size(), 0x200); + m_regs[Register::BYTES_AVAILABLE_HIGH] = (bytes_available >> 8) & 0xFF; + m_regs[Register::BYTES_AVAILABLE_LOW] = bytes_available & 0xFF; + SetInterruptFlag(Interrupt::RECEIVE_BUFFER_ABOVE_THRESHOLD, + m_receive_buffer.size() >= GetRxThreshold(), from_cpu); + // TODO: There is a second interrupt here, which the GameCube modem library + // expects to be used when large frames are received. However, the correct + // semantics for this interrupt aren't obvious. Reverse-engineering some games + // that use the modem adapter makes it look like this interrupt should trigger + // when there's any data at all in the receive buffer, but implementing the + // interrupt this way causes them to crash. Further research is needed. + // SetInterruptFlag(Interrupt::RECEIVE_BUFFER_NOT_EMPTY, !m_receive_buffer.empty(), from_cpu); +} + +void CEXIModem::SendComplete() +{ + // See comment in HandleWriteModemTransfer about why this is always true. + SetInterruptFlag(Interrupt::SEND_BUFFER_BELOW_THRESHOLD, true, true); +} + +void CEXIModem::AddToReceiveBuffer(std::string&& data) +{ + std::lock_guard g(m_receive_buffer_lock); + if (m_receive_buffer.empty()) + { + m_receive_buffer = std::move(data); + } + else + { + m_receive_buffer += data; + } + OnReceiveBufferSizeChangedLocked(false); +} + +void CEXIModem::AddATReply(const std::string& data) +{ + m_at_reply_data += data; + m_regs[Register::AT_REPLY_SIZE] = m_at_reply_data.size(); + SetInterruptFlag(Interrupt::AT_REPLY_DATA_AVAILABLE, !m_at_reply_data.empty(), false); +} + +void CEXIModem::RunAllPendingATCommands() +{ + for (size_t newline_pos = m_at_command_data.find_first_of("\r\n"); + newline_pos != std::string::npos; newline_pos = m_at_command_data.find_first_of("\r\n")) + { + std::string command = m_at_command_data.substr(0, newline_pos); + m_at_command_data = m_at_command_data.substr(newline_pos + 1); + + if (command == "ATZ") + { // Reset + m_network_interface->Deactivate(); + AddATReply("OK\r"); + } + else if (command.substr(0, 3) == "ATD") + { // Dial + if (m_network_interface->Activate()) + { + AddATReply("OK\rCONNECT 115200\r"); // Maximum baud rate + } + else + { + AddATReply("OK\rNO ANSWER\r"); + } + } + else + { + INFO_LOG_FMT(SP1, "Unhandled AT command: {}", command); + AddATReply("OK\r"); + } + } +} + +} // namespace ExpansionInterface diff --git a/Source/Core/Core/HW/EXI/EXI_DeviceModem.h b/Source/Core/Core/HW/EXI/EXI_DeviceModem.h new file mode 100644 index 0000000000..d6ba896667 --- /dev/null +++ b/Source/Core/Core/HW/EXI/EXI_DeviceModem.h @@ -0,0 +1,179 @@ +// Copyright 2008 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#endif + +#include + +#include "Common/Flag.h" +#include "Common/Network.h" +#include "Common/SocketContext.h" +#include "Core/HW/EXI/BBA/BuiltIn.h" +#include "Core/HW/EXI/EXI_Device.h" + +class PointerWrap; + +namespace ExpansionInterface +{ + +#define MODEM_RECV_SIZE 0x800 + +enum +{ + EXI_DEVTYPE_MODEM = 0x02020000, +}; + +enum class ModemDeviceType +{ + TAPSERVER, +}; + +class CEXIModem : public IEXIDevice +{ +public: + CEXIModem(Core::System& system, ModemDeviceType type); + virtual ~CEXIModem(); + void SetCS(int cs) override; + bool IsPresent() const override; + bool IsInterruptSet() override; + void ImmWrite(u32 data, u32 size) override; + u32 ImmRead(u32 size) override; + void DMAWrite(u32 addr, u32 size) override; + void DMARead(u32 addr, u32 size) override; + void DoState(PointerWrap& p) override; + +private: + enum Interrupt + { // Used for Register::INTERRUPT_MASK and Register::PENDING_INTERRUPT_MASK + AT_REPLY_DATA_AVAILABLE = 0x02, + SEND_BUFFER_BELOW_THRESHOLD = 0x10, + RECEIVE_BUFFER_ABOVE_THRESHOLD = 0x20, + RECEIVE_BUFFER_NOT_EMPTY = 0x40, + }; + enum Register + { + DEVICE_TYPE = 0x00, + INTERRUPT_MASK = 0x01, + PENDING_INTERRUPT_MASK = 0x02, + UNKNOWN_03 = 0x03, + AT_COMMAND_SIZE = 0x04, + AT_REPLY_SIZE = 0x05, + UNKNOWN_06 = 0x06, + UNKNOWN_07 = 0x07, + UNKNOWN_08 = 0x08, + BYTES_SENT_HIGH = 0x09, + BYTES_SENT_LOW = 0x0A, + BYTES_AVAILABLE_HIGH = 0x0B, + BYTES_AVAILABLE_LOW = 0x0C, + ESR = 0x0D, + TX_THRESHOLD_HIGH = 0x0E, + TX_THRESHOLD_LOW = 0x0F, + RX_THRESHOLD_HIGH = 0x10, + RX_THRESHOLD_LOW = 0x11, + STATUS = 0x12, + FWT = 0x13, + }; + + u16 GetTxThreshold() const; + u16 GetRxThreshold() const; + void SetInterruptFlag(uint8_t what, bool enabled, bool from_cpu); + void HandleReadModemTransfer(void* data, u32 size); + void HandleWriteModemTransfer(const void* data, u32 size); + void OnReceiveBufferSizeChangedLocked(bool from_cpu); + void SendComplete(); + void AddToReceiveBuffer(std::string&& data); + void RunAllPendingATCommands(); + void AddATReply(const std::string& data); + + static inline bool TransferIsResetCommand(u32 transfer_descriptor) + { + return (transfer_descriptor == 0x80000000); + } + static inline bool IsWriteTransfer(u32 transfer_descriptor) + { + return (transfer_descriptor & 0x40000000); + } + static inline bool IsModemTransfer(u32 transfer_descriptor) + { + return (transfer_descriptor & 0x20000000); + } + static inline u16 GetModemTransferSize(u32 transfer_descriptor) + { + return ((transfer_descriptor >> 8) & 0xFFFF); + } + static inline u32 SetModemTransferSize(u32 transfer_descriptor, u16 new_size) + { + return (transfer_descriptor & 0xFF000000) | (new_size << 8); + } + + class NetworkInterface + { + protected: + CEXIModem* m_modem_ref = nullptr; + explicit NetworkInterface(CEXIModem* modem_ref) : m_modem_ref{modem_ref} {} + + public: + virtual bool Activate() { return false; } + virtual void Deactivate() {} + virtual bool IsActivated() { return false; } + virtual bool SendFrames() { return false; } + virtual bool RecvInit() { return false; } + virtual void RecvStart() {} + virtual void RecvStop() {} + + virtual ~NetworkInterface() = default; + }; + + class TAPServerNetworkInterface : public NetworkInterface + { + public: + explicit TAPServerNetworkInterface(CEXIModem* modem_ref, const std::string& destination) + : NetworkInterface(modem_ref), m_destination(destination) + { + } + + public: + bool Activate() override; + void Deactivate() override; + bool IsActivated() override; + bool SendFrames() override; + bool RecvInit() override; + void RecvStart() override; + void RecvStop() override; + + private: + std::string m_destination; + Common::SocketContext m_socket_context; + + int m_fd = -1; + std::thread m_read_thread; + Common::Flag m_read_enabled; + Common::Flag m_read_shutdown; + + void ReadThreadHandler(); + }; + + std::unique_ptr m_network_interface; + + static constexpr u32 INVALID_TRANSFER_DESCRIPTOR = 0xFFFFFFFF; + + u32 m_transfer_descriptor = INVALID_TRANSFER_DESCRIPTOR; + + std::string m_at_command_data; + std::string m_at_reply_data; + std::string m_send_buffer; + std::mutex m_receive_buffer_lock; + std::string m_receive_buffer; + std::array m_regs; +}; +} // namespace ExpansionInterface diff --git a/Source/Core/Core/HW/EXI/Modem/TAPServer.cpp b/Source/Core/Core/HW/EXI/Modem/TAPServer.cpp new file mode 100644 index 0000000000..9db6e5e76e --- /dev/null +++ b/Source/Core/Core/HW/EXI/Modem/TAPServer.cpp @@ -0,0 +1,324 @@ +// Copyright 2020 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "Core/HW/EXI/EXI_DeviceModem.h" + +#ifdef _WIN32 +#include +#include +#else +#include +#include +#include +#include +#include +#endif + +#include "Common/CommonFuncs.h" +#include "Common/Logging/Log.h" +#include "Common/StringUtil.h" +#include "Core/HW/EXI/EXI_Device.h" + +namespace ExpansionInterface +{ + +#ifdef _WIN32 +static constexpr auto pi_close = &closesocket; +using ws_ssize_t = int; +#else +static constexpr auto pi_close = &close; +using ws_ssize_t = ssize_t; +#endif + +#ifdef __LINUX__ +#define SEND_FLAGS MSG_NOSIGNAL +#else +#define SEND_FLAGS 0 +#endif + +static int ConnectToDestination(const std::string& destination) +{ + if (destination.empty()) + { + ERROR_LOG_FMT(SP1, "Cannot connect: destination is empty\n"); + return -1; + } + + int ss_size; + struct sockaddr_storage ss; + memset(&ss, 0, sizeof(ss)); + if (destination[0] != '/') + { + // IP address or hostname + size_t colon_offset = destination.find(':'); + if (colon_offset == std::string::npos) + { + ERROR_LOG_FMT(SP1, "Destination IP address does not include port\n"); + return -1; + } + + struct sockaddr_in* sin = reinterpret_cast(&ss); + sin->sin_addr.s_addr = htonl(sf::IpAddress(destination.substr(0, colon_offset)).toInteger()); + sin->sin_family = AF_INET; + sin->sin_port = htons(stoul(destination.substr(colon_offset + 1))); + ss_size = sizeof(*sin); +#ifndef _WIN32 + } + else + { + // UNIX socket + struct sockaddr_un* sun = reinterpret_cast(&ss); + if (destination.size() + 1 > sizeof(sun->sun_path)) + { + ERROR_LOG_FMT(SP1, "Socket path is too long, unable to init BBA\n"); + return -1; + } + sun->sun_family = AF_UNIX; + strcpy(sun->sun_path, destination.c_str()); + ss_size = sizeof(*sun); +#else + } + else + { + ERROR_LOG_FMT(SP1, "UNIX sockets are not supported on Windows\n"); + return -1; +#endif + } + + int fd = socket(ss.ss_family, SOCK_STREAM, (ss.ss_family == AF_INET) ? IPPROTO_TCP : 0); + if (fd == -1) + { + ERROR_LOG_FMT(SP1, "Couldn't create socket; unable to init BBA\n"); + return -1; + } + +#ifdef __APPLE__ + int opt_no_sigpipe = 1; + if (setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &opt_no_sigpipe, sizeof(opt_no_sigpipe)) < 0) + INFO_LOG_FMT(SP1, "Failed to set SO_NOSIGPIPE on socket\n"); +#endif + + if (connect(fd, reinterpret_cast(&ss), ss_size) == -1) + { + std::string s = Common::LastStrerrorString(); + INFO_LOG_FMT(SP1, "Couldn't connect socket ({}), unable to init BBA\n", s.c_str()); + pi_close(fd); + return -1; + } + + return fd; +} + +bool CEXIModem::TAPServerNetworkInterface::Activate() +{ + if (IsActivated()) + return true; + + m_fd = ConnectToDestination(m_destination); + if (m_fd < 0) + { + return false; + } + + INFO_LOG_FMT(SP1, "Modem initialized."); + return RecvInit(); +} + +void CEXIModem::TAPServerNetworkInterface::Deactivate() +{ + if (m_fd >= 0) + { + pi_close(m_fd); + } + m_fd = -1; + + m_read_enabled.Clear(); + m_read_shutdown.Set(); + if (m_read_thread.joinable()) + { + m_read_thread.join(); + } + m_read_shutdown.Clear(); +} + +bool CEXIModem::TAPServerNetworkInterface::IsActivated() +{ + return (m_fd >= 0); +} + +bool CEXIModem::TAPServerNetworkInterface::RecvInit() +{ + m_read_thread = std::thread(&CEXIModem::TAPServerNetworkInterface::ReadThreadHandler, this); + return true; +} + +void CEXIModem::TAPServerNetworkInterface::RecvStart() +{ + m_read_enabled.Set(); +} + +void CEXIModem::TAPServerNetworkInterface::RecvStop() +{ + m_read_enabled.Clear(); +} + +bool CEXIModem::TAPServerNetworkInterface::SendFrames() +{ + while (!m_modem_ref->m_send_buffer.empty()) + { + size_t start_offset = m_modem_ref->m_send_buffer.find(0x7E); + if (start_offset == std::string::npos) + { + break; + } + size_t end_sentinel_offset = m_modem_ref->m_send_buffer.find(0x7E, start_offset + 1); + if (end_sentinel_offset == std::string::npos) + { + break; + } + size_t end_offset = end_sentinel_offset + 1; + size_t size = end_offset - start_offset; + + uint8_t size_bytes[2] = {static_cast(size), static_cast(size >> 8)}; + if (send(m_fd, size_bytes, 2, SEND_FLAGS) != 2) + { + ERROR_LOG_FMT(SP1, "SendFrames(): could not write size field"); + return false; + } + int written_bytes = + send(m_fd, m_modem_ref->m_send_buffer.data() + start_offset, size, SEND_FLAGS); + if (u32(written_bytes) != size) + { + ERROR_LOG_FMT(SP1, "SendFrames(): expected to write {} bytes, instead wrote {}", size, + written_bytes); + return false; + } + else + { + m_modem_ref->m_send_buffer = m_modem_ref->m_send_buffer.substr(end_offset); + m_modem_ref->SendComplete(); + } + } + return true; +} + +void CEXIModem::TAPServerNetworkInterface::ReadThreadHandler() +{ + enum class ReadState + { + SIZE, + SIZE_HIGH, + DATA, + SKIP, + }; + ReadState read_state = ReadState::SIZE; + + size_t frame_bytes_received = 0; + size_t frame_bytes_expected = 0; + std::string frame_data; + + while (!m_read_shutdown.IsSet()) + { + fd_set rfds; + FD_ZERO(&rfds); + FD_SET(m_fd, &rfds); + + timeval timeout; + timeout.tv_sec = 0; + timeout.tv_usec = 50000; + if (select(m_fd + 1, &rfds, nullptr, nullptr, &timeout) <= 0) + continue; + + // The tapserver protocol is very simple: there is a 16-bit little-endian + // size field, followed by that many bytes of packet data + switch (read_state) + { + case ReadState::SIZE: + { + u8 size_bytes[2]; + ws_ssize_t bytes_read = recv(m_fd, reinterpret_cast(size_bytes), 2, 0); + if (bytes_read == 1) + { + read_state = ReadState::SIZE_HIGH; + frame_bytes_expected = size_bytes[0]; + } + else if (bytes_read == 2) + { + frame_bytes_expected = size_bytes[0] | (size_bytes[1] << 8); + frame_data.resize(frame_bytes_expected, '\0'); + if (frame_bytes_expected > MODEM_RECV_SIZE) + { + ERROR_LOG_FMT(SP1, "Packet is too large ({} bytes); dropping it", frame_bytes_expected); + read_state = ReadState::SKIP; + } + else + { + read_state = ReadState::DATA; + } + } + else + { + ERROR_LOG_FMT(SP1, "Failed to read size field from destination: {}", + Common::LastStrerrorString()); + } + break; + } + case ReadState::SIZE_HIGH: + { + // This handles the annoying case where only one byte of the size field + // was available earlier. + u8 size_high = 0; + ws_ssize_t bytes_read = recv(m_fd, reinterpret_cast(&size_high), 1, 0); + if (bytes_read == 1) + { + frame_bytes_expected |= (size_high << 8); + frame_data.resize(frame_bytes_expected, '\0'); + if (frame_bytes_expected > MODEM_RECV_SIZE) + { + ERROR_LOG_FMT(SP1, "Packet is too large ({} bytes); dropping it", frame_bytes_expected); + read_state = ReadState::SKIP; + } + else + { + read_state = ReadState::DATA; + } + } + else + { + ERROR_LOG_FMT(SP1, "Failed to read split size field from destination: {}", + Common::LastStrerrorString()); + } + break; + } + case ReadState::DATA: + case ReadState::SKIP: + { + ws_ssize_t bytes_read = recv(m_fd, frame_data.data() + frame_bytes_received, + frame_data.size() - frame_bytes_received, 0); + if (bytes_read <= 0) + { + ERROR_LOG_FMT(SP1, "Failed to read data from destination: {}", + Common::LastStrerrorString()); + } + else + { + frame_bytes_received += bytes_read; + if (frame_bytes_received == frame_bytes_expected) + { + if (read_state == ReadState::DATA) + { + m_modem_ref->AddToReceiveBuffer(std::move(frame_data)); + } + frame_data.clear(); + frame_bytes_received = 0; + frame_bytes_expected = 0; + read_state = ReadState::SIZE; + } + } + break; + } + } + } +} + +} // namespace ExpansionInterface diff --git a/Source/Core/DolphinQt/Settings/BroadbandAdapterSettingsDialog.cpp b/Source/Core/DolphinQt/Settings/BroadbandAdapterSettingsDialog.cpp index 9716d8f211..27579b5ac8 100644 --- a/Source/Core/DolphinQt/Settings/BroadbandAdapterSettingsDialog.cpp +++ b/Source/Core/DolphinQt/Settings/BroadbandAdapterSettingsDialog.cpp @@ -49,7 +49,12 @@ void BroadbandAdapterSettingsDialog::InitControls() break; case Type::TapServer: - current_address = QString::fromStdString(Config::Get(Config::MAIN_BBA_TAPSERVER_DESTINATION)); + case Type::ModemTapServer: + { + bool is_modem = (m_bba_type == Type::ModemTapServer); + current_address = + QString::fromStdString(Config::Get(is_modem ? Config::MAIN_MODEM_TAPSERVER_DESTINATION : + Config::MAIN_BBA_TAPSERVER_DESTINATION)); #ifdef _WIN32 address_label = new QLabel(tr("Destination (address:port):")); address_placeholder = QStringLiteral(""); @@ -58,12 +63,24 @@ void BroadbandAdapterSettingsDialog::InitControls() #else address_label = new QLabel(tr("Destination (UNIX socket path or address:port):")); address_placeholder = QStringLiteral("/tmp/dolphin-tap"); - description = new QLabel(tr( - "The default value \"/tmp/dolphin-tap\" will work with a local tapserver and newserv. You " - "can also enter a network location (address:port) to connect to a remote tapserver.")); + if (is_modem) + { + description = new QLabel( + tr("The default value \"/tmp/dolphin-modem-tap\" will work with a local tapserver and " + "newserv. You " + "can also enter a network location (address:port) to connect to a remote tapserver.")); + } + else + { + description = new QLabel( + tr("The default value \"/tmp/dolphin-tap\" will work with a local tapserver and newserv. " + "You " + "can also enter a network location (address:port) to connect to a remote tapserver.")); + } #endif window_title = tr("BBA destination address"); break; + } case Type::BuiltIn: address_label = new QLabel(tr("Enter the DNS server to use:")); @@ -134,6 +151,9 @@ void BroadbandAdapterSettingsDialog::SaveAddress() case Type::TapServer: Config::SetBaseOrCurrent(Config::MAIN_BBA_TAPSERVER_DESTINATION, bba_new_address); break; + case Type::ModemTapServer: + Config::SetBaseOrCurrent(Config::MAIN_MODEM_TAPSERVER_DESTINATION, bba_new_address); + break; case Type::BuiltIn: Config::SetBaseOrCurrent(Config::MAIN_BBA_BUILTIN_DNS, bba_new_address); break; diff --git a/Source/Core/DolphinQt/Settings/BroadbandAdapterSettingsDialog.h b/Source/Core/DolphinQt/Settings/BroadbandAdapterSettingsDialog.h index 53863f1d15..08a8793a08 100644 --- a/Source/Core/DolphinQt/Settings/BroadbandAdapterSettingsDialog.h +++ b/Source/Core/DolphinQt/Settings/BroadbandAdapterSettingsDialog.h @@ -16,7 +16,8 @@ public: Ethernet, XLinkKai, TapServer, - BuiltIn + BuiltIn, + ModemTapServer }; explicit BroadbandAdapterSettingsDialog(QWidget* target, Type bba_type); diff --git a/Source/Core/DolphinQt/Settings/GameCubePane.cpp b/Source/Core/DolphinQt/Settings/GameCubePane.cpp index 944efa7a9f..c268625959 100644 --- a/Source/Core/DolphinQt/Settings/GameCubePane.cpp +++ b/Source/Core/DolphinQt/Settings/GameCubePane.cpp @@ -151,6 +151,7 @@ void GameCubePane::CreateWidgets() EXIDeviceType::EthernetXLink, EXIDeviceType::EthernetTapServer, EXIDeviceType::EthernetBuiltIn, + EXIDeviceType::ModemTapServer, }) { m_slot_combos[ExpansionInterface::Slot::SP1]->addItem(tr(fmt::format("{:n}", device).c_str()), @@ -354,7 +355,8 @@ void GameCubePane::UpdateButton(ExpansionInterface::Slot slot) has_config = (device == ExpansionInterface::EXIDeviceType::Ethernet || device == ExpansionInterface::EXIDeviceType::EthernetXLink || device == ExpansionInterface::EXIDeviceType::EthernetTapServer || - device == ExpansionInterface::EXIDeviceType::EthernetBuiltIn); + device == ExpansionInterface::EXIDeviceType::EthernetBuiltIn || + device == ExpansionInterface::EXIDeviceType::ModemTapServer); break; } @@ -406,6 +408,14 @@ void GameCubePane::OnConfigPressed(ExpansionInterface::Slot slot) dialog.exec(); return; } + case ExpansionInterface::EXIDeviceType::ModemTapServer: + { + BroadbandAdapterSettingsDialog dialog(this, + BroadbandAdapterSettingsDialog::Type::ModemTapServer); + SetQWidgetWindowDecorations(&dialog); + dialog.exec(); + return; + } case ExpansionInterface::EXIDeviceType::EthernetBuiltIn: { BroadbandAdapterSettingsDialog dialog(this, BroadbandAdapterSettingsDialog::Type::BuiltIn);