Duet3D Logo Duet3D
    • Tags
    • Documentation
    • Order
    • Register
    • Login

    RRF RESTAPI on Linux instead of RRF microcontroller

    Scheduled Pinned Locked Moved
    Duet Web Control
    1
    3
    123
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • xyzdimsundefined
      xyzdims
      last edited by xyzdims

      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)
      • /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.

      xyzdimsundefined 1 Reply Last reply Reply Quote 0
      • xyzdimsundefined
        xyzdims @xyzdims
        last edited by xyzdims

        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 another device 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.

        xyzdimsundefined 1 Reply Last reply Reply Quote 0
        • xyzdimsundefined
          xyzdims @xyzdims
          last edited by xyzdims

          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.

          1 Reply Last reply Reply Quote 2
          • First post
            Last post
          Unless otherwise noted, all forum content is licensed under CC-BY-SA