Difference between revisions of "Swagbadge2021 SoftwareDev"

From Open Hardware Miniconf
Jump to: navigation, search
(Directory structure)
(Hardware reference)
 
Line 211: Line 211:
 
Globals
 
Globals
 
* '''[] oled.oleds''' # an array of oleds, letting you address them separately if you choose
 
* '''[] oled.oleds''' # an array of oleds, letting you address them separately if you choose
* '''int oled.fg/oled.bg''' # get/set fg and bg colours (0 or 1)
+
* '''int oled.FG/oled.BG''' # get/set fg and bg colours (0 or 1)
 
* '''int oled.font_size''' # helpful to work out how much space a line of text will take up
 
* '''int oled.font_size''' # helpful to work out how much space a line of text will take up
* '''boolean oled.lock_title''' # true/false. True means the display always shows the title. False means it'll be wiped once the display is cleared.
+
* '''boolean oled.lock_title''' # true/false. True means the display always shows the title. False means it'll be wiped once the display is cleared. Annunciators always are displayed.
  
 
Functions
 
Functions
 
* '''oled.initialise()''' # uses configuration.oled.settings by default, normally already invoked by the base swagbadge handler
 
* '''oled.initialise()''' # uses configuration.oled.settings by default, normally already invoked by the base swagbadge handler
* '''oled.oleds_clear(colour)''' # pass in oled.bg or oled.fg for easy set. Wipes the display
+
* '''oled.oleds_clear(colour)''' # pass in oled.BG or oled.FG for easy set. Wipes the display
 
* '''oled.log(text)''' # scroll up text to insert a new line at the bottom of the display
 
* '''oled.log(text)''' # scroll up text to insert a new line at the bottom of the display
 
* '''oled.text(text, x, y, colour)''' # add text at the position x,y in the colour specified. Note: does not clear the contents at this spot first.
 
* '''oled.text(text, x, y, colour)''' # add text at the position x,y in the colour specified. Note: does not clear the contents at this spot first.
Line 238: Line 238:
 
=== Screen buttons ===
 
=== Screen buttons ===
  
If you push (gently!) on the screens, you'll discover they double as buttons: there's a small switch under each OLED that is pressed when you push down on the screen.
+
If you push (firmly but gently) on the screens, you'll discover they double as buttons: there's a small switch under each OLED that is pressed when you push down on the screen.
  
 
{{Note|'''Warning''': Push on the '''middle''' of the screen, not the edge. This will minimise the risk of bending... and breaking... the screen.}}
 
{{Note|'''Warning''': Push on the '''middle''' of the screen, not the edge. This will minimise the risk of bending... and breaking... the screen.}}
Line 247: Line 247:
  
 
<pre>
 
<pre>
from machine import Pin
+
from aiko import button
  
button_left = Pin(16, Pin.IN, Pin.PULL_UP)
+
left_screen = 16
button_right = Pin(17, Pin.IN, Pin.PULL_UP)
+
right_screen = 17
  
pressed = not(button_left.value())
+
def my_button_handler(number, state):
 +
  print("Button {}: {}".format(number, "press" if state else "release"))
 +
 
 +
button.add_button_handler(my_button_handler, [left_screen, right_screen])
 +
aiko.button.add_touch_handler(my_button_handler, [12, 14, 15, 27])
 +
aiko.button.remove_handler(my_button_handler)
 +
 
 +
 
 +
def my_multibutton_handler(pin_numbers):
 +
  print("Multibutton {}".format(pin_numbers))
 +
button.add_multibutton_handler(my_multibutton_handler, [12, 14])
 +
button.add_multibutton_handler(my_multibutton_handler, [15, 27])
 +
button.remove_handler(my_multibutton_handler)
 
</pre>
 
</pre>
 
The button sends "1" when it's ''not'' being pressed, and "0" when it is. This is counterintuitive to most of us, so I recommend inverting the value when reading it.
 
  
 
=== Sliders ===
 
=== Sliders ===
Line 266: Line 276:
  
 
<pre>
 
<pre>
from machine import Pin, TouchPad
+
import aiko.button as button
import aiko.common
+
import aiko.common as common
 +
 
 +
bottom_left = 12
 +
top_left = 15
 +
bottom_right = 14
 +
top_right = 27
 +
 
 +
def my_slider_handler(number, state, value):
 +
  print("Slider {}: {} {}".format(number, state, value))
 +
  # use the map handler to give you the slider values mapped from its min-max to yours
 +
  mapped_value = int(common.map_value(value, 0, 100, 1, 5))
 +
 
 +
aiko.button.add_slider_handler(my_slider_handler, bottom_left, top_left)
 +
aiko.button.add_slider_handler(my_slider_handler, bottom_right, top_right)
 +
aiko.button.remove_handler(my_slider_handler)
  
bottom_left = TouchPad(Pin(12))
 
top_left = TouchPad(Pin(15))
 
bottom_right = TouchPad(Pin(14))
 
top_right = TouchPad(Pin(27))
 
  
 
# is this button pressed?
 
# is this button pressed?
 
# the commons library returns true/false
 
# the commons library returns true/false
 
pressed_bottom_left = common.touch_pins_check([bottom_left])
 
pressed_bottom_left = common.touch_pins_check([bottom_left])
 
# check where we are along the slider
 
# the 'read' functions return an integer of the 'strength' of the
 
# capacitance. As you move your finger between the endpoints,
 
#the relative value of the difference between them will change.
 
slider_left = bottom_left.read() - bottom_left.read()
 
 
</pre>
 
</pre>
  
 
Micropython functions
 
Micropython functions
* '''touchpad.read()''' - gives you the current value of the sensor. It will be large when not touched (over 1000) and smaller when touched.
 
Aiko functions
 
 
* '''common.touch_pins_check(touchpad_array)''' - Returns true if ''all'' the touchpad sensors listed are being touched, false otherwise.
 
* '''common.touch_pins_check(touchpad_array)''' - Returns true if ''all'' the touchpad sensors listed are being touched, false otherwise.
 +
* '''common.map_value(input, in_min, in_max, out_min, out_max)''': map an input range to an output range and convert the input value to a value in the output range.
  
 
=== SAOs (simple addons) ===
 
=== SAOs (simple addons) ===
  
 
Information about [[Swagbadge2021_SAO|hardware]]
 
Information about [[Swagbadge2021_SAO|hardware]]

Latest revision as of 05:11, 25 January 2021


Want to write your own programs to run on your badge? This page gives a brief overview of the software architecture, an example of a program, and a reference guide on how to access the hardware.

Background

The badge runs MicroPython (written by Damien George from Melbourne!) and has the Aiko framework (written by OHMC's Andy Gelme) loaded on it.

MicroPython gives you libraries to access the ESP32 microprocessor functionality. Things like: reading and writing to specific pins on the processor or performing operating system functions such as accessing the file system).

Aiko gives you convenience libraries for driving the badge hardware like the OLED screens, as well as running threads to keep your badge connected to wifi, a connection to the MQTT server, the emergency-stop function to prevent runaway code, and an automatic upgrade function so we can ship upgrades to your device without you having to wrangle git.

Sample code

This code writes "Hello world" to one screen, while displaying the status of the wifi, MQTT connection, button presses and slider status on the left hand screen.

It demonstrates how to use the Aiko event loop to periodically poll for hardware state, as well as how to access some of the features of the badge.

You can find more example code in the examples directory of the Aiko repository on github.

Example code: use "expand" to view
 
# examples/helloworld.py
#
# Writes a message to the oled and displays some state info
#
# Usage
# ~~~~~
# import examples.helloworld as helloworld
# from examples.helloworld import run
# run()
#

from machine import Pin, TouchPad
import aiko.event as event
import aiko.oled as oled

title = "Hello!"

import configuration.main


# Buttons: underneath the screens
button_right = Pin(17, Pin.IN, Pin.PULL_UP)
button_left = Pin(16, Pin.IN, Pin.PULL_UP)

# Clean out the whole line that was there before
# Write some new text and show it
def oled_write_line(oled_target, x, y, text):
    oled_target.fill_rect(0,y,oled.width, oled.font_size, oled.bg)
    oled_target.text(text, x, y)
    oled_target.show()
    
def status():
    sliders = touch_slider_handler()
    oledL = oled.oleds[0]
    oledR = oled.oleds[1]
    oled.write_line(oledR, 1, 10, "Hello world!")
    oled_write_line(oledL, 1, 10, "Wifi: "+str(net.is_connected()))
    oled_write_line(oledL, 1, 20, "MQTT: "+str(mqtt.is_connected()))
    oled_write_line(oledL, 1, 30, " "+str(int(not(buttonL.value())))+" Button "+str(int(not(buttonR.value()))))


# Check on the status of the badge hardware and display that every 500ms          
def run():
    oled.oleds_clear(oled.bg)
    oled.set_title(title)
    event.add_timer_handler(status, 500)
    try:
        event.loop()
    finally:
        event.remove_timer_handler(statusbar)

Architecture of a Swagbadge application

An application runs when the Aiko framework is also running, so you can rely on having all the badge services available.

An application is designed to be triggered when the badge is rebooted (either via pushing the button on the back, or via using ^D in the repl). The badge knows which application to run by the state of the configuration\main.py file.

The framework will call your application's initialise() function. The framework is running an event loop, so the initialise function is the place to add in event handlers to respond to actions you take on the badge.

Note: Refreshing the OLEDs is reasonably intense on the microprocessor. If you have too many event loops triggering too often and/or too much screen activity, you can starve the badge of resources. This can produce wifi dropouts, or non-responsiveness. If you have a lot of screen activity, consider using a buffer or recording state and just doing a screen update periodically


Software reference

MicroPython environment

MicroPython docs

Special files

There are two special files which are run automatically by MicroPython if they exist on the filesystem: boot.py and main.py.

  • boot.py: This file is run first on power up/reset. We don't modify it from what MicroPython ships with, but it's important to know it exists (and not to delete it), and is interesting to look into it to see what it does.
  • main.py: This is run after boot.py so if you want something to automatically happen on boot/reset, this is where to put it. We use this to start up the Aiko framework, initialise the hardware (such as the network and mqtt) and start up any application that is configured inside configuration/main.py

Directory structure

What's on the processor? Where does it live?

  • main.py, boot.py - files run on bootup
  • lib - code in here is aiko framework code and automatically in the micropython 'path'.
    • aiko - the aiko framework
  • applications - collection of programs, one of which is configured to run automatically on bootup. Each application must have an initialise() function
  • configuration - control the behavior of Aiko by changing the "settings", e.g configuration/main.py specifies which application to run
  • examples - sample code!

You can't edit the files directly on the processor (there's no editing tooling): you edit a copy of it on your computer, then use mpfshell to put it onto the badge.

For micropython purposes, you can use the import statement to import anything in the lib directory without prefacing it with "lib", anything else is imported from root. For example:

  • to import lib/aiko/net.py, you would use import aiko.net
  • to import examples/showcase.py, you would use import examples.showcase

Because the aiko configuration files are, themselves, just python, you can also do import configuration.main.settings where the file is in /configuration/main.py but it contains a datastructure called 'settings'.

MQTT

See Swagbadge2021_MQTT


Software development

Aiko

The Aiko Engine is what runs after boot (FreeRTOS, then microPython). Aiko provides a framework for developing applications, by providing higher level abstractions over the basic hardware, events, messaging and other conveniences. Basically, a lot of house keeping that you'd end up writing yourself. After boot, there are two background threads, one looking after Wi-Fi connectivity and the other looking after MQTT connectivity. There are also three event handlers: MQTT keep-alive, firmware upgrader and the work-in-progress SwagBadge application handler.

>>> aiko.event.event_list.print()
<function swagbadge_handler at 0x3ffe6c70> every 5000 next 23099
<function upgrade_handler at 0x3ffedfa0> every 5000 next 23099
<function mqtt_ping_handler at 0x3ffe9820> every 60000 next 73672

That last handler, which every 5 seconds is updating the title bar "LCA2021" <--> "SwagBadge" (see https://github.com/geekscape/aiko_engine_mp/blob/master/applications/swagbadge.py ).

You can stop a handler, like this (this will stop the top bar status updater):

>>> import applications.swagbadge
>>> aiko.event.remove_timer_handler(application.swagbadge_handler)


Testing code

First write your code on your computer, and make sure it compiles ( python -m py_compile code.py )

Then, you can go in repl and use paste mode (^E). It only works if you paste a few lines at a time. End your paste with ^D and then if you pasted a function, call the function:

>>> ^E
paste mode; Ctrl-C to cancel, Ctrl-D to finish
=== PASTE_YOUR_CODE_HERE
=== ^D
>>>

Pushing new code

Occasionally you might find that the updated source code that you just used the mpfshell "put" command to transfer isn't properly stored on the ESP32 microPython flash filesystem. After rebooting, you might see unexpected errors. A simple way to check is to use the mpfshell "cat" command to check that the file was completely transferred.

The safest way to update code is to perform a reboot after transferring all the files. Doing a soft-reboot using Control-D doesn't take long, i.e it is faster and easier than doing a hard-reboot with the reset button. The good thing about a reboot (microPython interpreter restart) is that there is no confusion about the complete state of your system, i.e what references have been held onto from your older source code that might still be running in a thread or as an event handler (via the Aiko Engine framework).

You can also delete all running modules:

import sys
sys.modules.clear() 

More details in https://spectrum.chat/lca2021-swagbadge/software/safe-software-updates~ba541e3c-d8a2-41a8-ac47-71d87696de2d

Replacing the swagbadge application with your own

Edit configuration/main.py and set "application": "application/yourcode"

Reboot to shell / Force runaway code to stop

It's possible to write code that will leave your badge unresponsive. Rebooting just reruns the same bad code. What to do?

  • Place a finger on each of the bottom slider pads, either side of the microprocessor.
  • Trigger a restart by using the button on the back of the badge, or Ctrl-D in repl (if you can get into repl)

The badge will restart but not autorun any code, just drop you back at the repl prompt. From there you can put bugfixed code back onto the device.

See https://github.com/geekscape/aiko_engine_mp/blob/a5b5593d37509df64a3dcb4cdc3d13a411152d82/main.py#L20


Hardware reference

OLED screens

via MQTT

  • (oled:clear) : clears both screens
  • (oled:log Hello World!) : writes a message along the bottom of the screens, scrolling up whatever is there out of the way
  • (oled:pixel x y) : lights a pixel at that spot
  • (oled:text x y This is a test!) : Puts some text at the position x,y. It will be displayed over the top of whatever's there.

via API

On the badge, by default the two oleds work as a single long screen, with text split across both of them. You can access a specific oled and use the underlying functions to write to them

Library

  • from aiko import oled # make the oled functions available for use within your code

Globals

  • [] oled.oleds # an array of oleds, letting you address them separately if you choose
  • int oled.FG/oled.BG # get/set fg and bg colours (0 or 1)
  • int oled.font_size # helpful to work out how much space a line of text will take up
  • boolean oled.lock_title # true/false. True means the display always shows the title. False means it'll be wiped once the display is cleared. Annunciators always are displayed.

Functions

  • oled.initialise() # uses configuration.oled.settings by default, normally already invoked by the base swagbadge handler
  • oled.oleds_clear(colour) # pass in oled.BG or oled.FG for easy set. Wipes the display
  • oled.log(text) # scroll up text to insert a new line at the bottom of the display
  • oled.text(text, x, y, colour) # add text at the position x,y in the colour specified. Note: does not clear the contents at this spot first.
  • oled.oleds_show() # refresh the display with the latest text/image additions
  • oled.set_title(text) # set the title text
  • oled.write_title() # write the title text

Oled image push and I2C speed

- example #1: https://github.com/geekscape/aiko_engine_mp/blob/master/examples/oled_image.py

- example #2: https://github.com/geekscape/aiko_engine_mp/blob/master/examples/oled_benchmark.py

The 2nd example gets a speed of 21fps per screen (or about 10fps for both screens).

Using C++ code (see https://github.com/geekscape/aiko_engine_mp/commit/a22b3099584c9978807e08adf752f3082af65875 ) you get at least 61fps per screen (if you do assembly: 86fps, and with crazy I2C noacks hacks, you can get 150fps: https://bitbanksoftware.blogspot.com/2018/05/fast-ssd1306-oled-drawing-with-i2c-bit.html ).

Back to python, if you increase the CPU speed with machine.freq(240000000) , you'll get 26fps per screen, and if you replace the image pixel pushes by a full back or full white push, you get 29fps, which seems to be an upper limit of the python oled code. A better end to end C++ driver could give a lot more speed.

Screen buttons

If you push (firmly but gently) on the screens, you'll discover they double as buttons: there's a small switch under each OLED that is pressed when you push down on the screen.

Warning: Push on the middle of the screen, not the edge. This will minimise the risk of bending... and breaking... the screen.

The left screen button is pin 16. The right screen button is pin 17.

via API

from aiko import button

left_screen = 16
right_screen = 17

def my_button_handler(number, state):
   print("Button {}: {}".format(number, "press" if state else "release"))

button.add_button_handler(my_button_handler, [left_screen, right_screen])
aiko.button.add_touch_handler(my_button_handler, [12, 14, 15, 27])
aiko.button.remove_handler(my_button_handler)


def my_multibutton_handler(pin_numbers):
  print("Multibutton {}".format(pin_numbers))
button.add_multibutton_handler(my_multibutton_handler, [12, 14])
button.add_multibutton_handler(my_multibutton_handler, [15, 27])
button.remove_handler(my_multibutton_handler)

Sliders

The sliders operate via capacitive touch. You can use them to detect if someone has their finger on the circular pads at either end (which let you treat them like buttons), or you can detect the position of the finger along the slider by checking the relative values as the capacitance changes.

The left slider is on pins 12 (bottom) and 15 (top), right slider is on pins 14 (bottom) and 27 (top).

More info from MicroPython about capacitive touch.

import aiko.button as button
import aiko.common as common

bottom_left = 12
top_left = 15
bottom_right = 14
top_right = 27

def my_slider_handler(number, state, value):
   print("Slider {}: {} {}".format(number, state, value))
   # use the map handler to give you the slider values mapped from its min-max to yours
   mapped_value = int(common.map_value(value, 0, 100, 1, 5))

aiko.button.add_slider_handler(my_slider_handler, bottom_left, top_left)
aiko.button.add_slider_handler(my_slider_handler, bottom_right, top_right)
aiko.button.remove_handler(my_slider_handler)


# is this button pressed?
# the commons library returns true/false
pressed_bottom_left = common.touch_pins_check([bottom_left])

Micropython functions

  • common.touch_pins_check(touchpad_array) - Returns true if all the touchpad sensors listed are being touched, false otherwise.
  • common.map_value(input, in_min, in_max, out_min, out_max): map an input range to an output range and convert the input value to a value in the output range.

SAOs (simple addons)

Information about hardware