Gerrit Niezen

Maker of open-source software and hardware.

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.clear();
  g.setFontVector(30);
  g.setFontAlign(0,-1);
  g.drawString(lastTemp.toFixed(1), 64,5);
  g.setFontBitmap();
  g.drawString("Temperature", 64, 0);
  require("graph").drawLine(g, history, {
    axes : true,
    gridy : 5,
    x:0, y:37,
    width:128,
    height:27
  });
  g.flip();
}

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

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

  draw();
}

// 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);
    avrSum=0;
    avrNum=0;

    NRF.setServices({
      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') {
        device.gatt.disconnect()
        button.innerHTML = 'Connect'
        return
      }
      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 = event.target.value
    const temp = value.getUint16(0, true) / 100

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

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

The index.html file looks like this:

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

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 https://100daystooffload.com.

#100DaysToOffload #day62 #hydroponics

Comment on this post

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.

Seedlings

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 https://100daystooffload.com.

#100DaysToOffload #day61 #hydroponics

Comment on this post

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 https://100daystooffload.com.

#100DaysToOffload #day60 #hydroponics

Comment on this post

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 https://100daystooffload.com.

#100DaysToOffload #day59 #hydroponics

Comment on this post

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 https://100daystooffload.com.

#100DaysToOffload #day58 #hydroponics

Comment on this post

I'm putting together a spreadsheet of the various requirements to grow different vegetables in hydroponics, e.g.:

  • days to first harvest
  • pH range
  • EC (Electrical Conductivity) range
  • Light range
  • Temperature

I'm thinking that having something that people could collaboratively edit could be useful. For example, also stating the variety, nutrients, method or grow light that was used. That could lead to creating common “recipes” for specific types of crops. You could even have recipes that change during the growing the period, for example increasing night time temperatures and decreasing daytime temperatures for fruit production in pepper plants after they reach their mature height.


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

#100DaysToOffload #day57 #hydroponics

Comment on this post

I somehow got stuck on day 55 of #100DaysToOffload in July and suddenly it's September already! Oh well, I guess it's now day 56.

For the month of September I'm taking a sabbatical from work, to work on something different than what I'm usually working on. I decided to work on DIY hydroponics, which I've mentioned a couple of times on this blog in the past. I bought the book DIY Hydroponic Gardens by Tyler Baras, and started to build one of the designs in the book. The one I'm starting with is called a media bed, using a simple “flood and drain” (also called “ebb and flow” or “ebb and flood”) system. It consists of two containers on top of each other, with water pumped up from the bottom container and draining back into it using a pump and a timer.

I've already spray painted one of the containers, and most of the equipment I need have been ordered. I'll post some photos soon.


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

#100DaysToOffload #day56 #hydroponics

Comment on this post

A couple of weeks ago I mentioned that I was making a simple hydroponics planter on my 3D printer. Well, I just realised that I haven't even posted a picture of the planter, so here we go:

Hydroponics planter with first basil leaves showing

You may notice that the first leaves of the basil seed that I planted just appeared! 🎉️ I started off the seed in a Root Riot cube, surrounded by some clay pellets. I haven't yet put any nutrient solution in the reservoir, but will as soon as the first roots start appearing.


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

#100DaysToOffload #day55 #hydroponics

Comment on this post

This is the final instalment of my second series of blog posts on MTP. I now have my code working in both the browser and Node.js, where it connects to an Android device, reads the latest file and saves it locally.

First off, I had to write some code to figure out whether we're in the browser, in Node.js or in Electron:

let isBrowser = null;

if (typeof navigator !== 'undefined') {
  const userAgent = navigator.userAgent.toLowerCase();
  isBrowser = userAgent.indexOf(' electron/') === -1 && typeof window !== 'undefined';
} else {
  // Node.js process
  isBrowser = false;
}

I'm sure there are modules out there that do this better, but it works for now. Now we have to load some modules for Node.js when we're not in the browser:

let usb, fs = null;

if (!isBrowser) {
  // For Node.js and Electron
  usb = require('webusb').usb;
  EventTarget = require('events');
  fs = require('fs');
} else {
  usb = navigator.usb; // Yay, we're using WebUSB!
}

We also need to be able to save the file to disk in both the browser and Node.js:

const array = new Uint8Array(data.payload);

if (isBrowser) {
  const file = new Blob([array]);
  const a = document.createElement('a'),
  url = URL.createObjectURL(file);
  a.href = url;
  a.download = this.filename;
  document.body.appendChild(a);
  a.click();
  setTimeout(function() {
    document.body.removeChild(a);
    window.URL.revokeObjectURL(url);
  }, 0);
} else {
  fs.writeFileSync(this.filename, array);
}

We also need to deal with event listeners differently in the browser and Node.js:

const initMTP = () => {
  const mtp = new Mtp(0x0e8d, 0x201d);

  if (isBrowser) {
    mtp.addEventListener('error', err => console.log('Error', err));

    mtp.addEventListener('ready', async () => {
      mtp.addEventListener('data', (e) => mtp.dataHandler(e.detail));
      await mtp.openSession();
    } );
  } else {
    mtp.on('error', err => console.log('Error', err));
    mtp.on('ready', async () => {
      mtp.on('data', (data) => mtp.dataHandler(data));
      await mtp.openSession();
    });
  }
};

And finally, WebUSB requires a user interaction before it will show the permission prompt, so we need:

if (isBrowser) {
  document.addEventListener('DOMContentLoaded', event => {
    let button = document.getElementById('connect');

    button.addEventListener('click', async() => {
      initMTP();
    });
 });
} else {
  initMTP();
}

So, that's it! Over on GitHub I have the full implementation. If you run it in Node.js, it will save the newest file on your Android device to disk. If you run it in the browser, it will do the same after you click the Connect button:

<html>
  <head>
    <title>MTP over WebUSB</title>
    <script src="mtp.js"></script>
  </head>
  <body>
    <button id="connect">Connect</button>
  </body>
</html>

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

#100DaysToOffload #day54 #mtp

Comment on this post

In the previous post on MTP we looked at how to retrieve the name of a file on an Android device using MTP. Let's see how to decode that filename:

const array = new Uint8Array(data.payload);
const decoder = new TextDecoder('utf-16le');
filename = decoder.decode(array.subarray(1, array.byteLength - 2));
console.log('Filename:', filename);

In short, we need to remove the length byte at the beginning and the zero terminator bytes at the end, and then decode it as UTF-16LE. To retrieve the file itself, we use the following (where CODE.GET_OBJECT.value is 0x1009):

const getFile = {
  type: 1,
  code: CODE.GET_OBJECT.value,
  payload: [objectHandle],
};
await device.transferOut(0x01, buildContainerPacket(getFile));

All that's left to do is to save the data that is returned to a file! (OK, I also need to write some code that handle files that are larger than the buffer we use for transferIn.)

All the code is currently on a branch on GitHub: https://github.com/tidepool-org/node-mtp/blob/pure-js/mtp.js

Once I have it all working, I hope to replace the Node.js-specific bits so that it can run in the browser using WebUSB, and ideally have a proof-of-concept available on the web.


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

#100DaysToOffload #day53 #mtp

Comment on this post

Enter your email to subscribe to updates.