DeviceScriptでRaspberryPiPicoを扱う時のメモ
Raspbeery Pi PicoをDeviceScriptで操作する時のメモ
- https://microsoft.github.io/devicescript/
- https://github.com/microsoft/devicescript
- https://learn.adafruit.com/devicescript?view=all
ドキュメント通りにコードを書いても動作しないことが割とあるので、DeviceScript自体のソースコードを読んだりnode_modules/@devicescript
配下にある型やソースコードを読む必要が多々ある。
ドキュメントではVSCodeを使いVSCode拡張を利用して開発する方法を推奨しているようで、それがメインで書かれているが自分はVim使いなのでターミナルを利用する方法で開発していく。
よく使うのでピンアウトへのリンクも貼っておく。
DeviceScriptのインストールは以下のコマンドを実行する。
$ npm install -D @devicescript/cli@latest
これでCLIはインストールされるがcoreやi2cなどのビルトインパッケージはインストールされないのでbuildコマンドを実行してインストールする必要がある。
$ ./node_modules/.bin/devicescript --quiet build # もちろんnpxから実行する方法でも良い
npx --yes @devicescript/cli@latest init
で生成されるpackage.jsonのpostinstallにbuildが実行されるように書いてある(確か"postinstall": devicescript --quiet build"
が書いてある)のはこのためのようだ。不思議な作りだ……
そういえばdevsconfig.json
は必須なようなので空のJSONを書いておく。
$ echo '{}' > devsconfig.json
Raspberry Pi PicoにDeviceScriptのランタイムを書くにはflash
サブコマンドを使う。事前にRaspberry Pi Picoをそれのボード上にあるBOOTSELボタンを押しながらUSBで開発機に接続してストレージとして認識されるようにしておく。
$ ./node_modules/.bin/devicescript flash rp2040 --board pico
ボードの指定は必須のようだがrp2040
の指定がなくても書き込めたので、わざわざ指定する必要は無いがヘルプにはこのように書いてあるので一応指定する。
開発していると時々バグってソースコードの書き込みがうまくいかず、以前書き込んだプログラムで延々と起動し続けることがあるので、その場合はResetting Flash memoryのUF2ファイルを書き込んでフラッシュメモリを完全に空にする。再度flash
サブコマンドでランタイムを書き込み直せばおそらく直る。
改めて調べたらflash
コマンドに--clean
というオプションがあり、これを指定すると上記のResetting Flash memory
で配布されているflash_nuke.uf2
を書き込めるようなのでこちらを実行する方が楽だった。
$ ./node_modules/.bin/devicescript flash rp2040 --board pico --clean
開発はdevtools
コマンドで行う。
$ ./node_modules/.bin/devicescript devtools
実行すると http://localhost:8081 にアクセスできるようになる。シミュレーターを使っていたり、実機を接続している場合はデバイスが表示される。
ブラウザで http://localhost:8081 を表示しておらず、console.logがソースコードに書かれている場合はターミナルに表示される(--logging --serial
を付与して起動した場合なのかブラウザで表示していない時なのか?条件がよくわからない)がブラウザで表示している場合にはブラウザのDevTools(あるいはWebInspectorなどブラウザの開発者ツール)のConsoleに表示される。構文エラーや発生した例外もこれらに表示される。
vm
サブコマンドは仮想マシンを起動してコードを確認できるようだがよくわからない。実機がないとLEDだったりデバイスに依存しないコードくらいしか確認できない気はするが、逆にその辺りの確認であればvm
で確認できるのは手軽で良いことなのかもしれない。run
サブコマンドの--test
と合わせて用途によっては便利なのかも。
DeviceScriptはシミュレーターが充実しているので抽象化が上手にできるとdevtools
で起動してブラウザから確認する程度で開発ができるのかもしれない。
オンボードのLEDをチカチカさせる。
setStatusLight
はオンボードのLEDをチカチカさせる関数で、色を指定できる。けれどRaspberry Pi PicoのオンボードLEDは単色だったはずなので律儀に0x00ff00
を指定して緑とする必要はないのかもしれない。フルカラーLEDを搭載している機種なら色々な色にできるのだと思う。
import { delay } from "@devicescript/core";
import { schedule, setStatusLight } from "@devicescript/runtime";
await setStatusLight(0x000000);
schedule(async () => {
// green
await setStatusLight(0x00ff00);
await delay(500);
// off
await setStatusLight(0x000000);
await delay(500);
}, {
timeout: 0,
interval: 500
});
- https://microsoft.github.io/devicescript/samples/status-light-blinky
- https://microsoft.github.io/devicescript/developer/status-light
- https://microsoft.github.io/devicescript/api/runtime/schedule
せっかくなのでschedule
関数を使った。setTimeout
やsetInterval
だと第2引数に0を指定しても4msや16msの間隔で実行されるのだと思うがschedule
はきちんと指定した通りに実行されているように感じた。感じたが、きちんと計測したわけではないので実際はそんなことはないのかもしれない。schedule
関数だとコールバック関数にいくつかの引数が渡されるので用途によっては便利だと思う。
オンボードでないテキトーなGPIOの先に繋いだLEDをチカチカさせる。startLightBulb
を使うとLEDを抽象化してくれるらしく、devtools
で起動したサーバーにアクセスするとLEDを表示してくれる。
この擬似的なLEDは現在の状態を表示してくれるし、このLEDをクリックすることでブラウザからLEDの点灯・消灯を操作することもできる。
ここでは21番ピン(GPIO16のピン)に抵抗とLEDを接続している。
import { pins } from '@dsboard/pico';
import { startLightBulb } from "@devicescript/servers";
import { delay } from "@devicescript/core";
import { schedule } from "@devicescript/runtime";
const led = startLightBulb({
pin: pins.GP16
});
await led.intensity.write(0);
schedule(async () => {
await led.intensity.write(
await led.intensity.read() > 0 ? 0 : 1
);
await delay(500);
}, {
timeout: 0,
interval: 500
});
- https://microsoft.github.io/devicescript/devices/rp2040/pico#pins
- https://microsoft.github.io/devicescript/api/drivers/lightbulb
Raspberry Pi Picoのピン設定については上にも貼ったが https://microsoft.github.io/devicescript/devices/rp2040/pico#pins に書いてある。オンボードLEDであるGPIO25は設定されていない。setStatusLight
でなくGPIO経由で直接扱いたければ https://microsoft.github.io/devicescript/developer/drivers/digital-io にあるように gpio
関数を使って gpio(25)
で取得する。
あるいは https://microsoft.github.io/devicescript/devices/add-board のように新たなボードとして追加するのが良いのかもしれない。
を眺めていて思ったがpins
でなくled.pin
で取得すれば良いのか?🤔
I2C接続したAS5600という磁気エンコーダーから角度を取得する。AliExpressで買った白い四角い基盤に実装されたものを使ってブレッドボードに挿して試している。
- https://www.switch-science.com/products/3493
- https://www.digikey.jp/ja/product-highlight/a/ams/as5600-rotary-sensor
AS5600のピンはそれぞれ以下のように接続する。
AS5600 | Raspberry Pi Pico |
---|---|
VCC | 36ピン / 3V3(OUT) |
GND | 3,8,13,18,23,28,33,38ピンのいずれか |
SCL | 2ピン / GP1 / I2C0 SCL |
SDA | 1ピン / GP0 / I2C0 SDA |
コードは以下のような感じ。
import { pins } from '@dsboard/pico';
import { configureHardware } from "@devicescript/servers";
import { schedule } from '@devicescript/runtime';
import { i2c } from '@devicescript/i2c';
configureHardware({
i2c: {
pinSDA: pins.GP0,
pinSCL: pins.GP1,
inst: 0,
kHz: 1000 // 1MHz / 後述のSSD1306に合わせただけでもっと低くていい気はする
}
});
let prev: number | undefined = undefined;
schedule(async () => {
const buf = await i2c.readRegBuf(0x36, 0x0C, 2);
const angle = (buf[0] << 8) | buf[1];
const degree = ((angle / 4096) * 360) | 0;
if (prev !== undefined && prev !== degree) {
console.log('degree', degree);
}
prev = degree;
}, {
timeout: 0,
interval: 16
});
- https://microsoft.github.io/devicescript/api/clients/i2c
- https://microsoft.github.io/devicescript/developer/drivers/i2c#driver
これでAS5600のICの上で磁石を回転すると角度が出力される。
configureHardware
関数でI2Cのピンを指定する。I2C0とI2C1が使えるはずなのだがDeviceScriptでは指定できないのだろうか……🤔
のあたりを読むとI2C_INST
で切り替えているようだが、I2Cのピンを指定する方法がconfigureHardware
しか見当たらないし、なんらかの理由でI2C0とI2C1を同時に使う方法がわからない。
後述のSSD1306とAS5600を同時に使うとノイズが発生しているのか、AS5600に磁石を近づけていないのに謎の値が送信されてくる……🤔
degree
の値が大きすぎる値が来たりすることもあるのでif
でフィルターをしたりボーレートを下げたりした方が良さそう。
I2C接続したSSD1306に文字を表示する。
startCharacterScreenDisplay
を使うとディスプレイを抽象化してくれるらしく、devtools
で起動したサーバーにアクセスするとディスプレイを表示してくれる。これもまたLEDと同様、表示するテキストをブラウザから変更できる。スクリーンショットでは白いドットが並んでいるが、startCharacterScreenDisplay
でなく画像やドットを扱うディスプレイの関数を使えばおそらく表示されている通りにドットが表示されるのではないかと思う(未確認)。
SSD1306は以下のようにそれぞれピンを接続する。
AS5600 | Raspberry Pi Pico |
---|---|
VCC | 36ピン / 3V3(OUT) |
GND | 3,8,13,18,23,28,33,38ピンのいずれか |
SCL | 2ピン / GP1 / I2C0 SCL |
SDA | 1ピン / GP0 / I2C0 SDA |
import { pins } from '@dsboard/pico';
import { SSD1306Driver, startCharacterScreenDisplay } from "@devicescript/drivers"
import { configureHardware } from "@devicescript/servers";
import { schedule } from '@devicescript/runtime';
import { i2c } from '@devicescript/i2c';
configureHardware({
i2c: {
pinSDA: pins.GP0,
pinSCL: pins.GP1,
inst: 0, // なくても動作したので不要?I2C0を指定しているつもりだがこのフィールド自体optionalだし違うのかも
kHz: 1000 // 1MHz
}
});
const ssd1306 = new SSD1306Driver({
devAddr: 0x3C,
externalVCC: false, // 3.3V
width: 128,
height: 64,
});
const screen = await startCharacterScreenDisplay(
ssd1306,
{
rows: 64,
columns: 128
}
);
let i = 0;
schedule(async ({counter, elapsed, delta}) => {
await screen.message.write(
`i: ${i++}\n` +
`counter: ${counter}\n` +
`elapsed: ${elapsed}\n` +
`delta: ${delta}`
);
}, {
timeout: 0,
interval: 500
});
- https://microsoft.github.io/devicescript/api/drivers/ssd1306
- https://microsoft.github.io/devicescript/api/clients/characterscreen
500ミリ秒ごとにSSD1306の表示が更新される。
await ssd1306.rotate(false);
を実行すると上下反転する。
HIDキーボードとして認識させてボタンをキーボード代わりにする。
startHidKeyboard
でHIDキーボードになれるらしい。なんてお手軽なんだ。GP14を適当なタクタイルスイッチに繋ぎ、タクタイルスイッチの先をGNDに繋ぐ。スイッチを押すとa
が入力される。
import { pins } from '@dsboard/pico';
import {
delay,
HidKeyboardAction,
HidKeyboardModifiers,
HidKeyboardSelector,
} from "@devicescript/core"
import { schedule } from '@devicescript/runtime';
import { startButton, startHidKeyboard } from "@devicescript/servers";
const keyboard = startHidKeyboard({});
const button = startButton({
pin: pins.GP14,
});
button.down.subscribe(async () => {
await keyboard.key(HidKeyboardSelector.A, HidKeyboardModifiers.None, HidKeyboardAction.Press);
});
startButton
はButtonを返す。down
での監視以外にもup
やhold
で監視できる。
あまりに情報がないのでGitHubリポジトリを検索したらサンプルが多数見つかった……
- https://github.com/microsoft/devicescript/blob/45713d8d86aad8afcccc1d1affe6d4fe943fbc0e/packages/sampleprj/src/kbd.ts
- https://github.com/microsoft/devicescript/blob/45713d8d86aad8afcccc1d1affe6d4fe943fbc0e/packages/sampleprj/src/maincopypastebutton.ts
- https://github.com/microsoft/devicescript/blob/45713d8d86aad8afcccc1d1affe6d4fe943fbc0e/devs/samples/hid.ts
- https://microsoft.github.io/devicescript/api/clients/button/
何故か手元のcoc.nvimがHidKeyboardSelectorなどを補完してくれないのでnode_modules配下をgrepしたところ定義が見つかった。リポジトリを検索しても見つからないので別のリポジトリにあるのか、なんなのか……
enum HidKeyboardSelector { // uint16_t
None = 0x0,
ErrorRollOver = 0x1,
PostFail = 0x2,
ErrorUndefined = 0x3,
A = 0x4,
B = 0x5,
C = 0x6,
D = 0x7,
E = 0x8,
F = 0x9,
G = 0xa,
H = 0xb,
I = 0xc,
J = 0xd,
K = 0xe,
L = 0xf,
M = 0x10,
N = 0x11,
O = 0x12,
P = 0x13,
Q = 0x14,
R = 0x15,
S = 0x16,
T = 0x17,
U = 0x18,
V = 0x19,
W = 0x1a,
X = 0x1b,
Y = 0x1c,
Z = 0x1d,
_1 = 0x1e,
_2 = 0x1f,
_3 = 0x20,
_4 = 0x21,
_5 = 0x22,
_6 = 0x23,
_7 = 0x24,
_8 = 0x25,
_9 = 0x26,
_0 = 0x27,
Return = 0x28,
Escape = 0x29,
Backspace = 0x2a,
Tab = 0x2b,
Spacebar = 0x2c,
Minus = 0x2d,
Equals = 0x2e,
LeftSquareBracket = 0x2f,
RightSquareBracket = 0x30,
Backslash = 0x31,
NonUsHash = 0x32,
Semicolon = 0x33,
Quote = 0x34,
GraveAccent = 0x35,
Comma = 0x36,
Period = 0x37,
Slash = 0x38,
CapsLock = 0x39,
F1 = 0x3a,
F2 = 0x3b,
F3 = 0x3c,
F4 = 0x3d,
F5 = 0x3e,
F6 = 0x3f,
F7 = 0x40,
F8 = 0x41,
F9 = 0x42,
F10 = 0x43,
F11 = 0x44,
F12 = 0x45,
PrintScreen = 0x46,
ScrollLock = 0x47,
Pause = 0x48,
Insert = 0x49,
Home = 0x4a,
PageUp = 0x4b,
Delete = 0x4c,
End = 0x4d,
PageDown = 0x4e,
RightArrow = 0x4f,
LeftArrow = 0x50,
DownArrow = 0x51,
UpArrow = 0x52,
KeypadNumLock = 0x53,
KeypadDivide = 0x54,
KeypadMultiply = 0x55,
KeypadAdd = 0x56,
KeypadSubtrace = 0x57,
KeypadReturn = 0x58,
Keypad1 = 0x59,
Keypad2 = 0x5a,
Keypad3 = 0x5b,
Keypad4 = 0x5c,
Keypad5 = 0x5d,
Keypad6 = 0x5e,
Keypad7 = 0x5f,
Keypad8 = 0x60,
Keypad9 = 0x61,
Keypad0 = 0x62,
KeypadDecimalPoint = 0x63,
NonUsBackslash = 0x64,
Application = 0x65,
Power = 0x66,
KeypadEquals = 0x67,
F13 = 0x68,
F14 = 0x69,
F15 = 0x6a,
F16 = 0x6b,
F17 = 0x6c,
F18 = 0x6d,
F19 = 0x6e,
F20 = 0x6f,
F21 = 0x70,
F22 = 0x71,
F23 = 0x72,
F24 = 0x73,
Execute = 0x74,
Help = 0x75,
Menu = 0x76,
Select = 0x77,
Stop = 0x78,
Again = 0x79,
Undo = 0x7a,
Cut = 0x7b,
Copy = 0x7c,
Paste = 0x7d,
Find = 0x7e,
Mute = 0x7f,
VolumeUp = 0x80,
VolumeDown = 0x81,
}
enum HidKeyboardModifiers { // uint8_t
None = 0x0,
LeftControl = 0x1,
LeftShift = 0x2,
LeftAlt = 0x4,
LeftGUI = 0x8,
RightControl = 0x10,
RightShift = 0x20,
RightAlt = 0x40,
RightGUI = 0x80,
}
enum HidKeyboardAction { // uint8_t
Press = 0x0,
Up = 0x1,
Down = 0x2,
}
NKRO / Nキーロールオーバーできるのかどうかは確かめていない。
NKROを雑なコードで確かめてみた。
import { pins } from '@dsboard/pico';
import {
delay,
HidKeyboardAction,
HidKeyboardModifiers,
HidKeyboardSelector,
} from "@devicescript/core"
import { schedule } from '@devicescript/runtime';
import { startButton, startHidKeyboard } from "@devicescript/servers";
const keyboard = startHidKeyboard({});
const button1 = startButton({ pin: pins.GP2 });
const button2 = startButton({ pin: pins.GP3 });
const button3 = startButton({ pin: pins.GP4 });
const button4 = startButton({ pin: pins.GP5 });
const button5 = startButton({ pin: pins.GP6 });
const button6 = startButton({ pin: pins.GP7 });
const button7 = startButton({ pin: pins.GP8 });
button1.down.subscribe(async () => { await keyboard.key(HidKeyboardSelector.A, HidKeyboardModifiers.None, HidKeyboardAction.Down); });
button1.up.subscribe(async () => { await keyboard.key(HidKeyboardSelector.A, HidKeyboardModifiers.None, HidKeyboardAction.Up); });
button2.down.subscribe(async () => { await keyboard.key(HidKeyboardSelector.B, HidKeyboardModifiers.None, HidKeyboardAction.Down); });
button2.up.subscribe(async () => { await keyboard.key(HidKeyboardSelector.B, HidKeyboardModifiers.None, HidKeyboardAction.Up); });
button3.down.subscribe(async () => { await keyboard.key(HidKeyboardSelector.C, HidKeyboardModifiers.None, HidKeyboardAction.Down); });
button3.up.subscribe(async () => { await keyboard.key(HidKeyboardSelector.C, HidKeyboardModifiers.None, HidKeyboardAction.Up); });
button4.down.subscribe(async () => { await keyboard.key(HidKeyboardSelector.D, HidKeyboardModifiers.None, HidKeyboardAction.Down); });
button4.up.subscribe(async () => { await keyboard.key(HidKeyboardSelector.D, HidKeyboardModifiers.None, HidKeyboardAction.Up); });
button5.down.subscribe(async () => { await keyboard.key(HidKeyboardSelector.E, HidKeyboardModifiers.None, HidKeyboardAction.Down); });
button5.up.subscribe(async () => { await keyboard.key(HidKeyboardSelector.E, HidKeyboardModifiers.None, HidKeyboardAction.Up); });
button6.down.subscribe(async () => { await keyboard.key(HidKeyboardSelector.F, HidKeyboardModifiers.None, HidKeyboardAction.Down); });
button6.up.subscribe(async () => { await keyboard.key(HidKeyboardSelector.F, HidKeyboardModifiers.None, HidKeyboardAction.Up); });
button7.down.subscribe(async () => { await keyboard.key(HidKeyboardSelector.G, HidKeyboardModifiers.None, HidKeyboardAction.Down); });
button7.up.subscribe(async () => { await keyboard.key(HidKeyboardSelector.G, HidKeyboardModifiers.None, HidKeyboardAction.Up); });
面倒だったのでコピペした、眠いのでfor
文すら書かなかった……
http://localhost:8081 の画面ではすべてのスイッチが押されることがあるような気がするが、どうも安定しない気がする。ブレッドボードに横に並べた7個のタクタイルスイッチを同時に押すのが難しいので割り箸で押してるがちゃんと押せていないのか、押せているがやはりNKROに対応していないのか、なんなのかわからない。
やはりカスタムキーボード的に認識させる必要があるのだろうか。
以下のページでキーボードがNKROできるかを試すことができる。キーボードを1文字ずつ順番に押し続けていき、7文字目が入力できれば6KROよりは多く入力できるキーボードだと思う。このページは本当はアンチゴーストを確認するウェブページだとは思うが……
HidKeyboardをNKRO対応にするのが大変そう(おそらく既存のクラスを使ってディスクリプタを変更する方法がないので自前で作る必要がある)なので要件を満たさないような気がしてきた。ただ、それ以外の扱いに関してはそれなりにできたと思うのでちょっとしたものを作るのならDeviceScriptでも割と作れそうだと感じた。
あとはWS2812Bを扱ったりしたかったがNKROが解決できないのがちょっとな〜と思ったので、一旦は調査を止めて別の調査をする。以下を見る限りはLEDの扱いはそこまで苦労しないだろう。
- https://microsoft.github.io/devicescript/developer/leds
- https://microsoft.github.io/devicescript/developer/leds/driver
とりあえずここまで書いたソースコードを https://github.com/sasaplus1/rpp-playground-with-devicescript に書いておいた。