Merge branch 'master' of github:szmoore/MCTX3420
authorSam Moore <[email protected]>
Sat, 19 Oct 2013 15:40:22 +0000 (23:40 +0800)
committerSam Moore <[email protected]>
Sat, 19 Oct 2013 15:40:22 +0000 (23:40 +0800)
Conflicts:
server/control.c

16 files changed:
server/actuator.c
server/common.h
server/control.c
server/control.h
server/fastcgi.c
server/fastcgi.h
server/main.c
server/options.h
server/sensor.c
testing/MCTXWeb/public_html/control.html
testing/MCTXWeb/public_html/graph.html
testing/MCTXWeb/public_html/static/base64.js [deleted file]
testing/MCTXWeb/public_html/static/mctx.control.js [new file with mode: 0644]
testing/MCTXWeb/public_html/static/mctx.graph.js
testing/MCTXWeb/public_html/static/mctx.gui.js
testing/MCTXWeb/public_html/static/style.css

index f2ab4fe..1c105db 100644 (file)
@@ -8,9 +8,6 @@
 // Files containing GPIO and PWM definitions
 #include "bbb_pin.h"
 
-
-
-
 /** Number of actuators **/
 int g_num_actuators = 0;
 
@@ -79,12 +76,17 @@ void Actuator_SetMode(Actuator * a, ControlModes mode, void *arg)
        {
                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);
@@ -141,7 +143,7 @@ void Actuator_SetMode(Actuator * a, ControlModes mode, void *arg)
  */
 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);
 }
 
index 637a813..0875b34 100644 (file)
@@ -52,6 +52,7 @@
                                                                        }
 
 extern bool PathExists(const char * path);
+extern bool DirExists(const char * path);
 
 
 
index 776cdd1..6ebaba7 100644 (file)
@@ -8,16 +8,30 @@
 #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");
@@ -28,6 +42,43 @@ bool PathExists(const char *path)
        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..
@@ -40,10 +91,6 @@ void Control_Handler(FCGIContext *context, char *params) {
        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?)
@@ -54,43 +101,19 @@ void Control_Handler(FCGIContext *context, char *params) {
                {"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
 
@@ -111,67 +134,71 @@ void Control_Handler(FCGIContext *context, char *params) {
                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);
+       }
 }
 
 /**
@@ -192,18 +219,13 @@ const char* Control_SetMode(ControlModes desired_mode, void * arg)
        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.";
index a50eca1..7081c3f 100644 (file)
@@ -14,9 +14,9 @@ typedef enum ControlModes {
 } 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);
index a94a5ba..0a6783f 100644 (file)
  */ 
 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))
@@ -94,6 +98,7 @@ bool FCGI_LockControl(FCGIContext *context, const char * user_name, UserType use
        // 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 || 
@@ -109,7 +114,7 @@ bool FCGI_LockControl(FCGIContext *context, const char * user_name, UserType use
        // 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));
@@ -117,25 +122,38 @@ bool FCGI_LockControl(FCGIContext *context, const char * user_name, UserType use
        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; // :-)
 }
 
@@ -496,16 +514,17 @@ void * FCGI_RequestLoop (void *data)
        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
@@ -543,7 +562,7 @@ void * FCGI_RequestLoop (void *data)
                        //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;       
index 85dab57..de02501 100644 (file)
@@ -33,6 +33,8 @@ typedef enum StatusCodes {
 #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;
@@ -47,13 +49,15 @@ typedef struct
        /**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?**/
index 5aef29f..fadcd3a 100644 (file)
@@ -36,13 +36,11 @@ Options g_options; // options passed to program through command line arguments
  */
 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
 
@@ -50,9 +48,8 @@ void ParseArguments(int argc, char ** argv)
        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] != '-')
@@ -83,6 +80,10 @@ void ParseArguments(int argc, char ** argv)
                        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;
@@ -96,7 +97,13 @@ void ParseArguments(int argc, char ** argv)
        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')
        {
index 8dfbfa4..0e0b598 100644 (file)
@@ -31,11 +31,8 @@ typedef struct
        /** 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 **/
index 816a895..ae5cdf4 100644 (file)
@@ -122,11 +122,15 @@ void Sensor_SetMode(Sensor * s, ControlModes mode, void * arg)
                        {
                                // 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);
@@ -306,7 +310,6 @@ void Sensor_Handler(FCGIContext *context, char * params)
        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;
index 9ba8021..60817cf 100644 (file)
@@ -9,19 +9,51 @@
     <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">
+              &nbsp;
+            </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">
+                &nbsp;
+              </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>
index 7b86842..b7d87f1 100644 (file)
     <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();
diff --git a/testing/MCTXWeb/public_html/static/base64.js b/testing/MCTXWeb/public_html/static/base64.js
deleted file mode 100644 (file)
index 72bced4..0000000
+++ /dev/null
@@ -1,178 +0,0 @@
-/*
- * 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('');
-}
-
-
diff --git a/testing/MCTXWeb/public_html/static/mctx.control.js b/testing/MCTXWeb/public_html/static/mctx.control.js
new file mode 100644 (file)
index 0000000..24b0448
--- /dev/null
@@ -0,0 +1,166 @@
+/**
+ * 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("&nbsp;");
+   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("&nbsp;");
+    toggleControls(false);
+  }).always(function () {
+    stop.removeAttr('disabled');
+  });
+};
+
+$.fn.setPressure = function(pressure, result) {
+  result.html("&nbsp;");
+  
+  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
index e79b6a8..22d14d7 100644 (file)
@@ -23,6 +23,15 @@ mctx.graph.chart = null;
  * 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) {
@@ -97,19 +106,19 @@ $.fn.setDevices = function() {
   });
 };
 
-function setGraphStatus(on, failText) {
+function setGraphStatus(on, failText, keep) {
   if (on) {
     mctx.graph.running = true;
     $("#status-text").html("&nbsp;");
-    $("#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");
   }
 }
 
@@ -146,6 +155,11 @@ function graphUpdater() {
         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]) {
@@ -160,29 +174,29 @@ function graphUpdater() {
 
     //... 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 () {
index 48737b5..3df2bac 100644 (file)
@@ -25,8 +25,12 @@ mctx.menu = [
     {'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 = {
@@ -54,6 +58,9 @@ mctx.actuators = {
     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;
@@ -71,7 +78,14 @@ function debugLog () {
                 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]);
+              }
+            }
         }
     }
 }
@@ -265,11 +279,16 @@ $.fn.setErrorLog = function () {
     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) {
@@ -284,6 +303,14 @@ $.fn.setErrorLog = function () {
     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");
index b4736db..f282aba 100644 (file)
@@ -25,13 +25,6 @@ hr {
   border-bottom: 1px solid gray;
 }
 
-form.controls {
-  background-color: #F9F9F9;
-  border: 1px solid #808080;
-  padding: 1em;
-  margin: 1em auto;
-}
-
 a {
   color: #1188DD;
 }
@@ -54,7 +47,7 @@ img {
   font-weight: bold
 }
 
-div.centre {
+div.centre, p.centre {
   text-align: center;
   margin: auto;
 }
@@ -63,6 +56,22 @@ div.centre {
   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;
@@ -91,17 +100,30 @@ th {
 
 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;
@@ -112,9 +134,14 @@ table.horizontal {
 }
 
 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 {
@@ -149,6 +176,34 @@ input[type="text"]:focus, input[type="password"]:focus {
   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;
@@ -226,6 +281,7 @@ input[type="text"]:focus, input[type="password"]:focus {
   padding: 1em 2em;
   /*max-width: 1920px;*/
   margin: 1em auto;
+  transition: all 0.2s ease 0s;
 }
 
 #content  {
@@ -272,7 +328,7 @@ input[type="text"]:focus, input[type="password"]:focus {
 }
 
 #main .sub-title {
-  font-size: 18px;
+  font-size: 115%;
   font-weight: bold;
   margin-bottom: 0.25em;
 }
@@ -289,6 +345,10 @@ input[type="text"]:focus, input[type="password"]:focus {
   display: table-cell;
 }
 
+.left {
+  float: left;
+}
+
 .right {
   float: right;
 }
@@ -371,3 +431,16 @@ input[type="text"]:focus, input[type="password"]:focus {
 #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

UCC git Repository :: git.ucc.asn.au