Support V70 Stepper Online VFD, Set GCode variables #5400 and #<_tool>, Added #<_time...
authorJoseph Coffland <joseph@cauldrondevelopment.com>
Fri, 24 Apr 2020 22:02:13 +0000 (15:02 -0700)
committerJoseph Coffland <joseph@cauldrondevelopment.com>
Fri, 24 Apr 2020 22:02:13 +0000 (15:02 -0700)
13 files changed:
CHANGELOG.md
package.json
src/avr/src/config.h
src/avr/src/drv8711.c
src/avr/src/spindle.h
src/avr/src/vars.def
src/avr/src/vfd_spindle.c
src/pug/templates/tool-view.pug
src/py/bbctrl/Camera.py
src/py/bbctrl/Planner.py
src/py/bbctrl/State.py
src/py/bbctrl/Web.py
src/resources/config-template.json

index 3355047a2327d69a0d78bc20dec46361947d3d68..e1fcc84b8c3b8fcff6ed32ef4eabc9f7728746d5 100644 (file)
@@ -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.
index 6480bcc7d90ed5cdf8b333baa3e2a36d05e32e2d..a0fa55c0410cea5bba06289dbd33a9343fe1eb61 100644 (file)
@@ -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+",
index 39d83492c1040975f942dc7c8ba56c1573a7553b..2a379e08a582fa418f6cb76c9775dba2158066ba 100644 (file)
@@ -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 | \
index dbc4de3514fb72101af1950e3e4b38c92d17ea58..bd891383674e21b52947fbd7776a95990b9fef55 100644 (file)
@@ -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;
+}
index 61f17bb08e3395ec2e71d0e96f0218284df183e2..47286312b9bed29dbc19806a92ba6cc14f54cdef 100644 (file)
@@ -57,6 +57,7 @@ typedef enum {
   SPINDLE_TYPE_FR_D700,
   SPINDLE_TYPE_SUNFAR_E300,
   SPINDLE_TYPE_OMRON_MX2,
+  SPINDLE_TYPE_V70,
 } spindle_type_t;
 
 
index b9fdb570b2a5f1f08a63d0d89387ac829a403233..3c8a6ee55a586edc092c02c6334fa051831e7e4d 100644 (file)
@@ -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
 
index 21d46f6174648fc0f07b511b3996483839d25094..fb247fe8df4fb5823be8e73afe7a02dd943b4832 100644 (file)
@@ -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;
   }
 
index 4f331a4b53abb85f103a1299513731cb47b2fa75..25dcc7f98400db0180362f42cfa6811808b0f5f3 100644 (file)
@@ -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
index 17f1bb7dfea59b0bf9c4475a0a637b6e69616141..bc06d3ba56b9d5c84c135313be6125451135674a 100755 (executable)
@@ -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
index 7cdc03a399c10746823d6d7aa8a2a540896aaadd..b77fc78d1a12344bb410d8731076679beb0b74d8 100644 (file)
@@ -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()
index 614660dd656414d0af0da8dcd0cdadf44dbb6a31..82a1abba2942941a5606be86b220df05c10db31d 100644 (file)
@@ -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()
index 5b1a46c79e24b30523ab8773d09d8e46fad3c1b4..5615d75af32553ddc2e2283e8d37aac1572ae5b1 100644 (file)
@@ -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()
 
 
index a4f74faf47031fe415f4b5cf39380a5825cc28ef..19067c8c1c85cd48539cd9f64987c5bba39e9875 100644 (file)
           "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"
     },