update-rc.d resize2fs_once defaults
fi
-# Install pacakges
+# Install packages
apt-get install -y avahi-daemon avrdude minicom python3-pip i2c-tools
-pip-3.2 install tornado sockjs-tornado pyserial smbus
+pip-3.2 install --upgrade tornado sockjs-tornado pyserial smbus
# Clean
apt-get autoclean
# Enable I2C
sed -i 's/#dtparam=i2c/dtparam=i2c/' /boot/config.txt
+echo 'dtparam=i2c_vc=on' >> /boot/config.txt
echo i2c-bcm2708 >> /etc/modules
echo i2c-dev >> /etc/modules
chmod +x /etc/init.d/bbctrl
update-rc.d bbctrl defaults
-# TODO setup input and serial device permissions in udev
+# TODO setup input and serial device permissions in udev & forward 80 -> 8080
reboot
.content(class="{{currentView}}-view")
component(:is="currentView + '-view'", :index="index",
- :config="config", :template="template", keep-alive)
+ :config="config", :template="template", :state="state", keep-alive)
#templates
include ../../build/templates.jade
+++ /dev/null
-script#config-view-template(type="text/x-template")
- #config-page
- h1.title {{page}} {{motor}}
-
- .buttons
- button(@click="back") Back
- button(@click="next") Next
-
- component(:is="page + '-view'", :config="config.motors[motor]",
- :template="template")
-
- .buttons
- button(@click="back") Back
- button(@click="next") Next
form.pure-form.pure-form-aligned
fieldset
- .switch(v-for="sw in switches")
- h3 Switch {{$index}}
- templated-input(v-for="templ in template.switches", :name="$key",
- :model.sync="sw[$key]", :template="templ")
+ templated-input(v-for="templ in template.switches", :name="$key",
+ :model.sync="switches[$key]", :template="templ")
index: -1,
modified: false,
template: {motors: {}, axes: {}},
- config: {motors: [{}]}
+ config: {motors: [{}]},
+ state: {}
}
},
send: function (msg) {
- if (this.status == 'connected') this.sock.send(msg)
+ if (this.status == 'connected') {
+ console.debug('>', msg);
+ this.sock.send(msg)
+ }
},
methods: {
update: function () {
- $.get('/config-template.json').success(function (data, status, xhr) {
- this.template = data;
-
- api.get('load').done(function (data) {
- this.config = data;
- this.parse_hash();
+ $.get('/config-template.json', {cache: false})
+ .success(function (data, status, xhr) {
+ this.template = data;
+
+ api.get('load').done(function (data) {
+ this.config = data;
+ this.parse_hash();
+ }.bind(this))
}.bind(this))
- }.bind(this))
},
this.sock = new Sock('//' + window.location.host + '/ws');
this.sock.onmessage = function (e) {
- this.$broadcast('message', e.data);
+ var msg = e.data;
+
+ if (typeof msg == 'object')
+ for (var key in msg)
+ this.$set('state.' + key, msg[key]);
}.bind(this);
this.sock.onopen = function (e) {
+++ /dev/null
-'use strict'
-
-
-module.exports = {
- template: '#config-view-template',
-
-
- data: function () {
- return {
- page: 'motor',
- motor: 0,
- template: {},
- config: {"motors": [{}]}
- }
- },
-
-
- components: {
- 'motor-view': require('./motor-view'),
- 'switch-view': require('./switch-view')
- },
-
-
- ready: function () {
- $.get('/config-template.json').success(function (data, status, xhr) {
- this.template = data;
-
- $.get('/default-config.json').success(function (data, status, xhr) {
- this.config = data;
- }.bind(this))
- }.bind(this))
- },
-
-
- methods: {
- back: function() {
- if (this.motor) this.motor--;
- },
-
- next: function () {
- if (this.motor < this.config.motors.length - 1) this.motor++;
- }
- }
-}
module.exports = {
template: '#control-view-template',
- props: ['config'],
+ props: ['config', 'state'],
data: function () {
last_file: '',
files: [],
axes: 'xyzabc',
- state: {},
gcode: '',
speed_override: 1,
feed_override: 1
events: {
- jog: function (axis, move) {
- console.debug('jog(' + axis + ', ' + move + ')');
- this.send('g91 g0' + axis + move);
- },
-
-
- home: function (axis) {
- console.debug('home(' + axis + ')');
- this.send('$home ' + axis);
- },
-
-
- zero: function (axis) {
- console.debug('zero(' + axis + ')');
- this.send('$zero ' + axis);
- },
-
-
- message: function (data) {
- if (typeof data == 'object')
- for (var key in data)
- this.$set('state.' + key, data[key]);
- }
- },
+ jog: function (axis, move) {this.send('g91 g0' + axis + move)},
+ home: function (axis) {this.send('$home ' + axis)},
+ zero: function (axis) {this.send('$zero ' + axis)} },
ready: function () {
estop: function () {
- this.$set('state.es', !this.state.es);
+ this.send('$es=' + (this.state.es ? 0 : 1));
},
var template = this.template.spindle;
for (var key in template)
if (!this.spindle.hasOwnProperty(key))
- this.$set('spindle["' + key + '"]',
- template[key].default);
+ this.$set('spindle["' + key + '"]', template[key].default);
}.bind(this));
}
}
data: function () {
return {
- 'switches': []
+ switches: {}
}
},
Vue.nextTick(function () {
if (this.config.hasOwnProperty('switches'))
this.switches = this.config.switches;
- else this.switches = [];
-
- for (var i = 0; i < this.switches.length; i++) {
- var template = this.template.switches;
- for (var key in template)
- if (!this.switches[i].hasOwnProperty(key))
- this.$set('switches[' + i + ']["' + key + '"]',
- template[key].default);
- }
+ else this.switches = {};
+
+ var template = this.template.switches;
+ for (var key in template)
+ if (!this.switches.hasOwnProperty(key))
+ this.$set('switches["' + key + '"]', template[key].default);
}.bind(this));
}
}
import re
import serial
+import json
import logging
+from collections import deque
+
+import bbctrl
log = logging.getLogger('AVR')
+# These constants must be kept in sync with i2c.h from the AVR code
+I2C_NULL = 0
+I2C_ESTOP = 1
+I2C_PAUSE = 2
+I2C_OPTIONAL_PAUSE = 3
+I2C_RUN = 4
+I2C_FLUSH = 5
+I2C_STEP = 6
+I2C_REPORT = 7
+I2C_HOME = 8
+
class AVR():
def __init__(self, ctrl):
self.ctrl = ctrl
+ self.vars = {}
self.state = 'idle'
- self.line = -1
- self.step = 0
- self.f = None
+ self.stream = None
+ self.queue = deque()
+ self.in_buf = ''
+ self.command = None
+ self.flush_id = 1
try:
self.sp = serial.Serial(ctrl.args.serial, ctrl.args.baud,
- rtscts = 1, timeout = 0)
+ rtscts = 1, timeout = 0, write_timeout = 0)
+ self.sp.nonblocking()
except Exception as e:
log.warning('Failed to open serial port: %s', e)
return
- self.in_buf = ''
- self.out_buf = None
- self.ctrl.input_queue.put('$echo=0\n\n')
-
ctrl.ioloop.add_handler(self.sp, self.serial_handler, ctrl.ioloop.READ)
- ctrl.ioloop.add_handler(self.ctrl.input_queue._reader.fileno(),
- self.queue_handler, ctrl.ioloop.READ)
+ try:
+ self.i2c_bus = smbus.SMBus(ctrl.args.avr_port)
+ self.i2c_addr = ctrl.args.avr_addr
+
+ except FileNotFoundError as e:
+ self.i2c_bus = None
+ log.warning('Failed to open device: %s', e)
+
+ self.report()
+
+
+ def _state_transition_error(self, state):
+ raise Exception('Cannot %s in %s state' % (state, self.state))
+
+
+ def _state_transition(self, state, optional = False, step = False):
+ if state == self.state: return
+
+ if state == 'idle':
+ if self.stream is not None: self.stream.reset()
+
+ elif state == 'run':
+ if self.state in ['idle', 'pause'] and self.stream is not None:
+ self.set_write(True)
+
+ if step:
+ self._i2c_command(I2C_STEP)
+ state = 'pause'
+
+ else: self._i2c_command(I2C_RUN)
+
+ else: self._state_transition_error(state)
+
+ elif state == 'pause'
+ if self.state == 'run':
+ if optional: self._i2c_command(I2C_OPTIONAL_PAUSE)
+ else: self._i2c_command(I2C_PAUSE)
+
+ else: self._state_transition_error(state)
+
+ elif state == 'stop':
+ if self.state in ['run', 'pause']: self._flush()
+ else: self._state_transition_error(state)
+
+ elif state == 'estop': self._i2c_command(I2C_ESTOP)
+
+ elif state == 'home':
+ if self.state == 'idle': self._i2c_command(I2C_HOME)
+ else: self._state_transition_error(state)
- def close(self):
- self.sp.close()
+ else: raise Exception('Unrecognized state "%s"' % state)
+
+ self.state = state
+
+
+ def _i2c_command(self, cmd, word = None):
+ if word is not None:
+ self.i2c_bus.write_word_data(self.i2c_addr, cmd, word)
+ self.i2c_bus.write_byte(self.i2c_addr, cmd)
+
+
+ def _flush(self):
+ if self.stream is not None: self.stream.reset()
+
+ self._i2c_command(I2C_FLUSH, word = self.flush_id)
+ self.queue_command('$end_flush %u' % self.flush_id)
+
+ self.flush_id += 1
+ if 1 << 16 <= self.flush_id: self.flush_id = 1
+
+
+ def report(self): self._i2c_command(I2C_REPORT)
+
+
+ def load_next_command(self, cmd):
+ log.info(cmd)
+ self.command = bytes(cmd.strip() + '\n', 'utf-8')
def set_write(self, enable):
def serial_write(self):
- # Finish writing current line
- if self.out_buf is not None:
+ # Finish writing current command
+ if self.command is not None:
try:
- count = self.sp.write(self.out_buf)
- log.debug('Wrote %d', count)
+ count = self.sp.write(self.command)
+
except Exception as e:
self.set_write(False)
raise e
- self.out_buf = self.out_buf[count:]
- if len(self.out_buf): return
- self.out_buf = None
-
- # Close file if stopped
- if self.state == 'stop' and self.f is not None:
- self.f.close()
- self.f = None
-
- # Read next line if running
- if self.state == 'run':
- # Strip white-space & comments and encode to bytearray
- self.out_buf = self.f.readline().strip()
- self.out_buf = re.sub(r';.*', '', self.out_buf)
- if len(self.out_buf):
- log.info(self.out_buf)
- self.out_buf = bytes(self.out_buf + '\n', 'utf-8')
- else: self.out_buf = None
-
- # Pause if done stepping
- if self.step:
- self.step -= 1
- if not self.step:
- self.state = 'pause'
-
- # Stop if not longer running
- else:
- self.set_write(False)
- self.step = 0
+ self.command = self.command[count:]
+ if len(self.command): return # There's more
+ self.command = None
+
+ # Load next command from queue
+ if len(self.queue): self.load_next_command(self.queue.pop())
+
+ # Load next GCode command, if running or paused
+ elif self.state in ['run', 'pause'] and self.stream is not None:
+ cmd = self.stream.next()
+
+ if cmd is None: self.set_write(False)
+ else: self.load_next_command(cmd)
+
+ # Else stop writing
+ else: self.set_write(False)
def serial_read(self):
try:
- data = self.sp.read(self.sp.inWaiting())
+ data = self.sp.read(self.sp.in_waiting)
self.in_buf += data.decode('utf-8')
except Exception as e:
self.in_buf = self.in_buf[i + 1:]
if line:
- self.ctrl.output_queue.put(line)
- log.debug(line)
+ try:
+ msg = json.loads(line)
+ if 'firmware' in msg: self.report()
+ if 'es' in msg and msg['es']: self.estop()
- def queue_handler(self, fd, events):
- if self.ctrl.input_queue.empty(): return
+ self.vars.update(msg)
+ self.ctrl.web.broadcast(msg)
+ log.debug(line)
- data = self.ctrl.input_queue.get()
- self.sp.write(data.encode())
+ except Exception as e:
+ log.error('%s, data: %s', e, line)
- def home(self):
- if self.state != 'idle': raise Exception('Already running')
- # TODO
+ def queue_command(self, cmd):
+ self.queue.append(cmd)
+ self.set_write(True)
- def start(self, path):
- if self.f is None:
- self.f = open('upload' + path, 'r')
- self.line = 0
+ def load(self, path):
+ if self.stream is None:
+ self.stream = bbctrl.GCodeStream(path)
- self.set_write(True)
- self.state = 'run'
+ def mdi(self, cmd):
+ if self.state != 'idle':
+ raise Exception('Busy, cannot run MDI command')
+
+ self.queue_command(cmd)
+
+
+ def jog(self, axes):
+ # TODO jogging via I2C
+
+ if self.state != 'idle': raise Exception('Busy, cannot jog')
- def stop(self):
- if self.state == 'idle': return
- self.state == 'stop'
+ axes = ["{:6.5f}".format(x) for x in axes]
+ self.queue_command('$jog ' + ' '.join(axes))
- def pause(self, optional):
- self.state = 'pause'
+ def set(self, index, code, value):
+ self.queue_command('${}{}={}'.format(index, code, value))
- def step(self):
- self.step += 1
- if self.state == 'idle': self.start()
- else: self.state = 'run'
+ def home(self): self._state_transition('home')
+ def start(self): self._state_transition('run')
+ def estop(self): self._state_transition('estop')
+ def stop(self): self._state_transition('stop')
+ def pause(self, opt): self._state_transition('pause', optional = opt)
+ def step(self): self._state_transition('run', step = True)
--- /dev/null
+import json
+import logging
+
+import bbctrl
+
+
+log = logging.getLogger('Config')
+
+
+class Config(object):
+ def __init__(self, ctrl):
+ self.ctrl = ctrl
+
+ # Load config template
+ with open(bbctrl.get_resource('http/config-template.json'), 'r',
+ encoding = 'utf-8') as f:
+ self.template = json.load(f)
+
+
+ def load_path(self, path):
+ with open(path, 'r') as f:
+ return json.load(f)
+
+
+ def load(self):
+ try:
+ return self.load_path('config.json')
+
+ except Exception as e:
+ log.warning('%s', e)
+ return self.load_path(
+ bbctrl.get_resource('http/default-config.json'))
+
+
+ def save(self, config):
+ with open('config.json', 'w') as f:
+ json.dump(config, f)
+
+ self.update(config)
+
+ log.info('Saved')
+
+
+ def encode_cmd(self, index, value, spec):
+ if spec['type'] == 'enum': value = spec['values'].index(value)
+ elif spec['type'] == 'bool': value = 1 if value else 0
+ elif spec['type'] == 'percent': value /= 100.0
+
+ self.ctrl.avr.set(index, spec['code'], value)
+
+
+ def encode_category(self, index, config, category):
+ for key, spec in category.items():
+ if key in config:
+ self.encode_cmd(index, config[key], spec)
+
+
+ def encode(self, index, config, tmpl):
+ for category in tmpl.values():
+ self.encode_category(index, config, category)
+
+
+ def update(self, config):
+ # Motors
+ tmpl = self.template['motors']
+ for index in range(len(config['motors'])):
+ self.encode(index + 1, config['motors'][index], tmpl)
+
+ # Axes
+ tmpl = self.template['axes']
+ for axis in 'xyzabc':
+ if not axis in config['axes']: continue
+ self.encode(axis, config['axes'][axis], tmpl)
+
+ # Switches
+ tmpl = self.template['switches']
+ for index in range(len(config['switches'])):
+ self.encode_category(index + 1, config['switches'][index], tmpl)
+
+ # Spindle
+ tmpl = self.template['spindle']
+ self.encode_category('', config['spindle'], tmpl)
--- /dev/null
+import logging
+
+import bbctrl
+
+
+log = logging.getLogger('Ctrl')
+
+
+class Ctrl(object):
+ def __init__(self, args, ioloop):
+ self.args = args
+ self.ioloop = ioloop
+
+ self.config = bbctrl.Config(self)
+ self.web = bbctrl.Web(self)
+ self.avr = bbctrl.AVR(self)
+ self.jog = bbctrl.Jog(self)
+ self.lcd = bbctrl.LCD(self)
--- /dev/null
+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: 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
+ line += ' N%d' % self.line
+ self.line += 1
+
+ return line
# Listen for input events
class Jog(inevent.JogHandler):
def __init__(self, ctrl):
+ self.ctrl = ctrl
+
config = {
"deadband": 0.1,
"axes": [ABS_X, ABS_Y, ABS_RZ, ABS_Z],
def processed_events(self):
if self.v != self.lastV:
self.lastV = self.v
-
- v = ["{:6.5f}".format(x) for x in self.v]
- cmd = '$jog ' + ' '.join(v) + '\n'
- input_queue.put(cmd)
+ self.ctrl.avr.jog(self.v)
def changed(self):
def put_ok(self, path): self.ctrl.avr.start(path)
+class EStopHandler(bbctrl.APIHandler):
+ def put_ok(self): self.ctrl.avr.estop()
+
+
class StopHandler(bbctrl.APIHandler):
def put_ok(self): self.ctrl.avr.stop()
class Connection(sockjs.tornado.SockJSConnection):
def heartbeat(self):
self.timer = self.ctrl.ioloop.call_later(3, self.heartbeat)
- self.send_json({'heartbeat': self.count})
+ self.send({'heartbeat': self.count})
self.count += 1
- def send_json(self, data):
- self.send(str.encode(json.dumps(data)))
-
-
def on_open(self, info):
self.ctrl = self.session.server.ctrl
+ self.clients = self.ctrl.web.clients
self.timer = self.ctrl.ioloop.call_later(3, self.heartbeat)
self.count = 0;
- self.ctrl.clients.append(self)
- self.send_json(self.ctrl.state)
+ self.clients.append(self)
+ self.send(self.ctrl.avr.vars)
def on_close(self):
self.ctrl.ioloop.remove_timeout(self.timer)
- self.ctrl.clients.remove(self)
+ self.clients.remove(self)
def on_message(self, data):
- self.ctrl.input_queue.put(data + '\n')
+ self.ctrl.avr.mdi(data)
class Web(tornado.web.Application):
def __init__(self, ctrl):
self.ctrl = ctrl
+ self.clients = []
handlers = [
(r'/api/load', LoadHandler),
(r'/api/file(/.+)?', bbctrl.FileHandler),
(r'/api/home', HomeHandler),
(r'/api/start(/.+)', StartHandler),
+ (r'/api/estop', EStopHandler),
(r'/api/stop', StopHandler),
(r'/api/pause', PauseHandler),
(r'/api/pause/optional', OptionalPauseHandler),
sys.exit(1)
log.info('Listening on http://%s:%d/', ctrl.args.addr, ctrl.args.port)
+
+
+ def broadcast(self, msg):
+ if self.clients:
+ self.clients[0].broadcast(self.clients, msg)
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
from bbctrl.AVR import AVR
help = 'LCD I2C port')
parser.add_argument('--lcd-addr', default = 0x27, type = int,
help = 'LCD I2C address')
+ parser.add_argument('--avr-port', default = 0, type = int,
+ help = 'AVR I2C port')
+ parser.add_argument('--avr-addr', default = 0x2b, type = int,
+ help = 'AVR I2C address')
parser.add_argument('-v', '--verbose', action = 'store_true',
help = 'Verbose output')
parser.add_argument('-l', '--log', metavar = "FILE",
+++ /dev/null
-{
- "motors": [
- {
- "motor-map": "x",
- "step-angle": 1.8,
- "travel-per-rev": 3.175,
- "microsteps": 16,
- "polarity": "normal",
- "power-mode": "always-on",
- "power-level": 80,
- "stallguard": 70
- }, {
- "motor-map": "y"
- }, {
- "motor-map": "z"
- }, {
- "motor-map": "a"
- }
- ],
-
- "axes": {
- "x": {
- "mode": "standard",
- "max-velocity": 16000,
- "max-feedrate": 16000,
- "max-jerk": 40,
- "min-soft-limit": 0,
- "max-soft-limit": 150,
- "max-homing-jerk": 80,
- "junction-deviation": 0.05,
- "search-velocity": 500,
- "latch-velocity": 100,
- "latch-backoff": 5,
- "zero-backoff": 1
- },
-
- "y": {
- "mode": "standard"
- },
-
- "z": {
- "mode": "standard"
- },
-
- "a": {
- "mode": "radius",
- "max-velocity": 1000000,
- "max-feedrate": 1000000,
- "min-soft-limit": 0,
- "max-soft-limit": 0
- },
-
- "b": {
- "mode": "disabled"
- },
-
- "c": {
- "mode": "disabled"
- }
- },
-
- "switches": [
- {"type": "normally-open"},
- {},
- {},
- {},
- {},
- {},
- {},
- {}
- ],
-
- "spindle": {
- }
-}
"type": "percent",
"unit": "%",
"default": 70,
- "code": "sg"
+ "code": "th"
}
}
},
"code": "tm"
},
"min-switch": {
- "type": "int",
- "unit": "id",
- "min": 0,
- "max": 8,
- "default": 0,
+ "type": "enum",
+ "values": ["disabled", "normally-open", "normally-closed"],
+ "default": "disabled",
"code": "sn"
},
"max-switch": {
- "type": "int",
- "unit": "id",
- "min": 0,
- "max": 8,
- "default": 0,
+ "type": "enum",
+ "values": ["disabled", "normally-open", "normally-closed"],
+ "default": "disabled",
"code": "sx"
}
},
},
"switches": {
- "type": {
+ "estop": {
+ "type": "enum",
+ "values": ["disabled", "normally-open", "normally-closed"],
+ "default": "disabled",
+ "code": "et"
+ },
+ "probe": {
"type": "enum",
- "values": ["normally-open", "normally-closed"],
- "default": "normally-closed",
- "code": "sw"
+ "values": ["disabled", "normally-open", "normally-closed"],
+ "default": "disabled",
+ "code": "pt"
}
},
--- /dev/null
+{
+ "motors": [
+ {
+ "motor-map": "x",
+ "step-angle": 1.8,
+ "travel-per-rev": 3.175,
+ "microsteps": 16,
+ "polarity": "normal",
+ "power-mode": "always-on",
+ "power-level": 80,
+ "stallguard": 70
+ }, {
+ "motor-map": "y"
+ }, {
+ "motor-map": "z"
+ }, {
+ "motor-map": "a"
+ }
+ ],
+
+ "axes": {
+ "x": {
+ "mode": "standard",
+ "max-velocity": 16000,
+ "max-feedrate": 16000,
+ "max-jerk": 40,
+ "min-soft-limit": 0,
+ "max-soft-limit": 150,
+ "max-homing-jerk": 80,
+ "junction-deviation": 0.05,
+ "search-velocity": 500,
+ "latch-velocity": 100,
+ "latch-backoff": 5,
+ "zero-backoff": 1
+ },
+
+ "y": {
+ "mode": "standard"
+ },
+
+ "z": {
+ "mode": "standard"
+ },
+
+ "a": {
+ "mode": "radius",
+ "max-velocity": 1000000,
+ "max-feedrate": 1000000,
+ "min-soft-limit": 0,
+ "max-soft-limit": 0
+ },
+
+ "b": {
+ "mode": "disabled"
+ },
+
+ "c": {
+ "mode": "disabled"
+ }
+ },
+
+ "switches": {},
+ "spindle": {}
+}
h3, .pure-control-group
display inline-block
+@keyframes blink
+ 50%
+ fill #ff9d00
.control-view
table
td, th
border 1px solid #ddd
+ .axes
+ .axis-x .name
+ color #f00
-.axes
- .axis-x .name
- color #f00
+ .axis-y .name
+ color #0f0
- .axis-y .name
- color #0f0
+ .axis-z .name
+ color #00f
- .axis-z .name
- color #00f
+ .axis-a .name
+ color #f80
- .axis-a .name
- color #f80
+ .axis-b .name
+ color #0ff
- .axis-b .name
- color #0ff
+ .axis-c .name
+ color #f0f
- .axis-c .name
- color #f0f
+ td, th
+ padding 2px
- td, th
- padding 2px
+ th
+ text-align center
- th
- text-align center
+ td
+ text-align right
+ font-family Courier
- td
- text-align right
- font-family Courier
+ .axis
+ .name
+ text-transform capitalize
- .axis
- .name
- text-transform capitalize
+ .name, .position
+ font-size 36pt
+ line-height 36pt
- .name, .position
- font-size 36pt
- line-height 36pt
+ .estop
+ display inline-block
+ width 190px
+ transition 250ms
-@keyframes blink
- 50%
- fill #ff9d00
+ &.active .ring
+ animation blink 2s step-start 0s infinite
-.estop
- display inline-block
- width 190px
- transition 250ms
+ svg
+ cursor pointer
- &.active .ring
- animation blink 2s step-start 0s infinite
+ .button:hover
+ filter brightness(120%)
- svg
- cursor pointer
+ .jog
+ float right
- .button:hover
- filter brightness(120%)
+ .jog svg
+ text
+ font-family Sans
+ font-weight bold
+ stroke transparent
+ fill #444
-.jog
- float right
+ .button
+ cursor pointer
+ stroke #4c4c4c
-.jog svg
- text
- font-family Sans
- font-weight bold
- stroke transparent
- fill #444
+ &:hover
+ stroke #e55
- .button
- cursor pointer
- stroke #4c4c4c
+ path
+ overflow visible
- &:hover
- stroke #e55
+ .house
+ stroke #444
+ fill #444
- path
+ .ring
+ cursor pointer
overflow visible
- .house
- stroke #444
- fill #444
+ .button
+ stroke transparent
- .ring
- cursor pointer
- overflow visible
+ &:hover
+ stroke #e55
- .button
- stroke transparent
+ text
+ font-size 10pt
+ text-anchor middle
- &:hover
- stroke #e55
+ .info
+ float right
+ clear right
- text
- font-size 10pt
- text-anchor middle
+ th, td
+ padding 3px
-.info
- float right
- clear right
+ th
+ text-align right
- th, td
- padding 3px
+ .overrides
+ clear both
- th
- text-align right
+ .override
+ margin 0.5em
+ display inline-block
-.overrides
- clear both
+ .percent
+ display inline-block
+ width 3em
- .override
- margin 0.5em
- display inline-block
+ input
+ border-radius 0
+ margin -0.4em 0.5em
- .percent
- display inline-block
- width 3em
+ .mdi
+ clear both
+ white-space nowrap
+ margin 0.5em 0
input
- border-radius 0
- margin -0.4em 0.5em
+ width 90%
-.mdi
- clear both
- white-space nowrap
- margin 0.5em 0
-
- input
- width 90%
-
-.toolbar
- clear both
- margin 0.5em 0
+ .toolbar
+ clear both
+ margin 0.5em 0
- .spacer
- display inline-block
- width 1px
- height 1px
- margin 0 1em
-
-.gcode
- clear both
- border 2px inset #ccc
- border-radius 5px
- overflow auto
- width 100%
- max-width 100%
- min-width 100%
- height 200px
- padding 2px
- white-space pre
+ .spacer
+ display inline-block
+ width 1px
+ height 1px
+ margin 0 1em
+
+ .gcode
+ clear both
+ border 2px inset #ccc
+ border-radius 5px
+ overflow auto
+ width 100%
+ max-width 100%
+ min-width 100%
+ height 200px
+ padding 2px
+ white-space pre