This document is a tutorial in how to create a plugin under Mach3 that will override the native printer port driver, and allow one to send movement and I/O via another driver/Device. This device may use any of several protocols, including USB, TCP/IP, and common serial port communication.
Since the G100 Plugin is likely the most complex to be written due to its lack of an API (initially anyway), we will use it as the example for the process. Sample code posted in this document can also be seen in the Mach3 SDK under the G100 subfolder.
First, let's take a look at what's necessary for a typical plugin of the non-movement variety. "Movement" plugins are built from that foundation.
Files:
MachDevice.cpp/.h MachDevImplementation.cpp/.h XMLProfile.cpp/.h
These files define a plugin. The MachDevice.cpp/.h are files which will morph over time as more two-way functions are available to call between Mach3 and a plugin. All the required calls for the writer of a plugin are in MachDevImplementation.cpp. In there are calls such as:
void myInitControl (); // called during Mach initialisation
// you can influence subsequent init by actions here
// **** Not used in typical device plugin
void myPostInitControl (); //called when mach is fully set up
void myConfig (CXMLProfile *DevProf); // Called to configure the device
void myUpdate (); // 10Hz update loop
void myHighSpeedUpdate (); // called at 40Hz
void myCleanUp(); // Used to cleanup heap variables before
// destrucion of PlugIn
void myHome( short axis);
void myProbe();
Lets take a quick look at each..
void myInitControl ()
This routine is called when Mach3 is starting up. It allows a plugin to do some initialization work and to set itself up. In a movement plugin it is necessary to inform Mach3 that this IS a movement plugin, so the user should be asked if he wishes to take advantage of the non-native device. This is done by setting a variable in the Mach viewclass called m_PrinterOn. As in MachView->m_PrinterOn = false;
This call forces Mach to recognize that a plugin is available and to make sure the user has a chance to use it. It tells Mach3 not to load the printer port driver. You will see this in the InitControl call of the G100 plugin as well as some other housekeeping things we’ll go over later.
void myPostInitControl ();
This one gets called after Mach3 has setup all its various stages and is about to start running fully. In this call you still have a chance to use the Mach3 profile database. Normally, there is no access to the user settings database during run time; Mach3 cannot share it with the plugin. However, at startup and config time, access is granted. In functions where it is allowed, you will see calls such as ,//tt>DefDir-> GetProfileString("Preferences","DefDir","C:\\Mach3\\");</tt>
This is a call to the database to get the Default folder Mach is using.
void myConfig (CXMLProfile *DevProf);
This is called when the user asks for the configuration of a plugin. The G100 plugin then opens a dialog to allow setup of the G100’s parameters.
void myUpdate ();
This routine is called 10 times per second when Mach3 is running. Is is handy to keep a process synced or to run periodic tasks.
void myHighSpeedUpdate ();
If you need a faster update timer, this one is available. It is called 40 times per second when Mach3 is running. Don’t use it for much heavy work, it can affect Mach3’s available CPU time.
void myCleanUp();
This gets called when the program shuts down. A PlugIn can cleanup any variables it may have created.
Before you can effectively take over Mach3’s control, it's handy to know how exactly it works.
Mach3 is a great many timers and loops of messages. Each is self contained in a master loop, a loop which operates at about 10Hz. The Mach3 printer port engine operates on a loop of 25KHz, and is self contained and automatic. In other words, given a series of commands, Mach3’s printer port driver runs autonomously, and with no interference. The 10 hz loop in Mach3 keeps check on the printer port driver, sends it commands and responds to various semaphores to know when it needs to interact with the Engines variable block.
For example, if Mach3 wants to Jog with the printer port, it needs only to set a variable (Engine->Axis[n].Jogging = True after setting a max speed with Engine->Axis[n].MaxVelocity = 8334400, the jog would be automatic and will ramp up to speed and hold until told to stop. The 8334400 is the number of steps/second * 2^12 in order to give the integer based engine code a divisable integer to get many points of precision.
The G100 has to perform many of its functions autonomously (to replace the Pport Driver), so the Plugin will get a command like JogOn (x,nnn) telling it to do the job previously done by the printer port driver.
What is important to the External movement plugin author is how do I get those commands; and how do I tell Mach3 my states at various times. We’ll go over the entire problem here, and how the G100 implements it.
To speak to an external movement controller, we need a protocol. This
can be serial, TCP, USB, ect. So the first job for the plugin is to
implement a protocol. The one we’ll
use in the G100 is a TCP protocol. So we build it up from the start.
I'm using a TCP class that connects a TCP port to the plugin. We’ll do
that in the PostInitControl().
void myPostInitControl ()
// called when mach fully set up so all data can be used but initialization
// outcomes not affected
{
// In the case of a MotionControl plugin, this routine is only called to tell us to take
// over. We HAVE been selected and are now commanded to do whatever it takes to OWN the IO
// and movement of this run.
// For example, if you wish to zero all IO at this point, you're free to do so...
//
// First, let's add a message tracking facility to handle message traffic output.
// Output is in its own class because it's important that we do not overrun the G100's
// buffers or queues. We'll make the tracker object intelligent enough to be able
// to handle any errors, retransmissions and tracking ack numbers and such...
// Now let's open a Debug File. We'll only use it if instructed by Mach3's DebugThisRun
// selection in config/state
G100.Discovered.Stage = DISCNONE;
CTime time = CTime::GetCurrentTime();
CString stamp = time.Format( "%a - %H:%M:%S ---");
// Now a message tracker class. It will marshal messages and make sure
// we don't flood the G100.
if( G100Mess == NULL)
{
G100Mess = new MessageTracker(); //we'll delete this in the shutdown routine.
// Now let's initialise some G100 variables.
G100Mess->ResetQueue( );
}
if( G100Mon == NULL)
{
G100Mon = new Monitoring(); //we'll delete this in the shutdown routine.
// Now lets initialise some G100 variables.
}
G100.InJog = false;
InitJogMessage();
// In the G100 case, we'll open/start-up a network thread to deal with communications..
StartNetwork();
return;
} //myPostInitControl
The last command here is to start the network object. You'll see I have created other objects like a MessageTracker facility for the G100 and a safety monitor. You can ignore all those for now; just notice the call to startNetwork above. It starts the network process by this routine..
void StartNetwork()
{
int t; //error storage
G100.Discovered.Done = false;
G100.SEQUENCE = NOSEQ;
G100.Discovered.Stage = STAGE1; //set discovery to stage 1.
//
// udp setup..
G100.PHASE = DISCOVERY; //set G100 to Discovery phase with zeroed address
//automatically, due to code int he 10hz update, discovery
//will begin;
G100.Config.ipaddr = 0; //set out IP to zero until we discover it.
AfxSocketInit();
if( g100sock != NULL )
{
delete g100sock;
}
g100sock = new CUDPSocket(); //startup the TCP support
if( !g100sock->Create(13887))
{
G100.PHASE = DISABLED;
G100.Disability = "Socket Open Failure";
G100Mess->DebugOut( G100.Disability );
return;
}
//
G100.Disability = "Starting Discovery."; //set a tracking message in case of error.
int res = g100sock->SetSockOpt( SO_DONTROUTE, &t, 1);
res = g100sock->AsyncSelect( FD_READ | FD_WRITE | FD_OOB | FD_ACCEPT | FD_CONNECT |
FD_CLOSE ); //full capability
res = g100sock->SetSockOpt( SO_BROADCAST, &t, 1); //set socket to broadcast;
} //StartNetwork
In the case of theG100, I have created a large data structure to keep track of all its various states. It is called GRex, and has many variables in it.
struct GRex
{
WTConfig Config
Phase PHASE; // current phase of G100 .
Sequencing SEQUENCE; // Home and Probhe sequence
MessageSeq MESSAGESEQ;
Discovery Discovered; // process information
CString Disability; // Reason G100 is disabled.
AddressAssign G100Address; // Address Asignment Block
bool StartUp; // initial message traffic
CTime LastMessageTime; // Time last message was received
//
// Traffic variables.
short LastAck; //The last message Acked
short LastMess; //The last Message Sent.
short LastStored; //the last message stored.
short Window; //Last Window received.
//
//Condition Varibale
int InWord; //the last 32bit input
int OutputWord; //The last OutputWord sent
int CurrentISR; //Current G100 timer
int CurrentMISR; // Current G100 timer count. ISR counter
WTMsAbs LastAbs; //Last absolute structure
int CurDDA[6]; //Current DDA's from last status
double LastCoord[6]; //last ABS coordinate from status.
int CurrentQueueSeq; //Current Queue Seq.
int NextQueue; //the next movements queue
int EndQueueSeq; // As of last status, the last move
// message in the queue
int QueueFlags; // and the last Queue
int CurrentHaltBits; //Current monitoring bits
int CurrentHaltMask; // Current halt condition
int CurrentMonMask; //Current monitoring mask
bool IsStill; // Set by status if the G100 is
// state.sflags & QS_STILL
bool IsDrained; // set by last state is state.sflags & QS_MAVGZ
bool InJog; //Is any axis Jogging
int CurrentMavg; //Current Mavg in use.
int CalcMavg; //Expected Mavg
bool Dwelling; //bool that tells the system were waiting
// for a IsStill to clear a dwell
bool HasMoved; //tracks that a move has started for
// probeing and homing sequences.
bool NeedDigOutputs; //Trigger to send new outputs
bool NeedAnaOutputs; //Trigger to make Analogues go out
bool Homing; // We're in a homing move
int HomingMask; //needed for homing the appropriate axis
int HomeAxis; //current homing axis;
//
// Monitoring Vars.
int LastChallenge; //the last responded to
bool Monitoring; // Are we currently
int MonitorHaltBits; //halt conditions and levels
int MonitorHaltMask;
int MonitorMask; //Non critical monitoring
int CurrentChallenge; // Currently sent
bool SystemHalted; //System is currently
CTime WaitSet;
//
// G100 Sync Events
WaitState WaitForState;
unsigned int WaitForValue;
};
Most of these variables will become clear as you peruse the code of the plugin, they contain Booleans to trigger certain events under certain circumstances. Your movement plugin is unlikely to need this level of complexity. Most external hardware will have an API to do the majority of the work, or less of a configurable outcome than the G100.
As you can see I have a variable called PHASE which I use to do a several step process of contacting the G100 to see if I can take control of it. This phaseing is automatic and the end result is a PHASE called IPRUN which tells us its OK to do normal traffic. The stages are handled by the 10hz timer. IT will send messages and monitor responses until we can achieve stage IPRUN; meaning the hardware is all ready.
Look at how the discovery loop is called. We set PHASE to DISCOVERY and G100.Discovered.Stage to STAGE1, send a quick message of “Are you there” out the network port, and we now need to see what to do next. So the 10hz update loop checks it all.
void myUpdate ()
{
//................
CTime Now = CTime::GetCurrentTime();
CTimeSpan Since = Now - G100.LastMessageTime; //Time since last ???
switch ( G100.PHASE )
{
case( DISABLED ): return;
case( RUNONIP
if(Since.GetSeconds() > 1 && !Engine->EStop)
{
Engine->EStop = true; //set an ???
Engine->EStopReason = 12;
}
G100Mon->Update();
break; //this will process IO and such..
case( DISCOVERY ): DiscoveryLoop();
break;
}
} // myUpdate
Since the G100 variable PHASE is set to Discovery, no other action will happen except discovery work. SO the DiscoveryLoop is called repeatedly until the phase switches to RUNONIP. Let's look at DiscoveryLoop then..
It basically checks for what stage we’re in, and if the requirements of that stage have been met so we can progress to the next stage in discovery.
switch( G100.Discovered.Stage )
{
case( DISCNONE ): //just a holder while we startup the discovery..
break;
case( STAGE1 ): G100.Discovered.Stage = STAGE2;
res = g100sock->SendTo( NULL, 0, 13887 ,
"255.255.255.255" , 0);
if( res == SOCKET_ERROR )
{
error = g100sock->GetLastError();
G100.PHASE = DISABLED;
G100.Disability = " Error Sending on initial 13887 port.";
G100Mess->DebugOut( G100.Disability );
g100sock->Close();
fig = new G100Config();
fig->DoModal();
}
G100.Discovered.started = CTime::GetCurrentTime();
G100.Discovered.ended = CTime::GetCurrentTime();
break;
case( STAGE2) : // Here we'll check for a timeout on the initial discovery.
// We should have a response within a few seconds.
G100.Discovered.ended = CTime::GetCurrentTime();...................
Ok, So here is stage1</tt1> being checked. We just set it to <tt>Stage1 earlier, now the discovery loop knows to send out the “Are you there” question and bump the stage to STAGE2. Similarly, these stages will progress if the conditions are met until we hit the stage RUNONIP where the system will begin normal operations. You can see in <tt<stage2</tt> checking…
case( STAGE2) : // Here we'll check for a timeout on the initial discovery.
// We should have a response within a few seconds.
G100.Discovered.ended = CTime::GetCurrentTime();
sofar = G100.Discovered.ended - G100.Discovered.started;
seconds = sofar.GetSeconds();
if( seconds > 5 )
{
G100.Disability = " TimeOut in UDP Reception. No G100 Answered.";
G100Mess->DebugOut( G100.Disability );
G100.PHASE = DISABLED;
g100sock->Close();
fig = new G100Config();
fig->DoModal();
}
return;
It starts by checking the time to see if 5 seconds have passed. If we haven’t got a response from the G100 and the stage hasn’t been bumped to stage3 within 5 seconds, we will get an error. Each stage checks itself to make sure it hasn’t got hung up on an earlier stage: it will trigger a timeout message and that will open a dialog allowing the user to restart the discovery. So all this begs the question, what bumps it to stage3 so this timeout doesn’t happen?
Most of the messageing in the G100 plugin is triggered by the incoming messages. The TCP class Im using is Event driven; that is to say you don’t need to poll for data, the functions PlugRecieve() will be called whenever a message comes in and we're not in RUNONIP condition. So I use this function as a trigger to process incoming messages as well as to trigger events like stage checking. Lets look at it..
void PlugReceive( char *Buffer, int m_size )
{
if( ShutDown ) return;
//
// Discovery Traffic Message Handing
//
if( m_size == sizeof( WTConfig ) && G100.Discovered.Stage == STAGE2 )
//Is this a config packet responce??
{ //and are we lookign for one? If not, discard.
memcpy( &G100.Config, Buffer, m_size );
G100.Discovered.Stage = STAGE3; //we now have the G100 config.
// Switch to Stage 3, IP RUN switchover or error checking
//of the IP.
if( G100.Config.version == VERSION_DLM ) //check that the firmware is running..
{
G100.PHASE = DISABLED;
G100.Disability = " G100 not running firmware. Reboot G100";
return;
}
}
// if we're waiting for first IP responce, lets tell Discovery worker
// that we are now fully connected.
if( G100.Discovered.Stage == STAGE4 )
{
//We are getting Stage4 traffic, so bump to Stage 5. We're done with discovery;
G100.Discovered.Stage = STAGE5;
return;
}
As you can see it is bumping the stages up until finally we hit stage5 when we will switchover to RUNONIP, which is the normal run time of the plugin. All of this from config to full discovery takes about 3-5 seconds after startup depending on the system. So now we have a running TCP link, and message traffic is OK to go. We have a send message routine, and this function will now process messages with the remainder of its function.
//
// RUNONIP stage message handling.
// This will be the normal incoming traffic
// We only reach here with normal incoming traffic.
//
memcpy( &InMessage, Buffer, m_size );
//first copy the entire message into the message structure.
G100.Window = ((InMessage.Header.chanflgs & UCHAN_WNDO) & 0x07);
//set the max message window for the input buffer of the rabbit.
G100.LastMessageTime = CTime::GetCurrentTime();
//If we get a less than 6 byte messge, we have a sequence error..!!
if( m_size <= 6)
{
//Message Loss Error Usually happens at high traffic..
int lost = InMessage.Header.seq;
G100Mess->LostMessage( lost );
} else
{
G100Mess->AckMessage( InMessage.Header.ack );
G100TranslateMessage( InMessage, m_size );
}
//Messages have now been decoded, all G100 states shoudl be set accordingly.
// So lets update the Message Tracker so it knows
// its limitations based on any new information
G100Mess->Update(); //this doesnt send anythign,
//it just ensures the tracker knows how much it CAN send if needs be.
//So now lets send any output that is required.
//The translations above have set the parameters in the G100 Block
// to tell us it is now necessary to transmit an output message.
if( G100.NeedDigOutputs ) DoDigOutput();
//and analogue as well
if( G100.NeedAnaOutputs ) DoAnalogOutput();
//If we're Jogging, lets refresh the Jog..
if( G100.InJog ) Rejog();
//We really only need to send Inputs & Outputs
//every 1/4 second or so. So we'll just nudge it
// here to do a send if there is one holding..
DoGcodeMove(); //take care of any waiting Gcode movement;
if( G100Mess->nMessages > 0 ) G100Mess->SendMessages();
}
As you can see, now we have a set of routines that check G100 specific protocol, such as checking the message size. If it's less than 6 bytes, an error has occurred in transmision and error handlers get called. If it is more than 6, then it will Ack the message to the state routines, and translate it for processing. It will also run G100Mess->Update which will do things such as send any holding messages and such. Most of this is unimportant to USB and Serial authors, perhaps.. But it is an idea of how the message traffic is controlled. I set up a link, then continuously buffer messages, which are sent and acknowledged, translated, and dealt with in the routines we've discussed.
Mach3, in its 10Hz loop, looks at all incoming and outgoing I/O requirements. If a spindle needs to be turned on, or an input needs to be acknowledged, it is done in the 10hz loop, but can be done anytime.
In our case, the G100 is sending a status message every 250ms telling us the current input and output conditions. When we look at the G100TranslateMessage( InMessage, m_size ); from the Receiving loop of the PlugIn we can see the IO being checked.
switch( InMessage.Header.type )
{
case WT_MONITOR:
mon = (WTMonitor*) &InMessage.Message;
G100Mon->OnMonitorMessage( mon );
break;
case WT_MS_FULL:
status = (WTMsFull*) &InMessage.Message;
FillFromFullStatus( *status );
break;
case WT_MS_OUTIN:
oi = (WTMsOutIn*)&InMessage.Message;
CheckInputs(oi->in ); //active
CheckOutputs( oi->out ); //active
break;
case WT_MS_ASYNC:
asyn = (WTMsAsync*)&InMessage.Message;
CheckQueueCap( asyn->qcap ,true );
CheckRELPosition( asyn->pos ); //active
CheckCurrentState( asyn->qstate );
break;
case WT_MS_POS:
pos = (WTMsPos*)&InMessage.Message;
CheckRELPosition( *pos ); //active
break;
case WT_MS_CTR:
ctr = (WTMsCtr*)&InMessage.Message;
CheckEncoders( *ctr ); //active
break;
case WT_MS_IN:
in = (WTMsIn*)&InMessage.Message;
CheckInputs( *in );
break;
case WT_MS_OUT:
out = (WTMsOut*)&InMessage.Message;
CheckOutputs( *out ); //active
break;
case WT_MS_ABS:
abs = (WTMsAbs*)&InMessage.Message;
CheckABSPosition( *abs ); //active
break;
case WT_MS_QSTAT:
stat = (WTMsQstat*)&InMessage.Message;
CheckCurrentState( *stat );
break;
case WT_MS_QCAP:
cap = (WTMsQcap*)&InMessage.Message;
CheckQueueCap( *cap , true ); //check the queue capacity
break;
default:
break;
}
This loop looks at incoming message types. The input messages are WT_MS_IN and WT_MS_OUT so they call CheckInputs( *in ) and CheckOutputs( *out ) when needed. These routines look at the incoming bits and compare them to what Mach3 has; same for outputs. Lets look at outputs for example.
void CheckOutputs( WTMsOut out )
{
bool On = false;
G100.NeedDigOutputs = false;
unsigned int outs = 0;
for( int x = 0; x< nSigsOut; x++ )
{
if( Engine->OutSigs[x].OutPort == 1 &&
Engine->OutSigs[x].active &&
Engine->OutSigs[x].OutPin >= 1 &&
Engine->OutSigs[x].OutPin < 17 ) //pins 1-16
{
On = Engine->OutSigs[x].Activated;
if( Engine->OutSigs[x].Negated) On = !On;
//On is now true or false depending on the current state
SetBit( outs, On, Engine->OutSigs[x].OutPin); //set the bit in the output..
}
}
if( out.o != outs ) G100.NeedDigOutputs = true;
G100.NeedAnaOutputs = false;
if( (unsigned short)MainPlanner->ModOutputs[124] != out.aout[0] ||
//analogue outputs from analogue holding registers.
(unsigned short)MainPlanner->ModOutputs[125] != out.aout[1] ||
(unsigned short)MainPlanner->ModOutputs[126] != out.aout[2] ||
//analogue #3 seems unable to accept data
(unsigned short)MainPlanner->ModOutputs[127] != out.aout[3])
G100.NeedAnaOutputs = true;
}; //outputs
Mach3 has a variable block in the engine (seen in Engine.h) which has all the various output signals. Engine->OutSigs[x], this block is checked against the reported output of the G100. The end result of all this checking is G100.NeedDigOutputs = true; if the outputs are not in sync with what Mach3 knows should be set. The system will then (because this variable is set to true) send a message to the G100 to tell it to update the outputs to match Mach3. This same process is seen for inputs and outputs.
So in summary: Each time the G100 sends an input or output status, it is checked against required status, and a setoutput message is sent if the outputs are not aligned with expectations. If we look at inputs.
void CheckInputs( WTMsIn In )
{
In.i = ShuffleInWord( In.i); //shuffle the input word..
G100.InWord = In.i;
bool On = false;
for( int x = 0; x< nSigs; x++ )
{
if( Engine->InSigs[x].InPort == 1 &&
Engine->InSigs[x].Active &&
Engine->InSigs[x].InPin >= 1 &&
Engine->InSigs[x].InPin < 23 )
{
On = ((In.i >> (Engine->InSigs[x].InPin -1)) & 0x01);
Engine->InSigs[x].Activated = On;
if( Engine->InSigs[x].Negated ) Engine->InSigs[x].Activated = !On;
}
if( x < 64 )
{
int mask = 1 << x;
if( (Engine->WaitCondition & mask) > 0 )
if( Engine->InSigs[x].Active && Engine->InSigs[x].Activated )
//to make Mach3 waitcondition work..
Engine->WaitCondition &= ( 0xffffffff ^ mask );
}
}
MainPlanner->ModInputs[124] = In.ain[0];
//analogue inputs to the analogue holding registers.
MainPlanner->ModInputs[125] = In.ain[1];
MainPlanner->ModInputs[126] = In.ain[2];
MainPlanner->ModInputs[127] = In.ain[3];
}; //inputs
We can see the inputs are retrieved as bits, then shuffled into the Engine->inSigs[n] variable block so that Mach3 knows what signal is set and when. It will take care of the rest. If an input has been set, Mach3 will deal with it appropriately. If a particular input means your hardware needs to perform a particular function, you should then perform it.
SO we now have seen how Inputs and Outputs are done. Now we have to
create movement. The hard part! First, let's consider what Mach3 does
for movement. There are two types; Jogging and Trajectory movement.
Each is very important, and many don’t consider the importance of
Jogging highly enough. The tactility of Jogging is not to be
underestimated to make hardware use less frustrating. So first, we’ll
take a look as how jogging is done in a g100 system.
Any external device will require code to do its jogging. Mach3 only cares about two things, first that it be told jogging is underway so it will not attempt other movement functions, and second that when jogging stops, that it be told so, in order to update its position registers with proper position. Mach keeps track of its position as an integer value of current number of steps put out. Those values can be updated automatically, by setting the variable Engine->Sync to true. This value will change back to false when Mach3 completes the syncing operation.
The routines, void MyJogOn( short axis, short direction, double SpeedNorm) and void MyJogOff(short axis) are called directly from Mach3 to the ExternalMovement.cpp file in order to control Jogging.
The meanings are obvious, except perhaps for the SpeedNorm value in the jogon call. This value will be zero if normal jog speed is desired, and will be a value if a special speed is to be used. The routine also checks to see if the Keyboard is actively holding down the shift key. This is used to override the _setup->JogPercent value (which Mach3 maintains to tell the plugin writer what % of maximum speed should be used for this jog operation.) In other words, if the JogOn routine gets called and the keyboard is not shifted, and the SpeedNorm is zero, then Jog will be at (JogPerCent/100)* MainPlanner->Velocities[axis]. MainPlanner->Velocities[axis] is the holder for the maximum speed an axis is programmed to go. If the shift key is held, then that’s the speed which will be used. IF SpeedNorm is set, then that’s the speed the jog should be performed at. This is used for some special circumstances where jog must be controlled at a finer rate.
The routine MyJogOff(axis) will stop a jog in progress. In there, you will see that when all jogs are stopped, a call is made to resync positions by setting that variable Engine->Sync = true. Mach3 will then resync all its internal positions to be accurate when it sees all movement has stopped.
What about GCode movement? Well, we’ve seen that jogging is just a commanded movement to the external device. The G100 requires that this be confirmed every 250ms, so you'll see code in there to allow for the jogging messages to be re-sent every 250ms to keep jogging active. This is a safety; your hardware may not have that option. GCode movement is easy to get, but its difficulty in sending to your device depends a lot on the device. Lets look at the routine called void DoGcodeMove()
This routine gets called every time a message comes in from the G100, it can be called as frequenctly as you wish. It has one job, to see if movement is holding, and if so, to send it out. Mach3 buffers up movements and holds them in a ring buffer. Mach will stop buffering movement when either the buffer is full, or no more movement exists. A plugin basically just sits around sending I/O messages, checking the IO is proper, jogging or stopping jogging. It only needs to know that when the Gcode buffer is not empty; it needs to empty it by sending the data. It starts with this line:
if( Engine->TrajHead == Engine->TrajIndex || Engine->DwellTime > 0) return; //no moves to process
Engine->TrajHead is the head of a ringbuffer, TrajIndex is the tail. If these two values are the same, no data is holding. If DwellTime is not zero then we are dwelling and no movement should be sent. (Unless the device can buffer it up until the dwell is complete. It is up to the author of the plugin to make sure he sends no more information to the external device than it can handle. You’ll see queue size checks in the G100 code to do this. Most of the code in this routine can be ignored by other programmers, the important thing is to be able to tell if movement is holding, and if so, how to get it. The TrajIndex and TrajHead check will tell you if it is holding. To get it you need only reach into the ring buffer with a call like:
CurrentMove = MainPlanner->Movements[Engine->TrajIndex]
The CurrentMove variable is a GMoves structure. Youll see it in the trajectorycontrol.h file. Its structure is:
struct GMoves
{
int type; // 0 is linear, 1 is cubic
double cx,cy,cz; // center of move for cubics.
double ex,ey,ez,ea,eb,ec;
double sx,sy,sz,sa,sb,sc;
__int64 DDA1[6]; //DDA1's for cubics
__int64 DDA2[6];
__int64 DDA3[6];
double Time;
bool Stop;
};
The ring buffer is up to 2048 entries long. So there are 2048 (or less) GMove structures for you to send. You grab them one at a time and send the move to your device. Lets take a look at what information is in the entry.
“type” is a 1 or zero to tell you if this move is part of a line or arc.
Cx,cy,cz are the center corrdinates if this is an arc.
Ex,ey,ez,ea,eb,ec are the end points of this move in machine coordinates.
Sx,sy,syz… are the start points of the move.
Time is the time of the move in ms. These moves will all be 1-64ms long depending on the move. No move will be longer than 64ms. Since the time of the move is in the structure, acceleration has already been taken care of. Same with velocity. The velocity will be the change in position over time, and the acceleration is built in by the change in velocity from one move to the next. How you deal with this will depend on the hardware.
The DDA1,2 and 3 variables are specific to the G100, but can be used by other hardware. They are a “Differential Data Derivative” of a cubic move. In the case of the G100, only cubics are sent as moves, even if only a straight line. A DDA is a way of making a frequency based axis move in a straight line or a curve. They contain speed data over time with kick and jerk information to create a curved move. These DDA’s are computed froma fast Bezier curve routine to best approximate the move requested. DDA1,DDA2 and DDA3 are computed for every move. The way they work is a bit complex (or simple depending on the reader. )
Take a move like:
DDA1 = 10000
DDA2 = 5
DDD3 = -1
Time = 7
This would create a curve. DDA1 is the initial velocity to be put out in the move in the first 1ms of movement. Each ms of movement that occurs, DDA2 is added to that velocity AND DDA3 is added to DDA2. So during the 5ms of this commanded move:
Time in ms FrequencyVariable DDA2 DDA3
1 10000 5 -2
2 10005 3 -2
3 10008 1 -2
4 10009 -1 -2
5 10008 -3 -2
6 10005 -5 -2
7 10000 -7 -2
So as you can see in this 7ms movement, the speed has gone up and down in ms increments. And it's done all in the hardware with no intervention by Mach3. This allows a sub acceleration of 1ms control. Having two axes (or more) vary their speed is in essence a circular arc movement. The DDA’s are computed to ensure the movement is always a circular arc (though ellipsoids and such would be possible).
More information is available on the DDA’s in the file G200x.h which is the header file for the G100 firmware. Here is a brief explanation from that file:
/* This segment is intended for efficient generation of arcs and curved
motion. The math is simple: at each iteration (1024Hz rate) the
following operations are performed in sequence:
<current velocity> = dda1
dda1 += dda2
dda2 += dda3
At T=0, all ddas are set to the value in this message.
After T iterations of this algorithm,
<pos> = <start pos> + dda3*T*(T-1)*(T-2)/6 + dda2*T*(T-1)/2 + dda1*T
dda1 = dda3*T*(T-1)/2 + dda2*T + dda1
dda2 = dda3*T + dda2
dda3 = <unchanged>
where the right hand side dda's are the initial values.
Linear motion (with dda2=dda3=0) may run for 65536 iterations (the max) with
no loss of accuracy. Parabolic motion (dda3=0, dda2 != 0) may also run
for this many iterations. General cubic motion may run for about 4096
iterations, after which there may be some rounding error. (Rounding
error is defined as more than 1/2 an output step.)
Your planner does not need to use DDA’s, though the information is there if you care to and it matches your hardware. You can just use the 64ms movement coordinates just as easily. Since there are up to 2048 entries in the buffer, there is up to 131 seonds of movement that could be sent to buffer up the device, but at the moment, I create only a maximum of 2 seconds worth at any time, so its important to keep calling the DoGCode routine (or whatever yours is called) in order to keep your device buffer full.
Once you use a ring buffer element, increment the Index pointer with: Engine->TrajIndex++;
Engine->TrajIndex &= 0xfff;
This keeps it synced to the next move available and informs mach3 in its 10hz loop to add more data if it needs to.
So to control a plugin, you need to interface only a few
functions: Jog, GCode movment when data appears, and I/O. Reading the
G100 plugin will show you various variables used for syncing, or
developing positional data that Mach3 needs as feedback. Check and read
the message translators in Decoders.cpp to see how each of the incoming
messages deals with updating Mach3 as to what has been set in inputs,
what the current position is for the engine, and other semaphores used.
When you see a line like TajectoryControl-> or _setup-> or Engine->,
these are calls back to Mach3’s variables to tell Mach3 whats going on.
There are not many of them though; most operations are autonomous.
Mach3 runs, sets movements in the buffers and calls JogOn and JogOff when appropriate. You need only to make the moves happen, and tell Mach3 what you did.
You will undoubtedly have questions as you go. The G100 source code is
the best place to see how I handled various situations, such as homing
and such, but feel free to email me your questions as they arise and
please edit this wiki article to clarify any point that you eventually
understand.
Art Fenerty (PlugIn author) Artsoft Inc.