====== 常駐部の開発 ====== ===== コーディング ===== ファイルを置く場所を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で停止したり、コンソールを使ったデバッグも可能です。 最終的にはシステム起動時に自動起動し、デーモンとなるようにします。これは、別項に記します。