OXYGENHUB
◆ main openmsx / src / serial / RS232Raw.cc
9.8 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
1
#include "RS232Raw.hh"
2
3
#include "RS232Connector.hh"
4
5
#include "CliComm.hh"
6
#include "CommandController.hh"
7
#include "EventDistributor.hh"
8
#include "ImGuiCpp.hh"
9
#include "ImGuiUtils.hh"
10
#include "PlugException.hh"
11
#include "Scheduler.hh"
12
#include "serialize.hh"
13
14
#include "checked_cast.hh"
15
16
#include <imgui.h>
17
18
#include <cassert>
19
#include <chrono>
20
#include <string>
21
#include <vector>
22
23
namespace openmsx {
24
25
RS232Raw::RS232Raw(EventDistributor& eventDistributor_,
26
                   Scheduler& scheduler_,
27
                   CommandController& commandController)
28
	: eventDistributor(eventDistributor_), scheduler(scheduler_)
29
	, cliComm(commandController.getCliComm())
30
	, rs232RawPortSetting(
31
	        commandController, "rs232-raw-port",
32
	        "Serial port name for RS232 raw pluggable",
33
#ifdef _WIN32
34
	        "COM1"
35
#else
36
	        "/dev/ttyS0"
37
#endif
38
        )
39
{
40
	eventDistributor.registerEventListener(EventType::RS232_RAW, *this);
41
	rs232RawPortSetting.attach(*this);
42
}
43
44
RS232Raw::~RS232Raw()
45
{
46
	tearDown();
47
	rs232RawPortSetting.detach(*this);
48
	eventDistributor.unregisterEventListener(EventType::RS232_RAW, *this);
49
}
50
51
static serial::SerialParams buildParamsImpl(unsigned baud, SerialDataInterface::DataBits dataBits, SerialDataInterface::StopBits stopBits, bool parityEnabled, SerialDataInterface::Parity parity)
52
{
53
	serial::SerialParams p;
54
	p.baudRate = baud;
55
	p.dataBits = static_cast<int>(dataBits);
56
	switch (stopBits) {
57
	using enum SerialDataInterface::StopBits;
58
	case S1:   p.stopBits = 1; break;
59
	case S1_5: p.stopBits = 1; break;
60
	case S2:   p.stopBits = 2; break;
61
	default:   p.stopBits = 1; break;
62
	}
63
	if (parityEnabled) {
64
		p.parity = (parity == SerialDataInterface::Parity::ODD) ? 1 : 2;
65
	} else {
66
		p.parity = 0;
67
	}
68
	return p;
69
}
70
71
void RS232Raw::applyParams()
72
{
73
#ifdef _WIN32
74
	if (!handle && !thread.joinable()) return;
75
	tearDown();
76
	{
77
		std::scoped_lock lock(mutex);
78
		queue.clear();
79
	}
80
	auto portName = rs232RawPortSetting.getString();
81
	if (portName.empty()) return;
82
	setup(std::string(portName), buildParams());
83
#else
84
	// POSIX: nothing to reconfigure while the worker is still opening the
85
	// port (the open uses the parameters it was spawned with). Also
86
	// deliberately no tear down + reopen here: that would re-assert the
87
	// modem lines and might cause misbehavior on the connected device due
88
	// to the unexpected state change.
89
	if (!handle) return;
90
	auto p = buildParams();
91
	(void)handle->set_params(p);
92
	(void)handle->set_dtr(DTR);
93
	(void)handle->set_rts(RTS);
94
#endif
95
}
96
97
// Pluggable
98
void RS232Raw::plugHelper(Connector& connector_, EmuTime /*time*/)
99
{
100
	auto portName = rs232RawPortSetting.getString();
101
	if (portName.empty()) {
102
		throw PlugException("No serial port specified");
103
	}
104
105
	auto& rs232Connector = checked_cast<RS232Connector&>(connector_);
106
	directConn = &rs232Connector;
107
108
	// Initialize the modem-line state BEFORE the worker opens the port, so
109
	// the lines are applied in their final state (DTR/RTS asserted) and never
110
	// toggle afterwards: a transition resets ESP32-style auto-reset circuits.
111
	DCD = false;
112
	RI  = false;
113
	CTS = false;
114
	DSR = true;
115
	DTR = true;
116
	RTS = true;
117
118
	setup(std::string(portName), buildParams());
119
120
	rs232Connector.setDataBits(currentDataBits);
121
	rs232Connector.setStopBits(currentStopBits);
122
	rs232Connector.setParityBit(currentParityEnabled, currentParity);
123
124
	setConnector(&connector_);
125
}
126
127
void RS232Raw::unplugHelper(EmuTime /*time*/)
128
{
129
	tearDown();
130
	directConn = nullptr;
131
}
132
133
zstring_view RS232Raw::getName() const
134
{
135
	return "rs232-raw";
136
}
137
138
zstring_view RS232Raw::getDescription() const
139
{
140
	return "RS232 raw serial port pluggable. Connects the RS232 port to "
141
	       "a host serial port, selected with the 'rs232-raw-port' setting.";
142
}
143
144
void RS232Raw::handleImGuiExtraMenuItems()
145
{
146
	if (auto cur = rs232RawPortSetting.getString(); cur != currentPort) {
147
		ports = serial::list_ports();
148
		currentPort = std::string(cur);
149
	}
150
	im::Indent([&]{
151
		ImGui::SetNextItemWidth(ImGui::GetFontSize() * 12.0f);
152
		im::Combo("##rs232-raw-port", currentPort.c_str(), [this]{
153
			for (const auto& p : ports) {
154
				bool selected = (p == currentPort);
155
				if (ImGui::Selectable(p.c_str(), selected)) {
156
					rs232RawPortSetting.setString(p);
157
					currentPort = p;
158
				}
159
				if (selected) {
160
					ImGui::SetItemDefaultFocus();
161
				}
162
			}
163
		});
164
		simpleToolTip("Select the host serial port for RS232 raw");
165
		ImGui::SameLine();
166
		if (ImGui::SmallButton("Refresh")) {
167
			ports = serial::list_ports();
168
		}
169
	});
170
}
171
172
// Stops the worker thread and closes the port. Safe to call in any state.
173
void RS232Raw::tearDown()
174
{
175
	if (thread.joinable()) {
176
		poller->abort();
177
	}
178
	handle.reset(); // closes the port: unblocks the worker's pending read
179
	if (thread.joinable()) {
180
		thread.join();
181
	}
182
	poller.reset();
183
}
184
185
// Spawns the worker thread: it opens the port (retrying briefly) and then
186
// runs the read loop. The retry delays sleep the worker thread, never the
187
// emulation thread.
188
void RS232Raw::setup(std::string portName, serial::SerialParams params)
189
{
190
	poller.emplace();
191
	thread = std::thread([this, pn = std::move(portName), pm = std::move(params)]() mutable {
192
		openAndRun(std::move(pn), std::move(pm));
193
	});
194
}
195
196
serial::SerialParams RS232Raw::buildParams() const
197
{
198
	return buildParamsImpl(currentBaud, currentDataBits, currentStopBits,
199
	                       currentParityEnabled, currentParity);
200
}
201
202
// Opens the port (retrying briefly) and then runs the read loop. Runs on
203
// the worker thread: the retry delays sleep this host thread, never the
204
// emulation thread (a just-closed Windows COM port can stay owned by the
205
// OS for a few ms, so an immediate reopen can transiently fail).
206
void RS232Raw::openAndRun(std::string portName, serial::SerialParams params)
207
{
208
	auto h = serial::open(portName, params);
209
	for (int attempt = 0; !h.has_value() && attempt < 4 && !poller->aborted();
210
	     ++attempt) {
211
		std::this_thread::sleep_for(std::chrono::milliseconds(10));
212
		h = serial::open(portName, params);
213
	}
214
	if (poller->aborted()) {
215
		return; // unplugged while opening: drop the port (h closes it)
216
	}
217
	if (!h.has_value()) {
218
		cliComm.printWarning("Failed to open serial port ", portName, ": ",
219
		                     serial::to_string(h.error()));
220
		return;
221
	}
222
	(void)h->set_params(params);
223
	(void)h->set_dtr(DTR);
224
	(void)h->set_rts(RTS);
225
	handle = std::move(*h);
226
	run();
227
}
228
229
void RS232Raw::run()
230
{
231
	auto* conn = directConn;
232
	bool direct = conn && conn->directByteDelivery();
233
234
	while (handle && !poller->aborted()) {
235
#ifndef _WIN32
236
		if (poller->poll(handle->native_handle())) {
237
			break;
238
		}
239
#endif
240
241
		uint8_t b;
242
		auto r = handle->read(std::span<uint8_t>(&b, 1));
243
		if (!r.has_value()) {
244
			handle.reset();
245
		} else if (*r == 0) {
246
#ifdef _WIN32
247
			std::this_thread::sleep_for(std::chrono::milliseconds(1));
248
#endif
249
		} else if (direct) {
250
			conn->recvByte(b, EmuTime::zero());
251
		} else {
252
			assert(isPluggedIn());
253
			{
254
				std::scoped_lock lock(mutex);
255
				queue.push_back(b);
256
			}
257
			eventDistributor.distributeEvent(Rs232RawEvent());
258
		}
259
	}
260
}
261
262
// input
263
void RS232Raw::signal(EmuTime time)
264
{
265
	if (!handle) return;
266
267
	auto* conn = checked_cast<RS232Connector*>(getConnector());
268
269
	if (!conn->acceptsData()) {
270
		return;
271
	}
272
273
	if (!conn->ready() || !RTS) return;
274
275
	std::scoped_lock lock(mutex);
276
	if (queue.empty()) return;
277
	auto b = queue.pop_front();
278
	conn->recvByte(b, time);
279
}
280
281
// EventListener
282
bool RS232Raw::signalEvent(const Event& /*event*/)
283
{
284
	if (isPluggedIn()) {
285
		signal(scheduler.getCurrentTime());
286
	} else {
287
		std::scoped_lock lock(mutex);
288
		queue.clear();
289
	}
290
	return false;
291
}
292
293
// output
294
void RS232Raw::recvByte(uint8_t value, EmuTime /*time*/)
295
{
296
	if (!handle) return;
297
298
	if (!handle->write(std::span<const uint8_t>(&value, 1)).has_value()) {
299
		handle.reset();
300
	}
301
}
302
303
// Control lines
304
305
std::optional<bool> RS232Raw::getDSR(EmuTime /*time*/) const
306
{
307
	if (!handle) return {};
308
	return DSR.load();
309
}
310
311
std::optional<bool> RS232Raw::getCTS(EmuTime /*time*/) const
312
{
313
	if (!handle) return {};
314
	return CTS.load();
315
}
316
317
std::optional<bool> RS232Raw::getDCD(EmuTime /*time*/) const
318
{
319
	if (!handle) return {};
320
	return DCD.load();
321
}
322
323
std::optional<bool> RS232Raw::getRI(EmuTime /*time*/) const
324
{
325
	if (!handle) return {};
326
	return RI.load();
327
}
328
329
void RS232Raw::setDTR(bool status, EmuTime /*time*/)
330
{
331
	if (DTR == status) return;
332
	DTR = status;
333
	if (handle) {
334
		(void)handle->set_dtr(status);
335
	}
336
}
337
338
void RS232Raw::setRTS(bool status, EmuTime /*time*/)
339
{
340
	if (RTS == status) return;
341
	RTS = status;
342
	if (handle) {
343
		(void)handle->set_rts(status);
344
		if (RTS) {
345
			std::scoped_lock lock(mutex);
346
			if (!queue.empty()) {
347
				eventDistributor.distributeEvent(Rs232RawEvent());
348
			}
349
		}
350
	}
351
}
352
353
// Serial params
354
355
void RS232Raw::setDataBits(DataBits bits)
356
{
357
	if (currentDataBits.load() == bits) return;
358
	currentDataBits.store(bits);
359
	applyParams();
360
}
361
362
void RS232Raw::setStopBits(StopBits bits)
363
{
364
	if (currentStopBits.load() == bits) return;
365
	currentStopBits.store(bits);
366
	applyParams();
367
}
368
369
void RS232Raw::setParityBit(bool enable, Parity parity)
370
{
371
	if (currentParityEnabled.load() == enable && currentParity.load() == parity) return;
372
	currentParityEnabled.store(enable);
373
	currentParity.store(parity);
374
	applyParams();
375
}
376
377
void RS232Raw::setBaudRate(unsigned baud)
378
{
379
	if (currentBaud.load() == baud) return;
380
	currentBaud.store(baud);
381
	applyParams();
382
}
383
384
void RS232Raw::update(const Setting& /*setting*/) noexcept
385
{
386
	tearDown();
387
	{
388
		std::scoped_lock lock(mutex);
389
		queue.clear();
390
	}
391
392
	auto portName = rs232RawPortSetting.getString();
393
	if (!portName.empty() && isPluggedIn()) {
394
		setup(std::string(portName), buildParams());
395
	}
396
}
397
398
template<typename Archive>
399
void RS232Raw::serialize(Archive& /*ar*/, unsigned /*version*/)
400
{
401
	if constexpr (Archive::IS_LOADER) {
402
		if (!isPluggedIn()) return;
403
404
		auto* conn = dynamic_cast<RS232Connector*>(getConnector());
405
		if (!conn) return;
406
		directConn = conn;
407
408
		auto portName = rs232RawPortSetting.getString();
409
		if (portName.empty()) return;
410
411
		setup(std::string(portName), buildParams());
412
	}
413
}
414
INSTANTIATE_SERIALIZE_METHODS(RS232Raw);
415
REGISTER_POLYMORPHIC_INITIALIZER(Pluggable, RS232Raw, "RS232Raw");
416
417
} // namespace openmsx