Jump to content

State Machine Programming using variables (Part 1)


ergodic

Recommended Posts

Posted

Now that variables are available, I thought I’d update my 'state diagram' approach to ISY programming to use them. ISY variables – particularly the “state variables†- are a natural fit for this.

I’ve broken this into two posts. If you want to skip over the background here, the next post gets to an example of a new method using state variables.

To very briefly recap the state machine method: you first diagram your logic on paper with numbered circles (“statesâ€). The states represent all the various sub-actions your programming needs. Then draw arrows labeled with the trigger events that transition between the states.

Each state then breaks down into two ISY programs:

(1) A “body†program with no conditions that execute the actions of that state, and

(2) A “conditions†ISY program that only triggers on any of the incoming transitions to the state and then does a Run-Program on its corresponding body program.

If you want to read more about this older, non-variable method to implement this, dnl started with my various haphazard, rambling posts and remarkably elaborated it all into something coherent, here:

http://forum.universal-devices.com/topic/5410-triggers-and-conditions-and-ifs-oh-my/

This prior method used the true/false status of the body programs. Exactly one of the body programs was true at any time – representing the current state of the logic.

But there were drawbacks: achieving this required a lot of tedious, easy-to-screw-up, copy/paste boilerplate. It resulted in obnoxiously dense programs since each body program first had to ‘false-out’ all the others. And, state transitions translated to “Run Program†statements — these can sometimes be tricky because of their asynchronous nature in the ISY.

Anyone who seriously programs their ISY sooner or later come to realize why they have to break any nontrivial logic into two separate ISY programs. But it is worth restating in this case: The body program for a state MUST have no conditions so it is isolated from spurious ISY trigger events that could incorrectly cause it to be stopped.

So the condition(s) for a state need to be tested/triggered by a separate program. We don’t care how often that program evaluates false because nothing gets disrupted when that happens. In effect, the condition programs insulate their body programs from events, only allowing the ‘correct’ ones through.

If you try putting conditions on a body program, or statements in a condition program, you’ll quickly discover you need to be the love child of Countess Ada and Bertrand Russell to figure out what is going on. It would be nice if the ISY allowed us to simply delete the “Else†clause of a program as an indication we don’t want a program interruptible by false-state triggers – the Else section is rarely useful anyway. But we work with what we have.

The next post shows a way to update this approach nicely using state variables.

Posted

(Note: I edited this on 10/14/12 to restore some code that was missing in this post. I also had to make changes in order to work with the current ISY firmware. So these are a little different than originally posted. I have briefly tested them again and they seem to work correctly, but comments and revisions are always welcome. My apologies for the confusion -- ergodic)

 

OK. With the new state variables in the ISY we still keep the overall “state machine†logic, but we can improve a whole lot on the old method.

 

Here are the essentials:

 

(1) First, the set of programs gets an ISY state variable assigned to track and control the current state. It is important to define this as a state variable because we’ll be triggering on it.

 

(2) Next, we still have condition and body programs as before. But each body program will now have exactly one condition: a trigger that the state variable’s value equals that state’s number.

 

“You idiot! You just got finished above telling me how important it is not to have any conditions on the body programs.†But that’s the old way. Now we are triggering the body programs when the state variable changes value, and that is exactly the behavior we get from the condition. So when the variable value changes, the correct state is triggered and run, and the other states’ body programs go false and stop. Neat, huh?

 

(3) Lastly, transitions between states now are accomplished simply by assigning a new state number to the state variable. We then let the ISY triggering evaluation take over and do our heavy lifting. No more endless Run Program rubbish!

 

Here’s an example of the “KPL combination lock†programmed this way. It's a good example because it is simple but still illuminates the state method, as well as some other nice techniques using variables.

 

If you haven’t run into this example before, it requires the four auxiliary buttons on a 6-button KeypadLinc to be pressed in some predefined sequence. Here we will use A.. B.. C.. D as the sequence and 20 seconds for the timeout. There is also a 20 second timeout if no button gets pressed to continue to sequence.

 

If the correct sequence is entered then a light is flashed and the programs then reset. The wrong sequence or a timeout will just reset with no light flash.

 

Before we start, there are a few prerequisites:

 

(1) You need a recent ISY firmware version 3 that has variables.

 

(2) You have to have a KPL with the four buttons available.

 

(3) You have to associate each of the four KPL A-D buttons with a scene — this is an ISY/Insteon requirement to be able to control them independently.

 

(4) We need to define a state variable to control and track the current state: I’ll be using the highly creative name: “$Test.State†here. Again, is crucial to use a state variable; we need changes in the value to trigger ISY events.

 

(5) We're also using a second ISY state variable: $Test.Code. This is for tracking the code sequence as buttons are pressed. Since ISY variables are only integers, button A corresponds to 1, B to 2, C to 3 and D to 4. So if A... B... C have been pressed so far, $Test.Code will be set to 123.

 

To do I’m employing an old programmer’s trick. We multiply $Test.Code by 10 and then add in the next button's value (1..4) so that each digit of $Test.Code is always tracking the sequence entered. So if we end up with 1234 that means success. Any other 4-digit value means failure. Again, it is important to define $Test.Code as a state variable because we’re triggering on its changes.

 

We also need to create a non-state (integer) variable: $Test.Temp. This is just used to build the value we want to assign to $Test.Code before actually performing the assignment in Test.CA, etc. More on that below.

 

There are just three states in our state machine and they’re very easy to diagram:

 

State 0: The idle state waiting for the first button press, which when received jumps to state 1.

 

State 1: waiting for more button presses and will timeout after 20 seconds back to state 0 if the correct code hasn't been entered by then.

 

State 2: the success state from state 1 and flashes the light before returning to state 0.

 

In addition there are four other little programs, all similar, one for each button press A, B, C and D. They clear the button LED and update $Test.Code for the button's value. These are not state programs, just little routines to update $Test.Code.

 

First, the three body programs for states 0, 1, 2. Again, each body program always has one condition ("If state is value) that triggers when that state value is set. This format is required:

 

//TEST.B0 (initial state, waiting for the first button press)

If $Test.State is 0
Then
       $Test.Code  = 0
       Set Scene 'Scene: USO A' Off
       Set Scene 'Scene: USO B' Off
       Set Scene 'Scene: USO C' Off
       Set Scene 'Scene: USO D' Off

//TEST.B1 (waiting for more presses, or times out back to state 0 after 20 seconds)

If $Test.State is 1
Then
       Wait  20 seconds
       $Test.Code  = 0
       $Test.State  = 0

//TEST.B2 (success - see TEST.C0, flash the light and return to state 0)

If $Test.State is 2
Then
       Set 'US Office: Cans 6' On
       Wait  3 seconds
       Set 'US Office: Cans 6' Off
       $Test.Code  = 0
       $Test.State  = 0

 

Here are the corresponding condition programs that make the explicit transitions into each state. In addition to these, each state's body program can make a transition when exiting to another state.

 

//TEST.C0 (go back to initial state if wrong code entered while in state 1)

If $Test.State is 1
   And $Test.Code > 1000
   And $Test.Code is not 1234
Then
       $Test.Code  = 0
       $Test.State  = 0 

//TEST.C1.1 (Jump to state 1 on first button press)

If             $Test.State is 0
        And $Test.Code > 0
Then
       $Test.State  = 1

//TEST.C1.2 (Restart timer in state 1 on subsequent button presses)

If             $Test.State is 1
        And $Test.Code < 1000
Then
        Run Program 'Test.B1' (Then Path)

//TEST.C2 (Go to state 2 when buttons pressed in the right sequence)

If $Test.State is 1
   And $Test.Code is 1234
Then
       $Test.State  = 2

 

Finally, the routines that update $Test.Code on each different button press and then clear the button LED.

 

//TEST.CA

If Control 'US Office: Cans 6 / US Office: Cans A' is switched On
Then
       Set Scene 'Scene: USO A' Off
       $Test.Temp = $Test.Code
       $Test.Temp *= 10
       $Test.Temp += 1
       $Test.Code = $Test.Temp

//TEST.CB

If Control 'US Office: Cans 6 / US Office: Cans B' is switched On
Then
       Set Scene 'Scene: USO B' Off
       $Test.Temp = $Test.Code
       $Test.Temp *= 10
       $Test.Temp += 2
       $Test.Code = $Test.Temp

//TEST.CC

If Control 'US Office: Cans 6 / US Office: Cans C' is switched On
Then
       Set Scene 'Scene: USO C' Off
       $Test.Temp = $Test.Code
       $Test.Temp *= 10
       $Test.Temp += 3
       $Test.Code = $Test.Temp

//TEST.CD

If Control 'US Office: Cans 6 / US Office: Cans D' is switched On
Then
       Set Scene 'Scene: USO D' Off
       $Test.Temp = $Test.Code
       $Test.Temp *= 10
       $Test.Temp += 4
       $Test.Code = $Test.Temp

 

With this approach, it also is now simple to change the ‘secret code,’ as it is just a number in two places in the program set, instead of being wired into the logic. Simply change the two occurrences of 1234 to 1144 and now the press sequence is changed to AADD.

 

Test.C1 is split into two parts. This is because state variables trigger only when the assignment changes the value. We have to do a Run Program in the second one because the state value is already 1 and it isn't being changed. There are other ways to do this, but this seems the most straightforward.

 

The other note regards Test.CA/CB/CC/CD. Why is $Test.Temp needed to build and then assign the final value to $Test.Code? This takes some serious explanation.

 

So why not just assign these values directly to $Test.Code as with:

 

//TEST.CD (The WRONG way)

If Control 'US Office: Cans 6 / US Office: Cans D' is switched On
Then
       Set Scene 'Scene: USO D' Off
       $Test.Code *= 10
       $Test.Code += 4

 

In fact, I originally wrote these four programs exactly in this format, and spent quite a bit of time trying to figure out why they didn't work.

 

The problem is subtle. It is a result of the way the ISY handles state variable assignments. Each assignment creates a trigger event. The condition clauses of each 'triggerable' program are evaluated first. Then execution of any that match get queued up, in order for processing. In this case when the Test.CD program completes, freeing the ISY to process it's pending event/trigger queue.

 

But this leads to an unexpected behavior.

 

Consider the case of Test.CD getting a press of "D" as the final, successful button. Test.C0 gets queued up to execute it's Then part for the (wrong) 1230 value (resulting from the *= 10 assignment.) This execution happens even though by the time Test.C0 executes, the value of $Test.Code has already been set to the correct value of 1234. The condition on Test.C0 says not to execute if the value is 1234, but that test was done when the *= 10 took place.

 

And the second, correct, trigger on 1234 never gets the chance to fire off because Test.C0 "thinks" the wrong 4-button sequence was entered (1230) and so resets everything back to state 0 failure. Bummer.

 

It's worth noting you can accomplish the same thing without the temp variable, but including a small, one-second wait at the start of Test.C0. That permits the ISY to restart execution of Test.C0 with the pending queued-up correct value of 1234. But the delay is needless otherwise and the temp variable approach seems more appropriate.

 

There is some more discussion of this later on in this thread.

 

The bottom-line rule is: don't change the value of a state variable unless and until you want it to trigger on that value.

Posted

The <0> is apparently what happened to ">" when the forum system got hold of it. I'll try to edit the post.

 

The $test.state question is probably a good one. I'll have to look when I'm at the ISY console.

Posted

Thanks for catching that.

 

The .C1 and .C2 programs somehow disappeared in my copy/pastes to the forum.

 

I've edited the post to insert them and fix the ">". Hopefully that's it.

Posted

Nice. I've always enjoyed using state machine logic for complex programming. A few questions, though:

 

1) Why is the state variable initialized to -1 instead of 0, the steady state?

2) I don't understand what program Test2.C0 is intended to do.

3) Can you use a regular variable to hold the secret code so you only change it in a single place, not 3 places?

 

Randy

Posted

rhughes:

 

1) I think you could do a 0 init and run-at-startup, instead of the -1 init. I just started this attempt with that and left it that way.

 

2) I've fixed the test2.c0 which somehow got lines from one of the other programs - probably when I doing that copy-to-clipboard I just selected the wrong one. (Note to ISY: it would be really nice to have a copy-to-clipboard at the folder level.)

 

3) I tried several ways to boil the two occurrences of 1234 down to one. And also to try to make it a regular variable . I never found anything acceptable. The cures were all worse than the disease. If you come up with something I'd be interested to see it.

 

Part of the problem is there is no way to do variable-to-variable assigment, so that limits things you could otherwise do.

Posted

"Part of the problem is there is no way to do variable-to-variable assigment, so that limits things you could otherwise do."

 

Is this what you want to do with variable to variable

 

If

Control 'REMOTELINC-2 / REMOTELINC-2 - 3' is switched Off

Or Control 'REMOTELINC-2 / REMOTELINC-2 - 2' is switched Off

 

Then

$SVar1 = $IVar1

 

Else

- No Actions - (To add one, press 'Action')

Posted

Yes. That sort of thing.

 

About all you can do with regular (non-state) variables at the moment are very simple counting operations. I'm not trivializing that - it's useful, but anything much past that seems to need variable/variable assignment and compares.

Posted

ergodic

 

The following is an actual Program. Variables can be compared and assigned values from other variables.

 

If

Time is 5:54:00PM

And $IVar1 is $SVar1

 

Then

$SVar1 += $IVar1

 

Else

- No Actions - (To add one, press 'Action')

  • 7 months later...
Posted

How about $sSecretCode state variable with an init of 1234, then change your comparison to

 

//Test2.C0: (initial idle state, $Test.State init to -1)

 

If $Test.State 0> 1000

And $Test.Code is $sSecretCode

Then

$Test.State = 2

 

//Test2.C3: (failure state: just jump back to state 0)

 

If $Test.State is 1

And $Test.Code > 1000

And $Test.Code is not $sSecretCode

Then

$Test.State = 0

  • 8 months later...
Posted
Thanks for catching that.

 

The .C1 and .C2 programs somehow disappeared in my copy/pastes to the forum.

 

I've edited the post to insert them and fix the ">". Hopefully that's it.

 

ergodic - Great article. Thank you for taking the time to put it together. Did your corrections get lost? I don't see them in the original post.

Posted

It's a mess all right. No idea what's happened, it used to be OK - maybe the forum move had some impact.

 

I'm not sure I even have that code around anymore, so I'll need to look at this when I have some time and just post a new one from scratch. Sorry and thanks for pointing it out.

Posted

ergodic - Thanks for checking. It's still a useful post even without 100% coverage on the code. Maybe the edits will magically reappear someday and save you the trouble of rewriting it.

  • 2 weeks later...
Posted

I've re-worked and re-posted the original code.

 

In working it up again, I ran into what I think may be an ISY variable processing bug. I've noted it in the revised code posting.

 

This workaround I'm very sure wasn't necessary with the original ISY firmware when I first did it, so something's changed.

 

Maybe someone from UDI can shed some light on this problem. It's left me baffled, but I've spent as much time as I can to trying to figure it out. The workaround in the code I posted takes care of it in this case.

Posted

ergodic,

 

Thanks for taking the time to rework the post. It's a great reference and I have already used your approach to solve an otherwise messy problem.

Posted

Then
       Set Scene 'Scene: USO A' Off
       $Test.Temp = $Test.Code
       $Test.Temp *= 10
       $Test.Temp += 1
       $Test.Code = $Test.Temp

 

Hi ergodic,

 

The reason you need to use a temporary variable is that each calculation sends out a new event, but I think you are only concerned with the final value. We may be able to change this behavior slightly in the future, but there are no plans to do so at this point.

e.g. The following will send 2 events,  1) $Test.Code = 10   2) $Test.Code = 11

Then
      $Test.Code  = 10
      $Test.Code += 1

For a temporary variable, you may want to use an integer variable instead of state variable because it does not send out an event when its changed, and it does not start an iteration in the program test/run cycle.

Posted

I did use a non-state variable, though I should make that clear.

 

Do I understand correctly that ISY variable assignment processing is handled asynchronously and so can interrupt the program at the point of the assignment, or is this something different?

Posted

Do I understand correctly that ISY variable assignment processing is handled asynchronously and so can interrupt the program at the point of the assignment, or is this something different?

 

No, it will not interrupt the program at the point of the assignment. It works very much like a device status, such as the current state of a lampLinc.

Posted

Thanks.

 

So at what point is Test.C0's condition getting evaluated, relative to its body execution? I'm not quite getting it.

 

The condition basically says "and only execute this if $Test.Code is not 1234." But the body appears to gets executed anyway when $Test.Code IS 1234. I'm sure of that because I stuck an assignment of $Test.Code to a diagnostic variable at the beginning of the Test.C0, and sure enough: 1234.

 

Really just trying to wrap my head around why this happens, it's very counter-intuitive.

Posted

So at what point is Test.C0's condition getting evaluated, relative to its body execution? I'm not quite getting it.

 

If you had something like:

Then
   $Test.Code = 10
   $Test.Code = 20

This would put two events on the queue, therefore the first event would be evaluated (i.e. $Test.Code = 10), programs would run etc. based on program conditions. After that, the 2nd event would be evaluated (i.e. $Test.Code = 20), programs would run etc. based on program conditions.

 

Basically, we test the conditions of all programs then we do the actions for the programs. This is sequential, and thus, they do not interrupt each other.

Posted

Thanks. I'll modify the commentary on the original post to explain this.

 

I don't usually like retroactive edits like this, but in this case it seems

 

Since this is the defined behavior, I'd like to renew my suggestion for some kind of mini-wait (sub-second wait or that just puts current execution at the end of the queue with no other delay.)

 

It is a much simpler way to solve this kind of thing.

Guest
This topic is now closed to further replies.

×
×
  • Create New...