Fork me on GitHub

Using libev and libssd1306 to display a clock


In the earlier post demonstrating the use of libssd1306, we did not explain how one would use an event library such as libev to write an application that would use events to perform display changes. With this post we would like to show how to do that easily with a simple clock application that updates the OLED screen with the local time every second.

USING WITH libev

The example code is present in examples/libev_clock.c and is another application to test if the OLED screen is working or not. You can run it as below:

$ ./examples/test_libev_clock

This will run the application which will show that every second the local time in the format HH:MM:SS is being sent to the OLED screen using the I2C interface on the screen. With libssd1306 it is incredibly easy to make this display without handling any I2C commands yourself or even reading the datasheets.

UNDERSTANDING THE CODE

The code is well commented but we explain it in sections here.

Initializing the OLED screen

The first part of the main() function involves initializing the I2C device and initializing it. By default the device is assumed to be /dev/i2c-1 on the Raspberry Pi and the width x height is assumed to be 128x32 pixels. However, the user can pass the device path and the height as the command line arguments like below, for instance if they want to use a 128x64 pixel screen.

$ ./examples/test_libev_clock /dev/i2c-1 64

The initialization code will fail if the device path is incorrect or if the device is disconnected or other I2C failure has occurred. The application will exit if it cannot find the OLED screen on the I2C bus of the Raspberry Pi.

In addition to initializing the OLED screen, we also have to create a framebuffer object so that we can draw on the screen. Recall, from the earlier post, that we use a framebuffer object so that we can fill the screen contents in memory first before actually displaying it to the screen. This way we can support multiple framebuffer objects and are able to build several screens even before they have been displayed. This can be very useful for displaying games and motion picture images on the OLED screen. The I2C interface is slow, and being able to hold a lot of pre-drawn screens in memory can be very useful for good user experience.

Below is the section of the main() function doing the initialization of the display and also the creation of the framebuffer.

    ssd1306_i2c_t *oled = ssd1306_i2c_open(filename, 0x3c, 128, (uint8_t)height, NULL);
    if (!oled) {
        return -1;
    }
    /* initialize the I2C device */
    if (ssd1306_i2c_display_initialize(oled) < 0) {
        fprintf(stderr, "ERROR: Failed to initialize the display. Check if it is connected !\n");
        ssd1306_i2c_close(oled);
        return -1;
    }
    /* clear the display */
    ssd1306_i2c_display_clear(oled);
    /* create a framebuffer */
    ssd1306_framebuffer_t *fbp = ssd1306_framebuffer_create(oled->width, oled->height, oled->err);

Setting up the event loop

We now need to setup the event loop and timer callback. We want the event loop to invoke a callback every 1 second. For our example code, we also have it timeout after 30 seconds so that we can run this application as part of our test suite by running make check.

The idea is that the callback will then calculate the local time in HH:MM:SS format, print it to the framebuffer and then display the framebuffer on the OLED screen all at once in the callback.

However, you may notice that the ssd1306 object is in the main() function and we need to make it available for the callback. One way is to make the object global, but we have used the void *data member of the ev_timer object to pass the pointers we need into the callback. This allows us to make the callback re-entrant.

A single void *data maybe good if we want to pass a single pointer variable, but if we want to pass multiple variables to the callback we need to create a local struct with all the variables and then pass a pointer to that struct to the callback. We do that by creating our own i2c_clock_t structure in the code.

typedef struct {
    ssd1306_i2c_t *oled; /* the libssd1306 I2C object */
    ssd1306_framebuffer_t *fbp; /* the framebuffer object */
    int call_count; /* for calculating the 30 second timeout */
} i2c_clock_t;

Then we create an object instance of this struct in the main() function, followed by the setup of the event loop and the timer object. We initialize the timer object, set the data pointer and call start on the timer to run each second.

    /* create an object to send to the callbacks */
    i2c_clock_t timer_data = {
        .oled = oled, /* the object created earlier */
        .fbp = fbp, /* created earlier */
        .call_count = 30 /* timeout after 30 seconds */
    };

    /* create the loop variable by using the default */
    struct ev_loop *loop = EV_DEFAULT;

    /* create the timer event object */
    ev_timer timer_watcher = { 0 };

    /* initialize the timer to update each second and invoke callback */
    /* here the callback name is onesec_timer_cb */
    ev_timer_init(&timer_watcher, onesec_timer_cb, 1., 1.);

    /* set the data pointer for the callback to use it */
    timer_watcher.data = &timer_data;

    /* start the timer */
    ev_timer_start(loop, &timer_watcher);
    ev_run(loop, 0);

    /* cleanup before exiting main() */
    ssd1306_framebuffer_destroy(fbp);
    ssd1306_i2c_close(oled);

In the end, following the ev_run call in the main() function, we also cleanup the ssd1306_* objects.

Defining the timer callback

As seen above, the timer callback is called onesec_timer_cb which follows the libev structure of callbacks for all its event types. We reproduce it below.

The callback first calculates the local time using localtime_r() function from the C library. We choose this function since it is re-entrant and it makes sense to use a re-entrant function in the callback, especially if you are going to be doing multi-threading. Then we print the time to a buffer of 16 bytes, even though the format HH:MM:SS needs no more than 8-bytes but just to be safe we use 16.

The w argument is the ev_timer object that we created in the main() function, and will have the void *data member point to the timer_data object of type i2c_clock_t that we had created above. So we cast the data pointer to the i2c_clock_t * type and access the ssd1306 objects for the OLED screen and the framebuffer.

We then print the text buffer to the framebuffer object using the default font, and then push it via I2C to the OLED screen so that it displays the time in HH:MM:SS format.

For the timeout, we count down until we time out, so after 30 seconds.

void onesec_timer_cb(EV_P_ ev_timer *w, int revents)
{
    /* calculate the local time */
    struct tm _tm = { 0 };
    time_t _tsec = time(NULL);
    localtime_r(&_tsec, &_tm);

    /* print it to a buffer safely */
    char buf[16] = { 0 };
    snprintf(buf, sizeof(buf) - 1, "%02d:%02d:%02d", _tm.tm_hour, _tm.tm_min,
_tm.tm_sec);
    
    /* log the time to console to verify that it is correct */
    printf("INFO: Time is %s\n", buf);

    if (w) {
        /* retrieve the object that we set earlier in main() */
        i2c_clock_t *i2c = (i2c_clock_t *)(w->data);

        /* write the time buffer to framebuffer first */
        ssd1306_framebuffer_clear(i2c->fbp);
        ssd1306_framebuffer_box_t bbox;
        ssd1306_framebuffer_draw_text(i2c->fbp, buf, 0, 32, 16, SSD1306_FONT_DEFAULT, 4, &bbox);

        /* update the OLED screen with the framebuffer */
        if (ssd1306_i2c_display_update(i2c->oled, i2c->fbp) < 0) {
            fprintf(stderr, "ERROR: failed to update I2C display, exiting...\n");
            ev_break(EV_A_ EVBREAK_ALL);
        }

        /* count down until we time out */
        if ((i2c->call_count--) <= 0) {
            ev_break(EV_A_ EVBREAK_ALL);
        }
    }
}

DEMONSTRATION

Here is a short video of the demo at https://youtu.be/T_ox1At1x-o.