1SerialPort {
2	classvar <>devicePattern, allPorts;
3	var dataptr, semaphore, <isOpen = false;
4
5	var <>doneAction;
6
7	*initClass {
8		allPorts = Array[];
9		ShutDown.add {
10			this.closeAll;
11		};
12	}
13
14	// device listing
15	*devices {
16		var regQueryResult, devices;
17		var regexp;
18
19		// Backward compatibility
20		if(devicePattern.notNil) {
21			^devicePattern.pathMatch;
22		};
23
24		if(thisProcess.platform.name == \windows) {
25			// There are somewhere around a dozen ways you can get a list of serial port devices
26			// on Windows. We here duplicate the method used in JSSC (Java Simple Serial Connector),
27			// which is in turn used in the Arduino IDE. If it's good enough for Arduino, it's good
28			// enough for us.
29
30			// The registry key HKLM\HARDWARE\DEVICEMAP\SERIALCOMM can be queried using the below
31			// "reg query" command. The next step is to parse its output, which looks like this
32			// (note the use of 4 spaces rather than tabs):
33
34			// HKEY_LOCAL_MACHINE\Hardware\DeviceMap\SerialComm
35			//     \Device\Serial0    REG_SZ    COM3
36			//     \Device\LSIModem5    REG_SZ    COM4
37			//     \Device\USBSER000    REG_SZ    COM5
38
39			// The regexp "\\bREG_SZ {4}(.*)$" matches our desired port names (COM3/COM4/COM5).
40
41			regQueryResult = "reg query HKLM\\HARDWARE\\DEVICEMAP\\SERIALCOMM".unixCmdGetStdOut;
42			devices = [];
43			regQueryResult.split($\n).do { |line|
44				var match;
45				match = line.findRegexp("\\bREG_SZ {4}(.*)$");
46				if(match.notEmpty) {
47					devices = devices.add(match[1][1]);
48				};
49			};
50			^devices;
51		} {
52			// These regexps are also taken from the Arduino IDE:
53			// https://github.com/arduino/Arduino/blob/ec2e9a642a085b32701cf81297ee7c1570177195/arduino-core/src/processing/app/SerialPortList.java#L48
54			regexp = thisProcess.platform.name.switch(
55				\linux,   "^(ttyS|ttyUSB|ttyACM|ttyAMA|rfcomm|ttyO)[0-9]{1,3}",
56				\osx,     "^(tty|cu)\\..*"
57			);
58			devices = [];
59			PathName("/dev/").files.do { |path|
60				if(regexp.matchRegexp(path.fileName)) {
61					devices = devices.add(path.fullPath);
62				};
63			};
64			^devices;
65		};
66	}
67	*listDevices {
68		this.devices.do(_.postln);
69	}
70
71	*new {
72		| port,
73			baudrate(9600),
74			databits(8),
75			stopbit(true),
76			parity(nil),
77			crtscts(false),
78			xonxoff(false)
79			exclusive(false) |
80
81		if (port.isNumber) {
82			"Using integers to identify serial ports is deprecated. Please pass strings instead.".warn;
83			port = this.devices[port] ?? {
84				Error("invalid port index").throw;
85			};
86		}
87
88		^super.new.prInit(
89			port,
90			exclusive,
91			baudrate,
92			databits,
93			stopbit,
94			( even: 1, odd: 2 ).at(parity) ? 0,
95			crtscts,
96			xonxoff
97		)
98	}
99
100	prInit { | ... args |
101		semaphore = Semaphore(0);
102		if ( isOpen.not ){
103			this.prOpen(*args);
104			allPorts = allPorts.add(this);
105			isOpen = true;
106		}
107	}
108
109	close {
110		if (this.isOpen) {
111			this.prClose;
112			isOpen = false;
113		}
114	}
115
116	*closeAll {
117		var ports = allPorts;
118		allPorts = Array[];
119		ports.do(_.close);
120	}
121
122	*cleanupAll {
123		this.deprecated(thisMethod, SerialPort.findMethod('closeAll'));
124		this.closeAll
125	}
126
127	// non-blocking read
128	next {
129		_SerialPort_Next
130		^this.primitiveFailed
131	}
132	// blocking read
133	read {
134		var byte;
135		while { (byte = this.next).isNil } {
136			semaphore.wait;
137		};
138		^byte
139	}
140	// rx errors since last query
141	rxErrors {
142		_SerialPort_RXErrors
143		^this.primitiveFailed;
144	}
145
146	// always blocks
147	put { | byte, timeout=0.005 |
148		if (timeout != 0.005) {
149			"SerialPort:-put: the timeout argument is deprecated and will be removed in a future release".warn
150		};
151
152		if (isOpen) {
153			^this.prPut(byte);
154		}{
155			"SerialPort not open".warn;
156			^false;
157		}
158	}
159
160	putAll { | bytes, timeout=0.005 |
161		if (timeout != 0.005) {
162			"SerialPort:-putAll: the timeout argument is deprecated and will be removed in a future release".warn
163		};
164
165		bytes.do { |byte|
166			this.put(byte);
167		}
168	}
169
170	// PRIMITIVE
171	prOpen { | ... args |
172		_SerialPort_Open
173		^this.primitiveFailed
174	}
175
176	// Close the port; triggers doneAction
177	prClose {
178		_SerialPort_Close
179		^this.primitiveFailed
180	}
181
182	prPut { | byte |
183		_SerialPort_Put
184		^this.primitiveFailed
185	}
186
187	// Deletes the C++ object for this port.
188	prCleanup {
189		_SerialPort_Cleanup
190		^this.primitiveFailed
191	}
192
193	// callback from C++ read thread
194	prDataAvailable {
195		semaphore.signal;
196	}
197
198	// callback from C++ read thread when done
199	prDoneAction {
200		// Catches case where connection is closed unexpectedly.
201		isOpen = false;
202
203		// Ensure memory is freed even if doneAction throws.
204		protect {
205			this.doneAction.value
206		} {
207			// Needs to run after this callback; otherwise crash when
208			// we try to wait for this thread to join, from itself.
209			// Remove reference as last act, otherwise we risk GC running early.
210			{ this.prCleanup; allPorts.remove(this) }.defer(0);
211		}
212	}
213}
214