Demo mode, Right click to enable camera crosshair.
authorJoseph Coffland <joseph@cauldrondevelopment.com>
Wed, 20 Feb 2019 23:58:50 +0000 (15:58 -0800)
committerJoseph Coffland <joseph@cauldrondevelopment.com>
Wed, 20 Feb 2019 23:58:50 +0000 (15:58 -0800)
39 files changed:
CHANGELOG.md
scripts/buildbotics.gc [deleted file]
scripts/install.sh
src/avr/emu/src/emu.c
src/js/admin-general-view.js
src/js/admin-network-view.js
src/js/api.js
src/js/app.js
src/js/control-view.js
src/js/main.js
src/pug/index.pug
src/py/bbctrl/APIHandler.py
src/py/bbctrl/AVR.py
src/py/bbctrl/AVREmu.py
src/py/bbctrl/Camera.py
src/py/bbctrl/Cmd.py
src/py/bbctrl/Comm.py
src/py/bbctrl/CommandQueue.py
src/py/bbctrl/Config.py
src/py/bbctrl/Ctrl.py
src/py/bbctrl/FileHandler.py
src/py/bbctrl/GCodeStream.py [deleted file]
src/py/bbctrl/IOLoop.py [new file with mode: 0644]
src/py/bbctrl/Jog.py
src/py/bbctrl/LCD.py
src/py/bbctrl/Log.py [new file with mode: 0644]
src/py/bbctrl/Mach.py
src/py/bbctrl/Messages.py [deleted file]
src/py/bbctrl/Planner.py
src/py/bbctrl/Preplanner.py
src/py/bbctrl/Pwr.py
src/py/bbctrl/RequestHandler.py [new file with mode: 0644]
src/py/bbctrl/State.py
src/py/bbctrl/Web.py
src/py/bbctrl/__init__.py
src/resources/buildbotics.nc [new file with mode: 0644]
src/resources/images/in-use.jpg
src/resources/images/offline.jpg
src/stylus/style.styl

index c872b1fc5deb2a527c2045355a142dc662dae0f4..8325c6ef0289792527fe99a448e01c35ce704f45 100644 (file)
@@ -3,6 +3,9 @@ Buildbotics CNC Controller Firmware Changelog
 
 ## v0.4.6
  - Fixed a rare ``Negative s-curve time`` error.
+ - Don't allow manual axis homing when soft limits are not set.
+ - Right click to enable camera crosshair.
+ - Demo mode.
 
 ## v0.4.5
  - Fix for random errors while running VFD.
diff --git a/scripts/buildbotics.gc b/scripts/buildbotics.gc
deleted file mode 100644 (file)
index 8c5ddb0..0000000
+++ /dev/null
@@ -1,403 +0,0 @@
-G21
-(File: 'buildbotics_logo.tpl')
-G0 Z3
-F1600
-M3 S10000
-M6 T2
-G0 X59.25 Y5.85
-G1 Z-1.5
-G1 X61.68 Y6.7
-G1 X63.86 Y8.07
-G1 X65.68 Y9.89
-G1 X67.05 Y12.07
-G1 X67.9 Y14.5
-G1 X68.2 Y17.09
-G1 Y56.6
-G1 X67.73 Y59.04
-G1 X50.8
-G1 Y34.9
-G1 X50.65 Y34.55
-G1 X50.3 Y34.4
-G1 X23.46
-G1 X23.1 Y34.55
-G1 X22.96 Y34.9
-G1 X22.98 Y49.88
-G1 X22.96 Y59.05
-G1 X22.41
-G1 X19.26
-G1 X6.04
-G1 X5.56 Y56.53
-G1 Y17.09
-G1 X5.85 Y14.5
-G1 X6.7 Y12.07
-G1 X8.07 Y9.89
-G1 X9.89 Y8.07
-G1 X12.07 Y6.7
-G1 X14.5 Y5.85
-G1 X17.09 Y5.56
-G1 X56.67
-G1 X59.25 Y5.85
-G0 Z3
-G0 X64.26 Y64.72
-G1 Z-1.5
-G1 X61.78 Y66.52
-G1 X58.91 Y67.68
-G1 X56.54 Y68.08
-G1 X17.22
-G1 X14.84 Y67.68
-G1 X11.97 Y66.52
-G1 X9.49 Y64.72
-G1 X8.08 Y63.16
-G1 X27.35
-G1 X27.89 Y63.45
-G1 X27.96 Y63.48
-G1 X31.48 Y64.75
-G1 X31.52 Y64.76
-G1 X31.56 Y64.77
-G1 X35.19 Y65.41
-G1 X35.26
-G1 X35.97 Y65.44
-G1 X36.04 Y65.45
-G1 X36.07
-G1 X36.82 Y65.44
-G1 X36.83
-G1 X36.89
-G1 X36.95
-G1 X36.97
-G1 X37.72 Y65.43
-G1 X37.74
-G1 X37.8
-G1 X37.81
-G1 X37.88
-G1 X37.89
-G1 X38.65 Y65.38
-G1 X38.68
-G1 X38.75 Y65.37
-G1 X39.38 Y65.32
-G1 X39.44 Y65.31
-G1 X42.68 Y64.64
-G1 X42.76 Y64.62
-G1 X45.87 Y63.44
-G1 X45.93 Y63.41
-G1 X46.4 Y63.16
-G1 X65.67
-G1 X64.26 Y64.72
-G0 Z3
-G0 X36.88 Y9.4
-G1 Z-1.5
-G1 X37.31 Y9.64
-G1 X39.58 Y13.48
-G1 X39.63 Y13.6
-G1 X39.65 Y13.73
-G1 Y27.54
-G1 X41.67
-G1 Y25.39
-G1 X41.75 Y25.12
-G1 X41.97 Y24.93
-G1 X46.41 Y22.92
-G1 Y19.97
-G1 X45.44
-G1 X45.08 Y19.82
-G1 X44.94 Y19.47
-G1 Y13.73
-G1 X45.08 Y13.38
-G1 X45.44 Y13.23
-G1 X49.94 Y13.24
-G1 X50.29 Y13.39
-G1 X50.44 Y13.74
-G1 Y19.47
-G1 X50.29 Y19.83
-G1 X49.93 Y19.97
-G1 X48.92
-G1 Y23.61
-G1 X48.84 Y23.88
-G1 X48.63 Y24.06
-G1 X44.19 Y26.12
-G1 Y27.54
-G1 X49.22
-G1 X49.33 Y27.56
-G1 X49.44 Y27.6
-G1 X50.13 Y27.94
-G1 X50.25 Y28.02
-G1 X50.34 Y28.13
-G1 X50.73 Y28.77
-G1 X50.78 Y28.89
-G1 X50.8 Y29.03
-G1 Y33.05
-G1 Y34.25
-G1 Y34.65
-G1 X50.66 Y35.01
-G1 X50.3 Y35.15
-G1 X23.46
-G1 X23.1 Y35.01
-G1 X22.96 Y34.65
-G1 Y29.07
-G1 X22.97 Y28.94
-G1 X23.02 Y28.82
-G1 X23.4 Y28.17
-G1 X23.49 Y28.06
-G1 X23.6 Y27.98
-G1 X24.29 Y27.6
-G1 X24.4 Y27.56
-G1 X24.52 Y27.54
-G1 X25.55
-G1 Y26.4
-G1 X23.4 Y25.52
-G1 X23.17 Y25.33
-G1 X23.09 Y25.06
-G1 Y17.54
-G1 X23.23 Y17.19
-G1 X23.59 Y17.04
-G1 X24.6
-G1 Y10.36
-G1 X24.62 Y10.23
-G1 X24.66 Y10.11
-G1 X24.8 Y9.88
-G1 X24.88 Y9.77
-G1 X24.99 Y9.68
-G1 X25.25 Y9.54
-G1 X25.37 Y9.5
-G1 X25.49 Y9.48
-G1 X26.53
-G1 X26.65 Y9.49
-G1 X26.76 Y9.54
-G1 X27.01 Y9.66
-G1 X27.12 Y9.74
-G1 X27.21 Y9.85
-G1 X27.35 Y10.09
-G1 X27.41 Y10.22
-G1 X27.43 Y10.35
-G1 Y10.43
-G1 X27.47 Y17.04
-G1 X28.57
-G1 X28.92 Y17.19
-G1 X29.07 Y17.54
-G1 Y24.64
-G1 X30.72 Y25.3
-G1 X30.95 Y25.49
-G1 X31.03 Y25.77
-G1 X31.02 Y27.54
-G1 X34.03
-G1 Y13.73
-G1 X34.05 Y13.59
-G1 X34.1 Y13.47
-G1 X36.45 Y9.64
-G1 X36.88 Y9.4
-G0 Z3
-G0 X49.94 Y10.82
-G1 Z-1.5
-G1 X50.29 Y10.97
-G1 X50.44 Y11.32
-G1 Y13.34
-G1 X50.29 Y13.69
-G1 X49.94 Y13.84
-G1 X45.44
-G1 X45.08 Y13.69
-G1 X44.94 Y13.34
-G1 Y11.32
-G1 X45.08 Y10.97
-G1 X45.44 Y10.82
-G1 X49.94
-G0 Z3
-G0 X48.46 Y9.7
-G1 Z-1.5
-G1 X48.59 Y9.72
-G1 X48.71 Y9.77
-G1 X50.03 Y10.53
-G1 X50.21 Y10.71
-G1 X50.28 Y10.96
-G1 X50.14 Y11.31
-G1 X49.78 Y11.46
-G1 X45.62
-G1 X45.14 Y11.09
-G1 X45.37 Y10.53
-G1 X46.69 Y9.77
-G1 X46.81 Y9.72
-G1 X46.94 Y9.7
-G1 X48.46
-G0 Z3
-G0 X50.3 Y34.4
-G1 Z-1.5
-G1 X50.66 Y34.55
-G1 X50.8 Y34.9
-G1 Y49.88
-G1 X50.79 Y59.52
-G1 X50.76 Y59.69
-G1 X50.67 Y59.84
-G1 X50.09 Y60.52
-G1 X50.06 Y60.55
-G1 X50.02 Y60.58
-G1 X47.89 Y62.26
-G1 X47.85 Y62.28
-G1 X47.81 Y62.31
-G1 X44.46 Y64.03
-G1 X44.41 Y64.05
-G1 X44.37 Y64.06
-G1 X40.69 Y65.1
-G1 X40.62 Y65.12
-G1 X37.86 Y65.45
-G1 X37.8 Y65.46
-G1 X36.89 Y65.44
-G1 X36.83
-G1 X36.82
-G1 X36.07 Y65.45
-G1 X36.04 Y65.44
-G1 X35.97
-G1 X35.1 Y65.41
-G1 X35.04 Y65.4
-G1 X32.44 Y64.99
-G1 X32.37 Y64.97
-G1 X28.94 Y63.9
-G1 X28.89 Y63.88
-G1 X28.85 Y63.86
-G1 X25.74 Y62.2
-G1 X25.7 Y62.18
-G1 X25.66 Y62.15
-G1 X23.68 Y60.56
-G1 X23.65 Y60.53
-G1 X23.62 Y60.5
-G1 X23.08 Y59.87
-G1 X22.99 Y59.71
-G1 X22.96 Y59.54
-G1 X22.98 Y49.88
-G1 X22.96 Y34.9
-G1 X23.1 Y34.55
-G1 X23.46 Y34.4
-G1 X50.3
-G0 Z3
-G0 X55.2 Y43.67
-G1 Z-1.5
-G1 Y51.34
-G1 X55.11 Y51.94
-G1 X54.83 Y52.88
-G1 X54.39 Y53.88
-G1 X53.8 Y54.85
-G1 X53.09 Y55.74
-G1 X52.28 Y56.47
-G1 X51.41 Y56.98
-G1 X51.07 Y57.09
-G1 Y43.67
-G1 X55.2
-G0 Z3
-G0 X22.69 Y43.63
-G1 Z-1.5
-G1 Y57.09
-G1 X22.35 Y56.98
-G1 X21.47 Y56.47
-G1 X20.67 Y55.74
-G1 X19.95 Y54.85
-G1 X19.36 Y53.88
-G1 X18.92 Y52.88
-G1 X18.64 Y51.94
-G1 X18.55 Y51.34
-G1 Y43.63
-G1 X22.69
-G0 Z3
-G0 X28.55 Y35.84
-G1 Z-0.99
-G1 X30.11 Y36.15
-G1 X31.43 Y37.03
-G1 X32.32 Y38.35
-G1 X32.63 Y39.91
-G1 X32.32 Y41.47
-G1 X31.43 Y42.79
-G1 X30.11 Y43.68
-G1 X28.55 Y43.99
-G1 X26.99 Y43.68
-G1 X25.67 Y42.79
-G1 X24.79 Y41.47
-G1 X24.48 Y39.91
-G1 X24.79 Y38.35
-G1 X25.67 Y37.03
-G1 X26.99 Y36.15
-G1 X28.55 Y35.84
-G0 Z3
-G0 X45.33 Y35.93
-G1 Z-0.99
-G1 X46.88 Y36.24
-G1 X48.21 Y37.12
-G1 X49.09 Y38.45
-G1 X49.4 Y40
-G1 X49.09 Y41.56
-G1 X48.21 Y42.88
-G1 X46.88 Y43.77
-G1 X45.33 Y44.08
-G1 X43.77 Y43.77
-G1 X42.45 Y42.88
-G1 X41.56 Y41.56
-G1 X41.25 Y40
-G1 X41.56 Y38.45
-G1 X42.45 Y37.12
-G1 X43.77 Y36.24
-G1 X45.33 Y35.93
-G0 Z3
-G0 X45.2 Y39.12
-G1 Z-0.99
-G1 X45.7 Y39.19
-G1 X46.07 Y39.52
-G1 X46.22 Y40
-G1 X46.07 Y40.49
-G1 X45.7 Y40.81
-G1 X45.2 Y40.89
-G1 X44.74 Y40.68
-G1 X44.47 Y40.26
-G1 Y39.75
-G1 X44.74 Y39.33
-G1 X45.2 Y39.12
-G0 Z3
-G0 X28.43 Y39.03
-G1 Z-0.99
-G1 X28.92 Y39.1
-G1 X29.3 Y39.43
-G1 X29.44 Y39.91
-G1 X29.3 Y40.4
-G1 X28.92 Y40.72
-G1 X28.43 Y40.8
-G1 X27.97 Y40.59
-G1 X27.7 Y40.16
-G1 Y39.66
-G1 X27.97 Y39.24
-G1 X28.43 Y39.03
-G0 Z3
-G0 X55.76 Y0
-G1 Z-1.5
-G1 X59.27 Y0.35
-G1 X62.65 Y1.37
-G1 X65.76 Y3.03
-G1 X68.49 Y5.27
-G1 X70.73 Y8
-G1 X72.39 Y11.11
-G1 X73.42 Y14.49
-G1 X73.76 Y18
-G1 Y55.69
-G1 X73.42 Y59.2
-G1 X72.39 Y62.57
-G1 X70.73 Y65.69
-G1 X68.49 Y68.41
-G1 X65.76 Y70.65
-G1 X62.65 Y72.31
-G1 X59.27 Y73.34
-G1 X55.76 Y73.69
-G1 X18
-G1 X14.49 Y73.34
-G1 X11.11 Y72.31
-G1 X8 Y70.65
-G1 X5.27 Y68.41
-G1 X3.03 Y65.69
-G1 X1.37 Y62.57
-G1 X0.35 Y59.2
-G1 X0 Y55.69
-G1 Y18
-G1 X0.35 Y14.49
-G1 X1.37 Y11.11
-G1 X3.03 Y8
-G1 X5.27 Y5.27
-G1 X8 Y3.03
-G1 X11.11 Y1.37
-G1 X14.49 Y0.35
-G1 X18 Y0
-G1 X55.76
-G0 Z3
-M5
-G0 X40 Y75
-M2
index 11f64d304dd40adc58980368839795430eed1afc..49965e89d4f1a2b9e83ea40a2386c133fd64bc74 100755 (executable)
@@ -118,12 +118,6 @@ cp scripts/xinitrc ~pi/.xinitrc
 chmod +x ~pi/.xinitrc
 chown pi:pi ~pi/.xinitrc
 
-# Install default GCode
-if [ ! -d /var/lib/bbctrl/upload -o -z "$(ls -A /var/lib/bbctrl/upload)" ]; then
-    mkdir -p /var/lib/bbctrl/upload/
-    cp scripts/buildbotics.gc /var/lib/bbctrl/upload/
-fi
-
 # Install rc.local
 cp scripts/rc.local /etc/
 
index c42075873bf86d39da4aadbd7214f1bdc99bdc67..a0b06a5e5c8bb66700d01b53c287f28b65173a96 100644 (file)
@@ -83,6 +83,9 @@ void emu_init() {
   // So usart_flush() returns
   SERIAL_PORT.STATUS = USART_DREIF_bm | USART_TXCIF_bm;
 
+  // Clear motor fault
+  PIN_PORT(MOTOR_FAULT_PIN)->IN |= PIN_BM(MOTOR_FAULT_PIN);
+
   FD_ZERO(&readFDs);
 }
 
index 2d04bbc5b4cf7f8d7a13f4ad83d0f1a989924575..6c5d5fc9d9006d129d641390020172857a38a38b 100644 (file)
@@ -82,7 +82,7 @@ module.exports = {
         try {
           config = JSON.parse(e.target.result);
         } catch (ex) {
-          alert("Invalid config file");
+          api.alert("Invalid config file");
           return;
         }
 
@@ -91,7 +91,7 @@ module.exports = {
           this.configRestored = true;
 
         }.bind(this)).fail(function (error) {
-          alert('Restore failed: ' + error);
+          api.alert('Restore failed', error);
         })
       }.bind(this);
 
@@ -106,7 +106,7 @@ module.exports = {
         this.configReset = true;
 
       }.bind(this)).fail(function (error) {
-        alert('Reset failed: ' + error);
+        api.alert('Reset failed', error);
       });
     },
 
index 2572a15f285df946ead6b1ebf45abbe252c9eeb9..4268d35c46966bd2e1a38313738164c5315f1d80 100644 (file)
@@ -100,7 +100,7 @@ module.exports = {
         }.bind(this));
 
       }.bind(this)).fail(function (error) {
-        alert('Set hostname failed: ' + JSON.stringify(error));
+        api.alert('Set hostname failed', error);
       })
     },
 
@@ -109,19 +109,19 @@ module.exports = {
       api.put('remote/username', {username: this.username}).done(function () {
         this.usernameSet = true;
       }.bind(this)).fail(function (error) {
-        alert('Set username failed: ' + JSON.stringify(error));
+        api.alert('Set username failed', error);
       })
     },
 
 
     set_password: function () {
       if (this.password != this.password2) {
-        alert('Passwords to not match');
+        api.alert('Passwords to not match');
         return;
       }
 
       if (this.password.length < 6) {
-        alert('Password too short');
+        api.alert('Password too short');
         return;
       }
 
@@ -131,13 +131,14 @@ module.exports = {
       }).done(function () {
         this.passwordSet = true;
       }.bind(this)).fail(function (error) {
-        alert('Set password failed: ' + JSON.stringify(error));
+        api.alert('Set password failed', error);
       })
     },
 
 
     config_wifi: function () {
       this.wifiConfirm = false;
+
       this.rebooting = true;
 
       var config = {
@@ -148,8 +149,9 @@ module.exports = {
       }
 
       api.put('wifi', config).fail(function (error) {
-        alert('Failed to configure WiFi: ' + JSON.stringify(error));
-      })
+        api.alert('Failed to configure WiFi', error);
+        this.rebooting = false;
+      }.bind(this))
     }
   }
 }
index 251d5463fec65f03225005385d4b1549dedc9f35..8c07a0240f9b8ffe0d21744e149bac644bb3186e 100644 (file)
@@ -89,5 +89,16 @@ module.exports = {
 
   'delete': function (url, config) {
     return api_cb('DELETE', url, undefined, config);
+  },
+
+
+  alert: function (msg, error) {
+    if (typeof error != 'undefined') {
+      if (typeof error.message != 'undefined')
+        msg += '\n' + error.message;
+      else msg += '\n' + JSON.stringify(error);
+    }
+
+    alert(msg);
   }
 }
index 8ba50dccc7d4ec771966d1b3e9a82433755ee31a..aeb185f1a8ee382c4a418a4493440724f9edd4bb 100644 (file)
@@ -106,6 +106,7 @@ module.exports = new Vue({
       state: {},
       messages: [],
       video_size: cookie.get('video-size', 'small'),
+      crosshair: cookie.get('crosshair', false),
       errorTimeout: 30,
       errorTimeoutStart: 0,
       errorShow: false,
@@ -218,13 +219,20 @@ module.exports = new Vue({
     },
 
 
-    toggle_video: function () {
+    toggle_video: function (e) {
       if      (this.video_size == 'small')  this.video_size = 'large';
       else if (this.video_size == 'large')  this.video_size = 'small';
       cookie.set('video-size', this.video_size);
     },
 
 
+    toggle_crosshair: function (e) {
+      e.preventDefault();
+      this.crosshair = !this.crosshair;
+      cookie.set('crosshair', this.crosshair);
+    },
+
+
     estop: function () {
       if (this.state.xx == 'ESTOPPED') api.put('clear');
       else api.put('estop');
@@ -238,7 +246,7 @@ module.exports = new Vue({
         this.firmwareUpgrading = true;
 
       }.bind(this)).fail(function () {
-        alert('Invalid password');
+        api.alert('Invalid password');
       }.bind(this))
     },
 
@@ -262,7 +270,7 @@ module.exports = new Vue({
         this.firmwareUpgrading = true;
 
       }.bind(this)).error(function () {
-        alert('Invalid password or bad firmware');
+        api.alert('Invalid password or bad firmware');
       }.bind(this))
     },
 
@@ -356,7 +364,7 @@ module.exports = new Vue({
       api.put('config/save', this.config).done(function (data) {
         this.modified = false;
       }.bind(this)).fail(function (error) {
-        alert('Save failed: ' + error);
+        api.alert('Save failed', error);
       });
     },
 
index e756c91fed3faa1c0a2398d8e05921ee54660821..46c199ae95e637d99bbb23b1124cdd483e96122a 100644 (file)
@@ -281,7 +281,7 @@ module.exports = {
           this.$broadcast('gcode-reload', file.name);
 
         }.bind(this)).fail(function (error) {
-          alert('Upload failed: ' + error)
+          api.alert('Upload failed', error)
         }.bind(this));
     },
 
index 336a306da909529ecea9c80a796706b23d7932da..c3ebf6c96430b657d1d579a4565f8e012a01c222 100644 (file)
 'use strict';
 
 
+function cookie_get(name) {
+  var decodedCookie = decodeURIComponent(document.cookie);
+  var ca = decodedCookie.split(';');
+  name = name + '=';
+
+  for (var i = 0; i < ca.length; i++) {
+    var c = ca[i];
+    while (c.charAt(0) == ' ') c = c.substring(1);
+    if (!c.indexOf(name)) return c.substring(name.length, c.length);
+  }
+}
+
+
+function cookie_set(name, value, days) {
+  var d = new Date();
+  d.setTime(d.getTime() + days * 24 * 60 * 60 * 1000);
+  var expires = 'expires=' + d.toUTCString();
+  document.cookie = name + '=' + value + ';' + expires + ';path=/';
+}
+
+
+var uuid_chars =
+    'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_+';
+
+
+function uuid(length) {
+  if (typeof length == 'undefined') length = 52;
+
+  var s = '';
+  for (var i = 0; i < length; i++)
+    s += uuid_chars[Math.floor(Math.random() * uuid_chars.length)];
+
+  return s
+}
+
+
 $(function() {
+  if (typeof cookie_get('client-id') == 'undefined')
+    cookie_set('client-id', uuid(), 10000);
+
   // Vue debugging
   Vue.config.debug = true;
   //Vue.util.warn = function (msg) {console.debug('[Vue warn]: ' + msg)}
index dfd064717e1cd20b3d7f12bb8648e028b6d18b39..f960c0c197e5e8e8db5bf5178dd3118424606cd7 100644 (file)
@@ -96,7 +96,8 @@ html(lang="en")
                 span.left Build
                 span.right botics
               .subtitle
-                | Machine Controller v{{config.version}}
+                | CNC Controller #[b {{state.demo ? 'Demo ' : ''}}]
+                | v{{config.version}}
                 a.upgrade-version(v-if="show_upgrade()", href="#admin-general")
                   | Upgrade to v{{latestVersion}}
                 .fa.fa-check(v-if="!show_upgrade() && latestVersion",
@@ -105,8 +106,15 @@ html(lang="en")
             .estop(:class="{active: state.es}")
               estop(@click="estop")
 
-            .video(title="Plug camera into USB.\nClick to change video size.")
-              img(src="/api/video", @click="toggle_video", :class="video_size")
+            .video(title="Plug camera into USB.\n" +
+              "Left click to toggle video size.\n" +
+              "Right click to toggle crosshair.", @click="toggle_video",
+              @contextmenu="toggle_crosshair", :class="video_size")
+              .crosshair(v-if="crosshair")
+                .vertical
+                .horizontal
+                .box
+              img(src="/api/video")
 
         .clear
 
index 10bde7b2a36f5ab4b0c6adfee29ffe8237989c1d..c21ca5653890cc8b824ca51b5eb3445801c2a173 100644 (file)
 
 import json
 import traceback
-import logging
+import bbctrl
 
-from tornado.web import RequestHandler, HTTPError
+from tornado.web import HTTPError
 import tornado.httpclient
 
 
-log = logging.getLogger('API')
-log.setLevel(logging.DEBUG)
-
-
-class APIHandler(RequestHandler):
-    def __init__(self, app, request, **kwargs):
-        super().__init__(app, request, **kwargs)
-        self.ctrl = app.ctrl
-
-
-    # Override exception logging
-    def log_exception(self, typ, value, tb):
-        if isinstance(value, HTTPError) and value.status_code in (404, 408):
-            return
-
-        log.error(str(value))
-        trace = ''.join(traceback.format_exception(typ, value, tb))
-        log.debug(trace)
-
-
+class APIHandler(bbctrl.RequestHandler):
     def delete(self, *args, **kwargs):
         self.delete_ok(*args, **kwargs)
         self.write_json('ok')
@@ -76,7 +57,7 @@ class APIHandler(RequestHandler):
             try:
                 self.json = tornado.escape.json_decode(self.request.body)
             except ValueError:
-                raise HTTPError(400, 'Unable to parse JSON.')
+                raise HTTPError(400, 'Unable to parse JSON')
 
 
     def set_default_headers(self):
@@ -87,8 +68,13 @@ class APIHandler(RequestHandler):
         e = {}
 
         if 'message' in kwargs: e['message'] = kwargs['message']
+
         elif 'exc_info' in kwargs:
-            e['message'] = str(kwargs['exc_info'][1])
+            typ, value, tb = kwargs['exc_info']
+            if isinstance(value, HTTPError) and value.log_message:
+                e['message'] = value.log_message % value.args
+            else: e['message'] = str(kwargs['exc_info'][1])
+
         else: e['message'] = 'Unknown error'
 
         e['code'] = status_code
index 528f00d0014922094ed0387bba30eb684a17355f..a54354c7e62a65f30a90bc6e8dfe88a05d39e5b7 100644 (file)
 ################################################################################
 
 import serial
-import logging
 import time
 import traceback
 
 import bbctrl
 import bbctrl.Cmd as Cmd
 
-log = logging.getLogger('AVR')
 
 
 class AVR(object):
     def __init__(self, ctrl):
         self.ctrl = ctrl
+        self.log = ctrl.log.get('AVR')
+
         self.sp = None
         self.i2c_addr = ctrl.args.avr_addr
         self.read_cb = None
         self.write_cb = None
 
 
+    def close(self): pass
+
+
     def _start(self):
         try:
             self.sp = serial.Serial(self.ctrl.args.serial, self.ctrl.args.baud,
@@ -53,7 +56,7 @@ class AVR(object):
 
         except Exception as e:
             self.sp = None
-            log.warning('Failed to open serial port: %s', e)
+            self.log.warning('Failed to open serial port: %s', e)
 
         if self.sp is not None:
             self.ctrl.ioloop.add_handler(self.sp, self._serial_handler,
@@ -62,7 +65,7 @@ class AVR(object):
 
     def set_handlers(self, read_cb, write_cb):
         if self.read_cb is not None or self.write_cb is not None:
-            raise Exception('AVR handler already set')
+            raise Exception('Handler already set')
 
         self.read_cb = read_cb
         self.write_cb = write_cb
@@ -87,7 +90,7 @@ class AVR(object):
             self.read_cb(data)
 
         except Exception as e:
-            log.warning('%s: %s', e, data)
+            self.log.warning('%s: %s', e, data)
 
 
     def _serial_handler(self, fd, events):
@@ -96,11 +99,11 @@ class AVR(object):
             if self.ctrl.ioloop.WRITE & events: self._serial_write()
 
         except Exception as e:
-            log.warning('Serial handler error: %s', traceback.format_exc())
+            self.log.warning('Serial handler error: %s', traceback.format_exc())
 
 
     def i2c_command(self, cmd, byte = None, word = None, block = None):
-        log.info('I2C: %s b=%s w=%s d=%s' % (cmd, byte, word, block))
+        self.log.info('I2C: %s b=%s w=%s d=%s' % (cmd, byte, word, block))
         retry = 5
         cmd = ord(cmd[0])
 
@@ -113,10 +116,10 @@ class AVR(object):
                 retry -= 1
 
                 if retry:
-                    log.warning('AVR I2C failed, retrying: %s' % e)
+                    self.log.warning('I2C failed, retrying: %s' % e)
                     time.sleep(0.1)
                     continue
 
                 else:
-                    log.error('AVR I2C failed: %s' % e)
+                    self.log.error('I2C failed: %s' % e)
                     raise
index f06add33e03a150ae3eac655268e8456ee325d2f..02ee250edd84574bd6b3e386220b46974c0ef080 100644 (file)
 #                                                                              #
 ################################################################################
 
-import logging
 import os
 import sys
 import traceback
+import signal
 
 import bbctrl
 import bbctrl.Cmd as Cmd
 
-log = logging.getLogger('AVREmu')
-
 
 class AVREmu(object):
     def __init__(self, ctrl):
         self.ctrl = ctrl
+        self.log = ctrl.log.get('AVREmu')
+
         self.avrOut = None
         self.avrIn = None
         self.i2cOut = None
@@ -47,6 +47,13 @@ class AVREmu(object):
         self.pid = None
 
 
+    def close(self):
+        if self.pid is None: return
+        os.kill(self.pid, signal.SIGINT)
+        os.waitpid(self.pid, 0)
+        self.pid = None
+
+
     def _start(self):
         try:
             if self.pid is not None: os.waitpid(self.pid, 0)
@@ -104,7 +111,7 @@ class AVREmu(object):
         except Exception as e:
             self.pid = None
             self.avrOut, self.avrIn, self.i2cOut = None, None, None
-            log.exception('Failed to start bbemu')
+            self.log.exception('Failed to start bbemu')
 
 
     def set_handlers(self, read_cb, write_cb):
@@ -150,7 +157,8 @@ class AVREmu(object):
                 if not self.continue_write: break
 
         except Exception as e:
-            log.warning('AVR write handler error: %s', traceback.format_exc())
+            self.log.warning('AVR write handler error: %s',
+                             traceback.format_exc())
 
 
     def _avr_read_handler(self, fd, events):
@@ -165,7 +173,7 @@ class AVREmu(object):
             if data is not None: self.read_cb(data)
 
         except Exception as e:
-            log.warning('AVR read handler error: %s %s' %
+            self.log.warning('AVR read handler error: %s %s' %
                         (data, traceback.format_exc()))
 
 
index c0bfddec10f3fed69ff7dd50175aaeb9fa29871b..f8ab73b7537265a83086ef1b4e96286c3a92cc29 100755 (executable)
@@ -28,7 +28,6 @@
 
 import os
 import fcntl
-import logging
 import select
 import struct
 import mmap
@@ -43,8 +42,6 @@ try:
 except:
     import bbctrl.v4l2 as v4l2
 
-log = logging.getLogger('Camera')
-
 
 def array_to_string(a): return ''.join([chr(i) for i in a])
 
@@ -273,13 +270,14 @@ class VideoDevice(object):
         if self.fd is None: return
         try:
             os.close(self.fd)
-        except Exception as e: log.warning('While closing camera: %s', e)
         finally: self.fd = None
 
 
 class Camera(object):
     def __init__(self, ctrl):
         self.ctrl = ctrl
+        self.log = ctrl.log.get('Camera')
+
         self.width = ctrl.args.width
         self.height = ctrl.args.height
         self.fps = ctrl.args.fps
@@ -323,7 +321,7 @@ class Camera(object):
         try:
             self.clients[-1].write_frame(format_frame(frame))
         except Exception as e:
-            log.warning('Failed to write frame to client: %s' % e)
+            self.log.warning('Failed to write frame to client: %s' % e)
 
 
     def _fd_handler(self, fd, events):
@@ -337,7 +335,7 @@ class Camera(object):
         except Exception as e:
             if isinstance(e, BlockingIOError): return
 
-            log.warning('Failed to read from camera.')
+            self.log.warning('Failed to read from camera.')
             self.ctrl.ioloop.remove_handler(fd)
             self.close()
             return
@@ -350,15 +348,15 @@ class Camera(object):
             self.dev = VideoDevice(path)
 
             caps = self.dev.get_info()
-            log.info('%s, %s, %s, %s', caps._driver, caps._card, caps._bus_info,
-                     caps._caps)
+            self.log.info('%s, %s, %s, %s', caps._driver, caps._card,
+                          caps._bus_info, caps._caps)
 
             if caps.capabilities & v4l2.V4L2_CAP_VIDEO_CAPTURE == 0:
                 raise Exception('Video capture not supported.')
 
-            log.info('Formats: %s', self.dev.get_formats())
-            log.info('Sizes: %s', self.dev.get_frame_sizes(self.fourcc))
-            log.info('Audio: %s', self.dev.get_audio())
+            self.log.info('Formats: %s', self.dev.get_formats())
+            self.log.info('Sizes: %s', self.dev.get_frame_sizes(self.fourcc))
+            self.log.info('Audio: %s', self.dev.get_audio())
 
             self.dev.set_format(self.width, self.height, fourcc = self.fourcc)
             self.dev.set_fps(self.fps)
@@ -368,14 +366,21 @@ class Camera(object):
             self.ctrl.ioloop.add_handler(self.dev, self._fd_handler,
                                          self.ctrl.ioloop.READ)
 
-            log.info('Opened camera ' + path)
+            self.log.info('Opened camera ' + path)
 
 
         except Exception as e:
-            log.warning('While loading camera: %s' % e)
-            if not self.dev is None:
-                self.dev.close()
-                self.dev = None
+            self.log.warning('While loading camera: %s' % e)
+            self._close_dev()
+
+
+    def _close_dev(self):
+        if self.dev is None: return
+        try:
+            self.dev.close()
+        except Exception as e: self.log.warning('While closing camera: %s', e)
+
+        self.dev = None
 
 
     def close(self):
@@ -385,19 +390,19 @@ class Camera(object):
             try:
                 self.dev.stop()
             except: pass
-            self.dev.close()
+            self._close_dev()
 
             for client in self.clients:
                 client.write_frame_twice(self.offline_jpg)
 
-            log.info('Closed camera %s' % self.path)
+            self.log.info('Closed camera %s' % self.path)
 
-        except: log.warning('Closing camera')
+        except: self.log.warning('Closing camera')
         finally: self.dev = None
 
 
     def add_client(self, client):
-        log.info('Adding camera client: %d' % len(self.clients))
+        self.log.info('Adding camera client: %d' % len(self.clients))
 
         if len(self.clients):
             self.clients[-1].write_frame_twice(self.in_use_jpg)
@@ -409,7 +414,7 @@ class Camera(object):
 
 
     def remove_client(self, client):
-        log.info('Removing camera client')
+        self.log.info('Removing camera client')
         try:
             self.clients.remove(client)
         except: pass
@@ -422,7 +427,7 @@ class VideoHandler(web.RequestHandler):
 
     def __init__(self, app, request, **kwargs):
         super().__init__(app, request, **kwargs)
-        self.camera = app.ctrl.camera
+        self.camera = app.get_ctrl(self.get_cookie('client-id')).camera
 
 
     @web.asynchronous
@@ -470,9 +475,11 @@ class VideoHandler(web.RequestHandler):
 
 if __name__ == '__main__':
     class Ctrl(object):
-        def __init__(self):
-            from tornado import ioloop
-            self.ioloop = ioloop.IOLoop.current()
+        def __init__(self, args, ioloop):
+            self.args = args
+            self.ioloop = ioloop
+            self.log = bbctrl.log.Log(args, ioloop)
+            self.camera = Camera(self)
 
 
     class RootHandler(web.RequestHandler):
@@ -482,8 +489,8 @@ if __name__ == '__main__':
 
 
     class Web(web.Application):
-        def __init__(self, ctrl):
-            self.ctrl = ctrl
+        def __init__(self, args, ioloop):
+            self.ctrl = Ctrl(args, ioloop)
 
             handlers = [
                 (r'/', RootHandler),
@@ -493,6 +500,10 @@ if __name__ == '__main__':
             web.Application.__init__(self, handlers)
             self.listen(9000, address = '127.0.0.1')
 
+
+        def get_ctrl(self, id = None): return self.ctrl
+
+
     import argparse
     parser = argparse.ArgumentParser(description = 'Camera Server Test')
     parser.add_argument('--width', default = 640, type = int)
@@ -501,11 +512,8 @@ if __name__ == '__main__':
     parser.add_argument('--fourcc', default = 'MJPG')
     args = parser.parse_args()
 
+    from tornado import ioloop
+    ioloop = ioloop.IOLoop.current()
 
-    logging.basicConfig(level = logging.INFO)
-
-    ctrl = Ctrl()
-    ctrl.args = args
-    ctrl.camera = Camera(ctrl)
-    server = Web(ctrl)
-    ctrl.ioloop.start()
+    server = Web(args, ioloop)
+    ioloop.start()
index 3408fef313f0a9981f3e3820ff6f68a2c80cc690..3b5c1af8ecb3346662bd505e313ea7c2f908637d 100644 (file)
@@ -30,9 +30,6 @@
 import struct
 import base64
 import json
-import logging
-
-log = logging.getLogger('Cmd')
 
 # Keep this in sync with AVR code command.def
 SET          = '$'
@@ -261,12 +258,17 @@ def decode_command(cmd):
     elif cmd[0] == FLUSH:   data['type'] = 'flush'
     elif cmd[0] == RESUME:  data['type'] = 'resume'
 
-    print(json.dumps(data))
+    return data
 
 
 def decode(cmd):
     for line in cmd.split('\n'):
-        decode_command(line.strip())
+        yield decode_command(line.strip())
+
+
+def decode_and_print(cmd):
+    for data in decode(cmd):
+        print(json.dumps(data))
 
 
 if __name__ == "__main__":
@@ -274,8 +276,8 @@ if __name__ == "__main__":
 
     if 1 < len(sys.argv):
         for arg in sys.argv[1:]:
-            decode(arg)
+            decode_and_print(arg)
 
     else:
         for line in sys.stdin:
-            decode(line)
+            decode_and_print(line)
index 8e7f3a6a885c3ce951f9ad37eb5b8615e6f40515..742907c75b70c9fad40678657aaaaa40d6040f57 100644 (file)
@@ -26,7 +26,6 @@
 ################################################################################
 
 import serial
-import logging
 import json
 import time
 import traceback
@@ -35,13 +34,12 @@ from collections import deque
 import bbctrl
 import bbctrl.Cmd as Cmd
 
-log = logging.getLogger('Comm')
-
 
 class Comm(object):
     def __init__(self, ctrl, avr):
         self.ctrl = ctrl
         self.avr = avr
+        self.log = self.ctrl.log.get('Comm')
         self.queue = deque()
         self.in_buf = ''
         self.command = None
@@ -57,7 +55,7 @@ class Comm(object):
 
 
     def i2c_command(self, cmd, byte = None, word = None, block = None):
-        log.info('I2C: %s b=%s w=%s d=%s' % (cmd, byte, word, block))
+        self.log.info('I2C: %s b=%s w=%s d=%s' % (cmd, byte, word, block))
         self.avr.i2c_command(cmd, byte, word, block)
 
 
@@ -65,7 +63,7 @@ class Comm(object):
 
 
     def _load_next_command(self, cmd):
-        log.info('< ' + json.dumps(cmd).strip('"'))
+        self.log.info('< ' + json.dumps(cmd).strip('"'))
         self.command = bytes(cmd.strip() + '\n', 'utf-8')
 
 
@@ -114,20 +112,19 @@ class Comm(object):
                 self.queue_command(Cmd.set_axis(axis, position))
 
         except Exception as e:
-            log.warning('AVR reload failed: %s', traceback.format_exc())
+            self.log.warning('AVR reload failed: %s', traceback.format_exc())
             self.ctrl.ioloop.call_later(1, self.connect)
 
 
     def _log_msg(self, msg):
         level = msg.get('level', 'info')
-        if 'where' in msg: extra = {'where': msg['where']}
-        else: extra = None
+        where = msg.get('where')
         msg = msg['msg']
 
-        if   level == 'info':    log.info(msg,    extra = extra)
-        elif level == 'debug':   log.debug(msg,   extra = extra)
-        elif level == 'warning': log.warning(msg, extra = extra)
-        elif level == 'error':   log.error(msg,   extra = extra)
+        if   level == 'info':    self.log.info(msg,    where = where)
+        elif level == 'debug':   self.log.debug(msg,   where = where)
+        elif level == 'warning': self.log.warning(msg, where = where)
+        elif level == 'error':   self.log.error(msg,   where = where)
 
         if level == 'error': self.comm_error()
 
@@ -147,20 +144,20 @@ class Comm(object):
             self.in_buf = self.in_buf[i + 1:]
 
             if line:
-                log.info('> ' + line)
+                self.log.info('> ' + line)
 
                 try:
                     msg = json.loads(line)
 
                 except Exception as e:
-                    log.warning('%s, data: %s', e, line)
+                    self.log.warning('%s, data: %s', e, line)
                     continue
 
                 if 'variables' in msg: self._update_vars(msg)
                 elif 'msg' in msg: self._log_msg(msg)
 
                 elif 'firmware' in msg:
-                    log.info('AVR firmware rebooted')
+                    self.log.info('AVR firmware rebooted')
                     self.connect()
 
                 else:
@@ -194,5 +191,5 @@ class Comm(object):
             self.queue_command(Cmd.HELP) # Load AVR commands and variables
 
         except Exception as e:
-            log.warning('Connect failed: %s', e)
+            self.log.warning('Connect failed: %s', e)
             self.ctrl.ioloop.call_later(1, self.connect)
index ee20ee82b5d48540885c39dfcbb72d9b87e37054..7f89a89a91d73e05fa5767600e474baa58a483d8 100644 (file)
 #                                                                              #
 ################################################################################
 
-import logging
+import bbctrl
 from collections import deque
 
-log = logging.getLogger('CmdQ')
-log.setLevel(logging.WARNING)
-
 
 # 16-bit less with wrap around
 def id_less(a, b): return (1 << 15) < (a - b) & ((1 << 16) - 1)
 
 
 class CommandQueue():
-    def __init__(self):
+    def __init__(self, ctrl):
+        self.log = ctrl.log.get('CmdQ')
+        self.log.set_level(bbctrl.log.WARNING)
+
         self.lastEnqueueID = 0
         self.releaseID = 0
         self.q = deque()
@@ -53,7 +53,7 @@ class CommandQueue():
 
 
     def enqueue(self, id, cb, *args, **kwargs):
-        log.info('add(#%d) releaseID=%d', id, self.releaseID)
+        self.log.info('add(#%d) releaseID=%d', id, self.releaseID)
         self.lastEnqueueID = id
         self.q.append([id, cb, args, kwargs])
         self._release()
@@ -66,19 +66,19 @@ class CommandQueue():
             # Execute commands <= releaseID
             if id_less(self.releaseID, id): return
 
-            log.info('releasing id=%d' % id)
+            self.log.info('releasing id=%d' % id)
             self.q.popleft()
 
             try:
                 if cb is not None: cb(*args, **kwargs)
             except Exception as e:
-                log.exception('During command queue callback')
+                self.log.exception('During command queue callback')
 
 
 
     def release(self, id):
         if id and not id_less(self.releaseID, id):
-            log.debug('id out of order %d <= %d' % (id, self.releaseID))
+            self.log.debug('id out of order %d <= %d' % (id, self.releaseID))
         self.releaseID = id
 
         self._release()
index 9d353cef43c41a8ea289f77c6876158c7a1070aa..ad146c27c756fd0478ab8b8ec3790dda39450cf9 100644 (file)
 
 import os
 import json
-import logging
 import pkg_resources
 import subprocess
 import copy
 from pkg_resources import Requirement, resource_filename
 
-log = logging.getLogger('Config')
-
 
 def get_resource(path):
     return resource_filename(Requirement.parse('bbctrl'), 'bbctrl/' + path)
@@ -43,6 +40,8 @@ def get_resource(path):
 class Config(object):
     def __init__(self, ctrl):
         self.ctrl = ctrl
+        self.log = ctrl.log.get('Config')
+
         self.values = {}
 
         try:
@@ -53,7 +52,7 @@ class Config(object):
                       encoding = 'utf-8') as f:
                 self.template = json.load(f)
 
-        except Exception as e: log.exception(e)
+        except Exception as e: self.log.exception(e)
 
 
     def get(self, name, default = None):
@@ -64,22 +63,20 @@ class Config(object):
         return self.values.get(name, {}).get(str(index), None)
 
 
-    def load_path(self, path):
-        with open(path, 'r') as f:
-            return json.load(f)
+    def load(self):
+        path = self.ctrl.get_path('config.json')
 
-
-    def load(self, path = 'config.json'):
         try:
-            if os.path.exists(path): config = self.load_path(path)
+            if os.path.exists(path):
+                with open(path, 'r') as f: config = json.load(f)
             else: config = {'version': self.version}
 
             try:
                 self.upgrade(config)
-            except Exception as e: log.exception(e)
+            except Exception as e: self.log.exception(e)
 
         except Exception as e:
-            log.warning('%s', e)
+            self.log.warning('%s', e)
             config = {'version': self.version}
 
         self._defaults(config)
@@ -169,13 +166,13 @@ class Config(object):
         self.upgrade(config)
         self._update(config, False)
 
-        with open('config.json', 'w') as f:
+        with open(self.ctrl.get_path('config.json'), 'w') as f:
             json.dump(config, f)
 
         subprocess.check_call(['sync'])
 
         self.ctrl.preplanner.invalidate_all()
-        log.info('Saved')
+        self.log.info('Saved')
 
 
     def reset(self):
@@ -235,37 +232,3 @@ class Config(object):
 
 
     def reload(self): self._update(self.load(), True)
-
-
-
-if __name__ == "__main__":
-    import sys
-    import argparse
-
-    class State(object):
-        def config(self, name, value): print('config(%s, %s)' % (name, value))
-
-
-    class Ctrl(object):
-        def __init__(self):
-            self.state = State()
-
-    parser = argparse.ArgumentParser(description = 'Buildbotics Config Test')
-
-    parser.add_argument('configs', metavar = 'CONFIG', nargs = '*',
-                        help = 'Configuration file')
-    parser.add_argument('-u', '--update', action = 'store_true',
-                        help = 'Update config')
-    args = parser.parse_args()
-
-    config = Config(Ctrl())
-
-    def do_cfg(path):
-        cfg = config.load(path)
-        if args.update: config._update(cfg, True)
-        else: print(json.dumps(cfg, sort_keys = True, indent = 2))
-
-    if len(args.configs):
-        for path in args.configs: do_cfg(path)
-
-    else: do_cfg('')
index 4b447d0faa3e3f91b3fe587ba51ed20d9dc27a9b..5e5a7866f97da15943af9083e397d9fd5ff69e22 100644 (file)
 #                                                                              #
 ################################################################################
 
-import logging
+import os
 import bbctrl
 
 
-log = logging.getLogger('Ctrl')
-
-
-
 class Ctrl(object):
-    def __init__(self, args, ioloop):
+    def __init__(self, args, ioloop, id):
         self.args = args
-        self.ioloop = ioloop
+        self.ioloop = bbctrl.IOLoop(ioloop)
+        self.id = id
 
-        self.msgs = bbctrl.Messages(self)
+        if id and not os.path.exists(id): os.mkdir(id)
+
+        self.log = bbctrl.log.Log(self)
         self.state = bbctrl.State(self)
         self.config = bbctrl.Config(self)
-        self.web = bbctrl.Web(self)
 
         try:
-            if args.demo: avr = bbctrl.AVREmu(self)
-            else: avr = bbctrl.AVR(self)
+            if args.demo: self.avr = bbctrl.AVREmu(self)
+            else: self.avr = bbctrl.AVR(self)
 
             self.i2c = bbctrl.I2C(args.i2c_port, args.demo)
             self.lcd = bbctrl.LCD(self)
-            self.mach = bbctrl.Mach(self, avr)
+            self.mach = bbctrl.Mach(self, self.avr)
             self.preplanner = bbctrl.Preplanner(self)
             self.jog = bbctrl.Jog(self)
             self.pwr = bbctrl.Pwr(self)
@@ -63,7 +61,21 @@ class Ctrl(object):
             if not args.disable_camera:
                 self.camera = bbctrl.Camera(self)
 
-        except Exception as e: log.exception(e)
+        except Exception as e: self.log.get('Ctrl').exception(e)
+
+
+    def get_path(self, dir = None, filename = None):
+        path = './' + self.id if self.id else '.'
+        path = path if dir is None else (path + '/' + dir)
+        return path if filename is None else (path + '/' + filename)
+
+
+    def get_upload(self, filename = None):
+        return self.get_path('upload', filename)
+
+
+    def get_plan(self, filename = None):
+        return self.get_path('plans', filename)
 
 
     def configure(self):
@@ -78,3 +90,5 @@ class Ctrl(object):
 
     def close(self):
         if not self.camera is None: self.camera.close()
+        self.ioloop.close()
+        self.avr.close()
index 52cf476a39679b240ab4b76bfec653ad945dee43..0d8a7065fa2f0944c1b478f26edb74f2521318f6 100644 (file)
@@ -29,14 +29,10 @@ import os
 import bbctrl
 import glob
 import html
-import logging
 from tornado import gen
 from tornado.web import HTTPError
 
 
-log = logging.getLogger('FileHandler')
-
-
 def safe_remove(path):
     try:
         os.unlink(path)
@@ -50,30 +46,30 @@ class FileHandler(bbctrl.APIHandler):
     def delete_ok(self, filename):
         if not filename:
             # Delete everything
-            for path in glob.glob('upload/*'): safe_remove(path)
-            self.ctrl.preplanner.delete_all_plans()
-            self.ctrl.state.clear_files()
+            for path in glob.glob(self.get_upload('*')): safe_remove(path)
+            self.get_ctrl().preplanner.delete_all_plans()
+            self.get_ctrl().state.clear_files()
 
         else:
             # Delete a single file
             filename = os.path.basename(filename)
-            safe_remove('upload/' + filename)
-            self.ctrl.preplanner.delete_plans(filename)
-            self.ctrl.state.remove_file(filename)
+            safe_remove(self.get_upload(filename))
+            self.get_ctrl().preplanner.delete_plans(filename)
+            self.get_ctrl().state.remove_file(filename)
 
 
     def put_ok(self, *args):
         gcode = self.request.files['gcode'][0]
         filename = os.path.basename(gcode['filename'])
 
-        if not os.path.exists('upload'): os.mkdir('upload')
+        if not os.path.exists(self.get_upload()): os.mkdir(self.get_upload())
 
-        with open('upload/' + filename, 'wb') as f:
+        with open(self.get_upload(filename), 'wb') as f:
             f.write(gcode['body'])
 
-        self.ctrl.preplanner.invalidate(filename)
-        self.ctrl.state.add_file(filename)
-        log.info('GCode received: ' + filename)
+        self.get_ctrl().preplanner.invalidate(filename)
+        self.get_ctrl().state.add_file(filename)
+        self.get_log('FileHandler').info('GCode received: ' + filename)
 
 
     @gen.coroutine
@@ -81,7 +77,7 @@ class FileHandler(bbctrl.APIHandler):
         if not filename: raise HTTPError(400, 'Missing filename')
         filename = os.path.basename(filename)
 
-        with open('upload/' + filename, 'r') as f:
+        with open(self.get_upload(filename), 'r') as f:
             self.write(f.read())
 
-        self.ctrl.state.select_file(filename)
+        self.get_ctrl().state.select_file(filename)
diff --git a/src/py/bbctrl/GCodeStream.py b/src/py/bbctrl/GCodeStream.py
deleted file mode 100644 (file)
index e09b2d0..0000000
+++ /dev/null
@@ -1,86 +0,0 @@
-################################################################################
-#                                                                              #
-#                This file is part of the Buildbotics firmware.                #
-#                                                                              #
-#                  Copyright (c) 2015 - 2018, Buildbotics LLC                  #
-#                             All rights reserved.                             #
-#                                                                              #
-#     This file ("the software") is free software: you can redistribute it     #
-#     and/or modify it under the terms of the GNU General Public License,      #
-#      version 2 as published by the Free Software Foundation. You should      #
-#      have received a copy of the GNU General Public License, version 2       #
-#     along with the software. If not, see <http://www.gnu.org/licenses/>.     #
-#                                                                              #
-#     The software is distributed in the hope that it will be useful, but      #
-#          WITHOUT ANY WARRANTY; without even the implied warranty of          #
-#      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU       #
-#               Lesser General Public License for more details.                #
-#                                                                              #
-#       You should have received a copy of the GNU Lesser General Public       #
-#                License along with the software.  If not, see                 #
-#                       <http://www.gnu.org/licenses/>.                        #
-#                                                                              #
-#                For information regarding this software email:                #
-#                  "Joseph Coffland" <joseph@buildbotics.com>                  #
-#                                                                              #
-################################################################################
-
-import re
-import logging
-
-
-log = logging.getLogger('GCode')
-
-
-class GCodeStream():
-    comment1RE = re.compile(r';.*')
-    comment2RE = re.compile(r'\(([^\)]*)\)')
-
-
-    def __init__(self, path):
-        self.path = path
-        self.f = None
-
-        self.open()
-
-
-    def close(self):
-        if self.f is not None:
-            self.f.close()
-            self.f = None
-
-
-    def open(self):
-        self.close()
-
-        self.line = 0
-        self.f = open('upload' + self.path, 'r')
-
-
-    def reset(self): self.open()
-
-
-    def comment(self, s):
-        log.debug('Comment: %s', s)
-
-
-    def next(self):
-        line = self.f.readline()
-        if line is None or line == '': return
-
-        # Remove comments
-        line = self.comment1RE.sub('', line)
-
-        for comment in self.comment2RE.findall(line):
-            self.comment(comment)
-
-        line = self.comment2RE.sub(' ', line)
-
-        # Remove space
-        line = line.strip()
-
-        # Append line number
-        self.line += 1
-        line += ' N%d' % self.line
-
-        return line
diff --git a/src/py/bbctrl/IOLoop.py b/src/py/bbctrl/IOLoop.py
new file mode 100644 (file)
index 0000000..641ab45
--- /dev/null
@@ -0,0 +1,72 @@
+################################################################################
+#                                                                              #
+#                This file is part of the Buildbotics firmware.                #
+#                                                                              #
+#                  Copyright (c) 2015 - 2018, Buildbotics LLC                  #
+#                             All rights reserved.                             #
+#                                                                              #
+#     This file ("the software") is free software: you can redistribute it     #
+#     and/or modify it under the terms of the GNU General Public License,      #
+#      version 2 as published by the Free Software Foundation. You should      #
+#      have received a copy of the GNU General Public License, version 2       #
+#     along with the software. If not, see <http://www.gnu.org/licenses/>.     #
+#                                                                              #
+#     The software is distributed in the hope that it will be useful, but      #
+#          WITHOUT ANY WARRANTY; without even the implied warranty of          #
+#      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU       #
+#               Lesser General Public License for more details.                #
+#                                                                              #
+#       You should have received a copy of the GNU Lesser General Public       #
+#                License along with the software.  If not, see                 #
+#                       <http://www.gnu.org/licenses/>.                        #
+#                                                                              #
+#                For information regarding this software email:                #
+#                  "Joseph Coffland" <joseph@buildbotics.com>                  #
+#                                                                              #
+################################################################################
+
+import tornado.ioloop
+import bbctrl
+
+
+class IOLoop(object):
+    READ = tornado.ioloop.IOLoop.READ
+    WRITE = tornado.ioloop.IOLoop.WRITE
+    ERROR = tornado.ioloop.IOLoop.ERROR
+
+
+    def __init__(self, ioloop):
+        self.ioloop = ioloop
+        self.fds = set()
+        self.handles = set()
+
+
+    def close(self):
+        for fd in self.fds: self.ioloop.remove_handler(fd)
+        for h in self.handles: self.ioloop.remove_timeout(h)
+
+
+    def add_handler(self, fd, handler, events):
+        self.ioloop.add_handler(fd, handler, events)
+        if hasattr(fd, 'fileno'): fd = fd.fileno()
+        self.fds.add(fd)
+
+
+    def remove_handler(self, fd):
+        self.ioloop.remove_handler(fd)
+        if hasattr(fd, 'fileno'): fd = fd.fileno()
+        self.fds.remove(fd)
+
+
+    def update_handler(self, fd, events): self.ioloop.update_handler(fd, events)
+
+
+    def call_later(self, delay, callback, *args, **kwargs):
+        h = self.ioloop.call_later(delay, callback, *args, **kwargs)
+        self.handles.add(h)
+        return h
+
+
+    def remove_timeout(self, h):
+        self.ioloop.remove_timeout(h)
+        self.handles.remove(h)
index 04a5d08950781f3f4f3ffdd3561de59b6fcf159a..c4544e21785f8e15ab4c93c7c8fc13d2f51aa028 100644 (file)
 
 import inevent
 from inevent.Constants import *
-import logging
-
-log = logging.getLogger('Jog')
 
 
 # Listen for input events
 class Jog(inevent.JogHandler):
     def __init__(self, ctrl):
         self.ctrl = ctrl
+        self.log = ctrl.log.get('Jog')
 
         config = {
             "Logitech Logitech RumblePad 2 USB": {
@@ -81,7 +79,7 @@ class Jog(inevent.JogHandler):
                 self.ctrl.mach.jog(axes)
 
             except Exception as e:
-                log.warning('Jog: %s', e)
+                self.log.warning('Jog: %s', e)
 
         self.ctrl.ioloop.call_later(0.25, self.callback)
 
index f9547261101505f72cdebeae3e727b06f4b057b9..0d2cdcbcd28083f782d45cd38622e579e0add057 100644 (file)
 
 import lcd
 import atexit
-import logging
 from tornado.ioloop import PeriodicCallback
 
 
-log = logging.getLogger('LCD')
-
-
 class LCDPage:
     def __init__(self, lcd, text = None):
         self.lcd = lcd
@@ -77,6 +73,7 @@ class LCDPage:
 class LCD:
     def __init__(self, ctrl):
         self.ctrl = ctrl
+        self.log = ctrl.log.get('LCD')
 
         self.addrs = self.ctrl.args.lcd_addr
         self.addr = self.addrs[0]
@@ -102,7 +99,7 @@ class LCD:
             self.load_page(LCDPage(self, msg))
             self._update()
         except IOError as e:
-            log.warning('LCD communication failed: %s' % e)
+            self.log.warning('LCD communication failed: %s' % e)
 
 
     def new_screen(self):
@@ -189,7 +186,7 @@ class LCD:
             self.addr = self.addrs[self.addr_num]
             self.lcd = None
 
-            log.warning('LCD communication failed, ' +
+            self.log.warning('LCD communication failed, ' +
                         'retrying on address 0x%02x: %s' % (self.addr, e))
 
             self.reset = True
diff --git a/src/py/bbctrl/Log.py b/src/py/bbctrl/Log.py
new file mode 100644 (file)
index 0000000..c1a5205
--- /dev/null
@@ -0,0 +1,138 @@
+################################################################################
+#                                                                              #
+#                This file is part of the Buildbotics firmware.                #
+#                                                                              #
+#                  Copyright (c) 2015 - 2018, Buildbotics LLC                  #
+#                             All rights reserved.                             #
+#                                                                              #
+#     This file ("the software") is free software: you can redistribute it     #
+#     and/or modify it under the terms of the GNU General Public License,      #
+#      version 2 as published by the Free Software Foundation. You should      #
+#      have received a copy of the GNU General Public License, version 2       #
+#     along with the software. If not, see <http://www.gnu.org/licenses/>.     #
+#                                                                              #
+#     The software is distributed in the hope that it will be useful, but      #
+#          WITHOUT ANY WARRANTY; without even the implied warranty of          #
+#      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU       #
+#               Lesser General Public License for more details.                #
+#                                                                              #
+#       You should have received a copy of the GNU Lesser General Public       #
+#                License along with the software.  If not, see                 #
+#                       <http://www.gnu.org/licenses/>.                        #
+#                                                                              #
+#                For information regarding this software email:                #
+#                  "Joseph Coffland" <joseph@buildbotics.com>                  #
+#                                                                              #
+################################################################################
+
+import os
+import sys
+import datetime
+import traceback
+import pkg_resources
+from inspect import getframeinfo, stack
+import bbctrl
+
+
+DEBUG    = 0
+INFO     = 1
+WARNING  = 2
+ERROR    = 3
+
+
+def get_level_name(level): return 'debug info warning error'.split()[level]
+
+
+class Logger(object):
+    def __init__(self, log, name, level):
+        self.log = log
+        self.name = name
+        self.level = level
+
+
+    def set_level(self, level): self.level = level
+    def _enabled(self, level): return self.level <= level and level <= ERROR
+
+
+    def _log(self, level, msg, *args, **kwargs):
+        if not self._enabled(level): return
+
+        if not 'where' in kwargs:
+            caller = getframeinfo(stack()[2][0])
+            kwargs['where'] = '%s:%d' % (
+                os.path.basename(caller.filename), caller.lineno)
+
+        if len(args): msg %= args
+
+        self.log._log(msg, level = level, prefix = self.name, **kwargs)
+
+
+    def debug  (self, *args, **kwargs): self._log(DEBUG,   *args, **kwargs)
+    def info   (self, *args, **kwargs): self._log(INFO,    *args, **kwargs)
+    def warning(self, *args, **kwargs): self._log(WARNING, *args, **kwargs)
+    def error  (self, *args, **kwargs): self._log(ERROR,   *args, **kwargs)
+
+
+    def exception(self, *args, **kwargs):
+        msg = traceback.format_exc()
+        if len(args): msg = args[0] % args[1:] + '\n' + msg
+        self._log(ERROR, msg, **kwargs)
+
+
+class Log(object):
+    def __init__(self, ctrl):
+        self.listeners = []
+        self.loggers = {}
+
+        self.level = DEBUG if ctrl.args.verbose else INFO
+
+        if ctrl.args.demo: self.path = ctrl.get_path(filename = 'bbctrl.log')
+        else: self.path = ctrl.args.log
+        self.f = None if self.path is None else open(self.path, 'w')
+
+        # Log header
+        version = pkg_resources.require('bbctrl')[0].version
+        self._log('Log started v%s' % version)
+        self._log_time(ctrl.ioloop)
+
+
+    def get_path(self): return self.path
+
+    def add_listener(self, listener): self.listeners.append(listener)
+    def remove_listener(self, listener): self.listeners.remove(listener)
+
+
+    def get(self, name, level = None):
+        if not name in self.loggers:
+            self.loggers[name] = Logger(self, name, self.level)
+        return self.loggers[name]
+
+
+    def _log_time(self, ioloop):
+        self._log(datetime.datetime.now().strftime('%Y/%m/%d %H:%M:%S'))
+        ioloop.call_later(60 * 60, self._log_time, ioloop)
+
+
+    def broadcast(self, msg):
+        for listener in self.listeners: listener(msg)
+
+
+    def _log(self, msg, level = INFO, prefix = '', where = None):
+        if not msg: return
+
+        hdr = '%s:%s:' % ('DIWE'[level], prefix)
+        s = hdr + ('\n' + hdr).join(msg.split('\n'))
+
+        if self.f is not None:
+            self.f.write(s + '\n')
+            self.f.flush()
+
+        print(s)
+
+        # Broadcast to log listeners
+        if level == INFO: return
+
+        msg = dict(level = get_level_name(level), source = prefix, msg = msg)
+        if where is not None: msg['where'] = where
+
+        self.broadcast(dict(log = msg))
index 6bcbd137bcde4d6bf63972f478830503757289bc..50b172a7365c886c16b45a81a2d2537f0c3d06ee 100644 (file)
 #                                                                              #
 ################################################################################
 
-import logging
-
 import bbctrl
 from bbctrl.Comm import Comm
 import bbctrl.Cmd as Cmd
 from tornado.ioloop import PeriodicCallback
 
-log = logging.getLogger('Mach')
-
 
 # Axis homing procedure:
 #
@@ -65,7 +61,7 @@ for more information.\
 def overrides(interface_class):
     def overrider(method):
         if not method.__name__ in dir(interface_class):
-            log.warning('%s does not override %s' % (
+            raise Exception('%s does not override %s' % (
                 method.__name__, interface_class.__name__))
 
         return method
@@ -78,6 +74,8 @@ class Mach(Comm):
         super().__init__(ctrl, avr)
 
         self.ctrl = ctrl
+        self.mlog = self.ctrl.log.get('Mach')
+
         self.planner = bbctrl.Planner(ctrl)
         self.unpausing = False
         self.last_cycle = 'idle'
@@ -142,7 +140,7 @@ class Mach(Comm):
         for motor in range(4):
             key = '%ddf' % motor
             if key in update and update[key] & 0x1f:
-                log.error(motor_fault_error % motor)
+                self.mlog.error(motor_fault_error % motor)
 
         # Update cycle now, if it has changed
         self._update_cycle()
@@ -172,7 +170,7 @@ class Mach(Comm):
 
     def _unpause(self):
         pause_reason = self._get_pause_reason()
-        log.info('Unpause: ' + pause_reason)
+        self.mlog.info('Unpause: ' + pause_reason)
 
         if pause_reason == 'User stop':
             self.planner.stop()
@@ -214,7 +212,7 @@ class Mach(Comm):
     def _query_var(self, cmd):
         equal = cmd.find('=')
         if equal == -1:
-            log.info('%s=%s' % (cmd, self.ctrl.state.get(cmd[1:])))
+            self.mlog.info('%s=%s' % (cmd, self.ctrl.state.get(cmd[1:])))
 
         else:
             name, value = cmd[1:equal], cmd[equal + 1:]
@@ -251,34 +249,36 @@ class Mach(Comm):
     def home(self, axis, position = None):
         state = self.ctrl.state
 
-        if position is not None: self.mdi('G28.3 %c%f' % (axis, position))
-
-        else:
+        if axis is None: axes = 'zxyabc' # TODO This should be configurable
+        else: axes = '%c' % axis
+
+        for axis in axes:
+            enabled = state.is_axis_enabled(axis)
+            mode = state.axis_homing_mode(axis)
+
+            # If this is not a request to home a specific axis and the
+            # axis is disabled or in manual homing mode, don't show any
+            # warnings
+            if 1 < len(axes) and (not enabled or mode == 'manual'):
+                continue
+
+            # Error when axes cannot be homed
+            reason = state.axis_home_fail_reason(axis)
+            if reason is not None:
+                self.mlog.error('Cannot home %s axis: %s' % (
+                    axis.upper(), reason))
+                continue
+
+            if mode == 'manual':
+                if position is None: raise Exception('Position not set')
+                self.mdi('G28.3 %c%f' % (axis, position))
+                continue
+
+            # Home axis
+            self.mlog.info('Homing %s axis' % axis)
             self._begin_cycle('homing')
-
-            if axis is None: axes = 'zxyabc' # TODO This should be configurable
-            else: axes = '%c' % axis
-
-            for axis in axes:
-                # If this is not a request to home a specific axis and the
-                # axis is disabled or in manual homing mode, don't show any
-                # warnings
-                if 1 < len(axes) and (
-                        not state.is_axis_enabled(axis) or
-                        state.axis_homing_mode(axis) == 'manual'):
-                    continue
-
-                # Error when axes cannot be homed
-                reason = state.axis_home_fail_reason(axis)
-                if reason is not None:
-                    log.error('Cannot home %s axis: %s' % (
-                        axis.upper(), reason))
-                    continue
-
-                # Home axis
-                log.info('Homing %s axis' % axis)
-                self.planner.mdi(axis_homing_procedure % {'axis': axis}, False)
-                super().resume()
+            self.planner.mdi(axis_homing_procedure % {'axis': axis}, False)
+            super().resume()
 
 
     def unhome(self, axis): self.mdi('G28.2 %c0' % axis)
@@ -295,7 +295,7 @@ class Mach(Comm):
         filename = self.ctrl.state.get('selected', '')
         if not filename: return
         self._begin_cycle('running')
-        self.planner.load('upload/' + filename)
+        self.planner.load(filename)
         super().resume()
 
 
diff --git a/src/py/bbctrl/Messages.py b/src/py/bbctrl/Messages.py
deleted file mode 100644 (file)
index b6ae595..0000000
+++ /dev/null
@@ -1,69 +0,0 @@
-################################################################################
-#                                                                              #
-#                This file is part of the Buildbotics firmware.                #
-#                                                                              #
-#                  Copyright (c) 2015 - 2018, Buildbotics LLC                  #
-#                             All rights reserved.                             #
-#                                                                              #
-#     This file ("the software") is free software: you can redistribute it     #
-#     and/or modify it under the terms of the GNU General Public License,      #
-#      version 2 as published by the Free Software Foundation. You should      #
-#      have received a copy of the GNU General Public License, version 2       #
-#     along with the software. If not, see <http://www.gnu.org/licenses/>.     #
-#                                                                              #
-#     The software is distributed in the hope that it will be useful, but      #
-#          WITHOUT ANY WARRANTY; without even the implied warranty of          #
-#      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU       #
-#               Lesser General Public License for more details.                #
-#                                                                              #
-#       You should have received a copy of the GNU Lesser General Public       #
-#                License along with the software.  If not, see                 #
-#                       <http://www.gnu.org/licenses/>.                        #
-#                                                                              #
-#                For information regarding this software email:                #
-#                  "Joseph Coffland" <joseph@buildbotics.com>                  #
-#                                                                              #
-################################################################################
-
-import os
-import logging
-import bbctrl
-
-
-log = logging.getLogger('Msgs')
-
-
-class Messages(logging.Handler):
-    def __init__(self, ctrl):
-        logging.Handler.__init__(self)
-
-        self.ctrl = ctrl
-        self.listeners = []
-
-        debug = os.path.exists('/etc/bbctrl-dev-mode')
-        self.setLevel(logging.DEBUG if debug else logging.WARNING)
-
-        logging.getLogger().addHandler(self)
-
-
-    def add_listener(self, listener): self.listeners.append(listener)
-    def remove_listener(self, listener): self.listeners.remove(listener)
-
-
-    def broadcast(self, msg):
-        for listener in self.listeners:
-            listener(msg)
-
-
-    # From logging.Handler
-    def emit(self, record):
-        if record.levelno == logging.INFO: return
-
-        msg = dict(level = record.levelname.lower(),
-                   source = record.name,
-                   msg = record.getMessage())
-
-        if hasattr(record, 'where'): msg['where'] = record.where
-        else: msg['where'] = '%s:%d' % (record.filename, record.lineno)
-
-        self.broadcast({'log': msg})
index b4a101f7075db947afd20606ddffc781e3828bbe..5d272b72788c848d61871c1d0fd4fda2ee3e3872 100644 (file)
 import json
 import math
 import re
-import logging
 import time
 from collections import deque
 import camotics.gplan as gplan # pylint: disable=no-name-in-module,import-error
 import bbctrl.Cmd as Cmd
 from bbctrl.CommandQueue import CommandQueue
 
-log = logging.getLogger('Planner')
 
 reLogLine = re.compile(
     r'^(?P<level>[A-Z])[0-9 ]:'
@@ -58,7 +56,8 @@ def log_json(o): return json.dumps(log_floats(o))
 class Planner():
     def __init__(self, ctrl):
         self.ctrl = ctrl
-        self.cmdq = CommandQueue()
+        self.log = ctrl.log.get('Planner')
+        self.cmdq = CommandQueue(ctrl)
 
         ctrl.state.add_listener(self._update)
 
@@ -114,7 +113,7 @@ class Planner():
 
         if overrides: cfg['overrides'] = overrides
 
-        log.info('Config:' + log_json(cfg))
+        self.log.info('Config:' + log_json(cfg))
 
         return cfg
 
@@ -133,7 +132,7 @@ class Planner():
             value = self.ctrl.state.get(name[1:], 0)
             if units == 'IMPERIAL': value /= 25.4 # Assume metric
 
-        log.info('Get: %s=%s (units=%s)' % (name, value, units))
+        self.log.info('Get: %s=%s (units=%s)' % (name, value, units))
 
         return value
 
@@ -153,19 +152,16 @@ class Planner():
 
         if line is not None: line = int(line)
         if column is not None: column = int(column)
-        if where: extra = dict(where = where)
-        else: extra = None
 
-        if   level == 'I': log.info    (msg, extra = extra)
-        elif level == 'D': log.debug   (msg, extra = extra)
-        elif level == 'W': log.warning (msg, extra = extra)
-        elif level == 'E': log.error   (msg, extra = extra)
-        elif level == 'C': log.critical(msg, extra = extra)
-        else: log.error('Could not parse planner log line: ' + line)
+        if   level == 'I': self.log.info    (msg, where = where)
+        elif level == 'D': self.log.debug   (msg, where = where)
+        elif level == 'W': self.log.warning (msg, where = where)
+        elif level == 'E': self.log.error   (msg, where = where)
+        else: self.log.error('Could not parse planner log line: ' + line)
 
 
     def _enqueue_set_cmd(self, id, name, value):
-        log.info('set(#%d, %s, %s)', id, name, value)
+        self.log.info('set(#%d, %s, %s)', id, name, value)
         self.cmdq.enqueue(id, self.ctrl.state.set, name, value)
 
 
@@ -215,7 +211,7 @@ class Planner():
     def __encode(self, block):
         type, id = block['type'], block['id']
 
-        if type != 'set': log.info('Cmd:' + log_json(block))
+        if type != 'set': self.log.info('Cmd:' + log_json(block))
 
         if type == 'line':
             self._enqueue_line_time(block)
@@ -228,7 +224,7 @@ class Planner():
 
             if name == 'message':
                 msg = dict(message = value)
-                self.cmdq.enqueue(id, self.ctrl.msgs.broadcast, msg)
+                self.cmdq.enqueue(id, self.ctrl.log.broadcast, msg)
 
             if name in ['line', 'tool']: self._enqueue_set_cmd(id, name, value)
             if name == 'speed': return Cmd.speed(value)
@@ -300,13 +296,14 @@ class Planner():
 
 
     def mdi(self, cmd, with_limits = True):
-        log.info('MDI:' + cmd)
+        self.log.info('MDI:' + cmd)
         self.planner.load_string(cmd, self.get_config(True, with_limits))
         self.reset_times()
 
 
     def load(self, path):
-        log.info('GCode:' + path)
+        path = self.ctrl.get_path('upload', path)
+        self.log.info('GCode:' + path)
         self.planner.load(path, self.get_config(False, True))
         self.reset_times()
 
@@ -316,8 +313,8 @@ class Planner():
             self.planner.stop()
             self.cmdq.clear()
 
-        except Exception as e:
-            log.exception(e)
+        except:
+            self.log.exception()
             self.reset()
 
 
@@ -326,15 +323,15 @@ class Planner():
             id = self.ctrl.state.get('id')
             position = self.ctrl.state.get_position()
 
-            log.info('Planner restart: %d %s' % (id, log_json(position)))
+            self.log.info('Planner restart: %d %s' % (id, log_json(position)))
 
             self.cmdq.clear()
             self.cmdq.release(id)
             self._plan_time_restart()
             self.planner.restart(id, position)
 
-        except Exception as e:
-            log.exception(e)
+        except:
+            self.log.exception()
             self.stop()
 
 
@@ -345,6 +342,6 @@ class Planner():
                 cmd = self._encode(cmd)
                 if cmd is not None: return cmd
 
-        except Exception as e:
-            log.exception(e)
+        except:
+            self.log.exception()
             self.stop()
index b2aaafc94a68b214145a65eb72ef53c56961822a..9a706e667f3c3a9970411e1de695d27ebfb31a60 100644 (file)
@@ -26,7 +26,6 @@
 ################################################################################
 
 import os
-import logging
 import time
 import json
 import hashlib
@@ -39,16 +38,12 @@ from tornado import gen
 import bbctrl
 
 
-log = logging.getLogger('Preplanner')
-
-
 def hash_dump(o):
     s = json.dumps(o, separators = (',', ':'), sort_keys = True)
     return s.encode('utf8')
 
 
 def plan_hash(path, config):
-    path = 'upload/' + path
     h = hashlib.sha256()
     h.update('v4'.encode('utf8'))
     h.update(hash_dump(config))
@@ -68,10 +63,13 @@ class Preplanner(object):
     def __init__(self, ctrl, threads = 4, max_plan_time = 60 * 60 * 24,
                  max_loop_time = 300):
         self.ctrl = ctrl
+        self.log = ctrl.log.get('Preplanner')
+
         self.max_plan_time = max_plan_time
         self.max_loop_time = max_loop_time
 
-        if not os.path.exists('plans'): os.mkdir('plans')
+        path = self.ctrl.get_plan()
+        if not os.path.exists(path): os.mkdir(path)
 
         self.started = Future()
 
@@ -82,7 +80,7 @@ class Preplanner(object):
 
     def start(self):
         if not self.started.done():
-            log.info('Preplanner started')
+            self.log.info('Preplanner started')
             self.started.set_result(True)
 
 
@@ -101,7 +99,7 @@ class Preplanner(object):
 
 
     def delete_all_plans(self):
-        files = glob.glob('plans/*')
+        files = glob.glob(self.ctrl.get_plan('*'))
 
         for path in files:
             try:
@@ -112,7 +110,7 @@ class Preplanner(object):
 
 
     def delete_plans(self, filename):
-        files = glob.glob('plans/' + filename + '.*')
+        files = glob.glob(self.ctrl.get_plan(filename + '.*'))
 
         for path in files:
             try:
@@ -158,7 +156,7 @@ class Preplanner(object):
 
 
     def _clean_plans(self, filename, max = 2):
-        plans = glob.glob('plans/' + filename + '.*')
+        plans = glob.glob(self.ctrl.get_plan(filename + '.*'))
         if len(plans) <= max: return
 
         # Delete oldest plans
@@ -181,8 +179,8 @@ class Preplanner(object):
         try:
             os.nice(5)
 
-            hid = plan_hash(filename, config)
-            base = 'plans/' + filename + '.' + hid
+            hid = plan_hash(self.ctrl.get_upload(filename), config)
+            base = self.ctrl.get_plan(filename + '.' + hid)
             files = [
                 base + '.json', base + '.positions.gz', base + '.speeds.gz']
 
@@ -193,7 +191,7 @@ class Preplanner(object):
             if not found:
                 self._clean_plans(filename) # Clean up old plans
 
-                path = os.path.abspath('upload/' + filename)
+                path = os.path.abspath(self.ctrl.get_upload(filename))
                 with tempfile.TemporaryDirectory() as tmpdir:
                     cmd = (
                         '/usr/bin/env', 'python3',
@@ -203,7 +201,7 @@ class Preplanner(object):
                         '--max-loop=%s' % self.max_loop_time
                     )
 
-                    log.info('Running: %s', cmd)
+                    self.log.info('Running: %s', cmd)
 
                     with subprocess.Popen(cmd, stdout = subprocess.PIPE,
                                           stderr = subprocess.PIPE,
@@ -221,7 +219,8 @@ class Preplanner(object):
                         if cancel.is_set(): return
 
                         if proc.returncode:
-                            log.error('Plan failed: ' + errs.decode('utf8'))
+                            self.log.error('Plan failed: ' +
+                                           errs.decode('utf8'))
                             return # Failed
 
                     os.rename(tmpdir + '/meta.json', files[0])
@@ -234,4 +233,4 @@ class Preplanner(object):
 
             return meta, positions, speeds
 
-        except Exception as e: log.exception(e)
+        except Exception as e: self.log.exception(e)
index e7f9d9343ce8707037d673ec664a3dbc71db3074..989a3598ce09b66f50095c6d99a08234a2fa9ba6 100644 (file)
 #                                                                              #
 ################################################################################
 
-import logging
 from tornado.ioloop import PeriodicCallback
 
 import bbctrl
 
-log = logging.getLogger('PWR')
-
 
 # Must match regs in pwr firmware
 TEMP_REG        = 0
@@ -66,6 +63,7 @@ reg_names = 'temp vin vout motor load1 load2 vdd pwr_flags pwr_version'.split()
 class Pwr():
     def __init__(self, ctrl):
         self.ctrl = ctrl
+        self.log = ctrl.log.get('Pwr')
 
         self.i2c_addr = ctrl.args.pwr_addr
         self.regs = [-1] * 9
@@ -89,52 +87,52 @@ class Pwr():
         flags = self.regs[FLAGS_REG]
 
         if self.check_fault('under_voltage', flags & UNDER_VOLTAGE_FLAG):
-            log.error('Device under voltage')
+            self.log.error('Device under voltage')
 
         if self.check_fault('over_voltage', flags & OVER_VOLTAGE_FLAG):
-            log.error('Device over voltage')
+            self.log.error('Device over voltage')
 
         if self.check_fault('over_current', flags & OVER_CURRENT_FLAG):
-            log.error('Device total current limit exceeded')
+            self.log.error('Device total current limit exceeded')
 
         if self.check_fault('sense_error', flags & SENSE_ERROR_FLAG):
-            log.error('Power sense error')
+            self.log.error('Power sense error')
 
         if self.check_fault('shunt_overload', flags & SHUNT_OVERLOAD_FLAG):
-            log.error('Power shunt overload')
+            self.log.error('Power shunt overload')
 
         if self.check_fault('motor_overload', flags & MOTOR_OVERLOAD_FLAG):
-            log.error('Motor power overload')
+            self.log.error('Motor power overload')
 
         if self.check_fault('load1_shutdown', flags & LOAD1_SHUTDOWN_FLAG):
-            log.error('Load 1 over temperature shutdown')
+            self.log.error('Load 1 over temperature shutdown')
 
         if self.check_fault('load2_shutdown', flags & LOAD2_SHUTDOWN_FLAG):
-            log.error('Load 2 over temperature shutdown')
+            self.log.error('Load 2 over temperature shutdown')
 
         if self.check_fault('motor_under_voltage',
                             flags & MOTOR_UNDER_VOLTAGE_FLAG):
-            log.error('Motor under voltage')
+            self.log.error('Motor under voltage')
 
         if self.check_fault('motor_voltage_sense_error',
                             flags & MOTOR_VOLTAGE_SENSE_ERROR_FLAG):
-            log.error('Motor voltage sense error')
+            self.log.error('Motor voltage sense error')
 
         if self.check_fault('motor_current_sense_error',
                             flags & MOTOR_CURRENT_SENSE_ERROR_FLAG):
-            log.error('Motor current sense error')
+            self.log.error('Motor current sense error')
 
         if self.check_fault('load1_sense_error',
                             flags & LOAD1_SENSE_ERROR_FLAG):
-            log.error('Load1 sense error')
+            self.log.error('Load1 sense error')
 
         if self.check_fault('load2_sense_error',
                             flags & LOAD2_SENSE_ERROR_FLAG):
-            log.error('Load2 sense error')
+            self.log.error('Load2 sense error')
 
         if self.check_fault('vdd_current_sense_error',
                             flags & VDD_CURRENT_SENSE_ERROR_FLAG):
-            log.error('Vdd current sense error')
+            self.log.error('Vdd current sense error')
 
 
     def _update_cb(self, now = True):
@@ -167,9 +165,9 @@ class Pwr():
             if i < 6: # Older pwr firmware does not have regs > 5
                 self.failures += 1
                 msg = 'Pwr communication failed at reg %d: %s' % (i, e)
-                if self.failures != 5: log.info(msg)
+                if self.failures != 5: self.log.info(msg)
                 else:
-                    log.warning(msg)
+                    self.log.warning(msg)
                     self.failures = 0
                 return
 
diff --git a/src/py/bbctrl/RequestHandler.py b/src/py/bbctrl/RequestHandler.py
new file mode 100644 (file)
index 0000000..050a425
--- /dev/null
@@ -0,0 +1,63 @@
+################################################################################
+#                                                                              #
+#                This file is part of the Buildbotics firmware.                #
+#                                                                              #
+#                  Copyright (c) 2015 - 2018, Buildbotics LLC                  #
+#                             All rights reserved.                             #
+#                                                                              #
+#     This file ("the software") is free software: you can redistribute it     #
+#     and/or modify it under the terms of the GNU General Public License,      #
+#      version 2 as published by the Free Software Foundation. You should      #
+#      have received a copy of the GNU General Public License, version 2       #
+#     along with the software. If not, see <http://www.gnu.org/licenses/>.     #
+#                                                                              #
+#     The software is distributed in the hope that it will be useful, but      #
+#          WITHOUT ANY WARRANTY; without even the implied warranty of          #
+#      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU       #
+#               Lesser General Public License for more details.                #
+#                                                                              #
+#       You should have received a copy of the GNU Lesser General Public       #
+#                License along with the software.  If not, see                 #
+#                       <http://www.gnu.org/licenses/>.                        #
+#                                                                              #
+#                For information regarding this software email:                #
+#                  "Joseph Coffland" <joseph@buildbotics.com>                  #
+#                                                                              #
+################################################################################
+
+import traceback
+import bbctrl
+
+from tornado.web import HTTPError
+import tornado.web
+
+
+class RequestHandler(tornado.web.RequestHandler):
+    def __init__(self, app, request, **kwargs):
+        super().__init__(app, request, **kwargs)
+        self.app = app
+
+
+    def get_ctrl(self): return self.app.get_ctrl(self.get_cookie('client-id'))
+    def get_log(self, name = 'API'): return self.get_ctrl().log.get(name)
+
+
+    def get_path(self, path = None, filename = None):
+        return self.get_ctrl().get_path(path, filename)
+
+
+    def get_upload(self, filename = None):
+        return self.get_ctrl().get_upload(filename)
+
+
+    # Override exception logging
+    def log_exception(self, typ, value, tb):
+        if (isinstance(value, HTTPError) and
+            value.status_code in (400, 404, 408)): return
+
+        log = self.get_log()
+        log.set_level(bbctrl.log.DEBUG)
+
+        log.error(str(value))
+        trace = ''.join(traceback.format_exception(typ, value, tb))
+        log.debug(trace)
index d00b32452137de28eb656f73b07d8cd4c96bc6fb..bf1e0ab111e0f29e06804de025610731ee67f26c 100644 (file)
@@ -25,7 +25,6 @@
 #                                                                              #
 ################################################################################
 
-import logging
 import traceback
 import copy
 import uuid
@@ -33,12 +32,11 @@ import os
 import bbctrl
 
 
-log = logging.getLogger('State')
-
-
 class State(object):
     def __init__(self, ctrl):
         self.ctrl = ctrl
+        self.log = ctrl.log.get('State')
+
         self.callbacks = {}
         self.changes = {}
         self.listeners = []
@@ -51,6 +49,8 @@ class State(object):
             'tool': 0,
             'feed': 0,
             'speed': 0,
+            'sid': str(uuid.uuid4()),
+            'demo': ctrl.args.demo,
         }
 
         # Add computed variable callbacks for each motor.
@@ -74,8 +74,6 @@ class State(object):
         self.set_callback('metric', lambda name: 1 if self.is_metric() else 0)
         self.set_callback('imperial', lambda name: 0 if self.is_metric() else 1)
 
-        self.set('sid', str(uuid.uuid4()))
-
         self.reset()
         self.load_files()
 
@@ -96,10 +94,16 @@ class State(object):
     def load_files(self):
         self.files = []
 
-        if os.path.exists('upload'):
-            for path in os.listdir('upload'):
-                if os.path.isfile('upload/' + path):
-                    self.files.append(path)
+        upload = self.ctrl.get_upload()
+
+        if not os.path.exists(upload):
+            os.mkdir(upload)
+            from shutil import copy
+            copy(bbctrl.get_resource('http/buildbotics.nc'), upload)
+
+        for path in os.listdir(upload):
+            if os.path.isfile(upload + '/' + path):
+                self.files.append(path)
 
         self.files.sort()
         self.set('files', self.files)
@@ -144,7 +148,7 @@ class State(object):
                 listener(self.changes)
 
             except Exception as e:
-                log.warning('Updating state listener: %s',
+                self.log.warning('Updating state listener: %s',
                             traceback.format_exc())
 
         self.changes = {}
@@ -186,7 +190,9 @@ class State(object):
 
         if name in self.vars: return self.vars[name]
         if name in self.callbacks: return self.callbacks[name](name)
-        if default is None: log.warning('State variable "%s" not found' % name)
+        if default is None:
+            self.log.warning('State variable "%s" not found' % name)
+
         return default
 
 
@@ -318,13 +324,12 @@ class State(object):
 
         mode = self.motor_homing_mode(motor)
 
-        if mode == 'manual': return 'Configured for manual homing'
-
-        if mode == 'switch-min' and not int(self.get(axis + '_ls', 0)):
-            return 'Configured for min switch but switch is disabled'
+        if mode != 'manual':
+            if mode == 'switch-min' and not int(self.get(axis + '_ls', 0)):
+                return 'Configured for min switch but switch is disabled'
 
-        if mode == 'switch-max' and not int(self.get(axis + '_xs', 0)):
-            return 'Configured for max switch but switch is disabled'
+            if mode == 'switch-max' and not int(self.get(axis + '_xs', 0)):
+                return 'Configured for max switch but switch is disabled'
 
         softMin = int(self.get(axis + '_tn', 0))
         softMax = int(self.get(axis + '_tm', 0))
index 5e30ee8aa68cfdb39bf397f1d01d7ff7e74517d0..199801ec50cf4307bb12c5185506c015dd1838a3 100644 (file)
@@ -30,7 +30,6 @@ import sys
 import json
 import tornado
 import sockjs.tornado
-import logging
 import datetime
 import shutil
 import tarfile
@@ -43,8 +42,6 @@ from tornado import web, gen
 import bbctrl
 
 
-log = logging.getLogger('Web')
-
 
 def call_get_output(cmd):
     p = subprocess.Popen(cmd, stdout = subprocess.PIPE)
@@ -81,18 +78,13 @@ def check_password(password):
 
 class RebootHandler(bbctrl.APIHandler):
     def put_ok(self):
-        self.ctrl.lcd.goodbye('Rebooting...')
+        self.get_ctrl().lcd.goodbye('Rebooting...')
         subprocess.Popen('reboot')
 
 
-class LogHandler(tornado.web.RequestHandler):
-    def __init__(self, app, request, **kwargs):
-        super().__init__(app, request, **kwargs)
-        self.filename = app.ctrl.args.log
-
-
+class LogHandler(bbctrl.RequestHandler):
     def get(self):
-        with open(self.filename, 'r') as f:
+        with open(self.get_ctrl().log.get_path(), 'r') as f:
             self.write(f.read())
 
 
@@ -103,12 +95,7 @@ class LogHandler(tornado.web.RequestHandler):
         self.set_header('Content-Type', 'text/plain')
 
 
-class BugReportHandler(tornado.web.RequestHandler):
-    def __init__(self, app, request, **kwargs):
-        super().__init__(app, request, **kwargs)
-        self.app = app
-
-
+class BugReportHandler(bbctrl.RequestHandler):
     def get(self):
         import tarfile, io
 
@@ -123,12 +110,14 @@ class BugReportHandler(tornado.web.RequestHandler):
         def check_add_basename(path):
             check_add(path, os.path.basename(path))
 
-        check_add_basename(self.app.ctrl.args.log)
-        check_add_basename(self.app.ctrl.args.log + '.1')
-        check_add_basename(self.app.ctrl.args.log + '.2')
-        check_add_basename(self.app.ctrl.args.log + '.3')
+        ctrl = self.get_ctrl()
+        path = ctrl.log.get_path()
+        check_add_basename(path)
+        check_add_basename(path + '.1')
+        check_add_basename(path + '.2')
+        check_add_basename(path + '.3')
         check_add('config.json')
-        check_add('upload/' + self.app.ctrl.state.get('selected', ''))
+        check_add(ctrl.get_upload(ctrl.state.get('selected', '')))
 
         tar.close()
 
@@ -147,6 +136,9 @@ class HostnameHandler(bbctrl.APIHandler):
     def get(self): self.write_json(socket.gethostname())
 
     def put(self):
+        if self.get_ctrl().args.demo:
+            raise HTTPError(400, 'Cannot set hostname in demo mode')
+
         if 'hostname' in self.json:
             if subprocess.call(['/usr/local/bin/sethostname',
                                 self.json['hostname'].strip()]) == 0:
@@ -164,7 +156,11 @@ class WifiHandler(bbctrl.APIHandler):
         except: pass
         self.write_json(data)
 
+
     def put(self):
+        if self.get_ctrl().args.demo:
+            raise HTTPError(400, 'Cannot configure WiFi in demo mode')
+
         if 'mode' in self.json:
             cmd = ['config-wifi', '-r']
             mode = self.json['mode']
@@ -193,12 +189,18 @@ class UsernameHandler(bbctrl.APIHandler):
 
 
     def put_ok(self):
+        if self.get_ctrl().args.demo:
+            raise HTTPError(400, 'Cannot set username in demo mode')
+
         if 'username' in self.json: set_username(self.json['username'])
         else: raise HTTPError(400, 'Missing "username"')
 
 
 class PasswordHandler(bbctrl.APIHandler):
     def put(self):
+        if self.get_ctrl().args.demo:
+            raise HTTPError(400, 'Cannot set password in demo mode')
+
         if 'current' in self.json and 'password' in self.json:
             check_password(self.json['current'])
 
@@ -218,7 +220,7 @@ class PasswordHandler(bbctrl.APIHandler):
 
 
 class ConfigLoadHandler(bbctrl.APIHandler):
-    def get(self): self.write_json(self.ctrl.config.load())
+    def get(self): self.write_json(self.get_ctrl().config.load())
 
 
 class ConfigDownloadHandler(bbctrl.APIHandler):
@@ -230,15 +232,15 @@ class ConfigDownloadHandler(bbctrl.APIHandler):
                         'attachment; filename="%s"' % filename)
 
     def get(self):
-        self.write_json(self.ctrl.config.load(), pretty = True)
+        self.write_json(self.get_ctrl().config.load(), pretty = True)
 
 
 class ConfigSaveHandler(bbctrl.APIHandler):
-    def put_ok(self): self.ctrl.config.save(self.json)
+    def put_ok(self): self.get_ctrl().config.save(self.json)
 
 
 class ConfigResetHandler(bbctrl.APIHandler):
-    def put_ok(self): self.ctrl.config.reset()
+    def put_ok(self): self.get_ctrl().config.reset()
 
 
 class FirmwareUpdateHandler(bbctrl.APIHandler):
@@ -261,31 +263,31 @@ class FirmwareUpdateHandler(bbctrl.APIHandler):
         with open('firmware/update.tar.bz2', 'wb') as f:
             f.write(firmware['body'])
 
-        self.ctrl.lcd.goodbye('Upgrading firmware')
+        self.get_ctrl().lcd.goodbye('Upgrading firmware')
         subprocess.Popen(['/usr/local/bin/update-bbctrl'])
 
 
 class UpgradeHandler(bbctrl.APIHandler):
     def put_ok(self):
         check_password(self.json['password'])
-        self.ctrl.lcd.goodbye('Upgrading firmware')
+        self.get_ctrl().lcd.goodbye('Upgrading firmware')
         subprocess.Popen(['/usr/local/bin/upgrade-bbctrl'])
 
 
 class PathHandler(bbctrl.APIHandler):
     @gen.coroutine
     def get(self, filename, dataType, *args):
-        if not os.path.exists('upload/' + filename):
+        if not os.path.exists(self.get_upload(filename)):
             raise HTTPError(404, 'File not found')
 
-        future = self.ctrl.preplanner.get_plan(filename)
+        future = self.get_ctrl().preplanner.get_plan(filename)
 
         try:
             delta = datetime.timedelta(seconds = 1)
             data = yield gen.with_timeout(delta, future)
 
         except gen.TimeoutError:
-            progress = self.ctrl.preplanner.get_plan_progress(filename)
+            progress = self.get_ctrl().preplanner.get_plan_progress(filename)
             self.write_json(dict(progress = progress))
             return
 
@@ -323,80 +325,81 @@ class HomeHandler(bbctrl.APIHandler):
             if not 'position' in self.json:
                 raise HTTPError(400, 'Missing "position"')
 
-            self.ctrl.mach.home(axis, self.json['position'])
+            self.get_ctrl().mach.home(axis, self.json['position'])
 
-        elif action == '/clear': self.ctrl.mach.unhome(axis)
-        else: self.ctrl.mach.home(axis)
+        elif action == '/clear': self.get_ctrl().mach.unhome(axis)
+        else: self.get_ctrl().mach.home(axis)
 
 
 class StartHandler(bbctrl.APIHandler):
-    def put_ok(self): self.ctrl.mach.start()
+    def put_ok(self): self.get_ctrl().mach.start()
 
 
 class EStopHandler(bbctrl.APIHandler):
-    def put_ok(self): self.ctrl.mach.estop()
+    def put_ok(self): self.get_ctrl().mach.estop()
 
 
 class ClearHandler(bbctrl.APIHandler):
-    def put_ok(self): self.ctrl.mach.clear()
+    def put_ok(self): self.get_ctrl().mach.clear()
 
 
 class StopHandler(bbctrl.APIHandler):
-    def put_ok(self): self.ctrl.mach.stop()
+    def put_ok(self): self.get_ctrl().mach.stop()
 
 
 class PauseHandler(bbctrl.APIHandler):
-    def put_ok(self): self.ctrl.mach.pause()
+    def put_ok(self): self.get_ctrl().mach.pause()
 
 
 class UnpauseHandler(bbctrl.APIHandler):
-    def put_ok(self): self.ctrl.mach.unpause()
+    def put_ok(self): self.get_ctrl().mach.unpause()
 
 
 class OptionalPauseHandler(bbctrl.APIHandler):
-    def put_ok(self): self.ctrl.mach.optional_pause()
+    def put_ok(self): self.get_ctrl().mach.optional_pause()
 
 
 class StepHandler(bbctrl.APIHandler):
-    def put_ok(self): self.ctrl.mach.step()
+    def put_ok(self): self.get_ctrl().mach.step()
 
 
 class PositionHandler(bbctrl.APIHandler):
     def put_ok(self, axis):
-        self.ctrl.mach.set_position(axis, float(self.json['position']))
+        self.get_ctrl().mach.set_position(axis, float(self.json['position']))
 
 
 class OverrideFeedHandler(bbctrl.APIHandler):
-    def put_ok(self, value): self.ctrl.mach.override_feed(float(value))
+    def put_ok(self, value): self.get_ctrl().mach.override_feed(float(value))
 
 
 class OverrideSpeedHandler(bbctrl.APIHandler):
-    def put_ok(self, value): self.ctrl.mach.override_speed(float(value))
+    def put_ok(self, value): self.get_ctrl().mach.override_speed(float(value))
 
 
 class ModbusReadHandler(bbctrl.APIHandler):
-    def put_ok(self): self.ctrl.mach.modbus_read(int(self.json['address']))
+    def put_ok(self):
+        self.get_ctrl().mach.modbus_read(int(self.json['address']))
 
 
 class ModbusWriteHandler(bbctrl.APIHandler):
     def put_ok(self):
-        self.ctrl.mach.modbus_write(int(self.json['address']),
+        self.get_ctrl().mach.modbus_write(int(self.json['address']),
                                     int(self.json['value']))
 
 
 class JogHandler(bbctrl.APIHandler):
-    def put_ok(self): self.ctrl.mach.jog(self.json)
+    def put_ok(self): self.get_ctrl().mach.jog(self.json)
 
 
 # Base class for Web Socket connections
 class ClientConnection(object):
-    def __init__(self, ctrl):
-        self.ctrl = ctrl
+    def __init__(self, app):
+        self.app = app
         self.count = 0
 
 
     def heartbeat(self):
-        self.timer = self.ctrl.ioloop.call_later(3, self.heartbeat)
+        self.timer = self.app.ioloop.call_later(3, self.heartbeat)
         self.send({'heartbeat': self.count})
         self.count += 1
 
@@ -404,17 +407,19 @@ class ClientConnection(object):
     def send(self, msg): raise HTTPError(400, 'Not implemented')
 
 
-    def on_open(self, *args, **kwargs):
+    def on_open(self, id = None):
+        self.ctrl = self.app.get_ctrl(id)
+
         self.ctrl.state.add_listener(self.send)
-        self.ctrl.msgs.add_listener(self.send)
+        self.ctrl.log.add_listener(self.send)
         self.is_open = True
         self.heartbeat()
 
 
     def on_close(self):
-        self.ctrl.ioloop.remove_timeout(self.timer)
+        self.app.ioloop.remove_timeout(self.timer)
         self.ctrl.state.remove_listener(self.send)
-        self.ctrl.msgs.remove_listener(self.send)
+        self.ctrl.log.remove_listener(self.send)
         self.is_open = False
 
 
@@ -424,7 +429,7 @@ class ClientConnection(object):
 # Used by CAMotics
 class WSConnection(ClientConnection, tornado.websocket.WebSocketHandler):
     def __init__(self, app, request, **kwargs):
-        ClientConnection.__init__(self, app.ctrl)
+        ClientConnection.__init__(self, app)
         tornado.websocket.WebSocketHandler.__init__(
             self, app, request, **kwargs)
 
@@ -435,7 +440,7 @@ class WSConnection(ClientConnection, tornado.websocket.WebSocketHandler):
 # Used by Web frontend
 class SockJSConnection(ClientConnection, sockjs.tornado.SockJSConnection):
     def __init__(self, session):
-        ClientConnection.__init__(self, session.server.ctrl)
+        ClientConnection.__init__(self, session.server.app)
         sockjs.tornado.SockJSConnection.__init__(self, session)
 
 
@@ -446,6 +451,18 @@ class SockJSConnection(ClientConnection, sockjs.tornado.SockJSConnection):
             self.close()
 
 
+    def on_open(self, info):
+        cookie = info.get_cookie('client-id')
+        if cookie is None: self.send(dict(sid = '')) # Trigger client reset
+        else:
+            id = cookie.value
+
+            ip = info.ip
+            if 'X-Real-IP' in info.headers: ip = info.headers['X-Real-IP']
+            self.app.get_ctrl(id).log.get('Web').info('Connection from %s' % ip)
+            super().on_open(id)
+
+
 class StaticFileHandler(tornado.web.StaticFileHandler):
     def set_extra_headers(self, path):
         self.set_header('Cache-Control',
@@ -453,8 +470,13 @@ class StaticFileHandler(tornado.web.StaticFileHandler):
 
 
 class Web(tornado.web.Application):
-    def __init__(self, ctrl):
-        self.ctrl = ctrl
+    def __init__(self, args, ioloop):
+        self.args = args
+        self.ioloop = ioloop
+        self.ctrls = {}
+
+        # Init controller
+        if not self.args.demo: self.get_ctrl()
 
         handlers = [
             (r'/websocket', WSConnection),
@@ -491,25 +513,55 @@ class Web(tornado.web.Application):
             (r'/api/video', bbctrl.VideoHandler),
             (r'/(.*)', StaticFileHandler,
              {'path': bbctrl.get_resource('http/'),
-              "default_filename": "index.html"}),
+              'default_filename': 'index.html'}),
             ]
 
         router = sockjs.tornado.SockJSRouter(SockJSConnection, '/sockjs')
-        router.ctrl = ctrl
+        router.app = self
 
         tornado.web.Application.__init__(self, router.urls + handlers)
 
         try:
-            self.listen(ctrl.args.port, address = ctrl.args.addr)
+            self.listen(args.port, address = args.addr)
 
         except Exception as e:
-            log.error('Failed to bind %s:%d: %s', ctrl.args.addr,
-                      ctrl.args.port, e)
-            sys.exit(1)
+            raise Exception('Failed to bind %s:%d: %s' % (
+                args.addr, args.port, e))
+
+        print('Listening on http://%s:%d/' % (args.addr, args.port))
+
+        self._reap_ctrls()
+
+
+    def _reap_ctrls(self):
+        if not self.args.demo: return
+        now = time.time()
+
+        for id in list(self.ctrls.keys()):
+            ctrl = self.ctrls[id]
+
+            if ctrl.lastTS + self.args.client_timeout < now:
+                ctrl.close()
+                del self.ctrls[id]
+
+        self.ioloop.call_later(60, self._reap_ctrls)
+
+
+    def get_ctrl(self, id = None):
+        if not id or not self.args.demo: id = ''
+
+        if not id in self.ctrls:
+            ctrl = bbctrl.Ctrl(self.args, self.ioloop, id)
+            self.ctrls[id] = ctrl
+
+        else: ctrl = self.ctrls[id]
+
+        ctrl.lastTS = time.time()
 
-        log.info('Listening on http://%s:%d/', ctrl.args.addr, ctrl.args.port)
+        return ctrl
 
 
     # Override default logger
     def log_request(self, handler):
+        log = self.get_ctrl(handler.get_cookie('client-id')).log.get('Web')
         log.info("%d %s", handler.get_status(), handler._request_summary())
index 70f63be13f4439a3bb1838f3b6e042fd60fbd4b4..70f8e0710a546e41f3066abd8c4e207b08a130cf 100644 (file)
@@ -32,15 +32,12 @@ import sys
 import signal
 import tornado
 import argparse
-import logging
-import datetime
-import pkg_resources
 
 from pkg_resources import Requirement, resource_filename
 
+from bbctrl.RequestHandler import RequestHandler
 from bbctrl.APIHandler import APIHandler
 from bbctrl.FileHandler import FileHandler
-from bbctrl.GCodeStream import GCodeStream
 from bbctrl.Config import Config
 from bbctrl.LCD import LCD, LCDPage
 from bbctrl.Mach import Mach
@@ -52,7 +49,6 @@ from bbctrl.I2C import I2C
 from bbctrl.Planner import Planner
 from bbctrl.Preplanner import Preplanner
 from bbctrl.State import State
-from bbctrl.Messages import Messages
 from bbctrl.Comm import Comm
 from bbctrl.CommandQueue import CommandQueue
 from bbctrl.MainLCDPage import MainLCDPage
@@ -60,8 +56,10 @@ from bbctrl.IPLCDPage import IPLCDPage
 from bbctrl.Camera import Camera, VideoHandler
 from bbctrl.AVR import AVR
 from bbctrl.AVREmu import AVREmu
+from bbctrl.IOLoop import IOLoop
 import bbctrl.Cmd as Cmd
 import bbctrl.v4l2 as v4l2
+import bbctrl.Log as log
 
 
 ctrl = None
@@ -74,7 +72,7 @@ def get_resource(path):
 def on_exit(sig = 0, func = None):
     global ctrl
 
-    logging.info('Exit handler triggered: signal = %d', sig)
+    print('Exit handler triggered: signal = %d', sig)
 
     if ctrl is not None:
         ctrl.close()
@@ -83,11 +81,6 @@ def on_exit(sig = 0, func = None):
     sys.exit(1)
 
 
-def log_time(log, ioloop):
-    log.info(datetime.datetime.now().strftime('%Y/%m/%d %H:%M:%S'))
-    ioloop.call_later(60 * 60, log_time, log, ioloop)
-
-
 def parse_args():
     parser = argparse.ArgumentParser(
         description = 'Buildbotics Machine Controller')
@@ -126,6 +119,8 @@ def parse_args():
                         help = 'Enter demo mode')
     parser.add_argument('--fast-emu', action = 'store_true',
                         help = 'Enter demo mode')
+    parser.add_argument('--client-timeout', default = 5 * 60, type = int,
+                        help = 'Demo client timeout in seconds')
 
     return parser.parse_args()
 
@@ -135,45 +130,19 @@ def run():
 
     args = parse_args()
 
-    # Init logging
-    root = logging.getLogger()
-    level = logging.DEBUG if args.verbose else logging.INFO
-    root.setLevel(logging.NOTSET)
-    f = logging.Formatter('{levelname[0]}:{name}:{message}', style = '{')
-    h = logging.StreamHandler()
-    h.setLevel(level)
-    h.setFormatter(f)
-    root.addHandler(h)
-
-    if args.log:
-        h = logging.handlers.RotatingFileHandler(args.log, maxBytes = 1000000,
-                                                 backupCount = 5)
-        h.setLevel(level)
-        h.setFormatter(f)
-        root.addHandler(h)
-
-    # Log header
-    version = pkg_resources.require('bbctrl')[0].version
-    root.info('Log started v%s' % version)
-
     # Set signal handler
     signal.signal(signal.SIGTERM, on_exit)
 
     # Create ioloop
     ioloop = tornado.ioloop.IOLoop.current()
 
-    # Write time to log periodically
-    log_time(root, ioloop)
-
-    # Start controller
-    ctrl = Ctrl(args, ioloop)
+    # Start server
+    web = Web(args, ioloop)
 
     try:
         ioloop.start()
 
     except KeyboardInterrupt: on_exit()
-    except SystemExit: raise
-    except: logging.getLogger().exception('')
 
 
 if __name__ == '__main__': run()
diff --git a/src/resources/buildbotics.nc b/src/resources/buildbotics.nc
new file mode 100644 (file)
index 0000000..8c5ddb0
--- /dev/null
@@ -0,0 +1,403 @@
+G21
+(File: 'buildbotics_logo.tpl')
+G0 Z3
+F1600
+M3 S10000
+M6 T2
+G0 X59.25 Y5.85
+G1 Z-1.5
+G1 X61.68 Y6.7
+G1 X63.86 Y8.07
+G1 X65.68 Y9.89
+G1 X67.05 Y12.07
+G1 X67.9 Y14.5
+G1 X68.2 Y17.09
+G1 Y56.6
+G1 X67.73 Y59.04
+G1 X50.8
+G1 Y34.9
+G1 X50.65 Y34.55
+G1 X50.3 Y34.4
+G1 X23.46
+G1 X23.1 Y34.55
+G1 X22.96 Y34.9
+G1 X22.98 Y49.88
+G1 X22.96 Y59.05
+G1 X22.41
+G1 X19.26
+G1 X6.04
+G1 X5.56 Y56.53
+G1 Y17.09
+G1 X5.85 Y14.5
+G1 X6.7 Y12.07
+G1 X8.07 Y9.89
+G1 X9.89 Y8.07
+G1 X12.07 Y6.7
+G1 X14.5 Y5.85
+G1 X17.09 Y5.56
+G1 X56.67
+G1 X59.25 Y5.85
+G0 Z3
+G0 X64.26 Y64.72
+G1 Z-1.5
+G1 X61.78 Y66.52
+G1 X58.91 Y67.68
+G1 X56.54 Y68.08
+G1 X17.22
+G1 X14.84 Y67.68
+G1 X11.97 Y66.52
+G1 X9.49 Y64.72
+G1 X8.08 Y63.16
+G1 X27.35
+G1 X27.89 Y63.45
+G1 X27.96 Y63.48
+G1 X31.48 Y64.75
+G1 X31.52 Y64.76
+G1 X31.56 Y64.77
+G1 X35.19 Y65.41
+G1 X35.26
+G1 X35.97 Y65.44
+G1 X36.04 Y65.45
+G1 X36.07
+G1 X36.82 Y65.44
+G1 X36.83
+G1 X36.89
+G1 X36.95
+G1 X36.97
+G1 X37.72 Y65.43
+G1 X37.74
+G1 X37.8
+G1 X37.81
+G1 X37.88
+G1 X37.89
+G1 X38.65 Y65.38
+G1 X38.68
+G1 X38.75 Y65.37
+G1 X39.38 Y65.32
+G1 X39.44 Y65.31
+G1 X42.68 Y64.64
+G1 X42.76 Y64.62
+G1 X45.87 Y63.44
+G1 X45.93 Y63.41
+G1 X46.4 Y63.16
+G1 X65.67
+G1 X64.26 Y64.72
+G0 Z3
+G0 X36.88 Y9.4
+G1 Z-1.5
+G1 X37.31 Y9.64
+G1 X39.58 Y13.48
+G1 X39.63 Y13.6
+G1 X39.65 Y13.73
+G1 Y27.54
+G1 X41.67
+G1 Y25.39
+G1 X41.75 Y25.12
+G1 X41.97 Y24.93
+G1 X46.41 Y22.92
+G1 Y19.97
+G1 X45.44
+G1 X45.08 Y19.82
+G1 X44.94 Y19.47
+G1 Y13.73
+G1 X45.08 Y13.38
+G1 X45.44 Y13.23
+G1 X49.94 Y13.24
+G1 X50.29 Y13.39
+G1 X50.44 Y13.74
+G1 Y19.47
+G1 X50.29 Y19.83
+G1 X49.93 Y19.97
+G1 X48.92
+G1 Y23.61
+G1 X48.84 Y23.88
+G1 X48.63 Y24.06
+G1 X44.19 Y26.12
+G1 Y27.54
+G1 X49.22
+G1 X49.33 Y27.56
+G1 X49.44 Y27.6
+G1 X50.13 Y27.94
+G1 X50.25 Y28.02
+G1 X50.34 Y28.13
+G1 X50.73 Y28.77
+G1 X50.78 Y28.89
+G1 X50.8 Y29.03
+G1 Y33.05
+G1 Y34.25
+G1 Y34.65
+G1 X50.66 Y35.01
+G1 X50.3 Y35.15
+G1 X23.46
+G1 X23.1 Y35.01
+G1 X22.96 Y34.65
+G1 Y29.07
+G1 X22.97 Y28.94
+G1 X23.02 Y28.82
+G1 X23.4 Y28.17
+G1 X23.49 Y28.06
+G1 X23.6 Y27.98
+G1 X24.29 Y27.6
+G1 X24.4 Y27.56
+G1 X24.52 Y27.54
+G1 X25.55
+G1 Y26.4
+G1 X23.4 Y25.52
+G1 X23.17 Y25.33
+G1 X23.09 Y25.06
+G1 Y17.54
+G1 X23.23 Y17.19
+G1 X23.59 Y17.04
+G1 X24.6
+G1 Y10.36
+G1 X24.62 Y10.23
+G1 X24.66 Y10.11
+G1 X24.8 Y9.88
+G1 X24.88 Y9.77
+G1 X24.99 Y9.68
+G1 X25.25 Y9.54
+G1 X25.37 Y9.5
+G1 X25.49 Y9.48
+G1 X26.53
+G1 X26.65 Y9.49
+G1 X26.76 Y9.54
+G1 X27.01 Y9.66
+G1 X27.12 Y9.74
+G1 X27.21 Y9.85
+G1 X27.35 Y10.09
+G1 X27.41 Y10.22
+G1 X27.43 Y10.35
+G1 Y10.43
+G1 X27.47 Y17.04
+G1 X28.57
+G1 X28.92 Y17.19
+G1 X29.07 Y17.54
+G1 Y24.64
+G1 X30.72 Y25.3
+G1 X30.95 Y25.49
+G1 X31.03 Y25.77
+G1 X31.02 Y27.54
+G1 X34.03
+G1 Y13.73
+G1 X34.05 Y13.59
+G1 X34.1 Y13.47
+G1 X36.45 Y9.64
+G1 X36.88 Y9.4
+G0 Z3
+G0 X49.94 Y10.82
+G1 Z-1.5
+G1 X50.29 Y10.97
+G1 X50.44 Y11.32
+G1 Y13.34
+G1 X50.29 Y13.69
+G1 X49.94 Y13.84
+G1 X45.44
+G1 X45.08 Y13.69
+G1 X44.94 Y13.34
+G1 Y11.32
+G1 X45.08 Y10.97
+G1 X45.44 Y10.82
+G1 X49.94
+G0 Z3
+G0 X48.46 Y9.7
+G1 Z-1.5
+G1 X48.59 Y9.72
+G1 X48.71 Y9.77
+G1 X50.03 Y10.53
+G1 X50.21 Y10.71
+G1 X50.28 Y10.96
+G1 X50.14 Y11.31
+G1 X49.78 Y11.46
+G1 X45.62
+G1 X45.14 Y11.09
+G1 X45.37 Y10.53
+G1 X46.69 Y9.77
+G1 X46.81 Y9.72
+G1 X46.94 Y9.7
+G1 X48.46
+G0 Z3
+G0 X50.3 Y34.4
+G1 Z-1.5
+G1 X50.66 Y34.55
+G1 X50.8 Y34.9
+G1 Y49.88
+G1 X50.79 Y59.52
+G1 X50.76 Y59.69
+G1 X50.67 Y59.84
+G1 X50.09 Y60.52
+G1 X50.06 Y60.55
+G1 X50.02 Y60.58
+G1 X47.89 Y62.26
+G1 X47.85 Y62.28
+G1 X47.81 Y62.31
+G1 X44.46 Y64.03
+G1 X44.41 Y64.05
+G1 X44.37 Y64.06
+G1 X40.69 Y65.1
+G1 X40.62 Y65.12
+G1 X37.86 Y65.45
+G1 X37.8 Y65.46
+G1 X36.89 Y65.44
+G1 X36.83
+G1 X36.82
+G1 X36.07 Y65.45
+G1 X36.04 Y65.44
+G1 X35.97
+G1 X35.1 Y65.41
+G1 X35.04 Y65.4
+G1 X32.44 Y64.99
+G1 X32.37 Y64.97
+G1 X28.94 Y63.9
+G1 X28.89 Y63.88
+G1 X28.85 Y63.86
+G1 X25.74 Y62.2
+G1 X25.7 Y62.18
+G1 X25.66 Y62.15
+G1 X23.68 Y60.56
+G1 X23.65 Y60.53
+G1 X23.62 Y60.5
+G1 X23.08 Y59.87
+G1 X22.99 Y59.71
+G1 X22.96 Y59.54
+G1 X22.98 Y49.88
+G1 X22.96 Y34.9
+G1 X23.1 Y34.55
+G1 X23.46 Y34.4
+G1 X50.3
+G0 Z3
+G0 X55.2 Y43.67
+G1 Z-1.5
+G1 Y51.34
+G1 X55.11 Y51.94
+G1 X54.83 Y52.88
+G1 X54.39 Y53.88
+G1 X53.8 Y54.85
+G1 X53.09 Y55.74
+G1 X52.28 Y56.47
+G1 X51.41 Y56.98
+G1 X51.07 Y57.09
+G1 Y43.67
+G1 X55.2
+G0 Z3
+G0 X22.69 Y43.63
+G1 Z-1.5
+G1 Y57.09
+G1 X22.35 Y56.98
+G1 X21.47 Y56.47
+G1 X20.67 Y55.74
+G1 X19.95 Y54.85
+G1 X19.36 Y53.88
+G1 X18.92 Y52.88
+G1 X18.64 Y51.94
+G1 X18.55 Y51.34
+G1 Y43.63
+G1 X22.69
+G0 Z3
+G0 X28.55 Y35.84
+G1 Z-0.99
+G1 X30.11 Y36.15
+G1 X31.43 Y37.03
+G1 X32.32 Y38.35
+G1 X32.63 Y39.91
+G1 X32.32 Y41.47
+G1 X31.43 Y42.79
+G1 X30.11 Y43.68
+G1 X28.55 Y43.99
+G1 X26.99 Y43.68
+G1 X25.67 Y42.79
+G1 X24.79 Y41.47
+G1 X24.48 Y39.91
+G1 X24.79 Y38.35
+G1 X25.67 Y37.03
+G1 X26.99 Y36.15
+G1 X28.55 Y35.84
+G0 Z3
+G0 X45.33 Y35.93
+G1 Z-0.99
+G1 X46.88 Y36.24
+G1 X48.21 Y37.12
+G1 X49.09 Y38.45
+G1 X49.4 Y40
+G1 X49.09 Y41.56
+G1 X48.21 Y42.88
+G1 X46.88 Y43.77
+G1 X45.33 Y44.08
+G1 X43.77 Y43.77
+G1 X42.45 Y42.88
+G1 X41.56 Y41.56
+G1 X41.25 Y40
+G1 X41.56 Y38.45
+G1 X42.45 Y37.12
+G1 X43.77 Y36.24
+G1 X45.33 Y35.93
+G0 Z3
+G0 X45.2 Y39.12
+G1 Z-0.99
+G1 X45.7 Y39.19
+G1 X46.07 Y39.52
+G1 X46.22 Y40
+G1 X46.07 Y40.49
+G1 X45.7 Y40.81
+G1 X45.2 Y40.89
+G1 X44.74 Y40.68
+G1 X44.47 Y40.26
+G1 Y39.75
+G1 X44.74 Y39.33
+G1 X45.2 Y39.12
+G0 Z3
+G0 X28.43 Y39.03
+G1 Z-0.99
+G1 X28.92 Y39.1
+G1 X29.3 Y39.43
+G1 X29.44 Y39.91
+G1 X29.3 Y40.4
+G1 X28.92 Y40.72
+G1 X28.43 Y40.8
+G1 X27.97 Y40.59
+G1 X27.7 Y40.16
+G1 Y39.66
+G1 X27.97 Y39.24
+G1 X28.43 Y39.03
+G0 Z3
+G0 X55.76 Y0
+G1 Z-1.5
+G1 X59.27 Y0.35
+G1 X62.65 Y1.37
+G1 X65.76 Y3.03
+G1 X68.49 Y5.27
+G1 X70.73 Y8
+G1 X72.39 Y11.11
+G1 X73.42 Y14.49
+G1 X73.76 Y18
+G1 Y55.69
+G1 X73.42 Y59.2
+G1 X72.39 Y62.57
+G1 X70.73 Y65.69
+G1 X68.49 Y68.41
+G1 X65.76 Y70.65
+G1 X62.65 Y72.31
+G1 X59.27 Y73.34
+G1 X55.76 Y73.69
+G1 X18
+G1 X14.49 Y73.34
+G1 X11.11 Y72.31
+G1 X8 Y70.65
+G1 X5.27 Y68.41
+G1 X3.03 Y65.69
+G1 X1.37 Y62.57
+G1 X0.35 Y59.2
+G1 X0 Y55.69
+G1 Y18
+G1 X0.35 Y14.49
+G1 X1.37 Y11.11
+G1 X3.03 Y8
+G1 X5.27 Y5.27
+G1 X8 Y3.03
+G1 X11.11 Y1.37
+G1 X14.49 Y0.35
+G1 X18 Y0
+G1 X55.76
+G0 Z3
+M5
+G0 X40 Y75
+M2
index 17c43e6369f0f0f4c3a8d56f9a222e82d43c5eae..c82090f35dd8b78e8bb5ac750878ec556651b7c3 100644 (file)
Binary files a/src/resources/images/in-use.jpg and b/src/resources/images/in-use.jpg differ
index 86137b54ecc677098b80e589d55362b07fcf2389..1a4399bfe6b736fd38b58c413b7a7418ed559713 100644 (file)
Binary files a/src/resources/images/offline.jpg and b/src/resources/images/offline.jpg differ
index 6fd5abff68262bdfeb677c181a11dd9ee8ed6507..53204283b56b937dbf487ed719ee9341812ca081 100644 (file)
@@ -41,7 +41,8 @@ tt
     float right
     margin 5px
 
-  .video img
+  .video
+    position relative
     float right
     width 174px
     height 130px
@@ -57,6 +58,35 @@ tt
       margin 5px 0
       height inherit
 
+    .crosshair
+      > *
+        border 1px dashed #ccc
+        position absolute
+
+      .vertical
+        height 100%
+        width 0
+        left 50%
+        margin-left -1px
+
+      .horizontal
+        height 0
+        width 100%
+        top 50%
+        margin-top -1px
+
+      .box
+        width 16px
+        height 16px
+        top 50%
+        left 50%
+        margin-top -9px
+        margin-left -9px
+
+    img
+      width 100%
+      height 100%
+
   .banner
     float left
     padding-top 40px
@@ -661,10 +691,6 @@ tr.log-debug td
     background-color transparent
     color orange
 
-.tab-content .video
-  text-align center
-  min-height 300px
-
 tt.save
   display inline-block
   border-radius 2px