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