PIoneer

package module
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 5, 2026 License: MIT Imports: 3 Imported by: 0

README

PIoneer

PIoneer

GPIO control for Raspberry Pi over SSH — from any machine on your network

Installation · Quick Start · Protocols · MQTT · Configuration · Testing


Unlike other GPIO libraries that run directly on the Pi, PIoneer runs on your machine and controls the Pi remotely over SSH. Write your Go code on your Mac or server, keep your Pi deployment-free.

Your Machine  ──SSH──▶  Raspberry Pi GPIO
     │                        │
     └──MQTT──▶  Broker ◀─────┘

Still under active development.


Installation

go get github.com/EraldCaka/PIoneer

Requires sshpass on the machine running the library:

brew install sshpass   # macOS
apt install sshpass    # Linux

Quick Start

package main

import (
    "log"
    "os"

    "github.com/EraldCaka/PIoneer/pkg/handlers/pioneer"
)

func main() {
    f, err := os.Open("config.yaml")
    if err != nil {
        log.Fatal(err)
    }
    defer f.Close()

    device, err := pioneer.New(f)
    if err != nil {
        log.Fatal(err)
    }

    if err := device.Start(); err != nil {
        log.Fatal(err)
    }
    defer device.Stop()

    // read a pin
    val, _ := device.Read(3)
    log.Printf("pin 3: %d", val)

    // write a pin
    device.Write(3, 1)

    // watch for changes — event driven, no polling in your code
    events, _ := device.Watch(5)
    defer device.StopWatch(5)

    for event := range events {
        log.Printf("pin %d: %d → %d", event.Pin, event.OldValue, event.NewValue)
    }
}

Configuration

config:
  device-name: "pi"
  url: "192.168.1.x"
  port: "22"
  auth-method: "password"   # or "key"
  password: "yourpassword"
  # ssh-key-path: "/home/user/.ssh/id_rsa"
  pool-size: 3        # concurrent SSH connections
  max-retries: 5
  retry-delay: 3      # seconds between retries

chip:
  name: "gpiochip512"
  digital-pins:
    - id: "button"
      pin: 5
      value: 0
      direction: 0   # 0=input, 1=output
      edge: 1
    - id: "led"
      pin: 3
      value: 1
      direction: 1
      edge: 0
  pwm-pins:
    - id: "fan"
      pin: 18
      frequency: 1000
      duty-cycle: 0
  i2c-devices:
    - id: "temp-sensor"
      bus: 1
      address: "0x48"

# optional — enables full MQTT pub/sub
mqtt:
  broker: "tcp://192.168.1.x:1883"
  client-id: "pioneer-pi"
  topic: "pioneer"
  use-tls: false
  qos: 1

Pins listed under chip are auto-initialized on Start(). You can also call Read() and Write() on any pin without declaring it — pinctrl gives access to all 54 GPIO pins.


Protocols

Digital

val, err := device.Read(pin)      // returns 0 or 1
err     := device.Write(pin, 1)   // 0 or 1

PWM

Hardware PWM pins on Pi 4: 12, 13, 18, 19. Requires pigpiod running on the Pi.

device.SetDutyCycle(18, 75.0)   // 0.0–100.0
device.GetDutyCycle(18)
device.StopPWM(18)

I2C

device.I2CWrite(1, "0x48", []byte{0x01, 0xFF})
data, err := device.I2CRead(1, "0x48", 2)

Events

PIoneer polls pins internally and emits on change — your code stays clean and reactive:

events, _ := device.Watch(5)
defer device.StopWatch(5)

for event := range events {
    fmt.Printf("pin %d: %d → %d\n", event.Pin, event.OldValue, event.NewValue)
}

MQTT

When a broker is configured, PIoneer automatically publishes every state change and subscribes to control topics — no extra code needed.

Published topics

Topic Payload
pioneer/gpio/<pin>/state {"pin":3,"value":1,"label":"HIGH","direction":"output","timestamp":...,"device":"pi"}
pioneer/pwm/<pin>/state {"pin":18,"duty_cycle":50.0,"frequency_hz":1000,"timestamp":...}
pioneer/i2c/<bus>/<addr>/state {"bus":1,"address":"0x48","data":[...],"hex":"01ff","length":2,"timestamp":...}
pioneer/device/status {"device":"pi","status":"online","ssh_pool_size":3,"reconnects":0}
pioneer/device/error {"protocol":"gpio","error":"...","timestamp":...}

Control topics

Topic Payload Action
pioneer/gpio/<pin>/set "1" or "0" Write pin
pioneer/gpio/<pin>/get (empty) Read and publish pin state
pioneer/pwm/<pin>/set "50.0" Set duty cycle
pioneer/pwm/<pin>/stop (empty) Stop PWM
pioneer/i2c/write {"bus":1,"address":"0x48","data":[1,255]} I2C write
pioneer/i2c/read {"bus":1,"address":"0x48","length":2} I2C read and publish
pioneer/device/ping (empty) Trigger full status response

Modes

PIoneer supports two execution modes controlled by a single config field:

config:
  mode: "ssh"    # default — run on your Mac/server, control Pi remotely
  mode: "local"  # run directly on the Pi, direct hardware access

In local mode SSH fields are ignored. Use make deploy to cross-compile and push to the Pi automatically.


Health & Metrics

health := device.Health()
// health.Connected       bool
// health.Reconnects      int
// health.ActiveWatchers  int
// health.MQTTBound       bool

metrics := device.Metrics()
// metrics.TotalReads    int64
// metrics.TotalWrites   int64
// metrics.TotalErrors   int64
// metrics.SSHPoolSize   int
// metrics.Reconnects    int64

Config Reference

config

Field Type Default Description
device-name string — Device identifier
url string — Pi IP address
port string "22" SSH port
mode string "ssh" ssh or local
auth-method string — password or key
password string — SSH password
ssh-key-path string — Path to private key
pool-size int 3 Concurrent SSH connections
max-retries int 5 Retries on failure
retry-delay int 3 Seconds between retries

digital pin

Field Type Description
id string Unique identifier
pin int GPIO pin number (1–53)
value int Default value: 0 or 1
direction int 0=input, 1=output
edge int 0=none, 1=rising, 2=falling, 3=both

pwm pin

Field Type Description
id string Unique identifier
pin int Hardware PWM pin (12, 13, 18, 19)
frequency int Frequency in Hz
duty-cycle float Initial duty cycle (0–100)

i2c device

Field Type Description
id string Unique identifier
bus int I2C bus number (0 or 1)
address string Device address e.g. "0x48"

Pi Setup

sudo apt install -y i2c-tools pigpio
sudo raspi-config nonint do_i2c 0
sudo systemctl enable pigpiod && sudo systemctl start pigpiod
echo "pi ALL=(ALL) NOPASSWD: /usr/bin/pinctrl" | sudo tee /etc/sudoers.d/pinctrl

License

MIT


Testing

PIoneer has two test layers — unit tests that run anywhere, and integration tests that run against a real Pi.

# unit tests — no Pi required
go test ./...

# unit tests with coverage
go test ./... -cover

# integration tests — requires a live Pi
INTEGRATION=1 go test ./pkg/handlers/pioneer/... -v

Unit tests

Unit tests cover validation, parsing, error handling, and the event system without any hardware or network dependency.

func TestParsePinOutput_High(t *testing.T) {
    val, err := parsePinOutput("3: op -- pu | hi // GPIO3 = output")
    if err != nil || val != 1 {
        t.Errorf("expected 1, got %d", val)
    }
}

func TestSetDutyCycle_OutOfRange(t *testing.T) {
    d := newTestDevice(t)
    if err := d.SetDutyCycle(18, 101.0); err == nil {
        t.Error("expected error for duty > 100")
    }
}

func TestWatcher_EmitsEvents(t *testing.T) {
    log, _ := zap.NewDevelopment()
    w := newWatcher(nil, log)

    counter := 0
    readFn := func(pin int) (int, error) {
        counter++
        if counter%2 == 0 {
            return 1, nil
        }
        return 0, nil
    }

    ch, err := w.Watch(3, readFn)
    if err != nil {
        t.Fatalf("Watch failed: %v", err)
    }
    defer w.StopWatch(3)

    select {
    case event := <-ch:
        t.Logf("event: pin %d %d→%d", event.Pin, event.OldValue, event.NewValue)
    case <-time.After(2 * time.Second):
        t.Log("no event (ok in CI)")
    }
}

Integration tests

Integration tests run against a live Pi. Set INTEGRATION=1 and point your config.yaml at the Pi:

INTEGRATION=1 go test ./pkg/handlers/pioneer/... -v -run TestIntegration
=== RUN   TestIntegration_StartStop
--- PASS: TestIntegration_StartStop (3.21s)
=== RUN   TestIntegration_ReadWrite
    pioneer_test.go: Pin 3: 1
    pioneer_test.go: Pin 3 → HIGH
    pioneer_test.go: Pin 3 → LOW
--- PASS: TestIntegration_ReadWrite (1.84s)
=== RUN   TestIntegration_Watch
    pioneer_test.go: event: pin 5 changed 0→1
--- PASS: TestIntegration_Watch (0.72s)
=== RUN   TestIntegration_Metrics
    pioneer_test.go: reads=1 writes=2 errors=0 pool=3
--- PASS: TestIntegration_Metrics (2.10s)
PASS  ok  github.com/EraldCaka/PIoneer/pkg/handlers/pioneer  7.87s

Benchmark

go test ./pkg/handlers/pioneer/... -bench=. -benchmem
BenchmarkParsePinOutput-8     18492771    64.3 ns/op    0 B/op    0 allocs/op
BenchmarkWrite-8               1000000   1842 ns/op    48 B/op    2 allocs/op
BenchmarkRead-8                1000000   1756 ns/op    32 B/op    1 allocs/op

SSH round-trip latency (~5–15ms) dominates integration benchmarks — the library overhead itself is sub-microsecond.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Device

type Device = config.Device

func New

func New(file *os.File) (Device, error)

Directories

Path Synopsis
pkg

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL