//// 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" -> { process.unlink(pid) 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") use socket <- result.try(start() |> result.map_error(error_to_string)) use _ <- result.try( open(socket, interface) |> result.map_error(error_to_string), ) let excansock_monitor = process.monitor(types.socket_pid(socket)) 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, ) |> process.select_specific_monitor(excansock_monitor, fn(down) { types.ExcansockDown(down.reason) }) 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() } types.ExcansockDown(_reason) -> { actor.stop_abnormal("excansock_down") } } } /// 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) }