著者: Peter Hoddie
更新日: 2021年4月7日
著作権2019-2021 Moddable Tech, Inc.
このドキュメントは、Ecma TC53によって開発中のIOクラスパターン提案に基づく入出力(IO)に関する作業を紹介し、ESP8266マイクロコントローラ上でXS JavaScriptエンジンを使用した提案の実装について説明します。(実装はESP32もサポートしていますが、このドキュメントはESP8266に焦点を当てています)
Ecma TC53は、組み込みシステム向けのECMAScriptモジュールの標準化を推進するための委員会です。その目標は、リソースが制約された組み込みハードウェア向けのソフトウェアを開発するための標準的なJavaScript APIを定義することです。これは、W3CやWHATWGがウェブ向けのソフトウェアを開発するためのJavaScript APIを定義する作業に類似しています。TC53によって定義されたAPIは、ベンダーに依存せず、したがってホストオペレーティングシステム、CPUアーキテクチャ、およびJavaScriptエンジンに依存しないことを意図しています。IOは、組み込みシステムのほぼすべての用途にとって基本的であるため、TC53によって最初の作業領域として選ばれました。例えば、IOはセンサーと通信の両方をサポートするための前提条件です。
初期のTC53の作業、特にIOに関する作業の重要な特徴は、それらが低レベルのAPIであることです。これらはJavaScriptランタイムの最小ホストと見なすことができます。これらは、特定の種類の製品、市場、およびプログラミングスタイルのためのフレームワークを含む高レベルのAPIを構築するために使用されます。これらのAPIはドライバに似ており、小さくシンプルなAPIが信頼性を達成するために重要です。これらのAPIはJavaScriptで使用するのが簡単であるように設計されています。さらに、これらのAPIはネイティブコードで実装するのが簡単であることを目指しています。これらのAPIはポーティングレイヤーを構成するため、ポーティングレイヤーの実装者にとって明確である必要があります。ポーティング作業が圧倒的な量の作業にならないように、十分に小さくする必要があります。JavaScriptの使用がシンプルで、組み込みC開発者がJavaScript言語の専門家になることなく効率的なポートを作成できるようにする必要があります。
IOクラスパターンの提案は、これらの要件をうまくバランスさせているように見えます。この設計は、Johnny Five、Firmata、Node.js、およびModdable SDKなど、さまざまなJavaScriptプロジェクトからインスピレーションを得ています。これらのプロジェクトは、JavaScriptでさまざまな種類のIOと対話する方法について、実際の経験に基づいたアイデアを提供してきました。APIの実装の取り組みは非常に管理しやすく、CPU利用率、レイテンシ、およびメモリ使用量の両方の観点から効率的に動作する結果となりました。
IOクラスパターンの基本定義は2019年中頃から存在しており、設計の改良が進んでいます。提案された仕様の現在のドラフトが利用可能です。設計をよりよく理解するために、実装の取り組みが行われました。ESP8266マイクロコントローラーがテストベッドとして選ばれたのは、XS JavaScriptエンジンによってサポートされており、そのハードウェアリソースが現在のモダンなJavaScriptをサポートするデバイスの中で低い方に位置しているためです。さらに、その低コストと広い入手可能性により、多くの開発者が実験し、この取り組みに貢献することが可能です。
実装自体は可能な限り「ベアメタル」にしようとしています。例えば、デジタルおよびシリアルIOはハードウェアレジスタを直接操作することによって実装されています。このアプローチは、IOクラスの真に焦点を絞ったポートがどのようなものかを探るために採用されました。将来的な作業には、ネイティブのポーティングレイヤーを含めることが考えられます。
このドキュメントの残りの部分では、IOクラスパターンと、それがESP8266実装の各IOタイプにどのように適用されるかについて説明します。
IOクラスパターンの設計は、マイクロコントローラー上の大多数のIO操作が次の4つの基本操作によって記述されるという考えから始まります:
- Create -- IOリソースへの接続を確立し、構成する。
- Read -- IOリソースからデータを取得する。
- Write -- IOリソースにデータを送信する。
- Close -- IOリソースを解放する。
もちろん、すべての種類のIOが4つの操作すべてを使用するわけではありません。アナログ入力は書き込みを使用しません。デジタル出力は読み取りを使用しません。これらの違いが、このパターンが単なる「クラス」ではなく「クラスパターン」と呼ばれる理由です。各IOタイプは、このパターンをどのように使用するかを定義します。例えば、アナログ入力は書き込み操作がサポートされていないことを定義します。
IOクラスパターンは、すべての操作が非ブロッキングであるという長年のJavaScriptの慣例を採用しています。この動作は、リソースが制約されたデバイスでは特に重要です。なぜなら、ブロッキング操作のための並行実行コンテキストを作成するためのリソースが不足している可能性があるからです。これは、すべての操作が瞬時に完了するという意味ではありませんが、システムがタイムリーに受信イベントに応答する能力を妨げない程度に迅速に完了することを意味します。
作成操作は、IOクラスのコンストラクタによって実行されます。コンストラクタは、IOを構成するプロパティを含むオブジェクトを1つの引数として受け取ります。これは時々オプションオブジェクトと呼ばれます。例えば、次のサンプルでは、ピン16にバインドされたアナログ入力のインスタンスを作成します。
let analogIn = new Analog({
pin: 16,
});
IOの構成はホストハードウェアに依存します。APIは、ハードウェアの能力に大きな違いがあることを考慮しなければなりません。例えば、いくつかのハードウェアは、アナログ入力が提供する解像度のビット数を構成することをサポートしています。そのようなホストは、この目的のためにIO構成にresolution
プロパティを追加するかもしれません。
let analogIn = new Analog({
pin: 16,
resolution: 12,
});
IOクラスは、例えば新しいデータが読み取り可能になったときなど、特定のイベントの通知を提供する場合があります。通知が利用可能な場合、コールバック関数を呼び出します。この例では、シリアルIOインスタンスを作成し、シリアルインスタンスに新しいデータが読み取り可能になったときに呼び出されるonReadable
コールバック関数を設定します。
let serial = new Serial({
baud: 57600,
onReadable() {
trace("serial input available\n");
}
})
このように、コールバックを構成のプロパティとして提供する方法は便利なことが多いです。同じアプローチは、Simplified Constructionで説明されているように、Node.jsのストリームでも使用されます。
各IO実装はサポートする通知を定義します。IOクラスパターンの提案では、次の4つの通知が定義されています:
onReadable
-- 読み取るための新しいデータが利用可能です。onWritable
-- 出力バッファがより多くのデータを受け入れることができます。onError
-- 問題が発生しました。onReady
-- インスタンスが初期化され、使用可能です。
コールバックは通常オプションです。各コールバックにはランタイムコストが伴うことが多いため、スクリプトは必要なコールバックのみをインストールすることをお勧めします。とはいえ、ポーリングよりもコールバックの使用が一般的に推奨されます。
作成操作が完了すると、IOインスタンスの構成がロックされます。これにより、APIとその実装が簡素化されます。また、Secure ECMAScript(別の場所で詳述されているトピック)でIOを保護する作業も簡素化されます。変更が必要な稀な状況では、インスタンスを閉じて、希望する変更を加えて再作成します。
ハードウェアリソースの場合、作成操作は通常ハードウェアへの排他的アクセスを確立します。これにより、2つのインスタンスが互いに干渉するのを防ぎます。プロジェクトの異なる部分が同じデジタル入力のステータスを確認したい場合など、共有アクセスが望ましい場合もあります。単一のハードウェアリソースの複数のクライアントの複雑さをポーティングレイヤーで管理するのではなく、JavaScriptレイヤーに委ね、特定のプロジェクトのニーズに合わせたメカニズムを実装することができます。
作成操作はIOクラスの実装の中で最も大きな部分を占めることが多いです。これは、構成を検証し、IOリソースを初期化し、IOリソースをJavaScriptオブジェクトにバインドする必要があるためです。幸いなことに、作成操作は読み取りおよび書き込み操作に比べて頻度が少ないことが多いです。作成はデータ構造を設定する時間をかけることができるため、読み取りおよび書き込み操作が比較的迅速に実行できます。
読み取り操作はIOリソースからデータを返します。
読み取り操作によって返されるデータは、IOクラスの定義によって異なります。例えば、デジタル入力は0または1の数値を返します。
let input = new Digital({
pin: 0,
mode: Digital.Input,
});
console.log(input.read());
読み取り操作によって返されるデータの種類は、以下のデータフォーマットセクションで説明されているように、IOインスタンスのformat
プロパティによって決定されます。各IOクラスは、多くの状況に適したデフォルトフォーマットを定義しています。
データが利用できない場合、読み取り操作の結果はundefined
です。この状況では例外はスローされません。
読み取り操作を行うスクリプトは、いつでも読み取りを呼び出すことができます。ポーリングを避けるために、特定の入力に対するIOクラスはonReadable
コールバックをサポートする場合があります。onReadable
コールバックは、新しいデータが読み取り可能であることを通知しますが、データ自体は提供しません。データを読み取る唯一の方法は読み取り操作です。
ほとんどの種類のIOは、読み取り操作に対して以下のいずれかの動作を持ちます:
-
IOリソースの現在の値を返す。
これにはデジタル入力やアナログ入力が含まれます。読み取り操作を行っても、次の読み取り操作で返される内容は変わりません。IOリソース自体の変更のみが値を変更します。IOリソースの値が変わると
onReadable
コールバックが呼び出されます。 -
入力バッファからデータを返す。
これにはシリアルおよびTCPネットワーク接続が含まれます。シリアル接続からデータを読み取ると、そのデータは入力バッファから削除されます。次の読み取りではバッファ内の次のデータが受信されます。新しいデータが受信されると
onReadable
コールバックが呼び出されます。
書き込み操作はデータをIOリソースに送信します。
書き込み操作で受け入れられるデータは、IOクラスの定義によって異なります。例えば、デジタル出力は0または1の数値を期待します。
let output = new Digital({
pin: 2,
mode: Digital.Output,
});
output.write(1);
書き込み操作で受け入れられるデータの種類は、以下のデータフォーマットセクションで説明されているように、IOインスタンスのformat
プロパティによって決定されます。各IOクラスは、多くの状況に適したデフォルトフォーマットを定義しています。
書き込み操作を行うスクリプトは、いつでもwriteを呼び出すことができます。IOインスタンスは、出力バッファが満杯のときなど、常に新しいデータを受け入れることができるわけではありません。このような状況でwriteが呼び出されると、例外がスローされます。このようなIOインスタンスは一般的に、出力バッファに空きができたときに通知するonWritable
コールバックをサポートしています。以下のサンプルでは、onWritable
コールバックを使用して、アスタリスク(ASCII 42)文字の連続ストリームを送信します。
new Serial({
baud: 921600,
onWritable(count) {
while (count--)
this.write(42);
}
});
ほとんどの種類のIOは、書き込み操作に対して次のいずれかの動作を持ちます:
-
IOリソースによって出力される現在の値を変更する。
デジタル出力はこの動作の例です。書き込み操作を行うと、IOリソースによって出力されるものが即座に変更されます。この場合、値はいつでも変更できるため、
onWritable
コールバックは役に立ちません。 -
データを出力バッファに追加する。
これにはシリアルおよびTCPネットワーク接続が含まれます。シリアル接続からデータが書き込まれると、そのデータは一定期間にわたって送信されます。出力バッファに空きができると、
onWritable
コールバックが呼び出されます。
クローズ操作は、IOインスタンスに関連付けられたすべてのハードウェアリソースを解放します。
serial.close();
特定のIOクラスは、そのクローズ動作の詳細を定義します。例えば、デジタル出力は使用されていないときにデジタル入力になることで電力を節約する場合がありますし、シリアルポートは保留中の出力を終了する場合があります。
クローズ操作が開始されると、コールバックは一切呼び出されません。保留中のコールバックはキャンセルされます。
読み取りおよび書き込み操作は、何らかのデータに対して行われます。JavaScriptは型のない言語であるため、データの種類は言語がサポートするものであれば何でもかまいません。各種類のIOは、サポートするデータフォーマットまたはデータフォーマットを定義します。IOの種類が複数のデータフォーマットをサポートする場合、デフォルトのデータフォーマットも定義します。
IOクラスパターンは、使用されるフォーマットがすべてのIOインスタンスに存在するformat
プロパティを通じて管理されることを定義しています。format
プロパティの値は文字列です。format
は実行時に変更することができます。
シリアルIOタイプは2つのデータフォーマットをサポートしています。最初のフォーマットはnumber
で、これは1バイトずつ読み書きするために使用されます。2つ目のフォーマットはbuffer
で、バイトのバッファを読み書きするために使用されます。状況に応じて、どちらか一方がより便利または効率的です。
例えば、次のサンプルでは、シリアルから1バイトをバイトとして読み取り、その値を使用して次のバイトをバッファに読み取ります。
serial.format = "number";
let count = serial.read();
serial.format = "buffer";
let data = serial.read(count);
「文字列」データフォーマットはまだ実装されていませんが、いくつかのケースでは必要とされるようです。データフォーマット指定子には、例えば「string;ascii」や「string;utf8」のようにテキストエンコーディングを含める必要があります。
このデータフォーマットへのアプローチは、Node.jsのストリームと類似しており、さまざまなエンコーディングの文字列、バッファ、およびオブジェクトを返すことがあります。
IOクラスパターンは通知を提供するためにコールバック関数を使用します。この選択の動機の1つは、IOの一般的なネイティブ実装を反映しており、ポーティングを簡素化するのに役立つためです。もう1つの理由は、コールバックを使用してイベントやプロミスなどの他の一般的な通知形式を実装できるためです。元のIOクラスパターン提案はこの領域を深く探求しており、非同期およびイベントベースのAPIを提供するためにIOクラスにミックスインを適用する方法の例を示しています。IOクラスのシンプルさと一貫性により、汎用のミックスインの実装が小さく簡単になります。
上記のように、IOクラスパターンはIOクラスの基本的な動作を定義します。各特定の種類のIOは、その特定の特性とニーズに応じてこの定義を適用し、その特定の種類のIOのためのクラス定義を作成します。このセクションでは、ESP8266用に実装されたさまざまな種類のIOクラスとその特定の適応について説明します。
組み込みのDigital
IOクラスは、デジタル入力および出力に使用されます。
import Digital from "embedded:io/digital";
プロパティ | 説明 |
---|---|
pin |
制御するGPIO番号を示す0から16の数値。このプロパティは必須です。 |
mode |
IOのモードを示す値。Digital.Input 、Digital.InputPullUp 、Digital.InputPullDown 、Digital.InputPullUpDown 、Digital.Output 、または Digital.OutputOpenDrain のいずれか。このプロパティは必須です。 |
edge |
onReadable コールバックを呼び出す条件を示す値。値は Digital.Rising 、Digital.Falling 、および `Digital.Rising |
onReadable()
mode
プロパティの値に応じて入力値が変化したときに呼び出されます。
Digital
クラスは常に0と1の値を持つnumber
のデータフォーマットを使用します。
入力として構成されたデジタルIOインスタンスは書き込みを実装せず、出力として構成されたものは読み取りを実装しません。
次のサンプルは、ESP8266ボードの内蔵LEDを制御するデジタル出力を作成し、1を書き込むことでそれをオフにします。
const led = new Digital({
pin: 2,
mode: Digital.Output,
});
led.write(1); // off
次のサンプルは、ESP8266の内蔵フラッシュボタンを使用して、前のサンプルで作成したled
を制御します。
let button = new Digital({
pin: 0,
mode: Digital.InputPullUp,
edge: Digital.Rising | Digital.Falling,
onReadable() {
led.write(this.read());
}
});
注:
Digital
クラスは、次のセクションで説明するより一般的なDigitalBank
IOクラスを使用してJavaScriptで実装されています。
組み込みのDigitalBank
クラスは、デジタルピンのグループに同時にアクセスするためのものです。
import DigitalBank from "embedded:io/digitalbank";
ESP8266を含む多くのマイクロコントローラは、統一されたメモリマップドハードウェアポートを通じてデジタルピンにアクセスでき、複数のピンを単一の操作として読み書きすることが可能です。DigitalBank
IOはこの機能に直接アクセスを提供します。
プロパティ | 説明 |
---|---|
pins |
バンクに含めるピンを1に設定したビットマスク。例えば、ピン2と3にアクセスするバンクのビットマスクは0x0C(0b1100)です。このプロパティは必須です。 |
mode |
IOのモードを示す値。Digital.Input 、Digital.InputPullUp 、Digital.InputPullDown 、Digital.InputPullUpDown 、Digital.Output 、または Digital.OutputOpenDrain のいずれか。バンク内のすべてのピンは同じモードを使用します。このプロパティは必須です。 |
rises |
0から1に遷移する際にonReadable コールバックをトリガーするバンク内のピンを示すビットマスク。onReadable コールバックが提供される場合、少なくとも1つのピンがrises およびfalls に設定されている必要があります。 |
falls |
1から0に遷移する際にonReadable コールバックをトリガーするバンク内のピンを示すビットマスク。onReadable コールバックが提供される場合、少なくとも1つのピンがrises およびfalls に設定されている必要があります。 |
onReadable(triggers)
mode
、rises
、falls
プロパティの値に応じて入力値が変化したときに呼び出されます。onReadable
コールバックは、triggers
という単一の引数を受け取ります。これは、コールバックをトリガーした各ピンを1で示すビットマスクです。
DigitalBank
クラスは常にnumber
のデータフォーマットを使用します。値はビットマスクです。読み取り操作では、pins
ビットマスクに含まれないビット位置はすべて0に設定されます。この要件は、予約されたピンや他のバンクで使用されているピンの状態が漏れないようにするために重要です。
入力として構成されたデジタルIOバンクインスタンスは書き込みを実装せず、出力として構成されたものは読み取りを実装しません。
複数のDigitalBank
インスタンスを作成できますが、それらは同じピンを使用することはできません。
ESP8266は単一のハードウェアポートを通じて(1つを除く)すべてのデジタルピンにアクセスできるため、DigitalBank
はデジタルIOの基盤であり、単一ピンのDigital
IOクラスはそれに基づいて構築されています。マイクロコントローラが各ピンに独立してアクセスできる場合、DigitalBank
をDigital
を使用して実装するのが理にかなっています。Digital
とDigitalBank
のAPIは、実装の選択に関係なく一貫しています。
次のサンプルでは、ピン12から15に出力するDigitalBank
を作成します。
let leds = new DigitalBank({
pins: 0xF000,
mode: DigitalBank.Output,
});
leds.write(0xF000);
次のサンプルは、DigitalセクションのLEDのサンプルと機能的に同等ですが、DigitalBank
を使用しています。
let button = new DigitalBank({
pins: 1 << 0,
mode: DigitalBank.InputPullUp,
rises: 1 << 0,
falls: 1 << 0,
onReadable() {
led.write(this.read() ? 1 : 0);
}
});
次のサンプルでは、ピン1と15を入力として監視するDigitalBank
を使用します。ピン1が上昇したり、ピン15が下降したときに報告します。
let buttons = new DigitalBank({
pins: (1 << 1) | (1 << 15),
mode: DigitalBank.Input,
rises: 1 << 1,
falls: 1 << 15,
onReadable(triggered) {
if (triggered & (1 << 1))
trace("Pin 1 rise\n");
if (triggered & (1 << 15))
trace("Pin 15 fall\n");
}
});
組み込みのAnalog
IOクラスはアナログ入力ソースを表します。
import Analog from "embedded:io/analog";
プロパティ | 説明 |
---|---|
pin |
アナログ入力の番号。ESP8266には単一のアナログ入力しかないため、このプロパティは使用されません。 |
コールバックはサポートされていません。アナログ入力は一般的に常に変動しているため、値は常に変化しており、これにより onReadable
が継続的に呼び出されることになります。
データ形式は常に数値です。返される値は、アナログ入力の解像度に基づく最大値までの整数です。
ESP8266のアナログ入力は常に10ビットの値を提供します。アナログ入力デバイスには、インスタンスが返す値の解像度のビット数を示す読み取り専用の resolution
プロパティがあります。
onReadable
コールバックは有用かもしれません。これは、特定の範囲の値に入るか、あるいは特定の量以上に変化するなどの条件に基づいてトリガーされる可能性があります。これは、非常に低消費電力のコプロセッサを使用したエネルギー管理作業で使用されるトリガーに似ています。これは将来の作業のための領域です。
次のサンプルは、アナログ入力の値を0から1までの浮動小数点数として表示します。resolution
プロパティは read
呼び出しの結果をスケーリングするために使用されます。
let analog = new Analog({});
trace(analog.read() / (1 << analog.resolution), "\n");
組み込みの PWM
IOクラスは、ピンのパルス幅変調機能へのアクセスを提供します。
import PWM from "embedded:io/pwm";
プロパティ | 説明 |
---|---|
pin |
PWM出力として動作するGPIO番号を示す0から16までの数値。このプロパティは必須です。 |
hz |
Hz単位でPWM出力の周波数を指定する数値。このプロパティはオプションです。 |
コールバックはサポートされていません。
データ形式は常に数値です。write
呼び出しは、PWM出力の解像度に基づく最大値までの整数を受け入れます。
PWMインスタンスには、書き込み時に受け入れられる解像度のビット数を示す読み取り専用の resolution
プロパティがあります。ESP8266のPWM出力は常に10ビットの値を使用します。
ESP8266は、すべてのPWM出力ピンに対して単一のPWM出力周波数のみをサポートします。既存のPWMが異なる周波数を指定している場合に hz
を指定してPWMを構築しようとすると失敗します。元の周波数を要求したすべてのPWMインスタンスが閉じられた場合、新しい周波数を指定することができます。
PWMインスタンスが作成されると、write
が実行されるまでデューティサイクルは0%に設定されます。
次のサンプルでは、ピン5に10 kHzの出力周波数でPWM出力を作成し、デューティサイクルを50%に設定します。resolution
プロパティは、write
の引数をスケーリングするために使用されます。
let pwm = new PWM({ pin: 5, hz: 10000 });
pwm.write(0.5 * ((1 << pwm.resolution) - 1));
組み込みのI2C
クラスは、I2Cバス上の1つのアドレスと通信するためのI2Cマスターを実装しています。
import I2C from "embedded:io/i2c";
プロパティ | 説明 |
---|---|
data |
I2CデータピンのGPIO番号を0から16の範囲で指定します。このプロパティは必須です。 |
clock |
I2CクロックピンのGPIO番号を0から16の範囲で指定します。このプロパティは必須です。 |
hz |
I2Cバス上の通信速度。このプロパティは必須です。 |
address |
通信するI2Cスレーブデバイスの7ビットアドレス。 |
組み込みのI2C
にはコールバックはありません。すべての操作は同期的に実行されます。
データフォーマットは常にバッファです。write
呼び出しはArrayBuffer
またはTypedArray
を受け入れます。read
呼び出しは常にArrayBuffer
を返します。
多くのI2Cバスは、I2Cプロトコルの拡張である高レベルのSMBプロトコルを使用します。SMBus
クラスは、SMBusデバイスを操作するためのサポートを提供するI2C
クラスのサブクラスです。
I2Cプロトコルはトランザクションベースです。各読み取りおよび書き込み操作の終了時にストップビットが送信されます。ストップビットが1の場合はトランザクションの終了を示し、0の場合はトランザクションに追加の操作があることを示します。read
およびwrite
呼び出しはデフォルトでストップビットを1に設定します。read
およびwrite
のオプションの第2引数でストップビットを指定できます。ストップビットを0に設定するにはfalse
を、1に設定するにはtrue
を渡します。
次のサンプルでは、FT6206タッチセンサーからタッチポイントの数を読み取り、アクティブなタッチポイントのX座標とY座標を取得します。
let touch = new I2C({
data: 4,
clock: 5,
hz: 600000,
address: 0x38
});
touch.write(Uint8Array.of(2));
let count = touch.read(1);
count = (new Uint8Array(count))[0];
trace(`Touch points ${count}.\n`);
if (count) {
touch.write(Uint8Array.of(3), false);
const data = new Uint8Array(touch.read(6 * count));
// decode touch data points...
}
組み込みの Serial
クラスは、指定されたボーレートでシリアルポートを介した双方向通信を実装します。
import Serial from "embedded:io/serial";
プロパティ | 説明 |
---|---|
baud |
接続のボーレートを指定する数値。このプロパティは必須です。 |
注: ピンは指定されていません。なぜなら、ESP8266にはフルデュプレックスのハードウェアシリアルポートが1つしかなく、それが常にGPIOピン1と3に接続されているからです。
onReadable(bytes)
新しいデータが読み取り可能になると onReadable
コールバックが呼び出されます。コールバックは、利用可能なバイト数を示す単一の引数を受け取ります。
onWritable(bytes)
出力バッファに空きができると onWritable
コールバックが呼び出されます。コールバックは、オーバーフローせずに出力バッファに書き込めるバイト数を示す単一の引数を受け取ります。
データ形式は、個々のバイトの場合は number
、バイトのグループの場合は buffer
です。デフォルトのデータ形式は number
です。buffer
形式を使用する場合、write
呼び出しは ArrayBuffer
または TypedArray
を受け入れます。read
呼び出しは常に ArrayBuffer
を返します。
onWritable
コールバックが提供されている場合、インスタンス化直後に呼び出されます。
write
を試みると、書き込むデータを保持するのに十分なスペースが出力バッファにない場合、例外が発生して失敗します。部分的なデータは決して書き込まれません。
buffer
データ形式を使用する場合、引数なしで read
を呼び出すとFIFO内のすべてのバイトが返されます。読み取るバイト数を指定することもできます。要求されたバイト数よりもFIFO内のバイト数が少ない場合、利用可能なバイトのみが返されます -- 例外はスローされず、read
呼び出しは追加データの到着を待ちません。
ESP8266には、シリアル入力および出力の両方に128バイトのFIFOがあります。実装には追加のバッファは追加されません。
受信バッファのオーバーフローやハードウェアによって検出されたその他のエラーを報告するために、onError
コールバックをサポートすることができます。
コールバックが指定されていない場合、実装はコールバックへの参照を維持するために使用されるストレージを排除することでメモリ割り当てを削減します。
APIには、入力および出力FIFOをフラッシュする機能を含めるべきです。
次のサンプルは、シンプルなシリアルエコーを実装しています。個々のバイトを読み書きするために、デフォルトのデータ形式であるnumber
を使用します。
let serial = new Serial({
baud: 921600,
onReadable: function(count) {
while (count--)
this.write(this.read());
},
});
次のサンプルは、シリアル出力にテキストを連続的に出力します。onWritable
コールバックを使用して、出力FIFOをオーバーフローさせることなくできるだけ早くデータを書き込みます。このサンプルでは、スループットを最大化するためにbuffer
データ形式を使用します。
const message = ArrayBuffer.fromString("Since publication of the first edition in 1997, ECMAScript has grown to be one of the world's most widely used general-purpose programming languages. It is best known as the language embedded in web browsers but has also been widely adopted for server and embedded applications.\r\n");
let offset = 0;
const serial = new Serial({
baud: 921600,
onWritable: function(count) {
do {
const use = Math.min(count, message.byteLength - offset);
this.write(message.slice(offset, offset + use));
count -= use;
offset += use;
if (offset >= message.byteLength)
offset = 0;
} while (count);
},
});
serial.format = "buffer";
組み込みのTCP
ネットワークソケットクラスは、汎用の双方向TCP接続を実装します。
import TCP from "embedded:io/socket/tcp";
TCPソケットはTCP接続のみです。一部のネットワーキングライブラリのようにTCPリスナーではありません。TCPリスナーは別のクラスです。
プロパティ | 説明 |
---|---|
address |
接続先のリモートエンドポイントのIP(v4)アドレスを含む文字列。このプロパティは必須です。 |
port |
接続先のリモートポートを指定する数値。このプロパティは必須です。 |
nodelay |
ソケットでNagleのアルゴリズムを無効にするかどうかを示すブール値。このプロパティはBSDソケットAPIのTCP_NODELAY オプションに相当します。このプロパティはオプションで、デフォルトはfalseです。 |
from |
ネイティブソケットインスタンスを新しく作成されたソケットインスタンスで使用するために取得する既存のTCPソケットインスタンス。このプロパティはオプションであり、TCPリスナーと一緒に使用するために設計されています。サンプルはTCPリスナーセクションに記載されています。from プロパティを使用する場合、address およびport プロパティは必須ではなく、指定されていても無視されます。 |
onReadable(bytes)
新しいデータが読み取れるようになったときに呼び出されます。コールバックは、読み取れるバイト数を示す単一の引数を受け取ります。
onWritable(bytes)
追加のデータを出力するためのスペースが確保されたときに呼び出されます。コールバックは、出力バッファをオーバーフローさせずにTCPソケットに書き込めるバイト数を示す単一の引数を受け取ります。
onError
エラーが発生したときに呼び出されます。onError
が呼び出されると、接続はもはや使用できません。エラーの種類を報告することは今後の課題です。
データフォーマットは、個々のバイトに対してはnumber
、バイトのグループに対してはbuffer
です。デフォルトのデータフォーマットはbuffer
です。buffer
フォーマットを使用する場合、write
呼び出しはArrayBuffer
またはTypedArray
を受け入れます。read
呼び出しは常にArrayBuffer
を返します。
ソケットがリモートホストへの接続に成功すると、データの書き込みが可能になるため、onWritable
コールバックが呼び出されます。
リモートソケットが何らかの理由で切断されると、onError
コールバックが呼び出されます。これにはクリーンなTCP切断も含まれます。
write
リクエストに対して十分なバッファスペースがない場合、データは書き込まれず、例外がスローされます。
通常、TCPソケットを使用するスクリプトが複数の書き込み操作を単一のwrite
呼び出しにまとめる必要はありません。可能な場合、TCPソケット実装はJavaScriptイベントループの単一ターン内で発生する書き込みを結合します。
TCPソケットはlwipネットワーキングライブラリを使用して実装されています。最も低レベルの公開APIであるコールバックAPIを使用します。
バッファの代わりにバイトを読み書きするために使用されるnumber
データフォーマットのサポートは、シリアルの代わりにTCPを使用するプロトコルを実装する際に便利であることが証明されていますが、必須の機能ではありません。一方、文字列の直接サポートは重要であり、今後の課題です。
TCPソケットはリモートホストを指定するためにaddress
プロパティを受け入れます。これはいくつかの状況で必要ですが、ホスト名が既知であることが多いです。現在、ホスト名はソケットの外部で解決されます。アドレスの代わりにホスト名を渡すことができると便利です。セキュリティ上の理由から、ホスト名を使用してホワイトリストまたはブラックリストを適用し、ホストへのアクセスを制限する必要があるかもしれません。
オプションのキープアライブプロパティをコンストラクタに定義することは、将来の課題です。
次のサンプルは、HTTPサーバーに接続し、ルートに対してGETリクエストを送信し、応答をデバッグコンソールにトレースします。
new TCP({
address: "93.184.216.34", // www.example.com resolved outside this example
port: 80,
onWritable() {
if (this.requested)
return;
this.write(ArrayBuffer.fromString("GET / HTTP/1.1\r\n"));
this.write(ArrayBuffer.fromString("Host: www.example.com\r\n"));
this.write(ArrayBuffer.fromString("Connection: close\r\n"));
this.write(ArrayBuffer.fromString("\r\n"));
this.requested = true;
}
onReadable(count) {
trace(String.fromArrayBuffer(this.read()));
}
onError() {
trace("\n\n** Disconnected **\n");
}
});
組み込みのTCP Listener
クラスは、着信TCP接続要求をリッスンして受け入れる方法を提供します。
import Listener from "embedded:io/socket/listener";
プロパティ | 説明 |
---|---|
port |
リッスンするポートを指定する数値。このプロパティはオプションです。 |
onReadable(requests)
1つ以上の新しい接続要求が受信されたときに呼び出されます。コールバックは、保留中の接続要求の総数を示す単一の引数を受け取ります。
TCP Listener
クラスは、唯一のデータ形式として socket/tcp
を使用します。
read
関数は TCP
ソケットインスタンスを返します。このインスタンスはすでにリモートホストに接続されています。read
および write
関数は通常通り動作します。コールバック関数はインストールされていないため、スクリプトは onReadable
、onWritable
、または onError
の通知を受け取ることはできません。ソケットを構成するには、オプションの from
引数を使用して TCP
ソケットコンストラクタに渡します。以下にサンプルを示します。
コンストラクタは、特定のネットワークインターフェースにバインドするためのオプションの address
プロパティをサポートする必要があります。
次のサンプルは、シンプルなHTTPエコーサーバーを実装しています。着信要求を受け入れ、要求全体(要求ヘッダーを含む)を応答ボディとして返します。TCPEcho
クラスは要求を読み取り、応答を生成します。
class TCPEcho {
constructor(options) {
new TCP({
...options,
onReadable: this.onReadable
});
}
onReadable() {
const response = this.read();
this.write(ArrayBuffer.fromString("HTTP/1.1 200 OK\r\n"));
this.write(ArrayBuffer.fromString("connection: close\r\n"));
this.write(ArrayBuffer.fromString("content-type: text/plain\r\n"));
this.write(ArrayBuffer.fromString(`content-length: ${response.byteLength}\r\n`));
this.write(ArrayBuffer.fromString("\r\n"));
this.write(response);
this.close();
}
}
new Listener({
port: 80,
onReadable(count) {
while (count--) {
new TCPEcho({
from: this.read()
});
}
}
});
組み込みの UDP
ネットワークソケットクラスは、UDPパケットの送受信を実装します。
import UDP from "embedded:io/socket/udp";
プロパティ | 説明 |
---|---|
port |
UDPソケットをバインドするローカルポート番号。このプロパティはオプションです。 |
onReadable(packets)
1つ以上のパケットが受信されたときに呼び出されます。コールバックは、読み取ることができるパケットの総数を示す1つの引数を受け取ります。
データフォーマットは常に buffer
です。write
呼び出しは ArrayBuffer
または TypedArray
を受け入れます。read
呼び出しは常に ArrayBuffer
を返します。
read
呼び出しは、完全なUDPパケットを ArrayBuffer
として返します。部分的な読み取りはサポートされていません。返されたパケットデータには次の2つのプロパティが付属しています:
address
、パケット送信者のアドレスを含む文字列port
、パケット送信に使用されたポート番号
write
呼び出しは3つの引数を取ります:リモートアドレス文字列、リモートポート番号、およびパケットデータとしての ArrayBuffer
または TypedArray
。パケットを送信するのに十分なメモリがない場合、write
呼び出しは例外をスローします。
UDPソケットは lwip ネットワーキングライブラリを使用して実装されています。最も低レベルのlwip公開APIであるコールバックAPIを使用します。
マルチキャストをサポートするためにコンストラクタにオプションのプロパティを指定することは、将来の作業領域です。
TCPソケットと同様に、リモートエンドポイントのホスト名を指定できると便利です。
次のサンプルは、アドレス208.113.157.157のネットワークタイムサーバーから現在の時刻を取得するシンプルなSNTPクライアントを実装しています。応答が受信されるとUDPソケットは閉じられます。このサンプルは、受信したUDPパケットの送信者を示す address
および port
プロパティにアクセスする方法を示しています。
let sntpClient = new UDP({
onReadable: function(count) {
const buffer = this.read();
trace("Packet from ${buffer.address}:${buffer.port}\n`);
const packet = new DataView(buffer);
let milliseconds = (packet.getUint32(40) - 2208988800) * 1000;
trace("SNTP time " + (new Date(milliseconds)) + "\n");
this.close();
},
});
const packet = new Uint8Array(48);
packet[0] = (4 << 3) | (3 << 0); // version 4, mode 3 (client)
sntpClient.write("208.113.157.157", 123, packet);
ESP8266をディープスリープにすることはIOの範囲外です。System.deepSleep()
JavaScript関数は開発目的で提供されています。
プロパティ | 説明 |
---|---|
pin |
最近のウェイクがコールドブートによるものかディープスリープからのウェイクによるものかを検出するために使用するピン。ESP8266の場合、リセットピンには文字列 "RST" を設定する必要があります。 |
onReadable()
デバイスがディープスリープからウェイクされた場合、インスタンス化後に呼び出されます。
Wakeable Digital IOは常にnumber
のデータフォーマットを使用します。値が0の場合、デバイスはディープスリープからウェイクされていないことを示し、値が1の場合、ディープスリープからウェイクされたことを示します。
read
呼び出しはインスタンス化直後に利用可能です。したがって、マイクロコントローラがブートした後にピンの状態が変わることはないため、onReadable
コールバックは厳密には必要ありません。onReadable
コールバックは、プログラムの実行を終了しないライトスリープで有用です。
次のサンプルでは、Wakeable Digitalピンを使用してデバイスがハードリセットされたかディープスリープからウェイクされたかを確認します。
let wakeable = new WakeableDigital({
pin: "RST",
});
trace(wakeable.read() ? "Woke from deep sleep\n" : "Hard reset\n");
IOプロバイダーは、組み込みのIOリソース外部のIOリソースにアクセスします。IOプロバイダーは、外部のIOリソースにアクセスするために組み込みのIOリソースを使用することがよくあります。「外部」の定義は広範囲にわたります。
-
マイクロコントローラと同じボード上の別のコンポーネント。
これにはGPIOやアナログエクスパンダーが含まれます。これらはI2CやSPIのような共有バスプロトコルを介して動作し、追加のIOピンを提供します。
-
マイクロコントローラを保持するボードに物理的に接続された別のボード。
これの例として、Firmataプロトコルで使用されるシリアル接続を介してマイクロコントローラに接続されたArduinoがあります。
-
近接する別の物理デバイス。
これには、Bluetooth LEで接続された周辺機器や、UDPネットワーク接続を介してmDNSを使用する分散型アンビエント同期(DAS)が含まれます。
-
物理的に遠隔地にある別の物理デバイス。
これには、TCP接続上で動作するFirmataプロトコルや、HTTP/REST、MQTT、WebSocketなどのプロトコルを使用して動作する多くのIoTクラウドサービスが含まれます。
プロバイダのコンストラクタは、プロバイダを構成するプロパティを含む単一のオブジェクトとして、IOの種類と同じAPIを持ちます。次のサンプルでは、I2Cインターフェースを介して16のGPIOピンを提供するコンポーネントであるMCP23017 GPIOエクスパンダをインスタンス化します。
import Expander from "expander";
const expander = new Expander({
sda: 5,
scl: 4,
hz: 1000000,
address: 0x20,
});
コンストラクタは、外部IOリソースへの接続を確立するために必要なすべてのプロパティを受け取ります。IOリソースと同様に、これらのプロパティは構築時に固定されます。このサンプルでは、コンストラクタに渡されるプロパティは、コンポーネントがI2Cを介して通信するため、I2C接続を初期化するために必要なものと同じです。必要に応じて、外部IOリソースへの接続を構成するための追加のプロパティを定義することができます。
スクリプトがプロバイダを使用する必要がなくなった場合、インスタンスを閉じて接続を解除し、予約されたリソースを解放する必要があります。MCP23017エクスパンダの場合、クローズ操作はコンポーネントと通信するために使用されるI2C
インスタンスを解放します。
expander.close();
プロバイダから利用可能なIOリソースは、コンストラクタがプロバイダインスタンス上に配置されているIOクラスパターンに従います。次のサンプルでは、エクスパンダのピン13に書き込み操作を行います。
let led = new expander.Digital({
pin: 13,
mode: expander.Digital.Output,
});
led.write(1);
同様に、いくつかのデジタルピンはDigitalBank
を介して一緒にアクセスすることができます。次のサンプルでは、ピン8から15(含む)の値を読み取ります。
let buttons = new expander.DigitalBank({
pins: 0xFF00,
mode: expander.Digital.Input,
});
let result = buttons.read();
インスタンスからIOコンストラクタにアクセスするこの方法は、Johnny-Fiveロボティクスフレームワークで使用される方法と似ています。以下は、Johnny-Fiveドキュメントのhello worldサンプルの一部です。
var led = new five.Led(13);
led.blink(500);
MCP23017エクスパンダーには、1つ以上の入力の値が変化したときに割り込みをトリガーするオプションがあります。この機能を使用するには、コンストラクタをinterrupt
プロパティで構成する必要があります。このプロパティは、割り込みが接続されている組み込みGPIOピンを示します。次のサンプルでは、interrupt
プロパティが0に設定されており、割り込みがデジタルピン0に接続されていることを示しています。
const expander = new Expander({
sda: 5,
scl: 4,
hz: 1000000,
address: 0x20,
interrupt: 0,
});
割り込みが構成されると、onReadable
コールバックを使用できます。
let buttons = new expander.DigitalBank({
pins: 0xFF00,
rises: 0xFF00,
falls: 0xFF00,
mode: expander.Digital.Input,
onReadable(pins) {
const result = this.read();
trace(`Pins ${pins.toString(2)} changed. Buttons now ${result.toString(2)}.`);
}
});
注: 慣例として、IOクラスパターンの実装は、それが関連付けられているハードウェアを直接表現し、その機能と制限の両方を反映します。例えば、ポーリングを使用して割り込みピンを使用せずにonReadable
コールバックをサポートすることは可能ですが、これは高レイヤーにハードウェアの機能を正確に反映するために推奨されません。これにより、低レベルの実装を小さく、保守可能で効率的に保つことができます。もちろん、高レイヤーは、サポートするプログラミングモデルに一致するように、そのような機能を必要に応じて追加することができます。
プロバイダーを介してアクセスされるIOリソースは、非ブロッキングIOに関する一般的なルールが尊重される限り、同期および/または非同期操作をサポートすることができます。IOクラスパターンは、非同期操作が完了したときに呼び出すコールバックを定義します。プロバイダーはこれらを実装する必要はありませんが、表現するIOリソースが完了するまでに時間がかかる場合には、これを実装することが奨励されます。
一部のIOリソースは、コンストラクタが返された直後には使用できません。TCPクライアントコンストラクタはその一例であり、TCP接続が確立されるのを待ってからIO操作を行う必要があります。
IOプロバイダーは、リモートリソースへの接続が確立されるまで、どのIOリソースが利用可能かを知ることができません。このため、プロバイダーはコンストラクタが完了してからしばらくの間、そのインスタンスにIOコンストラクタを持たない場合があります。そのようなプロバイダーは、プロバイダーが使用可能になったときにスクリプトに通知するためにonReady
コールバックをサポートする必要があります。
let provider = new CloudProvider({
url: "mqtt://www.example.com",
onReady() {
let led = new this.Digital({
pin: 13,
mode: this.Digital.Output,
});
led.write(1);
}
});
MCP23017エクスパンダーは、マイクロコントローラーと同じボード上の別のコンポーネントがアクセスするため、onReady
コールバックを実装していないことに注意してください。そのため、重大な遅延はありません。
このドキュメントで以前に定義されたすべてのIO種類は、IOクラスパターンに直接従うことで非同期IOを実装しています。I2Cマスターを実装する方法はそれほど明白ではありません。IOプロバイダーAPIを通じたFirmataクライアントの実装は、この問題を探求し、解決策を見つける動機を提供しました。
I2Cは、シリアルおよびTCP IOと同様に、バイトのバッファを使用して読み書き操作を行います。シリアルおよびTCP(接続が確立された後)は本質的にピアプロトコルであり、接続のどちら側もいつでも書き込み操作を開始できます。対照的に、I2Cはマスター/スレーブプロトコルです。スレーブは、マスターが読み取りを要求したときにのみバイトを送信できます。そのため、マスターはデータを受け取るためにスレーブデバイスに読み取り要求を発行する必要があります。
I2Cのマスター/スレーブプロトコルを非同期にサポートするために、読み取り操作は2つのステップに分けられます。最初のステップは読み取り要求の発行です。I2Cでは、マスターは読み取るバイト数を指定します。したがって、read
関数の呼び出しには読み取るバイト数を含める必要があります。次の例では、読み取るバイト数は4です。
i2c.read(4);
これは読み取り操作をキューに入れますが、データは返しません(データが利用できない場合の結果であるundefined
を返します)。データが利用可能になると、プロバイダーはonReadable
コールバックを呼び出します。I2C
インスタンスを使用するスクリプトは、引数なしでread
関数を呼び出すことで読み取り操作の結果を取得します。
let i2c = new provider.I2C({
onReadable() {
let data = this.read();
trace(`I2C read returned ${data.byteLength} bytes.\n`);
}
});
i2c.write(Uint8Array.of(4));
i2c.read(2);
引数なしのread
呼び出しは、最も早く保留中の読み取り操作の結果を返します。結果が利用できない場合はundefined
を返します。この先入れ先出しのルールは、複数の非同期読み取り操作が保留中の場合に予測可能な動作を保証します。
IOクラスパターンは、JavaScriptでの幅広いIO用途に対応するために設計された小さなAPIです。コアAPIは、constructor
、close
、read
、write
の4つの関数と、onReady
、onReadable
、onWriteable
、onError
のいくつかのサポートコールバック関数のみで構成されています。この基盤から、個々のデジタル入力&出力、デジタルバンクの入力&出力、アナログ入力、シリアル、I2Cマスター、TCPソケット、TCPリスナー、UDPソケット、ウェイクピンの実装が作成されています。プロバイダークラスパターンは、あらゆる種類のリモートIOリソースで動作するようにIOクラスパターンを拡張します。
Moddable SDKのXSエンジンを使用してESP8266マイクロコントローラー向けにIOクラスパターンの広範なサポートを実装することで、さまざまな視点からAPIを使用する経験が得られます。実装自体はシンプルで、生のハードウェアI/OリソースをJavaScript言語に接続することに焦点を当てています。XS in C APIを使用して、ネイティブのIOの動作をJavaScript APIに適応させるための翻訳レイヤーは一切必要ありません。これは、そのような翻訳が信頼性を持って実装するのが難しいため、望ましいことです。
APIの視点から見ると、IOクラスパターンは新しいIOタイプのサポートを追加する設計者に明確なガイダンスを提供します。設計は、各IOタイプのAPIをゼロから定義するのではなく、IOの機能がどのようにパターンに適合するかを考慮することから始まります。このパターンは、さまざまなIO種類に適応できることが証明されています。将来の実装作業では、これを探求し、間違いなく拡張していくでしょう。
おそらく最も興味深い視点は、パターンに従うIOクラスを使用するスクリプトライターとしての視点です。小さなAPIサイズは覚えやすいです。これにより、さまざまなIOを迅速かつ快適に操作できます。もちろん、IOタイプごとに異なる詳細があります。デジタル入力はUDPソケットとは大きく異なります。それでも、これらの違いはIOのニーズに一致しており、異なる個人が異なる時期に異なる優先順位やプログラミングスタイルの好みで設計したために生じる任意の違いではありません。全体として、IOクラスパターンを適用するコードを読み書きするのは比較的簡単です。
このマイクロコントローラ用のIOクラスパターンの実装演習に基づいて、設計はその目標をうまく達成しています。APIは低レベルのスクリプト開発者がIOにアクセスするためのニーズを満たしており、リソースが制約された組み込みハードウェア上で効率的に実装することが可能であり、実装/移植の努力は集中して管理可能です。
IOクラスパターンを完全に探求するためには、まだ多くの作業が残っています。将来の作業からさらに多くのことが学ばれ、その教訓は設計の改良につながるでしょう。将来の作業の分野には、他のマイクロコントローラへの移植、Moddable SDK以外の他のランタイム環境のサポート、他の種類のIOおよびプロバイダの実装が含まれます。