#******************************************************************************
# PROGRAM NAME: NMEA 2000 Garmin Generic NMEA 2000 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 generic NMEA 2000 alarm 
# by sending PGNs
# - 126983 [ 0x1F007 ] "Alert"
# - 126985 [ 0x1F009 ] "Alert Text"
#
#------------------------------------------------------------------------------

#******************************************************************************
# VERSION CONTROL SECTION 
#
# v 1.00 07.10.2026
# 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) Garmin MFD that supports PGNs 126983 "Alert" and 126985 "Alert Text"
#
#------------------------------------------------------------------------------


#******************************************************************************
# 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

# same for Alert PGN tracking
alert_PGNs_sent = False

# 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

# increments each time on next alert
# valid range: 0..252
alert_occurence_number = 0

alert_temp_silence_status = False
alert_ack_status = False
alert_escalation_status = False
alert_temp_silence_support = False

alert_ack_support = False
alert_escalation_support = False

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

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

#------------------------------------------------------------------------------
# YDPG-01 N2K ISO stack config:

# N2K Device Class
# 120 = Display
YDPG_CLASS=120

# N2K Device Function
# 140 = Alarm Enunciator
YDPG_FUNCTION=140

#------------------------------------------------------------------------------
# N2K ALERT config:

# 0..15
# 1 = Emergency Alarm [ red ]
# 2 = (Regular) Alarm [ red ]
# 5 = Warning [ yellow ]
# 8 = Caution [ blue ]
ALERT_TYPE = 1

# 0..15
# 0 = Navigational
# 1 = Technical
ALERT_CATEGORY = 1

# 0..252
ALERT_SYSTEM = 0

# 0..252
ALERT_SUBSYSTEM = 0

# 0..65532
ALERT_ID = 0

# use Little-Endian byte order here
# for example, if target device Address Claim ( D0 -> D7 order ) is F6 87 AA 59 00 B4 46 C0
# write bytes in reverse order:
ALERT_SRC_NAME = 0xC046B40059AA87F6

# Device Instance or Data Instance of dta source that is monitored
# 0..255
# 253 = RSRVD
# 254 = ERR
# 255 = N/A
ALERT_DATA_SRC_INSTANCE = 0

# index into a set of data associated with the same DAta Source Instance 
# 0..255
# 253 = RSRVD
# 254 = ERR
# 255 = N/A
ALERT_DATA_SRC_INDEX = 0

ALERT_ACK_SOURCE_NAME = 0xFFFFFFFFFFFFFFFF

# 0..15
# 0 = manual
# 1 = auto
# 2 = test
# 3  = disabled
# 13 = RSRVD
# 14 = RANGE
# 15 = N/A
ALERT_TRIGGER_CONDITION = 1

# 0..15
# 0 = normal
# 1 = threshold exceeded
# 2 = extreme threshold exceeded
# 2 = low threshold exceeded
# 4 = extreme low threshold exceeded
# 13 = RSRVD
# 14 = RANGE
# 15 = N/A
ALERT_THRESHOLD_STATUS = 0


# 0..255
ALERT_PRIORITY = 0

# 0..255
# 0 = disabled
# 1 = normal
# 2 = active
# 3 = silenced
# 4 = acknowledged
# 5 = awaiting ack
# 253 = RSRVD
# 254 = ERR
# 255 = N/A
ALERT_STATE = 2


# 0..252
# 0 = EN US
# 1 = EN UK
# ... tbd
# 17 = RU
# ... tbd
# 19 = SW
ALERT_TEXT_LANG_ID = 0

# put any text, for UTF-8 make sure main.py is saved in UTF-8
# max len is 0..250 ASCII or 0..125 UNICODE characteers
ALERT_TEXT_DESCR_STR = "Bränslefiltret är igensatt!"

ALERT_TEXT_LOCATION_STR = "KTI System"

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

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


#------------------------------------------------------------------------------
# Timer thread, called each 10 ms 
def main_timer_callback(timer):
	
	global main_timer_counter
	
	global YDPG_ADDRESS, alarm_state, alert_PGNs_sent, alert_occurence_number
	
	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
			alert_PGNs_sent = False
	else:
		alarm_state = False
		alert_PGNs_sent = False


	if alarm_state:
	
		if not alert_PGNs_sent:
	
			print ("send Alarm PGN")
			
			payload = bytearray(28)
			
			# Field 1 & 2: Type (4 bits) + Category (4 bits) packed into 1 byte
			payload[0] = (ALERT_TYPE & 0x0F) | ((ALERT_CATEGORY & 0x0F) << 4)
			
			# Field 3 & 4: System (1 byte) + Subsystem (1 byte)
			payload[1] = ALERT_SYSTEM & 0xFF
			payload[2] = ALERT_SUBSYSTEM & 0xFF
			
			# Field 5: Alert ID (2 bytes, Little-Endian)
			payload[3:5] = ALERT_ID.to_bytes(2, "little")
			
			# Field 6: Data Source NAME (8 bytes, Little-Endian)
			payload[5:13] = ALERT_SRC_NAME.to_bytes(8, "little")
			
			# Field 7 & 8: Data Source Instance & Index (1 byte each)
			payload[13] = ALERT_DATA_SRC_INSTANCE & 0xFF
			payload[14] = ALERT_DATA_SRC_INDEX & 0xFF
			
			# Field 9: Alert Occurrence Number (1 byte, default 0x01 or 0xFF if N/A)
			payload[15] = alert_occurence_number
			
			# Field 10-13: Status bitfield flags (Defaulting to 0xFF / Not Available)
			payload[16] = 0x00 
			
			# Field 17: Acknowledge Source NAME (8 bytes, Little-Endian)
			payload[17:25] = ALERT_ACK_SOURCE_NAME.to_bytes(8, "little")
			
			# Field 18 & 19: Now properly using your config variables instead of hardcoded 15s
			payload[25] = (ALERT_TRIGGER_CONDITION & 0x0F) | ((ALERT_THRESHOLD_STATUS & 0x0F) << 4)
			
			# Field 20 & 21: Swapped to put Priority first, then State
			payload[26] = ALERT_PRIORITY & 0xFF
			payload[27] = ALERT_STATE & 0xFF
			
			try:
				# Transmit the completed 29-byte Fast-Packet payload to the gateway pipeline
				n2k.sendpgn(payload, 126983, src = YDPG_ADDRESS)
			except Exception:
				pass
				
				
			# Allocate exactly 16 bytes for the complete structural header
			header = bytearray(16)
			
			# Field 1 & 2: Alert Type (4 bits) + Alert Category (4 bits)
			header[0] = (ALERT_TYPE & 0x0F) | ((ALERT_CATEGORY & 0x0F) << 4)
			
			# Field 3 & 4: Alert System (1 byte) + Alert Sub-System (1 byte)
			header[1] = ALERT_SYSTEM & 0xFF
			header[2] = ALERT_SUBSYSTEM & 0xFF
			
			# Field 5: Alert ID (2 bytes, Little-Endian)
			header[3:5] = ALERT_ID.to_bytes(2, "little")
			
			# Field 6: Data Source Network ID NAME (8 bytes, Little-Endian)
			header[5:13] = ALERT_SRC_NAME.to_bytes(8, "little")
			
			# Field 7: Data Source Instance (1 byte)
			header[13] = ALERT_DATA_SRC_INSTANCE & 0xFF
			
			# Field 8: Data Source Index / Source (1 byte)
			header[14] = ALERT_DATA_SRC_INDEX & 0xFF
			
			# Field 9: Alert Occurrence Number (1 byte)
			# (Setting to 0 to match your log trace parameter)
			header[15] = alert_occurence_number
			
			# Field 10: Language ID (1 byte)
			lang_id_byte = bytes([ALERT_TEXT_LANG_ID & 0xFF])
			
			# Encode text components using your custom MicroPython string logic
			descr_bytes = encode_n2k_string(ALERT_TEXT_DESCR_STR)
			loc_bytes = encode_n2k_string(ALERT_TEXT_LOCATION_STR)
			
			# Combine into the full Fast Packet payload sequence:
			# Header (16 bytes) + Lang ID (1 byte) + Description String + Location String
			full_text_payload = header + lang_id_byte + descr_bytes + loc_bytes
			
			try:
				n2k.sendpgn(full_text_payload, 126985, src = YDPG_ADDRESS)
			except Exception:
				pass			
			
		
			# next alarm = next number
			alert_occurence_number = alert_occurence_number + 1
			if (alert_occurence_number > 252):
				alert_occurence_number = 0
			
			alert_PGNs_sent = True
			
		
		
	# 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()
		

#------------------------------------------------------------------------------
# encode normal string to N2K cursed string
def encode_n2k_string(text):
	# MicroPython hack: is it pure ASCII?
	is_ascii = all(ord(c) < 128 for c in text)
	
	if is_ascii:
		if len(text) > 250:
			text = text[:250]
		control_byte = 1
		encoded_chars = text.encode('utf-8')
	else:
		# --- FIXED: Manual UTF-16LE Byte Packing Loop for Embedded Systems ---
		if len(text) > 125:
			text = text[:125]
			
		control_byte = 0
		encoded_chars = bytearray()
		
		for c in text:
			code_point = ord(c)
			# Extract the least significant byte (Low Byte)
			low_byte = code_point & 0xFF
			# Extract the most significant byte (High Byte)
			high_byte = (code_point >> 8) & 0xFF
			
			encoded_chars.append(low_byte)
			encoded_chars.append(high_byte)
			
		encoded_chars = bytes(encoded_chars)
			
	count_byte = len(encoded_chars) + 2
	return bytes([count_byte, control_byte]) + encoded_chars


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


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

# INIT 

# change YDPG-01 N2K Class and Function
n2k.address_claim(devfunc=YDPG_FUNCTION, devclass=YDPG_CLASS)

# convert alarm text to N2K cursed binary strings
ALERT_TEXT_DESCR_BYTESTR = encode_n2k_string (ALERT_TEXT_DESCR_STR)
ALERT_TEXT_LOCATION_BYTESTR = encode_n2k_string (ALERT_TEXT_LOCATION_STR)

# 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
