Gauges in a Python Canvas

There are some nice Python packages like tk_tools, that can be used for IoT indicators and gauges.

My daughter and I had a project where we wanted to repurpose an old eReader to be a kitchen kiosk display. Unfortunately tk_tools doesn’t support Python 2.7, gray scale or larger text.

This blog documents how we made some simple update-able gauges using Python Tkinter Canvas objects that are supported in both Python 2.7 and 3.x .

Getting Started

Unfortunately the Python 2 and 3 Tkinter libaries are named differently (Tkinter in 2.7 vs tkinter in 3.x). If you are coding for both Python 2.7 and 3.x this gets messy, a simple workaround in your code is:

# Manage Python 2.7 and 3.x
#
import sys
# Check the version of Python and use the correct library
if sys.version_info[0] == 2:
    import Tkinter
else:
    import tkinter as Tkinter

Analog Clock

A Tkinter canvas supports a number of basic objects such as rectangles, circles, arcs, text, etc. The basic objects are positioned within the canvas space.

I found that as a first example an analog clock was a good place start. The first pass code for a clock with just the second hand would be:

# A Clock Second Hand Example
#
import tkinter as Tkinter # Python 3.x
import datetime

def update_sec():
    #Reposition the second hand starting position
    thesec = datetime.datetime.now().second
    arcstart = 90 - thesec*6  #0 sec = 90deg
    C.itemconfig(asec,start=arcstart) #pass the new start position
    C.after(1000, update_sec)

# Create a canvas object with an oval face and a second hand
top = Tkinter.Tk()

C = Tkinter.Canvas(top, bg="silver", height=250, width=300)
C.pack()


coord = 10, 50, 240, 210 
C.create_oval(coord,  fill="white")
# Have the second hand start at the top (90 deg) with 1 deg arc
asec = C.create_arc(coord, start=90, extent=1, width=3)

C.after(1000, update_sec)
top.mainloop()

The key point is to get the id of the seconds hand arc (asec). The itemconfig method is then used to change the starting position of seconds hand arc (C.itemconfig(asec,start=arcstart) ).

The arc positioning is a little backwards, 0 degrees is at 3o’clock and then goes counter-clockwise.

The next step is to add narrow arcs for the minutes and hours. Also text could be used to digitally show the date and time. For the hour and minute hand I used different colours and thicknesses.

#
# A Clock Example
#
import tkinter as Tkinter # Python 3.x
from datetime import datetime

def update_sec():
    # Position the hands 
    C.itemconfig(asec,start= 90 - datetime.now().second*6)
    C.itemconfig(amin,start= 90 - datetime.now().minute*6)
    C.itemconfig(ahour,start= 90 - datetime.now().hour*360/12)
    C.itemconfig(dtime,text = datetime.now().strftime("%d/%m/%Y %H:%M:%S"))
    C.after(1000, update_sec)

# Create a canvas object with an oval face and a second hand
top = Tkinter.Tk()

C = Tkinter.Canvas(top, bg="silver", height=250, width=300)
C.pack()

coord = 10, 50, 240, 210
C.create_oval(coord,  fill="white")
# Have the second hand start at the top (90 deg) with 1 deg arc
asec = C.create_arc(coord, start=90, extent=1, width=2)
amin = C.create_arc(coord, start=90, extent=1, width=4, outline='blue')
ahour = C.create_arc(coord, start=90, extent=1, width=6, outline='red')
dtime = C.create_text(120,20, font="Times 16 bold", text="00:00:00")

C.after(1000, update_sec)
top.mainloop()

Gauges

There are a number of different types of gauges. My first example was a speedometer graph, that used an arc for both the background and the gauge needle:

#
# Use Canvas to create a basic gauge
#
from tkinter import *
import random

def update_gauge():
    newvalue = random.randint(low_r,hi_r)
    cnvs.itemconfig(id_text,text = str(newvalue) + " %")
    # Rescale value to angle range (0%=120deg, 100%=30 deg)
    angle = 120 * (hi_r - newvalue)/(hi_r - low_r) + 30
    cnvs.itemconfig(id_needle,start = angle)
    root.after(3000, update_gauge)

    
# Create Canvas objects    

canvas_width = 400
canvas_height =300

root = Tk()

cnvs = Canvas(root, width=canvas_width, height=canvas_height)
cnvs.grid(row=2, column=1)

coord = 10, 50, 350, 350 #define the size of the gauge
low_r = 0 # chart low range
hi_r = 100 # chart hi range

# Create a background arc and a pointer (very narrow arc)
cnvs.create_arc(coord, start=30, extent=120, fill="white",  width=2) 
id_needle = cnvs.create_arc(coord, start= 119, extent=1, width=7)

# Add some labels
cnvs.create_text(180,20,font="Times 20 italic bold", text="Humidity")
cnvs.create_text(25,140,font="Times 12 bold", text=low_r)
cnvs.create_text(330,140,font="Times 12 bold", text=hi_r)
id_text = cnvs.create_text(170,210,font="Times 15 bold")

root.after(3000, update_gauge)

root.mainloop()

The basic gauge can be enhanced to have more value ranges and colour hihi/hi/low ranges:

#
# Use Canvas to create a basic gauge
#
from tkinter import *
import random

def update_gauge():
    newvalue = random.randint(low_r,hi_r)
    cnvs.itemconfig(id_text,text = str(newvalue) + " %")
    # Rescale value to angle range (0%=120deg, 100%=30 deg)
    angle = 120 * (hi_r - newvalue)/(hi_r - low_r) + 30
    cnvs.itemconfig(id_needle,start = angle)
    root.after(3000, update_gauge)

    
# Create Canvas objects    

canvas_width = 400
canvas_height =300

root = Tk()

cnvs = Canvas(root, width=canvas_width, height=canvas_height)
cnvs.grid(row=2, column=1)

coord = 10, 50, 350, 350 #define the size of the gauge
low_r = 0 # chart low range
hi_r = 100 # chart hi range

# Create a background arc with a number of range lines
numpies = 8
for i in range(numpies):
    cnvs.create_arc(coord, start=(i*(120/numpies) +30), extent=(120/numpies), fill="white",  width=1)    

# add hi/low bands
cnvs.create_arc(coord, start=30, extent=120, outline="green", style= "arc", width=40)
cnvs.create_arc(coord, start=30, extent=20, outline="red", style= "arc", width=40)
cnvs.create_arc(coord, start=50, extent=20, outline="yellow", style= "arc", width=40)
# add needle/value pointer
id_needle = cnvs.create_arc(coord, start= 119, extent=1, width=7)

# Add some labels
cnvs.create_text(180,15,font="Times 20 italic bold", text="Humidity")
cnvs.create_text(25,140,font="Times 12 bold", text=low_r)
cnvs.create_text(330,140,font="Times 12 bold", text=hi_r)
id_text = cnvs.create_text(170,210,font="Times 15 bold")

root.after(3000, update_gauge)

root.mainloop()

Our Final Project

Our final project had 4 gauges that were based on basic gauge code. Our Python app ran full screen on a Kobo eReader that we installed Debian Linux on. The app connected to our Home Assistant Pi and showed us our current weather conditions.

We had to tweek the basic code a little bit to account for the 800×600 screen size and grey scale graphics.

Summary

In this blog we only looked at some basic gauges, the Tkinter Canvas component can be used in a very variety of different applications such as: bar charts, real time charts, graphics etc.

Home Assistant (REST) API

There are a few methods to communicate with Home Assistant. In this blog I wanted to document my notes on using the REST API.

Getting Started

I found that loading the File Editor Add-on made configuration changes quite easy. To load the File Editor, select the Supervisor item, then Add-on Store:

With the File Editor option you be able to modify your /config/configuration.yaml file. For this you’ll need to add an api: statement. I also added a command line sensor that shows the Raspberry Pi CPU idle time. I did this so that I could see a dynamic analog value:

After I made these changes I restarted my HA application, by the “Configuration” -> “Server Controls”.

Next I needed to create a user token. Select your user and then click on “Create Token” in the Long-Lived Access Tokens section. This token is very long and it only is shown once, so copy and paste it somewhere save.

Access the REST API with CURL

Curl is a command line utility that exists on Linux, Mac OS and Windows. I work in Linux mostly and it’s pre-installed with Ubuntu. If you’re working in Windows you’ll need to install CURL.

Getting started with curl isn’t required and you got straight to programming in your favourite language, however I found that it was usefully testing things out in curl before I did any programming.

The REST API is essentially an HTTP URL with some headers and parameters passed to it. For a full definition see the HA API document. The key items in REST API are:

  • Request type – GET or POST (note: there are other types)
  • Authorization – this is where the user token is passed
  • Data – is used for setting and defining tags
  • URL – the Home Assistant URL and the end point (option to view or set)

To check that the HA API is running a curl GET command can used with the endpoint of /api/.

$ curl -X GET -H "Authorization: Bearer eyJ0eXAiO....zLjc"   http://192.168.0.103:8123/api/

{"message": "API running."}

The user token is super long so your can use the \ character to break up your command. For example:

curl -X GET \
   -H "Authorization: Bearer eyJ0eXAiOiJKV......zLjc" \
   http://192.168.0.106:8123/api/states

Read a Specific HA Item

To get a specific HA item you’ll need to know its entity id. This can found by looking at the “Configuration” -> “Entities” page:

For my example I created a sensor called Idle Time, its entity id is: sensor.idle_time.

A curl GET command with the endpoint of /states/sensor.idle_time will return information on this sensor. The full curl command and the results would look like:

$ curl -X GET   -H "Authorization: Bearer eyJ0eXAiOiJKV1Q....zLjc"  \   http://192.168.0.103:8123/api/states/sensor.idle_time

{"entity_id": "sensor.idle_time", "state": "98.45", "attributes": {"unit_of_measurement": "%", "friendly_name": "Idle Time"}, "last_changed": "2020-12-12T17:34:10.472304+00:00", "last_updated": "2020-12-12T17:34:10.472304+00:00", "context": {"id": "351548f602f5a3887ff09f26903712bc", "parent_id": null, "user_id": null}}

Write to a New HA Item

A new or dynamic items can be created and written to remotely using a POST command with the definitions included in the data section. An example to create an entity called myput1 with a value of 88.6 would be:

curl -X POST \
   -H "Authorization: Bearer eyJ0eXAiOi....zLjc" \
   -H "Content-Type: application/json" \
   -d '{"state":"88.6", "attributes": {"unit_of_measurement": "%", "friendly_name": "Remote Input 1"}}' \
   http://192.168.0.103:8123/api/states/sensor.myinput1

This new entity is now available to HA and shown on the dashboard.

Write to a Switch

If you have a writeable device such as a switch you can use the REST to remotely control it.

For myself I have a Wemo switch with an entity name of : switch.switch1.

To control the switch the entity id is passed in the data section and the endpoint uses either a turn_on or turn_off parameter.

curl -X POST \
   -H "Authorization: Bearer eyJ0eXAiOiJ....zLjc" \
   -H "Content-Type: application/json" \
   -d '{"entity_id": "switch.switch1"}' \
   http://192.168.0.103:8123/api/services/switch/turn_on

Python and the HA API

Python can parse the JSON responses from the reading a sensor value:

from requests import get
import json

url = "http://192.168.0.103:8123/api/states/sensor.idle_time"
token = "eyJ0eXAiOiJK...zLjc"

headers = {
    "Authorization": "Bearer " + token,
    "content-type": "application/json",
}

response = get(url, headers=headers)

print("Rest API Response\n")
print(response.text)

# Create a json variable
jdata = json.loads(response.text)

print("\nJSON values\n")
for i in jdata:
    print(i, jdata[i])

The output will be something like:

Rest API Response

 {"entity_id": "sensor.idle_time", "state": "98.46", "attributes": {"unit_of_measurement": "%", "friendly_name": "Idle Time"}, "last_changed": "2020-12-12T19:29:10.655530+00:00", "last_updated": "2020-12-12T19:29:10.655530+00:00", "context": {"id": "2509c01cadb9e5b0681fa22d914e7b10", "parent_id": null, "user_id": null}}

 JSON values

 entity_id sensor.idle_time
 state 98.46
 attributes {'unit_of_measurement': '%', 'friendly_name': 'Idle Time'}
 last_changed 2020-12-12T19:29:10.655530+00:00
 last_updated 2020-12-12T19:29:10.655530+00:00
 context {'id': '2509c01cadb9e5b0681fa22d914e7b10', 'parent_id': None, 'user_id': None}

To write a value to myinput1 in Home Assistant:

from requests import post
import json

url = "http://192.168.0.103:8123/api/states/sensor.myinput1"
token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJkMDI2YjAxY2VkZWU0M2E1OWY1NmI1OTM2OGU1NmI0OSIsImlhdCI6MTYwNzc5Mzc0NCwiZXhwIjoxOTIzMTUzNzQ0fQ.qEKVKdadxNWp249H3s_nmKyzQMIu5WDQkS9hiT-zLjc"

mydata = '{"state":"99.3", "attributes": {"unit_of_measurement": "%", "friendly_name": "Remote Input 1"}}'

headers = {
    "Authorization": "Bearer " + token,
    "content-type": "application/json",
}

response = post(url, headers=headers,data =mydata)

print("Rest API Response\n")
print(response.text)

# Create a json variable
jdata = json.loads(response.text)


print("\nJSON values\n")
for i in jdata:
    print(i, jdata[i])

The output will look something like:

Rest API Response

 {"entity_id": "sensor.myinput1", "state": "99.3", "attributes": {"unit_of_measurement": "%", "friendly_name": "Remote Input 1"}, "last_changed": "2020-12-12T20:40:05.797256+00:00", "last_updated": "2020-12-12T20:40:05.797256+00:00", "context": {"id": "31b422d02db41cde94470ebae7fac48c", "parent_id": null, "user_id": "1392a10c7bbb4cf0891a7f8a351740c7"}}

 JSON values

 entity_id sensor.myinput1
 state 99.3
 attributes {'unit_of_measurement': '%', 'friendly_name': 'Remote Input 1'}
 last_changed 2020-12-12T20:40:05.797256+00:00
 last_updated 2020-12-12T20:40:05.797256+00:00
 context {'id': '31b422d02db41cde94470ebae7fac48c', 'parent_id': None, 'user_id': '1392a10c7bbb4cf0891a7f8a351740c7'}

Final Comments

A REST API interface allows foreign devices such as PCs, Raspberry Pi and Arduino module to be used as remote I/O devices.

There are REST client HTTP libraries that are available for the Arduino, however it might be cleaner to implement an MQTT interface instead.

SQLite/Bottle Todo List

I wanted to do a Todo List web application that I could pass on to my kids to try. My goal was to give them an introduction to SQL, Web interfaces and Web templating.

For the Todo List application the Python Bottle Web Framework will be used. The Bottle library is a lightweight standalone micro web framework.

To store the Todo list items an SQLite database is used. SQLite is a file based server-less SQL (Structured Query Language) database which is ideal for small standalone applications.

Finally to build Web page, Python web templates will be used. The advantage of using web templates is that is reduces the amount of code written and it separates the presentation component from the backend logic.

 

Getting Started with Bottle

To install the Python bottle library:

pip install bottle

As a test we can make a program that has a home page (“/”) and a second page, then links can be put on each of the page to move back and forward.

todo_code_1

The @route is a decorator that links a URL call, like “/” the home page to a function. In this example a call to the home page “/” will call the home_page() function.

In the home_page() and pages2() functions the return call is used to pass HTML text to the web browser. The anchor tag (<a) is used to define page links.

The run() function will start the Bottle micro web server on: http://127.0.0.1:8080/

The output is below. 

SQLite

The Python SQLite library is one of the base libraries that is installed with Python.

SQLite has a number of tools and utilities that help manage your databases. One useful  light weight application is: DB Browser for SQLite. It is important to note that SQLite data can be view by multiple applications, but for edits/deletes only 1 application can be accessing SQLite.

For the Todo list we’ll start with a simple database structure using three fields:

  • Category – this a grouping such as: shopping, projects, activities etc.
  • theItem – this is the actual to do item
  • id – an unique index for each item. (This will used later for deleting rows)

The Todo database can be created with the SQLite Brower, or in Python code. The code below creates that database, adds a todo table and then inserts some records.


import sqlite3

print("Create a todo list database...")

conn = sqlite3.connect('todo.db') # Warning: This file is created in the current directory
conn.execute("CREATE TABLE todo (category char(50), theitem char(100),id INTEGER PRIMARY KEY )")
conn.execute("INSERT INTO todo (category, theitem) VALUES ('Shopping','eggs')")
conn.execute("INSERT INTO todo (category, theitem) VALUES ('Shopping','milk')")
conn.execute("INSERT INTO todo (category, theitem) VALUES ('Shopping','bread')")
conn.execute("INSERT INTO todo (category, theitem) VALUES ('Activities','snow tires')")
conn.execute("INSERT INTO todo (category, theitem) VALUES ('Activities','rack lawn')")
conn.commit()

print("Database todo.db created")

The DB Browser can be used to view the newly created database.

make_table

Viewing the Data in Python

An SQL SELECT command is used to get all the records in the todo database.  A fetchall() method is will return all the database rows in a Python tuple variable. Below is the code to write the raw returned data to a browser page.

# Send to raw SQL result to a Web Page
#
import sqlite3
from bottle import route, run

@route('/')
def todo_list():
    conn = sqlite3.connect('todo.db')
    c = conn.cursor()
    
    c.execute("SELECT * FROM todo")
    result = c.fetchall()
    c.close()
# note: the SQL results are an array of data (tuple)
# send results as a string
    return str(result)

run()

todo_raw

The output formatting can be improved by adding a sort to the SQL SELECT statement and then HTML code are be used to show category heading.

todo_code_2

web_rbr

For small applications putting HTML code in the Python code is fine, however for larger web projects it is recommended that the HTML be separated out from the database or backend code.

Web Templates

Web templates allow you to separate the database and back end logic from the web presentation. Bottle has a built-in template engine called Simple Template Engine. I found it did everything that I needed to do. It’s possible to use other Python template libraries, such as Jinja, Mako or Cheetah, if you feel you need some added functionality.

The earlier Python code is simplified by removing the HTML formatting logic. A template object is created with a template name of  sometime.tpl). An example would be:

output = template(make_table0, rows=result, headings = sqlheadings)

Where rows and headings are variable names that are used in the template file. The template file make_table0.tpl is in the working directory.

#
# Build a To Do List with a Web Template
#
import sqlite3
from bottle import route, run, template


@route('/')
def todo_list():
    # Send the output from a select query to a template
    conn = sqlite3.connect('todo.db')
    c = conn.cursor()    
    c.execute("SELECT * FROM todo order by category, theitem")
    result = c.fetchall()
    # define a template to be used, and pass the SQL results
    output = template('make_table0', rows=result)
    return output
        
run()

Templates are HTML pages with inline Python code. Python code can either be in blocks with a <% to start the block and a %> to end the block, or each line can start with %.

Two of the major differences of inline template Python code are:

  • indenting the line of Python is not required or used
  • control statements like : if and for need an end statement

A template that takes the SQL results and writes each row in a table would look like:

tpl_make_table

todo_tmp_table

A template that take the SQL results and writes a category heading and lists the items would look like:

tpl_make_list

web_tpl_rbr

Include ADD and DELETE to the Templates

The next step is to include ADD and DELETE functionality.

For the DELETE functionality, a form is added to the existing template. A ‘Delete’ button is placed beside all the Todo items and the button passes the unique index id (row[2]) of the item. The form has a POST request  with the action going to the page ‘/delete‘ on the Bottle Web Server.

For the ADD functionality, a new template is created and a %include call is put at the bottom of the main template. (You could also put everything in one file).

The main template now looks like:

tpl_show_todo

The new item template uses a dropdown HTML element with some predefined category options (Activities, Projects and Shopping). The item text will displace 25 characters but more can be entered. Pressing the save button will generate a POST request to the “/new” URL on the Bottle server.

The new_todo.tpl file is:

tpl_newtask

Bottle Python Code with /add and /delete Routes

The final step is to include routes for the /add and /delete URL references.

The new_item() function gets the category and Todo item from the form in the new_todo template. If the request passes a non-blank item (request.forms.get(“theitem”) then an SQL INSERT command will add a row. The unique item ID is automatically included because this field is defined as an index key.

The delete_item() function reads the unique item id that is passed from the button to issue an SQL DELETE statement.

At the end of the new_item() and delete_item() function the user is redirected back to the home (“/”) page.

#
# Build a Todo List 
#
import sqlite3
from bottle import route, run, template, request, redirect, post  

# The main page shows the Todo list, /new and /delete references are called from this page
@route('/')
def todo_list():
    conn = sqlite3.connect('todo.db')
    c = conn.cursor()
    
    c.execute("SELECT * FROM todo order by category,theitem ")
    result = c.fetchall()
    # in case column names are required
    colnames = [description[0] for description in c.description]
    numcol = len(colnames)
    # for now only the rows=result variables are used
    output = template('show_todo', rows=result, headings=colnames, numcol = numcol)
    return output

# Add new items into the database
@route('/new', method='POST')
def new_item():

    print("New Post:", request.body.read())
    theitem = request.forms.get("theitem")
    newcategory = request.forms.get("newcategory")

    if theitem != "":        
        
        conn = sqlite3.connect('todo.db')
        c = conn.cursor()
        c.execute("INSERT INTO todo (category,theitem) VALUES (?,?)", (newcategory,theitem))
        conn.commit()
        c.close()

    redirect("/") # go back to the main page   

# Delete an item in the database
@route('/delete', method='POST')
def delete_item():

    print("Delete:", request.body.read() )
    theid = request.forms.get("delitem").strip()
    print("theid: ", theid)
               
    conn = sqlite3.connect('todo.db')
    c = conn.cursor()
    sqlstr = "DELETE FROM todo WHERE id=" + str(theid)
    print(sqlstr)
    c.execute(sqlstr)
    conn.commit()
    c.close()

    redirect("/") # go back to the main page   
        
run()

The application will look something like:

todo_final

Final Clean-up

Some of the final clean-up could include:

  • enlarge the database to include fields like: status, due date, who is responsible etc.
  • add an “Are you sure?” prompt before doing adds and deletes
  • verify double entries aren’t included
  • include an edit feature
  • make the interface slicker

If you want to speed up the performance PyPy can be used instead of the Python interpreter. To use Pypy (after you’ve installed it), you will need to install the pip and bottle:

pypy3 -m ensurepip --user
pypy3 -mpip install bottle --user

Final Comments

As I was working on this I found a good BottleTutorial: Todo-List Application. This tutorial approaches the Todo list project differently but it is still a worthwhile reference.

Pi Charts with PySimpleGUI

There are a lot of good charting packages available for IoT and Rasperry Pi projects.

The PySimpleGUI python libary stands out in its ability to have the same code for both a local GUI and a Web interface. PySimpleGUI isn’t focused as a charting package but it has the canvas and graph elements that allow you to create real time bar charts and real time trend charts.

Getting Started with Graph Elements

For some background on PySimpleGUI see: PySimpleGUI – quick and easy interfaces

The graph element can have different co-ordinate orientations, for example the center can be (0,0) or the bottom left can be (0,0). A graph element is created with the syntax:

Graph(canvas_size, graph_bottom_left, graph_top_right …)

An example with 2 different co-ordinate orientations would be:


# A basic PySimpleGUI graph example
import PySimpleGUI as sg

bcols = ['blue','red','green']
myfont = "Ariel 18"

gtop = sg.Graph((200,200), (0,0),(200,200),background_color="white")
gcenter = sg.Graph((200,200), (-100,-100), (100,100),background_color="white")

layout = [[sg.Text('Graph: 0,0 at bottom left',font=myfont)],
[gtop],
[sg.Text('Graph: 0,0 at center',font=myfont)],
[gcenter],
[sg.Exit()]]

window = sg.Window('Graph Example', layout)

# Write text and lines
event, values = window.read(timeout=0)

gtop.draw_text(text="(0,0)", location=(0,0))
gcenter.draw_text(text="(0,0)", location=(0,0))

gtop.draw_text(text="(50,50)", location=(50,50))
gcenter.draw_text(text="(50,50)", location=(50,50))

gtop.draw_line((0,0),(50,50))
gcenter.draw_line((0,0),(50,50))

# Wait for a key to exit
window.read()

window.close()

sg_chart04

Bar Charts

Bar charts can be created using the graph.draw_rectangle() method. Below is an example that takes a command line input to toggle between a tkinter local interface and a web interface. This example has 3 input points that are scanned every 2 seconds.


import sys
import random

# Pass any command line argument for Web use
if len(sys.argv) &gt; 1: # if there is use the Web Interface
    import PySimpleGUIWeb as sg
    mode = "web"
    mysize = (20,2)
else: # default uses the tkinter GUI
    import PySimpleGUI as sg
    mode = "tkinter"
    mysize = (12,1)
BAR_WIDTH = 150
BAR_SPACING = 175
EDGE_OFFSET = 3
GRAPH_SIZE = (500,500)
DATA_SIZE = (500,500)

bcols = ['blue','red','green']
myfont = "Ariel 18"

#update with your ip
myip = '192.168.0.107'

graph = sg.Graph(GRAPH_SIZE, (0,0), DATA_SIZE)

layout = [[sg.Text('Pi Sensor Values',font=myfont)],
  [graph],
  [sg.Text('PI Tag 1',text_color=bcols[0],font=myfont,size= mysize ),
  sg.Text('PI Tag 2',text_color=bcols[1],font=myfont,size= mysize ),
  sg.Text('PI Tag 3',text_color=bcols[2],font=myfont,size= mysize)],
  [sg.Exit()]]

if mode == "web":
    window = sg.Window('Real Time Charts', layout,web_ip=myip, web_port = 8888, web_start_browser=False)
else:
    window = sg.Window('Real Time Charts', layout)
while True:
    event, values = window.read(timeout=2000)
    if event in (None, 'Exit'):
        break

    graph.erase()
    for i in range(3):
# Random value are used. Add interface to Pi sensors here:
        graph_value = random.randint(0, 400)
        graph.draw_rectangle(top_left=(i * BAR_SPACING + EDGE_OFFSET, graph_value),
        bottom_right=(i * BAR_SPACING + EDGE_OFFSET + BAR_WIDTH, 0), fill_color=bcols[i])
        <span id="mce_SELREST_start" style="overflow:hidden;line-height:0;"></span>graph.draw_text(text=str(graph_value), location=(i*BAR_SPACING+EDGE_OFFSET+15, graph_value+10),color=bcols[i],font=myfont)

window.close()

The presentation between the tkinter and Web interface is almost identical, but not 100% some tweeking on text sizing is required.

Real Time Trend Charts

It is important to note that the PySimpleGUIWeb is still in development so there may be some compatibility issues when trying to toggle between the tkinter and Web versions.

Below is an example that will create a realtime chart.

sg_realtimechart

#!/usr/bin/env python

import array
from datetime import datetime
import sys
import random

# Pass any command line argument for Web use
if len(sys.argv) > 1: # if there is use the Web Interface
    import PySimpleGUIWeb as sg
    mode = "web"
    mysize = (20,2)
else: # default uses the tkinter GUI
    import PySimpleGUI as sg
    mode = "tkinter"
    mysize = (12,1)

from threading import Thread

STEP_SIZE = 1  # can adjust for more data saved than shown
SAMPLES = 100 # number of point shown on the chart
SAMPLE_MAX = 100 # high limit of data points
CANVAS_SIZE = (1000, 600)
LABEL_SIZE = (1000,20)

# create an array of time and data value
pt_values = []
pt_times = []
for i in range(SAMPLES+1): 
    pt_values.append("")
    pt_times.append("")
    

def main():

    timebar = sg.Graph(LABEL_SIZE, (0, 0),(SAMPLES, 20), background_color='white', key='times')
    graph = sg.Graph(CANVAS_SIZE, (0, 0), (SAMPLES, SAMPLE_MAX),
          background_color='black', key='graph')

    layout = [
        [sg.Quit(button_color=('white', 'red')),sg.Button(button_text="Print Log", button_color=('white', 'green'),key="log"),
         sg.Text('     ',  key='output')],
        [graph],
        [timebar],       
    ]
    if mode == 'web':
        window = sg.Window('Pi Trend Chart', layout,
                       web_ip='192.168.0.107', web_port = 8888, web_start_browser=False)
    else:
        window = sg.Window('Pi Trend Chart', layout)

    graph = window['graph']
    output = window['output']

    i = 0
    prev_x, prev_y = 0, 0

    while True: # the Event Loop
  
        event, values = window.read(timeout=1000)
        if event in ('Quit', None):  # always give ths user a way out
            break
        if event in ('log'): # print the recorded time/data arrays
            print("\nReal Time Data\n")
            for j in range(SAMPLES+1):
                if pt_times[j] == "": #only print updated info
                    break
                print (pt_times[j], pt_values[j])

        # Get data point and time
        data_pt = random.randint(0, 100)
        now = datetime.now()
        now_time = now.strftime("%H:%M:%S")
        # update the point arrays
        pt_values[i] = data_pt       
        pt_times[i] = str(now_time) 

        window['output'].update(data_pt)
        
        if data_pt > SAMPLE_MAX:
            data_pt = SAMPLE_MAX
        new_x, new_y = i, data_pt

        if i >= SAMPLES:
            # shift graph over if full of data
            graph.move(-STEP_SIZE, 0)
            prev_x = prev_x - STEP_SIZE
            # shift the array data points
            for i in range(SAMPLES):
                pt_values[i] = pt_values[i+1]
                pt_times[i] = pt_times[i+1]
                    
        graph.draw_line((prev_x, prev_y), (new_x, new_y), color='red')
        
        prev_x, prev_y = new_x, new_y
        i += STEP_SIZE if i < SAMPLES else 0
    
        
        timebar.erase()
        # add a scrolling time value 
        time_x = i
        timebar.draw_text(text=str(now_time), location=((time_x - 2),7) )
        # add some extra times
        if i >= SAMPLES:
            timebar.draw_text(text=pt_times[int(SAMPLES * 0.25)], location=((int(time_x * 0.25) - 2),7) )
            timebar.draw_text(text=pt_times[int(SAMPLES * 0.5)], location=((int(time_x * 0.5) - 2),7) )
            timebar.draw_text(text=pt_times[int(SAMPLES * 0.75)], location=((int(time_x *0.75) - 2),7) )
        if i > 10:
            timebar.draw_text(text=pt_times[1], location=( 2,7) )
        
    window.close()


if __name__ == '__main__':
    main()

Final Comment

If you are looking at doing some charting and you want to have both a local and a web interface then PySimpleGUI and PySimpleGUIWeb will be something that you should take a look at.

PySimpleGUI – quick and easy interfaces

The Python PySimpleGUI project has two main goals, a simpler method for creating graphical user interfaces (GUIs), and common code for Tkinter, QT, xW and Web graphics.

I feel comfortable doing my own Python Tkinter and Web interfaces, but using common code for both local interfaces and Web apps could be extremely useful for Rasp Pi projects.

In the this blog I wanted to introduce PySimpleGUI by creating a local GUI/Web interface to control a Raspberry Pi Rover, all in less than 60 lines of code.

Getting Started with PySimpleGUI

The Python PySimpleGUI project has a number of “ports” or versions. The main version is for Tkinter based graphics and it is very fully featured. The versions for Qt,Wx and Web graphics are still in development so some testing may be required if you are hoping for full code compatibility between the different libraries.

There probably aren’t a lot of cases where you would want to flip between Qt, Wx and Tkinter graphic engines but it is remarkable that the possibility exists.

To install the default Tktinter  version of  Pysimplegui enter:

pip install pysimplegui

PySimplegui has a wide range of graphic widgets or elements. Graphic presentations are built by creating a layout variable. Graphic elements are placed in separate rows by open and closed square brackets.

gui_layout

A Button Interface Project

For my rover project I used a layout of 5 rows. The first row contains a feedback text, then rows 2-5 contains buttons.

The code below is a simple button app.


# Create a simple graphic interface
#
import PySimpleGUI as sg

layout = [ [sg.Text("the feedback" , key="feedback")],
           [sg.Button("FORWARD")],
           [sg.Button("LEFT"),sg.Button("RIGHT")],
           [sg.Button("STOP")],
           [sg.Button("QUIT")]
          ]
# Create the Window
window = sg.Window('My First App', layout)
# Event Loop to process "events"
while True:
    event, values = window.read()
    window['feedback'].Update(event) # show the button in the feedback text
    print(event,values)
    if event in (None, 'QUIT'):
        break
window.close()

The PySimplegui sg.window() call displays a window with the title and a layout definition (line 11). The window.read() will return events and values that have been changed (line 14). The feedback text element (line 5) is given a key name of  feedback, and this key name is used for updates to show the key press (line 15).

sg_basic

Standalone Web Apps with PySimpleGUIWeb

The PySimpleGUIWeb library is still under development, so be aware that not all the features in PySimpleGUI are fully supported in the Web version. PySimpleGUIWeb is an excellent way to create a lightweight standalone Web interface, but it is important to note that it isn’t designed to be a multi-page/multi-user Web environment.

To install PySimpleGUIWeb enter:

pip install remi
pip install pysimpleguiweb

The PySimpleGUIWeb window() call has a few more options, such as:

  • web_ip – the IP address to use for the PySimpleGUIWeb micro Web server
  • web_port – port on the micro Web server
  • web_start_browser – open a Web browser on app start

If you use our earlier button example but this time import PySimpleGUIWeb and add some web options we see an almost identical presentation however this time it’s in a Web interface.

sg_basic_web

Command line options can be used to toggle between the different libraries by:

import sys

# Pass any command line argument for Web use 
if len(sys.argv) > 1: # if there is use the Web Interface 
    import PySimpleGUIWeb as sg
    mode = "web"
else: # default uses the tkinter GUI
    import PySimpleGUI as sg
    mode = "tkinter"

Formatting of Display Elements

The next step is to adjust the graphic elements’  fonts, colors, and size properties.

Below is an example of changing the “FORWARD” button to have a size of 32 characters wide and 3 lines high with color and larger font.

[sg.Button("FORWARD", size=(32,3), 
  font="Ariel 32", 
  button_color=('white','green'))]

To make the interface more usable all the rover control buttons can be adjusted and the “QUIT” button can be left the default.

py_rovergui

Raspberry Pi Rover Interface

For my Raspberry Pi Rover project I used :

  • Arduino car chassis (~ $15),
  • a portable USB charger
  • Pimoroni Explorer Hat Pro (a Pi motor shield)

Below is the final code and it used a command line option (any character) to toggle into a Web application, otherwise it was the default PySimpleGUI interface. The application also included the Pi GPIO library to start/stop the car chassis motors.


# SGui_rover.py - use PySimpleGUI/Web to control a Pi Rover Pi
#

import sys
# Pass any command line argument for Web use
if len(sys.argv) > 1: # if there is use the Web Interface
    import PySimpleGUIWeb as sg
    mode = "web"
else: # default uses the tkinter GUI
    import PySimpleGUI as sg

import RPi.GPIO as gpio
gpio.setmode(gpio.BOARD)
# Define the motor pins to match your setup
motor1pin = 38 # left motor
motor2pin = 37 # right motor
gpio.setup(motor1pin, gpio.OUT)
gpio.setup(motor2pin, gpio.OUT)

# Send Action to Control Rover
def rover(action):
if action == "FORWARD":
    gpio.output(motor1pin, gpio.HIGH)
    gpio.output(motor2pin, gpio.HIGH)
if action == "LEFT":
    gpio.output(motor1pin, gpio.HIGH)
    gpio.output(motor2pin, gpio.LOW)
if action == "RIGHT":
    gpio.output(motor1pin, gpio.LOW)
    gpio.output(motor2pin, gpio.HIGH)
if action == "STOP":
    gpio.output(motor1pin, gpio.LOW)
    gpio.output(motor2pin, gpio.LOW)

# All the stuff inside your window.
myfont = "Ariel 32"
layout = [ [sg.Text(" ",size=(20,1) , key="feedback")],
[sg.Button("FORWARD", size=(32,3), font=myfont, button_color=('white','green'))],
[sg.Button("LEFT", size=(15,3), font=myfont),sg.Button("RIGHT", size=(15,3), font=myfont)],
[sg.Button("STOP", size=(32,3), font=myfont, button_color=('white','red'))],
[sg.Button("QUIT")]
]
# Create the Window
if mode == "web":
    window = sg.Window('PySimpleGUI Rover Control', layout,
        web_ip='192.168.0.106', web_port = 8888, web_start_browser=False)
else:
    window = sg.Window('PySimpleGUI Rover Control', layout )

# Event Loop to process "events" and pass them to the rover function
while True:
    event, values = window.read()
    print(event,values)
    if event in (None, 'QUIT'): # if user closes window or clicks cancel
        break
    window['feedback'].Update(event) # show the button in the feedback text
    rover(event)

window.close() # exit cleanly

py_sg_rover

Final Comment

I feel that PySimpleGUI and PySimpleGUIWeb have a lot of great potential for Raspberry Pi projects.

 

littleBit Dashboards (without Cloud Bits)

littleBits is a set of electronic components that magnetically connect together. litteBits is geared towards the kids STEP market and it is available in many schools and libraries.

The littleBits company has done an excellent job making their product easy to use. There is a large variety of different “bit” modules and for Internet applications there is a Cloud Bit ($59).

I found that the Cloud Bit was very easy to get up and running, but I found it was expensive at $59 and somewhat limiting, for example you are only access 1-input and 1-output. So if you want to do 2 inputs/output you would need to purchase a second Cloud bit module.

In this blog I’d like to document how I used a $39 Arduino Bit to do 3-inputs and 3-outputs. I also had the code talk directly to a free Web Dashboard (AdaFruit).

littleBits Arduino Program

A set of commands needs to be setup between the littlebits Arduino module and the PC/Pi. In my Arduino program I referenced the ports A,B,C as inputs (on the left side), and D,E,F as outputs (on the right side).

The commands from the PC/Pi would be : reference_pin:value, for example D:255 would set the D (top left pin) at 100%. It’s important to note that Arduino inputs and outputs are scaled from 0-255.

For inputs the littleBits would send the results as pin: reference_pin:value, for example B:255 would be the result at full scale for the A0 input.

ard_abc

My  test setup had:

  • A fork bit – this meant I only needed 1 power input source
  • A slider bit (0-1) on bit D0 (A)
  • A dimmer bit (0-255) on bit A0 (B)
  • A temperature bit on bit A1 (C)
  • An LED on bit d1 (D)
  • A number bit on D5 (E)
  • a bargraph bit on D9 (F)

lb_ard_setup

Below is the littleBits Arduino program that managed the serial communications.

// littleBits_2_Dashboards - create a serial interface to read/write to a PC/Pi
//
// Command from the littleBits: (A,B,C are the left pins) 
//  A:value <- for example B:24, pin A0 (2nd input) is 24 

// Commands from the PC/Pi: (D,E,F are the right pins)
//  D:output <- for example E:128, set pin A0 to 50% (128/255)
//
String thecmd; 
String thevalue;
String theinput;
char outstring[3];

void setup() {
  //define the littleBits right side pins 1,5 and 9 
  pinMode(1, OUTPUT);
  pinMode(5, OUTPUT);
  pinMode(9, OUTPUT);
  // define the littleBits left side inputs
  pinMode(0, INPUT);
  pinMode(A0, INPUT);
  pinMode(A1, INPUT);
  
  Serial.begin(9600); // this needs to match the PC/Pi baud rate
}
void loop() { 
  if (Serial.available() > 0) {
    thecmd = Serial.readStringUntil("\n"); 
    if (thecmd.length() > 2) { // ensure the msg size is big enough
      thevalue = thecmd.substring(2);
      if (thecmd.startsWith("D")) { analogWrite(1,thevalue.toInt()); }
      if (thecmd.startsWith("E")) { analogWrite(5,thevalue.toInt()); }
      if (thecmd.startsWith("F")) { analogWrite(9,thevalue.toInt()); }
    }     
  }
  // Try 3 different inputs: d0 = on/off , A0 = pot, A1 = temp sensor

  sprintf(outstring,"%d",digitalRead(0));
  Serial.write("A:");
  Serial.write(outstring);
  Serial.write("\n");

  sprintf(outstring,"%d",analogRead(A0));
  Serial.write("B:");
  Serial.write(outstring);
  Serial.write("\n");

// A1 is an "i12" littleBits temperature sensor
  int temp = analogRead(A1);
  temp = map(temp,0,1023,0,99); //rescale. Sensor range is 0-99 C or F
  sprintf(outstring,"%d",temp);
  Serial.write("C:");
  Serial.write(outstring);
  Serial.write("\n");
  

  delay(5000);
}

The Arduino IDE “Serial Monitor” can be used to view the output and set values.

msgbox

Python on the PC or Raspberry Pi

The Arduino program will send input data for A,B,C every 5 seconds. This input can be seen in Python by:

#
# littleBits Read Test
#
import serial

ser = serial.Serial(port='/dev/ttyACM1', baudrate=9600) # format for Linux
#ser = serial.Serial(port='COM1', baudrate=9600) # format for Windows

while True:
    inline = ser.readline()
    inline = inline.decode() # make a string
    pin = inline[0:1] # the first character is the pin
    thevalue = inline[2:-1] # the value is between ":" and "\n"<span id="mce_SELREST_start" style="overflow:hidden;line-height:0;"></span>
    print(pin,thevalue)

The output will look something like:

A  1
B  1023
C  21

To write commands from Python:

Write
#
# littleBits Write Test
#
import serial

ser = serial.Serial(port='/dev/ttyACM2', baudrate=9600) # format for Linux
#ser = serial.Serial(port='COM1', baudrate=9600) # format for Windows

while True:
    print("\nWrite an output value to littleBit")
    out = input("Enter pin:value, pin=A,B,C example: 'E:255' : ")
    out = out.upper() + "\n"
    out2 = out.encode('utf_8')
    ser.write(out2)

Adafruit Dashboards

There are lots of good free dashboards. For this project I used the Adafruit site. To get started you will need to log in and create a free account.

I’ve bought a number of components from Adafruit. I think that they are an excellent company that goes out of their way to create great user guides and products.

To get started with Adafruit Dashboards see: https://github.com/adafruit/Adafruit_IO_Python

The first step is to add some Adafruit tags that the code can read/write to.

Ada_feeds

In the Python code a couple of dictionaries (lb_inputs, lb_outputs)  were created to link the littleBit references (A-F) with the Adafruit tags. Also two dictionaries (lb_inlast, lb_outlast) are used to minimize communications traffic so that only new values were written.

#
# Import standard python modules
import time, random
import serial

# import Adafruit IO REST client
from Adafruit_IO import Client, Feed, RequestError

ADAFRUIT_IO_USERNAME = "put_your_username_here"
ADAFRUIT_IO_KEY = "c039f24ecb6...xxxxx"

aio = Client(ADAFRUIT_IO_USERNAME, ADAFRUIT_IO_KEY)

# Create dictionaries of inputs, output, and last values
lb_inputs = {"A":"lb-slide", "B":"lb-dimmer","C": "lb-temp"}
lb_inlast = {"A":0, "B":0,"C": 0}
lb_outputs = {"D":"lb-led", "E":"lb-number", "F":"lb-bar"}
lb_outlast = {"D":0, "E":0,"F": 0}

# Setup the serial port connection
ser = serial.Serial(port='/dev/ttyACM1', baudrate=9600)

while True:
    # Get values from littleBits and write to the dashboard
    inline = ser.readline()
    inline = inline.decode() #inline should look like: A:125\n
    pin = inline[0:1] # pin is the first character in string
    thevalue = inline[2:-1] # value is between ":" and "\n"
    if lb_inlast[pin] != thevalue: # Write only new values
        print(pin,thevalue, lb_inputs[pin])
        ada_item = aio.feeds(lb_inputs[pin])
        aio.send(ada_item.key,thevalue)
        lb_inlast[pin] = thevalue

    thetag = 'lb-slide'
    # Write new dash values to littleBits if they've changed
    for lbtag, dashtag in lb_outputs.items():
        print (lbtag,dashtag)
        thevalue = aio.receive(dashtag).value
        if lb_outlast[lbtag] != thevalue: # Write only new values
            outstr = lbtag + ":" + thevalue + "\n"
            print(outstr)
            ser.write(outstr.encode('utf_8'))
            lb_outlast[lbtag] = thevalue   

    time.sleep(2)

If everything is working correctly then new values should be written to in both directions. On the Adafruit Web site the Feed page should show the new values.

To make things look more presentable Adafruit Dashboards can be used.

ada_dash

Final Comments

In this project I used the Adafruit API, other good platforms would be IFTTT and Node-Red

Use MetPy to help answer your kid’s science questions

Being a good dad I try and answer my kids science questions, but sometimes it’s really tough.

There is an awesome Python library called MetPy that can help with some of those challenging science and weather questions.

In this blog I’d like to introduce the MetPy library and show how to use it to solve questions like:

  • Can they make snow when it’s above freezing ?
  • How much thinner is the air in places like Denver?
  • How can you figure out what the wind chill is?

Getting Started with MetPy

To install MetPy:

pip install metpy

One of nice things about MetPy is that it manages the scientific units, so variables  are defined with their units. Below is a Python example where the units module is used for a simple temperature conversion. The temperature today is defined as 40 degF. The to() method can be used to convert the temperature to degC.

>>>> from metpy.units import units
>>> tempToday = [40] * units.degF
>>> tempToday.to(units.degC)

Quantity([4.44444444], 'degree_Celsius')

You can also do some interesting mixing of units in math calculations, for example you can add 6 feet and 4 meters:

 >>> print( [6]*units.feet + [4] * units.m)
[19.123359580052494] foot

MetPy has a very large selection of thermodynamic and weather related functions. In the next sections I will show how to use some of the these functions.

How can they make snow when it’s above freezing ?

Ski resort can make snow by forcing water and pressurized air through a “snow gun”. Making snow can be an expensive operation but it allows ski resorts to extend their season.

snowgun

When you get a weather forecast the temperature is given as the ambient or dry bulb temperature. The wet bulb temperature takes the dry air temperature and relative humidity into account. The wet bulb temperature is always below the outside temperature. To start  making snow  a wet bulb temperature of -2.5°C or 27.5°F is required.

Metpy has a number of functions that can used to find humidity and wet bulb temperatures.  The wet_bulb_temperature function will find the wet bulb temperature using the pressure, dry temperature and dew point.

Below is an example where the temperature is below freezing, but it’s not possible to make snow because the wet bulb temperature is only -0.6°C  (not the required  -2.5°C).

>>> import metpy.calc
>>> 
>>> pressure = [101] * units.kPa
>>> temperature = [0.5] * units.degC
>>> dewpoint = [-2.5] * units.degC
>>> 
>>> metpy.calc.wet_bulb_temperature(pressure, temperature, dewpoint)

Quantity(-0.6491265444587265, 'degree_Celsius')

Knowing that -2.5°C (27.5°F) is the wet bulb temperature upper limit for snow making, the relative_humidity_wet_psychrometric function can be used to create a curve of humidity and dry temperature points where it is possible to make snow.

The code below iterates between -10 and 10 deg °C getting humidity values at a wet bulb temperature of -2.5°C.


#
# Find when you can make snow
#
import matplotlib.pyplot as plt
import metpy.calc
from metpy.units import units

print("Get temps vs. humidity")
print("-------------------")

plt_temp = []
plt_hum = []
for temp in range (-10,11): # Check dry temperatures between -10 - 10 C
    dry_temp = [temp] * units.degC
# Get the relative humidity
    rel_humid = metpy.calc.relative_humidity_wet_psychrometric(dry_temp, wet_temp,pres)
# Strip the humidity units for charting, and make a percent (0-100)
    the_humid = rel_humid.to_tuple()[0] * 100
    if (the_humid  0) : # Get valid points
        plt_temp.append(temp) # append a valid temp
        plt_hum.append(the_humid) # append a valid humidity
    print (temp, the_humid)

fig, ax = plt.subplots()
ax.plot(plt_temp, plt_hum )
ax.set(xlabel='Temperature (C)', ylabel='Humidity (%)',
title='When you can make Snow')
ax.grid()
#fig.savefig("makesnow.png")
plt.show()

makesnow

From the data we can see that it is possible to make snow when the temperature is above freezing and the humidity is low.

How much thinner is the air … in Denver?

We all know that the air is thinner when we’re up in an airplane, but how much thinner is it in Denver or  Mexico City compared to New York City ?

Using the height_to_pressure_std function it is possible to get a pressure value based on altitude. The to() method can be used to convert the pressure to standard atmospheres.

>>> import metpy.calc
>>> 
>>> New_York_alt = [33]*units.ft
>>> metpy.calc.height_to_pressure_std(New_York_alt).to(units.atm)
Quantity([0.99880745], 'standard_atmosphere')

>>> Denver_alt = [5280] * units.ft
>>> metpy.calc.height_to_pressure_std(Denver_alt).to(units.atm)
Quantity([0.82328412], 'standard_atmosphere')

>>> Mexico_city_alt = [7350]*units.ft
>>> metpy.calc.height_to_pressure_std(Mexico_city_alt).to(units.atm)
Quantity([0.76132418], 'standard_atmosphere')

Relative to New York City the air is about 18% thinner in Denver and 24% thinner in Mexico City.

Using the height_to_pressure_std function it is possible to create a chart of atmospheric pressure between sea level and the top of Mt. Everest (29,000 ft). At the top of Mt. Everest the air is 70% thinner than at sea level !!


#
# How much does the air thin as you climb ?
#
import matplotlib.pyplot as plt
import metpy.calc
from metpy.units import units
print("Get height vs. Atm Pressure")
print("---------------------------")
# create some plot variables
plt_ht = []
plt_press = []

# Check Atmospheric Pressure from sea level to Mt. Everest (29,000 ft) heights
for temp in range (0,30000,1000): # Check dry temperatures between -10 - 10 C
    height = [temp] * units.feet
    pressure = metpy.calc.height_to_pressure_std(height)
    atm = pressure.to(units.atm)
    print (height, atm)
    plt_ht.append (height.to_tuple()[0]) # put the value into plt list
    plt_press.append (atm.to_tuple()[0]) # put the value into plt list

fig, ax = plt.subplots()
ax.plot(plt_ht, plt_press )
ax.set(xlabel='Mountain Height (ft)', ylabel='Pressure (atm)',
title='How does the Pressure change Mountain Climbing?')
ax.grid()
fig.savefig("height_vs_press.png")
plt.show()

height_vs_press

How can I figure out the Wind Chill ?

The MetPy windchill function will return a “feels like” temperature based on a wind speed and ambient temperature. For example an outside ambient temperature of 40 deg °F with a wind of 20 mph feel like 28 deg °F.

>>> import metpy.calc
>>>
>>> temp = [40] * units.degF
>>> wind = [20] * units.mph
>>> metpy.calc.windchill(temp, wind, face_level_winds=True)
Quantity([28.42928573], 'degree_Fahrenheit')

When the temperature starts going below -20 °C parents should be keep a close eye on their kids for frost bite. Below is a code example that shows a curve of -20 °C based on ambient temperature and wind.


#
# Wind Chill
#
import matplotlib.pyplot as plt
import metpy.calc
from metpy.units import units

# Create some plotting variables
plt_temp = []
plt_speed = []

for temp in range (0,-46,-1): # Check dry temperatures between -10 - 10 C
    the_temp = [temp] * units.degC
    for wind in range(1,61,1):
        the_wind = [wind] * units.kph
        windchill =
        metpy.calc.windchill(the_temp,the_wind,face_level_winds=True)
# Select points with a wind chill around -20
        if (windchill.to_tuple()[0]) = -20.1) :
            plt_temp.append(temp)
            plt_speed.append(wind)

fig, ax = plt.subplots()
ax.fill_between(plt_temp, plt_speed, label="-20 C - Wind Chill", color="red" )

ax.set(xlabel='Temperature (C)', ylabel='Wind Speed (kph)',
title='When is the Wind Chill -20C ?')
ax.grid()
fig.savefig("windchill.png")
plt.show()

windchill

Summary

MetPy didn’t solve all my kids questions but the Metpy library is an excellent for science questions around water and weather.

If you have a budding chemist or chemical engineer in your house try taking a look at the Python Thermo library.

Text Graphics

While I was working on curses text based graphical interfaces I came across two interesting Linux text applications:

  • cowsay – generates ASCII pictures of a cow (or other animals) with a message
  • jp2a – a small utility that converts JPG images to ASCII.

I this blog I wanted to document some of the things that I found and how these utilities could be used in a ncurses program.

Cowsay

Cowsay has been around in the Linux world since 2007. It is now available in Windows and Android. To install cowsay in Linux or a Rasberry Pi :

sudo apt-get install cowsay

Coway takes text messages that you pass it.

~$ cowsay "I'm not worried about mad cow...because I'm a helicopter"
 _________________________________________
/ I'm not worried about mad cow...because \
\ I'm a helicopter                       /
 -----------------------------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

If you install the Linux fortune app (sudo apt install fortune) you can pass random fortune messages:

~$ fortune | cowsay
 _______________________________________
/ You get along very well with everyone \
\ except animals and people.            /
 ---------------------------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

There are a number of different images that can be used. To see the list of what is available:

~$ cowsay -l
Cow files in /usr/share/cowsay/cows:
apt bud-frogs bunny calvin cheese cock cower daemon default dragon
dragon-and-cow duck elephant elephant-in-snake eyes flaming-sheep
ghostbusters gnu hellokitty kiss koala kosh luke-koala mech-and-cow milk
moofasa moose pony pony-smaller ren sheep skeleton snowman stegosaurus
stimpy suse three-eyes turkey turtle tux unipony unipony-smaller vader
vader-koala www

To display all the images:

~$ for i in $(cowsay -l); do cowsay -f $i "$i"; done
 _____
< apt >
 -----
       \ (__)
         (oo)
   /------\/
  / |    ||
 *  /\---/\
    ~~   ~~
 ___________
< bud-frogs >
 -----------
     \
      \
          oO)-.                       .-(Oo
         /__  _\                     /_  __\
         \  \(  |     ()~()         |  )/  /
          \__|\ |    (-___-)        | /|__/
          '  '--'    ==`-'==        '--'  '
 _______
< bunny >
 -------
  \
   \   \
        \ /\
        ( )
      .( o ).

....and a bunch more

 Cowsay in Python

There is a native Python cowsay library:

~$ pip install cowsay --user

An example from the Python command line:

>>> import cowsay
>>> cowsay.cow("This is from Python")
  ___________________
< This is from Python >
  ===================
                        \
                         \
                           ^__^                             
                           (oo)\_______                   
                           (__)\       )\/\             
                               ||----w |           
                               ||     ||  

Cowsay in a Curses App

As an example I wanted to make a Raspberry Pi intrusion monitor. First I created a cowsay images with some eyes:

~$ cowsay -f eyes "Raspberry Pi - Intrusion Monitor"

eyes

Once I was happy with the presentation I saved the output to a file:

~$ cowsay -f eyes “Raspberry Pi – Intrusion Monitor” > eyes.txt

In my Python curses app I read the eyes.txt file and used the stdscr.addstr method to write it to the screen. (Note: For more info on writing Python/C curses or Lua curses)


# c_eyes.py - create a curses with a cowsay message
#
import curses , time, random

# create a curses object
stdscr = curses.initscr()
height, width = stdscr.getmaxyx() # get the window size

# define two color pairs
curses.start_color()
curses.init_pair(1, curses.COLOR_RED, curses.COLOR_WHITE)
curses.init_pair(2, curses.COLOR_YELLOW, curses.COLOR_BLACK)
curses.init_pair(3, curses.COLOR_BLUE, curses.COLOR_BLACK)

# Read the cowsay output file and write it to the screen

f = open("eyes.txt", "r")
eyes = f.read()
stdscr.addstr(0, 0, eyes,curses.color_pair(3))

# Add a footer
stdscr.addstr(height-1, 0, " " * (width-1),curses.color_pair(1))
stdscr.addstr(height-1, 0, " Key Commands : q - to quit " ,curses.color_pair(1))

# Add intrusion code here....
stdscr.addstr(15, 5, "PIR1 input: no movement" ,curses.color_pair(2) )
stdscr.addstr(16, 5, "PIR2 input: no movement" ,curses.color_pair(2) )

curses.curs_set(0) # don't show the cursor
stdscr.refresh()

# Cycle to update text. Enter a 'q' to quit
k = 0
stdscr.nodelay(1)
while (k != ord('q')):
    k = stdscr.getch()

curses.endwin()

c_eyes

jp2a –  converts JPG images to ASCII

jp2a is a Linux utility that is installed by:

apt-get install jp2a

I found that you’ve got to be selective of the jpeg image that you are trying to convert an example of a castle:

 jp2a castle.jpg --colors --background=light 

Another example was to convert a flag. For this example I found the width option to be useful:

 jp2a can.jpg --colors --width=70

 

 

Pi Rover using a Bottle Web Framework

There are a lot of Python web library choices, with each of the libraries offering different features and functions.

For simple Raspberry Pi Web application I was really impressed by the Python Bottle library. In this blog I’d like to document a Raspberry Pi rover project that I did using bottle.

Getting Started

To install the Python bottle library:

pip install bottle

There are some good tutorials for bottle. Probably the most important item is defining a decorator for a URL, and then linking the decorator to a function:

@route('/') # define a decorator for the start page
def my_homefuntion() # call a function for the start page
   return static_file("startpage.htm", root='') # call the start page

@route('/otherpage') # define a decorator for another page 
def my_otherpage_function() # call a function for the start page
   # do some stuff...

For the RaspPi rover I used a low cost car chassis (~$15).

It is not recommended to connect motors directly to Rasp Pi pin, for a few reasons:

  • larger motors require more power that a Pi GPIO pin can supply
  • power surges on motors can damage the Pi hardware
  • forward/backward motor directions require extra hardware
  • GPIO pins will only do ON/OFF, no variable speed settings.

There are a number of Raspberry Pi motor shield that can be used. For this project I used the Pimoroni Explorerhat Pro ($23). The Explorerhat has an excellent Python library that allows you to easily control the motor’s direction and speed.

Web Page

The goal for the web page (bottle1.htm) was to have 5 buttons for the controls and use AJAX to send the action request and then return the action code and display it on the page. The returned action would appear above the buttons ( in the lastcmd  paragraph tag).

Screen_bottle

For this example the button push was sent in the header as a GET request. So a forward button push would be a myaction: forward header item. In previous projects I’ve used POST request with parameters, however I found that header items can make things a little simpler.

<!DOCTYPE html>
<html>
<head> 
<title>Python Bottle Rover</title> 
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="data:,"> 
<style>
  html{font-family: Helvetica; display:inline-block; margin: 0px auto; text-align: center;}
  h1{color: #0F3376; padding: 2vh;}p{font-size: 1.5rem;}
  .button{display: inline-block; background-color: #4286f4; 
  border-radius: 4px; color: white; font-size: 30px; width:100%; height: 75px}
  .button2{background-color: green ;width:31%}
  .stop{background-color: red; width:33%}
</style>
</head>
<script>
function sendcmd(thecmd) {
  // send the action as a header item 
  var xhttp = new XMLHttpRequest();
  xhttp.open("GET","/action" , true);
  xhttp.setRequestHeader("myaction", thecmd);
  xhttp.send()
  xhttp.onreadystatechange = function() {
	// Get the response and put on it the screen
	if (this.readyState == 4 ) {	
		document.getElementById("lastcmd").innerHTML = "Last Command:<b>" +xhttp.responseText;
	}
  }
}
</script>
 
<body>
<h2>Python Bottle Rover</h2> 
<p id='lastcmd'></p>
<button onclick="sendcmd('forward')" class="button">FORWARD</button>
<button onclick="sendcmd('left')" class="button button2">LEFT</button>
<button onclick="sendcmd('stop')" class="button stop">STOP</button>
<button onclick="sendcmd('right')" class="button button2" >RIGHT</button>
<button onclick="sendcmd('backward')" class="button button">BACKWARD</button>
  
</body>
</html>

 

Bottle Rover App

For the rover app, there are two URL endpoints. The root (/) which would display the bottle1.htm page, and an action (/action) URL which would only be called from AJAX script when a button was pushed.

For this project the Raspberry Pi ip address was hardcoded into the code, for future projects dynamically getting the ip would be recommend. Also a web port of 8000 was used so as to not conflict with a dedicated web server (like Apache) that could be running on the Pi.

# Bottle2rover.py - web control for a RaspPi rover<span id="mce_SELREST_start" style="overflow:hidden;line-height:0;"></span>
#
from bottle import route, run, static_file, request

# Define RaspPi I/O pins
import explorerhat

# Send Action to Control Rover
def rover(action):
  if action == "forward":
    explorerhat.motor.one.speed(100)
    explorerhat.motor.two.speed(100)
  if action == "left":
    explorerhat.motor.one.speed(100)
    explorerhat.motor.two.speed(0)
  if action == "right":
    explorerhat.motor.one.speed(0)
    explorerhat.motor.two.speed(100)
  if action == "stop":
    explorerhat.motor.one.speed(0)
    explorerhat.motor.two.speed(0)
  if action == "backward":
    explorerhat.motor.one.speed(-50)
    explorerhat.motor.two.speed(-50)

# use decorators to link the function to a url
@route('/')
def server_static():
  return static_file("bottle1.htm", root='')
# Process an AJAX GET request, pass the action to the rocket launcher code
@route('/action')
def get_action():
  action = request.headers.get('myaction')
  print("Requested action: " + action)
  rover(action)
  return request.headers.get('myaction')

# Adjust to your required IP and port number
run(host = '192.168.0.106', port=8000, debug=False)

Final Comments

I was happily surprised how easy it was to get a Python bottle web app running. The bottle documentation was straightforward and I found that the code was quite lean.