Skip to content

Commit a358d70

Browse files
committed
fixed ser2tcp to work on windows platform #6
1 parent 27fec59 commit a358d70

File tree

2 files changed

+153
-11
lines changed

2 files changed

+153
-11
lines changed

ser2tcp/serial_proxy.py

Lines changed: 71 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import fnmatch as _fnmatch
44
import logging as _logging
5+
import socket as _socket
6+
import threading as _threading
57

68
import serial as _serial
79
import serial.tools.list_ports as _list_ports
@@ -35,6 +37,10 @@ class SerialProxy():
3537
def __init__(self, config, log=None):
3638
self._log = log if log else _logging.Logger(self.__class__.__name__)
3739
self._serial = None
40+
self._reader_thread = None
41+
self._reader_sock_r = None
42+
self._reader_sock_w = None
43+
self._reader_running = False
3844
self._servers = []
3945
self._serial_config = self._init_serial_config(config['serial'])
4046
self._match = self._serial_config.pop('match', None)
@@ -103,6 +109,44 @@ def _port_matches(self, port_info, match):
103109
def __del__(self):
104110
self.close()
105111

112+
def _start_reader_thread_if_needed(self):
113+
"""Start reader thread if serial port doesn't support fileno()"""
114+
try:
115+
self._serial.fileno()
116+
except OSError:
117+
self._start_reader_thread()
118+
119+
def _serial_reader_run(self):
120+
"""Reader thread: read from serial, forward to socketpair"""
121+
while self._reader_running:
122+
try:
123+
data = self._serial.read(size=max(1, self._serial.in_waiting))
124+
if data:
125+
self._reader_sock_w.sendall(data)
126+
except (OSError, _serial.SerialException):
127+
break
128+
129+
def _start_reader_thread(self):
130+
"""Start reader thread with socketpair for select() compatibility"""
131+
self._reader_sock_r, self._reader_sock_w = _socket.socketpair()
132+
self._reader_running = True
133+
self._reader_thread = _threading.Thread(
134+
target=self._serial_reader_run, daemon=True)
135+
self._reader_thread.start()
136+
self._log.debug("Serial reader thread started")
137+
138+
def _stop_reader_thread(self):
139+
"""Stop reader thread and close socketpair"""
140+
if self._reader_thread is None:
141+
return
142+
self._reader_running = False
143+
self._reader_thread.join(timeout=2)
144+
self._reader_sock_r.close()
145+
self._reader_sock_w.close()
146+
self._reader_thread = None
147+
self._reader_sock_r = None
148+
self._reader_sock_w = None
149+
106150
def connect(self):
107151
"""Connect to serial port"""
108152
if not self._serial:
@@ -120,6 +164,7 @@ def connect(self):
120164
return False
121165
self._log.info(
122166
"Serial %s connected", self._serial_config['port'])
167+
self._start_reader_thread_if_needed()
123168
return True
124169

125170
def has_connections(self):
@@ -132,6 +177,7 @@ def has_connections(self):
132177
def disconnect(self):
133178
"""Disconnect serial port, but if there are no active connections"""
134179
if self._serial and not self.has_connections():
180+
self._stop_reader_thread()
135181
self._serial.close()
136182
self._serial = None
137183
self._log.info(
@@ -149,7 +195,10 @@ def read_sockets(self):
149195
for server in self._servers:
150196
sockets += server.read_sockets()
151197
if self._serial:
152-
sockets.append(self._serial)
198+
if self._reader_sock_r:
199+
sockets.append(self._reader_sock_r)
200+
else:
201+
sockets.append(self._serial)
153202
return sockets
154203

155204
def write_sockets(self):
@@ -164,20 +213,31 @@ def send_to_connections(self, data):
164213
for server in self._servers:
165214
server.send(data)
166215

216+
def _process_serial_data(self):
217+
"""Read and forward serial data to connections"""
218+
try:
219+
if self._reader_sock_r:
220+
data = self._reader_sock_r.recv(4096)
221+
else:
222+
data = self._serial.read(size=self._serial.in_waiting)
223+
if data:
224+
self._log.debug("(%s): %s", self._serial_config['port'], data)
225+
self.send_to_connections(data)
226+
else:
227+
raise OSError("Serial reader closed")
228+
except (OSError, _serial.SerialException) as err:
229+
self._log.warning(err)
230+
for server in self._servers:
231+
server.close_connections()
232+
self.disconnect()
233+
167234
def process_read(self, read_sockets):
168235
"""Process sockets with read event"""
169236
for server in self._servers:
170237
server.process_read(read_sockets)
171-
if self._serial and self._serial in read_sockets:
172-
try:
173-
data = self._serial.read(size=self._serial.in_waiting)
174-
self._log.debug("(%s): %s", self._serial_config['port'], data)
175-
self.send_to_connections(data)
176-
except (OSError, _serial.SerialException) as err:
177-
self._log.warning(err)
178-
for server in self._servers:
179-
server.close_connections()
180-
self.disconnect()
238+
serial_sock = self._reader_sock_r or self._serial
239+
if self._serial and serial_sock in read_sockets:
240+
self._process_serial_data()
181241

182242
def process_write(self, write_sockets):
183243
"""Process sockets with write event"""

tests/test_serial_proxy.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ def _mock_init(self, config=None, log=None):
1212
"""Mock init that sets required attributes for __del__"""
1313
self._servers = []
1414
self._serial = None
15+
self._reader_thread = None
16+
self._reader_sock_r = None
17+
self._reader_sock_w = None
18+
self._reader_running = False
1519

1620

1721
def _make_port_info(device, vid=None, pid=None, serial_number=None,
@@ -223,5 +227,83 @@ def test_config_with_match_preserved(self):
223227
self.assertEqual(result['match'], {'vid': '0x303A'})
224228

225229

230+
class TestSerialReaderThread(unittest.TestCase):
231+
"""Test reader thread for platforms without fileno() support"""
232+
233+
def _make_proxy(self):
234+
proxy = SerialProxy.__new__(SerialProxy)
235+
_mock_init(proxy)
236+
proxy._log = MagicMock()
237+
proxy._serial_config = {'port': '/dev/ttyUSB0'}
238+
return proxy
239+
240+
def test_fileno_supported_no_thread(self):
241+
"""No reader thread when fileno() works"""
242+
proxy = self._make_proxy()
243+
proxy._serial = MagicMock()
244+
proxy._serial.fileno.return_value = 3
245+
proxy._start_reader_thread_if_needed()
246+
self.assertIsNone(proxy._reader_thread)
247+
248+
def test_fileno_not_supported_starts_thread(self):
249+
"""Reader thread started when fileno() raises OSError"""
250+
proxy = self._make_proxy()
251+
proxy._serial = MagicMock()
252+
proxy._serial.in_waiting = 0
253+
proxy._serial.fileno.side_effect = OSError("fileno")
254+
proxy._serial.read.side_effect = OSError("closed")
255+
proxy._start_reader_thread_if_needed()
256+
self.assertIsNotNone(proxy._reader_thread)
257+
proxy._stop_reader_thread()
258+
259+
def test_start_stop_reader_thread(self):
260+
"""Reader thread starts and stops cleanly"""
261+
proxy = self._make_proxy()
262+
proxy._serial = MagicMock()
263+
proxy._serial.in_waiting = 0
264+
proxy._serial.read.side_effect = OSError("closed")
265+
proxy._start_reader_thread()
266+
self.assertIsNotNone(proxy._reader_thread)
267+
self.assertIsNotNone(proxy._reader_sock_r)
268+
self.assertIsNotNone(proxy._reader_sock_w)
269+
self.assertTrue(proxy._reader_running)
270+
proxy._stop_reader_thread()
271+
self.assertIsNone(proxy._reader_thread)
272+
self.assertIsNone(proxy._reader_sock_r)
273+
self.assertIsNone(proxy._reader_sock_w)
274+
self.assertFalse(proxy._reader_running)
275+
276+
def test_reader_thread_forwards_data(self):
277+
"""Reader thread forwards serial data through socketpair"""
278+
proxy = self._make_proxy()
279+
proxy._serial = MagicMock()
280+
proxy._serial.in_waiting = 5
281+
proxy._serial.read.side_effect = [b'hello', OSError("closed")]
282+
proxy._start_reader_thread()
283+
proxy._reader_thread.join(timeout=2)
284+
data = proxy._reader_sock_r.recv(4096)
285+
self.assertEqual(data, b'hello')
286+
proxy._stop_reader_thread()
287+
288+
def test_read_sockets_uses_socketpair(self):
289+
"""read_sockets() returns socketpair when reader thread is active"""
290+
proxy = self._make_proxy()
291+
proxy._serial = MagicMock()
292+
proxy._serial.in_waiting = 0
293+
proxy._serial.read.side_effect = OSError("closed")
294+
proxy._start_reader_thread()
295+
sockets = proxy.read_sockets()
296+
self.assertIn(proxy._reader_sock_r, sockets)
297+
self.assertNotIn(proxy._serial, sockets)
298+
proxy._stop_reader_thread()
299+
300+
def test_read_sockets_uses_serial_directly(self):
301+
"""read_sockets() returns serial when no reader thread"""
302+
proxy = self._make_proxy()
303+
proxy._serial = MagicMock()
304+
sockets = proxy.read_sockets()
305+
self.assertIn(proxy._serial, sockets)
306+
307+
226308
if __name__ == "__main__":
227309
unittest.main()

0 commit comments

Comments
 (0)