Raspberry Pi Cmd Launcher

Raspberry Pi Cmd Launcher

Raspberry Piにキャラクタ液晶とプッシュスイッチを組み合わせてランチャーを製作する。
スイッチは左ボタンと右ボタンとエンターボタンの3つとする。液晶に表示されたメニューから左右ボタンで希望のコマンドを選択しエンターボタンで実行する。ユーザフィードバックはピエゾ素子で音を発生。

Hardware

液晶はi2c接続の2行8桁のものを使用する。秋月電子で販売中のバスリピーター付きのLCD(AQM0802A)
スイッチは小さなプッシュ型でエンターボタンは少し大きめのものを選んだ。家庭内在庫品だ。

graph LR
    subgraph Raspberry Pi
        i2c
        pwm
        gpio[GPIO]
    end
    lcd[LCD]
    sounder[Piezo Sounder]
    swl[Left SW]
    swe[Enter SW]
    swr[Right SW]
    i2c --- lcd
    pwm --- sounder
    gpio --- swl
    gpio --- swe
    gpio --- swr

Schematic

i2c端子とGPIOへデバイスを接続する。
Schematic

Software

ソフトウェアはRaspberry Pi OSと Pythonの組み合わせで開発する。gpioドライバはpigpioを使用する。LCDドライバはGPLのコードを入手し半角カナを表示できるようコードを追加した。

動作は下図をイメージしているがピエゾドライバとスイッチドライバはlauncher.pyに内蔵した。

sequenceDiagram
    Shell ->> Launcher: Start up
    Launcher ->> LCD driver: Display menu
    sw driver ->> Launcher: Request Operation
    Launcher ->> Piezo driver: Request Sound
    Launcher ->> Shell: Command

Code

LCD Driver

LCDドライバはみなさん作成されているのでその中から
https://gist.github.com/DenisFromHR/cc863375a6e19dce359d
を利用させていただき、半角カナを表示する処理を追加した。

i2clcd.py

#!/usr/bin/python3
# -*- coding: utf-8 -*-

"""
Changed by yamktm.
This is based on Denis Pleic's LCD library.
https://gist.github.com/DenisFromHR/cc863375a6e19dce359d
Made available under GNU GENERAL PUBLIC LICENSE
"""

import pigpio
import time

# backlight
LCD_BACKLIGHTPORT = 4   # BCM No.
LCD_BACKLIGHTON = 1
LCD_BACKLIGHTOFF = 0

# commands
LCD_CMD = 0x00
LCD_DATA = 0x40
LCD_CLEARDISPLAY = 0x01
LCD_RETURNHOME = 0x02
LCD_ENTRYMODESET = 0x04
LCD_DISPLAYCONTROL = 0x08
LCD_CURSORSHIFT = 0x10
LCD_FUNCTIONSET = 0x20
LCD_SETCGRAMADDR = 0x40
LCD_SETDDRAMADDR = 0x80
LCD_IS0 = 0x00
LCD_IS1 = 0x01

# flags for display entry mode
LCD_ENTRYRIGHT = 0x00
LCD_ENTRYLEFT = 0x02
LCD_ENTRYSHIFTINCREMENT = 0x01
LCD_ENTRYSHIFTDECREMENT = 0x00

# flags for display on/off control
LCD_DISPLAYON = 0x04
LCD_DISPLAYOFF = 0x00
LCD_CURSORON = 0x02
LCD_CURSOROFF = 0x00
LCD_BLINKON = 0x01
LCD_BLINKOFF = 0x00

# flags for display/cursor shift
LCD_DISPLAYMOVE = 0x08
LCD_CURSORMOVE = 0x00
LCD_MOVERIGHT = 0x04
LCD_MOVELEFT = 0x00

# flags for function set
LCD_8BITMODE = 0x10
LCD_4BITMODE = 0x00
LCD_2LINE = 0x08
LCD_1LINE = 0x00
LCD_5x10DOTS = 0x04
LCD_5x8DOTS = 0x00

# instruction table 0
LCD_SHIFT = 0x10
LCD_SCROLLON = 0x08
LCD_SCROLLOFF = 0x00
LCD_RIGHT = 0x04
LCD_LEFT = 0x00
LCD_SETCGRAM = 0x40

# instruction table 1
LCD_INTOSC = 0x10
LCD_BIASDIV4 = 0x08
LCD_BIASDIV5 = 0x00
LCD_CONTRAST = 0x70
LCD_PWRCTRL = 0x50
LCD_ICONON = 0x08
LCD_ICONOFF = 0x00
LCD_BOOSTON = 0x04
LCD_BOOSTOFF = 0x00
LCD_FOLWCTRL = 0x60
LCD_FOLWON = 0x08

# lcd address
LCD_DDRAMADDRESS = [0x00, 0x40, 0x14, 0x54]

Co = 0b10000000 # Control bit
Rs = 0b01000000 # Register select bit
Rw = 0b00000001 # Read/Write bit

class Lcd:
    #initializes objects and lcd
    def __init__(self, port, address, backlight=LCD_BACKLIGHTOFF, width=8, height=2):
        self.port = port
        self.address = address
        self.width = width
        self.height = height
        self.pi = pigpio.pi()
        self.bus = self.pi.i2c_open(port, address, 0)
        self.pi.set_mode(LCD_BACKLIGHTPORT, pigpio.OUTPUT)  # ready for backlight
        self.backlight(backlight) # turn on backlight

        # initialize
        self.func_cmd = LCD_FUNCTIONSET | LCD_8BITMODE | LCD_2LINE | LCD_5x8DOTS 
        self.write_cmd(self.func_cmd | LCD_IS0)   # 0x38
        self.write_cmd(self.func_cmd | LCD_IS1)   # 0x39
        self.write_cmd(LCD_INTOSC | LCD_BIASDIV5 | 4) # 0x14
        self.write_cmd(LCD_CONTRAST | 0) # 0x70 contrast low 4bit
        self.write_cmd(LCD_PWRCTRL | LCD_ICONOFF | LCD_BOOSTON | 2) # 0x56 contrast hi 2bit
        self.write_cmd(LCD_FOLWCTRL | LCD_FOLWON | 4) # 0x6c amp 3bit
        time.sleep(0.2)     # [s]
        self.write_cmd(LCD_FUNCTIONSET | LCD_8BITMODE | LCD_2LINE | LCD_5x8DOTS | LCD_IS0) # 0x38
        self.write_cmd(LCD_DISPLAYCONTROL | LCD_DISPLAYON | LCD_CURSOROFF | LCD_BLINKOFF)   # 0x0c
        self.write_cmd(LCD_CLEARDISPLAY) # 0x01
        time.sleep(0.001)    # [s]
        self.x = 0
        self.y = 0

    # write a command to lcd
    def write_cmd(self, data):
        self.pi.i2c_write_byte_data(self.bus, LCD_CMD, data)

    # write a character to lcd (or character rom)
    def write_chr(self, data):
        self.pi.i2c_write_byte_data(self.bus, LCD_DATA, data)

    # clear lcd and set to home
    def clear_screen(self):
        self.write_cmd(LCD_CLEARDISPLAY)
        time.sleep(0.001)
        self.write_cmd(LCD_RETURNHOME)
        time.sleep(0.001)
        self.x = 0
        self.y = 0

    # blink on/off
    def blink(self, sw):
        if sw:
            self.write_cmd(LCD_DISPLAYCONTROL | LCD_DISPLAYON | LCD_CURSOROFF | LCD_BLINKON)
        else:
            self.write_cmd(LCD_DISPLAYCONTROL | LCD_DISPLAYON | LCD_CURSOROFF | LCD_BLINKOFF)

    # define backlight on/off (lcd.backlight(1); off= lcd.backlight(0)
    def backlight(self, state): # for state, 1 = on, 0 = off
        self.pi.write(LCD_BACKLIGHTPORT, state)

    # add custom characters (0 - 7)
    def register_cgram(self, data):
        self.write_cmd(self.func_cmd | LCD_IS0)
        self.write_cmd(LCD_SETCGRAMADDR)
        for dots in data:
            for d in dots:
                self.write_chr(d)

    def limit_range(self, x, y):
        if x < 0:
            x = 0
        elif x > self.width - 1:
            x = self.width - 1
        if y < 0:
            y = 0
        elif y > self.height - 1:
            y = self.height - 1
        return x, y

    # define precise positioning (addition from the forum)
    def locate_and_print(self, x, y, string):
        self.locate(x, y)
        for c in string:
            self.write_chr(self.encode(c))
            self.shift_right(self.x, self.y)

    def print(self, string):
        for c in string:
            self.write_chr(self.encode(c))
            self.shift_right(self.x, self.y)

    def locate(self, x, y):
        x, y = self.limit_range(x, y)
        self.write_cmd(LCD_SETDDRAMADDR | (LCD_DDRAMADDRESS[y] + x))
        self.x = x
        self.y = y

    def shift_right(self, x, y):
        x = x + 1
        if x > self.width - 1:
            x = 0
            y = y + 1
            if y > self.height - 1:
                y = 0
            self.write_cmd(LCD_SETDDRAMADDR | (LCD_DDRAMADDRESS[y] + x))
        self.x = x
        self.y = y

    def encode(self, c):
        """ for special characters """
        d = ord(c)
        if d >= 0x0020 and d <= 0x007d:     # ascii
            r = d
        elif d >= 0xff61 and d <= 0xff9f:   # 半角カタカナ
            r = (d & 0xff) + (0xa1 - 0x61)
        else:
            r = 0
        return r

if __name__=='__main__':
    """ test """
    lcd = Lcd(1, 0x3e)  # port, address
    lcd.print('ナニカ?')
    lcd.locate_and_print(0, 1, 'What\'s?')

Launcher

辞書型でメニューの構成を定義しLCDに表示する。スイッチ割り込みでコールバックするイベント駆動型のメインルーチン。

launcher.py

#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
launcher
"""

import os, sys
import time
import subprocess
import pigpio
import i2clcd

MENU_DEF = [
    {
        'char': '0',
        'title': 'Command0',
        'command': "/home/pi/cmd0.py"
    },
    {
        'char': '1',
        'title': 'Beep   1',
        'command': "/home/pi/beeptest.py"
    },
    {
        'char': '2',
        'title': 'Sendmail',
        'command': "/home/pi/send001.py"
    }
]

E_SW_PORT: int = 26
L_SW_PORT: int = 23
R_SW_PORT: int = 24

FLAG_E_SW: int = 0b0001
FLAG_L_SW: int = 0b0010
FLAG_R_SW: int = 0b0100

BEEP_PORT: int = 13
BEEP_DUTY: int = 500000

pi = pigpio.pi()
lcd = i2clcd.Lcd(1, 0x3e, 1)    # port, address, backlight

# Enter sw
pi.set_mode(E_SW_PORT, pigpio.INPUT)
pi.set_pull_up_down(E_SW_PORT, pigpio.PUD_UP)
# L sw
pi.set_mode(L_SW_PORT, pigpio.INPUT)
pi.set_pull_up_down(L_SW_PORT, pigpio.PUD_UP)
# R sw
pi.set_mode(R_SW_PORT, pigpio.INPUT)
pi.set_pull_up_down(R_SW_PORT, pigpio.PUD_UP)

class Launcher:
    def __init__(self, menu, idx=0, backlight=1):
        self.menu = menu
        self.id = idx
        self.n_id = len(self.menu)
        self.backlight = backlight
        self.flag_int = 0
        self.int_procedure = self.operate_menu
        self.callback_e = pi.callback(E_SW_PORT, pigpio.FALLING_EDGE, self.int_E_sw)
        self.callback_l = pi.callback(L_SW_PORT, pigpio.FALLING_EDGE, self.int_L_sw)
        self.callback_r = pi.callback(R_SW_PORT, pigpio.FALLING_EDGE, self.int_R_sw)
        lcd.clear_screen()
        lcd.backlight(self.backlight)
        self.display_menu(self.menu, self.id)

    def int_E_sw(self, gpio, level, tick):     # enter
        if pi.read(E_SW_PORT) == 0:
            self.beep(0)
            self.flag_int = FLAG_E_SW

    def int_L_sw(self, gpio, level, tick):     # left
        if pi.read(L_SW_PORT) == 0:
            self.beep(1)
            self.flag_int = FLAG_L_SW

    def int_R_sw(self, gpio, level, tick):     # rgiht
        if pi.read(R_SW_PORT) == 0:
            self.beep(1)
            self.flag_int = FLAG_R_SW

    def display_menu(self, menu, id):
        if id < 3:
            prestr = ' ' * (3 - id)
            j = 0
        else:
            prestr = ''
            j = id - 3
        k = id + 4
        if k > self.n_id - 1:
            k = self.n_id - 1
        if id < self.n_id and id > self.n_id - 5:
            poststr = ' ' * (5 - id + self.n_id)
        else:
            poststr = ''
        bar = ''
        for i in range(j, k + 1):
            bar = bar + menu[i]['char']
        bar = prestr + bar + poststr
        lcd.blink(False)
        lcd.locate(0, 0)
        lcd.print(bar)
        title = menu[id]['title']
        title = title + ' ' * (8 - len(title))
        lcd.locate(0, 1)
        lcd.print(title)
        lcd.locate(3, 0)
        lcd.blink(True)

    def register_int_proc(self, proc):
        self.int_procedure = proc

    def operate_menu(self):
        if self.flag_int == FLAG_E_SW:  # Enter switch
            self.callback_e.cancel()
            self.callback_l.cancel()
            self.callback_r.cancel()
            lcd.backlight(0)
            lcd.blink(False)
            lcd.clear_screen()
            subprocess.run(self.menu[self.id]['command'].split())
            lcd.clear_screen()
            lcd.backlight(self.backlight)
            self.display_menu(self.menu, self.id)
            self.callback_e = pi.callback(E_SW_PORT, pigpio.FALLING_EDGE, self.int_E_sw)
            self.callback_l = pi.callback(L_SW_PORT, pigpio.FALLING_EDGE, self.int_L_sw)
            self.callback_r = pi.callback(R_SW_PORT, pigpio.FALLING_EDGE, self.int_R_sw)
            time.sleep(0.5)
        elif self.flag_int == FLAG_L_SW:    # shift Left switch
            self.id = self.id - 1
            if self.id < 0:
                self.id = self.n_id - 1
            self.display_menu(self.menu, self.id)
        elif self.flag_int == FLAG_R_SW:    # shift Right switch
            self.id = self.id + 1
            if self.id >= self.n_id:
                self.id = 0
            self.display_menu(self.menu, self.id)

    def run(self):
        while True:
            time.sleep(0.01) # [s]
            if self.flag_int != 0:
                self.operate_menu()
                self.register_int_proc(self.operate_menu)
                self.flag_int = 0

    def beep(self, pattern_id=0):
        BEEP_PATTERN = [
            {
                'tone': [
                    {
                        'freq': 2000,
                        'time': 0.2
                    },
                    {
                        'freq': 1000,
                        'time': 0.2
                    },
                ]
            },
            {
                'tone': [
                    {
                        'freq': 2500,
                        'time': 0.1
                    },
                ]
            },
        ]
        tones = BEEP_PATTERN[pattern_id]['tone']
        for tone in tones:
            pi.hardware_PWM(BEEP_PORT, tone['freq'], BEEP_DUTY)  # [Hz], [ppm]
            time.sleep(tone['time'])
        pi.hardware_PWM(BEEP_PORT, 0, 0)   # [Hz], [ppm]

if __name__=='__main__':
    """ test """
    laun = Launcher(MENU_DEF, 0)    # menu_info, initial_id
    laun.run()  # main loop
    pi.hardware_PWM(BEEP_PORT, 0, 0)   # [Hz], [ppm]
    pi.stop()

動作例

Raspberry Pi zero へ実装した動作例。
https://youtu.be/9o-HrrXlSLA

(つづく)

2021/10/14 バグフィックスと図象訂正

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です