Comment on page
Parser
シリアル電文パーサーの利用例
TWELITE 無線マイコンから UART (シリアルポート)経由での電文書式を解釈します。電文書式は、可読性・エラー検出などを目的として、伝送したいデータ列に対して所定の変換を行います。
ここではアスキー形式の解釈を行う
AsciiParser
について解説します。アスキー形式は、バイナリで構成されたデータ列を文字列で表現する方法です。TWELITE無線マイコンでは最も良く用いられる形式です。
例えばバイト列で
00A01301FF123456
をアスキー形式で表現すると、以下のようになります。先頭は :
で B1
がチェックサム、終端は [CR:0x0d][LF:0x0a]
となります。:00A01301FF123456B1[CR][LF]
終端のチェックサムを省略できます。チェックサムからCRLFの系列を
X
に置き換えます。文字化けによる誤ったデータ系列には弱くなりますが、実験などでデータを送付したいときに便利です。:00A01301FF123456X
====== | 元データのバイト数 | バイト数 | 解説 |
ヘッダ | | 1 | : (0x3A) コロンを指定します。 |
データ部 | N | 2N | 元データの各バイトをアスキー文字列2文字(A-F は大文字)で表現します。
例えば 0x1F は 1 (0x31) F (0x46) と表現します。 |
チェックサム | | 2 | データ部の各バイトの和を8ビット幅で計算し2の補数をとります。つまりデータ部の各バイトの総和+チェックサムバイトを8ビット幅で計算すると0になります。
チェックサムバイトをアスキー文字列2文字で表現します。
例えば 00A01301FF123456 では 0x00 + 0xA0 + ... + 0x56 = 0x4F となり、この二の補数は0xB1 です。(つまり 0x4F + 0xB1 = 0x00) |
フッタ | | 2 | [CR] (0x0D) [LF] (0x0A) を指定する。 |
// serial parser
AsciiParser parse_ascii(256);
上記の例では
parser_ascii
というオブジェクトを内部バッファ256バイトで生成しています。このバッファサイズは、書式の解釈後に必要なバイト数です。ASCII形式では実際のバイト数の約2倍の書式になります。例えば書式が200バイトの系列の場合は、実データは約100バイトになります。
無線パケットの最大格納バイト数が100バイト強であるため、この例では余裕をもって256バイトのバッファとしています。
このAsciiParserは1バイトずつの処理を行います。シリアルポートからは1バイトずつデータが到着するためです。
while (Serial.available()) {
int c = Serial.read();
parse_ascii << char_t(c);
if (parse_ascii) {
// completed!
}
}
上記例では、シリアルポートから1バイト読み出しては
parse_acsii
に1バイトずつ<<演算子を用いて読み込ませています。直後の
if(pars_ascii)
での判定は、アスキー形式の系列が正しく解釈できたかどうかを判定しています。
if (parse_ascii) {
auto&& p = parse_ascii.get_payload();
}
以下のように配列にアクセスできます。
auto&& p = parse_ascii.get_payload();
if (p[0] == 0x80) { ... } // 最初の要素
int len = p.length(); // 要素数
for (auto&& x : p) { // 各要素にアクセス
Serial.print(x, HEX);
Serial.print(" "); }
Parser
オブジェクトにデータ列が格納されている場合、>>
演算子を用いることで書式をシリアルポート(Serial
)やターミナル(ITerm
)などに出力することが出来ます。inline void IParser::operator >> (TWE::IStreamOut& fo_putchar)
// パーサーオブジェクトの宣言
AsciiParser o_parser(128);
// コード例
auto&& b = o_parser.get_payload(); // SmplBuf_Byte&としてデータ列を参照
b.clear();
b << 0x00;
b << 0x11;
b << 0x22;
b << 0x33;
// ターミナルへ出力(ITerm)
o_parser >> the_screen; // the_screen はターミナル(ITerm)
// -> :001122339A[改行]
// バッファーへ出力
SmplBuf_ByteSL<128> b1;
o_parser >> b1;
// -> ":001122339A[CR][LF]"
通常は、解釈用の
Parser
オブジェクトと出力用のParser
オブジェクトは別々に用意します。解釈用のオブジェクトはシリアル入力など都度処理が行われます。このオブジェクトを一時的なデータ出力の目的で使用すると、解釈中の内部データが破壊されます。
auto&& pkt = newTwePacket(parse_ascii.get_payload());
E_PKT pkt_typ = identify_packet_type(pkt);
if (pkt_typ == E_PKT::PKT_PAL) {
// TWELITE PAL (App_PAL)
} else {
// unknown packet type
}
newTwePacket()
は入力されたデータ系列を解釈して、spTwePacket
型のオブジェクトを生成します。このオブジェクトはTwePacket
型のデータを格納するスマートポインタstd::shared_ptr
で、メモリ管理を簡素化することができます。パケット種別は
E_PKT
列挙体で定義されています。ここではTWELITE PALの出力形式PKT_PAL
の解釈を行います。std::shared_ptr
の利用は、パケット情報を他にコピーして利用する場合を想定しています。例えば履歴配列に保存して、過去の情報を参照するような場合です。オブジェクトは参照カウンタにより管理されていて、所有者が0になれば破棄されます。
解釈されたパケットの種別は
identify_packet_type()
により判別します。パケット種別はE_PKT
列挙体で定義されています。
if (pkt == E_PKT::PKT_PAL) {
auto&& pal = refTwePacketPal(pkt);
PalEvent ev;
// acquire event data.
if (pal.has_PalEvent()) {
ev = pal.get_PalEvent();
}
switch(pal.get_PalDataType()) {
case E_PAL_DATA_TYPE::EVENT_ONLY:
break;
case E_PAL_DATA_TYPE::MAG_STD:
break;
case E_PAL_DATA_TYPE::AMB_STD:
break;
case E_PAL_DATA_TYPE::EX_CUE_STD:
break;
}
解釈したパケットが
E_PKT::PKT_PAL
と判定された場合は、refTwePacketPal()
によりTwePacketPal
型として参照できます。上記コード例ではユニバーサル参照auto&&
を用いて型名を推測させています。PALのパケット書式は様々な派生があります。
解釈時にまずイベントデータがあるかどうか取り出します。イベントが含まれるパケットは
pal.has_PalEvent()
により判定できます。イベントのみ受信できれば良い場合はpal.get_PalEvent()
で得られたイベントデータを参照して終了です。case E_PAL_DATA_TYPE::MAG_STD:
{
// generate pal board specific data structure.
PalMag mag = pal.get_PalMag();
switch(mag.u8MagStat) {
case 0: /* no magnet */ break;
case 1: /* N Pole */ break;
case 2: /* S Pole */ break;
}
}
break;
上記の例では、MAG(OPEN-CLOSE SENSE PAL)のオブジェクト
PalMag mag
を生成しています。ここではオープンクローズに応じた分岐にmag.u8MagStat
を読み出しています。最終更新 1yr ago