raspberry pi pico Wについて

raspberry pi picoは、Microcontroller Boardと呼ばれるもので、CPU、メモリ、入出力ポートを集積したmicrocontroller(日本ではマイコンと呼ばれます)を一枚の板の上に載せたものです。電源を含めた入出力pinが便利に使えるように展開されています。Raspberry pi picoではRP2040、pi pico 2ではRP2350という名前のmicrocontrollerが搭載されています。

Raspberry piとの大きな違いは、OSを持たないところです。ストーレッジは基本的にmicrocontroller内のフラッシュメモリに限られ、SDカードのような容量を持たない代わりに高速で動作します。OSの起動というステップがないので、電源投入後短時間で動作が開始されます。センサーなどからの読み取りデータは、外部の記憶媒体に保存しない限り、電源を落とすと消失します。

Arduinoとの違いは?

Arduinoもmicrocontroller boardの一つで、ATmega328Pが代表的なmicrocontrollerです。MicrocontrollerにはATmegaシリーズのほかESP32RP2040など、いろいろなものが各社から出されていますが、Arduino UNOのフォームファクター(大きさや入出力ピンの数や位置)が踏襲されているものが多いので、異なる製品でもほぼ同じように使うことができるようになっています。

今回取り上げるraspberry pi picoは、microcontroller boardで、コンピューターであるraspberry piとは異なります。いわゆるマイコンと呼ばれるものである点はArduinoと同じです。

pi pico, pi pico W, pi pico 2, pi pico 2 Wどう違う?

Raspberry pi picoとpico 2との違いはmicrocontrollerの違いです。上にも書きましたが、pi picoではRP2040、pi pico 2ではRP2350が搭載されています。WはWiFiユニットが搭載されていることを示します。

この記事では、pi pico 2 W を使用します。(記事の中で、pi pico, pi pico Wなどと略記することがあります)

RP2040とRP2350の違いは以下の通りですが、今回の記事の工作にはどちらでもかまいません。(表はCytron社のページより:https://sg.cytron.io/p-raspberry-pi-pico2-board

telegram bot + raspberry pi pico Wでできること

telegram botを通じて、外出先から自宅にある pi pico Wを操作します。これはすごいことです。

通常、自宅のネットワークはプライベートネットワークとしますので、外部からのアクセスが簡単にはできません。そうでないと、誰かが家のプリンターに印刷してしまう、というような事態が発生してしまいます。

外部からプライベートネットワークに接続するには、プライベートネットワーク内にVPNサーバーを設置するか、外部との境界にある機器に外からの通信を通すための特別な設定をしておく必要があります。どちらも気軽に、とはいかないレベルの選択ですし、不審なアクセスの検知やブロックに手間をかける必要が生じます。

しかーし、telegram bot + pi pico Wの組み合わせることで、外出先から自宅にあるpi pico Wにちょっとした指令をだすことができるようになります。何ができるようにするかは、工作する人のアイデア次第!まずは簡単な例から始めて、面白い使い道を工夫しましょう!この記事はそのための道しるべです。

工作に必要な材料

この記事では、チャットメッセージをなかなか見てくれない家族に、メッセージ見て!を催促するための簡単なガジェットを作ります。必要最低限の材料でまずは形にしてみましょう。

基本材料

図1:工作に用いる材料
  • 1.raspberry pi pico Wか、raspberry pi pico 2 W
    ヘッダーピンがついているものとついていないものとがあります。ついていないものを買った場合には、ヘッダーピンをはんだ付けしておいてください。
  • 2.breadboard
    両端に電源レール(写真の赤と青の線があるところ)があるものが便利です。
  • 3.micro servomotor
    SG90とか、TS90など。180度動くもの。サーボモーターには、ぐるぐる回るタイプ(ロータリーとよばれます)ものもありますのでご注意。今回は180度動くものを使います。
  • 4.5V USB電源
    5V, 2Aのものが推奨されています。
  • 5.USBケーブル(micro端子)
    raspberry pi picoの端子はUSB microです。USBケーブルの中には充電専用でデータを通さないものがあります。ここではデータ転送ができるものを用意します。
  • 6.jumper wires
    写真にあるようなもの。電源ラインやGNDラインなど色分けするとわかりやすいので、何色かあると便利です。
  • a, b, c. あるといいもの(なくてもできます)
    • キャパシター(コンデンサー):100 – 470 uF
      サーボモーターが一電流を大食いした一瞬を補うためのもの。
    • タクトボタンスイッチ(tactile switch)
      メッセージ見ました、の返事をするための仕組み用。
    • 抵抗:1K – 10K ohm
      ボタンスイッチのpull-up (down) 回路を作るために使います。

土台、デコレーション用材料

あとは工作用のあれこれをお好みで。

  • Legoブロックとプレート
  • 両面テープ
  • 厚紙
  • マーカー、ペン類

circuitpythonのインストールとライブラリについて

circuitpythonとは

Pythonでマイクロコントローラーのプログラミングをするためのファームウエアです。Adafruitによって開発維持されています(感謝)。

それぞれのマイクロコントローラー製品にあったものをインストールすることで、pythonコードを解釈してコントローラーに伝える通訳として機能してくれるほか、デバイスをUSBストーレッジとして認識するようにしてくれます。これにより、パソコンで編集したpythonコードを簡単にデバイスに転送することができます。ストーレッジ自体をVisual Studio Codeで開けば、デバイス内のpythonコードを直接編集することもできます。

circuitpythonのダウンロード

まず、Downloadのページ(https://circuitpython.org/downloads)を開きましょう。Circuitpythonを使うことのできるデバイスたちがずらりと並びます。どんなものでcircuitpythonが使えるのか見るだけでも楽しいですね!

さて、今回私はraspberry pi pico 2 Wを使いますので、Pico 2 Wをクリックします。raspberry pi pico Wをお使いの場合にはPico Wのほうをクリックしてください。上にも書きましたが、circuitpython(firmware)はそれぞれのデバイスに対応したものをインストールする必要があります。アップデートも盛んにおこなわれていますので、新しいことを始めるときにはこのページを開いて最新のものを取得するようにしましょう。

DOWLOAD .UF2 NOWをクリックします。執筆時点でのバージョンは10.0.3です。

adafruit-circuitpython-raspberry_pi_pico2_w-en_US-10.0.3.uf2

という名前のファイルがダウンロードされました。これがCircuitpython (ファームウエア)本体です。

circuitpythonのインストール

ここからは、raspberry pi pico Wとかraspberry pi pico 2 Wを短くpi picoなどと表現します。

USBケーブルとpi picoとをまずつなぎます。

次に、pi pico上にある、BOOTSELボタンを押したまま、USBケーブルをパソコンにつなぎます。しばらくすると、pi picoが“RP”で始まる名前のストーレッジとしてマウントされます。ここまできたらボタンを離してOKです。

私のpi pico 2 Wの場合、RP2350という名前のストーレッジとしてマウントされました。手持ちのpi pico Wで試したところ、RPI-RP2という名前でマウントされました。

いずれにせよ、下のような内容のものがマウントされるはずです。

先ほどダウンロードした、.UF2ファイルをマウントされたストーレッジにドラッグしてコピーします。

コピーには少し時間がかかります。完了すると、ストーレッジがいったんアンマウントされ、あらたにCIRCUITPYという名前で再マウントされます。

これでcircuitpythonのインストールは完了です。

ライブラリーのダウンロード

CircuitPythonのダウンロードページの上のほうにある、“Libraries”をクリックします。

Bundleのところをみると、Version 9.x用と、Version 10.x用とが見えます。今回ダウンロードしたcircuitpythonのバージョンは10.0.3なので、Version 10.xのほうをダウンロードして取得します。

adafruit-circuitpython-bundle-10.x-mpy-20260116.zip

という名前のファイルが得られました。zipアーカイブになっているので、展開しておきましょう。後ほど必要なライブラリをこのフォルダからデバイス(pi pico)側にコピーして使います。

Circuitpythonでも通常のpythonでのコーディングと同様、必要なライブラリはimportして使います。マイクロコントローラーのストーレッジ(フラッシュメモリ)容量は限られているので、必要なライブラリだけをデバイスにコピーすることが重要です。実現したいプロジェクトによっては必要なライブラリが多くてデバイス側のストーレッジが足りない!なんてことも起こります。そんな場合には、いかに少ないライブラリでやりたいことを実現するか、コードの無駄を省くか、で腕前を磨くことになります。

余談ですが、人類を月まで運んだアポロ計画で使われたコンピューターのROMは72KB相当、raspberry pi pico Wには2MB (pico 2 Wには4MB!) のフラッシュメモリが積まれてますから、月にいくよりもっと多くのことができるはず!と信じて楽しくコーディングしましょう。

ライブラリのpi picoへのインストールは以下のそれぞれのステップで説明します。

コーディング環境のセットアップ

Visual Studio Code (VSC)でpi picoのコード開発をする場合の方法について説明します。基本的にはpythonコードを書く、という部分に限ればどのエディタでも構わないのですが、マイクロコントローラのプログラミングにおいてはデバイスの言い分を聞くためのシリアルモニタが必須です。シリアルモニタをするアプリをエディタとは別に用意するのも手なのですが、VSCならシリアルモニタを拡張機能として導入することができて便利です。この記事ではVSCでのセットアップを用いて説明していきます。

  • 1.VSCの左側のバーにある、extensionsアイコンをクリック
  • 2.Searchのところに”serial monitor”と入力
  • 3.Microsoftが提供しているSerial Monitorが見るかるはず
  • 4.あるいは5.の”Install”をクリックして拡張機能をインストール

ここで、circuitpythonのインストールがすんだpi picoをパソコンに接続し、VSCのメニューからView -> Panelを選びます。あるいはショートカットを用いるならControl + J

VSCウインドウの下部に、次のようなパネルが現れて”TERMINAL“が表示されます。“SERIAL MONITOR”をクリックしてシリアルモニターに切り替えます。

次のような画面が現れるはず。PortのところのCOMの後の数字は下の例と同じでなくてもかまいませんBaudo rateが115200になっているのを確認してください。これはシリアルの通信速度です。もし違う数字が表示されている場合には115200をプルダウンから選択してください。

ここまででセットアップは完了です。

VSCはちょっと面倒だなあ、という場合

Circuitpythonでマイクロコントローラーのプログラミングをするために便利なアプリケーションがあります。書く、保存する、走らせる、がすべてできる便利ソフト。シリアルコンソールも装備されているので、これ一つで事足ります。手っ取り早く試してみたい、というときには重宝します。

コード書きのサイクルとスタイル

基本的に通常のpythonコードの開発と同じなんですが、circuitpythonの場合は少しだけ異なる部分がありますので、その点について説明します。

基本的には、

  1. コードを書く
  2. 書き足したり変更した部分を保存
  3. 実行して結果を確認

を繰り返すことになりますが、パソコン上でのpythonコード開発と異なる部分は、デバイス上にあるコードを保存するとそれが自動で実行されるようになっている*ところです。また、デバイス上でのメインコードは、code.pyあるいはmain.pyという名前であることが必須です。

*私の環境なのか、走らせるコードが理由なのか、保存後の自動実行がされたりされなかったりしました。自動実行されない場合には、デバイスをPCから外し、数秒まってから接続しなおす、という原始的な方法をとりました。

これはこれで便利なことも多いのですが、実際のコード書きではこまめに保存することが多いので、その都度コードが実行されると、ちょっと待ってくれ、まだ書き終わってない、もーいちいち動かないで!となってしまうことがあります。書きあがったコードの一部を変更するだけ、という場合ならいいのですが、実際コードが組みあがっていくまでの間には、まだ実行しちゃだめ、な期間がたくさんあるわけです。circuitpythonはそんなことにはお構いなしで、保存即実行、がデフォルトになっていますので、落ち着いてコーディングできない。これを解決するための一つのスタイルは、

  • コードファイルはパソコン側に置いたものを編集、保存、よし、実行して試そうとなってからデバイス側にコピーする。

このスタイルでは、コピーでデバイス側にコードファイルが保存(上書き)された段階で実行されるので、実行のタイミングを完全にコントロールすることができます。

もう一つのスタイルは、

  • コードファイルはデバイスにおき、それを直接編集する。

というものです。この場合、保存ごとにおこる自動実行の問題が生じるわけですが、実はこの自動実行を無効化する方法があります。書いているコードの最初に、

import supervisor

# prevent auto-reload
supervisor.runtime.autoreload = False

と書いておくと、保存に伴う自動実行を無効化できます。

では、実行はどうするか、ですが、シリアルモニターからControl+Dを送信することで実現できるようになっています*。

*これも私の環境のせいなのか、扱うコードによるのか、いまひとつはっきりしませんが、VSCのSERIAL MONITORの右下にある、Control + Dが動かないことが多かった。その場合、デバイスの再接続でのりきりました。

編集中のコードをどこに置くかは、どちらが良い、悪い、というものではないので、スタイル、として説明しました。実際に開発を始めると、それぞれの良さ、困るかもしれない点がわかると思います。

編集するファイルの場所良い点困るかもしれない点
スタイル1パソコン上デバイス側へのコピー時が実行時。デバイスへの書き込み回数も減らせるし、最終コードはいつもパソコンにある。デバイスへのドラッグドロップが意外と面倒。コード名をcode.py (またはmain.py)としないと実行されないので、注意が必要。
スタイル2デバイス上デバイスにあるファイルがいま開発しているファイル。わかりやすい。パソコン上にバックアップを取っておかないと、デバイスが壊れた時にコードも失うことになる。頻回の書き込みによるメモリのダメージも杞憂かもしれないけど心配。

実際に二つのスタイルを試してみて、どちらを採用するか、決めてください。どちらか一つにしなければいけない、というものでもないので、実際には両方使う、ということになりますが、軸になるやりかた、は決めておくといいと思います。(何度も書きますが、デバイス上で実行させたいメインコードは、code.pyあるいはmain.pyという名前にしておく必要があります。動かないぞ、なんで、なんで、と思ったらPC上で作った別名のままだった、ということがよくおこりますので、ご注意ください)

工作の進め方

最終的に作りたい、メッセージ見て!ガジェットで使いたい要素は、

  1. チャットグループに投稿された”/メッセージ”をtelegram botで受け取る
  2. サーボモーターを動かす
  3. ボタンが押されたことを検知する
  4. チャットグループにtelegram botで投稿する

です。要素ごとに実現できることを確認し、最後にそれらを組み合わせて完成させましょう。

pinouts

ピンの名前ですが、本体に一番近い列に書かれている番号か、その一つ外側に書かれている名前で呼びます。例えば、1番あるいはGP0という具合に。

pi pico 2 Wと、pi pico Wの両方のピンアウトダイアグラムを載せます。

工作1:サーボモーターを動かす

回路

図2:Sermvomotorテスト用の回路

Servomotorへの電源電圧は5Vからとります。40番のピン、VBUSUSB(5V)に直接つながっていますので、それをレールに流してservoへ引いています。Servomotorへのsignalは、GP13を使いました。pi picoのロジック電圧は3.3Vですが、servoへの入力としてそのまま使えます。

キャパシター(220uF)は、今回の動作と、servoの数なら、なくても大丈夫です。つけるとしたら、というときの参考です。

図3:Servomotorへの接続

Servomotorへの接続は間違えやすいので気を付けましょう。

コード

Servomotorの制御

Servomotorを使うときは、スペックを調べるようにしましょう。

Servoの型番 datasheet (あるいは specification)

などで検索すると情報が得られます。

今回わたくしが使うのは、TS90Aという型番のものです。調べてみると、こんな情報が得られました。

https://stelltron.education/product/9g-micro-servo-motor-type-ts90a/より

特に知りたいのはControl Signalに書かれている、

PWM 50HZ/0.5-2.5MS

です。50Hzのパルス間隔、すなわち、1秒に50回、”ON”のシグナルを送るようなタイミングで、0.5から2.5 msの範囲の”ON”の長さで使ってね、ということです。1秒に50回ということは、パルスとパルスとの間隔が1秒/50=0.02秒、すなわち20 msということです。20 msごとに”ON”の合図が来る。で、”ON”にしている長さを0.5 msから2.5 ms、マイクロ秒でいえば500 usから2500 usの間にしてね、ということです。

コード中では動きを角度で指定します。指定した角度と実際にモーターが表現する角度とをできるだけ一致させるためにこの両端の値が重要になります。

Servomotorは、この、一回の合図で行う”ON”の長さ(パルス幅)を変えることで回転角度をコントロールするようになっています。短いほうが0度(あるいは-90度とも言えます。その場合真ん中が0度)、長いほうが180度に対応します。真ん中が90度。説明図を描いてみましたのでご覧ください。このminとmaxの幅を、datasheetに書かれているパルス幅の範囲を参考に設定します。

図4:Servomotorの制御の説明

実際にServomotorを使ってみるとわかるのですが、どこかからコピペしてきたコードをそのまま使うとあれ?180度より狭いぞ、ということがよく起こります。Servoによって使えるパルス幅のレンジが違っているので、お使いのservoにあった範囲をコード側で調整する必要があります。通常デフォルトの値はたいてい安全側になっていて、180度より狭い範囲になることが多いです。というのは、もし180度よりも大きく動かす指令を出すことになると、servoが動ける範囲以上に動こうとして、損傷してしまうことになるからです。

まずはデフォルトで始め、動く幅を見て、徐々に範囲を広げてservoが悲鳴(無理してる音)をあげるちょい手前に収める、でもいいのですが、datasheetを調べるのが確実です。情報が得られない場合には1000 us – 2000 usぐらいからはじめて、100 usづつぐらい広げていき、servoがイテテテというところを見つけ、50 usぐらい狭めて微調整します。実際はデータシートの値に従っても若干動きの範囲が足りなかったりしますので、試行錯誤での微調整は、たいてい必要です。パルス幅の範囲をコードどこで設定するのかは、のちほど説明します。

Servoを動かすために必要なライブラリのインストール

Servomotorをコントロールするためのライブラリ、Adafruit_motorをpi picoにインストールします。上で説明した、Bundleライブラリ、ダウンロードしたzipファイルを展開したフォルダ(たとえばadafruit-circuitpython-bundle-10.x-mpy-20260116)の中に、libという名前のフォルダがあります。その中に、

Adafruit_motor

という名前のフォルダがあるはずです。このフォルダ自体を、pi picoのlibフォルダの中にコピーします。これがcircuitpythonのライブラリをpi picoにインストールする方法です。ライブラリによってはフォルダではなく、.mpyで終わる一つのファイルであることもあります。(telegram botのテストで用いるadafruit_requestsとadafruit_connection_managerはともに.mpyで終わる一枚のファイルです)

sweepさせてみるコード

LEDでいえばLチカにあたるものです。軸だけでは回転角度がわかりづらいので、servoに一緒についてくる腕、というか、十字のものでもいいんですが、を付けて動かしてみましょう。

下に示すコードを用意します。繰り返しになりますが、circuitpythonでは、デバイス上で動かしたいメインコードのファイルは、code.pyあるいはmain.pyという名前にします。

この記事ではいくつかコードを作っていきますが、区別のためにそれぞれに固有の名前をつけています。pi picoで実行したい時にcode.pyという名前のファイルにしてpi picoにコピー(スタイル1)あるいは、pi pico上にあるcode.pyという名前のファイルをPC上のエディタで直接編集(スタイル2)してください。

ようやくコードです。0度から180度の間を行ったり来たりさせるだけのものです。

  • パルス間隔50Hz
  • パルス幅のレンジ(min, max)

がどこでどう設定されているかを見てください。パルス間隔50Hzは、

pwm = pwmio.PWMOut(board.GP13, duty_cycle=2 ** 15, frequency=50)

パルス幅のレンジは、

my_servo.set_pulse_width_range(min_pulse=my_min, max_pulse=my_max)

で指定しています。

わたくしの場合、幅については、datasheetで500-2500 usという情報をwebから得ていましたが、実際に試すと動きが少し狭かったので、さらに微調整を繰り返し、servoが悲鳴を上げず、かつ、180度に近い広がりを持つように試行錯誤で決めました。最終的に、

my_min, my_max = 480, 2620  # my tuning

で落ち着きました。

この数字をそのまま使うと用いるservoによっては悲鳴を上げたり場合によっては壊れる可能性があります。まずは用いるservomotorのdatasheetを調べ、狭めの範囲から始めるようにしてください。

このコードは、まさに微調整して範囲を決めるために使えます。my_min, my_maxのところの数字を変えて、保存、実行を繰り返し、様子を見ながら数字を調節していってください。

# code-sweep.py   <-- 区別のための別名。pi pico上ではcode.pyとする。
# sweep servo back and forth

import time
import board
import pwmio
from adafruit_motor import servo

# create a PWMOut object on pin GP13
# set frequency to 50hz for a servo
# duty_cycle range is 0 (off) to 65535 (fully on)
# 2**1 - 1 = 0 -> 0% duty cycle
# 2**16 -1 = 65535 -> 100% duty cycle
# 2**15 is about half of 65535 -> about 50% duty cycle
# 2**15 here is a place holder, it will be overriden by the servo object.
pwm = pwmio.PWMOut(board.GP13, duty_cycle=2 ** 15, frequency=50)

# Create a servo object, my_servo.
my_servo = servo.Servo(pwm)
# 750, 2250 are defaults
# my_min, my_max = 500, 2500  # from TS90A datasheet
my_min, my_max = 480, 2620  # my tuning
my_servo.set_pulse_width_range(min_pulse=my_min, max_pulse=my_max)

# main loop
while True:
    for angle in range(0, 180, 5):  # 0 - 180 degrees, 5 degrees at a time.
        my_servo.angle = angle
        time.sleep(0.05)
    time.sleep(1)  # 180度まで来たら1秒休む。
    for angle in range(180, 0, -5): # 180 - 0 degrees, 5 degrees at a time.
        my_servo.angle = angle
        time.sleep(0.05)
    time.sleep(5)  # 180度の側とは区別するため、0度まできたら長めに休むようにしておく。

ということで、servoの動かし方と、パルス幅の調整が終わりました。次へ進みましょう。

工作2:ボタンを使う

ボタンが押されたことを検出する部分を作ってみましょう。

tactile switchについて

tactile switchは、押したときにクリックが指にフィードバックとして伝わるボタンスイッチです。下の図のように足が4本出ています。

図5:Tactile Switchの足たち。説明のために足に番号をつけた

スイッチというと、押したときにはじめてどこかがつながる、それまではどこもつながっていない、ということを想像しますが、tactile switchの場合は、ボタンを押さない状態で、1と2,3と4がつながっています。ボタンを押すと、その1と2のつながりと、3と4のつながりとがつながります。すなわち、全部がつながります。と言うとよくわからない感じになってしまうのですが、ボタンを押したとき、1と3がつながる、2と4がつながる、1と4がつながる、3と2がつながる、と言われると、いろいろに使えるなそりゃ、という感じがします。

このボタンを使って、ボタンが押されたことをpi picoで読み取る、をやってみましょう。

pull upとpull down

ボタンが押されたことを検知する場合、

  • ボタンが押されたらLOW
  • ボタンが押されたらHIGH

のどちらかをpi picoのpinで検知します。ボタンが押されたらLOW、を検知する場合は、押される前にはHIGHがpinに読まれるようにし、ボタンが押されたらHIGH、を検知する場合はその逆で、押される前にはLOWがpinに読まれるように回路を組みます。

次の回路を見てください。pi picoの19番、GP14でHIGH, LOWの読み取りをしようとしています(緑)。

左の写真ではボタンの右下(図5に合わせて足4ということにします)が(+)レール(pi picoからの3.3V)につながっています(オレンジ)。

右ではボタンの右下(足4)はGNDにつながっています(黒)。

図6:こんな回路でどうか

左は、ボタンが押されたらHIGH、右はLOWが読まれるようにしよう、という考えです。それでいいと思えますが、ボタンが押されていないときのことを考えてみますと、GP14の先が宙ぶらりんになっているのがわかります。回路のどこにも閉じていない工作したひとは、例えば左の場合、HIGHになったら反応してね、とGP14に期待しています。この、HIGHになったら、は、その前の状態がLOWであることを暗に期待しています。すなわち、GP14がLOWからHIGHへの変化を検出するためには、ボタンを押されるまではGP14のよみとりが確実にLOW、という状態に置かれる必要があります。いまLOW、まだLOW、で、ボタンが押されたときにおおこれは、HIGHだ、と言えるような仕組みにしておくことが肝心です。図のようにGP14が宙ぶらりんの状態だと、GP14がボタンを押される前の状態を確定できるかどうかに疑問が残ります。宙ぶらりんということは何も読んでいない。何も読んでいないのに、LOWあるいはHIGHの状態を確定することはむずかしい。pi pico側の何かのノイズで、ボタンを押していないのに状態変化しました!が検出されてしまう可能性がでてくる。

[たとえばなし] pinさん、 寒くなったら報告してね、といわれ、温度計を見たら5度だったとします。すぐに寒いって言うべきでしょうか?試しに寒い、と報告したら、5度は寒くない!そんなのノイズだ!と叱られました。何度なら寒いんでしょうか?基準が欲しい。そんなpinさんを、宙ぶらりんにせず、基準を与えておくのがpull-up/downの考え方です。

上の図にpull-down, pull-upの考えを入れたバージョンの回路が下の写真です。

図7:外部抵抗を用いたpull-down(左)とpull-up回路(右)

左の場合はGP14が読んでいるレーン(ボタンの足2のつながっているところ)を、10KΩの抵抗を介してGNDにつないでいます。こうすると、GP14は常にGNDを読むことになり、確実にLOWの状態読み取りを維持します。このようにして読み取りpinの状態をLOWの状態にたもつ仕組みをpull-downと呼びます。ボタンが押されると一瞬(+)につながることになり、このときpinはLOW->HIGHの変化をより確実に検知することができます。

同様に右の場合はボタンが押される前の状態がHIGHになるようにしています。pull-upです。この場合はボタンが押されたときに一瞬GNDにpinがつながり、HIGH->HOWの変化が検知される、という仕掛けです。pinに迷いが生じにくくなる、というわけです。

ボタン(tactile switch)の状態読み取りpinは宙ぶらりんにしないようにしましょう。

それでは簡単なコードをみてみましょう。ボタンが押される前後の変化を監視して、変化があったらprint()で知らせる、それだけの実験用コードです。

コード

pull-down version: LOW 👉 HIGHを検知

Pull-downのバージョンです。図7の左のように回路を組みます。ボタンを押したときのHIGHを読むためのバージョン

pin番号を指定してboardのpin objectを作り、DigitalInOut objectを作るときにそれを渡してinputであることを指定します。あとはそのobjectに.valueをつければLOWならFalse, HIGHならTrueが返ります。print()の結果はシリアルに流れます。

pi pico上ではコード名をcode.pyとするのをお忘れなく。

# code-tactile-switch-pull-down.py  <-- 区別のための別名。pi pico上ではcode.pyとする。
# pull-down version with external resistor
# detect LOW to HIGH change when button is pressed

import board
import digitalio
import time

# pin to read the tactile switch
button_pin = board.GP14

# create a digital input for the button
button = digitalio.DigitalInOut(button_pin)
# set the button as an input
button.direction = digitalio.Direction.INPUT

while True:
    # read the button state
    # note: button.value is boolean
    button_state = button.value
    # button.value is True when not pressed
    # button.value is False when pressed
    if button.value:
        print("HIGH!!! Button Pressed!")
    else:
        print("LOW")

    # Small delay to prevent excessive printing in the serial console
    time.sleep(0.2)

pull-up version: HIGH 👉 LOWを検知

Pull-upしておいてボタンを押したときのLOWを読むためのバージョン。図7の右側のように回路を組みます。コードがどう変わるか、pull-downバージョンと見比べてみてください。

# code-tactile-switch-pull-up.py  &lt;-- 区別のための別名。pi pico上ではcode.pyとする。
# pull-up version with external resistor
# detect HIGH to LOW change when button is pressed

import board
import digitalio
import time

# pin to read the tactile switch
button_pin = board.GP14

# create a digital input for the button
button = digitalio.DigitalInOut(button_pin)
# set the button as an input
button.direction = digitalio.Direction.INPUT

while True:
    # read the button state
    # note: button.value is boolean
    button_state = button.value
    # button.value is True when not pressed
    # button.value is False when pressed
    if not button.value:
        print("LOW!!! Button Pressed!")
    else:
        print("HIGH")

    # Small delay to prevent excessive printing in the serial console
    time.sleep(0.2)

Pull-upバージョンの動作の様子を動画で示します。

実は、、、

Pull-up, pull-downを実現するために、抵抗を使って回路を組みましたが、pi picoには外部抵抗を使わずに同様のことを実現するための内部抵抗が用意されています。個人的には外部抵抗を使うほうが好みです。というのは組んだ回路を見たときにpullがされていることを見て取れるからです。

ここでは参考のため、内部抵抗を使ってpullを実現するためコードを示します。コードはinternal pull-upを使う例です。回路は、図6(右)のように、ボタンを押したら読み取りpinがGNDにつながるように組んでおきます。

# code-tactile-switch-pull-up-internal.py  <-- 区別のための別名。pi pico上ではcode.pyとする。
# pull-up version with internal resistor
# detect HIGH to LOW change when button is pressed

import board
import digitalio
import time

# pin to read the tactile switch
button_pin = board.GP14

# create a digital input for the button
button = digitalio.DigitalInOut(button_pin)
# set the button as an input
button.direction = digitalio.Direction.INPUT

# --- このようにしてinternal pull-up抵抗を有効にする ---
# enable the internal pull-up resistor
button.pull = digitalio.Pull.UP

while True:
    # read the button state
    # note: button.value is boolean
    button_state = button.value
    # button.value is True when not pressed
    # button.value is False when pressed
    if not button.value:
        print("LOW!!! Button Pressed!")
    else:
        print("HIGH")

    # Small delay to prevent excessive printing in the serial console
    time.sleep(0.2)

ということで、ボタン一つ使うにもいろいろ勉強できますね。

次はtelegram botを使う部分に移りましょう。

tactile buttonや抵抗がない場合

Tactile buttonがなくても実験はできます。

下の図8のように回路を組み、緑とオレンジの先同士(AとB)を一瞬タッチさせればボタンと同じことができます。

図8:tactile switchがない場合の回路(pull-down)

抵抗もないよ、という場合には、内部抵抗を使いましょう。上で紹介した、internal pull-upを使う例のコードを使い、回路は下の図9のように組み、緑と黒の先同士(AとB)を一瞬タッチさせれば実験できます。

図9:抵抗もない場合の例。internal resistorでpull-upする場合の回路。

pull-upか、pull-downか

よく見かけるのはpull-upです。慣例的に、とか、トランジスタの特性上pull-upが多く使われた、というのが理由であるようです。ひとまず自分のデフォルト、を決めておくといいと思います。あと、どちらにすればコードのロジックが読みやすくなるかで決めるのも大切だと思います。if button.valueと、if not button.valueは読むときの抵抗が少し違います。特に複数のボタンやスイッチの状態を組み合わせたロジックを書く場合には、コードが読みやすくなるのはどちらかを考えて決めるのは大切だと思います。

工作3:telegram bot召喚

Servomotor, buttonときました。あとはtelegram botの動かし方がわかれば、メッセージ見て!ガジェットを組むことができます。

telegram botを使うために、以下の二つを用意します。

  • bot token
  • botを入れたチャットグループのchat id

これら二つの取得方法はすでに記事にしてありますので、そちらを参照してください。

簡単に流れだけをリストします。

  • botを用意、なければ作る
  • bot tokenをBotFatherに教えてもらう
  • botの入ったチャットグループを作る
  • 作ったチャットグループのchat idを調べる

コード

以下の二つのライブラリが必要になります。pi picoのlibフォルダにコピーしておきます。(どこにあるのそれ?->先にダウンロードしたライブラリのバンドル、adafruit-circuitpython-bundle-10.x-mpy-20260116.zipを展開した中にlibというフォルダがあります。その中から見つけてください)

  • adafruit_requests.mpy
  • adafruit_connection_manager.mpy

また、メインコードが動くために必要な、補助的ファイルを二つ準備します。

  • settings.toml
  • ssl_cert.py

まず、二つの補助ファイルについて説明します。

settings.toml

bot token, chat idに加え、接続したいwifiのアクセスポイント名接続パスワードをこのファイルに書き込んでおきます。通常、credentials(パスワードやbot tokenなど)はメインコードには書き込まないようにするのが鉄則です。いくつかやり方がありますが、バージョン8.0.0以降のcircuitpythonではsetting.tomlをcredentialの記述場所として使うことができるようになっていますので、その方法に従います。

Credentialを書き込んだファイルは、誤ってそのまま共有してしまわないように注意します。Gitでファイルを管理しているならば、.gitignoreにsettings.tomlを書いておきましょう。

次のようにsettings.tomlファイルを用意してご自分の環境の情報を書き入れてくださいファイルを保存して、pi pico側にコピーしておきます。テストコードを動かすために必要です。

#
# WiFi Details
#
WIFI_SSID = "ここにアクセスポイント名"
WIFI_PASSWORD = "アクセスポイントへの接続パスワード"

#
# Telegram
#
botToken = "ここにBotFatherに教えてもらったbot token"
chatID = "APIをたたいてしらべたchat id。マイナスサインから始まります"

ssl_cert.py

Pi picoとtelegramのサーバーの間のssl通信に必要なcertificatesを二つ含みます(intermediateとroot)。Google検索、GitHub copilitに助けてもらいつつ、あれこれ試行錯誤してようやく動くものを得ることができました。私のGitHubからダウンロードしてpi pico側にコピーしておきます。

テストコード

botを使ってできるようになりたいことは、以下の二つです。

  1. chat groupに送信されたメッセージを読む
  2. メッセージをchat groupに送信する

botは半角のスラッシュ(‘/’) から始まるメッセージ(スラッシュメッセージ)だけを読むことができます。人間のメンバー同士がおしゃべりしているスラッシュなしの会話はよみとりません。botに読ませたいメッセージは/につづけて書くようにします。

次の動作をさせることで、上の2つがカバーされるコードを書きました。

  1. pi pico起動、メインコード開始で、「準備完了!」メッセージをbotが送信
  2. スラッシュコマンドで、/うみ と送信すると「やま!」とbotが返事。

2ができるということは、メッセージの受信、送信両方ができている、ということになります。1はおまけです。

下のコードをcode.pyとしてpi picoにコピーし、動かしてみてください。

チャットの様子を下に示します。

図10:チャットの様子

# code-bot-test.py <-- 区別のための別名。pi pico上ではcode.pyとする。
# telegram bot test code
# credentials are stored in a separate file

# place the ssl_cert.py file in the same directory

import os
import socketpool
import ssl
import time
import wifi
from ssl_cert import ssl_cert
import adafruit_requests  # add this to the pi pico lib folder


def load_credentials():
    """
    Load Wi-Fi and Telegram bot credentials from settings.toml
    environment variables.
    """
    # Wi-Fi
    ssid = os.getenv("WIFI_SSID")
    password = os.getenv("WIFI_PASSWORD")
    # Telegram Bot
    telegrambot = os.getenv("botToken")
    chat_id = os.getenv("chatID")
    return ssid, password, telegrambot, chat_id


def connect_wifi(ssid, password):
    """
    Connect to Wi-Fi access point and create socket pool
    """
    print("Initializing...")
    wifi.radio.connect(ssid, password)
    print("connected to WiFi AP!\n")
    pool = socketpool.SocketPool(wifi.radio)
    print(f"IP Address: {wifi.radio.ipv4_address}", end='')
    print(f" '{ssid}' \n")
    return pool


def create_requests_session(pool):
    """
    Create requests session with SSL context using GoDaddy root certificate
    """
    print("creating ssl context...")
    ssl_context = ssl.create_default_context()
    ssl_context.load_verify_locations(cadata=ssl_cert)
    requests = adafruit_requests.Session(pool, ssl_context)
    print("ssl context created!\n")
    return requests


def check_for_commands(requests, telegrambot, chat_id, offset):
    """
    Check for new messages from Telegram bot using long polling.
    """
    print("Checking for new messages...")
    # Long polling: timeout=30 keeps connection open for up to 30 seconds
    url = f"https://api.telegram.org/bot{telegrambot}/getUpdates?timeout=30&offset={offset}"
    try:
        response = requests.get(url)
        messages = response.json()
        #print("Type of messages:", type(messages))
        #print(f"Response: {messages}\n")
        if messages["ok"] and messages["result"]:
            # Process all updates in this batch
            for result in messages["result"]:
                update_id = result["update_id"]
                message = result.get("message", {})
                text = message.get("text", "")
                chat = message.get("chat", {})
                if str(chat.get("id")) == chat_id:
                    print(f"Received message: {text}")
                    if text.startswith("/"):
                        # Return command and next offset (current update_id + 1)
                        return text, update_id + 1
                # Update offset even if message is not for us
                offset = update_id + 1
            # Return None command but updated offset
            return None, offset
    except Exception as e:
        print(f"Error getting updates: {e}")
    # Return None command and same offset on error
    return None, offset


def send_message(requests, telegrambot, chat_id, text):
    """
    Send a message via Telegram bot
    """
    print(f"Sending message: {text}")
    url = f"https://api.telegram.org/bot{telegrambot}/sendMessage"
    payload = {
        "chat_id": chat_id,
        "text": text
    }
    response = requests.post(url, json=payload)
    if response.status_code == 200:
        print("Message sent successfully!")
    else:
        print(f"Failed to send message: {response.status_code}")


def handle_command(requests, telegrambot, chat_id, command):
    """
    Handle received command and send appropriate response
    """
    return_message = ""
    if command == "/うみ":
        return_message = "やま!"
    else:
        return_message = f"I do not understand '{command}'"
    send_message(requests, telegrambot, chat_id, return_message)


def main():
    # Load wi-fi and telegram bot credentials
    ssid, password, telegrambot, chat_id = load_credentials()
    # Connect to Wi-Fi and create requests session
    pool = connect_wifi(ssid, password)
    requests = create_requests_session(pool)

    # Notify that bot is ready
    send_message(requests, telegrambot, chat_id, "準備完了!")

    # Main loop with long polling
    offset = 0  # Start from the beginning, or use None to get recent updates
    print("Starting bot with long polling (30s timeout)...\n")

    # main loop
    while True:
        command, offset = check_for_commands(requests, telegrambot, chat_id, offset)
        if command:
            handle_command(requests, telegrambot, chat_id, command)
        # No sleep needed - long polling handles timing
        # Only brief delay on errors to avoid hammering the server
        time.sleep(1) if command is None and offset == 0 else None


if __name__ == "__main__":
    main()

code-bot-test.pyについて

それほど凝ったことはしていないのに、結構長いコードになりました。最初にwifiへ接続したり、ssl通信用のオブジェクトを作るところが煩雑です。ssl certをpi pico上に用意しておくところもわかりづらく、raspberry piやPC、ESP32でtelegram botを使うときにはそんなの用意しなかったよな、なんで、なんで、と思いながら理解不足と戦ったので時間がかかりました。メッセージもjsonの中身を順にたどっていってchat idが正しいことを確認したり、同じメッセージを繰り返し読まないようにoffsetをincrementしたりしているのでごちゃごちゃして見えます。APIはsendMessage、getUpdatesを使います。Telegramのサーバーからpushでメッセージが送られてくるようにするにはpi picoは役不足ですし、少し高度な環境セットアップが必要なので、ここではgetUpdatesでこちらからお伺いをたてるようにしています。できるだけノックの回数を減らすためにtimeout=30を使うという工夫は、GitHub copilotが教えてくれました。そうすることである程度リアルタイム性も確保しつつトラフィックを減らせるという、すぐれた方法です。

工作4:メッセージ見て!ガジェット

ここまでで、Servomotorを動かす、ボタンが押されたことを検知する、telegram botでメッセージの送受信をする、がすべてできるようになりました。これらを組み合わせてガジェットを工作しましょう。

回路

Servomotor駆動用に上の電源レーンを5Vとします。

  • pin40 (VBUS, 5V, USBの5Vに直接つながっている) -> レーン赤
  • pin38 (GND) -> レーン青
  • キャパシタがあれば長い脚をレーン赤、短い方をレーン青

ボタンロジック用に下の電源レーンを3.3Vとします。

  • pin36 (3V3OUT) -> レーン赤
  • pin3 (GND) -> レーン青

Servomotor(駆動は5V、シグナルは3.3Vロジック

  • 上の電源レーン赤(5V)-> (+, 赤)
  • 上の電源レーン青(GND)-> (-, 茶色)
  • pin17 (GP13, OUTPUT) -> (signal, オレンジ)

Tacticle button(ロジックはすべて3.3V

  • pin19 (GP14, INPUT) -> 足2
  • 足2の列 -> 下の電源レーン赤(3.3V)
  • 足4 -> 下の電源レーン青(GND)

本質的ではないことなのですが、私は5Vには赤、3.3Vにはオレンジ、GNDには黒のジャンパーワイヤーを使うようにして、色で電源周りが確認できるようにしています。そのほかの信号の流れには白、黄色、緑、紫、茶色を使い、電源周りに使った色は使わないようにしています。(この習慣でいくと、Servoのシグナルラインがオレンジ、(-)が茶色なのが少し気に入らないが、電源ラインが赤で5V、暗めの色が(-)ということで、なんとか理解している。モノによって線の色が違うので、毎回調べなおしている)

図11:メッセージ見て!ガジェット用の回路組み立て

工作例

Legoのプレートとブロックを使い、こんな感じに工作しました。Legoブロックは何かを形にするときに本当に便利です。ちょっと大きくしよう、少し高くしよう、ケーブルがはみ出ないように隙間で止めよう、そんな試行錯誤が簡単にできるところが最高です。この段階ではまだプロトタイピングなので、輪ゴムで止めたり耐震マットでひとまず動かないようにボードを固定したりしています。実際動作させてみると、どんどんアイデアが浮かびます。とにかく触って動かせる形にする、動かしてみる、思いついたことを形にし直す、動かしてみる、のサイクルを繰り返しましょう。

アームには鈴もぶら下げて、目と耳からお知らせが入るようにしました。

図12:メッセージ見て!ガジェット工作例

コード

何をしたいか。シナリオを想像します。

  • What’s upやLineで家にいる家族に連絡しているのになかなか返事がない。そもそもメッセージを見てくれていないようだ。
  • telegramで’/めみ’と書いて送信(っせーじて の略)
  • ガジェットのアームが上がってお知らせ。ガジェットはリビングにおいてある。家族はリビングにいるはず。
  • ガジェットの見たよボタンを押すと、アームが下がって「誰かがボタンを押ししました。確認完了。」メッセージがtelegramに送信される。

それではコードです。コードを書いて試しに動かしている間に、いくつかのことを思いついたので追加しました。

  • アームが一回上がるだけでは見逃される可能性が高いので、何度も上がり下がりをするようにした。看板を見せて3秒、下がって1秒を繰り返すようにした。
  • もし、誰も気が付いてボタンが押されないとすると、いつまでもアームが動き続けることになってしまう。そこで、タイムアウトを設定して、決められた時間内にボタンが押されなければ動作を終了するようにした。

少し工夫が必要だったのは、servomotorを動かし続けながらボタンが押されたことをどうやって検出するか、という部分です。Servomotorを一定時間、動かし、待ち、動かしてまた待つ。それを繰り返すだけなら簡単ですが、繰り返している途中に絶えずボタンが押されたかな、を見張っている必要がある。ほかにもやり方があると思いますので、研究してみてください。

単純なことなのに、いざ実現しようとすると立ち止まってしまうことがよくありますが、できないことができるようになるいいチャンスだと考えて、粘るのがコツです。たいていのことは誰かが似た問題にぶちあたり、誰かの助けで解決に至っているはずですし(stack overflowなどが役立ちます)、今ならAIエージェントに実現したいこと、問題になっていることをうまく伝えれば、唸ってしまうような解決法を示してくれます。このコードはGitHub copilotに手伝ってもらって書きました。

# code-memi-gadget.py <- 区別のための別名。pi pico上ではcode.pyとする。
# メッセージ見て!ガジェット
# credentials are stored in a separate file

# place the ssl_cert.py file in the same directory

import board
import digitalio
import os
import pwmio
import socketpool
import ssl
import time
import wifi
from ssl_cert import ssl_cert
import adafruit_requests  # add this to the pi pico lib folder
from adafruit_motor import servo

#
# globals
# my_servo and button variables are accessed in multiple functions
# so I define them in module scope (global scope)
#
# create a PWMOut object on pin GP13
pwm = pwmio.PWMOut(board.GP13, duty_cycle=2**15, frequency=50)
my_servo = servo.Servo(pwm)
# 750, 2250 are defaults
# my_min, my_max = 500, 2500  # from TS90A datasheet
my_min, my_max = 480, 2620  # my tuning
my_servo.set_pulse_width_range(min_pulse=my_min, max_pulse=my_max)

# create pin to read the tactile switch
button_pin = board.GP14
button = digitalio.DigitalInOut(button_pin)
button.direction = digitalio.Direction.INPUT
# button.pull = digitalio.Pull.UP

REST_ANGLE = 90
UP_ANGLE = 180
UP_PAUSE_SECONDS = 3
REST_PAUSE_SECONDS = 1
BUTTON_POLL_SECONDS = 0.05
MAX_ACTIVE_SECOND = 30  # 30はテスト用


def load_credentials():
    """
    Load Wi-Fi and Telegram bot credentials from settings.toml
    environment variables.
    """
    # Wi-Fi
    ssid = os.getenv("WIFI_SSID")
    password = os.getenv("WIFI_PASSWORD")
    # Telegram Bot
    telegrambot = os.getenv("botToken")
    chat_id = os.getenv("chatID")
    return ssid, password, telegrambot, chat_id


def connect_wifi(ssid, password):
    """
    Connect to Wi-Fi access point and create socket pool
    """
    print("Initializing...")
    wifi.radio.connect(ssid, password)
    print("connected to WiFi AP!\n")
    pool = socketpool.SocketPool(wifi.radio)
    print(f"IP Address: {wifi.radio.ipv4_address}", end="")
    print(f" '{ssid}' \n")
    return pool


def create_requests_session(pool):
    """
    Create requests session with SSL context using GoDaddy root certificate
    """
    print("creating ssl context...")
    ssl_context = ssl.create_default_context()
    ssl_context.load_verify_locations(cadata=ssl_cert)
    requests = adafruit_requests.Session(pool, ssl_context)
    print("ssl context created!\n")
    return requests


def check_for_commands(requests, telegrambot, chat_id, offset):
    """
    Check for new messages from Telegram bot using long polling.
    """
    print("Checking for new messages...")
    # Long polling: timeout=30 keeps connection open for up to 30 seconds
    url = f"https://api.telegram.org/bot{telegrambot}/getUpdates?timeout=30&offset={offset}"
    try:
        response = requests.get(url)
        messages = response.json()
        # print("Type of messages:", type(messages))
        # print(f"Response: {messages}\n")
        if messages["ok"] and messages["result"]:
            # Process all updates in this batch
            for result in messages["result"]:
                update_id = result["update_id"]
                message = result.get("message", {})
                text = message.get("text", "")
                chat = message.get("chat", {})
                if str(chat.get("id")) == chat_id:
                    print(f"Received message: {text}")
                    if text.startswith("/"):
                        # Return command and next offset (current update_id + 1)
                        return text, update_id + 1
                # Update offset even if message is not for us
                offset = update_id + 1
            # Return None command but updated offset
            return None, offset
    except Exception as e:
        print(f"Error getting updates: {e}")
    # Return None command and same offset on error
    return None, offset


def send_message(requests, telegrambot, chat_id, text):
    """
    Send a message via Telegram bot
    """
    print(f"Sending message: {text}")
    url = f"https://api.telegram.org/bot{telegrambot}/sendMessage"
    payload = {"chat_id": chat_id, "text": text}
    response = requests.post(url, json=payload)
    if response.status_code == 200:
        print("Message sent successfully!")
        # adding this part removes duplicate message executed
        # in run_servo_until_button() function
        # I do not know why this removes the duplicate like a magic.
        try:
            data = response.json()
            message_id = data.get("result", {}).get("message_id")
            print(f"Telegram message_id: {message_id}")
        except Exception as e:
            print(f"Failed to parse response JSON: {e}")
        # --------------------------------------------- magic end
    else:
        print(f"Failed to send message: {response.status_code}")


def is_button_pressed():
    """
    Return True when button is pressed.
    Button wiring is pulled-up, so pressed = ground = False.
    """
    return not button.value


def wait_with_button_check(seconds):
    """
    Wait for the given seconds, but exit early if button is pressed.
    Returns True if button was pressed during wait.
    """
    start_time = time.monotonic()
    while (time.monotonic() - start_time) < seconds:
        if is_button_pressed():
            return True
        time.sleep(BUTTON_POLL_SECONDS)
    return False


def run_servo_until_button(requests, telegrambot, chat_id):
    """
    Move servo between 90° and 180° until button is pressed.
    When pressed, return servo to rest and send a confirmation message.
    """
    my_servo.angle = REST_ANGLE
    start_time = time.monotonic()

    while True:
        if (time.monotonic() - start_time) >= MAX_ACTIVE_SECOND:
            my_servo.angle = REST_ANGLE
            msg = "タイムアウトです。誰も反応してくれませんでした。"
            msg += "サーボモーターの動作を止めました。"
            send_message(
                requests,
                telegrambot,
                chat_id,
                msg,
            )
            return
        my_servo.angle = UP_ANGLE
        if wait_with_button_check(UP_PAUSE_SECONDS):
            break
        my_servo.angle = REST_ANGLE
        if wait_with_button_check(REST_PAUSE_SECONDS):
            break

    my_servo.angle = REST_ANGLE
    msg = "誰かがボタンを押ししました。確認完了。"
    send_message(
        requests, telegrambot, chat_id, msg
    )


def handle_command(requests, telegrambot, chat_id, command):
    """
    Handle received command and send appropriate response
    """
    return_message = ""
    if command == "/めみ":
        msg = "めみ起動!確認ボタンが押されるまでサーボが動作します。その間"
        msg += "メッセージを送っても反応しません。"
        msg += f"(最大動作時間は{MAX_ACTIVE_SECOND}秒です)"
        send_message(requests, telegrambot, chat_id, msg)
        run_servo_until_button(requests, telegrambot, chat_id)
        return
    if command == "/うみ":
        return_message = "やま!"
    else:
        return_message = f"I do not understand '{command}'"
    send_message(requests, telegrambot, chat_id, return_message)


def main():
    # I want to make sure that the servo is at rest angle at start
    my_servo.angle = REST_ANGLE
    # Load wi-fi and telegram bot credentials
    ssid, password, telegrambot, chat_id = load_credentials()
    # Connect to Wi-Fi and create requests session
    pool = connect_wifi(ssid, password)
    requests = create_requests_session(pool)

    # Notify that bot is ready
    send_message(requests, telegrambot, chat_id, "準備完了!")

    # Main loop with long polling
    offset = 0  # Start from the beginning, or use None to get recent updates
    print("Starting bot with long polling (30s timeout)...\n")

    # main loop
    while True:
        command, offset = check_for_commands(requests, telegrambot, chat_id, offset)
        if command:
            handle_command(requests, telegrambot, chat_id, command)
        # No sleep needed - long polling handles timing
        # Only brief delay on errors to avoid hammering the server
        time.sleep(1) if command is None and offset == 0 else None


if __name__ == "__main__":
    main()

こんな風に動きます。こうやって作ってみると、ボタンの手前にワイヤーがあると押しにくいな、とか、右手でボタンを押そうとするとアームに手があたりそうだとか、考えるだけではわからないことが見えてきます。これが考えを形にすることで考えをさらに進める方法です。一歩進めば一歩進んだ分だけ、進む前には見えなかった先が見えるようになります。形にしてみること、手に取ってみること。見て触って聞いて、においもあるなら嗅いでみて、全身全霊で作る。それが楽しい。

チャットに流れるメッセージはこんな感じです。

図13:テスト時のチャットのようす

発展アイデア

メッセージ見て!ガジェットで、こんなことができたらオモシロそうなことをリストします。

  • ビカビカ光るとか、何かがぐるんぐるん回るとか、飛び出すとか、走り回るとか、打ち上るとか、こぼれ落ちるとか、お知らせを派手にする。
  • 各部屋にあるGoogle homeなどから一斉放送でアナウンスが流れるようにする。
  • ボタンを押した人の写真を撮ってチャットに返送し、誰が押したのかが突き止められるようにする。

どうでしょうか?みなさんもいろいろと考えを巡らせてみてください。

pi pico Wとtelegram botを使って、うちで実際に使われた工作を一つ紹介します。

工作例:水やりマシン

少し長めに家を空ける場合、気になるのが植物への水やりです。pi pico Wとtelegram botでservomotorをコントロールする、水やりマシンを作りました。

ポンプなし。サイフォンの原理でアームが下がると水が流れ出るようにしました。アームを上げれば止まるようにタンク位置と水位を調節しました。

pi pico WをRobo Pico (Cytron)に乗せ、servo二つと温度湿度を測るためにDHT22を接続しました。雨やほこりを避けるため、Daisoで買ったプラスチックケースに収めました。

Servoは二つ。一つはアームの上げ下げ、もう一つはアームを左右に振るためのものです。どちらもLegoブロックでハウジングしました。3Dプリンターがほしいんですが、手っ取り早く形をつくるにはLegoが最高です。結構丈夫なものができますし、カラフルなのもいいです(配色は全く気にしてませんが)

図14:水やりマシン2025

チャットの様子。/wが水まきコマンド。スペースに続く数字をコード中で拾い、その秒数だけアームを振るようにしてあります。

図15:水やりマシン2025のチャット画面

動作の様子はこんな具合。どうぶつの森のじょうろでの水まきを思い出します。ぎこちないアームのぷるぷる加減がほどよく散水。

用いたコードはGitHubにあります

https://github.com/misson3/pipicow-telegram

にコードを置いておきましたので参考にしてください。

さいごに

いかがでしたか?長い記事でスクロールが大変ですが、raspberry pi picoで何か作ってみたい、というときに必要になる情報をてんこ盛りにしました。また思い出して見に来てください。

ABOUT ME
misson
ものづくりが趣味。 GitHub: https://github.com/misson3