Programmable LLM Control (PLLMC)
Proof of concept using an LLM to control Physical IO.
Features:
- Uses ModbusTCP for Remote IO
- Connect to Ollama server (local or remote) for LLM inference.
- Supports multiple LLM, can be selected via flow
- Dashboard to "reprogram" PLLMC via a chat window.
- Can be expanded for more inputs/outputs, this example only uses one DI and one DO.
[{"id":"bd90b094011bc34d","type":"tab","label":"Setup","disabled":false,"info":"","env":[]},{"id":"e373327fb5bfa311","type":"tab","label":"Setup","disabled":false,"info":"","env":[]},{"id":"e8c22a083bd31728","type":"tab","label":"LLM Control","disabled":false,"info":"","env":[]},{"id":"da1d4b2f6a7421cf","type":"group","z":"e8c22a083bd31728","name":"","style":{"stroke":"#ff0000","label":true},"nodes":["dbb54fcb39f70a6b","30dd81ab0efa6ccc","3d4d3eb9a7d882da","34e66ca74de78911","9f9321f20c696b66","b5dd5d3adab5c17a"],"x":34,"y":359,"w":772,"h":142},{"id":"3d7732d795e56622","type":"group","z":"e8c22a083bd31728","name":"","style":{"stroke":"#ffff00","label":true},"nodes":["f452898afab8b9ae","306ad9d41a10e7b9","a2fa2e8bf2e3ad5b","8bad9c970a61a52f","fdb10e8eae223a6f","004e25397c76b20d","a463ac1b4755a397","6c928a97005373a6","e629e6e998f3274f","f188df80af99d40c","b214c86bed9246bf","9674592a7e494ed6"],"x":34,"y":19,"w":692,"h":322},{"id":"c246a3b4301428ca","type":"modbus-client","name":"","clienttype":"tcp","bufferCommands":true,"stateLogEnabled":false,"queueLogEnabled":false,"failureLogEnabled":true,"tcpHost":"localhost","tcpPort":502,"tcpType":"DEFAULT","serialPort":"/dev/ttyUSB","serialType":"RTU-BUFFERD","serialBaudrate":9600,"serialDatabits":8,"serialStopbits":1,"serialParity":"none","serialConnectionDelay":100,"serialAsciiResponseStartDelimiter":"0x3A","unit_id":1,"commandDelay":1,"clientTimeout":1000,"reconnectOnTimeout":true,"reconnectTimeout":2000,"parallelUnitIdsAllowed":true,"showErrors":false,"showWarnings":true,"showLogs":true},{"id":"6beead54aa073781","type":"ui_tab","name":"Home","icon":"dashboard","disabled":false,"hidden":false},{"id":"b1dcc4c3e9a93adc","type":"ui_base","theme":{"name":"theme-light","lightTheme":{"default":"#0094CE","baseColor":"#0094CE","baseFont":"-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif","edited":true,"reset":false},"darkTheme":{"default":"#097479","baseColor":"#097479","baseFont":"-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif","edited":false},"customTheme":{"name":"Untitled Theme 1","default":"#4B7930","baseColor":"#4B7930","baseFont":"-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif"},"themeState":{"base-color":{"default":"#0094CE","value":"#0094CE","edited":false},"page-titlebar-backgroundColor":{"value":"#0094CE","edited":false},"page-backgroundColor":{"value":"#fafafa","edited":false},"page-sidebar-backgroundColor":{"value":"#ffffff","edited":false},"group-textColor":{"value":"#1bbfff","edited":false},"group-borderColor":{"value":"#ffffff","edited":false},"group-backgroundColor":{"value":"#ffffff","edited":false},"widget-textColor":{"value":"#111111","edited":false},"widget-backgroundColor":{"value":"#0094ce","edited":false},"widget-borderColor":{"value":"#ffffff","edited":false},"base-font":{"value":"-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif"}},"angularTheme":{"primary":"indigo","accents":"blue","warn":"red","background":"grey","palette":"light"}},"site":{"name":"Node-RED Dashboard","hideToolbar":"false","allowSwipe":"false","lockMenu":"false","allowTempTheme":"true","dateFormat":"DD/MM/YYYY","sizes":{"sx":48,"sy":48,"gx":6,"gy":6,"cx":6,"cy":6,"px":0,"py":0}}},{"id":"a5712d0d37a16325","type":"ui_group","name":"Default","tab":"6beead54aa073781","order":1,"disp":true,"width":"21","collapse":false,"className":""},{"id":"9d53e452a50df509","type":"inject","z":"e373327fb5bfa311","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"llama3.2:latest","payloadType":"str","x":160,"y":120,"wires":[["6a3820fb0372c2a7"]]},{"id":"6a3820fb0372c2a7","type":"change","z":"e373327fb5bfa311","name":"","rules":[{"t":"set","p":"model","pt":"global","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":450,"y":100,"wires":[["fad9cda2c0992383"]]},{"id":"fad9cda2c0992383","type":"function","z":"e373327fb5bfa311","name":"Model Loaded","func":"node.status({fill:\"green\",shape:\"dot\",text:msg.payload});\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":680,"y":100,"wires":[[]]},{"id":"836105d6c57f29e6","type":"inject","z":"e373327fb5bfa311","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"mistral:7b","payloadType":"str","x":140,"y":40,"wires":[["6a3820fb0372c2a7"]]},{"id":"a19583402e1a1b24","type":"inject","z":"e373327fb5bfa311","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"qwen2.5:3b","payloadType":"str","x":150,"y":80,"wires":[["6a3820fb0372c2a7"]]},{"id":"d3044206b3ac95e0","type":"inject","z":"e373327fb5bfa311","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"deepseek-r1:7b","payloadType":"str","x":160,"y":160,"wires":[["6a3820fb0372c2a7"]]},{"id":"b7b9a1e8dafcf166","type":"inject","z":"e373327fb5bfa311","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"192.168.6.101","payloadType":"str","x":160,"y":220,"wires":[["31e93dbef2e91c1c"]]},{"id":"31e93dbef2e91c1c","type":"change","z":"e373327fb5bfa311","name":"","rules":[{"t":"set","p":"url","pt":"global","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":430,"y":220,"wires":[["5645434a3bb16fc0"]]},{"id":"5645434a3bb16fc0","type":"function","z":"e373327fb5bfa311","name":"LLM URL","func":"node.status({fill:\"green\",shape:\"dot\",text:msg.payload});\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":660,"y":220,"wires":[[]]},{"id":"69769fc7fa4425c8","type":"inject","z":"e373327fb5bfa311","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"localhost","payloadType":"str","x":140,"y":260,"wires":[["31e93dbef2e91c1c"]]},{"id":"2fef5a1a9179599d","type":"http request","z":"e8c22a083bd31728","name":"","method":"POST","ret":"txt","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":810,"y":180,"wires":[["dbb54fcb39f70a6b"]]},{"id":"dbb54fcb39f70a6b","type":"function","z":"e8c22a083bd31728","g":"da1d4b2f6a7421cf","name":"Parse response","func":"var model = global.get(\"model\")\n\n// Split and parse responses\nvar lines = msg.payload.trim().split(\"\\n\");\n\nvar responses = lines.map(line => JSON.parse(line).response);\nvar combinedResponse = responses.join(\"\");\n\n// Retrieve chat history\nvar history = flow.get(\"chat_history\") || [];\n\n// Add AI's response to history\nhistory.push({ role: \"assistant\", content: combinedResponse });\n\n// Save updated history\nflow.set(\"chat_history\", history);\n\n// Construct the final JSON output\nmsg.payload = {\n model: JSON.parse(lines[0]).model,\n responses: combinedResponse,\n timestamp: new Date().toISOString()\n};\nnode.status({ fill: \"black\", shape: \"dot\", text: combinedResponse });\nreturn msg;\n\n\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":140,"y":460,"wires":[["3d4d3eb9a7d882da"]]},{"id":"30dd81ab0efa6ccc","type":"modbus-write","z":"e8c22a083bd31728","g":"da1d4b2f6a7421cf","name":"","showStatusActivities":false,"showErrors":false,"showWarnings":true,"unitid":"","dataType":"MCoils","adr":"512","quantity":"1","server":"c246a3b4301428ca","emptyMsgOnFail":false,"keepMsgProperties":false,"delayOnStart":false,"startDelayTime":"","x":700,"y":440,"wires":[[],[]]},{"id":"3d4d3eb9a7d882da","type":"function","z":"e8c22a083bd31728","g":"da1d4b2f6a7421cf","name":"DO Boolean","func":"// Extract the \"DO:x\" part from the response\nvar match = msg.payload.responses.match(/DO:\\s*(0|1)/);\n\n// Convert the extracted value to a boolean\nif (match) {\n msg.payload = match[1] === \"1\"; // \"1\" -> true, \"0\" -> false\n} else {\n msg.payload = null; // Handle unexpected input\n}\nif (msg.payload){\n node.status({fill:\"green\",shape:\"dot\",text:msg.payload});\n}else{\n node.status({fill:\"red\",shape:\"dot\",text:msg.payload});\n}\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":330,"y":440,"wires":[["34e66ca74de78911","9f9321f20c696b66"]]},{"id":"34e66ca74de78911","type":"function","z":"e8c22a083bd31728","g":"da1d4b2f6a7421cf","name":"DO array []","func":"//var data = [false, false, false, false, false, false, false, false];\nvar data = [];\ndata[0] = msg.payload;\nmsg.payload = data;\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":510,"y":440,"wires":[["30dd81ab0efa6ccc"]]},{"id":"9f9321f20c696b66","type":"ui_switch","z":"e8c22a083bd31728","g":"da1d4b2f6a7421cf","name":"","label":"DO-0","tooltip":"","group":"a5712d0d37a16325","order":1,"width":0,"height":0,"passthru":true,"decouple":"false","topic":"topic","topicType":"msg","style":"","onvalue":"true","onvalueType":"bool","onicon":"","oncolor":"","offvalue":"false","offvalueType":"bool","officon":"","offcolor":"","animate":false,"className":"","x":490,"y":400,"wires":[[]]},{"id":"b5dd5d3adab5c17a","type":"comment","z":"e8c22a083bd31728","g":"da1d4b2f6a7421cf","name":"Write Digital Output","info":"","x":150,"y":400,"wires":[]},{"id":"f452898afab8b9ae","type":"function","z":"e8c22a083bd31728","g":"3d7732d795e56622","name":"Program LLM Controller","func":"var model = global.get(\"model\");\nvar url = global.get(\"url\"); // http://192.168.6.101:11434/api/generate\nurl = \"http://\" + url + \":11434/api/generate\"\n// Forcefully reset chat history by clearing stored value first\nflow.set(\"chat_history\", undefined);\n\n// Reset history by explicitly setting it to an empty array\nvar history = [];\n\n// Add new user input as the first (and only) entry\n\nhistory.push({\n role: \"user\",\n content: msg.payload.command\n});\n\n\n// Construct payload with history\nmsg.payload = {\n model: model,\n prompt: `${history[0].role}: ${history[0].content}`, // Only one entry\n options: {\n seed: 123\n }\n};\nmsg.url = url;\n\n// Ensure flow context is completely reset\nflow.set(\"chat_history\", history);\n\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":590,"y":120,"wires":[["2fef5a1a9179599d"]]},{"id":"306ad9d41a10e7b9","type":"inject","z":"e8c22a083bd31728","g":"3d7732d795e56622","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":"","topic":"","payload":"false","payloadType":"bool","x":130,"y":220,"wires":[["fdb10e8eae223a6f"]]},{"id":"a2fa2e8bf2e3ad5b","type":"modbus-read","z":"e8c22a083bd31728","g":"3d7732d795e56622","name":"","topic":"","showStatusActivities":false,"logIOActivities":false,"showErrors":false,"showWarnings":true,"unitid":"","dataType":"Coil","adr":"0","quantity":"1","rate":"2","rateUnit":"s","delayOnStart":false,"startDelayTime":"","server":"c246a3b4301428ca","useIOFile":false,"ioFile":"","useIOForPayload":false,"emptyMsgOnFail":false,"x":150,"y":280,"wires":[["8bad9c970a61a52f","9674592a7e494ed6"],[]]},{"id":"8bad9c970a61a52f","type":"function","z":"e8c22a083bd31728","g":"3d7732d795e56622","name":"DI Bit 0","func":"msg.payload = msg.payload[0];\nif (msg.payload){\n node.status({fill:\"green\",shape:\"dot\",text:msg.payload});\n}else{\n node.status({fill:\"red\",shape:\"dot\",text:msg.payload});\n}\n// Save updated history in flow storage\nvar di = global.get(\"di\");\nif (msg.payload != di) {\n return msg;\n}","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":320,"y":240,"wires":[["fdb10e8eae223a6f","e629e6e998f3274f","004e25397c76b20d"]]},{"id":"fdb10e8eae223a6f","type":"function","z":"e8c22a083bd31728","g":"3d7732d795e56622","name":"Send Commands","func":"var model = global.get(\"model\");\nvar url = global.get(\"url\"); // http://192.168.6.101:11434/api/generate\nurl = \"http://\" + url + \":11434/api/generate\"\n\n// Retrieve previous conversation history from flow storage\nvar history = flow.get(\"chat_history\") || [];\n\n// Add new user input\nvar command = \"DI:\" + msg.payload;\nhistory.push({ role: \"user\", content: command });\n\n// Ensure history length does not exceed 6\nif (history.length > 6) {\n history = history.slice(-6); // Keep only the last 6 elements\n}\n\n// Construct payload with history\nmsg.payload = {\n model: model,\n prompt: history.map(entry => `${entry.role}: ${entry.content}`).join(\"\\n\") // Convert history to text\n};\nmsg.url = url;\n\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":570,"y":160,"wires":[["2fef5a1a9179599d"]]},{"id":"004e25397c76b20d","type":"change","z":"e8c22a083bd31728","g":"3d7732d795e56622","name":"","rules":[{"t":"set","p":"di","pt":"global","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":550,"y":240,"wires":[[]]},{"id":"a463ac1b4755a397","type":"inject","z":"e8c22a083bd31728","g":"3d7732d795e56622","name":"Setup","props":[{"p":"payload.command","v":"I want you to simulate being a PLC, you will only ever respond with 4 characters. Your responses will be parsed in NodeRED to control a real machine. I only want you to respond with DO:0 or DO:1 depending on what I send you. Don't add any other text or it will break the integration! When i send DI:true, return DO:false When I send DI:false, return DO:true","vt":"str"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":"1","topic":"","x":130,"y":100,"wires":[["f452898afab8b9ae"]]},{"id":"6c928a97005373a6","type":"inject","z":"e8c22a083bd31728","g":"3d7732d795e56622","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":"","topic":"","payload":"true","payloadType":"bool","x":130,"y":180,"wires":[["fdb10e8eae223a6f"]]},{"id":"e629e6e998f3274f","type":"ui_switch","z":"e8c22a083bd31728","g":"3d7732d795e56622","name":"","label":"DI-0","tooltip":"","group":"a5712d0d37a16325","order":1,"width":0,"height":0,"passthru":true,"decouple":"false","topic":"topic","topicType":"msg","style":"","onvalue":"true","onvalueType":"bool","onicon":"","oncolor":"","offvalue":"false","offvalueType":"bool","officon":"","offcolor":"","animate":false,"className":"","x":530,"y":200,"wires":[[]]},{"id":"f188df80af99d40c","type":"ui_form","z":"e8c22a083bd31728","g":"3d7732d795e56622","name":"","label":"Command","group":"a5712d0d37a16325","order":3,"width":0,"height":0,"options":[{"label":"PLC Logic","value":"command","type":"multiline","required":true,"rows":4}],"formValue":{"command":""},"payload":"","submit":"submit","cancel":"cancel","topic":"form","topicType":"str","splitLayout":"","className":"","x":130,"y":140,"wires":[["f452898afab8b9ae"]]},{"id":"b214c86bed9246bf","type":"comment","z":"e8c22a083bd31728","g":"3d7732d795e56622","name":"Read Digital Input","info":"","x":150,"y":60,"wires":[]},{"id":"9674592a7e494ed6","type":"function","z":"e8c22a083bd31728","g":"3d7732d795e56622","name":"DI Bit 1","func":"msg.payload = msg.payload[1];\nif (msg.payload){\n node.status({fill:\"green\",shape:\"dot\",text:msg.payload});\n}else{\n node.status({fill:\"red\",shape:\"dot\",text:msg.payload});\n}\n// Save updated history in flow storage\nvar di = global.get(\"di\");\nif (msg.payload != di) {\n return msg;\n}","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":320,"y":300,"wires":[[]]}]