The RMP Motion Controller APIs
RapidSequencerAPI.cs
1 using System;
2 using System.IO;
3 using System.Threading;
4 using System.Threading.Tasks;
5 using NUnit.Framework;
6 using RSI.RapidCode.dotNET;
7 using System.Diagnostics;
8 using 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 
RSI::RapidCode::RSIMotorFeedback
RSIMotorFeedback
Encoders per motor.
Definition: rsienums.h:1051
RSI::RapidSequencer.RapidSequencerFactory.Create
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 ...
Definition: RapidSequencer.cs:630
RSI::RapidSequencer.SequencerGlobal
Structure for describing a global tag. Contains information about the type, name, and value of the ta...
Definition: RapidSequencer.cs:35
RSI::RapidSequencer
RSI::RapidCode
RSI::RapidCode::RSIMotionHoldType
RSIMotionHoldType
Types for MotionHold attribute.
Definition: rsienums.h:993
RSI::RapidCode::RSIMotorType
RSIMotorType
Motor Type.
Definition: rsienums.h:1209
RSI
RSI::RapidCode::RSIControllerAddressType
RSIControllerAddressType
Used to get firmware address used in User Limits, Sequencers, etc.
Definition: rsienums.h:408
RSI::RapidCode::RSIAction
RSIAction
Action to perform on an Axis.
Definition: rsienums.h:1013
RSI::RapidCode::RSIFilterAlgorithm
RSIFilterAlgorithm
Filter algorithms.
Definition: rsienums.h:1057
RSI::RapidSequencer.RapidSequencerFactory
The RapidSequencerFactory provides static methods for creating RapidSequencer processes or discoverin...
Definition: RapidSequencer.cs:562
RSI::RapidSequencer.RapidSequencer
An object for interacting with a RapidSequencer process.
Definition: RapidSequencer.cs:64
RSI::RapidCode::RSIAxisAddressType
RSIAxisAddressType
Used to get firmware address used in User Limits, Sequencers, etc.
Definition: rsienums.h:423
RSI::RapidSequencer::API
RSI::RapidCode::RSIState
RSIState
State of an Axis or MultiAxis.
Definition: rsienums.h:786
RSI::RapidSequencer.RapidSequencer.EngineStart
void EngineStart(string rmpNode="NodeA", string rmpPath="")
Starts the runtime of the RapidSequencer process.
Definition: RapidSequencer.cs:252