Skip to content
Dave edited this page Oct 20, 2023 · 24 revisions

Welcome to the Thimble wiki!

Here you will find a step by step guide to using the Thimble web framework to add a REST API to your MicroPython projects.

Getting Started

You'll need a microcontroller running MicroPython and you'll need to have it configured to access your wifi network. There are plenty of examples on the web covering how to do that. You can also have a look at the boot.py I use on my ESP32.

Once you're booting and connected to wifi, you'll need a copy of thimble.py. It can go in the /lib directory or directly in the root of your flash storage.

Now you're ready to start using Thimble.

Creating a Simple Example

Hello World! It's everybody's favorite. Here's how you do it with Thimble:

from thimble import Thimble

app = Thimble()

@app.route('/world')
def say_hello(req):
    return 'Hello World!'

app.run()

Save the code above as main.py and reboot your microcontroller. You should see something like this when Thimble starts up:

Listening on 0.0.0.0:80

0.0.0.0 represents the default interface and :80 indicates the port.

All you have to do is point your web browser to the IP address of your microcontroller /world. In your browser, you should see Hello World! displayed. Or if curl is your thing, you can do this:

curl http://IP.AD.DR.ESS/world
Hello World!

Creating Additional Routes

Thimble works on the concept of linking functions to routes. If you've ever written an app with Python Flask or NodeJS Express, you should be familiar with the concept. If not, don't worry, it's not hard.

You can think of a route as a url. In the Hello World example, there's only one route for /world. Let's add a route to say "Hello Cleveland" in addition to "Hello World".

The code in main.py becomes this:

from thimble import Thimble

app = Thimble()

@app.route('/world')
def say_hello(req):
    return 'Hello World!'

@app.route('/cleveland')
def say_hello_cleveland(req):
    return 'Hello Cleveland!'

app.run()

Notice what was added:

@app.route('/cleveland')
def say_hello_cleveland(req):
    return 'Hello Cleveland!'

Let's take a closer look, line by line, to understand what's going on.

At the top, we have @app.route('/cleveland'). The technical term for this is "function decorator". Just think of it as the way to tell Thimble the URL path that will trigger your function. There's a little more to it, but we'll get to that a little later on.

The next line, def say_hello_cleveland(req): , defines the function. You're probably already familiar with this if you've been using Python for more than five minutes.

What's important to note here is the parameter req that gets passed to the function. This is how Thimble communicates the details of the HTTP request to your function. Like the example above, you don't have to use it, but you do have to include it. Otherwise, you're going to get an error.

The last line, return 'Hello Cleveland!' is the body of the response to the HTTP request. Thimble takes care of the HTTP status code and headers, but it's up to your function to supply the body of the reply. In this case, it's just a simple "Hello Cleveland!"

Try it out with a browser or REST API client.

Controlling GPIO

I'm going to guess you didn't by a microcontroller to send "Hello World" to a web browser. You bought it to read sensors and control LEDs and myriad other use cases. Thimble can be used to add a REST API to that project.

from machine import Pin
from thimble import Thimble

gpio_2 = Pin(2, Pin.OUT)

app = Thimble()

@app.route('/gpio/2', methods=['GET'])
def get_gpio_2(req):
    return gpio_2.value()

@app.route('/gpio/2', methods=['PUT'])
def set_gpio_2(req):
    if (body == '1' or body == 'on'):
        gpio_2.on()
    elif (body == '0' or body == 'off'):
        gpio_2.off()
    return gpio_2.value()

app.run(debug=True)

The code above lets you control one of the digital GPIO pins. Several microcontrollers have a built-in LED attached to GPIO2, so you should be able to get visual confirmation that things are working. If you don't have a built-in LED, you can still see the results in the REST API client.

Notice how the the routes now have a methods parameter and last line changes from app.run() to app.run(debug=True)

The methods parameter tells Thimble what HTTP verbs to consider for when looking for routes. The default for routes is to use methods=['GET'], which is how the Hello World example worked without explicitly specifying it. But, you can also use POST, PUT, DELETE, or any other HTTP request method. However, in REST APIs, GET is generally used for reading values while PUT is for changing, so that's what is used here.

Notice how the route for /gpio/2 GET isn't much different than the Hello World example. The only major change is return gpio_2.value() to use the current value of GPIO 2 as the body of the response.

Things get a little more complex with the /gpio/2 PUT route. Here we need to examine the req parameter to see what was passed in the body of the request. If it was a 1 or the word on, the GPIO value is set to on (logic high). A 0 or off will set the value to off (logic low.)

Because the LED is an active low device, the visual representation of on and off is actually reversed.

Also notice how the function for the /gpio/2 PUT route also returns that current state of the GPIO using gpio_2.value(). You don't have to do this, but it's good confirmation that the operation was successful.

Finally, take another look at the app.run(debug=True) line and notice how the console output from your device is suddenly more verbose. You'll see the IP address of the client machine, the HTTP request, and all the HTTP headers sent along. These are all values you can use in your function if you want.

Returning Multiple Values with JSON

Take a look at the route definition below.

@app.route('/adc/0')
def get_adc_0(req):
    return json.dumps({ "value": int(adc_0.read_uv() / 1000), "units": "millivolts" }), 200, 'application/json'

The function now returns three things: the JSON output from json.dumps(), 200 which is the HTTP return code, and 'application/json' which is the media type. If you're familiar with Flask, this is similar to the way you can return these values from your Flask @app.route functions.

Unlike Flask, Thimble does not handle the JSON output for you, but you can see from the example above that it's easy to do with json/dumps. Just remember to include the import line, like this:

import json

Outputting JSON is easy and so is decoding JSON from input. The trouble is validating that input to avoid crashing your program.

Dealing With Bad User Input

Here's the code that controls GPIO 2 rewritten to accept JSON-formatted requests and return JSON in the reply:

@app.route('/gpio/2', methods=['PUT'])
def set_gpio_2(req):
    body = None
    try:
        body = json.loads(req['body'])
    except:
        return json.dumps({"Error": "Invalid JSON"}), 400
    else:
        if (body == '1' or body == 'on'):
            gpio_2.on()
        elif (body == '0' or body == 'off'):
            gpio_2.off()

    return json.dumps({ "value": gpio_2.value()}), 200, 'application/json'

Notice the try / except / else code that was added to the function. This is needed because json.loads will throw an exception if the input is not formatted properly.

Also notice how encountering an exception will return two values.

return json.dumps({"Error": "Invalid JSON"}), 400

What all this does is to return an HTTP "400 Bad Request" error whenever json.loads throws an exception. This informs the client that there was a problem and it was probably due to the input they provided.

There are other ways clients can send invalid requests, and some of these are handled by Thimble internally. For example, asking for a route that does not exist results in a "404 Not Found", while an unhandled exception in a route function will result in a "500 Internal Server Error" being sent.

Other Types of Errors

Add the following routes to one of the examples above.

@app.route('/error/1')
def simulate_error1(req):
    raise Exception('Oops!')

@app.route('/error/2')
def simulate_error2():  # Missing req argument
    return 'Say something.'

@app.route('/error/3')
def simulate_error_3():  # Explicitly returning an error
    return 'Error', 500

Point your web browser (or even better, a REST API client) to any of the URLs defined in these new routes and see what happens. You should see a "500 Internal Server Error" in each case.

Running in an Asynchronous Loop

Thimble always runs asynchronously, creating it's own loop object. The app.run() can take a parameter named loop that allows you to pass in a loop that you create with your own code.

Here's the Hello World example configured that way:

from thimble import Thimble
from uasyncio import get_event_loop

app = Thimble(default_content_type='application/json')

@app.route('/')
def say_hello(req):
    return 'Hello World!'

@app.route('/cleveland')
def say_hello_cleveland(req):
    return 'Hello Cleveland!'

loop = get_event_loop()
app.run(loop=loop)
loop.run_forever()

Notice the uasyncio import statement at the top and three lines of code at the bottom. Let's look at those lines in detail.

  • First, is the loop = get_event_loop(). This lets you access the event loop using a loop variable that you create.
  • Next is the call to Thimble's app.run(loop=loop). Thimble will insert itself into the loop object you passed rather than creating its own.
  • Finally, there's loop.run_forever() that keeps the program from exiting.

You also have the option of using async functions in your routes. Like this:

@app.route('/gpio/2', methods=['POST'])
async def set_gpio_2(req):
    gpio_2.on()
    return gpio_2.value()

You can also use plain old non-async routes, like in the Hello World example. Or you can mix and match the two. Like this:

@app.route('/gpio/2', methods=['GET'])
def set_gpio_2(req):
    return gpio_2.value()

@app.route('/gpio/2', methods=['POST'])
async def set_gpio_2(req):
    gpio_2.on()
    return gpio_2.value()

The examples above are rather trivial. Normally, it shouldn't be necessary to use async for route functions, unless they take more than a second or so to return a value, and GPIO read/write operations are quick to execute.

Next Steps

If you made it this far, you should have enough familiarity with the Thimble web framework to add a REST API to just about any project you can dream up. Grab a microcontroller and a few sensors and see what you can make.

Here's a few ideas:

  • A BME280 or DHT22 sensor for an API enabled weather station.
  • A strip of NeoPixel LEDs for an API controlled color changing light.
  • A light dependent resistor as part of a voltage divider attached to an ADC input for light readings via web browser.

Once you've added a REST API to your project, you can integrate it with a home automation system like Home Assistant. Or if you're good with HTML and JavaScript, you can create a custom web interface.

There are more examples in the repository

Whatever you do, have fun with it!