## 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.
+++ /dev/null
-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
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/
// 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);
}
try {
config = JSON.parse(e.target.result);
} catch (ex) {
- alert("Invalid config file");
+ api.alert("Invalid config file");
return;
}
this.configRestored = true;
}.bind(this)).fail(function (error) {
- alert('Restore failed: ' + error);
+ api.alert('Restore failed', error);
})
}.bind(this);
this.configReset = true;
}.bind(this)).fail(function (error) {
- alert('Reset failed: ' + error);
+ api.alert('Reset failed', error);
});
},
}.bind(this));
}.bind(this)).fail(function (error) {
- alert('Set hostname failed: ' + JSON.stringify(error));
+ api.alert('Set hostname failed', error);
})
},
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;
}
}).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 = {
}
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))
}
}
}
'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);
}
}
state: {},
messages: [],
video_size: cookie.get('video-size', 'small'),
+ crosshair: cookie.get('crosshair', false),
errorTimeout: 30,
errorTimeoutStart: 0,
errorShow: false,
},
- 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');
this.firmwareUpgrading = true;
}.bind(this)).fail(function () {
- alert('Invalid password');
+ api.alert('Invalid password');
}.bind(this))
},
this.firmwareUpgrading = true;
}.bind(this)).error(function () {
- alert('Invalid password or bad firmware');
+ api.alert('Invalid password or bad firmware');
}.bind(this))
},
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);
});
},
this.$broadcast('gcode-reload', file.name);
}.bind(this)).fail(function (error) {
- alert('Upload failed: ' + error)
+ api.alert('Upload failed', error)
}.bind(this));
},
'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)}
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",
.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
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')
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):
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
################################################################################
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,
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,
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
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):
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])
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
# #
################################################################################
-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
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)
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):
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):
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()))
import os
import fcntl
-import logging
import select
import struct
import mmap
except:
import bbctrl.v4l2 as v4l2
-log = logging.getLogger('Camera')
-
def array_to_string(a): return ''.join([chr(i) for i in a])
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
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):
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
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)
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):
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)
def remove_client(self, client):
- log.info('Removing camera client')
+ self.log.info('Removing camera client')
try:
self.clients.remove(client)
except: pass
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
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):
class Web(web.Application):
- def __init__(self, ctrl):
- self.ctrl = ctrl
+ def __init__(self, args, ioloop):
+ self.ctrl = Ctrl(args, ioloop)
handlers = [
(r'/', RootHandler),
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)
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()
import struct
import base64
import json
-import logging
-
-log = logging.getLogger('Cmd')
# Keep this in sync with AVR code command.def
SET = '$'
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__":
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)
################################################################################
import serial
-import logging
import json
import time
import traceback
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
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)
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')
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()
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:
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)
# #
################################################################################
-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()
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()
# 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()
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)
class Config(object):
def __init__(self, ctrl):
self.ctrl = ctrl
+ self.log = ctrl.log.get('Config')
+
self.values = {}
try:
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):
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)
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):
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('')
# #
################################################################################
-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)
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):
def close(self):
if not self.camera is None: self.camera.close()
+ self.ioloop.close()
+ self.avr.close()
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)
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
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)
+++ /dev/null
-################################################################################
-# #
-# 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
--- /dev/null
+################################################################################
+# #
+# 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)
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": {
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)
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
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]
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):
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
--- /dev/null
+################################################################################
+# #
+# 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))
# #
################################################################################
-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:
#
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
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'
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()
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()
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:]
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)
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()
+++ /dev/null
-################################################################################
-# #
-# 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})
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 ]:'
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)
if overrides: cfg['overrides'] = overrides
- log.info('Config:' + log_json(cfg))
+ self.log.info('Config:' + log_json(cfg))
return cfg
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
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)
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)
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)
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()
self.planner.stop()
self.cmdq.clear()
- except Exception as e:
- log.exception(e)
+ except:
+ self.log.exception()
self.reset()
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()
cmd = self._encode(cmd)
if cmd is not None: return cmd
- except Exception as e:
- log.exception(e)
+ except:
+ self.log.exception()
self.stop()
################################################################################
import os
-import logging
import time
import json
import hashlib
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))
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()
def start(self):
if not self.started.done():
- log.info('Preplanner started')
+ self.log.info('Preplanner started')
self.started.set_result(True)
def delete_all_plans(self):
- files = glob.glob('plans/*')
+ files = glob.glob(self.ctrl.get_plan('*'))
for path in files:
try:
def delete_plans(self, filename):
- files = glob.glob('plans/' + filename + '.*')
+ files = glob.glob(self.ctrl.get_plan(filename + '.*'))
for path in files:
try:
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
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']
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',
'--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,
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])
return meta, positions, speeds
- except Exception as e: log.exception(e)
+ except Exception as e: self.log.exception(e)
# #
################################################################################
-import logging
from tornado.ioloop import PeriodicCallback
import bbctrl
-log = logging.getLogger('PWR')
-
# Must match regs in pwr firmware
TEMP_REG = 0
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
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):
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
--- /dev/null
+################################################################################
+# #
+# 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)
# #
################################################################################
-import logging
import traceback
import copy
import uuid
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 = []
'tool': 0,
'feed': 0,
'speed': 0,
+ 'sid': str(uuid.uuid4()),
+ 'demo': ctrl.args.demo,
}
# Add computed variable callbacks for each motor.
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()
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)
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 = {}
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
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))
import json
import tornado
import sockjs.tornado
-import logging
import datetime
import shutil
import tarfile
import bbctrl
-log = logging.getLogger('Web')
-
def call_get_output(cmd):
p = subprocess.Popen(cmd, stdout = subprocess.PIPE)
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())
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
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()
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:
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']
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'])
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):
'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):
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
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
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
# 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)
# 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)
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',
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),
(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())
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
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
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
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()
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')
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()
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()
--- /dev/null
+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
float right
margin 5px
- .video img
+ .video
+ position relative
float right
width 174px
height 130px
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
background-color transparent
color orange
-.tab-content .video
- text-align center
- min-height 300px
-
tt.save
display inline-block
border-radius 2px