From 842a4f507b66f3a99852180e61ad4854a34ae3bf Mon Sep 17 00:00:00 2001 From: Andrew Tridgell Date: Tue, 2 Jul 2024 12:01:20 +1000 Subject: [PATCH] AP_Scripting: added NMEA 2000 EFI driver this has been tested on a marine engine, and correctly produces key telemetry data the NMEA_2000.lua module is broken out to make it easier to add other NMEA 2000 based drivers --- libraries/AP_Scripting/drivers/EFI_NMEA2k.lua | 196 ++++++++++++++++++ libraries/AP_Scripting/drivers/EFI_NMEA2k.md | 45 ++++ libraries/AP_Scripting/modules/NMEA_2000.lua | 119 +++++++++++ 3 files changed, 360 insertions(+) create mode 100644 libraries/AP_Scripting/drivers/EFI_NMEA2k.lua create mode 100644 libraries/AP_Scripting/drivers/EFI_NMEA2k.md create mode 100644 libraries/AP_Scripting/modules/NMEA_2000.lua diff --git a/libraries/AP_Scripting/drivers/EFI_NMEA2k.lua b/libraries/AP_Scripting/drivers/EFI_NMEA2k.lua new file mode 100644 index 0000000000..42993852ee --- /dev/null +++ b/libraries/AP_Scripting/drivers/EFI_NMEA2k.lua @@ -0,0 +1,196 @@ +--[[ + EFI driver using NMEA 2000 marine CAN protocol +--]] + +local MAV_SEVERITY = {EMERGENCY=0, ALERT=1, CRITICAL=2, ERROR=3, WARNING=4, NOTICE=5, INFO=6, DEBUG=7} + +PARAM_TABLE_KEY = 48 +PARAM_TABLE_PREFIX = "EFI_2K_" + +local efi_backend = nil +local efi_state = EFI_State() +local cylinder_state = Cylinder_Status() +if not efi_state or not cylinder_state then + return +end + +-- bind a parameter to a variable given +function bind_param(name) + local p = Parameter(name) + assert(p, 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 + +-- Setup EFI Parameters +assert(param:add_table(PARAM_TABLE_KEY, PARAM_TABLE_PREFIX, 10), 'could not add EFI_2K param table') + +--[[ + // @Param: EFI_2K_ENABLE + // @DisplayName: Enable NMEA 2000 EFI driver + // @Description: Enable NMEA 2000 EFI driver + // @Values: 0:Disabled,1:Enabled + // @User: Standard +--]] +local EFI_2K_ENABLE = bind_add_param('ENABLE', 1, 1) +if EFI_2K_ENABLE:get() < 1 then + return +end + +--[[ + // @Param: EFI_2K_CANDRV + // @DisplayName: NMEA 2000 CAN driver + // @Description: NMEA 2000 CAN driver. Use 1 for first CAN scripting driver, 2 for 2nd driver + // @Values: 0:Disabled,1:FirstCAN,2:SecondCAN + // @User: Standard +--]] +local EFI_2K_CANDRV = bind_add_param('CANDRV', 2, 0) -- CAN driver number + +--[[ + // @Param: EFI_2K_OPTIONS + // @DisplayName: NMEA 2000 options + // @Description: NMEA 2000 driver options + // @Bitmask: 0:EnableLogging + // @User: Standard +--]] +EFI_2K_OPTIONS = bind_add_param("OPTIONS", 3, 0) + +local OPTION_LOGGING = (1<<0) + +--[[ + return true if an option is enabled +--]] +local function option_enabled(option) + return (EFI_2K_OPTIONS:get() & option) ~= 0 +end + +-- Register for the CAN drivers +local CAN_BUF_LEN = 25 +local can_driver = nil + +if EFI_2K_CANDRV:get() == 1 then + gcs:send_text(MAV_SEVERITY.INFO, string.format("EFI_2K: attaching to CAN1")) + can_driver = CAN:get_device(CAN_BUF_LEN) +elseif EFI_2K_CANDRV:get() == 2 then + gcs:send_text(MAV_SEVERITY.INFO, string.format("EFI_2K: attaching to CAN2")) + can_driver = CAN:get_device2(CAN_BUF_LEN) +end + +if not can_driver then + gcs:send_text(MAV_SEVERITY.ERROR, string.format("EFI_2K: invalid CAN driver")) + return +end + +-- load NMEA_2000 module +local NMEA_2000 = require("NMEA_2000") +if not NMEA_2000 then + gcs:send_text(MAV_SEVERITY.ERROR, "EFI_2K: Unable to load NMEA_2000.lua module") + return +end + +--[[ + create PGN table +--]] +local PGN_ENGINE_PARAM_RAPID = 0x1F200 +local PGN_ENGINE_PARAM_DYNAMIC = 0x1F201 + +local PGN_TABLE = { + [PGN_ENGINE_PARAM_RAPID] = 8, + [PGN_ENGINE_PARAM_DYNAMIC] = 26 +} + +NMEA_2000.set_PGN_table(PGN_TABLE) + +local frame_count = 0 +local state = {} + +local function log_frame(frame) + local id = frame:id() + logger:write("CANF",'Id,DLC,FC,B0,B1,B2,B3,B4,B5,B6,B7','IBIBBBBBBBB', + 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 + +--[[ + parse the higher rate engine data, giving RPM and pressure +--]] +local function parse_engine_param_rapid(data) + state.instance, state.speed, state.boost_presssure, state.tilt, _ = string.unpack("> 16) & 0xFF + local RDP = (message_id >> 24) & 0x3 + if PF < 0xF0 then + return (RDP << 16) | (PF << 8) + else + local PS = (message_id >> 8) & 0xFF + return (RDP << 16) | (PF << 8) | PS + end +end + +--[[ + extract data from a CAN frame as a lua binary string +--]] +local function extract_data(frame, max_len) + local ret = "" + local dlc = frame:dlc() + local len = math.min(dlc, max_len) + for ofs = 1, len do + ret = ret .. string.char(frame:data(ofs-1)) + end + return ret +end + +--[[ + set table of PGNs that we are interested in along with the expected packet size + + The table should be indexed by the PGN and give the expected size + of that PGN any frames with PGNs not in this table will be + discarded +--]] +function M.set_PGN_table(t) + M.PGN_table = t +end + +-- Parse CAN frame and reassemble messages +function M.parse(can_frame) + if not can_frame:isExtended() then + -- NMEA 2000 frame are always extended (29 bit address) + return nil + end + local message_id = can_frame:id_signed() + + local pgn = extract_pgn(message_id) + local dlc = can_frame:dlc() + + local exp_size = M.PGN_table[pgn] + if not exp_size then + -- discard unwated frame and reset pending + M.pending.pgn = nil + return nil + end + + if exp_size <= 8 and exp_size > dlc then + -- discard short frame + M.pending.pgn = nil + return nil + end + + if exp_size <= 8 then + -- single frame + local data = extract_data(can_frame, exp_size) + M.pending.pgn = nil + return pgn, data + end + + -- multi-frame + local data = extract_data(can_frame, dlc) + local subframe = string.byte(data, 1) & 0x1F + if M.pending.pgn ~= pgn or subframe ~= M.pending.count then + -- reset + M.pending.pgn = nil + M.pending.data = "" + M.pending.count = 0 + + if subframe ~= 0 then + -- discard, lost first frame or out of order + return nil + end + end + + if subframe == 0 then + M.pending.expected_size = string.byte(data, 2) + if M.pending.expected_size < exp_size then + M.pending.pgn = nil + return nil + end + M.pending.data = M.pending.data .. string.sub(data, 3, #data) + else + M.pending.data = M.pending.data .. string.sub(data, 2, #data) + end + M.pending.pgn = pgn + M.pending.count = M.pending.count + 1 + + -- do we have a complete frame + if #M.pending.data >= M.pending.expected_size then + M.pending.pgn = nil + return pgn, M.pending.data + end + + return nil +end + +return M