A CAN bus library for Go with pluggable hardware adapters.
v2 is a redesign with a smaller, more idiomatic API, and is what this repository now contains. Migrating from v1? See MIGRATION.md. The v1 source is frozen at tag v1.4.8.
go get github.com/roffe/gocan/v2
import (
gocan "github.com/roffe/gocan/v2"
_ "github.com/roffe/gocan/v2/adapters/canusb" // registers "CANUSB VCP"
)
bus, err := gocan.Open(ctx, "CANUSB VCP", gocan.Config{
Port: "/dev/ttyUSB0",
CANRate: 500,
})
if err != nil {
return err
}
defer bus.Close()
// Fire and forget
err = bus.Send(ctx, gocan.NewFrame(0x240, []byte{0x3F, 0x81}))
// Request / reply, bounded by the context
ctx, cancel := context.WithTimeout(ctx, 200*time.Millisecond)
defer cancel()
reply, err := bus.Request(ctx, gocan.NewFrame(0x240, data), 0x258, 0x266)
// Stream frames
for frame := range bus.Frames(ctx, 0x1A0, 0x280) {
fmt.Println(frame)
}- The core is dependency-free.
github.com/roffe/gocan/v2is pure stdlib. Adapters live in their own packages (adapters/<name>) that register themselves on import, likedatabase/sqldrivers — you compile and link only the hardware support you actually import. Frameis a plain value. 16 bytes, no pointers, no hidden state. Copy it, reuse it, share it between goroutines freely.- Contexts are the only timeout mechanism. No timeout parameters, no
library-specific timeout errors.
context.WithTimeoutboundsRecv,Requestand friends; cancelling a subscription's context ends it. - Adapters are three methods.
Open,Send,Close. Incoming traffic is pushed to the bus (bus.Deliver), notifications are events (bus.Emit), and an unrecoverable failure (bus.Fatal) terminates the bus. No channel plumbing. Sendconfirms the write. The bus serializes senders and an adapter returns fromSendonce the frame reached the hardware, giving natural inter-frame pacing without a separate sync API.
// One frame, any of the given IDs (empty = any frame at all)
frame, err := bus.Recv(ctx, 0x258)
// Channel, for use in a select loop. Closed when ctx ends or the bus dies.
ch := bus.Subscribe(ctx, 0x258)
// Iterator over the same thing
for frame := range bus.Frames(ctx, 0x258) { ... }Delivery is non-blocking: a subscriber that stops draining loses frames (and a warning event tells you so).
bus, err := gocan.Open(ctx, name, cfg,
gocan.WithLogger(slog.Default()), // forward events to slog
gocan.WithEventFunc(func(e gocan.Event) { // or handle them yourself
fmt.Println(e)
}),
)
stop := bus.OnEvent(func(e gocan.Event) { ... }) // add/remove at runtime
defer stop()
err = bus.Wait(ctx) // block until the bus dies; returns the fatal error, if anybus.Done() / bus.Err() / bus.Context() expose the same lifecycle for
select loops. A clean Close reports nil from Err.
Adapters register themselves when their package is imported, from
github.com/roffe/gocan/v2/adapters/<pkg>:
| Package | Registers as | Build tag |
|---|---|---|
canusb |
CANUSB VCP |
— |
combi |
CombiAdapter |
— (cgo, libusb) |
just4trionic |
Just4Trionic |
— |
slcan |
SLCan |
— |
elm327 |
ELM327 |
— |
scantool |
OBDLink SX, OBDLink EX, STN1170, STN2120 |
— |
obdx |
OBDX Pro Wifi |
— |
txbridge |
txbridge wifi |
— |
yaca |
YACA |
— |
drewtech |
Drewtech Mongoose |
— (linux) |
socketcan |
SocketCAN <dev>, one per interface |
— (linux) |
canlib |
CANlib #N <device>, one per Kvaser channel |
canlib |
j2534 |
one per installed J2534 DLL | j2534 (windows) |
pcan |
one per PEAK channel | pcan (windows) |
rcan |
rCAN |
rcan |
| — | loopback, built into the core |
— |
Adapters needing a vendor SDK are behind build tags, so a default build
links none of them. canusb and scantool additionally register
direct-FTDI variants on Windows under the ftdi tag (and canusb a DLL
variant under canusb).
adapters/all blank-imports the adapters that build everywhere, for GUI apps
that list them at runtime; combi, rcan, yaca and elm327 are excluded
and must be imported directly.
gocan.Adapters() / gocan.AdapterNames() list what is registered, with
descriptions and capabilities for building UIs.
canlang embeds Lua so request/response flows can be scripted against a bus without recompiling the host — see its README for the script API. Like the adapters, it is a separate package: the core stays dependency-free unless you import it.
for f in bus:frames(0x1A0) do
print("rpm " .. f:u16(1))
endRun scripts standalone with cmd/canlang, or embed
with canlang.Run(ctx, bus, "script.lua").
Implement three methods and register a constructor from your own package. The loopback adapter is the minimal example; adapters/canusb is a complete serial adapter with a reply parser and hardware-free tests.
type Adapter interface {
Open(ctx context.Context, bus *Bus) error // start; push frames via bus.Deliver
Send(ctx context.Context, f Frame) error // write one frame; never called concurrently
Close() error
}
func init() {
gocan.Register(gocan.AdapterInfo{
Name: "my adapter",
New: func(cfg gocan.Config) (gocan.Adapter, error) { ... },
})
}Rules of thumb:
- Goroutines started in
Openshould exit whenctxis done. - A read error while
ctxis still alive is a dead port: callbus.Fatal. Ifctxis already done it's a shutdown: just return. - Recoverable trouble is an event (
bus.Emit), not an error return.