├── README.md ├── timelapse.py └── um3api.py /README.md: -------------------------------------------------------------------------------- 1 | Ultimaker 3 Timelapse Maker 2 | =========================== 3 | 4 | A script that makes timelapse videos from the onboard camera on your Ultimaker 3. 5 | 6 | ![Bulbasaur](https://thumbs.gfycat.com/EntireGlassAlaskanmalamute-size_restricted.gif) 7 | 8 | Usage 9 | ----- 10 | ``` 11 | $ ./timelapse.py HOST DELAY OUTFILE 12 | ``` 13 | 14 | This script requires Python 3.5 or later and [FFmpeg](https://ffmpeg.org/). 15 | Run the script. It will wait for your Ultimaker to begin printing, then it will start taking pictures. 16 | When the print finishes, the script will compile all the snapshots it took into a video. 17 | Video is encoded using H.264 at 30 fps, but you can easily change this by editing `ffmpegcmd` in the script. 18 | 19 | | Option | Description | 20 | | ------- | ----------- | 21 | | HOST | The IP address of your Ultimaker 3. You can find this through the menu by going to System > Network > Connection Status. | 22 | | DELAY | The time between snapshots in seconds. You'll probably want to figure this out based on how long your print will take and how long you want the video to be. For example, if I want a 10 second video at 30 fps, that will be 300 frames. If the print will take five hours, then 5 hours / 300 frames = 60 seconds between frames. | 23 | | OUTFILE | This is the name of the video file you want to make. I recommend giving it either a .mkv or .mp4 extension, although you could choose any container format that supports H.264. | 24 | 25 | Thanks 26 | ------ 27 | 28 | [Ultimaker 3 API library](https://ultimaker.com/en/community/23329-inside-the-ultimaker-3-day-3-remote-access-part-2) by Daid 29 | -------------------------------------------------------------------------------- /timelapse.py: -------------------------------------------------------------------------------- 1 | #!/bin/python3 2 | import os 3 | import argparse 4 | from requests import exceptions 5 | from tempfile import mkdtemp 6 | from time import sleep 7 | from urllib.request import urlopen 8 | from um3api import Ultimaker3 9 | 10 | cliParser = argparse.ArgumentParser(description= 11 | 'Creates a time lapse video from the onboard camera on your Ultimaker 3.') 12 | cliParser.add_argument('HOST', type=str, 13 | help='IP address of the Ultimaker 3') 14 | cliParser.add_argument('DELAY', type=float, 15 | help='Time between snapshots in seconds') 16 | cliParser.add_argument('OUTFILE', type=str, 17 | help='Name of the video file to create. Recommended formats are .mkv or .mp4.') 18 | options = cliParser.parse_args() 19 | 20 | imgurl = "http://" + options.HOST + ":8080/?action=snapshot" 21 | 22 | api = Ultimaker3(options.HOST, "Timelapse") 23 | #api.loadAuth("auth.data") 24 | 25 | def printing(): 26 | status = None 27 | # If the printer gets disconnected, retry indefinitely 28 | while status == None: 29 | try: 30 | status = api.get("api/v1/printer/status").json() 31 | if status == 'printing': 32 | state = api.get("api/v1/print_job/state").json() 33 | if state == 'wait_cleanup': 34 | return False 35 | else: 36 | return True 37 | else: 38 | return False 39 | except exceptions.ConnectionError as err: 40 | status = None 41 | print_error(err) 42 | 43 | def progress(): 44 | p = None 45 | # If the printer gets disconnected, retry indefinitely 46 | while p == None: 47 | try: 48 | p = api.get("api/v1/print_job/progress").json() * 100 49 | return "%05.2f %%" % (p) 50 | except exceptions.ConnectionError as err: 51 | print_error(err) 52 | 53 | def print_error(err): 54 | print("Connection error: {0}".format(err)) 55 | print("Retrying") 56 | print() 57 | sleep(1) 58 | 59 | tmpdir = mkdtemp() 60 | filenameformat = os.path.join(tmpdir, "%05d.jpg") 61 | print(":: Saving images to",tmpdir) 62 | 63 | if not os.path.exists(tmpdir): 64 | os.makedirs(tmpdir) 65 | 66 | print(":: Waiting for print to start") 67 | while not printing(): 68 | sleep(1) 69 | print(":: Printing") 70 | 71 | count = 0 72 | 73 | while printing(): 74 | count += 1 75 | response = urlopen(imgurl) 76 | filename = filenameformat % count 77 | f = open(filename,'bw') 78 | f.write(response.read()) 79 | f.close 80 | print("Print progress: %s Image: %05i" % (progress(), count), end='\r') 81 | sleep(options.DELAY) 82 | 83 | print() 84 | print(":: Print completed") 85 | print(":: Encoding video") 86 | ffmpegcmd = "ffmpeg -r 30 -i " + filenameformat + " -vcodec libx264 -preset veryslow -crf 18 " + options.OUTFILE 87 | print(ffmpegcmd) 88 | os.system(ffmpegcmd) 89 | -------------------------------------------------------------------------------- /um3api.py: -------------------------------------------------------------------------------- 1 | #!/bin/python3 2 | # This program is free software: you can redistribute it and/or modify 3 | # it under the terms of the GNU Affero General Public License as 4 | # published by the Free Software Foundation, either version 3 of the 5 | # License, or (at your option) any later version. 6 | # 7 | # This program is distributed in the hope that it will be useful, 8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | # GNU Affero General Public License for more details. 11 | # 12 | # You should have received a copy of the GNU Affero General Public License 13 | # along with this program. If not, see . 14 | 15 | import requests 16 | import json 17 | import os 18 | import time 19 | from getpass import getuser 20 | 21 | ## Ultimaker 3 API access class. 22 | # Allows for access of the Ultimaker 3 API with authentication. 23 | # Uses the python requests library to do the actual http requests, which does most of the work for us. 24 | class Ultimaker3: 25 | # @param ip: IP address of the printer 26 | # @param application: name of the application in string form, used during authentication requests and is shown on the printer. 27 | def __init__(self, ip, application): 28 | self.__ip = ip 29 | self.__application = application 30 | self.__session = requests.sessions.Session() 31 | self.__setAuthData("", "") 32 | 33 | # Set new authentication data, authentication data is send with each HTTP request to make sure we can PUT/POST data. 34 | def __setAuthData(self, id, key): 35 | self.__auth_id = id 36 | self.__auth_key = key 37 | self.__auth = requests.auth.HTTPDigestAuth(self.__auth_id, self.__auth_key) 38 | 39 | # Load authentication data from a file. If this file does not exists, or the data in it is invalid, we request a new authentication set and store it in the file. 40 | def loadAuth(self, filename): 41 | try: 42 | data = json.load(open(filename, "rt")) 43 | self.__setAuthData(data["id"], data["key"]) 44 | except IOError: 45 | self.__checkAuth() 46 | self.saveAuth(filename) 47 | if not self.__checkAuth(): 48 | self.saveAuth(filename) 49 | 50 | # Save the authentication data to a file. 51 | def saveAuth(self, filename): 52 | json.dump({"id": self.__auth_id, "key": self.__auth_key}, open(filename, "wt")) 53 | 54 | # Check if our authentication is valid, and if it is not request a new ID/KEY combination, this function can block till the user selected ALLOW/DENY on the printer. 55 | def __checkAuth(self): 56 | if self.__auth_id == "" or self.get("api/v1/auth/verify").status_code != 200: 57 | print("Auth check failed, requesting new authentication") 58 | response = self.post("api/v1/auth/request", data={"application": self.__application, "user": getuser()}) 59 | if response.status_code != 200: 60 | raise RuntimeError("Failed to request new API key") 61 | data = response.json() 62 | self.__setAuthData(data["id"], data["key"]) 63 | while True: 64 | time.sleep(1) 65 | response = self.get("api/v1/auth/check/%s" % (self.__auth_id)) 66 | data = response.json() 67 | print(data["message"]) 68 | if data["message"] == "authorized": 69 | print("Authorized.") 70 | break 71 | if data["message"] == "unauthorized": 72 | raise RuntimeError("Authorization denied") 73 | return False 74 | return True 75 | 76 | # Do a new HTTP request to the printer. It formats data as JSON, and fills in the IP part of the URL. 77 | def request(self, method, path, **kwargs): 78 | if "data" in kwargs: 79 | kwargs["data"] = json.dumps(kwargs["data"]) 80 | if "headers" not in kwargs: 81 | kwargs["headers"] = {"Content-type": "application/json"} 82 | return self.__session.request(method, "http://%s/%s" % (self.__ip, path), auth=self.__auth, **kwargs) 83 | 84 | # Shorthand function to do a "GET" request. 85 | def get(self, path, **kwargs): 86 | return self.request("get", path, **kwargs) 87 | 88 | # Shorthand function to do a "PUT" request. 89 | def put(self, path, **kwargs): 90 | return self.request("put", path, **kwargs) 91 | 92 | # Shorthand function to do a "POST" request. 93 | def post(self, path, **kwargs): 94 | return self.request("post", path, **kwargs) 95 | --------------------------------------------------------------------------------