Does Python have an interface

by Ralf Beesner
Electronics laboratory projects AVR







Why Python?

I find programming exciting, but I just don't have any programming talent. Therefore it is only sufficient for small, clear microcontroller projects.

However, quite a few had missed the possibility of conveniently communicating with the microcontrollers from the PC or notebook via a graphical interface (GUI) with buttons, switches and input fields - not just via a terminal program.

Some attempts to "sniff" into IDEs or toolkits for graphic programming, however, came to nothing. That was all too complicated for me and / or had too short a half-life due to frequent updates and API changes.

I came across Python through Burkhard's article about Python and TK on the Raspberry Pi. Python code is relatively easy to read, and since the Python "inventor" is Dutch, it seems that he was thinking of people who don't sit in front of a US keyboard and don't want to clench their hands while typing in curly braces.

I had always dismissed interpreter languages ​​as lame and cumbersome (because of the interpreter required on the target system), but that was nonsense. Speed ​​is rarely an issue. If Python is installed, you already have the interpreter, a simple IDE ("Idle") and the graphic toolkit ("TKinter") on board, and the resource requirements - compared to "modern" programming environments - are almost ridiculous. Python is available as standard on almost all Linux systems and the usual MSI installation packages are available for Windows.

In this respect, the Raspberry Pi was a game changer - it made both Linux and Python popular as "child's play" alternatives to Windows and C ++ or Java. You can now primarily rely on Linux and only think of Windows users secondarily.

Admittedly - Idle and TKinter are already quite dusty and have all sorts of weaknesses, but are reasonably manageable for beginners. When the Python knowledge is a little more established, you can always switch to something better.



Which version?

Python exists in two main versions, which are not completely compatible, but have been maintained in parallel for about 10 years. Python2 is "developed from" - there are updates, but hardly any functional changes. So I first decided on Python2 (Python 2.7 and pyserial 2.7). Python2 code can be rewritten in Python3 code with little effort.

Countless software packages can be installed using Python. In order to reduce complexity and sources of error, I initially limited myself to the "pyserial" package, which is required if you want to use serial interfaces.



Serial interfaces under Python

As a beginner's project, I set myself the goal of a GUI for the AK module bus AD210 interface. A clone of the AD210 interface is described here:

AD210Clone.html

To try out the Python code, you can use an unmodified LP microcontroller hardware instead of the AD210 replica - the voltages displayed are then incorrect.

The AD210 only answers when asked. If you send it the byte "1" (not to be confused with the ASCII character for "1", which is 49), it replies with the byte "100". First a small (text mode) program that asks for the ID without a GUI, i.e. on the command line:

#! / usr / bin / python

import serial, sys, time

ser = serial.Serial ("/ dev / ttyUSB1", 38400)
time.sleep (0.5) # Waiting for DTR (supplies AD210 with voltage)
ser.flushInput ()
print "starting"

while1:
ser.write (chr (1))
x = ser.read ()
char = ord (x)
print char
time.sleep (0.5)



 

The name of the serial interface may have to be changed. If you abort the program with "Control-C", an error message appears. It would be better if you don't have to shut down the program from the outside, but can end it in an orderly manner with a key, e.g. "q".



Another console program

With "import sys" operating system commands are imported, with the command "kbd" one can query keyboard entries. However, there is a problem: waiting for keystrokes blocks the program and with it all serial communication. In addition, "kbd" expects that all entries are completed with the return key.

The solution is to move the keyboard query to a separate thread. It is restarted by the main program. Under Python, global variables (which also apply outside of the respective function) are frowned upon. However, if my Python Beginner's Book is right, they are required if a thread is to communicate with its higher-level function. Phew, lucky (if you program a lot on the ATtiny13 with its 64 byte RAM, you get used to the use of global variables, because with local variables you quickly get puzzling problems due to stack overflows).


In the following program, the function for the keyboard query ("input_thread") is defined first.

The main program first tests whether the specified serial interface is available at all, then sends out a byte "1" and listens to see whether a byte "100" comes back as a sign of life from the AD210.

Then the previously defined keyboard function is started as a separate thread.

The central program loop follows. It runs within the "while (1)" block and cyclically sends the command bytes "58" and "59" to the AD210 and outputs its responses in text mode.

I tried this query block first:

ser.write (chr (58)) # query first ADC
time.sleep (0.002) # 2 ms response time for the ATtiny
wait = ser.inWaiting ()
if (wait! = 0):


It didn't make sense as the AD210 needs some time to respond. You therefore have to wait a short time of 1-2 ms before checking for received bytes. However, the waiting time is critical: if you wait too long, the response from the AD210 is lost, if you don't wait, there is nothing in memory yet and the program also ignores the query. In both cases, implausible values ​​appear in the output or the two channels appear interchanged.

It turned out to be best to do without "ser.inWaiting ()". However, the program will be blocked if the AD210 no longer responds for any reason. But you can specify a timeout (e.g. 2s) when opening the serial connection, then the program will continue to run after this time has elapsed and you can at least terminate it normally.

If the variable "kbd" is not empty, e.g. because you have entered "q Return" or "200 Return", the program is terminated in the first case and the PWM output is set to 200 in the second case.

Danger; this program only runs in the idle console if you repeatedly press the return key. However, it runs flawlessly directly in a Linux Xterm or on the Windows command line.

#! / usr / bin / python

# license: wtfpl
# http://www.wtfpl.net/txt/copying/

# AD210 does not work with ASCII characters, but "raw" bytes (8 bit)
# 1-byte commands, one or two bytes (bytehi, bytelow) as a response
# "1" requests the ID of the AD210
# "58" reads ADC2 (PinB4)
# "59" reads ADC2 (PinB4)
# "64" + 0 ... 255 sets PWM output (PinB0)

# raw_input

# Service:
# raw_input must always be terminated with
# q : end
# Number 0 ... 255 : Set PWM output


import serial, sys, time, thread


definput_thread ():
global kbd
kbd = ''
while (1):
kbd = raw_input ()



# Main program

try:
ser = serial.Serial ("/ dev / ttyUSB1", 38400, timeout = 2) # under Windows: "com1" to "com255"
time.sleep (0.5) # Waiting for DTR (supplies AD210 with voltage)
ser.flushInput ()
except:
print "-----> serial interface not available"
quit ()

ser.write (chr (1)) # sends raw 1
time.sleep (0.01)
if (ser.inWaiting () == 0):
print "-----> AD210 does not answer!"
quit ()

x = ord (ser.read ()) # 1-byte string, converted to raw (integer)

if (x == 100): # "Sign of life" of the AD210
print "-----> AD210 connected!"
else:
print "-----> No connect"
quit ()


thread.start_new_thread (input_thread, ()) # start input thread


while1:

ser.write (chr (58)) # query first ADC
x1 = ser.read () # hi_byte
x2 = ser.read () # low_byte
# the 1-byte strings are first converted into ints, then added,
# then converted back to string
s1 = str (256 * ord (x1) + ord (x2))

ser.write (chr (59)) # query second ADC, better variant
y1 = ser.read ()
y2 = ser.read ()
s2 = str (256 * ord (y1) + ord (y2))
print "ch1 =", s1, "ch2 =", s2 #

time.sleep (0.5) # can be reduced to 0



if (kbd! = ""):
if (kbd == "q"):
quit ()

try:
pwm = int (kbd)
if (pwm> 255) or (pwm <0):
print "-----> number between 0 and 255!"
else:
ser.write (chr (64)) # announce pwm value
ser.write (chr (pwm))
except:
print "-----> enter number"
kbd = "" # delete old value




 



Now finally a GUI program!


 

First, the thread that communicates with the AD210 is defined ("get_values"). Before it enters its main loop "while (1)", some initial values ​​are defined and the function "choose_ser ()" is called.

"Choose_ser" first calls another function "scan_serial" and lets it list the existing serial interfaces. "Scan_serial" tries - both on Windows systems and under Linux - to close possibly existing serial interfaces (I "borrowed" the nice trick from an ELEKTOR Python listing). The error messages for non-existent interfaces are intercepted (with "try" - "exept pass"), the successful attempts are entered in the "portnames" list. The list is returned to the calling function "choose_ser" with "return portnames" and processed there under the same name (which, however, only applies within the respective function and is not defined as a global variable).

"choose_ser" then sends a byte "1" to the interfaces in the list and tries to find one that sends a byte "100" back.

It can lead to problems if the function also closes serial interfaces to which uninvolved devices are attached, but I just liked the automatic too much compared to a "manual" selection of the interface via a dialog box.

"choose_ser" returns the variables "port" and "ser". "ser" contains the parameters of the selected serial interface and "port" is displayed in the GUI via the "text8" variable.

The other functions "get_interval", start_log "," stop_log "and" end "are called by the GUI.

While the measured values ​​are updated as quickly as possible in the GUI, any interval> ~ 10 msec can be selected for the storage process. Decimal fractions must be entered with a point instead of a comma. You can choose the file name freely; if none is entered, it is set to "ad210.csv".

The GUI is defined below main = Tk (). It consists of 8 frames that are arranged one above the other. Tkinter thinks about the arrangement of the elements. With the parameters side = "left" / "right" or "top" / "bottom" and anchor = "w" (for "west") you can maneuver the elements halfway to the places where you want them . Tkinter sets the size of the frames as small as possible. If you want to create some "air" around the control elements, the parameters "padx" and "pady" are used. It should be noted that different standard fonts influence the relationship between the frames and elements and can change the arrangement.

The names of the frames are numbered and I have also given the elements the same numbers in order to "climb through" better. The construction of the GUI could be written more compactly (e.g. with program loops for recurring elements or by squeezing several of the individual instructions into one line), but it is certainly clearer for us beginners.

The main program is at the very end and consists of only two lines. The first starts the thread, the second the GUI.

To the left of the slider is displayed the voltage that is set when the PWM signal is smoothed with a low pass (e.g. with 10 kOhm / 1µF).


 


Closing remark

As a beginner, cackling immediately after "laying the first programming eggs" is quite double-edged, because you pass on "improvable" programming style and cumbersome approach to the reader. On the other hand, it shows that as a beginner in Python you can "paste up" a GUI very quickly that allows comfortable communication with a microcontroller.

In addition, the integration of a serial interface is more likely to be seen as an advanced topic - it was not dealt with in my Python beginners book. So I hope the harm outweighs the benefit, and anyone who knows more is invited to fix and improve the code. But then please with plenty of commented source text so that you get something out of it. "Elegant" code is often difficult to understand - at least for beginners.


Download the source code: 0517-ad210-python.zip

#! / usr / bin / python

# license: wtfpl
# http://www.wtfpl.net/txt/copying/

import thread
import time
import serial
from Tkinter import *
import tkMessageBox
file_is_open = 0


# ----- thread that runs independently of the GUI: ------

defget_values ​​():

port, ser = choose_serial ()
text8 = port
time_old = time.time ()
scale_old = 0
pwm = "0.00"
text2.delete (1.0,2.0)
text2.insert (INSERT, pwm + "V")

while (1):


## Query slider, generate PWM

scale_new = scale.get () # scale_get returns integer
if (scale_new! = scale_old): # only send if value has been changed
ser.write (chr (64) + chr (scale_new))
time.sleep (0.001)
scale_old = scale_new
scale_f = "{0: 2.2f}". format (scale_new * 0.01295)
pwm = str (scale_f)
text2.delete (1.0,2.0)
text2.insert (INSERT, pwm + "V")

## query first ADC

reference1 = ref1.get ()
if (reference1 == "lo"):
ser.write (chr (56))
else:
ser.write (chr (58))
x11 = ser.read ()
x12 = ser.read ()
# the two 1-byte strings are first converted into ints,
# then added, then converted back into a string:
res11 = 256 * ord (x11) + ord (x12)
if (reference1 == "lo"):
res12 = res11 * 0.01 # Conversion of ADC values ​​into volts
else:
res12 = res11 * 0.03
res12_f = "{0: 2.2f}". format (res12) # format to 2 decimal places
res13 = str (res12_f)
lb_adc1.delete (1.0,2.0)
lb_adc1.insert (INSERT, res13 + "V")

## query second ADC
reference2 = "lo"
reference2 = ref2.get ()
if (reference2 == "lo"):
ser.write (chr (57))
else:
ser.write (chr (59))
x21 = ser.read ()
x22 = ser.read ()
res21 = 256 * ord (x21) + ord (x22)
if (reference2 == "lo"):
res22 = res21 * 0.01
else:
res22 = res21 * 0.03
res22_f = "{0: 2.2f}". format (res22)
res23 = str (res22_f)
lb_adc2.delete (1.0,2.0)
lb_adc2.insert (INSERT, res23 + "V")


## write in the file:
if (file_is_open == 1):
interval = get_interval ()
time_new = time.time ()
if ((time_new - time_old)> = interval):
time_old = time_new
try:
ltime = time.localtime ()
h, m, s = ltime [3: 6]
time_string = "{0: 02d}: {1: 02d}: {2: 02d}". format (h, m, s)
logfile.write (time_string + chr (9) + res13 + chr (9) + res23 + "\ r")
except:
print "writing failed"

# ---- end of thread -----



defget_interval ():

get_interval = entry5.get ()
try:
interval = float (get_interval)
except:
interval = 1
return interval


defstart_log ():

global logfile
global file_is_open

filename = "ad210.csv"
getfilename = entry6.get ()
if getfilename! = "":
filename = getfilename
logfile = open (filename, "a +") # + a: append to file
file_is_open = 1
print "filename =", filename



defstop_log ():
global file_is_open
try:
logfile.close ()
file_is_open = 0
except:
passport



# ----- search for AD210 -----

defchoose_serial ():

speed = "38400"
ad210port = ""
portnames = scan_serial ()
print "found", portnames
for port in portnames:

try:
ser = serial.Serial (port, speed, timeout = 2)
ser.setDTR () # DTR = power supply for AD210
ser.setRTS () # ditto
time.sleep (0.2) # Waiting time for power supply (> 0.1s)
ser.flushInput ()
ser.flushOutput ()
ser.write (chr (1))
time.sleep (0.02)
if (ser.inWaiting ()! = 0):
x = ord (ser.read ()) # 1-byte string is converted to int
if (x == 100): # 100: Sign of life of the AD210
print "ad210 connected to", port
text8 ["text"] = "Port:" + port
ad210port = port
break
# ser.close ()
except:
passport
iflen (ad210port) == 0:
print "no ad210 found"
tkMessageBox.showerror ("Error", "no AD210 found!")

return ad210port, ser


# ----- search for existing serial interfaces -----

defscan_serial ():

portnames = []

# Windows
for i inrange (127):
try:
name = "COM" + str (i)
s = serial.Serial (name)
s.close ()
portnames.append (name)
except:
passport

# Linux (fixed interfaces)
for i inrange (8):
try:
name = "/ dev / ttyS" + str (i)
s = serial.Serial (name)
s.close ()
portnames.append (name)
except:
passport

# Linux (USB serial)
for i inrange (8):
try:
name = "/ dev / ttyUSB" + str (i)
s = serial.Serial (name)
s.close ()
portnames.append (name)
except:
passport

# Linux (HID serial)
for i inrange (8):
try:
name = "/ dev / ttyACM" + str (i)
s = serial.Serial (name)
s.close ()
portnames.append (name)
except:
passport
iflen (portnames) == 0:
print "no serial found"
tkMessageBox.showerror ("Error", "No serial interface found!")
quit ()
return portnames



#----- Exit program ------

defende ():
print ("End!")
try:
ser.write (chr (64) + chr (0))
ser.close ()
logfile.close ()
thread.stop (get_values, ())
except:
passport
main.destroy ()



# ----- GUI elements ---------------

main = Tk ()

# frame1
frame1 = (Frame (main))
frame1.pack ()
text1 = label (frame1, text = "control program for AD210", font = "bold")
text1.pack (anchor = "w")


# frame2, slider
frame2 = (Frame (main))
frame2.pack (anchor = "w")
label2 = label (frame2, text = "PWM")
label2.pack (side = "left", padx = 10, pady = 16)
text2 = Text (frame2, bg = "orange", width = 6, height = 1, font = (12))
text2.pack (side = "left", pady = 10)
scale = Scale (frame2, orient = "horizontal",
length = 256, resolution = 1, to = 255) # Length slider on screen, resolution, end value
scale.pack (side = "right", padx = 10)


# frame3, ADC1
frame3 = (Frame (main))
frame3.pack (anchor = "w")
label3 = label (frame3, text = "ADC1")
label3.pack (side = "left", padx = 10)
lb_adc1 = Text (frame3, bg = "orange", width = 6, height = 1, font = (12))
lb_adc1.pack (side = "left")
# Selection button measuring range ADC1
ref1 = StringVar ()
ref1.set ("lo")
rb3 = radio button (frame3, text = "10V", variable = ref1, value = "lo")
rb3.pack (side = "left", padx = 10)
rb3 = radio button (frame3, text = "30V", variable = ref1, value = "hi")
rb3.pack (side = "left")


# frame4, ADC 2
frame4 = (Frame (main))
frame4.pack (anchor = "w")
label4 = label (frame4, text = "ADC2")
label4.pack (side = "left", padx = 10)
lb_adc2 = Text (frame4, bg = "orange", width = 6, height = 1, font = (12))
lb_adc2.pack (side = "left", pady = 10)
# Selection button measuring range ADC2
ref2 = StringVar ()
ref2.set ("lo")
rb4 = radio button (frame4, text = "10V", variable = ref2, value = "lo")
rb4.pack (side = "left", padx = 10)
rb4 = radio button (frame4, text = "30V", variable = ref2, value = "hi")
rb4.pack (side = "left")


# frame5, input interval
frame5 = (Frame (main))
frame5.pack (anchor = "w")
entry5 = Entry (frame5, width = 5, bg = "white")
entry5.pack (side = "left", padx = 10)
text5 = Label (frame5, text = "Interval of log file entries (in s)")
text5.pack (side = "left")


# frame6, input log file name
frame6 = (Frame (main))
frame6.pack (anchor = "w")
entry6 = Entry (frame6, width = 16, bg = "white")
entry6.pack (side = "left", padx = 10)
text6 = Label (frame6, text = "Name of the log file")
text6.pack (side = "left", pady = 10)


# frame7, start / stop log
frame7 = (Frame (main))
frame7.pack (anchor = "w")
widget_start = Button (frame7, text = "Start Log", command = start_log)
widget_start.pack (side = "left", padx = 10)
widget_stop = button (frame7, text = "Stop Log", command = stop_log)
widget_stop.pack (side = "left")


# frame8, Quit button
text8 = label (main, text = "")
text8.pack (side = "left", padx = 10, pady = 10)
button1 = Button (main, text = "Quit", command = end)
button1.pack (side = "right", padx = 10)



# ----- Main Loop ---------------

thread.start_new_thread (get_values, ()) # thread for querying the serial interface
main.mainloop () # calls GUI