Home > Mobile >  How to forward serial port data to a multiclient socket server in Python?
How to forward serial port data to a multiclient socket server in Python?

Time:12-07

I would like to forward data captured on one serial port to a multiclient TCP Server. In short, I need a serial to TCPIP bridge.

import sys
import socket
from threading import Thread
import serial
import serial.threaded


class SerialToNet(serial.threaded.Protocol):
    """serial->socket"""

    def __init__(self):
        self.sockets: list[socket.socket] = []

    def __call__(self):
        return self

    def data_received(self, data):
        """Forward data from Serial to IP client Sockets"""
        for socket in self.sockets:
            socket.sendall(data)


class NetToSerial(Thread):
    """socket->serial"""

    serial_worker: serial.threaded.ReaderThread

    def __init__(self, client_socket):
        Thread.__init__(self)
        self._socket = client_socket

    def run(self):
        try:
            while True:
                data = self._socket.recv(1024)
                serial_worker.write(data)
        except (ConnectionAbortedError, ConnectionResetError):
            print("NetToSerial client disconnection")
            return


if __name__ == "__main__":

    # Serial connection
    SERIAL_COM_PORT = 'COM9'
    try:
        ser = serial.Serial(SERIAL_COM_PORT, 115200, timeout=2)
    except serial.SerialException:
        sys.exit(f"Serial port {SERIAL_COM_PORT} it not available")

    serial_to_net = SerialToNet()
    serial_worker = serial.threaded.ReaderThread(ser, serial_to_net)
    serial_worker.start()

    # TCP Server
    # :todo Use socketserver.TCPServer
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server_socket.bind(('', 3490))
    mythreads = []

    try:
        # Wait new IP clients
        while True:
            server_socket.listen()
            print("Server: waiting TCP client connection")
            (client_socket, _) = server_socket.accept()

            # New client
            net_to_serial_thread = NetToSerial(client_socket)
            net_to_serial_thread.serial_worker = serial_worker
            serial_to_net.sockets.append(client_socket)

            net_to_serial_thread.start()
            mythreads.append(net_to_serial_thread)
    except KeyboardInterrupt:
        pass

    for t in mythreads:
        t.join()

This implementation is quite working but I don't known how to update sockets in SerialToNet class when a TCP client disconnect.

CodePudding user response:

You need to implement some logic for when a network client disconnects.

You know a client has disconnected because you receive an empty response (b'') from the socket. You're receiving data from network clients in NetToSerial, here:

    def run(self):
        try:
            while True:
                data = self._socket.recv(1024)
                serial_worker.write(data)
        except (ConnectionAbortedError, ConnectionResetError):
            print("NetToSerial client disconnection")
            return

You need to check the value of data, and if it's empty implement your disconnect logic:

  • Close the associated socket.
  • Exit the thread.

That might look like:

class NetToSerial(Thread):
    """socket->serial"""

    serial_worker: serial.threaded.ReaderThread

    def __init__(self, client_socket):
        Thread.__init__(self)
        self._socket = client_socket

    def run(self):
        try:
            while True:
                data = self._socket.recv(1024)
                if not data:
                    break
                serial_worker.write(data)
        except (ConnectionAbortedError, ConnectionResetError):
            print("NetToSerial client disconnection")
            return
        finally:
            self._socket.close()

But that's only half the solution, because you're writing to this socket in your SerialToNet class. You need to remove the socket from SerialToNet sockets array. You can have the class remove the socket in response to an exception when writing, like this:

class SerialToNet(serial.threaded.Protocol):
    """serial->socket"""

    def __init__(self):
        self.sockets: list[socket.socket] = []

    def __call__(self):
        return self

    def data_received(self, data):
        """Forward data from Serial to IP client Sockets"""
        for socket in self.sockets[:]:
            try:
                socket.sendall(data)
            except OSError:
                self.sockets.remove(socket)

Note that because it's not possible to remove an item from a list over which you're currently iterating, we are iterating over a copy of self.sockets in the above code. This means we're free to remove sockets from self.sockets from inside the loop.


With the above changes I believe your code will operate as you intend.

Not directly related to your question, but I'd like to make a comment about your code: as written, it allows multiple network clients to write to the serial port at the same time. That seems like a recipe for disaster and I cannot think of any situation in which that would make sense. You may want to reconsider that aspect of your code.

  • Related