Looping a script while tag is on

Hi,
fairly new to python coding but have experience elsewhere. I'm trying to make a simple script that will loop an audio file while a certain tag is on. In other programming languages I would just use a jump and a lable to continue the loop and use if / else statements but I havent figured out jumps and labels in python yet

I have a custom binding on the page directed at the tag I am looking to use

def transform(self, value, quality, timestamp):
while value == True:
self.getChild("Audio20").props.play = True
import time
time.sleep(30)
else:
self.getChild("Audio20").props.play = False

return value

The problem I am having is that when the tag turns on the sound plays, waits and plays again. but when the tag shuts off the sound continues to play every 30 seconds and it never shuts off.

I also tried to just use the self.props.loop on the audio file but I cant figure out how to put a delay in that. that would work too. If someone could explain that I might learn from that either way,

Also,
can I make a variable for the entire page so if i use this script 30 times for different sounds I can change the wait time for all of them at the same time?

say x=30 and change the 30 in time.sleep(30) to time.sleep(x)? Where would I create that variable at?

Welcome to the forum, Jason.

Tips:
Have a look at Wiki - how to post code on this forum. It will then render as (but I'm guessing your indentation):

def transform(self, value, quality, timestamp):
    while value == True:
        self.getChild("Audio20").props.play = True
        import time
        time.sleep(30)
    else:
        self.getChild("Audio20").props.play = False
    return value
  • Pretty much any time you are using sleep() in a script you are doing something wrong.
  • You have import time inside a while loop. It should be outside so it only happens once (but you shouldn't be using sleep() anyway).
  • You have an else statement with no if. (See @pascal.fragnoud's comment below.)
  • while value == True: can be reduced to while value:.

General principles for Ignition scripting:

  1. Avoid while loops, they're almost never the answer.
  2. Seriously, don't use sleep().
  3. Avoid side effects in a transform. Transforms should only ever affect the value they are given, modify it in place, and return a new value.
  4. Don't do anything slow in a transform, like calling out over the network or interacting with a database or, y'know, explicitly sleeping for thirty seconds.

Even more broad advice: Don't use scripting if you don't have to.

In this specific case, you don't need any scripting at all.
If the loop property of the audio component is set to true, all you have to do is bind the play property to your tag:

That's literally it. Perspective will automatically set up the subscription infrastructure, so as soon as the tag value changes, the binding will re-evaluate and the property will change. This is fundamental to how Perspective (and Vision) work and is extremely important to learn.

If you actually need the behavior of "play the sound, looping as many times as possible whenever the tag goes true, but stop playing after thirty seconds", then you'll want an approach like @Transistor suggested above. But I'd definitely recommend simplifying the requirement.

5 Likes

Python accepts else after loops. It's a weird construct that you don't see much, but it does exist.
Basically, it runs the except if you didn't break out of the loop.

3 Likes

Thanks for the info but this was what I had mentioned in my comment. If I want to use the loop tag and add a 30 second delay between loops how would I do that?

I want it to play a 2 second sound clip, wait x seconds, then if the tag is still true play again.

I COULD add 30 seconds to the sound clip so its 2 seconds of sound, 30 seconds of silence and bind the loop to the tag but then i cant have control over the silence. I want to adjust the time between 2 second payments.

  1. Add a custom property (let's call it condition) to the sound component.
  2. Bind condition to your condition tag.
  3. Add a change script to this custom property. This change script writes the current time to an additional custom property, timestamp, using system.date.now().
  4. Bind the play property using an expression:
{this.custom.condition} && secondsBetween({this.custom.timestamp}, now(1000)) % 30 <= 2

The now(1000) defines a poll rate for the expression, so it will automatically re-evaluate every second, check the condition(s), and return a result. No while loops needed.

The 30 and 2 values in the expression can be bound to any other arbitrary properties.

Component JSON

If you copy this to the clipboard you should be able to paste it in to a view as a self contained sample component:

[
  {
    "type": "ia.display.audio",
    "version": 0,
    "props": {},
    "meta": {
      "name": "Audio"
    },
    "position": {
      "x": 317,
      "y": 101,
      "width": 300,
      "height": 55
    },
    "custom": {
      "timestamp": {
        "$": [
          "ts",
          192,
          1721321870088
        ],
        "$ts": 1721321870065
      }
    },
    "propConfig": {
      "custom.condition": {
        "binding": {
          "type": "tag",
          "config": {
            "mode": "direct",
            "tagPath": "[default]trigger",
            "fallbackDelay": 2.5,
            "publishInitial": false
          }
        },
        "onChange": {
          "script": "\tself.custom.timestamp = currentValue.timestamp",
          "enabled": null
        }
      },
      "props.play": {
        "binding": {
          "type": "expr",
          "config": {
            "expression": "{this.custom.condition} && secondsBetween({this.custom.timestamp}, now(1000)) % 30 <= 2"
          }
        }
      }
    }
  }
]
5 Likes

wow, I have so much to learn. I don't understand half of that.

In another language I use I would simply do this
*top
if tag(1)=true
play sound clip #1
timer (x)
jump *top
else
exit

and as soon as tag(10=true it would play, wait x seconds, and then check again....

thanks for the feedback / help. I will dig into what you posted and try to see what i can learn from that.

It's mind boggling to me that adding a 30 (or x) second delay between playing a sound could require this much code.

That is an example of sequential programming, with a task that doesn't need to do anything else at the same time.

Ignition has to do many thousands of things in any short interval. Sequential program design is an utter disaster for such systems.

Ignition is therefore almost entirely event driven, and events are expected to "do their thing" as quickly as possible and return, letting the next event do its thing in turn. Anything involving delays, in an event driven architecture, are about scheduling some event, or regularly checking for the end of the delay.

If you have experience with PLC real time tasks, they work much like the latter--every scan runs really quickly, and any timer instructions check every scan whether their time is up.

9 Likes

correct. Usually that sub program is running and loops constantly so its always running in the background.

I should have added the else = false would also jumped back to *top and it would continually loop waiting for the tag to change.

essentially ignition has to be doing the same thing waiting for a PLC tag (input) to change right? wouldn't that be the same as waiting for an external input to turn on or off in a PLC?

Sorry... I know I have a lot of learning to do it just blows my mind on the complexity of what I would consider a simple task.

I still havent been able to get this to work and I feel like I've wasted half a day on it.

No, not at all. Event driven systems have loops waiting for any event (of a certain kind), and hand off to the code that is set to handle it. PLC drivers schedule polling events to regularly read live data, and pass the results to Ignition's tag system. That tag system checks new value against prior value and injects appropriate new events for those into the correct loop's queue. Tag bindings in Ignition are subscriptions to such tag events.

Simple loops that repeated check one condition are simply not acceptable in realtime or near-realtime systems.

If you used your sample code in a real PLC, it would fault, because unlimited looping ties up the task in which it occurs.

Paul gave you the answer. No script.

2 Likes

You could use the SFC module for tihs. :man_shrugging:

No he didnt give me the answer. I did ask how to use the loop property to make this work (because I understand that is what it is for) but with the looping property tag on it constantly loops. It becomes a 2 second loop I need x seconds in between loops.

that's the whole point of why i was using a time.sleep function which everyone is saying is bad but I still don't understand what else to use as a wait.

I do understand the last part about binding loop to play. I had originally just bound them both to the tag that turned on and off and it worked fine except it looped too quickly.

The issue is it constantly loops. there is no option to add a variable delay inbetween when it plays. thats the reason i was trying to use a script.

I play a 2 second sound clip "assistance needed station 10" and then i need 30,60,or 90 seconds before it plays again.

If there was an option "add x seconds between loops" I would have used that.

The example I gave you above does what you're asking for, but in an event driven way.

Instead of one single script controlling everything (which means locking up an entire OS thread, busy-waiting for 30 seconds - hopefully you can see how that won't scale as you have more and more sessions running and more and more audio clips potentially playing), you're breaking down the control into smaller pieces of logic, and the only scripts and expressions involved will be event driven and complete in milliseconds.

I literally posted an example you can copy and paste directly into your project, but here's some more detail. There's exactly 3 things involved - one tag binding, one expression binding, and one property change script. All of them are basic, fundamental tools you'll have to become comfortable with in Perspective development.

In step 2, I'm just using a simple direct tag binding to my test boolean trigger tag:

In step 3, I'm right clicking the condition property and selecting 'Add Change Script':


The script I'm adding is just this: Changing the value of the timestamp custom property to the new value that was just written to the tag. This should be ~identical to the current time, which you could also retrieve via system.date.now() - they're equivalent.

def valueChanged(self, previousValue, currentValue, origin, missedEvents):
	self.custom.timestamp = currentValue.timestamp

In step 4, I'm adding that expression, the most complicated piece of this whole thing. Expressions are, by definition, simple units that unconditionally return some value. In this case, we're only ever going to return true or false, because we're using this expression to control the play property.

{this.custom.condition} 
&& secondsBetween({this.custom.timestamp}, now(1000)) % 15 <= 2

There's two elements to the expression
{this.custom.condition} is just a reference to the existing custom property. This, combined with the subsequent && operator, combines to a boolean logical AND operation - thus our expression will always evaluate to true or false.

secondsBetween({this.custom.timestamp}, now(1000)) % 30 <= 2
Walking this from the innermost piece out, you've got:

  1. now(1000). This just means the whole expression will poll at the given rate. This is the most "magic" piece of this operation, and generally falls into the "you just have to learn this" category. See also: now | Ignition User Manual
  2. secondsBetween({this.custom.timestamp}, now(1000)). As the name implies, it's just giving you the integer seconds between two timestamps. This delta forms the basis of the following logic.
  3. % 30 is using the modulo operator to give you the remainder of a division operation. So 1 mod 30 = 1, 30 mod 30 == 0, 31 mod 30 == 1, 45 mod 30 == 15, and so on. The end result is a constant sawtooth ramp - we go from 0 to 30, then drop back to 0.
  4. <= 2 then, check whether that modulo result is less than or equal to 2. Thus, for 2 seconds out of every 30, the second half of the expression will be true.

Here's the same logic, except with a 15 second modulo to make the recording a little shorter, and with all the intermediate logic broken out in separate custom properties:

7 Likes

Thank you so much for the detailed reply. I think I was frustrated before and haven't had a chance to get back to this until today. When you break it down into small parts it makes much more sense.

Also I didnt know that sleep put the whole thing to sleep for the time. I understand why not to use it now.

I was able to get the looping to work but it loops immediately with no wait in between. I tried adjust the different variables in the expression binded on play with no luck.

And since we are on the topic of the time inbetween loops lets assume I have a page called sound with 14 different sound components each playing a different audio file on it and depending on the tag that is tripped one of the audio files will play. Can I have a global variable for the entire page that I can change and it will change the delay for all 14 audio files so I dont need to add it to each sound component?

Furthermore on this topic - Is there a way I can make sure the audio files don't play over each other if they are tripped at the same time?

Make a custom prop at the root and then replace the hardcoded delay in each expression with a reference to that prop

{this.custom.condition} 
&& secondsBetween({this.custom.timestamp}, now(1000)) % {root.custom.global_delay} <= 2

Actually, Paul mentioned it in his original reply:

You might be able to have just one audio component with a bound source as well, but I don't know if that would introduce a significant buffering delay when swapping sources.

Thanks Felipe,

after my post I reread Paul's post and saw the part of the variable. I got that to work. Still not getting the looping to work with the delay but I'm still working on it.

Not easily.

1 Like

Late to the party, but I wouldn't be adding 14 of these audio components, I would be adding one, and adding a binding for the source audio file path based on your conditions. Then by design, you will only ever play a single audio file

6 Likes