Using the Button input device

What I want to do

Une one of the physical buttons of my M5Stack to simulate a click on a logical button on the screen

My setup

A MStack running the latest micropython with lvgl bindings (I included the modification described in https://forum.lvgl.io/t/hardware-button-multiple-points/1953/7)

What happens

If I use an Encoder input device, my button indeed recieves the verious events and behaves as expected. If I turn to the Button encoder device, nothing happens.

My code

import lvgl as lv
import lvesp32
from ili9341 import ili9341

from input import DigitalInput
from micropython import schedule, const, alloc_emergency_exception_buf
from machine import Pin

alloc_emergency_exception_buf(150)

BUTTON_A_PIN = const(39)
BUTTON_B_PIN = const(38)
BUTTON_C_PIN = const(37)

# Physical button class
class BoutonPhy(object):
    def __init__(self, pin, id=0):
        self._press = False
        self._changed = False
        self._id = id
        self.setPin(pin)
    
    def _cb(self, pin, press):
        if self._press != press:
            print("State change [{}] {}->{}".format(pin, self._press, press))
            self._changed = True
            self._press = press
    
    @property
    def pressed(self):
        return self._press
    
    @property
    def changed(self):
        ch = self._changed
        self._changed = False
        return ch
    
    @property
    def id(self):
        return self._id
    
    def setPin(self, pin):
        # DigitalIput : the class that monitors/debounces pin. Uses an irq internally
        # see : https://github.com/tuupola/micropython-m5stack/blob/master/firmware/lib/input.py
        # Can be replaced with an explicit irq on the pin object.
        self._input = DigitalInput(pin, self._cb)

# Constructs the button reader
def buttonReadConstr(but):
    def button_read(drv, data):
        press = but.pressed
        if press:
            data.state = lv.INDEV_STATE.PR
        else:
            data.state = lv.INDEV_STATE.REL
        data.btn_id = but.id
        if but.changed:
            print("Button read [{}] ({}) state: {}, id:{}".format(but,drv,data.state, data.btn_id))
        return False
    
    return button_read

# Dummy event handler
def eventHandler(obj, event):
    print("Event handler [{}] event: {}".format(obj, event))
    

# M5Stack screen
disp = ili9341(miso=19, mosi=23, clk=18, cs=14, dc=27, rst=33, backlight=32,power=-1,power_on=-1, backlight_on=1,
        mhz=40, factor=4, hybrid=True, width=320, height=240,
        colormode=ili9341.COLOR_MODE_BGR, rot=ili9341.MADCTL_ML, invert=False, double_buffer=False)

# I want button A to click on the button in the center of the screen
# Stting up button A
boutonA = BoutonPhy(Pin(BUTTON_A_PIN), id=1)
button_readA = buttonReadConstr(boutonA)

# Setting up the screen
scr = lv.obj()
bout = lv.btn(scr)
bout.align(None, lv.ALIGN.CENTER,0,0)
lab = lv.label(bout)
lab.set_text("Test")
bout.set_event_cb(eventHandler)
lv.scr_load(scr)

# Setting up the input device
indev_drv = lv.indev_drv_t()
lv.indev_drv_init(indev_drv)
indev_drv.type = lv.INDEV_TYPE.BUTTON
indev_drv.read_cb = button_readA

# Screen center (160,120) right above the centered button bout.
point = lv.point_t()
point.x = 160
point.y = 120

win_drv = lv.indev_drv_register(indev_drv)
lv.indev_set_button_points(win_drv, [point])

With this code, I get messages showing the button is pressed/released, but the event handler is not called.

If I replace indev_drv.type = lv.INDEV_TYPE.BUTTON with indev_drv.type = lv.INDEV_TYPE.ENCODER and add

group = lv.group_create()
lv.group_add_obj(group, bout)
lv.indev_set_group(win_drv, group)

at the end of the script, the logical button is focused (and I get a message to that effect from the event handler) and clicks are transmitted: I get messages from the message handler…

Addon to the previous message

Here are the printouts of a sample run of the script with one (short) button press. I changed the prints in order for them to be more informative:

With a button indev:

import lvButtons
ILI9341 initialization completed
Enable backlight
Single buffer
State change [Pin(39)] False->True
Button read [<BoutonPhy object at 3f819df0>] (struct lv_indev_drv_t) state: 1, id:1
State change [Pin(39)] True->False
Button read [<BoutonPhy object at 3f819df0>] (struct lv_indev_drv_t) state: 0, id:1

Notice that there is no call to the event handler.

With an Ecoder indev:

import lvButtons
ILI9341 initialization completed
Enable backlight
Single buffer
Event handler [lvgl btn] event: 12
State change [Pin(39)] False->True
Button read [<BoutonPhy object at 3f819df0>] struct lv_indev_drv_t state: 1, id:1
Event handler [lvgl btn] event: 0
State change [Pin(39)] True->False
Button read [<BoutonPhy object at 3f819df0>] struct lv_indev_drv_t state: 0, id:1
Event handler [lvgl btn] event: 3
Event handler [lvgl btn] event: 6
Event handler [lvgl btn] event: 7

Here I have what I wanted: a button that is pressed, then released shortly after. Thus a presse (event 0) that is a short click (events 3, 6) followed by a release (event 7)…

What I don’t want (and cannot avoid with an encoder group) is the focused event and the focus ring around the logical button.

I found it !
The error was my assigning data.btn_id = but.id. I had to wade through the source code (see indev_button_proc in line 661 of lv_core/lv_indev.c) to discover that the btn_id parameter is the index where the input device driver finds the point to press.

So if I declare

lv.indev_set_button_points(win_drv, [pointA, pointB])

then if in the read callback, I set data.btn_id = 1, the point that will be pressed is the whose coordinates are in pointB.

The documentation is less than clear on this. A question remains: how do I press more than one screen point at once? I thought that the button_points array listed the points that would be clicked…

Related post: Hardware Button multiple points