From c84da249758359a5de58efaf95e29249d621dece Mon Sep 17 00:00:00 2001 From: Sillyfrog Date: Mon, 4 Jun 2018 08:27:11 +1000 Subject: [PATCH] Initial commit --- Dockerfile | 94 +++++++++++++++++++++++++++++++++++++++ README.md | 31 ++++++++++++- klippy.sudoers | 1 + start.py | 57 ++++++++++++++++++++++++ udev/README.md | 7 +++ udev/okmd-udev.rules | 2 + udev/printer-connected | 20 +++++++++ udev/printer-disconnected | 61 +++++++++++++++++++++++++ 8 files changed, 272 insertions(+), 1 deletion(-) create mode 100644 Dockerfile create mode 100644 klippy.sudoers create mode 100755 start.py create mode 100644 udev/README.md create mode 100644 udev/okmd-udev.rules create mode 100755 udev/printer-connected create mode 100755 udev/printer-disconnected diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6088402 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,94 @@ + +FROM python:2.7 +EXPOSE 8080 + +RUN apt-get update && \ + apt-get install -y cmake libjpeg62-turbo-dev g++ wget unzip psmisc + +RUN cd /tmp/ && \ + wget https://github.com/jacksonliam/mjpg-streamer/archive/master.zip && \ + unzip master + +RUN cd /tmp/mjpg-streamer-master/mjpg-streamer-experimental/ && \ + make && \ + make install + +EXPOSE 5000 + +ENV CURA_VERSION=15.04.6 +ARG tag=master + +WORKDIR /opt/octoprint + +#install ffmpeg +RUN cd /tmp \ + && wget -O ffmpeg.tar.xz https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-32bit-static.tar.xz \ + && mkdir -p /opt/ffmpeg \ + && tar xvf ffmpeg.tar.xz -C /opt/ffmpeg --strip-components=1 \ + && rm -Rf /tmp/* + +#install Cura +RUN cd /tmp \ + && wget https://github.com/Ultimaker/CuraEngine/archive/${CURA_VERSION}.tar.gz \ + && tar -zxf ${CURA_VERSION}.tar.gz \ + && cd CuraEngine-${CURA_VERSION} \ + && mkdir build \ + && make \ + && mv -f ./build /opt/cura/ \ + && rm -Rf /tmp/* + +#Create an octoprint user +RUN useradd -ms /bin/bash octoprint && adduser octoprint dialout +RUN chown octoprint:octoprint /opt/octoprint +USER octoprint + +#This fixes issues with the volume command setting wrong permissions +RUN mkdir /home/octoprint/.octoprint + +#Install Octoprint +RUN git clone --branch $tag https://github.com/foosel/OctoPrint.git /opt/octoprint \ + && virtualenv venv \ + && ./venv/bin/python setup.py install \ + && echo 2 + +RUN /opt/octoprint/venv/bin/python -m pip install https://github.com/FormerLurker/Octolapse/archive/master.zip && \ +/opt/octoprint/venv/bin/python -m pip install https://github.com/pablogventura/Octoprint-ETA/archive/master.zip && \ +/opt/octoprint/venv/bin/python -m pip install https://github.com/1r0b1n0/OctoPrint-Tempsgraph/archive/master.zip && \ +/opt/octoprint/venv/bin/python -m pip install https://github.com/dattas/OctoPrint-DetailedProgress/archive/master.zip && \ +/opt/octoprint/venv/bin/python -m pip install https://github.com/kennethjiang/OctoPrint-Slicer/archive/master.zip && \ +/opt/octoprint/venv/bin/python -m pip install https://github.com/marian42/octoprint-preheat/archive/master.zip && \ +/opt/octoprint/venv/bin/python -m pip install https://github.com/jneilliii/OctoPrint-TasmotaMQTT/archive/0.3.0.zip && \ +/opt/octoprint/venv/bin/python -m pip install https://github.com/mikedmor/OctoPrint_MultiCam/archive/master.zip + +# Installing from sillyfrog until the PR is merged to master +RUN /opt/octoprint/venv/bin/python -m pip install https://github.com/sillyfrog/Octoslacka/archive/master.zip && \ +/opt/octoprint/venv/bin/python -m pip install https://github.com/sillyfrog/OctoPrint-MQTT/archive/devel.zip + +VOLUME /home/octoprint/.octoprint + + +### Klipper setup ### + +USER root + +RUN apt-get install -y sudo + +COPY klippy.sudoers /etc/sudoers.d/klippy + +RUN useradd -ms /bin/bash klippy + +USER octoprint + +WORKDIR /home/octoprint + +RUN git clone https://github.com/KevinOConnor/klipper + +RUN ./klipper/scripts/install-octopi.sh + +RUN cp klipper/config/printer-anet-a8-2017.cfg /home/octoprint/printer.cfg + +USER root + +COPY start.py / + +CMD ["/start.py"] diff --git a/README.md b/README.md index b906ae2..8e3e1e0 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,31 @@ # OctoPrint-Klipper-mjpg-Dockerfile -A Dockerfile for running OctoPrint Klipper and mjpg in a single container +A Dockerfile for running OctoPrint Klipper and mjpg in a single container. + +My initial goal was to run these across different containers, but I couldn't get the Docker permissions to play nicely. + +This is very much written for what I needed, so you'll likely need to hack this up for your setup. I've been using it for a little while now and it's going well. + +Also included are some udev rules for reference that I use. These will need to be updated with your API key etc, however it makes connecting/disconnecting (power on/off) of the printer much less painful. + +## Running the container + +Once the container is built (the usual `docker build . -t okmd`), I use the following command to run it (again, you will need to customise for your setup, I have 3 cameras also connected): + +``` +docker kill octoprint2 +docker rm octoprint2 +docker run --name octoprint2 -d -v /etc/localtime:/etc/localtime:ro -v /home/user/Documents/octoprint-config:/home/octoprint/.octoprint \ + --device /dev/ttyUSB0:/dev/ttyUSB0 \ + --device /dev/video0:/dev/video0 \ + --device /dev/video1:/dev/video1 \ + --device /dev/video2:/dev/video2 \ + -p 5000:5000 -p 8080:8080 -p 8081:8081 -p 8082:8082\ + -e "MJPG=input_uvc.so -r HD -d /dev/video2" \ + -e "MJPG1=input_uvc.so -r HD -d /dev/video0" -e "MJPG_PORT1=8081" \ + -e "MJPG2=input_uvc.so -r HD -d /dev/video1" -e "MJPG_PORT2=8082" \ + okmd +``` + +Your Klipper `printer.cfg` should be kept in the OctoPrint config directory (this is where it looks for it at startup). + +If you have any questions, feel free to log an issue on this project, and I'll see if I can help. diff --git a/klippy.sudoers b/klippy.sudoers new file mode 100644 index 0000000..f953c18 --- /dev/null +++ b/klippy.sudoers @@ -0,0 +1 @@ +octoprint ALL=(ALL:ALL) NOPASSWD: ALL diff --git a/start.py b/start.py new file mode 100755 index 0000000..6f3311d --- /dev/null +++ b/start.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python + +import subprocess +import time +import os +import pwd + +MJPG = [ + "/usr/local/bin/mjpg_streamer", + "-i", + "%(input)s", + "-o", + "output_http.so -p %(port)s -w /usr/local/share/mjpg-streamer/www/", +] + +MJPG_INPUT_DEFAULT = "input_uvc.so -r HD" + +OCTOPRINT = ["/opt/octoprint/venv/bin/octoprint", "serve"] + + +def main(): + mjpg_processes = [] + mjpg_ports = [5000] # Reserve the OctoPrint port + for k, v in os.environ.iteritems(): + if k.startswith("MJPG") and not k.startswith("MJPG_"): + v = v.strip() + port_env = "MJPG_PORT" + k[4:] + port = int(os.environ.get(port_env, 8080)) + if port in mjpg_ports: + raise ValueError("Port %s from key %s already in use" % (port, port_env)) + if not v: + v = MJPG_INPUT_DEFAULT + subs = {'input': v, 'port': port} + cmd = [] + for part in MJPG: + cmd.append(part % subs) + print("Starting: %s" % (cmd,)) + mjpg_processes.append(subprocess.Popen(cmd)) + + # Start klipper + klipper = subprocess.Popen(['sudo', '-u', 'octoprint', '/home/octoprint/klippy-env/bin/python', '/home/octoprint/klipper/klippy/klippy.py', '/home/octoprint/.octoprint/printer.cfg']) + + os.setgid( + 1000 + ) # Drop privileges, https://stackoverflow.com/questions/2699907/dropping-root-permissions-in-python#2699996 + os.setuid(1000) + os.environ['HOME'] = '/home/octoprint' + # subprocess.Popen('env', shell=True).wait() + while 1: + Poctoprint = subprocess.Popen(OCTOPRINT) + Poctoprint.wait() + time.sleep(1) + + +if __name__ == '__main__': + main() + diff --git a/udev/README.md b/udev/README.md new file mode 100644 index 0000000..b7955e6 --- /dev/null +++ b/udev/README.md @@ -0,0 +1,7 @@ +# udev Rules and Commands + +This directory contains the commands and rules I add to udev to make turning the printer on and off go smoothly, including re-connecting (my Docker container runs 24x7, however I turn off the printer when not in use). + +The `okmd-udev.rules` file should be copied to `/etc/udev/rules.d/`, and updated to have the correct paths and device names for your environment. + +The commands (`printer-*`), should also be updated to match your device names, and include your OctoPrint API key. diff --git a/udev/okmd-udev.rules b/udev/okmd-udev.rules new file mode 100644 index 0000000..827362f --- /dev/null +++ b/udev/okmd-udev.rules @@ -0,0 +1,2 @@ +ACTION=="add", KERNEL=="ttyUSB*", SUBSYSTEM=="tty", RUN+="/somewhere/printer-connected" +ACTION=="remove", KERNEL=="ttyUSB*", SUBSYSTEM=="tty", RUN+="/somewhere/printer-disconnected" diff --git a/udev/printer-connected b/udev/printer-connected new file mode 100755 index 0000000..513f115 --- /dev/null +++ b/udev/printer-connected @@ -0,0 +1,20 @@ +#!/usr/bin/env python + +import time +import os + +logf = open('/var/tmp/printer-connected.log', 'a') + +DEVNAME = os.environ['DEVNAME'] + +def log(msg): + logf.write('%s\n' % (msg,)) + +def main(): + log(time.asctime()) + log(DEVNAME) + os.system('chmod a+wr %s' % (DEVNAME,)) + +if __name__ == '__main__': + main() + diff --git a/udev/printer-disconnected b/udev/printer-disconnected new file mode 100755 index 0000000..e5a9e54 --- /dev/null +++ b/udev/printer-disconnected @@ -0,0 +1,61 @@ +#!/usr/bin/env python + +import time +import os +import requests +import sys + +logf = open('/var/tmp/printer-connected.log', 'a') + +DEVNAME = os.environ['DEVNAME'] + +RESTART_STATES = [ + 'Offline', + 'Cancelling', + 'Operational', + ] + +OCTOPRINT_URL = 'http://localhost:5000/' +API_KEY = 'XXX' + +def log(msg): + logf.write('%s\n' % (msg,)) + +def currentstate(): + headers = {'X-Api-Key': API_KEY} + url = OCTOPRINT_URL + '/api/job' + r = requests.get(url, headers=headers) + return r.json()['state'] + +def main(): + log(time.asctime()) + log("Disconnecting: {}".format(DEVNAME)) + + state = currentstate() + if state not in RESTART_STATES and DEVNAME != '/dev/ttyUSB0': + log("Not restarting. State: {} DEVNAME: {}".format(state, DEVNAME)) + # Don't reset the firmware if not in a known safe state + return + + headers = {'X-Api-Key': API_KEY} + json = { + "command": "FIRMWARE_RESTART", + } + + url = OCTOPRINT_URL + '/api/printer/command' + r = requests.post( + url, + json=json, + headers=headers + ) + + if (r.status_code == 204): + log("Disconnected OK") + sys.exit(0) + else: + log("Error disconnecting: %s" % (r,)) + sys.exit(1) + +if __name__ == '__main__': + main() +