V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐学习书目
Learn Python the Hard Way
Python Sites
PyPI - Python Package Index
http://diveintopython.org/toc/index.html
Pocoo
值得关注的项目
PyPy
Celery
Jinja2
Read the Docs
gevent
pyenv
virtualenv
Stackless Python
Beautiful Soup
结巴中文分词
Green Unicorn
Sentry
Shovel
Pyflakes
pytest
Python 编程
pep8 Checker
Styles
PEP 8
Google Python Style Guide
Code Style from The Hitchhiker's Guide
mylovesaber
V2EX  ›  Python

[手滑没写完就发出来了,稍等] 请教树莓派利用 curses 库和红外传感器实现 2.4g 无线键盘遥控和避障功能实现逻辑的一个疑问(内含具体源码和详细解释,逻辑介绍和遇到的问题)

  •  
  •   mylovesaber · 2020-04-19 21:35:29 +08:00 · 2293 次点击
    这是一个创建于 1678 天前的主题,其中的信息可能已经有所发展或是发生改变。

    py 新人买了个树莓派练习,目的是实现一个 2.4g 键盘遥控的小车(已经实现的其他功能后面都不提),同时具有针对延迟问题的避障处理,目前基本都实现了,就差一个避障逻辑没弄明白只实现了一半,下面分三个部分介绍下目前我的情况,希望有前辈能指点一下:

    • 基本控制源码 demo (只放了前进后退的功能)
    • 个人对于避障逻辑的介绍
    • 实践出现的问题

    基本控制源码 demo

    关于 curses 库的官方文档和更详细的文档: https://docs.python.org/zh-cn/3/howto/curses.html http://doc.codingdict.com/python_352/library/curses.html

    #!/usr/bin/python3
    # -*- coding: utf-8 -*-
    import curses
    import subprocess
    import RPi.GPIO as io
    
    # 定义各种硬件对应的 GPIO 引脚编号
    # BCM 编码是需要查询官方引脚图定义的
    # 面包板使用的扩展板有详细标记(例):GPIO xx
    
    io.setmode(io.BCM)
    rt1 = 16        # rt1/rt0:右前后轮正负极,1 正 0 负,r 右 l 左后面全都这么设置的
    rt0 = 20
    lt1 = 21        # lt1/lt0:左前后轮正负极
    lt0 = 26
    ena = 12        # a 右 b 左使能引脚
    enb = 19
    disb = 25       # 红外模块引脚
    freq = 50       #频率 50
    
    #进行 curses 库的初始化设置
    screen = curses.initscr()
    curses.noecho()
    curses.cbreak()
    screen.keypad(True)
    io.setwarnings(False)
    
    # 除了避障模块为读取信号,其余均为输出信号
    io.setup(rt1,io.OUT)
    io.setup(rt0,io.OUT)
    io.setup(lt1,io.OUT)
    io.setup(lt0,io.OUT)
    io.setup(disb,io.IN)                  #模块检测距离不够输出 0 给树莓派,距离正常输出 1 给树莓派
    io.setup(ena,io.OUT)
    io.setup(enb,io.OUT)
    
    # 设置,初始化并启用舵机和马达 pwm 调速
    enr = io.PWM(ena,50)
    enl = io.PWM(enb,50)
    enr.start(0)
    enl.start(0)
    
    # 小车前进( 20 占空比)
    def forward():
        io.output(rt1,io.HIGH)           # 正高电平,负低电平,pwm 设置慢速前进
        io.output(rt0,io.LOW)
        io.output(lt1,io.HIGH)
        io.output(lt0,io.LOW)
        enr.ChangeDutyCycle(20)
        enl.ChangeDutyCycle(20)
    
    # 小车后退( 20 占空比)
    def backward():
        io.output(rt1,io.LOW)           # 正低电平,负高电平,pwm 设置慢速后退
        io.output(rt0,io.HIGH)
        io.output(lt1,io.LOW)
        io.output(lt0,io.HIGH)
        enr.ChangeDutyCycle(20)
        enl.ChangeDutyCycle(20)
    
    # 停止舵机和小车
    def stop():
        io.output(rt1,io.LOW)           # 四个轮子重置为低电平
        io.output(rt0,io.LOW)
        io.output(lt1,io.LOW)
        io.output(lt0,io.LOW)
        enr.ChangeDutyCycle(0)          # enr/enl 左右车轮占空比重置为 0
        enl.ChangeDutyCycle(0)
    
    # 清理
    def clean():
        curses.nocbreak()
        screen.keypad(0)
        curses.echo()
        curses.endwin()
        pwmud.stop()
        pwmlr.stop()
        enr.stop()
        enl.stop()
        io.cleanup()
    
    try:
        while True:
            char = screen.getch()                             #等待键盘按下按键
            if char == ord('k'):                                 #按键 k 会让整个程序退出
                break
            elif char == 10:                #我的蓝牙键盘方向键中间是 ok 键,按下就是车轮停止工作
                stop()
            elif char == curses.KEY_UP:      #按方向上键,先进行判断红外传感器如果读到 0 就让按键不工作
                if io.input(disb) == 0:                        #就是前面有障碍就让上键不能让小车前进
                    stop()                                           
                else:
                    while True:                                   #否则进入让小车停止的判断
                        forward()
                        #screen = curses.initscr()
                        #char = screen.getch()
                        if char == 10:             #如果前方无障碍物,按下 ok 键,小车停止前进跳出循环
                            stop()                                   #之后可以用方向下键后退
                            break
                        if io.input(disb) == 0:                #如果按过前进按键后不操作直到快撞上
                            stop()                                  #红外距离不够,自动停车
                            break
                            #if char == 10:                          #后面 7 行是测试的,好像没用
                            #    stop()
                            #    enr.ChangeDutyCycle(0)
                            #    enl.ChangeDutyCycle(0)
                        #elif char == 10:
                            #stop()
                            #break
    
            elif char == curses.KEY_DOWN:                  #如果按下后退键,小车就后退
                backward()
    
    finally:
        clean()
        print("已释放 GPIO 口")
    
    7 条回复    2020-04-23 14:09:20 +08:00
    mylovesaber
        1
    mylovesaber  
    OP
       2020-04-19 22:01:53 +08:00
    问题来源于子循环里面 screen = curses.initscr()和 char = screen.getch(),在同样“小车前进时没有障碍物”的条件下出现:
    1. 这俩行注释了,小车在按下 ok 键无反映,只能等快撞上由红外强行停止
    2. 这俩行不注释,小车可以通过 ok 键停止,但红外防撞功能失效

    另外发现这个 getch()好像会累计你的按键,比如我使用问题 1,我按上,ok,下键,没反映,但靠近红外模块后小车立马停车后紧接着后退

    不知道我这逻辑写法哪里出错了,各位前辈如果能看懂的话可否介绍下经验呢?感谢!
    wangkai0351
        2
    wangkai0351  
       2020-04-19 23:11:09 +08:00
    软硬件联调的活儿是吃经验且累人的,留给楼下大佬来帮帮孩子吧
    mylovesaber
        3
    mylovesaber  
    OP
       2020-04-19 23:46:47 +08:00
    @wangkai0351 谢谢,其实键值不是问题,都是测试能用的,而且这个 curses 库识别键值如果是英文字母直接就是它本身,根本不用考虑,只是这个在子循环加上读取键值就红外失效,不加上按键失效就有点尴尬。。。
    no1xsyzy
        4
    no1xsyzy  
       2020-04-20 12:40:28 +08:00
    首先,两重死循环必定是程序写错了。
    请先画 FSM 。
    mylovesaber
        5
    mylovesaber  
    OP
       2020-04-22 00:38:06 +08:00
    @no1xsyzy @wangkai0351
    ![避障逻辑图]( https://s1.ax1x.com/2020/04/22/JYEuKU.png)
    我重读了 curses 的文档,是我疏忽了 getch()函数的特性,
    即: `char = screen.getch()` 这行代码功能是等待读取键值

    进入这行代码的执行的时候,读取到按键之前一定会出现整个程序的暂停,所以出现一旦我取消注释,一楼程序就进入等待按键的阶段,后面的红外测距判断就不会被执行的情况,但现在发现问题需要解决的逻辑我好像没想出来。。

    再说明白点就是这两行代码是先后绑定在一起的,避障生效只能是在运行这两行代码之前,但避障使用的这个红外一直在读取到信息,也就是不停读取非 0 即 1,直接进行 if-else 进行判断的话,一旦小车动起来第一时间树莓派没有读取到 0 的值时就进入了等待输入 ok 键的阶段导致这个避障功能不生效,所以我第一反应是使用一个循环,但后面怎么改善我蒙逼了:
    ```python
    char = screen.getch()
    print("这段话绝对不会被打印在屏幕上")
    if char == 10:
    print("读取到 ok 键被按下这段话就会被输出到屏幕上")
    ```
    请教下二位有没有什么可能的思路来解决呢?
    no1xsyzy
        6
    no1xsyzy  
       2020-04-22 01:17:13 +08:00
    @mylovesaber #5 非正交多输入全部进 FSM 最方便。
    FSM 不是指流程图(控制流图),而是 “有限状态机”,大致上可以实现为 Moore Machine 和 Mealy Machine,特点就是根据当前状态和输入决定下一状态(状态转移),而状态或者状态转移决定输出。

    你同时需要等待停止后退等按钮输入,又需要等待红外的输入,你觉得阻塞方式可能做到吗?
    mylovesaber
        7
    mylovesaber  
    OP
       2020-04-23 14:09:20 +08:00
    文档中有个 window.timeout(delay)函数,能以非阻塞方式轮询

    import time
    import curses

    screen = curses.initscr()
    curses.noecho()
    screen.timeout(0) # 设置 screen 为非阻塞读取(无延迟模式)

    while True:
    char = screen.getch() # 在无延迟模式下,getch 会立即返回不管有没有输入
    if char == -1: # 如果没有输入,getch 会返回 -1
    print("没有输入")
    else:
    print("输入了 {} ({})".format(chr(char), char))
    time.sleep(0.2) # sleep 避免轮询过快


    我滴妈,我的问题终于解决了
    网上其他所谓键盘遥控自动避障小车项目好像都没有用到这个函数所以都不完整
    此贴终结
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2862 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 22ms · UTC 09:12 · PVG 17:12 · LAX 01:12 · JFK 04:12
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.