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.