pull/1961/head
Scavanger 6 months ago
commit 4b0e09aa10

@ -0,0 +1,732 @@
'use strict';
var child_process = require('child_process');
var fs = require('fs');
var path = require('path');
var minimist = require('minimist');
var archiver = require('archiver');
var del = require('del');
var NwBuilder = require('nw-builder');
var semver = require('semver');
var gulp = require('gulp');
var concat = require('gulp-concat');
const commandExistsSync = require('command-exists').sync;
// Each key in the *sources* variable must be an array of
// the source files that will be combined into a single
// file and stored in *outputDir*. Each key in *sources*
// must be also present in *output*, whose value indicates
// the filename for the output file which combines the
// contents of the source files.
//
// Keys must be camel cased and end with either 'Css' or
// 'Js' (e.g. someSourcesCss or someSourcesJs). For each
// key, a build task will be generated named by prepending
// 'build-' and converting the key to dash-separated words
// (e.g. someSourcesCss will generate build-some-sources-css).
//
// Tasks with names ending with '-js' will be executed by the
// build-all-js task, while the ones ending with '-css' will
// be done by build-all-css. There's also a build task which
// runs both build-all-css and build-all-js.
//
// The watch task will monitor any files mentioned in the *sources*
// variable and regenerate the corresponding output file when
// they change.
//
// See README.md for details on the other tasks.
var sources = {};
sources.css = [
'./main.css',
'./js/libraries/jquery.nouislider.min.css',
'./js/libraries/jquery.nouislider.pips.min.css',
'./js/libraries/flightindicators.css',
'./src/css/tabs/*.css',
'./src/css/opensans_webfontkit/fonts.css',
'./src/css/font-awesome/css/font-awesome.css',
'./src/css/dropdown-lists/css/style_lists.css',
'./js/libraries/switchery/switchery.css',
'./js/libraries/jbox/jBox.css',
'./node_modules/openlayers/dist/ol.css',
'./src/css/logic.css',
'./src/css/defaults_dialog.css',
'./src/css/groundstation.css',
];
sources.js = [
'./js/libraries/google-analytics-bundle.js',
'./node_modules/jquery/dist/jquery.min.js',
'./node_modules/jquery-ui-npm/jquery-ui.min.js',
'./node_modules/marked/lib/marked.js',
'./js/libraries/d3.min.js',
'./js/libraries/jquery.nouislider.all.min.js',
'./node_modules/three/build/three.min.js',
'./node_modules/three/examples/js/loaders/GLTFLoader.js',
'./node_modules/three/examples/js/controls/OrbitControls.js',
'./js/libraries/nw-dialog.js',
'./js/libraries/bundle_xml2js.js',
'./js/libraries/Projector.js',
'./js/libraries/CanvasRenderer.js',
'./js/libraries/jquery.flightindicators.js',
'./js/libraries/semver.js',
'./js/libraries/jbox/jBox.min.js',
'./js/libraries/switchery/switchery.js',
'./js/libraries/jquery.ba-throttle-debounce.js',
'./js/helpers.js',
'./node_modules/inflection/inflection.min.js',
'./node_modules/bluebird/js/browser/bluebird.min.js',
'./js/injected_methods.js',
'./js/intervals.js',
'./js/timeouts.js',
'./js/pid_controller.js',
'./js/simple_smooth_filter.js',
'./js/walking_average_filter.js',
'./js/gui.js',
'./js/msp/MSPCodes.js',
'./js/msp/MSPHelper.js',
'./js/msp/MSPchainer.js',
'./js/port_handler.js',
'./js/connection/connection.js',
'./js/connection/connectionBle.js',
'./js/connection/connectionSerial.js',
'./js/connection/connectionTcp.js',
'./js/connection/connectionUdp.js',
'./js/servoMixRule.js',
'./js/motorMixRule.js',
'./js/logicCondition.js',
'./js/settings.js',
'./js/outputMapping.js',
'./js/model.js',
'./js/serial_backend.js',
'./js/data_storage.js',
'./js/fc.js',
'./js/msp.js',
'./js/protocols/stm32.js',
'./js/protocols/stm32usbdfu.js',
'./js/localization.js',
'./js/boards.js',
'./js/servoMixerRuleCollection.js',
'./js/motorMixerRuleCollection.js',
'./js/logicConditionsCollection.js',
'./js/logicConditionsStatus.js',
'./js/globalVariablesStatus.js',
'./js/programmingPid.js',
'./js/programmingPidCollection.js',
'./js/programmingPidStatus.js',
'./js/vtx.js',
'./main.js',
'./js/tabs.js',
'./tabs/*.js',
'./js/eventFrequencyAnalyzer.js',
'./js/periodicStatusUpdater.js',
'./js/serial_queue.js',
'./js/msp_balanced_interval.js',
'./tabs/advanced_tuning.js',
'./tabs/ez_tune.js',
'./js/peripherals.js',
'./js/appUpdater.js',
'./js/feature_framework.js',
'./js/defaults_dialog.js',
'./js/safehomeCollection.js',
'./js/safehome.js',
'./js/waypointCollection.js',
'./js/waypoint.js',
'./node_modules/openlayers/dist/ol.js',
'./js/libraries/plotly-latest.min.js',
'./js/sitl.js',
'./js/CliAutoComplete.js',
'./node_modules/jquery-textcomplete/dist/jquery.textcomplete.js',
'./js/ltmDecoder.js',
'./js/groundstation.js'
];
sources.receiverCss = [
'./src/css/tabs/receiver_msp.css',
'./src/css/opensans_webfontkit/fonts.css',
'./js/libraries/jquery.nouislider.min.css',
'./js/libraries/jquery.nouislider.pips.min.css',
];
sources.receiverJs = [
'./node_modules/jquery/dist/jquery.min.js',
'./node_modules/jquery-ui-npm/jquery-ui.min.js',
'./js/libraries/jquery.nouislider.all.min.js',
'./tabs/receiver_msp.js'
];
sources.debugTraceJs = [
'./js/debug_trace.js'
];
sources.hexParserJs = [
'./js/workers/hex_parser.js',
];
var output = {
css: 'styles.css',
js: 'script.js',
receiverCss: 'receiver-msp.css',
receiverJs: 'receiver-msp.js',
debugTraceJs: 'debug-trace.js',
hexParserJs: 'hex_parser.js',
};
var outputDir = './build/';
var distDir = './dist/';
var appsDir = './apps/';
function get_task_name(key) {
return 'build-' + key.replace(/([A-Z])/g, function($1){return "-"+$1.toLowerCase();});
}
function getArguments() {
return minimist(process.argv.slice(2));
}
function getPlatforms() {
const defaultPlatforms = ['win32', 'win64', 'osx64', 'linux32', 'linux64'];
const platform = getArguments().platform;
if (platform) {
if (defaultPlatforms.indexOf(platform) < 0) {
throw new Error(`Invalid platform "${platform}". Available ones are: ${defaultPlatforms}`)
}
return [platform];
}
return defaultPlatforms;
}
function execSync() {
const cmd = arguments[0];
const args = Array.prototype.slice.call(arguments, 1);
const result = child_process.spawnSync(cmd, args, {stdio: 'inherit'});
if (result.error) {
throw result.error;
}
}
// Define build tasks dynamically based on the sources
// and output variables.
var buildCssTasks = [];
var buildJsTasks = [];
(function() {
// Convers fooBarBaz to foo-bar-baz
for (var k in output) {
(function (key) {
var name = get_task_name(key);
if (name.endsWith('-css')) {
buildCssTasks.push(name);
} else if (name.endsWith('-js')) {
buildJsTasks.push(name);
} else {
throw 'Invalid task name: "' + name + '": must end with -css or -js';
}
gulp.task(name, function() {
return gulp.src(sources[key])
.pipe(concat(output[key]))
.pipe(gulp.dest(outputDir));
});
})(k);
}
})();
gulp.task('build-all-js', gulp.parallel(buildJsTasks))
gulp.task('build-all-css', gulp.parallel(buildCssTasks));
gulp.task('build', gulp.parallel('build-all-css', 'build-all-js'));
gulp.task('clean', function() { return del(['./build/**', './dist/**'], {force: true}); });
// Real work for dist task. Done in another task to call it via
// run-sequence.
gulp.task('dist-build', gulp.series('build', function() {
var distSources = [
'./package.json', // For NW.js
'./manifest.json', // For Chrome app
'./eventPage.js',
'./*.html',
'./tabs/*.html',
'./images/**/*',
'./_locales/**/*',
'./build/*',
'./src/css/font-awesome/webfonts/*',
'./src/css/opensans_webfontkit/*.{eot,svg,ttf,woff,woff2}',
'./resources/*.json',
'./resources/models/*',
'./resources/osd/analogue/*.mcm',
'./resources/motor_order/*.svg',
'./resources/sitl/windows/*',
'./resources/sitl/linux/*'
];
return gulp.src(distSources, { base: '.' })
.pipe(gulp.dest(distDir));
}));
gulp.task('dist', gulp.series('clean', 'dist-build'));
// Create app directories in ./apps
gulp.task('apps', gulp.series('dist', function(done) {
var builder = new NwBuilder({
files: './dist/**/*',
buildDir: appsDir,
platforms: getPlatforms(),
flavor: 'normal',
macIcns: './images/inav.icns',
winIco: './images/inav.ico',
version: get_nw_version(),
zip: false
});
builder.on('log', console.log);
builder.build(function (err) {
if (err) {
console.log("Error building NW apps:" + err);
done();
return;
}
// Package apps as .zip files
done();
});
}));
function get_nw_version() {
return semver.valid(semver.coerce(require('./package.json').dependencies.nw));
}
function get_release_filename_base(platform) {
return 'INAV-Configurator_' + platform;
}
function get_release_filename(platform, ext, addition = '') {
var pkg = require('./package.json');
return get_release_filename_base(platform) + addition + '_' + pkg.version + '.' + ext;
}
function build_win_zip(arch) {
return function build_win_zip_proc(done) {
var pkg = require('./package.json');
// Create ZIP
console.log(`Creating ${arch} ZIP file...`);
var src = path.join(appsDir, pkg.name, arch);
var output = fs.createWriteStream(path.join(appsDir, get_release_filename(arch, 'zip')));
var archive = archiver('zip', {
zlib: { level: 9 }
});
archive.on('warning', function(err) { throw err; });
archive.on('error', function(err) { throw err; });
archive.pipe(output);
archive.directory(src, 'INAV Configurator');
return archive.finalize();
}
}
function build_win_iss(arch) {
return function build_win_iss_proc(done) {
if (!getArguments().installer) {
done();
return null;
}
// Create Installer
console.log(`Creating ${arch} Installer...`);
const innoSetup = require('@quanle94/innosetup');
const APPS_DIR = './apps/';
const pkg = require('./package.json');
// Parameters passed to the installer script
const parameters = [];
// Extra parameters to replace inside the iss file
parameters.push(`/Dversion=${pkg.version}`);
parameters.push(`/DarchName=${arch}`);
parameters.push(`/DarchAllowed=${(arch === 'win32') ? 'x86 x64' : 'x64'}`);
parameters.push(`/DarchInstallIn64bit=${(arch === 'win32') ? '' : 'x64'}`);
parameters.push(`/DsourceFolder=${APPS_DIR}`);
parameters.push(`/DtargetFolder=${APPS_DIR}`);
// Show only errors in console
parameters.push(`/Q`);
// Script file to execute
parameters.push("assets/windows/installer.iss");
innoSetup(parameters, {},
function(error) {
if (error != null) {
console.error(`Installer for platform ${arch} finished with error ${error}`);
} else {
console.log(`Installer for platform ${arch} finished`);
}
done();
});
}
}
gulp.task('release-win32', gulp.series(build_win_zip('win32'), build_win_iss('win32')));
gulp.task('release-win64', gulp.series(build_win_zip('win64'), build_win_iss('win64')));
gulp.task('release-osx64', function(done) {
var pkg = require('./package.json');
var src = path.join(appsDir, pkg.name, 'osx64', pkg.name + '.app');
// Check if we want to sign the .app bundle
if (getArguments().codesign) {
// macapptool can be downloaded from
// https://github.com/fiam/macapptool
//
// Make sure the bundle is well formed
execSync('macapptool', '-v', '1', 'fix', src);
// Sign
const codesignArgs = ['macapptool', '-v', '1', 'sign'];
const codesignIdentity = getArguments()['codesign-identity'];
if (codesignIdentity) {
codesignArgs.push('-i', codesignIdentity);
}
codesignArgs.push('-e', 'entitlements.plist');
codesignArgs.push(src)
execSync.apply(this, codesignArgs);
// Check if the bundle is signed
const codesignCheckArgs = [ 'codesign', '-vvv', '--deep', '--strict', src ];
execSync.apply(this, codesignCheckArgs);
}
// 'old' .zip mode
if (!getArguments().installer) {
const zipFilename = path.join(appsDir, get_release_filename('macOS', 'zip'));
console.log('Creating ZIP file: ' + zipFilename);
var output = fs.createWriteStream(zipFilename);
var archive = archiver('zip', {
zlib: { level: 9 }
});
archive.on('warning', function(err) { throw err; });
archive.on('error', function(err) { throw err; });
archive.pipe(output);
archive.directory(src, 'INAV Configurator.app');
output.on('close', function() {
if (getArguments().notarize) {
console.log('Notarizing DMG file: ' + zipFilename);
const notarizeArgs = ['macapptool', '-v', '1', 'notarize'];
const notarizationUsername = getArguments()['notarization-username'];
if (notarizationUsername) {
notarizeArgs.push('-u', notarizationUsername)
}
const notarizationPassword = getArguments()['notarization-password'];
if (notarizationPassword) {
notarizeArgs.push('-p', notarizationPassword)
}
notarizeArgs.push(zipFilename)
execSync.apply(this, notarizeArgs);
}
done();
});
archive.finalize();
}
// 'new' .dmg mode
else {
const appdmg = require('appdmg');
var target = path.join(appsDir, get_release_filename('macOS', 'dmg'));
console.log('Creating DMG file: ' + target);
var basepath = path.join(appsDir, pkg.name, 'osx64');
console.log('Base path: ' + basepath);
if (fs.existsSync(target)) {
fs.unlinkSync(target);
}
var specs = {};
specs["title"] = "INAV Configurator";
specs["contents"] = [
{ "x": 448, "y": 342, "type": "link", "path": "/Applications" },
{ "x": 192, "y": 344, "type": "file", "path": pkg.name + ".app", "name": "INAV Configurator.app" },
];
specs["background"] = path.join(__dirname, 'assets/osx/dmg-background.png');
specs["format"] = "UDZO";
specs["window"] = {
"size": {
"width": 638,
"height": 479,
}
};
const codesignIdentity = getArguments()['codesign-identity'];
if (getArguments().codesign) {
specs['code-sign'] = {
'signing-identity': codesignIdentity,
}
}
const ee = appdmg({
target: target,
basepath: basepath,
specification: specs,
});
ee.on('progress', function(info) {
//console.log(info);
});
ee.on('error', function(err) {
console.log(err);
});
ee.on('finish', function() {
if (getArguments().codesign) {
// Check if the bundle is signed
const codesignCheckArgs = [ 'codesign', '-vvv', '--deep', '--strict', target ];
execSync.apply(this, codesignCheckArgs);
}
if (getArguments().notarize) {
console.log('Notarizing DMG file: ' + target);
const notarizeArgs = ['xcrun', 'notarytool', 'submit'];
notarizeArgs.push(target);
const notarizationUsername = getArguments()['notarization-username'];
if (notarizationUsername) {
notarizeArgs.push('--apple-id', notarizationUsername)
} else {
throw new Error('Missing notarization username');
}
const notarizationPassword = getArguments()['notarization-password'];
if (notarizationPassword) {
notarizeArgs.push('--password', notarizationPassword)
} else {
throw new Error('Missing notarization password');
}
const notarizationTeamId = getArguments()['notarization-team-id'];
if (notarizationTeamId) {
notarizeArgs.push('--team-id', notarizationTeamId)
} else {
throw new Error('Missing notarization Team ID');
}
notarizeArgs.push('--wait');
const notarizationWebhook = getArguments()['notarization-webhook'];
if (notarizationWebhook) {
notarizeArgs.push('--webhook', notarizationWebhook);
}
execSync.apply(this, notarizeArgs);
console.log('Stapling DMG file: ' + target);
const stapleArgs = ['xcrun', 'stapler', 'staple'];
stapleArgs.push(target);
execSync.apply(this, stapleArgs);
console.log('Checking DMG file: ' + target);
const checkArgs = ['spctl', '-vvv', '--assess', '--type', 'install', target];
execSync.apply(this, checkArgs);
}
done();
});
}
});
function post_build(arch, folder) {
return function post_build_linux(done) {
if ((arch === 'linux32') || (arch === 'linux64')) {
const metadata = require('./package.json');
// Copy Ubuntu launcher scripts to destination dir
const launcherDir = path.join(folder, metadata.name, arch);
console.log(`Copy Ubuntu launcher scripts to ${launcherDir}`);
return gulp.src('assets/linux/**')
.pipe(gulp.dest(launcherDir));
}
return done();
}
}
// Create the dir directory, with write permissions
function createDirIfNotExists(dir) {
fs.mkdir(dir, '0775', function(err) {
if (err && err.code !== 'EEXIST') {
throw err;
}
});
}
function release_deb(arch) {
return function release_deb_proc(done) {
if (!getArguments().installer) {
done();
return null;
}
// Check if dpkg-deb exists
if (!commandExistsSync('dpkg-deb')) {
console.warn(`dpkg-deb command not found, not generating deb package for ${arch}`);
done();
return null;
}
const deb = require('gulp-debian');
const LINUX_INSTALL_DIR = '/opt/inav';
const metadata = require('./package.json');
console.log(`Generating deb package for ${arch}`);
return gulp.src([path.join(appsDir, metadata.name, arch, '*')])
.pipe(deb({
package: metadata.name,
version: metadata.version,
section: 'base',
priority: 'optional',
architecture: getLinuxPackageArch('deb', arch),
maintainer: metadata.author,
description: metadata.description,
preinst: [`rm -rf ${LINUX_INSTALL_DIR}/${metadata.name}`],
postinst: [
`chown root:root ${LINUX_INSTALL_DIR}`,
`chown -R root:root ${LINUX_INSTALL_DIR}/${metadata.name}`,
`xdg-desktop-menu install ${LINUX_INSTALL_DIR}/${metadata.name}/${metadata.name}.desktop`,
],
prerm: [`xdg-desktop-menu uninstall ${metadata.name}.desktop`],
depends: ['libgconf-2-4', 'libatomic1'],
changelog: [],
_target: `${LINUX_INSTALL_DIR}/${metadata.name}`,
_out: appsDir,
_copyright: 'assets/linux/copyright',
_clean: true,
}));
}
}
function post_release_deb(arch) {
return function post_release_linux_deb(done) {
if (!getArguments().installer) {
done();
return null;
}
if ((arch === 'linux32') || (arch === 'linux64')) {
var rename = require("gulp-rename");
const metadata = require('./package.json');
const renameFrom = path.join(appsDir, metadata.name + '_' + metadata.version + '_' + getLinuxPackageArch('.deb', arch) + '.deb');
const renameTo = path.join(appsDir, get_release_filename_base(arch) + '_' + metadata.version + '.deb');
// Rename .deb build to common naming
console.log(`Renaming .deb installer ${renameFrom} to ${renameTo}`);
return gulp.src(renameFrom)
.pipe(rename(renameTo))
.pipe(gulp.dest("."));
}
return done();
}
}
function release_rpm(arch) {
return function release_rpm_proc(done) {
if (!getArguments().installer) {
done();
return null;
}
// Check if rpmbuild exists
if (!commandExistsSync('rpmbuild')) {
console.warn(`rpmbuild command not found, not generating rpm package for ${arch}`);
done();
return;
}
const buildRpm = require('rpm-builder');
const NAME_REGEX = /-/g;
const LINUX_INSTALL_DIR = '/opt/inav';
const metadata = require('./package.json');
console.log(`Generating rpm package for ${arch}`);
// The buildRpm does not generate the folder correctly, manually
createDirIfNotExists(appsDir);
const options = {
name: get_release_filename_base(arch), // metadata.name,
version: metadata.version.replace(NAME_REGEX, '_'), // RPM does not like release candidate versions
buildArch: getLinuxPackageArch('rpm', arch),
vendor: metadata.author,
summary: metadata.description,
license: 'GNU General Public License v3.0',
requires: ['libatomic1'],
prefix: '/opt',
files: [{
cwd: path.join(appsDir, metadata.name, arch),
src: '*',
dest: `${LINUX_INSTALL_DIR}/${metadata.name}`,
}],
postInstallScript: [`xdg-desktop-menu install ${LINUX_INSTALL_DIR}/${metadata.name}/${metadata.name}.desktop`],
preUninstallScript: [`xdg-desktop-menu uninstall ${metadata.name}.desktop`],
tempDir: path.join(appsDir, `tmp-rpm-build-${arch}`),
keepTemp: false,
verbose: false,
rpmDest: appsDir,
execOpts: { maxBuffer: 1024 * 1024 * 16 },
};
buildRpm(options, function(err) {
if (err) {
console.error(`Error generating rpm package: ${err}`);
}
done();
});
}
}
function getLinuxPackageArch(type, arch) {
let packArch;
switch (arch) {
case 'linux32':
packArch = 'i386';
break;
case 'linux64':
if (type === 'rpm') {
packArch = 'x86_64';
} else {
packArch = 'amd64';
}
break;
default:
console.error(`Package error, arch: ${arch}`);
process.exit(1);
break;
}
return packArch;
}
function releaseLinux(bits) {
return function() {
console.log(`Generating zip package for linux${bits}`);
var dirname = 'linux' + bits;
var pkg = require('./package.json');
var src = path.join(appsDir, pkg.name, dirname);
var output = fs.createWriteStream(path.join(appsDir, get_release_filename(dirname, 'tar.gz')));
var archive = archiver('tar', {
zlib: { level: 9 },
gzip: true
});
archive.on('warning', function(err) { throw err; });
archive.on('error', function(err) { throw err; });
archive.pipe(output);
archive.directory(src, 'INAV Configurator');
return archive.finalize();
}
}
gulp.task('release-linux32', gulp.series(releaseLinux(32), post_build('linux32', appsDir), release_deb('linux32'), post_release_deb('linux32')));
gulp.task('release-linux64', gulp.series(releaseLinux(64), post_build('linux64', appsDir), release_deb('linux64'), post_release_deb('linux64'), release_rpm('linux64')));
// Create distributable .zip files in ./apps
gulp.task('release', gulp.series('apps', getPlatforms().map(function(v) { return 'release-' + v; })));
gulp.task('watch', function () {
for(var k in output) {
gulp.watch(sources[k], gulp.series(get_task_name(k)));
}
});
gulp.task('default', gulp.series('build'));

@ -176,6 +176,49 @@
<div id="scrollicon"></div>
<div class="wrapper"></div>
</div>
<div id="view-groundstation" style="display: none;">
<div id="groundstation-telemetry">
<h2 class="groundstation-telemetry__header" data-i18n="gsTelemetry"></h2>
<div class="groundstation-telemetry__row groundstation-telemetry__row--big">
<label for="gs-telemetry-voltage" class="groundstation-telemetry__label" data-i18n="gsTelemetryVoltageShort"></label>
<div class="groundstation-telemetry__value" id="gs-telemetry-voltage">-</div>
</div>
<div class="groundstation-telemetry__row groundstation-telemetry__row--big">
<label for="gs-telemetry-altitude" class="groundstation-telemetry__label" data-i18n="gsTelemetryAltitudeShort"></label>
<div class="groundstation-telemetry__value" id="gs-telemetry-altitude">-</div>
</div>
<div class="groundstation-telemetry__row groundstation-telemetry__row--big">
<label for="gs-telemetry-speed" class="groundstation-telemetry__label" data-i18n="gsTelemetrySpeed"></label>
<div class="groundstation-telemetry__value" id="gs-telemetry-speed">-</div>
</div>
<div class="groundstation-telemetry__row">
<label for="gs-telemetry-latitude" class="groundstation-telemetry__label" data-i18n="gsTelemetryLatitude"></label>
<div class="groundstation-telemetry__value" id="gs-telemetry-latitude">-</div>
</div>
<div class="groundstation-telemetry__row">
<label class="groundstation-telemetry__label" for="gs-telemetry-longitude" data-i18n="gsTelemetryLongitude"></label>
<div class="groundstation-telemetry__value" id="gs-telemetry-longitude">-</div>
</div>
<div class="groundstation-telemetry__row">
<label class="groundstation-telemetry__label" for="gs-telemetry-longitude" data-i18n="gsTelemetrySats"></label>
<div class="groundstation-telemetry__value" id="gs-telemetry-sats">-</div>
</div>
<div class="groundstation-telemetry__row">
<label class="groundstation-telemetry__label" for="gs-telemetry-longitude" data-i18n="gsTelemetryFix"></label>
<div class="groundstation-telemetry__value" id="gs-telemetry-fix">-</div>
</div>
</div>
<div id="groundstation-map"></div>
</div>
<div class="tab_container">
<div id="tabs">
<ul class="mode-disconnected">

@ -0,0 +1,194 @@
'use strict';
var helper = helper || {};
helper.groundstation = (function () {
let publicScope = {},
privateScope = {};
privateScope.activated = false;
privateScope.$viewport = null;
privateScope.$gsViewport = null;
privateScope.mapHandler = null;
privateScope.mapLayer = null;
privateScope.mapView = null;
privateScope.cursorStyle = null;
privateScope.cursorPosition = null;
privateScope.cursorFeature = null;
privateScope.cursorVector = null;
privateScope.cursorLayer = null;
privateScope.textGeometry = null;
privateScope.textFeature = null;
privateScope.textVector = null;
privateScope.textSource = null;
privateScope.mapInitiated = false;
publicScope.isActivated = function () {
return privateScope.activated;
};
publicScope.activate = function ($viewport) {
if (privateScope.activated) {
return;
}
helper.interval.add('gsUpdateGui', privateScope.updateGui, 200);
privateScope.$viewport = $viewport;
privateScope.$viewport.find(".tab_container").hide();
privateScope.$viewport.find('#content').hide();
privateScope.$viewport.find('#status-bar').hide();
privateScope.$viewport.find('#connectbutton a.connect_state').text(chrome.i18n.getMessage('disconnect')).addClass('active');
privateScope.$gsViewport = $viewport.find('#view-groundstation');
privateScope.$gsViewport.show();
privateScope.mapInitiated = false;
setTimeout(privateScope.initMap, 100);
privateScope.activated = true;
GUI.log(chrome.i18n.getMessage('gsActivated'));
}
privateScope.initMap = function () {
//initialte layers
if (globalSettings.mapProviderType == 'bing') {
privateScope.mapLayer = new ol.source.BingMaps({
key: globalSettings.mapApiKey,
imagerySet: 'AerialWithLabels',
maxZoom: 19
});
} else if (globalSettings.mapProviderType == 'mapproxy') {
privateScope.mapLayer = new ol.source.TileWMS({
url: globalSettings.proxyURL,
params: { 'LAYERS': globalSettings.proxyLayer }
})
} else {
privateScope.mapLayer = new ol.source.OSM();
}
//initiate view
privateScope.mapView = new ol.View({
center: ol.proj.fromLonLat([0, 0]),
zoom: 3
});
//initiate map handler
privateScope.mapHandler = new ol.Map({
target: document.getElementById('groundstation-map'),
layers: [
new ol.layer.Tile({
source: privateScope.mapLayer
})
],
view: privateScope.mapView
});
};
publicScope.deactivate = function () {
if (!privateScope.activated) {
return;
}
helper.interval.remove('gsUpdateGui');
if (privateScope.$viewport !== null) {
privateScope.$viewport.find(".tab_container").show();
privateScope.$viewport.find('#content').show();
privateScope.$viewport.find('#status-bar').show();
}
if (privateScope.$gsViewport !== null) {
privateScope.$gsViewport.hide();
}
privateScope.activated = false;
GUI.log(chrome.i18n.getMessage('gsDeactivated'));
}
privateScope.updateGui = function () {
let telemetry = helper.ltmDecoder.get();
if (telemetry.gpsFix && telemetry.gpsFix > 1) {
let lat = telemetry.latitude / 10000000;
let lon = telemetry.longitude / 10000000;
//On first initiation, set zoom to 15
if (!privateScope.mapInitiated) {
//Place UAV on the map
privateScope.cursorStyle = new ol.style.Style({
image: new ol.style.Icon(({
anchor: [0.5, 0.5],
opacity: 1,
scale: 0.6,
src: '../images/icons/icon_mission_airplane.png'
}))
});
privateScope.cursorPosition = new ol.geom.Point(ol.proj.fromLonLat([lon, lat]));
privateScope.cursorFeature = new ol.Feature({
geometry: privateScope.cursorPosition
});
privateScope.cursorFeature.setStyle(privateScope.cursorStyle);
privateScope.cursorVector = new ol.source.Vector({
features: [privateScope.cursorFeature]
});
privateScope.cursorLayer = new ol.layer.Vector({
source: privateScope.cursorVector
});
privateScope.mapHandler.addLayer(privateScope.cursorLayer);
privateScope.mapView.setZoom(17);
privateScope.mapInitiated = true;
}
//Update map center
let position = ol.proj.fromLonLat([lon, lat]);
privateScope.mapView.setCenter(position);
//Update position of cursor
privateScope.cursorPosition.setCoordinates(position);
//Update orientation of cursor
privateScope.cursorStyle.getImage().setRotation((telemetry.heading / 360.0) * 6.28318);
//Update text
privateScope.$viewport.find("#gs-telemetry-latitude").html(lat);
privateScope.$viewport.find("#gs-telemetry-longitude").html(lon);
}
privateScope.$viewport.find("#gs-telemetry-altitude").html(telemetry.altitude / 100.0 + 'm');
privateScope.$viewport.find("#gs-telemetry-voltage").html(telemetry.voltage / 100.0 + 'V');
privateScope.$viewport.find("#gs-telemetry-sats").html(telemetry.gpsSats);
privateScope.$viewport.find("#gs-telemetry-speed").html(telemetry.groundSpeed * 100 + 'm/s');
let fixText = '';
if (telemetry.gpsFix == 3) {
fixText = '3D';
} else if (telemetry.gpsFix == 2) {
fixText = '2D';
} else {
fixText = 'No fix';
}
privateScope.$viewport.find("#gs-telemetry-fix").html(fixText);
};
return publicScope;
})();

@ -0,0 +1,260 @@
'use strict';
var helper = helper || {};
helper.ltmDecoder = (function () {
let TELEMETRY = {
//A frame
pitch: null,
roll: null,
heading: null,
//S frame
voltage: null,
currectDrawn: null,
rssi: null,
airspeed: null,
flightmode: null,
flightmodeName: null,
armed: null,
failsafe: null,
//G frame
latitude: null,
longitude: null,
altitude: null,
groundSpeed: null,
gpsFix: null,
gpsSats: null,
//X frame
hdop: null,
sensorStatus: null,
frameCounter: null,
disarmReason: null,
disarmReasonName: null
};
let publicScope = {},
privateScope = {};
const LTM_TIMEOUT_MS = 5000;
const LTM_FRAME_TIMEOUT_MS = 700;
const LTM_HEADER_START_1 = '$';
const LTM_HEADER_START_2 = 'T';
const LTM_FRAMELENGTH = {
'G': 18,
'A': 10,
'S': 11,
'O': 18,
'N': 10,
'X': 10
};
const LTM_FLIGHT_MODE_NAMES = [
"MANUAL",
"RATE",
"ANGLE",
"HORIZON",
"ACRO",
"STABALIZED1",
"STABALIZED2",
"STABILIZED3",
"ALTHOLD",
"GPSHOLD",
"WAYPOINTS",
"HEADHOLD",
"CIRCLE",
"RTH",
"FOLLOWME",
"LAND",
"FLYBYWIRE1",
"FLYBYWIRE2",
"CRUISE",
"UNKNOWN",
"LAUNCH",
"AUTOTUNE"
];
const LTM_DISARM_REASON_NAMES = [
"NONE",
"TIMEOUT",
"STICKS",
"SWITCH_3D",
"SWITCH",
"KILLSWITCH",
"FAILSAFE",
"NAVIGATION",
"LANDING"
];
const LTM_STATE_IDLE = 0;
const LTM_STATE_HEADER_START_1 = 1;
const LTM_STATE_HEADER_START_2 = 2;
const LTM_STATE_MSGTYPE = 3;
privateScope.protocolState = LTM_STATE_IDLE;
privateScope.lastFrameReceivedMs = null;
privateScope.frameType = null;
privateScope.frameLength = null;
privateScope.receiverIndex = 0;
privateScope.serialBuffer = [];
privateScope.frameProcessingStartedAtMs = 0;
privateScope.readByte = function (offset) {
return privateScope.serialBuffer[offset];
};
privateScope.readInt = function (offset) {
return privateScope.serialBuffer[offset] + (privateScope.serialBuffer[offset + 1] << 8);
}
privateScope.readInt32 = function (offset) {
return privateScope.serialBuffer[offset] + (privateScope.serialBuffer[offset + 1] << 8) + (privateScope.serialBuffer[offset + 2] << 16) + (privateScope.serialBuffer[offset + 3] << 24);
}
privateScope.push = function (data) {
let charCode = String.fromCharCode(data);
//If frame is processed for too long, reset protocol state
if (privateScope.protocolState != LTM_STATE_IDLE && new Date().getTime() - privateScope.frameProcessingStartedAtMs > LTM_FRAME_TIMEOUT_MS) {
privateScope.protocolState = LTM_STATE_IDLE;
privateScope.frameProcessingStartedAtMs = new Date().getTime();
console.log('LTM privateScope.protocolState: TIMEOUT, forcing into IDLE, processed frame: ' + privateScope.frameType);
}
if (privateScope.protocolState == LTM_STATE_IDLE) {
if (charCode == LTM_HEADER_START_1) {
privateScope.protocolState = LTM_STATE_HEADER_START_1;
privateScope.frameProcessingStartedAtMs = new Date().getTime();
}
return;
} else if (privateScope.protocolState == LTM_STATE_HEADER_START_1) {
if (charCode == LTM_HEADER_START_2) {
privateScope.protocolState = LTM_STATE_HEADER_START_2;
} else {
privateScope.protocolState = LTM_STATE_IDLE;
}
return;
} else if (privateScope.protocolState == LTM_STATE_HEADER_START_2) {
//Check if incoming frame type is a known one
if (LTM_FRAMELENGTH[charCode] == undefined) {
//Unknown frame type, reset protocol state
privateScope.protocolState = LTM_STATE_IDLE;
console.log('Unknown frame type, reset protocol state');
} else {
//Known frame type, store it and move to next state
privateScope.frameType = charCode;
privateScope.frameLength = LTM_FRAMELENGTH[charCode];
privateScope.receiverIndex = 0;
privateScope.serialBuffer = [];
privateScope.protocolState = LTM_STATE_MSGTYPE;
console.log('protocolState: LTM_STATE_MSGTYPE', 'will expext frame ' + privateScope.frameType, 'expected length: ' + privateScope.frameLength);
}
return;
} else if (privateScope.protocolState == LTM_STATE_MSGTYPE) {
/*
* Check if last payload byte has been received.
*/
if (privateScope.receiverIndex == privateScope.frameLength - 4) {
/*
* If YES, check checksum and execute data processing
*/
let checksum = 0;
for (let i = 0; i < privateScope.serialBuffer.length; i++) {
checksum ^= privateScope.serialBuffer[i];
}
if (checksum != data) {
console.log('LTM checksum error, frame type: ' + privateScope.frameType + ' rejected');
privateScope.protocolState = LTM_STATE_IDLE;
privateScope.serialBuffer = [];
privateScope.receiverIndex = 0;
return;
}
if (privateScope.frameType == 'A') {
TELEMETRY.pitch = privateScope.readInt(0);
TELEMETRY.roll = privateScope.readInt(2);
TELEMETRY.heading = privateScope.readInt(4);
}
if (privateScope.frameType == 'S') {
TELEMETRY.voltage = privateScope.readInt(0);
TELEMETRY.currectDrawn = privateScope.readInt(2);
TELEMETRY.rssi = privateScope.readByte(4);
TELEMETRY.airspeed = privateScope.readByte(5);
let fm = privateScope.readByte(6);
TELEMETRY.flightmode = fm >> 2;
TELEMETRY.flightmodeName = LTM_FLIGHT_MODE_NAMES[TELEMETRY.flightmode];
TELEMETRY.armed = (fm & 0x02) >> 1;
TELEMETRY.failsafe = fm & 0x01;
}
if (privateScope.frameType == 'G') {
TELEMETRY.latitude = privateScope.readInt32(0);
TELEMETRY.longitude = privateScope.readInt32(4);
TELEMETRY.groundSpeed = privateScope.readByte(8);
TELEMETRY.altitude = privateScope.readInt32(9);
let raw = privateScope.readByte(13);
TELEMETRY.gpsSats = raw >> 2;
TELEMETRY.gpsFix = raw & 0x03;
}
if (privateScope.frameType == 'X') {
TELEMETRY.hdop = privateScope.readInt(0);
TELEMETRY.sensorStatus = privateScope.readByte(2);
TELEMETRY.frameCounter = privateScope.readByte(3);
TELEMETRY.disarmReason = privateScope.readByte(4);
TELEMETRY.disarmReasonName = LTM_DISARM_REASON_NAMES[TELEMETRY.disarmReason];
}
privateScope.protocolState = LTM_STATE_IDLE;
privateScope.serialBuffer = [];
privateScope.lastFrameReceivedMs = new Date().getTime();
privateScope.receiverIndex = 0;
} else {
/*
* If no, put data into buffer
*/
privateScope.serialBuffer.push(data);
privateScope.receiverIndex++;
}
}
}
publicScope.read = function (readInfo) {
var data = new Uint8Array(readInfo.data);
for (var i = 0; i < data.length; i++) {
privateScope.push(data[i]);
}
};
publicScope.isReceiving = function () {
return privateScope.lastFrameReceivedMs !== null && (new Date().getTime() - privateScope.lastFrameReceivedMs) < LTM_TIMEOUT_MS;
};
publicScope.wasEverReceiving = function () {
return privateScope.lastFrameReceivedMs !== null;
};
publicScope.get = function () {
return TELEMETRY;
};
return publicScope;
})();

@ -84,18 +84,9 @@ var MSP = {
last_received_timestamp: null,
analog_last_received_timestamp: null,
processData: null,
lastFrameReceivedMs: 0,
init() {
mspQueue.setPutCallback(this.putCallback);
mspQueue.setremoveCallback(this.removeCallback);
},
setProcessData(cb) {
this.processData = cb;
},
read(readInfo) {
read: function (readInfo) {
var data = new Uint8Array(readInfo.data);
for (var i = 0; i < data.length; i++) {
@ -252,7 +243,8 @@ var MSP = {
_dispatch_message(expected_checksum) {
if (this.message_checksum == expected_checksum) {
// message received, process
this.processData(this);
mspHelper.processData(this);
this.lastFrameReceivedMs = Date.now();
} else {
console.log('code: ' + this.code + ' - crc failed');
this.packet_error++;
@ -395,7 +387,17 @@ var MSP = {
this.packet_error = 0; // reset CRC packet error counter for next session
this.callbacks_cleanup();
},
isReceiving: function () {
return Date.now() - this.lastFrameReceivedMs < 5000;
},
wasEverReceiving: function () {
return this.lastFrameReceivedMs > 0;
}
};
module.exports = MSP;
MSP.SDCARD_STATE_NOT_PRESENT = 0;
MSP.SDCARD_STATE_FATAL = 1;
MSP.SDCARD_STATE_CARD_INIT = 2;
MSP.SDCARD_STATE_FS_INIT = 3;
MSP.SDCARD_STATE_READY = 4;

@ -159,8 +159,13 @@ var SerialBackend = (function () {
GUI.updateManualPortVisibility();
});
$('div.connect_controls a.connect').on('click', function () {
if (GUI.connect_lock != true) { // GUI control overrides the user control
$('div.connect_controls a.connect').click(function () {
if (helper.groundstation.isActivated()) {
helper.groundstation.deactivate();
}
if (GUI.connect_lock != true) { // GUI control overrides the user control
var clicks = $(this).data('clicks');
var selected_baud = parseInt(privateScope.$baud.val());
@ -354,23 +359,31 @@ var SerialBackend = (function () {
store.set('last_used_bps', CONFIGURATOR.connection.bitrate);
store.set('wireless_mode_enabled', $('#wireless-mode').is(":checked"));
CONFIGURATOR.connection.addOnReceiveListener(publicScope.read_serial);
CONFIGURATOR.connection.addOnReceiveListener(read_serial);
CONFIGURATOR.connection.addOnReceiveListener(helper.ltmDecoder.read);
// disconnect after 10 seconds with error if we don't get IDENT data
helper.timeout.add('connecting', function () {
/*
// disconnect after 10 seconds with error if we don't get IDENT data
timeout.add('connecting', function () {
if (!CONFIGURATOR.connectionValid) {
GUI.log(i18n.getMessage('noConfigurationReceived'));
//As we add LTM listener, we need to invalidate connection only when both protocols are not listening!
if (!CONFIGURATOR.connectionValid && !helper.ltmDecoder.isReceiving()) {
GUI.log(chrome.i18n.getMessage('noConfigurationReceived'));
mspQueue.flush();
mspQueue.freeHardLock();
mspQueue.freeSoftLock();
CONFIGURATOR.connection.emptyOutputBuffer();
$('div.connect_controls a').trigger( "click" ); // disconnect
}
}, 10000);
*/
$('div.connect_controls a').click(); // disconnect
}
}, 10000);
//Add a timer that every 1s will check if LTM stream is receiving data and display alert if so
helper.interval.add('ltm-connection-check', function () {
if (helper.ltmDecoder.isReceiving()) {
helper.groundstation.activate($('#main-wrapper'));
}
}, 1000);
FC.resetState();

@ -5620,5 +5620,38 @@
},
"ezTuneNote": {
"message": "<strong>Important</strong> Ez Tune is enabled. All settings on this tab are set and controlled by the Ez Tune. To use PID Tuning tab you have to disable Ez Tune. To do it, uncheck the <strong>Enabled</strong> checkbox on the Ez Tune tab."
},
"gsActivated": {
"message": "Ground station mode activated"
},
"gsDeactivated": {
"message": "Ground station mode deactivated"
},
"gsTelemetry": {
"message": "Telemetry"
},
"gsTelemetryLatitude": {
"message": "Latitude"
},
"gsTelemetryLongitude": {
"message": "Longitude"
},
"gsTelemetryAltitude": {
"message": "Altitude"
},
"gsTelemetryAltitudeShort": {
"message": "Alt"
},
"gsTelemetryVoltageShort": {
"message": "Vbat"
},
"gsTelemetrySats": {
"message": "Sats"
},
"gsTelemetryFix": {
"message": "Fix"
},
"gsTelemetrySpeed": {
"message": "Speed"
}
}

@ -0,0 +1,37 @@
#view-groundstation {
background-color: #2e2e2e;
width: 100%;
height: 100%;
display: flex;
}
#groundstation-map {
width: 100%;
height: 100%;
}
#groundstation-telemetry {
width: 20%;
color: #ddd;
}
.groundstation-telemetry__header {
margin: 1em;
text-align: center;
}
.groundstation-telemetry__row {
display: flex;
justify-content: space-between;
margin: 1em;
font-size: 1.1em;
}
.groundstation-telemetry__row--big {
font-size: 2.2em;
}
.groundstation-telemetry__label {
color: #aaa;
font-weight: bold;
}

@ -231,48 +231,50 @@ TABS.ports.initialize = function (callback) {
port_configuration_e.data('serialPort', serialPort);
port_configuration_e.find('select.msp_baudrate').val(serialPort.msp_baudrate);
port_configuration_e.find('select.telemetry_baudrate').val(serialPort.telemetry_baudrate);
port_configuration_e.find('select.sensors_baudrate').val(serialPort.sensors_baudrate);
port_configuration_e.find('select.peripherals_baudrate').val(serialPort.peripherals_baudrate);
port_configuration_e.find('.identifier').text(portIdentifierToNameMapping[serialPort.identifier]);
if (serialPort.identifier >= 30) {
port_configuration_e.find('.softSerialWarning').css("display", "inline")
} else {
port_configuration_e.find('.softSerialWarning').css("display", "none")
}
port_configuration_e.data('index', portIndex);
port_configuration_e.data('port', serialPort);
//Append only port different than USB VCP
if (serialPort.identifier != 20) {
port_configuration_e.find('select.msp_baudrate').val(serialPort.msp_baudrate);
port_configuration_e.find('select.telemetry_baudrate').val(serialPort.telemetry_baudrate);
port_configuration_e.find('select.sensors_baudrate').val(serialPort.sensors_baudrate);
port_configuration_e.find('select.peripherals_baudrate').val(serialPort.peripherals_baudrate);
port_configuration_e.find('.identifier').text(portIdentifierToNameMapping[serialPort.identifier]);
if (serialPort.identifier >= 30) {
port_configuration_e.find('.softSerialWarning').css("display", "inline")
} else {
port_configuration_e.find('.softSerialWarning').css("display", "none")
}
port_configuration_e.data('index', portIndex);
port_configuration_e.data('port', serialPort);
for (var columnIndex = 0; columnIndex < columns.length; columnIndex++) {
var column = columns[columnIndex];
for (var columnIndex = 0; columnIndex < columns.length; columnIndex++) {
var column = columns[columnIndex];
var functions_e = $(port_configuration_e).find('.functionsCell-' + column);
let functions_e_id = "portFunc-" + column + "-" + portIndex;
functions_e.attr("id", functions_e_id);
var functions_e = $(port_configuration_e).find('.functionsCell-' + column);
let functions_e_id = "portFunc-" + column + "-" + portIndex;
functions_e.attr("id", functions_e_id);
for (let i = 0; i < portFunctionRules.length; i++) {
var functionRule = portFunctionRules[i];
var functionName = functionRule.name;
if (functionRule.groups.indexOf(column) == -1) {
continue;
}
if (functionRule.groups.indexOf(column) == -1) {
continue;
}
var select_e;
if (column !== 'telemetry' && column !== 'peripherals' && column !== 'sensors') {
var checkboxId = 'functionCheckbox-' + portIndex + '-' + columnIndex + '-' + i;
functions_e.prepend('<span class="function"><input type="checkbox" class="togglemedium" id="' + checkboxId + '" value="' + functionName + '" /><label for="' + checkboxId + '"> ' + functionRule.displayName + '</label></span>');
var select_e;
if (column !== 'telemetry' && column !== 'peripherals' && column !== 'sensors') {
var checkboxId = 'functionCheckbox-' + portIndex + '-' + columnIndex + '-' + i;
functions_e.prepend('<span class="function"><input type="checkbox" class="togglemedium" id="' + checkboxId + '" value="' + functionName + '" /><label for="' + checkboxId + '"> ' + functionRule.displayName + '</label></span>');
if (serialPort.functions.indexOf(functionName) >= 0) {
var checkbox_e = functions_e.find('#' + checkboxId);
checkbox_e.prop("checked", true);
}
if (serialPort.functions.indexOf(functionName) >= 0) {
var checkbox_e = functions_e.find('#' + checkboxId);
checkbox_e.prop("checked", true);
}
} else {
} else {
var selectElementName = 'function-' + column;
var selectElementSelector = 'select[name=' + selectElementName + ']';
@ -286,14 +288,15 @@ TABS.ports.initialize = function (callback) {
}
select_e.append('<option value="' + functionName + '">' + functionRule.displayName + '</option>');
if (serialPort.functions.indexOf(functionName) >= 0) {
select_e.val(functionName);
if (serialPort.functions.indexOf(functionName) >= 0) {
select_e.val(functionName);
}
}
}
}
}
ports_e.find('tbody').append(port_configuration_e);
ports_e.find('tbody').append(port_configuration_e);
}
}
}
@ -310,8 +313,8 @@ TABS.ports.initialize = function (callback) {
function on_save_handler() {
// update configuration based on current ui state
SERIAL_CONFIG.ports = [];
//Clear ports of any previous for serials different than USB VCP
SERIAL_CONFIG.ports = SERIAL_CONFIG.ports.filter(item => item.identifier == 20)
$('.tab-ports .portConfiguration').each(function () {
@ -319,6 +322,10 @@ TABS.ports.initialize = function (callback) {
var oldSerialPort = $(this).data('serialPort');
if (oldSerialPort.identifier == 20) {
return;
}
var functions = $(portConfiguration_e).find('input:checkbox:checked').map(function() {
return this.value;
}).get();

Loading…
Cancel
Save