From a5de100d73c467f56289edb856d1dbe86bef4adf Mon Sep 17 00:00:00 2001 From: JensDiemer Date: Sun, 22 Mar 2026 16:49:00 +0100 Subject: [PATCH] New App: "ESPNowChat" simple chat app between devices Just send messages via https://docs.micropython.org/en/latest/library/espnow.html to all devices via broadcast address and display all messages that are send from other devices... Some limitations: All devices must send on the same WiFi channel. --- .../META-INF/MANIFEST.JSON | 24 ++++ .../assets/espnow_chat.py | 136 ++++++++++++++++++ .../res/mipmap-mdpi/icon_64x64.png | Bin 0 -> 3096 bytes 3 files changed, 160 insertions(+) create mode 100644 internal_filesystem/apps/com.micropythonos.espnow_chat/META-INF/MANIFEST.JSON create mode 100644 internal_filesystem/apps/com.micropythonos.espnow_chat/assets/espnow_chat.py create mode 100644 internal_filesystem/apps/com.micropythonos.espnow_chat/res/mipmap-mdpi/icon_64x64.png diff --git a/internal_filesystem/apps/com.micropythonos.espnow_chat/META-INF/MANIFEST.JSON b/internal_filesystem/apps/com.micropythonos.espnow_chat/META-INF/MANIFEST.JSON new file mode 100644 index 00000000..2f8e59c2 --- /dev/null +++ b/internal_filesystem/apps/com.micropythonos.espnow_chat/META-INF/MANIFEST.JSON @@ -0,0 +1,24 @@ +{ +"name": "ESPNowChat", +"publisher": "MicroPythonOS", +"short_description": "ESPNow Chat", +"long_description": "Simple chat app using EspNow protocol for communication between devices.", +"icon_url": "https://apps.micropythonos.com/apps/com.micropythonos.espnow_chat/icons/com.micropythonos.espnow_chat_0.0.1_64x64.png", +"download_url": "https://apps.micropythonos.com/apps/com.micropythonos.espnow_chat/mpks/com.micropythonos.espnow_chat_0.0.1.mpk", +"fullname": "com.micropythonos.espnow_chat", +"version": "0.1.0", +"category": "development", +"activities": [ + { + "entrypoint": "assets/espnow_chat.py", + "classname": "EspNowChat", + "intent_filters": [ + { + "action": "main", + "category": "launcher" + } + ] + } + ] +} + diff --git a/internal_filesystem/apps/com.micropythonos.espnow_chat/assets/espnow_chat.py b/internal_filesystem/apps/com.micropythonos.espnow_chat/assets/espnow_chat.py new file mode 100644 index 00000000..87b74c2c --- /dev/null +++ b/internal_filesystem/apps/com.micropythonos.espnow_chat/assets/espnow_chat.py @@ -0,0 +1,136 @@ +""" +https://docs.micropython.org/en/latest/library/espnow.html +""" + +from collections import deque + +import lvgl as lv +import machine +from micropython import const +from mpos import Activity, MposKeyboard, TaskManager +from mpos.time import localtime + +try: + import aioespnow +except ImportError: + aioespnow = None + +try: + import network +except ImportError: + network = None + +BROADCAST_MAC = const(b"\xbb\xbb\xbb\xbb\xbb\xbb") + + +def pformat_mac(mac): + if mac: + return ":".join(f"{b:02x}" for b in mac) + else: + return "" + + +class EspNowChat(Activity): + def onCreate(self): + main_content = lv.obj() + main_content.set_flex_flow(lv.FLEX_FLOW.COLUMN) + main_content.set_style_pad_gap(10, 0) + + self.input_textarea = lv.textarea(main_content) + self.input_textarea.set_placeholder_text("Message input...") + self.input_textarea.set_one_line(True) + self.input_textarea.set_style_text_font(lv.font_montserrat_16, lv.PART.MAIN) + self.input_textarea.set_width(lv.pct(100)) + self.input_textarea.add_event_cb(self.show_keyboard, lv.EVENT.CLICKED, None) + + self.keyboard = MposKeyboard(main_content) + self.keyboard.set_textarea(self.input_textarea) + self.keyboard.add_event_cb(self.keyboard_cb, lv.EVENT.READY, None) + self.keyboard.add_flag(lv.obj.FLAG.HIDDEN) + + self.messages = lv.label(main_content) + self.messages.set_style_text_font(lv.font_montserrat_14, 0) + + # Buffer to store and display the latest 20 messages: + self.messages_buffer = deque((), 20) + + self.setContentView(main_content) + + if aioespnow and network: + print("Initialize WLAN interface...") + sta = network.WLAN(network.WLAN.IF_STA) + sta.active(True) + + self.own_id = pformat_mac(machine.unique_id()) + + self.info("Initialize ESPNow...") + self.espnow = aioespnow.AIOESPNow() + self.espnow.active(True) + self.espnow.add_peer(BROADCAST_MAC) + + if sta.isconnected(): + self.info(f"Connected to WiFi: {sta.config('essid')}") + self.info(f"Use WiFi Channel: {sta.config('channel')}") + else: + self.own_id = "" + self.info("ESPNow not available on this platform") + + def info(self, text): + now = localtime() + hour, minute, second = now[3], now[4], now[5] + message = f"{hour:02}:{minute:02}:{second:02} {text}" + print(message) + self.messages_buffer.appendleft(message) + self.messages.set_text("\n".join(self.messages_buffer)) + + def keyboard_cb(self, event): + message = self.input_textarea.get_text() + if not message: + print("Ignore empty input") + else: + self.input_textarea.set_text("") + print(f"Create task to send {message=}...") + TaskManager.create_task(self.send_messages(message)) + + def show_keyboard(self, event): + print("Show keyboard") + self.keyboard.remove_flag(lv.obj.FLAG.HIDDEN) + + async def send_messages(self, message): + self.info(f"Sending: {message} ({self.own_id})") + try: + await self.espnow.asend(BROADCAST_MAC, message.encode()) + except OSError as err: + print(f"Error sending message: {err}") + else: + print(f"{message=} sent") + + async def receive_messages(self): + await self.send_messages(f"{self.own_id} joins ESPNow chat.") + async for mac, msg in self.espnow: + if not msg: + print("Ignore empty message from", pformat_mac(mac)) + continue + try: + msg = msg.decode() + except UnicodeError as err: + msg = f"" + self.info(f"{msg} ({pformat_mac(mac)})") + raise RuntimeError("ESPNow receive loop exited, which shouldn't happen") + + def onResume(self, screen): + super().onResume(screen) + if aioespnow and network: + TaskManager.create_task(self.receive_messages()) + + def onPause(self, screen): + if aioespnow and network: + self.espnow.send( + BROADCAST_MAC, f"{self.own_id} leaves ESPNow chat.".encode() + ) + + print("Stop ESPNow...") + self.espnow.active(False) + print("ESPNow deactivated") + + super().onPause(screen) diff --git a/internal_filesystem/apps/com.micropythonos.espnow_chat/res/mipmap-mdpi/icon_64x64.png b/internal_filesystem/apps/com.micropythonos.espnow_chat/res/mipmap-mdpi/icon_64x64.png new file mode 100644 index 0000000000000000000000000000000000000000..e7412cda8e1ebdb58c10b1e9acdb28a3990426cf GIT binary patch literal 3096 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