Need help to find the right process for revision control

What is the best way to perform revision control on PAC Control projects? Are there any native or non-native options to efficiently keep track of all the changes?

Thanks,
Alan

Hi Alan. Welcome to the Opto forums.

Your not going to be too thrilled in the answers, but this is the best thread on the subject:

Not much has changed.
I can bump it again and see what the software guys are thinking, as I say in that thread, it does not come up very often.

Personally (20 years using PAC Control in a hospital with about 30 controllers) I just did my own manually via some sub-directories and copy/pasting the archives.
The hospital had me prove that I could do a ā€˜disaster recoveryā€™ of the automation system in 30 minutes, no Internet was allowed and running my own git repo was not something I was all that interested in at the time. (Also, not being a programmer with any training, skill or appreciation for automatic version control did not help).

You can use Git (locally or cloud) for strategies with some success. It wonā€™t work as well as it does for supported languages, but simply by showing you where the compiled Forth code changes, it can at least lead you to when/where things changed, and if you commit an entire project directory, you can revert to a previous version if needed. It also allows you to restore to do a side-by-side comparison if necessary.

If youā€™re really disciplined, you can print all chart instructions and track a diff on a more human-readable file, but there are many pitfalls to that approach (itā€™s a manual process, itā€™s one file per strategy instead of per chart, subroutines are not included, etc.).

My hope is still that at some point Opto developers will save a .txt file alongside chart and subroutine files that contain the plain text ā€œprint outā€ that automatically updates on save. The code to generate the plain text representation of charts and subroutines already exists. Hooking that code into the save mechanism and storing a plain text file alongside the .cht and .ccd files for each chart would be game changing for change tracking.

3 Likes

Thank you guys for the helpful information.

Are you using SNAP, EPIC, or both? Are you the primary flow editor?

If homogeneous EPIC and thereā€™s not much legacy engineering, then you might look at some of the more modern options with revision control support or the alternative controller, codesys, which has a subversion plug in. (Tail wagging the dog, I know, but now may be time to consider your options; EPIC is very capable).

If youā€™re the primary flow editor, then +1 for backups sans version control. I mainly use version control when Iā€™m coordinating with others to avoid overwriting their changes, but otherwise version control doesnā€™t always add much real benefit. A diff tool can be used outside of version control*, and thereā€™s less concern with trashing the history with an errant command or other corruption. Iā€™d recommend making backups to read-only media in addition to your backups to read-write media. Make checksums to verify backup integrity.

* Footnote: In general.

1 Like

Well said @Indigo
I honestly think that backups are more important than version trackingā€¦ But that should really be another topic.

1 Like

I came up with a semi-solution, a morning epiphany.

In the post @Beno linked, @varland wrote that subroutines are not included in the builtin printing methods of PAC Control. However, the subroutine strings are included in the string section of the chart executable. You can view the strings in the executable with the strings program included with GNU binutils. The strings command is installed by default in many linux distributions. Iā€™m unaware (and skeptical) of a windows port.

I downloaded and unzipped the subroutines sample and ran a command from a linux shell, $ strings Powerup.cht. The subroutines appeared to be intact. Hereā€™s the output,

CChartDoc
O22FlowchartDocument
O22Connection
O22ConnectionVertex@
O22CyranoGraphic
O22CyranoBaseGraphic@
O22CyranoObject
O22ScriptBlock
  // This block demonstrates the format of numeric literals.
  // Although they can be used in other contexts, this block only
  // uses them in simple assignment statements.
  // Decimal Integer 32 Literals
  n1 = 0;
  n2 = 10;
  n3 = -123;
  // Hexadecimal Integer 32 Literals must start with '0x' and may contain the
  // digits 0 through 9 and A through F.
  n1 = 0x0;
  n2 = 0x10;
  n3 = 0x12AB34CD;
  n3 = 0x12ab34cd; // the A-F digits may be upper or lower case
  // Decimal Integer 64 Literals are similar to Decimal Integer 32 Literals, 
  // except they have a "i64" appended at the end.
  d1 = 0i64;
  d2 = 10i64;
  d3 = -123i64;
  // Hexadecimal Integer 64 Literals assigned to variables
  d1 = 0x0i64;
  d2 = 0x10i64;
  d3 = 0x1234567890ABCDEFi64;
  d3 = 0x1234567890abcdefi64;
  // Float Literals assigned to variables
  f1 = 0.0;
  f2 = 12.3;
  f3 = -123.456;
  f3 = -1.23456e2;   // floats literals may use scientific notation
  f3 = -1.23456e+2;  // floats literals may use scientific notation
  f3 = -12345.6e-2;
O22InstructionBlock
"Arial
Numeric Literals.
  // This block demonstrates making assignments to numeric variables
  // from other numeric variables and literals.
  // Here are some simple Integer 32 assignments
  n1 = 1;
  n2 = n1;
  // Here are some simple Integer 64 assignments
  d1 = 2i64;
  d2 = d1;
  // Here are some simple Float assignments
  f1 = 3.0;
  f2 = f1;
  // Here are some simple assignments between different data types.
  // The types will be automatically converted to match.
  n1 = 4.0;
  d1 = n1;
  f1 = n1;
  n1 = 5i64;
  d1 = n1;
  f1 = n1;
  f1 = 6;
  n1 = f1;
  d1 = f1;
  f1 = 7i64;
  n1 = f1;
  d1 = f1;
  d1 = 8;
  n1 = d1;
  f1 = d1;
  d1 = 8.0;
  n1 = d1;
  f1 = d1;
"Arial
Numeric Assignments-
  // This block demonstrates math expressions. Although math
  // expressions can be used in other contexts, this block only
  // uses them in simple assignment statements.
  // Addition
  n1 = 1  +  2;
  n1 = n2 +  1;
  n1 = n2 + n3;
  n1 = 1  + n2 + n3;
  f1 = f2 + 1.0;
  f1 = f2 + n2 + d2 + 1; // note that types may be mixed
  // Subtraction
  n1 = n2 - 1;
  n1 = n2 - n3;
  n1 = 1  - n2 - n3;
  f1 = f2 - 1.0;
  f1 = f2 - n2 - d2 - 1; // note that types may be mixed
  // Multiplication
  n1 = n2 * 2;
  n1 = n2 * n3;
  n1 = 2  * n2 * n3;
  f1 = f2 * 2.0;
  f1 = f2 * n2 * d2 * 2; // note that types may be mixed
  // Division
  n1 = n2 / 2;
  n1 = n2 / n3;
  n1 = 2  / n2 / n3;
  f1 = f2 / 2.0;
  f1 = f2 / n2 / d2 / 2; // note that types may be mixed
  // Modulo Division
  n1 = n2 % 2;
  n1 = n2 % n3;
  n1 = 2  % n2 % n3;
  f1 = f2 % 2.0;
  f1 = f2 % n2 % d2 % 2; // note that types may be mixed
  // Mixture of operators
  n1 = n2 + n3 - 4;
  n1 = n2 + n3 * 4;
  n1 = f2 + n3 / 4;
  n1 = f2 + n3 % 4;
  // Use paranthesis to clarify groupings and meaning
  n1 = n2 * (f2 - 2.0);
  n1 = (n2 + 2) * (n3 - 3);
  n1 = (n2 + 2) * (n3 + (f1 / (f2 - 2)) - 3);
  // The *, /, and % operators have greater precedence than + and -.
  // For instance, example line #1 is equivalent to line #3, not #2.
  n1 = n2 + n3 * n4;   // ex. 1; this is equivalent to line #3
  n1 = (n2 + n3) * n4; // ex. 2
  n1 = n2 + (n3 * n4); // ex. 3; this is equivalent to line #1
"Arial
Math Expressions,
  // This block demonstrates string expressions. 
  // The + operator is used to  paste strings together. 
  // The + operator must be used in an assignment statement.
  s1 = "Hello ";
  s2 = "world";
  s3 = "!";
  s4 = s1 + s2 + s3;
  s4 = s1 + "world" + s3 + "!!";
  // The Chr() operator may be used to convert a numeric value into 
  // a one-element string.
  s5 = s1 + s2 + Chr('!'); 
  s5 = s1 + s2 + Chr(33); // use the ASCII value 33 for '!'
  // The += operator may be used to appended a string or character
  // to the end of a string.
  s1 += s2;        // Append s2 to s1
  s2 += Chr('a');  // Append the letter 'a' to s2
"Arial
String Expressions+
  // This block demonstrates the use of pointers and pointer tables.
  // Use the assignment operator to set a pointer. The & operator is used
  // to get the address of an object.
  // The types must match or the control engine will generate an error.
  pn1   = null;
  pn1   = &n1;
  pf1   = &f1;
  ps1   = &s1;
  pcht1 = &Powerup;
  // Use '*' to dereference a pointer. This will behave just like
  // the variable to which the pointer is pointing.
  n2 = *pn1 + *pf1;
  n2 = n1 + f1;      // this is equivalent to the previous command;
  // To see if a pointer is pointing to something, compare it to 'null'.
  // For example:   
  n2 = (pn1    == null);   // In this context, the paranthesis are not necessary
  n2 = (null   == pn1);    // but may make the code more readable
  n2 = (pt1[0] == null);
  n2 = (null   == pt1[0]);
  // Pointer tables can be set. Types are not checked when putting pointers
  // into a pointer table.
  pt1[0] = null;
  pt1[1] = &n1;
  pt1[2] = &f1;
  pt1[3] = &s1;
  pt1[4] = &Powerup;
  // When a pointer is moved from a pointer table element into a pointer variable,
  // the types are checked by the control engine and must match.
  pn1  = pt1[1]; // this is good
  pf1  = pt1[2]; // this is good
  pnt1 = pt1[3]; // this is bad and will cause a control engine error
  // Pointers are very useful when you don't know what variables need to be
  // used until runtime. For instance, the next example uses a 'switch' statement
  // to determine which variable to use based on the day of the week. It then uses 
  // a pointer to perform a calculation using the correct variable.
  switch (GetDayOfWeek())
    case 0:  // Sunday
      pn1 = &n2; 
      break  
    case 6:  // Saturday
      pn1 = &n3; 
      break  
    default: // Monday-Friday
      pn1 = &n4; 
      break  
  endswitch
  // By using the pointer, set the chosen variable.
  *pn1 = n1 * f1 - 5; 
"Arial
Pointers*
  // This block demonstrates string literals. Although string
  // literals can be used in other contexts, this block only
  // uses them in simple assignment statements.
  // String literals must reside entirely on one line.
  s1 = ""; // empty string
  s1 = "Hello, world!"; // basic string assignment
"Arial
String
Literals)
  // This block demonstrates making assignments to string variables
  // from other string variables and literals.
  s1 = "Hello, world!"; // assign a string literal
  s2 = s1; // assign from another string
  // The Chr() operator can be used to assign a character value
  // to a string variable. The Chr() operator is treated like a string
  // containing just the given character. A character may be quoted or 
  // its ASCII value given. For example, the following two statements
  // are equivalent.
  s1 = Chr('A');
  s1 = Chr(65);
  // A string may be thought of as a table of characters.
  s1 = "Hello. ";
  s1[6] = '!';
  s1[7] = s1[6]; // this will result with s1 as "Hello!!";
  // String indexes are one-based, which means that the first 
  // index is 1. Tables, on the other hand, are zero-based.
  // A character element of a string variable may be treated like
  // an Integer 32 value;
  n1 = s1[1] * s1[2];
"Arial
String Assignments(
  // This block demonstrates numeric tables. Although numeric
  // tables can be used in other contexts, this block only
  // uses them in simple assignment and expression statements.
  // The square brackets '[' and ']' are used to specify an index.
  // For example, the following example assigns the value at 
  // index 2 of integer table "nt1" into integer variable "n1";
  n1 = nt1[2];
  // Numeric tables are zero-based, which means that the first 
  // element has an index of 0.
  // Numeric table elements can be used almost anywhere a numeric
  // variable can be used. For example,
  nt1[0] = 1;
  nt1[1] = 2.0;
  nt1[2] = n1;
  nt1[3] = nt1[2];
  nt1[4] = nt1[nt1[0]];
  nt1[5] = n1 + nt1[2] * 3.1;
  n1 = nt1[0];
  n1 = (nt1[0] + nt1[1]) * nt1[2];
"Arial
Numeric Tables'
  // This block demonstrates string tables. Although string
  // tables can be used in other contexts, this block only
  // uses them in simple assignment and expression statements.
  // String table elements can be used almost anywhere a string
  // variable can be used. For example,
  st1[0] = "Hello";
  st1[1] = "world";
  st1[2] = st1[0] + " " + st1[1] + Chr('!');
  s1 = st1[2];
  st1[3] = s1;
"Arial
String
Tables&
  // This block demonstrates the use of logical operators.
  // Although they can be used in other contexts, this block only
  // uses them in simple assignment statements.
  // Depending on the operand types, a logical operators may 
  // return either an Integer 32 or Integer 64 value.
  // A zero value means FALSE and a non-zero value means TRUE.
  // OptoScript supports the following logical operators for comparing
  // two numeric values:
  //   and    (both values are TRUE)
  //   or     (at least one value is TRUE)
  //   xor    (only one value is TRUE)
  //   not    (inverse the logical value)
  // For example:
  n1 = n2 and n3;
  n1 = n2 or  n3;
  n1 = n2 xor n3;
  n1 = not n2;
  // Logical operations may be chained together:
  n1 = n2 and n3 and n4;
  n1 = n2 and n3 or n4;
  n1 = n2 xor n3 and n4 or n5;
  // Logical operators a left-associative. For example, the next two 
  // lines are equivalent:
  n1 = n2 and n3 or n4;
  n1 = (n2 and n3) or n4;
  // The not operator precedes a value (it only takes a value on its
  // right hand side)
  n1 = not n2;
  n1 = not (n2 < 5); // same as  "n1 = i2 >= 5;"
  // The following two lines are equivalent:
  n1 = not n1 and not n2;
  n1 = not (n1 and (not n2));
  // Logical operators can be combined with comparison operators
  // to create complex logical expressions. For example:
  n1 = (n2 < 1) and (n3 == 6.5);
  n1 = (n2 < 1) and (s1 == "abc");
  n1 = ((n2 < 1) and (n3 == 6.5)) or (n4 xor n5) or (not f1 == f2);
"Arial
Logical Operators%
  // This block demonstrates the use of bitwise operators.
  // Although they can be used in other contexts, this block only
  // uses them in simple assignment statements.
  // All OptoScript bitwise operators operate on integer values.
    
  // OptoScript supports the following logical operators for comparing
  // two numeric values:
  //   bitnot    (bitwise not)
  //   bitand    (bitwise and)
  //   bitor     (bitwise or)
  //   bitxor    (bitwise xor)
  //   <<        (left bit shift)
  //   >>        (right bit shift)
  // bitwise not operator: invert the bits of the given value
  n1 = bitnot n2;
  n1 = bitnot 0x0002; // hex literals are convenient
  // bitwise and operator: and together the two values bit by bit
  n1 = n2 bitand 2;    
  n1 = n2 bitand 0x0002; // hex literals are convenient
  n1 = n2 bitand n3;   
  // bitwise or operator: or together the two values bit by bit
  n1 = n2 bitor 2;    
  n1 = n2 bitor 0x0002; // hex literals are convenient
  n1 = n2 bitor n3;   
  // bitwise xor operator: xor together the two values bit by bit
  n1 = n2 bitxor 2;    
  n1 = n2 bitxor 0x0002; // hex literals are convenient
  n1 = n2 bitxor n3;   
  // Left-shift operator: shifts the left value's bits to the left 
  // by the right value 
  n1 = n2 << 2;    // left shift i2's value by 2
  n1 = n2 << n3;   // left shift i2's value by i3
  // Right-shift operator: shifts the left value's bits to the right 
  // by the right value 
  n1 = n2 >> 2;    // right shift i2's value by 2
  n1 = n2 >> n3;   // right shift i2's value by i3
"Arial
Bitwise Operators$
  // This block demonstrates the use of comparison operators.
  // Although they can be used in other contexts, this block only
  // uses them in simple assignment statements.
  // All OptoScript comparison operators return an Integer 32 value.
  // A zero value means FALSE and a non-zero value means TRUE.
  // OptoScript supports the following logical operators for comparing
  // two numeric values:
  //   ==    (equivalence)
  //   <>    (non-equivalence)
  //   <     (less-than)
  //   <=    (less-than or equal)
  //   >     (greater than)
  //   >=    (greater-than or equal)
  // For example:
  n1 = n2 == f3;
  f1 = n2 <> f3;
  n1 = n2 <  f3;
  n1 = n2 <= f3;
  n1 = n2 >  f3;
  n1 = n2 >= f3;
  // More complex expression can be created:
  n1 = (n2 * 2) == (f3 / 9.5);
  n1 = (n2 * 2) <  (f3 / 9.5);
  // OptoScript also supports the ability to test if two strings
  // are equal. For example:
  n1 = s1 == s2;
  n1 = s1 == "abc";
  n1 = s1 == st1[0];
  n1 = st1[0] == st1[1];
"Arial
Comparison Operators#
  // Any numeric value may be tested by the 'if' statement
  if (n1) then
    f1 = 2.0;
  endif
  // Since a comparison operator returns an Integer 32 value,
  // it may be used as the test value.
  if (n1 > 3) then
    f1 = 2.0;
    f2 = 6.5;
  endif
  // Complex logical operations may also be used
  if ((n1 > 3) and (n1 <= 10) and (not n1 == 6)) then
    f1 = 2.0;
    f2 = 6.5;
  endif
  // An optional 'else' statement may be added in.
  if (n1 > 3) then
    f1 = 2.0;
    f2 = 6.5;
  else
    f3 = 8.8;
  endif
  // An optional 'elseif' statement may be added in to chain
  // together several tests.
  if (n1 > 3) then
    f1 = 2.0;
    f2 = 6.5;
  elseif (n1 < -3) then
    f3 = 8.8;
  endif
  // Multiple 'elseif' statements may be used. The 'else' statement
  // is still allowed at the end.
  if (n1 > 3) then
    f1 = 2.0;
    f2 = 6.5;
  elseif (n1 < -3) then
    f3 = 8.8;
  elseif (n1 == 0) then
    f3 = f1 * f2;
  elseif (n1 == 1) then
    f1 = n1 + 3;
    f2 = f1 / 0.8;
  else
    f1 = 0;
    f2 = 0;
    f3 = 0;
  endif
  // If statements may be nested. Note that each 'if' needs an 'endif'.
  if (n1 > 3) then
    f1 = 2.0;
    f2 = 6.5;
    if (n1 % 10) then
      f1 = f1 * 2;
      f2 = f2 * 3;
    else
      f3 = 0;
    endif
  endif
"Arial
if-then-
else-endif"
  // A 'switch' statement may be used in place of 'if' statements
  // in some situations.
  switch (n1)
    case 1:
      f1 = 10;
      break
    case 2:
      f1 = 15;
      break
    case 3:
      f1 = 20;
      break
    case (n2 * 2): // a numeric expression may be used as a case.
      f1 = 20;
      break
    default: // the optional default item must be at the end of the list
      f1 = 0;
      f2 = -1;
      break
  endswitch
"Arial
switch/case Statements!
  // The 'for' loop can be used to do a list of statements
  // a certain number of times.
  // This example results in variable i2 equaling 6
  n2 = 1;
  for n1 = 0 to 4 step 1
    n2 = n2 + 1; 
  next
  // The 'for' loop index parameter may be used in the loop.
  // This example sets the first 5 elements of table nt1 to 10.
  for n1 = 0 to 4 step 1
    nt1[n1] = 10;
  next
  // Different step amounts may be used.
  // This example sets elements 0, 2, and 4 of table w1 to 20
  for n1 = 0 to 4 step 2
    nt1[n1] = 20;
  next
  // The given ranges may be a numeric expression, but they 
  // are only evaluated at the begining of the loop.
  // For instance, following example will loop 0 to 15 because
  // the upper limit of i2*3 is only evaluted at the begining
  // of the loop, not each time through the loop.
  n2 = 5;
  for n1 = 0 to (n2*3) step 1 
    n2 = 1;
  next
  // 'for' loops may be nested and also contain other kinds
  // of program statements. For example:
  for n1 = 0 to 9 step 1
    
    // Initialize table x1
    nt1[n1] = n1 * 2;
    // There's one special case
    if (n1 == 5) then
      nt1[5] = -1;
    endif
  next
  // Each 'for' loop needs a 'next' at the end.
  for n1 = 0 to 9 step 1
    for n2 = 0 to 9 step 1
      // Initialize table x1
      nt1[n1*10 + n2] = n1;
      if (n1 < 5) then
        // Initialize table x2, which is only 50 elements long
        nt2[n1*10 + n2] = n2;
      endif
    next
  next
      
"Arial
Loops 
  // The 'while' loop is used to execute a list of statements
  // while a given condition is true. The test is executed at 
  // the begining of each loop.
  // This example sets the first 5 elements of table x1 to 10.
  n1 = 0; // initialize the counter
  while (n1 < 5)
    nt1[n1] = 10; // set the table elements
    n1 = n1 + 1; // increment the counter
  wend
    
  // 'while' loops may be nested and also contain other kinds
  // of program statements. Each 'while' statement needs a matching
  // 'wend' at the end. For example:
  n1 = 0;
  while (n1 < 100)
    while ((n1 > 50) and (n1 < 60))
      
      nt1[n1] = n1 * 100;
      n1 = n1 + 1;
    wend
    nt1[n1] = n1;
    n1 = n1 + 1;
  wend
"Arial
while
Loops
  // The 'repeat' loop is used to execute a list of statements
  // while a given condition is true. The test is executed at 
  // the end of each loop. The content of the loop will always
  // execute at least once.
  // This example sets the first 5 elements of table x1 to 10.
  n1 = 0; // initialize the counter
  repeat 
    nt1[n1] = 10; // set the table elements
    n1 = n1 + 1; // increment the counter
  until (n1 >= 5);
    
  // 'repeat loops may be nested and also contain other kinds
  // of program statements. Each 'repeat' statement needs a matching
  // 'until' test at the end. 
  // For instance, this example sets each table element to its index.
  // This could be handled much better in a 'for' loop, but this just
  // helps to illustrate nested 'repeat' loops.
  n1 = 0;
  repeat 
       
    repeat      
      nt1[n1] = n1 + 100;  // set the table element
      n1 = n1 + 1;         // increment the counter
    until (not (n1 % 10)); // breakout of the loop every 10 (10, 20, 30, etc.)
    nt1[n1] = n1; // set the table element
    n1 = n1 + 1;  // increment the counter
  until (n1 >= 50);
"Arial
repeat
Loops
  // OptoScript has full access to the ioControl command set.
  // Some commands, such as "Add", "OR", "Move", and "Append 
  // String to String" are not avaible as a command within 
  // OptoScript because their functionality is provided as an
  // inherent feature of OptoScript.
  // Procedure commands take a list of zero or more parameters.
  // They do not return a value. The main difference between 
  // procedures and functions is that functions return a value
  // and procedures do not.
  // OptoScript command names are similiar to their ioControl
  // instruction list versions, except they do not contain any 
  // whitespace and are sometimes abbreviated.
  // Here are some examples of procedures that have no parameters.
  ClearAllErrors();
  RemoveCurrentError();
  // Here are some examples of procedures that have at least 
  // one parameter.
  s1 = "Hello!";
  StringToUpperCase(s1);
  nt1[5] = 20; // set a table value to illustrate clamping
  ClampInt32TableElement(10, 0, 5, nt1);
"Arial
Procedure Commands
  // This block demonstrates the use of function commands.
  // Although they can be used in other contexts, this block only
  // uses them in simple assignment statements.
  // OptoScript has full access to the ioControl command set.
  // Some commands, such as "Add", "OR", "Move", and "Append 
  // String to String" are not avaible as a command within 
  // OptoScript because their functionality is provided as an
  // inherent feature of OptoScript.
  // Function commands take a list of zero or more parameters.
  // and return a value. The main difference between 
  // procedures and functions is that functions return a value
  // and procedures do not.
  // OptoScript command names are similiar to their ioControl
  // instruction list versions, except they do not contain any 
  // whitespace and are sometimes abbreviated.
  // Here are some examples of functions that have no parameters.
  n1 = GetMonth();
  n3 = GetIdOfBlockCausingCurrentError();
  // Here are some examples of functions that have at least 
  // one parameter.
  f1 = 3.5;
  f2 = Cosine(3.5);
  n2 = GetChartStatus(Powerup);
  f1 = SquareRoot(99);
  s1 = "Test String";
  n2 = GenerateChecksumOnString(0, s1);
  s2 = s1 + Chr(n2); // add the checksum byte to the end
  n3 = VerifyChecksumOnString(0, s2);
  n1 = FindCharacterInString('S', 0, s2);
"Arial
Function Commands
  // Most of the example code in this strategy uses variables to make
  // illustrations. However, I/O units and points can also be used
  // within OptoScript code.
  // This strategy uses a SNAP Ethernet Demo Center as its I/O unit.
  // These demo centers have a standard arrangement of I/O points
  // and are used by Opto 22 to make various demos.
  // I/O points are treated just like numeric variables.
  // Digital points behave like integers variables and analog points
  // behave like float variables. An I/O point may be used anywhere 
  // that a numeric variable can be used in a program.
  // Digital points are treated as integers, but their only meaningful 
  // states are true and false. A digital point is on, or true, when
  // it is a non-zero value (usually -1). It is off, or false, when
  // it is zero. For example:
  n1 = diSwitchD1; // store the state of Button d1 into i1
  n1 = not diSwitchD1; // store the inverse of Button d1 into i1
  doLedD7 = 0;   // turn off LED d7 
  doLedD7 = 1;   // turn on LED d7
  doLedD7 = 0;   // turn off LED d7 
  doLedD7 = 456; // turn on LED d7 (non-zero is TRUE)
  doLedD6 = diButtonD2; // use d2 to set d6
  f1 = aiTemperatureA12;  // store the temperature from point 12 into f1
  f2 = aiPotA16;  // store the potentiometer value from point 16 into f2
  aoMeterA8 = 2.5;  // store 2.5 into the meter, point 8
  aoMeterA8 = aiPotA16;  // store the potentiometer value into the meter output
  // Since I/O points are treated just like numeric variables, they 
  // may also be used in more complex situations. For example:
  doLedD5 = 0; // turn off point d5
  doLedD6 = 0; // turn off point d6
  if ((diSwitchD0 and diSwitchD1) or (diButtonD2 and diButtonD3)) then
    doLedD5   = 1; // turn on d5
    aoMeterA8 = 8.0;
  else
    doLedD6   = 1; // turn on d6
    aoMeterA8 = 2.0;
  endif
  while (aiPotA16 >= 3.5)
    doLedD5 = diSwitchD1;
    doLedD6 = not diButtonD2;
  wend
  // Use I/O points in math expressions:
  f1 = (aiTemperatureA12 + aiPotA16) * 2.3;
  // I/O points may be passed to commands:
  n1 = GetCounter(diButtonD3);
  f1 = GetAnalogMaxValue(aiTemperatureA12) - 
       GetAnalogMinValue(aiTemperatureA12);
  // Blink an output
  TurnOn(doLedD5);   // turn on the point
  DelayMsec(250);    // delay for 250 milliseconds
  TurnOff(doLedD5);  // turn off the point
"Arial
  // All the previous example OptoScript code has mostly used simple
  // assignments to demonstrate different elements of the language.
  // However, some of the real power of OptoScript is seen in more 
  // complex examples.
  // Here is a review of the different kinds of assignments we have
  // made in previous blocks:
  n1 = 123;            // numeric literal
  n2 = n1;             // numeric variable
  n2 = nt1[0];          // numeric table elements
  n2 = 123 + 456;      // math expression
  n3 = 2 * (n2 - 5);   // math expression
  n4 = s1[0];          // string characters are treated as integers
  n5 = GetDay();       // function call
  f1 = Cosine(12.34);  // function call
  n1 = n2 <= 500;      // comparison operator
  n6 = n1 and n2;      // logical operator
  n3 = n1 bitand n2;   // bitwise operator
  // Since numeric literals, numeric variables, numeric table elements, 
  // math expressions, string characters, math expressions, 
  // function calls, comparison operators, logical operators, and bitwise
  // operators all result in a number, they can be used anywhere a number is 
  // expected.
  // For example, here are some complex expressions combining different 
  // elements:
  n1 = GetDay() * 24;
  n2 = 5 + Cosine(n1);
  n3 = (n1 + n2) * (n1 / (n2 - GetErrorCount()));
  // For example, here's some different ways of giving a table index: 
  n1 = 5; // initialize i1 for the example
  nt1[0]                 = 1; // numeric literal
  nt1[n1]                = 2; // numeric variable
  nt1[nt1[0]]            = 3; // numeric table element
  nt1[n1 + 1]            = 4; // math expression
  nt1[2 * (n1 - 1)]      = 5; // math expression
  nt1[GetDay()]          = 6; // function call
  nt1[GetDay() + 5]      = 7; // function call in a math expression
  nt1[n1 bitand 0x0002]  = 8; // bitwise operation
  // Logic tests are another place where complex expressions can yield 
  // powerful results. For example:
  n1 = GetDay();
  f1 = 120.8;
  nt1[0] = 6;
  if (((n1 + 1) > 4) and (GetHours() == 12)) then
    n2 = 0;
  elseif ((n1 == 4) xor (not (nt1[0] == 5))) then
    n2 = 5;
  endif
  // This while loop will wait until the next minute starts
  while (GetSeconds() > 0)
    DelayMSec(100);           // keep waiting
  wend
  // Fill up the nt1 table with random numbers. Note that the table
  // length is determined at runtime.
  for n1 = 0 to (GetLengthOfTable(nt1) - 1) step 1
    nt1[n1] = GenerateRandomNumber() * 10000;
  next
  // Set i4 to be the number of seconds elapsed this month
  n4 = ((GetDay() - 1) * 24 * 60 * 60) +  
       (GetHours() * 60 * 60) + 
       (GetMinutes() * 60) + 
       GetSeconds();
  // A more concise version of the above:
  n5 = ((GetDay() - 1) * 86400) + GetSecondsSinceMidnight();
  // Generate a checksum for a string.
  n2 = 0; // use i2 to calculate the checksum
  s1 = "This is a test string.";
  for n1 = 1 to GetStringLength(s1) step 1
    n2 = n2 + s1[n1]; // add each character to i2
  next
  n2 = n2 % 256; // the checksum is the remainder of i2 divided by 256
  // Compare the generated checksum to GenerateChecksumOnString()
  if (not n2 == GenerateChecksumOnString(0, s1)) then
    AddUserErrorToQueue(30); // add a user error
  endif
"Arial
Complex Expressions
  // There are several special OptoScript keywords to aid in the debugging
  // of OptoScript itself. There may be other purposes for these.
  // Here are the special debugging keywords:
  n1 = __LINE_NUM__;  // line number as integer literal
  s1 = __LINE_STR__;  // line number as string literal
  n2 = __CHART_NUM__; // chart ID as integer literal
  s2 = __CHART_STR__; // chart name as string literal
  n3 = __BLOCK_NUM__; // block ID as integer literal
  s3 = __BLOCK_STR__; // block name as string literal
  // Here is an example of creating a message string using these helpers:
  st1[0] = "Problem on line #" + __LINE_STR__ + " in block '" + 
           __BLOCK_STR__ + "' in chart '" + __CHART_STR__ + "'.";
"Arial
Testing/QA Helpers
O22GraphicText
"Arial
This strategy demonstrates some example OptoScript code.
It uses numerous variables and a SNAP Ethernet Learning/Demo Center. Make sure to change the IP address of the Ethernet I/O unit.
As a whole, the example code doesn't do anything meaningful. Each block demonstrates a particular aspect of OptoScript.
"Arial
OptoScript Example Code
O22ActionBlock
Arial
Arial
Start
O22TypeMask
O22CyranoDefaults
Arial
Arial
Arial
Arial
Arial

You can clearly see the subroutines along with the other strings in the strings section of the executable.

Caution: The lines of a subroutine are not guaranteed to be in order in every chart, every save of the chart, or every version of the chart schema, so do not rely on the method described in this post for production.

EDIT: You can put a special comment at the top and bottom of each opto script block, then pipe the strings command into a shell script (written and provided by you) to separate each opto script block contents into its own file, and then put each of those into revision control.

Also keep in mind that opto script blocks can do everything that all the other opto flow blocks can do, so you could conceivably put the vast majority of your chart contents into revision control.

This is an unsupported method. Now Iā€™m waiting on an engineer to tell me if this is a bad idea or not.

2 Likes

@Indigo this a cool idea I was wondering if there was some CLI tools that could be utilized to assist in this processā€¦ā€¦of course there is for Linux :penguin: