| // Copyright 2014 The Chromium Authors. All rights reserved. |
| // 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" |
| |
| #define INITGUID |
| #include <devpkey.h> |
| #include <setupapi.h> |
| #include <windows.h> |
| |
| #include <utility> |
| |
| #include "base/bind.h" |
| #include "base/macros.h" |
| #include "base/message_loop/message_loop_current.h" |
| #include "base/scoped_observer.h" |
| #include "base/sequence_checker.h" |
| #include "device/base/device_info_query_win.h" |
| #include "device/base/device_monitor_win.h" |
| #include "services/device/serial/serial_device_enumerator_win.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)); |
| } |
| |
| class SerialIoHandlerWin::UiThreadHelper final |
| : public DeviceMonitorWin::Observer { |
| public: |
| UiThreadHelper( |
| base::WeakPtr<SerialIoHandlerWin> io_handler, |
| scoped_refptr<base::SingleThreadTaskRunner> io_thread_task_runner) |
| : device_observer_(this), |
| io_handler_(io_handler), |
| io_thread_task_runner_(io_thread_task_runner) {} |
| |
| ~UiThreadHelper() { DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); } |
| |
| static void Start(UiThreadHelper* self) { |
| DETACH_FROM_THREAD(self->thread_checker_); |
| DeviceMonitorWin* device_monitor = DeviceMonitorWin::GetForAllInterfaces(); |
| if (device_monitor) |
| self->device_observer_.Add(device_monitor); |
| } |
| |
| private: |
| // DeviceMonitorWin::Observer |
| void OnDeviceRemoved(const GUID& class_guid, |
| const std::string& device_path) override { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| io_thread_task_runner_->PostTask( |
| FROM_HERE, base::BindOnce(&SerialIoHandlerWin::OnDeviceRemoved, |
| io_handler_, device_path)); |
| } |
| |
| THREAD_CHECKER(thread_checker_); |
| ScopedObserver<DeviceMonitorWin, DeviceMonitorWin::Observer> device_observer_; |
| |
| // This weak pointer is only valid when checked on this task runner. |
| base::WeakPtr<SerialIoHandlerWin> io_handler_; |
| scoped_refptr<base::SingleThreadTaskRunner> io_thread_task_runner_; |
| |
| DISALLOW_COPY_AND_ASSIGN(UiThreadHelper); |
| }; |
| |
| void SerialIoHandlerWin::OnDeviceRemoved(const std::string& device_path) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| DeviceInfoQueryWin device_info_query; |
| if (!device_info_query.device_info_list_valid()) { |
| DVPLOG(1) << "Failed to create a device information set"; |
| return; |
| } |
| |
| // This will add the device so we can query driver info. |
| if (!device_info_query.AddDevice(device_path)) { |
| DVPLOG(1) << "Failed to get device interface data for " << device_path; |
| return; |
| } |
| |
| if (!device_info_query.GetDeviceInfo()) { |
| DVPLOG(1) << "Failed to get device info for " << device_path; |
| return; |
| } |
| |
| std::string friendly_name; |
| if (!device_info_query.GetDeviceStringProperty(DEVPKEY_Device_FriendlyName, |
| &friendly_name)) { |
| DVPLOG(1) << "Failed to get device service property"; |
| return; |
| } |
| |
| base::Optional<base::FilePath> path = |
| SerialDeviceEnumeratorWin::GetPath(friendly_name); |
| if (!path) { |
| DVPLOG(1) << "Failed to get device path from \"" << friendly_name << "\"."; |
| return; |
| } |
| |
| if (port() == *path) |
| CancelRead(mojom::SerialReceiveError::DEVICE_LOST); |
| } |
| |
| bool SerialIoHandlerWin::PostOpen() { |
| DCHECK(!comm_context_); |
| DCHECK(!read_context_); |
| DCHECK(!write_context_); |
| |
| base::MessageLoopCurrentForIO::Get()->RegisterIOHandler( |
| file().GetPlatformFile(), this); |
| |
| comm_context_.reset(new base::MessagePumpForIO::IOContext()); |
| read_context_.reset(new base::MessagePumpForIO::IOContext()); |
| write_context_.reset(new base::MessagePumpForIO::IOContext()); |
| |
| scoped_refptr<base::SingleThreadTaskRunner> io_thread_task_runner = |
| base::ThreadTaskRunnerHandle::Get(); |
| helper_ = |
| new UiThreadHelper(weak_factory_.GetWeakPtr(), io_thread_task_runner); |
| ui_thread_task_runner()->PostTask( |
| FROM_HERE, base::BindOnce(&UiThreadHelper::Start, helper_)); |
| |
| // A ReadIntervalTimeout of MAXDWORD will cause async reads to complete |
| // immediately with any data that's available, even if there is none. |
| // This is OK because we never issue a read request until WaitCommEvent |
| // signals that data is available. |
| COMMTIMEOUTS timeouts = {0}; |
| timeouts.ReadIntervalTimeout = MAXDWORD; |
| if (!::SetCommTimeouts(file().GetPlatformFile(), &timeouts)) { |
| VPLOG(1) << "Failed to set serial timeouts"; |
| return false; |
| } |
| |
| return true; |
| } |
| |
| void SerialIoHandlerWin::ReadImpl() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| DCHECK(pending_read_buffer()); |
| DCHECK(file().IsValid()); |
| |
| if (!SetCommMask(file().GetPlatformFile(), EV_RXCHAR)) { |
| VPLOG(1) << "Failed to set serial event flags"; |
| } |
| |
| event_mask_ = 0; |
| BOOL ok = ::WaitCommEvent(file().GetPlatformFile(), &event_mask_, |
| &comm_context_->overlapped); |
| if (!ok && GetLastError() != ERROR_IO_PENDING) { |
| VPLOG(1) << "Failed to receive serial event"; |
| QueueReadCompleted(0, mojom::SerialReceiveError::SYSTEM_ERROR); |
| } |
| is_comm_pending_ = true; |
| } |
| |
| void SerialIoHandlerWin::WriteImpl() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| DCHECK(pending_write_buffer()); |
| DCHECK(file().IsValid()); |
| |
| BOOL ok = ::WriteFile(file().GetPlatformFile(), pending_write_buffer(), |
| pending_write_buffer_len(), NULL, |
| &write_context_->overlapped); |
| if (!ok && GetLastError() != ERROR_IO_PENDING) { |
| VPLOG(1) << "Write failed"; |
| QueueWriteCompleted(0, mojom::SerialSendError::SYSTEM_ERROR); |
| } |
| } |
| |
| void SerialIoHandlerWin::CancelReadImpl() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| DCHECK(file().IsValid()); |
| ::CancelIo(file().GetPlatformFile()); |
| } |
| |
| void SerialIoHandlerWin::CancelWriteImpl() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| DCHECK(file().IsValid()); |
| ::CancelIo(file().GetPlatformFile()); |
| } |
| |
| bool SerialIoHandlerWin::ConfigurePortImpl() { |
| DCB config = {0}; |
| config.DCBlength = sizeof(config); |
| if (!GetCommState(file().GetPlatformFile(), &config)) { |
| VPLOG(1) << "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 = TRUE; |
| 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)) { |
| VPLOG(1) << "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)), |
| event_mask_(0), |
| is_comm_pending_(false), |
| helper_(nullptr), |
| weak_factory_(this) {} |
| |
| SerialIoHandlerWin::~SerialIoHandlerWin() { |
| ui_thread_task_runner()->DeleteSoon(FROM_HERE, helper_); |
| } |
| |
| void SerialIoHandlerWin::OnIOCompleted( |
| base::MessagePumpForIO::IOContext* context, |
| DWORD bytes_transferred, |
| DWORD error) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (context == comm_context_.get()) { |
| DWORD errors; |
| COMSTAT status; |
| if (!ClearCommError(file().GetPlatformFile(), &errors, &status) || |
| errors != 0) { |
| 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 { |
| ReadCompleted(0, mojom::SerialReceiveError::SYSTEM_ERROR); |
| } |
| return; |
| } |
| |
| if (read_canceled()) { |
| ReadCompleted(bytes_transferred, read_cancel_reason()); |
| } else if (error != ERROR_SUCCESS && error != ERROR_OPERATION_ABORTED) { |
| ReadCompleted(0, mojom::SerialReceiveError::SYSTEM_ERROR); |
| } else if (pending_read_buffer()) { |
| BOOL ok = ::ReadFile(file().GetPlatformFile(), pending_read_buffer(), |
| pending_read_buffer_len(), NULL, |
| &read_context_->overlapped); |
| if (!ok && GetLastError() != ERROR_IO_PENDING) { |
| VPLOG(1) << "Read failed"; |
| ReadCompleted(0, mojom::SerialReceiveError::SYSTEM_ERROR); |
| } |
| } |
| } else if (context == read_context_.get()) { |
| if (read_canceled()) { |
| ReadCompleted(bytes_transferred, read_cancel_reason()); |
| } else if (error != ERROR_SUCCESS && error != ERROR_OPERATION_ABORTED) { |
| ReadCompleted(0, mojom::SerialReceiveError::SYSTEM_ERROR); |
| } else { |
| ReadCompleted(bytes_transferred, |
| error == ERROR_SUCCESS |
| ? mojom::SerialReceiveError::NONE |
| : mojom::SerialReceiveError::SYSTEM_ERROR); |
| } |
| } else if (context == write_context_.get()) { |
| DCHECK(pending_write_buffer()); |
| if (write_canceled()) { |
| WriteCompleted(0, write_cancel_reason()); |
| } else if (error != ERROR_SUCCESS && error != ERROR_OPERATION_ABORTED) { |
| WriteCompleted(0, mojom::SerialSendError::SYSTEM_ERROR); |
| if (error == ERROR_GEN_FAILURE && IsReadPending()) { |
| // For devices using drivers such as FTDI, CP2xxx, when device is |
| // disconnected, the context is comm_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); |
| } |
| } else { |
| WriteCompleted(bytes_transferred, |
| error == ERROR_SUCCESS |
| ? mojom::SerialSendError::NONE |
| : mojom::SerialSendError::SYSTEM_ERROR); |
| } |
| } else { |
| NOTREACHED() << "Invalid IOContext"; |
| } |
| } |
| |
| bool SerialIoHandlerWin::Flush() const { |
| if (!PurgeComm(file().GetPlatformFile(), PURGE_RXCLEAR | PURGE_TXCLEAR)) { |
| VPLOG(1) << "Failed to flush serial port"; |
| return false; |
| } |
| return true; |
| } |
| |
| mojom::SerialPortControlSignalsPtr SerialIoHandlerWin::GetControlSignals() |
| const { |
| DWORD status; |
| if (!GetCommModemStatus(file().GetPlatformFile(), &status)) { |
| VPLOG(1) << "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) { |
| if (!EscapeCommFunction(file().GetPlatformFile(), |
| signals.dtr ? SETDTR : CLRDTR)) { |
| VPLOG(1) << "Failed to configure DTR signal"; |
| return false; |
| } |
| } |
| if (signals.has_rts) { |
| if (!EscapeCommFunction(file().GetPlatformFile(), |
| signals.rts ? SETRTS : CLRRTS)) { |
| VPLOG(1) << "Failed to configure RTS signal"; |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| mojom::SerialConnectionInfoPtr SerialIoHandlerWin::GetPortInfo() const { |
| DCB config = {0}; |
| config.DCBlength = sizeof(config); |
| if (!GetCommState(file().GetPlatformFile(), &config)) { |
| VPLOG(1) << "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; |
| } |
| |
| bool SerialIoHandlerWin::SetBreak() { |
| if (!SetCommBreak(file().GetPlatformFile())) { |
| VPLOG(1) << "Failed to set break"; |
| return false; |
| } |
| return true; |
| } |
| |
| bool SerialIoHandlerWin::ClearBreak() { |
| if (!ClearCommBreak(file().GetPlatformFile())) { |
| VPLOG(1) << "Failed to clear break"; |
| return false; |
| } |
| return true; |
| } |
| |
| } // namespace device |