Balboa & Raspberry Pi

I’ve built the Balboa. Easy build with the great instructions and it works perfectly. I fitted a RPi Zero-W and that works great. Wireless is working. Now to get the two to talk.

I looked over the RPiSlave library mentioned in the Balboa manual and of course it doesn’t work. Won’t even compile with the Balance example. But I didn’t expect it to work. It’s written for a different platform.

In order to balance the Balboa needs to use the IMU, obviously, so it’s programmed as a TWI master. But the RPiSlave library is expecting the Balboa to act as a slave. Balboa can’t be both. I’m thinking about it but not sure where to go next. I can reverse the roles, make the RPi the slave and keep the Balboa as master. But then the RPi isn’t the higher authority, it’s a peripheral. Or make the Balboa a slave and have the RPi interface with the IMU and send readings down to the Balboa.

The latter seems to be the better approach. But what about latency, with the Pi being a general purpose computer? I would appreciate anyone’s thoughts on the issue.

Thanks!!

Brand new to Pololu by the way!

-D

Hello.

We’ve been trying to take the second approach you mentioned (“make the Balboa a slave and have the RPi interface with the IMU and send readings down to the Balboa”).

I have an early implementation of some software that does this, but I never got it tuned to actually balance well. In any case, the tuning is going to depend on your choice of Raspberry Pi (with the Zero being a lot lighter than a full-size 3B, for example) and what kind of arms or bumpers you attach to the Balboa (which are especially recommended to protect the Raspberry Pi).

Although it’s not ready for an official release, I just made this version of the software available on GitHub in the “balboa” branch of the pololu-rpi-slave-arduino-library repository, so you could give it a try.

It works similarly to the existing demos for the A-Star and Romi, except you need to run server_balboa.py instead of server.py, and the web interface presents you with a “Calibrate” button that you need to click to calibrate the IMU before the driving controls appear for you to use (including a “Stand up” button). As I mentioned before, you’ll definitely have to adjust the balancing constants; you can do that by editing balance.py.

Please let us know if you have any trouble with the software. If you are successful getting it to work, we’d be interested to see your results!

Kevin

Thank you! I’ll certainly try it out and let you know.

Hi,

I’m also new to the Balboa. I’m resurrecting this thread because my question follows on and seems relevant. I’m glad to report that the server_balboa.py flask app works fine with my RPi3 Model B.

When I call the calibrate() function and then stand_up() in a python script, outside the flask web app, the Balboa flips over and won’t balance! But it works fine in the web app! I’m about to walk the code to see what I missed but thought it might be quicker to ask here.

How can I use the balance.py functions in a standalone python script to make it stand up?

(I’ve also attached a VL6180X ToF sensor to the I2C bus on the top header row and it works fine (with some help from here)

All the best!

2019-01-21T18:58:00Z

Hello.

Can you post your Python script?

- Amanda

Hi Amanda,

Yes, of course. I’m sure it looks familiar! The halt call doesn’t work but I’m not actually calling it right now. The robot plays all of the notes but fails to stand up and flips over.

I’ve left the other Python scripts in the directory unchanged (Balance.py for example).

I’m assuming there’s some variables that are not set which must be set somewhere between the javascript and the python app perhaps.

#!/usr/bin/env python3

# Copyright Pololu Corporation.  For more information, see https://www.pololu.com/

import time

from a_star import AStar
a_star = AStar()

from balance import Balancer
balancer = Balancer()

import json

led0_state = False
led1_state = False
led2_state = False

def status():
    buttons = a_star.read_buttons()
    analog = a_star.read_analog()
    battery_millivolts = a_star.read_battery_millivolts()
    encoders = a_star.read_encoders()
    calibrated = balancer.calibrated

    data = {
        "buttons": buttons,
        "battery_millivolts": battery_millivolts,
        "analog": analog,
        "encoders": encoders,
        "calibrated": calibrated
    }
    return json.dumps(data)


def calibrate():
    balancer.setup()
    balancer.start()
    return ""


def stand_up():
    balancer.stand_up()
    return ""

def drive(left, right):
    balancer.drive(int(left), int(right))
    return ""

def leds(led0, led1, led2):
    a_star.leds(led0, led1, led2)
    global led0_state
    global led1_state
    global led2_state
    led0_state = led0
    led1_state = led1
    led2_state = led2
    return ""

def hearbeat(state):
    if state == 0:
      a_star.leds(led0_state, led1_state, led2_state)
    else:
        a_star.leds(not led0_state, not led1_state, not led2_state)
    return ""

def play_notes(notes):
    a_star.play_notes(notes)
    return ""

def halt():
    call(["bash", "-c", "(sleep 2; sudo halt)&"])

def shutting_down():
    return "Shutting down in 2 seconds! You can remove power when the green LED stops flashing."

calibrate()
play_notes("l16ceg>c")
time.sleep(5)
play_notes("l16deg>d")
stand_up()
play_notes("l16deeg>de")

Can you add an infinite loop at the end of your script? For example,

while True:
  time.sleep(1)

I suspect that when your script terminates, the balancer stops running, causing your Balboa to stop keeping itself upright.

- Amanda

Yes, that works - many thanks.

I see now there’s no inner loop in the balance.py code. It could be my imagination, but it does seem to twitch about a lot, presumably because of latency as it checks the loop in the python code but I can fix that later.

Here is the working code for the VL6180X to work in the flask web app. First up, get the ST_VL6108X python code from here. Save the ST_VL6180X file in the same directory as the server_balboa.py file. Connect the sensor to the I2C bus to access the sensor from the Raspberry Pi. The pinout diagram shows how to connect it up.

In the index.html file located in the pi/server_balboa_resources/templates folder, add the following HTML table:

<table>
<tr><th rowspan=2>VL6180X Time Of Flight</th><td>ToF Lux</td><td>ToF Distance</td></tr>
<tr><td id="tof_lux"></td><td id="tof_distance"></td></tr>
</table>

then, the pi/server_balboa.py script should be modified to add the setup for the sensor and then render the values in the HTML table:

#!/usr/bin/env python3

# Copyright Pololu Corporation.  For more information, see https://www.pololu.com/
from flask import Flask
from flask import render_template
from flask import redirect
from subprocess import call
app = Flask(__name__, static_folder='server_balboa_resources/static', template_folder='server_balboa_resources/templates')
app.debug = True

import sys

from a_star import AStar
a_star = AStar()

from ST_VL6180X import VL6180X

debug = False

tof_address = 0x29
tof_sensor = VL6180X(address=tof_address, debug=debug)
# apply pre calibrated offset
tof_sensor.set_range_offset(23)
tof_sensor.default_settings()

from balance import Balancer
balancer = Balancer()

tof_distance = 255

import json

led0_state = False
led1_state = False
led2_state = False

@app.route("/")
def hello():
    return render_template("index.html")

@app.route("/status.json")
def status():
    buttons = a_star.read_buttons()
    analog = a_star.read_analog()
    battery_millivolts = a_star.read_battery_millivolts()
    encoders = a_star.read_encoders()
    calibrated = balancer.calibrated
    tof_distance = tof_sensor.get_distance()
    tof_lux = round(tof_sensor.get_ambient_light(20), 2)
    data = {
        "buttons": buttons,
        "battery_millivolts": battery_millivolts,
        "analog": analog,
        "encoders": encoders,
        "calibrated": calibrated,
        # adding in the sensor values here
        "tof_distance": tof_distance,
        "tof_lux": tof_lux
    }
    return json.dumps(data)

@app.route("/calibrate")
def calibrate():
    balancer.setup()
    balancer.start()
    return ""

@app.route("/stand_up")
def stand_up():
    balancer.stand_up()
    return ""

@app.route("/drive/<left>,<right>")
def drive(left, right):
    balancer.drive(int(left), int(right))
    return ""

@app.route("/leds/<int:led0>,<int:led1>,<int:led2>")
def leds(led0, led1, led2):
    a_star.leds(led0, led1, led2)
    global led0_state
    global led1_state
    global led2_state
    led0_state = led0
    led1_state = led1
    led2_state = led2
    return ""

@app.route("/heartbeat/<int:state>")
def hearbeat(state):
    if state == 0:
      a_star.leds(led0_state, led1_state, led2_state)
    else:
        a_star.leds(not led0_state, not led1_state, not led2_state)
    return ""

@app.route("/play_notes/<notes>")
def play_notes(notes):
    a_star.play_notes(notes)
    return ""

@app.route("/halt")
def halt():
    call(["bash", "-c", "(sleep 2; sudo halt)&"])
    return redirect("/shutting-down")

@app.route("/shutting-down")
def shutting_down():
    return "Shutting down in 2 seconds! You can remove power when the green LED stops flashing."

if __name__ == "__main__":
    app.run(host = "0.0.0.0")

finally the Javascript in pi/server_balboa_resources/static needs to be modified to pass the collected JSON values back to the HTML.

function update_status(json) {
  s = JSON.parse(json)
  $("#button0").html(s["buttons"][0] ? '1' : '0')
  $("#button1").html(s["buttons"][1] ? '1' : '0')
  $("#button2").html(s["buttons"][2] ? '1' : '0')

  $("#battery_millivolts").html(s["battery_millivolts"])

  $("#analog0").html(s["analog"][0])
  $("#analog1").html(s["analog"][1])
  $("#analog2").html(s["analog"][2])
  $("#analog3").html(s["analog"][3])
  $("#analog4").html(s["analog"][4])
  $("#analog5").html(s["analog"][5])
    
  $("#tof_distance").html(s["tof_distance"])
  $("#tof_lux").html(s["tof_lux"])
  
  $("#encoders0").html(s["encoders"][0])
  $("#encoders1").html(s["encoders"][1])

  if(!calibrated && s["calibrated"])
  {
    calibrateDone()
    calibrated = true
  }

  setTimeout(poll, 100)
}

Should look like this:

1 Like

Hi,

I was trying Paul’s code with some minor modifications in balance.py for initial angle and other constants. However, I have been getting the following error
image

I tried to increase the sleep times for read write i2c operations in a_star.py which didn’t work.

Thanks in advance for your help!

Hey Hari,

That’s about as far as I got too. Kevin helpfully pointed to a likely problem in the write_i2c_block_data here, but I never solved it. Didn’t matter what values I put in there, I always (inconsistently) got IO errors. So I gave up - figured it was something lower level that I didn’t have access to in the code.

Hi, Hari_2021.

Does the original unmodified code work fine for you? If so, could you summarize what you changed and show the differences in your code?

Kevin

Hello Kevin,

I changed the ANGLE_RATE_RATIO, ANGLE_RESPONSE and the DISTANCE_RESPONSE in balance.py given here pololu-rpi-slave-arduino-library/balance.py at balboa · pololu/pololu-rpi-slave-arduino-library · GitHub

The modified server_balboa.py code I tried is given below:

from a_star import AStar
a_star = AStar()

from balance import Balancer
balancer = Balancer()

import json

led0_state = False
led1_state = False
led2_state = False

def hello():
    return render_template("index.html")


def status():
    buttons = a_star.read_buttons()
    analog = a_star.read_analog()
    battery_millivolts = a_star.read_battery_millivolts()
    encoders = a_star.read_encoders()
    calibrated = balancer.calibrated
    data = {
        "buttons": buttons,
        "battery_millivolts": battery_millivolts,
        "analog": analog,
        "encoders": encoders,
        "calibrated": calibrated
    }
    return json.dumps(data)


def calibrate():
    balancer.setup()
    balancer.start()
    return ""


def stand_up():
    balancer.stand_up()
    return ""

def drive(left, right):
    balancer.drive(int(left), int(right))
    return ""


def leds(led0, led1, led2):
    a_star.leds(led0, led1, led2)
    global led0_state
    global led1_state
    global led2_state
    led0_state = led0
    led1_state = led1
    led2_state = led2
    return ""

def hearbeat(state):
    if state == 0:
      a_star.leds(led0_state, led1_state, led2_state)
    else:
        a_star.leds(not led0_state, not led1_state, not led2_state)
    return ""

def play_notes(notes):
    a_star.play_notes(notes)
    return ""

def halt():
    call(["bash", "-c", "(sleep 2; sudo halt)&"])
    return redirect("/shutting-down")

def shutting_down():
    return "Shutting down in 2 seconds! You can remove power when the green LED stops flashing."

if __name__ == "__main__":
    calibrate()
    time.sleep(5)
    stand_up()
    while True:
        time.sleep(1)

Hi Paul_Rowen,

I didn’t add a sensor. Just tried to run the code without the webapp. It looked like it worked for you by incorporating AmandaS 's suggestion.

What type of Raspberry Pi and what I2C frequency are you using, and what value do you have for the delay template parameter for the PololuRPiSlave object in the sketch? (This defaults to 5 in the line PololuRPiSlave<struct Data,5> slave;.)

Kevin

Hi Kevin,

I am using a raspberry pi zero w. The I2C frequency is 100 Hz.
I haven’t changed the delay parameter.

I tried the following code. The leds and encoders work fine. However, the motors don’t run.

from a_star import AStar
import time

a_star = AStar()
a_star.leds(0,0,0)
time.sleep(0.5)
a_star.leds(1,1,1)
time.sleep(0.5)
batteryVoltage = a_star.read_battery_millivolts()
print(batteryVoltage)
encoderValue = read_encoders()
print(encoderValue)
a_star.motors(10,10)
time.sleep(5)
a_star.motors(-10,-10)
time.sleep(5)
a_star.motors(0,0)

Could you try changing the delay parameter in the sketch to 10? After that, please try running benchmark.py on the RPi a few times and make sure it doesn’t fail with any errors. (You can also try the benchmark before changing the delay to see if that is in fact causing problems. Again, it is important to try running it several times because it might not always fail even if the delay is not set properly.)

As for your latest program, note that the motor functions take a speed in the range of 0-300, so I suspect that 10 is just too low of a speed (duty cycle) for the motors to actually start turning. Could you try something closer to 100 and see if that works?

Kevin

Hi Kevin,

I tried your suggestions for delay parameters of 6,8,10,15,20. With the last three, read and write operations without errors was possible for all the 20 trials of benchmark.py.
However, the error in the original code still persists. Do we need higher delays?
I tried with delay time in a_star.py of 0.01, 0.002, 0.005, 0.02 and default values for all the sketch values.
Regards,

Just to be clear, do you still see a “Remote I/O error” even with the original, unmodified Python script (server_balboa.py)?

I expect a delay parameter of 10 in the sketch to work for a RPi Zero W using 100 kHz, and you should not have to change anything in a_star.py. You mentioned you changed the delays in that file earlier, so could you try restoring a_star.py to its original state on the RPi (with time.sleep(0.0001)), but keeping the modified delay parameter of 10 on the Balboa’s program, and then trying that combination? If you are not sure whether you have other leftover changes on the RPi, you might want to try redownloading the Python software.

Kevin

Yes Kevin. I tried with modified delay parameter of 10 on balboa and time.sleep(0.0001) on RPi.
I’ll try the suggestion with python.

Hello Kevin,

I reinstalled python. The remote IO error persists.