# Robin DR400-135cdi Electrical System
# Bea Wolf (D-ECHO) 2021
# Reference(s)	:
#			Ref. [1] page 1-2 ff.
#			Ref. [5] chapter AMM-60-91

# based on turboprop engine electrical system by Syd Adams and C172S electrical system   ####

# Power Consumption References:		TODO
#	COM/NAV		GNC255A		??	https://static.garmin.com/pumac/190-01182-01_e.pdf
#						https://buy.garmin.com/en-US/US/p/102764
#	XPDR		GTX328		??	https://static.garmin.com/pumac/GTX328Transponder_PilotsGuide.pdf
#						https://buy.garmin.com/en-US/US/p/8712/pn/010-00634-01

# Basic props.nas objects
var electrical = props.globals.getNode("systems/electrical");
var electrical_sw = electrical.initNode("internal-switches");
var output = electrical.getNode("outputs");
var breakers = props.globals.initNode("/controls/circuit-breakers");
var controls = props.globals.getNode("/controls/electric");
var light_ctrl = props.globals.getNode("/controls/lighting");

# Helper functions
var check_or_create = func ( prop, value, type ) {
	var obj = props.globals.getNode(prop, 1);
	if( obj.getValue() == nil ){
		return props.globals.initNode(prop, value, type);
	} else {
		return obj;
	}
}

#	Switches
var switches = {
	battery_switch:		controls.initNode("battery-switch",				0,	"BOOL"),
	avionics_master:	controls.initNode("avionics-master",				0,	"BOOL"),
	engine_master:		check_or_create("controls/engines/engine/master",		0,	"BOOL"),
	starter:		check_or_create("controls/engines/engine[0]/starter-cmd",	0,	"INT"),
	electric_pump:		check_or_create("controls/fuel/tank/boost-pump",		0,	"BOOL"),
	
	pitot_heat:		props.globals.getNode("/controls/anti-ice/pitot-heat", 0, "BOOL"),
	#	Lights
	panel_light: [
		light_ctrl.initNode("panel-light[0]",	0.0, 	"DOUBLE"),
		light_ctrl.initNode("panel-light[1]",	0.0, 	"DOUBLE"),
		light_ctrl.initNode("panel-light[2]",	0.0, 	"DOUBLE"),
	],
	instrument_light:	light_ctrl.initNode("instrument-lights",	0.0,	"DOUBLE"),
	nav_light:		light_ctrl.initNode("nav-lights",		0,	"BOOL"),
	strobe_light:		light_ctrl.initNode("strobe-light",		0,	"BOOL"),
	landing_light:		light_ctrl.initNode("landing-light",		0,	"BOOL"),
	taxi_light:		light_ctrl.initNode("taxi-light",		0,	"BOOL"),
	flood_light:		light_ctrl.initNode("flood-lights",		0,	"BOOL"),
};

#	Additional (not directly consumer-related) Circuit Breakers
var circuit_breakers = {
	main:			breakers.initNode("main",		1,	"BOOL"),
	avionics:		breakers.initNode("avionics",		1,	"BOOL"),
	backup_bat:		breakers.initNode("backup-bat",		1,	"BOOL"),
};

#	Internal (virtual) switches
var int_switches = {
	strobe_light:		props.globals.initNode("/sim/model/lights/strobe/state",	0,	"BOOL"),
};

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

var main_battery_load = 0.0;
var alternator_load = 0.0;

## Lights
#				EXTERIOR
#	Landing Light
#		systems/electrical/outputs/landing-light
#	Taxi Light
#		systems/electrical/outputs/taxi-light
#	Navigation Lights
#		systems/electrical/outputs/nav-lights
#	Strobe Light
#		systems/electrical/outputs/strobe-light



var strobeLight = aircraft.light.new("/sim/model/lights/strobe", [0.04, 1], switches.strobe_light);


#	TODO calculate battery temperature correctly
var BatteryClass = {
	new : func( switch, volt, amps, amp_hours, charge_percent, charge_amps, n){
		m = { 
			parents : [BatteryClass],
			switch:		props.globals.initNode(switch, 1, "BOOL"),
			temp:		electrical.initNode("battery-temperature["~n~"]", 15.0, "DOUBLE"),
			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();
		if( me.switch.getBoolValue() ){
			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;
		}else return 0;
	},
	
	get_output_volts : func {
		if( me.switch.getBoolValue() ){
			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;
		}else return 0;
	},
	
	get_output_amps : func {
		if( me.switch.getBoolValue() ){
			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;
		}else return 0;
	}
};

# var alternator = AlternatorClass.new( "/engines/engine[0]/rpm", 800.0, 14.0, 12.0 );
##
# Alternator model class.
#
var AlternatorClass = {
	new: func ( rpm_source, rpm_threshold, ideal_volts, ideal_amps ) {
		var obj = { parents : [AlternatorClass],
				rpm_source : rpm_source,
				rpm_threshold : rpm_threshold,
				ideal_volts : ideal_volts,
				ideal_amps : ideal_amps };
		setprop( obj.rpm_source, 0.0 );
		return obj;
	},
	apply_load: func( amps ){
		var dt = delta_sec.getDoubleValue();
		
		setprop("/systems/electrical/alternator-amps", 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 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_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 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 );
		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 rpm = getprop( me.rpm_source );
		var factor = rpm / me.rpm_threshold;
		if ( factor > 1.0 ) {
			factor = 1.0;
		}
		
		# print( "alternator amps = ", ideal_amps * factor );
		return me.ideal_amps * factor;
	},

};

#	Exact battery specification (ref. [5], ch.AMM-60-91, p.4, n.11):	Batterie Cadmium nickel 1300mAH 12V TUDOR TD 1.3
var main_battery = BatteryClass.new("/systems/electrical/main-battery/serviceable", 12.0, 30, 1.3, 1.0, 7.0, 0);

var alternator = AlternatorClass.new( "/engines/engine[0]/rpm", 800.0, 14.0, 90.0 );

#	Consumer Class
#		Functions:
#			* power: takes bus_volts, applies to relevant outputs/ property and returns electrical load
#			* automatically trips its circuit breaker when maximum load is exceeded
var consumer = {
	new: func( name, switch, watts, cb_max ){
		m = { parents : [consumer] };
		m.cb = breakers.initNode(name, 1, "BOOL");
		m.switch_type = "none";
		if( switch != nil ){
			m.switch = switch;
			if ( switch.getType() == "DOUBLE" or switch.getType() == "FLOAT" ) {
				m.switch_type = "double";
			} else if ( switch.getType() == "BOOL" ) {
				m.switch_type = "bool";
			} else {
				die("Consumer (non-int) switch of unsupported type: "~ switch.getType() ~ "!");
			}
		} else {
			m.switch = nil;
		}
		m.output = output.initNode(name, 0.0, "DOUBLE");
		m.watts = watts;
		m.cb_max = cb_max;
		return m;
	},
	power: func( bus_volts ){
		if( me.cb.getBoolValue() and bus_volts != 0.0 ){
			if ( me.switch_type == "none" or ( me.switch_type == "bool" and me.switch.getBoolValue() ) ) {
				me.output.setDoubleValue( bus_volts );
				if( me.watts/bus_volts > me.cb_max ){
					# me.cb.setBoolValue( 0 );
					return 0.0;
				}
				return me.watts / bus_volts;
			} else if ( me.switch_type == "double" ) {
				me.output.setDoubleValue( bus_volts * me.switch.getDoubleValue() );
				if( me.watts / bus_volts * me.switch.getDoubleValue() > me.cb_max ){
					# me.cb.setBoolValue( 0 );
					return 0.0;
				}
				return me.watts / bus_volts * me.switch.getDoubleValue();
			} else {
				me.output.setDoubleValue( 0.0 );
				return 0.0;
			}
		} else {
			me.output.setDoubleValue( 0.0 );
			return 0.0;
		}
	},
};
# Consumer with support for integer switches
var consumer_int = {
	new: func( name, switch, watts, cb_max, int, mode ){
		m = { parents : [consumer_int] };
		m.cb = breakers.initNode(name, 1, "BOOL");
		if ( switch.getType() == "INT" ) {
			m.switch = switch;
			m.int = int;
			# Mode: 0 means "=="; 1 means "!="
			if( mode != nil ){
				m.mode = mode;
			} else {
				m.mode = 0;
			}
		} else {
			die("Consumer (int) switch of unsupported type: "~ switch.getType() ~ "!");
		}
		m.output = output.initNode(name, 0.0, "DOUBLE");
		m.watts = watts;
		m.cb_max = cb_max;
		return m;
	},
	power: func( bus_volts ){
		if( me.cb.getBoolValue() and bus_volts != 0.0 ){
			if ( ( ( me.mode == 0 and me.switch.getIntValue() == me.int ) or ( me.mode == 1 and me.switch.getIntValue() != me.int ) ) ) {
				me.output.setDoubleValue( bus_volts );
				if( me.watts / bus_volts > me.cb_max ){
					# me.cb.setBoolValue( 0 );
					return 0.0;
				}
				return me.watts / bus_volts;
			} else {
				me.output.setDoubleValue( 0.0 );
				return 0.0;
			}
		} else {
			me.output.setDoubleValue( 0.0 );
			return 0.0;
		}
	},
};

var amps = {};

#	Electrical Bus Class
var bus = {
	new: func( name, on_update, consumers ) {
		m = {	
			parents: [bus],
			name: name,
			volts: check_or_create("systems/electrical/bus/" ~ name ~ "-volts", 0.0, "DOUBLE"),
			serviceable: check_or_create("systems/electrical/bus/" ~ name ~ "-serviceable", 1, "BOOL"),
			on_update: on_update,
			bus_volts: 0.0,
			consumers: consumers,
		};
		amps.name = 0.0;	# register amps
		return m;
	},
	update_consumers: func () {
		#print("Update consumers of bus "~ me.name);
		load = 0.0;
		foreach( var c; me.consumers ) {
			load += c.power( me.bus_volts );
		}
		return load;
	},
};

var engine_connector = 0;	#	-1 = main bus to engine bus, 0 = off, 1 = engine bus to main bus


var main_bus = bus.new(
	"main-bus",
	func() {
		me.src = "";
		me.bus_volts = 0.0;
		
		if( me.serviceable.getBoolValue() and circuit_breakers.main.getBoolValue() ){
			if( switches.battery_switch.getBoolValue() ){
				me.bus_volts = main_battery.get_output_volts();
				me.src = "battery";
			}
			if( switches.engine_master.getBoolValue() and engine_bus.bus_volts > me.bus_volts and engine_connector == 1 ){
				me.bus_volts = engine_bus.bus_volts;
				me.src = "alternator";
			}
		}
		
		var load = me.update_consumers();
		load += radio_bus.on_update( me.bus_volts );
		
		if( me.src == "battery" ){
			main_battery_load += load;
		} else if ( me.src == "alternator" ){
			alternator_load += load;
		} else if ( load > 0.0 ){
			print( "Main Bus load leak: "~ load );
		}
		
		me.volts.setDoubleValue( me.bus_volts );
	},
	[
		consumer.new( "autopilot", nil, 0.1, 5 ),
		consumer.new( "safety-plane", nil, 0.1, 1 ),
		consumer.new( "ancillary-system", nil, 0.1, 40 ),
		consumer.new( "display", nil, 0.1, 5 ),
		consumer.new( "warning-lights", nil, 0.1, 1 ),
		consumer.new( "indicators", nil, 0.1, 2 ),
		consumer.new( "auxiliary-system", nil, 0.1, 5 ),
		consumer.new( "ancillary-12v", nil, 0.1, 5 ),
		consumer.new( "glow", nil, 1.0, 70 ),
		consumer.new( "panel-light[0]", switches.panel_light[0], 0.1, 1 ),
		consumer.new( "panel-light[1]", switches.panel_light[1], 0.1, 1 ),
		consumer.new( "panel-light[2]", switches.panel_light[2], 0.1, 3 ),
		#	These consumers don't have dedicated circuit breakers
		consumer.new( "nav-lights", switches.nav_light, 0.1, 99 ),
		consumer.new( "strobe-light", int_switches.strobe_light, 0.1, 99 ),
		consumer.new( "landing-light", switches.landing_light, 0.1, 99 ),
		consumer.new( "taxi-light", switches.taxi_light, 0.1, 99 ),
		consumer.new( "pitot-heat", switches.pitot_heat, 0.1, 99 ),
		consumer.new( "turn-coordinator", nil, 0.1, 99 ),
	],
);

var radio_bus = bus.new(
	"radio-bus",
	func( bv ) {
		me.src = "";
		if( me.serviceable.getBoolValue() and switches.avionics_master.getBoolValue() ){
			me.bus_volts = bv;
		} else {
			me.bus_volts = 0.0;
		}
		
		var load = me.update_consumers();
		
		me.volts.setDoubleValue( me.bus_volts );
		
		return load;
	},
	[
		consumer.new( "ipad", nil, 0.0, 3 ),
		consumer.new( "tcas", nil, 0.1, 1 ),
		consumer.new( "audio", nil, 0.1, 1 ),
		consumer.new( "comm[0]", nil, 0.1, 10 ),
		consumer.new( "nav[0]", nil, 0.1, 5 ),
		consumer.new( "transponder", nil, 0.1, 5 ),
	],
);

var engine_bus = bus.new(
	"engine-bus",
	func( ){
		me.src = "";
		me.bus_volts = 0.0;
		
		if( me.serviceable.getBoolValue() and switches.engine_master.getBoolValue() ){
			var alternator_volts = alternator.get_output_volts();
			me.bus_volts = main_bus.bus_volts;
			if( alternator_volts > math.min( me.bus_volts + 0.5, 12.5 ) ){
				me.bus_volts = alternator_volts;
				me.src = "alternator";
				engine_connector = 1;
			} else {
				me.src = "battery";
				engine_connector = -1;
			}
		}
		
		var load = me.update_consumers();
		
		me.volts.setDoubleValue( me.bus_volts );
		;
		if( me.src == "alternator" ){
			alternator_load += load;
		} else if( me.src == "battery" ){
			main_battery_load += load;
		} else if ( load > 0.0 ){
			print( "Engine Bus load leak: "~ load );
		}
	},
	[
		consumer.new( "fadec-main", nil, 0.1, 40 ),
		consumer.new( "fadec-a", nil, 0.1, 20 ),
		consumer.new( "fadec-b", nil, 0.1, 20 ),
		consumer_int.new( "starter", switches.starter, 0.1, 10, 2, 0 ),
		#	These consumers don't have dedicated circuit breakers
		consumer.new( "fuel-pump", switches.electric_pump, 0.5, 99 ),
	],
);


var main_essential_tie = 0;

var update_electrical = func {
	
	main_bus.on_update();
	engine_bus.on_update();
	
	main_battery.apply_load( main_battery_load );
	main_battery_load = 0.0;
	
	alternator.apply_load( alternator_load );
	alternator_load = 0.0;
}

var electrical_updater = maketimer( 0.0, update_electrical );
electrical_updater.simulatedTime = 1;
electrical_updater.start();

var turn_indicator_spin = props.globals.getNode("/instrumentation/turn-indicator/spin");

var check_watts = func {
	var tc_spin = turn_indicator_spin.getDoubleValue();
	
	if( tc_spin == 1.0 ){
		main_bus.consumers[1].watts = 4.2;
		# print( "Turn Coordinator running" );
	} else {
		main_bus.consumers[1].watts = 18;
		# print( "Turn Coordinator starting" );
	}
}

var watts_updater = maketimer( 0.1, check_watts );
watts_updater.simulatedTime = 1;
watts_updater.start();

