MaixUI 基础使用指导

如何正确的食用 MaixUI 项目?

为什么要开发它?它的意义和存在价值是什么?

在任何芯片下永远存在对 UI 框架的基本需求,但由于 K210 无法在支持 Ai 功能的情况下继续使用 LVGL 环境,导致 UI 失去了本来存在的意义。

也就是在不能用 QT 也不能用 LVGL 的时候,又希望能够使用 Python 编写 UI 应用,所以才诞生了基于 image 的 MaixUI UI 框架。

对 MaixUI 的要求

在最新 MaixPy 固件的基础上 2020年10月7日 满足如下要求。

  • 确保 MicroPython 的 GC 内存在任何时候都是使用可回收可控的。

  • 确保 UI 组件代码独立,不包含在固件,可被调试修改。

  • 确保系统稳定性,保证代码和硬件资源均可重入,不会出现 core dump 现象。

  • 运行可重入,也就运行动态代码展示 UI 样式,类似 HTML5 / CSS 的设计。

  • Python 的异常捕获实时反馈到屏幕上,快速定位出错行。

  • UI 相关的绘制函数可被多处装饰使用,也可独立运行。

  • 框架提供的所有 MicroPython 硬件驱动均可独立运行相应的单元测试。

  • 框架运行时允许动态加载外部符合结构的 UI 应用,可以从 storage 或 network 上获取用户自定义应用。

所以在最基础的示例中,它将严格控制内存占用控制在 512k ~ 1M ,并将绘图性能保持 15 ~ 24fps 之间。

如何食用?

来,我们从最简单的入口代码开始说起,完整的代码在这里 app_main.py

# This file is part of MaixUI
# Copyright (c) sipeed.com
#
# Licensed under the MIT license:
#   http://www.opensource.org/licenses/mit-license.php
#

import time, gc, math, sys

try:
  from core import agent, system
  from dialog import draw_dialog_alpha
  from ui_canvas import ui, print_mem_free
  from ui_container import container
  from wdt import protect
  from creater import get_time_curve
except ImportError as e:
  sys.print_exception(e)
  from lib.core import agent, system
  from lib.dialog import draw_dialog_alpha
  from ui.ui_canvas import ui, print_mem_free
  from ui.ui_container import container
  from driver.wdt import protect
  from lib.creater import get_time_curve

分别是运行它所需要 import 的依赖代码,有如下依赖:

  • from core import agent, system
    • 提供一个 agent 软定时器和一个全局实例 system 软定时器对象。
  • from dialog import draw_dialog_alpha
    • 提供了一个圆角边框 MessageBox 控件的绘图操作。
  • from ui_canvas import ui, print_mem_free
    • 提供了一个 UI 画布的基础接口,通过它来管理全局的统一绘图操作。
  • from ui_container import container
    • 提供了一种运行 UI 应用的容器模块,可以通过它切换不同的 UI 应用。
  • from wdt import protect
    • 看门狗,保证系统在出现 core dump 后能够重启恢复过来。
  • from creater import get_time_curve
    • 一种基于时间或计数器的曲线生成函数,用来维持非线性动画效果。

这两段代码是用来 import 加载到不同区域(在 Flash/SD 的根目录或文件夹下)的代码,所以你知道怎么 import 代码了就行。

  • 可以使用 MaixPy IDE 发送文件,也可以使用 mpfshell-lite put 文件到硬件的 flash 或 sd 中。
  • 可以使用 SD 读卡器,把整个 maixui 仓库下的文件夹放到 SD 卡中启动即可。

定义 UI 应用

接着介绍一种典型的基础应用的案例,准备如下代码(class launcher 静态类)。


class launcher:

  def load():
    __class__.ctrl = agent()
    __class__.ctrl.event(20, __class__.draw)

  def free():
    __class__.ctrl = None

  @ui.warp_template(ui.blank_draw)
  @ui.warp_template(ui.grey_draw)
  @ui.warp_template(ui.bg_in_draw)
  @ui.warp_template(ui.anime_in_draw)
  @ui.warp_template(ui.help_in_draw)
  #@ui.warp_template(taskbar.time_draw)
  #@ui.warp_template(taskbar.mem_draw)
  #@catch # need sipeed_button
  def draw():
    height = 100 + int(get_time_curve(3, 250) * 60)
    pos = draw_dialog_alpha(ui.canvas, 20, height, 200, 20, 10, color=(255, 0, 0), alpha=200)
    ui.canvas.draw_string(pos[0] + 10, pos[1] + 10, "Welcome to MaixUI", scale=2, color=(0,0,0))
    ui.display()

  def event():
    __class__.ctrl.cycle()

在这里, class 类似于 实例类 中的 this 指针,可以通过它访问当前类的全局变量。

该静态类拥有有 load / free / event 三个生命周期函数用以提供给 UI 容器维持该 UI 应用的持续运行。

  • load 只会执行一次,用于 UI 应用的初始化。
  • free 只会执行一次,用于 UI 应用的释放。
  • event 将会提供给 UI 容器循环执行其中的操作。
    • UI 容器指的是 ui/ui_container.py
    • 当然你也可以不通过 UI 容器来维持运行。

可以看到该 UI 应用在 load 的时候定义了 agent 软定时器和设置了绘图函数的期望执行周期为 20ms ,设置再小也不会低于真实运行的周期。

    __class__.ctrl = agent()
    __class__.ctrl.event(20, __class__.draw)

然后在 event 函数中维持 软定时器 ctrl 拥有的分时事件(非阻塞 no-block),因此基于此设计你可以制作很多个不同定时的分时任务。

    __class__.ctrl.cycle()

它可以周期执行,也可以用完删除,就如下示范。

    self.ctrl = agent()
    # loop
    self.ctrl.event(5, self.draw)
    # once
    def into_launcher(self):
      container.reload(launcher)
      self.remove(into_launcher)
    self.ctrl.event(2000, into_launcher)

接着我们看到具体的 UI 绘图事件,不同于按键/触摸等硬件驱动事件,但无论是哪类事件,我们都期望它能够尽快结束,交出运行核心。

  @ui.warp_template(ui.blank_draw)
  @ui.warp_template(ui.grey_draw)
  @ui.warp_template(ui.bg_in_draw)
  @ui.warp_template(ui.anime_in_draw)
  @ui.warp_template(ui.help_in_draw)
  #@ui.warp_template(taskbar.time_draw)
  #@ui.warp_template(taskbar.mem_draw)
  #@catch # need sipeed_button
  def draw():
    height = 100 + int(get_time_curve(3, 250) * 60)
    pos = draw_dialog_alpha(ui.canvas, 20, height, 200, 20, 10, color=(255, 0, 0), alpha=200)
    ui.canvas.draw_string(pos[0] + 10, pos[1] + 10, "Welcome to MaixUI", scale=2, color=(0,0,0))
    ui.display()

在这里,我们有一个最基础的 draw() 绘图函数,也为它装饰了 5 个基础函数,事实上装饰只是好看,它实际上等效于如下代码,所以是否使用取决于你的喜好。

  def draw():
    ui.blank_draw()    # 准备一个空白的 image 画布对象
    ui.grey_draw()     # 给 画布 画上灰色
    ui.bg_in_draw()    # 给 画布 画上内置的 背景图 一个 sipeed 的 logo 。
    ui.anime_in_draw() # 给 画布 加载四周水波动画效果
    ui.help_in_draw()  # 给 画布 画上 内置的 帮助说明。

    height = 100 + int(get_time_curve(3, 250) * 60) # 获取基于时间的正弦曲线值
    # 在指定位置画出 圆角边框的 MessageBox 的效果,并获取边框的 左上角起点 。
    pos = draw_dialog_alpha(ui.canvas, 20, height, 200, 20, 10, color=(255, 0, 0), alpha=200)
    # 在指定位置打印 "Welcome to MaixUI" 字符串。
    ui.canvas.draw_string(pos[0] + 10, pos[1] + 10, "Welcome to MaixUI", scale=2, color=(0,0,0))
    # 把当前的画布显示到屏幕上,多次执行也不影响,执行后会释放当前画布对象。
    ui.display()

接入其他按键/触摸/摄像头的事件亦如此,可以在此查看 UI 绘图的具体实现 ui/ui_canvas.py

运行 UI 框架

在真正进入上述的业务逻辑之前,我们需要把 UI 框架跑起来,因此我们需要一个入口函数,如 if __name__ == "__main__": 中的代码。


if __name__ == "__main__":
  container.reload(launcher)
  while True:
    container.forever()

讲解一下,我们看到使用 UI 容器 (container.reload(launcher)) 加载一个名为 launcher 的 UI 应用即可运行,可以在此查看 UI 容器的具体实现 ui/ui_container.py

但仅仅这样写是不够稳定的,所以我们可以通过两个 while True 保持程序永远不会退出(除非系统 core dump 崩溃)。

并通过 last 与 当前 tick_ms 做差得到当前的 fps 值,建议非调试场合建议关闭 print 这个函数,它非常耗时(ms 级)。

  while True:
    while True:
      last = time.ticks_ms() - 1
      while True:
        try:
          #time.sleep(0.1)
          print(1000 // (time.ticks_ms() - last), 'fps')
          last = time.ticks_ms()
        except Exception as e:
          gc.collect()
          print(e)
        finally:
          try:
            ui.display()
          except:
            pass

然后我们加强一下环境的稳定性,加入看门狗的维持(protect.keep())和 GC 内存回收(gc.collect()),还有维持一个全局的软定时器(system.parallel_cycle()),用作全局的定时器线程。


if __name__ == "__main__":
  container.reload(launcher)
  while True:
    while True:
      last = time.ticks_ms() - 1
      while True:
        try:
          #time.sleep(0.1)
          print(1000 // (time.ticks_ms() - last), 'fps')
          last = time.ticks_ms()

          gc.collect()
          container.forever()
          system.parallel_cycle()

          protect.keep()
          #gc.collect()
          #print_mem_free()
        except KeyboardInterrupt:
          protect.stop()
          raise KeyboardInterrupt
        #except Exception as e:
          #gc.collect()
          #print(e)
        finally:
          try:
            ui.display()
          except:
            pass

  • 你可以通过 time.sleep(0.1) 来降低 UI 容器的执行速率来观察 UI 的变化状态是否符合预期,有时候高于 15 fps 的变化人眼感知不到,就可以减少不必要的绘图过程,压缩绘图过程提高性能。
  • 你可以通过 except Exception as e: 来保证任何异常都不会导致 UI 框架的崩溃,但调试的时候可以把这个注释,来捕获可能出现的异常。

默认情况下程序超过 10 秒没有执行 protect.keep() 重置看门狗,则系统自动重启,这从 import wdt 驱动的时候就开始计时了,详细可以看 driver/wdt.py 驱动。

最后再加入捕获 KeyboardInterrupt 异常事件来保证程序可以在 IDE 或 Ctrl + C 输入后,停下来并被重新运行,并停下看门狗事件(protect.stop()),同时还要在 finally 中试图执行 ui.display() 防止绘图事件中存在异常导致没有释放画布,保证 image 画布对象永远都能在循环的最后被释放。

  try:
    protect.keep()
  except KeyboardInterrupt:
    protect.stop()
    raise KeyboardInterrupt
  except Exception as e:
    gc.collect()
    print(e)
  finally:
    try:
      ui.display()
    except:
      pass

以上就是 MaixUI 框架最基础的示范,虽然 MaixUI 只会提供 Cube 和 Amigo 的应用案例,但只要基于 MaixPy 均可使用,或者说,支持 image 接口对象的 MicroPython 环境均可使用。

希望我们未来能会同步到 CPython 共用的,也就是可以在 CPython 上进行 UI 样式的开发同步到 MicroPython 环境中,这会高效率的完成开发的,但性能也不能落下。

最后

本文档介绍如何运行最基础的示例,如果想看更多示例,可以参考 app_cube.py & app_amigo.py 两个案例。

截至目前 2020年10月7日 已经完成 MaixPy 的常见功能使用的 App 案例,不过这需要你亲自烧写一下体验看看了 XD , 说明里只有一点简单的交互与动画展示。

目前 app_main.py 运行效果如下: