2015年2月22日日曜日

ClosedXMLをお試し

以前に仕事で、C#のプログラムからExcelのファイルを扱ったことがある。あの時は、まだExcelの新しい形式(xslx:OpenXML)がでてくる前で、C#側からExcelのCOMオブジェクトを呼び出して、Excelファイルを操作していた。
しかし、これが厄介な代物で、Excelのバージョン間でCOMオブジェクトの互換性が無く、開発環境上でどれかのバージョンでC#プログラムをビルドすると、実行環境で別のバージョンのExcelがインストールされていると使えなかった。
で、それじゃまずい、ってことで、リフレクションを使いながら何とか切り抜けた。結構大変だった。

さて、昔話はさておき、最近のExcelでは保存形式がOpenXMLになっており、仕様がオープンのため、Excelを扱う様々なライブラリが登場しているらしい。

で、その中でも、「ClosedXML」というのが使いやすい、ってことなので、試しに使ってみた。

以下に示すプログラムでは示していないが、Foo,Bar,Bazというクラスがあって、Foo->Bar->Bazという関連を形成している。今回は、この情報をExcelファイルに書き出してみたい。

usingusing System;
using System.Collections;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Reflection;
using ClosedXML.Excel;

namespace ClosedXmlSample
{
    class Program
    {
        static void Main(string[] args)
        {
            // 不要ファイルを削除
            File.Delete("a.xlsx");


            // オブジェクトを作成
            var baz1 = new Baz() { Name = "BazObj1", SampleValue = 2.4 };
            var baz2 = new Baz() { Name = "BazObj2", SampleValue = 2.9 };
            var baz3 = new Baz() { Name = "BazObj3", SampleValue = 5.1 };

            var bar1 = new Bar() { Name = "BarObj1", Baz = baz1 };
            var bar2 = new Bar() { Name = "BarObj2", Baz = baz2 };
            var bar3 = new Bar() { Name = "BarObj3", Baz = baz3 };
            var bar4 = new Bar() { Name = "BarObj4", Baz = baz1 };
            var bar5 = new Bar() { Name = "BarObj5", Baz = baz1 };
            var bar6 = new Bar() { Name = "BarObj6", Baz = baz2 };

            var foo1 = new Foo() { Name = "FooObj1", Bar = bar1 };
            var foo2 = new Foo() { Name = "FooObj2", Bar = bar2 };
            var foo3 = new Foo() { Name = "FooObj3", Bar = bar3 };
            var foo4 = new Foo() { Name = "FooObj4", Bar = bar6 };
            var foo5 = new Foo() { Name = "FooObj5", Bar = bar6 };
            var foo6 = new Foo() { Name = "FooObj6", Bar = bar6 };

            var bazs = new ObservableCollection() { baz1, baz2, baz3, };
            var bars = new ObservableCollection() { bar1, bar2, bar3, bar4, bar5, bar6 };
            var foos = new ObservableCollection() { foo1, foo2, foo3, foo4, foo5, foo6 };


            // Linqを使って複数のコレクションの要素を結合したものを用意する
            var query = from bar in bars
                        join baz in bazs on bar.BazId equals baz.ID into gj
                        from subBaz in gj.DefaultIfEmpty()
                        select new
                        {
                            bar.ID,
                            bar.Name,
                            bar.BazId,
                            Baz_Name = (subBaz == null ? String.Empty : subBaz.Name),
                            Baz_SampleValue = (subBaz == null ? 0.0 : subBaz.SampleValue),
                        };


            // ワークブックを作成
            var wb = new XLWorkbook();

            // 4つのシートを作成
            MakeSheet(wb, foos, "FooObjs");
            MakeSheet(wb, bars, "BarObjs");
            MakeSheet(wb, bazs, "BazObjs");
            MakeSheet(wb, query.ToList(), "Query");

            // ファイルに保存
            wb.SaveAs("a.xlsx");
        }

        /// シートを作成する
        static void MakeSheet(XLWorkbook book, IList collection, string name)
        {
            var sheet = book.Worksheets.Add(name);

            sheet.Protect("123")
                .SetFormatCells()
                .SetInsertColumns()
                .SetDeleteColumns()
                .SetDeleteRows();

            PropertyInfo[] properties = collection[0].GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);

            sheet.Cell("A1").SetActive();
            MakeHeader(sheet, properties, collection[0], string.Empty, false);
            MoveToNextRowFirstColumn(sheet);

            foreach (var v in collection)
            {
                MakeValue(sheet, properties, v, false);
                MoveToNextRowFirstColumn(sheet);
            }

            sheet.Columns().AdjustToContents();
        }

        /// 次の行の先頭列に移動する
        static void MoveToNextRowFirstColumn(IXLWorksheet sheet)
        {
            sheet.ActiveCell.CellBelow(1).CellLeft(sheet.ActiveCell.Address.ColumnNumber - 1).SetActive();
        }

        /// ヘッダ行を作成する
        static void MakeHeader(IXLWorksheet sheet, PropertyInfo[] properties, object obj, string prefix, bool isExternal)
        {
            foreach (var property in properties)
            {
                if ((property.GetValue(obj) is ValueType || property.GetValue(obj) is string) == false)
                {
                    PropertyInfo[] childProperties = property.PropertyType.GetProperties(BindingFlags.Public | BindingFlags.Instance);

                    MakeHeader(sheet, childProperties, property.GetValue(obj), prefix + property.PropertyType.Name + ".", true);
                    continue;
                }

                if ((property.GetValue(obj) is Guid) && (String.Compare(property.Name, "ID", true) != 0))
                {
                    continue;
                }

                sheet.ActiveCell.Value = prefix + property.Name;
                if (isExternal)
                {
                    sheet.ActiveCell.Style
                        .Protection.SetLocked(true)
                        .Fill.SetBackgroundColor(XLColor.LightGray);
                }
                sheet.ActiveCell.CellRight(1).SetActive();
            }
        }

        /// ヘッダ以外の値の表示領域を一行分作成する
        static void MakeValue(IXLWorksheet sheet, PropertyInfo[] properties, object obj, bool isExternal)
        {
            foreach (var property in properties)
            {
                if ((property.GetValue(obj) is ValueType || property.GetValue(obj) is string) == false)
                {
                    PropertyInfo[] childProperties = property.PropertyType.GetProperties(BindingFlags.Public | BindingFlags.Instance);

                    MakeValue(sheet, childProperties, property.GetValue(obj), true);
                    continue;
                }

                if ((property.GetValue(obj) is Guid) && (String.Compare(property.Name, "ID", true) != 0))
                {
                    continue;
                }

                sheet.ActiveCell.Value = property.GetValue(obj);
                if (isExternal)
                {
                    sheet.ActiveCell.Style
                        .Protection.SetLocked(true)
                        .Fill.SetBackgroundColor(XLColor.LightGray);
                }
                sheet.ActiveCell.CellRight(1).SetActive();
            }
        }
    }
}

2015年1月3日土曜日

SATソルバのお試し(後編)

前回「SATソルバのお試し(前編)」の続きとして、SATソルバで何か例題を解いてみる。

検索すると、数独を例題としているサイトがあったので、それに倣ってみる。

実は、このサイトではruby-minisatを使って解いているので、ソースコードそのまんまでも動く。
でも、それでは学習にならないので、解説だけは参考にしながら独自に実装。
後述するが、このコードは汚いので、追試する人は先ほど紹介した参考サイトを見たほうが良いだろう。
#
# * 2 | * 4
# 1 * | * *
# ---+---
# * * | 3 *
# * * | * *
#

require "minisat"
solver = MiniSat::Solver.new

arr =
  (0...4).map do
    (0...4).map do
      (0...4).map { solver.new_var }
    end
  end

arr.each do |row|
  row.each do |square|
    solver << square
  end
end

for k in 0...4 do
  for j in 0...4 do
    solver << [-arr[0][j][k], -arr[1][j][k]]
    solver << [-arr[0][j][k], -arr[2][j][k]]
    solver << [-arr[0][j][k], -arr[3][j][k]]
    solver << [-arr[1][j][k], -arr[2][j][k]]
    solver << [-arr[1][j][k], -arr[3][j][k]]
    solver << [-arr[2][j][k], -arr[3][j][k]]
  end
end

for k in 0...4 do
  for i in 0...4 do
    solver << [-arr[i][0][k], -arr[i][1][k]]
    solver << [-arr[i][0][k], -arr[i][2][k]]
    solver << [-arr[i][0][k], -arr[i][3][k]]
    solver << [-arr[i][1][k], -arr[i][2][k]]
    solver << [-arr[i][1][k], -arr[i][3][k]]
    solver << [-arr[i][2][k], -arr[i][3][k]]
  end
end

solver << arr[0][1][1]
solver << arr[0][3][3]
solver << arr[1][0][0]
solver << arr[2][2][2]

solver.solve

ans =
  arr.map do |row|
    row.map do |square|
      (1..4).find do |i|
        solver[square[i - 1]]
      end
    end
  end

ans.each do |row|
  puts row.join(" ")
end
実行した結果は、以下の通り。
$ ruby sudoku.rb
3 2 1 4
1 4 2 3
4 1 3 2
2 3 4 1
$
どうやら正しい結果が出てるっぽい。
しかし、参考サイトとソースコードを比べると、僕のはとても汚い。

まず、同じ行に同じ数字が登場しない、というルールを表すのに、
for k in 0...4 do
  for j in 0...4 do
    solver << [-arr[0][j][k], -arr[1][j][k]]
    solver << [-arr[0][j][k], -arr[2][j][k]]
    solver << [-arr[0][j][k], -arr[3][j][k]]
    solver << [-arr[1][j][k], -arr[2][j][k]]
    solver << [-arr[1][j][k], -arr[3][j][k]]
    solver << [-arr[2][j][k], -arr[3][j][k]]
  end
end
とやってしまっている所が良くない。
ループを回すのに直値を使ってしまっているし、1行の中の4マスから任意に2マスを選択するところは、全組み合わせを手書きしてしまっている。

この部分は、参考サイトでは、
# 各行に同じ数字が 2 回以上現れない
field.each do |row|
  row.transpose.each do |vars|
    vars.combination(2) do |x, y|
      solver << [-x, -y]
    end
  end
end
となっており美しい。

僕が知らなかった、transposeやcombinationといったメソッドを利用している。
transposeは、配列の行と列を転置するメソッドの様だ。
それから、combinationはその名の通り組み合わせを返してくれるらしい。
こういうメソッドを知っていると、配列のサイズが変更になったときにも対応できる美しいコードが書ける。
いや、知らなくても、こういうメソッド無いかな?って調べれば良かったんだ。
もし、調べてみても見つからなかったら、自分でメソッドを書いても良かったはずだ。
反省。
その他、zipやらflattenなどの、配列を上手く操作するメソッドを多様されていて、とても勉強になった。

さて、このように便利なSATソルバだが、半導体のフロアレイアウトの自動決定などに使われている、ということを最近知った。会員制の記事で恐縮だが、興味のある方は一読をおススメする。

それにしても、機械に自動的に問題を解かせる、ってのは男のロマンだな。久しぶりに、手を動かしながらワクワク感を覚えた。

SATソルバのお試し(前編)

以前から使ってみたかったSATソルバを使ってみたので、その記録。

SATソルバのひとつminisatというのが、非常に高速と評判なので、これを使ってみよう。
SATソルバへは、連言標準形の形式で命題論理式を与える必要がある。
業界標準?では、DIMACS CNFという記法が一般的らしいけど、命題論理が全て数値で記述されており、とても読みにくい。
式が大きくなると書くだけで大変になるのは明白である。
そこで、命題論理式を書く部分を多少なりとも楽にするために、今回はRubyからminisatを利用できるruby-minisatを使ってみる。

以下、Ubuntu-14.04上で実行した。
$ sudo gem install ruby-minisat
Building native extensions.  This could take a while...
ERROR:  Error installing ruby-minisat:
        ERROR: Failed to build gem native extension.

        /usr/bin/ruby1.9.1 extconf.rb
/usr/lib/ruby/1.9.1/rubygems/custom_require.rb:36:in `require': cannot load such file -- mkmf (LoadError)
        from /usr/lib/ruby/1.9.1/rubygems/custom_require.rb:36:in `require'
        from extconf.rb:1:in `<main>'

Gem files will remain installed in /var/lib/gems/1.9.1/gems/ruby-minisat-2.2.0 for inspection.
Results logged to /var/lib/gems/1.9.1/gems/ruby-minisat-2.2.0/ext/minisat/gem_make.out
$
いきなりエラー。
そこで、このページを参考にして、ruby-devとg++をインストール。
$ sudo apt-get install ruby-dev
(略)
$sudo apt-get install g++
(略)
$
その後、以下の通りして、ruby-minisatのインストール完了。
$ sudo gem install ruby-minisatBuilding native extensions.  This could take a while...
Successfully installed ruby-minisat-2.2.0
1 gem installed
Installing ri documentation for ruby-minisat-2.2.0...
Installing RDoc documentation for ruby-minisat-2.2.0...
$
まずは、ruby-minisatのWebサイトにある、例題を解かせてみる。
例題は、以下の通りの簡単なもの。
# solve (a or b) and (not a or b) and (a or not b)

require "minisat"
solver = MiniSat::Solver.new

a = solver.new_var
b = solver.new_var

solver << [a, b] << [-a, b] << [a, -b]
p solver.solve  #=> true (satisfiable)

p solver[a]  #=> true
p solver[b]  #=> true
冒頭のコメント行にもある通り、
(a or b)、(not a or b)、(a or not b)の全てを満たす、aとbの組み合わせは存在するか?
という問題。
ここでのaとかbは、TrueかFalseの2値を取る。
この問題では、aとbが両方ともTrueの時に、命題全体が成立する。

これをsample.rbとして保存して実行。
結果は、以下の通りとなった。
$ ruby sample.rb
true
true
true
命題論理を与えるときに、
solver << [a, b] << [-a, b] << [a, -b]
なんて書けちゃうところが、大変楽チン。
ruby-minisatを作って下さった方に感謝。

というわけで、とりあえず、最初の一歩を踏み出した。
後編では、もうちょっとパズルっぽい問題を解かせてみたいと思う。

2012年4月8日日曜日

万年カレンダーの製作

我が家ではカレンダーというものは購入したことがありません。
カレンダーを壁に貼ると、見た目にごちゃごちゃしてしまうし、何よりもお金がもったいないからです。いちおう、その月の分の予定だけは夫婦で共有する必要があるので、一月分だけパソコンで印刷して冷蔵庫に貼り、予定を書き込んで使っています。
(これが「ごちゃごちゃ」しているじゃないか、というツッコミは無しってことで。。。)

で、今までは必要になったら私がEXCELで作成するようにしていましたが、毎度のことで面倒になったので、このたび万年カレンダーを作ってみました。
おそらく、車輪の再発明となっているでしょうが、EXCELの各種関数の使い方を覚えるには、良い題材でした。
普段はEXCELマクロ(VBA)を使うことも多いのですが、今回はマクロを一切使用せず、EXCELのシート関数のみで構築してみました。

まずは、「設定」シート。
入力箇所は1箇所のみで、西暦の年を入力するだけです。
入力された西暦年に基づき、全ての休日を自動で求め、テーブルを作成します。
ここでは、ハッピーマンデーの算出、毎年の暦で変化する「春分の日」「秋分の日」の対応、祝日が日曜だった場合の「振替休日」、祝日と祝日に挟まれた「国民の休日」などなど、わりと良い頭の体操でした。

続いて「カレンダー」シート。
カレンダー表示は、日曜始まりと月曜始まりの2種類を用意しました。
こちらも、全て自動で生成されます。
前述の祝日テーブルをVLOOKUP関数で検索し、祝日であれば赤文字にする、などしてます。

以下のリンクよりダウンロード可能です。
検証がいい加減で、バグが残ってる可能性がありますので、各位の責任でご使用ください。

2011年9月18日日曜日

伊丹スカイパークで飛行機三昧

大阪国際空港(伊丹空港)の脇にある、「伊丹スカイパーク」に遊びに行ってきました。

伊丹スカイパークは、もともと空港周辺地域への航空機騒音軽減のために作られたため、ビルの3階建てくらいの高さの築堤が1km以上も続く、かなり大きな公園です。
位置的には、空港の管制塔から滑走路をはさんでちょうど真向かいにあたる位置にあり、飛行機の眺めは抜群です。

さらに、伊丹スカイパークの隣には、原田下水処理場があり、その屋上が緑化されており「スカイランドHARADA」として一般公開されています。こちらの方がさらに高い位置にあるため、空港全体を俯瞰するような形になり、またひと味違った風景が楽しめます。航空無線を聞きながら眺めると、気分は航空管制官です(^^;

どちらの公園も、とにかくすぐ近くを飛行機が離着陸するため、かなりの迫力です。飛行機が好きじゃない人でも、大興奮するんじゃないかと思います。
コンパクトデジカメで手持ち撮影したので、あまり良い写真は撮れてませんが、その中から厳選したのを以下に数枚ほど。

航空機の撮影は、望遠性能の良さがモノを言う世界です。会社ではもともと写真部に所属していたにもかかわらず、「重い」という理由でデジタル一眼を手放した私ですが、こういう時は一眼にデッカいレンズを着けて撮影したくなります。というか、周りで写真撮ってる人は、そういうカメラを持ってる人達ばかりでした。

園内の管理棟の中には、退役したレーダー表示機が展示されていました。

レーダー表示機には、子供向けのゲーム画面が表示されていました。(本物のレーダー表示ってどんな物なのかなぁ、と思いました。)

公園のすぐ脇には、本物のレーダーのアンテナがありました。アンテナ設備には何かと反応しちゃう私です。

そんなわけで、とても楽しい公園でした。飛行機好きには特におススメしたい場所です。

2011年9月11日日曜日

ネットワークフォトフレームの製作

[2012/8/25追記]本記事の内容は古くなっています。
最新の情報は mpod mother board をご覧ください。

概要
USBメモリのインタフェースを備えるディジタルフォトフレームを、フォトフレーム側への改造をせずにネットワーク対応させます。
フォトフレームの電源を投入しスイッチを押すと、Picasaウェブアルバムから写真を取って来て、自動で表示を始めます。


概念図
このような目的に使える、ネットワークとUSBの両方を備える便利なデバイス、と言えば、もちろんmbedでしょう。
ネットワークは、パルストランス付きのRJ45コネクタをmbedに繋ぐだけ。
USBメモリをUSBスイッチ(FSUSB30MUX:フェアチャイルドセミコンダクタ)を介してmbedとフォトフレームに接続し、mbedからの制御信号によりUSBメモリの接続先を「mbedのUSBホスト」と「フォトフレーム」とに切り替えて使用します。
最初は、mbed自身のファーム格納領域であるUSB mass strageを間借りして写真も保存しようと思いましたが、容量が約2MBと小さく、たくさんの写真を保存するには適していないため、このような構成にしています。


この構成では、フォトフレーム側からは本機はただのUSBメモリとして見えるため、フォトフレームには一切手を加えずに済むのが特徴です。USBスイッチがmbed側に倒れている時は、フォトフレームからUSBメモリが抜き取られたのと同じ状態です。


外観


使い方
まず、本機のUSBコネクタをパソコンにつなげます。
USBメモリの中身が見えるので、トップのフォルダに"netusbm.cfg"というテキストファイルを作成し、自分のPicasaウェブアルバムの特定のアルバムのURLを記述します。
http://picasaweb.google.com/********/********?authkey=***********#
本機にダウンロードさせたい写真は、ここで指定したアルバムに保存しておくようにします。
また、同じフォルダに"DCIM"という空のフォルダも作っておきます。
フォトフレームは、この"DCIM"フォルダの下にある画像を探すので、本機はダウンロードした画像をこのフォルダに保存します。


ここまで準備ができたら、EtherNetケーブルを本機に接続し、パソコンからUSBコネクタを取り外し、そのUSBケーブルをフォトフレームに繋げます。
フォトフレームの電源を入れると、本機の中のUSBメモリがマウントされ、画像を読み込もうとしますが、最初は画像が無いので何も表示されないか、エラー画面などが出ると思います。
ここで、本機の上面にあるスイッチを押すと、フォトフレームからUSBメモリがアンマウントされ、mbed側に接続されます。mbedはEtherNet経由で画像を収集し、完了したらUSBメモリを再びフォトフレーム側に接続します。フォトフレーム側はUSBメモリの接続を検知し、保存されている画像を表示し始めるはずです。


部品表
    項目メーカ型番価格
    (円)
    数量備考
    ディジタル
    フォトフレーム
    kodakEasyShare P72501
    会社の新年会ビンゴ景品
    USBメモリBuffaloRUF2PS4GBK20001
    作品全体のサイズを小さくするため、
    小型のUSBメモリを選択した
    コントローラNXPmbed NXP LPC176860001
    USBスイッチFairchildFSUSB30MUX1601
    Digi-Keyで購入したため
    2000円の送料が必要
    RJ45コネクタ3601
    USBコネクタBメス
    (receptacle)
    1601
    フォトフレーム接続用
    USBコネクタAメス
    (receptacle)
    1301
    USBメモリ接続用
    ケーステイシン電機TB-51B1201
    D40×W70×H25

    回路図

    ケース加工
    mbedとUSBコネクタ、RJ45コネクタ、USBメモリを入れるため、かなり窮屈です。
    もともとmbedについているUSBコネクタとリセットスイッチがケースと干渉するので、写真のように内部を彫刻刀で削りました。
    また、スイッチを外に出すための穴を開けています。

    回路実装
    mbed用のコネクタの間にUSBメモリを配置し、さらに基板を切り欠いてUSB-BコネクタとRJ45コネクタをホットメルトで固定しています。
    USBメモリ用のUSB-Aコネクタの配置が窮屈で、USBメモリが取り外し不可能になってしまいました。USBメモリは約2000円と高価ですが、仕方なく本機専用としました。


    ソフトウェア
    以下に示すのが、私が書いた部分です。
    これ以外に、以下のライブラリも利用しています。
    ・EtherNetIf
    ・FATFileSystem
    ・HTTPClient
    ・MSCUsbHost
    なお、動作を優先させたため、リファクタリングの余地が多分に残っていますので、その点ご了承ください。

    main.cpp
    #include "mbed.h"
    #include "MSCFileSystem.h"
    #include "EthernetNetIf.h"
    #include "HTTPClient.h"
    #include "picasaUrlParser.h"
    
    #define URL_BUF_SIZE (100)
    
    DigitalOut led(LED1);
    DigitalInOut usbsel(p8);
    DigitalIn startsw(p9);
    
    EthernetNetIf eth;
    HTTPClient http;
    
    HTTPResult result;
    bool completed = false;
    
    void
    request_callback(HTTPResult r) {
        result = r;
        completed = true;
    }
    
    int
    main(void) {
    
        printf("EtherIF Setting up...\n");
        EthernetErr ethErr = eth.setup();
        if (ethErr) {
            printf("Error %d in setup.\n", ethErr);
            return -1;
        }
        printf("EtherIF Setup OK\n\n");
    
        while (1) {
            usbsel.output();
            usbsel = 1;
            led = !led;
            completed = false;
    
            while (1) {
                if (startsw == 0) {
                    break;
                }
            }
            usbsel = 0;
            led = !led;
            usbsel.input();
    
            {
                char url[URL_BUF_SIZE];
                
                // Mount flash drive under the name "msc"
                MSCFileSystem msc = MSCFileSystem("msc");
    
                // Read the target URL from configuration file.
                FILE *fpcfg = fopen("/msc/netusbm.cfg", "r");
                fgets(url, URL_BUF_SIZE, fpcfg);
                fclose(fpcfg);
                url[strlen(url)-1] = '\0';
    
                // Create parser
                FILE *fplist = fopen("/msc/url_list.txt", "w");
                picasaUrlParser* parser = picasaUrlParser_create(fplist);
    
                HTTPStream stream;
    
                char BigBuf[512 + 1] = {0};
                stream.readNext((byte*)BigBuf, 512);
    
                printf("%s\n", url);
                HTTPResult r = http.get(url, &stream, request_callback);
    
                while (!completed) {
                    Net::poll(); //Polls the Networking stack
                    if (stream.readable()) {
                        BigBuf[stream.readLen()] = 0;
    
                        int i = 0;
                        while (0 != BigBuf[i]) {
                            picasaUrlParser_setChar(parser, BigBuf[i]);
                            i++;
                        }
                        stream.readNext((byte*)BigBuf, 512);
                    }
                }
                printf("--------------\n");
                if (result == HTTP_OK) {
                    printf("Read completely\n\n");
                } else {
                    printf("Error %d\n\n", result);
                }
    
                // delete parser
                picasaUrlParser_destroy(parser);
                fclose(fplist);
    
                fplist = fopen("/msc/url_list.txt", "r");
                if (fplist == NULL) {
                    error("Could not open file for read\n");
                }
                int j=0;
                char target[128];
                while (NULL != fgets(target, 128, fplist)) {
                    char* tmp = target;
                    while ('\n' != *tmp) {
                        tmp++;
                    }
                    *tmp = '\0';
    
                    printf("%s\n", target);
    
                    char filename[64];
                    sprintf(filename, "/msc/DCIM/img%03d.jpg", j);
                    printf("filename : %s\n", filename);
                    HTTPFile fpimg = HTTPFile (filename);
    
                    // Request a page and store it into a file.
                    HTTPResult r2 = http.get(target, &fpimg);
    
                    if (r2 == HTTP_OK) {
                        printf("Read completely\n\n");
                    } else {
                        printf("Error %d\n\n", r2);
                    }
    
                    // Close the file.
                    fpimg.clear();
                    j++;
                }
                fclose(fplist);
    
                // Work is done!
            }
        }
        return 0;
    }
    

    Picasaから送られるHTMLから、画像ファイルのURLだけを抜き出すため、ステートマシンによる簡単なパーサを作りました。
    URLの部分は、以下の様な特徴的な文字列が並びます。
    [{"url":"http://lh5.ggpht.com/(中略)/IMGP0486.JPG","height":1200,"width":1600,"type":"image/jpeg"}]
    
    こういった文字列を含むファイル全体を、1文字ずつパーサに流し込んで行き、あるところで
    [{"url":"
    
    を検出したらURLの読み取りを開始し、次に
    "
    
    を検出したらURLの終了だと判断します。
    厳密には検出のための条件をもっと増やした方が良いかもしれませんが、個人で使う分にはこれで十分です。当然のことながら、Googleが仕様を変更したら、パーサも設計し直す必要があります。
    今回はパーサを独自に作りましたが、もし汎用の正規表現ライブラリで良いのがあれば、それを使った方が良いでしょう。私はC言語で使える正規表現ライブラリをひとつ評価しましたが、使い勝手が自分の要求には合わなかったので、自作しました。

    picasaUrlParser.h
    #ifndef PICASAURLPARSER_HEADER_DEFINED
    #define PICASAURLPARSER_HEADER_DEFINED
    
    #include "FATFileSystem.h"
    
    #define MAX_URL_LEN (128)
    
    typedef enum {
        initalized,
        judge1,
        judge2,
        judge3,
        judge4,
        judge5,
        judge6,
        judge7,
        judge8,
        parsing
    }picasaUrlParserState;
    
    typedef struct {
        int     iPos;
        char    url[MAX_URL_LEN];
        picasaUrlParserState    state;
        FILE*   fpurl;
    }picasaUrlParser;
    
    picasaUrlParser*
    picasaUrlParser_create(FILE* fp);
    
    void
    picasaUrlParser_destroy(picasaUrlParser* pThis);
    
    void
    picasaUrlParser_setChar(picasaUrlParser* pThis, char target);
    
    #endif /* PICASAURLPARSER_HEADER_DEFINED */
    
    picasaUrlParser.cpp
    #include <stdio.h>
    #include <stdlib.h>
    #include "picasaUrlParser.h"
    
    picasaUrlParser*
    picasaUrlParser_create(FILE* fp) {
        picasaUrlParser* pThis;
    
        pThis = (picasaUrlParser*)malloc(sizeof(picasaUrlParser));
        if (NULL == pThis) {
            printf("Memory Allocation Error.\n");
            while (1) {
                //Do nothing
            }
        }
    
        pThis->iPos = 0;
        pThis->state = initalized;
        pThis->fpurl = fp;
    
        return pThis;
    }
    
    void
    picasaUrlParser_destroy(picasaUrlParser* pThis) {
        if (NULL == pThis) {
            while (1) {
                //Do nothing
            }
        }
    
        free(pThis);
        return;
    }
    
    void
    picasaUrlParser_setChar(picasaUrlParser* pThis, char target) {
        if (NULL == pThis) {
            while (1) {
                //Do nothing
            }
        }
        switch (pThis->state) {
            case initalized:
                if ('[' == target) {
                    pThis->state = judge1;
                }
                break;
    
            case judge1:
                if ('{' == target) {
                    pThis->state = judge2;
                } else {
                    pThis->state = initalized;
                }
                break;
    
            case judge2:
                if ('"' == target) {
                    pThis->state = judge3;
                } else {
                    pThis->state = initalized;
                }
                break;
    
            case judge3:
                if ('u' == target) {
                    pThis->state = judge4;
                } else {
                    pThis->state = initalized;
                }
                break;
    
            case judge4:
                if ('r' == target) {
                    pThis->state = judge5;
                } else {
                    pThis->state = initalized;
                }
                break;
    
            case judge5:
                if ('l' == target) {
                    pThis->state = judge6;
                } else {
                    pThis->state = initalized;
                }
                break;
    
            case judge6:
                if ('"' == target) {
                    pThis->state = judge7;
                } else {
                    pThis->state = initalized;
                }
                break;
    
            case judge7:
                if (':' == target) {
                    pThis->state = judge8;
                } else {
                    pThis->state = initalized;
                }
                break;
    
            case judge8:
                if ('"' == target) {
                    pThis->state = parsing;
                } else {
                    pThis->state = initalized;
                }
                break;
    
            case parsing:
                if ('"' != target) {
                    pThis->url[pThis->iPos] = target;
                    (pThis->iPos)++;
                } else {
                    pThis->url[pThis->iPos] = '\0';
                    fprintf(pThis->fpurl, "%s\n", pThis->url);
                    pThis->iPos=0;
                    pThis->state = initalized;
                }
                break;
    
            default:
                break;
        }
    
        return;
    }
    

    まとめ
    mbedを使ってディジタルフォトフレームに手を加えずにネットワーク対応しました。
    このような用途にmbedはうってつけです。HTTP、USBホストなどの有用なライブラリが揃っているからです。
    今回、これらのライブラリが無ければ、ここまで辿り着けなかったことでしょう。
    ライブラリを提供してくださった皆様には、大変感謝しております。ありがとうございました。

    製作してから、この文章を書くまで、半年以上が経過してしまいました。この点は反省。
    次回からは、作ったらすぐにドキュメントにまとめるようにしたいと思います。

    お約束
    このドキュメントに書かれている内容について、正確になるよう筆者は努力しますが、保証はできません。
    もし、間違いなどを発見されましたら、ご指摘頂ければ幸いです。

    このドキュメントに起因するいかなる損害についても、筆者は一切の責任を負いません。
    ご利用になる場合は、各位にて十分な検証を行うようにお願いします。

    また、本ドキュメントに関する著作権は筆者に帰属するものとします。
    ただし、ドキュメントの内容は自由に複製したり、改変して使用して頂いても構いません。

    2011年2月27日日曜日

    FeliCa リーダー・ライター RC-S620S で IDm 読み取り実験

    スイッチサイエンスで購入したFeliCa リーダー・ライター RC-S620Sを使って、Felicaの製造番号"IDm"を読み取ってみる実験。

    RC-S620SはUARTで読み書きができるので、マイコンからもFelicaに簡単にアクセスできます。
    今回は、RC-S620Sの挙動を確かめるため、パソコンのシリアルポートを使って、IDmを読み取るだけの簡単な実験をしてみます。

    まず、写真の様に「RC-S620S」と「USBシリアル変換モジュール」をブレッドボード上で接続します。
    写真左に見えるのがRC-S620S、右上がフラットケーブル変換基板(自作)、右中がUSBシリアル変換モジュール、右下がUSBコネクタTypeB。

    RC-S620Sへは、以下の通り結線します。
    USBシリアル変換      RC-S620S
    ----------------------------
             VDD          ⇔     VDD
             TXD          ⇔     RXD
             RXD          ⇔     TXD
             GND          ⇔     GND
    ----------------------------
    ソフトウェアは、ソニーさんが提供している「Arduino向けRC-S620/S制御ライブラリ」をもとに、C#に移植。
    シリアルポートを選択してFelicaカードをかざすとIDmを表示するようなGUIをかぶせて、ひとまず実験レベルのものが完成。
    読み取ると音が鳴ってIDmとPMmの表示が書き換わります。

    楽しくなって、家中のFelicaカードをかき集めてきて実験している様子。
    まだ2枚重ねがうまくいったりいかなかったり。
    もう少し規格を読んでみる必要があるかも。

    誰かの役に立つかどうか分からないが、C#のプロジェクト一式を公開。
    最下部のリンクよりダウンロードできます。
    実験レベルのものなので、エラーチェックとかを省いてたり、逆に余計なコードが残っています。
    C#の書き方のスジが悪い、Felicaリーダー・ライターへのコマンドの出し方が違う、とかのご指摘はやんわりとお願いしますm(_ _)m

    なお、本ソフトウェアは、RC-S620/S制御ライブラリ 使用許諾契約の第1条1項(2)に基づき公開していますが、ライブラリ部は私が移植したためソニーさんには一切の責任はありません。
    お約束ですが、本ソフトウェアは無保証で提供します。利用者各位の責任においてご利用下さい。

    最後に、ライブラリを提供してくださったソニーさんに感謝申し上げます。