前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >基于树莓派的多功能USB实现--显示屏和按键交互菜单

基于树莓派的多功能USB实现--显示屏和按键交互菜单

作者头像
PedroQin
发布2020-02-12 11:00:59
1.4K0
发布2020-02-12 11:00:59
举报
文章被收录于专栏:WriteSimpleDemoWriteSimpleDemo

接上期基于树莓派的多功能USB实现--系统安装 显示屏和按键交互扩展板主要是为了方便在使用设备时多种模式切换,以及给与相应回显反馈。

git链接

https://github.com/pedroqin/RaspberryPi-based-multi-functional-USB-Device

实现功能

  • 头部显示IP和模式
  • 中间部分显示菜单内容和执行结果,模拟翻页
  • 底部显示load average 和 温度

硬件信息

扩展板引脚功能

功能引脚

树莓派接口(BCM)

描述

KEY1

P21

按键1GPIO

KEY2

P20

按键2GPIO

KEY3

P16

按键3GPIO

摇杆UP

P6

摇杆上

摇杆Down

P19

摇杆下

摇杆Left

P5

摇杆左

摇杆Right

P26

摇杆右

摇杆Press

P13

摇杆按下

SCLK

P11/SCLK

SPI时钟线

MOSI

P10/MOSI

SPI数据线

CS

P8/CE0

片选

DC

P25

数据/命令选择

RST

P27

复位

BL

P24

背光

LCD 及其控制器

本款LCD使用的内置控制器为ST7789VM,是一款240 x RGB x 320像素的LCD控制器,而本LCD本身的像素为240(H)RGB x 240(V),同时由于初始化控制可以初始化为横屏和竖屏两种,因此LCD的内部RAM并未完全使用。 该LCD支持12位,16位以及18位每像素的输入颜色格式,即RGB444,RGB565,RGB666三种颜色格式。 LCD使用四线SPI通信接口,这样可以大大的节省GPIO口,同时通信是速度也会比较快

实现

实现思路

由于是要做较上层应用,故不必纠结于底层和驱动屏幕,只需在其实现的基础上做开发即可。店家提供了c,python2的操作扩展板的示例程序和使用fbtft驱动显示屏的示例程序,这里我们在python2示例程序基础上开发。

如果忽略LCD扩展版底层实现,以显示图片为一个功能模块,则符合以下流程

即,交互菜单可以简化为获取执行按键选项==>绘制图片==>显示图片
屏幕分配
  1. 红色区域显示IP以及当前模式,于程序运行时初始化,后无需刷新
  2. 绿色区域显示菜单以及命令内容
  3. 蓝色区域显示当前load average以及温度,随绿色区域一起刷新
按键
  1. Key1/方向键 中 作为确认
  2. Key2 作为返回
  3. 方向键 左/上,菜单及运行结果查看时上翻
  4. 方向键 右/下,菜单及运行结果查看时下翻
  5. Key3/方向键 中 保留
程序实现
扩展板初始化
import spidev as SPI
import ST7789
import time,os,sys,logging
import RPi.GPIO as GPIO
from PIL import Image,ImageDraw,ImageFont

# Raspberry Pi pin configuration:
RST = 27
DC = 25
BL = 24
BUS = 0
DEVICE = 0

KEY_UP_PIN     = 6
KEY_DOWN_PIN   = 19
KEY_LEFT_PIN   = 5
KEY_RIGHT_PIN  = 26
KEY_PRESS_PIN  = 13

KEY1_PIN       = 21
KEY2_PIN       = 20
KEY3_PIN       = 16

# 240x240 display with hardware SPI:
disp = ST7789.ST7789(SPI.SpiDev(BUS, DEVICE),RST, DC, BL)
disp.Init() # Initialize library.
disp.clear() # Clear display.

#init GPIO
GPIO.setmode(GPIO.BCM)
GPIO.setup(KEY_UP_PIN,      GPIO.IN, pull_up_down=GPIO.PUD_UP) # Input with pull-up
GPIO.setup(KEY_DOWN_PIN,    GPIO.IN, pull_up_down=GPIO.PUD_UP) # Input with pull-up
GPIO.setup(KEY_LEFT_PIN,    GPIO.IN, pull_up_down=GPIO.PUD_UP) # Input with pull-up
GPIO.setup(KEY_RIGHT_PIN,   GPIO.IN, pull_up_down=GPIO.PUD_UP) # Input with pull-up
GPIO.setup(KEY_PRESS_PIN,   GPIO.IN, pull_up_down=GPIO.PUD_UP) # Input with pull-up
GPIO.setup(KEY1_PIN,        GPIO.IN, pull_up_down=GPIO.PUD_UP) # Input with pull-up
GPIO.setup(KEY2_PIN,        GPIO.IN, pull_up_down=GPIO.PUD_UP) # Input with pull-up
GPIO.setup(KEY3_PIN,        GPIO.IN, pull_up_down=GPIO.PUD_UP) # Input with pull-up
......
菜单定义
menu_str="""
1. Get Status ->  ./get_info.sh Get_Status
2. U Disk Mode -> ./get_info.sh U-DiskMode
3. Ether Mode -> ./get_info.sh EtherMode
4. AP Mode -> ./get_info.sh EnableAP
5. Keyboard Mode -> ./get_info.sh PI-as-keyboard
6. Flash U Disk -> ./get_info.sh Flash_U_Disk
7. Re-GetIP -> ./get_info.sh Re-GetIP
8. Cat Cmdline -> ./get_info.sh CatCmdline
9. Cat WlanCfg -> ./get_info.sh CatWlanCfg
10. Export Log -> ./get_info.sh Export_log %s
11. REBOOT -> sync;reboot
12. POWEROFF -> sync;poweroff
13. EXIT -> exit
"""
log记录
# logger
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
log_name = os.path.join("/tmp","main_" + time.strftime("%Y%m%d%H%M%S", time.localtime()) + ".log")
file_handler = logging.FileHandler(filename = log_name,mode="w")
file_handler.setLevel(logging.DEBUG)
console_hander = logging.StreamHandler()
console_hander.setLevel(logging.DEBUG)

formatter = logging.Formatter("%(asctime)s %(name)s %(levelname)s %(message)s")
file_handler.setFormatter(formatter)
console_hander.setFormatter(formatter)
logger.addHandler(file_handler)
logger.addHandler(console_hander)
屏幕头部
# the header had use 50
def draw_header(color="YELLOW"):
    # say hello
    image = Image.open(os.path.join(whereami,"pic/hi.jpg"))
    disp.ShowImage(image,0,0)

    # Draw a white filled box to clear the image
    draw.text((10, 10), 'IP :', fill = color, font=my_font1)
    draw.text((10, 30), os.popen("ip1=$(ifconfig wlan0 | awk '/inet / {print $2}');ip2=$(ifconfig usb0 2>/dev/null| awk '/inet / {print $2}');echo ${ip1:-$ip2}").read(), fill = color, font=my_font2)
    draw.text((120, 10), 'MODE :', fill = color, font=my_font1)
    draw.text((120, 30), os.popen("cat /proc/cmdline |grep -oE 'modules-load=[0-9a-z,_]+'|cut -d= -f2").read(), fill = color, font=my_font2)
    draw.line([(5,50),(235,50)], fill = "BLACK",width = 5)
屏幕底部
# load average and temperature, three colors for three ranges
def draw_bottom():
    load_info = os.popen("cat /proc/loadavg|cut -d ' ' -f'-3'").read()
    min_load = float(load_info.split()[0])
    if min_load >= 1:
        color="RED"
    elif min_load >= 0.75 and min_load < 1.0:
        color="GOLD"
    else:
        color="BLUE"
    draw.text((10, 225), "Load: " + load_info, fill = color)
    temp_info = os.popen('echo "scale=3;$(cat /sys/class/thermal/thermal_zone0/temp)/1000" | bc').read()
    temp2float = float(temp_info)
    if temp2float >= 50:
        color="RED"
    elif temp2float >= 45 and temp2float < 50:
        color="GOLD"
    else:
        color="BLUE"
    draw.text((150, 225),"Temp: " + temp_info, fill = color)
格式化字符

由于扩展板屏幕宽度有限,需考虑字符串长度超出屏幕宽度时进行换行。此处将输入字串按固定长度进行分割,返回list

def format_str(input_str,skip_null=0,line_number=0):
    line_max_width=220
    result_str=[]
    index=0
    for str_tmp in input_str.split("\n"):
        if skip_null :
            if not str_tmp : continue
        index += 1
        if line_number: str_tmp = str(index) + " " + str_tmp
        cur_width,cur_height=my_font3.getsize(str_tmp)
        if cur_width <= line_max_width:
            result_str.append(str_tmp)
        else:
            new_line=''
            for char in str_tmp:
                new_line += char
                cur_width,cur_height=my_font3.getsize(new_line)
                if cur_width > line_max_width:
                    result_str.append(new_line)
                    new_line='  '
                else: continue
            result_str.append(new_line)
    return result_str
按键事件
  • 按键事件需考虑按键抖动,在监测到按键后适当delay 消抖
  • 做菜单切换时根据功能会需要disable特定功能键,故作为参数带入
def listen_kbd(source_list,enable_ok=1,enable_cancel=1):
    global select_index
    while 1:
        if not GPIO.input(KEY_UP_PIN):
            select_index -= 1
            show(source_list)
            time.sleep(interval_second)

        if not GPIO.input(KEY_LEFT_PIN):
            select_index -= 1
            show(source_list)
            time.sleep(interval_second)

        if not GPIO.input(KEY_RIGHT_PIN):
            select_index += 1
            show(source_list)
            time.sleep(interval_second)

        if not GPIO.input(KEY_DOWN_PIN):
            select_index += 1
            show(source_list)
            time.sleep(interval_second)

        if not GPIO.input(KEY_PRESS_PIN):
            print("CENTER")
            time.sleep(interval_second)

        if not GPIO.input(KEY1_PIN):
            if not enable_ok: continue
            logger.debug("KEY1,ok")
            show(source_list,1)
            time.sleep(interval_second)

        if not GPIO.input(KEY2_PIN):
            if not enable_cancel: continue
            logger.debug("KEY2,cancel")
            time.sleep(interval_second)
            break

        if not GPIO.input(KEY3_PIN):
            logger.debug("KEY3,reset")
            time.sleep(interval_second)
绘制图片
  • 选中行用红色底纹标示
  • 同时要考虑到一页最多显示17行,实现翻页功能
# begin >= 0:begin at $begin line of centent ,-1 begin at end 
def draw_str(centent,begin=0,selected=0,color="BLUE"):
    x_offset = 10
    y_offset = 50
    single_line_height = 10
    max_width=220
    index_cur=0
    if begin >= 0 :
        centent=centent[begin:begin+max_lines_can_be_shown]
    elif begin < 0 :
        centent=centent[-max_lines_can_be_shown:]
    for line in centent:
        if index_cur == selected:
            draw.rectangle((x_offset,y_offset+(index_cur)*single_line_height,x_offset+max_width,y_offset+(index_cur+1)*single_line_height),fill = "RED", outline=0)
        draw.text((x_offset, y_offset+(index_cur)*single_line_height), line, fill = color, font=my_font3)
        index_cur += 1

# begin at 50 , end at 235, minus the lines used by bottom :10 ,so draw_info begin at (5,50) and end at (225,225), width :220 , height 175
# max lines : 17 
def draw_info(info_str,color="BLUE"):
    global all_centent,cur_display_first,select_index
    draw.rectangle((5,50,235,235), fill = "WHITE", outline=0)
    draw_bottom()
    all_centent.extend(info_str)

    if select_index + 1 - max_lines_can_be_shown > cur_display_first :
        cur_display_first = select_index + 1 - max_lines_can_be_shown
    elif select_index - cur_display_first <= 0 :
        cur_display_first = select_index
    draw_str(all_centent , begin = cur_display_first , selected=select_index - cur_display_first)
    disp.ShowImage(image1,0,0)
执行菜单操作
  • 对特殊操作执行特定动作,如退出时显示bye.jpg图片等
  • 当选中并执行操作后log显示
  • 记录操作log
# show list / show cmd is different
def show(source_list,run_cmd=0):
    global select_index,cur_display_first,all_centent
    # clear all_centent
    all_centent=[]

    options_num=len(source_list)
    select_index = select_index % options_num
    if not run_cmd:
        draw_info(source_list)
    else:
        logger.debug("select: " + str(select_index) + " tittle: " + menu[select_index] + " cmd: " + cmd[select_index])
        command_str=cmd[select_index]
        if command_str.find("Export_log") > 0:
            command_str=command_str % log_name
        if command_str.strip() == "exit" :
            image = Image.open(os.path.join(whereami,"pic/bye.jpg"))
            disp.ShowImage(image,0,0)
            sys.exit(0)
        elif command_str.find("poweroff") > 0:
            image = Image.open(os.path.join(whereami,"pic/bye.jpg"))
            disp.ShowImage(image,0,0)
            cmd_result=os.popen(command_str).read()
        else:
            select_index_bp=select_index
            cur_display_first_bp=cur_display_first
            select_index=0
            cur_display_first=1
            draw_info(format_str("Command: \n" + command_str))
            draw_info(format_str("Waiting..."))
            cmd_result=os.popen(command_str).read()
            logger.debug("Command:\n" + command_str + "\n" + cmd_result)
            draw_info(format_str("Result: \n" + cmd_result))
            draw_info(format_str("complete"))
            listen_kbd(all_centent,enable_ok=0)
            select_index=select_index_bp
            cur_display_first=cur_display_first_bp
            show(menu)

实现效果

http://mpvideo.qpic.cn/0bf24eaaeaaan4aoqrrxijpfbyodalqqaaqa.f10003.mp4?dis_k=08a335218e71ac1995a4eccb23246954&dis_t=1581476348

待完善

  • 由于早期直接基于商家示例做应用扩展,没有对程序做模块化编程,不利于扩展维护,待后续优化
  • 程序中使用的是while True轮询方式等待取键盘输入,会过多占用系统资源,后续做优化时可用GPIO.add_event_detect函数替代

参考链接

店家显示器模块参考(http://www.waveshare.net/wiki/1.3inch_LCD_HAT)

往期回顾

基于树莓派的多功能USB实现--系统安装

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-02-04,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 WriteSimpleDemo 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • git链接
  • 实现功能
  • 硬件信息
    • 扩展板引脚功能
      • LCD 及其控制器
      • 实现
        • 实现思路
          • 屏幕分配
            • 按键
              • 程序实现
                • 扩展板初始化
                • 菜单定义
                • log记录
                • 屏幕头部
                • 屏幕底部
                • 格式化字符
                • 按键事件
                • 绘制图片
                • 执行菜单操作
            • 实现效果
            • 待完善
            • 参考链接
            • 往期回顾
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档