====== 常駐部の開発 ======
===== コーディング =====
ファイルを置く場所をbin以下とし、ディレクトリを作成します。
cd /usr/local/kitchen_timer
mkdir bin
vi kitchen_timer.rb
==== ひな形の作成 ====
[[alworker:基本機能|AlWorker基本機能]]の項を参照しながら、まずひな形を作り、必要な初期化コードを入れます。
require "al_worker_ipc"
require "al_worker_message"
require "al_worker_timer"
require "al_worker_debug"
class KitchenTimer < AlWorker
##
# constructor
#
def initialize()
super( "kitchen_timer" )
end
##
# イニシャライザ
#
def initialize2()
@timer_counter = 0
@input_str = ""
set_state( "INPUT" )
set_value( "display_digit", format_count(@timer_counter) )
AlWorker.log.level = Logger::DEBUG
@ipc = Ipc.new()
@ipc.chmod = 0666
@ipc.run( self )
@timer = Timer.periodic( 1 )
@msg = NumberedMessage.new()
Debug.run( self )
end
##
# 表示フォーマッティング
#
def format_count( n )
case n
when Numeric
return sprintf( "%02d:%02d", n / 60, n % 60 )
when String
s = "0000" + n
return "#{s[-4,2]}:#{s[-2,2]}"
end
end
end
KitchenTimer.new.run
使用する機能は、[[alworker:ipc_プロセス間通信|IPC]]、[[alworker:番号付きメッセージキュー|番号付きメッセージキュー]]、および[[alworker:タイマー|タイマー]]です。\\
変数は以下のように使います。
@timer_counter\\
実際のカウントダウンに使う。
@input_str\\
入力文字を保持する。
注意点としては、初期ステートを"INPUT"とするようにset_stateメソッドを使っている点と、表示には[[alworker:値の保持・提供]]機能を使っている点です。この利点については、後ほど説明します。
==== IPCアクションの用意 ====
ブラウザ上のボタンクリックはIPCを使って伝えられることになるので、それをイベントに変換するコードを記述します。
##
# (IPC) 数字キーが押された場合
#
def ipc_digit( sock, param )
digit = param["value"]
trigger_event( "DIGIT", digit )
reply( sock, 200, "OK" )
end
##
# (IPC) 開始が押された
#
def ipc_start( sock, param )
trigger_event( "START" )
reply( sock, 200, "OK" )
end
##
# (IPC) 停止が押された
#
def ipc_stop( sock, param )
trigger_event( "STOP" )
reply( sock, 200, "OK" )
end
また、ServerSentEventsを使ってブラウザにリクエストを出すので、そのためのコードを記述します。\\
これはほぼ定型ですので、多少のアレンジはあっても、だいたいどのプログラムでも以下のようになるでしょう。
##
# (IPC) クライアント(ブラウザ, JavaScript)からのリクエストを受け、
# メッセージ(イベント)をServerSentEventsプロトコルで返す。
#
def ipc_a_listen_ssev( sock, param )
tid = param["LAST_EVENT_ID"] == 0 ? @msg.tid : param["LAST_EVENT_ID"]
@msg.cycle( tid + 1 ) { |m|
sock.puts "id: #{m[:TID].to_i}"
sock.puts "data: #{m.to_json}"
sock.puts ""
}
end
==== 状態遷移表をみながらハンドラを記述する ====
状態遷移表の各セルの内容を、def state_状態名_event_イベント名() の形式で、メソッドとして記述します。\\
あわせて、何もしないセル「/」や、あり得ないセル「×」を、:na を使って宣言しておきます。
##
# 数字入力
#
def state_INPUT_event_DIGIT( digit )
@input_str << digit.to_s
set_value( "display_digit", format_count(@input_str) )
@msg.send( :value=>format_count(@input_str) )
end
##
# カウント開始
#
def state_INPUT_event_START()
s = "0000#{@input_str}"
@timer_counter = s[-4,2].to_i * 60 + s[-2,2].to_i
return if @timer_counter == 0
@input_str = ""
@timer.run() { timer_work() }
next_state( "COUNTDOWN" )
end
##
# 入力値クリア
#
def state_INPUT_event_STOP()
@input_str = ""
set_value( "display_digit", format_count(@input_str) )
@msg.send( :value=>format_count(@input_str) )
end
na :state_INPUT_event_TIMER
na :state_INPUT_event_TIMEUP
na :state_COUNTDOWN_event_DIGIT
na :state_COUNTDOWN_event_START
##
# カウント中断
#
def state_COUNTDOWN_event_STOP()
@timer.stop()
set_value( "display_digit", format_count(0) )
@msg.send( :value=>format_count(0) )
next_state( "INPUT" )
end
##
# カウントダウン中 表示更新
#
def state_COUNTDOWN_event_TIMER()
set_value( "display_digit", format_count(@timer_counter) )
@msg.send( :value=>format_count(@timer_counter) )
end
##
# タイムアップ
#
def state_COUNTDOWN_event_TIMEUP()
set_value( "display_digit", "TIMEUP!" )
@msg.send( :value => "TIMEUP!" )
@msg.send( :sound => "play" )
next_state( "ALERM" )
end
na :state_ALERM_event_DIGIT
na :state_ALERM_event_START
##
# アラーム停止
#
def state_ALERM_event_STOP()
@timer.stop()
set_value( "display_digit", format_count(0) )
@msg.send( :value=>format_count(0) )
next_state( "INPUT" )
end
##
# アラームを鳴らす
#
def state_ALERM_event_TIMER()
@msg.send( :sound => "play" )
end
na :state_ALERM_event_TIMEUP
==== タイマーハンドラを記述する ====
イニシャライザで1秒ごとのタイマーを定義しました。このタイマーのイベントハンドラを記述します。\\
ハンドラでは、カウンターをデクリメントし、通常はTIMERイベント、ゼロになった瞬間にTIMEUPイベントを発生させる仕様とします。これは状態遷移表をみながら、仕様を決定します。
##
# タイマーによる1秒ごとのワーク
#
def timer_work()
@timer_counter -= 1
if @timer_counter == 0
trigger_event( "TIMEUP" )
else
trigger_event( "TIMER" )
end
end
===== ステートプログラミング結果の確認 =====
プログラムを解析し、状態遷移表をリバース生成するツールが付属していますので、それを使って正しく記述できたか確認します。
$ /usr/local/kitchen_timer/lib/make_state_chart.rb kitchen_timer.rb
INPUT COUNTDOWN ALERM
DIGIT full / /
START full / /
STOP full full full
TIMER / full full
TIMEUP / full /
ステート、イベントとも設計通り表示されすべてのセルが埋まっていますので、意図通りプログラミングできていることが確認できました。
===== テスト =====
ターミナルから、以下のように起動します。
ruby -I/usr/local/kitchen_timer/lib $/usr/local/kitchen_timer/bin/kitchen_timer.rb
まだここでは run()メソッドを使っているので、実行してもプロンプトへ制御が帰りません
そのため、^Cで停止したり、コンソールを使ったデバッグも可能です。
最終的にはシステム起動時に自動起動し、デーモンとなるようにします。これは、別項に記します。