├── README.md ├── dwc2.cfg ├── rr_handler.py ├── screenshots └── screen.PNG └── web_dwc2.py /README.md: -------------------------------------------------------------------------------- 1 | # dwc2-for-klipper-socket 2 | 3 | This is a rewrite of [dwc2-for-klipper](https://github.com/Stephan3/dwc2-for-klipper). As Klipper offers now a unixsocket API, its time to use it and run outside klippers main thread. 4 | 5 | ![screen](screenshots/screen.PNG?raw=true "screen") 6 | 7 | ### Things you should know 8 | - It works everywhere where klipper works, not only with duet boards 9 | - Klipper is not RepRapFirmware 10 | - This is a translator between [klipper](https://github.com/KevinOConnor/klipper) and [Duet Web Control](https://github.com/Duet3D/DuetWebControl) 11 | - The DWC service can be restarted at any time without restarting klipper 12 | - Sometimes buttons get a bad response - especially macros 13 | - Usually a timing issue 14 | - Make sure action gets performed 15 | - **Set AJAX retries to 0 for now:** 16 | - Settings > Machine-Specific > Number of maximum AJAX retries 17 | - There is a configfile now 18 | - Klipper's printer.cfg is displayed as a virtual file (config.g) in system section 19 | - Restart after configuration edits works 20 | - The macros you define in printer.cfg are displayed as virtual files wthin DWC's macros/klipper folder 21 | - For pause and resume macros you can use: 22 | - Klipper gcode macros pause_print, resume_print, cancel_print (not case sensitive) 23 | - DWC macros pause.g, resume.g, cancel.g - this is in line with RRF 24 | - DWC macros are overriding Klipper's macros 25 | 26 | ### Installation ### 27 | 28 | @th33xitus made a installer, see: 29 | [Installer](https://github.com/th33xitus/kiauh) 30 | 31 | ##### Klipper needs to run with an additional arg ```-a /tmp/klippy_uds``` #### 32 | 33 | This is my klipper systemd service located at ```/etc/systemd/system/klipper.service``` 34 | ``` 35 | [Unit] 36 | Description=klipper printer service 37 | After=network.target 38 | 39 | [Service] 40 | User=pi 41 | Group=pi 42 | ExecStart=/usr/bin/python2 /home/pi/klipper/klippy/klippy.py /home/pi/printer.cfg -l /tmp/klippy.log -a /tmp/klippy_uds 43 | WorkingDirectory=/root/klipper 44 | 45 | [Install] 46 | WantedBy=multi-user.target 47 | ``` 48 | 49 | ---- todo add /etc/default here ---- 50 | 51 | Make sure klipper is up and running with unixsocket enabled before next steps. 52 | 53 | ``` 54 | # clone this repo: 55 | cd ~ 56 | git clone https://github.com/Stephan3/dwc2-for-klipper-socket 57 | pip3 install tornado 58 | 59 | # get dwc: 60 | mkdir -p ~/sdcard/web 61 | cd ~/sdcard/web 62 | wget https://github.com/Duet3D/DuetWebControl/releases/download/v3.2.2/DuetWebControl-SD.zip 63 | unzip *.zip && for f_ in $(find . | grep '.gz');do gunzip ${f_};done 64 | rm DuetWebControl-SD.zip 65 | ``` 66 | 67 | dwc2-for-klipper-socket can run with systemd too. Here is the service I use for it, located at ```/etc/systemd/system/dwc.service``` 68 | ``` 69 | [Unit] 70 | Description=dwc_webif 71 | After=klipper.service 72 | 73 | [Service] 74 | ExecStart=/usr/bin/python3 /home/pi/dwc2-for-klipper-socket/web_dwc2.py 75 | WorkingDirectory=/home/pi/dwc2-for-klipper-socket 76 | 77 | [Install] 78 | WantedBy=multi-user.target 79 | ``` 80 | Please make sure that all paths matching your setup. 81 | 82 | You might want to reload your services with ```systemctl daemon-reload``` 83 | The webinterface can be launched by ```systemctl start dwc``` and enabled at startup ```systemctl enable dwc``` 84 | -------------------------------------------------------------------------------- /dwc2.cfg: -------------------------------------------------------------------------------- 1 | # 2 | # Specify adress and port here like 3 | # [webserver] 4 | # listen_adress: 192.168.10.130 5 | # port: 4750 6 | # will make the webif reachable at http://192.168.10.130:4750 7 | # adress can 0.0.0.0 too which make it listen on all interfaces. 8 | # 9 | [webserver] 10 | listen_adress: 0.0.0.0 11 | web_root: ~/sdcard/web 12 | port: 4750 13 | 14 | # 15 | # one can filter gcode replys. 16 | # That is usefull if you set acceleraions midprint. 17 | # on longer prints it prevents you log from that spam. 18 | # 19 | [reply_filters] 20 | regex: 21 | max_accel: \d+.\d+ 22 | max_accel_to_decel: \d+.\d+ 23 | square_corner_velocity: \d+.\d+ 24 | max_velocity: \d+.\d+ -------------------------------------------------------------------------------- /rr_handler.py: -------------------------------------------------------------------------------- 1 | 2 | import tornado.web 3 | from tornado.ioloop import IOLoop 4 | import json 5 | import time 6 | import os, shutil 7 | import datetime 8 | import re 9 | 10 | class rr_handler(tornado.web.RequestHandler): 11 | 12 | def initialize(self, dwc2): 13 | self.clients = dwc2.clients 14 | self.sd_root = dwc2.sd_root 15 | self.poll_data = dwc2.poll_data 16 | self.klippy = dwc2.klippy 17 | self.ioloop = dwc2.ioloop 18 | self.pending_requests = dwc2.pending_requests 19 | self.init_done = dwc2.init_done 20 | self.regex_filter = '|'.join(dwc2.regex_filter) 21 | 22 | async def get(self, *args): 23 | 24 | repl_ = None 25 | if self.request.remote_ip not in self.clients.keys() and "rr_connect" not in self.request.uri and self.request.remote_ip != '127.0.0.1': 26 | # response 408 timeout to force the webif reload after klippy restarts us 27 | self.clear() 28 | self.set_status(408) 29 | self.finish() 30 | return 31 | 32 | if self.request.remote_ip in self.clients.keys(): 33 | self.clients[self.request.remote_ip]['last_seen'] = time.time() 34 | 35 | # polldata fetch - curl http://127.0.0.1:4750/rr_poll_data |jq 36 | if "rr_poll_data" in self.request.uri: 37 | self.write(json.dumps(self.poll_data)) 38 | return 39 | # clients - curl http://127.0.0.1:4750/rr_clients |jq 40 | if "rr_clients" in self.request.uri: 41 | self.write(json.dumps(self.clients)) 42 | return 43 | # plopp to debug - curl http://127.0.0.1:4750/rr_entry 44 | if "rr_entry" in self.request.uri: 45 | import pdb; pdb.set_trace() 46 | return 47 | # 48 | # 49 | # connection request 50 | if "rr_connect" in self.request.uri: 51 | await rr_connect(self) 52 | return 53 | # configuration 54 | if "rr_config" in self.request.uri: 55 | await rr_config(self) 56 | return 57 | if "rr_delete" in self.request.uri: 58 | await rr_delete(self) 59 | return 60 | if "rr_disconnect" in self.request.uri: 61 | await rr_disconnect(self) 62 | return 63 | if "rr_download" in self.request.uri: 64 | await rr_download(self) 65 | return 66 | if "rr_fileinfo" in self.request.uri: 67 | repl_ = await rr_fileinfo(self) 68 | self.write(repl_) 69 | return 70 | # filehandling - dirlisting 71 | if "rr_filelist" in self.request.uri: 72 | await rr_filelist(self) 73 | return 74 | # running gcodes 75 | if "rr_gcode" in self.request.uri: 76 | await rr_gcode(self) 77 | return 78 | # creating directories 79 | if "rr_mkdir" in self.request.uri: 80 | await rr_mkdir(self) 81 | return 82 | # moving files/dirs 83 | if "rr_move" in self.request.uri: 84 | await rr_move(self) 85 | return 86 | # sending reply to gcodes 87 | if "rr_reply" in self.request.uri: 88 | await rr_reply(self) 89 | return 90 | # Status request. main datatransport 91 | if "rr_status" in self.request.uri: 92 | self.clients[self.request.remote_ip]['last_seen'] = time.time() 93 | type_ = int( self.get_argument('type') ) 94 | await rr_status(self, status=type_ ) 95 | return 96 | # Status request. main datatransport 97 | if "rr_status" in self.request.uri: 98 | await rr_status(self, status=type_ ) 99 | # 100 | print("DWC2 - unhandled? GET " + self.request.uri) 101 | self.write( json.dumps({"err": "Requesttype not impelemented in dwc translator :\n " + self.request.uri}) ) 102 | 103 | async def post(self, *args): 104 | 105 | # filehandling - uploads 106 | if "rr_upload" in self.request.uri: 107 | await rr_upload(self) 108 | return 109 | # 110 | print("DWC2 - unhandled? POST " + self.request.uri) 111 | self.write( json.dumps({"err":1}) ) 112 | # 113 | # 114 | # 115 | 116 | async def rr_connect(self): 117 | if self.request.remote_ip not in self.clients.keys(): 118 | self.clients[self.request.remote_ip] = { 119 | "last_seen": time.time() , 120 | "gcode_replys": [] , 121 | "gcode_command": {} 122 | } 123 | io_loop = IOLoop.current() 124 | io_loop.call_later(600, clear_client, self.request.remote_ip, self) 125 | 126 | self.write(json.dumps({ 127 | "err":0, 128 | "sessionTimeout":8000, # config value? 129 | "boardType":"duetmaestro" # that one is for you immutef 130 | })) 131 | 132 | # 133 | async def rr_config(self): 134 | 135 | if not self.klippy.connected or not self.init_done or \ 136 | 'Printer is ready' != self.poll_data.get('webhooks', {}).get('state_message', "Knackwurst"): 137 | self.write(json.dumps({ 138 | "axisMins": [], 139 | "axisMaxes": [], 140 | "accelerations": [], 141 | "currents": [] , # can we fetch data from tmc drivers here ? 142 | "firmwareElectronics": "OFFLINE", 143 | "firmwareName": "Klipper", 144 | "firmwareVersion": "OFFLINE", 145 | "dwsVersion": "OFFLINE", 146 | "firmwareDate": "1970-01-01", # didnt get that from klippy 147 | "idleCurrentFactor": 30, 148 | "idleTimeout": 30, 149 | "minFeedrates": [ ] , 150 | "maxFeedrates": [ ] 151 | })) 152 | return 153 | 154 | config = self.poll_data['configfile']['config'] 155 | x = config.get('stepper_x', config.get('stepper_a')) 156 | y = config.get('stepper_y', config.get('stepper_b')) 157 | z = config.get('stepper_z', config.get('stepper_c')) 158 | 159 | self.write(json.dumps({ 160 | # min(with posmin?) 161 | "axisMins": [ float( x.get('position_endstop', x.get('position_min', 0)) ), 162 | float( y.get('position_endstop', y.get('position_min', 0)) ), 163 | float( z.get('position_endstop', z.get('position_min', 0)) ) 164 | ], 165 | "axisMaxes": [ float( x.get('position_max', 0) ), 166 | float( y.get('position_max', 0) ), 167 | float( z.get('position_max', 0) ) 168 | ], 169 | "accelerations": [ self.poll_data['toolhead']['max_accel'] for x in self.poll_data['toolhead']['position'] ], 170 | "currents": [ 0, 0, 0, 0 ] , # can we fetch data from tmc drivers here ? 171 | "firmwareElectronics": self.poll_data['info']['cpu_info'], 172 | "firmwareName": "Klipper", 173 | "firmwareVersion": self.poll_data['info']['software_version'], 174 | "dwsVersion": self.poll_data['info']['software_version'], 175 | "firmwareDate": "1970-01-01", # didnt get that from klippy 176 | "idleCurrentFactor": 30, 177 | "idleTimeout": 30, 178 | "minFeedrates": [ 1 for x in self.poll_data['toolhead']['position'] ] , 179 | "maxFeedrates": [ self.poll_data['toolhead']['max_velocity'] for x in self.poll_data['toolhead']['position'] ] # unitconversion ? 180 | })) 181 | # 182 | async def rr_delete(self): 183 | if not self.sd_root: 184 | self.write({'err': 1}) 185 | return 186 | 187 | path_ = self.sd_root + self.get_argument('name').replace("0:", "") 188 | 189 | if os.path.isdir(path_): 190 | shutil.rmtree(path_) 191 | 192 | if os.path.isfile(path_): 193 | os.remove(path_) 194 | 195 | self.write({'err': 0}) 196 | async def rr_disconnect(self): 197 | self.clients.pop(self.request.remote_ip, None) 198 | async def rr_download(self): 199 | 200 | if self.sd_root: 201 | path = self.sd_root + self.get_argument('name').replace("0:", "") 202 | else: 203 | path = None 204 | 205 | # ovverride for config file 206 | if "config.g" in self.get_argument('name').replace("0:", ""): 207 | path = self.poll_data['info']['config_file'] 208 | 209 | # handle heigthmap 210 | if 'heightmap.csv' in path: 211 | repl_ = get_heigthmap(self) 212 | if repl_ and path: 213 | with open(path, "w") as f: 214 | for line in repl_: 215 | f.write( line + '\n') 216 | 217 | if os.path.isfile(path): 218 | 219 | self.set_header( 'Content-Type', 'application/force-download' ) 220 | self.set_header( 'Content-Disposition', 'attachment; filename=%s' % os.path.basename(path) ) 221 | 222 | with open(path, "rb") as f: 223 | self.write( f.read() ) 224 | async def rr_fileinfo(self): 225 | if not self.sd_root: 226 | self.write({'err': 1}) 227 | return 228 | 229 | path = None 230 | 231 | try: 232 | path = self.sd_root + self.get_argument('name').replace("0:", "") 233 | except: 234 | # happens if we sart midprint 235 | selcted = self.poll_data['print_stats']['filename'] 236 | if selcted: 237 | path = self.sd_root + '/' + selcted 238 | 239 | if path: 240 | return parse_gcode(path, self) 241 | else: 242 | return {} 243 | async def rr_filelist(self): 244 | 245 | directory = self.get_argument('dir', self.poll_data['last_path']) 246 | 247 | # creating the infoblock 248 | response = { 249 | "dir": directory , 250 | "first": self.get_argument('first', 0) , 251 | "files": [] , 252 | "next": 0 , 253 | "err": 0 254 | } 255 | 256 | # virtual config file 257 | if "/sys" in directory.replace("0:", ""): 258 | response['files'].append({ 259 | "type": "f", 260 | "name": "config.g" , 261 | "size": 1 , 262 | "date": datetime.datetime.fromtimestamp(os.stat(self.poll_data.get('info',{}).get('config_file',1)).st_mtime).strftime("%Y-%m-%dT%H:%M:%S") 263 | }) 264 | 265 | if not self.sd_root: 266 | if "/sys" in directory.replace("0:", ""): 267 | self.write(json.dumps(response)) 268 | else: 269 | self.write(json.dumps({'err':1})) 270 | self.clients[self.request.remote_ip]['gcode_replys'].append("Error: Can´t detect virtual sdcard.") 271 | return 272 | 273 | path = self.sd_root + directory.replace("0:", "") 274 | 275 | # if rrf is requesting directory, it has to be there. 276 | if not os.path.exists(path): 277 | pass 278 | 279 | # append elements to files list matching rrf syntax 280 | if os.path.exists(path): 281 | 282 | for file in os.listdir(path): 283 | os.rename(os.path.join(path, file), os.path.join(path, file.replace(' ', '_'))) 284 | 285 | for el_ in os.listdir(path): 286 | el_path = path + "/" + str(el_) 287 | response['files'].append({ 288 | "type": "d" if os.path.isdir(el_path) else "f" , 289 | "name": str(el_) , 290 | "size": os.stat(el_path).st_size , 291 | "date": datetime.datetime.fromtimestamp(os.stat(el_path).st_mtime).strftime("%Y-%m-%dT%H:%M:%S") 292 | }) 293 | 294 | # add klipper macros as virtual files 295 | if len(self.poll_data['klipper_macros']) > 0 and self.get_argument('dir').replace("0:", "") == '/macros': 296 | response['files'].append({ 297 | "type": "d" , 298 | "name": "Klipper" , 299 | "date": datetime.datetime.fromtimestamp(os.stat(self.poll_data.get('info',{}).get('config_file',1)).st_mtime).strftime("%Y-%m-%dT%H:%M:%S") 300 | }) 301 | if self.get_argument('dir').replace("0:", "") == '/macros/Klipper': 302 | for macro in self.poll_data['klipper_macros']: 303 | response['files'].append({ 304 | "type": "f" , 305 | "name": macro , 306 | "size": 1 , 307 | "date": datetime.datetime.fromtimestamp(os.stat(self.poll_data.get('info',{}).get('config_file',1)).st_mtime).strftime("%Y-%m-%dT%H:%M:%S") 308 | }) 309 | 310 | self.poll_data['last_path'] = path 311 | self.write(json.dumps(response)) 312 | # 313 | async def rr_gcode(self): 314 | 315 | gcodes = str( self.get_argument('gcode') ).replace('0:', '').replace('"', '').split("\n") 316 | 317 | # Handle emergencys - just do it now 318 | for code in gcodes: 319 | if 'M112' in code: 320 | cmd_M112(self) 321 | 322 | rrf_commands = { 323 | 'G10': cmd_G10 , # set heaters temp 324 | 'M0': cmd_M0 , # cancel SD print 325 | 'M24': cmd_M24 , # resume sdprint 326 | 'M25': cmd_M25 , # pause print 327 | 'M32': cmd_M32 , # Start sdprint 328 | 'M98': cmd_M98 , # run macro 329 | 'M106': cmd_M106 , # set fan 330 | 'M120': cmd_M120 , # save gcode state 331 | 'M121': cmd_M121 , # restore gcode state 332 | #'M140': cmd_M140 , # set bedtemp(limit to 0 mintemp) 333 | 'M141': cmd_M141 , 334 | 'M290': cmd_M290 , # set babysteps 335 | 'M999': cmd_M999 # issue restart 336 | } 337 | handover = "" 338 | for g in gcodes: 339 | 340 | params = parse_params(g) 341 | execute = params['#original'] 342 | 343 | if params['#command'] in rrf_commands.keys(): 344 | func_ = rrf_commands.get(params['#command']) 345 | execute = func_(params, self) 346 | 347 | handover += execute + "\n" 348 | 349 | req_ = self.klippy.form_request( "gcode/script", {'script': handover} ) 350 | self.pending_requests[req_.id] = req_ 351 | await self.klippy.send_request(req_) 352 | 353 | try: 354 | res = await req_.wait(6) # needs 6 as dwc webapp is waiting 8 355 | except Exception as e: 356 | # asume longrunning command 357 | print("timeout reached with: " + str(e)) 358 | self.write(json.dumps("")) 359 | return 360 | 361 | if 'error' in res.keys(): 362 | # bluebear will tell us 363 | self.write(json.dumps('{"buff": 241}')) 364 | else: 365 | self.clients[self.request.remote_ip]['gcode_replys'].append("") 366 | self.write('{"buff": 241}') 367 | return 368 | 369 | # 370 | async def rr_mkdir(self): 371 | path = self.sd_root + self.get_argument('dir').replace("0:", "").replace(' ', '_') 372 | if not os.path.exists(path): 373 | os.makedirs(path) 374 | return await rr_filelist(self) 375 | return {'err': 1} 376 | async def rr_move(self): 377 | if "config.g" in self.get_argument('old').replace("0:", ""): 378 | src_ = self.poll_data.get('info',{}).get('config_file',1) 379 | dst_ = self.poll_data.get('info',{}).get('config_file',1) + ".backup" 380 | shutil.copyfile(src_ , dst_) 381 | return {'err': 0} #await rr_filelist(self) 382 | else: 383 | src_ = self.sd_root + self.get_argument('old').replace("0:", "") 384 | dst_ = self.sd_root + self.get_argument('new').replace("0:", "") 385 | try: 386 | shutil.move(src_ , dst_) 387 | except Exception as e: 388 | return {"err": 1} 389 | return await rr_filelist(self) 390 | async def rr_reply(self): 391 | output = "" 392 | try: 393 | reps = self.clients[self.request.remote_ip]['gcode_replys'] 394 | if len(reps) > 10: 395 | while len(reps) > 0: 396 | line = reps.pop(0) 397 | line = line.replace("!!", "Error: ").replace("//", "") 398 | replys = line.split('\n') 399 | for r_ in replys: 400 | if translate_status(self) == 'P' and re.findall(self.regex_filter, r_): 401 | continue 402 | output += re.sub(r'^\s', '', r_) + '\n' 403 | else: 404 | line = reps.pop(0) 405 | line = line.replace("!!", "Error: ").replace("//", "") 406 | replys = line.split('\n') 407 | for r_ in replys: 408 | if translate_status(self) == 'P' and re.findall(self.regex_filter, r_): 409 | continue 410 | output += re.sub(r'^\s', '', r_) + '\n' 411 | except Exception as e: 412 | pass 413 | else: 414 | self.write(output) 415 | async def rr_status(self, status=0): 416 | 417 | def get_axes_homed(): 418 | q_ = self.poll_data.get('toolhead', {}).get('homed_axes', []) 419 | return [ 1 if "x" in q_ else 0 , 1 if "y" in q_ else 0 , 1 if "z" in q_ else 0 ] 420 | 421 | # if no klippy connection there provide minimalistic dummy data 422 | if not self.klippy.connected or \ 423 | not self.init_done or translate_status(self) == 'O': 424 | self.write(json.dumps({ 425 | "status": "O", 426 | "seq": len(self.clients[self.request.remote_ip]['gcode_replys']) , 427 | "coords": { 428 | "xyz": [] , 429 | "machine": [] , 430 | "extr": [] 431 | }, 432 | "speeds": {}, 433 | "sensors": { 434 | "fanRPM": 0 435 | }, 436 | "params": { 437 | "fanPercent": [] 438 | } , 439 | "temps": { 440 | "state": [], 441 | "extra": [{}], 442 | "current": [], 443 | "tools": { "active": [] }, 444 | "names": [] 445 | } , 446 | "probe": {} , 447 | "axisNames": "" , 448 | "tools": [] , 449 | "volumes": 1, 450 | "mountedVolumes": 1 , 451 | "name": self.poll_data.get('info',{}).get('hostname', "offline") 452 | })) 453 | return 454 | 455 | # 456 | 457 | gcode_move = self.poll_data.get('gcode_move', {}) 458 | 459 | response = { 460 | "status": translate_status(self), 461 | "coords": { 462 | "axesHomed": get_axes_homed(), # [1,1,1] 463 | "xyz": [a - b for a, b in zip(gcode_move.get('position',[0,0,0,0])[:3], gcode_move.get('homing_origin',[0,0,0,0]))] , 464 | "machine": [ 0, 0, 0 ], # what ever this is? no documentation. 465 | "extr": gcode_move.get('position',[0,0,0,0])[3:] 466 | }, 467 | "speeds": { 468 | "requested": gcode_move.get('speed', 60) / 60 , # only last speed not current 469 | "top": gcode_move.get('speed', 60) / 60 * gcode_move.get('speed_factor', 1) # not available on klipepr 470 | }, 471 | "currentTool": 0, #self.current_tool, # still not cool 472 | "params": { 473 | "atxPower": -1, 474 | "fanNames": [ "" ], 475 | "fanPercent": [ self.poll_data.get('fan', {}).get('speed', 0)*100 ] , 476 | "speedFactor": gcode_move.get('speed_factor',1) * 100, 477 | "extrFactors": [ gcode_move.get('extrude_factor',1) * 100 ], 478 | "babystep": gcode_move.get('homing_origin',[0,0,0])[2] 479 | }, 480 | "seq": len(self.clients[self.request.remote_ip]['gcode_replys']) , 481 | "sensors": { 482 | "fanRPM": [ -1 ] 483 | }, 484 | "time": self.poll_data.get('toolhead', {}).get('print_time', 0) , # feels wrong unsure about that value 485 | "temps": { 486 | # this can be better -> will fail onprinters without a bed -> will fail on machines with more that 1 extruder 487 | "bed": { 488 | "current": self.poll_data.get('heater_bed',{}).get('temperature', 0) , 489 | "active": self.poll_data.get('heater_bed',{}).get('target', 0) , 490 | "state": 0 if self.poll_data.get('heater_bed',{}).get('target',0) < 20 else 2 , 491 | "heater": 0 492 | }, 493 | "current": [ self.poll_data.get('heater_bed',{}).get('temperature',0), self.poll_data['extruder']['temperature'] ] , 494 | "state": [ 0 if self.poll_data.get('heater_bed',{}).get('target',0) < 20 else 2, 0 if self.poll_data['extruder']['target'] < 20 else 2 ] , 495 | "names": [ "Bed" ] + [ "extruder0" ] , # name is 0 for a extruder 0 496 | "tools": { 497 | "active": [ [ self.poll_data['extruder']['target'] ] ], 498 | "standby": [ [ 0 ] ] 499 | }, 500 | # for loop that gives extrasensors available? 501 | "extra": [] 502 | }, 503 | # 504 | # STATUS 2 from here 505 | # 506 | "coldExtrudeTemp": int(self.poll_data['configfile']['config'].get('extruder', {}).get('min_extrude_temp', 170)) , 507 | "coldRetractTemp": int(self.poll_data['configfile']['config'].get('extruder', {}).get('min_extrude_temp', 170)) , 508 | "compensation": "None", 509 | "controllableFans": 1, # not cool 510 | "tempLimit": int(self.poll_data['configfile']['config'].get('extruder', {}).get('max_temp', 280)) , 511 | "endstops": 4088, # what does this do? 512 | "firmwareName": "Klipper", 513 | "geometry": self.poll_data['configfile']['config']['printer']['kinematics'], 514 | "axes": len(get_axes_homed()), 515 | "totalAxes": len(get_axes_homed()) + 1, 516 | "axisNames": "XYZ", #+ "".join([ "U" for ex_ in extr_stat ]), 517 | "volumes": 1, 518 | "mountedVolumes": 1, 519 | "name": self.poll_data['info']['hostname'], 520 | "probe": { 521 | "threshold": 2000, 522 | "height": 0, 523 | "type": 8 524 | }, 525 | } 526 | # tools extruder if there 527 | response.update({ 528 | "tools": [ 529 | { 530 | "number": 0, 531 | "name": "extruder0", 532 | "heaters": [ 1 ] , 533 | "drives": [ 0 ] , 534 | "axisMap": [ 1 ], 535 | "fans": 1, 536 | "filament": "", 537 | "offsets": [ 0, 0, 0 ] 538 | } ] 539 | }) 540 | 541 | # fetch temperarutere fans 542 | for key in self.poll_data.keys(): 543 | if key.startswith('temperature_fan'): 544 | # chamber is this you? 545 | if key.endswith('chamber'): 546 | state = 0 547 | if self.poll_data[key]['target'] > 0 : state = 2 548 | if self.poll_data[key]['speed'] > 0 : state = 1 549 | response['temps'].update({ 550 | "chamber": { 551 | "current": self.poll_data[key]['temperature'] , 552 | "active": self.poll_data[key]['target'] , 553 | "state": state , 554 | "heater": 2 , # extruders + bett ? 555 | }, 556 | "current": response['temps']['current'] + [ self.poll_data[key]['temperature'] ], 557 | "state": response['temps']['state'] + [ state ] , 558 | "names": response['temps']['names'] + [ "chamber" ] , 559 | }) 560 | response['temps']['extra'].append({ 'name': 'tf_chamber speed [%]', 561 | 'temp': self.poll_data[key]['speed']*100 }) 562 | else: 563 | response['temps']['extra'].append({ 'name': key, 564 | 'temp': self.poll_data[key]['temperature'] }) 565 | # accels as graph 566 | response['temps']['extra'].append({ 'name': 'max_accel [*10]', 'temp': self.poll_data['toolhead']['max_accel']/10 }) 567 | # pwms as graph 568 | response['temps']['extra'].append({ 'name': 'extr pwm [%]', 'temp': self.poll_data.get('extruder',{}).get('power', 0) *100 }) 569 | response['temps']['extra'].append({ 'name': 'bed pwm [%]', 'temp': self.poll_data.get('heater_bed',{}).get('power', 0) *100 }) 570 | if status == 3: 571 | k_stats = self.poll_data.get('print_stats', {}) 572 | sdcard = self.poll_data.get('virtual_sdcard', {}) 573 | 574 | try: 575 | f_data = self.poll_data['running_file'] 576 | except Exception as e: 577 | f_data = self.poll_data['running_file'] = await rr_fileinfo(self) 578 | duration = round( k_stats.get('print_duration', 1), 3) # dur in secs 579 | progress = round( sdcard.get('progress', 1), 3) # prgress fkt 580 | filament_used = max( k_stats.get('filament_used', 1), .1) 581 | filament_togo = sum(f_data.get('filament', [1])) - filament_used 582 | 583 | response.update({ 584 | "currentLayer": 0, 585 | "currentLayerTime": 0, 586 | "extrRaw": [ filament_used ], 587 | "fractionPrinted": progress, 588 | "filePosition": sdcard.get('file_position', 0), 589 | "firstLayerDuration": 0, 590 | "firstLayerHeight": f_data.get('firstLayerHeight', 0), 591 | "printDuration": duration, 592 | "warmUpDuration": k_stats.get('total_duration', 0) - duration, 593 | "timesLeft": { 594 | "file": (1-progress) * duration / max( progress, 0.000001), 595 | "filament": filament_togo * duration / filament_used, 596 | "layer": 60 #self.print_data['tleft_layer'] 597 | } 598 | }) 599 | 600 | self.write(response) 601 | async def rr_upload(self): 602 | 603 | ret_ = {"err":1} 604 | if self.sd_root: 605 | path = self.sd_root + self.get_argument('name').replace("0:", "").replace(' ', '_') 606 | else: 607 | path = None 608 | 609 | if "config.g" in self.get_argument('name'): 610 | path = self.poll_data['info']['config_file'] 611 | 612 | dir_ = os.path.dirname(path) 613 | if not os.path.exists(dir_): 614 | os.makedirs(dir_) 615 | 616 | open(path.replace(" ","_"), "wb").write(self.request.body) 617 | 618 | if os.path.isfile(path): 619 | ret_ = {"err":0} 620 | 621 | self.write(json.dumps(ret_)) 622 | # 623 | # 624 | # 625 | 626 | # rrf G10 command - set heaterstemp 627 | def cmd_G10(params, self): 628 | return str("M104 T%d S%0.2f" % ( int(params['P']), int(params['S']) ) ) 629 | # rrf M0 - cancel print from sd 630 | def cmd_M0(params, self): 631 | response = "SDCARD_RESET_FILE" + "\n" 632 | path = self.sd_root + "/macros/print/cancel.g" 633 | if os.path.isfile(path): 634 | response += rrf_macro(path) 635 | elif 'CANCEL_PRINT' in self.poll_data['klipper_macros']: 636 | response += 'CANCEL_PRINT' 637 | return response 638 | # rrf M24 - start/resume print from sdcard 639 | def cmd_M24(params, self): 640 | response = 'M24\n' 641 | # rrf resume macro 642 | if self.poll_data['virtual_sdcard']['file_position']> 0: 643 | path = self.sd_root + "/macros/print/resume.g" 644 | if os.path.isfile(path): 645 | response += rrf_macro(path) 646 | elif 'RESUME_PRINT' in self.poll_data['klipper_macros']: 647 | response += 'RESUME_PRINT' 648 | return response 649 | # rrf M25 - pause print 650 | def cmd_M25(params, self): 651 | response = 'M25\n' 652 | self.poll_data['pausing'] = True 653 | # rrf pause macro: 654 | path = self.sd_root + "/macros/print/pause.g" 655 | if os.path.isfile(path): 656 | response += rrf_macro(path) 657 | elif 'PAUSE_PRINT' in self.poll_data['klipper_macros']: 658 | response += 'PAUSE_PRINT' 659 | return response 660 | # rrf M32 - start print from sdcard 661 | def cmd_M32(params, self): 662 | 663 | # file dwc1 - 'zzz/simplify3D41.gcode' 664 | # file dwc2 - '/gcodes/zzz/simplify3D41.gcode' 665 | file = '/'.join(params['#original'].split(' ')[1:]) 666 | if '/gcodes/' not in file: # DWC 1 work arround 667 | fullpath = '/gcodes/' + params['#original'].split()[1] 668 | else: 669 | fullpath = file 670 | 671 | return 'SDCARD_PRINT_FILE FILENAME=' + fullpath + '\n' 672 | # start macro 673 | def cmd_M98(params, self): 674 | 675 | path = self.sd_root + "/" + "/".join(params['#original'].split("/")[1:]) 676 | 677 | if not os.path.exists(path): 678 | klipma = params['#original'].split("/")[-1] 679 | if klipma in self.poll_data['klipper_macros']: 680 | return klipma 681 | else: 682 | return 0 683 | else: 684 | return rrf_macro(path) 685 | # rrf M106 translation to klipper scale 686 | def cmd_M106(params, self): 687 | 688 | if float(params['S']) < 1.01: 689 | command = str( params['#command'] + " S" + str(int( float(params['S']) * 255 )) ) 690 | else: 691 | command = str( params['#command'] + " S" + str(int( float(params['S']) )) ) 692 | 693 | if float(params['S']) < .05: 694 | command = str("M107") 695 | 696 | return command 697 | # emergency 698 | def cmd_M112(self): 699 | req_ = self.klippy.form_request( "emergency_stop", {} ) 700 | self.ioloop.spawn_callback(self.klippy.send_request, req_) 701 | # save states butttons 702 | def cmd_M120(params, self): 703 | return 'SAVE_GCODE_STATE NAME=DWC_BOTTON' 704 | # restore states butttons 705 | def cmd_M121(params, self): 706 | return 'RESTORE_GCODE_STATE NAME=DWC_BOTTON MOVE=0' 707 | def cmd_M141(params, self): 708 | target = int( params['S'] ) 709 | return 'SET_TEMPERATURE_FAN_TARGET temperature_fan=chamber target=' + str(target) 710 | # setting babysteps: 711 | def cmd_M290(params, self): 712 | mm_step = float( params['Z'] ) 713 | return 'SET_GCODE_OFFSET Z_ADJUST=' + str(mm_step) + ' MOVE=1' 714 | def cmd_M999(params, self): 715 | self.init_done = False 716 | return 'RESTART' 717 | # 718 | # 719 | # 720 | 721 | def get_heigthmap(self): 722 | def calc_mean(matrix_): 723 | 724 | matrix_tolist = [] 725 | for line in matrix_: 726 | matrix_tolist += line 727 | 728 | return float(sum(matrix_tolist)) / len(matrix_tolist) 729 | 730 | def calc_stdv(matrix_): 731 | from statistics import stdev 732 | 733 | matrix_tolist = [] 734 | for line in matrix_: 735 | matrix_tolist += line 736 | 737 | return stdev(matrix_tolist) 738 | 739 | # 740 | 741 | bed_mesh = self.poll_data.get('bed_mesh', {}) 742 | 743 | if bed_mesh.get('probed_matrix', None): 744 | hmap = [] 745 | z_matrix = bed_mesh['mesh_matrix'] 746 | #z_matrix = bed_mesh['probed_matrix'] 747 | mesh_data = bed_mesh # see def print_mesh in bed_mesh.py line 572 748 | 749 | meane_ = round( calc_mean(z_matrix), 3) 750 | stdev_ = round( calc_stdv(z_matrix) , 3) 751 | 752 | hmap.append( 'RepRapFirmware height map file v2 generated at ' + str(datetime.datetime.now().strftime('%Y-%m-%d %H:%M')) + ', mean error ' + str(meane_) + ', deviation ' + str(stdev_)) 753 | hmap.append('xmin,xmax,ymin,ymax,radius,xspacing,yspacing,xnum,ynum') 754 | xspace_ = ( mesh_data['mesh_max'][0] - mesh_data['mesh_min'][0] ) / len(z_matrix[0]) 755 | yspace_ = ( mesh_data['mesh_max'][1] - mesh_data['mesh_min'][1] ) / len(z_matrix) 756 | hmap.append( str(mesh_data['mesh_min'][0]) + ',' + str(mesh_data['mesh_max'][0]) + ',' + str(mesh_data['mesh_min'][1]) + ',' + str(mesh_data['mesh_max'][1]) + \ 757 | ',-1.00,' + str(xspace_) + ',' + str(yspace_) + ',' + str( len(z_matrix[0])) + ',' + str(len(z_matrix)) ) 758 | 759 | for line in z_matrix: 760 | read_by_offset = map(lambda x: round(x- mean ,3),line) 761 | read = map(lambda x: round(x,3),line) 762 | hmap.append( ' ' + ', '.join( map(str, read) )) 763 | 764 | return hmap 765 | 766 | else: 767 | self.clients[self.request.remote_ip]['gcode_replys'].append("Bed has not been probed") 768 | return 769 | def clear_client(client_ip, self): 770 | if time.time() - self.clients.get(self.request.remote_ip, {}).get('last_seen', 0) > 1800: 771 | self.clients.pop(self.request.remote_ip, None) 772 | else: 773 | self.ioloop.call_later(600, clear_client, client_ip, self) 774 | def parse_gcode(path, self): 775 | slicers = { 776 | 'Cura': 777 | { 778 | 'name': 'with\s(.+?)_SteamEngine', 779 | 'version': 'SteamEngine\s(.+?)\n', 780 | 'object_h': ';MAXZ:\d+.\d', 781 | 'first_h': ';MINZ:\d+.\d', 782 | 'layer_h': ';Layer height: \d.\d+', 783 | 'duration': ';TIME:\d+', 784 | 'filament': [ ';Filament used: \d*.\d+m' , 1000 ] 785 | }, 786 | 'ideaMaker': 787 | { 788 | 'name': ';Sliced by (ideaMaker?)', 789 | 'version': ';Sliced by ideaMaker(.+?),', 790 | 'object_h': ';Z:\d+.\d+', 791 | 'first_h': ';Z:\d+.\d+', 792 | 'layer_h': ';HEIGHT:\d+.\d+', 793 | 'duration': ';Print Time:.*', 794 | 'filament': [ ';Material.*1 Used: \d+.\d' , 1 ] 795 | }, 796 | 'KISSlicer': 797 | { 798 | 'name': '; (KISSlicer?) - .*', 799 | 'version': '; version (.+\.?)', 800 | 'object_h': '; END_LAYER_OBJECT z=.*', 801 | 'first_h': '; END_LAYER_OBJECT z=.*', 802 | 'layer_h': '; layer_thickness_mm =.*', 803 | 'duration': '\s\s\d*\.\d*\sminutes', 804 | 'filament': ['Ext.*1.*mm.*\(', 1] 805 | }, 806 | 'PrusaSlicer': 807 | { 808 | 'name': '; generated\sby\s(PrusaSlicer?)\s\d.\d+', 809 | 'version': ';\sgenerated\sby\sPrusaSlicer\s(.+?)\son\s.*', 810 | 'object_h': 'G1 (Z.+?) F.*', 811 | 'first_h': '; first_layer_height = \d.\d+\%|\d.\d+', 812 | 'layer_h': '; layer_height = \d.\d+', 813 | 'duration': '; estimated printing time.*(\d+d\s)?(\d+h\s)?(\d+m\s)?(\d+s)', 814 | 'filament': [ '; filament\sused\s.mm.\s=\s[0-9\.]+', 1 ] 815 | }, 816 | 'Simplify3D': 817 | { 818 | 'name': 'G-Code generated by\s(.+?)\(R\)', 819 | 'version': '\sVersion\s(.*?)\n', 820 | 'object_h' : '\sZ\\d+.\\d*', 821 | 'first_h': '; layer 1, Z = .*', 822 | 'layer_h': '; layerHeight,\d.\d+', 823 | 'duration': ';\s\s\sBuild time:\s.*', 824 | 'filament': [ '; Filament length: \d*.\d+', 1 ] 825 | }, 826 | 'SuperSlicer': 827 | { 828 | 'name': '; generated\sby\s(SuperSlicer?)\s\d.\d+', 829 | 'version': '; generated by SuperSlicer (.*?)\son\s.*', 830 | 'object_h': 'G1\sZ\d*\.\d*', 831 | 'first_h': '; first_layer_height = \d.\d+', 832 | 'layer_h': '; layer_height = \d.\d+', 833 | 'duration': '; estimated printing time.*(\d+d\s)?(\d+h\s)?(\d+m\s)?(\d+s)', 834 | 'filament': ['; filament\sused\s.mm.\s=\s[0-9\.]+', 1] 835 | } 836 | } 837 | 838 | def calc_time(in_): 839 | 840 | dimensions = { 841 | '(\d+(\s)?days|\d+(\s)?d)': 86400 , 842 | '(\d+(\s)?hours|\d+(\s)?h)': 3600 , 843 | '(([0-9]*\.[0-9]+)\sminutes|\d+(\s)?m)': 60 , 844 | '(\d+(\s)?seconds|\d+(\s)?s)': 1 845 | } 846 | dursecs = 0 847 | for key, value in dimensions.items(): 848 | extr = re.search(re.compile(key),in_) 849 | if extr: 850 | dursecs += float(re.sub('[a-z]|[A-Z]', '', extr.group())) * value 851 | 852 | if dursecs == 0: 853 | dursecs += float(''.join(re.findall("\d", in_))) 854 | 855 | return dursecs 856 | 857 | # 858 | 859 | metadata = { "slicer": "Slicer is not implemented" } 860 | 861 | # read 20k bytes from each side 862 | f_size = os.stat(path).st_size 863 | seek_amount = min( f_size , 80000 ) 864 | 865 | with open(path, 'rb') as f: 866 | content = f.readlines(seek_amount) # gimme the first chunk 867 | f.seek(0, os.SEEK_END) # find the end 868 | f.seek(seek_amount*-1,os.SEEK_CUR) # back up some 869 | content = content + f.readlines() # read the remainder 870 | content = [ x.decode('utf-8') for x in content ] 871 | to_analyse = " ".join(content) 872 | 873 | try: 874 | for key, value in slicers.items(): 875 | if re.search(value['name'], to_analyse): 876 | version = re.search(value['version'], to_analyse).group(1) 877 | metadata['slicer'] = re.search(value['name'], to_analyse).group(1) + " " + version 878 | metadata['objects_h'] = max( [ float(mat_) for mat_ in re.findall("\d+\.\d+", \ 879 | ' '.join(re.findall(value['object_h'], to_analyse )) ) ] + [-1] ) 880 | metadata['first_h'] = min( [ float(mat_) for mat_ in re.findall("\d+\.\d+", \ 881 | ' '.join(re.findall(value['object_h'], to_analyse )) ) ] + [10000] ) 882 | metadata['layer_h'] = min( [ float(mat_) for mat_ in re.findall("\d+\.\d+", \ 883 | ' '.join(re.findall(value['layer_h'], to_analyse )) ) ] + [10000] ) 884 | metadata['duration'] = calc_time( re.search(value['duration'], to_analyse ).group() ) 885 | metadata['filament'] = max( [ float(mat_) for mat_ in re.findall("\d+\.\d+", \ 886 | ' '.join(re.findall(value['filament'][0], to_analyse )) ) ] + [-1] ) * value['filament'][1] 887 | except Exception as e: 888 | print('Error on gcode processing ' + repr(e)) 889 | #import pdb; pdb.set_trace() 890 | 891 | response = { 892 | "size": int(os.stat(path).st_size) , 893 | "lastModified": str(datetime.datetime.utcfromtimestamp( os.stat(path).st_mtime )\ 894 | .strftime("%Y-%m-%dT%H:%M:%S")) , 895 | "height": float( metadata.get("objects_h",-1 ) ) , 896 | "firstLayerHeight": metadata.get("first_h",-1 ) , 897 | "layerHeight": float( metadata.get("layer_h",-1) ) , 898 | "printTime": int( metadata.get("duration",-1) ) , # in seconds 899 | "filament": [ float( metadata.get("filament",-1) ) ] , # in mm 900 | "generatedBy": str( metadata.get("slicer","<< Slicer not implemented >>") ) , 901 | "fileName": '0:' + str(path).replace(self.sd_root, '') , 902 | "layercount": ( float(metadata.get("objects_h",-1)) \ 903 | - metadata.get("first_h",-1) ) / float(metadata.get("layer_h",-1) ) + 1 , 904 | "err": 0 905 | } 906 | 907 | return response 908 | def parse_params(line): 909 | args_r = re.compile('([A-Z_]+|[A-Z*/])') 910 | # Ignore comments and leading/trailing spaces 911 | line = origline = line.strip() 912 | cpos = line.find(';') 913 | if cpos >= 0: 914 | line = line[:cpos] 915 | # Break line into parts and determine command 916 | parts = args_r.split(line.upper()) 917 | numparts = len(parts) 918 | cmd = "" 919 | if numparts >= 3 and parts[1] != 'N': 920 | cmd = parts[1] + parts[2].strip() 921 | elif numparts >= 5 and parts[1] == 'N': 922 | # Skip line number at start of command 923 | cmd = parts[3] + parts[4].strip() 924 | # Build gcode "params" dictionary 925 | params = { parts[i]: parts[i+1].strip() for i in range(1, numparts, 2) } 926 | params['#original'] = origline 927 | params['#command'] = parts[1] + parts[2].strip() 928 | 929 | return params 930 | def rrf_macro(path): 931 | response = "" 932 | if os.path.exists(path): 933 | with open( path ) as f: 934 | lines = f.readlines() 935 | for line in [x.strip() for x in lines]: 936 | response += line + "\n" 937 | return response 938 | def translate_status(self): 939 | 940 | # case 'F': return 'updating'; 941 | # case 'O': return 'off'; 942 | # case 'H': return 'halted'; 943 | # case 'D': return 'pausing'; 944 | # case 'S': return 'paused'; 945 | # case 'R': return 'resuming'; 946 | # case 'P': return 'processing'; ?printing? 947 | # case 'M': return 'simulating'; 948 | # case 'B': return 'busy'; 949 | # case 'T': return 'changingTool'; 950 | # case 'I': return 'idle'; 951 | 952 | state = 'I' 953 | 954 | if 'Printer is ready' != self.poll_data.get('webhooks', {}).get('state_message', "Knackwurst") : 955 | return 'O' 956 | 957 | if self.poll_data['idle_timeout']['state'] == 'Printing': 958 | state = 'B' 959 | 960 | stats = self.poll_data.get('print_stats', None) 961 | if stats: 962 | s_ = stats['state'] 963 | if s_ == 'printing': state = 'P' 964 | if self.poll_data.get('pausing', False): state = 'D' 965 | if s_ == 'paused': 966 | state = 'S' 967 | self.poll_data['pausing'] = False 968 | else: 969 | self.poll_data['pausing'] = False 970 | 971 | return state 972 | -------------------------------------------------------------------------------- /screenshots/screen.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stephan3/dwc2-for-klipper-socket/1ce04e889d3a77069f6271728bd885290ac99e93/screenshots/screen.PNG -------------------------------------------------------------------------------- /web_dwc2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import argparse 3 | import json 4 | import time 5 | import socket 6 | import tornado.web 7 | import rr_handler 8 | import os, sys 9 | import configparser 10 | from tornado import gen, iostream 11 | from tornado.ioloop import IOLoop, PeriodicCallback 12 | from tornado.locks import Event 13 | from random import randint 14 | 15 | class dwc2(): 16 | 17 | def __init__(self, config): 18 | self.httpserver = None 19 | self.sd_root = None 20 | 21 | # config - web section 22 | self.config = config 23 | self.web_root = os.path.expanduser( config.get('webserver', 'web_root', \ 24 | fallback=os.path.dirname(os.path.abspath(__file__)) + "/web") ) 25 | self.ip = config.get('webserver', 'listen_adress', fallback='0.0.0.0') 26 | self.port = config.get('webserver', 'port', fallback=4750) 27 | # config regex 28 | regex = config.get('reply_filters', 'regex', fallback=None) 29 | if regex: 30 | self.regex_filter = [ x for x in regex.split('\n') if len(x)>0 ] 31 | self.klippy = klippy_uplink(self.process_klippy_response, self.connection_lost) 32 | self.pending_requests = {} 33 | self.clients = {} 34 | self.poll_data = {} 35 | self.poll_data['last_path'] = None 36 | self.poll_data['klipper_macros'] = [] 37 | self.init_done = False 38 | 39 | self.ioloop = IOLoop.current() 40 | 41 | # 42 | # 43 | 44 | def start(self): 45 | 46 | def tornado_logger(req): 47 | fressehaltn = [] 48 | fressehaltn = [ "/favicon.ico", "/rr_status?type=1", "/rr_status?type=2", "/rr_status?type=3", "/rr_config", "/rr_reply" ] 49 | values = [str(time.time())[-8:], req.request.remote_ip, req.request.method, req.request.uri] 50 | if req.request.uri not in fressehaltn: 51 | print("Tornado:" + " - ".join(values)) # bind this to debug later 52 | 53 | application = tornado.web.Application( 54 | [ 55 | (r"/css/(.*)", tornado.web.StaticFileHandler, {"path": self.web_root + "/css/"}), 56 | (r"/js/(.*)", tornado.web.StaticFileHandler, {"path": self.web_root + "/js/"}), 57 | (r"/fonts/(.*)", tornado.web.StaticFileHandler, {"path": self.web_root + "/fonts/"}), 58 | (r"/(rr_.*)", rr_handler.rr_handler, { "dwc2": self } ), 59 | (r"/.*", self.MainHandler, { "web_root": self.web_root }), 60 | ], 61 | log_function=tornado_logger) 62 | self.httpserver = tornado.httpserver.HTTPServer( application, max_buffer_size=512*1024*1024 ) 63 | self.httpserver.listen( self.port, self.ip ) 64 | self.ioloop.spawn_callback( self.init_ ) 65 | def config_def(section, key, default): 66 | res = self.config.get(section,key) 67 | def connection_lost(self): 68 | self.klippy.connected = False 69 | self.init_done = False 70 | self.ioloop.spawn_callback( self.init_ ) 71 | res = config.get(section, key) 72 | async def init_(self): 73 | 74 | if not self.klippy.connected: 75 | await self.klippy.connect() 76 | self.ioloop.call_later(1, self.init_) 77 | return 78 | 79 | self.poll_data = {} 80 | self.poll_data['last_path'] = None 81 | self.poll_data['klipper_macros'] = [] 82 | 83 | l_ = { "gcode/help": {}, "info": {}, "objects/list": {}, "list_endpoints": {}, "gcode/subscribe_output": { "response_template": {"DWC_2": "dwc2_subscription_to_gcode_replys"} } } 84 | for item in l_.keys(): 85 | req_ = self.klippy.form_request( item, params=l_[item] ) 86 | self.pending_requests[req_.id] = req_ 87 | await self.klippy.send_request(req_) 88 | res = await req_.wait(10) 89 | self.poll_data[item] = res.get('result', "") 90 | # subscribe to all Objects 91 | objects = {} 92 | for s_ in self.poll_data["objects/list"]['objects']: 93 | objects[s_] = None 94 | req_ = self.klippy.form_request( "objects/subscribe", params={"objects": objects, "response_template": {"its_me": "waiting_for_answers_from_klippy"} } ) 95 | self.pending_requests[req_.id] = req_ 96 | await self.klippy.send_request(req_) 97 | objects_init = await req_.wait(10) 98 | for key in objects_init['result']['status']: 99 | self.poll_data[key] = objects_init['result']['status'][key] 100 | # pick sd_root from config. 101 | configfile = self.poll_data.get('configfile', None) 102 | if configfile: 103 | self.sd_root = os.path.expanduser( configfile.get('config',{}).get('virtual_sdcard',{}).get('path', None) ) 104 | # fetching klipper macros 105 | for key, val in self.poll_data['gcode/help'].items(): 106 | if val == "G-Code macro": 107 | self.poll_data['klipper_macros'].append(key) 108 | self.init_done = True 109 | def process_klippy_response(self, out_): 110 | #print("GOT: \t" + json.dumps(out_)) 111 | # poll of incomming things, once they change 112 | if '\'params\'' in str(out_) and '\'status\'' in str(out_): 113 | for key in out_['params']['status']: 114 | self.poll_data[key].update(out_['params']['status'][key]) 115 | return 116 | # ids - requests we made 117 | req_ = self.pending_requests.pop(out_.get('id', None), None) 118 | if req_: 119 | req_.notify(out_) 120 | return 121 | # gcode replys that have a reply 122 | if 'dwc2_subscription_to_gcode_replys' in str(out_): 123 | for client in self.clients.keys(): 124 | self.clients[client]['gcode_replys'].append(out_['params']['response']) 125 | return 126 | 127 | print("!! not covered !!" + json.dumps(out_)) 128 | class MainHandler(tornado.web.RequestHandler): 129 | 130 | def initialize(self, web_root): 131 | self.web_root = web_root 132 | 133 | async def get(self): 134 | 135 | if os.path.isfile(self.web_root + self.request.uri): 136 | with open(self.web_root + self.request.uri, "rb") as f: 137 | self.write( f.read() ) 138 | self.finish() 139 | return 140 | 141 | self.render( self.web_root + "/index.html" ) 142 | class klippy_uplink(object): 143 | 144 | # example dialog: 145 | 146 | def __init__(self, reply_handler, connection_lost): 147 | self.ioloop = IOLoop.current() 148 | self.iostream = None 149 | self.reply_handler = reply_handler 150 | self.con_loss = connection_lost 151 | self.connected = False 152 | 153 | # establish connection to klippys unixsocket 154 | async def connect(self): 155 | 156 | self.client = iostream.IOStream( socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) ) 157 | try: 158 | await self.client.connect("/tmp/klippy_uds") 159 | except Exception as e: 160 | print("Cant connect to klippy with: " + str(e)) 161 | self.connected = False 162 | else: 163 | self.ioloop.spawn_callback(self.read_stream, self.client) 164 | self.client.set_close_callback(self.con_loss) 165 | self.connected = True 166 | 167 | async def send_request(self, request): 168 | data = json.dumps(request.to_dict()).encode() + b"\x03" 169 | try: 170 | await self.client.write(data) 171 | except Exception as e: 172 | raise 173 | 174 | async def read_stream(self, stream): 175 | while not stream.closed(): 176 | try: 177 | data = await stream.read_until(b'\x03') 178 | except Exception as e: 179 | self.con_loss 180 | try: 181 | out_ = json.loads(data[:-1]) 182 | self.reply_handler(out_) 183 | except Exception as e: 184 | raise 185 | import pdb; pdb.set_trace() 186 | 187 | class form_request: 188 | def __init__(self, method, params): 189 | self.id = randint(100000000000, 999999999999) 190 | self.method = method 191 | self.params = params 192 | self._event = Event() 193 | self.response = None 194 | 195 | async def wait(self, timeout): 196 | start_time = time.time() 197 | while True: 198 | timeout = time.time() + timeout 199 | try: 200 | await self._event.wait(timeout=timeout) 201 | except TimeoutError: 202 | raise 203 | break 204 | #if isinstance(self.response): 205 | # raise self.response 206 | return self.response 207 | 208 | def notify(self, response): 209 | self.response = response 210 | self._event.set() 211 | 212 | def to_dict(self): 213 | return {'id': self.id, 'method': self.method, 214 | 'params': self.params} 215 | 216 | def main(): 217 | # set default files 218 | default_config = os.path.dirname(os.path.abspath(__file__)) + '/dwc2.cfg' 219 | default_log = "/tmp/dwc2.log" 220 | 221 | # parse start arguments 222 | parser = argparse.ArgumentParser(description="dwc2-for-klipper-socket") 223 | parser.add_argument("-l", "--logfile", default=default_log, metavar="", help="log file name and location") 224 | parser.add_argument("-c", "--configfile", default=default_config, metavar="", help="config file name and location") 225 | system_args = parser.parse_args() 226 | 227 | class Logger: 228 | def write(self, msg): 229 | open(system_args.logfile, "a").write(msg) 230 | def flush(self): 231 | pass 232 | 233 | open(system_args.logfile, "w").write("========== Started ==========\n") 234 | sys.stdout = Logger() 235 | sys.stderr = Logger() 236 | 237 | config = configparser.ConfigParser() 238 | config.read(system_args.configfile) 239 | 240 | io_loop = IOLoop.current() 241 | server = dwc2(config) 242 | 243 | server.start() 244 | io_loop.start() 245 | 246 | sys.stderr.close() 247 | 248 | 249 | if __name__ == '__main__': 250 | main() 251 | --------------------------------------------------------------------------------