Wearable Solution
QuecPython has launched a GUI solution for the wearable industry, including clock display, call making/answering, heart rate/temperature/blood oxygen measurement, step count display, system settings and other optional functions.
The wearable industry solution uses LVGL to draw graphical interfaces. LVGL is a lightweight, open source embedded graphics library. QuecPython has integrated LVGL and uses NXP's GUI Guider as a graphical interface design tool to automatically generate QuecPython code, greatly improving the efficiency of embedded platform graphical interface design.
Interface Introduction
The wearable solution provides home page, dial page, application page, standby page, dial page, heart rate/blood oxygen/temperature detection page, settings page and other pages.
Dial Page
Components:
- Common Top Bar:
- Signal display
- Operator display
- Battery level display
- Navigation icon display
- Dial Content:
- Time display: The time is refreshed in real time
- Hour/Minute/Second hand: Updated in real time with the current time
- Background dial showing current time
- Date display: The date is refreshed in real time
- Interactions:
- Swipe left on the digital dial to enter the lock screen, swipe right to enter the analog dial
- Swipe left on the analog dial to enter the digital dial, swipe right to enter the app list 1
- After 30s of inactivity, the device will enter lock screen. Swipe right to unlock and enter dial
Application List Page
- Heart Rate Measurement:
- Tap to enter heart rate measurement page
- Phone:
- Tap to enter phone dial page
- Chat:
- Tap to enter chat page to send/receive voice
- Timer:
- Tap to enter timer page
- Interactions:
- Swipe right on app list page to enter app list page 2
- Swipe left on app list page 2 to return to app list page 1, swipe right to enter detection pages
Detection Page
The detection page includes body temperature/blood oxygen/heart rate measurement, measurement status and historical measurement display.
Phone Page
Provides phone dialing, incoming/outgoing call modes and displays.
Step Count Page
Provides daily step count, historical step count display, and step goal setting display.
Timer Page
The timer page provides time setting, countdown timer, and alert when timer ends.
Alarm Page
Provides alarm setting and deletion, alarm alerts and displays.
Settings Page
- About Device
- Provides basic system information like manufacturer, model, OS version
- System Upgrade
- Used to check for system software updates to ensure all features are up-to-date
- May include auto-update options to automatically download and install updates when available
- Set Watch Face
- Allows switching and displaying different watch faces
- Device Ringtone
- Set incoming call ringtone
- Set alarm ringtone
- Vibration Mode
- If device has a vibration motor, vibration mode can be set
- Factory Reset
- Reset device to factory default settings
- Power Off
- Send power off command to device
Code Introduction
Code Github Repo: https://github.com/QuecPython/solution-wearable
Code Directory Structure
Copy code.
|-- EventMesh.py
|-- common.py
|-- constant.py
|-- lcd.py
|-- css.py
|-- ui.py
|-- mgr.py
|-- main_t.py
|-- 16px.bin
|-- 28px.bin
|-- 56px.bin
|-- img
|-- point.png
|-- gps.png
|-- bat4.png
|-- ...png
EventMesh.py
is an event management module. When events are triggered on screen controls, users only need to call related interfaces to send events and EventMesh will automatically switch screen contents.common.py
mainly provides common abstract base classes and common dependent classes. For example,class Abstract
provides lifecycle abstractions.constant.py
mainly provides constant configurations.lcd.py
mainly provides LCD driver initialization, TP driver initialization, and LVGL initialization. The screen will only display after successful initialization of this file.css.py
mainly provides css functionalities, static styles, font styles etc for screen objects inui.py
to use.ui.py
- Provides base class
class Screen()
for screen objects, with public methods needed by all screens, like screen creation, status bar info display (operator name, signal strength, system time, battery level etc). class **Screen
implements a class that inherits fromclass Screen()
, representing code implementation for each screen's display content. The interface code is mainly automatically generated by the GUI Guider tool, with minor modifications done afterwards, primarily in the object initialization method__init__()
and control event handler callbacks__xxx_event_cb()
. Modifications can also be done at various lifecycle stages.
- Provides base class
mgr.py
mainly provides background functionalities, allowing interfaces to interact with various managers at different lifecycle stages or upon event triggers.main_t.py
is the application entry script file.- Calls
lcd.py
to initialize screen drivers and display. - Initializes
APP
, the wearable app. - Calls
class UI()
inui.py
to create the GUI object for the wearable solution, and adds required screen objects to this GUI object's screen object adder, which would execute the page's initialization method during addition. - Adds managers to
APP
and executes managers' initialization methods. - Starts
APP
. - Jumps to main interface.
- Calls
Application Startup Process
The application startup process is as follows:
Software Design
Software Framework Design
Application Design
The software design framework diagram for the wearable solution is as follows:
Interface Switching Mechanism
EventMesh Event Management Framework
As mentioned above, interface switching is driven by the EventMesh event management framework.
- EventMesh is an application framework based on the publish-subscribe mechanism. Users can subscribe to any topics they are interested in, representing specific events.
- When subscribing to a topic, a callback function needs to be registered to handle the generated event.
- Users can publish the event from anywhere events are triggered, carrying message content. The registered callback functions of subscribers will then be invoked to process the event.
The EventMesh design framework is shown below:
Interface Switching Principles
Page Template and Lifecycle Example
# ui.py
# Any new class **Screen must inherit from Screen. The lv_obj must be predefined.
class DemoScreen(Screen):
# NAME is required, it is used to find the screen when switching
# For example we use EventMesh.publish("load_screen", {"screen": "demo_screen"}) to find this screen
NAME = "demo_screen"
def __init__(self):
super().__init__()
# meta is the core switching object
self.meta = app_list_screen
# prop contains info passed during the jump
# if EventMesh.publish("load_screen", {"screen": "demo_screen", “test”:1}) is called
# then prop = {"screen": "demo_screen", “test”:1}
self.prop = None
# Called only once when ui.add_screen(DemoScreen())
# Users can implement initialization here
def post_processor_after_instantiation(self, *args, **kwargs):
"""Called after instantiation"""
pass
# These methods will be called in sequence whenever
# EventMesh.publish("load_screen", {"screen": "demo_screen"}) switches to this screen
# post_processor_before_initialization
# initialization
# post_processor_after_initialization
# Users can implement functionality here, called every time when switching to this screen
def post_processor_before_initialization(self, *args, **kwargs):
"""Called before initialization"""
pass
def initialization(self, *args, **kwargs):
"""initialization"""
pass
def post_processor_after_initialization(self, *args, **kwargs):
"""Called after initialization"""
pass
# Called when leaving this screen, for example below will
# first call deactivate() before calling main screen init()
def deactivate(self, *args, **kwargs):
"""initialize load"""
pass
# Note: If the screen is in a tileview, the mapping must be added for switching with
# EventMesh.publish("load_screen", {"screen": "main_screen"}) to work
# See example mapping below in UI class
class UI(Abstract):
def __init__(self):
self.screens = []
self.current = None
# display_screen maps to tile at column 0 row 0
# main_screen maps to tile at column 0 row 1, etc
self.tileview_map = {
"display_screen": [0, 0, lv.ANIM.OFF],
"main_screen": [1, 0, lv.ANIM.OFF],
"watch_face_screen": [2, 0, lv.ANIM.OFF],
"app_list_1_screen": [3, 0, lv.ANIM.OFF],
#...
}
# Reverse mapping of above
self.tileview_position_map = {
(0, 0): "display_screen",
(1, 0): "main_screen",
(2, 0): "watch_face_screen",
#...
}
# What is tileview? For example, tile1 is at position column 1 row 0, supports left/right slide
tile1 = tileview.add_tile(1, 0, lv.DIR.RIGHT | lv.DIR.LEFT)
# Put main_screen on tile1, swiping left enters tile at (0,0), swiping right enters tile at (2, 0)
main_screen = lv_obj(
parent=tile1,
#...
)
As can be seen from the above code, the callback implementation for each touch event is similar.
Taking the application list control touch as an example, the code is:
# Event handling when app icon clicked
def btn_click(self, event, i):
screen_info = self.profile[i][1]
if screen_info:
# Found click event, switch page
EventMesh.publish("load_screen", screen_info)
# Render app icons
def post_processor_after_instantiation(self):
for i, btn_profile in enumerate(self.profile):
btn = lv_img(
parent=self.container,
#...
)
# Add click event to each button
btn.add_event_cb(lambda event, cur=i: self.btn_click(event, cur), lv.EVENT.CLICKED, None)
self.btn_list.append(btn)
The core statement is EventMesh.publish("load_screen", screen_info)
, which sends a "load_screen"
event indicating loading a new screen, carrying the message screen_info
which contains the name of the screen to switch to, like "blood_screen"
.
UI Startup
The "load_screen"
event is subscribed in the start()
method of class AgriUi(object)
in ui.py
:
class UI(Abstract):
def lv_load(self, event, msg):
# Find screen based on name
for screen in self.screens:
if screen.NAME == msg["screen"]:
# Switch logic
pass
def start(self):
# Subscribe event
EventMesh.subscribe("load_screen", self.lv_load)
# Load first screen
EventMesh.publish("load_screen", {"screen": "main_screen"})
The above code starts up the wearable GUI:
add_screen
creates metadata for each screenEventMesh.subscribe()
subscribes to"load_screen"
, invokinglv_load()
when event occurslv_load() finds the target screen based on name, loads it with lv.scr_load()
First screen loaded with
EventMesh.publish("load_screen", {"screen": "main_screen"})
This completes wearable GUI startup, subsequent screen switching is driven by user touch.
Graphical Interface Design
As mentioned before, QuecPython uses NXP's GUI Guider tool for graphical interface design. This tool allows not only interface layout design, but also automatic QuecPython code generation. Click here for a GUI Guider tutorial.
The following sections demonstrate the graphical interface design process using the wearable solution app as an example.
Layout and Background Setup
Start by selecting a suitable template and background design for a watch face app. This example starts from a blank template, setting the resolution to 240*280px.
Interface Drawing
This section focuses on drawing the main dial page to demonstrate layout and quick code implementation.
- Top Bar
Components: obj, img, label
Operator and Signal (Box 1):
An obj containing an img for signal and label for operator name, left aligned in layout, 4px gap between img and label
- obj size is specific pixel width * 20px
- img size is 20px * 20px
Navigation and Battery (Box 2):
An obj containing a navigation img and battery level img, 8px gap between them, sizes:
- obj size is 48px * 20px
- img size is 20px * 20px
Overall Layout:
- Flex layout, default horizontal axis
- Size 240px * 34px
- Main axis left-right aligned using space-between
- Children aligned flex-end on cross axis, i.e. bottom
Layout padding: top 0px, right 12px, bottom 4px, left 12px
Implementation:
# Styles from css.py
# Main page
main_screen = lv_obj(
size=(240, 280),
style=[...]
)
# Top bar
main_top = lv_obj(
parent=main_screen,
size=(240, 34),
flex_flow=lv.FLEX_FLOW.ROW,
flex_align=(lv.FLEX_ALIGN.SPACE_BETWEEN, lv.FLEX_ALIGN.END, lv.FLEX_ALIGN.END),
style=[...]
)
# Box 1
main_top_cont_1 = lv_obj(
parent=main_top,
size=(88, 20),
flex_flow=lv.FLEX_FLOW.ROW,
flex_align=(...),
style=[
# gap set to 4px in style
(style_main_top_cont_1, lv.PART.MAIN | lv.STATE.DEFAULT),
...]
)
# Signal img parent is Box 1
main_top_cont_1_img_signal = lv_img(
parent=main_top_cont_1,
size=(20, 20),
src="U:/media/s4.png",
style[...]
)
# Operator label parent is Box 1
main_top_cont_1_label_operator = lv_label(
parent=main_top_cont_1,
size=(64, 19),
text="Operator",
style[...]
)
# Box 2
main_top_cont_2 = lv_obj(
parent=main_top,
size=(48, 20),
flex_flow=lv.FLEX_FLOW.ROW,
flex_align=(...),
style[...]
)
# Navigation img parent is Box 2
main_top_cont_2_img_gps = lv_img(
parent=main_top_cont_2,
...)
# Battery img parent is Box 2
main_top_cont_2_img_bat = lv_img(
parent=main_top_cont_2,
...)
- Time Section
- Time Section
- Size 116px * 200px
- Position:
- top: 64px
- left: 16px
- Hour Hand Section (Box 1)
- Size 118px * 200px
- 2 images
- Each image size 58 * 100px
- Flex layout, horizontal axis
- Minute Hand Section (Box 2)
- 2 images
- Image size 58 * 100px each
- Flex layout, horizontal axis
- Layout:
- Flex layout, vertical axis
# ... main_screen
# Create time section
main_content_cont_1 = lv_obj(
parent=main_screen,
size=(116, 200),
pos=(19, 64),
flex_flow=lv.FLEX_FLOW.COLUMN,
flex_align=(...),
style[...]
)
# Create Box 1
main_content_cont_1_hour = lv_obj(
parent=main_content_cont_1,
size=(116, 100),
flex_flow=lv.FLEX_FLOW.ROW,
flex_align=(...),
style[...]
)
# Hour hand img 1 in Box 1
main_content_cont_1_hour_0 = lv_img(
parent=main_content_cont_1_hour,
size=(58, 100),
src="U:/media/h0.png",
style[...]
)
# Hour hand img 2 in Box 1
main_content_cont_1_hour_1 = lv_img(
...)
# Create Box 2
main_content_cont_1_m = lv_obj(
parent=main_content_cont_1,
# Inherits vertical flex layout from parent
flex_flow=lv.FLEX_FLOW.ROW,
...)
# Minute hand img 0 in Box 2
main_content_cont_1_m_0 = lv_img(
...)
# Minute hand img 1 in Box 2
main_content_cont_1_m_1 = lv_img(
...)
When placing elements in GUI Guider, ensure components align to grid and layout appropriately for different screen sizes and resolutions. The design should be clean and minimalist, use appropriate color schemes to make key info prominent yet non-intrusive, interactive elements should provide instant feedback e.g. hover/click change appearance. Test on different devices to ensure usability and performance.
If the system will be used under various lighting conditions like direct sunlight, adjust contrast and brightness to enhance visibility. Finally, the design should be intuitive for new users to navigate without extensive training, reflecting good user experience (UX) design principles.
The code generated by GUI tools lacks flexibility and maintainability. It's recommended to refactor as shown above for better encapsulation, simplicity and maintainability.