Solidoodle UDEV Scripts

Linux UDEV can be used to automate things when you plug or unplug the solidoodle.

The scripts documented here recognize my solidoodle when I plug in the USB cable, use my home automation system to turn on the A/C power, and make a /dev/solidoodle symlink pointing to whatever /dev/ttyACM* device happened to be created (I work on other arduino projects sometimes which also make make a /dev/ttyACM*, so these script bypass any confusion with device names.) After all that, it also starts up RepetierHost automatically so I'm ready to go just by plugging in one USB cable.

Another recent addition to these scripts recognizes when I have my AVRISP mkII programmer plugged in. If I have it plugged in, then these scripts interefere with using it to upload new microcode, so the scripts now check and don't do anything if the ISP is connected to the system.

The only thing the scripts don't do is sort things out if you plug in more than one solidoodle - that would take more fiddling (and I have no idea how I'd know which symlink to make for which printer short of changing the firmware in each one to print a different ID string I could look for.)

Start with the UDEV rules file:

/etc/udev/rules.d/85-solidoodle.rules

ACTION=="add", SUBSYSTEMS=="usb", ATTRS{idVendor}=="16c0", ATTRS{idProduct}=="0483", ENV{ID_USB_DRIVER}=="cdc_acm", RUN+="/usr/local/bin/solidoodle-udev"

ACTION=="remove", ENV{ID_BUS}=="usb", ENV{ID_MODEL_ID}=="0483", ENV{ID_VENDOR_ID}=="16c0", ENV{ID_USB_DRIVER}=="cdc_acm", RUN+="/usr/local/bin/solidoodle-udev"

That makes udev automagically run the /usr/local/bin/solidoodle-udev script when I plug or unplug the solidoodle. Experimentation found that the ID_USB_DRIVER environment only appears in one udev event, so I use it to make sure the script only runs once when I plug in the solidoodle. I also found through trial and error that matching with ATTRS only works with the add action, for remove I had to use ENV.

Turns out that any old teensy device is gonna be triggered by this same script. For instance, here's one of my AardRemote devices plugged into the computer along with the solidoodle. Can you tell which is which?

[root@zooty local]# lsusb
...
Bus 003 Device 043: ID 16c0:0483 Van Ooijen Technische Informatica Teensyduino Serial
...
Bus 003 Device 044: ID 16c0:0483 Van Ooijen Technische Informatica Teensyduino Serial
...

Which leads to the development of the send-this program:

/usr/local/bin/send-this

/* Silly program used to verify a tty is in fact connected to a solidoodle.
 * Change B115200 to different baud rate define if your firmware is set
 * to operate at a different rate.
 *
 * typical usage: send-this /dev/ttyACM0 M115
 *
 * That will send an M115 gcode command which should respond by printing the
 * info about the firmware and wot-not. If it doesn't maybe the device you
 * just plugged in isn't really a solidoodle...
 */

#include <sys/time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <termios.h>
#include <unistd.h>
#include <sys/select.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <stdio.h>

int
main(int argc, char ** argv) {
   struct termios settings;
   int fd;
   fd_set rset;
   struct timeval tmo;
   int selstat;
   char buf[1024];
   int i;

   /* Make sure there is at least a device name and one word to send */

   if (argc < 2) {
      fprintf(stderr, "usage: send-this tty string...\n");
      exit(2);
   }

   /* Make sure we can open the device in nonblocking mode */

   fd = open(argv[1], O_RDWR|O_NONBLOCK);
   if (fd == -1) {
      i = errno;
      fprintf(stderr, "Unable to open %s : %s (%d)\n",
         argv[1], strerror(i), i);
      exit(2);
   }

   /* Set it to "raw" mode */

   memset((void *)&settings, 0, sizeof(settings));
   cfmakeraw(&settings);
   cfsetospeed(&settings, B115200);
   cfsetispeed(&settings, B115200);
   if (tcsetattr(fd, TCSANOW, &settings) == -1) {
      i = errno;
      fprintf(stderr, "Unable to set %s to raw mode : %s (%d)\n",
         argv[1], strerror(i), i);
      exit(2);
   }

   /* If there is any gibberish buffered up, discard it */

   for ( ; ; ) {
      FD_ZERO(&rset);
      FD_SET(fd, &rset);
      tmo.tv_sec = 0;
      tmo.tv_usec = 0;
      selstat = select(fd+1, &rset, NULL, NULL, &tmo);
      if (selstat <= 0) {
         break;
      }
      read(fd, buf, sizeof(buf));
   }

   /* Now send space separated words from argument list followed by newline */

   for (i = 2; i < argc; ++i) {
      write(fd, " ", 1);
      write(fd, argv[i], strlen(argv[i]));
   }
   write(fd, "\r", 1);

   /* Echo everything returned till we get no data for 1/10 of a second */

   for ( ; ; ) {
      FD_ZERO(&rset);
      FD_SET(fd, &rset);
      tmo.tv_sec = 0;
      tmo.tv_usec = 100000;
      selstat = select(fd+1, &rset, NULL, NULL, &tmo);
      if (selstat <= 0) {
         break;
      }
      i = read(fd, buf, sizeof(buf));
      write(fileno(stdout), buf, i);
   }
   return 0;
}

This allows me to send a M115 gcode query to the device and see if it actually generates a response that looks like a solidoodle. Now I can use it from the udev scripts.

/usr/local/bin/solidoodle-udev

#!/bin/bash
#
# This script is run from a UDEV rule. To work within the UDEV framework, it
# must run quick like a bunny and not wait on anything, so basically, this
# script mostly does nothing except spawn another script in the background,
# passing useful environment variables as arguments. (I find the login
# option on su is required to completely sever the connection with UDEV).
#
su -l root /bin/bash -c \
   "/usr/local/bin/bg-dammit /bin/bash /usr/local/bin/solidoodle-bg-udev $ACTION $DEVNAME"

I found that UDEV gets very upset if you take a lot of time to do something in a script. Since I need to wait for the device to stabalize before I can check it, the /usr/local/bin/solidoodle-udev script is written to simply invoke /usr/local/bin/solidoodle-bg-udev as a completely disconnected background task which it passes useful environment variables:

/usr/local/bin/solidoodle-bg-udev

#!/bin/bash
#
# This script is started in the background from solidoodle-udev. It is safe
# in here to now do long term operations, UDEV isn't waiting on this.
#
# Two arguments are passed in, the first is the ACTION environment variable
# from the UDEV script, the second is the DEVNAME environment variable.
#
export ACTION="$1"
export DEVNAME="$2"

# The ever helpful systemd will seek out and destroy any udev cgroup
# tasks that hang around too long for it, so we need this perfectly
# obvious line to make this a user cgroup task instead of a udev one.

echo $$ > /sys/fs/cgroup/systemd/user.slice/tasks

# The device isn't really there till a little time has passed, so wait
# a bit.

sleep 2

#
# First trick - these scripts are intended to do funny things when I plug in
# my solidoodle, but if I have my AVRISP mkII programmer plugged in, then it
# is the one that wants to do funny things, so if lsusb says I have the ISP
# plugged in, exit right away.
#
avrisp=`lsusb | fgrep 'ID 03eb:2104 Atmel Corp. AVR ISP mkII' | wc -l`
if [ "$avrisp" -ge 1 ]
then
   exit
fi
if [ "$ACTION" = "add" ]
then

   # Now find out if $DEVNAME is really a solidoodle by sending a gcode
   # command to query the device and see if it responds with a string that
   # says solidoodle somewhere.

   solid=`/usr/local/bin/send-this "$DEVNAME" M115 2>/dev/null | \
          fgrep -i solidoodle | wc -l`
   if [ "$solid" -ge 1 ]
   then

      # Looks like it really is a solidoodle. Make a symlink for this
      # device named /dev/solidoodle so we don't have to worry about
      # plugging in multiple different ACM devices in random orders
      # screwing up our printer config info.

      rm -f /dev/solidoodle
      ln -s $DEVNAME /dev/solidoodle
      su -l tom /bin/bash -c \
         "/usr/local/bin/bg-dammit /bin/bash /home/tom/scripts/solidoodle-on"
   fi
   exit
fi
if [ "$ACTION" = "remove" ]
then

   # If /dev/solidoodle is a symlink to $DEVNAME, then remove the symlink.

   solid=`readlink /dev/solidoodle 2>/dev/null`
   if [ "$solid" = "$DEVNAME" ]
   then
      rm -f /dev/solidoodle
      su -l tom /bin/bash -c \
         "/usr/local/bin/bg-dammit /bin/bash /home/tom/scripts/solidoodle-off"
   fi
   exit
fi

This script can now wait a bit to give the device a chance to be fully initialized and send the query which should say something about solidoodle in the reply. If it does, I make a symlink from the (not necessarily constant) device name to /dev/solidoodle, so no matter which order you plug in the different teensy devices, the /dev/solidoodle link will point to the actual solidoodle.

The only gotcha is that you have to manually edit the printer device name down under the ~/.mono/registry/CurrentUser/software/repetier/printer directories to say /dev/solidoodle since RepetierHost refuses to list the symlink as a possible device in the printer settings dialog. In my case the specific file I needed to edit was solidoodle2/values.xml (I named my printer solidoodle2, hence the directory name.) The value name to edit is port, giving it the string value /dev/solidoodle

At this point the ACTION=remove case in the solidoodle-bg-udev script makes sense. It checks the /dev/solidoodle symlink to see if it matches the device just unplugged and removes the link and turns off the power.

So you can plug and unplug a variety of teensy devices in any order and it will manage the solidoodle correctly (of course, I hope sending M115 to some other arduino device doesn't trigger self destruct or anything like that :-).

Now my user level scripts can be run as me, but only when a verified solidoodle is plugged or unplugged:

~/scripts/solidoodle-on

#!/bin/bash
#
# The USB cable for the solidoodle was plugged in, so turn on the power cube
# for the motors, heaters, etc.
#
curl 'http://newvera.my.lan:3480/data_request?id=action&output_format=xml&DeviceNum=5&serviceId=urn:upnp-org:serviceId:SwitchPower1&action=SetTarget&newTargetValue=1'

The solidoodle-on script does two things. First, it talks to my Vera z-wave controller to turn on A/C power to the solidoodle power cube, then it gives that a few seconds to get going and starts up Repetier Host on my local display. All that from just plugging in a USB cable :-).

UPDATE: This used to be able to run RepetierHost directly. That worked perfectly for years, then after some updates to things which seemed completely irrelevant, it suddenly stopped working. I can run any X program I care to, and it works fine, just not RepetierHost. I never figured out why, but as you see now, the script touches a file in my home directory. A file which this program watches, and starts RepetierHost for me (sheesh!):

/usr/local/bin/stoopid

// Suddenly RepetierHost started refusing to run when I tried to
// start it from the udev scripts for my 3d printer.
//
// I gave up trying to understand why, and now I'm writing this
// stupid program to use inotify to notice when a file is touched
// and start RepetierHost.

#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <errno.h>
#include <sys/inotify.h>

#define MAGIC_FILE "/home/tom/.stoopid"

int
main(int argc, char ** argv) {
   struct inotify_event ev;

   int fd = inotify_init1(IN_CLOEXEC);
   inotify_add_watch(fd, MAGIC_FILE, IN_ATTRIB);
   int len;
   for ( ; ; ) {
      len = read(fd, (void *)&ev, sizeof(ev));
      if (len == sizeof(ev)) {

         // I'm only watching one file for one event, start
         // RepetierHost on a successful read.

         pid_t kid = fork();
         if (kid == 0) {
            execl("/usr/local/bin/bg-dammit", "/usr/local/bin/bg-dammit",
                  "/home/tom/scripts/RepetierHost", NULL);
            _exit(2);
         } else {
            int status;
            waitpid(kid, &status, 0);
         }
      } else {
         if ((len != -1) || (errno != EINTR)) {
            // Weird error, get out of here.
            break;
         }
      }
   }
   return 0;
}

I run this as part of my login, so it is always sitting in the background waiting to start RepetierHost for me.

(Note that this is a really good example of a reason to get a home automation system you can control from command line scripts rather than being forced to use some “helpful” GUI interface.)

The solidoodle-off script is simpler. It just turns the A/C power off when I unplug the USB cable:

~/scripts/solidoodle-off

#!/bin/bash
#
# When solidoodle is unplugged, this turns off the power.
#
curl 'http://newvera.my.lan:3480/data_request?id=action&output_format=xml&DeviceNum=5&serviceId=urn:upnp-org:serviceId:SwitchPower1&action=SetTarget&newTargetValue=0'

A heck of a lot of trouble, but very convenient when you get it all working.

A lot of the scripts on this page use the bg-dammit utility, so I'm including it here.

/usr/local/bin/bg-dammit

// The nohup program seems to let things kill its kids in fedora 20.
// This program will endeavor to eradicate whatever is leaking through
// nohup.

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
#include <string.h>

int bastards[] = {
   SIGHUP, SIGINT, SIGQUIT, SIGABRT, SIGFPE, SIGPIPE, SIGTERM,
   SIGUSR1, SIGUSR2
};

#ifdef DO_DEBUG

#define DBPRINTF(args) \
   { \
      FILE * dbf = fopen("/home/tom/dammit.log", "a"); \
      fprintf args; \
      fclose(dbf); \
   }

#else

#define DBPRINTF(args)

#endif

int
main(int argc, char ** argv) {

   // Make everything point to /dev/null, close all other file descriptors

   int null_out_fd = open("/dev/null", O_WRONLY);
   int null_in_fd = open("/dev/null", O_RDONLY);
   dup2(null_out_fd, fileno(stdout));
   dup2(null_out_fd, fileno(stderr));
   dup2(null_in_fd, fileno(stdin));
   for (int i = 3; i < 60000; ++i) {
      close(i);
   }

   // Ignore every ignorable signal

   struct sigaction act;
   for (int i = 0; i < sizeof(bastards)/sizeof(bastards[0]); ++i) {
      memset((void *)&act, 0, sizeof(act));
      act.sa_handler = SIG_IGN;
      sigaction(bastards[i], &act, NULL);
   }

   // Fork a child we can make into a new session

   pid_t kid = fork();
   if (kid == 0) {
      // This is the child. Make a new session here.
      pid_t s = setsid();
      DBPRINTF((dbf,"1st setsid call returns %d\n",(int)s))
      // Now fork yet another child to do all the real work and make
      // the intermediate child exit.
      pid_t grandkid = fork();
      if (grandkid == 0) {
         s = setsid(); // Just to make doubly sure.
         DBPRINTF((dbf,"2nd setsid call returns %d\n",(int)s))
         ++argv;
         --argc;
         execvp(argv[0], argv);
         DBPRINTF((dbf,"Oops. Executed past the exec of %s\n",argv[0]))
         _exit(2);
      } else {
         _exit(0);
      }
   } else {
      // This is the parent, wait for the kid.
      int status;
      waitpid(kid, &status, 0);
   }
   return 0;
}

This is a program that backgrounds things as forcefully as I can possibly background them.

Go back to my main Solidoodle page.

Page last modified Sat Aug 8 18:52:31 2020