From 9c998580f80770fa2cdff0b319338aed7c851a58 Mon Sep 17 00:00:00 2001 From: lukas Date: Mon, 21 Nov 2016 01:15:57 +0000 Subject: [PATCH 01/50] Added servo arm support --- Adafruit_I2C.py | 161 +++++++++++++++++++++++++++++++++++ Adafruit_PWM_Servo_Driver.py | 92 ++++++++++++++++++++ arm.py | 48 +++++++++++ drive.py | 4 +- templates/arm.html | 59 +++++++++++++ 5 files changed, 362 insertions(+), 2 deletions(-) create mode 100755 Adafruit_I2C.py create mode 100644 Adafruit_PWM_Servo_Driver.py create mode 100644 arm.py create mode 100644 templates/arm.html diff --git a/Adafruit_I2C.py b/Adafruit_I2C.py new file mode 100755 index 0000000..2b24b67 --- /dev/null +++ b/Adafruit_I2C.py @@ -0,0 +1,161 @@ +#!/usr/bin/python +import re +import smbus + +# =========================================================================== +# Adafruit_I2C Class +# =========================================================================== + +class Adafruit_I2C(object): + + @staticmethod + def getPiRevision(): + "Gets the version number of the Raspberry Pi board" + # Revision list available at: http://elinux.org/RPi_HardwareHistory#Board_Revision_History + try: + with open('/proc/cpuinfo', 'r') as infile: + for line in infile: + # Match a line of the form "Revision : 0002" while ignoring extra + # info in front of the revsion (like 1000 when the Pi was over-volted). + match = re.match('Revision\s+:\s+.*(\w{4})$', line) + if match and match.group(1) in ['0000', '0002', '0003']: + # Return revision 1 if revision ends with 0000, 0002 or 0003. + return 1 + elif match: + # Assume revision 2 if revision ends with any other 4 chars. + return 2 + # Couldn't find the revision, assume revision 0 like older code for compatibility. + return 0 + except: + return 0 + + @staticmethod + def getPiI2CBusNumber(): + # Gets the I2C bus number /dev/i2c# + return 1 if Adafruit_I2C.getPiRevision() > 1 else 0 + + def __init__(self, address, busnum=-1, debug=False): + self.address = address + # By default, the correct I2C bus is auto-detected using /proc/cpuinfo + # Alternatively, you can hard-code the bus version below: + # self.bus = smbus.SMBus(0); # Force I2C0 (early 256MB Pi's) + # self.bus = smbus.SMBus(1); # Force I2C1 (512MB Pi's) + self.bus = smbus.SMBus(busnum if busnum >= 0 else Adafruit_I2C.getPiI2CBusNumber()) + self.debug = debug + + def reverseByteOrder(self, data): + "Reverses the byte order of an int (16-bit) or long (32-bit) value" + # Courtesy Vishal Sapre + byteCount = len(hex(data)[2:].replace('L','')[::2]) + val = 0 + for i in range(byteCount): + val = (val << 8) | (data & 0xff) + data >>= 8 + return val + + def errMsg(self): + print "Error accessing 0x%02X: Check your I2C address" % self.address + return -1 + + def write8(self, reg, value): + "Writes an 8-bit value to the specified register/address" + try: + self.bus.write_byte_data(self.address, reg, value) + if self.debug: + print "I2C: Wrote 0x%02X to register 0x%02X" % (value, reg) + except IOError, err: + return self.errMsg() + + def write16(self, reg, value): + "Writes a 16-bit value to the specified register/address pair" + try: + self.bus.write_word_data(self.address, reg, value) + if self.debug: + print ("I2C: Wrote 0x%02X to register pair 0x%02X,0x%02X" % + (value, reg, reg+1)) + except IOError, err: + return self.errMsg() + + def writeRaw8(self, value): + "Writes an 8-bit value on the bus" + try: + self.bus.write_byte(self.address, value) + if self.debug: + print "I2C: Wrote 0x%02X" % value + except IOError, err: + return self.errMsg() + + def writeList(self, reg, list): + "Writes an array of bytes using I2C format" + try: + if self.debug: + print "I2C: Writing list to register 0x%02X:" % reg + print list + self.bus.write_i2c_block_data(self.address, reg, list) + except IOError, err: + return self.errMsg() + + def readList(self, reg, length): + "Read a list of bytes from the I2C device" + try: + results = self.bus.read_i2c_block_data(self.address, reg, length) + if self.debug: + print ("I2C: Device 0x%02X returned the following from reg 0x%02X" % + (self.address, reg)) + print results + return results + except IOError, err: + return self.errMsg() + + def readU8(self, reg): + "Read an unsigned byte from the I2C device" + try: + result = self.bus.read_byte_data(self.address, reg) + if self.debug: + print ("I2C: Device 0x%02X returned 0x%02X from reg 0x%02X" % + (self.address, result & 0xFF, reg)) + return result + except IOError, err: + return self.errMsg() + + def readS8(self, reg): + "Reads a signed byte from the I2C device" + try: + result = self.bus.read_byte_data(self.address, reg) + if result > 127: result -= 256 + if self.debug: + print ("I2C: Device 0x%02X returned 0x%02X from reg 0x%02X" % + (self.address, result & 0xFF, reg)) + return result + except IOError, err: + return self.errMsg() + + def readU16(self, reg, little_endian=True): + "Reads an unsigned 16-bit value from the I2C device" + try: + result = self.bus.read_word_data(self.address,reg) + # Swap bytes if using big endian because read_word_data assumes little + # endian on ARM (little endian) systems. + if not little_endian: + result = ((result << 8) & 0xFF00) + (result >> 8) + if (self.debug): + print "I2C: Device 0x%02X returned 0x%04X from reg 0x%02X" % (self.address, result & 0xFFFF, reg) + return result + except IOError, err: + return self.errMsg() + + def readS16(self, reg, little_endian=True): + "Reads a signed 16-bit value from the I2C device" + try: + result = self.readU16(reg,little_endian) + if result > 32767: result -= 65536 + return result + except IOError, err: + return self.errMsg() + +if __name__ == '__main__': + try: + bus = Adafruit_I2C(address=0) + print "Default I2C bus is accessible" + except: + print "Error accessing default I2C bus" diff --git a/Adafruit_PWM_Servo_Driver.py b/Adafruit_PWM_Servo_Driver.py new file mode 100644 index 0000000..35c993c --- /dev/null +++ b/Adafruit_PWM_Servo_Driver.py @@ -0,0 +1,92 @@ +#!/usr/bin/python + +import time +import math +from Adafruit_I2C import Adafruit_I2C + +# ============================================================================ +# Adafruit PCA9685 16-Channel PWM Servo Driver +# ============================================================================ + +class PWM : + # Registers/etc. + __MODE1 = 0x00 + __MODE2 = 0x01 + __SUBADR1 = 0x02 + __SUBADR2 = 0x03 + __SUBADR3 = 0x04 + __PRESCALE = 0xFE + __LED0_ON_L = 0x06 + __LED0_ON_H = 0x07 + __LED0_OFF_L = 0x08 + __LED0_OFF_H = 0x09 + __ALL_LED_ON_L = 0xFA + __ALL_LED_ON_H = 0xFB + __ALL_LED_OFF_L = 0xFC + __ALL_LED_OFF_H = 0xFD + + # Bits + __RESTART = 0x80 + __SLEEP = 0x10 + __ALLCALL = 0x01 + __INVRT = 0x10 + __OUTDRV = 0x04 + + general_call_i2c = Adafruit_I2C(0x00) + + @classmethod + def softwareReset(cls): + "Sends a software reset (SWRST) command to all the servo drivers on the bus" + cls.general_call_i2c.writeRaw8(0x06) # SWRST + + def __init__(self, address=0x40, debug=False): + self.i2c = Adafruit_I2C(address) + self.i2c.debug = debug + self.address = address + self.debug = debug + if (self.debug): + print "Reseting PCA9685 MODE1 (without SLEEP) and MODE2" + self.setAllPWM(0, 0) + self.i2c.write8(self.__MODE2, self.__OUTDRV) + self.i2c.write8(self.__MODE1, self.__ALLCALL) + time.sleep(0.005) # wait for oscillator + + mode1 = self.i2c.readU8(self.__MODE1) + mode1 = mode1 & ~self.__SLEEP # wake up (reset sleep) + self.i2c.write8(self.__MODE1, mode1) + time.sleep(0.005) # wait for oscillator + + def setPWMFreq(self, freq): + "Sets the PWM frequency" + prescaleval = 25000000.0 # 25MHz + prescaleval /= 4096.0 # 12-bit + prescaleval /= float(freq) + prescaleval -= 1.0 + if (self.debug): + print "Setting PWM frequency to %d Hz" % freq + print "Estimated pre-scale: %d" % prescaleval + prescale = math.floor(prescaleval + 0.5) + if (self.debug): + print "Final pre-scale: %d" % prescale + + oldmode = self.i2c.readU8(self.__MODE1); + newmode = (oldmode & 0x7F) | 0x10 # sleep + self.i2c.write8(self.__MODE1, newmode) # go to sleep + self.i2c.write8(self.__PRESCALE, int(math.floor(prescale))) + self.i2c.write8(self.__MODE1, oldmode) + time.sleep(0.005) + self.i2c.write8(self.__MODE1, oldmode | 0x80) + + def setPWM(self, channel, on, off): + "Sets a single PWM channel" + self.i2c.write8(self.__LED0_ON_L+4*channel, on & 0xFF) + self.i2c.write8(self.__LED0_ON_H+4*channel, on >> 8) + self.i2c.write8(self.__LED0_OFF_L+4*channel, off & 0xFF) + self.i2c.write8(self.__LED0_OFF_H+4*channel, off >> 8) + + def setAllPWM(self, on, off): + "Sets a all PWM channels" + self.i2c.write8(self.__ALL_LED_ON_L, on & 0xFF) + self.i2c.write8(self.__ALL_LED_ON_H, on >> 8) + self.i2c.write8(self.__ALL_LED_OFF_L, off & 0xFF) + self.i2c.write8(self.__ALL_LED_OFF_H, off >> 8) diff --git a/arm.py b/arm.py new file mode 100644 index 0000000..39be508 --- /dev/null +++ b/arm.py @@ -0,0 +1,48 @@ +#!/usr/bin/python + + +from flask import Flask, render_template, request, Response, send_file + +import os +import atexit + +from Adafruit_PWM_Servo_Driver import PWM +import time + +pwm = PWM(0x40, debug=True) + +servoMin = 150 # Min pulse length out of 4096 +servoMax = 600 # Max pulse length out of 4096 + +pwm.setPWMFreq(60) # Set frequency to 60 Hz + +app = Flask(__name__) + +def setServoPulse(channel, pulse): + pulseLength = 1000000 # 1,000,000 us per second + pulseLength /= 60 # 60 Hz + print "%d us per period" % pulseLength + pulseLength /= 4096 # 12 bits of resolution + print "%d us per bit" % pulseLength + pulse *= 1000 + pulse /= pulseLength + pwm.setPWM(channel, 0, pulse) + + +@app.route("/") +def main(): + return render_template('arm.html') + +@app.route('/slider') +def slider(): + num = int(request.args.get('num')) + value = int(request.args.get('value')) + print("%i %i" % (num, value)) + fraction = int(servoMin + (value/100.0)*(servoMax - servoMin)) + print("Setting servo to %i" % fraction) + pwm.setPWM(num, 0, fraction) + return "" + + +if __name__ == "__main__": + app.run() diff --git a/drive.py b/drive.py index d047972..a2b8789 100644 --- a/drive.py +++ b/drive.py @@ -26,9 +26,9 @@ def turnOffMotors(): ################################# DC motor test! mFL = mh.getMotor(1) -mBL = mh.getMotor(2) +mBL = mh.getMotor(4) mBR = mh.getMotor(3) -mFR = mh.getMotor(4) +mFR = mh.getMotor(2) def wakeup(m): # set the speed to start, from 0 (off) to 255 (max speed) diff --git a/templates/arm.html b/templates/arm.html new file mode 100644 index 0000000..2ffecd7 --- /dev/null +++ b/templates/arm.html @@ -0,0 +1,59 @@ + + + + + + + + + + From 8340688087d46001d2e8e71757b92b75f7643e46 Mon Sep 17 00:00:00 2001 From: lukas Date: Tue, 22 Nov 2016 22:22:57 +0000 Subject: [PATCH 02/50] better arm interface --- static/arm.js | 164 +++++++++++++++++++++++++++++++++++++++++++++ static/style.css | 18 +++++ templates/arm.html | 93 +++++++++++++------------ 3 files changed, 227 insertions(+), 48 deletions(-) create mode 100644 static/arm.js create mode 100644 static/style.css diff --git a/static/arm.js b/static/arm.js new file mode 100644 index 0000000..5970d97 --- /dev/null +++ b/static/arm.js @@ -0,0 +1,164 @@ +function call(name) { + console.log(name); + var xhr = new XMLHttpRequest(); + xhr.open('GET', name, true); + xhr.send(); +} + +function saveFields() { + var data = {}; + data.max = []; + data.min = []; + for (i=0; i<5; i++) { + data.min[i] = document.getElementById("tmin" + i).value; + data.max[i] = document.getElementById("tmax" + i).value; + } + localStorage.setItem("myData", JSON.stringify(data)); +} + +function loadFields() { + var data = localStorage.getItem("myData"); + var dataObject; + if (data != null) { + dataObject = JSON.parse(data); + for (i=0; i<5; i++) { + document.getElementById("tmin" + i).value = dataObject.min[i] + document.getElementById("tmax" + i).value = dataObject.max[i] + } + + } else { + for (i=0; i<5; i++) { + document.getElementById("tmin" + i).value = 0 + document.getElementById("tmax" + i).value = 100 + } + } +} + +function slider(num, value) { + console.log(num); + tid = "t" + num; + document.getElementById(tid).value = value; + var xhr = new XMLHttpRequest(); + xhr.open('GET', "http://github.com/slider?num="+num+"&value="+value); + xhr.send(); +} + +function sliderInc(num) { + sid = "s" + num; + v = parseInt(document.getElementById(sid).value); + document.getElementById(sid).value = v+1; + slider(num,document.getElementById(sid).value); +} + +function sliderDec(num) { + sid = "s" + num; + v = parseInt(document.getElementById(sid).value); + document.getElementById(sid).value = v-1; + slider(num,document.getElementById(sid).value); +} + +function sliderReset(num) { + sid = "s" + num; + v = parseInt(document.getElementById(sid).value); + document.getElementById(sid).value = 50; + slider(num,document.getElementById(sid).value); +} + +function randomizeTargets() { + for (i=0; i<5; i++) { + min = parseInt(document.getElementById("tmin" + i).value); + max = parseInt(document.getElementById("tmax" + i).value); + r = Math.floor(Math.random() * (max-min) + min) + document.getElementById("target"+i).value = r + } +} + + + +document.onkeyup = function(e) { + +} + +document.onkeydown = function(e) { + var key = e.keyCode ? e.keyCode : e.which; + console.log(key); + + if (key == 81) { //q + sliderDec(0); + } else if (key == 69) { + sliderInc(0); + } else if (key == 87) { + sliderReset(0); + } + else if (key == 65) { + sliderDec(1); + } else if (key == 68) { + sliderInc(1); + } else if (key == 83) { + sliderReset(1); + } + else if (key == 90) { + sliderDec(2); + } else if (key == 67) { + sliderInc(2); + } else if (key == 88) { + sliderReset(2); + } + else if (key == 84) { + sliderDec(3); + } else if (key == 85) { + sliderInc(3); + } else if (key == 89) { + sliderReset(3); + } + else if (key == 71) { + sliderDec(4); + } else if (key == 74) { + sliderInc(4); + } else if (key == 72) { + sliderReset(4); + } + else if (key == 80) { + randomizeTargets(); + } else if (key == 77) { + moveToTarget(); + } +} + +function stepToTarget() { + stepSize = 1; + for (i=0; i<5; i++) { + stopFlag = true; + pos = parseInt(document.getElementById("s" + i).value); + target = parseInt(document.getElementById("target" + i).value); + if ( pos == target ) { + newPos = target; + } else if ( Math.abs(pos-target) < stepSize) { + newPos = target; + } else if ( pos < target ) { + stopFlag = false; + newPos = pos + stepSize; + } else if ( pos > target ) { + stopFlag = false; + newPos = pos - stepSize; + } + document.getElementById("s" + i).value = newPos; + slider(i,newPos); + if (stopFlag) { + clearFlag(timer); + } + } +} + + + +var timer; +function moveToTarget() { + timer=setInterval(function() { stepToTarget(); }, 100); + +} + + +window.onload = function() { + loadFields(); +} diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..28710c1 --- /dev/null +++ b/static/style.css @@ -0,0 +1,18 @@ +body { + font-family: sans-serif; background: #eee; +} +input[type="number"] { + width: 50px; +} + +.min { + readonly: "readonly" +} + +.max { + readonly: "readonly" +} + +.target { + readonly: "readonly" +} \ No newline at end of file diff --git a/templates/arm.html b/templates/arm.html index 2ffecd7..c1936c4 100644 --- a/templates/arm.html +++ b/templates/arm.html @@ -1,57 +1,54 @@ - + + + - - function slider(num, value) { - console.log(num); - var xhr = new XMLHttpRequest(); - xhr.open('GET', "http://github.com/slider?num="+num+"&value="+value); - xhr.send(); - } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Claw
Wrist
Elbow
Sholder
Rotate
- document.onkeyup = function(e) { - call('stop'); - } - - document.onkeydown = function(e) { - var key = e.keyCode ? e.keyCode : e.which; - console.log(key); - - if (key == 37) { //Left - call('l'); - } else if (key == 38) { //up - call('b'); - } else if (key == 39) {//right - call('r'); - } else if (key == 40) {// down - call('f'); - } else if (key == 90) {//z - call('stop'); - } else if (key == 32) { //space - call('img_rec'); - } - } - - - - - From 0fe52836b04a116b72ecf31374e3d73d0dc36a81 Mon Sep 17 00:00:00 2001 From: lukas Date: Tue, 27 Dec 2016 19:54:55 +0000 Subject: [PATCH 03/50] cleaned up and refactored code --- README.md | 40 +++++++- drive.py | 250 ------------------------------------------------ drive_server.py | 94 ++++++++++++++++++ image.py | 9 +- robot.py | 66 +++++++++++++ server.sh | 2 +- 6 files changed, 206 insertions(+), 255 deletions(-) delete mode 100644 drive.py create mode 100644 drive_server.py create mode 100755 robot.py diff --git a/README.md b/README.md index 5a863e4..fe088f7 100644 --- a/README.md +++ b/README.md @@ -1 +1,39 @@ -# robot \ No newline at end of file +# robot + +This will run a simple robot with a webserver on a raspberry PI with the Adafruit Motor Hat. I wrote this up for myself for fun and to help me remember how I set things up. + +This is all designed for a Raspberry PI 3 with the Adafruit Motor Hat for cars and the Adafruit Servo Hat for arms + +# Programs + +The robot.py program will run commands from the commandline +The drive_server.py runs a web server for driving around + +# Wiring + +M1 - Front Left +M2 - Back Left (optional) +M3 - Back Right (optional) +M4 - Front Right + + +## Install + +To setup the webservice service modify + +/etc/systemd/system/gunicorn.service + +``` +[Unit] +Description=gunicorn daemon +After=network.target + +[Service] +User=pi +Group=www-data +WorkingDirectory=/home/pi/robot +ExecStart=/usr/local/bin/gunicorn --workers 3 --bind unix:/home/pi/drive.sock drive:app + +[Install] +WantedBy=multi-user.target +``` \ No newline at end of file diff --git a/drive.py b/drive.py deleted file mode 100644 index a2b8789..0000000 --- a/drive.py +++ /dev/null @@ -1,250 +0,0 @@ -#!/usr/bin/python -from Adafruit_MotorHAT import Adafruit_MotorHAT, Adafruit_DCMotor - -from flask import Flask, render_template, request, Response, send_file - -from camera_pi import Camera - -app = Flask(__name__) - - -import os -import time -import atexit - -# create a default object, no changes to I2C address or frequency -mh = Adafruit_MotorHAT(addr=0x60) - -# recommended for auto-disabling motors on shutdown! -def turnOffMotors(): - mh.getMotor(1).run(Adafruit_MotorHAT.RELEASE) - mh.getMotor(2).run(Adafruit_MotorHAT.RELEASE) - mh.getMotor(3).run(Adafruit_MotorHAT.RELEASE) - mh.getMotor(4).run(Adafruit_MotorHAT.RELEASE) - -atexit.register(turnOffMotors) - -################################# DC motor test! -mFL = mh.getMotor(1) -mBL = mh.getMotor(4) -mBR = mh.getMotor(3) -mFR = mh.getMotor(2) - -def wakeup(m): - # set the speed to start, from 0 (off) to 255 (max speed) - m.setSpeed(150) - m.run(Adafruit_MotorHAT.FORWARD); - # turn on motor - m.run(Adafruit_MotorHAT.RELEASE); - - -wakeup(mFL) -wakeup(mBL) -wakeup(mFR) -wakeup(mBL) - - - - -def gof(): - mBR.run(Adafruit_MotorHAT.FORWARD) - mBL.run(Adafruit_MotorHAT.FORWARD) - mFL.run(Adafruit_MotorHAT.FORWARD) - mFR.run(Adafruit_MotorHAT.FORWARD) - mBR.setSpeed(200) - mBL.setSpeed(200) - mFR.setSpeed(200) - mFL.setSpeed(200) - -def gob(): - mBR.run(Adafruit_MotorHAT.BACKWARD) - mBL.run(Adafruit_MotorHAT.BACKWARD) - mFR.run(Adafruit_MotorHAT.BACKWARD) - mFL.run(Adafruit_MotorHAT.BACKWARD) - mBR.setSpeed(200) - mBL.setSpeed(200) - mFR.setSpeed(200) - mFL.setSpeed(200) - -def gol(): - mBR.run(Adafruit_MotorHAT.BACKWARD) - mBL.run(Adafruit_MotorHAT.FORWARD) - mFR.run(Adafruit_MotorHAT.BACKWARD) - mFL.run(Adafruit_MotorHAT.FORWARD) - mBR.setSpeed(200) - mBL.setSpeed(200) - mFR.setSpeed(200) - mFL.setSpeed(200) - -def gor(): - mBR.run(Adafruit_MotorHAT.FORWARD) - mBL.run(Adafruit_MotorHAT.BACKWARD) - mFR.run(Adafruit_MotorHAT.FORWARD) - mFL.run(Adafruit_MotorHAT.BACKWARD) - mBR.setSpeed(200) - mBL.setSpeed(200) - mFR.setSpeed(200) - mFL.setSpeed(200) - -def stop(): - mFL.run(Adafruit_MotorHAT.RELEASE) - mFR.run(Adafruit_MotorHAT.RELEASE) - mBL.run(Adafruit_MotorHAT.RELEASE) - mBR.run(Adafruit_MotorHAT.RELEASE) - - - -def forward(speed, dur): - print "Forward! " - mR.run(Adafruit_MotorHAT.FORWARD) - mL.run(Adafruit_MotorHAT.FORWARD) - mR.setSpeed(speed) - mL.setSpeed(speed) - time.sleep(dur) - - mL.run(Adafruit_MotorHAT.RELEASE) - mR.run(Adafruit_MotorHAT.RELEASE) - return '' - -def backward(speed, dur): - print "Backward! " - mR.run(Adafruit_MotorHAT.BACKWARD) - mL.run(Adafruit_MotorHAT.BACKWARD) - mR.setSpeed(speed) - mL.setSpeed(speed) - time.sleep(dur) - - mL.run(Adafruit_MotorHAT.RELEASE) - mR.run(Adafruit_MotorHAT.RELEASE) - return '' - -def left(speed, dur): - print "Left " - mR.run(Adafruit_MotorHAT.BACKWARD) - mR.setSpeed(speed) - - time.sleep(dur) - mR.run(Adafruit_MotorHAT.RELEASE) - return '' - -def right(speed, dur): - print "Right " - mL.run(Adafruit_MotorHAT.BACKWARD) - mL.setSpeed(speed) - - time.sleep(dur) - mL.run(Adafruit_MotorHAT.RELEASE) - return '' - - - - -#forward(200, 3) -#backward(200, 3) - - -@app.route("/") -def main(): - return render_template('index.html') - -@app.route('/forward') -def fward(): - forward(200, 3) - return '' - -@app.route('/backward') -def bward(): - backward(200, 3) - return '' - -@app.route('/left') -def l(): - left(200, 3) - return '' - -@app.route('/forward') -def r(): - right(200, 3) - return '' - -@app.route('/f') -def gof_r(): - gof() - return '' - -@app.route('/b') -def gob_r(): - gob() - return '' - -@app.route('/r') -def gor_r(): - gor() - return '' - -@app.route('/l') -def gol_r(): - gol() - return '' - -@app.route('/stop') -def stop_r(): - stop() - return '' - -@app.route('/setSpeed') -def setSpeed(): - print 'gotParams'; - print request.query_string - print request.args - print request.args.getlist('name[]') - # print request.form.get('left',1,type=int) - - #print request.form.get('right',1,type=int) - print request.args['right'] - print request.args['left'] - - return '' - -@app.route('/latest.jpg') -def latest(): -# img_num = request.args.get('i') -# if img_num is None: - filename = 'images/latest_img.jpg' -# else: -# filename = 'images/img'+img_num+'.jpg' - - return send_file(filename, mimetype='image/jpg') - -@app.route('/data') -def data(): - img_num = request.args.get('i') - if img_num is None: - filename = 'images/latest_data' - else: - filename = 'images/data'+img_num - - f = open(filename, 'r') - data = f.read() - return data - -@app.route('/img_rec') -def img_rec(): - os.system('python image.py') - -def gen(camera): - """Video streaming generator function.""" - while True: - frame = camera.get_frame() - yield (b'--frame\r\n' - b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n') - time.sleep(0.5) - -@app.route('/video_feed') -def video_feed(): - """Video streaming route. Put this in the src attribute of an img tag.""" - return Response(gen(Camera()), - mimetype='multipart/x-mixed-replace; boundary=frame') - -if __name__ == "__main__": - app.run() diff --git a/drive_server.py b/drive_server.py new file mode 100644 index 0000000..95b4d3d --- /dev/null +++ b/drive_server.py @@ -0,0 +1,94 @@ +#!/usr/bin/python + +from flask import Flask, render_template, request, Response, send_file +from camera_pi import Camera +import wheels + +app = Flask(__name__) + +@app.route("/") +def main(): + return render_template('index.html') + +@app.route('/forward') +def forward(): + wheels.forward(200, 3) + return '' + +@app.route('/backward') +def backward(): + wheels.backward(200, 3) + return '' + +@app.route('/left') +def left(): + wheels.left(200, 3) + return '' + +@app.route('/right') +def right(): + wheels.right(200, 3) + return '' + +@app.route('/f') +def f(): + wheels.forward(200) + return '' + +@app.route('/b') +def b(): + wheels.backward(200) + return '' + +@app.route('/r') +def r(): + wheels.right(200) + return '' + +@app.route('/l') +def l(): + wheels.left(200) + return '' + +@app.route('/stop') +def stop(): + wheels.stop() + return '' + +@app.route('/latest.jpg') +def latest(): + filename = 'images/latest_img.jpg' + return send_file(filename, mimetype='image/jpg') + +@app.route('/data') +def data(): + img_num = request.args.get('i') + if img_num is None: + filename = 'images/latest_data' + else: + filename = 'images/data'+img_num + + f = open(filename, 'r') + data = f.read() + return data + +@app.route('/img_rec') +def img_rec(): + os.system('python image.py') + +def gen(camera): + """Video streaming generator function.""" + while True: + frame = camera.get_frame() + yield (b'--frame\r\n' + b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n') + time.sleep(0.5) + +@app.route('/video_feed') +def video_feed(): + """Video streaming route. Put this in the src attribute of an img tag.""" + return Response(gen(Camera()), + mimetype='multipart/x-mixed-replace; boundary=frame') + +if __name__ == "__main__": + app.run() diff --git a/image.py b/image.py index 90b8b5e..48ccb61 100644 --- a/image.py +++ b/image.py @@ -17,14 +17,17 @@ def classify(n): dataFile = 'images/data'+suffix latestImage = 'images/latest_img.jpg' latestData = 'images/latest_data' - do('echo "I\'m thinking." | flite') + do('echo "I\'m thinking." | flite -voice slt') do('cp /dev/shm/mjpeg/cam.jpg '+imageFile); do('ln -f '+imageFile+' '+latestImage); + do('echo "thinking" > ' + dataFile); + do('ln -f '+dataFile+' '+latestData); do('bash run_and_parse_inception.sh '+imageFile+ " " +dataFile) - do('ln -f '+dataFile+' '+latestData); - do('{ echo "I think I see a "; cat '+dataFile+' | sed -e \'$ ! s/$/. or maybe a/\'; } | flite') + + + do('{ echo "I think I see ah "; head -1 '+dataFile+' | sed -e \'$ ! s/$/. or maybe a/\'; } | flite -voice slt') do('echo '+suffix+' > images/INDEX') diff --git a/robot.py b/robot.py new file mode 100755 index 0000000..21c09cb --- /dev/null +++ b/robot.py @@ -0,0 +1,66 @@ +#! /usr/bin/python + +import drive +import sys + +usage = ''' +robot + +Tell the robot what to do. + +Commands + +forward +backward +left +right +stop +wheel1 +wheel2 +wheel3 +wheel4 +shake +''' + +print sys.argv + +if (len(sys.argv) == 1): + print usage + exit + +cmd = sys.argv[1] + +if (cmd == 'forward'): + drive.forward(200, 1) + +elif (cmd == 'backward'): + drive.backward(200, 1) + +elif (cmd == 'left'): + drive.left(200, 1) + +elif (cmd == 'right'): + drive.right(200, 1) + +elif (cmd == 'wheel1'): + drive.spinMotor(1, 200, 1) + +elif (cmd == 'wheel2'): + drive.spinMotor(2, 200, 1) + +elif (cmd == 'wheel3'): + drive.spinMotor(3, 200, 1) + +elif (cmd == 'wheel4'): + drive.spinMotor(4, 200, 1) + +elif (cmd == 'shake'): + drive.left(200, 0.25) + drive.right(200, 0.25) + +else: + print usage + + + + diff --git a/server.sh b/server.sh index 076a24c..ce3c1ba 100644 --- a/server.sh +++ b/server.sh @@ -1 +1 @@ -gunicorn -w 4 -b0.0.0.0:8000 --timeout 1000 drive:app +gunicorn -w 4 -b0.0.0.0:8000 --timeout 1000 drive_server:app From 0092b3f11dd37bf6201df8db9f88d463b0e5ff36 Mon Sep 17 00:00:00 2001 From: lukas Date: Tue, 27 Dec 2016 20:01:41 +0000 Subject: [PATCH 04/50] more cleaning and adding requirements --- README.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fe088f7..bb613a2 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,9 @@ This is all designed for a Raspberry PI 3 with the Adafruit Motor Hat for cars a The robot.py program will run commands from the commandline The drive_server.py runs a web server for driving around -# Wiring +# Wiring Wheels + +You can easily change this but this is what wheels.py expects M1 - Front Left M2 - Back Left (optional) @@ -19,7 +21,14 @@ M4 - Front Right ## Install -To setup the webservice service modify + +### nginx + +copy the configuration file at nginx/nginx.conf to /etc/nginx/nginx.conf + +### gunicorn + +To setup the gunicorn webservice service modify /etc/systemd/system/gunicorn.service From ba52ce4f88ecc33884d92b32d4eaf2154ebb0402 Mon Sep 17 00:00:00 2001 From: lukas Date: Tue, 27 Dec 2016 21:59:51 +0000 Subject: [PATCH 05/50] updated sonar --- README.md | 30 ++++++++++++++++++++++-------- drive_safe.py | 1 + sonar.py | 4 ++++ 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index bb613a2..2654397 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,23 @@ This will run a simple robot with a webserver on a raspberry PI with the Adafrui This is all designed for a Raspberry PI 3 with the Adafruit Motor Hat for cars and the Adafruit Servo Hat for arms -# Programs +## Programs -The robot.py program will run commands from the commandline -The drive_server.py runs a web server for driving around +robot.py program will run commands from the commandline +sonar.py tests sonar wired into GPIO ports +drive_server.py runs a web server for driving around +drive_safe.py runs a simple object avoidance algorithm -# Wiring Wheels +## Wiring The Robot +### Sonar + +If you want to use the default sonar configuation + +Left sonar trigger GPIO port 23 echo 24 +Center sonar trigger GPIO port 17 echo 18 +Right sonar trigger GPIO port 22 echo 27 + +### Wheels You can easily change this but this is what wheels.py expects @@ -19,14 +30,17 @@ M3 - Back Right (optional) M4 - Front Right -## Install +## Installation + +### server +To run a webserver in the background with a camera you need to setup gunicorn and nginx -### nginx +#### nginx -copy the configuration file at nginx/nginx.conf to /etc/nginx/nginx.conf +copy the configuration file from nginx/nginx.conf to /etc/nginx/nginx.conf -### gunicorn +#### gunicorn To setup the gunicorn webservice service modify diff --git a/drive_safe.py b/drive_safe.py index 4286fca..1364747 100644 --- a/drive_safe.py +++ b/drive_safe.py @@ -18,6 +18,7 @@ def do(cmd): GPIO.setmode(GPIO.BCM) +#left center right TRIG = [23, 17, 22] ECHO = [24, 18, 27] diff --git a/sonar.py b/sonar.py index e8e4ac5..8a2f7a1 100644 --- a/sonar.py +++ b/sonar.py @@ -3,6 +3,10 @@ import time import atexit +#check a sonar with trigger argv1 and echo argv2 +#example usage +#python sonar.py 22 27 + # recommended for auto-disabling motors on shutdown! def turnOffGPIO(): GPIO.cleanup() From 56e49becc38ecc9da4b3e13731facef671045d9d Mon Sep 17 00:00:00 2001 From: lukas Date: Tue, 27 Dec 2016 22:32:58 +0000 Subject: [PATCH 06/50] fixed gunicorn documentation and configuration file --- README.md | 19 +------------------ gunicorn/gunicorn.service | 2 +- 2 files changed, 2 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 2654397..c6fc04c 100644 --- a/README.md +++ b/README.md @@ -42,21 +42,4 @@ copy the configuration file from nginx/nginx.conf to /etc/nginx/nginx.conf #### gunicorn -To setup the gunicorn webservice service modify - -/etc/systemd/system/gunicorn.service - -``` -[Unit] -Description=gunicorn daemon -After=network.target - -[Service] -User=pi -Group=www-data -WorkingDirectory=/home/pi/robot -ExecStart=/usr/local/bin/gunicorn --workers 3 --bind unix:/home/pi/drive.sock drive:app - -[Install] -WantedBy=multi-user.target -``` \ No newline at end of file +copy configuration file from gunicorn/gunicorn.service /etc/systemd/system/gunicorn.service diff --git a/gunicorn/gunicorn.service b/gunicorn/gunicorn.service index 897b5c5..d48254a 100644 --- a/gunicorn/gunicorn.service +++ b/gunicorn/gunicorn.service @@ -8,7 +8,7 @@ After=network.target User=pi Group=www-data WorkingDirectory=/home/pi/robot -ExecStart=/usr/local/bin/gunicorn --workers 3 --bind unix:/home/pi/drive.sock drive:app +ExecStart=/usr/local/bin/gunicorn --workers 3 --bind unix:/home/pi/drive.sock drive_safe:app [Install] WantedBy=multi-user.target From 985ea0c7fd33e28a5ec9784cdfb8e86591ac1373 Mon Sep 17 00:00:00 2001 From: lukas Date: Tue, 27 Dec 2016 22:40:38 +0000 Subject: [PATCH 07/50] Refactored motor code to wheels.py --- wheels.py | 109 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 wheels.py diff --git a/wheels.py b/wheels.py new file mode 100644 index 0000000..f47913c --- /dev/null +++ b/wheels.py @@ -0,0 +1,109 @@ + +from Adafruit_MotorHAT import Adafruit_MotorHAT, Adafruit_DCMotor + +import os +import time +import atexit + +# create a default object, no changes to I2C address or frequency +mh = Adafruit_MotorHAT(addr=0x60) + +# recommended for auto-disabling motors on shutdown! +def turnOffMotors(): + mh.getMotor(1).run(Adafruit_MotorHAT.RELEASE) + mh.getMotor(2).run(Adafruit_MotorHAT.RELEASE) + mh.getMotor(3).run(Adafruit_MotorHAT.RELEASE) + mh.getMotor(4).run(Adafruit_MotorHAT.RELEASE) + +atexit.register(turnOffMotors) + +################################# DC motor test! +mFL = mh.getMotor(1) +mBL = mh.getMotor(2) +mBR = mh.getMotor(3) +mFR = mh.getMotor(4) + + + +def wakeup(m): + # set the speed to start, from 0 (off) to 255 (max speed) + m.setSpeed(150) + m.run(Adafruit_MotorHAT.FORWARD); + # turn on motor + m.run(Adafruit_MotorHAT.RELEASE); + + +wakeup(mFL) +wakeup(mBL) +wakeup(mFR) +wakeup(mBL) + +def spin(wheel, speed): + if (speed > 0): + wheel.run(Adafruit_MotorHAT.FORWARD) + wheel.setSpeed(speed) + elif (speed < 0): + wheel.run(Adafruit_MotorHAT.BACKWARD) + wheel.setSpeed(-speed) + else: + wheel.run(Adafruit_MotorHAT.RELEASE) + +def spinMotor(motorId=1, speed=200, dur=-1): + m = mh.getMotor(motorId) + spin(m, speed) + if (dur >= 0): + time.sleep(dur) + stop() + + +def stop(): + mFL.run(Adafruit_MotorHAT.RELEASE) + mFR.run(Adafruit_MotorHAT.RELEASE) + mBL.run(Adafruit_MotorHAT.RELEASE) + mBR.run(Adafruit_MotorHAT.RELEASE) + + +def forward(speed=200, dur=-1): + spin(mFR, speed) + spin(mFL, speed) + spin(mBR, speed) + spin(mBL, speed) + + if (dur >= 0): + time.sleep(dur) + stop() + + +def backward(speed, dur=-1): + spin(mFR, -speed) + spin(mFL, -speed) + spin(mBR, -speed) + spin(mBL, -speed) + + if (dur >- 0): + time.sleep(dur) + stop() + + +def left(speed, dur=-1): + spin(mFR, speed) + spin(mFL, -speed) + spin(mBR, speed) + spin(mBL, -speed) + + if (dur >- 0): + time.sleep(dur) + stop() + +def right(speed, dur=-1): + spin(mFR, -speed) + spin(mFL, speed) + spin(mBR, -speed) + spin(mBL, speed) + + if (dur >- 0): + time.sleep(dur) + stop() + + + From 290cf7c78669a56a371e14ccd87325863d1c126f Mon Sep 17 00:00:00 2001 From: lukas Date: Tue, 27 Dec 2016 23:01:14 +0000 Subject: [PATCH 08/50] Added a requirements.txt --- requirements.txt | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..ece1fa0 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +Adafruit-MotorHAT +camera_pi +flask +RPi.GPIO From 99ed23bb6baa21e43f527f70fe56f9e4242155b4 Mon Sep 17 00:00:00 2001 From: lukas Date: Tue, 27 Dec 2016 23:06:35 +0000 Subject: [PATCH 09/50] Fixed changed dependency --- robot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/robot.py b/robot.py index 21c09cb..36e06b6 100755 --- a/robot.py +++ b/robot.py @@ -1,6 +1,6 @@ #! /usr/bin/python -import drive +import wheels import sys usage = ''' From 80ad5a80900eee04602fe80b2df78405d19dbeed Mon Sep 17 00:00:00 2001 From: lukas Date: Tue, 27 Dec 2016 23:19:38 +0000 Subject: [PATCH 10/50] Updated README --- README.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/README.md b/README.md index c6fc04c..160200e 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,32 @@ M4 - Front Right ## Installation +### basic setup + +There are a ton of articles on how to do basic setup of a Raspberry PI - one good one is here https://www.howtoforge.com/tutorial/howto-install-raspbian-on-raspberry-pi/ + +You will need to turn on i2c and optionall the camera + +``` +raspi-config +``` + +Next you will need to download i2c tools and smbus + +``` +sudo apt-get install i2c-tools python-smbus python3-smbus +``` + +Test that your hat is attached and visible with + +``` +sudo i2cdetect -y 1 +``` + + + + + ### server To run a webserver in the background with a camera you need to setup gunicorn and nginx @@ -40,6 +66,14 @@ To run a webserver in the background with a camera you need to setup gunicorn an copy the configuration file from nginx/nginx.conf to /etc/nginx/nginx.conf +``` +sudo cp nginx/nginx.conf /etc/nginx/nginx.conf +``` + #### gunicorn copy configuration file from gunicorn/gunicorn.service /etc/systemd/system/gunicorn.service + +``` +sudo cp gunicorn/gunicorn.service /etc/systemd/system/gunicorn.service +``` \ No newline at end of file From 413809dee852b5d6916b28f83b9b7600319e867e Mon Sep 17 00:00:00 2001 From: lukas Date: Tue, 27 Dec 2016 23:21:09 +0000 Subject: [PATCH 11/50] Substitute wheels for drive --- robot.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/robot.py b/robot.py index 36e06b6..3ccc846 100755 --- a/robot.py +++ b/robot.py @@ -31,32 +31,32 @@ cmd = sys.argv[1] if (cmd == 'forward'): - drive.forward(200, 1) + wheels.forward(200, 1) elif (cmd == 'backward'): - drive.backward(200, 1) + wheels.backward(200, 1) elif (cmd == 'left'): - drive.left(200, 1) + wheels.left(200, 1) elif (cmd == 'right'): - drive.right(200, 1) + wheels.right(200, 1) elif (cmd == 'wheel1'): - drive.spinMotor(1, 200, 1) + wheels.spinMotor(1, 200, 1) elif (cmd == 'wheel2'): - drive.spinMotor(2, 200, 1) + wheels.spinMotor(2, 200, 1) elif (cmd == 'wheel3'): - drive.spinMotor(3, 200, 1) + wheels.spinMotor(3, 200, 1) elif (cmd == 'wheel4'): - drive.spinMotor(4, 200, 1) + wheels.spinMotor(4, 200, 1) elif (cmd == 'shake'): - drive.left(200, 0.25) - drive.right(200, 0.25) + wheels.left(200, 0.25) + wheels.right(200, 0.25) else: print usage From a938db878074ce851a7125fa7e000cae1654252a Mon Sep 17 00:00:00 2001 From: lukas Date: Tue, 27 Dec 2016 23:23:51 +0000 Subject: [PATCH 12/50] More updates to installation notes --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 160200e..78cc92b 100644 --- a/README.md +++ b/README.md @@ -54,9 +54,17 @@ Test that your hat is attached and visible with sudo i2cdetect -y 1 ``` +Install dependencies +``` +pip install -r requirements.txt +``` +At this point you should be able to drive your robot locally, try: +``` +./robot.py forward +``` ### server From 1742ac60af90b1c1dfed069dd4e85ad623ae2564 Mon Sep 17 00:00:00 2001 From: Lukas Date: Wed, 28 Dec 2016 00:02:44 +0000 Subject: [PATCH 13/50] Fixed typo in gunicorn service --- gunicorn/gunicorn.service | 2 +- requirements.txt | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/gunicorn/gunicorn.service b/gunicorn/gunicorn.service index d48254a..8cb269d 100644 --- a/gunicorn/gunicorn.service +++ b/gunicorn/gunicorn.service @@ -8,7 +8,7 @@ After=network.target User=pi Group=www-data WorkingDirectory=/home/pi/robot -ExecStart=/usr/local/bin/gunicorn --workers 3 --bind unix:/home/pi/drive.sock drive_safe:app +ExecStart=/usr/bin/gunicorn --workers 3 --bind unix:/home/pi/drive.sock drive_server:app [Install] WantedBy=multi-user.target diff --git a/requirements.txt b/requirements.txt index ece1fa0..1edb861 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,7 @@ +ez_setup Adafruit-MotorHAT -camera_pi +picamera flask RPi.GPIO +smbus + From badd579218c521f77546ef985b8cb9e36ff9beb2 Mon Sep 17 00:00:00 2001 From: lukas Date: Wed, 28 Dec 2016 00:05:28 +0000 Subject: [PATCH 14/50] More explicit gunicorn instructions --- README.md | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 78cc92b..04aebc2 100644 --- a/README.md +++ b/README.md @@ -72,16 +72,44 @@ To run a webserver in the background with a camera you need to setup gunicorn an #### nginx +install nginx + +``` +sudo apt-get install nginx +``` + copy the configuration file from nginx/nginx.conf to /etc/nginx/nginx.conf ``` sudo cp nginx/nginx.conf /etc/nginx/nginx.conf ``` +restart nginx + +``` +sudo nginx -s reload +``` + #### gunicorn +install gunicorn + +``` +sudo cp gunicorn/gunicorn.service /etc/systemd/system/gunicorn.service +``` + copy configuration file from gunicorn/gunicorn.service /etc/systemd/system/gunicorn.service ``` sudo cp gunicorn/gunicorn.service /etc/systemd/system/gunicorn.service -``` \ No newline at end of file +``` + +start gunicorn service + +``` +sudo systemctl daemon-reload +sudo systemctl start gunicorn +``` + +Your webservice should be started now. You can try driving your robot with buttons or arrow keys + From dce9e5f02ef3cb58f862c1d79cb1c76c5a78b7d4 Mon Sep 17 00:00:00 2001 From: lukas Date: Thu, 29 Dec 2016 04:16:56 +0000 Subject: [PATCH 15/50] updated README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index c6fc04c..71783e6 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,8 @@ To run a webserver in the background with a camera you need to setup gunicorn an #### nginx +Nginx is a lightway fast reverse proxy - we store the camera image in RAM and serve it up directly. This was the only way I was able to get any kind of decent fps from the raspberry pi camera. We also need to proxy to gunicorn so that the user can control the robot from a webpage. + copy the configuration file from nginx/nginx.conf to /etc/nginx/nginx.conf #### gunicorn From a3a6b6d97be918873e793bdf0fc984fa5b3f8297 Mon Sep 17 00:00:00 2001 From: lukas Date: Thu, 29 Dec 2016 04:42:31 +0000 Subject: [PATCH 16/50] More debugging of README --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 3d48447..203509c 100644 --- a/README.md +++ b/README.md @@ -86,4 +86,12 @@ copy configuration file from gunicorn/gunicorn.service /etc/systemd/system/gunic ``` sudo cp gunicorn/gunicorn.service /etc/systemd/system/gunicorn.service +``` + +start the gunicorn service + + +``` +sudo systemctl enable gunicorn +sudo systemctl start gunicorn ``` \ No newline at end of file From a4fcc7c99f31ad4680123b148eb9b55b706e4e11 Mon Sep 17 00:00:00 2001 From: lukas Date: Wed, 4 Jan 2017 03:09:53 +0000 Subject: [PATCH 17/50] Added camera instructions --- README.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 04aebc2..057039c 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ sudo apt-get install i2c-tools python-smbus python3-smbus Test that your hat is attached and visible with ``` -sudo i2cdetect -y 1 +i2cdetect -y 1 ``` Install dependencies @@ -113,3 +113,13 @@ sudo systemctl start gunicorn Your webservice should be started now. You can try driving your robot with buttons or arrow keys +#### camera + +In order to stream from the camera you can use RPi-cam. It's documented at http://elinux.org/RPi-Cam-Web-Interface but you can also just run the following + +``` +git clone https://github.com/silvanmelchior/RPi_Cam_Web_Interface.git +cd RPi_Cam_Web_Interface +chmod u+x *.sh +./install.sh +``` \ No newline at end of file From b53596681b321b22c0089ef78dfcf17eda6afd85 Mon Sep 17 00:00:00 2001 From: Lukas Biewald Date: Wed, 4 Jan 2017 19:30:07 -0800 Subject: [PATCH 18/50] Added an inception server --- inception_server.py | 226 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 226 insertions(+) create mode 100644 inception_server.py diff --git a/inception_server.py b/inception_server.py new file mode 100644 index 0000000..3a2caca --- /dev/null +++ b/inception_server.py @@ -0,0 +1,226 @@ +# Copyright 2015 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +"""Simple image classification server that runs the inception model Inception. + +Run image classification with Inception trained on ImageNet 2012 Challenge data +set. + +This is modified from the classify_image_timed.py script in the tensorflow-on-rasberry-pi +github repository +""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import os.path +import re +import sys +import tarfile + +import numpy as np +from six.moves import urllib +import tensorflow as tf + +FLAGS = tf.app.flags.FLAGS + +tf.app.flags.DEFINE_string( + 'model_dir', '/tmp/imagenet', + """Path to classify_image_graph_def.pb, """ + """imagenet_synset_to_human_label_map.txt, and """ + """imagenet_2012_challenge_label_map_proto.pbtxt.""") +tf.app.flags.DEFINE_string('image_file', '', + """Absolute path to image file.""") +tf.app.flags.DEFINE_integer('num_top_predictions', 5, + """Display this many predictions.""") + +DATA_URL = 'http://download.tensorflow.org/models/image/imagenet/inception-2015-12-05.tgz' +IMAGE_PATH = '/tmp/imagenet/cropped_panda.jpg' + + +class NodeLookup(object): + """Converts integer node ID's to human readable labels.""" + + def __init__(self, + label_lookup_path=None, + uid_lookup_path=None): + if not label_lookup_path: + label_lookup_path = os.path.join( + FLAGS.model_dir, 'imagenet_2012_challenge_label_map_proto.pbtxt') + if not uid_lookup_path: + uid_lookup_path = os.path.join( + FLAGS.model_dir, 'imagenet_synset_to_human_label_map.txt') + self.node_lookup = self.load(label_lookup_path, uid_lookup_path) + + def load(self, label_lookup_path, uid_lookup_path): + """Loads a human readable English name for each softmax node. + + Args: + label_lookup_path: string UID to integer node ID. + uid_lookup_path: string UID to human-readable string. + + Returns: + dict from integer node ID to human-readable string. + """ + if not tf.gfile.Exists(uid_lookup_path): + tf.logging.fatal('File does not exist %s', uid_lookup_path) + if not tf.gfile.Exists(label_lookup_path): + tf.logging.fatal('File does not exist %s', label_lookup_path) + + # Loads mapping from string UID to human-readable string + proto_as_ascii_lines = tf.gfile.GFile(uid_lookup_path).readlines() + uid_to_human = {} + p = re.compile(r'[n\d]*[ \S,]*') + for line in proto_as_ascii_lines: + parsed_items = p.findall(line) + uid = parsed_items[0] + human_string = parsed_items[2] + uid_to_human[uid] = human_string + + # Loads mapping from string UID to integer node ID. + node_id_to_uid = {} + proto_as_ascii = tf.gfile.GFile(label_lookup_path).readlines() + for line in proto_as_ascii: + if line.startswith(' target_class:'): + target_class = int(line.split(': ')[1]) + if line.startswith(' target_class_string:'): + target_class_string = line.split(': ')[1] + node_id_to_uid[target_class] = target_class_string[1:-2] + + # Loads the final mapping of integer node ID to human-readable string + node_id_to_name = {} + for key, val in node_id_to_uid.items(): + if val not in uid_to_human: + tf.logging.fatal('Failed to locate: %s', val) + name = uid_to_human[val] + node_id_to_name[key] = name + + return node_id_to_name + + def id_to_string(self, node_id): + if node_id not in self.node_lookup: + return '' + return self.node_lookup[node_id] + + +def create_graph(): + """Creates a graph from saved GraphDef file and returns a saver.""" + # Creates graph from saved graph_def.pb. + with tf.gfile.FastGFile(os.path.join( + FLAGS.model_dir, 'classify_image_graph_def.pb'), 'rb') as f: + graph_def = tf.GraphDef() + graph_def.ParseFromString(f.read()) + _ = tf.import_graph_def(graph_def, name='') + + +def run_inference_on_image(image): + """Runs inference on an image. + + Args: + image: Image file name. + + Returns: + results of inference + """ + print("Running inference") + if not tf.gfile.Exists(image): + tf.logging.fatal('File does not exist %s', image) + image_data = tf.gfile.FastGFile(image, 'rb').read() + + # Creates graph from saved GraphDef. + create_graph() + + with tf.Session() as sess: + # Some useful tensors: + # 'softmax:0': A tensor containing the normalized prediction across + # 1000 labels. + # 'pool_3:0': A tensor containing the next-to-last layer containing 2048 + # float description of the image. + # 'DecodeJpeg/contents:0': A tensor containing a string providing JPEG + # encoding of the image. + # Runs the softmax tensor by feeding the image_data as input to the graph. + softmax_tensor = sess.graph.get_tensor_by_name('softmax:0') + + predictions = sess.run(softmax_tensor, + {'DecodeJpeg/contents:0': image_data}) + + predictions = np.squeeze(predictions) + + node_lookup = NodeLookup() + + top_k = predictions.argsort()[-1:][::-1] + results = [] + for node_id in top_k: + print(node_id) + human_string = node_lookup.id_to_string(node_id) + score = predictions[node_id] + print('%s (score = %.5f)' % (human_string, score)) + results.append(human_string) + + return results + + + + +def maybe_download_and_extract(): + """Download and extract model tar file.""" + dest_directory = FLAGS.model_dir + if not os.path.exists(dest_directory): + os.makedirs(dest_directory) + filename = DATA_URL.split('/')[-1] + filepath = os.path.join(dest_directory, filename) + if not os.path.exists(filepath): + def _progress(count, block_size, total_size): + sys.stdout.write('\r>> Downloading %s %.1f%%' % ( + filename, float(count * block_size) / float(total_size) * 100.0)) + sys.stdout.flush() + filepath, _ = urllib.request.urlretrieve(DATA_URL, filepath, + reporthook=_progress) + print() + statinfo = os.stat(filepath) + print('Succesfully downloaded', filename, statinfo.st_size, 'bytes.') + tarfile.open(filepath, 'r:gz').extractall(dest_directory) + + +def main(_): + maybe_download_and_extract() + image = (FLAGS.image_file if FLAGS.image_file else + os.path.join(FLAGS.model_dir, 'cropped_panda.jpg')) + run_inference_on_image(image) + + + + + +from flask import Flask +app = Flask(__name__) + +@app.route('/') +def hello_world(): + result = run_inference_on_image(IMAGE_PATH) + print("found") + print(result) + return(result[0]) + + +if __name__ == '__main__': + print("Starting up") + maybe_download_and_extract() + print("Warming up by running an inference") + # "warm up" by running the classifer on an image + run_inference_on_image(IMAGE_PATH) + print("Ready to label!") + app.run(host='0.0.0.0', port=9999) From 6e8430e973f2d91bd2a2223cc2458f5f20c97c5a Mon Sep 17 00:00:00 2001 From: lukas Date: Thu, 5 Jan 2017 03:32:59 +0000 Subject: [PATCH 19/50] Added a few clarifications to README --- README.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f10a401..1ef1ddd 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,19 @@ This will run a simple robot with a webserver on a raspberry PI with the Adafruit Motor Hat. I wrote this up for myself for fun and to help me remember how I set things up. -This is all designed for a Raspberry PI 3 with the Adafruit Motor Hat for cars and the Adafruit Servo Hat for arms +This is all designed for a Raspberry PI 3 with the Adafruit Motor Hat for cars and the Adafruit Servo Hat for arms. + +I use cheap HC-SR04 sonar sensors but I think other ones will work fine. + +Pretty much any chassis with a DC motors (4wd or 2wd) works well. ## Programs robot.py program will run commands from the commandline sonar.py tests sonar wired into GPIO ports -drive_server.py runs a web server for driving around +drive_server.py runs a web server for controlling a robot wheels and arms drive_safe.py runs a simple object avoidance algorithm +inception_server.py runs an image classifying microservice ## Wiring The Robot ### Sonar From e7cd02cf2248c79d1da404e54bc31a3c4bb3fb14 Mon Sep 17 00:00:00 2001 From: lukas Date: Thu, 5 Jan 2017 03:40:41 +0000 Subject: [PATCH 20/50] Cleaned up javascript file for index.html --- static/robot.js | 72 +++++++++++++++++++++++++++++++++++++++++++ templates/index.html | 73 ++------------------------------------------ 2 files changed, 75 insertions(+), 70 deletions(-) create mode 100644 static/robot.js diff --git a/static/robot.js b/static/robot.js new file mode 100644 index 0000000..6a8180f --- /dev/null +++ b/static/robot.js @@ -0,0 +1,72 @@ +function refreshCam() { + var camImg = document.getElementById("camera"); + camImg.src="http://github.com/cam.jpg?t="+new Date().getTime(); +} +function refreshLatest() { + var latestImg = document.getElementById("latest"); + latestImg.src="http://github.com/latest.jpg?t="+new Date().getTime(); + + var xhr = new XMLHttpRequest(); + xhr.open('GET', 'http://github.com/data', true); + xhr.onload = function() { + console.log('HERE'); + var status = xhr.status; + if (status == 200) { + var guesses = document.getElementById("guesses"); + guesses.innerHTML = xhr.response + } + }; + xhr.send(); + +} + +setInterval(refreshCam, 200); +setInterval(refreshLatest, 1001); +function call(name) { + console.log(name); + var xhr = new XMLHttpRequest(); + xhr.open('GET', name, true); + xhr.send(); +} + +function setSpeed(left, right) { + var xhr = new XMLHttpRequest(); + xhr.open('GET', 'http://github.com/setSpeed?left='+left+'&right='+right, true); + xhr.send(); +} + +document.onkeyup = function(e) { + call('stop'); +} + +document.onkeydown = function(e) { + var key = e.keyCode ? e.keyCode : e.which; + console.log(key); + + if (key == 37) { //Left + call('l'); + } else if (key == 38) { //up + call('b'); + } else if (key == 39) {//right + call('r'); + } else if (key == 40) {// down + call('f'); + } else if (key == 90) {//z + call('stop'); + } else if (key == 32) { //space + call('img_rec'); + } +} + +if (window.DeviceMotionEvent != undefined) { + window.ondevicemotion = function(e) { + ax = event.accelerationIncludingGravity.x * 5; + ay = event.accelerationIncludingGravity.y * 5; + + left = ax + ay; + right = -ax + ay; + + + setSpeed(left, right); + } +} diff --git a/templates/index.html b/templates/index.html index 6476a9f..14b48a3 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,79 +1,12 @@ - - - + - function setSpeed(left, right) { - var xhr = new XMLHttpRequest(); - xhr.open('GET', 'http://github.com/setSpeed?left='+left+'&right='+right, true); - xhr.send(); - } - - document.onkeyup = function(e) { - call('stop'); - } - - document.onkeydown = function(e) { - var key = e.keyCode ? e.keyCode : e.which; - console.log(key); + - if (key == 37) { //Left - call('l'); - } else if (key == 38) { //up - call('b'); - } else if (key == 39) {//right - call('r'); - } else if (key == 40) {// down - call('f'); - } else if (key == 90) {//z - call('stop'); - } else if (key == 32) { //space - call('img_rec'); - } - } - if (window.DeviceMotionEvent != undefined) { - window.ondevicemotion = function(e) { - ax = event.accelerationIncludingGravity.x * 5; - ay = event.accelerationIncludingGravity.y * 5; - - left = ax + ay; - right = -ax + ay; - setSpeed(left, right); - } - } From 9ca1460a9e943c763800f1b19c739c2d14602f29 Mon Sep 17 00:00:00 2001 From: lukas Date: Thu, 5 Jan 2017 04:11:24 +0000 Subject: [PATCH 21/50] Added inception service and renamed gunicorn service --- README.md | 29 +++++++++++++++++++++++++---- gunicorn/gunicorn.service | 14 -------------- inception_server.py | 2 +- 3 files changed, 26 insertions(+), 19 deletions(-) delete mode 100644 gunicorn/gunicorn.service diff --git a/README.md b/README.md index 1ef1ddd..d46f78b 100644 --- a/README.md +++ b/README.md @@ -102,15 +102,15 @@ sudo cp gunicorn/gunicorn.service /etc/systemd/system/gunicorn.service copy configuration file from gunicorn/gunicorn.service /etc/systemd/system/gunicorn.service ``` -sudo cp gunicorn/gunicorn.service /etc/systemd/system/gunicorn.service +sudo cp services/web.service /etc/systemd/system/web.service ``` -start gunicorn service +start gunicorn web app service ``` sudo systemctl daemon-reload -sudo systemctl enable gunicorn -sudo systemctl start gunicorn +sudo systemctl enable web +sudo systemctl start web ``` Your webservice should be started now. You can try driving your robot with buttons or arrow keys @@ -128,4 +128,25 @@ chmod u+x *.sh Now a stream of images from the camera should be constantly updating the file at /dev/shm/mjpeg. Nginx will serve up the image directly if you request localhost/cam.jpg. +#### tensorflow + +There is a great project at https://github.com/samjabrahams/tensorflow-on-raspberry-pi that gives instructions on installing tensorflow on the Raspberry PI. Recently it's gotten much easier, just do + +``` +wget https://github.com/samjabrahams/tensorflow-on-raspberry-pi/releases/download/v0.11.0/tensorflow-0.11.0-cp27-none-linux_armv7l.whl +sudo pip install tensorflow-0.11.0-cp27-none-linux_armv7l.whl +``` + +Next start a tensorflow service that loads up an inception model and does object recognition the the inception model + +``` +sudo cp services/inception.service /etc/systemd/system/inception.service +sudo systemctl daemon-reload +sudo systemctl enable inception +sudo systemctl start inception +``` + + + + diff --git a/gunicorn/gunicorn.service b/gunicorn/gunicorn.service deleted file mode 100644 index 8cb269d..0000000 --- a/gunicorn/gunicorn.service +++ /dev/null @@ -1,14 +0,0 @@ -#goes in /etc/systemd/system/gunicorn.service - -[Unit] -Description=gunicorn daemon -After=network.target - -[Service] -User=pi -Group=www-data -WorkingDirectory=/home/pi/robot -ExecStart=/usr/bin/gunicorn --workers 3 --bind unix:/home/pi/drive.sock drive_server:app - -[Install] -WantedBy=multi-user.target diff --git a/inception_server.py b/inception_server.py index 3a2caca..50916fb 100644 --- a/inception_server.py +++ b/inception_server.py @@ -48,7 +48,7 @@ """Display this many predictions.""") DATA_URL = 'http://download.tensorflow.org/models/image/imagenet/inception-2015-12-05.tgz' -IMAGE_PATH = '/tmp/imagenet/cropped_panda.jpg' +IMAGE_PATH = '/dev/shm/mjpeg/cam.jpg' class NodeLookup(object): From 86731120aebcc83905e44bac7cedf2bea8c8e165 Mon Sep 17 00:00:00 2001 From: lukas Date: Thu, 5 Jan 2017 04:11:58 +0000 Subject: [PATCH 22/50] Added inception service and renamed gunicorn service --- services/gunicorn.service~ | 12 ++++++++++++ services/inception.service | 14 ++++++++++++++ services/inception.service~ | 14 ++++++++++++++ services/web.service | 14 ++++++++++++++ services/web.service~ | 14 ++++++++++++++ 5 files changed, 68 insertions(+) create mode 100644 services/gunicorn.service~ create mode 100644 services/inception.service create mode 100644 services/inception.service~ create mode 100644 services/web.service create mode 100644 services/web.service~ diff --git a/services/gunicorn.service~ b/services/gunicorn.service~ new file mode 100644 index 0000000..62d85a1 --- /dev/null +++ b/services/gunicorn.service~ @@ -0,0 +1,12 @@ +[Unit] +Description=gunicorn daemon +After=network.target + +[Service] +User=pi +Group=www-data +WorkingDirectory=/home/pi/robot +ExecStart=/usr/local/bin/gunicorn --workers 3 --bind unix:/home/pi/drive.sock drive:app + +[Install] +WantedBy=multi-user.target diff --git a/services/inception.service b/services/inception.service new file mode 100644 index 0000000..d4f0893 --- /dev/null +++ b/services/inception.service @@ -0,0 +1,14 @@ +#goes in /etc/systemd/system/inception.service + +[Unit] +Description=inception daemon +After=network.target + +[Service] +User=pi +Group=www-data +WorkingDirectory=/home/pi/robot +ExecStart=/usr/local/bin/gunicorn --workers 1 -b0.0.0.0:9999 --timeout 10000 inception_server:app + +[Install] +WantedBy=multi-user.target diff --git a/services/inception.service~ b/services/inception.service~ new file mode 100644 index 0000000..4daf873 --- /dev/null +++ b/services/inception.service~ @@ -0,0 +1,14 @@ +#goes in /etc/systemd/system/inception.service + +[Unit] +Description=inception daemon +After=network.target + +[Service] +User=pi +Group=www-data +WorkingDirectory=/home/pi/robot +ExecStart=/usr/bin/gunicorn --workers 1 -b0.0.0.0:9999 --timeout 10000 inception_server:app + +[Install] +WantedBy=multi-user.target diff --git a/services/web.service b/services/web.service new file mode 100644 index 0000000..6d16460 --- /dev/null +++ b/services/web.service @@ -0,0 +1,14 @@ +#goes in /etc/systemd/system/web.service + +[Unit] +Description=gunicorn web daemon +After=network.target + +[Service] +User=pi +Group=www-data +WorkingDirectory=/home/pi/robot +ExecStart=/usr/bin/gunicorn --workers 3 --bind unix:/home/pi/drive.sock drive_server:app + +[Install] +WantedBy=multi-user.target diff --git a/services/web.service~ b/services/web.service~ new file mode 100644 index 0000000..8cb269d --- /dev/null +++ b/services/web.service~ @@ -0,0 +1,14 @@ +#goes in /etc/systemd/system/gunicorn.service + +[Unit] +Description=gunicorn daemon +After=network.target + +[Service] +User=pi +Group=www-data +WorkingDirectory=/home/pi/robot +ExecStart=/usr/bin/gunicorn --workers 3 --bind unix:/home/pi/drive.sock drive_server:app + +[Install] +WantedBy=multi-user.target From c8ccf271aa28cd7025602d63c6a523e47e0e539e Mon Sep 17 00:00:00 2001 From: Lukas Date: Mon, 16 Jan 2017 23:11:39 +0000 Subject: [PATCH 23/50] Removed erroneous backup files --- services/gunicorn.service~ | 12 ------------ services/inception.service~ | 14 -------------- services/web.service~ | 14 -------------- 3 files changed, 40 deletions(-) delete mode 100644 services/gunicorn.service~ delete mode 100644 services/inception.service~ delete mode 100644 services/web.service~ diff --git a/services/gunicorn.service~ b/services/gunicorn.service~ deleted file mode 100644 index 62d85a1..0000000 --- a/services/gunicorn.service~ +++ /dev/null @@ -1,12 +0,0 @@ -[Unit] -Description=gunicorn daemon -After=network.target - -[Service] -User=pi -Group=www-data -WorkingDirectory=/home/pi/robot -ExecStart=/usr/local/bin/gunicorn --workers 3 --bind unix:/home/pi/drive.sock drive:app - -[Install] -WantedBy=multi-user.target diff --git a/services/inception.service~ b/services/inception.service~ deleted file mode 100644 index 4daf873..0000000 --- a/services/inception.service~ +++ /dev/null @@ -1,14 +0,0 @@ -#goes in /etc/systemd/system/inception.service - -[Unit] -Description=inception daemon -After=network.target - -[Service] -User=pi -Group=www-data -WorkingDirectory=/home/pi/robot -ExecStart=/usr/bin/gunicorn --workers 1 -b0.0.0.0:9999 --timeout 10000 inception_server:app - -[Install] -WantedBy=multi-user.target diff --git a/services/web.service~ b/services/web.service~ deleted file mode 100644 index 8cb269d..0000000 --- a/services/web.service~ +++ /dev/null @@ -1,14 +0,0 @@ -#goes in /etc/systemd/system/gunicorn.service - -[Unit] -Description=gunicorn daemon -After=network.target - -[Service] -User=pi -Group=www-data -WorkingDirectory=/home/pi/robot -ExecStart=/usr/bin/gunicorn --workers 3 --bind unix:/home/pi/drive.sock drive_server:app - -[Install] -WantedBy=multi-user.target From 1c8613a85c469f65d04067768019419a18bd90c5 Mon Sep 17 00:00:00 2001 From: Lukas Date: Mon, 16 Jan 2017 23:13:20 +0000 Subject: [PATCH 24/50] Fix readme typos --- README.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/README.md b/README.md index d46f78b..87b48b9 100644 --- a/README.md +++ b/README.md @@ -95,11 +95,8 @@ sudo nginx -s reload install gunicorn -``` -sudo cp gunicorn/gunicorn.service /etc/systemd/system/gunicorn.service -``` -copy configuration file from gunicorn/gunicorn.service /etc/systemd/system/gunicorn.service +copy configuration file from services/web.service /etc/systemd/system/web.service ``` sudo cp services/web.service /etc/systemd/system/web.service From d94e7a34c451f4b9d46e3c035ce20e7df33547ee Mon Sep 17 00:00:00 2001 From: Lukas Date: Mon, 16 Jan 2017 23:21:04 +0000 Subject: [PATCH 25/50] Added shaking --- drive_server.py | 7 +++++++ robot.py | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/drive_server.py b/drive_server.py index 95b4d3d..2c5e5b2 100644 --- a/drive_server.py +++ b/drive_server.py @@ -20,6 +20,13 @@ def backward(): wheels.backward(200, 3) return '' + +@app.route('/shake') +def shake(): + wheels.left(200, 0.2) + wheels.right(200, 0.2) + return '' + @app.route('/left') def left(): wheels.left(200, 3) diff --git a/robot.py b/robot.py index 3ccc846..42ec4a5 100755 --- a/robot.py +++ b/robot.py @@ -55,8 +55,8 @@ wheels.spinMotor(4, 200, 1) elif (cmd == 'shake'): - wheels.left(200, 0.25) - wheels.right(200, 0.25) + wheels.left(200, 0.2) + wheels.right(200, 0.2) else: print usage From 57864bf605b4d7bf094b9fe48a1d9191c1084f19 Mon Sep 17 00:00:00 2001 From: lukas Date: Tue, 17 Jan 2017 02:39:11 +0000 Subject: [PATCH 26/50] Added speaking ability --- drive_server.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/drive_server.py b/drive_server.py index 95b4d3d..2e97347 100644 --- a/drive_server.py +++ b/drive_server.py @@ -3,6 +3,7 @@ from flask import Flask, render_template, request, Response, send_file from camera_pi import Camera import wheels +import speaker app = Flask(__name__) @@ -60,6 +61,12 @@ def latest(): filename = 'images/latest_img.jpg' return send_file(filename, mimetype='image/jpg') +@app.route('/say') +def say(): + text = request.args.get('text') + speaker.say(text) + return '' + @app.route('/data') def data(): img_num = request.args.get('i') From 50dd8a7c49c5ae6a658093c5ed734b73dc86d38e Mon Sep 17 00:00:00 2001 From: lukas Date: Tue, 17 Jan 2017 02:39:33 +0000 Subject: [PATCH 27/50] Added a speaker --- speaker.py | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 speaker.py diff --git a/speaker.py b/speaker.py new file mode 100644 index 0000000..07a09b7 --- /dev/null +++ b/speaker.py @@ -0,0 +1,6 @@ +import subprocess + +def say(text): + subprocess.Popen(['flite', '-t', text]) + + From 0d68302f31ddf9ea5b86e7a27ab5b9b73ba5aa4c Mon Sep 17 00:00:00 2001 From: lukas Date: Thu, 19 Jan 2017 05:53:54 +0000 Subject: [PATCH 28/50] Added ability to set voice --- image.py | 2 +- speaker.py | 6 +++++- wheels.py | 16 ++++++++-------- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/image.py b/image.py index 48ccb61..bef0cce 100644 --- a/image.py +++ b/image.py @@ -27,7 +27,7 @@ def classify(n): - do('{ echo "I think I see ah "; head -1 '+dataFile+' | sed -e \'$ ! s/$/. or maybe a/\'; } | flite -voice slt') + do('{ echo "I think I see ah "; head -1 '+dataFile+' | sed -e \'$ ! s/$/. or maybe a/\'; } | flite -voice slt') do('echo '+suffix+' > images/INDEX') diff --git a/speaker.py b/speaker.py index 07a09b7..5141607 100644 --- a/speaker.py +++ b/speaker.py @@ -1,6 +1,10 @@ import subprocess def say(text): - subprocess.Popen(['flite', '-t', text]) + voice = os.environ['VOICE']: + if voice: + subprocess.Popen(['flite', '-voice', voice,'-t', text]) + else: + subprocess.Popen(['flite', '-t', text]) diff --git a/wheels.py b/wheels.py index f47913c..b49f5db 100644 --- a/wheels.py +++ b/wheels.py @@ -86,20 +86,20 @@ def backward(speed, dur=-1): def left(speed, dur=-1): - spin(mFR, speed) - spin(mFL, -speed) - spin(mBR, speed) - spin(mBL, -speed) + spin(mFR, -speed) + spin(mFL, speed) + spin(mBR, -speed) + spin(mBL, speed) if (dur >- 0): time.sleep(dur) stop() def right(speed, dur=-1): - spin(mFR, -speed) - spin(mFL, speed) - spin(mBR, -speed) - spin(mBL, speed) + spin(mFR, speed) + spin(mFL, -speed) + spin(mBR, speed) + spin(mBL, -speed) if (dur >- 0): time.sleep(dur) From 3ea58b46b2da7d84765ab041a04cbf6df0975281 Mon Sep 17 00:00:00 2001 From: lukas Date: Thu, 19 Jan 2017 06:15:33 +0000 Subject: [PATCH 29/50] Fixed template --- templates/index.html | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/templates/index.html b/templates/index.html index 14b48a3..a11c18f 100644 --- a/templates/index.html +++ b/templates/index.html @@ -8,13 +8,12 @@ - - + + - -

+ From f89d3a426e7c7397c12f35a091875c119d06f79b Mon Sep 17 00:00:00 2001 From: lukas Date: Thu, 19 Jan 2017 06:34:36 +0000 Subject: [PATCH 30/50] cleaned up sonar code --- robot.py | 8 ++++++ sonar.py | 77 ++++++++++++++++++++++++++++++++------------------------ 2 files changed, 52 insertions(+), 33 deletions(-) diff --git a/robot.py b/robot.py index 42ec4a5..afabdef 100755 --- a/robot.py +++ b/robot.py @@ -2,6 +2,7 @@ import wheels import sys +import sonar usage = ''' robot @@ -58,6 +59,13 @@ wheels.left(200, 0.2) wheels.right(200, 0.2) +elif (cmd == 'leftdist'): + print sonar.ldist() +elif (cmd == 'rightdist'): + print sonar.rdist() +elif (cmd == 'centerdist'): + print sonar.cdist() + else: print usage diff --git a/sonar.py b/sonar.py index 8a2f7a1..d4bd113 100644 --- a/sonar.py +++ b/sonar.py @@ -1,55 +1,66 @@ import RPi.GPIO as GPIO -import sys +import os import time -import atexit -#check a sonar with trigger argv1 and echo argv2 -#example usage -#python sonar.py 22 27 +def setup(): + for i in range(3): + GPIO.setup(TRIG[i],GPIO.OUT) + GPIO.setup(ECHO[i],GPIO.IN) + GPIO.output(TRIG[i], False) -# recommended for auto-disabling motors on shutdown! -def turnOffGPIO(): - GPIO.cleanup() + print "Waiting For Sensor To Settle" -atexit.register(turnOffGPIO) +if (os.environ['LTRIG']): + TRIG = [int(os.environ['LTRIG']), + int(os.environ['CTRIG']), + int(os.environ['RTRIG'])] + ECHO = [int(os.environ['LECHO']), + int(os.environ['CECHO']), + int(os.environ['RECHO'])] -GPIO.setmode(GPIO.BCM) + GPIO.setmode(GPIO.BCM) + setup() -TRIG = int(sys.argv[1]) -ECHO = int(sys.argv[2]) -print "Distance Measurement In Progress" -GPIO.setup(TRIG,GPIO.OUT) -GPIO.setup(ECHO,GPIO.IN) +def distance(i): +# print "Distance Measurement In Progress" + print TRIG[i] + GPIO.output(TRIG[i], True) -GPIO.output(TRIG, False) + time.sleep(0.00001) -print "Waiting For Sensor To Settle" + GPIO.output(TRIG[i], False) -time.sleep(2) + pulse_end = 0; + pulse_start = 0; + + while GPIO.input(ECHO[i])==0: + pulse_start = time.time() -GPIO.output(TRIG, True) + while GPIO.input(ECHO[i])==1: + pulse_end = time.time() + + if (pulse_end == 0 or pulse_start==0): + return 1000 + + pulse_duration = pulse_end - pulse_start -time.sleep(0.00001) + distance = pulse_duration * 17150 -GPIO.output(TRIG, False) + distance = round(distance, 2) -while GPIO.input(ECHO)==0: - pulse_start = time.time() + # print "Distance:",distance,"cm" -while GPIO.input(ECHO)==1: - pulse_end = time.time() + return distance -pulse_duration = pulse_end - pulse_start +def ldist(): + return distance(0) -distance = pulse_duration * 17150 +def rdist(): + return distance(2) -distance = round(distance, 2) - -print "Distance:",distance,"cm" - - - +def cdist(): + return distance(1) From 9f75d10d1a171db88840c0ea4fe23d7034ac2e5b Mon Sep 17 00:00:00 2001 From: lukas Date: Thu, 19 Jan 2017 15:27:47 +0000 Subject: [PATCH 31/50] Added autonomous mode --- autonomous.py | 54 +++++++++++++++++++++++++++++++++++++++++++++++++++ sonar.py | 7 ++++++- 2 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 autonomous.py diff --git a/autonomous.py b/autonomous.py new file mode 100644 index 0000000..6eaee2b --- /dev/null +++ b/autonomous.py @@ -0,0 +1,54 @@ + +import wheels +import sonar +import time + + + + + +FORWARD=1 +LEFT=2 +RIGHT=3 +BACKWARD=4 + +def autodrive(dur): + start_time = time.time() + end_time = time.time() + dur + + mode = FORWARD + + wheels.forward(-100) + + + while(time.time() < end_time): + time.sleep(0.1) + cdist = sonar.cdist() + ldist = sonar.ldist() + rdist= sonar.rdist() + + print ("%d %d %d" % (ldist, cdist, rdist)) + + if (mode == FORWARD): + if (cdist < 25 or ldist <4 or rdist < 4): + wheels.stop() + if (ldist < rdist): + mode=RIGHT + wheels.right(-100) + else: + mode=LEFT + wheels.left(-100) + + if (mode==LEFT or mode==RIGHT): + if (cdist > 50): + mode=FORWARD + wheels.forawrd(-100) + + + + wheels.stop() + + + +if (__name__ == '__main__'): + autodrive(10) diff --git a/sonar.py b/sonar.py index d4bd113..b175486 100644 --- a/sonar.py +++ b/sonar.py @@ -1,6 +1,7 @@ import RPi.GPIO as GPIO import os import time +import atexit def setup(): for i in range(3): @@ -21,13 +22,17 @@ def setup(): GPIO.setmode(GPIO.BCM) setup() +def turnOffGPIO(): + GPIO.cleanup() +atexit.register(turnOffGPIO) + def distance(i): # print "Distance Measurement In Progress" - print TRIG[i] + GPIO.output(TRIG[i], True) time.sleep(0.00001) From 172316f7ecb775012c8d87e2227a0e138f62ef66 Mon Sep 17 00:00:00 2001 From: lukas Date: Thu, 19 Jan 2017 15:55:06 +0000 Subject: [PATCH 32/50] Fixed autonomous mode bugs --- autonomous.py | 9 +++++---- drive_server.py | 9 +++++++++ sonar.py | 50 ++++++++++++++++++++++++++++++++++++++++++++++++- speaker.py | 4 ++-- 4 files changed, 65 insertions(+), 7 deletions(-) diff --git a/autonomous.py b/autonomous.py index 6eaee2b..4f27b86 100644 --- a/autonomous.py +++ b/autonomous.py @@ -18,7 +18,7 @@ def autodrive(dur): mode = FORWARD - wheels.forward(-100) + wheels.forward(-150) while(time.time() < end_time): @@ -30,8 +30,9 @@ def autodrive(dur): print ("%d %d %d" % (ldist, cdist, rdist)) if (mode == FORWARD): - if (cdist < 25 or ldist <4 or rdist < 4): - wheels.stop() + if (cdist < 35 or ldist <6 or rdist < 6): + print ("turning") + wheels.stop() if (ldist < rdist): mode=RIGHT wheels.right(-100) @@ -42,7 +43,7 @@ def autodrive(dur): if (mode==LEFT or mode==RIGHT): if (cdist > 50): mode=FORWARD - wheels.forawrd(-100) + wheels.forward(-150) diff --git a/drive_server.py b/drive_server.py index d572cde..33767a9 100644 --- a/drive_server.py +++ b/drive_server.py @@ -4,6 +4,7 @@ from camera_pi import Camera import wheels import speaker +import autonomous app = Flask(__name__) @@ -68,6 +69,14 @@ def latest(): filename = 'images/latest_img.jpg' return send_file(filename, mimetype='image/jpg') + +@app.route('/drive') +def drive(): + time = request.args.get('time') + if time is None: + time = 10 + autodrive(time) + @app.route('/say') def say(): text = request.args.get('text') diff --git a/sonar.py b/sonar.py index b175486..b0ce0b7 100644 --- a/sonar.py +++ b/sonar.py @@ -2,6 +2,7 @@ import os import time import atexit +import sys def setup(): for i in range(3): @@ -11,7 +12,7 @@ def setup(): print "Waiting For Sensor To Settle" -if (os.environ['LTRIG']): +if ('LTRIG' in os.environ): TRIG = [int(os.environ['LTRIG']), int(os.environ['CTRIG']), int(os.environ['RTRIG'])] @@ -28,7 +29,47 @@ def turnOffGPIO(): atexit.register(turnOffGPIO) +def raw_distance(TRIG, ECHO): + #check a sonar with trigger argv1 and echo argv2 + #example usage + #python sonar.py 22 27 + # recommended for auto-disabling motors on shutdown! + GPIO.setmode(GPIO.BCM) + + + print "Distance Measurement In Progress" + + GPIO.setup(TRIG,GPIO.OUT) + + GPIO.setup(ECHO,GPIO.IN) + + GPIO.output(TRIG, False) + + print "Waiting For Sensor To Settle" + + time.sleep(2) + + GPIO.output(TRIG, True) + + time.sleep(0.00001) + + GPIO.output(TRIG, False) + + while GPIO.input(ECHO)==0: + pulse_start = time.time() + + while GPIO.input(ECHO)==1: + pulse_end = time.time() + + pulse_duration = pulse_end - pulse_start + + distance = pulse_duration * 17150 + + distance = round(distance, 2) + + print "Distance:",distance,"cm" + def distance(i): # print "Distance Measurement In Progress" @@ -69,3 +110,10 @@ def rdist(): def cdist(): return distance(1) + + +if __name__ == "__main__": + TRIG = int(sys.argv[1]) + + ECHO = int(sys.argv[2]) + raw_distance(TRIG, ECHO) diff --git a/speaker.py b/speaker.py index 5141607..0bbc555 100644 --- a/speaker.py +++ b/speaker.py @@ -1,8 +1,8 @@ import subprocess def say(text): - voice = os.environ['VOICE']: - if voice: + if 'VOICE' in os.environ: + voice = os.environ['VOICE'] subprocess.Popen(['flite', '-voice', voice,'-t', text]) else: subprocess.Popen(['flite', '-t', text]) From f1797cf27391942cfa651f2da811a8782448275d Mon Sep 17 00:00:00 2001 From: lukas Date: Thu, 19 Jan 2017 16:06:15 +0000 Subject: [PATCH 33/50] Added potential configuration file --- drive_server.py | 10 ++++++---- sonar.py | 5 +++++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/drive_server.py b/drive_server.py index 33767a9..319d469 100644 --- a/drive_server.py +++ b/drive_server.py @@ -72,10 +72,12 @@ def latest(): @app.route('/drive') def drive(): - time = request.args.get('time') - if time is None: - time = 10 - autodrive(time) + time = 10 + if 'time' in request.args: + time = request.args.get('time') + + + autonomous.autodrive(time) @app.route('/say') def say(): diff --git a/sonar.py b/sonar.py index b0ce0b7..16f6518 100644 --- a/sonar.py +++ b/sonar.py @@ -3,6 +3,11 @@ import time import atexit import sys +from subprocess import call + +if (os.path.exists("configure.sh")): + print "Loading configuation file" + call(["bash", "configure.sh"]) def setup(): for i in range(3): From ddc612181124ffce1297e8980a41c14a34c0ea63 Mon Sep 17 00:00:00 2001 From: lukas Date: Thu, 19 Jan 2017 16:22:55 +0000 Subject: [PATCH 34/50] Added configuration file --- drive_server.py | 9 +++++++-- sonar.py | 31 +++++++++++++++++++------------ 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/drive_server.py b/drive_server.py index 319d469..6962ce8 100644 --- a/drive_server.py +++ b/drive_server.py @@ -5,6 +5,7 @@ import wheels import speaker import autonomous +import os app = Flask(__name__) @@ -78,6 +79,8 @@ def drive(): autonomous.autodrive(time) + + return '' @app.route('/say') def say(): @@ -96,10 +99,12 @@ def data(): f = open(filename, 'r') data = f.read() return data - + @app.route('/img_rec') def img_rec(): - os.system('python image.py') + wheels.stop() +# os.system('python image.py') + return '' def gen(camera): """Video streaming generator function.""" diff --git a/sonar.py b/sonar.py index 16f6518..01be3c5 100644 --- a/sonar.py +++ b/sonar.py @@ -4,10 +4,8 @@ import atexit import sys from subprocess import call +import configure -if (os.path.exists("configure.sh")): - print "Loading configuation file" - call(["bash", "configure.sh"]) def setup(): for i in range(3): @@ -17,16 +15,25 @@ def setup(): print "Waiting For Sensor To Settle" -if ('LTRIG' in os.environ): - TRIG = [int(os.environ['LTRIG']), - int(os.environ['CTRIG']), - int(os.environ['RTRIG'])] - ECHO = [int(os.environ['LECHO']), - int(os.environ['CECHO']), - int(os.environ['RECHO'])] +#if ('LTRIG' in os.environ): +# TRIG = [int(os.environ['LTRIG']), +# int(os.environ['CTRIG']), +# int(os.environ['RTRIG'])] +# ECHO = [int(os.environ['LECHO']), +# int(os.environ['CECHO']), +# int(os.environ['RECHO'])] - GPIO.setmode(GPIO.BCM) - setup() + +if ('LTRIG' in configure.data): + TRIG = [int(configure.data['LTRIG']), + int(configure.data['CTRIG']), + int(configure.data['RTRIG'])] + ECHO = [int(configure.data['LECHO']), + int(configure.data['CECHO']), + int(configure.data['RECHO'])] + + GPIO.setmode(GPIO.BCM) + setup() def turnOffGPIO(): GPIO.cleanup() From fdc2462f64bb2529175989bfca343b4b40a76782 Mon Sep 17 00:00:00 2001 From: lukas Date: Thu, 19 Jan 2017 16:28:35 +0000 Subject: [PATCH 35/50] cleaned up --- drive_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drive_server.py b/drive_server.py index 6962ce8..0bdb094 100644 --- a/drive_server.py +++ b/drive_server.py @@ -75,7 +75,7 @@ def latest(): def drive(): time = 10 if 'time' in request.args: - time = request.args.get('time') + time = int(request.args.get('time')) autonomous.autodrive(time) From 126a45eaffb8158567b90f784953b2e4505543c6 Mon Sep 17 00:00:00 2001 From: lukas Date: Thu, 19 Jan 2017 16:34:29 +0000 Subject: [PATCH 36/50] added configure file --- configure.py | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 configure.py diff --git a/configure.py b/configure.py new file mode 100644 index 0000000..5157fdf --- /dev/null +++ b/configure.py @@ -0,0 +1,7 @@ +import json + +data={} + +with open('robot.conf') as data_file: + data = json.load(data_file) + From 80f7b5030cb3ece01a3e36924aff92877275988a Mon Sep 17 00:00:00 2001 From: lukas Date: Thu, 19 Jan 2017 19:15:10 +0000 Subject: [PATCH 37/50] missing import --- autonomous.py | 12 +++++++++--- speaker.py | 5 +++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/autonomous.py b/autonomous.py index 4f27b86..9d90f18 100644 --- a/autonomous.py +++ b/autonomous.py @@ -35,11 +35,17 @@ def autodrive(dur): wheels.stop() if (ldist < rdist): mode=RIGHT - wheels.right(-100) + wheels.backward(-100) + time.sleep(1) + wheels.right(-250) + time.sleep(1) else: mode=LEFT - wheels.left(-100) - + wheels.backward(-100) + time.sleep(1) + wheels.left(-250) + time.sleep(1) + if (mode==LEFT or mode==RIGHT): if (cdist > 50): mode=FORWARD diff --git a/speaker.py b/speaker.py index 0bbc555..4981b52 100644 --- a/speaker.py +++ b/speaker.py @@ -1,8 +1,9 @@ import subprocess +import os def say(text): - if 'VOICE' in os.environ: - voice = os.environ['VOICE'] + if 'VOICE' in os.environ: + voice = os.environ['VOICE'] subprocess.Popen(['flite', '-voice', voice,'-t', text]) else: subprocess.Popen(['flite', '-t', text]) From b212a93618609de813d90d14bc9b4c03f95523bc Mon Sep 17 00:00:00 2001 From: lukas Date: Sun, 12 Feb 2017 21:08:07 +0000 Subject: [PATCH 38/50] Made work without robot.conf file and added default file --- configure.py | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/configure.py b/configure.py index 5157fdf..e494f50 100644 --- a/configure.py +++ b/configure.py @@ -1,7 +1,28 @@ import json +import os.path data={} -with open('robot.conf') as data_file: - data = json.load(data_file) +# Default configuration +# If you want to change make a json file that looks like +# { +# "LTRIG":"17", +# "CTRIG":"23", +# "RTRIG":"22", +# "LECHO":"18", +# "CECHO":"24", +# "RECHO":"27" +# } +data["LTRIG"] = 17 # default pin for left sonar trigger +data["CTRIG"] = 23 # default pin for center sonar trigger +data["RTRIG"] = 22 # default pin for right sonar trigger +data["LTRIG"] = 18 # default pin for left sonar echo +data["CTRIG"] = 24 # default pin for center sonar echo +data["RTRIG"] = 27 # default pin for right sonar echo + +if os.path.isfile('robot.conf'): + with open('robot.conf') as data_file: + data = json.load(data_file) +else: + print("Couldn't find robot.conf file, using default configuration") From c401141d58103b23b7c9b408179f5bec2243ffde Mon Sep 17 00:00:00 2001 From: Lukas Date: Sun, 12 Feb 2017 21:10:00 +0000 Subject: [PATCH 39/50] Made robot.py use configure --- robot.py | 1 + 1 file changed, 1 insertion(+) diff --git a/robot.py b/robot.py index afabdef..94ba8d0 100755 --- a/robot.py +++ b/robot.py @@ -3,6 +3,7 @@ import wheels import sys import sonar +import configure usage = ''' robot From 30042972dd6d26bc89881c3e736201c4c106a5ee Mon Sep 17 00:00:00 2001 From: Lukas Date: Sun, 12 Feb 2017 21:12:50 +0000 Subject: [PATCH 40/50] Fixed robot.py --- configure.py | 6 +++--- robot.py | 15 +++++++++++++-- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/configure.py b/configure.py index e494f50..4af7dae 100644 --- a/configure.py +++ b/configure.py @@ -17,9 +17,9 @@ data["LTRIG"] = 17 # default pin for left sonar trigger data["CTRIG"] = 23 # default pin for center sonar trigger data["RTRIG"] = 22 # default pin for right sonar trigger -data["LTRIG"] = 18 # default pin for left sonar echo -data["CTRIG"] = 24 # default pin for center sonar echo -data["RTRIG"] = 27 # default pin for right sonar echo +data["LECHO"] = 18 # default pin for left sonar echo +data["CECHO"] = 24 # default pin for center sonar echo +data["RECHO"] = 27 # default pin for right sonar echo if os.path.isfile('robot.conf'): with open('robot.conf') as data_file: diff --git a/robot.py b/robot.py index 94ba8d0..e084197 100755 --- a/robot.py +++ b/robot.py @@ -12,23 +12,34 @@ Commands +Actions +------- forward backward left right stop +shake + +Test Wheels +----------- wheel1 wheel2 wheel3 wheel4 -shake + +Test Sonar +---------- +leftdist +rightdist +centerdist ''' print sys.argv if (len(sys.argv) == 1): print usage - exit + exit(1) cmd = sys.argv[1] From ea39b4721c718bae923a6191290267d0c4faa8ff Mon Sep 17 00:00:00 2001 From: Lukas Date: Sun, 12 Feb 2017 21:19:33 +0000 Subject: [PATCH 41/50] Further cleaned up README --- README.md | 23 ++++++++++++++--------- configure.py | 8 ++++---- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 87b48b9..09e8495 100644 --- a/README.md +++ b/README.md @@ -10,20 +10,25 @@ Pretty much any chassis with a DC motors (4wd or 2wd) works well. ## Programs -robot.py program will run commands from the commandline -sonar.py tests sonar wired into GPIO ports -drive_server.py runs a web server for controlling a robot wheels and arms -drive_safe.py runs a simple object avoidance algorithm -inception_server.py runs an image classifying microservice +- robot.py program will run commands from the commandline +- drive_server.py runs a web server for controlling a robot wheels and arms + +- sonar.py tests sonar wired into GPIO ports +- wheels.py tests simple DC motor wheels +- arm.py tests a servo controlled robot arm +- autonomous.py implements a simple driving algorithm using the wheels and sonal +- inception_server.py runs an image classifying microservice ## Wiring The Robot ### Sonar If you want to use the default sonar configuation -Left sonar trigger GPIO port 23 echo 24 -Center sonar trigger GPIO port 17 echo 18 -Right sonar trigger GPIO port 22 echo 27 +Left sonar trigger GPIO pin 23 echo 24 +Center sonar trigger GPIO pin 17 echo 18 +Right sonar trigger GPIO pin 22 echo 27 + +You can modify the pins by making a robot.conf file. ### Wheels @@ -41,7 +46,7 @@ M4 - Front Right There are a ton of articles on how to do basic setup of a Raspberry PI - one good one is here https://www.howtoforge.com/tutorial/howto-install-raspbian-on-raspberry-pi/ -You will need to turn on i2c and optionall the camera +You will need to turn on i2c and optionally the camera ``` raspi-config diff --git a/configure.py b/configure.py index 4af7dae..32a4986 100644 --- a/configure.py +++ b/configure.py @@ -14,11 +14,11 @@ # "RECHO":"27" # } -data["LTRIG"] = 17 # default pin for left sonar trigger -data["CTRIG"] = 23 # default pin for center sonar trigger +data["LTRIG"] = 23 # default pin for left sonar trigger +data["CTRIG"] = 17 # default pin for center sonar trigger data["RTRIG"] = 22 # default pin for right sonar trigger -data["LECHO"] = 18 # default pin for left sonar echo -data["CECHO"] = 24 # default pin for center sonar echo +data["LECHO"] = 24 # default pin for left sonar echo +data["CECHO"] = 18 # default pin for center sonar echo data["RECHO"] = 27 # default pin for right sonar echo if os.path.isfile('robot.conf'): From f816256f017a92938bf463fef0d25ce132fb002b Mon Sep 17 00:00:00 2001 From: Lukas Biewald Date: Sun, 12 Feb 2017 13:54:00 -0800 Subject: [PATCH 42/50] Added picture of a few robots --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 87b48b9..f5e149a 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,12 @@ drive_server.py runs a web server for controlling a robot wheels and arms drive_safe.py runs a simple object avoidance algorithm inception_server.py runs an image classifying microservice +# Example Robots + +Here are two robots I made that use this software + +![Robots](https://joyfulgrit.files.wordpress.com/2013/10/img_0183.jpg?w=700) + ## Wiring The Robot ### Sonar From dc84c63d0e6a2b307a2405b2eb2992c6b8051a89 Mon Sep 17 00:00:00 2001 From: Lukas Date: Mon, 13 Feb 2017 04:14:11 +0000 Subject: [PATCH 43/50] Updated README --- README.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 09e8495..ffc3d6e 100644 --- a/README.md +++ b/README.md @@ -2,17 +2,21 @@ This will run a simple robot with a webserver on a raspberry PI with the Adafruit Motor Hat. I wrote this up for myself for fun and to help me remember how I set things up. -This is all designed for a Raspberry PI 3 with the Adafruit Motor Hat for cars and the Adafruit Servo Hat for arms. +High level overview can be found in this article: https://www.oreilly.com/learning/how-to-build-a-robot-that-sees-with-100-and-tensorflow -I use cheap HC-SR04 sonar sensors but I think other ones will work fine. +## Hardware + +- Raspberry PI 3 +- Adafruit Motor Hat (for wheels) +- Adafruit Servo Hat (for arms) +- HC-SR04 sonars +- Pretty much any chassis with DC motors - for example: https://www.amazon.com/Emgreat-Chassis-Encoder-wheels-Battery/dp/B00GLO5SMY/ref=sr_1_2?ie=UTF8&qid=1486959207&sr=8-2&keywords=robot+chassis +- Any stepper motor arm - I used SainSmart DIY Control Palletizing Robot Arm for the arm (https://www.amazon.com/dp/B0179BTLZ2/ref=twister_B00YTW763Y?_encoding=UTF8&psc=1) -Pretty much any chassis with a DC motors (4wd or 2wd) works well. ## Programs - robot.py program will run commands from the commandline -- drive_server.py runs a web server for controlling a robot wheels and arms - - sonar.py tests sonar wired into GPIO ports - wheels.py tests simple DC motor wheels - arm.py tests a servo controlled robot arm From c9b633a0e76438cc9bb0d0aebfaa3d6c3a42d48f Mon Sep 17 00:00:00 2001 From: Lukas Date: Mon, 13 Feb 2017 04:16:10 +0000 Subject: [PATCH 44/50] Updated README --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e3f0cf1..4ca9057 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ High level overview can be found in this article: https://www.oreilly.com/learni - Pretty much any chassis with DC motors - for example: https://www.amazon.com/Emgreat-Chassis-Encoder-wheels-Battery/dp/B00GLO5SMY/ref=sr_1_2?ie=UTF8&qid=1486959207&sr=8-2&keywords=robot+chassis - Any stepper motor arm - I used SainSmart DIY Control Palletizing Robot Arm for the arm (https://www.amazon.com/dp/B0179BTLZ2/ref=twister_B00YTW763Y?_encoding=UTF8&psc=1) +To get started, you should be able to make the robot work without the arm, sonar and servo hat. ## Programs @@ -32,11 +33,11 @@ Here are two robots I made that use this software ## Wiring The Robot ### Sonar -If you want to use the default sonar configuation +If you want to use the default sonar configuation, wire like this: -Left sonar trigger GPIO pin 23 echo 24 -Center sonar trigger GPIO pin 17 echo 18 -Right sonar trigger GPIO pin 22 echo 27 +- Left sonar trigger GPIO pin 23 echo 24 +- Center sonar trigger GPIO pin 17 echo 18 +- Right sonar trigger GPIO pin 22 echo 27 You can modify the pins by making a robot.conf file. From ac393e43e0b6adaf287903b458a080174db81a90 Mon Sep 17 00:00:00 2001 From: Lukas Date: Mon, 13 Feb 2017 04:19:02 +0000 Subject: [PATCH 45/50] Clarifying README --- README.md | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 4ca9057..a27feab 100644 --- a/README.md +++ b/README.md @@ -45,10 +45,10 @@ You can modify the pins by making a robot.conf file. You can easily change this but this is what wheels.py expects -M1 - Front Left -M2 - Back Left (optional) -M3 - Back Right (optional) -M4 - Front Right +- M1 - Front Left +- M2 - Back Left (optional - leave unwired for 2wd chassis) +- M3 - Back Right (optional - leave unwired for 2wd chassis) +- M4 - Front Right ## Installation @@ -75,6 +75,14 @@ Test that your hat is attached and visible with i2cdetect -y 1 ``` +Install this code + +``` +sudo apt-get install git +git clone https://github.com/lukas/robot.git +cd robot +``` + Install dependencies ``` From c73c8c060beb8a2c43d97dc874542cf0fd0b7644 Mon Sep 17 00:00:00 2001 From: Lukas Date: Mon, 13 Feb 2017 04:19:41 +0000 Subject: [PATCH 46/50] Clarifying README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a27feab..4dfea26 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,7 @@ Nginx is a lightway fast reverse proxy - we store the camera image in RAM and se copy the configuration file from nginx/nginx.conf to /etc/nginx/nginx.conf ``` +sudo apt-get install nginx sudo cp nginx/nginx.conf /etc/nginx/nginx.conf ``` From a673c3a2c63d96e30e00ba0963f1764decdf3fec Mon Sep 17 00:00:00 2001 From: Lukas Date: Mon, 13 Feb 2017 04:20:20 +0000 Subject: [PATCH 47/50] Clarifying README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4dfea26..18f5173 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ To get started, you should be able to make the robot work without the arm, sonar - autonomous.py implements a simple driving algorithm using the wheels and sonal - inception_server.py runs an image classifying microservice -# Example Robots +## Example Robots Here are two robots I made that use this software From 00fd6d2286dc74b711fbc2def44991c56a82ac60 Mon Sep 17 00:00:00 2001 From: Lukas Date: Mon, 13 Feb 2017 04:40:09 +0000 Subject: [PATCH 48/50] Clarifying README --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 18f5173..76712bf 100644 --- a/README.md +++ b/README.md @@ -7,11 +7,13 @@ High level overview can be found in this article: https://www.oreilly.com/learni ## Hardware - Raspberry PI 3 +- 16GB (or larger) SIM Card - Adafruit Motor Hat (for wheels) +- Any chassis with DC motors - for example: https://www.amazon.com/Emgreat-Chassis-Encoder-wheels-Battery/dp/B00GLO5SMY/ref=sr_1_2?ie=UTF8&qid=1486959207&sr=8-2&keywords=robot+chassis - Adafruit Servo Hat (for arms) - HC-SR04 sonars -- Pretty much any chassis with DC motors - for example: https://www.amazon.com/Emgreat-Chassis-Encoder-wheels-Battery/dp/B00GLO5SMY/ref=sr_1_2?ie=UTF8&qid=1486959207&sr=8-2&keywords=robot+chassis -- Any stepper motor arm - I used SainSmart DIY Control Palletizing Robot Arm for the arm (https://www.amazon.com/dp/B0179BTLZ2/ref=twister_B00YTW763Y?_encoding=UTF8&psc=1) +- Any stepper motor arm - for example: SainSmart DIY Control Palletizing Robot Arm for the arm (https://www.amazon.com/dp/B0179BTLZ2/ref=twister_B00YTW763Y?_encoding=UTF8&psc=1) +- Raspberry PI compatible camera - for example: https://www.amazon.com/Raspberry-Pi-Camera-Module-Megapixel/dp/B01ER2SKFS/ref=sr_1_1?s=electronics&ie=UTF8&qid=1486960149&sr=1-1&keywords=raspberry+pi+camera To get started, you should be able to make the robot work without the arm, sonar and servo hat. From c5c14ed14ef80984a4eab7df67e93e56087146d5 Mon Sep 17 00:00:00 2001 From: Lukas Date: Mon, 13 Feb 2017 06:19:34 +0000 Subject: [PATCH 49/50] Removing old files --- DCTest.py | 59 ------------- camera_pi.py | 54 ------------ drive_safe.py | 239 -------------------------------------------------- 3 files changed, 352 deletions(-) delete mode 100644 DCTest.py delete mode 100644 camera_pi.py delete mode 100644 drive_safe.py diff --git a/DCTest.py b/DCTest.py deleted file mode 100644 index bd3b3d7..0000000 --- a/DCTest.py +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/python -from Adafruit_MotorHAT import Adafruit_MotorHAT, Adafruit_DCMotor -import sys -import time -import atexit - -# create a default object, no changes to I2C address or frequency -mh = Adafruit_MotorHAT(addr=0x60) - -# recommended for auto-disabling motors on shutdown! -def turnOffMotors(): - mh.getMotor(1).run(Adafruit_MotorHAT.RELEASE) - mh.getMotor(2).run(Adafruit_MotorHAT.RELEASE) - mh.getMotor(3).run(Adafruit_MotorHAT.RELEASE) - mh.getMotor(4).run(Adafruit_MotorHAT.RELEASE) - -atexit.register(turnOffMotors) - -################################# DC motor test! -print int(sys.argv[1]) -myMotor = mh.getMotor(int(sys.argv[1])) - -# set the speed to start, from 0 (off) to 255 (max speed) -myMotor.setSpeed(150) -myMotor.run(Adafruit_MotorHAT.FORWARD); -# turn on motor -myMotor.run(Adafruit_MotorHAT.RELEASE); - - -while (True): - print "Forward! " - myMotor.run(Adafruit_MotorHAT.FORWARD) - - print "\tSpeed up..." - for i in range(255): - myMotor.setSpeed(i) - time.sleep(0.01) - - print "\tSlow down..." - for i in reversed(range(255)): - myMotor.setSpeed(i) - time.sleep(0.01) - - print "Backward! " - myMotor.run(Adafruit_MotorHAT.BACKWARD) - - print "\tSpeed up..." - for i in range(255): - myMotor.setSpeed(i) - time.sleep(0.01) - - print "\tSlow down..." - for i in reversed(range(255)): - myMotor.setSpeed(i) - time.sleep(0.01) - - print "Release" - myMotor.run(Adafruit_MotorHAT.RELEASE) - time.sleep(1.0) diff --git a/camera_pi.py b/camera_pi.py deleted file mode 100644 index 9dbbbbe..0000000 --- a/camera_pi.py +++ /dev/null @@ -1,54 +0,0 @@ -import time -import io -import threading -import picamera - - -class Camera(object): - thread = None # background thread that reads frames from camera - frame = None # current frame is stored here by background thread - last_access = 0 # time of last client access to the camera - - def initialize(self): - if Camera.thread is None: - # start background frame thread - Camera.thread = threading.Thread(target=self._thread) - Camera.thread.start() - - # wait until frames start to be available - while self.frame is None: - time.sleep(0) - - def get_frame(self): - Camera.last_access = time.time() - self.initialize() - return self.frame - - @classmethod - def _thread(cls): - with picamera.PiCamera() as camera: - # camera setup - camera.resolution = (320, 240) - camera.hflip = True - camera.vflip = True - - # let camera warm up - camera.start_preview() - time.sleep(2) - - stream = io.BytesIO() - for foo in camera.capture_continuous(stream, 'jpeg', - use_video_port=True): - # store frame - stream.seek(0) - cls.frame = stream.read() - - # reset stream for next frame - stream.seek(0) - stream.truncate() - - # if there hasn't been any clients asking for frames in - # the last 10 seconds stop the thread - if time.time() - cls.last_access > 10: - break - cls.thread = None diff --git a/drive_safe.py b/drive_safe.py deleted file mode 100644 index 1364747..0000000 --- a/drive_safe.py +++ /dev/null @@ -1,239 +0,0 @@ -import subprocess - -def do(cmd): - print(cmd) - p =subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) - -from random import random - -from Adafruit_MotorHAT import Adafruit_MotorHAT, Adafruit_DCMotor - -from flask import Flask, render_template, request, Response, send_file - -from camera_pi import Camera - -import RPi.GPIO as GPIO - -import time - -GPIO.setmode(GPIO.BCM) - -#left center right -TRIG = [23, 17, 22] -ECHO = [24, 18, 27] - - -def setup(): - for i in range(3): - GPIO.setup(TRIG[i],GPIO.OUT) - GPIO.setup(ECHO[i],GPIO.IN) - GPIO.output(TRIG[i], False) - - print "Waiting For Sensor To Settle" - - -def distance(i): -# print "Distance Measurement In Progress" - - GPIO.output(TRIG[i], True) - - time.sleep(0.00001) - - GPIO.output(TRIG[i], False) - - pulse_end = 0; - pulse_start = 0; - - while GPIO.input(ECHO[i])==0: - pulse_start = time.time() - - while GPIO.input(ECHO[i])==1: - pulse_end = time.time() - - if (pulse_end == 0 or pulse_start==0): - return 1000 - - pulse_duration = pulse_end - pulse_start - - distance = pulse_duration * 17150 - - distance = round(distance, 2) - - # print "Distance:",distance,"cm" - - return distance - - - - -import os -import time -import atexit - -# create a default object, no changes to I2C address or frequency -mh = Adafruit_MotorHAT(addr=0x60) - -# recommended for auto-disabling motors on shutdown! -def turnOffMotors(): - mh.getMotor(1).run(Adafruit_MotorHAT.RELEASE) - mh.getMotor(2).run(Adafruit_MotorHAT.RELEASE) - mh.getMotor(3).run(Adafruit_MotorHAT.RELEASE) - mh.getMotor(4).run(Adafruit_MotorHAT.RELEASE) - GPIO.cleanup() - -atexit.register(turnOffMotors) - -################################# DC motor test! -mFL = mh.getMotor(1) -mBL = mh.getMotor(2) -mBR = mh.getMotor(3) -mFR = mh.getMotor(4) - -def wakeup(m): - # set the speed to start, from 0 (off) to 255 (max speed) - m.setSpeed(150) - m.run(Adafruit_MotorHAT.FORWARD); - # turn on motor - m.run(Adafruit_MotorHAT.RELEASE); - - -wakeup(mFL) -wakeup(mBL) -wakeup(mFR) -wakeup(mBL) -setup() - -def gof(): - mBR.run(Adafruit_MotorHAT.BACKWARD) - mBL.run(Adafruit_MotorHAT.BACKWARD) - mFL.run(Adafruit_MotorHAT.BACKWARD) - mFR.run(Adafruit_MotorHAT.BACKWARD) - mBR.setSpeed(200) - mBL.setSpeed(200) - mFR.setSpeed(200) - mFL.setSpeed(200) - -def setSpeed(speed): - mBR.setSpeed(speed) - mBL.setSpeed(speed) - mFR.setSpeed(speed) - mFL.setSpeed(speed) - -def backward(speed, dur): - print "Backward! " - mFR.run(Adafruit_MotorHAT.FORWARD) - mFL.run(Adafruit_MotorHAT.FORWARD) - mFR.setSpeed(speed) - mFL.setSpeed(speed) - time.sleep(dur) - - mFL.run(Adafruit_MotorHAT.RELEASE) - mFR.run(Adafruit_MotorHAT.RELEASE) - return '' - -def stop(): - mFL.run(Adafruit_MotorHAT.RELEASE) - mFR.run(Adafruit_MotorHAT.RELEASE) - mBL.run(Adafruit_MotorHAT.RELEASE) - mBR.run(Adafruit_MotorHAT.RELEASE) - -def left(speed, dur): - print "Left " - mFR.run(Adafruit_MotorHAT.BACKWARD) - mFR.setSpeed(speed) - - time.sleep(dur) - mFR.run(Adafruit_MotorHAT.RELEASE) - return '' - -def right(speed, dur): - print "Right " - mFL.run(Adafruit_MotorHAT.BACKWARD) - mFL.setSpeed(speed) - - time.sleep(dur) - mFL.run(Adafruit_MotorHAT.RELEASE) - return '' - - -def getAttention(): - with open('../python-mindave-mobile/ATTENTION', 'r') as f: - read_data = f.read() - print("Read Speed: " + read_data) - - spd=0 - if read_data == '': - spd = 0 - else: - spd = int(read_data) - - if (spd < 50): - newSpd = 0 - else: - newSpd = (spd-50)*4 - - return newSpd - -stopped = True; -turning = False; -THRESH = 25; -turnCount = 0; -maxTurnCount = 3; - -while(1==1): - print("driving!") - -# while True: -# s = getAttention(); -# print "Speed "+str(s) -# if s > 0: -# break -# setSpeed(0) -# time.sleep(0.2) - - mind = 1000 - d=[] - - for i in range(3): - d.append( distance(i)); - if d[i] < mind: - mind = d[i] - print(d[i]); - - print(d) - print("Min d " + str(mind)) - - if (mind<6 and stopped==True and turning==True): - do('echo "backing up." | flite ') - backward(255, 1) - elif (turnCount > maxTurnCount): - do('echo "backing up." | flite ') - backward(255, 1); - turnCount = 0; - elif (mind>=THRESH and stopped==True): - stopped=False; - turning = False; - gof(); - turnCount = 0 - elif (mind d[0]: - do('echo "turning left." | flite ') - left(255, 0.5); - else: - do('echo "turning right." | flite ') - right(255, 0.5); - - time.sleep(0.3); - stopped=True -# elif (turning==False): # robot is moving happily - #newSpd = getAttention(); - #setSpeed(newSpd); From 810a68d753ec662c51fb029d6ebb940ebc8b59b4 Mon Sep 17 00:00:00 2001 From: Lukas Biewald Date: Mon, 13 Feb 2017 16:10:36 -0800 Subject: [PATCH 50/50] Added back camera_pi --- camera_pi.py | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 camera_pi.py diff --git a/camera_pi.py b/camera_pi.py new file mode 100644 index 0000000..9dbbbbe --- /dev/null +++ b/camera_pi.py @@ -0,0 +1,54 @@ +import time +import io +import threading +import picamera + + +class Camera(object): + thread = None # background thread that reads frames from camera + frame = None # current frame is stored here by background thread + last_access = 0 # time of last client access to the camera + + def initialize(self): + if Camera.thread is None: + # start background frame thread + Camera.thread = threading.Thread(target=self._thread) + Camera.thread.start() + + # wait until frames start to be available + while self.frame is None: + time.sleep(0) + + def get_frame(self): + Camera.last_access = time.time() + self.initialize() + return self.frame + + @classmethod + def _thread(cls): + with picamera.PiCamera() as camera: + # camera setup + camera.resolution = (320, 240) + camera.hflip = True + camera.vflip = True + + # let camera warm up + camera.start_preview() + time.sleep(2) + + stream = io.BytesIO() + for foo in camera.capture_continuous(stream, 'jpeg', + use_video_port=True): + # store frame + stream.seek(0) + cls.frame = stream.read() + + # reset stream for next frame + stream.seek(0) + stream.truncate() + + # if there hasn't been any clients asking for frames in + # the last 10 seconds stop the thread + if time.time() - cls.last_access > 10: + break + cls.thread = None