You are here: Surpac Concepts > Macros > SCL > Third-party Developer Guidelines > Remote Control Guide
GEOVIA Surpac

Controlling Surpac using TCL over Sockets

Overview

This topic explains how Surpac responds to remote TCL requests. Although this feature is supported in Surpac 6.3, there is no guarantee that it will continue to work in this way in subsequent versions of Surpac. This topic covers the background for adding this functionality, how to enable it, use it, and some sample code for implementing the function.

Requirements

To get the most out of this information, it is recommended that you:

  • are familiar with TCL and specifically Surpac's SCL extensions
  • have a basic understanding of networking, that is, Sockets, ports, and TCP
  • have an understanding of the C/C++ programming language
  • understand the use of shared memory as a technology for sharing data between programs

Background

This functionality was developed in response to requests for third parties to be able to develop integrations with Surpac, and to create automated solutions with Surpac.

The solution

Controlling Surpac using a TCP/IP socket has a number of aspects. These include:

  1. Disabling the user interface controls. That is, the menus and toolbars.
  2. Using shared memory as a discovery mechanism.
  3. Using TCP/IP as the communications chanuel.

1. Disabling the user interface

This section is only relevant if you are interested in embedding Surpac in another application. If you are interested in implementing your program within Surpac, skip to the next section.

Add the -no_frame_decoration switch to remove the window frame from the application. This means the minimize, maximize, restore buttons will not be visible, nor will the title bar, or the border to resize it with.

However, this does not remove any of the user interface components. To remove other parts of the user interface, you need to use a custom edited surpac_vision.tcl, or the user profiles. The profiles files are all located in share/etc/resource/scripts and share/etc/profiles.

2. Using shared memory as a discovery mechanism

This section assumes you have a detailed knowledge of shared memory, and can understand a C programming language implementation that will demonstrate how it works.

A shared memory block with a predictable name is created when you start Surpac.The name of the shared memory is "GemcomSharedMemory<unsignedprocessID". This name can be built by appending a character representation of the unsigned value of the process ID to "GemcomSharedMemory".

One or more blocks will be present in the shared memory block. Each block complies with a defined structure using 8 byte aligned boundaries. Each subsequent block must comply with the following requirements:

  • int (8 byte aligned) defining the size of the block.
  • int (8 byte aligned) defining the ID of the block. This ID number defines the layout of the memory, that is, which fields are where.

Product information - Memory block ID = 1

Memory block data type Block size Description
unsigned 8 Size of the product info memory block
Int 8 Identifies the ID for the product info memory
Char 64 Null terminated string with the product name "Surpac"
Int 8 Product major version identifier
Int 8 Product minor version identifier
Int 8 Product maintenance version identifier
Int 8 TCP/IP port number for client/server communications
The end of the block of memory is represented by an end marker which has...
Int 8 end marker - always 0
Int 8 end marker type - always 0

Code example

The code sample below is in C programming language. You can use this example to create a program to experiment with the shared memory feature, to find the port number that you need to use to communicate with Surpac.

//

// The shared memory is created with a name that is built from

// "GemcomSharedMemory" to which is appended the process ID (an unsigned int).

// To construct the name of the shared memory an external program must:

// 1. Iterate through all processes

// 2. For each Surpac, Quarry, MineSched or Xplorpac process that is found

// append the process ID to "GemcomSharedMemory" and then attempt to open

// the shared memory as shown by the example code below.

//

// below is a command to build this program from a Visual Studio command window

//

// cl -RTCcsu -EHsc -Iinclude -Zi -DEBUG -DEBUGTYPE:cv testmem.cpp

//

#include <Windows.h>

#include <stdio.h>

#include <stdlib.h>

#include <sharedmem.h>

#include <string>

/**

* Open a block of shared memory with a specified name.

*

* @param memName [I] The name of the shared memory

* @param lpMapAddress [O] The starting address of the mapped memory. NULL if

* failed to open.

*

* @return Handle to the block of shared memory. 0 if unsuccessful,

* non-zero if successful. Can be used with closeSharedMemory() to

* free resources.

*

*/

HANDLE openSharedMemory(const char *memName, LPVOID *lpMapAddress)

{

HANDLE hMapFile = NULL;

*lpMapAddress = NULL;

hMapFile =

OpenFileMapping(FILE_MAP_READ, // read access only

FALSE, // don't inherit the handle - whatever that means

memName); // name of mapping object

 

if (hMapFile) {

// now get the shared memory address and write details to shared memory

*lpMapAddress =

MapViewOfFile(hMapFile, // handle to mapping object

FILE_MAP_READ, // read permission

0, // high DWORD to offset to map

0, // low DWORD to offset to map

0); // map the entire file

}

return(hMapFile);

}

/**

* Close reference to a block of shared memory previously opened

* using opeSharedMemory().

*

* @param hMapFile [I] Handle to the shared memory object obtained

* using openSharedMemory()

* @param lpMapAddress [I] The pointer to teh mapped view when it was

* originally mapped. See the lpMapAddress arg of

* openSharedMemory()

*/

void closeSharedMemory(HANDLE hMapFile, LPVOID lpMapAddress)

{

if (lpMapAddress) {

UnmapViewOfFile(lpMapAddress);

}

if (hMapFile) {

CloseHandle(hMapFile);

}

}

/**

* Get the address of the next block of shared memory. Returns NULL if no

* more blocks of memory to process.

*

* @param blockPtr [I] Address of the current block of memory. Initially

* obtained using openSharedMemory()

*

* @return pointer to the next block of shared memory, NULL if no more memory

* blocks available in the shared memory.

*

*/

void *nextSharedMemoryBlock(void *blockPtr)

{

SharedMemBlockInfo *blockInfo;

void *nextBlockPtr;

 

blockInfo = (SharedMemBlockInfo *)blockPtr;

if (blockInfo != NULL && blockInfo->memBlockSize > 0) {

nextBlockPtr = (void *)(((char *)blockPtr) + blockInfo->memBlockSize);

} else {

nextBlockPtr = NULL;

}

return (nextBlockPtr);

}

/**

* Creates a named block of shared memory to aid discovery of Surpac process

* specifics.

*

* The shared memory block has these characteristics:

*

* The name is "GemcomSharedMemory<unsigned process ID>" - external programs

* can build the name using "GemcomSharedMemory" and appending to it a

* character representation of the unsigned value of the process ID.

*

* Product Info - Memory block ID = 1

* ----------------------------------

* unsigned - Size of the product info memory block

* int - Identifies the ID for the product info memory block

* char[64] - Null terminated string with product name - "Surpac"

* int - Product major version identifier

* int - Product minor version identifier

* int - Product maintenance version identifier

* int - TCP/IP port number for client/server communications

*

* The end of the block of memory is represented by and end marker which has:

* int - end marker - always 0

* int - end marker type always 0

*

*/

int main(int argc, char **argv)

{

// create a named block of shared memory area with a well defined name

// so that external programs can check the contents of the shared memory

// for information about the running executable

//

// This requirement comes the remote control capabilities of the software

// which, to work, need to know what TCP/IP port is being used for

// client/server comms. The port number and other useful items can be

// found in the shared memory

//

//

// see

//ms-help://MS.VSCC.2003/MS.MSDNQTR.2003FEB.1033/fileio/base/creating_named_shared_memory.htm

// for an example of using shared memory

//

HANDLE hMapFile = NULL;

char errText[1024];

char memname[1024];

unsigned pid;

DWORD err;

 

printf("Enter process id :");

scanf("%u", &pid);

if (pid > 0) {

LPVOID lpMapAddress = NULL;

LPVOID orig_lpMapAddress = NULL;

 

sprintf(memname, "GemcomSharedMemory%u", pid);

hMapFile = openSharedMemory(memname, &lpMapAddress);

orig_lpMapAddress = lpMapAddress; // the original pointer is required later

// to properly close the mapped view

if (lpMapAddress) {

SharedMemBlockInfo *blockInfo;

do {

blockInfo = (SharedMemBlockInfo *)lpMapAddress;

if (blockInfo->memBlockID == 0) {

SharedMemEndMarker *endMarker = (SharedMemEndMarker *)lpMapAddress;

printf("Shared memory end marker size =%d\n", endMarker->memBlockSize);

printf("Shared memory end marker type =%d\n", endMarker->memBlockID);

break;

} else if (blockInfo->memBlockID == 1) {

// product info memory block

SharedMemProductInfo *productInfo = (SharedMemProductInfo *)lpMapAddress;

printf("\n");

printf("Product info memory size =%u\n", productInfo->blockInfo.memBlockSize);

printf("Product info block ID =%u\n", productInfo->blockInfo.memBlockID);

printf("Product name =%s\n", productInfo->productName);

printf("Product major version =%u\n", productInfo->majorProductVersion);

printf("Product minor version =%u\n", productInfo->minorProductVersion);

printf("Product sub release version =%u\n", productInfo->maintenanceProductVersion);

printf("Server port number =%u\n", productInfo->serverPortNumber);

}

lpMapAddress = nextSharedMemoryBlock(lpMapAddress);

} while (lpMapAddress);

 

closeSharedMemory(hMapFile, orig_lpMapAddress);

 

} else {

DWORD err = GetLastError();

FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, 0, err, 0, errText,

sizeof(errText), 0);

}

}

closeSharedmemory(hMapFile);

exit(0);

}

3. Using TCP/IP as the communication channel

You can obtain sample code from the GEOVIA Support website Knowledge Base to do the communication. The sample is written in VB.Net.

Tip: You can find the Knowledge Base article by logging onto GEOVIA Support, navigating to the Surpac Knowledge Base, and searching for “Surpac remote control”.

The .NET class is called SurpacRemoteControl. To use it you instantiate the class, and then call the runTCLSript()method. You can pass this control:

  • any arbitrary script
  • the arguments for the script
  • a handler delegate that is called with the results of the command once you receive a response

Here is a sample visual basic code that calls this:

Public Sub ResponseReceived(ByVal command As Integer, _
ByVal response As String)
MsgBox response
End Sub
 
Dim remote = New SurpacRemoteControl(5000)
Dim commandID = remote.runTCLScript("set result 42", "", _
New SurpacRemoteControl.ResponseHandler( _
AddressOf ResponseReceived))
		

This executes the TCL script "set result 42", and because of the way TCL works, the most recently assigned value will be the result of the whole script. This means the number 42 will be returned and passed into our ResponseReceived() subroutine, which in turn displays a message box.

Implementation details

This section details the messages that are passed back and forth between the client (the 3rd party application) and the server (Surpac).

The socket connection is a TCL stream socket, which listens on the port provided. Each connection has a lifetime of one script only.

Protocol

After connecting, the client sends only one message. There are two forms, one that executes a TCL script with arguments, and one that executes the script without arguments.

Note: A \n indicates a hex character 0xA0, or in visual basic CHR(10).

The form for a script without arguments is as follows, where script contents is a place holder for the TCL code you want to run.

RCTL\n
TCLSCRIPTBEGIN\n
script contents
TCLSCRIPTEND

The form for a script with arguments is as follows, where arguments is a place holder for the TCL arguments, and ‘script contents’ is a place holder for the TCL code you want to run.

RCTL\n
TCLARGSBEGIN\n
arguments
TCLARGSEND\n
TCLSCRIPTBEGIN\n
script contents
TCLSCRIPTEND

After sending this, the client needs to wait for a response, which will be the most recently assigned TCL variable, as if it was printed to a text buffer using puts.