Dieser Post ist Teil der Serie über mein privates BeagleBone-Black-Projekt. Die Services-Schicht des Projekts ist in Go geschrieben und stellt Hardware-Peripherie über eine REST API bereit. Damit lassen sich GPIO, I2C und UART von außen steuern — ohne direkt mit dem Board verbunden zu sein.
API-Design
Die API folgt REST-Prinzipien: Ressourcen statt Aktionen, HTTP-Verben mit Bedeutung, JSON als Datenformat.
Basis-URL: http://beaglebone:8080/api/v1
Endpunkt-Übersicht:
GET /gpio/{pin} → Pin-Zustand lesen
PUT /gpio/{pin} → Pin-Zustand setzen
GET /i2c/{bus}/{addr}/{reg} → I2C-Register lesen
POST /i2c/{bus}/{addr} → I2C-Bytes schreiben
GET /uart/{port} → UART-Daten lesen (polling)
POST /uart/{port} → UART-Daten sendenKeine HATEOAS, kein GraphQL — das ist ein privates Projekt, nicht eine Enterprise-API. Einfach und funktional.
Anbindung an die HAL-Schicht
Go kann keine Rust-Bibliotheken direkt laden — aber C-kompatible Shared Libraries. Rust kompiliert als C-kompatible Library, Go bindet sie via cgo ein.
package hal
// #cgo LDFLAGS: -L${SRCDIR}/../../rust/target/armv7-unknown-linux-gnueabihf/release -lbbb_hal
// #cgo LDFLAGS: -L${SRCDIR}/../../c/build-arm -lbbb_drivers
// #include "bbb_hal.h"
import "C"
import "unsafe"Der cgo-Kommentarblock wird vom Go-Compiler direkt ausgewertet.
LDFLAGS zeigt auf die cross-kompilierten ARM-Libraries.
GPIO über HTTP
Handler
type GPIOHandler struct {
hal *hal.HAL
}
func (h *GPIOHandler) Get(w http.ResponseWriter, r *http.Request) {
pin, err := strconv.Atoi(chi.URLParam(r, "pin"))
if err != nil {
http.Error(w, "invalid pin", http.StatusBadRequest)
return
}
value, err := h.hal.GPIORead(pin)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(map[string]any{
"pin": pin,
"value": value,
})
}
func (h *GPIOHandler) Put(w http.ResponseWriter, r *http.Request) {
pin, _ := strconv.Atoi(chi.URLParam(r, "pin"))
var body struct {
Value int `json:"value"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
http.Error(w, "invalid body", http.StatusBadRequest)
return
}
if body.Value != 0 && body.Value != 1 {
http.Error(w, "value must be 0 or 1", http.StatusBadRequest)
return
}
if err := h.hal.GPIOWrite(pin, body.Value); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
}Beispiel-Requests
# Pin 48 lesen (LED auf BeagleBone Black)
curl http://beaglebone:8080/api/v1/gpio/48
# {"pin":48,"value":0}
# Pin 48 auf HIGH setzen
curl -X PUT http://beaglebone:8080/api/v1/gpio/48 \
-H "Content-Type: application/json" \
-d '{"value": 1}'
# 204 No ContentI2C über HTTP
I2C ist etwas komplexer: Bus-Nummer, Device-Adresse und Register-Adresse sind alle Teil des Pfads.
Handler
func (h *I2CHandler) Get(w http.ResponseWriter, r *http.Request) {
bus, _ := strconv.Atoi(chi.URLParam(r, "bus"))
addr, _ := strconv.ParseUint(chi.URLParam(r, "addr"), 0, 8) // hex: 0x48
reg, _ := strconv.ParseUint(chi.URLParam(r, "reg"), 0, 8)
value, err := h.hal.I2CRead(bus, uint8(addr), uint8(reg))
if err != nil {
// I2C-Fehler → 502 Bad Gateway (Hardware-Downstream-Fehler)
http.Error(w, err.Error(), http.StatusBadGateway)
return
}
json.NewEncoder(w).Encode(map[string]any{
"bus": bus,
"addr": fmt.Sprintf("0x%02x", addr),
"reg": fmt.Sprintf("0x%02x", reg),
"value": value,
})
}Fehlerbehandlung bei Bus-Fehlern
Hardware-Fehler sind keine Client-Fehler (4xx) sondern Upstream-Fehler (5xx).
Ein nicht reagierendes I2C-Device ist 502 Bad Gateway,
eine falsche Adresse im Request ist 400 Bad Request.
var (
ErrI2CDeviceNotFound = errors.New("i2c device not found")
ErrI2CBusError = errors.New("i2c bus error")
)
func httpStatusForHALError(err error) int {
switch {
case errors.Is(err, ErrI2CDeviceNotFound):
return http.StatusNotFound // 404: Device nicht vorhanden
case errors.Is(err, ErrI2CBusError):
return http.StatusBadGateway // 502: Bus-Fehler
default:
return http.StatusInternalServerError
}
}Beispiel-Request
# Temperatur-Sensor LM75 auf Bus 1, Adresse 0x48, Register 0x00
curl http://beaglebone:8080/api/v1/i2c/1/0x48/0x00
# {"bus":1,"addr":"0x48","reg":"0x00","value":42}UART über HTTP
UART über HTTP ist die ungewöhnlichste Kombination. Zwei Varianten: Polling (Request/Response) und Streaming. Für dieses Projekt reicht Polling.
Handler
func (h *UARTHandler) Get(w http.ResponseWriter, r *http.Request) {
port := chi.URLParam(r, "port") // "ttyO1", "ttyO2", etc.
// Timeout aus Query-Parameter, default 100ms
timeout := 100 * time.Millisecond
if t := r.URL.Query().Get("timeout"); t != "" {
if d, err := time.ParseDuration(t); err == nil {
timeout = d
}
}
data, err := h.hal.UARTRead(port, timeout)
if err != nil {
http.Error(w, err.Error(), http.StatusBadGateway)
return
}
json.NewEncoder(w).Encode(map[string]any{
"port": port,
"data": base64.StdEncoding.EncodeToString(data),
"len": len(data),
})
}
func (h *UARTHandler) Post(w http.ResponseWriter, r *http.Request) {
port := chi.URLParam(r, "port")
var body struct {
Data string `json:"data"` // base64-kodiert
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
http.Error(w, "invalid body", http.StatusBadRequest)
return
}
raw, err := base64.StdEncoding.DecodeString(body.Data)
if err != nil {
http.Error(w, "data must be base64", http.StatusBadRequest)
return
}
if err := h.hal.UARTWrite(port, raw); err != nil {
http.Error(w, err.Error(), http.StatusBadGateway)
return
}
w.WriteHeader(http.StatusNoContent)
}UART-Daten sind Binärdaten — base64 ist die sicherste Kodierung für JSON. Wer nur ASCII sendet, kann auch direkt einen String nutzen, muss aber mit Encoding-Problemen rechnen. |
Beispiel: vollständiger Request-Flow
Wie sieht der Weg eines HTTP-Requests bis zum GPIO-Register aus?
HTTP Client
GET /api/v1/gpio/48
│
▼
Go Router (chi)
GPIOHandler.Get(w, r)
│
▼
hal.GPIORead(48) ← Go-Funktion
│
▼
hal_gpio_read(48, &value) ← cgo-Aufruf → Rust pub extern "C"
│
▼
gpio_read_sysfs(48, &value) ← Rust ruft C-Treiber auf (unsafe)
│
▼
open("/sys/class/gpio/gpio48/value") ← C, Linux sysfs
│
▼
AM335x GPIO-RegisterJede Schicht hat eine klar definierte Aufgabe. Go kennt keine Dateinamen, C kennt keine HTTP-Handler.
Deployment als systemd-Service
# /etc/systemd/system/bbb-hal.service
[Unit]
Description=BeagleBone Black HAL REST API
After=network.target
[Service]
Type=simple
User=pi
WorkingDirectory=/opt/bbb-hal
ExecStart=/opt/bbb-hal/bbb-api --port 8080
Restart=always
RestartSec=5
# Hardware-Zugriff erlauben
SupplementaryGroups=gpio i2c dialout
[Install]
WantedBy=multi-user.targetsudo systemctl enable --now bbb-hal
sudo systemctl status bbb-halNächster Post in der Serie: Rust in der HAL — warum und wie