{ "version": "https://jsonfeed.org/version/1", "title": "Daniel Foote", "home_page_url": "https://freefoote.net", "feed_url": "https://freefoote.net/json", "description": "The personal website of Daniel Foote. Posts on random projects over the years, and an online resume of sorts.", "items": [ { "id": "https://freefoote.net/washing-machine-completion-monitor/", "content_html": "

Like many homes, we have a washing machine and dryer. But ours are a few generations\nback, and don't have any of the more modern Wifi features that are becoming common.

\n

In addition, they're on the other side of the house, and we often don't hear\nwhen they finish. The net result of this is that sometimes we have wet washing\nleft in the machine, and if left for a few days, it needs to be washed again!

\n

So to prevent this, I added a sensor to the washing machine to send us notifications\nwhen it's completed, to allow us to empty it promptly to prevent double-washing.

\n

This isn't at all a new or unique idea. A bit of research on the internet led me to\nAndrew Dupont's writeup.\nThis used an accelerometer to tell when the machine stopped moving, and send a\nnotification when it had determined it was finished.

\n

Theory of operation

\n

Like Andrew's design, I also opted to go with an accelerometer based approach,\nto save me having to modify the washing machine itself.

\n

In our case, we have the dryer stacked on top of the washer, so the same sensor\nworks for both appliances - we're not fussed which one has finished, but we want\nto know when both machines are idle. (So we can load the washing into the dryer,\nbut we can't do that until the dryer is also idle, so this works!)

\n

In a nutshell, we keep an eye on the raw accelerometer values, and distill them\ninto a single movement figure. When this figure is above a threshold for long\nenough, we consider the machine running. Then, when it drops below this threshold\nfor a period of time, it's considered idle again. The timeouts are to allow for\nshort term quiet periods; for example the washing machine will often come to\na complete halt for 20-30 seconds during it's cycle to allow the water to settle\nand then it will start up again; we need to consider this as a continuous operation.

\n

Once the algorithm determines that it's running or idle, it can then send the\nrelevant notifications; in our case via the Pushover service.

\n

As far as the communication goes - I kept the code on the device very simple;\njust doing basic calculations and reporting data every 10 seconds. The data was\nthen sent via MQTT to a Node-Red server, which then determined from that data\nif the machine was running or not. The Node-Red server then sent the Pushover\nnotifications as needed.

\n

Hardware

\n

It turns out I had a M5Stack M5StickC-Plus\non the shelf; I'd purchased it for another project, but no longer needed it.\nThis contains everything we need for the project; a built in accelerometer,\na display, Wifi, and a suitably powerful processor.

\n

It also came with an extra bonus - internally it contains a magnet which means\nI can just magnet it to the washing machines - no other mounting hardware was\nrequired. This was an unexpected bonus!

\n

The device also comes with a Micropython firmware (well, a M5Stack variant of it)\nwhich made for quick coding on the device. The code is fairly lightweight and thus\ndidn't need the complexity of coding in C.

\n

Phase 1

\n

Firstly, I had to collect some raw data to work out what readings meant "working"\nand what readings meant "idle". I felt that the best way to do this was simply\nto code a very simple program on the device that reported raw accelerometer values\nvia MQTT every second. On my server, I then subscribed to that topic and piped it\nall to a file for later processing.

\n

On the M5StickC, this is the program that reported the values:

\n
# boot.py\nprint("Boot setup; Wifi!")\n\nimport wifiCfg\nwifiCfg.autoConnect(lcdShow=True)\n\n# main.py\nfrom m5stack import *\nfrom m5ui import *\nfrom uiflow import *\nimport json\nfrom m5mqtt import M5mqtt\nimport imu\n\nprint("Main program startup...")\n\nsetScreenColor(0x111111)\n\n# MQTT setup.\nm5mqtt = M5mqtt(\n    'washing',\n    server='xxxx',\n    port=1883,\n    user='xxxx',\n    password='xxxx',\n    keepalive=60,\n    ssl=False,\n    ssl_params=None\n)\n\n# Start connection\nprint("MQTT connection.")\nm5mqtt.start()\n\n# IMU init.\nprint("IMU Init")\nimu0 = imu.IMU()\n\n# UI setup.\nXL = M5TextBox(50, 5, "0", lcd.FONT_DejaVu40, 0xFFFFFF, rotate=90)\nYL = M5TextBox(85, 80, "0", lcd.FONT_DejaVu40, 0xFFFFFF, rotate=90)\nZL = M5TextBox(50, 150, "0", lcd.FONT_DejaVu40, 0xFFFFFF, rotate=90)\n\ndef update_data():\n    acc = imu0.acceleration\n    XL.setText("%.2f" % acc[0])\n    YL.setText("%.2f" % acc[1])\n    ZL.setText("%.2f" % acc[2])\n\n    m5mqtt.publish('washingmachine/raw', json.dumps(acc))\n    print(json.dumps(acc))\n\nwhile True:\n    update_data()\n    wait_ms(1000)\n
\n

And then on my server, I just dumped the values to a file:

\n
# mosquitto_sub -v -h localhost -p 1883 -t 'washingmachine/raw' -u xxxx -P xxxx >> wmr.txt\n
\n

After a few days, and a few runs of the washing machine, I had some data:

\n
# tail wmr.txt\nwashingmachine/raw [0.0, -0.181, 1.042]\nwashingmachine/raw [-0.003, -0.18, 1.052]\nwashingmachine/raw [0.005, -0.185, 1.036]\nwashingmachine/raw [0.006, -0.181, 1.033]\nwashingmachine/raw [0.006, -0.178, 1.039]\nwashingmachine/raw [0.009, -0.181, 1.031]\nwashingmachine/raw [0.012, -0.18, 1.043]\nwashingmachine/raw [0.009, -0.175, 1.031]\nwashingmachine/raw [0.012, -0.167, 1.037]\nwashingmachine/raw [0.01, -0.189, 1.042]\n
\n

Now it's time to come up with the algorithm to work out when the machine is running\nor not! I happened to have PHP handy for this one on the server in question, so\nthe prototype script was written in PHP. No reason it couldn't have been any other\nlanguage; this just happened to work for me!

\n

I wasn't as fancy as Andrew with his system and state machine. I just had a basic\nalgorithm. In short, it just worked out the combined threshold figure, and then\naveraged this over 10 seconds, and I called every 10 seconds a bucket. Then,\nthere was a simple threshold - if it was above the threshold for a few buckets,\nit was considered "busy", and then when it dropped below the threshold for a few\nbuckets, it then became idle. I made them constants at the top of the script,\nand reran the script a few times adjusting it until the output looked like I\nexpected it to look.

\n

Here is the script:

\n
<?php\n\n// Raw input line looks like this:\n// washingmachine/raw [-0.033, 1.019, 0.083]\n\n// Slurp them into memory. We've got heaps, right?\n$lines = file('wmr.txt');\n\n$readings = array();\nforeach ($lines as $line) {\n\t$bits = explode("washingmachine/raw", $line);\n\t$raw = json_decode($bits[1]);\n\t$readings[] = $raw;\n}\n\n// Now the constants used in the algorithm later on.\nconst BUCKET_LENGTH = 10;\nconst MOVEMENT_AVERAGE = 0.12;\nconst ON_TIME_THRESHOLD = 6; // in bucket lengths\nconst OFF_TIME_THRESHOLD = -6; // in bucket lengths\n\n// Ignore the first 3600 readings (1 hour) - I was moving\n// the sensor into place and got some random readings.\n$readings = array_slice($readings, 3600);\n\n// Split the 1 second reading into bucket sizes.\n$buckets = array_chunk($readings, BUCKET_LENGTH);\n\n// A function called for each bucket to summarize it.\nfunction processBucket($bucket) {\n\t$last = null;\n\n\t$minL = 10;\n\t$maxL = -1;\n\t$sumL = 0.0;\n\t$countL = 0;\n\n\tforeach ($bucket as $reading) {\n\t\tif (!is_null($last)) {\n\t\t\t$deltaX = abs($reading[0] - $last[0]);\n\t\t\t$deltaY = abs($reading[1] - $last[1]);\n\t\t\t$deltaZ = abs($reading[2] - $last[2]);\n\n\t\t\t$force = $deltaX + $deltaY + $deltaZ;\n\n\t\t\t$minL = min($minL, $force);\n\t\t\t$maxL = max($maxL, $force);\n\t\t\t$sumL += $force;\n\t\t\t$countL += 1;\n\t\t}\n\n\t\t$last = $reading;\n\t}\n\n\t// I've used bcmul() to round off the digits,\n\t// but it is slower than using floating points.\n\treturn array(\n\t\t'min' => bcmul($minL, '1.00', 2),\n\t\t'max' => bcmul($maxL, '1.00', 2),\n\t\t'avg' => bcmul($sumL / $countL, '1.00', 2)\n\t);\n}\n\n// Our kinda-state machine state.\n$state = 0;\n$stateCounter = 0;\n$stateChangeTime = 0;\n\n// Index loosely equals seconds, as the data points are one per second.\n$index = 0;\n\nforeach ($buckets as $bucket) {\n\t$result = processBucket($bucket);\n\n\tif ($result['avg'] > MOVEMENT_AVERAGE && $state == 0) {\n\t\t// Currently "off", and we're above the movement threshold, increase the counter.\n\t\t$stateCounter += 1;\n\t} else if ($result['avg'] < MOVEMENT_AVERAGE && $state == 1) {\n\t\t// Currently "on", and we're below the movement threshold, decrease the counter.\n\t\t$stateCounter -= 1;\n\t}\n\n\tif ($state == 0) {\n\t\t// Is currently off - should transition to on?\n\t\tif ($stateCounter >= ON_TIME_THRESHOLD) {\n\t\t\t$state = 1;\n\t\t\t$stateCounter = 0;\n\n\t\t\techo "OFF FOR: ", ($index - $stateChangeTime) * BUCKET_LENGTH, " seconds\\n";\n\t\t\techo "State change -> ON\\n";\n\n\t\t\t$stateChangeTime = $index;\n\t\t}\n\t} else if ($state == 1) {\n\t\t// Is currently on - should transition to off?\n\t\tif ($stateCounter <= OFF_TIME_THRESHOLD) {\n\t\t\t$state = 0;\n\t\t\t$stateCounter = 0;\n\n\t\t\techo "ON FOR: ", ($index - $stateChangeTime) * BUCKET_LENGTH, " seconds\\n";\n\t\t\techo "State change -> OFF\\n";\n\n\t\t\t$stateChangeTime = $index;\n\t\t}\n\t}\n\n\t$index += 1;\n}\n
\n

And with my test data, it gave me this result, which matched what I had in my mind.\nThere are still some false positives, but this bucket size gave me reasonable results.

\n

I'm happy with a few false positives as we'll know when we've had it on or off.

\n
# php wmr.php\nOFF FOR: 166080 seconds\nState change -> ON\nON FOR: 80 seconds\nState change -> OFF\nOFF FOR: 60 seconds\nState change -> ON\nON FOR: 110 seconds\nState change -> OFF\nOFF FOR: 33370 seconds\nState change -> ON\nON FOR: 4070 seconds\nState change -> OFF\nOFF FOR: 100 seconds\nState change -> ON\nON FOR: 1450 seconds\nState change -> OFF\nOFF FOR: 81730 seconds\nState change -> ON\nON FOR: 2900 seconds\nState change -> OFF\nOFF FOR: 222980 seconds\nState change -> ON\nON FOR: 2800 seconds\nState change -> OFF\nOFF FOR: 8620 seconds\nState change -> ON\nON FOR: 310 seconds\nState change -> OFF\nOFF FOR: 90000 seconds\nState change -> ON\nON FOR: 1290 seconds\nState change -> OFF\nOFF FOR: 2840 seconds\nState change -> ON\nON FOR: 4510 seconds\nState change -> OFF\nOFF FOR: 8450 seconds\nState change -> ON\nON FOR: 1470 seconds\nState change -> OFF\n
\n

Phase 2

\n

With an algorithm that I'm happy with, it's time to make something for production\nuse! To save having to fetch and reprogram the sensor all the time, I decided\nto make the sensor just collate the readings into 10 second buckets and report\nthat value via MQTT.

\n

So on the sensor, the code was changed to:

\n
from m5stack import *\nfrom m5ui import *\nfrom uiflow import *\nimport json\nfrom machine import Pin, I2C\nfrom m5mqtt import M5mqtt\nimport imu\n\nprint("Main program startup...")\n\nsetScreenColor(0x111111)\n\n# MQTT setup.\nm5mqtt = M5mqtt(\n    'washing',\n    server='xxxx',\n    port=1883,\n    user='xxxx',\n    password='xxxx',\n    keepalive=60,\n    ssl=False,\n    ssl_params=None\n)\n\n# Start connection\nprint("MQTT connection.")\nm5mqtt.start()\n\n# IMU init.\nprint("IMU Init")\nimu0 = imu.IMU()\n\n# UI setup.\nXL = M5TextBox(50, 5, "0", lcd.FONT_DejaVu40, 0xFFFFFF, rotate=90)\nYL = M5TextBox(85, 80, "0", lcd.FONT_DejaVu40, 0xFFFFFF, rotate=90)\nZL = M5TextBox(50, 150, "0", lcd.FONT_DejaVu40, 0xFFFFFF, rotate=90)\n\nlast = None\nbucket = []\n\ndef update_data():\n    global last\n    global bucket\n\n    acc = imu0.acceleration\n    XL.setText("%.2f" % acc[0])\n    YL.setText("%.2f" % acc[1])\n    ZL.setText("%.2f" % acc[2])\n\n    if last is not None:\n        deltaX = abs(acc[0] - last[0])\n        deltaY = abs(acc[1] - last[1])\n        deltaZ = abs(acc[2] - last[2])\n\n        force = deltaX + deltaY + deltaZ\n\n        bucket.append(force)\n\n    if len(bucket) == 10:\n        avg = sum(bucket) / len(bucket)\n        bucket = []\n\n        print("10s average", avg)\n        m5mqtt.publish('washingmachine/10s', "%0.4f" % avg)\n\n    last = acc\n\n    print(json.dumps(acc))\n\nwhile True:\n    update_data()\n    wait_ms(1000)\n
\n

Then, in my node-red instance I set up the following nodes:

\n

\"The

\n

The meat of this is in the decision function node. It re-implements what we had in the PHP\nscript, but with a few minor differences. It uses the context available to the function,\nbut that applies only to this function. In my Node-red instance, I've configured\na persistent save location for this data which was needed for other things that\nmy Node-red instance does. If you don't have this configured, it'll be lost\neach time you restart Node-red.

\n

But the code mostly speaks for itself below!

\n
// Parse the value into a float.\nvar avg = parseFloat(msg.payload);\n\n// Our hard-coded threshold and settings.\nvar MOVEMENT_AVERAGE = 0.12;\nvar ON_TIME_THRESHOLD = 6; // in bucket lengths\nvar OFF_TIME_THRESHOLD = -6; // in bucket lengths\n\n// Reload our context.\nvar state = context.get('state') || 0;\nvar stateCounter = context.get('stateCounter') || 0;\nvar stateChangeTime = context.get('stateChangeTime') || Date.now();\n\n// Determine where we're at.\nif (avg > MOVEMENT_AVERAGE && state == 0) {\n    stateCounter += 1;\n} else if (avg < MOVEMENT_AVERAGE && state == 1) {\n    stateCounter -= 1;\n}\n\nvar newMessage = null;\nvar now = Date.now();\n\n// node.warn("Now is " + now);\n\nif (state == 0) {\n    // Is off - should transition to on?\n    if (stateCounter >= ON_TIME_THRESHOLD) {\n        state = 1;\n        stateCounter = 0;\n\n        var offFor = now - stateChangeTime;\n        newMessage = {}\n        newMessage.payload = true;\n        newMessage.duration = offFor;\n\n        stateChangeTime = now;\n    }\n} else if (state == 1) {\n    // Is on - should transition to off?\n    if (stateCounter <= OFF_TIME_THRESHOLD) {\n        state = 0;\n        stateCounter = 0;\n\n        var onFor = now - stateChangeTime;\n        newMessage = {}\n        newMessage.payload = false;\n        newMessage.duration = onFor;\n\n        stateChangeTime = now;\n    }\n}\n\n// node.warn("State " + state + " state counter " + stateCounter + " stt " + stateChangeTime);\n\n// Save our context.\ncontext.set('state', state);\ncontext.set('stateCounter', stateCounter);\ncontext.set('stateChangeTime', stateChangeTime);\n\n// Return the new message. If there is no change,\n// this will be null causing it not to emit the message.\nreturn newMessage;\n
\n

And then the tiny node to format this for Pushover. Basically, we ignore if it\nturns on; we're only interested when it turns off.

\n
var newMessage = null;\n\nif (msg.payload) {\n    // Ignore... we won't notify when it starts.\n} else {\n    var timeLength = Math.floor(msg.duration / 1000);\n    newMessage = {};\n    newMessage.topic = "Washing machine or Dryer finished after " + timeLength + " seconds";\n    newMessage.payload = "Time to empty!";\n}\n\nreturn newMessage;\n
\n

It then gets routed into a pushover node, using the standard\nNode-red pushover node.\nI created an application for Home automation messages, and then added our device\nkeys to the node. (They don't appear in the export below for security reasons!)

\n

For completeness, here is the JSON export from Node-red:

\n
[\n    {\n        "id": "04569ce94308cf33",\n        "type": "tab",\n        "label": "Flow 2",\n        "disabled": false,\n        "info": "",\n        "env": []\n    },\n    {\n        "id": "7dc3c88ef8ad68a6",\n        "type": "mqtt in",\n        "z": "04569ce94308cf33",\n        "name": "Washing Machine Raw Input",\n        "topic": "washingmachine/10s",\n        "qos": "2",\n        "datatype": "utf8",\n        "broker": "b093c481fe186acb",\n        "nl": false,\n        "rap": true,\n        "rh": 0,\n        "inputs": 0,\n        "x": 160,\n        "y": 60,\n        "wires": [\n            [\n                "00d5be41e5bdc801"\n            ]\n        ]\n    },\n    {\n        "id": "00d5be41e5bdc801",\n        "type": "function",\n        "z": "04569ce94308cf33",\n        "name": "Determine Washing Machine State",\n        "func": "// Parse the value into a float.\\nvar avg = parseFloat(msg.payload);\\n\\n// Our hard-coded threshold and settings.\\nvar MOVEMENT_AVERAGE = 0.12;\\nvar ON_TIME_THRESHOLD = 6; // in bucket lengths\\nvar OFF_TIME_THRESHOLD = -6; // in bucket lengths\\n\\n// Reload our context.\\nvar state = context.get('state') || 0;\\nvar stateCounter = context.get('stateCounter') || 0;\\nvar stateChangeTime = context.get('stateChangeTime') || Date.now();\\n\\n// Determine where we're at.\\nif (avg > MOVEMENT_AVERAGE && state == 0) {\\n    stateCounter += 1;\\n} else if (avg < MOVEMENT_AVERAGE && state == 1) {\\n    stateCounter -= 1;\\n}\\n\\nvar newMessage = null;\\nvar now = Date.now();\\n\\n// node.warn(\\"Now is \\" + now);\\n\\nif (state == 0) {\\n    // Is off - should transition to on?\\n    if (stateCounter >= ON_TIME_THRESHOLD) {\\n        state = 1;\\n        stateCounter = 0;\\n        \\n        var offFor = now - stateChangeTime;\\n        newMessage = {}\\n        newMessage.payload = true;\\n        newMessage.duration = offFor;\\n        \\n        stateChangeTime = now;\\n    }\\n} else if (state == 1) {\\n    // Is on - should transition to off?\\n    if (stateCounter <= OFF_TIME_THRESHOLD) {\\n        state = 0;\\n        stateCounter = 0;\\n        \\n        var onFor = now - stateChangeTime;\\n        newMessage = {}\\n        newMessage.payload = false;\\n        newMessage.duration = onFor;\\n        \\n        stateChangeTime = now;\\n    }\\n}\\n\\n// node.warn(\\"State \\" + state + \\" state counter \\" + stateCounter + \\" stt \\" + stateChangeTime);\\n\\n// Save our context.\\ncontext.set('state', state);\\ncontext.set('stateCounter', stateCounter);\\ncontext.set('stateChangeTime', stateChangeTime);\\n\\n// Return the new message. If there is no change,\\n// this will be null causing it not to emit the message.\\nreturn newMessage;",\n        "outputs": 1,\n        "noerr": 0,\n        "initialize": "",\n        "finalize": "",\n        "libs": [],\n        "x": 500,\n        "y": 60,\n        "wires": [\n            [\n                "56764b0aabc085ab",\n                "7ae019ac7f3d83e2"\n            ]\n        ]\n    },\n    {\n        "id": "56764b0aabc085ab",\n        "type": "debug",\n        "z": "04569ce94308cf33",\n        "name": "",\n        "active": true,\n        "tosidebar": true,\n        "console": false,\n        "tostatus": false,\n        "complete": "true",\n        "targetType": "full",\n        "statusVal": "",\n        "statusType": "auto",\n        "x": 790,\n        "y": 60,\n        "wires": []\n    },\n    {\n        "id": "2f51cc59ab47e1a5",\n        "type": "inject",\n        "z": "04569ce94308cf33",\n        "name": "Test Active",\n        "props": [\n            {\n                "p": "payload"\n            },\n            {\n                "p": "topic",\n                "vt": "str"\n            }\n        ],\n        "repeat": "",\n        "crontab": "",\n        "once": false,\n        "onceDelay": 0.1,\n        "topic": "washingmachine/10s",\n        "payload": "0.35",\n        "payloadType": "str",\n        "x": 220,\n        "y": 120,\n        "wires": [\n            [\n                "00d5be41e5bdc801"\n            ]\n        ]\n    },\n    {\n        "id": "623e6c7622f31eb5",\n        "type": "inject",\n        "z": "04569ce94308cf33",\n        "name": "Test Idle",\n        "props": [\n            {\n                "p": "payload"\n            },\n            {\n                "p": "topic",\n                "vt": "str"\n            }\n        ],\n        "repeat": "",\n        "crontab": "",\n        "once": false,\n        "onceDelay": 0.1,\n        "topic": "washingmachine/10s",\n        "payload": "0.05",\n        "payloadType": "str",\n        "x": 220,\n        "y": 160,\n        "wires": [\n            [\n                "00d5be41e5bdc801"\n            ]\n        ]\n    },\n    {\n        "id": "7ae019ac7f3d83e2",\n        "type": "function",\n        "z": "04569ce94308cf33",\n        "name": "Format for Pushover",\n        "func": "var newMessage = null;\\n\\nif (msg.payload) {\\n    // Ignore... we won't notify when it starts.\\n} else {\\n    var timeLength = Math.floor(msg.duration / 1000);\\n    newMessage = {};\\n    newMessage.topic = \\"Washing machine or Dryer finished after \\" + timeLength + \\" seconds\\";\\n    newMessage.payload = \\"Time to empty!\\";\\n}\\n\\nreturn newMessage;",\n        "outputs": 1,\n        "noerr": 0,\n        "initialize": "",\n        "finalize": "",\n        "libs": [],\n        "x": 540,\n        "y": 140,\n        "wires": [\n            [\n                "56764b0aabc085ab",\n                "dca8df092dea0d17"\n            ]\n        ]\n    },\n    {\n        "id": "dca8df092dea0d17",\n        "type": "pushover",\n        "z": "04569ce94308cf33",\n        "name": "Send to Pushover",\n        "device": "",\n        "title": "",\n        "priority": 0,\n        "sound": "",\n        "url": "",\n        "url_title": "",\n        "html": false,\n        "x": 810,\n        "y": 140,\n        "wires": []\n    },\n    {\n        "id": "b093c481fe186acb",\n        "type": "mqtt-broker",\n        "name": "FAFF MQTT",\n        "broker": "xxxx",\n        "port": "1883",\n        "clientid": "",\n        "autoConnect": true,\n        "usetls": false,\n        "protocolVersion": "4",\n        "keepalive": "60",\n        "cleansession": true,\n        "birthTopic": "",\n        "birthQos": "0",\n        "birthPayload": "",\n        "birthMsg": {},\n        "closeTopic": "",\n        "closeQos": "0",\n        "closePayload": "",\n        "closeMsg": {},\n        "willTopic": "",\n        "willQos": "0",\n        "willPayload": "",\n        "willMsg": {},\n        "sessionExpiry": ""\n    }\n]\n
\n

Results

\n

It works! Now it's just up to the human factor of paying attention to the notifications\nunloading the machine once it finishes. We'll just have to see how that goes...!

\n

Future improvements

\n

While it works, there is still a bit of a grey area in determining if the machine\nis working or not. For more accuracy, we'd need to modify the washing machine's wiring,\nor use something pointed at the control panel to tell if it's done.

\n

But for now, it works!

\n", "url": "https://freefoote.net/washing-machine-completion-monitor/", "title": "Washing Machine Completion Monitor", "summary": "We kept missing when the washing machine or dryer finished, so I made a monitor to internet enable it!", "date_modified": "2022-10-16T14:17:27.301Z" }, { "id": "https://freefoote.net/virtual-usb-flash-drive-for-a-non-networked-cnc-router/", "content_html": "

Recently, a business that we worked with purchased a brand new 1325 CNC router -\nthat is a CNC router with a 1300mm x 2500mm working area, fitted with an automatic\ntool changer. While a fantastic machine, there were a few things missing from the machine.

\n

The machine uses a Weihong NK105 G3 control system. It's a solid control system,\nbut naturally has it's own quirks! One of the main issues we had with the machine\nis that the only way to get jobs onto the machine is via a USB drive. It has no\nnetworking options at all. This was known before the machine was purchased; to\nsave costs, this controller was selected rather than an upgraded one that had\nnetworking functionality.

\n

This CNC router is working on a wide variety of different jobs each day, which\ncan involve several dozen plug-unplug cycles on both the CNC router and the\ncomputer used to generate the gcode for the machine. This adds wear and tear to all\nthe components involved, not to mention the reaching up and down to where\nthe drive is located on the machine.

\n

But that doesn't mean we can't network it...

\n

A quick bit of internet research shows that a Raspberry Pi Zero's USB ports\nhave a gadget mode - such that you can actually use it to create a virtual USB\ndrive. So while we can't fully network the device, we can at least stop the constant\ntumble of unplugging and re plugging in a USB drive all the time.

\n

The goal of this project was simple - just set up a Raspberry Pi Zero to act as\na USB flash drive, and allow it to accept file updates from a remote computer\nover the network.

\n

Once the files are available to the CNC, jobs were started from the control handle\non the CNC. Once the job is loaded into memory on the CNC controller, you can\nactually disconnect the USB flash drive, which means that we can reload new files\nwhile the current job is in progress.

\n

Overview

\n

In this specific case, the user of the CNC machine works on a Mac laptop, and they\nuse Fusion 360 to generate toolpaths. This allowed me to take a few shortcuts\nwith the development of this project!

\n

Fusion 360 typically post processes files into a single folder; and it's often\neasier just to use a single folder for this. This gives us a single source of files\nto send to the CNC.

\n

Then, as it was a Mac laptop, it already comes with rsync, which is a battle\nhardened way to transfer files via a network reliably, so why not make use of it, rather\nthan reinventing the wheel in a less perfect way?

\n

The user in question is tech savvy; we created a shortcut for them to be\nable to quickly sync the Fusion 360 folder over to the virtual drive.

\n

For other users, I would deploy this a bit differently - I was originally going\nto find a simple web-based file manager that allowed uploading of files, and set\nthat up on the Raspberry Pi, and modifying it a touch to allow you to manually\nmount or unmount the virtual drive. However, time is money, and this solution\nrobustly met the requirements with minimal time, although it's not the\nmost user friendly solution.

\n

Hardware

\n

The core hardware required is:

\n\n

And optional hardware, which was needed due to our specific setup:

\n\n

Setup of the Raspberry Pi Zero

\n

We flashed Raspbian Lite using the official imaging tool on a Windows machine.\nAt time of writing this was 2022-04-04, and the 32bit version.

\n

When using the imaging tool, we used the advanced configuration to set:

\n\n

After it was flashed, the card was inserted and the Pi was booted. I connected to\nit via SSH, and checked that it expanded the filesystem correctly to the full drive.\nYou can also take this time to apply other standard setup steps for your environment,\nlike installing monitoring tools and so forth.

\n

Set up the gadget mode overlay

\n

We need to enable the gadget overlay, and get it to insert the module at boot.

\n
# sudo nano /boot/config.txt\n... at end add:\ndtoverlay=dwc2\n\n# sudo nano /etc/modules\n... at end add:\ndwc2\n
\n

Now reboot the Pi, and we'll create the filesystem.

\n

Creating the filesystem

\n

Firstly we create the file that will be the virtual block device:

\n
# sudo dd bs=1M if=/dev/zero of=/drive.bin count=2048\n
\n

I then connected it to a Windows 10 machine and got it to partition the\nblock device and format it. Yes, technically I could have done this all with\nLinux, but I was aiming for compatibility with the CNC router, which worked\nmore reliably with drives formatted by a Windows machine.

\n

To get it to expose the file as a block device over USB ("plugging in" the USB drive):

\n
# sudo modprobe g_mass_storage file=/piusb.bin stall=0\n
\n

Then using a Windows machine, partition the drive and format it - we used FAT32\nto get the compatibility we needed. When you're done, you can release the block\ndevice as follows ("unplugging" the USB drive):

\n
# sudo modprobe -r g_mass_storage\n
\n

Now we'll set up the mount point on the Pi.

\n
# sudo mkdir /mnt/drive\n
\n

Then comes the interesting part.\nBasically, we're using rsync as a destination, without a password or encryption.\n(Yes, this is insecure, but it's on a private network and we're aiming for simplicty in this instance).\nBefore the transfer starts, rsync runs an "early exec" script, which unmounts\nthe virtual drive from the CNC, and then remounts the filesystem locally, allowing\nthe file transfer to take place. After the sync, it runs a "post-xfer exec" script,\nwhich umounts the drive from Linux and re-exports it to the CNC. As mentioned\nbefore, this was the quickest and most robust solution to meet this specific\nusers requirements, although it's not super user friendly.

\n

So let's set up Rsync. Turns out that systemd has solid additional default\nsettings used to harden rsync installations. These settings are ideal for almost\nall installations of rsync - except for ours! So we had to relax the systemd rules\nslightly to allow our very unusual disc usage pattern.

\n

Let's loosen the systemd rules for rsync slightly, to allow the exec scripts\nto be able to mount filesytems and run modprobes. Without this change, the scripts\nwill fail to run. I did spend a good 30 minutes working this one out:

\n
# sudo vim /lib/systemd/system/rsync.service\n... modify:\nPrivateDevices=off\n\n# sudo systemctl daemon-reload\n
\n

Now let's configure the rsync daemon:

\n
# sudo vim /etc/rsyncd.conf\n\n... the entire contents of this file should be:\n\nuid = root\ngid = root\nmax connections = 10\nsocket options = SO_KEEPALIVE\n\n[cnc]\npath = /mnt/drive\ncomment = CNC Drive\nread only = false\nearly exec = /home/pi/pre-copy.sh\npost-xfer exec = /home/pi/post-copy.sh\n
\n

Set up the early exec script. Note this can't be a "pre-xfer exec" as it's not\nearly enough in the transfer; rsync already has aquired file handles for the folder,\nand doesn't see the mount point made inside the script. Yes, this took a few goes\nto work out...!

\n
# sudo vim /home/pi/pre-copy.sh\n#!/bin/bash -x\n\nset -e\n\necho "Removing from CNC..."\nsudo modprobe -r g_mass_storage\n\nsleep 1\n\necho "Mounting drive locally..."\nsudo mount -o loop,offset=65536,sizelimit=2144337920 /drive.bin /mnt/drive\n\nsleep 1\n\necho "Ready to receive files."\n
\n

Wait a sec! What's happening here? For simplicity, I'm not setting up a loopback\nmount device for the file which would allow us to address the partions separately,\nas this was extra configuration and more complexity. Instead, I worked out the\noffset and partition size from the file image, and put them into the mount\ncommand, bypassing the need to set up a loopback device. The numbers below will\nbe different for your device, but it gives you an idea.\n(Snippet originally from StackExchange)\nNote that the numbers are in sectors, so multiply them by 512 (in this case)\nto get the actual physical numbers for the mount command.

\n
# sudo fdisk -lu sda.img\n...\nUnits = sectors of 1 * 512 = 512 bytes\nSector size (logical/physical): 512 bytes / 512 bytes\n...\n  Device Boot      Start         End      Blocks   Id  System\nsda.img1   *          56     6400000     3199972+   c  W95 FAT32 (LBA)\n
\n

And the post copy script, to unmount the drive and expose it to the CNC again.\nNote that it uses the "ro=1" flag for g_mass_storage, meaning that the CNC has\na read only view of the virtual drive. There isn't a specific need for this, but\nthere also isn't a reason for the CNC controller to manage files on the drive.

\n
# sudo vim /home/pi/post-copy.sh\n#!/bin/bash -x\n\nset -e\n\necho "Unmounting drive..."\numount /mnt/drive\n\necho "Re-exporting mass storage..."\nmodprobe g_mass_storage file=/drive.bin stall=0 ro=1\n\necho "Completed."\n
\n

And finally, a script to execute on boot, which exposes the drive to the CNC on boot.

\n
# sudo vim /home/pi/on-reboot.sh\n#!/bin/bash -x\n\nset -e\n\necho "Exporting mass storage..."\n/usr/sbin/modprobe g_mass_storage file=/drive.bin stall=0 ro=1\n\necho "Completed."\n
\n

Make all these scripts executable:

\n
# sudo chmod a+x /home/pi/*.sh\n
\n

And start rsync:

\n
# sudo systemctl restart rsync\n
\n

And we're going to use crontab to run the on-boot script to expose the drive.\nThere are other ways to accomplish this, but this is the way I chose to do it:

\n
# sudo crontab -e\n...\n@reboot /home/pi/on-reboot.sh\n
\n

And we're ready to test! At this stage, if you reboot the Pi and connect to it\nvia USB, you should get a virtual flash drive. Now, if you're ready to send\nfiles to it, you can simply use this rsync command. This will work from a Linux\nor Mac host directly without any additional software to install in most cases. You'll see\nthe drive vanish from your computer when you start the transfer, and then re-appear\nshortly afterwards:

\n
# rsync --progress --recursive --verbose --delete source/* rsync://cncdrive.local/cnc\n
\n

It should give you progress as it transfers the files, and will delete on remote\nso that the remote folder looks exactly like your local source folder.

\n

Adding Ethernet

\n

As mentioned before, in our specific installation, we didn't have 2.4GHz wireless\navailable at the installation location, but only 5GHz wireless. So we had to add\nan external ethernet device to the setup.

\n

This was based on the Raspberry Pi Spy's instructions\nalthough our module was different to the one pictured, and had different markings\non the pins. Also, the documentation for our module had some inconsistencies too\nwhich led to some confusion. But here is the lowdown:

\n\n

Once wired up, setting up the Pi was very simple:

\n
# sudo nano /boot/config.txt\n\n... add or alter:\ndtparam=spi=on\ndtoverlay=enc28j60\n\n# sudo reboot\n
\n

On reboot, the ethernet port will appear as eth0, and automatically aqcuire an\naddress via DHCP if it can. It's actually that simple...! I was surprised actually\nby just how easy the ethernet setup was.

\n

Installation

\n

For installation, I made a small panel out of acrylic that the Raspberry Pi Zero and\nthe ENC28J60 modules were screwed to, to keep them together. Then we made a larger\nbox to house the wireless router, USB power supply for the Pi, and a powerboard. This\nkept all the components safe and shielded them from the very dusty woodworking shop\nthat they were installed into. This box was mounted on a wall near the CNC router,\nand the USB A-to-A cable was used to connect to the CNC. The A-to-A cable was 3 meters long allowing\nus to route the USB cable out of the way, and it just plugged into the front panel\nof the CNC router.

\n

Should the Raspberry Pi Zero fail, we can easily unplug it from the CNC router and\nresume using the USB thumb drive as we used to do, allowing us to keep working\neven if it fails.

\n

Results

\n

A week after the installation, the user of the machine was very happy with this\naddition to their CNC router. It's saved them a lot of time swapping USB drives\nand has made for a much easier workflow for them, as they're often tweaking or\nadjusting gcodes for various jobs, so as to get the perfect results. Even though\nit's not that user friendly, everything is handed by a single click on the Mac,\nso it's more than easy enough to use for this application.

\n

Future improvements

\n

With more time, we'd make a few improvements to the system:

\n\n", "url": "https://freefoote.net/virtual-usb-flash-drive-for-a-non-networked-cnc-router/", "title": "Virtual USB flash drive for a non-networked CNC router", "summary": "CNC controller doesn't support any other method of getting files to it other than a USB drive? Let me fix that for you...", "date_modified": "2022-07-09T11:13:27.301Z" }, { "id": "https://freefoote.net/3d-printed-normally-closed-cnc-tool-height-setter/", "content_html": "

Recently, a business that we worked with purchased a brand new 1325 CNC router -\nthat is a CNC router with a 1300mm x 2500mm working area, fitted with an automatic\ntool changer. While a fantastic machine, there were a few things missing from the machine.

\n

One of those things was a mobile tool height setter. The Auto Tool Changer (ATC)\nhad a special tool height sensor mounted at the back of the machine, which it used\nto check the tool heights. However, there was no sensor that could be used before\nstarting a job, as many of the jobs were programmed with the Z=0 at the top of the\nworkpiece for convenience. The machine was intended for use in a cabinet shop,\nwhere all the jobs would be programmed with Z=0 at the machine bed, but instead\nit was being used for a range of beautiful wood toys and furniture.

\n

The machine used a Weihong NK105 G3 control system. It's a solid control system,\nbut naturally has it's own quirks!

\n

Normally Closed switch

\n

In addition to this, after some investigation on the machine, the tool height\nsetter was actually a normally closed switch. Other CNC machines I had worked with\npreviously were normally open, and closed a circuit through a probe. So this meant\nthat the approach used in the past wouldn't work on this machine! We'd actually\nneed a switch of some kind to be able to automate this part.

\n

Altering the wiring

\n

Some reading of the NK105 G3 manual shows that it only has a single input pin for\ntool setting. So that means that the mobile tool setter needs to be inline with\nthe existing tool setter. Such that when either of the normally closed switches\nare opened, it signals the controller that the tool has activated. A diagram is\nshown below.

\n

Version 1 - just the micro switch

\n

For a test, I grabbed a spare micro switch that I had on hand, and soldered on some\nwires. We then ran some extra wiring through the drag chains of the machine, right\ndown to the spindle head. Then, I added the switch inline with the existing tool\nsetter.

\n

I then mounted the switch in between two pieces of acrylic to give it a stable platform.\nFor our testing, we then used the tool set command on the controller (Shift + 9)\nand then tool started probing downwards toward the switch. The first issue that we\nhad is that you had to have the tiny little micro switch in exactly the right position\nto contact the tool. Some tools might not engage the switch correctly depending on\nhow they were rotated! The top of the micro switch was also rounded, meaning it didn't\nalways touch the same spot each time, meaning that it wasn't that accurate.

\n

It proved the concept though - that the switch would work and act as a mobile\ntool setter. So now it was a matter of coming up with something more accurate\nand easier to use.

\n

Why not buy one?

\n

Most of the cheap commercially available tool setters are the normally open type;\nthat is just a block of metal that conducts when the tool touches it. The other\neasily available type are the fixed ones that sit at the back of the machine, and\nthey tend to be quite tall.

\n

In this instance, we decided to make one instead, to meet our design goals as shown\nbelow.

\n

Design goals

\n

There were two primary design goals for the tool setter:

\n
    \n
  1. Be accurate down to around 0.1mm. This fits into the specifications for the jobs\nthat are run on this machine.
  2. \n
  3. Be as thin as possible, so as not to use up any more of the Z clearance than\nneeded. The fixed tool setters we looked at online were quite tall (100mm) which would\nuse up most of the official 100mm clearance that the CNC router had. Sometimes, work pieces\nthat were up to 80mm thick would be placed into the machine, and we wanted to be\nable to use the tool setter on these.
  4. \n
\n

The design

\n

So this was the design that we came up with. It just uses an ordinary sub miniature\nmicro switch which is very common. It's designed with two 3D printed sleeves, basically,\nthat allow the inner sleeve to move against the outer sleeve. It's designed\nsuch that they contact quite well, to prevent the platform from twisting where\npossible, to ensure that it consistently activates the switch at the same vertical\nlocation each time.

\n

Internally, three springs keep the inner sleeve up against the top, and provides\nforce for the tool to push against. The switch mounts internally with two screws to\nkeep it securely in place and allow for a repeatable measurement.

\n

The design ended up being three parts - the outer sleeve, the inner sleeve, and the base.\nThe outer sleeve holds everything together and is slightly tapered at the top\nto keep the inner sleeve in. The inner sleeve has channels for the springs to keep them\nin place. The base has channels for the springs too, and for the switch to mount to.

\n

The maximum designed travel for the sensor was 3mm, but it never actually intends\nto use that much - the micro switch was mounted just under the surface and should\nactivate within 1mm of travel.

\n

\"Cutaway

\n

The base has a lip that's slightly bigger than the outer - it's intended to friction\nfit together, as I was struggling to find a good spot to add screws.

\n

You can also view and download the design as well - it was made in Fusion 360.\nIf you don't have Fusion 360, I've also saved the STL files here:

\n\n

Printing and assembly

\n

3D printing was straightforward, and done in PETG with standard settings. The production\nmodel didn't have a larger lip on the base, and it did friction fit - kinda! In the\nend we glued this one together, but I've adjusted the design since then and would\nmake a new one differently.

\n

I wasn't sure just how close I could make the inner sleeve to the outer sleeve; I\nwanted it as close as possible, but it still has to move. The first print had this\ngap at just 0.05mm - which was too close, unfortunately - the inner sleeve got stuck\non the outer sleeve. For the second run, I increased this to 0.2mm, and this provided\na really nice fit.

\n

However, the inner sleeve still grated slightly against the outer sleeve, as the lines\nin the 3D print made the two contact surfaces slightly rough. This was actually an\neasy fix - a few moments with some sandpaper (240 grit) around the outside of\nthe inner shell, and on the inside of the outer shell, and then they fit together\nand slid over each other very smoothly. To further improve this, we also added\nsome medium thickness grease over the surfaces. Once we did this, they smoothly\nslid over each other without catching - just perfect for accurate measurements\non the CNC.

\n

Setup on the CNC

\n

Although it was carefully designed, we didn't know exactly at what height the\ntool would engage the micro switch. This would have to be done by testing.

\n

The CNC controller has a setting for how tall the mobile tool setter device is,\nin millimeters. To start with, I set this to zero, and then set up the mobile\ntool setter on the bed of the CNC, with a tool in place, and then ran the\ntool setter function. The CNC slowly moved the tool down onto the sensor until\nthe switch triggered, and then repeated this a few times slowing down the speed\neach time, to get an accurate result.

\n

Once it completed this tool setting, I then manually moved the tool down slowly\nuntil it just touched the bed, using the old "paper" trick - getting it close\nto the bed such that it just grabs a sheet of plain paper underneath. I know\nthis isn't super accurate, but it's more than accurate enough for our use. Then,\nI took this offset and updated the CNC controller with this number.

\n

I then repeated the automatic tool setting with the offset set. Then I moved the\ntool down manually again until the CNC read Z=0. I found it was off the bed by\napproximately 0.1mm - that is 0.1mm too far away. I added this to the existing\noffset in the controller, and ran the test again. This time, it was spot on the bed.

\n

I repeated this several times, getting the same result each time. At this stage,\nwe were ready to go!

\n

I estimate the accuracy of this tool setter to be 0.07mm. This is well and truly\nappropriate for the woodworking that the machine spends most of it's life doing,\nso we left it as is! For other applications, you would need a higher accuracy but\nthere are appropriate tools for those applications.

\n

And normally open too?

\n

There is no reason this couldn't be done with a normally open switch as well!\nThe standard micro switches have NO and NC terminals, so it's just a matter of\nchoosing the terminals that match your requirements. In the future, I plan to add\none of these to our other CNC machines too, which have a normally open setup.

\n

Future improvements

\n

I stopped development at this stage as we met the goals, and I had other projects\nto work on! But there are a few things I would improve for a future version:

\n\n", "url": "https://freefoote.net/3d-printed-normally-closed-cnc-tool-height-setter/", "title": "3D Printed Normally Closed CNC Tool Height Setter", "summary": "A 3D printed tool height setter for a CNC.", "date_modified": "2022-06-26T11:13:27.301Z" }, { "id": "https://freefoote.net/resume/", "content_html": "

This page is a work in progress. Over time, I will update this with my resume.

\n

Really, I promise!

\n

In the meantime, please get in touch if you have any questions.

\n\n", "url": "https://freefoote.net/resume/", "title": "Resume", "summary": "Resume and about me", "date_modified": "2022-06-26T00:00:00.000Z" }, { "id": "https://freefoote.net/tv-content-based-backlight/", "content_html": "

Quite a while ago someone told me about the Ambilight\nsystem that was incorporated into certain TVs. I was intrigued by the concept,\nand wondered if it would catch on.

\n
\n\n
\n

Whilst it didn't seem to catch on in too many consumer TV sets, it did seem to catch\non with DIYers in general. I've seen quite a few different types around the place, and most\nof them involve quite a bit of wiring to get enough resolution around the TV edges.\nThe extensive wiring turned me off, as it's a lot of work to put together and maintain,\nlet alone if you need to move the TV around.

\n

But just a few months ago I came across the WS2812 family of RGB LEDs. These nifty little\ndevices allow you to chain a series of them with just a single input to drive them.\nThey also have a constant current source inside each chip, meaning that the colours\nand brightness are more stable across a series of these chips - anyone who's bought\ncheap 12V LED strips off various internet sites will know that the LEDs can vary quite a bit.

\n

I first saw them on Adafruit's web store. They'd already written an Arduino library,\nand written up how to use them properly, especially with their very tight timing\nrequirements to send data to them. I certainly applaud Adafruit for making their library\npublic, and I feel bad that it's significantly more expensive to source parts through\nthem or a reseller, as I live in Australia.

\n

A few weeks after seeing them in Adafruit's store, I randomly happened to search\nfor them to see where else I could get them from, and stumbled across an eBay listing,\nwhich offered a 5 metre strip for $60 (with 30 LEDs per metre), from a seller in Hong Kong.\nHaving ordered items from overseas on many occasions, I was used to the 4 or so week\nlead time on items from Hong Kong. I ordered it just to see what would happen.

\n

Whilst it was coming, I realised that I could use the strip to build my own version\nof an Ambilight, something that I'd been keen to try for some time.

\n

\"A

\n

Capturing the Screen

\n

A problem that anyone who builds an Ambilight system runs into is how to actually\nfigure out what colours to show for each region of the screen.

\n

In an ideal world, you would put something inline with the HDMI input to the TV\n(especially if you already have a home theatre receiver doing the HDMI switching for you),\nand it would pick colours for you and use them. In practice, this isn't so easy.\nTo start with, HDMI is a high bandwidth bus - you need specialised chips and\nprobably a FPGA to process that data. And if that wasn't enough - there is HDCP,\nmeaning that the signal is encrypted between the two.

\n

One of the best systems that I've seen used a HDMI splitter, then an HDMI to S-Video\nconverter, and finally a microprocessor that reads the S-Video to sample the colours\nfor the display. This is a good approach, but it's higher cost than I would like -\nand if you don't get a suitable HDMI splitter, you lose HDCP, and finally, there\nis an analog component to the system.

\n

I'll continue to research some simple way to read in HDMI data\n(and see about the DMCA issues with that too).

\n

In the meantime, that leaves screencapture as the only other way to get the data.\nThis also restricts input sources to be computers, unfortunately, but it's the\ncheapest way to get everything to work together.

\n

Parts List

\n

The following parts were what I ended up using for my system.

\n\n

Hardware - LEDS

\n

I'm very fortunate to have a Sharp 70" TV that I acquired when I moved into my new\nhouse in February 2013. After a month or so, I wall mounted it to give me more\noptions with the other equipment in the theatre.

\n

Most Ambilight systems are fitted to smaller TVs. So when you actually do the\nmeasurements of the actual edge of the LCD panel, you end up with just short of 5 metres.

\n

After measuring the LCD panel, and rounding to the nearest LED cut point on\nthe strip, I cut the LED strip and soldered each end together. For where I have\nthe hardware, I started at the bottom left corner, went right, then up, then left\nand finally down again. With 30 LEDs/metre, I ended up with 46 LEDs across,\nand 26 LEDs high. (Or 144 channels if you wanted to think of it that way).

\n

The LED strip that I bought conveniently has a 3M branded peel off sticky back.\nSo, with the assistance of a friend, we simply stuck the LED strip to the edges\nof the back of the TV in the order I had decided.

\n

The only remaining connections were for 5V power, and the single data input wire\nfor the Arduino. For power, I temporarily soldered on a molex connector and powered\nit from a desktop computer that I have in the theatre.

\n

\"The

\n

\"The

\n

\"A

\n

Hardware - driver

\n

To drive the system, I used an Arduino Uno R3 that I had sitting around the place.\nI also had an ethernet shield spare, which turned out to be an excellent addition.

\n

The hardware side is very simple. Attach ethernet shield to Arduino, and then plug\nin the ground and driver pin for the LED strip. The Arduino is currently powered\nvia USB off the desktop computer in the theatre.

\n

\"The

\n

Firmware - Arduino

\n

The Arduino sketch is pretty simple. It revolves around two different types of\nUDP packets sent to it via the network.

\n

The first packet type is just 6 bytes long, and is an input select packet.\nIt contains a magic constant to indicate what it is, and then another\nbyte to select what input, and finally four more bytes that\nare the colour to set the entire strip to.

\n

The second packet type is up to 606 bytes long in my sketch. To make it longer,\nyou'll have to modify the sketch. This packet contains the magic byte to indicate\nit contains pixel data, then another byte to say what input it's for. If the input\nbyte doesn't match the current input (set with the input packet type), the rest of\nit is ignored. If it does match, the next two bytes indicate how many pixels follow.\nAfter that is the pixels, 32 bits each. All of these multi byte values are assumed\nto be in the correct byte order for AVR microcontrollers (little endian). You'll see\nlater the computer software changes the byte order before the packets even leave the\ncomputer, to save trying to wangle byte orders on the Arduino itself.

\n

When the Arduino gets a pixel packet, it just writes the pixels out onto the strip\ndirectly. It doesn't do anything special for synchronisation.

\n

The sketch I used is checked into the tv-backlight repository on Github.

\n

Sofware - The prototype

\n

My initial temporary target platform was Windows. I had a movie day planned a\nweek ahead, and had chosen Windows as the playback platform for a few reasons\nrelating to the performance of my home network. It's a long story.

\n

Anyway, I knew I was going to have to delve into some deep graphics related code,\nand graphics coding is one of my weak points. So, purely to get it working,\nI wrote a Python script on my Linux desktop to prototype it. I ended up using GTK,\nthinking that it would allow me to test the prototype on Windows as well, and even OSX.

\n

The Python script grabbed the screen, calculated the edge pixels, and sent off the\nUDP packet, and then repeated this forever.

\n

To calculate the edge pixels, I cheated somewhat. After taking the screenshot\n(at 1920x1080), I resized it down to the number of pixels on the TV (46x26),\nwhich dropped back to some nice fast C code. The resize took less than a millisecond\nto complete. I used the nearest interpolation method so it would choose hard\nvalues for each pixel, rather than trying to smooth them with the adjacent pixels.\nAfter it was resized, it's a lot more feasible to scan the edges with Python.

\n

Python also contains the super helpful struct library, which made it trivially\neasy to take the colours and convert them into binary data (with the appropriate byte order)\nfor the UDP packets.

\n

On my Linux machine, Ubuntu 12.04 with compositing turned on, the GTK screen\ncapture took about 350ms. The rest of the processing and UDP sending accounted\nfor another 5ms or so. So the prototype worked, and worked accurately, but it wasn't fast enough.

\n

The scripts I used are checked into the tv-backlight repository on Github.

\n

Software - Windows

\n

For kicks, I decided to install Python 2.7, PyGTK, and Scipy on my Windows desktop\nto see how that did. Predictably, it was capturing frames in 2.7 seconds, and then\ncompleting the processing in another few milliseconds. So I asked a friend who is\nmuch more conversant in graphics programming about a better way to capture frames\nin Windows 7. The short answer was "this is a rabbit hole". His only suggestion\nwas to disable Aero and see if that helped.

\n

So disable Aero I did. Suddenly, my Python script was capturing frames at 40FPS -\nincluding a 1080p video in VLC - and passing that long to the LED strip. In realtime,\nthe effect was quite spectacular and exactly what I had been trying to build!

\n

With some more tweaking - specifically to average several frames together to stop\ninstant changes in the scene causing flashing, and code to detect the black bars\nin theatrical style widescreen videos, the system was production ready for a Lord\nof the Rings marathon. 12 hours of 1080p video later, and neither the computer nor\nthe Arduino showed any signs of stopping.

\n

Interestingly enough, I later tried to run the Python script on my Linux laptop,\nwhich is fitted with an Intel HD3000 graphics chipset. With compositing turned on,\nit was capturing at upwards of 80 frames per second.

\n

Some other testing revealed that the script isn't able to capture DirectX surfaces.\nSome games that I tried did work with quirks, and other games just came up black\nto the script. To fix this properly, I suspect something using DirectX (or even\nsomething lower level) will be required to get it to work.

\n

Sofware - OSX

\n

My primary media centre is a Mac Mini running Plex. So having it working great\non a Windows machine wasn't that useful to me. However, the Python script didn't\nwant to work on the Mac, and my simple attempts to install PyGTK on the Mac resulted\nin something weird that didn't work...

\n

A little bit of Googling showed that OSX ships with Python Objective C bindings,\nwhich allow access to core graphics. After some copy and pasting of examples from\nStackOverflow, I had my script updated to capture images and process them. CoreGraphics\nwas a lot lower level than I expected, so rather than trying to figure out how to get it\nto composite the average image like the PyGTK one does, I hacked together a simple numerical\naveraging algorithm to compare frames.

\n

On OSX, the end result is a Python script that can capture and send frames anywhere\nfrom 40 to 70 frames per second. The net result is a very nice ambilight system for my media centre.

\n

Switching inputs

\n

When I built my home theatre setup in February 2013, I was pleasantly surprised that\nbasically everything was IP controllable. The only item that wasn't IP controllable\nwas my cheapy DealExtreme LED strip lighting, which required IR signals. I had a spare\niTach IR gateway, so everything became IP controllable.

\n

Rather than toting three remotes to control everything, I wrote a simple Python\nweb application, affectionately called Theaterizr, which I access through my Android phone.\nIt's also accessable via my partner's iPhone too, which was the reason for making it a web app.\nThis web app is organised into scenes, and as you enter each scene, it sends the\nappropriate IP commands to the devices to configure them correctly - much easier\nfor my partner to use than juggling three remote controls!

\n

Because I have this web application already, I can hook it to send input switch\ncommands to my Arduino. This means I can have the ambilight script running all\nthe time on the Windows desktop and the Mac desktop, and the Arduino will simply\nignore any packets that don't match it's input.

\n

Quirks and limitations

\n

The first quirk is that the WS2812 LEDs are very, very bright. In fact, they were\ntoo bright and drowned out the TV when I first set them up. To get around this,\nthe script divides the colour values by two to half their brightness. This obviously\nisn't colour correct, but worked well enough for my setup.

\n

The other quirk was the colour definition. Yellows and Reds show up fantastically\nand match the screen colours almost exactly. Greens are close to the screens colours,\nbut not exact. And blue seems to be quite different from what's on the screen.\nI don't really know much about colour and colour systems, but I suspect the difference\nis for two reasons. The first being that I adjusted the TV with the help of a friend\nto make the colours work better on my TV set. The second reason is that the wall\ncolour isn't white. It's a Dulux colour called "Buff It" which probably interferes with the blue mostly.

\n

Enhancements to come

\n

There are still a few enhancements to be made to this system. But they are relatively minor:

\n\n

Building your own

\n

If you would like to build your own system, here are a few notes about the code and system that I've published:

\n\n", "url": "https://freefoote.net/tv-content-based-backlight/", "title": "TV Content Based Backlight", "summary": "Expanding the TV onto the wall.", "date_modified": "2013-08-01T11:13:27.301Z" }, { "id": "https://freefoote.net/rain-testing-via-the-internet-sprinkler-controller/", "content_html": "

The sprinkler controller described here applies to my previous house. The controller worked\nflawlessly for four years. The new owners of the house wanted a traditional controller, so\nit was uninstalled and replaced.

\n

Wow! This is an old project. Your milage may vary with this project. You might want\nto look into Tasmota on an ESP32 based chip for\nthe microcontroller aspects of this project. Or even into the\nOpenSprinkler project which can do most of this already and\nis in fact what I'm currently using at my current home.

\n

When I bought my first house, the gardens attached to it had reticulation in them already,\nwhich was great. The lawn was not reticulated, but it turns out that an appropriate\nhose had been placed near the lawn, but never hooked up to sprinklers. So it wasn't\ntoo hard a job to reticulate the lawn as well.

\n

However, the one thing about the reticulation is that there were a series of valves.\nYou turned on the valve that you wanted, and then the tap, and then the garden got watered.

\n

Which is all good and well. In Perth we have some serious (and much needed) water\nrestrictions relating to the watering of gardens. You can only water your garden\ntwo days a week, on a roster based on the last digit on your house number.\n(My last digit is 5, which makes my watering days Sunday and Wednesday).\nThe other restriction is that you have to water before 9AM, or after 6PM -\nand only in one of those two intervals for that day (ie, you can't water\nSunday morning and Sunday night too).

\n

I personally got irritated by having to manage the sprinklers. You'd have to\nturn them on, time them for a few minutes, and then switch to another station.\nBeing the lazy person that I am, I figured I had to automate the whole system.

\n

So that's what I did. The current system is probably what you would call version 2.

\n

Some parts of the system were scavenged parts. That is why the hardware is kinda ugly...\nbut amazingly enough, the system works, and hasn't broken down since its construction.

\n

Features of my System

\n

The current system has these features:

\n\n

Ideally it would contain a rain sensor here that it collects data from, rather than\nviewing the rainfall at the nearest government weather station, which is just over\n6 kilometres away, but this system works well enough.

\n

Version One

\n

The original version of this sprinkler controller was serially controlled instead.\nThe controller was a small PIC16F series microcontroller, with a simple program I\nfound on the net. You passed it serial bytes which told it which outputs to turn on;\nand it did that. That was about as complicated as the controller got.

\n

I actually used a PIC experimenters kit that I built quite a few years ago as the\nbase for the project. I connected the serial output to my terminal server, so that\nI could ethernet-enable this sprinkler controller (I have an ancient\nLantronix ETS16P\nterminal server - it was a cheapy on Ebay).

\n

Other than that, version one did not differ from the current version.

\n

Hardware

\n

Solenoids and plumbing

\n

At the local Bunnings I acquired four water solenoids. They are 24VAC soleniods\nthat you just connect inline to the pipes in question. I bought ones that matched\nthe pipes I already had, so they were pretty easy to hook up.

\n

You can see them in the photo below.

\n

\"Sprinkler

\n

The valves were existing, and I just cut the pipes at the appropriate spots and\ninserted the solenoids. Originally I just had the three solenoids - but I quickly\nfound out that it was impossible to prevent the supply side from leaking, no matter\nwhat I did to it. So I then added the master valve, and kept whatever was on the\nsupply side of that as tight as possible. It still leaks a little, but not much at\nall (much, much less than a dripping tap).

\n

Because of the master valve, you have to open one of the other valves first\n(to select what to water) and then open the master valve. Turning the sprinklers\noff is just reversing this proceedure.

\n

Relay board

\n

As the solenoids were 24VAC, you can't just hook a pin from a PIC up to them and\nhave it work (PICs are pretty good, but that's a little out of their range).\nSo, you need relays to turn on the solenoids

\n

And then you need a 24VAC power source. Which are, surprisingly, not that common\n(at least, when you're scrounging for parts). However, I have a weakness for fairy lights -\nand as such have quite a number of sets of fairy lights. Naturally, some of these\nare really cheap and nasty - and poorly made. I had one such set of lights where 60%\nof the lights stopped working (and yes, I did go through and replace globes to no avail)

\n\n

Then comes the relay board. I had some relays sitting in my cupboard that I had\npulled out of some other piece of equipment a long time ago. They had a 9VDC coil,\nbut could switch up to 110VAC through them. Perfect!

\n

I also needed to step down the 24VAC to 9VDC for the relays, and also for the\nethernet controller board (which can take 7-35V DC, but not AC). So I put together\na simple 7809-based regulator circuit to step the voltage down. (The version of the\nregulator circuit shown in the photos is hardware version 2 - I managed to blow up the\nfirst version, but that was me being silly and just soldering the parts together,\nand not to veroboard like you see in the photos.)

\n

Below is a schematic for the power supply:

\n

\"Power

\n

You still can't power a relay from a PIC pin. So I dug up a few NPN transistors,\na few resistors (all spares in my cupboard), a bit of veroboard, and created the\nmonstrosity that is my relay board.

\n

Below is a schematic of what the relay board looks like. Only one of the four\nidentical relay circuits is shown.

\n

\"Sprinkler

\n

Below is a photo of the relay board, on both sides. As you can see, it's really,\nreally ugly. Pretty much to the point where I'm embarassed to publish it on the internet.

\n

\"Top

\n

\"Bottom

\n

Here is the power supply (before it got converted into the cooling stack, more\ndetails on that later):

\n

\"Power

\n

Ethernet Controller board

\n

The ethernet controller board is a SBC45\nfrom Modtronix, in Queensland, Australia. It is a neat little\nboard - comes with a PIC18F452, a 32k serial EEPROM, RTL8019 ethernet controller.\nIt also has a chip to bring the RS232 outputs up to RS232 levels.

\n

Below is a photo of the board itself.

\n

\"SBC\"

\n

Below is a photo of the soldered connections to the board.

\n

\"SBC

\n

Hooking it all together

\n

Connecting the relay board to the ethernet controller was quite simple.\nI chose to solder the wires between the two. Not quite sure why, but I don't\nexpect to make too many changes.

\n

After that the rest of the connections are fairly straightforward. Below is a\nphoto of all the parts connected together, on the bench.

\n

\"Sprinkler

\n

Installation into a weatherproof box

\n

This is yet another ugly part of the project. I stuffed all the electronics into\na weatherproof box, so it could be mounted outside, near the solenoids that it controls.

\n

The boards are held down by sticky pads that you can put cable ties through.\nNo doubt this is going to fall apart inside the box, probably during summer...

\n

Below is a photo showing the layout of the parts inside the box.

\n

\"Sprinkler

\n

The Cooling Stack

\n

Future Daniel to past Daniel: just use an off the shelf buck/boost module. So much\nmore efficient than a linear regulator that I used at the time, and it won't get\nhot like mine did!

\n

You can see in the photos up until this point that I just bolted a lump of\naluminium to the 7809 regulator on the power supply in an attempt to keep it cool.\nHowever, at idle, the plate still got quite hot to the touch - hot enough that I\ncouldn't hold it with my fingers for more than a few seconds. Not really something\nyou want to have inside the box...

\n

And why, you may ask, did it get hot if it isn't drawing much power? When you do\nthe maths on it - the incoming voltage is 24VAC, which we convert to DC. Due to\nthe poor regulation of the plugpack, it's more like 30VAC incoming. Once rectified,\nit's still 28VDC. The 7809 regulator then has 28VDC on one side and 9VDC on the\nother side. If it draws up to 1 amp of current (as it is designed to do at peak),\nit has to dissipate 19W of heat. (28V - 9V times 1A). At idle (say 100mA) it only\ndissipates 1.9W of heat - still enough to require some better heatsinking.

\n

My final solution required another hole in the box. I got a lump of 40mm2\nsteel tube, and bolted that to the top of the box. The 7809 is then bolted to the\ninside of this tube. This does the job and dissipates all of the heat. After installation,\nthe system survived a 44°C day, so I figure the cooling is adequate.

\n

Below are a few construction photos of it. I chose to move the capacitors into\nthe stack too, but the ends were covered with heatshink before they were placed\ninto the tube, to prevent any shorts.

\n

\"Sprinkler

\n

\"Sprinkler

\n

\"Sprinkler

\n

Final Installation outside

\n

The whole setup was eventually fixed to the wall near the sprinklers. I placed\nit high up so it was just under the eaves, to keep it out of the sun.

\n

In the end I decided to feed both power and ethernet via two CAT5 cables. Both\ncables run back to the rack in my storeroom - one was terminated at both ends with\nRJ45 jacks and hooked up to the ethernet switch. The other cable had a DC power\nconnector soldered on to it at the rack end - to prevent me trying to plug it into\na RJ45 socket - and then had the transformer plugged into that. This also keeps\nthe transformer inside and away from the weather (it was an 'indoor only'\ntransformer anyway).

\n

Below are two photos of the setup, one before I siliconed up all the holes, and\nanother after the job was complete.

\n

\"Sprinkler

\n

\"Sprinkler

\n

Software

\n

On the ethernet controller board

\n

The ethernet controller board already comes with a built in webserver and set of pages.

\n

You can download new web pages to it via FTP, so I just replaced the existing\nindex.htm, and added an index.cgi. These pages are really simple - they just provide\nbuttons to toggle each sector, and also a button to turn all outputs off.

\n

\"Sprinkler

\n

The firmware that comes with the controller board is not 100% perfect, though. I\nwas hoping to get away without having to flash the controller, but I was unable to do so.

\n

In the end, only two changes were required to the firmware. The first one was to\ninitialise all the outputs to off when the system started. Before I made this change,\nthe outputs would randomly be on or off when the power was applied to the system -\nobviously unacceptable, as you could easily have the sprinklers turn on, and the\nsystem wouldn't realise!

\n

The other change I made was to the inbuilt FTP server. It didn't respond to the\nBIN command, which meant that UNIX ftp clients tried to upload the files in ASCII mode,\nwhich resulted in a broken web pages image. After this change, the system worked perfectly.

\n

On the server

\n

Cron Entries from Google Calendar

\n

To get the cron entries from Google Calendar, I hacked together a python script\nto download an iCal (.ics) version of the relevant calendar, and then parse it,\nand create cron entries/

\n

When I wrote it I was being quite lazy and I looked around for an iCal library.\nThere doesn't seem to be too many good complete libraries for iCal. In the end I\nfound one that would parse an iCal file, and then be able to answer the query,\n"are there any events X days from now". So I do this from tomorrow up to seven days\nfrom now, and parse those events into appropriate crontab entries - absolute entries,\nwith the date in them. (So they are all one-shot).

\n

A quick python module to read and write the current users crontab allowed for\nrelatively simple modifiation of the users crontab. The entries are in their own\ncomment-delimited section at the bottom of the users crontab, leaving other\nexisting entries untouched.

\n

This script is kinda wierd: in one sense, it's way underengineered; and in another\nsense, it is way overengineered. What happens to the calendar events is described\nin a ini file, which in my case looks like this: (as you can see, this is the overengineered bit)

\n
[global]\ndaysahead = 7\n\n[calendars]\nwatering = http://www.google.com/calendar/ical/[snip - contains private UID here]/basic.ics\n\n[commands]\nwatering = sprinklers raincheck\n\n[cmd-sprinklers]\n# match contains named match groups: (?P<foo>.*)\n# oncommand contains printf string for python, where %(foo)s is\n#    replaced with named "foo" from match\n# Special items available: (these take precendence over the ones from match)\n# calendar = symbolic name of calendar (from above)\n# date     = date of run (ontime) (string in YYYY-MM-DD)\n# time     = time of run (ontime) (string in HH:MM:SS)\n# offdate  = offdate (string in YYYY-MM-DD) (Only if offcommand is set)\n# offtime  = offtime (string in HH:MM:SS)   (Only if offcommand is set)\n# duration = duration (string in seconds, eg, 3600)\n\nmatch = Garden Water (?P<area>\\w+)\noncommand  = /home/daniel/sprinklers/sprinkler-control-wrapper %(area)s on\noffcommand = /home/daniel/sprinklers/sprinkler-control-wrapper %(area)s off\n\n[cmd-raincheck]\nmatch = Rain Check\noncommand = cd /home/daniel/sprinklers; ./rain-last-few-days.py\n
\n

This results in the following crontab for my user:

\n
# Update the local crontab from Google Calendar. 00:10 each day.\n10 0 * * * cd /home/daniel/icaltocron; ./icaltocron.py\n\n# ICALTOCRON-START\n# Automatically generated - if you edit, any changes will be removed\n# next time icaltocron is run.\n27 6 23 9 * /home/daniel/sprinklers/sprinkler-control-wrapper frontgarden on\n37 6 23 9 * /home/daniel/sprinklers/sprinkler-control-wrapper frontgarden off\n16 6 23 9 * /home/daniel/sprinklers/sprinkler-control-wrapper backgarden on\n26 6 23 9 * /home/daniel/sprinklers/sprinkler-control-wrapper backgarden off\n5 6 23 9 * /home/daniel/sprinklers/sprinkler-control-wrapper lawn on\n15 6 23 9 * /home/daniel/sprinklers/sprinkler-control-wrapper lawn off\n0 6 23 9 * cd /home/daniel/sprinklers; ./rain-last-few-days.py\n27 6 26 9 * /home/daniel/sprinklers/sprinkler-control-wrapper frontgarden on\n37 6 26 9 * /home/daniel/sprinklers/sprinkler-control-wrapper frontgarden off\n16 6 26 9 * /home/daniel/sprinklers/sprinkler-control-wrapper backgarden on\n26 6 26 9 * /home/daniel/sprinklers/sprinkler-control-wrapper backgarden off\n5 6 26 9 * /home/daniel/sprinklers/sprinkler-control-wrapper lawn on\n15 6 26 9 * /home/daniel/sprinklers/sprinkler-control-wrapper lawn off\n0 6 26 9 * cd /home/daniel/sprinklers; ./rain-last-few-days.py\n27 6 30 9 * /home/daniel/sprinklers/sprinkler-control-wrapper frontgarden on\n37 6 30 9 * /home/daniel/sprinklers/sprinkler-control-wrapper frontgarden off\n16 6 30 9 * /home/daniel/sprinklers/sprinkler-control-wrapper backgarden on\n26 6 30 9 * /home/daniel/sprinklers/sprinkler-control-wrapper backgarden off\n5 6 30 9 * /home/daniel/sprinklers/sprinkler-control-wrapper lawn on\n15 6 30 9 * /home/daniel/sprinklers/sprinkler-control-wrapper lawn off\n0 6 30 9 * cd /home/daniel/sprinklers; ./rain-last-few-days.py\n# ICALTOCRON-END\n
\n

You can download the whole icaltocron script.

\n

Determining if there was enough rain

\n

To determine if there was enough rain, we look at the rain for the last few days\nat the nearest government weather station. The Australian federal government kindly\nputs the last few days of observations (direct from the weather station) onto the web.\nThese are not quality controlled, but are really just raw observations.

\n

(Disclaimer: I used to work for the Australian Bureau of Meteorology.\nThat's how I know about these things.)

\n

So I simply screen-scrape the page, tally up the rain for the last few days,\nand if it is over a certain threshhold, the sprinklers dont get turned on.\nAs you can see, there are lots of hardcoded things in the script - bad idea -\nand also, all it does is write a file, whose contents is the amount of rain for\nthe last few days, if it is over the built in threshold of 5mm.

\n

To make this trivial, I am using the BeautifulSoup library for Python. It makes\nscreenscraping mostly-trivial for Python. (One of my past jobs was to write\nscreenscrapers, and Python was my favourite for writing them).

\n

Future Daniel to past Daniel: the BOM now provides a JSON feed of this data instead,\nwhich saves all this finangling that I did below... here is an example.

\n
#!/usr/bin/env python\n\nfrom BeautifulSoup import BeautifulSoup\nimport urllib2\nimport os\n\n# URL for Jandakot airport last few days obs.\nURL = "<a href="http://www.bom.gov.au/products/IDW60801/IDW60801.94609.shtml">http://www.bom.gov.au/products/IDW60801/IDW60801.94609.shtml</a>"\nFLAGFILE = "/home/daniel/sprinklers/enough-rain"\n\ntry:\n\tos.unlink(FLAGFILE)\nexcept OSError:\n\t# File not found... ignore...\n\tpass\n\npage = urllib2.urlopen(URL)\nsoup = BeautifulSoup(page)\n\ntotalrain = 0.0\nsteprain  = 0.0\n\n# For each table with data in it.\ntables = soup.findAll('table', attrs={'class': "tabledata"})\ntables.reverse()\nfor table in tables:\n\trows = table.findAll('tr', attrs={'class': "rowleftcolumn"})\n\trows.reverse()\n\tfor row in rows:\n\t\tcell = row.findChildren()[13]\n\t\ttry:\n\t\t\train = float(cell.contents[0])\n\t\texcept ValueError, e:\n\t\t\t# Hmm... invalid input.\n\t\t\train = steprain\n\t\tif rain > steprain:\n\t\t\tsteprain = rain\n\t\tif rain < steprain:\n\t\t\ttotalrain += steprain\n\t\t\tsteprain = 0.0\n\n# And now we have the rain for the last few days.\n# More than 5mm? Set our "too much rain" flag.\nif totalrain >= 5.0:\n\tf = open(FLAGFILE, 'w')\n\tf.write(str(totalrain))\n\tf.close()\n
\n

You can download the rain-last-few-days.py script.\nIf you want to use it (in Australia, at least) you can just find your nearest weather\nstation and replace the URL in the script with that one.

\n

Toggling sprinkler outputs

\n

With the old serial controller it was much harder to toggle bits, because I had\nto open a TCP socket to the terminal server, and then write down a series of bytes\n(binary values) to tell it what outputs to turn on. I (for some odd reason) decided\nto do this in a PHP script (I can't recall why). Fortunately, with the new ethernet\ncontroller, this is no longer a problem - all the outputs can actually be toggled\nwith just a shell script - with the help of wget.

\n

And to toggle the outputs, I ended up with the following shell script. As you\ncan see, it's pretty ugly, and uses a file to let itself know it is already turned on.

\n
#!/bin/bash -e\n\n# TODO: This is hardcoded...\nCONTROLLER="10.41.61.10"\nONFILE="/tmp/sprinklers-lock"\n\nMASTERVALVE="4"\nLAWNVALVE="0"\nFRONTVALVE="1"\nBACKVALVE="2"\n\nfunction valveon {\n\twget -O /dev/null -o /dev/null http://$CONTROLLER/ioval.cgi?B$1=0\n}\nfunction valveoff {\n\twget -O /dev/null -o /dev/null http://$CONTROLLER/ioval.cgi?B$1=1\n}\n\n: ${1?"Usage: $0 <area> <on|off> Areas: lawn, backgarden, frontgarden"}\n\nif [ "$1" = "clean" ];\nthen\n\tvalveoff $MASTERVALVE\n\tvalveoff $FRONTVALVE\n\tvalveoff $BACKVALVE\n\tvalveoff $LAWNVALVE\n\trm -f $ONFILE\nfi\n\necho -n "Turning "\necho -n "$1"\necho "$2"\n\nif [[ "$1" = "lawn" || "$1" == "backgarden" || "$1" == "frontgarden" ]];\nthen\n\tif [ "$1" = "lawn" ];\n\tthen\n\t\tTHISVALVE="$LAWNVALVE"\n\tfi\n\tif [ "$1" = "backgarden" ];\n\tthen\n\t\tTHISVALVE="$BACKVALVE"\n\tfi\n\tif [ "$1" = "frontgarden" ];\n\tthen\n\t\tTHISVALVE="$FRONTVALVE"\n\tfi\n\n\tif [ "$2" = "on" ];\n\tthen\n\t\tif [ ! -e "$ONFILE" ];\n\t\tthen\n\t\t\techo "$1" > "$ONFILE"\n\t\t\tvalveon $THISVALVE\n\t\t\tvalveon $MASTERVALVE\n\t\telse\n\t\t\techo "Sprinklers already on!"\n\t\t\techo "Try with 'clean'"\n\t\tfi\n\telse\n\t\tvalveoff $MASTERVALVE off\n\t\tvalveoff $FRONTVALVE off\n\t\tvalveoff $BACKVALVE off\n\t\tvalveoff $LAWNVALVE off\n\t\trm -f "$ONFILE"\n\tfi\nelse\n\techo "Invalid area name."\nfi\n
\n

You can download the sprinkler-control.sh script.

\n

Combining the rain measurement with the sprinklers

\n

The sprinkler-control script itself has no means to check if there has been too\nmuch rain - as it should be; because this should be able to be called by itself,\nto turn the sprinklers on manually.

\n

So, for that reason, a wrapper script was written to check how much rain there\nwas, and then turn the sprinklers on, if relevant.

\n
#!/bin/sh\n\n# Only start the sprinklers if it has not rained too much\n# over the last few days.\n\nRAINFILE="/home/daniel/sprinklers/enough-rain"\nCONTROL="/home/daniel/sprinklers/sprinkler-control"\n\nif [ "$2" = "on" ];\nthen\n\tif [ ! -e "$RAINFILE" ];\n\tthen\n\t\t$CONTROL $1 $2\n\telse\n\t\tRAINQTY=`cat "$RAINFILE"`\n\t\techo "Not turning on sprinklers - seen $RAINQTY mm in last few days"\n\tfi\nelse\n\t$CONTROL $1 $2\nfi\n
\n

You can download the sprinkler-control-wrapper.sh script.

\n

Fun with Sprinklers

\n

One year someone organised an easter egg hunt at my place. It turns out I was the\none who went and hid all the eggs in my backyard, and then let everyone loose to go and find them.

\n

To clarify, this was before I had children and before my friends had children. This was grown\nadults running around a backyard finding easter eggs...

\n

And there is only one way to make an easter egg hunt interesting: turn the sprinklers\non. Except I didn't want to run back inside the house to do it (too obvious, and\nI miss the fun)... and I also wanted randomness. So, I knocked up a few more lines\nof python code to do this for me.

\n
#!/usr/bin/env python\n\nimport random\nimport datetime\nimport time\nimport os\n\nrandom.seed()\n\nwhile 1:\n\tnextFire    = random.randrange(1, 4)\n\tfireCircuit = random.randrange(1, 3)\n\n\tnow = datetime.datetime.now()\n\tthen = datetime.datetime.now() + datetime.timedelta(0, nextFire * 60)\n\n\tprint "Now: %s firing %s" % (now.strftime("%H:%M:%S"), then.strftime("%H:%M:%S"))\n\n\ttime.sleep(nextFire * 60)\n\n\tif fireCircuit == 1:\n\t\tos.system("./sprinkler-control lawn on && sleep 5 && ./sprinkler-control lawn off")\n\telse:\n\t\tos.system("./sprinkler-control backgarden on && sleep 5 && ./sprinkler-control backgarden off")\n
\n

The script will choose a random time period, between 1 to 3 minutes, and then choose\none of the sprinkler areas to turn on (either the lawn or the back garden, the\ntwo locations where people were picking through for eggs). If the sprinklers\ncame on, they would only be on for 5 seconds (enough to dampen people caught in\nit, but not waste too much water).

\n

It ended up running for about 30 minutes whilst people were looking for eggs.\nI actually found out afterwards that it had a bug - only the lawn sprinklers ever\nturned on. Originally, when choosing the fireCircuit value, the range was 1,2 -\nwhich meant that it would only ever return 1. Oops. It still worked ok, because my\nlawn was very unruly at this point, so they kept finding eggs in it until they\nstopped looking.

\n

This actually occured with version 1 of my sprinkler controller, but it would\nwork with no changes in the current version of the hardware.

\n

You can download the tempt-fate.py script.

\n

Results

\n

This document has been many months in the making. As a result, I've had an automated\nsprinkler system for around 18 months now. It has gone through two hardware revisions,\nand I'm really happy with the system. It really does look after itself - if I happen\nto be awake at 6AM, I can hear the sprinklers going off exactly like they are supposed\nto... and then turn over and go back to sleep until I really have to get up.

\n

Since putting the system together, I've had no failures - it has always worked if\nit was supposed to work. Which is my kind of system - it just works! No doubt there\nwill be a day when it fails to work, but I'll deal with that when it happens.

\n

The only other things to note are the uglyness of the hardware. As I've said, most\nof it was built from parts I scavenged from my spare parts bin. If I was building\nthe system for another person, I would clean it up a lot.\nA suitable relay board\ncan actually be purchased from Modtronix - although the board takes a SBC65 series\nSBC. In fact, another person used a SBC65 and an IOR5E board from Modtronix into a\nsprinkler controller of their own\nwhich is much neater than my solution. He also modified the firmware on the device\nto make the device enforce the on-time of the relays.

\n

Other than that, enjoy...

\n", "url": "https://freefoote.net/rain-testing-via-the-internet-sprinkler-controller/", "title": "Rain-testing via the internet sprinkler controller (old)", "summary": "A very, very old sprinkler controller that decided whether or not to turn on based on data fetched from the internet.", "date_modified": "2005-06-26T11:13:27.301Z" } ] }