PlotBot


Varias veces vi1 este tipo de plotters verticales donde el cursor es controlado por un par de correas sostenidas por motores y siempre quise uno. Así que lo armé.

Parte 1: ¿Cómo y por qué?

Todo empezó con una suposición: es suficiente con determinar la longitud de los segmentos. Un grafico más tarde tenía esto:

Inicialmente lo pensé como la intersección de dos círculos; recién después de un par de implementaciones me di cuenta que se podía simplificar a «la distancia del punto a los centros» (el modelo con dos círculos es más fácil de visualizar porque no hace falta calcular la intersección).

El modelo es menos que perfecto; si uno se pone a leer sobre el tema (cosa que no hice inicialmente) empiezan a aparecer problemas con la tensión de las cuerdas que hacen que se pierda precisión, y la inercia que causa rayas no deseadas… pero para dibujar monas chinas en un pizarrón no son un tema de vida o muerte.

Cinco minutos de matemática más tarde, arranqué Processing2 para iniciar un prototipo que me permita visualizar la idea: dos círculos en esquinas opuestas, y calculamos el radio de cada uno como la distancia de su centro al punto del mouse, resultando en una intersección:

PVector c1p = new PVector(0, 0);
PVector c2p = new PVector(800, 0);

void setup() {
  size(800, 600);
}

void draw() {
  PVector mousePos = new PVector(mouseX, mouseY);
  float c1r = mousePos.dist(c1p);
  float c2r = mousePos.dist(c2p);

  background(224);
  noFill();
  stroke(128);
  circle(c1p.x, c1p.y, 2 * c1r);
  circle(c2p.x, c2p.y, 2 * c2r);
  stroke(0);
  line(c1p.x, c1p.y, mouseX, mouseY);
  line(c2p.x, c2p.y, mouseX, mouseY);
}

Resulta que este sistema de coordenadas se llama «Bicéntrico»; una vez que encontré esto me simplificó un montón de ecuaciones. Lo único que hago de especial es correr y hacia abajo para que (0, 0) no quede entre los dos polos: en ese punto las cuerdas tendrían máxima tensión y un movimiento equivocado podría romper todo. Con esto tengo un poquito más de seguridad.

Armado con confianza poco merecida, avancé.

Parte 2: El espantapájaros y su cerebro.

Para esta receta necesitás:

  • Un Arduino Uno
  • Un shield Arduino CNC (o al menos un par de drivers de motor)
  • Dos motores paso a paso para controlar la posición (yo usé unos parecidos a estos)
  • Una forma de montar los motores (yo usé un par de piezas impresas en 3D)
  • Una cuerda, correa o similar, con un a forma de sujetarla a los motores (yo usé la correa de una persiana rodante, y otra pieza impresa)
  • Un marcador, lapicera, lápiz, o similar.
  • Una forma de montar el marcador a la cuerda (yo hice… cosas raras con un par de piezas descartadas de un intento anterior)

Opcional: - Un Servo, para controlar el marcador.

El código del Arduino es una traducción fiel (excepto por la clase PVector) del de Processing. El prototipo en Processing estaba escrito pensando en las limitaciones del Arduino: el motor no conoce su posición absoluta, y la cantidad de pasos en su movimiento depende de los engranajes del motor como del diámetro del eje3. Traté de mantener ambas versiones lo más similares que se podía: si ambas se comportan igual, es más fácil debuggear la que me deja ver exactamente dónde están las cosas. Muchos de estos factores son configurables, así si querés hacerlo más grande, más chico, o con menos precisión, se puede configurar.

La diferencia principal entre la versión de Arduino y la de Processing es que en la de Arduino en cada loop() se parsea la línea, se calcula el segmento, y se mueve el motor; mientras que la de Processing, como es más visual, divide la ejecución en varios pasos, para poder animarla.

Parte 2a: Do you speak-a my language?

Todo muy lindo con que el cursor se mueva, pero ¿cómo le digo a dónde? Afortunadeamente, como muchos de mis problemas, ya lo resolvieron 30 años de que nazca: G-code es un lenguaje usado en CNC para comunicarse con máquinas herramienta.

Cada línea tiene una serie de instrucciones que le dicen al puntero hacia dónde y cómo tiene que moverse. Hay un montón de instrucciones, pero la mayoría no me interesan. Acá sólo uso:

  • G00: Movimiento rápido; no importa el camino. En mi implementación resulta en una curva.
  • G01: Interpolación lineal. Mueve el cursor en pasos de longitud fija.
  • G02 y G03: Movimiento circular. Mueve el cursor circularmente en pasos de longitud fija.
  • X e Y: Posición (absoluta) del destino.
  • I y J: Posición (relativa) del centro del círculo.
  • R: Radio del círculo.
  • Z: No lo implementé aún, pero controla la profundidad del corte o, en mi caso, si el marcador escribe o no.

El modo de fallo es bastante generoso: mientras sea algo parseable, lo aceptamos; si es una instrucción que no conocemos, la ignoramos.

El paso siguiente fue implementar el parser de G-Code. Como sabía que al Arduino le iba a hablar mediante puerto serie, lo implementé para que consuma un caracter a la vez (aunque Arduino también me da un método readFloat() que me simplifica bocha):

  • Si es un %, lo ignoramos; indica el inicio del programa, pero es opcional.
  • Si es un (, descartamos hasta encontrar el ); son comentarios.
  • Si es un espacio, lo salteamos.
  • Si es la letra ‘G’, sólo me interesa si es 0, 1, 2, o 3; los demás modos no me importan (pero, si me importaran, por ejemplo, para cambiar a pulgadas, los implementaría acá).
  • Finalmente, cualquier otro caracter (que damos por asumido es una letra), simplemente almacenamos el valor.

Algunos valores son especiales y permanecen seteados (como el tipo de movimiento que se ejecuta); otros sólo nos importan para el comando actual (como el radio del círculo). Para hacerla fácil, el parser siempre mantiene los valores anteriores.

Parte 2b: One step at a time

Procesamos un poquito los valores obtenidos, y podemos hacer interpolación en pasos de longitud fija, cosa que se repite hasta que estemos suficientemente cerca del punto final.

Finalmente, para cada segmento de la interpolación hay que mover los motores, que lo hacemos con un algoritmo de dibujo de líneas bastante simple: Calculamos cuantos pasos tiene que dar cada motor con respecto al otro, y vamos moviendo un paso, revisando si estamos muy lejos de la inclinación requerida, y moviendo el otro cuando haga falta.

Aprovechamos también para calcular la distancia (en realidad, el cuadrado de la distancia, para ahorrar una raíz cuadrada en un bucle tan cerrado), y hacer una leve pausa (ajustada por la distancia) para fijar la velocidad del movimiento.

Acá sería un excelente lugar para poner un failsafe y parar la máquina si sale fuera del rango de impresión definido.

Enough talk

Funciona sorprendentemente bien para lo simple que es:

El path fue generado directamente por Inkscape.


  1. Remember, remember…↩︎

  2. Está muy lindo para prototipar cosas visuales sin mucho laburo de antemano. Un abrazo a Daniel Shiffman y The Coding Train.↩︎

  3. En una versión anterior había usado un piolín envuelto por el eje. Funcionó, pero el cambio del diámetro en el eje al envolverse el piolín era suficiente para agregar una distorsión enorme.↩︎