Overview

The TCP Server plugin hosts configurable TCP connections to which networked client computers can connect and communicate with running JADE systems. Clients open a connection to the hosted IP address and port as defined in the TCP Server configuration. Multiple simultaneous connections are supported and handled concurrently, and the maximum number of connections allowed is configurable (-1 means no limit; see configuration details below). Connected clients can then send requests to the TCP Server, to perform actions such as:

  • fetch data from any plugins the TCP Server has subscribed to (configurable, of course)
  • send control messages to the Worker instance (ex. run or shut down a plugin instance; see The Worker documentation for details)
  • send messages to any plugins which support control messages (see documentation for specific plugins for details)

User Interface

The TCP Server interface displays both the Latest Message (latest subscription message to arrive) and Merged Messages (an aggregate of all subscription messages under a unique key for each subscription source - a plugin or the Worker).

Subscription Data Handling

The TCP Server plugin accepts published JSON messages and displays both the Latest Message and Merged Messages. As data arrives, the TCP Server plugin aggregates that data into the Merged Messages object, which can then be queried by external applications (more on this later). Let’s look at an example to help build our mental model for how subscription data is handled. Suppose the TCP Server plugin subscribes to two plugins named MySerialPublisher1 and MySerialPublisher2 which publish temperature and pressure data respectively.

Suppose MySerialPublisher1 publishes:

{
    "instanceName": "MySerialPublisher1",
    "temperature": 22.4,
    "unit": "Celcius"
}

Suppose MySerialPublisher2 publishes:

{
    "instanceName": "MySerialPublisher2",
    "pressure": 148.7,
    "unit": "PSI"
}

When those published messages arrive, the TCP Server will look for the special key instanceName which uniquely identifies the source of the data and use its value as a top level key name for storing the incoming data. It does this “namespacing” of sorts to avoid naming collisions in the Merged Messages object. Don’t worry, you can configure the list of such “special keys” in the TCP Server’s messageSourceKeyNames configuration option, but almost all publishing plugins use instanceName. In this case, the incoming data results in the following Merged Messages object:

{
    "MySerialPublisher1": {
        "instanceName": "MySerialPublisher1",
        "temperature": 22.4,
        "unit": "Celcius"
    },
    "MySerialPublisher2": {
        "instanceName": "MySerialPublisher2",
        "pressure": 148.7,
        "unit": "PSI"
    }
}

So as long as the TCP Server plugin also supports requests to get data from the Merged Messages object, external applications can get data from any plugin which the TCP Server has subscribed to (more on this later).

Subscription Data Special Cases

Ok, so we know we can handle messages published from plugins who use a special key to identify themselves. Let’s cover some special cases:

  1. What happens if data comes in without the special instaneName key?
    In that case, the TCP Server will simply put that data under a top level key named __UNKNOWN_MESSAGE__.

  2. I know the Worker can publish data such as the statuses of all plugins. How do I subscribe to that data and how does it get set in the Merged Messages object?
    First, the TCP Server would need to subscribe to __PLUGIN_INFO__ (literally just add __PLUGIN_INFO__ to the subscribesTo array in configuration). Then the question becomes, what special key does the Worker use to identify itself. The answer is workerName. So as long as the messageSourceKeyNames array in the TCP Server’s configuration has workerName in it, you’re all set.

  3. What if I want my plugin data to go under a different top level key in Merged Messages?
    Well, the list of special keys are be configured in messageSourceKeyNames. The messageSourceKeyNames (string array) defaults to ["instanceName", "workerName"] to cover the common/standard cases right “out of the box”, but you have full control over this just in case.

  4. What if my plugin publishes both an instanceName as well as a uniqueId key, but I want to use the uniqueId instead of the instanceName for the top level key in Merged Messages?
    In this case, you’d just add uniqueId to the front of the messageSourceKeyNames array. When incoming messages are processed, the order of the strings in messageSourceKeyNames determines the order of precedence for which special key gets used. In other words, the first one in the array to be found in the incoming subscription message gets used.

Request Handling

Now that we know how subscription data is handled, let’s take a look at how the TCP Server handles requests (one of which will, of course, allow us to fetch subscription data). Let’s first understand the required structure of a request, see an example, and then do the same for a response.

Requests

When a request is received, the TCP Server will read and interpret the first 4 bytes (the header) as a signed 32-bit integer (big endian byte order) whose value must be equal to the size of the rest of the reuqest (the body). The body is expected to be a serialized JSON object with the following top level keys:

  • target (string, one of: __SERVER__, __WORKER__, or any plugin instance name): the target of the message; i.e. who the message is intended for
  • message (type depends on the request, often a JSON object): the message to process or forward along to the specified target

Here’s an example (4-byte header not shown, but must preceed the JSON object):

{
    "target": "__SERVER__",
    "message": {
        "operation": "Get Data",
        "data": {
            "path": "MySerialPublisher1.tempature"
        }
    }
}

Notice the target seems to specify the TCP Server itself (__SERVER__). Also notice the operation is Get Data. That’s a supported operation by the TCP Server, which will get data from the Merged Messages object at the specified path. Here the path is MySerialPublisher1.temperature. Looking back at our subscription data handling example, this would appear to be a request to get the temperature published by MySerialPublisher1 (which in that example would have a value of 22.4). Now the question is: what does the response look like?

Responses

All responses from the TCP Server use the same header concept (first 4 bytes are a number represented as a signed 32-bit integer, big endian byte order, whose value is equal to the size of the rest of the response) and the body is a serialized JSON object with the following top level keys:

  • value (a valid JSON type: boolean, string, number, array, or object): the value specified
  • error (object with boolean status, integer code, and string source)

The error will contain error or warning information, if any, but will always be returned as an object with it’s elements. The status element will be true if an error occurred and false otherwise. The code element will be non-zero for errors or warnings (the distinction between an error and a warning is simply that for warnings the status is false) and will be 0 for no error or warning. The source element will contain human readable text describing the error or warning, or an empty string if there is no error or warning to report.

So what would the response to our request example above look lik (assuming the data from our Subscription Data Handling section above)? Here’s the answer (4-byte header not shown, but must preceed the JSON object):

{
    "value": 22.4,
    "error": {
        "status": false,
        "code": 0,
        "source": ""
    }
}

Now that we have an idea for how to send requests and read responses, let’s take a look at the supported requests.

Messages Routing

The TCP Server plugin routes messages contained in requests based on the specified target. In the example above we saw the __SERVER__ target, which refers to the TCP Server itself, which currently only supports the Get Data message noted in our example above. But we also noted that the target could be __WORKER__ or any plugin instance name. If the target is __WORKER__ then the message will be routed to the Worker; this is how control messages can be sent to the Worker from an external application. Similarly, if the target is some plugin instance name, the message will be sent to the corresponding plugin. So now the question becomes, what messages are supported by other plugins? And the Worker? Well, each plugin determines if and what messages are supported, and each plugin should have documentation for all its supported messages. Similarly, the Worker documents all its supported messages, which can be found in The Worker documentation.

One important question is: what can I expect back from the Worker or plugins when my message is routed to them? The answer is, you’ll get a standard acknowledgement from the TCP Server that your request has been received, as shown below:

{
    "value": "Message received.",
    "error": {
        "status": false,
        "code": 0,
        "source": ""
    }
}

At this time, the TCP Server does not wait for a response from the Worker or any plugin, and in fact those components do not respond at all to their control messages, rather they simply take the action defined in the message. Since these plugins or components communicate over internally managed queues (not dependent on a network connection), there is essentially no chance that such messages will be lost. If an invalid message is sent, plugins will generally generate an error which can be seen in the Worker (or seen in the Worker’s published __PLUGIN_INFO__ data).


Configuration Example

Plugin Defaults
0
0
0

Configuration Details

Filter:Search in:
ROOT object
This top level object holds all configuration information for this plugin.
Required: true
Default: (not specified; see any element defaults within)
subscribesTo array
An array of plugin instance names corresponding to plugin instances which will be subscribed to by this plugin instance.
Required: true
Default:
[]
subscribesTo[n] string
A plugin instance name (corresponding to a plugin you wish to subscribe to) or a topic published by the worker (ex. __PLUGIN_INFO__).
Required: false
Default: ""
options object
Configuration options specific to this plugin. Note that variables and expressions are generally allowed in this section.
Required: true
Default: (not specified; see any element defaults within)
options.messageSourceKeyNames array
An array of key names to look for when inspecting incoming messages for merge. The first element in this array found as a key name in the Latest Message will be used as the key under which that message will be placed in the Merged Messages object. If no key is found, the message will be placed under a key named "__UNKNOWN_SOURCE__".
Required: true
Default:
[
    "workerName",
    "instanceName"
]
options.messageSourceKeyNames[n] string
A key name to look for when inspecting incoming messages for merge.
Required: false
Default: ""
options.server object
An object with parameters for creating the server.
Required: true
Default: (not specified; see any element defaults within)
options.server.address string
The network address on which to listen for connections. Use an empty string here to listen on all available network cards on the computer.
Required: true
Default: ""
options.server.port integer
The port on which to listen.
Required: true
Default: 6341
options.server.createListenerTimeout integer
The amount of time to wait (before returning an error) when creating the TCP listener for the specified address and port.
Required: true
Default: 25000
options.server.clientMessageReadTimeout integer
While the server waits indefinitely for a message to arrive, the message read process is two read operations: 1. read the first 4 bytes (the message header, of sorts) representing the size of the core message content to follow and 2. read the core message content. The first read operation waits indefinitely and only breaks when the connection is eventually broken (ex. when the plugin is shut down). The second read operation uses the timeout specified here to ensure that if the client makes a mistake by specifying too large a header / size with too small a message to follow, that this read doesn't sit and wait forever and block subsequent requests. All requests are responded to and in the event of any errors, the client will receive details in the response.
Required: true
Default: 2000
options.server.maxClientConnections integer
The maximum number of connections allowed by the server. -1 means no limit.
Required: true
Default: -1
options.logger object
Defines the logging (data and errors) for this plugin. Note that a LOG variable space is provided here, as well as the VAR variable space. Available variables are: @LOG{LOGGERNAME}, @LOG{TIMESTAMP}, @LOG{LOGMESSAGE}, @LOG{ERRORMESSAGE}, and @VAR{instanceName} are available variables. note: @LOG{LOGGERNAME} is equal to the @VAR{instanceName} here.
Required: true
Default: (not specified; see any element defaults within)
options.logger.Enable boolean
Whether to enable the logger.
Required: true
Default: true
options.logger.LogFolder string
The folder in which to write log files.
Required: true
Default: "\\JADE_LOGS\\@VAR{instanceName}"
options.logger.FileNameFormat string
The filename to use when creating log files. Note: if the filesize limit is reached new files will be created with enumerated suffixes such as: MyLogFile-1.txt, MyLogFile-2.txt, etc.
Required: true
Default: "@VAR{instanceName}-@LOG{TIMESTAMP}.log"
options.logger.ErrorsOnly boolean
Whether to log only errors.
Required: true
Default: true
options.logger.DiskThrashPeriod integer
The period in milliseconds with which to flush the file buffer to ensure it's committed to the hard drive. Note: This is a performance consideration to prevent writing to disk too frequently.
Required: true
Default: 1000
options.logger.FileSizeLimit integer
The file size at which to create new files.
Required: true
Default: 1000000
options.logger.StartLogFormat string
The initial string to put into the log file when opened for the first time.
Required: true
Default: "**** START LOGGER - @LOG{LOGGERNAME} (@LOG{TIMESTAMP}) ****"
options.logger.EndLogFormat string
The final string to put in the log file when closed.
Required: true
Default: "\n\n**** END LOGGER - @LOG{LOGGERNAME} (@LOG{TIMESTAMP}) ****"
options.logger.LogEntryFormat string
The format to use when writing log entries when errors are not present.
Required: true
Default: "\n\n@LOG{LOGMESSAGE}\n\n"
options.logger.ErrorLogEntryFormat string
The message format used to construct error log entries.
Required: true
Default: "\n\n@LOG{ERRORMESSAGE}\n\n"
options.logger.TimestampFormat string
The format used by the @LOG{TIMESTAMP} variable.
Required: true
Default: "%Y-%m-%d %H-%M-%S%3u"
panel object
Required: true
Default: (not specified; see any element defaults within)
panel.open boolean
Whether to open the front panel immediately when run.
Required: true
Default: true
panel.state enum (string)
The state in which the window will open.
Required: true
Default: "Standard"
Enum Items: "Standard" | "Hidden" | "Closed" | "Minimized" | "Maximized"
panel.transparency integer
The transparency of the window. 0 = opaque, 100 = invisible.
Required: true
Default: 0
panel.title string
The title of the plugin window when it runs. Note that the variable 'instanceName' is provided here in a VAR variable container.
Required: true
Default: "@VAR{instanceName}"
panel.titleBarVisible boolean
Whether the window title bar is visible.
Required: true
Default: true
panel.showMenuBar boolean
Whether the menu bar is visible.
Required: true
Default: false
panel.showToolBar boolean
Whether the toolbar is visible.
Required: true
Default: false
panel.makeActive boolean
Whether the window becomes active when opened.
Required: true
Default: false
panel.bringToFront boolean
Whether the window is brought to the front / top of other windows when opened.
Required: true
Default: false
panel.minimizable boolean
Whether the window is minimizable.
Required: true
Default: true
panel.resizable boolean
Whether the window is resizable.
Required: true
Default: true
panel.closeable boolean
Whether the window is closeable.
Required: true
Default: true
panel.closeWhenDone boolean
Whether to close the window when complete.
Required: true
Default: true
panel.center boolean
Whether to center the window when opened. Note: this property overrides the 'position' property.
Required: true
Default: false
panel.position object
The position of the window when opened the first time.
Required: true
Default: (not specified; see any element defaults within)
panel.position.top integer
The vertical position of the window in pixels from the top edge of the viewport. Note: this property is overriden by the 'center' property.
Required: true
Default: 100
panel.position.left integer
The horizontal position of the window in pixels from the left edge of the viewport. Note: this property is overriden by the 'center' property.
Required: true
Default: 100
panel.size object
The size of the window when opened the first time.
Required: false
Default: (not specified; see any element defaults within)
panel.size.width integer
The width of the window in pixels. -1 means use the default width for the panel. Note that depending on panel features exposed, there may be a limit to how small a panel can become.
Required: true
Default: -1
panel.size.height integer
The height of the window in pixels. -1 means use the default height for the panel. Note that depending on panel features exposed, there may be a limit to how small a panel can become.
Required: true
Default: -1
channel object
The communication channel definition used by this plugin. Note: this section rarely needs modifications. In many cases, the underlying plugin implementation depends on at least some of these settings having the values below. Consult with a JADE expert before making changes to this section if you are unfamiliar with the implications of changes to this section.
Required: true
Default: (not specified; see any element defaults within)
channel.SendBreakTimeout integer
The timeout duration in milliseconds to wait for sending messages.
Required: true
Default: 1000
channel.WaitOnBreakTimeout integer
The timeout duration in milliseconds to wait for receiving messages. Note: -1 means wait indefinitely or until shutdown is signalled.
Required: true
Default: -1
channel.WaitOnShutdownTimeout integer
The timeout duration in milliseconds to wait for shutdown acknowledgment.
Required: true
Default: 2000
channel.ThrowTimeoutErrors boolean
Whether to throw timeout errors vs simply returning a boolean indicating whether a timeout occurred.
Required: true
Default: false
channel.ThrowShutdownUnacknowledgedErrors boolean
Whether to throw 'shutdown unacknowledged' errors.
Required: true
Default: true
channel.QueueSize integer
The size of the underlying communication queue in bytes. Note: -1 means unbounded (i.e. grow as needed with available memory).
Required: true
Default: -1
channel.SendBreakEnqueueType enum (string)
The enqueue strategy employed on the underlying queue for standard messages.
Required: true
Default: "Enqueue"
Enum Items: "Enqueue" | "EnqueueAtFront" | "LossyEnqueue" | "LossyEnqueueAtFront"
channel.SendErrorEnqueueType enum (string)
The enqueue strategy employed on the underlying queue for error messages.
Required: true
Default: "Enqueue"
Enum Items: "Enqueue" | "EnqueueAtFront" | "LossyEnqueue" | "LossyEnqueueAtFront"
channel.SendShutdownEnqueueType enum (string)
The enqueue strategy employed on the underlying queue for the shutdown message.
Required: true
Default: "LossyEnqueueAtFront"
Enum Items: "Enqueue" | "EnqueueAtFront" | "LossyEnqueue" | "LossyEnqueueAtFront"
channel.FlushQueueBeforeWaitingOnBreak boolean
Whether to flush the queue upon waiting for new messages (i.e. whether to clear the queue and wait for the next 'new' message; this has the effect of removing old messages and waiting for the next message.
Required: true
Default: false
channel.FlushQueueAfterBreaking boolean
Whether to flush the queue after receiving a new message (i.e. whether to handle the next message coming in the queue and then flush; this has the effect of handling the oldest message (if it exsits) or the next message before flushing the queue.
Required: true
Default: false