Only if they are entered when a new item is added.
Hmm, that gives me a completely different idea. Using that process to drive a more generic mobile UI with an easy connection to Node-RED. Some form of mobile UI-builder would be needed of course but that could be mostly, if not all, an offline capable mobile web UI.
Could the QR code generator be turned into something generic with settings so you could link it to anything?
Ah, that's a shame, I was hoping that they would do it automatically - I'm too lazy to put expiry dates in manually. ![]()
Hmm, that gives me a completely different idea. Using that process to drive a more generic mobile UI with an easy connection to Node-RED. Some form of mobile UI-builder would be needed of course but that could be mostly, if not all, an offline capable mobile web UI.
Julian,
Feel free to peak behind the scenes of the BARRED Native app ![]()
Enrolment Barcode Code Scanned: node-red-barred/Mobile Application Project/Barred/Barred Client/Enrol.xaml.cs at 142334951e70aefbfd135a9e41f1a3e59c79e967 · marcus-j-davies/node-red-barred · GitHub
Casts the Scanned barcode to this object (invitation): node-red-barred/Mobile Application Project/Barred/Barred Client/Invitiation.cs at 142334951e70aefbfd135a9e41f1a3e59c79e967 · marcus-j-davies/node-red-barred · GitHub
The Invitation object is JSONd/GZIpped/Base64d to fit in a small enough QR Code, which is later casted to a C# equivalent object
The invitation object, is then used throughout this source file (the main scanner screen) : node-red-barred/Mobile Application Project/Barred/Barred Client/Scanner.xaml.cs at main · marcus-j-davies/node-red-barred · GitHub
.NET/C# Socket IO
Feel free to peak behind the scenes of the BARRED Native app
Ah, well, therein lies a problem. I can't do mobile apps. I think native Android requires Java (which I detest and refuse to use) and iOS requires whatever dastardly language Apple have foisted on the world which not only do I refuse to use but I also refuse to pay them for the privilege and refuse to buy a Mac just for that. ![]()
And I would want something that works both on Apple and Android. Web is the only game in town I'm afraid.
For sure, I could come up with nodes to build a dynamic web UI along with the appropriate web manifest and workers. But the rest would need some help.
Was more to show the inner workings
- but yes, I hear you - the development setup is a little tedious
simply using a retained context variable should be more than sufficient.
You are such a spoilsport! ![]()
Was more to show the inner workings
I will add to my ever growing list of things to look at when I have more time!
Just started putting some ideas together for a Spice Monitor using SQLite as the dB.
I'll publish the code/flow once it's working relieably.
Looking good Dave!
@marcus-j-davies we need Photo and OCR extensions too, pretty please! ![]()
Photo and OCR extensions
Oooh, the ability to photo something like a broccoli head or a loose pepper and have it recognised by the picture? Yes please.
![]()
we need Photo and OCR extensions too
I think the idea was to avoid the need for manual entry of new items ?
photo something like a broccoli head
What if you had 3 of these with different expiry ![]()
I think the idea was to avoid the need for manual entry of new items ?
Indeed, although Julian's thoughts are another possibility!
Just finished building my 'Spice Monitor' - all seems to work for @ghayne and myself.
[{"id":"spices_flow","type":"tab","label":"Spice Tracker (BARRED)","disabled":false,"info":""},{"id":"70360db4e5317c10","type":"group","z":"spices_flow","name":"","style":{"fill":"#e3f3d3","label":true},"nodes":["6164d849e91285a1","b2ca26bd9363230f","33d6018d69053bfa","398bbdb5714fcbf1","a1dab0ec604a7010","585a6d354058f3ac","876a3389a414240f"],"x":34,"y":119,"w":612,"h":262},{"id":"8955cf2843bd4469","type":"group","z":"spices_flow","name":"","style":{"fill":"#e3f3d3","label":true},"nodes":["sqlite_node","debug_out","a5f931c68b82a703","cad5db41fc8a40f9","1f01515d56b50e5a","15ae5f0567797614","3022635ec3ba0ad4","083a74ebdc9372e0","0e215495b8dcee17","a33316d9b35d7d78","016c14abdb7c8e7d","6e1546f86e0e0477","c3cfd5458e5ac253","f2b82b8ff60eb8f0","4e6e60955798706e"],"x":34,"y":439,"w":1012,"h":382},{"id":"sqlite_node","type":"sqlite","z":"spices_flow","g":"8955cf2843bd4469","mydb":"sqlite_config","sqlquery":"msg.topic","sql":"","name":"SQLite Spices","x":680,"y":640,"wires":[["6e1546f86e0e0477","debug_out"]]},{"id":"debug_out","type":"debug","z":"spices_flow","g":"8955cf2843bd4469","name":"Output","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":850,"y":700,"wires":[]},{"id":"6164d849e91285a1","type":"function","z":"spices_flow","g":"70360db4e5317c10","name":"SELECT * FROM spices ORDER by name","func":"msg.topic = `SELECT * FROM spices ORDER BY name`;\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":410,"y":280,"wires":[["sqlite_node"]]},{"id":"b2ca26bd9363230f","type":"inject","z":"spices_flow","g":"70360db4e5317c10","name":"SELECT","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"1","payloadType":"num","x":140,"y":280,"wires":[["6164d849e91285a1"]]},{"id":"a5f931c68b82a703","type":"barred-action","z":"spices_flow","g":"8955cf2843bd4469","name":"Incoming Actions","stack":"5eb95b3f58db0bdd","x":140,"y":580,"wires":[["cad5db41fc8a40f9","016c14abdb7c8e7d"]]},{"id":"cad5db41fc8a40f9","type":"debug","z":"spices_flow","g":"8955cf2843bd4469","name":"Incoming","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":320,"y":540,"wires":[]},{"id":"1f01515d56b50e5a","type":"barred-result","z":"spices_flow","g":"8955cf2843bd4469","name":"Send Results to scanner","defaultStatus":"OK","x":910,"y":560,"wires":[[]]},{"id":"15ae5f0567797614","type":"barred-item","z":"spices_flow","g":"8955cf2843bd4469","name":"Incoming Items","stack":"5eb95b3f58db0bdd","x":140,"y":720,"wires":[["3022635ec3ba0ad4"]]},{"id":"3022635ec3ba0ad4","type":"switch","z":"spices_flow","g":"8955cf2843bd4469","name":"","property":"menu_option","propertyType":"flow","rules":[{"t":"eq","v":"A","vt":"str"},{"t":"eq","v":"D","vt":"str"}],"checkall":"true","repair":false,"outputs":2,"x":310,"y":720,"wires":[["083a74ebdc9372e0","c3cfd5458e5ac253"],["0e215495b8dcee17"]]},{"id":"083a74ebdc9372e0","type":"debug","z":"spices_flow","g":"8955cf2843bd4469","name":"A","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":470,"y":740,"wires":[]},{"id":"0e215495b8dcee17","type":"debug","z":"spices_flow","g":"8955cf2843bd4469","name":"D","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":470,"y":780,"wires":[]},{"id":"a33316d9b35d7d78","type":"comment","z":"spices_flow","g":"8955cf2843bd4469","name":"First stab at Spice Monitor","info":"","x":170,"y":480,"wires":[]},{"id":"fc068410fbc9e879","type":"comment","z":"spices_flow","name":"First stab at SPICE RACK for Garry Hayne","info":"","x":200,"y":80,"wires":[]},{"id":"016c14abdb7c8e7d","type":"function","z":"spices_flow","g":"8955cf2843bd4469","name":"Linear Sequence Machine (LSM)","func":"flow.set(\"menu_option\", msg.payload.action.name);\n\nif (msg.payload.action.name == 'DEFAULT') {\n msg.status = 'MENU' // Menu Trigger\n msg.topic = 'Spices Menu' // Menu Title\n msg.payload = {\n 'List': { action: 'L', scan: false },\n 'Add new item': { action: 'A', scan: true },\n 'Check expiry': { action: 'C', scan: false },\n 'Delete an item ': { action: 'D', destructive: true, scan: true }\n }\n return [msg, null]\n}\n\n\nelse if (msg.payload.action.name == \"L\") {\n msg.topic = `SELECT * FROM spices ORDER BY name`;\n return [null, msg]\n}\n\nelse if (msg.payload.action.name == \"A\") {\n const Barcode = msg.payload.barcode.barcode;\n\n msg.status = 'INFO'\n msg.payload = {\n Name: 'string',\n Quantity: 'string',\n Expiry_date: 'date',\n Barcode: Barcode\n }\n return [msg, null];\n\n}\n\nelse if (msg.payload.action.name == \"C\") {\n msg.topic = \"SELECT * FROM spices WHERE expiry <= date('now', '+7 days') ORDER BY expiry\";\n return [null, msg];\n}\n\nelse if (msg.payload.action.name == \"D\") {\n\n flow.set(\"barcode\", msg.payload.barcode.barcode);\n\n msg.status = 'MENU' \n msg.topic = 'Delete barcode ?' \n msg.payload = {\n 'Yes': { action: 'Y', scan: false },\n 'No': { action: 'N', scan: false }\n }\n return [msg, null];\n\n}\n\nelse if (msg.payload.action.name == \"N\") {\n msg.payload = \"OK - action ignored\";\n return [msg, null];\n}\n\nelse if (msg.payload.action.name == \"Y\") {\n const barcode = flow.get(\"barcode\");\n msg.topic = `DELETE FROM spices WHERE barcode='${barcode}'`;\n return [null, msg];\n}\n","outputs":2,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":400,"y":580,"wires":[["1f01515d56b50e5a"],["sqlite_node"]],"outputLabels":["send_result","sqlite"]},{"id":"6e1546f86e0e0477","type":"function","z":"spices_flow","g":"8955cf2843bd4469","name":"Format dB results","func":"// Get the length of the results array from the MySQL query\n\nlet sql = msg.topic;\n\n// Make sure msg.topic exists\nif (sql && sql.toUpperCase().includes(\"DELETE\")) {\n // Extract barcode from SQL string\n let match = sql.match(/barcode\\s*=\\s*'(\\d+)'/i);\n\n if (match) {\n let barcode = match[1]; // e.g. \"88888\"\n msg.payload = `Item with barcode ${barcode}\\nhas been deleted.`;\n } else {\n msg.payload = \"Item deleted (barcode not found).\";\n }\n return msg;\n}\n\nelse {\n\n let length = msg.payload.length;\n\n node.status({ text: \"Rows returned = \" + length });\n\n let action = flow.get(\"menu_option\");\n\n let ap_info = \"\"\n\n if (action == \"L\") {\n\n ap_info = \"*** Spice Rack inventory ***\\r\\n\\r\\n\";\n\n if (length == 0) {\n ap_info += \"Spice Rack appears to be empty\"\n }\n\n // Construct main body of the message\n for (let row = 0; row < length; row++) {\n ap_info += \"Name: \" + msg.payload[row].name + \"\\r\\n\";\n ap_info += \"Barcode: \" + msg.payload[row].barcode + \"\\r\\n\";\n ap_info += \"Quantity: \" + msg.payload[row].quantity + \"\\r\\n\";\n ap_info += \"Expiry date: \" + msg.payload[row].expiry + \"\\r\\n\\r\\n\";\n }\n\n msg.payload = ap_info;\n return msg;\n }\n\n else if (action == \"C\") {\n ap_info = \"*** Items expiring soon ***\\r\\n\\r\\n\";\n\n if (length == 0) {\n ap_info += \"Nothing expiring soon\"\n }\n\n // Construct main body of the message\n for (let row = 0; row < length; row++) {\n ap_info += \"Name: \" + msg.payload[row].name + \"\\r\\n\";\n ap_info += \"Barcode: \" + msg.payload[row].barcode + \"\\r\\n\";\n ap_info += \"Quantity: \" + msg.payload[row].quantity + \"\\r\\n\";\n ap_info += \"Expiry date: \" + msg.payload[row].expiry + \"\\r\\n\\r\\n\";\n }\n\n msg.payload = ap_info;\n return msg;\n\n }\n}","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":890,"y":640,"wires":[["1f01515d56b50e5a"]]},{"id":"c3cfd5458e5ac253","type":"function","z":"spices_flow","g":"8955cf2843bd4469","name":"INSERT new item","func":"let item = msg.payload.item;\n\n// Prepare SQL query for output 2\nlet sqlMsg = {\n topic: `INSERT INTO spices (name, quantity, expiry, barcode)\n VALUES ('${item.Name}', '${item.Quantity}', date(${item.Expiry_date / 1000}, 'unixepoch'), '${item.Barcode}')`\n};\n\n// Prepare confirmation message for output 1\nlet payloadMsg = {\n payload: `The item ${item.Name}\\nwith barcode ${item.Barcode}\\nhas been stored.`\n};\n\n// Return messages in the order of outputs\nreturn [payloadMsg, sqlMsg];\n","outputs":2,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":490,"y":700,"wires":[["f2b82b8ff60eb8f0","4e6e60955798706e"],["sqlite_node"]]},{"id":"33d6018d69053bfa","type":"function","z":"spices_flow","g":"70360db4e5317c10","name":"Delete all rows in table 'spices'","func":"msg.topic = `DELETE FROM spices`;\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":370,"y":340,"wires":[["sqlite_node"]]},{"id":"398bbdb5714fcbf1","type":"inject","z":"spices_flow","g":"70360db4e5317c10","name":"DELETE","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"1","payloadType":"num","x":140,"y":340,"wires":[["33d6018d69053bfa"]]},{"id":"f2b82b8ff60eb8f0","type":"debug","z":"spices_flow","g":"8955cf2843bd4469","name":"INSERT","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":700,"y":780,"wires":[]},{"id":"a1dab0ec604a7010","type":"comment","z":"spices_flow","g":"70360db4e5317c10","name":"This is for TESTING and creating the 'spices' table on the SQLite dB demo.db","info":"","x":330,"y":160,"wires":[]},{"id":"4e6e60955798706e","type":"barred-send-item","z":"spices_flow","g":"8955cf2843bd4469","name":"Send to Scanners","stack":"5eb95b3f58db0bdd","x":890,"y":740,"wires":[[]]},{"id":"adf9f60103303bea","type":"comment","z":"spices_flow","name":"Added extra 'flow' to confirm delete an item","info":"","x":580,"y":80,"wires":[]},{"id":"876a3389a414240f","type":"function","z":"spices_flow","g":"70360db4e5317c10","name":"CREATE TABLE IF NOT EXISTS spices","func":"msg.topic = `CREATE TABLE IF NOT EXISTS spices (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT,\n quantity TEXT,\n expiry DATE,\n barcode TEXT\n)`;\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":460,"y":220,"wires":[["sqlite_node"]]},{"id":"585a6d354058f3ac","type":"inject","z":"spices_flow","g":"70360db4e5317c10","name":"Create 'spices' table","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"1","payloadType":"num","x":170,"y":220,"wires":[["876a3389a414240f"]]},{"id":"b69bb2bee3a0ddc9","type":"comment","z":"spices_flow","name":"This flow uses SQLite with a database at: /home/pi/demo.db","info":"","x":900,"y":320,"wires":[]},{"id":"sqlite_config","type":"sqlitedb","db":"/home/pi/demo.db","mode":"RWC"},{"id":"5eb95b3f58db0bdd","type":"barred-config","name":"BARRED Config","department":"Spice Monitor","color":"#008040","rate":"3000","rtimeout":"1000","scanners":{"646af871-ccf6-476f-b781-283beb1cd691":{"index":0,"scannerLabel":"Scanner A","scannerId":"646af871-ccf6-476f-b781-283beb1cd691"}},"host":"192.168.1.152","port":"50031"}]
The flow uses SQLite so the database is located locally on your Raspberry Pi.
What if you had 3 of these with different expiry
Well, they don't put expiry dates on such things in the UK any more so I just want to use the oldest first - I might keep some stickers around to help with that. ![]()
I might keep some stickers around to help with that.
Laser engraver, very good for broccoli stems too! ![]()
we need Photo and OCR extensions too, pretty please!
Don't forget RFID - that's the current "thing" in industry I believe. It also individualises the items.
I my linkedin today I found this video explaining how RFID is used in a fashion store --> https://www.youtube.com/watch?v=tghdauI1g-Y
Laser engraver, very good for broccoli stems too!
In that case you can add a barcode to your vegetables ![]()
Nice addition of the "Create Table" Inject node!
Don't forget RFID - that's the current "thing" in industry I believe. It also individualises the items.
It's been on my mind to tap into the available API's on the mobile - but that is for another day currently. if I do, the same mechanisms will be at play - send the RFID data over the socket, and respond accordingly.
The API's are there (per platform) just need to educate myself around them all.





