Getting Started with CANlib

Download and install Kvaser CANlib SDK and Driver

With the driver and SDK installed, you can get started right away. Virtual channels installed with the driver lets you test your code without yet having an actual Kvaser interface.

Download page

Use the samples

In the samples folder of the SDK installation, you will find a number of samples to get you started.

For the beginner, a good choice to get started is the C# tutorial. It covers everything you need to get started. They are installed in the \Samples\Getting Started folder where you installed CANlib. There are tutorials for other languages, but the C# is the most comprehensive.

Read the CANlib tutorial Here

Developer blog

When you need further information on topics, please take a look at our developer blog.

There you will find in-depth articles on a number of subjects, to help you take your software to the next level.

Before you start

Here we will go through some things that might be good to know before you start programming with Canlib.

Importing Canlib

In order to program using Canlib, you will need to add a reference to the library. To do this in Visual Studio, follow these steps:

  1. Right click on your project in the Solution Explorer and select “Add reference”. In some versions of Visual Studio, you need to right click the References folder under your project instead.
  2. Right click on your project and select “Properties” (Hotkey: Alt+Enter). Then select Build. Under “Configurations”, select “All Configurations” and under “Target platform” select either “x86” or “x64”.
  3. Press “Browse” and browse to the correct version of the CanlibCLSNET.dll. It will be in the “Canlib\dotnet” directory, but which file to use depends on your target platform and .NET version. If you’re building your project for 32 bit processors, select the x86 directory. If you’re building for 64 bit processors, select x64. Next, you need to pick the correct dll for your version of the .NET Framework. If you use a version older than 4.0, select fw11 (x86 only), otherwise select fw40. In this directory, you will find the .dll you need.
  4. You should now be able to write a program by importing the CanlibCSLNET namespace. (using CanlibCSLNET)

CanKing

Kvaser CanKing is a CAN bus monitor which you can use to test your programs. You can find it here.

CanKing can be used for both sending and receiving messages to and from your own programs.

Finding more information

For detailed information about the Canlib API, please see the help file . If you want to know more about a method or some other part of the API while programming in Visual Studio, you can right click on it in your code and select “Go To Definition”.

Part 1: Hello world!

In this part, we will demonstrate how to open a channel and send a message on it.

Step 1: Preliminaries

Make sure you have added the correct version of canlibCLSNET.dll as a reference in your project. In your the class file for the project, import the canlibCLSNET namespace:

using canlibCLSNET;

Step 2: Initializing Canlib and setting up a channel

The first thing we need to do is to initialize the Canlib library with a call to Canlib.initializeLibrary(). This always needs to be done before doing anything with the library.

Next, we open up channel 0 and receive a handle to it. A handle is just an integer which is associated with a circuit on a CAN device. Depending on which type of devices you have, you might want to change the channel number (the first parameter in the call). The canOPEN_ACCEPT_VIRTUAL flag means that channel 0 can be on a virtual device.

If the call to Canlib.canOpenChannel is successful, it will return an integer which is greater than or equal to zero. However, is something goes wrong, it will return an error status which is a negative number. To check for errors and print any possible error message, we can use the DisplayError method. This method takes a Canlib.canStatus (which is an enumerable) and the method name as an argument. If the status is an error code, it will print the corresponding error message. Most Canlib method return a status, and checking it with a method like this is a useful practice.

Once we have successfully opened a channel, we need to set its bitrate. We do this using canSetBusParams, which takes the handle and the desired bitrate (another enumerable) as parameters. The rest of the parameters are set to 0 in this example.

Next, take the channel on bus using the canBusOn method. This needs to be done before we can send a message.

Step 3: Send a message

In the beginning of the method we declared a byte array called “message”. This will be the body of the message we send to the channel. We send the message using the canWrite. This method takes five parameters: the channel handle, the message identifier, the message body, the message length (in bytes) and optional flags. After this, we wait for at most 100 ms for the message to be send, using canWriteSync.

Step 4: Go off bus and close the channel

Once we are done using the channel, we go off bus and close it using the canBusOff and canCloseChannel methods, respectively. The both take the handle as their only argument.

Exercises

  • The canWriteWait method combines canWrite with canWriteSync. Try it out.
  • Use some other program (such as Kvaser CanKing) to listen for messages on a channel connected to the one used in the program. Make sure to use the same bitrate.
  • Change the fourth parameter in the call to canWrite to 4. What happens to the message on the receiving side?
  • Change the message identifier to something large, like 10000. What happens on the receiving side? Then, change the fifth parameter to Canlib.canMSG_EXT. What happens now?

Part 2: Receiving messages

In the previous part, we demonstrated how to construct a program which sends a message on a CAN channel. In this part, we will show how to read messages from the channel.

Step 1: Setup

Like in the previous example, we need to initialize the library, open a channel and go on bus. We’ve also reused the CheckStatus method.

Step 2: Waiting for messages

In the DumpMessageLoop method, we first declare some variables for holding the incoming messages. The incoming messages consist of the same parameters as the outgoing ones we saw in the previous part (identifier, body, length and flags), as well as a timestamp.

Next, we start a loop where we call the canReadWait method to wait for a message on the channel. This method has a timeout parameter which in this case is set to 100 ms. If a message is received during this time, it will return the status code canOK and the message will be written to the output parameters. If no message is received, it will return canERR_NOMSG.

We might receive more than one message during the timeout period. In that case, we need to empty the queue and print all the messages. This is done in the inner while loop. Here, we check that the returned status is OK (which means that a message has been received), and prints the result if it is. If the message contains an error flag (which implies a different kind of error than if an error signal had been returned), an error message will be shown. If not, the DumpMessage method prints the message. We then call the canRead method to get the next message in the queue. This method works just like canReadWait, but returns canERR_NOMSG immediately if there is no message on the channel.

Step 3: Exiting the program

The last thing we do in the loop is to print any error status except for canERR_NOMSG and exit the program if one appears, since they would imply that an error in reading the messages has occurred. We also check if the user has pressed the Escape key during the last loop, in which case we also exit.

When we’re done reading messages, we go off bus and close the channel, as always.

Exercises

  • Start this program, then run the Hello World program from the previous example. Make sure to modify one of the programs so they use different channels on the same device.
  • Send an error message to your program using the canMSG_ERROR_FRAME flag.
  • The canReadSync method waits until there is a message on the channel, but doesn’t read the message. Rewrite the program to use this method instead of canReadWait.

Part 3: canIoCtl

This part of the tutorial is intended to demonstrate the capabilities of the canIoCtl method, and to give you some methods you can use in your own projects. canIoCtl can perform several different operations, depending on which parameters is passed to it. For the sake of conveniance, we have wrapped each of the different functions into its own function/method, with proper parameters and return types. We have also added some exception throws for cases where the wrong input parameters are given, or when the call to canIoCtl returns an error.

How canIoCtl works

In C, the signature of canIoCtl is:

canStatus canIoCtl ( const int hnd, unsigned int func, void * buf, unsigned int buflen )

It takes four parameters: a channel handle which it acts on, an integer which decides which function to perform (each function has its own constant), a pointer to a buffer and the length of the buffer. The buffer holds any input to the function, and if the function returns an output, the buffer will be overwritten with that value. It returns a canStatus which has the value canOK if no error occurred, and an error otherwise.

In C#, the method has four different signatures:

static Canlib.canStatus canIoCtl(int handle, int func, int val);
for functions that take an integer as argument and return nothing.
static Canlib.canStatus canIoCtl(int handle, int func, out int val);
for functions that might take an integer as a value and return an integer.
static Canlib.canStatus canIoCtl(int handle, int func, out string str_buf);
for functions that return a string.
static Canlib.canStatus canIoCtl(int handle, int func, ref object obj_buf);
for functions that act on an object.

The library and the sample program

The library

In order to make working with canIoCtl a bit easier, we have included a library with methods which use the various functions of canIoCtl. These methods are all static and always take the handle as their first argument. If thecanIoCtl function requires an additional parameter, it is the second argument of the function. Instead of output parameters, the methods return any result of the call. Any error messages from canIoCtl result in exceptions of the class canIoCtlException, which is defined in the same file as the library.

To use these methods, you can either import the entire namespace or just copy the parts you need to your own project. If you choose to copy a method, you will need to include the canIoCtlException class as well as theCheckForException method, or modify the method to handle errors in some other way.

Helper and utility methods

The program includes some helper methods. They are mostly for testing purposes, but you might find some of them useful.

private static void CheckStatus(Canlib.canStatus status, string method)
If the status is an error code, this method will display an error message stating in which method the error occurred.
private static void DumpMessage(int hnd, int id, byte[] data, int dlc, int flags, long time)
Displays a message to the console, including the receiving handle.
private static string BusTypeToString(int type)
Returns the string representation of a bus type.
private static void CheckForException(Canlib.canStatus status)
Throws a CanIoException if the status is an error code, with the error text of the code.

The test program

In the Main method of the program, we demonstrate how our methods can be used. The program does the following things:

  1. Set up handles for testing. In this sample, we assume that channels 0 and 1 are connected. We also assume that channel 2 is on a remote device. You might need to change this depending on your setup.
  2. Show how to use prefer EXT and prefer STD to enable automatic setting of the EXT flag.
  3. Show how to read and flush the error counters.
  4. Changing the timer scale.
  5. Demonstrating the Bus On Timer Reset setting.
  6. Using Transmit Acknowledgements.
  7. Showing how to read the RX queue size, how to flush the queue and how to set a limit on it.
  8. Enabling Transmit requests.
  9. Getting and setting IO port data (commented out by default as it might generate an exception if it is used on an unsupported device).
  10. Turning on Transmit echo.
  11. Setting a minimum transmit interval.
  12. Turning off error frame reporting.
  13. Testing channel quality, getting RTT, devname and bus type from a remote device, as well as reading the time since the last communication with the device.
  14. Changing and reading the throttle value of the channel.
  15. Wait for the user to press a key and exit.

canIoCtl functions

canIOCTL_PREFER_EXT and canIOCTL_PREFER_STD

These functions determine whether or not the EXT or STD flags should be enabled by default when messages are written to the channel. If a message has an extended identifier but no EXT flag, the most significant bits of the identifier will be cut off.

In this library, we have wrapped these functions in PreferExt and PreferStd, respectively. They only take the handle as an argument. An example of their usage can be found in the Main method, where we try to send a message with an identifier which is too large to be properly sent without the EXT flag.

canIOCTL_CLEAR_ERROR_COUNTERS

Each channel handle logs how many errors have occurred. These are divided into three types: transmit errors, receive errors and overrun errors. The error counters can be found by using the canReadErrorCounters method. By calling canIOCTL_CLEAR_ERROR_COUNTERS, these counters are reset to zero.

We have wrapped this function in the ClearErrorCounter method. It takes the channel handle as its parameter and does not return anything. In the Main method, we create a transmit error on channel 0 by closing channel 1 and trying to write a message from channel 0. We then clear the error counter.

canIOCTL_SET_TIMER_SCALE and canIOCTL_GET_TIMER_SCALE

The timer scale determines how precisely the channel’s timestamps will be displayed. Note that it does not change the accuracy of the clock.

These functions are wrapped as SetTimerScale, which takes the handle and the resolution (in microseconds) as parameters, and GetTimerScale. We test them by setting the resolution very low (so the clock only updates every 100 ms) and sending messages 50 ms apart, thus giving several messages the same timestamp.

canIO_SET_TXACK and canIOCTL_GET_TXACK

Enabling transmit ACKs on a channel results in that channel receiving a message with the TXACK flag enabled every time a message is successfully transmitted. The ACKs are enabled by setting this value to 1 and disabled by setting it to 0 (which is the default value). A third value, 2, also exists. With this setting, transmit ACKs are disabled even for internal use. Any functions which depend on them will stop working properly.

In the library, we have added the functions SetTXACK and GetTXACK for this purpose.

canIOCTL_GET_RX_BUFFER_LEVEL and canIOCTL_GET_TX_BUFFER_LEVEL

Returns the number of messages waiting to be received or transmitted, respectively. These functions can be reached using GetRXQueueLevel and GetTXQueueLevel, which both take the handle as their parameters.

canIOCTL_FLUSH_RX_BUFFER and canIOCTL_FLUSH_TX_BUFFER

Empties the buffers and removes any messages waiting to be received/transmitted. You can use FlushRXBuffer or FlushTXBuffer to call these functions. They are also demonstrated in Main, where we send a message to a channel,check the size of the RX buffer and then flush the buffer, removing the message.

canIOCTL_SET_TXRQ

Turns transmit requests on or off. If transmit requests are enabled on a channel, the channel will receive a message any time it writes a message to the channel. This method can be called using SetTransmitRequest, which takes the handle and a boolean as parameters, where the boolean parameter’s value determines if transmit requests should be enabled or not.

canIOCTL_GET_EVENTHANDLE

This method returns an event handle, which can be used to make certain things happen when an event occurs on the bus. It is further explained in section 5. It is wrapped by GetEventHandle, but not demonstrated in the program.

canIOCTL_GET_USER_IOPORT and canIOCTL_SET_USER_IOPORT

This returns (or sets) a canUserIoPortData object, which contains information about the port ID and value. They only work on supported devices. You can use the SetUserIoPortData and GetUserIoPortData methods to call them.

canIOCTL_SET_RX_QUEUE_SIZE

Determines the maximum size of the RX buffer. Setting this to a too high value will consume nonpaged pool memory. You can call it by using SetRXQueueSize, which takes the handle and the new size of the buffer as arguments. In the Main method, we demonstrate this function by decreasing the buffer size to 5.

canIOCTL_SET_BUSON_TIME_AUTO_RESET

This function can enable or disable automatic time reset on bus on. By default, this is enabled, so the timer will automatically reset when a handle goes on bus. You can use the SetClockResetAtBusOn function for this. It takes a handle and a boolean as parameters, where the boolean value determines whether or not the clock should reset.

canIOCTL_SET_LOCAL_TXECHO

Turns local transmit echo on or off. If local transmit echo is on (which it is by default) and one handle on a channel transmits a message, any other handle on the same channel will receive it as a normal message. Use the function SetLocalTXEcho to turn this feature on or off. Like the previous method, it takes a handle and a boolean parameter.

canIOCTL_SET_ERROR_FRAMES_REPORTING

This function turns error frame reporting on or off. If it is off, the channel handle will ignore any error frames it receives. The SetErrorFramesReporting method wraps this function.

canIOCTL_GET_CHANNEL_QUALITY

Returns the quality of the channel, between 0 and 100%. It is wrapped by GetChannelQuality.

canIOCTL_GET_ROUNDTRIP_TIME

Returns the round trip time to a device. Wrapped by GetRoundTripTime.

canIOCTL_GET_BUS_TYPE

Returns the bus type of the channel handle. Can be either Internal, Remote, Virtual or Local. The return type is an integer and Canlib contains constants for each type. It is wrapped by GetBusType and you can use the helper function BusTypeToString to get the result as a string.

canIOCTL_GET_DEVNAME_ASCII

This function returns the device name as a string. It is not supported by all devices and will return an error if called on a device which does not support it. In the library, it is wrapped by the GetDevNameASCII function.

canIOCTL_GET_TIME_SINCE_LAST_SEEN

Returns the time (in ms) since the device on the provided handle was last seen. Mostly useful for remote devices. Wrapped by GetTimeSinceLastSeen.

canIOCTL_TX_INTERVAL

This function returns or sets the transmission interval of the handle in microseconds. The handle will wait for at least a full interval between message transmissions. If the interval is set to zero, the handle will transmit as quickly as possible. By inputting -1 as the third parameter in the canIoCtl call, it will overwrite the parameter with the current interval.

In the library, this function is wrapped by GetTXInterval (which does not take any additional parameter) for returning the current inteval, and SetTXInterval for setting the interval. SetTXInterval will throw an exception if the interval is negative or higher than one second.

canIOCTL_SET_USB_THROTTLE_SCALED and canIOCTL_GET_USB_THROTTLE_SCALED

Used for setting or returning the throttle value of the device. A device with low throttle will be very responsive and a device with high throttle will require less system resources. The throttle value will always be between 0 and 100. Some devices do not support setting a throttle value and might ignore it even if they do not return an error.

These functions are wrapped by SetThrottleScaled and GetThrottleScaled, respectively.

Making a graphical application

 

In this series, we will demonstrate how to create a simple program that can read an write messages to a channel. For the GUI, we will use the Windows Presentation Foundation system.

Preliminaries

As usual, you need to have a reference in your project to the correct canlibCLSNET.dll. You also need to import the library in your class files.

The GUI

Constructing a GUI in Visual Studio is rather simple. We first create a WPF project and then drag and drop the different components we want onto the window. In our example, the result looks something like this:

Mainframe

As seen in the picture, there are buttons for each of the basic Canlib functions needed for setting up a channel and writing messages for it. Each of these buttons has a function which calls its respective Canlib functions. There is also a text box where all the messages to the handle are displayed. This is activated as soon as we go on bus.

The program doesn’t do much error handling, but after each action, the status bar at the bottom is updated with information about which action was taken and if it succeeded.

The XAML

XAML (Extensible Application Markup Language) is a markup language used in WPF to define the graphical components of the GUI. When we used the drag and drop features in Visual Studio, some of it is created, but it’s not enough to make a very exciting program. To make the program useful, we add some properties to the elements to make stuff happen:

  1. The Click property of a button specifies a function which is called whenever the button is clicked.
  2. The PreviewTextInput property is used in this example to call a function which prevents the user from entering non-integer values into fields which should only take integers.
  3. The Tag property can be sued to attach some generic object to an element. In this program, we attached number to each of the flag checkboxes to make calculating the flag values of the messages a bit easier.

The back-end

As previously mentioned, each button calls a certain method which reads any possible parameters from the text fields and in turn calls a Canlib function. If you’ve read the first part of this tutorial, you’re already familiar with them.

When the Bus On button is clicked and we put the channel on bus, we also start a BackgroundWorker which was created in the initialization method. This BackgroundWorker will run the DumpMessageLoop method, which is similar to the one used in Part 2 of this tutorial. It loops as long as the handle is on bus and no error occur, and prints any messages received on the channel. One difference is that this loop does not print to the console but rather calls the BackgroundWorker‘s ReportProgress method which passes the value on to the ProcessMessage method. The reason for using BackgroundWorker like instead of just spawning a new thread which runs DumpMessageLoop is that such a thread would not be able to access the GUI components and thus would be unable to update the output field.

One important thing to notice is that we do not use the same handle in DumpMessageLoop as we do in our main thread. This is because using the same handle in different threads at the same time can cause unexpected errors. Instead, we create a new one. Just like the original handle, we need to call BusOn and BusOff with this one.

One thing that hasn’t been explained much previously in the tutorial is the message flag. The message flag is a field in the message which contains information about what kind of message is being sent. Each flag has a constant defined in Canlib. To get the flag value, the set flags’ contants are added together. Each flag constant is a power of two, so the result of adding a certain set of flags is always unique. To find out if a flag is set, use the bitwise AND operator (&) on the message’s flag and the flag constant. The result will be either 0 or the constant, depending on whether the flag is set or not. Some of the more important flags include:

canMSG_RTR
Means that the message is a remote request.
canMSG_EXT
The message has an extended identifier (29 bits instead of 11).
canMSG_ERR
The message is an error frame. When an error frame is received, the other fields in the message are usually garbage. For this reason, we don’t display these fields in our program when we receive an error frame.

Using events

This part is a continuation of the previous part, where we created a graphical application that could send and receive messages. That program used canReadWait to wait for messages. In this program we will use event handles instead.

About events

Canlib can create Win32 event handles, which are signaled whenever something happens on the channel. These event handles can then be used to wait for an event to occur. These events include received messages, transmit events, error frames and status events.

Creating event handles

In this program, we’ve added the class CanlibWaitEvent, which extends the WaitHandle class. This class contains a SafeWaitHandle object, which in turn wraps the event handle which we receive from Canlib.

The event handle itself is created by a call to canIoCtl with the canIOCTL_GET_EVENTHANDLE function code. This call gives a pointer to the event handle, which is in turn sent to the SafeWaitHandle constructor.

Using event handles

In our application, we declare an instance variable of the WaitHandle class. This object is created when we go on bus. We still have the BackgroundWorker which is used to run the DumpMessageLoop method. In DumpMessageLoop, we add a call to the WaitOne method of our event handle. This call will block until an event occurs (in which case it returns true) or until a timeout is reached (which means it returns false). The parameter in the call is how long the call should wait for an event, measured in milliseconds. In this case it is set to 100 ms. A value of -1 means that it will wait indefinitely. Once WaitAny returns true, we read all the messages in the receive buffer, like in the previous example.

More information

To learn more about event handles, take a look at the documentation for the WaitHandle and SafeWaitHandle classes.

Part 6: Databases

This section introduces CAN databases and shows how you can use them in your own applications. The sample program loads a database file and then interprets any incoming messages according to the specification in the database.

Step 0: Understanding CAN databases

A CAN database contains information about messages. Each message has (among other attributes) an identifier, a name and one or several signals. When a CAN message is received, we want to look up the message in the database that has the corresponding identifier. The signals’ attributes contain information about how the CAN message data will be interpreted. These attributes include its name, start bit, length, offset, scaling factor, min/max values and its unit. The start bit and length determine which bits of the CAN message data that contain this signal’s value. The offset and scaling factor determine how the value should be interpreted.

For example, consider a message with the id 123, a DLC of 2 and the following signals:

Name Start bit Length Offset Scaling factor Min Max Unit Comment
“Temperature” 0 8 -50 0.5 -50 78 “C” “Temperature as measured in degrees Celsius”
“Pressure” 8 8 800 1 800 1055 “hPa” “Air pressure”

If we receive a message with id 123, DLC 2 and data {141, 211}, we can use our database to interpret this as the temperature being 20.5 C and the air pressure being 1022 hPa.

To create or modify databases, you can use the Kvaser Database Editor, which can be found in the Downloads section on our web page.

Step 1: Setup

To run this program, you need to add a reference to kvadblibCLSNET.dll in your project. You also need kvadlib.dll in your path.

Much of the code in this sample is reused from Part 2. Like in that example, we have a method which loops and receives messages, then sends the data to another method which prints them.

Step 2: Accessing the database

To access the database, we need to create a handle for it. We do this using the Open and ReadFile methods in Kvadblib. This handle is then used for retreiving information from the database.

Step 3: Reading messages

The DumpMessageLoop is the method that differs the most from Part 2. When we receive a message on the bus, we use our database handle to look for a message with the correct id and receive a MessageHnd using the GetMsgByIdmethod. If the message has an extended identifier, its most significant bit will be set to 1 in the database. To match the database message’s identifier with the CAN message’s identifier, we flip this bit if the flag is set.

We read the message’s name and print it, then go through the message’s signals using GetFirstSignal and GetNextSignal to create SignalHnd objects. Just like the Canlib methods for reading messages of a channel, these methods return an error status if there is no signal, so we use a while loop like the one we use when reading the messages in the RX queue.

For each signal, we read its name and unit and use the GetSignalValueFloat method to convert the data from the message into a physical value.

Step 4: Running the program

By default, the program will use channel 0 for listening and the supplied .dlc file for looking up messages. If you want to use a different database file or channel, you can supply them as arguments in the command line.

Run the program and send a message with an id that matches one of the messages in the database. The program will output the physical value of the signals in the message.

Part 7: A graphical application with databases

In this example, we create a program which can listen for messages matching a specific id and plot the input values in a graph.

Setup

In order to run the program, you need to have the WPF Toolkit installed. It can be downloaded for free from here. WPF doesn’t provide any chart feature by default, which is why this extension is needed.

Using the program

  1. Press “Load Database” and select a database file.
  2. Select a message in the drop-down menu and press “Load”
  3. Enter a channel number and press “Initialize”.
  4. Press “Start logging”
  5. Any incoming messages whose identifier matches the selected one will be interpreted as signal values and displayed in the chart.

Understanding the program

This application is rather similar to the ones in part 4 and 6. Like the application in part 4, it uses a BackgroundWorker to run a loop which listens for incoming messages, and just like in part 6 we use GetSignalValueFloat to get the physical values of the signals.

For each signal, we put the physical values along with the timestamp into an ObservableCollection of KeyValuePairs. Each collection is bound to one line in the chart, and each KeyValuePair represents a data point. Whenever a new point is added, the binding makes sure that the line is updated.

Part 8: Sending messages with databases

The previous two examples showed how to receive messages with signal values and extract these values using a database. In this example, we will use a database to create messages from signal values.

Using the program

Load the database file, select a message and initiate the channel like in the previous example. Enter some (valid) values for the different signals and press “Send message”. You can also use the “Start auto transmit” button to transmit messages automatically. If the “Randomize” button is checked, the values of the signals will change every time a message is sent. Feel free to run this program in parallel with the program from part 7.

Understanding the program

Reading the message and the signals works exactly like in the previous example. The main difference here is that we read the signal values and use them to construct a message. The signals with their values are stored in a global list, and the values are updated every time the content in one of the textboxes changes. When we want to construct a message, we create a byte array and call StoreSignalValuePhys with each signal handle, the array and the signal’s physical value as arguments. This updates the byte array with the signal’s value. We then transmit the message with the selected message’s identifer to the channel.

Try running this program in parallel with the one in Part 6 or 7 for a complete demonstration off how to transmit physical values over CAN interfaces.

Debug on...