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) を指定する。

AsciiParser の使用方法

オブジェクトの生成

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

解釈済みの系列はget_payload()メソッドにて取得できます。get_payload()SmplBuf_Byteの参照型を戻します。

以下のように配列にアクセスできます。

auto&& p = parse_ascii.get_payload();

if (p[0] == 0x80) { ... } // 最初の要素
int len = p.length();     // 要素数
for (auto&& x : p) {      // 各要素にアクセス
  Serial.print(x, HEX);
  Serial.print(" "); }

ASCII書式での出力

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列挙体で定義されています。

PALパケットの解釈

if (pkt == E_PKT::PKT_PAL) {
    auto&& pal = refTwePacketPal(pkt);
    
    if (pal.u8palpcb == E_PAL_PCB::MAG) {
      // for MAG
    } else
    if (pal.u8palpcb == E_PAL_PCB::AMB) {
      // for AMB
    }
}

解釈したパケットがE_PKT::PKT_PALと判定された場合は、refTwePacketPal()によりTwePacketPal型として参照できます。上記コード例ではユニバーサル参照auto&&を用いて型名を推測させています。

PALボード種別解釈

if (pal.u8palpcb == E_PAL_PCB::MAG) {
    PalMag mag = pal.get_PalMag();
    
    if (mag.u8MagStat == 0) {
       // closed
    } else {
       // opened
    }
}

PALのボード種別に応じたオブジェクトを生成して、ボード種別特有のデータにアクセスすることが出来ます。

上記の例では、MAG(OPEN-CLOSE SENSE PAL)のオブジェクトmagを生成しています。ここではオープンクローズに応じた分岐にmag.u8MagStatを読み出しています。

最終更新