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.