peterhinch / micropython-touch

A GUI for touch panel displays.
MIT License
17 stars 2 forks source link

mp-touch with mqtt-as ? #4

Closed beetlegigg closed 3 months ago

beetlegigg commented 3 months ago

This is not an issue as such, but seeking a knowledgeable comment on the best way to proceed with utilising the mp-touch lib together with the mqtt-as lib.

Delving too deep into the code is rather hard for my level of expertise, but I think the mp-touch starts an async loop when a user created sub class of the Screen class is instantiated.

I suppose that in this Screen subclass, apart from creating all the screen widgets, I could also include all the necessary task creations to get the mqtt-as up and running in the async loop as well.

Before going too far into having a go at this I thought I would check with you to see if I'm spouting nonsense or if you have any tips how (or if) one should go about using a touch screen that receives and publishes data via mqtt messages.

My scenario is the touch screen is linked to a temperature sensor (directly or via an mqtt message) that displays this temperature reading and also displays a current thermostat setting.

This Thermostat setting can be changed via up/down buttons. The new thermostat reading is published via mqtt to the boiler control / boiler status program (on another computer), and receives mqtt messages back from the same to display the boiler status on the screen.

This is all currently working on a raspberry pi with a large screen and I'm looking to package this up with a smaller display unit. Currently I also have a small display unit (ili9341) which runs nano-gui and mqtt-as as a display only screen.

I could quite easily use this ili9341 screen with manual buttons and its the way I will go if my thoughts on getting the touch screen ability of the ili9341 working with mqtt-as prove to be too complicated (for me :-) ). So before I embark on checking this out I thought I would get your quick thoughts on the feasibility of getting the mp-touch and mqtt-as working together on a rpi-pico board.

Thanks.

peterhinch commented 3 months ago

I haven't tried this but it should be possible. You are correct in that micropython-touch starts asyncio which runs until the GUI terminates - or forever. If you look at the mqtt_as demos, such as range.py, the demo itself starts asyncio - line 78. The whole try...finally block could be replaced by a line

asyncio.create_task(main(client))

This line of code would need to be run by the GUI. It could be run in an after_open method of your base Screen which would start mqtt_as automatically, or you could run it in (say) a Button callback.

beetlegigg commented 3 months ago

Following your comment I had a quick noddy test with a couple of updating labels on the screen and the range.py being called from an after_open method. Wonderful working like a charm.

Just to note that importing range into my screen program and issuing asyncio.create_task(main(client)) from the after_open did not work (NameError: name 'client' isn't defined) but putting that code into the range.py, replacing the try...finally, and then importing range in the after_open method did the trick.

Too late in the day for me to think this through but you've put me on the right path and I will delve into a more sophisticated program test when I've got a pcb for the pico and ili9341 done and then built my own mp with the touch and mqtt-as as frozen modules.

I'll close this now and many thanks for the comment, (and for touch and mqtt-as of course :-) )

peterhinch commented 3 months ago

This is something I've had in the back of my mind to try, so I've written some code (I may post this as a demo). Salient points:

import hardware_setup  # Create a display instance
from gui.core.tgui import Screen, ssd

from gui.widgets import Label, Button, CloseButton, LED
from gui.core.writer import CWriter

# Font for CWriter
import gui.fonts.arial10 as arial10
from gui.core.colors import *

# MQTT stuff
from mqtt_as import MQTTClient
from mqtt_local import wifi_led, blue_led, config
import asyncio

TOPIC = "shed"  # For demo publication and last will use same topic

outages = 0

# Define configuration
config["will"] = (TOPIC, "Goodbye cruel world!", False, 0)
config["keepalive"] = 120
config["queue_len"] = 1  # Use event interface with default queue

# Set up client. Enable optional debug statements.
MQTTClient.DEBUG = True

async def pulse():  # This demo pulses blue LED each time a subscribed msg arrives.
    blue_led(True)
    await asyncio.sleep(1)
    blue_led(False)

class BaseScreen(Screen):
    mqtt_task = None  # Ensure task is a singleton

    def __init__(self):
        def my_callback(button, arg):
            print("Button pressed", arg)

        super().__init__()
        wri = CWriter(ssd, arial10, GREEN, BLACK)  # Verbose
        col = 2
        row = 2
        Label(wri, row, col, "Topic")
        self.lbltopic = Label(wri, row, col + 50, 200, bdcolor=GREEN)
        self.lbltopic.value("Waiting...")
        row = 20
        Label(wri, row, col, "Message")
        self.lblmsg = Label(wri, row, col + 50, 200, bdcolor=GREEN)
        self.lblmsg.value("Waiting...")
        row = 60
        Label(wri, row + 15, col, "Network")
        self.led = LED(wri, row, col + 50, color=RED, bdcolor=WHITE)
        self.led.value(True)

        row = 150
        self.btnyes = Button(
            wri, row, col, text="Yes", litcolor=WHITE, callback=self.pub, args=("Yes",)
        )
        self.btnyes.greyed_out(True)
        col += 60
        self.btnno = Button(
            wri, row, col, litcolor=WHITE, text="No", callback=self.pub, args=("No",)
        )
        self.btnno.greyed_out(True)
        CloseButton(wri, 30)  # Quit the application
        self.client = MQTTClient(config)
        if BaseScreen.mqtt_task is None:
            BaseScreen.mqtt_task = asyncio.create_task(self.main())

    async def main(self):
        try:
            await self.client.connect()
        except OSError:
            print("Connection failed.")
            Screen.back()  # Abort: probable MQTT or WiFi config error
        for task in (self.up, self.down, self.messages):
            asyncio.create_task(task())
        n = 0
        while True:
            await asyncio.sleep(60)
            print("publish", n)
            # If WiFi is down the following will pause for the duration.
            await self.client.publish(
                TOPIC,
                "{} repubs: {} outages: {}".format(n, self.client.REPUB_COUNT, outages),
                qos=1,
            )
            n += 1

    async def messages(self):
        async for topic, msg, retained in self.client.queue:
            print(f'Topic: "{topic.decode()}" Message: "{msg.decode()}" Retained: {retained}')
            self.lbltopic.value(topic.decode())
            self.lblmsg.value(msg.decode())
            asyncio.create_task(pulse())

    async def down(self):
        global outages
        while True:
            await self.client.down.wait()  # Pause until connectivity changes
            self.client.down.clear()
            self.led.color(RED)
            self.btnyes.greyed_out(True)
            self.btnno.greyed_out(True)
            outages += 1
            print("WiFi or broker is down.")

    async def up(self):
        while True:
            await self.client.up.wait()
            self.client.up.clear()
            self.led.color(GREEN)
            self.btnyes.greyed_out(False)
            self.btnno.greyed_out(False)
            print("We are connected to broker.")
            await self.client.subscribe("foo_topic", 1)

    def pub(self, button, msg):  # Button callback
        asyncio.create_task(self.client.publish(TOPIC, msg, qos=1))

def test():
    print("MQTT demo: button presses publish.")
    Screen.change(BaseScreen)  # A class is passed here, not an instance.

test()

Publications are on a button press, with a status pub every minute. Subscription messages are shown on screen. A screen LED widget shows WiFi status.

beetlegigg commented 3 months ago

Thats a very nice demo of the capabilities of the touch and mqtt.
When you say you pre-complied the mqtt-as.py I take it this means that the mqtt-as.py should be run on its own first, before then running the demo program and it does not mean that an mqtt-as.mpy is required. Anyway I had run the range.py to ensure the mqtt-as.py had all the supporting stuff set up correctly, and the demo then ran without a hitch.
With that very nice framework of how to use both modules together I now keen to get stuck in to my project, the grass cutting will have to wait :-)

peterhinch commented 3 months ago

No, I created mqtt_as.mpy. Loading the .py first may work by forcing an early compilation, but I am wary of loading anything until the Framebuf is instantiated because the buffer requires so much contiguous RAM.

There are some obvious simplifications to the above code - e.g. my_callback is completely redundant.

I added an extra LED and a few explanatory Label items but soon hit a limit, with an allocation fail occurring after it received a few messages. It's clearly running close to the limit, but given that you're prepared to freeze code I don't think you'll have any problems.

Using litcolor with Button instances is handy as it provides confirmation of a touch.

Good luck with your project!