Skip to content

linetsとtscatコマンド

Rich Mikan edited this page Jul 15, 2020 · 1 revision

標準入出力上のデータの流れるタイミングを記録・再生する方法

英語版はこちら(English document is here

例えば何らかのIoTデバイスがあるとします。そしてそれは、/dev/ttyのような特殊ファイルからメッセージが送られる形式の場合もあります。あるいは、ネットワーク上からcURLやnetcat、MQTTなどで取得する場合もあります。そしてあなたは今、そのデバイスからのデータを取り込んで何らかの動作をするシステムのプログラムを開発する立場にあるとしましょう。そんな時、あなたは届くデータのタイミングが再現できたら嬉しいと思いませんか?

今日はそういう場面で役立つ2つのコマンドを紹介します。一つは、"linets"という標準入出力を流れるデータのタイミングを記録するコマンド。もう一つは、"tscat"というそのタイミングを再現するコマンドです。

コマンドの紹介

"linets" ― 各行にタイムスタンプを付与するコマンド

このコマンドは標準入力からデータを受け取り、そして直ちに標準出力にそれらを送り出します。ただし、送り出す前に、それら各行がコマンドに到来した日時のタイムスタンプを付与するようになっています。

具体的には次のようにしてタイムスタンプを付与できます。

$ cat /dev/ttyS1 | linets
20200318200358 Go
20200318200301 Stop
20200318200301 Turn left
20200318200302 Go
           :
           :

なおタイムスタンプのデフォルトフォーマットは、そのホストが存在するタイムゾーンでのカレンダー形式(YYYYMMDDhhmmss)です。(これは環境変数TZを使うと変更可能です)

タイムスタンプフォーマットはオプションで変更可能です。"-e"を指定すればUNIX時間になります。"-Z"を指定した場合は、最初の1行が到来した時刻を0とし、そこからの経過秒数で表現されます。そして、"-3"、"-6"、"-9"というオプションを指定すれば、小数点以下の時刻も付与できます。例えば"-3"であれば、".nnn"という小数点以下の3桁が付きます。つまりミリ秒です。これらを踏まえ、次の例を見てください。

$ cat /dev/ttyS1 | linets -Z3
0 Go
3.501 Stop
3.999 Turn left
4.499 Go
      :
      :

詳細は、コマンドヘルプまたはGitHub上のソースファイルを見てください。

"tscat" ― タイムスタンプを考慮したcatコマンド

このコマンドは、各行の先頭にタイムスタンプが付与されたテキストデータを受け取り、それを標準出力に書き出しますが、タイムスタンプが示す時刻になるまで待機するのと、そのタイムスタンプ文字列を除去するという動作をします。もっとも単純な例は次のとおりです。

$ date
Wed Mar 18 20:30:00 JST 2020

(Suppose that you executed the following command in 0 seconds)
$ echo '20200318203005.5 foo' | tscat
foo                                  <== この行は実行してから5.5秒後に表示される
$ 

上記の例では、まずdateコマンドで日時を確認し、その5.5秒後の日時を示すタイムスタンプを付けた文字列を(一切の間をあけずに)tscatコマンドに与えたという想定の例です。デフォルトではカレンダー時間(YYYYMMDDhhmmss)のフォーマットでタイムスタンプが与えられたものと解釈します。なお、そのカレンダー時間はホストがある地域のタイムゾーンに属していますが、これは環境変数TZで変更可能です。

また、既に上記の例で示したように、小数点以下を(ナノ秒単位まで)指定することも可能です。そして、"-e"オプションを指定すれば、次の例のようにカレンダー時間の代わりにUNIX時間を受け入れ可能になります(下記の例が表しているUNIX時間は上記の例と同一の日時です)。

$ date
Wed Mar 18 20:30:00 JST 2020

(Suppose that you executed the following command in 0 second)
$ echo '1584531005.5 foo' | tscat -e <== 1584531005 = 2020-03-18 20:30:05
foo                                  <== この行は実行してから5.5秒後に表示される
$ 

そして重要なのが"-Z"オプションです。これを指定すると、tscatコマンドはタイムスタンプの示す日時に関わらず1行目を直ちに出力します。そして2行目以降はそれぞれ、1行目のタイプスタンプとの時間差に基づいて出力されます。以下にその例を記します。

$ date
Wed Mar 18 20:30:00 JST 2020
$ cat <<__TEXTDATA__ | tscat -Z
> 20390101102030 1st   <== このタイムスタンプは明らかに未来の日時
> 20390101102031 2nd
> 20390101102040 3rd
__TEXTDATA__
1st   <== この行はすぐに表示される
2nd   <== この行は上の行の1秒後に表示される
3rd   <== この行は上の行のさらに9秒後に表示される
$ 

最初にdateコマンドで日時を確認し、次のcatコマンドではその日時より明らかに未来の日時を指定しています。"-Z"オプションを付けたことにより1行目が直ちに出力され、以下、1秒後、さらにその9秒後に出力されます。このオプションはとても有用で、過去に記録したタイミングをいつでも再現する目的にも使えます。

詳細は、コマンドヘルプまたはGitHub上のソースファイルを見てください。

"linets"と"tscat"の組み合わせによるテクニック

この記事では、2つのテクニックを紹介することにします。

タイミングの記録と再生

今、手元にとあるロボットがあるとします。そしてそのロボットは、テキストメッセージによる命令を、"robot_controller"というコマンドに流し込むことによって操縦します。一方、その操作用のメッセージは今、"/dev/ttyS1"から送られてくることになっており、従って次のようにして操縦できます。

$ cat /dev/ttyS1           | <== /dev/ttyS1からロボットの指示が送られる
> robot_controller           <== 指示に基づきロボットを操作する何らかのコマンド

まず最初に、上記の"cat"と"robot_controller"コマンドの間に、次の3つのコマンドを挿みましょう。1つ目のlinetsはミリ秒までの時刻を付与しています。2つ目のteeコマンドはその結果、各行がそれぞれ何時送られてきたかというタイミング情報の付加されれたデータを"data_with_timing.txt"というファイルに書き出しています。3つ目のsedコマンドは、タイムスタンプ情報を除去するためのものです。なお、そのsedコマンドの手前にあるptwというコマンドは、フルバッファリングモードを回避するためのものです。詳しくは、ptwコマンドの記事を見てください。

$ cat /dev/ttyS1           |
> linets -3                | <== ミリ秒までのタイムスタンプを付加
> tee data_with_timing.txt | <== タイムスタンプ付加データをファイルに保存
> ptw sed 's/^[^ ]* //'    | <== タイムスタンプ文字列を取り除き、元に戻す
> robot_controller

なぜ除去するのかというと、タイムスタンプが来ることなど想定していない"robot_controller"を正しく動かすためです。このようにして、既存のコマンドに影響を及ぼさないようにしながらタイミング情報を記録できます。

記録したら、今度は生成されたファイルを、tscatコマンドを経由させてから"robot_controller"に送りましょう。そうすれば、ロボットは先程とまったく同じようにダンスしてくれます。

$ cat data_with_timing.txt |
> tscat -Z                 | <== 完全に同じタイミングでメッセージを次のコマンドに送信する
> robot_controller

ロボットの動作を元々のスピードから半分にする

単にタイミングを再現するのみならず、再現スピードを変更することもできます。

まずは先程の例と同様にしてタイミングを記録しましょう。ただし、linetsコマンドのオプションが若干異なります。今度は"-z"オプションを付け足します。これは、デフォルトのカレンダー時間の代わりに、linetsコマンド起動時からの秒数を使うためのものです。

$ cat /dev/ttyS1           |
> linets -z3               | <== linetsコマンド起動時からの秒数をミリ秒単位まで付加する
> tee data_with_timing.txt |
> ptw sed 's/^[^ ]* //'    |
> robot_controller

そして、ロボットのダンスを再現するにあたり、次のようなAWKコマンドを間に挿んでください。これは1行目の値(すなわち秒数)を2倍するものです。

$ cat data_with_timing.txt                       |
> ptw awk '{print $1*2,substr($0,strlen($1)+2)}' | <== 1列目の値を2倍にする
> tscat -Z                                       |
> robot_controller

この数を増やせば、ロボットの動きをもっとじっくり観察できるようになるでしょう。

注意事項

linetsコマンドはナノ秒単位までタイムスタンプを付けられます。しかし、その精度はお使いのコンピューターの性能に依存します。2020年現在、そこまでの精度を出せるコンピューターは殆どありません。

tscatもまた、ナノ秒単位までのタイムスタンプを解釈できますが、精度はいくつかの要素の影響を受けます。例えば、まずコンピューターの性能、そしてカーネルが持つスケジューリングアルゴリズムの出来栄え、今OS上にあるプロセスの総数、あるいは自分のプロセス優先度、などです。このコマンドはタイミング再現のためにnanosleeep()というシステムコールを利用しますが、大抵の場合「寝坊」します。これは、殆どのUNIX系OSがマルチタスクOSであるという宿命によります。マルチタスクOSでは、例えsleepし終えても、直ちに自分に処理実行の順番が回ってくるとは限らないからです。