Buildbotics CNC Controller Firmware Changelog
=============================================
+## v0.4.15
+ - Set GCode variables #5400 and #<_tool>.
+ - Added #<_timestamp> GCode variable.
+ - Print program start, stop and end with timestamp.
+
## v0.4.14
- Handle file uploads with '#' or '?' in the name.
- Added "step mode" to Web based jogging.
- Fixed touch screen Web jogging.
+ - Support V70 Stepper Online VFD.
## v0.4.13
- Support for OMRON MX2 VFD.
{
"name": "bbctrl",
- "version": "0.4.14",
+ "version": "0.4.15",
"homepage": "http://buildbotics.com/",
"repository": "https://github.com/buildbotics/bbctrl-firmware",
"license": "GPL-3.0+",
#define DRV8711_BLANK (0x32 | DRV8711_BLANK_ABT_bm)
#define DRV8711_DECAY (DRV8711_DECAY_DECMOD_MIXED | 16)
-#define DRV8711_STALL (DRV8711_STALL_SDCNT_2 | \
- DRV8711_STALL_VDIV_4 | 200)
#define DRV8711_DRIVE (DRV8711_DRIVE_IDRIVEP_50 | \
DRV8711_DRIVE_IDRIVEN_100 | \
DRV8711_DRIVE_TDRIVEP_500 | \
drv8711_state_t state;
current_t drive;
current_t idle;
- float stall_threspause;
+ uint16_t stall_vdiv;
+ uint8_t stall_thresh;
uint8_t microstep;
stall_callback_t stall_cb;
case SS_WRITE_OFF: return DRV8711_WRITE(DRV8711_OFF_REG, DRV8711_OFF);
case SS_WRITE_BLANK: return DRV8711_WRITE(DRV8711_BLANK_REG, DRV8711_BLANK);
case SS_WRITE_DECAY: return DRV8711_WRITE(DRV8711_DECAY_REG, DRV8711_DECAY);
- case SS_WRITE_STALL: return DRV8711_WRITE(DRV8711_STALL_REG, DRV8711_STALL);
+
+ case SS_WRITE_STALL: {
+ uint16_t reg = drv->stall_thresh | DRV8711_STALL_SDCNT_2 | drv->stall_vdiv;
+ return DRV8711_WRITE(DRV8711_STALL_REG, reg);
+ }
+
case SS_WRITE_DRIVE: return DRV8711_WRITE(DRV8711_DRIVE_REG, DRV8711_DRIVE);
case SS_WRITE_TORQUE:
uint16_t get_driver_flags(int driver) {return drivers[driver].flags;}
bool get_driver_stalled(int driver) {return drivers[driver].stalled;}
+
+
+float get_stall_volts(int driver) {
+ if (driver < 0 || DRIVERS <= driver) return 0;
+
+ float vdiv;
+ switch (drivers[driver].stall_vdiv) {
+ case DRV8711_STALL_VDIV_4: vdiv = 4; break;
+ case DRV8711_STALL_VDIV_8: vdiv = 8; break;
+ case DRV8711_STALL_VDIV_16: vdiv = 16; break;
+ default: vdiv = 32; break;
+ }
+
+ return 1.8 / 256 * vdiv * drivers[driver].stall_thresh;
+}
+
+
+void set_stall_volts(int driver, float volts) {
+ if (driver < 0 || DRIVERS <= driver) return;
+
+ uint16_t vdiv = DRV8711_STALL_VDIV_32;
+ uint16_t thresh = (uint16_t)(volts * 256 / 1.8);
+
+ if (thresh < 4 << 8) {
+ thresh >>= 2;
+ vdiv = DRV8711_STALL_VDIV_4;
+
+ } else if (thresh < 8 << 8) {
+ thresh >>= 3;
+ vdiv = DRV8711_STALL_VDIV_8;
+
+ } else if (thresh < 16 << 8) {
+ thresh >>= 4;
+ vdiv = DRV8711_STALL_VDIV_16;
+
+ } else {
+ if (thresh < 32 << 8) thresh >>= 5;
+ else thresh = 255;
+ }
+
+ drivers[driver].stall_vdiv = vdiv;
+ drivers[driver].stall_thresh = thresh;
+}
SPINDLE_TYPE_FR_D700,
SPINDLE_TYPE_SUNFAR_E300,
SPINDLE_TYPE_OMRON_MX2,
+ SPINDLE_TYPE_V70,
} spindle_type_t;
VAR(active_current, ac, f32, MOTORS, 0, 0) // Motor current now
VAR(driver_flags, df, u16, MOTORS, 1, 1) // Motor driver flags
VAR(driver_stalled, sl, b8, MOTORS, 0, 0) // Motor driver status
+VAR(stall_volts, tv, f32, MOTORS, 1, 0) // Motor BEMF threshold voltage
VAR(encoder, en, s32, MOTORS, 0, 0) // Motor encoder
VAR(error, ee, s32, MOTORS, 0, 0) // Motor position error
};
+const vfd_reg_t v70_regs[] PROGMEM = {
+ {REG_MAX_FREQ_READ, 0x0005, 0}, // Maximum operating frequency
+ {REG_FREQ_SET, 0x0201, 0}, // Set frequency in 0.1Hz
+ {REG_STOP_WRITE, 0x0200, 0}, // Stop
+ {REG_FWD_WRITE, 0x0200, 1}, // Run forward
+ {REG_REV_WRITE, 0x0200, 5}, // Run reverse
+ {REG_FREQ_READ, 0x0220, 0}, // Read operating frequency
+ {REG_STATUS_READ, 0x0210, 0}, // Read status
+ {REG_DISABLED},
+};
+
+
static vfd_reg_t regs[VFDREG];
static vfd_reg_t custom_regs[VFDREG];
case SPINDLE_TYPE_FR_D700: _load(fr_d700_regs); break;
case SPINDLE_TYPE_SUNFAR_E300: _load(sunfar_e300_regs); break;
case SPINDLE_TYPE_OMRON_MX2: _load(omron_mx2_regs); break;
+ case SPINDLE_TYPE_V70: _load(v70_regs); break;
default: break;
}
|
| and spindle type. The VFD must be rebooted after changing
| the above settings.
+
+ .notes(v-if="tool_type.startsWith('V70')")
+ h2 Notes
+ p Set the following using the VFD's front panel.
+ table.modbus-regs.fixed-regs
+ tr
+ th Address
+ th Value
+ th Meaning
+ th Description
+ tr
+ td.reg-addr F001
+ td.reg-value 2
+ td Communication port
+ td Control mode
+ tr
+ td.reg-addr F002
+ td.reg-value 2
+ td Communication port
+ td Frequency setting selection
+ tr
+ td.reg-addr F163
+ td.reg-value 1
+ td Slave address
+ td Must match #[tt bus-id] above
+ tr
+ td.reg-addr F164
+ td.reg-value 1
+ td 9600 BAUD
+ td Must match #[tt baud] above
+ tr
+ td.reg-addr F165
+ td.reg-value 3
+ td 8 data, no parity, 1 stop, RTU
+ td Must match #[tt parity] above
+ p
+ | Other settings according to the
+ |
+ a(href="https://buildbotics.com/upload/vfd/stepperonline-v70.pdf",
+ target="_blank") Stepper Online V70 VFD manual
import socket
import ctypes
from tornado import gen, web, iostream
-import bbctrl
try:
import v4l2
return b''.join(frame)
-def get_image_resource(path):
- path = bbctrl.get_resource(path)
-
- with open(path, 'rb') as f:
- return format_frame(f.read())
-
-
class VideoDevice(object):
def __init__(self, path = '/dev/video0'):
self.fd = os.open(path, os.O_RDWR | os.O_NONBLOCK | os.O_CLOEXEC)
class Camera(object):
def __init__(self, ioloop, args, log):
self.ioloop = ioloop
- self.log = log.get('Camera')
+ self.log = log
self.width = args.width
self.height = args.height
self.udevCtx = pyudev.Context()
self.udevMon = pyudev.Monitor.from_netlink(self.udevCtx)
self.udevMon.filter_by(subsystem = 'video4linux')
- ioloop.add_handler(self.udevMon, self._udev_handler, ioloop.READ)
self.udevMon.start()
+ ioloop.add_handler(self.udevMon, self._udev_handler, ioloop.READ)
def _udev_handler(self, fd, events):
action, device = self.udevMon.receive_device()
- if device is None or self.dev is not None: return
+ if device is None: return
path = str(device.device_node)
- if action == 'add':
+ if action == 'add' and self.dev is None:
self.have_camera = True
self.open(path)
- if action == 'remove' and path == self.path:
+ if action == 'remove' and self.dev is not None and path == self.path:
self.have_camera = False
self.close()
def open(self, path):
try:
+ self.log.info('Opening ' + path)
+
self._update_client_image()
self.path = path
if self.overtemp: return
self.dev = VideoDevice(path)
caps = self.dev.get_info()
- self.log.info('%s, %s, %s, %s', caps._driver, caps._card,
+ self.log.info(' Device: %s, %s, %s, %s', caps._driver, caps._card,
caps._bus_info, caps._caps)
if caps.capabilities & v4l2.V4L2_CAP_VIDEO_CAPTURE == 0:
formats = self.dev.get_formats()
sizes = self.dev.get_frame_sizes(fourcc)
- self.log.info('Formats: %s', formats)
- self.log.info('Sizes: %s', sizes)
- self.log.info('Audio: %s', self.dev.get_audio())
+ fmts = ', '.join(map(lambda s: s[1], formats))
+ sizes = ' '.join(map(lambda s: '%dx%d' % s, sizes))
+ audio = ' '.join(self.dev.get_audio())
+
+ self.log.info(' Formats: %s', fmts)
+ self.log.info(' Sizes: %s', sizes)
+ self.log.info(' Audio: %s', audio)
hasFormat = False
for name, description in formats:
self.ioloop.add_handler(self.dev, self._fd_handler,
self.ioloop.READ)
- self.log.info('Opened camera ' + path)
-
-
except Exception as e:
self.log.warning('While loading camera: %s' % e)
self._close_dev()
except: pass
self._close_dev()
- self.log.info('Closed camera')
+ self.log.info('Closed camera\n')
except: self.log.exception('Exception while closing camera')
finally: self.dev = None
def __init__(self, app, request, **kwargs):
super().__init__(app, request, **kwargs)
+ self.app = app
self.camera = app.camera
def write_img(self, name):
- self.write_frame_twice(get_image_resource('http/images/%s.jpg' % name))
+ path = self.app.get_image_resource(name)
+ if path is None: return
+
+ with open(path, 'rb') as f:
+ img = format_frame(f.read())
+ self.write_frame_twice(img)
def write_frame(self, frame):
def on_connection_close(self): self.camera.remove_client(self)
+
+
+def parse_args():
+ import argparse
+
+ parser = argparse.ArgumentParser(description = 'Web Camera Server')
+
+ parser.add_argument('-p', '--port', default = 80,
+ type = int, help = 'HTTP port')
+ parser.add_argument('-a', '--addr', metavar = 'IP', default = '127.0.0.1',
+ help = 'HTTP address to bind')
+ parser.add_argument('--width', default = 1280, type = int,
+ help = 'Camera width')
+ parser.add_argument('--height', default = 720, type = int,
+ help = 'Camera height')
+ parser.add_argument('--fps', default = 24, type = int,
+ help = 'Camera frames per second')
+ parser.add_argument('--camera_clients', default = 4,
+ help = 'Maximum simultaneous camera clients')
+
+ return parser.parse_args()
+
+
+if __name__ == '__main__':
+ import tornado
+ import logging
+
+
+ class Web(tornado.web.Application):
+ def __init__(self, args, ioloop, log):
+ tornado.web.Application.__init__(self, [(r'/.*', VideoHandler)])
+
+ self.args = args
+ self.ioloop = ioloop
+ self.camera = Camera(ioloop, args, log)
+
+ try:
+ self.listen(args.port, address = args.addr)
+
+ except Exception as e:
+ raise Exception('Failed to bind %s:%d: %s' % (
+ args.addr, args.port, e))
+
+ print('Listening on http://%s:%d/' % (args.addr, args.port))
+
+
+ def get_image_resource(self, name): return None
+
+
+ args = parse_args()
+ logging.basicConfig(level = logging.INFO, format = '%(message)s')
+ log = logging.getLogger('Camera')
+ ioloop = tornado.ioloop.IOLoop.current()
+ app = Web(args, ioloop, log)
+
+ try:
+ ioloop.start()
+ except KeyboardInterrupt: pass
import math
import re
import time
+import datetime
from collections import deque
import camotics.gplan as gplan # pylint: disable=no-name-in-module,import-error
import bbctrl.Cmd as Cmd
if len(name) and name[0] == '_':
value = self.ctrl.state.get(name[1:], 0)
- if units == 'IMPERIAL': value /= 25.4 # Assume metric
+ try:
+ float(value)
+ if units == 'IMPERIAL': value /= 25.4 # Assume metric
+ except ValueError: value = 0
self.log.info('Get: %s=%s (units=%s)' % (name, value, units))
else: self.log.error('Could not parse planner log line: ' + line)
+ def _log_time(self, prefix):
+ self.log.info(prefix + datetime.datetime.now().isoformat())
+
+
def _add_message(self, text):
self.ctrl.state.add_message(text)
sw = self.ctrl.state.get_switch_id(block['switch'])
return Cmd.seek(sw, block['active'], block['error'])
- if type == 'end': return '' # Sends id
+ if type == 'end':
+ self.cmdq.enqueue(id, self._log_time, 'Program End: ')
+ return '' # Sends id
raise Exception('Unknown planner command "%s"' % type)
self.where = path
path = self.ctrl.get_path('upload', path)
self.log.info('GCode:' + path)
+ self._log_time('Program Start: ')
self._sync_position()
self.planner.load(path, self.get_config(False, True))
self.reset_times()
try:
self.planner.stop()
self.cmdq.clear()
+ self._log_time('Program Stop: ')
except:
self.log.exception()
import copy
import uuid
import os
+import time
import bbctrl
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_callback('timestamp', lambda name: time.time())
self.reset()
self.load_files()
if not args.disable_camera:
if self.args.demo: log = bbctrl.log.Log(args, ioloop, 'camera.log')
else: log = self.get_ctrl().log
- self.camera = bbctrl.Camera(ioloop, args, log)
+ self.camera = bbctrl.Camera(ioloop, args, log.get('Camera'))
else: self.camera = None
# Init controller
print('Listening on http://%s:%d/' % (args.addr, args.port))
+ def get_image_resource(self, name):
+ return bbctrl.get_resource('http/images/%s.jpg' % name)
+
+
def opened(self, ctrl): ctrl.clear_timeout()
"scale": 25.4,
"default": 5,
"code": "zb"
+ },
+ "stall-volts": {
+ "type": "float",
+ "min": 0,
+ "unit": "v",
+ "default": 12,
+ "code": "tv"
}
}
}
"values": ["Disabled", "PWM Spindle", "Huanyang VFD", "Custom Modbus VFD",
"AC-Tech VFD", "Nowforever VFD", "Delta VFD015M21A (Beta)",
"YL600, YL620, YL620-A VFD (Beta)", "FR-D700 (Beta)",
- "Sunfar E300 (Beta)", "OMRON MX2"],
+ "Sunfar E300 (Beta)", "OMRON MX2", "V70"],
"default": "Disabled",
"code": "st"
},