2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ブラウザとRaspberry Pi PicoをBluetoothで接続する

Last updated at Posted at 2024-12-30

はじめに

冬休みの自由工作として、Raspberry Pi Pico Wで物理的な工作をしようかと思い立ちました。
しかしRasPi Pico単体では柔軟な設定などができないため、PCやスマホとの連携を考えたくなります。

RasPi Pico WではPC等との接続方法として、USB等での有線接続のほかにWifiとBluetoothでの無線接続に対応しています。今回はBluetoothを選択してみます1

またPCやスマホでの実装としては、マルチプラットフォームかつGUI付きのものを実装するために、ブラウザ上で動くWebアプリの実装を選択します。

ブラウザからデバイスのBluetoothを触る手段として、Web Bluetooth APIという規格が制定されているので、これを利用します。

RasPi側

環境構築

RasPi Picoを触ったことがある方はもうお馴染みかと思いますが、RasPi PicoにMicroPythonを焼いておきます。

  1. こちらからMicroPythonのRasPi Pico向けポートをダウンロードします2
  2. RasPi PicoをBOOTSEL ボタンを押しながらPCにUSB接続し、マウントされる RPI-RP2 というドライブに上記ファイルをコピーします
  3. ドライブとの接続が自動的に切断されるのを確認します

また、PC側の開発環境も構築しておきます。

  1. VSCodeに MicroPico という拡張機能をインストールます
  2. プロジェクトディレクトリとして適当なディレクトリを用意します
  3. VSCodeでそのディレクトリを開き、左のエクスプローラバーからそのディレクトリを右クリックして "Setup MicroPico Project" を選択します

実行

下記のPythonコードを適当な名前で保存し、VSCode下部のステータスバーの "Run" から実行します。

import asyncio
import aioble
import bluetooth

# mimicking nRF41822 UART
SERVICE_UUID = bluetooth.UUID('6E400001-B5A3-F393-E0A9-E50E24DCCA9E')

service = aioble.Service(SERVICE_UUID)
send_ch = aioble.Characteristic(
    service = service,
    uuid = bluetooth.UUID('6E400003-B5A3-F393-E0A9-E50E24DCCA9E'),
    read = True,
    notify = True,
)
recv_ch = aioble.Characteristic(
    service=service,
    uuid=bluetooth.UUID('6E400002-B5A3-F393-E0A9-E50E24DCCA9E'),
    write=True,
    write_no_response=True,
    capture=True
)

aioble.register_services(service)

async def advertise_task():
    while True:
        async with await aioble.advertise(
            interval_us=500_000,
            name="bt-test",
            services=[SERVICE_UUID]
        ) as connection: # type: ignore
            print("connected: ", connection.device)
            await connection.disconnected(timeout_ms=None)

async def receive_task():
    while True:
        await recv_ch.written() # type: ignore
        data = recv_ch.read()
        print(f"received: {data}")
        send_ch.write(b'you said: ' + data, True)

async def main():
    tasks = [
        asyncio.create_task(receive_task()),
        asyncio.create_task(advertise_task())
    ]
    await asyncio.gather(*tasks)

asyncio.run(main())

今回実装するBluetooth BLEのGATTの詳細についての解説は他のサイトを見ていただくこととしますが、大雑把に言うと、一つのデバイスが一つ以上のサービス(≒機能)を提供し、各サービスがそれぞれ複数のキャラクタリスティック(≒読み書きできる値)から構成されるような形になります。

使用するサービスやキャラクタリスティックは何でもよいのですが、今回はNordic社のnRF41822のUART機能を模倣します。
これはSerial Bluetooth TerminalというAndroidアプリがnRF41822のUART機能をサポートしており、デバッグにちょうど良かったためです。

Webアプリ側

次にWebアプリ側ですが、読者の皆さんのお好きなフレームワークをお好きなようにセットアップしてください3
筆者は React + Next.js + MUI というありがちな構成をとりました。

また、プロジェクトに@types/web-bluetooth
を追加しておいてください。

次に以下のコードを適当な場所に追加し、下記の使用法を参考に使用してください。

class SimpleBluetooth {
  private textEncoder = new TextEncoder();
  private textDecoder = new TextDecoder('utf-8');
  private recvCharacteristic: BluetoothRemoteGATTCharacteristic | undefined;
  private sendCharacteristic: BluetoothRemoteGATTCharacteristic | undefined;
  
  public async connect(params: {
    deviceName: string,
    onReceived: (value: string) => void
  }) {
    const td = this.textDecoder;
    function onReceivedWrapper(this: BluetoothRemoteGATTCharacteristic, ev: Event): void {
      const v = this.value;
      if (v === undefined) {
        return;
      }
      const str = td.decode(v);
      params.onReceived(str);
    }

    const device = await navigator.bluetooth.requestDevice({
      filters: [{
        name: params.deviceName
      }],
      optionalServices: ['6e400001-b5a3-f393-e0a9-e50e24dcca9e']
    })

    const server = await device.gatt?.connect();
    if (server === undefined) throw new Error("GATT server not provided");
    
    const service = await server.getPrimaryService('6e400001-b5a3-f393-e0a9-e50e24dcca9e');

    this.sendCharacteristic = await service.getCharacteristic('6e400002-b5a3-f393-e0a9-e50e24dcca9e');
    this.recvCharacteristic = await service.getCharacteristic('6e400003-b5a3-f393-e0a9-e50e24dcca9e');

    this.recvCharacteristic.addEventListener('characteristicvaluechanged', onReceivedWrapper);

    await this.recvCharacteristic.startNotifications().catch(e => {
      this.recvCharacteristic!!.removeEventListener('characteristicvaluechanged', onReceivedWrapper);
      throw e;
    });

  }

  public async send(str: string) {
    if (this.sendCharacteristic === undefined) {
      throw new Error("Bluetooth device not connected");
    }
    const payload = this.textEncoder.encode(str);
    return this.sendCharacteristic.writeValueWithoutResponse(payload);
  } 
}

使用法

  1. 上記クラスにデバイス名(今回はbt-test)と適当なコールバック関数を渡してインスタンス化します
  2. connect を呼んでRasPi Picoと接続します。この際、ユーザのアクションに起因したイベント内から呼ばなければいけないことに注意してください
  3. 適当な文字列を引数にして send を呼びます

動作確認

上記クラスを使用した適当な実装を用意します。筆者は下記のような、非常に雑なコードを使用しました。

'use client'
import { useState } from "react";
import { ThemeProvider, createTheme } from '@mui/material/styles';
import { Container, IconButton, Stack, TextField, Grid2 as Grid } from "@mui/material";
import { Bluetooth, Send } from '@mui/icons-material';

class SimpleBluetooth {
  /*
    :
    :
  */
}

export default function Home() {
  const [output, setOutput] = useState<string>("");
  const [command, setCommand] = useState<string>("");
  const [bt] = useState<SimpleBluetooth>(new SimpleBluetooth());

  const startConnection = async () => {
    bt.connect({
      deviceName: 'bt-test',
      onReceived: (str: string) => setOutput(str)
    });
  }

  const theme = createTheme({
    colorSchemes: {
      light: true,
      dark: true,
    }
  });

  return (
    <ThemeProvider theme={theme}>
      <Container maxWidth="sm" component="main">
        <Stack sx={{ marginTop: '10px' }} spacing={2}>
          <TextField label="Output" multiline rows={10} value={output}/>
          <Grid container spacing={0}>
            <Grid size={1}>
              <IconButton onClick={startConnection} size="large">
                <Bluetooth fontSize="inherit"/>
              </IconButton>
            </Grid>
            <Grid size={10}>
              <TextField onChange={ e => setCommand(e.target.value) } fullWidth></TextField>
            </Grid>
            <Grid size={1}>
              <IconButton onClick={ _ =>  bt.send(command)} size="large">
                <Send fontSize="inherit"/>
              </IconButton>
            </Grid>
          </Grid>
        </Stack>
      </Container>
    </ThemeProvider>
  );
}

BluetoothアイコンのボタンをクリックしてRasPi Picoに接続した後、その隣のテキストボックスに適当な文字列をセットして送信すると、上のテキストエリアに "you said: (送信した文字列)"と出てくることが確認できるかと思います。

ただし、Web Bluetooth API自体があまり成熟しておらず、サポートしているブラウザがChromeやChromium、Edge等に限られること、localhost内かHTTPS接続下でなければ動作しないことにご注意ください。

最後に

今回はごく基本的なエコーバックのみを実装しましたが、これを拡張し、JSONで通信するなどすれば、ブラウザとRasPiの間で自由に通信できるようになるかと思います。
RasPiの使い道の拡張にでも参考にしていただければと思います。

  1. よりチャレンジングで面白そうなので

  2. 筆者が使用したファイルは RPI_PICO_W-20241129-v1.24.1.uf2 です

  3. この辺りは読者の皆さんの方が詳しいでしょうし。いちおうTypeScript縛りという条件はありますが、さすがに今日日JS直書きじゃないと嫌という方は珍しいかと思います

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?