OpenWRT on Pi – Light OS with Network Features

OpenWRT is an open source firmware that can be loaded on many commercially available routers. The firmware offers an upgrade path for older or obsolete hardware and customization/advanced features for newer equipment.

With OpenWRT’s small footprint (176MB vs. 1.7GB Raspbian image), it’s ideal for older Raspberry hardware where speed and memory usage is an issue.

For small home and office projects OpenWRT will offer some interesting networking solutions, such as making the Pi into an access point or using it as a wireless LAN bridge. However it’s important to note that the Raspberry Pi WiFi hardware will not offer the same performance as you’ll find on dedicated routers.

In this blog I documented my notes on getting an old Raspberry Pi Model B up and running with OpenWRT. I also tried to explain how to install many of the common Pi functions (loading software, GPIO, I2C…)

Pro’s and Con’s

Before jumping into a Pi OpenWRT project it’s useful to know where it stands versus the default Raspberry Pi OS.

Some of the OpenWRT advantages (over Raspbian/Raspberry PI OS) include:

  • It’s fast. It was designed to run on lower end CPU’s with limited memory
  • Network focused
  • Fast bootup
  • Smaller OS footprint so even older 4 GB SD card be used (2GB is possible but may not be able to load many software packages).

Some OpenWRT limitations:

  • No X-window desktop.
  • System setup isn’t streamlined like the default Raspberry PI OS (there’s no raspi-config)
  • Default installation is quite bare, so you need to manually load packages (even Bash and Nano)
  • Some packages could be a challenge to install (for example Node-Red)

Getting Started

Despite the fact that OpenWRT is designed to run on routers, in many ways its easier to play with on a Raspberry Pi. Some of the benefits of using a PI are:

  • you can’t ‘brick’ the Raspi. (I’ve come close many times playing with home routers)
  • SD/microSD card have considerably more space than most home routers. (This was in issue for me with older routers).
  • Pi has an HDMI connection so you can work directly on the PI (good for getting started)

For the OpenWRT Raspberry Pi Images see: https://openwrt.org/toh/raspberry_pi_foundation/raspberry_pi

You can use the standard Raspi imager to install OpenWRT on an SD card. For Linux systems the imager can be installed by:

sudo apt update && sudo apt install rpi-imager

After an SD card has been loaded up there are several way to get an OpenWRT system up and running. Below are the steps that I used.

1. Connect a monitor and keyboard to the Pi

Let it boot up and then go to the command prompt, and setup an static LAN address:

# Enter your own static IP. Note: change the gateway/DNS for your setup
uci set network.lan.proto="static"
uci set network.lan.ipaddr="192.168.0.140"
uci set network.lan.netmask="255.255.255.0"
uci set network.lan.gateway="192.168.0.1"
uci set network.lan.dns="192.168.0.1"
uci commit network

If you able to see IP addresses on your LAN you can simple do:

# Set Pi for DHCP and then check its address
uci set network.lan.proto="dhcp"
uci commit network

2. Hardwire Pi to Your LAN

Connect the 100BaseT port of the Pi to your LAN and then reboot the module.

From a laptop you now can use OpenWRT’s Web Interface or you can use the secure shell (ssh) command line interface.

I like the ssh option to get started because I can paste in groups of commands. To get started with ssh (with my static IP) :

$ ssh root@192.168.0.140


BusyBox v1.35.0 (2023-01-03 00:24:21 UTC) built-in shell (ash)

  _______                     ________        __
 |       |.-----.-----.-----.|  |  |  |.----.|  |_
 |   -   ||  _  |  -__|     ||  |  |  ||   _||   _|
 |_______||   __|_____|__|__||________||__|  |____|
          |__| W I R E L E S S   F R E E D O M
 -----------------------------------------------------
 OpenWrt 22.03.3, r20028-43d71ad93e
 -----------------------------------------------------
=== WARNING! =====================================
There is no root password defined on this device!
Use the "passwd" command to set up a new password
in order to prevent unauthorized SSH logins.
--------------------------------------------------
root@OpenWrt:~# 

3. Install Your Required Software

The default OpenWRT installation is extremely bare, so will probably need a number of basic packages.

OpenWRT uses opkg to install software. (Note: apt and apt-get are not supported in OpenWRT).

As a bare minimum I like to load:

# Install some software, but update list first
opkg update
# Install Bash, nano editor
opkg install bash nano 
# For Python projects install the base Python3 and pip
opkg install python3 python3-pip
# For Pi GPIO projects 
opkg install  gpioctl-sysfs gpiod-tools 

If you are planned to do some projects on your Raspberry Pi adding Bash, Python, and GPIO hardware support will be useful.

LuCI – OpenWRT Web Interface

After the system was up and running, the LuCI web interface can be used to setup other features such as networking interfaces and custom software. Many of the software packages also include additions to a LuCI web add-on.

From the LuCI interface you are easily see how much memory and disk space is available. It is important to check this screen if you’re planning on loading a large number of software packages.

Setup a USB WiFi Adapter

For Raspberry Pi models 3 and 4 OpenWRT will identify their built-in WiFi hardware, however for earlier models or projects where you want to use a USB WiFi adapter you’ll need to do this step.

On OpenWRT the configuring of USB devices has to be added manually. The first step is to install the USB utilities package:

# Update the package list
opkg update
# Add USB Utils (has lsusb)
opkg install usbutils

Once usbutils in added, the lsusb command will show information about the connected USB devices (Note: drivers still need to be loaded). For my Raspberry Pi B setup with an old Realtek WiFi adapter my output was:

root@OpenWrt_Pi1:~# lsusb
Bus 001 Device 004: ID 0bda:8189 Manufacturer_Realtek RTL8187B_WLAN_Adapter
Bus 001 Device 003: ID 0424:ec00  
Bus 001 Device 002: ID 0424:9512  
Bus 001 Device 001: ID 1d6b:0002 Linux 5.10.176 dwc_otg_hcd DWC OTG Controller

From this listing I can see that I need to search for a RTL8187B_WLAN_Adapter. The Luci Web Interface can be used to find and install the required network driver.

Once the USB WiFi adaptor is enabled, there are several different wireless arrangements that can be used, some of the more common setups would be:

  • Wireless application node, (like a typically Raspberry Pi setup)
  • Access point node, to setup your own custom network
  • Wireless bridge, to extend an existing wireless network

For a simple wireless application node, go into the LuCI web interface Network->Wireless menu option, and then select your radio and scan for a wireless network. After a SSID (a wireless connection) is configured, select Edit to add the network information.

Ensure that you select both lan and wwan, by doing this you will be able to remotely access the Pi node via both wireless and wired connections.

Using SFTP and FileZilla

Remotely working a OpenWRT can be a little challenging if you are only using the SSH and Luci web interface. A Secure File Transfer Protocol (SFTP) server package can be used to greatly improve editing and saving of configuration and coding files.

The OpenSSH-FTP server package is installed by:

# For remote work add an SFTP server
opkg update
opkg install openssh-sftp-server

After this software has been loaded a graphical SFTP client package like FileZilla can be used on a laptop.

FileZilla allows for an easy visual interface for remote coding and maintenance. Below is an example of FileZilla connecting into an OpenWRT Raspberry Pi node. A file association on FileZilla has been setup to link py files to the basic Python Idle editor.

A Raspi 3 Example

I wanted to do a more complete Pi functions test. For this project I used Pi 3 because I wanted to test controlling the USB power. (Note: USB power control is only available on Pi 3 and 4).

My goal was to play with GPIO, I2C and USB power. In the final project I used OpenWRT’s built-in uhttpd web server to show the results.

Below are some of the individual steps that I did to get things running.

GPIO Setup

The base GPIO packages need to loaded:

 # update list of available packages
opkg update
opkg install  gpioctl-sysfs gpiod-tools 

After the basic GPIO software is installed Raspberry Pi pin can be read to and written to in Bash (or ash which is the default OpenWRT shell).

Before a GPIO pin can be used it needs to defined. The tee command can be used to create a gpio definition with all the required files, for example:

# Create a gpio4 definition
echo 4 | tee /sys/class/gpio/export

# Show the file structure
ls /sys/class/gpio/gpio4
active_low  direction   power       uevent
device      edge        subsystem   value

A example to create a GPIO definition, then to write to the pin and read the value back is:

# Define the pin as an output
echo "out" > /sys/class/gpio/gpio4/direction
# Set the pin on (1)
echo 1 > /sys/class/gpio/gpio4/value
 # Read the pin value
cat /sys/class/gpio/gpio4/value
1

If you are doing some playing around and you want to remove a gpio definition then write to the unexport file:

# Remove GPIO4
echo 4 | tee /sys/class/gpio/unexport

I2C Setup

To get the I2C setup working the Raspberry Pi I2C chipset driver and the I2C tools need to be loaded:

# Update the opk list. This needs to be done once after a power up
opkg update
opkg install kmod-i2c-bcm2835 i2c-tools

The next step is to edit the file: /boot/config.txt

nano /boot/config.txt

At the bottom on the file add three dtparam lines:

################################################################################
# Bootloader configuration - config.txt
################################################################################

################################################################################
# For overclocking and various other settings, see:
# https://www.raspberrypi.org/documentation/configuration/config-txt/README.md
################################################################################

# OpenWrt config
include distroconfig.txt

[all]
# Place your custom settings here.
dtparam=i2c1=on
dtparam=spi=on
dtparam=i2s=on

Once the file is edited the system needs to be rebooted. After this I2C devices can be connected up. To check that the I2C addresses for wired devices use the i2cdetect command:

i2cdetect -y 1
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:                         -- -- -- -- -- -- -- -- 
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
20: -- -- -- -- -- -- -- -- 28 -- -- -- -- -- -- -- 
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
40: -- -- -- -- -- -- -- -- 48 -- -- -- -- -- -- -- 
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
70: -- -- -- -- -- -- -- 77     

(Note: for this i2cdetect example, addresses 28 and 48 were on from the Pimoroni Explorer HAT, and address 77 was a BME280 sensor).

Typically the next step is to install Python with pip and then any required I2C device library. For this project I was using a BME280 temperature/humidity sensor.

BME280 Sensor Setup

Each sensor will have its own particular library, however you’ll probably need the python3-smbus support.

For my sensor I needed to:

opkg update
opkg install python3 python3-pip
opkg install python3-smbus
# Install Python BME280 temperature/humidity sensor library
pip install bme280

The Python bme280 library also has a Bash interface, so to get the data from I2C address 77:

# Get the BME280 sensor data from address 0x77 
read_bme280  --i2c-address 0x77

1005.19 hPa
  38.38 %
  19.25 C

Control USB Power

Controlling the USB power is not something that is done very often. Some use cases might be: USB fans and lights or resetting an Arduino controller that a Pi is powering/connected to.

For this feature you’ll need to run:

opkg install usbutils uhubctl

The lsusb command will show which smart USB devices are connected, it will not show simple hardware like USB fans and lights.

lsusb --tree
/:  Bus 01.Port 1: Dev 1, Class=root_hub, Driver=dwc_otg/1p, 480M
    |__ Port 1: Dev 2, If 0, Class=, Driver=hub/5p, 480M
        |__ Port 1: Dev 3, If 0, Class=, Driver=smsc95xx, 480M

For the Raspberry Pi 3 and 4 the power on all USB ports is ganged together through port 2, so unfortunately it is not possible to power up/down an individual USB port. As mentioned earlier, the chip set on the Pi 1 and 2 do not support USB power control.

The Pi 4 has two internal USB hubs. Hub 1-1 connects to all the USB port on the USB 2.10 standard. Hub 2 controls to all the ports on the USB 3.00 standard and the Ethernet jack.

The commands to turn on/off/toggle the USB ports and keep the Ethernet jack powered are:

uhubctl -l 1-1 -p 2 -a on
uhubctl -l 1-1 -p 2 -a off
uhubctl -l 1-1 -p 2 -a toggle

OpenWRT uhttpd Web Server

OpenWRT has the lightweight uhttp web server pre-installed. This server is used for the LuCI Web Interface but it can also be used for custom applications.

For custom web projects add your files to: /www/cgi-bin

This directory allows any CGI program (Bash, Python, Lua, etc.) to run. Remember to set execute permissions on your CGI script files:

chmod +x mycgifile

A Pi3 CGI Web Example

The hardware setup on this project used a Pimoroni Explorer HAT Pro. This Pi Top had four built-in coloured LEDs (on pins 4, 17,27 and 5). Before toggling a GPIO pin, the device needs to be defined and a direction needs to be set:

#!/bin/bash
#
# setgpio - setup GPIO 
#
for pin in 4 17 27 5
do
  gpiodev=/sys/class/gpio/
  gpiodev+=$pin

  echo $pin | tee /sys/class/gpio/export  
  echo "out" > /sys/class/gpio/gpio$pin/direction
done
ls /sys/class/gpio

For my Web Page, some buttons were used to pass a query string variable that is read at the top of the script before outputting the HTML. The BME280 sensor data is shown as a simple Bash variable.

#!/bin/bash
#
# toggle - Bash CGI to show some standard functions
#           -  toggle GPIO LEDs and USB power
#           - show some I2C sensor results
#      Note: GPIO pins need to be setup as outputs
set -e
# Toggle a pin if it's passed in the queue string
if  [[ "$QUERY_STRING" =~ "gpio" ]]; then
  pin=$QUERY_STRING
 
  # Read the GPIO pin value and then toggle it
  if [ "$(cat /sys/class/gpio/$pin/value)" == 1 ]; then
    echo 0 > /sys/class/gpio/$pin/value
  else
    echo 1 > /sys/class/gpio/$pin/value
  fi
fi

# Toggle USB power, ensure to add '&' for CGI use
if [[ "$QUERY_STRING" =~ "usb" ]]; then
  uhubctl -l 1-1 -p 2 -a toggle &> /dev/null
fi

# Get BME280 sensor data
data=$(read_bme280 --i2c-address 0x77)


echo "Content-type: text/html"
echo ""

echo "
<html>
<head>
<meta http-equiv='Content-Type' content='text/html; charset=UTF-8'>
<title>OpenWRT PI Toogle</title>
</head>
<body>
<h1>OpenWRT PI CGI </h1>
<h3>Toggle Hardware</h3>
<hr>
<button style='background-color:skyblue;'
   onclick='location.href=\"?gpio4\"'>Toggle GPIO4  LED1
</button><br>
<button style='background-color:yellow;'
   onclick='location.href=\"?gpio17\"'>Toggle GPIO17 LED2
</button><br>
<button style='background-color:red;'
   onclick='location.href=\"?gpio27\"'>Toggle GPIO27 LED3
</button><br>
<button style='background-color:green;'
   onclick='location.href=\"?gpio5\"'>Toggle GPIO5 LED4
</button><br><br>
<button style='background-color:white;'
   onclick='location.href=\"?usb\"'>Toggle USB Power
</button><br>
<hr><h3>BME280 Sensor Data</h3>
<b>$data</b>
</body>
</html>"

exit 0

The web pages is called by: http://pi_OpenWRT_ip/cgi-bin/toggle

Below is a picture of the web page and the Pi setup.

Summary

OpenWRT can breath a lot of life into old Raspberry Pi hardware. I’ve noticed a nice performance improvement on the old Pi hardware that makes the system quite usable.

Long term Raspberry Pi users may find OpenWRT’s lack of a pre-installed applications, drivers and no desktop a little frustrating at first.

It’s important to note that certain packages like Node-Red will take some work to get installed.

This blog only really looked at Raspberry Pi functionality under OpenWRT. I never documented some of the OS’s real strengths on the networking side.

Add Internet Data to Your Home Automation Site

Home automation packages offer a huge selection of data import and integration tools, but they can never cover everything.

Many of us have some personal or hobby topics that we check periodically. These personal data points could be the wave height at a local surf spot, the bug levels at a favorite camping area, or when is the best time of go fishing.

In this blog I will look at how to scrape some hobby data points from web pages with just a single line of Bash code. I will then show how these results can be integrated into two home Internet of Things packages, Home Assistant and Node-Red.

Getting Started

Each of the different automation solutions offer web scraping tools. For example Python has the Beautiful Soup library, Home Assistant has the Scrape sensor, and Node-Red has the Scrape-it flow. These web scraping tools are all very usable, but unfortunately they require a detailed knowledge of the HTML/Document-Object-Model for the requested page.

A simple alternate approach is to use the lynx text based browser. This command line tool can be used to strip out all the HTML tagging and dump out just the text on a page. The output from lynx can be piped to common commands like grep, sed, and awk to filter out the required values. The advantage in the lynx approach is that you don’t need to understand the internal HTML on a page, and it only takes 1 line of Bash to get what you need.

To install Lynx on Raspian/Debian/Ubuntu:

sudo apt install lynx

Two Off-line Examples

The first step in developing custom web scraped data points is to find the required Bash commands. Working directly on either Home Assistant or Node-Red can be a little challenging. Luckily, you can do all your basic testing on a laptop and then once you’ve got things working you can move the code over to your IoT system.

Two personal interest topics for me, are the pollen levels in a local hiking region, and the amount of snow at a ski resort that I’m planning to take the family to.

The Lynx -dump option will output a stream of text with HTML tags, HTML encoding and Javascript removed. The command syntax that I’m looking for is:

lynx -dump http://somepage.com  | "filter the output until I get the result"

The picture below shows how I used lynx to find the pollen level from a weather page. For this example I first looked at the top 10 lines in the output, and I compared this output to the actual web page. A quick check showed that the pollen level value was on the sixth line. The sed utility can be used to delete all but the sixth line by setting the option to : ‘6!d’.

The full Bash script to get the pollen level from my favourite hiking area is:

# define the weather page URL
theurl="https://www.theweathernetwork.com/en/city/ca/ontario/lions-head/pollen"
# get the pollen value (on the 6th line)
lynx -dump $theurl | sed '6!d'

The second example, (picture below), uses lynx with a grep call to find the text “Top Lift” on a web page. In this snapshot, the output is returned as four words: Top Lift: 2.3 m. The snow depth (2.3) is the third word in the string. There are a few ways to extract words in a string, and for this example I used the awk utility.

The full Bash script to get the snow base for my upcoming ski trip is:

# define the ski resort URL
theurl="https://www.snow-forecast.com/resorts/Whistler-Blackcomb/6day/mid"
# find the line with "Top Lift", and 
# then parse out the 3rd (snow) value
lynx -dump $theurl | grep 'Top Lift' | awk '{ print $3 }'

Now that I’ve got the Bash commands for my personal data points I can move to the next step of adding them to my Home Assistant or Node-Red systems.

Home Assistant Command Line Sensors

The Home Assistant (HA) Command Line sensors offer an interface that allows the output from Bash commands to be used as HA viewable entities.

Before our earlier commands can be run, the Lynx utility needs to be installed. There are several ways to install applications into HA. The important point is that Lynx needs to be available in the same working space that the Command Line sensors run in. A simple way to ensure this, is to use a Command Line sensor to install Lynx directly.

To install sensors, modify the /config/configuration.yaml file. This file can accessed through the File Editor or Terminal HA add-ons, or via a secure shell (SSH) connection.

The picture below shows a temporary sensor called Install_Lynx that has been added to /config/configuration.yaml. This sensor will run the apk add command to install software. After this file is updated and saved, a Home Assistant restart will be required.

Once the required software is installed, it would be recommended that you remove this temporary “install” sensor, otherwise the system will try to re-install Lynx every 60 seconds.

Another approach would be to only install the software if isn’t there. (Note: the sensor is still run every minute by default). The command for this would be:

if ! ( apk list lynx ); then apk add lynx; fi

After Lynx is installed some new command line sensors can be added that access the personal web page data. The image below shows a sample /config/configuration.yaml file with the Install_Lynx sensor removed and two new sensors added that use our Bash commands.

These web pages don’t update too frequently so the scan_interval is set to 1 hour (3600 seconds). Also it good to ensure that the Lynx command is given enough time to run, so I set the command_timeout to 15 seconds. Like in the previous step HA needs to be restarted after new sensors are added.

The final step is to put the new sensor tags into a viewable presentation. The Home Dashboard can be selected from HA’s Overview option. The pic below shows the addition of a card that contains the newly created web scraped command line sensors.

Using Lynx with Node-Red

Like Home Assistant, the Lynx utility needs to be loaded on the Node-Red system. For Raspian/Debian/Ubuntu system this is done by: sudo apt install lynx.

To test that Lynx is working within Node-Red an inject, exec and a debug node can be used.

For my test I used the Bash statement to find pollen levels as the command in the exec flow. It’s important to note that the top output connector of exec flow is the output result (stdout).

The next step is to create some logic that schedules the scraping of web pages and then shows the data on a web dashboard. The image below shows a Node-Red example that uses a Big Timer to trigger the scraping of web pages. For the presentation of data a Text dashboard flow is used to show the pollen levels, and a Gauge dashboard flow is used for the snow levels.

The realtime data is available at: http://my_node_red_ip:1880/ui .

Summary

To create a successful home automation solution it’s important to access and view all the relevant signals.

From my personal experience I’ve found that once I setup my key measuring and controlling devices I would only infrequently look at the system. However, since I started adding hobby topics to the system I’ve noticed that I’m using the system a lot more often.

Gtkdialog – Custom Bash Dialogs

While I was working on an older Puppy Linux system I came across qtkdialog.

I’m a huge fan of Zenity and Yad for making Bash dialog, but qtkdialog has the advantage in that it allows users to create custom interfaces using an XML-like syntax.

Unfortunately qtkdialog is fairly old, so there isn’t a lot of documentation.

This blog references some of my notes. Some of the key features in qtkdialog include:

  • free form layouts
  • Custom text fonts, colors and sizing
  • Support for frames and tabs

Installation

To install use the following steps:

sudo apt install build-essential
wget https://storage.googleapis.com/google-code-archive-downloads/v2/code.google.com/gtkdialog/gtkdialog-0.8.3.tar.gz

tar -zxvf gtkdialog-0.8.3.tar.gz
cd gtkdialog-0.8.3/

./configure
make
sudo make install

If you plan on using a lot of icons, I’d recommend getting an icon browser/viewer. (For this I use yad-icon-browser that is bundled with yad).

A First Example

A first example a dialog with static text and one active button would be:

#!/bin/bash 
#
# gtk1.sh - gthdialog example with text and two buttons
#
export MAIN_DIALOG=' 

<window title="My First Program" icon-name="gtk-about"> 
  <vbox>
    <text>	 
      <label>Static Text and one active button</label> 
    </text>
    <button>
      <label>Echo the date</label>
      <action>echo "$(date)"</action> 
     </button> 
     <button ok></button>
  </vbox> 
</window> 
' 
# Open the dialog 
gtkdialog --program=MAIN_DIALOG --center

For this example (gtk1.sh), a variable, MAIN_DIALOG, is created with all the XML code. This variable is then passed to gtkdialog using the –program option. An alternate approach would be to pass an XML file to the program:

gtkdialog --file=gtk1.xml

Some of the key points in this example are:

  • start with <window> and end with </window>
  • format the presentation with vertical (vbox) or horizontal (hox) tags (at least 1 is required)
  • static labels must be within a <text> </text> block
  • buttons can call Bash code using <action> tags

A Diagnostic Example

A more advanced example will use a couple of frames to group together buttons with icons. These buttons will launch a Zenity windows with diagnostic data.

#!/bin/bash 
#
# gtk5.sh - create a diagnostic interface
#

GTKDIALOG=gtkdialog 

# Pass a custom color file if required
#export GTK2_RC_FILES=gtkrc_mono

export MAIN_DIALOG=' 

<window 
   title="Diagnostics" 
   icon-name="applications-utilities"
   name="MyWindowBg" >
  <vbox>
    <text use-markup="true"><label>"
      <span color='"'red'"' font-family='"'Comic'"' weight='"'bold'"' size='"'xx-large'"'>
        <i>System Tools</i>
      </span>"</label>
    </text>

    <hbox>
      <frame CPU Info>	
	<hbox>
	  <button>	    
            <input file icon="view-sort-descending"></input>
            <label>TOP - Table Of Processes</label>
            <width>30</width>
	    <action>top -b -n 1 | zenity --text-info --title "TOP"</action>
	  </button>  
	</hbox>
	<hbox>
	  <button use-underline="true" tooltip-text="Run VMSTAT Utility">
	    <input file icon="gnome-dev-memory"></input>
            <label>VMSTAT - CPU Stats </label>
            <width>55</width>
	    <action>yad --text="$(sensors)" --title=Sensors</action>
	  </button>  
	</hbox>
      </frame>

      <frame Device Info>	
	<hbox>
	  <button>	    
            <input file icon="media-removable-symbolic"></input>
            <label>LSUSB - List USB Devices</label>
            <width>40</width>
	    <action>lsusb | zenity --text-info --title "lsusb"</action>
	  </button>  
	</hbox>
	<hbox>
	  <button>
	    <input file icon="harddrive"></input>
            <label>LSPCI - List PCI Devices </label>
            <width>45</width>
	    <action>lspci | zenity --text-info --title "lspci"</action>
	  </button>  
	</hbox>
      </frame>

    </hbox>
    <button ok></button>
  </vbox>
</window> 
' 

case $1 in 
	-d | --dump) echo "$MAIN_DIALOG" ;; 
	*) $GTKDIALOG --program=MAIN_DIALOG --center ;; 

esac

Some of the key points in the code are:

  • Font color, type and size can be adjusted with the <span> tag. Note: see the Pango Markup Language for details.
  • To put icon on button use: <input file icon=xxxx></input>
  • Output from Bash commands can be piped to the Zenity dialog tool.

Custom Colors

A color file can be used to define many of the gtkdialog objects. To reference a custom color file:

  • Add a name item to the window object (< window name=”MyWindowBg”)
  • Create a GTK2_RC_FILES variable and reference the color file ( export GTK2_RC_FILES=gtkrc_mono )

The color file (gtkrc_mono in this example) references the name of the window (MyWindowBg) and any required custom color settings.

style "bgWhite" { bg[NORMAL] = "#FFFFFF" }
style "fgWhite" { fg[NORMAL] = "#FFFFFF" }
style "bgBlack" { bg[NORMAL] = "#000000" }
style "bgRed" { bg[NORMAL] = "#FF0000" }
style "fgRed" { fg[NORMAL] = "#FF0000" }
style "bgGreen" { bg[NORMAL] = "#FFFF00" }
style "fgGreen" { fg[NORMAL] = "#FFFF00" }
style "bgBlue" { bg[NORMAL] = "#0000FF" }
style "fgBlue" { fg[NORMAL] = "#0000FF" }

widget "MyWindowBg" style "bgBlack"
widget "MyWindowBg.GtkVBox.GtkHBox.MyButtonBg" style "bgGreen"

widget "MyWindowBg.GtkVBox.myEVB" style "bgBlue"

widget_class "*<GtkFrame>.GtkLabel" style "fgWhite"
widget_class "*<GtkButton>.*.GtkLabel" style "fgBlue"

For the earlier example if the line : # export GTK2_RC_FILES=gtkrc_mono is changed to:

# Pass a custom color file if required
export GTK2_RC_FILES=gtkrc_mono

The dialog will be changed to a black background with white frames and blue button text:

Summary

Understanding gtkdialog is a useful exercise, especially if you’re working on older system.

Manually creating gtkdialog layouts can quickly become painful. Tools like Glade offer a visual interface for creating complex graphic interfaces.

There are some Python libraries that support gtk graphic interfaces.

I’ll probably stick using basic Zenity or Yad for my Bash GUIs, or Python for more complex projects.

Micro:bit Extensions: Add extra sensors and devices

I have been using the BBC Micro:bit modules (~$20) with our kids to teach them basic hardware and software. The platform offers a block programming interface (very much like Scratch) and Python programming. The interesting thing is that the interface will toggle seamlessly between the two programming languages.

To add functionality to your projects Micro:bit supports extensions. This feature allows Arduino and Raspberry Pi sensors and devices to be connected to the basic module.

In this blog I wanted to:

  • Show a extensions example with deviices that are not directly available in the basic list
  • Comment on some limitations
  • Document how to add Micro:bit parts in Fritzing wire drawing tool

An Extension Example

Use the Extension menu item to add a new set of functionality to your Micro:bit’s project.

For this example I wanted to use some devices/sensors that I was using on my Arduino projects. These devices included:

It is important to note that the extension may not be readily available from the Microbit web page. For my project I did an Internet search to find the required github links. Once you have the URL it can be pasted into the Extension’s page:

Below is a picture of the Micro:bit with the three added devices

The Micro:bit logic used an on_start block to setup the pins for the TM1637 4-digit display, and initialize the OLED display.

The forever block:

  • queried the DHT11 sensor (on Pin 0)
  • showed the humidity on the Micro:bit display
  • showed the temperature on the TM1637 display
  • showed both the temperature and humidity on the 0.91″ OLED
  • cycled every 5 seconds

The block code can be viewed (or edited)in Python:

"""

Devices:

DHT11 Temperature/Humidity Sensor 

TM1637 4-Digit Display 

I2C 9.91" OLED Display

Show Temperature on TM1637

Show Humidity on Microbit screen

Show both Temperature and Humidity on the the OLED

"""
tm = TM1637.create(DigitalPin.P13, DigitalPin.P14, 7, 4)
MuseOLED.init()

def on_forever():
    dht11_dht22.query_data(DHTtype.DHT11, DigitalPin.P0, True, False, True)
    basic.show_string("H: " + str(dht11_dht22.read_data(dataType.HUMIDITY)) + " %")
    tm.show_number(dht11_dht22.read_data(dataType.TEMPERATURE))
    MuseOLED.clear()
    MuseOLED.write_string("Temperature: " + str(dht11_dht22.read_data(dataType.TEMPERATURE)) + " C")
    MuseOLED.new_line()
    MuseOLED.write_string("Humidity: " + str(dht11_dht22.read_data(dataType.HUMIDITY)) + " %")
    basic.pause(5000)
basic.forever(on_forever)

Limitations

Some of the limitations that I found were:

  • Not all Arduino sensors and devices were supported
  • Not all Arduino functionality is available with Micro:Bit. For example fonts on OLED devices.
  • Finding the correct extension can be tricky. For example searching 0.91 OLED doesn’t return any hits.
  • Some devices were supported in software, however they required 5V. A good example of this is the 2×16 LCD display

Documenting Wiring in Fritzing

Fritzing is an excellent free tool for wiring drawings (Note: for some platforms a donation might be required).

To add some Micro:bit parts to Fritzing see:

Once a parts file is downloaded it is imported into a “My Parts” grouping.

Summary

By adding extension you can greatly extend the usability of Micro:bits.

I found that for many simple projects block programming was quicker to create than Python, but it nice that the Python code gets autogenerated.

WOOB – Web Outside Of Browsers

The Web Outside of Browsers Project allows users to access Internet data in a generic way without using a Web Browser.

Woob offers a common interface for accessing a variety of different Internet data sources. So rather than using a specific API to access to weather and then another to access a job boards, the Woob API/tools can be used for both of these data sources.

Woob can be used as a:

  • Python library,
  • A command line tool, or by
  • Bash scripting

In this blog I wanted to document my Linux notes on:

  • basic command line tool usage
  • Bash/Python examples for getting weather data
  • Bash/Python examples for playing Internet streaming music

Note: WOOB is in development so features may change. Also there is a large list of application modules, but at present most are European based.

Getting Started

To install woob:

$ pip install woob

Once woob is installed a list of features can be shown by entering woob:

$ woob
usage: woob [--version] <command> [<args>]

Use one of this commands:
   bands           display bands and suggestions
   bank            manage bank accounts
   bill            get/download documents and bills
   books           manage rented books
   bugtracker      manage bug tracking issues
   calendar        see upcoming events
   cinema          search movies and persons around cinema
   cli             call a method on backends
   config          manage backends or register new accounts
   contentedit     manage websites content
   dating          interact with dating websites
   debug           debug backends
   gallery         browse and download web image galleries
   gauge           display sensors and gauges values
   geolocip        geolocalize IP addresses
   housing         search for housing
   job             search for a job
   lyrics          search and display song lyrics
   money           import bank accounts into Microsoft Money
   msg             send and receive message threads
   parcel          manage your parcels
   paste           post and get pastes from pastebins
   pricecompare    compare products
   radio           search, show or listen to radio stations
   recipes         search and consult recipes
   repos           manage a woob repository
   rpg             manage RPG data
   shop            obtain details and status of e-commerce orders
   smtp            daemon to send and check messages
   subtitles       search and download subtitles
   torrent         search and download torrents
   translate       translate text from one language to another
   travel          search for train stations and departures
   video           search and play videos
   weather         display weather and forecasts

For more information about a command, use:
   $ man woob-<command>
or
   $ woob <command> --help

Each features has a backend (database) associated with it. For example to define the weather.com backend for the weather command:

$ woob weather
Warning: there is currently no configured backend for weather
Do you want to configure backends? (Y/n): y

Available modules:
1) [ ] ilmatieteenlaitos   Get forecasts from the Ilmatieteenlaitos.fi website
2) [ ] lameteoagricole   lameteoagricole website
3) [ ] meteofrance       Get forecasts from the MeteoFrance website
4) [ ] weather           Get forecasts from weather.com
a) --all--               install all backends
q) --stop--

Select a backend to create (q to stop): 4
Backend "weather" successfully added.

If a backend needs to be removed, for example meteofrance, use the backend remove command:

$ wood weather backend remove meteofrance

To check which backends are defined, use the backend list command:

$ woob weather backend list
Enabled: weather

$ woob radio backend list
Enabled: freeteknomusic, somafm

Using the Weather Command

Woob offers a number of weather modules. A good generic option is weather.com.

To get started at the command line, enter: woob weather

$ woob weather 
Welcome to weather v3.0

Copyright(C) 2010-2022 Romain Bignon
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

Type "help" to display available commands.

Loaded backends: weather

weather> help
Weather Commands:
    cities PATTERN
    current CITY_ID
    debug
    forecasts CITY_ID

Woob Commands:
    formatter [list | FORMATTER [COMMAND] | option OPTION_NAME [on | off]]
    select [FIELD_NAME]... | "$direct" | "$full"
    ls [-d] [-U] [PATH]
    cd [PATH]
    condition [EXPRESSION | off]
    count [NUMBER | off]
    backends [ACTION] [BACKEND_NAME]...
    quit
    logging [LEVEL]

Type "help <command>" for more info about a command.

The first step is to search for a city with the cities option. Once a city is found the id (1-20) is used to show the current weather and the future forecast.

The example below searches for Toronto. The first item (Toronto, Ontario, Canada) is selected (current 1) to show the present Toronto weather. Toronto’s future weather can be checked with the forecast command (forecast 1).

Bash Scripting for Weather

The command line utility can be used with Bash script, (For more info on the interface).

A scripting example to show the current and forecast weather for Toronto (the first item in the list):

$ # use the -n 1 option to get the first item
$ woob weather cities 'Toronto' -n 1
d8ccf908e3c4c748e232720575df7cdbca6e0f1b412bca8595d8a28d0c28e8bc@weather — Toronto, Ontario, Canada

$ # get the id (first string) and save to a variable
$ city_id=$(woob weather cities 'Toronto' -n 1 | awk '{print $1}')

$ # pass the city id to get the weather current
$ woob weather current $city_id 
2022-12-08: -1 °C - 1030.2hPa (Rising) - humidity 73% - feels like -4 °C/24 °F - Fair

An example is to get the current weather for a place and show it as a Linux system tray notification:

$ # Create a notification note with current weather information
$ my_city="Sauble Beach"
$ city_id=$(woob weather cities "$my_city" -n 1 | awk '{print $1}')
$ notify-send -t 1000000 "$my_city" "$(woob weather current $city_id )

To show the long term forecast information in a Zenity dialog:

$ # Create an Info Dialog with weather forecast information
$ my_city="Sauble Beach"
$ city_id=$(woob weather cities "$my_city" -n 1 | awk '{print $1}')
$ wf=$(woob weather forecast $city_id)
$ zenity --width=650 --info --text="$wf" --title="$my_city"

Python Woob Weather Example

A Python example to select the first city in a list and then show the forecast would be:

#!/usr/bin/python3
#
# wweather.py - Search for weather forecast for a place
#
from woob.core import Woob
from woob.capabilities.weather import CapWeather

w=Woob()
w.load_backends(CapWeather)

# Find the id for a location
for city in w.iter_city_search("Sauble Beach"):
    # stop at first hit    
    print(city.id, city.name)
    break

# Get the Forecast for that location
for w0 in w.iter_forecast(city.id):
    print("Date:", w0.date)
    print("Forecast:", w0.text)
    print("High:", w0.high)
    print("Low", w0.low, "\n")

For more information on the Weather calls.

Using the Radio Command

There are a number of radio modules to choose from. The first step is to add at least one module:

$ woob radio
Warning: there is currently no configured backend for radio
Do you want to configure backends? (Y/n): y

Available modules:
1) [ ] audioaddict       Internet radios powered by audioaddict.com services
2) [ ] bandcamp          Bandcamp music website
3) [ ] freeteknomusic    freeteknomusic website
4) [ ] ina               INA French TV video archives
5) [ ] nectarine         Nectarine Demoscene Radio
6) [ ] nova              Nova French radio
7) [ ] ouifm             OÜI FM French radio
8) [ ] radiofrance       Radios of Radio France: Inter, Info, Bleu, Culture, Musique, FIP, Le Mouv'
9) [ ] somafm            SomaFM web radio
10) [ ] virginradio       VirginRadio french radio
a) --all--               install all backends
q) --stop--

Select a backend to create (q to stop): 9
Backend "somafm" successfully added.

After one of more radio modules are added, you can search for radio stations that play a genre:

$ woob radio
Welcome to radio v3.0

Type "help" to display available commands.

Loaded backends: freeteknomusic, somafm

radio> search radio trance
id: thetrip@somafm
title: The Trip
description: Progressive house / trance. Tip top tunes.
current: B With U (Salinas' Dub Mix) - Tommyboy And Soultan Feat. Zara
streams: ['130Kbps/aac' ('http://somafm.com/thetrip130.pls'), 'fast/mp3' ('http://somafm.com/thetrip.pls'), '64Kbps/aacp' ('http://somafm.com/thetrip64.pls'), '32Kbps/aacp' ('http://somafm.com/thetrip32.pls')]

radio:/search> play 1

For this radio station search of “trance” only 1 station was found, so play 1 is used to listen to that station. (Note: if 3 were returned then you could listen to the third station by: play 3).

Bash Scripting for Internet Radio

There are some woob options that will make scripting a little easier. Some genres like “rock” or “ambient” will return too many hits, so the -n option can be set to limit the number of returned items. The -s option will return only selected fields.

$ # Show the id and description 
$ #  for the first 2 ambient station 
$ woob radio search radio "ambient" -s id,description -n 2
id: deepspaceone@somafm
description: Deep ambient electronic, experimental and space music. For inner and outer space exploration.

id: groovesalad@somafm
description: A nicely chilled plate of ambient/downtempo beats and grooves.

An example script (wlisten.sh) takes a music genre entered on the command line and then finds and plays the first related station.

#!/bin/bash
#
# wlisten.sh - find a Woob Radio station and play it
#
echo -e "\nWoob Internet Radio Player\n"

echo -n "Enter a type of Music to listen to: " 
read mtype
echo "Now playing : $mtype music ..."
woob radio play $(woob radio search radio "$mtype" -n 1 -s id | awk '{print $2}' )

To run this script (wlisten.sh) to play a reggae station:

$ bash wlisten.sh

Woob Internet Radio Player

Enter a type of Music to listen to: reggae
Now playing : reggae music ...

This example only uses the first returned station. By using a couple of Zenity list dialongs a Bash script can be created to present a list of preset genres, then the user can select a specific station.

#!/bin/bash
#
# wradio.sh - find a Woob Radio station and play it
#
mtype=$(zenity --title="Play Internet Radio" \
	--list --height=500 --width=300 --column="Music Type" \
        80s Ambient Dance House Jazz Pop Rock Reggae Top Trance) 
# if a music type is selected then get stations
echo "Music Type: $mtype"
if [[ -n "$mtype" ]] ; then
  stn=$(woob radio search radio $mtype -f radio_list  | zenity --list \
    --title="Select Radio Station" \
    --column=station --column=description --width=900 --height=300)
  # Get the station ID, its the 1st part of the station string
  stn_id="${stn%%—*}"
  # If the station string is not empty, play the station
  if [[ -n "$stn_id" ]]  ; then
    echo "Station Id: $stn_id"
    woob radio play $stn_id
  fi
fi

Python Woob Radio Example

To get a selection of radio stations for a genre:

from woob.core import Woob
from woob.capabilities.radio import CapRadio

w = Woob()
w.load_backends(CapRadio)

genre="rock"

for stns in w.iter_radios_search(genre):
        print(stns.id, stns.title, stns.description, "\n")

See the Woob tech api docs for more details on the radio API.

A Python Tkinter GUI example to select a genre and then play a radio station:

#!/usr/bin/python3
#
# wradio.py - Tkinter GUI for Woob Radio    
#
from tkinter import *
from tkinter import ttk
import subprocess
from woob.core import Woob
from woob.capabilities.radio import CapRadio

w = Woob()
w.load_backends(CapRadio)

# Fill the tree with genre stations
def show_stations():
    genre = cbo.get()
    tree.delete(*tree.get_children())
    # Insert Rows into tree based on found radio stations
    for stns in w.iter_radios_search(genre):
        tree.insert('','end',text=stns.id,values=(stns.id, stns.title, stns.description))    

# Play selected station
def tree_getitem(a):
    curItem = tree.focus()
    stn_info=tree.item(curItem) # station is dictionary variable
    thestn=stn_info['text'] # get the station id
    p=subprocess.Popen(["killall", "woob"])
    p=subprocess.Popen(["killall", "mpv"])
    if thestn != "":
        labelText.set("Playing Station: " + thestn)
        p=subprocess.Popen(["woob", "radio", "play", thestn])

def stop_music():
    labelText.set("")
    p=subprocess.Popen(["killall", "woob"])
    p=subprocess.Popen(["killall", "mpv"])
    

# Create an instance of tkinter frame
root = Tk()
genre="Rock"
root.title('Play Internet Radio Stations')
root.geometry('900x300')

# Create a listbox of station genres
label1 = ttk.Label(text = "Enter a genre: ").grid(row=0,column=0)
cbo = ttk.Combobox(root,height = 1)
genres= {"80s","Ambient", "Dance", "House","Jazz","Pop","Rock", "Reggae","Trance"}
cbo['value'] = [m for m in genres]
cbo.grid(row=0, column=1)

# Create a button to update station tree
bt_update = ttk.Button(root,text="Select Genre", command=show_stations)
bt_update.grid(row=0, column=2)

# Create a tree list with 3 columns
tree = ttk.Treeview(root, column=("Id","Station", "Desciption"), show='headings') 
tree.column("#1", width=100)
tree.heading("#1", text="ID")
tree.column("#2", width=200)
tree.heading("#2", text="Station")
tree.column("#3", width=600)
tree.heading("#3", text="Description")
tree.grid(row=1, column=0, columnspan=3)
tree.bind('<ButtonRelease-1>', tree_getitem)

# Create a label to show station being played
labelText = StringVar()
label2 = ttk.Label(textvariable=labelText).grid(row=2,column=0)

# Add a button to start and stop music
bt_stop = ttk.Button(root,text="Stop Music", command=stop_music)
bt_stop.grid(row=2,column=1)

root.mainloop()
    

Summary

The Woob project has a lot of potential, however because it is still in the development stage it can be a little challenging to use.

Due to the lack of examples, I found that I was able to get up and running faster with Bash scripts rather than Python. To build a Bash script I would first test what I wanted to do in the online tool, then I would mimic this in Bash.

1-Line of Bash to add Apps to the System Tray

Putting your commonly used apps and scripts into the Linux system tray could be quite useful.

In this blog I’ll look at two approaches:

  • alltray – a command line utility to dock any program into the system tray
  • yad – (Yet Another Dialog) tool is a Bash GUI builder that also supports trays notifications.

The alltray approach is dead simple and it works for any linux application or script. The yad utility offers a little more functionality by adding the ability to create command line dialogs and it can dynamically change the tray icon, actions, menus and tool tip.

Alltray

To install alltray in Debian/Raspian/Ubuntu:

sudo apt install alltray

An simple alltray example that calls an xterm window with the top utility (to show top running processes):

# Use alltray to put a terminal window app in the the tray
#  usage: alltray [options] ["] <program_name> [parameters] ["]
#
alltray  "xterm -hold -T 'Top Processes' -e 'top'"

As a default alltray will use the icon of the command that is being called.

For this example I used xterm to open a new terminal with the options of: -hold (keep teminal open) , -t ( add title) and -e (execute a command).

If you want to change the font name and size, use the -fa and -fs options, for example:

xterm -hold -fa Monospace -fs 14  -T "Top Processes" -e  "top"

Alltray also supports a custom icon and right-click menus, an example of this would be:

# Show a tray item with a custom icon and right-click menu options
#   syntax for adding menus is: --menu "menu-label: command"
#
alltray  "xterm -hold -T 'Top Processes' -e  'top'" \
  -i /usr/share/icons/Adwaita/256x256/legacy/face-glasses.png \
  --menu "Disk Usage:xterm -hold -T 'df' -e 'df'" \
  --menu "Sensors:xterm -hold -T 'Sensors' -e 'sensors' " 

For this example a custom icon (face-glasses.png) is added along with 2 right-click menu options.

YAD – Yet Another Dialog

The yad utility is a command line dialog tool that supports a good selection of different dialog types, also yad can be configured for system tray applications without needing alltray.

For Debian/Raspian and Ubuntu systems yad can be installed by:

sudo apt install yad

Yad has a lot of options, (see the man pages or help for more details). To create a simple two button dialog:

# Show a simple YAD dialog
yad  --text="SOME TEXT"  --title="My Dialog"

To put this simple yad dialog on the system tray with a custom icon:

# Create a YAD system tray item to call a YALL dialog
#
yad --notification --image="gtk-execute" \ 
  --command="yad --text='SOME TEXT' --title='My Dialog' " \
  --text="My Tooltip"

Icons are fairly easy to manage using the yad tool: yad-icon-browser .

Like alltray, yad supports menus, below is a menu example:

# Create a YAD system tray item with a right-click menu
#
yad --notification --image="gtk-execute" \
 --command="yad --text='SOME TEXT' --title='My Dialog'" \
 --menu="Memory! yad --text='$(vmstat)' --title=VMSTAT \
        | Sensors! yad --text='$(sensors)' --title=Sensors \
        | USB ! yad --text='$(lsusb)' --title=USB \
        | Quit ! killall yad" \
 --text="My Tooltip"

The syntax for menus is:

menu=STRING
              STRING must be in the form:
              menu_label1[! action1[! icon1]]|label2[! action2[! icon2]]....   
              Menus are separated with `|' or --separator  argument.
              Menu items are separated with `!' or --item-separator argument.

For this menuing example I passed the output from command line tools like vmstat, sensors and lsusb to the yad –text parameter.

Unlike alltray, yad doesn’t have a built in quit menu option, but this functionality can be added with:

Quit ! killall yad

Remotely Change a YAD Tray Item

The yad notification option has a –listen parameter that allows commands to be sent from stdin (standard input) to yad in the form command:args. Possible commands are icon, tooltip, visible, action, menu and quit.

The yad stdio can be redirected a named pipe and this will enable other bash scripts to be able to send it commands. Below is a basic Bash script that creates a named pipe variable (mytraypipe=”/tmp/tray1.pipe”) and then it creates the named pipe if it doesn’t exist.

The Bash command: exec 1<> $mytraypipe , redirects stdio (file 1) to the named pipe. The final step is to call the yad with <&1 , to redirect the stdio into the command.

#!/bin/bash
#
# dyn_tray.sh - create a system tray item that can be modified
#             - write changes to the named pipe: $mytraypipe 
#
mytraypipe="/tmp/tray1.pipe"

# Make the pipe if required
if ! test -e "$mytraypipe"; then
  mkfifo $mytraypipe
fi

# redirect the stdio (file 1) to the named pipe
exec 1<> $mytraypipe

# create the notification icon
yad --notification                  \
    --listen                        \
    --image="emblem-colors-grey"              \
    --text="Dummy tooltip"   \
    --command="yad --text='Test Tray App' " <&1

Below is an example that changes the icon and tool tip for the earlier Bash script. This first step is to define a variable with the correct named pipe, after this commands can be send with an echo statement to the named pipe.

Summary

If you are looking for a quick way to pull together some commonly used apps and scripts both alltray and yad offer a simple 1 line of Bash solution.

If you want to dynamically change the tray item then yad is the tool for you.

Charts in 1 Line of Bash

There are some great charting packages out there, but for quick tests and playing around it’s nice to keep things simple. I wanted to do some basic Raspberry Pi charting and found that with the Gnuplot utility it took as little as 1 line of Bash to get the job done.

This blog shows some examples, probably the key points are:

  • Data can be piped to Gnuplot
  • Bash While-loops can be used to create real time/dynamic plots

Single Line Chart from a File

Gnuplot plot has a full scripting interface that allows users to create very complex presentations.

Gnuplot script can also be passed on the command line, with the options: -p (persist, spawn chart) and -e (execute command):

$ # Plot a file with 5 points, show file first
$ cat 5pnts.txt
2.7
3.5
4.1
3.9
5.6
$ # Plot the file as a line
$ gnuplot -p -e "plot '5pnts.txt' with lines title '5 Test Points' "

Multi-line Chart

The multi-line example is like the single line chart with the exception that the using statement is called for each plot line, 1:2 is the first line (x-axis column):(y-axis column) , and 1:3 is the second series line.

$ # Show the first 5 line of the data
$ head -n 5 data1.txt, Note: Gnuplot skips row starting with "#"
#t  Angle Error 
0.0	-14.7	3.6
1.0	8.6	    3.6
2.1	28.8	3.0
3.1	46.7	3.4
$
$ # plot, first column is x, next 2 columns are y series
$ gnuplot -p -e "plot 'data1.txt' \
  using 1:2 with lines title 'Angle','data1.txt' \
  using 1:3 with lines title 'Error'"

For-Loop Piped to a Line Chart

Rather than creating a text file, the output from a group of echo statements can be piped to Gnuplot. The ‘<cat’ definition is used for piped input.

$# Plot 10 random points
$ ( for i in `seq 1 10`; do echo "$RANDOM\n";  done )| \
  gnuplot -p -e "plot '<cat' with lines title '10 Random Pts'"

A more complex example would be to add a time on the X-axis and run a calculation at a sampled interval.

# Create a calc function
mycalc() {
  # Show the time and cpu idle time
  thetime=$(date +'%T')
  idle=$(top -n 1 | grep id | awk '{print $8}')
  echo "$thetime $idle\n"
}
# Run the calc function in the for-loop
# Define the y range, x scale as time
( for i in `seq 1 20`; do mycalc; sleep 1;  done )| \
  gnuplot -p -e "set yrange [50:100]; set xdata time; \
  set timefmt '%H:%M:%S';set format x '%H:%M:%S'; \
  plot '<cat' using 1:2 with lines title 'CPU Idle Time' "

Bar Charts from a File

For the bar chart example the plot options are defined in a variable. This example uses the iostat command to get the user and idle time usage. Awk is used to parse the 4th row and then get the individual values.

# Create a file with user and idle CPU usage
# Column: 1=bar position, 2=value, 3=label
iostat | awk '{if(NR==4) print "0 " $1 " user"}' > cpu0.dat
iostat | awk '{if(NR==4) print "1 " $3 " idle" }' >> cpu0.dat

# Define the options as a variable
options="set title 'CPU Diagnostics'; set boxwidth 0.5; \
  set style fill solid; "

# Plot the bars with labels, note: the label offset 
gnuplot -p -e "$options ;plot 'cpu0.dat' \
 using 1:2:xtic(3) with boxes notitle, \
 '' using 1:2:2 with labels font 'Helvetica,15' \
 offset 0.9,0.8  notitle" 

Bars with Dynamic Updates

A Bash while-loop can be used to dynamically get new data and pass it to Gnuplot.

Below is an example that refreshes a bar chart every 5 seconds. The refresh time is shown in the bar series title.

mycalc2() {
  # Show the time and user/cpu idle time
  # note: top gets instantaneous values, iostat uses averages
  top -n 1 | grep %Cpu | awk '{print "0 " $2 " user"}' > cpu0.dat
  top -n 1 | grep %Cpu | awk '{print "1 " $8 " idle"}' >> cpu0.dat
}
# define chart options
options="set title 'CPU Diagnostics'; set boxwidth 0.5; \
  set style fill solid; set yrange [0:100]; set xrange [-0.5:1.5]"

# create an infinite while loop to get data, and then plot
# note1: gnuplot needs a while loop to replot
# note2: use a big pause time in gnuplot or exiting will be tough 
( while :; do mycalc2; sleep 10;  done )| \
gnuplot -p -e "$options ; plot 'cpu0.dat' \
 using 1:2:xtic(3) with boxes \
 title \"$(date '+%T')\" ; while (1) { replot; pause 5 }" 

Summary

Using Gnuplot with the command line option allows for some quick and easy charting. For more detailed work try creating a Gnuplot script file.

Web Scraping with 1 Line of Bash

In the past Python with the Beautiful Soup library has been a great approach for my web scraping.

I was recently doing a small project and I was amazing at what one Bash statement would get me.

My approach was to use the text based Lynx browser and pipe the output to a grep search.

Below is an example where I used Lynx to dump the “Sunshine Village Snow Forecast” web page to find how much snow they had.

The Lynx Text Browser

The first step in web scraping is to get a web page into a searchable format.

I started out by looking at using cURL with the html2text tool, but I found that using the Lynx browser offered a one step solution with a cleaner text output.

To install Lynx on Raspian/Debian/Ubuntu use:

sudo apt install lynx

The lynx -dump option will output a web page to text with HTML tags and Javascript removed. It’s important to note that what you see on the page may not match the outputted text.

Below is an example where I wanted to get the new snow at Sunshine Village. On the web page Javascript is used to show the snow depth as either centimetres or inches, but on the text output both units and their values are shown.

Bash has a good selection of string manipulation tools. Below is an example to extract the first part of string to only show the snow in centimeters (cm):

$ theurl="https://www.snow-forecast.com/resorts/Sunshine/6day/mid"
$ thestr="New snow in Sunshine Village:"
$ # Create a variable with the result string from Lynx
$ newsnow=$(lynx -dump "$theurl" | grep "$thestr")
$ 
$ echo "$newsnow"
   New snow in Sunshine Village:  4.8cm1.9in on Fri 8th (after 3 PM)
$    
$ # Get the first part of the string before "cm"
$ # The %% gets the first part
$ echo "${newsnow%%cm*} cm"
   New snow in Sunshine Village:  4.8 cm

My Final App

We were going on a family ski trip and to get pumped I created a morning notification script that showed the new morning snow and the base.

#!/bin/bash
#
# skitrip.sh - show the Sunshine ski conditions in a notification
#
theurl="https://www.snow-forecast.com/resorts/Sunshine/6day/mid"

# Get the new snow depth
thestr="New snow in Sunshine Village:"
result=$(lynx -dump "$theurl" | grep "$thestr")
newsnow="${result%%cm*} cm"

# Get the base
thestr="Top Lift:"
base=$(lynx -dump "$theurl" | grep "$thestr")

# Show the results in a desktop notification
msg="$newsnow\n$base (base)"
icon="$HOME/Downloads/mountain.png"
notify-send -t 10000000 -i "$icon"  "Sunshine Ski Resort" "$msg"

The notify-send utility will put a message on a Linux desktop, another option could be to send a SMS message.

Summary

Scraping web pages can be tricky and the pages can change at anytime.

I found that Lynx worked on many pages but not all.

The grep utility is extremely useful and it offers a lot of interesting options, such as getting lines before or after the found string.

Text Interfaces with Whiptail and Dialog

Text interfaces are extremely useful when an MS-Window client is trying to connect into a Linux or Raspberry Pi node.

There are some good options for creating Bash text interfaces, two of the most popular are Whiptail and Dialog. Raspberry Pi users will be familiar with Pi configuration tool, raspi-config, which uses Whiptail to create all its menu and application screens.

Whiptail comes pre-installed on Raspbian and on many Linux images. Whiptail is a lighter version of the more complete Dialog package.

This blog will show some examples using Whiptail and Dialog.

Getting Started

For my work I preferred using Dialog, I found that it had many more options and a slightly easier interface than Whiptail. However for projects where I needed to distribute my code to other users or nodes I would keep things simple and use Whiptail.

To install Whiptail and Dialog on a Raspbian/Debian/Ubuntu:

sudo apt install whiptail
sudo apt install dialog

A simple Whiptail test to show the date/time in a message box:

whiptail --title "Time/Date" --msgbox  "$(date)" 0 0

This statement will clear the terminal screen and show a default background with message box centered in the window. A height and width of : 0 0 will autosize the message box.

Refreshing YESNO Dialog

A message box only has a single OK button. A yesno dialog has two button.

Whiptail only supports a basic yesno function. The Dialog utility supports changing the button labels and a timeout so the window can be automatically refreshed.

Below is an example that refreshed an yesno dialog every 5 seconds. The show_dlg function generates a random temperature and humidity valid, and then calls the dialog utility. The YES button, relabelled as “Refresh”, will manually force a refresh of the data and redraw the window. The NO button, relabelled as “Cancel” will close the script and clear the screen.

#!/usr/bin/bash
#
# dyesno.sh - A freshing yes/no dialog with simulated sensor data

YES=0
TIMEOUT=255

show_dlg() {  
   # simulate some data
   data1="Temperature: $(( ( RANDOM % 10 )  + 20 )) C";   
   data2="Humidity: $(( ( RANDOM % 10 )  + 30 )) %";   
   message="$data1\n\n$data2";   
   dialog --begin 2 2 --backtitle "Raspberry Pi Data - $(date +'%T' )" \
       --yes-label "Refresh" --no-label "Cancel" --timeout 5 \
       --title "DHT11 Sensor Results" --yesno  "$message" 8 30; 
} 

response=0
# Cycle is the response is YES button or the dialog timed out
while [ "$response" == "$YES" ] || [ "$response" == "$TIMEOUT" ]; do
  show_dlg
  response=$? ;# Get the output from the yesno dialog
done
clear

Radio Dialog to Toggle Pi GPIO Pins

A radio dialog allows a user to select one option from a list.

For this example I’m using 3 GPIO pins (0, 2, and 7) and the user can select one of these pins and toggle the output using the Raspberry Pi gpio utility.

The Dialog utility will output the item selected (0, 2 or 7), and the OK/Cancel button code (0=OK,1=Cancel).

#!/usr/bin/bash
#
# dradio1.sh - toggle a GPIO output pin
#
thepin=$(dialog --begin 2 2 --title "Toggle GPIO Pins" \
     --backtitle "Raspberry PI GPIO" --stdout \
     --radiolist "Select Pin:" 10 40 3 \
      0 "GPIO_0 - physical pin 11" off \
      2 "GPIO_2 - physical pin 13" off \
      7 "GPIO_7 - physical pin 7 " off )
clear
# Toggle if OK entered and a pin is selected
if [ "$?" == "0" ] && (( ${#thepin} > 0 )) ; then
  echo "Toggling (wPi) pin: $thepin"
  gpio toggle $thepin
  echo "Pin $thepin is: $(gpio read $thepin)"
fi   

Weather Station Form

A Form dialog can be used text with captions and allow user input to saved. Below is an example of a Weather station with view only and editable data.

#!/usr/bin/bash
#
# dform1.sh - Form to show data, and allow data entry

# Weather Sensor inputs (connect real inputs here)
wspeed="5 kph"
wdir="NW (350)"
wtemp="11 C"

# Show a dialog with viewonly and data entry values, save to a file
dialog --begin 2 2 --ok-label "Save" --backtitle "Pi Weather Station" \
     --title "North Beach" --stdout  \
     --form  "Data at : $(date +'%T' )"  12 65 0 \
	"Wind Speed :"      1 1 "$wspeed"  1 30 0 0 \
	"Wind Direction:"   2 1 "$wdir"    2 30 0 0 \
	"Water Temp:"       3 1 "$wtemp"   3 30 0 0 \
	"Beach Conditions:" 4 1 ""  	   4 30 30 30 \
	"Wildlife:"         5 1 ""  	   5 30 30 30 > beach.txt     
clear

The syntax for the form is:

--form text height width formheight [ label y x item y x flen ilen ]

where: y = line position
       x = position in line
    item = view only or editable data
    flen = field length , 0 = view only
    ilen = input length , 0 = view only

For this example the last 2 items (Beach Conditions and Wildlife) have a field and input length defined to 30 characters so data can be entered into these fields. If the OK button is selected the user entered data is saved to the file beach.txt.

Menu Example

Menuing applications are probably the most useful feature in the Whiptail and Dialog utilities.

Below is an System Information application, that has 3 options: node, disk space, and memory stats. Each of the menu items call a display_dialog function that presents the results of a Bash statement in a message box.

#!/bin/bash
#
# dmenu.sh - Bash Dialog Menu showing some system stats
#

# Display menu results in a msgbox
display_result() {
  dialog --title "$1" \
    --backtitle "System Information" \
    --no-collapse \
    --msgbox "$result" 0 0
}

while true; do
  selection=$(dialog --stdout \
    --backtitle "System Information" \
    --title "Key Features" \
    --clear \
    --cancel-label "Exit" \
    --menu "Please select:" 0 0 4 \
    "1" "Display Node Information" \
    "2" "Display Disk Space" \
    "3" "Display Memory Stats" \
     )
  exit_status=$?
  if [ $exit_status == 1 ] ; then
      clear
      exit
  fi
  case $selection in
    1 )
      result=$(echo "Hostname: $HOSTNAME"; uptime)
      display_result "System Information"
      ;;
    2 )
      result=$(df -h)
      display_result "Disk Space"
      ;;
    3 )
      result=$(vmstat --stats)
      display_result "Memory Stats"
      ;;
  esac
done

Changing Whiptail Default Colors

Whiptail uses the newt graphic library. A NEWT_COLORS variable can be created with custom colors. An example would be:

export NEWT_COLORS='
  window=,red
  border=white,red
  textbox=white,red
  button=black,white'
# to reset the color back to default use:
# unset NEWT_COLORS

A full definition of all the options and colors:

root                  root fg, bg
border                border fg, bg
window                window fg, bg
shadow                shadow fg, bg
title                 title fg, bg
button                button fg, bg
actbutton             active button fg, bg
checkbox              checkbox fg, bg
actcheckbox           active checkbox fg, bg
entry                 entry box fg, bg
label                 label fg, bg
listbox               listbox fg, bg
actlistbox            active listbox fg, bg
textbox               textbox fg, bg
acttextbox            active textbox fg, bg
helpline              help line
roottext              root text
emptyscale            scale full
fullscale             scale empty
disentry              disabled entry fg, bg
compactbutton         compact button fg, bg
actsellistbox         active & sel listbox
sellistbox            selected listbox

bg and fg can be:

color0  or black
color1  or red
color2  or green
color3  or brown
color4  or blue
color5  or magenta
color6  or cyan
color7  or lightgray
color8  or gray
color9  or brightred
color10 or brightgreen
color11 or yellow
color12 or brightblue
color13 or brightmagenta
color14 or brightcyan
color15 or white

Changing Default Dialog Colors

The custom Dialog colors are defined in the file: ~/.dialogrc

To create and edit this file:

dialog --create-rc  ~/.dialogrc
nano $HOME/.dialogrc

Within the ~/.dialogrc file, an important option is:

# Shadow dialog boxes? This also turns on color.
use_shadow = OFF

Dialog supports inline color (this isn’t supported in Whiptail) with the –colors option. Inline colors are defined with by “/Zx”:

ANSI colors:

/Z0 = black
/Z1 = red
/Z2 = green
/Z3 = yellow
/Z4 = blue
/Z5 = magenta
/Z6 = cyan
/Z7 = white

Other options:

/Zb = bold, /ZB = reset bold
/Zr = reverse, /ZR = reset reverse
/Zu = underline, /ZU = reset underline
/Zn = restore settings to normal

An example:

$ msg="Temperature: \Zb\Z3 28 \Zn Deg C"
$ dialog --title "Outside" --colors --msgbox  "$msg" 0 0 ; clear

Final Comments

I’m a big fan of Zenity and YAD X-Window dialog tools, and I found that it wasn’t a big transition to use Whiptail and Dialog.

It’s important to note that the Dialog option –stdout is needed if you want to pass the Dialog output to a variable. Passing the output from Whiptail is a little trickier, use: 3>&2 2>&1 1>&3