Add velocity-mode jogging (M700) - #1264
Open
meeloo wants to merge 8 commits into
Open
Conversation
Adds a second way to command motion: signed speed per axis, instead of a destination, so that an analogue input such as a joystick can drive the axes directly. M700 X25 Y-12 means "X at +25 mm/s, Y at -12 mm/s". Rather than a new execution path, JogController synthesises a stream of short constant-velocity moves and feeds them to movement system 0 through exactly the path a G1 takes (MovementState::raw -> GCodes::ReadMove -> DDARing::AddStandardMove). Reusing that path buys three things that a bespoke path would have had to reinvent: - lookahead blends consecutive chunks, so a steady stick gives steady motion and a change of direction produces a normal junction, not a stop-start; - DDA::InitStandardMove sets endSpeed = 0 until a following move exists, so the last move in the ring always plans to stop. A command stream that dies - cable pulled, host crashed, task starved - decelerates the machine under its normal limits instead of stopping it dead; - per-axis speed and acceleration limits, kinematics, bed compensation and tool offsets all keep working unchanged. Safety: - the axis letters present define the whole velocity vector, so an axis that is not mentioned is stopped and a truncated command cannot leave one running; - watchdog: no M700 within R ms (default 250) zeroes the velocity; - each chunk goes through Kinematics::LimitPosition with initialCoords set, so the whole line is checked. An axis that reaches its limit stops while the others keep their commanded speed, because the chunk still takes one chunk time to execute; - speed is clamped to M203 and to 2.a.chunkTime, which is the ceiling InitStandardMove imposes anyway - commanding more would silently not be obeyed rather than going faster; - starting a jog requires the same axes homed that a G1 would, refuses while printing, and takes the movement lock once to start from a known standstill; - jogging is cancelled by M112/M999 and by anything that waits for standstill on movement system 0, which would otherwise never be reached. Response to a stick movement is about D * P plus the ~50ms Move prepares ahead, so ~150-200ms at the defaults. See Developer-documentation for the trade-offs. Builds clean for Duet3_MB6HC (arm-gnu-toolchain 15.3.Rel1), no new warnings under -Wall -Werror. Untested on hardware. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0132JC621uq1434yhpzBNNgJ
The original 3 does not work, and the emulator says so precisely. A 20Hz M700 stream at 10mm/s with D3 collapses to 2.5mm/s at chunk boundaries and loses 7% of the commanded distance (711 steps against 767 over the same interval). D4 is clean; D5 is the default for margin, because a real machine has heaters, networking and the SD card competing for the same main loop that tops the queue up. The cause is not a blending failure, which is what the stutter looks like. It is starvation: JogController::Spin adds at most one chunk per pass because it waits for ms.segmentsLeft to fall back to zero, so the fill rate is a ping-pong between the GCode and Move tasks. When the ring runs down to a single move, lookahead correctly plans that move to stop at its end - the deceleration is right, there was just nothing queued behind it. This costs latency: about 300ms now rather than the 150-200ms the documentation claimed, which was wrong on both counts. The docs now say so, and record that the real lever is the one-chunk-per-pass handoff rather than the queue depth.
Four claims, all now measured under the emulator rather than argued: blending, the 2.a.P speed ceiling, decelerate-on-loss-of-input, and per-axis limit clamping. The ceiling turned out to be exactly right and linear in P - 20.08, 40.23 and 81.31 mm/s measured against 20, 40 and 80 predicted, all from a commanded 90mm/s. The limit clamping holds too: with M208 X0:5 and M700 X10 Y10, X stops at 5.000mm while Y continues to 10.000mm at an undisturbed 10.00mm/s, which is the subtlest of the four and the one most likely to have been wrong. Also notes what this does not prove: step timing comes from the TC model, and nothing here checks that a real TMC5160 would follow the pulses.
Defaults move from D5/P50 to D2/P20, measured on the emulator by timing from command injection to the step pins changing rate. D5 P50 -> 257ms D3 P20 -> 126ms D2 P20 -> 50ms D2 P15 -> 62ms D2 P10 -> 127ms D2/P20 is an optimum rather than a compromise. Below about 40ms of queued motion the latency stops following D*P and gets worse, because Move wants roughly MoveTiming::UsualMinimumPreparedTime queued before it will run moves - so shortening the chunk or the queue past that point makes jogging less responsive, not more. Doubling the command rate changed the result by 0.3ms, so the floor is in the firmware rather than in how fast a host can send. The earlier stutter that pushed the depth up to 5 was with 50ms chunks, where the ring holds far more time and the producer has correspondingly longer to fall behind; depth 2 with 20ms chunks measures clean over a 20Hz stream. Going below ~50ms means reducing the preparation window in MoveTiming, which also affects print moves and CAN expansion timing. Not done here.
The comment and docs claimed the sub-40ms floor was Move refusing to run until MoveTiming::UsualMinimumPreparedTime was queued. Measured, that is false. hypothesis test result UsualMinimumPreparedTime is the floor halved it, 50ms -> 25ms 50.3 -> 50.2 ms lookahead grace period M595 R0, and R0 P40 ~2 ms host command rate doubled to 10ms cadence 0.3 ms Also filled in the D=2 curve, which is cleaner than the numbers previously recorded: P=15 -> 44.6, P=20 -> 38.5, P=25 -> 67.2, P=30 -> 90.2 ms. Above the optimum latency tracks the queued time D*P as a FIFO should. Sizing each chunk to the requested speed (T >= v/2a, so a slow jog gets a short chunk) was implemented and measured before being dropped: 3->15mm/s went 39.5 -> 75.4 ms and 1->3mm/s went 79 -> 245 ms. Not in the tree; recorded so it is not tried again. The floor is real and its cause is still unknown. No behaviour change here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0132JC621uq1434yhpzBNNgJ
RepRap::GetStatusIndex treats any live movement as busy. Jogging is continuous by nature, so M700 pinned the status at busy for as long as the operator held the stick - and DWC and AxisControl grey their controls out when the machine is busy, including the controls that send the jog commands. Reported from a real AxisControl session against the emulator: the jog panel disabled itself on every speed update. Jog motion no longer counts towards busy. The machine is manually controlled and accepting commands, which is what idle means to a client. Measured on the emulator, state.status sampled during a jog: before idle busy busy busy after idle idle idle idle A normal G1 still reports busy, so only the jog case changed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0132JC621uq1434yhpzBNNgJ
|
All contributors have signed the CLA ✍️ ✅ |
Contributor
|
Please discuss this feature on the Duet3D forum first. We try not to clash with existing allocated gcodes as much as possible even if they are not allocated in RRF. for example M700: |
Author
|
I have read the Duet3D CLA v2.0 and I hereby sign it |
Author
|
Thanks — raised on the forum as asked: https://forum.duet3d.com/topic/39395/velocity-mode-jogging-joystick-control Happy to take whatever number (or sub-code under an M-code RRF already owns) you would prefer, and I will renumber this branch. Leaving the PR open for the code, but the numbering decision belongs in that thread. |
GenerateChunk advanced ms.currentUserPosition before the distance test and the sub-threshold path returned without putting it back. SetMoveBufferDefaults seeds initialCoords from raw.coords, so the rejected target became the next chunk's baseline and the error compounded instead of recovering. Commanding below MinChunkDistance/chunkTime (0.05 mm/s at the default P=20) made M114 and the object model climb at the commanded speed while the machine stood still. Measured with 40 x "M700 X0.01": before X:3.447 Count 0 0 0 0 <- 3.4mm claimed, not one step taken after X:0.000 Count 0 0 0 0 Fixed at both ends: the target is restored on the reject path, and ClampSpeeds zeroes a speed too low to express in one chunk. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0132JC621uq1434yhpzBNNgJ
meeloo
force-pushed
the
upstream/velocity-jog-m700
branch
from
August 23, 2026 10:42
00a735b to
6262de7
Compare
JogController::Spin only stood aside for IsReallyPrintingOrResuming. A macro or a tool change moves axes on its own account, so jogging carried on underneath it and the two fought over the same movement system. DoingFileMacro deliberately excludes daemon.g (GCodes.cpp:374), so a daemon on its normal cycle does not chop the jog stream up - which is what made this safe to add. Deliberately NOT included: WaitingForAcknowledgement. 'Jog to the workpiece corner, then press OK' is a standard CNC setup pattern; the machine is stationary and the operator is at the controls, so blocking it would remove a useful workflow for no safety gain. No regression: 25 x 'M700 X5' gives 536 step edges before and after. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0132JC621uq1434yhpzBNNgJ
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Normal motion commands say where to go.
M700says how fast to go, and in which direction, per axis, so an analogue input such as a joystick can drive the machine directly. The motivating case is a gamepad jogging a CNC.Axis letters give signed speed in mm/sec (degrees/sec for rotational axes). The axis letters present define the whole velocity vector - any axis not mentioned is set to zero, so a truncated or partially-parsed command cannot leave an axis running.
S0stops.Pis chunk time (10-200ms, default 20),Ra watchdog timeout (default 250ms),Dthe queued-move depth (2-8, default 2). No parameters reports status.How it works.
JogControllergenerates short constant-velocity chunks into movement system 0 and tops the queue up fromGCodes::Spin. It goes through the normalSetMoveBufferDefaults/ToolOffsetTransform/LimitPositionpath, so tool offsets,M208limits and coordinate transforms all apply unchanged.Safety properties, each of which is deliberate:
IsReallyPrintingOrResuming), and is stopped byResetand byLockMovementSystemAndWaitForStandstill;2*a*P.DDA::InitStandardMovecaps entry speed atsqrt(2*a*d)so any move can be the last one queued and still stop within itself; withd = v*Pthat solves tov <= 2*a*P. Commanding more would not go faster, it would quietly not be obeyed, soM700clamps and reports the real ceiling instead.Latency. Measured on an emulator, from command injection to the step pins changing rate: 38.5ms at the default
D2 P20. Above ~40ms of queued motion latency tracksD x Pas a FIFO should (P25-> 67ms,P30-> 90ms); below it, it degrades, andP10cannot sustain 15mm/s at all. The defaults are therefore a measured optimum rather than a guess. What the floor is not, all measured: theMoveTimingpreparation window (halving it moved 0.1ms), the lookahead grace period (~2ms), and the host command rate (0.3ms).One behaviour change outside
JogController: jog motion no longer counts towardsbusyinRepRap::GetStatusIndex. Jogging is continuous, so it pinned the status at busy for as long as the stick was held, and DWC and AxisControl grey their controls out when busy - including the controls sending the jog commands. OrdinaryG1moves still report busy.Documented in
Developer-documentation/Velocity jogging (M700).md.Please treat the command number as a proposal. M700 was free on this branch but you may want it elsewhere; happy to renumber