Skip to content

Tutorial 1: Draw something

This is the first of three tutorials that build one app step by step. At the end of the third, your display shows the chance of rain for the next sixteen hours. The data comes from a real weather service, and you set the location in the web UI.

In this first tutorial you draw a picture.

Tutorial What it adds
Tutorial 1: Draw something pixels on the display, from numbers you type in yourself
Tutorial 2: Give it a memory state, a clock, and a setting the user can change
Tutorial 3: Feed it real data a real forecast over the network

You need an AWTRIX on your network and a browser.


Make the script

Open the web UI, go to the Scripts tab, and create a script called Rain.

Paste this in and press Save, or Ctrl-S:

# @name Rain
# @desc Chance of rain, hour by hour

class Rain
  def draw()
    clear()
    text(1, 6, "rain", 0x0088FF)
  end
end

return Rain()

What the script shows on the display

Your app is now in the rotation, the list of apps that take turns on the display. Wait a few seconds until it comes up, or press the right button on the device to skip to it. The display shows rain in blue. The 1 and the 6 in text() are its position on the display, explained below.

That is a complete app. Three things make it one:

It is a class. Everything the app owns lives inside it. Two scripts can both use class Rain without problems, because each script is kept separate.

It has a draw(). This is the only method an app must have. AWTRIX calls it about forty times a second while your app is the one shown, and whatever it paints is the frame.

It ends with return Rain(). That hands AWTRIX the instance to run.

The editor knows the API

Built-in calls are highlighted in their own color, and Ctrl-. completes them. A misspelled pixel stays plain, so you see it before you save. The list always matches the firmware on your device.

When it goes wrong

Nothing you do here can harm the device. A broken script breaks only itself: the display shows ERR: in red when its turn comes, every other app keeps running, and saving the script again clears it. The message is waiting next to your script in the Scripts tab.

These are the three errors you will meet most often, with the exact words AWTRIX uses:

Message What happened
script must end with 'return YourApp()' the last line is missing
no draw() method the class has no draw(), so there is nothing to paint
syntax_error: unexpected token … usually a missing end

Berry closes every if, for, while, def and class with end. A missing end is the most common mistake. The line number in the message is often after the line with the mistake, sometimes several lines after. Look upwards from it.

Errors that only happen while the app runs are shown the same way. "x" + 5 raises type_error: unsupported operand type(s) for +: 'string' and 'int', because Berry does not join a number to a string for you. Write "x" + str(5).


How the display is addressed

The display is a grid of pixels, 52 wide and 16 tall. x counts the columns from 0 on the left. y counts the rows from 0 at the top, so a larger y is further down.

Light one pixel in each corner. The two pixel() lines go inside draw(), next to the text() call you already have:

# @name Rain
# @desc Chance of rain, hour by hour

class Rain
  def draw()
    clear()
    text(1, 6, "rain", 0x0088FF)
    pixel(0, 0, 0xFF0000)                        # top left
    pixel(width() - 1, height() - 1, 0x00FF00)   # bottom right
  end
end

return Rain()

What the script shows on the display

The red pixel is (0, 0). The green one is (51, 15).

In text(), y is the baseline, not the top. The baseline is the line the letters stand on. text(1, 6, …) puts it on row 6, tinted in the picture. The letters sit right above it, on rows 1 to 5. x = 1 is the column where the first letter starts. A second line goes 8 rows lower, at y = 14. Other fonts fill other rows: Where text sits lists them.

Use width() and height() instead of writing 52 and 16. Other clocks have a different display size, and a script that asks adapts to them automatically.

Anything you draw outside the display is cut off. Drawing at x = 500 is not an error. It just does not appear, so a chart that is too long cannot crash your app.

How the display works explains the grid with more pictures.


Colors are just numbers

A color is a single number. What a color picker calls #0088FF is 0x0088FF here.

These three lines paint exactly the same blue, three different ways:

    text(1, 6, "rain", 0x0088FF)            # written directly
    text(1, 6, "rain", rgb(0, 136, 255))    # from channels, each 0 to 255
    text(1, 6, "rain", hsv(208, 100, 100))  # from hue 0 to 360, sat and value 0 to 100

hsv() is useful when the color should follow a value. We use it later on this page.

The LEDs are very bright in a dark room. 0xFFFFFF is fine for a few letters. For larger areas, something like 0x202020 is enough. Strong colors at medium brightness are easier to read than the same color at full brightness.


Center the text properly

A fixed start position stops working when the text changes length. Measure the text instead:

# @name Rain
# @desc Chance of rain, hour by hour

class Rain
  def draw()
    clear()
    var s = "rain"
    text((width() - text_ink_width(s)) / 2, 6, s, 0x0088FF)
  end
end

return Rain()

What the script shows on the display

text_ink_width() measures only the lit pixels, so use it to center text. text_width() also counts the small gap after the last letter, so use it when you put two texts side by side.


Now the bars

The app should show sixteen numbers, one per hour, each a percentage.

We do not have real numbers yet, so type some in. The code stays the same when the real numbers arrive in tutorial 3.

# @name Rain
# @desc Chance of rain, hour by hour

class Rain
  var hours

  def init()
    self.hours = [0, 0, 5, 20, 45, 70, 90, 60, 30, 10, 0, 0, 15, 40, 25, 5]
  end

  def draw()
    clear()
    var h = height()
    var w = width() / size(self.hours)
    for i : 0 .. size(self.hours) - 1
      var v = self.hours[i]
      var bar = v * h / 100
      if bar > 0
        rect_fill(i * w, h - bar, w, bar, 0x0088FF)
      end
    end
  end
end

return Rain()

What the script shows on the display

Sixteen bars side by side. w is the width of one bar: the display width divided by the number of bars. On the 52 pixel display each bar is three pixels wide. The four columns left over on the right stay dark.

This code has three new parts.

var hours and init(). A value the app needs to remember lives in a member (a variable that belongs to the app). You declare it with var at the top of the class and give it a value in init(), which runs once when the app starts. Members keep their value from one frame to the next.

Declare every member, and give it a value

Berry lets you assign to a member you never declared, so self.total = 0 somewhere works. Reading a member you never declared does not. It raises attribute_error: the 'Rain' object has no attribute 'total' and the display shows ERR:. A var line at the top of the class and a value in init() prevent this.

The loop. for i : 0 .. size(self.hours) - 1 goes through every position in the list. Berry writes ranges with .., and every block closes with end.

The arithmetic. v * h / 100 scales a percentage into pixels. Multiply first, then divide. Two whole numbers divided in Berry give a whole number, so v / 100 * h would turn every bar under 100% into zero.

The if bar > 0 check makes an hour with zero percent draw nothing at all. So you can see the difference between "no rain" and "a little rain".


Let the color carry the meaning

A 52 × 16 display has no room for a legend, so color has to show the meaning.

    for i : 0 .. size(self.hours) - 1
      var v = self.hours[i]
      var bar = v * h / 100
      if bar > 0
        rect_fill(i * w, h - bar, w, bar, hsv(208, 100, clamp(v, 25, 90)))
      end
    end

hsv(208, 100, v) keeps the color blue and lets the brightness follow the percentage. A likely hour is bright, an unlikely one is dim. clamp(v, 25, 90) keeps the dimmest bar visible and the brightest one comfortable to look at.

The bar height already shows the number. Brightness shows it a second time, so you can read the chart from across the room.


The finished script

# @name Rain
# @desc Chance of rain, hour by hour
# @version 1.0

class Rain
  var hours

  def init()
    self.hours = [0, 0, 5, 20, 45, 70, 90, 60, 30, 10, 0, 0, 15, 40, 25, 5]
  end

  def draw()
    clear()
    var h = height()
    var w = width() / size(self.hours)
    for i : 0 .. size(self.hours) - 1
      var v = self.hours[i]
      var bar = v * h / 100
      if bar > 0
        rect_fill(i * w, h - bar, w, bar, hsv(208, 100, clamp(v, 25, 90)))
      end
    end
  end
end

return Rain()

What the script shows on the display

Change a number in the list and save. The display changes immediately. Try it a few times: you will work this way in the next two tutorials.


What you learned

  • An app is a class with a draw() and a final return YourClass().
  • draw() runs about forty times a second and paints one frame from what the app already knows.
  • x counts the columns from the left, y the rows from the top. In text(), y is the baseline the letters stand on.
  • width() and height() beat hardcoded numbers.
  • Colors are integers, and hsv() is how you make one follow a value.
  • Members declared with var and set in init() are how an app remembers anything.

Next

Tutorial 2: Give it a memory lets the app change the sixteen numbers by itself. They change on a timer, the app keeps its last values after a reboot, and the user can pick the color in the web UI without editing code.