Arduino IDE用拡張機能開発実験

概要

昔にerase_flashを実行するだけのプラグインを作成しましたが、Arduino IDEのバージョンが2系にあがり、まったく別の開発環境のなったために使えなくなっています。

そこでArduino IDE2系での拡張機能開発を調査しつつ、ESP32のフラッシュ削除ツールを作ってみました。

Arduino IDEの変更

Arduino IDEはバージョン1系はJavaで作成されており、2系はEclipse Theiaで作成されています。これはVisual Studio Codeと同じ拡張機能が利用できます。

https://github.com/arduino/arduino-ide/blob/main/docs/advanced-usage.md

上記に詳細が書いてありますが、ざっくりいうとWindowsの場合には「Ctrl+ Shift+P」でコマンドパレットが開き、任意のコマンドが実行できます。

拡張機能やテーマに関しては「C:\Users\%username%\.arduinoIDE\」以下に「plugins」というフォルダを作成して保存して、Arduino IDEを再起動すると読み込まれます。

開発環境の準備

VSCodeを準備する

VSCode上で開発しますので、環境を準備します。通常の拡張機能に関してはそのままデバッグまでできるのですが、今回はArduino IDE上で動かすのでちょっと面倒です。

Node.jsを入れる

バージョンはなんでもよいと思いますが、私は長期安定版であるLTSを入れました。

必要なツールのインストール

npm install -g yo generator-code

雛形を作成するためにYeomanとgenerator-codeをいれます。

Arduino連携用プラグイン追加

npm install vscode-arduino-api --save

Arduino IDEの内部の情報を取得するパッケージを追加。開発中のコード補完などにも利用されるはず。

雛形作成

yo code

対話型の雛形作成コマンドを実行します。

What type of extension do you want to create? (Use arrow keys)

→ New Extension (TypeScript)

を選択。JavaScriptよりはTypeScriptが一般的なはず。

What’s the name of your extension?

→ arduino-esp32-erase

作成する拡張機能名を入力します。

What’s the identifier of your extension?

→ Enter

通常は拡張機能名と同じはずなのでそのままエンターで決定。

What’s the description of your extension?

→ ESP32 Erase Flash

拡張機能の説明。適当でよいはず。

Initialize a git repository?

→ n

Gitリポジトリの初期化。あとでやるので私はnを選択。

Bundle the source code with webpack?

→ n

たぶん使わないはずなのでn。

Which package manager to use?

→ npm

とりあえずデフォルトのnpm。

Do you want to open the new folder with Visual Studio Code?

→ Skip

VSCodeで開くかはあとで自分で開くのでSkip。そのまま開いても構いません。

フォルダ移動

cd arduino-esp32-erase

とりあえず作成したフォルダに移動します。

README.md編集

README.mdは編集しないとパッケージに失敗するので、適当に編集する。

ライセンスファイル作成

何かしらのライセンスファイルを入れておかないとパッケージの際に確認されるのでとりあえず入れておく。

bitsadmin /TRANSFER download https://creativecommons.org/publicdomain/zero/1.0/legalcode.txt %CD%\LICENSE.txt

空でもよいのでファイルが必要なはず。MITライセンスなどを使うのが無難だと思います。

gitリポジトリ追加

  "repository": {
    "url": "git+ssh://git@github.com/"
  },

package.jsonにrepository項目を追加する。何か書いていないとパッケージの際に確認されるのでとりあえず埋めておく。

プラグインフォルダー作成

mkdir %USERPROFILE%\.arduinoIDE\plugins

Windowsの場合には上記の場所にフォルダを作成する。

mkdir ~/.arduinoIDE/plugins

LinuxとmacOSの場合には上記のはず。ただし、今回はWindows決め打ちでコマンドを実行しています。

パッケージのテスト実行

npx vsce package

とりあえずビルドしてパッケージが成功するか確認します。途中でYかNかを聞かれた場合にはYを答えることでビルドは進みます。ライセンスとgitリポジトリが未設定だと確認が入るはずです。README.mdを未編集の場合には問答無用で失敗します。

転送

copy /y *.vsix %USERPROFILE%\.arduinoIDE\plugins\

パッケージ化された.vsixをpluginsフォルダにコピーします。

Arduino IDE起動

「Ctrl+ Shift+P」でコマンドパレットを開き、「Hello World」コマンドを実行します。

上記のように右下にインフォメーションが表示されれば成功です。

パッケージからArduino IDE起動までを自動実行

call npx vsce package
copy /y *.vsix %USERPROFILE%\.arduinoIDE\plugins\
"%USERPROFILE%\AppData\Local\Programs\Arduino IDE\Arduino IDE.exe"

上記の内容をバッチファイルの保存して実行することでパッケージ、コピー、Arduino IDEの起動ができるはずです。私は「run.bat」という名前で作成をしました。

コマンドプロンプトからArduino IDEを立ち上げることでconsole.logが取得できます。手で自動するとログが出力されないのでかなりデバッグが面倒ですので、コマンドラインからの実行をおすすめします。

Arduino IDEの内部情報取得

GitHub - dankeboy36/vscode-arduino-api: Arduino IDE API for VS Code extensions
Arduino IDE API for VS Code extensions. Contribute to dankeboy36/vscode-arduino-api development by creating an account o...

上記のプラグインを利用するのですが、なにかおかしいです。GitHubの説明を確認していきます。

インポート

import type { ArduinoContext } from 'vscode-arduino-api';

上記でインポートします。ここは問題ありません。

拡張取得

export function activate(context: vscode.ExtensionContext) {
  const context: ArduinoContext = vscode.extensions.getExtension(
    'dankeboy36.vscode-arduino-api'
  )?.exports;
  if (!context) {
    // Failed to load the Arduino API.
    return;
  }

vscode.extensionsから、このツールを読み込みます。ここでcontextという名前が多重宣言されているのでエラーになります。

export function activate(context: vscode.ExtensionContext) {
  const arduinoContext: ArduinoContext = vscode.extensions.getExtension(
    'dankeboy36.vscode-arduino-api'
  )?.exports;
  if (!arduinoContext) {
    return;
  }

上記のようにarduinoContextと別の名前をつけてあげましょう。

内部情報取得

console.log('arduinoContext? ' + JSON.stringify(arduinoContext));

上記のように先程取得したarduinoContextから情報を取得できます。

https://github.com/dankeboy36/vscode-arduino-api/blob/main/docs/interfaces/ArduinoContext.md

取得できる情報は上記にドキュメントがありました。

arduinoContext?.sketchPath

スケッチがあるpathになります。しかしながらDeprecatedの非推奨のため、「arduinoContext?.currentSketch?.sketchPath」を取得しろとあります。

ただし、currentSketchが私のArduino IDE2.2.1だと取得できませんでした。今後のバージョンアップで取得できるようになる可能性がありますが、DeprecatedのarduinoContext.sketchPathを現在は利用したほうが良さそうです。

arduinoContext?.port?.address

シリアルポートを取得します。こちらもDeprecatedで「arduinoContext?.currentSketch?.port」を取得しろとありますが現在は取得できませんでした。

あとは起動した直後では常に未定義のundefinedで取得できません。一度他のシリアルポートに変更して戻すなどの処理をしないとだめでした。もしくはボードを変更ですね。こちらもバグのような気がしますので今後修正を期待したいと思います。

arduinoContext?.boardDetails

ボード定義を取得します。こちらもDeprecatedで「arduinoContext?.currentSketch?.boardDetails」以下略。

ここの情報は非常に大量にあるので注意してください。

私の環境で取得したものを参考のため上記に貼り付けます。この中から必要そうなものを抜き出します。

"tools.esptool_py.path": "C:\\Users\\tanaka\\AppData\\Local\\Arduino15\\packages\\esp32\\tools\\esptool_py\\4.5.1",
"tools.esptool_py.cmd.linux": "esptool.py",
"tools.esptool_py.cmd": "esptool.exe",

eraseコマンドはesptoolを実行しますので、pathを調べる必要があります。上記からわかりました。今回はWindows決め打ちなのでtools.esptool_py.pathとtools.esptool_py.cmdを使います。

  • arduinoContext?.boardDetails.buildProperties[‘tools.esptool_py.path’]
  • arduinoContext?.boardDetails?.buildProperties[‘tools.esptool_py.cmd’]

上記で取得可能です。本当はOSを取得してコマンドを書き換える必要があります。

"tools.esptool_py.erase.protocol": "serial",
"tools.esptool_py.erase.params.verbose": "",
"tools.esptool_py.erase.params.quiet": "",
"tools.esptool_py.erase.pattern_args": "--chip esp32s3 --port \"{serial.port}\" --baud 921600  --before default_reset --after hard_reset erase_flash",
"tools.esptool_py.erase.pattern": "\"{path}/{cmd}\" {erase.pattern_args}",
"tools.esptool_py.erase.pattern.linux": "python3 \"{path}/{cmd}\" {erase.pattern_args}",

あとは上記に細かいコマンドがありますがポートだけ指定すれば動くのでこの文字列は利用しません。本当はこの文字列を使ったほうが好ましいです。

ボタンの追加

const button = vscode.window.createStatusBarItem(
  vscode.StatusBarAlignment.Right,
  200
);
button.command = 'arduino-esp32-erase.erase';
button.text = '$(trash) ESP32 Erase';
context.subscriptions.push(button);
button.show();

コマンドパレットからの実行は面倒なので、ステータスバーにボタンを追加したいと思います。createStatusBarItemで右から優先度200で追加しています。優先度が大きいほど左側に位置します。

Product Icon Reference
Reference of all product icons by id

上記にアイコンの一覧がありますので$(trash)でゴミ箱のアイコンを表示しています。

これでステータスバーにボタンが追加されると嬉しいのですが、追加されないはずです。activate関数でボタンを追加しているのですが、デフォルトはコマンドパレットから呼び出した初回のみ追加されます。コマンドパレット一度呼び出すとボタンが追加されるはずです。

起動時にボタンを自動追加

  "activationEvents": [
    "onStartupFinished"
  ],

package.jsonのactivationEventsを書き換えます。アクティベーションをonStartupFinishedで実行します。

Activation Events
To support lazy activation of Visual Studio Code extensions (plug-ins), your extension controls when it should be loaded...

上記に説明がありますが、起動が終わったときにactivate関数が呼ばれるようになるはずです。これで起動するとボタンが増えると思います。

Arduino IDEにログ出力

const writeEmitter = new vscode.EventEmitter<string>();
let writerReady: boolean = false;

上記でEventEmitterを利用することで、Arduino IDEのターミナルを開くことができます。

GitHub - earlephilhower/arduino-littlefs-upload: Build and uploads LittleFS filesystems for the Arduino-Pico RP2040 and ESP8266 cores under Arduino IDE 2.2.1 or higher
Build and uploads LittleFS filesystems for the Arduino-Pico RP2040 and ESP8266 cores under Arduino IDE 2.2.1 or higher -...

上記の処理を参考にさせてもらいました。Raspberry Pi Pico RP2040とESP8266のファイル転送用拡張機能なのですが、何故かESP32には対応していない。。。対応していたらこのブログ書かなかったのに。。。

function makeTerminal(title: string) {
	let w = vscode.window.terminals.find((w) => ((w.name === title) && (w.exitStatus === undefined)));
	if (w !== undefined) {
		w.show(false);
		return;
	}

	const pty = {
		onDidWrite: writeEmitter.event,
		open: () => { writerReady = true; },
		close: () => { writerReady = false; },
		handleInput: () => { }
	};
	const terminal = (<any>vscode.window).createTerminal({ name: title, pty });
	terminal.show();
}

こんな感じでターミナルを開く処理をしていました。

makeTerminal("ESP32 erase flash");

let cnt = 0;
while (!writerReady) {
	if (cnt++ >= 50) {
		vscode.window.showErrorMessage("Unable to open upload terminal");
		return;
	}
	await new Promise(resolve => setTimeout(resolve, 100));
}

上記のように開き終わるまで待機していないとだめでした。

writeEmitter.fire(str + '\r\n');

以降はfireで出力可能です。ただし改行コードが\r\nなので注意してください。

function log(str: string) {
	writeEmitter.fire(str + '\r\n');
	console.log(str);
}

私は上記のようにconsole.logとターミナルの両方に出力するようにしました。

コマンド実行

if (arduinoContext?.boardDetails) {
  let esptool = String(arduinoContext?.boardDetails.buildProperties['tools.esptool_py.path'] + '/' + arduinoContext?.boardDetails?.buildProperties['tools.esptool_py.cmd']);
  log(esptool);
  let opts = ['--port', arduinoContext?.port?.address, 'erase_flash'];
  log(JSON.stringify(opts));
  let exitCode = await runCommand(esptool, opts);
  if (exitCode) {
    // Error
    vscode.window.showInformationMessage('Failed to connect to Espressif device');
  } else {
    vscode.window.showInformationMessage('Chip erase completed successfully');
  }
} else {
  log('Please reselect the port');
  vscode.window.showInformationMessage('Please reselect the port');
}

上記の処理になります。

if (arduinoContext?.boardDetails) {

上記で正しくボードが選択されているか確認しています。ここがセットされていない場合にはポートかボードを選択しなおす必要があります。

let opts = ['--port', arduinoContext?.port?.address, 'erase_flash'];

コマンドのオプションは上記で設定しています。ポート番号だけ指定してあとはデフォルトになります。

  let exitCode = await runCommand(esptool, opts);
  if (exitCode) {
    // Error
    vscode.window.showInformationMessage('Failed to connect to Espressif device');
  } else {
    vscode.window.showInformationMessage('Chip erase completed successfully');
  }

実行は上記で、arduino-littlefs-uploadの関数を参考にして使わせてもらっています。

async function runCommand(exe: string, opts: any[]) {
  const cmd = spawn(exe, opts);
  for await (const chunk of cmd.stdout) {
    log(String(chunk));
  }
  for await (const chunk of cmd.stderr) {
    log(String(chunk));
  }
  let exitCode = await new Promise((resolve, reject) => {
    cmd.on('close', resolve);
  });
  return exitCode;
}

こんな感じでspawnで実行して、標準出力と標準エラーをログ出力しつつ、実行完了まで待機する関数です。

完成物

上記のように下にあるボタンを押すとターミナルが開き、eraseを実行する拡張機能を作ることができました。

完成したソース

GitHub - tanakamasayuki/arduino-esp32-erase: Arduino IDE extension for ESP32 erase flash
Arduino IDE extension for ESP32 erase flash. Contribute to tanakamasayuki/arduino-esp32-erase development by creating an...

上記においてありますので参考にしてみてください。package.jsonとsrc/extension.tsしか編集する必要はないはずです。

まとめ

本当はmkspiffsとmklittlefsを使ってdataフォルダにあるファイルをESP32のフラッシュに転送するツールを作りたいのですが、いろいろ面倒なことがありそうなので単純なErase Flashを作ってみました。

いろいろ考えるとWi-Fi接続してWebDAVアクセスできるプログラムを転送してコピーとか、URLからファイルをダウンロードしてきてフラッシュに保存するだけのプログラムを実行したほうがもしかしたら楽かもしれません。

dataフォルダ転送のためのメモ

SPIFFS Filesystem - ESP32 - — ESP-IDF Programming Guide latest documentation

mkspiffsのコマンドは上記に説明がありますが、パーティション情報が必要になります。

  • arduinoContext?.compileSummary?.buildPath

コンパイルを一度でもすると上記が取得できます。この中にpartitions.csvがあるので中身をパースすれば取得できるはずです。

arduino-littlefs-uploadをみてみたら「arduinoContext?.boardDetails?.configOptions」を探していましたがESP32だとここにSPIFFSなどの情報はないようです。コンパイル時に不要な情報だもんね。

なのでcsvをパースして、オフセットとサイズを取得して、spiffsかlittlefsは判定難しいのでダイアログか設定ファイルから取得しないとだけかもしれません。あとFatFsもサポート追加されていますがmkfatfsはまだ提供されていないようです。。。

いろいろ考えるとPlatformIOを使うほうが簡単そうですね。

GitHub - labplus-cn/mkfatfs: A tools use for pack/unpack flash file system.
A tools use for pack/unpack flash file system. Contribute to labplus-cn/mkfatfs development by creating an account on Gi...

ちなみにPlatformIOでFatFSをアップロードするときには上記のツールを使っているようでした。

コメント