I implemented a basic RRF RESTAPI with Python FastAPI, here some code-snippet:
def rrf_server(): from fastapi import FastAPI, File, UploadFile, HTTPException, Query, Request from fastapi.responses import JSONResponse import uvicorn from typing import Dict, Optional import threading app = FastAPI() port = 8050 if m := re.search(r'ttyUSB(\d+)$',conf['device']): port = 8050 + int(m[1]) elif m := re.search(r'ttyACM(\d+)$',conf['device']): port = 8100 + int(m[1]) UPLOAD_FOLDER = f'rrf_files-{port}' ALLOWED_EXTENSIONS = { 'gcode', 'gc' } state = 'idle' thread = None size = 0 pos = 0 resp = { "message": "OK" } if not os.path.exists(UPLOAD_FOLDER): os.mkdir(UPLOAD_FOLDER) def allowed_file(filename: str) -> bool: return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS def local_file(file): return os.path.join(UPLOAD_FOLDER,os.path.basename(file)) @app.post("/rr_upload") # -- upload file(s) async def upload_file(req: Request): #if not req.headers.get('content-type', '').startswith('multipart/form-data'): # raise HTTPException(status_code=400, detail="Expected multipart/form-data") data = dict(req.query_params) form = await req.form() fn = local_file(data['name']) iprint(f"uploading {fn}") with open(fn, "wb") as fh: fh.write(await req.body()) # -- file-content is in the body (not in the form / multipart) resp = { "message": "OK" } return resp ''' @app.get("/rr_download") async def download(req: Request): data = dict(req.query_params) if 'name' not in data: raise HTTPException(status_code=400, detail="No filename provided") fn = data['name'] resp = { "message": "OK" } return resp ''' @app.get("/rr_delete") async def cancel_print(req: Request): data = dict(req.query_params) if 'name' not in data: raise HTTPException(status_code=400, detail="No filename provided") fn = local_file(data['name']) if os.path.exists(fn): os.remove(fn) resp = { "message": "OK" } return resp @app.get("/rr_connect") async def connect(req: Request): data = dict(req.query_params) resp = { "message": "Connected" } return resp @app.get("/rr_gcode") # -- send actual G-code async def gcode(gcode: str): nonlocal resp, size, pos, state iprint(f"Gcode: '{gcode}'") if gcode == 'M0': # -- stop unconditionally state = 'idle' elif gcode == 'M27': # -- report SD print status if state == 'printing': resp = { "message": f"SD printing byte {pos}/{size}" } else: resp = { "message": "Not SD printing" } elif gcode == 'M115': # -- report Firmware resp = { "message": sendSingleGcode(gcode) } elif gcode == 'M122': # -- report board ID resp = { "message": sendSingleGcode(gcode) } elif m := re.search(r'^M32\s+"([^"]+)',gcode): # -- select file & start SD print # -- start printing fn = local_file(m[1]) pos = 0 size = os.path.getsize(fn) state = 'printing' def tracking(p): nonlocal pos, state pos = p if pos == size: state = 'idle' def continue_check(): return state == 'printing' def print_job(*args,**argv): nonlocal state printGcode(*args,**argv) iprint(f"print job {fn} finished") state = 'idle' thread = threading.Thread(target=lambda: print_job(fn,callback=tracking,continue_check=continue_check)) thread.start() else: resp = { "message": "OK" } return resp['message'] @app.get("/rr_reply") # -- echo last response async def reply(): nonlocal resp iprint(f"Response: '{resp['message']}'") return resp['message'] iprint(f"rrf-client running on {port}") uvicorn.run(app, host="0.0.0.0", port=port)It supports:
upload file (POST): /rr_upload?name=file.gcode start printing (GET): /rr_gcode?gcode=M32 "file.gcode" stop printing (GET): /rr_gcode?gcode=M0 delete file (GET): /rr_delete?name=file.gcode report progress (GET): /rr_gcode?gcode=M27 get progress (GET): /rr_replyIt's compatible with RepRapFirmwarePyAPI I coded a while ago: https://github.com/Spiritdude/RepRapFirmwarePyAPI
I'm eventually going to integrate it into https://github.com/Spiritdude/Prynt3r (I'm terribly back logged with updating the github repo with the copy I have locally) as prynt3r -d /dev/ttyUSB0 rrf-client and then use from outside it looks like a RRF board, but it's a Linux box which wires multiple 3D printers which are connected with USB:
printhost> prynt3r -d /dev/ttyUSB0 rrf-client & printhost> prynt3r -d /dev/ttyUSB1 rrf-client & ...and then on another machine:
rrf printer #0 RESTAPI: http://printhost:8050 rrf printer #1 RESTAPI: http://printhost:8051 ...This will lift up RRF RESTAPI to level of networked printing.
As I wrote earlier in the thread, I have been streaming G-code via TCP and then again to /dev/ttyUSB* but with a saturated WIFI connection the printing started to stutter; by using RRF RESTAPI one uploads a file, and then starts the print, and observe the progress and/or stop it.
Missing:
Barely tested 😉 Error handling (thermal runaway and other errors cause stop of printing) but error isn't passed back yetAnyway, if the RRF server would be more expanded (not sure how much work it would involve), we could eventually run DWC on it, and then have any 3D Printer controlled via USB then DWC controlled as well - any old time Marlin-based printers having their own DWC interface as well.