OXYGENHUB
◆ main openmsx / src / utils / SerialPort.cc
13.9 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
1
#include "SerialPort.hh"
2
3
#include "ReadDir.hh"
4
#include "StringOp.hh"
5
#include "one_of.hh"
6
#include "strCat.hh"
7
#include "utf8_checked.hh"
8
9
#include <cerrno>
10
#include <cstdlib>
11
#include <cstring>
12
#include <algorithm>
13
#include <array>
14
#ifndef _WIN32
15
#include <fcntl.h>
16
#include <sys/ioctl.h>
17
#include <sys/types.h>
18
#include <sys/stat.h>
19
#include <termios.h>
20
#include <unistd.h>
21
#ifdef __APPLE__
22
#include <TargetConditionals.h>
23
#endif
24
#if defined(__APPLE__) && TARGET_OS_OSX
25
#include <IOKit/serial/ioss.h>
26
#endif
27
#endif
28
#ifdef _WIN32
29
#include <windows.h>
30
#endif
31
32
namespace openmsx::serial {
33
34
// Windows already defines INVALID_HANDLE_VALUE as the HANDLE sentinel; on
35
// POSIX serial handles are plain ints with -1 as the invalid value.
36
#ifndef INVALID_HANDLE_VALUE
37
#define INVALID_HANDLE_VALUE -1
38
#endif
39
40
// Captures the platform error state at the moment of failure.
41
static ErrorCode currentError()
42
{
43
#ifdef _WIN32
44
	return ErrorCode{GetLastError()};
45
#else
46
	return ErrorCode{errno};
47
#endif
48
}
49
50
#ifdef _WIN32
51
52
static DCB serialParamsToDCB(const SerialParams& params)
53
{
54
	DCB dcb = {};
55
	dcb.DCBlength = sizeof(dcb);
56
	dcb.BaudRate = params.baudRate;
57
	dcb.fBinary = TRUE;
58
	dcb.fParity = params.parity != 0 ? TRUE : FALSE;
59
	dcb.fOutxCtsFlow = FALSE;
60
	dcb.fOutxDsrFlow = FALSE;
61
	dcb.fDtrControl = DTR_CONTROL_DISABLE;
62
	dcb.fDsrSensitivity = FALSE;
63
	dcb.fTXContinueOnXoff = FALSE;
64
	dcb.fOutX = FALSE;
65
	dcb.fInX = FALSE;
66
	dcb.fErrorChar = FALSE;
67
	dcb.fNull = FALSE;
68
	dcb.fRtsControl = RTS_CONTROL_DISABLE;
69
	dcb.fAbortOnError = TRUE;
70
	dcb.ByteSize = static_cast<BYTE>(params.dataBits);
71
	switch (params.stopBits) {
72
	case 1:  dcb.StopBits = ONESTOPBIT; break;
73
	case 2:  dcb.StopBits = TWOSTOPBITS; break;
74
	default: dcb.StopBits = ONE5STOPBITS; break; // 1.5
75
	}
76
	switch (params.parity) {
77
	case 0:  dcb.Parity = NOPARITY; break;
78
	case 1:  dcb.Parity = ODDPARITY; break;
79
	case 2:  dcb.Parity = EVENPARITY; break;
80
	case 3:  dcb.Parity = MARKPARITY; break;
81
	case 4:  dcb.Parity = SPACEPARITY; break;
82
	default: dcb.Parity = NOPARITY; break;
83
	}
84
	return dcb;
85
}
86
87
std::expected<Handle, ErrorCode> open(zstring_view portName, const SerialParams& params)
88
{
89
	std::string fullName = R"(\\.\)";
90
	fullName += portName;
91
	HANDLE h = CreateFileA(
92
		fullName.c_str(),
93
		GENERIC_READ | GENERIC_WRITE,
94
		0, nullptr,
95
		OPEN_EXISTING,
96
		FILE_FLAG_OVERLAPPED,
97
		nullptr);
98
	if (h == INVALID_HANDLE_VALUE) {
99
		return std::unexpected(currentError());
100
	}
101
102
	if (!SetupComm(h, 4096, 4096)) {
103
		auto ec = currentError();
104
		CloseHandle(h);
105
		return std::unexpected(ec);
106
	}
107
108
	if (DCB dcb = serialParamsToDCB(params);!SetCommState(h, &dcb)) {
109
		auto ec = currentError();
110
		CloseHandle(h);
111
		return std::unexpected(ec);
112
	}
113
114
	COMMTIMEOUTS timeouts = {};
115
	timeouts.ReadIntervalTimeout = 0;
116
	timeouts.ReadTotalTimeoutMultiplier = 0;
117
	timeouts.ReadTotalTimeoutConstant = 0;
118
	timeouts.WriteTotalTimeoutMultiplier = 0;
119
	timeouts.WriteTotalTimeoutConstant = 0;
120
	if (!SetCommTimeouts(h, &timeouts)) {
121
		auto ec = currentError();
122
		CloseHandle(h);
123
		return std::unexpected(ec);
124
	}
125
126
	return Handle(h);
127
}
128
129
IoResult Handle::read(std::span<uint8_t> buf) const
130
{
131
	DWORD bytesRead = 0;
132
	OVERLAPPED ov = {};
133
	ov.hEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr);
134
	if (!ov.hEvent) return std::unexpected(currentError());
135
136
	if (!ReadFile(handle, buf.data(), static_cast<DWORD>(buf.size()), &bytesRead, &ov)) {
137
		if ((GetLastError() == ERROR_IO_PENDING) && (WaitForSingleObject(ov.hEvent, INFINITE) == WAIT_OBJECT_0)) {
138
			if (!GetOverlappedResult(handle, &ov, &bytesRead, FALSE)) {
139
				auto ec = currentError();
140
				CloseHandle(ov.hEvent);
141
				return std::unexpected(ec);
142
			}
143
		} else {
144
			auto ec = currentError();
145
			CloseHandle(ov.hEvent);
146
			return std::unexpected(ec);
147
		}
148
	}
149
	CloseHandle(ov.hEvent);
150
	return IoResult(bytesRead);
151
}
152
153
IoResult Handle::write(std::span<const uint8_t> buf) const
154
{
155
	DWORD bytesWritten = 0;
156
	OVERLAPPED ov = {};
157
	ov.hEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr);
158
	if (!ov.hEvent) return std::unexpected(currentError());
159
160
	if (!WriteFile(handle, buf.data(), static_cast<DWORD>(buf.size()), &bytesWritten, &ov)) {
161
		if ((GetLastError() == ERROR_IO_PENDING) && (WaitForSingleObject(ov.hEvent, INFINITE) == WAIT_OBJECT_0)) {
162
			if (!GetOverlappedResult(handle, &ov, &bytesWritten, FALSE)) {
163
				auto ec = currentError();
164
				CloseHandle(ov.hEvent);
165
				return std::unexpected(ec);
166
			}
167
		} else {
168
			auto ec = currentError();
169
			CloseHandle(ov.hEvent);
170
			return std::unexpected(ec);
171
		}
172
	}
173
	CloseHandle(ov.hEvent);
174
	return IoResult(bytesWritten);
175
}
176
177
std::expected<void, ErrorCode> Handle::set_params(const SerialParams& params) const
178
{
179
	DCB dcb = {};
180
	dcb.DCBlength = sizeof(dcb);
181
	if (!GetCommState(handle, &dcb)) return std::unexpected(currentError());
182
183
	dcb.BaudRate = params.baudRate;
184
	dcb.fParity = params.parity != 0 ? TRUE : FALSE;
185
	dcb.ByteSize = static_cast<BYTE>(params.dataBits);
186
	switch (params.stopBits) {
187
	case 1:  dcb.StopBits = ONESTOPBIT; break;
188
	case 2:  dcb.StopBits = TWOSTOPBITS; break;
189
	default: dcb.StopBits = ONE5STOPBITS; break;
190
	}
191
	switch (params.parity) {
192
	case 0:  dcb.Parity = NOPARITY; break;
193
	case 1:  dcb.Parity = ODDPARITY; break;
194
	case 2:  dcb.Parity = EVENPARITY; break;
195
	case 3:  dcb.Parity = MARKPARITY; break;
196
	case 4:  dcb.Parity = SPACEPARITY; break;
197
	default: dcb.Parity = NOPARITY; break;
198
	}
199
200
	if (!SetCommState(handle, &dcb)) return std::unexpected(currentError());
201
	return {};
202
}
203
204
std::expected<void, ErrorCode> Handle::set_dtr(bool on) const
205
{
206
	if (!EscapeCommFunction(handle, on ? SETDTR : CLRDTR)) {
207
		return std::unexpected(currentError());
208
	}
209
	return {};
210
}
211
212
std::expected<void, ErrorCode> Handle::set_rts(bool on) const
213
{
214
	if (!EscapeCommFunction(handle, on ? SETRTS : CLRRTS)) {
215
		return std::unexpected(currentError());
216
	}
217
	return {};
218
}
219
220
std::expected<Handle::ModemStatus, ErrorCode> Handle::get_modem_status() const
221
{
222
	DWORD status;
223
	if (!GetCommModemStatus(handle, &status)) {
224
		return std::unexpected(currentError());
225
	}
226
	return ModemStatus{
227
		.cts = (status & MS_CTS_ON) != 0,
228
		.dsr = (status & MS_DSR_ON) != 0,
229
		.dcd = (status & MS_RLSD_ON) != 0,
230
		.ri  = (status & MS_RING_ON) != 0,
231
	};
232
}
233
234
std::vector<std::string> list_ports()
235
{
236
	std::vector<std::string> ports;
237
	for (int i = 1; i <= 256; ++i) {
238
		auto name = strCat("COM", i);
239
		std::string fullName = R"(\\.\)" + name;
240
		HANDLE h = CreateFileA(
241
			fullName.c_str(),
242
			GENERIC_READ | GENERIC_WRITE,
243
			0, nullptr,
244
			OPEN_EXISTING,
245
			FILE_FLAG_OVERLAPPED,
246
			nullptr);
247
		if (h != INVALID_HANDLE_VALUE) {
248
			CloseHandle(h);
249
			ports.push_back(name);
250
		}
251
	}
252
	return ports;
253
}
254
255
#else // POSIX (Linux / macOS / BSD)
256
257
struct BaudMapItem {
258
	unsigned baud;
259
	speed_t speed;
260
};
261
262
static constexpr auto BAUDRATE_MAP = std::to_array<BaudMapItem>({
263
	{ 50, B50 },
264
	{ 75, B75 },
265
	{ 110, B110 },
266
	{ 134, B134 },
267
	{ 150, B150 },
268
	{ 200, B200 },
269
	{ 300, B300 },
270
	{ 600, B600 },
271
	{ 1200, B1200 },
272
	{ 1800, B1800 },
273
	{ 2400, B2400 },
274
	{ 4800, B4800 },
275
	{ 9600, B9600 },
276
	{ 19200, B19200 },
277
	{ 38400, B38400 },
278
	{ 57600, B57600 },
279
	{ 115200, B115200 },
280
	{ 230400, B230400 },
281
#ifdef B460800
282
	{ 460800, B460800 },
283
#endif
284
#ifdef B921600
285
	{ 921600, B921600 },
286
#endif
287
});
288
289
#ifdef __linux__
290
#ifndef BOTHER
291
#define BOTHER 0010000
292
#endif
293
#endif
294
295
static speed_t baudToSpeedExact(unsigned baud)
296
{
297
	for (auto& item : BAUDRATE_MAP) {
298
		if (item.baud == baud) return item.speed;
299
	}
300
	return B0;
301
}
302
303
#ifndef __linux__
304
static speed_t baudToSpeedNearest(unsigned baud)
305
{
306
	speed_t bestSpeed = B115200;
307
	unsigned bestDiff = UINT_MAX;
308
	for (auto& item : BAUDRATE_MAP) {
309
		unsigned diff = (item.baud > baud) ? item.baud - baud
310
		                                  : baud - item.baud;
311
		if (diff < bestDiff) {
312
			bestSpeed = item.speed;
313
			bestDiff = diff;
314
			if (diff == 0) break;
315
		}
316
	}
317
	return bestSpeed;
318
}
319
#endif
320
321
static void set_termios_baud(struct termios& tio, unsigned baud)
322
{
323
	if (speed_t speed = baudToSpeedExact(baud); speed != B0) {
324
		cfsetispeed(&tio, speed);
325
		cfsetospeed(&tio, speed);
326
	} else {
327
#ifdef __linux__
328
		cfsetispeed(&tio, BOTHER);
329
		cfsetospeed(&tio, BOTHER);
330
		tio.c_ispeed = static_cast<speed_t>(baud);
331
		tio.c_ospeed = static_cast<speed_t>(baud);
332
#else
333
		speed = baudToSpeedNearest(baud);
334
		cfsetispeed(&tio, speed);
335
		cfsetospeed(&tio, speed);
336
#endif
337
	}
338
}
339
340
static bool apply_custom_baud_post([[maybe_unused]] int fd, unsigned baud)
341
{
342
	if (baudToSpeedExact(baud) != B0) return true;
343
#if defined(__APPLE__) && TARGET_OS_OSX
344
	int speed = static_cast<int>(baud);
345
	return ioctl(fd, IOSSIOSPEED, &speed) >= 0;
346
#else
347
	return true;
348
#endif
349
}
350
351
std::expected<Handle, ErrorCode> open(zstring_view portName, const SerialParams& params)
352
{
353
	int fd = ::open(portName.c_str(), O_RDWR | O_NOCTTY | O_NONBLOCK);
354
	if (fd < 0) return std::unexpected(currentError());
355
356
	struct termios tio = {};
357
	if (tcgetattr(fd, &tio) < 0) {
358
		auto ec = currentError();
359
		::close(fd);
360
		return std::unexpected(ec);
361
	}
362
363
	// Input modes
364
	tio.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON);
365
	// Output modes
366
	tio.c_oflag &= ~OPOST;
367
	// Control modes
368
	tio.c_cflag &= ~(CSIZE | CSTOPB | PARENB | PARODD | CRTSCTS);
369
	tio.c_cflag |= CS8 | CREAD | CLOCAL;
370
	// Local modes
371
	tio.c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN);
372
373
	// Apply data bits, parity, stop bits
374
	tio.c_cflag &= ~CSIZE;
375
	switch (params.dataBits) {
376
	case 5: tio.c_cflag |= CS5; break;
377
	case 6: tio.c_cflag |= CS6; break;
378
	case 7: tio.c_cflag |= CS7; break;
379
	default: tio.c_cflag |= CS8; break;
380
	}
381
	if (params.parity != 0) {
382
		tio.c_cflag |= PARENB;
383
		if (params.parity == 1) tio.c_cflag |= PARODD; // odd
384
	}
385
	if (params.stopBits >= 2) {
386
		tio.c_cflag |= CSTOPB;
387
	}
388
389
	// Set timeouts for non-blocking read
390
	tio.c_cc[VMIN] = 0;
391
	tio.c_cc[VTIME] = 1; // 100ms timeout
392
393
	set_termios_baud(tio, params.baudRate);
394
	if (tcsetattr(fd, TCSANOW, &tio) < 0) {
395
		auto ec = currentError();
396
		::close(fd);
397
		return std::unexpected(ec);
398
	}
399
	if (!apply_custom_baud_post(fd, params.baudRate)) {
400
		auto ec = currentError();
401
		::close(fd);
402
		return std::unexpected(ec);
403
	}
404
405
	// Clear non-blocking for regular I/O
406
	int flags = fcntl(fd, F_GETFL, 0);
407
	if (flags >= 0) {
408
		fcntl(fd, F_SETFL, flags & ~O_NONBLOCK);
409
	}
410
411
	tcflush(fd, TCIOFLUSH);
412
413
	return Handle(fd);
414
}
415
416
IoResult Handle::read(std::span<uint8_t> buf) const
417
{
418
	auto n = ::read(handle, buf.data(), buf.size());
419
	if (n >= 0) return IoResult(n);
420
	return std::unexpected(currentError());
421
}
422
423
IoResult Handle::write(std::span<const uint8_t> buf) const
424
{
425
	auto n = ::write(handle, buf.data(), buf.size());
426
	if (n >= 0) return IoResult(n);
427
	return std::unexpected(currentError());
428
}
429
430
std::expected<void, ErrorCode> Handle::set_params(const SerialParams& params) const
431
{
432
	struct termios tio = {};
433
	if (tcgetattr(handle, &tio) < 0) return std::unexpected(currentError());
434
435
	tio.c_cflag &= ~CSIZE;
436
	switch (params.dataBits) {
437
	case 5: tio.c_cflag |= CS5; break;
438
	case 6: tio.c_cflag |= CS6; break;
439
	case 7: tio.c_cflag |= CS7; break;
440
	default: tio.c_cflag |= CS8; break;
441
	}
442
	if (params.parity != 0) {
443
		tio.c_cflag |= PARENB;
444
		if (params.parity == 1) tio.c_cflag |= PARODD;
445
	} else {
446
		tio.c_cflag &= ~PARENB;
447
	}
448
	if (params.stopBits >= 2) {
449
		tio.c_cflag |= CSTOPB;
450
	} else {
451
		tio.c_cflag &= ~CSTOPB;
452
	}
453
454
	set_termios_baud(tio, params.baudRate);
455
	if (tcsetattr(handle, TCSANOW, &tio) < 0) return std::unexpected(currentError());
456
	if (!apply_custom_baud_post(handle, params.baudRate)) {
457
		return std::unexpected(currentError());
458
	}
459
	return {};
460
}
461
462
std::expected<void, ErrorCode> Handle::set_dtr(bool on) const
463
{
464
	int status = TIOCM_DTR;
465
	if (ioctl(handle, on ? TIOCMBIS : TIOCMBIC, &status) < 0) {
466
		return std::unexpected(currentError());
467
	}
468
	return {};
469
}
470
471
std::expected<void, ErrorCode> Handle::set_rts(bool on) const
472
{
473
	int status = TIOCM_RTS;
474
	if (ioctl(handle, on ? TIOCMBIS : TIOCMBIC, &status) < 0) {
475
		return std::unexpected(currentError());
476
	}
477
	return {};
478
}
479
480
std::expected<Handle::ModemStatus, ErrorCode> Handle::get_modem_status() const
481
{
482
	int status;
483
	if (ioctl(handle, TIOCMGET, &status) < 0) {
484
		return std::unexpected(currentError());
485
	}
486
	return ModemStatus{
487
		.cts = (status & TIOCM_CTS) != 0,
488
		.dsr = (status & TIOCM_DSR) != 0,
489
		.dcd = (status & TIOCM_CD) != 0,
490
		.ri  = (status & TIOCM_RI) != 0,
491
	};
492
}
493
494
// Serial device name prefixes in /dev (OMAP, CH340/341/343, ...)
495
static constexpr auto portPrefixes = std::to_array<std::string_view>({
496
	"ttyS", "ttyUSB", "ttyACM", "ttyAMA",
497
	"tty.usb", "tty.USA", "cu.usb", "cu.USA",
498
	"ttyXRUSB", "ttyO", "ttyCH"
499
});
500
501
std::vector<std::string> list_ports()
502
{
503
	std::vector<std::string> ports;
504
	ReadDir dir("/dev");
505
	while (auto* entry = dir.getEntry()) {
506
		for (auto pfx : portPrefixes) {
507
			if (std::string_view(entry->d_name).starts_with(pfx)) {
508
				ports.push_back(strCat("/dev/", entry->d_name));
509
				break;
510
			}
511
		}
512
	}
513
	std::ranges::sort(ports);
514
	return ports;
515
}
516
517
#endif
518
519
// Handle move/destructor code, shared between the Windows and POSIX
520
// implementations (only the "close" call and the invalid-handle sentinel
521
// are platform-specific).
522
Handle::Handle(serial_handle_t handle_)
523
	: handle(handle_)
524
{
525
}
526
527
Handle::Handle(Handle&& other) noexcept
528
	: handle(std::exchange(other.handle, INVALID_HANDLE_VALUE))
529
{
530
}
531
532
Handle& Handle::operator=(Handle&& other) noexcept
533
{
534
	if (this != &other) {
535
		release();
536
		handle = std::exchange(other.handle, INVALID_HANDLE_VALUE);
537
	}
538
	return *this;
539
}
540
541
Handle::~Handle()
542
{
543
	release();
544
}
545
546
void Handle::release() noexcept
547
{
548
	if (handle != INVALID_HANDLE_VALUE) {
549
#ifdef _WIN32
550
		CloseHandle(handle);
551
#else
552
		::close(handle);
553
#endif
554
		handle = INVALID_HANDLE_VALUE;
555
	}
556
}
557
558
std::string to_string(ErrorCode ec)
559
{
560
#ifdef _WIN32
561
	std::array<wchar_t, 512> buffer{};
562
563
	DWORD char_count = FormatMessageW(
564
		FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
565
		nullptr,
566
		static_cast<DWORD>(ec.value),
567
		MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
568
		buffer.data(),
569
		static_cast<DWORD>(buffer.size()),
570
		nullptr
571
	);
572
573
	if (char_count == 0) {
574
		return "Unknown error (" + std::to_string(ec.value) + ")";
575
	}
576
	std::wstring_view sv(buffer.data(), char_count);
577
578
	while (!sv.empty() && sv.back() == one_of(L'\n', L'\r')) {
579
		sv.remove_suffix(1);
580
	}
581
582
	return utf8::utf16to8(sv);
583
#else
584
	return strerror(ec.value);
585
#endif
586
}
587
588
} // namespace openmsx::serial