--[[ Name: EFI Scripting backend driver for SkyPower Authors: Andrew Tridgell & Josh Henderson The protocol has high CAN utilization due to CAN packets that are used internal to the enigne being published externally, as well as being limited to 500 kbit/s. CAN_D1_PROTOCOL 10 (Scripting Driver 1) CAN_P1_DRIVER 1 (First driver) CAN_D1_BITRATE 500000 (500 kbit/s) --]] ---@diagnostic disable: param-type-mismatch ---@diagnostic disable: need-check-nil ---@diagnostic disable: undefined-field ---@diagnostic disable: missing-parameter -- Check Script uses a miniumum firmware version local SCRIPT_AP_VERSION = 4.3 local SCRIPT_NAME = "EFI: Skypower CAN" local VERSION = FWVersion:major() + (FWVersion:minor() * 0.1) assert(VERSION >= SCRIPT_AP_VERSION, string.format('%s Requires: %s:%.1f. Found Version: %s', SCRIPT_NAME, FWVersion:type(), SCRIPT_AP_VERSION, VERSION)) local MAV_SEVERITY_ERROR = 3 local K_THROTTLE = 70 local K_HELIRSC = 31 local MODEL_DEFAULT = 0 local MODEL_SP_275 = 1 PARAM_TABLE_KEY = 36 PARAM_TABLE_PREFIX = "EFI_SP_" -- bind a parameter to a variable given function bind_param(name) local p = Parameter() assert(p:init(name), string.format('could not find %s parameter', name)) return p end -- add a parameter and bind it to a variable function bind_add_param(name, idx, default_value) assert(param:add_param(PARAM_TABLE_KEY, idx, name, default_value), string.format('could not add param %s', name)) return bind_param(PARAM_TABLE_PREFIX .. name) end function get_time_sec() return millis():tofloat() * 0.001 end -- Type conversion functions function get_uint16(frame, ofs) return frame:data(ofs) + (frame:data(ofs + 1) << 8) end function constrain(v, vmin, vmax) if v < vmin then v = vmin end if v > vmax then v = vmax end return v end local efi_backend = nil -- Setup EFI Parameters assert(param:add_table(PARAM_TABLE_KEY, PARAM_TABLE_PREFIX, 15), 'could not add EFI_SP param table') --[[ // @Param: EFI_SP_ENABLE // @DisplayName: Enable SkyPower EFI support // @Description: Enable SkyPower EFI support // @Values: 0:Disabled,1:Enabled // @User: Standard --]] local EFI_SP_ENABLE = bind_add_param('ENABLE', 1, 0) --[[ // @Param: EFI_SP_CANDRV // @DisplayName: Set SkyPower EFI CAN driver // @Description: Set SkyPower EFI CAN driver // @Values: 0:None,1:1stCANDriver,2:2ndCanDriver // @User: Standard --]] local EFI_SP_CANDRV = bind_add_param('CANDRV', 2, 1) -- CAN driver to use --[[ // @Param: EFI_SP_UPDATE_HZ // @DisplayName: SkyPower EFI update rate // @Description: SkyPower EFI update rate // @Range: 10 200 // @Units: Hz // @User: Advanced --]] local EFI_SP_UPDATE_HZ = bind_add_param('UPDATE_HZ', 3, 200) -- Script update frequency in Hz --[[ // @Param: EFI_SP_THR_FN // @DisplayName: SkyPower EFI throttle function // @Description: SkyPower EFI throttle function. This sets which SERVOn_FUNCTION to use for the target throttle. This should be 70 for fixed wing aircraft and 31 for helicopter rotor speed control // @Values: 0:Disabled,70:FixedWing,31:HeliRSC // @User: Standard --]] local EFI_SP_THR_FN = bind_add_param('THR_FN', 4, 0) -- servo function for throttle --[[ // @Param: EFI_SP_THR_RATE // @DisplayName: SkyPower EFI throttle rate // @Description: SkyPower EFI throttle rate. This sets rate at which throttle updates are sent to the engine // @Range: 10 100 // @Units: Hz // @User: Advanced --]] local EFI_SP_THR_RATE = bind_add_param('THR_RATE', 5, 50) -- throttle update rate --[[ // @Param: EFI_SP_START_FN // @DisplayName: SkyPower EFI start function // @Description: SkyPower EFI start function. This is the RCn_OPTION value to use to find the R/C channel used for controlling engine start // @Values: 0:Disabled,300:300,301:301,302:302,303:303,304:304,305:305,306:306,307:307 // @User: Standard --]] local EFI_SP_START_FN = bind_add_param('START_FN', 6, 0) -- start control function (RC option) --[[ // @Param: EFI_SP_GEN_FN // @DisplayName: SkyPower EFI generator control function // @Description: SkyPower EFI generator control function. This is the RCn_OPTION value to use to find the R/C channel used for controlling generator start/stop // @Values: 0:Disabled,300:300,301:301,302:302,303:303,304:304,305:305,306:306,307:307 // @User: Standard --]] local EFI_SP_GEN_FN = bind_add_param('GEN_FN', 7, 0) -- generator control function (RC option) --[[ // @Param: EFI_SP_MIN_RPM // @DisplayName: SkyPower EFI minimum RPM // @Description: SkyPower EFI minimum RPM. This is the RPM below which the engine is considered to be stopped // @Range: 1 1000 // @User: Advanced --]] local EFI_SP_MIN_RPM = bind_add_param('MIN_RPM', 8, 100) -- min RPM, for engine restart --[[ // @Param: EFI_SP_TLM_RT // @DisplayName: SkyPower EFI telemetry rate // @Description: SkyPower EFI telemetry rate. This is the rate at which extra telemetry values are sent to the GCS // @Range: 1 10 // @Units: Hz // @User: Advanced --]] local EFI_SP_TLM_RT = bind_add_param('TLM_RT', 9, 0) -- rate for extra telemetry values --[[ // @Param: EFI_SP_LOG_RT // @DisplayName: SkyPower EFI log rate // @Description: SkyPower EFI log rate. This is the rate at which extra logging of the SkyPower EFI is performed // @Range: 1 50 // @Units: Hz // @User: Advanced --]] local EFI_SP_LOG_RT = bind_add_param('LOG_RT', 10, 10) -- rate for logging --[[ // @Param: EFI_SP_ST_DISARM // @DisplayName: SkyPower EFI allow start disarmed // @Description: SkyPower EFI allow start disarmed. This controls if starting the engine while disarmed is allowed // @Values: 0:Disabled,1:Enabled // @User: Standard --]] local EFI_SP_ST_DISARM = bind_add_param('ST_DISARM', 11, 0) -- allow start when disarmed --[[ // @Param: EFI_SP_MODEL // @DisplayName: SkyPower EFI ECU model // @Description: SkyPower EFI ECU model // @Values: 0:Default,1:SP_275 // @User: Standard --]] local EFI_SP_MODEL = bind_add_param('MODEL', 12, MODEL_DEFAULT) --[[ // @Param: EFI_SP_GEN_CTRL // @DisplayName: SkyPower EFI enable generator control // @Description: SkyPower EFI enable generator control // @Values: 0:Disabled,1:Enabled // @User: Standard --]] local EFI_SP_GEN_CTRL = bind_add_param('GEN_CRTL', 13, 1) --[[ // @Param: EFI_SP_RST_TIME // @DisplayName: SkyPower EFI restart time // @Description: SkyPower EFI restart time. If engine should be running and it has stopped for this amount of time then auto-restart. To disable this feature set this value to zero. // @Range: 0 10 // @User: Standard // @Units: s --]] local EFI_SP_RST_TIME = bind_add_param('RST_TIME', 14, 2) if EFI_SP_ENABLE:get() == 0 then gcs:send_text(0, string.format("EFISP: disabled")) return end -- Register for the CAN drivers local driver1 local CAN_BUF_LEN = 25 if EFI_SP_CANDRV:get() == 1 then driver1 = CAN.get_device(CAN_BUF_LEN) elseif EFI_SP_CANDRV:get() == 2 then driver1 = CAN.get_device2(CAN_BUF_LEN) end if not driver1 then gcs:send_text(0, string.format("EFISP: Failed to load driver")) return end local now_s = get_time_sec() function C_TO_KELVIN(temp) return temp + 273.15 end --[[ we allow the engine to run if either armed or the EFI_SP_ST_DISARM parameter is 1 --]] function allow_run_engine() return arming:is_armed() or EFI_SP_ST_DISARM:get() == 1 end --[[ EFI Engine Object --]] local function engine_control(_driver) local self = {} -- Build up the EFI_State that is passed into the EFI Scripting backend local efi_state = EFI_State() local cylinder_state = Cylinder_Status() -- private fields as locals local rpm = 0 local air_pressure = 0 local inj_time = 0 local target_load = 0 local current_load = 0 local throttle_angle = 0 local ignition_angle = 0 local supply_voltage = 0 local fuel_consumption_lph = 0 local fuel_total_l = 0 local last_fuel_s = 0 local driver = _driver local last_rpm_t = get_time_sec() local last_state_update_t = get_time_sec() local last_thr_update = get_time_sec() local last_telem_update = get_time_sec() local last_log_t = get_time_sec() local last_stop_message_t = get_time_sec() local engine_started = false local generator_started = false local engine_start_t = 0.0 local last_throttle = 0.0 local sensor_error_flags = 0 local thermal_limit_flags = 0 local starter_rpm = 0 -- frames for sending commands local FRM_500 = uint32_t(0x500) local FRM_505 = uint32_t(0x505) local FRM_506 = uint32_t(0x506) -- Generator Data Structure local gen = {} gen.amps = 0.0 gen.rpm = 0.0 gen.batt_current = 0.0 -- Temperature Data Structure local temps = {} temps.egt = 0.0 -- Exhaust Gas Temperature temps.cht = 0.0 -- Cylinder Head Temperature temps.imt = 0.0 -- intake manifold temperature temps.oilt = 0.0 -- oil temperature temps.cht2 = 0.0 temps.egt2 = 0.0 temps.imt2 = 0.0 temps.oil2 = 0.0 -- read telemetry packets function self.update_telemetry() local max_packets = 25 local count = 0 while count < max_packets do frame = driver:read_frame() count = count + 1 if not frame then break end -- All Frame IDs for this EFI Engine are not extended if not frame:isExtended() then self.handle_EFI_packet(frame) end end if last_rpm_t > last_state_update_t then -- update state if we have an updated RPM last_state_update_t = last_rpm_t self.set_EFI_State() end end -- handle an EFI packet function self.handle_EFI_packet(frame) local id = frame:id_signed() if EFI_SP_MODEL:get() == MODEL_SP_275 then -- updated telemetry for SP-275 ECU if id == 0x100 then rpm = get_uint16(frame, 0) ignition_angle = get_uint16(frame, 2) * 0.1 throttle_angle = get_uint16(frame, 4) * 0.1 last_rpm_t = get_time_sec() elseif id == 0x101 then air_pressure = get_uint16(frame, 4) elseif id == 0x102 then inj_time = get_uint16(frame, 4) -- inj_ang = get_uint16(frame, 2) * 0.1 elseif id == 0x104 then supply_voltage = get_uint16(frame, 0) * 0.1 elseif id == 0x105 then temps.cht = get_uint16(frame, 0) * 0.1 temps.imt = get_uint16(frame, 2) * 0.1 temps.egt = get_uint16(frame, 4) * 0.1 temps.oilt = get_uint16(frame, 6) * 0.1 elseif id == 0x107 then sensor_error_flags = get_uint16(frame, 0) thermal_limit_flags = get_uint16(frame, 2) elseif id == 0x107 then target_load = get_uint16(frame, 6) * 0.1 elseif id == 0x10C then temps.cht2 = get_uint16(frame, 4) * 0.1 temps.egt2 = get_uint16(frame, 6) * 0.1 elseif id == 0x10D then current_load = get_uint16(frame, 2) * 0.1 elseif id == 0x113 then gen.amps = get_uint16(frame, 2) elseif id == 0x2E0 then starter_rpm = get_uint16(frame, 4) elseif id == 0x10B then fuel_consumption_lph = get_uint16(frame,6)*0.001 if last_fuel_s > 0 then local dt = now_s - last_fuel_s local fuel_lps = fuel_consumption_lph / 3600.0 fuel_total_l = fuel_total_l + fuel_lps * dt end last_fuel_s = now_s end else -- original SkyPower driver if id == 0x100 then rpm = get_uint16(frame, 0) ignition_angle = get_uint16(frame, 2) * 0.1 throttle_angle = get_uint16(frame, 4) * 0.1 last_rpm_t = get_time_sec() elseif id == 0x101 then current_load = get_uint16(frame, 0) * 0.1 target_load = get_uint16(frame, 2) * 0.1 inj_time = get_uint16(frame, 4) -- inj_ang = get_uint16(frame, 6) * 0.1 elseif id == 0x104 then supply_voltage = get_uint16(frame, 0) * 0.1 ecu_temp = get_uint16(frame, 2) * 0.1 air_pressure = get_uint16(frame, 4) fuel_consumption_lph = get_uint16(frame,6)*0.001 if last_fuel_s > 0 then local dt = now_s - last_fuel_s local fuel_lps = fuel_consumption_lph / 3600.0 fuel_total_l = fuel_total_l + fuel_lps * dt end last_fuel_s = now_s elseif id == 0x105 then temps.cht = get_uint16(frame, 0) * 0.1 temps.imt = get_uint16(frame, 2) * 0.1 temps.egt = get_uint16(frame, 4) * 0.1 temps.oilt = get_uint16(frame, 6) * 0.1 end end end -- Build and set the EFI_State that is passed into the EFI Scripting backend function self.set_EFI_State() -- Cylinder_Status cylinder_state:cylinder_head_temperature(C_TO_KELVIN(temps.cht)) cylinder_state:exhaust_gas_temperature(C_TO_KELVIN(temps.egt)) cylinder_state:ignition_timing_deg(ignition_angle) cylinder_state:injection_time_ms(inj_time*0.001) efi_state:engine_speed_rpm(uint32_t(rpm)) efi_state:engine_load_percent(math.floor(current_load)) efi_state:fuel_consumption_rate_cm3pm(fuel_consumption_lph * 1000.0 / 60.0) efi_state:estimated_consumed_fuel_volume_cm3(fuel_total_l * 1000.0) efi_state:throttle_position_percent(math.floor(throttle_angle*100.0/90.0+0.5)) efi_state:atmospheric_pressure_kpa(air_pressure*0.1) efi_state:ignition_voltage(supply_voltage) efi_state:intake_manifold_temperature(C_TO_KELVIN(temps.imt)) efi_state:throttle_out(last_throttle * 100) -- copy cylinder_state to efi_state efi_state:cylinder_status(cylinder_state) local last_efi_state_time = millis() efi_state:last_updated_ms(last_efi_state_time) -- Set the EFI_State into the EFI scripting driver efi_backend:handle_scripting(efi_state) end --- send throttle command, thr is 0 to 1 function self.send_throttle(thr) last_throttle = thr local msg = CANFrame() msg:id(FRM_500) msg:data(0,1) msg:data(1,0) thr = math.floor(thr*1000) msg:data(2,thr&0xFF) msg:data(3,thr>>8) msg:dlc(8) driver:write_frame(msg, 10000) end -- send an engine start command function self.send_engine_start() if EFI_SP_MODEL:get() == MODEL_SP_275 then -- the SP-275 needs a stop before a start will work self.send_engine_stop() end local msg = CANFrame() msg:id(FRM_505) msg:data(0,10) msg:dlc(8) driver:write_frame(msg, 10000) end -- send an engine stop command function self.send_engine_stop() local msg = CANFrame() msg:id(FRM_505) msg:data(7,10) msg:dlc(8) driver:write_frame(msg, 10000) local now = get_time_sec() if now - last_stop_message_t > 0.5 then last_stop_message_t = now gcs:send_text(0, string.format("EFISP: stopping engine")) end end -- start generator function self.send_generator_start() local msg = CANFrame() msg:id(FRM_506) msg:data(2,10) msg:dlc(8) driver:write_frame(msg, 10000) end -- stop generator function self.send_generator_stop() local msg = CANFrame() msg:id(FRM_506) msg:data(2,0) msg:dlc(8) driver:write_frame(msg, 10000) end -- update starter control function self.update_starter() local start_fn = EFI_SP_START_FN:get() if start_fn == 0 then return end local start_state = rc:get_aux_cached(start_fn) if start_state == nil then start_state = 0 end local should_be_running = false if start_state == 0 and engine_started then engine_started = false engine_start_t = 0 self.send_engine_stop() end if start_state == 2 and not engine_started and allow_run_engine() then engine_started = true gcs:send_text(0, string.format("EFISP: starting engine")) engine_start_t = get_time_sec() self.send_engine_start() should_be_running = true end if start_state > 0 and engine_started and allow_run_engine() then should_be_running = true end local min_rpm = EFI_SP_MIN_RPM:get() if min_rpm > 0 and engine_started and rpm < min_rpm and allow_run_engine() then local now = get_time_sec() local dt = now - engine_start_t if EFI_SP_RST_TIME:get() > 0 and dt > EFI_SP_RST_TIME:get() then gcs:send_text(0, string.format("EFISP: re-starting engine")) self.send_engine_start() engine_start_t = get_time_sec() end end --[[ cope with lost engine stop packets --]] if rpm > min_rpm and not should_be_running then engine_started = false engine_start_t = 0 self.send_engine_stop() end end -- update generator control function self.update_generator() if EFI_SP_GEN_CTRL:get() == 0 then return end local gen_state = rc:get_aux_cached(EFI_SP_GEN_FN:get()) if gen_state == 0 and generator_started then generator_started = false gcs:send_text(0, string.format("EFISP: stopping generator")) self.send_generator_stop() end if gen_state == 2 and not generator_started then generator_started = true gcs:send_text(0, string.format("EFISP: starting generator")) self.send_generator_start() end end -- update throttle output function self.update_throttle() local thr_func = EFI_SP_THR_FN:get() local thr_rate = EFI_SP_THR_RATE:get() if thr_func == 0 or thr_rate == 0 then return end local now = get_time_sec() if now - last_thr_update < 1.0 / thr_rate then return end last_thr_update = now local thr = 0.0 local scaled = SRV_Channels:get_output_scaled(thr_func) if thr_func == K_THROTTLE then thr = scaled * 0.01 elseif thr_func == K_HELIRSC then thr = scaled * 0.001 end self.send_throttle(thr) end -- update telemetry output for extra telemetry values function self.update_telem_out() local rate = EFI_SP_TLM_RT:get() if rate <= 0 then return end local now = get_time_sec() if now - last_telem_update < 1.0 / rate then return end last_telem_update = now gcs:send_named_float('EFI_OILTMP', temps.oilt) gcs:send_named_float('EFI_TRLOAD', target_load) gcs:send_named_float('EFI_VOLTS', supply_voltage) gcs:send_named_float('EFI_GEN_AMPS', gen.amps) gcs:send_named_float('EFI_CHT2', temps.cht2) gcs:send_named_float('EFI_STARTRPM', starter_rpm) end -- update custom logging function self.update_logging() local rate = EFI_SP_LOG_RT:get() if rate <= 0 then return end local now = get_time_sec() if now - last_log_t < 1.0 / rate then return end last_log_t = now logger.write('EFSP','Thr,CLoad,TLoad,OilT,RPM,gRPM,gAmp,gCur,SErr,TLim,STRPM', 'ffffffffHHH', last_throttle, current_load, target_load, temps.oilt, rpm, gen.rpm, gen.amps, gen.batt_current, sensor_error_flags, thermal_limit_flags, starter_rpm) end -- return the instance return self end -- end function engine_control local engine1 = engine_control(driver1) function update() now_s = get_time_sec() if not efi_backend then efi_backend = efi:get_backend(0) if not efi_backend then return end end -- Parse Driver Messages engine1.update_telemetry() engine1.update_starter() engine1.update_generator() engine1.update_throttle() engine1.update_telem_out() engine1.update_logging() end gcs:send_text(0, SCRIPT_NAME .. string.format(" loaded")) -- wrapper around update(). This calls update() and if update faults -- then an error is displayed, but the script is not stopped function protected_wrapper() local success, err = pcall(update) if not success then gcs:send_text(MAV_SEVERITY_ERROR, "Internal Error: " .. err) -- when we fault we run the update function again after 1s, slowing it -- down a bit so we don't flood the console with errors return protected_wrapper, 1000 end return protected_wrapper, 1000 / EFI_SP_UPDATE_HZ:get() end -- start running update loop return protected_wrapper()