import { i18n } from "../localization"; import GUI, { TABS } from "../gui"; import { tracking } from "../Analytics"; import { bit_check } from "../bit"; import VirtualFC from "../VirtualFC"; import FC from "../fc"; import MSP from "../msp"; import MSPCodes from "../msp/MSPCodes"; import CONFIGURATOR, { API_VERSION_1_45, API_VERSION_1_46, API_VERSION_1_47 } from "../data_storage"; import LogoManager from "../LogoManager"; import { gui_log } from "../gui_log"; import semver from "semver"; import jBox from "jbox"; import inflection from "inflection"; import debounce from "lodash.debounce"; import $ from "jquery"; import FileSystem from "../FileSystem"; import { have_sensor } from "../sensor_helpers"; const FONT = {}; const SYM = {}; const OSD = {}; SYM.loadSymbols = function () { SYM.BLANK = 0x20; SYM.VOLT = 0x06; SYM.RSSI = 0x01; SYM.LINK_QUALITY = 0x7b; SYM.AH_RIGHT = 0x02; SYM.AH_LEFT = 0x03; SYM.THR = 0x04; SYM.FLY_M = 0x9c; SYM.ON_M = 0x9b; SYM.AH_CENTER_LINE = 0x72; SYM.AH_CENTER = 0x73; SYM.AH_CENTER_LINE_RIGHT = 0x74; SYM.AH_BAR9_0 = 0x80; SYM.AH_DECORATION = 0x13; SYM.LOGO = 0xa0; SYM.AMP = 0x9a; SYM.MAH = 0x07; SYM.METRE = 0xc; SYM.FEET = 0xf; SYM.KPH = 0x9e; SYM.MPH = 0x9d; SYM.MPS = 0x9f; SYM.FTPS = 0x99; SYM.SPEED = 0x70; SYM.TOTAL_DIST = 0x71; SYM.GPS_SAT_L = 0x1e; SYM.GPS_SAT_R = 0x1f; SYM.GPS_LAT = 0x89; SYM.GPS_LON = 0x98; SYM.HOMEFLAG = 0x11; SYM.PB_START = 0x8a; SYM.PB_FULL = 0x8b; SYM.PB_EMPTY = 0x8d; SYM.PB_END = 0x8e; SYM.PB_CLOSE = 0x8f; SYM.BATTERY = 0x96; SYM.ARROW_NORTH = 0x68; SYM.ARROW_SOUTH = 0x60; SYM.ARROW_EAST = 0x64; SYM.ARROW_SMALL_UP = 0x75; SYM.ARROW_SMALL_RIGHT = 0x77; SYM.HEADING_LINE = 0x1d; SYM.HEADING_DIVIDED_LINE = 0x1c; SYM.HEADING_N = 0x18; SYM.HEADING_S = 0x19; SYM.HEADING_E = 0x1a; SYM.HEADING_W = 0x1b; SYM.TEMPERATURE = 0x7a; SYM.TEMP_F = 0x0d; SYM.TEMP_C = 0x0e; SYM.STICK_OVERLAY_SPRITE_HIGH = 0x08; SYM.STICK_OVERLAY_SPRITE_MID = 0x09; SYM.STICK_OVERLAY_SPRITE_LOW = 0x0a; SYM.STICK_OVERLAY_CENTER = 0x0b; SYM.STICK_OVERLAY_VERTICAL = 0x16; SYM.STICK_OVERLAY_HORIZONTAL = 0x17; SYM.BBLOG = 0x10; SYM.ALTITUDE = 0x7f; SYM.PITCH = 0x15; SYM.ROLL = 0x14; SYM.KM = 0x7d; SYM.MILES = 0x7e; }; FONT.initData = function () { if (FONT.data) { return; } FONT.data = { // default font file name loaded_font_file: "default", // array of arry of image bytes ready to upload to fc characters_bytes: [], // array of array of image bits by character characters: [], // an array of base64 encoded image strings by character character_image_urls: [], }; }; FONT.constants = { MAX_CHAR_COUNT: 256, SIZES: { /** NVM ram size for one font char, actual character bytes **/ MAX_NVM_FONT_CHAR_SIZE: 54, /** NVM ram field size for one font char, last 10 bytes dont matter **/ MAX_NVM_FONT_CHAR_FIELD_SIZE: 64, CHAR_HEIGHT: 18, CHAR_WIDTH: 12, }, COLORS: { // black 0: "rgba(0, 0, 0, 1)", // also the value 3, could yield transparent according to // https://www.sparkfun.com/datasheets/BreakoutBoards/MAX7456.pdf 1: "rgba(255, 255, 255, 0)", // white 2: "rgba(255,255,255, 1)", }, }; FONT.pushChar = function (fontCharacterBytes, fontCharacterBits) { // Only push full characters onto the stack. if (fontCharacterBytes.length !== FONT.constants.SIZES.MAX_NVM_FONT_CHAR_FIELD_SIZE) { return; } FONT.data.characters_bytes.push(fontCharacterBytes.slice(0)); FONT.data.characters.push(fontCharacterBits.slice(0)); FONT.draw(FONT.data.characters.length - 1); }; /** * Each line is composed of 8 asci 1 or 0, representing 1 bit each for a total of 1 byte per line */ FONT.parseMCMFontFile = function (dataFontFile) { const data = dataFontFile.trim().split("\n"); // clear local data FONT.data.characters.length = 0; FONT.data.characters_bytes.length = 0; FONT.data.character_image_urls.length = 0; // reset logo image info when font data is changed LogoManager.resetImageInfo(); // make sure the font file is valid if (data.shift().trim() !== "MAX7456") { const msg = "that font file doesnt have the MAX7456 header, giving up"; console.debug(msg); Promise.reject(msg); } const characterBits = []; const characterBytes = []; // hexstring is for debugging FONT.data.hexstring = []; for (let i = 0; i < data.length; i++) { const line = data[i]; // hexstring is for debugging FONT.data.hexstring.push(`0x${parseInt(line, 2).toString(16)}`); // every 64 bytes (line) is a char, we're counting chars though, which are 2 bits if (characterBits.length === FONT.constants.SIZES.MAX_NVM_FONT_CHAR_FIELD_SIZE * (8 / 2)) { FONT.pushChar(characterBytes, characterBits); characterBits.length = 0; characterBytes.length = 0; } for (let y = 0; y < 8; y = y + 2) { const v = parseInt(line.slice(y, y + 2), 2); characterBits.push(v); } characterBytes.push(parseInt(line, 2)); } // push the last char FONT.pushChar(characterBytes, characterBits); return FONT.data.characters; }; FONT.openFontFile = function () { return new Promise(function (resolve) { const suffix = "mcm"; FileSystem.pickOpenFile( i18n.getMessage("fileSystemPickerFiles", { typeof: suffix.toUpperCase() }), `.${suffix}`, ) .then((file) => { FONT.data.loaded_font_file = file.name; FileSystem.readFile(file).then((contents) => { FONT.parseMCMFontFile(contents); resolve(); }); }) .catch((error) => { console.error("could not load whole font file:", error); }); }); }; /** * returns a canvas image with the character on it */ const drawCanvas = function (charAddress) { const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); const pixelSize = 1; const width = pixelSize * FONT.constants.SIZES.CHAR_WIDTH; const height = pixelSize * FONT.constants.SIZES.CHAR_HEIGHT; canvas.width = width; canvas.height = height; for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { if (!(charAddress in FONT.data.characters)) { console.log("charAddress", charAddress, " is not in ", FONT.data.characters.length); } const v = FONT.data.characters[charAddress][y * width + x]; ctx.fillStyle = FONT.constants.COLORS[v]; ctx.fillRect(x, y, pixelSize, pixelSize); } } return canvas; }; FONT.draw = function (charAddress) { let cached = FONT.data.character_image_urls[charAddress]; if (!cached) { cached = FONT.data.character_image_urls[charAddress] = drawCanvas(charAddress).toDataURL("image/png"); } return cached; }; FONT.msp = { encode(charAddress) { return [charAddress].concat( FONT.data.characters_bytes[charAddress].slice(0, FONT.constants.SIZES.MAX_NVM_FONT_CHAR_SIZE), ); }, }; FONT.upload = function ($progress) { return FONT.data.characters .reduce( (p, x, i) => p.then(() => { $progress.val((i / FONT.data.characters.length) * 100); return MSP.promise(MSPCodes.MSP_OSD_CHAR_WRITE, FONT.msp.encode(i)); }), Promise.resolve(), ) .then(function () { console.log(`Uploaded all ${FONT.data.characters.length} characters`); gui_log(i18n.getMessage("osdSetupUploadingFontEnd", { length: FONT.data.characters.length })); OSD.GUI.fontManager.close(); return MSP.promise(MSPCodes.MSP_SET_REBOOT); }); }; FONT.preview = function ($el) { $el.empty(); for (let i = 0; i < SYM.LOGO; i++) { const url = FONT.data.character_image_urls[i]; $el.append(``); } }; FONT.symbol = function (hexVal) { return hexVal === "" || hexVal === null ? "" : String.fromCharCode(hexVal); }; OSD.getNumberOfProfiles = function () { return OSD.data.osd_profiles.number; }; OSD.getCurrentPreviewProfile = function () { const osdprofileElement = $(".osdprofile-selector"); if (osdprofileElement.length > 0) { return osdprofileElement.val(); } else { return 0; } }; // parsed fc output and output to fc, used by to OSD.msp.encode OSD.initData = function () { OSD.data = { video_system: null, unit_mode: null, alarms: [], statItems: [], warnings: [], displayItems: [], timers: [], last_positions: {}, preview: [], tooltips: [], osd_profiles: {}, VIDEO_COLS: { PAL: 30, NTSC: 30, HD: 53, }, VIDEO_ROWS: { PAL: 16, NTSC: 13, HD: 20, }, VIDEO_BUFFER_CHARS: { PAL: 480, NTSC: 390, HD: 1590, }, }; }; OSD.initData(); OSD.getVariantForPreview = function (osdData, elementName) { return osdData.displayItems.find((element) => element.name === elementName).variant; }; OSD.generateAltitudePreview = function (osdData) { const unit = FONT.symbol(osdData.unit_mode === 0 ? SYM.FEET : SYM.METRE); const variantSelected = OSD.getVariantForPreview(osdData, "ALTITUDE"); return `${FONT.symbol(SYM.ALTITUDE)}399${variantSelected === 0 ? ".7" : ""}${unit}`; }; OSD.generateVTXChannelPreview = function (osdData) { const variantSelected = OSD.getVariantForPreview(osdData, "VTX_CHANNEL"); let value; switch (variantSelected) { case 0: value = "R:2:200:P"; break; case 1: value = "200"; break; } return value; }; OSD.generateBatteryUsagePreview = function (osdData) { const variantSelected = OSD.getVariantForPreview(osdData, "MAIN_BATT_USAGE"); let value; switch (variantSelected) { case 0: value = FONT.symbol(SYM.PB_START) + FONT.symbol(SYM.PB_FULL).repeat(9) + FONT.symbol(SYM.PB_END) + FONT.symbol(SYM.PB_EMPTY) + FONT.symbol(SYM.PB_CLOSE); break; case 1: value = FONT.symbol(SYM.PB_START) + FONT.symbol(SYM.PB_FULL).repeat(5) + FONT.symbol(SYM.PB_END) + FONT.symbol(SYM.PB_EMPTY).repeat(5) + FONT.symbol(SYM.PB_CLOSE); break; case 2: value = `${FONT.symbol(SYM.MAH)}67%`; break; case 3: value = `${FONT.symbol(SYM.MAH)}33%`; break; } return value; }; OSD.generateGpsLatLongPreview = function (osdData, elementName) { const variantSelected = OSD.getVariantForPreview(osdData, elementName); let value; switch (variantSelected) { case 0: value = elementName === "GPS_LON" ? `${FONT.symbol(SYM.GPS_LON)}-000.0000000` : `${FONT.symbol(SYM.GPS_LAT)}-00.0000000 `; break; case 1: value = elementName === "GPS_LON" ? `${FONT.symbol(SYM.GPS_LON)}-000.0000` : `${FONT.symbol(SYM.GPS_LAT)}-00.0000 `; break; case 2: const degreesSymbol = FONT.symbol(SYM.STICK_OVERLAY_SPRITE_HIGH); value = elementName === "GPS_LON" ? `${FONT.symbol(SYM.GPS_LON)}00${degreesSymbol}000'00.0"N` : `${FONT.symbol(SYM.GPS_LAT)}00${degreesSymbol}00'00.0"E `; break; case 3: value = `${FONT.symbol(SYM.GPS_SAT_L)}${FONT.symbol(SYM.GPS_SAT_R)}000000AA+BBB`; break; } return value; }; OSD.generateTimerPreview = function (osdData, timerIndex) { let preview = ""; switch (osdData.timers[timerIndex].src) { case 0: case 3: preview += FONT.symbol(SYM.ON_M); break; case 1: case 2: preview += FONT.symbol(SYM.FLY_M); break; } switch (osdData.timers[timerIndex].precision) { case 0: preview += "00:00"; break; case 1: preview += "00:00.00"; break; case 2: preview += "00:00.0"; break; } return preview; }; OSD.generateTemperaturePreview = function (osdData, temperature) { let preview = FONT.symbol(SYM.TEMPERATURE); switch (osdData.unit_mode) { case 0: let temperatureConversion = temperature * (9.0 / 5.0); temperatureConversion += 32.0; preview += Math.floor(temperatureConversion) + FONT.symbol(SYM.TEMP_F); break; case 1: case 2: preview += temperature + FONT.symbol(SYM.TEMP_C); break; } return preview; }; OSD.generateLQPreview = function () { const crsfIndex = FC.getSerialRxTypes().findIndex((name) => name === "CRSF"); const isXF = crsfIndex === FC.RX_CONFIG.serialrx_provider; return FONT.symbol(SYM.LINK_QUALITY) + (isXF ? "2:100" : "8"); }; OSD.generateCraftName = function () { let preview = "CRAFT_NAME"; const craftName = semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_45) ? FC.CONFIG.craftName : FC.CONFIG.name; if (craftName !== "") { preview = craftName.toUpperCase(); } return preview; }; // for backwards compatibility before API_VERSION_1_45 OSD.generateDisplayName = function () { let preview = "DISPLAY_NAME"; if (FC.CONFIG.displayName) { preview = FC.CONFIG.displayName?.toUpperCase(); } return preview; }; // added in API_VERSION_1_45 OSD.generatePilotName = function () { let preview = "PILOT_NAME"; if (FC.CONFIG.pilotName) { preview = FC.CONFIG.pilotName?.toUpperCase(); } return preview; }; OSD.drawStickOverlayPreview = function () { function randomInt(count) { return Math.floor(Math.random() * Math.floor(count)); } const STICK_OVERLAY_SPRITE = [ SYM.STICK_OVERLAY_SPRITE_HIGH, SYM.STICK_OVERLAY_SPRITE_MID, SYM.STICK_OVERLAY_SPRITE_LOW, ]; const OVERLAY_WIDTH = 7; const OVERLAY_HEIGHT = 5; const stickX = randomInt(OVERLAY_WIDTH); const stickY = randomInt(OVERLAY_HEIGHT); const stickSymbol = randomInt(3); // From 'osdDrawStickOverlayAxis' in 'src/main/io/osd.c' const stickOverlay = []; for (let x = 0; x < OVERLAY_WIDTH; x++) { for (let y = 0; y < OVERLAY_HEIGHT; y++) { let symbol = null; if (x === stickX && y === stickY) { symbol = STICK_OVERLAY_SPRITE[stickSymbol]; } else if (x === (OVERLAY_WIDTH - 1) / 2 && y === (OVERLAY_HEIGHT - 1) / 2) { symbol = SYM.STICK_OVERLAY_CENTER; } else if (x === (OVERLAY_WIDTH - 1) / 2) { symbol = SYM.STICK_OVERLAY_VERTICAL; } else if (y === (OVERLAY_HEIGHT - 1) / 2) { symbol = SYM.STICK_OVERLAY_HORIZONTAL; } if (symbol !== null) { const element = { x, y, sym: symbol, }; stickOverlay.push(element); } } } return stickOverlay; }; OSD.drawCameraFramePreview = function () { const FRAME_WIDTH = OSD.data.parameters.cameraFrameWidth; const FRAME_HEIGHT = OSD.data.parameters.cameraFrameHeight; const cameraFrame = []; for (let x = 0; x < FRAME_WIDTH; x++) { const sym = x === 0 || x === FRAME_WIDTH - 1 ? SYM.STICK_OVERLAY_CENTER : SYM.STICK_OVERLAY_HORIZONTAL; const frameUp = { x, y: 0, sym }; const frameDown = { x, y: FRAME_HEIGHT - 1, sym }; cameraFrame.push(frameUp); cameraFrame.push(frameDown); } for (let y = 1; y < FRAME_HEIGHT - 1; y++) { const sym = SYM.STICK_OVERLAY_VERTICAL; const frameLeft = { x: 0, y, sym }; const frameRight = { x: FRAME_WIDTH - 1, y, sym }; cameraFrame.push(frameLeft); cameraFrame.push(frameRight); } return cameraFrame; }; OSD.formatPidsPreview = function (axis) { const pidDefaults = FC.getPidDefaults(); const p = pidDefaults[axis * 5].toString().padStart(3); const i = pidDefaults[axis * 5 + 1].toString().padStart(3); const d = pidDefaults[axis * 5 + 2].toString().padStart(3); const f = pidDefaults[axis * 5 + 4].toString().padStart(3); return `${p} ${i} ${d} ${f}`; }; OSD.loadDisplayFields = function () { let videoType = OSD.constants.VIDEO_TYPES[OSD.data.video_system]; // All display fields, from every version, do not remove elements, only add! OSD.ALL_DISPLAY_FIELDS = { MAIN_BATT_VOLTAGE: { name: "MAIN_BATT_VOLTAGE", text: "osdTextElementMainBattVoltage", desc: "osdDescElementMainBattVoltage", defaultPosition: -29, draw_order: 20, positionable: true, preview: `${FONT.symbol(SYM.BATTERY)}16.8${FONT.symbol(SYM.VOLT)}`, }, RSSI_VALUE: { name: "RSSI_VALUE", text: "osdTextElementRssiValue", desc: "osdDescElementRssiValue", defaultPosition: -59, draw_order: 30, positionable: true, preview: `${FONT.symbol(SYM.RSSI)}99`, }, TIMER: { name: "TIMER", text: "osdTextElementTimer", desc: "osdDescElementTimer", defaultPosition: -39, positionable: true, preview: `${FONT.symbol(SYM.ON_M)} 11:11`, }, THROTTLE_POSITION: { name: "THROTTLE_POSITION", text: "osdTextElementThrottlePosition", desc: "osdDescElementThrottlePosition", defaultPosition: -9, draw_order: 110, positionable: true, preview: `${FONT.symbol(SYM.THR)} 69`, }, CPU_LOAD: { name: "CPU_LOAD", text: "osdTextElementCpuLoad", desc: "osdDescElementCpuLoad", defaultPosition: 26, positionable: true, preview: "15", }, VTX_CHANNEL: { name: "VTX_CHANNEL", text: "osdTextElementVtxChannel", desc: "osdDescElementVtxChannel", defaultPosition: 1, draw_order: 120, positionable: true, variants: ["osdTextElementVTXchannelVariantFull", "osdTextElementVTXchannelVariantPower"], preview(osdData) { return OSD.generateVTXChannelPreview(osdData); }, }, VOLTAGE_WARNING: { name: "VOLTAGE_WARNING", text: "osdTextElementVoltageWarning", desc: "osdDescElementVoltageWarning", defaultPosition: -80, positionable: true, preview: "LOW VOLTAGE", }, ARMED: { name: "ARMED", text: "osdTextElementArmed", desc: "osdDescElementArmed", defaultPosition: -107, positionable: true, preview: "ARMED", }, DISARMED: { name: "DISARMED", text: "osdTextElementDisarmed", desc: "osdDescElementDisarmed", defaultPosition: -109, draw_order: 280, positionable: true, preview: "DISARMED", }, CROSSHAIRS: { name: "CROSSHAIRS", text: "osdTextElementCrosshairs", desc: "osdDescElementCrosshairs", defaultPosition() { return ( (OSD.data.VIDEO_COLS[videoType] >> 1) + ((OSD.data.VIDEO_ROWS[videoType] >> 1) - 2) * OSD.data.VIDEO_COLS[videoType] - 2 ); }, draw_order: 40, positionable() { return true; }, preview() { return ( FONT.symbol(SYM.AH_CENTER_LINE) + FONT.symbol(SYM.AH_CENTER) + FONT.symbol(SYM.AH_CENTER_LINE_RIGHT) ); }, }, ARTIFICIAL_HORIZON: { name: "ARTIFICIAL_HORIZON", text: "osdTextElementArtificialHorizon", desc: "osdDescElementArtificialHorizon", defaultPosition() { return ( (OSD.data.VIDEO_COLS[videoType] >> 1) + ((OSD.data.VIDEO_ROWS[videoType] >> 1) - 5) * OSD.data.VIDEO_COLS[videoType] - 1 ); }, draw_order: 10, positionable() { return true; }, preview() { const artificialHorizon = []; for (let j = 1; j < 8; j++) { for (let i = -4; i <= 4; i++) { let element; // Blank char to mark the size of the element if (j !== 4) { element = { x: i, y: j, sym: SYM.BLANK }; // Sample of horizon } else { element = { x: i, y: j, sym: SYM.AH_BAR9_0 + 4 }; } artificialHorizon.push(element); } } return artificialHorizon; }, }, HORIZON_SIDEBARS: { name: "HORIZON_SIDEBARS", text: "osdTextElementHorizonSidebars", desc: "osdDescElementHorizonSidebars", defaultPosition() { return ( (OSD.data.VIDEO_COLS[videoType] >> 1) + ((OSD.data.VIDEO_ROWS[videoType] >> 1) - 2) * OSD.data.VIDEO_COLS[videoType] - 1 ); }, draw_order: 50, positionable() { return true; }, preview() { const horizonSidebar = []; const hudwidth = OSD.constants.AHISIDEBARWIDTHPOSITION; const hudheight = OSD.constants.AHISIDEBARHEIGHTPOSITION; let element; for (let i = -hudheight; i <= hudheight; i++) { element = { x: -hudwidth, y: i, sym: SYM.AH_DECORATION }; horizonSidebar.push(element); element = { x: hudwidth, y: i, sym: SYM.AH_DECORATION }; horizonSidebar.push(element); } // AH level indicators element = { x: -hudwidth + 1, y: 0, sym: SYM.AH_LEFT, }; horizonSidebar.push(element); element = { x: hudwidth - 1, y: 0, sym: SYM.AH_RIGHT, }; horizonSidebar.push(element); return horizonSidebar; }, }, CURRENT_DRAW: { name: "CURRENT_DRAW", text: "osdTextElementCurrentDraw", desc: "osdDescElementCurrentDraw", defaultPosition: -23, draw_order: 130, positionable: true, preview() { return ` 42.00${FONT.symbol(SYM.AMP)}`; }, }, MAH_DRAWN: { name: "MAH_DRAWN", text: "osdTextElementMahDrawn", desc: "osdDescElementMahDrawn", defaultPosition: -18, draw_order: 140, positionable: true, preview() { return ` 690${FONT.symbol(SYM.MAH)}`; }, }, CRAFT_NAME: { name: "CRAFT_NAME", text: "osdTextElementCraftName", desc: "osdDescElementCraftName", defaultPosition: -77, draw_order: 150, positionable: true, preview: OSD.generateCraftName, }, ALTITUDE: { name: "ALTITUDE", text: "osdTextElementAltitude", desc: "osdDescElementAltitude", defaultPosition: 62, draw_order: 160, positionable: true, variants: ["osdTextElementAltitudeVariant1DecimalAGL", "osdTextElementAltitudeVariantNoDecimalAGL"], preview(osdData) { return OSD.generateAltitudePreview(osdData); }, }, ONTIME: { name: "ONTIME", text: "osdTextElementOnTime", desc: "osdDescElementOnTime", defaultPosition: -1, positionable: true, preview: `${FONT.symbol(SYM.ON_M)}05:42`, }, FLYTIME: { name: "FLYTIME", text: "osdTextElementFlyTime", desc: "osdDescElementFlyTime", defaultPosition: -1, positionable: true, preview: `${FONT.symbol(SYM.FLY_M)}04:11`, }, FLYMODE: { name: "FLYMODE", text: "osdTextElementFlyMode", desc: "osdDescElementFlyMode", defaultPosition: -1, draw_order: 90, positionable: true, preview: "ANGL", }, GPS_SPEED: { name: "GPS_SPEED", text: "osdTextElementGPSSpeed", desc: "osdDescElementGPSSpeed", defaultPosition: -1, draw_order: 810, positionable: true, preview(osdData) { const UNIT_METRIC = OSD.constants.UNIT_TYPES.indexOf("METRIC"); const unit = FONT.symbol(osdData.unit_mode === UNIT_METRIC ? SYM.KPH : SYM.MPH); return `${FONT.symbol(SYM.SPEED)}40${unit}`; }, }, GPS_SATS: { name: "GPS_SATS", text: "osdTextElementGPSSats", desc: "osdDescElementGPSSats", defaultPosition: -1, draw_order: 800, positionable: true, preview: `${FONT.symbol(SYM.GPS_SAT_L)}${FONT.symbol(SYM.GPS_SAT_R)}14`, }, GPS_LON: { name: "GPS_LON", text: "osdTextElementGPSLon", desc: "osdDescElementGPSLon", defaultPosition: -1, draw_order: 830, positionable: true, variants: [ "osdTextElementGPSVariant7Decimals", "osdTextElementGPSVariant4Decimals", "osdTextElementGPSVariantDegMinSec", "osdTextElementGPSVariantOpenLocation", ], preview(osdData) { return OSD.generateGpsLatLongPreview(osdData, "GPS_LON"); }, }, GPS_LAT: { name: "GPS_LAT", text: "osdTextElementGPSLat", desc: "osdDescElementGPSLat", defaultPosition: -1, draw_order: 820, positionable: true, variants: [ "osdTextElementGPSVariant7Decimals", "osdTextElementGPSVariant4Decimals", "osdTextElementGPSVariantDegMinSec", "osdTextElementGPSVariantOpenLocation", ], preview(osdData) { return OSD.generateGpsLatLongPreview(osdData, "GPS_LAT"); }, }, DEBUG: { name: "DEBUG", text: "osdTextElementDebug", desc: "osdDescElementDebug", defaultPosition: -1, draw_order: 240, positionable: true, preview: "DBG 0 0 0 0", }, PID_ROLL: { name: "PID_ROLL", text: "osdTextElementPIDRoll", desc: "osdDescElementPIDRoll", defaultPosition: 0x800 | (10 << 5) | 2, // 0x0800 | (y << 5) | x draw_order: 170, positionable: true, preview: `ROL ${OSD.formatPidsPreview(0)}`, }, PID_PITCH: { name: "PID_PITCH", text: "osdTextElementPIDPitch", desc: "osdDescElementPIDPitch", defaultPosition: 0x800 | (11 << 5) | 2, // 0x0800 | (y << 5) | x draw_order: 180, positionable: true, preview: `PIT ${OSD.formatPidsPreview(1)}`, }, PID_YAW: { name: "PID_YAW", text: "osdTextElementPIDYaw", desc: "osdDescElementPIDYaw", defaultPosition: 0x800 | (12 << 5) | 2, // 0x0800 | (y << 5) | x draw_order: 190, positionable: true, preview: `YAW ${OSD.formatPidsPreview(2)}`, }, POWER: { name: "POWER", text: "osdTextElementPower", desc: "osdDescElementPower", defaultPosition: (15 << 5) | 2, draw_order: 200, positionable: true, preview() { return " 142W"; }, }, PID_RATE_PROFILE: { name: "PID_RATE_PROFILE", text: "osdTextElementPIDRateProfile", desc: "osdDescElementPIDRateProfile", defaultPosition: 0x800 | (13 << 5) | 2, // 0x0800 | (y << 5) | x draw_order: 210, positionable: true, preview: "1-2", }, BATTERY_WARNING: { name: "BATTERY_WARNING", text: "osdTextElementBatteryWarning", desc: "osdDescElementBatteryWarning", defaultPosition: -1, positionable: true, preview: "LOW VOLTAGE", }, AVG_CELL_VOLTAGE: { name: "AVG_CELL_VOLTAGE", text: "osdTextElementAvgCellVoltage", desc: "osdDescElementAvgCellVoltage", defaultPosition: 12 << 5, draw_order: 230, positionable: true, preview: `${FONT.symbol(SYM.BATTERY)}3.98${FONT.symbol(SYM.VOLT)}`, }, PITCH_ANGLE: { name: "PITCH_ANGLE", text: "osdTextElementPitchAngle", desc: "osdDescElementPitchAngle", defaultPosition: -1, draw_order: 250, positionable: true, preview: `${FONT.symbol(SYM.PITCH)}-00.0`, }, ROLL_ANGLE: { name: "ROLL_ANGLE", text: "osdTextElementRollAngle", desc: "osdDescElementRollAngle", defaultPosition: -1, draw_order: 260, positionable: true, preview: `${FONT.symbol(SYM.ROLL)}-00.0`, }, MAIN_BATT_USAGE: { name: "MAIN_BATT_USAGE", text: "osdTextElementMainBattUsage", desc: "osdDescElementMainBattUsage", defaultPosition: -17, draw_order: 270, positionable: true, variants: [ "osdTextElementMainBattUsageVariantGraphrRemain", "osdTextElementMainBattUsageVariantGraphUsage", "osdTextElementMainBattUsageVariantValueRemain", "osdTextElementMainBattUsageVariantValueUsage", ], preview(osdData) { return OSD.generateBatteryUsagePreview(osdData); }, }, ARMED_TIME: { name: "ARMED_TIME", text: "osdTextElementArmedTime", desc: "osdDescElementArmedTime", defaultPosition: -1, positionable: true, preview: `${FONT.symbol(SYM.FLY_M)}02:07`, }, HOME_DIR: { name: "HOME_DIRECTION", text: "osdTextElementHomeDirection", desc: "osdDescElementHomeDirection", defaultPosition: -1, draw_order: 850, positionable: true, preview: FONT.symbol(SYM.ARROW_SOUTH + 2), }, HOME_DIST: { name: "HOME_DISTANCE", text: "osdTextElementHomeDistance", desc: "osdDescElementHomeDistance", defaultPosition: -1, draw_order: 840, positionable: true, preview(osdData) { const unit = FONT.symbol(osdData.unit_mode === 0 ? SYM.FEET : SYM.METRE); return `${FONT.symbol(SYM.HOMEFLAG)}432${unit}`; }, }, NUMERICAL_HEADING: { name: "NUMERICAL_HEADING", text: "osdTextElementNumericalHeading", desc: "osdDescElementNumericalHeading", defaultPosition: -1, draw_order: 290, positionable: true, preview: `${FONT.symbol(SYM.ARROW_EAST)}90`, }, NUMERICAL_VARIO: { name: "NUMERICAL_VARIO", text: "osdTextElementNumericalVario", desc: "osdDescElementNumericalVario", defaultPosition: -1, draw_order: 300, positionable: true, preview(osdData) { const unit = FONT.symbol(osdData.unit_mode === 0 ? SYM.FTPS : SYM.MPS); return `${FONT.symbol(SYM.ARROW_SMALL_UP)}8.7${unit}`; }, }, COMPASS_BAR: { name: "COMPASS_BAR", text: "osdTextElementCompassBar", desc: "osdDescElementCompassBar", defaultPosition: -1, draw_order: 310, positionable: true, preview() { return ( FONT.symbol(SYM.HEADING_W) + FONT.symbol(SYM.HEADING_LINE) + FONT.symbol(SYM.HEADING_DIVIDED_LINE) + FONT.symbol(SYM.HEADING_LINE) + FONT.symbol(SYM.HEADING_N) + FONT.symbol(SYM.HEADING_LINE) + FONT.symbol(SYM.HEADING_DIVIDED_LINE) + FONT.symbol(SYM.HEADING_LINE) + FONT.symbol(SYM.HEADING_E) ); }, }, WARNINGS: { name: "WARNINGS", text: "osdTextElementWarnings", desc: "osdDescElementWarnings", defaultPosition: -1, draw_order: 220, positionable: true, preview: "LOW VOLTAGE", }, ESC_TEMPERATURE: { name: "ESC_TEMPERATURE", text: "osdTextElementEscTemperature", desc: "osdDescElementEscTemperature", defaultPosition: -1, draw_order: 900, positionable: true, preview(osdData) { return `E${OSD.generateTemperaturePreview(osdData, 45)}`; }, }, ESC_RPM: { name: "ESC_RPM", text: "osdTextElementEscRpm", desc: "osdDescElementEscRpm", defaultPosition: -1, draw_order: 1000, positionable: true, preview: ["22600", "22600", "22600", "22600"], }, REMAINING_TIME_ESTIMATE: { name: "REMAINING_TIME_ESTIMATE", text: "osdTextElementRemaningTimeEstimate", desc: "osdDescElementRemaningTimeEstimate", defaultPosition: -1, draw_order: 80, positionable: true, preview: "01:13", }, RTC_DATE_TIME: { name: "RTC_DATE_TIME", text: "osdTextElementRtcDateTime", desc: "osdDescElementRtcDateTime", defaultPosition: -1, draw_order: 360, positionable: true, preview: "2017-11-11 16:20:00", }, ADJUSTMENT_RANGE: { name: "ADJUSTMENT_RANGE", text: "osdTextElementAdjustmentRange", desc: "osdDescElementAdjustmentRange", defaultPosition: -1, draw_order: 370, positionable: true, preview: "PITCH/ROLL P: 42", }, TIMER_1: { name: "TIMER_1", text: "osdTextElementTimer1", desc: "osdDescElementTimer1", defaultPosition: -1, draw_order: 60, positionable: true, preview(osdData) { return OSD.generateTimerPreview(osdData, 0); }, }, TIMER_2: { name: "TIMER_2", text: "osdTextElementTimer2", desc: "osdDescElementTimer2", defaultPosition: -1, draw_order: 70, positionable: true, preview(osdData) { return OSD.generateTimerPreview(osdData, 1); }, }, CORE_TEMPERATURE: { name: "CORE_TEMPERATURE", text: "osdTextElementCoreTemperature", desc: "osdDescElementCoreTemperature", defaultPosition: -1, draw_order: 380, positionable: true, preview(osdData) { return `C${OSD.generateTemperaturePreview(osdData, 33)}`; }, }, ANTI_GRAVITY: { name: "ANTI_GRAVITY", text: "osdTextAntiGravity", desc: "osdDescAntiGravity", defaultPosition: -1, draw_order: 320, positionable: true, preview: "AG", }, G_FORCE: { name: "G_FORCE", text: "osdTextGForce", desc: "osdDescGForce", defaultPosition: -1, draw_order: 15, positionable: true, preview: "1.0G", }, MOTOR_DIAG: { name: "MOTOR_DIAGNOSTICS", text: "osdTextElementMotorDiag", desc: "osdDescElementMotorDiag", defaultPosition: -1, draw_order: 335, positionable: true, preview: FONT.symbol(0x84) + FONT.symbol(0x85) + FONT.symbol(0x84) + FONT.symbol(0x83), }, LOG_STATUS: { name: "LOG_STATUS", text: "osdTextElementLogStatus", desc: "osdDescElementLogStatus", defaultPosition: -1, draw_order: 330, positionable: true, preview: `${FONT.symbol(SYM.BBLOG)}16`, }, FLIP_ARROW: { name: "FLIP_ARROW", text: "osdTextElementFlipArrow", desc: "osdDescElementFlipArrow", defaultPosition: -1, draw_order: 340, positionable: true, preview: FONT.symbol(SYM.ARROW_EAST), }, LINK_QUALITY: { name: "LINK_QUALITY", text: "osdTextElementLinkQuality", desc: "osdDescElementLinkQuality", defaultPosition: -1, draw_order: 390, positionable: true, preview: OSD.generateLQPreview, }, FLIGHT_DIST: { name: "FLIGHT_DISTANCE", text: "osdTextElementFlightDist", desc: "osdDescElementFlightDist", defaultPosition: -1, draw_order: 860, positionable: true, preview(osdData) { const unit = FONT.symbol(osdData.unit_mode === 0 ? SYM.FEET : SYM.METRE); return `${FONT.symbol(SYM.TOTAL_DIST)}653${unit}`; }, }, STICK_OVERLAY_LEFT: { name: "STICK_OVERLAY_LEFT", text: "osdTextElementStickOverlayLeft", desc: "osdDescElementStickOverlayLeft", defaultPosition: -1, draw_order: 400, positionable: true, preview: OSD.drawStickOverlayPreview, }, STICK_OVERLAY_RIGHT: { name: "STICK_OVERLAY_RIGHT", text: "osdTextElementStickOverlayRight", desc: "osdDescElementStickOverlayRight", defaultPosition: -1, draw_order: 410, positionable: true, preview: OSD.drawStickOverlayPreview, }, ...(semver.lt(FC.CONFIG.apiVersion, API_VERSION_1_45) ? { DISPLAY_NAME: { name: "DISPLAY_NAME", text: "osdTextElementDisplayName", desc: "osdDescElementDisplayName", defaultPosition: -77, draw_order: 350, positionable: true, preview(osdData) { return OSD.generateDisplayName(osdData, 1); }, }, } : {}), ...(semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_45) ? { PILOT_NAME: { name: "PILOT_NAME", text: "osdTextElementPilotName", desc: "osdDescElementPilotName", defaultPosition: -77, draw_order: 350, positionable: true, preview(osdData) { return OSD.generatePilotName(osdData, 1); }, }, } : {}), ESC_RPM_FREQ: { name: "ESC_RPM_FREQ", text: "osdTextElementEscRpmFreq", desc: "osdDescElementEscRpmFreq", defaultPosition: -1, draw_order: 1010, positionable: true, preview: ["22600", "22600", "22600", "22600"], }, RATE_PROFILE_NAME: { name: "RATE_PROFILE_NAME", text: "osdTextElementRateProfileName", desc: "osdDescElementRateProfileName", defaultPosition: -1, draw_order: 420, positionable: true, preview: "RATE_1", }, PID_PROFILE_NAME: { name: "PID_PROFILE_NAME", text: "osdTextElementPidProfileName", desc: "osdDescElementPidProfileName", defaultPosition: -1, draw_order: 430, positionable: true, preview: "PID_1", }, OSD_PROFILE_NAME: { name: "OSD_PROFILE_NAME", text: "osdTextElementOsdProfileName", desc: "osdDescElementOsdProfileName", defaultPosition: -1, draw_order: 440, positionable: true, preview: "OSD_1", }, RSSI_DBM_VALUE: { name: "RSSI_DBM_VALUE", text: "osdTextElementRssiDbmValue", desc: "osdDescElementRssiDbmValue", defaultPosition: -1, draw_order: 395, positionable: true, preview: `${FONT.symbol(SYM.RSSI)}-130`, }, RC_CHANNELS: { name: "OSD_RC_CHANNELS", text: "osdTextElementRcChannels", desc: "osdDescElementRcChannels", defaultPosition: -1, draw_order: 445, positionable: true, preview: ["-1000", " 545", " 689", " 1000"], }, CAMERA_FRAME: { name: "OSD_CAMERA_FRAME", text: "osdTextElementCameraFrame", desc: "osdDescElementCameraFrame", defaultPosition: -1, draw_order: 450, positionable: true, preview: OSD.drawCameraFramePreview, }, OSD_EFFICIENCY: { name: "OSD_EFFICIENCY", text: "osdTextElementEfficiency", desc: "osdDescElementEfficiency", defaultPosition: -1, draw_order: 455, positionable: true, preview(osdData) { const unit = FONT.symbol(osdData.unit_mode === 0 ? SYM.MILES : SYM.KM); return `1234${FONT.symbol(SYM.MAH)}/${unit}`; }, }, TOTAL_FLIGHTS: { name: "OSD_TOTAL_FLIGHTS", text: "osdTextTotalFlights", desc: "osdDescTotalFlights", defaultPosition: -1, draw_order: 460, positionable: true, preview: "#9876", }, OSD_UP_DOWN_REFERENCE: { name: "OSD_UP_DOWN_REFERENCE", text: "osdTextElementUpDownReference", desc: "osdDescUpDownReference", defaultPosition: 238, draw_order: 465, positionable: true, preview: "U", }, OSD_TX_UPLINK_POWER: { name: "OSD_TX_UPLINK_POWER", text: "osdTextElementTxUplinkPower", desc: "osdDescTxUplinkPower", defaultPosition: -1, draw_order: 470, positionable: true, preview: `${FONT.symbol(SYM.RSSI)}250MW`, }, WH_DRAWN: { name: "WH_DRAWN", text: "osdTextElementWhDrawn", desc: "osdDescElementWhDrawn", defaultPosition: -1, draw_order: 475, positionable: true, preview: "1.10 WH", }, AUX_VALUE: { name: "AUX_VALUE", text: "osdTextElementAuxValue", desc: "osdDescElementAuxValue", defaultPosition: -1, draw_order: 480, positionable: true, preview: "AUX", }, READY_MODE: { name: "READY_MODE", text: "osdTextElementReadyMode", desc: "osdDescElementReadyMode", defaultPosition: -1, draw_order: 485, positionable: true, preview: "READY", }, RSNR_VALUE: { name: "RSNR_VALUE", text: "osdTextElementRSNRValue", desc: "osdDescElementRSNRValue", defaultPosition: -1, draw_order: 490, positionable: true, preview: `${FONT.symbol(SYM.RSSI)}15`, }, SYS_GOGGLE_VOLTAGE: { name: "SYS_GOGGLE_VOLTAGE", text: "osdTextElementSysGoggleVoltage", desc: "osdDescElementSysGoggleVoltage", defaultPosition: -1, draw_order: 485, positionable: true, preview: "G 16.8V", }, SYS_VTX_VOLTAGE: { name: "SYS_VTX_VOLTAGE", text: "osdTextElementSysVtxVoltage", desc: "osdDescElementSysVtxVoltage", defaultPosition: -1, draw_order: 490, positionable: true, preview: "A 12.6V", }, SYS_BITRATE: { name: "SYS_BITRATE", text: "osdTextElementSysBitrate", desc: "osdDescElementSysBitrate", defaultPosition: -1, draw_order: 495, positionable: true, preview: "50MBPS", }, SYS_DELAY: { name: "SYS_DELAY", text: "osdTextElementSysDelay", desc: "osdDescElementSysDelay", defaultPosition: -1, draw_order: 500, positionable: true, preview: "24.5MS", }, SYS_DISTANCE: { name: "SYS_DISTANCE", text: "osdTextElementSysDistance", desc: "osdDescElementSysDistance", defaultPosition: -1, draw_order: 505, positionable: true, preview: `10${FONT.symbol(SYM.METRE)}`, }, SYS_LQ: { name: "SYS_LQ", text: "osdTextElementSysLQ", desc: "osdDescElementSysLQ", defaultPosition: -1, draw_order: 510, positionable: true, preview: `G${FONT.symbol(SYM.LINK_QUALITY)}100`, }, SYS_GOGGLE_DVR: { name: "SYS_GOGGLE_DVR", text: "osdTextElementSysGoggleDVR", desc: "osdDescElementSysGoggleDVR", defaultPosition: -1, draw_order: 515, positionable: true, preview: `${FONT.symbol(SYM.ARROW_SMALL_RIGHT)}G DVR 8.4G`, }, SYS_VTX_DVR: { name: "SYS_VTX_DVR", text: "osdTextElementSysVtxDVR", desc: "osdDescElementSysVtxDVR", defaultPosition: -1, draw_order: 520, positionable: true, preview: `${FONT.symbol(SYM.ARROW_SMALL_RIGHT)}A DVR 1.6G`, }, SYS_WARNINGS: { name: "SYS_WARNINGS", text: "osdTextElementSysWarnings", desc: "osdDescElementSysWarnings", defaultPosition: -1, draw_order: 525, positionable: true, preview: "VTX WARNINGS", }, SYS_VTX_TEMP: { name: "SYS_VTX_TEMP", text: "osdTextElementSysVtxTemp", desc: "osdDescElementSysVtxTemp", defaultPosition: -1, draw_order: 530, positionable: true, preview(osdData) { return `V${OSD.generateTemperaturePreview(osdData, 45)}`; }, }, SYS_FAN_SPEED: { name: "SYS_FAN_SPEED", text: "osdTextElementSysFanSpeed", desc: "osdDescElementSysFanSpeed", defaultPosition: -1, draw_order: 535, positionable: true, preview: `F${FONT.symbol(SYM.TEMPERATURE)}5`, }, GPS_LAP_TIME_CURRENT: { name: "GPS_LAP_TIME_CURRENT", text: "osdTextElementLapTimeCurrent", desc: "osdDescElementLapTimeCurrent", defaultPosition: -1, draw_order: 540, positionable: true, preview: "1:23.456", }, GPS_LAP_TIME_PREVIOUS: { name: "GPS_LAP_TIME_PREVIOUS", text: "osdTextElementLapTimePrevious", desc: "osdDescElementLapTimePrevious", defaultPosition: -1, draw_order: 545, positionable: true, preview: "1:23.456", }, GPS_LAP_TIME_BEST3: { name: "GPS_LAP_TIME_BEST3", text: "osdTextElementLapTimeBest3", desc: "osdDescElementLapTimeBest3", defaultPosition: -1, draw_order: 550, positionable: true, preview: "1:23.456", }, DEBUG2: { name: "DEBUG2", text: "osdTextElementDebug2", desc: "osdDescElementDebug2", defaultPosition: -1, draw_order: 560, positionable: true, preview: "DBG2 0 0 0 0", }, CUSTOM_MSG0: { name: "CUSTOM_MSG1", text: "osdTextElementCustomMsg0", desc: "osdDescElementCustomMsg0", defaultPosition: -1, draw_order: 570, positionable: true, preview: "CUSTOM MSG1", }, CUSTOM_MSG1: { name: "CUSTOM_MSG2", text: "osdTextElementCustomMsg1", desc: "osdDescElementCustomMsg1", defaultPosition: -1, draw_order: 580, positionable: true, preview: "CUSTOM MSG2", }, CUSTOM_MSG2: { name: "CUSTOM_MSG3", text: "osdTextElementCustomMsg2", desc: "osdDescElementCustomMsg2", defaultPosition: -1, draw_order: 590, positionable: true, preview: "CUSTOM MSG3", }, CUSTOM_MSG3: { name: "CUSTOM_MSG4", text: "osdTextElementCustomMsg3", desc: "osdDescElementCustomMsg3", defaultPosition: -1, draw_order: 600, positionable: true, preview: "CUSTOM MSG4", }, }; if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_47) && have_sensor(FC.CONFIG.activeSensors, "gps")) { OSD.ALL_DISPLAY_FIELDS.ALTITUDE.variants.push("osdTextElementAltitudeVariant1DecimalASL"); OSD.ALL_DISPLAY_FIELDS.ALTITUDE.variants.push("osdTextElementAltitudeVariantNoDecimalASL"); } }; OSD.constants = { VISIBLE: 0x0800, VARIANTS: 0xc000, VIDEO_TYPES: ["AUTO", "PAL", "NTSC", "HD"], UNIT_TYPES: ["IMPERIAL", "METRIC", "BRITISH"], TIMER_PRECISION: ["SECOND", "HUNDREDTH", "TENTH"], AHISIDEBARWIDTHPOSITION: 7, AHISIDEBARHEIGHTPOSITION: 3, UNKNOWN_DISPLAY_FIELD: { name: "UNKNOWN", text: "osdTextElementUnknown", desc: "osdDescElementUnknown", defaultPosition: -1, positionable: true, preview: "UNKNOWN ", }, ALL_STATISTIC_FIELDS: { MAX_SPEED: { name: "MAX_SPEED", text: "osdTextStatMaxSpeed", desc: "osdDescStatMaxSpeed", }, MIN_BATTERY: { name: "MIN_BATTERY", text: "osdTextStatMinBattery", desc: "osdDescStatMinBattery", }, MIN_RSSI: { name: "MIN_RSSI", text: "osdTextStatMinRssi", desc: "osdDescStatMinRssi", }, MAX_CURRENT: { name: "MAX_CURRENT", text: "osdTextStatMaxCurrent", desc: "osdDescStatMaxCurrent", }, USED_MAH: { name: "USED_MAH", text: "osdTextStatUsedMah", desc: "osdDescStatUsedMah", }, USED_WH: { name: "USED_WH", text: "osdTextStatUsedWh", desc: "osdDescStatUsedWh", }, MAX_ALTITUDE: { name: "MAX_ALTITUDE", text: "osdTextStatMaxAltitude", desc: "osdDescStatMaxAltitude", }, BLACKBOX: { name: "BLACKBOX", text: "osdTextStatBlackbox", desc: "osdDescStatBlackbox", }, END_BATTERY: { name: "END_BATTERY", text: "osdTextStatEndBattery", desc: "osdDescStatEndBattery", }, FLYTIME: { name: "FLY_TIME", text: "osdTextStatFlyTime", desc: "osdDescStatFlyTime", }, ARMEDTIME: { name: "ARMED_TIME", text: "osdTextStatArmedTime", desc: "osdDescStatArmedTime", }, MAX_DISTANCE: { name: "MAX_DISTANCE", text: "osdTextStatMaxDistance", desc: "osdDescStatMaxDistance", }, BLACKBOX_LOG_NUMBER: { name: "BLACKBOX_LOG_NUMBER", text: "osdTextStatBlackboxLogNumber", desc: "osdDescStatBlackboxLogNumber", }, TIMER_1: { name: "TIMER_1", text: "osdTextStatTimer1", desc: "osdDescStatTimer1", }, TIMER_2: { name: "TIMER_2", text: "osdTextStatTimer2", desc: "osdDescStatTimer2", }, RTC_DATE_TIME: { name: "RTC_DATE_TIME", text: "osdTextStatRtcDateTime", desc: "osdDescStatRtcDateTime", }, STAT_BATTERY: { name: "BATTERY_VOLTAGE", text: "osdTextStatBattery", desc: "osdDescStatBattery", }, MAX_G_FORCE: { name: "MAX_G_FORCE", text: "osdTextStatGForce", desc: "osdDescStatGForce", }, MAX_ESC_TEMP: { name: "MAX_ESC_TEMP", text: "osdTextStatEscTemperature", desc: "osdDescStatEscTemperature", }, MAX_ESC_RPM: { name: "MAX_ESC_RPM", text: "osdTextStatEscRpm", desc: "osdDescStatEscRpm", }, MIN_LINK_QUALITY: { name: "MIN_LINK_QUALITY", text: "osdTextStatMinLinkQuality", desc: "osdDescStatMinLinkQuality", }, FLIGHT_DISTANCE: { name: "FLIGHT_DISTANCE", text: "osdTextStatFlightDistance", desc: "osdDescStatFlightDistance", }, MAX_FFT: { name: "MAX_FFT", text: "osdTextStatMaxFFT", desc: "osdDescStatMaxFFT", }, STAT_TOTAL_FLIGHTS: { name: "STAT_TOTAL_FLIGHTS", text: "osdTextStatTotalFlights", desc: "osdDescStatTotalFlights", }, STAT_TOTAL_FLIGHT_TIME: { name: "STAT_TOTAL_FLIGHT_TIME", text: "osdTextStatTotalFlightTime", desc: "osdDescStatTotalFlightTime", }, STAT_TOTAL_FLIGHT_DIST: { name: "STAT_TOTAL_FLIGHT_DIST", text: "osdTextStatTotalFlightDistance", desc: "osdDescStatTotalFlightDistance", }, MIN_RSSI_DBM: { name: "MIN_RSSI_DBM", text: "osdTextStatMinRssiDbm", desc: "osdDescStatMinRssiDbm", }, MIN_RSNR: { name: "MIN_RSNR", text: "osdTextStatMinRSNR", desc: "osdDescStatMinRSNR", }, STAT_BEST_3_CONSEC_LAPS: { name: "STAT_BEST_3_CONSEC_LAPS", text: "osdTextStatBest3ConsecLaps", desc: "osdDescStatBest3ConsecLaps", }, STAT_BEST_LAP: { name: "STAT_BEST_LAP", text: "osdTextStatBestLap", desc: "osdDescStatBestLap", }, STAT_FULL_THROTTLE_TIME: { name: "STAT_FULL_THROTTLE_TIME", text: "osdTextStatFullThrottleTime", desc: "osdDescStatFullThrottleTime", }, STAT_FULL_THROTTLE_COUNTER: { name: "STAT_FULL_THROTTLE_COUNTER", text: "osdTextStatFullThrottleCounter", desc: "osdDescStatFullThrottleCounter", }, STAT_AVG_THROTTLE: { name: "STAT_AVG_THROTTLE", text: "osdTextStatAvgThrottle", desc: "osdDescStatAvgThrottle", }, }, ALL_WARNINGS: { ARMING_DISABLED: { name: "ARMING_DISABLED", text: "osdWarningTextArmingDisabled", desc: "osdWarningArmingDisabled", }, BATTERY_NOT_FULL: { name: "BATTERY_NOT_FULL", text: "osdWarningTextBatteryNotFull", desc: "osdWarningBatteryNotFull", }, BATTERY_WARNING: { name: "BATTERY_WARNING", text: "osdWarningTextBatteryWarning", desc: "osdWarningBatteryWarning", }, BATTERY_CRITICAL: { name: "BATTERY_CRITICAL", text: "osdWarningTextBatteryCritical", desc: "osdWarningBatteryCritical", }, VISUAL_BEEPER: { name: "VISUAL_BEEPER", text: "osdWarningTextVisualBeeper", desc: "osdWarningVisualBeeper", }, CRASH_FLIP_MODE: { name: "CRASH_FLIP_MODE", text: "osdWarningTextCrashFlipMode", desc: "osdWarningCrashFlipMode", }, ESC_FAIL: { name: "ESC_FAIL", text: "osdWarningTextEscFail", desc: "osdWarningEscFail", }, CORE_TEMPERATURE: { name: "CORE_TEMPERATURE", text: "osdWarningTextCoreTemperature", desc: "osdWarningCoreTemperature", }, RC_SMOOTHING_FAILURE: { name: "RC_SMOOTHING_FAILURE", text: "osdWarningTextRcSmoothingFailure", desc: "osdWarningRcSmoothingFailure", }, FAILSAFE: { name: "FAILSAFE", text: "osdWarningTextFailsafe", desc: "osdWarningFailsafe", }, LAUNCH_CONTROL: { name: "LAUNCH_CONTROL", text: "osdWarningTextLaunchControl", desc: "osdWarningLaunchControl", }, GPS_RESCUE_UNAVAILABLE: { name: "GPS_RESCUE_UNAVAILABLE", text: "osdWarningTextGpsRescueUnavailable", desc: "osdWarningGpsRescueUnavailable", }, GPS_RESCUE_DISABLED: { name: "GPS_RESCUE_DISABLED", text: "osdWarningTextGpsRescueDisabled", desc: "osdWarningGpsRescueDisabled", }, RSSI: { name: "RSSI", text: "osdWarningTextRSSI", desc: "osdWarningRSSI", }, LINK_QUALITY: { name: "LINK_QUALITY", text: "osdWarningTextLinkQuality", desc: "osdWarningLinkQuality", }, RSSI_DBM: { name: "RSSI_DBM", text: "osdWarningTextRssiDbm", desc: "osdWarningRssiDbm", }, OVER_CAP: { name: "OVER_CAP", text: "osdWarningTextOverCap", desc: "osdWarningOverCap", }, RSNR: { name: "RSNR", text: "osdWarningTextRSNR", desc: "osdWarningRSNR", }, LOAD: { name: "LOAD", text: "osdWarningTextLoad", desc: "osdWarningLoad", }, }, FONT_TYPES: [ { file: "default", name: "osdSetupFontTypeDefault" }, { file: "bold", name: "osdSetupFontTypeBold" }, { file: "large", name: "osdSetupFontTypeLarge" }, { file: "extra_large", name: "osdSetupFontTypeLargeExtra" }, { file: "betaflight", name: "osdSetupFontTypeBetaflight" }, { file: "digital", name: "osdSetupFontTypeDigital" }, { file: "clarity", name: "osdSetupFontTypeClarity" }, { file: "vision", name: "osdSetupFontTypeVision" }, { file: "impact", name: "osdSetupFontTypeImpact" }, { file: "impact_mini", name: "osdSetupFontTypeImpactMini" }, ], }; OSD.searchLimitsElement = function (arrayElements) { // Search minimum and maximum const limits = { minX: 0, maxX: 0, minY: 0, maxY: 0, }; if (arrayElements.length === 0) { return limits; } if (arrayElements[0].constructor === String) { limits.maxY = arrayElements.length; limits.minY = 0; limits.minX = 0; arrayElements.forEach(function (valor) { limits.maxX = Math.max(valor.length, limits.maxX); }); } else { arrayElements.forEach(function (valor) { limits.minX = Math.min(valor.x, limits.minX); limits.maxX = Math.max(valor.x, limits.maxX); limits.minY = Math.min(valor.y, limits.minY); limits.maxY = Math.max(valor.y, limits.maxY); }); } return limits; }; // Pick display fields by version, order matters, so these are going in an array... pry could iterate the example map instead OSD.chooseFields = function () { let F = OSD.ALL_DISPLAY_FIELDS; OSD.constants.DISPLAY_FIELDS = [ F.RSSI_VALUE, F.MAIN_BATT_VOLTAGE, F.CROSSHAIRS, F.ARTIFICIAL_HORIZON, F.HORIZON_SIDEBARS, F.TIMER_1, F.TIMER_2, F.FLYMODE, F.CRAFT_NAME, F.THROTTLE_POSITION, F.VTX_CHANNEL, F.CURRENT_DRAW, F.MAH_DRAWN, F.GPS_SPEED, F.GPS_SATS, F.ALTITUDE, F.PID_ROLL, F.PID_PITCH, F.PID_YAW, F.POWER, F.PID_RATE_PROFILE, F.WARNINGS, F.AVG_CELL_VOLTAGE, F.GPS_LON, F.GPS_LAT, F.DEBUG, F.PITCH_ANGLE, F.ROLL_ANGLE, F.MAIN_BATT_USAGE, F.DISARMED, F.HOME_DIR, F.HOME_DIST, F.NUMERICAL_HEADING, F.NUMERICAL_VARIO, F.COMPASS_BAR, F.ESC_TEMPERATURE, F.ESC_RPM, F.REMAINING_TIME_ESTIMATE, F.RTC_DATE_TIME, F.ADJUSTMENT_RANGE, F.CORE_TEMPERATURE, F.ANTI_GRAVITY, F.G_FORCE, F.MOTOR_DIAG, F.LOG_STATUS, F.FLIP_ARROW, F.LINK_QUALITY, F.FLIGHT_DIST, F.STICK_OVERLAY_LEFT, F.STICK_OVERLAY_RIGHT, // show either DISPLAY_NAME or PILOT_NAME depending on the MSP version semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_45) ? F.PILOT_NAME : F.DISPLAY_NAME, F.ESC_RPM_FREQ, F.RATE_PROFILE_NAME, F.PID_PROFILE_NAME, F.OSD_PROFILE_NAME, F.RSSI_DBM_VALUE, F.RC_CHANNELS, F.CAMERA_FRAME, F.OSD_EFFICIENCY, F.TOTAL_FLIGHTS, F.OSD_UP_DOWN_REFERENCE, F.OSD_TX_UPLINK_POWER, ]; if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_45)) { OSD.constants.DISPLAY_FIELDS = OSD.constants.DISPLAY_FIELDS.concat([ F.WH_DRAWN, F.AUX_VALUE, F.READY_MODE, F.RSNR_VALUE, F.SYS_GOGGLE_VOLTAGE, F.SYS_VTX_VOLTAGE, F.SYS_BITRATE, F.SYS_DELAY, F.SYS_DISTANCE, F.SYS_LQ, F.SYS_GOGGLE_DVR, F.SYS_VTX_DVR, F.SYS_WARNINGS, F.SYS_VTX_TEMP, F.SYS_FAN_SPEED, ]); } if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_46)) { OSD.constants.DISPLAY_FIELDS = OSD.constants.DISPLAY_FIELDS.concat([ F.GPS_LAP_TIME_CURRENT, F.GPS_LAP_TIME_PREVIOUS, F.GPS_LAP_TIME_BEST3, ]); } if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_47)) { OSD.constants.DISPLAY_FIELDS = OSD.constants.DISPLAY_FIELDS.concat([ F.DEBUG2, F.CUSTOM_MSG0, F.CUSTOM_MSG1, F.CUSTOM_MSG2, F.CUSTOM_MSG3, ]); } // Choose statistic fields // Nothing much to do here, I'm preempting there being new statistics F = OSD.constants.ALL_STATISTIC_FIELDS; // ** IMPORTANT ** // // Starting with 1.39.0 (Betaflight 3.4) the OSD stats selection options // are ordered in the same sequence as displayed on-screen in the OSD. // If future versions of the firmware implement changes to the on-screen ordering, // that needs to be implemented here as well. Simply appending new stats does not // require a completely new section for the version - only reordering. // Starting with 1.39.0 OSD stats are reordered to match how they're presented on screen OSD.constants.STATISTIC_FIELDS = [ F.RTC_DATE_TIME, F.TIMER_1, F.TIMER_2, F.MAX_SPEED, F.MAX_DISTANCE, F.MIN_BATTERY, F.END_BATTERY, F.STAT_BATTERY, F.MIN_RSSI, F.MAX_CURRENT, F.USED_MAH, F.MAX_ALTITUDE, F.BLACKBOX, F.BLACKBOX_LOG_NUMBER, F.MAX_G_FORCE, F.MAX_ESC_TEMP, F.MAX_ESC_RPM, F.MIN_LINK_QUALITY, F.FLIGHT_DISTANCE, F.MAX_FFT, F.STAT_TOTAL_FLIGHTS, F.STAT_TOTAL_FLIGHT_TIME, F.STAT_TOTAL_FLIGHT_DIST, F.MIN_RSSI_DBM, ]; if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_45)) { OSD.constants.STATISTIC_FIELDS = OSD.constants.STATISTIC_FIELDS.concat([F.USED_WH, F.MIN_RSNR]); } if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_46)) { OSD.constants.STATISTIC_FIELDS = OSD.constants.STATISTIC_FIELDS.concat([ F.STAT_BEST_3_CONSEC_LAPS, F.STAT_BEST_LAP, F.STAT_FULL_THROTTLE_TIME, F.STAT_FULL_THROTTLE_COUNTER, F.STAT_AVG_THROTTLE, ]); } // Choose warnings // Nothing much to do here, I'm preempting there being new warnings F = OSD.constants.ALL_WARNINGS; OSD.constants.WARNINGS = [ F.ARMING_DISABLED, F.BATTERY_NOT_FULL, F.BATTERY_WARNING, F.BATTERY_CRITICAL, F.VISUAL_BEEPER, F.CRASH_FLIP_MODE, F.ESC_FAIL, F.CORE_TEMPERATURE, F.RC_SMOOTHING_FAILURE, F.FAILSAFE, F.LAUNCH_CONTROL, F.GPS_RESCUE_UNAVAILABLE, F.GPS_RESCUE_DISABLED, F.RSSI, F.LINK_QUALITY, F.RSSI_DBM, F.OVER_CAP, ]; OSD.constants.TIMER_TYPES = ["ON_TIME", "TOTAL_ARMED_TIME", "LAST_ARMED_TIME", "ON_ARM_TIME"]; if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_45)) { OSD.constants.WARNINGS = OSD.constants.WARNINGS.concat([F.RSNR]); } if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_46)) { OSD.constants.WARNINGS = OSD.constants.WARNINGS.concat([F.LOAD]); } }; OSD.updateDisplaySize = function () { let videoType = OSD.constants.VIDEO_TYPES[OSD.data.video_system]; if (videoType === "AUTO") { videoType = "PAL"; } // compute the size OSD.data.displaySize = { x: OSD.data.VIDEO_COLS[videoType], y: OSD.data.VIDEO_ROWS[videoType], total: null, }; }; OSD.drawByOrder = function (selectedPosition, field, charCode, x, y) { // Check if there is other field at the same position if (OSD.data.preview[selectedPosition] !== undefined) { const oldField = OSD.data.preview[selectedPosition][0]; if ( oldField != null && oldField.draw_order !== undefined && (field.draw_order === undefined || field.draw_order < oldField.draw_order) ) { // Not overwrite old field return; } // Default action, overwrite old field OSD.data.preview[selectedPosition] = [field, charCode, x, y]; } }; OSD.msp = { /** * Note, unsigned 16 bit int for position ispacked: * 0: unused * v: visible flag * b: blink flag * y: y coordinate * x: x coordinate * p: profile * t: variant type * ttpp vbyy yyyx xxxx */ helpers: { unpack: { position(bits, c) { const displayItem = {}; const positionable = typeof c.positionable === "function" ? c.positionable() : c.positionable; const defaultPosition = typeof c.defaultPosition === "function" ? c.defaultPosition() : c.defaultPosition; displayItem.positionable = positionable; OSD.updateDisplaySize(); // size * y + x const xpos = ((bits >> 5) & 0x0020) | (bits & 0x001f); const ypos = (bits >> 5) & 0x001f; displayItem.position = positionable ? OSD.data.displaySize.x * ypos + xpos : defaultPosition; displayItem.isVisible = []; for (let osd_profile = 0; osd_profile < OSD.getNumberOfProfiles(); osd_profile++) { displayItem.isVisible[osd_profile] = (bits & (OSD.constants.VISIBLE << osd_profile)) !== 0; } displayItem.variant = (bits & OSD.constants.VARIANTS) >> 14; return displayItem; }, timer(bits) { return { src: bits & 0x0f, precision: (bits >> 4) & 0x0f, alarm: (bits >> 8) & 0xff, }; }, }, pack: { position(displayItem) { const isVisible = displayItem.isVisible; const position = displayItem.position; const variant = displayItem.variant; let packed_visible = 0; for (let osd_profile = 0; osd_profile < OSD.getNumberOfProfiles(); osd_profile++) { packed_visible |= isVisible[osd_profile] ? OSD.constants.VISIBLE << osd_profile : 0; } const variantSelected = variant << 14; const xpos = position % OSD.data.displaySize.x; const ypos = (position - xpos) / OSD.data.displaySize.x; return ( packed_visible | variantSelected | ((ypos & 0x001f) << 5) | ((xpos & 0x0020) << 5) | (xpos & 0x001f) ); }, timer(timer) { return (timer.src & 0x0f) | ((timer.precision & 0x0f) << 4) | ((timer.alarm & 0xff) << 8); }, }, }, encodeOther() { const result = [-1, OSD.data.video_system]; if (OSD.data.state.haveOsdFeature) { result.push8(OSD.data.unit_mode); // watch out, order matters! match the firmware result.push8(OSD.data.alarms.rssi.value); result.push16(OSD.data.alarms.cap.value); result.push16(0); // This value is unused by the firmware with configurable timers result.push16(OSD.data.alarms.alt.value); let warningFlags = 0; for (let i = 0; i < OSD.data.warnings.length; i++) { if (OSD.data.warnings[i].enabled) { warningFlags |= 1 << i; } } if (CONFIGURATOR.virtualMode) { OSD.virtualMode.warningFlags = warningFlags; } console.log(warningFlags); result.push16(warningFlags); result.push32(warningFlags); result.push8(OSD.data.osd_profiles.selected + 1); result.push8(OSD.data.parameters.overlayRadioMode); result.push8(OSD.data.parameters.cameraFrameWidth); result.push8(OSD.data.parameters.cameraFrameHeight); if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_46)) { result.push16(OSD.data.alarms.link_quality.value); } if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_47)) { result.push16(OSD.data.alarms.rssi_dbm.value); } } return result; }, encodeLayout(displayItem) { if (CONFIGURATOR.virtualMode) { OSD.virtualMode.itemPositions[displayItem.index] = this.helpers.pack.position(displayItem); } const buffer = []; buffer.push8(displayItem.index); buffer.push16(this.helpers.pack.position(displayItem)); return buffer; }, encodeStatistics(statItem) { if (CONFIGURATOR.virtualMode) { OSD.virtualMode.statisticsState[statItem.index] = statItem.enabled; } const buffer = []; buffer.push8(statItem.index); buffer.push16(statItem.enabled); buffer.push8(0); return buffer; }, encodeTimer(timer) { if (CONFIGURATOR.virtualMode) { OSD.virtualMode.timerData[timer.index] = {}; OSD.virtualMode.timerData[timer.index].src = timer.src; OSD.virtualMode.timerData[timer.index].precision = timer.precision; OSD.virtualMode.timerData[timer.index].alarm = timer.alarm; } const buffer = [-2, timer.index]; buffer.push16(this.helpers.pack.timer(timer)); return buffer; }, processOsdElements(data, itemPositions) { // Now we have the number of profiles, process the OSD elements for (const item of itemPositions) { const j = data.displayItems.length; let c; let suffix; let ignoreSize = false; if (data.displayItems.length < OSD.constants.DISPLAY_FIELDS.length) { c = OSD.constants.DISPLAY_FIELDS[j]; } else { c = OSD.constants.UNKNOWN_DISPLAY_FIELD; suffix = (1 + data.displayItems.length - OSD.constants.DISPLAY_FIELDS.length).toString(); ignoreSize = true; } data.displayItems.push( $.extend( { name: c.name, text: suffix ? [c.text, suffix] : c.text, desc: c.desc, index: j, draw_order: c.draw_order, preview: suffix ? c.preview + suffix : c.preview, variants: c.variants, ignoreSize, }, this.helpers.unpack.position(item, c), ), ); } // Generate OSD element previews and positionable that are defined by a function for (const item of data.displayItems) { if (typeof item.preview === "function") { item.preview = item.preview(data); } } }, // Currently only parses MSP_MAX_OSD responses, add a switch on payload.code if more codes are handled decode(payload) { const view = payload.data; const d = OSD.data; let displayItemsCountActual = OSD.constants.DISPLAY_FIELDS.length; d.flags = view.readU8(); if (d.flags > 0 && payload.length > 1) { d.video_system = view.readU8(); if (bit_check(d.flags, 0)) { d.unit_mode = view.readU8(); d.alarms = {}; d.alarms["rssi"] = { display_name: i18n.getMessage("osdTimerAlarmOptionRssi"), value: view.readU8() }; d.alarms["cap"] = { display_name: i18n.getMessage("osdTimerAlarmOptionCapacity"), value: view.readU16(), }; // This value was obsoleted by the introduction of configurable timers, and has been reused to encode the number of display elements sent in this command view.readU8(); displayItemsCountActual = view.readU8(); d.alarms["alt"] = { display_name: i18n.getMessage("osdTimerAlarmOptionAltitude"), value: view.readU16(), }; } } d.state = {}; d.state.haveSomeOsd = d.flags !== 0; d.state.haveMax7456Configured = bit_check(d.flags, 4); d.state.haveFrSkyOSDConfigured = bit_check(d.flags, 3); d.state.haveMax7456FontDeviceConfigured = d.state.haveMax7456Configured || d.state.haveFrSkyOSDConfigured; d.state.isMax7456FontDeviceDetected = bit_check(d.flags, 5); d.state.haveOsdFeature = bit_check(d.flags, 0); d.state.isOsdSlave = bit_check(d.flags, 1); d.state.isMspDevice = bit_check(d.flags, 6) && semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_45); d.displayItems = []; d.statItems = []; d.warnings = []; d.timers = []; d.parameters = {}; d.parameters.overlayRadioMode = 0; d.parameters.cameraFrameWidth = 24; d.parameters.cameraFrameHeight = 11; // Read display element positions, the parsing is done later because we need the number of profiles const itemsPositionsRead = []; while (view.offset < view.byteLength && itemsPositionsRead.length < displayItemsCountActual) { const v = view.readU16(); itemsPositionsRead.push(v); } // Parse statistics display enable const expectedStatsCount = view.readU8(); if (expectedStatsCount !== OSD.constants.STATISTIC_FIELDS.length) { console.error( `Firmware is transmitting a different number of statistics (${expectedStatsCount}) to what the configurator ` + `is expecting (${OSD.constants.STATISTIC_FIELDS.length})`, ); } for (let i = 0; i < expectedStatsCount; i++) { const v = view.readU8(); // Known statistics field if (i < OSD.constants.STATISTIC_FIELDS.length) { const c = OSD.constants.STATISTIC_FIELDS[i]; d.statItems.push({ name: c.name, text: c.text, desc: c.desc, index: i, enabled: v === 1, }); // Read all the data for any statistics we don't know about } else { const statisticNumber = i - OSD.constants.STATISTIC_FIELDS.length + 1; d.statItems.push({ name: "UNKNOWN", text: ["osdTextStatUnknown", statisticNumber], desc: "osdDescStatUnknown", index: i, enabled: v === 1, }); } } // Parse configurable timers let expectedTimersCount = view.readU8(); while (view.offset < view.byteLength && expectedTimersCount > 0) { const v = view.readU16(); const j = d.timers.length; d.timers.push( $.extend( { index: j, }, this.helpers.unpack.timer(v), ), ); expectedTimersCount--; } // Read all the data for any timers we don't know about while (expectedTimersCount > 0) { view.readU16(); expectedTimersCount--; } // Parse enabled warnings view.readU16(); // obsolete const warningCount = view.readU8(); // the flags were replaced with a 32bit version const warningFlags = view.readU32(); for (let i = 0; i < warningCount; i++) { const enabled = (warningFlags & (1 << i)) !== 0; // Known warning field if (i < OSD.constants.WARNINGS.length) { d.warnings.push($.extend(OSD.constants.WARNINGS[i], { enabled })); // Push Unknown Warning field } else { const warningNumber = i - OSD.constants.WARNINGS.length + 1; d.warnings.push({ name: "UNKNOWN", text: ["osdWarningTextUnknown", warningNumber], desc: "osdWarningUnknown", enabled, }); } } // OSD profiles d.osd_profiles.number = view.readU8(); d.osd_profiles.selected = view.readU8() - 1; // Overlay radio mode d.parameters.overlayRadioMode = view.readU8(); // Camera frame size d.parameters.cameraFrameWidth = view.readU8(); d.parameters.cameraFrameHeight = view.readU8(); if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_46)) { d.alarms["link_quality"] = { display_name: i18n.getMessage("osdTimerAlarmOptionLinkQuality"), value: view.readU16(), }; } if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_47)) { d.alarms["rssi_dbm"] = { display_name: i18n.getMessage("osdTimerAlarmOptionRssiDbm"), value: view.read16(), }; } this.processOsdElements(d, itemsPositionsRead); OSD.updateDisplaySize(); }, decodeVirtual() { const d = OSD.data; d.displayItems = []; d.statItems = []; d.warnings = []; d.timers = []; // Parse statistics display enable const expectedStatsCount = OSD.constants.STATISTIC_FIELDS.length; for (let i = 0; i < expectedStatsCount; i++) { const v = OSD.virtualMode.statisticsState[i] ? 1 : 0; // Known statistics field if (i < expectedStatsCount) { const c = OSD.constants.STATISTIC_FIELDS[i]; d.statItems.push({ name: c.name, text: c.text, desc: c.desc, index: i, enabled: v === 1, }); // Read all the data for any statistics we don't know about } else { const statisticNumber = i - expectedStatsCount + 1; d.statItems.push({ name: "UNKNOWN", text: ["osdTextStatUnknown", statisticNumber], desc: "osdDescStatUnknown", index: i, enabled: v === 1, }); } } // Parse configurable timers const expectedTimersCount = 3; for (let i = 0; i < expectedTimersCount; i++) { d.timers.push( $.extend( { index: i, }, OSD.virtualMode.timerData[i], ), ); } // Parse enabled warnings const warningCount = OSD.constants.WARNINGS.length; const warningFlags = OSD.virtualMode.warningFlags; for (let i = 0; i < warningCount; i++) { const enabled = (warningFlags & (1 << i)) !== 0; // Known warning field if (i < warningCount) { d.warnings.push($.extend(OSD.constants.WARNINGS[i], { enabled })); // Push Unknown Warning field } else { const warningNumber = i - warningCount + 1; d.warnings.push({ name: "UNKNOWN", text: ["osdWarningTextUnknown", warningNumber], desc: "osdWarningUnknown", enabled, }); } } this.processOsdElements(OSD.data, OSD.virtualMode.itemPositions); OSD.updateDisplaySize(); }, }; OSD.GUI = {}; OSD.GUI.preview = { onMouseEnter() { if (!$(this).data("field")) { return; } $(`#element-fields .field-${$(this).data("field").index}`).addClass("mouseover"); $(`.preview .char.field-${$(this).data("field").index}`).addClass("mouseover"); }, onMouseLeave() { if (!$(this).data("field")) { return; } $(`#element-fields .field-${$(this).data("field").index}`).removeClass("mouseover"); $(`.preview .char.field-${$(this).data("field").index}`).removeClass("mouseover"); }, onDragStart(e) { const ev = e.originalEvent; const displayItem = OSD.data.displayItems[$(ev.target).data("field").index]; let xPos = ev.currentTarget.dataset.x; let yPos = ev.currentTarget.dataset.y; let offsetX = 6; let offsetY = 9; if (displayItem.preview.constructor === Array) { const arrayElements = displayItem.preview; const limits = OSD.searchLimitsElement(arrayElements); xPos -= limits.minX; yPos -= limits.minY; offsetX += xPos * 12; offsetY += yPos * 18; } ev.dataTransfer.setData("text/plain", $(ev.target).data("field").index); ev.dataTransfer.setData("x", ev.currentTarget.dataset.x); ev.dataTransfer.setData("y", ev.currentTarget.dataset.y); if (GUI.operating_system !== "Linux") { // latest NW.js (0.6x.x) has introduced an issue with Linux displaying a rectangle while moving an element ev.dataTransfer.setDragImage($(this).data("field").preview_img, offsetX, offsetY); } }, onDragOver(e) { const ev = e.originalEvent; ev.preventDefault(); ev.dataTransfer.dropEffect = "move"; $(this).css({ background: "rgba(0,0,0,.5)", }); }, onDragLeave() { // brute force un-styling on drag leave $(this).removeAttr("style"); }, onDrop(e) { const ev = e.originalEvent; const fieldId = parseInt(ev.dataTransfer.getData("text/plain")); const displayItem = OSD.data.displayItems[fieldId]; let position = $(this).removeAttr("style").data("position"); const cursor = position; const cursorX = cursor % OSD.data.displaySize.x; console.log(`cursorX=${cursorX}`); if (displayItem.preview.constructor === Array) { console.log(`Initial Drop Position: ${position}`); const x = parseInt(ev.dataTransfer.getData("x")); const y = parseInt(ev.dataTransfer.getData("y")); console.log(`XY Co-ords: ${x}-${y}`); position -= x; position -= y * OSD.data.displaySize.x; console.log(`Calculated Position: ${position}`); } if (!displayItem.ignoreSize) { if (displayItem.preview.constructor !== Array) { // Standard preview, string type const overflowsLine = OSD.data.displaySize.x - ((position % OSD.data.displaySize.x) + displayItem.preview.length); if (overflowsLine < 0) { position += overflowsLine; } } else { // Advanced preview, array type const arrayElements = displayItem.preview; const limits = OSD.searchLimitsElement(arrayElements); const selectedPositionX = position % OSD.data.displaySize.x; let selectedPositionY = Math.trunc(position / OSD.data.displaySize.x); if (arrayElements[0].constructor === String) { if (position < 0) { return; } if (selectedPositionX > cursorX) { // TRUE -> Detected wrap around position += OSD.data.displaySize.x - selectedPositionX; selectedPositionY++; } else if (selectedPositionX + limits.maxX > OSD.data.displaySize.x) { // TRUE -> right border of the element went beyond left edge of screen. position -= selectedPositionX + limits.maxX - OSD.data.displaySize.x; } if (selectedPositionY < 0) { position += Math.abs(selectedPositionY) * OSD.data.displaySize.x; } else if (selectedPositionY + limits.maxY > OSD.data.displaySize.y) { position -= (selectedPositionY + limits.maxY - OSD.data.displaySize.y) * OSD.data.displaySize.x; } } else { if (limits.minX < 0 && selectedPositionX + limits.minX < 0) { position += Math.abs(selectedPositionX + limits.minX); } else if (limits.maxX > 0 && selectedPositionX + limits.maxX >= OSD.data.displaySize.x) { position -= selectedPositionX + limits.maxX + 1 - OSD.data.displaySize.x; } if (limits.minY < 0 && selectedPositionY + limits.minY < 0) { position += Math.abs(selectedPositionY + limits.minY) * OSD.data.displaySize.x; } else if (limits.maxY > 0 && selectedPositionY + limits.maxY >= OSD.data.displaySize.y) { position -= (selectedPositionY + limits.maxY - OSD.data.displaySize.y + 1) * OSD.data.displaySize.x; } } } } $(`input.${fieldId}.position`).val(position).change(); }, }; const osd = { analyticsChanges: {}, }; osd.initialize = function (callback) { if (GUI.active_tab !== "osd") { GUI.active_tab = "osd"; } if (CONFIGURATOR.virtualMode) { VirtualFC.setupVirtualOSD(); } $("#content").load("./tabs/osd.html", function () { // Prepare symbols depending on the version SYM.loadSymbols(); OSD.loadDisplayFields(); // Generate font type select element const fontPresetsElement = $(".fontpresets"); OSD.constants.FONT_TYPES.forEach(function (e) { const option = $("