Getting actual spindle speed from "M3 R1" Mcode
-
I am running the 3.4.5 release of RRF right now. I have a BLDC Spindle controlled by an ODrive Pro controller. I am controlling the ODrive using a DSF Python script. It works fantastically. My Python script is intercepting M3, M4, and M5 Mcodes and sending the ODrive commands over USB. I finally got around to implementing my pause.g and resume.g files. Pause.g is working great as it just calls M5 and moves out of the way. The problem I have is the M3 R1 in my resume.g script. My DSF Python script is getting sent the R1 parameter and I am not sure how to resolve that into the spindle speed prior to the M5 Mcode in the pause.g. Looking at the Object Module I would think that I need to resolve {state.restorePoints[0].spindleSpeeds[0]} to get the value but when I run this code:
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 .... # 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) )
I get:
Closing connection: Internal Server Exception Traceback (most recent call last): File "odrivespindle2.py", line 115, in start_intercept rpm = getRRFVar( '{state.restorePoints[0].spindleSpeeds[0]}', cde.channel ) File "odrivespindle2.py", line 74, in getRRFVar res = command_connection.perform_command(evaluate_expression( channel, dsf_variable )) File "/usr/local/lib/python3.7/dist-packages/dsf_python-3.4.5.post2-py3.7.egg/dsf/connections/base_connection.py", line 65, in perform_command command, response.error_type, response.error_message dsf.connections.exceptions.InternalServerException: Internal Server Exception
I will say that Python is not my strongest programming language so it is possible I just have a syntax error or something. It is also possible I am just being dumb.
-
@CthulhuLabs Some notes:
-
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.
-
Does
echo state.restorePoints[0].spindleSpeeds[0]
work from DWC? -
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?
-
-
- 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()
- 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^'
- 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?
-
@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 inresume.g
. Thespindles[]
root key is definitely available. -
@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.
-
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.
-
@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.
-
@dc42 ahhh I saw it in the object model:
that is a good alternative though. So I should do something like:
set global.PauseRPM = spindles[0].active
-
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.
-
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.
-
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.
-
That did it. @chrishamm @dc42 thank you for your help.
-
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()
-
@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 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.