/*global $*/ 'use strict'; var SYM = SYM || {}; SYM.VOLT = 0x06; SYM.RSSI = 0x01; SYM.AH_RIGHT = 0x02; SYM.AH_LEFT = 0x03; SYM.THR = 0x04; SYM.THR1 = 0x05; SYM.FLY_M = 0x9C; SYM.ON_M = 0x9B; SYM.AH_CENTER_LINE = 0x26; SYM.AH_CENTER_LINE_RIGHT = 0x27; SYM.AH_CENTER = 0x7E; 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.GPS_SAT = 0x1F; var FONT = FONT || {}; 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 = { 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, LINE: 30 }, 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)' } }; /** * 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 (data) { data = data.split("\n"); // clear local data FONT.data.characters.length = 0; FONT.data.characters_bytes.length = 0; FONT.data.character_image_urls.length = 0; // make sure the font file is valid if (data.shift().trim() != 'MAX7456') { var msg = 'that font file doesnt have the MAX7456 header, giving up'; console.debug(msg); Promise.reject(msg); } var character_bits = []; var character_bytes = []; // hexstring is for debugging FONT.data.hexstring = []; var pushChar = function () { FONT.data.characters_bytes.push(character_bytes); FONT.data.characters.push(character_bits); FONT.draw(FONT.data.characters.length - 1); //$log.debug('parsed char ', i, ' as ', character); character_bits = []; character_bytes = []; }; for (var i = 0; i < data.length; i++) { var 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 (character_bits.length == FONT.constants.SIZES.MAX_NVM_FONT_CHAR_FIELD_SIZE * (8 / 2)) { pushChar() } for (var y = 0; y < 8; y = y + 2) { var v = parseInt(line.slice(y, y + 2), 2); character_bits.push(v); } character_bytes.push(parseInt(line, 2)); } // push the last char pushChar(); return FONT.data.characters; }; //noinspection JSUnusedLocalSymbols FONT.openFontFile = function ($preview) { return new Promise(function (resolve) { //noinspection JSUnresolvedVariable chrome.fileSystem.chooseEntry({type: 'openFile', accepts: [ {extensions: ['mcm']} ]}, function (fileEntry) { FONT.data.loaded_font_file = fileEntry.name; //noinspection JSUnresolvedVariable if (chrome.runtime.lastError) { //noinspection JSUnresolvedVariable console.error(chrome.runtime.lastError.message); return; } fileEntry.file(function (file) { var reader = new FileReader(); reader.onloadend = function (e) { //noinspection JSUnresolvedVariable if (e.total != 0 && e.total == e.loaded) { FONT.parseMCMFontFile(e.target.result); resolve(); } else { console.error('could not load whole font file'); } }; reader.readAsText(file); }); }); }); }; /** * returns a canvas image with the character on it */ var drawCanvas = function (charAddress) { var canvas = document.createElement('canvas'); var ctx = canvas.getContext("2d"); // TODO: do we want to be able to set pixel size? going to try letting the consumer scale the image. var pixelSize = pixelSize || 1; var width = pixelSize * FONT.constants.SIZES.CHAR_WIDTH; var height = pixelSize * FONT.constants.SIZES.CHAR_HEIGHT; canvas.width = width; canvas.height = height; for (var y = 0; y < height; y++) { for (var x = 0; x < width; x++) { if (!(charAddress in FONT.data.characters)) { console.log('charAddress', charAddress, ' is not in ', FONT.data.characters.length); } var 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) { var 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: function (charAddress) { return [charAddress].concat(FONT.data.characters_bytes[charAddress].slice(0, FONT.constants.SIZES.MAX_NVM_FONT_CHAR_SIZE)); } }; FONT.upload = function ($progress) { return Promise.mapSeries(FONT.data.characters, function (data, i) { $progress.val((i / FONT.data.characters.length) * 100); return MSP.promise(MSPCodes.MSP_OSD_CHAR_WRITE, FONT.msp.encode(i)); }) .then(function () { OSD.GUI.jbox.close(); return MSP.promise(MSPCodes.MSP_SET_REBOOT); }); }; FONT.preview = function ($el) { $el.empty(); for (var i = 0; i < SYM.LOGO; i++) { var url = FONT.data.character_image_urls[i]; $el.append(''); } }; FONT.symbol = function (hexVal) { return String.fromCharCode(hexVal); }; var OSD = OSD || {}; // 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: [], display_items: [], last_positions: {}, preview: [] }; }; OSD.initData(); OSD.constants = { VISIBLE: 0x0800, VIDEO_TYPES: [ 'AUTO', 'PAL', 'NTSC' ], VIDEO_LINES: { PAL: 16, NTSC: 13 }, VIDEO_BUFFER_CHARS: { PAL: 480, NTSC: 390 }, UNIT_TYPES: [ 'IMPERIAL', 'METRIC' ], AHISIDEBARWIDTHPOSITION: 7, AHISIDEBARHEIGHTPOSITION: 3, // All display fields, from every version, do not remove elements, only add! ALL_DISPLAY_FIELDS: { MAIN_BATT_VOLTAGE: { name: 'MAIN_BATT_VOLTAGE', default_position: -29, positionable: true, preview: FONT.symbol(SYM.VOLT) + '16.8' }, RSSI_VALUE: { name: 'RSSI_VALUE', default_position: -59, positionable: true, preview: FONT.symbol(SYM.RSSI) + '99' }, TIMER: { name: 'TIMER', default_position: -39, positionable: true, preview: FONT.symbol(SYM.ON_M) + ' 11:11' }, THROTTLE_POSITION: { name: 'THROTTLE_POSITION', default_position: -9, positionable: true, preview: FONT.symbol(SYM.THR) + FONT.symbol(SYM.THR1) + ' 69' }, CPU_LOAD: { name: 'CPU_LOAD', default_position: 26, positionable: true, preview: '15' }, VTX_CHANNEL: { name: 'VTX_CHANNEL', default_position: 1, positionable: true, preview: 'CH:1' }, VOLTAGE_WARNING: { name: 'VOLTAGE_WARNING', default_position: -80, positionable: true, preview: 'LOW VOLTAGE' }, ARMED: { name: 'ARMED', default_position: -107, positionable: true, preview: 'ARMED' }, DISARMED: { name: 'DISARMED', default_position: -109, positionable: true, preview: 'DISARMED' }, CROSSHAIRS: { name: 'CROSSHAIRS', default_position: -1, positionable: false }, ARTIFICIAL_HORIZON: { name: 'ARTIFICIAL_HORIZON', default_position: -1, positionable: false }, HORIZON_SIDEBARS: { name: 'HORIZON_SIDEBARS', default_position: -1, positionable: false }, CURRENT_DRAW: { name: 'CURRENT_DRAW', default_position: -23, positionable: true, preview: FONT.symbol(SYM.AMP) + '42.0' }, MAH_DRAWN: { name: 'MAH_DRAWN', default_position: -18, positionable: true, preview: FONT.symbol(SYM.MAH) + '690' }, CRAFT_NAME: { name: 'CRAFT_NAME', default_position: -77, positionable: true, preview: '[CRAFT_NAME]' }, ALTITUDE: { name: 'ALTITUDE', default_position: 62, positionable: true, preview: function (osd_data) { return '399.7' + FONT.symbol(osd_data.unit_mode === 0 ? SYM.FEET : SYM.METRE) } }, ONTIME: { name: 'ONTIME', default_position: -1, positionable: true, preview: FONT.symbol(SYM.ON_M) + ' 4:11' }, FLYTIME: { name: 'FLYTIME', default_position: -1, positionable: true, preview: FONT.symbol(SYM.FLY_M) + ' 4:11' }, FLYMODE: { name: 'FLYMODE', default_position: -1, positionable: true, preview: 'STAB' }, GPS_SPEED: { name: 'GPS_SPEED', default_position: -1, positionable: true, preview: '40' }, GPS_SATS: { name: 'GPS_SATS', default_position: -1, positionable: true, preview: FONT.symbol(SYM.GPS_SAT) + '14' } } }; // Pick display fields by version, order matters, so these are going in an array... pry could iterate the example map instead OSD.chooseFields = function () { var F = OSD.constants.ALL_DISPLAY_FIELDS; OSD.constants.DISPLAY_FIELDS = [ F.RSSI_VALUE, F.MAIN_BATT_VOLTAGE, F.CROSSHAIRS, F.ARTIFICIAL_HORIZON, F.HORIZON_SIDEBARS, F.ONTIME, F.FLYTIME, 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 ] }; OSD.updateDisplaySize = function () { var video_type = OSD.constants.VIDEO_TYPES[OSD.data.video_system]; if (video_type == 'AUTO') { video_type = 'PAL'; } // compute the size OSD.data.display_size = { x: FONT.constants.SIZES.LINE, y: OSD.constants.VIDEO_LINES[video_type], total: null }; }; //noinspection JSUnusedLocalSymbols OSD.msp = { /** * Note, unsigned 16 bit int for position ispacked: * 0: unused * v: visible flag * b: blink flag * y: y coordinate * x: x coordinate * 0000 vbyy yyyx xxxx */ helpers: { unpack: { position: function (bits, c) { var display_item = {}; // size * y + x display_item.position = FONT.constants.SIZES.LINE * ((bits >> 5) & 0x001F) + (bits & 0x001F); display_item.isVisible = (bits & OSD.constants.VISIBLE) != 0; return display_item; } }, pack: { position: function (display_item) { return (display_item.isVisible ? 0x0800 : 0) | (((display_item.position / FONT.constants.SIZES.LINE) & 0x001F) << 5) | (display_item.position % FONT.constants.SIZES.LINE); } } }, encodeOther: function () { var result = [-1, OSD.data.video_system]; 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(OSD.data.alarms.time.value); result.push16(OSD.data.alarms.alt.value); return result; }, encode: function (display_item) { var buffer = []; buffer.push8(display_item.index); buffer.push16(this.helpers.pack.position(display_item)); return buffer; }, /* * Currently only parses MSP_MAX_OSD responses, add a switch on payload.code if more codes are handled */ decode: function (payload) { var view = payload.data; var d = OSD.data; d.compiled_in = view.readU8(); d.video_system = view.readU8(); d.unit_mode = view.readU8(); d.alarms = {}; d.alarms['rssi'] = { display_name: 'Rssi', value: view.readU8() }; d.alarms['cap'] = { display_name: 'Capacity', value: view.readU16() }; d.alarms['time'] = { display_name: 'Minutes', value: view.readU16() }; d.alarms['alt'] = { display_name: 'Altitude', value: view.readU16() }; d.display_items = []; // start at the offset from the other fields while (view.offset < view.byteLength) { var v = null; v = view.readU16(); var j = d.display_items.length; var c = OSD.constants.DISPLAY_FIELDS[j]; if (c) { d.display_items.push($.extend({ name: c.name, index: j, positionable: c.positionable, preview: typeof(c.preview) === 'function' ? c.preview(d) : c.preview }, this.helpers.unpack.position(v, c))); } } OSD.updateDisplaySize(); } }; OSD.GUI = {}; OSD.GUI.preview = { onMouseEnter: function () { if (!$(this).data('field')) { return; } $('.field-' + $(this).data('field').index).addClass('mouseover') }, onMouseLeave: function () { if (!$(this).data('field')) { return; } $('.field-' + $(this).data('field').index).removeClass('mouseover') }, onDragStart: function (e) { var ev = e.originalEvent; //noinspection JSUnresolvedVariable ev.dataTransfer.setData("text/plain", $(ev.target).data('field').index); //noinspection JSUnresolvedVariable ev.dataTransfer.setDragImage($(this).data('field').preview_img, 6, 9); }, onDragOver: function (e) { var ev = e.originalEvent; ev.preventDefault(); //noinspection JSUnresolvedVariable ev.dataTransfer.dropEffect = "move"; $(this).css({ background: 'rgba(0,0,0,.5)' }); }, onDragLeave: function (e) { // brute force unstyling on drag leave $(this).removeAttr('style'); }, onDrop: function (e) { var ev = e.originalEvent; var position = $(this).removeAttr('style').data('position'); //noinspection JSUnresolvedVariable var field_id = parseInt(ev.dataTransfer.getData('text')); var display_item = OSD.data.display_items[field_id]; var overflows_line = FONT.constants.SIZES.LINE - ((position % FONT.constants.SIZES.LINE) + display_item.preview.length); if (overflows_line < 0) { position += overflows_line; } $('input.' + field_id + '.position').val(position).change(); } }; TABS.osd = {}; TABS.osd.initialize = function (callback) { if (GUI.active_tab != 'osd') { GUI.active_tab = 'osd'; } $('#content').load("./tabs/osd.html", function () { // translate to user-selected language localize(); // Open modal window OSD.GUI.jbox = new jBox('Modal', { width: 600, height: 240, closeButton: 'title', animation: false, attach: $('#fontmanager'), title: 'OSD Font Manager', content: $('#fontmanagercontent') }); // 2 way binding... sorta function updateOsdView() { // ask for the OSD config data MSP.promise(MSPCodes.MSP_OSD_CONFIG) .then(function (info) { var i, type; OSD.chooseFields(); if (info.length <= 1) { $('.unsupported').fadeIn(); return; } $('.supported').fadeIn(); OSD.msp.decode(info); // video mode var $videoTypes = $('.video-types').empty(); for (i = 0; i < OSD.constants.VIDEO_TYPES.length; i++) { $videoTypes.append( $('') .prop('checked', i === OSD.data.video_system) .data('type', i) ) ); } $videoTypes.find(':radio').click(function () { OSD.data.video_system = $(this).data('type'); MSP.promise(MSPCodes.MSP_SET_OSD_CONFIG, OSD.msp.encodeOther()) .then(function () { updateOsdView(); }); }); // units $('.units-container').show(); var $unitMode = $('.units').empty(); for (i = 0; i < OSD.constants.UNIT_TYPES.length; i++) { var $checkbox = $('' ) .prop('checked', i === OSD.data.unit_mode) .data('type', i) ); $unitMode.append($checkbox); } $unitMode.find(':radio').click(function (e) { OSD.data.unit_mode = $(this).data('type'); MSP.promise(MSPCodes.MSP_SET_OSD_CONFIG, OSD.msp.encodeOther()) .then(function () { updateOsdView(); }); }); // alarms $('.alarms-container').show(); var $alarms = $('.alarms').empty(); for (let k in OSD.data.alarms) { var alarm = OSD.data.alarms[k]; var alarmInput = $('' + alarm.display_name + ''); alarmInput.val(alarm.value); alarmInput.blur(function (e) { OSD.data.alarms[$(this)[0].id].value = $(this)[0].value; MSP.promise(MSPCodes.MSP_SET_OSD_CONFIG, OSD.msp.encodeOther()) .then(function () { updateOsdView(); }); }); var $input = $('