Working with I²C peripherals via Kubernetes on Raspberry Pi

Inter-integrated circuit (I²C) protocol is one of the supported communication mechanisms to connect peripheral devices on Raspberry Pi. In this post we will look at working with BME280 sensor via Kubernetes pod. This will allow us to schedule a k8s workload to capture temperature, humidity and pressure values. The image above shows the setup consisting of two Raspberry Pi’s, which form a Kubernetes cluster, and the sensor is attached to one of them (seen on the far right).
Once everything is setup and running properly the logs show the sensor data streaming in:
└─ $ ▶ kubectl --namespace=bme280 logs -f bme280
{"level":"info","time":"2021-08-13T15:12:54.785Z","name":"bme280","msg":"detected bme280 with device id: 0x60"}
{"level":"info","time":"2021-08-13T15:12:59.796Z","name":"bme280.data","msg":"data","temperature":27.29,"pressure":1007.56,"humidity":43.02,"altitude":47}
{"level":"info","time":"2021-08-13T15:13:04.801Z","name":"bme280.data","msg":"data","temperature":27.27,"pressure":1007.54,"humidity":43.07,"altitude":47}
As you can see, the logs from the pod are streaming the sensor data we are interested in. I’ll skip the details on setting up Kubernetes on Raspberry Pi in this post but will briefly mention the configuration as follows:
- Hardware consisted of a Raspberry Pi 4 and a Raspberry Pi 3 both running Debian 64-bit OS
- BME280 sensor was attached to one of the nodes via I²C
- k0s was used to deploy Kubernetes cluster on the nodes
- I²C bus was enabled on the nodes via
raspi-config
└─ $ ▶ kubectl get nodes -o wide
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
rpi4-* Ready <none> 25h v1.21.3+k0s ***.***.*.** <none> Debian GNU/Linux 10 (buster) 5.10.52-v8+ containerd://1.4.8
rpi3-* Ready <none> 25h v1.21.3+k0s ***.***.*.** <none> Debian GNU/Linux 10 (buster) 5.10.52-v8+ containerd://1.4.8
Furthermore, I put together a Go based binary to interface with I²C bus and capture the sensor data, which can be used directly on the node to confirm that the data capture is working well:
pi@raspberry:~ $ ./bme280 run --interval-seconds=2 --total-count=100 | jq '.'
{
"level": "info",
"time": "2021-05-21T17:53:10.210-0700",
"name": "bme280",
"msg": "detected bme280 with device id: 0x60"
}
{
"level": "info",
"time": "2021-05-21T17:53:12.228-0700",
"name": "bme280.data",
"msg": "data",
"temperature": 23.87,
"pressure": 1003.94,
"humidity": 32.5,
"altitude": 77
}
{
"level": "info",
"time": "2021-05-21T17:53:14.251-0700",
"name": "bme280.data",
"msg": "data",
"temperature": 23.87,
"pressure": 1003.93,
"humidity": 32.39,
"altitude": 77
}
...<redacted>
At this point we can now build a container image with this binary so it can be deployed as a Kubernetes pod. The Dockerfile
below shows steps to build arm64
binary and encapsulate it within a so-called distroless base container image.
# Build the manager binary
FROM docker.io/library/golang:1.16.7 as builderWORKDIR /workspace
# Copy the Go Modules manifests
COPY go.mod go.mod
COPY go.sum go.sum# Copy the go source
COPY main.go main.go
COPY cmd/ cmd/
COPY flags/ flags/
COPY run/ run/
COPY vendor/ vendor/# Build binaries for both arm64 arch
RUN CGO_ENABLED=0 GOOS=linux GOARCH=arm64 GO111MODULE=on go build -mod=vendor -a -o manager.arm64 main.go# Use distroless as minimal base image to package the manager binary
# Refer to https://github.com/GoogleContainerTools/distroless for more details
FROM gcr.io/distroless/static:nonroot
WORKDIR /
COPY --from=builder /workspace/manager.arm64 /manager
USER 65532:65532
I won’t go over the details on how to put together the binary to capture sensor data in this post. You can read more about it in the link below:
Pod Spec
Assuming that binary works well and the container is ready, we can now begin to put together a pod spec:
apiVersion: v1
kind: Pod
metadata:
name: bme280
spec:
securityContext:
runAsNonRoot: true
runAsGroup: 998
runAsUser: 65532
fsGroup: 998
nodeSelector:
kubernetes.io/hostname: "node-where-sensor-is-attached"
imagePullSecrets:
- name: registry-key
volumes:
- name: dev
hostPath:
path: /dev
containers:
- name: bme280
image: your-registry/your-account/bme280:1.2.3
imagePullPolicy: IfNotPresent
command:
- "/manager"
args:
- "run"
- "--total-count"
- "0"
- "--total-duration-minutes"
- "0"
securityContext:
privileged: true
volumeMounts:
- mountPath: /dev
name: dev
The important point to note in the pod spec is the securityContext
which indicates user id and group id that should be used to access I²C bus. Group id can be fetched by listing groups id’s belonging to the pi
user on the node
pi@rpi:~ $ whoami
pipi@rpi:~ $ id
uid=1000(pi) gid=1000(pi) groups=1000(pi),4(adm),20(dialout),24(cdrom),27(sudo),29(audio),44(video),46(plugdev),60(games),100(users),105(input),109(netdev),997(gpio),998(i2c),999(spi)
As you can see the id of 998
is assigned to the group i2c
, which is what we can use in the pod spec.
Another critical aspect of making this work via a containerized binary is to provide access to /dev
mount paths. The I²C devices show up as follows and we also need to confirm that the ownership on these files match the group level permission we have assigned to the pod.
pi@rpi3-0:~ $ ls -la /dev/i2c-*
crw-rw---- 1 root i2c 89, 1 Aug 13 04:24 /dev/i2c-1
crw-rw---- 1 root i2c 89, 2 Aug 13 04:24 /dev/i2c-2
Finally, we can deploy the pod spec and it should communicate with the underlying hardware!
└─ $ ▶ kubectl --namespace=bme280 get pods
NAME READY STATUS RESTARTS AGE
bme280 1/1 Running 0 176m└─ $ ▶ kubectl --namespace=bme280 logs -f bme280
{"level":"info","time":"2021-08-13T15:12:54.785Z","name":"bme280","msg":"detected bme280 with device id: 0x60"}
{"level":"info","time":"2021-08-13T15:12:59.796Z","name":"bme280.data","msg":"data","temperature":27.29,"pressure":1007.56,"humidity":43.02,"altitude":47}
{"level":"info","time":"2021-08-13T15:13:04.801Z","name":"bme280.data","msg":"data","temperature":27.27,"pressure":1007.54,"humidity":43.07,"altitude":47}