autorope / donkeycar

Open source hardware and software platform to build a small scale self driving car.
http://www.donkeycar.com
MIT License
3.17k stars 1.3k forks source link

Extending support for PinProvider to include Pico #1196

Open DocGarbanzo opened 2 months ago

DocGarbanzo commented 2 months ago

Integration of Pi Pico into the Donkeycar pin ecosystem

Because PiGPIO is not supported on RPi 5 any longer we have to move the pin support for realtime pin tasks to the Pi Pico. This allows to migrate PWM input pins onto the Pico for using an RC controller to drive the car, as well as using the Pico's PWM pins to provide output pins for the servo and ESC. Other pins that required realtime signals like odometery or IR receivers can also be migrated to the Pico because we also support a PulseIn pin. In addition we aim to add support for the 4 analog pins to the pico, which for example could be used to monitor the battery voltage.

Changes in the pins module

The following new pins will be added:

class InputPinPico
class OutputPinPico
class InputPwmPinPico
class PwmPinPico
class PulseInPico
class AnalogInPico

The pico serial communicator

The following new class will be added. This is not a DonkeyCar part, but a low-level serial communication link between the above mentioned pins and the Pico board, connected to the RPi. It will send serial data as fast as possible back and forth between the Pico and the internal pin cache of the car, such that signals can be transferred at much higher rates than the car frequency. This class runs the serial connection in its own thread.

class Pico:
    """
    Pi Pico class to communicate with the Pico over usb. We use the same usb
    cable for power and data. The Pico is connected to the computer and can
    handle file copy / repl on /dev/ttyACM0 while doing bidirectional data
    transfer on /dev/ttyACM1.

    To set up the pins on the pico, we need to send a configuration dictionary.
    An example is here:

    pin_configuration = {
        'input_pins': {
            'GP13': dict(mode='INPUT', pull_up=False),
            'GP18': dict(mode='PULSE_IN', maxlen=8, auto_clear=True),
            'GP16': dict(mode='PULSE_IN', maxlen=4),
            'GP17': dict(mode='PWM_IN', duty=0.09),
            'GP28': dict(mode='ANALOG_IN'),
        },
        'output_pins': {
            'GP14': dict(mode='OUTPUT', value=0),
            'GP15': dict(mode='PWM', frequency=60, duty_cycle=0.06),
        },
    }

    The dictionary is required to have either 'input_pins' or 'output_pins'
    or both as keys. Input and output refers to the pins on the pico.

    The values of 'input_pins' and 'output_pins' contain dictionaries with
    the pin name as key and the pin configuration as value. The pin
    configuration is a dictionary with key-values depending on the mode of
    the pin. The 'mode' key is required for all pins. The different supported 
    input modes are:
        'INPUT': for digital input
        'PULSE_IN': for pulse input
        'PWM_IN': for pwm input from an RC controller
        'ANALOG_IN': for analog input
    The different output modes are:
        'OUTPUT': for digital output
        'PWM': for pulse width modulation output
    See above examples for the required keys for each mode.
    """

    def __init__(self, port: str = '/dev/ttyACM1'):
        """
        Initialize the Pico communicator.
        :param port: port for data connection
        """
        self.serial = serial.Serial(port, 115200)
        self.counter = 0
        self.running = True
        self.pin_configuration = dict()
        self.send_dict = dict()
        self.receive_dict = dict()
        self.lock = Lock()
        self.start = None
        logger.info(f"Pico created on port: {port}")
        # send the initial setup dictionary to clear all pins
        pack = json.dumps(dict(input_pins={}, output_pins={})) + '\n'
        self.serial.write(pack.encode())
        self.t = Thread(target=self.loop, args=(), daemon=True)
        self.t.start()

    def loop(self):
        """
        Donkey parts interface. We are sending newline delimited json strings
        and expect the same in return.
        """
        # clear the input buffer
        self.serial.reset_input_buffer()
        self.start = time.time()
        # start loop of continuous communication
        while self.running:
            try:
                pack = None
                with self.lock:
                    pack = json.dumps(self.send_dict) + '\n'
                self.serial.write(pack.encode())
                time.sleep(0)
                bytes_in = self.serial.read_until()
                time.sleep(0)
                str_in = bytes_in.decode()[:-1]
                received_dict = json.loads(str_in)
                with self.lock:
                    self.receive_dict.update(received_dict)
                if self.counter % 1000 == 0:
                    logger.debug(f'Last sent: {pack}')
                    logger.debug(f'Last received: {str_in}')
            except ValueError as e:
                logger.error(f'Failed to load json in loop {self.counter} '
                             f'because of {e}. Expected json, but got: '
                             f'+++{str_in}+++')
            except Exception as e:
                logger.error(f'Problem with serial input {e}')
            self.counter += 1
        logger.info('Pico loop stopped.')

    def write(self, gpio: str, value: float or int) -> None:
        """
        :param gpio:    the gpio pin to write to
        :param value:   the value to write
        """
        # Wait until threaded loop has at least run once, so we don't have to)
        # process None values. This blocks until the first data is received.
        while self.counter == 0:
            time.sleep(0.1)
        assert gpio in self.send_dict, f"Pin {gpio} not in send_dict."
        with self.lock:
            self.send_dict[gpio] = value

    def read(self, gpio):
        """
        :param gpio:    the gpio pin to read from
        :return:        the value of the pin
        """
        # Wait until threaded loop has at least run once, so we don't have to
        # process None values. This blocks until the first data is received.
        while self.counter == 0:
            time.sleep(0.1)
        with self.lock:
            if gpio not in self.receive_dict:
                msg = (f"Pin {gpio} not in receive_dict. Known pins: "
                       f"{', '.join(self.receive_dict.keys())}")
                logger.error(msg)
                raise RuntimeError(msg)
            return self.receive_dict[gpio]

    def stop(self):
        logger.info("Stopping Pico communication.")
        self.running = False
        time.sleep(0.1)
        self.t.join()
        logger.info("Pico communication stopped.")
        self.serial.close()
        total_time = time.time() - self.start
        logger.info(f"Pico communication disconnected, ran {self.counter} "
                    f"loops, each loop taking "
                    f"{total_time * 1000 / self.counter:5.1f} ms.")

    def setup_input_pin(self, gpio: str, mode: str, **kwargs) -> None:
        """
        :param gpio:    the gpio pin to set up
        :param mode:    the mode of the pin
        :param kwargs:  additional arguments for the mode
        """
        assert mode in ('INPUT', 'PULSE_IN', 'ANALOG_IN', 'PWM_IN'), \
            f"Mode {mode} not supported for input pins."

        setup_dict = dict(input_pins={gpio: dict(mode=mode, **kwargs)})
        logger.info(f"Setting up input pin {gpio} in mode {mode} using "
                    f"setup dict {setup_dict}")
        with self.lock:
            # send the setup dictionary
            pack = json.dumps(setup_dict) + '\n'
            self.serial.write(pack.encode())
        self.receive_dict[gpio] = 0

    def setup_output_pin(self, gpio: str, mode: str, **kwargs) -> None:
        """
        :param gpio:    the gpio pin to set up
        :param mode:    the mode of the pin
        :param kwargs:  additional arguments for the mode
        """

        assert mode in ('OUTPUT', 'PWM'), \
            f"Mode {mode} not supported for output pins on Pico"
        setup_dict = dict(output_pins={gpio: dict(mode=mode, **kwargs)})
        logger.info(f"Setting up output pin {gpio} in mode {mode} using "
                    f"setup dict {setup_dict}")
        with self.lock:
            # send the setup dictionary
            pack = json.dumps(setup_dict) + '\n'
            self.serial.write(pack.encode())
        self.send_dict[gpio] = 0 if mode == 'OUTPUT' else kwargs['duty']

    def remove_pin(self, gpio: str) -> None:
        """
        :param gpio:    the gpio pin to remove
        """
        setup_dict = dict()
        logger.info(f"Removing pin {gpio}")
        if gpio in self.receive_dict:
            setup_dict['input_pins'] = {gpio: {}}
            del self.receive_dict[gpio]
        elif gpio in self.send_dict:
            setup_dict['output_pins'] = {gpio: {}}
            del self.send_dict[gpio]
        else:
            logger.warning(f"Pin {gpio} not in send or receive dict.")
            return
        with self.lock:
            # send the setup dictionary
            pack = json.dumps(setup_dict) + '\n'
            self.serial.write(pack.encode())

Circuitpython code and usage for the Pico

Circuitpython code to be installed on the Pico by copying files. Note, this has to be done only once. All pin creations and deletions will then be handled in software only. The boot.py file enables the second usb channel for communication. That channel will usually show up on the Pi under /dev/ttyACM1. The shell for accessing the REPL or the print command outputs can be accessed under /dev/ttyACM0.

# file boot.py
import usb_cdc
usb_cdc.enable(console=True, data=True)
# file code.py
import time
import board
import digitalio
import analogio
import pulseio
import pwmio
import usb_cdc
import json
import microcontroller

class PulseInResettable:
    def __init__(self, gpio, maxlen=2, auto_clear=False, **kwargs):
        self.pin = pulseio.PulseIn(pin=gpio, maxlen=maxlen, **kwargs)
        self.auto_clear = auto_clear
        self.pin.clear()

    def get_readings(self):
        l = len(self.pin)
        res = list(self.pin[i] for i in range(l))
        if self.auto_clear:
            self.pin.clear()
        return res

    def deinit(self):
        self.pin.deinit()

class PWMIn(PulseInResettable):
    def __init__(self, gpio, frequency=60, duty=0.09, min_us=1000,
                 max_us=2000, **kwargs):
        super().__init__(gpio, maxlen=2, auto_clear=False, **kwargs)
        self.duty = duty
        self.min_us = min_us
        self.max_us = max_us
        self.frequency = frequency

    def get_readings(self):
        """
        Get the duty cycle from the last two readings. Assuming min and max
        us are 1000 and 2000, respectively,
        """
        r = super().get_readings()
        if len(r) > 1:
            duty_us = min(r[-2], r[-1])
            # High signals should be between min_us and max_us. As we
            # occasionally see duplicated readings like [1000, 1000] instead
            # of [15666, 1000] we ignore readings which are out by more than
            # 10% of the min_us or max_us.

            if duty_us < 0.9 * self.min_us or duty_us > 1.1 * self.max_us:
                return self.duty
            self.duty = duty_us * self.frequency * 1e-6
        return self.duty

class PWMOut:
    def __init__(self, gpio, frequency=60, duty_cycle=0.09, **kwargs):
        self.pin = pwmio.PWMOut(pin=gpio, frequency=frequency, **kwargs)
        self.pin.duty_cycle = int(duty_cycle * 65535)

    def deinit(self):
        self.pin.deinit()

    def set_duty_cycle(self, value):
        """ Set the duty cycle of the PWM output. """
        self.pin.duty_cycle = int(value * 65535)

def bytes_to_dict(byte_data, count):
    if byte_data == b'':
        return {}
    str_in = byte_data.decode()[:-1]
    if not str_in:
        return {}
    try:
        out_dict = json.loads(str_in)
        return out_dict
    except ValueError as e:
        print(f'Failed to decode JSON because of {e}',
              f'from {str_in} in loop {count}.')
    return {}

def dict_to_bytes(dict_data):
    str_out = json.dumps(dict_data) + '\n'
    byte_out = str_out.encode()
    return byte_out

def pin_from_dict(pin_name, d):
    print(f'Creating pin {pin_name} from dict: {d}')
    # convert from pin_name string to board pin object
    gpio = getattr(board, pin_name)
    assert gpio != board.LED, 'Cannot assign LED pin as input or output.'
    pin = None
    if d['mode'] == 'INPUT':
        pin = digitalio.DigitalInOut(gpio)
        pin.direction = digitalio.Direction.INPUT
        pull = d.get('pull')  # None will work here, otherwise map
        if pull == 1:
            pull = digitalio.Pull.UP
        elif pull == 2:
            pull = digitalio.Pull.DOWN
        pin.pull = pull
        print(f'Configured digital input pin, gpio: {gpio}, pull: {pin.pull}')
    elif d['mode'] == 'PULSE_IN':
        pin = PulseInResettable(gpio, maxlen=d.get('maxlen', 2),
                                auto_clear=d.get('auto_clear', False))
        print(f'Configured pulse-in pin, gpio: {gpio}, maxlen:',
              f'{pin.pin.maxlen}, auto_clear: {pin.auto_clear}')
    elif d['mode'] == 'PWM_IN':
        pin = PWMIn(gpio, duty=d.get('duty_center', 0.09))
        print(f'Configured pwm-in pin, gpio: {gpio}, duty_center: {pin.duty}')
    elif d['mode'] == 'ANALOG_IN':
        pin = analogio.AnalogIn(gpio)
        print(f'Configured analog input pin, gpio: {gpio}')
    elif d['mode'] == 'OUTPUT':
        pin = digitalio.DigitalInOut(gpio)
        pin.direction = digitalio.Direction.OUTPUT
        pin.value = False
        print(f'Configured digital output pin, gpio: {gpio},',
              f'value: {pin.value}')
    elif d['mode'] == 'PWM':
        duty_cycle = d.get('duty_cycle', 0.09)
        freq = int(d.get('frequency', 60))
        pin = PWMOut(gpio, frequency=freq, duty_cycle=duty_cycle)
        print(f'Configured pwm output pin, gpio: {gpio},',
              f'frequency: {pin.pin.frequency},',
              f'duty_cycle: {pin.pin.duty_cycle / 65535}')
    return pin

def deinit_pin(pin):
    try:
        pin.deinit()
        print(f'De-initialise pin: {pin}')
    except AttributeError as e:
        print(f'Pin has no deinit method: {e}')
    except Exception as e:
        print(f'Pin deinit failed: {e}')

def reset_all_pins(input_pins, output_pins):
    for pin in (input_pins | output_pins).values():
        deinit_pin(pin)
    input_pins.clear()
    output_pins.clear()
    print(f'Reset all pins.')

def setup(setup_dict, input_pins, output_pins):
    if not setup_dict:
        return False
    # if both input_pins and output_pins are empty, we are clearing all pins
    print(f'Starting setup -------->')
    print(f'Received setup dict: {setup_dict}')
    in_dict, out_dict = tuple(setup_dict.get(key, {})
                              for key in ['input_pins', 'output_pins'])
    if len(in_dict) == 0 and len(out_dict) == 0:
        reset_all_pins(input_pins, output_pins)
    # merge pins from setup dict into input_pins and output_pins
    t_list = zip((in_dict, out_dict), (input_pins, output_pins))
    for setup_io_dict, pins in t_list:
        for pin_name, pin_dict in setup_io_dict.items():
            if pin_name in pins:
                deinit_pin(pins[pin_name])
                if len(pin_dict) == 0:
                    print(f'Removing pin {pin_name}')
                    del pins[pin_name]
                    continue
                else:
                    print(f'Overwriting {pin_name}')
            try:
                pins[pin_name] = pin_from_dict(pin_name, pin_dict)
            except Exception as e:
                print(f'Setup of {pin_name} failed because of {e}.')
                print(f'Finished setup unexpectedly <--------')
                return False

    print(f'Updated input pins: {input_pins}')
    print(f'Updated output pins: {output_pins}')
    print(f'Finished setup <--------')
    return True

def update_output_pins(output_data, output_pins):
    for pin_name, value in output_data.items():
        out_pin = output_pins.get(pin_name)
        try:
            if isinstance(out_pin, digitalio.DigitalInOut):
                out_pin.value = bool(value)
            elif isinstance(out_pin, PWMOut):
                out_pin.set_duty_cycle(value)
            else:
                print(f'Cannot update pin out_pin: {pin_name} \
                      because of unknown type {type(out_pin)}.')
        except ValueError as e:
            print(f'Failed update output pin {pin_name} because of {e}')

def read(serial, input_pins, output_pins, led, is_setup, count):
    if serial.in_waiting > 0:
        led.value = True
        bytes_in = serial.readline()
        #serial.reset_input_buffer()
        read_dict = bytes_to_dict(bytes_in, count)
        # if setup dict sent, this contains 'input_pins' or 'output_pins'
        if 'input_pins' in read_dict or 'output_pins' in read_dict:
            is_setup = setup(read_dict, input_pins, output_pins)
        # only call update_output_pins if setup has been done
        elif is_setup:
            update_output_pins(read_dict, output_pins)
    else:
        led.value = False
    return is_setup

def write(serial, input_pins, write_dict):
    """ Return list if no error or return error as string"""
    for name, pin in input_pins.items():
        if type(pin) in (digitalio.DigitalInOut, analogio.AnalogIn):
            write_dict[name] = pin.value
        elif type(pin) in (PulseInResettable, PWMIn):
            write_dict[name] = pin.get_readings()

    byte_out = dict_to_bytes(write_dict)
    n = serial.write(byte_out)
    return n

def main():
    print('\n************ Starting pi pico ************')
    microcontroller.cpu.frequency = 180000000
    print(f'Current CPU frequency: {microcontroller.cpu.frequency}')

    serial = usb_cdc.data
    serial.timeout = 0.01
    serial.reset_input_buffer()
    led = digitalio.DigitalInOut(board.LED)
    led.direction = digitalio.Direction.OUTPUT
    led.value = False

    input_pins = {}
    output_pins = {}
    write_dict = {}
    is_setup = False
    count = 0
    tic = time.monotonic()
    total_time = 0
    try:
        while True:
            # reading input
            is_setup = read(serial, input_pins, output_pins, led,
                            is_setup, count)
            # sending output, catching number of bytes written
            if is_setup:
                n = write(serial, input_pins, write_dict)
            toc = time.monotonic()
            total_time += toc - tic
            tic = toc
            count += 1
    except KeyboardInterrupt:
        led.value = False

if __name__ == '__main__':
    main()

Running on the Pico

Here is a shell output from running above code on the Pico:

************ Starting pi pico ************
Current CPU frequency: 180000000
Starting setup -------->
Received setup dict: {'input_pins': {}, 'output_pins': {}}
Reset all pins.
Updated input pins: {}
Updated output pins: {}
Finished setup <--------
Starting setup -------->
Received setup dict: {'input_pins': {'GP19': {'mode': 'PWM_IN', 'duty': 0.09}}}
Creating pin GP19 from dict: {'mode': 'PWM_IN', 'duty': 0.09}
Configured pwm-in pin, gpio: board.GP19, duty_center: 0.09
Updated input pins: {'GP19': <PWMIn object at 0x2000ef70>}
Updated output pins: {}
Finished setup <--------
Starting setup -------->
Received setup dict: {'input_pins': {'GP19': {}}}
De-initialise pin: <PWMIn object at 0x2000ef70>
Removing pin GP19
Updated input pins: {}
Updated output pins: {}
Finished setup <--------

As you can see, this was a simple session where only a single PWM pin was created and deleted. Deletion happens by calling stop() on the pin. Here is the output of another session where multiple pins were created and deleted. This is the output created by running the pins.py script multiple times with different arguments creating different pins.

************ Starting pi pico ************
Current CPU frequency: 180000000
Starting setup -------->
Received setup dict: {'output_pins': {'GP1': {'mode': 'OUTPUT'}}}
Creating pin GP1 from dict: {'mode': 'OUTPUT'}
Configured digital output pin, gpio: board.GP1, value: False
Updated input pins: {}
Updated output pins: {'GP1': <DigitalInOut>}
Finished setup <--------
Starting setup -------->
Received setup dict: {'input_pins': {'GP0': {'mode': 'INPUT', 'pull': 1}}}
Creating pin GP0 from dict: {'mode': 'INPUT', 'pull': 1}
Configured digital input pin, gpio: board.GP0, pull: digitalio.Pull.UP
Updated input pins: {'GP0': <DigitalInOut>}
Updated output pins: {'GP1': <DigitalInOut>}
Finished setup <--------
Starting setup -------->
Received setup dict: {'input_pins': {'GP0': {}}}
De-initialise pin: <DigitalInOut>
Removing pin GP0
Updated input pins: {}
Updated output pins: {'GP1': <DigitalInOut>}
Finished setup <--------
Starting setup -------->
Received setup dict: {'input_pins': {'GP0': {'mode': 'INPUT', 'pull': 1}}}
Creating pin GP0 from dict: {'mode': 'INPUT', 'pull': 1}
Configured digital input pin, gpio: board.GP0, pull: digitalio.Pull.UP
Updated input pins: {'GP0': <DigitalInOut>}
Updated output pins: {'GP1': <DigitalInOut>}
Finished setup <--------
Starting setup -------->
Received setup dict: {'input_pins': {'GP0': {}}}
De-initialise pin: <DigitalInOut>
Removing pin GP0
Updated input pins: {}
Updated output pins: {'GP1': <DigitalInOut>}
Finished setup <--------
Starting setup -------->
Received setup dict: {'output_pins': {'GP1': {}}}
De-initialise pin: <DigitalInOut>
Removing pin GP1
Updated input pins: {}
Updated output pins: {}
Finished setup <--------
Starting setup -------->
Received setup dict: {'input_pins': {'GP18': {'mode': 'PWM_IN', 'duty': 0.09}}}
Creating pin GP18 from dict: {'mode': 'PWM_IN', 'duty': 0.09}
Configured pwm-in pin, gpio: board.GP18, duty_center: 0.09
Updated input pins: {'GP18': <PWMIn object at 0x200108d0>}
Updated output pins: {}
Finished setup <--------
Starting setup -------->
Received setup dict: {'input_pins': {'GP18': {}}}
De-initialise pin: <PWMIn object at 0x200108d0>
Removing pin GP18
Updated input pins: {}
Updated output pins: {}
Finished setup <--------
Starting setup -------->
Received setup dict: {'input_pins': {'GP19': {'mode': 'PWM_IN', 'duty': 0.09}}}
Creating pin GP19 from dict: {'mode': 'PWM_IN', 'duty': 0.09}
Configured pwm-in pin, gpio: board.GP19, duty_center: 0.09
Updated input pins: {'GP19': <PWMIn object at 0x20012a00>}
Updated output pins: {}
Finished setup <--------
Starting setup -------->
Received setup dict: {'input_pins': {'GP19': {}}}
De-initialise pin: <PWMIn object at 0x20012a00>
Removing pin GP19
Updated input pins: {}
Updated output pins: {}
Finished setup <--------
Starting setup -------->
Received setup dict: {'input_pins': {'GP19': {'mode': 'PWM_IN', 'duty': 0.09}}}
Creating pin GP19 from dict: {'mode': 'PWM_IN', 'duty': 0.09}
Configured pwm-in pin, gpio: board.GP19, duty_center: 0.09
Updated input pins: {'GP19': <PWMIn object at 0x20012410>}
Updated output pins: {}
Finished setup <--------
Starting setup -------->
Received setup dict: {'input_pins': {'GP19': {}}}
De-initialise pin: <PWMIn object at 0x20012410>
Removing pin GP19
Updated input pins: {}
Updated output pins: {}
Finished setup <--------
Starting setup -------->
Received setup dict: {'input_pins': {'GP20': {'mode': 'PWM_IN', 'duty': 0.09}}}
Creating pin GP20 from dict: {'mode': 'PWM_IN', 'duty': 0.09}
Configured pwm-in pin, gpio: board.GP20, duty_center: 0.09
Updated input pins: {'GP20': <PWMIn object at 0x2000fd60>}
Updated output pins: {}
Finished setup <--------
Starting setup -------->
Received setup dict: {'input_pins': {'GP20': {}}}
De-initialise pin: <PWMIn object at 0x2000fd60>
Removing pin GP20
Updated input pins: {}
Updated output pins: {}
Finished setup <--------
Starting setup -------->
Received setup dict: {'input_pins': {'GP19': {'mode': 'PWM_IN', 'duty': 0.09}}}
Creating pin GP19 from dict: {'mode': 'PWM_IN', 'duty': 0.09}
Configured pwm-in pin, gpio: board.GP19, duty_center: 0.09
Updated input pins: {'GP19': <PWMIn object at 0x200116d0>}
Updated output pins: {}
Finished setup <--------
Starting setup -------->
Received setup dict: {'input_pins': {'GP19': {}}}
De-initialise pin: <PWMIn object at 0x200116d0>
Removing pin GP19
Updated input pins: {}
Updated output pins: {}
Finished setup <--------
Starting setup -------->
Received setup dict: {'input_pins': {}, 'output_pins': {}}
Reset all pins.
Updated input pins: {}
Updated output pins: {}
Finished setup <--------
Starting setup -------->
Received setup dict: {'input_pins': {'GP19': {'mode': 'PWM_IN', 'duty': 0.09}}}
Creating pin GP19 from dict: {'mode': 'PWM_IN', 'duty': 0.09}
Configured pwm-in pin, gpio: board.GP19, duty_center: 0.09
Updated input pins: {'GP19': <PWMIn object at 0x20013680>}
Updated output pins: {}
Finished setup <--------
Starting setup -------->
Received setup dict: {'input_pins': {'GP19': {}}}
De-initialise pin: <PWMIn object at 0x20013680>
Removing pin GP19
Updated input pins: {}
Updated output pins: {}
Finished setup <--------
Starting setup -------->
Received setup dict: {'input_pins': {}, 'output_pins': {}}
Reset all pins.
Updated input pins: {}
Updated output pins: {}
Finished setup <--------
Starting setup -------->
Received setup dict: {'input_pins': {'GP19': {'mode': 'PWM_IN', 'duty': 0.09}}}
Creating pin GP19 from dict: {'mode': 'PWM_IN', 'duty': 0.09}
Configured pwm-in pin, gpio: board.GP19, duty_center: 0.09
Updated input pins: {'GP19': <PWMIn object at 0x20015d20>}
Updated output pins: {}
Finished setup <--------
Starting setup -------->
Received setup dict: {'input_pins': {'GP19': {}}}
De-initialise pin: <PWMIn object at 0x20015d20>
Removing pin GP19
Updated input pins: {}
Updated output pins: {}
Finished setup <--------
Starting setup -------->
Received setup dict: {'input_pins': {}, 'output_pins': {}}
Reset all pins.
Updated input pins: {}
Updated output pins: {}
Finished setup <--------
Starting setup -------->
Received setup dict: {'input_pins': {'GP19': {'mode': 'PWM_IN', 'duty': 0.09}}}
Creating pin GP19 from dict: {'mode': 'PWM_IN', 'duty': 0.09}
Configured pwm-in pin, gpio: board.GP19, duty_center: 0.09
Updated input pins: {'GP19': <PWMIn object at 0x2000ebd0>}
Updated output pins: {}
Finished setup <--------
Starting setup -------->
Received setup dict: {'input_pins': {'GP19': {}}}
De-initialise pin: <PWMIn object at 0x2000ebd0>
Removing pin GP19
Updated input pins: {}
Updated output pins: {}
Finished setup <--------

Code done running.

Press any key to enter the REPL. Use CTRL-D to reload.
soft reboot

Auto-reload is on. Simply save files over USB to run them or enter REPL to disable.
code.py output:

The output above was created by initialising and running two digital input and output pins in the python interpreter (GP0 and GP1) and afterwards calling the pin.py script with different parameters like:

pins.py -w PICO.BCM.19 -tm 10

Viewing serial output for debugging

The easiest way to view the above output is by installing and running tio, a linux terminal program that can be attached to the serial port by tio /dev/ttyACM0. Note, that tio provides you with a terminal, which means you can type Ctrl-C to enter the REPL and work on the Pico interactively or press Ctrl-D to return to the above program. The corresponding familiar terminal code shown on the end of that last session.