Brotherの複合機の通信プロトコル解析(スキャナ部分)(3) - スキャナへのPCの登録

以下の続き。

本題のスキャンデータの取り込み部分をまとめていたらめんどくさくなってきたので、やっぱり簡単なほうから先に書くことにする。スキャナ側の操作で取り込み先として指定するPC一覧に表示されるPCの登録方法についてまとめる。

以下、PC名をhogeIPアドレスを192.168.0.1、スキャナのIPアドレスを192.168.0.2とする。スキャナへのPCの登録は、概要にも書いたように、PCからスキャナにSNMPのSet-Requestを送ることで行う。

  • OID: 1.3.6.1.4.1.2435.2.3.9.2.11.1.1.0
  • community: internal

に対して、以下のようなSet-Requestを送る。

TYPE=BR;BUTTON=SCAN;USER="hoge";FUNC=IMAGE;HOST=192.168.0.1:54925;APPNUM=1;DURATION=360;
TYPE=BR;BUTTON=SCAN;USER="hoge";FUNC=OCR;HOST=192.168.0.1:54925;APPNUM=3;DURATION=360;
TYPE=BR;BUTTON=SCAN;USER="hoge";FUNC=EMAIL;HOST=192.168.0.1:54925;APPNUM=2;DURATION=360;
TYPE=BR;BUTTON=SCAN;USER="hoge";FUNC=FILE;HOST=192.168.0.1:54925;APPNUM=5;DURATION=360;

末尾に改行は含まない。4つのリクエストを同じOID宛に同時に送る。FUNC=に対応する値IMAGE、OCR、EMAIL、FILEが名前のとおりそれぞれのメニュー項目に対応する。ここで、1つのデータしか送らなかった場合などは、指定されなかったデータは登録を抹消される。

  • TYPE: BR固定。ブラザー?
  • BUTTON: SCAN固定。
  • USER: 表示されるPC名。ASCII文字以外は"?"となるようだ。最長16文字。PC名に設定された'"'やその他記号のエスケープはしていない。
  • FUNC: メニュー項目。
  • HOST: スキャナからのスキャンのリクエストを受け付けるPCのIPアドレスとポート。ここにUDPのパケットが送られる。
  • APPNUM: スキャンのリクエスト時に一緒に送られる。Brotherのアプリケーションでは、IMAGE: 1、OCR: 3、EMAIL: 2、FILE: 5となっているが、ほかの値でも問題ない。
  • DURATION: スキャナに登録される有効時間を秒単位で指定する。Brotherのアプリケーションでは、360秒を指定している。有効時間内に再度登録された場合は、(長くても短くても)新しい値で上書きされる。最長でどの程度まで有効なのかは未調査。
  • BRID: パスワードを設定した場合は、ここに16進8桁の値が設定される。パスワードを設定しない場合はこの項目はない。

以上の値を設定し、上の例のように値の後ろにセミコロンをつけてつなげる。USERに設定されたPC名とBRIDの組が各PCを区別するキーとなる。再度Set-Requestを送ったときに、これらが同じ場合上書きされ、どちらか片方でも異なる場合(IPアドレスが同じでも)別PCとして新規登録される。

スキャンキー用のパスワードを設定した場合は、BRIDという項目が増えて16進8桁の値が設定される。パスワードはスキャナ側でチェックするのみで、PC側でのチェックは行わない。前述のとおり、USERとBRIDの組でPCを識別しているため、パスワードの設定を変えると同じ名前のPCが複数登録されるのに、パスワードが異なるので解析中どれがどれかわからなくなって困った。BRIDは、一方向関数でなく、ビットの入れ替えとXORで復元できる簡易的なものだった。あえて解説する必要はないと思うので、割愛する。

というわけで通信手順がわかったので、スキャナにPCを登録するRubyスクリプトを書いてみた。SNMPプロトコルによるリクエストの送信には、
http://members.at.infoseek.co.jp/m6809/index-j.html
のs2nmpを使わせていただいた。

require 'socket'
require 's2nmp'

OID = '1.3.6.1.4.1.2435.2.3.9.2.11.1.1.0'
PC_ADDR = '192.168.0.1'
PC_PORT = 54925
PC_NAME = 'hoge'
SCANNER_ADDR = '192.168.0.2'

functions = [
  {:FUNC => 'IMAGE', :APPNUM => 1},
  {:FUNC => 'OCR'  , :APPNUM => 3},
  {:FUNC => 'EMAIL', :APPNUM => 2},
  {:FUNC => 'FILE' , :APPNUM => 5},
]

request = functions.map do |func|
  [OID, 4, %Q{TYPE=BR;BUTTON=SCAN;USER="#{PC_NAME}";FUNC=#{func[:FUNC]};HOST=#{PC_ADDR}:#{PC_PORT};APPNUM=#{func[:APPNUM]};DURATION=360;}]
end

snmp = SNMP.new(SCANNER_ADDR)
snmp.set('internal', request)

スクリプトを実行し、その後スキャナを操作してみると、"hoge"というPCが選択肢に現れていることを確認できた。ここで、"スキャンの実行"までしてしまうと、スキャンを行う部分はまだ用意していないので、タイムアウトまで長い間スキャナの操作できなくなるので注意。

PCの登録部分なら簡単なのですぐに書き終わるかと思ったら意外と長くなった。今回はこれで終わり。

Brotherの複合機の通信プロトコル解析(スキャナ部分)(2) - 概要

先日(id:ke-k:20100426:brother_scanprotocol)の続き。BrotherのMyMio(おそらくJUSTIOも同じ)で、ネットワーク経由でスキャンするときにどういうやり取りしてるの?って話です。ようやく本題。

Brotherのネットワークスキャナでは、スキャナ、PCのそれぞれ以下のポートで待ち受けている。

  • スキャナ側
    • snmp(161)/udp: スキャナ、プリンタの状態取得、スキャナ側の操作で保存できるPCの登録
    • 54921/tcp: スキャンデータの取得
  • PC側
    • 54925/udp: スキャナの起動通知、スキャンのリクエス

スキャンを行う大まかな流れは以下のようになっている。

  1. スキャナ起動時
    • PCの54925番ポートにUDPのブロードキャストで自分のIPアドレスとポート番号を知らせる。
  2. スキャナへのデータ取り込み先PCの登録
    • スキャナ側の操作でPCにファイルを保存する際の選択に表示されるPCは、PCからスキャナに通知を送ることで登録される。
    • SNMPプロトコルのSet-RequestでPC名、IPアドレス、ポート番号等の情報を登録する。
    • PC側のポート番号のデフォルトは54925。別の番号を登録することでおそらく変更可能。
    • 有効期間(自分で設定可)があるので、切れる前に更新する。ContorolCenterでは、期間を360秒として、約1分ごとに更新していた。
    • SNMPプロトコルは、プリンタの状態取得等にも使われているようだ。ここは今回の目的ではないので割愛。
  3. スキャナ側の操作で、保存先PCを選んで「スタートボタン」を押したとき
    • スキャナから選択したPCに、UDPでスキャンのリクエストを投げる。この際に、登録した情報もつける。
    • PCは受信したUDPのパケットをそのままスキャナに送り返しているが、返さなくても問題ないようだ。
  4. スキャンの実行
    • PCからスキャナの54921番ポートにTCP接続し、スキャンを実行する。

PCからスキャンしたいときは、1-3を省略して、4のスキャンの実行をすればOK。PCのControlCenterからスキャンを実行するのと、スキャナ側の操作でPCにデータを送信するのに、本質的な違いはなかった。スキャナ側のパネルで操作してPCに保存を選んだときは、スキャナからPCに「スキャンしてくれー」、と1つパケットを投げるだけで、あとはPCからのスキャンの実行を待つ。ここで、PCが何もしないと、スキャナ側ではタイムアウトまで(1分くらい?)操作できない。キャンセルも不可。

あと、PCのシャットダウン時に行うPCの登録解除みたいなのがあるんじゃなかろうか。もしくは有効期限切れになって消えるまでそのままかもしれない。

さて、大まかな流れを説明したところで、もう飽きてきたので、実際やり取りされるデータの詳細は次回…は、あるんだろうか。

Brotherの複合機の通信プロトコル解析(スキャナ部分)

Brotherの複合機を買った。フラットベッド+ADFのスキャナ、インクジェットプリンタ、コピー機が合わさったMyMio DCP-595CNというやつ。Amazonで1万2千円とちょっと。あと数千円出せばFAX付きのが買えたが、我が家には電話回線がないので、これで十分。

最近、紙類(だけではないが)を棄てられないのが物が多くなる一因だと気づいたので、スキャナで取り込んで電子データ化してどんどん捨てていこうという目論見。うーん、1ヶ月後には挫折していそうだ。

とにかく、そんなわけでスキャナがメイン、あとの機能はおまけで十分。画質はそこまでこだわらないので安くて手軽にスキャンして整理できるのがいい、というわけでこれに決定。ADFつき、PCからのネットワークスキャンのほかに、スキャナ側の操作だけでPCを選んでネットワーク経由でスキャンできたり、USBメモリをスキャナに挿して直接ファイル保存できるなど、安いのになかなか便利そうな機能が豊富なところがいい。

なんだか宣伝みたいになっているな。宣伝ついでに商品へのリンクを張っておこう。 BROTHER Mymio A4インクジェット複合機 DCP-595CN


で、先週に届いたので、使ってみた。
・・・うーん、便利ではあるんだけど、かゆいところに手が届かない感じ。あと、これができれば・・・と思うところがいくつか。現在の仕様の中で実現できるはずなのに、敢えて機能を省いているような気がしなくもない。ターゲットがライトユーザなんだろうか。残念ながら、これで書類を全部スキャンしてやるぜ!という気分にはなれなさそうだ。(以上はスキャナの感想。その他の機能はまったく触っていないので。)

前置きが長くなったが、動作仕様の細かなところに思うところがあったのと、ネットワークでスキャンする仕組みが気になったので、週末に解析してみた。通信プロトコル解析、なんて大仰なタイトルをつけたが、パケットを覗き見てちょこちょこっと再現するスクリプトを書いただけ。もっとちゃんとした解析はほかの人がやっている気がする。いやむしろ公式サイトに仕様が公開されてたりして。。。(確認…)なかった。需要があるのかすらわからないが、簡単なサンプルスクリプトの動作まで確認できたので、とりあえずまとめてみる。

つづく。

rule がうまく機能しない

Rakeで、 rule の依存先が複数の場合、うまく動かないことがあるようです。
以下のような Rakefile の場合、

rule '.foobar' => ['.foo', '.bar'] do |t|
  sh "cat #{t.prerequisites} > #{t.name}"
end

rule '.foo' do |t|
  sh "echo #{t.name} > #{t.name}"
end

rule '.bar' do |t|
  sh "echo #{t.name} > #{t.name}"
end

この場合、"hoge.foobar" の依存先は ["hoge.foo", "hoge.bar"] になると思うのですが、

% rake hoge.foobar
(in /tmp)
echo hoge.foo > hoge.foo
echo hoge.foo > hoge.foo
cat hoge.foo > hoge.foobar

rule の1つ目しか実行されていないようです(しかもなぜか2回)。
ただし例外として、2つ目以降のルールがタスクとして登録されている場合には問題ないようです。

% rake hoge.bar hoge.foobar
(in /tmp)
echo HOGE.BAR > hoge.bar
echo hoge.foo > hoge.foo
cat hoge.foo hoge.bar > hoge.foobar

rake.rb のソースを見ると、 attempt_rule 内で

    def attempt_rule(task_name, extensions, block, level)
      sources = make_sources(task_name, extensions)
      prereqs = sources.collect { |source|
        if File.exist?(source) || Rake::Task.task_defined?(source)
          source
        elsif parent = enhance_with_matching_rule(sources.first, level+1)
          parent.name
        else
          return nil
        end
      }
      task = FileTask.define_task({task_name => prereqs}, &block)
      task.sources = prereqs
      task
    end

enhance_with_matching_rule を呼び出すときに sources.first を渡してますが、ここは source じゃないかと思います。

        elsif parent = enhance_with_matching_rule(source, level+1)

とすることで、今回のケースは解決しました。

一応svnのtrunkとのdiffです。

Index: rake.rb
===================================================================
--- rake.rb     (revision 639)
+++ rake.rb     (working copy)
@@ -1756,7 +1756,7 @@
       prereqs = sources.collect { |source|
         if File.exist?(source) || Rake::Task.task_defined?(source)
           source
-        elsif parent = enhance_with_matching_rule(sources.first, level+1)
+        elsif parent = enhance_with_matching_rule(source, level+1)
           parent.name
         else
           return nil

RakeFileUtils :noop, :verbose の デフォルトオプションがおかしい

id:ke-k:20080211:rakefileutils でもちょろっと書きましたが、 RakeFileUtils がうまく動いていないようです。

nowrite(true)
verbose(true)

sh 'mkdir hoge'
mkdir 'fuga'

とすると、sh には :noop, :verbose オプションが渡されているのですが、 mkdir には渡されていません。

前回も見た

module RakeFileUtils
  # ...
  FileUtils::OPT_TABLE.each do |name, opts|
    default_options = []
    if opts.include?('verbose')
      default_options << ':verbose => RakeFileUtils.verbose_flag'
    end
    if opts.include?('noop')
      default_options << ':noop => RakeFileUtils.nowrite_flag'
    end

    next if default_options.empty?
    module_eval(<<-EOS, __FILE__, __LINE__ + 1)
    def #{name}( *args, &block )
      super(
        *rake_merge_option(args,
          #{default_options.join(', ')}
          ), &block)
    end
    EOS
  end

の部分を見直してみると、 opts.include?('verbose') としてますが、ここにはSymbolが入っているので、opts.include?(:verbose) が正しいですね。 OPT_TALBE['sh'] の代入の部分もおかしいです。
というわけで、svnのtrunkとのdiffです。

Index: rake.rb
===================================================================
--- rake.rb     (revision 639)
+++ rake.rb     (working copy)
@@ -871,8 +871,8 @@
 module FileUtils
   RUBY = File.join(Config::CONFIG['bindir'], Config::CONFIG['ruby_install_name'])

-  OPT_TABLE['sh']  = %w(noop verbose)
-  OPT_TABLE['ruby'] = %w(noop verbose)
+  OPT_TABLE['sh']  = [:noop, :verbose]
+  OPT_TABLE['ruby'] = [:noop, :verbose]

   # Run the system command +cmd+. If multiple arguments are given the command
   # is not run with the shell (same semantics as Kernel::exec and
@@ -970,10 +970,10 @@

   FileUtils::OPT_TABLE.each do |name, opts|
     default_options = []
-    if opts.include?('verbose')
+    if opts.include?(:verbose)
       default_options << ':verbose => RakeFileUtils.verbose_flag'
     end
-    if opts.include?('noop')
+    if opts.include?(:noop)
       default_options << ':noop => RakeFileUtils.nowrite_flag'
     end

これでOKのはずです。

挙動がおかしい

Rakeを使い始めて間もないですが、いろいろとおかしな挙動に当たります。仕様なのかバグなのか。短期間でこんなにハマるっていうのは、僕の使い方がそんなに特殊なんでしょうか…。
作者にコンタクトとってみたいですが、英語が苦手なのできっついです。バグらしきものに関してはパッチを上げておくので、誰か作者に送ってくれないかなぁ(他力本願)