Search code examples
pythonnmeanmea2000

Cantools dbc file


I am trying to use cantools in python to decode a message. Every example I see is using a dbc file 'tests/files/dbc/motohawk.dbc' or 'path to dbc file' is listed. Do you have to purchase this file or its contents? If not, how do you get a file that will decode the NMEA2000 PGN's? Does Cantools come with one?

import cantools
from pprint import pprint

db = cantools.database.load_file('tests/files/dbc/motohawk.dbc')
db.messages
example_message = db.get_message_by_name('ExampleMessage')
pprint(example_message.signals)`

I am ultimately interested in parsing PGN's 127501/127502 for standard NMEA 2000 digital switching and not Maretron or CZone or Empribus.

I am using YachtDevices YDSC controller and Oceanic relay module for lighting. But would like an app to control things beside my Garmin chart plotter.

The cantools site (https://pypi.org/project/cantools/) has many examples but all reference the tests/files/dbc/motohawk.dbc file which for me doesn't exist.

So the question is two fold:

  1. where can I find an NMEA 2000 dbc file to decode the message data?
  2. How can you decode the raw data if no dbc file is available?

Solution

  • After a conversation with NMEA2000.org I found out that their database is needed in order to purchase an NMEA2000 dbc file from a 3rd party. The NMEA2000 database is very costly, thousands. So I decided to go in another direction and decode it myself.

    Using Python Can, the can message structure in the can lib works great. Using this github document on NMEA2000 PGN structures you can pretty much do what ever you like. Github can boat project.

    Debugging this issue further I was able to come up with a working proof of concept python script. This was designed on a Raspberry Pi 4 B with a PiCAN-M hat for NMEA2000 interfacing. You could sub the can0 init line for a serial port/USB and use ActiSense NGT-1.

    There are also button graphics needed, or just change them to colors for on/off. This was designed using standard NMEA2000 equipment, YachtDevices YDSC-04 (for switches) and Oceanic Systems relay module (for lights). It supports standard NMEA2000 digital switching and not the proprietary ones.

    This specifically addresses sending PGN127501 (switch status) and PGN127502 (switch state). It will also decode 127502 to update the switch state. The interface is only a graphical touch screen GUI for 8 switches. There is print debug statement(s) that will print all PGN numbers received, no data. PGN127501 isn't decoded in this proof of concept but the data structure is the same as 127502, so the code is there to do so.

    from tkinter import *
    import tkinter.font as fnt
    import array as arr 
    import can
    import time
    import os
    import threading 
    from bitstring import BitArray
    from can import Message
                
    def button_click(btn):
        if btn == 'b1':       
            if onoff[0]==1:
                button1.config(image=imgon)
                onoff[0]=2
                snd501(b'\xfd\xff') 
                snd502(b'\xfd\xff')        
            else:
                button1.config(image=imgoff)
                onoff[0]=1
                snd501(b'\xfc\xff')
                snd502(b'\xfc\xff')
        elif btn == 'b2':
            if onoff[1]==1:
                button2.config(image=imgon)
                onoff[1]=2
                snd501(b'\xf7\xff')
                snd502(b'\xf7\xff')
            else:
                button2.config(image=imgoff)
                onoff[1]=1
                snd501(b'\xf3\xff')
                snd502(b'\xf3\xff')
        elif btn == 'b3':      
            if onoff[2]==1:
                button3.config(image=imgon)
                onoff[2]=2
                snd501(b'\xdf\xff')  
                snd502(b'\xdf\xff')        
            else:
                button3.config(image=imgoff)
                onoff[2]=1
                snd501(b'\xcf\xff')
                snd502(b'\xcf\xff')
        elif btn == 'b4':
            if onoff[3]==1:
                button4.config(image=imgon)
                onoff[3]=2
                snd501(b'\x7f\xff')
                snd502(b'\x7f\xff')
            else:
                button4.config(image=imgoff)
                onoff[3]=1
                snd501(b'\x3f\xff')
                snd502(b'\x3f\xff')
        elif btn == 'b5':
            if onoff[4]==1:
                button5.config(image=imgon)
                onoff[4]=2
                snd501(b'\xff\xfd')
                snd502(b'\xff\xfd')
            else:
                button5.config(image=imgoff)
                onoff[4]=1
                snd501(b'\xff\xfc')
                snd502(b'\xff\xfc')
        elif btn == 'b6':
            if onoff[5]==1:
                button6.config(image=imgon)
                onoff[5]=2
                snd501(b'\xff\xf7') 
                snd502(b'\xff\xf7') 
            else:     
                button6.config(image=imgoff)
                onoff[5]=1
                snd501(b'\xff\xf3')
                snd502(b'\xff\xf3')
        elif btn == 'b7':
            if onoff[6]==1:
                button7.config(image=imgon)
                onoff[6]=2
                snd501(b'\xff\xdf')
                snd502(b'\xff\xdf')         
            else:
                button7.config(image=imgoff)
                onoff[6]=1
                snd501(b'\xff\xcf')
                snd502(b'\xff\xcf')
        elif btn == 'b8':
            if onoff[7]==1:
                button8.config(image=imgon)
                onoff[7]=2
                snd501(b'\xff\x7f')
                snd502(b'\xff\x7f')
            else:
                button8.config(image=imgoff)
                onoff[7]=1
                snd501(b'\xff\x3f')
                snd502(b'\xff\x3f')
    
    def snd501(BANKSTATUS):
        msg = Message(data=bytearray(BANK.to_bytes(1,'big') + BANKSTATUS + BLANK),
                        arbitration_id=PGN501,
                        dlc=8,
                        channel='can0',
                        timestamp=time.time(),
                        is_extended_id=True)
        bus.send(msg)
        print('PGN SENT: ' + PGN501 + ' ID: ' + str(msg.arbitration_id) + ' Data: ' + str(msg.data ))
    
    def snd502(BANKSTATUS):
        msg = Message(data=bytearray(BANK.to_bytes(1,'big') + BANKSTATUS + BLANK),
                        arbitration_id=PGN502,
                        dlc=8,
                        channel='can0',
                        timestamp=time.time(),
                        is_extended_id=True)
        bus.send(msg)
        print('PGN SENT: ' + PGN502 + ' ID: ' + str(msg.arbitration_id) + ' Data: ' + str(msg.data ))
    
    def getpgn(pgn): 
        pgn=bin(pgn) #Get bin of the id
        pgn = pgn[2:len(pgn)] # strip off the '0b'
        while len(pgn) < 29 : # make sure it's 29 bits
            pgn = '0' + pgn 
        pgn = pgn[3:-8] #get the middle 18 bits
        pgn = int(pgn, 2) #convert to int
        return pgn
    
    def handle_data(PGN, data):
        if PGN == 127502:
            msgData = data
            if getBank(msgData) == BANK:
                bankstatus = str(data)
                # print(bankstatus[18:-26])
                # print(bankstatus[22:-22])
                bankstatus1 = bankstatus[18:-26]
                SetBank1Value(bankstatus1)
                bankstatus2 = bankstatus[22:-22]
                SetBank2Value(bankstatus2)
        if PGN == 127501:
            msgData = data
            if getBank(msgData) == BANK:
                bankstatus = str(data)
                print(bank status)
    
    def SetBank1Value(bankstatus):
        if bankstatus != 'ff':
            if bankstatus == 'fd':
                button1.config(image=imgon)
                onoff[0]=2
            elif bankstatus == 'fc':
                button1.config(image=imgoff)
                onoff[0]=1
            elif bankstatus == 'f7':
                button2.config(image=imgon)
                onoff[1]=2
            elif bankstatus == 'f3':
                button2.config(image=imgoff)
                onoff[1]=1               
            elif bankstatus == 'df':
                button3.config(image=imgon)
                onoff[2]=2
            elif bankstatus == 'cf':
                button3.config(image=imgoff)
                onoff[2]=1
            elif bankstatus == '7f':
                button3.config(image=imgon)
                onoff[3]=2
            elif bankstatus == '3f':
                button3.config(image=imgoff)
                onoff[3]=1
        
    def SetBank2Value(bankstatus):
        if bankstatus != 'ff':
            if bankstatus == 'fd':
                button5.config(image=imgon)
                onoff[4]=2
            elif bankstatus == 'fc':
                button5.config(image=imgoff)
                onoff[4]=1
            elif bankstatus == 'f7':
                button6.config(image=imgon)
                onoff[5]=2
            elif bankstatus == 'f3':
                button6.config(image=imgoff)
                onoff[5]=1               
            elif bankstatus == 'df':
                button7.config(image=imgon)
                onoff[6]=2
            elif bankstatus == 'cf':
                button7.config(image=imgoff)
                onoff[6]=1
            elif bankstatus == '7f':
                button8.config(image=imgon)
                onoff[7]=2
            elif bankstatus == '3f':
                button8.config(image=imgoff)
                onoff[7]=1
    
    def getBank(data):
        try:
            bank = str(data)
            bank = bank[13:-30]
            bank = int('0' + bank, 0)
            return bank
        except:
            return 999 #return an non exisant number
    
    def read_from_port():
        connected = False
        try:
            while not connected:
                message = bus.recv()
                for msg in bus:
                    PGN = getpgn(msg.arbitration_id)
                    if PGN == 127501 or PGN == 127502 : 
                        print(str(PGN) + " : " + str(msg.arbitration_id) + " : " + str(msg.data ))
                        handle_data(PGN, msg.data)
                    else:
                        print(str(PGN) + " : " + str(msg.arbitration_id) + " : " + str(msg.data ))
        except:
            exit()
    
        connected = True
    
    def exit_app():
            connected = True
            bus.shutdown()
            time.sleep(0.3)
            os._exit
            time.sleep(0.1)
            app.destroy()
            exit()
        
    app = Tk() 
    
    # needed to be able to shutdown port
    connected = False
    
    PGN501 = 233966978
    PGN502 = 233967275
    
    # Switch Bank var's
    BANK = 12 # BANK.to_bytes(1,'big') 
    BLANK = bytearray(b'\xff\xff\xff\xff\xff')
    
    # Switches 1-->8
    #lower nibble of first byte is 2,1 upper nibble is 4,3
    #lower nibble of 2nd byte is 6,5 upper nibble is 8,7
    #the mask to leave a byte unchanged is ff or f if a nibble.
    # BANKSTATUS = bytearray(b'\xff\xff')
    
    os.system("sudo /sbin/ip link set can0 up type can bitrate 500000")
    time.sleep(0.1) 
    
    app.title("Reel Nauti Switching") 
    #app.geometry("1920x1080") #Window dimensions
    
    app.geometry("1200x700") #Window dimensions
    app.configure(background='black')
    
    #image size is 202x315 XxY
    imgblank = PhotoImage(file = r"./images/blank.png")
    imgon = PhotoImage(file = r"./images/on.png")
    imgoff = PhotoImage(file = r"./images/off.png")
    
    spacer = 20
    btnw = 202
    btnh = 315
    
    PGN = any
    
    # define new font
    newfont = fnt.Font(family='Arial', size=22, weight=fnt.BOLD)
    
    # internal button state array 0=uninitated, 1=off, 2=on
    onoff = arr.array('i',[0,0,0,0,0,0,0,0]) # array starts at 0 using 0-7
    
    # Create button 1
    button1 = Button(app, text="Engine\nRoom\nLights", image=imgblank, borderwidth=0, font=newfont, compound="center",
                     command=lambda m="b1":button_click(m), cursor="hand2",fg='white', bg='black', activebackground="black",
                     activeforeground="white", highlightbackground="black", highlightcolor="green", highlightthickness=0)
    button1.pack() 
    button1.place(x=spacer , y=spacer)
    
    # Create button 2
    button2 = Button(app, text="Courtesy\nLights", image=imgblank, borderwidth=0, font=newfont, compound="center",
                     command=lambda m="b2":button_click(m), cursor="hand2",fg='white', bg='black', activebackground="black",
                     activeforeground="white", highlightbackground="black", highlightcolor="green", highlightthickness=0,)
    button2.pack() 
    button2.place(x=btnw + (2 * spacer) , y=spacer)
    
    # Create button 3
    button3 = Button(app, text="Spreader\nLights", image=imgblank, borderwidth=0, font=newfont, compound="center",
                     command=lambda m="b3":button_click(m), cursor="hand2",fg='white', bg='black', activebackground="black",
                     activeforeground="white", highlightbackground="black", highlightcolor="green", highlightthickness=0,)
    button3.pack() 
    button3.place(x=(2 *btnw) + (3 * spacer) , y=spacer)
    
    # Create button 4
    button4 = Button(app, text="Under\nWater\nLights", image=imgblank, borderwidth=0, font=newfont, compound="center",
                     command=lambda m="b4":button_click(m), cursor="hand2",fg='white', bg='black', activebackground="black",
                     activeforeground="white", highlightbackground="black", highlightcolor="green", highlightthickness=0,)
    button4.pack() 
    button4.place(x=(3 * btnw) + (4 * spacer) , y=spacer)
    
    # Create button 5 on second row
    button5 = Button(app, text="Arch\nLights", image=imgblank, borderwidth=0, font=newfont, compound="center",
                     command=lambda m="b5":button_click(m), cursor="hand2",fg='white', bg='black', activebackground="black",
                     activeforeground="white", highlightbackground="black", highlightcolor="green", highlightthickness=0,)
    button5.pack() 
    button5.place(x=spacer, y=btnh + (spacer * 2))
    
    # Create button 6 on second row
    button6 = Button(app, text="Dash\nLights", image=imgblank, borderwidth=0, font=newfont, compound="center",
                     command=lambda m="b6":button_click(m), cursor="hand2",fg='white', bg='black', activebackground="black",
                     activeforeground="white", highlightbackground="black", highlightcolor="green", highlightthickness=0,)
    button6.pack() 
    button6.place(x= btnw + (2 * spacer) , y=btnh + (spacer * 2))
    
    # Create button 7 on second row
    button7 = Button(app, text="Cockpit\nLights", image=imgblank, borderwidth=0, font=newfont, compound="center",
                     command=lambda m="b7":button_click(m), cursor="hand2",fg='white', bg='black', activebackground="black",
                     activeforeground="white", highlightbackground="black", highlightcolor="green", highlightthickness=0,)
    button7.pack() 
    button7.place(x= (2 * btnw) + (3 * spacer) , y=btnh + (spacer * 2))
    
    # Create button 8 on second row
    button8 = Button(app, text="Salon\nHi-Hats", image=imgblank, borderwidth=0, font=newfont, compound="center",
                     command=lambda m="b8":button_click(m), cursor="hand2",fg='white', bg='black', activebackground="black",
                     activeforeground="white", highlightbackground="black", highlightcolor="green", highlightthickness=0,)
    button8.pack() 
    button8.place(x= (3 * btnw) + (4 * spacer) , y=btnh + (spacer * 2))
    
    # Create exit button - where to put it?
    button9 = Button(app, text="Exit", command=exit_app)
    button9.pack() 
    button9.place(x= (4 * btnw) + (5 * spacer) , y=btnh + (spacer * 2))
    
    connected = False
    
    try:
        bus = can.interface.Bus(channel='can0', bustype='socketcan')
        print('Ready')
    except:
        print('Init of PICAN failed')
    
    # thread to handle the can port
    thread = threading.Thread(target=read_from_port)
    thread.start()
    
    # Mainloop that will run forever 
    app.mainloop()