├── LICENSE ├── README.md ├── device.png ├── example-endpoint-data.json └── main.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Chaiyapat Tantiworachot 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # M5Paper Micropython Data Text Display 2 | 3 | ![This thingy](device.png) 4 | 5 | ## Introduction 6 | 7 | I have been wanted an ambient display in my room for quite a while, but I don't want it to be just another LCD screen that I have to plug in at all times. Then I found this M5Paper thingy one day when I browsed a microcontroller store. So I bought it, played around with it, and this is like an experimental, yet usable project. 8 | 9 | I'm not going to update this code / provide support because I'm moving to code using C so that I have more control over stuff. 10 | 11 | ## Design Rationale 12 | 13 | I built this code so that I don't have to edit the on-device code much. The data collection and processing are done on the cloud. It just displayed whatever it received. Like in the image, it shows whatever it receives from the endpoint. I display information that I feel like I can read while I brew my coffee, or just take a walk to the kitchen, so the information here isn't really important or time-sensitive. 14 | 15 | ## Usage 16 | 17 | 1. Flash UiFlow into your M5Paper [follow this instruction](https://docs.m5stack.com/en/quick_start/m5core/uiflow) 18 | 2. Edit some variables in main.py, then upload the file to the device (either via UiFlow or USB mode + Micropython) 19 | 3. Implement an endpoint for the device to get data from (See next section) 20 | 21 | ## Endpoint Implementation 22 | 23 | The endpoint is resting on my cloud server. It is one of the microservices on my server so that everything isn't tightly coupled. 24 | 25 | First, I have a data aggregation service that fetches data from various sources, at different intervals based on the API limitation. Then I format those data to my liking. 26 | 27 | Second, I implement the endpoint for this device, it gets the aggregated data from my database, format it into an appropriate object (see `example-endpoint-data.json`), and return the request when the device fetches. 28 | 29 | Note that, you can see I send some device metrics to the endpoint too (Line 31), I logged these metrics on the cloud, but you can discard it. 30 | 31 | ### Endpoint Data Note 32 | 33 | - `shutdownDelay` is the delay from the device finished fetching to it being shutdown again (seconds) 34 | - `wakeupInterval` is the delay between fetching, during this period, the device sleep to conserve battery (seconds) 35 | - `textN` and `firstLine` have around 14 characters limit (except menu0, around 28) 36 | - `textLargeN` has around 6 characters limit (except menu0, around 13) 37 | - `actionName` has around 17 characters limit (except menu0, around 37) 38 | - `textLarge1` replaces `text1` and `text2`, similarly with `menuN.textLarge2` replaces `text3` and `text4` 39 | - `actionName` is the last line, I originally intended to allow interaction on-device, but I felt like this ambient display shouldn't have interaction on itself, the name remains 40 | 41 | ## Further Reading & Known M5Paper and M5Burner Issue 42 | 43 | - [https://www.gwendesign.ch/kb/m5stack/m5paper/#light-sleep-deep-sleep-and-shutdown-current](https://www.gwendesign.ch/kb/m5stack/m5paper/#light-sleep-deep-sleep-and-shutdown-current) 44 | - Install this if M5Paper doesn't play well with Mac - M5Paper Serial Driver for MacOS Big Sur [https://github.com/Xinyuan-LilyGO/LilyGo-T-Call-SIM800/issues/139#issuecomment-904390716](https://github.com/Xinyuan-LilyGO/LilyGo-T-Call-SIM800/issues/139#issuecomment-904390716) 45 | - M5Burner has several problem when setting, be sure to update it to the latest version in the app. In my case the site show 2.2.8 as latest but it has problem setting mode, I had to update in-app to 2.3.0. Do this via M5Burner setting (top-right cog icon, then update) 46 | -------------------------------------------------------------------------------- /device.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pixelart7/m5paper-micropython-data-text-display/6e25826564bb4ea93ba54660e5704ddf43f98fc0/device.png -------------------------------------------------------------------------------- /example-endpoint-data.json: -------------------------------------------------------------------------------- 1 | { 2 | "tz": "Asia/Tokyo", 3 | "time": 1643174972454, 4 | "timeDisplay": "14:29", 5 | "hour": 14, 6 | "minute": 29, 7 | "menu0": { 8 | "firstLine": "Upcoming: (9c)", 9 | "text1": "", 10 | "text2": "", 11 | "textLarge1": "P. cloudy", 12 | "text3": "Now: Shower nearby 6c (~3c)", 13 | "text4": "01/27: P. cloudy (5/11c)", 14 | "textLarge2": "", 15 | "text5": "", 16 | "actionName": "Message of the day" 17 | }, 18 | "menu1": { 19 | "firstLine": "January", 20 | "text1": "", 21 | "text2": "", 22 | "textLarge1": "Wed", 23 | "text3": "", 24 | "text4": "", 25 | "textLarge2": "26", 26 | "text5": "", 27 | "actionName": "2022" 28 | }, 29 | "menu2": { 30 | "firstLine": "JP) Slowdowns i", 31 | "text1": "n U.S. and Chin", 32 | "text2": "a will hold bac", 33 | "textLarge1": "", 34 | "text3": "TH) Ex-Tour cha", 35 | "text4": "mpion Bernal 'c", 36 | "textLarge2": "", 37 | "text5": "onscious' after", 38 | "actionName": "JP/TH NEWS" 39 | }, 40 | "menu3": { 41 | "firstLine": "dytiscidae", 42 | "text1": "(noun)", 43 | "text2": "water beetles", 44 | "textLarge1": "", 45 | "text3": "", 46 | "text4": "", 47 | "textLarge2": "", 48 | "text5": "", 49 | "actionName": "RANDOM WORD" 50 | }, 51 | "menu4": { 52 | "firstLine": "Toto was paid m", 53 | "text1": "ore than some o", 54 | "text2": "f the humans in", 55 | "textLarge1": "", 56 | "text3": "'The Wizard of", 57 | "text4": "Oz'", 58 | "textLarge2": "", 59 | "text5": "", 60 | "actionName": "RANDOM FACT" 61 | }, 62 | "menu5": { 63 | "firstLine": "", 64 | "text1": "", 65 | "text2": "", 66 | "textLarge1": "", 67 | "text3": "", 68 | "text4": "", 69 | "text5": "", 70 | "textLarge2": "", 71 | "actionName": "" 72 | }, 73 | "shutdownDelay": 5, 74 | "wakeupInterval": 1800 75 | } -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from m5stack import * 2 | from m5ui import * 3 | from uiflow import * 4 | import wifiCfg 5 | from easyIO import * 6 | import urequests 7 | import json 8 | 9 | import time 10 | 11 | data = {} 12 | 13 | ssid = 'ssid' # SSID 14 | wifipassword = 'password' # Password 15 | endpoint = 'https://example.com/data-for-m5paper' # HTTP Endpoint (No trailing slash) 16 | authorizationHeader = 'Bearer FOOBAR' # Auth Header 17 | screenTitle = 'Home' # Title to display on the device 18 | 19 | wifiCfg.doConnect(ssid, wifipassword) 20 | 21 | def aggregateData(): 22 | global data 23 | data['batteryPercentage'] = str(map_value((bat.voltage() / 1000), 3.2, 4.3, 0, 100)) + '%' 24 | data['isWifiConnected'] = wifiCfg.wlan_sta.isconnected() 25 | data['temperature'] = '{0:.2f}'.format(sht30.temperature) 26 | data['humidity'] = str(int(sht30.humidity)) 27 | data['voltage'] = str(bat.voltage() / 1000) 28 | try: 29 | req = urequests.request( 30 | method='GET', 31 | url=(endpoint + '?temperature=' + data['temperature'] + '&humidity=' + data['humidity'] + '&battery=' + str(map_value((bat.voltage() / 1000), 3.2, 4.3, 0, 100)) + '&voltage=' + data['voltage'] ), 32 | headers={ 33 | 'Authorization': authorizationHeader 34 | }) 35 | data['json'] = json.loads((req.text)) 36 | except: 37 | pass 38 | 39 | def showLoading(): 40 | width = 88 41 | M5Rect(540-width, 0, 100, 40, 2, 0) 42 | M5Circle(540-width+18, 20, 5, 0, 15) 43 | M5TextBox(540-width+32, 3, '...', lcd.FONT_DejaVu24, 15, rotate=0) 44 | lcd.partial_show(540-width, 0, width, 40) 45 | 46 | def showActive(isActive, partialRender): 47 | width = 88 48 | if bool(isActive): 49 | M5Rect(540-width, 0, 100, 40, 2, 0) 50 | M5Circle(540-width+18, 20, 5, 15, 15) 51 | M5TextBox(540-width+32, 10, str(data['json']['shutdownDelay']) + 's', lcd.FONT_DejaVu24, 15, rotate=0) 52 | else: 53 | M5Rect(540-width, 0, width, 40, 15, 15) 54 | if partialRender: 55 | lcd.partial_show(540-width, 0, width, 40) 56 | 57 | def renderMenu(keyName, constX, constY, height, width, margin, padding, background, border): 58 | M5Rect(constX, constY, width, height, background, border) 59 | M5TextBox(constX+padding, constY+padding, data['json'][keyName]['firstLine'], lcd.FONT_DejaVu24, 0) # 14 / 28 60 | if bool(data['json'][keyName]['textLarge1']): 61 | M5TextBox(constX+padding, constY+padding+28, data['json'][keyName]['textLarge1'], lcd.FONT_DejaVu56, 0) # 6 / 13 62 | else: 63 | M5TextBox(constX+padding, constY+padding+28, data['json'][keyName]['text1'], lcd.FONT_DejaVu24, 0) # 14 / 28 64 | M5TextBox(constX+padding, constY+padding+(28*2), data['json'][keyName]['text2'], lcd.FONT_DejaVu24, 0) # 14 / 28 65 | if bool(data['json'][keyName]['textLarge2']): 66 | M5TextBox(constX+padding, constY+padding+(28*3), data['json'][keyName]['textLarge2'], lcd.FONT_DejaVu56, 0) # 6 67 | else: 68 | M5TextBox(constX+padding, constY+padding+(28*3), data['json'][keyName]['text3'], lcd.FONT_DejaVu24, 0) # 14 / 28 69 | M5TextBox(constX+padding, constY+padding+(28*4), data['json'][keyName]['text4'], lcd.FONT_DejaVu24, 0) # 14 / 28 70 | M5TextBox(constX+padding, constY+padding+(28*5), data['json'][keyName]['text5'], lcd.FONT_DejaVu24, 0) # 14 / 28 71 | M5TextBox(constX+padding, constY+height-padding-16, data['json'][keyName]['actionName'], lcd.FONT_DejaVu18, 6) # 17 / 37 72 | 73 | def render(): 74 | lcd.fillScreen(15) 75 | lcd.font(lcd.FONT_DejaVu24) 76 | lcd.setTextColor(0, 15) 77 | M5Rect(0, 0, 540, 40, 15, 15) 78 | M5TextBox(12, 10, screenTitle, lcd.FONT_DejaVu24, 0, rotate=0) 79 | M5Line(M5Line.PLINE, 0, 40, 540, 40, 0) 80 | showActive(True, False) 81 | 82 | border = 0 83 | background = 15 84 | 85 | margin = 12 86 | padding = 12 87 | 88 | # menu0 89 | constX = margin 90 | constY = 40 + margin 91 | width = 540 - (constX * 2) 92 | height = 212 93 | renderMenu('menu0', constX, constY, height, width, margin, padding, background, border) 94 | 95 | # menu1 96 | constX = margin 97 | constY = constY + height + margin 98 | width = int(width / 2) - int(constX / 2) 99 | renderMenu('menu1', constX, constY, height, width, margin, padding, background, border) 100 | 101 | # menu2 102 | constX = margin + width + margin 103 | renderMenu('menu2', constX, constY, height, width, margin, padding, background, border) 104 | 105 | # menu3 106 | constX = margin 107 | constY = constY + height + margin 108 | renderMenu('menu3', constX, constY, height, width, margin, padding, background, border) 109 | 110 | constX = margin + width + margin 111 | renderMenu('menu4', constX, constY, height, width, margin, padding, background, border) 112 | 113 | constX = margin 114 | constY = constY + height + margin 115 | renderMenu('menu5', constX, constY, height, width, margin, padding, background, border) 116 | 117 | # System us 18 font size, the rest should use 24 118 | constX = margin + width + margin 119 | M5Rect(constX, constY, width, height, background, border) 120 | M5TextBox(constX+padding, constY+padding, 'Time: ' + str(data['json']['timeDisplay']) + ' ~' + str(int(data['json']['wakeupInterval'] / 60)) + 'm', lcd.FONT_DejaVu18, 0) 121 | M5TextBox(constX+padding, constY+padding+22, 'WiFi: ' + ssid if data['isWifiConnected'] else '? ' + ssid, lcd.FONT_DejaVu18, 0) 122 | M5TextBox(constX+padding, constY+padding+(22*2), 'Battery: ' + data['batteryPercentage'], lcd.FONT_DejaVu18, 0) 123 | M5TextBox(constX+padding, constY+padding+(22*3), 'Voltage: ' + data['voltage'], lcd.FONT_DejaVu18, 0) 124 | M5TextBox(constX+padding, constY+padding+(22*4), 'Temperature: ' + data['temperature'], lcd.FONT_DejaVu18, 0) 125 | M5TextBox(constX+padding, constY+padding+(22*5), 'Humidity: ' + data['humidity'] + '%', lcd.FONT_DejaVu18, 0) 126 | 127 | M5TextBox(constX+padding, constY+height-padding-16, 'SYSTEM', lcd.FONT_DejaVu18, 6) 128 | 129 | showLoading() 130 | 131 | aggregateData() 132 | render() 133 | lcd.show() 134 | 135 | wait(int(data['json']['shutdownDelay'])) # delay sleep 136 | showActive(False, True) 137 | 138 | power.restart_after_seconds(int(data['json']['wakeupInterval'])) # uncomment this when deploy --------------------------------------------------------------------------------