tedstriker / node-red-contrib-sequencer

Record and replay node-red msg-object sequences
2 stars 3 forks source link

Persistent sequence #2

Closed bartbutenaers closed 7 years ago

bartbutenaers commented 7 years ago

Hi Ted,

Thanks for contributing this very useful node! I'm trying to use Node-Red for my video surveillance at home, but I'm afraid I need your assitance...

I have an IP camera pointed at a door. As soon as the door opens, a magnetic sensor triggers my Node-Red flow, which starts capturing images from my IP camera. Now I want to store that sequence of images on my disc (with your Recorder), and load it afterwards from disc - with the correct delays in between - into my flow (with your Player): this way I could replay the camera stream afterwards in my dashboard, to see what has happened.

However if I'm not mistaken, the messages are stored in memory (array), not on the file system:

this.addElement = function(data, delay) {
            _queue.push(new Element(data, delay));
};

As a result the memory consumption on my Raspberry PI would become horrible (since images consume lots of bytes), and all evidence would be lost in case of power shutdown...

So it would be great if the msg sequence could be persisted in some specified directory. Which means every message could be written to a file. I assume the (human readible?) timestamp should be part of the file name, since the birthtime seems to be OS dependant...

If I could be of any help (e.g. via a pull request), don't hesitate to let me know!

Thanks in advance !! Bart

tedstriker commented 7 years ago

Hi Bart,

I like the idea of persisting large record sequences, to be gentle on an embedded systems RAM. Something like a temporary swap file, if the in-memory array grows too big, would be a good thing to implement. PRs for this are welcome.

Besides that, persisting a message to a file should not be the sequencer-nodes task - there should be a dedicated img2file-node doing the heavy lifting. I'll give an example with the scenario you described:

In your case the pictures of your camera should go straight to disk. To a known target directory with a known naming pattern. You now should record just the filename. That way we only need to record a reference instead of the image itself, which has at least two advantages:

see why: if you decide to save your images to a database one day, instead as plain file, you would need to change the sequencer-nodes source code if we go the way you proposed. Using the reference you only have to tweak your flow. (new image-saver node and image-loader node)

To replay the sequence you'd go the way in reverse. You'd make a node which can load an image from disk for a given filename. The filenames would be provided by the player node in the desired timing. You could load the files instantly when triggered. You could even preload some images for faster response, if necessary.

I hope I could transport the idea. If you have questions, feel free to ask.

Regards Ted

bartbutenaers commented 7 years ago

Hi Ted,

Really appreciate your feedback!

Indeed after I registered this issue, I was also in two minds whether the sequencer should be responsible for storing information in the filesystem. Because someone else perhaps wants to store the info in a database, or somewhere else. So I totally agree to keep it separate.

To handle only image references in the sequencer is the solution I was looking for. Simple and straigthforward. Don't know why I couldn't think of it myself... Some way as some databases handle blobs.

I have been experimenting with it, and it works great:

Storing images on the filesystem

image

[{"id":"6b89958c.5ef2ec","type":"recorder","z":"f711b886.a58148","name":"","maxElements":"7","maxDuration":"0","startImmediately":true,"x":473.38288497924805,"y":129.77735137939453,"wires":[["ed49db47.8d8ca8"]]},{"id":"13e55209.37db5e","type":"inject","z":"f711b886.a58148","name":"Start recording","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"x":122.015625,"y":129.671875,"wires":[["380d1482.0c8dac"]]},{"id":"380d1482.0c8dac","type":"function","z":"f711b886.a58148","name":"Get URLs","func":"function sendDelayed(i) {\n    setTimeout(function(){\n        var url = 'http://scrollmagic.io/img/example_imagesequence_0' + i +'.png';\n        var file = '/tmp/image_' + i + '.png';\n        if(i==1) {\n            node.send({url: url, filename:file, start: 1});\n        }\n        \n        node.send({url: url, filename:file});            \n    }, 1000 * i);\n}\n\n//node.send({start});\n\nfor (i = 1; i < 8; i++) {\n   sendDelayed(i);\n}\n\n//node.send({stop});","outputs":1,"noerr":0,"x":291.01175689697266,"y":129.60547637939453,"wires":[["6b89958c.5ef2ec","d211eb30.fba2f8"]]},{"id":"d211eb30.fba2f8","type":"http request","z":"f711b886.a58148","name":"Get image","method":"GET","ret":"bin","url":"","tls":"","delay":0,"x":481,"y":177.52735137939453,"wires":[["32516bbe.65df24"]]},{"id":"32516bbe.65df24","type":"file","z":"f711b886.a58148","name":"Write image file","filename":"","appendNewline":false,"createDir":false,"overwriteFile":"true","x":676.0039405822754,"y":178.03906917572021,"wires":[]},{"id":"86237993.edaea8","type":"file","z":"f711b886.a58148","name":"Write TOC file","filename":"/tmp/images.toc","appendNewline":false,"createDir":false,"overwriteFile":"true","x":875.7656326293945,"y":129.75000953674316,"wires":[]},{"id":"ed49db47.8d8ca8","type":"change","z":"f711b886.a58148","name":"Set sequence","rules":[{"t":"set","p":"payload","pt":"msg","to":"sequence","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":676.6666870117188,"y":129.66666412353516,"wires":[["86237993.edaea8"]]}]

In this example, 7 images are grabbed (with 1 second in between) and stored as .png files. Meanwhile the image file paths are recorded. Once all 7 image paths have been recorded, a TableOfContents file is save in the filesystem:

image

That TOC file contains all the image file paths: image

Loading images from the filesystem

The TOC file is loaded from the file system and send to the player, which will generate messages containing the image file path references (at the correct time intervals):

image

[{"id":"3249f87a.8022d8","type":"player","z":"f711b886.a58148","name":"","runOnLoad":true,"x":652.3750801086426,"y":253.28129959106445,"wires":[["a062292a.40c2b8"]]},{"id":"7aeeac14.2f8a04","type":"inject","z":"f711b886.a58148","name":"Start playing","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"x":120.765625,"y":253.75,"wires":[["bbb32aa2.4e8078"]]},{"id":"bbb32aa2.4e8078","type":"file in","z":"f711b886.a58148","name":"Read TOC file","filename":"/tmp/images.toc","format":"utf8","x":300.8333435058594,"y":253.66666412353516,"wires":[["42de8364.36bf5c"]]},{"id":"42de8364.36bf5c","type":"change","z":"f711b886.a58148","name":"Set sequence","rules":[{"t":"set","p":"sequence","pt":"msg","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":489.50011444091797,"y":253.66667366027832,"wires":[["3249f87a.8022d8"]]},{"id":"3d6ef454.a06c7c","type":"base64","z":"f711b886.a58148","name":"","x":1004.8333473205566,"y":252.33334016799927,"wires":[["8321cb35.5652a8"]]},{"id":"8321cb35.5652a8","type":"ui_template","z":"f711b886.a58148","group":"ce97ed94.7aa07","name":"Player test","order":1,"width":"6","height":"6","format":"<img width=\"16\" height=\"16\" alt=\"player test...\" src=\"data:image/png;base64,{{msg.payload}}\" />\n","storeOutMessages":true,"fwdInMessages":true,"x":1170.666690826416,"y":252.66667580604553,"wires":[[]]},{"id":"a062292a.40c2b8","type":"file in","z":"f711b886.a58148","name":"Read image file","filename":"","format":"","x":825.6666259765625,"y":252.66665649414062,"wires":[["3d6ef454.a06c7c"]]},{"id":"ce97ed94.7aa07","type":"ui_group","z":"","name":"Magneet contacten","tab":"d16565fd.7f88d8","order":1,"disp":true,"width":"6"},{"id":"d16565fd.7f88d8","type":"ui_tab","z":"","name":"Sensoren","icon":"input","order":3}]

Then the image files themselves are loaded, based on the image file references in the TOC file. The last node displays the image animation in the Node-Red dashboard:

image

P.S. This way of working also allows cleaning up the disc in background. A background job could delete images (together with their TOC file) as soon as disc space need to be freed, without causing any conflicts later on in my Node-Red flow ...

Sequencer enhancement request

To keep my flow simple, I have manipulated the Recorder code locally.

Currently the Recorder can be stopped (and send a TOC on the output port) in two different ways:

However the Recorder (if I'm not mistaken) can only be started in a single way:

I worked around this by executing following code in my Chrome debugger (when the first message arrives): image

Hope to hear from you soon. Bart

tedstriker commented 7 years ago

Hi Bart,

I'm happy the "reference" solution suites your needs.

The recorder already has two record modes. Check "start immediately" to, you guessed it, start the recording when msg.start is received. Otherwise the recorder waits for the first message after msg.start has been received to start recording. If I got you right, that's exactly what you were asking for.

screenshot
bartbutenaers commented 7 years ago

Hello Ted,

Yes I had already selected that checkbox, however it doesn't seem to record anything.

When my first message arrives (which contains no start command), I can see in the Chrome debugger that it goes directly to the recordElement (which is fine):

image

However once arrived there, nothing is recorded since all booleans are false (it goes directly to the end of the method):

image

So nothing is recorded. Same for the next messages that arrive. That is not the behaviour that I expected. Could you please have a look a this, because from your explanation I assume that it works fine when you use it ??

Please let me know if you need extra information!

Kind regards, Bart

tedstriker commented 7 years ago

At the moment I don't use the sequencer, but I remember my intentions. To get the desired behavior do the following:

  1. uncheck "start immediately" because you don't want to start immediately, you want a delayed start.
  2. send a message containing a start property (msg.start) to "arm" the recorder. You can also see the record indicator at the node being a circle (armed) or a filled dot (recording).
  3. send the messages you want to record. The recording starts with the first message after msg.start

This difference of a checked/unchecked "start immediately" checkbox is the behavior after a msg.start has been received. checked means, the sequence starts immediately after msg.start. unchecked means, the sequence starts with the first message after msg.start

bartbutenaers commented 7 years ago

Hi Ted,

Thank for all the spare time you have spend to this issue. Now I can at least record my camera images and replay them afterwards. Great contribution!

Greetings from Belgium, Bart