| // Copyright 2014 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "services/device/serial/serial_io_handler_win.h" |
| |
| #include <windows.h> |
| |
| #include <memory> |
| #include <utility> |
| |
| #include "base/functional/bind.h" |
| #include "base/notimplemented.h" |
| #include "base/sequence_checker.h" |
| #include "base/task/current_thread.h" |
| #include "base/task/single_thread_task_runner.h" |
| #include "base/time/time.h" |
| #include "components/device_event_log/device_event_log.h" |
| |
| namespace device { |
| |
| namespace { |
| |
| int BitrateToSpeedConstant(int bitrate) { |
| #define BITRATE_TO_SPEED_CASE(x) \ |
| case x: \ |
| return CBR_##x; |
| switch (bitrate) { |
| BITRATE_TO_SPEED_CASE(110); |
| BITRATE_TO_SPEED_CASE(300); |
| BITRATE_TO_SPEED_CASE(600); |
| BITRATE_TO_SPEED_CASE(1200); |
| BITRATE_TO_SPEED_CASE(2400); |
| BITRATE_TO_SPEED_CASE(4800); |
| BITRATE_TO_SPEED_CASE(9600); |
| BITRATE_TO_SPEED_CASE(14400); |
| BITRATE_TO_SPEED_CASE(19200); |
| BITRATE_TO_SPEED_CASE(38400); |
| BITRATE_TO_SPEED_CASE(57600); |
| BITRATE_TO_SPEED_CASE(115200); |
| BITRATE_TO_SPEED_CASE(128000); |
| BITRATE_TO_SPEED_CASE(256000); |
| default: |
| // If the bitrate doesn't match that of one of the standard |
| // index constants, it may be provided as-is to the DCB |
| // structure, according to MSDN. |
| return bitrate; |
| } |
| #undef BITRATE_TO_SPEED_CASE |
| } |
| |
| int DataBitsEnumToConstant(mojom::SerialDataBits data_bits) { |
| switch (data_bits) { |
| case mojom::SerialDataBits::SEVEN: |
| return 7; |
| case mojom::SerialDataBits::EIGHT: |
| default: |
| return 8; |
| } |
| } |
| |
| int ParityBitEnumToConstant(mojom::SerialParityBit parity_bit) { |
| switch (parity_bit) { |
| case mojom::SerialParityBit::EVEN: |
| return EVENPARITY; |
| case mojom::SerialParityBit::ODD: |
| return ODDPARITY; |
| case mojom::SerialParityBit::NO_PARITY: |
| default: |
| return NOPARITY; |
| } |
| } |
| |
| int StopBitsEnumToConstant(mojom::SerialStopBits stop_bits) { |
| switch (stop_bits) { |
| case mojom::SerialStopBits::TWO: |
| return TWOSTOPBITS; |
| case mojom::SerialStopBits::ONE: |
| default: |
| return ONESTOPBIT; |
| } |
| } |
| |
| int SpeedConstantToBitrate(int speed) { |
| #define SPEED_TO_BITRATE_CASE(x) \ |
| case CBR_##x: \ |
| return x; |
| switch (speed) { |
| SPEED_TO_BITRATE_CASE(110); |
| SPEED_TO_BITRATE_CASE(300); |
| SPEED_TO_BITRATE_CASE(600); |
| SPEED_TO_BITRATE_CASE(1200); |
| SPEED_TO_BITRATE_CASE(2400); |
| SPEED_TO_BITRATE_CASE(4800); |
| SPEED_TO_BITRATE_CASE(9600); |
| SPEED_TO_BITRATE_CASE(14400); |
| SPEED_TO_BITRATE_CASE(19200); |
| SPEED_TO_BITRATE_CASE(38400); |
| SPEED_TO_BITRATE_CASE(57600); |
| SPEED_TO_BITRATE_CASE(115200); |
| SPEED_TO_BITRATE_CASE(128000); |
| SPEED_TO_BITRATE_CASE(256000); |
| default: |
| // If it's not one of the standard index constants, |
| // it should be an integral baud rate, according to |
| // MSDN. |
| return speed; |
| } |
| #undef SPEED_TO_BITRATE_CASE |
| } |
| |
| mojom::SerialDataBits DataBitsConstantToEnum(int data_bits) { |
| switch (data_bits) { |
| case 7: |
| return mojom::SerialDataBits::SEVEN; |
| case 8: |
| default: |
| return mojom::SerialDataBits::EIGHT; |
| } |
| } |
| |
| mojom::SerialParityBit ParityBitConstantToEnum(int parity_bit) { |
| switch (parity_bit) { |
| case EVENPARITY: |
| return mojom::SerialParityBit::EVEN; |
| case ODDPARITY: |
| return mojom::SerialParityBit::ODD; |
| case NOPARITY: |
| default: |
| return mojom::SerialParityBit::NO_PARITY; |
| } |
| } |
| |
| mojom::SerialStopBits StopBitsConstantToEnum(int stop_bits) { |
| switch (stop_bits) { |
| case TWOSTOPBITS: |
| return mojom::SerialStopBits::TWO; |
| case ONESTOPBIT: |
| default: |
| return mojom::SerialStopBits::ONE; |
| } |
| } |
| |
| } // namespace |
| |
| // static |
| scoped_refptr<SerialIoHandler> SerialIoHandler::Create( |
| const base::FilePath& port, |
| scoped_refptr<base::SingleThreadTaskRunner> ui_thread_task_runner) { |
| return new SerialIoHandlerWin(port, std::move(ui_thread_task_runner)); |
| } |
| |
| bool SerialIoHandlerWin::PostOpen() { |
| DCHECK(!read_context_); |
| DCHECK(!write_context_); |
| |
| if (!base::CurrentIOThread::Get()->RegisterIOHandler(file().GetPlatformFile(), |
| this)) { |
| return false; |
| } |
| |
| read_context_ = std::make_unique<base::MessagePumpForIO::IOContext>(); |
| write_context_ = std::make_unique<base::MessagePumpForIO::IOContext>(); |
| |
| // Based on the MSDN documentation setting both ReadIntervalTimeout and |
| // ReadTotalTimeoutMultiplier to MAXDWORD should cause ReadFile() to return |
| // immediately if there is data in the buffer or when a byte arrives while |
| // waiting. |
| // |
| // ReadTotalTimeoutConstant is set to a value low enough to ensure that the |
| // timeout case is exercised frequently but high enough to avoid unnecessary |
| // wakeups as it is not possible to have ReadFile() return immediately when a |
| // byte is received without specifying a timeout. |
| // |
| // https://docs.microsoft.com/en-us/windows/win32/api/winbase/ns-winbase-commtimeouts#remarks |
| COMMTIMEOUTS timeouts = {0}; |
| timeouts.ReadIntervalTimeout = MAXDWORD; |
| timeouts.ReadTotalTimeoutMultiplier = MAXDWORD; |
| timeouts.ReadTotalTimeoutConstant = base::Minutes(5).InMilliseconds(); |
| if (!::SetCommTimeouts(file().GetPlatformFile(), &timeouts)) { |
| SERIAL_PLOG(DEBUG) << "Failed to set serial timeouts"; |
| return false; |
| } |
| |
| return true; |
| } |
| |
| void SerialIoHandlerWin::ReadImpl() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| DCHECK(IsReadPending()); |
| |
| ClearPendingError(); |
| if (!IsReadPending()) |
| return; |
| |
| if (!ReadFile(file().GetPlatformFile(), pending_read_buffer().data(), |
| pending_read_buffer().size(), nullptr, |
| read_context_->GetOverlapped()) && |
| GetLastError() != ERROR_IO_PENDING) { |
| OnIOCompleted(read_context_.get(), 0, GetLastError()); |
| } |
| } |
| |
| void SerialIoHandlerWin::WriteImpl() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| DCHECK(IsWritePending()); |
| |
| if (!WriteFile(file().GetPlatformFile(), pending_write_buffer().data(), |
| pending_write_buffer().size(), nullptr, |
| write_context_->GetOverlapped()) && |
| GetLastError() != ERROR_IO_PENDING) { |
| OnIOCompleted(write_context_.get(), 0, GetLastError()); |
| } |
| } |
| |
| void SerialIoHandlerWin::CancelReadImpl() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| DCHECK(file().IsValid()); |
| |
| if (!PurgeComm(file().GetPlatformFile(), PURGE_RXABORT)) |
| SERIAL_PLOG(DEBUG) << "RX abort failed"; |
| } |
| |
| void SerialIoHandlerWin::CancelWriteImpl() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| DCHECK(file().IsValid()); |
| if (!PurgeComm(file().GetPlatformFile(), PURGE_TXABORT)) |
| SERIAL_PLOG(DEBUG) << "TX abort failed"; |
| } |
| |
| bool SerialIoHandlerWin::ConfigurePortImpl() { |
| DCB config = {0}; |
| config.DCBlength = sizeof(config); |
| if (!GetCommState(file().GetPlatformFile(), &config)) { |
| SERIAL_PLOG(DEBUG) << "Failed to get serial port info"; |
| return false; |
| } |
| |
| // Set up some sane default options that are not configurable. |
| config.fBinary = TRUE; |
| config.fParity = TRUE; |
| config.fAbortOnError = FALSE; |
| config.fOutxDsrFlow = FALSE; |
| config.fDtrControl = DTR_CONTROL_ENABLE; |
| config.fDsrSensitivity = FALSE; |
| config.fOutX = FALSE; |
| config.fInX = FALSE; |
| |
| DCHECK(options().bitrate); |
| config.BaudRate = BitrateToSpeedConstant(options().bitrate); |
| |
| DCHECK(options().data_bits != mojom::SerialDataBits::NONE); |
| config.ByteSize = DataBitsEnumToConstant(options().data_bits); |
| |
| DCHECK(options().parity_bit != mojom::SerialParityBit::NONE); |
| config.Parity = ParityBitEnumToConstant(options().parity_bit); |
| |
| DCHECK(options().stop_bits != mojom::SerialStopBits::NONE); |
| config.StopBits = StopBitsEnumToConstant(options().stop_bits); |
| |
| DCHECK(options().has_cts_flow_control); |
| if (options().cts_flow_control) { |
| config.fOutxCtsFlow = TRUE; |
| config.fRtsControl = RTS_CONTROL_HANDSHAKE; |
| } else { |
| config.fOutxCtsFlow = FALSE; |
| config.fRtsControl = RTS_CONTROL_ENABLE; |
| } |
| |
| if (!SetCommState(file().GetPlatformFile(), &config)) { |
| SERIAL_PLOG(DEBUG) << "Failed to set serial port info"; |
| return false; |
| } |
| return true; |
| } |
| |
| SerialIoHandlerWin::SerialIoHandlerWin( |
| const base::FilePath& port, |
| scoped_refptr<base::SingleThreadTaskRunner> ui_thread_task_runner) |
| : SerialIoHandler(port, std::move(ui_thread_task_runner)), |
| base::MessagePumpForIO::IOHandler(FROM_HERE) {} |
| |
| SerialIoHandlerWin::~SerialIoHandlerWin() = default; |
| |
| void SerialIoHandlerWin::OnIOCompleted( |
| base::MessagePumpForIO::IOContext* context, |
| DWORD bytes_transferred, |
| DWORD error) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (context == read_context_.get()) { |
| if (read_canceled()) { |
| ReadCompleted(bytes_transferred, read_cancel_reason()); |
| } else if (error == ERROR_SUCCESS || error == ERROR_OPERATION_ABORTED) { |
| ReadCompleted(bytes_transferred, mojom::SerialReceiveError::NONE); |
| } else if (error == ERROR_ACCESS_DENIED || error == ERROR_BAD_COMMAND || |
| error == ERROR_DEVICE_REMOVED) { |
| ReadCompleted(0, mojom::SerialReceiveError::DEVICE_LOST); |
| } else { |
| SERIAL_LOG(DEBUG) << "Read failed: " |
| << logging::SystemErrorCodeToString(error); |
| ReadCompleted(0, mojom::SerialReceiveError::SYSTEM_ERROR); |
| } |
| } else if (context == write_context_.get()) { |
| DCHECK(IsWritePending()); |
| if (write_canceled()) { |
| WriteCompleted(0, write_cancel_reason()); |
| } else if (error == ERROR_SUCCESS || error == ERROR_OPERATION_ABORTED) { |
| WriteCompleted(bytes_transferred, mojom::SerialSendError::NONE); |
| } else if (error == ERROR_GEN_FAILURE) { |
| WriteCompleted(0, mojom::SerialSendError::DISCONNECTED); |
| } else { |
| SERIAL_LOG(DEBUG) << "Write failed: " |
| << logging::SystemErrorCodeToString(error); |
| if (error == ERROR_GEN_FAILURE && IsReadPending()) { |
| // For devices using drivers such as FTDI, CP2xxx, when device is |
| // disconnected, the context is |read_context_| and the error is |
| // ERROR_OPERATION_ABORTED. |
| // However, for devices using CDC-ACM driver, when device is |
| // disconnected, the context is |write_context_| and the error is |
| // ERROR_GEN_FAILURE. In this situation, in addition to a write error |
| // signal, also need to generate a read error signal |
| // mojom::SerialOnReceiveError which will notify the app about the |
| // disconnection. |
| CancelRead(mojom::SerialReceiveError::SYSTEM_ERROR); |
| } |
| WriteCompleted(0, mojom::SerialSendError::SYSTEM_ERROR); |
| } |
| } else { |
| NOTREACHED() << "Invalid IOContext"; |
| } |
| } |
| |
| void SerialIoHandlerWin::ClearPendingError() { |
| DWORD errors; |
| if (!ClearCommError(file().GetPlatformFile(), &errors, nullptr)) { |
| SERIAL_PLOG(DEBUG) << "Failed to clear communication error"; |
| return; |
| } |
| |
| if (errors & CE_BREAK) { |
| ReadCompleted(0, mojom::SerialReceiveError::BREAK); |
| } else if (errors & CE_FRAME) { |
| ReadCompleted(0, mojom::SerialReceiveError::FRAME_ERROR); |
| } else if (errors & CE_OVERRUN) { |
| ReadCompleted(0, mojom::SerialReceiveError::OVERRUN); |
| } else if (errors & CE_RXOVER) { |
| ReadCompleted(0, mojom::SerialReceiveError::BUFFER_OVERFLOW); |
| } else if (errors & CE_RXPARITY) { |
| ReadCompleted(0, mojom::SerialReceiveError::PARITY_ERROR); |
| } else if (errors != 0) { |
| NOTIMPLEMENTED() << "Unexpected communication error: " << std::hex |
| << errors; |
| ReadCompleted(0, mojom::SerialReceiveError::SYSTEM_ERROR); |
| } |
| } |
| |
| void SerialIoHandlerWin::Flush(mojom::SerialPortFlushMode mode) const { |
| DWORD flags; |
| switch (mode) { |
| case mojom::SerialPortFlushMode::kReceiveAndTransmit: |
| flags = PURGE_RXCLEAR | PURGE_TXCLEAR; |
| break; |
| case mojom::SerialPortFlushMode::kReceive: |
| flags = PURGE_RXCLEAR; |
| break; |
| case mojom::SerialPortFlushMode::kTransmit: |
| flags = PURGE_TXCLEAR; |
| break; |
| } |
| |
| if (!PurgeComm(file().GetPlatformFile(), flags)) |
| SERIAL_PLOG(DEBUG) << "Failed to flush serial port"; |
| } |
| |
| void SerialIoHandlerWin::Drain() { |
| if (!FlushFileBuffers(file().GetPlatformFile())) |
| SERIAL_PLOG(DEBUG) << "Failed to drain serial port"; |
| } |
| |
| mojom::SerialPortControlSignalsPtr SerialIoHandlerWin::GetControlSignals() |
| const { |
| DWORD status; |
| if (!GetCommModemStatus(file().GetPlatformFile(), &status)) { |
| SERIAL_PLOG(DEBUG) << "Failed to get port control signals"; |
| return mojom::SerialPortControlSignalsPtr(); |
| } |
| |
| auto signals = mojom::SerialPortControlSignals::New(); |
| signals->dcd = (status & MS_RLSD_ON) != 0; |
| signals->cts = (status & MS_CTS_ON) != 0; |
| signals->dsr = (status & MS_DSR_ON) != 0; |
| signals->ri = (status & MS_RING_ON) != 0; |
| return signals; |
| } |
| |
| bool SerialIoHandlerWin::SetControlSignals( |
| const mojom::SerialHostControlSignals& signals) { |
| if (signals.has_dtr && !EscapeCommFunction(file().GetPlatformFile(), |
| signals.dtr ? SETDTR : CLRDTR)) { |
| SERIAL_PLOG(DEBUG) << "Failed to configure data-terminal-ready signal"; |
| return false; |
| } |
| if (signals.has_rts && !EscapeCommFunction(file().GetPlatformFile(), |
| signals.rts ? SETRTS : CLRRTS)) { |
| SERIAL_PLOG(DEBUG) << "Failed to configure request-to-send signal"; |
| return false; |
| } |
| if (signals.has_brk && |
| !EscapeCommFunction(file().GetPlatformFile(), |
| signals.brk ? SETBREAK : CLRBREAK)) { |
| SERIAL_PLOG(DEBUG) << "Failed to configure break signal"; |
| return false; |
| } |
| |
| return true; |
| } |
| |
| mojom::SerialConnectionInfoPtr SerialIoHandlerWin::GetPortInfo() const { |
| DCB config = {0}; |
| config.DCBlength = sizeof(config); |
| if (!GetCommState(file().GetPlatformFile(), &config)) { |
| SERIAL_PLOG(DEBUG) << "Failed to get serial port info"; |
| return mojom::SerialConnectionInfoPtr(); |
| } |
| auto info = mojom::SerialConnectionInfo::New(); |
| info->bitrate = SpeedConstantToBitrate(config.BaudRate); |
| info->data_bits = DataBitsConstantToEnum(config.ByteSize); |
| info->parity_bit = ParityBitConstantToEnum(config.Parity); |
| info->stop_bits = StopBitsConstantToEnum(config.StopBits); |
| info->cts_flow_control = config.fOutxCtsFlow != 0; |
| return info; |
| } |
| |
| } // namespace device |