Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update/real performance #12

Merged
merged 5 commits into from
Aug 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/actions/spelling/excludes.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ ignore$
^Autocoders/Python/test/.*\.xml$
/doc/xml/
/third-party/
/vendor/
/modified-vendor/
\.min\.
\.bak$
\.bin$
Expand Down
3 changes: 3 additions & 0 deletions .github/actions/spelling/expect.txt
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ FONTPATH
FONTSIZE
fprime
fptable
framerate
fromtimestamp
frontend
fsw
Expand Down Expand Up @@ -567,6 +568,7 @@ thtcp
thudp
timebase
timedelta
timescales
timestep
timestring
timetype
Expand Down Expand Up @@ -664,6 +666,7 @@ xcode
xhtml
xhttp
xl
xy
xlsx
xml
yaml
Expand Down
7 changes: 6 additions & 1 deletion src/fprime_gds/flask/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,15 @@ def construct_app():
# Optionally serve log files
if app.config["SERVE_LOGS"]:
api.add_resource(
fprime_gds.flask.logs.FlaskLogger,
fprime_gds.flask.logs.LogList,
"/logdata",
resource_class_args=[app.config["LOG_DIR"]],
)
api.add_resource(
fprime_gds.flask.logs.LogFile,
"/logdata/<name>",
resource_class_args=[app.config["LOG_DIR"]],
)
return app, api


Expand Down
36 changes: 26 additions & 10 deletions src/fprime_gds/flask/logs.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,30 @@
####
#
# Handles GDS logs in a lazy-loading way
####
import os
import flask_restful
import flask_restful.reqparse


class FlaskLogger(flask_restful.Resource):
class LogList(flask_restful.Resource):
""" A list of log files as produced by the GDS. """

def __init__(self, logdir):
"""
Constructor used to setup the log directory.

:param logdir: log directory to search for logs
"""
self.logdir = logdir

def get(self):
""" Returns a list of log files that are available. """
logs = {}
listing = os.listdir(self.logdir)
return {"logs": [name for name in listing if name.endswith(".log")]}


class LogFile(flask_restful.Resource):
"""
Command dictionary endpoint. Will return dictionary when hit with a GET.
"""
Expand All @@ -19,16 +37,14 @@ def __init__(self, logdir):
"""
self.logdir = logdir

def get(self):
def get(self, name):
"""
Returns the logdir.
"""
logs = {}
listing = os.listdir(self.logdir)
for path in [path for path in listing if path.endswith(".log")]:
full_path = os.path.join(self.logdir, path)
offset = 0
with open(full_path) as file_handle:
file_handle.seek(offset)
logs[path] = file_handle.read()
full_path = os.path.join(self.logdir, name)
offset = 0
with open(full_path) as file_handle:
file_handle.seek(offset)
logs[name] = file_handle.read()
return logs
102 changes: 102 additions & 0 deletions src/fprime_gds/flask/static/addons/chart-display/addon-templates.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/**
* addon-templates.js:
*
* Contains the HTML templates for the chart addon. This includes a chart wrapper and the chart itself.
*
* @type {string}
*/

export let chart_wrapper_template = `
<div class="fp-flex-repeater">

<div class="row mt-2">
<div class="col-md-10">
<button class="btn btn-sm btn-secondary" v-on:click="addChart">
<span class="fp-chart-btn-icon">&plus;</span><span class="fp-chart-btn-text">Add Chart</span>
</button>
<button class="btn btn-sm" :class="{'btn-secondary': !this.siblings.in_sync, 'btn-success': siblings.in_sync}" v-on:click="siblings.in_sync = !siblings.in_sync">
<span class="fp-chart-btn-text">Lock Timescales</span>
</button>
</div>
<div class="col-md-2">
<button class="btn btn-sm btn-secondary float-right" v-on:click="isHelpActive = !isHelpActive">
<span class="fp-chart-btn-text">Help</span>
</button>
</div>
</div>

<transition name="fade">
<div v-if="isHelpActive">
<div class="alert alert-warning alert-dismissible fade show" role="alert">
<div class="row">
<div class="col-6">
<strong>Zoom in and out</strong> by holding <strong>ALT</strong> and using mouse wheel to scroll while hovering over an axis <br/>
<strong>Zoom in</strong> by holding <strong>ALT</strong> and clicking and dragging a selection on the chart
</div>
<div class="col-6">
<strong>Pan</strong> by holding <strong>SHIFT</strong> and clicking and dragging the chart <br/>
<strong>Change size</strong> by clicking and dragging the icon at the bottom right of the chart box
</div>
<p>
<button type="button" class="close">
<span v-on:click="isHelpActive = !isHelpActive">&times;</span>
</button>
</div>
</div>
</transition>
<component v-for="(chartInst, index) in wrappers" is="chart-display" :key="chartInst.id"
:id="chartInst.id" :siblings="siblings" v-on:delete-chart="deleteChart">
</component>
</div>
`;

export let chart_display_template = `
<div class="mt-3">
<div class="card">

<div class="card-header">
<button type="button" class="close ml-2">
<span v-on:click="emitDeleteChart(id)">&times;</span>
</button>
<button type="button" class="close ml-2" v-on:click="isCollapsed = !isCollapsed">
<span v-if="!isCollapsed">&minus;</span>
<span v-if="isCollapsed">&#9744;</span>
</button>
<span class="card-subtitle text-muted">{{ selected }} </span>
</div>

<div class="card-body" v-bind:class="{'collapse': isCollapsed}">

<div class="row">
<div class="col-md-4">
<v-select placeholder="Select a Channel" id="channelList" label="option" style="flex: 1 1 auto;"
:clearable="false" :searchable="true" :filterable="true" :options="channelNames"
v-model="selected">
</v-select>
</div>
</div>

<div class="row justify-content-between">
<div class="col-md-4 mt-2">
<button type="button" class="btn" v-bind:class="{'btn-warning': !pause, 'btn-success': pause}"
v-on:click="toggleStreamFlow()" v-if="chart != null">
<span v-if="!pause">&#10074;&#10074;</span>
<span v-if="pause">&#9654;</span>
</button>

<button type="button" class="btn btn-warning" v-on:click="resetZoom()" v-if="chart != null">
Reset Zoom
</button>
</div>
</div>

<div class="row">
<div class="col-md-12 mt-2 fp-resize-box">
<canvas id="ds-line-chart" style="min-width: 50%"></canvas>
</div>
</div>

</div>
</div>
</div>
`;
172 changes: 172 additions & 0 deletions src/fprime_gds/flask/static/addons/chart-display/addon.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
/**
* addons/chart-display.js:
*
* Visualize selected telemetry channels using time series charts. This is done in realtime. Time-shifted signals
* will need to be panned into focus.
*
* @author saba-ja
*/
import {generate_chart_config} from "./config.js";
import {chart_wrapper_template, chart_display_template} from "./addon-templates.js";
import { _datastore } from '../../js/datastore.js';
import {_loader} from "../../js/loader.js";
import {SiblingSet} from './sibling.js';
import {timeToDate} from "../../js/vue-support/utils.js"

import './vendor/chart.js';
import './vendor/chartjs-adapter-luxon.min.js';
import './vendor/hammer.min.js';
// Note: these are modified versions of the original plugin files
import './modified-vendor/chartjs-plugin-zoom.js';
import './modified-vendor/chartjs-plugin-streaming.js';

/**
* Wrapper component to allow user add multiple charts to the same page. This component handles the functions for
* selecting the chart channel before the chart is created.
*/
Vue.component("chart-wrapper", {
data: function () {
return {
counter: 1,
locked: false,
isHelpActive: true,
wrappers: [{"id": 0}], // Starts with a single chart
siblings: new SiblingSet()
};
},
template: chart_wrapper_template,
methods: {
/**
* Add new chart handling the Chart+ button.
*/
addChart(type) {
this.wrappers.push({'id': this.counter});
this.counter += 1;
},
/**
* Remove chart with the given id for handling the X button on a chart wrapper
*/
deleteChart(id) {
const index = this.wrappers.findIndex(f => f.id === id);
this.wrappers.splice(index,1);
},
}
});

/**
* Main chart component. This displays the chart JS object and routes data too it.
*/
Vue.component("chart-display", {
template: chart_display_template,
props: ["id", "siblings"],
data: function () {
let names = Object.values(_loader.endpoints["channel-dict"].data).map((value) => {return value.full_name});
return {
channelNames: names,
selected: null,
oldSelected: null,

isCollapsed: false,
pause: false,

chart: null,
};
},
methods: {
/**
* Allow user to pause the chart stream
*/
toggleStreamFlow() {
const realtimeOpts = this.chart.options.scales.x.realtime;
realtimeOpts.pause = !realtimeOpts.pause;
this.pause = realtimeOpts.pause;
this.siblings.pause(realtimeOpts.pause);
},
/**
* Register a new chart object
*/
registerChart() {
// If there is a chart object destroy it to reset the chart
this.destroy();
_datastore.registerChannelConsumer(this);
let config = generate_chart_config(this.selected);
config.options.plugins.zoom.zoom.onZoom = this.siblings.syncToAll;
config.options.plugins.zoom.pan.onPan = this.siblings.syncToAll;
// Category IV magic: do not alter
config.options.scales.x.realtime.onRefresh = this.siblings.sync;
this.showControlBtns = true;
try {
this.chart = new Chart(this.$el.querySelector("#ds-line-chart"), config);
} catch(err) {
// Todo. This currently suppresses the following bug error
// See ChartJs bug report https://github.com/chartjs/Chart.js/issues/9368
}
this.siblings.add(this.chart);
},
/**
* Reset chart zoom back to default. This should affect all siblings when timescales are locked.
*/
resetZoom() {
this.chart.resetZoom("none");
this.siblings.reset();
},
/**
* Destroy a chart object.
*/
destroy() {
// Guard against destroying that which is destroyed
if (this.chart == null) {
return;
}
_datastore.deregisterChannelConsumer(this);
this.chart.data.datasets.forEach((dataset) => {dataset.data = [];});
this.chart.destroy();
this.siblings.remove(this.chart);
this.chart = null;
},

/**
* sending message up to the parent to remove this chart with this id
* @param {int} id of current chart instance known to the parent
*/
emitDeleteChart(id) {
this.destroy();
this.$emit('delete-chart', id);
},
/**
* Callback to handle new channels being pushed at this object.
* @param channels: new set of channels (unfiltered)
*/
sendChannels(channels) {
if (this.selected == null || this.chart == null) {
return;
}
let name = this.selected;
// Filter channels down to the graphed channel
let new_channels = channels.filter((channel) => {
return channel.template.full_name === name
});
// Convert to chart JS format
new_channels = new_channels.map(
(channel) => {
return {x: timeToDate(channel.time), y: channel.val}
}
);

// Graph and update
this.chart.data.datasets[0].data.push(...new_channels);
this.chart.update('quiet');
}
},
/**
* Watch for new selection of channel and re-register the chart
*/
watch: {
selected: function() {
if (this.selected !== this.oldSelected) {
this.oldSelected = this.selected;
this.registerChart();
}
},
}
});
Loading