MIO03 Detailed Description

Please note the following restrictions with MIO03 Revision 0:

  • You cannot use MVB, CAN or RS485 interface while the USB SERVICE interface is connected! Remove the USB SERVICE interface before using the other interfaces.
  • Wifi performance is not ideal and will be improved in coming revisons.

Port Reference Table

In case you want to address the services of the MIO03 via IP:Port, you can use the following table to find the correct port number:

Port Function
10000 MVB Sniffer
10002 CAN Sniffer

MVB Sniffer

Features

The MIO03 has a MVB sniffer function block to capture MVB frames on the MVB bus and provide them as a stream of telegrams to the host application. A telegram is the combination of the master frame with the corresponding answer from the slave.

The MVB sniffer operates listen-only, i.e. can only read from the bus, it cannot send any data, simply because the hardware has no transmitter. Furthermore, the receiver is coupled through a high impedance circuit to the bus, therefore the MVB sniffer cannot influence the bus operation.

Features:

  • MVB ESD and EMD support
  • Timestamping of received data with microsecond resolution
  • Capturing of all MVB frame types (FCodes)
  • Per stream filtering of MVB frames by address and FCode
  • Can capture up to 20000 MVB telegrams per second
  • Built-in MVB frame generator for internal self test

Connection

The MVB sniffer has two 9-pin D-Sub connectors, internally connected 1:1 with the following pinout. One connector is a plug, the second is a socket. The pinout is compliant with the MVB standard IEC61375-3-1.

Pin Symbol Description
1 A.Data_P positive wire Line_A
2 A.Data_N negative wire Line_A
3 TXE not connected to the MVB Sniffer / just passed through to other connector
4 B.Data_P positive wire Line_B
5 B.Data_N negative wire Line_B
6 A.Bus_GND not connected to the MVB Sniffer / just passed through to other connector
7 B.Bus_GND not connected to the MVB Sniffer / just passed through to other connector
8 A.Bus_5V not connected to the MVB Sniffer / just passed through to other connector
9 B.Bus_5V not connected to the MVB Sniffer / just passed through to other connector

WARNING: If the gender of your MVB cables don’t match to the MIO03’s connectors, don’t use gender changers! Most gender changers will swap pins, so that Line_A and Line_B and the polarities are swapped. This will lead to a non-working MVB Sniffer and/or non-working bus!

Functional Description

The MVB sniffer firmware permanently captures all frames from both lines of the MVB. Redundant frames are discarded (only one frame of the two redunant frames is used).

The MVB sniffer combines a master frame and a corresponding slave response into a Telegram, so the application receives the timestamp, MVB address, FCode and Data in a single object.

All types of FCodes are supported, but MVB Message Data re-assembly has to be done by the application.

When a host application starts a stream, it tells the MVB sniffer which types of telegrams it wants to receive. Application can define four filters per stream, each filter can specify a MVB address range, FCode mask and whether to receive timed out frames. The MVB sniffer will only send telegrams to the host application that match the filter criteria.

Multiple applications may be connected simultaneously to the same MVB sniffer, each application can use a different filter.

Handling of Erroneous MVB Frames

  • MVB frames with CRC errors are discarded
  • MVB slave frames without a master frame before are discarded
  • MVB slave frames that arrive too late may or may not be discarded, depending on the application’s filter settings

Using the io4edge API to access the MVB Sniffer

First, install the io4edge client library.

Want to have a quick look to the examples? See our Github repository.

First, install the io4edge client library.

Want to have a quick look to the examples? See our Github repository.

Connect to the MVB Sniffer Function

To access the MVB Sniffer Function, create a Client and save it to the variable c. Pass as address either a service address or an ip address with port. Example:

  • As a service address: MIO03-1-mvbSniffer
  • As an IP/Port: e.g. 192.168.201.1:10000

We need this client variable for all further access methods.

import (
  "fmt"
  "os"
  "time"

  "log"

  "github.com/ci4rail/io4edge-client-go/functionblock"
  "github.com/ci4rail/io4edge-client-go/mvbsniffer"
  mvbpb "github.com/ci4rail/io4edge_api/mvbSniffer/go/mvbSniffer/v1"
)

func main() {
  const timeout = 0 // use default timeout

  c, err := canl2.NewClientFromUniversalAddress("MIO03-1-mvbSniffer", timeout)
  if err != nil {
    log.Fatalf("Failed to create client: %v\n", err)
  }
}

To access the MVB Sniffer Function, create a Client and save it to the variable mvb_client. Pass as address either a service address or an ip address with port. Examples:

  • As a service address: MIO03-1-mvbSniffer
  • As an IP/Port: 192.168.201.1:10000

We need this client variable for all further access methods.

import io4edge_client.mvbsniffer as mvb
import io4edge_client.functionblock as fb

def main():
  mvb_client = mvb.Client(address)

Receiving MVB Telegrams

Start a Stream

To receive telegrams from the MVB, the API provides functions to start a Stream. When starting the stream, you can specify a filter to receive only telegrams that match the filter criteria.

The following code shows a typical filter setting: Receive all frames with all FCodes, but no timed out frames.

  // start stream
  err = c.StartStream(
    mvbsniffer.WithFilterMask(mvbsniffer.FilterMask{
      // receive any telegram, except timed out frames
      FCodeMask:             0xFFFF,
      Address:               0x0000,
      Mask:                  0x0000,
      IncludeTimedoutFrames: false,
    }),
    mvbsniffer.WithFBStreamOption(functionblock.WithBucketSamples(100)),
    mvbsniffer.WithFBStreamOption(functionblock.WithBufferedSamples(200)),
  )
  if err != nil {
    log.Errorf("StartStream failed: %v\n", err)
  }
    stream_start = mvb.Pb.StreamControlStart()
    stream_start.filter.add(f_code_mask=0x0FFF, include_timedout_frames=False)

    mvb_client.start_stream(
        stream_start,
        fb.Pb.StreamControlStart(
            bucketSamples=100,
            keepaliveInterval=1000,
            bufferedSamples=200,
            low_latency_mode=False,
        ),
    )

The filter algorithm for MVB addresses is pass_filter = (Address & Mask) == (Received_Address & Mask).

The filter algorithm for FCode is pass_filter = (FCodeMask & (1<<Received_FCode)) != 0.

You can combine up to 4 filters in one stream. If a telegram matches any filter, it is passed to the stream. This example defines two filters:

  // start stream
  err = c.StartStream(
    mvbsniffer.WithFilterMask(mvbsniffer.FilterMask{
      // receive only process data telegram, at any address
      FCodeMask:             0x001F,  // matches FCodes 0..4
      Address:               0x0000,
      Mask:                  0x0000,
      IncludeTimedoutFrames: false,
    }),
    mvbsniffer.WithFilterMask(mvbsniffer.FilterMask{
      // receive only FCode 15 telegrams with address 0x01xx
      FCodeMask:             0x8000,  // matches FCode 15
      Address:               0x0100,  // matches addresses 0x01xx
      Mask:                  0xFF00,
      IncludeTimedoutFrames: false,
    }),
    mvbsniffer.WithFBStreamOption(functionblock.WithBucketSamples(100)),
    mvbsniffer.WithFBStreamOption(functionblock.WithBufferedSamples(200)),
  )
    stream_start = mvb.Pb.StreamControlStart()
    # receive only process data telegram, at any address
    stream_start.filter.add(
      f_code_mask=0x001F,
      address=0x0000,
      mask=0x0000,
      include_timedout_frames=False
    )
    # receive only FCode 15 telegrams with address 0x01xx
    stream_start.filter.add(
      f_code_mask=0x8000,
      address=0x0100,
      mask=0xFF00,
      include_timedout_frames=False
    )

    mvb_client.start_stream(
        stream_start,
        fb.Pb.StreamControlStart(
            bucketSamples=100,
            keepaliveInterval=1000,
            bufferedSamples=200,
            low_latency_mode=False,
        ),
    )

Receive Telegrams

In the stream the firmware generates Buckets, where each Bucket contains a number of Telegrams. Each Telegram contains timestamp, FCode, MVB address, and data. Details can be found in the API protobuf definition.

To read samples from the stream:

  ...
  prevTs := uint64(0)
  for {
    // read next bucket from stream
    sd, err := c.ReadStream(time.Second * 1)

    if err != nil {
      log.Errorf("ReadStreamData failed: %v\n", err)
    } else {
      telegramCollection := sd.FSData.GetEntry()

      for _, telegram := range telegramCollection {
        dt := uint64(0)
        if prevTs != 0 {
          dt = telegram.Timestamp - prevTs
        }
        prevTs = telegram.Timestamp

        if telegram.State != uint32(mvbpb.Telegram_kSuccessful) {
          if telegram.State&uint32(mvbpb.Telegram_kTimedOut) != 0 {
            log.Errorf("No slave frame has been received to a master frame\n")
          }
          if telegram.State&uint32(mvbpb.Telegram_kMissedMVBFrames) != 0 {
            log.Errorf("one or more MVB frames are lost in the device since the last telegram\n")
          }
          if telegram.State&uint32(mvbpb.Telegram_kMissedTelegrams) != 0 {
            log.Errorf("one or more telegrams are lost\n")
          }
        }

        fmt.Printf("dt=%d %s\n", dt, telegramToString(telegram))
      }
    }
  }
  ...

func telegramToString(t *mvbpb.Telegram) string {
  s := fmt.Sprintf("addr=%06x, ", t.Address)
  s += fmt.Sprintf("%s, ", mvbpb.Telegram_Type_name[int32(t.Type)])
  if len(t.Data) > 0 {
    s += "data="
    for i := 0; i < len(t.Data); i++ {
      s += fmt.Sprintf("%02x ", t.Data[i])
    }
  }
  return s
}
    while True:
        try:
            generic_stream_data, telegrams = mvb_client.read_stream(timeout=3)
        except TimeoutError:
            print("Timeout while reading stream")
            continue

        print(
            "Received %d telegrams, seq=%d"
            % (len(telegrams.entry), generic_stream_data.sequence)
        )
        for telegram in telegrams.entry:
            if telegram.state != mvb.TelegramPb.Telegram.State.kSuccessful:
                if telegram.state & mvb.TelegramPb.Telegram.State.kTimedOut:
                    print("No slave frame has been received to a master frame")
                if telegram.state & mvb.TelegramPb.Telegram.State.kMissedMVBFrames:
                    print(
                        "one or more MVB frames are lost in the device since the last telegram"
                    )
                if telegram.State & mvb.TelegramPb.Telegram.State.kMissedMVBFrames:
                    print("one or more telegrams are lost")
            print(telegram_to_str(telegram))


def telegram_to_str(telegram):
    ret_val = "addr=%03x, " % telegram.address
    ret_val += "%s" % mvb.TelegramPb._TELEGRAM_TYPE.values_by_number[telegram.type].name
    if len(telegram.data) > 0:
        ret_val += ", data="
        for b in telegram.data:
            ret_val += "%02x " % b
    return ret_val

NOTE: At the moment, timestamps are expressed in micro seconds relative to the start of the MIO03. Future client libraries will map the time to the host’s time domain

Controlling the Stream

The stream behavior can be fine-tuned to the application needs. If you do not specify any parameters, the default values are used.

  • The BucketSamples parameter (default: 25) defines the number of samples per bucket. If the bucket contains BucketSamples, it is sent to the client.

  • The KeepAliveInterval parameter (default: 1000) defines the maximum time in ms between two buckets. If the bucket is not full, it is sent after the configured interval.

  • The BufferedSamples parameter (default: 50) defines the number of samples that can be buffered in the device. If the buffer is full, the oldest samples are overwritten. As a rule of thumb, BufferedSamples should be at least two times the BucketSamples. Select a higher number if your reception process is slow to avoid buffer overruns.

  // configure stream to send the bucket at least once a second
  // configure the maximum samples per bucket to 100
  // configure low latency mode
  // configure the buffered samples to 200
  err = c.StartStream(
      mvbsniffer.WithFBStreamOption(functionblock.WithKeepaliveInterval(1000)),
      mvbsniffer.WithFBStreamOption(functionblock.WithBucketSamples(100)),
      mvbsniffer.WithFBStreamOption(functionblock.WithLowLatencyMode(true))
      mvbsniffer.WithFBStreamOption(functionblock.WithBufferedSamples(200)),
  )

The stream behavior can be fine-tuned to the application needs:

  • The bucketSamples parameter defines the number of samples per bucket. If the bucket contains bucketSamples, it is sent to the client.

  • The keepAliveInterval parameter defines the maximum time in ms between two buckets. If the bucket is not full, it is sent after the configured interval.

  • The bufferedSamples parameter defines the number of samples that can be buffered in the device. If the buffer is full, the oldest samples are overwritten. As a rule of thumb, bufferedSamples should be at least two times the bucketSamples. Select a higher number if your reception process is slow to avoid buffer overruns.

    stream_start = mvb.Pb.StreamControlStart()
    stream_start.filter.add(f_code_mask=0x0FFF, include_timedout_frames=False)

    mvb_client.start_stream(
        stream_start,
        fb.Pb.StreamControlStart(
            bucketSamples=100,
            keepaliveInterval=1000,
            bufferedSamples=200,
            low_latency_mode=False,
        ),
    )

CANBus Interface

The MIO03 has one CANBus interfaces, labelled FB LO.

Features

  • ISO 11898 CANBus Interface, up to 1MBit/s
  • Usable for direct I/O or as data logger with multiple data streams.
  • SocketCAN Support
  • Standard and Extended Frame Support
  • RTR Frame Support
  • Fixed Listen Only Mode
  • One Acceptance Mask/Filter

Connection

Connection is done via 9-pin DSub plug. On the same connector, two RS485 sniffer interfaces are available, which are not used by the CANBus interface.

Pin Symbol Description
1 RS485_A_RX+ Not connected
2 CAN_L CAN Signal (dominant low)
3 GND_ISO CAN Ground
4 RS485_B_RX+ Not used by CAN Interface
5 SHIELD Shield
6 RS485_A_RX- Not used by CAN Interface
7 CAN_H CAN Signal (dominant high)
8 - Not connected
9 RS485_B_RX- Not used by CAN Interface

Using the io4edge API to access CAN Function

First, install the io4edge client library.

Want to have a quick look to the examples? See our Github repository.

First, install the io4edge client library.

Want to have a quick look to the examples? See our Github repository.

Connect to the CAN Function

To access the CAN Function, create a Client and save it to the variable c. Pass as address either a service address or an ip address with port. Example:

  • As a service address: MIO03-1-can
  • As an IP/Port: e.g. 192.168.201.1:10002

We need this client variable for all further access methods.

import (
  "fmt"
  "os"
  "time"
  "github.com/ci4rail/io4edge-client-go/canl2"
  fspb "github.com/ci4rail/io4edge_api/canL2/go/canL2/v1alpha1"
)

func main() {
  const timeout = 0 // use default timeout

  c, err := canl2.NewClientFromUniversalAddress("MIO03-1-can", timeout)
  if err != nil {
    log.Fatalf("Failed to create canl2 client: %v\n", err)
  }
}

To access the CAN Function, create a Client and save it to the variable can_client. Pass as address either a service address or an ip address with port. Examples:

  • As a service address: MIO03-1-can
  • As an IP/Port: 192.168.201.1:10000

We need this client variable for all further access methods.

import io4edge_client.canl2 as canl2
import io4edge_client.functionblock as fb

def main():
  can_client = canl2.Client("MIO03-1-can")

Bus Configuration

There are two ways to configure the CAN function:

  • Using a persistent parameter that is stored in the flash of the MIO03, as described here.
  • Temporarily, via the io4edge CANL2 API, as shown below
Temporary Bus Configuration

When applying a configuration via the API, the configuration is only active until the next restart of the device. The configuration is not stored in flash. When the device is restarted, it will apply the persistent configuration stored in flash, or - if no persistent configuration is available - will keep the CAN controller disabled.

Bus Configuration can be set via UploadConfiguration.

  err = c.UploadConfiguration(
    canl2.WithBitRate(125000),
    canl2.WithSamplePoint(0.625),
    canl2.WithSJW(1),
    canl2.WithListenOnly(true),
  )

Bus Configuration can be set via upload_configuration.

Note: The sample point is given as a thousandth of a percent, e.g. 625 for 62.5%.

    can_client.upload_configuration(
        canl2.Pb.ConfigurationSet(
            baud=125000,
            samplePoint=625,
            sjw=1,
            listenOnly=True,
        )
    )

Receiving CAN Data

To receive data from the CANbus, the API provides functions to start a Stream.

In the stream the firmware generates Buckets, where each Bucket contains a number of Samples. Each sample contains:

  • A timestamp of the sample
  • The CAN frame (may be missing in case of bus state changes or error events)
  • The CAN Bus state (Ok, error passive or bus off)
  • Error events (currently: receive buffer overruns)

For efficiency, multiple samples are gathered are sent as one Bucket to the host.

Without any parameters, the stream receives all CAN frames:

// start stream
err = c.StartStream()

Missing parameters to ´StartStream` will take default values:

  • Filter off (let all CAN frames pass through)
  • Maximum samples per bucket: 25
  • Buffered Samples: 50
  • Keep Alive Interval: 1000ms
  • Low Latency Mode: off
  for {
    // read next bucket from stream
    sd, err := c.ReadStream(time.Second * 5)

    if err != nil {
      log.Printf("ReadStreamData failed: %v\n", err)
    } else {
      samples := sd.FSData.Samples
      fmt.Printf("got stream data with %d samples\n", len(samples))

      for _, s := range samples {
        fmt.Printf("  %s\n", dumpSample(s))
      }
    }
  }

func dumpSample(sample *fspb.Sample) string {
  var s string

  s = fmt.Sprintf("@%010d us: ", sample.Timestamp)
  if sample.IsDataFrame {
    f := sample.Frame
    s += "ID:"
    if f.ExtendedFrameFormat {
      s += fmt.Sprintf("%08x", f.MessageId)
    } else {
      s += fmt.Sprintf("%03x", f.MessageId)
    }
    if f.RemoteFrame {
      s += " R"
    }
    s += " DATA:"
    for _, b := range f.Data {
      s += fmt.Sprintf("%02x ", b)
    }
    s += " "
  }
  s += "ERROR:" + sample.Error.String()
  s += " STATE:" + sample.ControllerState.String()

  return s
}

    # start stream, accept all frames
    stream_start = canl2.Pb.StreamControlStart(
        acceptanceCode=0, acceptanceMask=0
    )

    can_client.start_stream(
        stream_start,
        fb.Pb.StreamControlStart(
            bucketSamples=100,
            keepaliveInterval=1000,
            bufferedSamples=200,
            low_latency_mode=False,
        ),
    )

    while True:
        try:
            generic_stream_data, stream_data = can_client.read_stream(timeout=3)
        except TimeoutError:
            print("Timeout while reading stream")
            continue

        print(
            "Received %d samples, seq=%d" % (len(stream_data.samples), generic_stream_data.sequence)
        )

        for sample in stream_data.samples:
            print(sample_to_str(sample))


def sample_to_str(sample):
    ret_val = "%10d us: " % sample.timestamp
    if sample.isDataFrame:
        frame = sample.frame
        ret_val += "ID:"
        if frame.extendedFrameFormat:
            ret_val += "%08X" % frame.messageId
        else:
            ret_val += "%03X" % frame.messageId
        if frame.remoteFrame:
            ret_val += " R"
        ret_val += " DATA:"
        for i in range(len(frame.data)):
            ret_val += "%02X " % frame.data[i]
        ret_val += " "

    ret_val += "ERROR: " + canl2.Pb._ERROREVENT.values_by_number[sample.error].name
    ret_val += (
        " STATE: "
        + canl2.Pb._CONTROLLERSTATE.values_by_number[sample.controllerState].name
    )
    return ret_val

NOTE: At the moment, timestamps are expressed in micro seconds relative to the start of the MIO03. Future client libraries will map the time to the host’s time domain

Controlling the Stream

The stream behavior can be fine-tuned to the application needs. If you do not specify any parameters, the default values are used.

  • The BucketSamples parameter (default: 25) defines the number of samples per bucket. If the bucket contains BucketSamples, it is sent to the client.

  • The KeepAliveInterval parameter (default: 1000) defines the maximum time in ms between two buckets. If the bucket is not full, it is sent after the configured interval.

  • The BufferedSamples parameter (default: 50) defines the number of samples that can be buffered in the device. If the buffer is full, the oldest samples are overwritten. As a rule of thumb, BufferedSamples should be at least two times the BucketSamples. Select a higher number if your reception process is slow to avoid buffer overruns.

  • If you want low latency on the received data, you can enable the “low latency” mode by using LowLatencyMode (default: false). In this mode, samples are sent as soon as possible after they have been received. This means that the buckets contain 1..BufferedSamples samples.

  // configure stream to send the bucket at least once a second
  // configure the maximum samples per bucket to 25
  // configure low latency mode
  // configure the buffered samples to 200
  err = c.StartStream(
      canl2.WithFBStreamOption(functionblock.WithKeepaliveInterval(1000)),
      canl2.WithFBStreamOption(functionblock.WithBucketSamples(25)),
      canl2.WithFBStreamOption(functionblock.WithLowLatencyMode(true))
      canl2.WithFBStreamOption(functionblock.WithBufferedSamples(200)),
  )

If you don’t want to receive all CAN identifiers, you can specify an acceptance code and mask that is applied to each received frame. The filter algorithm is pass_filter = (code & mask) == (received_frame_id & mask). The same filter is applied to extended frames and standard frames.

  // apply a filter. Frames with an identifier of 0x1xx pass the filter, other frames are filtered out
  code := 0x100
  mask := 0x700
  err = c.StartStream(
    canl2.WithFilter(code, mask),
  )

The stream behavior can be fine-tuned to the application needs:

  • The bucketSamples parameter defines the number of samples per bucket. If the bucket contains bucketSamples, it is sent to the client.

  • The keepAliveInterval parameter defines the maximum time in ms between two buckets. If the bucket is not full, it is sent after the configured interval.

  • The bufferedSamples parameter defines the number of samples that can be buffered in the device. If the buffer is full, the oldest samples are overwritten. As a rule of thumb, bufferedSamples should be at least two times the bucketSamples. Select a higher number if your reception process is slow to avoid buffer overruns.

  • If you want low latency on the received data, you can enable the “low latency” mode by setting low_latency_mode to True. In this mode, samples are sent as soon as possible after they have been received. This means that the buckets contain 1..bufferedSamples samples.

If you don’t want to receive all CAN identifiers, you can specify an acceptance code and mask that is applied to each received frame. The filter algorithm is pass_filter = (code & mask) == (received_frame_id & mask). The same filter is applied to extended frames and standard frames.

    # apply a filter. Frames with an identifier of 0x1xx pass the filter, other frames are filtered out
    stream_start = canl2.Pb.StreamControlStart(
        acceptanceCode=0x100, acceptanceMask=0x700
    )

    can_client.start_stream(
        stream_start,
        fb.Pb.StreamControlStart(
            bucketSamples=100,
            keepaliveInterval=1000,
            bufferedSamples=200,
            low_latency_mode=args.lowlatency,
        ),
    )

Error Indications and Bus State

The samples in the stream contain also error events and the current bus state.

Error events can be:

  • ErrorEvent_CAN_NO_ERROR - no event
  • ErrorEvent_CAN_RX_QUEUE_FULL - either the CAN controller dropped a frame or the stream buffer was full

Each sample contains also the bus state. When the bus state changes, a sample without a CAN frame may be generated. Furthermore, client method GetCtrlState may be used to query the current status.

Bus States can be:

  • ControllerState_CAN_OK - CAN controller is “Error Active”
  • ControllerState_CAN_ERROR_PASSIVE - CAN controller is “Error Passive”
  • ControllerState_CAN_BUS_OFF - CAN controller is bus off

Error events can be:

  • canl2.Pb.ErrorEvent.CAN_NO_ERROR - no event
  • canl2.Pb.ErrorEvent.CAN_RX_QUEUE_FULL - either the CAN controller dropped a frame or the stream buffer was full

Each sample contains also the bus state. When the bus state changes, a sample without a CAN frame may be generated. Furthermore, client method ctrl_state may be used to query the current status.

Bus States can be:

  • canl2.Pb.ControllerState.CAN_OK - CAN controller is “Error Active”
  • canl2.Pb.ControllerState.CAN_ERROR_PASSIVE - CAN controller is “Error Passive”
  • canl2.Pb.ControllerState.CAN_BUS_OFF - CAN controller is bus off

Bus Off Handling

When the CAN controller detects serious communication problems, it enters “Bus off” state. In this state, the CAN controller cannot communicate anymore with the bus.

When bus off state is entered, The firmware waits 3 seconds and then resets the CAN controller.

Multiple Clients

It is possible to have multiple clients active at the same time. For example: One client receiving a stream with a specific filter and a another client receiving a stream with a different filter.

Using SocketCAN

In Linux, SocketCAN is the default framework to access the CANBus from applications.

The MIO03 can be integrated into SocketCAN using the socketcan-io4edge gateway:

MIO03 product view

NOTE: When using SocketCAN, you must configure the CAN Controller persistently as shown here MIO03, as described here.

In Ci4Rail Linux Images, the socketcan-io4edge gateway is started automatically by socketcan-io4edge-runner which detects available io4edge devices with CAN support and start an instance of the socketcan-io4edge gateway, if the corresponding virtual can instance exists. For an example, see here.