I'd rather have the Or... option that I added later:
But even better still - and would certainly break *everyone's* rig, so it probably won't happen - would be to have a Ladder Logic editor like most industrial controls platforms have, or at least something that covers all of the same functionality.
Ladder Logic started as a literal, physical relay schematic, with electrical power split into two rails, and each "rung" on the "ladder" is its own independent circuit, with switches and relay contacts on the left and relay coils on the right. Each coil controls its own set of contacts, and from that you can build pretty much any logic you want. When you walk into a plant that still uses this old control method and open the cabinet, it sounds like popcorn whenever something happens!
A classic ladder rung, used to control something with a separate button for ON and OFF, that will stay in either state until you push the other button:
Code:
| |
|------NOT [OFF]------+------[ON]------+------(Run)------|
| | | |
| +------[Run]-----+ |
| |
For a computer program, it doesn't matter whether the OFF button comes first or the branch does, but in the old electrical version, it does matter, because the branch isn't even powered at all when you push the OFF button. Could make a small difference for troubleshooting or for electrical safety.
Now with software, each relay - really each unique name - has become a bit in a computer, and we're no longer limited to a finite number of contacts per relay. And we can modify the behavior so that it behaves *mostly* like the electrical circuit that it once was, but not quite, in ways that are plumb easy for an interpreter to make sense of.
I especially like the way that Allen-Bradley / Rockwell Automation does it. Each expression starts on the left side with a default of "true", and as you follow an imaginary dot along the line to the right, it gets modified by each block that it encounters:
- When it hits a block, its value is AND'ed with the value of that block. Almost equivalent to a Condition in Adv. SS, and of course each block has both a "straight" version and an "inverse" or NOT version.
- When it hits a branch, the value at that moment is copied to both/all branches.
- When branches combine, their values are OR'ed to continue across the screen.
- When the single, possibly recombined (OR'ed) dot reaches the end, its value is the result of the expression.
- Important for this discussion, a one-shot is simply a block like any other, that can go anywhere, not necessarily at the end and not necessarily in every branch. It could easily be all of the above or any subset. Its value to AND with the incoming value, is simply NOT the previous incoming value.
Thus, it becomes obvious what happens if you put a "NOT Startup" block after a one-shot, and then branch around both with something else, just for one example.
With the present list of conditions, you kinda have to "just know" that you effectively have all of your open parentheses up front (remember math class?), and you close one with every condition. It's the easiest way to write an interpreter of that kind, and I've done it too, but the existence of others means that a newbie can't know for sure. And it's still ambiguous what the "only on change" one-shot actually does in detail.
Not so with Ladder. The graphical nature makes it obvious what's actually happening...until you start doing something complicated, at which point you probably need to split it anyway.
A-B / RA even allows intermediate output blocks, which would radically change the behavior of a physical circuit, but in their version, it continues to work as above, and simply takes the value at whatever point the output happens to be, and sends it somewhere else while also passing it down the line unchanged. This might not be necessary in Adv. SS (yet?), but I thought it was worth mentioning.
A-B / RA is also interpreted sequentially, not simultaneously, so that an intermediate output on a higher branch (visually) can affect what happens on a lower branch, or towards the right, on the *same* scan, if another block in either of those positions happens to look at the same bit in the computer. This seems like a trivial way for an interpreter to do it, and has some...interesting ramifications for those that want to do something detailed.
---
What I'm thinking for Adv. SS, at least to start with, if it even happens at all, is to have a sequential interpreter that behaves as above, with a single fixed output on the far right side. If this output is true, the macro runs on this scan. If false, it doesn't.
Some essential blocks:
- [Always false], used to disable a branch or the whole thing. Considered bad practice for production (just delete that branch or macro), but useful for troubleshooting. To force something true, just branch around it with nothing in that branch, which is also considered bad practice for production, but useful for troubleshooting.
- [One-shot], as above. Its own value to AND with the incoming value, is NOT the previous incoming value.
- [This macro running], used to prevent parallel copies of the same macro, usually while it runs a Wait action.
- [Startup], true on the first scan, and never again. The existence of this one might affect the decision of which default value the [One-shot] should have.
The default logic for a new macro is not empty - which would be "always true": there must always be a complete "circuit", and the start is always true - but:
Code:
| |
|------[Always false]------[One-shot]------NOT [This macro running]------(Run)--|
| |
Add things in between and delete them as needed. When you're ready to run, delete the [Always false] if you haven't already.
From the descriptions above, I'm sure you can guess what this default does in a variety of different cases, if you replace any of them with anything...except the (Run) at the end; that's permanent.
Pausing a macro might make its starting value "false", which because it's AND'ed all the way across, never replaced, makes the Run output always false as well. But the conditions still run.
---
And once you've got *that* idea, then we can move the Actions up into the rungs as well, as their own output blocks, and remove the one fixed (Run) block: the rung value is always passed through an Action unchanged, and if it's "true" at that point, the action runs.
The Wait action doesn't really make sense anymore with that structure, but there are always ways to get that functionality back, using Timers.
At this point, Timers become their own Actions or output blocks, not part of any Condition or input, and their state can be read by an input block...
So this:
Code:
Do A
Wait 5
Do B
Wait 3
Do C
might become this:
Code:
| |
|------+------[Conditions...]------[One-shot]------[Do A]------+------[Timer A, set to 5]------|
| | | |
| +--------------------------------[Timer A Running]------+ |
| |
| |
|------+------[Timer A Done]-------[One-shot]------[Do B]------+------[Timer B, set to 3]------|
| | | |
| +--------------------------------[Timer B Running]------+ |
| |
| |
|---------------------------------------[Timer B Done]-------[One-shot]------[Do C]------------|
| |
or this:
Code:
| |
|------+------[Conditions...]------[One-shot]------+------[Timer, set to <enough>]------|
| | | |
| +----------------------[Timer Running]------+ |
| |
| |
|---------------------------------[Timer Value > 0]---------[One-shot]------[Do A]------|
| |
| |
|---------------------------------[Timer Value > 5]---------[One-shot]------[Do B]------|
| |
| |
|---------------------------------[Timer Value > 5+3]-------[One-shot]------[Do C]------|
| |
Assuming that:
- The Timer is enabled by a true input value, and both disabled and reset by a false input value.
- [Timer Running] is only true when the Timer is enabled but not expired yet.
- [Timer Done] is only true when the Timer is both enabled and expired.