Coding for Device Control#

The goal of this notebook is to demonstrate coding practices that lead to

%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, ADC
import time

        
50.86748 75.14153
51.0872 75.0927
50.94072 75.0927
23.56451 75.38568
22.61235 75.53216
22.61235 75.48333
22.46586 75.23918
22.5391 75.72747
22.58793 75.26361
20.24415 75.19035
90.01297 75.11711
98.48631 74.82262
100.0 74.74937
72.52766 74.82262
44.93324 75.11711
13.38216 75.04387
0.5859465 75.14153
0.6103609 75.21477
0.6103609 75.0927
0.6347754 75.11711
from machine import Pin, I2C, ADC, PWM
import time
from lcd1602 import LCD1602 as LCD

class Knob(object):
    def __init__(self, gpio):
        self.gpio = gpio
        self.adc = ADC(Pin(gpio))
        
    def value(self):
        return 100*self.adc.read_u16()/65535


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

## set up lcd display 0
sda = Pin(8, Pin.OUT)
scl = Pin(9, Pin.OUT)
i2c0 = I2C(0, sda=sda, scl=scl)

dsp0 = LCD(i2c0, 2, 16)
dsp0.clear()
dsp0.setCursor(0, 0)
dsp0.print("Hello, World")
dsp0.setCursor(0, 1)
dsp0.print("Display 0")

## set up lcd display 1
sda = Pin(6, Pin.OUT)
scl = Pin(7, Pin.OUT)
i2c1 = I2C(1, sda=sda, scl=scl)

dsp1 = LCD(i2c1, 2, 16)
dsp1.clear()
dsp1.setCursor(0, 0)
dsp1.print("Hello, World")
dsp1.setCursor(0, 1)
dsp1.print("Display 1")

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

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

## set up servo motor
pwm = PWM(Pin(16))
pwm.freq(50)
pwm.duty_ns(1000*1500)

start = time.time()
ball_position = 0

while time.time() - start < 20:

    # read distance
    # send pulse
    dst.init(Pin.OUT)
    dst.value(0)
    time.sleep_us(2)
    dst.value(1)
    time.sleep_us(10)
    dst.value(0)
    
    # listen for response
    dst.init(Pin.IN)
    
    # wait for on
    t0 = time.ticks_us()
    count = 0
    while count < 10000:
        if dst.value():
            break
        count += 1
        
    # wait for off
    t1 = time.ticks_us()
    count = 0
    while count < 10000:
        if not dst.value():
            break
        count += 1
    
    t2 = time.ticks_us()
    
    if t1 - t2 < 530:
        ball_position = (t2 - t1) / 29 / 2
        
    # read analog sensor
    ball_setpoint = 50*knob0.value()/100
       
    # display ball state
    dsp0.clear()
    dsp0.setCursor(0, 0)
    dsp0.print(f"SP = {ball_setpoint:0.2f} cm")
    dsp0.setCursor(0, 1)
    dsp0.print(f"PV = {ball_position}")
    
    # measure control gain
    Kp = knob1.value()
    
    # update servo
    
    # adjust servo
    u = Kp*(ball_setpoint - ball_position)
    dt_us = int(1500 + max(-1000, min(1000, u)))
    pwm.duty_ns(1000*dt_us)
    
    # display controller state
    dsp1.clear()
    dsp1.setCursor(0, 0)
    dsp1.print(f"Kp = {Kp}")
    dsp1.setCursor(0, 1)
    dsp1.print(f"MV = {dt_us}")
    
    time.sleep(0.1)
    
...

Discuss#

  • Does this code provide a working prototype?

  • Is this code maintaina