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へデバイスを接続する。
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 バグフィックスと図象訂正