Skip to content

Add UART receive interrupt handler#147

Merged
ThomasAkam merged 4 commits intopyControl:devfrom
alustig3:UART-receive-handler
Aug 13, 2025
Merged

Add UART receive interrupt handler#147
ThomasAkam merged 4 commits intopyControl:devfrom
alustig3:UART-receive-handler

Conversation

@alustig3
Copy link
Contributor

@alustig3 alustig3 commented Apr 8, 2025

I have a custom syringe pump that uses UART serial messages to communicate with pyControl. It is sometimes useful to receive messages from the pump, for instance to tell the task that a syringe is empty, so the task will automatically stop.

Previously, to get this working would require constant polling the syringe pump UART to see if there were any messages available. Something like:

from pyControl.utility import *
from devices import PumpController, Breakout_dseries_1_6

bb = Breakout_dseries_1_6()  # breakout board
syringes = PumpController(port=bb.port_11)

states = ["state_A"]
events = ["check_for_message"]

initial_state = "state_A"


def run_start():
    set_timer("check_for_message", 200, output_event=False)


def all_states(event):
    if event == "check_for_message":
        set_timer("check_for_message", 200, output_event=False)
        msg = syringes.read_serial()
        if msg == "syringe_empty":
            stop_framework()
        else:
            print(msg)


def state_A(event):
    pass

This pull request adds a UART_handler class that can be attached to a UART's RX interrupt. Now a pyControl framework event will be output whenever a UART message is received. So the task code now looks something like this:

from pyControl.utility import *
from devices import PumpController, Breakout_dseries_1_6

bb = Breakout_dseries_1_6()  # breakout board
syringes = PumpController(port=bb.port_11, event_name="msg_received")

states = ["state_A"]
events = ["msg_received"]

initial_state = "mystate"


def all_states(event):
    if event == "msg_received":
        msg = syringes.read_serial()
        if msg == "syringe_empty":
            stop_framework()
        else:
            print(msg)


def state_A(event):
    pass

This gets rid of the constantly polling timer and greatly reduces response time.

Here is a look at my PumpController device class to see how it is implemented:

from machine import UART
from pyControl.hardware import UART_handler


class PumpController:
    def __init__(self, port, event_name, pybv1=False):
        assert port.UART is not None, "! Pump needs port with UART."

        self.uart = UART(port.UART)
        self.uart.init(115200, bits=8, parity=None, stop=1, timeout=100, rxbuf=130)

        handler = UART_handler(event_name, first_char_interrupt=pybv1)
        self.uart.irq(trigger=UART.IRQ_RXIDLE, handler=handler.ISR)

        # default pump settings
        self.configure_pump(
            steps_per_rev=200,
            syringe_id_mm=26.7,
            lead_mm=2,
            acceleration=5000,
            max_velocity=180_000,
        )
        self.uart.read()  # clear buffer
        self._send("reset")

        self._left = "l1"
        self._right = "r1"

    def _send(self, command, payload=""):
        self.uart.write(f"{command};{payload}\n")

    def configure_pump(
        self,
        steps_per_rev,
        syringe_id_mm,
        lead_mm,
        acceleration,
        max_velocity,
        pump="all",
    ):
        if pump == "all":
            for pump in ["l1", "l2", "r1", "r2"]:
                self._send(
                    "eval",
                    f"{pump}.configure({steps_per_rev}, {syringe_id_mm}, {lead_mm}, {acceleration}, {max_velocity})",
                )
        else:
            self._send(
                "eval", f"{pump}.configure({steps_per_rev}, {syringe_id_mm}, {lead_mm}, {acceleration}, {max_velocity})"
            )

    def display_animal_number(self, number):
        self._send("animal", number)

    def stop(self):
        self._send("stop")

    def infuse(self, pump, volume):
        self._send("eval", f"{pump}.dispense({volume})")

    def left_infuse(self, volume):
        self.infuse(self._left, volume)

    def right_infuse(self, volume):
        self.infuse(self._right, volume)

    def get_version(self):
        self._send("version")

    def read_serial(self):
        if self.uart.any():
            return self.uart.readline().decode("utf-8").strip("\n")
        return None

This improves the situation described in the docs

Polling may be necessary if you need to respond to events send over a serial connection (e.g. from an external device that communicates via UART). In this case either poll at the lowest frequency necessary to ensure an acceptable response latency to the serial input, or if possible trigger the serial read using a separate digital input from the external device

@alustig3
Copy link
Contributor Author

@ThomasAkam bumping this in case you missed it. No rush though.

@ThomasAkam
Copy link
Collaborator

Thanks Andy, and apologies for the delay in getting to this - I've been swamped the last month. This does look like useful functionallity but would it be possible to put it in a UART_handler device rather than in hardware.py? This would avoid increasing the size of the framework which is desirable on the pyboard v1.1 due to the limited memory resources.

@alustig3
Copy link
Contributor Author

No worries at all! Thanks for taking a look. I moved the code into a separate device file

@alustig3
Copy link
Contributor Author

alustig3 commented Aug 6, 2025

Hi @ThomasAkam, a friendly reminder to consider this and the other pending pull requests when you get a chance.

@ThomasAkam
Copy link
Collaborator

Thanks Andy, merging now. If you get time to add something to the hardware docs describing this functionallity that would be great. Also, the hardware.py file seems to be formatted slightly differently from how my Black formatter likes it.

Apologies again for the very belated response!

@ThomasAkam ThomasAkam merged commit 5768d9f into pyControl:dev Aug 13, 2025
@alustig3 alustig3 deleted the UART-receive-handler branch August 26, 2025 18:53
@alustig3 alustig3 mentioned this pull request Aug 26, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants