Limitations of node-RED or just me not searching correctly?

562 views
Skip to first unread message

Adam S

unread,
Apr 28, 2015, 9:17:58 AM4/28/15
to node...@googlegroups.com
I'll start off with what I'm trying to do.  I'm trying to add in a button that will read a drop down from an HTML page and update a global variable to disable (set to 0) my PWM fan and the drop down should be updated to display the current global variable when the page is loaded.  Currently I'm using an HTTP GET node with a template node to create a page. 

Is it possible to call some sort of function in the JavaScript code to send data directly back to a flow?  Maybe I just need to know what the options are for sending data back and forth from an HTML page and flow.  I know this isn't WebIOPI (and that is probably the source of my confusion), but in WebIOPI there were Python macros which you could call from JavaScript to update you page based on any JavaScript functions or output from Python.  The main thing I liked about WebIOPI was the ease of passing data between Python and JavaScript.  The overall ease of node-RED is spectacular, but when you get to mixing and matching data around it feels like it is a bit more closed than WebIOPI.  I guess like the old saying goes...you can't have your cake and eat it too.  Maybe what I really need to do is have a separate web server and pass data between the 2 web servers (is this even possible)?  From what I have gathered so far I feel like the pages that are able to be made with the HTTP GET nodes are too basic for what I want to do.

You can see what I have below.  I am experimenting with the template node, but it doesn't seem to accept JavaScript functions in the header, and I can't see any way to call a function node from the web page I have created there.  Typing in "function test(){document.getElementByName("fan_status").value = "Active"}" just prints that line at the top of the page.  I'm sure there is a way to do this I just haven't been able to figure it out yet.  Relating to this, how do you read values from a post?  For example, after I hit the "Update Fan Status" button, how to I then read the value of the "fan_status" drop down in the post flow?

On an unrelated note, is anyone here outputting data to a database using a pi?  I would like to store some of this data in a database to create graphs on temps.  I have seen MongoDB mentioned but googleing suggested that it is not ready for the pi yet.  Any suggestions there are appreciated.

Here is my current flow:
[{"id":"e00a1942.7d2d4","type":"mqtt-broker","broker":"localhost","port":"1883","clientid":""},{"id":"b3bb40e2.2d92","type":"http in","name":"","url":"/temp","method":"get","x":65,"y":344,"z":"abafa038.54fa88","wires":[["61d29d3b.18a4cc"]]},{"id":"ca26a7dc.33f038","type":"http response","name":"","x":735.5,"y":344,"z":"abafa038.54fa88","wires":[]},{"id":"61d29d3b.18a4cc","type":"function","name":"Get Current Values","func":"msg.current = {\n    insidetemp: context.global.insidetemp,\n    outsidetemp: context.global.outsidetemp\n}\nreturn msg;","outputs":1,"valid":true,"x":288,"y":344,"z":"abafa038.54fa88","wires":[["76e37074.8e19a8"]]},{"id":"76e37074.8e19a8","type":"template","name":"","field":"payload","format":"handlebars","template":"<html>\n<head>\n</head>\n<body>\n    <form method=\"POST\" action=\"/disablefans\">\n        <p>Inside Temp: <input name=\"insidetemp\" value=\"{{current.insidetemp}}\"></p>\n        <p>Outside Temp: <input name=\"outsidetemp\" value=\"{{current.outsidetemp}}\"></p>\n        <p>Fan Status: <select name=\"fan_status\"><option value=\"Disabled\">Disabled</option><option value=\"Active\">Active</option></select></p>\n        <p><input type=\"submit\" value=\"Update Temp\" onclick=\"history.go(0)\"><input type=\"submit\" value=\"Update Fan Status\"></p>\n    </form>\n</body>\n</html>","x":525.5,"y":344,"z":"abafa038.54fa88","wires":[["ca26a7dc.33f038"]]},{"id":"b3231d0a.18df28","type":"function","name":"Set Values/Update Fan","func":"var fahrenheit = msg.payload * (9/5) + 32;\ncontext.global.fanspeed;\n\ncontext.global.insidetemp = fahrenheit;\ncontext.global.fansdisabled = false;\n\nif (context.global.fansdisabled == false){\n    if (fahrenheit < 70){\n        context.global.fanspeed = 0;\n    }\n    if (fahrenheit >= 70 && fahrenheit < 72){\n        context.global.fanspeed = 15;\n    }\n    if (fahrenheit >= 72 && fahrenheit < 74){\n        context.global.fanspeed = 30;\n    }\n    if (fahrenheit >= 74 && fahrenheit < 76){\n        context.global.fanspeed = 45;\n    }\n    if (fahrenheit >= 76 && fahrenheit < 78){\n        context.global.fanspeed = 60;\n    }\n    if (fahrenheit >= 78 && fahrenheit < 80){\n        context.global.fanspeed = 75;\n    }\n    if (fahrenheit >= 80 && fahrenheit < 82){\n        context.global.fanspeed = 90;\n    }\n    if (fahrenheit >= 82){\n        context.global.fanspeed = 100;\n    }\n}\nelse{\n    context.global.fanspeed = 0;\n}\n\nmsg.current = {\n    fanspeed: context.global.fanspeed,\n    insidetemp: context.global.insidetemp\n}\n\nmsg.payload = context.global.fanspeed\n\nreturn msg;","outputs":1,"valid":true,"x":300,"y":156,"z":"abafa038.54fa88","wires":[["69a1289e.ec28c","c9b4cb6e.c568a8"]]},{"id":"177e6002.bff4e8","type":"ds18b20","name":"Shed Inisde","sensorid":"28-000006154ba3","timer":".3","x":78,"y":156,"z":"abafa038.54fa88","wires":[["b3231d0a.18df28"]]},{"id":"b647f4e6.6196f8","type":"debug","name":"","active":false,"console":"false","complete":"false","x":721,"y":88,"z":"abafa038.54fa88","wires":[]},{"id":"6d56f263.9f937c","type":"function","name":"Set Values","func":"var fahrenheit = msg.payload * (9/5) + 32;\n\ncontext.global.outsidetemp = fahrenheit;\n\nmsg.current = {\n    outsidetemp: context.global.outsidetemp\n}\nreturn msg;","outputs":1,"valid":true,"x":263,"y":223,"z":"abafa038.54fa88","wires":[["790f4fa7.5fe4f"]]},{"id":"16104bda.b13fdc","type":"ds18b20","name":"Shed Outside","sensorid":"28-000006155474","timer":".3","x":82,"y":223,"z":"abafa038.54fa88","wires":[["6d56f263.9f937c"]]},{"id":"b93bf43d.ad6c8","type":"debug","name":"","active":false,"console":"false","complete":"false","x":717,"y":287,"z":"abafa038.54fa88","wires":[]},{"id":"69a1289e.ec28c","type":"rpi-gpio out","name":"","pin":"22","set":"","level":"0","out":"pwm","x":542,"y":87,"z":"abafa038.54fa88","wires":[]},{"id":"c9b4cb6e.c568a8","type":"template","name":"","field":"payload","format":"handlebars","template":"Current fan speed: {{current.fanspeed}} Current inside temp: {{current.insidetemp}}","x":532,"y":155,"z":"abafa038.54fa88","wires":[["b647f4e6.6196f8","be901cc1.2c2b1"]]},{"id":"790f4fa7.5fe4f","type":"template","name":"","field":"payload","format":"handlebars","template":"Current outside temp: {{current.outsidetemp}}","x":530,"y":223,"z":"abafa038.54fa88","wires":[["b93bf43d.ad6c8","996c686c.e78c58"]]},{"id":"be901cc1.2c2b1","type":"mqtt out","name":"","topic":"fans","qos":"","retain":"","broker":"e00a1942.7d2d4","x":738,"y":155,"z":"abafa038.54fa88","wires":[]},{"id":"996c686c.e78c58","type":"mqtt out","name":"","topic":"fans","qos":"","retain":"","broker":"e00a1942.7d2d4","x":736,"y":223,"z":"abafa038.54fa88","wires":[]},{"id":"7c376171.5b3758","type":"http in","name":"","url":"/disablefans","method":"post","x":95.5,"y":28,"z":"abafa038.54fa88","wires":[["cb0d7eb7.90fb98"]]},{"id":"9ee21763.3627f","type":"function","name":"Redirect back to /temp","func":"msg.res.redirect(\"/temp\");\n","outputs":"0","valid":true,"x":695,"y":28,"z":"abafa038.54fa88","wires":[]},{"id":"cb0d7eb7.90fb98","type":"function","name":"Set Values/Update Fan","func":"context.global.fanspeed;\ncontext.global.fansdisabled;\n\nif (context.global.fansdisabled == true){\n    msg.current = {\n        fanspeed: 0,\n        insidetemp: context.global.insidetemp\n    }\n}\nelse{\n    msg.current = {\n        fanspeed: context.global.fanspeed,\n        insidetemp: context.global.insidetemp\n    }\n}\n\nmsg.payload = context.global.fanspeed;\n\nreturn msg;","outputs":1,"valid":true,"x":300,"y":28,"z":"abafa038.54fa88","wires":[["9ee21763.3627f","69a1289e.ec28c"]]}]

Julian Knight

unread,
Apr 28, 2015, 12:11:49 PM4/28/15
to node...@googlegroups.com
Hi Adam, if I've read you correctly, you want to display a web page populated with initial data and then have an interactive way of passing information back and forth between the web page and NR?

If I'm correct, there are two basic approaches that you might take (and probably many others that I haven't thought about but Nick and Dave will doubtless correct me!).

The simplest is to implement a websocket in NR and subscribe to that from your web page. You can then use the socket to pass messages back and forth. I've been experimenting with a small set of helper code to automatically do that (well the web page part is automatic anyway). My version uses JQuery to listen for any changes to input elements on the page and forwards those to the websocket. NR then listens to the socket and you create flows to process the inbound information. similarly, you can do that in reverse, creating a process in the web page for receiving data over the socket and updating the page. Doing this using your own code is OK for simple web pages/sites but quickly becomes difficult to manage with larger sets of pages which is when you would turn to a front-end framework such as Backbone, Angular or Polymer or some such that has the capability to make the DOM more dynamic.

The second approach is similar but more flexible. Instead of using raw websockets, if you have an MQTT server that supports websockets, you can use the Paho library to manage the connection and push everything over MQTT. This is more flexible since you can dynamically set up new topics with MQTT, you can't do that with raw websockets so easily. One downside of this approach is that you need a websocket enabled version of an MQTT broker. Mosca does this out of the box but is relatively resource hungry. Mosquitto can do it but you may need to compile it yourself for use on the Pi.

Don't forget to take into account security though if you are allowing access outside a local, wired network. Fully securing websockets can be a little involved.

Regarding db's on the Pi. Yes, I do that. However, I have the same issue as you, I started using MongoDB on my Synology NAS before realising that I'd rather overcommitted the NAS and it was struggling! MongoDB certainly isn't ready for ARM platforms and isn't likely to be really ready for some time as it looks like it requires some fairly hefty internal rewriting. So my MongoDB is still on the NAS with all my other processing - NR & Mosquitto - on the Pi2.

I'm currently looking again at RethinkDB to see how feasible that is to use on a Pi. It certainly has some nice features. Otherwise, CouchDB is OK for some things and has a REST interface so shouldn't be too hard to integrate. SQLite should be good for tabular data. Really depends on what you want to do.


On Tuesday, 28 April 2015 14:17:58 UTC+1, Adam S wrote:
I'll start off with what I'm trying to do.  I'm trying to add in a button that will read a drop down from an HTML page and update a global variable to disable (set to 0) my PWM fan and the drop down should be updated to display the current global variable when the page is loaded.  Currently I'm using an HTTP GET node with a template node to create a page. 

....

Adam S

unread,
Apr 28, 2015, 1:48:10 PM4/28/15
to node...@googlegroups.com
Thanks for you reply Julian.  Do you have any examples of the websockets and MQTT websockets you can share?

In my attempt to find how variables are passed with POST, I found out through a debug node that they are just a regular old payload so I'm good there.

I am still hitting a wall where I cant do what I want, mainly because of how the HTML drop downs work.  I want my drop down to default to whatever the current value is and I can't do that without some sort of JavaScript support in the template node.  Is the template node editable to allow this, or is there some other node I should be using?  I thought I could get away with <p>Fan Status: <select name="fan_status" value="{{current.fansdisabled}}"><option value="Disabled">Disabled</option><option value="Active">Active</option></select></p> but that did not work either.  Is Here is what I have now:

[{"id":"e00a1942.7d2d4","type":"mqtt-broker","broker":"localhost","port":"1883","clientid":""},{"id":"b3bb40e2.2d92","type":"http in","name":"","url":"/temp","method":"get","x":63.5,"y":416,"z":"abafa038.54fa88","wires":[["61d29d3b.18a4cc"]]},{"id":"ca26a7dc.33f038","type":"http response","name":"","x":734,"y":416,"z":"abafa038.54fa88","wires":[]},{"id":"61d29d3b.18a4cc","type":"function","name":"Get Current Values","func":"msg.current = {\n    insidetemp: context.global.insidetemp,\n    outsidetemp: context.global.outsidetemp,\n    fansdisabled: context.global.fansdisabled\n}\nreturn msg;","outputs":1,"valid":true,"x":286.5,"y":416,"z":"abafa038.54fa88","wires":[["76e37074.8e19a8"]]},{"id":"76e37074.8e19a8","type":"template","name":"","field":"payload","format":"handlebars","template":"<html>\n<head>\n</head>\n<body>\n    <form method=\"POST\" action=\"/disablefans\">\n        <p>Inside Temp: <input name=\"insidetemp\" value=\"{{current.insidetemp}}\"></p>\n        <p>Outside Temp: <input name=\"outsidetemp\" value=\"{{current.outsidetemp}}\"></p>\n        <p>Fan Status: <select name=\"fan_status\" value=\"{{current.fansdisabled}}\"><option value=\"Disabled\">Disabled</option><option value=\"Active\">Active</option></select></p>\n        <p><input type=\"submit\" value=\"Update Fan Status\"></p>\n    </form>\n    <form method=\"POST\" action=\"/updatetemp\">\n        <p><input type=\"submit\" value=\"Update Temp\" onclick=\"history.go(0)\"></p>\n    </form>\n</body>\n</html>","x":524,"y":416,"z":"abafa038.54fa88","wires":[["ca26a7dc.33f038"]]},{"id":"b3231d0a.18df28","type":"function","name":"Set Values/Update Fan","func":"var fahrenheit = msg.payload * (9/5) + 32;\ncontext.global.fanspeed;\n\ncontext.global.insidetemp = fahrenheit;\n\nif (context.global.fansdisabled == \"Active\"){\n    if (fahrenheit < 70){\n        context.global.fanspeed = 0;\n    }\n    if (fahrenheit >= 70 && fahrenheit < 72){\n        context.global.fanspeed = 15;\n    }\n    if (fahrenheit >= 72 && fahrenheit < 74){\n        context.global.fanspeed = 30;\n    }\n    if (fahrenheit >= 74 && fahrenheit < 76){\n        context.global.fanspeed = 45;\n    }\n    if (fahrenheit >= 76 && fahrenheit < 78){\n        context.global.fanspeed = 60;\n    }\n    if (fahrenheit >= 78 && fahrenheit < 80){\n        context.global.fanspeed = 75;\n    }\n    if (fahrenheit >= 80 && fahrenheit < 82){\n        context.global.fanspeed = 90;\n    }\n    if (fahrenheit >= 82){\n        context.global.fanspeed = 100;\n    }\n}\nif (context.global.fansdisabled == \"Disabled\"){\n    context.global.fanspeed = 0;\n}\n\nmsg.current = {\n    fanspeed: context.global.fanspeed,\n    insidetemp: context.global.insidetemp\n}\n\nmsg.payload = context.global.fanspeed\n\nreturn msg;","outputs":1,"valid":true,"x":298.5,"y":228,"z":"abafa038.54fa88","wires":[["69a1289e.ec28c","c9b4cb6e.c568a8"]]},{"id":"177e6002.bff4e8","type":"ds18b20","name":"Shed Inisde","sensorid":"28-000006154ba3","timer":".3","x":76.5,"y":228,"z":"abafa038.54fa88","wires":[["b3231d0a.18df28"]]},{"id":"b647f4e6.6196f8","type":"debug","name":"","active":false,"console":"false","complete":"false","x":719.5,"y":160,"z":"abafa038.54fa88","wires":[]},{"id":"6d56f263.9f937c","type":"function","name":"Set Values","func":"var fahrenheit = msg.payload * (9/5) + 32;\n\ncontext.global.outsidetemp = fahrenheit;\n\nmsg.current = {\n    outsidetemp: context.global.outsidetemp\n}\nreturn msg;","outputs":1,"valid":true,"x":261.5,"y":295,"z":"abafa038.54fa88","wires":[["790f4fa7.5fe4f"]]},{"id":"16104bda.b13fdc","type":"ds18b20","name":"Shed Outside","sensorid":"28-000006155474","timer":".3","x":80.5,"y":295,"z":"abafa038.54fa88","wires":[["6d56f263.9f937c"]]},{"id":"b93bf43d.ad6c8","type":"debug","name":"","active":false,"console":"false","complete":"false","x":715.5,"y":359,"z":"abafa038.54fa88","wires":[]},{"id":"69a1289e.ec28c","type":"rpi-gpio out","name":"","pin":"22","set":"","level":"0","out":"pwm","x":540.5,"y":159,"z":"abafa038.54fa88","wires":[]},{"id":"c9b4cb6e.c568a8","type":"template","name":"","field":"payload","format":"handlebars","template":"Current fan speed: {{current.fanspeed}} Current inside temp: {{current.insidetemp}}","x":530.5,"y":227,"z":"abafa038.54fa88","wires":[["b647f4e6.6196f8","be901cc1.2c2b1"]]},{"id":"790f4fa7.5fe4f","type":"template","name":"","field":"payload","format":"handlebars","template":"Current outside temp: {{current.outsidetemp}}","x":528.5,"y":295,"z":"abafa038.54fa88","wires":[["b93bf43d.ad6c8","996c686c.e78c58"]]},{"id":"be901cc1.2c2b1","type":"mqtt out","name":"","topic":"fans","qos":"","retain":"","broker":"e00a1942.7d2d4","x":736.5,"y":227,"z":"abafa038.54fa88","wires":[]},{"id":"996c686c.e78c58","type":"mqtt out","name":"","topic":"fans","qos":"","retain":"","broker":"e00a1942.7d2d4","x":734.5,"y":295,"z":"abafa038.54fa88","wires":[]},{"id":"7c376171.5b3758","type":"http in","name":"","url":"/disablefans","method":"post","x":94,"y":100,"z":"abafa038.54fa88","wires":[["cb0d7eb7.90fb98"]]},{"id":"9ee21763.3627f","type":"function","name":"Redirect back to /temp","func":"msg.res.redirect(\"/temp\");\n","outputs":"0","valid":true,"x":693.5,"y":100,"z":"abafa038.54fa88","wires":[]},{"id":"cb0d7eb7.90fb98","type":"function","name":"Set Values/Update Fan","func":"context.global.fanspeed;\ncontext.global.fansdisabled;\n\nif (msg.payload.fan_status == \"Disabled\"){\n    msg.current = {\n        fanspeed: 0,\n        insidetemp: context.global.insidetemp\n    }\n    context.global.fanspeed = 0;\n    context.global.fansdisabled = \"Disabled\";\n}\nif (msg.payload.fan_status == \"Active\"){\n    msg.current = {\n        fanspeed: context.global.fanspeed,\n        insidetemp: context.global.insidetemp\n    }\n    context.global.fansdisabled = \"Active\";\n}\n\nmsg.payload = context.global.fanspeed;\n\nreturn msg;","outputs":1,"valid":true,"x":298.5,"y":100,"z":"abafa038.54fa88","wires":[["9ee21763.3627f","69a1289e.ec28c"]]},{"id":"dfc10029.7e1ec","type":"http in","name":"","url":"/updatetemp","method":"post","x":95,"y":36.883331298828125,"z":"abafa038.54fa88","wires":[["a56fe28c.7dd64"]]},{"id":"a56fe28c.7dd64","type":"function","name":"Redirect back to /temp","func":"msg.res.redirect(\"/temp\");\n","outputs":"0","valid":true,"x":694.5,"y":36.883331298828125,"z":"abafa038.54fa88","wires":[]}]

Mark Setrem

unread,
Apr 28, 2015, 2:26:47 PM4/28/15
to node...@googlegroups.com
If you take your function node "Get Current Values"  could you create an IF statement that builds just the html drop down list?

so something like...  (not checked for syntax but hopefully you get the gist! )

If (context.global.fansdisabled == "Disabled") { msg.formoptions = "<select name=\"fan_status\" ><option selected >Disabled</option><option >Active</option>" ;}
else { msg.formoptions = "<select name=\"fan_status\" ><option >Disabled</option><option selected >Active</option>" ;}

then replace the form option bit in your template with {{msg.formoptions}}


Toshi Bass

unread,
Apr 28, 2015, 2:56:20 PM4/28/15
to node...@googlegroups.com
Hi Adam

If your interested to try web sockets I followed this:   http://tech.scargill.net/a-node-red-websockets-web-page/   part 1 2 & 3 , nice example.

Toshi

Julian Knight

unread,
Apr 28, 2015, 3:01:53 PM4/28/15
to node...@googlegroups.com
Doh! I knew you'd ask. I've still been ironing out edge cases so I hadn't shared. And of course, I always make things too complex! Let me see if I can put something on the flows site or GitHub. I've examples of both.

Julian Knight

unread,
Apr 28, 2015, 3:04:01 PM4/28/15
to node...@googlegroups.com
Yes, Peter does it in a more standard fashion with an enter button and websocket. My example uses live updates. No buttons needed unless you want them.

Adam S

unread,
Apr 28, 2015, 3:14:01 PM4/28/15
to node...@googlegroups.com
I think I may be able to get away with Mark's suggestion (I'll test that and report back), but none the less, I want to learn about websockets as well.  I'll check out the link Toshi sent but if you want to share your code I am interested in it as well Julian.

Adam S

unread,
Apr 28, 2015, 3:28:14 PM4/28/15
to node...@googlegroups.com
I really like mark's suggestion, I never thought of that and it gives me a lot of ideas.  However, I ran in to a slight problem when I use it...  It seems that the template node somehow escapes the string so that it displays the string in the HTML instead of using it as HTML code.  This is how it is displayed in the HTML when I view the source: <p>Fan Status: &lt;select name=&quot;fan_status&quot; &gt;&lt;option &gt;Disabled&lt;&#x2F;option&gt;&lt;option selected &gt;Active&lt;&#x2F;option&gt;</p>  Is there any way to tell the template to keep its hands off my data?

Nicholas O'Leary

unread,
Apr 28, 2015, 3:30:30 PM4/28/15
to node...@googlegroups.com

Use {{{triple brackets}}}

Worth having a look at the mustache format documentation linked from the template node's help to see what else you can do.

Nick


On Tue, 28 Apr 2015 20:28 Adam S <ats1...@gmail.com> wrote:
I really like mark's suggestion, I never thought of that and it gives me a lot of ideas.  However, I ran in to a slight problem when I use it...  It seems that the template node somehow escapes the string so that it displays the string in the HTML instead of using it as HTML code.  This is how it is displayed in the HTML when I view the source: <p>Fan Status: &lt;select name=&quot;fan_status&quot; &gt;&lt;option &gt;Disabled&lt;&#x2F;option&gt;&lt;option selected &gt;Active&lt;&#x2F;option&gt;</p>  Is there any way to tell the template to keep its hands off my data?

--
http://nodered.org
---
You received this message because you are subscribed to the Google Groups "Node-RED" group.
To unsubscribe from this group and stop receiving emails from it, send an email to node-red+u...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Julian Knight

unread,
Apr 28, 2015, 3:45:40 PM4/28/15
to node...@googlegroups.com
OK, I'm persuaded! It is rather rough and ready, hope people can make sense of it. If not, let me know.

https://github.com/TotallyInformation/node-red-example-liveupdates

Julian Knight

unread,
Apr 29, 2015, 4:46:00 AM4/29/15
to node...@googlegroups.com
I've also added a somewhat simpler example of live sharing of data from Node-Red to a web page over MQTT.

https://github.com/TotallyInformation/node-red-example-debug

The example comes from a debugging setup I'm using to get round some of the limitations of the NR debug output.

Adam S

unread,
Apr 29, 2015, 8:27:37 AM4/29/15
to node...@googlegroups.com
Thanks for sharing Julian.  Mark's suggestion was one of those epiphany moments and I think I can get everything I want done by creating the HTML code in a function and passing the variables to the template.  I am still interested in the websockets for possibly making a mobile app, so when the time comes I will be looking in to that.

Shem Jamieson

unread,
Apr 29, 2015, 10:09:07 AM4/29/15
to node...@googlegroups.com
Thanks for the example flows Julian, an amazing head-start for anyone thinking of doing that!
Reply all
Reply to author
Forward
0 new messages