The RMP Motion Controller APIs
RapidSequencerAPI.cs
1using System;
2using System.IO;
3using System.Threading;
4using System.Threading.Tasks;
5using NUnit.Framework;
6using RSI.RapidCode.dotNET;
7using System.Diagnostics;
8using System.Linq;
9
10
11 //[TestFixture(RSI.RapidSequencer.Platform.Windows, 200)]
12 [TestFixture(RSI.RapidSequencer.Platform.INtime, 200)]
13 [Category("Software")]
14 class RapidSequencerAPIExample : SampleAppTestBase
15 {
16#if X64
17 static string samplesDirectory = TestContext.CurrentContext.TestDirectory + @"\examples\rapidsequencer\";
18#else
19 static string samplesDirectory = TestContext.CurrentContext.TestDirectory + @"\..\examples\rapidsequencer\"; // x86
20#endif
21
22 string multipleTasksSampleFile = samplesDirectory + "MultipleTasks.sq";
23
24 int GLOBAL_TEST_LOOPS;// = 200;
25
26 public String _nodeName;// = "NodeA";
27 public String _rmpNodeName;// = "NodeA";
28 public RSI.RapidSequencer.Platform _platform;// = RapidSequencer.Platform.Windows;
29
30 public String workingDir = TestContext.CurrentContext.TestDirectory;
31
32 public int port;
33 public ulong timeoutMs;
34
35 private void ShutdownSequencers()
36 {
37 RSI.RapidSequencer.API.KillAll();
38 //var sequencers = RSI.RapidSequencer.RapidSequencerFactory.Discover(RSI.RapidSequencer.DiscoveryType.Local_All);
39 //foreach (var sequencer in sequencers)
40 //{
41 // sequencer.Shutdown();
42 //}
43 }
44
45 string runShell(string cmd, string cmdArgs)
46 {
47 Process cliProcess = new Process()
48 {
49 StartInfo = new ProcessStartInfo(cmd, cmdArgs)
50 {
51 UseShellExecute = false,
52 RedirectStandardOutput = true
53 }
54 };
55 cliProcess.Start();
56 string cliOut = cliProcess.StandardOutput.ReadToEnd();
57 cliProcess.WaitForExit();
58 cliProcess.Close();
59
60 return cliOut;
61 }
62
63 bool hasValipIP(string ifconfigOutput)
64 {
65 string[] invalidIPs = { "0.0.0.0", "127.0.0.1" };
66 System.Text.RegularExpressions.Regex ipv4Regex = new System.Text.RegularExpressions.Regex(@"inet\s+\b(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\b");
67
68 System.Text.RegularExpressions.Match ipv4Match = ipv4Regex.Match(ifconfigOutput);
69
70 while (ipv4Match.Success)
71 {
72 string ipv4String = ipv4Match.Groups[1].Value;
73 if (!invalidIPs.Contains(ipv4String))
74 {
75 return true;
76 }
77
78 ipv4Match = ipv4Match.NextMatch();
79 }
80 return false;
81 }
82
83 void ConfigureINtimeNetworkStack()
84 {
85 string intime_base = Environment.ExpandEnvironmentVariables("%intime%");
86 string piperta = intime_base + "bin\\piperta";
87 string ifconfig = "-node " + this._nodeName + " \"" + intime_base + "network7\\ifconfig\"";
88 string netload = "-node " + this._nodeName + " \"" + intime_base + "network7\\netload\"";
89
90 string ifconfigOut = runShell(piperta, ifconfig);
91 bool validIP = hasValipIP(ifconfigOut);
92 if (validIP)
93 {
94 return;
95 }
96
97 runShell(piperta, netload);
98
99 ifconfigOut = runShell(piperta, ifconfig);
100
101 validIP = hasValipIP(ifconfigOut);
102
103 Assert.IsTrue(validIP, "ifconfig did not return a valid IP address (even after netload)!");
104 }
105
106 [OneTimeSetUp]
107 public void SetupFixture()
108 {
109 if (this._platform == RSI.RapidSequencer.Platform.INtime)
110 {
111 ConfigureINtimeNetworkStack();
112 }
113 ShutdownSequencers();
114
115 if (controller != null)
116 {
117 controller.InterruptEnableSet(false);
118 controller.Delete();
119 }
120 controller = MotionController.CreateFromSoftware(TestContext.CurrentContext.TestDirectory);
121 int frameSendReceiveMicroseconds = 0; // no need for the firmware to wait for EtherCAT frames to send/receive since this is a software test
122 controller.MemorySet(controller.AddressGet(RSIControllerAddressType.RSIControllerAddressTypeFRAME_SEND_RECEIVE_MICROSECONDS), frameSendReceiveMicroseconds);
123
124 for (int index = 0; index < controller.AxisCountGet(); index++)
125 {
126 AxisSetup(controller.AxisGet(index));
127 }
128 }
129
135 private void FastWaitForDone(Axis axis)
136 {
137 const int MAX_LOOPS = 100;
138 int loops = 0;
139 while (!axis.MotionDoneGet())
140 {
141 Thread.Sleep(1);
142 loops++;
143 if (loops > MAX_LOOPS)
144 Assert.Fail("Failed waiting for motion done (fast) on Axis " + axis.NumberGet());
145 }
146 }
147
148 public static UInt64 DedidcatedInputAddressGet(Axis axis)
149 {
150 // Be sure to thank Shota for the great~ helper method
151 var dedicatedInPointerAddress = axis.AddressGet(RSIAxisAddressType.RSIAxisAddressTypeDEDICATED_INPUTS_POINTER);
152 var dedicatedInPointer = axis.rsiControl.MemoryGet(dedicatedInPointerAddress);
153 var dedicatedInAddress = axis.rsiControl.HostAddressGet((uint)dedicatedInPointer);
154 return dedicatedInAddress;
155 }
156
157 public void AxisSetup(Axis axis)
158 {
159 while (axis.ErrorLogCountGet() > 0)
160 {
161 axis.ErrorLogClear();
162 }
163 axis.ThrowExceptions(true);
164
165 const double USER_UNITS = 1000.0;
166 axis.UserUnitsSet(USER_UNITS);
167
168 axis.InterruptEnableSet(false);
169
170 const double ACCELERATION = 1000000;
171 axis.EStopDecelerationSet(ACCELERATION);
172
173 axis.MotorTypeSet(RSIMotorType.RSIMotorTypePHANTOM);
174 axis.AmpFaultActionSet(RSIAction.RSIActionNONE);
175 axis.HardwareNegLimitActionSet(RSIAction.RSIActionNONE);
176 axis.HardwareNegLimitTriggerStateSet(true); // default
177 axis.HardwarePosLimitActionSet(RSIAction.RSIActionNONE);
178 axis.HardwarePosLimitTriggerStateSet(true); // default
179 axis.ErrorLimitActionSet(RSIAction.RSIActionNONE);
180
181 const double COARSE_POSITION_PHANTOM = Double.MaxValue / 10;
182 axis.PositionToleranceCoarseSet(COARSE_POSITION_PHANTOM);
183
184 const double FINE_POSITION_PHANTOM = Double.MaxValue / 10;
185 axis.PositionToleranceFineSet(FINE_POSITION_PHANTOM);
186 ulong dedicatedInAddress = DedidcatedInputAddressGet(axis);
187 axis.rsiControl.MemorySet(dedicatedInAddress, 0); // clear all dedicated inputs (this is the default/startup state for phantoms)
188
189 // clear faults, enable, etc
190 axis.Abort();
191 FastWaitForDone(axis);
192 axis.ClearFaults();
193 Assert.That(axis.StateGet(), Is.EqualTo(RSIState.RSIStateIDLE));
194 axis.PositionSet(0);
195 axis.CommandPositionSet(0);
196 axis.CommandPositionDirectSet(0);
197 axis.MotionCamRepeatStop();
198 axis.DefaultVelocitySet(Constants.VELOCITY);
199 axis.DefaultAccelerationSet(Constants.ACCELERATION);
200 axis.DefaultDecelerationSet(Constants.ACCELERATION);
201 axis.DefaultJerkPercentSet(Constants.JERK_PERCENT);
202 axis.BacklashWidthSet(0.0);
203 axis.BacklashRateSet(0.0);
204
205 //reset gearing
206 axis.GearingDisable();
207
208 axis.FilterAlgorithmSet(RSIFilterAlgorithm.RSIFilterAlgorithmPID); // default
209
210 // restore default attr mask
211 axis.MotionAttributeMaskDefaultSet();
212 axis.MotionHoldTypeSet(RSIMotionHoldType.RSIMotionHoldTypeNONE);
213
214 Assert.That(axis.StateGet(), Is.EqualTo(RSIState.RSIStateIDLE), "Axis " + axis.NumberGet() + " could not clear faults in AxisSetup. Source is: " + axis.SourceNameGet(axis.SourceGet()));
215 Assert.That(axis.CommandPositionGet(), Is.EqualTo(0), "Axis " + axis.NumberGet() + " command position is not zero.");
216
217 axis.FeedRateSet(1.0); // restore default FeedRate
218
219 // restore defaults
220 axis.EStopDecelerationSet(10000.0);
221 axis.EStopJerkPercentSet(50.0);
222 axis.TriggeredModifyDecelerationSet(10000.0);
223 axis.TriggeredModifyJerkPercentSet(50.0);
224 axis.SoftwareNegLimitActionSet(RSIAction.RSIActionE_STOP);
225 axis.SoftwarePosLimitActionSet(RSIAction.RSIActionE_STOP);
226 axis.SoftwareNegLimitTriggerValueSet(-1125899906842620); // this is what shows as default in RapidSetup
227 axis.SoftwarePosLimitTriggerValueSet(1125899906842620); // this is what shows as default in RapidSetup
228 axis.ErrorLimitTriggerValueSet(1000);
229
230 axis.FilterDualLoopSet((Axis)axis, RSIMotorFeedback.RSIMotorFeedbackPRIMARY, false); // setting to false should restore to default
231
232 axis.HomeOffsetSet(0.0); // restoring this to zero shaved 11 seconds off Axis tests!!!
233
234 axis.ClearFaults();
235 axis.AmpEnableSet(true);
236 }
237
238 [OneTimeTearDown]
239 public void TeardownFixture()
240 {
241 ShutdownSequencers();
242 }
243
244 [TearDown]
245 public void TearDownSequencers()
246 {
247 ShutdownSequencers();
248 }
249
250 public RapidSequencerAPIExample(RSI.RapidSequencer.Platform platform, int loops)
251
252 {
253 this._platform = platform;
254
255 this._nodeName = "NodeB";
256 this._rmpNodeName = "NodeA";
257
258 GLOBAL_TEST_LOOPS = loops;
259
260 port = 50051;
261 timeoutMs = 20000;
262 }
263
264
265 [Test]
266 public void TwoTasksSimultaneously()
267 {
268 Assert.That(this._platform, Is.EqualTo(RSI.RapidSequencer.Platform.INtime), "Timing tests are only valid on real-time operating systems!");
269
271 // Create two RapidSequencer instances
272 RSI.RapidSequencer.RapidSequencer sequencer1 = RSI.RapidSequencer.RapidSequencerFactory.Create(this._platform, _nodeName, _rmpNodeName, workingDir, port, $"Sequencer-{port}", timeoutMs);
273 RSI.RapidSequencer.RapidSequencer sequencer2 = RSI.RapidSequencer.RapidSequencerFactory.Create(this._platform, _nodeName, _rmpNodeName, workingDir, port + 1, $"Sequencer-{port + 1}", timeoutMs);
274
275 // Call EngineStart() on both sequencers before performing any other actions.
276 string rmpPath = TestContext.CurrentContext.TestDirectory;
277 sequencer1.EngineStart(this._rmpNodeName, rmpPath);
278 sequencer2.EngineStart(this._rmpNodeName, rmpPath);
279
280 // Compile the same RapidScript program, but with different entry points.
281 sequencer1.Compile(multipleTasksSampleFile, "RunEverySample");
282 sequencer2.Compile(multipleTasksSampleFile, "RunEvery50Samples");
283
284 // Start both tasks and run them asynchronously.
285 string runEverySampleTaskId = sequencer1.RunAsync();
286 string runEvery50SamplesTaskId = sequencer2.RunAsync();
287
288 // Wait a little. These tasks run forever. Dont want to wait for them to finish.
289 Thread.Sleep(1000);
290
291 for (int i = 0; i < GLOBAL_TEST_LOOPS; i++)
292 {
293 // Get the values of the global variables.
294 RSI.RapidSequencer.SequencerGlobal loopCout = sequencer1.GlobalVariableGet("loopCounterDelta");
295 RSI.RapidSequencer.SequencerGlobal loopCout50 = sequencer2.GlobalVariableGet("loopCounterDelta50");
296
297 // Perform some checks on the values of the global variables.
298 // These asserts are just for demonstration and testing purposes, and are not be necessary in an actual application.
299 Assert.That(loopCout.Type, Is.EqualTo(RSI.RapidSequencer.SequencerGlobal.DataType.Int32), "Loop counter for every sample must be int32");
300 Assert.That(loopCout.Value, Is.EqualTo(1), "This must be 1 if the program is running every sample. Failed on loop " + i);
301 Assert.That(loopCout50.Type, Is.EqualTo(RSI.RapidSequencer.SequencerGlobal.DataType.Int32), "Loop counter for every 50 samples must be int32");
302 Assert.That(loopCout50.Value, Is.EqualTo(50), "This must be 50 if the program is running every 50 samples. Failed on loop " + i);
303
304 Thread.Sleep(2); // wait a couple samples so the values are updated
305 }
306
307 // Force the tasks to stop
308 sequencer1.TaskStop(runEverySampleTaskId);
309 sequencer2.TaskStop(runEvery50SamplesTaskId);
311 }
312
313
314 private void RunFunctionAndTestGlobal(string functionName, string globalName, int expectedValue)
315 {
316 Assert.That(this._platform, Is.EqualTo(RSI.RapidSequencer.Platform.INtime), "Timing tests are only valid on real-time operating systems!");
317
318 RSI.RapidSequencer.RapidSequencer sequencer = RSI.RapidSequencer.RapidSequencerFactory.Create(this._platform, _nodeName, _rmpNodeName, workingDir, port, "RapidSequencer", timeoutMs);
319
320 string rmpPath = TestContext.CurrentContext.TestDirectory;
321 sequencer.EngineStart(this._rmpNodeName, rmpPath);
322
323 sequencer.Compile(multipleTasksSampleFile, functionName);
324
325 string taskId = sequencer.RunAsync();
326
327 Thread.Sleep(1000); // wait a litte for it to get running
328
329 NUnit.Framework.Constraints.IResolveConstraint constraint;
330 if (this._platform == RSI.RapidSequencer.Platform.INtime)
331 {
332 constraint = Is.EqualTo(expectedValue);
333 }
334 else
335 {
336 constraint = Is.GreaterThanOrEqualTo(expectedValue);
337 }
338
339 for (int i = 0; i < GLOBAL_TEST_LOOPS; i++)
340 {
341 RSI.RapidSequencer.SequencerGlobal globalValue = sequencer.GlobalVariableGet(globalName);
342 Assert.That(globalValue.Type, Is.EqualTo(RSI.RapidSequencer.SequencerGlobal.DataType.Int32), "Global value must be int32");
343 Assert.That(globalValue.Value, constraint, "This must be " + expectedValue + " if the program is running every " + expectedValue + " samples. Failed on loop #" + i);
344 Thread.Sleep(1); // wait at least one sample so the values are updated
345 }
346
347 sequencer.TaskStop(taskId);
348 }
349
350 [Test]
351 public void RunEverySample()
352 {
353 RunFunctionAndTestGlobal("RunEverySample", "loopCounterDelta", 1);
354 }
355
356 [Test]
357 public void RunEvery50Samples()
358 {
359 RunFunctionAndTestGlobal("RunEvery50Samples", "loopCounterDelta50", 50);
360 }
361
362
363 //@[GlobalDataExample]
364 [Test]
365 public void BasicGlobalData()
366 {
368 // Create a sequencer using the factory
369 // platform = the platform the RapidSequencerSequencer runs on (RSI.RapidSequencer.Platform.INtime or RSI.RapidSequencer.Platform.Windows)
370 // _nodeName = the INtime node the RapidSequencer will run on
371 // _rmpNodeName = the INtime node the RMP runs on
372 // workingDir = the directory where the RapidSequencer executable is located
373 // port = the port the RapidSequencer listens on
374 // timeoutMs = the timeout in milliseconds after which Create() fails if the sequencer hasn't been created yet.
375 RSI.RapidSequencer.RapidSequencer sequencer = RSI.RapidSequencer.RapidSequencerFactory.Create(_platform, _nodeName, _rmpNodeName, workingDir, port, "RapidSequencer", timeoutMs);
376
377 // Start the sequencer. The sequencer must call `EngineStart()` before calling any other functions.
378 // rmpPath = the path to RMP.rta
379 string rmpPath = TestContext.CurrentContext.TestDirectory;
380 sequencer.EngineStart(_rmpNodeName, rmpPath);
381
382 // Compile the RapidSequencer script, giving it the path to the file and the name of the function to run
383 sequencer.Compile(samplesDirectory + "BasicGlobalData.sq", "main");
384
385 // Run the RapidSequencer asynchronously so we can adjust values while it's running.
386 string taskId = sequencer.RunAsync();
387
388 // Wait a second for it to run for a bit. This sleep is just for demonstration purposes, and may not be necessary in an actual application
389 Thread.Sleep(1000);
391
392
394 string globalIntName = "globalInt"; // the name of the global integer as a string.
395 int expectedIntValue = 2; // The value that we expect the integer to have.
396
397 // Get the value of a global integer in the RapidScript program, referring to it by name.
398 RSI.RapidSequencer.SequencerGlobal globalInt = sequencer.GlobalVariableGet(globalIntName);
399
400 // Make sure that the type is a 32-bit integer.
401 if (globalInt.Type == RSI.RapidSequencer.SequencerGlobal.DataType.Int32)
402 {
403 int intValue = globalInt.Value;
404
405 // This line does not need to be in most applications, and is just for demonstration / testing purposes.
406 Assert.That(intValue, Is.EqualTo(expectedIntValue));
407 }
409 Assert.That(globalInt.Type, Is.EqualTo(RSI.RapidSequencer.SequencerGlobal.DataType.Int32), $"Global variable {globalIntName} was not of type int32!");
410
412 // Set the value of a global double in the RapidSequencer script
413 string globalDoubleName = "globalDouble"; // The name of the global double as a string.
414 double expectedDoubleValue = globalInt.Value * 3.14159; // The value we want to set the double value to.
415
416 // The data type of the global value we're setting. If we want to set a global double, this must be SequencerGlobal.DataType.Double
417 RSI.RapidSequencer.SequencerGlobal.DataType dataType = RSI.RapidSequencer.SequencerGlobal.DataType.Double;
418
419 // Set the value of the global double named "globalDouble" to the desired value.
420 sequencer.GlobalVariableSet(globalDoubleName, dataType, expectedDoubleValue);
422
423 // Check that the global double's value was changed to what we expected
425 // Get the value of a global double in the RapidScript program, referring to it by name.
426 RSI.RapidSequencer.SequencerGlobal globalDouble = sequencer.GlobalVariableGet(globalDoubleName);
427
428 // Make sure that the type is a double.
429 if (globalDouble.Type == RSI.RapidSequencer.SequencerGlobal.DataType.Double)
430 {
431 double doubleValue = globalDouble.Value;
432
433 // This line does not need to be in most applications, and is just for demonstration / testing purposes.
434 Assert.That(doubleValue, Is.EqualTo(expectedDoubleValue));
435 }
437 Assert.That(globalDouble.Type, Is.EqualTo(RSI.RapidSequencer.SequencerGlobal.DataType.Double), $"Global variable {globalDoubleName} was not of type double!");
438
439 // Wait a second for the value to be updated then printed (printing is a slow operation)
440 Thread.Sleep(1000);
441
442 // Check the console output of the RapidSequencer
443 string output = sequencer.TaskOutputGet(taskId);
444 StringAssert.Contains($"{expectedDoubleValue}", output, $"Sequencer program output does not contain expected value {expectedDoubleValue}. Output: {output}");
445
446 // Stop the task
447 sequencer.TaskStop(taskId);
448 }
449 //@[GlobalDataExample]
450
451 [Test]
452 public void MoveSCurve()
453 {
454 string fileNameString = "MoveSCurve.sq";
455
457 RSI.RapidSequencer.RapidSequencer sequencer = RSI.RapidSequencer.RapidSequencerFactory.Create(_platform, _nodeName, _rmpNodeName, workingDir, port, "RapidSequencer");
458
459 string rmpPath = TestContext.CurrentContext.TestDirectory;
460 sequencer.EngineStart(_rmpNodeName, rmpPath);
461
462 string entryPoint = "main";
463 sequencer.Compile(samplesDirectory + fileNameString, entryPoint);
464
465 string taskId = sequencer.Run();
466
467 string output = sequencer.TaskOutputGet(taskId);
469
470 double expectedFinalPosition = 1.0;
471 Console.WriteLine($"output: {output}");
472 StringAssert.Contains($"{expectedFinalPosition}", output, $"Sequencer program axis move did not reach final destination: {expectedFinalPosition}. Output: {output}");
473
474 }
475}
476
Axis * AxisGet(int32_t axisNumber)
AxisGet returns a pointer to an Axis object and initializes its internals.
void MemorySet(uint64_t address, int32_t data)
Write a value to controller memory.
uint64_t AddressGet(RSIControllerAddressType type)
Get the an address for some location on the MotionController.
static MotionController * CreateFromSoftware()
Initialize and start the RMP EtherCAT controller.
void Delete(void)
Delete the MotionController and all its objects.
int32_t AxisCountGet()
Get the number of axes processing.
void InterruptEnableSet(bool enable)
Control interrupts for this class.
string TaskOutputGet(string taskId)
Gets the string output of the specified task.
string RunAsync(ulong[] breakpoints=null)
Run the last compiled script. Does not wait for the script to finish excecution. Returns the ID of th...
void TaskStop(string taskId)
Stops the specified task.
CompileSuccess Compile(string path, string entryPoint=DEFAULT_ENTRY_POINT, string exceptionHandler="")
Compiles the script at the given path. If successful, saves this compilation result to run at a later...
void GlobalVariableSet(string name, SequencerGlobal.DataType type, dynamic value)
Sets the value of the specified global variable.
string Run(ulong[] breakpoints=null)
Run the last compiled script. Waits for the script to finish execution. Returns the ID of the resulti...
void EngineStart(string rmpNode="NodeA", string rmpPath="")
Starts the runtime of the RapidSequencer process.
SequencerGlobal GlobalVariableGet(string name)
Gets the value of the global variable with the given name.
static RapidSequencer Create(Platform platform, string sequencerNodeName, string rmpNodeName, string executablePath, int grpcPort=DEFAULT_GRPC_PORT, string friendlyName="RapidServer", ulong timeoutMs=DEFAULT_TIMEOUT_MS, int discoveryPort=DEFAULT_DISCOVER_PORT)
Creates a RapidSequencer process on the given platform at the specified port if one does not already ...
The RapidSequencerFactory provides static methods for creating RapidSequencer processes or discoverin...
An object for interacting with a RapidSequencer process.
RSIFilterAlgorithm
Filter algorithms.
Definition rsienums.h:1095
RSIControllerAddressType
Used to get firmware address used in User Limits, Sequencers, etc.
Definition rsienums.h:404
RSIAction
Action to perform on an Axis.
Definition rsienums.h:1051
RSIAxisAddressType
Used to get firmware address used in User Limits, Sequencers, etc.
Definition rsienums.h:425
RSIMotorType
Motor Type.
Definition rsienums.h:1247
RSIMotionHoldType
Types for MotionHold attribute.
Definition rsienums.h:1031
RSIMotorFeedback
Encoders per motor.
Definition rsienums.h:1089
Structure for describing a global tag. Contains information about the type, name, and value of the ta...