Encoder input on Linux using evdev driver

Description

I’m having a fair amount of difficulty getting an encoder to “click” a button. I have two device nodes: /dev/input/event0 which corresponds to the encoder axis, and I have /dev/input/event1 which corresponds to the “click” mechanism.

When I start my program, I am able to change focus between two buttons. Once I click one of them, though, the program no longer responds to the axis input, and the focus remains on the clicked button. Long-pressing does nothing. I have further confirmed through GDB that the lv_timer_handler() is “canceling” any long-presses. Shortly after my callback sends the LV_INDEV_STATE_PRESSED, I find that the callback forces it back to “released” even if I have the button held down as I run the debugger.

What MCU/Processor/Board and compiler are you using?

Luckfox Lyra B (embedded Linux)

What do you want to achieve?

I want the encoder to function as described in the documentation. Specifically, clicking a button should not prevent me from changing the focus to another one.

What have you tried so far?

All of the following include adding my lv_indev_t * and buttons to an lv_group_t*.

  1. One callback to handle both encoder and encoder button
  2. Separate callbacks for both encoder and button
  3. Changing the button type to LV_INDEV_TYPE_KEYPAD
  4. Adding a dummy event callback to both buttons
  5. Forcing the group to not allow editing/freezing
  6. Setting the indev long press time

Code to reproduce

Add the relevant code snippets here.
NOTE Not all of the code here is seen in the repro video. I can provide that if it is necessary for further analysis. This should be able to reproduce the behavior seen.

void encoder_read(lv_indev_t*, lv_indev_data_t*);
void *tick_thread(void *args) 
{
    while(1) {
        usleep(5000);   /* Sleep for 5000 microseconds (5 milliseconds) */
        lv_tick_inc(5);     /* Tell LVGL that 5 milliseconds have elapsed */
    }
}
static int encoder_wheel_fd, encoder_btn_fd;
int main(int argc, const char ** argv) {
    /* DRM and Display Init */
    const char *device = "/dev/dri/card0";
    lv_display_t *disp = lv_linux_drm_create();
    lv_linux_drm_set_file(disp, device, -1);

    /* Create dummy Encoder input (because LVGL doesn't handle evdev correctly) */
    lv_group_t * g = lv_group_create();
    lv_indev_t *scroll = lv_indev_create();
    lv_indev_set_type(scroll, LV_INDEV_TYPE_ENCODER);
    lv_indev_set_read_cb(scroll, encoder_read);
    lv_indev_set_group(scroll, g);

    /* Use evdev to open encoder nodes */
    encoder_wheel_fd = open("/dev/input/event0", O_RDONLY | O_NONBLOCK | O_CLOEXEC);
    encoder_btn_fd = open("/dev/input/event1", O_RDONLY | O_NONBLOCK | O_CLOEXEC);

    if (encoder_wheel_fd < 0 || encoder_btn_fd < 0)
    {
        LV_LOG_WARN("Encoder failed to initialize fully");
    }
    /* Create Menu Buttons */

    lv_style_t style_btn_press;
    lv_style_init(&style_btn_press);
    lv_style_set_border_color(&style_btn_press, lv_color_make(0xFF, 0xFF, 0xFF));
    lv_style_set_border_width(&style_btn_press, 2);

    lv_obj_t * menuicon1 = lv_imagebutton_create(lv_screen_active());
    lv_imagebutton_set_src(menuicon1, LV_IMAGEBUTTON_STATE_RELEASED, NULL, "A:/menu.png", NULL);
    lv_obj_add_style(menuicon1, &style_btn_press, LV_STATE_FOCUSED);
    lv_obj_align(menuicon1, LV_ALIGN_BOTTOM_MID, -64, 0);
    lv_group_add_obj(g, menuicon1);

    lv_obj_t * menuicon = lv_imagebutton_create(lv_screen_active());
    lv_imagebutton_set_src(menuicon, LV_IMAGEBUTTON_STATE_RELEASED, NULL, "A:/menu.png", NULL);
    lv_obj_add_style(menuicon, &style_btn_press, LV_STATE_FOCUSED);
    lv_obj_align(menuicon, LV_ALIGN_BOTTOM_MID, 0, 0);

    lv_group_add_obj(g, menuicon);

    pthread_t thread_id;
    pthread_create(&thread_id, NULL, tick_thread, NULL);
    /* Handle LVGL tasks */
    while (1) {
        lv_timer_handler();
        usleep(5000);
    }

    close(encoder_wheel_fd);
    close (encoder_btn_fd);

    return 0;
}

void encoder_read(lv_indev_t * indev, lv_indev_data_t * data)
{
    struct input_event ev1;
    ssize_t n1 = read(encoder_wheel_fd, &ev1, sizeof(struct input_event));
    struct input_event ev2;
    ssize_t n2 = read(encoder_btn_fd, &ev2, sizeof(struct input_event));

    if (n1 == (ssize_t)sizeof(struct input_event) && ev1.type != 0)
    {
        data->enc_diff = ev1.value;
    }

    if (n2 == (ssize_t)sizeof(struct input_event) && ev2.type != 0)
    {
        if (ev2.value == 0)
        {
            data->state = LV_INDEV_STATE_PRESSED;
            data->key = LV_KEY_ENTER;


        }
        else
        {
            data->state = LV_INDEV_STATE_RELEASED;
        }
    }

}

Screenshot and/or video

repro_vid.zip (3.3 MB)

I believe I have found a solution to my problem, but I would appreciate if someone else could comment on this since, as it is not mentioned anywhere in the documentation and it makes absolutely no sense at all, I feel this is a bug.

The solution I had is to add a static “is-pressed” variable to my callback to keep track of the encoder’s previous state. The new code is as follows:

void encoder_read(lv_indev_t * indev, lv_indev_data_t * data)
{
    // NEW - KEEP TRACK OF ENCODER STATE
    static encoder_state;

    struct input_event ev1;
    ssize_t n1 = read(encoder_wheel_fd, &ev1, sizeof(struct input_event));
    struct input_event ev2;
    ssize_t n2 = read(encoder_btn_fd, &ev2, sizeof(struct input_event));

    if (n1 == (ssize_t)sizeof(struct input_event) && ev1.type != 0)
    {
        data->enc_diff = ev1.value;
    }

    if (n2 == (ssize_t)sizeof(struct input_event) && ev2.type != 0)
    {
        if (ev2.value == 0)
        {
            // NEW -- DO NOT WRITE DIRECTLY TO DATA->STATE
            encoder_state = LV_INDEV_STATE_PRESSED;
            data->key = LV_KEY_ENTER;
        }
        else
        {
            // NEW -- DO NOT WRITE DIRECTLY TO DATA->STATE
            encoder_state = LV_INDEV_STATE_RELEASED;
        }
    }
    data->state = encoder_state;
}

Now for my commentary. Nowhere in the encoder documentation is maintaining the encoder button state listed as something the programmer is responsible for. Furthermore, I assume that LVGL would keep track of the previous/current encoder states, seeing as I am providing only “deltas” in this callback.

I am open to being wrong in my understanding of how this works, but at the very least I would strongly suggest a documentation update, as this subtle misunderstanding has cost me days.