-- get GPS data from ardupilot's native bindings then resynthesize into a -- virtual NMEA GPS and feed back through the serial device sim bindings. -- demonstrates the bindings and provides the opportunity for script-controlled -- tampering and other such activities. -- parameters: -- SCR_ENABLE 1 -- SCR_SDEV_EN 1 -- SCR_SDEV1_PROTO 5 -- SERIAL3_PROTOCOL 5 -- SERIAL4_PROTOCOL -1 -- GPS2_TYPE 5 -- GPS_PRIMARY 1 -- GPS_AUTO_SWITCH 0 local ser_device = serial:find_simulated_device(5, 0) if not ser_device then error("SCR_SDEV_EN must be 1 and SCR_SDEVn_PROTO must be 5") end function convert_coord(coord, dir) -- convert ardupilot degrees*1e7 to NMEA degrees + decimal minutes + dir. -- the first character of dir is used if the coordinate is positive, -- the second if negative. -- handle sign if coord < 0 then coord = -coord dir = dir:sub(2, 2) else dir = dir:sub(1, 1) end local degrees = coord // 10000000 -- integer divide coord = coord - (degrees * 10000000) -- remove that portion local minutes = coord * (60/10000000) -- float divide return ("%03d%08.5f,%s"):format(degrees, minutes, dir) end function convert_time(time_week, time_week_ms) -- convert ardupilot GPS time to NMEA UTC date/time strings -- GPS week 1095 starts on Dec 31 2000 local seconds_per_week = uint32_t(86400*7) timestamp_s = uint32_t(time_week - 1095)*seconds_per_week -- subtract one day to get to Jan 1 2001, then 18 additional seconds to -- account for the GPS to UTC leap second induced offset timestamp_s = timestamp_s - uint32_t(86400 + 18) -- add in time within the week timestamp_s = timestamp_s + (time_week_ms/uint32_t(1000)) timestamp_s = timestamp_s:toint() -- seconds since Jan 1 2001 local ts_year = 2001 local day_seconds = 86400 while true do local year_seconds = day_seconds * ((ts_year % 4 == 0) and 366 or 365) if timestamp_s >= year_seconds then timestamp_s = timestamp_s - year_seconds ts_year = ts_year + 1 else break end end local month_days = {31, (ts_year % 4 == 0) and 29 or 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31} local ts_month = 1 for _, md in ipairs(month_days) do local month_seconds = 86400 * md if timestamp_s >= month_seconds then timestamp_s = timestamp_s - month_seconds ts_month = ts_month + 1 else break end end local ts_day = 1+(timestamp_s // 86400) timestamp_s = timestamp_s % 86400 local ts_hour = timestamp_s // 3600 timestamp_s = timestamp_s % 3600 local ts_minute = timestamp_s // 60 local ts_second = timestamp_s % 60 local date = ("%02d%02d%02d"):format(ts_year-2000, ts_month, ts_day) local time = ("%02d%02d%02d.%01d"):format(ts_hour, ts_minute, ts_second, (time_week_ms % 1000):toint()//100) return date, time end function get_gps_data(instance) -- get GPS data from ardupilot scripting bindings in native format local data = { hdop = gps:get_hdop(instance), time_week_ms = gps:time_week_ms(instance), time_week = gps:time_week(instance), sats = gps:num_sats(instance), crs = gps:ground_course(instance), spd = gps:ground_speed(instance), loc = gps:location(instance), status = gps:status(instance), } if data.status < gps.GPS_OK_FIX_3D then return nil -- don't bother with invalid data end return data end function arrange_nmea(data) -- convert ardupilot data entries to NMEA-compatible format local ts_date, ts_time = convert_time(data.time_week, data.time_week_ms) return { time = ts_time, lat = convert_coord(data.loc:lat(), "NS"), lng = convert_coord(data.loc:lng(), "EW"), spd = data.spd / 0.514, -- m/s to knots crs = data.crs, -- degrees date = ts_date, sats = data.sats, hdop = data.hdop, alt = data.loc:alt()/100, } end function wrap_nmea(msg) -- compute checksum and add header and footer local checksum = 0 for i = 1,#msg do checksum = checksum ~ msg:byte(i, i) end return ("$%s*%02X\r\n"):format(msg, checksum) end function format_nmea(data) -- format data into complete NMEA sentences local rmc_raw = ("GPRMC,%s,A,%s,%s,%03f,%03f,%s,000.0,E"):format( data.time, data.lat, data.lng, data.spd, data.crs, data.date) local gga_raw = ("GPGGA,%s,%s,%s,1,%02d,%05.2f,%06.2f,M,0,M,,"):format( data.time, data.lat, data.lng, data.sats, data.hdop/100, data.alt) return wrap_nmea(rmc_raw), wrap_nmea(gga_raw) end function update() -- get data from first instance (we are the second) local ardu_data = get_gps_data(0) if ardu_data then local nmea_data = arrange_nmea(ardu_data) local rmc, gga = format_nmea(nmea_data) if ser_device:writestring(rmc) ~= #rmc or ser_device:writestring(gga) ~= #gga then error("overflow, ardupilot is not processing the data, check config!") end end return update, 200 -- 5Hz like a real GPS end return update()