Ruby + SocketCAN: Listening to Your Car's Hardware
SocketCAN exposes the CAN bus as a standard Linux network interface. This post shows how to read real OBD-II vehicle data directly from a Ruby script using nothing but the standard Socket class.

Modern cars communicate internally over CAN bus, a protocol where every module on the vehicle broadcasts short frames that any other module can read. Linux exposes this bus through SocketCAN, a kernel framework that abstracts a CAN adapter as a standard network interface. Once the interface is up, any application that can open a socket can read from it, including Ruby with no external gems.
The first step is getting the virtual interface running for local testing, plus the can-utils package that ships cansend.
# Loading vcan kernel module
sudo modprobe vcan
# Create a virtual CAN interface
sudo ip link add dev vcan0 type vcan
# Bring vcan0 up
sudo ip link set up vcan0
# Install CAN utilities
sudo apt-get install -y can-utils
For a real USB-CAN adapter you would instead bring up can0 with the correct bitrate for the vehicle:
sudo ip link set can0 up type can bitrate 500000
With the interface up, you need something sending frames to read. This script simulates an ECU replying to OBD-II speed requests once per second with a random value between 0 and 180 km/h.
# car_simulator.rb
INTERFACE = "vcan0"
ECU_ID = "7E8"
MODE = "41" # OBD-II mode 01 response
PID_SPEED = "0D" # Speed PID (SAE J1979)
PADDING = "0000000000"
loop do
speed = rand(0..180).to_s(16).rjust(2, '0')
system("cansend #{INTERFACE} #{ECU_ID}#04#{MODE}#{PID_SPEED}#{speed}#{PADDING}")
sleep 1
end
The cansend format is INTERFACE ID#DATA. The ECU response ID 7E8 is the standard OBD-II reply address. Mode 41 means "mode 01 response", PID 0D is vehicle speed, and the speed byte is a single hex value where the raw byte equals km/h directly.
The monitor opens a raw CAN socket, binds it to the interface using a couple of low-level ioctl calls, then sits in a loop reading 16-byte frames and decoding only the speed ones.
# monitor.rb
require 'socket'
PF_CAN = 29
CAN_RAW = 1
INTERFACE = "vcan0"
SIOCGIFINDEX = 0x8933
ECU_RESPONSE = 0x7E8 # OBD-II ECU response ID
MODE_RESPONSE = 0x41 # mode 01 response
PID_SPEED = 0x0D # vehicle speed PID (SAE J1979)
FRAME_SIZE = 16
IFREQ_PACK = "a16i"
SOCKADDR_PACK = "S S i Q"
FRAME_UNPACK = "V C x3 a8"
socket = Socket.new(PF_CAN, Socket::SOCK_RAW, CAN_RAW)
# Build an ifreq struct: interface name (16 bytes) + integer placeholder
# The kernel will fill the integer with the interface index
ifreq = [INTERFACE, 0].pack(IFREQ_PACK)
# Ask the kernel for the numeric index of the interface (e.g. vcan0 -> 3)
# SIOCGIFINDEX is a Linux ioctl that resolves interface name -> index
socket.ioctl(SIOCGIFINDEX, ifreq)
# Extract the interface index written by the kernel into bytes 16..19
if_index = ifreq[16, 4].unpack1("i")
# Build a sockaddr_can struct and bind the socket to the CAN interface
# The kernel requires: [family, index, padding] packed as binary
socket.bind([PF_CAN, if_index, 0, 0].pack(SOCKADDR_PACK))
loop do
# Block until a CAN frame arrives - always 16 bytes (linux/can.h)
raw_frame = socket.recv(FRAME_SIZE)
# Unpack the binary frame:
# V = 32-bit little-endian CAN ID
# C = 1 byte data length
# x3 = skip 3 padding bytes
# a8 = 8 bytes of raw data payload
can_id, _, data = raw_frame.unpack(FRAME_UNPACK)
# Ignore frames that are not OBD-II ECU responses (ID 0x7E8)
next unless can_id == ECU_RESPONSE
# Byte 1 = mode response (0x41), byte 2 = PID - skip if not speed
next unless data[1].ord == MODE_RESPONSE && data[2].ord == PID_SPEED
# Byte 3 is the speed value - OBD-II formula: speed (km/h) = A directly
speed = data[3].ord
puts speed
end
A few details worth understanding. PF_CAN (29) is the protocol family for CAN sockets, the same way PF_INET is for TCP/IP. The frame format is fixed at 16 bytes by the kernel's linux/can.h: 4 bytes for the CAN ID, 1 byte for the data length, 3 padding bytes, and 8 bytes of payload. The unpack format string "V C x3 a8" maps exactly to that layout.
The two next unless guards do the filtering: the first rejects any frame not from the ECU reply address, and the second checks that the mode and PID bytes match so you only act on speed frames and ignore everything else on the bus.
Running the simulator in one terminal and the monitor in another gives you a live stream of speed values with no gems, no OBD library, and no abstraction layer between Ruby and the kernel socket.