## 基于MicroPython的Micro-GUI
来自[[https://github.com/peterhinch/micropython-micro-gui|Peter Hinch在Github上的项目分享]]
它是一个轻量级的、可移植的MicroPython GUI库,用于显示,并从framebuf子类化了驱动程序。它是用Python编写的,可以在标准的MicroPython固件构建下运行。输入资料的选项包括:
- 根据应用程序,通过2到5个按钮。
- 通过一个开关导航操纵杆。
- 通过两个按钮和一个像这样的旋转编码器。
由于对输入的支持,它比nano-gui更大、更复杂。它可以在屏幕之间切换和启动模态窗口。除了nano-gui小部件外,它还支持列表框、下拉列表、各种输入或显示浮点值的方法以及其他小部件。
它与nano-gui的所有显示驱动程序兼容,因此可移植到各种显示器上。它还可以在主机之间移植。
### UI
# Arbitray waveform generator (AWG) with GUI - in short AWG -
#
#
# updated:
version_date = '03-Oct-2021'
# RP2040 based arbitrary wave form generator (AWG) with GUI
#
# GUI based on micro-gui released under the MIT License (MIT). See LICENSE.
# Copyright (c) 2021 Peter Hinch
#
# AWG based on "Arbitrary waveform generator for Rasberry Pi Pico"
# Rolf Oldeman, 13/2/2021. CC BY-NC-SA 4.0 licence
# AWG Hardware modified for 10bit R2R ladder network DAC
# AWG sofware modified to fit 10bit and AWG UI use model
# micro python package consists of:
# ui.py -this file- user interface and generator controls
# wave_gen.py calculates wave form and starts PIO (generator)
# hardware_setup.py display driver and ui initialisation
# main.py standard micropython file starts imports ui.py
# Minimize memory fragmentation to avoid running out of memory:
# Hardware_setup must be imported first before other modules because of RAM use.
# Next buffers for AWG are created, then AWG functions are imported,
# Finally all gui functions are imported
# garbage collector gc is called at specific points during imports and run time
#
import hardware_setup # Create a display instance
import gc
gc.collect() # precaution to free up unused RAM
# make buffers for the waveform.
# large buffers give better results but are slower to fill
# buffer and sample size used are calculated dynamically based on frequency of AWG
# in function setup_wave in wage_gen module
wavbuf={}
maxnsamp= 512 # must be a multiple of 4
wavbuf[0]=bytearray(maxnsamp*2)
#wavbuf[1]=bytearray(maxnsamp*2)
#ibuf=0
#remark: ibuf and wavbuf[1] are for later implementation
# AWG_status flag:
# status: Meaning:
# ------- --------
# stopped generator output stopped, new set up trigger is allowed, initialization status
# calc wave generetor set up is trigger, wave form is calculated, no further trigger is allowed
# running calculation finished, generator output active, new set up trigger is allowed
# -- init -- intitialization, generator not yet started using start button
#import AWG functions
from wave_gen import *
#define wave with init defaults
wave = {'func' : sine,
'frequency' : 40000,
'amplitude' : 0.5,
'offset' : 0.5,
'phase' : 0,
'replicate' : 1,
'pars' : [0.2, 0.4, 0.2],
'frequency_value' : 40000,
'freq_range' : 1,
'AWG_status' : '- init -',
'nsamp' : 0,
'F_out' : 0,}
# amplitude max default values to avoid overdriving AD8055 input stage
max_ampl = {'sine' : 0.48,
'pulse' : 0.89,
'gauss' : 0.55,
'sinc' : 0.5,
'expo' : 0.5,
'noise' : 1,
}
gc.collect() # precaution to free up unused RAM
# import gui functions
from gui.core.ugui import Screen, Window, ssd
from gui.widgets.label import Label
from gui.widgets.buttons import Button, CloseButton
from gui.widgets.dropdown import Dropdown
from gui.widgets.sliders import HorizSlider
from gui.widgets.scale_log import ScaleLog
from gui.core.writer import CWriter
# set font for CWriter
import gui.fonts.font6 as font # FreeSans 14 pix
from gui.core.colors import *
import sys
from machine import Pin, freq
import utime
import utime
# set GP23 to high to switch Pico power supply from PFM to PWM to reduce PWS noise
GP23 = Pin(23, Pin.OUT)
GP23.value(1)
gc.collect() # precaution to free up unused RAM
#======= define UI screen =======
head_line = ' Arbirtaty wave form generator ws'
version = version_date
class BaseScreen(Screen):
def __init__(self):
startstop_buttons = []
table_startstop_buttons = (
{'text' : 'setup', 'args' : ('setup',), 'bgcolor' : LIGHTGREEN, 'bdcolor' : False, 'litcolor' : GREEN},
{'text' : 'stop', 'args' : ('stop',), 'bgcolor' : LIGHTRED, 'bdcolor' : False, 'litcolor' : RED},
)
# definition of call backs
# call backs need to be defined first as they are called further down when gui input functions are defined
def startstop_cb(button, val):
gc.collect()
#print('0: mem at startstop callback', gc.mem_free())
if val == 'setup':
wave['AWG_status']='calc wave'
update_status(wave['AWG_status'])
wave['frequency'] = wave['frequency_value'] * wave['freq_range']
setupwave(wavbuf[0],wave)
update_status(wave['AWG_status'])
nsamp_lbl.value(str(wave['nsamp']))
# due to digital wave synthesis, AWG frequency can deviate from frequency entered
# actual frequency is calculated while wave is generated, displayed to the user here
f_out = wave['F_out']
if f_out > 999999:
fout_lbl.value('{:7.3f}'.format(f_out/1000000) + ' MHz')
elif f_out > 999:
fout_lbl.value('{:7.3f}'.format(f_out/1000) + ' kHz')
else:
fout_lbl.value('{:7.3f}'.format(f_out) + ' Hz')
elif val == 'stop':
wave['AWG_status']='stopped'
update_status(wave['AWG_status'])
wave['nsamp'] = 0
nsamp_lbl.value(str(wave['nsamp']))
fout_lbl.value('0' + ' Hz')
stopDMA()
else:
print('wrong button received')
def function_cb(dd):
fun = dd.textvalue()
# for each function
# enable/disable function parameter controls as required by function
if fun == 'sine':
rise_Slider.greyed_out(val=1)
up_Slider.greyed_out(val=1)
fall_Slider.greyed_out(val=1)
width_Slider.greyed_out(val=1)
noise_Slider.greyed_out(val=1)
expo_Slider.greyed_out(val=1)
wave['func'] = sine
wave['replicate'] = 1
Amplitude.value(max_ampl['sine'])
Offset.value(0.5)
wave['amplitude'] = max_ampl['sine']
wave['offset'] = 0.5
elif fun == 'pulse':
width_Slider.greyed_out(val=1)
rise_Slider.greyed_out(val=0)
up_Slider.greyed_out(val=0)
fall_Slider.greyed_out(val=0)
noise_Slider.greyed_out(val=1)
expo_Slider.greyed_out(val=1)
wave['func'] = pulse
wave['replicate'] = 1
Amplitude.value(max_ampl['pulse'])
Offset.value(0)
rise_Slider.value(0.05)
wave['amplitude'] = max_ampl['pulse']
wave['offset'] = 0
wave['pars'][0] = 0.05
elif fun == 'gauss':
width_Slider.greyed_out(val=0)
rise_Slider.greyed_out(val=1)
up_Slider.greyed_out(val=1)
fall_Slider.greyed_out(val=1)
noise_Slider.greyed_out(val=1)
expo_Slider.greyed_out(val=1)
wave['func'] = gaussian
wave['replicate'] = 1
Amplitude.value(max_ampl['gauss'])
Offset.value(0)
width_Slider.value(0.1)
wave['amplitude'] = max_ampl['gauss']
wave['offset'] = 0
wave['pars'][0] = 0.023
elif fun == 'noise':
width_Slider.greyed_out(val=1)
rise_Slider.greyed_out(val=1)
up_Slider.greyed_out(val=1)
fall_Slider.greyed_out(val=1)
noise_Slider.greyed_out(val=0)
expo_Slider.greyed_out(val=1)
wave['func'] = noise
wave['replicate'] = 1
Offset.value(0)
noise_Slider.value(0.5)
wave['pars'][0] = 4
elif fun == 'sinc':
width_Slider.greyed_out(val=0)
rise_Slider.greyed_out(val=1)
up_Slider.greyed_out(val=1)
fall_Slider.greyed_out(val=1)
noise_Slider.greyed_out(val=1)
expo_Slider.greyed_out(val=1)
wave['func'] = sinc
wave['replicate'] = 1
Amplitude.value(max_ampl['sinc'])
Offset.value(0.5)
width_Slider.value(0.1)
wave['amplitude'] = 0.5
wave['offset'] = 0.5
wave['pars'][0] = 0.03
elif fun == 'expo':
width_Slider.greyed_out(val=0)
rise_Slider.greyed_out(val=1)
up_Slider.greyed_out(val=1)
fall_Slider.greyed_out(val=1)
noise_Slider.greyed_out(val=1)
expo_Slider.greyed_out(val=0)
wave['func'] = exponential
wave['replicate'] = -1
Amplitude.value(max_ampl['expo'])
Offset.value(0)
width_Slider.value(0.1)
expo_Slider.value(0.49)
wave['amplitude'] = max_ampl['expo']
wave['offset'] = 0
wave['pars'][0] = 0.1
else:
print('no valid function selected')
# define call backs for each UI element changable by buttons and encoder
def amplitude_cb(s):
v = s.value()
wave['amplitude'] = v
def offset_cb(s):
v = s.value()
wave['offset'] = v
def freqlog_cb(f):
v = f.value()
if v < 80:
freq_lbl.value('{:3.1f}'.format(2*v))
else:
freq_lbl.value('{:4.0f}'.format(2*v))
wave['frequency_value'] = int(2*v)
def freq_range_cb(dd):
f = dd.textvalue()
if f == 'Hz':
wave['freq_range'] = 1
if f == 'kHz':
wave['freq_range'] = 1000
def rise_cb(s):
v = s.value()
rise_lbl.value('{:0.3f}'.format(v))
wave['pars'][0] = v
def up_cb(s):
v = s.value()
up_lbl.value('{:0.3f}'.format(v))
wave['pars'][1] = v
def fall_cb(s):
v = s.value()
fall_lbl.value('{:0.3f}'.format(v))
wave['pars'][2] = v
def width_cb(s):
v = s.value()
width_lbl.value('{:0.3f}'.format(v*0.2+0.003))
wave['pars'][0] = v*0.2+0.003
def expo_cb(s):
v = s.value()
if v < 0.5:
repli = -1
else:
repli = 1
#print('v= ', v, 'repli= ', repli)
expo_lbl.value('{:1.0f}'.format(repli))
wave['replicate'] = repli
def noise_cb(s):
v = s.value()
noise_lbl.value('{:1.0f}'.format(int(v*8)))
wave['pars'][0] = int(v*8)
def update_status(s):
if s == 'stopped':
status_lbl.value(text = s, bdcolor = None, bgcolor = LIGHTRED, fgcolor = WHITE)
elif s == 'calc wave':
status_lbl.value(text = s, bdcolor = None, bgcolor = BLACK, fgcolor = ORANGE)
elif s == 'running':
status_lbl.value(text = s, bdcolor = None, bgcolor = LIGHTGREEN, fgcolor = WHITE)
elif s == '- init -':
status_lbl.value(text = s, bdcolor = None, bgcolor = DARKBLUE, fgcolor = ORANGE)
else:
status_lbl.value(text = 'no stat' , bdcolor = RED, bgcolor = WHITE, fgcolor = RED)
# legend_cb shows only k(Hz) in the scale. This is consistent (my view)
# example 1 MHZ will show as 1000 kHz
def legend_cb(f):
if f < 1999:
return '{:<1.0f}'.format(2*f)
return '{:<1.0f}K'.format(2*f/1000)
# ======== instantiate screen and writer =======
super().__init__()
wri = CWriter(ssd, font, GREEN, BLACK, verbose=False)
# headline and version
col = 2
row = 2
Label(wri, row, col, head_line + version, fgcolor = BLUE, bgcolor = LIGHTGREY)
# create function labels
row = 25
Label(wri, row, col, 'Function:')
row -=2
Label(wri, row, col+215, 'AWG:', fgcolor = BLUE)
status_lbl=Label(wri, row, col+260, 65, bgcolor = ORANGE, fgcolor = BLACK)
update_status(wave['AWG_status'])
row += 45
Label(wri, row, col, 'Frequency:', fgcolor = CYAN)
row += 45
Label(wri, row, col, 'Amplitude:', fgcolor = LIGHTGREY)
col += 170
Label(wri, row, col, 'Offset:', fgcolor = LIGHTGREY)
row = 40
col = 215
Label(wri, row, col, 'samples:', fgcolor = BLUE)
nsamp_lbl = Label(wri, row, col+60, '000', bgcolor = BLUE, fgcolor = WHITE)
# ======= create AWG controls =======
# dropdown for function
col = 80
row = 22
Dropdown(wri, row, col, callback=function_cb,
elements = ('sine', 'pulse', 'gauss', 'sinc', 'expo', 'noise'),
bdcolor = GREEN, bgcolor = DARKGREEN)
# FREQUENCY: slider and frequency range dropdown
# Instantiate Label first, because Slider callback will run now.
row +=35
freq_lbl = Label(wri, row+11, col+120, 50, bdcolor=CYAN, fgcolor=YELLOW)
ScaleLog(wri, row-5, col, width = 110, legendcb = legend_cb,
pointercolor=RED, fontcolor=YELLOW, bdcolor=CYAN,
callback=freqlog_cb, value=1000, decades = 4, active=True)
Dropdown(wri, row+10, col+180, callback=freq_range_cb, elements = ('Hz', 'kHz'),
bdcolor = CYAN, fgcolor = YELLOW, bgcolor = DARKGREEN)
# Amplitude and offset sliders
row +=60
Amplitude = HorizSlider(wri, row, col, callback=amplitude_cb,
divisions = 10, width = 70, height = 12, fgcolor = LIGHTGREY, bdcolor=ORANGE, slotcolor=BLUE,
legends=('0', '0.5', '1'), value=0.5, active=True)
Offset = HorizSlider(wri, row, col+150, callback=offset_cb,
divisions = 10, width = 70, height = 12, fgcolor = LIGHTGREY, bdcolor=ORANGE, slotcolor=BLUE,
legends=('0', '0.5', '1'), value=0.5, active=True)
# Parameters for pulse will fill pars[0], pars[1] and pars[2].
# implemented as "hidden sliders" and value label
# as micro-gui doesn not have a active numerical label(yet), sliders with minimal width are used for numerical input
# number is displayed by a label next to slider
row +=35
col = 2
Label(wri, row, col, 'rise', fgcolor = BLUE)
rise_lbl = Label(wri, row, col+50, 40, bdcolor=False, fgcolor=LIGHTGREY)
rise_Slider = HorizSlider(wri, row, col+30, callback=rise_cb, fgcolor=BLUE, bdcolor=False, slotcolor=BLUE, width=16,
legends=None, value=0.05, active=True)
rise_Slider.greyed_out(1)
col = 110
Label(wri, row, col, 'up', fgcolor = BLUE)
up_lbl = Label(wri, row, col+40, 40, bdcolor=False, fgcolor=LIGHTGREY)
up_Slider = HorizSlider(wri, row, col+20, callback=up_cb, fgcolor=BLUE, bdcolor=False, slotcolor=BLUE, width=16,
legends=None, value=0.5, active=True)
up_Slider.greyed_out(1)
col = 220
Label(wri, row, col, 'fall', fgcolor = BLUE)
fall_lbl = Label(wri, row, col+50, 40, bdcolor=False, fgcolor=LIGHTGREY)
fall_Slider = HorizSlider(wri, row, col+25, callback=fall_cb, fgcolor=BLUE, bdcolor=False, slotcolor=BLUE, width=16,
legends=None, value=0.05, active=True)
fall_Slider.greyed_out(1)
#Parameter "width" for Gauss and Sinc, Paremeters "expo" for Expo and "noiseq" for Noise
# all will fill parameter pars[0] but are implemented separately as they have have different ranges
row +=30
col = 2
Label(wri, row, col, 'width', fgcolor = BLUE)
width_lbl = Label(wri, row, col+60, 40, bdcolor=False, fgcolor=LIGHTGREY)
width_Slider = HorizSlider(wri, row, col+40, callback=width_cb, fgcolor=BLUE, bdcolor=False, slotcolor=BLUE, width=16,
legends=None, value=0.5, active=True)
width_Slider.greyed_out(1)
col = 110
Label(wri, row, col, 'expo', fgcolor = BLUE)
expo_lbl = Label(wri, row, col+60, 30, bdcolor=False, fgcolor=LIGHTGREY)
expo_Slider = HorizSlider(wri, row, col+40, callback=expo_cb, fgcolor=BLUE, bdcolor=False, slotcolor=BLUE, width=16,
legends=None, value=0.49, active=True)
expo_Slider.greyed_out(1)
col = 200
Label(wri, row, col, 'noiseq', fgcolor = BLUE)
noise_lbl = Label(wri, row, col+80, 50, bdcolor=False, fgcolor=LIGHTGREY)
noise_Slider = HorizSlider(wri, row, col+40, callback=noise_cb, fgcolor=BLUE, bdcolor=False, slotcolor=BLUE, width=16,
legends=None, value=0.5, active=True)
noise_Slider.greyed_out(1)
# "Setup" button to start output of the AWG, "stop" button to stop output of the AWG
col = 80
row = 210
for t in table_startstop_buttons:
startstop_buttons.append(Button(wri, row, col, textcolor = WHITE, fgcolor = BLUE,
callback=startstop_cb, shape=CLIPPED_RECT, height = 25, **t))
col +=80 # second button distance to the right
col = 220
Label(wri, row-2, col+10, 'Frequency out:', fgcolor = ORANGE)
fout_lbl = Label(wri, row+14, col+10, 90,fgcolor = ORANGE)
fout_lbl.value('0' + 'Hz')
# close button to stop the UI disabled, power off switch used insted
# CloseButton(wri) # Quit the application
# start of main program
try:
gc.collect() # precaution to free up unused RAM after all is initialised
print('0: starting ui, mem: ', gc.mem_free())
#run the ui
Screen.change(BaseScreen)
except KeyboardInterrupt:
# DMA must be stopped, when program is interrupted, otherwise RP2040 needs a reset to restat DMA
print('0: Got ctrl-c')
stopDMA()
except Exception as e:
print('0: mainloop crashed: ', e)
finally:
print('0 finally: cleaning up')
stopDMA()
#sys.exit()
### Hardware_setup
# hardware_setup.py customised for KMRTM28028-SPI 8-Jul-21 ws
# Released under the MIT License (MIT). See LICENSE.
# Copyright (c) 2021 Peter Hinch
# As written, supports:
# ili9341 240x320 displays on Pi Pico
# Initialisation procedure designed to minimise risk of memory fail
# when instantiating the frame buffer. The aim is to do this as early as
# possible before importing other modules.
# WIRING
# Pico Display
# GPIO Pin
# 5V na VCC
# 3v3 36 LED
# IO21 27 RESET
# IO20 26 D/C
# IO19 25 SD (AKA MOSI)
# IO18 24 CLK Hardware SPI0
# GND 23 GND
# IO17 22 CS
# IO16 21 SDO (AKA MISO)
# Miso is assigned, because SPI0 default pin is used otherwise, not used here
# Pushbuttons are wired between the pin and Gnd
# remark: using hardware debounce results eliminates (rare) hang ups of RP2040
# Pico pin Function
# IO11 15 Select next control
# IO12 16 Select previous control
# IO13 17 Select / operate current control
# IO14 19 Increase value of current control
# IO15 20 Decrease value of current control
# n/a 18 Gnd
from machine import Pin, SPI, freq
import gc
from drivers.ili93xx.ili9341 import ILI9341 as SSD
freq(250_000_000) # RP2 overclock
# Create and export an SSD instance
pdc = Pin(20, Pin.OUT, value=0) # Arbitrary pins
prst = Pin(21, Pin.OUT, value=1)
pcs = Pin(17, Pin.OUT, value=1)
spi = SPI(0, baudrate=30_000_000, mosi=Pin(19), miso=Pin(16), sck=Pin(18))
gc.collect() # Precaution before instantiating framebuf
ssd = SSD(spi, pcs, pdc, prst, usd=False)
from gui.core.ugui import Display
# Create and export a Display instance
# Define control buttons
nxt = Pin(11, Pin.IN) # Move to next control
prev = Pin(12, Pin.IN) # Move to previous control
sel = Pin(13, Pin.IN) # Operate current control
increase = Pin(15, Pin.IN) # Increase control's value
decrease = Pin(14, Pin.IN) # Decrease control's value
display = Display(ssd, nxt, sel, prev, increase, decrease, encoder=4) # with encoder
#display = Display(ssd, nxt, sel, prev, increase, decrease) # with buttons