Scripting guide¶
Write your own apps for AWTRIX: small programs that draw on the display, fetch data and react to buttons.
New here?
How the display works shows where things sit on the display and which text moves by itself.
What you get¶
Your first script takes five steps in the web UI:
- Open the web UI in your browser and go to the Scripts tab.
- Press + to start a new app, and type
Hellointo the name field. -
Replace the text in the editor with this:
-
Press Save (or Ctrl-S).
- Press Show on the clock. The display shows hi in green.
Hello is an app in the rotation from now on, and it comes back after a reboot. The name you
typed is its install name: AWTRIX saves the script under it and tells apps apart by it. See
Install name.
Other ways in
- Ready-made scripts: the AWTRIX Hub collects finished scripts from the community. Send one to your clock from its page. The web UI offers later versions as an update.
- Step by step: the tutorials, starting with Tutorial 1: Draw something, grow one app from a blank display to a live weather chart.
- Not a programmer? Build an app with AI gives you a prompt for a chatbot. Describe the app in plain words and paste the script it writes into the web UI.
How it behaves¶
- A script is an app. It takes its turn in the rotation like a pushed app. Unlike a pushed app, it makes its own content, fetches its own data and stays on AWTRIX after a reboot.
- Your app is a class. AWTRIX calls its methods, the hooks, at set moments.
The file ends with
return YourClass(), which hands AWTRIX the app to run. draw()paints one frame, about 40 times a second, only while your app is shown. Every frame starts black, sodraw()paints everything each time. Onlydraw()can paint: drawing calls in other hooks do nothing.loop()runs about once a second, also while your app is hidden. Fetch, count and build your text there, and keep the result in a member such asself.label.draw()then only paints it.on_show()runs each time your app appears, before its first frame. Reset a scroll position or a counter there.on_hide()runs when your app has left.- An error stops only your app. The display shows
ERR:in red in its turn, and the Scripts tab shows the message next to the script. Fix the line it names and save again. SeeERR:on the display.
How the display works explains where things sit and which text moves.
Keep a value between frames¶
The members of your class keep their values from one call to the next. Declare them with var
at the top of the class, give them a starting value in init(), and change them in loop():
class Seconds
var count, label
def init()
self.count = 0
self.label = "0"
end
def loop()
self.count += 1 # about once a second
self.label = str(self.count) # build the text here ...
end
def draw()
clear()
text(1, 6, self.label, 0xFFFFFF) # ... and only paint it here
end
end
return Seconds()
The number goes up once a second. draw() paints the same text about 40 times in that second.
Keeping scripts small shows why the text is built in
loop().
React to a button¶
on_button(btn) is called when a button is pressed while your app is shown. btn is "left",
"select" or "right". Return true to keep the press for your app:
class Pages
var page, label
def init()
self.page = 1
self.label = "1"
end
def on_button(btn)
if btn == "left"
self.page -= 1
elif btn == "right"
self.page += 1
else
return false # select works as usual
end
self.label = str(self.page)
return true # the display stays on this app
end
def draw()
clear()
text(1, 6, self.label, 0xFFFFFF)
end
end
return Pages()
A press your app takes stays with it: left and right do not switch apps, select does not dismiss
a notification, and a double press does not switch the display off. Each press your app takes
also gives it its full time on the display again. Return nothing, false or nil, and the button
works as usual. Button events adds holding and releasing.
What a script can do¶
Each topic has its own page in the menu. Find the row that matches the app you have in mind, and follow the link. In the editor, Ctrl-. lists every call.
| For | The calls |
|---|---|
| Being an app | draw() init() setup() loop() on_show() on_hide() on_button() |
| Skipping a turn or staying longer | should_show() duration() |
| Running as a background script or from the device menu | # @headless true, # @ondemand |
| Drawing | clear() pixel() line() rect() rect_fill() circle() circle_fill() icon() width() height() rgb() hsv() |
| Writing text | text() text_width() text_ink_width() font() |
| Text that moves or shades | scroll_text() ramp_text() |
| Charts and bars | bar_chart() line_chart() progress() |
| Animated backgrounds | effect() overlay() |
| The clock and the calendar | hour() minute() second() weekday() day() month() year() now_ms() epoch_ms() |
| Working with numbers | num() round() clamp() min() max() |
| Doing something later | timer.after() timer.every() timer.cancel() |
| Buttons | on_button() on_button_event() |
| What the device measures | sensor.temperature() sensor.humidity() sensor.pressure() sensor.light() sensor.battery() sensor.battery_volts() |
| Remembering across a reboot | store.get() store.set() |
| Letting the user change something | a # @config line, then store.get() |
| Handing values to another app | shared.set() shared.get() shared.age() shared.keys() |
| Sharing code between apps | a # @module file, then import |
| Fetching from the internet | http.get() http.post() http.put() http.patch() http.delete() http.request() |
| Picking a value out of a reply | re.search() re.match() re.matchall(), or json.load() |
| Home automation | mqtt.publish() mqtt.subscribe() |
| Reading a Modbus device | modbus.readHoldingRegisters() modbus.readInputRegisters() modbus.readCoils() modbus.readDiscreteInputs() |
| Making a noise | sound.play() sound.stop() sound.playing() sound.can() |
| Interrupting with an alert | notify() |
| Moving the rotation along | rotation.show() rotation.next() rotation.previous() rotation.pause() rotation.resume() rotation.close() |
| Turning the display on and off | display.power() display.is_on() |
| What the owner configured | settings.get() settings.set() settings.apply_case() |
| Working out what went wrong | log() |
| Which firmware is running | version() |
| Shipping the icons you draw | a # @icons line, then icon() |
| Naming the scripts yours needs | a # @requires line |
| Saying which clocks it runs on | # @needs and # @display lines |
| Getting it ready to share | the header checklist |
What needs an import. The drawing, time and number calls are plain functions. http, mqtt,
store, shared, settings, sensor, display, sound, rotation, timer and re
are ready to use. modbus, the general modules
json, string and math, and your own modules need an import line at the top of
the file.
Just enough Berry¶
Scripts are written in Berry, a small language that looks a bit like Python. You do not need to know it to start: copy the examples and change them. The full language guide is there when you want more.
Comments start with # and run to the end of the line.
Variables are made with var. They have no declared type. These are the kinds of value:
var count = 3 # integer: a whole number
var temp = 21.5 # real: a number with decimals
var name = "kitchen" # string: text
var ready = true # bool: true or false
var readings = [21, 23, 22] # list: several values in order
var spec = {"text": "Hi", "hold": true} # map: values stored under names
var empty = nil # nil: nothing yet, or no answer
You will also meet a range (0 .. width() - 1 below) and a function, which you can pass
around like any value. See Callbacks. type(v) answers "int", "real",
"string", "bool" or "nil". Lists and maps both answer "instance", so use
isinstance(v, list) or isinstance(v, map) for those.
Do not name a variable like a built-in call. A variable called text hides the call
text(), and the next text(…) in that method stops your app with type_error: 'string' value
is not callable. Pick another name: var label = "21°".
Numbers calculate with +, -, *, / and % (the remainder). Whole numbers divide
without a remainder: 7 / 2 is 3, and 50 / 100 is 0. Multiply before you divide
(50 * 8 / 100 is 4), or make one of the numbers a real (50 / 100.0 is 0.5).
Text is joined with +. Turn a number into text with str() first:
text(1, 6, "room " + name, 0xFFFFFF) # joining text: fine
text(1, 6, str(temp) + "°", 0xFFFFFF) # a number needs str() first
format() puts numbers into a pattern. %d is a whole number, %02d a whole number with at
least two digits, and %.1f a number with one decimal:
Decisions use if … elif … else, closed with end. Compare with ==, !=, <, >=
and so on. Combine with && (and) and || (or):
if temp >= 30
text(1, 6, "HOT", 0xFF0000)
elif temp >= 18 && temp < 30
text(1, 6, "ok", 0x00FF00)
else
text(1, 6, "cold", 0x0000FF)
end
For a choice between two values, condition ? a : b answers a when the condition is true and
b otherwise:
Repetition uses for over a range or a list, or while with a condition. Both close with
end:
Lists: [] makes an empty one, push() adds, remove(i) deletes by position, size()
counts, l[0] is the first entry and l[-1] the last:
var readings = []
readings.push(21)
readings.push(23)
text(1, 6, str(readings[-1]), 0xFFFFFF) # the newest one
Maps: read a value with find(). It answers nil when the key is missing. m["key"] raises
an error on a missing key, so use it only for keys you wrote yourself. Maps also pass settings to
notify() and effect():
var spec = {"text": "Hi", "hold": true}
var t = spec.find("text") # "Hi"
var pic = spec.find("icon") # nil: absent, not an error
var shown = spec.find("icon", "none") # "none": your own fallback
Modules add calls that are not built in. Load one with import at the top of the file,
above the class. What needs an import lists them:
Errors you expect are caught with try … except … end. The script keeps running, and the
lines after except decide what happens instead:
var label
try
label = str(data["current"]["temp"]) # [] raises an error on a missing key
except .. as err, msg
log(msg)
label = "--"
end
except .. catches every error. err and msg are its name and message. Without try, an error
stops your app and the display shows ERR:.
Every if, for, while, def and class needs its own end. When saving reports a
syntax_error, look for a missing end first. The line the error names can be further down.
Callbacks¶
Some calls take a function and call it later: http.get() when the answer arrives,
timer.after() when the time is up, mqtt.subscribe() for every message. Such a function is a
callback. There are two ways to write one:
# short: / arguments -> one expression
timer.after(3000, / -> self.reset())
# long: def (arguments) ... end, for several lines
mqtt.subscribe("home/door", def (topic, payload)
self.door = payload
self.changed = true
end)
- A callback written inside a method of your app can use
self, your app, so it can change your app's members. - The short form holds one expression. Usually it hands everything on to a method of your app:
/ body, status -> self.on_body(body, status). - Write callbacks where you start something: in
setup(),loop()or a button handler. Never write them indraw().
Good to know¶
- Whole numbers divide without a remainder:
50 / 100is0. Multiply before you divide, or write100.0. - Do not name a variable like a built-in call. A variable called
texthidestext(), and the next call to it fails. Use another name, such aslabel. - Build lists, maps and text outside
draw(). One built indraw()is built again 40 times a second. Build it ininit(),loop()or a callback, and keep it in a member. - Drawing works only in
draw(). Atext()inloop()orsetup()shows nothing and raises no error. Keep the value in a member and paint it indraw(). - A
syntax_errorwhen you save usually means a missingend. The line it names can be further down.
Details¶
The lifecycle¶
The hooks are methods on your class. Only draw() is required. Add the others when you need
them.
| Method | When it is called | Can draw? |
|---|---|---|
init() |
once, when AWTRIX creates your app | no |
setup() |
once, right after the app loads, before the first frame | no |
loop() |
about once a second, whether or not your app is shown | no |
draw() |
every frame (~40/s) while your app is shown | yes |
on_show() |
your app has just appeared | no |
on_hide() |
your app has just left the display | no |
on_button(btn) |
a button was pressed while your app is shown. btn is "left", "select" or "right". Return true to take the press |
no |
on_button_event(btn, event) |
a press, hold or release of a button your app took. See Button events | no |
should_show() |
the rotation moves on to your app. Return false to skip this turn. See Skip a turn |
no |
duration() |
your app appears. Return milliseconds to set how long it stays. See Stay longer | no |
init()orsetup()? Give members their starting values ininit(). Do first fetches and logging insetup(). Your store is ready in both.- After a reboot, the apps start their first
loop()a couple of seconds apart, so they do not all fetch in the same second. width(),height()and the text measurements work in every hook.- A misspelled built-in call (
clesr()) shows up when you save, as asyntax_error. Your own methods are looked up when they are called, soself.helper()may use a method defined further down the class. - Only the app shown is asked about a button. If
on_button()raises an error, the press goes through as usual, so a broken app cannot lock the buttons. - The class name is yours. Two scripts may both use
class App. should_show(),duration()and the header lines that keep an app out of the rotation are in Several apps together.
The editor¶
Built-in calls are shown in their own color, so a misspelled pixel stays plain. Ctrl-.
lists the calls, Ctrl-S saves, Ctrl-/ comments lines in or out, and Tab and
Shift-Tab indent a selection. The status line shows the cursor position and the size of the
script. Every button of the Scripts tab is in The web UI.
Related¶
- Tutorial 1: Draw something: the first of three tutorials that build one app, followed by recipes and Keeping scripts small
- Build an app with AI: this API as a prompt for a chatbot
- Icons: what
icon()can draw, and how to upload more - App & notification payload: the format
notify(),effect()and the charts share - Pushed apps: the simpler way to show your own content, when something outside AWTRIX sends it
- Modbus in scripts: read an energy meter or inverter over Modbus TCP
- MQTT: AWTRIX's own topics
- Limits: every limit a script runs under, in one table