CAN Devices on Other Platforms
This is a guide on how to use our CAN devices on other platforms like PLCs and Raspberry Pi based control platforms.
Overview
Baud rate: 1 Mbps
Device data transmission interval: 10 ms (100 Hz)
This guide and the use of our sensors on other platforms is currently a beta project. Use at your own risk and feedback any issues or bugs to [email protected].
Wiring & Connections
We use a 5-pin connector on all our CAN devices. For most devices, the 4th pin is not in use.
The devices require a stable 24V DC power supply.
Wiring of each CAN line should be done with a single long line and short stubs to each sensor. Use a 120 resistor shortly after the last device.
PIN 1 (red): +24V
PIN 2 (yellow): CAN H
PIN 3 (white): CAN L
PIN 4 (green): Not Connected
PIN 5 (black): GroundSetup Process
Our design support 8 of the same device type per CAN bus (node ID 0-7). All devices are shipped with node ID 0, so in order to have multiple devices on a single CAN bus you need to assign each one a unique node ID (described in detail in Common Features). The easiest way to do this, is to connect the first device, give it node ID 1, then connect the next one - give it node ID 2 and so on until you have reached 7, and then the last one can keep node ID 0. If you have 8 density sensors, the result should look like this:
Density sensor @ node id 0
Density sensor @ node id 1
Density sensor @ node id 2
Density sensor @ node id 3
Density sensor @ node id 4
Density sensor @ node id 5
Density sensor @ node id 6
Density sensor @ node id 7
The same procedure goes for other devices.
CAN ID and Message Design
The CAN messages use the extended CAN ID format of 29 bits. It contains information about the message priority, sender node type, receiver node type, secondary node id and message type:
priority(Priority: 2 bits - 4 values) = 2/29 bitssenderNodeType(Sender Node Type: 8 bits - 256 values) = 10/29 bitsreceiverNodeType(Receiver Node Type: 8 bits - 256 values) = 18/29 bitssecondaryNodeId(Secondary Node Identifier: 3 bits - 8 values) = 21/29 bitsmsgType(Message Type: 8 bits - 256 values) = 29/29 bits
To extract these fields from your received CAN message, use the following bit shifting scheme:
/* Extract fields from CAN ID */
priority = (message.identifier >> 27) & 0x03;
senderNodeType = (message.identifier >> 19) & 0xFF;
receiverNodeType = (message.identifier >> 11) & 0xFF;
secondaryNodeId = (message.identifier >> 8) & 0x07;
msgType = message.identifier & 0xFF;To create a new CAN ID, the following function can be used:
static inline uint32_t getCANid(uint8_t priority, uint8_t senderNodeType,
uint8_t receiverNodeType, uint8_t secondaryNodeId, uint8_t msgType) {
uint32_t canId = (static_cast<uint32_t>(priority) << 27) |
(static_cast<uint32_t>(senderNodeType) << 19) |
(static_cast<uint32_t>(receiverNodeType) << 11) |
(static_cast<uint32_t>(secondaryNodeId) << 8) |
(static_cast<uint32_t>(msgType));
return canId;
}The first byte in the the CAN message is a sub-index, which is used to distinguish between values of the same type, e.g. temperature reading 1, 2 and 3, all using the same message type. The way to extract the data correctly is shown below.
/* Extract sub-index (first byte of data) */
subIndex = message.data[0];
/* Copy raw data payload (excluding sub-index) to the provided buffer */
dataLength = message.length - 1; // Data length without sub-index
memcpy(buffer, &message.data[1], dataLength);So let's say we want to process an incoming CAN message containing a density value from a density sensor, we could do something like:
#define MAX_NODES 8
float densitySg[MAX_NODES] = {0}; // Storage for each node's density value
if (can.receive(priority, senderNodeType, receiverNodeType, secondaryNodeId, msgType, subIndex, data, dataLength))
{
switch (senderNodeType)
{
case NODE_TYPE_DENSITY_SENSOR:
{
switch (msgType)
{
case MSG_TYPE_DENSITY:
{
/* Extract sub-index (first byte of data) */
subIndex = message.data[0]; // Line not necessary, just here for clarity
/* Copy the next "size of a float" bytes to your variable */
if (secondaryNodeId < MAX_NODES)
{
/* Copy incoming float into the correct array index for this node */
memcpy(&densitySg[secondaryNodeId], &message.data[1], sizeof(float));
}
}
}
}
}
}
Common Features
These features are common for all CAN devices, and can be implemented as general functions in your application.
Update Sensor Node ID
The firmware on our devices support up to 8 of the same node type on the same bus.
Pseudo-code for implementing a function that sends a new node ID to a density sensor is shown below:
bool updateDensitySensorId(uint32_t currentNodeId /* [0-7] */, uint32_t newNodeId /* [0-7] */)
{
int subIndex = 0; /* Unused for this purpose, set to 0 */
can_message_t message;
message.identifier = getCANid(PRIORITY_MEDIUM,
NODE_TYPE_PLC,
NODE_TYPE_DENSITY_SENSOR,
currentNodeId,
MSG_TYPE_NODE_ID);
message.extd = 1;
message.length = dataLength;
uint8_t data[5];
data[0] = subIndex;
data[1] = (newNodeId >> 24) & 0xFF;
data[2] = (newNodeId >> 16) & 0xFF;
data[3] = (newNodeId >> 8) & 0xFF;
data[4] = newNodeId & 0xFF;
memcpy(message.data, data, dataLength);
return(can.transmit(message));
}Out of the box, your density sensor will have node ID 0. So to update it to 1, you would do updateDensitySensorId(0, 1); . To set it back to factory default (and make it compatible with FCS again) you would do updateDensitySensorId(1, 0); . Similarly, you can do this for other devices by changing the NODE_TYPE_DENSITY_SENSOR to your desired node type.
Sending Data to a Device
A general function for sending bytes of data to your devices are shown below.
bool sendCanData(uint8_t priority, uint8_t senderNodeType,
uint8_t receiverNodeType, uint8_t secondaryNodeId,
uint8_t msgType, uint8_t *data, size_t dataLength) {
const uint32_t canId = (uint32_t(priority) << 27)
| (uint32_t(senderNodeType) << 19)
| (uint32_t(receiverNodeType) << 11)
| (uint32_t(secondaryNodeId) << 8)
| uint32_t(msgType);
can_message_t message{};
message.identifier = canId;
message.length = (dataLength > 8) ? 8 : dataLength;
std::memcpy(message.data, data, message.length);
return(can.transmit(message));
}Using this function, you can create a specific function for sending specific data types, as shown below:
bool sendFloatCan(uint8_t priority, uint8_t senderNodeType, uint8_t receiverNodeType,
uint8_t secondaryNodeId, uint8_t msgType, float value) {
uint8_t data[5]; // Adjusted to 5 bytes to include sub-index
data[0] = 0; // Set the sub-index as the first byte of the data array
memcpy(&data[1], &value, sizeof(float)); // Copy float to the next 4 bytes
return sendCanData(priority, senderNodeType, receiverNodeType, secondaryNodeId,
msgType, data, sizeof(data));
}bool sendUintCan(uint8_t priority, uint8_t senderNodeType, uint8_t receiverNodeType,
uint8_t secondaryNodeId, uint8_t msgType, uint32_t value) {
uint8_t payload[5];
payload[0] = 0; // subIndex
payload[1] = uint8_t((value >> 24) & 0xFF);
payload[2] = uint8_t((value >> 16) & 0xFF);
payload[3] = uint8_t((value >> 8) & 0xFF);
payload[4] = uint8_t(value & 0xFF);
return sendCanData(priority, senderNodeType, receiverNodeType, secondaryNodeId,
msgType, payload, sizeof(payload));
}Device Specific Configuration and Data
Density / Temperature Sensor
The density/temperature sensor sends density on request (not at 10 ms interval) and constantly transmits temperature data. To start a new measurement, send a CAN message with the message type MSG_TYPE_START_MEASUREMENT , the data in the message can be left empty.
Calibration
To calibrate your density sensor, send the correct value from your EasyDense (or similar) as a float with the message type MSG_TYPE_CALIBRATION_CMD using the SG unit (important!). You can then listen for the MSG_TYPE_CALIBRATION_ACK message with acknowledgement types described below.
Node type: NODE_TYPE_DENSITY_SENSOR = 4
Message types
To device
MSG_TYPE_NODE_ID = 36MSG_TYPE_START_MEASUREMENT_CMD = 33MSG_TYPE_CALIBRATION_CMD = 28
From device
MSG_TYPE_DENSITY = 14MSG_TYPE_TEMPERATURE = 12MSG_TYPE_CALIBRATION_ACK = 29
Acknowledgement types
ACK_TYPE_NONE = 0ACK_TYPE_CALIBRATING = 1ACK_TYPE_OK = 2ACK_TYPE_ERROR = 3
Examples
Receive density value (SG)
#define MAX_NODES 8
float densitySg[MAX_NODES] = {0}; // Storage for each node's density value
if (can.receive(priority, senderNodeType, receiverNodeType, secondaryNodeId, msgType, subIndex, data, dataLength))
{
switch (senderNodeType)
{
case NODE_TYPE_DENSITY_SENSOR:
{
switch (msgType)
{
case MSG_TYPE_DENSITY:
{
/* Extract sub-index (first byte of data) */
subIndex = message.data[0]; // Line not necessary, just here for clarity
/* Copy the next "size of a float" bytes to your variable */
if (secondaryNodeId < MAX_NODES)
{
/* Copy incoming float into the correct array index for this node */
memcpy(&densitySg[secondaryNodeId], &message.data[1], sizeof(float));
}
}
}
}
}
}Receive density sensor temperature value (°C)
#define MAX_NODES 8
float densityTemperature[MAX_NODES] = {0}; // Storage for each node's density value
if (can.receive(priority, senderNodeType, receiverNodeType, secondaryNodeId, msgType, subIndex, data, dataLength))
{
switch (senderNodeType)
{
case NODE_TYPE_DENSITY_SENSOR:
{
switch (msgType)
{
case MSG_TYPE_TEMPERATURE:
{
/* Extract sub-index (first byte of data) */
subIndex = message.data[0]; // Line not necessary, just here for clarity
/* Copy the next "size of a float" bytes to your variable */
if (secondaryNodeId < MAX_NODES)
{
/* Copy incoming float into the correct array index for this node */
memcpy(&densityTemperature[secondaryNodeId], &message.data[1], sizeof(float));
}
}
}
}
}
}Send new calibration to sensor @ node ID 3
float originalGravity = 1.054;
int nodeId = 3;
can.sendFloatCan(PRIORITY_HIGH, NODE_TYPE_PLC, NODE_TYPE_DENSITY_SENSOR,
nodeId, MSG_TYPE_CALIBRATION_CMD, originalGravity);Receive calibration ack (pseudo-code, extend as you like)
bool densitySensorCalibrated = false;
if (can.receive(priority, senderNodeType, receiverNodeType, secondaryNodeId, msgType, subIndex, data, dataLength))
{
switch (senderNodeType)
{
case NODE_TYPE_DENSITY_SENSOR:
{
switch (msgType)
{
case MSG_TYPE_CALIBRATION_ACK:
{
/* Extract sub-index (first byte of data) */
subIndex = message.data[0]; // Line not necessary, just here for clarity
if (secondaryNodeId < MAX_NODES)
{
const uint32_t ack =
(uint32_t(data[1]) << 24)
| (uint32_t(data[2]) << 16)
| (uint32_t(data[3]) << 8)
| uint32_t(data[4]);
if(ack == ACK_TYPE_OK)
{
densitySensorCalibrated = true;
}
}
}
}
}
}
}Pressure Sensor
The pressure sensor sends the measured pressure constantly.
Calibration
To calibrate your pressure sensor, send an empty CAN message with the message type MSG_TYPE_CALIBRATION_CMD , and listen for the MSG_TYPE_CALIBRATION_ACK message with acknowledgement types described below. It is important that the sensor is in atmospheric pressure at the time of calibration, since the calibration will read this as the zero pressure.
Node type: NODE_TYPE_PRESSURE_SENSOR = 3
Message types
To device
MSG_TYPE_NODE_ID = 36MSG_TYPE_CALIBRATION_CMD = 28
From device
MSG_TYPE_PRESSURE = 13MSG_TYPE_CALIBRATION_ACK = 29
Acknowledgement types
ACK_TYPE_NONE = 0ACK_TYPE_OK = 2ACK_TYPE_ERROR = 3
Examples
Receive pressure value (bar)
#define MAX_NODES 8
float pressure[MAX_NODES] = {0}; // Storage for each node's pressure value
if (can.receive(priority, senderNodeType, receiverNodeType, secondaryNodeId, msgType, subIndex, data, dataLength))
{
switch (senderNodeType)
{
case NODE_TYPE_PRESSURE_SENSOR:
{
switch (msgType)
{
case MSG_TYPE_PRESSURE:
{
/* Extract sub-index (first byte of data) */
subIndex = message.data[0]; // Line not necessary, just here for clarity
/* Copy the next "size of a float" bytes to your variable */
if (secondaryNodeId < MAX_NODES)
{
/* Copy incoming float into the correct array index for this node */
memcpy(&pressure[secondaryNodeId], &message.data[1], sizeof(float));
}
}
}
}
}
}Send calibration command to sensor @ node ID 7 (will read atmospheric pressure and subtract it)
int nodeId = 7;
can.sendFloatCan(PRIORITY_HIGH, NODE_TYPE_PLC, NODE_TYPE_PRESSURE_SENSOR,
nodeId, MSG_TYPE_CALIBRATION_CMD, 0.0);Receive calibration ack (pseudo-code, extend as you like)
bool pressureSensorCalibrated = false;
if (can.receive(priority, senderNodeType, receiverNodeType, secondaryNodeId, msgType, subIndex, data, dataLength))
{
switch (senderNodeType)
{
case NODE_TYPE_PRESSURE_SENSOR:
{
switch (msgType)
{
case MSG_TYPE_CALIBRATION_ACK:
{
/* Extract sub-index (first byte of data) */
subIndex = message.data[0]; // Line not necessary, just here for clarity
if (secondaryNodeId < MAX_NODES)
{
const uint32_t ack =
(uint32_t(data[1]) << 24)
| (uint32_t(data[2]) << 16)
| (uint32_t(data[3]) << 8)
| uint32_t(data[4]);
if(ack == ACK_TYPE_OK)
{
pressureSensorCalibrated = true;
}
}
}
}
}
}
}Radar Level Sensor
The level sensor sends the measured distance in meters constantly. You can use this value to calculate the volume in your tank.
Node type: NODE_TYPE_LEVEL_SENSOR = 5
Message types
To device
MSG_TYPE_NODE_ID = 36MSG_TYPE_MIN = 41(Used to set the minimum measure distance of the sensor, usually not necessary to change from default (0.25 m))MSG_TYPE_MAX = 42(Used to set the maximum measure distance of the sensor; the sensor can give higher quality measurements if this value is specified. Default is 3.0 m)
From device
MSG_TYPE_LEVEL = 16
Examples
Send min/max distance value
float min = 0.03; // 0.03 m / 3 cm
float max = 1.2; // 1.2 m / 120 cm
int nodeId = 3;
/* Send min value */
can.sendFloatCan(PRIORITY_HIGH, NODE_TYPE_PLC, NODE_TYPE_LEVEL_SENSOR,
nodeId, MSG_TYPE_MIN, min);
/* Send max value */
can.sendFloatCan(PRIORITY_HIGH, NODE_TYPE_PLC, NODE_TYPE_LEVEL_SENSOR,
nodeId, MSG_TYPE_MAX, max);Receive distance from level sensor (m)
#define MAX_NODES 8
float levelDistance[MAX_NODES] = {0}; // Storage for each node's distance value
if (can.receive(priority, senderNodeType, receiverNodeType, secondaryNodeId, msgType, subIndex, data, dataLength))
{
switch (senderNodeType)
{
case NODE_TYPE_LEVEL_SENSOR:
{
switch (msgType)
{
case MSG_TYPE_LEVEL:
{
/* Extract sub-index (first byte of data) */
subIndex = message.data[0]; // Line not necessary, just here for clarity
/* Copy the next "size of a float" bytes to your variable */
if (secondaryNodeId < MAX_NODES)
{
/* Copy incoming float into the correct array index for this node */
memcpy(&levelDistance[secondaryNodeId], &message.data[1], sizeof(float));
}
}
}
}
}
}Agitator
The agitator sends its measured rounds per minute (RPM) of the motor shaft constantly. It can be controlled by specifying the power from 0-100%.
Node type: NODE_TYPE_AGITATOR_ACTUATOR = 6
Message types
To device
MSG_TYPE_NODE_ID = 36MSG_TYPE_PWM = 27
From device
MSG_TYPE_RPM = 17
Examples
Send agitator power value (0-100%)
uint32_t agitatorPower = 50; // 0-100
can.sendUintCan(PRIORITY_HIGH, NODE_TYPE_PLC,
NODE_TYPE_AGITATOR_ACTUATOR, 0, MSG_TYPE_PWM, agitatorPower);Receive rounds per minute value from agitator (RPM)
#define MAX_NODES 8
float agitatorRPM[MAX_NODES] = {0}; // Storage for each node's RPM value
if (can.receive(priority, senderNodeType, receiverNodeType, secondaryNodeId, msgType, subIndex, data, dataLength))
{
switch (senderNodeType)
{
case NODE_TYPE_AGITATOR_ACTUATOR:
{
switch (msgType)
{
case MSG_TYPE_RPM:
{
/* Extract sub-index (first byte of data) */
subIndex = message.data[0]; // Line not necessary, just here for clarity
if (secondaryNodeId < MAX_NODES)
{
agitatorRPM[secondaryNodeId] =
(uint32_t(data[1]) << 24)
| (uint32_t(data[2]) << 16)
| (uint32_t(data[3]) << 8)
| uint32_t(data[4]);
}
}
}
}
}
}Last updated
Was this helpful?

