// Text of project Altered States written on 4/25/97 at 1:31 PM // Beginning of file protoFSM // Before Script for "_userproto000" // Newton Developer Technical Support Sample Code // protoFSM - An NTK Finite State Machine User Proto // by Jim Schram, Newton Developer Technical Support // Copyright ©1996 Apple Computer, Inc. All rights reserved. // // You may incorporate this sample code into your applications without // restriction. This sample code has been provided "AS IS" and the // responsibility for its operation is 100% yours. You are not // permitted to modify and redistribute the source as "DTS Sample Code." // If you are going to re-distribute the source, we require that you // make it clear in the source that the code was descended from // Apple-provided sample code, but that you've made changes. // kFSMBuildTemplateFunc - compile time only // // Function to call at compile time to create a FSM from a text file definition or other non-layout source. // Performs basic sanity checking, constructs a protoFSM user template, and returns it. // // // DefConst ( 'kMyFSMTemplate, // call kFSMBuildTemplateFunc with ( 'myFSM, // symbol = required name of this FSM // nil, // frame or nil = optional context frame // Home & "myFSM.f", // string or frame = required name of frame definition file to load, or frame definition // GetLayout("protoFSM") // frame = required reference to protoFSM engine, or other engine // ) // ); // // // fsm := kMyFSMTemplate:Instantiate(); // kFSMBuildTemplateFunc := func(name, context, definition, engine) begin local fsmSymbol := name; if not fsmSymbol then begin fsmSymbol := 'unknown; print("A protoFSM implementation is unnamed (kFSMBuildTemplateFunc requires the name parameter)."); end; if not engine then begin print("The '|" & fsmSymbol & "| protoFSM engine is invalid."); return; end; definitionFrame := if IsInstance(definition, 'string) then Load(definition) else definition; if not IsInstance(definitionFrame, 'frame) then begin print("The '|" & fsmSymbol & "| protoFSM definition is invalid."); return; end; if not definitionFrame.Genesis then print("The '|" & fsmSymbol & "| protoFSM implementation is missing the required '|Genesis| state."); local templateFrame := if IsInstance(context, 'frame) then context else {}; templateFrame._proto := engine; templateFrame.declareSelf := fsmSymbol; templateFrame.fsm_private_states := definitionFrame; if not kDebugOn then // GoToState is a debug-only function! begin RemoveSlot(templateFrame._proto, 'GoToState); RemoveSlot(templateFrame, 'GoToState); end; RemoveSlot(templateFrame._proto, '_proto); RemoveSlot(templateFrame._proto, 'viewBounds); templateFrame; end; // kFSMCleanUpFunc - compile time only // // Function to call in the afterScript of your FSM layout. // Converts NTK layout data structures into "pure" FSM data structures used by protoFSM. // // Example: // // // call kFSMCleanUpFunc with (thisView); // let's assume the filename of this layout is "myFSM.t" // // // fsm := GetLayout("myFSM.t"):Instantiate(); // kFSMCleanUpFunc := func(fsmFrame) begin local fsmSymbol, stateSymbol, eventSymbol, hasGenesisState; RemoveSlot(fsmFrame._proto, '_proto); RemoveSlot(fsmFrame._proto, 'viewBounds); RemoveSlot(fsmFrame, 'viewBounds); RemoveSlot(fsmFrame, 'viewJustify); fsmFrame.fsm_private_states := { }; if not fsmSymbol := GetSlot(fsmFrame, 'declareSelf) then begin fsmSymbol := 'unknown; print("A protoFSM implementation is unnamed (you forgot the declareSelf slot)."); end; if fsmFrame.stepChildren then foreach stateFrame in fsmFrame.stepChildren do begin RemoveSlot(stateFrame, '_proto); RemoveSlot(stateFrame, 'viewBounds); RemoveSlot(stateFrame, 'viewJustify); if not stateSymbol := GetSlot(stateFrame, 'declareSelf) then begin stateSymbol := 'unknown; print("A protoState in the '|" & fsmSymbol & "| protoFSM implementation is unnamed (you forgot the declareSelf slot)."); end; if fsmFrame.fsm_private_states.(stateSymbol) then print("The '|" & stateSymbol & "| protoState in the '|" & fsmSymbol & "| protoFSM implementation already exists (duplicate declareSelf slot value)."); else fsmFrame.fsm_private_states.(stateSymbol) := stateFrame; if stateFrame.stepChildren then foreach eventFrame in stateFrame.stepChildren do begin RemoveSlot(eventFrame, '_proto); RemoveSlot(eventFrame, 'viewBounds); RemoveSlot(eventFrame, 'viewJustify); if not eventSymbol := GetSlot(eventFrame, 'declareSelf) then begin eventSymbol := 'unknown; print("A protoEvent in the '|" & stateSymbol & "| state of the '|" & fsmSymbol & "| protoFSM implementation is unnamed (you forgot the declareSelf slot)."); end; if stateFrame.(eventSymbol) then print("The '|" & eventSymbol & "| protoEvent in the '|" & stateSymbol & "| protoState in the '|" & fsmSymbol & "| protoFSM implementation already exists (duplicate declareSelf slot value)."); else stateFrame.(eventSymbol) := eventFrame; RemoveSlot(eventFrame, 'declareSelf); end; RemoveSlot(stateFrame, 'declareSelf); RemoveSlot(stateFrame, 'stepChildren); hasGenesisState := hasGenesisState or stateSymbol = 'Genesis; end; if not hasGenesisState then print("The '|" & fsmSymbol & "| protoFSM implementation is missing the required '|Genesis| state."); if not kDebugOn then // GoToState is a debug-only function! begin RemoveSlot(fsmFrame._proto, 'GoToState); RemoveSlot(fsmFrame, 'GoToState); end; RemoveSlot(fsmFrame, 'stepChildren); nil; end _userproto000 := {viewBounds: {left: 8, top: 8, right: 232, bottom: 280}, DoEvent: func(eventSymbol, paramArray) // SELF can be anything that inherits to the finite state machine instance frame begin local x := :?DoEvent_Check('|protoFSM:DoEvent|); if not x then return; if kDebugOn then if paramArray and PrimClassOf(paramArray) <> 'Array then Throw('|evt.ex.msg|, "protoFSM:DoEvent 2nd argument must be Nil or Array"); x.pendingEventQueue:EnQueue(eventSymbol); x.pendingParamsQueue:EnQueue(paramArray); if not x.busy then begin x.busy := true; AddDelayedSend(x.fsm, 'DoEvent_Loop, nil, x.turtle); end; nil; end, instantiate: // SELF is the finite state machine template frame, e.g.: // // local fsm := GetLayout("myFSM"):Instantiate(); // // "myFSM" is assumed to be a layout based on protoFSM, func() begin local obj := { _proto: self, fsm: nil, currentStateFrame: nil, currentEventFrame: nil, fsm_private_context: { fsm: nil, turtle: 1, level: 0, busy: nil, waitView: nil, waitAborted: nil, isNewPendingState: true, pendingState: 'Genesis, pendingEventQueue: QueueTemplate:Instantiate(), pendingParamsQueue: QueueTemplate:Instantiate(), currentState: nil, currentEvent: nil, currentParams: nil, }, }; obj.currentStateFrame := { _proto: obj.fsm_private_states.Genesis, _parent: obj, }; obj.fsm := obj.fsm_private_context.fsm := obj; end, dispose: func() // SELF should be the finite state machine instance frame begin if not fsm_private_context then return; fsm_private_context.pendingEventQueue:Reset(); fsm_private_context.pendingParamsQueue:Reset(); foreach slot, value in fsm_private_context do fsm_private_context.(slot) := nil; fsm := currentStateFrame := currentEventFrame := fsm_private_context := nil; nil; // guaranteed to return nil so that the caller can conveniently nil out the FSM container variable end, DoEvent_Loop: func() // SELF is the finite state machine instance frame begin repeat local x := :?DoEvent_Check('|protoFSM:DoEvent_Loop|); if not x then return; local ok := nil; local pendingStateFrame := nil; if x.pendingState then if fsm_private_states.(x.pendingState) then if fsm_private_states.(x.pendingState).(x.pendingEventQueue:Peek()) then ok := true; else begin if kDebugOn then :?DebugFSM('UnknownEvent, x.pendingState, x.pendingEventQueue:Peek(), x.pendingParamsQueue:Peek()); // ignore if event not programmed end; else begin if kDebugOn then :?DebugFSM('UnknownState, x.pendingState, x.pendingEventQueue:Peek(), x.pendingParamsQueue:Peek()); // error --> remain in current state end; else begin if kDebugOn then :?DebugFSM('NilState, x.pendingState, x.pendingEventQueue:Peek(), x.pendingParamsQueue:Peek()); // machine halted end; if not ok then begin currentStateFrame := nil; currentEventFrame := nil; x.isNewPendingState := true; x.pendingEventQueue:DeQueue(); // there is a problem with this state or event x.pendingParamsQueue:DeQueue(); // so remove the offending pending queue elements end; else begin x.currentState := x.pendingState; x.currentEvent := x.pendingEventQueue:DeQueue(); x.currentParams := x.pendingParamsQueue:DeQueue(); if x.isNewPendingState then begin x.isNewPendingState := nil; currentStateFrame := { _proto: fsm_private_states.(x.currentState), _parent: self, }; end; currentEventFrame := { _proto: fsm_private_states.(x.currentState).(x.currentEvent), _parent: currentStateFrame, }; if currentEventFrame.Action then begin if kDebugOn then :?TraceFSM('PreAction, x.currentState, x.currentEvent, x.currentParams); x.level := x.level + 1; try Perform(currentEventFrame, 'Action, x.currentParams); onexception |evt.ex| do begin try :?ExceptionHandler(CurrentException()); onexception |evt.ex| do nil; end; x.level := x.level - 1; if kDebugOn then :?TraceFSM('PostAction, x.currentState, x.currentEvent, x.currentParams); end; if currentEventFrame.nextState then begin x.pendingState := currentEventFrame.nextState; x.isNewPendingState := (x.currentState <> x.pendingState); end; if kDebugOn then :?TraceFSM('NextState, x.pendingState, x.pendingEventQueue:Peek(), x.pendingParamsQueue:Peek()); end; if x.waitView // check for terminal state & exit waitView if necessary and x.pendingState and pendingStateFrame := fsm_private_states.(x.pendingState) then if pendingStateFrame.terminal then begin x.pendingEventQueue:Reset(); x.pendingParamsQueue:Reset(); AddDelayedCall( func() if x.waitView then x.waitView:Close(), nil, 1 ); end; x.busy := not x.pendingEventQueue:IsEmpty(); until not(x.busy and currentEventFrame and currentEventFrame.nextNoIdle); if x.busy then AddDelayedSend(self, 'DoEvent_Loop, nil, x.turtle); nil; end, SetSpeed: func(newSpeed) // SELF is the finite state machine instance frame begin fsm_private_context.turtle := newSpeed; end, IsBusy: func() // SELF is the finite state machine instance frame begin fsm_private_context.busy; end, GetSpeed: func() // SELF is the finite state machine instance frame begin fsm_private_context.turtle; end, GoToState: // SELF is the finite state machine instance frame // This function is for DEBUGGING USE ONLY ! ! ! // It is STRIPPED from the resulting package when kDebugOn = nil func(newState) begin local x := fsm_private_context; x.pendingState := newState; x.pendingEventQueue:Reset(); x.pendingParamsQueue:Reset(); nil; end, QueueTemplate: { Instantiate: func() // This is a very simple First-In-First-Out queue { _proto: self, queue: [], }, Reset: func() SetLength(queue, 0), Peek: func() if Length(queue) > 0 then queue[0], // else nil DeQueue: func() if Length(queue) > 0 then // else nil begin local data := queue[0]; RemoveSlot(queue, 0); data; end, EnQueue: func(data) begin AddArraySlot(queue, data); nil; end, GetQueueSize: func() Length(queue), IsEmpty: func() Length(queue) = 0, }, ProtoClone: func(object) begin local f := func native(obj) begin if not IsFrame(obj) or IsFunction(obj) then Throw('|evt.ex.msg|, "ProtoClone only works with frames."); local new := {_proto: obj}; foreach slot, value in obj do if IsFrame(value) and not IsFunction(value) then new.(slot) := call f with (value); new; end; call f with (object); end, WaitForTerminal: func(options) begin local x := fsm_private_context; if x.waitView or x.level <> 0 or x.pendingEventQueue:IsEmpty() then return; x.waitView := BuildContext(waitViewTemplate); x.waitView:SetOwnerContext(x, options); x.waitView:ModalDialog(); x.waitAborted; // return the value of waitAborted (true = user aborted via the status slip, nil = FSM terminal state was reached normally) end, waitViewTemplate: { viewClass: clView, viewFlags: vVisible, viewFormat: vfNone, viewBounds: { left: 0, top: 0, right: 0, bottom: 0, }, statusView: nil, statusViewOptions: nil, fsmContext: nil, aborted: nil, SetOwnerContext: func(owner, options) begin self.statusView := nil; self.statusViewOptions := options; // frame of options the caller of WaitForTerminal is passing us (e.g. progress messages, etc.) self.fsmContext := owner; // the fsm_private_context slot of the FSM self.aborted := nil; end, viewIdleScript: func() begin statusView := BuildContext(statusViewTemplate); statusView:SetOwnerContext(self, statusViewOptions); statusView:ModalDialog(); nil; end, viewSetupDoneScript: func() begin inherited:?ViewSetupDoneScript(); if not statusViewOptions then :SetUpIdle(2000); else if statusViewOptions.delayUntilStatusVisible then :SetUpIdle(if statusViewOptions.delayUntilStatusVisible <= 0 then 1 else statusViewOptions.delayUntilStatusVisible); end, viewQuitScript: func() begin if statusView then statusView:Close(); fsmContext.waitAborted := aborted; fsmContext.waitView := nil; end, statusViewTemplate: { _proto: protoStatusTemplate, initialSetup: nil, waitView: nil, delayUntilAbortTimer: nil, delayUntilAbortVisible: nil, abortButtonText: nil, viewIdleScript: func() begin inherited:?viewIdleScript(); local contents := { name: 'vBarber, values: { barber: true, }, }; if delayUntilAbortVisible then begin delayUntilAbortTimer := delayUntilAbortTimer + 300; if delayUntilAbortTimer > delayUntilAbortVisible then begin delayUntilAbortVisible := nil; contents.values.primary := { text: abortButtonText, script: func() begin waitView.aborted := true; waitView:Close(); end, }; base:ViewSet(contents); return 300; end; end; base:UpdateIndicator(contents); 300; end, viewSetupDoneScript: func() begin inherited:?ViewSetupDoneScript(); :SetUpIdle(100); self.delayUntilAbortTimer := 0; end, SetOwnerContext: func(owner, options) begin self.waitView := owner; self.delayUntilAbortVisible := if options then options.delayUntilAbortVisible else 8000; self.abortButtonText := if options and options.abortButtonText then options.abortButtonText else "Abort"; self.initialSetup := { name: 'vBarber, values: { icon: ROM_routeUpdateBitmap, statusText: if options then options.statusText else "Please wait...", titleText: if options then options.titleText else nil, barber: true, primary: nil, closeBox: nil, }, }; end, }, }, ExceptionHandler: func(exception) begin local x := fsm_private_context; local message := if x then "The following exception occured in event (" & x.currentEvent & ") of state (" & x.currentState & ") of finite state machine (" & x.fsm.declareSelf & "): " else "The following exception occured: "; local exceptionStr := ""; try exceptionStr := :ObjectToString(exception); onexception |evt.ex| do nil; :ExceptionHandler_Notify(message & exceptionStr); nil; end, ObjectToString: // Converts (any?) NewtonScript data type into a string representation. // Follows _proto pointers if the global var printProto is true. // Follows _parent pointers if the global var printParent is true. // Respects the global var printDepth (printDepth < 0 is effectively "infinite depth"). // Respects the global var printLength (printLength < 0 or nil is effectively "infinite length"). // Traps recursive references avoiding endless loops. // Gives each frame and array a "reference number" so backpointer references make sense. // Prints real numbers according to the global var printReal (a string defining the print format, e.g. "%.2f"). // Punts on any exception, like evt.ex.outofmem. // NOTE: This routine is primarily intended for use only during debugging. func(obj) begin constant separator := ", "; constant separatorLen := StrLen(separator); local backIndex := nil; local backList := [nil]; local runProto := GetGlobalVar('printProto) <> nil; local runParent := GetGlobalVar('printParent) <> nil; local floatFormat := GetGlobalVar('printReal); if not IsString(floatFormat) then floatFormat := "%.16e"; local maxDepth := GetGlobalVar('printDepth); if not IsNumber(maxDepth) or maxDepth < 0 then maxDepth := 65535; // arbitrarily large value local maxLength := GetGlobalVar('printLength); if not IsNumber(maxLength) or maxLength < 0 then maxLength := 65535; // arbitrarily large value local p := func native(s) begin if EndsWith(s, separator) then StrMunger(s, StrLen(s) - separatorLen, nil, nil, 0, nil) else s; end; local f := func native(obj, depth) begin local x := 0; ( if IsMagicPtr(obj) then "@+" & RefOf(obj) else if IsFunction(obj) then begin x := GetFunctionArgCount(obj); ParamStr("func(^0 arg^?1|s|)", [x, x = 1]); end else if IsFrame(obj) then if backIndex := LSearch(backList, obj, 1, '|=|, nil) then "<" & backIndex & ">" else begin local s := "{<" & Length(backList) & "> "; AddArraySlot(backList, obj); if depth > maxDepth then s := s & "+" & RefOf(obj); else foreach slot, item in obj do if (x := x + 1) > maxLength then begin s := s & "..."; break; end else s := s & slot & ": " & if (slot = '_parent and not runParent) or (slot = '_proto and not runProto) then "" & separator else call f with (item, depth + 1); call p with (s) & "}"; end else if IsArray(obj) then if backIndex := LSearch(backList, obj, 1, '|=|, nil) then "<" & backIndex & ">" else begin local s := "[<" & Length(backList) & "> " & (if ClassOf(obj) <> 'Array then ClassOf(obj) & ": "); AddArraySlot(backList, obj); if depth > maxDepth then s := s & "+" & RefOf(obj); else foreach item in obj do if (x := x + 1) > maxLength then begin s := s & "..."; break; end else s := s & call f with (item, depth + 1) ; call p with (s) & "]"; end else if IsString(obj) then $" & obj & $" else if IsSymbol(obj) then $' & obj else if IsInteger(obj) then (if obj > 0 then "+" else "") & NumberStr(obj) else if IsNumber(obj) then (if obj > 0 then "+" else "") & FormattedNumberStr(obj, floatFormat) else if IsImmediate(obj) then if not obj then "nil" else if obj = true then "true" else SPrintObject(obj) else if IsBinary(obj) then "<" & ClassOf(obj) & ", length " & Length(obj) & ">" else SPrintObject(obj) ) & separator; end; try call p with (call f with (obj, 0)); onexception |evt.ex.outofmem| do begin backList := nil; ""; end; onexception |evt.ex| do begin backList := nil; ""; end; end, DoEvent_Check: func(where) // This routine checks to see if the FSM has been unexpectedly disposed. That's especially bad if there's a delayed action/call/send about to trigger! begin local x := fsm_private_context; if x then return x; if kDebugOn then :?ExceptionHandler_Notify("An active protoFSM object has mysteriously vanished! Discovered in " & SPrintObject(where) & ". You should consider resetting your Newton device now."); nil; end, ExceptionHandler_Notify: func(message) begin GetRoot():Notify(kNotifyAlert, kAppName, message); if kDebugOn then begin print(message); if GetGlobalVar('BreakOnThrows) then BreakLoop(); end; nil; end, _proto: @218 }; constant |layout_protoFSM| := _userproto000; // End of file protoFSM // Beginning of text file ExampleFSM DefConst( 'kExampleFSMContext, { fTheAnswerToTheUltimateQuestionOfLifeTheUniverseAndEverything: nil, DebugFSM: func(reason, state, event, params) begin local s := "Reason = " & :ObjectToString(reason) & "\nState = " & :ObjectToString(state) & "\nEvent = " & :ObjectToString(event) & "\nParams = " & :ObjectToString(params); :MTrace(s); print(SubstituteChars(s, "\n", "\t")); end, ExceptionHandler: func(exception) begin inherited:?ExceptionHandler(exception); // Implement your own exception handler here, or use the cheesy default one provided by protoFSM. end, MTrace: func(s) begin SetValue(GetRoot().(kAppSymbol).vTraceBox, 'text, s); RefreshViews(); end, TraceFSM: func(when, state, event, params) begin local s := "When = " & :ObjectToString(when) & "\nState = " & :ObjectToString(state) & "\nEvent = " & :ObjectToString(event) & "\nParams = " & :ObjectToString(params); :MTrace(s); print(SubstituteChars(s, "\n", "\t")); end, } ); DefConst( 'kExampleFSM, call kFSMBuildTemplateFunc with ( 'ExampleFSM, kExampleFSMContext, HOME & "ExampleFSM.f", GetLayout("protoFSM") ) ); // See ResetFSM in Main.t for Instantiate syntax. // End of text file ExampleFSM // Beginning of file Main.t // Before Script for "Example FSM" // Newton Developer Technical Support Sample Code // Maint.t - An NTK Finite State Machine Example Implementation // by Jim Schram, Newton Developer Technical Support // Copyright ©1996 Apple Computer, Inc. All rights reserved. // // You may incorporate this sample code into your applications without // restriction. This sample code has been provided "AS IS" and the // responsibility for its operation is 100% yours. You are not // permitted to modify and redistribute the source as "DTS Sample Code." // If you are going to re-distribute the source, we require that you // make it clear in the source that the code was descended from // Apple-provided sample code, but that you've made changes. Example FSM := {title: kAppName & " (Text Files FSM)", viewBounds: {left: 0, top: 10, right: 219, bottom: 205}, viewFormat: 83952209, viewSetupDoneScript: func() begin :ResetFSM(); end, FSM: nil, viewQuitScript: // must return the value of inherited:?viewQuitScript(); func() begin FSM := FSM:Dispose(); inherited:?viewQuitScript(); // this method is defined internally end, ResetFSM: func() begin if FSM then FSM := FSM:Dispose(); FSM := kExampleFSM:Instantiate(); FSM:DoEvent('Create, ["The answer to the ultimate question of life the universe and everything is 42."]); end, ReorientToScreen: ROM_DefRotateFunc, viewSetupFormScript: func() begin // resize to fit on all "small" newtons. constant kMaxWidth := 240; constant kMaxHeight := 336; local b := GetAppParams(); self.viewBounds := RelBounds(b.appAreaLeft, b.appAreaTop, MIN(b.appAreaWidth, kMaxWidth), MIN(b.appAreaHeight, kMaxHeight)); end, debug: "Example FSM", _proto: @157 }; Reset := { buttonClickScript: func() begin :ResetFSM(); end, text: "Reset", viewBounds: {left: 82, top: 26, right: 142, bottom: 46}, debug: "Reset", _proto: @226 }; AddStepForm(Example FSM, Reset); Event 1 := { buttonClickScript: func() begin if FSM:IsBusy() then GetRoot():Notify(kNotifyAlert, kAppName, "The state machine is busy. Please try again later."); else FSM:DoEvent('Event1, nil); end, text: "Event 1", viewBounds: {left: 34, top: 58, right: 102, bottom: 78}, debug: "Event 1", _proto: @226 }; AddStepForm(Example FSM, Event 1); Event 2 := { buttonClickScript: func() begin if FSM:IsBusy() then GetRoot():Notify(kNotifyAlert, kAppName, "The state machine is busy. Please try again later."); else FSM:DoEvent('Event2, nil); end, text: "Event 2", viewBounds: {left: 122, top: 58, right: 190, bottom: 78}, debug: "Event 2", _proto: @226 }; AddStepForm(Example FSM, Event 2); vTraceBox := {text: "", viewBounds: {left: 9, top: 89, right: 218, bottom: 170}, viewJustify: 0, viewFont: simpleFont9, debug: "vTraceBox", _proto: @218 }; AddStepForm(Example FSM, vTraceBox); StepDeclare(Example FSM, vTraceBox, 'vTraceBox); constant |layout_Main.t| := Example FSM; // End of file Main.t