"use strict";
var connection = null;
var WebSocket = WebSocket || MozWebSocket;
// Implement bootstrap 3 style button loading support
(function($) {
$.fn.button = function(action) {
if (action === 'loading' && this.data('loading-text')) {
this.data('original-text', this.html()).html(this.data('loading-text')).prop('disabled', true);
feather.replace();
}
if (action === 'reset' && this.data('original-text')) {
this.html(this.data('original-text')).prop('disabled', false);
feather.replace();
}
};
}(jQuery));
// HTML escaping
var entityMap = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'/': '/',
'`': '`',
'=': '='
};
function escapeHtml(string) {
return String(string).replace(/[&<>"'`=\/]/g, function (s) {
return entityMap[s];
});
}
function displayStatus(message) {
$('#status-content').html('
' + escapeHtml(message) + '
');
}
// Enable and disable buttons based on connection status
var connectedButtonIds = ['systemRestart', 'networkApproach', 'networkAddress', 'networkMask', 'networkGateway', 'networkDNS', 'visionUp', 'visionDown', 'visionTerm', 'visionKill', 'systemReadOnly', 'systemWritable', 'visionClient', 'visionTeam', 'visionDiscard', 'addCamera', 'applicationType'];
var connectedButtonClasses = ['cameraName', 'cameraPath', 'cameraPixelFormat', 'cameraWidth', 'cameraHeight', 'cameraFps', 'cameraBrightness', 'cameraWhiteBalance', 'cameraExposure', 'cameraProperties', 'cameraRemove']
var writableButtonIds = ['networkSave', 'visionSave', 'applicationSave'];
var systemStatusIds = ['systemMemoryFree1s', 'systemMemoryFree5s',
'systemMemoryAvail1s', 'systemMemoryAvail5s',
'systemCpuUser1s', 'systemCpuUser5s',
'systemCpuSystem1s', 'systemCpuSystem5s',
'systemCpuIdle1s', 'systemCpuIdle5s',
'systemNetwork1s', 'systemNetwork5s'];
function displayDisconnected() {
displayReadOnly();
$('#connectionBadge').removeClass('badge-primary').addClass('badge-secondary').text('Disconnected');
$('#visionServiceStatus').removeClass('badge-primary').removeClass('badge-secondary').addClass('badge-dark').text('Unknown Status');
for (var i = 0; i < connectedButtonIds.length; i++) {
$('#' + connectedButtonIds[i]).prop('disabled', true);
}
for (var i = 0; i < connectedButtonClasses.length; i++) {
$('.' + connectedButtonClasses[i]).prop('disabled', true);
}
for (var i = 0; i < systemStatusIds.length; i++) {
$('#' + systemStatusIds[i]).text("");
}
}
function displayConnected() {
$('#connectionBadge').removeClass('badge-secondary').addClass('badge-primary').text('Connected');
for (var i = 0; i < connectedButtonIds.length; i++) {
$('#' + connectedButtonIds[i]).prop('disabled', false);
}
for (var i = 0; i < connectedButtonClasses.length; i++) {
$('.' + connectedButtonClasses[i]).prop('disabled', false);
}
}
// Enable and disable buttons based on writable status
function displayReadOnly() {
for (var i = 0; i < writableButtonIds.length; i++) {
$('#' + writableButtonIds[i]).prop('disabled', true);
}
$('#systemReadOnly').addClass('active').prop('aria-pressed', true);
$('#systemWritable').removeClass('active').prop('aria-pressed', false);
}
function displayWritable() {
for (var i = 0; i < writableButtonIds.length; i++) {
$('#' + writableButtonIds[i]).prop('disabled', false);
}
$('#systemReadOnly').removeClass('active').prop('aria-pressed', false);
$('#systemWritable').addClass('active').prop('aria-pressed', true);
}
// Handle Read-Only and Writable buttons
$('#systemReadOnly').click(function() {
var $this = $(this);
if ($this.hasClass('active')) return;
var msg = {
type: 'systemReadOnly'
};
connection.send(JSON.stringify(msg));
});
$('#systemWritable').click(function() {
var $this = $(this);
if ($this.hasClass('active')) return;
var msg = {
type: 'systemWritable'
};
connection.send(JSON.stringify(msg));
});
// Vision settings
var visionSettingsServer = {};
var visionSettingsDisplay = {'cameras': []};
function pushVisionLogEnabled() {
var msg = {
type: 'visionLogEnabled',
value: visionLogEnabled.prop('checked')
};
connection.send(JSON.stringify(msg));
}
// WebSocket automatic reconnection timer
var reconnectTimerId = 0;
// Establish WebSocket connection
function connect() {
if (connection && connection.readyState !== WebSocket.CLOSED) return;
var serverUrl = "ws://" + window.location.hostname;
if (window.location.port !== '') {
serverUrl += ':' + window.location.port;
}
connection = new WebSocket(serverUrl, 'frcvision');
connection.onopen = function(evt) {
if (reconnectTimerId) {
window.clearInterval(reconnectTimerId);
reconnectTimerId = 0;
}
displayConnected();
pushVisionLogEnabled();
};
connection.onclose = function(evt) {
displayDisconnected();
if (!reconnectTimerId) {
reconnectTimerId = setInterval(function() { connect(); }, 2000);
}
};
// WebSocket incoming message handling
connection.onmessage = function(evt) {
var msg = JSON.parse(evt.data);
if (msg === null) {
return;
}
switch (msg.type) {
case 'systemStatus':
for (var i = 0; i < systemStatusIds.length; i++) {
$('#' + systemStatusIds[i]).text(msg[systemStatusIds[i]]);
}
break;
case 'visionStatus':
var elem = $('#visionServiceStatus');
if (msg.visionServiceStatus) {
elem.text(msg.visionServiceStatus);
}
if (msg.visionServiceEnabled && !elem.hasClass('badge-primary')) {
elem.removeClass('badge-dark').removeClass('badge-secondary').addClass('badge-primary');
} else if (!msg.visionServiceEnabled && !elem.hasClass('badge-secondary')) {
elem.removeClass('badge-dark').removeClass('badge-primary').addClass('badge-secondary');
}
break;
case 'visionLog':
visionLog(msg.data);
break;
case 'networkSettings':
$('#networkApproach').val(msg.networkApproach);
$('#networkAddress').val(msg.networkAddress);
$('#networkMask').val(msg.networkMask);
$('#networkGateway').val(msg.networkGateway);
$('#networkDNS').val(msg.networkDNS);
updateNetworkSettingsView();
break;
case 'visionSettings':
visionSettingsServer = msg.settings;
visionSettingsDisplay = $.extend(true, {}, visionSettingsServer);
updateVisionSettingsView();
break;
case 'applicationSettings':
$('#applicationType').val(msg.applicationType);
updateApplicationView();
break;
case 'applicationSaveComplete':
$('#applicationSave').button('reset');
updateApplicationView();
break;
case 'systemReadOnly':
displayReadOnly();
break;
case 'systemWritable':
displayWritable();
break;
case 'status':
displayStatus(msg.message);
break;
}
};
}
// Button handlers
$('#systemRestart').click(function() {
var msg = {
type: 'systemRestart'
};
connection.send(JSON.stringify(msg));
});
$('#visionUp').click(function() {
var msg = {
type: 'visionUp'
};
connection.send(JSON.stringify(msg));
});
$('#visionDown').click(function() {
var msg = {
type: 'visionDown'
};
connection.send(JSON.stringify(msg));
});
$('#visionTerm').click(function() {
var msg = {
type: 'visionTerm'
};
connection.send(JSON.stringify(msg));
});
$('#visionKill').click(function() {
var msg = {
type: 'visionKill'
};
connection.send(JSON.stringify(msg));
});
$('#visionLogEnabled').change(function() {
pushVisionLogEnabled();
});
//
// Vision console output
//
var visionConsole = document.getElementById('visionConsole');
var visionLogEnabled = $('#visionLogEnabled');
var _linesLimit = 100;
/*
function escape_for_html(txt) {
return txt.replace(/[&<>]/gm, function(str) {
if (str == "&") return "&";
if (str == "<") return "<";
if (str == ">") return ">";
});
}
*/
function visionLog(data) {
if (!visionLogEnabled.prop('checked')) {
return;
}
var wasScrolledBottom = (visionConsole.scrollTop === (visionConsole.scrollHeight - visionConsole.offsetHeight));
var div = document.createElement('div');
var p = document.createElement('p');
p.className = 'inner-line';
// escape HTML tags
data = escapeHtml(data);
p.innerHTML = data;
div.className = 'line';
div.addEventListener('click', function click() {
if (this.className.indexOf('selected') === -1) {
this.className = 'line-selected';
} else {
this.className = 'line';
}
});
div.appendChild(p);
visionConsole.appendChild(div);
if (visionConsole.children.length > _linesLimit) {
visionConsole.removeChild(visionConsole.children[0]);
}
if (wasScrolledBottom) {
visionConsole.scrollTop = visionConsole.scrollHeight;
}
}
// Show details when appropriate for network approach
function updateNetworkSettingsView() {
if ($('#networkApproach').val() === "dhcp") {
$('#networkIpDetails').collapse('hide');
} else {
$('#networkIpDetails').collapse('show');
}
}
$('#networkApproach').change(function() {
updateNetworkSettingsView();
});
// Network Save button handler
$('#networkSave').click(function() {
var msg = {
type: 'networkSave',
networkApproach: $('#networkApproach').val(),
networkAddress: $('#networkAddress').val(),
networkMask: $('#networkMask').val(),
networkGateway: $('#networkGateway').val(),
networkDNS: $('#networkDNS').val()
};
connection.send(JSON.stringify(msg));
});
// Show details when appropriate for NT client
$('#visionClient').change(function() {
if (this.checked) {
$('#visionClientDetails').collapse('show');
} else {
$('#visionClientDetails').collapse('hide');
}
});
function updateVisionCameraView(camera, value) {
if ('name' in value) {
camera.find('.cameraTitle').text('Camera ' + value.name);
camera.find('.cameraName').val(value.name);
}
if ('path' in value) {
camera.find('.cameraPath').val(value.path);
}
camera.find('.cameraPixelFormat').val(value['pixel format']);
camera.find('.cameraWidth').val(value.width);
camera.find('.cameraHeight').val(value.height);
camera.find('.cameraFps').val(value.fps);
camera.find('.cameraBrightness').val(value.brightness);
camera.find('.cameraWhiteBalance').val(value['white balance']);
camera.find('.cameraExposure').val(value.exposure);
camera.find('.cameraProperties').val(JSON.stringify(value.properties));
}
function appendNewVisionCameraView(value, i) {
var camera = $('#cameraNEW').clone();
camera.attr('id', 'camera' + i);
camera.addClass('cameraSetting');
camera.removeAttr('style');
updateVisionCameraView(camera, value);
camera.find('.cameraStream').attr('href', 'http://' + window.location.hostname + ':' + (1181 + i) + '/');
camera.find('.cameraRemove').click(function() {
visionSettingsDisplay.cameras.splice(i, 1);
camera.remove();
});
camera.find('.cameraSettingsFile').change(function() {
if (this.files.length <= 0) {
return false;
}
var fr = new FileReader();
fr.onload = function(e) {
var result = JSON.parse(e.target.result);
if (!('name' in result)) {
result.name = visionSettingsDisplay.cameras[i].name;
}
if (!('path' in result)) {
result.path = visionSettingsDisplay.cameras[i].path;
}
visionSettingsDisplay.cameras[i] = result;
updateVisionCameraView(camera, result);
};
fr.readAsText(this.files.item(0));
});
camera.find('[id]').each(function() {
$(this).attr('id', $(this).attr('id').replace('NEW', i));
});
camera.find('[for]').each(function() {
$(this).attr('for', $(this).attr('for').replace('NEW', i));
});
camera.find('[data-target]').each(function() {
$(this).attr('data-target', $(this).attr('data-target').replace('NEW', i));
});
$('#cameras').append(camera);
}
function updateVisionSettingsView() {
var isClient = !visionSettingsDisplay.ntmode || visionSettingsDisplay.ntmode === 'client';
$('#visionClient').prop('checked', isClient);
if (isClient) {
$('#visionClientDetails').collapse('show');
} else {
$('#visionClientDetails').collapse('hide');
}
$('#visionTeam').val(visionSettingsDisplay.team);
$('.cameraSetting').remove();
visionSettingsDisplay.cameras.forEach(function (value, i) {
appendNewVisionCameraView(value, i);
});
feather.replace();
}
$('#visionSave').click(function() {
// update json from view
visionSettingsDisplay.ntmode = $('#visionClient').prop('checked') ? 'client' : 'server';
visionSettingsDisplay.team = parseInt($('#visionTeam').val(), 10);
visionSettingsDisplay.cameras.forEach(function (value, i) {
var camera = $('#camera' + i);
value.name = camera.find('.cameraName').val();
value.path = camera.find('.cameraPath').val();
value['pixel format'] = camera.find('.cameraPixelFormat').val();
value.width = parseInt(camera.find('.cameraWidth').val(), 10);
if (isNaN(value.width)) {
delete value["width"];
}
value.height = parseInt(camera.find('.cameraHeight').val(), 10);
if (isNaN(value.height)) {
delete value["height"];
}
value.fps = parseInt(camera.find('.cameraFps').val(), 10);
if (isNaN(value.fps)) {
delete value["fps"];
}
var brightness = camera.find('.cameraBrightness').val();
if (brightness !== '') {
value.brightness = parseInt(brightness);
if (isNaN(value.brightness)) {
value.brightness = brightness;
}
} else {
delete value['brightness'];
}
var whiteBalance = camera.find('.cameraWhiteBalance').val();
if (whiteBalance !== '') {
value['white balance'] = parseInt(whiteBalance);
if (isNaN(value['white balance'])) {
value['white balance'] = whiteBalance;
}
} else {
delete value['white balance'];
}
var exposure = camera.find('.cameraExposure').val();
if (exposure !== '') {
value.exposure = parseInt(exposure);
if (isNaN(value.exposure)) {
value.exposure = exposure;
}
} else {
delete value['exposure'];
}
try {
value.properties = JSON.parse(camera.find('.cameraProperties').val());
} catch (err) {
delete value['properties'];
}
});
var msg = {
type: 'visionSave',
settings: visionSettingsDisplay
};
connection.send(JSON.stringify(msg));
});
$('#visionDiscard').click(function() {
visionSettingsDisplay = $.extend(true, {}, visionSettingsServer);
updateVisionSettingsView();
});
$('#addCamera').click(function() {
var i = visionSettingsDisplay.cameras.length;
visionSettingsDisplay.cameras.push({});
appendNewVisionCameraView({}, i);
});
var applicationFiles = [];
// Show details when appropriate for application type
function updateApplicationView() {
if ($('#applicationType').val().startsWith("upload")) {
$('#applicationUpload').collapse('show');
} else {
$('#applicationUpload').collapse('hide');
}
$('#applicationFile').val(null);
applicationFiles = [];
}
$('#applicationType').change(function() {
updateApplicationView();
});
$('#applicationFile').change(function() {
applicationFiles = this.files;
});
$('#applicationSave').click(function() {
var msg = {
type: 'applicationSave',
applicationType: $('#applicationType').val()
};
connection.send(JSON.stringify(msg));
// upload the file if requested
if (applicationFiles.length <= 0) {
return;
}
$('#applicationSave').button('loading');
var fr = new FileReader();
fr.onload = function(e) {
connection.send(e.target.result);
};
fr.readAsArrayBuffer(applicationFiles.item(0));
});
// Start with display disconnected and start initial connection attempt
displayDisconnected();
updateNetworkSettingsView();
updateVisionSettingsView();
updateApplicationView();
connect();