Tutorial Overview
- Preliminaries
- 1. Hello world!
- 2. Receiving messages
- 3. canIoCtl
- 4. Creating a GUI
- 5. Events
- 6. Databases
- 7. More databases and GUI
- 8. Working with signals
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:
- 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.
- 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”.
- Press “Browse” and browse to the correct version of the CanlibCLSNET.dll. It will be in the “Canlibdotnet” 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.
- 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 canIoCt
l 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:
- 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.
- Show how to use prefer EXT and prefer STD to enable automatic setting of the EXT flag.
- Show how to read and flush the error counters.
- Changing the timer scale.
- Demonstrating the Bus On Timer Reset setting.
- Using Transmit Acknowledgements.
- Showing how to read the RX queue size, how to flush the queue and how to set a limit on it.
- Enabling Transmit requests.
- Getting and setting IO port data (commented out by default as it might generate an exception if it is used on an unsupported device).
- Turning on Transmit echo.
- Setting a minimum transmit interval.
- Turning off error frame reporting.
- 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.
- Changing and reading the throttle value of the channel.
- 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 PreferEx
t 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:
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:
- TheÂ
Click
 property of a button specifies a function which is called whenever the button is clicked. - 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. - 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 GetMsgById
method. 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
- Press “Load Database” and select a database file.
- Select a message in the drop-down menu and press “Load”
- Enter a channel number and press “Initialize”.
- Press “Start logging”
- 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 KeyValuePair
s. 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.