Coding Paradigms for Device Control#

%serialconnect
Found serial ports: /dev/cu.usbmodem14101, /dev/cu.Bluetooth-Incoming-Port 
Connecting to --port=/dev/cu.usbmodem14101 --baud=115200 
Ready.

The Coding Challenge#

PD control for a Ball on beam device. The device is to sense the position of a ball on a 50cm beam, compare to a setpoint, and adjust beam position with servo motor. The setpoint and control constant is to be given by the device user. Display all relevant data. Use a button to start and stop operation.

Devices:

  • Distance sensor - sense ball position

  • Analog actuator - change beam angle

  • Analog sensor - proportional gain

  • Analog sensor - derivative time

  • Analog sensor - setpoint

  • Display - display position, angle

  • Display - display control parameters

  • Button - Start/Stop

Create code to:

  • Measure the ball position

  • Perform an action in response to the analog signal

  • Display state on LCD

  • Use on-board LED to show operational status

Coding Paradigms#

  • Single threaded, imperative coding

  • Python classes

    • further modularizes coding

    • data logging classes* Python classes

    • further modularizes coding

    • data logging classes

  • Python generators

    • separates event loop from device details

    • modularizes the device coding

    • each device can maintain a separate state

  • Asynchronous coding

    • further abstraction the event loop

    • non-blocking

  • multi-threading

Single threaded, imperative coding#

from machine import Pin, PWM
import time

class Servo(object):
    def __init__(self, gpio, freq=50):
        self.gpio = gpio
        self.pwm = PWM(Pin(gpio, Pin.IN))
        self.pwm.freq(freq)
        self.pwm.duty_ns(0)
        
    def set_value(self, value):
        self.pulse_us = 500 + 20*max(0, min(100, value))
        self.pwm.duty_ns(int(1000*self.pulse_us))
        
    def off(self):
        self.pwm.duty_ns(0)
        

servo = Servo(16)

servo.set_value(0)
time.sleep(2)
servo.set_value(100)
time.sleep(2)
    
servo.off()
.
from machine import Pin, I2C
from lcd1602 import LCD1602 as LCD


class Screen(object):
    def __init__(self, id, sda, scl):
        self.sda = Pin(sda, Pin.OUT)
        self.scl = Pin(scl, Pin.OUT)
        self.i2c = I2C(id, sda=self.sda, scl=self.scl)
        self.lcd = LCD(self.i2c, 2, 16)
        self.lcd.clear()
        
    def print(self, lines):
        for k, line in enumerate(lines):
            if line is not None:
                self.lcd.setCursor(0, k)
                self.lcd.print(line)
        
        
screen0 = Screen(0, sda=8, scl=9)
screen1 = Screen(1, sda=6, scl=7)
        
screen0.print(("Hello World", "Go Irish!"))
screen1.print(["", "Hello"])
from machine import Pin

class PWM_motor(object):
    def __init__(self, gpio):
        self.pwm = PWM(Pin(gpio))
           
class Servo(PWM_motor):
    def __init__(self, gpio, freq=50):
        super(Y, self).__init__(gpio)
        self.pwm.freq(freq)
        
    def set_value(self, value):
        self.pulse_us = 500 + 20*max(0, min(100, value))
        self.pwm.duty_ns(int(1000*self.pulse_us))
        
servo = Servo(16)
servo.set_value(50)
[missing-OK]O
[missing-OK]
[missing-OK]
Traceback (most recent call last):
[missing-OK]
  File "<stdin>
[missing-OK]
", line 16, in <module>
[missing-OK]
  File "<stdin>
[missing-OK]
", line 9, in __init__
[missing-OK]
NameError: name 'Y' isn't defined
[missing-OK]
>
[missing-OK]
**[ys] 
[missing-OK]
<class 'serial.serialutil.SerialException'>
[missing-OK]
**[ys] 
[missing-OK]
read failed: [Errno 6] Device not configured
[missing-OK]
**[ys] 
[missing-OK]
<class 'serial.serialutil.SerialException'>
[missing-OK]
**[ys] 
[missing-OK]
read failed: [Errno 6] Device not configured
import machine
import time

class UltrasonicSensor(object):
    def __init__(self, gpio):
        self.pin = Pin(gpio)
        
    def get_distance_cm(self):
        # send pulse
        self.pin.init(Pin.OUT)
        self.pin.value(0)
        time.sleep_us(2)
        self.pin.value(1)
        time.sleep_us(10)
        self.pin.value(0)

        # listen for response
        self.pin.init(Pin.IN)

        # wait for on
        t0 = time.ticks_us()
        count = 0
        while count < 10000:
            if self.pin.value():
                break
            count += 1

        # wait for off
        t1 = time.ticks_us()
        count = 0
        while count < 10000:
            if not self.pin.value():
                break
            count += 1

        t2 = time.ticks_us()

        if t1 - t2 < 530:
            return (t2 - t1) / 29 / 2
        else:
            return 0
        
sensor = UltrasonicSensor(20)
print(sensor.get_distance_cm())
12.7069
from machine import Pin, I2C, ADC, PWM
import time
from lcd1602 import LCD1602 as LCD
from knob import Knob


class Servo(object):
    def __init__(self, gpio, freq=50):
        self.gpio = gpio
        self.pwm = PWM(Pin(gpio))
        self.pwm.freq(freq)
        self.pwm.duty_ns(0)
        
    def set_value(self, value):
        self.pulse_us = 500 + 20*max(0, min(100, value))
        self.pwm.duty_ns(int(1000*self.pulse_us))
        
    def off(self):
        self.pwm.duty_ns(0)


class Screen(object):
    def __init__(self, id, sda, scl):
        self.sda = Pin(sda, Pin.OUT)
        self.scl = Pin(scl, Pin.OUT)
        self.i2c = I2C(id, sda=self.sda, scl=self.scl)
        self.lcd = LCD(self.i2c, 2, 16)
        self.lcd.clear()
        
    def print(self, lines):
        for k, line in enumerate(lines):
            if line is not None:
                self.lcd.setCursor(0, k)
                self.lcd.print(line)


## set up led
led = Pin(25, Pin.OUT)

## set up lcd display 0
display0 = Screen(0, sda=8, scl=9)
display1 = Screen(1, sda=6, scl=7)

## setup rotary angle sensors
knob0 = Knob(26)
knob1 = Knob(27)

## setup ultra-sonic distance sensor on Pin 20
sensor = UltrasonicSensor(20)

## set up servo motor
servo = Servo(16)

start = time.time()
ball_position = 0

while time.time() - start < 20:

    ball_position = sensor.get_distance_cm()
    ball_setpoint = 50*knob0.get_value()/100
    display0.print([f"SP = {ball_setpoint:0.2f} cm", 
                f"PV = {ball_position:0.2f} cm"])
    
    Kp = knob1.get_value()
    u = Kp*(ball_setpoint - ball_position)
    servo.set_value(u)
    
    display1.print([f"Kp = {Kp}", f"MV = {dt_us}"])
    time.sleep(0.1)

servo.off()
[missing-OK]O....
[missing-OK]
[missing-OK]
>
..............................................................................................................................................................................
[missing-OK]
aw 
.
[missing-OK]
a
..
[missing-OK]
**[ys] 
[missing-OK]
<class 'serial.serialutil.SerialException'>
[missing-OK]
**[ys] 
[missing-OK]
device reports readiness to read but returned no data (device disconnected or multiple access on port?)
.....
[missing-OK]
ra REPL;
.
[missing-OK]
aw
...
[missing-OK]
raw REPL; CTRL-B to exit
[missing-OK]
aw>
...
[missing-OK]
**[ys] 
[missing-OK]
<class 'serial.serialutil.SerialException'>
[missing-OK]
**[ys] 
[missing-OK]
device reports readiness to read but returned no data (device disconnected or multiple access on port?)

Discuss#

  • Does this code provide a working prototype?

  • Is this code maintaina