From 1b53396b45fc54949f6a0033e2857f2a12830cff Mon Sep 17 00:00:00 2001 From: Austin Gill Date: Mon, 9 Jul 2018 17:44:25 -0500 Subject: [PATCH 01/10] Add Python gitignore --- .gitignore | 107 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0772a39 --- /dev/null +++ b/.gitignore @@ -0,0 +1,107 @@ +# Editor stuff +.vscode + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ From 1860eb738baa9dee91a117ca24697e7343ba3218 Mon Sep 17 00:00:00 2001 From: Austin Gill Date: Mon, 9 Jul 2018 17:44:44 -0500 Subject: [PATCH 02/10] Reorder imports --- geekbot.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/geekbot.py b/geekbot.py index 8d94b72..fb7cdd5 100644 --- a/geekbot.py +++ b/geekbot.py @@ -1,9 +1,8 @@ -import serial -import serial.tools.list_ports -from time import sleep as wait from struct import pack, unpack -from collections import namedtuple +from time import sleep as wait +import serial +import serial.tools.list_ports robot_vid = 0x0403 robot_pid = 0x6001 @@ -53,7 +52,7 @@ def find_robot(self): return str(i.device) print("No Geekbot found!") return None - + def is_connected(self): return self.connected @@ -70,7 +69,7 @@ def map_short(self, num): #where num is a num 0 - 100 return int(temp) def pack_short(self,num): - return pack("h", int(num)) + return pack("h", int(num)) def send_cmd(self,flag, data): self.port.write(chr(flag).encode()) @@ -96,7 +95,7 @@ def turn(self, speed, seconds=None): def drive_forward(self, speed, adjust=None, seconds=None): if adjust == None: self.send_cmd(drive_flag, speed) - else: + else: self.drive_left_wheel(speed) adjusted = speed+adjust if adjusted > 100: @@ -114,7 +113,7 @@ def drive_forward(self, speed, adjust=None, seconds=None): def drive_backward(self, speed, adjust=None, seconds=None): if adjust == None: self.send_cmd(drive_flag, -speed) - else: + else: self.drive_left_wheel(-speed) adjusted = speed+adjust if adjusted > 100: @@ -130,7 +129,7 @@ def drive_backward(self, speed, adjust=None, seconds=None): def drive_right_wheel(self, speed): self.send_cmd(right_flag, -speed) - + def drive_left_wheel(self, speed): self.send_cmd(left_flag, -speed) @@ -142,5 +141,3 @@ def get_ir_distance(self): def set_ir_position(self, angle): self.send_cmd(ir_pos_flag, angle) - - From e202fc6e7dbfcb9348954fb537306cb55ae9e7f3 Mon Sep 17 00:00:00 2001 From: Austin Gill Date: Mon, 9 Jul 2018 17:47:08 -0500 Subject: [PATCH 03/10] Capitalize constants --- geekbot.py | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/geekbot.py b/geekbot.py index fb7cdd5..10b69bd 100644 --- a/geekbot.py +++ b/geekbot.py @@ -4,20 +4,20 @@ import serial import serial.tools.list_ports -robot_vid = 0x0403 -robot_pid = 0x6001 -robot_pid2 = 0x6015 +ROBOT_VID = 0x0403 +ROBOT_PID = 0x6001 +ROBOT_PID2 = 0x6015 -handshake = 0x77 -end_flag = 0x33 +HANDSHAKE = 0x77 +END_FLAG = 0x33 -drive_flag = 0x45 -left_flag = 0x36 -right_flag = 0x35 -lights_flag = 0x30 +DRIVE_FLAG = 0x45 +LEFT_FLAG = 0x36 +RIGHT_FLAG = 0x35 +LIGHTS_FLAG = 0x30 -ir_read_flag = 0x27 -ir_pos_flag = 0x28 +IR_READ_FLAG = 0x27 +IR_POS_FLAG = 0x28 class Robot: @@ -35,7 +35,7 @@ def __init__(self, baud, file=None): if self.location != None: self.port.open() wait(2) - self.port.write(chr(handshake).encode()) + self.port.write(chr(HANDSHAKE).encode()) wait(0.5) while self.port.read() != chr(0x77).encode(): print("Waiting for handshake") @@ -47,7 +47,7 @@ def find_robot(self): port_list = serial.tools.list_ports.comports() for i in port_list: print(i.device) - if (i.vid == robot_vid) and (i.pid == robot_pid or i.pid == robot_pid2): + if (i.vid == ROBOT_VID) and (i.pid == ROBOT_PID or i.pid == ROBOT_PID2): print("Geekbot found:" + i.device) return str(i.device) print("No Geekbot found!") @@ -76,17 +76,17 @@ def send_cmd(self,flag, data): self.port.write(self.pack_short(self.map_short(data))) def lights_on(self): - self.send_cmd(lights_flag, 0x01) + self.send_cmd(LIGHTS_FLAG, 0x01) def lights_off(self): - self.send_cmd(lights_flag, 0x00) + self.send_cmd(LIGHTS_FLAG, 0x00) def halt(self): - self.send_cmd(drive_flag, 0) + self.send_cmd(DRIVE_FLAG, 0) def turn(self, speed, seconds=None): - self.send_cmd(left_flag, -speed) - self.send_cmd(right_flag, speed) + self.send_cmd(LEFT_FLAG, -speed) + self.send_cmd(RIGHT_FLAG, speed) if seconds != None: wait(seconds) self.halt() @@ -94,7 +94,7 @@ def turn(self, speed, seconds=None): def drive_forward(self, speed, adjust=None, seconds=None): if adjust == None: - self.send_cmd(drive_flag, speed) + self.send_cmd(DRIVE_FLAG, speed) else: self.drive_left_wheel(speed) adjusted = speed+adjust @@ -112,7 +112,7 @@ def drive_forward(self, speed, adjust=None, seconds=None): def drive_backward(self, speed, adjust=None, seconds=None): if adjust == None: - self.send_cmd(drive_flag, -speed) + self.send_cmd(DRIVE_FLAG, -speed) else: self.drive_left_wheel(-speed) adjusted = speed+adjust @@ -128,16 +128,16 @@ def drive_backward(self, speed, adjust=None, seconds=None): self.halt() def drive_right_wheel(self, speed): - self.send_cmd(right_flag, -speed) + self.send_cmd(RIGHT_FLAG, -speed) def drive_left_wheel(self, speed): - self.send_cmd(left_flag, -speed) + self.send_cmd(LEFT_FLAG, -speed) def get_ir_distance(self): - self.send_cmd(ir_read_flag, 1) + self.send_cmd(IR_READ_FLAG, 1) data = self.port.read(2) dist = unpack(">H", data) return dist[0] def set_ir_position(self, angle): - self.send_cmd(ir_pos_flag, angle) + self.send_cmd(IR_POS_FLAG, angle) From def534d931da74fb0dbf1c33460c48e8003c8dfe Mon Sep 17 00:00:00 2001 From: Austin Gill Date: Mon, 9 Jul 2018 18:00:09 -0500 Subject: [PATCH 04/10] Add docstrings to class and __init__ --- geekbot.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/geekbot.py b/geekbot.py index 10b69bd..c96bedb 100644 --- a/geekbot.py +++ b/geekbot.py @@ -19,11 +19,17 @@ IR_READ_FLAG = 0x27 IR_POS_FLAG = 0x28 -class Robot: - +class Robot(object): + """ + A Robot class to interact with a Trossen Robotics Geekbot. + """ def __init__(self, baud, file=None): + """ + Connect and initialize a Geekbot. Connects to a Geekbot via serial using the given + baudrate. If the serial port is not given, attempt to find it. + """ self.port = serial.Serial() - if file != None: + if file is not None: self.location = file else: self.location = self.find_robot() @@ -32,23 +38,27 @@ def __init__(self, baud, file=None): self.port.timeout = 1 self.port.dtr = 1 self.connected = False - if self.location != None: + if self.location is not None: self.port.open() wait(2) self.port.write(chr(HANDSHAKE).encode()) wait(0.5) - while self.port.read() != chr(0x77).encode(): + while self.port.read() != chr(HANDSHAKE).encode(): print("Waiting for handshake") - #wait(0.5) + # wait(0.5) self.connected = True - def find_robot(self): + @staticmethod + def find_robot(): + """ + Attempts to find a serial port with a Geekbot connected. + """ print("Finding Geekbot's USB port...") port_list = serial.tools.list_ports.comports() for i in port_list: print(i.device) if (i.vid == ROBOT_VID) and (i.pid == ROBOT_PID or i.pid == ROBOT_PID2): - print("Geekbot found:" + i.device) + print("Geekbot found: " + i.device) return str(i.device) print("No Geekbot found!") return None From 2b3c0f590d6e763bde3cb778d37cbe0a62b205b1 Mon Sep 17 00:00:00 2001 From: Austin Gill Date: Mon, 9 Jul 2018 18:02:14 -0500 Subject: [PATCH 05/10] Remove getter method. Not "Pythonic". :P In Python, it's normal to access attributes directly. If it every becomes necessary to wrap an attribute in a getter/setter, there are @decorators that will maintain backwards compatability. --- geekbot.py | 7 ++++--- geekbot_test.py | 5 +---- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/geekbot.py b/geekbot.py index c96bedb..0bd977d 100644 --- a/geekbot.py +++ b/geekbot.py @@ -63,12 +63,13 @@ def find_robot(): print("No Geekbot found!") return None - def is_connected(self): - return self.connected - def shutdown(self): + """ + Stops all motion and disconnects from the Geekbot. + """ self.halt() self.port.close() + self.connected = False def map_short(self, num): #where num is a num 0 - 100 temp = (num * 32767)/100 diff --git a/geekbot_test.py b/geekbot_test.py index 322f3a3..1029449 100755 --- a/geekbot_test.py +++ b/geekbot_test.py @@ -4,7 +4,7 @@ import time geek = geekbot.Robot(57600) -if geek.is_connected() == False: +if geek.connected is False: exit() # Check lights @@ -48,6 +48,3 @@ geek.drive_forward(80) time.sleep(1) geek.halt() - - - From d9025cfcf52abaf464a81e62bf2328b8479d1f54 Mon Sep 17 00:00:00 2001 From: Austin Gill Date: Mon, 9 Jul 2018 18:08:26 -0500 Subject: [PATCH 06/10] Document map_short, use __future__ import for floor division --- geekbot.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/geekbot.py b/geekbot.py index 0bd977d..a38680e 100644 --- a/geekbot.py +++ b/geekbot.py @@ -1,3 +1,4 @@ +from __future__ import print_function, division from struct import pack, unpack from time import sleep as wait @@ -71,8 +72,22 @@ def shutdown(self): self.port.close() self.connected = False - def map_short(self, num): #where num is a num 0 - 100 - temp = (num * 32767)/100 + @staticmethod + def map_short(num): + """ + Maps the given number, within 0 and 100, to an int16_t. + + Example: + + >>> from geekbot import Robot + >>> Robot.map_short(100) + 32767 + >>> Robot.map_short(0) + 0 + >>> Robot.map_short(-100) + -32767 + """ + temp = (num * 32767) // 100 if temp > 32767: return 32767 elif temp < -32767: From e9e057db7234a7c86f1ece7b5bebaf6549a653b1 Mon Sep 17 00:00:00 2001 From: Austin Gill Date: Mon, 9 Jul 2018 18:11:56 -0500 Subject: [PATCH 07/10] Document pack_short --- geekbot.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/geekbot.py b/geekbot.py index a38680e..d720e24 100644 --- a/geekbot.py +++ b/geekbot.py @@ -94,7 +94,11 @@ def map_short(num): return -32767 return int(temp) - def pack_short(self,num): + @staticmethod + def pack_short(num): + """ + Packs the given int16_t into a struct for use with a serial object. + """ return pack("h", int(num)) def send_cmd(self,flag, data): From 1ba89bc7c6821b9726de7f410da4470275e6f924 Mon Sep 17 00:00:00 2001 From: Austin Gill Date: Mon, 9 Jul 2018 18:33:20 -0500 Subject: [PATCH 08/10] Document send_cmd, and add several TODO comments. The Arduino interface needs to be documented better, or at least mentioned. but I don't have enough information to do so myself. The documentation added in this commit also makes a few assumptions on the ranges of inputs, and their meaning. These should not be assumptions. --- geekbot.py | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/geekbot.py b/geekbot.py index d720e24..ec05e39 100644 --- a/geekbot.py +++ b/geekbot.py @@ -28,6 +28,9 @@ def __init__(self, baud, file=None): """ Connect and initialize a Geekbot. Connects to a Geekbot via serial using the given baudrate. If the serial port is not given, attempt to find it. + + TODO: What is the Arduino code the Geekbot is running? Is it custom? Default provided + by Trossen? This needs to be documented. """ self.port = serial.Serial() if file is not None: @@ -101,7 +104,31 @@ def pack_short(num): """ return pack("h", int(num)) - def send_cmd(self,flag, data): + def send_cmd(self, flag, data): + """ + Send a generic command to the connected Geekbot. + + data - The command value to pass to the Geekbot. Specified by each command flag. + flag - The command flag to send. One of the following: + + DRIVE_FLAG - Drive both wheels with equal direction and speed + LEFT_FLAG - Control the left wheel + RIGHT_FLAG - Control the right wheel + + data - An integer value between -100 and 100 whose sign indicates direction, and + whose magnitude indicates speed. + + LIGHTS_FLAG - Control the robot's LED + data - True: Light on. False: Light off. + + IR_POS_FLAG - Set the angle of the IR distance sensor + TODO: Verify + data - An angle between -360 and 360. Angle is measured from the front of the robot, + angles moving right, and negative angles moving left. + TODO: Verify that 0x1 is a placeholder and doesn't have any special meaning. + IR_READ_FLAG - Request an update from the IR distance sensor + data - A placeholder (0x1) should be given. + """ self.port.write(chr(flag).encode()) self.port.write(self.pack_short(self.map_short(data))) From df5c918f4981a0387507fe94c673f2a59f1e96cb Mon Sep 17 00:00:00 2001 From: Austin Gill Date: Mon, 9 Jul 2018 18:58:29 -0500 Subject: [PATCH 09/10] Document the command methods. Simplify the drive_forward and drive_backward implementations. Note that the send_cmd() method already maps numbers to a short integer, so we don't need to do so ourselves. (Verify?) Also, the locomotion methods should make a note of the type of locomotion they implement. --- geekbot.py | 83 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 49 insertions(+), 34 deletions(-) diff --git a/geekbot.py b/geekbot.py index ec05e39..9b150a6 100644 --- a/geekbot.py +++ b/geekbot.py @@ -102,6 +102,7 @@ def pack_short(num): """ Packs the given int16_t into a struct for use with a serial object. """ + # An int16_t in native byte order (little-endian). return pack("h", int(num)) def send_cmd(self, flag, data): @@ -133,68 +134,82 @@ def send_cmd(self, flag, data): self.port.write(self.pack_short(self.map_short(data))) def lights_on(self): + """Turn on the Robot's LED""" self.send_cmd(LIGHTS_FLAG, 0x01) def lights_off(self): + """Turn off the Robot's LED""" self.send_cmd(LIGHTS_FLAG, 0x00) def halt(self): + """Stop the Robot in its tracks.""" self.send_cmd(DRIVE_FLAG, 0) def turn(self, speed, seconds=None): + """ + Turns the Robot. + + TODO: Make note of special Robotics terminology for this type of locomotion. + + speed - The speed at which to turn the robot. May be between -100 and 100. + seconds - The amount of time the turn should take. May be fractional. + """ self.send_cmd(LEFT_FLAG, -speed) self.send_cmd(RIGHT_FLAG, speed) - if seconds != None: + + if seconds is not None: wait(seconds) self.halt() - return - def drive_forward(self, speed, adjust=None, seconds=None): - if adjust == None: - self.send_cmd(DRIVE_FLAG, speed) - else: - self.drive_left_wheel(speed) - adjusted = speed+adjust - if adjusted > 100: - self.drive_right_wheel(100) - elif adjusted < 0: - self.drive_right_wheel(0) - else: - self.drive_right_wheel(adjusted) - if seconds == None: - return - wait(seconds) - self.halt() + def drive_forward(self, speed, adjust=0, seconds=None): + """ + Drives the Robot forward. + TODO: Make note of special Robotics terminology for this type of locomotion. - def drive_backward(self, speed, adjust=None, seconds=None): - if adjust == None: - self.send_cmd(DRIVE_FLAG, -speed) - else: - self.drive_left_wheel(-speed) - adjusted = speed+adjust - if adjusted > 100: - self.drive_right_wheel(-100) - elif adjusted < 0: - self.drive_right_wheel(0) - else: - self.drive_right_wheel(-(adjusted)) - if seconds == None: - return - wait(seconds) - self.halt() + speed - The speed at which the Robot should drive. An integer between -100 and 100. + adjust - The speed by which the right wheel should differ from the left wheel. Optional + seconds - The amount of time you wish the Robot to drive forward. Optional. If not + given, the Robot will drive forward until instructed otherwise. + """ + self.drive_left_wheel(speed) + # self.send_cmd() already handles range overflows, so just add the adjustment. + self.drive_right_wheel(speed + adjust) + + if seconds is not None: + wait(seconds) + self.halt() + + def drive_backward(self, speed, adjust=0, seconds=None): + """ + Drives the Robot backward. + + TODO: Make note of special Robotics terminology for this type of locomotion. + + speed - The speed at which the Robot should drive. An integer between -100 and 100. + adjust - The speed by which the right wheel should differ from the left wheel. Optional + seconds - The amount of time you wish the Robot to drive backward. Optional. If not + given, the Robot will drive backward until instructed otherwise. + """ + # Negating both speed and adjust is equivalent to adding, and then negating the result. + self.drive_forward(-speed, -adjust, seconds) def drive_right_wheel(self, speed): + """Drives the right wheel indefinitely at the given speed.""" self.send_cmd(RIGHT_FLAG, -speed) def drive_left_wheel(self, speed): + """Drives the left wheel indefinitely at the given speed.""" self.send_cmd(LEFT_FLAG, -speed) def get_ir_distance(self): + """Gets the distance measured by the Robot's IR sensor.""" self.send_cmd(IR_READ_FLAG, 1) data = self.port.read(2) + # A uint16_t, in big-endian byte order. dist = unpack(">H", data) return dist[0] def set_ir_position(self, angle): + """Sets the angle (in degrees) the Robot's IR sensor is pointing.""" self.send_cmd(IR_POS_FLAG, angle) From aad456db350a5fec700aa8bff3ba8302584612c1 Mon Sep 17 00:00:00 2001 From: Austin Gill Date: Mon, 9 Jul 2018 19:31:18 -0500 Subject: [PATCH 10/10] Style improvements. Major: * align constants even though our beloved PEP8 forbids it. * Add optional timeout to drive_*_wheel commands so that they act the same as the drive_forward and drive_backward commands. Minor: * Add comments, examples, solve world hunger, etc., etc. --- README.md | 45 ++++++++++++++++++ geekbot.py | 132 ++++++++++++++++++++++++++++++++++++++--------------- 2 files changed, 140 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 04158d3..85ab285 100644 --- a/README.md +++ b/README.md @@ -4,3 +4,48 @@ Authors: Kali Regenold, Remington Bullis This repository will hold the development code for the GenCyber Camp Robotics Class. Everyday of the camp, the stable code here will be duplicated into a daily folder in the GenCyber18 repo, aka the production repo. + +```python +import geekbot +from time import sleep + +inigo_montoya = geekbot.Robot(57600) +if not inigo_montoya.connected: + exit() + +# Check lights +inigo_montoya.lights_on() +sleep(.5) +inigo_montoya.lights_off() + +# Check IR servo and sensor +inigo_montoya.set_ir_position(0) +sleep(.25) +print(inigo_montoya.get_ir_distance()) +sleep(.25) +inigo_montoya.set_ir_position(90) +sleep(.25) +print(inigo_montoya.get_ir_distance()) +sleep(.25) +inigo_montoya.set_ir_position(-90) +sleep(.25) +print(inigo_montoya.get_ir_distance()) +sleep(.25) +inigo_montoya.set_ir_position(0) +sleep(.5) + +# Check left wheel servo +inigo_montoya.drive_left_wheel(90, seconds=1) +inigo_montoya.drive_left_wheel(-90, seconds=1) +inigo_montoya.drive_left_wheel(0, seconds=0.5) + +# Check right wheel servo +inigo_montoya.drive_right_wheel(90, seconds=1) +inigo_montoya.drive_right_wheel(-90, seconds=1) +inigo_montoya.drive_right_wheel(0, seconds=0.5) + +# Check drive forward and halt +inigo_montoya.drive_forward(80, seconds=1) + +inigo_montoya.shutdown() +``` diff --git a/geekbot.py b/geekbot.py index 9b150a6..ed33329 100644 --- a/geekbot.py +++ b/geekbot.py @@ -5,32 +5,66 @@ import serial import serial.tools.list_ports -ROBOT_VID = 0x0403 -ROBOT_PID = 0x6001 -ROBOT_PID2 = 0x6015 -HANDSHAKE = 0x77 -END_FLAG = 0x33 +ROBOT_VID = 0x0403 +ROBOT_PID = 0x6001 +ROBOT_PID2 = 0x6015 -DRIVE_FLAG = 0x45 -LEFT_FLAG = 0x36 -RIGHT_FLAG = 0x35 -LIGHTS_FLAG = 0x30 +HANDSHAKE = 0x77 +END_FLAG = 0x33 + +DRIVE_FLAG = 0x45 +LEFT_FLAG = 0x36 +RIGHT_FLAG = 0x35 +LIGHTS_FLAG = 0x30 IR_READ_FLAG = 0x27 IR_POS_FLAG = 0x28 + class Robot(object): """ A Robot class to interact with a Trossen Robotics Geekbot. + + Example: + + >>> from time import sleep + >>> # Robots aren't complete without names. + >>> randy = Robot(57600) + >>> if not randy.connected: + ... exit() + >>> randy.lights_on() + >>> sleep(0.5) + >>> randy.lights_off() + >>> # Drive the left wheel forwards for 1 second. + >>> randy.drive_left_wheel(90, seconds=1) + >>> # Drive the left wheel backwards for 1.5 seconds. + >>> randy.drive_left_wheel(-90, seconds=1.5) + >>> # Drive both wheels forward at the same speed for 1 second. + >>> randy.drive_forward(80, seconds=1) + >>> # Drive the right wheel a 10 speed units slower than the left for 1 second. + >>> randy.drive_forward(80, adjust=-10, seconds=1) + >>> # Shutdown randy after he's had his fun. + >>> randy.shutdown() """ + def __init__(self, baud, file=None): """ - Connect and initialize a Geekbot. Connects to a Geekbot via serial using the given - baudrate. If the serial port is not given, attempt to find it. + Connect and initialize a Geekbot. Connects to a Geekbot via serial. + + baud - The baudrate to use for the serial connection. + file - The optional serial port to use for the connection. If not given, we will attempt + to find the right port. TODO: What is the Arduino code the Geekbot is running? Is it custom? Default provided by Trossen? This needs to be documented. + + Example: + + >>> george = Robot(57600) + >>> if not george.connected: + ... exit() + >>> george.shutdown() """ self.port = serial.Serial() if file is not None: @@ -42,35 +76,35 @@ def __init__(self, baud, file=None): self.port.timeout = 1 self.port.dtr = 1 self.connected = False + if self.location is not None: self.port.open() + # Give the connection some time to open before we try to use it. wait(2) + # Tell the Robot we want to connect to it, and wait for it to consent. self.port.write(chr(HANDSHAKE).encode()) wait(0.5) while self.port.read() != chr(HANDSHAKE).encode(): print("Waiting for handshake") # wait(0.5) self.connected = True + else: + print("Could not connect to Geekbot.") @staticmethod def find_robot(): - """ - Attempts to find a serial port with a Geekbot connected. - """ + """Attempts to find a serial port with a Geekbot connected.""" print("Finding Geekbot's USB port...") - port_list = serial.tools.list_ports.comports() - for i in port_list: - print(i.device) - if (i.vid == ROBOT_VID) and (i.pid == ROBOT_PID or i.pid == ROBOT_PID2): - print("Geekbot found: " + i.device) - return str(i.device) + for port in serial.tools.list_ports.comports(): + print(port.device) + if (port.vid == ROBOT_VID) and (port.pid == ROBOT_PID or port.pid == ROBOT_PID2): + print("Geekbot found: " + port.device) + return str(port.device) print("No Geekbot found!") return None def shutdown(self): - """ - Stops all motion and disconnects from the Geekbot. - """ + """Stops all motion and disconnects from the Geekbot.""" self.halt() self.port.close() self.connected = False @@ -99,9 +133,7 @@ def map_short(num): @staticmethod def pack_short(num): - """ - Packs the given int16_t into a struct for use with a serial object. - """ + """Packs the given int16_t into a struct for use with a serial object.""" # An int16_t in native byte order (little-endian). return pack("h", int(num)) @@ -115,9 +147,8 @@ def send_cmd(self, flag, data): DRIVE_FLAG - Drive both wheels with equal direction and speed LEFT_FLAG - Control the left wheel RIGHT_FLAG - Control the right wheel - data - An integer value between -100 and 100 whose sign indicates direction, and - whose magnitude indicates speed. + whose magnitude indicates speed. LIGHTS_FLAG - Control the robot's LED data - True: Light on. False: Light off. @@ -125,12 +156,16 @@ def send_cmd(self, flag, data): IR_POS_FLAG - Set the angle of the IR distance sensor TODO: Verify data - An angle between -360 and 360. Angle is measured from the front of the robot, - angles moving right, and negative angles moving left. - TODO: Verify that 0x1 is a placeholder and doesn't have any special meaning. + angles moving right, and negative angles moving left. + TODO: Verify that 0x01 is a placeholder and doesn't have any special meaning. IR_READ_FLAG - Request an update from the IR distance sensor - data - A placeholder (0x1) should be given. + data - A placeholder (0x01) should be given. """ + # Send the command flag and then the data. We send the flag first so that the Geekbot knows + # what the data means. Otherwise, it's just a number that could mean anything. self.port.write(chr(flag).encode()) + # The Geekbot expects numbers from SHRT_MIN to SHRT_MAX, but those aren't so great for + # teaching young children robotics. So map numbers from [-100, 100] to [SHRT_MIN, SHRT_MAX]. self.port.write(self.pack_short(self.map_short(data))) def lights_on(self): @@ -151,12 +186,13 @@ def turn(self, speed, seconds=None): TODO: Make note of special Robotics terminology for this type of locomotion. - speed - The speed at which to turn the robot. May be between -100 and 100. + speed - The speed at which to turn the robot. May be between -100 and 100. seconds - The amount of time the turn should take. May be fractional. """ self.send_cmd(LEFT_FLAG, -speed) self.send_cmd(RIGHT_FLAG, speed) + # We specify how far the robot turns by waiting different lengths of time before stopping. if seconds is not None: wait(seconds) self.halt() @@ -194,17 +230,39 @@ def drive_backward(self, speed, adjust=0, seconds=None): # Negating both speed and adjust is equivalent to adding, and then negating the result. self.drive_forward(-speed, -adjust, seconds) - def drive_right_wheel(self, speed): - """Drives the right wheel indefinitely at the given speed.""" + def drive_right_wheel(self, speed, seconds=None): + """ + Drives the right wheel. + + speed - The speed at which to drive the right wheel. + seconds - The amount of time the right wheel with drive. If not given, the wheel will + drive at the given speed until instructed otherwise. + """ + # TODO: Document why the speed is negated. self.send_cmd(RIGHT_FLAG, -speed) - def drive_left_wheel(self, speed): - """Drives the left wheel indefinitely at the given speed.""" + if seconds is not None: + wait(seconds) + self.halt() + + def drive_left_wheel(self, speed, seconds=None): + """ + Drives the left wheel. + + speed - The speed at which to drive the left wheel. + seconds - The amount of time the left wheel with drive. If not given, the wheel will + drive at the given speed until instructed otherwise. + """ + # TODO: Document why the speed is negated. self.send_cmd(LEFT_FLAG, -speed) + if seconds is not None: + wait(seconds) + self.halt() + def get_ir_distance(self): """Gets the distance measured by the Robot's IR sensor.""" - self.send_cmd(IR_READ_FLAG, 1) + self.send_cmd(IR_READ_FLAG, 0x01) data = self.port.read(2) # A uint16_t, in big-endian byte order. dist = unpack(">H", data)