출처
- Bluetooth Commands – Raspberry Pi Projects
- Raspberry Pi BLE: Read/Write Sensor Network Communication
- pip install error: externally-managed-environment 해결 방법
List Bluetooth Adaptors
pi@raspberrypi:~$ hciconfig
hci0: Type: Primary Bus: UART
BD Address: D8:3A:DD:BA:79:78 ACL MTU: 1021:8 SCO MTU: 64:1
UP RUNNING
RX bytes:3771 acl:0 sco:0 events:398 errors:0
TX bytes:68389 acl:0 sco:0 commands:398 errors:
Scan for Bluetooth devices
pi@raspberrypi:~$ hcitool scan
Scanning ...
C4:47:4E:BE:57:FC n/a
F8:63:3F:0B:D3:3E LAPTOP-Q2A61D9E
Scan for BLE devices
pi@raspberrypi:~$ sudo hcitool lescan | grep BBL_SHUTTER
20:6E:F1:47:9D:76 BBL_SHUTTER
20:6E:F1:47:9D:76 BBL_SHUTTER
gatttool을 사용하여 장치에 연결
pi@raspberrypi:~$ sudo gatttool -b 20:6E:F1:47:9D:76 -t random --interactive
[20:6E:F1:47:9D:76][LE]>
라즈베리파이 환경 준비
pi@raspberrypi:~$ sudo apt update
pi@raspberrypi:~$ sudo apt install bluetooth bluez python3-bluez python3-dbus
실행 오류
pi@raspberrypi:~$ python3 RPi_BLE_Shutter_Client.py
Traceback (most recent call last):
File "/home/pi/RPi_BLE_Shutter_Client.py", line 2, in
from bleak import BleakClient, BleakScanner
ModuleNotFoundError: No module named 'bleak'
bleak 설치 오류
pi@raspberrypi:~$ sudo pip3 install bleak
error: externally-managed-environment
× This environment is externally managed
╰─> To install Python packages system-wide, try apt install
python3-xyz, where xyz is the package you are trying to
install.
If you wish to install a non-Debian-packaged Python package,
create a virtual environment using python3 -m venv path/to/venv.
Then use path/to/venv/bin/python and path/to/venv/bin/pip. Make
sure you have python3-full installed.
For more information visit http://rptl.io/venv
note: If you believe this is a mistake, please contact your Python installation or OS distribution provider. You can override this, at the risk of breaking your Python installation or OS, by passing --break-system-packages.
hint: See PEP 668 for the detailed specification.
pip에 설치 권한 부여
pi@raspberrypi:~$ python -m pip config set global.break-system-packages true
Writing to /home/pi/.config/pip/pip.conf
bleak 설치
pi@raspberrypi:~$ pip3 install bleak
Collecting bleak
Downloading bleak-1.1.1-py3-none-any.whl.metadata (5.1 kB)
Collecting dbus-fast>=1.83.0 (from bleak)
Downloading dbus_fast-2.44.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl.metadata (10 kB)
Downloading bleak-1.1.1-py3-none-any.whl (136 kB)
Downloading dbus_fast-2.44.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl (868 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 868.4/868.4 kB 2.7 MB/s eta 0:00:00
Installing collected packages: dbus-fast, bleak
Successfully installed bleak-1.1.1 dbus-fast-2.44.5
WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager, possibly rendering your system unusable. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv. Use the --root-user-action option if you know what you are doing and want to suppress this warning.
RPi_BLE_Shutter_Client.py (최종적으로 실패)
import asyncio
from bleak import BleakClient, BleakScanner
from bleak.backends.device import BLEDevice
from bleak.backends.scanner import AdvertisementData
# BLE HID 서비스 및 특성 UUID
# HID Service: 00001812-0000-1000-8000-00805f9b34fb
HID_SERVICE_UUID = "00001812-0000-1000-8000-00805f9b34fb"
# Report Characteristic: 00002a4d-0000-1000-8000-00805f9b34fb (일반적인 HID Report UUID)
REPORT_CHAR_UUID = "00002a4d-0000-1000-8000-00805f9b34fb"
# 찾고자 하는 셔터 장치의 이름 (예시: 실제 장치 이름으로 변경 필요)
TARGET_DEVICE_NAME = "BBL_SHUTTER"
def hid_report_handler(characteristic: int, data: bytearray):
"""
셔터 장치로부터 HID Report를 수신할 때 호출되는 콜백 함수.
셔터 이벤트(볼륨 업/다운)는 보통 6~8바이트 길이의 HID Report로 전송됩니다.
볼륨 업(셔터) 키는 보통 Report의 두 번째 바이트에 특정 값으로 표시됩니다.
"""
print(f"[{characteristic}]: Received HID Report: {data.hex()}")
# 셔터 버튼 이벤트 감지 (HID 키보드 보고서 형식의 일반적인 패턴)
# 볼륨 업(셔터)은 보통 두 번째 바이트(인덱스 1)가 0x80 또는 0x0E 등으로 설정됩니다.
if len(data) >= 2:
# 0xE9는 일반적으로 볼륨 업 키를 나타내는 HID Usage ID입니다.
if data[1] == 0xE9:
print("🚀 **셔터 버튼 (볼륨 업) 눌림 감지!**")
elif data[1] == 0x00:
print("셔터 버튼 떼짐 감지.")
else:
# 기타 키 또는 HID 이벤트
pass
async def scan_and_connect():
"""타겟 장치를 스캔하고 연결을 시도하는 메인 함수"""
print(f"Scanning for BLE device named '{TARGET_DEVICE_NAME}'...")
# 스캔을 통해 장치를 찾습니다.
scanner = BleakScanner()
# 10초 동안 스캔
devices = await scanner.discover(timeout=10.0)
target_device = None
for device in devices:
if device.name and TARGET_DEVICE_NAME in device.name:
target_device = device
break
if not target_device:
print(f"Error: Could not find device with name '{TARGET_DEVICE_NAME}'.")
print("Please ensure the device is discoverable and check the name.")
return
address = target_device.address
print(f"Found target device at address: {address}. Connecting...")
# 연결 시도
async with BleakClient(address) as client:
if not client.is_connected:
print("Failed to connect.")
return
print("Connection successful! Discovering services...")
# HID Report Characteristic 찾기
report_char = None
# 연결된 장치의 모든 서비스를 검색합니다.
for service in client.services:
print(f"\n[Service] UUID: {service.uuid} (Handle: {service.handle})")
# HID 서비스 UUID 확인
if service.uuid == HID_SERVICE_UUID:
print(f"Found HID Service: {service.uuid}")
for char in service.characteristics:
# Report Characteristic UUID 확인
print(f"uuid = {char.uuid}")
if char.uuid == REPORT_CHAR_UUID:
report_char = char
print(f"Found Report Characteristic: {char.uuid}")
break
if report_char:
break
if not report_char:
print(f"Error: Could not find Report Characteristic ({REPORT_CHAR_UUID}).")
return
# Notification 구독 시작 (셔터 이벤트 수신)
print("---> Notification 구독 시작 (셔터 이벤트 수신)")
try:
await client.start_notify(REPORT_CHAR_UUID, hid_report_handler)
print("Subscribed to Report Characteristic notifications. Waiting for shutter events...")
# 연결 유지 및 이벤트 수신 대기
# 사용자가 Ctrl+C를 누를 때까지 무한정 대기
while client.is_connected:
await asyncio.sleep(1)
except Exception as e:
print(f"An error occurred during notification: {e}")
finally:
print("Stopping notifications and disconnecting...")
await client.stop_notify(REPORT_CHAR_UUID)
if __name__ == "__main__":
# Bleak는 비동기 함수(async)를 사용하므로 asyncio.run()으로 실행
try:
asyncio.run(scan_and_connect())
except KeyboardInterrupt:
print("\nClient stopped by user (KeyboardInterrupt).")
except Exception as e:
print(f"An unhandled error occurred: {e}")