Count machine runtime in PLC - plc

I am working on a simple machine project in PLC using structured text programming language. Now, I want to count the runtime (hours, mins and seconds) of a machine after Start Command is pressed. As I am new, I struggle to develop an idea. I tried to do it by using 3 counter for each seconds, minutes and hours. But could not achieve solid result. Can you please share the idea how to count runtime hours of a machine on PLC (ST or FB) ?

There are two issues here:
Correctly handling delta time
Using data types to your advantage
Since you want the time since the Start Command, you need to first store the time when the start command is pressed. Then you calculate the delta time (current time - start time) to obtain the seconds elapsed. Then take the seconds elapsed and get your hours, minutes, seconds. You can even simply use a TON FUNCTION_BLOCK for this - the ET output is in the TIME data type.
There are a number of good built-in data times for date and time. TOD (or TIME_OF_DAY) automatically is in the HH:MM:SS.mmm format for you. If your max elapsed time is less than 24 hours, you can use this format. If it could be longer, then use the TIME will go to 49 and some days, otherwise DT (DATE_AND_TIME) will count until the year 2106. You will get YYYY-MM-DD-HH:MM:SS in that format.
The following is a TON trick you can use, assuming 49 days or less is true.
PROGRAM PLC_PRG
VAR
Running : BOOL := FALSE;
Start : BOOL := FALSE;
MyTimer : TON;
Elapsed : TOD;
MyEt : TIME;
END_VAR
//calculate Elapsed time only if running
IF Running THEN
//ET is in milliseconds, so can easily convert to TOD
Elapsed := TIME_TO_TOD(MyEt);
ELSE
Elapsed := TOD#0:0:0.000;
END_IF
//run a TON timer, but set the PT to the absolute limit
//this trick is only valid for 49 days or less!
MyTimer(IN:=Running,PT:=DWORD_TO_TIME(16#FFFFFFFF),ET=>MyEt);
//only start once
IF Start AND NOT Running THEN
Running := TRUE;
Start := FALSE;
END_IF
If you are adverse to the trick, you can also do something like below (requires the SysTime library). In your online debugger, you will see Elapsed in HH:MM:SS format. Of course you should create a FUNCTION_BLOCK for this.
PROGRAM PLC_PRG
VAR
Running : BOOL := FALSE;
Start : BOOL := FALSE;
Now : ULINT;
StartTime : ULINT;
Elapsed : TOD;
pResult: SysTimeRtc.RTS_IEC_RESULT;
END_VAR
//get current time (result is in seconds)
Now := DWORD_TO_ULINT(SysTimeRtcGet(pResult));
//calculate Elapsed time only if running
IF Running THEN
//TOD is in milliseconds, so we x1000
Elapsed := ULINT_TO_TOD((Now - StartTime) * 1000);
ELSE
Elapsed := TOD#0:0:0.000;
END_IF
//only start once
IF Start AND NOT Running THEN
StartTime := Now;
Running := TRUE;
Start := FALSE;
END_IF

Related

Time and number counter in one?

I want to check how long a motor is working since the last check up and how many times the motor startet up since then.
#xSekundenzaehler := "Clock_1Hz";
REGION Allgemein
#Betriebszeit_Anlage(CU:="eHauptschuetz" AND #xSekundenzaehler,
R:="Lebensdauer".Allgemein.xResetBetriebszeit,
PV:=0,
CV=>#rSecondsAnlage);
"Lebensdauer".Allgemein.rLaufzeitAnlage := #rSecondsAnlage / 3600;
END_REGION
This is the code i used. Do i have to use another "CTU" again or is there a smarter way to do this in the already existing one that i don't know of?
Usually storing important values on CTU is not recommendable because you have no control on whether TiaPortal may restore the CTU instance.
It is preferable to build a counter by yourself so its value is kept on a non volatile db var:
"R_TRIG_1Hz"(CLK := "Clock_1Hz");
// Condition to count up running time
IF "eHauptschuetz" THEN
// Only at rising edges of seconds
IF "R_TRIG_1Hz".Q THEN
"DBZeit".LebensdauerSeconds := "DBZeit".LebensdauerSeconds + 1;
END_IF;
END_IF;
// Reset prevails over increment
IF #xReset THEN
#xReset := false;
"DBZeit".LebensdauerSeconds := 0;
END_IF;
// Hour total
"DBZeit".LebensdauerHours := "DBZeit".LebensdauerSeconds / 3600;
// Hour total with a decimal place
"DBZeit".LebensdauerDeciHours := "DBZeit".LebensdauerSeconds / 360;
This is the definition of the DB
Please note that only seconds are remanent, as hours and decihours are calculated on every cycle.
It would be better to store hours and decihours in other DB, so this one is dedicated to remanent values. When you modify the DB variables their values are reset to the start values.
Please remember to make a snapshot and copy them to start values prior to any changes.

How to calculate the next minute and next 5 minute intevals given a ZonedDateTime

I have a instance of a ZonedDatetime.
ZonedDateTime.now(ZoneId.of("America/New_York"))
I basically need a function that will take an instance of a ZonedDateTime and return the next 1 minute and 5 minute values.
So if the current time is:
2021-10-24T19:46:10.649817
The next minute will be 19:47:00 and the next 5 minute will be 19:50:00
The next 5 minute interval is always like:
1:00
1:05
1:10
1:15
1:20
1:25
...
1:50
1:55
2:00
i.e. the next 5 minute interval is not based on exactly 5 minutes from now, but rather the next 5 minutes based on starting from the beginning of the hour. Same goes for the next 1 minute interval in the future.
def nextIntervals(zdt: ZonedDateTime): (ZonedDateTime, ZonedDateTime) = {
???
}
It is fairly simple to do so without hardcoding the values. Unfortunately I'm not familiar with scala so I'll give you some pseudo code, I believe you'll be able to easily translate it.
nextIntervals(zdt) {
timestamp = zdt.toUnixTimestamp();
return [
new ZonedDateTime(timestamp + (60 - timestamp % 60)),
new ZonedDateTime(timestamp + (300 - timestamp % 300))
]
}
The above code assumes that ZonedDateTime can be instantiated by giving it a unix timestamp, measured in seconds. And also that it can be converted to a unix timestamp.
The idea is pretty simple: the remainder of the modulus will be the time that has elapsed since the last required period (in your case 1 minute or 5 minutes). Take that away from the period itself and you have the time that's left until the next period. Add that to the current time and you have the exact datetime.
Edit:
Here's a working javascript example
function nextIntervals(date) {
let t = date.getTime();
return [
60e3,
300e3,
].map(i => new Date(t + i - t % i));
}
console.log(nextIntervals(new Date));
You can use the following functions to meet your requirements:
ZonedDateTime#plusMinutes
ZonedDateTime#minusMinutes
ZonedDateTime#truncatedTo
Demo:
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
public class Main {
public static void main(String[] args) {
ZonedDateTime now = ZonedDateTime.now(ZoneId.of("America/New_York"));
ZonedDateTime nextMinute = now.plusMinutes(1).truncatedTo(ChronoUnit.MINUTES);
ZonedDateTime nextMultipleOfFiveMin = now.truncatedTo(ChronoUnit.MINUTES)
.minusMinutes(now.getMinute() % 5)
.plusMinutes(5);
System.out.println(now);
System.out.println(nextMinute);
System.out.println(nextMultipleOfFiveMin);
}
}
Output from a sample run:
2021-10-25T16:59:22.662943-04:00[America/New_York]
2021-10-25T17:00-04:00[America/New_York]
2021-10-25T17:00-04:00[America/New_York]
Output from another sample run after a while:
2021-10-25T17:05:09.596952-04:00[America/New_York]
2021-10-25T17:06-04:00[America/New_York]
2021-10-25T17:10-04:00[America/New_York]
ONLINE DEMO
Learn more about the modern Date-Time API from Trail: Date Time. Check this answer and this answer to learn how to use java.time API with JDBC.
Note: The java.util Date-Time API and their formatting API, SimpleDateFormat are outdated and error-prone. It is recommended to stop using them completely and switch to the modern Date-Time API*.
* If you are working for an Android project and your Android API level is still not compliant with Java-8, check Java 8+ APIs available through desugaring. Note that Android 8.0 Oreo already provides support for java.time.
We do need a little bit of hand-coded math to handle the 5-minute interval case. Excuse my Java syntax.
ZonedDateTime now = ZonedDateTime.now(ZoneId.of("America/New_York"));
System.out.println("Now: " + now);
// Truncate to the previous 5 minutes
ZonedDateTime zdt = now.truncatedTo(ChronoUnit.MINUTES);
zdt = zdt.withMinute(zdt.getMinute() / 5 * 5);
for (int i = 0; i <= 12; i++) {
zdt = zdt.plusMinutes(5);
System.out.println(zdt);
}
Example output:
Now: 2021-10-25T15:23:31.357567-04:00[America/New_York]
2021-10-25T15:25-04:00[America/New_York]
2021-10-25T15:30-04:00[America/New_York]
2021-10-25T15:35-04:00[America/New_York]
2021-10-25T15:40-04:00[America/New_York]
2021-10-25T15:45-04:00[America/New_York]
2021-10-25T15:50-04:00[America/New_York]
2021-10-25T15:55-04:00[America/New_York]
2021-10-25T16:00-04:00[America/New_York]
2021-10-25T16:05-04:00[America/New_York]
2021-10-25T16:10-04:00[America/New_York]
2021-10-25T16:15-04:00[America/New_York]
2021-10-25T16:20-04:00[America/New_York]
2021-10-25T16:25-04:00[America/New_York]
The trick to truncate to a whole multiple of 5 minutes is to divide by 5, obtain a whole number and discard any remainder, and multiply by 5 again.
The 1-minute interval is similar, only a bit simpler: we don’t need to do any math ourselves, java.time takes care of it all.

How to get a date in yyyy-MM-dd'T'HH: mm: ss.SSSZ format

I want to get the current time, and format it in the following way:
yyyy-MM-dd'T'HH:mm:ss.SSSZ
(where SSS is the milliseconds and Z the time zone)
the code so far I have it as follows:
formatted_date() ->
{{Y,M,D},{H,Min,S}} = erlang:localtime(),
{Ms, _, _} = os:timestamp(),
{Days, {Hours, _,_}} = calendar:time_difference(erlang:universaltime(), erlang:localtime()),
Difference = 24*Days + Hours,
Tz = [case Difference < 0 of
true ->
$-;
false ->
$+
end | io_lib:format("~2..0B00",[Difference])],
io_lib:format("[~4..0B-~2..0B-~2..0BT~2..0B:~2..0B:~2..0B.~3..0B ~5.s]",
[Y, M, D, H, Min, S, Ms, Tz]).
The fact is that it always returns the same Ms, therefore, I think that I am not doing it well, and in other questions I only see how to obtain the total time in milliseconds, but not how to format it in this way.
Thank you.
The easiest way is to use the library function calendar:system_time_to_rfc3339/2 - it seems to fulfil all your requirements.
> calendar:system_time_to_rfc3339(os:system_time(millisecond), [{unit, millisecond}]).
"2021-03-03T18:42:08.497+05:30"
This function was added in Erlang/OTP 21.0, which was released in June 2018.
The reason your code always gets the same value for Ms is that the first value in the tuple returned by os:timestamp/0 is megaseconds, not milliseconds; the three values are megaseconds, seconds and microseconds. This comes from a time when Erlang did not support large integers, so splitting up the timestamp was necessary. These days you can just call os:system_time/1 or a number of other functions, depending on what kind of time you need, and get the result in a single integer.

Pine scripting: how to find the price X days ago

In Pine Script, how do I find the price based on a certain number of days ago? I've tried something like this...
// Find the price 90 days ago
target = time - 90 * 60 * 60 * 24 * 1000
valuewhen(time < target, close, 1)
...however time < target never seems to return true – presumably because the current bar's time cannot also be in the past at the same time. Perhaps valuewhen() wasn't designed to be used with dynamic values that change on every bar?
Do I need to use a loop instead, and scan through every past bar until I find the date I'm looking for?
Perhaps there's a better way, but the workaround I'm using currently using is a function with a for loop, scanning backwards until the appropriate date is found. Here is my function:
priceXDaysAgo(numDays) =>
targetTimestamp = time - numDays*60*60*24*1000
// Declare a result variable with a "void" value
float result = if false
1
// We'll scan backwards through the preceding bars to find the first bar
// earlier than X days ago (it might be a little greater than X days if
// there was a break in trading: weekend, public holiday, etc.)
for i = 1 to 1000
if time[i] < targetTimestamp
result := close[i]
break
result
You can then call the function anywhere in your script:
priceXDaysAgo(90)

Change system time of Beckhoff controller by programming

For the last couple of hours, I am facing problems with changing system date and time by structured-text programming. I have used function block FB_LocalSystemTime where I can read the system time. But I could not find any function or function block to write new system time. I have checked NT_SetLocalTime, that also did not work. Do you have any idea how can I do that?
For further information: I have included the sample code like:
/**
Declaration Part
**/
fbLocalSystemTime:FB_localSystemTime;
fbSetLocalTime:NT_SetLocalTime;
newTime:TIMESTRUCT:=(wHour:=5);
/**
DEFINITION PART
**/
fbLocalSystemTime(); /*This gives system time */
fbSetLocalTime.TIMESTR:=newTimne; /* New time to set */
fbSetLocalTime.START:=TRUE;
fbSetLocalTime(); /** This does NOT set the system time which I guess should set **/
I understand the question, but unfortunately I don't have much experience with beckhoff plcs. Have you tried calling their support line? This should be a non application specific question that should be easy for them to help you with.
You may consider using FB_LocalSystemTime in the way indicated below. This will synchronize the local PLC time with the system with the given AMS ID passed to the parameter sNetID. If you do not pass sNetID parameter local OS system will be used as reference for setting the local PLC time. The time will be synchronized on rising edge of the signal bEnable and then in the interval given by the parameter dwCycle
VAR
{attribute 'hide'}
LocalSysTime : FB_LocalSystemTime;
SynchNodeAmsId : STRING := '10.10.10.1.1.1';
END_VAR
LocalSysTime(
sNetID:= SynchNodeAmsId,
bEnable:= TRUE,
dwCycle:= 60,
dwOpt:= ,
tTimeout:= ,
bValid=> ,
systemTime=> ,
tzID=> );
You are correct. You should be using NT_SetLocalTime.
If you open the function block fbSetLocalTime(), you will realize your function block returns an error with error ID 1862.
The definition of the errors can be found here: https://infosys.beckhoff.com/english.php?content=../content/1033/tcplclib_tc2_utilities/18014398544490635.html&id=
1862 means error in win32 system.
This is because TIMESTRUCT consists of Year, Month, Week, etc, but you only initialize the Hour as 5. This means that other things will become 0. The Year needs to be between 1970 and 2106, and there are many things to follow, shown below:
After you use a valid TIMESTRUCT variable, your code should be able to execute without problem and your computer system will be changed.
Had a similiar problem, using TwinCat3. Sometimes it works to change local system time, sometimes not.
I use a small state machine to solve that (also there could be a more advanced solution) - just write 3 times the new time....
Here is an code example:
VAR
nState : BYTE := 0; //local state machine
ntSetLocalTime : Tc2_Utilities.NT_SetLocalTime;
tTimestructSet : Tc2_Utilities.TIMESTRUCT; // time to set
nErrId : UDINT;
nRetryCnt : BYTE := 0;
and the state machine:
CASE nState OF
0: //wait for change
bBusy := FALSE;
nRetryCnt := 0;
1: //trigger change
bBusy := TRUE;
ntSetLocalTime(
NETID:= '',
TIMESTR:= tTimestructSet,
START:= TRUE,
TMOUT:= ,
BUSY=> ,
ERR=> ,
ERRID=> );
nState := 2; //wait for writing
2: //wait till written
ntSetLocalTime(
START:= FALSE,
ERRID => nErrId);
IF NOT ntSetLocalTime.BUSY THEN
nState := 3;
END_IF
3: //retry and after some retries go back to init
nRetryCnt := nRetryCnt + 1;
IF nRetryCnt >=3 THEN
nState := 0;
ELSE
nState := 1;
END_IF
END_CASE
Otherwise you could call Beckhoff hotline, in most cases they have a really good support.