UI Lite for Python 可视化开发实战

简介: HaaS Python的目标是帮助中小开发者聚焦业务,实现设备安全上云,加速设备创新迭代,真正做到“Python也可以轻松开发智能设备”。HaaS Python 轻应用在2022年终于开始支持ESP32的开发板了,解决了开发板向生成转换时的顾虑。Python轻应用通过LVGL 8.1整合 UI得到了提升。UI Lite for Python以LVGL 8.1为基础进行打造,阿里云的官方文档提供了一个官方文档的链接。也提供了各种UI组件的展示,用代码进行 UI 设计让我们的同事非常头大。于是我找到了可视化的UI开发工具SquareLine Studio。

安装HaaS开发环境

安装VSCode

如果网络条件允许可以直接在VSCode官网下载。

VSCode启动界面.jpg

安装HaaS Studio

HaaS Studio setup.jpg

完成后会提示激活Aos插件,确认即可。

启用HaaS进入HaaS Studio启动界面

开发环境安装完成.jpg

下载ESP32固件并烧录

采用M5 Core2作为开发板,内置屏幕方便设置,该版本是单独的固件,区别是ESP32-8M。

image.png

M5 CORE2是ESP32平台,并非C3或S3。需要注意区分。

固件烧录工具.jpg

烧录完成后,准备工作结束。

关于HaaS UI

在搜索HaaS Studio插件的时候,HaaS UI无法运行在ESP32单片机上,对于硬件需求很高。是基于Vue.js开发的一套物联网UI。

ESP32不要安装.jpg

本文不涉及相关内容,为防止迷惑,特此说明。


代码写UI Lite for Python

创建ESP32轻应用工程

image.png

示例程序中有3个包含了UI Lite for Python,分别是倒计时计数器、音乐播放器和温湿度面板。

以倒计时计数器为例,看一下代码。

载入LVGL

importlvglaslvisStarted=FalseisAnimationComplete=Falsearc= [None, None, None, None]
anim= [None, None, None, None]
timeCount= [1, 3, 5, 10]
currentSelect=0minuteLabel=NonesecondLabel=NonemillionLabel=Noneanim_timeline=NonestartLabel=NonecurrentValue=0lvglInitialized=FalsedefsetLabelValue(value):
globalcurrentValueglobalminuteLabelglobalsecondLabelglobalmillionLabelglobalstartLabelcurrentValue=valuecurrentMillionSecond=value*20minute=currentMillionSecond/1000/60minuteLabel.set_text('%02d'%minute)
second=currentMillionSecond/1000%60secondLabel.set_text('%02d'%second)
million=value%60millionLabel.set_text('%02d'%million)
defset_time_value(obj, v):
setLabelValue(v)
obj.set_value(v)
defreset_button_event_handler(e):
globalisStartedglobalisAnimationCompleteglobalcurrentValueglobaltimeCountglobalarcglobalanimglobalanim_timelineglobalstartLabelif (isStarted):
returnisAnimationComplete=FalsecurrentValue=timeCount[currentSelect] *60*50arc[currentSelect].set_value(currentValue)
anim[currentSelect] =lv.anim_t()
anim[currentSelect].init()
anim[currentSelect].set_var(arc[currentSelect])
anim[currentSelect].set_time(currentValue*20)
anim[currentSelect].set_values(currentValue, 0)
anim[currentSelect].set_custom_exec_cb(lambdaa1, val: set_time_value(arc[currentSelect], val))
anim_timeline=lv.anim_timeline_create()
lv.anim_timeline_add(anim_timeline, 0, anim[currentSelect])
startLabel.set_text("START")
setLabelValue(currentValue)
defarc_event_handler(e, index):
globalisStartedglobalcurrentSelectglobalarcprint("index: "+str(index) +" currentSelect: "+str(currentSelect))
print("isStarted: "+str(isStarted))
if (isStartedorcurrentSelect==index):
returnarc[currentSelect].set_value(0)
arc[currentSelect].set_style_arc_width(2, lv.PART.INDICATOR)
arc[currentSelect].set_style_arc_width(2, lv.PART.MAIN)
currentSelect=indexarc[currentSelect].set_style_arc_width(8, lv.PART.INDICATOR)
arc[currentSelect].set_style_arc_width(8, lv.PART.MAIN)
reset_button_event_handler(e)
defstart_button_event_handler(e):
globalisStartedglobalisAnimationCompleteglobalanim_timelineglobalstartLabelglobalanimglobalcurrentSelectglobalcurrentValueif (isAnimationComplete):
returnif (isStarted):
isStarted=Falselv.anim_timeline_stop(anim_timeline)
lv.anim_timeline_del(anim_timeline)
anim_timeline=NonestartLabel.set_text("RESUME")
anim[currentSelect] =lv.anim_t()
anim[currentSelect].init()
anim[currentSelect].set_var(arc[currentSelect])
anim[currentSelect].set_time(currentValue*20)
anim[currentSelect].set_values(currentValue, 0)
anim[currentSelect].set_custom_exec_cb(lambdaa1, val: set_time_value(arc[currentSelect],val))
anim_timeline=lv.anim_timeline_create()
lv.anim_timeline_add(anim_timeline, 0, anim[currentSelect])
else:
isStarted=Truelv.anim_timeline_start(anim_timeline)
startLabel.set_text("PAUSE")
classCountDown:
defcreatePage(self, value1=1, value2=3, value3=5, value4=10):
globalisStartedglobalisAnimationCompleteglobalarcglobalanimglobaltimeCountglobalcurrentSelectglobalminuteLabelglobalsecondLabelglobalmillionLabelglobalanim_timelineglobalstartLabelglobalcurrentValuegloballvglInitializediflvglInitialized==False:
importdisplay_driverlvglInitialized=Trueprint("Enter count down")
timeCount= [value1, value2, value3, value4]
# init scrscr=lv.obj()
win=lv.obj(scr)
win.set_size(scr.get_width(), scr.get_height())
win.set_style_border_opa(0, 0)
win.set_style_radius(0, 0)
win.set_style_bg_color(lv.color_black(), 0)
win.clear_flag(lv.obj.FLAG.SCROLLABLE)
isStarted=FalsecurrentSelect=0# count downfunc_col_dsc= [40, 5, 30, 5, 20, lv.GRID_TEMPLATE.LAST]
func_row_dsc= [30, lv.GRID_TEMPLATE.LAST]
timeContainer=lv.obj(win)
timeContainer.set_style_bg_opa(0, 0)
timeContainer.set_style_border_opa(0, 0)
timeContainer.set_layout(lv.LAYOUT_GRID.value)
timeContainer.set_style_grid_column_dsc_array(func_col_dsc, 0)
timeContainer.set_style_grid_row_dsc_array(func_row_dsc, 0)
timeContainer.set_grid_align(lv.GRID_ALIGN.SPACE_BETWEEN, lv.GRID_ALIGN.SPACE_BETWEEN)
timeContainer.set_style_pad_all(0, 0)
timeContainer.set_size(240, 70)
timeContainer.center()
minuteLabel=lv.label(timeContainer)
minuteLabel.set_style_text_font(lv.font_montserrat_48, 0)
minuteLabel.set_style_text_color(lv.color_white(), 0)
minuteLabel.set_grid_cell(lv.GRID_ALIGN.START, 0, 1, lv.GRID_ALIGN.CENTER, 0, 1)
signLabel=lv.label(timeContainer)
signLabel.set_style_text_font(lv.font_montserrat_48, 0)
signLabel.set_style_text_color(lv.color_white(), 0)
signLabel.set_text(":")
signLabel.set_grid_cell(lv.GRID_ALIGN.CENTER, 1, 1, lv.GRID_ALIGN.CENTER, 0, 1)
secondLabel=lv.label(timeContainer)
secondLabel.set_style_text_font(lv.font_montserrat_48, 0)
secondLabel.set_style_text_color(lv.color_white(), 0)
secondLabel.set_grid_cell(lv.GRID_ALIGN.CENTER, 2, 1, lv.GRID_ALIGN.CENTER, 0, 1)
signLabel=lv.label(timeContainer)
signLabel.set_style_text_font(lv.font_montserrat_48, 0)
signLabel.set_style_text_color(lv.color_white(), 0)
signLabel.set_text(":")
signLabel.set_grid_cell(lv.GRID_ALIGN.CENTER, 3, 1, lv.GRID_ALIGN.CENTER, 0, 1)
millionLabel=lv.label(timeContainer)
millionLabel.set_style_text_font(lv.font_montserrat_36, 0)
millionLabel.set_style_text_color(lv.color_white(), 0)
millionLabel.set_grid_cell(lv.GRID_ALIGN.END, 4, 1, lv.GRID_ALIGN.START, 0, 1)
setLabelValue(timeCount[currentSelect] *60*50)
startButton=lv.btn(win)
startButton.align(lv.ALIGN.CENTER, 0, 40)
startButton.set_size(126, 54)
startButton.set_style_radius(45, lv.PART.MAIN)
startButton.set_style_shadow_opa(0, 0)
startButton.set_style_bg_color(lv.color_make(0xFF, 0xA8, 0x48), lv.PART.MAIN)
startButton.align(lv.ALIGN.BOTTOM_LEFT, 12, -12)
startButton.add_event_cb(start_button_event_handler, lv.EVENT.CLICKED, None)
startLabel=lv.label(startButton)
startLabel.set_text("START")
startLabel.set_style_text_color(lv.color_black(), 0)
startLabel.set_style_text_font(lv.font_montserrat_20, 0)
startLabel.center()
resetButton=lv.btn(win)
resetButton.align(lv.ALIGN.CENTER, 0, 40)
resetButton.set_size(126, 54)
resetButton.set_style_radius(45, lv.PART.MAIN)
resetButton.set_style_shadow_opa(0, 0)
resetButton.set_style_bg_color(lv.color_white(), lv.PART.MAIN)
resetButton.align(lv.ALIGN.BOTTOM_RIGHT, -12, -12)
resetButton.add_event_cb(reset_button_event_handler, lv.EVENT.CLICKED, None)
resetLabel=lv.label(resetButton)
resetLabel.set_text("REST")
resetLabel.set_style_text_color(lv.color_black(), 0)
resetLabel.set_style_text_font(lv.font_montserrat_20, 0)
resetLabel.center()
# select timecol_dsc= [75, 75, 75, 75, lv.GRID_TEMPLATE.LAST]
row_dsc= [60, 80, 60, lv.GRID_TEMPLATE.LAST]
funcContainer=lv.obj(win)
funcContainer.set_layout(lv.LAYOUT_GRID.value)
funcContainer.set_style_bg_opa(0, 0)
funcContainer.set_style_border_opa(0, 0)
funcContainer.set_style_grid_column_dsc_array(col_dsc, 0)
funcContainer.set_style_grid_row_dsc_array(row_dsc, 0)
funcContainer.set_grid_align(lv.GRID_ALIGN.SPACE_BETWEEN, lv.GRID_ALIGN.SPACE_BETWEEN)
funcContainer.set_size(300, 90)
funcContainer.set_style_align(lv.ALIGN.TOP_MID, 0)
maxMillionSecond=timeCount[0] *60*50arc[0] =lv.arc(funcContainer)
arc[0].set_style_arc_color(lv.color_white(), lv.PART.INDICATOR)
arc[0].set_style_arc_color(lv.color_make(0x33, 0x33, 0x33), lv.PART.MAIN)
arc[0].set_range(0, maxMillionSecond)
arc[0].set_size(55, 55)
arc[0].set_rotation(90)
arc[0].set_bg_angles(0, 360)
arc[0].remove_style(None, lv.PART.KNOB)
arc[0].set_value(maxMillionSecond)
arc[0].set_style_arc_width(8, lv.PART.INDICATOR)
arc[0].set_style_arc_width(8, lv.PART.MAIN)
arc[0].set_grid_cell(lv.GRID_ALIGN.CENTER, 0, 1, lv.GRID_ALIGN.CENTER, 0, 1)
arc[0].clear_flag(lv.obj.FLAG.CLICKABLE)
totalTime=lv.label(funcContainer)
totalTime.set_text(str(timeCount[0]))
totalTime.set_style_text_font(lv.font_montserrat_18, 0)
totalTime.set_style_text_color(lv.color_white(), 0)
totalTime.set_grid_cell(lv.GRID_ALIGN.CENTER, 0, 1, lv.GRID_ALIGN.CENTER, 0, 1)
totalTime.add_flag(lv.obj.FLAG.CLICKABLE)
totalTime.add_event_cb(lambdae: arc_event_handler(e, 0), lv.EVENT.CLICKED, None)
totalTime.set_ext_click_area(30)
anim[0] =lv.anim_t()
anim[0].init()
anim[0].set_var(arc[0])
anim[0].set_time(maxMillionSecond*20)
anim[0].set_values(maxMillionSecond, 0)
anim[0].set_custom_exec_cb(lambdaa1, val: set_time_value(arc[0], val))
anim_timeline=lv.anim_timeline_create()
lv.anim_timeline_add(anim_timeline, 0, anim[0])
arc[1] =lv.arc(funcContainer)
arc[1].set_style_arc_color(lv.color_white(), lv.PART.INDICATOR)
arc[1].set_style_arc_color(lv.color_make(0x33, 0x33, 0x33), lv.PART.MAIN)
arc[1].set_range(0, maxMillionSecond)
arc[1].set_size(55, 55)
arc[1].set_rotation(90)
arc[1].set_bg_angles(0, 360)
arc[1].remove_style(None, lv.PART.KNOB)
arc[1].set_value(0)
arc[1].set_style_arc_width(2, lv.PART.INDICATOR)
arc[1].set_style_arc_width(2, lv.PART.MAIN)
arc[1].set_grid_cell(lv.GRID_ALIGN.CENTER, 1, 1, lv.GRID_ALIGN.CENTER, 0, 1)
arc[1].clear_flag(lv.obj.FLAG.CLICKABLE)
totalTime=lv.label(funcContainer)
totalTime.set_text(str(timeCount[1]))
totalTime.set_style_text_font(lv.font_montserrat_18, 0)
totalTime.set_style_text_color(lv.color_white(), 0)
totalTime.set_grid_cell(lv.GRID_ALIGN.CENTER, 1, 1, lv.GRID_ALIGN.CENTER, 0, 1)
totalTime.add_flag(lv.obj.FLAG.CLICKABLE)
totalTime.add_event_cb(lambdae: arc_event_handler(e, 1), lv.EVENT.CLICKED, None)
totalTime.set_ext_click_area(30)
arc[2] =lv.arc(funcContainer)
arc[2].set_style_arc_color(lv.color_white(), lv.PART.INDICATOR)
arc[2].set_style_arc_color(lv.color_make(0x33, 0x33, 0x33), lv.PART.MAIN)
arc[2].set_range(0, maxMillionSecond)
arc[2].set_size(55, 55)
arc[2].set_rotation(90)
arc[2].set_bg_angles(0, 360)
arc[2].remove_style(None, lv.PART.KNOB)
arc[2].set_value(0)
arc[2].set_style_arc_width(2, lv.PART.INDICATOR)
arc[2].set_style_arc_width(2, lv.PART.MAIN)
arc[2].set_grid_cell(lv.GRID_ALIGN.CENTER, 2, 1, lv.GRID_ALIGN.CENTER, 0, 1)
arc[2].clear_flag(lv.obj.FLAG.CLICKABLE)
totalTime=lv.label(funcContainer)
totalTime.set_text(str(timeCount[2]))
totalTime.set_style_text_font(lv.font_montserrat_18, 0)
totalTime.set_style_text_color(lv.color_white(), 0)
totalTime.set_grid_cell(lv.GRID_ALIGN.CENTER, 2, 1, lv.GRID_ALIGN.CENTER, 0, 1)
totalTime.add_flag(lv.obj.FLAG.CLICKABLE)
totalTime.add_event_cb(lambdae: arc_event_handler(e, 2), lv.EVENT.CLICKED, None)
totalTime.set_ext_click_area(30)
arc[3] =lv.arc(funcContainer)
arc[3].set_style_arc_color(lv.color_white(), lv.PART.INDICATOR)
arc[3].set_style_arc_color(lv.color_make(0x33, 0x33, 0x33), lv.PART.MAIN)
arc[3].set_range(0, maxMillionSecond)
arc[3].set_size(55, 55)
arc[3].set_rotation(90)
arc[3].set_bg_angles(0, 360)
arc[3].remove_style(None, lv.PART.KNOB)
arc[3].set_value(0)
arc[3].set_style_arc_width(2, lv.PART.INDICATOR)
arc[3].set_style_arc_width(2, lv.PART.MAIN)
arc[3].set_grid_cell(lv.GRID_ALIGN.CENTER, 3, 1, lv.GRID_ALIGN.CENTER, 0, 1)
arc[3].clear_flag(lv.obj.FLAG.CLICKABLE)
totalTime=lv.label(funcContainer)
totalTime.set_text(str(timeCount[3]))
totalTime.set_style_text_font(lv.font_montserrat_18, 0)
totalTime.set_style_text_color(lv.color_white(), 0)
totalTime.set_grid_cell(lv.GRID_ALIGN.CENTER, 3, 1, lv.GRID_ALIGN.CENTER, 0, 1)
totalTime.add_flag(lv.obj.FLAG.CLICKABLE)
totalTime.add_event_cb(lambdae: arc_event_handler(e, 3), lv.EVENT.CLICKED, None)
totalTime.set_ext_click_area(30)
# load contentlv.scr_load(scr)

运行示例

image.png

安装SquareLine Studio


SquareLine Studio 是一个可视化工具,可使用 LVGL 图形库开发 UI,且支持多个平台,如 MacOS、Windows 和 Linux。在该工具中,我们通过拖放就可以在屏幕上添加和移动小控件,图像和字体的处理也变得十分简单。

image.png

安装完成后的界面

image.png

30天无限制试用,注册个人账号可免费试用。


配置显示支持

屏幕类型

image.png

需要修改项目的默认配置,屏幕的宽度、高度、色深。

M5 Core2 的宽度应当设置为320 高度240 色深16bit


输出类型

语言切换为MicroPython。

LVGL版本需要选择8.1,否则会发生错误。


拖拽UI组件

image.png

左侧为图层和UI组件选择,也可以在Assets中加载自己设计的UI组件。


定义事件

image.png

可以在EVENT处添加事件,对应元素响应、显示或消失、或者执行一个函数。这个函数的内容可以在导出后的源文件中编辑。


image.png

编辑器提供的示例Thermostat,可以作为参考。


导出MicroPython文件

以下python文件会被创建

  • ui.py UI 代码
  • ui_helper.pyui.py 中加载
  • ui_images.py UI图像
  • ui_font_*.bin 字体
  • ui_events.py 自定义的事件函数

配置MicroPython UI

  • import lvgl 之后,调用初始化函数 lv.init()
  • 设置显示驱动
  • import ui


效果展示


M5Core2.jpg

制作了一个Wi-Fi搜索,并提供虚拟键盘输入密码配网。总共有4个界面,这是第二个。

为了方便调试,显示了帧率和内存、CPU占用率。


写在最后

尝试了很多次才成功加载并与HaaS Python兼容,虽然HaaS Python兼容MicroPython,但在一些地方还是有不同定义,需要耐心修改。由于HaaS Python和SquareLine Studio都出现了更新,新建项目遇到的困难减少了很多。

在LVGL的官网看到了久违的AliOS的Logo,SquareLine Studio也是LVGL官方推出的编辑器。希望后续HaaS能够直接整合工作流,方便UI开发。不需要再像我这样折腾了。写于2022年底。

目录
相关文章
|
2月前
|
人工智能 JavaScript API
零基础构建MCP服务器:TypeScript/Python双语言实战指南
作为一名深耕技术领域多年的博主摘星,我深刻感受到了MCP(Model Context Protocol)协议在AI生态系统中的革命性意义。MCP作为Anthropic推出的开放标准,正在重新定义AI应用与外部系统的交互方式,它不仅解决了传统API集成的复杂性问题,更为开发者提供了一个统一、安全、高效的连接框架。在过去几个月的实践中,我发现许多开发者对MCP的概念理解透彻,但在实际动手构建MCP服务器时却遇到了各种技术壁垒。从环境配置的细节问题到SDK API的深度理解,从第一个Hello World程序的调试到生产环境的部署优化,每一个环节都可能成为初学者的绊脚石。因此,我决定撰写这篇全面的实
385 67
零基础构建MCP服务器:TypeScript/Python双语言实战指南
|
2月前
|
数据采集 数据可视化 搜索推荐
Python数据分析全流程指南:从数据采集到可视化呈现的实战解析
在数字化转型中,数据分析成为企业决策核心,而Python凭借其强大生态和简洁语法成为首选工具。本文通过实战案例详解数据分析全流程,涵盖数据采集、清洗、探索、建模、可视化及自动化部署,帮助读者掌握从数据到业务价值的完整技能链。
251 0
|
2月前
|
存储 数据可视化 BI
Python可视化应用——学生成绩分布柱状图展示
本程序使用Python读取Excel中的学生成绩数据,统计各分数段人数,并通过Matplotlib库绘制柱状图展示成绩分布。同时计算最高分、最低分及平均分,实现成绩可视化分析。
120 0
|
2月前
|
数据采集 NoSQL 调度
当生成器遇上异步IO:Python并发编程的十大实战兵法
本文通过十大实战场景,详解Python中生成器与异步IO的高效结合。从协程演进、背压控制到分布式锁、性能剖析,全面展示如何利用asyncio与生成器构建高并发应用,助你掌握非阻塞编程核心技巧,提升I/O密集型程序性能。
84 0
|
30天前
|
数据采集 存储 Web App开发
Python爬虫库性能与选型实战指南:从需求到落地的全链路解析
本文深入解析Python爬虫库的性能与选型策略,涵盖需求分析、技术评估与实战案例,助你构建高效稳定的数据采集系统。
167 0
|
10天前
|
数据采集 数据可视化 API
驱动业务决策:基于Python的App用户行为分析与可视化方案
驱动业务决策:基于Python的App用户行为分析与可视化方案
|
2月前
|
缓存 监控 API
1688平台开放接口实战:如何通过API获取店铺所有商品数据(Python示列)
本文介绍如何通过1688开放平台API接口获取店铺所有商品,涵盖准备工作、接口调用及Python代码实现,适用于商品同步与数据监控场景。
|
2月前
|
存储 数据安全/隐私保护 开发者
Python深浅拷贝全解析:从原理到实战的避坑指南
在Python开发中,深浅拷贝是处理对象复制的关键概念。直接赋值仅复制引用,修改副本会影响原始数据。浅拷贝(如切片、copy方法)创建新容器但共享嵌套对象,适用于单层结构或需共享子对象的场景;而深拷贝(copy.deepcopy)递归复制所有层级,确保完全独立,适合嵌套结构或多线程环境。本文详解二者原理、实现方式及性能考量,帮助开发者根据实际需求选择合适的拷贝策略,避免数据污染与性能浪费。
148 1
|
30天前
|
传感器 人工智能 JavaScript
Playwright实战:写UI自动化脚本,速度直接起飞
简介: 测试工程师老王因UI自动化问题深夜奋战,反映出传统测试工具的局限性。微软开源的Playwright凭借智能等待、跨域操作、移动端模拟与网络拦截等强大功能,正迅速取代Selenium,成为新一代自动化测试标准。其稳定高效的设计显著降低维护成本,助力企业构建高质量测试流程。
|
2月前
|
移动开发 安全 Linux
Python文件操作的"保险箱":with语句深度实战指南
本文深入解析Python中`with`语句的原理与高级应用,通过“保险箱”类比,形象展示资源管理机制。从上下文管理协议到实战场景,涵盖文件、数据库、网络等多种资源的高效安全处理方式,助你写出更优雅、可靠的代码。
61 1

热门文章

最新文章

推荐镜像

更多