OB7PY:
A python module for controlling the OB7 robotics platform by Productive Robotics
I just completed the first big chunk of this project and decided to release it as an open source python package in beta. It is a single .py file python module the sole aim of which is to integrate the OB7 platform with python and, consequently, the Raspberry Pi, MicroPython, CircuitPython, Python 3.x, etc domains, which will potentially bring more industry people to Python, bring more Python people to industry, etc.
I did a similar thing with the Haas CNC platform a few years ago. That is to say, I wrote a small software library to directly access macro variables on the Haas control, though I never released it. It was written in C++ to be used on the Teensy 4 microcontroller. I will likely rewrite it for Python in the coming months.
Anyway, continuing on.
Introduction and setup:
The library leans heavily on the PyModbusTCP and pymodbus libraries for fetching, writing, and storing data registers. The OB7 allows outside communication via either its TCP ASCII server or Modbus TCP protocol. I chose to go the Modbus route for this library, mostly because its a relatively standard paradigm for industry, at least as opposed to an ASCII server, which I feel is to product-specific, the implementation of which would teach me nothing in the process.
The library is installed in your python path or virtual environment the standard way in your terminal:
pip install ob7py
There are no arguments to pass, as this is a beta version, only 0.0.2 is released.
Once it is downloaded, you import it into your project the standard way, followed by an instantiation of an object of the class Robot_:
main.py
import ob7py
add = input("Enter IP address (ex: 127.0.0.3): ")
port = input("Enter port number (ex: 5020): ")
robot = ob7py.Robot_(add, int(port)) #add= str, port=int
Were assigning an IP address and a port number to create an instance of the ob7py.Robot_ class and calling it “robot”. Pretty simple. We’re forcing the user input for the port number into an int, as that parameter is required to be an int. The IP address is a string, the natural result of an input() call, so we don’t need to do anything there.
Now would be a good time to note, the examples presented here have no error trapping or try: except: routines- you’re going to integrate something like that, especially if whatever you’re building is public-facing or going to be used by anyone other than you. For example, you may want to get your port number from the user like this:
main.py
import ob7py
add = input("Enter IP address (ex: 127.0.0.3): ")
while True:
port = input("Enter port number (ex: 5020): ")
if isinstance(port.strip(), int):
break
else:
print("invalid, try again")
robot = ob7py.Robot_(add, int(port)) #add= str, port=int
This is very simple error trapping, simply testing to make sure the value given for port is the correct data type. There are a number of ways to make sure the input is a valid one before passing it to the Robot_ class, but how deep you want to go in idiot-proofing your code is up to you. In an industrial environment, minimal error trapping is usually ok as long as type and range details on your user inputs is documented. A technical guy working on your system later on down the road after you’ve already moved on can be trusted to check the documentation quite a bit more than the average off-the-street user of a public web app or something like that.
Basic Control Functions:
Enough on error handling, lets get on with implementation. A basic strategy for controlling an ob7 with this library would be as follows:
Instantiate an object of the class Robot_, in this example we’re calling it “robot”. Check to see if the systems are talking to each other by calling robot.connect(), which returns the boolean state of PyModbusTCP’s self.client.open() method. We would likely check to see if robot.connect() == True before continuing, else throw an error message saying they’re not connecting and possible explanations for why.
Next, we may want to read the current position of each of the 7 joints in degrees. To do this, we’d do the following:
robot.ang_units(0) #0 sets the angular units to degrees. 1 sets it to radians
positions = robot.readj() #a dictionary of joint positions.
In this instance, the variable positions is a dictionary of joint positions that would print something like this:
{j1: 0.123, j2: 2.500, j3: 0.011, j4: 3.999, j5: 30.557, j6: 87.000, j7: 5.500}
if you had called robot.readj_list(), it would have returned a list object with the joint values only, for example:
[0.123, 2.500, 0.011, 3.999, 30.557, 87.000, 5.500]
If you want to command the robot to a different position by directly addressing joint angles, you could try:
robot.movej( j1 = 1.0, j2 = 2.5) #moves joints 1 and 2
…or if you want to command all seven joints, you could send a list type object with movej_list()…
comPos = [1.0, 2.5, 3.6, 5.5, 8.8, 1.3, 0.5]
robot.movej_list(comPos)
These above examples were assuming you’d want to read/write robot joint positions. If you wanted to use cartesian X, Y, Z, Roll, Pitch, Yaw coordinates, you would use these functions with very similar syntax:
robot.readt() # returns x,y,z,r,p,yaw values for the robots tool position as a dict type
robot.readt_list() # returns the above values as a list type
robot.movet(x=0.0, yaw= 3.5) # sends robot tool to positions named x,y,z,r,p, and yaw
robot.movet_list(comPos) # sends robot to positions placed in list object comPos
To check the status of one of the robot interface’s general equipment inputs:
input25 = robot.gei_in(25)
… would put the value of the robot’s input 25 in the variable input25.
In Conclusion:
There are a number of functions available, all of which are pretty straightforward and documented well enough in the docstrings. In the near future I will be implementing Trajectory Streaming, which will allow near real time spatial and logical control of the robot. There is currently a way to control the gripper, but not isolate one gripper or the other if there is a dual gripper or multi-tool setup on the robot. I am in communication with Productive Robotics to get this implemented, hopefully soon. This writeup will be redone when trajectory streaming and gripper support are complete, and the real beta version is released.
Here are the raw docstrings:
NAME
ob7py.ob7py - A small package for controlling I/O, controlling motion, and polling data on the Productive Robotics platform.
DESCRIPTION
Allows direct communication with ob7 control over modbusTCP protocol.
ob7py leans heavily on the pyModbusTCP library for register read/write over ethernet,
and the pymodbus library for establishing a virtual "dummy server" for testing functions.
Basic code example:
Create an instance of the class Robot_:
robot = Robot('123,0,0,3', 5020) #args: server_ip(str) and server_port(int)
robot.connect() #connect to server
robot.movet(1,2,3,1,2,3) #move tool to cartesian x,y,z,roll,pitch,yaw coordinates. Pass one or many values(float).
robot.movet_list(list) #move to cartesian coordinates. Pass all values in list format.
robot.movej(1,2,3,1,2,3,4) #move joints J1-J7 in angular units. Pass one or many values(float)
robot.movej_list(list) #as above, pass in list format.
robot.eoa_in(2) #return the status of an End of Arm input. Takes int 1-4
robot.gei_in(25) #return the status of a General Equipment Interface input. Takes int 1-32
To get a comprehensive list of all gei io and their corresponding registers, run list_all_io(260)
260 to show inputs, 514 to show outputs.
robot.eoa_out(2, 1) # assign the int value 1 to output 2. To just read the current int of eoa output#2,
simply omit second argument.
robot.gei_out(25, 1) # as above. Assign 1 to output 25. Read int by omitting second arg.
robot.readt() returns a dictionary of cartesian coordinates for the tool.
robot.readt_list() returns a list (x,y,z,roll,pitch,yaw) for tool.
robot.readj() and robot.readj_list() are similar, but return joint positions.
author:
me company email address etc
CLASSES
builtins.object
Robot_
class Robot_(builtins.object)
| Robot_(server_ip, server_port=502)
|
| Instance of ModbusTcpClient coupled with methods to control and read ob7.
|
| Methods defined here:
|
| __init__(self, server_ip, server_port=502)
| Identify the server and instatiate a client.
|
| ang_units(self, arg=None)
| Reads/write robot angular units registry.
|
| Arguments:
| arg: int| can be either 0 or 1. 0 is Degrees(default) and 1 is Radians.
|
| Example:
|
| passing no argument returns the current setting in str format.
| example.ang_units()
| returns "Degrees"
|
| passing an argument (0 or 1) sets the ob7's angular units to Degrees if 0
| is passed, and Radians if any other nonzero integer is passed. Method returns
| boolean True to tell you the function was successful.
|
| close(self)
| Close the connection to the server.
|
| cmd_value(self, reg, val)
| Fill a robot command value register pair with a float.
|
| Used for a few different functions. There are 7 general purpose floating point variables used for
| positioning the robot, reading out positions, etc...
|
| Further detail will be provided inside the docstring of the individual methods that utilize
| these registers.
|
| This method fills one robot command value. cmd_values() will fill all 7 with one call.
|
| cmd_values() will be req'd for position streaming, to be described later.
|
| cmd_values(self, vals)
| Fill all 7 robot command value registers with a single call.
|
| args: val | list filled with seven items corresponding to the 7 command value regs.
|
| command_status(self, val)
| Return robot command status: the status of the machine after being issued the previous command.
|
| connect(self)
| Connect to the server.
|
| dis_units(self, arg=None)
| Return ob7 distance units. Similar syntactically to ang_units()
|
| eoa_in(self, eoainum)
| Read register for End Of Arm input and return it.
|
| eoa_out(self, eoaonum, arg_=None)
| Read register for End Of Arm output and return it. If argument is passed,
| assign the value to the specified register.
|
| gei_in(self, geiinum)
| Read register for General Equipment Interface input and return it.
|
| gei_out(self, geionum, arg_=None)
| Read register for General Equipment Interface ouyput and return it.
| If argument is passed, assign the value to the specified register.
|
| gprr(self, add=None, val=None)
| EXPERIMENTAL: Read/write *general purpose robot registers 0 - 31*
| Method written for field experimentation only.
|
| grip(self, grippernumber, state)
| NOTES ON THIS FUNCTION:
| I NEED TO FIGURE OUT HOW TO GET OR SET THE CURRENT TOOL
| IE GRIPPER
| IT IS ADDRESSED IN THE DOCUMENTATION UNDER ASCII COMMANDS,
| BUT NOT IN MODBUS TCP
|
| list_all_io(self, base=260)
| Print a table to the terminal illustrating the relationship between ModbusTCP register
| addresses and the arguments we will use to read/write them.
|
| Arguments:
| base: integer, starting address. 260 to show inputs, 514 to show outputs.
|
| movej(self, j1=None, j2=None, j3=None, j4=None, j5=None, j6=None, j7=None)
| Move to position specified by the following joint angles. Returns bool True
|
| movej_list(self, cart_list)
| Move to position specified by the following joint angles. Returns bool True
|
| movet(self, x=None, y=None, z=None, r=None, p=None, yaw=None)
| Move to tool pos x,y,z, and orient the tool roll, pitch, yaw. Returns bool True
|
| movet_list(self, cart_list)
| Move to tool pos x,y,z, and orient the tool roll, pitch, yaw using a list. Returns bool True
|
| obj_gripped(self, gripper_num)
| Return whether a gripper is gripping an object or not.
|
| payload(self, value=None)
| Set weight of payload with value, in current weight units. Return payload if no arg passed.
|
| read_float(self, address)
| Read the bytes from the Modbus server- at address specified, for two consecutive
| registers, from unit=1 (meaning the server typically, or any modbus device we labeled as #1).
|
| read_uint16(self, address)
| Read a 16-bit unsigned integer from a Modbus register.
|
| readj(self)
| Return a dictionary of joint names and their respective positions as floats.
|
| readj_list(self)
| Return a list of floats of joint positions in ascending order from j1-j7
|
| readt(self)
| Return a dictionary of cartesian tool position names and float values
|
| readt_list(self)
| Return a list of cartesian tool position float values
|
| robot_command(self, val)
| Set robot command register, in effect issuing the robot to perform a task, usually
| involving numbers preloaded into the command value registers using cmd_value() or cmd_values()
|
| 0: No command
| 1: Move to joint position. The robot will move to the joint
| positions in the robot command values 1-7, which will be
| interpreted using the current angular units.
| 2: Move to tool position. The robot will move the tool point to the
| x, y, and z positions in command values 1, 2, and 3 respectively,
| and orient it to the roll, pitch, and yaw values in command values
| 4, 5, and 6 respectively.
| 3: Begin trajectory. This will begin assembly of a new trajectory.
| 4: Add joint trajectory point. This will add a new trajectory point
| using the values in command values 1-7 as the joint positions,
| interpreted as in command 1.
| 5: Add tool trajectory point. This will add a new trajectory point
| using the values in command values 1-6 as the tool position,
| interpreted as in command 2.
| 6: Execute trajectory. This will begin execution of a trajectory
| started with command 3 and move the robot through points
| added with commands 4 and 5.
| 7: Stop move. This will stop any currently executing moves.
| 8: Stream joint position. Move the robot to the joint positions
| specified in command values 1-7, interpreted as in command 1.
| See section \u201cRobot Trajectory Streaming\u201d.
| 9: Enable joint position streaming. See section \u201cRobot Trajectory
| Streaming\u201d
| 10: Move tool relative. When no job is executing, this will offset
| the tool point by the x, y, and z amount in command values 1, 2,
| and 3 respectively, and offset the roll, pitch, and yaw of the tool by
| the command values 4, 5, and 6 respectively. During job execution
| this command may be issued to supply a relative move to an
| Offset Move block.
| 11: Open gripper. This will open the gripper to the width specified
| in command value 1.
| 12: Close gripper. This will close the gripper to the width specified
| in command value 1.
| 13: Stop gripper. Stops any active gripper commands.
| 255: Shutdown the robot. This will stop any executing jobs and
| shut down the robot controller.
|
| robot_state(self)
| Read the register # 768 and return text describing the Robot's State.
| Returns None otherwise.
|
| stream_buffer_size(self, val=None)
| Read/write the value of the stream butter size, default 10 at power up, for trajectory streaming.
|
| If no argument is passed, return value. If argument is <0, write argument to buffer size register 1040.
|
| weight_units(self, arg=None)
| Return ob7 weight units. Similar syntactically to ang_units()
|
| write_float(self, address, value)
| Convert a floating-point number to IEEE 754 format.
|
| Arguments:
| address: int, the number of the register address we're writing
| value: float, the number we're going to convert and write to register
|
| write_uint16(self, address, value)
| Write a 16-bit unsigned integer(value) to a Modbus register.
|
| ----------------------------------------------------------------------
| Data descriptors defined here:
|
| __dict__
| dictionary for instance variables (if defined)
|
| __weakref__
| list of weak references to the object (if defined)
