// Files containing GPIO and PWM definitions
#include "bbb_pin.h"
-
-
-
/** Number of actuators **/
int g_num_actuators = 0;
{
case CONTROL_START:
{
+ // Set filename
char filename[BUFSIZ];
- const char *experiment_name = (const char*) arg;
+ const char *experiment_path = (const char*) arg;
+ int ret;
+
+ ret = snprintf(filename, BUFSIZ, "%s/actuator_%d", experiment_path, a->id);
- if (snprintf(filename, BUFSIZ, "%s_a%d", experiment_name, a->id) >= BUFSIZ)
+ if (ret >= BUFSIZ)
{
- Fatal("Experiment name \"%s\" too long (>%d)", experiment_name, BUFSIZ);
+ Fatal("Experiment path \"%s\" too long (%d, limit %d)",
+ experiment_path, ret, BUFSIZ);
}
Log(LOGDEBUG, "Actuator %d with DataFile \"%s\"", a->id, filename);
*/
void Actuator_SetModeAll(ControlModes mode, void * arg)
{
- for (int i = 0; i < ACTUATORS_MAX; i++)
+ for (int i = 0; i < g_num_actuators; i++)
Actuator_SetMode(&g_actuators[i], mode, arg);
}
}
extern bool PathExists(const char * path);
+extern bool DirExists(const char * path);
#include "sensor.h"
#include "actuator.h"
#include <dirent.h>
+#include <sys/types.h>
+#include <sys/stat.h>
typedef struct ControlData {
ControlModes current_mode;
pthread_mutex_t mutex;
struct timespec start_time;
char user_name[31]; // The user who owns the currently running experiment
+ char experiment_dir[BUFSIZ]; //Directory for experiment
+ char experiment_name[BUFSIZ];
} ControlData;
ControlData g_controls = {CONTROL_STOP, PTHREAD_MUTEX_INITIALIZER, {0}};
+bool DirExists(const char *path)
+{
+ DIR *dir = opendir(path);
+ if (dir) {
+ closedir(dir);
+ return true;
+ }
+ return false;
+}
+
bool PathExists(const char *path)
{
FILE *fp = fopen(path, "r");
return false;
}
+/**
+ * Lists all experiments for the current user.
+ * @param The context to work in
+ */
+void ListExperiments(FCGIContext *context)
+{
+ DIR * dir = opendir(context->user_dir);
+ if (dir == NULL)
+ {
+ FCGI_RejectJSON(context, "Failed to open user directory");
+ return;
+ }
+ struct dirent * ent;
+ FCGI_BeginJSON(context, STATUS_OK);
+ FCGI_JSONKey("experiments");
+ FCGI_PrintRaw("[");
+
+ bool first = true;
+ while ((ent = readdir(dir)) != NULL) {
+ char *ext = strrchr(ent->d_name, '.');
+ if (ext && !strcmp(ext, ".exp")) {
+ if (!first) {
+ FCGI_PrintRaw(", ");
+ }
+
+ *ext = '\0'; // Ummm... probably not a great idea
+ FCGI_PrintRaw("\"%s\"", ent->d_name);
+ first = false;
+ }
+ }
+ FCGI_PrintRaw("]");
+ FCGI_EndJSON();
+
+ closedir(dir);
+ return;
+}
+
/**
* System control handler. This covers high-level control, including
* admin related functions and starting/stopping experiments..
bool force = false;
ControlModes desired_mode;
-
-
-
-
// Login/auth now handled entirely in fastcgi.c and login.c
//TODO: Need to not have the ability for any user to stop someone else' experiment...
// (achieve by storing the username of the person running the current experiment, even when they log out?)
{"name", &name, FCGI_STRING_T}
};
-
if (!FCGI_ParseRequest(context, params, values, 3))
return;
- //HACKETY HACK HACK (should really be a seperate function)
- if (strcmp(action, "list") == 0)
- {
- DIR * dir = opendir(context->user_name);
- if (dir == NULL)
- {
- FCGI_RejectJSON(context, "Failed to open user directory");
- return;
- }
- struct dirent * ent;
+ if (!strcmp(action, "identify")) {
FCGI_BeginJSON(context, STATUS_OK);
- FCGI_JSONKey("experiments");
- FCGI_PrintRaw("[");
-
- bool first = true;
- while ((ent = readdir(dir)) != NULL)
- {
- char * c;
- for (c = ent->d_name; *c != '\0' && *c != '.'; ++c);
-
- if (*c != '\0' && strcmp(c, ".exp") == 0)
- {
- if (!first)
- FCGI_PrintRaw(",");
- *c = '\0'; // Ummm... probably not a great idea
- FCGI_PrintRaw(ent->d_name);
- first = false;
- }
- }
- FCGI_PrintRaw("]");
+ FCGI_JSONLong("control_state_id", g_controls.current_mode);
+ FCGI_JSONPair("control_user_name", g_controls.user_name);
+ FCGI_JSONPair("control_experiment_name", g_controls.experiment_name);
FCGI_EndJSON();
-
- return; // Dear god this is terrible
+ return;
+ } else if (!strcmp(action, "list")) {
+ ListExperiments(context);
+ return;
}
//TODO: Need a "load" action to set data files (but not run) from a past experiment
return;
}
- if (*g_controls.user_name != '\0' && strcmp(g_controls.user_name,context->user_name) != 0)
+ if ((*g_controls.user_name) != '\0' && strcmp(g_controls.user_name, context->user_name) != 0)
{
- if (context->user_type != USER_ADMIN)
- {
+ if (context->user_type != USER_ADMIN) {
FCGI_RejectJSON(context, "Another user has an experiment in progress.");
return;
}
- if (!force)
- {
+ if (!force) {
Log(LOGERR, "User %s is currently running an experiment!", g_controls.user_name);
FCGI_RejectJSON(context, "Pass \"force\" to take control over another user's experiment");
return;
}
}
-
-
- //HACK
- chdir(context->user_name);
-
void *arg = NULL;
+ char experiment_dir[BUFSIZ] = {0};
if (desired_mode == CONTROL_START) {
- if (PathExists(name) && !force) {
- FCGI_RejectJSON(context, "An experiment with that name already exists.");
- chdir(g_options.root_dir); // REVERSE HACK
+ int ret;
+
+ if (*name == '\0') {
+ FCGI_RejectJSON(context, "An experiment name must be specified.");
+ return;
+ } if (strpbrk(name, INVALID_CHARACTERS)) {
+ FCGI_RejectJSON(context,
+ "An experiment name must not contain: " INVALID_CHARACTERS_JSON);
return;
}
- char * c = (char*)name;
- for (c = (char*)name; *c != '\0' && *c != '.'; ++c);
- if (*c == '.')
- {
- FCGI_RejectJSON(context, "Can't include '.' characters in experiment names (at this point we don't care anymore, go find someone who does).");
- chdir(g_options.root_dir); // REVERSE HACK
+
+ if (*(context->user_dir) == '\0') {
+ FCGI_RejectJSON(context, "Not creating experiment in root dir.");
return;
}
- arg = (void*)name;
- }
-
+ ret = snprintf(experiment_dir, BUFSIZ, "%s/%s.exp",
+ context->user_dir, name);
+ if (ret >= BUFSIZ) {
+ FCGI_RejectJSON(context, "The experiment name is too long.");
+ return;
+ } else if (DirExists(experiment_dir) && !force) {
+ FCGI_RejectJSON(context, "An experiment with that name already exists.");
+ return;
+ }
+
+ arg = (void*) experiment_dir;
+ }
const char *ret;
- if ((ret = Control_SetMode(desired_mode, arg)) != NULL)
- {
+ if ((ret = Control_SetMode(desired_mode, arg)) != NULL) {
FCGI_RejectJSON(context, ret);
- }
- else
- {
- if (desired_mode == CONTROL_STOP)
+ } else {
+ if (desired_mode == CONTROL_STOP) {
g_controls.user_name[0] = '\0';
- else
- {
- snprintf(g_controls.user_name, sizeof(g_controls.user_name), context->user_name);
+ g_controls.experiment_dir[0] = '\0';
+ g_controls.experiment_name[0] = '\0';
+ } else if (desired_mode == CONTROL_START) {
+ snprintf(g_controls.user_name, sizeof(g_controls.user_name),
+ "%s", context->user_name);
+ snprintf(g_controls.experiment_dir, sizeof(g_controls.experiment_dir),
+ "%s", experiment_dir);
+ snprintf(g_controls.experiment_name, sizeof(g_controls.experiment_name),
+ "%s", name);
}
- FCGI_BeginJSON(context, STATUS_OK);
- FCGI_JSONPair("description", "ok");
- FCGI_EndJSON();
- }
- //REVERSE HACK
- chdir(g_options.root_dir);
+ FCGI_AcceptJSON(context, "Ok", NULL);
+ }
}
/**
else switch (desired_mode) {
case CONTROL_START:
if (g_controls.current_mode == CONTROL_STOP) {
- const char * name = arg;
- if (!*name)
- ret = "An experiment name must be specified";
- else if (strpbrk(name, INVALID_CHARACTERS))
- ret = "The experiment name must not contain: " INVALID_CHARACTERS_JSON;
- else {
- FILE *fp = fopen((const char*) arg, "a");
- if (fp) {
- fclose(fp);
- clock_gettime(CLOCK_MONOTONIC, &(g_controls.start_time));
- } else
- ret = "Cannot open experiment name marker";
+ const char * path = arg;
+ if (mkdir(path, 0777) != 0 && errno != EEXIST) {
+ Log(LOGERR, "Couldn't create experiment directory %s - %s",
+ path, strerror(errno));
+ ret = "Couldn't create experiment directory.";
+ } else {
+ clock_gettime(CLOCK_MONOTONIC, &(g_controls.start_time));
}
} else
ret = "Cannot start when not in a stopped state.";
} ControlModes;
/** Invalid filename characters **/
-#define INVALID_CHARACTERS "\"*/:<>?\\|"
+#define INVALID_CHARACTERS "\"*/:<>?\\|. "
/** The same as INVALID_CHARACTERS, except escaped for use in JSON strings **/
-#define INVALID_CHARACTERS_JSON "\\\"*/:<>?\\\\|"
+#define INVALID_CHARACTERS_JSON "\\\"*/:<>?\\\\|. "
extern void Control_Handler(FCGIContext *context, char *params);
extern const char* Control_SetMode(ControlModes desired_mode, void * arg);
*/
static void IdentifyHandler(FCGIContext *context, char *params) {
bool ident_sensors = false, ident_actuators = false;
- bool has_control = FCGI_HasControl(context, getenv("COOKIE_STRING"));
+ char control_key[CONTROL_KEY_BUFSIZ];
+ bool has_control;
int i;
+ snprintf(control_key, CONTROL_KEY_BUFSIZ, "%s", getenv("COOKIE_STRING"));
+ has_control = FCGI_HasControl(context, control_key);
+
FCGIValue values[2] = {{"sensors", &ident_sensors, FCGI_BOOL_T},
{"actuators", &ident_actuators, FCGI_BOOL_T}};
if (!FCGI_ParseRequest(context, params, values, 2))
// Get current time
time_t now = time(NULL);
bool expired = now - context->control_timestamp > CONTROL_TIMEOUT;
+ int i;
// Can't lock control if: User not actually logged in (sanity), or key is still valid and the user is not an admin
if (user_type == USER_UNAUTH ||
// Generate a SHA1 hash for the user
SHA_CTX sha1ctx;
unsigned char sha1[20];
- int i = rand();
+ i = rand();
SHA1_Init(&sha1ctx);
SHA1_Update(&sha1ctx, &now, sizeof(now));
SHA1_Update(&sha1ctx, &i, sizeof(i));
for (i = 0; i < sizeof(sha1); i++)
sprintf(context->control_key + i * 2, "%02x", sha1[i]);
- // Set the IP address
+ // Set the IPv4 address
snprintf(context->control_ip, 16, "%s", getenv("REMOTE_ADDR"));
+
// Set the user name
int uname_len = strlen(user_name);
- if (snprintf(context->user_name, sizeof(context->user_name), "%s", user_name) < uname_len)
- {
- Log(LOGERR, "Username at %d characters too long (limit %d)", uname_len, sizeof(context->user_name));
+ i = snprintf(context->user_name, sizeof(context->user_name), "%s", user_name);
+ if (i < uname_len) {
+ Log(LOGERR, "Username at %d characters too long (limit %d)",
+ uname_len, sizeof(context->user_name));
return false; // :-(
}
// Set the user type
context->user_type = user_type;
+
+ // Build the user directory
+ i = snprintf(context->user_dir, sizeof(context->user_dir), "%s/%s",
+ g_options.experiment_dir, context->user_name);
+ if (i >= sizeof(context->user_dir)) {
+ Log(LOGERR, "Experiment dir too long (required %d, limit %d)",
+ i, sizeof(context->user_dir));
+ return false;
+ }
+
+ Log(LOGDEBUG, "User dir: %s", context->user_dir);
// Create directory
- if (mkdir(user_name, 0777) != 0 && errno != EEXIST)
+ if (mkdir(context->user_dir, 0777) != 0 && errno != EEXIST)
{
- Log(LOGERR, "Couldn't create user directory %s/%s - %s", g_options.root_dir, user_name, strerror(errno));
+ Log(LOGERR, "Couldn't create user directory %s - %s",
+ context->user_dir, strerror(errno));
return false; // :-(
}
-
return true; // :-)
}
while (FCGI_Accept() >= 0) {
ModuleHandler module_handler = NULL;
- char module[BUFSIZ], params[BUFSIZ];
- //Don't need to copy if we're not modifying string contents
- const char *cookie = getenv("COOKIE_STRING");
+ char module[BUFSIZ], params[BUFSIZ], control_key[CONTROL_KEY_BUFSIZ];
//strncpy doesn't zero-truncate properly
snprintf(module, BUFSIZ, "%s", getenv("DOCUMENT_URI_LOCAL"));
snprintf(params, BUFSIZ, "%s", getenv("QUERY_STRING"));
+ //Hack to get the nameless cookie only
+ snprintf(control_key, CONTROL_KEY_BUFSIZ, "%s", getenv("COOKIE_STRING"));
+
Log(LOGDEBUG, "Got request #%d - Module %s, params %s", context.response_number, module, params);
- Log(LOGDEBUG, "Cookie: %s", cookie);
+ Log(LOGDEBUG, "Control key: %s", control_key);
//Remove trailing slashes (if present) from module query
//if (module_handler != Login_Handler && module_handler != IdentifyHandler && module_handler)
if (false) // Testing
{
- if (!FCGI_HasControl(&context, cookie))
+ if (!FCGI_HasControl(&context, control_key))
{
FCGI_RejectJSON(&context, "Please login. Invalid control key.");
continue;
#define FCGI_RECEIVED(x) ((x) & FCGI_PARAM_RECEIVED)
#define FCGI_TYPE(x) ((x) & ~(FCGI_PARAM_REQUIRED | FCGI_PARAM_RECEIVED))
+#define CONTROL_KEY_BUFSIZ 41
+
typedef struct FCGIValue {
const char *key;
void *value;
/**The time of last valid user access possessing the control key**/
time_t control_timestamp;
/**A SHA-1 hash that is the control key, determining who is logged in**/
- char control_key[41];
+ char control_key[CONTROL_KEY_BUFSIZ];
/**The IPv4 address of the logged-in user**/
char control_ip[16];
/**Determines if the user is an admin or not**/
UserType user_type;
/**Name of the logged in user**/
char user_name[31];
+ /**User directory for the logged in user**/
+ char user_dir[BUFSIZ];
/**The name of the current module**/
const char *current_module;
/**For debugging purposes?**/
*/
void ParseArguments(int argc, char ** argv)
{
-
-
g_options.program = argv[0]; // program name
g_options.verbosity = LOGDEBUG; // default log level
// Set the main directory
- if (getcwd(g_options.root_dir, sizeof(g_options.root_dir)) == NULL)
- Fatal("Couldn't get current working directory - %s", strerror(errno));
+ //if (getcwd(g_options.root_dir, sizeof(g_options.root_dir)) == NULL)
+ // Fatal("Couldn't get current working directory - %s", strerror(errno));
clock_gettime(CLOCK_MONOTONIC, &(g_options.start_time)); // Start time
g_options.auth_method = AUTH_NONE; // Don't use authentication
g_options.auth_uri = ""; //
g_options.ldap_base_dn = "";
+ g_options.experiment_dir = ".";
-
-
for (int i = 1; i < argc; ++i)
{
if (argv[i][0] != '-')
case 'd':
g_options.ldap_base_dn = argv[++i];
break;
+ case 'e':
+ // Experiments directory
+ g_options.experiment_dir = argv[++i];
+ break;
default:
Fatal("Unrecognised switch %s", argv[i]);
break;
Log(LOGDEBUG, "Pin Module Enabled: %d", g_options.enable_pin);
Log(LOGDEBUG, "Auth URI: %s", g_options.auth_uri);
Log(LOGDEBUG, "LDAP Base DN: %s", g_options.ldap_base_dn);
- Log(LOGDEBUG, "Root directory: %s", g_options.root_dir);
+ //Log(LOGDEBUG, "Root directory: %s", g_options.root_dir);
+ Log(LOGDEBUG, "Experiment directory: %s", g_options.experiment_dir);
+
+ if (!DirExists(g_options.experiment_dir))
+ {
+ Fatal("Experiment directory '%s' does not exist.", g_options.experiment_dir);
+ }
if (g_options.auth_uri[0] != '\0')
{
/** Authentication method **/
enum {AUTH_NONE, AUTH_LDAP, AUTH_SHADOW} auth_method;
- /** Starting directory **/
- char root_dir[BUFSIZ];
-
-
-
+ /** Experiments directory **/
+ const char *experiment_dir;
} Options;
/** The only instance of the Options struct **/
{
// Set filename
char filename[BUFSIZ];
- const char *experiment_name = (const char*) arg;
+ const char *experiment_path = (const char*) arg;
+ int ret;
+
+ ret = snprintf(filename, BUFSIZ, "%s/sensor_%d", experiment_path, s->id);
- if (snprintf(filename, BUFSIZ, "%s_%d", experiment_name, s->id) >= BUFSIZ)
+ if (ret >= BUFSIZ)
{
- Fatal("Experiment name \"%s\" too long (>%d)", experiment_name, BUFSIZ);
+ Fatal("Experiment path \"%s\" too long (%d, limit %d)",
+ experiment_path, ret, BUFSIZ);
}
Log(LOGDEBUG, "Sensor %d with DataFile \"%s\"", s->id, filename);
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
double current_time = TIMEVAL_DIFF(now, *Control_GetStartTime());
-
int id = 0;
const char * name = "";
double start_time = 0;
<script type="text/javascript" src="static/jquery-1.10.1.min.js"></script>
<script type="text/javascript" src="static/jquery.flot.min.js"></script>
<script type="text/javascript" src="static/mctx.gui.js"></script>
+ <script type="text/javascript" src="static/mctx.control.js"></script>
<link rel="stylesheet" type="text/css" href="static/style.css">
<link rel="stylesheet" type="text/css" href="static/nav-menu.css">
<script type="text/javascript">
runBeforeLoad().done(function () {
$(document).ready(function () {
- $("#main_controls").submit(function () {
- //Validate!
- return false;
- });
+ $("form").submit(function () { //Prevent form submit globally
+ return false;
+ })
+
+ //Set the status updated
+ $("#state-exp").setStatusUpdater();
+
+ //Set the logic for the start controls
+ $("#start-controls").submit(function () {
+ var start = $("#start-controls input[type='button']");
+ var force = $("#start-controls input[name='start_force']");
+
+ $(this).startExperiment(start, $("#experiment_name").val(),
+ force.is(":checked"), $("#start-result"));
+ force.prop("checked", false);
+ });
+
+ //Set the logic for the stop button
+ $("#experiment-stop").click(function () {
+ $(this).stopExperiment($("#stop-status"));
+ });
+
+ //Set the logic for the pressure controls
+ $("#pressure-controls").submit(function () {
+ var pressure = {
+ set : $("#pressure-set").val(),
+ step : $("#pressure-stepsize").val(),
+ wait : $("#pressure-stepwait").val(),
+ count : $("#pressure-stepcount").val()
+ };
+ $(this).setPressure(pressure, $("#pressure-result"));
+ });
});
- })
-
+ }).fail(function () {
+ $(document).ready(function () {
+ $("#state-exp").text("Connection failure").parent().addClass("fail");
+ });
+ });
</script>
</head>
<div id="main">
<div class="widget">
- <div class="title">Experiment Status</div>
- <table class="horizontal">
- <tr>
+ <div class="title">Experiment Overview</div>
+ <table class="horizontal medium">
+ <tr id="state-exp-r">
<th>Experiment state</th>
- <td id="state-exp">No experiment running.</td>
- </tr>
- <tr>
- <th>Software status</th>
- <td id="state-software"></td>
+ <td id="state-exp"></td>
+ <td style="text-align: right;">
+ <input id="experiment-stop" type="button" value="Stop">
+ </td>
</tr>
</table>
- <div class="bold">Error and warning messages</div>
+ <div id="stop-status">
+
+ </div>
+
+ <div class="sub-title">Error and warning messages</div>
<textarea id="errorlog" wrap="off" rows="4" cols="30" readonly>
</textarea>
</div>
- <div class="widget">
- <div class="bold">Main controls</div>
- <form id="main-controls" action="">
+ <div id="start-widget" class="widget">
+ <div class="title centre">Start an experiment</div>
+ <form id="start-controls" class="nice" action="#">
+ <p>
+ <label for="experiment_name">Experiment name</label>
+ <input id="experiment_name" type="text">
+
+ <label for="start_force">Overwrite existing</label>
+ <input type="checkbox" name="start_force" id="start_force">
+ </p>
+ <p id="start-result">
+
+ </p>
+ <p class="centre">
+ <input type="submit" name="start_strain" value="Strain test">
+ <input type="submit" name="start_explode" value="Explode test">
+ </p>
+ </form>
+ </div>
+
+ <div id="pressure-widget" class="widget">
+ <form id="pressure-controls" action="#" class="nice clear">
<table>
<tr>
- <td>Experiment name</td>
- <td><input name="experiment_name" type="text"></td>
+ <td><label for="pressure-set">Starting pressure (kPa)</label></td>
+ <td><label for="pressure-stepsize">Pressure step size (kPa)</label></td>
+ </tr>
+ <tr>
+ <td><input id="pressure-set" type="text"></td>
+ <td><input id="pressure-stepsize" type="text"></td>
</tr>
<tr>
- <td>Experiment mode</td>
- <td>
- <input name="experiment_type" value="strain" type="radio"> Strain it
- <input name="experiment_type" value="explode" type="radio"> Explode it
- </td>
+ <td><label for="pressure-stepcount">Number of steps to make</label></td>
+ <td><label for="pressure-stepwait">Wait time between steps (s)</label></td>
</tr>
<tr>
- <td>
- </td>
- <td align="right">
- <input type="submit" value="Start">
- <input type="submit" value="Pause">
- <input type="submit" value="Stop">
- </td>
+ <td><input id="pressure-stepcount" type="text"></td>
+ <td><input id="pressure-stepwait" type="text"></td>
</tr>
</table>
+ <p class="left" id="pressure-result">
+
+ </p>
+ <p class="right">
+ <input type="submit" value="Set pressure">
+ </p>
</form>
-
- <div class="bold">Pressure controls</div>
- <form id="pressure-controls" action="">
+ </div>
+
+ <div id="stats-widget" class="widget">
+ <form id="stats" action="#" class="nice clear">
<table>
<tr>
- <td>Experiment name</td>
- <td><input name="experiment_name" type="text"></td>
+ <td><label for="stats-mainspressure">Mains pressure (kPa)</label></td>
+ <td><label for="stats-canpressure">Can pressure (kPa)</label></td>
</tr>
<tr>
- <td>Experiment mode</td>
- <td>
- <input name="experiment_type" value="strain" type="radio"> Strain it
- <input name="experiment_type" value="explode" type="radio"> Explode it
- </td>
+ <td><input id="stats-mainspressure" type="text" readonly></td>
+ <td><input id="stats-canpressure" type="text" readonly></td>
</tr>
- <tr>
- <td>
- </td>
- <td align="right">
- <input type="submit" value="Start">
- <input type="submit" value="Pause">
- <input type="submit" value="Stop">
- </td>
+ <tr class="stats-strain">
+ <td><label for="stats-strain1">Central hoop strain</label></td>
+ <td><label for="stats-strain2">Central longitudinal strain</label></td>
+ </tr>
+ <tr class="stats-strain">
+ <td><input id="stats-strain1" type="text" readonly></td>
+ <td><input id="stats-strain2" type="text" readonly></td>
+ </tr>
+ <tr class="stats-strain">
+ <td><label for="stats-strain3">End hoop strain</label></td>
+ <td><label for="stats-strain4">End longitudinal strain</label></td>
+ </tr>
+ <tr class="stats-strain">
+ <td><input id="stats-strain3" type="text" readonly></td>
+ <td><input id="stats-strain4" type="text" readonly></td>
+ </tr>
+ <tr class="stats-strain">
+ <td><label for="stats-dilatometer">Dilatometer reading</label></td>
+ <td><label for="stats-dilatometer">Camera feed</label></td>
+ </tr>
+ <tr class="stats-strain">
+ <td><input id="stats-dilatometer" type="text" readonly></td>
+ <td><a href="#">Link</a></td>
</tr>
</table>
</form>
</div>
+
</div>
<!-- End main content -->
</div>
<link rel="stylesheet" type="text/css" href="static/style.css">
<link rel="stylesheet" type="text/css" href="static/nav-menu.css">
<script type="text/javascript">
- runBeforeLoad().done(function() {
- $(document).ready(function() {
-
- }).always(function() {
+ runBeforeLoad().always(function() {
$(document).ready(function() {
$("#graph-controls").setDevices();
});
<input type="button" value="Open New Graph" disabled>
<input type="button" value="Save Graph Image" id="saveimage">
<input type="button" value="Dump Raw Data" disabled>
- <script>
+ <script type="text/javascript">
$("#saveimage").click(function() {
$("canvas").each(function() {
var image = new Image();
+++ /dev/null
-/*
- * Copyright (c) 2010 Nick Galbreath
- * http://code.google.com/p/stringencoders/source/browse/#svn/trunk/javascript
- *
- * Permission is hereby granted, free of charge, to any person
- * obtaining a copy of this software and associated documentation
- * files (the "Software"), to deal in the Software without
- * restriction, including without limitation the rights to use,
- * copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the
- * Software is furnished to do so, subject to the following
- * conditions:
- *
- * The above copyright notice and this permission notice shall be
- * included in all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
- * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
- * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
- * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
- * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
- * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
- * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
- * OTHER DEALINGS IN THE SOFTWARE.
- */
-
-/* base64 encode/decode compatible with window.btoa/atob
- *
- * window.atob/btoa is a Firefox extension to convert binary data (the "b")
- * to base64 (ascii, the "a").
- *
- * It is also found in Safari and Chrome. It is not available in IE.
- *
- * if (!window.btoa) window.btoa = base64.encode
- * if (!window.atob) window.atob = base64.decode
- *
- * The original spec's for atob/btoa are a bit lacking
- * https://developer.mozilla.org/en/DOM/window.atob
- * https://developer.mozilla.org/en/DOM/window.btoa
- *
- * window.btoa and base64.encode takes a string where charCodeAt is [0,255]
- * If any character is not [0,255], then an DOMException(5) is thrown.
- *
- * window.atob and base64.decode take a base64-encoded string
- * If the input length is not a multiple of 4, or contains invalid characters
- * then an DOMException(5) is thrown.
- */
-var base64 = {};
-base64.PADCHAR = '=';
-base64.ALPHA = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
-
-base64.makeDOMException = function() {
- // sadly in FF,Safari,Chrome you can't make a DOMException
- var e, tmp;
-
- try {
- return new DOMException(DOMException.INVALID_CHARACTER_ERR);
- } catch (tmp) {
- // not available, just passback a duck-typed equiv
- // https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Global_Objects/Error
- // https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Global_Objects/Error/prototype
- var ex = new Error("DOM Exception 5");
-
- // ex.number and ex.description is IE-specific.
- ex.code = ex.number = 5;
- ex.name = ex.description = "INVALID_CHARACTER_ERR";
-
- // Safari/Chrome output format
- ex.toString = function() { return 'Error: ' + ex.name + ': ' + ex.message; };
- return ex;
- }
-}
-
-base64.getbyte64 = function(s,i) {
- // This is oddly fast, except on Chrome/V8.
- // Minimal or no improvement in performance by using a
- // object with properties mapping chars to value (eg. 'A': 0)
- var idx = base64.ALPHA.indexOf(s.charAt(i));
- if (idx === -1) {
- throw base64.makeDOMException();
- }
- return idx;
-}
-
-base64.decode = function(s) {
- // convert to string
- s = '' + s;
- var getbyte64 = base64.getbyte64;
- var pads, i, b10;
- var imax = s.length
- if (imax === 0) {
- return s;
- }
-
- if (imax % 4 !== 0) {
- throw base64.makeDOMException();
- }
-
- pads = 0
- if (s.charAt(imax - 1) === base64.PADCHAR) {
- pads = 1;
- if (s.charAt(imax - 2) === base64.PADCHAR) {
- pads = 2;
- }
- // either way, we want to ignore this last block
- imax -= 4;
- }
-
- var x = [];
- for (i = 0; i < imax; i += 4) {
- b10 = (getbyte64(s,i) << 18) | (getbyte64(s,i+1) << 12) |
- (getbyte64(s,i+2) << 6) | getbyte64(s,i+3);
- x.push(String.fromCharCode(b10 >> 16, (b10 >> 8) & 0xff, b10 & 0xff));
- }
-
- switch (pads) {
- case 1:
- b10 = (getbyte64(s,i) << 18) | (getbyte64(s,i+1) << 12) | (getbyte64(s,i+2) << 6);
- x.push(String.fromCharCode(b10 >> 16, (b10 >> 8) & 0xff));
- break;
- case 2:
- b10 = (getbyte64(s,i) << 18) | (getbyte64(s,i+1) << 12);
- x.push(String.fromCharCode(b10 >> 16));
- break;
- }
- return x.join('');
-}
-
-base64.getbyte = function(s,i) {
- var x = s.charCodeAt(i);
- if (x > 255) {
- throw base64.makeDOMException();
- }
- return x;
-}
-
-base64.encode = function(s) {
- if (arguments.length !== 1) {
- throw new SyntaxError("Not enough arguments");
- }
- var padchar = base64.PADCHAR;
- var alpha = base64.ALPHA;
- var getbyte = base64.getbyte;
-
- var i, b10;
- var x = [];
-
- // convert to string
- s = '' + s;
-
- var imax = s.length - s.length % 3;
-
- if (s.length === 0) {
- return s;
- }
- for (i = 0; i < imax; i += 3) {
- b10 = (getbyte(s,i) << 16) | (getbyte(s,i+1) << 8) | getbyte(s,i+2);
- x.push(alpha.charAt(b10 >> 18));
- x.push(alpha.charAt((b10 >> 12) & 0x3F));
- x.push(alpha.charAt((b10 >> 6) & 0x3f));
- x.push(alpha.charAt(b10 & 0x3f));
- }
- switch (s.length - imax) {
- case 1:
- b10 = getbyte(s,i) << 16;
- x.push(alpha.charAt(b10 >> 18) + alpha.charAt((b10 >> 12) & 0x3F) +
- padchar + padchar);
- break;
- case 2:
- b10 = (getbyte(s,i) << 16) | (getbyte(s,i+1) << 8);
- x.push(alpha.charAt(b10 >> 18) + alpha.charAt((b10 >> 12) & 0x3F) +
- alpha.charAt((b10 >> 6) & 0x3f) + padchar);
- break;
- }
- return x.join('');
-}
-
-
--- /dev/null
+/**
+ * Code for the controls page.
+ * @date 19-10-2013
+ */
+
+mctx.control = {};
+mctx.control.api = mctx.api + 'control'
+mctx.control.states = {
+ start : 0,
+ pause : 1,
+ resume : 2,
+ stop : 3,
+ emergency : 4
+};
+mctx.control.state = null;
+
+function toggleControls(running) {
+ if (running) {
+ $("#experiment-stop").show();
+ $("#pressure-widget").show();
+ $("#start-widget").hide();
+ } else {
+ $("#start-widget").show();
+ $("#experiment-stop").hide();
+ $("#pressure-widget").hide();
+ }
+}
+
+$.fn.setStatusUpdater = function () {
+ var result = this;
+
+ var updater = function () {
+ $.ajax({
+ url : mctx.control.api,
+ data : {'action' : 'identify'}
+ }).done(function (data) {
+ if (!result.checkStatus(data)) {
+ $(result).parent().addClass("fail");
+ setTimeout(updater, 4000);
+ return;
+ }
+
+ var text;
+ var running = false;
+ var fail = false;
+ switch (data.control_state_id) {
+ case mctx.control.states.start:
+ text = "Experiment started - '" + data.control_experiment_name +
+ "' by " + data.control_user_name;
+ running = true;
+ break;
+ case mctx.control.states.pause:
+ text = "Experiment paused - '" + data.control_experiment_name +
+ "' by " + data.control_user_name;
+ running = true;
+ break;
+ case mctx.control.states.stop:
+ text = "No experiment running.";
+ break;
+ case mctx.control.states.emergency:
+ text = "Emergency mode - '" + data.control_experiment_name +
+ "' by " + data.control_user_name;
+ running = true;
+ fail = true;
+ default:
+ text = "Unknown mode: " + data.control_state_id;
+ fail = true;
+ }
+
+ if (data.control_state_id !== mctx.control.state) {
+ toggleControls(running);
+ $(result).text(text);
+ if (fail) {
+ $(result).parent().addClass("fail");
+ } else {
+ $(result).parent().addClass("pass");
+ }
+
+ mctx.control.state = data.control_state_id;
+ }
+
+ setTimeout(updater, 2000);
+ })
+ .fail(function () {
+ $(result).text("Connection failed.").parent().addClass("fail");
+ setTimeout(updater, 4000);
+ });
+ };
+
+ updater();
+};
+
+
+$.fn.startExperiment = function (group, experiment, force, result) {
+ $(group).attr('disabled', 'disabled');
+
+ if (!experiment || !experiment.match(/^[a-zA-Z0-9_-]+$/)) {
+ result.text("Experiment names must be composed of alphanumeric characters" +
+ " or the characters -_-").addClass("fail");
+ $(group).removeAttr('disabled');
+ return;
+ }
+
+ var data = {action : "start", name : experiment};
+ if (force) {
+ data.force = 1;
+ }
+
+ $.ajax({
+ url : mctx.control.api,
+ data : data
+ }).done(function (data) {
+ if (!result.checkStatus(data)) {
+ return;
+ }
+ result.html(" ");
+ toggleControls(true);
+ }).always(function () {
+ $(group).removeAttr('disabled');
+ });
+};
+
+$.fn.stopExperiment = function (result) {
+ var stop = this;
+ stop.attr('disabled', 'disabled');
+ result.text("Stopping the experiment...");
+
+ $.ajax({
+ url : mctx.control.api,
+ data : {action : "stop"}
+ }).done(function (data) {
+ if (!result.checkStatus(data)) {
+ return;
+ }
+ result.html(" ");
+ toggleControls(false);
+ }).always(function () {
+ stop.removeAttr('disabled');
+ });
+};
+
+$.fn.setPressure = function(pressure, result) {
+ result.html(" ");
+
+ for (var k in pressure) {
+ var n = Number(pressure[k]);
+ if (isNaN(n) || n < 0) {
+ result.text("You must give positive numeric values.").addClass("fail");
+ return;
+ }
+ pressure[k] = n;
+ }
+
+ var set = pressure['set'] + "," + pressure['wait'] + ","
+ pressure['size'] + "," + pressure['count'];
+ $.ajax({
+ url : mctx.api + "actuators",
+ data : {id : mctx.actuator.pressure_regulator, set : set}
+ }).done(function (data) {
+ if (!result.checkStatus(data)) {
+ return;
+ }
+
+ result.text("Set ok!").removeClass("fail").addClass("pass");
+ });
+};
\ No newline at end of file
* Appends each value pair to the result
* @returns result
*/
+/**
+ * Helper - Calculate pairs of (dependent, independent) values
+ * Given input as (time, value) pairs for dependent and independent
+ * Appends each value pair to the result
+ * @param {array[][]} dependent Dependent data to be correlated with independent
+ * @param {array[][]} independent Independent data
+ * @param {array[][]} result Storage location
+ * @returns {dataMerge.result}
+ */
function dataMerge(dependent, independent, result) {
var j = 0;
for (var i = 0; i < dependent.length-1; ++i) {
});
};
-function setGraphStatus(on, failText) {
+function setGraphStatus(on, failText, keep) {
if (on) {
mctx.graph.running = true;
$("#status-text").html(" ");
- $("#graph-run").text("Pause");
+ $("#graph-run").prop("value", "Pause");
} else {
mctx.graph.running = false;
if (failText) {
$("#status-text").text(failText).addClass("fail");
- } else {
+ } else if (!keep) {
$("#status-text").text("Graph stopped").removeClass("fail");
}
- $("#graph-run").text("Run");
+ $("#graph-run").prop("value", "Run");
}
}
responses.push($.ajax({url : urls[val.urltype], data : parameters})
.done(function(json) {
//alert("Hi from " + json.name);
+ if (!$("#status-text").checkStatus(json)) {
+ setGraphStatus(false, null, true); //Don't reset text, checkstatus just set it.
+ return;
+ }
+
var dev = val.data;
for (var i = 0; i < json.data.length; ++i) {
if (dev.length <= 0 || json.data[i][0] > dev[dev.length-1][0]) {
//... When the response is received, then() will happen (I think?)
$.when.apply(this, responses).then(function () {
- var plot_data = [];
- yaxis.each(function() {
- //alert("Add " + $(this).val() + " to plot");
- if (xaxis.attr("alt") === "time") {
- //alert("Against time");
- plot_data.push(devices[$(this).attr("alt")].data);
+ if (mctx.graph.running) {
+ var plot_data = [];
+
+ yaxis.each(function() {
+ //alert("Add " + $(this).val() + " to plot");
+ if (xaxis.attr("alt") === "time") {
+ //alert("Against time");
+ plot_data.push(devices[$(this).attr("alt")].data);
+ } else {
+ var result = []
+ dataMerge(devices[xaxis.attr("alt")].data,
+ devices[$(this).attr("alt")].data, result);
+ plot_data.push(result);
+ }
+ });
+
+ if (mctx.graph.chart !== null) {
+ mctx.graph.chart.setData(plot_data);
+ mctx.graph.chart.setupGrid();
+ mctx.graph.chart.draw();
} else {
- var result = []
- dataMerge(devices[xaxis.attr("alt")].data,
- devices[$(this).attr("alt")].data, result);
- plot_data.push(result);
+ mctx.graph.chart = $.plot("#graph", plot_data);
}
- });
-
- //$.plot("#graph", plot_data);
- if (mctx.graph.chart !== null) {
- mctx.graph.chart.setData(plot_data);
- mctx.graph.chart.setupGrid();
- mctx.graph.chart.draw();
- } else {
- mctx.graph.chart = $.plot("#graph", plot_data);
- }
- if (mctx.graph.running) {
mctx.graph.timer = setTimeout(updater, 1000);
}
}, function () {
{'text' : 'Help', href : mctx.location + 'help.html'}
];
-mctx.statusCodes = {
- STATUS_OK : 1
+mctx.status = {
+ OK : 1,
+ ERROR : -1,
+ UNAUTHORIZED : -2,
+ NOTRUNNING : -3,
+ ALREADYEXISTS : -4
};
mctx.statusCodesDescription = {
3 : {name : "Pressure regulator"}
};
+mctx.actuator = {};
+mctx.actuator.pressure_regulator = 0;
+
mctx.strain_gauges = {};
mctx.strain_gauges.ids = [0, 1, 2, 3];
mctx.strain_gauges.time_limit = 20;
alert(arguments[i]);
}
} else {
- console.log.apply(this, arguments);
+ try {
+ console.log.apply(this, arguments);
+ } catch (e) {
+ //Chromie
+ for (var i = 0; i < arguments.length; i++) {
+ console.log(arguments[i]);
+ }
+ }
}
}
}
var url = mctx.api + "errorlog";
var outdiv = this;
+ if ($(this).length <= 0) {
+ //No error log, so do nothing.
+ return;
+ }
+
var updater = function () {
$.ajax({url : url}).done(function (data) {
outdiv.text(data);
outdiv.scrollTop(
- outdiv[0].scrollHeight - outdiv.height()
+ outdiv[0].scrollHeight - outdiv.height()
);
setTimeout(updater, 3000);
}).fail(function (jqXHR) {
return this;
};
+$.fn.checkStatus = function(data) {
+ if (data.status !== mctx.status.OK) {
+ $(this).text(data.description).removeClass("pass").addClass("fail");
+ return false;
+ }
+ return true;
+};
+
$(document).ready(function () {
//Show the content!
$("#content").css("display", "block");
border-bottom: 1px solid gray;
}
-form.controls {
- background-color: #F9F9F9;
- border: 1px solid #808080;
- padding: 1em;
- margin: 1em auto;
-}
-
a {
color: #1188DD;
}
font-weight: bold
}
-div.centre {
+div.centre, p.centre {
text-align: center;
margin: auto;
}
font-weight: bold;
}
+.medium {
+ font-size: 115% !important;
+}
+
+.large {
+ font-size: 130% !important;
+}
+
+.normal {
+ font-size: inherit !important;
+}
+
+.small {
+ font-size: 80% !important;
+}
+
/*
table {
border: none;
table {
border-radius: 3px;
- border: 1px solid #ccc;
border-collapse: collapse;
word-wrap: break-word;
line-height: 1.3em;
width: 100%;
+ margin: 1em auto;
+ vertical-align: middle;
}
td {
padding: 0.2em 0.5em;
}
+tr.pass {
+ color: #000;
+ background-color: #7AE309;
+ font-weight: inherit;
+}
+
+tr.fail, tr.fail .fail {
+ color: #000;
+ background-color: #E30909;
+ font-weight: inherit;
+}
+
table.centre {
margin: auto;
text-align: center;
}
table.horizontal th {
- border-right: 1px solid #ccc;
+ text-align: left;
border-bottom: 0;
- width: 20%;
+ width: 15%;
+ font-weight: normal;
+}
+
+table.horizontal.medium th {
+ width: 22%;
}
img.centre {
box-shadow: 0 0 2px #BBBBBB;
}
+form.nice {
+ line-height: 1.8em;
+}
+
+form.nice p {
+ margin: 0.25em auto;
+}
+
+form.nice input {
+ font-size: 150%;
+}
+
+form.nice label {
+ font-size: 110%;
+}
+
+form.nice input[type="submit"], form.nice input[type="button"] {
+ font-size: inherit;
+ padding: 0.4em 0.8em;
+}
+
+form.controls {
+ background-color: #F9F9F9;
+ border: 1px solid #808080;
+ padding: 1em;
+ margin: 1em auto;
+}
+
#header-wrap {
background-color: #2a2a2a;
border-top: 0.3em solid #1188DD;
padding: 1em 2em;
/*max-width: 1920px;*/
margin: 1em auto;
+ transition: all 0.2s ease 0s;
}
#content {
}
#main .sub-title {
- font-size: 18px;
+ font-size: 115%;
font-weight: bold;
margin-bottom: 0.25em;
}
display: table-cell;
}
+.left {
+ float: left;
+}
+
.right {
float: right;
}
#login-container #result {
clear: both;
}
+
+#experiment-stop {
+ padding: 0.2em 0.4em;
+ display: none;
+}
+
+#start-widget {
+ display: none;
+}
+
+#pressure-widget {
+ display: none;
+}
\ No newline at end of file