MicroFastApi – A Micro Adventure in Python

·

In the previous post, I covered the very basic implementation of a web API structure using python decorators. But, it was just the beginning of a journey I have started. I decided to challenge myself and gradually develop this library into a real one!

Accordingly, I will write up the process of design and implementation of features for it.

MicroPython Threading

First of all, we have to implement a function running on a separate thread inside the MicroFastApi class to receive HTTP requests and send back responses in the most efficient way possible. However it’s an experimental module, fortunately, MicroPython has a low-level threading module _thread available which saves us a lot of headaches.

So let’s briefly take a look at a simple example of running a thread in micropython. To create a thread we should call the function start_new_thread() in the _thread module as follows:

import _thread
import time

def _http_handler():
    counter=0
    while True:        
        print(counter)
        counter+=1
        time.sleep(0.1) # 100 ms

threadobject = _thread.start_new_thread(_http_handler,())

Note that the time module provides the function sleep() which receives a floating point as the number of seconds to sleep.

This is the simplest use case that we are gonna embed inside the MicroFastApi class. Other than that, since the routes dictionary object is accessed in both the main and the dispatcher thread, we are gonna create a lock object using the allocate_lock() function and use it to make sure no race condition is gonna happen. Then we can either use acquire() and release() functions of the lock object explicitly or use python with statement.

with self.lock:
    self.routes[route] = decorated_get_func

So the class will become like this:

import functools #pip install micropython-functools
import _thread
import time

class MicroFastApi:

    def __init__(self, ip, mac,):
        self.routes = {}
        self.address = ip
        self.mac = mac
        self.lock = _thread.allocate_lock()
        self.thread = _thread.start_new_thread(self._http_handler, ())

    def __str__(self):
        server_info = f"""
            device ip = {self.address}
            device mac = {self.mac}
            open the URL provided below in your browser \n http://{self.address}/"""
        return server_info

    def get(self,route:str):
        def decorate_get_api(func):
            @functools.wraps(func)
            def decorated_get_func(*args, **kwargs):
                # pre-processing 
                ret_val = func(*args, **kwargs)

                return ret_val
            with self.lock:
                self.routes[route] = decorated_get_func
            return decorated_get_func
        return decorate_get_api

    def _http_handler(self):

        while True:

            with self.lock: # acquire the lock and release it when done.
                for route in self.routes:
                    print(f"route {route} is mapped to {self.routes[route].__name__}. result:"+ self.routes[route]())

            time.sleep(1)

Handling API Requests

Now the general idea is listening to a port specified in the constructor, receiving requests in the thread loop, parsing the incoming HTTP requests, calling the corresponding routine defined the in the routes dictionary, and eventually returning the JSON result.

The most basic way is listening to a TCP connection using the micrpython implementation of the python low-level networking module named socket, and then parsing the receiving packets.

To do so, we modify the construction function to receive the port number we intend the server to listen to. Next, we create a socket object and bind it to the specified port in the constructor.

addr = socket.getaddrinfo("0.0.0.0", self.port)[0][-1]
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)        
self.socket.bind(addr)
self.socket.listen(1)

Note that passing the “0.0.0.0” IP address to the bind() function means that the server can receive connections from any possible IPv4 addresses. Also, we passed the socket.AF_INET and socket.SOCK_STREAM to the socket() factory function respectively to create an IPV4 socket with a TPC protocol connection. The integer value passed to the listen() function, defines the concurrent connections capacity of the socket.

After setting up the socket connection, now we can accept() connections in a loop inside the http_handler() function we had before.

while True:
    try:
        client_socket, client_addr = self.socket.accept()                
        print('client connected from', client_addr)
        testjson = {'userId':'1'}
        response = json.dumps(testjson)
        client_socket.send('HTTP/1.0 200 OK\r\nContent-type: application/json\r\n\r\n')
        client_socket.send(response)
        client_socket.close()
        print("client closed!")
    except OSError as e:
        print(e)

The accept() function returns a socket to the client along with its address which should be used to send back the results to the client. In this example, we created a dictionary and converted it to a JSON string, and sent the result through the client socket. If we run the project and click on the server URL provided in the terminal, we can expect the following JSON result in the browser:

API server running on http://10.0.0.67:80
client connected from ('10.0.0.30', 55780)
client closed!
{"userId": "1"}

This solution is quite clear, but since the MicroPython _thread module is in experimental development, it’s not reliable. If you run the code above, the socket inside the thread function works just once and after that, it will freeze! I was struggling with this issue for several hours today and eventually found out there is an open issue in MicroPython GitHub for this problem. Let’s cross our fingers to see a bug fix in a near future.

To be able to continue this project, I commented out the threading-related code and added a function called Run(). Within that, called the _http_handler() function which contains the main loop.

Now, we have a very basic working structure for the providing WebApi on the RasbperyPi Pico, let’s get to next step, which is developing a simple HTTP request parser to be used for our goal.

A Simple HTTP Request Parser

First of all we should take a look at the anatomy of a HTTP request. Each request has three main sections as follows.

  • Request LineGenerally, this line is made of HTTP Method + URI + HTTP Protocol. HTTP Method defines the type of a request, whether it is a GETPOSTPUT, or DELETE request. After the type, it contains the URI of the request, which corresoponds to the route we have defined in the server. And Finaly, the HTTP protocol version which can be one of the HTTP/1.1HTTP/2 , or HTTP/3. For example the request line to get the root route would be like:GET / HTTP/1.1\r\n

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *