From 3a6b153d5536d032b07f5a58a4e3bcd3222c2d80 Mon Sep 17 00:00:00 2001
From: Andrew Tridgell
Date: Fri, 17 Jun 2022 21:49:31 +1000
Subject: [PATCH] Tools: added filter tool to web-firmware
makes it easier others to PR changes to improve the tool
---
.../Tools/FilterTool/FileSaver.js | 171 ++++++
.../web-firmware/Tools/FilterTool/filters.js | 571 ++++++++++++++++++
.../web-firmware/Tools/FilterTool/index.html | 180 ++++++
Tools/autotest/web-firmware/images/filter.png | Bin 0 -> 29215 bytes
Tools/autotest/web-firmware/index.html | 2 +
5 files changed, 924 insertions(+)
create mode 100644 Tools/autotest/web-firmware/Tools/FilterTool/FileSaver.js
create mode 100644 Tools/autotest/web-firmware/Tools/FilterTool/filters.js
create mode 100644 Tools/autotest/web-firmware/Tools/FilterTool/index.html
create mode 100644 Tools/autotest/web-firmware/images/filter.png
diff --git a/Tools/autotest/web-firmware/Tools/FilterTool/FileSaver.js b/Tools/autotest/web-firmware/Tools/FilterTool/FileSaver.js
new file mode 100644
index 0000000000..5d204aee15
--- /dev/null
+++ b/Tools/autotest/web-firmware/Tools/FilterTool/FileSaver.js
@@ -0,0 +1,171 @@
+/*
+* FileSaver.js
+* A saveAs() FileSaver implementation.
+*
+* By Eli Grey, http://eligrey.com
+*
+* License : https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md (MIT)
+* source : http://purl.eligrey.com/github/FileSaver.js
+*/
+
+// The one and only way of getting global scope in all environments
+// https://stackoverflow.com/q/3277182/1008999
+var _global = typeof window === 'object' && window.window === window
+ ? window : typeof self === 'object' && self.self === self
+ ? self : typeof global === 'object' && global.global === global
+ ? global
+ : this
+
+function bom (blob, opts) {
+ if (typeof opts === 'undefined') opts = { autoBom: false }
+ else if (typeof opts !== 'object') {
+ console.warn('Deprecated: Expected third argument to be a object')
+ opts = { autoBom: !opts }
+ }
+
+ // prepend BOM for UTF-8 XML and text/* types (including HTML)
+ // note: your browser will automatically convert UTF-16 U+FEFF to EF BB BF
+ if (opts.autoBom && /^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(blob.type)) {
+ return new Blob([String.fromCharCode(0xFEFF), blob], { type: blob.type })
+ }
+ return blob
+}
+
+function download (url, name, opts) {
+ var xhr = new XMLHttpRequest()
+ xhr.open('GET', url)
+ xhr.responseType = 'blob'
+ xhr.onload = function () {
+ saveAs(xhr.response, name, opts)
+ }
+ xhr.onerror = function () {
+ console.error('could not download file')
+ }
+ xhr.send()
+}
+
+function corsEnabled (url) {
+ var xhr = new XMLHttpRequest()
+ // use sync to avoid popup blocker
+ xhr.open('HEAD', url, false)
+ try {
+ xhr.send()
+ } catch (e) {}
+ return xhr.status >= 200 && xhr.status <= 299
+}
+
+// `a.click()` doesn't work for all browsers (#465)
+function click (node) {
+ try {
+ node.dispatchEvent(new MouseEvent('click'))
+ } catch (e) {
+ var evt = document.createEvent('MouseEvents')
+ evt.initMouseEvent('click', true, true, window, 0, 0, 0, 80,
+ 20, false, false, false, false, 0, null)
+ node.dispatchEvent(evt)
+ }
+}
+
+// Detect WebView inside a native macOS app by ruling out all browsers
+// We just need to check for 'Safari' because all other browsers (besides Firefox) include that too
+// https://www.whatismybrowser.com/guides/the-latest-user-agent/macos
+var isMacOSWebView = _global.navigator && /Macintosh/.test(navigator.userAgent) && /AppleWebKit/.test(navigator.userAgent) && !/Safari/.test(navigator.userAgent)
+
+var saveAs = _global.saveAs || (
+ // probably in some web worker
+ (typeof window !== 'object' || window !== _global)
+ ? function saveAs () { /* noop */ }
+
+ // Use download attribute first if possible (#193 Lumia mobile) unless this is a macOS WebView
+ : ('download' in HTMLAnchorElement.prototype && !isMacOSWebView)
+ ? function saveAs (blob, name, opts) {
+ var URL = _global.URL || _global.webkitURL
+ var a = document.createElement('a')
+ name = name || blob.name || 'download'
+
+ a.download = name
+ a.rel = 'noopener' // tabnabbing
+
+ // TODO: detect chrome extensions & packaged apps
+ // a.target = '_blank'
+
+ if (typeof blob === 'string') {
+ // Support regular links
+ a.href = blob
+ if (a.origin !== location.origin) {
+ corsEnabled(a.href)
+ ? download(blob, name, opts)
+ : click(a, a.target = '_blank')
+ } else {
+ click(a)
+ }
+ } else {
+ // Support blobs
+ a.href = URL.createObjectURL(blob)
+ setTimeout(function () { URL.revokeObjectURL(a.href) }, 4E4) // 40s
+ setTimeout(function () { click(a) }, 0)
+ }
+ }
+
+ // Use msSaveOrOpenBlob as a second approach
+ : 'msSaveOrOpenBlob' in navigator
+ ? function saveAs (blob, name, opts) {
+ name = name || blob.name || 'download'
+
+ if (typeof blob === 'string') {
+ if (corsEnabled(blob)) {
+ download(blob, name, opts)
+ } else {
+ var a = document.createElement('a')
+ a.href = blob
+ a.target = '_blank'
+ setTimeout(function () { click(a) })
+ }
+ } else {
+ navigator.msSaveOrOpenBlob(bom(blob, opts), name)
+ }
+ }
+
+ // Fallback to using FileReader and a popup
+ : function saveAs (blob, name, opts, popup) {
+ // Open a popup immediately do go around popup blocker
+ // Mostly only available on user interaction and the fileReader is async so...
+ popup = popup || open('', '_blank')
+ if (popup) {
+ popup.document.title =
+ popup.document.body.innerText = 'downloading...'
+ }
+
+ if (typeof blob === 'string') return download(blob, name, opts)
+
+ var force = blob.type === 'application/octet-stream'
+ var isSafari = /constructor/i.test(_global.HTMLElement) || _global.safari
+ var isChromeIOS = /CriOS\/[\d]+/.test(navigator.userAgent)
+
+ if ((isChromeIOS || (force && isSafari) || isMacOSWebView) && typeof FileReader !== 'undefined') {
+ // Safari doesn't allow downloading of blob URLs
+ var reader = new FileReader()
+ reader.onloadend = function () {
+ var url = reader.result
+ url = isChromeIOS ? url : url.replace(/^data:[^;]*;/, 'data:attachment/file;')
+ if (popup) popup.location.href = url
+ else location = url
+ popup = null // reverse-tabnabbing #460
+ }
+ reader.readAsDataURL(blob)
+ } else {
+ var URL = _global.URL || _global.webkitURL
+ var url = URL.createObjectURL(blob)
+ if (popup) popup.location = url
+ else location.href = url
+ popup = null // reverse-tabnabbing #460
+ setTimeout(function () { URL.revokeObjectURL(url) }, 4E4) // 40s
+ }
+ }
+)
+
+_global.saveAs = saveAs.saveAs = saveAs
+
+if (typeof module !== 'undefined') {
+ module.exports = saveAs;
+}
diff --git a/Tools/autotest/web-firmware/Tools/FilterTool/filters.js b/Tools/autotest/web-firmware/Tools/FilterTool/filters.js
new file mode 100644
index 0000000000..94104b7ab6
--- /dev/null
+++ b/Tools/autotest/web-firmware/Tools/FilterTool/filters.js
@@ -0,0 +1,571 @@
+function calc_lowpass_alpha_dt(dt, cutoff_freq)
+{
+ if (dt <= 0.0 || cutoff_freq <= 0.0) {
+ return 1.0;
+ }
+ var rc = 1.0/(Math.PI*2*cutoff_freq);
+ return dt/(dt+rc);
+}
+
+function LPF_1P(sample_rate,cutoff) {
+ this.reset = function(sample) {
+ this.value = sample;
+ }
+ if (cutoff <= 0) {
+ this.apply = function(sample) {
+ return sample;
+ }
+ return this;
+ }
+ this.alpha = calc_lowpass_alpha_dt(1.0/sample_rate,cutoff)
+ this.value = 0.0;
+ this.apply = function(sample) {
+ this.value += this.alpha * (sample - this.value);
+ return this.value;
+ }
+ return this;
+}
+
+function DigitalBiquadFilter(sample_freq, cutoff_freq) {
+ this.delay_element_1 = 0;
+ this.delay_element_2 = 0;
+ this.cutoff_freq = cutoff_freq;
+
+ if (cutoff_freq <= 0) {
+ // zero cutoff means pass-thru
+ this.reset = function(sample) {
+ }
+ this.apply = function(sample) {
+ return sample;
+ }
+ return this;
+ }
+
+ var fr = sample_freq/cutoff_freq;
+ var ohm = Math.tan(Math.PI/fr);
+ var c = 1.0+2.0*Math.cos(Math.PI/4.0)*ohm + ohm*ohm;
+
+ this.b0 = ohm*ohm/c;
+ this.b1 = 2.0*this.b0;
+ this.b2 = this.b0;
+ this.a1 = 2.0*(ohm*ohm-1.0)/c;
+ this.a2 = (1.0-2.0*Math.cos(Math.PI/4.0)*ohm+ohm*ohm)/c;
+ this.initialised = false;
+
+ this.apply = function(sample) {
+ if (!this.initialised) {
+ this.reset(sample);
+ this.initialised = true;
+ }
+ var delay_element_0 = sample - this.delay_element_1 * this.a1 - this.delay_element_2 * this.a2;
+ var output = delay_element_0 * this.b0 + this.delay_element_1 * this.b1 + this.delay_element_2 * this.b2;
+
+ this.delay_element_2 = this.delay_element_1;
+ this.delay_element_1 = delay_element_0;
+ return output;
+ }
+
+ this.reset = function(sample) {
+ this.delay_element_1 = this.delay_element_2 = sample * (1.0 / (1 + this.a1 + this.a2));
+ }
+
+ return this;
+}
+
+function sq(v) {
+ return v*v;
+}
+
+function constrain_float(v,vmin,vmax) {
+ if (v < vmin) {
+ return vmin;
+ }
+ if (v > vmax) {
+ return vmax;
+ }
+ return v;
+}
+
+function NotchFilter(sample_freq,center_freq_hz,bandwidth_hz,attenuation_dB) {
+ this.sample_freq = sample_freq;
+ this.center_freq_hz = center_freq_hz;
+ this.bandwidth_hz = bandwidth_hz;
+ this.attenuation_dB = attenuation_dB;
+ this.need_reset = true;
+ this.initialised = false;
+
+ this.calculate_A_and_Q = function() {
+ this.A = Math.pow(10.0, -this.attenuation_dB / 40.0);
+ if (this.center_freq_hz > 0.5 * this.bandwidth_hz) {
+ var octaves = Math.log2(this.center_freq_hz / (this.center_freq_hz - this.bandwidth_hz / 2.0)) * 2.0;
+ this.Q = Math.sqrt(Math.pow(2.0, octaves)) / (Math.pow(2.0, octaves) - 1.0);
+ } else {
+ this.Q = 0.0;
+ }
+ }
+
+ this.init_with_A_and_Q = function() {
+ if ((this.center_freq_hz > 0.0) && (this.center_freq_hz < 0.5 * this.sample_freq) && (this.Q > 0.0)) {
+ var omega = 2.0 * Math.PI * this.center_freq_hz / this.sample_freq;
+ var alpha = Math.sin(omega) / (2 * this.Q);
+ this.b0 = 1.0 + alpha*sq(this.A);
+ this.b1 = -2.0 * Math.cos(omega);
+ this.b2 = 1.0 - alpha*sq(this.A);
+ this.a0_inv = 1.0/(1.0 + alpha);
+ this.a1 = this.b1;
+ this.a2 = 1.0 - alpha;
+ this.initialised = true;
+ } else {
+ this.initialised = false;
+ }
+ }
+
+ // check center frequency is in the allowable range
+ if ((center_freq_hz > 0.5 * bandwidth_hz) && (center_freq_hz < 0.5 * sample_freq)) {
+ this.calculate_A_and_Q();
+ this.init_with_A_and_Q();
+ } else {
+ this.initialised = false;
+ }
+
+ this.apply = function(sample) {
+ if (!this.initialised || this.need_reset) {
+ // if we have not been initialised when return the input
+ // sample as output and update delayed samples
+ this.signal1 = sample;
+ this.signal2 = sample;
+ this.ntchsig = sample;
+ this.ntchsig1 = sample;
+ this.ntchsig2 = sample;
+ this.need_reset = false;
+ return sample;
+ }
+ this.ntchsig2 = this.ntchsig1;
+ this.ntchsig1 = this.ntchsig;
+ this.ntchsig = sample;
+ var output = (this.ntchsig*this.b0 + this.ntchsig1*this.b1 + this.ntchsig2*this.b2 - this.signal1*this.a1 - this.signal2*this.a2) * this.a0_inv;
+ this.signal2 = this.signal1;
+ this.signal1 = output;
+ return output;
+ }
+
+ this.reset = function(sample) {
+ this.need_reset = true;
+ this.apply(sample);
+ }
+
+ return this;
+}
+
+function HarmonicNotchFilter(sample_freq,enable,mode,freq,bw,att,ref,fm_rat,hmncs,opts) {
+ this.notches = []
+ var chained = 1;
+ var dbl = false;
+ if (opts & 1) {
+ dbl = true;
+ }
+
+ this.reset = function(sample) {
+ for (n in this.notches) {
+ this.notches[n].reset(sample);
+ }
+ }
+
+ if (enable <= 0) {
+ this.apply = function(sample) {
+ return sample;
+ }
+ return this;
+ }
+
+ if (mode == 0) {
+ // fixed notch
+ }
+ if (mode == 1) {
+ var motors_throttle = Math.max(0,get_form("Throttle"));
+ var throttle_freq = freq * Math.max(fm_rat,Math.sqrt(motors_throttle / ref));
+ freq = throttle_freq;
+ }
+ if (mode == 2) {
+ var rpm = get_form("RPM1");
+ freq = Math.max(rpm/60.0,freq) * ref;
+ }
+ if (mode == 5) {
+ var rpm = get_form("RPM2");
+ freq = Math.max(rpm/60.0,freq) * ref;
+ }
+ if (mode == 3) {
+ if (opts & 2) {
+ chained = get_form("NUM_MOTORS");
+ }
+ var rpm = get_form("ESC_RPM");
+ freq = Math.max(rpm/60.0,freq) * ref;
+ }
+ for (var n=0;n<8;n++) {
+ var fmul = n+1;
+ if (hmncs & (1< samples/10) {
+ integral_in += Math.abs(sample);
+ }
+ if (sample >= 0 && last_in < 0) {
+ crossing_in_i = i;
+ }
+ last_in = sample;
+ for (var j=0;j samples/10) {
+ integral_out += Math.abs(sample);
+ }
+ if (sample >= 0 && last_out < 0) {
+ crossing_sum += (i-crossing_in_i);
+ crossing_count ++;
+ }
+ last_out = sample;
+ }
+ var ratio = integral_out/integral_in;
+ var avg_lag = crossing_sum / crossing_count;
+ var lag = (360.0 * avg_lag * freq) / sample_rate;
+ return [ratio,lag];
+}
+
+var chart;
+
+function calculate_filter() {
+ var sample_rate = get_form("GyroSampleRate");
+ var filters = []
+ var freq_max = get_form("MaxFreq");
+ var samples = 100000;
+ var freq_step = 1;
+ filters.push(new HarmonicNotchFilter(sample_rate,
+ get_form("INS_HNTCH_ENABLE"),
+ get_form("INS_HNTCH_MODE"),
+ get_form("INS_HNTCH_FREQ"),
+ get_form("INS_HNTCH_BW"),
+ get_form("INS_HNTCH_ATT"),
+ get_form("INS_HNTCH_REF"),
+ get_form("INS_HNTCH_FM_RAT"),
+ get_form("INS_HNTCH_HMNCS"),
+ get_form("INS_HNTCH_OPTS")));
+ filters.push(new HarmonicNotchFilter(sample_rate,
+ get_form("INS_HNTC2_ENABLE"),
+ get_form("INS_HNTC2_MODE"),
+ get_form("INS_HNTC2_FREQ"),
+ get_form("INS_HNTC2_BW"),
+ get_form("INS_HNTC2_ATT"),
+ get_form("INS_HNTC2_REF"),
+ get_form("INS_HNTC2_FM_RAT"),
+ get_form("INS_HNTC2_HMNCS"),
+ get_form("INS_HNTC2_OPTS")));
+ filters.push(new DigitalBiquadFilter(sample_rate,get_form("INS_GYRO_FILTER")));
+ filters.push(new LPF_1P(sample_rate,get_form("FLTD")));
+
+ var num_notches = 0;
+ for (const f in filters) {
+ if ('notches' in filters[f]) {
+ num_notches += filters[f].notches.length;
+ }
+ }
+ samples /= (num_notches+1);
+ samples /= Math.max(1, freq_max/150.0);
+ // console.log("samples: " + samples)
+
+ var attenuation = []
+ var phase_lag = []
+ var max_phase_lag = 0.0;
+ var phase_wrap = 0.0;
+ for (freq=1; freq<=freq_max; freq++) {
+ var v = run_filters(filters, freq, sample_rate, samples);
+ attenuation.push({x:freq, y:v[0]});
+ var phase = v[1] + phase_wrap;
+ if (phase < max_phase_lag-100) {
+ // we have wrapped
+ phase_wrap += 360.0;
+ phase += 360.0;
+ }
+ phase_lag.push({x:freq, y:-phase});
+ if (phase > max_phase_lag) {
+ max_phase_lag = phase;
+ }
+ }
+ max_phase_lag = Math.ceil((max_phase_lag+10)/10)*10;
+ max_phase_lag = Math.min(get_form("MaxPhaseLag"), max_phase_lag);
+ if (chart) {
+ chart.data.datasets[0].data = attenuation;
+ chart.data.datasets[1].data = phase_lag;
+ chart.options.scales.xAxes[0].ticks.max = freq_max;
+ chart.options.scales.yAxes[1].ticks.min = -max_phase_lag;
+ chart.options.scales.yAxes[1].ticks.max = 0;
+ chart.update();
+ } else {
+ chart = new Chart("Attenuation", {
+ type : "scatter",
+ data: {
+ datasets: [
+ {
+ label: 'Attenuation',
+ yAxisID: 'Attenuation',
+ pointRadius: 4,
+ borderColor: "rgba(0,0,255,1.0)",
+ pointBackgroundColor: "rgba(0,0,255,1.0)",
+ data: attenuation,
+ showLine: true,
+ fill: false
+ },
+ {
+ label: 'PhaseLag',
+ yAxisID: 'PhaseLag',
+ pointRadius: 4,
+ borderColor: "rgba(255,0,0,1.0)",
+ pointBackgroundColor: "rgba(255,0,0,1.0)",
+ data: phase_lag,
+ showLine: true,
+ fill: false
+ }
+ ]
+ },
+ options: {
+ legend: {display: true},
+ scales: {
+ yAxes: [
+ {
+ scaleLabel: { display: true, labelString: "Attenuation" },
+ id: 'Attenuation',
+ position: 'left',
+ ticks: {min:0, max:1.0, stepSize:0.1}
+ },
+ {
+ scaleLabel: { display: true, labelString: "Phase Lag(deg)" },
+ id: 'PhaseLag',
+ position: 'right',
+ ticks: {min:-max_phase_lag, max:0, stepSize:10}
+ }
+ ],
+ xAxes: [
+ {
+ scaleLabel: { display: true, labelString: "Frequency(Hz)" },
+ ticks: {min:0, max:freq_max, stepSize:10}
+ }
+ ],
+ }
+ }
+ });
+ }
+ //console.log("At 15Hz: " + attenuation[14].y + " " + phase_lag[14].y);
+ //console.log("At 30Hz: " + attenuation[29].y + " " + phase_lag[29].y);
+}
+
+function setCookie(c_name, value) {
+ var exdate = new Date();
+ var exdays = 365;
+ exdate.setDate(exdate.getDate() + exdays);
+ var c_value = escape(value) + ";expires=" + exdate.toUTCString();
+ document.cookie = c_name + "=" + c_value + ";path=/";
+}
+
+function getCookie(c_name, def_value) {
+ let name = c_name + "=";
+ let decodedCookie = decodeURIComponent(document.cookie);
+ let ca = decodedCookie.split(';');
+ for(let i = 0; i -1 ? cookie.substr(0, eqPos) : cookie;
+ document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT";
+ }
+}
+
+function save_parameters() {
+ var params = "";
+ var inputs = document.forms["params"].getElementsByTagName("input");
+ for (const v in inputs) {
+ var name = "" + inputs[v].name;
+ if (name.startsWith("INS_")) {
+ var value = inputs[v].value;
+ params += name + "=" + value + "\n";
+ }
+ }
+ var blob = new Blob([params], { type: "text/plain;charset=utf-8" });
+ saveAs(blob, "filter.param");
+}
+
+async function load_parameters(file) {
+ var text = await file.text();
+ var lines = text.split('\n');
+ for (i in lines) {
+ var line = lines[i];
+ if (line.startsWith("INS_")) {
+ v = line.split(/[\s,=\t]+/);
+ if (v.length >= 2) {
+ var vname = v[0];
+ var value = v[1];
+ var fvar = document.getElementById(vname);
+ if (fvar) {
+ fvar.value = value;
+ console.log("set " + vname + "=" + value);
+ }
+ }
+ }
+ }
+ fill_docs();
+ calculate_filter();
+}
+
+function fill_docs()
+{
+ var inputs = document.forms["params"].getElementsByTagName("input");
+ for (const v in inputs) {
+ var name = inputs[v].name;
+ var doc = document.getElementById(name + ".doc");
+ if (!doc) {
+ continue;
+ }
+ inputs[v].onchange = fill_docs;
+ var value = parseFloat(inputs[v].value);
+ if (name.endsWith("_ENABLE")) {
+ if (value >= 1) {
+ doc.innerHTML = "Enabled";
+ } else {
+ doc.innerHTML = "Disabled";
+ }
+ } else if (name.endsWith("_MODE")) {
+ switch (Math.floor(value)) {
+ case 0:
+ doc.innerHTML = "Fixed notch";
+ break;
+ case 1:
+ doc.innerHTML = "Throttle";
+ break;
+ case 2:
+ doc.innerHTML = "RPM Sensor 1";
+ break;
+ case 3:
+ doc.innerHTML = "ESC Telemetry";
+ break;
+ case 4:
+ doc.innerHTML = "Dynamic FFT";
+ break;
+ case 5:
+ doc.innerHTML = "RPM Sensor 2";
+ break;
+ default:
+ doc.innerHTML = "INVALID";
+ break;
+ }
+ } else if (name.endsWith("_OPTS")) {
+ var ival = Math.floor(value);
+ var bits = [];
+ if (ival & 1) {
+ bits.push("Double Notch");
+ }
+ if (ival & 2) {
+ bits.push("Dynamic Harmonic");
+ }
+ if (ival & 4) {
+ bits.push("Loop Rate");
+ }
+ if (ival & 8) {
+ bits.push("All IMUs Rate");
+ }
+ doc.innerHTML = bits.join(", ");
+ } else if (name.endsWith("_HMNCS")) {
+ var ival = Math.floor(value);
+ var bits = [];
+ if (ival & 1) {
+ bits.push("Fundamental");
+ }
+ if (ival & 2) {
+ bits.push("1st Harmonic");
+ }
+ if (ival & 4) {
+ bits.push("2nd Harmonic");
+ }
+ if (ival & 8) {
+ bits.push("3rd Harmonic");
+ }
+ if (ival & 16) {
+ bits.push("4th Harmonic");
+ }
+ if (ival & 32) {
+ bits.push("5th Harmonic");
+ }
+ if (ival & 64) {
+ bits.push("6th Harmonic");
+ }
+ doc.innerHTML = bits.join(", ");
+ }
+
+ }
+}
diff --git a/Tools/autotest/web-firmware/Tools/FilterTool/index.html b/Tools/autotest/web-firmware/Tools/FilterTool/index.html
new file mode 100644
index 0000000000..0f80369157
--- /dev/null
+++ b/Tools/autotest/web-firmware/Tools/FilterTool/index.html
@@ -0,0 +1,180 @@
+
+
+
+
+ArduPilot Filter Analysis
+
+
+
+
+
+ArduPilot Filter Analysis
+
+The following form will display the attenuation and phase lag for an
+ArduPilot 4.2 filter setup.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Tools/autotest/web-firmware/images/filter.png b/Tools/autotest/web-firmware/images/filter.png
new file mode 100644
index 0000000000000000000000000000000000000000..bc541d07d59c23b5f629ecfd456b9a72feef8015
GIT binary patch
literal 29215
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%0AP}XSSmM
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
F{{xFyQtSW#
literal 0
HcmV?d00001
diff --git a/Tools/autotest/web-firmware/index.html b/Tools/autotest/web-firmware/index.html
index 2e15d88e3c..4ceef07a40 100644
--- a/Tools/autotest/web-firmware/index.html
+++ b/Tools/autotest/web-firmware/index.html
@@ -98,6 +98,8 @@ The software and hardware we provide is only for use in unmanned
alt="Companion">Companion
- Companion Computer example code and Images
AP_Periph - UAVCAN Peripheral Firmware
+FilterTool - Filter Analysis Tool
Types of firmware available