Date: Fri, 29 Mar 2024 12:03:00 +0000 (UTC) Message-ID: <1802063984.1.1711713780457@6c03d871af1c> Subject: Exported From Confluence MIME-Version: 1.0 Content-Type: multipart/related; boundary="----=_Part_0_213692333.1711713780440" ------=_Part_0_213692333.1711713780440 Content-Type: text/html; charset=UTF-8 Content-Transfer-Encoding: quoted-printable Content-Location: file:///C:/exported.html
The dev_stream
abstraction targets stream based d=
rivers, such as UART, CAN etc. where information can flow in and out of the=
peripheral. This abstraction is more than just a hardware abstraction (HAL=
) residing just above the peripheral. For example, a UART Stream I/O abstra=
cts it away from specific configuration of the end-user peripheral, such as=
the baud rate, and the parity bit configuration. This also abstracts the u=
se of the RTOS; for instance, if the intent is to write 10 characters to UA=
RT, the underlying Device Stream Glue could send the data to a queue, and t=
he high level Application code is agnostic to the specific driver level det=
ails. In this sense, the Stream I/O is a Platform Abstraction<=
/strong>, and not necessarily just a HAL.
The objective is that the high level Sibros code should be agnos= tic to specific workings of a peripheral of the end-user.
Device Stream abstraction does not address specialized register based pe= ripherals, such as a PWM configuration or the manipulation of a Watchdog ti= mer; that abstraction is addressed using a different design. Device stream = can abstract anything you believe is feasible, and factoring in the benefit= s, you can apply it where it fits. For instance, the GPIO is not necessaril= y a stream based peripheral device, but we can easily apply it to reap the = benefits; see the GPIO example belo= w.
The Stream Abstraction provides:
Consistent API to read and write diverse set of peripheral devices (i.e.= : CAN, I2C, Flash memory), while avoiding API fragmentation.
Portable and re-usable higher level Application code which increases int= eroperability between product developers.
A single tunnel for many communication peripherals. This helps:
Facilitate Unit-Testing; it becomes easier for the developers to deal wi= th only one kind of "Mock".
Make your software "ready" for Software in-the-loop (SIL)= strong> simulation. You can re-compile the code for Linux and re-direc= t the I/O to behave differently
A consistent and well understood I/O abstraction is necessary to encapsu= late vendor specific peripheral drivers and reduce API fragmentation. An em= bedded platform can vary wildly between different micro-controllers, and in= order to maximize productivity and code re-use, there is benefit to be abl= e to read and write diverse I/O through the same code interfac= e.
The abstraction mimics a Linux device such that any interaction with the=
underlining hardware can occur using read()
or =
;write()
methods. To minimize namespace and application i=
ntegration conflicts we prepend each API with dev_stream__
, such asdev_stream__read()
and dev_stream__w=
rite()
.
Device stream abstraction is ideally suited for stream based I/O, howeve= r, the aim is to also solve for sophisticated I/O to minimize abstraction f= ragmentation. Network I/O is a more complex case, and the goal includes to = abstract this case also because it leaves the end-user with a single code m= odule (Device Stream Glue) to hook Sibros applications to your platform.
Our solution resembles FreeRTOS+IO, but we do not use the same function names as=
FreeRTOS+IO because the RTOS itself may be abstracted away. Another distin=
ction from FreeRTOS+IO is that we do not implement very domain specific con=
trol, such as polled, or zero copy ioctl
types, henc=
e making trivial Device stream glue simpler.
Sometimes, developers create an abstraction but the use cases are not do= cumented or tested. When this occurs, it creates two problems to deal with.= First, the user of the API may not understand how to use different pieces = of API to solve a particular problem. Secondly, and worse, the API may not = even have solved very specific problems at all because maybe a awkward use = case was missing. With this in mind, we created the Sibros Device Stream no= t only with abundant documentation, but also with examples of how it would = work for different use cases.
To perform I/O, the two variants are File I/O and Device I/O. Here is th=
e comparison of the read()
function between the two:=
// File size_t fread ( void * ptr, size_t size, size_t count, FILE * stream ); // Device ssize_t read(int fd, void *buf, size_t count);
Since we want to be able to get error codes back while reading (or writi=
ng) a device, it is preferred to use the Device I/O since the return type i=
s a signed type instead of size_t
. The one thing that the=
Device I/O lacks is that the size_t count
is missin=
g, which we added to our dev_stream__read()
and&nbs=
p;dev_stream__write()
API.
The Device stream easily allows to re-direct the API to linux devices.= p>
#ifdef = LINUX #define dev_stream__read(device, input_buffer, bytes_per_element, num_eleme= nts) \ read(device, input_buffer, (bytes_per_element * num_elements)) #endif
The specifications and the contact of the Device Stream Abstraction is c= ontained in the code below. The implementation of this file is what we call= Device Stream Glue which the end-user (you) imp= lements to glue our modular code blocks to your software. Don't worry, ther= e is not much to implement if you already have your plat= form drivers, and hence it should be just a re-direction from an API intent to your existing code.
dev_stream.h This header file provides the contract or the "tunnel" to read and write stream based=
peripheral drivers. There is sample code at the end of this guide which provides hel=
pful reference for the implementation.
#pragma= once #include <stdbool.h> #include <stdint.h> #include <stdlib.h> /**************************************************************************= ***** * * D E F I N E S * **************************************************************************= *****/ /**************************************************************************= ***** * * E N U M S * **************************************************************************= *****/ /** * This provides different control types for dev_stream__control() * The use cases are documented for the Device Stream control types */ typedef enum { DEV_STREAM__CONTROL_NONE =3D 0, /** * @{ * Set a read or a write timeout on a device. Initial timeouts on a devi= ce should * default to infinity. After a read or write timeout is applied, then t= he device * should start to respect the read timeout during read() and write timeo= ut during * write() * Argument: 'uint32_t timeout_ms' * * Examples: * - Set a transmit() timeout for a UART putchar * - Set a recv() timeout on a socket */ DEV_STREAM__CONTROL_SET_READ_TIMEOUT, DEV_STREAM__CONTROL_SET_WRITE_TIMEOUT, /** @} */ /** * Flush the data stream completely before returning * Argument: None * * Examples * - Flush transmit FIFO of the UART */ DEV_STREAM__CONTROL_FLUSH_TX, /** * @{ * A device may require a mutex to be able to use it from multiple contex= ts * * For a device like an SPI, DEV_STREAM__CONTROL_ACCESS_REQUEST can perfo= rm multiple * things upon this request such as: * - Take mutex * - Device chip-select (CS) */ DEV_STREAM__CONTROL_ACCESS_REQUEST, ///< Argument: 'uint32_t timeout_= ms' DEV_STREAM__CONTROL_ACCESS_RELEASE, ///< Argument: None /** @} */ /** * I2C bus requires the START and STOP conditions for a data transfer muc= h * like the SPI chip-select and de-select. In particular, the I2C bus * read transaction involves, START, WRITE ... REPEAT-START, READ ... STO= P */ DEV_STREAM__CONTROL_I2C_START, ///< Argument: 'uint32_t timeou= t_ms' that can be used to take an I2C mutex DEV_STREAM__CONTROL_I2C_REPEAT_START, ///< Argument: None DEV_STREAM__CONTROL_I2C_STOP, ///< Argument: None /** @} */ /** * A TCP listening socket uses this control type to perform the accept() * Argument: 'dev_stream_T *accepted_client' */ DEV_STREAM__CONTROL_TCP_ACCEPT, /** * @{ * A UDP socket is just a resource and it can be used to send the data pa= cket * to any IP address. These control types "set" the destination IP and p= ort. */ DEV_STREAM__CONTROL_UDP_SEND_TO_HOST, ///< Argument: 'uint16_t port' DEV_STREAM__CONTROL_UDP_SEND_TO_PORT, ///< Argument: 'const char * ho= st' /** @} */ } dev_stream__control_E; /** * Flag types while opening a device * This provides distinctions such as: * - An input or output GPIO * - A transmit or a receive CAN mailbox */ typedef enum { DEV_STREAM__FLAGS_NONE =3D 0, DEV_STREAM__FLAGS_INPUT, DEV_STREAM__FLAGS_OUTPUT, DEV_STREAM__FLAGS_INPUT_AND_OUTPUT, } dev_stream__flags_E; /// Seek types used at dev_stream__lseek() typedef enum { DEV_STREAM__SEEK_NONE =3D 0, DEV_STREAM__SEEK_SET, ///< Set the seek to the absolute location DEV_STREAM__SEEK_CURRENT, ///< Set the seek to current location plus = the offset DEV_STREAM__SEEK_END, ///< Set the seek to size of the file plus = the offset } dev_stream__seek_E; /**************************************************************************= ***** * * T Y P E D E F S * **************************************************************************= *****/ /** * The Device Stream is simply a unique descriptor * It is used as an identifier to figure out how the read() and write() sho= uld * re-direct the request for a particular descriptor (i.e.: UART vs. SPI) */ typedef int dev_stream_T; /**************************************************************************= ***** * * P U B L I C F U N C T I O N S * **************************************************************************= *****/ /** * Open a device for read and/or write * @param descriptive_path The path of the device, such as "/dev/uart0" * @param flags @see dev_stream__flags_E; this is an int to co= nform with linux style open() * * @returns the descriptor (0 is a valid value), or -1 if there was an erro= r acquiring the device */ dev_stream_T dev_stream__open(const char *descriptive_path, dev_stream__fla= gs_E flags); /** * Closes a device * Typically, a persistent device such as UART doesn't need a closure * The use case for this is to close network sockets * * @returns 0 on success, or non-zero which is an error code */ int dev_stream__close(dev_stream_T device); /** * Reads num_elements with each element of size bytes_per_element * to the input_buffer from the device referenced by dev_stream_T * * @returns -1 upon an error * @returns the number of bytes read into the input_buffer * * @note the number of bytes read (and returned) may be fewer than requeste= d (num_elements * bytes_per_element) */ int dev_stream__read(dev_stream_T device, void *input_buffer, size_t bytes_= per_element, size_t num_elements); /** * Writes num_elements with each element of size bytes_per_element * from the output_buffer to the device referenced by dev_stream_T * * @returns -1 upon an error * @returns 0 if the device was not ready to be written * @returns the number of bytes written to the device */ int dev_stream__write(dev_stream_T device, const void *output_buffer, size_= t bytes_per_element, size_t num_elements); /** * Perform a special operation on the descriptor * @param request_type @see dev_stream__control_E * @param argument Depends on the value of request_type; @see dev_stream__c= ontrol_E * * @returns zero upon success, or non-zero error code upon failure */ int dev_stream__control(dev_stream_T device, dev_stream__control_E request_= type, void *argument); /** * Perform a seek operation on the device (file seek) for the next read() o= r write() * The meaning of this API depends on the device, but is ideally suited for * an address based device, such as a file, or flash memory device * * @returns -1 upon an error * @returns the resulting offset location as measured in bytes from the beg= inning of the file. */ int dev_stream__lseek(dev_stream_T device, int offset, dev_stream__seek_E w= hence); /** * @returns true if the device descriptor is valid * * Typically, this should return true if the device descriptor is >=3D 0 * Sometimes, value of zero may be considered invalid */ bool dev_stream__is_valid(dev_stream_T device);
The dev_stream_t device descriptor essentially acts as the socket number= . It should NEVER be copied. Copying this is similar to copying a pointer, = leaving it at the mercy of future developers. The core issue is that if one= copy is destructed (ie: connection closed) then the second copy should als= o be destructed. This may be somewhat possible in C++ and other higher-leve= l languages but this is especially hard to enforce in C.
The stream abstraction is designed to solve various different use cases = of common micro-controller peripherals.
UART
Write output
Wait for input (with timeout)
GPIO
Read and Write
Configure a complex GPIO
SPI
Transmit only
Receive only
Data transfer with a mutex
Flash Memory
Read
Write (to a specific location)
I2C
Write a byte stream to an addressed I2C device
Read a byte stream
CAN
Write a dedicated mailbox
Poll for a message from a receive mailbox
Read a message from a mailbox with a timeout
Sockets
Client socket to connect to a server
Server (listening) socket
UDP receive
UDP transmit
We do not implement polled and queue configuration on the high level cod=
e. Instead, we leave it up to the Device Stream Glue to provide the desired=
behavior. For example, in the code snippet below, we can assume that the U=
ART0 is opened with the RTOS backed queue to send and receive data. Initial=
ly when the device is opened, the timeout value should be infinity, but aft=
er the timeouts are set using dev_stream__control_E
=
enumeration then the dev_stream__write()
and
const u= int32_t timeout_ms =3D 1000; dev_stream_T uart =3D dev_stream__open("/dev/uart0", DEV_STREAM__MODE_INPUT= _AND_OUTPUT); if (!dev_stream__is_valid(uart)) { // handle error return; } // Send a message and wait for data to be bufferred or sent const char message[] =3D "hello world\r\n"; dev_stream__write(uart, message, sizeof(message), 1); // Attempt to send data within a defined timeout dev_stream__control(uart, DEV_STREAM__CONTROL_SET_WRITE_TIMEOUT, &timeo= ut_ms); if (dev_stream__write(uart, message, sizeof(message), 1) !=3D sizeof(messag= e)) { // The entirety of the data was not sent successfully } // By default, we wait forever for the input char input[4] =3D {0}; if (dev_stream__read(uart, input, sizeof(input), 1) > 0) { // Do something with the input } // Get a byte with a timeout dev_stream__control(uart, DEV_STREAM__CONTROL_SET_READ_TIMEOUT, &timeou= t_ms); if (dev_stream__read(uart, input, 1, 1) > 0) { // Got data within the timeout
The GPIO can be configured as input, output, or input/output through the=
dev_stream__flags_E
, and from thereafter dev_=
stream__read()
or dev_stream__write()
ca=
n be used to access or control it. This abstraction has a very small overhe=
ad, in general if an application needs faster timing requirements than this=
supports, a different module should be used.
dev_str= eam_T GPIO; // In general, Sibros code modules may ask for general GPIOs // where the name of the descriptor expresses the intent GPIO =3D dev_stream__open("/dev/GPIO/spi_flash_cs", DEV_STREAM__MODE_OUTPUT= ); // Your application may use very specific GPIOs GPIO =3D dev_stream__open("/dev/GPIO/PD1", DEV_STREAM__MODE_INPUT); GPIO =3D dev_stream__open("/dev/GPIO/PC2", DEV_STREAM__MODE_OUTPUT); GPIO =3D dev_stream__open("/dev/GPIO/PA0", DEV_STREAM__MODE_INPUT_AND_OUTPU= T); // GPIO that can be read or written // the device name is arbitrary and it can be used to identify specific con= figuration GPIO =3D dev_stream__open("/dev/GPIO/PA0/oc", DEV_STREAM__MODE_OUTPUT); = // GPIO - Output and open collector GPIO =3D dev_stream__open("/dev/GPIO/PB1/slew/high", DEV_STREAM__MODE_OUTPU= T); // GPIO - Output with high slew rate // Below is a sample GPIO abstraction /// @file GPIO_io.h void GPIO_set(dev_stream_T GPIO) { static const bool set =3D true; dev_stream__write(GPIO, &set, sizeof(set), 1); } void GPIO_unset(dev_stream_T GPIO) { static const bool unset =3D !true; dev_stream__write(GPIO, &unset, sizeof(unset), 1); } bool GPIO_read(dev_stream_T GPIO) { bool status =3D false; dev_stream__read(GPIO, &status, sizeof(status), 1); return status; } void GPIO_toggle(dev_stream_T GPIO) { const bool current =3D GPIO_read(GPIO); if (current) { GPIO_unset(GPIO); } else { GPIO_set(GPIO); } }
The SPI peripheral must send data out in order to receive the data. The =
transmit case is simple because the write()
would si=
mply discard the input data. The read()
case assumes=
that the Device Stream Glue sends 0xFF
for each byt=
e that shall be received.
dev_str= eam_T SPI_device =3D dev_stream__open("/dev/spi1", DEV_STREAM__MODE_INPUT_A= ND_OUTPUT); if (!dev_stream__is_valid(spi)) { // handle error return; } // Write only -- discards data input const char data_out[] =3D {0, 1, 2, 3}; dev_stream__write(spi, data_out, sizeof(data_out), 1); // Read only // Assumes 0xFF is sent for each output byte in order to capture a byte char input[4] =3D {0}; if (dev_stream__read(spi, input, sizeof(input), 1) =3D=3D sizeof(input)) { // Do something with the input }
Often times, SPI devices may implement a transaction such that there are= bytes written to the bus, and then bytes can be read back, and this "trans= action" is a single chip-select data transfer.
dev_str= eam_T SPI_device =3D dev_stream__open("/dev/spi2", DEV_STREAM__MODE_INPUT_A= ND_OUTPUT); const uint32_t timeout_ms =3D 1000; // Device Stream Glue should perform the CS operation upon DEV_STREAM__CONT= ROL_ACCESS_REQUEST if (dev_stream__control(spi, DEV_STREAM__CONTROL_ACCESS_REQUEST, &timeo= ut_ms) =3D=3D 0) { const char data_out[] =3D {0, 1, 2, 3}; dev_stream__write(spi, data_out, sizeof(data_out), 1); char data_in[4] =3D {0}; dev_stream__read(spi, data_in, sizeof(data_in), 1); // Device Stream Glue should perform the DS operation upon DEV_STREAM__CO= NTROL_ACCESS_RELEASE dev_stream__control(spi, DEV_STREAM__CONTROL_ACCESS_RELEASE, NULL) }
End-user code that utilizes SPI may implement mutex to guard the SPI res=
ource from being used from multiple contexts. To implement portability with=
this caveat in mind, there is a control
type with t=
he intent of taking and giving back a mutex. The Device Stream Glue shall b=
e responsible to create the mutex if necessary.
dev_str= eam_T SPI_device =3D dev_stream__open("/dev/spi3", DEV_STREAM__MODE_INPUT_A= ND_OUTPUT); const uint32_t timeout_ms =3D 1000; // Device Stream Glue should take the mutex upon DEV_STREAM__CONTROL_ACCESS= _REQUEST if (dev_stream__control(spi, DEV_STREAM__CONTROL_ACCESS_REQUEST, &timeo= ut_ms) =3D=3D 0) { const char data_out[] =3D {0, 1, 2, 3}; dev_stream__write(spi, data_out, sizeof(data_out), 1); // Device Stream Glue should return the mutex upon DEV_STREAM__CONTROL_AC= CESS_RELEASE dev_stream__control(spi, DEV_STREAM__CONTROL_ACCESS_RELEASE, NULL) }
Typically this peripheral writes data and reads back the data onto the s= ame source. Fundamentally, this peripheral is designed such that a shift re= gister latches an output bit, and as it shifts the one bit that was output,= it reads the new bit from the other side. When 8-bits have been shifted-ou= t, 8 new bits would have been shifted-in.
The write()
function sends data out, although we =
have not yet implemented storing the received data that was input while sen=
ding the data out of the SPI peripheral. The plan of record is to add a&nbs=
p;control
type to perform something like DEV_S=
TREAM__CONTROL_SET_READ_BACK_PTR
that can be used during the ne=
xt dev_stream__write()
but since there has not been =
a use-case for this (so far), it has not taken inception yet. We are follow=
ing the Agile or YAGNI pri=
nciple here.
For a generic flash memory driver, such as on-board flash driver, the us= ual intent is to write blocks of data at a specific location. Remember that= flash memory can only be written if it was erased. With this in mind, we c= an design with the following key points:
dev_stream__open()
can not only "open" but erase the m=
emory block
If the type was DEV_STREAM__MODE_INPUT
then it ca=
n be read-only and erase is not necessary
Once the flash memory block is opened, it can be written by seeking to a= n appropriate address
Address bounds can be checked within dev_stream__write()
// The = "calibration" area should be erased if the intent is to write it (DEV_STREA= M__MODE_OUTPUT) dev_stream_T flash =3D dev_stream__open("/dev/flash/calibration", DEV_STREA= M__MODE_OUTPUT); // Seek to an address to write dev_stream__lseek(flash, 0xF0008000, DEV_STREAM__SEEK_SET); // Write data at this address const uint8_t data[512] =3D {0}; dev_stream__write(flash, data, 512, 1);
Below is an example where we write 0x02
and =
0x03
to an I2C device with address 0xF0
=
. Typically, this means that you want to write the register 0x02=
with the value of 0x03
(a typical I2C t=
ransaction).
dev_str= eam_T I2C_device =3D dev_stream__open("/dev/i2c0", DEV_STREAM__MODE_INPUT_A= ND_OUTPUT); const uint32_t timeout_ms =3D 100; if (dev_stream__control(I2C_device, DEV_STREAM__CONTROL_I2C_START, &tim= eout_ms) !=3D 0) { return; } const char write_dev_F0[] =3D {0xF0, 0x02, 0x03}; if (dev_stream__write(I2C_device, write_dev_F0, sizeof(write_dev_F0), 1) != =3D sizeof(write_dev_F0)) { // Handle error } dev_stream__control(I2C_device, DEV_STREAM__CONTROL_I2C_STOP, NULL);
The read transaction is a little tricky with the I2C bus because you hav= e to send bytes, and then receive bytes in a single transaction. The sample= code below assumes the the I2C bus has an underlining driver that can send= and receive bytes independently.
dev_str= eam_T I2C_device =3D dev_stream__open("/dev/i2c0", DEV_STREAM__MODE_INPUT_A= ND_OUTPUT); if (dev_stream__control(I2C_device, DEV_STREAM__CONTROL_I2C_START, NULL) != =3D 0) { return; } // Send 3 bytes which will send // START, byte, byte, byte const char write_dev_F0[] =3D {0xF0, 0x02, 0x03}; if (dev_stream__write(I2C_device, write_dev_F0, sizeof(write_dev_F0), 1) != =3D sizeof(write_dev_F0)) { // Handle error } // Receive 3 bytes which will send // REPEAT START, rx byte, rx byte, rx byte char bytes_to_read[3] =3D {0}; dev_stream__control(I2C_device, DEV_STREAM__CONTROL_I2C_REPEAT_START, NULL)= ; if (dev_stream__read(I2C_device, bytes_to_read, sizeof(bytes_to_read), 1) &= gt; 0) { } dev_stream__control(I2C_device, DEV_STREAM__CONTROL_I2C_STOP, NULL);
For CAN I/O, we can implement various different descriptors that map to =
the CAN mailboxes. The flags
argument for dev_stream__open()
is used to identify if the descriptor is go=
ing to be used for either reading or writing a CAN HW mailbox.
can_mes= sage_S message; // Note that DEV_STREAM__MODE_INPUT distinguishes between a read or a write= mailbox dev_stream_T CAN_receive =3D dev_stream__open("/dev/can0", DEV_STREAM__MODE= _INPUT); // By default, this API should block forever until a message is received if (dev_stream__read(CAN_receive, &message, sizeof(message), 1) =3D=3D = sizeof(message)) { // Handle received message }
CAN drivers typically implement a mask and m= essage id that can be used to setup filters to receive particular= messages. The Stream Device does not target very specific behavior for suc= h a case, and it can be implied that there is an intent to receive filtered= data only.
can_mes= sage_S message; // Intent is to receive (DEV_STREAM__MODE_INPUT) "gateway" messages only // The Device Stream Glue should setup the mailbox filters appropriately dev_stream_T CAN_receive =3D dev_stream__open("/dev/can0/gateway", DEV_STRE= AM__MODE_INPUT); // Wait until we receive a message from the gateway if (dev_stream__read(CAN_receive, &message, sizeof(message), 1) =3D=3D = sizeof(message)) { // Handle received message }
The descriptor name can identify the purpose for a CAN mailbox transmiss=
ion. For example, if Sibros performs a dev_stream__open()
=
with /dev/can0/ecu_info
then the Device Stream=
Glue can trigger on this name and map the descriptor to a dedicated transm=
ission mailbox. Therefore, the code sitting above the Sibros Device abstrac=
tion is agnostic to a particular CAN mailbox and it is up to the Device Str=
eam Glue to link the descriptor to their choice of actual hardware CAN mail=
box.
It is also up to the Device Stream Glue to ensure that the wr=
ite()
API blocks forever until the underlining driver is ready =
to write the Hardware CAN mailbox. If the user sets the timeout on the =
;CAN_transmit
descriptor, then the Device Stream Glue can=
deposit the data into an RTOS queue with the configured timeout.
can_mes= sage_S message; // Note that DEV_STREAM__MODE_OUTPUT distinguishes between a receive or a t= ransmit mailbox dev_stream_T CAN_transmit =3D dev_stream__open("/dev/can0/ecu_info", DEV_ST= REAM__MODE_OUTPUT); // By default, the write() API will block forever until a message can be wr= itten to the HW // But here we set a timeout const uint32_t timeout_ms =3D 500; dev_stream__control(CAN_transmit, DEV_STREAM__CONTROL_SET_WRITE_TIMEOUT, &a= mp;timeout_ms); if (dev_stream__write(CAN_transmit, &message, sizeof(message), 1) !=3D = sizeof(message)) { // Handle error }
The network I/O is a more complex type of stream. There needs to be a co=
nsistent method of encoding and decoding network paths and we designed a co=
de module to handle this. Basically, the following type of encoded paths al=
low you to open
a stream while identifying what type=
of stream we really desire:
/net/udp/hostname/1234
/net/tcp/server/localhost/1234
/net/tcp/client/localhost/1234
The intention of the TCP client is to connect to another peer. The Devic=
e Stream Glue can default the connection timeout to a sane value, such as 3=
seconds before giving up and returning NULL
as a re=
sponse from dev_stream__open()
.
// Conn= ect to <host> @ TCP port 1234 as a client dev_stream_T TCP_client =3D dev_stream__open("/net/tcp/client/host/1234"); if (dev_stream__is_valid(TCP_client)) { dev_stream__write(TCP_client, "hello", 5, 1); }
dev_str= eam_T TCP_client =3D dev_stream__open("/net/tcp/client/google/12000"); if (!dev_stream__is_valid(TCP_client) { return; } const uint32_t timeout_ms =3D 500; // Set timeout to receive data dev_stream__control(TCP_client, DEV_STREAM__CONTROL_SET_READ_TIMEOUT, &= timeout_ms); // Note that with a network socket, you may receive fewer bytes than reques= ted char input[4] =3D { 0 }; const size_t requested_bytes =3D sizeof(input); const int receive_byte_count =3D dev_stream__read(TCP_client, input, reques= ted_bytes, 1); if (receive_byte_count > 0) { } // Set timeout to transmit data dev_stream__control(TCP_client, DEV_STREAM__CONTROL_SET_WRITE_TIMEOUT, &= ;timeout_ms); // Note that with network sockets, you may be able to send only partial amo= unt of data const char output[] =3D {1, 2, 3, 4}; const size_t transmit_request =3D sizeof(output); const int transmit_byte_count =3D dev_stream__write(TCP_client, output, tra= nsmit_request, 1); if (transmit_byte_count !=3D transmit_request) { }
A listening socket doesn't perform read()
or =
;write()
and thus we need a special function to perform the equ=
ivalent of an accept()
function which we implement b=
y using a control()
function.
// Host= a TCP listening socket @ port 1234 dev_stream_T TCP_listen_socket =3D dev_stream__open("/net/tcp/server/1234")= ; // Use the listening socket to accept a new connection // By default, this should block until a new client has connected dev_stream_T client =3D -1; if (dev_stream__control(TCP_listen_socket, DEV_STREAM__CONTROL_TCP_ACCEPT, = &client) =3D=3D 0) { // We can use client to read() or write() but for this // example, we simply close and release this resource dev_stream__close(client); } // If we wanted to accept with a timeout, we can apply a read timeout first const uint32_t timeout_ms =3D 3000; dev_stream__control(TCP_listen_socket, DEV_STREAM__CONTROL_SET_READ_TIMEOUT= , &timeout_ms); // Check if a client accepted within the timeout if (dev_stream__control(TCP_listen_socket, DEV_STREAM__CONTROL_TCP_ACCEPT, = &client) =3D=3D 0) { dev_stream__close(client); }
The one limitation to the code below is that a UDP socket can receive da=
ta from anyone on the network (it is connection-less), and the d=
ev_stream__read()
does not identify where the data came from. F=
or the time being, there is no proposal to address this limitation, and can=
be designed upon request.
// Bind= to local port 1234 for UDP dev_stream_T UDP_socket =3D dev_stream__open("/net/udp/1234"); uint8_t input[4] =3D { 0 }; const size_t requested_bytes =3D sizeof(input); // Block until we get data from someone destined for this port const int receive_byte_count =3D dev_stream__read(UDP_socket, input, reques= ted_bytes, 1); if (receive_byte_count > 0) { }
The UDP transmission is a little tricky because a UDP socket may be used= to send data to anyone because the UDP socket is not associated with just = one peer, and this socket is just a resource that can be used to send data = to anyone.
// We s= till need a local port to bind to in case we use this socket to receive dat= a dev_stream_T UDP_socket =3D dev_stream__open("/net/udp/1234"); const uint16_t someones_port =3D 1234; dev_stream__control(UDP_socket, DEV_STREAM__CONTROL_UDP_SEND_TO_PORT, &= someones_port); const char * host =3D "google"; dev_stream__control(UDP_socket, DEV_STREAM__CONTROL_UDP_SEND_TO_HOST, host)= ; // UDP transmission doesn't require the implementation of timeouts const char output[] =3D {1, 2, 3, 4}; const size_t transmit_request =3D sizeof(output); const int transmit_byte_count =3D dev_stream__write(UDP_socket, output, tra= nsmit_request, 1); if (transmit_byte_count !=3D transmit_request) { }
You do not need to implement a lot of boiler plate code to glue your har=
dware platform to the Device Stream
. There is a control()
types that are not relevant for certain dev=
ices, and thus the implementation can be omitted.
The Device Stream Dispatcher
pro=
vides the implementation of the dev_stream.h
for you=
, and you only should care about the registered callbacks. In addition, it =
features the following:
Match the dev_stream__flags_E
during d=
ev_stream__open()
Protection against multiple dev_stream__open()
Not invoking open_fptr()
, write_fptr() etc. if they are NULL
close()
will invoke your close_fptr()
and free the descriptor for re-use
dev_stream__open()
takes care of matching the descript=
or name and supports
Partial matching to allow for socket API behavior using IP lib (LWIP)
Note that each dispatcher you register, such as CAN
&nb=
sp;or LWIP
can have their own descriptor IDs and the=
y may even overlap! The dispatcher will take care of mapping the right IDs =
to each of the registered dispatchers. This is why the d0
=
and d1
can both exist at different dispatchers=
.
Here is the sample code that registers your CAN
a=
nd SPI
Device stream handlers. Contact us to get the=
implementation of the dev_stream_dispatcher.h
// Defi= ne your CAN open dev_stream_T can__open(const char *path) { return 0; } // Define your SPI open dev_stream_T spi__open(const char *path) { return 0; } // Define your way of sending a CAN message int can__read(dev_stream_T device, void *input_buffer, size_t bytes_per_ele= ment, size_t num_elements) { // ... return (bytes_per_element * num_elements); } // Define your way of writing SPI int spi_write(dev_stream_T device, const void *input_buffer, size_t bytes_p= er_element, size_t num_elements) { // ... return (bytes_per_element * num_elements); } // Register the 'dispatchers' // The 'dev_stream_dispatcher.h` implements the `dev_stream.h` API dev_stream__dispatcher_S dispatchers[] =3D { [0] =3D { .device_name_for_open_operation =3D "/dev/can0", .flag_to_validate_during_open_operation =3D DEV_STREAM__FLAGS_INPUT= , .open_fptr =3D can__open, .read_fptr =3D can__read, }, [1] =3D { .device_name_for_open_operation =3D "/dev/spi1", .flag_to_validate_during_open_operation =3D DEV_STREAM__FLAGS_OUTPU= T, .open_fptr =3D spi__open, .write_fptr =3D spi__write, }, // Set aside 2 descriptors for networking // We only match the 'beginning' part of the name and let the // net__open() handle the logic to check the rest of the string [2] =3D { .device_name_for_open_operation =3D "/net/", .flag_to_validate_during_open_operation =3D DEV_STREAM__FLAGS_INPUT= _AND_OUTPUT, .device_name_matching_scheme =3D dev_stream__string_match_beginning= , .open_fptr =3D net__open, .read_fptr =3D net__read, .write_fptr =3D net__write, .close_fptr =3D net__close, }, [3] =3D { .device_name_for_open_operation =3D "/net/", .flag_to_validate_during_open_operation =3D DEV_STREAM__FLAGS_INPUT= _AND_OUTPUT, .device_name_matching_scheme =3D dev_stream__string_match_beginning= , .open_fptr =3D net__open, .read_fptr =3D net__read, .write_fptr =3D net__write, .close_fptr =3D net__close, }, }; dev_stream__dispatcher_register(dispatchers, ARRAY_SIZE(dispatchers));
Refer to this link.