I got my first Arduino a couple of years ago and as most people do one of the first projects I endeavored to achieve was to hook up the Arduino to a DHT sensor. Not satisfied with that I added 433Mhz transmitters/receivers for measuring the environment in the bedroom/living room/kitchen/attic/cellar etc.
Sending all the info back to one receiver. I was pleased as Punch!
Then I decided that I wanted to log the data and have a nice graphics display on my computer. For this, I used Python and TKinter.
So I wrote the program in Python to get the data from the USB and display on the screen.
I want to share some code which was the kicking off point to the development, rather than publish the whole Python program and the Arduino sketch which not easily readable for the beginner. This is just for motivation purposes.
First the Arduino sketch
void setup() {
Serial.begin(9600);
}
void loop() {
// Change this string to anything you like
// but remember to leave \n at the end, as this
// marker is required by Python readline()
Serial.write("I am the Walrus, Coo Coo Ca Choo\n");
delay(5000);
}
Now the Python code
#!/usr/bin/python
import serial
import time
# you may need to run pip install pyserial first
# change port to port name you are using
# to get the list of available ports
# run "python -m serial.tools.list_ports" command in the terminal
# initialise the serial object
ser = serial.Serial(
port='/dev/cu.usbmodem1461',\
baudrate=9600,\
parity=serial.PARITY_NONE,\
stopbits=serial.STOPBITS_ONE,\
bytesize=serial.EIGHTBITS,\
timeout=None
)
print("connected to: " + ser.portstr)
while True: # loop forever
print ("Waiting....")
# get data from serial port
byteArray = ser.readline() # reads the incoming bytearray until a linefeed (\n) is encountered
print(byteArray) # prints the received data array it's raw form
mList = [] # initialise zero list
for mByte in byteArray:
if mByte !=10: # ignore the linefeed
# print the byte value, then a space, then the equivalent ASCII code
print(str(mByte) +" "+chr(mByte))
# append bytes to list
mList.append(mByte)
# print full byte list
print(mList)
# just for fun, print the timestamp of the incoming data
localtime = time.asctime( time.localtime(time.time()) )
print(localtime)
# clear serial buffer
ser.reset_input_buffer()
ser.close()
I have tried to make this very self-explanatory and as simple as possible as a jumping off point for anyone who cares to use it.
I also use Python/Arduino for setting the date/time on RTC chips and getting CSV data from my hard drive.
Have fun expanding on this! It is all about the learning process!
Great example Steve. I love the way you kept the example as simple as possible. Great work!
I do have a question concerning hardware and OS. What are you running the python on?
I notice that when you open the serial port you use the following code:
port='/dev/cu.usbmodem1461',\
Is that port going to be machine or OS dependent? For example, if I ran this Python code on a Raspberry Pi would it still work?
I'm looking forward to trying to run this on my Windows 10 Notebook computer. But I don't have time to do it at the moment. But my first thought was to wonder if it would also run on a Raspberry Pi, or if it would need to be modified to run on the Pi.
DroneBot Workshop Robotics Engineer
James
Python, in my case, is running on MacOS but that is irrelevant, just use the same port name as shown in the Arduino IDE. Or check the System Info on Windows. It should run on a RaspPI as well.
Can't say much more about Windows operating system, because my last Windows computer ran 98SE and landed in the corner of my office in pieces after the 10th BSOD in one day, and being thrown with considerable force at the wall.
Thanks for the info. I'll see if I can find time to try on my Windows notebook and see if it works. Then maybe later I'll try it on a Pi. I'm only just now learning to use Python.
DroneBot Workshop Robotics Engineer
James
The most significant discovery I made when I first ran this code was that the Arduino communicates exclusively with the outside world using byte arrays, both for serial and I2C communication.
At that point, the penny dropped and explained why some of the things I was trying to do, just didn't work.
I was thinking too much in terms of a microprocessor and not enough about microcontrollers.
Have fun expanding on this! It is all about the learning process!
Great post @pugwash!
I use python to link to my microcontrollers and the RPI. For a few of my projects, I have added a GUI element to the app. I have used Tkinter since it's already included with the python installs. Sometimes is nice to add an easy interface to your project.
Here is an example app:
The code:
#!/usr/bin/env python # # For Linux, BSD or Mac OSX you can chmod +x on this script to make executable # ########### # Rover control app # # Allows for simple multi-byte commands. # # Written by Scott Beasley - 2015 # Free to use or modify. Enjoy. # ########### import sys import serial import time from Tkinter import * import tkFont import tkMessageBox # Create the window for the application class App (Frame): # Make the window def createWidgets (self): self.connected = False self.message = StringVar ( ) # Make a little font for the gripper set buttons. helv6 = tkFont.Font (family = 'Helvetica', size = 6, weight = 'normal') self.frame = Frame (self.master) self.frame.pack ( ) self.f1 = Frame (self.frame) self.l1 = Label (self.f1, text = "Comm Port: ") self.l1.pack (side = LEFT) self.comm_entry = Entry (self.f1, bd = 5, name = "comm_entry") self.comm_entry.pack (side = LEFT) self.connectButton = Button (self.f1, text = "Connect", command = self.SerialConnect, name = "b_connect") self.connectButton.pack (side = LEFT) self.disconnectButton = Button (self.f1, text = "Disconnect", command = self.SerialDisconnect, name = "b_disconnect") self.disconnectButton.pack (side = RIGHT) self.f1.grid (row = 0, column = 0) self.f2 = LabelFrame (self.frame, bd = 3, relief = "groove", text="Ground Control") self.g_vert_fm = Frame (self.f2) self.grip_vert = Scale (self.g_vert_fm, from_ = 0, to = 180) # Bind on mouse button 1 release to set the boom position self.grip_vert.bind ('', self.GripperY) self.grip_vert.grid (row = 0, column = 0, rowspan = 4, sticky = W) self.master.bind ("", self.GripperY) self.g_vert_fm.grid (row = 0, column = 0, rowspan = 4, sticky = W) self.leftforwardButton = Button (self.f2, text = "\\", command = self.TurnLeft45, name = "b_left_forward") self.leftforwardButton.grid (row = 0, column = 1) self.master.bind ("<a rel="nofollow">", self.TurnLeft45) self.leftButton = Button (self.f2, text = "<", command = self.TurnLeft, name = "b_left") self.leftButton.grid (row = 1, column = 1) self.master.bind ("</a><a rel="nofollow">", self.TurnLeft) self.rightforwardButton = Button (self.f2, text = "/", command = self.TurnRight45, name = "b_right_forward") self.rightforwardButton.grid (row = 0, column = 3) self.master.bind ("</a><a rel="nofollow">", self.TurnRight45) self.haltButton = Button (self.f2, text = "Halt!", command = self.Halt, name = "b_halt") self.haltButton.grid (row = 1, column = 2) self.master.bind ("</a><a rel="nofollow">", self.Halt) self.rightButton = Button (self.f2, text=">", command = self.TurnRight, name = "b_right") self.rightButton.grid(row = 1, column = 3) self.master.bind ("</a><a rel="nofollow">", self.TurnRight) self.upButton = Button (self.f2, text="^", command = self.Forward, name = "b_forward") self.upButton.grid (row = 0, column = 2) self.master.bind ("</a><a rel="nofollow">", self.Forward) self.leftdownButton = Button (self.f2, text = "/", command = self.TurnRight45, name = "b_left_down") self.leftdownButton.grid (row = 2, column = 1) self.downButton = Button (self.f2, text="V", command = self.Reverse, name = "b_reverse") self.downButton.grid (row = 2, column = 2) self.master.bind ("</a><a rel="nofollow">", self.Reverse) self.f2.grid (row = 1, column = 0, pady = 25) self.rightdownButton = Button (self.f2, text = "\\", command = self.TurnLeft45, name = "b_right_down") self.rightdownButton.grid (row = 2, column = 3) self.g_horz_fm = Frame (self.f2) self.grip_horz = Scale (self.g_horz_fm, from_ = 0, to = 180, orient = HORIZONTAL) # Bind on mouse button 1 release to set the gripper position self.grip_horz.bind ('</a><a rel="nofollow">', self.GripperX) self.grip_horz.grid (row = 0, column = 0, columnspan = 7, sticky = E) self.master.bind ("<u>", self.GripperX) self.g_horz_fm.grid (row = 4, column = 0, columnspan = 7, sticky = E) self.master.bind ("</u></a><a rel="nofollow"><u>", self.GripperHome) self.f_spd_dist = Frame (self.frame) self.l_speed = Label (self.f_spd_dist, text = " Speed: ") self.l_speed.grid (row = 0, column = 0) self.speed_spin = Spinbox (self.f_spd_dist, values = (1, 2, 3), width = 2, command = self.SetSpeed) self.speed_spin.grid (row = 0, column = 1) self.l_dist = Label (self.f_spd_dist, text = " Distance: ") self.l_dist.grid (row = 0, column = 3) self.dist_spin = Spinbox (self.f_spd_dist, values = ('Short', 'Medium', 'Continuous'), width = 10, command = self.SetDistance) self.dist_spin.grid (row = 0, column = 4) self.l_turns = Label (self.f_spd_dist, text = " Turns: ") self.l_turns.grid (row = 0, column = 5) self.turns_spin = Spinbox (self.f_spd_dist, values = ('Hard', 'Soft'), width = 5, command = self.SetTurns) self.turns_spin.grid (row = 0, column = 6) self.f_spd_dist.grid (row = 2, column = 0) self.f3 = Frame (self.frame) self.l2 = Label (self.f3, text = "Last action: ") self.l2.pack (side = LEFT) self.l3 = Label (self.f3, text=" ", textvariable = self.message) self.l3.pack (side = RIGHT) self.f3.grid (row = 5, column = 0, pady = 8) # Set the state of the bot control buttons. Enable when connected, # Disabled otherwise. def CtrlButtonsState (self, bstate): self.leftforwardButton.config (state = bstate) self.leftButton.config (state = bstate) self.rightforwardButton.config (state = bstate) self.rightButton.config (state = bstate) self.upButton.config (state = bstate) self.leftdownButton.config (state = bstate) self.downButton.config (state = bstate) self.rightdownButton.config (state = bstate) self.haltButton.config (state = bstate) self.disconnectButton.config (state = bstate) self.grip_horz.config (state = bstate) self.grip_vert.config (state = bstate) # Set the state of the comm port entry. Enable when not connected, # Disabled when the bot is connected. def ConnCtrlsState (self, bstate): self.connectButton.config (state = bstate) self.comm_entry.config (state = bstate) # Connect to the comm port typed in the comm entry field. def SerialConnect (self): try: # Change the baud rate here if diffrent then 9600 self.ser = serial.Serial (self.comm_entry.get ( ), 9600) except IOError: tkMessageBox.showerror ("Invalid comm port", "Comm port not found.") return self.ConnCtrlsState (DISABLED) self.CtrlButtonsState (NORMAL) self.message.set ("SerialConnect") self.connected = True time.sleep (3) # Dwell a bit for the connection to happen # Do some after connection bot talking. if self.connected == True: self.setbotParms ( ) # Disconnect from the bot (close the comm port). def SerialDisconnect (self): try: # Send a Halt command just in case the bot is still moving. self.send_cmd ('h', "Halt!") time.sleep (1) self.ser.close ( ) except IOError: print "Could not close port..." self.message.set ("SerialDisconnect") self.ConnCtrlsState (NORMAL) self.CtrlButtonsState (DISABLED) self.connected = False time.sleep (2) # Dwell a bit for the disconnection to happen # Send the command to the open comm port # the action input variable is a tuple that allows for multy part commands def send_cmd (self, action, msg): if self.connected == True: for val in action: self.ser.write (val) self.ser.flush ( ) self.message.set (msg) # Send the bot a turn-left command. def TurnLeft (self, event = None): self.send_cmd ('a', "Left") # Send the bot a turn-left-up command. def TurnLeft45 (self, event = None): self.send_cmd ('q', "Left45") # Send the bot a turn-right command. def TurnRight (self, event = None): self.send_cmd ('s', "Right") # Send the bot a turn-right-up command. def TurnRight45 (self, event = None): self.send_cmd ('e', "Right45") # Send the bot a Forward command. def Forward (self, event = None): self.send_cmd ('w', "Up") # Send the bot a Reverse command. def Reverse (self, event = None): self.send_cmd ('z', "Down") # Send the bot a Halt command. def Halt (self, event = None): self.send_cmd ('h', "Halt!") # Set the gripper (X). def GripperX (self, event = None): # Read the slider control and send the value to the bot controller # Note: 0 is all the way closed and 180 is all the way open # This is a multi-byte command. The command + the servo set val # + a buffer clear command (optional) grp_change = ('>', chr (self.grip_horz.get ( )), chr (255)) self.send_cmd (grp_change, "Gripper X") self.boomchange = None # Set the gripper Y. def GripperY (self, event = None): # Read the slider control and send the value to the bot controller # Note: 0 is all the way up and 180 is all the way down # This is a multi-byte command. The command + the servo set val # + a buffer clear command (optional) grp_change = ('^', chr (self.grip_vert.get ( )), chr (255)) self.send_cmd (grp_change, "Gripper Y") self.gripperchange = None # Set the gripper to the "home" position. def GripperHome (self, event = None): self.send_cmd (('c', chr (255)), "Gripper Home") self.grip_horz.set (90) self.grip_vert.set (90) # Set the speed of the rover. def SetSpeed (self, event = None): spd_str = self.speed_spin.get ( ) if spd_str == "1": speed = 1 if spd_str == "2": speed = 2 if spd_str == "3": speed = 3 # Sends a multi-byte command to set the speed. self.send_cmd (('v', chr (speed)), "Set Speed") # Set the distance travled when a for or rev is issued to # the rover. def SetDistance (self, event = None): dist = 1 dist_str = self.dist_spin.get ( ) if dist_str == "Short": dist = 1 if dist_str == "Medium": dist = 2 if dist_str == "Continuous": dist = 3 # Sends a multi-byte command to set the distance. self.send_cmd (('d', chr (dist)), "Set Distance") # Set the turn style to use turning. def SetTurns (self, event = None): turn = 1 turn_str = self.turns_spin.get ( ) if turn_str == "Hard": turn = 1 if turn_str == "Soft": turn = 2 # Sends a multi-byte command to set the turn style. self.send_cmd (('t', chr (turn)), "Set Turns") def setbotParms (self): self.Halt ( ) self.GripperHome ( ) self.SetSpeed ( ) self.SetDistance ( ) self.SetTurns ( ) def __init__ (self, master = None): Frame.__init__ (self, master) self.pack ( ) self.createWidgets ( ) self.CtrlButtonsState (DISABLED) # Kick off the GUI (Tk) then size and title the app window def main ( ): root = Tk ( ) root.geometry ("450x360") root.wm_title ("Rover Control Center (RCC)") app = App (master = root) app.mainloop ( ) if __name__ == '__main__': main ( )
Thanks for sharing your Tkinter GUI code Scott. I've messed around with Tkinter GUI before myself. But it's nice to have example code like you've posted to work from.
This DroneBot Forum was a GREAT idea that Bill had. It's going to be a gold mine of resources.
DroneBot Workshop Robotics Engineer
James