Learning Go with the InfluxDB Go Library

Navigate to:

I’m not a Golang developer. Let’s just get that out of the way up front. I’ve developed a few things in Go, but a Go developer I’m not. I sort of need to be, but it hasn’t been essential. I decided that it was really time to take the plunge and get serious about Go. Seriously, there’s only so much you can learn by reading the internet.

To that end, I have taken 4 actions:

  1. I'm going to Gophercon in Denver next week.
  2. I ported a library from C to Go.
  3. I wrote a new library in Go.
  4. I started using these libraries, along with the InfluxDB Go Library, to store the data in InfluxDB.

It’s those last 3 that I’ll write about here today and not because I think I did an exceptionally good job of it but because it may be useful to others. I hope you find it useful.

Background

I’ve been working on a little IoT project (duh) that is using a Raspberry Pi. It also uses a Bosch BME280 breakout board from Adafruit and a SenseAir k30 CO2 sensor. Those would be super easy to deal with if I was running them on an Arduino—and in fact I have been. But I’m not right now. And yes, I’m aware that there are ways to just run Arduino sketches on Raspberry Pi but I really am not such a fan of Arduino sketches, so I decided to do it another way.

Porting a Library

The BME 280 sensor is I2C, of course, so there was that. It’s easy to make the I2C bus accessible on Raspberry Pi (just run raspi-config and Bob’s your Uncle), but dealing with I2C devices is a little harder. I tried a couple of C-based I2C libraries but most of them gave…unexpected results. The one I found that was the closest was by a GitHub user called “BitBank2”  (https://github.com/bitbank2/bme280) so I decided to use his. I was able to compile it and run the example program with at least reasonable results. But then I still had to call it from some user-space program in order to get the results. I should have smelled a rathole coming, but of course I didn’t.

I’ll just port it to Go! Sounded reasonable at the time.

It was actually a lot easier than I thought it would be. First, there’s a great Go I2C library from @rakyll that works great. I had used it to access a SenseAir K30 CO2 sensor (the library I’ll talk about next), so I thought I’d start there.

Since the library I was starting from worked, I figured the easiest thing to do would be to just do a semi-straight translation. I’d copy in a few lines of the C code, and then make it Go. There were, of course, some things that just wouldn’t work well. For instance, the I2C library wants to deal in bytes and byte slices, so I couldn’t very well just use the ints that the C library used. Also, the C library used a slew of static global variables, and that was also not going to work well in go. So I made adjustments:

static int calT1,calT2,calT3;
static int calP1, calP2, calP3, calP4, calP5, calP6, calP7, calP8, calP9;
static int calH1, calH2, calH3, calH4, calH5, calH6;

became:

type BME280 struct {
 Dev *i2c.Device
 tConfig []int
 pConfig []int
 hConfig []int
}

and

device.tConfig = make([]int, 3)
device.pConfig = make([]int, 9)
device.hConfig = make([]int, 6)

Pretty much the rest of it was a simple translation of turning a C language construct into a Golang construct.

// Prepare temperature calibration data
calT1 = ucCal[0] + (ucCal[1] << 8);
calT2 = ucCal[2] + (ucCal[3] << 8);
if (calT2 > 32767) calT2 -= 65536; // negative value
calT3 = ucCal[4] + (ucCal[5] << 8);
if (calT3 > 32767) calT3 -= 65536;

Turned into:

// time to set up the calibration
 device.tConfig[0] = int(ucCal[0]) + (int(ucCal[1]) << 8)
 device.tConfig[1] = int(ucCal[2]) + (int(ucCal[3]) << 8)
 if device.tConfig[1] > 32767 {
     device.tConfig[1] -= 65536
 }
 device.tConfig[2] = int(ucCal[4]) + (int(ucCal[5]) << 8)
 if device.tConfig[2] > 32767 {
     device.tConfig[2] -= 65536
 }

And so on.

Now any Go program can simply do the following:

package main
import (
  "fmt"
  "github.com/davidgs/bme280_go"
  "time"
)
func main() {
  dev := "/dev/i2c-1"
  bme := bme280_go.BME280{}
  r := bme.BME280Init(dev)
  if r < 0 {
    fmt.Println("Error")
  }
  rets := bme.BME280ReadValues()
  f := 0.00
  f = float64(rets[0]) / 100.00
  fmt.Println("Temp: ", f)
  f = float64(rets[2]) / 1024.00
  fmt.Println("Humidity: ", f)
  bme.Dev.Close()
}

because a call to BME280ReadValues returns a simple slice of ints as Temperature, Pressure, and Humidity, in that order. Note: The pressure calculation is currently broken, so I don’t suggest using it.

As I said, it was surprisingly easy to get it all working! I now have a mostly-fully functioning library for the Adafruit BME280 Breakout Board in GoLang! If you’re interested in using this library, it’s freely available on my GitHub.

Writing My Own Library

Next up is to write a similar library for the SenseAir K30 sensor. I’d already been successfully working with this sensor via Go for a while, so I already had the Go code, that worked. All I had to do was turn it into a library that others can use.

It was even easier than the porting exercise. I just had to add a few things, like a K30 object:

type K30 struct {
	Dev     *i2c.Device
	co2Read []byte
}

And the Address of the device:

const (
	CO2_ADDR = 0x68
)

And then make some changes to the Init() and ReadValue() functions. Since the sensor only returns a single vase—the concentration of CO2 in Parts Per Million the value return was easier as well. Like the BME280 library, this one is also freely available on my GitHub.

Using the Values with InfluxDB

Great! So now I have these 2 device drivers in Go that I can use on my Raspberry Pi, which is also running InfluxDB, of course. All I have to do is get the values into InfluxDB. Turns out that’s simple as well. All you need is the InfluxDB Go Client library!

To start with, you’ll need to import the 3 libraries required:

import (
    "fmt"
    "github.com/davidgs/SenseAir_K30_go"
    "github.com/davidgs/bme280_go"
    "github.com/influxdata/influxdb/client/v2"
    "time"
)

Then you simply initialize everything:

   c, err := client.NewHTTPClient(client.HTTPConfig{
    Addr: "http://localhost:8086",
    })
    if err != nil {
        fmt.Println("Error creating InfluxDB Client: ", err.Error())
    }
    defer c.Close()
    dev := "/dev/i2c-1"
    bme := bme280_go.BME280{}
    r := bme.BME280Init(dev)
    if r < 0 {
        fmt.Println("Error")
    }
    defer bme.Dev.Close()
    k30 := SenseAir_K30_go.K30{}
    r = k30.K30Init(dev)
    if r < 0 {
        fmt.Println("Error")
    }
    defer k30.Dev.Close()
    temp_tags := map[string]string{"sensor": "bme_280"}
    c_tags := map[string]string{"sensor": "k30_co2"}

You’ll notice I created a couple of maps there which contain the tags that I will be using for each of the measurements. You’ll see how those are used shortly.

The CO2 sensor can only really be read (at most) every 2 seconds, and in reality, something like 3.5 - 4 seconds is a bit more reasonable, but I want the temperature and humidity reading more often. What I did was to read the temperature and humidity every second, and do that 4 times, and then read the CO2.

for 1 > 0 {
    count := 0
// Create a new point batch
    bp, _ := client.NewBatchPoints(client.BatchPointsConfig{
        Database:  "telegraf",
        Precision: "s",
    })
    for count < 4 {
        rets := bme.BME280ReadValues()
        t := float64(float64(rets[0]) / 100.00)
        fmt.Println("Temp: ", t)
        h := float64(rets[2]) / 1024.00
        fmt.Println("Humidity: ", h)
        if rets[0] != -1 && t > -100 {
            temp_fields := map[string]interface{}{
                "temp_c":   t,
                "humidity": h,
            }
	// Create a point and add to batch
            pt, err := client.NewPoint("bme_280", temp_tags, temp_fields, time.Now())
            if err != nil {
                fmt.Println("Error: ", err.Error())
            }
            bp.AddPoint(pt)
        }
        time.Sleep(1000 * time.Millisecond)
        count += 1
    }

There are a few things going on in there that need some explaining. The first is the Batch Point. This is a point that will be written to the InfluxDB instance, eventually. In creating the point I need to define the database to which it will go, and the precision. In this case, I’m using second-level precision, since I’m only reading every second. Next, I add the fields and their values— namely temperature and humidity—and then a create a new point using the tags and the fields. Finally, I add this point to the Batch Point.

Now, I could just insert each point as it is created, but this would be wildly inefficient both in terms of database writes and in terms of resource utilization on the client side, so I will just continue to add points to the Batch Point for a bit—in fact, I’ll add all 4 temperature and humidity readings before moving on.

Next, I’ll take a CO2 reading, and add a that reading to the Batch Point before writing it all to the database:

    co2_value := k30.K30ReadValue()
    if co2_value > 0 {
    fmt.Println("CO2: ", co2_value)
    // Create a point and add to batch
    c_fields := map[string]interface{}{
        "co2": co2_value,
    }
    pt, err := client.NewPoint("k30_reader", c_tags, c_fields, time.Now())
    if err != nil {
        fmt.Println("Error: ", err.Error())
    }
    bp.AddPoint(pt)
}
// Write the batch
c.Write(bp)

And that completes the loop. Each time through the loop I’ll create a new Batch Point, take 4 temperature/humidity readings, and add them to the Batch Point. I’ll then take one CO2 reading and add it to the Batch Point before writing all 5 points to the database.

What I’ll see then is 5 points written to the database every 4(ish) seconds. I could, to increase efficiency some, keep a separate counter and add a few dozen points to the batch before writing it, but for a local database, that isn’t doing much else—this works just fine.

Conclusion

This was a really interesting project for me to expand my knowledge of Go and to use the existing InfluxDB Go library to pull together a small project using some sensors.

Of course this is all part of a larger project that I’ll be unveiling in the near future so stay tuned for that!