はじめに
RaspberryPi Pico Wで、MicroPythonを使って、Bluetooth(BLE)のペリフェラル作る方法を解説します。
コードはPico Wの公式の記事で紹介されている、公式チュートリアルのものを使用、より詳細かつ、分かりやすくした内容を、日本語で解説していきます。
チュートリアルには以下の3つのコードが載っていますが、今回は「② Pico WをBLEのペリフェラルにするコード」の解説です。
チュートリアルのコードを見て「これどういう意味?」と思った方はぜひご覧ください。
No. | 内容 | ファイル名 |
1 | BLE用のユーティリティ | ble_advertising.py |
2 | Pico WをBLEのペリフェラルにする | picow_ble_temp_sensor.py |
3 | Pico WをBLEのセントラルにする | picow_ble_temp_reader.py |
※ ③の「Pico WをBLEセントラルにする」についてはこちらの記事で解説しています。
~ この記事の内容 / Contents ~
環境
この記事で使用する環境は以下の通りです。
環境 | バージョンなど | 備考 |
開発用PCのOS | Windows11 | Windows10でもOKです |
言語 | MicroPython | |
開発環境 Thonny | 4.0.2 | |
ボード | RaspberryPi Pico | |
ファームウェア | micropython-firmware-pico-w-130623.uf2 |
RaspberryPi Pico W との接続
Pico W 本体の温度センサを使用します。部品や回路をつなぐ必要はありません。
使用する部品
Pico W 本体
Raspberry Pi Pico W本体です。技適対応品かどうか心配…という方は、以下のページで出品者を「共立エレショップAセレクト」(国内の有名ショップ)にして購入してください。
USBケーブル Micro-B
本体にUSBケーブルが付属していないので、別途購入が必要です。PicoW側の形状は「Micro-B」、ひと昔前のスマホと同じタイプを使います。現在主流のTypeCではないので注意が必要です。

スマホ(Android / iPhone)
Pico W とのBluetooth接続に使います。お手持ちのものでOKです。私は高スペック・低価格なモトローラ製を愛用しています。

プログラム概要
今回のプログラムの概要は以下の通りです。Pico本体の温度センサの値をBluetoothで通知、スマホのアプリ(LightBlue)で表示します。
- Pico WをBLEのペリフェラルとして設定する
- 温度センサの値を通知(Notify)する
- スマホアプリで温度センサの値を表示する。
実行結果
後述するコードの実行結果は以下の通りです。スマホアプリでPico Wの温度センサの値を取得できます。
ファームウェアの更新
PicoWでBluetoothを使うには、最新のファームウェアが必要です。こちらの公式サイトのDLページからダウンロードしてPicoWを更新してください。
いつも通り「BOOTSEL」を押して起動、ファイルをコピーするだけで更新できます。
初めて行う方は、こちらのnote記事(無料)に手順をまとめているので、リンク先をご覧ください。
LightBlueのインストール
スマホにBluetoothアプリの「LightBlue」をインストールします。
全体コード
全体コードは以下の通りです。詳細な内容は後述する「コードのポイント」で解説します。
以下のコードを実行するには、「ble_advertising.py」のファイルも必要です。こちらの公式GitHubページでダウンロードして、Pico本体に保存してください。
2023/10/24 コード修正
74行目の[ 1]が消えていたので修正しました。[ 1]が消えた状態だと以下のエラーが表示されてしまうので修正をお願いします。
TypeError: object with buffer protocol required
情報いただいた114さん、ありがとうございます。
import bluetooth
import random
import struct
import time
import machine
import ubinascii
from ble_advertising import advertising_payload
from micropython import const
from machine import Pin
_IRQ_CENTRAL_CONNECT = const(1)
_IRQ_CENTRAL_DISCONNECT = const(2)
_IRQ_GATTS_INDICATE_DONE = const(20)
_FLAG_READ = const(0x0002)
_FLAG_NOTIFY = const(0x0010)
_FLAG_INDICATE = const(0x0020)
# 温度情報サービスのUUIDの定義です
_ENV_SENSE_UUID = bluetooth.UUID(0x181A)
#
# 特徴(Characteristic)を設定するタプルです。
# TypeのUUIDと、Permissionのフラグを設定しています
#
_TEMP_CHAR = (
bluetooth.UUID(0x2A6E),
_FLAG_READ | _FLAG_NOTIFY | _FLAG_INDICATE,
)
#
# サービスを設定するタプルです
# サービスの定義(Declaration)のTypeに設定するUUIDと
# 上記で作成した特徴(Characteristic)をセットしています。
#
_ENV_SENSE_SERVICE = (
_ENV_SENSE_UUID,
(_TEMP_CHAR,),
)
#
# ペリフェラルの分類を示す定数です、温度センサの値である768を使用します。
#
_ADV_APPEARANCE_GENERIC_THERMOMETER = const(768)
#
# Picoの温度情報を配信するペリフェラルのクラスです
#
class BLETemperature:
def __init__(self, ble, name=""):
# 温度センサに使うピンを設定します。
self._sensor_temp = machine.ADC(4)
# 呼び出し元から渡された、BLE用のオブジェクトをクラス内部にセットし、
# BLEを有効化します。
self._ble = ble
self._ble.active(True)
# ライブラリから呼び出してもらう関数をセットします。
self._ble.irq(self._irq)
# ライブラリにサービスをセットします。
# 戻り値のうち、_TEMP_CHARへのハンドルのみをメンバ変数に取得しています。
((self._handle,),) = self._ble.gatts_register_services((_ENV_SENSE_SERVICE,))
# 配列をset関数で初期化します
self._connections = set()
# サービスの名前が指定されていない場合は、MACアドレスの名前を使用します。
if len(name) == 0:
name = 'Pico %s' % ubinascii.hexlify( self._ble.config('mac')[1],':').decode().upper()
print('Sensor name %s' % name)
# サービスの名前とオブジェクトを指定して、
# アドバタイズするデータ(ペイロード)を作成します
self._payload = advertising_payload(
name=name, services=[_ENV_SENSE_UUID]
)
# アドバタイズを行います
self._advertise()
#
# ライブラリから呼び出してもらう関数の定義です
#
def _irq(self, event, data):
# セントラルからの接続時は、その接続ハンドルを自身のリストに追加します
if event == _IRQ_CENTRAL_CONNECT:
conn_handle, _, _ = data
self._connections.add(conn_handle)
# セントラルからの切断時は、自身のリストから接続ハンドルを削除し、
# 停止していたアドバタイズを再開します。
elif event == _IRQ_CENTRAL_DISCONNECT:
conn_handle, _, _ = data
self._connections.remove(conn_handle)
self._advertise()
# Indicateの完了を受け取った時は、接続状態、値のハンドル、ステータスのハンドルを取得します。
elif event == _IRQ_GATTS_INDICATE_DONE:
conn_handle, value_handle, status = data
#
# 温度センサの値を更新して、notify/indicateを行う関数です
#
def update_temperature(self, notify=False, indicate=False):
# 温度センサから温度を取得・表示します
temp_deg_c = self._get_temp()
print("write temp %.2f degc" % temp_deg_c);
# キャラクタリスティック内の温度情報を更新します。
self._ble.gatts_write(self._handle, struct.pack("<h", int(temp_deg_c * 100)))
print("temp "+ str(int(temp_deg_c * 100)))
# 自身で保持している全ての接続ハンドル(セントラル)に対して、notify/indicateを行います
if notify or indicate:
for conn_handle in self._connections:
if notify:
self._ble.gatts_notify(conn_handle, self._handle)
if indicate:
self._ble.gatts_indicate(conn_handle, self._handle)
# 自身で保持しているペイロードをセットしてアドバタイズする関数です
def _advertise(self, interval_us=500000):
self._ble.gap_advertise(interval_us, adv_data=self._payload)
# pico W本体の温度センサの値を取得して、電圧->温度に変換する関数です
def _get_temp(self):
conversion_factor = 3.3 / (65535)
reading = self._sensor_temp.read_u16() * conversion_factor
return 27 - (reading - 0.706) / 0.001721
# メインの処理です
def demo():
# bleライブラリのオブジェクト(変数)を作成します
ble = bluetooth.BLE()
# ペリフェラルのオブジェクト(変数)を作成します
temp = BLETemperature(ble)
# Pico本体のLEDのピンを取得します
counter = 0
led = Pin('LED', Pin.OUT)
# 10秒に1回、温度センサの値を取得して、notifyします
# LEDは1秒ごとにON/OFFを繰り返します.
while True:
if counter % 10 == 0:
temp.update_temperature(notify=True, indicate=False)
led.toggle()
time.sleep_ms(1000)
counter += 1
if __name__ == "__main__":
demo()
コードのポイント
GATTとサービス
BLE(Bluetooth Low Energy)では、GATT(Generic Attribute Profile) というデータ構造をとっており、セントラル・ペリフェラルとも、GATTのデータ構造をしています。
GATTの大きな枠組みとして、まず「サービス」があります。サービスは複数の機能をまとめたグループや フォルダのようなもので、ペリフェラル内に複数作ることができます。
サービスは、特定の機能を持つ「特徴(Characteristic/キャラクタリスティック)」で構成されており、サービス内に複数作ることができます。
特徴(Characteristic)の構成
特徴(Characteristic)も、以下の3つの要素から構成されたまとまりです。
- 宣言(Declaration)
- 値(Value)
- 説明(Descriptor)
ATT
上記3つは、アトリビュート(ATT)と呼ばれ、以下の4つの要素でできています。ATTは最小構成なのでこれ以上の要素に分解することはできません。
- ハンドル(Handle)
- 型(Type)
- 値(Value)
- 権限(Permission)
ペリフェラルの構成まとめ
上記の特徴をまとめると以下のようになります。
上記の図を見ると、特徴(Characteristic)とは別に「サービスの宣言(Declaration)」として、ATTが存在します。
コード内の _ENV_SENSE_UUIDは、このサービスの宣言(Declaration)の「Type」に設定されるUUIDです。
特徴(Characteristic)のタプルでは、「Type」にセットするUUID、「Permissoin」に設定するフラグを設定しています。前述の図に値をセットすると以下のようになります。
# 温度情報サービスのUUIDの定義です
_ENV_SENSE_UUID = bluetooth.UUID(0x181A)
#
# 特徴(Characteristic)を設定するタプルです。
# TypeのUUIDと、Permissionのフラグを設定しています
#
_TEMP_CHAR = (
bluetooth.UUID(0x2A6E),
_FLAG_READ | _FLAG_NOTIFY | _FLAG_INDICATE,
)
#
# サービスを設定するタプルです
# サービスの定義(Declaration)のTypeに設定するUUIDと
# 上記で作成した特徴(Characteristic)をセットしています。
#
_ENV_SENSE_SERVICE = (
_ENV_SENSE_UUID,
(_TEMP_CHAR,),
)
呼び出し用の関数の設定
BLEライブラリでは、セントラルからの接続・切断などをライブラリが自動的に監視してくれます。
接続などのイベントが発生した場合、ライブラリは設定された割り込み(IRQ)関数を呼び出すことで、ユーザーに通知を行います。
そのため、ユーザーはイベントごとに処理したい内容を関数に実装し、ライブラリに対してその関数を登録するだけで、イベントを処理できます。
登録は以下のように、ライブラリのirq関数の引数に、自身の関数を渡すだけで登録できます
# ライブラリから呼び出してもらう関数をセットします。
self._ble.irq(self._irq)
サービスの登録
BLEライブラリに、サービスを登録します。
関数を実行すると、登録したサービスの特徴(Characteristic)の「ハンドル※」が返されます。ハンドルは各サービスごとにタプルにまとめられて返されます。
今回のコードではサービスが1つ、特徴(Characteristic)も1つなので、1つ目のサービスの1つ目のハンドルのみを取得しています。以下のイメージです。
# ライブラリにサービスをセットします。
# 戻り値のうち、_TEMP_CHARへのハンドルのみをメンバ変数に取得しています。
((self._handle,),) = self._b
※ ここでいう特徴(Characteristic)への「ハンドル」はプログラミング的なハンドル(オブジェクトなどを操作するためのもの)です。
BLEの用語でも「ハンドル」が存在するため混乱しますが、チュートリアルのコードなので、そのまま解説します。
サービス名の登録
init関数にサービス名(name)を指定しない場合は「Pico + MACアドレス」が名前として設定されます。
# サービスの名前が指定されていない場合はデフォルトの名前を設定します。
if len(name) == 0:
name = 'Pico %s' % ubinascii.hexlify(self._ble.config('mac')
,':').decode().upper()
print('Sensor name %s' % name)
MACアドレスはconfig
関数で取得できるのですが、byte配列型の生の値なので、そのままではサービス名(文字列)として扱いずらい形式です。そのため以下の変換を行い、uft-8などの扱いやすい文字列に置き換えを行います。
config関数からは、「addr_type」「byte配列型のMACアドレス値」の2つが返るため、インデックスを指定してMACアドレス値のみ取得します。
self._ble.config('mac')
上記のMACアドレスはバイトは配列型(≒数値)なのでubinascii.hexlufy関数でMACアドレスの数値を、ASCI文字列で表現した値に変換します(数値の0なら、ASCIIで0x30などの変換)。
また、上記関数は指定した文字を「区切り文字」として追加できるので「:コロン」を指定します。
ubinascii.hexlify(self._ble.config('mac')
,':')
上記の状態でも、MACアドレスはまだバイト型の配列であるため、decode関数を使って unicode文字列に変換します。また、upper関数を使っているので、文字列はすべて大文字になります。
ubinascii.hexlify(self._ble.config('mac')
,':').decode().upper()
28:CD:C1:0B:B4:C0
ペイロードの作成
advertising_payload関数に、サービスの名前・タプルデータ渡すことで、通信のペイロード(≒パケット)を戻り値として取得できます。
取得したペイロードはクラス内のメンバ変数「_payload」に保持します。
# サービスの名前とオブジェクトを指定して、
# アドバタイズするデータ(ペイロード)を作成します
self._payload = advertising_payload(
name=name, services=[_ENV_SENSE_UUID]
)
advertising_payload関数はチュートリアルの「①BLE用のユーティリティコード」に実装されています。そのため、今回の「②Pico WをBLEのペリフェラルにする」コードを実行するときは、上記2つのコード(ファイル)を「Pico W本体」に保存してください。
アドバタイズの実行
_advertise関数でアドバタイズを実行します。送信するペイロードは前述したadvertising_payload関数でメンバ変数に保持している「_payload」を使用します。
# アドバタイズを行います
self._advertise()
# ~~~~~ 中略 ~~~~~
# 自身で保持しているペイロードをセットしてアドバタイズを行う関数です
def _advertise(self, interval_us=500000):
self._ble.gap_advertise(interval_us, adv_data=self._payload)
イベント検知時の処理
ライブラリがイベント発生時に呼び出す処理を記載した関数です。イベント内容は「event」の引数に、ハンドルなどのデータは「data」にセットされて、ライブラリから呼び出されます。
#
# ライブラリから呼び出してもらう関数の定義です
#
def _irq(self, event, data):
# セントラルからの接続時は、その接続ハンドルを自身のリストに追加します
if event == _IRQ_CENTRAL_CONNECT:
conn_handle, _, _ = data
self._connections.add(conn_handle)
# セントラルからの切断時は、自身のリストから接続ハンドルを削除し、
# 停止していたアドバタイズを再開します。
elif event == _IRQ_CENTRAL_DISCONNECT:
conn_handle, _, _ = data
self._connections.remove(conn_handle)
self._advertise()
# Indicateの完了を受け取った時は、接続状態、値のハンドル、ステータスのハンドルを取得します。
elif event == _IRQ_GATTS_INDICATE_DONE:
conn_handle, value_handle, status = data
タプル型である「data」の内容は、イベントごとに異なります。そのため上記関数を改造・拡張する際は、BLEライブラリのリファレンスを見ながら改造・修正を行ってください。
温度の取得とnotify(indicate)
Pico本体の温度センサから値を取得、セントラルに対して発信(notify/indicate)する関数です。
温度センサの値は直接送信するのではなく、gatts_write関数でTEMP_CHARの特徴(Characteristic)に設定し、notify/indicateの際に、特徴(Characteristic)のハンドルを渡すことで通知します。
温度情報は「31.52度」のような少数点付きの値になりますが、通信の際は少数点が邪魔になります。そのため、100倍することで少数点をなくし、strcut.pack関数でバイト列のデータに変換しています。
1つ目の引数「<h」はバイト列に変換する際、short(2byteの符号付整数)を、リトルエンディアンで変換するという指定です。
nofify/indicateは「_connections」で管理しているすべてのセントラルに対して行われますが、このリストはライブラリ側で接続・切断が発生した際にirq関数で自動的に更新されるものです。そのため、ユーザーはリストの状態を気にせずに、notify/indicateを行うことができます。
温度の取得
Picoの温度センサの値は、27度からの差分が電圧値として出力されるため、電圧1単位あたりの温度をかけることで計算できます。詳細はこちらの温度センサの記事をご覧ください。
#
# 温度センサの値を更新して、notify/indicateを行う関数です
#
def update_temperature(self, notify=False, indicate=False):
# 温度センサから温度を取得・表示します
temp_deg_c = self._get_temp()
print("write temp %.2f degc" % temp_deg_c);
# キャラクタリスティック内の温度情報を更新します。
self._ble.gatts_write(self._handle, struct.pack("<h", int(temp_deg_c * 100)))
print("temp "+ str(int(temp_deg_c * 100)))
# 自身で保持している全ての接続ハンドル(セントラル)に対して、notify/indicateを行います
if notify or indicate:
for conn_handle in self._connections:
if notify:
self._ble.gatts_notify(conn_handle, self._handle)
if indicate:
self._ble.gatts_indicate(conn_handle, self._handle)
# 自身で保持しているペイロードをセットしてアドバタイズする関数です
def _advertise(self, interval_us=500000):
self._ble.gap_advertise(interval_us, adv_data=self._payload)
# pico W本体の温度センサの値を取得して、電圧->温度に変換する関数です
def _get_temp(self):
conversion_factor = 3.3 / (65535)
reading = self._sensor_temp.read_u16() * conversion_factor
return 27 - (reading - 0.706) / 0.001721
#~~~~~ 中略 ~~~~~
# pico W本体の温度センサの値を取得して、電圧->温度に変換する関数です
def _get_temp(self):
conversion_factor = 3.3 / (65535)
reading = self._sensor_temp.read_u16() * conversion_factor
return 27 - (reading - 0.706) / 0.001721
メイン処理
メインとなる処理です。10秒に1回update_temperature関数を呼び出すことで、温度センサの取得とnotifyを行います。
また、毎周期(1秒)ごとに、Pico本体のLEDをトグル(点灯/消灯)も併せて行います。
# 10秒に1回、温度センサの値を取得して、notifyします
# LEDは1秒ごとにON/OFFを繰り返します.
while True:
if counter % 10 == 0:
temp.update_temperature(notify=True, indicate=False)
led.toggle()
time.sleep_ms(1000)
counter += 1
LightBlueの操作方法
LightBlueを起動後、以下の手順で操作するとPico Wの温度情報を表示できます。
Pico Wで前述したプログラムを実行した後で、LightBlueを起動します。
起動すると、Pico+Macアドレスの行が表示されるので「CONNECT」をタップします。
接続に成功するとPico W(ペリフェラル)の情報がいろいろと表示されるので、一番下の「Tempreture」をタップします。
特性(Characteristic)の情報が表示されるので、下の方の「Data format」で「Hex」を選択し「READ AGAIN」か「SUBSCRIBE」をタップします。
タップすると数値が表示されますが、ここでの表示は「リトルエンディアン」なので、「AC0D」を、「0DAC」に置き換え、電卓等で「0DAC」を10進数で表示させると「3500」となります。
Picoでの温度情報は「35.00度」であっため、温度情報が100倍されて、スマホアプリに正しく送信されたことが確認できます。
まとめ
RaspberryPi Pico WのBluetoothをMicroPythonで使う方法を解説しました。
BLEでは、サービスやキャラクタリスティックなど聞き慣れない言葉が多く、チュートリアルを見て混乱された方も多いんじゃないかと思います。
私もその内の一人でしたが、調べようにもPico WやMicroPythonでBLEを使う方法はかなり少なく大変だったので、今回の記事を作成してみました。
覚えたてかつ、調べながら作った記事ではありますが、Pico WのBLEの使い方で悩んでいる方の参考になればうれしいです。
分かりやすいサイト・書籍
BLEの解説として、とても「きれい」で「分かりやすく」解説されている記事を(勝手に)ご紹介します。BLEの基本的な仕組みが解説されているので、知識ゼロからチュートリアルを読み進める場合は、非常に役に立つありがたい記事です。
書籍は以下を使いました。BLEの大筋は上記みかんさんの記事で理解できますが、より細かい内容を確認したり、補完したい場合に使えます。

お知らせ
MicroPythonのプログラミングガイドブックが遂に発売!
「MicroPython」の本が遂にでました。
この一冊で、MicroPythonの言語仕様から、プログラミングの仕方まで”ガッツリ”学べます!
内容は、普段別言語で開発している人や、これからマイコンを始める(工学系の)学生を対象としているので「初心者向け」ではありません。しかし、「自前のライブラリの作成」が目標なので、これ一冊で「ガッツリ」とMicroPythonを学ぶことができます。
全ての内容はここでは紹介しきれないので、詳細は以下のAmazonページをご覧ください。目次だけでも”ガッツリ”なのが確認できると思います。

Pico/Pico W関連のおすすめ本
RaspberryPi Pico / Pico W関連のおすすめ本を独断と偏見で3つ選んでみました。Picoやるならとりあえずこれ買っとけ的な本や、電子工作全般で使える本などを厳選しています。
質問・要望 大歓迎です
「こんな解説記事作って」「こんなことがしたいけど、〇〇で困ってる」など、コメント欄で教えてください。 質問・要望に、中の人ができる限り対応します。
使えたよ・設定できたよの一言コメントも大歓迎。気軽に足跡を残してみてください。記事を紹介したい方はブログ、SNSにバシバシ貼ってもらってOKです。
はじめましてこんにちは。
ラズパイ歴2ヶ月くらいの初心者です。
現在ラズパイpico wでbluetoothクラシックの接続をしようと色々やってみてるのですが、うまくいっていません。
解説記事をやってくれないでしょうか?
やりたいことは車で使うdefiというメーカーのメーターから水温とか油温をシリアル通信で取得、そのデータをRacechronoというスマホアプリにbluetooth(RFCOMM)で飛ばしたいです。
ラズパイ4ではうまくいったので、それをラズパイpico wで実現させたいのですが、bluetoothとの接続部分(RFCOMM)でつまずいています。
↓これはラズパイ4での完成形でこれをラズパイpico wで実現したいです。
https://youtu.be/o6LAgey7kro
かずさん
コメント・記事リクエストありがとうございます。
えすです。
動画拝見しました。ラズパイ歴2ヶ月でこのデータ取得できるなんて凄すぎですね。
PicoWでのBluetooth Classicなのですが、調べたところ、現時点でMicroPythonではサポートされていないようです。
Arduino(言語)ではサポートされているようなのですが、Pico W+Arduino言語での解説記事でも問題ないでしょうか?
返信ありがとうございます!
自分が見つけきれてないだけだと思ってましたが、サポートされていないんですね(^_^;)
Arduinoでの解説で問題ないです!
前にArduinoもやろうかなと思って本体は買ったのですが、どこにやったか行方不明中ですが(^_^;)
かずさん
えすです、こちらこそ返信ありがとうございます。
了解しました。それではArduino言語でちょっと試しつつ、記事にしてみますので少々お待ち下さい。
ちなみに「プログラミング言語」はArduino言語になりますが、(C言語ライクなもの)、ボードは「RaspberryPi Pico W」を使うので捜索しなくても大丈夫ですよ
(うちにも大昔に買ったArduinoが眠ってます…w)
Arduino言語ってArduinoのボードがないと使えないのかと勘違いしていました(笑)
記事楽しみに待っておきます!
こんにちは。
ble_advertising.pyをpico本体に保存するとはどのように行うのでしょうか?
Thonnyを使ってble_advertising.pyとpicow_ble_temp_sensor.py.pypicoをpico本体に保存するで保存しました。
すると、
Traceback (most recent call last):
File “”, line 166, in
File “”, line 150, in demo
File “”, line 74, in __init__
TypeError: object with buffer protocol required
このようなエラーが出てしまいました。
どうすればよろしいでしょうか?
114さん
コメントありがとうございます。えすです。
すみません。ブログにコードを表示させるプラグインの仕様(バグ?)で、
コードの一部が消えていました。
ファイル: picow_ble_temp_sensor.py
場所: 74行目 self._ble.config(‘mac’)[1] の部分で、[1]が消えていた。
上記部分の修正してもらえば、エラーは消えると思います(記事のコードも修正しました)
自分の環境でも確認してはいますが、エラーが消えたらまたコメントいただけるとうれしいです。