##
# Procedural model of a Diamond DA62 electrical system.  Includes a
# preliminary battery charge/discharge model and realistic ammeter
# gauge modeling.
#

#	based on
#		flight manual [ref 1], chapter 7.10
#		maintenance manual [ref 2], chapter 24

#	the DA62 DC electrical system has the following buses:
#		-RELAY Bus
#		-HOT Battery Bus: directly connected to the main battery
#			-> powers pilot map/reading lights
#		-BATTERY BUS: connected to main battery by ELECT. MASTER switch
#			-> powers LH+RH MAIN Bus + starters
#		-LH MAIN Bus: connected to BATTERY BUS via 90A CB
#			-> powers consumers
#		-RH MAIN Bus: connected to BATTERY BUS via 90A CB
#			-> powers consumers + AVIONIC Bus 
#		-LH ECU Bus: connected to LH MAIN Bus + LH alternator power output
#			-> powers LH ECU A + ECU A fuel pump
#			-> powers LH+RH ECU B + ECU B fuel pump
#		-RH ECU Bus; connected to RH MAIN Bus + RH alternator power output
#			-> powers RH ECU A + ECU A fuel pump
#			-> powers LH+RH ECU B + ECU B fuel pump
#		LH and RH ECU Bus have their own ECU backup batteries -> for 30 min
#		-AVIONICS Bus: connected to RH MAIN Bus by AVIONIC MASTER switch


##
# Initialize internal values
#

var vbus_volts = 0.0;
var ebus1_volts = 0.0;
var ebus2_volts = 0.0;

var ammeter_ave = 0.0;

##
# Properties
#
var ctrls = props.globals.getNode("/controls", 1);
var cb_prop = ctrls.getNode("circuit-breakers", 1);
var switch_prop = ctrls.getNode("switches", 1);
var lighting_prop = ctrls.getNode("lighting", 1);

var electrical = props.globals.getNode("/systems/electrical", 1);
var output = electrical.getNode("outputs", 1);
var buses = electrical.getNode("buses", 1);
var batteries = electrical.initNode("batteries-charge", 1);
var serviceable = electrical.getNode("/systems/electrical/serviceable", 1);
# Hot Battery Bus 
var reading_lights = props.globals.getNode("/controls/lighting/reading-lights", 1);

##
# Battery model class.
#
# var battery = BatteryClass.new(24.0, 30.0, 3.1875, 7.0);

var BatteryClass = {
	new: func (ideal_volt, ideal_amp, amp_hour, charge_amp, name){
		var obj = { parents : [BatteryClass],
			ideal_volts : ideal_volt,
			ideal_amps : ideal_amp,
			amp_hours : amp_hour,
			charge_percent : getprop("/systems/electrical/batteries"~name) or 1.0,
			charge_amps : charge_amp,
			charge_prop : batteries.initNode(name, 0.0, "DOUBLE"),
		};
		obj.charge_prop.setDoubleValue(obj.charge_percent);
		return obj;
	},
	apply_load: func (amps, dt) {
		var old_charge_percent = me.charge_prop.getDoubleValue();
		
		if (getprop("/sim/freeze/replay-state"))
			return me.amp_hours * old_charge_percent;
		
		var amphrs_used = amps * dt / 3600.0;
		var percent_used = amphrs_used / me.amp_hours;
		
		var new_charge_percent = std.max(0.0, std.min(old_charge_percent - percent_used, 1.0));
		
		if (new_charge_percent < 0.1 and old_charge_percent >= 0.1)
			gui.popupTip("Warning: Low battery! Enable alternator or apply external power to recharge battery!", 10);
		me.charge_percent = new_charge_percent;
		
		me.charge_prop.setDoubleValue(new_charge_percent);
		return me.amp_hours * new_charge_percent;
	},
	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;
		return me.ideal_volts * factor;
	},
	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;
		return me.ideal_amps * factor;
	},
	reset_to_full_charge: func {
		me.apply_load(-(1.0 - me.charge_percent) * me.amp_hours, 3600);
	},
};

##
# Alternator model class.
#
# var alternator = AlternatorClass.new("/engines/engine[0]/rpm", 800.0, 28.0, 60.0);

var AlternatorClass = {
	new: func (src, switch, thrsh, ideal_volt, ideal_amp){
		var obj = { parents : [AlternatorClass],
		rpm_source : src,
		alt_switch: props.globals.getNode(switch),
		rpm_threshold : thrsh,
		ideal_volts : ideal_volt,
		ideal_amps : ideal_amp };
		setprop( obj.rpm_source, 0.0 );
		return obj;
	},
	apply_load: func( amps, dt ) {
		# 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 rpm = getprop( me.rpm_source );
		var factor = rpm / me.rpm_threshold;
		if ( factor > 1.0 ) {
			factor = 1.0;
		}
		# print( "alternator amps = ", me.ideal_amps * factor );
		var available_amps = me.ideal_amps * factor;
		return available_amps - amps;
	},
	get_output: func {
		if( me.alt_switch.getBoolValue() ){
			# scale alternator output for rpms < threshold.  For rpms >= threshold
			# give full output.  This is just a WAG, and probably not how
			# it really works but I'm keeping things "simple" to start.
			var rpm = getprop( me.rpm_source );
			var factor = rpm / me.rpm_threshold;
			if ( factor > 1.0 ) {
				factor = 1.0;
			}
			# print( "alternator volts = ", me.ideal_volts * factor );
			var output = [me.ideal_volts*factor, me.ideal_amps*factor];
		}else{
			var output = [0.0,0.0];
		}
		return output;
	},
};

# [1] page 6-20: 
#	main battery is RG24-15: reference http://concordebattery.com/otherpdf/RG24-15.pdf
# 	ECU backup batteries are LC-R127R2P: reference https://b2b-api.panasonic.eu/file_stream/pids/fileversion/3535	
var batteries = {	
	main_battery: BatteryClass.new(24.0, 30.0, 13.6, 7.0, "main"),
	ECU_backup_battery_LH1: BatteryClass.new(12.0, 5.0, 7.2, 2.88, "ECU_backup_LH1"),
	ECU_backup_battery_LH2: BatteryClass.new(12.0, 5.0, 7.2, 2.88, "ECU_backup_LH2"),
	ECU_backup_battery_RH1: BatteryClass.new(12.0, 5.0, 7.2, 2.88, "ECU_backup_RH1"),
	ECU_backup_battery_RH2: BatteryClass.new(12.0, 5.0, 7.2, 2.88, "ECU_backup_RH2"),
};
var alternators = [
	AlternatorClass.new("/engines/engine[0]/rpm", "/controls/switches/alternator[0]", 1400.0, 28.0, 70.0),
	AlternatorClass.new("/engines/engine[1]/rpm", "/controls/switches/alternator[1]", 1400.0, 28.0, 70.0),
];
var starter=[props.globals.getNode("/controls/engines/engine[0]/starter", 1),props.globals.getNode("/controls/engines/engine[1]/starter", 1),];
	

var circuit_breakers={
	lh_main_bus: ["batt", "alt", "comm[0]", "gpsnav1", "transponder", "eng-inst", "pitot", "de-ice", "pfd", "adc", "ahrs", "taximap-acl", "gear-wrn", "gear", "aux-pumps"],
	rh_main_bus: ["batt", "alt", "mfd", "sam", "stall-wrn", "flap", "ldg-lt-start", "nav-lt-flood", "avcdu-fan", "inst-lt", "pedals", "av-cont", "avionic-bus"],
	avionic_bus: ["comm[1]", "gpsnav2", "audio", "afcses-pusp", "data-link", "adf", "dme", "tas", "wxrdr", "twx", "iridium", "evs"],
	lh_ecu_bus: ["fuel-pump-a", "fuel-pump-b", "ecu-bus", "ecu-b", "ecu-a"],
	rh_ecu_bus: ["fuel-pump-a", "fuel-pump-b", "ecu-bus", "ecu-b", "ecu-a"],
};

var outputs = {
	reading: output.initNode("reading-lights", 0.0, "DOUBLE"),
	starter0: output.initNode("starter[0]", 0.0, "DOUBLE"),
	starter1: output.initNode("starter[1]", 0.0, "DOUBLE"),
};
var switches = {
	electric_master: switch_prop.getNode("electric-master", 1),
	avionic_master: switch_prop.getNode("avionic-master", 1),
	pitot: switch_prop.getNode("pitot-heat", 1),
	landing: lighting_prop.getNode("landing", 1),
	taxi: lighting_prop.getNode("taxi", 1),
	position: lighting_prop.getNode("position", 1),
	strobe: lighting_prop.getNode("strobe", 1),
	engine_master_lh: switch_prop.getNode("engine-master[0]", 1),
	engine_master_rh: switch_prop.getNode("engine-master[1]", 1),
	alt_lh: switch_prop.getNode("alternator[0]", 1),
	alt_rh: switch_prop.getNode("alternator[1]", 1),
	fuel_pump_lh: switch_prop.getNode("fuel-pump[0]", 1),
	fuel_pump_rh: switch_prop.getNode("fuel-pump[1]", 1),
};

var reset_battery_and_circuit_breakers = func {
	# Charge battery to 100 %
	battery.reset_to_full_charge();
	
	# Reset circuit breakers
	foreach(var bus; keys(circuit_breakers)){
		foreach(var cb; bus){
			cb_prop.getNode(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) {
		hot_battery_bus(dt);
	}
};

var hot_battery_bus = func( dt ) {
	var bus_volts = 0.0;
	var load = 0.0;
	if( serviceable.getBoolValue() ){
		bus_volts = batteries.main_battery.get_output_volts();
	}
	
	# Cockpit Reading/Map Lights
	# reference: (similar) https://www.aircraftspruce.com/pages/ps/cockpitaccessories_lighting/led_maplight.php
	# 20mA used at 24V: P=0.48W
	var reading_lights_factor = reading_lights.getDoubleValue();
	outputs.reading.setDoubleValue(bus_volts*reading_lights_factor);
	load += (0.48 * reading_lights_factor)/bus_volts;
	
	load += battery_bus(bus_volts);
	
	if ( load > 0 ) {
		batteries.main_battery.apply_load( load, dt );
	} elsif ( load < 0 ) {
		batteries.main_battery.apply_load( -batteries.main_battery.charge_amps, dt );
	}
	
	buses.getNode("hot-battery").setDoubleValue(bus_volts);
}

var battery_bus = func(bv) {
	var bus_volts = 0.0;
	var load = 0.0;
	if( switches.electric_master.getBoolValue() ){
		bus_volts = bv;
	}
	
	# Starter Motors
	# Engine Installation Manual: up to 200A
	# 150A used at 24V: P=3600W
	if( starter[0].getBoolValue() ){
		outputs.starter0.setDoubleValue(bus_volts);
		load += 3600/bus_volts;
	}
	if( starter[1].getBoolValue() ){
		outputs.starter1.setDoubleValue(bus_volts);
		load += 3600/bus_volts;
	}
	load += lh_main_bus(bus_volts);
	load += rh_main_bus(bus_volts);
	
	
	buses.getNode("battery").setDoubleValue(bus_volts);
	return load;
}

var lh_main_bus = func(bv) {
	var bus_volts = 0.0;
	var load = 0.0;
	if( cb_prop.getNode(circuit_breakers.lh_main_bus[0]).getBoolValue() ){
		bus_volts = bv;
	}
	for(var i=2; i<size(circuit_breakers.lh_main_bus); i=i+1){
		if ( cb_prop.getNode(circuit_breakers.lh_main_bus[i]).getBoolValue() ){
			output.getNode(circuit_breakers.lh_main_bus[i]).setDoubleValue(bus_volts);
		}
	}
	load += 2;
	
	load += lh_ecu_bus(bus_volts);
	
	
	buses.getNode("lh-main").setDoubleValue(bus_volts);
	return load;
}
var rh_main_bus = func(bv) {
	var bus_volts = 0.0;
	var load = 0.0;
	if( cb_prop.getNode(circuit_breakers.rh_main_bus[0]).getBoolValue() ){
		bus_volts = bv;
	}
	for(var i=2; i<size(circuit_breakers.rh_main_bus)-1; i=i+1){
		if ( cb_prop.getNode(circuit_breakers.rh_main_bus[i]).getBoolValue() ){
			output.getNode(circuit_breakers.rh_main_bus[i]).setDoubleValue(bus_volts);
		}
	}
	load += 2;
	
	load += avionics_bus(bus_volts);
	load += rh_ecu_bus(bus_volts);
	
	
	buses.getNode("rh-main").setDoubleValue(bus_volts);
	return load;
}

var avionics_bus = func(bv) {
	var bus_volts = 0.0;
	var load = 0.0;
	if ( switches.avionic_master.getBoolValue() and cb_prop.getNode(circuit_breakers.rh_main_bus[12]).getBoolValue() ){
		bus_volts = bv;
	}
	
	if ( bus_volts > 0.0 ){
		for(var i=2; i<size(circuit_breakers.avionic_bus); i=i+1){
			if ( cb_prop.getNode(circuit_breakers.avionic_bus[i]).getBoolValue() ){
				output.getNode(circuit_breakers.avionic_bus[i]).setDoubleValue(bus_volts);
			}
		}
		load += 2;
	}
	
	buses.getNode("avionics").setDoubleValue(bus_volts);
	return load;
}

var lh_ecu_bus = func(bv) {
	var bus_volts = 0.0;
	var load = 0.0;
	var mc = cb_prop.getNode(circuit_breakers.lh_main_bus[1]).getBoolValue();
	if( mc ){
		var battery_volts = bv;
	}
	var alternator_out = alternators[0].get_output();
	var alternator_volts = alternator_out[0];
	if( battery_volts > alternator_volts ){
		bus_volts = battery_volts;
		source = "battery";
	}else if(alternator_volts > 0.0){
		bus_volts = alternator_volts;
		source = "alternator";
	}else{
		bus_volts = batteries.ECU_backup_battery_LH1.get_output_volts() + batteries.ECU_backup_battery_LH2.get_output_volts();
		source = "backup";
	}
	
	buses.getNode("lh-ecu").setDoubleValue(bus_volts);
	if( source == "battery"){
		return load;
	}else if ( source == "alternator"){
		load -= alternator_out[1];
		return load;
	}else{
		batteries.ECU_backup_battery_LH1.apply_load(load/2);
		batteries.ECU_backup_battery_LH2.apply_load(load/2);
		return 0.0;
	}
}
var rh_ecu_bus = func(bv) {
	var bus_volts = 0.0;
	var load = 0.0;
	var mc = cb_prop.getNode(circuit_breakers.rh_main_bus[1]).getBoolValue();
	if( mc ){
		var battery_volts = bv;
	}
	var alternator_out = alternators[1].get_output();
	var alternator_volts = alternator_out[0];
	if( battery_volts > alternator_volts ){
		bus_volts = battery_volts;
		source = "battery";
	}else if(alternator_volts > 0.0){
		bus_volts = alternator_volts;
		source = "alternator";
	}else{
		bus_volts = batteries.ECU_backup_battery_RH1.get_output_volts() + batteries.ECU_backup_battery_RH2.get_output_volts();
		source = "backup";
	}
	
	buses.getNode("rh-ecu").setDoubleValue(bus_volts);
	if( source == "battery"){
		return load;
	}else if ( source == "alternator"){
		load -= alternator_out[1];
		return load;
	}else{
		batteries.ECU_backup_battery_RH1.apply_load(load/2);
		batteries.ECU_backup_battery_RH2.apply_load(load/2);
		return 0.0;
	}
}

##
# Initialize the electrical system
#

var system_updater = ElectricalSystemUpdater.new();

# checking if battery should be automatically recharged
if (!getprop("/systems/electrical/save-battery-charge")) {
	foreach(var i; keys(batteries)){
		batteries[i].reset_to_full_charge();
	}
};

# initialize circuit breakers
foreach(var bus; keys(circuit_breakers)){
	foreach(var cb; circuit_breakers[bus]){
		cb_prop.initNode(cb, 1, "BOOL");
	}
}

# initialize outputs
for(var i=2; i<size(circuit_breakers.lh_main_bus); i=i+1){
	output.initNode(circuit_breakers.lh_main_bus[i], 0.0, "DOUBLE");
}
for(var i=2; i<size(circuit_breakers.rh_main_bus)-1; i=i+1){
	output.initNode(circuit_breakers.rh_main_bus[i], 0.0, "DOUBLE");
}
for(var i=0; i<size(circuit_breakers.avionic_bus); i=i+1){
	output.initNode(circuit_breakers.avionic_bus[i], 0.0, "DOUBLE");
}

system_updater.enable();

print("Electrical system initialized");

