From: Joseph Coffland Date: Fri, 24 Apr 2020 22:02:13 +0000 (-0700) Subject: Support V70 Stepper Online VFD, Set GCode variables #5400 and #<_tool>, Added #<_time... X-Git-Url: https://git.buildbotics.com/?a=commitdiff_plain;h=7bca546253bf0d9c63c3ba8f047ba7abf98253a5;p=bbctrl-firmware Support V70 Stepper Online VFD, Set GCode variables #5400 and #<_tool>, Added #<_timestamp> GCode variable, Print program start, stop and end with timestamp, Updates to camera code, Work on stall detect homing --- diff --git a/CHANGELOG.md b/CHANGELOG.md index 3355047..e1fcc84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,16 @@ 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. diff --git a/package.json b/package.json index 6480bcc..a0fa55c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "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+", diff --git a/src/avr/src/config.h b/src/avr/src/config.h index 39d8349..2a379e0 100644 --- a/src/avr/src/config.h +++ b/src/avr/src/config.h @@ -151,8 +151,6 @@ enum { #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 | \ diff --git a/src/avr/src/drv8711.c b/src/avr/src/drv8711.c index dbc4de3..bd89138 100644 --- a/src/avr/src/drv8711.c +++ b/src/avr/src/drv8711.c @@ -81,7 +81,8 @@ typedef struct { 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; @@ -162,7 +163,12 @@ static uint16_t _driver_spi_command(drv8711_driver_t *drv) { 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: @@ -433,3 +439,46 @@ void set_driver_flags(int driver, uint16_t flags) { 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; +} diff --git a/src/avr/src/spindle.h b/src/avr/src/spindle.h index 61f17bb..4728631 100644 --- a/src/avr/src/spindle.h +++ b/src/avr/src/spindle.h @@ -57,6 +57,7 @@ typedef enum { SPINDLE_TYPE_FR_D700, SPINDLE_TYPE_SUNFAR_E300, SPINDLE_TYPE_OMRON_MX2, + SPINDLE_TYPE_V70, } spindle_type_t; diff --git a/src/avr/src/vars.def b/src/avr/src/vars.def index b9fdb57..3c8a6ee 100644 --- a/src/avr/src/vars.def +++ b/src/avr/src/vars.def @@ -55,6 +55,7 @@ VAR(homed, h, b8, MOTORS, 1, 1) // Motor homed status 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 diff --git a/src/avr/src/vfd_spindle.c b/src/avr/src/vfd_spindle.c index 21d46f6..fb247fe 100644 --- a/src/avr/src/vfd_spindle.c +++ b/src/avr/src/vfd_spindle.c @@ -168,6 +168,18 @@ const vfd_reg_t omron_mx2_regs[] PROGMEM = { }; +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]; @@ -386,6 +398,7 @@ void vfd_spindle_init() { 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; } diff --git a/src/pug/templates/tool-view.pug b/src/pug/templates/tool-view.pug index 4f331a4..25dcc7f 100644 --- a/src/pug/templates/tool-view.pug +++ b/src/pug/templates/tool-view.pug @@ -402,3 +402,43 @@ script#tool-view-template(type="text/x-template") | | 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 diff --git a/src/py/bbctrl/Camera.py b/src/py/bbctrl/Camera.py index 17f1bb7..bc06d3b 100755 --- a/src/py/bbctrl/Camera.py +++ b/src/py/bbctrl/Camera.py @@ -36,7 +36,6 @@ import base64 import socket import ctypes from tornado import gen, web, iostream -import bbctrl try: import v4l2 @@ -71,13 +70,6 @@ def format_frame(frame): 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) @@ -280,7 +272,7 @@ class VideoDevice(object): 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 @@ -306,21 +298,21 @@ class Camera(object): 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() @@ -364,13 +356,15 @@ class Camera(object): 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: @@ -380,9 +374,13 @@ class Camera(object): 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: @@ -399,9 +397,6 @@ class Camera(object): 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() @@ -427,7 +422,7 @@ class Camera(object): 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 @@ -465,6 +460,7 @@ class VideoHandler(web.RequestHandler): def __init__(self, app, request, **kwargs): super().__init__(app, request, **kwargs) + self.app = app self.camera = app.camera @@ -485,7 +481,12 @@ class VideoHandler(web.RequestHandler): 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): @@ -508,3 +509,61 @@ class VideoHandler(web.RequestHandler): 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 diff --git a/src/py/bbctrl/Planner.py b/src/py/bbctrl/Planner.py index 7cdc03a..b77fc78 100644 --- a/src/py/bbctrl/Planner.py +++ b/src/py/bbctrl/Planner.py @@ -29,6 +29,7 @@ import json 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 @@ -138,7 +139,10 @@ class Planner(): 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)) @@ -168,6 +172,10 @@ class Planner(): 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) @@ -285,7 +293,9 @@ class Planner(): 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) @@ -336,6 +346,7 @@ class Planner(): 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() @@ -345,6 +356,7 @@ class Planner(): try: self.planner.stop() self.cmdq.clear() + self._log_time('Program Stop: ') except: self.log.exception() diff --git a/src/py/bbctrl/State.py b/src/py/bbctrl/State.py index 614660d..82a1abb 100644 --- a/src/py/bbctrl/State.py +++ b/src/py/bbctrl/State.py @@ -29,6 +29,7 @@ import traceback import copy import uuid import os +import time import bbctrl @@ -75,6 +76,7 @@ class State(object): self.set_callback('metric', lambda name: 1 if self.is_metric() else 0) self.set_callback('imperial', lambda name: 0 if self.is_metric() else 1) + self.set_callback('timestamp', lambda name: time.time()) self.reset() self.load_files() diff --git a/src/py/bbctrl/Web.py b/src/py/bbctrl/Web.py index 5b1a46c..5615d75 100644 --- a/src/py/bbctrl/Web.py +++ b/src/py/bbctrl/Web.py @@ -502,7 +502,7 @@ class Web(tornado.web.Application): 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 @@ -564,6 +564,10 @@ class Web(tornado.web.Application): 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() diff --git a/src/resources/config-template.json b/src/resources/config-template.json index a4f74fa..19067c8 100644 --- a/src/resources/config-template.json +++ b/src/resources/config-template.json @@ -205,6 +205,13 @@ "scale": 25.4, "default": 5, "code": "zb" + }, + "stall-volts": { + "type": "float", + "min": 0, + "unit": "v", + "default": 12, + "code": "tv" } } } @@ -216,7 +223,7 @@ "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" },