Files
ardupilot/libraries/AP_Scripting/drivers/EFI_Halo6000.lua
George Zogopoulos 8f7fbc8546 AP_Scripting: EFI HALO6000 fix
Sets the "is flying" bit of the 1A0 control message to 1.
This should force the engine to think the aircraft is in the air and
start producing power.
2025-04-21 21:53:00 +10:00

377 lines
11 KiB
Lua

--[[
EFI Scripting backend driver for Halo6000 generator
--]]
---@diagnostic disable: param-type-mismatch
---@diagnostic disable: undefined-field
---@diagnostic disable: missing-parameter
---@diagnostic disable: need-check-nil
-- Check Script uses a miniumum firmware version
local SCRIPT_AP_VERSION = 4.3
local SCRIPT_NAME = "EFI: Halo6000 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 = {EMERGENCY=0, ALERT=1, CRITICAL=2, ERROR=3, WARNING=4, NOTICE=5, INFO=6, DEBUG=7}
PARAM_TABLE_KEY = 40
PARAM_TABLE_PREFIX = "EFI_H6K_"
UPDATE_RATE_HZ = 40
-- 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_uint8(frame, ofs)
return frame:data(ofs)
end
function get_uint16(frame, ofs)
return frame:data(ofs) + (frame:data(ofs+1) << 8)
end
function constrain(v, vmin, vmax)
return math.max(math.min(v,vmax),vmin)
end
local efi_backend = nil
-- Setup EFI Parameters
assert(param:add_table(PARAM_TABLE_KEY, PARAM_TABLE_PREFIX, 10), 'could not add EFI_H6K param table')
--[[
// @Param: EFI_H6K_ENABLE
// @DisplayName: Enable Halo6000 EFI driver
// @Description: Enable Halo6000 EFI driver
// @Values: 0:Disabled,1:Enabled
// @User: Standard
--]]
local EFI_H6K_ENABLE = bind_add_param('ENABLE', 1, 0)
--[[
// @Param: EFI_H6K_CANDRV
// @DisplayName: Halo6000 CAN driver
// @Description: Halo6000 CAN driver. Use 1 for first CAN scripting driver, 2 for 2nd driver
// @Values: 0:Disabled,1:FirstCAN,2:SecondCAN
// @User: Standard
--]]
local EFI_H6K_CANDRV = bind_add_param('CANDRV', 2, 0) -- CAN driver number
--[[
// @Param: EFI_H6K_START_FN
// @DisplayName: Halo6000 start auxilliary function
// @Description: The RC auxilliary function number for start/stop of the generator. Zero to disable start function
// @Values: 0:Disabled,300:300,301:301,302:302,303:303,304:304,305:305,306:306,307:307
// @User: Standard
--]]
local EFI_H6K_START_FN = bind_add_param('START_FN', 3, 0)
--[[
// @Param: EFI_H6K_TELEM_RT
// @DisplayName: Halo6000 telemetry rate
// @Description: The rate that additional generator telemetry is sent
// @Units: Hz
// @User: Standard
--]]
local EFI_H6K_TELEM_RT = bind_add_param('TELEM_RT', 4, 2)
--[[
// @Param: EFI_H6K_FUELTOT
// @DisplayName: Halo6000 total fuel capacity
// @Description: The capacity of the tank in litres
// @Units: litres
// @User: Standard
--]]
local EFI_H6K_FUELTOT = bind_add_param('FUELTOT', 5, 20)
--[[
// @Param: EFI_H6K_OPTIONS
// @DisplayName: Halo6000 options
// @Description: Halo6000 options
// @Bitmask: 0:LogAllCanPackets
// @User: Standard
--]]
local EFI_H6K_OPTIONS = bind_add_param('OPTIONS', 6, 0)
local OPTION_LOGALLFRAMES = 0x01
if EFI_H6K_ENABLE:get() == 0 then
return
end
-- Register for the CAN drivers
local CAN_BUF_LEN = 25
if EFI_H6K_CANDRV:get() == 1 then
gcs:send_text(0, string.format("EFI_H6K: attaching to CAN1"))
driver1 = CAN.get_device(CAN_BUF_LEN)
elseif EFI_H6K_CANDRV:get() == 2 then
gcs:send_text(0, string.format("EFI_H6K: attaching to CAN2"))
driver1 = CAN.get_device2(CAN_BUF_LEN)
end
if not driver1 then
gcs:send_text(0, string.format("EFI_H6K: Failed to load driver"))
return
end
local frame_count = 0
--[[
frame logging - can be replayed with Tools/scripts/CAN/CAN_playback.py
--]]
local function log_can_frame(frame)
logger.write("CANF",'Id,DLC,FC,B0,B1,B2,B3,B4,B5,B6,B7','IBIBBBBBBBB',
frame:id(),
frame:dlc(),
frame_count,
frame:data(0), frame:data(1), frame:data(2), frame:data(3),
frame:data(4), frame:data(5), frame:data(6), frame:data(7))
frame_count = frame_count + 1
end
--[[
EFI Engine Object
--]]
local function engine_control()
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 throttle_pos = 0
local fuel_pct = 0
local cyl_temp = 0
local cool_temp = 0
local out_volt = 0
local out_current = 0
local last_state_update_t = 0
local last_rpm_t = 0
local last_telem_update_t = 0
local last_stop_message_t = 0
local C_TO_KELVIN = 273.2
local engine_started = false
-- read telemetry packets
function self.update_telemetry()
local max_packets = 25
local count = 0
while count < max_packets do
frame = driver1:read_frame()
count = count + 1
if not frame then
break
end
if EFI_H6K_OPTIONS:get() & OPTION_LOGALLFRAMES ~= 0 then
log_can_frame(frame)
end
-- All Frame IDs for this EFI Engine are in the 11-bit address space
if not frame:isExtended() then
self.handle_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_packet(frame)
local id = frame:id_signed()
if id == 0x1c1 then
-- 20Hz telem
rpm = get_uint16(frame, 0)
last_rpm_t = get_time_sec()
throttle_pos = get_uint16(frame, 2)*0.1
fuel_pct = get_uint8(frame, 4)*0.5
cyl_temp = get_uint8(frame, 5) - 40
cool_temp = get_uint8(frame, 6) - 40
elseif id == 0x1c2 then
-- 20Hz telem2
out_volt = get_uint16(frame, 0)*0.2
out_current = get_uint16(frame, 2)*0.2 - 200.0
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(cyl_temp + C_TO_KELVIN)
cylinder_state:exhaust_gas_temperature(cool_temp + C_TO_KELVIN)
efi_state:engine_speed_rpm(uint32_t(rpm))
efi_state:intake_manifold_temperature(cool_temp + C_TO_KELVIN)
efi_state:throttle_position_percent(math.floor(throttle_pos))
efi_state:ignition_voltage(out_volt)
efi_state:estimated_consumed_fuel_volume_cm3((100.0-fuel_pct)*0.01*EFI_H6K_FUELTOT:get()*1000)
-- copy cylinder_state to efi_state
efi_state:cylinder_status(cylinder_state)
efi_state:last_updated_ms(millis())
-- Set the EFI_State into the EFI scripting driver
efi_backend:handle_scripting(efi_state)
logger.write('H6K','Curr,Volt,CoolT,CylT,FuelPct', 'fffff',
out_current, out_volt, cool_temp, cyl_temp, fuel_pct)
end
-- update telemetry output for extra telemetry values
function self.update_telem_out()
local rate = EFI_H6K_TELEM_RT:get()
if rate <= 0 then
return
end
local now = get_time_sec()
if now - last_telem_update_t < 1.0 / rate then
return
end
last_telem_update_t = now
gcs:send_named_float('H6K_FUEL', fuel_pct)
gcs:send_named_float('H6K_CYLT', cyl_temp)
gcs:send_named_float('H6K_COOLT', cool_temp)
gcs:send_named_float('H6K_AMPS', out_current)
end
-- send a frame with checksum
function self.write_frame_checksum(msg)
local sum = 0
for i = 0, 6 do
sum = sum + msg:data(i)
end
local checksum = (0 - sum) % 0x100
msg:data(7, checksum)
msg:dlc(8)
driver1:write_frame(msg, 10000)
end
-- send an engine start command
function self.send_engine_start()
local msg = CANFrame()
msg:id(0x1A0)
msg:data(0,1)
msg:data(7,1)
self.write_frame_checksum(msg)
end
-- send an engine stop command
function self.send_engine_stop()
local msg = CANFrame()
msg:id(0x1A0)
msg:data(0,0)
self.write_frame_checksum(msg)
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("EFIH6K: stopping generator"))
end
end
-- update starter control
function self.update_starter()
local start_fn = EFI_H6K_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 then
engine_started = true
gcs:send_text(0, string.format("EFIH6K: starting generator"))
engine_start_t = get_time_sec()
self.send_engine_start()
should_be_running = true
end
if start_state > 0 and engine_started then
should_be_running = true
end
local min_rpm = 500
if min_rpm > 0 and engine_started and rpm < min_rpm then
local now = get_time_sec()
local dt = now - engine_start_t
if dt > 2.0 then
gcs:send_text(0, string.format("EFIH6K: re-starting generator"))
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
-- return the instance
return self
end -- end function engine_control()
local engine1 = engine_control()
function update()
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_telem_out()
engine1.update_starter()
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 / UPDATE_RATE_HZ
end
-- start running update loop
return protected_wrapper()