#******************************************************************************
# PROGRAM NAME: NMEA 2000 Fuel Pressure alarm
#
# AUTHOR: Korolev Alex, neko-koneko@yandex.ru
#
# COPYRIGHT: Yacht Devices LTD, 2025-2026
# 
# PROGRAM DESCRIPTION: 
#
# Program monitors YDPG-01 device GPIO input state 
# and if YDPG-01 RX- (Blue) wire is connected to GND (clear),
# triggers NMEA 2000 engine alarm 
# by sending PGN 127489 "Engine Parameters, Dynamic" (PORT engine)
# with "Engine Discrete Status 1" data field 
# 	= b 0000 0000 0001 0000 = 0x10 = Low Fuel Pressure flag
# with 500 ms interval.
# All other data fields equals
# to last received PGN 127489 "Engine Parameters, Dynamic" 
# from the PORT engine gateway.
#
# If PORT engine gateway goes offline, sends PGN 127489
# with all data fields = n/a
#
# Also can be set up to repeat PORT engine data 
# - PGN 127488 "Engine Parameters, Rapid Update"
# - PGN 127493 "Transmission Parameters, Dynamic"
# - PGN 127489 "Engine Parameters, Dynamic"
# but from YDPG-01 own address.
#
# This allows to select YDPG-01 as a PORT engine data source 
# and get alarm + engine data on plotters that support and require 
# an explicit engine data source selection at NMEA 2000 device (64-bit NAME UID) level
#
#
# Tested with: 
# - Raymarine Axiom LightHouse 4.9.330
# - Simrad GO XSR software 22.3
# - Garmin Echomap 62cv
#
#------------------------------------------------------------------------------

#******************************************************************************
# VERSION CONTROL SECTION 
#
# v 1.04 18.09.2026
# added exception handler on CAN transmission failure
#
# v 1.03 27.11.2025
# fixed CPU resources usage, causing too slow processing of 
# PGN 127489 "Engine Parameters, Dynamic":
# pin polling changed to IRQ handler
#
# v 1.02 25.11.2025
# added forwarding of PGN 127493 "Transmission Parameters, Dynamic"
#
# v 1.01b 24.11.2025
# Simrad GO and Garmin support
#
# v 1.00b 13.11.2025
# initial release
#
#------------------------------------------------------------------------------

#******************************************************************************
# HARDWARE: Yacht Devices YDPG-01 Python gateway, f/w 1.05
#
#	To install the Program:
#	- connect YDPG-01 to PC using USB cable
#	- YDPG-01 will be recognised as a USB disk external storage
#	- replace main.py in the YDPG-01 disk root folder
#
#	To debug: 
#	- connect YDPG-01 to PC using USB cable
#	- YDPG-01 will also be recognised as a USB SERIAL port
#   - open YDPG-01 serial port in console (e.g. PUTTY) and see debug info
#	NB: console: 
#		CTRL+D to restart Program 
#		CTRL+C to stop Program
#
#-------------------------------------------------------------------------------
# EXTERNAL HARDWARE: 
#
# 1) any control panel with alarm relay 
# 	with dry NO contacts, contacts open when no alarm
#	contacts closed when alarm triggered
#
#	connect YDPG-01 RX- (blue) to panel alarm relay NC
#	connect YDPG-01 GND (clear/shield) to panel alarm relay COM
#
# 2) 	Raymarine AXIOM MFD with LightHouse 4.9.330
#			OR	
#		Simrad GO XSR software 22.3
#			OR
#		Garmin MFD
#
#------------------------------------------------------------------------------
# MFD SETUP: 
#
# a) AXIOM: works out of the box, no extra setup required
#
# !!! set MFD_MODE = MFD_AXIOM in "USER SETTINGS SECTION" below !!!
#------------------------------------------------------------------------------
#
# b) any other MFD - that supports engine data source selection 
# by NMEA 2000 device NAME UID:
#
# !!! set MFD_MODE = MFD_NAVICO in "USER SETTINGS SECTION" below !!!
#
# - PORT engine ON
# - PORT engine gateway online
# - make sure you got PORT engine data on MFD (from gateway)
# - select YDPG-01 as PORT engine data source device
# - make sure you got PORT engine data on MFD (from YDPG-01)
#------------------------------------------------------------------------------
#
# c) any other MFD - that does NOT support engine data source selection 
# by NMEA 2000 device NAME UID:
#
# - add our YDNB-07 Bridge to combine PGNs 127489 "Engine Parameters, Dynamic"
# 	from YDPG-01 and PORT engine gateway
#------------------------------------------------------------------------------


#******************************************************************************
# L I C E N S E   I N F O R M A T I O N
#
# This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program.  If not, see <https://www.gnu.org/licenses/>.
#
#------------------------------------------------------------------------------


#******************************************************************************
# IMPORT SECTION
import machine
import utime
#------------------------------------------------------------------------------

#******************************************************************************
# HARDWARE INIT SECTION

# we do not need NMEA 0183 library
try:
	n0183.deinit()
except(NameError, AttributeError):
	pass
	
# we do not need UART
try:
	uart_rx.deinit()
except(NameError, AttributeError):
	pass

# configure "RX-" (blue) as GPIO input
# returns 0 if "RX-" is connected to GND
# returns 1 if "RX-" floats
pin = machine.Pin("rx", machine.Pin.IN)

#------------------------------------------------------------------------------

#******************************************************************************
# VARIABLES SECTION

# global variable that reflects alarm state, True = Alarm active
alarm_state = False

# track PGN 127489 "Engine Parameters, Dynamic", PORT engine
# last received timestamp
PGN_127489_PORT_rx_time = utime.ticks_ms() 

# current PGN 127489 sending mode
# 0 = FOLLOW 
# 	send modified PGN 127489 "Engine Parameters, Dynamic" to NMEA 2000
# 	each time after PORT engine gateway sends it
# 1 = AUTONOMOUS
#	send empty PGN 127489 "Engine Parameters, Dynamic" to NMEA 2000
#	when there is no other sources of PGN 127489 detected for 1500 ms
#	e.g. when OG engine gateway [ PGN 127489 source ] is offline
MODE_FOLLOW = 0
MODE_AUTONOMOUS = 1

# default mode FOLLOW
ENGINE_PGNS_PORT_TX_MODE = MODE_FOLLOW

# MFD algo
MFD_AXIOM = 1
MFD_NAVICO = 2

# YDPG own N2K address
YDPG_ADDRESS = n2k.address()

# constantly incrementing generic use counter, resets on value 1000
main_timer_counter = 0

# track when pin was triggered
pin_triggered_timestamp_ms = utime.ticks_ms() 
# and into which state, default = 1, no alarm
pin_triggered_value = 1

#------------------------------------------------------------------------------

#******************************************************************************
# USER SETTINGS SECTION

# PGN repeat mode

# MFD_AXIOM - set if you got Axiom only. Saves on bus traffic.
# MFD_NAVICO - set if you got Navico (Simrad, Lowrance, B&G) or Garmin MFD

MFD_MODE = MFD_AXIOM

#------------------------------------------------------------------------------

#******************************************************************************
# FUNCTIONS SECTION

#------------------------------------------------------------------------------
# processing of received PGN 127488 "Engine Parameters, Rapid Update"
def rx_127488(n2k, data, msgid, seq=-1):

	global YDPG_ADDRESS, alarm_state

	# copy entire assembled payload into mutable bytearray
	assembled_payload = bytearray(data)
	
	# which engine ?
	engine_instance = int.from_bytes( assembled_payload[ 0 : 1 ], 'little', False)
	
	# PORT engine ?
	if engine_instance == 0:

		# send copy of 127488 "Engine Parameters, Rapid Update" PGN back to NMEA 2000
		try:
			n2k.sendpgn(assembled_payload, 127488, src = YDPG_ADDRESS)
		except:
			pass		
#------------------------------------------------------------------------------

#------------------------------------------------------------------------------
# processing of received PGN 127493 "Transmission Parameters, Dynamic"
def rx_127493(n2k, data, msgid, seq=-1):

	global YDPG_ADDRESS, alarm_state

	# copy entire assembled payload into mutable bytearray
	assembled_payload = bytearray(data)
	
	# which engine ?
	engine_instance = int.from_bytes( assembled_payload[ 0 : 1 ], 'little', False)
	
	# PORT engine ?
	if engine_instance == 0:

		# send copy of PGN 127493 "Transmission Parameters, Dynamic" PGN back to NMEA 2000
		try:
			n2k.sendpgn(assembled_payload, 127493, src = YDPG_ADDRESS)
		except:
			pass		
#------------------------------------------------------------------------------

#------------------------------------------------------------------------------
# processing of received PGN 127489 "Engine Parameters, Dynamic"
def rx_127489(n2k, data, msgid, seq=-1):
	
	global PGN_127489_PORT_rx_time, ENGINE_PGNS_PORT_TX_MODE, YDPG_ADDRESS, alarm_state

	# copy entire assembled payload into mutable bytearray
	assembled_payload = bytearray(data)
	
	# which engine ?
	engine_instance = int.from_bytes( assembled_payload[ 0 : 1 ], 'little', False)
	
	# PORT engine ?
	if engine_instance == 0:
		
		# remember when PGN was received
		PGN_127489_PORT_rx_time = utime.ticks_ms() 

		if alarm_state:
			
			# read original PORT engine gateway "engine status 1" flags
			# and convert to unsigned 16-bit integer
			position = 20
			eng_flags_1 = int.from_bytes( assembled_payload[ position : position + 2 ], 'little', False)

			# need to retrigger alarm on Axiom ?
			if ENGINE_PGNS_PORT_TX_MODE == MODE_AUTONOMOUS:
				ENGINE_PGNS_PORT_TX_MODE = MODE_FOLLOW
				
				# clear "Low Fuel Pressure" flag
				new_eng_flags_1 = eng_flags_1 & (~0x10)
				print ("FOLLOW RETRIGGER: send modified PGN 127489 copy - ALARM")
				
			elif ENGINE_PGNS_PORT_TX_MODE == MODE_FOLLOW:
				# set "Low Fuel Pressure" flag
				new_eng_flags_1 = eng_flags_1 | 0x10
				print ("FOLLOW: send modified PGN 127489 copy - ALARM")

			
			# convert unsigned 16-bit integer back to string
			new_eng_flags_1_str = new_eng_flags_1.to_bytes(2, 'little', False)

			# convert PGN payload string to array 
			data_array = bytearray(assembled_payload)
			
			# set "engine status 1" flags
			data_array[ position : position + 2 ] = new_eng_flags_1_str
			
			# send modified PGN 127489 "Engine Parameters, Dynamic" back to NMEA 2000
			try:
				n2k.sendpgn(data_array, 127489, src = YDPG_ADDRESS)
			except:
				pass
		
		else: # no alarm
			
			if MFD_MODE == MFD_NAVICO:
				
				ENGINE_PGNS_PORT_TX_MODE = MODE_FOLLOW
				
				print ("FOLLOW: send PGN 127489 copy")
				
				# send unmodified PGN 127489 "Engine Parameters, Dynamic" back to NMEA 2000
				try:
					n2k.sendpgn(assembled_payload, 127489, src = YDPG_ADDRESS)
				except:
					pass	
#------------------------------------------------------------------------------

#------------------------------------------------------------------------------
# Timer thread, called each 10 ms 
def main_timer_callback(timer):
	
	global main_timer_counter
	
	global ENGINE_PGNS_PORT_TX_MODE, PGN_127489_PORT_rx_time, YDPG_ADDRESS, alarm_state
	
	global pin_triggered_timestamp_ms, pin_triggered_value

	# main timer loop incrementing counter
	main_timer_counter = main_timer_counter + 1
	
	# software debounce:
	# if pin held low for more than 100 ms, trigger alarm
	# if pin goes high, cancel alarm immediately
	now_ms = utime.ticks_ms()
	if utime.ticks_diff( now_ms, pin_triggered_timestamp_ms ) > 100:
		if pin_triggered_value == 0:
			alarm_state = True
		else: 
			alarm_state = False
	else:
		alarm_state = False

	# each 500 ms
	if (main_timer_counter % 50 == 0 ):

		if alarm_state:
		
			# should we send our own fake PGN 127489 "Engine Parameters, Dynamic"
			# or send a copy of PGN 127489 from the PORT engine gateway ?
			if ENGINE_PGNS_PORT_TX_MODE == MODE_FOLLOW:

				# if no PGN 127489 from the PORT engine gateway was received for more than 4500 ms
				# Axiom's engine GUI timeouts
				# and engine alarm can be retriggered

				now_ms = utime.ticks_ms()  

				if utime.ticks_diff( now_ms, PGN_127489_PORT_rx_time ) > 4500:
					# fall back to autonomous mode
					ENGINE_PGNS_PORT_TX_MODE = MODE_AUTONOMOUS
					
					print ("AUTONOMOUS RETRIGGER: send fake PGN 127489 - NO ALARM")
					
					# send fake PGN 127489 "Engine Parameters, Dynamic"
				
					# all fields n/a
					# except 
					# "Engine Instance" = 0 PORT; 
					# "engine status 1" = 0 = no alarm
					# 						| eng. instance = 0 PORT | 	oil pressure |	oil temp 	|	eng. temp	|	alternator V |	fuel rate	|	eng hours			|	clnt pressure	|	fuel pressure	|	RSVD	|	STATUS1		| 	STATUS2		|	load%	| 	torq%
					data_array = bytearray(( 	0x00,					0xFF,0xFF, 		0xFF,0xFF, 		0xFF,0xFF, 		0xFF,0x7F, 		0xFF,0x7F, 		0xFF,0xFF,0xFF,0xFF,	0xFF,0xFF,			0xFF,0xFF,			0xFF,		0x00, 0x00,		0x00,0x00,		0x7F,		0x7F	))
					
					try:
						n2k.sendpgn(data_array, 127489, src = YDPG_ADDRESS)	
					except:
						pass
			
			# are we in autonomous mode ?
			elif ENGINE_PGNS_PORT_TX_MODE == MODE_AUTONOMOUS:
			
				print ("AUTONOMOUS: send fake PGN 127489 - ALARM")
				
				# send fake PGN 127489 "Engine Parameters, Dynamic"
				
				# all fields n/a
				# except 
				# "Engine Instance" = 0 PORT; 
				# "engine status 1" = with "Low Fuel Pressure" flag set
				# 						| eng. instance = 0 PORT | 	oil pressure |	oil temp 	|	eng. temp	|	alternator V |	fuel rate	|	eng hours			|	clnt pressure	|	fuel pressure	|	RSVD	|	STATUS1		| 	STATUS2		|	load%	| 	torq%
				data_array = bytearray(( 	0x00,					0xFF,0xFF, 		0xFF,0xFF, 		0xFF,0xFF, 		0xFF,0x7F, 		0xFF,0x7F, 		0xFF,0xFF,0xFF,0xFF,	0xFF,0xFF,			0xFF,0xFF,			0xFF,		0x10, 0x00,		0x00,0x00,		0x7F,		0x7F	))
				
				try:
					n2k.sendpgn(data_array, 127489, src = YDPG_ADDRESS)
				except:
					pass
		
		else: # no alarm
			
			if MFD_MODE == MFD_NAVICO:
			
				if ENGINE_PGNS_PORT_TX_MODE == MODE_AUTONOMOUS:
					
					print ("AUTONOMOUS: send fake PGN 127489 - NO ALARM")
					
					# send fake PGN 127489 "Engine Parameters, Dynamic"
				
					# all fields n/a
					# except 
					# "Engine Instance" = 0 PORT; 
					# "engine status 1" = 0 = no alarm
					# 						| eng. instance = 0 PORT | 	oil pressure |	oil temp 	|	eng. temp	|	alternator V |	fuel rate	|	eng hours			|	clnt pressure	|	fuel pressure	|	RSVD	|	STATUS1		| 	STATUS2		|	load%	| 	torq%
					data_array = bytearray(( 	0x00,					0xFF,0xFF, 		0xFF,0xFF, 		0xFF,0xFF, 		0xFF,0x7F, 		0xFF,0x7F, 		0xFF,0xFF,0xFF,0xFF,	0xFF,0xFF,			0xFF,0xFF,			0xFF,		0x00, 0x00,		0x00,0x00,		0x7F,		0x7F	))
					
					try:
						n2k.sendpgn(data_array, 127489, src = YDPG_ADDRESS)		
					except:
						pass

					ENGINE_PGNS_PORT_TX_MODE = MODE_FOLLOW
		
	# counter overflow and reset 
	if main_timer_counter >= 1000:
		main_timer_counter = 0
#------------------------------------------------------------------------------

#------------------------------------------------------------------------------
# process pin state change
def pin_callback(pin_):

	global pin_triggered_timestamp_ms, pin_triggered_value
	
	print ( "pin state change:", pin_.value() )
	
	pin_triggered_timestamp_ms = utime.ticks_ms()
	pin_triggered_value = pin_.value()
		
#------------------------------------------------------------------------------

# FUNCTIONS SECTION ENDS
#------------------------------------------------------------------------------


###############################################################################
# MAIN PROGRAM

# set up interception of PGN 127489 "Engine Parameters, Dynamic"
n2k.rxcallback(0, rx_127489, 127489)

if MFD_MODE != MFD_AXIOM:
	# set up interception of PGN 127488 "Engine Parameters, Rapid Update"
	n2k.rxcallback(1, rx_127488, 127488)
	
	# set up interception of PGN 127493 "Transmission Parameters, Dynamic"
	n2k.rxcallback(2, rx_127493, 127493)
	
# set up main timer, calling "main_timer_callback" each 10 ms
main_timer = machine.Timer(period = 10, callback = main_timer_callback)

# set up pin IRQ handler
pin.irq(handler=pin_callback)

# MAIN PROGRAM ENDS
#------------------------------------------------------------------------------
#EOF
