##
# Procedural model of a Diamond HK36 TTC electrical system.
# Reference: Flight Manual, Maintenance Manual and Engine Operation Manual


##
# Initialize Properties
#

var delta_sec		=	props.globals.getNode("/sim/time/delta-sec", 1);

var electrical		=	props.globals.getNode("/systems/electrical", 1);

var serviceable		=	electrical.initNode( "serviceable", 1, "BOOL");

var amps		= 	electrical.getNode( "amps", 1);
var volts		=	electrical.initNode( "volts", 0.0, "DOUBLE");

var switches		=	props.globals.getNode("/controls/switches", 1);
var master_switch	=	switches.getNode("master",	1);
var mode_switch		=	switches.getNode("mode",	1);	# 0 = SOARING; 1 = POWERED FLIGHT
var acl_switch		=	switches.getNode("acl",		1);
var pos_switch		=	switches.getNode("pos",		1);
var land_switch		=	switches.getNode("ldg",		1);
var instr_switch	=	switches.getNode("instr",	1);
var fp_switch		=	switches.getNode("fuelpump",	1);
var starter		=	switches.getNode("starter",	1);

var outputs		=	electrical.getNode("outputs", 1);

var starter_volts	=	outputs.initNode("starter", 0.0, "DOUBLE");
var engine_fuel_pump	=	outputs.initNode("engine-fuel-pump", 0.0, "DOUBLE");

var breakers		=	props.globals.getNode("/controls/circuit-breakers", 1);
var cb = {
	batt	:	breakers.initNode("batt",	1,	"BOOL"),
	gen	:	breakers.initNode("gen",	1,	"BOOL"),
	gen_ctrl:	breakers.initNode("gen-ctrl",	1,	"BOOL"),
	fuel_pump:	breakers.initNode("fuel-pump",	1,	"BOOL"),
};

var acl = aircraft.light.new( "/sim/model/lights/strobe", [ 0.05, 0.5 ], acl_switch );
var acl_int_switch	=	props.globals.getNode("/sim/model/lights/strobe/state", 1);
	
##
# Initialize properties used to determine electrical load
#
var com_ptt	= props.globals.getNode("/instrumentation/comm[0]/ptt", 1);
var com_start	= props.globals.getNode("/instrumentation/comm[0]/start", 1);
#var vario_vol	= props.globals.getNode("/instrumentation/ilec-sc7/volume", 1);
#var vario_aud	= props.globals.getNode("/instrumentation/ilec-sc7/audio", 1);
#var vario_read	= props.globals.getNode("/instrumentation/ilec-sc7/te-reading-mps", 1);
var flarm_receive = props.globals.getNode("/instrumentation/FLARM/receive-internal", 1);
var xpdr_on	=	props.globals.getNode("/instrumentation/it-gtx327/internal/system-alive", 1);
var att_spin	=	props.globals.getNode("/instrumentation/attitude-indicator/spin", 1);
var att_suction	=	props.globals.getNode("/systems/electrical/outputs/attitude-indicator-suction-inhg", 1);
var dir_spin	=	props.globals.getNode("/instrumentation/heading-indicator-dg/spin", 1);

##
# Battery model class.
#

var BatteryClass = {
	new : func( volt, amps, amp_hours, charge_percent, charge_amps, n){
		m = { 
			parents : [BatteryClass],
			ideal_volts:	volt,
			ideal_amps:	amps,
			volt_p:		electrical.initNode("battery-volts["~n~"]", 0.0, "DOUBLE"),
			amp_hours:	amp_hours,
			charge_percent:	charge_percent, 
			charge_amps:	charge_amps,
		};
		return m;
	},
	apply_load : func( load ) {
		var dt = delta_sec.getDoubleValue();
		var amphrs_used = load * dt / 3600.0;
		var percent_used = amphrs_used / me.amp_hours;
		me.charge_percent -= percent_used;
		if ( me.charge_percent < 0.0 ) {
			me.charge_percent = 0.0;
		} elsif ( me.charge_percent > 1.0 ) {
			me.charge_percent = 1.0;
		}
		var output =me.amp_hours * me.charge_percent;
		return output;
	},
	
	get_output_volts : func {
		var x = 1.0 - me.charge_percent;
		var tmp = -(3.0 * x - 1.0);
		var factor = (tmp*tmp*tmp*tmp*tmp + 32) / 32;
		var output =me.ideal_volts * factor;
		me.volt_p.setDoubleValue( output );
		return output;
	},
	
	get_output_amps : func {
		var x = 1.0 - me.charge_percent;
		var tmp = -(3.0 * x - 1.0);
		var factor = (tmp*tmp*tmp*tmp*tmp + 32) / 32;
		var output =me.ideal_amps * factor;
		return output;
	},
	
	reset_to_full_charge : func {
		me.charge_percent = 1.0;
	},
};


##
# Alternator model class.
#


var AlternatorClass = {
	new: func ( rpm_source, rpm_threshold, ideal_volts, ideal_amps ) {
		var obj = { 
			parents : [AlternatorClass],
			rpm_source : props.globals.getNode( rpm_source, 1),
			rpm_threshold : rpm_threshold,
			ideal_volts : ideal_volts,
			ideal_amps : ideal_amps,
			amps_p:	props.globals.initNode( "/systems/electrical/alternator-amps", 0.0, "DOUBLE"),
		};
		obj.rpm_source.setDoubleValue( 0.0 );
		return obj;
	},
	apply_load: func( amps ){
		var dt = delta_sec.getDoubleValue();
		
		me.amps_p.setDoubleValue( amps );
		
		# Computes available amps and returns remaining amps after load is applied
		# Scale alternator output for rpms < 800.  For rpms >= 800
		# give full output.  This is just a WAG, and probably not how
		# it really works but I'm keeping things "simple" to start.
		var factor = math.min( me.rpm_source.getDoubleValue() / me.rpm_threshold, 1.0 );
		
		# print( "alternator amps = ", me.ideal_amps * factor );
		var available_amps = me.ideal_amps * factor;
		return available_amps - amps;
	},
	get_output_volts: func {
		# Return output volts based on rpm
		
		# scale alternator output for rpms < 800.  For rpms >= 800
		# give full output.  This is just a WAG, and probably not how
		# it really works but I'm keeping things "simple" to start.
		var factor = math.min( me.rpm_source.getDoubleValue() / me.rpm_threshold, 1.0 );
		
		# print( "alternator volts = ", me.ideal_volts * factor );
		return me.ideal_volts * factor;
	},
	get_output_amps: func {
		# Return output amps available based on rpm.
		
		# scale alternator output for rpms < 800.  For rpms >= 800
		# give full output.  This is just a WAG, and probably not how
		# it really works but I'm keeping things "simple" to start.
		var factor = math.min( me.rpm_source.getDoubleValue() / me.rpm_threshold, 1.0 );
		
		# print( "alternator amps = ", ideal_amps * factor );
		return me.ideal_amps * factor;
	},

};

#		Battery
#
#	AMM 2.6.1
#		voltage: 12V
#		capacity: 18Ah
var battery = BatteryClass.new( 12.0, 30, 18, 1.0, 7.0, 0);

#		Alternator
#
#	Engine Operation Manual 7.1.1:
#		voltage: 	12V
#		amperage:	20A	(maximal)
var alternator = AlternatorClass.new( "/engines/engine[0]/rpm", 800.0, 14.0, 12.0 );

var reset_battery_and_circuit_breakers = func {
	# Charge battery to 100 %
	battery.reset_to_full_charge();
	
	# Reset circuit breakers
	foreach( var b; keys(cb) ) {
		b.setBoolValue(1);
	}
	
	foreach( var b; soaring_bus.consumers ){
		b.cb.setBoolValue( 1 );
	}
	foreach( var b; main_bus.consumers ){
		b.cb.setBoolValue( 1 );
	}
}

##
# This is the main electrical system update function.
#

var ElectricalSystemUpdater = {
	new : func {
		var m = {
			parents: [ElectricalSystemUpdater]
		};
		# Request that the update function be called each frame
		m.loop = updateloop.UpdateLoop.new(components: [m], update_period: 0.0, enable: 0);
		return m;
	},
	
	enable: func {
		me.loop.reset();
		me.loop.enable();
	},
	
	disable: func {
		me.loop.disable();
	},
	
	reset: func {
		# Do nothing
	},
	
	update: func (dt) {
		update_virtual_bus(dt);
	}
};

##
# Model the system of relays and connections that join the battery,
# alternator, starter, master/alt switches, external power supply.
#

var update_virtual_bus = func (dt) {
	var external_volts = 0.0;
	var load = 0.0;
	var battery_volts = 0.0;
	var alternator_volts = 0.0;
	if ( serviceable.getBoolValue() ) {
		battery_volts = battery.get_output_volts();
		alternator_volts = alternator.get_output_volts();
	}
	
	# switch state
	var master = master_switch.getBoolValue();
	if (getprop("/controls/electric/external-power"))
	{
		external_volts = 14;
	}
	
	# determine power source
	var bus_volts = 0.0;
	var power_source = nil;
	if ( master and cb.batt.getBoolValue() ) {
		bus_volts = battery_volts;
		power_source = "battery";
	}
	if ( master and (alternator_volts > bus_volts) and cb.gen.getBoolValue() and cb.gen_ctrl.getBoolValue() ) {
		bus_volts = alternator_volts;
		power_source = "alternator";
	}
	if ( external_volts > bus_volts ) {
		bus_volts = external_volts;
		power_source = "external";
	}
	#print( "virtual bus volts = ", bus_volts );
	
	# bus network (1. these must be called in the right order, 2. the
	# bus routine itself determins where it draws power from.)
	load += soaring_bus.update( bus_volts );
	load += main_bus.update( bus_volts );
	
	#print(load);
	# system loads and ammeter gauge
	var ammeter = 0.0;
	if ( bus_volts > 1.0 ) {
		# ammeter gauge
		if ( power_source == "battery" ) {
			ammeter = -load;
		} else {
			ammeter = battery.charge_amps;
		}
	}
	# print( "ammeter = ", ammeter );
	
	# The engine fuel pump always works if the alternator is supplying power
	if ( bus_volts > 9 and power_source == "alternator" ){
		engine_fuel_pump.setDoubleValue( bus_volts );
		load += 32.4 / bus_volts;
	} else {#
		engine_fuel_pump.setDoubleValue( 0.0 );
	}
	
	# charge/discharge the battery
	if ( power_source == "battery" ) {
		battery.apply_load( load, dt );
		if( load > 50 ){
			cb.batt.setBoolValue( 0 );
		}
	} elsif ( power_source == "alternator" ) {
		battery.apply_load( -alternator.apply_load( load, dt ), dt );
		if( load > 50 ){
			cb.gen.setBoolValue( 0 );
		}
	} elsif ( power_source == "external" ) {
		battery.apply_load( -battery.charge_amps, dt );
	}
	
	# The starter motor is directly connected to the battery relay
	# power consumption is ~75A at 12VDC
	if( starter.getBoolValue() and bus_volts > 8 ){
		starter_volts.setDoubleValue( bus_volts );
		battery.apply_load( 1050 / bus_volts );
	} else {
		starter_volts.setDoubleValue( 0.0 );
	}
	
	
	# outputs
	amps.setDoubleValue( ammeter );
	volts.setDoubleValue( bus_volts );
}

var consumer = {
	new: func( name, volt_threshold, consumption, breaker_rating, switch ){
		var obj = {
			parents: [consumer],
			name: name,
			volt_threshold: volt_threshold or 9,	# 9 is used as the standard volt threshold for this implementation
			consumption: consumption,
			breaker_rating: breaker_rating,
			switch: switch,
			cb: breakers.initNode( name, 1, "BOOL"),
			output: outputs.initNode( name, 0.0, "DOUBLE"),
		};
		return obj;
	},
	update: func( bv ){
		if( bv > me.volt_threshold and me.cb.getBoolValue() and ( me.switch == nil or me.switch.getBoolValue() ) ){
			me.output.setDoubleValue( bv );
			
			var load = me.consumption / bv;
			# print( me.name ~ " uses " ~ me.consumption ~ "W at "~ bv ~ "V: " ~ load ~ "A" );
			if( load > me.breaker_rating ){
				me.cb.setBoolValue( 0 );
				return 0.0;
			} else {
				return me.consumption / bv;
			}
		} else {
			me.output.setDoubleValue( 0.0 );
			return 0.0;
		}
	},
};

#Load sources:
#	com:		https://www.skyfox.com/becker-ar6201-022-vhf-am-sprechfunkgeraet-8-33.html
#	vario:		http://www.ilec-gmbh.com/ilec/manuals/SC7pd.pdf
#	flarm:		http://flarm.com/wp-content/uploads/man/FLARM_InstallationManual_D.pdf
#	flarm display:	https://www.air-store.eu/Display-V3-FLARM

var soaring_bus = {
	init: func {
		soaring_bus.consumers[0].update = func( bv ){
			if( me.cb.getBoolValue() ){
				me.output.setDoubleValue( bv );
				if(com_ptt.getBoolValue() and com_start.getValue()==1){
					return 19.2 / bv;
				}else{
					return 1.02 * com_start.getDoubleValue() / bv;
				}
			} else {
				me.output.setDoubleValue( 0.0 );
				return 0.0;
			}
		};
	},
	consumers: [
		consumer.new( "comm", 9, nil, 5, nil ),
		consumer.new( "flarm", 9, 0.66, 9999, nil ),
	],
	update: func( bv ) {
		
		var bus_volts = bv;
		var load = 0.0;
		
		
		# The soaring bus only powers essential equipment, namely COM1 and the electric vario (not installed)
		foreach( var el; me.consumers ){
			load += el.update( bus_volts );
		}
		
		
		return load;
		
	},
};

var main_bus = {
	init: func {
		main_bus.consumers[2].update = func( bv ){
			if( bv > me.volt_threshold and me.cb.getBoolValue() ){
				me.output.setDoubleValue( bv );
				att_suction.setDoubleValue( bv/2 );	#simulate suction, as this actually is an electrically-driven attitude gyro
				if( att_spin.getValue() <= 0.99 ) {
					return 56 / bv; #4.0A at 14VDC
				} else {
					return 14 / bv; #1.0A at 14VDC
				}
			} else {
				me.output.setDoubleValue( 0.0 );
				att_suction.setDoubleValue( 0.0 );
			}
		};
		
		main_bus.consumers[3].update = func( bv ){
			if( bv > me.volt_threshold and me.cb.getBoolValue() ){
				me.output.setDoubleValue( bv );
				if( dir_spin.getValue() <= 0.99 ) {
					return 56 / bv; #4.0A at 14VDC
				} else {
					return 14 / bv; #1.0A at 14VDC
				}
			} else {
				me.output.setDoubleValue( 0.0 );
			}
		};
	},
	consumers: [
		consumer.new( "gps", 11, 5.5, 3, nil ), # 500mA MAX at min voltage of 11V
		consumer.new( "transponder", nil, 18, 5, nil ), # 1.5A at 12VDC
		consumer.new( "attitude-indicator", 11, nil, nil, nil ),
		consumer.new( "DG", 11, nil, nil, nil ),
		consumer.new( "anti-collision-light", 9, 36, 3.5, acl_int_switch ), # 3.0A at 12 VDC
		consumer.new( "navigation-lights", 9, 90, 10, pos_switch ), # 7.5A at 12 VDC
		consumer.new( "landing-light", 9, 90, 10, land_switch ), # 7.5A at 12 VDC
		consumer.new( "fuel-qty-oil-temp", 10, 12, 2, nil ), # electrical.xml copies the output to /fuel-quantity-indicator and /oil-temp-indicator
		consumer.new( "cht-oil-press", 10, 12, 2, nil ), # electrical.xml copies the output to /cht-indicator and /oil-press-indicator
		consumer.new( "boost-fuel-pump", 9, 15.6, 5, fp_switch ),
	],
	update: func( bv ){
		var bus_volts = 0.0;
		var load = 0.0;
		# The main bus powers everything else, it is activated when additionally the mode switch is placed in the "powered flight" position
		if ( mode_switch.getBoolValue() ){
			bus_volts = bv;
		}
		# Consumers:
		#	GPS					Garmin GPS100	http://retro-gps.info/Manuals/downloads/files/Garmin%20GPS%20100AVD_Inst.pdf / http://static.garmin.com/pumac/GPS100STD_OwnersManual.pdf
		#									FIG A-5:	CB: 3A
		#	XPDR					Garmin GTX328	http://static.garmin.com/pumac/GTX328Transponder_MaintenanceManual.pdf	
		#									chapter 5.5:	power consumption up to 3A, CB assume: 5A
		#								http://static.garmin.com/pumac/GTX328Transponder_PilotsGuide.pdf
		#	Electric Attitude Indicator		~0.8A		AMM 2.6.20	RCA 26EK	https://kellymfg.com/images/RCA26-EK_Information.pdf	CB: 4A
		#	Directional Gyro			~0.8A		AMM 2.6.20	CB: assume: 4A
		#	COMM									AR-6201		https://www.becker-avionics.com/wp-content/uploads/2017/08/AR6201_IO_SW3050149.pdf	CB: 5A (internal!)
		#	ACL (Anti-Collision Light/Strobe)	~3.0A		AMM 2.6.20
		#	Position Lights				~7.5A		AMM 2.6.20
		#	Landing Light (opt)			~7.5A		AMM 2.6.20
		#	starter					~75A (max.120A)	( max.: AMM 2.6.20 )
		#	fuel pumps (main 2.7A, boost +1.3A)			AMM 2.6.20
		#	TCU (0.3A)						AMM 2.6.20
		#	engine instruments (~2.0A)				AMM 2.6.20
		#		including propeller adjustment			AMM 2.6.20
		#		1. Fuel Quantity + Oil Temp
		#		2. Oil Pressure + CHT
		
		foreach( var el; me.consumers ){
			load += el.update( bus_volts );
		}
		
			
		# return cumulative load
		return load;
	},
};


##
# Initialize the electrical system
#
var system_updater = ElectricalSystemUpdater.new();
soaring_bus.init();
main_bus.init();

# checking if battery should be automatically recharged
if (!getprop("/systems/electrical/save-battery-charge")) {
	battery.reset_to_full_charge();
};

system_updater.enable();

print("Electrical system initialized");

