RRF RESTAPI on Linux instead of RRF microcontroller
-
I like to run a RRF RESTAPI server on a Linux system (SBC or laptop) and then send G-code via USB to other printers - quasi networked printing using RRF RESTAPI.
Has this been done already?
I like to adapt the RRF RESTAPI and not invent another notion -
my brainstorming:/rr_upload
multiple files/rr_start
printing files to multiple connected 3D printers:- beside providing the
name
(filename) also provide device (e.g.device=/dev/ttyUSB0
)
- beside providing the
/rr_status
providing status on multiple print jobs running- the same for
/rr_cancel
,_pause
,_resume
supporting multiple print jobs
this would allow SBC or any Linux device act as RRF host sort of, and provide the same functionality like a RRF enabled 3D printer.
Any thoughts?
Why? I run networked infrastructure where I stream/send .gcode direct (ser2net) but WIFI sometimes stutters that it affects the print quality (e.g. 1-2s interruptions), and I thought to upload .gcode files direct to the 3D printer host, and then send the .gcode to multiple connected 3D printers then to avoid the WIFI bottleneck.
-
To further brainstorm: in order to maintain outmost compatibility, running a RESTAPI for each /dev/ttyUSB<n> and this way all
/rr_upload|status|start|...
being fully compatible, and no need to introduce anotherdevice
reference as it would be implied via the port:- port 8080 -> /dev/ttyUSB0
- port 8081 -> /dev/ttyUSB1
- port 8100 -> /dev/ttyACM0
- port 8101 -> /dev/ttyACM1
for example.
-
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_reply
It'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 yet
Anyway, 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.
- upload file (POST):