Learning Go With the InfluxDB Go Library
Want to learn more about translating C to Go with the InfluxDB library? Check out this developer's experience using the InfluxDB library and a Raspberry Pi.
Join the DZone community and get the full member experience.
Join For FreeI'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 on the Internet.
To that end, I have taken four actions:
- I'm going to Gophercon in Denver next week.
- I ported a library from C to Go.
- I wrote a new library in Go.
- I started using these libraries, along with the InfluxDB Go Library, to store the data in InfluxDB.
It's those last three 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. I'm aware that there are ways to just run Arduino sketches on a Raspberry Pi, but I really am not 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 is 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 I 2C libraries, but most of them gave me... unexpected results. The one I found that was the closest was by a GitHub user called "BitBank2." So, I decided to use this. I was able to compile it and run the example program with 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 I2 C 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. So, I made some 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;
This 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)
The rest of it was a simple translation, turning a C language construct into a Go language 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;
This 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 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 the temperature, pressure, and humidity, in that order, 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! Now, I have an almost-fully functioning library for the Adafruit BME280 Breakout Board in the Go language! If you're interested in using this library, it's available on my GitHub.
Writing My Own Library
Next up, it's time 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, I needed to add the address of the device:
const (
CO2_ADDR = 0x68
)
And, then, I had to make some changes to the Init()
and ReadValue()
functions. Since the sensor only returns a single vase-the concentration of CO 2 in Parts Per Million, the value return was easier as well. Like the BME280 library, this one is also available on my GitHub.
Using the Values With InfluxDB
Great! Now, I have these two 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 three 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 that I created a couple of maps that contain tags for each of the measurements. You'll see how those are used shortly.
The CO 2 sensor can only really be read (at most) every two seconds, and in reality, something like 3.5- 4 seconds is a bit more reasonable, but I want the temperature and humidity to be read more often. What I did was have it read the temperature and humidity every second and do that four times, and then, I had it read the CO 2.
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, I 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 four temperature and humidity readings before moving on.
Next, I'll take a CO 2 reading and add 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) co2_value := k30.K30ReadValue() if co2_value > 0 { fmt.Println("CO2: ", co2_va
And, that completes the loop. Each time through the loop I'll create a new Batch Point, take four temperature/humidity readings, and add them to the Batch Point. Then, I'll take one CO 2 reading and add it to the Batch Point before writing all five points to the database.
What I'll see then is five points written to the database every four(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 many 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 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!
Published at DZone with permission of David G. Simmons, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments