Ruby + Sequel を使って、ADOやODBCでMS Access(accdb) へ接続する

以前、Ruby + DBIを使ってMS Accessへと接続してみました。
Ruby + Sinatra + DBI + ODBC で、MS Access(accdb)へ接続する - メモ的な思考的な

しかし、毎回生SQL文を書いた上でShift_JISに変換する必要があり少々手間に感じました。そのため、PadrinoのページにまとめられているORMを対象に、そこらへんをうまくやってくれるものがないかを調べてみました。
Guides Generators - Padrino Ruby Web Framework

SequelにはADOやODBCの記載がある*1一方、ActiveRecordやDataMapperでは使えそうになかったり*2、記載がありませんでした*3

そこで、SequelでADOやODBCを使った接続を試してみることにしました。

 

環境

 

接続

ADOやODBCを使うときのパラメータなどはSequelのドキュメントに記載されています。
sequel/doc/opening_databases.rdoc at master - jeremyevans/sequel - GitHub

それを元に、

の3つ方法で接続するコードを書いてみて気になったところを表にまとめてみました。

項目 ADO ODBC + DSN ODBC + DSNレス
gemruby-odbc - 必要 必要
DSNの設定 - 必要 -
ネットワーク上のホストの指定 IPアドレスやホスト名を使う ネットワークドライブを割り当てる IPアドレスやホスト名を使う
プリベアードステートメントで使う:$n*4文字コード変換 - 必要(Shift_JIS) 必要(Shift_JIS)
Accessの列名のシンボル表記(例:Hoge列の場合) そのまま(row[:Hoge]) 小文字化(row[:hoge]) 小文字化(row[:hoge]

 
ODBC文字コードの変換や列名の指定で手間がかかりそうなので、ADOが使える環境であればADOを使いたいと感じました。

 

ソースコード

DB接続時の設定は以下にまとめました。
Sequel: Connecting to Microsoft Access(*.accdb)

また、Sinatraとともに使うときのコードはGitHubに置いておきました。 thinkAmi/SinatraToMSAccess-Sample - GitHub

 

参考

Google Chrome + ChromeAppsのchrome.serial API で取得したデータを、WebSocketを使ってSinatraへ飛ばしてみる

この前はシリアルポートから受信したデータをChromeAppsの画面に表示しました。

それだけではあまり使い道がないので、その受信したデータをSinatraへ飛ばしてみようとChromeAPIを探してみたところ、chrome.sockets.tcpchrome.sockets.udpAPIがありました。

 
ただ、それぞれのsend()メソッドを調べてみたところ、送信するデータ型がArrayBufferSinatraでは扱いが面倒そうだったので、今回はChromeAPIを使うのは見送ることにしました。

他の手段を試してみたところ、ChromeAppsでWebSocketが使えることが分かりました。そこで、気になっていたWebSocketの勉強を兼ねて、ChromeAppsからSinatraへWebSocketを使ってデータを送信してみました。

 

環境

 

全体の構成

今回はWebSocket送信の成否は判定していないため、それぞれ片方向通信となっています。

ChromeApp ---(WebSocket)---> Sinatra (port 4567) ---(WebSocket)---> SinatraのView (index.erb)

 

ChromeApps側

ChromeAppsでWebSocketが使えるかどうかが問題でしたが、特に問題なく動作しました。
そのため、前回のコードを少し変更するだけで済みました。

シリアルポートからの送信は一回行うだけなので、「ボタンを押したら接続」「データ送信」「送信後に接続を閉じる」という簡単な実装になりました。

const WEBSOCKET_HOST = "ws://192.168.0.1:4567";
var clickedWebSocketListener = function() {
  var ws = new WebSocket(WEBSOCKET_HOST);

  ws.onopen = function() {
    console.log('opened');
    ws.send(arrayReceived.join(''));

    //  送ったら閉じとく
    ws.close();

    //  変数の初期化とlistのクリア
    stringReceived = '';
    arrayReceived = [];
    var upload = document.getElementById('upload');
    while (upload.firstChild) {
      upload.removeChild(upload.firstChild);
    }
  };

  ws.onclose = function() {
    console.log('closed');
  };
};
document.getElementById('connectWebSocket').addEventListener("click", clickedWebSocketListener, false);

 

Sinatra

SinatraでWebSocketを扱うようなものがないかを探してみたところ、stackoverflowに以下のような回答がありました。
ruby - Any success with Sinatra working together with EventMachine WebSockets? - Stack Overflow

今回は、sinatra-websocket gemを使うことにしました。
simulacre/sinatra-websocket - GitHub

Sinatra部分は、ほぼ sinatra-websocket のサンプルと同じです。
bundle exec ruby app.rb -e production にて起動します。

get '/' do
  logger.info('accessed')

  unless request.websocket?
    erb :index
  else
    logger.info('received')
    request.websocket do |ws|
      ws.onopen do
        logger.info('opened')
        settings.sockets << ws
      end

      ws.onmessage do |msg|
        logger.info(msg)
        EM.next_tick { settings.sockets.each{|s| s.send(msg) } }

      end
      ws.onclose do
        logger.info('closed')
      end
    end
  end
end

 
なお、SinatraのViewに記載したJavaScriptはChromeApps側とほぼ同じになっているため、記述は省略します。

 

画面イメージとコンソール出力

ChromeApps - シリアルポートからのデータ受信前

f:id:thinkAmi:20140605045946p:plain

 

ChromeApps - シリアルポートからの受信後

f:id:thinkAmi:20140605050117p:plain

 

ChromeApps - シリアルポート受信~WebSocket送信までのコンソールログ

f:id:thinkAmi:20140605050254p:plain

 

Sinatra - WebSocket受信前

f:id:thinkAmi:20140605050147p:plain

 

Sinatra - WebSocket受信後

f:id:thinkAmi:20140605050205p:plain

 

ソースコード

GitHubに上げておきました。
ChromeApps-sample/serial_websocket at master · thinkAmi/ChromeApps-sample

 

参考

WebSocket

上記のstackoverflowの回答以外のgemなどを探してみたところ、faye-websocket gemがありました。こちらの方がStar数が多かったので、メジャーなのかもしれません。
faye-websocket-ruby

試している日本語のBlogなどは以下がありました。
Sinatra + Faye::WebSocketでlambda do |env|したくない - 技術ネタ書ける人になりたい

WebSocketに関するその他の参考記事

 

JavaScript

Windows7 + Nexus7 2012 で Android Wear Developer Preview の環境を構築してみた

Android Wear Developer Preview が公開されていたので、手元のWindows7 + Nexus7 2012 で環境構築をしてみた時のメモを残します。

Web上では、Nexus7に対応していなさそうな情報もありましたが、手元の環境では問題なく構築できました。

また、構築にあたっては以下の記事がとても参考になりました。
Android Wear Developer Preview 開発環境構築 - Android(アンドロイド)情報-ブリリアントサービス

 

環境

以前Ruboto用の開発環境を構築していましたが*1、ディスク交換後は特に構築していなかったので*2、イチからの構築となりました。

 

Android Wear の Developer Preview に Sign Up

以下のサイトより登録しました。一度休日に登録したものの3営業日くらい返事がなかったので、再度登録したらメールが届きました。
Get Started with the Developer Preview | Android Developers

受信したメールのうち、Downloadと書かれたものはPCで、Opt-in to become a testerはNexus7で、それぞれダウンロードやインストールをしました。

 

Android Studio のセットアップ

Android SDKなどをインストールしていなかったので、それらが同梱されているAndroid Studioをインストールすることにしました。

Android StudioWindows7 x64で使うにはJDKの64bit版が必要なので、以下のページより現時点で最新のJDKをダウンロードします (現時点のものはjdk-8u5-windows-x64.exe)。
Java SE - Downloads | Oracle Technology Network | Oracle

任意の場所にインストール後、JDKにパスを通しておきます。自分の場合は C:\jdk8x64 にインストールしたため、JAVA_HOMEにもC:\jdk8x64を設定しました。

次に、以下よりAndroid Studioをダウンロード・インストールします。
Getting Started with Android Studio | Android Developers

インストール先のパスは特に変更しませんでしたが、自分の場合は以下にインストールされました(AppDataは隠しフォルダ)。

%USERPROFILE%\AppData\Local\Android\android-studio\

 
インストールが終わったらAndroid Studioを起動し、以下に従ってAndroid SDKJDKのパスを設定します。
Android StudioにおけるAndroidSDKやJDKのパス設定 - Qiita

 
その後、Android Studioに含まれるAndroid SDK Managerを使って、前述の参考ブログに記載されている必要なものをダウンロードします。

Android SDK Tools revision 22.6以降
Android Wear ARM EABI v7a System Image
Android Support Libraryの最新版
Android Support Repository

なお、自分の環境ではAndroid Studioを初めて起動した時にはAndroid Wear ARM EABI v7a System Imageがなく、それ以外をダウンロード後に表示されました。

また、後述のとおり、Nexus7の接続時に必要になることもあるため、Google USB Driverもダウンロードしました。

あとはAndroid Wear のエミュレータを起動しておきます。

 

Nexus7 2012 側のセットアップ

Nexus7をPCにUSB接続し、adb devices を実行します。なお、自分の環境では adb.exe は以下のパスにありました。

%USERPROFILE%\AppData\Local\Android\android-studio\sdk\platform-tools

 
実行結果はエミュレータのみ認識されており、Nexus7は認識されていない状態となっていました。また、デバイスマネージャーで確認しても、「その他のデバイス - Nexus7 (?マーク付)」と表示されていました。

そのため、stackoverflowを参考にして、以下の作業を行います。
android - Nexus 7 not visible over USB via "adb devices" from Windows 7 x64 - Stack Overflow

  • USBドライバーのインストール (自分の場合は、 %USERPROFILE%\AppData\Local\Android\android-studio\sdk\extras\google\usb_driver にありました)
  • Nexus7のUSB接続を `カメラ(PTP)' へと変更

再度Nexus7をUSB接続して adb devices を実行すると、Nexus7も認識されていました。

 

アプリの実行

認識されたところで、以下を実行すると、Nexus7で「Connected」と表示されます。

adb -d forward tcp:5601 tcp:5601

試しにNexus7宛にメールを送ってみたところエミュレータ側で反応があったため、連動して動作していることが確認できました。

 

Android Wear ハッカソン情報

調べてみたところ、全国各地でハッカソンが開催されるようです。
android wearで検索 - AZusaar!!

例えば、6/21(土)には塩尻市のえんぱーくにて、GDG信州による「Android Wear ハッカソン」が開催されます。
Android Wear Hackathon in Shinshu on Zusaar

Androidに詳しい@さんや@さんのコードラボもありますので、いろいろとお話を聞いたり質問したりできそうです。

Google Chrome + ChromeAppsのchrome.serial API を使って、シリアルCOMポートからデータを受信する

前回Rubyを使ってシリアルCOMポートからデータを受信してみましたが、他にも何か方法がないかを探してみたところ、ChromeAppsに chrome.serial API がありました。
chrome.serial - Google Chrome

そこで、chrome.serial API を使って、シリアルCOMポートからデータを取ってみました。

なお、ChromeAppsの概要は以下のスライドが参考になりました。
Chrome Apps 概要

 

環境

 

用意するファイル

以下のBlogのファイルを参考に、いくつかファイルを用意しました。今回使用するファイルの名前もこのBlogに合わせてあります。
JavaScript + Chrome Packaged Apps API を使用したシリアル通信Webアプリ - Tech-Sketch

  • manifest.json
  • background.js
  • serial.js
  • window.html
  • png画像

なお、png画像は以下のサイトで生成したものを使いました。ありがとうございます。
Placehold.jp|ダミー画像生成 モック用画像作成

manifest.json

Chrome拡張と同じく、manifest.jsonを用意します。
個人的に書き方で悩んだところとしては、

  • manifest_versionに 2 を指定
  • backgroundに、バックグラウンドで動作する background.js を指定 (background.js の中で、アプリで使うhtmlを指定している)
  • permissionsに、serial を指定

といったところです。

{
  "name": "chrome serial example",
  "description": "chrome serial example",
  "version": "1",
  "manifest_version": 2,
  "app": {
    "background": {
      "scripts": ["background.js"]
    }
  },
  "icons": { "128": "128x128.png",
              "16": "16x16.png" },
  "permissions": [
     "serial"
  ]
}

 

background.js

chrome.app.window.create() メソッドで、表示する画面のhtmlを指定します。また、必要に応じて画面のサイズや表示位置も指定します。

chrome.app.runtime.onLaunched.addListener(function(){
  chrome.app.window.create('window.html', {
    'bounds': {
      'width': 250,
      'height': 200, 
      'top': 0,
      'left': 100
    }
  });
});

 

window.html

表示する画面と処理を実行するJavaScriptを指定します。今回は、

  • シリアルCOMポートを選択する、select menu (id="port")
  • シリアルCOMポートに接続・データ受信をするためのボタン (id ="connect")
  • 全シリアルCOMポートの表示領域 (id="list")
  • 実際に接続・データ受信処理を実行するJavaScript (serial.js)

を記載しています。

<select id="port"></select>

<button id="connect">connect</button>

<div>
  <ul id="list">
  </ul>
</div>

<script src="serial.js"></script>

 

serial.js

メインの処理を記述しています。

なお、window.alert() を使おうとすると「window.alert() is not available in packaged apps.」というエラーが発生したため、実行結果などは console.log でコンソールへと出力しています。
Using the Console - Google Chrome

 

COMポートの列挙と追加

ロード時に現在の端末にあるCOMポートを列挙し、画面とselect menu に追加します。今のところ、手元の環境ではport.displayNameが設定されませんでした。

また、以下のようなBlog記事があったため、loadイベントの設定ではwindow.addEventListener()を使いました。
Loox Uと初音ミクで行こう!: Google ChromeでDOMContentLoadedが発生しないケースがある

var loadedListener = function() {
  console.log('loaded');

  //  デバイスをリスト化して、画面に表示する
  chrome.serial.getDevices(function(devices) {

    var list = document.getElementById('list');
    var selection = document.getElementById('port');

    devices.forEach(function(port){

      //  画面に表示
      var li = document.createElement('li');
      li.textContent = port.path;
      list.appendChild(li);


      //  select menu に追加
      var option = document.createElement('option');
      option.value = port.path;
      //  今のところ、手元の環境ではport.displayNameが設定されていない
      option.text = port.displayName ? port.displayName : port.path;
      selection.appendChild(option);
    });
  });
};
window.addEventListener('load', loadedListener, false);

 

COMポートへの接続

ボタンを押した時にCOMポートへ接続するようにするコードです。

なお、onReceiveイベントで使用するため、接続時の connectionId はこのイベントで取得したものを保持しておきます。

また、receiveTimeout を設定しておくことで onReceiveError イベントが発生することを利用し、データ受信の完了を知らせるようにしています。

var onConnectCallback = function(connectionInfo){
  //  onReceiveイベントでconnectionIdの一致を確認するので、保持しておく
  connectionId = connectionInfo.connectionId;
}


var clickedListener = function() {
  console.log('clicked');

  var e = document.getElementById('port');
  var port = e.options[e.selectedIndex].value;

  chrome.serial.connect(port, {bitrate: 9600, receiveTimeout: 5000}, onConnectCallback);
}
document.getElementById('connect').addEventListener("click", clickedListener, false);

 

シリアルCOMポートからデータを受信

公式のサンプルを参考に、データを受信する部分を記載します。
Serial Devices - Google Chrome

なお、公式のサンプルには expectedConnectionIdconvertArrayBufferToString() の定義は見つけられませんでした。

そこで、前者は接続時に保持しておいたconnectionInfo.connectionId、後者は公式のサンプルにあった関数のことだろうと判断し、コードを書きました。
chrome-app-samples/serial/ledtoggle/main.js | GoogleChrome/chrome-app-samples

/* Interprets an ArrayBuffer as UTF-8 encoded string data. */
function convertArrayBufferToString(buf){
  var bufView = new Uint8Array(buf);
  var encodedString = String.fromCharCode.apply(null, bufView);
  return decodeURIComponent(escape(encodedString));
}

var onReceiveCallback = function(info) {
  console.log('received');
  if (info.connectionId == connectionId && info.data) {

    //  こいつがうまく動かない:メソッドを定義してあげるとよい
    var str = convertArrayBufferToString(info.data);

    //  改行コード来たら、一つのデータの終端
    if (str.charAt(str.length-1) === '\n') {
      arrayReceived.push(stringReceived)
      stringReceived = '';
    } else {
      stringReceived += str;
    }
  }
};
chrome.serial.onReceive.addListener(onReceiveCallback);

 

受信したデータのコンソールへの表示

接続時に receiveTimeout を設定しているため、全件受信した後にしばらく待つと onReceiveError イベントが発生します。そのタイミングで、受信したデータの配列をコンソールへと表示しています。

また、データ受信が完了した時は接続を切るようにします。

var onDisconnectCallback = function(result) {
  if (result) { console.log('disconnected'); }
  else { console.log('error'); }
}

var onReceiveErrorCallback = function(info) {
  console.log('end');
  //  receiveTimeoutで設定した時間が経過すると、このイベントが発生
  //  ->データの受信は全て完了したとみなし、受信データを一覧表示する
  console.log(arrayReceived.join(','));

  var disconnect = chrome.serial.disconnect(connectionId, onDisconnectCallback)
}
chrome.serial.onReceiveError.addListener(onReceiveErrorCallback);

 

画面イメージとコンソール出力

画面の枠を作っていないので分かりにくいですが、画面イメージです。

f:id:thinkAmi:20140530054409p:plain

コンソールへの表示はこちら。

f:id:thinkAmi:20140530054433p:plain

 

ソースコード全体

GitHubに上げておきました。
ChromeApps-sample/serial at master · thinkAmi/ChromeApps-sample

 

参考

API関連
JavaScript関連

Ruby + serialport + Sinatra で、シリアル(COM)ポートから受信したデータを表示する

以前、Windows Form を使ってシリアル(COM)ポートでの通信を試してみました。
WindowsFormで、シリアルCOMポートから受信したデータを処理する

.Net以外でのシリアルポートで通信する方法を調べてみたところ、 Rubyserialport gemがあったたため試してみました。

 

環境

 

アプリの流れ

  1. localhost:4567 にアクセス
  2. シリアルポートを選択して、受信ボタンを押す
  3. Z-1170によるアップロード
  4. アップロードしたデータを画面に表示

 

内蔵シリアルポート一覧の取得

以下を参考に、win32oleを使って取得しました。
利用可能なシリアルポート一覧を取得する方法(Windows) | TipsZone

なお、今回は内蔵のシリアルポートのみ対象にしたため、 Win32_SerialPort のみ取得します。 また、取得したシリアルポート名称はそのまま使うとSinatraで表示する際に文字化けをするため、UTF-8に変換します。

def list_com_port
  locator = WIN32OLE.new("WbemScripting.SWbemLocator")
  services = locator.ConnectServer()

  result = {}
  services.ExecQuery("SELECT * FROM Win32_SerialPort").each do |item|
    # 取得したitem.Nameはそのままでは文字化けするため、UTF-8にする
    result[item.DeviceID] = to_utf8(item.Name)
  end

  result
end


def to_utf8(str)
  str ? NKF.nkf('-w', str) : ""
end

 

シリアルポートからの受信

以下の公式ドキュメントやBlogを参考にしました。

 
接続については公式ドキュメントに

This behaves like SerialPort#new, except that you can pass a block to which the new serial port object will be passed. In this case the connection is automaticaly closed when the block has finished.

と記載されていたため、今回は newメソッドではなく、openメソッドを使いました。
This behaves like SerialPort#new — Documentation for serialport (1.3.0)

また、操作待ちのための sleep 5 を入れています。
ほかに、例外 EOFError を捉えることで受信完了とみなすために read_timeout の値を設定しました。デフォルトの 0 のままでは例外は発生せず、ずっと待っているような動きでした。

def recieve(port_name)
  result = []

  # close()のことを考えないよう、open()を使うようにしてみた
  SerialPort.open(port_name,
                  baud: 9600,
                  data_bits: 8,
                  stop_bits: 1,
                  parrity: SerialPort::NONE) do |sp|
    
    # 操作する時間も考えて、とりあえず5秒待つ
    sleep 5

    # 指定しなかったり、"0"だと常に待ち続けてしまうので、
    # とりあえず"5000"ミリ秒待っても来ない場合はタイムアウトにしとく
    sp.read_timeout = 5000

    loop do
      begin
        # 読み込んだデータには末尾に改行と空白が入っているので、それらを落とす
        r = sp.readline.chomp.strip
        p r
        result << r

      # readline()は最後まで来るとEOFを返すので、それで通信終了とみなす
      rescue EOFError
        break
      end
    end
  end

  result
end

 

スクリーンショット

Z-1170に「1,2,3」の3つのデータを受信・表示しています。

実行前

f:id:thinkAmi:20140528055740p:plain

実行後

f:id:thinkAmi:20140528055754p:plain

 

ソースコード

Gistに上げておきました。
Ruby + serialport + Sinatra で、シリアルCOMポートから受信したデータを表示するサンプル

なお、1ファイルでSinatraを書いたことがなかったので、それを試しています。

 

その他参考

Ruby + ActiveLdapでActiveDirectoryにいるユーザーの所属グループを取得する

Ruby + ActiveLdapでActiveDirectoryにLDAP接続し、ユーザーの所属グループを取得することがあったので、その時のメモを残します。  

環境

また、ActiveLdapの書き方については公式チュートリアルがとても参考になりました。
TutorialJa - ruby-activeldap
 

ActiveDirectoryの構成

ドメインの直下にユーザーhogehogeおよびOUhogeを置いて、OUhogeの中にユーザーfuga_logonを入れました*1

example.local
| |
| `--hoge (OU)
|     `--fuga_logon (User)
`--fugafuga (User)

 
ユーザー情報については以下の通りです。

ログオンアカウント名 フルネーム(CN) 所属するグループ
fuga_logon fuga_cn hogehogeとpiyopiyo
fugafuga ふがふが hogehoge

 
また、以下を参考にLDAPインタフェースへのアクセスをログに残すようにしました。

 

Gemfile

activeldapだけでは動作しないため、LDAPクライアントとしてnet-ldapを使いました。

source 'https://rubygems.org'

gem 'activeldap'
gem 'net-ldap'

 
 

ActiveDirectoryへの接続設定

デフォルトではActiveDirectoryに対する匿名接続は許可されていないため、ActiveDirectoryのLDAPにBindするアカウント名をfugafugaとして接続するようにします。
なお、これ以降の例でも、ActiveLdap::Base.setup_connectionは同じ内容です。

require 'active_ldap'


HOST_IP_V4 = '192.168.0.1'
BIND_USER_CN = 'ふがふが'
BIND_USER_OU = ''
BIND_USER_PASSWORD = 'fugafuga'


def create_ou_string
  BIND_USER_OU.empty? ? '' : "ou=#{BIND_USER_OU},"
end


# ENV['USERDNSDOMAIN']には、example.localという形で入っている
domain = ENV['USERDNSDOMAIN'].split('.')
base = "dc=#{domain[0]},dc=#{domain[1]}"
bind_user = "cn=#{BIND_USER_CN},#{create_ou_string}#{base}"

# 今回の範囲内では、Bindするユーザーは「Domain users」グループでも構わない
ActiveLdap::Base.setup_connection host: HOST_IP_V4,
                                  port: 636,
                                  method: 'ssl',
                                  base: base,
                                  bind_dn: bind_user,
                                  password: BIND_USER_PASSWORD

 
ENV['USERDNSDOMAIN']でログインしている端末のドメインが取得できるので、それを元にbaseで使う文字列を作成します。なお、Windowsで設定されているENVの一覧は、以下のコードで調べました。

ENV.each { |env| p env}

 
ldapsを利用するため、ポートを636、methodを ssl として設定します。
Sinatra + ActiveLDAPで簡易LDAP管理インターフェースを作った - ~nabeken/diary/

 
bind_dnに指定する「接続時にBindするユーザー情報」は、ドメインコントローラーなどでldp.exeを利用して確認します。
第3回 LDAPを使ってActive Directoryを制御しよう[その1:ldpとcsvde] - 知られざるActive Directory技術の「舞台裏」

   

ldap_mapping

ActiveLdap::Baseを継承したクラスにて、 ldap_mappingメソッドを使ってマッピングをします。

class User < ActiveLdap::Base
  ldap_mapping dn_attribute: "cn", prefix: ''
end

 
dn_attributeとprefixの関係については、Rubyist Magazine27号の記事中の図が参考になりました。
ActiveLdap を使ってみよう(前編) クラス定義と ldap_mapping - Rubyist Magazine27号

また、prefixが不要な場合であっても、prefixパラメータには空文字を渡します。 Re: ActiveLdap questions - Forum: Ruby

 
 

ActiveDirectoryからユーザー情報を抽出

example.localドメイン内のユーザー情報を抽出

find()メソッドの引数として、ActiveDirectoryユーザーのフルネーム(CN)を指定します*2

class User < ActiveLdap::Base
  ldap_mapping dn_attribute: "cn", prefix: ''
end

p User.find('fuga').dn
p User.find('ふがふが').dn

 
とすると、以下のような結果が得られます。

#<ActiveLdap::DistinguishedName:0x27b28c0 @rdns=[{"cn"=>"fuga"}, {"OU"=>"hoge"}, {"dc"=>"EXAMPLE"}, {"dc"=>"local"}]>
 
#<ActiveLdap::DistinguishedName:0x27730b0 @rdns=[{"cn"=>"\u3075\u304C\u3075\u304C"}, {"dc"=>"EXAMPLE"}, {"dc"=>"local"}]>

 
なお、ユーザーを作成時にフルネーム欄へ指定した値がCNと表示名(displayName)として設定されるため、何もしなければCNと表示名は同じ値になっているかと思います。このあたりの詳細は以下のサイトに記載がありました。

 

特定のOU以下のユーザー情報を抽出 (例:OUhoge以下)
class OuUser  < ActiveLdap::Base
  ldap_mapping dn_attribute: "cn", prefix: "ou=hoge"
end

p OuUser.find('fuga').dn
p OuUser.find('ふがふが').dn

とすると、

#<ActiveLdap::DistinguishedName:0x43342b0 @rdns=[{"cn"=>"fuga"}, {"ou"=>"hoge"}, {"dc"=>"EXAMPLE"}, {"dc"=>"local"}]>
 
path/to/vendor/bundle/ruby/2.0.0/gems/activeldap-4.0.3/lib/active_ldap/operations.rb:347:in `find_one': Couldn't find OuUser: DN: 縺オ縺後・縺・ filter: ["cn", "\u3075\u304C\u3075\u304C"] (ActiveLdap::EntryNotFound)

のように、OUhoge以下に存在しないユーザーについては例外が発生します。

 

ActiveDirectoryユーザーのユーザーログオン名で検索する場合

filterオプションにsAMAccountNameを使用した条件を渡します。 なお、同一ドメイン内ではユーザーログオン名は重複しないと考え、:firstを指定しています。

class LogonUser < ActiveLdap::Base
  ldap_mapping dn_attribute: "cn", prefix: ''
end
logon_user = 'fuga'
p LogonUser.find(:first, filter: "(sAMAccountName=#{logon_user})").dn

 
とすると、

#<ActiveLdap::DistinguishedName:0x2761728 @rdns=[{"cn"=>"fuga"}, {"OU"=>"hoge"}, {"dc"=>"EXAMPLE"}, {"dc"=>"local"}]>

と、結果が返ってきます。

sAMAccountName以外にfilterオプションとして使えそうなものは、ldp.exeを使ったり、以下の記事の「オブジェクトを一括登録する場合のCSVファイルの属性」を指定してみたりします。
第8回 Active Directoryの導入後の作業 (2/3) - @IT 管理者のためのActive Directory入門

また、ActiveLdapでのfilterオプションの書き方は、以下が参考になりました。
ActiveLdap::Base#find の :filter オプション - tashenの日記

 

ユーザーの所属するグループを取得する

ユーザーの所属するグループは、find()メソッドの戻り値のmemberOf以下に入っていますので、次のコードで確認できます。

class User < ActiveLdap::Base
  ldap_mapping dn_attribute: "cn", prefix: ''
end

user = User.find('fuga')
p user.memberOf
p user.memberOf[0].rdns[0]['CN']

 
結果は、

[#<ActiveLdap::DistinguishedName:0x4428e28 @rdns=[{"CN"=>"hogehoge"}, {"OU"=>"hoge"}, {"DC"=>"EXAMPLE"}, {"DC"=>"local"}]>, #<ActiveLdap::DistinguishedName:0x4428690 @rdns=[{"CN"=>"piyo"}, {"OU"=>"hoge"}, {"DC"=>"EXAMPLE"}, {"DC"=>"local"}]>]
 
"hogehoge"

となります。

そのため、「ユーザーがあるグループに所属しているかどうか」は次のコードで調べられます*3

class LdapUser < ActiveLdap::Base
  # OU'hoge'内に制限するなら、prefixに 'hoge' を渡す
  ldap_mapping dn_attribute: 'cn', prefix: ''

  def member?(group)
    # 見つからない場合、Array#indexはnilを返すのを利用
    result = self.memberOf.index do |m|
               m.rdns.index { |r| r.has_value?(group) }
             end

    !result.nil?
  end
end


# ドメイン内ではユーザーログオン名は一意になるため、 :firstを指定しておく
# CNで探す場合
user1 = LdapUser.find(:first, 'fuga_cn')

p user1.memberOf

if user1.member?('hogehoge')
  puts 'CNで検索し、所属を確認しました'
else
  puts 'CNで検索し、所属を確認できませんでした'
end


# ログオン名で探す場合
user2 = LdapUser.find(:first, filter: '(sAMAccountName=fuga_logon)')

p user2.memberOf

if user2.member?('hogehoge')
  puts 'ユーザーログオン名で検索し、所属を確認しました'
else
  puts 'ユーザーログオン名で検索し、所属を確認できませんでした'
end

 
結果は、

[#<ActiveLdap::DistinguishedName:0x4351110 @rdns=[{"CN"=>"piyopiyo"}, {"OU"=>"hoge"}, {"DC"=>"EXAMPLE"}, {"DC"=>"local"}]>, #<ActiveLdap::DistinguishedName:0x4350978 @rdns=[{"CN"=>"hogehoge"}, {"OU"=>"hoge"}, {"DC"=>"EXAMPLE"}, {"DC"=>"local"}]>]
 
CNで検索し、所属を確認しました
 
[#<ActiveLdap::DistinguishedName:0x43783b0 @rdns=[{"CN"=>"piyopiyo"}, {"OU"=>"hoge"}, {"DC"=>"EXAMPLE"}, {"DC"=>"local"}]>, #<ActiveLdap::DistinguishedName:0x4383bd0 @rdns=[{"CN"=>"hogehoge"}, {"OU"=>"hoge"}, {"DC"=>"EXAMPLE"}, {"DC"=>"local"}]>]
 
ユーザーログオン名で検索し、所属を確認しました

となり、所属を確認することができました。
 
 

ソースコード

所属を確認する部分とGemfileはGistに上げました。
ActriveLdapのサンプルコード

 

参考資料

ActiveLdap関連

 

ActiveDirectory / LDAP関連

*1:直下にユーザーを置くのはあまりよい構成ではない気がします

*2:最初、引数はユーザーログオン名だと思っていて軽くハマりました

*3:より良い書き方がありそうですが...

Ruby + Sinatra + SNMPでPX-105のインク残量を取得・表示する

手元の環境ではプリンターのセットアップをラクにするために、WindowsServerのActiveDirectoryでGPOを使ってプリンターを配信しています。
Windows Server 2008 R2 のGPOにて、クライアントにプリンタを配布する - メモ的な思考的な
 

その環境でEPSONのPX-105プリンタを配信しているのですが、

  • 本体に液晶がない
  • プリンターのインクが切れた時に、インク切れのインクタンクが光らない
  • インク残量を見るためには、各端末にツールを入れる必要がある

というプリンタ仕様*1のため、インク残量が分かりにくく悩んでいました。
 

そんな中、設定ユーティリティのEpsonNet Config V4SNMPの項目があることに気づきました。試しにRubyスクリプトを使ってデータをとってみたところ、インク残量を取得することができました。

そこで、Sinatraを使ってグラフ表示すると自分以外の人でも状態が見えてラクだと考え、作ってみることにしました。

 

環境

 

 

Rubyを使った、SNMPの Printer MIBによるインク残量の取得

Rubyによるインク残量の取得は以下の記事がとても参考になりました。ありがとうございました。
rubyでsnmpを使ってネットワークプリンタのトナー残量チェック - ころもの記録
 

なお、community名はEpsonNet Config V4で確認したものを使いました。
EpsonNet Config V4 - EPSON Download
 

また、EPSONのPrinter MIBに関する資料は以下のページにありました(が、実際のものを見るには登録が必要...)。
MIB2に対するエプソンの解釈標準 - EPSON
 

他に、Printer MIBに関するRFCを探してみたところ、以下のRFC1759とRFC3805がありました。上記のエプソンのページでは、

RFC1759で規定されているPrinterMIBの各項目

と書かれていたため、PX-105でもRFC1759を採用しているのかなと想像しましたが、詳しいところはよく分かりませんでした。

 

Sinatraでのグラフ表示

グラフ表示については、以前HighChartsを使ったことがあったため、今回も使ってみることにしました。
Ruby + Heroku + Highchartsで食べたリンゴの割合をグラフ化してみた - メモ的な思考的な
 

HighChartsを手頃に使えそうなgemを探してみたところ、 LazyHighChartsがあったため、使ってみることにしました。
michelson/lazy_high_charts
 

SInatraで使う場合には、公式のサンプルを参考に以下の記述を追加しました。
Sinatra lazy high charts example - michelson/lazy_high_charts

  • gemにactionviewを追加
  • requireした後にinclude LazyHighCharts::LayoutHelperして使う
     

なお、今回使用するStacked Columnsのサンプルは以下にありました。
Example for stacked column - michelson/lazy_high_charts
 

また、「残量/100」のうちの残量をタンクの色にしたかったため、複数のグラフを用意して

div.chart {
    display: table-cell;
}

のように、cssで横並びにする方法にしました。
本当は一つのグラフだけで表現したかったのですが、やり方が分からなかったため見送りました。

 

 

インク残量の定期メール送信

ブラウザでグラフ表示するのも手間に感じることもあるため、定期的にインク残量をチェックし、メールで送信することも考えました。

定期実行する方法

gemを探してみたところ、wheneverWindows上では動かないようだったので、rufus-schedulerの3.x系を使いました。
jmettraux/rufus-scheduler

使い方については、以下のBlogを参考にしました。
webサーバー死活監視自作してみた by ruby + sinatra + heroku - shoprevのブログ

 

メールの送信

Sinatraの公式でも紹介されていたgem Pony を使用しました。
How do I send email from Sinatra? - Sinatra adamwiggins/pony

メールに関する設定は gem dotenvを使って、.envファイルに記載するようにしました。

 

RubyファイルのWindowsサービス化

普通のWindows7端末なので、WindowsUpdateなどが走ると再起動してしまうことから、以下を参考にWindowsサービス化しました。
Ruby 2.0 + Sinatra + Thin を Windows サービス化してみた - 記憶は削除の方向で

なお、サービスのGUIには表示されますが、GUIを使ってサービスの再起動をしてみたところ失敗したため、再起動などはコマンドラインから実行する必要があるようです。

 

Sinatraまわりで調べたこと

起動時に一度だけ行う処理の記載

configureメソッドを使います。今回はrufus-schedulerのインスタンスを入れました。
Intro Configuration - Sinatra
 

configureで指定できる設定内容

以下に記載がありました。
Configuring Settings - Sinatra

 

ソースコード

GitHubに上げました。初めてSinatraをModuler Applicationとして作れたので良かったです。
thinkAmi/PrinterStatus - GitHub

 

実行結果のサンプル

f:id:thinkAmi:20140522052248p:plain

 

参考資料

SNMPのPrinter MIB に関して、MIBオブジェクトの日本語解説が以下の書籍にあったので参考になりました。

複合型プリントサーバ

複合型プリントサーバ

*1:販売価格を考えると仕方ないと思いますが

*2:明示的に入れておかないとエラーで落ちた