Final, Final, Microbit RC

Following on from the last post about the remote control microbit powered bitbot, I think I mentioned all the way along that the car source code and functionality I would like to include is constantly butting up against the program size restriction that the microbit can accomodate. If you create too big a program, then after the flashing process finishes you will see a "memory allocation" error scrolling across the screen.

After a bit of searching I found this post useful. It turns out that classes, code comments, long functions and long variable names all contribute to larger program sizes.

One of the selling points for the code as I had always organized this project was the easy understandability of the classes for motor, led etc. It looked like I was going to have to abandon some of those and replace some variables if I wanted to continue to add functionality to the project.

Handset

The handset code didn't change very much because that has always been small enough (about 100 lines) and all I needed was to add a couple of extra commands to send to the car.

Commands

I remapped the A and B buttons to the headlights and horn commands respectively and moved the not very often used fast and slow commands to the edge connectors. Hitting the A button cycles the cars lights through on, off and auto settings, where auto means to use the microbit's light sensor to decide if it needs to turn on the headlights. Hitting the B button sounds a quick double toot on the horn to let people know you are coming. I also added a horn sound when the car starts reversing as a nod to safety and realism.

Watchdog

I also noticed during testing that if I pulled the plug on the handset, the car stopped receiving messages but could continue to drive the motors, perhaps with disasterous results. I decided that a cut off would be useful if the two halves ever lost contact with each other. As well as the normal movement commands I added a watchdog 'hi' to send out to the car approximately every 5 seconds even when no control commands are being sent.


from microbit import *
import radio

def wait_for_contact():
    hello = 'hi'
    waiting = True

    while waiting:
        display.set_pixel(2, 2, 9)
        sleep(100)
        radio.send(hello)

        msg = radio.receive()

        if msg and msg == hello:
            waiting = False
        else:
            display.set_pixel(2, 2, 0)
            sleep(100)

def show_contact_made():
    for i in range(5):
        display.show(Image.HEART_SMALL)
        sleep(500)
        display.show(Image.HEART)
        sleep(500)

fwd_rev_tilt = 250
steering_tilt = 250

forward_command = 'F'
stop_command = 'S'
left_command = 'L'
right_command = 'R'
reverse_command = 'B'
fast_command = 'Z'
slow_command = 'W'
lights_command = 'I'
horn_command = 'P'
watchdog_command = 'hi'

display.scroll("bitBot RC handset")

radio.on()

wait_for_contact()
show_contact_made()

last_cmd = ''
watchdog_counter = 0

while True:

    cmd = ''

    if button_a.was_pressed():
        cmd = lights_command
    elif button_b.was_pressed():
        cmd = horn_command
    elif pin0.is_touched():
        cmd = slow_command
    elif pin2.is_touched():
        cmd = fast_command
    else:
        forward_back = accelerometer.get_y()
        side_to_side = accelerometer.get_x()

        if abs(forward_back) >= abs(side_to_side):
            # priority to fwd reverse
            if forward_back < -fwd_rev_tilt:
                display.show(Image.ARROW_N)
                cmd = forward_command
            elif forward_back > fwd_rev_tilt:
                display.show(Image.ARROW_S)
                cmd = reverse_command
        else:
            if side_to_side > steering_tilt:
                display.show(Image.ARROW_E)
                cmd = right_command
            elif side_to_side < -steering_tilt:
                display.show(Image.ARROW_W)
                cmd = left_command

    if cmd == '':
        display.show(Image.SQUARE_SMALL)
        cmd = stop_command

    if cmd != '' and cmd != last_cmd:
        radio.send(cmd)
        last_cmd = cmd

    # try to keep in constant contact
    # even if nothing has changed so
    # we don't run away
    watchdog_counter += 1

    # send every 5 seconds or so
    if watchdog_counter >= 20:
        radio.send(watchdog_command)
        watchdog_counter = 0

    sleep(250)

Car

On to the car. After flattening out the functions from their original classes, I needed to some renaming to make their use more obvious and to organize them into a more coherent structure. First motor commands, then lights, then overall "car" commands. Notice the number of string and number literals I had to use to get the code shoehorned into the microbit.

Lights

The last version had an auto headlight feature using the light detector but this time I added a manual on and off override as well as adjusting the intensity of all the lights.

Horn

As mentioned above, the bitbot piezo horn is put to use to warn everyone when reversing and for tooting when the driver hits the B button on the handset. Just like a real car!

Watchdog

The final feature was implementing a more sophisticated run loop. The outer loop still runs forever but I added an inner loop to wait for initial contact, then run as long as the handset was in communication with the car. Either by sending a movement command or by the special 'hi' command. We increment the watchdog count every time through the loop and reset the count when we get another message over the radio. If we go too long without contact, we drop out of the inner while loop, stop the motors and wait for contact to be established again at the top of the outer loop.


from microbit import *
import radio
import neopixel

# Motors - speed and direction
left_motor = [pin0, pin8]
right_motor = [pin1, pin12]

# Motor Functions
def pulse_width_modulate(motor, speed, direction):
    if 1023 >= speed >= 0:
        motor[0].write_analog(speed)
        motor[1].write_digital(direction)

def motor_forward(motor, percent):
    pulse_width_modulate(motor, percent * 10.23, 0)

def motor_reverse(motor, percent):
    pulse_width_modulate(motor, 1023-(percent * 10.23), 1)

def motor_stop(motor):
    pulse_width_modulate(motor, 0, 0)

# LED Functions
neopixels = neopixel.NeoPixel(pin13, 12)

def set_leds(pixels, colour):
    for i in pixels:
        neopixels[i] = colour
    neopixels.show()

# Colours 
black = (0, 0, 0)
white = (75, 75, 75) # full intensity is REALLY bright
red = (75, 0, 0)
amber = (75, 40, 0)

def mainbeam_lights(off=False):
    set_leds([5, 11], black if off else white)

def reverse_lights(off=False):
    set_leds([0, 6], black if off else white)

def brake_lights(off=False):
    set_leds([0, 6], black if off else red)

def left_blinker(off=False):
    set_leds([3, 4], black if off else amber)

def right_blinker(off=False):
    set_leds([9, 10], black if off else amber)

# High level commands
direction = 'S'
speed = 50
headlights = 'auto'

def go_faster():
    global speed
    speed = min(speed + 20, 100)

def go_slower():
    global speed
    speed = max(speed - 20, 0)

def go_forward():
    global direction
    direction = 'F'
    motor_forward(left_motor, speed)
    motor_forward(right_motor, speed)

def reverse():
    global direction
    direction = 'B'
    sound_horn()
    motor_reverse(left_motor, speed)
    motor_reverse(right_motor, speed)
    
def steer_left():
    global direction
    direction = 'L'
    motor_forward(left_motor, speed / 2)
    motor_forward(right_motor, speed)

def steer_right():
    global direction
    direction = 'R'
    motor_forward(left_motor, speed)
    motor_forward(right_motor, speed / 2)

def stop():
    global direction
    direction = 'S'
    motor_stop(left_motor)
    motor_stop(right_motor)

def cycle_lights():
    global headlights
    if headlights == 'auto':
        headlights = 'off'
    elif headlights == 'off':
        headlights = 'on'
    else:
        headlights = 'auto'

def update_headlights():
    main_beams = True if headlights == 'on' else False

    if headlights == 'auto':
        main_beams = False if display.read_light_level() >= 75 else True

    if main_beams:
        mainbeam_lights()
    else:
        mainbeam_lights(off=True)
    
def update_blinkers():
    left_blinker(off=direction != 'L')
    right_blinker(off=direction != 'R')

def update_rear_lights():
    if direction == 'S':
        brake_lights()
    elif direction == 'F' or direction == 'L' or direction == 'R':
        brake_lights(off=True)
        reverse_lights(off=True)
    elif direction == 'B':
        reverse_lights()
    
def update_lights():
    update_headlights()
    update_blinkers()
    update_rear_lights()

def sound_horn():
    for i in range(2):
        pin14.write_digital(1)
        sleep(100)
        pin14.write_digital(0)
        sleep(200)
        
def wait_for_contact():
    display.clear()
    waiting = True

    while waiting:
        display.set_pixel(2, 2, 9)
        sleep(100)
        msg = radio.receive()

        if msg and msg == 'hi':
            radio.send(msg)
            waiting = False
        else:
            display.set_pixel(2, 2, 0)
            sleep(100)

def show_contact_made():
    for i in range(5):
        display.show(Image.HEART_SMALL)
        sleep(500)
        display.show(Image.HEART)
        sleep(500)

    display.clear()
    
display.scroll("bitBot RC")
radio.on()

while True:
    wait_for_contact()
    show_contact_made()

    watchdog_counter = 0

    while watchdog_counter < 100:
        watchdog_counter += 1
        update_lights()
        sleep(100)

        msg = radio.receive()

        if msg:
            watchdog_counter = 0
            
            if msg == 'S':
                stop()
            elif msg == 'F':
                go_forward()
            elif msg == 'B':
                reverse()
            elif msg == 'L':
                steer_left()
            elif msg == 'R':
                steer_right()
            elif msg == 'W':
                go_slower()
            elif msg == 'Z':
                go_faster()
            elif msg == 'I':
                cycle_lights()
            elif msg == 'P':
                sound_horn()

    stop()
    display.show(Image.SKULL)
    sleep(2000)
    

I had anticipated the code for the car to be much longer in the non-class version but it turned out to be about 200 lines so well within manageable limits for future maintenance. Now that I have promised myself I am done with features, that shouldn't be a concern :)