Source code for printerc

#!/usr/bin/env python
# coding=utf-8
# Author: Pedro I. López
# Contact: dreilopz@gmail.com

'''Numerical controller for the printer73x system.

**printerc** is an interactive command line interface for the numerical control
of the **printer73x** system.  printerc drives printerm through mcircuit;
printerc stablishes a serial connection with mcircuit, and mcircuit is coupled
to printerm through the motors.  mcircuit (actually MM12, its subsystem) loads
in its memory the routines that correspond to the translations across the
:math:`X`, :math:`Y` and :math:`Z` axis, and then printerc execute these
routines in order to produce the trajectory of the tool printing the image.
'''

# Standard library imports.
from __future__ import division
from pprint import pprint
import sys
import os
import atexit
import gc
import time

# Related third party imports.
import serial
import IPython
import numpy as np # remove
import matplotlib.image as mpimg
import matplotlib.pyplot as plt
import matplotlib.cm as cm

# Same as **printer73x**.
__version__ = '0.09'

# GLOBAL CONSTANT names.  *if main* section at bottom sets global names too.
# ==========================================================================
# ==========================================================================
LOGF = 'log.rst'
'''Path to the log file.'''

INTRO_MSG = '''\

**printerc**

Welcome!

'''
'''Welcome message for command line interface.'''

# MM12
# ==========================================================================
TRANSITIONS_PER_STEP = 2
'''The board sets the microstepping format with the jumpers *MS1* and *MS2*.
Use the following table to set this constant:

============ ============ ===============
MS1          MS2          TRANSITIONS_PER_STEP
============ ============ ===============
connected    connected    1
disconnected connected    2
connected    disconnected 4
disconnected disconnected 8
============ ============ ===============

.. note:: Both stepper motor driver boards must have the same jumper
          configuration.
'''

STEPS_PER_PIXEL = 90
'''Number of steps the stepper motor needs to translate 1 pixel across the
:math:`X` or :math:`Y` axes.'''

TRANSITIONS_PER_PIXEL = STEPS_PER_PIXEL * TRANSITIONS_PER_STEP
'''Number of low-to-high transitions the stepper motors need to translate 1
pixel across the :math:`X` or :math:`Y` axes.'''

SRV_SIGNAL_CHANNEL_TARGET_OFF = 940
''':math:`Z` axis servo motor pulse width in units of quarter-:math:`\\mu s`
that enables printing (moves the tool down).'''

SRV_SIGNAL_CHANNEL_TARGET_ON = 2175
SRV_SIGNAL_CHANNEL_TARGET_ON = 1580
''':math:`Z` axis servo motor pulse width in units of quarter-:math:`\\mu s`
that disables printing (moves the tool up).'''

STEPPER_CHANNELS_TARGET_ON = 6800
'''Target value in units of quarter-:math:`\\mu s` that drives the stepper
channels high.'''

STEPPER_CHANNELS_TARGET_OFF = 5600
'''Target value in units of quarter-:math:`\\mu s` that drives the stepper
channels low.'''

MM12_AXES_CHANNELS = {
    'X' : {
        'dir_channel' : 0,
        'dir_positive' : STEPPER_CHANNELS_TARGET_OFF,
        'dir_negative' : STEPPER_CHANNELS_TARGET_ON,
        'step_channel': 1,
    },
    'Y' : {
        'dir_channel' : 2,
        'dir_positive' : STEPPER_CHANNELS_TARGET_ON,
        'dir_negative' : STEPPER_CHANNELS_TARGET_OFF,
        'step_channel': 3,
    },
    'Z' : {
        'channel' : 4,
        'on'      : SRV_SIGNAL_CHANNEL_TARGET_OFF,
        'off'     : SRV_SIGNAL_CHANNEL_TARGET_ON,
    },
}
'''Configuration of the MM12 channels for the servo and stepper motors outputs.'''

SUB_STEPPER_PIXEL_TEMPLATE = '''sub {name}
  {{{{ntransitions}}}}
  {dir} {dir_channel} servo          # set direction
  begin
    dup
    while
    {off} {step_channel} servo
    {{delay}} delay
    {on} {step_channel} servo
    {{delay}} delay
    1 minus
  repeat
  quit
'''
'''Template for the MM12 script subroutines that drive a stepper motor in units
of pixels,'''

SUB_STEPPER_PULSE_TEMPLATE = '''sub {name}
  {dir} {dir_channel} servo          # set direction
  {off} {step_channel} servo
  {{delay}} delay
  {on} {step_channel} servo
  {{delay}} delay
  quit
'''
'''Template for the MM12 script subroutines that drive a stepper motor in units
of low-to-high transitions, for a precise but slow translation.'''

SUB_SERVO_TEMPLATE = '''sub {name}
  {position} {channel} servo
  begin
    get_moving_state
  while
    # wait until is is no longer moving.
  repeat
  75 delay
  quit
'''
'''Template for the MM12 script subroutine that drives the servo motor.'''

MM12_SCRIPT_INIT = '''\
{{servo_acceleration}} {servo_channel} acceleration
{{servo_speed}} {servo_channel} speed
'''.format(servo_channel=MM12_AXES_CHANNELS['Z']['channel'])
'''MM12 script initialization.'''

MM12_SUBROUTINES = {
    'X-p' : {
        'subroutine_id'       : 0,
        'subroutine_body' :
            SUB_STEPPER_PULSE_TEMPLATE.format(
                name='x_neg_pulse', dir=MM12_AXES_CHANNELS['X']['dir_negative'],
                dir_channel=MM12_AXES_CHANNELS['X']['dir_channel'],
                off=STEPPER_CHANNELS_TARGET_OFF,
                step_channel=MM12_AXES_CHANNELS['X']['step_channel'],
                on=STEPPER_CHANNELS_TARGET_ON),
    },
    'X+p' : {
        'subroutine_id'       : 1,
        'subroutine_body' :
            SUB_STEPPER_PULSE_TEMPLATE.format(
                name='x_pos_pulse', dir=MM12_AXES_CHANNELS['X']['dir_positive'],
                dir_channel=MM12_AXES_CHANNELS['X']['dir_channel'],
                off=STEPPER_CHANNELS_TARGET_OFF,
                step_channel=MM12_AXES_CHANNELS['X']['step_channel'],
                on=STEPPER_CHANNELS_TARGET_ON),
    },
    'X-P' : {
        'subroutine_id'       : 2,
        'subroutine_body' :
            SUB_STEPPER_PIXEL_TEMPLATE.format(
                name='x_neg_pixel', dir=MM12_AXES_CHANNELS['X']['dir_negative'],
                dir_channel=MM12_AXES_CHANNELS['X']['dir_channel'],
                off=STEPPER_CHANNELS_TARGET_OFF,
                step_channel=MM12_AXES_CHANNELS['X']['step_channel'],
                on=STEPPER_CHANNELS_TARGET_ON ),
    },
    'X+P' : {
        'subroutine_id'       : 3,
        'subroutine_body' :
            SUB_STEPPER_PIXEL_TEMPLATE.format(
                name='x_pos_pixel', dir=MM12_AXES_CHANNELS['X']['dir_positive'],
                dir_channel=MM12_AXES_CHANNELS['X']['dir_channel'],
                off=STEPPER_CHANNELS_TARGET_OFF,
                step_channel=MM12_AXES_CHANNELS['X']['step_channel'],
                on=STEPPER_CHANNELS_TARGET_ON ),
    },
    'Y-p' : {
        'subroutine_id'       : 4,
        'subroutine_body' :
            SUB_STEPPER_PULSE_TEMPLATE.format(
                name='y_neg_pulse', dir=MM12_AXES_CHANNELS['Y']['dir_negative'],
                dir_channel=MM12_AXES_CHANNELS['Y']['dir_channel'],
                off=STEPPER_CHANNELS_TARGET_OFF,
                step_channel=MM12_AXES_CHANNELS['Y']['step_channel'],
                on=STEPPER_CHANNELS_TARGET_ON),
    },
    'Y+p' : {
        'subroutine_id'       : 5,
        'subroutine_body' :
            SUB_STEPPER_PULSE_TEMPLATE.format(
                name='y_pos_pulse', dir=MM12_AXES_CHANNELS['Y']['dir_positive'],
                dir_channel=MM12_AXES_CHANNELS['Y']['dir_channel'],
                off=STEPPER_CHANNELS_TARGET_OFF,
                step_channel=MM12_AXES_CHANNELS['Y']['step_channel'],
                on=STEPPER_CHANNELS_TARGET_ON),
    },
    'Y-P' : {
        'subroutine_id'       : 6,
        'subroutine_body' :
            SUB_STEPPER_PIXEL_TEMPLATE.format(
                name='y_neg_pixel', dir=MM12_AXES_CHANNELS['Y']['dir_negative'],
                dir_channel=MM12_AXES_CHANNELS['Y']['dir_channel'],
                off=STEPPER_CHANNELS_TARGET_OFF,
                step_channel=MM12_AXES_CHANNELS['Y']['step_channel'],
                on=STEPPER_CHANNELS_TARGET_ON ),
    },
    'Y+P' : {
        'subroutine_id'       : 7,
        'subroutine_body' :
            SUB_STEPPER_PIXEL_TEMPLATE.format(
                name='y_pos_pixel', dir=MM12_AXES_CHANNELS['Y']['dir_positive'],
                dir_channel=MM12_AXES_CHANNELS['Y']['dir_channel'],
                off=STEPPER_CHANNELS_TARGET_OFF,
                step_channel=MM12_AXES_CHANNELS['Y']['step_channel'],
                on=STEPPER_CHANNELS_TARGET_ON ),
    },
    'Z-' : {
        'subroutine_id'       : 8,
        'subroutine_body' :
            SUB_SERVO_TEMPLATE.format(
                name='z_position_off',
                channel=MM12_AXES_CHANNELS['Z']['channel'],
                position=MM12_AXES_CHANNELS['Z']['off']*4)
    },
    'Z+' : {
        'subroutine_id'       : 9,
        'subroutine_body' :
            SUB_SERVO_TEMPLATE.format(
                name='z_position_on',
                channel=MM12_AXES_CHANNELS['Z']['channel'],
                position=MM12_AXES_CHANNELS['Z']['on']*4)
    },
}
'''Structure that builds and identifies the MM12 script subroutines.'''

MM12_SCRIPT_RUNNING = '\x00'
'''Byte value that the MM12 returns when the script is running.'''

MM12_SCRIPT_STOPPED = '\x01'
'''Byte value that the MM12 returns when the script is stopped.'''
# ==========================================================================

# ==========================================================================
# ==========================================================================

[docs]class Getch: """Gets a single character from standard input. Does not echo to the screen. References ========== .. [GETCHRECIPE] http://code.activestate.com/recipes/134892/ """ def __init__(self): try: self.impl = _GetchWindows() except ImportError: self.impl = _GetchUnix() def __call__(self): return self.impl()
class _GetchUnix: '''Unix implementation of class ``Getch``.''' def __init__(self): import tty, sys def __call__(self): import sys, tty, termios fd = sys.stdin.fileno() old_settings = termios.tcgetattr(fd) try: tty.setraw(sys.stdin.fileno()) ch = sys.stdin.read(1) finally: termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) return ch class _GetchWindows: '''Windows implementation of class ``Getch``.''' def __init__(self): import msvcrt def __call__(self): import msvcrt return msvcrt.getch()
[docs]def on_exit(): '''Actions to do on exit.''' try: print >>logf, 'Closing ``{0}`` *command port*'.format(sp.port) sp.close() except NameError: pass print >>logf, 'END' logf.close() print '\nThanks for using ``printerc``!\n' # Invoke the garbage collector. gc.collect()
[docs]def scan_serial_ports(): '''Scan system for available physical or virtual serial ports. Returns ------- available : list of tuples Each element of the list is a ``(num, name)`` tuple with the number and name of the port. Notes ----- Directly copied from example from `pyserial <http://sourceforge.net/projects/pyserial/files/package>`_ project. ''' available = [] for i in range(256): try: s = serial.Serial(i) available.append( (i, s.portstr)) s.close() except serial.SerialException: pass return available
[docs]def mm12_script_status(): '''Indicate whether the MM12 script is running or stopped. Returns ------- script_status : {``MM12_SCRIPT_RUNNING``, ``MM12_SCRIPT_STOPPED``} ''' assert sp.write('\xae') == 1 return sp.read(1)
[docs]def translate(adm, confirm=False): '''Translate the printerm tool across the :math:`XYZ` space. printer73x can only perform translations across a single axis at a time. Parameters ---------- adm: str *adm* stands for Axis, Direction, Mode. Use the following table to select the kind of translation you want to perform (where :math:`n` is the number of pulses for the printerm tool to translate a pixel unit across the respective axis). ======= ================================================================ *adm* translation ======= ================================================================ ``X-p`` send 1 single pulse for negative translation across :math:`X`. ``X+p`` send 1 single pulse for positive translation across :math:`X`. ``X-P`` send :math:`n` pulses for negative translation across :math:`X`. ``X+P`` send :math:`n` pulses for positive translation across :math:`X`. ``Y-p`` send 1 single pulse for negative translation across :math:`Y`. ``Y+p`` send 1 single pulse for positive translation across :math:`Y`. ``Y-P`` send :math:`n` pulses for negative translation across :math:`Y`. ``Y+P`` send :math:`n` pulses for positive translation across :math:`Y`. ``Z-`` move the tool to the off position (:math:`Z`). ``Z+`` move the tool to the on position (:math:`Z`). ======= ================================================================ confirm: boolean, optional If ``True``, the user must confirm the translation by pressing Enter (default is ``False``). ''' # Start until script is not running. while mm12_script_status() == MM12_SCRIPT_RUNNING: pass subroutine_id = chr( MM12_SUBROUTINES[adm]['subroutine_id']) str2write = ''.join(['\xa7', subroutine_id]) if confirm: raw_input() assert sp.write(str2write) == 2 #if 'Z' in adm: #time.sleep(0.1)
[docs]def build_mm12_script(fpath, ntransitions=TRANSITIONS_PER_PIXEL, delay=1, servo_acceleration=0, servo_speed=100): '''Build a script to be loaded on the MM12. Parameters ---------- fpath : str-like Path location where to save the script file. ntransitions : int, optional Number of low-to-high transitions to perform in the subroutines that performs translation in units of pixels through the stepper motor (default is ``TRANSITIONS_PER_PIXEL``). delay : int, optional Delay (in milliseconds) between each transition in the subroutines that perform translation through the stepper motors (default is 1). servo_acceleration : int, optional Sets the acceleration of the servo signal channel in units of (0.25 us)/(10 ms)/(80 ms) (default is 0). servo_speed : int, optional Sets the speed of the servo signal channel in units of (0.25 us)/(10 ms) (default is 100). ''' def get_subroutine_key_by_id(subroutine_id): for key, value in MM12_SUBROUTINES.items(): if subroutine_id is value['subroutine_id']: return key for intarg in (ntransitions, delay, servo_acceleration, servo_speed): assert isinstance(intarg, int) with open(fpath, 'w') as f: print >>f, MM12_SCRIPT_INIT.format(servo_acceleration=servo_acceleration, servo_speed=servo_speed) for i in range(len( MM12_SUBROUTINES)): subroutine_key = get_subroutine_key_by_id(i) subroutine_body = MM12_SUBROUTINES[subroutine_key]['subroutine_body'] if 'Z' not in subroutine_key: subroutine_body = subroutine_body.format(delay=delay) if 'P' in subroutine_key: subroutine_body = subroutine_body.format(ntransitions=ntransitions) print >>f, subroutine_body
[docs]def prepare_img(imgpath, invert=False, show=False): '''Perform any necessary processing for the input image to be reproduced by printerm. Parameters ---------- imgpath : str-like Path to the image file. Must be PNG, 8-bit grayscale, non-interlaced. invert : boolean, optional Invert the image if ``True`` (default is ``False``). show : boolean, optional Show the image if ``True`` (default is ``False``). Notes ----- This function sets the following global names: **img** : array of booleans 2-d array representation of the image. **b** : int Image's height, number of rows in the array representation. **w** : int Image's width, number of columns in the array representation. ''' global img, b, w print 'Loading ``{0}``...'.format(imgpath) img = mpimg.imread(fname=imgpath, format='png') b, w = img.shape npixels = b * w nprints = nnotprints = 0 assert (b > 0) and (w > 0) print 'Processing the image...' # only total black and white, no grays. for i in range(b): for j in range(w): if img[i][j] < 0.9: img[i][j] = 0.0 else: img[i][j] = 1.0 if invert: print 'Inverting image...' for i in range(b): for j in range(w): if img[i][j] > 0.0: img[i][j] = 0.0 else: img[i][j] = 1.0 # Check for pixel with and without color. for i in range(b): for j in range(w): if img[i][j] > 0.0: nnotprints += 1 else: nprints += 1 assert (nnotprints + nprints) == npixels # If ``nprints == 0`` then no pixel will be printed. assert nprints > 0 print 'Loaded ``{0}`` with {1} pixels, {2} of which have color'.format( imgpath, npixels, nprints) plt.close('all') if show: plt.imshow(img, cmap=cm.gray) plt.show()
[docs]def connect_printerm(commandport_id): '''Connect printerc with printerm through the MM12 command port. Parameters ---------- commandport_id : str or int Serial device name or port number number of the MM12 serial command port. ''' global sp sp = serial.Serial(port=commandport_id) assert sp.isOpen() print >>logf, '``{0}`` just opened *command port* ``{1}``'.format(PN, sp.port) msg = '``{0}`` is now connected to ``printerm`` through ``{1}``'.format( PN, sp.port) for f in (logf, sys.stdout): print >>f, msg
[docs]def manual_translation_mode(precise=True): '''Manually translate the printerm tool across the :math:`XY` plane. Parameters ---------- precise : boolean, optional If ``True``, perform translation in units of single low-to-high transitions sent to the stepper motor drivers (how much the tool is translated depends on the microstep format selected through the XMS1, XMS2, YMS1, YMS2 jumpers in mcircuit). If ``False`` perform translation in units of pixels (default is True). ''' if precise: keys2translation = { 'h' : 'X-p', 'l' : 'X+p', 'j' : 'Y+p', 'k' : 'Y-p', 'i' : 'Z+', 'o' : 'Z-', } else: keys2translation = { 'h' : 'X-P', 'l' : 'X+P', 'j' : 'Y+P', 'k' : 'Y-P', 'i' : 'Z+', 'o' : 'Z-', } getch = Getch() while True: ch = getch() if ch not in keys2translation.keys(): break while mm12_script_status() == MM12_SCRIPT_RUNNING: pass translate(keys2translation[ch], confirm=False)
if __name__ == "__main__": # program name from file name. PN = os.path.splitext(sys.argv[0])[0] logf = open(LOGF, 'w') print >>logf, 'START' atexit.register(on_exit) IPython.Shell.IPShellEmbed()( INTRO_MSG)