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:
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()