Search code examples
pythonwhile-loopserial-portpyserialkeyboardinterrupt

Better way to read input in While True loop


I'm working on some code to read serial data from an Arduino. I'd like to wait for a serial data to come in, give an input in the terminal, then continue waiting for the next serial input or for the user to exit loop.

def assign_values(name, serial_connection):
    print(f"Waiting for signal from {name}...")
    try:
        dict = {}
        while True:
            key = serial_connection.readline().decode("utf-8").strip()
            if key and key not in dict:
                value = input(f"Detected {key}: ")
                dict[key] = value
    except KeyboardInterrupt as k:
        return dict

The above code works but has some problems that I'd like to clean up. First off, if I give multiple serial inputs before a user input, they get backed up. I'd rather it just ignore anything coming in while the user is typing their own input. The same problem also happens in reverse. Any user input is tracked while waiting for the serial data and immedietly populates the terminal when calling input(). Also I feel like the try -> while True isn't the best way to do this, but I can't find any other way to achieve my goal.

In case it is relevant, here is the Arduino code:

#include <IRremote.h>
#define IR_RECEIVE_PIN 7

void setup() {
  Serial.begin(9600);
  IrReceiver.begin(IR_RECEIVE_PIN);
}

void loop() {
  if (IrReceiver.decode()) {
    IrReceiver.resume();
    int cmd = IrReceiver.decodedIRData.command;
    Serial.println(cmd);
  }
}

Solution

  • The serial inputs get backed up because they are in the buffer. So you will need to flush it and do a readline() to get only the latest input.

    def assign_values(name, serial_connection):
        print(f"Waiting for signal from {name}...")
        try:
            dict = {}
            while True:
                serial_connection.flush_input() # this won't work, see the update
                key = serial_connection.readline().decode("utf-8").strip()
                if key and key not in dict:
                    value = input(f"Detected {key}: ")
                    dict[key] = value
        except KeyboardInterrupt as k:
            return dict
    

    If you don't want to use try and while True to detect a keyboard interrupt to quit, then you could ask the user to type quit() to quit the program. Then do a check and break.

    value = input(f"Detected {key}: ")
    if value == "quit()":
        break
    

    Update

    1. Take only the latest serial input: It is actually flushInput() and since version 3.0 it has been renamed to reset_input_buffer() as pointed out by @Zachary Elkins.
    serial_connection.reset_input_buffer()
    key = serial_connection.readline().decode("utf-8").strip()
    
    1. Ignore user input before input(): You can try using msvcrt for windows or termios to flush the input as shown in this answer.
    2. Quit program without KeyboardInterrupt: With pynput you could attach a listener that listens for a particular key like escape, then quits the program as shown here. For example:
    from pynput import keyboard
    
    keep_running = True
    
    def flush_input():
        try:
            import msvcrt
            while msvcrt.kbhit():
                msvcrt.getch()
        except ImportError:
            import sys, termios    #for linux/unix
            termios.tcflush(sys.stdin, termios.TCIOFLUSH)
    
    def on_press(key):
        global keep_running
        if key == keyboard.Key.esc:
            keep_running = False
    listener =  keyboard.Listener(on_press=on_press)
    listener.start()
    
    def assign_values(name, serial_connection):
        print(f"Waiting for signal from {name}...")
        dict = {}
        while keep_running:
            serial_connection.reset_input_buffer()
            key = serial_connection.readline().decode("utf-8").strip()
            if key and key not in dict:
                flush_input()
                value = input(f"Detected {key}: ")
                dict[key] = value
        return dict