Import measurements from GOM Inspect XML
Automatically extract measurements, features and characteristics from XML written in GOM Inspect Exchange Format.
High-level process flow:
- Measure part in GOM Inspect
- Export XML with measurements to a network drive
- Node-RED monitors shared folder and kicks off the data load flow
- Read and parse XML
- Save parts, features, characteristics and measurements to SQL database
- Move XML file to archive folder
Required nodes
npm install node-red-contrib-readdir
npm install node-red-contrib-fs-ops
npm install node-red-contrib-re-postgres
Configuration
For details and sample SQL code go to our blog article.
Edit blue PostgreSQL nodes and enter your database connection information. Additionally, edit Inject nodes and change folder path to your XML files.
Create archive folder inside source folder.
General notes
File name should be in specific format: partNumber_partName.xml or partName.xml i.e. 9877212_Trunk lid.xml
Measurement date is based on file modify date.
You could add file watcher instead of an interval, however, depending on your operating system and where files are located it might not be supported or not working correctly. Pulling is a safe bet.
SQL code in the flow is written in a way that it won’t create duplicate parts, features, characteristics and measurements even if you reprocess the same file.
Released under MIT License
Copyright (c) 2020 Kenso Software, LLC
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
[{"id":"9e4dffea.e8e61","type":"tab","label":"GOM import","disabled":false,"info":"Automatically extract measurements, features and characteristics from XML written in GOM Inspect Exchange Format and save them in Postgres database. Visit https://blog.kensobi.com/ for details."},{"id":"cb0241a4.4559b","type":"inject","z":"9e4dffea.e8e61","name":"ScanBox1","props":[{"p":"dirPath","v":"/filedrop","vt":"str"},{"p":"archiveDirPath","v":"/filedrop/archive","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":100,"y":100,"wires":[["abb81dfe.435a2"]]},{"id":"abb81dfe.435a2","type":"fs-ops-dir","z":"9e4dffea.e8e61","name":"List XML files in dir","path":"dirPath","pathType":"msg","filter":"*.xml","filterType":"str","dir":"payload","dirType":"msg","x":310,"y":160,"wires":[["c840c1.e3918f4"]]},{"id":"c840c1.e3918f4","type":"fs-ops-stats","z":"9e4dffea.e8e61","name":"","path":"dirPath","pathType":"msg","filename":"payload","filenameType":"msg","stats":"stats","sizeType":"msg","x":500,"y":160,"wires":[["83b87c8f.a6c5f"]]},{"id":"551c71e8.49b08","type":"split","z":"9e4dffea.e8e61","name":"Split files","splt":"\\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","x":840,"y":160,"wires":[["d55f632f.930d4"]]},{"id":"b70f2bb5.a816f8","type":"template","z":"9e4dffea.e8e61","name":"Create part SQL","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"WITH new_parts AS (\n INSERT INTO parts (number, name)\n VALUES('{{payload.partNumber}}','{{payload.partName}}') \n ON CONFLICT(number) DO NOTHING\n RETURNING id\n) SELECT COALESCE(\n (SELECT id FROM new_parts),\n (SELECT id FROM parts WHERE number = '{{payload.partNumber}}')\n) AS partid;\n","output":"str","x":1220,"y":160,"wires":[["75d440f3.0c187"]]},{"id":"83b87c8f.a6c5f","type":"function","z":"9e4dffea.e8e61","name":"Get part info","func":"var partNumbers = [];\n\nfor (var i = 0; i < msg.payload.length; i++) {\n var part = msg.payload[i].substring(0,msg.payload[i].length-4);\n var partSplit = part.split(\"_\");\n \n partNumbers.push({\n partNumber: partSplit[0] ,\n partName: partSplit.length > 1 ? partSplit[1] : \"\",\n filename: msg.dirPath + \"/\" + msg.payload[i],\n file: msg.payload[i],\n modifyDate: new Date(msg.stats[i].mtimeMs).toISOString()\n });\n}\n\nmsg.payload = partNumbers;\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":670,"y":160,"wires":[["551c71e8.49b08"]]},{"id":"75d440f3.0c187","type":"postgres","z":"9e4dffea.e8e61","postgresdb":"701d5a17.0e1664","name":"Save part","output":true,"perrow":false,"rowspermsg":"1","outputs":1,"x":1400,"y":160,"wires":[["1c00210f.9f53ff"]]},{"id":"1c00210f.9f53ff","type":"change","z":"9e4dffea.e8e61","name":"Save partId","rules":[{"t":"set","p":"partId","pt":"msg","to":"payload[0].partid","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":330,"y":240,"wires":[["56e30574.de896c"]]},{"id":"56e30574.de896c","type":"file in","z":"9e4dffea.e8e61","name":"Read file","filename":"","format":"utf8","chunk":false,"sendError":false,"encoding":"utf8","x":500,"y":240,"wires":[["3f046e0e.4977c2"]]},{"id":"3f046e0e.4977c2","type":"xml","z":"9e4dffea.e8e61","name":"Parse XML","property":"payload","attr":"","chr":"","x":670,"y":240,"wires":[["790cf2af.a76bac"]]},{"id":"d55f632f.930d4","type":"change","z":"9e4dffea.e8e61","name":"Set part settings","rules":[{"t":"set","p":"filename","pt":"msg","to":"payload.filename","tot":"msg"},{"t":"set","p":"partNumber","pt":"msg","to":"payload.partNumber","tot":"msg"},{"t":"set","p":"file","pt":"msg","to":"payload.file","tot":"msg"},{"t":"set","p":"modifyDate","pt":"msg","to":"payload.modifyDate","tot":"msg"},{"t":"delete","p":"stats","pt":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1020,"y":160,"wires":[["b70f2bb5.a816f8"]]},{"id":"790cf2af.a76bac","type":"change","z":"9e4dffea.e8e61","name":"Get nominals","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload[\"gom\"].nominal[0]","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":850,"y":240,"wires":[["9246ba31.16fba8"]]},{"id":"9246ba31.16fba8","type":"split","z":"9e4dffea.e8e61","name":"Split by geometry prim. type","splt":"\\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"featureType","x":1080,"y":240,"wires":[["b4f9f0bf.b2a15"]]},{"id":"b4f9f0bf.b2a15","type":"split","z":"9e4dffea.e8e61","name":"Split by elements","splt":"\\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","x":1330,"y":240,"wires":[["992747c1.f22a68"]]},{"id":"992747c1.f22a68","type":"change","z":"9e4dffea.e8e61","name":"","rules":[{"t":"set","p":"featureName","pt":"msg","to":"payload.$.name","tot":"msg"},{"t":"set","p":"result","pt":"msg","to":"payload.result[0]","tot":"msg"},{"t":"set","p":"payload","pt":"msg","to":"payload.result[0]","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":340,"y":320,"wires":[["cc4eeaf2.0837d8"]]},{"id":"cc4eeaf2.0837d8","type":"template","z":"9e4dffea.e8e61","name":"Create feature SQL","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"WITH new_features AS (\n INSERT INTO features (partid, name, type)\n VALUES({{partId}},'{{featureName}}', '{{featureType}}') \n ON CONFLICT(partid, name) DO NOTHING\n RETURNING id\n) SELECT COALESCE(\n (SELECT id FROM new_features),\n (SELECT id FROM features WHERE partid = '{{partId}}' AND name = '{{featureName}}') \n) AS featureid;","output":"str","x":550,"y":320,"wires":[["4b64ddab.daf704"]]},{"id":"4b64ddab.daf704","type":"postgres","z":"9e4dffea.e8e61","postgresdb":"701d5a17.0e1664","name":"Save feature","output":true,"perrow":false,"rowspermsg":"1","outputs":1,"x":750,"y":320,"wires":[["309bcc00.3d7f04"]]},{"id":"fb2e3b42.b42608","type":"split","z":"9e4dffea.e8e61","name":"","splt":"\\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"topic","x":1130,"y":320,"wires":[["ba02d4a0.a9f388"]]},{"id":"ba02d4a0.a9f388","type":"switch","z":"9e4dffea.e8e61","name":"Filter tolerance category","property":"topic","propertyType":"msg","rules":[{"t":"neq","v":"all","vt":"str"}],"checkall":"true","repair":false,"outputs":1,"x":1330,"y":320,"wires":[["69035250.f636bc"]]},{"id":"69035250.f636bc","type":"switch","z":"9e4dffea.e8e61","name":"Filter out normal","property":"topic","propertyType":"msg","rules":[{"t":"neq","v":"normal","vt":"str"}],"checkall":"true","repair":false,"outputs":1,"x":1320,"y":380,"wires":[["cf4f9c0.dd38a68"]]},{"id":"cf4f9c0.dd38a68","type":"switch","z":"9e4dffea.e8e61","name":"Filter out inplane","property":"topic","propertyType":"msg","rules":[{"t":"neq","v":"inplane","vt":"str"}],"checkall":"true","repair":false,"outputs":1,"x":1320,"y":440,"wires":[["72b5882f.116ce8"]]},{"id":"72b5882f.116ce8","type":"change","z":"9e4dffea.e8e61","name":"","rules":[{"t":"set","p":"tolerance","pt":"msg","to":"payload[0].tolerance[0].$","tot":"msg"},{"t":"set","p":"nominal","pt":"msg","to":"payload[0].nominal_scalar[0].$.value","tot":"msg"},{"t":"set","p":"measured","pt":"msg","to":"payload[0].measured[0].$.value","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":340,"y":500,"wires":[["f0ccd31d.2a6e9"]]},{"id":"f0ccd31d.2a6e9","type":"template","z":"9e4dffea.e8e61","name":"Create characteristic and meas. SQL","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"INSERT INTO characteristics (featureId, name, nominal, usl, lsl)\nVALUES({{featureid}},'{{topic}}', {{nominal}}, {{tolerance.upper_limit}}, {{tolerance.lower_limit}}) \nON CONFLICT(featureid, name) DO NOTHING;\n\nINSERT INTO measurements (characteristicid, value, timestamp)\nVALUES((SELECT id FROM characteristics WHERE featureid = {{featureid}} AND name = '{{topic}}'),{{measured}}, '{{modifyDate}}') \nON CONFLICT(characteristicid, timestamp) DO NOTHING\n","output":"str","x":610,"y":500,"wires":[["69e55066.5749"]]},{"id":"309bcc00.3d7f04","type":"change","z":"9e4dffea.e8e61","name":"Reapply payload","rules":[{"t":"set","p":"featureid","pt":"msg","to":"payload[0].featureid","tot":"msg"},{"t":"set","p":"payload","pt":"msg","to":"result","tot":"msg"},{"t":"delete","p":"result","pt":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":950,"y":320,"wires":[["fb2e3b42.b42608"]]},{"id":"69e55066.5749","type":"postgres","z":"9e4dffea.e8e61","postgresdb":"701d5a17.0e1664","name":"Save characteristic and meas.","output":true,"perrow":false,"rowspermsg":"1","outputs":1,"x":930,"y":500,"wires":[["caab671d.a40108"]]},{"id":"85357399.7e1c4","type":"fs-ops-move","z":"9e4dffea.e8e61","name":"File archive","sourcePath":"dirPath","sourcePathType":"msg","sourceFilename":"file","sourceFilenameType":"msg","destPath":"archiveDirPath","destPathType":"msg","destFilename":"file","destFilenameType":"msg","link":false,"x":1350,"y":500,"wires":[[]]},{"id":"caab671d.a40108","type":"fs-ops-access","z":"9e4dffea.e8e61","name":"","path":"dirPath","pathType":"msg","filename":"file","filenameType":"msg","read":true,"write":true,"throwerror":false,"x":1170,"y":500,"wires":[["85357399.7e1c4"],[]]},{"id":"5315c77f.424368","type":"inject","z":"9e4dffea.e8e61","name":"ScanBox2","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":100,"y":160,"wires":[["abb81dfe.435a2"]]},{"id":"93a51668.ebfa08","type":"inject","z":"9e4dffea.e8e61","name":"CMM1","props":[{"p":"dirPath","v":"/filedrop","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":90,"y":220,"wires":[["abb81dfe.435a2"]]},{"id":"701d5a17.0e1664","type":"postgresdb","z":"","hostname":"host.docker.internal","port":"5432","db":"postgres","ssl":false}]