Baxter Builds

How to Shorten Your Micropython LVGL Code

August 5, 2023 by Baxter

A custom LVGL widget

Often when you are working on a project, you will find that parts of the code pretty much repeat over and over, and life would be so much easier, if you could just combine all that code into one neat little package.

Normally this is not that hard, you just use a function, but when you start mixing in LVGL widgets it can be hard to keep in control of everything and simplify at the same time. That is why today I will show you an example on how to simplify your LVGL code.

Setting Up a Micropython Screen

Obviously, if you are creating a program that uses LVGL, you have to have access to a screen.

Because there are so many different screens out there, I can’t add support for all of them, so to make this example work, you will need to add your screen’s setup code to the beginning of this example.

There is a little more information on this process at How to get a LVGL Micropython Screen to Work if you aren’t familiar on how it’s done.

here is the example code.

import lvgl

def Dropdown(name,final_size,scr):
    container = lvgl.obj(scr)
    container.set_size(300,40)
    container.set_pos(5,5)
    container.set_style_radius(15,0)
    container.set_style_pad_all(0,0)
    container.set_scrollbar_mode(lvgl.SCROLLBAR_MODE.OFF)

    label1 = lvgl.label(container)
    label1.set_text(name)
    label1.set_pos(10,10)

    sw1 = lvgl.switch(container)
    sw1.set_size(60,30)
    sw1.set_pos(230,4)

    line = lvgl.line(container)
    line.set_pos(0,40)
    points = [{"x":5,"y":0},{"x":290,"y":0}]
    line.set_points(points,2)
    line.set_style_line_color(lvgl.color_hex(0xDDDDDD),0)


    down = lvgl.anim_t()
    down.init()
    down.set_var(container)
    down.set_time(300)
    down.set_values(40,final_size)

    down.set_custom_exec_cb(lambda not_used,value : container.set_height( value))

    up = lvgl.anim_t()
    up.init()
    up.set_var(container)
    up.set_time( 300 )
    up.set_values( final_size, 40 )

    up.set_custom_exec_cb( lambda not_used, value : container.set_height( value ))
    
    sandbox = lvgl.obj(container)
    sandbox.set_pos(5,35)
    sandbox.add_flag(lvgl.obj.FLAG.HIDDEN)
    sandbox.set_size(290,final_size-40)
    sandbox.set_style_border_side(lvgl.BORDER_SIDE.NONE,0)



    def switch_clicked(data): 
        switch = data.get_target()
        state = switch.has_state(lvgl.STATE.CHECKED)
        print(state)
        if state:
            down.start()
            sandbox.clear_flag(lvgl.obj.FLAG.HIDDEN)
            line.clear_flag(lvgl.obj.FLAG.HIDDEN)
        else:
            up.start()
            sandbox.add_flag(lvgl.obj.FLAG.HIDDEN)
            line.add_flag(lvgl.obj.FLAG.HIDDEN)
                 
    sw1.add_event_cb(switch_clicked,lvgl.EVENT.CLICKED,None)   
    return (container,sandbox)
   


Container,Sandbox = Dropdown("Something To Control",150,lvgl.scr_act())
Container.set_pos(10,10)


Some_Text = lvgl.label(Sandbox)
Some_Text.set_text("Controls for it")
Some_Text.center()

Understanding This LVGL Code

Ok, now that you have seen all the code, let’s step through each section, so you can understand the basic tricks to making you code simpler.

Using a Function

As mentioned before, Functions are a great start to making code smaller, so our example code begins by creating one. We will call this function Dropdown and make it take three arguments.

def Dropdown(name,final_size,scr):

A Container

The next big trick is to create a LVGL object. Later when we start creating more LVGL objects, we will always put them inside of this first object.

This will dramatically simplify group actions, like if you want to move every object 10 pixels to the left, all you have to do is move the container 10 pixels, and everything inside of it will move ten pixels as well.

    container = lvgl.obj(scr)

After that, we will set some settings like the size of the the container, it’s position, and how rounded the corners should be.

    container.set_size(300,40)
    container.set_pos(5,5)
    container.set_style_radius(15,0)

LVGL’s default padding is just a little to much for the size of my screen, so I removed it like this.

    container.set_style_pad_all(0,0)

Next we can remove the scrollbar. Because we will use animations later, there will be a small amount of time where the content is to large for our container. LVGL by default will try to add a scrollbar when that happens, which totally ruins how everything looks for a few seconds.

    container.set_scrollbar_mode(lvgl.SCROLLBAR_MODE.OFF)

Some Content

After that we will add a little content inside of our main object. This is all pretty standard stuff, so I will not bore you with the details.

    label1 = lvgl.label(container)
    label1.set_text(name)
    label1.set_pos(10,10)

    sw1 = lvgl.switch(container)
    sw1.set_size(60,30)
    sw1.set_pos(230,4)

    line = lvgl.line(container)
    line.set_pos(0,40)
    points = [{"x":5,"y":0},{"x":290,"y":0}]
    line.set_points(points,2)
    line.set_style_line_color(lvgl.color_hex(0xDDDDDD),0)

Animations

We are going to use animations to smoothly change the size of are custom widget. Animations are complex and really out of the scope of this post, so I will not explain them here. If you are not very familiar with animations, you might be interested in seeing some simple Animation Examples.

    down = lvgl.anim_t()
    down.init()
    down.set_var(container)
    down.set_time(300)
    down.set_values(40,final_size)

    down.set_custom_exec_cb(lambda not_used,value : container.set_height( value))

    up = lvgl.anim_t()
    up.init()
    up.set_var(container)
    up.set_time( 300 )
    up.set_values( final_size, 40 )
import lvgl
    up.set_custom_exec_cb( lambda not_used, value : container.set_height( value ))

Another Container

Next we are going to create another container. We will use it later as a sandbox, just a little area were you can add anything you want to it.

    sandbox = lvgl.obj(container)
    sandbox.set_pos(5,35)
    sandbox.add_flag(lvgl.obj.FLAG.HIDDEN)
    sandbox.set_size(290,final_size-40)
    sandbox.set_style_border_side(lvgl.BORDER_SIDE.NONE,0)

Creating Some Action

Up to this point, we have created a bunch of basic widgets. Now what we are going to do is add an event handler. We kind of skipped over it, but earlier we created a switch, what we want to happen now is when ever that switch is turned on the hole container get larger and an extra section pops out.

Whenever we need to respond to some action in LVGL, we use handlers, so lets create one.

    def switch_clicked(data): 

The first thing we do in this handler function, will be to check if the switch is on or off.

        switch = data.get_target()
        state = switch.has_state(lvgl.STATE.CHECKED)

Then we will print that answer to the terminal.

        print(state)

After that, we will create an if statement to respond to the current state.

        if state:
            down.start()
            sandbox.clear_flag(lvgl.obj.FLAG.HIDDEN)
            line.clear_flag(lvgl.obj.FLAG.HIDDEN)
        else:
            up.start()
            sandbox.add_flag(lvgl.obj.FLAG.HIDDEN)
            line.add_flag(lvgl.obj.FLAG.HIDDEN)

While the if statement is pretty simple, the code inside of it is less so. Here is what it does.

First if the switch has been turned on, it will start the down animation we created earlier. All that animations does is change the height of our sandbox container to 150px.

            down.start()

Then, it makes all of the stuff inside that container not hidden aka visible.

            sandbox.clear_flag(lvgl.obj.FLAG.HIDDEN)
            line.clear_flag(lvgl.obj.FLAG.HIDDEN)

And if the Switch was turned off, it does the opposite. It shrinks the container to 40 pixels tall and hides the stuff inside of the container.

            up.start()
            sandbox.add_flag(lvgl.obj.FLAG.HIDDEN)
            line.add_flag(lvgl.obj.FLAG.HIDDEN)

Convenience

This last line of code is really short, but also really important.

return (container,sandbox)

What it does is return both the main container and the smaller one inside it. Take note of the parenthesizes and coma, they condense multiple items into one item, so we can use it as the return value of our function.

Passing the main container is useful, because it easily allows us to move everything around the screen, and passing the second container is useful ,because it allows us to quickly edit what we want.

Using Our Function

Now that we have created our function lets use it. Because this function returns multiple variables when it ends, we will use a trick called unpacking to get all of them available as quickly as possible.

Container,Sandbox = Dropdown("Something To Control",150,lvgl.scr_act())

Now we have two variables: Container and Sandbox. We will start out by moving the Container, when we move it, everything we have created will move with it as well.

Container.set_pos(10,10)

We made the Sandbox variable to be a little area that will occasionally pop out. If we create something inside of it, when Sandbox appears so will what ever we created.

Lets create a basic label and center it inside of our Sandbox.

Some_Text = lvgl.label(Sandbox)
Some_Text.set_text("Controls for it")
Some_Text.center()

What Next?

This example creates a relatively unique widget, so I would suggest first of all just running the code and seeing what it creates. Then start playing around with it. While this example is a bit long, that is mostly because I went a bit overboard with the animations and styles. The basic principles here are actually very simple.

  1. First, you put your code inside of a function.
  2. Second, you create an LVGL object, which all further objects are put inside of.
  3. Third, you return all important LVGL objects from the function using tuples and unpacking.

If you found this tutorial interesting you might like to see a complete LVGL Timer App Example.

Filed Under: Micropython

FreeRTOS Mutex Example

May 19, 2023 by Baxter

the esp32 that I run the code on

Mutexes can be one of the harder parts of FreeRTOS to learn. They are slower than semaphores, but don’t seem to do anything special. I am sure you have probably looked up what the difference is and heard about things like Priority inversion and Priority inheritance.

But it is always nice when working through computer theory, to have a simple example that really shows what you are trying to learn, so I have created the following code that shows the basics of how and why you use a mutex in FreeRTOS.

Here is the code

#define LED 16

SemaphoreHandle_t GuardSerial;


void low_priority(void* args) {
  while (true) {
    xSemaphoreTake(GuardSerial, portMAX_DELAY);
    for (int i = 0; i < 200 ; i++) {
      Serial.print("L-");
    }
    Serial.println();
    xSemaphoreGive(GuardSerial);
    delay(500);
  }
}


void medium_priority(void* args) {
  pinMode(LED, OUTPUT);
  while (true) {
    for (int i = 0; i < 10000 ; i++) {
      int k = sin(random(0, 1));
      k += random(0, 20);
      k -= random(0, 10);
      k -= cos(random(0, 1));
      k *= tan(random(0, 1));
      //just waist some time
    }
    digitalWrite(LED, !digitalRead(LED));
    delay(100);
  }
}

void high_priority(void* args) {
  while (true) {
    xSemaphoreTake(GuardSerial, portMAX_DELAY);
    Serial.print("*******high_priority task testing mutexes*******\n");
    xSemaphoreGive(GuardSerial);
    delay(500);
  }
}


void setup() {
  Serial.begin(9600);

  // GuardSerial = xSemaphoreCreateBinary();
  GuardSerial = xSemaphoreCreateMutex();

  xTaskCreate(low_priority, "LOW", 2048, NULL, 1, NULL);

  xTaskCreate(medium_priority, "MID1", 2048, NULL, 2, NULL);
  xTaskCreate(medium_priority, "MID2", 2048, NULL, 2, NULL);
  xTaskCreate(medium_priority, "MID3", 2048, NULL, 2, NULL);
  xTaskCreate(medium_priority, "MID4", 2048, NULL, 2, NULL);

  xTaskCreate(high_priority, "HIGH", 2048, NULL, 3, NULL);
}

void loop() {
}

Understanding this Code

Now that you have seen this example, lets go through each section and see how it works.

The LED

Part of this example is going to use an LED, so to start, we need to define which pin it is connected to.

#define LED 16

Mutex vs Semaphore

A mutex is just a binary semaphore with some added safety measures. Because of this, they are slightly slower than semaphores, but much more reliable (more on this later).

To be able to access a mutex, we will need to create a handler for it, and because mutexes are related to semaphores, we will use the standard semaphore handler like this.

SemaphoreHandle_t GuardSerial;

The Low Priority Task

This example uses three tasks, so lets start working on the first task. We will begin it like most task functions with an infinite loop and a delay.

void low_priority(void* args) {
  while (true) {

    delay(500);
  }
}

Then we will add in some code that prints “L-” 200 times to the Serial port.

void low_priority(void* args) {
  while (true) {
    for (int i = 0; i < 200 ; i++) {
      Serial.print("L-");
    }
    Serial.println();
    delay(500);
  }
}

For this example, we are using mutexes to guard the Serial port, so before we use the Serial port to print “L-” 200 times, we need to get permission from the mutex with the xSemaphoreTake() function.

That function requires two arguments the first one is the semaphore or mutex we want permission from and the second one is the maximum time we should wait for permission before giving up. portMAX_DELAY = forever

void low_priority(void* args) {
  while (true) {
    xSemaphoreTake(GuardSerial, portMAX_DELAY);
    for (int i = 0; i < 200 ; i++) {
      Serial.print("L-");
    }
    Serial.println();
    delay(500);
  }
}

And when we are done with the Serial port we need to release it, so the other tasks can use it with the xSemaphoreGive() function.

void low_priority(void* args) {
  while (true) {
    xSemaphoreTake(GuardSerial, portMAX_DELAY);
    for (int i = 0; i < 200 ; i++) {
      Serial.print("L-");
    }
    Serial.println();
    xSemaphoreGive(GuardSerial);
    delay(500);
  }
}

The Middle Task

This next task doesn’t actually use mutexes, instead its only job is to waist computer power and blink an LED. Later, we will use it to show why mutexes are better than binary semaphores.

void medium_priority(void* args) {
  pinMode(LED, OUTPUT);
  while (true) {
    for (int i = 0; i < 10000 ; i++) {
      int k = sin(random(0, 1));
      k += random(0, 20);
      k -= random(0, 10);
      k -= cos(random(0, 1));
      k *= tan(random(0, 1));
      //just waist some time
    }
    digitalWrite(LED, !digitalRead(LED));
    delay(100);
  }
}

The High Priority Task

This last task is the simplest of the bunch. All it does is get permission from the mutex, send data through the Serial port, release the mutex, and finally wait a while before doing it all over again.

void high_priority(void* args) {
  while (true) {
    xSemaphoreTake(GuardSerial, portMAX_DELAY);
    Serial.print("*******high_priority task testing mutexes*******\n");
    xSemaphoreGive(GuardSerial);
    delay(500);
  }
}

Wrapping it all Together

Up to this point, we have only created some task functions, now we need to turn them into real tasks. We will do all of this and a few other things in the setup() function. Here is what it starts out like.

void setup() {
  Serial.begin(9600);


}

The first thing we will do is create a new mutex and then update the semaphore handler we created earlier to hold it.

void setup() {
  Serial.begin(9600);

  // GuardSerial = xSemaphoreCreateBinary();
  GuardSerial = xSemaphoreCreateMutex();

}

Above you may have noticed that one line of code is commented out. If you uncommit it and commit out the line that comes after it, the program will use a binary semaphore instead of a mutex.

The next thing we will do is create the low priority task, and if you were wondering, we will create it with a priority of one.

void setup() {
  Serial.begin(9600);

  // GuardSerial = xSemaphoreCreateBinary();
  GuardSerial = xSemaphoreCreateMutex();

  xTaskCreate(low_priority, "LOW", 2048, NULL, 1, NULL);

}

After that, we will create four medium priority tasks. We need four of them to just make sure the computer is always running at least one on each core.

void setup() {
  Serial.begin(9600);

  // GuardSerial = xSemaphoreCreateBinary();
  GuardSerial = xSemaphoreCreateMutex();

  xTaskCreate(low_priority, "LOW", 2048, NULL, 1, NULL);

  xTaskCreate(medium_priority, "MID1", 2048, NULL, 2, NULL);
  xTaskCreate(medium_priority, "MID2", 2048, NULL, 2, NULL);
  xTaskCreate(medium_priority, "MID3", 2048, NULL, 2, NULL);
  xTaskCreate(medium_priority, "MID4", 2048, NULL, 2, NULL);
}

And lastly we will create the high priority task.

void setup() {
  Serial.begin(9600);

  // GuardSerial = xSemaphoreCreateBinary();
  GuardSerial = xSemaphoreCreateMutex();

  xTaskCreate(low_priority, "LOW", 2048, NULL, 1, NULL);

  xTaskCreate(medium_priority, "MID1", 2048, NULL, 2, NULL);
  xTaskCreate(medium_priority, "MID2", 2048, NULL, 2, NULL);
  xTaskCreate(medium_priority, "MID3", 2048, NULL, 2, NULL);
  xTaskCreate(medium_priority, "MID4", 2048, NULL, 2, NULL);

  xTaskCreate(high_priority, "HIGH", 2048, NULL, 3, NULL);
}

Why Use Mutexes

When we started, I said I would show you how and why to use a mutex. We have seen the how, now lets look at the why.

In this example at line 48, there is some code that is commented out.

// GuardSerial = xSemaphoreCreateBinary();
GuardSerial = xSemaphoreCreateMutex();

If you uncommit the first line and commit out the second line, the example will use a binary semaphore instead of a mutex.

Try running it, if you do you will see something a little strange happening, or more accurately you will see nothing happening in the Serial Monitor, but the led will keep blinking.

What this means is that the low and high priority tasks have been halted, and only the medium priority task is running which is not what we want. The high priority task should always be run first, and that is what mutexes are for, they guarantee that your high priority tasks get run as soon as possible.

Mutexes can do a lot but not every thing. There are still a few things that semaphores are better at, and if you want to learn a little more about them, you might like to see a semaphore example.

Filed Under: Arduino, FreeRTOS

FreeRTOS Task Notification Example

May 9, 2023 by Baxter

the esp32 board I use with "notifications" written above it

FreeRTOS has many conventional tools like semaphores and queues available to it, but it also has an extremely powerful unconventional tool as well, notifications.

Notifications were designed as a low level way to synchronize tasks. because of that, they are very fast and allow you to synchronize tasks in a lot of different ways.

There are two interfaces that you can use with notifications, so below I have prepared two simple examples about how to use each.

Here is the first example

TaskHandle_t address = NULL;


void send(void* arg){
 while(true){
  xTaskNotifyGive(address);
  delay(800);
 }
}

void receive(void* arg){
  while(true){
    ulTaskNotifyTake(true,portMAX_DELAY);
    Serial.println("GOT MESSAGE");
  }
}


void setup() {
  Serial.begin(115200);

  xTaskCreate(receive,"RECEIVE",2048,NULL,1,&address);
  xTaskCreate(send,"SEND",2048,NULL,1,NULL);
}


void loop() {
}

How It Works

This example uses the simpler interface which I prefer, so we will start with it. Why don’t we quickly go through it and see how it works.

The Address

One of the big differences between Notifications and Semaphores, Mutexes, etc. is that when you send a notification you have to tell it the address of the task you are sending it to.

This address is actually the handler of the task, so for now we need to create a variable to hold that address like this.

TaskHandle_t address = NULL;

Of course, we can’t actually set the address until we create the task, so we will have to wait for now.

The Sender Function

Next, we are going to create a function for the task that sends the notifications, and we will start it with an infinite loop and a delay.

void send(void* arg){
 while(true){

  delay(800);
 }
}

The actual line of code that send the notification is also pretty simple.

void send(void* arg){
 while(true){
  xTaskNotifyGive(address);
  delay(800);
 }
}

Receiving Notifications

The receiver task also starts out pretty simple.

void receive(void* arg){
  while(true){
 
  }
}

Then, we use the xTaskNotifyTake() function to halt the task until it receives a notification. This function takes two arguments, ClearCount and TicksToWait.

TicksToWait is pretty standard, Its just the maximum amount of time the function should wait for a notification, but ClearCount is a little more interesting.

First of all, here is what this command looks like when we add it in. I have also added a Serial.println() function to tell the user when it receives a notification.

void receive(void* arg){
  while(true){
    ulTaskNotifyTake(true,portMAX_DELAY);
    Serial.println("GOT MESSAGE");
  }
}

Now back to ClearCount, above you can see that I set it to true. Here is a quick table that shows you what ClearCount does.

FalseMakes the notification act like a counting semaphore, allows you to GIVE multiple times in a row and then TAKE the same number of GIVES.
TrueMakes the notification act like a binary semaphore, allows you to GIVE multiple times in a row and then TAKE only once

Order Matters

The last thing we need to do is turn those functions into tasks, and we will also need to update the address variable.

The order in which you create the tasks is extremly important. When I was making this code I made the mistake of creating the sender first, but the program kept crashing.

The reason the order matters is that, when you create the sender task it will immediately run, and if the receiver does not exist yet, when it tries to send a notification the whole computer will crash, so you have to create the receiver first.

void setup() {
  Serial.begin(115200);

  xTaskCreate(receive,"RECEIVE",2048,NULL,1,&address);
  xTaskCreate(send,"SEND",2048,NULL,1,NULL);
}

The Second Example

Besides the simple mechanisms above, notifications also have a slightly more complex way to send a message that also has a 32-bit number sent with it, so here is the next example.

TaskHandle_t receiver = NULL;


void send(void* arg){
 while(true){
  unsigned int val = random(0,10);
  Serial.printf("Sending Number: %i \n",val);
  xTaskNotify(receiver,val,eSetValueWithOverwrite);
  delay(800);
 }
}

void receive(void* arg){
  while(true){
    uint32_t msg = 0;
    xTaskNotifyWait(0,0,&msg,portMAX_DELAY);
    Serial.printf("Got Number: %i \n",msg);
  }
}


void setup() {
  Serial.begin(115200);

  xTaskCreate(receive,"RECEIVE",2048,NULL,1,&receiver);
  xTaskCreate(send,"SEND",2048,NULL,1,NULL);
}


void loop() {
}

Because this example is so similar to the last, I will just explain the two areas that have changed.

Sending

The sending code has changed as follows.

unsigned int val = random(0,10);
Serial.printf("Sending Number: %i \n",val);
xTaskNotify(receiver,val,eSetValueWithOverwrite);

This example uses the xTaskNotify() function. The main advantage of using this function is that not only can you send a notification, but you can also directly manipulate the 32-bit number that is sent with it.

Of course to send a number, we first need to create one. For this example, we will just randomly generate one.

unsigned int val = random(0,10);

And then,it sends it with this.

xTaskNotify(receiver,val,eSetValueWithOverwrite);

If you look at the code above you will see eSetValueWithOverwrite, this controls what the notification does with the value. Besides overwrite, there are four other options which you can learn about at the notification documentation.

Receiving

Here is where we changed the receiving code.

uint32_t msg = 0;
xTaskNotifyWait(0,0,&msg,portMAX_DELAY);
Serial.printf("Got Number: %i \n",msg);

The first thing we do is create a variable to hold the incoming value from the notification.

uint32_t msg = 0;

Then, we use the xTaskNotifyWait() function. This function takes four arguments the first two are a bit complicated, so I will let the documentation explain them to you, but you can set them to zero if you don’t care about them.

The last two arguments are really easy, you just pass it the variable we made, so it can set it to the correct value, and after that you pass it the maximum time it should wait for a notification. portMAX_DELAY = forever

Serial.printf("Got Number: %i \n",msg);

The Downside to Notifications

While Notifications are extremely powerful and are a great tool to have around when working with microcontrollers, they have one main downside, complexity.

Admittedly, it’s not that hard to create a single notification, but because you have to store the address of your tasks and then determine which address to use later, It can be very hard to manage a large number of notifications.

If you are interested in some of FreeRTOS’s other powerful protocols, you might like to see how a mutex works.

Filed Under: Arduino, FreeRTOS

FreeRTOS Queue Example

May 4, 2023 by Baxter

ESP32 with LED connected to it, running the Morse code example

One of the big problems of creating multitasking programs is keeping all of the tasks in sync which is why there are so many systems in place to help with that.

Today, I am going to show you a simple FreeRTOS example about one of those systems called a queue. Queues are normally used when you have a lot of tasks that need to send a lot of data, but to make this example very clear, we will stick with a simple two task program that sends a single number between the tasks.

Here is the code

QueueHandle_t connection;

void sender_task(void* arg) {
  while (true) {
    int num = random(0, 10);
    xQueueSend(connection, &num, portMAX_DELAY);
    Serial.printf("Sent: %i \n", num);
    delay(500);
  }
}

void receiver_task(void* arg) {
  while (true) {
    int num;
    xQueueReceive(connection, &num, portMAX_DELAY);
    Serial.printf("Recived: %i \n", num);
  }
}


void setup() {
  Serial.begin(115200);
  connection = xQueueCreate(10, sizeof(int));

  xTaskCreate(sender_task, "SEND", 2048, NULL, 1, NULL);
  xTaskCreate(receiver_task, "RECEIVE", 2048, NULL, 1, NULL);
}


void loop() {
}

How It Works?

Now that you have seen the code, let’s look through the interesting sections of it to see how queues work.

Queue Handlers

Obviously if we create a queue, we want to use it, but in FreeRTOS instead of directly accessing and using a queue, you need to go through a queue handler, so the first thing, we are going to do, is create one like this.

QueueHandle_t connection;

The First Task

The whole point of a queue is to allow two or more tasks to send information to each other, so we need to create some tasks. Lets start with the one that writes information to the queue.

void sender_task(void* arg) {
  while (true) {
 

  }
}

Next, we will need to create some data to send. When you create a queue, you tell it what size of data it should expect. We haven’t done this yet, but when we do we will set it to the size of integers, so lets create a random integer.

void sender_task(void* arg) {
  while (true) {
    int num = random(0, 10);

  }
}

When we actually want to send data to the queue, we use the xQueueSend() function. It takes three arguments: the queue handler, a pointer to the data to send, and the maximum time to wait, if the queue is full.

void sender_task(void* arg) {
  while (true) {
    int num = random(0, 10);
    xQueueSend(connection, &num, portMAX_DELAY);
    Serial.printf("Sent: %i \n", num);
    delay(500);
  }
}

Reading the Queue

Now we are going to create the task function that receives data from the queue. It starts like most task functions with an infinite loop.

void receiver_task(void* arg) {
  while (true) {

  }
}

Our queue is setup to hold integers, so to read them we need to create a blank integer.

void receiver_task(void* arg) {
  while (true) {
    int num;

  }
}

And then we use the xQueueReceive() function to get the data. It is almost exactly like the xQueueSend() function, the only difference is that when you call this function instead of sending num it actually sets num equal to what ever is in the queue.

void receiver_task(void* arg) {
  while (true) {
    int num;
    xQueueReceive(connection, &num, portMAX_DELAY);
    Serial.printf("Recived: %i \n", num);
  }
}

Creating the Queue

It is kind of surprising, but up to this point, we have not even created the queue yet. To create it, we will need to use xQueueCreate() in the setup function.

void setup() {
  Serial.begin(115200);
  connection = xQueueCreate(10, sizeof(int));
}

That function takes two arguments: the first is the maximum amount of data it should hold and the second is how big each piece of data should be.

The last little thing to take notice of is that we set our queue handler equal to the return value of xQueueCreate().

To finish up, we will need to start each of the tasks.

void setup() {
  Serial.begin(115200);
  connection = xQueueCreate(10, sizeof(int));

  xTaskCreate(sender_task, "SEND", 2048, NULL, 1, NULL);
  xTaskCreate(receiver_task, "RECEIVE", 2048, NULL, 1, NULL);
}

Bonus

While this example is very good at explaining the basics of queues, it is not that interesting, so as a little bonus here is a slightly more complex example that uses one task to retrieve some text and another to send the text out of an LED by Morse code.

I will let you try to figure this one out on your own.

#define LED_PIN 23
#define MORSE_SPEED 100

#define MESSAGE "Hello there"




QueueHandle_t connection;



void process(void* arg) {
  while (true) {
    char text[] = MESSAGE;
    int len = strlen(text);
    for (int i = 0; i < len; i++) {
      xQueueSend(connection, &text[i], portMAX_DELAY);
      Serial.printf("Sent: %c \n", text[i]);
    }
    delay(30000);
  }
}


void dot() {
  digitalWrite(LED_PIN, true);
  delay(MORSE_SPEED);
  digitalWrite(LED_PIN, false);
  delay(MORSE_SPEED);
}
void dash() {
  digitalWrite(LED_PIN, true);
  delay(MORSE_SPEED * 3);
  digitalWrite(LED_PIN, false);
  delay(MORSE_SPEED);
}
void end_letter() {
  digitalWrite(LED_PIN, false);
  delay(MORSE_SPEED * 2);
}
void space() {
  digitalWrite(LED_PIN, false);
  delay(MORSE_SPEED * 4);
}


void  blink_led(void* arg) {
  while (true) {
    char c;
    xQueueReceive(connection, &c, portMAX_DELAY);
    c = toupper(c);

    switch (c) {
      case 'A':
        dot();
        dash();
        end_letter();
        break;
      case 'B':
        dash();
        dot();
        dot();
        dot();
        end_letter();
        break;
      case 'C':
        dash();
        dot();
        dash();
        dot();
        end_letter();
        break;
      case 'D':
        dash();
        dot();
        dot();
        end_letter();
        break;
      case 'E':
        dot();
        end_letter();
        break;
      case 'F':
        dot();
        dot();
        dash();
        dot();
        end_letter();
        break;
      case 'G':
        dash();
        dash();
        dot();
        end_letter();
        break;
      case 'H':
        dot();
        dot();
        dot();
        dot();
        end_letter();
        break;
      case 'I':
        dot();
        dot();
        end_letter();
        break;
      case 'J':
        dot();
        dash();
        dash();
        dash();
        end_letter();
        break;
      case 'K':
        dash();
        dot();
        dash();
        end_letter();
        break;
      case 'L':
        dot();
        dash();
        dot();
        dot();
        end_letter();
        break;
      case 'M':
        dash();
        dash();
        end_letter();
        break;
      case 'N':
        dash();
        dot();
        end_letter();
        break;
      case 'O':
        dash();
        dash();
        dash();
        end_letter();
        break;
      case 'P':
        dot();
        dash();
        dash();
        dot();
        end_letter();
        break;
      case 'Q':
        dash();
        dash();
        dot();
        dash();
        end_letter();
        break;
      case 'R':
        dot();
        dash();
        dot();
        end_letter();
        break;
      case 'S':
        dot();
        dot();
        dot();
        end_letter();
        break;
      case 'T':
        dash();
        end_letter();
        break;
      case 'U':
        dot();
        dot();
        dash();
        end_letter();
        break;
      case 'V':
        dot();
        dot();
        dot();
        dash();
        break;
      case 'W':
        dot();
        dash();
        dash();
        end_letter();
        break;
      case 'X':
        dash();
        dot();
        dot();
        dash();
        end_letter();
        break;
      case 'Y':
        dash();
        dot();
        dash();
        dash();
        end_letter();
        break;
      case 'Z':
        dash();
        dash();
        dot();
        dot();
        end_letter();
        break;
      case ' ':
        space();
        break;
    }
  }
}


void setup() {
  Serial.begin(115200);
  pinMode(LED_PIN, OUTPUT);
  connection = xQueueCreate(20, sizeof(char));

  xTaskCreate(process, "PROCESS", 2048, NULL, 1, NULL);
  xTaskCreate(blink_led, "BLINK", 2048, NULL, 1, NULL);
}

void loop() {
}

I you are looking for some more cool examples about FreeRTOS, you might like to see how FreeRTOS Semaphores work.

Filed Under: FreeRTOS, Micropython

FreeRTOS Semaphore Example

May 2, 2023 by Baxter

an ESP32(the board I am using) I with some text above it

Examples are one of the best tools for learning a new library, and FreeRTOS is no exception, so to help you along in your journey of learning FreeRTOS and in particular FreeRTOS Semaphores here is a quick example.

It works by taking two lists of numbers and adding them together. It does this three times the first time with one task doing all the work, the second with two, and the third with five, and to keep all of those tasks in sync, we will use a counting semaphore.

Here is the code

int A[7] = {1, 2, 3, 4, 5, 6, 7};
int B[7] = {4, 7, 3, 8, 2, 2, 9};


SemaphoreHandle_t data_left;




void process(void* arg) {
  while (xSemaphoreTake(data_left, portMAX_DELAY)) {
    unsigned int count = uxSemaphoreGetCount(data_left);
    Serial.printf("%i: %i + %i = %i \n", count, A[count], B[count], A[count] + B[count]);
    delay(500);
  }
  vTaskDelete(NULL);
}



void setup() {
  Serial.begin(115200);
  unsigned int time = millis();

  data_left = xSemaphoreCreateCounting(7, 7);


  Serial.println("Using One Thread");
  xTaskCreate(process, "P-ONE", 2048, NULL, 1, NULL);


  while (uxSemaphoreGetCount(data_left) != 0) delay(100);
  Serial.printf("Time: %i\n", millis() - time);
  time = millis();

  vSemaphoreDelete(data_left);
  data_left = xSemaphoreCreateCounting(7, 7);

  Serial.println("Using Two Threads");
   xTaskCreate(process, "P-TWO", 2048, NULL, 1, NULL);

  while (uxSemaphoreGetCount(data_left) != 0) delay(100);
  Serial.printf("Time: %i\n", millis() - time);
  time = millis();
  
  vSemaphoreDelete(data_left);
  data_left = xSemaphoreCreateCounting(7, 7);

  Serial.println("Using Five Threads");
  xTaskCreate(process, "P-THREE", 2048, NULL, 1, NULL);
  xTaskCreate(process, "P-FOUR", 2048, NULL, 1, NULL);
  xTaskCreate(process, "P-FIVE", 2048, NULL, 1, NULL);



  while (uxSemaphoreGetCount(data_left) != 0) delay(100);
  Serial.printf("Time: %i\n", millis() - time);

}

void loop() {

}

How This Code Works

Now, that you have seen the example code, why don’t we look through the interesting sections of it and see what they do and how they work.

The Numbers

The first thing the program does is create two lists with 7 numbers in each. These will be the numbers we will add together later.

int A[7] = {1, 2, 3, 4, 5, 6, 7};
int B[7] = {4, 7, 3, 8, 2, 2, 9};

Creating The Semaphore Handler

You don’t actually access semaphores directly, instead you have to go through a semaphore handler, so the next thing, we will do, is create one.

SemaphoreHandle_t data_left;

The Task Function

We will start out with a basic task function that we will call process.

void process(void* arg) {


  vTaskDelete(NULL);
}

Next, we are going to add an infinite loop that checks, if there is any data left to process.

void process(void* arg) {
  while (xSemaphoreTake(data_left, portMAX_DELAY)) {

  }
  vTaskDelete(NULL);
}

If there is data to process, we will find the position of the data, process it(aka. add the two numbers together), print the result to Serial, and then delay for 500 milliseconds.

void process(void* arg) {
  while (xSemaphoreTake(data_left, portMAX_DELAY)) {
    unsigned int count = uxSemaphoreGetCount(data_left);
    Serial.printf("%i: %i + %i = %i \n", count, A[count], B[count], A[count] + B[count]);
    delay(500);
  }
  vTaskDelete(NULL);
}

Running The Task

This last bit of code is all run in side of the setup function.

void setup() {
  Serial.begin(115200);
  
}

We are going to time how long it takes to process the data, so we will begin by recording the starting time in a variable.

void setup() {
  Serial.begin(115200);
  unsigned int time = millis();

}

Then, we will create a new semaphore and put it in the semaphore handler we made earlier. This semaphore is a counting semaphores which means when we create it, we need to give it a max value and starting value.

Because we have 7 pairs of numbers to add, we will set the max value to 7 and the starting value to 7 as well.

void setup() {
  Serial.begin(115200);
  unsigned int time = millis();

  data_left = xSemaphoreCreateCounting(7, 7);

}

Then, we will setup our task function. At this point, everything is done for the first test.

void setup() {
  Serial.begin(115200);
  unsigned int time = millis();

  data_left = xSemaphoreCreateCounting(7, 7);


  Serial.println("Using One Thread");
  xTaskCreate(process, "P-ONE", 2048, NULL, 1, NULL);
}

Next, we will tell the setup function to wait until all the numbers have been processed, and then print how long it took.

void setup() {
  Serial.begin(115200);
  unsigned int time = millis();

  data_left = xSemaphoreCreateCounting(7, 7);


  Serial.println("Using One Thread");
  xTaskCreate(process, "P-ONE", 2048, NULL, 1, NULL);

  while (uxSemaphoreGetCount(data_left) != 0) delay(100);
  Serial.printf("Time: %i\n", millis() - time);
}

The next test will use two tasks. We already have one task running in the background, so now all we need to do is create another one, reset the data, and of course we will need to time everything.

void setup() {
  Serial.begin(115200);
  unsigned int time = millis();

  data_left = xSemaphoreCreateCounting(7, 7);


  Serial.println("Using One Thread");
  xTaskCreate(process, "P-ONE", 2048, NULL, 1, NULL);

  while (uxSemaphoreGetCount(data_left) != 0) delay(100);
  Serial.printf("Time: %i\n", millis() - time);
  

  
  time = millis();

  vSemaphoreDelete(data_left);
  data_left = xSemaphoreCreateCounting(7, 7);

  Serial.println("Using Two Threads");
  xTaskCreate(process, "P-TWO", 2048, NULL, 1, NULL);

  while (uxSemaphoreGetCount(data_left) != 0) delay(100);
  Serial.printf("Time: %i\n", millis() - time);
}

The third test is just like the last two, except now there are 5 tasks running.

void setup() {
  Serial.begin(115200);
  unsigned int time = millis();

  data_left = xSemaphoreCreateCounting(7, 7);


  Serial.println("Using One Thread");
  xTaskCreate(process, "P-ONE", 2048, NULL, 1, NULL);

  while (uxSemaphoreGetCount(data_left) != 0) delay(100);
  Serial.printf("Time: %i\n", millis() - time);



  time = millis();

  vSemaphoreDelete(data_left);
  data_left = xSemaphoreCreateCounting(7, 7);

  Serial.println("Using Two Threads");
   xTaskCreate(process, "P-TWO", 2048, NULL, 1, NULL);

  while (uxSemaphoreGetCount(data_left) != 0) delay(100);
  Serial.printf("Time: %i\n", millis() - time);



  time = millis();
  
  vSemaphoreDelete(data_left);
  data_left = xSemaphoreCreateCounting(7, 7);

  Serial.println("Using Five Threads");
  xTaskCreate(process, "P-THREE", 2048, NULL, 1, NULL);
  xTaskCreate(process, "P-FOUR", 2048, NULL, 1, NULL);
  xTaskCreate(process, "P-FIVE", 2048, NULL, 1, NULL);

  while (uxSemaphoreGetCount(data_left) != 0) delay(100);
  Serial.printf("Time: %i\n", millis() - time);

}

This is what you should see in the Serial Monitor when you run the code.

A Little Misleading

If you look at the results from the test, you will see that it takes 3 seconds for the test with one task, 1.5 seconds for the test with two tasks, and 0.5 seconds for the test with five tasks.

Because of this you might be led to think, that the more tasks the better, but because we have the delay(500); in the process() function, the results you are seeing are greatly exaggerated.

In reality, because the ESP32 is a two core microcontroller, it should hit maximum performance while using two tasks.

I you are looking for some more cool examples about FreeRTOS, you might like to see how FreeRTOS Queues work.

Filed Under: Arduino, FreeRTOS

A Simple ESP32 FreeRTOS Example

April 28, 2023 by Baxter

Three LEDs being blinked by an ESP32

FreeRTOS is a really useful library, but learning how to use it is a bit hard, so I have created this simple example that uses FreeRTOS to blink three LEDs at different frequencies. The main focus of this example is how to use task functions and pass arguments to them.

The Parts

This project is mostly about the code, but here are the parts I used.

  1. 3 LEDs
  2. 3 resistors (220 ohm)
  3. 4 wires
  4. a breadboard
  5. an ESP32

Wiring up LEDs is pretty much the first thing you’re taught when learning electronics, so I will not waist your time with something you already now. I will say that I connected my LEDs to pins 17,18, and 5 of my ESP32, if you are trying to follow along exactly.

The Example Code

Here it is

struct blinkStruct {
  int pin;
  unsigned int time;
};


void blinkLED(void* arg) {
  blinkStruct data = *(blinkStruct*)arg;
  pinMode(data.pin, OUTPUT);
  while (true) {
   digitalWrite(data.pin, HIGH);
   delay(data.time);
   digitalWrite(data.pin, LOW);
   delay(data.time);
  }
  vTaskDelete(NULL);
}



void setup() {
  static blinkStruct LEDone;
  LEDone.pin = 17;
  LEDone.time = 1000;

  static blinkStruct LEDtwo;
  LEDtwo.pin = 18;
  LEDtwo.time = 300;

  static blinkStruct LEDthree;
  LEDthree.pin = 5;
  LEDthree.time = 1500;


  // blink led one
  xTaskCreate(blinkLED,"LED ONE",2048,(void*) &LEDone,1,NULL);

  // blink led two
  xTaskCreate(blinkLED,"LED TWO",2048, (void*) &LEDtwo,1, NULL);

  // blink led three
  xTaskCreate(blinkLED,"LED THREE",2048, (void*) &LEDthree,1, NULL);

}

void loop() {
}

How It Works

Now that you have seen all the code, let’s step through each section, so you can understand exactly what each part does. 

The Struct

In C++, structs are a way to create a custom variable. For this example, we will need to create one that can store data for each blinking LED.

struct blinkStruct {
  int pin;
  unsigned int time;
};

This struct has two internal variables pin and time. They are pretty self explanatory, pin is the gpio pin your LED is connected to, and time is how quickly the LED should blink.

The FreeRTOS Task

Next, we are going to need to create a task function. This function will be in charge of blinking one LED.

void blinkLED(void* arg) {
  // some more code coming

  vTaskDelete(null);
}

The last thing any task function should do is call vTaskDelete(null);. Doing this deletes the task, so your computer can keep running.

Later when we call this function, we are going to pass some data to it, but that data will be stored in a void pointer(void* ), so we need to use type casting to turn it into useful data.

void blinkLED(void* arg) {
  blinkStruct data = *(blinkStruct*)arg;

  vTaskDelete(null);
}

Inside our function, we now have a variable called data, and it has two internal properties: pin and time. The next thing we will need to do is set the mode of that pin to OUTPUT.

void blinkLED(void* arg) {
  blinkStruct data = *(blinkStruct*)arg;
  pinMode(data.pin, OUTPUT);

  vTaskDelete(NULL);
}

Then, we will simply create an infinite loop, that will turn the LED on, then wait a bit, then turn it off and wait, etc.

void blinkLED(void* arg) {
  blinkStruct data = *(blinkStruct*)arg;
  pinMode(data.pin, OUTPUT);
  while (true) {
   digitalWrite(data.pin, HIGH);
   delay(data.time);
   digitalWrite(data.pin, LOW);
   delay(data.time);
  }
  vTaskDelete(NULL);
}

The Setup

This last little bit of code is going to go in the setup funciton.

void setup() {

}

We will start by creating a blinkStruct variable. Because we will use it with a task, this variable should never be deleted, so you need to make it a global or static or dynamic etc. variable. Just do something that forces the computer to not delete it.

void setup() {
  static blinkStruct LEDone;

}

Then, we will tell the computer what pin the first LED is connected to, in my case it’s pin 17, and we will tell it to blink that LED once every 1000 milliseconds(1 second) .

void setup() {
  static blinkStruct LEDone;
  LEDone.pin = 17;
  LEDone.time = 1000;
}

Now, we will just repeat that process for the remaining two LEDs.

void setup() {
  static blinkStruct LEDone;
  LEDone.pin = 17;
  LEDone.time = 1000;

  static blinkStruct LEDtwo;
  LEDtwo.pin = 18;
  LEDtwo.time = 300;

  static blinkStruct LEDthree;
  LEDthree.pin = 5;
  LEDthree.time = 1500;
}

We have several variables and a function to blink a LED, now we just need to combine them together using the xTaskCreate() function.

void setup() {
  static blinkStruct LEDone;
  LEDone.pin = 17;
  LEDone.time = 1000;

  static blinkStruct LEDtwo;
  LEDtwo.pin = 18;
  LEDtwo.time = 300;

  static blinkStruct LEDthree;
  LEDthree.pin = 5;
  LEDthree.time = 1500;


  // blink led one
  xTaskCreate(blinkLED,"LED ONE",2048,(void*) &LEDone,1,NULL);
}

And we will do the same thing for the last two LEDs.

void setup() {
  static blinkStruct LEDone;
  LEDone.pin = 17;
  LEDone.time = 1000;

  static blinkStruct LEDtwo;
  LEDtwo.pin = 18;
  LEDtwo.time = 300;

  static blinkStruct LEDthree;
  LEDthree.pin = 5;
  LEDthree.time = 1500;


  // blink led one
  xTaskCreate(blinkLED,"LED ONE",2048,(void*) &LEDone,1,NULL);

  // blink led two
  xTaskCreate(blinkLED,"LED TWO",2048, (void*) &LEDtwo,1, NULL);

  // blink led three
  xTaskCreate(blinkLED,"LED THREE",2048, (void*) &LEDthree,1, NULL);

}

What Next?

Experimenting with code is a great way to learn, so once you get this code running, you could add some extra LEDs to the code or change the timing of the ones that are already there or if you really wanted to change it, you could rewrite the blinkLED() function to flash the LEDs in a different pattern.

If you are interested in learning more about FreeRTOS, you might like to see a FreeRTOS Semaphore Example.

Filed Under: Arduino, FreeRTOS

How to Program an ESP RC transmitter and Receiver

April 20, 2023 by Baxter

The remote I use next to an ESP32

Because so many projects require a remote control, many people build there own.

So I did the same. For my project, I chose to use an ESP8266 as the Transmitter and an ESP32 as the receiver. Then I quickly wiped up some code to run it with, and it worked, but just barely plus it was a pain to use.

So I took what I learned from that, and created the code on this page, and today I will explain how you can use it for your own remote.

What This Code Does

The code has three main things it does.

  1. The Transmitter automatically pairs with a near by Receiver.
  2. If the connection is lost, The Transmitter will try to reconnect.
  3. You can have multiple transmitters and receivers, and they should all work with each other.

And to be upfront here is two things that could be improved about the code.

  1. Using WiFi is over kill, a simpler protocol like ESPNOW would be more efficient.
  2. The pairing process is very simple, and will not work well if you have multiple transmitters and receivers on at the same time.

The Code

Here is the Transmitter Code for the ESP8266

#include <ESP8266WiFi.h>


#define PASSWORD "REMOTE_V1"

String name = ""; 
uint32_t last_message = 0;
bool waiting = false;



String connect_to_reciever() {
  int count = WiFi.scanNetworks();
  for (int i = 0; i < count; i++) {
    if (strncmp((WiFi.SSID(i)).c_str(), "REMOTEDV", 8) == 0) {
      Serial.print("\nName: ");
      Serial.print(WiFi.SSID(i));
      Serial.print("  Strength: ");
      Serial.println(WiFi.RSSI(i));
      return WiFi.SSID(i);
    }
  }
  return "";
}


void setup() {
  Serial.begin(115200);

  name = connect_to_reciever();

  while (name == "") {
    Serial.print("<looking>");
    delay(50);
    name = connect_to_reciever();
  }

  Serial.println("");
  Serial.println(name);

  WiFi.begin(name, "REMOTE_V1");
  while (WiFi.status() != WL_CONNECTED) {
    delay(100);
    Serial.print(".");
  }
  Serial.print("\n");
}


void loop() {
  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("\nLost Connection");
    WiFi.begin(name, PASSWORD);
    while (WiFi.status() != WL_CONNECTED) {
      delay(100);
      Serial.print(".");
    }
    Serial.println();
  }


  WiFiClient client;
  if (client.connect("192.168.4.1", 80)) {
    last_message = millis();
    while (millis() - last_message <= 2000) {
      if (waiting) {
        while (client.available()) {
          client.read();
          waiting = false;
          last_message = millis();
        }
      } else {
        char data[] = "hello there";
        client.print(data);
        waiting = true;
        last_message = millis();
      }
    }
    waiting = false;
  }
  Serial.println("Client Timed Out");
}

And here is the Receiver code for the ESP32.

#include <WiFi.h>


#define NETWORKNAME "REMOTEDV-ImCar"
#define PASSWORD "REMOTE_V1"
 

WiFiServer remote(80);

void setup() {
  Serial.begin(115200);

  WiFi.mode(WIFI_AP);

  IPAddress ip(192, 168, 4, 1);
  IPAddress net(255, 255, 255, 0);

  WiFi.softAPConfig(ip, ip, net);
  WiFi.softAP(NETWORKNAME, PASSWORD,1,false,1);

  remote.begin();
}


bool reply = false;
uint32_t last_message = 0; 


void loop() {
  WiFiClient client = remote.available();
  if (client) {
    Serial.println("Got a remote");
    last_message = millis();
    while (client.connected()) {
      if(millis() - last_message >= 2000){
        break;
      }
      if (!reply) {
        while (client.available()) {
          Serial.write(client.read());
          reply = true;
          last_message = millis();
        }
      } else {
        delay(100);
        client.print("got it");
        Serial.println("");
        reply = false;
        last_message = millis();
      }
    }

  }
  reply = false;
  Serial.println("No Connection");
}

Setting It Up

At this point if you wanted to, you could upload both programs to your boards, and run the code, but all that would happen is that the transmitter would send “hello there” to the receiver repeatedly so lets look at how you send your own data.

Transmitter code line 73-74

char data[] = "hello there";
client.print(data);

Thankfully the sending code is really easy, You can simple change the text inside of the data string. In fact, the data does not even have to be a string. You can send integers or just raw binary data, if you want.

Even so strings are really easy to process on the receiving side, so we will stick with them.

Receiver code line 39-43

while (client.available()) {
   Serial.write(client.read());
   reply = true;
   last_message = millis();
}

You can ignore the “reply = true;” and “last_message = millis();”. They are just there to make sure the boards stay in sink.

You can read the incoming data by using the client .read() function. Because it returns only one byte, normally you would use code that looks more like this.

if (client.available()) {
  String buf = "";
  while (client.available()) {
     buf += (char)client.read();
  }

  Serial.println(buf);
  //do something else with buf

  reply = true;
  last_message = millis();
}

Because I have no idea how your remote is setup, The best I can do now is point you in the right direction.

I would suggest you look into the ArduinoJson library. It allows you to turn ints, bools, chars, etc into text, which you can easily send over WiFi, and then your receiver can use that library to turn the text back into the original ints, bools, chars, etc.

What is really nice about the ArduinoJson library is that it can process data as it comes in, so you will not have to write code that buffers the data.

Some Additional Settings

Every receiver creates an access point. The name of that access point is defined at the begging of the code.

Receiver code line 4

#define NETWORKNAME "REMOTEDV-ImCar"

The name has to start with REMOTEDV (it stands for Remote Device), but otherwise you can change the name as much as you want.

If you want multiple receivers to be on at the same time, you will need to make sure they all have different names.

The network Password is also defined at the beginning.

Receiver code line 5 and transmitter code line 4.

#define PASSWORD "REMOTE_V1"

You can change this if you want, but you will need to make sure the transmitter and receiver have the same password.

Things To Keep In Mind

First, the reconnect mechanism works pretty well, but the one downside to it is that it takes about 5 second for it to kick in.

And Second, you don’t have to just use ESP boards. If you rewrite the code a bit, you should be able to support pretty much any microcontroller that has wifi.

If you are look at ways to remotely control your projects with WiFi, you might also like to see how to create a text input webserver.

Filed Under: Arduino

Micropython with an ESP32 Camera

April 11, 2023 by Baxter

ESP32-CAM board

Putting an ESP32 camera in your projects is just fun, but programming one is not always that fun. Normally you would program a microcontroller camera with c++ which is a very powerful and very complicated language, but if you are just creating a project for fun, micropython is a little simpler to use and has some powerful features of its own.

The Firmware

Because cameras take a lot of computer power to run and micropython is not the fastest language, the best thing to do is to write the camera driver in a fast language like c++ and compile it into the firmware.

Thankfully, somebody has all ready done this for us. All you have to do is download one of the firmwares and upload it to your board with what ever tool you like.

On that page, there are actually two firmwares you can choose from. I am using the one with BLE, but the code we are working with today does not use Bluetooth, so it does not matter which one you choose.

The Code

Taking a picture with micropython is a breeze. You just import the camera module, then initialize it, and finally take a picture.

import camera
camera.init(0)
pic = camera.capture()

Unfortunately, pictures take a lot of memory. If you start taking lots of pictures, your ESP32 is going to run out of memory fast, so we have to find somewhere to put those images.

Because the ESP32-CAM comes with a SD card slot built in, we will use it to store the pictures.

import camera,os,machine

os.mount(machine.SDCard(),'/sd')

camera.init(0)
camera.speffect(camera.EFFECT_GREEN)
print("Camera Initlized")

pic = camera.capture()
print("Picture Taken")
print("Size:",len(pic))

file = open("/sd/picture.jpeg",'wb')
file.write(pic)
file.close()

print("Finished Storing")

del pic
camera.deinit()
os.umount('/sd')

How That Code Works

Let’s quickly run through that code, and see the interesting parts.

Setting Up

The code starts by importing the needed libraries and initializing the SD card.

import camera,os,machine

os.mount(machine.SDCard(),'/sd')

Initializing the Camera

The simplest way to initialize the camera is like this.

camera.init(0)

You always have to put the zero there, but I can’t find any information about what it actually does.

If your camera is wired differently than the ESP32-CAM board, you can tell micropython the different pins you want to use like this.

camera.init(0,d0 = 14,d1 = 15,... )

There is also a few other functions you can use to change the camera. For example, here is one that filters out all the colors except green.

camera.speffect(camera.EFFECT_GREEN)

You can find the rest that are available by running this snippet of code on your board. It will print out all the functions and some of the values you can set them to.

import camera
print(help(camera))

Taking the Picture

All you have to do to take a picture is call the capture funciton.

pic = camera.capture()

The pic variable now has a picture in it. The picture will be in the JPEG format. It is possible to use a different format, but it is known to be glitchy and every time I have done it, I get error after error.

Storing the Photo

To save the picture to the SD card, we first create a file called picture.jpeg on the SD card and open it.

file = open("/sd/picture.jpeg",'wb')

Then, we write the picture to that file.

file.write(pic)

And finally, we close the file.

file.close()

To make sure this program can be run more than once without rebooting, we will add a little clean up code.

del pic
camera.deinit()
os.umount('/sd')

What Next?

Once you have run the above program, you can take your SD card out of the ESP32 and put it in your computer. On it, you will find a file called picture.jpeg. If you open it, you will see the picture the ESP32 took.

Now that you have a basic understanding of how to use the ESP32-CAM with micropython, you probably want to hook your camera up to wifi. The same people that made the firmware also made a example sketch that uses wifi.

To use it, you will need to upload about 7 files to your ESP32 and edit at least one of them, but you will have a cool web server when you’re done.

If you are interested in cameras, you might also like to see how to get a microphone to work with micropython.

Filed Under: Micropython

Running a Neural Net on an Arduino UNO

April 6, 2023 by Baxter

For a long time, I have wanted to run a neural net on an Arduino UNO, but when you start looking for libraries, you will find that most of them only work on the more powerful Arduino boards, and not the UNO.

Because of that and because I thought it would be fun, I decide to write my own simple neural net from scratch, and Today I will show you that code and how it works, so you can use it or create your own code.

Two Steps

There is two major things, we are going to have to do.

First, we will need to create the basic neural net and the algorithm that runs it. For this code, we are creating a net with 2 input nodes, 2 hidden nodes, and 2 output nodes.

picture of neural net with 2 input,2 hidden, and 2 output nodes, and all the connects

Then, we will need to create the training algorithm. This is normally the hard part, so I kept this algorithm as simple as possible.

Creating The Net

The actual net is really easy to create. It’s just a bunch of variables.

double hidden[2];
double output[2];

double weightH[2][2]; // weights for hidden nodes
double biasH[2]; // hidden node biases

double weightO[2][2]; // weights for output nodes
double biasO[2]; // output node biases

If we just left those variables with random values, it could cause the net to behave weirdly, so we can use the following function to set the default values.

void setup_net() {
  for (int i = 0; i < 2; i++) {
    biasO[i] = 0;
    biasH[i] = 0;
    for (int j = 0; j < 2; j++) {
      weightH[i][j] = 0.1;
      weightO[i][j] = 0.1;
    }
  }
}

Running the net is also pretty simple, but before we do that we need to create a new function.

double sigmoid(double num) {
  return 1 / (1 + exp(-num));
}

The sigmoid is a math function, which has some unique properties, in particular the output of it is always greater than 0 and smaller than 1.

Using a sigmoid or some similar function, can dramatically increase the power of your net, but it’s totally optional.

Here is the function that actually runs the neural net.

void run_network(double* input) {
  for (int i = 0; i < 2; i++) {// loop through hidden nodes
    double val = biasH[i];  //add bias to value
    for (int j = 0; j < 2; j++) { //loop thought input nodes
      val += input[j] * weightH[i][j];//add (input node)*(its weight) to value  
    }
    hidden[i] = sigmoid(val);
  }
  for (int i = 0; i < 2; i++) {// loop through output nodes
    double val = biasO[i]; //add bias to value
    for (int j = 0; j < 2; j++) { //loop thought hidden nodes
      val += hidden[j] * weightO[i][j];//add (hidden node)*(its weight) to value  
    }
    output[i] = sigmoid(val);
  }
}

Training the Net

There are four functions, we will need to create to train our net. We will start with this one.

double single_train(double* input, double* desired) {
  run_network(input);
  backpropigate(input, desired);
  double loss = loss_function(output[0], desired[0]);
  loss += loss_function(output[1], desired[1]);
  return loss;
}

As this function’s name suggests, its job is to take one set of inputs and one set of outputs, and train the net from them.

You may have noticed the loss_function. Because neural nets rarely are a 100% accurate, we need a way to judge if the network is accurate enough, which is where the loss_function comes in. Here is the one I am using.

double loss_function(double value, double desired) {
  return (value - desired) * (value - desired);
}

You also might have noticed the backpropigate() function. It’s the heart of the training process. Here is what it looks like.

void backpropigate(double* input, double* desired) {
  double output_err[2];
  for (int i = 0; i < 2; i++) { // loop through output nodes
    output_err[i] = (desired[i] - output[i]);
    biasO[i] += training_speed * output_err[i];
    for (int j = 0; j < 2; j++) { // loop thought connections
      weightO[i][j] += training_speed * hidden[i] * output_err[i];
    }
  }
  for (int i = 0; i < 2; i++) { // loop through hidden nodes
    float hidden_err =  weightO[0][i] * output_err[0] + weightO[1][i] * output_err[1];
    biasH[i] += training_speed * hidden_err;
    for (int j = 0; j < 2; j++) { // loop thought connections
      weightH[i][j] += training_speed * input[i] * hidden_err;
    }
  }
}

What the above function does is loop through every weight and bias in the system, and it changes each one based roughly on the following equation.

effect_on_output * needed_change * training_speed

effect_on_output has to be recalculated for each weight and bias. There is two ways to do that calculation. You could ether change the weight or bias by a small amount e.g. 0.0001 and then see how much the output changes, or you could use calculus. The code above uses the calculus method.

needed_change is different from node to node, but there is a lot of repeats. For this net we only need to calculate it twice. I stored those two values in the array output_err.

For complicated neural nets, training speed can change as the program runs, but it does not need to, so for this program I just defined it at the start of the program like this.

#define training_speed 0.1

Training in Bulk

Up to this point, we have all the stuff needed to train one piece of data into our net, but most of the time, we want to be able to train a lot of data into the net, so we need one more function.

double list_train(double input[][2], double desired[][2], int length) {
  double loss;
  for (int j = 0; j < MAX_LOOPS_FOR_LIST; j++) {
    Serial.println();
    for (int x = 0; x < MAX_LOOP; x++) {
      Serial.print(".");
      loss = 0;
      for (int i = 0; i < length; i++) {
        loss += single_train(input[i], desired[i]);
      }
      loss /= length;
      if (loss <= MAX_ERROR)break;
    }
    Serial.println();
    Serial.print("Loss: ");
    Serial.println(loss, DEC);
    if (loss <= MAX_ERROR)break;
  }
  return loss;
}

To make this function work, you give it a list of inputs and a list of desired outputs, and it trains the net for each pair of inputs and outputs.

It then repeats that process MAX_LOOP times and prints out the current loss of the net. It then repeats that whole process MAX_LOOPS_FOR_LIST times.

As a reminder as the loss number gets smaller, it means are net is getting more accurate, so while the network is running all of those loops, it’s also checking the loss variable, and if it ever dips below MAX_ERROR, the computer stops training because the net is accurate enough.

All of the MAX_LOOP, MAX_ERROR and so on, need to be define at the begining of the program like this.

#define MAX_LOOP  50
#define MAX_ERROR 0.00001

#define MAX_LOOPS_FOR_LIST 200

Full Code

You now have a working neural net, but to be complete, here is all the code combined, plus a little extra code to show how you can run the net.

#define training_speed 0.1

#define MAX_LOOP  50
#define MAX_ERROR 0.00001

#define MAX_LOOPS_FOR_LIST 200


double sigmoid(double num) {
  return 1 / (1 + exp(-num));
}
double loss_function(double value, double desired) {
  return (value - desired) * (value - desired);
}


//------------ setting up net-------------------------------

double hidden[2];
double output[2];

double weightH[2][2]; // weights for hidden nodes
double biasH[2]; // hidden node biases

double weightO[2][2]; // weights for output nodes
double biasO[2]; // output node biases

void run_network(double* input) {
  for (int i = 0; i < 2; i++) {// loop through hidden nodes
    double val = biasH[i];  //add bias to value
    for (int j = 0; j < 2; j++) { //loop thought input nodes
      val += input[j] * weightH[i][j];//add (input node)*(its weight) to value  
    }
    hidden[i] = sigmoid(val);
  }
  for (int i = 0; i < 2; i++) {// loop through output nodes
    double val = biasO[i]; //add bias to value
    for (int j = 0; j < 2; j++) { //loop thought hidden nodes
      val += hidden[j] * weightO[i][j];//add (hidden node)*(its weight) to value  
    }
    output[i] = sigmoid(val);
  }
}

void setup_net() {
  for (int i = 0; i < 2; i++) {
    biasO[i] = 0;
    biasH[i] = 0;
    for (int j = 0; j < 2; j++) {
      weightH[i][j] = 0.1;
      weightO[i][j] = 0.1;
    }
  }
}
//------------done setting up net----------------------------


//---------------training code-------------------------------

void backpropigate(double* input, double* desired) {
  double output_err[2];
  for (int i = 0; i < 2; i++) { // loop through output nodes
    output_err[i] = (desired[i] - output[i]);
    biasO[i] += training_speed * output_err[i];
    for (int j = 0; j < 2; j++) { // loop thought connections
      weightO[i][j] += training_speed * hidden[i] * output_err[i];
    }
  }
  for (int i = 0; i < 2; i++) { // loop through hidden nodes
    float hidden_err =  weightO[0][i] * output_err[0] + weightO[1][i] * output_err[1];
    biasH[i] += training_speed * hidden_err;
    for (int j = 0; j < 2; j++) { // loop thought connections
      weightH[i][j] += training_speed * input[i] * hidden_err;
    }
  }
}

double single_train(double* input, double* desired) {
  run_network(input);
  backpropigate(input, desired);
  double loss = loss_function(output[0], desired[0]);
  loss += loss_function(output[1], desired[1]);
  return loss;
}


double list_train(double input[][2], double desired[][2], int length) {
  double loss;
  for (int j = 0; j < MAX_LOOPS_FOR_LIST; j++) {
    Serial.println();
    for (int x = 0; x < MAX_LOOP; x++) {
      Serial.print(".");
      loss = 0;
      for (int i = 0; i < length; i++) {
        loss += single_train(input[i], desired[i]);
      }
      loss /= length;
      if (loss <= MAX_ERROR)break;
    }
    Serial.println();
    Serial.print("Loss: ");
    Serial.println(loss, DEC);
    if (loss <= MAX_ERROR)break;
  }
  return loss;
}

//---------------done with training code---------------------



void setup() {
  Serial.begin(9600);
  setup_net();

  double inputs[2][2] = {
    {0.1, 0.1},
    {0.3, 0.3}
  };
  double outputs[2][2] = {
    {0.6, 0.6},
    {0.8, 0.4}
  };
  list_train(inputs, outputs, 2);

  Serial.println("---Testing Net---");
  for (int i = 0; i < 2; i++) {
    run_network(inputs[i]);
    
    Serial.print("Input: ");
    Serial.print(inputs[i][0]);
    Serial.print(", ");
    Serial.print(inputs[i][1]);
    Serial.print("  Output: ");
    Serial.print(output[0]);
    Serial.print(", ");
    Serial.print(output[1]);
    Serial.print("  Desired: ");
    Serial.print(outputs[i][0]);
    Serial.print(", ");
    Serial.println(outputs[i][1]);
  }
}

void loop() {
 
}

If you like seeing how computer algorithms work, you might like to see the algorithms that makes robots draw straight lines.

Filed Under: Arduino

How to Repurpose A 16*2 LCD For a Breadboard Computer

March 30, 2023 by Baxter

16*2 LCD with wires and LEDs attached to it.

Building a computer form scratch is one of the coolest things you could ever do. Unfortunately doing it, requires a ton of parts that are rarely just siting around.

At least that is what I thought for a long time, but that is actually not true. A lot of common chips can be repurposed to build your own computer.

So today we are going to look at one of the many chips, you can reuse for your computer: the 16*2 LCD.

You can find this chip everywhere, and it just so happens, that this chip has about 80 bytes a ram inside of it. I think you can see where this is going.

The Parts

Here is the parts I used to setup my test.

  • 8 resistors ( 1K )
  • 11 resistors (10K)
  • 8 LEDs
  • Lots of wires
  • 16*2 LCD (without I2C)
  • UNO
  • Potentiometer (10K)

Don’t worry if that list looks a bit long. You only need an LCD and a few wires. I used the rest of the parts to speed up the testing process.

The Wiring

Here are the pins on a 16*2 LCD.

PinUse
VSS ground (-)
VDD+5V
VOContrast Control
RSRegister Select
RWRead/Write select
EEnable Pin
D0 – D7Data pins
A (not on all LCDs)Backlight (+)
K (not on all LCDs)Backlight (-)

VO, A, and K are all related to how the LCD looks, which is important if you are using the LCD as a display, but when using it for ram, all those pins can be left unconnected. Even so I connected them up, so I could see what the screen was doing.

We will of course need the VSS and VDD pins. Now would be a good time to plug those into the breadboard power rails.

Here is what the rest of the pins do.

The Data pins

Pins D0-D7 are the 8 data pins. We will use these pins to write and read data from the LCD ram. For testing, it is really nice if you have an LED on each of the data pins.

You will also want a pull-down resistor on each pin. Here is the schematic of my setup for a single data pin.

Schematic for data pins

RS

This pin controls if the screen is in Data mode or in Command mode.

RSMode
HIGHData
LOWCommand

When the LCD is in Data mode, you can read and write data to the ram chip, but you can’t select where you write or read.

In Command mode, you can do a lot of things, but most importantly, you can tell the LCD where you want to write and read data from.

RW

This pin tells the LCD, if you want to read data from it or write data to it.

RWMode
HIGHRead
LOWWrite

E

Whenever this pin goes from LOW to HIGH the LCD checks the current state of the RW and RS pins, and then decides, if it should send data out of the data pins or read data in.

Using The Chip

Now that you know how the pins work, lets see how you can use the chip as RAM.

Writing

Here is how you write data. First, you set the following pins to the correct states.

PinState
RSHIGH
RWLOW

Once the screen is ready to write, you put an 8-bit binary number on pins D0-D7 with the least significant bit(LSB) at D0.

Lastly, you set the E pin HIGH wait a small amount of time and then set it LOW.

Reading

Reading is very similar to writing. You simply put the following pins into the following states.

PinState
RSHIGH
RWHIGH

Then, you pull the E pin HIGH, and the data at the current address will be outputted on the data pins, and when you are done reading, you pull the E pin LOW .

Setting the Current Address

When the LCD turns on, the default address is 0, but what if you want a different address.

To set the current address, you start by setting the following pins.

RSLOW
RWLOW
D7HIGH

Next, you write a 7-bit binary number to pins D0-D6. Because there is only 80 bytes of ram, well there is actually a little more, but it requires a different system to get to, you need to make sure you don’t try to access memory that doesn’t exist for example address 81.

Once you have finished setting the data pins, you trigger the enable pin to make the LCD set the address.

The Address Counter

One thing that is a little unique about this chip is that it has an address counter. After you write or read data to the LCD, the current address gets incremented by one.

Another Use

Besides using these LCDs for RAM, you could also use them as an address counter. After all, I just mentioned they have one built-in.

If you want to use your LCD like this, you will need to know how to read the current address. I am not going to explain how that is done on this page, but you can look at the LCD datasheet to see how it’s done.

If you like learning about how fancy machines work behind the scenes, you might like to look at How to Control a Robot Arm.

Filed Under: Arduino

How to Improve Your ESP32 Web Pages

March 22, 2023 by Baxter

the web page that the code on this page makes

Lets say you have just created a web page for your ESP32 and it does not really look right to you, but your not sure how to make it look better.

This may or may not happen to you, but it definitely has happened to me, so today I will explain some of the tricks I use to make my pages look and work better.

An Example

Instead of just talking about how you can improve you web page. lets work through an example.

Here is some simple HTML that I made for the last tutorial.

<!DOCTYPE HTML>
<html>
 <body>
  <form>
   <label for = "input">Message</label>
   <input type = "text" id = "input"  name = "message">
   <input type = "submit" value = "Send">
  </form>
 </body>
</html>

And here is what running that HTML looks like.

super simple page with label, text box and button.

And I will admit it, we are starting from the absolute bottom of the barrel, but lets see where we can take this page.

POST vs GET

The purpose of the above code is that you can write some text and then send it to the server. If you actually ran it, you would notice to behaviors that are not always desirable.

First, you can always see the last message sent in the URL. If you look at the picture above you can see that I sent the message “hello there” because you can see those words in the URL.

Well actually, URLs format the data slightly so “hello there” becomes “hello+there“, but you get the idea.

The second problem is that every time I send something to the server, the page reloads. This is inefficient and can sometimes be problematic.

All of these problems occur, because our page is using the HTTP GET method. If we were to use the HTTP POST method instead, we could remove all these problems, so lets update the example page to use POST.

<!DOCTYPE HTML>
<html>
 <body>
  <form method = "POST">
   <label for = "input">Message</label>
   <input type = "text" id = "input"  name = "message">
   <input type = "submit" value = "Send">
  </form>
 </body>
</html>

The only thing that has changed is line 4, were we specifically tell the form to use the POST method.

You will also need to update the ESP32 code, so it can handle POST requests correctly. Here is a standard function, that you might normally use.

void page(){
 Serial.println("Client connected");
  if(server.hasArg("message")){
   Serial.println(server.arg("message"));
  }
 server.send(200,"text/html",WebPageCode);
}

And here is how you upgrade it to handle POST requests.

void page() {
  if (server.method() == HTTP_POST) {
    Serial.println("Client connected with post");
    if (server.hasArg("message")) {
      Serial.println(server.arg("message"));
    }
    server.send(204, "text/html", "");
  } else {
    Serial.println("Client connected");
    server.send(200, "text/html", PAGE);
  }
}

The above code should be pretty self explanatory, except line 7 which sends a 204 with blank text “”.

Servers have to send back data when a page makes a request or the page will crash. Unfortunatly if you send a message to a page it will reload the page with the new message.

Sending a 204 is a way to send a message that keeps the page from crashing, but at the same time does not reload the page.

MDNS

Typing in a long IP address every time you want to access you board is a pain, but there is a way round this.

With the magic of MDNS, you can use some text like CoolName.local instead of your IP address, and even cooler, it only takes two lines of code to turn MDNS on.

In your project you start by including the MDNS library. It should already be installed by default.

#include <ESPmDNS.h>

Then in the setup function, you run this.

MDNS.begin("myserver");

And of course you can replace myserver with any name you want with in reason (no spaces, no slashes, jada jada ja).

Then you can use WhatEverYouNamedYouBoard + .local instead of you board’s IP address.

I would highly suggest using MDNS in your projects, because it is so simple to use, and dramatically increases the quality of your page.

Styling Your Page

Up to this point we have talked about some of the small things you can do to make your web page look and work better, but so far we have neglected one of the most important things you can do: styling.

Just a quick note, styling is done with CSS. This post is not intended to teach you CSS, the point of this section is to give you a small list of things you need to be able to do in CSS, and point out some of the best sites to learn them from.

CSS controls a lot of your page’s settings, when you are getting started the two most important things you will want to learn are controlling how objects look, and where there get placed on the screen.

for controlling how objects look, I would look into CSS:

  • background-color
  • border
  • font-family

and for positioning your object you will want to learn:

  • margin
  • padding
  • flex

You might have heard of margin and padding before, but you probably have never heard about flex.

Flex

Flex is one of CSS’s layouts. There are other options you can choose from, but if you are just learning CSS, flex is a good one, because it’s very powerful and not terribly complicated. By far, the best site I have see that explains the flex layout is CSS Tricks.

For the rest of the CSS, I suggested you learn from W3S which is a good place to start and MDN Web Docs which is good if you want a lot of detail.

Grouping

When ever you have a bunch of items that work together on your screen, it’s best if you visually group them together. Here is how you can do that with a div.

<!DOCTYPE HTML>
<html>
 <body>
  <div id = "group">
   <form method = "POST">
    <label for = "input">Message</label>
    <input type = "text" id = "input"  name = "message">
    <input type = "submit" value = "Send">
   </form>
  </div> 
 </body>
</html>

As you can see, we start by putting every thing we want to group together inside of a <div>. We also give the <div> an id, so we can style it later.

<!DOCTYPE HTML>
<html>
 <body>
  <div id = "group">
   <form method = "POST">
    <label for = "input">Message</label>
    <input type = "text" id = "input"  name = "message">
    <input type = "submit" value = "Send">
   </form>
  </div> 
  <style>
      #group{
        background-color: #e2e2e2;
        border: 1px solid black;
        padding:10px;
        display: inline-block;
      }
  </style>
 </body>
</html>

Then we add an extra style. Here is how that works.

The first thing we do is create a rule targeting the <div> with id group.

#group{
  /* styles go here*/
}

Next we set the background color to gray and create a black border around our <div>.

#group{
  background-color: #e2e2e2;
  border: 1px solid black;
}

If you ran the code at this point, it would look like this.

Label, text box, and button, all inside of border that fills width of screen.

To fix the fact that the border runs into the button and the text box, we will add some padding and to stop the <div> from going way past the button we will make the <div> an inline-block.

#group{
  background-color: #e2e2e2;
  border: 1px solid black;
  padding:10px;
  display: inline-block;
}

here is what the result looks like.

Label, text box, and button, all inside of border with gray background

Don’t Reinvent the Wheel

This is probably the most important advice on this page. Before you start creating your web page, go look at other people’s web pages that do something similar to what you want, and study there pages to see how you could design your own.

This should go without saying, but First only imitate people you want to be like. If you want you web page to look good, make sure to look at high quality web pages, and second make the page your own. There is a big difference between getting some ideas of how to create your page, and just cloning the page.

Here is the complete rehaul of the code we started with.

I updated it from GET to POST, grouped everything together, and took inspiration from a similar web page to create it.

<!DOCTYPE HTML>
<html>
 <body>
 <div id="content">
  <form method = "post">
   <label for = "input">Message</label>
   <input type = "text" id = "input"  name = "message">
   <input type = "submit" id = "button" value = "Send">
  </form>
  </div>
  <style>
   html{
      height:auto;    
      background-color: #dee2de;
   }
      
   body{   
      position: absolute;
      top: 50%;
      left: 50%;
      transform: translate(-50%,-100%);
   }
  
   #content{
      background-color: white;
      border-radius: 6px;
      padding: 10px;
      font-family: arial, sans-serf;
      border: 1px solid black;
   }
  
   #button{
      background-color: #3dafb9;
      border: 1px solid transparent;
      border-radius: 2px;
   }
  
  </style>
 </body>
</html>

If you are interest in a full Arduino sketch or how the original HTML code works, you should check out Creating a ESP32 Web Server with Text Input.

Filed Under: ESP32 Web Page

Creating a ESP32 Web Server with Text Input

March 16, 2023 by Baxter

Simple web page with text box and send button

Being able to send text to your board from a web page is extremely useful. With it, you could create a chat server, simple command line, or any number of other cool projects, so today I will show you a simple way to create a web page with text input.

The Path to Victory

Before we go any further its important to quickly explain, how we are going to reach our goal.

We will start by create a simple HTML web page, and I do mean simple, it is only ten lines long.

Even so I do hope you understand the basics of HTML like what a tag is. If you are a little rusty on how HTML works, there is a ton of great web pages out there that explain HTML. I find a lot of my information from W3S html.

Then we are going to create an ESP32 program to host that page and read any message the page sends out.

The HTML page

Here is the code that we are going to talk about.

<!DOCTYPE HTML>
<html>
 <body>
  <form>
   <label for = "input">Message</label>
   <input type = "text" id = "input"  name = "message">
   <input type = "submit" value = "Send">
  </form>
 </body>
</html>

Now lets look at how it works.

<!DOCTYPE HTML>

We start all HTML files with the above tag. It is always put at the beginning of your HTML file, and its job is to make sure the browser uses the correct version of HTML to run your code.

Next we have the <html> tags.

<!DOCTYPE HTML>
<html>

</html>

All of the rest of our HTML code needs to go between the start <html> tag and the end </html> tag. Don’t forget the / on the end tag.

The next tag we use is the <body> tag.

<!DOCTYPE HTML>
<html>
 <body>

 </body>
</html>

Anything that is seen on your web page like images, text, buttons, etc. needs to go between the start <body> tag and the end </body> tag.

Now we have reached something that is a little more interesting, the <form> tag.

<!DOCTYPE HTML>
<html>
 <body>
  <form>

  </form>
 </body>
</html>

Forms are designed, so that you give them a bunch of inputs, like text areas and checkboxes, and if you press a special button on the screen the form sends the current value of all its inputs to the server, in are case that would be an ESP32.

We will start by putting a text box in our form.

<!DOCTYPE HTML>
<html>
 <body>
  <form>

   <input type = "text" id = "input"  name = "message">

  </form>
 </body>
</html>

As you can see to create our text box, we use the <input> tag. One thing that is a little unique about the <input> tag is that it does not have an end </input> tag.

We have to pass three attributes to the <input> tag: type, id, name.

We are setting the type of our input to text, which makes it a single line text box, but there are several other options you can choose from.

The id attribute is used for a lot of things, but today we will use it to create a label, which we are going to talk about in just a second. For now, we will set id equal to “input“, but you can call it what ever you want.

Imagine for a moment that you have a form that has several text boxes, when you send the form to the server how will it now which message comes from which text box.

Well you give each text box a name, and when the server reads a message, it is also given the name of the text box that sent the message. In HTML, you give inputs names in this way name=”SomeName”.

HTML actually takes this a step further and says, if an input doesn’t have a name, it should not be sent to the server, so make sure you give all of your inputs names.

Labels

If we ran the code right now, all we would see on the screen is a little text box, but that is not very user friendly. You almost always want a label next to your inputs, that explains what they are for, so lets create one.

<!DOCTYPE HTML>
<html>
 <body>
  <form>

   <label for = "input">Message</label>
   <input type = "text" id = "input"  name = "message">

  </form>
 </body>
</html>

Thankfully labels are pretty simple. You put the label’s text in between the start and end <label> tag.

And what makes labels cool is that you can connect them to your inputs. For example if you connect a label to a button, and then you click the label the button will be clicked as well.

You connect a label to an input with the for attribute. All you have to do is set the for attribute equal to the id of the input. We set the id of our text box to “input” earlier, so we connect our label to the textbox with for = “input”.

The Submit Button

I said earlier that there is supposed to be a button that when you click it, the form gets upload to the ESP32. Well we still need to create that button.

Fortunately it is simple. You create a new input with type = “submit” and set the value of it to what ever you want the text of the button to be.

<!DOCTYPE HTML>
<html>
 <body>
  <form>
   <label for = "input">Message</label>
   <input type = "text" id = "input"  name = "message">
   <input type = "submit" value = "Send">
  </form>
 </body>
</html>

The ESP32 Code

Ok, you have seen all the HTML code, now we will look at the ESP32 code. We are mainly going to focus on the part of the code that process the messages set by the page, but we will briefly skim through the rest of the code as well.

Here is the complete code.

#include <WiFi.h>
#include <WebServer.h>
#include <ESPmDNS.h>

WebServer server(80);



String PAGE = R"rrrrr(
<!DOCTYPE HTML>
<html>
 <body>
  <form>
   <label for = "input">Message</label>
   <input type = "text" id = "input"  name = "message">
   <input type = "submit" value = "Send">
  </form>
 </body>
</html>
)rrrrr";



void page(){
  if(server.hasArg("message")){
    Serial.println(server.arg("message"));
  }
  server.send(200,"text/html",PAGE);
}


void setup() {
  Serial.begin(115200);
  WiFi.begin("Your Wifi Name", "Your Password");
  while (WiFi.status() != WL_CONNECTED) {
    delay(100);
    Serial.print('.');
  }
  Serial.println();
  MDNS.begin("myserver");

  server.on("/",page);
  server.begin();
}

void loop() {
  server.handleClient();
  delay(5);
}

Now lets go through the code.

#include <WiFi.h>
#include <WebServer.h>
#include <ESPmDNS.h>

We start by including the three libraries needed to make our webpage.

WebServer server(80);

Then we create a web server on port 80.

String PAGE = R"rrrrr(
<!DOCTYPE HTML>
<html>
 <body>
  <form>
   <label for = "input">Message</label>
   <input type = "text" id = "input"  name = "message">
   <input type = "submit" value = "Send">
  </form>
 </body>
</html>
)rrrrr";

This is the HTML code we create earlier. You can write a string that covers multiple line if you start and end it with R”some_identifier( and )some_identifier” which is what the code above does.

void page(){
  if(server.hasArg("message")){
    Serial.println(server.arg("message"));
  }
  server.send(200,"text/html",PAGE);
}

This is where all the magic happens. The function above is called whenever someone tries to access the ESP32 and it does three things.

First, it checks if the server has an argument called message. In the HTML we named the textbox message, so what this line of code is really doing is checking, if the textbox has sent anything to the ESP32.

Secondly, if it has sent something, we print the message out to the Serial Monitor.

And lastly, we send the HTML code back to the browser, so that it can show our webpage.

void setup() {
  Serial.begin(115200);
  WiFi.begin("Your Wifi Name", "Your Password");
  while (WiFi.status() != WL_CONNECTED) {
    delay(100);
    Serial.print('.');
  }
  Serial.println();
  MDNS.begin("myserver");

  server.on("/",page);
  server.begin();
}

Above is the setup function, its job is to connect to WIFI and start the server along with a few other things.

make sure to change the line that says “Your Wifi Name” and “Your Password” to your WIFI network and its password.

WiFi.begin("Your Wifi Name", "Your Password");

You might also be interested in this line.

 MDNS.begin("myserver");

It allows you to connect to your ESP32 with the name myserver.local instead of having to use the ESP32 IP address. You can change the myserver part to what ever you want.

void loop() {
  server.handleClient();
  delay(5);
}

And of course we run the above code to make sure the webpage responds to anyone that connects to it.

Simple web page with text box and send button

Using the Code

There are two ways you can run the code on this page.

On Your Computer

Whenever you actually create you own webpage, which I highly suggest you do, its best if you write and test the code on your computer. If you want to play around with running the HTML just on your computer going to The Tools Needed to Write HTML for Microcontrollers will explain how its done.

On the ESP32

But honestly you probably just want to run the code. Take the complete arduino code( the one with numbers next to the code) and like I said earlier update the WIFI name and password.

Then you will need to upload the code to your ESP32.

Make sure you computer is on the same network that you told the ESP32 to connect to, and then type in your browser “myserver.local“. If you get an error message, it probably just means that your board has not had a enough time to connect to the internet. All you have is wait a second or two and restart the page.

Once the page loads type what ever you want into the text box, and then hit enter or click the send button.

Serial Monitor with lots of text that ends with "hello there" the message set from the client

If you look at the Serial Monitor for your ESP32 you should see what you typed in the box printed to the screen.

Filed Under: ESP32 Web Page

Learning JavaScript For the ESP32

March 13, 2023 by Baxter

To create a web page for your ESP32, you need to know HTML, CSS, and JavaScript, and while I can’t help you much with HTML and CSS, JavaScript is a different story.

JavaScript is very similar to C++ (the Arduino IDE language) and if you already know C++ there is a faster way to learn it.

Instead of learning the entire language from scratch, you can learn the major differences between C++ and JavaScript. Which should take much less time then reading an entire beginners guide to JavaScript.

Where To Put Your Code

It is pretty well know that JavaScript is part of how web pages are made, but a little less know is how you actual put it in the page. There is two main ways and I hope you understand a little HTML other wise this will probably be complete gibberish.

The first place is in-between the HTML script tags, which looks something like this.

<script>

// Your code goes here 

</script>

Or you can create a file, lets say you called it mycode.js, and then include it with the script tags.

<script src="mycode.js"></script>

If you use the file method make sure to put your js file in the same folder as your HTML code.

Void Loop and Void Setup

You should be familiar with the idea that Arduino boards have a setup and loop function and they are where you start your code from. They usually look something like this.

void setup(){
 //some setup code
}

void loop(){
  //some loop code
}

JavaScript does not have ether of these functions, instead you write code where ever you want. Think of it like the entire program is inside the setup function.

Here is how you would write the code above in JavaScript.


//some setup code 

while(true){

//some loop code

}

Variables

Arduino has a lot of different ways you can declare a variable. You have int, String, char, byte, bool, etc. JavaScript is different in that it only has four ways you create variables.

Var

The var keyword replaces all of the Arduino variable types. Lets run through a few examples.

This is how you create a number variable in C++.

int number = 123;

And this is how its done with JS (JavaScript).

var number = 123;

Here is a letter variable in C++.

char letter = 'L';

And here is how its done with JS.

var letter = 'L';

You should see the pattern, you just replace any keyword you would normally use like int with the var keyword.

Const

The Const keyword is used to create a variable that can be read but never changed.

Here is how you use it.

const number = 123;

Here is the C++ equivalent.

const int number = 123;

Let

The let keyword is used to make your programs slightly more memory efficient.

If you used the var keyword to create a variable inside of an if statement, you can still access the variable after the if statement has finished like in the code below.

if(true){
var myvar = 10;
}

alert(myvar); // totally allowed

But many times you don’t need that variable any more, and it is just waisting your memory.

The let keyword tells the computer to automatically delete the variable once the if statement is completed.

if(true){
let myvar = 10;
}

alert(myvar); // myvar no longer exists so it is undefined

In fact the let command works for any code inside of brackets {}, so it is also useful with loops.

Nothing

This is probably the weirdest of the bunch, but you are also allowed to create variables with out a keyword at all.

myvar = 10;

Functions

Functions are pretty much identical except the parts that use variables. It is easiest, if I show an example.

Here is a C++ function.

int add(int a,int b){
  return a + b;
}

And here is the same function in JS.

function add(a,b){
  return a + b;
}

The key things to notice is that all functions start with the word function and that the arguments for a function are simply a list of names and never have words like var or let with them.

Comparisons

In C++ comparisons are done with the ==, !=, >=, etc. In JavaScript you need to add an extra = to the end of the == and != commands. As usual here is an example.

C++ code.

int value = 200;

if(value >= 1000){
  Serial.println("High");
}else if(value == 0){
  Serial.println("Zero");
}else{
  Serial.println("In-between");
}

Add here is the same thing in JavaScript.

var value = 200;

if(value >= 1000){
  alert("High");
}else if(value === 0){
  alert("Zero");
}else{
  alert("In-between");
}

Arrays

JavaScript and C++ arrays are actually very different behind the scenes. To start here is a C++ array.

int numbers[10] = {0,10,20,30,40,50,60,70,80,90};

Serial.println(numbers[3]);

And here is the same array but in JS.

var numbers = [0,10,20,30,40,50,60,70,80,90];

alert(numbers[3]);

While those two arrays may look similar, there are actually two main differences, JavaScript arrays can change in size which is why, when you create a new array, you don’t have to specify the size.

JavaScript arrays can also contain multiple types of data at the same time, look at this array.

var mixedArray = [10,"some text",true,"and some more"];

Strings

Arduino boards have two types of strings used with them C style strings, and the String library. JavaScript strings are closest to the String library.

Here is a simple example of them in action.

var string = "hello world";
string = string + "!" + 1;

alert(string); 

Input and Output

When you are writing code, it is always nice, if you have some simple way to send debug information out and receive commands in.

You can get basic input and output by using the alert and prompt commands.

alert("some message");

var answer = prompt("a question");

The alert command creates a popup box with the message you give it and The prompt command will create a popup box that asks a question and below that there is a little text area that you can type your answer in. Your answer will then be returned by the prompt command.

Math

Thankfully math is pretty much the same in both languages. You can use the +, -, *, / or the +=, -=, etc. The only thing you will need to keep in mind is that JavaScript will automatically turn strings into numbers, if the strings are in a math equation.

var result = "100" - 20;

The result of the above code will be 80. This rule is true for all operations except addition.

var result = "100" + 20;

Instead a equaling 120, the code above actually results in “10020”. With addition, the number will be turned to a string, and then the two strings will be added together.

Loops

JavaScript loops are the same as C++.

for(var i = 0; i < 100;i++){
//do something
}

while(true){
//do something
}

Obliviously I can’t explain the entire JavaScript language in a single post, but by now you should have a basic understanding of how it works.

Filed Under: ESP32 Web Page

The Tools Needed to Write HTML for Microcontrollers

March 10, 2023 by Baxter

Web page with text Hello World

Have you ever wanted to create a web page for an esp32, and I don’t mean just have you esp32 host a web page, I mean have you ever wanted to write your own web page from scratch.

The fact that your here, probably means you do, and just like you need a saw to cut wood, there are tools you need to have to write a web page. Today we are going to look at those tools.

What are Web Pages Made of

I am glad you asked. It turns out that web pages are made of three languages: HTML, JavaScript, and CSS.

HTML

HTML is like the king of the three. While it is totally possible to create a web page without JavaScript or CSS, HTML is just required. The good news is that HTML is pretty simple.

JavaScript

JavaScript is what you would normally have to worry about. It’s a full on coding language with lots and lost of powerful features, but you likely already have a huge head start. JavaScript is very similar to C++ (the Arduino IDE language) which means that if you can program an Arduino board, learning JavaScript should also be pretty simple.

CSS

CSS controls how your web page looks. It can put borders around images, change the background color of your website, set the font used, etc. Learning the basics is straight forward, though I will be honest and say that mastering it is a different story.

The Required Tools

To write in the above coding languages you need two things: A browser and a text editor.

The factor that you can see this web page means you probably have a browser and most computers have at least one text editor by default, so you already have every thing you need.

Some text editors for example Microsoft Word add extra information to the text so your can’t use them to write HTML,JavaScript, or CSS.

Notepad or Gedit are two good options to start with. You can also find editors specially designed for HTML, JavaScript, and CSS. I use notepad++, but at the end of the day chose what ever is most convenient for you.

Hidden Extensions

If you look around on you computer for a minute do any of the file names you see end with .png, .jpeg, .txt, etc. If you can not find any files that end with .something you probably have hidden extensions turned on.

If extensions are hidden, it will be really hard to create your code files correctly so you will probably want to turn off hidden extensions. Just search the internet for “disabling hidden extensions” and you will find a few good pages on how its done.

A Quick Example

You have all the tools you need, so lets create a simple web page.

Create a file on your computer and name it “mypage.html“.

Then open that file with your text editor and write the following “Hello World” and after that save the file and close the editor.

picture of notepad++ with file open that has the text Hello World

Then right click on that file, select the open with option and then select your browser(in my case chrome).

A new browser tab should open with the text “Hello World” on the page.

Web page with text Hello World

If you want a little more of a webpage, you might like this page: Creating a ESP32 Web Server with Text Input

Some Extras

While these are the basic tools need to create a web page, there are a few more you might be interested in.

WYSIWYG

While I highly suggest learning HTML, it can be a pain to write a ton of HTML code, even if you now how. If you are creating a bigger page, it’s often a good idea to find a WYSIWYG program. These types of programs allow you to create a web page by a drag and drop interface or something similar, and then it generates the HTML code for you.

You will still have to create the JavaScript code and you will probably need to go in and edit the HTML a bit, but they can speed up the process of creating a web page a lot.

HTML Compressors

HTML compressors remove any unneeded space in your code so it is as small as possible. Which really helps when you have to work with microcontrollers that don’t have a lot of memory available.

You should now have a pretty good setup to write your own web page code.

Filed Under: ESP32 Web Page

How to Make a Robot Arm Draw a Straight Line

March 7, 2023 by Baxter

If you have ever tried to make a robot arm, one problem you have probably ran into is being able to draw straight lines.

It turns out that robot arms love to draw arcs, so today I will show you the system I used to move my robot arm in straight lines instead of arcs.

The Basic Plan

Before we go any farther, lets look at the basic way, we will solve the problem, and of course before we look at the solution, its important to look at the problem itself.

The problem

There is really only two more things you need to now about the arcs the arm draws.

First of all, the arc’s size depends on how far the arm moves. For example when I tell my robot arm to move 4 cm the arc it makes is almost nonexistent, but if I tell it to move 50 cm, it makes a big arc.

Secondly, the arcs in the pictures are exaggerated to make them visible, but usually they are much smaller and only get big and problematic when you move large distances.

It really does not matter, but this page would be a bit incomplete, if I did not mention that the shape the robot draws, may not be an arcs at all, but something similar like a segment of an ellipse or a parabola.

The Solution

Because small lines have very small arcs, We can solve our problem by taking the big line we want to move, and dividing it into a bunch of smaller lines.

Two lines that are identical except that the one on the right is made out of a bunch of smaller lines, and the one on the left is a single line.

Then if we use all of those little lines to move the robot arm, it will move something like this.

two lines, one made of a bunch of small straight lines and the other made bunch of curved lines.

As you can see the resulting line is pretty close to the straight line. If we make the little line segments smaller and smaller, the resulting line will become straighter and straighter, so now lets do it.

Rise and Run

When we can calculate all of those little line segments, we are going to need to find two important numbers and here is how you find them. The first number we need is the height of our line which is called the Rise of the line, and then we need to find the width of our line called the Run.

This picture should hopefully clear up what I a trying to say.

A diagonal line with its rise and run pointed out.

We use the following equation to find the rise of our line.

y2-y

And calculating the run is also very simple.

x2-x

The Math

We are going to create a few random start values. We’ll say that we need to process a line that starts at 0,10 and ends at 5,0 and we want to divided it into a bunch of smaller line segments with a size of 2 (this will be explained later).

We will start by defining some variables with the above values.

int x = 0;
int y = 10;

int x2 = 5;
int y2 = 0;

float size = 2.00;

This is also a good place to define a few variables we will need later.

float unit;  
float x_pluss;
float y_pluss;

Next, we need to calculate the rise and run for our line.

float rise = y2-y;
float run = x2-x;

For this scenario, the rise = 10 and the run = -5. Now we need to check if the abs of run is greater than or equal to the abs of rise.

If you are not familiar with the abs (Absolute Value) function, all it does is make sure we never get a negative number, for example abs(-10) = 10 and abs(10) = 10.

if (abs(run) >= abs(rise)) {
  unit = abs(run) / size;
} else {
  unit = abs(rise) / size;
}

That block of code we just ran calculates how many small lines are able to fit into our big line and sets the unit variable equal to that number.

Line Size

The above equations use the size variable, which we set earlier to 2. The size variable controls how big each little line is.

Unfortunately, setting size to 2 does not mean that every line segment is 2 long, but instead means every line will be approximately any where from 2 to 2.8384 in length. If you use a different number for size you can use the following equations to calculate the smallest and biggest possible length of your lines.

float smallest_size = size;
float biggest_size = size*sqrt(2);

The Small Lines

The next thing ,we need to do is calculate what the rise and run of each of the little lines needs to be. For now we will call the run x_pluss and the rise y_pluss.

x_pluss = run / unit;
y_pluss = rise / unit;

Generating the lines

We actually create the lines by generating a bunch of points, and if you tell your robot to move from one point to the next, it will create the mini lines.

We will create all the points, with the following for loop.

float point_x = x;
float point_y = y;
for (int i = 0; i < unit; i++) {
  // so something here
  point_x += x_pluss;
  point_y += y_pluss;
}
// so something here

You have two options now, where the code says //do something here you could run some code the stores all of the points in a array, and then you could use those points later, but that can be a complicated process, so for my machine as soon as I calculated a point I made the robot move to that point. It went something like this.

float point_x = x;
float point_y = y;
for (int i = 0; i < unit; i++) {
  move_arm(point_x, point_y);
  point_x += x_pluss;
  point_y += y_pluss;
}
move_arm(x2, y2);

Take note that I have the arm move one more time out side of the for loop. You have to have that there to make sure the robot moves to the end destination.

The Example

The following code using the above code, and has a few extra lines, so that when you run it, if you open the serial monitor you can type in four numbers like 0,10,5,0 and the Arduino board will run the math on those number and then print the results to the serial monitor.

void move_arm(float x, float y) {
  Serial.print("Move x: ");
  Serial.print(x);
  Serial.print(" Move y: ");
  Serial.println(y);
}



void setup() {
  Serial.begin(9600);
  Serial.setTimeout(300);
}

void loop() {
  // put your main code here, to run repeatedly:
  while (not Serial.available()) delay(1);


  int x = Serial.parseInt();
  int y = Serial.parseInt();

  int x2 = Serial.parseInt();
  int y2 = Serial.parseInt();

  float size = 2.00;

  float unit;
  float x_pluss;
  float y_pluss;

  float rise = y2 - y;
  float run = x2 - x;

  if (abs(run) >= abs(rise)) {
    unit = abs(run) / size;
  } else {
    unit = abs(rise) / size;
  }
  x_pluss = run / unit;
  y_pluss = rise / unit;

  float point_x = x;
  float point_y = y;
  for (int i = 0; i < unit; i++) {
    move_arm(point_x, point_y);
    point_x += x_pluss;
    point_y += y_pluss;
  }
  move_arm(x2,y2);


  Serial.print("Size: ");
  Serial.print(size);
  Serial.print(" Unit: ");
  Serial.print(unit);
  Serial.print(" Xp: ");
  Serial.print(x_pluss);
  Serial.print(" Yp: ");
  Serial.print(y_pluss);
  Serial.print(" Rise: ");
  Serial.print(rise);
  Serial.print(" Run: ");
  Serial.print(run);
}

Customizing the Code

One last thing to talk about is the size variable. The rough gist is that when you set the size to a small number the arm will move in a pretty straight line, but it will also take a lot of computer power to run the arm.

For the most part you can just play around with the number until you get the results you want. For my robot, I use the number 1. Because my inverse kinematics program measures things in cm, setting size to 1 means that my arm moves in little lines about 1 cm long.

And that is an important point, this code gives x,y coordinates out which are going to need to be fed to your inverse kinematics driver to work.

If you are building a robot arm you might be interested in How to Control a Robot Arm With Arduino (Inverse Kinematics) and Using an Arduino to Accurately Position a Stepper Motor In Degrees

Filed Under: Arduino

How to Control a Robot Arm With Arduino

February 27, 2023 by Baxter

simple drawing of 2 segment arm and a point near it

Building a robot arm is hard, but controlling one can often be even harder, especially if you want to use real distances, like telling the motor to move 5cm to the right and 10cm up.

Today we will look at the math need to control a supper simpler robot arm, in the hopes that even if you aren’t building this exact robot, you will have a good base line to start your own from.

Measuring Your Robot

To make the math work, we need to find a few numbers.

All we need is the length of each segment of the arm. For this code, we will call the length of the big arm base and the small are we will call top.

Make sure that you measure from joint to joint. Both of my robot’s arms are longer than 40 cm, but when I actually measure from joint to joint, I get 30 cm and 26.5 cm.

the real robot arm being used

What ever unit you measure your robot with, will also be what you will control it with. Which means that because I measured my robot in cm, when I tell my robot arm to move 40 to the left it moves 40 cm to the left.

Because of this, you also need to make sure you use the same unit for every measurement.

simple drawing of 2 segment arm and a point near it

Moving the Arm Correctly

We control the entire robot with two motors, one is connected to angle A and the other is connected to Angle C. The code, we are going to create today, will spit out the two angles those motors will need to be at to move the arm to the red dot.

And of course if the computer spits out a number like 90 degrees you need to be able to position your motor to exactly 90 degrees. Servo motors almost always do this for you automatically, but if you are using stepper motors like me, then you will probably need to do a little math.

If you are using stepper motors, you can learn more about that process at Using an Arduino to Accurately Position a Stepper Motor In Degrees

Polor Coordinates

The target position of our robot is measures in x,y coordinates. I left the x and y for the dot in the picture unmarked, because the math works for any point in reach of the robot.

The first step in our journey will be to turn x,y coordinates to polor coordinates. Polor coordinates have to values distance and angle.

Distance

drawing of robot arm with a point that has lines showing its distance from 0,0 position

To calculate the distance of our point from the center, we use the following line of code. You may recognize the equation below from algebra. It is the Pythagorean theorem.

float distance = sqrt(x*x+y*y);

Angle

drawing of robot arm with a point that has lines showing its angle from 0,0 position

The basic equation to calculate the angle is as follows.

float angle = atan(y / x);

Unfortunately this equation has one big problem. If you put in a negative number for x, it gives a completely wrong. There are two things. we need to do to fix this.

First we will make sure that the equation never gets a negative number. We will pass x through the abs() function. It forces all numbers passing through it to be positive.

float angle = atan(y / abs(x));

While the code above will not fail as big, it still doesn’t allow are robot arm to move to negative x positions, so we will add the following code after it to fix that.

if(x < 0){
  angle *= -1;
  angle += 180;
}

This code simply checks if x is negative. If x is negative, it will multiply the original angle by -1 and add 180 to it. This final angle value is exactly what we want.

Radians Vs. Degrees

The are two main ways people measure angles: Radians and Degrees. I am most familiar with degrees, so it is what I am using for this code, but Arduino by default uses radians for its math functions, and one of those functions happens to be the atan() function, we used earlier.

All of that means that the original angle we calculated is measured in radians but we need it to be in degrees. Thankfully, turning radians to degrees is as simple as multiplying our number by 57.2957 like shown below.

float angle = atan(y / abs(x)) * 57.2957;

As a recap, all of the code we have made up to this point should look like this.

float distance = sqrt(x*x+y*y);

float angle = atan(y / abs(x)) * 57.2957;
if(x < 0){
  angle *= -1;
  angle += 180;
}

The Real Math

We are almost to the end, but we have one last major task. The next two equations use a lot of trigonometry. This post is not intend to teach you trig, but if you want to research more about what we are doing, you can look up The Law of Cosines.

simple drawing of 2 segment arm and a point near it

We are going to need this image again for reference.

The first equation we will use will calculate the angle C.

float C = acos(((distance * distance) - ((top_arm * top_arm) + (base_arm * base_arm))) / (-2 * top_arm * base_arm));

We need to give the equation three things.

First we need to give it the distance we calculated earlier.

After that we need to give it the the length of the base arm and the top arm. For simplicity I used the following code do define those values.

#define base_arm 30
#define top_arm 26.5

Like the other equation we used earlier, this one will give us an angle in radians, so we need multiply it by 57.2957.

C *= 57.2957;

The equation for angle A is very similar to the one for angle C.

float A =  acos((((top_arm * top_arm) - (base_arm * base_arm)) - (distance * distance)) / (-2 * base_arm * distance));
A *= 57.2957;

The last thing we need to do to angle A is add the angle we calculated earlier.

A += angle;

Summing Up

drawing of robot arm reaching a point

Here is all the code put together.

float distance = sqrt(x*x+y*y);

float angle = atan(y / abs(x)) * 57.2957;
if(x < 0){
  angle *= -1;
  angle += 180;
}

#define base_arm 30
#define top_arm 26.5

float C = acos(((distance * distance) - ((top_arm * top_arm) + (base_arm * base_arm))) / (-2 * top_arm * base_arm));
C *= 57.2957;

float A =  acos((((top_arm * top_arm) - (base_arm * base_arm)) - (distance * distance)) / (-2 * base_arm * distance));
A *= 57.2957;
A += angle;

To make the code work for you robot: replace x and y with the position you want your robot to move to, replace base_arm and top_arm with the correct measurements, and then make your base motor move to angle A and make your top motor move to angle C.

Inverse

It’s important to note that there is actually more than one way your motors can move your arm to reach the target. Here is the other option.

another way  the robot can reach a point

With a robot arm that has only two segments there are only two options, but if you build a robot with three segments or more there are in theory an infinite number of possible ways to reach the target.

All this means is that your program will need to handle all of those options. To day we made the program just use the first option it found and ignore the rest, but if you code in some extra logic, you can potentially find faster ways to move your robot.

Filed Under: Arduino

How to Control Multiple Stepper Motors

February 23, 2023 by Baxter

two stepper motors

In my journey to make a simple robot arm, I came across another problem. How do you control both stepper motors at the same time. Today I will show you the code I used, so you can use it or modify it to make your own.

The Hardware

Before we start with the code, its import to mention the hardware I used with it.

For motors I am using two 28BYJ-48s and I am using the Arduino Stepper library to do the basic motor control. Then the new code runs on top and adds the multi motor and speed support.

The microcontroller I am using is a esp32. The code uses lots of floating point math, which is fine for an esp32 but If you use a different microcontroller that’s smaller, the program may run a bit slow.

Here is the code

class parallel_motor_driver {
  private:

    int total_steps = 0;
    int SPR = 0;
    int UPDATE_EVERY = 1000;
    int sign = true;

    unsigned int last_time = 0;
    bool ON = false;
    float steps_to_move = 0.00;
    float addon = 0;

  public:
    parallel_motor_driver(int _SPR, int _UPDATE_EVERY) {
      SPR = _SPR;
      UPDATE_EVERY = _UPDATE_EVERY;
    }
    void start() {
      ON = true;
      last_time = millis();
      addon = 0;
    }
    void resume() {
      ON = true;
      last_time = millis();
    }
    void stop() {
      ON = false;
    }
    void set(int steps) {
      total_steps = abs(steps);
      if (steps < 0) {
        sign = -1;
      } else {
        sign = 1;
      }
    }
    void set_speed_RPM(float speed) {
      float temp = float(float(SPR) * speed);
      steps_to_move = temp / (60 * (1000 / UPDATE_EVERY));
      if (steps_to_move == 0) {
        steps_to_move == 1;
      }
    }

    void update() {
      if (total_steps <= 0) {
        if (ON) {
          Serial.printf("Steps: %i Difference: ",steps);
          Serial.println(addon);
        }

        ON = false;
      }
      if (millis() - last_time >= UPDATE_EVERY && ON) {

        int steps_to_move_pluss_error = int(round(steps_to_move));
        addon += float(steps_to_move - steps_to_move_pluss_error);
        while(addon >= 1) {
          steps_to_move_pluss_error += 1;
          addon -= 1;
        }
        while (addon <= -1) {
          steps_to_move_pluss_error -= 1;
          addon += 1;
        }


        int real_steps = min(total_steps, steps_to_move_pluss_error) * sign;
        /*
        Serial.printf("Total_steps: %i Real_steps: %i Step_error: %i Addon: ", total_steps, real_steps,steps_to_move_pluss_error);
        Serial.print(addon);
        Serial.print(" Cal: ");
        Serial.print(float(steps_to_move - steps_to_move_pluss_error),6);
        Serial.print(" Steps_to_move: ");
        Serial.println(steps_to_move,6);
        */
        total_steps -= real_steps * sign;
        steps += real_steps;
        last_time += UPDATE_EVERY;
      }

    }
    bool state(){
      return ON;
    }
    bool finished(){
      return not bool(total_steps);
    }

    int steps = 0;
};

How to Use This Code

To use this code, you need to add it to your Arduino sketch preferably above the setup function. After that for every motor you will need run the following code.

First you will need to initialize it as follows.

parallel_motor_driver some_name(SPR, UPDATE);

You will want to replace some_name with what ever you want to call the driver.

Then, you will need to replace SPR (Steps Per Revolution) with the number of steps your motor takes to make one rotation. Usually you can find that number on the internet.

You will also want to replace UPDATE. It controls how often your motor updates its position. The lower this number, the smoother your motor will run, but as the number gets smaller, it will also take more and more of your computer’s power to run the program.

That number is actually how many millisecond to wait between moving your motor. I run my motors at 10, but going up to 50 is still pretty good.

The last thing you will need to do is run the following code somewhere in your loop function.

some_name.update();
if(some_name.steps){
  my_motor.step(steps);
  some_name.steps = 0;
}

That concludes the setup, but there are a few other functions you will probably want to use.

Setting the RPM

The following line of code is how you set the speed of you motor in rotations per minute(RPM). To make it as accurate as possible, you are allowed to give it fractional RPMs like 0.27.

some_name.set_speed_RPM(SOME_RPM);

The Set Function

The set function tells the driver how many steps you want your motor to move. Unlike a normal stepper motor driver, it does not actually start moving when you call set.

some_name.set(steps);

The rest of the functions are explained below.

MethodWhat it Does
some_name.start();Start the motor running
some_name.stop();Pauses the motor
some_name.resume();Turns motor back on
some_name.state();Tells you if the motor is on or off.
some_name.finished();Tells you if the motor has finished moving

Below is the program I used to test this code. If you try running it, you will probably need to change the pins for the stepper motors.

#include <Stepper.h>


Stepper motor = Stepper(2038, 27, 12, 14, 13);
Stepper motor2 = Stepper(2038, 26, 33, 25, 32);

class parallel_motor_driver {
  private:

    int total_steps = 0;
    int SPR = 0;
    int UPDATE_EVERY = 1000;
    int sign = true;

    unsigned int last_time = 0;
    bool ON = false;
    float steps_to_move = 0.00;
    float addon = 0;

  public:
    parallel_motor_driver(int _SPR, int _UPDATE_EVERY) {
      SPR = _SPR;
      UPDATE_EVERY = _UPDATE_EVERY;
    }
    void start() {
      ON = true;
      last_time = millis();
      addon = 0;
    }
    void resume() {
      ON = true;
      last_time = millis();
    }
    void stop() {
      ON = false;
    }
    void set(int steps) {
      total_steps = abs(steps);
      if (steps < 0) {
        sign = -1;
      } else {
        sign = 1;
      }
    }
    void set_speed_RPM(float speed) {
      float temp = float(float(SPR) * speed);
      steps_to_move = temp / (60 * (1000 / UPDATE_EVERY));
      if (steps_to_move == 0) {
        steps_to_move == 1;
      }
    }

    void update() {
      if (total_steps <= 0) {
        if (ON) {
          Serial.printf("Steps: %i Difference: ",steps);
          Serial.println(addon);
        }

        ON = false;
      }
      if (millis() - last_time >= UPDATE_EVERY && ON) {

        int steps_to_move_pluss_error = int(round(steps_to_move));
        addon += float(steps_to_move - steps_to_move_pluss_error);
        while(addon >= 1) {
          steps_to_move_pluss_error += 1;
          addon -= 1;
        }
        while (addon <= -1) {
          steps_to_move_pluss_error -= 1;
          addon += 1;
        }


        int real_steps = min(total_steps, steps_to_move_pluss_error) * sign;
        /*
        Serial.printf("Total_steps: %i Real_steps: %i Step_error: %i Addon: ", total_steps, real_steps,steps_to_move_pluss_error);
        Serial.print(addon);
        Serial.print(" Cal: ");
        Serial.print(float(steps_to_move - steps_to_move_pluss_error),6);
        Serial.print(" Steps_to_move: ");
        Serial.println(steps_to_move,6);
        */
        total_steps -= real_steps * sign;
        steps += real_steps;
        last_time += UPDATE_EVERY;
      }

    }
    bool state(){
      return ON;
    }
    bool finished(){
      return not bool(total_steps);
    }

    int steps = 0;
};

parallel_motor_driver parallel(2038, 10);
parallel_motor_driver parallel2(11937, 10);//11937


void setup() {
  Serial.begin(115200);
  motor.setSpeed(12);
  motor2.setSpeed(12);
}

void loop() {
  if (Serial.available()) {
    if(parallel.finished()){
    int steps = Serial.parseInt();
    float speed = Serial.parseFloat();
    parallel.set(steps);
    parallel.set_speed_RPM(speed);

    steps = Serial.parseInt();
    speed = Serial.parseFloat();
    parallel2.set(steps);
    parallel2.set_speed_RPM(speed);

    parallel.start();
    parallel2.start();
    }else{
      Serial.read();
      if(parallel.state()){
        parallel.stop();
      }else{
        parallel.resume();
      }
    }
  }



  parallel.update();
  parallel2.update();

  if (parallel.steps) {
    motor.step(parallel.steps);
    parallel.steps = 0;
  }
  if (parallel2.steps) {
    motor2.step(parallel2.steps);
    parallel2.steps = 0;
  }
}

If you are thinking about building a robot arm or something similar, you might be interested in Accurately Position a Stepper Motor In Degrees or How to Control a Robot Arm With Arduino.

Filed Under: Arduino

Using an Arduino to Accurately Position a Stepper Motor In Degrees

February 22, 2023 by Baxter

full view of robot arm used here

Recently, I have been trying to make a simple robot arm. One of the first problems I ran into when writing the code was positioning the motors where I wanted them in degrees, so I created the code on this page and today we will look at how it works and how you use it.

The code is at the bottom of the page. The only thing special you need to know to get it to work is how to calculate the SPR of your motor setup, which you can learn about in the Math section.

The setup I am using is an Arduino UNO and a 28BYJ-48 stepper motor wired on pins 8,9,10, and 11. That being said, this post will not to teach you how to use stepper motors with Arduino, instead it will explain how you can get better control of the motors you already have.

The Math

The equation to turn degrees into steps is pretty simple. Once you have replaced all the variables in it, you simply run it, and the number it gives you is the number of steps you need to move your motor.

(Steps_Per_Revolution / 360) * Degrees

The two interesting parts are Steps_Per_Revolution and Degrees.

Degrees is how many degrees you want your motor to move.

Steps_Per_Revolution is how many steps your motor takes to make one complete rotation.

Calculating SPR

The easiest way to find the SPR (Steps_Per_Revolution) of your motor is to look it up online. Though you will get better results, if you search “steps per revolution” for your motor. You will also want to factor in any gears connected to your motor.

For example, one of the motors on my robot is a 28BYJ-48. When you look up the SPR for it on the internet you will eventually find out that it is 2038. Connected to that motor are two gears with a 7 to 41 gear ratio which means the actual SPR of that motor is the result of the following equation.

2038 / (7 / 41)

How to Test

Math is great, but all to often when you start using math in the real world, you find it doesn’t work as nice as it did on paper. Because of this, it is always good to have a way to test that your math is working.

Bellow is the setup that I used. There are three lines marked A, B, and C. What looks like a K is actually supposed to be a line with an arrow pointing at it.

I tested my math by positioning the arms so A and B were next to each other and then told the arm to rotate 180 degrees if everything went correctly A would end up next to C.

The Code

To make this hole process simpler I created the following code. It’s a class that you can use to make all the math easier. It also adds a few extra features that I will explain below.

class  step_degree_driver {
  private:
    bool clockwise = true; // does motor turn clockwise with positive steps
    bool flip_rotation = false;
    bool last_direction = true;
    float last_angle = 0;
    float steps_per_degree = 0;
    int gear_error = 0;

    bool get_dir(int a) {
      if (a >= 0) {
        return not clockwise;
      } else {
        return clockwise;
      }
    }

  public:
    step_degree_driver(float _steps_per_rotation, bool _clockwise) {
      clockwise = _clockwise;
      steps_per_degree = double(_steps_per_rotation / 360);
    }

    void set_gear_error(int _gear_error) {
      gear_error = _gear_error;
    }
    void set_flip_rotation(bool flip) {
      flip_rotation = flip;
    }
    void set_current_angle(float current_angle) {
      last_angle = current_angle;
    }

    int step_error(int steps) {
      if (steps && last_direction != get_dir(steps)) {
        if (get_dir(steps)) {
          return steps + gear_error;
        } else {
          return steps - gear_error;
        }
      } else {
        return steps;
      }
    }

    int angle(float angle) {
      if (flip_rotation) {
        if (clockwise) {
          angle = angle * -1;
        }
      } else if (not clockwise) {
        angle = angle * -1;
      }

      int steps = round(steps_per_degree * float(angle - last_angle));
      steps = step_error(steps);

      last_angle = angle;
      last_direction = get_dir(steps);
      return steps;
    }

    int direct_angle(float angle) {
      if (flip_rotation) {
        if (clockwise) {
          angle = angle * -1;
        }
      } else if (not clockwise) {
        angle = angle * -1;
      }

      int steps = round(steps_per_degree * float(angle));
      steps = step_error(steps);

      last_angle = angle+last_angle;
      last_direction = get_dir(steps);
      return steps;
    }

};

How do you Use the Code?

First, you need to add the above code to your sketch. Its best if it’s put before the setup function.

Then, you will want to run the following code.

step_degree_driver some_name(SPR,Rotation);

You will need to replace SPR with the SPR of your motor setup. You will also need to set Rotation based on the table below.

Valuereason
trueIf your motor moves clockwise when given a positive number of steps.
falseIf your motor moves clockwise when given a negative number of steps.

There are two ways you can control the motor: direct_angle and angle. For both functions you give them an angle and then they will return the number of steps you need to move your motor.

The direct_angle function will simply calculate the number of steps needed to move your motor.

int steps = some_name.direct_angle(some_angle);

The angle function will actually memorize the last position of the motor and use it in the math for example if your motor starts out at 90 degrees and you tell it to move to 95 degrees your motor will not actually move 95 degrees but instead will move 5 degrees.

int steps = some_name.angle(some_angle);

That is all you need to get the driver to work, but there are a few other settings available.

Starting Position

This only matters ,if you are using the angle function. You can use it to modify what angle it thinks its currently at.

some_name.set_current_angle(180.00);

Gear Error

Because I am using cheap motors in my robot, the gears inside them can cause some slight inaccuracies for my robot the following code helps reduce some of that error.

some_name.set_gear_error(5);

Flipping the Rotation

By default if you told the computer to move 45 degrees, it would move 45 degrees clockwise and if you told it to move -45, it would move counter clockwise. You can reverse this behavior with the setting below.

some_name.set_flip_rotation(true);

One Last Example

Below is the program that I used to test that the code above works. If you run it, open the serial monitor and type in any angle then hit send and the motor will move that many degrees.

#include <Stepper.h>

Stepper motor = Stepper(2038, 11, 9, 10, 8);



class  step_degree_driver {
  private:
    bool clockwise = true; // does motor turn clockwise with positive steps
    bool flip_rotation = false;
    bool last_direction = true;
    float last_angle = 0;
    float steps_per_degree = 0;
    int gear_error = 0;

    bool get_dir(int a) {
      if (a >= 0) {
        return not clockwise;
      } else {
        return clockwise;
      }
    }

  public:
    step_degree_driver(float _steps_per_rotation, bool _clockwise) {
      clockwise = _clockwise;
      steps_per_degree = double(_steps_per_rotation / 360);
    }

    void set_gear_error(int _gear_error) {
      gear_error = _gear_error;
    }
    void set_flip_rotation(bool flip) {
      flip_rotation = flip;
    }
    void set_current_angle(float current_angle) {
      last_angle = current_angle;
    }

    int step_error(int steps) {
      if (steps && last_direction != get_dir(steps)) {
        if (get_dir(steps)) {
          return steps + gear_error;
        } else {
          return steps - gear_error;
        }
      } else {
        return steps;
      }
    }

    int angle(float angle) {
      if (flip_rotation) {
        if (clockwise) {
          angle = angle * -1;
        }
      } else if (not clockwise) {
        angle = angle * -1;
      }

      int steps = round(steps_per_degree * float(angle - last_angle));
      steps = step_error(steps);

      last_angle = angle;
      last_direction = get_dir(steps);
      return steps;
    }

    int direct_angle(float angle) {
      if (flip_rotation) {
        if (clockwise) {
          angle = angle * -1;
        }
      } else if (not clockwise) {
        angle = angle * -1;
      }

      int steps = round(steps_per_degree * float(angle));
      steps = step_error(steps);

      last_angle = angle+last_angle;
      last_direction = get_dir(steps);
      return steps;
    }

};




step_degree_driver drv(2038, false);






void setup() {
  Serial.begin(9600);
  Serial.println("running stepper_angle");

  motor.setSpeed(5);


  drv.set_gear_error(5);
  drv.set_current_angle(180.00);
  drv.set_flip_rotation(true);
}

void loop() {
  if (Serial.available()) {
    float direction = Serial.parseFloat();
    int steps = drv.direct_angle(direction);
    
    Serial.print(direction);
    Serial.print(" : ");
    Serial.println(steps);
    motor.step(steps);
  }
}

If you are interested in building a robot arm you might like to look at How to Control a Robot Arm With Arduino and How to Control Multiple Stepper Motors.

Filed Under: Arduino

How to Connect an ESP32 to a Raspberry Pi with WIFI

February 20, 2023 by Baxter

terminal output for raspberry pi
repl output for esp32

For a long time I have wanted a way to be able to run code on a raspberry pi and a esp32, and have the two programs talk to each other. Recently, I found a way to do that using sockets.

Sockets can be used for a lot of things, so to be more specific, today we will look at how to use wifi + sockets to connect two python programs together.

esp32 (CORE2) next to raspberry pi 4

Hardware

Before we dive into the code, its important to mention the hardware required.

The Computer

I am using a raspberry pi 4B, but you can use any computer that runs python and has internet access. In fact when I started testing, I was using my laptop for both the RPI and ESP32 sides.

The Other Computer

I am using an upgraded esp32 with more flash and psram, but the code is not specific to my board. It should run on any esp32 that has micropython installed.

You don’t even have to use an esp32, you could use any other computer or microcontroller that has python and WIFI. You don’t even need python, you should be able to use c++ instead. That being said the esp32 code is hardware specific, so you will need to create or modify your own code, if you use a different computer.

terminal output of raspberry pi

The RPI Code

We will start with the raspberry pi’s code because its simpler.

import socket

s = socket.socket()
s.bind(('',100))
s.listen(5)

c,a = s.accept()

while True:
        c.recv(5)
        print('sending')
        c.send(b'hello')

First we import the socket module, by default it should be installed with python.

import socket

Then we create a new socket, which we will call s.

s = socket.socket()

Then we need to bind our socket, which essentially turns it into a server. When you bind a socket you have to pass it a tuple, as a reminder tuples are like lists and they have the format (data1, data2, data3, …). The tuple we give it has two values Address and Port.

The Address can be an IP address,the host name of your computer, or some other connection to your computer. If you give ” for your address, the socket will manage all incoming traffic to your computer on its Port.

The Port number allows you to run multiple sockets at the same time. Its almost as if you are naming a socket. For this program we will use port 100.

If you choose to use a different port, it might be a good idea to research the meaning of port numbers for example if you used port 80 instead of 100, you might temporarily lose access to the internet on your computer .

I have rambled on long enough, here is the code we use to bind our socket to Address: ” and Port:100.

s.bind(('',100))

Then, we will tell the socket how many clients it can except at the same time. For this code, we will use 5, but there is really no point to that number, since the code will actually only be able to handle one client.

s.listen(5)

After that we tell the socket to wait until somebody connects to it. When somebody eventually connects to our socket, it will give us two new variables c and a. c stands for connection and a stands for address. For now we only care about c.

c,a = s.accept()

The Loop

Now the program will enter an infinite loop.

while True:

In that loop, we will wait for a message from the esp32 that is 5 characters long or shorter.

        c.recv(5)

Then, we will print ‘sending’ to the terminal.

        print('sending')

After that, we send hello back to the esp32.

        c.send(b'hello')
micropython repl output

The ESP32

Here is the esp32 code.

import network
wifi = network.WLAN(network.STA_IF)
wifi.active(True)
wifi.connect('MYChannel','')


import time
while not wifi.isconnected():
    time.sleep(0.1)

import socket
new_s = socket.socket()
new_s.connect(('Your IP Address',100))


def send(data):
    data = str(data).encode()
    left = len(data)
    while left:
        left -= new_s.send(data)
        
        
import select
def get():
    result,_,_ = select.select([new_s],[],[])
    if result:
        print(result)
        res = new_s.recv(5)
        print(res)
        new_s.send('hello')
        time.sleep(0.5)
        

send(b'start')
while True:
    get()
    time.sleep(0.1)

The Network Connection

For our sockets to work we need an internet connection. I turned my raspberry pi into a WIFI hotspot or more accurately a access point, and gave it the name MYChannel and then had my esp32 connect to it.

On the raspberry pi once you get the connection started, you will need to run the command hostname -I this should return the IP address of it. We will need it latter. If you get more than one IP address, you might have to try each of them individually.

On the ESP32, I run the following code to connect to my raspberry pi access point.

import network
wifi = network.WLAN(network.STA_IF)
wifi.active(True)
wifi.connect('MYChannel','')


import time
while not wifi.isconnected():
    time.sleep(0.1)

The Socket

Once you find some way to get your esp32 and raspberry pi on the same internet connection, we can move on to the socket code.

First we are going to import the socket module and create a new socket like we did earlier.

import socket
new_s = socket.socket()

Then, we will connect to the raspberry pi on port 100. You will also need to add your raspberry pi’s IP address.

new_s.connect(('Your IP Address',100))

The socket send and recv functions aren’t perfect. Sometimes you might try to send or receive some data and its not possible. Because of this, it’s best if you run a little extra code to guarantee everything goes as expected.

Below is how you can make a more reliable send function.

def send(data):
    data = str(data).encode()
    left = len(data)
    while left:
        left -= new_s.send(data)

The next function will use the select module, so we need to import it.

import select

This function was supposed to be a more reliable receive function, but I kind of went over board and its not really a receive function. Its more of a respond function.

def get():

The following code uses the select module to tell if our socket has any data to read.

    result,_,_ = select.select([new_s],[],[])
    if result:

If there is something to read, the code will print the result variable.

        print(result)

Then, it will ready 5 bytes of data from the esp32.

        res = new_s.recv(5)

After that, it prints the data it received.

        print(res)

And lastly it will send the text ‘hello’, and wait for half a second.

        new_s.send('hello')
        time.sleep(0.5)

We now use the fancy send function to send the text ‘start’.

send(b'start')

The last code we need to run is an infinite loop that repeats every 0.1 seconds and runs our get function.

while True:
    get()
    time.sleep(0.1)

The Close Function

The programs above have one vital thing missing from them. When your are done with a socket, you are supposed to call the close function on them like this.

YourSocketName.close()

The two programs above are infinite loops and never stop, so we never used the close function in the code and this can cause problems. For instance if you run the raspberry pi code, stop the program, and then try to run it again you will get errors, because the old program locked the socket.

To fix this, you will need to restart your pi. There are probably other ways to fix this, but that is the only way I now how to.

If you like playing around with esp32s and python, you will probably like LVGL. One of my favorite projects with LVGL is Building a Timer App with Micropython LVGL.

Filed Under: Micropython

How to Use Micropython LVGL Windows

February 15, 2023 by Baxter

a lvgl window

Its easy to make a ridged GUI. Everything has a place that it always stays in, and while this type of program is easy to build. Its not the funnest to use, so its best to allow the user to be able to customize the interface a bit.

Windows are one of the simplest ways to pull this off. They allow you to group a bunch of objects together, and the user can move that group around the screen. Today we will look at how to create a LVGL window and make it dragable.

Setting Up a Micropython Screen

Because there are so many different screens out there, I can’t add support for all of them to the example, so to make the example work, you will need to add your screen’s setup code to the beginning of it.

There is a little more information on this process at How to get a LVGL Micropython Screen to Work if you aren’t familiar on how it’s done.

Here is the example

import lvgl

window = lvgl.win(lvgl.scr_act(),30)
window.set_size(200,200)
window.set_style_border_side(lvgl.BORDER_SIDE.FULL,0)
window.set_style_border_width(2,0)
window.set_style_border_color(lvgl.color_hex(0x454545),0)

window.add_title('Your Program')

close = window.add_btn(lvgl.SYMBOL.CLOSE,45)

def close_window(data):
    window.delete()
close.add_event_cb(close_window,lvgl.EVENT.CLICKED,None)

cont = window.get_content()
text = lvgl.label(cont)
text.set_text('content goes here')
text.center()


def follow(data):
    input_dev = lvgl.indev_get_act()
    move = lvgl.point_t()
    input_dev.get_vect(move)
    window.set_pos(window.get_x()+move.x,move.y+window.get_y())
    
header = window.get_header()
header.add_event_cb(follow,lvgl.EVENT.PRESSING,None)

How Does This Code Work?

Since you’ve seen the program above, why don’t we walk through the code and see how it works. This should make it clearer how you can modify this code for your own uses…

The Setup

First we need to import the LVGL module. This is also a good spot to put your screen setup code.

import lvgl

The Window

We start by creating a empty window. The 30 tells the computer to make the header section 30 pixels tall.

window = lvgl.win(lvgl.scr_act(),30)

Then we will set the windows size to 200*200 pixels.

window.set_size(200,200)

Our window look a lot like the background, and it can sometimes be hard to tell where it is on the screen, so we will create a border around our window to make it’s position clear.

To begin we need to have our window enable borders on all sides.

window.set_style_border_side(lvgl.BORDER_SIDE.FULL,0)

Then we will set the border width to 2 pixels.

window.set_style_border_width(2,0)

Then we will set the border color to a dark grey.

window.set_style_border_color(lvgl.color_hex(0x454545),0)

The Header Content

Windows have a header section. Often some text or a button is put there. To show how its done we will add both.

First we will add a tittle.

window.add_title('Your Program')

Then we will add a button. It will be 45 pixels long and have the close symbol in the center of it.

close = window.add_btn(lvgl.SYMBOL.CLOSE,45)

Next we are going to create a function that will delete our window.

def close_window(data):
    window.delete()

Finally we connect that function to the close button, so when the button is clicked the the window is deleted from the screen.

close.add_event_cb(close_window,lvgl.EVENT.CLICKED,None)

The Content

We have created the window, but now we need something to put in it. The first step in that process is to retrieve the content area of the window.

cont = window.get_content()

Then, we create a label inside of the content area.

text = lvgl.label(cont)

Next, we will set that label’s text.

text.set_text('content goes here')

And lastly, we center it to the window’s content area.

text.center()

Making the Window Dragable

You can’t just click a window and it move to were you want it by default, but that is what we want it to be able to do today.

To start we need to create a new function.

def follow(data):

In side that function we retrieve the current input device in my case that is a touch screen.

    input_dev = lvgl.indev_get_act()

After that we create a empty point object.

    move = lvgl.point_t()

Next we set that point equalt to the movement of the input deviece.

    input_dev.get_vect(move)

Lastly we move the window in the direct of the input device.

    window.set_pos(window.get_x()+move.x,move.y+window.get_y())

We want to make the window move if you click the header, but not move if you click the body of the window, so we retrieve the window’s header.

header = window.get_header()

Then we connect that function we made to the header.

header.add_event_cb(follow,lvgl.EVENT.PRESSING,None)

The Result

Once you run the program, the window should appear. If you click the header, the dark bar at the top, and drag it, the window will follow your finger. If you click the close button the window will be deleted and disappear.

Windows are a great way to organize things on your screen. It is best if you have a larger screen and a cursor, but with a little creativity they can work great for small touch screens as well.

A good replaysment for windows on small screens is tileviews. If you want to learn more about what they are and how they work check out LVGL Tileviews.

Filed Under: Micropython

Making an Arduino Distance Sensor More Accurate

February 10, 2023 by Baxter

ultrasonic distance sensor measuring object that is 10 centimeters away.

When I think of Arduino and measuring distance the readily available and cheep ultrasonic sensors come to mind. Add while these senors are great for most projects there are a few project that need a little more accuracy.

So I ran a few experiments to see if the accuracy of the HC-SR04 could be improved. I will be honest and say that while I did increase the accuracy it was only by a small amount. If you want to see the exact experiments I did and the results, read on.

The Control

Before we can do any experiments, we need to know how accurate the sensor is normally, so I hooked up my sensor to pins 8 and 9, and ran the following code.

#define SEND_PIN 8
#define RECIEVE_PIN 9

void setup() {
  Serial.begin(9600);
  pinMode(8,OUTPUT);
  pinMode(9,INPUT);
  
}

void send(int length_of_pulse){
  digitalWrite(SEND_PIN,LOW);
  delayMicroseconds(4);
  digitalWrite(SEND_PIN,HIGH);
  delayMicroseconds(length_of_pulse);
  digitalWrite(SEND_PIN,LOW);
}
int recieve(int timeout_in_seconds){
  return pulseIn(RECIEVE_PIN,HIGH,timeout_in_seconds*100000);
}

//generic speed of sound 0.0343
//custom speed of sound 0.03412

void loop() {
  send(10);
  unsigned long time = recieve(1);
  float cm = ((time * 0.0343 / 2 ));
  Serial.println(cm);
  delay(1000);
}

With this code I took three types of measurements. The first was at 10cm way and at room temperature. The sensor gave readings that were with easily with in one centimeter of the actual distance. Here is the setup I use and a picture of the results in the Serial Monitor.

ultrasonic distance sensor measuring object that is 10 centimeters away.
screen of measurement around 9.76 cm

I even though I have tried my best, it is hard to position a piece of wood exactly 10 cm away and to account for that and any other errors that might be affecting the result, I will round up and say that the sensor can measure distances with one centimeter of potential error.

The next measurements I took were at long range from 100-300 cm and I got essentially the same result. I was afraid that the accuracy would go down as the distance increased but all in all nothing really changed. This is the basic setup I used.

ultrasonic distance sensor measuring object that is far away.

The last test I took outside. This was the only test to give a different result. Unfortunately I forgot to take pictures, but The piece of wood was 100 cm away and the sensor was only able to get with 2cm of that value. What made outside different was that the temperature was 31F.

Temperature and Sound

These sensors work by using the speed of sound. If the air temperature changes the speed of sound also changes slightly.

Normally when we code up these sensor, we assume the speed of sound is 343 m/s but if you where to measure the temperature around you and calculate the speed of sound from that, you could get a more accurate number.

So that is what I did. The first few times, I measured the temperature with a thermometer then put it through an on line converter and then inserted the result into the code.

To make this process faster, I hooked up a temperature sensor to my board and made it do the calculations before each measurement. That temperature sensor also measured humidity which has a slight effect on sound so I factored that in as well.

ultrasonic distance sensor measuring object that is 19 centimeters away.

If you look in the bottom corner you can see the temperature sensor underneath all the wires.

I calculated the speed of sound with the following equation.

331.4 + 0.6*temperature + 0.0124*humidity

One important thing to mention is that the equation above returns the speed of sound in meters per second, but the Arduino board needs centimeters per microsecond, so we divide the speed of sound by 10,000 which turns it into centimeters per microsecond which the Arduino can easily use.

Here is the full code I used.

#include <Wire.h>
#include "Adafruit_HTU21DF.h"

#define SEND_PIN 8
#define RECIEVE_PIN 9

Adafruit_HTU21DF sensor = Adafruit_HTU21DF();

void setup() {
  Serial.begin(9600);
  pinMode(8,OUTPUT);
  pinMode(9,INPUT);
  sensor.begin();
  delay(100);
  
}

void send(int length_of_pulse){
  digitalWrite(SEND_PIN,LOW);
  delayMicroseconds(4);
  digitalWrite(SEND_PIN,HIGH);
  delayMicroseconds(length_of_pulse);
  digitalWrite(SEND_PIN,LOW);
}
int recieve(int timeout_in_seconds){
  return pulseIn(RECIEVE_PIN,HIGH,timeout_in_seconds*100000);
}

float get_speed_of_sound(){
  float temp = sensor.readTemperature();
  float humidity = sensor.readHumidity();
  return (331.4 + 0.6*temp + 0.0124*humidity)/10000;
}

void loop() {
  float speed = get_speed_of_sound();
  send(10);
  unsigned long time = recieve(1);
  float cm = ((time * speed / 2 ));
  Serial.println(cm);
  delay(1000);
}

The result of this code is that the distance senor will all ways stays within 1cm of the correct value. If you take it outside or leave it in, it never decreases in accuracy.

What Improved?

The table below shows the values I got from the experiments. As you can see under normal conditions using a thermometer does nothing to increase the accuracy. If you take the sensor somewhere very cold the thermometer code will work better.

 Noraml CodeTemp Code
Inside 68F1 cm1 cm
Outside 31F2 cm1 cm

What I take away form this experiment is that only if you distance sensor is going to be exposed to extreme changes in temperature you should run the above code above but most of the time its not worth the work.

When I started this project I was thinking about building a Arduino sonar system. To make one you need to be able to accurately position your sonar sensor and if you are thinking about building one of your own you might be interested in Accurately Position a Stepper Motor In Degrees.

Filed Under: Arduino

How to Use LVGL Image Buttons

February 9, 2023 by Baxter

two blue buttons

If you need a button that has a lot of detail, it is usually best to create it with an image instead of drawing it from scratch. Doing it this way can simplify the process .

Today we will see how you can create a image button with LVGL. The images we will use to create the button are at the bottom of the page if you want to follow this tutorial exactly.

Setting Up a Micropython Screen

Because there are so many different screens out there, I can’t add support for all of them to this example, so to make this example work, you will need to add your screen’s setup code to the beginning of it.

There is a little more information on this process at How to get a LVGL Micropython Screen to Work if you aren’t familiar on how it’s done.

Here is the code

import lvgl
import fs_driver

drv = lvgl.fs_drv_t()
fs_driver.fs_register(drv,'A')


btn = lvgl.imgbtn(lvgl.scr_act())
btn.set_src(lvgl.imgbtn.STATE.RELEASED,'A:sd/left_button.png','A:sd/middle_button.png','A:sd/right_button.png')
btn.align(lvgl.ALIGN.BOTTOM_MID,0,-15)

otherbtn = lvgl.imgbtn(lvgl.scr_act())
otherbtn.set_src(lvgl.imgbtn.STATE.RELEASED,None,None,'A:sd/right_button.png')
otherbtn.align(lvgl.ALIGN.TOP_MID,0,15)


def clicked(data):
    print('button one clicked')

btn.add_event_cb(clicked,lvgl.EVENT.CLICKED,None)

def clicked_two(data):
    print('button two clicked')

otherbtn.add_event_cb(clicked_two,lvgl.EVENT.CLICKED,None)

How Does This Code Work?

Since you’ve seen the program above, why don’t we walk through the code and see how it works. This should make it clearer how you can use image buttons in your projects.

The Setup

First, we import the LVGL module. This is also a good spot to put your screen setup code.

import lvgl

The File System

Because images take up so much memory it is easiest if they are stored in a separate file than the code but, to be able to use those images, we need to tell LVGL how to use the file system.

To begin, we import the fs_driver module.

import fs_driver

After that, we create a new LVGL file system driver.

drv = lvgl.fs_drv_t()

Then we use the fs_driver module to setup that driver and install it. Every driver gets a single letter name in our case ‘A’. Later we will use that letter to access this driver.

fs_driver.fs_register(drv,'A')

The Button

We start by creating a new image button object.

btn = lvgl.imgbtn(lvgl.scr_act())

Image buttons are made of three images: Left, Middle, and Right. The left and right buttons are the ends of the button often times they have rounded corners. The middle image is the main body of the button.

To make our image button work we need to set those three images. I put mine on a SD card connected to my board. If you put them some where else, you will need to update the addresses below to the correct ones.

RELEASED is the default state, but you can add images to any of the other states and those images will be show when the buttons in that state.

btn.set_src(lvgl.imgbtn.STATE.RELEASED,'A:sd/left_button.png','A:sd/middle_button.png','A:sd/right_button.png')

Lastly we put the button near the botton of the screen.

btn.align(lvgl.ALIGN.BOTTOM_MID,0,-15)

The Other Button

If you don’t want to use three different images, you can use the None keyword instead. We will create one more image button, except this one will only use the right image.

otherbtn = lvgl.imgbtn(lvgl.scr_act())
otherbtn.set_src(lvgl.imgbtn.STATE.RELEASED,None,None,'A:sd/right_button.png')
otherbtn.align(lvgl.ALIGN.TOP_MID,0,15)

Connecting Everything Up

Once you create an image button they can be used just like a normal button. Lets make the computer print which button was pressed

We will begin by create a simple handler function.

def clicked(data):

Inside that function we print if that button one was pressed.

    print('button one clicked')

Then we connect it to the first image button we created.

btn.add_event_cb(clicked,lvgl.EVENT.CLICKED,None)

We repeat that process for the other button too.

def clicked_two(data):
    print('button two clicked')

otherbtn.add_event_cb(clicked_two,lvgl.EVENT.CLICKED,None)

Things To Think About

Creating a button and having it do something when its clicked is normally one of the simplest things you could do but when you use image buttons it gets really complicated both to code and to run.

That brings us to the last point images buttons require powerful microcontrollers to use them. You have to have a lot of ram and flash, plus you need a fast processor. I ran the above code on a esp32 which has both and there was some lag after you press the button to it responding.

All and all, image buttons are very useful but you need a good computer to run them.

LVGL has many types of interesting inputs, if you like image buttons you might also like LVGL Spinboxes.

I am not much of an artist but here is the images I used.

Filed Under: Micropython

How to Use Micropython LVGL Spinboxes

February 7, 2023 by Baxter

spinbox with four buttons

Many GUIs need a way to input a number. Normally this is done with a Keypad and a Textarea but that takes a lot of screen space. Spinboxes can do the same job but they are much smaller.

Lets look at how you can create a spinbox and use some buttons to control it.

Setting Up a Micropython Screen

Because there are so many different screens out there, I can’t add support for all of them to the example, so to make the example work, you will need to add your screen’s setup code to the beginning of it.

There is a little more information on this process at How to get a LVGL Micropython Screen to Work if you aren’t familiar on how it’s done.

Here is the code

import lvgl

spin = lvgl.spinbox(lvgl.scr_act())
spin.align(lvgl.ALIGN.CENTER,-17,-19)
spin.set_size(100,35)
spin.set_range(0,100000)
spin.set_digit_format(6,4)

up = lvgl.btn(lvgl.scr_act())
up.set_size(40,35)
up.align(lvgl.ALIGN.CENTER,53,-19)

down = lvgl.btn(lvgl.scr_act())
down.set_size(40,35)
down.align(lvgl.ALIGN.CENTER,53,17)
 
left = lvgl.btn(lvgl.scr_act())
left.set_size(48,35)
left.align(lvgl.ALIGN.CENTER,-42,17)

right = lvgl.btn(lvgl.scr_act())
right.set_size(48,35)
right.align(lvgl.ALIGN.CENTER,8,17)

def add_lb(text,obj):
    lb = lvgl.label(obj)
    lb.set_text(text)
    lb.center()
    
add_lb(lvgl.SYMBOL.UP,up)
add_lb(lvgl.SYMBOL.DOWN,down)
add_lb(lvgl.SYMBOL.LEFT,left)
add_lb(lvgl.SYMBOL.RIGHT,right)


def up_handler(data):
    spin.increment()
up.add_event_cb(up_handler,lvgl.EVENT.CLICKED,None)

def down_handler(data):
    spin.decrement()
down.add_event_cb(down_handler,lvgl.EVENT.CLICKED,None)

position = 0

def left_handler(data):
    global position
    if position == 0:
        position = 1
    position = min(position*10,100000)
    spin.set_step(position)
   
left.add_event_cb(left_handler,lvgl.EVENT.CLICKED,None)

def right_handler(data):
    global position
    position = int(max(position/10,0))
    spin.set_step(position)
    
right.add_event_cb(right_handler,lvgl.EVENT.CLICKED,None)

How Does This Code Work?

Since you’ve seen the program above, why don’t we walk through the code and see how it works. This should make it clearer how you can use spinners in your projects.

The Setup

First we import the LVGL module. This is also a good spot to put your screen setup code.

import lvgl

Creating the Spinner

To begin, we create a new spinner.

spin = lvgl.spinbox(lvgl.scr_act())

Then, we will position it on the screen.

spin.align(lvgl.ALIGN.CENTER,-17,-19)

Next, we set is size to 100*35 pixels.

spin.set_size(100,35)

After that, we will set the range of the spinbox to 0-100,000. The range limits the number inside the spinbox so for us that number can never be bigger that 100,000 or smaller than 0.

spin.set_range(0,100000)

Finally we will format the spinbox. When you format a spinbox you give it two numbers: the first tells LVGL how many digits the spinbox should have, and the second number tells LVGL where to put the decimal point.

spin.set_digit_format(6,4)

The Buttons

To use the spin box we need a few buttons. We will start by creating the up button.

up = lvgl.btn(lvgl.scr_act())

Then we will set its size.

up.set_size(40,35)

Lastly we set its position on the screen.

up.align(lvgl.ALIGN.CENTER,53,-19)

We create the rest of the buttons the same way.

down = lvgl.btn(lvgl.scr_act())
down.set_size(40,35)
down.align(lvgl.ALIGN.CENTER,53,17)
 
left = lvgl.btn(lvgl.scr_act())
left.set_size(48,35)
left.align(lvgl.ALIGN.CENTER,-42,17)

right = lvgl.btn(lvgl.scr_act())
right.set_size(48,35)
right.align(lvgl.ALIGN.CENTER,8,17)

The Labels

We now have four buttons but they all look the same so we need to add a label to each of them. To make that process easier we will create a function.

def add_lb(text,obj):

This function will create a label inside a button that we choose, set its text, and center it.

    lb = lvgl.label(obj)
    lb.set_text(text)
    lb.center()

After that we use the above function to add the UP symbol to the up button.

add_lb(lvgl.SYMBOL.UP,up)

We add the rest of the symbols like we did the first.

add_lb(lvgl.SYMBOL.DOWN,down)
add_lb(lvgl.SYMBOL.LEFT,left)
add_lb(lvgl.SYMBOL.RIGHT,right)

Connecting the Buttons

We last thing we have to do is connect the buttons we have made to the spinbox. We use handler functions to do that.

As usual we will start with the up button by create a new function.

def up_handler(data):

In side that function we simply tell the spin box to increment its value.

    spin.increment()

And finally we attach that handler to the up button so whenever the button is clicked the function is called.

up.add_event_cb(up_handler,lvgl.EVENT.CLICKED,None)

The down button is the same as the up bottom except we decrement the value instead of increment.

def down_handler(data):
    spin.decrement()
down.add_event_cb(down_handler,lvgl.EVENT.CLICKED,None)

The left and right buttons require a little more work we will start with the left.

To begin we create a new variable.

position = 0

Then we create the left handler function.

def left_handler(data):

Inside that function we import the position variable we created.

    global position

Then we check if position equals zero if it is we set position equal to one.

    if position == 0:
        position = 1

Then we multiply position by 10 and constrain it so its not bigger than 100,000.

    position = min(position*10,100000)

After that we set the spinbox’es cursor to the correct position.

    spin.set_step(position)

To finish the left button off we add that function to the button.

left.add_event_cb(left_handler,lvgl.EVENT.CLICKED,None)

The right buttons function is very similar to the one we just made except its a little simpler.

def right_handler(data):
    global position
    position = int(max(position/10,0))
    spin.set_step(position)

Last of all we add that function to the right button.

right.add_event_cb(right_handler,lvgl.EVENT.CLICKED,None)

What Should Happen?

Once you run the program, the spinbox and the four buttons should appear on your screen. If you click the up or down buttons the number in the spinbox that the cursor is on will get bigger or smaller. When you press the left or right button the cursor in the spinbox will move left or right.

Spinboxes are extremely customizeable. You can fit them into pretty much any little gap on your screen. The only side affect of using spinboxes is that they are a little slow to put numbers in.

In general a full on keypad will be nicer to use but if you don’t have the room spinboxes are a excellent replacement.

If you are looking for other ways to shrink down your GUI then you might like to look into LVGL dropdown boxes.

Filed Under: Micropython

How to Use Micropython LVGL Themes

February 6, 2023 by Baxter

two buttons each with different themes applied to them.

Applying individual styles to every object on your screen is a lot of work. Because of how annoying this process is LVGL has super styles aka themes. You apply a theme to your display, and it styles every object on your screen.

Today we will work through how to create a custom theme and modify an existing theme.

How Themes Work

Themes are actually a function. Essentially a you create a function, and every time an object is created your function is called. Inside that function, you check what type of object was created and then apply a style to it.

Setting Up a Micropython Screen

Because there are so many different screens out there, I can’t add support for all of them to the example, so to make the example work, you will need to add your screen’s setup code to the beginning of it.

There is a little more information on this process at How to get a LVGL Micropython Screen to Work if you aren’t familiar on how it’s done.

Here is the example

import lvgl

display = lvgl.disp_get_default()
defualt_theme = lvgl.theme_get_from_obj(lvgl.scr_act())

def apply(th,obj):
    if obj.get_class() == lvgl.btn_class:
        obj.set_style_bg_color(lvgl.color_hex(0x009900),0)

theme = lvgl.theme_t()
theme.set_parent(defualt_theme)
theme.set_apply_cb(apply)

display.set_theme(theme)


btn = lvgl.btn(lvgl.scr_act())
btn.center()
btn.set_size(60,40)

gen_theme = lvgl.theme_default_init(display,lvgl.color_hex(0x990000),lvgl.color_hex(0x00ff00),False,lvgl.font_montserrat_16)

display.set_theme(gen_theme)

btn = lvgl.btn(lvgl.scr_act())
btn.align(lvgl.ALIGN.CENTER,0,60)
btn.set_size(60,40)

Understanding This Code

Just looking at a block of code is not always very informational. To make how this micropython code works more clear, lets walk through the lines of code above and see what they do.

The Setup

Of course before we can use LVGL we need to import it. This is also a good spot to put your screen setup code.

import lvgl

We also need to retrieve the active screen object. Themes are applied to physical screens, so if you have more than one screen connected to your board, your are going to need to apply a theme to each one.

display = lvgl.disp_get_default()

Custom Themes

Lets create a custom theme. To make this theme simple, it will only target LVGL buttons, so we will use the default themes settings for all the other widgets. First we are going to retrieve the default theme.

defualt_theme = lvgl.theme_get_from_obj(lvgl.scr_act())

Next we create the theme’s function.

def apply(th,obj):

Inside that function we check if the current object is a button.

    if obj.get_class() == lvgl.btn_class:

If it is, we change the buttons color to green.

        obj.set_style_bg_color(lvgl.color_hex(0x009900),0)

Now we create a empty theme object.

theme = lvgl.theme_t()

Then we set its parent to the default theme any thing our theme does not deal with the parent theme takes care of.

theme.set_parent(defualt_theme)

After that we set the themes function to the function we created earlier.

theme.set_apply_cb(apply)

Lastly we add our theme to the the screen.

display.set_theme(theme)

The Button

Our new theme is applied but nothing is actually using it. Lets create button to show its working.

btn = lvgl.btn(lvgl.scr_act())
btn.center()
btn.set_size(60,40)

Generating a Theme

Manually creating a theme that styles every widget takes a lot of work, so LVGL comes with some pre-made themes that you can modify to your liking.

Making a new theme, only takes one line of code.

gen_theme = lvgl.theme_default_init(display,lvgl.color_hex(0x990000),lvgl.color_hex(0x00ff00),False,lvgl.font_montserrat_16)

The theme_default_init() function takes five arguments. The first argument is the active display. The other four arguments are explained below.

Argument Value Controls
Primary Color Any LVGL Color the color of most objects
Secondary Color Any LVGL Color the color of a few objects
Dark/Light Mode True or False whether LVGL uses dark or light colors
Font Any LVGL Font the default font of LVGL

With that one line of code we have generated a complete theme that works for all widgets, but we still need to apply it to the screen to see it work.

display.set_theme(gen_theme)

One Last Button

Of course, we need to create one more button to show that our theme is working.

btn = lvgl.btn(lvgl.scr_act())
btn.align(lvgl.ALIGN.CENTER,0,60)
btn.set_size(60,40)

Things To Think About

One obvious question that comes up, if we change the theme why doesn’t every object on the screen get that new theme applied to it. The answer to that question is I don’t now. Sometimes it seems that adding a theme does overrides the current theme and some time it seems to let the old one exist in the background.

We looked at two ways to create themes today. The last one was clearly easier and is the best option for normal use. The first method should really only be used, if you need an extremely custom theme.

If you like LVGL Themes then you might like LVGL Drawing Descriptors. They are another great way to customize your projects.

Filed Under: Micropython

How to use Micropython LVGL Spinners

February 3, 2023 by Baxter

two lvgl spinners the one on the left is bigger.

Every now and then you will create a program that takes a while to complete. When that happens, it is best if you give the user some indicator of what is happening like a progress bar. One of the most common, indicators is the spinner.

Today, we will look at how you make and edit spinners. Below is some example code and below that is an explanation of how the code works

Setting Up a Micropython Screen

Obviously, if you are creating a program that draws things on a screen, you have to have access to a screen.

Because there are so many different screens out there, I can’t add support for all of them, so to make this example work, you will need to add your screen’s setup code to the beginning of the example.

There is a little more information on this process at How to get a LVGL Micropython Screen to Work if you aren’t familiar on how it’s done.

Here is the code

import lvgl

spin = lvgl.spinner(lvgl.scr_act(),900,60)
spin.set_pos(200,100)
spin.set_size(50,50)


background = lvgl.style_t()
background.init()
background.set_arc_color(lvgl.color_hex(0x555555))
background.set_arc_width(40)

indicator = lvgl.style_t()
indicator.init()
indicator.set_arc_color(lvgl.color_hex(0x00FF00))
indicator.set_arc_rounded(0)
indicator.set_arc_width(20)

wait = lvgl.spinner(lvgl.scr_act(),1000,200)
wait.set_size(80,80)
wait.align(lvgl.ALIGN.CENTER,-70,0)
wait.add_style(background,0)
wait.add_style(indicator,lvgl.PART.INDICATOR)

Understanding This LVGL Code

Ok, now that you have seen all the code, let’s step through each section, so you can understand exactly what each part does. This will make it much easier to actually use spinners in your projects…

The Setup

First we need to import the LVGL module. This is also a good spot to put you screen setup code.

import lvgl

A Basic Spinner

Because spinners are usually used when you microcontroller is doing some hard task its best to make them as simple as possible so they don’t slow down your microcontroller.

We will start by creating a new spinner. “lvgl.scr_act()” tells the computer to put the spinner on the main screen.

The first numbers is how many milliseconds it takes for the spinner to make one rotation, and the last number controls how big the blue part of the spinner is.

spin = lvgl.spinner(lvgl.scr_act(),900,60)

Then we will set the position of the spinner.

spin.set_pos(200,100)

And finally we set the size of the spinner.

spin.set_size(50,50)

This is all it takes to make a simple spinner.

Creating Some Styles

We are going to create one more spinner, and this one will have some styles added to it.

first we create the style for the background.

background = lvgl.style_t()

Then, we initialize our new style.

background.init()

After that, we are going to set the background color to a dark grey.

background.set_arc_color(lvgl.color_hex(0x555555))

Then, we will set the width of the arc.

background.set_arc_width(40)

Next, we make the style for the indicator.

indicator = lvgl.style_t()

Then, we initialize it.

indicator.init()

After that, we set the it’s color to a bright green.

indicator.set_arc_color(lvgl.color_hex(0x00FF00))

We are also going to make the edges of the indicator flat, normally they would be rounded.

indicator.set_arc_rounded(0)

And lastly, we will set the indicators width to 20 pixels.

indicator.set_arc_width(20)

The Last Spinner

We will start by creating a new spinner.

wait = lvgl.spinner(lvgl.scr_act(),1000,200)

Then we will set it’s size to 80*80 pixels.

wait.set_size(80,80)

We will align it to the center and then have it move 70 pixels to the left.

wait.align(lvgl.ALIGN.CENTER,-70,0)

The last thing we need to do is add the styles we created. First we add the background style like normal.

wait.add_style(background,0)

Then we add the indicator style to the indicator part.

wait.add_style(indicator,lvgl.PART.INDICATOR)

What Should Happen?

Once you run the program, the two spinners we created should spin. The big spinner should rotate slightly slower the the small one. For most projects, I would use the smaller spinner because it is simpler.

Once you task is done you should call .delete() on your spinner for example you would delete the to spinners we just created like this:

spin.delete()
wait.delete()

If you want a indicator that gives a little more information to the user, I would suggest looking into LVGL Bars.

Filed Under: Micropython

Building a Timer App with Micropython LVGL

February 2, 2023 by Baxter

timer set to 0 second and minute and not started

Like all things, to get good at making GUIs you need practice. Creating a timers is a good place to start. They are simple enough to build, but they still contain a few complex elements to get you thinking.

Below is some code that creates a timer app. Today we will go through the most important parts of the code, and see how it works. Don’t worry about the size of the code, its very repetitive but not terribly complicated.

To get this example to work you will need to add your screen setup code to the beginning of the code below. If you want more information, you can try How to get a LVGL Micropython screen to work.

import lvgl

background = lvgl.obj(lvgl.scr_act())
background.set_size(320,240)
background.set_style_pad_all(0,0)

background.set_layout( lvgl.LAYOUT_FLEX.value )
background.set_flex_flow( lvgl.FLEX_FLOW.COLUMN )
background.set_flex_align( lvgl.FLEX_ALIGN.CENTER, lvgl.FLEX_ALIGN.CENTER, lvgl.FLEX_ALIGN.CENTER )

content = lvgl.obj(background)
content.set_style_pad_all(0,0)
content.set_size(300,150)

inputs = lvgl.obj(background)
inputs.set_style_pad_all(0,0)
inputs.set_size(300,60)

num_l = []
for x in range(60):
    num_l.append(str(x))
num_s = '\n'.join(num_l)


min_r = lvgl.roller(content)
min_r.set_size(80,130)
min_r.align(lvgl.ALIGN.LEFT_MID,20,0)
min_r.set_options(num_s,lvgl.roller.MODE.INFINITE)
min_r.set_visible_row_count(4)

min_l = lvgl.label(content)
min_l.set_text(':min')
min_l.align(lvgl.ALIGN.CENTER,-25,0)

sec_r = lvgl.roller(content)
sec_r.set_size(80,130)
sec_r.align(lvgl.ALIGN.RIGHT_MID,-55,0)
sec_r.set_options(num_s,lvgl.roller.MODE.INFINITE)
sec_r.set_visible_row_count(4)

sec_l = lvgl.label(content)
sec_l.set_text(':sec')
sec_l.align(lvgl.ALIGN.RIGHT_MID,-20,0)

start = lvgl.btn(inputs)
start.set_size(280,40)
start.center()
start_l = lvgl.label(start)
start_l.set_text('START')
start_l.center()





time_l = lvgl.label(content)
time_l.add_flag(lvgl.obj.FLAG.HIDDEN)
time_l.center()

reset = lvgl.btn(inputs)
reset.set_size(142,40)
reset.add_flag(lvgl.obj.FLAG.HIDDEN)
reset.align(lvgl.ALIGN.LEFT_MID,4,0)
reset_l = lvgl.label(reset)
reset_l.set_text('RESET')
reset_l.center()


pause = lvgl.btn(inputs)
pause.set_size(142,40)
pause.add_flag(lvgl.obj.FLAG.HIDDEN)
pause.align(lvgl.ALIGN.RIGHT_MID,-4,0)
pause_l = lvgl.label(pause)
pause_l.set_text('PAUSE')
pause_l.center()

resume = lvgl.btn(inputs)
resume.set_size(142,40)
resume.add_flag(lvgl.obj.FLAG.HIDDEN)
resume.align(lvgl.ALIGN.RIGHT_MID,-4,0)
resume_l = lvgl.label(resume)
resume_l.set_text('RESUME')
resume_l.center()


minute = 0
second = 0

def update_time(t):
    global minute,second
    
    if minute == 0 and second == 0:
        timer.pause()
        set_mode()
        alarm()
    else:
        if second == 0:
            second = 59
            minute -= 1
        else:
            second -= 1
        if second < 10:
            s = '0'+str(second)
        else:
            s = str(second)
        time_l.set_text(str(minute)+':'+s)
        print(minute,':',second)

timer = lvgl.timer_create(update_time,1000,None)
timer.set_repeat_count(-1)
timer.pause()


def set_mode():
    start.clear_flag(lvgl.obj.FLAG.HIDDEN)
    min_r.clear_flag(lvgl.obj.FLAG.HIDDEN)
    min_l.clear_flag(lvgl.obj.FLAG.HIDDEN)
    sec_r.clear_flag(lvgl.obj.FLAG.HIDDEN)
    sec_l.clear_flag(lvgl.obj.FLAG.HIDDEN)
    
    time_l.add_flag(lvgl.obj.FLAG.HIDDEN)
    pause.add_flag(lvgl.obj.FLAG.HIDDEN)
    reset.add_flag(lvgl.obj.FLAG.HIDDEN)
    resume.add_flag(lvgl.obj.FLAG.HIDDEN)
    
    
def timer_mode():
    start.add_flag(lvgl.obj.FLAG.HIDDEN)
    min_r.add_flag(lvgl.obj.FLAG.HIDDEN)
    min_l.add_flag(lvgl.obj.FLAG.HIDDEN)
    sec_r.add_flag(lvgl.obj.FLAG.HIDDEN)
    sec_l.add_flag(lvgl.obj.FLAG.HIDDEN)
    
    time_l.clear_flag(lvgl.obj.FLAG.HIDDEN)
    pause.clear_flag(lvgl.obj.FLAG.HIDDEN)
    reset.clear_flag(lvgl.obj.FLAG.HIDDEN)
    resume.add_flag(lvgl.obj.FLAG.HIDDEN)
    
    
def t(m,s):
    global minute,second
    minute = m
    second = s
    timer.reset()
    timer.resume()    
    
    
def start_handler(data):
    m = min_r.get_selected()
    s = sec_r.get_selected()
    timer_mode()
    if s < 10:
        s = '0' + str(s)
    s = str(s)
    
    time_l.set_text(str(m)+':'+s)
    t(m,int(s))

def pause_handler(data):
    timer.pause()
    pause.add_flag(lvgl.obj.FLAG.HIDDEN)
    resume.clear_flag(lvgl.obj.FLAG.HIDDEN)
    
def resume_handler(data):
    timer.resume()
    pause.clear_flag(lvgl.obj.FLAG.HIDDEN)
    resume.add_flag(lvgl.obj.FLAG.HIDDEN)
    
def reset_handler(data):
    timer.pause()
    set_mode()
    

pause.add_event_cb(pause_handler,lvgl.EVENT.CLICKED,None)
reset.add_event_cb(reset_handler,lvgl.EVENT.CLICKED,None)
resume.add_event_cb(resume_handler,lvgl.EVENT.CLICKED,None)
start.add_event_cb(start_handler,lvgl.EVENT.CLICKED,None)    


def alarm():
    cover = lvgl.obj(lvgl.scr_act())
    cover.set_size(320,240)
    cover.set_style_opa(lvgl.OPA._80,0)
    
    text = lvgl.label(cover)
    text.set_text('TIMER DONE')
    text.center()
    def delete(data):
        cover.delete()
    cover.add_event_cb(delete,lvgl.EVENT.RELEASED,None)
    text.add_event_cb(delete,lvgl.EVENT.RELEASED,None)

The Background

For the program to work, we need to create a background object. We set its size to cover the entire screen, and remove all the padding from it. We will also enable the flex layout for it.

background = lvgl.obj(lvgl.scr_act())
background.set_size(320,240)
background.set_style_pad_all(0,0)

background.set_layout( lvgl.LAYOUT_FLEX.value )
background.set_flex_flow( lvgl.FLEX_FLOW.COLUMN )
background.set_flex_align( lvgl.FLEX_ALIGN.CENTER, lvgl.FLEX_ALIGN.CENTER, lvgl.FLEX_ALIGN.CENTER )

Inside of that object, we create two more objects: one is for all of the objects that show the time and one is for all the buttons.

content = lvgl.obj(background)
content.set_style_pad_all(0,0)
content.set_size(300,150)

inputs = lvgl.obj(background)
inputs.set_style_pad_all(0,0)
inputs.set_size(300,60)
timer set to 9 min and 13 sec a=not started
timer running screen
timer finished screen

Screens

This program has three major parts, the first is the settings screen, the second is the screen that shows how much time is left, and the third is the time over screen.

 
timer set to 9 min and 13 sec a=not started

The Settings Screen

In the content area, we create two rollers with a range from 0-59 and a label next to each roller.

num_l = []
for x in range(60):
    num_l.append(str(x))
num_s = '\n'.join(num_l)


min_r = lvgl.roller(content)
min_r.set_size(80,130)
min_r.align(lvgl.ALIGN.LEFT_MID,20,0)
min_r.set_options(num_s,lvgl.roller.MODE.INFINITE)
min_r.set_visible_row_count(4)

min_l = lvgl.label(content)
min_l.set_text(':min')
min_l.align(lvgl.ALIGN.CENTER,-25,0)

sec_r = lvgl.roller(content)
sec_r.set_size(80,130)
sec_r.align(lvgl.ALIGN.RIGHT_MID,-55,0)
sec_r.set_options(num_s,lvgl.roller.MODE.INFINITE)
sec_r.set_visible_row_count(4)

sec_l = lvgl.label(content)
sec_l.set_text(':sec')
sec_l.align(lvgl.ALIGN.RIGHT_MID,-20,0

In the inputs area, we create the start button.

start = lvgl.btn(inputs)
start.set_size(280,40)
start.center()
start_l = lvgl.label(start)
start_l.set_text('START')
start_l.center()
 
timer running screen

The Timer Screen

This screen is a little harder because we have to make all of the objects invisible by default.

The only thing we will add to the content section is a label to show how much time is left in the timer. We make the label invisible by adding the HIDDEN flag.

time_l = lvgl.label(content)
time_l.add_flag(lvgl.obj.FLAG.HIDDEN)
time_l.center()

In the inputs area we add three buttons, all of them are invisible.

reset = lvgl.btn(inputs)
reset.set_size(142,40)
reset.add_flag(lvgl.obj.FLAG.HIDDEN)
reset.align(lvgl.ALIGN.LEFT_MID,4,0)
reset_l = lvgl.label(reset)
reset_l.set_text('RESET')
reset_l.center()


pause = lvgl.btn(inputs)
pause.set_size(142,40)
pause.add_flag(lvgl.obj.FLAG.HIDDEN)
pause.align(lvgl.ALIGN.RIGHT_MID,-4,0)
pause_l = lvgl.label(pause)
pause_l.set_text('PAUSE')
pause_l.center()

resume = lvgl.btn(inputs)
resume.set_size(142,40)
resume.add_flag(lvgl.obj.FLAG.HIDDEN)
resume.align(lvgl.ALIGN.RIGHT_MID,-4,0)
resume_l = lvgl.label(resume)
resume_l.set_text('RESUME')
resume_l.center()

How We Know the Time

Before we move on to the last screen, lets see how the timer works behind the scenes.

First of all there are two variables: minute and second. They hold how many minutes and seconds are left on the timer.

minute = 0
second = 0

The update Function

Next, we create a function to update the time.

def update_time(t):

In that function, we begin by importing the minute and second variables.

    global minute,second

Then, we check if the minute and second variables equal zero.

    if minute == 0 and second == 0:

If they do, we we call three functions: one to end the timer (timer.pause), one two go back to the start screen (set_mode), and one to turn on the end screen (alarm).

If you have a speaker connected to your board this is where you could signal an alarm.

        timer.pause()
        set_mode()
        alarm()

If they didn’t equal zero, we minus one second from the current time, and then print the new time to the terminal and to the screen.

    else:
        if second == 0:
            second = 59
            minute -= 1
        else:
            second -= 1
        if second < 10:
            s = '0'+str(second)
        else:
            s = str(second)
        time_l.set_text(str(minute)+':'+s)
        print(minute,':',second)

We want that function to be called once every second, so we will use a LVGL timer to do that.

timer = lvgl.timer_create(update_time,1000,None)

We then make the timer repeat forever.

timer.set_repeat_count(-1)

Finally we turn it off for right now.

timer.pause()

The Helper Functions

There are also a few functions used to help control the timer.

The set_mode function makes all the start screen objects visible and hides the rest of the objects.

def set_mode():
    start.clear_flag(lvgl.obj.FLAG.HIDDEN)
    min_r.clear_flag(lvgl.obj.FLAG.HIDDEN)
    min_l.clear_flag(lvgl.obj.FLAG.HIDDEN)
    sec_r.clear_flag(lvgl.obj.FLAG.HIDDEN)
    sec_l.clear_flag(lvgl.obj.FLAG.HIDDEN)
    
    time_l.add_flag(lvgl.obj.FLAG.HIDDEN)
    pause.add_flag(lvgl.obj.FLAG.HIDDEN)
    reset.add_flag(lvgl.obj.FLAG.HIDDEN)
    resume.add_flag(lvgl.obj.FLAG.HIDDEN)

The timer_mode function does the opposite of the previous function it, hides the start screen objects and makes the timer objects visible.

    
    
def timer_mode():
    start.add_flag(lvgl.obj.FLAG.HIDDEN)
    min_r.add_flag(lvgl.obj.FLAG.HIDDEN)
    min_l.add_flag(lvgl.obj.FLAG.HIDDEN)
    sec_r.add_flag(lvgl.obj.FLAG.HIDDEN)
    sec_l.add_flag(lvgl.obj.FLAG.HIDDEN)
    
    time_l.clear_flag(lvgl.obj.FLAG.HIDDEN)
    pause.clear_flag(lvgl.obj.FLAG.HIDDEN)
    reset.clear_flag(lvgl.obj.FLAG.HIDDEN)
    resume.add_flag(lvgl.obj.FLAG.HIDDEN)
    
    

The T function is used to set the timer. It sets the minute and second variables and then resets and starts the timer.

def t(m,s):
    global minute,second
    minute = m
    second = s
    timer.reset()
    timer.resume()   

The Last Screen and the Handlers

These are the last few functions.

The start_handler is called when the the start button is clicked. It initializes the timer with the values from the rollers.

def start_handler(data):
    m = min_r.get_selected()
    s = sec_r.get_selected()
    timer_mode()
    if s < 10:
        s = '0' + str(s)
    s = str(s)
    
    time_l.set_text(str(m)+':'+s)
    t(m,int(s))

The pause_handler is connected to the pause button and obliviously pauses the timer.

def pause_handler(data):
    timer.pause()
    pause.add_flag(lvgl.obj.FLAG.HIDDEN)
    resume.clear_flag(lvgl.obj.FLAG.HIDDEN)

The resume_handler and reset_handler should be self explanatory.

def resume_handler(data):
    timer.resume()
    pause.clear_flag(lvgl.obj.FLAG.HIDDEN)
    resume.add_flag(lvgl.obj.FLAG.HIDDEN)
    
def reset_handler(data):
    timer.pause()
    set_mode()

The last step for these handlers is to connect them to the correct objects for example the pause_handler is connected to the pause button.

pause.add_event_cb(pause_handler,lvgl.EVENT.CLICKED,None)

Here is the rest of them.

reset.add_event_cb(reset_handler,lvgl.EVENT.CLICKED,None)
resume.add_event_cb(resume_handler,lvgl.EVENT.CLICKED,None)
start.add_event_cb(start_handler,lvgl.EVENT.CLICKED,None)    

There is one last function and then we are done with this program.

timer finished screen

The Alarm

This function is called when the timer is finished. Its creates the timer over screen which is simply an object that covers the entire screen with the text “TIMER DONE”. If you click the object it disappears and you can use the program as usual.

def alarm():
    cover = lvgl.obj(lvgl.scr_act())
    cover.set_size(320,240)
    cover.set_style_opa(lvgl.OPA._80,0)
    
    text = lvgl.label(cover)
    text.set_text('TIMER DONE')
    text.center()
    def delete(data):
        cover.delete()
    cover.add_event_cb(delete,lvgl.EVENT.RELEASED,None)
    text.add_event_cb(delete,lvgl.EVENT.RELEASED,None)

What Next?

The hole point of this program is to get practice with LVGL, so once you get it working, experiment around with it. The color and placement of some of the objects could be improved, and the most glaring problem is that the font for the current time is way to small.

Because enabling larger fonts takes a lot of work, I left that out of this program, but if you want to do it, you can find so more information on the process at How to Change Micropython LVGL Fonts.

Filed Under: Micropython

How to Use Micropython LVGL Bars

February 1, 2023 by Baxter

three lvgl bars set to about 75%

Output Bars are one of those things we see all of the time. You can find them in scroll bars, progress bars, etc. They are all over the place, because bars allow you to show numbers in a more visual way.

Today I will show you how to create and edit LVGL bars. We will work through the three main types of bars and setting each of there values.

Setting Up a Micropython Screen

Obviously, if you are creating a program that draws things on a screen, you have to have access to a screen.

Because there are so many different screens out there you could use with micropython, I can’t add support for all of them, so to make this example work, you will need to add your screen’s setup code to the beginning of the example.

There is a little more information on this process at How to get a LVGL Micropython Screen to Work if you aren’t familiar on how it’s done.

Here is the code we will work through.

import lvgl


bar1 = lvgl.bar(lvgl.scr_act())
bar1.set_size(100,20)
bar1.align(lvgl.ALIGN.CENTER,-100,0)
bar1.set_value(50,lvgl.ANIM.OFF)
bar1.set_range(0,100)
bar1.set_style_radius(0,0)
bar1.set_style_radius(0,lvgl.PART.INDICATOR)

bar2 = lvgl.bar(lvgl.scr_act())
bar2.set_size(10,60)
bar2.center()
bar2.set_value(50,lvgl.ANIM.OFF)
bar2.set_range(-50,50)
bar2.set_mode(lvgl.bar.MODE.SYMMETRICAL)

bar3 = lvgl.bar(lvgl.scr_act())
bar3.set_size(100,15)
bar3.align(lvgl.ALIGN.CENTER,100,0)
bar3.set_value(50,lvgl.ANIM.OFF)
bar3.set_range(0,100)
bar3.set_mode(lvgl.bar.MODE.RANGE)

import time
while True:
    for x in range(101):
        bar1.set_value(x,lvgl.ANIM.ON)
        bar2.set_value(x-50,lvgl.ANIM.ON)
        bar3.set_value(max(min(x+10,100),10),lvgl.ANIM.ON)
        bar3.set_start_value(max(min(x-10,99),0),lvgl.ANIM.ON)
        time.sleep(0.05)
    for x in range(100,-1,-1):
        bar1.set_value(x,lvgl.ANIM.ON)
        bar2.set_value(x-50,lvgl.ANIM.ON)
        bar3.set_value(max(min(x+10,100),10),lvgl.ANIM.ON)
        bar3.set_start_value(max(min(x-10,99),0),lvgl.ANIM.ON)
        time.sleep(0.05)

The Setup

The very first thing, we need to do is import the LVGL module. Around here is a good place to put your screen setup code.

import lvgl

Creating the First Bar

We will start by creating a new bar.

bar1 = lvgl.bar(lvgl.scr_act())

Next, we will set the size of our bar.

bar1.set_size(100,20)

After that, we will align our bar to a hundred pixels left of the center of the screen.

bar1.align(lvgl.ALIGN.CENTER,-100,0)

Then we will set the default value of our bar.

bar1.set_value(50,lvgl.ANIM.OFF)

We will also set the range.

bar1.set_range(0,100)

Next we are going to make this bar rectangular, normally bars have rounded edges but that is not what we want here.

bar1.set_style_radius(0,0)

To make the bar completely rectangular we need to remove the rounded edges from the indicator too.

bar1.set_style_radius(0,lvgl.PART.INDICATOR)

Making the Vertical Bar

The next bar we want to create is the vertical one. LVGL makes this simple, first you make a bar like normal.

bar2 = lvgl.bar(lvgl.scr_act())

Then, all you have to do is make the height of the bar bigger than the width.

bar2.set_size(10,60)

To make the bar fit, we put it in the center of the screen.

bar2.center()

After that, we will set the default value.

bar2.set_value(50,lvgl.ANIM.OFF)

Next, we set the range of this bar.

bar2.set_range(-50,50)

Bars have modes. We are going to put this bar into symmetrical mode which means that the bar fills from the middle out, looking at the middle bar in the picture above might help.

bar2.set_mode(lvgl.bar.MODE.SYMMETRICAL)

The Range Bar

We want this last bar to be in range mode. In range mode you can set both the bar’s end value and also the bar’s start value. In the picture above you can see that this bar, the third to the right, has only a little dot colored blue.

We begin as usual by creating an new bar.

bar3 = lvgl.bar(lvgl.scr_act())

Then, we do all the normal things set its size, range, value, etc.

bar3.set_size(100,15)
bar3.align(lvgl.ALIGN.CENTER,100,0)
bar3.set_value(50,lvgl.ANIM.OFF)
bar3.set_range(0,100)

And finally, we set it to range mode.

bar3.set_mode(lvgl.bar.MODE.RANGE)

Creating Some Values

You probably cannot tell it from the picture, but the bars are supposed to be constantly changing their value so lets make a little code that does that for us.

First the time module needs imported.

import time

Then, we will create a infinite loop.

while True:

After that, we will create a for loop that counts from 0 up to 100.

    for x in range(101):

X represents a number from 0 to 100 so we will simple set the value of bar1 to x.

        bar1.set_value(x,lvgl.ANIM.ON)

The range of bar2 is from -50 to 50 and because x can be bigger that 50, we subtract 50 from x before we use it.

        bar2.set_value(x-50,lvgl.ANIM.ON)

Bar3 has two values we have to set. For the first value, we add 10 to x then we do a little constraining math on x so it never dips below 10 or above 100

        bar3.set_value(max(min(x+10,100),10),lvgl.ANIM.ON)

Then, we set the start value to x minus 10 constrained to 0 through 99.

        bar3.set_start_value(max(min(x-10,99),0),lvgl.ANIM.ON)

Next, we tell the computer to wait 0.05 seconds before it resets the value of the bars.

        time.sleep(0.05)

Now that we have made our first for loop, we will create one more. This for loop is just like the previous, accept instead of counting up from 0-100 it counts down from 100-0.

    for x in range(100,-1,-1):
        bar1.set_value(x,lvgl.ANIM.ON)
        bar2.set_value(x-50,lvgl.ANIM.ON)
        bar3.set_value(max(min(x+10,100),10),lvgl.ANIM.ON)
        bar3.set_start_value(max(min(x-10,99),0),lvgl.ANIM.ON)
        time.sleep(0.05)

What Should Happen?

When you run the program, three bars should appear. Each should slowly increase in value and then slowly decrease and that process should repeat for ever. Don’t forget that you can stop the program by hitting the ctrl+c keys.

Bars have tons of customizability. We only went into some of things you can do like changing size and squaring, but you can also change their colors, rotate them, or shrink their indicators. That’s what makes bars so useful, you can put them in any GUI and with a little styling, they feel like they have always belonged.

If you like bars you might like arcs. They are like a bar that has been curved. If you want to now more on how to use them check out LVGL Arcs.

Filed Under: Micropython

How to Use Micropython LVGL LEDs

January 31, 2023 by Baxter

lvgl leds used to make a circle and a seven segment display

LVGL has several widgets intended to polish off your projects. LEDs are one of them. They are very simple to use because they do all of the color math and styling for you.

Today, I will show you how to create a LED and also how to use LEDs to make a 7 segment display. We will do that by walking through how the code down below works.

Setting Up a Micropython Screen

Obviously, if you are creating a program that draws things on a screen, you have to have access to a screen.

Because there are so many different screens out there, I can’t add support for all of them, so to make this example work, you will need to add your screen’s setup code to the beginning of the example.

There is a little more information on this process at How to get a LVGL Micropython Screen to Work if you aren’t familiar on how it’s done.

Here is that code

import lvgl
from time import sleep

led1 = lvgl.led(lvgl.scr_act())
led1.set_brightness(100)
led1.set_color(lvgl.color_hex(0x00ff00))
led1.set_pos(160,45)
led1.set_size(150,150)



def add_led(x,y,w,h):
    led = lvgl.led(lvgl.scr_act())
    led.set_style_radius(0,0)
    led.set_pos(x,y)
    led.set_size(w,h)
    led.off()
    return led

t = add_led(20,40,70,10)
m = add_led(20,110,70,10)
b = add_led(20,180,70,10)

tl = add_led(10,50,10,60)
tr = add_led(90,50,10,60)

bl = add_led(10,120,10,60)
br = add_led(90,120,10,60)


def display(top,middle,bottom,top_left,top_right,bottom_left,bottom_right):
    if top:
        t.on()
    else:
        t.off()   
    if middle:
        m.on()
    else:
        m.off()
    if bottom:
        b.on()
    else:
        b.off()
        
    if top_left:
        tl.on()
    else:
        tl.off()
    if top_right:
        tr.on()
    else:
        tr.off()
        
    if bottom_left:
        bl.on()
    else:
        bl.off()
    if bottom_right:
        br.on()
    else:
        br.off()

def update(num):
    if num == 0:
        display(1,0,1,1,1,1,1)        
    if num == 1:
        display(0,0,0,0,1,0,1)
    if num == 2:
        display(1,1,1,0,1,1,0)
    if num == 3:
        display(1,1,1,0,1,0,1)
    if num == 4:
        display(0,1,0,1,1,0,1)
    if num == 5:
        display(1,1,1,1,0,0,1)
    if num == 6:
        display(1,1,1,1,0,1,1)   
    if num == 7:
        display(1,0,0,0,1,0,1)
    if num == 8:
        display(1,1,1,1,1,1,1)
    if num == 9:
        display(1,1,0,1,1,0,1)

while True:
    for x in range(10):
        update(x)
        led1.set_brightness(x*20)
        sleep(1)

Understanding This Code

Just looking at a block of code is not always very informational. To make how this micropython code works more clear, lets walk through the lines of code above and see what they do.

The Setup

Before we can use LVGL we need to import it. This is also a good spot to add your screen setup code.

import lvgl

Some of this code requires special timing, so we import the sleep function from the time module.

from time import sleep

Creating a Simple LED

Will start by creating a LED.

led1 = lvgl.led(lvgl.scr_act())

Next we will set that LED’s brightness to 100. The brightness range is between 0 and 255.

led1.set_brightness(100)

Then we will set the color of the LED to green.

led1.set_color(lvgl.color_hex(0x00ff00))

After that we set its position.

led1.set_pos(160,45)

Lastly we set its size to 150 pixels wide by 150 pixels tall.

led1.set_size(150,150)

Simplifying the Process

We are going to have to create a lot of LEDs, so lets create a function that will make that process easier.

To start we define the function.

def add_led(x,y,w,h):

After that we create a new LED.

    led = lvgl.led(lvgl.scr_act())

Then we set the radius to zero. This makes the LEDs square instead of Circular.

    led.set_style_radius(0,0)

Next we set the position of the LED and its size.

    led.set_pos(x,y)
    led.set_size(w,h)

After that we turn it off which is the same as setting the brightness to 0.

    led.off()

Finally we return the LED object we created, so the program can use it.

    return led

Creating Lots of LEDs

For the 7 segment display, we need 7 LEDs so lets create them.

We will use that function we made earlier to create our LEDS. It takes four numbers; the first two numbers are the x and y coordinates of the led and the second two numbers are the width and height of the led.

We start by creating the top,middle, and bottom LEDs.

t = add_led(20,40,70,10)
m = add_led(20,110,70,10)
b = add_led(20,180,70,10)

Then we create the top_left and top_right LEDs.

tl = add_led(10,50,10,60)
tr = add_led(90,50,10,60)

Lastly we create the bottom left and bottom right LEDs.

bl = add_led(10,120,10,60)
br = add_led(90,120,10,60)

The Control Function

The next thing we are going to do is create a function to control all of those LEDs.

We begin by defining it.

def display(top,middle,bottom,top_left,top_right,bottom_left,bottom_right):

After that we check if the top LED should be on.

 if top:
        t.on()
    else:
        t.off()   

I won’t bore you with the process, but we do the exact same thing for the rest of the LEDs.

    if middle:
        m.on()
    else:
        m.off()
    if bottom:
        b.on()
    else:
        b.off()
        
    if top_left:
        tl.on()
    else:
        tl.off()
    if top_right:
        tr.on()
    else:
        tr.off()
        
    if bottom_left:
        bl.on()
    else:
        bl.off()
    if bottom_right:
        br.on()
    else:
        br.off()

Numbers to LEDs

We want the leds to display numbers. So we are going to create one last function. We will give this function a number like 4 and it figures out which LEDs should be turned on to show the number four.

As usual we start by defining the function.

def update(num):

Then we check if the number given to this function is the number zero.

    if num == 0:

If it was we turn on the correct LEDs to display zero.

        display(1,0,1,1,1,1,1)        

We repeat this process for the numbers 1-9.

    if num == 1:
        display(0,0,0,0,1,0,1)
    if num == 2:
        display(1,1,1,0,1,1,0)
    if num == 3:
        display(1,1,1,0,1,0,1)
    if num == 4:
        display(0,1,0,1,1,0,1)
    if num == 5:
        display(1,1,1,1,0,0,1)
    if num == 6:
        display(1,1,1,1,0,1,1)   
    if num == 7:
        display(1,0,0,0,1,0,1)
    if num == 8:
        display(1,1,1,1,1,1,1)
    if num == 9:
        display(1,1,0,1,1,0,1)

Generating The Numbers

The last thing we want to do is make the LEDs display different numbers.

First we create a infinite loop.

while True:

Next we use a for loop to generate our numbers.

    for x in range(10):

Then we update the LEDs to equal x ,which will be a number from 0-9, and we also change the brightness of the original LED.

        update(x)
        led1.set_brightness(x*20)

Lastly, we tell the computer to wait one second before it does anything else.

        sleep(1)

What Should Happen

Once you run the above code, you should see a number next to a green circle. If you let the program keep running the number get bigger and bigger and the circle get brighter. Eventually the number reaches 9 and the hole thing starts back from 0 and the circle goes dark.

LEDs are great because they can be made into almost anything. You can create complex displays like we did today, or you can create a simple dot that turns on and off.

A lot of the control of LEDS comes from using styles, if you want to learn more about how they work LVGL Style Examples might interest you.

Filed Under: Micropython

How to Change Micropython LVGL Fonts

January 30, 2023 by Baxter

screen displaying multiple lvgl fonts

Being able to change fonts is extremely important. Unfortunately It takes a bit of work to get new fonts onto microcontrollers. Thankfully once they are there, LVGL makes it easy to use them.

Today I will show you how to add some extra fonts to your board and use those fonts with LVGL. There is also some example code at the bottom to test if your fonts work.

Compiling

To add new fonts, you need to recompile micropython LVGL. This tutorial is not intended to teach you how to compile micropython LVGL. If you are not familiar on how that is done, the best place to find information is in the readme files in the micropython LVGL source code.

Make sure to follow the instructions at the main readme and your boards readme, for example here is what I use Micropython Readme, Esp32 Readme, and Core2 Readme.

Adding Builtin Fonts

To add a font, we need to enable it in the lv_conf.h file. Of course the first step is to find the lv_conf.h file.

First find the lv_micropython folder. If you have compiled LVGL before, then this should already be downloaded onto your computer. Usually its put in your root directory.

Inside of the lv_micropython folder, find and enter the lib folder.

Then find and enter the lv_binding folder. Your path should look something like: Home/lv_micropython/lib/lv_binding

Inside of the lv_binding folder you will find the lv_conf.h file.

Editing lv_conf.h

The lv_conf.h file controls pretty much all of LVGL. Open it in your text editor and around line 346, you should see a list of fonts similar to the picture below.

screenshot of lv_conf.h file

In each font name there is a number, that number is equal to how many pixels that font is high for example in LV_FONT_MONTSERRAT_30 each letter should be 30 pixels high.

Next to each font name there is ether a 1 or a 0. If the number is a one the font is added to LVGL if it is zero its not added. You can see from the picture above that I have the 14,16,20,30, and 46 size fonts enabled.

All you have to do now is add a one next to the fonts you want, save the file, and then recompile LVGL as normal, and the fonts you chose will be added to LVGL.

Fonts take a lot of memory, so if you add to many fonts your computer might not have enough memory to work. Its usually best if you only enable the fonts you know you will use.

Using Your Fonts with LVGL

Styles are what controls fonts. The most basic way you can use styles to change the font is like this.

YourObject.set_style_text_font(lvgl.YourFontName,0)

You will need to replace YourObject with what ever object you want to change the font of, and replace YourFontName with the name of your font, usually in lower case. Below is some code that shows this process in action.

The Screen

To get this or any code for that matter to work, you need to enable a screen for LVGL. If you don’t know how to do that then there is a little more information at How to get a LVGL Micropython screen to work.

import lvgl

big_lb = lvgl.label(lvgl.scr_act())
big_lb.set_style_text_font(lvgl.font_montserrat_46,0)
big_lb.align(lvgl.ALIGN.CENTER,0,-30)
big_lb.set_text('Hello 46')

large_lb = lvgl.label(lvgl.scr_act())
large_lb.set_style_text_font(lvgl.font_montserrat_30,0)
large_lb.align(lvgl.ALIGN.CENTER,0,0)
large_lb.set_text('Hello 30')

mid_lb = lvgl.label(lvgl.scr_act())
mid_lb.set_style_text_font(lvgl.font_montserrat_20,0)
mid_lb.align(lvgl.ALIGN.CENTER,0,20)
mid_lb.set_text('Hello 20')

default_lb = lvgl.label(lvgl.scr_act())
default_lb.set_style_text_font(lvgl.font_montserrat_16,0)
default_lb.align(lvgl.ALIGN.CENTER,0,35)
default_lb.set_text('Hello 16')

Other Options

You now can enable any of the default fonts LVGL has available but you might want a little more variaty for your projects. Thankfully you can add any font you want to LVGL. You just have to process them with LVGL Font Converter.

If you don’t want to go through that process there is a way around it. Down near the bottom of the lv_conf.h file there are some settings for enabling TTF fonts.

If enabled it is possible to load a normal font from the file system, meaning this method doesn’t even require you to recompile the the firmware when you add a new font.

In the code above we had to create a individual label for for each font we wanted to add. Because of how annoying this is, LVGL has the span. Spans are like labels but you can have more than one type of font in them.

If you are interested in spans you can learn more at LVGL Spans.

Filed Under: Micropython

How to use Micropython LVGL Dropdown Boxes

January 23, 2023 by Baxter

LVGL dropdown box with different times that you can select.

When creating a GUI, there is never enough room on the screen. You want to add this list here or that button there and there is no room left. Dropdown boxes are the age old trick to free up some space.

Dropdown boxes free space by shrinking lists, which usually take up a lot of your screen, into a small button. If you tap the button the list appears, then you can select the option you want and the list disappears again.

Today I will show you how to create, modify, and style dropdown boxes.

Setting Up a Micropython Screen

Obviously, if you are creating a program that draws things on a screen, you have to have access to a screen.

Because there are so many different screens out there, I can’t add support for all of them, so to make this example work, you will need to add your screen’s setup code to the beginning of the example.

There is a little more information on this process at How to get a LVGL Micropython Screen to Work if you aren’t familiar on how it’s done.

Here is the Example

import lvgl

drop = lvgl.dropdown(lvgl.scr_act())
drop.align(lvgl.ALIGN.CENTER,0,-50)

drop.set_dir(lvgl.DIR.BOTTOM)
drop.set_symbol('')
drop.set_selected_highlight(False)
drop.set_text('Time')

start = ['12:00','3:30','9:30','7:00','2:00','5:00']
opt = '\n'.join(start)

drop.set_options(opt)

def print_select(data):
    dr = data.get_target()
    ID = dr.get_selected()
    name = start[ID]
    print(name)
    
drop.add_event_cb(print_select,lvgl.EVENT.VALUE_CHANGED,None)

ls = drop.get_list()
ls.set_style_bg_color(lvgl.color_hex(0xDDFFFF),0)
ls.set_style_max_height(100,0)

Understanding This Code

Just looking at a block of code is not always very informational. To make how this micropython code works more clear, lets walk through the lines of code above and see what they do.

The Setup

First we need to import LVGL this is also a good spot to put your screen setup code.

import lvgl

The Dropdown

To begin, we create a new dropdown box.

drop = lvgl.dropdown(lvgl.scr_act())

Then we align it to 50 pixels above the center of the screen.

drop.align(lvgl.ALIGN.CENTER,0,-50)

Next we select which direction we want the list to pop out of.

drop.set_dir(lvgl.DIR.BOTTOM)

Dropdown boxes usually have a symbol next to their text. For this example, we don’t want that, so we set the symbol to ” (aka. blank text).

drop.set_symbol('')

Dropdown boxes can also highlight the list item that was last clicked. We will disable that too.

drop.set_selected_highlight(False)

After that we will set the default text of our dropdown box.

drop.set_text('Time')

Setting the Options

The whole point of the dropdown box is that it gives us a list of options and we select one. Obliviously before it can do that, it has to have a list of options, so it’s finally time to create our list of options.

To start we create a list of texts.

start = ['12:00','3:30','9:30','7:00','2:00','5:00']

Then we format the list correctly. The code below turns a list like [ ’12:00′,’3:30′,’9:30′] into the string ’12:00\n3:30\n9:30′.

opt = '\n'.join(start)

Lastly, we add the correctly formatted list of options to the dropdown box.

drop.set_options(opt)

Finding the Clicked Option

The next major thing we need to do is find the clicked option. We do that by using LVGL events.

Firstly, we need to create a new function.

def print_select(data):

Inside it we run some code that finds the object (aka. our dropdown box) that called this function.

    dr = data.get_target()

Then we use that object to find the currently selected option.

    ID = dr.get_selected()

After that we use the list options that we originally created to figure out the text of the selected option.

    name = start[ID]

Finally we print that text to the terminal.

    print(name)

Hooking Up That Function

We just created our event handler function but now we need to connect it to the dropdown box for it to work.

drop.add_event_cb(print_select,lvgl.EVENT.VALUE_CHANGED,None)

Styling the List

The last thing we want to do is style the list that appears when the dropdown is clicked.

First, we collect the list object.

ls = drop.get_list()

Then we will change the background.

ls.set_style_bg_color(lvgl.color_hex(0xDDFFFF),0)

And finally, we set the maximum size of our list.

ls.set_style_max_height(100,0)

What Should Happen

Once you run this program you should see a button that says Time. If you click it, a list will appear with different times on it and if you click one of those times, the list will disappear and the time you clicked will appear in your REPL window.

Dropdowns are useful when you don’t have lots of screen space or you are just trying to make your GUI interesting.

If you have a little more space on your screen, then LVGL rollers are a great alternative to dropdowns. Rollers make really cool GUIs but dropdowns still win when it comes to simplicity and ease of use.

Filed Under: Micropython

How to Draw Arcs and Circles with Micropython LVGL

January 19, 2023 by Baxter

A blue circle and 2 arcs that are draw on screen with lvgl.

Being able to draw basic shapes is extremely important in building a GUI. This tutorial is part of a series on drawing primitive shapes. Today we will explore arcs.

If you are not familiar with LVGL descriptors then I would suggest looking at the intro page for this series. How to Draw a Rectangle with Micropython LVGL

Arcs are the tool, that lets you draw rounded shapes. Today we will look at how you create and modify arcs and how to use an arc to create a circle.

Setting Up a Micropython Screen

Obviously if you are creating a program that draws things on a screen, you have to have access to a screen.

Because there or so many different screens out there, I can’t add support for all of them, so to make this code work, you will need to add your screen’s setup code to the beginning of the example.

There is a little more information on this process at How to get a LVGL Micropython Screen to Work if you aren’t familiar on how it’s done.

Here is the Example

import lvgl


def draw(data):    
    arc = lvgl.draw_arc_dsc_t()
    arc.init()
    arc.color = lvgl.color_hex(0x0000FF)
    arc.width = 50
    arc.rounded = 1
    
    spot = lvgl.point_t()
    spot.x = 120
    spot.y = 160
    
    arc2 = lvgl.draw_arc_dsc_t()
    arc2.init()
    arc2.color = lvgl.color_hex(0x00ff00)
    arc2.width = 20
    
    spot2 = lvgl.point_t()
    spot2.x = 260
    spot2.y = 120
    
    arc3 = lvgl.draw_arc_dsc_t()
    arc3.init()
    arc3.color = lvgl.color_hex(0xFF0000)
    arc3.width = 10
    arc3.rounded = 1
    
    spot3 = lvgl.point_t()
    spot3.x = 140
    spot3.y = 140
        
    draw_ctx = data.get_draw_ctx()
    draw_ctx.arc(arc,spot,50,90,89) 
    draw_ctx.arc(arc2,spot2,55,90,-90)
    draw_ctx.arc(arc3,spot3,100,250,-60)

scr = lvgl.scr_act()
scr.add_event_cb(draw,lvgl.EVENT.DRAW_MAIN_END,None)
scr.invalidate()

Understanding This Code

Just looking at a block of code is not always very informational. To make how the code works more clear, lets walk through the lines of code above and see what they do.

The Setup

To begin, we need to import the LVGL module. This would be a good spot to add your screen setup code.

import lvgl

The Draw Function

When drawing shapes directly to the screen, it’s best if we put all the drawing related code inside of a draw function.

To do that we start by defining a new function.

def draw(data):

To make the inside of this function more understandable we will divide it into four parts.

The First Arc

First, we need to create a new arc descriptor (it’s like a LVGL style).

    arc = lvgl.draw_arc_dsc_t()

We also need to initialize it.

    arc.init()

Then, we will set the color of our arc to blue.

    arc.color = lvgl.color_hex(0x0000FF)

After that, we will set the width of the arc to 50 pixels. Later on we will set the radius of the arc to 50 as well. If the radius and width of an arc are the same then your arc will be a circle.

Something to keep in mind is that we can control how much of an arc is visible, so even thought this arc starts out as a circle that doesn’t mean it will be a perfect circle when we are done with it.

    arc.width = 50

Next, we tell the computer that we want rounded corners.

    arc.rounded = 1

Our next step is to create a LVGL point object.

    spot = lvgl.point_t()

Then, we will set the x and y coordinates of that point.

    spot.x = 120
    spot.y = 160

The Second Arc

This arc is very similar to the last one. We start by creating and initializing a new arc descriptor.

    arc2 = lvgl.draw_arc_dsc_t()
    arc2.init()

After that, we set the color to blue and the width to 20 pixels.

    arc2.color = lvgl.color_hex(0x00ff00)
    arc2.width = 20

Then, we create a new point and set its x and y values.

    spot2 = lvgl.point_t()
    spot2.x = 260
    spot2.y = 120

The Third Arc

First, we create a new arc and initilise it.

    arc3 = lvgl.draw_arc_dsc_t()
    arc3.init()

Then, we set its color and width, and we will make it have rounded corners as well.

    arc3.color = lvgl.color_hex(0xFF0000)
    arc3.width = 10
    arc3.rounded = 1

After that, we create a new point and set its x and y values.

    spot3 = lvgl.point_t()
    spot3.x = 140
    spot3.y = 140

 Making the Arcs Visible

We have created three sets of arc descriptors and their points. Now we need to use draw_ctx to actually draw them onto the screen.

To begin, lets get the draw_ctx variable.

    draw_ctx = data.get_draw_ctx()

Then, we will use it to draw the first arc. The three numbers mean the following things:

The 50 tells the computer to draw an arc that has a radius of 50 pixels.

The the 90 tells the computer to start drawing the arc at 90 degrees.

The 89 tells it to end the arc at 89 degrees. Because LVGL draws arcs clockwise it will start the arc at 90 degrees and go all the way round until it stop at 89 degrees. That is why this arc looks like a circle.

    draw_ctx.arc(arc,spot,50,90,89) 

After that, we draw the second arc.

    draw_ctx.arc(arc2,spot2,55,90,-90)

Finally, we draw the last arc.

    draw_ctx.arc(arc3,spot3,100,250,-60)

Running the Draw Function

We just made a function that draws three arcs. The last step we have to do is connect it to the object that it will draw on.

We are going to use the screen object but first we have to retrieve it.

scr = lvgl.scr_act()

Then, we add the draw function to the screen object.

scr.add_event_cb(draw,lvgl.EVENT.DRAW_MAIN_END,None)

Finally, we invalidate the screen so the arcs get drawn.

scr.invalidate()

What You Should See

Once you run the code above, your screen should display a blue circle, a red arc, and a green arc. If you play around with the width, radius, x, y, start_angle, and end_angle, you can create pretty much any rounded shape you want.

What makes arcs super useful is the control you have. Not only can you draw arcs on the screen with minimal memory usage but you can edit any arcs that was already going to be draw which means you can customize the widgets that already exist to suit your needs.

If you want to know how to use some more draw descriptors you might be interested in LVGL rectangles or LVGL polygons.

Filed Under: Micropython

How to use Micropython LVGL Button Matrixes

January 17, 2023 by Baxter

Simple calculator made with lvgl button matrix

If you have ever had to create a program that uses lots of buttons, you know what a pain it can be to have to create, style, and position each one. That is why button matrixes were created.

The button matrix groups all the buttons together. This makes it easy to apply styles to the entire group and means it takes less memory to create and use the buttons.

Today, I will show you how to create and edit your own button matrix. We will do that by walking through some example code, which creates a button matrix for a simple calculator.

The Screen

To get the code to work with your screen, you will need to add your screen setup code to the beginning of it. If you want more information, you can try How to get a LVGL Micropython screen to work.

Here is the code

import lvgl

matrix = lvgl.btnmatrix(lvgl.scr_act())

MAP = ['7','8','9','+','\n',
       '4','5','6','-','\n',
       '1','2','3','=','\n',
       '0',lvgl.SYMBOL.REFRESH,
       None]

matrix.set_map(MAP)
matrix.set_btn_width(12,3)
matrix.set_btn_width(13,1)

matrix.set_size(320,150)
matrix.align(lvgl.ALIGN.BOTTOM_MID,0,0)

matrix.set_btn_ctrl(3,lvgl.btnmatrix.CTRL.CHECKABLE)
matrix.set_btn_ctrl(7,lvgl.btnmatrix.CTRL.CHECKABLE)
matrix.set_btn_ctrl(11,lvgl.btnmatrix.CTRL.CHECKABLE)

matrix.set_one_checked(True)

label = lvgl.label(lvgl.scr_act())
label.align(lvgl.ALIGN.TOP_MID,0,20)
label.set_text('')

def update_button(data):
    btn_matx = data.get_target() 
    btn_id = btn_matx.get_selected_btn()
    btn_text = btn_matx.get_btn_text(btn_id)
    if btn_text == lvgl.SYMBOL.REFRESH:
        label.set_text('')
    elif btn_text == '=':
        try:
            label.set_text(str(eval(label.get_text())))
        except:
            pass
    else:
        label.set_text(label.get_text() + btn_text) 
        
matrix.add_event_cb(update_button,lvgl.EVENT.VALUE_CHANGED,None)

Understanding the Code

Now that you have seen the code, lets walk through it to figure out how it works.

The Setup

To begin, we need to import the LVGL module. Around here is a good place to put your screen setup code.

import lvgl

Creating the Button Matrix

We will start by creating an empty button matrix.

matrix = lvgl.btnmatrix(lvgl.scr_act())

Next ,we will create a list of names for the buttons in our button matrix.

MAP = ['7','8','9','+','\n',
       '4','5','6','-','\n',
       '1','2','3','=','\n',
       '0',lvgl.SYMBOL.REFRESH,
       None]

How the List Works

For the most part the list above is just a bunch of text, but there are three special types of items in the list.

First there is the text ‘\n’, whenever a button matrix sees this it starts a new row of buttons. Without this command all the buttons in a button matrix would be squeezed into one row.

Secondly, there is the code lvgl.SYMBOL.REFRESH. A buttons label can be any kind of string. LVGL considers words and numbers as strings but it also considers symbols as strings. lvgl.SYMBOL.REFRESH is one of the special symbols available to LVGL.

Thirdly, The None at the end of the list is part of the background code. I will not bore you with why it’s there, but you should always put a None at the end of you list of buttons.

Now that we have created our list of button labels, we need to add it to the button matrix.

matrix.set_map(MAP)

The last row has only two buttons in it: ‘0’ and REFRESH SYMBOL. Normally the buttons would be the same size, but for this example we want the the 0 button to be three times bigger that the REFRESH button.

First we tell the matrix that button 12,the zero button, should have a relative size of three.

matrix.set_btn_width(12,3)

Then we tell the matrix that button 13,the REFRESH button, should have a relative size of one. Because both buttons are in the same row that makes the zero button three times bigger than the REFRESH button.

matrix.set_btn_width(13,1)

We are also going to set the size of the the button matrix and align it to the bottom of the screen to make it look nicer.

matrix.set_size(320,150)
matrix.align(lvgl.ALIGN.BOTTOM_MID,0,0)

Next we want the ‘+’, ‘-‘, and ‘=’ buttons to be checkable. If you tap a checkable button it turns on and if tap it again, it will turn of. So we set the the ctrl checkable on each of those buttons.

matrix.set_btn_ctrl(3,lvgl.btnmatrix.CTRL.CHECKABLE)
matrix.set_btn_ctrl(7,lvgl.btnmatrix.CTRL.CHECKABLE)
matrix.set_btn_ctrl(11,lvgl.btnmatrix.CTRL.CHECKABLE)

Then we will turn one_check on. The feature just makes sure that only one button can be checked at the same time.

matrix.set_one_checked(True)

The Label

We are going to create a label that will display the buttons we have pressed. To start we create an empty label object.

label = lvgl.label(lvgl.scr_act())

Then we align it to the middle of the top of the screen.

label.align(lvgl.ALIGN.TOP_MID,0,20)

Then we will set the text so the default is blank.

label.set_text('')

The LVGL Event Handler

Event handlers are functions that are run when ever something important happens. We need to create one that runs when the button matrix is clicked.

To begin we create a new function.

def update_button(data):

After that we get the object that called this function.

    btn_matx = data.get_target() 

Then we use that object to figure out what button was pressed.

    btn_id = btn_matx.get_selected_btn()

The computer gave us the id of the button pressed but the actual text of the button is more useful so we will retrieve that to.

    btn_text = btn_matx.get_btn_text(btn_id)

Next we use a IF statement to process which button was pressed. We start by checking if the button is the refresh button.

    if btn_text == lvgl.SYMBOL.REFRESH:

If it was we delete all the text from the label we made earlier.

        label.set_text('')

Then we check if the button was the ‘=’ button.

    elif btn_text == '=':

If it was we then get the label’s text (it is supposed to contain a math equation) and try to calculate the result. After that we set the label’s text to the result.

We do all this in try except blocks so that any errors that occurs in the calculation don’t crash the hole program.

        try:
            label.set_text(str(eval(label.get_text())))
        except:
            pass

Lastly if the button pressed was not any of the previous buttons, we add the button’s text the the label.

    else:
        label.set_text(label.get_text() + btn_text)

Connecting the Handler

The only thing left to do is connect the function, we just made, to the button matrix. We will connect them so whenever a button is pressed(value changed) it calls our function.

matrix.add_event_cb(update_button,lvgl.EVENT.VALUE_CHANGED,None)

What It Outputs

Once you run the code, you should see a number keyboard. If you hit any of the numbers, the +, or the – buttons, the symbol you pressed will be added to a label above the keyboard. If you click the equal button, It will try to calculate the equation in the label and put the result back into the label.

Button matrixes are great if you need lots of buttons but don’t have lots of memory on your board. They also allow you to style all the buttons at once.

Button matrixes are so often used to create keyboards, that LVGL has a widget called the keyboard that is just a button matrix with some settings changed. If you want you can learn more about keyboards at Micropython LVGL Keyboard Examples.

Filed Under: Micropython

How to Draw a Polygon with Micropython LVGL

January 13, 2023 by Baxter

a green polygon in the center of a screen

Sometimes you need to be able to draw things that don’t use LVGL’s objects, buttons, sliders, etc. It is possible to do this, if you directly manipulate LVGL draw engine.

This post is intended to explain some of the more complex features of LVGL’s draw engine. You can find the fundamentals of the draw engine at How to Draw a Rectangle with Micropython LVGL.

Today, I will explain LVGL’s polygon. A polygon is essentially any shape that is not a circle and does not have parts of a circle in it. Some common polygons are squares, triangles, pentagons, etc.

There is one main limitation of LVGL’s polygon; LVGL can’t draw concave polygons (don’t worry if you don’t know what that is I will explain it latter.)

The Screen

To get the code below to work with your screen, you will need to add your screen setup code to the beginning of it. If you want more information, you can try How to get a LVGL Micropython screen to work.

Here is the code we will work through

import lvgl

def draw(data):    
    background = lvgl.draw_rect_dsc_t()
    background.init()
    background.bg_color = lvgl.color_hex(0x00ff00)
    
    points = []
    
    def add_point(x,y):
        pt = lvgl.point_t()
        pt.x = x
        pt.y = y
        points.append(pt)
    
    add_point(160,120)
    add_point(0,100)
    add_point(100,200)
    add_point(250,170)
    
    draw_ctx = data.get_draw_ctx()
    draw_ctx.polygon(background,points,len(points))
 
scr = lvgl.scr_act()
scr.add_event_cb(draw,lvgl.EVENT.DRAW_MAIN_END,None)
scr.invalidate()

Understanding This LVGL Code

Ok, now that you have seen all the code, let’s step through each section, so you can understand exactly what each part does. This will make it much easier to actually use polygons in your projects…

The Setup

We start by importing the LVGL library. Around here is a good place to put your screen setup code.

import lvgl

The Draw Function

We need to create a function to draw the polygon on the screen, so we start by defining it.

def draw(data):    

Behind the scenes polygons use the same object as rectangles to apply styles to them, so we create a new one.

    background = lvgl.draw_rect_dsc_t()

Next we have to initialize that variable.

    background.init()

Just to show what this is for, we will set the polygons background color to green.

    background.bg_color = lvgl.color_hex(0x00ff00)

To create a polygon you give LVGL a list of points and it will color in between those points. There are some restrictions on the points you give it, they are explained in the next section. For now we create an empty list.

    points = []

To make this code smaller, we are going to create a function that helps us add points to that list. will start by defining it.

    def add_point(x,y):

Inside this function, we start by creating a LVGL point.

        pt = lvgl.point_t()

After that we will set the point’s x and y values.

        pt.x = x
        pt.y = y

Lastly, we append that point to the list of points.

        points.append(pt)

Now we use the function we just created to add four points to our list.

    add_point(160,120)
    add_point(0,100)
    add_point(100,200)
    add_point(250,170)

To be able to draw on the screen, we need to get the draw_ctx class.

    draw_ctx = data.get_draw_ctx()

Finally, we use draw_ctx to put our polygon on the screen. To make that work we have to give draw_ctx the background variable we created, the list of points, and how many points are in our list.

    draw_ctx.polygon(background,points,len(points))

Restrictions

There are two types of polygons convex and concave. LVGL can not draw concave polygons.

Convex polygon on left and concave polygon of right.

What makes Concave polygons special is that one point dips inside of all the others. That part of the polygon looks like the > symbol.

If you do accidentally tell LVGL to draw one of these, all that will happen is that you board will freeze and you will have to restart it.

If you want to draw a concave polygon, you need to divide it into smaller pieces and draw those pieces separately.

Applying The Draw Function

We created a function to draw a polygon, but we need to add it to some object so its visible. For this example we will add it to the screen object. The first thing we need to do is collect the screen object.

scr = lvgl.scr_act()

Then we add our draw function to the screen.

scr.add_event_cb(draw,lvgl.EVENT.DRAW_MAIN_END,None)

And of course, we invalidate the screen so are polygon gets drawn.

scr.invalidate()

What Next?

Now that you have seen the code, try running it. You should see a green polygon that looks kind of like a rectangle that has been squished. If you modify the list of points you can create lots of new shapes. Don’t forget that you can also change the number of points you use.

The background variable we created is very similar to styles and has many of the same properties. You can use it to customize you polygons color,opacity, and several other properties.

The Polygon is probably the most important shape to know how to draw. The next shapes I would suggest learning is LVGL Arcs and Circles using polygons and arcs you can draw any shape.

 

Filed Under: Micropython

How to Use Micropython LVGL Masks

January 10, 2023 by Baxter

Micropython LVGL masks darken left and right side of screen but leave center normal

Once you have created a project the normal way with buttons, sliders, and such, you sometimes would like to do a little more. Maybe you want to fade out a portion of your screen or darken parts of it. For that reason LVGL has masks.

Masks allow you to disable a portion of the screen from being edited. If you created a button, any part of it that fell inside a mask would not be drawn, making that part invisible.

Today, I will show you how to use the line mask. We will use it twice. First to darken the left side of the screen and second to darken the right side of the screen. The center is left untouched.

Cautions

Masks are LVGL’s lowest drawing functions which means that they can be very glitchy. If they break, they can make your GUI look like it went through a blender. Thankfully they don’t cause any real damage. They are just hard to work with.

The Screen

To get this example to work you will need to add your screen setup code to the beginning of the example. If you want more information, you can try How to get a LVGL Micropython screen to work.

Here the code is

import lvgl

def draw_rect(x1,y1,x2,y2,ctx,opa):
    rect = lvgl.draw_rect_dsc_t()
    rect.init()
    rect.bg_opa = opa
    rect.bg_color = lvgl.color_hex(0x000000)
    
    size_and_spot = lvgl.area_t()
    size_and_spot.x1 = x1
    size_and_spot.y1 = y1
    size_and_spot.x2 = x2
    size_and_spot.y2 = y2
    
    ctx.rect(rect,size_and_spot)

def mask(data):
    line = lvgl.draw_mask_line_param_t()
    line.points_init(35,240,65,0,0)
    
    num = lvgl.draw_mask_add(line,None)
    draw_rect(0,0,320,240,data.get_draw_ctx(),lvgl.OPA._50)
    lvgl.draw_mask_remove_id(num)
            
            
    line2 = lvgl.draw_mask_line_param_t()
    line2.points_init(255,240,285,0,1)
    
    num2 = lvgl.draw_mask_add(line2,None)
    draw_rect(0,0,320,240,data.get_draw_ctx(),lvgl.OPA._50)
    lvgl.draw_mask_remove_id(num2)  
        
scr = lvgl.scr_act()
scr.add_event_cb(mask,lvgl.EVENT.DRAW_POST_END,None)

#This is just some code to create something on the screen so we can show the mask is working
textarea = lvgl.textarea( lvgl.scr_act() )
textarea.set_text( 'Hello this is a test to show how masks work. The left and right sides of the screen should be darkened.' )
textarea.set_size(250,200)
textarea.center()
#End of random code

scr.invalidate()

How It Works

Masks are complicated so to make this example more complete, below is an explanation on how the code works and some of the options that are available to masks.

The Setup

To use LVGL we need to import it. This would also be a good spot to add your screen setup code.

import lvgl

Drawing Rectangles

To prove that our mask is working, we need to have a function that edits part of the screen. Conventionally, masks are pared with draw descriptors to do this.

Draw descriptors are also complicated so I will not explain them here. You can learn more about them at How to Draw a Rectangle with Micropython LVGL if you wish.

The function below works by selecting an area of the screen and darken it by the amount you set.

def draw_rect(x1,y1,x2,y2,ctx,opa):
    rect = lvgl.draw_rect_dsc_t()
    rect.init()
    rect.bg_opa = opa
    rect.bg_color = lvgl.color_hex(0x000000)
    
    size_and_spot = lvgl.area_t()
    size_and_spot.x1 = x1
    size_and_spot.y1 = y1
    size_and_spot.x2 = x2
    size_and_spot.y2 = y2
    
    ctx.rect(rect,size_and_spot)

The Handlar

You have to think about what time you use masks. If you use them at the wrong time half of your screen might disappear.

So we use handlers to get all the timing right. To start we need to create a new function.

def mask(data):

Then we are going to create a new mask parameter. These store all our mask’s settings.

    line = lvgl.draw_mask_line_param_t()

Then we need to initialize all the settings. The prototype of that function looks like this points_init(x1,y1,x2,y2,side). This function is used to create a line.

The starting coordinates of the line are x1 and y1 and the end coordinates are x2 and y2. The side variable uses numbers to control which side of the line to mask off and which side to keep.

The Side numbers are show below.

SIDE TO KEEPLeftRightTopBottom
NUMBER0123
    line.points_init(35,240,65,0,0) 

Now that we have created the line mask. We need to apply it so it actually masks off part of the screen. The function that does this also gives us a number we need to store for later.

    num = lvgl.draw_mask_add(line,None)

With the mask finally in place, we are going to use the draw function we defined earlier to darken the entire screen. Of course the mask will actually only let it darken part of the screen.

    draw_rect(0,0,320,240,data.get_draw_ctx(),lvgl.OPA._50)

That was all we need the mask for, so now we need to remove it. When we applied the mask we got a number, that we stored in the variable num. With that number, we can delete the mask.

    lvgl.draw_mask_remove_id(num)

Now to finish this program we need to do all that again.

We begin by creating a new line mask.

    line2 = lvgl.draw_mask_line_param_t()

Next we initialize its points.

    line2.points_init(255,240,285,0,1)

After that we apply it to the screen.

    num2 = lvgl.draw_mask_add(line2,None)

Then we darken the screen while the mask is applied.

    draw_rect(0,0,320,240,data.get_draw_ctx(),lvgl.OPA._50)

Finally, we delete the second mask now that its not needed.

    lvgl.draw_mask_remove_id(num2) 

Setting the Handler Up

We just spent a lot of work creating that handler, but unless we add it to an object it will not run. For this project we will add it to the entire screen but masks can be added to normal LVGL objects to.

First we need to get the the screen object.

scr = lvgl.scr_act()

Then we add the handler to the screen.

scr.add_event_cb(mask,lvgl.EVENT.DRAW_POST_END,None)

You may have noticed the lvgl.EVENT.DRAW_POST_END. There are four main events that you can use here. They control at what stage of drawing your masks are created and deleted.

lvgl.EVENT.Time
DRAW_MAIN_BEGINBefore any thing is drawn
DRAW_MAIN_ENDAfter most things are drawn
DRAW_POST_BEGINBefore Post drawing (aka. mostly scrollbars)
DRAW_POST_ENDAfter everything is drawn

Besides just creating and deleting a mask in one event, it is also possible to create a mask and delete it in two different events for example you could create a mask in MAIN_BEGIN and then delete it in POST_END.

The Content

To show that the masks are working we need content. Below is some code that creates a textarea and gives it some text.

#This is just some code to create something on the screen so we can show the mask is working
textarea = lvgl.textarea( lvgl.scr_act() )
textarea.set_text( 'Hello this is a test to show how masks work. The left and right sides of the screen should be darkened.' )
textarea.set_size(250,200)
textarea.center()
#End of random code

INVALIDATE

This is a very important step not to forget. Because we created the masks, LVGL doesn’t know they exist and so it has not updated the screen. Calling invalidate on the screen will force it to update and show that our code is working.

scr.invalidate()

What Next?

You have seen the code above. If you try running it, a textarea should appear then the left and right side of the screen should darken. Once you get it to work, experimenting with moving the masks around the screen is a great way to increase your understanding of masks.

The main reason we use masks is because they are a fast way to draw complex shapes. Once you understand the line mask, its a great idea to learn the other masks. They are the angle, fade, map, radius, and polygon masks.

If you are looking for information on them the LVGL source code is where I found most of the information about them and the LVGL docs give a basic description of how they work.

Filed Under: Micropython

How to Draw a Rectangle with Micropython LVGL

January 6, 2023 by Baxter

green rectangle drawn by LVGL draw engine

No matter how long you have been using LVGL, you have probably wonder at some point how to draw shapes like rectangles or circles on a screen. While creating things like buttons and lists is easy, drawing basic shapes is a bit more complicated because you have to go through LVGL’s drawing engine.

Today I will explain how you can use draw descriptors to draw a rectangle any where on your screen. In LVGL the basic principles are the same for all shapes but we will start with the rectangle.

Cautions about Drawing

When you use drawing descriptors you are directly manipulating the draw engine. Doing this can sometimes result in glitches: buttons covering your text, things disappearing, etc. This should not stop you from using them, but it is something to keep in mind.

To get the code below to work with your screen you will need to add your screen setup code to the beginning of it. If you want more information, you can try How to get a LVGL Micropython screen to work.

Here is some example code

import lvgl

scr = lvgl.obj(lvgl.scr_act())
scr.set_size(200,200)
scr.center()

def draw(data):    
    rect = lvgl.draw_rect_dsc_t()
    rect.init()
    rect.bg_color = lvgl.color_hex(0x00ff00)
    
    size_and_spot = lvgl.area_t()
    size_and_spot.x1 = 0
    size_and_spot.y1 = 20
    size_and_spot.x2 = 260
    size_and_spot.y2 = 100
    
    draw_ctx = data.get_draw_ctx()
    draw_ctx.rect(rect,size_and_spot)
 
def extend(data):
    data.set_ext_draw_size(60)
    
scr.add_event_cb(draw,lvgl.EVENT.DRAW_MAIN_END,None)
scr.add_event_cb(extend,lvgl.EVENT.REFR_EXT_DRAW_SIZE,None)

scr.refresh_ext_draw_size()

scr.invalidate()

Understanding This LVGL Code

Ok, now that you have seen all the code, let’s step through each section, so you can understand exactly what each part does. This will make it much easier to actually use descriptors in your projects…

The Setup

We need to import the LVGL module to be able to use it. This would also be a good place to add your screen setup code.

import lvgl

Creating the Canvas

To be able to draw we need something to draw on. You can draw on the background but because of the some of the things we are going to do latter we will create a new object to draw on.

scr = lvgl.obj(lvgl.scr_act())

Then we will set it’s size and put it in the center of the screen.

scr.set_size(200,200)
scr.center()

If you wanted to draw on the background you could replace the three lines above with the line below.

scr = lvgl.scr_act()

The Draw Function

If you draw a rectangle on your screen and just leave it, the next time LVGL refreshes the screen your drawing will disappear. To fix that, we will make a system that redraws the rectangle after every refresh.

First thing we need is a new function. Inside this function we will draw the rectangle.

def draw(data):    

Next we need to create a rectangle descriptor this is essentially a special type of style.

    rect = lvgl.draw_rect_dsc_t()

And just like styles we need to initialize it first.

    rect.init()

The only setting we will edit is the background color of the rectangle.

    rect.bg_color = lvgl.color_hex(0x00ff00)

The way you set the size and position of a rectangle is with an area variable so lets create a new one.

    size_and_spot = lvgl.area_t()

Now we will set the starting position of our rectangle.

    size_and_spot.x1 = 0
    size_and_spot.y1 = 20

Then we set the end position. If you subtract the starting position from the end you will get the size of the rectangle.

(x2 – x1, y2 – y1) == (260 – 0, 100 – 20) == (260,80)

    size_and_spot.x2 = 260
    size_and_spot.y2 = 100

Latter we are going to use this function that we are creating as a handler, and handler functions are always passed an event variable. From that data ,we can get the draw_ctx class which is how we access the draw engine.

    draw_ctx = data.get_draw_ctx()

Now that we have the draw_ctx, we can use it to draw a rectangle all we have to give it is our rectangle descriptor and the area its supposed to be put in.

    draw_ctx.rect(rect,size_and_spot)

Extending The Draw Area

We are drawing on a object that we created earlier called scr. Currently the rectangle we have made is partially out side of scr and LVGL will crop anything that goes out side of an object.

Normally that is what we want ,but sometimes its not, so I will show how you can extend the drawing area.

To begin we need to create another function.

def extend(data):

Then inside that function we add the command to extend the draw area by 60 pixels.

    data.set_ext_draw_size(60)

Latter we will hook up this function to the scr object so it has the correct timing.

Adding the Handlers

We now have created two handler functions and its time to connect them to the scr object.

Lets start with the draw function. We could run this function after every post drawing and that would make it be drawn on top of everything else.

But running the function after the main drawing is more practical. Things like scroll bars would be draw on top of our rectangle which is how they are supposed to work.

scr.add_event_cb(draw,lvgl.EVENT.DRAW_MAIN_END,None)

Next we will connect the function that extends the draw area.

scr.add_event_cb(extend,lvgl.EVENT.REFR_EXT_DRAW_SIZE,None)

Triggering Some Events

Everything is ready to go but because nothing has append to our screen it has not updated anything. We are going to manually trigger some of the events ,so the screen actually shows the changes we’ve made.

To start we need to call the extend area function by using this command on our object.

scr.refresh_ext_draw_size()

Lastly we need to tell LVGL to just redraw our object. The invalidate command tell LVGL that an object needs updating so we run it on our scr object.

scr.invalidate()

What Should Happen

You have seen the code. If you run it, a large green rectangles should appear near the top of your screen. Over all your screen should look like the picture of mine.

The most useful feature of knowing how to use drawing descriptors is that you can modify the ones that already exist. LVGL widgets use drawing descriptors, which means you can edit those descriptors to change how any widget looks.

Now that you understand how to draw a rectangle, learning to draw the rest of LVGL’s shapes will be easy. I would suggest learning how to use LVGL Polygons and LVGL Arcs and Circles

Drawing descriptors are powerful but they are hard to get working and a can be glitchy. If you have a project that needs custom drawing Micropython LVGL Canvases can be a simpler and more reliable way to reach your goal.

Filed Under: Micropython

How To Use Micropython LVGL Tables

January 5, 2023 by Baxter

lvgl table with equations like 1+1, 2+2, etc. and sums like 2, 4 , etc.

If you want to organize a lot of text on your screen, then usually you need lots of processing power, but there is another way. LVGL has a widget called the table.

The table organizes text into rows and columns. It generates and styles the text on the fly, so its fast and memory efficient. The side affect of doing it this way is that it can be hard to style individual cells. Highlighting a single piece of text or changing its size takes a lot of code.

Today I will show you how to create and modify a table. Below is some example code on tables and an explanation on how that code works.

To get this example to work you will need to add your screen code to the beginning of the code below. If you want more information, you can try How to get a LVGL Micropython screen to work.

Here is the example

import lvgl

table = lvgl.table(lvgl.scr_act())
table.set_col_width(0,185)
table.set_height(240)
table.set_row_cnt(10)
table.set_col_cnt(2)

style = lvgl.style_t()
style.init()
style.set_border_side(lvgl.BORDER_SIDE.LEFT | lvgl.BORDER_SIDE.RIGHT | lvgl.BORDER_SIDE.TOP | lvgl.BORDER_SIDE.BOTTOM)
style.set_border_color(lvgl.color_hex(0x004422))

table.add_style(style,lvgl.STATE.DEFAULT | lvgl.PART.ITEMS)

table.set_cell_value(0,0,'Equation')
table.set_cell_value(0,1,'Sum')

table.set_cell_value(1,0,'0')
table.add_cell_ctrl(1,0,lvgl.table.CELL_CTRL.MERGE_RIGHT)

for x in range(8):
    table.set_cell_value(x+2,0,str(x+1)+'+'+str(x+1))
    table.set_cell_value(x+2,1,str(x+x+2))
    
def clicked(data):
    col = lvgl.C_Pointer()
    row = lvgl.C_Pointer()
    tar = data.get_target()
    tar.get_selected_cell(row,col)
    col = col.uint_val
    row = row.uint_val
    print(row,col)

table.add_event_cb(clicked,lvgl.EVENT.VALUE_CHANGED,None)

How Does This Code Work?

Since you’ve seen the program above, why don’t we walk through the code and see how it works. This should make it clearer how you can modify this code for your own uses…

The Setup

To be able to use LVGL, we need to import it.

import lvgl

Creating the Table

The first thing we need to do is create an empty table object.

table = lvgl.table(lvgl.scr_act())

To make the table look more interesting, we are going to make the first column bigger than the second.

The first column has the id of 0, and we will set its width to 185 pixels which is a little bigger than a normal column.

table.set_col_width(0,185)

By setting the height of the table, we can force it to stay the same size, normally it would grow to the size of the data you give it.

If not all the data fits into the restricted table size, then the table will become scroll able so you can reach the rest of the data.

table.set_height(240)

We are going to tell the table how many rows and columns we will eventually create. This is not required, but it will speed up the process latter.

table.set_row_cnt(10)
table.set_col_cnt(2)

Styling the Borders

We want the table to have lines separating the rows and columns, so we need to create a new style.

style = lvgl.style_t()

Styles need to be initialized before use, so we do that as well.

style.init()

The way we are going to create the lines is with borders. To make that work, we first have to enable boarders on all sides.

style.set_border_side(lvgl.BORDER_SIDE.LEFT | lvgl.BORDER_SIDE.RIGHT | lvgl.BORDER_SIDE.TOP | lvgl.BORDER_SIDE.BOTTOM)

We are also going to set the boarder color to a greenish-black.

style.set_border_color(lvgl.color_hex(0x004422))

Now that we have created the style we need to add it to the table.

To make the border appear around the individual cells instead of one boarder around the entire table, we need to target the cells.

We do that by applying the style to lvgl.PART.ITEMS (aka. the cells).

table.add_style(style,lvgl.STATE.DEFAULT | lvgl.PART.ITEMS)

Adding the First Data

To demonstrate how tables work we need some data. For this example, the data we are using is a list of equations and their answers.

The equations will follow the form 0+0, 1+1, 2+2, 3+3, etc. and the answers will simply be numbers like 0, 2, 4, 6, etc.

At the top of the page we also want two boxes one that says ‘Equation‘ and another that says ‘Sum‘.

We will start by creating the ‘Equation’ label, it’s position is the first cell on the table which has the address 0,0.

table.set_cell_value(0,0,'Equation')

Then we will add the ‘Sum’ label to the address 0,1.

table.set_cell_value(0,1,'Sum')

Merging Cells

The first equation would be 0+0 = 0 which is a bit boring, so to make things interesting we will combine the Equation and Sum cells together into one big cell that has the text 0.

To begin we set the first cell to the value of 0.

table.set_cell_value(1,0,'0')

Then we will merge the first cell and the cell to the right of it to create the new over sized cell.

table.add_cell_ctrl(1,0,lvgl.table.CELL_CTRL.MERGE_RIGHT)

The Rest of the Cells

Instead of manually adding each cell, we will use a For Loop that creates the equations and the answers.

first we create a loop that runs 8 times.

for x in range(8):

Then we will generate and add the text to the Equation cell.

    table.set_cell_value(x+2,0,str(x+1)+'+'+str(x+1))

After that we calculate the answer, and put the result in the Sum cell.

   table.set_cell_value(x+2,1,str(x+x+2))

Getting the Clicked Cell

Being able to figure out if a cell is clicked is extremely useful , but takes a little bit of work.

First we need to create is a function to process if any cells are clicked.

def clicked(data):

In that function, we need to create two pointers one for that column and one for the row.

    col = lvgl.C_Pointer()
    row = lvgl.C_Pointer()

Then we will retrieve the table that called this function. Because we have only one table, this is pointless, but if you create additional tables then this will keep the program working.

    tar = data.get_target()

Then we load the row and column of the clicked cell.

    tar.get_selected_cell(row,col)

The row and column variables now contain the address of the clicked cell, but they are still pointers, which aren’t easy to use, so we will reset the pointers to be normal integers.

    col = col.uint_val
    row = row.uint_val

Finally the program knows the exact cell that was clicked. For now we will simply print that data to the terminal but you could do whatever you wanted with it.

    print(row,col)

Connecting the Table

To make the function we just created work we need it to be called whenever the table is clicked, which we do like this.

table.add_event_cb(clicked,lvgl.EVENT.VALUE_CHANGED,None)

What Next?

That you have seen how the code works, try running it. You should see a table that has 2 columns and 10 rows. About six of the rows will be visible, the rest you will have to scroll down to see. If you tap on any of the cells, the board will print to your terminal the coordinates of the cell that you clicked.

Tables can be used for a lot of different projects. Because of how light weight they are, you can use them to simply optimize a program you have already made or you could experiment even further and give them more complex styles to make a spread sheet or something else.

Tables are useful in displaying lots of text, but if you have a lot of numbers then micropython LVGL charts may be a better tool.

Filed Under: Micropython

Micropython LVGL Calendar Example

January 3, 2023 by Baxter

lvgl calendar with highlighted days

Calendars are how we tell time, which is why they are needed so often. If you have ever tried to create a calendar, you will know that it’s hard. You need lots of math and weird algorithms to pull it off. Thankfully LVGL has a widget that does all that stuff for us.

Today I will show you some code that creates a LVGL calendar and modifies some of its properties. After the code there is an explanation on how it works.

To get this example to work you will need to add your screen code to the beginning of the code below. If you want more information, you can try How to get a LVGL Micropython screen to work.

Here is the code:

import lvgl

cal = lvgl.calendar(lvgl.scr_act())
cal.set_size(320,240)
cal.set_today_date(2023,1,2)
cal.set_showed_date(2023,1)
lvgl.calendar_header_arrow(cal)

highlight = []

bug_fix = lvgl.calendar_date_t({'year':1,'month':1,'day':1})
highlight.append(bug_fix)

def unhighlight_list(date):
    for x in highlight:
        if x.year == date.year and x.month == date.month and x.day == date.day:
            highlight.remove(x)
            return True
    return False
  
def date_clicked(data):
    date = lvgl.calendar_date_t()
    if not cal.get_pressed_date(date) == lvgl.RES.OK:
        return
    print(date.year,date.month,date.day)
    if not unhighlight_list(date):
        highlight.append(date)
    cal.set_highlighted_dates(highlight,len(highlight))

cal.add_event_cb(date_clicked,lvgl.EVENT.CLICKED,None)

Understanding This LVGL Code

Ok, now that you have seen all the code, let’s step through each section, so you can understand exactly what each part does. This will make it much easier to actually use calendars in your future projects…

The LVGL Module

Before we can use LVGL we have to import it, so that is the first thing we do.

import lvgl

Creating the Calendar

To begin, we need to create an empty calendar object.

cal = lvgl.calendar(lvgl.scr_act())

Next, we set its size. My screen is 320*240, so setting the calendar’s size to 320*240 makes it fill the entire screen.

cal.set_size(320,240)

Setting the Date

If we left the calendar as is, it would display some random day, so we need to tell it what date to display.

To do this correctly, you need some sort of clock that has the current date, but getting something like that can be a bit of work. Instead, I set the date to the day I created the code.

cal.set_today_date(2023,1,2)

The calendar can only show one month at a time, so we have to set what month is visible. I set it to show the date we created earlier.

cal.set_showed_date(2023,1)

The Header

The default calendar will only show the days of the week. If you want your calendar to show the year and the month there are two options.

lvgl calendar with dropdown month and year controls

First, you could use the dropdown method. This will create two dropdown lists one for the year and one for the month. This code enables the dropdown lists.

lvgl.calendar_header_dropdown(cal)
lvgl calendar with arrow controls for month

Second, you could use the arrows method. This way creates a title at the top of the calendar that displays the year and month, then creates two buttons that allow you to move to the next or previous month. The code below will turn arrows on.

lvgl.calendar_header_arrow(cal)

I prefer the arrow method, so it’s the one used in this example.

Highlighting Dates

One feature of Calendars is that you can give it a list of dates to highlight. So we are going to start by creating an empty list to use latter.

highlight = []

Once you tell a Calendar to highlight some dates, you seem to have to always leave at least one date highlighted. The way we can get around this glitch is to highlight a random date that no one will see.

First we have to create an date object with the desired year,month, and day.

bug_fix = lvgl.calendar_date_t({'year':1,'month':1,'day':1})

Then we add it to the list of highlighted dates.

highlight.append(bug_fix)

The Unhighlight Function

To make the code more streamlined we need to create a function that helps us unhighlight a date that we have already highlighted.

We start by creating the new function.

def unhighlight_list(date):

Then we will use a for loop to check if the date we care about is in the highlight list.

    for x in highlight:

We will use the if statement below to check the date.

        if x.year == date.year and x.month == date.month and x.day == date.day:

If the date is found in the list, we remove it from the list, and then the functions returns True.

            highlight.remove(x)
            return True

If the date was not in the list, then the function returns False.

    return False

The Event Handler

The end goal is that when a date is clicked, it is highlighted, and if you click it again, it will unhighlight. To do all that, we need to create a handler function.

We begin as usual by defining it.

def date_clicked(data):

Next, we are going to create an empty date object.

    date = lvgl.calendar_date_t()

This line of code does two things.

First, the if statement checks if a date was clicked.

Second, the get_pressed_date() part sets the date object we created to the clicked date.

    if not cal.get_pressed_date(date) == lvgl.RES.OK:

If no date was clicked, the the functions stops running and returns.

        return

To make sure everything is working well, the computer prints to the terminal the date that it thinks was clicked.

    print(date.year,date.month,date.day)

This code runs the unhighlight function we created on the date that was clicked.

    if not unhighlight_list(date):

If the unhighlight function could not find the date we gave it, we will add that date to the highlight list.

The overall effect of these two lines of code is that it toggles whether a date is highlighted or not.

        highlight.append(date)

All we have really done up to this point is add and remove dates from a list. Now we need to give that list to the calendar so it can highlight the actual dates.

    cal.set_highlighted_dates(highlight,len(highlight))

Connecting the Handler

The last step we need to do is connect the handler we made to the calendar, so whenever a date is clicked the function is called.

cal.add_event_cb(date_clicked,lvgl.EVENT.CLICKED,None)

What Next?

Now that you have seen the code, try running it. You should see a large calendar that displays the date 1/2/2023. Clicking on a date will highlight that date. If you click a highlighted date it will remove the highlight. If you press one of the blue buttons in the corners it will change the visible month.

Calendars by themselves aren’t super useful. A great next step would be hooking up a clock system, so you can tell the real date. You could also experiment with styling to make it look nicer and might add some code that allows you to attach text to certain dates like Christmas or New Years Eve.

If you liked LVGL Calendars another widget that you might also like is the micropython LVGL Tileview.

Filed Under: Micropython

How To Get A Microphone To Work With Micropython LVGL

January 2, 2023 by Baxter

Screen with blue line representing microphone data

Microphones add lots of important features to your project, but unfortunately, they can be a pain to get working.

Today I would like to show you how to get an audio recording from a PDM microphone (there will be more on PDM later) and display it with LVGL.

Below you will find a definition of some terms, some code, and a walk-through of how that code works.

What is I2S

I2S is how we connect the microphone to our computer. It takes four wires to fully hook it up. If you are using only a microphone and not an additional speaker, then you can get away with three wires.

PDM vs. PCM

First I need to explain that PDM and PCM are the format that your microphone sends data in. So you still use I2S to get data from both types of microphones, there is no difference there.

PCM is essentially raw data. It’s high quality and simple to use.

PDM is more complicated, I don’t work with microphones that much so I don’t know what exactly makes it so special.

The microphone on my board is a PDM microphone, so the code below will work with PDM microphones. If you are use a PCM microphone, there is one line of code you will need to change. Down in the explanation there is a section on how to do that.

Micropython LVGL

Besides getting the actual microphone data, I wanted to display it on a screen, so I chose to use LVGL.

To get this example to work you will need to add your screen code to the beginning of this example. If you want more information try How to get a LVGL Micropython screen to work.

The Espidf Module

One of the features of micropython LVGL is that is has a module called espidf. This module is how we process the PDM signal. There are simpler ways if you are using a PCM microphone.

If you are using a PCM mic, try the machine module.

Two important facts to keep in mind are that the espidf module only works on esp boards like the esp32, and that you are unlikely to find it in a normal micropython firmware.

Here is the code:

import espidf,lvgl

config = espidf.i2s_config_t()
config.mode = espidf.I2S_MODE.MASTER| espidf.I2S_MODE.RX | espidf.I2S_MODE.PDM 
config.sample_rate = 16000
config.bits_per_sample = espidf.I2S_BITS_PER_SAMPLE._16BIT
config.channel_format = espidf.I2S_CHANNEL_FMT.ONLY_RIGHT
config.communication_format = espidf.I2S_COMM_FORMAT.I2S
config.intr_alloc_flags = 0
config.dma_buf_count = 2
config.dma_buf_len = 800
config.use_apll = False

mic = espidf.i2s_pin_config_t()
mic.bck_io_num = 12
mic.ws_io_num = 0
mic.data_in_num = 34
mic.data_out_num = 2

espidf.i2s_driver_install(espidf.I2S_NUM._0,config,0,None)
espidf.i2s_set_pin(espidf.I2S_NUM._0,mic)

chart = lvgl.chart( lvgl.scr_act() )
chart.set_size( 320, 240 )
chart.center()
chart.set_point_count( 100 )
chart.set_style_size( 0, 0, lvgl.PART.INDICATOR )
chart.set_range(lvgl.chart.AXIS.PRIMARY_Y,0,240)
series1 = chart.add_series( lvgl.color_hex( 0x0099FF ), lvgl.chart.AXIS.PRIMARY_Y )



def get_round(data,High,nHigh):
    div = High/nHigh
    return int(data/div)

def get_val(data):
    total = 0
    for x in range(int(len(data)/2)):
        total += get_round(data[2*x+1]+(data[2*x]*256),65536,240)
    total = total/(len(data)/2)
    return int(total)

import struct
val = 0
while True:
    leng = struct.pack('P',val)
    buffer= bytearray(800)
    length = espidf.i2s_read(espidf.I2S_NUM._0,buffer,300,leng,1000000)
    chart.set_next_value( series1, get_val(buffer))

How It Works

Now that you have seen the code, let’s step through it to see how it works behind the scenes.

The Setup

Before we do anything we need to import two modules. The lvgl module is a graphics library, and the espidf module is what lets us talk to the microphone.

import espidf,lvgl

The Config

First thing we need to do is create a I2S config object. This is what tells the the computer how to process the I2S signals.

config = espidf.i2s_config_t()

Then we need to set the configs mode. Modes control whether your devise is a master or slave, whether you device is transmitting or receiving, and whether your device uses PDM or PCM.

config.mode = espidf.I2S_MODE.MASTER| espidf.I2S_MODE.RX | espidf.I2S_MODE.PDM 

Enabling PCM

If you have a PCM microphone you will need to replace the above code with the following line. This should work, but I have not tested it yet, so I can’t say for sure.

config.mode = espidf.I2S_MODE.MASTER| espidf.I2S_MODE.RX 

Next we set the sample rate. This is how many chunks of audio you want to revive per second. 16000 seams to be the minimum value for the esp32. For reference, 44100 is CD quality.

config.sample_rate = 16000

After that we need to set the word length which tells the computer how big a chunck of audio is. For my microphone that is 16 bits.

config.bits_per_sample = espidf.I2S_BITS_PER_SAMPLE._16BIT

Many headphones or speakers are stereo, which means that the computer can tell the headphones to send data only to a specific ear, like your left ear. Because my microphone is not stereo, I have to set which channel I want it to send data to. I chose the right channel, but it doesn’t really matter.

config.channel_format = espidf.I2S_CHANNEL_FMT.ONLY_RIGHT

Another feature we are going to set is the audio format. Because there are some slight differences in how I2S data can be sent, we need to change which format we are using. For my microphone, I need standard I2S. If you are not sure which format to use, start with standard.

config.communication_format = espidf.I2S_COMM_FORMAT.I2S

Next we set the interrupt flags. Setting it to zero is a simple way to get it to work.

config.intr_alloc_flags = 0

Because the microphone sends so much data, we need to give it a buffer to temporally store the data in.

To start, we need to tell the computer how many buffers we want. You will want at least two, but having more can potentially speed up performance.

config.dma_buf_count = 2

Then we need to set how large each of those buffers are. I chose 800 samples per buffer.

config.dma_buf_len = 800

This last line of code controls if the apall clock is used. It will give you more accurate timing, but because we don’t need it, I turned it off.

config.use_apll = False

The Wiring

Next thing we need to do is setup the pins that we are using. I2S as four pins that can be used. They are as follows.

NAMEDESCRIPTIONOTHER NAMES
BCKThis is the clock pinCLK
WSThis pin selects whether the microphone is to send the left or right channel dataLRCK, LR
DATA INThis pin is how your computer receives audio.DATA, DIN, RX
DATA OUTThis pin is how your computer sends audio.DATA, DOUT, TX

Because I am using only one microphone I don’t not have to wire up the DATA OUT or WS pins. If your microphone has a WS pin than it might still be wise to wire it up, to avoid glitches.

NAMEESP32 PIN
BCK12
WS (optional)0
DATA IN34

Setting Up the Pins

Now that we have deciede on the pins we are going to use we need to set them up.

first we create a I2S pin object.

mic = espidf.i2s_pin_config_t()

Then we set the BCK pin.

mic.bck_io_num = 12

We set the WS and DATA IN pin the same way.

mic.ws_io_num = 0
mic.data_in_num = 34

Next we have to give a value to the data out pin. There is a way to disable this pin, but it’s simpler just to give it an fake pin.

mic.data_out_num = 2

Turning the I2S on

Finally, we can take all the configs we have created and turn on the I2S.

To start, we add the driver config we created earlier to I2S port 0.

espidf.i2s_driver_install(espidf.I2S_NUM._0,config,0,None)

Then we setup the pins for I2S 0 as well.

espidf.i2s_set_pin(espidf.I2S_NUM._0,mic)

Displaying the Data

We have the microphone data now, but what do we do with it? I decided that I would build a graph that shows the sound being recorded.

First, we have to create a LVGL chart that is 320 pixels wide and 240 pixels tall.

chart = lvgl.chart( lvgl.scr_act() )
chart.set_size( 320, 240 )

Then we center it on the screen.

chart.center()

The code below sets up some of the features of the chart. Because this is a tutorial on microphones and not charts I will not waste your time explaining it.

chart.set_point_count( 100 )
chart.set_style_size( 0, 0, lvgl.PART.INDICATOR )
chart.set_range(lvgl.chart.AXIS.PRIMARY_Y,0,240)
series1 = chart.add_series( lvgl.color_hex( 0x0099FF ), lvgl.chart.AXIS.PRIMARY_Y )

The Rounding Functions

The microphone we are using gives so much data that the screen would be swamped in data if we tried to use it all. So instead, I created the following two rounder functions to help shrink the data.

def get_round(data,High,nHigh):
    div = High/nHigh
    return int(data/div)

def get_val(data):
    total = 0
    for x in range(int(len(data)/2)):
        total += get_round(data[2*x+1]+(data[2*x]*256),65536,240)
    total = total/(len(data)/2)
    return int(total)

Getting the Audio

To begin, we need to import the struct module. This is used to create a pointer which is needed to read the mic’s data.

import struct

Then we create an empty variable called val.

val = 0

Next, we create a infinite loop so the computer constantly reads the microphone’s data.

while True:

After that we need to create a pointers so we can read the mic.

    leng = struct.pack('P',val)

We also need a buffer to store the microphone’s data in.

    buffer= bytearray(800)

And finaly, we actually read the microphone.

    length = espidf.i2s_read(espidf.I2S_NUM._0,buffer,300,leng,1000000)

The last thing we do is update the chart so it has the new mic data.

    chart.set_next_value( series1, get_val(buffer))

Wraping Up

Now that you have seen the code, try running it. If every thing works right, you should see a bunch a squiggly lines that change when you say something.

After you get the code to run you can experiment with it. Maybe you can hook up a speaker and record some audio then play it back.

When I was building this project, I started by recording data to an SD card. Then I played it on my computer. That is a great way to test that you are getting a good recording from your mic.

If you liked this project you might also like How To Use A PNG Image With Micropython LVGL.

Filed Under: Micropython

How To Style Parts Of Text In Micropython LVGL

December 13, 2022 by Baxter

lvgl span with green background, and text that in some places is highlighted or underlined

The LVGL library has a lot of powerful features for styling text. You can set the color, underline the text, set the opacity, etc., but if you want to style a small part of some text instead of the entire thing, that gets a little more complicated. The best way to pull that off is with the span.

What Is A Span?

The span is an LVGL widget that is similar to the label. Think of them like a container that holds a bunch of individual labels inside. Spans take care of all the details like word wrap and line positioning, and let you take care of the other details like styling the text.

How Do You Use Spans?

The rough idea is that you create a Spangroup(aka. Span), and inside of it you create spans. Spans are little snippets of text, plus any styles you want to add to that text. To make the process a little clearer I have included an example below. Down there is also an explanation of how the example works.

Here is that code:

import lvgl

group = lvgl.spangroup(lvgl.scr_act())
group.set_size(120,100)
group.set_mode(lvgl.SPAN_MODE.BREAK)
group.set_style_bg_color(lvgl.color_hex(0x005050),0)
group.set_style_bg_opa(255,0)
group.set_style_text_color(lvgl.color_hex(0x000000),0)
group.center()

span = group.new_span()
span.set_text('This is some normal text but')

other_span = group.new_span()
other_span.set_text(' some of it is colored')
other_span.style.set_text_color(lvgl.color_hex(0x00FF50))

other_other_span = group.new_span()
other_other_span.set_text(' and some of it is underlined')
other_other_span.style.set_text_decor(lvgl.TEXT_DECOR.UNDERLINE)

other_other_other_span = group.new_span()
other_other_other_span.set_text(' and that is because spans allow you to style individual parts of your text')

group.refr_mode()

Understanding This LVGL Code

Ok, now that you have seen all the code, let’s step through each section, so you can understand exactly what each part does. This will make it much easier to actually use the span in your projects…

The Setup

First we have to import LVGL so we can use it.

import lvgl

Setting Up the Spangroup

To start, we need to create an empty spangroup.

group = lvgl.spangroup(lvgl.scr_act())

Then we will set its size to 120 pixels wide and 100 pixels high.

group.set_size(120,100)

If you put more text into the span than it can hold, it will normally cut off that extra text, but Spans have modes to control what they do. Below, I set the mode to BREAK, which makes the span increase its height if it has to much text.

If you don’t care what it does, then you can remove this line of code entirely and the span will continue to work fine.

group.set_mode(lvgl.SPAN_MODE.BREAK)

After that we will change the background color to a greenish blue.

group.set_style_bg_color(lvgl.color_hex(0x005050),0)

The background of spans is normally invisible, so to see the new color we added, we need to make the background visible by setting its opacity to 255.

group.set_style_bg_opa(255,0)

To make the text more readable we are going to change its color to black. This code will change the default color but later we can override it to highlight portions of the text that we want.

group.set_style_text_color(lvgl.color_hex(0x000000),0)

Lastly we will center the span because that makes it look a little better.

group.center()

Creating the First Span

It is finally time to actually create a span which is not to be confused with the spangroup.

span = group.new_span()

After that we set its text.

span.set_text('This is some normal text but')

The Other Span

This span is colored greenish. We start just like we did last time by creating a new span.

other_span = group.new_span()

Then we set its text.

other_span.set_text(' some of it is colored')

Here is where it gets interesting. We are going to add a style to the span to change the color. Take a good look at the code below, because for some reason, the way you add styles to spans is done differently than normal styles.

I haven’t seen any other object that uses styles this way. It seems to be unique to spans.

other_span.style.set_text_color(lvgl.color_hex(0x00FF50))

The Underlined Span

In this span, we are going to underline the text, but first we have to create it.

other_other_span = group.new_span()

After that the text needs to be set.

other_other_span.set_text(' and some of it is underlined')

Add then we add the style that underlines the text.

other_other_span.style.set_text_decor(lvgl.TEXT_DECOR.UNDERLINE)

The Last Span

This last span is again set to the default settings and works like all the previous ones. First we create it.

other_other_other_span = group.new_span()

Then we set its text.

other_other_other_span.set_text(' and that is because spans allow you to style individual parts of your text')

Making Every Thing Work Right

This last line of code is very important. Whenever we change something in the spangroup, like creating a span, changing some text, or adding a style, we need to refresh the main spangroup otherwise our updates might not be displayed.

The code below is what calls that refresh. Thankfully, it’s pretty simple.

group.refr_mode()

What Next?

Now that you know how the above code works, try running it. Your screen should look like the picture at the top of this page. Hopefully, in spite of my poor choice of colors, it is clear what spans can do and how useful they are.

One of the best features about spans is that they are similar to labels, so if you have created a project that uses labels, it is not too complicated to update those labels with spans.

If you’re looking for useful ways to display text on your screens then Micropython LVGL Textarea Examples might be able to interest you.

 

Filed Under: Micropython

Micropython LVGL Buttons, Switches, and Checkboxes

December 12, 2022 by Baxter

lvgl switch

Having good inputs for you projects is extremely important. Today I am going to show you three of the simplest yet most important inputs that the LVGL graphics library has to offer.

The first example will explain how to create buttons and tell when they have been clicked.

After that, I will show you how switches work, and how you know what state they are in.

Then finally, I take you through the process of creating checkboxes and figuring out if they are checked.

After each example we can walk through the code to see how they work. This will make it easier to add these inputs to your own projects.

To get these examples to work you will need to add your screen code to the beginning of this example. If you want more information try How to get a LVGL Micropython screen to work.

lvgl blue button

Micropython LVGL Buttons

Buttons are the most important and most basic input that I can think of. The code below will create a button and give it a label. When you click the button it will change the label’s text to clicked, and if you press the button for a long time it will change the text to ‘long pressed’.

import lvgl

btn = lvgl.btn(lvgl.scr_act())
btn.center()
lb = lvgl.label(btn)
lb.set_text('default')


def handler(data):
    if data.get_code() == lvgl.EVENT.LONG_PRESSED:
        print('long pressed')
        lb.set_text('long pressed')
    elif data.get_code() == lvgl.EVENT.CLICKED:
        print('clicked')
        lb.set_text('clicked')
        
btn.add_event_cb(handler,lvgl.EVENT.ALL,None)

The Setup

The first thing the code does is import the LVGL library so we can use it.

import lvgl

The Button

Then we have to create the button object.

btn = lvgl.btn(lvgl.scr_act())

The button will look better if we put it in the center of the screen.

btn.center()

The Label

Next we want to give the button some text, so we create a label inside of our button.

lb = lvgl.label(btn)

After that we will give the label the text ‘default’ .

lb.set_text('default')

The Handler

When the button is pressed we want it to change the text. To begin, we need to create a function.

def handler(data):

Inside that function we are going to do a few things. First, we are going to check if the button was long pressed.

    if data.get_code() == lvgl.EVENT.LONG_PRESSED:

If it was, we are going to run the code below, which prints the text ‘long pressed’ and changes the label’s text to ‘long pressed’.

        print('long pressed')
        lb.set_text('long pressed')

If the the button wasn’t long pressed we are going to check if it was clicked.

    elif data.get_code() == lvgl.EVENT.CLICKED:

And if it was clicked, the code prints to the terminal ‘clicked’ and changes the label’s text to ‘clicked’.

        print('clicked')
        lb.set_text('clicked')

Connecting It All Together

Finally we have to connect the handler function we just created to the button.

btn.add_event_cb(handler,lvgl.EVENT.ALL,None)
lvgl switch that is on

Micropython Switches

Switches are a GUI object we are all familiar with. LVGL makes it pretty easy to make a switch, though it will take a little work to get the current position of it.

Here is the basic code:

import lvgl

sw = lvgl.switch(lvgl.scr_act())
sw.set_size(100,50)
sw.center()

def handler(data):
    switch = data.get_target()
    print(switch.has_state(lvgl.STATE.CHECKED))

sw.add_event_cb(handler,lvgl.EVENT.CLICKED,None)   

The Setup

Before we do anything else we need to import the LVGL library.

import lvgl

Creating the Switch

To create a switch we first have to create an empty switch object.

sw = lvgl.switch(lvgl.scr_act())

To make this switch a little more interesting and more visible for my camera, let’s make it bigger.

sw.set_size(100,50)

Lastly, we center it so it looks more pleasing.

sw.center()

The Handler

To be able to tell if the slider is on or off we need to start by creating a handler function. Because I am not very creative, we will name that function, “handler”.

def handler(data):

After that, we are going to run some code to find the switch that called the handler.

    switch = data.get_target()

Now that we know which switch called this function, we figure out if it is checked or not, and print that value to the terminal.

    print(switch.has_state(lvgl.STATE.CHECKED))

Wrapping Up

The last thing we need to do for this example is to connect the handler we just made to the switch we created earlier.

sw.add_event_cb(handler,lvgl.EVENT.CLICKED,None) 
lvgl checkbox that is checked

LVGL Checkboxes

Checkboxes are similar to switches. The main difference between them is how they look, but most of the code behind them is the same.

Here is the checkbox code:

import lvgl

ch = lvgl.checkbox(lvgl.scr_act())
ch.set_text('hello there')
ch.center()

def handler(data):
    check = data.get_target()
    print(check.has_state(lvgl.STATE.CHECKED))

ch.add_event_cb(handler,lvgl.EVENT.CLICKED,None)    

The Setup

As usual, we start by importing LVGL.

import lvgl

Creating the checkbox

First, we are going to create the checkbox object.

ch = lvgl.checkbox(lvgl.scr_act())

Checkboxes allow you to add some text next to them. If you click the text it will also check the box.

ch.set_text('hello there')

Lastly we center the box.

ch.center()

The Handler

This handler is pretty much identical to the one used for switches. It just prints if the checkbox is checked or not.

def handler(data):
    check = data.get_target()
    print(check.has_state(lvgl.STATE.CHECKED))

Connecting Everything Together

The last step is to connect the handler to the checkbox.

ch.add_event_cb(handler,lvgl.EVENT.CLICKED,None)

What Next?

Buttons are the simplest type of input, which is why they are so powerful. They can be modified in tons of creative ways to meat whatever you need. The example I gave you was intended to show some of the special features that buttons have available, but you could use the handler functions we created for the switch or checkbox, to simplify the code.

Switches are great if you want an input that stays on after you have pressed it. They are perfect if you are trying to make a settings menu, or if you just want your GUI to look more polished.

Checkboxes aren’t needed in everyday projects, but they can make a large GUI interesting. They are practically identical to switches, so you can easily add them in wherever you put switches.

If you want some more complicated inputs to play with, then you might like Micropython LVGL Keyboard Examples.

Filed Under: Micropython

Micropython LVGL Arc Example

December 9, 2022 by Baxter

A screen showing a lvgl arc in an arc

It always seems difficult to make a GUI interesting. Thankfully, the LVGL library has some useful objects to help our projects. One of those objects is the arc. They are like sliders, but are curved and have a few more settings.

Today, I am going to show and explain some code for creating and positioning arcs, modifying some of their features, and getting their current value. By the end you should have a firm understanding of arcs.

To get this example to work you will need to add your screen code to the beginning of the example. If you want more information try How to get a LVGL Micropython screen to work.

Here is that code:

import lvgl

arc_main = lvgl.arc(lvgl.scr_act())
arc_main.set_size(200,200)
arc_main.set_bg_angles(180,90)
arc_main.center()

min_arc = lvgl.arc(lvgl.scr_act())
min_arc.set_size(100,100)
min_arc.set_bg_angles(0,270)
min_arc.set_mode(lvgl.arc.MODE.SYMMETRICAL)
min_arc.center()
min_arc.remove_style(None,lvgl.PART.KNOB)

label = lvgl.label(lvgl.scr_act())
label.set_text('0')

pointer = lvgl.obj(lvgl.scr_act())
pointer.set_size(10,40)
pointer.set_style_bg_color(lvgl.color_hex(0x000000),0)
pointer.center()

label.set_text('91')
min_arc.set_value(91)
arc_main.set_value(91)

arc_main.align_obj_to_angle(label,0)
arc_main.rotate_obj_to_angle(pointer,-80)

def new_val(data):
    arc = data.get_target()
    
    label.set_text(str(arc.get_value()))
    
    min_arc.set_value(arc.get_value())
    
    arc_main.set_value(arc.get_value())
    arc_main.align_obj_to_angle(label,0)
    arc_main.rotate_obj_to_angle(pointer,-80)

arc_main.add_event_cb(new_val,lvgl.EVENT.VALUE_CHANGED,None)
min_arc.add_event_cb(new_val,lvgl.EVENT.VALUE_CHANGED,None)

Understanding This LVGL Code

Ok, now that you have seen all the code, let’s step through each section, so you can understand exactly what each part does. This will make it much easier to actually use arcs in your future projects…

The Setup

First we have to import the LVGL library.

import lvgl

Creating the Main Arc

We want a large arc to go around the screen, so first we have to create an empty arc object.

arc_main = lvgl.arc(lvgl.scr_act())

Secondly, we want to set the size of are new arc.

arc_main.set_size(200,200)

Then we will set the start and end angle of our arc.

arc_main.set_bg_angles(180,90)

Lastly, we will put the arc in the center of the screen.

arc_main.center()

The Small Arc

We also want a smaller arc in the center of the screen, so let’s create that.

min_arc = lvgl.arc(lvgl.scr_act())

Then we set its size.

min_arc.set_size(100,100)

Next, the angles need to be set.

min_arc.set_bg_angles(0,270)

Instead of our arc shading the background blue from the start, we want it to start shading from the middle. You can see that happening in the picture at the beginning of this page. To do that, we will have to change the mode.

min_arc.set_mode(lvgl.arc.MODE.SYMMETRICAL)

Then we center the small arc.

min_arc.center()

The last little detail is to remove the blue knob.

min_arc.remove_style(None,lvgl.PART.KNOB)

The Label

We want the arc’s knob to have the current position written on it, so we need to create a label now. Later we will setup the code that has it follow the knob.

label = lvgl.label(lvgl.scr_act())

Next, let’s set the default text to 0.

label.set_text('0')

The Pointer

In center of the screen we want an indicator that points to where the arc currently is. To start we will make our pointer out of an object.

pointer = lvgl.obj(lvgl.scr_act())

Then we will set its size, making it tall and thin.

pointer.set_size(10,40)

After that we will make it black.

pointer.set_style_bg_color(lvgl.color_hex(0x000000),0)

And of course we will center it on the screen.

pointer.center()

Creating a Default Value

If we left this program as is, when it starts up, all of the arcs, pointers, and labels would be in random positions. We need to set them all to the correct values.

The default value I decided for this system is 91, so we need to set the label’s text to 91 first.

label.set_text('91')

Then we will set both of the arcs to position 91.

min_arc.set_value(91)
arc_main.set_value(91)

Finally, we need to put the label on the main arc’s knob, and make the pointer point at the the main arc’s knob.

arc_main.align_obj_to_angle(label,0)
arc_main.rotate_obj_to_angle(pointer,-80)

How to Update Everything

Whenever the arcs are moved, we need to update the rest of the screen. Because we have two arcs, we normally would need two functions to update the screen, but since the arcs update the same thing, we will make one function and have both of them use it.

To start, we define our new function.

def new_val(data):

When the function is called, we need to be able to tell which arc called it so we add this.

    arc = data.get_target()

Then we get the new value of that arc and set the label’s text to that value.

    label.set_text(str(arc.get_value()))

After that, we are going to set the the arc that did not change to the value of the one that changed. Unfortunately, we don’t know which arc did not change, so we simply update both arcs.

    min_arc.set_value(arc.get_value())
    arc_main.set_value(arc.get_value())

The last thing we need to do is move the label to the new arcs position and point the pointer at the arc.

    arc_main.align_obj_to_angle(label,0)
    arc_main.rotate_obj_to_angle(pointer,-80)

Setting Up the Events

Finally, we need to connect that function to both the arcs, so when their value changes, that function gets called.

arc_main.add_event_cb(new_val,lvgl.EVENT.VALUE_CHANGED,None)
min_arc.add_event_cb(new_val,lvgl.EVENT.VALUE_CHANGED,None)

What Happens Next?

Once you have run the program above, it will create two arcs, a pointer, and a label. If you move ether one of the arcs, the other arc will move with it. The current position will be displayed on the label, and the pointer will point at the position of the main arc.

While the example on this page is not extremely useful, it does show you most of the features available for arcs, and gives you a chance to experiment with arcs to see how they work.

Events are used with arcs to get their values. If you want some more examples on how events work, Micropython LVGL Event Examples might be helpful.

Filed Under: Micropython

Micropython LVGL Roller Example

December 7, 2022 by Baxter

roller with red center and black background

Trying to get a lot of information on a small screen is hard. Fortunately, LVGL has a widget called the roller. It allows you to let the user choose between a large amount of options, but at the same time, the roller only takes a small amount of your screen space.

Rollers work by showing only a few of the options available, usually three, and then if you want to see the other options you have to scroll up or down. Today I would like to show you how to create and style your own roller and print the selected option in your terminal.

To get this example to work you will need to add your screen code to the beginning of this example. If you want more information try How to get a LVGL Micropython screen to work.

Here is that code:

import lvgl

names = ['one','two','three','four','five']
text = '\n'.join(names)

#Another way you could have create the list above.
other_way_text = 'one\ntwo\nthree\nfour\nfive'


roll = lvgl.roller(lvgl.scr_act())
roll.set_size(200,100)
roll.center()
roll.set_options(text,lvgl.roller.MODE.INFINITE)

main = lvgl.style_t()
main.init()
main.set_text_line_space(30)
main.set_bg_color(lvgl.color_hex(0x000000))

select = lvgl.style_t()
select.init()
select.set_bg_color(lvgl.color_hex(0x300000))
select.set_text_color(lvgl.color_hex(0xFFAAAA))

roll.add_style(main,lvgl.PART.MAIN)
roll.add_style(select,lvgl.PART.SELECTED)
roll.set_visible_row_count(4)

def handler(data):
    roller = data.get_target()
    print(names[roller.get_selected()])
    
roll.add_event_cb(handler,lvgl.EVENT.VALUE_CHANGED,None)

How Does This Code Work?

Since you’ve seen the program above, why don’t we walk through the code and see how it works. This should make it clearer so you know how you can modify this code for your own uses…

The Setup

Before we can use the LVGL library we need to import it.

import lvgl

The Options

Selecting which option you want is the whole point of the roller, so we have to create the options.

First, we will create a list of the option names.

names = ['one','two','three','four','five']

Then we will have to create a string with all of those names, each one being separated by a ‘\n’.

text = '\n'.join(names)

Another potentially simpler way you could create that list is to do it by hand, like this.

other_way_text = 'one\ntwo\nthree\nfour\nfive'

Creating the Roller

To begin, we need to create a empty roller object.

roll = lvgl.roller(lvgl.scr_act())

Secondly, we will set the size of the roller. Later on we will adjust the height, but the width will stay 200 throughout the program.

roll.set_size(200,100)

Everything looks better when put in the center of the screen.

roll.center()

Now we have to add the options that we created earlier and set the mode.

Rollers have two modes available, NORMAL and INFINITE. In INFINITE mode, when you scroll to the last option and continue scrolling, it will send you back to the first option. In NORMAL mode, if you hit the last option, it doesn’t let you scroll any more.

roll.set_options(text,lvgl.roller.MODE.INFINITE)

The Main Style

To make our roller look better, we need to add some styles. The first style will set the background properties.

To start, we need to create and initialize our style.

main = lvgl.style_t()
main.init()

Then we will set the text spacing (how far apart the options are).

main.set_text_line_space(30)

Lastly, we’re going to set the background color to black.

main.set_bg_color(lvgl.color_hex(0x000000))

The Selected Style

This style modifies the option that is currently selected. As usual we create and initialize the new style.

select = lvgl.style_t()
select.init()

Secondly, we will set the background color to a dark red.

select.set_bg_color(lvgl.color_hex(0x300000))

Finally, we will set the text color to a reddish gray.

select.set_text_color(lvgl.color_hex(0xFFAAAA))

Adding the Styles to the Roller

First, we will add the main style to the roller.

roll.add_style(main,lvgl.PART.MAIN)

Then we will add the select style to the roller. The lvgl.PART.SELECTED is what apples this style only to the selected options.

roll.add_style(select,lvgl.PART.SELECTED)

Editing the Roller Height

To make the roller look more professional, we’ll set the ruler to display only 4 options at a time. Because of how rollers work, it will display 3 options in the middle and half of one option at the top and at the bottom of the roller.

roll.set_visible_row_count(4)

Printing The Selected Option

The function below will be called when the roller’s value changes. It simply gets the roller object, then gets the index of the option that was selected. Lastly, we feed that index into the list of names we created earlier to get the name of the selected option.

def handler(data):
    roller = data.get_target()
    print(names[roller.get_selected()])

The next thing we will need to do is connect the handler to the roller and set the handler to only be called when the roller’s value changes.

roll.add_event_cb(handler,lvgl.EVENT.VALUE_CHANGED,None)

What next?

Since you now can create rollers and style them, and you know how to print out the currently selected option, you can now modify the code above or create entirely new code for your own custom rollers.

If you have a screen with more space available on it, then another widget that works similar to the roller is the list. You can find some more information that might be helpful about them at Micropython LVGL List Example.

Filed Under: Micropython

How To Use A PNG Image With Micropython LVGL

December 6, 2022 by Baxter

lvgl screen with small picture in the center

Images are extremely important when building a GUI, but LVGL does not have a very simple way to add images to your project. I have been searching for awhile for a good way to add them to my projects, and today, I am going to show you the best way I have found so far.

The Code below will load a PNG image from your file system, setup all the necessary settings for that image, and display it in the center of your screen. The image I tested with is at the bottom of this page. It’s 150*150 pixels wide and about 18 kb in size.

To get this example to work you will need to add your screen code to the beginning of the example. If you want more information try How to get a LVGL Micropython screen to work.

Here is the Code:

import lvgl,imagetools

driver = lvgl.img.decoder_create()
driver.open_cb = imagetools.open_png
driver.info_cb = imagetools.get_png_info

file = open('sd/lvgl_test.png','rb')
raw_image = file.read()
file.close()

image_description = lvgl.img_dsc_t()
image_description.data = raw_image
image_description.data_size = len(raw_image)

image = lvgl.img(lvgl.scr_act())
image.set_src(image_description)
image.center()

lvgl.scr_act().set_style_bg_color(lvgl.color_hex(0x000000),0)

Things To Know Before Running

The Image

For this code to work you will need to put an image somewhere in your file system. I attached an SD card to my board and put the image on that.

If you don’t want to use an SD card in your project, you will need to upload the image to your board’s file system. I know the AMPY tool can do that, but there might be others that you could use as well.

Memory

Images take a lot of memory. Besides the flash or SD memory used to store the image, you will also need a decent amount of ram. LVGL seems to load the entire image into ram before using it.

For the image I used, it seemed to take about 32kb of ram. If you run into problems about not having enough memory, you might need to get a smaller image. To get my images to work, I scaled them down with GIMP.

Understanding This LVGL Code

Since you have seen the code, let’s step through each section, so you can understand exactly what each part does. This will make it much easier to actually add images to your future projects…

The Setup

First we will need to import the lvgl library and the imagetools library.

import lvgl,imagetools

The Decoder

To be able to display a PNG image, we need a PNG decoder. To start, we have to create a plain image decoder.

driver = lvgl.img.decoder_create()

Then we will use parts of the imagetools library to modify that decoder to work with PNGs.

driver.open_cb = imagetools.open_png
driver.info_cb = imagetools.get_png_info

Loading the Image

Now we need to load your image from your file system. Replace the “sd/lvgl_test.png” with the path to your image.

file = open('sd/lvgl_test.png','rb')

Next, it loads the image to the raw_image variable.

raw_image = file.read()

Then it closes the now unused file.

file.close()

The Image Descriptor

To begin we create a blank LVGL descriptor.

image_description = lvgl.img_dsc_t()

Then we set the descriptors source to the image we loaded earlier.

image_description.data = raw_image

Lastly we set the descriptors size to the size of the image.

image_description.data_size = len(raw_image)

Displaying The Image

Up to this point we have just been setting up the image. Now we will tell it were we want it to be put on the screen.

First we create a new img object.

image = lvgl.img(lvgl.scr_act())

Then we set the picture it’s going to display to the image descriptor we made before.

image.set_src(image_description)

The image will look nicer if its centered on the screen.

image.center()

Last Touches

Because the image does not fill the whole screen, we will want the background to be black, so we add this line of code.

lvgl.scr_act().set_style_bg_color(lvgl.color_hex(0x000000),0)

What Next?

This method for loading images to micropython LVGL is simple, which is why I like it. Unfortunately, it leaves a little to be desired when it comes to efficiency. With the computers I am using, I am going to have to be careful about how many images I use.

The best part about using PNGs and LVGL is that they are straightforward. You don’t have to recompile your firmware, heavily edit the image on your computer, or create a custom image driver.

blurry image of a lvgl list that looks like a file system.
Here is that image I promised. I scaled it down and changed the color palette which is why it looks blurry.

Filed Under: Micropython

Micropython LVGL Style Example

December 2, 2022 by Baxter

LVGL screen with textarea and  keyboard

If you have ever worked with LVGL, you’ve probably wondered how to change the color of text or the background color of the screen. The way you do that in micropython lvgl is with styles.

When you are finished with this code tutorial today, you will be able to apply styles on the background to a keyboard and to a textarea, and you will now how to set styles to specific parts of an object.

To get this example to work, you will need to add your screen code to the beginning of the example. If you want more information try How to get a LVGL Micropython screen to work.

Here is the code:

import lvgl

ta_style = lvgl.style_t()
ta_style.init()
ta_style.set_bg_color(lvgl.color_hex(0x303030))
ta_style.set_text_color(lvgl.color_hex(0x00df00))

kb_style = lvgl.style_t()
kb_style.init()
kb_style.set_bg_color(lvgl.color_hex(0x000000))

btn_style = lvgl.style_t()
btn_style.init()
btn_style.set_bg_color( lvgl.color_hex( 0x505050 ))
btn_style.set_text_color( lvgl.color_hex( 0x00DF00 ))

obj_style = lvgl.style_t()
obj_style.init()
obj_style.set_bg_color(lvgl.color_hex(0x000000))
lvgl.scr_act().add_style(obj_style,0)

ta = lvgl.textarea( lvgl.scr_act() )
ta.set_size( 320, 100 )
ta.set_pos( 0, 0 )
ta.add_style(ta_style,0)

kyb = lvgl.keyboard( lvgl.scr_act() )
kyb.set_textarea(ta)
kyb.add_style(kb_style,lvgl.PART.MAIN | lvgl.STATE.DEFAULT)
kyb.add_style(btn_style,lvgl.PART.ITEMS | lvgl.STATE.DEFAULT )

How Does This Code Work?

Since you’ve seen the program above, why don’t we walk through the code and see how it works. This should make it clearer how you can modify this code for your own uses…

The Setup

First we have to import the LVGL library so we can use it.

import lvgl

The Textarea Style

To start, we will need a style for the textarea we are going to create later.

ta_style = lvgl.style_t()

Then we need to initialize it so it’s ready to use.

ta_style.init()

Next we want to set the background color to gray (aka. 0x303030).

ta_style.set_bg_color(lvgl.color_hex(0x303030))

Finally we will change the text color to green (aka. 0x00df00)

ta_style.set_text_color(lvgl.color_hex(0x00df00))

The Keyboard Style

We need to divide the keyboard style into two parts: the main keyboard style and the buttons style.

The Main Keyboard Style

To start we will create and initialize the style.

kb_style = lvgl.style_t()
kb_style.init()

Then we will set the keyboard background color to black.

kb_style.set_bg_color(lvgl.color_hex(0x000000))

The Button Style

As usual we create and initialize the new style.

btn_style = lvgl.style_t()
btn_style.init()

Finally we will set the styles background color to light gray (aka. 0x505050) and the text color to green (aka. 0x00DF00).

btn_style.set_bg_color( lvgl.color_hex( 0x505050 ))
btn_style.set_text_color( lvgl.color_hex( 0x00DF00 ))

The Background Style

To begin we will create the new style and initialize it.

obj_style = lvgl.style_t()
obj_style.init()

Then we will set the background color to black.

obj_style.set_bg_color(lvgl.color_hex(0x000000))

Connecting the Background Style Up

To actually use a style we need to add it to an object. The lvgl.scr_act() command gets the screen object and then we can add the obj_style to it.

lvgl.scr_act().add_style(obj_style,0)

Creating the Textarea

To start we will make a new textarea.

ta = lvgl.textarea( lvgl.scr_act() )

After that we will set its size to 320*240 pixels and set its position to 0,0.

ta.set_size( 320, 100 )
ta.set_pos( 0, 0 )

Finally we will need to connect the ta_style we created earlier to our textarea.

ta.add_style(ta_style,0)

Creating the Keyboard

First, we will generate a new keyboard.

kyb = lvgl.keyboard( lvgl.scr_act() )

Next, we will connect the textarea to the keyboard so that when you press a button, the textarea will display the pressed button.

kyb.set_textarea(ta)

Then the main keyboard style gets added. This is the one that sets the keyboards background color to black.

The lvgl.PART.MAIN tells the computer to put the style on the main part of the keyboard and the lvgl.STATE.DEFAULT makes it the default style. The bar( | ) tells the computer to run both of those commands together.

kyb.add_style(kb_style,lvgl.PART.MAIN | lvgl.STATE.DEFAULT)

After that, we will add another style to the keyboard buttons to change their color.

The lvgl.PART.ITEMS adds the style to the keyboards buttons.

kyb.add_style(btn_style,lvgl.PART.ITEMS | lvgl.STATE.DEFAULT )

What Happens Next

Today you have created a keyboard and textarea and styled them with different colors. Styles can also be used to add borders, shadows, opacity, etc. You can find a complete list at the LVGL docs.

The best way to learn code is to experiment. You could try adding other styles properties to the code above or just changing the color of the ones that already exist.

If you would like some more examples on textareas, you could try Micropython LVGL Textarea Examples, and for keyboards, Micropython LVGL Keyboard Examples might be helpful.

Filed Under: Micropython

Micropython LVGL Tileview Example

November 30, 2022 by Baxter

lvgl screen with text that says 'swipe->'

Today, I am going to show you how to create a program that will have multiple pages, and when you swipe the screen, it will change the visible page. Normally, it would take tons of code to pull this off, but with lvgl its a breeze.

To get this example to work you will need to add your screen code to the beginning of the example. If you want more information try How to get a LVGL Micropython screen to work.

Here is the code:

import lvgl

tv = lvgl.tileview( lvgl.scr_act() )

def lb( text, src ):
    label = lvgl.label( src )
    label.set_text( text )
    label.center()

view1 =  tv.add_tile( 0, 0, lvgl.DIR.HOR )
lb( 'swipe ->', view1 )

view2 = tv.add_tile( 1, 0, lvgl.DIR.HOR )
lb( 'keep going ->', view2 )

view3 = tv.add_tile( 2, 0, lvgl.DIR.HOR | lvgl.DIR.BOTTOM )
lb( 'swipe down if you like', view3 )

view4 = tv.add_tile( 2, 1, 0 )

btn = lvgl.btn( view4 )
btn.center()

lb( 'Back', btn )

def handler( data ):
    tv.set_tile_id( 0, 0, lvgl.ANIM.ON )

btn.add_event_cb( handler, lvgl.EVENT.CLICKED, None )

Understanding This LVGL Code

Ok, now that you have seen all the code, let’s step through each section, so you can understand exactly what each part does. This will make it much easier to actually use the code in your future projects…

Setting Up the LVGL Tileview

First thing we have to do is import the lvgl graphics library.

import lvgl

Secondly, we create the tileview. For simplicity, we will abbreviate it as tv.

tv = lvgl.tileview( lvgl.scr_act() )

The Label Function

We are going to make a function to build the content of the pages that we will create later. Lets give it the name ‘lb’.

def lb( text, src ):

Then we want it to create a label on the current page (aka. src).

    label = lvgl.label( src )

After that the label should be given some text to display.

    label.set_text( text )

The label will look nicer if it’s in the center of the page.

    label.center()

Creating the First Page

We will start by creating a empty tile (what I call a page). The first two numbers are the column and row of the tile, and the lvgl.DIR.XYZ is the direction you are allowed to swipe in.

view1 = tv.add_tile( 0, 0, lvgl.DIR.HOR )

Because we created the ‘lb’ function earlier, all we have to do now is call it.

lb( 'swipe ->', view1 )

The Second Page

Setting this page up is practically identical to setting up the first one.

view2 = tv.add_tile( 1, 0, lvgl.DIR.HOR )
lb( 'keep going ->', view2 )

The Third Page

Except for the fact that this page allows you to swipe down as well as horizontally, it’s the same as the last two pages.

view3 = tv.add_tile( 2, 0, lvgl.DIR.HOR | lvgl.DIR.BOTTOM )
lb( 'swipe down if you like', view3 )

The Last Page

This page is a little more interesting. As usual we will start out by creating a new tile.

view4 = tv.add_tile( 2, 1, 0 )

For the last page we will want a button instead of a label. Like the label, we will also want to center it.

btn = lvgl.btn( view4 )
btn.center()

We want the button to have text, so that old ‘lb’ function can be reused to give the button a label.

lb( 'Back', btn )

When the button is pressed we want it to take us back to the first page. First thing we need to do is code a function to take us back.

def handler( data ):
    tv.set_tile_id( 0, 0, lvgl.ANIM.ON )

Then we connect that function to the button so that when its clicked the function is called.

btn.add_event_cb( handler, lvgl.EVENT.CLICKED, None )

What happens next?

Now that you know how tileviews work and how to create their pages, try running the code above, you will see how when you swipe, it smoothly transitions to the next page, and when you press the button on the last page, it takes you back to the first page.

Experimenting with code is a great way to learn, maybe you could add more pages or rearrange the ones that already exist.  

Filed Under: Micropython

Micropython LVGL Msgbox Example

November 28, 2022 by Baxter

The micropython lvgl library can be hard to find any good information on, so today I am going to give you some code and explain it. When you finish, you will have a program that creates 3 different message boxes and by pressing different buttons you will be able to pick which one is on the screen.

To get this example to work you will need to add your screen code to the beginning of the example. If you want more information try How to get a LVGL Micropython screen to work.

Here is the code:

import lvgl

msg = lvgl.msgbox( None, 'WHERE?', 'where do you want to go?', ['pre', 'next'], True )
msg.center()

def handler( data ):
    msg = data.get_current_target()
    btn = msg.get_active_btn_text()
    if btn == 'next':
        msg.close()
        new = lvgl.msgbox( lvgl.scr_act(), 'HERE', 'message here', None, True )
        new.center()
    elif btn == 'pre':
        msg.close()
        new = lvgl.msgbox( None, 'HELLO', 'you made it', None, False )
        new.center()
        title = new.get_title()
        title.set_style_text_color( lvgl.color_hex(0x0FFF00), 0 )

msg.add_event_cb( handler, lvgl.EVENT.VALUE_CHANGED, None )

Understanding This LVGL Code

Ok, now that you have seen all the code, let’s step through each section, so you can understand exactly what each part does. This will make it much easier to actually use the code in your future projects…

The Import Command

This adds the lvgl library to our project so we can use it.

import lvgl

Creating the Msgbox

This code creates a msgbox with the title ‘WHERE?’, the description ‘where do you want to go?’, and two buttons ‘pre’ and ‘next’.

msg = lvgl.msgbox( None, 'WHERE?', 'where do you want to go?', ['pre', 'next'], True )

Centering the Msgbox

It looks nicer if the box is in the center of the screen, so we add this.

msg.center()

Creating the Event Handler

This function will be used to control the active msgbox. The next steps will be mostly setting up the controls.

def handler( data ):

Finding the Button

First we need to find which button was pressed. This is the code that does that.

    msg = data.get_current_target()
    btn = msg.get_active_btn_text()

Check the Pressed button

Then we check if the pressed button has the text ‘next’. If it does, we will close the old msgbox and create a new one.

    if btn == 'next':
        msg.close()
        new = lvgl.msgbox( lvgl.scr_act(), 'HERE', 'message here', None, True )
        new.center()

Check again

If the button was not the ‘next’ button, we will check if it was the ‘pre’ button. If it is, the code will close the old box, create another box, and change the title color on the new box.

    elif btn == 'pre':
        msg.close()
        new = lvgl.msgbox( None, 'HELLO', 'you made it', None, False )
        new.center()
        title = new.get_title()
        title.set_style_text_color( lvgl.color_hex(0x0FFF00), 0 )

Start the Handler

Finally we connect the handler function to the msgbox, so when a button is pressed the function is called.

msg.add_event_cb( handler, lvgl.EVENT.VALUE_CHANGED, None )

What Happens Next

Once you have this code running, a message box will appear if you press the ‘pre’ button and a new box will appear and will have a green title. If on the first box you had pressed the ‘next’ button, it would make a new message box with some text and a close button.

Experimenting with examples is a great way to learn code. If you modified this code, you could make a popup that warns you if your battery is low or a notification that your board has finished downloading a file.

 

Filed Under: Micropython

Micropython LVGL List Example

November 23, 2022 by Baxter

pretend file system with a lvgl list containing folders and other things

As you probably already know, LVGL is a graphics library. What I’d like to show you today is how to create a list of items, and assign those items to a print command that you can see in the terminal window. This can be really helpful if you are trying to create your own filesystem.

Also, with just a little code modification, you could use this for working directly with any button that is clicked by the user.

When you are finished with this code tutorial today, you’ll be able to tap any of the items in this list, and you’ll see the name of the list item that you clicked in the terminal window. You can then develop additional code that will allow you to interact with this code to create additional actions.

To get this example to work you will need to add your screen code to the beginning of the example. If you want more information try How to get a LVGL Micropython screen to work.

Let’s get started with the code below…

import lvgl

def handler( data ):
    theBTN = data.get_target()
    print( ls.get_btn_text( theBTN ))
    

ls = lvgl.list( lvgl.scr_act() )
ls.set_size( 320, 240 )
ls.center()

ls.add_text( 'Internal' )

BACK = ls.add_btn( lvgl.SYMBOL.HOME, 'BACK' )
BACK.add_event_cb( handler, lvgl.EVENT.CLICKED, None )

FOLDER_1 = ls.add_btn( lvgl.SYMBOL.DIRECTORY, 'FOLDER 1' )
FOLDER_1.add_event_cb( handler, lvgl.EVENT.CLICKED, None )

FOLDER_2 = ls.add_btn( lvgl.SYMBOL.DIRECTORY, 'FOLDER 2' )
FOLDER_2.add_event_cb( handler, lvgl.EVENT.CLICKED, None )

FILE = ls.add_btn( lvgl.SYMBOL.FILE, 'FILE 1' )
FILE.add_event_cb( handler, lvgl.EVENT.CLICKED, None )

ls.add_text( 'External' )

CARD = ls.add_btn( lvgl.SYMBOL.SD_CARD, 'ED\'s Card' )
CARD.add_event_cb( handler, lvgl.EVENT.CLICKED, None )

DRIVE = ls.add_btn( lvgl.SYMBOL.USB, 'DRIVE LARGE' )
DRIVE.add_event_cb( handler, lvgl.EVENT.CLICKED, None )

Understanding This LVGL Code

Ok, now that you have seen all the code, let’s step through each section, so you can understand exactly what each part does. This will make it much easier to actually use the code in your future projects…

The Import Command

This basically imports the lvgl library, so we can use it to put things on our display.

import lvgl

The Terminal Printing Code

First, we will declare the handler function. Next, we will get the target button and store it in a variable. Finally, we will use the print command to print the name of the button to the terminal window.

def handler( data ):
     theBTN = data.get_target()
     print( ls.get_btn_text( theBTN ))

Creating The List And Declaring The Size

Here, we are telling LVGL that we want to use the list feature. We will also declare the size of the list and use and some formatting options so that our list displays correctly on the screen.

ls = lvgl.list( lvgl.scr_act() )
ls.set_size( 320, 240 )
ls.center()

Adding The First Label To The FileSystem View

We’ll use the add_text command to print a label to our screen with the word, “Internal”.

ls.add_text( 'Internal' )

Adding The Back/Home Button

Here, we are going to give our button the name “Back”, and then we will add the button to the display, and use the LVGL symbol “HOME” so that it shows on the screen.

You can see in the example screenshot that this symbol is a little house icon. In a true file system, clicking this button would take the user back to the main page or home page of the application.

BACK = ls.add_btn( lvgl.SYMBOL.HOME, 'BACK' )
BACK.add_event_cb( handler, lvgl.EVENT.CLICKED, None )

Adding The Folder Icons To The FileSystem List

In this code, we will create two more filesystem items to add to our list. We will give the items the names “Folder 1” and “Folder 2”, which will be shown on the display.

Notice that this time, we have chosen to use the LVGL symbol for a Directory, which is this folder icon you see in the example above.

FOLDER_1 = ls.add_btn( lvgl.SYMBOL.DIRECTORY, 'FOLDER 1' ) FOLDER_1.add_event_cb( handler, lvgl.EVENT.CLICKED, None )

FOLDER_2 = ls.add_btn( lvgl.SYMBOL.DIRECTORY, 'FOLDER 2' ) FOLDER_2.add_event_cb( handler, lvgl.EVENT.CLICKED, None )

Adding Files To The FileSystem List

This code is very similar to the Folder code above, but in this case, we are going to display a file icon and call the item, ‘FILE 1’. All the other code is the same as in the previous example.

FILE = ls.add_btn( lvgl.SYMBOL.FILE, 'FILE 1' ) 
FILE.add_event_cb( handler, lvgl.EVENT.CLICKED, None )

Adding Additional Labels To The FileSystem List

This code below is identical to the “Internal” label that you added earlier. You can see how you can use the combination of labels and icons to create a visual filesystem that has structure to it.

ls.add_text( 'External' )

Adding External Memory/SD Card Icons To The FileSystem List

This code will declare a “Card” button value, and we will use the built in LVGL symbol for an SD card. In a filesystem, you could use this to allow users to access the filesystem of an inserted SD card into their device.

CARD = ls.add_btn( lvgl.SYMBOL.SD_CARD, 'ED\'s Card' )
CARD.add_event_cb( handler, lvgl.EVENT.CLICKED, None )

Adding External Drive Icons To The FileSystem List

This code is very similar to the code above, but uses the USB icon from the LVGL library. You can imagine how you might use this icon and structure in a real filesystem list that you would interact with on your device.

DRIVE = ls.add_btn( lvgl.SYMBOL.USB, 'DRIVE LARGE' )
DRIVE.add_event_cb( handler, lvgl.EVENT.CLICKED, None )

What Happens Next?

Ok, now that you understand how this code works and can use the different types of symbols and the LVGL list feature, you can see how this can be used to build your own filesystem.

Try out the code above, and notice that when you click on any of the menu items, you will see the name of the button that was clicked on your terminal screen.

Now, you can imagine how this can be used to interact with the touch screen. You could develop your code further so that when a button is pushed, and the terminal reads the data, it then begins to process this further and execute additional code.

Using this simple code is a great way to get started with building a working filesystem so that you can interact with stored information on your device.

If you haven’t learned how to use the Flex and Grid layout features, these micropython lvgl layout examples would be a good place to go next to learn these features…

Filed Under: Micropython

Micropython LVGL Layout Examples

November 21, 2022 by Baxter

lvgl grid layout made of 5 boxes

What layouts are there?

Lvgl currently supports two layouts: FLEX and GRID. The flex layout is simple, but not very controllable.

The grid layout is more complicated, but gives you more control. This page has examples for both of them.

How to use these examples

To get these examples to work you will need to add some screen code to the beginning of each example. If you want more information try How to get a LVGL Micropython screen to work.

lvgl flex layout with three box stacked vertically

LVGL Flex Basic Example

This code creates a container with the flex layout enabled, and puts three objects in it.

import lvgl

cont = lvgl.obj( lvgl.scr_act() )
cont.set_size( 320, 240 )
cont.center()
cont.set_layout( lvgl.LAYOUT_FLEX.value )
cont.set_flex_flow( lvgl.FLEX_FLOW.COLUMN )

def area( text, width, height ):
    area = lvgl.obj( cont )
    area.set_size( width, height )
    
    label = lvgl.label( area )
    label.set_text( text )
    label.center()
    
    return area

area( 'one', 70, 70 )
area( 'two', 70, 70 )
area( 'three', 70, 70 )
lvgl flex layout made of 6 box

Flex Grow and New Line

Items in a flex layout can be set to fill any unused space in their row or column. This code shows how to set that grow factor, and how to force items onto new lines.

import lvgl

cont = lvgl.obj( lvgl.scr_act() )
cont.set_size( 320, 240 )
cont.center()
cont.set_layout( lvgl.LAYOUT_FLEX.value )
cont.set_flex_flow( lvgl.FLEX_FLOW.ROW_WRAP )

def area( text, width, height ):
    area = lvgl.obj( cont )
    area.set_size( width, height )
    
    label = lvgl.label( area )
    label.set_text( text )
    label.center()    
    return area

def grow_area( text, height, grow ):
    area = lvgl.obj( cont )
    area.set_flex_grow( grow )
    area.set_height( height )
    label = lvgl.label( area )
    label.set_text( text )
    label.center()    
    return area

area( 'one', 70, 45 )
grow_area( 'two', 50, 1 )
grow_area( 'three', 50, 2 )
grow_area( 'four', 50, 1 ).add_flag( lvgl.obj.FLAG.LAYOUT_1 ) #this is how you start a new line
area( 'five', 70, 50 )
area( 'six', 70, 50 )
lvgl layout with 6 boxes

LVGL Flex Align

The flex layout allows you to align its tracks (the rows or columns), and to align the items in the tracks. You can also tell it how to align different sized objects.

import lvgl

cont = lvgl.obj( lvgl.scr_act() )
cont.set_size( 320, 240 )
cont.center( )
cont.set_layout( lvgl.LAYOUT_FLEX.value )
cont.set_flex_flow( lvgl.FLEX_FLOW.ROW_WRAP )
cont.set_flex_align( lvgl.FLEX_ALIGN.SPACE_EVENLY, lvgl.FLEX_ALIGN.END, lvgl.FLEX_ALIGN.CENTER )

def area( text, width, height ):
    area = lvgl.obj( cont )
    area.set_size( width, height )
    
    label = lvgl.label( area )
    label.set_text( text )
    label.center()
    
    return area

area( 'one', 80, 50 )
area( 'two', 80, 70 )
area( 'three', 80, 70 )
area( 'four', 80, 70 )
area( 'five', 80, 50 )
area( 'five', 80, 50 )
lvgl grid layout with 4 buttons

LVGL Grid Basic Example

This Grid layout, as you can see, takes a little more code to get working. But if you can get them to work, they look pretty good.

import lvgl

#you can replace the lvgl.grid_fr(x) with pixel width/height
row = [ lvgl.grid_fr(1), lvgl.grid_fr(1), lvgl.GRID_TEMPLATE.LAST ]
column = [ lvgl.grid_fr(1), lvgl.grid_fr(1), lvgl.GRID_TEMPLATE.LAST ]

cont = lvgl.obj( lvgl.scr_act() )
cont.set_size( 200, 200 )
cont.center()
cont.set_layout( lvgl.LAYOUT_GRID.value )
cont.set_style_grid_row_dsc_array( row, 0 ) 
cont.set_style_grid_column_dsc_array( column, 0 )

def button( text, row_x, column_y ):
    btn = lvgl.btn( cont )
    
    label = lvgl.label( btn )
    label.set_text( text )
    label.center()
    
    btn.set_grid_cell( lvgl.GRID_ALIGN.STRETCH, row_x, 1, lvgl.GRID_ALIGN.CENTER, column_y, 1 )
    
    return btn

button( 'hi1', 0, 0 )
button( 'hi2', 1, 0 )
button( 'hi3', 0, 1 )
button( 'hi4', 1, 1 )
lvgl grid layout with 5 boxes

Spacing and Grids

The grid layout allows you to make items span multiple rows or columns.

import lvgl

row = [ lvgl.grid_fr(1), lvgl.grid_fr(1), lvgl.grid_fr(1), lvgl.grid_fr(1), lvgl.GRID_TEMPLATE.LAST ]
column = [ lvgl.grid_fr(1), lvgl.grid_fr(1), lvgl.grid_fr(1), lvgl.grid_fr(1), lvgl.GRID_TEMPLATE.LAST ]
 
cont = lvgl.obj( lvgl.scr_act() )
cont.set_size( 320, 240 )
cont.center()
cont.set_layout( lvgl.LAYOUT_GRID.value )
cont.set_style_grid_row_dsc_array( row, 0 )
cont.set_style_grid_column_dsc_array( column, 0 )

def area( text, row_x, column_y, width, height ):
    area = lvgl.obj( cont )
    
    label = lvgl.label( area )
    label.set_text( text )
    label.center()
    
    area.set_grid_cell( lvgl.GRID_ALIGN.STRETCH, row_x, width, lvgl.GRID_ALIGN.STRETCH, column_y, height )
    
    return area

area( 'tall', 0, 0, 1, 2 )
area( 'short', 1, 0, 3, 1 )
area( 'tiny', 1, 1, 1, 1 )
area( 'small', 2, 1, 2, 1 )
area( 'long', 0, 2, 4, 2 )

Whats next

Today you have learned how to use both the flex and grid layouts. If you want some more practice, try experimenting with the code. Maybe you could mix buttons and objects in your layouts and see what happens.

Whatever you do, knowing how to use layouts is a great skill to have.

Filed Under: Micropython

Micropython LVGL Textarea Examples

November 17, 2022 by Baxter

lvgl textarea with green cursor

What Are Textareas?

Textareas are intended for places where you need to display text that changes often. They are usually used with keyboards or some input device. This post has examples on creating textareas, styling cursors, and creating password textareas.

How to use these examples

To get these examples to work you will need to add some screen code to the beginning of each example. If you want more information try How to get a LVGL Micropython screen to work.

lvgl text area that says 'Hello World'

The Basic Example

This code creates a textarea and shows how adding text works.

import lvgl
from time import sleep

textarea = lvgl.textarea( lvgl.scr_act() )
textarea.set_text( 'Hello ' )
textarea.center()

sleep( 1 )
textarea.add_text( 'W' )
sleep( 0.5 )
textarea.add_text( 'o' )
sleep( 0.5 )
textarea.add_text( 'rld' )
sleep( 0.5 )
textarea.set_cursor_pos( 0 )
lvgl text area that says 'Hello world is'

Maximum Characters and One Line Mode

LVGL allows you to set the textarea to only use one line and to set the maximum number of characters that are allowed in the textarea.

import lvgl

textarea = lvgl.textarea( lvgl.scr_act() )
textarea.set_max_length( 15 )
textarea.center()
textarea.set_text( 'Hello world is to long' )
textarea.set_one_line( True )
textarea.set_cursor_click_pos( True )

#does not work with touch input might with mouse
textarea.set_text_selection( True )
lvgl text area that says 'heo word with out the'

Accepted Characters

Textareas can filter out any character they don’t want and not put them on the screen. This code creates a textarea that does not display the letter L.

import lvgl

textarea = lvgl.textarea( lvgl.scr_act() )

#l is the only letter not accepted
textarea.set_accepted_chars( 'abcdefghijk mnopqrstuvwxyz' )
textarea.set_text( 'hello world with out the l' )
textarea.center()
micropython lvgl textarea password

Passwords and Textareas

One of the features of micropython textareas is that they can be used for passwords. This code show the most import features of password textareas.

import lvgl
from time import sleep

textarea = lvgl.textarea( lvgl.scr_act() )
textarea.set_text( 'super secret text' )
textarea.center()
textarea.set_password_mode( True )
sleep( 2.5 )

#the bullet should be only one character long
textarea.set_password_bullet( 'O' )

#update text area to see new bullet
textarea.add_text( '' )

print( textarea.get_text() )
lvgl text area with cursor

The Cursor

Micropython lvgl allows you to create and style cursors. This code will randomly move the cursor around the screen and add some styles to it.

import lvgl
from time import sleep

textarea = lvgl.textarea( lvgl.scr_act() )
textarea.set_text( 'Hello this is some random text prettend it does not exist just forget what it says wait is it possible to forget what does not exist' )
textarea.center()

cursor_style = lvgl.style_t()
cursor_style.set_border_side( lvgl.BORDER_SIDE.TOP | lvgl.BORDER_SIDE.BOTTOM )
cursor_style.set_bg_color( lvgl.color_hex( 0x009A00 ))
cursor_style.set_bg_opa( 100 )

textarea.add_style( cursor_style,lvgl.PART.CURSOR | lvgl.STATE.FOCUSED )

textarea.add_state( lvgl.STATE.FOCUSED )

while True:
    textarea.set_cursor_pos( lvgl.rand( 0, 70 ))
    sleep( 0.5 )
    textarea.cursor_right()
    sleep( 0.5 )
    textarea.cursor_down()
    sleep( 0.5 )
    textarea.cursor_left()
    sleep( 0.5 )
    textarea.cursor_up()
    sleep( 0.5 )

Where to go next

Textareas are nice, but they are not super useful with out an input. A great input to use is keyboards.

You can wire up a custom keyboard with physical buttons, but if you want a simpler (but more annoying) keyboard, you could try micropython lvgl keyboard examples. The last example has code for connecting keyboards to textareas.

Filed Under: Micropython

How To Get A LVGL Micropython Screen To Work

November 16, 2022 by Baxter

The ILI9341 Display

This is some simple code to get a ili9341 display to work with micropython lvgl. You will want to add it, or your own screen code, to the beginning of most of the lvgl examples on this site.

Instead of putting it at the beginning, you could just put the code in the micropython boot file. It seems to work best there.

import lvgl

#this is the code that works for my screen:
#from ili9XXX import ili9341
#screen = ili9341(mosi=23, miso=38, clk=18, dc=15, cs=5, invert=True rot=0x10, width=320, height=240 )

# this is some generic code that might work better for you
from ili9XXX import ili9341
screen = ili9341(mosi=23, miso=38, clk=18, dc=15, cs=5,width=320, height=240)

#You will want some touch screen code
#here is what I use
from ft6x36 import ft6x36
touch = ft6x36()

Filed Under: Micropython

Next Page »

Recent Posts

  • How to Shorten Your Micropython LVGL Code
  • FreeRTOS Mutex Example
  • FreeRTOS Task Notification Example
  • FreeRTOS Queue Example
  • FreeRTOS Semaphore Example

Categories

  • Arduino
  • ESP32 Web Page
  • FreeRTOS
  • LEGO Technic
  • LEGO tips
  • LEGO war robots
  • Lego's
  • Legos
  • LEGOS-build your own
  • Micropython

Copyright © 2025 · Minimum Pro Theme on Genesis Framework · WordPress · Log in