ユーザ用ツール

サイト用ツール


kitchen_timer:常駐部の開発_コーディング

常駐部の開発

コーディング

ファイルを置く場所をbin以下とし、ディレクトリを作成します。

cd /usr/local/kitchen_timer
mkdir bin
vi kitchen_timer.rb

ひな形の作成

AlWorker基本機能の項を参照しながら、まずひな形を作り、必要な初期化コードを入れます。

kitchen_timer.rb
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

使用する機能は、IPC番号付きメッセージキュー、およびタイマーです。
変数は以下のように使います。

@timer_counter
実際のカウントダウンに使う。

@input_str
入力文字を保持する。

注意点としては、初期ステートを"INPUT"とするようにset_stateメソッドを使っている点と、表示には値の保持・提供機能を使っている点です。この利点については、後ほど説明します。

IPCアクションの用意

ブラウザ上のボタンクリックはIPCを使って伝えられることになるので、それをイベントに変換するコードを記述します。

kitchen_timer.rb
  ##
  # (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を使ってブラウザにリクエストを出すので、そのためのコードを記述します。
これはほぼ定型ですので、多少のアレンジはあっても、だいたいどのプログラムでも以下のようになるでしょう。

kitchen_timer.rb
  ##
  # (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 を使って宣言しておきます。

kitchen_timer.rb
  ##
  # 数字入力
  #
  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イベントを発生させる仕様とします。これは状態遷移表をみながら、仕様を決定します。

kitchen_timer.rb
  ##
  # タイマーによる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で停止したり、コンソールを使ったデバッグも可能です。

最終的にはシステム起動時に自動起動し、デーモンとなるようにします。これは、別項に記します。

kitchen_timer/常駐部の開発_コーディング.txt · 最終更新: 2014/11/21 01:16 by hirohito