/* NMEA2000.h Copyright (c) 2015-2021 Timo Lappalainen, Kave Oy, www.kave.fi Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. NMEA2000 device class definition. With NMEA2000 device class you can send, read and forward messages to NMEA2000 bus. As default library creates system, which acts like Actisense NGT NMEA2000->PC interface forwarding all messages from bus to PC. By changing mode to N2km_NodeOnly, one can make e.g. temperature source device to NMEA2000 bus. !Note. Each device on NMEA2000 bus should have own address on range 0-253. This class uses J1939 automatic address claiming (or dynamic addressing). So that you can start your device with some address set by method SetMode(...). It is also important to set your device "name" with method SetDeviceInformation(...) so that it would be unique. If you do not set "name" to unique, you devices changes address on start randomly. In principle they should still work fine. It is also good idea to save device address to the EEPROM. In this way if you connect two of your devices to the bus, they will do the automatic address claiming. If later save address to the EEPROM and use that on next start, they does not need to change address anymore. See also method ReadResetAddressChanged(). */ #ifndef _NMEA2000_H_ #define _NMEA2000_H_ #include "NMEA2000_CompilerDefns.h" #include "N2kStream.h" #include "N2kMsg.h" #include "N2kCANMsg.h" #if !defined(N2K_NO_GROUP_FUNCTION_SUPPORT) #include "N2kGroupFunction.h" #endif #define N2kPGNIsoAddressClaim 60928L #define N2kPGNProductInformation 126996L #define N2kPGNConfigurationInformation 126998L // Document says for leghts 33,40,24,32, but then values // has not been translated right on devices. #define Max_N2kModelID_len 32 #define Max_N2kSwCode_len 32 #define Max_N2kModelVersion_len 32 #define Max_N2kModelSerialCode_len 32 // Define length of longest info string above + 1 termination char #define Max_N2kProductInfoStrLen 33 // I do not know what standard says about max field length, but according to tests NMEAReader crashed with // lenght >=90. Some device was reported not to work string length over 70. #define Max_N2kConfigurationInfoField_len 71 // 70 + '/0' #define Max_N2kMsgBuf_Time 100 #define N2kMessageGroups 2 #define N2kMaxCanBusAddress 251 #define N2kNullCanBusAddress 254 class tNMEA2000 { public: static bool IsProprietaryMessage(unsigned long PGN); static void ClearCharBuf(size_t MaxLen, char *buf); static void SetCharBuf(const char *str, size_t MaxLen, char *buf); static void ClearSetCharBuf(const char *str, size_t MaxLen, char *buf); // max and min are not available on all systems, so use own definition. template static T N2kMax(T a, T b) { return (a>b?a:b); } template static T N2kMin(T a, T b) { return (a>21; } void SetDeviceInstance(unsigned char _DeviceInstance) { DeviceInformation.DeviceInstance=_DeviceInstance; } unsigned char GetDeviceInstance() const { return DeviceInformation.DeviceInstance; } unsigned char GetDeviceInstanceLower() const { return DeviceInformation.DeviceInstance & 0x07; } unsigned char GetDeviceInstanceUpper() const { return (DeviceInformation.DeviceInstance>>3) & 0x1f; } void SetDeviceFunction(unsigned char _DeviceFunction) { DeviceInformation.DeviceFunction=_DeviceFunction; } unsigned char GetDeviceFunction() const { return DeviceInformation.DeviceFunction; } void SetDeviceClass(unsigned char _DeviceClass) { DeviceInformation.DeviceClass=((_DeviceClass&0x7f)<<1); } unsigned char GetDeviceClass() const { return DeviceInformation.DeviceClass>>1; } void SetIndustryGroup(unsigned char _IndustryGroup) { DeviceInformation.IndustryGroupAndSystemInstance=(DeviceInformation.IndustryGroupAndSystemInstance&0x0f) | (_IndustryGroup<<4) | 0x80; } unsigned char GetIndustryGroup() const { return (DeviceInformation.IndustryGroupAndSystemInstance>>4) & 0x07; } void SetSystemInstance(unsigned char _SystemInstance) { DeviceInformation.IndustryGroupAndSystemInstance=(DeviceInformation.IndustryGroupAndSystemInstance&0xf0) | (_SystemInstance&0x0f); } unsigned char GetSystemInstance() const { return DeviceInformation.IndustryGroupAndSystemInstance&0x0f; } uint64_t GetName() const { return DeviceInformation.Name; } void SetName(uint64_t _Name) { DeviceInformation.Name=_Name; } inline bool IsSame(uint64_t Other) { return GetName()==Other; } }; class tDevice { protected: uint8_t Source; unsigned long CreateTime; tDeviceInformation DevI; public: tDevice(uint64_t _Name, uint8_t _Source=255) { Source=_Source; DevI.SetName(_Name); CreateTime=millis(); } virtual ~tDevice() {;} uint8_t GetSource() const { return Source; } unsigned long GetCreateTime() const { return CreateTime; } // Device information inline uint64_t GetName() const { return DevI.GetName(); } inline bool IsSame(uint64_t Other) { return DevI.IsSame(Other); } inline uint32_t GetUniqueNumber() const { return DevI.GetUniqueNumber(); } inline uint16_t GetManufacturerCode() const { return DevI.GetManufacturerCode(); } inline unsigned char GetDeviceInstance() const { return DevI.GetDeviceInstance(); } inline unsigned char GetDeviceInstanceLower() const { return DevI.GetDeviceInstanceLower(); } inline unsigned char GetDeviceInstanceUpper() const { return DevI.GetDeviceInstanceUpper(); } inline unsigned char GetDeviceFunction() const { return DevI.GetDeviceFunction(); } inline unsigned char GetDeviceClass() const { return DevI.GetDeviceClass(); } inline unsigned char GetIndustryGroup() const { return DevI.GetIndustryGroup(); } inline unsigned char GetSystemInstance() const { return DevI.GetSystemInstance(); } // Product information virtual unsigned short GetN2kVersion() const=0; virtual unsigned short GetProductCode() const=0; virtual const char * GetModelID() const=0; virtual const char * GetSwCode() const=0; virtual const char * GetModelVersion() const=0; virtual const char * GetModelSerialCode() const=0; virtual unsigned short GetCertificationLevel() const=0; virtual unsigned short GetLoadEquivalency() const=0; // Configuration information virtual const char * GetManufacturerInformation() const { return 0; } virtual const char * GetInstallationDescription1() const { return 0; } virtual const char * GetInstallationDescription2() const { return 0; } virtual const unsigned long * GetTransmitPGNs() const { return 0; } virtual const unsigned long * GetReceivePGNs() const { return 0; } }; class tMsgHandler { private: friend class tNMEA2000; tMsgHandler *pNext; tNMEA2000 *pNMEA2000; protected: unsigned long PGN; virtual void HandleMsg(const tN2kMsg &N2kMsg)=0; tNMEA2000 *GetNMEA2000() { return pNMEA2000; } public: tMsgHandler(unsigned long _PGN=0, tNMEA2000 *_pNMEA2000=0) { PGN=_PGN; pNext=0; pNMEA2000=0; if ( _pNMEA2000!=0 ) _pNMEA2000->AttachMsgHandler(this); } virtual ~tMsgHandler() { if ( pNMEA2000!=0 ) pNMEA2000->DetachMsgHandler(this); } inline unsigned long GetPGN() const { return PGN; } }; public: // Type how to forward messages in listen mode typedef enum { fwdt_Actisense, // Forwards messages to output port in Actisense format. Note that some Navigation sw uses this. fwdt_Text // Forwards messages to output port in clear text. This is e.g. for debuging. } tForwardType; // System mode. Meaning how it acts in NMEA2000 bus. typedef enum { N2km_ListenOnly, // Default mode. Listen bus and forwards messages to default port in Actisense format. You can not send any data to the bus. N2km_NodeOnly, // This is for devices, which only sends data to the bus e.g. RPM or temperature monitor. Remember to set right device information first. N2km_ListenAndNode, // In this mode, device can be e.g. temperature monitor and as N2km_ListenOnly. N2km_SendOnly, // Only for message sending. Device will not inform itself to the bus. Messages will not be forwarded to the stream. // By setting message handler, you can still read messages and handle them by yourself. N2km_ListenAndSend // Listen bus and forwards messages to default port in Actisense format. Messages can be send. Device will not inform itself to the bus. } tN2kMode; // For debugging we have some cases for SendMsg typedef enum {dm_None, // Directs data to CAN bus dm_ClearText, // Directs sended data to serial as clear text dm_Actisense, // Directs sended data to serial as Actisense format. } tDebugMode; struct tConfigurationInformation { const char *ManufacturerInformation; const char *InstallationDescription1; const char *InstallationDescription2; }; protected: class tInternalDevice { public: uint8_t N2kSource; tDeviceInformation DeviceInformation; // Product information const tProductInformation *ProductInformation; tProductInformation *LocalProductInformation; char *ManufacturerSerialCode; unsigned long PendingIsoAddressClaim; unsigned long PendingProductInformation; unsigned long PendingConfigurationInformation; unsigned long AddressClaimStarted; uint8_t AddressClaimEndSource; // Transmit and receive PGNs const unsigned long *TransmitMessages; const unsigned long *ReceiveMessages; // Fast packet PGNs sequence counters size_t MaxPGNSequenceCounters; unsigned long *PGNSequenceCounters; #if !defined(N2K_NO_ISO_MULTI_PACKET_SUPPORT) tN2kMsg PendingTPMsg; unsigned long NextDTSendTime; // Time, when next data packet can be send on TP broadcast uint8_t NextDTSequence; #endif #if !defined(N2K_NO_HEARTBEAT_SUPPORT) unsigned long HeartbeatInterval; unsigned long DefaultHeartbeatInterval; unsigned long NextHeartbeatSentTime; #endif public: tInternalDevice() { N2kSource=0; ProductInformation=0; LocalProductInformation=0; ManufacturerSerialCode=0; PendingIsoAddressClaim=0; PendingProductInformation=0; PendingConfigurationInformation=0; AddressClaimStarted=0; AddressClaimEndSource=N2kMaxCanBusAddress; //GetNextAddressFromBeginning=true; TransmitMessages=0; ReceiveMessages=0; MaxPGNSequenceCounters=0; PGNSequenceCounters=0; #if !defined(N2K_NO_ISO_MULTI_PACKET_SUPPORT) NextDTSendTime=0; NextDTSequence=0; #endif #if !defined(N2K_NO_HEARTBEAT_SUPPORT) HeartbeatInterval=60000; DefaultHeartbeatInterval=60000; NextHeartbeatSentTime=0; #endif } void SetPendingIsoAddressClaim(unsigned long FromNow=2) { PendingIsoAddressClaim=millis()+FromNow; } bool QueryPendingIsoAddressClaim() { return (PendingIsoAddressClaim?PendingIsoAddressClaim0 ) { AddressClaimEndSource--; } else { AddressClaimEndSource=N2kMaxCanBusAddress; } } }; protected: // Forward mode bit settings. static const int FwdModeBit_EnableForward = BIT(0); // If set, forward is enabled static const int FwdModeBit_SystemMessages = BIT(1); // System messages will be forwarded static const int FwdModeBit_OnlyKnownMessages = BIT(2); // Only known messages will be forwarded. System messages will be forwarded according its own bit. static const int FwdModeBit_OwnMessages = BIT(3); // Forward also all messages, what this device will send static const int HandleModeBit_OnlyKnownMessages = BIT(4); // Only known messages will be handled. protected: tDebugMode dbMode; // Default dm_None tN2kMode N2kMode; // Default N2km_ListenOnly. tForwardType ForwardType; // Default fwdt_Actisense. unsigned int ForwardMode; // Default all messages - also system and own. N2kStream *ForwardStream; tMsgHandler *MsgHandlers; bool DeviceReady; bool AddressChanged; bool DeviceInformationChanged; // Device information tInternalDevice *Devices; int DeviceCount; // unsigned long N2kSource[Max_N2kDevices]; // Configuration information char *LocalConfigurationInformationData; tConfigurationInformation ConfigurationInformation; const unsigned long *SingleFrameMessages[N2kMessageGroups]; const unsigned long *FastPacketMessages[N2kMessageGroups]; class tCANSendFrame { public: unsigned long id; unsigned char len; unsigned char buf[8]; bool wait_sent; public: void Clear() {id=0; len=0; for (int i=0; i<8; i++) { buf[i]=0; } } }; protected: // Buffer for received messages. tN2kCANMsg *N2kCANMsgBuf; uint8_t MaxN2kCANMsgs; tCANSendFrame *CANSendFrameBuf; uint16_t MaxCANSendFrames; uint16_t CANSendFrameBufferWrite; uint16_t CANSendFrameBufferRead; uint16_t MaxCANReceiveFrames; // Handler callbacks void (*MsgHandler)(const tN2kMsg &N2kMsg); // Normal messages bool (*ISORqstHandler)(unsigned long RequestedPGN, unsigned char Requester, int DeviceIndex); // 'ISORequest' messages #if !defined(N2K_NO_GROUP_FUNCTION_SUPPORT) tN2kGroupFunctionHandler *pGroupFunctionHandlers; #endif protected: // Virtual functions for different interfaces. Currently there are own classes // for Arduino due internal CAN (NMEA2000_due), external MCP2515 SPI CAN bus controller (NMEA2000_mcp), // Teensy FlexCAN (NMEA2000_Teensy), NMEA2000_avr for AVR, NMEA2000_mbed for MBED and NMEA2000_socketCAN for e.g. RPi. virtual bool CANSendFrame(unsigned long id, unsigned char len, const unsigned char *buf, bool wait_sent=true)=0; virtual bool CANOpen()=0; virtual bool CANGetFrame(unsigned long &id, unsigned char &len, unsigned char *buf)=0; // This will be called on Open() before any other initialization. Inherit this, if buffers can be set for the driver // and you want to change size of library send frame buffer size. See e.g. NMEA2000_teensy.cpp. virtual void InitCANFrameBuffers(); #if defined(DEBUG_NMEA2000_ISR) virtual void TestISR() {;} #endif protected: bool SendFrames(); // Sends pending frames bool SendFrame(unsigned long id, unsigned char len, const unsigned char *buf, bool wait_sent=true); tCANSendFrame *GetNextFreeCANSendFrame(); // Currently Product Information and Configuration Information will we pended on ISO request. // This is because specially for broadcasted response it may take a while, when higher priority // devices sends their response. void SendPendingInformation(); protected: void InitDevices(); bool IsInitialized() { return (N2kCANMsgBuf!=0); } #if !defined(N2K_NO_ISO_MULTI_PACKET_SUPPORT) void FindFreeCANMsgIndex(unsigned long PGN, unsigned char Source, unsigned char Destination, bool TPMsg, uint8_t &MsgIndex); #else void FindFreeCANMsgIndex(unsigned long PGN, unsigned char Source, unsigned char Destination, uint8_t &MsgIndex); #endif uint8_t SetN2kCANBufMsg(unsigned long canId, unsigned char len, unsigned char *buf); bool IsFastPacketPGN(unsigned long PGN); bool IsFastPacket(const tN2kMsg &N2kMsg); bool CheckKnownMessage(unsigned long PGN, bool &SystemMessage, bool &FastPacket); bool HandleReceivedSystemMessage(int MsgIndex); void ForwardMessage(const tN2kMsg &N2kMsg); void ForwardMessage(const tN2kCANMsg &N2kCanMsg); void RespondISORequest(const tN2kMsg &N2kMsg, unsigned long RequestedPGN, int iDev); void HandleISORequest(const tN2kMsg &N2kMsg); #if !defined(N2K_NO_GROUP_FUNCTION_SUPPORT) void RespondGroupFunction(const tN2kMsg &N2kMsg, tN2kGroupFunctionCode GroupFunctionCode, unsigned long PGNForGroupFunction, int iDev); void HandleGroupFunction(const tN2kMsg &N2kMsg); #endif void StartAddressClaim(int iDev); bool IsAddressClaimStarted(int iDev); void HandleISOAddressClaim(const tN2kMsg &N2kMsg); void HandleCommandedAddress(uint64_t CommandedName, unsigned char NewAddress, int iDev); void HandleCommandedAddress(const tN2kMsg &N2kMsg); void GetNextAddress(int DeviceIndex, bool RestartAtAnd=false); bool IsMySource(unsigned char Source); int FindSourceDeviceIndex(unsigned char Source); int GetSequenceCounter(unsigned long PGN, int iDev); size_t GetFastPacketTxPGNCount(int iDev); bool ForwardEnabled() const { return ((ForwardMode&FwdModeBit_EnableForward)>0 && (N2kMode!=N2km_SendOnly)); } bool ForwardSystemMessages() const { return ((ForwardMode&FwdModeBit_SystemMessages)>0); } bool ForwardOnlyKnownMessages() const { return ((ForwardMode&FwdModeBit_OnlyKnownMessages)>0); } bool ForwardOwnMessages() const { return ((ForwardMode&FwdModeBit_OwnMessages)>0); } bool HandleOnlyKnownMessages() const { return ((ForwardMode&HandleModeBit_OnlyKnownMessages)>0); } void RunMessageHandlers(const tN2kMsg &N2kMsg); bool HandleReceivedMessage(unsigned char Destination) { return (/* HandleMessagesToAnyDestination() */ true || tNMEA2000::IsBroadcast(Destination) || FindSourceDeviceIndex(Destination)>=0); } bool IsActiveNode() { return (N2kMode==N2km_NodeOnly || N2kMode==N2km_ListenAndNode); } bool IsValidDevice(int iDev) const { return (iDev>=0 && iDev=DeviceCount) return tDeviceInformation(); return Devices[iDev].DeviceInformation; } // Class handles automatically address claiming and tell to the bus about itself. void SendIsoAddressClaim(unsigned char Destination=0xff, int DeviceIndex=0, unsigned long delay=0); #if !defined(N2K_NO_ISO_MULTI_PACKET_SUPPORT) bool SendProductInformation(unsigned char Destination, int DeviceIndex, bool UseTP); bool SendConfigurationInformation(unsigned char Destination, int DeviceIndex, bool UseTP); void SendTxPGNList(unsigned char Destination, int DeviceIndex, bool UseTP=false); void SendRxPGNList(unsigned char Destination, int DeviceIndex, bool UseTP=false); #else void SendTxPGNList(unsigned char Destination, int DeviceIndex); void SendRxPGNList(unsigned char Destination, int DeviceIndex); #endif bool SendProductInformation(int DeviceIndex=0); bool SendConfigurationInformation(int DeviceIndex=0); #if !defined(N2K_NO_HEARTBEAT_SUPPORT) // According to document https://www.nmea.org/Assets/20140102%20nmea-2000-126993%20heartbeat%20pgn%20corrigendum.pdf // all NMEA devices shall transmit heartbeat PGN 126993. // With this function you can set transmission interval in ms (range 1000-655320 ms, default 60000). Set <1000 to disable it. // You can temporaly change interval by setting SetAsDefault parameter to false. Then you can restore default interval // with interval parameter value 0xfffffffe void SetHeartbeatInterval(unsigned long interval, bool SetAsDefault=true, int iDev=-1); // Heartbeat interval may be changed by e.g. MFD by group function. I have not yet found should changed value be saved // for next startup or not. unsigned long GetHeartbeatInterval(int iDev=0) { if (iDev<0 || iDev>=DeviceCount) return 60000; return Devices[iDev].HeartbeatInterval; } // Send heartbeat for specific device. void SendHeartbeat(int iDev); // Library will automatically send heartbeat, if interval is >0. You can also manually send it any time or force sent, if interval=0; void SendHeartbeat(bool force=false); #endif // Set this before open. You can not change mode after Open(). // Note that other than N2km_ListenOnly modes will automatically start initialization and address claim procedure. // You have to call ParseMessages() periodically to handle these procedures. // If you know your system, define source something other address you allready have on your bus. void SetMode(tN2kMode _N2kMode, uint8_t _N2kSource=15); // Set type how messages will be forwarded in listen mode. Defult is fwdt_Actisense void SetForwardType(tForwardType fwdType) { ForwardType=fwdType; } // Set the stream, where messages will be forwarded in listen mode. void SetForwardStream(N2kStream* _stream) { ForwardStream=_stream; } // You can call this. It will be called anyway automatically by ParseMessages(); bool Open(); // Generate N2k message e.g. by using N2kMessages.h and simply send it to the bus. bool SendMsg(const tN2kMsg &N2kMsg, int DeviceIndex=0); // Call this periodically to handle N2k messages. Note that even if you only send e.g. // temperature to the bus, you should call this so the code will automatically inform // abot itselt to others. void ParseMessages(); // Set the message handler for incoming N2kMessages. void SetMsgHandler(void (*_MsgHandler)(const tN2kMsg &N2kMsg)); // Old style - callback function pointer void AttachMsgHandler(tMsgHandler *_MsgHandler); void DetachMsgHandler(tMsgHandler *_MsgHandler); void SetISORqstHandler(bool(*ISORequestHandler)(unsigned long RequestedPGN, unsigned char Requester, int DeviceIndex)); // ISORequest messages #if !defined(N2K_NO_GROUP_FUNCTION_SUPPORT) void RemoveGroupFunctionHandler(tN2kGroupFunctionHandler *pGroupFunctionHandler); void AddGroupFunctionHandler(tN2kGroupFunctionHandler *pGroupFunctionHandler); #endif // Read address for current device. // Multidevice support is under construction. unsigned char GetN2kSource(int DeviceIndex=0) const { if (DeviceIndex>=0 && DeviceIndex