Gerrit Niezen

Maker of open-source software and hardware.

Moisture sensor

To know when I need to water my seedlings in the propagator, I got an analogue capacitive soil moisture sensor for £3.67 (including P&P).

The pinout is a simple three-wire interface, with Vcc, ground and the analogue output pin. To read the values, I just needed to do the following in Espruino:

const moistureSensorPin = A5;
const airMoisture = 955;
const waterMoisture = 670;

pinMode(moistureSensorPin, 'analog');

const moisture = Math.round(analogRead(moistureSensorPin) * 1000);
const moisturePercent = Math.round(map(moisture, airMoisture, waterMoisture, 0, 100));
console.log(`Moisture is ${moisture} ~ ${moisturePercent} %`);

if (moisturePercent < 16) {
  console.log('Needs more water');
  // TODO: alarm notification

To get airMoisture and waterMoisture, I took readings from the sensor when it was dry and placed in a glass of water respectively. The map() function I got from the Arduino libraries:

function map(x, in_min, in_max, out_min, out_max) {
  // from
  return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;

I place the sensor directly under the rockwool cubes of the seedlings to measure the moisture content.

Water level sensor

To detect the height of the water level sensor, I'm using a VL53L0X LIDAR sensor, that uses a laser and time-of-flight measurements to accurately measure distances between 50mm and 1200mm. It's pretty amazing that they can fit a laser and a detector into a package that's barely larger than a grain of rice.

The sensor uses an I2C interface, so I connected the pins as follows:

  • SDA pin to Espruino B3
  • SCL pin to Espruino B10
  • GND pin to ground
  • VIN pin to Espruino 3.3V
  • X pin to Espruino 3.3V (maybe not necessary)

To set it up in Espruino, you just do:

  I2C2.setup({ sda: B3, scl: B10 } );
  laser = require("VL53L0X").connect(I2C2);

To then perform a measurement, you do:

// measure water level
const distance = laser.performSingleMeasurement().distance;
if (distance > 20) {
  console.log(distance, 'mm');
  // TODO: minimum water level notification

I’m publishing this as part of 100 Days To Offload. You can join in yourself by visiting

#100DaysToOffload #day67 #hydroponics

For my hydroponics setup there are a bunch of reasons why I would want to be notified of an issue, e.g.:

  • Temperature of range
  • Lights on/off outside of schedule
  • Water overflow
  • Humidity out of range
  • Water level too low

Since my setup is based around using Bluetooth LE for wireless communication, that means I need to think about how to get notified about these issues on my mobile device.

It used to super simple to get mobile notifications in Android from Bluetooth using Eddystone-URLs, but Google disabled this in 2018 due to abuse by marketers. Eddystone and iBeacons are now only supported in native apps.

It is possible to send web push notifications from a PWA, but this requires service worker support, which is not supported by Web Bluetooth yet.

This leaves having a web server somewhere within range of the Bluetooth device, that can subscribe to Bluetooth notifications an send send these out as web push notifications to your mobile device.

Edit: There is also another option: The Physical Web Association now provides an unbranded native app that can be used to interact with Eddystone-URL beacons. Maybe this app can then launch the PWA?

I’m publishing this as part of 100 Days To Offload. You can join in yourself by visiting

#100DaysToOffload #day66 #hydroponics

For my hydroponics setup, I want to be able to turn on and off both the LED grow lights and the water pump using a microcontroller. This is typically done using relays, and is the implementation I described in yesterday's blog post.

I've been thinking it over more and realising that having a solution that can be implemented by anybody shouldn't rely on dealing with AC mains voltages and wiring. Smart plugs using Bluetooth or WiFi having been coming down in price a lot, to the extent that they're a viable alternative to relays, especially if you take the cost of AC sockets and enclosures into account. If you don't want to have exposed wiring, you may want to consider putting your electronics inside a plug case.

Smart plug options

Bluetooth LE smart plugs are used by HomeKit and Philips Hue, but they're still a bit expensive at around £30 per plug. They also don't make use of the standardised Bluetooth profiles and services, but implemented their own proprietary BLE services.

WiFi smart plugs are around £10 each, and you can buy a WiFi power Strip with 3 outlets for £30. Unfortunately these smart plugs are getting more locked down, where you may be forced to use the manufacturer's app and proprietary API to get access to the plugs.

Thanks to Espruino's excellent documentation, I found another option: remote control sockets at around £5 each. They operate in the 433MHz range, which means you use a dirt cheap 433MHz transmitter to talk to them.

I really like this last option, as they use a simple wireless protocol that's easy to implement, and they're almost cheaper than using relays.

I’m publishing this as part of 100 Days To Offload. You can join in yourself by visiting

#100DaysToOffload #day65 #hydroponics

For my ebb-and-flood (also called flood-and-drain, or ebb-and-flow) hydroponics system, I need to pump water over the flood table at regular intervals. The time between flooding depends on various things, of which the growing medium is the deciding factor:

  • Expanded clay pebbles: 4 to 8 times a day (every 2 to 4 hours)
  • Coconut coir: 3 to 5 times a day (every 3 to 5 hours)
  • Rockwool: 1 to 5 times a day (once a day to every 3 hours)

I'm going to put rockwool starter cubes in expanded clay pebbles, so every 3 hours sounds like a good idea. I then also need to figure out how long to flood the table for, which depends on how quickly the pump floods the table. Let's say it's around 30 seconds, which then leads to the following Espruino script for my Pixl.js:

const floodIntervalMinutes = 180;
const floodDurationSeconds = 30;

const pumpPin = B15;
const waterSensorPin = B3;

setInterval(() => {
  if (!digitalRead(waterSensorPin)) {
    console.log('Water detected!');
    digitalWrite(pumpPin, 0);
}, 1000);

setInterval(() => {
  // TODO: check if daylight
  // TODO: max delay between floodings

  digitalWrite(pumpPin, 1);

  setTimeout(() => {
    digitalWrite(pumpPin, 0);
  }, floodDurationSeconds * 1000);

}, floodIntervalMinutes * 60 * 1000);

function onInit() {
  pinMode(waterSensorPin, 'input');
  pinMode(pumpPin, 'output');

  console.log('Current time:', (new Date()).toString());
  digitalWrite(pumpPin, 0); // make sure pump is off

Note that I also added a water sensor for switching off the pump in case there's an overflow.

What I still need to do is to add a light sensor to check if there's daylight, and then only flood during daylight hours. I then also need add a maximum delay between floodings, so that the plants don't go for more than 12 hours without water. Thanks to ChilliChump for these ideas.

I’m publishing this as part of 100 Days To Offload. You can join in yourself by visiting

#100DaysToOffload #day64 #hydroponics

This morning I discovered Trefle, an API for plants, through the r/hydro Reddit. What is exciting is that it does seem to cover a number of the variables I'm looking to include in my growing spreadsheet:

  • days to harvest
  • pH range
  • light range
  • temperature range

There are also other interesting fields, e.g.:

  • humidity
  • spread
  • average height

Trefle gets its information from a bunch of sources, including USDA, Kew Royal Botanical Gardens, Wikimedia and PlantNet.

While Trefle itself isn't open-source, it should always be free for open-source projects. Another alternative to consider, that is open-source, is OpenFarm.

I’m publishing this as part of 100 Days To Offload. You can join in yourself by visiting

#100DaysToOffload #day63 #hydroponics

For my hydroponics project, I would like to measure the ambient temperature. I've done this in the past as a WiFi temperature sensor, but this time round I wanted to use my Pixl.js board with Bluetooth LE connectivity.

Here is the code to read data from a DS18B20 temperature sensor, display a temperature graph on the Pixl.js screen, and also make it available via Bluetooth LE as an Environmental Sensing Service.

const PIN_SENSOR = A1;  // Pin temperature sensor is connected to

let ow, sensor, history;
let avrSum = 0, avrNum = 0;
let lastTemp = 0;

// Draw the temperature and graph to the screem
function draw() {
  g.drawString(lastTemp.toFixed(1), 64,5);
  g.drawString("Temperature", 64, 0);
  require("graph").drawLine(g, history, {
    axes : true,
    gridy : 5,
    x:0, y:37,

// Called when we get a temperature reading
function onTemp(temp) {
  if (temp === null) return; // bad reading
  avrSum += temp;
  lastTemp = temp;

  // send on BLE
  const th = Math.round(lastTemp * 100);
    0x181A : { // environmental_sensing
      0x2A6E : { // temperature
        value : [th&255,th>>8],
        readable: true,
        notify: true


// take temp reading and update graph
var readingInterval = setInterval(function() {
  if (sensor) sensor.getTemp(onTemp);
}, 10000);

// save average to history
var histInterval = setInterval(function() {
  history.set(new Float32Array(history.buffer,4));
  history[history.length-1] = avrSum / avrNum;
  avrSum = 0;
  avrNum = 0;
}, 60000); // 1 minute

// At startup, set up OneWire
function onInit() {
  try {
    ow = new OneWire(PIN_SENSOR);
    sensor = require("DS18B20").connect(ow);
    history = new Float32Array(128);

      0x181A: { // environmental_sensing
        0x2A6E: { // temperature
          notify: true,
          readable: true,
          value : [0x00, 0x00, 0x7F, 0xFF, 0xFF],
    }, { advertise: [ '181A' ] });
  } catch (e) {
    console.log('Error:', e);

This code is based on the lovely Pixl.js Freezer Alarm example.

I then wanted to be able to connect to the sensor via Web Bluetooth in the browser, which led to this web app:

document.addEventListener('DOMContentLoaded', event => {
  const button = document.getElementById('connectButton')
  let device

  button.addEventListener('click', async (e) => {
    try {
      if (button.innerHTML === 'Disconnect') {
        button.innerHTML = 'Connect'
      document.getElementById('p1').innerHTML = 'Connecting'
      console.log('Requesting Bluetooth Device...')
      device = await navigator.bluetooth.requestDevice({
        filters: [{
          namePrefix: 'Pixl.js'
        optionalServices: ['environmental_sensing']

      device.addEventListener('gattserverdisconnected', onDisconnected)

      console.log('Connecting to GATT Server...')
      const server = await device.gatt.connect()
      button.innerHTML = 'Disconnect'

      console.log('Getting Environmental Sensing Service...')
      const service = await server.getPrimaryService('environmental_sensing')

      console.log('Getting Temperature Characteristic...')
      const characteristic = await service.getCharacteristic('temperature')

      console.log('Reading temperature...')
      const value = await characteristic.readValue()
      const temp = value.getUint16(0, true) / 100

      console.log('Temperature is ' + temp)
      document.getElementById('p1').innerHTML = temp

      await characteristic.startNotifications()

      console.log('Notifications started')
      characteristic.addEventListener('characteristicvaluechanged', handleNotifications)
    } catch (error) {
      console.log('Argh! ' + error)
      document.getElementById('p1').innerHTML = error
      button.innerHTML = 'Connect'

  function handleNotifications (event) {
    const value =
    const temp = value.getUint16(0, true) / 100

    console.log('Temperature is now ' + temp)
    document.getElementById('p1').innerHTML = temp

  function onDisconnected () {
    button.innerHTML = 'Connect'
    document.getElementById('p1').innerHTML = 'Disconnected'

The index.html file looks like this:

    <meta name="viewport" content="width=device-width, initial-scale=1">
    <script src="main.js"></script>
    <link rel="stylesheet" type="text/css" href="main.css">
    <button id="connectButton" class="buttons">Connect</button>
    <p id="p1" class="text"></p>

One Web Bluetooth discrepancy that I discovered between desktop Chrome and Chrome on Android, is that desktop Chrome will still read a value from a service, even if the characteristic is not set to be readable. Chrome on Android, on the other hand, will throw an error. That's why you see both readable: true and notify: true in the code above.

This works great! Temperature readings in the browser are automatically updated, and it even shows you when you get disconnected from the sensor.

I’m publishing this as part of 100 Days To Offload. You can join in yourself by visiting

#100DaysToOffload #day62 #hydroponics

I've been working on building a DIY hydroponics system for the past two weeks, and have inevitably run into a number of issues already.

Spray painting

I bought two clear containers that I spray painted to prevent light from entering the container. Light could cause algae growth in the nutrient inside the containers. The first problem that cropped up was that spray painting can be very time consuming. The actual painting part is relatively quick, but you then have to wait around 20 to 60 minutes between coats, and at least 24 hours for the paint to fully dry. If you don't have a covered area to do the painting, rain delays progress even further.

As I didn't use a primer, the paint start cracking off as soon as the plastic started flexing. Flexing can happen when filling the container with water, or when drilling holes for piping.

As such, I'd recommend not spray painting plastic containers if you can avoid it. I ended up going to IKEA and buying two opaque TROFAST storage boxes instead.

3D-printed parts

As the fittings for ebb-and-flood systems are pretty expensive in the UK (£13 vs $6 in the US), I thought I'd 3D print them. The full print took about 13 hours, which is still faster than next-day delivery. They worked great until I switched to the new containers, when one of the fittings broke off completely.

I printed them in ABS, which is maybe not the greatest option for functional parts. I've now ordered some PETG filament from Prusa which should result in sturdier parts.


The kale seeds I planted in the propagator only took a day or two to start sprouting. They started stretching very quickly due to lack of natural light, even though the propagator was placed on a windowsill. My Spider-Farmer SF1000 grow light arrived yesterday, so this morning I planted some new seeds and placed the propagator under the grow light immediately.

It has not been smooth sailing so far, but if you don't fail, you don't learn.

I’m publishing this as part of 100 Days To Offload. You can join in yourself by visiting

#100DaysToOffload #day61 #hydroponics

According to the r/hydro reddit, the best value-for-money LED grow lights are the so-called “quantum boards”. These look like a bare PCB with typically Samsung LM301B LEDs, mounted to a metal panel that acts as a heat sink, and with a Meanwell LED driver. They differ from older LED grow light designs that are actively cooled with a fan, being both noisier and bulkier in size.

I found a bunch of the quantum boards on AliExpress/Alibaba, but unfortunately it's always hard to know if you're really getting the original Samsung LEDs, and if you can even return them if something goes wrong.

A couple of Youtubers that specialise in hydroponics mentioned the brand Spider Farmer, whose panels are the typical quantum-style, but have mounted the driver onto the heat sink. They provide an efficacy rating of 2.7 umol/J, and have an after-sales service. At a 10% markup (£126 versus £115 for similar AliExpress boards) that doesn't sound like too bad a deal. They also have a UK warehouse, which means you don't get surprise customs fees on delivery.

£126 sounds like a lot of money for just growing plants, but it's still cheaper than the lights you'll find in a hydroponics shop. After working my way through a mind-numbing amount of conflicting information about grow lights, this does indeed appear to be the best solution at the moment.

I’m publishing this as part of 100 Days To Offload. You can join in yourself by visiting

#100DaysToOffload #day60 #hydroponics

For the hydroponics stuff I'm building, I needed to spray some plastic totes. This is so that they don't let any light in that can promote algae growth. Chalkboard spray paint was recommended so that's what I got.

It was only when I got home that I read on the can that plastic needs a primer to go on first. It looked good after spraying it without primer, but you have to handle it very carefully afterwards, otherwise the paint will just crack off.

I also learned that you need to spray from a distance of around 30 cm, otherwise the paint job will look blotchy and very uneven. So there you have it – two tips if you ever need to spray paint plastic containers.

I’m publishing this as part of 100 Days To Offload. You can join in yourself by visiting

#100DaysToOffload #day59 #hydroponics

This morning I had a look at the seeds I currently have and started some seeds in rock wool as follows:

  1. Placed rock wool cubes (Grodan 36mm SBS) into propagator (Garland Super 7) trays
  2. Prepared the cubes with half-strength nutrient solution (5mL Formulex in 1L water)
  3. Seeded one or two seeds per cube
  4. Labelled each tray
  5. Placed trays on heated propagator mat

I planted some “Habanada” sweet peppers (1 seed per cube), “Wild Garden Mix” kale (2 seeds per cube, as well as some lamb lettuce or corn salad (2 seeds per cube).

I also ordered some “Red Robin” tomatoes, which should grow only around 30cm tall, as well as some “Bright Lights” chard, which should be fun to grow.

I’m publishing this as part of 100 Days To Offload. You can join in yourself by visiting

#100DaysToOffload #day58 #hydroponics

Enter your email to subscribe to updates.