├── .gitignore ├── .vscode └── settings.json ├── README.md ├── etc ├── init.d │ └── touchscreen └── systemd │ └── system │ └── touchscreen.service ├── img ├── logo.png └── tentacle-20x20.png ├── main.py ├── octoprint.cfg.sample ├── os_utils.py ├── panels.kv ├── pics ├── screenshot_control.png ├── screenshot_job.png ├── screenshot_osutils.png ├── screenshot_status.png └── screenshot_temps.png └── start.sh /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | octoprint.cfg 3 | libs 4 | requests 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "Octoprint", 4 | "Popen", 5 | "ifconfig", 6 | "ifdown", 7 | "ifup", 8 | "inet", 9 | "nicname" 10 | ] 11 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OctoPiTouchPanel 2 | Touchscreen interface for OctoPrint using the kivy gui framework 3 | 4 | The layout is designed for the official Raspberry Pi Foundation 7" touchscreen. I would recommend using the Raspberry Pi 2 or 3 Model B for the Octoprint server. I think the Pi 1 might be a bit under powered for running this touchscreen app. 5 | 6 | # Installation 7 | Big thanks to Tim Vaillemans for documenting this installation process. 8 | ## Installation for python2 9 |
 10 | Install OctoPi (https://octopi.octoprint.org/)
 11 | ssh to your Octoprint server
 12 | sudo su -
 13 | apt-get update
 14 | apt-get upgrade
 15 | apt-get install python-pip
 16 | sudo apt-get install libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev \
 17 |    pkg-config libgl1-mesa-dev libgles2-mesa-dev \
 18 |    python-setuptools libgstreamer1.0-dev git-core \
 19 |    gstreamer1.0-plugins-{bad,base,good,ugly} \
 20 |    gstreamer1.0-{omx,alsa} python-dev libmtdev-dev \
 21 |    xclip
 22 | pip install -U Cython==0.27.3
 23 | pip install git+https://github.com/kivy/kivy.git@master
 24 | pip install kivy-garden
 25 | garden install graph
 26 | 
27 | 28 | ## Installation for python3 29 |
 30 | Install OctoPi 0.16 or higher (https://octopi.octoprint.org/)
 31 | ssh to your Octoprint server
 32 | sudo apt-get update
 33 | sudo apt-get upgrade
 34 | sudo apt-get install python3 python3-pip python3-dev python3-setuptools
 35 | sudo apt-get install libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev \
 36 |    pkg-config libgl1-mesa-dev libgles2-mesa-dev git-core \
 37 |    gstreamer1.0-plugins-{bad,base,good,ugly} libgstreamer1.0-dev \
 38 |    gstreamer1.0-{omx,alsa}  libmtdev-dev xclip
 39 | 
 40 | python3 -m pip install --upgrade --user pip wheel setuptools
 41 | python3 -m pip install --upgrade --user Cython==0.29.10 pillow PySDL2
 42 | python3 -m pip install --user git+https://github.com/kivy/kivy.git@master
 43 | ~/.local/bin/garden install --upgrade graph
 44 | 
45 | 46 | ## Configure kivy to use the touchscreen input 47 | [Copied from https://kivy.org/docs/installation/installation-rpi.html]
48 | If you are using the official Raspberry Pi touch display, you need to configure Kivy to use it as an input source. To do this, edit the file ~/.kivy/config.ini and go to the [input] section. Add this: 49 |
 50 | mouse = mouse
 51 | mtdev_%(name)s = probesysfs,provider=mtdev
 52 | hid_%(name)s = probesysfs,provider=hidinput
 53 | 
54 | 55 | ## Disable console screen blanking 56 | Raspbian Jessie has console screen blanking turned on by default. It is set to 600 seconds. To disable this you must edit this file: 57 |
 58 | vi /boot/cmdline.txt
 59 | 
60 | 61 | After 'console=tty1' add 'consoleblank=0'. Your cmdline.txt should look something like this: 62 |
 63 | dwc_otg.lpm_enable=0 console=serial0,115200 console=tty1 consoleblank=0 root=PARTUUID=17e26144-02 rootfstype=ext4 elevator=deadline fsck.repair=yes rootwait
 64 | 
65 | Note the cmdline.txt file must be a single line of text. After saving this file reboot your Pi. 66 | 67 | ## Clone this repo 68 |
 69 | cd ~
 70 | git clone https://github.com/matt448/OctoPiTouchPanel.git
 71 | 
72 | 73 | ## Configure OctoPiTouchPanel 74 |
 75 | cd OctoPiTouchPanel
 76 | cp octoprint.cfg.sample octoprint.cfg
 77 | edit octoprint.cfg (with vim, nano, etc.)
 78 | The important items to edit are in the APISettings section.
 79 | - host: should almost always be 127.0.0.1
 80 | - apikey: get API key from your octoprint webpage. Looks something like AADDEEDD0BA48F891F3966F856765FB
 81 | - nicname: Use ifconfig to identify network interface Looks something like eth0 or wlan0
 82 | 
83 | 84 | 85 | ## Configure system to start touchscreen app on boot 86 | Older Linux distros use start scripts in /etc/init.d. Newer Linux distros use start scripts in /etc/systemd/system. 87 | I've included both with this project but since systemd is now the standard I will detail how to set that up. 88 |
 89 | sudo cp OctoPiTouchPanel/etc/systemd/system/touchscreen.service /etc/systemd/system
 90 | sudo chmod +x /etc/systemd/system/touchscreen.service
 91 | sudo systemctl enable touchscreen
 92 | 
93 | The touchscreen app should now launch when the Pi boots up. 94 | 95 | 96 | ## Manually starting and stopping the app 97 |
 98 | sudo systemctl start touchscreen
 99 | sudo systemctl stop touchscreen
100 | 
101 | 102 | # Screenshots 103 | 104 | ![screenshot](pics/screenshot_status.png) 105 | 106 |
107 | 108 | ![screenshot_control](pics/screenshot_control.png) 109 | 110 |
111 | 112 | ![screenshot_temps](pics/screenshot_temps.png) 113 | 114 |
115 | 116 | ![screenshot_temps](pics/screenshot_job.png) 117 | 118 |
119 | 120 | ![screenshot_temps](pics/screenshot_osutils.png) 121 | -------------------------------------------------------------------------------- /etc/init.d/touchscreen: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | ### BEGIN INIT INFO 3 | # Provides: touchscreen 4 | # Required-Start: $remote_fs $syslog 5 | # Required-Stop: $remote_fs $syslog 6 | # Default-Start: 2 3 4 5 7 | # Default-Stop: 0 1 6 8 | # Short-Description: Octoprint touchscreen 9 | # Description: Octoprint touchscreen 10 | ### END INIT INFO 11 | 12 | # Author: Matthew McMillan 13 | # 14 | 15 | # Do NOT "set -e" 16 | 17 | # PATH should only include /usr/* if it runs after the mountnfs.sh script 18 | PATH=/sbin:/usr/sbin:/bin:/usr/bin 19 | DESC="Description of the service" 20 | NAME=touchscreen 21 | DAEMON=/root/OctoPiTouchPanel/main.py 22 | DAEMON_ARGS="" 23 | PIDFILE=/var/run/$NAME.pid 24 | SCRIPTNAME=/etc/init.d/$NAME 25 | CHDIR='/root/OctoPiTouchPanel' 26 | CHUID='root' 27 | 28 | # Exit if the package is not installed 29 | [ -x "$DAEMON" ] || exit 0 30 | 31 | # Read configuration variable file if it is present 32 | [ -r /etc/default/$NAME ] && . /etc/default/$NAME 33 | 34 | # Load the VERBOSE setting and other rcS variables 35 | . /lib/init/vars.sh 36 | 37 | # Define LSB log_* functions. 38 | # Depend on lsb-base (>= 3.2-14) to ensure that this file is present 39 | # and status_of_proc is working. 40 | . /lib/lsb/init-functions 41 | 42 | # 43 | # Function that starts the daemon/service 44 | # 45 | do_start() 46 | { 47 | # Return 48 | # 0 if daemon has been started 49 | # 1 if daemon was already running 50 | # 2 if daemon could not be started 51 | log_daemon_msg "Starting $NAME daemon" 52 | sleep 5 53 | start-stop-daemon --start --quiet --background --chuid $CHUID --chdir $CHDIR --pidfile $PIDFILE --make-pidfile --exec $DAEMON --test > /dev/null \ 54 | || return 1 55 | start-stop-daemon --start --quiet --background --chuid $CHUID --chdir $CHDIR --pidfile $PIDFILE --make-pidfile --exec $DAEMON -- \ 56 | $DAEMON_ARGS \ 57 | || return 2 58 | log_end_msg $? 59 | # Add code here, if necessary, that waits for the process to be ready 60 | # to handle requests from services started subsequently which depend 61 | # on this one. As a last resort, sleep for some time. 62 | } 63 | 64 | # 65 | # Function that stops the daemon/service 66 | # 67 | do_stop() 68 | { 69 | # Return 70 | # 0 if daemon has been stopped 71 | # 1 if daemon was already stopped 72 | # 2 if daemon could not be stopped 73 | # other if a failure occurred 74 | log_daemon_msg "Stopping $NAME daemon" 75 | start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE 76 | RETVAL="$?" 77 | log_end_msg $RETVAL 78 | [ "$RETVAL" = 2 ] && return 2 79 | # Wait for children to finish too if this is a daemon that forks 80 | # and if the daemon is only ever run from this initscript. 81 | # If the above conditions are not satisfied then add some other code 82 | # that waits for the process to drop all resources that could be 83 | # needed by services started subsequently. A last resort is to 84 | # sleep for some time. 85 | start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --exec $DAEMON 86 | [ "$?" = 2 ] && return 2 87 | # Many daemons don't delete their pidfiles when they exit. 88 | rm -f $PIDFILE 89 | return "$RETVAL" 90 | } 91 | 92 | # 93 | # Function that sends a SIGHUP to the daemon/service 94 | # 95 | do_reload() { 96 | # 97 | # If the daemon can reload its configuration without 98 | # restarting (for example, when it is sent a SIGHUP), 99 | # then implement that here. 100 | # 101 | start-stop-daemon --stop --signal 1 --quiet --pidfile $PIDFILE --name $NAME 102 | return 0 103 | } 104 | 105 | case "$1" in 106 | start) 107 | [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME" 108 | do_start 109 | case "$?" in 110 | 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; 111 | 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; 112 | esac 113 | ;; 114 | stop) 115 | [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME" 116 | do_stop 117 | case "$?" in 118 | 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; 119 | 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; 120 | esac 121 | ;; 122 | status) 123 | status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $? 124 | ;; 125 | #reload|force-reload) 126 | # 127 | # If do_reload() is not implemented then leave this commented out 128 | # and leave 'force-reload' as an alias for 'restart'. 129 | # 130 | #log_daemon_msg "Reloading $DESC" "$NAME" 131 | #do_reload 132 | #log_end_msg $? 133 | #;; 134 | restart|force-reload) 135 | # 136 | # If the "reload" option is implemented then remove the 137 | # 'force-reload' alias 138 | # 139 | log_daemon_msg "Restarting $DESC" "$NAME" 140 | do_stop 141 | case "$?" in 142 | 0|1) 143 | do_start 144 | case "$?" in 145 | 0) log_end_msg 0 ;; 146 | 1) log_end_msg 1 ;; # Old process is still running 147 | *) log_end_msg 1 ;; # Failed to start 148 | esac 149 | ;; 150 | *) 151 | # Failed to stop 152 | log_end_msg 1 153 | ;; 154 | esac 155 | ;; 156 | *) 157 | #echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}" >&2 158 | echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2 159 | exit 3 160 | ;; 161 | esac 162 | 163 | : 164 | -------------------------------------------------------------------------------- /etc/systemd/system/touchscreen.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Octoprint Touchscreen Interface 3 | After=network.target 4 | 5 | [Service] 6 | Type=simple 7 | User=pi 8 | WorkingDirectory=/home/pi/OctoPiTouchPanel 9 | ExecStart=/usr/bin/python3 /home/pi/OctoPiTouchPanel/main.py 10 | Restart=on-abort 11 | 12 | [Install] 13 | WantedBy=multi-user.target 14 | -------------------------------------------------------------------------------- /img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matt448/OctoPiTouchPanel/dd90f76aa475212dab89b36ab38f0b288f52df4b/img/logo.png -------------------------------------------------------------------------------- /img/tentacle-20x20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matt448/OctoPiTouchPanel/dd90f76aa475212dab89b36ab38f0b288f52df4b/img/tentacle-20x20.png -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # kivy imports 4 | from kivy.app import App 5 | from kivy.uix.tabbedpanel import TabbedPanel 6 | from kivy.uix.floatlayout import FloatLayout 7 | from kivy.uix.boxlayout import BoxLayout 8 | from kivy.uix.gridlayout import GridLayout 9 | from kivy.lang import Builder 10 | from kivy.config import Config 11 | from kivy.core.window import Window 12 | from kivy.clock import Clock 13 | from kivy.utils import get_color_from_hex as rgb 14 | from kivy.garden.graph import Graph, MeshLinePlot, SmoothLinePlot 15 | 16 | # other imports 17 | import time 18 | import os 19 | from math import sin, cos 20 | import requests 21 | import json 22 | import configparser 23 | import sys 24 | from subprocess import * 25 | import pprint 26 | from collections import deque # Fast pops from the ends of lists 27 | import os_utils 28 | 29 | # Temperature lists 30 | hotendactual_list = deque([]) 31 | hotendtarget_list = deque([]) 32 | bedactual_list = deque([]) 33 | bedtarget_list = deque([]) 34 | 35 | g_hotend_actual = 0 36 | g_hotend_target = 0 37 | g_bed_actual = 0 38 | g_bed_target = 0 39 | 40 | # Initialize Temperature lists 41 | temperature_list_size = 201 42 | temperature_graph_time_max = 30000 # Minutes * 10000 43 | for i in range(temperature_list_size): 44 | hotendactual_list.append(0) 45 | hotendtarget_list.append(0) 46 | bedactual_list.append(0) 47 | bedtarget_list.append(0) 48 | # Fill timestamp list with 1000x time vals in seconds 49 | graphtime_list = [] 50 | for i in range(temperature_list_size): # Fill the list with zeros 51 | graphtime_list.append(0) 52 | 53 | graphtime_list[0] = temperature_graph_time_max 54 | for i in range(temperature_list_size - 1): # Replace values with decreasing seconds from 30 to 0 55 | val = graphtime_list[i] - (temperature_graph_time_max / (temperature_list_size - 1)) 56 | graphtime_list[i + 1] = (val) 57 | 58 | 59 | # Read settings from the config file 60 | settings = configparser.ConfigParser() 61 | settings.read('octoprint.cfg') 62 | host = settings.get('APISettings', 'host') 63 | nicname = settings.get('APISettings', 'nicname') 64 | apikey = settings.get('APISettings', 'apikey') 65 | debug = int(settings.get('Debug', 'debug_enabled')) 66 | hotend_max = int(settings.get('MaxTemps', 'hotend_max')) 67 | bed_max = int(settings.get('MaxTemps', 'bed_max')) 68 | invert_X = int(settings.get('AxisInvert', 'invert_X')) 69 | invert_Y = int(settings.get('AxisInvert', 'invert_Y')) 70 | invert_Z = int(settings.get('AxisInvert', 'invert_Z')) 71 | 72 | # Define Octoprint constants 73 | httptimeout = 3 # http request timeout in seconds 74 | printerapiurl = 'http://' + host + '/api/printer' 75 | printheadurl = 'http://' + host + '/api/printer/printhead' 76 | bedurl = 'http://' + host + '/api/printer/bed' 77 | toolurl = 'http://' + host + '/api/printer/tool' 78 | jobapiurl = 'http://' + host + '/api/job' 79 | connectionurl = 'http://' + host + '/api/connection' 80 | commandurl = 'http://' + host + '/api/printer/command' 81 | headers = {'X-Api-Key': apikey, 'content-type': 'application/json'} 82 | 83 | if debug: 84 | print("*********** DEBUG ENABLED ************") 85 | print(headers) 86 | 87 | bed_temp_val = 0.0 88 | hotend_temp_val = 0.0 89 | jogincrement = 10 90 | 91 | platform = sys.platform # Grab platform name for platform specific commands 92 | 93 | start_time = time.time() 94 | 95 | ################################ 96 | # Load the Kivy widget layout 97 | Builder.load_file('panels.kv') 98 | 99 | 100 | class Panels(TabbedPanel): 101 | global bed_max 102 | global hotend_max 103 | 104 | def gettemps(self, *args): 105 | global g_hotend_actual 106 | global g_hotend_target 107 | global g_bed_actual 108 | global g_bed_target 109 | self.ids.hotendslider.max = hotend_max 110 | self.ids.bedslider.max = bed_max 111 | try: 112 | if debug: 113 | print('[GET TEMPS] Trying /printer API request to Octoprint...') 114 | r = requests.get(printerapiurl, headers=headers, timeout=httptimeout) 115 | if debug: 116 | print('[GET TEMPS] STATUS CODE: ', str(r.status_code)) 117 | except requests.exceptions.RequestException as e: 118 | r = False 119 | if debug: 120 | print('[GET TEMPS] ERROR: Couldn\'t contact Octoprint /printer API') 121 | print(e) 122 | if r and r.status_code == 200: 123 | if debug: 124 | print('[GET TEMPS] JSON Data: ' + str(r.json())) 125 | if r and r.status_code == 200 and 'tool0' in r.json()['temperature']: 126 | printeronline = True 127 | g_hotend_actual = int(r.json()['temperature']['tool0']['actual']) 128 | g_hotend_target = int(r.json()['temperature']['tool0']['target']) 129 | g_bed_actual = int(r.json()['temperature']['bed']['actual']) 130 | g_bed_target = int(r.json()['temperature']['bed']['target']) 131 | printing = r.json()['state']['flags']['printing'] 132 | paused = r.json()['state']['flags']['paused'] 133 | operational = r.json()['state']['flags']['operational'] 134 | 135 | if debug: 136 | print(' BED ACTUAL: ' + str(g_bed_actual)) 137 | print('HOTEND ACTUAL: ' + str(g_hotend_actual)) 138 | print(' PRINTING: ' + str(printing)) 139 | print(' PAUSED: ' + str(paused)) 140 | print(' OPERATIONAL: ' + str(operational)) 141 | 142 | # Update text color on Temps tab if values are above 40C 143 | if g_bed_actual > 40: 144 | self.ids.tab3_bed_actual.color = [1, 0, 0, 1] 145 | else: 146 | self.ids.tab3_bed_actual.color = [1, 1, 1, 1] 147 | 148 | if g_bed_target > 40: 149 | self.ids.tab3_bed_target.color = [1, 0, 0, 1] 150 | else: 151 | self.ids.tab3_bed_target.color = [1, 1, 1, 1] 152 | 153 | if g_hotend_actual > 40: 154 | self.ids.tab3_hotend_actual.color = [1, 0, 0, 1] 155 | else: 156 | self.ids.tab3_hotend_actual.color = [1, 1, 1, 1] 157 | 158 | if g_hotend_target > 40: 159 | self.ids.tab3_hotend_target.color = [1, 0, 0, 1] 160 | else: 161 | self.ids.tab3_hotend_target.color = [1, 1, 1, 1] 162 | 163 | # Enable/Disable extruder buttons 164 | if g_hotend_actual < 130 or printing or paused: 165 | self.ids.extrude.disabled = True 166 | self.ids.retract.disabled = True 167 | else: 168 | self.ids.extrude.disabled = False 169 | self.ids.retract.disabled = False 170 | 171 | # Set pause/resume label on pause button 172 | if paused: 173 | self.ids.pausebutton.text = 'Resume' 174 | else: 175 | self.ids.pausebutton.text = 'Pause' 176 | 177 | # Enable/Disable print job buttons 178 | if printing or paused: 179 | self.ids.printbutton.disabled = True 180 | self.ids.cancelbutton.disabled = False 181 | self.ids.pausebutton.disabled = False 182 | else: 183 | self.ids.printbutton.disabled = False 184 | self.ids.cancelbutton.disabled = True 185 | self.ids.pausebutton.disabled = True 186 | 187 | # Set position of slider pointer 188 | self.ids.hotendpb.value = (g_hotend_actual / self.ids.hotendslider.max) * 100 189 | self.ids.bedpb.value = (g_bed_actual / self.ids.bedslider.max) * 100 190 | 191 | # Update temperature values with new data 192 | self.ids.bed_actual.text = str(g_bed_actual) + u"\u00b0" + ' C' 193 | self.ids.hotend_actual.text = str(g_hotend_actual) + u"\u00b0" + ' C' 194 | if g_bed_target > 0: 195 | self.ids.bed_target.text = str(g_bed_target) + u"\u00b0" + ' C' 196 | else: 197 | self.ids.bed_target.text = 'OFF' 198 | if g_hotend_target > 0: 199 | self.ids.hotend_target.text = str(g_hotend_target) + u"\u00b0" + ' C' 200 | else: 201 | self.ids.hotend_target.text = 'OFF' 202 | else: 203 | if r: 204 | print('Error. API Status Code: ', str(r.status_code)) # Print API status code if we have one 205 | # If we can't get any values from Octoprint just fill values with not available. 206 | self.ids.bed_actual.text = 'N/A' 207 | self.ids.hotend_actual.text = 'N/A' 208 | self.ids.bed_target.text = 'N/A' 209 | self.ids.hotend_target.text = 'N/A' 210 | 211 | def home(self, *args): 212 | axis = args[0] 213 | print('HOME AXIS: ' + axis) 214 | if axis == 'xy': 215 | homedata = {'command': 'home', 'axes': ['x', 'y']} 216 | else: 217 | homedata = {'command': 'home', 'axes': ['z']} 218 | try: 219 | if debug: 220 | print('[HOME ' + axis + '] Trying /API request to Octoprint...') 221 | r = requests.post(printheadurl, headers=headers, json=homedata, timeout=httptimeout) 222 | if debug: 223 | print('[HOME ' + axis + '] STATUS CODE: ' + str(r.status_code)) 224 | except requests.exceptions.RequestException as e: 225 | r = False 226 | if debug: 227 | print('ERROR: Couldn\'t contact Octoprint /job API') 228 | print(e) 229 | 230 | def jogaxis(self, *args): 231 | axis = args[0] 232 | direction = args[1] 233 | global invert_X 234 | global invert_Y 235 | global invert_Z 236 | global jogincrement 237 | invert_axis = {'x': invert_X, 'y': invert_Y, 'z': invert_Z} 238 | 239 | print('AXIS: ' + axis) 240 | print('DIRECTION: ' + direction) 241 | print('INCREMENT: ' + str(jogincrement)) 242 | 243 | if direction == 'up' or direction == 'forward' or direction == 'left': 244 | if invert_axis[axis]: 245 | inc = jogincrement * -1 246 | else: 247 | inc = jogincrement 248 | 249 | if direction == 'down' or direction == 'backward' or direction == 'right': 250 | if invert_axis[axis]: 251 | inc = jogincrement 252 | else: 253 | inc = jogincrement * -1 254 | 255 | jogdata = {'command': 'jog', axis: inc} 256 | print('JOGDATA: ' + str(jogdata)) 257 | try: 258 | if debug: 259 | print('[JOG ' + axis + ' ' + direction + '] Trying /API request to Octoprint...') 260 | r = requests.post(printheadurl, headers=headers, json=jogdata, timeout=httptimeout) 261 | if debug: 262 | print('STATUS CODE: ' + str(r.status_code)) 263 | except requests.exceptions.RequestException as e: 264 | r = False 265 | if debug: 266 | print('ERROR: Couldn\'t contact Octoprint /job API') 267 | print(e) 268 | 269 | def jogincrement(self, *args): 270 | global jogincrement 271 | if debug: 272 | print('[JOG INCREMENT] Button pressed') 273 | print('[JOG INCREMENT] INC: ' + str(args[0])) 274 | jogincrement = args[0] 275 | 276 | def connect(self, *args): 277 | connectiondata = {'command': 'connect', 'port': '/dev/ttyACM0', 'baudrate': 250000, 278 | 'save': False, 'autoconnect': False} 279 | try: 280 | if debug: 281 | print('[CONNECT] Trying /job API request to Octoprint...') 282 | print('[CONNECT] ' + connectionurl + str(connectiondata)) 283 | r = requests.post(connectionurl, headers=headers, json=connectiondata, timeout=httptimeout) 284 | if debug: 285 | print('[CONNECT] STATUS CODE: ' + str(r.status_code)) 286 | print('[CONNECT] RESPONSE: ' + r.text) 287 | except requests.exceptions.RequestException as e: 288 | r = False 289 | if debug: 290 | print('[CONNECT] ERROR: Couldn\'t contact Octoprint /job API') 291 | print(e) 292 | 293 | def disconnect(self, *args): 294 | disconnectdata = {'command': 'disconnect'} 295 | try: 296 | if debug: 297 | print('[DISCONNECT] Trying /job API request to Octoprint...') 298 | print('[DISCONNECT] ' + connectionurl + str(disconnectdata)) 299 | r = requests.post(connectionurl, headers=headers, json=disconnectdata, timeout=httptimeout) 300 | if debug: 301 | print('[DISCONNECT] STATUS CODE: ' + str(r.status_code)) 302 | print('[DISCONNECT] RESPONSE: ' + r.text) 303 | except requests.exceptions.RequestException as e: 304 | r = False 305 | if debug: 306 | print('[DISCONNECT] ERROR: Couldn\'t contact Octoprint /job API') 307 | print(e) 308 | 309 | def setbedtarget(self, *args): 310 | bedsliderval = args[0] 311 | bedtargetdata = {'command': 'target', 'target': bedsliderval} 312 | if debug: 313 | print('[BED TARGET] New Value: ' + str(bedsliderval) + ' C') 314 | try: 315 | if debug: 316 | print('[BED TARGET] Trying /API request to Octoprint...') 317 | r = requests.post(bedurl, headers=headers, json=bedtargetdata, timeout=httptimeout) 318 | if debug: 319 | print ('[BED TARGET] STATUS CODE: ' + str(r.status_code)) 320 | except requests.exceptions.RequestException as e: 321 | print('[BED TARGET] ERROR: Couldn\'t contact Octoprint /job API') 322 | print(e) 323 | r = False 324 | 325 | def sethotendtarget(self, *args): 326 | hotendsliderval = args[0] 327 | hotendtargetdata = {'command': 'target', 'targets': {'tool0': hotendsliderval}} 328 | if debug: 329 | print('[HOTEND TARGET] New Value: ' + str(hotendsliderval) + ' C') 330 | try: 331 | if debug: 332 | print('[HOTEND TARGET] Trying /API request to Octoprint...') 333 | r = requests.post(toolurl, headers=headers, json=hotendtargetdata, timeout=httptimeout) 334 | if debug: 335 | print('[HOTEND TARGET] STATUS CODE: ' + str(r.status_code)) 336 | except requests.exceptions.RequestException as e: 337 | r = False 338 | if debug: 339 | print('[HOTEND TARGET] ERROR: Couldn\'t contact Octoprint /job API') 340 | print(e) 341 | 342 | def extrudefilament(self, *args): 343 | posneg = args[0] 344 | extrudeamount = (int(self.ids.extrudeamount.text) * posneg) 345 | if debug: 346 | print('[EXTRUDE FILAMENT] Amount: ' + str(extrudeamount)) 347 | extrudedata = {'command': 'extrude', 'amount': extrudeamount} 348 | if debug: 349 | print('[EXTRUDE FILAMENT] Extruding: ' + str(extrudeamount) + ' mm') 350 | try: 351 | if debug: 352 | print ('[EXTRUDE FILAMENT] Trying /API request to Octoprint...') 353 | r = requests.post(toolurl, headers=headers, json=extrudedata, timeout=httptimeout) 354 | if debug: 355 | print('[EXTRUDE FILAMENT] STATUS CODE: ' + str(r.status_code)) 356 | except requests.exceptions.RequestException as e: 357 | r = False 358 | if debug: 359 | print('[EXTRUDE FILAMENT] ERROR: Couldn\'t contact Octoprint /job API') 360 | print(e) 361 | 362 | def fanspeed(self, *args): 363 | speed_percent = int(args[0]) 364 | speed_pwm = int(speed_percent * 2.551) 365 | fan_gcode = 'M106 S' + str(speed_pwm) 366 | fancmd = {"commands": [fan_gcode]} 367 | if debug: 368 | print('[FAN CONTROL] Speed: ' + str(speed_pwm)) 369 | print('[FAN CONTROL] ' + str(fancmd)) 370 | try: 371 | r = requests.post(commandurl, headers=headers, json=fancmd, timeout=httptimeout) 372 | except requests.exceptions.RequestException as e: 373 | r = False 374 | 375 | def jobcontrol(self, *args): 376 | jobcommand = args[0] 377 | jobdata = {'command': jobcommand} 378 | try: 379 | if debug: 380 | print ('[JOB COMMAND] Trying /API request to Octoprint...') 381 | # Send job request to the job api 382 | r = requests.post(jobapiurl, headers=headers, json=jobdata, timeout=httptimeout) 383 | if debug: 384 | print ('[JOB COMMAND] STATUS CODE: ' + str(r.status_code)) 385 | print ('[JOB COMMAND] COMMAND: ' + str(jobcommand)) 386 | print ('[JOB COMMAND] BUTTON TEXT: ' + self.ids.pausebutton.text) 387 | # Update pause button text 388 | if r.status_code == 204 and jobcommand == 'pause' and self.ids.pausebutton.text == 'Pause': 389 | self.ids.pausebutton.text = 'Resume' 390 | elif r.status_code == 204 and jobcommand == 'pause' and self.ids.pausebutton.text == 'Resume': 391 | self.ids.pausebutton.text = 'Pause' 392 | 393 | except requests.exceptions.RequestException as e: 394 | r = False 395 | if debug: 396 | print ('[JOB COMMAND] ERROR: Couldn\'t contact Octoprint /job API') 397 | print(e) 398 | 399 | def getstats(self, *args): 400 | try: 401 | if debug: 402 | print ('[GET STATS] Trying /job API request to Octoprint...') 403 | r = requests.get(jobapiurl, headers=headers, timeout=1) 404 | except requests.exceptions.RequestException as e: 405 | r = False 406 | if debug: 407 | print('[GET STATS] ERROR: Couldn\'t contact Octoprint /job API') 408 | print(e) 409 | if r and r.status_code == 200: 410 | if debug: 411 | print ('[GET STATS] JSON Data: ' + str(r.json())) 412 | printerstate = r.json()['state'] 413 | jobfilename = r.json()['job']['file']['name'] 414 | jobpercent = r.json()['progress']['completion'] 415 | jobprinttime = r.json()['progress']['printTime'] 416 | jobprinttimeleft = r.json()['progress']['printTimeLeft'] 417 | if debug: 418 | print ('[GET STATS] Printer state: ' + printerstate) 419 | print ('[GET STATS] Job percent: ' + str(jobpercent) + '%') 420 | if jobfilename is not None: 421 | jobfilenamefull = jobfilename 422 | jobfilename = jobfilename[:25] # Shorten filename to 25 characters 423 | self.ids.jobfilename.text = jobfilename 424 | self.ids.jobfilenamefull.text = jobfilenamefull 425 | else: 426 | self.ids.jobfilename.text = '-' 427 | if printerstate is not None: 428 | self.ids.printerstate.text = printerstate 429 | self.ids.printerstate2.text = printerstate 430 | self.ids.printerstate3.text = printerstate 431 | else: 432 | self.ids.printerstate.text = 'Unknown' 433 | self.ids.printerstate2.text = 'Unknown' 434 | self.ids.printerstate3.text = 'Unknown' 435 | if jobpercent is not None: 436 | jobpercent = int(jobpercent) 437 | self.ids.jobpercent.text = str(jobpercent) + '%' 438 | self.ids.progressbar.value = jobpercent 439 | else: 440 | self.ids.jobpercent.text = '---%' 441 | self.ids.progressbar.value = 0 442 | if jobprinttime is not None: 443 | hours = int(jobprinttime / 60 / 60) 444 | if hours > 0: 445 | minutes = int(jobprinttime / 60) - (60 * hours) 446 | else: 447 | minutes = int(jobprinttime / 60) 448 | seconds = int(jobprinttime % 60) 449 | self.ids.jobprinttime.text = str(hours).zfill(2) + ':' + \ 450 | str(minutes).zfill(2) + ':' + str(seconds).zfill(2) 451 | else: 452 | self.ids.jobprinttime.text = '00:00:00' 453 | if jobprinttimeleft is not None: 454 | hours = int(jobprinttimeleft / 60 / 60) 455 | if hours > 0: 456 | minutes = int(jobprinttimeleft / 60) - (60 * hours) 457 | else: 458 | minutes = int(jobprinttimeleft / 60) 459 | seconds = int(jobprinttimeleft % 60) 460 | self.ids.jobprinttimeleft.text = str(hours).zfill(2) + \ 461 | ':' + str(minutes).zfill(2) + ':' + str(seconds).zfill(2) 462 | else: 463 | self.ids.jobprinttimeleft.text = '00:00:00' 464 | 465 | else: 466 | if r: 467 | print ('Error. API Status Code: ' + str(r.status_code)) # Print API status code if we have one 468 | # If we can't get any values from Octoprint API fill with these values. 469 | self.ids.jobfilename.text = 'N/A' 470 | self.ids.printerstate.text = 'Unknown' 471 | self.ids.printerstate2.text = 'Unknown' 472 | self.ids.printerstate3.text = 'Unknown' 473 | self.ids.jobpercent.text = 'N/A' 474 | self.ids.progressbar.value = 0 475 | self.ids.jobprinttime.text = '--:--:--' 476 | self.ids.jobprinttimeleft.text = '--:--:--' 477 | 478 | def update_ip_addr(self, *args): 479 | ip_addr = os_utils.get_ip_address(platform, nicname, debug) 480 | if ip_addr: 481 | self.ids.ip_addr.text = str(ip_addr) 482 | else: 483 | self.ids.ip_addr.text = 'Unknown Platform' 484 | 485 | def button_restart_os(self, *args): 486 | command = args[0] 487 | os_utils.restart_os(platform, command, debug) 488 | 489 | def button_exit_app(self, *args): 490 | os_utils.exit_app() 491 | 492 | def button_restart_networking(self, *args): 493 | os_utils.restart_networking(platform, nicname, debug) 494 | 495 | def graphpoints(self, *args): 496 | global g_hotend_actual 497 | global g_hotend_target 498 | global g_bed_actual 499 | global g_bed_target 500 | hotendactual_plot = SmoothLinePlot(color=[1, 0, 0, 1]) 501 | hotendtarget_plot = MeshLinePlot(color=[1, 0, 0, .75]) 502 | bedactual_plot = SmoothLinePlot(color=[0, 0, 1, 1]) 503 | bedtarget_plot = MeshLinePlot(color=[0, 0, 1, .75]) 504 | 505 | # Update temperature graph arrays with new data 506 | hotendactual_list.popleft() 507 | hotendactual_list.append(g_hotend_actual) 508 | hotendtarget_list.popleft() 509 | hotendtarget_list.append(g_hotend_target) 510 | bedactual_list.popleft() 511 | bedactual_list.append(g_bed_actual) 512 | bedtarget_list.popleft() 513 | bedtarget_list.append(g_bed_target) 514 | 515 | # Build list of plot points tuples from temp and time lists 516 | hotendactual_points_list = [] 517 | hotendtarget_points_list = [] 518 | bedactual_points_list = [] 519 | bedtarget_points_list = [] 520 | for i in range(temperature_list_size): 521 | hotendactual_points_list.append((graphtime_list[i] / 1000.0 * -1, int(hotendactual_list[i]))) 522 | hotendtarget_points_list.append((graphtime_list[i] / 1000.0 * -1, int(hotendtarget_list[i]))) 523 | bedactual_points_list.append((graphtime_list[i] / 1000.0 * -1, int(bedactual_list[i]))) 524 | bedtarget_points_list.append((graphtime_list[i] / 1000.0 * -1, int(bedtarget_list[i]))) 525 | 526 | # Remove all old plots from the graph before drawing new ones 527 | while(len(self.my_graph.plots) != 0): 528 | # TODO - add a counter so we can abort after a certain number of tries. 529 | self.my_graph.remove_plot(self.my_graph.plots[0]) 530 | 531 | # Draw the new graphs 532 | hotendactual_plot.points = hotendactual_points_list 533 | self.my_graph.add_plot(hotendactual_plot) 534 | hotendtarget_plot.points = hotendtarget_points_list 535 | self.my_graph.add_plot(hotendtarget_plot) 536 | bedactual_plot.points = bedactual_points_list 537 | self.my_graph.add_plot(bedactual_plot) 538 | bedtarget_plot.points = bedtarget_points_list 539 | self.my_graph.add_plot(bedtarget_plot) 540 | 541 | 542 | class TabbedPanelApp(App): 543 | def build(self): 544 | Window.size = (800, 480) 545 | panels = Panels() 546 | Clock.schedule_once(panels.gettemps, 0.5) # Update bed and hotend at startup 547 | Clock.schedule_interval(panels.gettemps, 5) # Update bed and hotend temps every 5 seconds 548 | 549 | Clock.schedule_interval(panels.getstats, 5) # Update job stats every 5 seconds 550 | 551 | Clock.schedule_once(panels.update_ip_addr, 0.5) # Update IP addr once right away 552 | Clock.schedule_interval(panels.update_ip_addr, 30) # Then update IP every 30 seconds 553 | 554 | Clock.schedule_once(panels.graphpoints, 1) # Update graphs right away at startup 555 | graph_interval = ((temperature_graph_time_max / 1000) /(temperature_list_size - 1)) * 60 556 | print('Graph update interval:', graph_interval, ' seconds') 557 | Clock.schedule_interval(panels.graphpoints, graph_interval) # Update graphs every 10 seconds 558 | return panels 559 | 560 | 561 | if __name__ == '__main__': 562 | TabbedPanelApp().run() 563 | -------------------------------------------------------------------------------- /octoprint.cfg.sample: -------------------------------------------------------------------------------- 1 | [APISettings] 2 | host: 127.0.0.1 3 | apikey: PUTYOUROCTOPRINTAPIKEYHERE 4 | nicname: eth0 5 | 6 | [Debug] 7 | #0 = Debug output disabled 8 | #1 = Debug output enabled 9 | debug_enabled: 0 10 | 11 | [MaxTemps] 12 | #This is the max temp in celsius for the sliders 13 | #that set the hot end and bed temps. 14 | hotend_max: 300 15 | bed_max: 100 16 | 17 | [AxisInvert] 18 | #Enable if the jog buttons move the wrong direction. 19 | #0 = disable axis invert 20 | #1 = enable axis invert 21 | invert_X: 0 22 | invert_Y: 0 23 | invert_Z: 0 24 | -------------------------------------------------------------------------------- /os_utils.py: -------------------------------------------------------------------------------- 1 | ########################################## 2 | # These are items for the 'OS Utils' tab 3 | from subprocess import Popen 4 | from subprocess import PIPE 5 | from subprocess import os 6 | 7 | ################################### 8 | # Exit the touchscreen application 9 | def exit_app(): 10 | exit() 11 | 12 | #################################### 13 | # Ask the OS to restart networking 14 | # This will restart the nic listed in the config file 15 | def restart_networking(platform, nicname, debug): 16 | if 'linux' in platform or 'Linux' in platform: 17 | cmd = 'sudo ifconfig ' + nicname + ' down' 18 | p = Popen(cmd, shell=True, stdout=PIPE) 19 | cmd_output = p.communicate()[0].decode('utf-8') 20 | if debug: 21 | print('[RESTART NETWORK]: ' + cmd_output) 22 | cmd = 'sudo ifconfig ' + nicname + ' up' 23 | p = Popen(cmd, shell=True, stdout=PIPE) 24 | cmd_output = p.communicate()[0].decode('utf-8') 25 | if debug: 26 | print('[RESTART NETWORK]: ' + cmd_output) 27 | else: 28 | if debug: 29 | print('Unknown Platform. Not restarting network interface') 30 | 31 | 32 | ########################################## 33 | # Request an OS restart 34 | def restart_os(platform, command, debug): 35 | if 'linux' in platform or 'Linux' in platform: 36 | print ('[RESTART] OS is going to ' + str(command)) 37 | if command == 'reboot': 38 | cmd = "sudo shutdown now -r" 39 | elif command == 'shutdown': 40 | cmd = "sudo shutdown now -h" 41 | else: 42 | cmd = "true" 43 | os.system(cmd) 44 | else: 45 | print ('[RESTART] ' + str(command) + ' Unsupported OS') 46 | 47 | 48 | ############################################# 49 | # Detect IP address of the OS 50 | def get_ip_address(platform, nicname, debug): 51 | ip = '' 52 | if 'linux' in platform or 'Linux' in platform: 53 | cmd = "ip addr show " + nicname + " | grep inet | grep -v inet6 | awk '{print $2}' | cut -d/ -f1" 54 | p = Popen(cmd, shell=True, stdout=PIPE) 55 | cmd_output = p.communicate()[0].decode('utf-8') 56 | ip = cmd_output 57 | else: 58 | if debug: 59 | print('Unknown platform. Can\'t detect IP address') 60 | return(ip) -------------------------------------------------------------------------------- /panels.kv: -------------------------------------------------------------------------------- 1 | #:kivy 1.11.1 2 | #:import rgb kivy.utils.get_color_from_hex 3 | : 4 | size_hint: 1, 1 5 | pos_hint: {'center_x': .5, 'center_y': .5} 6 | do_default_tab: False 7 | tab_height: '60dp' 8 | my_graph: my_graph 9 | ############## 10 | # Tab1 11 | ############## 12 | TabbedPanelItem: 13 | id: tab1 14 | text: 'Status' 15 | font_size: '20sp' 16 | BoxLayout: 17 | orientation: 'horizontal' 18 | ###################### 19 | # Left Stats Area 20 | ###################### 21 | BoxLayout: 22 | size_hint: (.4, 1) 23 | orientation: 'vertical' 24 | padding: 10 25 | Image: 26 | source: 'img/logo.png' 27 | size_hint_y: None 28 | height: 100 29 | GridLayout: 30 | cols: 2 31 | Label: 32 | text: 'IP Address:' 33 | Label: 34 | id: ip_addr 35 | size_hint_x: 1.75 36 | text: 'N/A' 37 | Label: 38 | text: 'Machine State:' 39 | Label: 40 | id: printerstate 41 | text: 'Unknown' 42 | Label: 43 | text: 'File:' 44 | Label: 45 | id: jobfilename 46 | text: ' ' 47 | Label: 48 | text: 'Time Elapsed:' 49 | Label: 50 | id: jobprinttime 51 | text: '00:00:00' 52 | Label: 53 | text: 'Est. Time Left:' 54 | Label: 55 | id: jobprinttimeleft 56 | text: '00:00:00' 57 | Label: 58 | text: 'Printed: ' 59 | Label: 60 | id: jobpercent 61 | text: '---%' 62 | ProgressBar: 63 | id: progressbar 64 | size_hint_x: .97 65 | size_hint_y: None 66 | height: '15dp' 67 | value: 0 68 | ###################### 69 | # Right Stats Area 70 | ###################### 71 | BoxLayout: 72 | size_hint: (.6, 1) 73 | orientation: 'vertical' 74 | GridLayout: 75 | cols: 3 76 | size_hint: (1, .3) 77 | Label: 78 | text: ' ' 79 | size_hint_y: None 80 | height: 20 81 | Label: 82 | text_size: self.size 83 | halign: 'center' 84 | valign: 'middle' 85 | text: 'Actual' 86 | Label: 87 | text_size: self.size 88 | halign: 'center' 89 | valign: 'middle' 90 | text: 'Target' 91 | Label: 92 | size_hint_y: None 93 | height: 40 94 | text_size: self.size 95 | bold: True 96 | halign: 'right' 97 | valign: 'middle' 98 | text: 'Hot End:' 99 | Label: 100 | id: hotend_actual 101 | text_size: self.size 102 | font_size: 30 103 | bold: True 104 | halign: 'center' 105 | valign: 'middle' 106 | text: 'N/A' + u"\u00b0" + ' C' 107 | Label: 108 | id: hotend_target 109 | text_size: self.size 110 | font_size: 30 111 | bold: True 112 | halign: 'center' 113 | valign: 'middle' 114 | text: 'N/A' + u"\u00b0" + ' C' 115 | Label: 116 | size_hint_y: None 117 | height: 40 118 | bold: True 119 | text_size: self.size 120 | halign: 'right' 121 | valign: 'middle' 122 | text: 'Bed:' 123 | Label: 124 | id: bed_actual 125 | text_size: self.size 126 | font_size: 30 127 | bold: True 128 | halign: 'center' 129 | valign: 'middle' 130 | text: 'N/A' + u"\u00b0" + ' C' 131 | Label: 132 | id: bed_target 133 | text_size: self.size 134 | font_size: 30 135 | bold: True 136 | halign: 'center' 137 | valign: 'middle' 138 | text: 'N/A' + u"\u00b0" + ' C' 139 | Graph: 140 | id:my_graph 141 | xlabel: 'Time' 142 | ylabel: 'Temp' 143 | x_ticks_minor: 5 144 | x_ticks_major: 5 145 | y_ticks_major: 50 146 | y_grid_label: True 147 | x_grid_label: True 148 | padding: 5 149 | xlog: False 150 | ylog: False 151 | x_grid: True 152 | y_grid: True 153 | xmin: -30 154 | xmax: 0 155 | ymin: 0 156 | ymax: 300 157 | label_options: { 'color': rgb('444444'), 'bold': True} 158 | background_color: rgb('f8f8f2') # back ground color of canvas 159 | tick_color: rgb('808080') # ticks and grid 160 | border_color: rgb('808080') # border drawn around each graph 161 | ############## 162 | # Tab2 163 | ############## 164 | TabbedPanelItem: 165 | text: 'Control' 166 | font_size: '20sp' 167 | FloatLayout: 168 | #X/Y Axis buttons 169 | Label: 170 | text: 'X/Y' 171 | font_size: '35sp' 172 | pos: -265, 95 173 | Button: 174 | text: '^' 175 | id: jogforward 176 | disabled: False 177 | on_press: root.jogaxis('y', 'forward') 178 | pos: 100, 200 179 | size_hint: .10, .15 180 | Button: 181 | text: '<' 182 | id:jogleft 183 | disabled: False 184 | on_press: root.jogaxis('x', 'left') 185 | pos: 10, 130 186 | size_hint: .10, .15 187 | Button: 188 | text: 'H' 189 | id: homexy 190 | disabled: False 191 | on_press: root.home('xy') 192 | pos: 100, 130 193 | size_hint: .10, .15 194 | Button: 195 | text: '>' 196 | id: jogright 197 | disabled: False 198 | on_press: root.jogaxis('x', 'right') 199 | pos: 190, 130 200 | size_hint: .10, .15 201 | Button: 202 | text: 'v' 203 | id: jogbackward 204 | disabled: False 205 | on_press: root.jogaxis('y', 'backward') 206 | pos: 100, 60 207 | size_hint: .10, .15 208 | #Z axis buttons 209 | Label: 210 | text: 'Z' 211 | font_size: '35sp' 212 | pos: -65, 95 213 | Button: 214 | size_hint_x: None 215 | width: '25dp' 216 | id: jogzup 217 | disabled: False 218 | text: '^' 219 | on_press: root.jogaxis('z', 'up') 220 | pos: 300, 200 221 | size_hint: .10, .15 222 | Button: 223 | text: 'H' 224 | id: homez 225 | disabled: False 226 | on_press: root.home('z') 227 | pos: 300, 130 228 | size_hint: .10, .15 229 | Button: 230 | text: 'v' 231 | id: jogzdown 232 | disabled: False 233 | on_press: root.jogaxis('z', 'down') 234 | pos: 300, 60 235 | size_hint: .10, .15 236 | #Connect Button 237 | Label: 238 | id: printerstate2 239 | text: 'Unknown' 240 | #pos: 310, 195 241 | pos: 335, 195 242 | Button: 243 | text: 'Connect' 244 | id: connect 245 | on_press: root.connect() 246 | pos: 675, 345 247 | size_hint: .15, .10 248 | Button: 249 | text: 'Disconnect' 250 | id: connect 251 | on_press: root.disconnect() 252 | pos: 675, 295 253 | size_hint: .15, .10 254 | ##################### 255 | # Jog increments 256 | ##################### 257 | ToggleButton: 258 | text: '0.1' 259 | id: joginc01 260 | on_press: root.jogincrement(0.1) 261 | group: 'jogincrement' 262 | state: 'normal' 263 | pos: 10, 0 264 | size_hint: .1, .09 265 | ToggleButton: 266 | text: '1' 267 | id: joginc1 268 | on_press: root.jogincrement(1) 269 | group: 'jogincrement' 270 | state: 'normal' 271 | pos: 105, 0 272 | size_hint: .1, .09 273 | ToggleButton: 274 | text: '10' 275 | id: joginc10 276 | on_press: root.jogincrement(10) 277 | group: 'jogincrement' 278 | state: 'down' 279 | pos: 200, 0 280 | size_hint: .1, .09 281 | ToggleButton: 282 | text: '100' 283 | id: joginc100 284 | on_press: root.jogincrement(100) 285 | group: 'jogincrement' 286 | state: 'normal' 287 | pos: 300, 0 288 | size_hint: .1, .09 289 | ###################### 290 | # Extrude/Retract 291 | ###################### 292 | Spinner: 293 | id: extrudeamount 294 | text: '5' 295 | values: '1', '5', '10', '20', '100' 296 | pos: 450, 0 297 | size_hint: .15, .09 298 | Label: 299 | text: 'mm' 300 | pos: 138, -189 301 | Button: 302 | text: 'Extrude' 303 | id: extrude 304 | disabled: False 305 | on_press: root.extrudefilament(1) 306 | pos: 450, 60 307 | size_hint: .15, .15 308 | Button: 309 | text: 'Retract' 310 | id: retract 311 | disabled: False 312 | on_press: root.extrudefilament(-1) 313 | pos: 450, 130 314 | size_hint: .15, .15 315 | ###################### 316 | # Fan Controls 317 | ###################### 318 | Slider: 319 | id: fanslider 320 | max: 100 321 | value: int(100) 322 | orientation: 'vertical' 323 | on_value: fanslider.value = int(self.value) 324 | size_hint: .15, .42 325 | pos: 620, 0 326 | Button: 327 | size_hint_x: None 328 | width: '25dp' 329 | id: fanpercent 330 | disabled: False 331 | text: 'FAN ' + str(int(fanslider.value)) + '%' 332 | on_press: root.fanspeed(fanslider.value) 333 | pos: 700, 100 334 | size_hint: .10, .15 335 | Button: 336 | text: 'FAN OFF' 337 | id: fanoff 338 | disabled: False 339 | on_press: root.fanspeed(0) 340 | pos: 700, 12 341 | size_hint: .10, .15 342 | 343 | ############################### 344 | # Tab3 - Temperature Controls 345 | ############################### 346 | TabbedPanelItem: 347 | text: 'Temps' 348 | font_size: '20sp' 349 | FloatLayout: 350 | ################# 351 | #Bed temp slider 352 | ################# 353 | Slider: 354 | id: bedslider 355 | max: 100 356 | value: int(0) 357 | on_value: bedslider.value = int(self.value) 358 | size_hint: .80, .15 359 | pos: 160, 295 360 | ProgressBar: 361 | id: bedpb 362 | size_hint: .76, .15 363 | pos: 176, 265 364 | value: 0 365 | Label: 366 | text: 'Selected Bed Target: ' + str(bedslider.value) + u"\u00b0" + ' C' 367 | font_size: '20sp' 368 | halign: 'left' 369 | pos: 50, 150 370 | Button: 371 | id: setbedtarget 372 | text: 'Set Bed Target' 373 | on_press: root.setbedtarget(bedslider.value) 374 | size_hint: .18, .12 375 | pos: 10, 325 376 | Button: 377 | id: setbedoff 378 | text: 'Turn Off Bed' 379 | on_press: root.setbedtarget(0) 380 | size_hint: .18, .12 381 | pos: 10, 270 382 | Label: 383 | id: tab3_bed_actual 384 | text: 'Bed Actual: ' + bed_actual.text 385 | font_size: '16sp' 386 | pos: -60, 75 387 | Label: 388 | id: tab3_bed_target 389 | text: 'Bed Target: ' + bed_target.text 390 | font_size: '16sp' 391 | pos: 200, 75 392 | 393 | #################### 394 | #Hot end temp slider 395 | #################### 396 | Slider: 397 | id: hotendslider 398 | max: 300 399 | value: int(0) 400 | on_value: hotendslider.value = int(self.value) 401 | size_hint: .80, .15 402 | pos: 160, 120 403 | ProgressBar: 404 | id: hotendpb 405 | size_hint: .76, .15 406 | pos: 176, 90 407 | value: 0 408 | Label: 409 | text: 'Selected Hot End Target: ' + str(hotendslider.value) + u"\u00b0" + ' C' 410 | font_size: '20sp' 411 | halign: 'left' 412 | pos: 50, -15 413 | Button: 414 | id: sendhotendtarget 415 | text: 'Set Hot End Target' 416 | on_press: root.sethotendtarget(hotendslider.value) 417 | size_hint: .18, .12 418 | pos: 10, 150 419 | Button: 420 | id: sethotendoff 421 | text: 'Turn Off Hot End' 422 | on_press: root.sethotendtarget(0) 423 | size_hint: .18, .12 424 | pos: 10, 95 425 | Label: 426 | id: tab3_hotend_actual 427 | text: 'Hot End Actual: ' + hotend_actual.text 428 | font_size: '16sp' 429 | pos: -60, -100 430 | Label: 431 | id: tab3_hotend_target 432 | text: 'Hot End Target: ' + hotend_target.text 433 | font_size: '16sp' 434 | pos: 200, -100 435 | 436 | ####################### 437 | # Tab4 - Job Controls 438 | ####################### 439 | TabbedPanelItem: 440 | text: 'Job' 441 | font_size: '20sp' 442 | FloatLayout: 443 | GridLayout: 444 | cols: 2 445 | pos: 0, 320 446 | size_hint: .75, .2 447 | Label: 448 | text: 'Loaded File: ' 449 | text_size: self.size 450 | size_hint: .2, 1 451 | halign: 'right' 452 | valign: 'middle' 453 | Label: 454 | id: jobfilenamefull 455 | text: 'N/A' 456 | text_size: self.size 457 | halign: 'left' 458 | valign: 'middle' 459 | Label: 460 | text: 'Status: ' 461 | text_size: self.size 462 | size_hint: .2, 1 463 | halign: 'right' 464 | valign: 'middle' 465 | Label: 466 | id: printerstate3 467 | text: 'Unknown' 468 | text_size: self.size 469 | halign: 'left' 470 | valign: 'middle' 471 | 472 | Button: 473 | id: printbutton 474 | text: 'Print' 475 | disabled: False 476 | on_press: root.jobcontrol('start') 477 | size_hint: .18, .18 478 | pos: 20, 220 479 | Button: 480 | id: pausebutton 481 | text: 'Pause' 482 | disabled: False 483 | on_press: root.jobcontrol('pause') 484 | size_hint: .18, .18 485 | pos: 20, 120 486 | Button: 487 | id: cancelbutton 488 | text: 'Cancel' 489 | disabled: False 490 | on_press: root.jobcontrol('cancel') 491 | size_hint: .18, .18 492 | pos: 20, 20 493 | 494 | 495 | ####################### 496 | # Tab5 - OS Utils 497 | ####################### 498 | TabbedPanelItem: 499 | text: 'OS Utils' 500 | font_size: '20sp' 501 | FloatLayout: 502 | Button: 503 | id: restartOS 504 | text: 'Reboot' 505 | on_press: root.button_restart_os('reboot') 506 | size_hint: .18, .12 507 | pos: 10, 95 508 | Button: 509 | id: shutdownOS 510 | text: 'Shutdown' 511 | on_press: root.button_restart_os('shutdown') 512 | size_hint: .18, .12 513 | pos: 10, 30 514 | Button: 515 | id: exitApp 516 | text: 'Exit' 517 | on_press: root.button_exit_app() 518 | size_hint: .18, .12 519 | pos: 10, 195 520 | Button: 521 | id: restartNetworking 522 | text: 'Restart Networking' 523 | on_press: root.button_restart_networking() 524 | size_hint: .18, .12 525 | pos: 210, 195 526 | 527 | -------------------------------------------------------------------------------- /pics/screenshot_control.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matt448/OctoPiTouchPanel/dd90f76aa475212dab89b36ab38f0b288f52df4b/pics/screenshot_control.png -------------------------------------------------------------------------------- /pics/screenshot_job.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matt448/OctoPiTouchPanel/dd90f76aa475212dab89b36ab38f0b288f52df4b/pics/screenshot_job.png -------------------------------------------------------------------------------- /pics/screenshot_osutils.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matt448/OctoPiTouchPanel/dd90f76aa475212dab89b36ab38f0b288f52df4b/pics/screenshot_osutils.png -------------------------------------------------------------------------------- /pics/screenshot_status.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matt448/OctoPiTouchPanel/dd90f76aa475212dab89b36ab38f0b288f52df4b/pics/screenshot_status.png -------------------------------------------------------------------------------- /pics/screenshot_temps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matt448/OctoPiTouchPanel/dd90f76aa475212dab89b36ab38f0b288f52df4b/pics/screenshot_temps.png -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script can be used to start the touchscreen app in the background. 4 | # If you want to start and stop the app with init use the script etc/init.d/touchscreen 5 | 6 | DAEMON='/root/OctoPiTouchPanel/main.py' 7 | PIDFILE=/var/run/touchscreen.pid 8 | CHDIR='/root/OctoPiTouchPanel' 9 | start-stop-daemon --start --quiet --chdir $CHDIR --pidfile $PIDFILE --exec $DAEMON 10 | --------------------------------------------------------------------------------