Current section
Files
Jump to
Current section
Files
src/cangaroo.gleam
//// cangaroo - Gleam bindings for SocketCAN communication on Linux.
////
//// This module provides the public API for interacting with CAN bus
//// interfaces through the Excansock Elixir library. It manages CAN socket
//// connections using OTP actors.
////
//// cangaroo attempts to provide a simple, idiomatic Gleam API.
//// Each CAN client runs as an OTP actor that
//// automatically receives incoming CAN frames and forwards them to your
//// application via a process subject.
import cangaroo/bridge
import cangaroo/errors
import cangaroo/excansock
import cangaroo/internal
import cangaroo/types.{
type ActorMessage, type CanClient, type CanFrame, type CanSocket,
}
import gleam/erlang/atom
import gleam/erlang/process
import gleam/list
import gleam/otp/actor
import gleam/result
fn start() -> Result(CanSocket, errors.CanError) {
let #(status, pid) = excansock.start_link()
case atom.to_string(status) {
"ok" -> Ok(types.new_socket(pid))
other_msg -> Error(errors.StartLinkError(other_msg))
}
}
fn open(socket: CanSocket, interface: String) -> Result(Nil, errors.CanError) {
let raw = excansock.open(types.socket_pid(socket), interface, False)
internal.decode_result(raw)
}
/// Creates a new CAN client and connects it to the specified network interface.
///
/// This function starts an OTP actor that manages the CAN socket connection.
/// The actor automatically receives incoming CAN frames from the bus and
/// forwards them to a subject that you can access via `types.messages(client)`.
///
/// The `interface` parameter should be the name of a CAN network interface
/// on your Linux system, such as `"can0"` for a physical CAN adapter or
/// `"vcan0"` for a virtual CAN interface used in testing.
///
/// Returns `Ok(CanClient)` on success, or an error if the connection fails.
pub fn start_link(interface: String) -> Result(CanClient, errors.CanError) {
let user_frames = process.new_subject()
let actor_result =
{
actor.new_with_initialiser(5000, fn(self_subject) {
let can_tag = atom.create("can_data_frame")
let extended_tag = atom.create("can_extended_frame")
let selector =
process.new_selector()
|> process.select(for: self_subject)
|> process.select_record(
tag: can_tag,
fields: 1,
mapping: types.RawCanData,
)
|> process.select_record(
tag: extended_tag,
fields: 1,
mapping: types.RawExtendedCanData,
)
use socket <- result.try(start() |> result.map_error(error_to_string))
use _ <- result.try(
open(socket, interface) |> result.map_error(error_to_string),
)
let client = types.new_client(socket, user_frames, self_subject)
actor.initialised(client)
|> actor.selecting(selector)
|> actor.returning(client)
|> Ok
})
}
|> actor.on_message(handle_message)
|> actor.start()
case actor_result {
Ok(started) -> Ok(started.data)
Error(actor.InitFailed(reason)) -> Error(errors.StartLinkError(reason))
Error(actor.InitExited(_)) -> Error(errors.StartLinkError("exited"))
Error(actor.InitTimeout) -> Error(errors.StartLinkError("timeout"))
}
}
fn error_to_string(err: errors.CanError) -> String {
case err {
errors.StartLinkError(reason) -> reason
errors.InterfaceBoundError(interface) -> "interface_bound:" <> interface
errors.UnknownError(reason) -> reason
errors.InvalidMessageTypeError -> "invalid_message_type"
}
}
fn handle_message(
client: CanClient,
msg: ActorMessage,
) -> actor.Next(CanClient, ActorMessage) {
case msg {
types.RawCanData(raw_data) -> {
let frame = bridge.unwrap_frame(raw_data)
let id = bridge.get_id(frame)
let data = bridge.get_data(frame)
let gleam_frame = types.CanFrame(id:, data:, extended: False)
process.send(types.messages(client), gleam_frame)
actor.continue(client)
}
types.RawExtendedCanData(raw_data) -> {
let frame = bridge.unwrap_extended_frame(raw_data)
let id = bridge.get_id(frame)
let data = bridge.get_data(frame)
let gleam_frame = types.CanFrame(id:, data:, extended: True)
process.send(types.messages(client), gleam_frame)
actor.continue(client)
}
types.Shutdown -> {
let socket = types.client_socket(client)
let _ = excansock.close(types.socket_pid(socket))
let _ = excansock.stop(types.socket_pid(socket))
actor.stop()
}
}
}
/// Closes the CAN client and releases all associated resources.
///
/// This function sends a shutdown message to the CAN client actor, which
/// will close the underlying CAN socket, stop the GenServer and stop the actor process. After
/// calling this function, the client should no longer be used.
///
/// This operation is asynchronous, the function returns immediately after
/// sending the shutdown message. The actual cleanup happens in the background.
pub fn close(client: CanClient) -> Nil {
process.send(types.client_actor_subject(client), types.Shutdown)
Nil
}
/// Sends a CAN frame over the bus.
///
/// The frame must be a `CanFrame` containing:
/// - `id`: The CAN identifier (11-bit standard or 29-bit extended)
/// - `data`: The payload as a `BitArray` (up to 8 bytes for standard CAN)
/// - `extended`: `True` for 29-bit extended IDs, `False` for 11-bit standard IDs
///
/// Returns `Ok(Nil)` if the frame was successfully queued for transmission,
/// or an error if the send operation failed.
pub fn send(client: CanClient, frame: CanFrame) -> Result(Nil, errors.CanError) {
let types.CanFrame(id:, data:, extended:) = frame
let elixir_frame = bridge.new_frame(id, data, extended)
let socket = types.client_socket(client)
let status = excansock.send(types.socket_pid(socket), elixir_frame)
internal.check_status(status)
}
/// Enables or disables local loopback of transmitted CAN frames.
///
/// When loopback is enabled, frames sent by this socket are also delivered
/// to other CAN sockets on the same interface (but not to this socket itself,
/// unless `recv_own_messages` is also enabled).
///
/// By default, loopback is typically enabled on CAN interfaces.
///
/// ## Parameters
///
/// - `client`: The CAN client to configure
/// - `value`: `True` to enable loopback, `False` to disable it
pub fn set_loopback(
client: CanClient,
value: Bool,
) -> Result(Nil, errors.CanError) {
let socket = types.client_socket(client)
let status = excansock.set_loopback(types.socket_pid(socket), value)
internal.check_status(status)
}
/// Controls whether this client receives its own transmitted CAN frames.
///
/// When enabled, frames sent by this client will also be received back
/// through the `messages` subject. This can be useful for confirming
/// successful transmission or for debugging purposes.
///
/// By default, receiving own messages is disabled.
///
/// Note: This setting only has an effect when loopback is also enabled
/// (see `set_loopback`).
///
/// ## Parameters
///
/// - `client`: The CAN client to configure
/// - `value`: `True` to receive own messages, `False` to filter them out
pub fn recv_own_messages(
client: CanClient,
value: Bool,
) -> Result(Nil, errors.CanError) {
let socket = types.client_socket(client)
let status = excansock.recv_own_messages(types.socket_pid(socket), value)
internal.check_status(status)
}
/// Sets CAN ID filters to selectively receive only matching frames.
///
/// Filters allow you to specify which CAN frames should be received based
/// on their CAN ID. Each filter consists of an `id` and a `mask`:
///
/// Multiple filters can be specified - a frame is accepted if it matches
/// ANY of the filters (OR logic).
///
/// Calling this function replaces any previously set filters.
///
/// ## Parameters
///
/// - `client`: The CAN client to configure
/// - `filters`: A list of `CanFilter` values defining the acceptance criteria
pub fn set_filters(
client: CanClient,
filters: List(types.CanFilter),
) -> Result(Nil, errors.CanError) {
let socket = types.client_socket(client)
let elixir_filters =
list.map(filters, fn(f) { bridge.new_filter(f.id, f.mask, f.extended) })
let status = excansock.set_filters(types.socket_pid(socket), elixir_filters)
internal.check_status(status)
}
/// Sets the error filter mask for receiving CAN error frames.
///
/// CAN error frames are special frames generated by the CAN controller
/// to report bus errors, such as bit errors, CRC errors, or bus-off conditions.
/// The error filter is a bitmask that determines which types of errors
/// should be reported to this client.
///
/// ## Parameters
///
/// - `client`: The CAN client to configure
/// - `filter`: A bitmask specifying which error types to receive
pub fn set_error_filter(
client: CanClient,
filter: Int,
) -> Result(Nil, errors.CanError) {
let socket = types.client_socket(client)
let status = excansock.set_error_filter(types.socket_pid(socket), filter)
internal.check_status(status)
}