Queuing Methods
I saw the queuing method posted in a recent technote and I was not happy to see all of the string manipulation. I think that string operations are some of the most processor intensive commands, so I decided to test the performance of the provided queue versus the two queues I have written. Here are the results:
The two queues I use are storing the data in a structure (or it could be an array depending on needs) and using two pointers one that indicates insertion point and one the indicates current command to send. You can see that using pointers is WAY faster than string concatenation.
I also found one other problem with the queue provided by AMX. They have a limit of 50000 chars for queuing, but netlinx concatenation falls apart at 16000 chars. If you are using 200 character commands (think to a touch panel more than to a 232 device), then you will hit the problem area at around 79 commands being queued. Be aware of this limitation when using the queue they provide.
Jeff
Line 1 (10:58:20.046):: ********************************************************* Line 2 (10:58:20.062):: * TEST 1 REPORT: Load String Queue Line 3 (10:58:20.062):: * Most recent 5 runs: Line 4 (10:58:20.062):: * 1: 4542ms Line 5 (10:58:20.062):: * 2: 4562ms Line 6 (10:58:20.062):: * 3: 5252ms Line 7 (10:58:20.062):: * 4: 4544ms Line 8 (10:58:20.062):: * 5: 4542ms Line 9 (10:58:20.062):: *---------------------------------------------------------- Line 10 (10:58:20.062):: * Average run time: 4687ms - over 5 tests Line 11 (10:58:20.062):: ********************************************************* Line 12 (10:58:20.062):: ********************************************************* Line 13 (10:58:20.062):: * TEST 2 REPORT: Load Structure Queue, no tracking Line 14 (10:58:20.062):: * Most recent 5 runs: Line 15 (10:58:20.062):: * 1: 199ms Line 16 (10:58:20.062):: * 2: 229ms Line 17 (10:58:20.062):: * 3: 227ms Line 18 (10:58:20.062):: * 4: 198ms Line 19 (10:58:20.062):: * 5: 196ms Line 20 (10:58:20.062):: *---------------------------------------------------------- Line 21 (10:58:20.062):: * Average run time: 209ms - over 5 tests Line 22 (10:58:20.078):: ********************************************************* Line 23 (10:58:20.078):: ********************************************************* Line 24 (10:58:20.078):: * TEST 3 REPORT: Load Structure Queue, last command tracking Line 25 (10:58:20.078):: * Most recent 5 runs: Line 26 (10:58:20.078):: * 1: 206ms Line 27 (10:58:20.078):: * 2: 241ms Line 28 (10:58:20.078):: * 3: 238ms Line 29 (10:58:20.078):: * 4: 207ms Line 30 (10:58:20.078):: * 5: 204ms Line 31 (10:58:20.078):: *---------------------------------------------------------- Line 32 (10:58:20.078):: * Average run time: 218ms - over 5 tests Line 33 (10:58:20.078):: ********************************************************* Line 34 (10:58:20.078):: ********************************************************* Line 35 (10:58:20.078):: * TEST 4 REPORT: Send String Queue Line 36 (10:58:20.078):: * Most recent 5 runs: Line 37 (10:58:20.078):: * 1: 3232ms Line 38 (10:58:20.078):: * 2: 3299ms Line 39 (10:58:20.078):: * 3: 3687ms Line 40 (10:58:20.093):: * 4: 3234ms Line 41 (10:58:20.093):: * 5: 3232ms Line 42 (10:58:20.093):: *---------------------------------------------------------- Line 43 (10:58:20.093):: * Average run time: 3336ms - over 5 tests Line 44 (10:58:20.093):: ********************************************************* Line 45 (10:58:20.093):: ********************************************************* Line 46 (10:58:20.093):: * TEST 5 REPORT: Send Structure Queue, no tracking Line 47 (10:58:20.093):: * Most recent 5 runs: Line 48 (10:58:20.093):: * 1: 257ms Line 49 (10:58:20.093):: * 2: 304ms Line 50 (10:58:20.093):: * 3: 269ms Line 51 (10:58:20.093):: * 4: 256ms Line 52 (10:58:20.093):: * 5: 257ms Line 53 (10:58:20.093):: *---------------------------------------------------------- Line 54 (10:58:20.093):: * Average run time: 268ms - over 5 tests Line 55 (10:58:20.093):: ********************************************************* Line 56 (10:58:20.093):: ********************************************************* Line 57 (10:58:20.093):: * TEST 6 REPORT: Send Structure Queue, last command tracking Line 58 (10:58:20.109):: * Most recent 5 runs: Line 59 (10:58:20.109):: * 1: 233ms Line 60 (10:58:20.109):: * 2: 253ms Line 61 (10:58:20.109):: * 3: 232ms Line 62 (10:58:20.109):: * 4: 233ms Line 63 (10:58:20.109):: * 5: 234ms Line 64 (10:58:20.109):: *---------------------------------------------------------- Line 65 (10:58:20.109):: * Average run time: 236ms - over 5 tests Line 66 (10:58:20.109):: *********************************************************
The two queues I use are storing the data in a structure (or it could be an array depending on needs) and using two pointers one that indicates insertion point and one the indicates current command to send. You can see that using pointers is WAY faster than string concatenation.
I also found one other problem with the queue provided by AMX. They have a limit of 50000 chars for queuing, but netlinx concatenation falls apart at 16000 chars. If you are using 200 character commands (think to a touch panel more than to a 232 device), then you will hit the problem area at around 79 commands being queued. Be aware of this limitation when using the queue they provide.
Jeff
0
Comments
Plus loading the queue is usually event related, push a button or a string_event and you load a command into queue and possibly a follow up query. So how fast does queue loading really need to be?
The easiest way to get around that is what I believe Spire_Jeff was getting at. Rather than continuously shifting the queue upwards so that it is always firing out the command thats sitting in index 1 on the array use two pointers. One which tracking the next available position to fill and one which point to the next position to fire out. That way you can create something along the lines of:
STRUCTURE _sDeviceQueue { CHAR cCommand[50][32] INTEGER nOut INTEGER nIn }Then use a timeline to fire out the commands at an interval the device likes. When a command is fired out increment the out pointer and if it is the same as the in pointer set them both back to 1.
IF(!(nCURRENT_COMMAND == 1 + ( nCURRENT_QUEUE % AP_QUEUE_SIZE))) { nCURRENT_QUEUE = 1 + ( nCURRENT_QUEUE % AP_QUEUE_SIZE) //Increase queue insertion point to next place unless we have reached max queue size uAP_CMD[nCURRENT_QUEUE].CMD = sCMND uAP_CMD[nCURRENT_QUEUE].dvDEV = dvOUT } ELSE { nCURRENT_COMMAND = 1 + ( nCURRENT_COMMAND % AP_QUEUE_SIZE) //Drop oldest command nCURRENT_QUEUE = 1 + ( nCURRENT_QUEUE % AP_QUEUE_SIZE) //Increase queue insertion point to next place unless we have reached max queue size uAP_CMD[nCURRENT_QUEUE].CMD = sCMND uAP_CMD[nCURRENT_QUEUE].dvDEV = dvOUT }Jeff
P.S.
As I look at this code, I am thinking that reversing the if and else code and dropping the ! could make the code more efficient
I guess "find_string" is a time consuming process when you think about it and remove string possibly even longer.
Not sure if I'm ready to convert but it is an interested concept for queueing which I hadn't considered. Changing the pointer for song lists yes but for a send_command or send_string queue I never would have thunk of it.
I have gone both ways on this. In the situation that queue was written for, I started by throwing errors, but I recently switched to overwrite. It doesn't happen often, but when there is a little lag, it makes more sense to overwrite old messages. It also does not make sense to dump the queue when the device drops offline for a second or two (IP device).
Jeff
possible to show more of your code, definition and Time_Line? Thanks much!
If (nWait!=1){ nWait=1 //Do Spire's Magic WAIT 'wFail' 10{ nWait=0 } }Would you still get the performance boost?I generally use a combination of the timed based cueing and feedback. If you set up your timeline which fires the commands out to do so at the interval specified as the minimum consecutive command gap (if your lucky it may be in the protocol, otherwise 40ms seems to be about right for a lot of devices) but combine it with an ACK timeout (say 1 second). That way each iteration of the timeline will fire of the next command in the cue if the previous command has been an acknowledged, otherwise it will skip it and wait until the next timeline event until the timeout has been reached. If the ACK times out it will try and send the command again up to 3 times, after 3 connsecutive time outs it will bin the command and flag an error - either in RMS if applicable or just send it to 0:0:0 so I can see it as I debug. That way you're verifying all commands for devices that give you nice feedback (for the ones that don't you can always set up some seperate polling logic and make sure its always in the state your code is expecting it to be so no one can play with those damn hardware buttons), and making sure your system doesn't completely lock up in the event of something going wrong. Additionally it will always be firing out the commands as fast as the device can accept them.
DEFINE_FUNCTION INTEGER SEND_CMD (DEV dvOUT, CHAR sCMND[260]) { IF(TIMELINE_ACTIVE(TL_CMDS)) { IF(!(nCURRENT_COMMAND == 1 + ( nCURRENT_QUEUE % AP_QUEUE_SIZE))) { nCURRENT_QUEUE = 1 + ( nCURRENT_QUEUE % AP_QUEUE_SIZE) uAP_CMD[nCURRENT_QUEUE].CMD = sCMND uAP_CMD[nCURRENT_QUEUE].dvDEV = dvOUT } ELSE { nCURRENT_COMMAND = 1 + ( nCURRENT_COMMAND % AP_QUEUE_SIZE) nCURRENT_QUEUE = 1 + ( nCURRENT_QUEUE % AP_QUEUE_SIZE) uAP_CMD[nCURRENT_QUEUE].CMD = sCMND uAP_CMD[nCURRENT_QUEUE].dvDEV = dvOUT } } ELSE { nCURRENT_QUEUE = 1 + ( nCURRENT_QUEUE % AP_QUEUE_SIZE) uAP_CMD[nCURRENT_QUEUE].CMD = sCMND uAP_CMD[nCURRENT_QUEUE].dvDEV = dvOUT TIMELINE_CREATE(TL_CMDS,lAP_COMM_TL,1,TIMELINE_RELATIVE, TIMELINE_REPEAT) } RETURN 0 } .... TIMELINE_EVENT[TL_CMDS] { nCURRENT_COMMAND = 1 + (nCURRENT_COMMAND % AP_QUEUE_SIZE) SEND_STRING uAP_CMD[nCURRENT_COMMAND].dvDEV,uAP_CMD[nCURRENT_COMMAND].CMD IF(DEBUG>2) SEND_STRING 0,"'COMMAND SENT: ',LEFT_STRING(uAP_CMD[nCURRENT_COMMAND].CMD,25)" IF(nCURRENT_COMMAND == nCURRENT_QUEUE) TIMELINE_KILL(TL_CMDS) }Jeff
Here's one that I use - similar to Jeff's. It uses two functions, One to queue the command, the second to send the command. It also uses a timeline, array, and a structure. This one's not so fancy though, as I don't wait for ACK responses from the device. It could be modified to test for ACK and resend if an error message is received, but this has worked well enough for me. One caveat is that if you fire enough commands at it to wrap the array around so that the queuing position passes the sending position, you can cause problems. One advantage of this method though is that it's simple to send commands to multiple devices and the Queue keeps track of which msg goes to which device.
(***********************************************************) (* FILE CREATED ON: 08/02/2008 AT: 11:04:56 *) (***********************************************************) (***********************************************************) (***********************************************************) (* FILE_LAST_MODIFIED_ON: 08/02/2008 AT: 11:31:30 *) (***********************************************************) (***********************************************************) (* DEVICE NUMBER DEFINITIONS GO BELOW *) (***********************************************************) DEFINE_DEVICE dvMaster = 5001:1:0 dvTP = 10001:1:0 (***********************************************************) (* CONSTANT DEFINITIONS GO BELOW *) (***********************************************************) DEFINE_CONSTANT //fnQueueTheCommand, fnSendtheCommand INTEGER TL_Queue = 1 (***********************************************************) (* DATA TYPE DEFINITIONS GO BELOW *) (***********************************************************) DEFINE_TYPE STRUCTURE _sCmdQueue { DEV dvSendtoDevice CHAR cCmdtoSend[50] } (***********************************************************) (* VARIABLE DEFINITIONS GO BELOW *) (***********************************************************) DEFINE_VARIABLE //fnQueueTheCommand, fnSendtheCommand _sCmdQueue _CmdQueue[50] VOLATILE LONG nSendSpacing[1]= {200} //Time between commands PERSISTENT INTEGER nCurrentQSendingPosition PERSISTENT INTEGER nCurrentQueueingPosition (***********************************************************) (* LATCHING DEFINITIONS GO BELOW *) (***********************************************************) DEFINE_LATCHING (***********************************************************) (* MUTUALLY EXCLUSIVE DEFINITIONS GO BELOW *) (***********************************************************) DEFINE_MUTUALLY_EXCLUSIVE (***********************************************************) (* SUBROUTINE/FUNCTION DEFINITIONS GO BELOW *) (***********************************************************) (* EXAMPLE: DEFINE_FUNCTION <RETURN_TYPE> <NAME> (<PARAMETERS>) *) (* EXAMPLE: DEFINE_CALL '<NAME>' (<PARAMETERS>) *) DEFINE_FUNCTION fnQueueTheCommand(DEV dvDevice,CHAR cMsg[50]) { _CmdQueue[nCurrentQueueingPosition].dvSendtoDevice = dvDevice _CmdQueue[nCurrentQueueingPosition].cCmdtoSend = cMsg nCurrentQueueingPosition++ IF (nCurrentQueueingPosition>50) nCurrentQueueingPosition=1 IF(!TIMELINE_ACTIVE(TL_Queue)) TIMELINE_CREATE(TL_Queue, nSendSpacing, 1, TIMELINE_ABSOLUTE, TIMELINE_REPEAT) } DEFINE_FUNCTION fnSendtheCommand() { SEND_STRING _CmdQueue[nCurrentQSendingPosition].dvSendtoDevice,"_CmdQueue[nCurrentQSendingPosition].cCmdtoSend" _CmdQueue[nCurrentQSendingPosition].dvSendtoDevice = 0:0:0 //Clear the Buffer _CmdQueue[nCurrentQSendingPosition].cCmdtoSend = "''" //Clear the Buffer nCurrentQSendingPosition++ IF(nCurrentQSendingPosition>50) nCurrentQSendingPosition=1 IF(nCurrentQSendingPosition==nCurrentQueueingPosition) //If the current sending position is the same as the current queue position then the Queue is empty, kill the timeline TIMELINE_KILL (TL_Queue) } (***********************************************************) (* STARTUP CODE GOES BELOW *) (***********************************************************) DEFINE_START nCurrentQSendingPosition=1 nCurrentQueueingPosition=1 (***********************************************************) (* THE EVENTS GO BELOW *) (***********************************************************) DEFINE_EVENT DEFINE_EVENT TIMELINE_EVENT[TL_Queue] { fnSendtheCommand() } (***********************************************************) (* THE ACTUAL PROGRAM GOES BELOW *) (***********************************************************) DEFINE_PROGRAM (***********************************************************) (* END OF PROGRAM *) (* DO NOT PUT ANY CODE BELOW THIS COMMENT *) (***********************************************************)--John
Jeff - couple of questions.
nCurrent_command is the command that just was sent correct?
nCurrent_Queue is the location of the last command in the Q?
You have one Q for all the devices in the system?
Do you run into situations where your volume control "stutters" because a command for another device falls into the Q between volume ramping commands? (say you are monitoring the projector(s) power status every 5 seconds and the user just happens to be changing the volume during that time.)
I know that I tend to use separate Q's for each device, As well as triggering the next command from the ACK or NAK from the device. (I too will throw out error codes for NAKs and try up to 5 times for each command - mostly for the RMS apps), so one Q for all the devices with a set timeline makes me a little nervous.
I think it would be more useful if there is a way to insert 10 miliseconds into the outgoing buffer *after* the command has been sent so that the next command is being executed properly by the device. This will save implementing any ques, I think!?
But I think the command listed above will just insert spacing between each character sent. good for slowing down a serial data stream, butnot for queing commands going to the device. I think.
"27,19,<time>" Insert a time delay before transmitting the next character.Reading this line would make me think this is a one time deal and where it's inserted in affect creates a wait before the strings that follow are sent, but...Example: SEND_COMMAND RS232_1,"27,19,10" Inserts a 10 millisecond delay before transmitting characters to the RS232_1 device.Reading this line makes me think it may work like JohnMichnr describe and insert a delay between all subsequent characters.Hmmm, which way does it actually work? If it's the 1st method then it could be usefull to delay individual commands similar to queue timing except you're using the output buffer as the queue and you would then have to send this delay to the buffer after every command to insert this delay. I don't really see a reason for the second 2nd interpretation since if you have to slow down every character it should be running at a lower baud unless there's some funky timing requirements.
I don't use one central queue for all the devices on the system but I think you're right, using one queue for a lot of devices could potentially hurt you on real-time stuff like volume control, PTZ, etc.
--John
If we're all guessing, I think it's the first one, it puts in a one-time delay. I think the example is if you send that naked string, and immediately send a second send_command, there will be a 10 ms delay before transmitting characters.
e.g.
I'll try to test it this weekend if no one posts a definitive answer.
--John
Here's the code I was using if anyone else wants to fiddle about. I just open the debug window and change the variables to see what happens and like I said I don't see it doing anything reliably so I wouldn't use this at all for anything. Maybe it's me but....
Code used:
DEFINE_DEVICE dvRS232_1 = 5001:1:0 ; dvRS232_2 = 5001:2:0 ; DEFINE_VARIABLE VOLATILE INTEGER nRS232_1_SendStr = 0 ; VOLATILE INTEGER nRS232_1_PreCmd = 0 ; VOLATILE INTEGER nRS232_1_PostCmd = 0 ; VOLATILE INTEGER nRS232_1_PreStr = 0 ; VOLATILE INTEGER nRS232_1_PostStr = 0 ; VOLATILE INTEGER nRS232_1_NumStrSend = 10 ; VOLATILE INTEGER nRS232_1_NumMilliSec = 10 ; DEFINE_FUNCTION fnSend_RS232_Str(INTEGER iIndx) { if(nRS232_1_PreCmd) { SEND_COMMAND dvRS232_1,"27,19,nRS232_1_NumMilliSec" ; } if(nRS232_1_PreStr) { SEND_STRING dvRS232_1,"27,19,nRS232_1_NumMilliSec" ; } SEND_STRING dvRS232_1,"'Test String ',itoa(iIndx),' Out RS232_1'" ; if(nRS232_1_PostCmd) { SEND_COMMAND dvRS232_1,"27,19,nRS232_1_NumMilliSec" ; } if(nRS232_1_PostStr) { SEND_STRING dvRS232_1,"27,19,nRS232_1_NumMilliSec" ; } } DATA_EVENT [dvRS232_1] //SET BAUD 9600,N,8,1 485 DISABLE and gets zone status { ONLINE: { SEND_COMMAND dvRS232_1,'SET BAUD 9600,N,8,1 485 DISABLE' ; } STRING: { SEND_STRING 0,"'RS232_1 RCV''D, ',DATA.TEXT" ; } } DATA_EVENT [dvRS232_2] //SET BAUD 9600,N,8,1 485 DISABLE and gets zone status { ONLINE: { SEND_COMMAND dvRS232_2,'SET BAUD 9600,N,8,1 485 DISABLE' ; } STRING: { SEND_STRING 0,"'RS232_2 RCV''D, ',DATA.TEXT" ; } } DEFINE_PROGRAM if(nRS232_1_SendStr) { STACK_VAR INTEGER i ; nRS232_1_SendStr = 0 ; for(i = 1 ; i <= nRS232_1_NumStrSend ; i++) { fnSend_RS232_Str(i) ; } }FYI, I used a crossover cable from port1 to port 2.I tried about every combination of string vs commands, before the string and after the string, a couple strings vs 10 strings, 10 milliseconds vs various milliseconds and nothing I did gave anything near the result I was hoping for.
Oh well.
This is a que that I use frequently. It doesn't use a lot of overhead and you can set the time between commands by adjusting the wait time at the bottom. It works great for me;
DEFINE_FUNCTION AP_DISCONNECT(INTEGER OUTPUT) { AP_COMM("'DL0O',ITOA(OUTPUT),'T'") } DEFINE_FUNCTION AP_COMM(CHAR COMM_STR[15]) { AP_COMM_WAITING++ IF(AP_COMM_WAITING > nAP_BUFFER_SIZE) //if at the end, loop around to the start AP_COMM_WAITING = 1 cAP_BUFFER[AP_COMM_WAITING] = COMM_STR } DEFINE_PROGRAM IF((AP_COMM_WAITING > 0) && AP_COMM_READY) //IF WE HAVE A COMMAND AND IT'S READY TO SEND, { OFF[AP_COMM_READY] AP_COMM_SENDING++ IF(AP_COMM_SENDING > nAP_BUFFER_SIZE) //IF AT THE END OF THE BUFFER, LOOP AROUND TO THE FIRST INDEX AP_COMM_SENDING = 1 SEND_STRING DvAP,cAP_BUFFER[AP_COMM_SENDING] cAP_BUFFER[AP_COMM_SENDING] = '' IF(cAP_BUFFER[AP_COMM_WAITING] = '') //LAST COMMAND IN BUFFER { OFF[AP_COMM_WAITING] OFF[AP_COMM_SENDING] } WAIT 2 ON[AP_COMM_READY] }