@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.
Posts made by CthulhuLabs
-
RE: Getting actual spindle speed from "M3 R1" Mcode
-
RE: Getting actual spindle speed from "M3 R1" Mcode
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()
-
RE: Getting actual spindle speed from "M3 R1" Mcode
That did it. @chrishamm @dc42 thank you for your help.
-
RE: Getting actual spindle speed from "M3 R1" Mcode
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.
-
RE: Getting actual spindle speed from "M3 R1" Mcode
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.
-
RE: Getting actual spindle speed from "M3 R1" Mcode
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.
-
RE: Getting actual spindle speed from "M3 R1" Mcode
@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
-
RE: Getting actual spindle speed from "M3 R1" Mcode
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.
-
RE: Getting actual spindle speed from "M3 R1" Mcode
@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.
-
RE: Getting actual spindle speed from "M3 R1" Mcode
- 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?
-
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.
-
RE: Feasibility of running DSF on X86
I just got an Adafruit FT232H Breakout board. I am realizing that testing this is not as easy as I first though. Most of the simple microcontroller firmwares like Arduino / Circuit Python lack direct support for SPI Slave mode on the newer microcontrollers. They all seem to think that people only want to use their microcontroller as the SPI Master. That said it looks like the original AVR based Arduino's have good documentation on how to initialize SPI Slave through the SPI Registers. I have an Arduino MEGA from my original RAMPS 1.4 controller from 2014. Hopefully it is still working and I can get the SPI Bus speed up to 8Mhz in Slave mode.
My plan is to have the FT232H enable the CS and then transmit to the MEGA the current timestamp with as high of accuracy as I can get. It will then disable the CS. It will then wait for the MEGA to set its interrupt pin high. The MEGA will sleep for 1ms and then set its interrupt pin high. The FT232H will then enable the CS and read from the MEGA the last timestamp. It will then disable the CS and finally print on the CLI the current timestamp, the timestamp it received from the MEGA, and the difference minus 1ms (accounting for the MEGA's programmed delay). This should be the latency. I will have this whole process running in a loop. Depending on how much latency I am seeing, Ill probably have it loop like this 10 times a second. Then I will run this while doing other things on the USB bus, (moving a mouse, typing, accessing a USB2 thumb drive, accessing a USB3 thumb drive, etc.) Ill also test this with the FT232H going through a hub as well as being directly connected to the motherboards USB host controller. Ill take the output and generate some nice pretty graphs.
@chrishamm & @T3P3Tony does this sound like a valid test? Any recommendations on things to change?
-
RE: Feasibility of running DSF on X86
@chrishamm Looks like people are claiming the latency is 1ms:
https://forums.adafruit.com/viewtopic.php?f=19&t=93211
There is also a latency timer built into the chip that FTDI's techs say can be set as low as 1ms. Not internally sure what that does.
I'll order one and play around with it to see what kind of results I can get.
-
RE: Feasibility of running DSF on X86
@chrishamm what kind of latency / jitter in the SPI communication is acceptable for DSF to work properly?
-
RE: Feasibility of running DSF on X86
@o_lampe Mostly customization. For instance I have a python script that overrides the M3,M4, and M5 commands to control my external spindle with closed loop control. You have to be running in SBC mode for that to work.
-
RE: Feasibility of running DSF on X86
@T3P3Tony That is a good point. A user moving their mouse could potentially generate thousands of USB interrupts. I was wondering why you guys didnt make the SBC mode use USB directly. This is why I asked.
How tight do the timings need to be? Shouldnt be too hard to test with an arduino. Simply write a sketch that echo's input on the arduino's SPI back to PC SPI. Then send a "ping" to the arduino at a regular interval and log how long the response took to come back. While that is running do a bunch of USB intensive stuff.
-
RE: Feasibility of running DSF on X86
@Falcounet x86 SBCs are just as expensive as ARM ones from what I am seeing.
What is not expensive is the computer you already have. For instance I recently upgraded my desktop. I turned my old PC into a Linux file server. It has an 8th gen Intel, 32gigs of RAM, an NVMe SSD, and several large drives. 99% of the time it is doing nothing. Running DSF on it should be perfectly fine.
If DSF on x86 becomes a thing, I could see people containerizing DSF in say Docker and running a print farm off a single server.
-
Feasibility of running DSF on X86
As many know Raspberry Pi 4s (and other SBCs for that matter) have gotten ridiculously expensive for what they are, which makes running a Duet in SBC mode costly. That said there are a number of benefits of running in SBC mode in terms of advanced configurations / customizations. This got me thinking a few weeks ago about running DSF on an x86 Linux server. Like many people I have an older PC laying around that should be able to run circles around a RPI4 in terms of performance and with tweaks to the power profiles actually doesn't draw that much power.
Now there are two main issues with running DSF on X86 that I see.
-
DSF is written for ARM so it would need to be ported.
-
The lack of GPIO and SPI on most x86 boards. I do know of some boards that do have GPIO pins, but I dont think I have see any with SPI pins broken out. I know many of them use SPI for things like sensors and the like, but that is all internal.
I know DSF is written in .NET which gives me hope that getting it to run on x86 might be as simple as a recompile. I have not actually dug into the code though. I don't know if there is any ARM specific stuff.
As for the lack of SPI / GPIO I was contemplating what it would take to use a USB to SPI/GPIO board. For instance there is this guy from Adafruit:
https://www.adafruit.com/product/2264
that can break out SPI and GPIO pins and has Linux drivers. It is a very low cost option and seems like it could fit the bill.
There are other options as well such as using a RPI Zero as a GPIO expander:
https://magpi.raspberrypi.com/articles/pi-zero-gpio-expander
This one is less desirable as RPI Zeros are just as hard to find as RPI4s.
I am pretty positive though that these devices do not break out the GPIO / SPI interfaces in the same way a RPI4 and other ARM SBCs do. I would need to purchase one and check in /dev to see. As such this will likely require code changes if DSF rather than just config.json changes to support these devices. I am also not sure if the SPI interface on these is fast enough.
Before I attempt to go down this path I was wondering a few things:
- Have I missed anything crucial that would completely prevent this from working?
- How much interest is there from the community?
- Would anyone be interested in helping?
-
-
Split Firmware Version In ObjectModel
Please add a way to get the Major, Minor, and Point info on a boards firmware version in the object model. I am in the process of updating my Surfacing Macro ( https://forum.duet3d.com/topic/26762/surfacing-macro ) to take advantage of 3.5.0's new M291 options for user input. I want to be able to check the firmware version to make sure the Macro will work properly and if not provide feedback to the user. The issue is that:
echo {boards[0].firmwareVersion} ; currently returns "3.4.5"
returns the version number as a string like "3.4.5" (I have not update to the beta yet). Without basic string functions there is no way to split that apart and check if the version is equal to or greater than "3.5.0". If however there was:
echo {boards[0].firmwareVersion} ; returns "3.4.5" echo {boards[0].firmwareVersion.major} ; returns "3" echo {boards[0].firmwareVersion.minor} ; returns "4" echo {boards[0].firmwareVersion.point} ; returns "5"
I could then do something like:
if boards[0].firmwareVersion.major == 3 && boards[0].firmwareVersion.minor >= 5 ; Do all the things ... else M291 P"Sorry but your firmware version is not compatible with this Macro" R"Error" S2
-
RE: Surfacing Macro
So I am working on a new version of this Macro that will prompt for user input with the M291 command. It will make the Macro far more interactive for things like DepthOfCut, RPM, FeedRate, etc. One of the prompts I want to add is to ask if it should work in Pass mode or Depth mode.
In pass mode it will prompt you for the number of passes to take. It will then do that many passes before prompting you whether to surface another layer. Default will obviously be 1.
In Depth mode it will prompt you for how much material to remove. So if your DepthOfCut is say 0.3mm and you enter a depth of 1mm the Macro will do three passes at 0.3mm and then a fourth pass at 0.1mm for a total of 1mm.