Help needed to understand CAN messages

Hello, if someone could help me understand how CAN messages work that would be amazing

I am trying to create a CAN-bus based gauge to display information from my Haltech ECU in my project car. I embarked on this with little to no knowledge on how to code but figured i'd give it a shot. I bought a Sparkfun CAN-bus shield, Uno R3, Adafruit 1.3" 128x64 OLED, and an RTC breakout. The idea was that I'd replace my clock in the car with this screen that would show me a few sensor outputs as reported from the ecu as well as the time (so I'm not late :slight_smile: ).

For those unfamiliar with the Haltech, it is a 3rd party standalone ecu that broadcasts a couple of sensor's data via the CAN protocol for use with an aftermarket dash/gauge. They're kind enough to provide a document (which I don't fully understand, attached) with the CAN ids, btyes that are relevant for the specific channel, and a couple of other useful tid bits.

The Goal:
I would like to be able to receive the data of a specific message and print it on the OLED

My problem:
I don't understand the CAN library I'm using. Below is the code I'm trying to use from the examples. The part that I don't understand is how I can extract a single message's data and assign it to a float so I can perform the mathematical operations to convert it into a useful measurement.

/****************************************************************************
CAN Read Demo for the SparkFun CAN Bus Shield. 

Written by Stephen McCoy. 
Original tutorial available here: http://www.instructables.com/id/CAN-Bus-Sniffing-and-Broadcasting-with-Arduino
Used with permission 2016. License CC By SA. 

Distributed as-is; no warranty is given.
*************************************************************************/

#include <Canbus.h>
#include <defaults.h>
#include <global.h>
#include <mcp2515.h>
#include <mcp2515_defs.h>

//********************************Setup Loop*********************************//

void setup() {
  Serial.begin(9600); // For debug use
  Serial.println("CAN Read - Testing receival of CAN Bus message");  
  delay(1000);
  
  if(Canbus.init(CANSPEED_500))  //Initialise MCP2515 CAN controller at the specified speed
    Serial.println("CAN Init ok");
  else
    Serial.println("Can't init CAN");
    
  delay(1000);
}

//********************************Main Loop*********************************//

void loop(){

  tCAN message;
if (mcp2515_check_message()) 
	{
    if (mcp2515_get_message(&message)) 
	{
        //if(message.id == 0x620 and message.data[2] == 0xFF)  //uncomment when you want to filter
             //{
               
               Serial.print("ID: ");
               Serial.print(message.id,HEX);
               Serial.print(", ");
               Serial.print("Data: ");
               Serial.print(message.header.length,DEC);
               for(int i=0;i<message.header.length;i++) 
                {	
                  Serial.print(message.data[i],HEX);
                  Serial.print(" ");
                }
               Serial.println("");
             //}
           }}

}

From what I gather I don't need the majority of this code as I'm only concerned with the actual value that should be in the message. So I simplified it to the below with the addition of the relevant can message id and bytes according to the provided documentation.

tCAN message;
    if (mcp2515_check_message()) {
      if (mcp2515_get_message(&message)) {
        if (message.id == 0x012) {
          uint16_t maifoldp = 0;
          manifoldp = (uint_16t)buf[16] << 8;
          manifoldp |= buf[17];
        }
      }   
    }

This is based off the library provided and some comments from this forum (link). What I don't understand is that the check message doesn't seem to reference any specific message (not sure if thats just the blanket command to pull all messages then you filter out the noise in the next few if statements) and in the following If the &message also doesn't seem to reference anything in specific.

My biggest issue is that I do know how to pull the relevant bytes of information from this message or even if im reading the Haltech document correctly. From my understanding the CAN id for Manifold pressure should be 0x012 with the relevant bytes being 16 and 17. Am I correct in saying this? Based on the other post in this forum I learned I need to gather the two relevant bytes into a single 16 bit byte? which is where that uint16_t bit came from but as mentioned above I don't know how to assign buf[16] and buf[17] to the relevant bytes.

Any help would be greatly appreciated.

At the bottom is my full code including everything I've done most of which works but may not be the most efficient/conventional/neat way to do things. Feel free to enlighten me with any tips/suggestions. This code contains dummy values for the data I wanted to read so I could configure how it would look

If you made it this far into this wall of text and still feel like helping I thank you and owe you a beer or a few should we meet.

PS. I did a fair bit of research before posting here and nothing I found I could make sense of other than the biggest underlying message being learn how to post on these forums before posting. Hopefully I have done everything correctly :slight_smile:

Haltech CAN Protocol Document v1.1.pdf (17.7 KB)

Full code here as I exceeded 9000 character limit (oops)

#include <Canbus.h>
#include <defaults.h>
#include <global.h>
#include <mcp2515.h>
#include <mcp2515_defs.h>
#include "RTClib.h"
#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Fonts/FreeSans24pt7b.h>

int buttonState12 = 0;
int buttonState13 = 0;
int buttonState7 = 0;
int page = 0;
int HOUR = 0;
int manifoldp = -1000; //dummy values start, This one is an integer due to the resolution being a 1 mBar

float cts = 83.6;
float vbatt = 13.86;
float iat = 36.6;
float roadspeed = 179.8; //dummy values end

RTC_DS1307 rtc;

#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET     8
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

void setup()
{
  pinMode(12, INPUT);
  pinMode(13, INPUT);
  pinMode(7, INPUT);
  Serial.begin(57600);
  Canbus.init(CANSPEED_500);
  //rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));

  display.begin(SSD1306_SWITCHCAPVCC, 0x3D);
  display.setTextColor(SSD1306_WHITE);
  display.display();
  delay(50);

}

void loop()
{
  buttonState13 = digitalRead(13);
  if (buttonState13 == LOW) {
    if  (page < 6){
       page = page + 1;
    }
  }
  delay(50);
  
  buttonState12 = digitalRead(12);
  if (buttonState12 == LOW) {
    if  (page > 0){
       page = page - 1;
    }
  }
  delay(50);
  
  buttonState7 = digitalRead(7);
  if (buttonState7 == HIGH) {
    if  (page > 0){
       page = 0;
    }
  }
  delay(50);
  if (page == 0){
    display.clearDisplay();
    DateTime now = rtc.now();   
    display.setCursor(10,54);
    display.setTextSize(1);
    display.setFont(&FreeSans24pt7b);
    if (now.hour() > 12) {
      HOUR = now.hour() - 12;
    }
      else {
        HOUR = now.hour();
      }
    if (HOUR < 10) {
      display.setCursor(38,54);
    }
    display.print(HOUR, DEC);
    if ((now.second() & 0x01) == 0) {
      display.setCursor(62,54);
      display.print(':');
    }
    display.setCursor(73,54);
    if (now.minute() <10){
      display.print('0');
    }
    display.print(now.minute(), DEC);
    display.display();
    delay(10);
  }
  if (page == 1){
    display.clearDisplay();
    tCAN message;
    if (mcp2515_check_message()) {
      if (mcp2515_get_message(&message)) {
        if (message.id == 0x012) {
          uint16_t maifoldp = 0;
          manifoldp = (uint16_t)buf[16] << 8;
          manifoldp |= buf[17];
        }
      }   
    }
    display.setTextSize(1);
    display.setFont();
    display.setCursor(0,0);
    display.println("Manifold Pressure    (psi)");
    display.setFont(&FreeSans24pt7b);
    display.setCursor(10,50);
    display.print(manifoldp / 68.948, 1);
    display.display();
    delay(10);
  }
  if (page == 2){
    display.clearDisplay();
    display.setTextSize(1);
    display.setFont();
    display.setCursor(0,0);
    display.print("Water Temperature");    
    display.setFont(&FreeSans24pt7b);
    display.setCursor(40,50);
    display.print((cts / 10) + 273.15,0);
    display.setFont();
    display.setTextSize(2);
    display.setCursor(115,38);
    display.print("C");
    display.drawCircle(110,40,2,SSD1306_WHITE);
    display.display();
    delay(10);
  }
  if (page == 3){
    display.clearDisplay();
    display.setTextSize(1);
    display.setFont();
    display.setCursor(0,0);
    display.print("Intake Air Temp");
    display.setFont(&FreeSans24pt7b);
    display.setCursor(40,50);
    display.print((iat / 10) + 273.15,0);
    display.setFont();
    display.setTextSize(2);
    display.setCursor(115,38);
    display.print("C");
    display.drawCircle(110,40,2,SSD1306_WHITE);
    display.display();
    delay(50);
  }
  if (page == 4){
    display.clearDisplay();
    display.setTextSize(1);
    display.setFont();
    display.setCursor(0,0);
    display.println("Battery Voltage");
    display.setFont(&FreeSans24pt7b);
    display.setCursor(10,50);
    display.print(vbatt / 10, 1);
    display.display();
    delay(10);
  }
  if (page == 5){
    display.clearDisplay();
    display.setTextSize(1);
    display.setFont();
    display.setCursor(0,0);
    display.println("Speed");
    display.setFont(&FreeSans24pt7b);
    display.setCursor(15,50);
    display.print(roadspeed / 10,0);
    display.setFont();
    display.print(" km/h");
    display.display();
    delay(50);
  }
}

Your assumptions are mostly correct, except that all can messages are a maximum of 8 bytes long.

You will see in the documentation it splits the message over multiple different CANIDs, each of 8 bytes each. Although less than helpfully it seems to have just continued the numbering scheme, rather than resetting to zero. Therefore the 2 bytes you want are in buff[0] and buff[1] when you get a 0x012, the preceding 16 bytes having been received (and discarded) previously as they had the wrong IDs.

The library does allow you to set a hardware filter mask on a range of CANIDs if you want. Without it - yes - it sends you everything and you have to throw away what you don’t want. Usually the Arduino can keep up with software processing, as long as you aren’t doing anything too taxing in your software loop.

Edit: Perhaps I missed it. Where did you declare buf?
Also, it’s possible the values could be Big or Little Endian (high or low byte first). The spec doc doesn’t say, but perhaps it’s in one of the other documents it references. Regardless it should be easy to tell once you have some readings.
Your use of delay() May cause problems. Wait too long and the hardware buffer in the CANBUS chip on the shield will overflow. Then you will start missing messages. Listening for all IDs without a filter will make that even worse. Good Arduino coding practice is, as I’m sure you know, to avoid using delay....