Simultaneously Listening to Two Serial Ports
I'm slowly understanding the data flowing between main board and control panel of a Canon Pixma MX340 multi-function inkjet. It's small enough to be tractable for my skill level, but too complex to be practical on an oscilloscope screen or logic analyzer timeline view. Going beyond those instruments, I've decided to tackle this challenge by writing a data filter program on my computer, using two USB serial adapters to hear both sides of the conversation.
I started with a single USB serial adapter to listen to the traffic from control panel to main board, which was successful enough for me to quickly learn all button matrix status reports (scan codes). I quickly learned adding a second serial port will more than double the complexity of my program. When I'm only listening to one port, I could make a blocking call to read()
and let it wait for the next byte of data to arrive. But if I block waiting for data to come in on one port, data might arrive on the other port and I wouldn't know until I get around to calling read()
on that other port.
One approach is to create another unit of execution. Whether it be another thread, process, etc. One per serial port and they can each block on their respective calls to read()
. Whichever one gets data first gets to execute. There are a few problems with this approach. The first is Python's historically poor support for multi-threading, leaving a legacy of tricky gotchas that I don't want to spend time to learn right now. The second problem is when I have two independent units of execution it will take work to coordinate between them. For example, if I want to link main board commands with the matching 0x20 sent by control panel as acknowledgement. They're solvable problems, but not the next one: I have ambition to create a microcontroller project to reuse this control panel in the future, so I want to work on logic that can conceivably be ported to a microcontroller. While FreeRTOS running on ESP32 has concept of tasks, ATmega328 Arduino has no such counterpart.
Due to those concerns, I will first try an alternate approach. Check to see if data is available before I commit to a serial read operation. If so, read only what's already available for processing. This allows my code to rapidly cycle through all my serial ports checking for available data. And if found, process only the amount available in order to avoid blocking execution any longer than I have to. This pattern is bad for modern computers because polling prevents dropping the big CPU to a low power state, but is common for microcontrollers.
If I want to eventually port this code, though, I should at least make sure it's theoretically possible. I found good news there. The ability to check serial data availability in a non-blocking manner seems to be pretty common across different serial data APIs.
- Python pySerial:
serial.Serial.in_waiting
- Arduino:
Serial.available()
- Espressif ESP-IDF:
uart_get_buffered_data_len()
Looks promising enough for a test drive.
This teardown ran far longer than I originally thought it would. Click here to rewind back to where this adventure started.