.sconf_temp/
.sconsign.dblite
/build
+/dist
+/pkg
+/mnt
node_modules
*~
\#*
--- /dev/null
+recursive-include src/py/bbctrl/http *
+include package.json README.md scripts/install.sh
RSYNC_EXCLUDE := \*.pyc __pycache__ \*.egg-info \\\#* \*~ .\\\#\*
RSYNC_EXCLUDE := $(patsubst %,--exclude %,$(RSYNC_EXCLUDE))
-RSYNC_OPTS := $(RSYNC_EXCLUDE) -rLv --no-g --delete --force
+RSYNC_OPTS := $(RSYNC_EXCLUDE) -rv --no-g --delete --force
+
+VERSION := $(shell sed -n 's/^.*"version": "\([^"]*\)",.*$$/\1/p' package.json)
+PKG_NAME := bbctrl-$(VERSION)
+PUB_PATH := root@buildbotics.com:/var/www/buildbotics.com/bbctrl
ifndef DEST
DEST=mnt
all: html css js static
-copy: all
- mkdir -p $(DEST)/bbctrl/src/py $(DEST)/bbctrl/build
- rsync $(RSYNC_OPTS) src/py $(DEST)/bbctrl/src/
- rsync $(RSYNC_OPTS) setup.py README.md $(DEST)/bbctrl
+copy: pkg
+ rsync $(RSYNC_OPTS) pkg/$(PKG_NAME)/ $(DEST)/bbctrl/
+
+pkg: all
+ ./setup.py sdist
+
+publish: pkg
+ echo -n $(VERSION) > dist/latest.txt
+ rsync $(RSYNC_OPTS) dist/$(PKG_NAME).tar.bz2 dist/latest.txt $(PUB_PATH)/
mount:
mkdir -p $(DEST)
rm -f $(shell find "$(DIR)" -name \*~)
clean: tidy
- rm -rf build html
+ rm -rf build html pkg
dist-clean: clean
rm -rf node_modules
-.PHONY: all install html css static templates clean tidy copy mount umount
+.PHONY: all install html css static templates clean tidy copy mount umount pkg
{
"name": "bbctrl",
- "private": true,
+ "version": "0.1.5",
+ "homepage": "https://github.com/buildbotics/rpi-firmware",
+ "license": "GPL 3+",
"dependencies": {
"autoprefixer": ">=3.0.0",
--- /dev/null
+#!/bin/bash
+
+sudo ./setup.py install
+sudo service bbctrl restart
chmod +x /etc/init.d/bbctrl
update-rc.d bbctrl defaults
+# Install upgrade script
+cp upgrade-bbctrl /usr/local/bin
+
# Disable Pi 3 USART BlueTooth swap
echo -e "\ndtoverlay=pi3-disable-bt" >> /boot/config.txt
# sudo systemctl disable hciuart
--- /dev/null
+#!/bin/bash -e
+
+(
+ flock -n 9
+
+ VERSION=$(curl -s https://buildbotics.com/bbctrl/latest.txt)
+ PKG_NAME=bbctrl-$VERSION
+ PKG=$PKG_NAME.tar.bz2
+ PKG_URL=https://buildbotics.com/bbctrl/$PKG
+
+ logger Installing bbctrl firmware $VERSION
+
+ echo Downloading $PKG_URL
+ curl -s $PKG_URL > $PKG
+
+ echo Unpacking $PKG
+ tar xf $PKG
+
+ echo Installing $PKG
+ (cd $PKG_NAME; ./scripts/install.sh)
+
+ echo Cleaning up
+ rm -rf $PKG_NAME $PKG
+
+ echo Success
+
+ logger bbctrl firmware $VERSION installed
+
+) 9> /var/lock/bbctrl.upgrade.lock
--- /dev/null
+[sdist]
+formats=bztar
#!/usr/bin/env python3
from setuptools import setup
+import json
+
+pkg = json.load(open('package.json', 'r'))
+
setup(
- name = 'bbctrl',
- version = '0.0.2',
+ name = pkg['name'],
+ version = pkg['version'],
description = 'Buildbotics Machine Controller',
long_description = open('README.md', 'rt').read(),
author = 'Joseph Coffland',
author_email = 'joseph@buildbotics.org',
platforms = ['any'],
- license = 'GPL 3+',
- url = 'https://github.com/buildbotics/rpi-firmware',
+ license = pkg['license'],
+ url = pkg['homepage'],
package_dir = {'': 'src/py'},
packages = ['bbctrl', 'inevent', 'lcd'],
include_package_data = True,
- eager_resources = ['bbctrl/http/*'],
entry_points = {
'console_scripts': [
'bbctrl = bbctrl:run'
li.pure-menu-item(v-for="motor in config.motors")
a.pure-menu-link(:href="'#motor:' + $index") Motor {{$index}}
- li.pure-menu-heading
- a.pure-menu-link(href="#axis:x") Axes
-
- li.pure-menu-item(v-for="axis in 'xyzabc'")
- a.pure-menu-link(href="#axis:{{axis}}") {{axis | capitalize}} Axis
-
li.pure-menu-heading
a.pure-menu-link(href="#spindle") Spindle
#templates
include ../../build/templates.jade
+ iframe#download-target(style="display:none")
+
script(src="//code.jquery.com/jquery-1.11.3.min.js")
script(src="//cdn.jsdelivr.net/vue/1.0.17/vue.js")
script(src="js/sockjs.min.js")
script#admin-view-template(type="text/x-template")
#admin
- h2 Backup configuration
+ h2 Configuration
button.pure-button.pure-button-primary(@click="backup") Backup
- h2 Restore configuration
- button.pure-button.pure-button-primary(@click="restore") Restore
+ label.pure-button.pure-button-primary.file-upload
+ input(type="file", accept=".json", @change="restore")
+ | Restore
+ message(:show.sync="configRestored")
+ h3(slot="header") Success
+ p(slot="body") Configuration restored.
- h2 Reset to default configuration
- button.pure-button.pure-button-primary(@click="reset") Reset
+ button.pure-button.pure-button-primary(@click="confirmReset = true")
+ | Reset
+ message(:show.sync="confirmReset")
+ h3(slot="header") Reset to default configuration?
+ p(slot="body") All configuration changes will be lost.
+ div(slot="footer")
+ button.pure-button.button-error(@click="confirmReset = false") Cancel
+ button.pure-button.button-success(@click="reset") OK
- h2 Check for new firmware
- button.pure-button.pure-button-primary(@click="check") Check
+ message(:show.sync="configReset")
+ h3(slot="header") Success
+ p(slot="body") Configuration reset.
- h2 Upgrade firmware
+ h2 Firmware
+ button.pure-button.pure-button-primary(@click="check") Check
button.pure-button.pure-button-primary(@click="upgrade") Upgrade
+
+ p
+ table.pure-table
+ tr
+ th Current version
+ td {{config.version}}
+
+ tr(v-if="latest")
+ th Latest version
+ td {{latest}}
+
+ message(:show.sync="firmwareUpgrading")
+ h3(slot="header") Firmware upgrading
+ p(slot="body") Please wait. . .
+++ /dev/null
-script#axis-view-template(type="text/x-template")
- #axis
- h1 {{index | capitalize}} Axis Configuration
-
- form.pure-form.pure-form-aligned
- fieldset(v-for="category in template.axes", :class="$key")
- h2 {{$key}}
-
- templated-input(v-for="templ in category", :name="$key",
- :model.sync="axis[$key]", :template="templ")
option(v-for="file in files", :value="file") {{file}}
.gcode(:class="{placeholder: !gcode}")
- {{{gcode || 'GCode displays here.'}}}
+ | {{{gcode || 'GCode displays here.'}}}
section#content2.tab-content
.mdi.pure-form
--- /dev/null
+script#message-template(type="text/x-template")
+ .modal-mask(v-show="show", transition="modal")
+ .modal-wrapper
+ .modal-container
+ .modal-header
+ slot(name="header") default header
+ .modal-body
+ slot(name="body") default body
+ .modal-footer
+ slot(name="footer")
+ button.pure-button.button-success(@click="show = false") OK
'use strict'
+var api = require('./api');
+
+
module.exports = {
template: '#admin-view-template',
props: ['config'],
- ready: function () {
+ data: function () {
+ return {
+ configRestored: false,
+ confirmReset: false,
+ configReset: false,
+ firmwareUpgrading: false,
+ latest: '',
+ }
},
+ events: {
+ connected: function () {
+ if (this.firmwareUpgrading) location.reload(true);
+ }
+ },
+
+
+ ready: function () {},
+
+
methods: {
backup: function () {
- alert('Not yet implemented');
+ document.getElementById('download-target').src = '/api/config/download';
},
- restore: function () {
- alert('Not yet implemented');
+ restore: function (e) {
+ var files = e.target.files || e.dataTransfer.files;
+ if (!files.length) return;
+
+ var fr = new FileReader();
+ fr.onload = function (e) {
+ var config;
+
+ try {
+ config = JSON.parse(e.target.result);
+ } catch (e) {
+ alert("Invalid config file");
+ return;
+ }
+
+ api.put('config/save', config).done(function (data) {
+ this.$dispatch('update');
+ this.configRestored = true;
+
+ }.bind(this)).fail(function (error) {
+ alert('Restore failed: ' + error);
+ })
+ }.bind(this);
+
+ fr.readAsText(files[0]);
},
reset: function () {
- alert('Not yet implemented');
+ this.confirmReset = false;
+ api.put('config/reset').done(function () {
+ this.configReset = true;
+
+ }.bind(this)).fail(function (error) {
+ alert('Reset failed: ' + error);
+ });
},
check: function () {
- alert('Not yet implemented');
+ $.ajax({
+ type: 'GET',
+ url: 'https://buildbotics.com/bbctrl/latest.txt',
+ cache: false
+
+ }).done(function (data) {
+ this.latest = data;
+
+ }.bind(this)).fail(function (error) {
+ alert('Failed to get latest version information');
+ });
},
upgrade: function () {
- alert('Not yet implemented');
+ this.firmwareUpgrading = true;
+ api.put('upgrade');
}
}
}
config = $.extend({
type: method,
url: '/api/' + url,
- dataType: 'json'
+ dataType: 'json',
+ cache: false
}, config);
if (typeof data == 'object') {
'estop': {template: '#estop-template'},
'loading-view': {template: '<h1>Loading...</h1>'},
'control-view': require('./control-view'),
- 'axis-view': require('./axis-view'),
'motor-view': require('./motor-view'),
'spindle-view': require('./spindle-view'),
'switches-view': require('./switches-view'),
},
- connected: function () {this.update()}
+ connected: function () {this.update()},
+ update: function () {this.update()}
},
update: function () {
- $.get('/config-template.json', {cache: false})
+ $.ajax({type: 'GET', url: '/config-template.json', cache: false})
.success(function (data, status, xhr) {
this.template = data;
- api.get('load').done(function (data) {
+ api.get('config/load').done(function (data) {
this.config = data;
this.parse_hash();
}.bind(this))
save: function () {
- api.put('save', this.config).done(function (data) {
+ api.put('config/save', this.config).done(function (data) {
this.modified = false;
- }.bind(this)).fail(function (xhr, status) {
- alert('Save failed: ' + status + ': ' + xhr.responseText);
+ }.bind(this)).fail(function (error) {
+ alert('Save failed: ' + error);
});
}
}
+++ /dev/null
-'use strict'
-
-
-module.exports = {
- template: '#axis-view-template',
- props: ['index', 'config', 'template'],
-
-
- data: function () {
- return {
- active: false,
- axis: {}
- }
- },
-
-
- watch: {
- index: function() {this.update();}
- },
-
-
- events: {
- 'input-changed': function() {
- this.$dispatch('config-changed');
- return false;
- }
- },
-
-
- attached: function () {this.active = true; this.update()},
- detached: function () {this.active = false},
-
-
- methods: {
- update: function () {
- if (!this.active) return;
-
- Vue.nextTick(function () {
- if (this.config.hasOwnProperty('axes'))
- this.axis = this.config.axes[this.index];
- else this.axes = {};
-
- var template = this.template.axes;
- for (var category in template)
- for (var key in template[category])
- if (!this.axis.hasOwnProperty(key))
- this.$set('axis["' + key + '"]',
- template[category][key].default);
- }.bind(this));
- }
- }
-}
enabled: function (axis) {
var axis = axis.toLowerCase();
- return axis in this.config.axes &&
- this.config.axes[axis].mode != 'disabled';
+
+ for (var i = 0; i < this.config.motors.length; i++) {
+ var motor = this.config.motors[i];
+ if (motor.axis.toLowerCase() == axis &&
+ (motor.enabled || typeof motor.enabled == 'undefined')) return true;
+ }
+
+ return false;
},
// Register global components
Vue.component('templated-input', require('./templated-input'));
+ Vue.component('message', require('./message'));
// Vue app
require('./app');
--- /dev/null
+'use strict'
+
+
+module.exports = {
+ template: '#message-template',
+
+ props: {
+ show: {
+ type: Boolean,
+ required: true,
+ twoWay: true
+ }
+ }
+}
self.write_json(e)
- def write_json(self, data):
- self.write(json.dumps(data))
+ def write_json(self, data, pretty = False):
+ if pretty: data = json.dumps(data, indent = 2, separators = (',', ': '))
+ else: data = json.dumps(data)
+ self.write(data)
import json
import logging
+import pkg_resources
import bbctrl
-
log = logging.getLogger('Config')
+default_config = {
+ "motors": [
+ {"axis": "X"},
+ {"axis": "Y"},
+ {"axis": "Z"},
+ {"axis": "A"},
+ ],
+ "switches": {},
+ "spindle": {},
+ }
+
class Config(object):
def __init__(self, ctrl):
self.ctrl = ctrl
+ self.version = pkg_resources.require('bbctrl')[0].version
+ default_config['version'] = self.version
# Load config template
with open(bbctrl.get_resource('http/config-template.json'), 'r',
except Exception as e:
log.warning('%s', e)
- return self.load_path(
- bbctrl.get_resource('http/default-config.json'))
+ return default_config
def save(self, config):
+ self.update(config)
+
+ config['version'] = self.version
+
with open('config.json', 'w') as f:
json.dump(config, f)
- self.update(config)
-
log.info('Saved')
# 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)
+ self.encode(index, config['motors'][index], tmpl)
# Switches
tmpl = self.template['switches']
for index in range(len(config['switches'])):
- self.encode_category(index + 1, config['switches'][index], tmpl)
+ self.encode_category(index, config['switches'][index], tmpl)
# Spindle
tmpl = self.template['spindle']
import tornado
import sockjs.tornado
import logging
+import datetime
import bbctrl
-class LoadHandler(bbctrl.APIHandler):
+class ConfigLoadHandler(bbctrl.APIHandler):
def get(self): self.write_json(self.ctrl.config.load())
-class SaveHandler(bbctrl.APIHandler):
+class ConfigDownloadHandler(bbctrl.APIHandler):
+ def set_default_headers(self):
+ filename = datetime.date.today().strftime('bbctrl-%Y%m%d.json')
+ self.set_header('Content-Type', 'application/octet-stream')
+ self.set_header('Content-Disposition',
+ 'attachment; filename="%s"' % filename)
+
+ def get(self):
+ self.write_json(self.ctrl.config.load(), pretty = True)
+
+
+class ConfigSaveHandler(bbctrl.APIHandler):
def put_ok(self): self.ctrl.config.save(self.json)
+class ConfigResetHandler(bbctrl.APIHandler):
+ def put_ok(self): self.ctrl.config.reset()
+
+
+class UpgradeHandler(bbctrl.APIHandler):
+ def put_ok(self):
+ import subprocess
+ ret = subprocess.Popen(['upgrade-bbctrl'])
+
+
class HomeHandler(bbctrl.APIHandler):
def put_ok(self): self.ctrl.avr.home()
self.ctrl.avr.mdi(data)
+class StaticFileHandler(tornado.web.StaticFileHandler):
+ def set_extra_headers(self, path):
+ self.set_header('Cache-Control',
+ 'no-store, no-cache, must-revalidate, max-age=0')
+
class Web(tornado.web.Application):
def __init__(self, ctrl):
self.clients = []
handlers = [
- (r'/api/load', LoadHandler),
- (r'/api/save', SaveHandler),
+ (r'/api/config/load', ConfigLoadHandler),
+ (r'/api/config/download', ConfigDownloadHandler),
+ (r'/api/config/save', ConfigSaveHandler),
+ (r'/api/config/reset', ConfigResetHandler),
+ (r'/api/upgrade', UpgradeHandler),
(r'/api/file(/.+)?', bbctrl.FileHandler),
(r'/api/home', HomeHandler),
(r'/api/start(/.+)', StartHandler),
(r'/api/zero(/[xyzabcXYZABC])?', ZeroHandler),
(r'/api/override/feed/([\d.]+)', OverrideFeedHandler),
(r'/api/override/speed/([\d.]+)', OverrideSpeedHandler),
- (r'/(.*)', tornado.web.StaticFileHandler,
+ (r'/(.*)', StaticFileHandler,
{'path': bbctrl.get_resource('http/'),
"default_filename": "index.html"}),
]
{
"motors": {
"general": {
- "motor-map": {
+ "axis": {
"type": "enum",
- "values": ["x", "y", "z", "a", "b", "c"],
- "default": "x",
- "code": "ma"
+ "values": ["X", "Y", "Z", "A", "B", "C"],
+ "default": "X",
+ "code": "an"
+ }
+ },
+
+ "power": {
+ "power-mode": {
+ "type": "enum",
+ "values": ["disabled", "always-on", "in-cycle", "when-moving"],
+ "default": "in-cycle",
+ "code": "pm"
+ },
+ "min-power": {
+ "type": "percent",
+ "unit": "%",
+ "default": 30,
+ "code": "pl"
+ },
+ "max-power": {
+ "type": "percent",
+ "unit": "%",
+ "default": 80,
+ "code": "th"
},
+ "idle-power": {
+ "type": "percent",
+ "unit": "%",
+ "default": 10,
+ "code": "ip"
+ }
+ },
+
+ "motion": {
"step-angle": {
"type": "float",
"min": 0,
"default": 16,
"code": "mi"
},
- "polarity": {
- "type": "enum",
- "values": ["normal", "reversed"],
- "default": "normal",
- "code": "po"
- }
- },
-
- "power": {
- "power-mode": {
- "type": "enum",
- "values": ["disabled", "always-on", "in-cycle", "when-moving"],
- "default": "in-cycle",
- "code": "pm"
- },
- "power-level": {
- "type": "percent",
- "unit": "%",
- "default": 80,
- "code": "pl"
- },
- "stallguard": {
- "type": "percent",
- "unit": "%",
- "default": 70,
- "code": "th"
- }
- }
- },
-
- "axes": {
- "motion": {
- "mode": {
- "type": "enum",
- "values": ["disabled", "standard", "inhibited", "radius"],
- "default": "disabled",
- "code": "am"
+ "reverse": {
+ "type": "bool",
+ "default": false,
+ "code": "rv"
},
"max-velocity": {
"type": "float",
},
"max-feedrate": {
"type": "float",
- "min": 0, "unit":
- "mm/min",
+ "min": 0,
+ "unit": "mm/min",
"default": 16000,
"code": "fr"
},
"unit": "mm/min³",
"default": 20,
"code": "jm"
- },
- "junction-deviation": {
- "type": "float",
- "min": 0,
- "unit": "mm",
- "default": 0.05,
- "code": "jd"
}
},
- "limits": {
+ "homing": {
+ "homing-mode": {
+ "type": "enum",
+ "values": [
+ "disabled", "stall", "min-normally-open", "min-normally-closed",
+ "max-normally-open", "max-normally-closed"],
+ "default": "disabled",
+ "code": "hm"
+ },
"min-soft-limit": {
"type": "float",
"unit": "mm",
"default": 150,
"code": "tm"
},
- "min-switch": {
- "type": "enum",
- "values": ["disabled", "normally-open", "normally-closed"],
- "default": "disabled",
- "code": "sn"
- },
- "max-switch": {
- "type": "enum",
- "values": ["disabled", "normally-open", "normally-closed"],
- "default": "disabled",
- "code": "sx"
- }
- },
-
- "homing": {
- "max-homing-jerk": {
- "type": "float",
- "min": 0,
- "unit": "mm/min³",
- "default": 40,
- "code": "jh"
- },
"search-velocity": {
"type": "float",
"min": 0,
"default": "PWM",
"code": "st"
},
- "spin-polarity": {
- "type": "enum",
- "values": ["normal", "reversed"],
- "default": "normal",
- "code": "sp"
+ "spin-reversed": {
+ "type": "bool",
+ "default": "false",
+ "code": "sr"
},
"max-spin": {
"type": "float",
"unit": "RPM",
"min": 0,
"default": 10000,
- "code": "ss"
+ "code": "sx"
+ },
+ "min-spin": {
+ "type": "float",
+ "unit": "RPM",
+ "min": 0,
+ "default": 0,
+ "code": "sm"
},
"spin-min-pulse": {
"type": "float",
+++ /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": {}
-}
border 1px solid #ddd
padding 0.5em
+.modal-mask
+ position fixed
+ z-index 9998
+ top 0
+ left 0
+ width 100%
+ height 100%
+ background-color rgba(0, 0, 0, .25)
+ display table
+ transition opacity .3s ease
+
+ .modal-wrapper
+ display table-cell
+ vertical-align middle
+
+ .modal-container
+ width 300px
+ margin 0px auto
+ padding 20px 30px
+ background-color #fff
+ border-radius 2px
+ box-shadow 0 2px 8px rgba(0, 0, 0, .33)
+ transition all .3s ease
+ font-family Helvetica, Arial, sans-serif
+
+
+.modal-enter, .modal-leave
+ opacity 0
+
+.modal-enter .modal-container, .modal-leave .modal-container
+ transform scale(1.1)
+
+
+label.file-upload
+ display inline-block
+
+ input[type="file"]
+ position fixed
+ top -1000px
@media only screen and (max-width 48em)
.header