From: Mark Probert Date: 2004-11-03T13:43:50+09:00 Subject: Re: fsmgen 0.1 (longish) Hi .. vruz wrote: > > Great work, this is a nice addition that could well be (after polished > and debugged to death, of course) part of the stdlib. > Thank you. It needs polishing and debugging, that is for sure. It does work, though I am not 100% on all of the failure paths. > That YAML code is the FSM definition, now... > how do you jump from one state to another ? > all in Ruby code ? > Fairly easily (Ruby is great for this kind of thing). I take states, actions and events and create hashes. The actions are keyed off an MD5 hash of state and action (this is really a DFA machine -- for an action, each of the events must be unique). When an incoming message (event) matches for a given action, the action callback is fired. By default, this returns a message on the socket. > Taking as an example this vending machine (sorry, in C# + XML) > http://www.codeproject.com/csharp/xmlfsm.asp > What would be the corresponding code in Ruby + YAML ? > This is in fact the example I have included :-), though I took the example courtesy of Robert C Martin's article 'UML Tutorial: Finite State Machines' from C++ Report http://www.objectmentor.com/publications/UMLFSM.PDF The example, without comments, is as follows (if you want the comments, then I can send the package): # --------------------( turnstile.yaml ) Server: name: Turnstile # we need a name type: TCP # default server type [TCP|UDP] port: 13345 # mandatory field Events: COIN: Coin is placed in the turnstile PASS: The turnstile has been passed RESET: Turn off the alarm READY: Ready for normal operation Actions: LOCK: [ lock, "The turnstile is now locked." ] UNLOCK: [ unlock, "The turnstile is now unlocked." ] THANKS: [ donation, "Thank you for your donation." ] ALARM_ON: [ alarm_on, "Woop! Woop!" ] ALARM_OFF: [ alarm_off, "The alarm is now turned off." ] READY: [ ready, "The system is now ready." ] States: Start: [ LOCK, Locked ] Locked: COIN: [ UNLOCK, Unlocked ] PASS: [ ALARM_ON, Violation ] Unlocked: COIN: [ THANKS, Unlocked ] PASS: [ LOCK, Locked ] Violation: RESET: [ ALARM_OFF, Violation ] READY: [ [ ALARM_OFF, LOCK ], Locked ] PASS: COIN: You run 'fsmgen' on the YAML file to produce the server and a simple test client. The basic server looks like: # -----------------( Turnstile.rb ) require 'FSM' class Turnstile < FSM::DFA # -----( alarm_off ) # Action for event: ALARM_OFF # def alarm_off ( params ) @sess.puts "The alarm is now turned off." end # -----( unlock ) # Action for event: UNLOCK # def unlock ( params ) val = params.to_i if val < 25 @sess.puts "Not enough bud! (#{val}c) You need 25c." @change_state = false else @sess.puts "The turnstile is now unlocked." end end # ... etc end # -----( done ) class TurnstileSvr < FSM::SimpleFSMServer # ----- # Basic startup # def initialize(cfg, verbose=false) super(cfg, verbose) end # ----- # Basic server startup # def start srv = TCPServer.new(@port) puts "server started" while (session = srv.accept) Thread.new(session) do |s| domian, port, ipname, ipaddr = s.peeraddr str = "connection from #{ipname}" puts str; @log.info str @fsm = Turnstile.new(@config, @log) @fsm.verbose = @verbose while true do event = s.gets.chomp @log.info "client(#{ipname}) event(#{event})" @fsm.handle_response(s, event) end end end end end # ----------( server ) # ---------------------------------- # # MAIN -- Server Start # # ---------------------------------- # def end_program() puts " *** Terminating *** " exit end trap("SIGINT") { end_program } trap("SIGKILL") { end_program } svr = TurnstileSvr.new("turnstile.yaml", true) svr.start # --------------------( done ) And a basic test client like: # -----------------( Turnstile_client.rb ) require 'socket' PORT = 13345 HOST = ARGV[0] || "localhost" def test_events(sess, evts) evts.each do |evt| sess.puts evt s = sess.gets puts "sent(#{evt}) response --> #{s}" end end puts "Testing:" session = TCPSocket.new(HOST, PORT) puts "Correct sequence .." evts = ["PASS", "RESET", "COIN 25", "PASS", "READY"] test_events(session, evts) session.close puts "...done" I hope that this helps a little in explaining what I have done. Regards, -mark.