High-level abstraction for serial communication

123 Views Asked by At

On a hardware device with serial communication, I need to continously query "foo", forever, every second (for example to log temperature of the device). At a random timing, another thread may query "bar".

One option is to interrupt the "foo" queries, ask for "bar", and once done, re-start the "foo" queries again - but I'm looking for a more general solution.

Is there a way to make a high-level abstraction, allowing us to just do:

def thread1():
    while True:
        serial_device_abstraction.get("foo")
        time.sleep(1)

def thread2()
    time.sleep(random.random())
    serial_device_abstraction.get("bar")

threading.Thread(target=thread1).start()
threading.Thread(target=thread2).start()

and letting the high-level abstraction handle the low-level concurrency problems?

NB: obviously, with

def get(query):                      
   serial_port.write(query)          
   data = self.serial_port.read(8)   

it won't work, because if two threads send bits through the serial wire at the same time, it will send/receive corrupted data.

1

There are 1 best solutions below

0
sawdust On

On a hardware device with serial communication, I need to continously query "foo", forever, every second (for example to log temperature of the device). At a random timing, another thread may query "bar".

Presumably this is a strict request-response protocol. That is, the remote device, upon receiving a "foo" or "bar" query/request, is busy until it sends back its response. This remote device is assumed to be incapable of processing more than one request at a time. This is a common limitation of many simple master-slave connections.

Therefore the host or master must respect/enforce/implement this request-response protocol. The transmission of a request message must be followed by the reception of the reply (or a suitable timeout period). Another transmission of a (request) message must never commence until the completion of a request-response cycle.


A thread that implements this request-response protocol is one scheme that would abstract these requirements on the host/master side. Other threads can queue their query, and then wait for a reply from this thread.

This thread simply processes each queue entry by sending the attached request message, and then waits for a response from the remote device. (This "waiting" can be accomplished by simply using a blocking read syscall.)
The response from the remote device (or a timeout error) is then sent back to the waiting thread.

Any contention for the serial port is obscured by the queuing mechanism of this thread. The critical region is each thread's queue for receiving notices from other threads. The thread that implements this request-response protocol accepts notices to send a specific, attached query to the remote device. This notice would also include the address of the queue to send a notice containing the response (so that the waiting thread can resume).


Is there a way to make a high-level abstraction, allowing us to just do:
...
and letting the high-level abstraction handle the low-level concurrency problems?

Instead of a thread handling the protocol, an alternate but less elegant scheme could use a mutex and common code shared by requesting threads. Any and all usage of the file descriptor for the serial port/terminal must be controlled by its mutex. Assume that the file descriptor for the serial port/terminal and a mutex are global.

In psuedocode (I think this would suffice):

procedure serial_messaging(u8 *request_mesg, int rqlen, u8 *response_mesg, int rslen)
{
    int rc;

    acquire_the_mutex();    /* else calling thread is blocked */

    rc = write(serial_fd, request_mesg, rqlen);
    if (rc < 0)
        then handle error condition;
    tcdrain(serial_fd);         /* necessary only for measuring timeout */

    rc = read(serial_fd, response_mesg, rslen);   /* use blocking mode to wait */
    if (rc < 0)
        then handle error condition;

    release_the_mutex();    /* let another thread use serial port */
    return;   /* with received data in response_mesg */
}

You could then have threads issue "foo" or "bar" queries/requests as you previously suggested.


NB: obviously, with
...
it won't work, because if two threads send bits through the serial wire at the same time, it will send/receive corrupted data.

Incorrect, the output would not be co-mingled at the bit or byte level. Threads do not have direct access to the hardware, and only can only make syscalls to have the kernel driver perform the I/O. The I/O is not performed in synchronization with the any thread execution.

You mention "concurrency problems", but there is nothing here to perform any synchronization. You mention a scheme "to interrupt the "foo" queries", but neglect any synchronization to ensure that the thread is "interrupted" only while it is not using the serial port.

The real problem is that the above scheme does not enforce the request-response protocol, i.e. it does not wait for the completion of each write-read cycle. It does not prevent sending another query while the remote device is busy and unable to accept another query.

The solution to a "concurrency problem" is usually either
(a) enforced serialization (e.g. queueing), or
(b) synchronization to enforce exclusive access (e.g. using semaphores and/or mutexes).