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

    Getting actual spindle speed from "M3 R1" Mcode

    Scheduled Pinned Locked Moved
    DSF Development
    3
    15
    693
    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.
    • chrishammundefined
      chrishamm administrators @CthulhuLabs
      last edited by

      @CthulhuLabs Some notes:

      1. When you intercept a code like M3, you must flush the code channel to ensure that pending codes have been executed before you actually handle it. Otherwise, you'll see out-of-order execution. See the other examples from the dsf-python library.

      2. Does echo state.restorePoints[0].spindleSpeeds[0] work from DWC?

      3. It looks like there is already some handling for S parameter expressions in your code. If 2. works, does M3 S{state.restorePoints[0].spindleSpeeds[0]} work?

      Duet software engineer

      CthulhuLabsundefined 1 Reply Last reply Reply Quote 1
      • CthulhuLabsundefined
        CthulhuLabs @chrishamm
        last edited by

        @chrishamm

        1. I am flushing. I left out a good chunk of my code. Here is the full code:
        #!/usr/bin/env python3
        """
        Script for controlling an ODrive powered BDLC spindle
        """
        
        import subprocess
        import traceback
        
        from dsf.connections import InterceptConnection, InterceptionMode, CommandConnection
        from dsf.commands.code import CodeType
        from dsf.commands.generic import evaluate_expression
        from dsf.commands.code_channel import CodeChannel
        from dsf.object_model import MessageType
        
        import serial
        from time import sleep
        
        port = '/dev/ttyACM0'   # serial port
        baud = 115200           # baudrate
        timeout = 1             # read timeout
        
        
        # function to return the serial port or none if it fails
        def getSerial(portname,baud,to):
            try:
                return serial.Serial(port=portname,baudrate=baud,timeout=to)
            except:
                return None
        
        
        # function to set the ODrive rps (Rotations Per Second#)
        def setRPS(rps):
            attempt = True
            while attempt:
                ser = getSerial(port,baud,timeout)
                if (ser is not None):
                    # get the current state of the ODrive
                    ser.write(b'r axis0.current_state\n')
                    axisState = ser.readline().decode().strip()
        
                    # get the currently set velocity in RPS
                    ser.write(b'r axis0.controller.input_vel\n')
                    reqVel = ser.readline().decode().strip()
        
                    # check if ODrive is ideal and the desired rps is not 0
                    if (( axisState == "1" ) and ( rps != 0 )):
                        # if so set the ODrive state to 8 (CLOSED LOOP CONTROL)
                        ser.write(b'w axis0.requested_state 8\n')
                        ser.flush()
        
                    # check if ODrive is not ideal and the desired rps is 0
                    if (( axisState != "1" ) and ( rps == 0)):
                        # if so set the ODrive state to 1 (IDLE)
                        ser.write(b'w axis0.requested_state 1\n')
                        ser.flush()
        
                    # check if the current set velocity is not the desired rps
                    if ( float(reqVel) != rps ):
                        # if so set the velocity to rps
                        command = str.encode('w axis0.controller.input_vel %.6f\n' %rps )
                        ser.write(command)
                        ser.flush()
        
                    ser.close()
                    attempt = False
                else:
                    sleep(0.1)
        
        def getRRFVar( dsf_variable, channel):
            command_connection = CommandConnection(debug=True)
            command_connection.connect()
        
            try:
                res = command_connection.perform_command(evaluate_expression( channel, dsf_variable ))
                result = res.result
                print(f"Evaluated expression: {result}")
            finally:
                command_connection.close()
        
            return result
        
        
        def start_intercept():
            filters = ["M3","M4","M5"]
            intercept_connection = InterceptConnection(InterceptionMode.PRE, filters=filters, debug=True)
        
            while True:
                intercept_connection.connect()
        
                try:
                    while True:
                        # Wait for a code to arrive
                        cde = intercept_connection.receive_code()
        
                        # Check for the type of the code
                        if cde.type == CodeType.MCode and cde.majorNumber in [3,4,5]:
                            # --------------- BEGIN FLUSH ---------------------
                            # Flushing is only necessary if the action below needs to be in sync with the machine
                            # at this point in the GCode stream. Otherwise it can an should be skipped
        
                            # Flush the code's channel to be sure we are being in sync with the machine
                            success = intercept_connection.flush(cde.channel)
        
                            # Flushing failed so we need to cancel our code
                            if not success:
                                print("Flush failed")
                                intercept_connection.cancel_code()
                                continue
                            # -------------- END FLUSH ------------------------
        
                            # M3
                            if cde.majorNumber == 3 :
        
                                if cde.parameter("R") :
                                    rpm = getRRFVar( '{state.restorePoints[0].spindleSpeeds[0]}', cde.channel )
                                    setRPS( float(int(rpm) / 60) )
                                    intercept_connection.resolve_code(MessageType.Success, "ODrive RPM set to " + str(rpm) )
                                else :
                                    if cde.parameter("S") is None :
                                        # Let DCS know there was an ERROR
                                        intercept_connection.resolve_code(MessageType.Error, "M3 must include an S parameter")
                                    else :
                                        rpm = cde.parameter("S").string_value
                                        if cde.parameter("S").is_expression :
                                            rpm = getRRFVar( rpm, cde.channel )
                                        setRPS( float(int(rpm) / 60) )
                                        # Resolve it so that DCS knows we took care of it
                                        intercept_connection.resolve_code(MessageType.Success, "ODrive RPM set to " + str(rpm) )
        
                            # M4
                            if cde.majorNumber == 4:
                                if cde.parameter("S") is None :
                                    # Let DCS know there was an ERROR
                                    intercept_connection.resolve_code(MessageType.Error, "M4 must include an S parameter")
                                else :
                                    rpm = cde.parameter("S").string_value
                                    if cde.parameter("S").is_expression :
                                        rpm = getRRFVar( rpm, cde.channel )
                                    setRPS( -( float(int(rpm) / 60) ) )
                                    # Resolve it so that DCS knows we took care of it
                                    intercept_connection.resolve_code(MessageType.Success, "Ordive RPM set to -" + str(rpm) )
        
                            # M5
                            if cde.majorNumber == 5:
                                setRPS( 0 )
                                # Resolve it so that DCS knows we took care of it
                                intercept_connection.resolve_code(MessageType.Success, "ODrive RPM set to 0")
                        else:
                            # We did not handle it so we ignore it and it will be continued to be processed
                            intercept_connection.ignore_code()
                except Exception as e:
                    print("Closing connection: ", e)
                    traceback.print_exc()
                    intercept_connection.close()
        
                sleep(5)
        
        
        if __name__ == "__main__":
            start_intercept()
        
        1. I get this:
        echo state.restorePoints[0].spindleSpeeds[0]
        Error: Failed to evaluate "state.restorePoints[0].spindleSpeeds[0]": unknown value 'spindleSpeeds^'
        

        which I guess is the real issue here. I also tried:

        echo state.restorePoints[0].spindleSpeeds[1]
        Error: Failed to evaluate "state.restorePoints[0].spindleSpeeds[1]": unknown value 'spindleSpeeds^'
        
        echo state.restorePoints[1].spindleSpeeds[0]
        Error: Failed to evaluate "state.restorePoints[1].spindleSpeeds[0]": unknown value 'spindleSpeeds^'
        
        echo state.restorePoints[1].spindleSpeeds[1]
        Error: Failed to evaluate "state.restorePoints[1].spindleSpeeds[1]": unknown value 'spindleSpeeds^'
        

        I even tried with the the tool number:

        echo state.restorePoints[0].spindleSpeeds[251]
        Error: Failed to evaluate "state.restorePoints[0].spindleSpeeds[251]": unknown value 'spindleSpeeds^'
        
        1. I know that if I put:
        M3 S{state.restorePoints[0].spindleSpeeds[0]}
        

        in my resume.g file my ODrive control script would just get "{state.restorePoints[0].spindleSpeeds[0]}" as the string_value for cde.parameter("S") so I figured I would try doing this in my resume.g:

        var ResumeRPM = 0;
        set var.ResumeRPM = {state.restorePoints[0].spindleSpeeds[0]}
        
        ; turn the spindle back on
        M3 S{var.ResumeRPM}
        

        This way {state.restorePoints[0].spindleSpeeds[0]} would get resolved by RRF and not my code. Unfortunately {state.restorePoints[0].spindleSpeeds[0]} does not actually evaluate.

        So the question now is where is the my spindle speed getting save to? Could it have something to do with the fact that I am also intercepting the M5 Mcode so the save state for the spindle is not actually getting created?

        chrishammundefined 1 Reply Last reply Reply Quote 0
        • chrishammundefined
          chrishamm administrators @CthulhuLabs
          last edited by

          @CthulhuLabs It looks like state.restorePoints[].spindleSpeeds[] isn't available from RRF, so the evaluation (obviously) fails. I guess it was planned at some point when the spindle code was refactored but then it was discarded again without an update of the DSF/DWC OM definitions. @dc42 Any thoughts about this?

          I suggest you save the current spindle value in pause.g to a global variable and then restore it in resume.g. The spindles[] root key is definitely available.

          Duet software engineer

          CthulhuLabsundefined 1 Reply Last reply Reply Quote 1
          • CthulhuLabsundefined
            CthulhuLabs @chrishamm
            last edited by CthulhuLabs

            @chrishamm I switched:

            intercept_connection.resolve_code(MessageType.Success, "ODrive RPM set to 0")
            

            for the M5 command to:

            intercept_connection.ignore_code()
            

            and now the state.restorePoints[0].spindleSpeeds[0] is getting set. However when I try to resume my M3 code is getting executed but my spindle does not change speed. Then the entire tool path is skilled up until my next tool change. Let me get a full run's debug output for my script and ill post it.

            CthulhuLabsundefined 1 Reply Last reply Reply Quote 0
            • CthulhuLabsundefined
              CthulhuLabs @CthulhuLabs
              last edited by

              Sorry I was mistaken. My code is not getting executed. It is still throwing this error:

              Error: {state.restorePoints[0].spindleSpeeds[0]}unknown value 'spindleSpeeds^' of resume.g
              

              to the console and my code is NOT getting executed.

              dc42undefined 1 Reply Last reply Reply Quote 0
              • dc42undefined
                dc42 administrators @CthulhuLabs
                last edited by

                @CthulhuLabs currently the state saved in a restore point does not include the spindle speed(s). So I suggest that in pause.g you save the spindle speed in a global variable as @chrishamm suggested.

                Duet WiFi hardware designer and firmware engineer
                Please do not ask me for Duet support via PM or email, use the forum
                http://www.escher3d.com, https://miscsolutions.wordpress.com

                CthulhuLabsundefined 1 Reply Last reply Reply Quote 1
                • CthulhuLabsundefined
                  CthulhuLabs @dc42
                  last edited by

                  @dc42 ahhh I saw it in the object model:

                  https://github.com/Duet3D/RepRapFirmware/wiki/Object-Model-Documentation#staterestorepointsspindlespeeds

                  that is a good alternative though. So I should do something like:

                  set global.PauseRPM = spindles[0].active

                  CthulhuLabsundefined 1 Reply Last reply Reply Quote 0
                  • CthulhuLabsundefined
                    CthulhuLabs @CthulhuLabs
                    last edited by

                    Well "set global.PauseRPM = spindles[0].active" is evaluating to 0 so something is not right there. Also when I resume it is moving to the position it paused at which is good but it is not completing the tool path. Instead it is prompting me to change tools and continues on with the next tool path.

                    CthulhuLabsundefined 1 Reply Last reply Reply Quote 0
                    • CthulhuLabsundefined
                      CthulhuLabs @CthulhuLabs
                      last edited by CthulhuLabs

                      I commented out all the code in pause.g and resume.g related to the spindle and it is still skipping the rest of the tool path on resume.

                      CthulhuLabsundefined 1 Reply Last reply Reply Quote 0
                      • CthulhuLabsundefined
                        CthulhuLabs @CthulhuLabs
                        last edited by

                        I fixed the toolpath issue. My CAD software post processor was generating tool paths like this:

                        G1Z-1.016F304.8
                        X-163.747Y-122.162F1524.0
                        X-162.316Y-123.371
                        X-160.765Y-124.590
                        X-159.269Y-125.682
                        X-157.679Y-126.760
                        X-156.063Y-127.774
                        X-154.436Y-128.716
                        X-152.260Y-129.862
                        X-152.125Y-129.929
                        X-149.886Y-130.974
                        X-147.582Y-131.923
                        X-145.238Y-132.763
                        X-145.096Y-132.810
                        X-142.726Y-133.532
                        X-140.273Y-134.157
                        

                        I switched it to generate tool paths like this:

                        G1Z-1.016F304.8
                        G1X-163.747Y-122.162F1524.0
                        G1X-162.316Y-123.371
                        G1X-160.765Y-124.590
                        G1X-159.269Y-125.682
                        G1X-157.679Y-126.760
                        G1X-156.063Y-127.774
                        G1X-154.436Y-128.716
                        G1X-152.260Y-129.862
                        G1X-152.125Y-129.929
                        G1X-149.886Y-130.974
                        G1X-147.582Y-131.923
                        G1X-145.238Y-132.763
                        G1X-145.096Y-132.810
                        G1X-142.726Y-133.532
                        G1X-140.273Y-134.157
                        

                        I guess resume doesn't know how to pick up in the middle of a G1/0 command.

                        As for the M5 and M3 I think I am going to make my dsf-python script save the spindle speed to an internal LastRPM variable on M5 and restore that when it gets an M3 R1. Ill let you know if that works.

                        CthulhuLabsundefined dc42undefined 2 Replies Last reply Reply Quote 0
                        • CthulhuLabsundefined
                          CthulhuLabs @CthulhuLabs
                          last edited by

                          That did it. @chrishamm @dc42 thank you for your help.

                          CthulhuLabsundefined 1 Reply Last reply Reply Quote 1
                          • CthulhuLabsundefined
                            CthulhuLabs @CthulhuLabs
                            last edited by

                            Incase someone finds this thread and is looking to do something similar here is my dsf-python script for controlling my ODrive Spindle:

                            #!/usr/bin/env python3
                            """
                            Script for controlling an ODrive powered BDLC spindle
                            """
                            
                            import subprocess
                            import traceback
                            
                            from dsf.connections import InterceptConnection, InterceptionMode, CommandConnection
                            from dsf.commands.code import CodeType
                            from dsf.commands.generic import evaluate_expression
                            from dsf.commands.code_channel import CodeChannel
                            from dsf.object_model import MessageType
                            
                            import serial
                            from time import sleep
                            
                            port = '/dev/ttyACM0'   # serial port
                            baud = 115200           # baudrate
                            timeout = 1             # read timeout
                            LastRPS = 0             # What the spindle speed was in RPS before the last M5 was called
                            
                            
                            # function to return the serial port or none if it fails
                            def getSerial(portname,baud,to):
                                try:
                                    return serial.Serial(port=portname,baudrate=baud,timeout=to)
                                except:
                                    return None
                            
                            
                            # function to set the ODrive rps (Rotations Per Second#)
                            def setRPS(rps):
                                attempt = True
                                while attempt:
                                    ser = getSerial(port,baud,timeout)
                                    if (ser is not None):
                                        # get the current state of the ODrive
                                        ser.write(b'r axis0.current_state\n')
                                        axisState = ser.readline().decode().strip()
                            
                                        # get the currently set velocity in RPS
                                        ser.write(b'r axis0.controller.input_vel\n')
                                        reqVel = ser.readline().decode().strip()
                            
                                        # check if ODrive is ideal and the desired rps is not 0
                                        if (( axisState == "1" ) and ( rps != 0 )):
                                            # if so set the ODrive state to 8 (CLOSED LOOP CONTROL)
                                            ser.write(b'w axis0.requested_state 8\n')
                                            ser.flush()
                            
                                        # check if ODrive is not ideal and the desired rps is 0
                                        if (( axisState != "1" ) and ( rps == 0)):
                                            # if so set the ODrive state to 1 (IDLE)
                                            ser.write(b'w axis0.requested_state 1\n')
                                            ser.flush()
                            
                                        # check if the current set velocity is not the desired rps
                                        if ( float(reqVel) != rps ):
                                            # if so set the velocity to rps
                                            command = str.encode('w axis0.controller.input_vel %.6f\n' %rps )
                                            ser.write(command)
                                            ser.flush()
                            
                                        ser.close()
                                        attempt = False
                                    else:
                                        sleep(0.1)
                            
                            
                            # function to get the ODrive rps (Rotations Per Second#)
                            def getRPS():
                                attempt = True
                                rps = 0
                                while attempt:
                                    ser = getSerial(port,baud,timeout)
                                    if (ser is not None):
                                        # get the currently set velocity in RPS
                                        ser.write(b'r axis0.controller.input_vel\n')
                                        rps = float(ser.readline().decode().strip())
                            
                                        ser.close()
                                        attempt = False
                                    else:
                                        sleep(0.1)
                            
                                return rps
                            
                            
                            
                            def getRRFVar( dsf_variable, channel):
                                command_connection = CommandConnection(debug=True)
                                command_connection.connect()
                            
                                try:
                                    res = command_connection.perform_command(evaluate_expression( channel, dsf_variable ))
                                    result = res.result
                                    print(f"Evaluated expression: {result}")
                                finally:
                                    command_connection.close()
                            
                                return result
                            
                            
                            def start_intercept():
                                filters = ["M3","M4","M5"]
                                intercept_connection = InterceptConnection(InterceptionMode.PRE, filters=filters, debug=True)
                            
                                while True:
                                    intercept_connection.connect()
                            
                                    try:
                                        while True:
                                            # Wait for a code to arrive
                                            cde = intercept_connection.receive_code()
                            
                                            # Check for the type of the code
                                            if cde.type == CodeType.MCode and cde.majorNumber in [3,4,5]:
                                                # --------------- BEGIN FLUSH ---------------------
                                                # Flushing is only necessary if the action below needs to be in sync with the machine
                                                # at this point in the GCode stream. Otherwise it can an should be skipped
                            
                                                # Flush the code's channel to be sure we are being in sync with the machine
                                                success = intercept_connection.flush(cde.channel)
                            
                                                # Flushing failed so we need to cancel our code
                                                if not success:
                                                    print("Flush failed")
                                                    intercept_connection.cancel_code()
                                                    continue
                                                # -------------- END FLUSH ------------------------
                            
                                                # M3
                                                if cde.majorNumber == 3 :
                                                    if cde.parameter("R") :
                                                        if LastRPS < 0 :
                                                            # this is M3 LastRPS should be positve
                                                            setRPS( LastRPS * -1 )
                                                        else :
                                                            setRPS( LastRPS )
                                                        intercept_connection.resolve_code(MessageType.Success, "ODrive resuming RPM " + str( LastRPS * 60 ) )
                                                    else :
                                                        if cde.parameter("S") is None :
                                                            # Let DCS know there was an ERROR
                                                            intercept_connection.resolve_code(MessageType.Error, "M3 must include an S parameter")
                                                        else :
                                                            rpm = cde.parameter("S").string_value
                                                            if cde.parameter("S").is_expression :
                                                                rpm = getRRFVar( rpm, cde.channel )
                                                            setRPS( float(int(rpm) / 60) )
                                                            # Resolve it so that DCS knows we took care of it
                                                            intercept_connection.resolve_code(MessageType.Success, "ODrive RPM set to " + str(rpm) )
                            
                                                # M4
                                                if cde.majorNumber == 4:
                                                    if cde.parameter("R") :
                                                        if LastRPS > 0 :
                                                            # this is M4 LastRPS should be negative
                                                            setRPS( LastRPS * -1 )
                                                        else :
                                                            setRPS( LastRPS )
                                                        intercept_connection.resolve_code(MessageType.Success, "ODrive resuming RPM " + str( LastRPS * 60 ) )
                                                    else :
                                                        if cde.parameter("S") is None :
                                                            # Let DCS know there was an ERROR
                                                            intercept_connection.resolve_code(MessageType.Error, "M4 must include an S parameter")
                                                        else :
                                                            rpm = cde.parameter("S").string_value
                                                            if cde.parameter("S").is_expression :
                                                                rpm = getRRFVar( rpm, cde.channel )
                                                            setRPS( -( float(int(rpm) / 60) ) )
                                                            # Resolve it so that DCS knows we took care of it
                                                            intercept_connection.resolve_code(MessageType.Success, "Ordive RPM set to -" + str(rpm) )
                            
                                                # M5
                                                if cde.majorNumber == 5:
                                                    LastRPS = getRPS()
                                                    setRPS( 0 )
                                                    # Resolve it so that DCS knows we took care of it
                                                    intercept_connection.resolve_code(MessageType.Success, "ODrive RPM set to 0")
                                            else:
                                                # We did not handle it so we ignore it and it will be continued to be processed
                                                intercept_connection.ignore_code()
                                    except Exception as e:
                                        print("Closing connection: ", e)
                                        traceback.print_exc()
                                        intercept_connection.close()
                            
                                    sleep(5)
                            
                            
                            if __name__ == "__main__":
                                start_intercept()
                            
                            
                            1 Reply Last reply Reply Quote 0
                            • dc42undefined
                              dc42 administrators @CthulhuLabs
                              last edited by dc42

                              @CthulhuLabs said in Getting actual spindle speed from "M3 R1" Mcode:

                              I guess resume doesn't know how to pick up in the middle of a G1/0 command.

                              Yes, that will be the reason. When you pause the print, RRF may cancel some moves that are in the movement queue. When you resume, RRF rewinds the input file back to the offset in that file of the first movement command that was cancelled. However, RRF doesn't know the context of that move, for example whether it is following on from a G0, G1, G2, G3 or some other command.

                              I have raised https://github.com/Duet3D/RepRapFirmware/issues/871.

                              dc42 created this issue in Duet3D/RepRapFirmware

                              open Problem with pausing in CNC or Laser mode #871

                              Duet WiFi hardware designer and firmware engineer
                              Please do not ask me for Duet support via PM or email, use the forum
                              http://www.escher3d.com, https://miscsolutions.wordpress.com

                              CthulhuLabsundefined 1 Reply Last reply Reply Quote 0
                              • CthulhuLabsundefined
                                CthulhuLabs @dc42
                                last edited by

                                @dc42 Would you like me to send you the original GCode file I was using before I altered the post processor? Each tool path is just one giant G1 Gcode.

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