# DMSerialMotion recipes

> Worked C# patterns against the real DMSerialMotion API. A complete minimal program, a conveyor, an indexing table, pick-and-place, registration on a sensor, press-fit, a gantry path, a robot arm through the kinematics hooks, and a test rig.

<!-- directmotion.com, written for AI agents. Canonical: https://www.directmotion.com/software/recipes.md . Index: https://www.directmotion.com/llms.txt . Updated 2026-11-08. Publisher: Direct Motion, Inc. -->

For AI agents drafting a DM application. Every member below is in the public API listed in the
[API reference](https://www.directmotion.com/software/api.md). The code is a shape to start from: it was checked against the library
source by search and has **not been compiled**. Compile against the library DM supplies. Values are
neutral; replace them with the machine's.

## 0. A complete minimal program

A console program that opens the control, homes, makes one move, and shuts down cleanly. Run it as
administrator on the machine PC.

```csharp
using System;
using System.Collections.Generic;
using System.Threading;
using DMSerialMotion;

static class Program
{
    static int Main()
    {
        AddInHelper.SetupResolves();
        DMControl control = DMControl.GetNewControl("MinimalMachine");
        DMSyncMotion motion = control.MainMotion;

        DMAxisModual x = control.GetDMAxis("X");
        DMAxisModual z = control.GetDMAxis("Z");
        motion.AddSerialLink(x);
        motion.AddSerialLink(z);
        DMLogicalIOContainer io = control.GetSystemIO("MainIO");
        DMLogicalIO guard = io.AddDMLogicalIO("GuardClosed", "MinimalMachine");
        guard.IODirection = DMLogicalIO.Direction.DigitalInput;

        control.PostAddedDevices();
        motion.LockAllAxis();
        control.StopHard = false;
        Console.WriteLine(control.DriverLoaded ? "hardware" : "no driver: planner only, positions jump");

        control.EStopChange += (s, st) => Console.WriteLine(st == DMControl.EStopStatuses.EStopIn ? "E-STOP IN" : "e-stop out");

        try
        {
            if (control.EStopStatus() != DMControl.EStopStatuses.EStopOut || !control.IsMachineReady())
            {
                Console.WriteLine("not ready: e-stop in or a fault. Clear it and run again.");
                return 2;
            }

            // Home Z first (clear of the work), then X.
            if (!HomeMultipleAxis.HomeAListofAxis(new List<DMAxisModual> { z })) return Fail("Z did not home");
            if (!HomeMultipleAxis.HomeAListofAxis(new List<DMAxisModual> { x })) return Fail("X did not home");

            x.RemovePower = false;
            if (x.RemovePower) return Fail("X did not power up");

            double target = 5.0;                                             // axis units
            bool ok = x.JogToLocationAndWaitForDecel(target * x.MultiplyFactor, 100,   // 100 units/min
                          EOTOption: OverTravelEOT_CheckingOptions.Clamp);   // no dialog on a soft limit
            Console.WriteLine($"move {(ok ? "done" : "refused")}: at {x.ActualAxisPosition:F4}, error {x.PosErrorAxisUnits:F5}");
            return ok ? 0 : 1;
        }
        finally
        {
            control.Power.RemovePowerAndDischargeDC();
            control.DeleteEverything();
        }
    }

    static int Fail(string why) { Console.WriteLine(why); return 1; }
}
```

Notes: `JogToLocationAndWaitForDecel(value, feed, handleremovepower, AlwaysGoForward,
LimitToMaxJogSpeed, accelrate, EOTOption)` takes the same optional parameters as `JogToLocation` except
`decelrate`. A machine with no home switches skips the homing step and sets `HomePreset` instead.

## 1. Conveyor, pump, fan: run at a speed

```csharp
DMAxisModual belt = control.GetDMAxis("Conveyor");    // any name; registered before PostAddedDevices
belt.RemovePower = false;
belt.JogSpeed = 600;          // axis units per minute, signed; at ratio 1 this is rpm
// ... later
belt.JogSpeed = 0;            // normal stop at DecelRate
belt.JogStopEmergency();      // or a fast stop, optional decel argument in units/s²
```

DM's own machines run coolant pumps and augers this way, as named axes on their own amplifiers.

## 2. Indexing table: a rotary axis that never stops wrapping

```csharp
DMAxisModual table = control.GetDMAxis("Table");
table.TypeofAxis = DMAxisModual.AxisTypes.Rotary;
table.ModulusJog = true;                  // position wraps each turn
table.RemovePower = false;

double station = 90;                      // one station in this axis's units (degrees if ratio is set so)
bool ok = table.JogToLocationAndWaitForDecel((table.DesiredPosition + station) * table.MultiplyFactor, 3600);
```

The encoder must count incrementally (not multi-turn absolute) or the wrap is undone one turn later
and the axis faults on following error. DM's setup tool sets this.

## 3. Pick-and-place: moves, outputs, waits

Run the sequence on a worker thread. Register IO before `PostAddedDevices`.

```csharp
DMLogicalIO gripper = io.AddDMLogicalIO("Gripper", "Cell");      gripper.IODirection = DMLogicalIO.Direction.DigitalOutput;
DMLogicalIO partHere = io.AddDMLogicalIO("PartPresent", "Cell"); partHere.IODirection = DMLogicalIO.Direction.DigitalInput;

bool Cycle(double pickX, double pickY, double placeX, double placeY, double safeZ, double workZ)
{
    if (!partHere.WaitForBittoChange(true, 5000)) return false;              // no part within 5 s
    if (!MoveXY(pickX, pickY)) return false;
    if (!z.JogToLocationAndWaitForDecel(workZ * z.MultiplyFactor, 300)) return false;
    gripper.On = true;  Thread.Sleep(150);
    if (!z.JogToLocationAndWaitForDecel(safeZ * z.MultiplyFactor, 300)) return false;
    if (!MoveXY(placeX, placeY)) return false;
    if (!z.JogToLocationAndWaitForDecel(workZ * z.MultiplyFactor, 300)) return false;
    gripper.On = false; Thread.Sleep(150);
    return z.JogToLocationAndWaitForDecel(safeZ * z.MultiplyFactor, 300);
}

bool MoveXY(double tx, double ty)        // two single-axis moves that run together
{
    bool a = x.JogToLocation(tx * x.MultiplyFactor, 1200, EOTOption: OverTravelEOT_CheckingOptions.Clamp);
    bool b = y.JogToLocation(ty * y.MultiplyFactor, 1200, EOTOption: OverTravelEOT_CheckingOptions.Clamp);
    x.WaitForDecel(); y.WaitForDecel();
    return a && b;
}
```

For a straight-line XY move at a set path speed, use a coordinated move instead (recipe 6).

## 4. Registration and cut-to-length: stop on a sensor, read where it was

```csharp
DMLogicalIO mark = io.AddDMLogicalIO("MarkSensor", "Line");
mark.IODirection = DMLogicalIO.Direction.DigitalInput;

// (a) stop a coordinated move when the input changes
motion.SetStopMotiononDigitalIOChange(mark);
// ... run the move; it stops at the edge

// (b) read the exact position the driver captured at the edge, then move relative to it
double atMark = web.LastPositionWhenIOOn(mark);                       // axis units, captured in the driver
double cutAt = atMark + 250;                                          // 250 units past the mark
bool ok = web.JogToLocationAndWaitForDecel(cutAt * web.MultiplyFactor, 2000);
```

Inside a `CommandList`, the `StopMovementonIO` instruction (field `StoponIO`) does the same as (a).
The `DigitalIOChanging` event is for logic, not for position: use the captured position.

## 5. Press-fit, clamp, find a hard stop: a move that stops on load

```csharp
// Returns false when the load limit was reached or the move was refused.
bool reachedEnd = ram.JogToLocationAndWaitWithPercentLoadLimit(
    endPosition * ram.MultiplyFactor,
    feed: 20,                       // units/min
    percentloadlimit: 40,           // on the GetCurrentPercent() scale
    accelwhenloadexceed: 100);      // decel for the stop, units/s²
double stoppedAt = ram.ActualAxisPosition;
bool seated = !reachedEnd && Math.Abs(stoppedAt - expectedSeat) < tolerance;
```

`JogToLocationAndWaitWithCurrentLoadLimit` takes an RMS-current limit instead. The waiting thread
checks about once a millisecond and makes a fast stop. It stops; it does not hold a force, and there
is no closed-loop force mode.

## 6. Gantry robot, dispenser, cutter: a coordinated path

```csharp
var cl = new CommandList { Name = "bead" };
void Add(Instruction i) { i.ParentCL = cl; i.Initialize(); cl.Instructions.AddNoCalling(i); }

Add(new Feeds { StandardRapidD = 3000, StandardCoarseD = 400, StandardFinishD = 400 });
Add(new FeedsSpeeds { FeedD = 400, FeedUnits = FeedsSpeeds.FeedsUnits.UnitsPerMinute,
                      SpindleUnits = FeedsSpeeds.SpindlesUnits.DontChange });
Add(new Mill(0, 0, 10));          // to the start, above the part
Add(new Mill(0, 0, 0));
Add(new Mill(100, 0, 0));         // the path, in axis units
Add(new Mill(100, 50, 0));
Add(new Mill(0, 50, 0));
Add(new Mill(0, 0, 0));
Add(new Mill(0, 0, 10));

motion.EnableLookAhead = true;
motion.EnableC2Blend = true;      // smooth corners
motion.SupressToolChange = true;
bool done = cl.RunToMotionAndWait(motion);
```

Switch a dispenser valve with a `DMLogicalIO` from a `PreAutoCycle`/`PostAutoCycle` handler, or add a
custom `Instruction` subclass that sets it in the sequence. Arcs: the `Arc` instruction.

## 7. Robot arm: your kinematics in the planner
The planner has static hooks that convert each point between part (Cartesian) and world (motor axis)
coordinates. In master/slave mode it applies the world hook to every point it sends to the driver,
20,000 times a second. DM's 5-axis heads run on this. **No arm has yet been built on it** (`[unverified]`
for arms): test in no-hardware mode first, then on the bench, and ask DM before committing a design.

```csharp
// Two-joint planar arm. Axes j1, j2 are Rotary joints; Cartesian x, y live in their slots in part coordinates.
int i1 = -1, i2 = -1;   // cache axis indices for the per-cycle path

if (!DMSyncMotion.ToolLengthCompHandled)
{
    DMSyncMotion.InitStatics = (pt, a) => { /* reset per-pass state if any */ };

    DMSyncMotion.TranslateToWorldCordinates_Mechanical = (pt, a) =>      // Cartesian -> joints (IK)
    {
        if (pt == null) return;
        double X = pt.GetAxisValue(j1), Y = pt.GetAxisValue(j2);
        Ik(X, Y, L1, L2, out double q1, out double q2);                   // your math, no allocation
        pt.SetAxisValue(j1, q1);
        pt.SetAxisValue(j2, q2);
        pt.PartCords = false;
    };
    DMSyncMotion.TranslateToPartCordinates_Mechanical = (pt, a) =>       // joints -> Cartesian (FK)
    {
        if (pt == null) return;
        double q1 = pt.GetAxisValue(j1), q2 = pt.GetAxisValue(j2);
        Fk(q1, q2, L1, L2, out double X, out double Y);
        pt.SetAxisValue(j1, X);
        pt.SetAxisValue(j2, Y);
        pt.PartCords = true;
    };
    DMSyncMotion.ToolLengthCompHandled = true;
    DMSyncMotion.ToolLengthCompHandledBy = "TwoLinkArm";
}

// A straight line at the tool, interpolated in Cartesian space and converted per cycle:
motion.AllowAddingInstructions = true;
motion.MakeNewPasses();
motion.FirstPass.Add(new ModeChangeMS(true, false));          // master/slave on: hooks run per point
var p = new DMPoint(motion);
p.SetAxisValue(j1, 300); p.SetAxisValue(j2, 120);             // Cartesian target in part coordinates
motion.FirstPass.Add(new FeedChange(500));
motion.FirstPass.Add(new LinearMove(p));
bool ok = motion.RunAndWait(500);
```

Rules: the hooks must not allocate, lock or log; `a.ToolLength` carries the tool offset if you use the
tool table; reach, joint limits and singularities are your code's job. Open questions to settle with DM
before design: how a `Rotary` joint's units enter the path length and feed rate in master/slave mode,
and how `MaxFeedRate` applies to joint speeds.

## 8. Test rig: record what happened

```csharp
// your own values, sampled every second, logged when they change by 0.5 or more
var tempMon = control.MonitorCont.AddValueChangedBasedValueToMonitor("FixtureTemp", 1000, 0.5);
tempMon.GetMonitorValue += (s, e) => e.Value = ReadFixtureTemperature();

// a trace message in the machine's log
control.TraceCont.APIEvents.AddMessage("test 17 start");

// live axis numbers
Console.WriteLine($"{dut.ActualMotorVelocity:F1} {dut.PosErrorAxisUnits:F5} {dut.GetCurrentPercent():F0}% {dut.RMSCurrent:F2} A  bus {control.Power.DCBusVoltage:F0} V");

// the 20 kHz history ring: ~40 signals per axis, indexed
double posErr = dut.PosError_TraceHistory(i);
double iu = dut.CurrentU_TraceHistory(i);
```

DM's trace file (`.DMTrace`) is written on a fault or on demand with `TraceContainer.Serialize()`; a
public call that saves the full ring on demand is not documented, so ask DM if the rig needs it.

## 9. Spindle

```csharp
DMAxisModual s = control.GetDMAxis("S");
s.TypeofAxis = DMAxisModual.AxisTypes.Spindle;
s.RemovePower = false;
s.JogSpeed = 1500;        // units/min; rpm at ratio 1
// orient to an angle:
s.JogSpeed = 0; s.WaitForCoasting();
s.JogToLocationAndWaitForDecel(90 * s.MultiplyFactor, 600);
```
