PAC Control on EPIC

I am looking for some PAC Control coding support as I cannot think of a method of achieving what my customer is requesting.

Background
I installed an EPIC PR1, serial module and DI module some years ago to provide a client with a tank farm level monitoring system. It uses PAC Control and Opto Modbus Integration Kit to get Modbus RTU data via the serial card from Tank Volume and Temperature gauges. This is displayed on the clients PC using PAC Display with Supertrends.
There are 30 tanks on site and the volume are stored in a float table after every read.

Required
The client would like a daily report listing ALL the volume movements both in and out, and the time the movement began. A movement is a change in volume of 500 litres, but of course they want to be able to change this, so it would be a variable for each tank!
As an example, they have asked:
For example if the tank level goes from 50,000 to 49,500 then it’s logged as an “out” or if the tank level goes from 50,000 to 50,500 then it’s logged as an “in” for that particular tank?
Example report:

Date / time From:	05/09/24, 00:00	Date / time to:	05/09/24, 23:59
Tank 1
Movement Date	Movement Time	In/Out	Volume
05/09/24	06:35	Out	12,000
05/09/24	08:27	In	13,000
05/09/24	11:34	In	10,000
 	 	 	 
Total movements In	2	 	 
Total movements Out	1	 	 
Total Volume in	23,000	 	 
Total Volume out	12,000	 	 
 	 	 	 
Tank 2
Movement Date	Movement Time	In/Out	Volume
05/09/24	07:35	In	12,000
 	 	 	 
Total movements In	1	 	 
Total movements Out	0	 	 
Total Volume in	12,000	 	 
Total Volume out	0	 	 

Having looked at the historical trends it is rare for an in or an out to have a flow rate of greater than 50 l/min. I have calculated this by simply measuring change in volume over time from the historical trends.
Using code to achieve this is where I am struggling and would appreciate some help, please.
There are no external triggers available to the system to say when a movement starts and ends, either from IO or human button pressing.

Any thoughts with example PAC Control code if possible would be greatly appreciated.
Tahnks in advance.

Good question, perfect for the forums!

Similar task at the hospital I used to work at - doing the automation with Opto.

We had 100’s of zones of air-conditioning. We had to allow for a small adjustment, in your case, volume unit to track.

I took that amount and added and subtracted from the current value, so now we have a window.
If the volume moves outside that limit (Use the command ‘within limits’ of the upper and lower new set points), when it does, log it.

For the reports I would look at PAC Display logging. You could do it ‘on screen’ and have a print button, I’ve done that before. Depends on if they want an email every day or paper trail only when needed.

Either way, loops clearly are going to be in your future.

Just some inital thoughts.
I suspect others will be along to mull other options.

There are going to be a lot of little nuances to this - best to break it down into smaller problems and build up.

I would recommend starting with the logic you would use for just one tank before turning it into a loop going over all the tanks.

Figure out how you are going to determine a flow has ended and then run your logic to see if it changed enough to log. You will probably be storing the last logged level (for future logging) and the last detected level (to determine if the level is no longer changing) and use the current level for comparison.

Then you can decide how you are going to store each event - in a PAC Control table to read and report later - or through PAC Display logging to be processed by another application - or through a database and a reporting server or something else.

2 Likes

@Beno Thank you for your suggestions. Never thought of the ‘within limits’ command. Great starting place.
@philip I agree with you on starting small with one tank. Will probably use timer to try and detect ‘no change’ in volume to use for flow ended, then do the maths for the movement.
I will use PAC Control tables for this and perhaps use the number in INS or OUTS counts as the index in the tables.
As client wants a PDF report at the end of each day, I might use Node Red to do this before clearing the daily tables ready for the next day!

Thank you both. Much appreciated.

Hi guys, thanks for your pointers I now have the volumes being stored in string table for each tank:
image
I now need to create a PDF report. I have seen some posts about using Node Red and PDFMAKE, so I decided to create a CSV file using Node Red flow:
[{“id”:“728419205f65d485”,“type”:“pac-read”,“z”:“299aa966304b7bb0”,“device”:“12030ac3.3a560d”,“dataType”:“string-table”,“tagName”:“stTank1_Movements”,“tableStartIndex”:“1”,“tableLength”:“msg.payload.tableLength”,“value”:“”,“valueType”:“msg.payload”,“topic”:“”,“topicType”:“none”,“name”:“T1 Movements”,“x”:620,“y”:120,“wires”:[[“bf066090d6ca5388”,“9cf6adb6d80b746d”]]},{“id”:“12030ac3.3a560d”,“type”:“pac-device”,“address”:“localhost”,“protocol”:“https”,“msgQueueFullBehavior”:“REJECT_NEW”}]

But I can’t work out how to only get the indexes which have data. I have this stored in a numeric table, index 1 for Tank 1, and was trying to use a PAC Read node using Dynamic Settings:
msg.payload.tableLength - a number for the number of table elements to read.

Is there a better way to create the CSV file and store it in the unsecured file area?

You can use the communication commands in PAC control to write to a file.

Terry and I are currently driving to deliver a trade show exhibit, but can provide some ideas on either a function node loop or opto script loop to detect and drop the empty indexes and so make a clean CSV file to be converted to the PDF.

@Beno I would prefer to use PAC Control as suggested by Philip. Currently looking at the User Manual.
Any example of script block to create clean CSV for one tank that I can expand would be very helpful, thank you!
Once the daily PDF is created for each tank, I intend to use PAC Control to send it as an attachment to an email, then clear out the files for the next day.
Any tanks without movement, nothing in its string table, can be ignored.

Safe trip to both of you!

Here is some logic I have that checks the file position and writes out the header if needed that I put in a subroutine for writing logs. It should get you going.

nStatus = 0;

if(not IsCommunicationOpen(chLog)) then
  nStatus = OpenOutgoingCommunication(chLog);
endif

if(nStatus == 0) then

  //See if this is a new file
  nPos = SendCommunicationHandleCommand(chLog, "getpos");
  if(nPos == 0 and not sHeaderRow == "") then
    //Write header row
    sWrite = sHeaderRow + Chr(13) + Chr(10);
    TransmitString(sWrite, chLog);
  endif

  sWrite = sLogEntry + Chr(13) + chr(10);

  nStatus = TransmitString(sWrite, chLog);

  if(not nLeaveOpen) then
    CloseCommunication(chLog);
  endif

endif

Comm handle is set to file:a,~/unsecured/filename.csv

To be clear, pac control can not make a PDF.
It can email the CSV as an attachment, but you will need Node-RED to make the PDF if that is a requirement.

@philip Thank you, I will try and apply it to tables.

@Beno Yes, the client wants a ‘nice and tidy’ PDF :frowning: , so I will need to create the CSV file and use Node Red to create the PDF, store that in the unsecure area, and use PAC Control to email it.
At least that’s current my plan of attack.

I’d also use Node-RED to email it, but let’s get it made nice and neat first.
We are at our first rest stop, 6 hours to go.

Up to use Node-RED for email if you think that’s better.

6 hours, long time for first rest :laughing:

Now that we’re back and up for air, I’ve built up an example flow that manually injects the sample data from your screenshot as an array of comma-separated values, then filters out any blank entries, and formats it for pdfmake. There’s a few node packages, the one I picked is:
https://flows.nodered.org/node/@platmac/node-red-pdfbuilder
EDIT: Use node-red-contrib-pdfmake (node) - Node-RED as suggested by @Beno, for these purposes it is functionally the same and it installs cleanly on Node-RED v3.1.7; I updated the example below to use this node.

Here’s what the output PDF and flow look like:

The main thing was getting the msg.payload “content” format right, then have the pdfmake node output base64 and also use base64 in the write file node.
To build the “content” I turned the one-dimensional string table into a multi-dimensional array so that each row itself is an array of each of the columns — that made it an easy drop-in for the pdfmake format, and as part of that loop I skip over any blank entries in the original table.

I put the two-dimensional array in its own property msg.table so you can see what that looks like in the debug node.


Here’s the code from the function node, plus the example flow export at the end of the post.
(The imported version of the code has comments.)

var raw = msg.payload;
var table = [];
for(var i in raw) {
    if(raw[i] === "") continue;
    else {
        var row = raw[i].split(",");
        table.push(row);
    }
}
msg.table = table;

msg.payload = {
    content: [
        { text: 'Example table PDF\n\n', style: 'header' },
        {
            style: 'tableExample',
            table: {
                widths: ['*', '*', '*'],
                body: table
            }
        }
    ]
}
return msg; 
[{"id":"dc7f2e84100d34c5","type":"function","z":"4be5ce8c7e250d04","name":"format PDF","func":"// Save the raw incoming payload on a local variable\nvar raw = msg.payload;\n// The output will be a two dimensional array of non-blank entries\nvar table = [];\n// Loop through all of the raw incoming data; each entry holds one row of data\nfor(var i in raw) {\n    // If this row is blank, skip over it...\n    if(raw[i] === \"\") continue;\n    // Otherwise separate out the columns that make up this row\n    else {\n        // Each row is separated by a comma, so use that to split into an array\n        var row = raw[i].split(\",\");\n        // Add it to the end of the table\n        table.push(row);\n    }\n}\n// Make table a message property for easy debugging\nmsg.table = table;\n\n// Create the payload for pdfmake\nmsg.payload = {\n    // The payload must follow the `content:[..]` format\n    content: [\n        // Define a header, title, and add any other necessary information\n        { text: 'Example table PDF\\n\\n', style: 'header' },\n        // Separate section for the table\n        {\n            style: 'tableExample',\n            // Create the table object itself\n            table: {\n                // Widths for each of the columns, using '*' to take as much space as possible\n                widths: ['*', '*', '*'],\n                // The body is just the two dimensional table defined in the loop above\n                body: table\n            }\n        }\n    ]\n}\nreturn msg; ","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":250,"y":380,"wires":[["1d34b30a9a1be780","b1ff6ab02405aac7"]]},{"id":"ea44361c0ab9d0e5","type":"file","z":"4be5ce8c7e250d04","name":"write file","filename":"/home/dev/unsecured/PDF_test.pdf","filenameType":"str","appendNewline":false,"createDir":false,"overwriteFile":"true","encoding":"base64","x":440,"y":440,"wires":[[]]},{"id":"1d34b30a9a1be780","type":"debug","z":"4be5ce8c7e250d04","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","l":false,"x":395,"y":380,"wires":[]},{"id":"b1ff6ab02405aac7","type":"pdfmake","z":"4be5ce8c7e250d04","name":"","outputType":"base64","inputProperty":"payload","options":"{}","outputProperty":"payload","x":240,"y":440,"wires":[["ea44361c0ab9d0e5"]]},{"id":"8b63b34e3e81ceab","type":"debug","z":"4be5ce8c7e250d04","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","l":false,"x":395,"y":320,"wires":[]},{"id":"c2f8603117aa8a9f","type":"inject","z":"4be5ce8c7e250d04","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","x":230,"y":320,"wires":[["dc7f2e84100d34c5","8b63b34e3e81ceab"]]}]
3 Likes

Thank you Terry.
I will give it a go.
I was in the middle of testing PAC Control code for creating the PDFMake data, but this looks neater!

@torchard I failed at the first hurdle trying to install pdfbuilder flow on my EPIC-PR1, System Version3.6.0-b.32.
Error message: [err] ERR! notsup Unsupported engine for pdfmake@0.3.0-beta.10: wanted: {“node”:“>=18”} (current: {“node”:“14.20.0”,“npm”:“6.14.17”})

Which device did you test this flow on?

I got a feeling that @torchard tested this on his laptop, but we should be able to narrow down a version of PDF maker for the EPIC.

Did you try the trick of installing a version ‘0’?

@Beno Tried it with 0 and 1.
0 Reported no file exists, and 1 gave version error as before. :slightly_frowning_face:

To get you up and running while we look at things a bit more on this end, can you please install (from the manage pallet menu, not the groov Manage node management) this package:

1 Like