peterhinch / micropython-micro-gui

A lightweight MicroPython GUI library for display drivers based on framebuf, allows input via pushbuttons. See also micropython-touch.
MIT License
270 stars 40 forks source link

Micro-gui practice #43

Closed tonymonty73 closed 10 months ago

tonymonty73 commented 10 months ago

Hi Peter, I'm pretty new to Python/micropython/micro-gui. Your libraries are top notch. I've practiced on a couple different screens. I'm using micro-gui with an ili9341. I'm practicing with a stepper and linear actuator. Everything is working except one place where I tried changing a label value to new text. It works elsewhere in the code but not in this one method. I hope it is OK that I just pasted the code below. The problem is in the button labeled with text "CYCLE". In it's callback, I try to issue self.been_home_lbl.value("CYCLING") and it doesn't change on the screen. The same label allows a change in another method with a successful change on the screen. I would also much appreciate your advice on my code anywhere else I may not be understanding things if it isn't too much trouble. Thank you, Tony M

from machine import Pin from stepper import Stepper import time

import hardware_setup from gui.core.ugui import Screen, ssd from gui.widgets import Button, CloseButton, Label from gui.core.writer import CWriter import gui.fonts.arial10 as arial10 import gui.fonts.arial35 as arial35 from gui.core.colors import * from gui.widgets import Label, Scale, ScaleLog, Button, CloseButton, Slider, HorizSlider, Knob, Checkbox

step_pin = 2 home_pin = 3 dir_pin = 5

s1 = Stepper(step_pin,dir_pin,steps_per_rev=200,speed_sps=1000) endswitch = machine.Pin(home_pin, machine.Pin.IN, machine.Pin.PULL_UP) wri = CWriter(ssd, arial10, WHITE, BLACK, verbose=False) big_btn_wri = CWriter(ssd, arial35, WHITE, BLACK, verbose=False)

def step_n(n,d): for i in range (n): s1.step(d)

def home_seq(): s1.speed(400) s1.free_run(-1) while endswitch.value() == 0: pass s1.stop() s1.overwrite_pos(0) s1.target(0) s1.speed(1000) s1.track_target()

class Motor1Screen(Screen):

def __init__(self):
    self.been_home=0
    self.last_pos=0

    super().__init__()
    Label(big_btn_wri, 2, 2, 'Motor 1 screen.')

    Button(big_btn_wri, 40, 10, height = 50, width = 50, callback = self.home_btn,
           fgcolor = WHITE, bgcolor = BLUE,
           text = "HOME", shape = RECTANGLE)

    self.cyc_btn = Button(big_btn_wri, 40, 140, height = 50, width = 50, callback = self.cycle_btn,
           fgcolor = WHITE, bgcolor = BLUE,
           text = "CYCLE", shape = RECTANGLE)

    self.pos_lbl = Label(big_btn_wri, 180, 20, 100, bdcolor=WHITE)
    self.been_home_lbl = Label(big_btn_wri, 180, 140, 175, bdcolor=WHITE)

    self.hslider = HorizSlider(wri, 120, 20, height = 50, width = 250,
                               callback = self.slider_cb, bdcolor=GREEN,
                               value=0.0)

    CloseButton(wri)

def home_btn(self, button):
    self.homing_seq()

def homing_seq(self):
    home_seq()
    self.been_home=1
    self.last_pos=0
    self.been_home_lbl.value("I'M HOME!")
    self.hslider.value(0.0)

def cycle_btn(self, button):
    self.homing_seq()
    self.been_home_lbl.value("CYCLING")
    step_n(2000,-1)
    time.sleep(2)
    step_n(1500,-1)
    time.sleep(2)
    for i in range(4):
        step_n(2000,-1)
        time.sleep(2)
        step_n(2000,1)
        time.sleep(2)
    for i in range(4):
        step_n(2000,1)
        time.sleep(2)
        step_n(2000,-1)
        time.sleep(2)
    self.homing_seq()

def slider_cb(self, s):
    v = s.value() * 5000
    if v<=5000 and self.been_home>0:
        steps = v-self.last_pos
        if steps>0:
            step_n(abs(steps),-1)

        if steps<0:
            step_n(abs(steps),1)

        self.last_pos = v
        self.pos_lbl.value('{:5.0f}'.format(v))
        if v > 4000 and v<5000:
            s.color(YELLOW)
            self.been_home_lbl.value("CAUTION!")
            self.been_home_lbl.value(fgcolor=YELLOW, bdcolor=YELLOW)
        elif v==5000:
            s.color(RED)
            self.been_home_lbl.value("LIMIT!")
            self.been_home_lbl.value(fgcolor=RED, bdcolor=RED)
        else:
            s.color(GREEN)
            self.been_home_lbl.value("OK!")
            self.been_home_lbl.value(fgcolor=GREEN, bdcolor=GREEN)

class Motor2Screen(Screen):

def __init__(self):
    super().__init__()
    Label(big_btn_wri, 2, 2, 'Motor 2 screen.')
    CloseButton(wri)

class HomeScreen(Screen):

def __init__(self):

    super().__init__()
    Label(big_btn_wri, 2, 2, 'Home Screen')
    Button(big_btn_wri, 40, 40, height = 50, width = 100, callback = self.mot1button,
           fgcolor = WHITE, bgcolor = BLUE,
           text = "Motor 1 Screen", shape = RECTANGLE)
    Button(big_btn_wri, 100, 40, height = 50, width = 100, callback = self.mot2button,
           fgcolor = WHITE, bgcolor = GREEN,
           text = "Motor 2 Screen", shape = RECTANGLE)        
    CloseButton(wri)

def mot1button(self, button):
    Screen.change(Motor1Screen)

def mot2button(self, button):
    Screen.change(Motor2Screen)

def test(): print('Stepper demo.') Screen.change(HomeScreen)

test()

peterhinch commented 10 months ago

I think the problem is as follows.

Internally the micro_gui library uses asyncio to achieve concurrency (see the tutorial). I wrote the library so it is usable by people without needing to write asynchronous code. However this has limits. When a callback runs, it needs to terminate before the library can refresh the screen with any changes. In particular issuing time.sleep() in a callback is undesirable.

The solution is to write slow running code as an asynchronous task. Something like:

async def cycle(self):
    self.homing_seq()
    self.been_home_lbl.value("CYCLING")
    step_n(2000,-1)
    await asyncio.sleep(2)  # Screen is refreshed during this delay
    step_n(1500,-1)
   await asyncio.sleep(2)
    for i in range(4):
        step_n(2000,-1)
        await asyncio.sleep(2)
        step_n(2000,1)
        await asyncio.sleep(2)
    for i in range(4):
        step_n(2000,1)
        await asyncio.sleep(2)
        step_n(2000,-1)
       await asyncio.sleep(2)
    self.homing_seq()

def cycle_btn(self, button):  # Callback launches a task
    asyncio.create_task(self.cycle())

There is a little more work involved in making this a practical solution - you need to ensure that pressing the button while the task is running does not create another instance - but I've kept it simple to explain the concept. Welcome to the world of asynchronous programming!

tonymonty73 commented 10 months ago

Thank you very much for taking the time to help me. That explains a lot. I was also getting some ticks in the stepper motion. I'll look at that tutorial for sure. Again, these libraries are amazing! Thanks, Tony

peterhinch commented 10 months ago

you need to ensure that pressing the button while the task is running does not create another instance

One way is to have the task disable the button at the start, then re-enable it on completion.