Open12

DeviceScriptでRaspberryPiPicoを扱う時のメモ

sasa+1sasa+1

Raspbeery Pi PicoをDeviceScriptで操作する時のメモ

ドキュメント通りにコードを書いても動作しないことが割とあるので、DeviceScript自体のソースコードを読んだりnode_modules/@devicescript配下にある型やソースコードを読む必要が多々ある。

ドキュメントではVSCodeを使いVSCode拡張を利用して開発する方法を推奨しているようで、それがメインで書かれているが自分はVim使いなのでターミナルを利用する方法で開発していく。

よく使うのでピンアウトへのリンクも貼っておく。

sasa+1sasa+1

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
sasa+1sasa+1

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
sasa+1sasa+1

開発はdevtoolsコマンドで行う。

$ ./node_modules/.bin/devicescript devtools 

実行すると http://localhost:8081 にアクセスできるようになる。シミュレーターを使っていたり、実機を接続している場合はデバイスが表示される。

ブラウザで http://localhost:8081 を表示しておらず、console.logがソースコードに書かれている場合はターミナルに表示される(--logging --serialを付与して起動した場合なのかブラウザで表示していない時なのか?条件がよくわからない)がブラウザで表示している場合にはブラウザのDevTools(あるいはWebInspectorなどブラウザの開発者ツール)のConsoleに表示される。構文エラーや発生した例外もこれらに表示される。

sasa+1sasa+1

vmサブコマンドは仮想マシンを起動してコードを確認できるようだがよくわからない。実機がないとLEDだったりデバイスに依存しないコードくらいしか確認できない気はするが、逆にその辺りの確認であればvmで確認できるのは手軽で良いことなのかもしれない。runサブコマンドの--testと合わせて用途によっては便利なのかも。

DeviceScriptはシミュレーターが充実しているので抽象化が上手にできるとdevtoolsで起動してブラウザから確認する程度で開発ができるのかもしれない。

sasa+1sasa+1

オンボードの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
});

せっかくなのでschedule関数を使った。setTimeoutsetIntervalだと第2引数に0を指定しても4msや16msの間隔で実行されるのだと思うがscheduleはきちんと指定した通りに実行されているように感じた。感じたが、きちんと計測したわけではないので実際はそんなことはないのかもしれない。schedule関数だとコールバック関数にいくつかの引数が渡されるので用途によっては便利だと思う。

sasa+1sasa+1

オンボードでないテキトーなGPIOの先に繋いだLEDをチカチカさせる。startLightBulbを使うとLEDを抽象化してくれるらしく、devtoolsで起動したサーバーにアクセスするとLEDを表示してくれる。

DeviceScriptのシミュレーター画面で擬似的な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
});

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 のように新たなボードとして追加するのが良いのかもしれない。

https://github.com/microsoft/devicescript-pico/blob/e35cfdf3829da501822b2320b1ad672c0b906ada/boards/rp2040/pico.board.json

を眺めていて思ったがpinsでなくled.pinで取得すれば良いのか?🤔

sasa+1sasa+1

I2C接続したAS5600という磁気エンコーダーから角度を取得する。AliExpressで買った白い四角い基盤に実装されたものを使ってブレッドボードに挿して試している。

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
});

これでAS5600のICの上で磁石を回転すると角度が出力される。

configureHardware関数でI2Cのピンを指定する。I2C0とI2C1が使えるはずなのだがDeviceScriptでは指定できないのだろうか……🤔

https://github.com/microsoft/devicescript-pico/blob/868c8c3e0a34838eb058c8f0d8db58eb026b0d4b/src/i2c.c

のあたりを読むとI2C_INSTで切り替えているようだが、I2Cのピンを指定する方法がconfigureHardwareしか見当たらないし、なんらかの理由でI2C0とI2C1を同時に使う方法がわからない。


後述のSSD1306とAS5600を同時に使うとノイズが発生しているのか、AS5600に磁石を近づけていないのに謎の値が送信されてくる……🤔

degreeの値が大きすぎる値が来たりすることもあるのでifでフィルターをしたりボーレートを下げたりした方が良さそう。

sasa+1sasa+1

I2C接続したSSD1306に文字を表示する。

DeviceScriptのシミュレーター画面で擬似的なディスプレイが表示されている

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
});

500ミリ秒ごとにSSD1306の表示が更新される。

await ssd1306.rotate(false);を実行すると上下反転する。

sasa+1sasa+1

HIDキーボードとして認識させてボタンをキーボード代わりにする。

DeviceScriptのシミュレーターの画面で擬似的なボタンが表示されている

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);
});

startButtonButtonを返す。downでの監視以外にもupholdで監視できる。

あまりに情報がないのでGitHubリポジトリを検索したらサンプルが多数見つかった……

何故か手元のcoc.nvimがHidKeyboardSelectorなどを補完してくれないのでnode_modules配下をgrepしたところ定義が見つかった。リポジトリを検索しても見つからないので別のリポジトリにあるのか、なんなのか……

node_modules/@devicescript/core/src/devicescript-spec.d.ts
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キーロールオーバーできるのかどうかは確かめていない。

sasa+1sasa+1

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に対応していないのか、なんなのかわからない。

やはりカスタムキーボード的に認識させる必要があるのだろうか。

https://learn.adafruit.com/custom-hid-devices-in-circuitpython/n-key-rollover-nkro-hid-device

以下のページでキーボードがNKROできるかを試すことができる。キーボードを1文字ずつ順番に押し続けていき、7文字目が入力できれば6KROよりは多く入力できるキーボードだと思う。このページは本当はアンチゴーストを確認するウェブページだとは思うが……

https://www.microsoft.com/applied-sciences/projects/anti-ghosting-demo

sasa+1sasa+1

HidKeyboardをNKRO対応にするのが大変そう(おそらく既存のクラスを使ってディスクリプタを変更する方法がないので自前で作る必要がある)なので要件を満たさないような気がしてきた。ただ、それ以外の扱いに関してはそれなりにできたと思うのでちょっとしたものを作るのならDeviceScriptでも割と作れそうだと感じた。

あとはWS2812Bを扱ったりしたかったがNKROが解決できないのがちょっとな〜と思ったので、一旦は調査を止めて別の調査をする。以下を見る限りはLEDの扱いはそこまで苦労しないだろう。

とりあえずここまで書いたソースコードを https://github.com/sasaplus1/rpp-playground-with-devicescript に書いておいた。