NMEA 2000 Shield

Thanks Timo, that sounds like a sensible way forward. There's a lot I don't understand yet so I might be back!

timolappalainen:
Then just poll the buffer like on NMEA2000 library and filter messages you are interested.

Humour me please: I'm still very much a noob and need some high level pointers :slight_smile:

Presumably the frames from the engine canbus are being put into buffers within an array as they arrive.

Presumably your NMEA code sends out its packets to the N2K network at preset intervals. Faster for some things like Engine Rapid PGN's I suppose and slower for Temperature PGN's.

Presumably I need to get your N2K code to look for the correct frame ID (e.g. 0x288) in the array and then extract the data I'm interested in (e.g. byte 1 for coolant temp).

How do I do this last part? Is it some form of If ID=0x288 then get the data from buf[2] or some such into a variable.

Your help, as ever, is appreciated.

Hi Timo, thanks for your hard work.
I would like to make an arduino project who's light a lamp when it see on the nmea2000 bus the alarm msg PGN126983.
Do you think I can use your library for that? and could you suggest me how?
Thanks
Bye
Benoit :slight_smile:

keith-i,

The simplest way is just poll other CAN on Teensy 3.6. It would be something like this:

void PollEngineCAN() {
  CAN_message_t Msg;

  if ( Can1.available()>0 ) {
    Can1.read(Msg);
    if ( Msg.flags.extended==0 ) { // Standard message
      //Filter and parse message here to global data EngineRPM, EngineOilPressure, etc.
    }
  }
}

void SendN2kEngineRapid() {
  static unsigned int NextSendTime=5000;

  if ( NextSendTime<millis() ) {
    NextSendTime+=100;
    tN2kMsg N2kMsg;
    
    SetN2kEngineParamRapid(N2kMsg,0,EngineRPM); 
    NMEA2000.SendMsg(N2kMsg);
  }
}

void SendN2kEngineDynamic() {
  static unsigned int NextSendTime=5000;

  if ( NextSendTime<millis() ) {
    NextSendTime+=500;
    tN2kMsg N2kMsg;
    
    SetN2kEngineDynamicParam(N2kMsg,0,EngineOilPressure,EngineOilTemp,EngineCoolantTemp,
                                               N2kDoubleNA,N2kDoubleNA,N2kDoubleNA);
    NMEA2000.SendMsg(N2kMsg);
  }
}

void loop() {
  NMEA2000.ParseMessages();
  PollEngineCAN();
  SendN2kEngineRapid();
  SendN2kEngineDynamic();
}

Note that you should provide NA value for each data, which is not available. Also you should take care that if your Engine CAN will not send some data anymore, you should update that data value to NA after some timeout. This can be easily done by using class for values.

kenobiben,

Yes you can use library to handle PGN 126983 alert messages as any NMEA 2000 message. In N2kMessages module there are set and parser functions for most common messages. So for PGN 126983 you can write your own parser function. Then you make handler functions like in DataDisplay example.

The one problem is that I do not have field description for PGN 126983 or currently any device sending that to do reverse engineering.

Thanks Timo that's very helpful to me.

Thanks Timo, I will try and tell you

Hi Timo

I've set up a bit of test code to send some dummy data to the N2k network and have put N2kDoubleNA and N2kInt8NA as appropriate in the unused fields of PGN 127489 (engine dynamic).

However when I look at the NMEA guide for 127489 it shows 14 data fields whereas your messages.h file only seems to refer to 12 fields. When I check everything on Actisense reader it puts odd data into fields 13 and 14 which are engine load and torque. Does your messages.h file perhaps need some more data fields adding to comply? I might be misunderstanding how the engine status flags are set as these appear to me to be the missing fields.

Keith

Edit: I was trying to be clever and putting fields in the order of the NMEA PGN rather than your N2kMessages.h file. I would still be keen to know how you set engine flags though.

Ok, well here's my attempt at coding this ECU to N2k gateway. I'd appreciate any comments you might have about it, particuarly the variable declarations and calculations on the data bytes as they are the most likely sources of any mistakes. I'm waiting on delivery of a Teensy 3.6 to be able to try it for real.

It would also be good to have some ideas on how to set a timeout such that the N2k fields get populated with NA if the engine is not on.

// Convert VW Marine ECU canbus data to N2k. Oct 2019
// based on Collin80 and Timo Lappalainen code

#include <Arduino.h>
#include <NMEA2000_CAN.h>  // This will automatically choose right CAN library and create suitable NMEA2000 object
#include <N2kMessages.h>
#include <NMEA2000_teensy.h>
#include <FlexCAN.h>


#ifndef __MK66FX1M0__
  #error "Teensy 3.6 with dual CAN bus is required to run this example"
#endif

// List below N2k messages your device will transmit.
const unsigned long TransmitMessages[] PROGMEM={127488L,127489,0}; //engine rapid and dynamic

static CAN_message_t msg; //sets up a storage buffer?
static uint8_t hex[17] = "0123456789abcdef";
  
//-----------------------------------------------------------------------------------------
void setup() {
  
  delay(5000);
  // Set Product information
  NMEA2000.SetProductInformation("000300", // Manufacturer's Model serial code
                                 200, // Manufacturer's product code
                                 "Arduino VW Gateway",  // Manufacturer's Model ID
                                 "1.1 (2019)",  // Manufacturer's Software version code
                                 "1.A (2019)" // Manufacturer's Model version
                                 );
  // Det device information
  NMEA2000.SetDeviceInformation(303030, // Unique number. Use e.g. Serial number.
                                160, // Device function=engine gateway. 
                                50, // Device class=propulsion. 
                                443 // Just choosen free from code list - VDO                                
                               );
  
  // Uncomment 2 rows below to see, what device will send to bus. Use e.g. OpenSkipper or Actisense NMEA Reader                           
  //Serial.begin(115200); // uncomment to send to serial for PC use
  //NMEA2000.SetForwardStream(&Serial); // uncomment to send to serial - default is Actisense format for PC use

  // If you want to use simple ascii monitor like Arduino Serial Monitor, uncomment next line
  //NMEA2000.SetForwardType(tNMEA2000::fwdt_Text); // uncomment to send to serial as plain text - overides above Actisense format
  
  // If you also want to see all traffic on the bus use N2km_ListenAndNode instead of N2km_NodeOnly below
  NMEA2000.SetMode(tNMEA2000::N2km_NodeOnly,22);
  
  //NMEA2000.SetDebugMode(tNMEA2000::dm_Actisense); // Uncomment this, so you can test code without CAN bus chips on Arduino Mega
  // Comment out next line for Actisense PC use
  NMEA2000.EnableForward(false); // Disable all forward messaging from N2K bus to USB (=Serial) - default is True. Comment for PC use
  
  NMEA2000.ExtendTransmitMessages(TransmitMessages);
  NMEA2000.Open();

  //Can0 should be automatically set at default 250kbs for N2k
  Can1.begin(500000); //set baud for VW ECU.
  Can1.setListenOnly (true); //hopefully set listen only mode. Set after begin()
  }

  /*
   * 0x280 byte 1 = torque (*0.39)%
   * 0x280 byte 2low&3high = rpm (*0.25)/min
   * 0x288 byte 1 = coolant ((*0.75)-48)C
   * 0x380 byte 1 = IAT ((*0.75)-48)C
   * 0x480 byte 2&3 = fuel rate ul
   * 0x588 byte 4 = boost (*0.01)bar
   */
      
  //maybe use int not uint8_t
  uint8_t torque = 0; //torque %
  uint16_t rpm = 0; //rpm
  uint8_t coolant = 0; //coolant temp in degC
  uint16_t fuel = 0;  //fuel rate in ul
  uint8_t boost = 0; //boost pressure in Pa. 1bar = 100,000Pa

void PollEngineCAN()
  {
  CAN_message_t inMsg;
    
    if (Can1.available()>0) {//or maybe use while?
      Can1.read(inMsg); //read message into inMsg
      //filter and parse message here to global data
      if (inMsg.id == 0x280){
        torque = (inMsg.buf[1]*0.39);
        rpm = (inMsg.buf[3]<<8)+inMsg.buf[2];}
      if (inMsg.id == 0x288){
        coolant = ((inMsg.buf[1]*0.75)-48);}
//      if (inMsg.id == 0x480){
//        fuel = (((inMsg.buf[4]&0b01111111)<<8)+inMsg.buf[3]);}
      if (inMsg.id == 0x588){
        boost = (inMsg.buf[4]*0.01);} 
    }
  }
//might want a timeout to put NA if engine is off

void SendN2kEngineRapid() 
  {
  static unsigned int NextSendTime=5000;
  if ( NextSendTime<millis() )
    {
    NextSendTime+=100;
    tN2kMsg N2kMsg;
 
    SetN2kEngineParamRapid(N2kMsg, 0, rpm, boost, N2kInt8NA);
    NMEA2000.SendMsg(N2kMsg);
    }
  }

void SendN2kEngineDynamic() 
  {
  static unsigned int NextSendTime=5000; 
  if ( NextSendTime<millis() )
    {
    NextSendTime+=500;
    tN2kMsg N2kMsg;
 
    SetN2kEngineDynamicParam(N2kMsg, 0, N2kDoubleNA, N2kDoubleNA, coolant, 
    N2kDoubleNA, fuel, N2kDoubleNA, N2kDoubleNA, N2kDoubleNA, N2kInt8NA, torque);
    NMEA2000.SendMsg(N2kMsg);
    }
  }
//-----------------------------------------------------------------------------------------
void loop() 
  
{
  NMEA2000.ParseMessages();
  PollEngineCAN();
  SendN2kEngineRapid();
  SendN2kEngineDynamic();
  }

Some things.

  • When you calculate values, with real coefficient, you can not use int. You have to use float or double. With Teensy 3.6 I use just double, since it seem to be fast enough.

  • In SetN2kEngineParamRapid you do not need to set EngineTiltTrim, since that has been set to NA as default.

  • Your PollEngineCAN now test every time all msg id:s. You can either use if () { } else if () {}... way or in your case when you test against constant you can use switch, which is faster than if-else

so

  double torque = N2kDoubleNA; //torque %
  double rpm = N2kDoubleNA; //rpm
  double coolant = N2kDoubleNA; //coolant temp in degC
  double fuel = N2kDoubleNA;  //fuel rate in ul
  double boost = N2kDoubleNA; //boost pressure in Pa. 1bar = 100,000Pa
  unsigned long LastRPM=0;

void PollEngineCAN() {
  CAN_message_t inMsg;
   
    if (Can1.available()>0) {//or maybe use while?
      Can1.read(inMsg); //read message into inMsg
      //filter and parse message here to global data
      switch ( inMsg.id ) {
        case 0x280:
          torque = ( inMsg.buf[1]*0.39 );
          rpm = ( inMsg.buf[3]<<8)+inMsg.buf[2];
          LastRPM=millis();
          break;
        case 0x288:
          coolant = ( (inMsg.buf[1]*0.75)-48);
          break;
//        case 0x480:
//        fuel = (((inMsg.buf[4]&0b01111111)<<8)+inMsg.buf[3]);
//        break;
        case 0x588:
          boost = ( inMsg.buf[4]*0.01 );
          break;
      }
    }
  }

Note that I added LastRPM variable, which you can then use on sending:

#define EngineTimeout 5000

void loop() {
  NMEA2000.ParseMessages();
  PollEngineCAN();
  if ( LastRPM+EngineTimeout > millis() ) {
    SendN2kEngineRapid();
    SendN2kEngineDynamic();
  }
}

Other way is to set values to NA on polling:

void PollEngineCAN() {
    if (Can1.available()>0) {//or maybe use while?
      ...
    }
    if ( LastRPM+EngineTimeout < millis() ) {
      torque = N2kDoubleNA;
      rpm = N2kDoubleNA;
      coolant = N2kDoubleNA;
      fuel = N2kDoubleNA; 
      boost = N2kDoubleNA;
    }
}

You can also have own timeout counter for each message.

Setting NA is a bit how you would it like to work. I have system, which starts to alarm, if value drops to NA for RPM.

Thanks Timo. Once again your detailed help is very much appreciated.

Has someone been lucky with PGN 127489 and B&G Triton T41? I have an issue with that PGN, T41 does not show it.

Here is my whole code:

// NMEA2000 library. Send engine data to the bus.

#include <Arduino.h>
#include <NMEA2000_CAN.h>  // This will automatically choose right CAN library and create suitable NMEA2000 object
#include <N2kMessages.h>
 

// List here messages your device will transmit.
const unsigned long TransmitMessages[] PROGMEM={60928UL,127489UL,127505UL,0};

void setup() {
  // Set Product information
  NMEA2000.SetProductInformation("112233", // Manufacturer's Model serial code
                                 100, // Manufacturer's product code
                                 "Engine monitor",  // Manufacturer's Model ID
                                 "1.1.0.21 (2016-12-31)",  // Manufacturer's Software version code
                                 "1.1.0.0 (2016-12-31)" // Manufacturer's Model version
                                 );
  // Set device information
  NMEA2000.SetDeviceInformation(112233, // Unique number. Use e.g. Serial number.
                                140, // Device function=Engine. See codes on http://www.nmea.org/Assets/20120726%20nmea%202000%20class%20&%20function%20codes%20v%202.00.pdf
                                50, // Device class=Propulsion. See codes on  http://www.nmea.org/Assets/20120726%20nmea%202000%20class%20&%20function%20codes%20v%202.00.pdf
                                2040 // Just choosen free from code list on http://www.nmea.org/Assets/20121020%20nmea%202000%20registration%20list.pdf                               
                               );

  // Uncomment 2 rows below to see, what device will send to bus. Use e.g. OpenSkipper or Actisense NMEA Reader                           
  Serial.begin(115200);
  NMEA2000.SetForwardStream(&Serial);
  // If you want to use simple ascii monitor like Arduino Serial Monitor, uncomment next line
  //NMEA2000.SetForwardType(tNMEA2000::fwdt_Text); // Show in clear text. Leave uncommented for default Actisense format.

  // If you also want to see all traffic on the bus use N2km_ListenAndNode instead of N2km_NodeOnly below
  NMEA2000.SetMode(tNMEA2000::N2km_NodeOnly,22);
  //NMEA2000.SetDebugMode(tNMEA2000::dm_Actisense); // Uncomment this, so you can test code without CAN bus chips on Arduino Mega
  //NMEA2000.EnableForward(false); // Disable all msg forwarding to USB (=Serial)
  // Here we tell library, which PGNs we transmit
  NMEA2000.ExtendTransmitMessages(TransmitMessages);
  NMEA2000.Open();
}


void loop() {
  SendN2kTankData();
  SendN2kEngineData();
  SendAddress();
  NMEA2000.ParseMessages();
}

double ReadOilPressure() {
  return mBarToPascal(2870); // fixed value for testing
}

double ReadEngineCoolantTemp() {
  return CToKelvin(85); // fixed value for testing
}

double ReadAlternatorVoltage() {
  return 13.7; // fixed value for testing
}

double ReadFuelRate() {
  return N2kDoubleNA;
}

#define EngineDataUpdatePeriod 500

void SendN2kEngineData() {
  static unsigned long EngineDataUpdated=millis();
  tN2kMsg N2kMsg;

  if ( EngineDataUpdated+EngineDataUpdatePeriod<millis() ) {
    EngineDataUpdated=millis();
    SetN2kEngineDynamicParam(N2kMsg, 0, ReadOilPressure(), N2kDoubleNA, ReadEngineCoolantTemp(), ReadAlternatorVoltage(), ReadFuelRate(), N2kDoubleNA);
    NMEA2000.SendMsg(N2kMsg);    
  }
}

double ReadTankLevel() {
  return 55; // fixed value for testing
}

#define TankDataUpdatePeriod 2500
#define TankCapacity 75

void SendN2kTankData() {
  static unsigned long TankDataUpdated=millis();
  tN2kMsg N2kMsg;

  if ( TankDataUpdated+TankDataUpdatePeriod<millis() ) {
    TankDataUpdated=millis();
    SetN2kFluidLevel(N2kMsg, 0, N2kft_Fuel, ReadTankLevel(), TankCapacity);
    NMEA2000.SendMsg(N2kMsg);    
  }
}

#define IsoAddressClaimUpdatePeriod 30000

void SendAddress() {
  static unsigned long IsoAddressClaimUpdated=millis();
  if ( IsoAddressClaimUpdated+IsoAddressClaimUpdatePeriod<millis() ) {
    IsoAddressClaimUpdated=millis();
    NMEA2000.SendIsoAddressClaim();
  }
}

PGN 127505 is ok, T41 shows it (it is fuel amount).

Do anyone have ideas?

I have B&G Triton 2 and it reads MessageSender example hard coded data:
SetN2kEngineDynamicParam(N2kMsg,0,656000,CToKelvin(86.3),CToKelvin(82.1),14.21,5.67,hToSeconds(2137.55),N2kDoubleNA,N2kDoubleNA,N2kInt8NA,N2kInt8NA,true);

There should not be changes, which effect for that, but please download latest library update and try with hard coded data.

An other thing may effect is that your send buffer may be filled up. Depending of board and you sw, increase send buffer at beginning of setup with command:

NMEA2000.SetN2kCANSendFrameBufSize(100);

Default size is 40. As I see you do not send that much data, so this should not be the case.

Timo, thanks for your advices!

I copied your hard coded SetN2kEngineDynamicParam clause from MessageSender example to my code. No success.

I downloaded and installed latest versions from NMEA2000 and NMEA2000_esp32 libraries. No success.

I set NMEA2000.SetN2kCANSendFrameBufSize(100); No success.

I tried your whole MessageSender example. It worked.

I copied NMEA2000.SetProductInformation and NMEA2000.SetDeviceInformation clauses from your MessageSender example to my code. It worked.

I tried with my original SetN2kEngineDynamicParam clause. It worked.

So there is something wrong with my original product and device information. It is also weird that now my mfd does not show my device as source for engine data but it shows right values in right fields.

Weird.

Please try find some logic for that. I'll expect that only SetDeviceInformation will have effect. SetDeviceInformation data is just informative.

You could also try add fake sending of Engine rapid data PGN 127488UL. Just set all data fields to NA, but set engine instance field same as on your other engine data.

It is also possible that if tell your function as engine, MFD will request for PGN 127498 "Engine static data" and when it does not get it, it does not listen your engine data. You could try to see requests by setting your device mode to N2km_ListenAndNode and listen your USB with Actisense NMEA Reader. You may see PGN 59904 from MFD with data 127498. The problem is that there will be so many requests that data will change, before you notice it. One way is to put NMEA Reader to record mode File-"Log connection data" and then parse log with Actisense EBL Reader.

Other way is to add use NMEA2000.SetISORqstHandler and write handler function for that. Then if RequestedPGN is 127498, you print a message.

Note that you do not need to have 60928UL on transmit messages, since that is system message and listed as default.

Now it works :slight_smile:

It needs also PGN 127488 (Engine Rapid Data) to be sent (can be without values, except engine instance). Then it reads product and device information right and shows my device as source for engine data in Triton T41.

I set my device mode to N2km_ListenAndNode as you adviced and listened USB with Actisense NMEA Reader. MFD sends PGN 59904 twice and asks for 126996 (Product Information) and 60928 (ISO Address Claim). Device sends them and then everything is ok.

This is a weird world. Thank you, Timo

I had a bit similar with my Raymarine autopilot. It did not work with True Heading message from my GPS compass, so I programmed my converter box to send Magnetic Heading with Variation correction. Stupid thing to correct wrong direction. Then on other boat I noticed they are sending Variation also on own PGN. I tested that and got it working with HDT message without stupid conversion.

Also in your code you send 60928 ISO Address claim periodically. No device should require that, since it should be automatic and can be requested with 59904 if necessary. So you could test without. I know that some older Garmin did not work without <5 s periodic sending. I have GMI 20 and it works fine and time to time requests it.

Hi Timo and all...

I'm confused about how to use the n2kMsg.Add4ByteUDouble() method.

How should the precision parameter be used? I'm trying to send a float that looks like 7.25 or 15.18 and I have tried using 2e-4 and 10.01 but I don't seem to get a proper result. If I manually convert the float value to a 4 byte hex number and send the bytes one at a time using AddByte() it works.

Thanks...

timolappalainen:
Also in your code you send 60928 ISO Address claim periodically. No device should require that, since it should be automatic and can be requested with 59904 if necessary. So you could test without.

Yes, I removed 60928 regular sending and it works fine.

As in e.g. SetN2kPGN128267 you set N2kMsg.Add4ByteUDouble(DepthBelowTransducer,0.01); You provide DepthBelowTransducer in real value in prefered units - in this case meters. On set you define precision how it will be converted to integer - in this case 0.01 unit. These precisions has been defined by NMEA2000. So if you are sending standard PGN, you have to use right precision.

On read side you use same precision e.g. DepthBelowTransducer=N2kMsg.Get4ByteUDouble(0.01,Index);

Which PGN you are trying to create?