LINEDC Advent Calendar 2024 20日目です!
この記事は初心者・初登壇Welcome!LINEを使ったLT大会 #10でLTさせていただいた内容の記事です。
平成レトロ1とは
80年代後半から2000年代初頭にかけての平成初期の文化に光を当て、当時のアニメ、ドラマ、音楽、ファッションなどを再評価する潮流です。
へぇボタンとは
2002年10月8日から2006年9月27日までレギュラー放送(意外と短い)されていた「トリビアの泉」という番組に登場するボタンです。
以下Wikipediaからの引用です。
紹介された「トリビア」に対し、「品評会会長」のタモリを含む5人のパネリストからなる「トリビア品評会」が「トリビア」の驚き、意外性、また「確認VTR」の面白さなどを感銘度とし「へぇボタン」と称する丸形の青いボタンを押して評価する。このボタンを押すと「へぇ」という女性の声が流れる(この声の主は、初回の収録に参加していた女性カメラアシスタントである)。
1人につき「20へぇ」が与えられ、その合計値で「トリビア」の優劣をつける。「20へぇ」の評価で「満へぇ」となり、5人全員が「満へぇ」、つまり合計「100へぇ」で満点となる。
約20年のときを経て「へぇボタン」をGET
「へぇボタン」を番組グッズとして販売していないか調べたところ、商品ページを見つけました。
ただ、2003年11月中旬発売となっており、現在は当然ながら販売終了していましたが、メルカリで入手することができました。(ありがとう出品者様、ありがとうメルカリ様)
私の友人(ただしくん)で昭和レトロな技術を突き詰めている方がいるので、私は 「へぇボタン」 をハックして平成レトロで対抗しようと思いました。
機能検討
どんな改造するか考えていましたが、こんなニュースが入ってきました。
ということで、提供終了前にLINE Notifyを使おうと思い、へぇボタンをトリガにLINEへ通知を送る機能を追加してみようと思いました。
無線モジュールが搭載されたマイコンボードはESP32の開発ボード、Arduino、Raspberry Piなどを考えましたが、コスト、情報量、サイズからRaspberry Pi Pico Wを選定しました。
今回は外部からへぇボタンを制御するのではなく、マイコンボードも筐体内で収めたいと考えていたため特にサイズは重要な要素でした。
開発環境
- マイコンボード
- Raspberry Pi Pico W
- 開発言語
- MicroPython
- IDE
- VS Code (拡張機能のMicroPico)
改造箇所
へぇボタンの中身は空中配線が多く、テスター片手に回路を追うのが面倒そうでした。
簡単な回路図を起こしてくれている方がいたので参考にさせていただきました。
①へぇボタンの押下をラズパイに検知させる回路
何が一番面倒かというと、へぇボタンの電源電圧(単三電池×3直列=4.5V) と ラズパイの電源電圧(3.3V) が異なるため直接ラズパイに引き込むとラズパイが故障する恐れがある点です。
そのため、ラズパイに直接入力するのではなく、抵抗分圧を使った回路を組みました。
回路について
以下の式より抵抗値を決めています。
3.3\ \text{V} = 4.5\ \text{V} \times \frac{1\ \text{M}\Omega}{(x + 1)\ \text{M}\Omega}
x = \frac{(4.5-3.3)\ }{3.3\ } \fallingdotseq 0.36\ \text{M}\Omega = 360\ \text{k}\Omega
(電池電圧の変動、前段の100Ω、スイッチの接触抵抗は無視しています)
②BLE通信でラズパイからボタン押下させる回路
Raspberry Pi Pico Wの無線モジュール(CY43439)はWi-Fiだけでなく、BLE(Bluetooth Low Energy)にも対応しているため、せっかくであればBLE通信を使って遠隔でへぇボタンを押せるような仕組みを作りたいと思いました。
そこで上記①に追加して、BLE通信でスイッチのON/OFF制御ができるようにトランジスタを使った回路を組みました。
トランジスタは様々な役割がありますがこの回路の場合、電気的にスイッチを押してくれる部品くらいのイメージでよいかと思います。
回路について
GND側に大きな負荷抵抗があるためPNPトランジスタを使用する必要があります。
ただPNPトランジスタを使用した場合、ベースの方が電圧が低いためHi出力しても電流が逆流してしまいます。
そこで、PNPトランジスタとNPNトランジスタを組み合わせてラズパイを保護しながらスイッチングができるようにしました。
(手元にあったテキトーなDTA114YEとDTC114YEを使いました。)
③カウンターをラズパイからリセットさせる回路
従来はリセットボタンを押すことで「へぇ」のカウントをリセットしていましたが、LINEへ通知を送ったあと自動的にカウンターをリセットさせる回路を組みました。
回路について
上記②同様トランジスタを追加します。ちなみにボタンを押した際のメインマイコンへの入力の論理が上記②とは逆になります。
状態 | ②回路 | ③回路 |
---|---|---|
常時 | 0V (Lo) | 4.5V (Hi) |
ボタン押下時 | 4.5V (Hi) | 0V (Lo) |
④ラズパイを電池で動かす回路
電池をラズパイのVSYS端子につなげる。以上。
回路について
一応へぇボタンにヒューズが入っていたのでヒューズを通ったあとの電源ラインをラズパイのVSYS端子に接続しました。
データシートをみて初めて知りましたが、ラズパイPicoの3.3Vを作るDCDCコンバータは昇降圧型なのですね。便利。
VSYS is the main system input voltage, which can vary in the allowed range 1.8V to 5.5V, and is used by the on-board
SMPS to generate the 3.3V for the RP2040 and its GPIO
Raspberry Pi Pico Wへの接続
- 1番ピン(GPIO0)を上記①360kΩと1MΩの間に接続
- 2番ピン(GPIO1)を上記②トランジスタのベースに接続
- 4番ピン(GPIO2)を上記③トランジスタのベースに接続
- 38番ピン(GND)を上記④電池のGND側に接続
- 39番ピン(VSYS)を上記④電池のプラス側に接続
Raspberry Pi Pico Wのソースコード
前述の通り、2025年3月31日以降はLINE Notifyは利用できないため、それ以降はLINE Messaging APIへの切り替えなどが必要になります。
main.py
from machine import Pin # ピンを操作するためのモジュールをインポートします
import network # ネットワーク(Wi-Fi)を扱うためのモジュールをインポートします
import time # 時間に関するモジュールをインポートします
import urequests # HTTPリクエストを送るためのモジュールをインポートします
import bluetooth # Bluetoothを扱うためのモジュールをインポートします
from ble_simple_peripheral import BLESimplePeripheral # 簡単にBLEを扱うためのクラスをインポートします
import sys # 標準入力出力を扱うためのモジュールをインポートします
# Wi-Fiの設定を行います
ssid_password_list = [ # 修正: 複数のSSIDとパスワードをリストで定義
('YOUR_SSID1', 'YOUR_PASSWORD1'),
('YOUR_SSID2', 'YOUR_PASSWORD2')
]
# Wi-Fiに接続します
wlan = network.WLAN(network.STA_IF) # ステーションモードでWi-Fiを利用します
wlan.active(True) # Wi-Fiを有効にします
# GPIO1とGPIO2を出力モードで初期化し、Loに設定
gpio1 = Pin(1, Pin.OUT)
gpio1.value(0)
gpio2 = Pin(2, Pin.OUT)
gpio2.value(0)
for ssid, password in ssid_password_list: # 修正: 複数のSSIDを順に試行
wlan.connect(ssid, password)
time.sleep(5) # 接続を待つ時間を調整
if wlan.isconnected():
break
# Wi-Fiに接続されるまで待機します
while not wlan.isconnected():
print('Connecting...') # 'Connecting...'と表示します
Pin("LED", Pin.OUT).high() # 内蔵LEDを点灯します
time.sleep(0.5) # 0.5秒待機します
Pin("LED", Pin.OUT).low() # 内蔵LEDを消灯します
time.sleep(0.5) # 0.5秒待機します
print('Connection successful!', wlan.ifconfig()) # 接続成功とネットワーク情報を表示します
# Wi-Fi接続後にGPIO1で1回ボタン押下をシミュレート
gpio1.value(1) # GPIO1をHiに設定
time.sleep(0.5) # 0.5秒待機
gpio1.value(0) # GPIO1をLoに戻す
# GPIO2をHiに設定してボタン押下をシミュレート
gpio2.value(1) # ピンの値を1に設定します
time.sleep(0.5) # 0.5秒待機します
gpio2.value(0) # ピンの値を0に戻します
# ピンの設定を行います
button_pin = 0 # ボタンが接続されているピンの番号です
button = Pin(button_pin, Pin.IN) # ボタン用のピンを入力モードで初期化します
# LINE Notifyの設定を行います
notify_url = "https://notify-api.line.me/api/notify" # LINE NotifyのエンドポイントURLです
line_token = "YOUR_LINE_NOTIFY_TOKEN" # LINE Notifyのトークンを設定します
# line_token = "YOUR_LINE_NOTIFY_TOKEN_DEBAG" # デバッグ用
# グローバル変数を初期化します
count = 0 # ボタンが押された回数を記録するカウンターです
last_press_time = 0 # 最後にボタンが押された時間を記録します
NOTIFY_DELAY = 3 # 通知を送るまでの遅延時間(秒)です
MAX_COUNT = 20 # カウンターの最大値です
rx_buffer = "" # 受信したデータを蓄積するバッファです
last_data_time = 0 # 最後にデータを受信した時間を記録します
WAIT_TIME = 500 # 受信完了まで待機する時間(ミリ秒)です
DEBOUNCE_MS = 500 # デバウンス時間(ミリ秒)です
# 特殊文字をエンコードする関数です
def quote(s):
res = ''
for c in s:
if ('A' <= c <= 'Z') or ('a' <= c <= 'z') or ('0' <= c <= '9') or c in ['_', '.', '-', '~']:
res += c # 安全な文字はそのまま追加します
else:
for b in c.encode('utf-8'):
res += '%%%02X' % b # 特殊文字はエンコードします
return res
# URLエンコードを行う関数です
def urlencode(params):
encoded = ""
for key, value in params.items():
if encoded:
encoded += "&"
encoded += "{}={}".format(quote(key), quote(value))
return encoded
# LINE Notifyで通知を送信する関数です
def send_notify(message):
headers = {
"Authorization": "Bearer {}".format(line_token), # 認証ヘッダーを設定します
"Content-Type": "application/x-www-form-urlencoded" # コンテンツタイプを設定します
}
payload = urlencode({'message': message}) # メッセージをURLエンコードします
try:
response = urequests.post(notify_url, headers=headers, data=payload) # POSTリクエストを送信します
print("Notify response status:", response.status_code) # ステータスコードを表示します
print("Notify response text:", response.text) # レスポンステキストを表示します
response.close() # レスポンスを閉じます
if response.status_code != 200:
print("Failed to send notification, check the token and endpoint.") # エラー時のメッセージを表示します
except Exception as e:
print("Failed to send notification:", e) # 例外発生時のメッセージを表示します
# ボタンが押されたときに呼ばれるハンドラー関数です
def button_handler(pin):
global count, last_press_time # グローバル変数を参照します
current_time = time.ticks_ms() # 現在の時間を取得します
# デバウンス処理:前回の押下から DEBOUNCE_MS ミリ秒以内の場合は無視
if time.ticks_diff(current_time, last_press_time) < DEBOUNCE_MS:
return
count += 1 # カウントを1増やします
last_press_time = current_time # 最後にボタンが押された時間を更新します
print("Button pressed, current count:", count) # ボタンが押されたことを表示します
Pin("LED", Pin.OUT).high() # LEDを点灯します
time.sleep(0.5) # 0.5秒待機します
Pin("LED", Pin.OUT).low() # LEDを消灯します
# カウントが最大値以上の場合
if count >= MAX_COUNT:
send_notify("{}へぇ".format(count)) # LINEに通知を送信します
reset_counter() # カウンターをリセットします
# カウンターをリセットする関数です
def reset_counter():
global count # グローバル変数を参照します
count = 0 # カウンターを0にリセットします
print("Counter reset") # リセットしたことを表示します
# GPIO2をHiに設定してボタン押下をシミュレート
gpio2.value(1) # ピンの値を1に設定します
Pin("LED", Pin.OUT).high() # LEDを点灯します
time.sleep(0.5) # 0.5秒待機します
gpio2.value(0) # ピンの値を0に戻します
Pin("LED", Pin.OUT).low() # LEDを消灯します
# ボタンの割り込みを設定します
button.irq(trigger=Pin.IRQ_RISING, handler=button_handler) # 修正: 割り込みトリガーをRISINGに変更
# BLEの設定を行います
ble = bluetooth.BLE() # BLEオブジェクトを作成します
ble.active(False) # BLEを一旦無効にします
time.sleep(1) # 1秒待機します
ble.active(True) # BLEを有効にします
sp = BLESimplePeripheral(ble) # 簡易BLEペリフェラルを作成します
# BLEでデータを受信したときに呼ばれる関数です
def on_rx(data):
global rx_buffer, last_data_time # グローバル変数を参照します
rx_buffer += data.decode('utf-8').strip() # 受信データをバッファに追加します
print("Received data so far:", rx_buffer) # 現在のバッファ内容を表示します
last_data_time = time.ticks_ms() # 最後にデータを受信した時間を更新します
# ボタン押下をシミュレートする関数です
def simulate_button_presses(count):
for i in range(count):
print(f"Simulating button press: {i + 1}") # シミュレーション中の回数を表示します
# GPIO1をHiに設定してボタン押下をシミュレート
gpio1.value(1) # GPIO1をHiに設定
Pin("LED", Pin.OUT).high() # LEDを点灯します
time.sleep(0.5) # 0.5秒待機します
gpio1.value(0) # GPIO1をLoに戻す
Pin("LED", Pin.OUT).low() # LEDを消灯します
time.sleep(0.1) # 0.1秒待機します
send_notify(f"{count}へぇ") # LINEに通知を送信します
reset_counter() # カウンターをリセットします
# データ受信時のコールバックを設定します
sp.on_write(on_rx) # データを受信したときにon_rxを呼び出します
# メインループです
while True:
current_time = time.ticks_ms() # 現在の時間を取得します
# 受信データがあり、最後の受信から一定時間経過した場合
if rx_buffer and time.ticks_diff(current_time, last_data_time) > WAIT_TIME:
if 1 <= len(rx_buffer) <= 2: # データ長が1桁または2桁の場合
try:
received_count = int(rx_buffer) # 受信データを整数に変換します
print("Received count via BLE:", received_count) # 受信したカウントを表示します
if 0 <= received_count <= 20:
simulate_button_presses(received_count) # ボタン押下をシミュレートします
else:
print(f"Received count ({received_count}) out of acceptable range (0-20). Ignoring.") # 範囲外の場合は無視します
except ValueError:
print("Invalid data received, ignoring.") # 数値に変換できない場合は無視します
else:
print(f"Received data length ({len(rx_buffer)}) is not 2 digits. Ignoring.") # データ長が2以外の場合は無視します
rx_buffer = "" # バッファをリセットします
# 一定時間ボタンが押されておらず、カウントが0より大きい場合
if (time.ticks_diff(current_time, last_press_time) >= NOTIFY_DELAY * 1000):
if count > 0:
send_notify("{}へぇ".format(count)) # LINEに通知を送信します
reset_counter() # カウンターをリセットします
time.sleep(0.1) # 0.1秒待機します
以下の2つのファイルは、BLE通信するためにラズパイへ書き込む必要があります。
以下のGitHubページのソースコードをそのまま利用しています。
ble_advertising.py
# Helpers for generating BLE advertising payloads.
# A more fully-featured (and easier to use) version of this is implemented in
# aioble. This code is provided just as a basic example. See
# https://github.com/micropython/micropython-lib/tree/master/micropython/bluetooth/aioble
from micropython import const
import struct
import bluetooth
# Advertising payloads are repeated packets of the following form:
# 1 byte data length (N + 1)
# 1 byte type (see constants below)
# N bytes type-specific data
_ADV_TYPE_FLAGS = const(0x01)
_ADV_TYPE_NAME = const(0x09)
_ADV_TYPE_UUID16_COMPLETE = const(0x3)
_ADV_TYPE_UUID32_COMPLETE = const(0x5)
_ADV_TYPE_UUID128_COMPLETE = const(0x7)
_ADV_TYPE_UUID16_MORE = const(0x2)
_ADV_TYPE_UUID32_MORE = const(0x4)
_ADV_TYPE_UUID128_MORE = const(0x6)
_ADV_TYPE_APPEARANCE = const(0x19)
_ADV_MAX_PAYLOAD = const(31)
# Generate a payload to be passed to gap_advertise(adv_data=...).
def advertising_payload(limited_disc=False, br_edr=False, name=None, services=None, appearance=0):
payload = bytearray()
def _append(adv_type, value):
nonlocal payload
payload += struct.pack("BB", len(value) + 1, adv_type) + value
_append(
_ADV_TYPE_FLAGS,
struct.pack("B", (0x01 if limited_disc else 0x02) + (0x18 if br_edr else 0x04)),
)
if name:
_append(_ADV_TYPE_NAME, name)
if services:
for uuid in services:
b = bytes(uuid)
if len(b) == 2:
_append(_ADV_TYPE_UUID16_COMPLETE, b)
elif len(b) == 4:
_append(_ADV_TYPE_UUID32_COMPLETE, b)
elif len(b) == 16:
_append(_ADV_TYPE_UUID128_COMPLETE, b)
# See org.bluetooth.characteristic.gap.appearance.xml
if appearance:
_append(_ADV_TYPE_APPEARANCE, struct.pack("<h", appearance))
if len(payload) > _ADV_MAX_PAYLOAD:
raise ValueError("advertising payload too large")
return payload
def decode_field(payload, adv_type):
i = 0
result = []
while i + 1 < len(payload):
if payload[i + 1] == adv_type:
result.append(payload[i + 2 : i + payload[i] + 1])
i += 1 + payload[i]
return result
def decode_name(payload):
n = decode_field(payload, _ADV_TYPE_NAME)
return str(n[0], "utf-8") if n else ""
def decode_services(payload):
services = []
for u in decode_field(payload, _ADV_TYPE_UUID16_COMPLETE):
services.append(bluetooth.UUID(struct.unpack("<h", u)[0]))
for u in decode_field(payload, _ADV_TYPE_UUID32_COMPLETE):
services.append(bluetooth.UUID(struct.unpack("<d", u)[0]))
for u in decode_field(payload, _ADV_TYPE_UUID128_COMPLETE):
services.append(bluetooth.UUID(u))
return services
def demo():
payload = advertising_payload(
name="micropython",
services=[bluetooth.UUID(0x181A), bluetooth.UUID("6E400001-B5A3-F393-E0A9-E50E24DCCA9E")],
)
print(payload)
print(decode_name(payload))
print(decode_services(payload))
if __name__ == "__main__":
demo()
ble_simple_peripheral.py
# This example demonstrates a UART periperhal.
# This example demonstrates the low-level bluetooth module. For most
# applications, we recommend using the higher-level aioble library which takes
# care of all IRQ handling and connection management. See
# https://github.com/micropython/micropython-lib/tree/master/micropython/bluetooth/aioble
import bluetooth
import random
import struct
import time
from ble_advertising import advertising_payload
from micropython import const
_IRQ_CENTRAL_CONNECT = const(1)
_IRQ_CENTRAL_DISCONNECT = const(2)
_IRQ_GATTS_WRITE = const(3)
_FLAG_READ = const(0x0002)
_FLAG_WRITE_NO_RESPONSE = const(0x0004)
_FLAG_WRITE = const(0x0008)
_FLAG_NOTIFY = const(0x0010)
_UART_UUID = bluetooth.UUID("6E400001-B5A3-F393-E0A9-E50E24DCCA9E")
_UART_TX = (
bluetooth.UUID("6E400003-B5A3-F393-E0A9-E50E24DCCA9E"),
_FLAG_READ | _FLAG_NOTIFY,
)
_UART_RX = (
bluetooth.UUID("6E400002-B5A3-F393-E0A9-E50E24DCCA9E"),
_FLAG_WRITE | _FLAG_WRITE_NO_RESPONSE,
)
_UART_SERVICE = (
_UART_UUID,
(_UART_TX, _UART_RX),
)
class BLESimplePeripheral:
def __init__(self, ble, name="mpy-uart"):
self._ble = ble
self._ble.active(True)
self._ble.irq(self._irq)
((self._handle_tx, self._handle_rx),) = self._ble.gatts_register_services((_UART_SERVICE,))
self._connections = set()
self._write_callback = None
self._payload = advertising_payload(name=name, services=[_UART_UUID])
self._advertise()
def _irq(self, event, data):
# Track connections so we can send notifications.
if event == _IRQ_CENTRAL_CONNECT:
conn_handle, _, _ = data
print("New connection", conn_handle)
self._connections.add(conn_handle)
elif event == _IRQ_CENTRAL_DISCONNECT:
conn_handle, _, _ = data
print("Disconnected", conn_handle)
self._connections.remove(conn_handle)
# Start advertising again to allow a new connection.
self._advertise()
elif event == _IRQ_GATTS_WRITE:
conn_handle, value_handle = data
value = self._ble.gatts_read(value_handle)
if value_handle == self._handle_rx and self._write_callback:
self._write_callback(value)
def send(self, data):
for conn_handle in self._connections:
self._ble.gatts_notify(conn_handle, self._handle_tx, data)
def is_connected(self):
return len(self._connections) > 0
def _advertise(self, interval_us=500000):
print("Starting advertising")
self._ble.gap_advertise(interval_us, adv_data=self._payload)
def on_write(self, callback):
self._write_callback = callback
def demo():
ble = bluetooth.BLE()
p = BLESimplePeripheral(ble)
def on_rx(v):
print("RX", v)
p.on_write(on_rx)
i = 0
while True:
if p.is_connected():
# Short burst of queued notifications.
for _ in range(3):
data = str(i) + "_"
print("TX", data)
p.send(data)
i += 1
time.sleep_ms(100)
if __name__ == "__main__":
demo()
BLEターミナル
BLEターミナルは以下の記事を参考にBluetooth Terminalというアプリを利用しました。
裏話
実は当初は既成のアプリを利用するのではなく、自作のWebアプリを作るつもりでした。
Web Bluetooth APIを使ってbolt.newにWebアプリを作ってもらいました。
PCで動作確認を終え、いざスマホ(iPhone)で実運用をしてみると動きませんでした。。。
Chromeは対応しているものと思っていましたが、対応しているのは"Chrome Android"だったというオチです。
改造後のへぇボタン
ケースを開けた写真です。細い銅線が張り巡らされており、改造していくなかで何度も千切れてはつけ直しを繰り返しました。
拡大した基板です。分かりづらいですが、トランジスタを2個実装しています。
音量調整
また、私のモノづくりタイムは早朝なのですが、デバッグでへぇボタンを押すと近所迷惑なのでスピーカーに可変抵抗を取り付けました。(ちなみツマミはこれ)
↓ はんだづけで可変抵抗が断線したため調査していましたが、おそらくこれっぽいです。
可変抵抗をつけた箇所です。左の不自然な青丸は穴開けを失敗して青いホットボンドで埋めた箇所です。
デモ
あとがき
6月~10月の4か月間プロトタイピングを学んでいました。
自分の"ハードウェア"の知識と学んだ"ソフトウェア"の知識を組み合わせて、 モノづくりができるエンジニア になることを目標にしていましたが、1歩目が踏み出せたと思っています。
今回は既製品を改造して制作を行いましたが、0から作るモノづくりにも挑戦していきたいと思います。
LINEDC Advent Calendar 2024 また明日以降もお楽しみに!
-
株式会社トランス.「平成レトロとは?今までのレトロブーム・Y2Kとの違い」.平成レトロがアツい理由とは?今流行る理由と当時との違い、Y2Kトレンドも解説.2024-06-14,https://www.trans.co.jp/column/trend/heisei_retro/, (参照 2024-10-24). ↩