Engage Cmd Basics - rallytac/pub GitHub Wiki
The basics of engage-cmd
Our engage-cmd
application is a great little app that we use internally at Rally Tactical Systems to test our APIs, conduct load tests, prove out scnearios, and more. While its not a production-quality application [remember that its an internal testing too], we do make it available to partners and customers to use for their own engineering purposes.
So what is it?
Simply put, engage-cmd
is a terminal/command-line application that is statically linked with the Engage Engine library, resulting in a single executable. Its available on Windows, Linux, and Mac right now - i.e. operating systems that offer some kind of terminal interface.
What's needed to run it?
To use engage-cmd
you're minimally going to need:
- The
engage-cmd
executable for your platform - check out https://hq.rallytac.com/builds/ for binaries. - A certificate store - an encrypted file containing X.509 certificates and keys that the underlying Engage Engine within
engage-cmd
will use. - An engine policy file which is a JSON text file containing engine-level policy information such as licensing, global settings, and so on.
- A mission file; also a JSON text file that defines the set of groups/channels that
engage-cmd
will be using.
Here's a quick example:
Let's say that your certificate store is named all-rts-certs.certstore
, your policy file is ep.json
, and your mission file is mi.json
.
Let's further assume all these files are in the same directory.
dev t % ls -lsa
total 24848
0 drwxr-xr-x 6 dev staff 192 May 23 08:31 .
0 drwxr-xr-x 25 dev staff 800 May 23 08:27 ..
16 -rw-r--r-- 1 dev staff 5122 May 23 08:28 all-rts-certs.certstore
24816 -rwxr-xr-x 1 dev staff 12702072 May 16 16:18 engage-cmd
8 -rw-r--r-- 1 dev staff 409 May 23 08:29 ep.json
8 -rw------- 1 dev staff 374 May 23 08:28 mi.json
dev t %
You can download these example configuration files:
- all-rts-certs.certstore - Default RTS certificate store
- ep.json - Example engine policy file
- mi.json - Example mission file
And they look as follows:
all-rts-certs.certstore (a binary file)
Engage CertStore^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@ ....
ep.json
{
"dataDirectory": "./data/${node_id}",
"licensing": {
"entitlement": "",
"key": "",
"activationCode": ""
},
"networking": {
"defaultNic": "en0",
"preventMulticastFailover": true
},
"audio": {
"internalRate": 8000,
"internalChannels": 1
}
}
NOTE: Take note of the
defaultNic
setting in the policy file. In the example the NIC name isen0
- you'll need to change this value to the name of the network interface card on the compuer whereengage-cmd
is running. This is very important for IP multicast because multicast has no routing and therefore the Engage Engine needs to be told which NIC to use for sending and receiving multicast. If you don't specify a network interface, Engage will pick the first multicast-capable NIC it finds - and that might not be the one you want used.
mi.json
{
"id": "a unique id for this mission",
"name": "Sample mission",
"description": "Just a sample mission",
"groups": [
{
"id": "AS-01",
"type": 1,
"name": "AirSupport",
"txAudio": {
"encoder": 25,
"framingMs": 60
},
"tx": {
"address": "237.1.1.1",
"port": 21000
},
"rx": {
"address": "237.1.1.1",
"port": 21000
}
},
{
"id": "C2-MAIN",
"type": 1,
"name": "Primary C2",
"txAudio": {
"encoder": 25,
"framingMs": 60
},
"tx": {
"address": "237.1.1.2",
"port": 21002
},
"rx": {
"address": "237.1.1.2",
"port": 21002
}
}
]
}
To run engage-cmd
with these files, your command line would be:
./engage-cmd -cs:all-rts-certs.certstore -ep:ep.json -mi:mi.json -acj
You should get something like:
sbotha@Shaun-M4 t % ./engage-cmd -cs:all-rts-certs.certstore -ep:ep.json -mi:mi.json -acj
---------------------------------------------------------------------------------
Engage-Cmd version 1.251.9091 [RELEASE] for darwin_arm64
Copyright (c) 2019 Rally Tactical Systems, Inc.
Build time: May 16 2025 @ 15:28:31
Revision: 7720542ea.3922026.3e38fc5
---------------------------------------------------------------------------------
[252592546][3]:[EngageCmd]:crypto engine is NOT FIPS validated
2025-05-23 08:44:39.058 [93729/0x20afd9f00- ecMain] I/CertStore: Loading 'all-rts-certs.certstore'
2025-05-23 08:44:39.068 [93729/0x20afd9f00- ecMain] I/CertStore: loaded 'rtsCA'
2025-05-23 08:44:39.068 [93729/0x20afd9f00- ecMain] I/CertStore: loaded 'rtsFactoryDefaultRpSrv'
2025-05-23 08:44:39.068 [93729/0x20afd9f00- ecMain] I/CertStore: loaded 'rtsFactoryDefaultEngage'
2025-05-23 08:44:39.069 [93729/0x20afd9f00- ecMain] I/====EngageInterface====: Engage Engine version 1.251.9091 [RELEASE] for darwin_arm64 v0.0.0, build time May 16 2025 @ 15:27:08, cpu=16, mdf=256, pid=93729, cwd='/Users/sbotha/Global/shaunwork/rp/t'
2025-05-23 08:44:39.075 [93729/0x20afd9f00- ecMain] I/====EngageInterface====: nic: name='en0' (Wi-Fi), description=IEEE80211, family=2, addresses='192.168.0.107', available=1, loopBack=0, supportsMulticast=1
2025-05-23 08:44:39.076 [93729/0x16d4c3000- apiCallQ] I/Engine: using certstore -enginedefault 'rtsFactoryDefaultEngage'
2025-05-23 08:44:39.076 [93729/0x16d4c3000- apiCallQ] I/Engine: default security certificate:
subject..........: [/C=US/ST=Washington/L=Seattle/O=Rally Tactical Systems, Inc./OU=(c) 2019 Rally Tactical Systems, Inc. - For authorized use only/CN=Engage Factory Default Certificate/[email protected]]
issuer...........: [/C=US/ST=Washington/L=Seattle/O=Rally Tactical Systems, Inc./OU=(c) 2019 Rally Tactical Systems, Inc. - For authorized use only/CN=Rally Tactical Systems Root CA 1/[email protected]]
selfSigned.......: [0]
version..........: [1]
notBefore........: [Sep 7 01:53:06 2019 GMT]
notAfter.........: [Sep 4 01:53:06 2029 GMT]
serial...........: [AD:CB:61:C8:99:4E:21:E1]
fingerprint......: [7D:1A:6C:E6:C0:EC:53:1C:9D:99:20:48:81:E3:C3:87:43:31:C3:26]
2025-05-23 08:44:39.076 [93729/0x16d4c3000- apiCallQ] I/Engine: using certstore -cadefault 'rtsCA'
2025-05-23 08:44:39.076 [93729/0x16d4c3000- apiCallQ] I/Engine: ca certificate:
subject..........: [/C=US/ST=Washington/L=Seattle/O=Rally Tactical Systems, Inc./OU=(c) 2019 Rally Tactical Systems, Inc. - For authorized use only/CN=Rally Tactical Systems Root CA 1/[email protected]]
issuer...........: [/C=US/ST=Washington/L=Seattle/O=Rally Tactical Systems, Inc./OU=(c) 2019 Rally Tactical Systems, Inc. - For authorized use only/CN=Rally Tactical Systems Root CA 1/[email protected]]
selfSigned.......: [1]
version..........: [1]
notBefore........: [Aug 26 23:39:46 2019 GMT]
notAfter.........: [Aug 23 23:39:46 2029 GMT]
serial...........: [AF:E6:1B:51:BC:F5:04:31]
fingerprint......: [FA:C2:3C:B4:82:10:B9:C8:C4:02:DF:8B:6B:10:B8:2E:95:38:64:AE]
2025-05-23 08:44:39.076 [93729/0x16d4c3000- apiCallQ] I/Engine: timeline signing certificate:
subject..........: [/C=US/ST=Washington/L=Seattle/O=Rally Tactical Systems, Inc./OU=(c) 2019 Rally Tactical Systems, Inc. - For authorized use only/CN=Engage Factory Default Certificate/[email protected]]
issuer...........: [/C=US/ST=Washington/L=Seattle/O=Rally Tactical Systems, Inc./OU=(c) 2019 Rally Tactical Systems, Inc. - For authorized use only/CN=Rally Tactical Systems Root CA 1/[email protected]]
selfSigned.......: [0]
version..........: [1]
notBefore........: [Sep 7 01:53:06 2019 GMT]
notAfter.........: [Sep 4 01:53:06 2029 GMT]
serial...........: [AD:CB:61:C8:99:4E:21:E1]
fingerprint......: [7D:1A:6C:E6:C0:EC:53:1C:9D:99:20:48:81:E3:C3:87:43:31:C3:26]
2025-05-23 08:44:39.076 [93729/0x16d4c3000- apiCallQ] I/Engine: all audio set to rate of 8Khz mono
2025-05-23 08:44:39.076 [93729/0x16d4c3000- apiCallQ] I/Engine: using './data/{ef97994c-2bf7-4149-8e41-6f82df4f2707}' as the engine root data directory
2025-05-23 08:44:39.077 [93729/0x16d4c3000- apiCallQ] I/Engine: using './data/{ef97994c-2bf7-4149-8e41-6f82df4f2707}/timelines' as the timeline persistence root
2025-05-23 08:44:39.077 [93729/0x16d4c3000- apiCallQ] W/Engine: limiting max tx/rx due to non-license
Groups:
index=0, type=1, id=AS-01, name=AirSupport, encrypted=no, fullDuplex=no
index=1, type=1, id=C2-MAIN, name=Primary C2, encrypted=no, fullDuplex=no
2025-05-23 08:44:39.115 [93729/0x20afd9f00- ecMain] I/====EngageInterface====: run state transitioning from rsStopped to rsStarting
2025-05-23 08:44:39.116 [93729/0x16d5db000- engineMainQ] I/TimelineManager: timeline manager configured for 1024000000 memory bytes, 1024000000 disk bytes
2025-05-23 08:44:39.116 [93729/0x16d5db000- engineMainQ] I/TimelineManager: rebuilding timeline database due to maintenance flag set by engine
2025-05-23 08:44:39.116 [93729/0x16d5db000- engineMainQ] I/Engine: discovery has been disabled
2025-05-23 08:44:39.116 [93729/0x20afd9f00- ecMain] I/====EngageInterface====: run state transitioning from rsStarting to rsStarted
2025-05-23 08:44:39.116 [93729/0x16d5db000- engineMainQ] W/Engine: limiting max tx/rx due to non-license
2025-05-23 08:44:39.116 [93729/0x16d5db000- engineMainQ] W/Engine: (updateLicense) license invalid - maximum transmit time has been limited
............running >[252592604][4]:[EngageCmd]:on_ENGAGE_ENGINE_STARTED
[252592605][4]:[EngageCmd]:on_ENGAGE_LICENSE_EXPIRED
[252592605][4]:[EngageCmd]:on_ENGAGE_GROUP_CREATED AS-01 - {"groupCreationDetail":{"id":"AS-01","status":1}}
[252592605][4]:[EngageCmd]:on_ENGAGE_GROUP_CREATED C2-MAIN - {"groupCreationDetail":{"id":"C2-MAIN","status":1}}
[252592605][4]:[EngageCmd]:on_ENGAGE_GROUP_JOINED AS-01
[252592605][4]:[EngageCmd]:on_ENGAGE_GROUP_JOINED C2-MAIN
[252592608][4]:[EngageCmd]:on_ENGAGE_GROUP_CONNECTED C2-MAIN, {"groupConnectionDetail":{"asFailover":false,"connectionType":1,"id":"C2-MAIN","peer":"237.1.1.2:21002/237.1.1.2:21002","reason":""}}
[252592608][4]:[EngageCmd]:on_ENGAGE_GROUP_CONNECTED AS-01, {"groupConnectionDetail":{"asFailover":false,"connectionType":1,"id":"AS-01","peer":"237.1.1.1:21000/237.1.1.1:21000","reason":""}}
2025-05-23 08:44:40.117 [93729/0x16d5db000- engineMainQ] W/Engine: (housekeeper) license invalid - maximum transmit time has been limited
[252593605][4]:[EngageCmd]:on_ENGAGE_LICENSE_EXPIRED
............running >
NOTE: You might notice that the log complains about not having a license in this example. That's because there isn't a license in the
ep.json
file. But that's OK because the software will function 100% without a license except for a 3-second limitation on individual audio transmissions.
Now that the app is running, you're presented with a prompt where you can enter commands. There's a whole bunch of these so we won't get into detail but to get a quick idea, type ?
end press [Enter].
That said, we will be using a few commands, so let's get to it.
sg
command
Seeing the list of groups - use the ............running >sg
Groups:
index=0, type=1, id=AS-01, name=AirSupport, encrypted=no, fullDuplex=no
index=1, type=1, id=C2-MAIN, name=Primary C2, encrypted=no, fullDuplex=no
............running >
These are the groups defined in the mi.json
file. Index 0
is the first group and is the Air Support
group while index 1
is the Primary C2
group. [Remember these index numbers!]
b
command
Begin transmitting on a group - use the ............running >b0
............running >2025-05-23 08:53:40.173 [94136/0x16de9b000- engineMainQ] I/OSXMicrophone: using specified hardware id 'BuiltInMicrophoneDevice'
[253133744][4]:[EngageCmd]:on_ENGAGE_GROUP_TX_MUTED AS-01
[253133744][4]:[EngageCmd]:on_ENGAGE_GROUP_TIMELINE_EVENT_STARTED AS-01, {"alias":"APL-0000FFDD7EFB","audio":{"ms":0.0,"samples":0},"direction":2,"groupId":"AS-01","groupName":"AirSupport","id":"{70f23be1-0348-4ccc-ba0e-3bd67da63fbf}","inProgress":true,"nodeId":"{dd6e4daf-74ec-4fe9-a2fb-9da89856afa0}","started":1748015620171,"thisNodeId":"{dd6e4daf-74ec-4fe9-a2fb-9da89856afa0}","type":1,"uri":"file://./data/{dd6e4daf-74ec-4fe9-a2fb-9da89856afa0}/timelines/AS-01/{70f23be1-0348-4ccc-ba0e-3bd67da63fbf}.wav"}
[253133744][4]:[EngageCmd]:on_ENGAGE_GROUP_TX_STARTED AS-01, x={"groupTxDetail":{"id":"AS-01","localPriority":0,"status":1,"txId":0}}
[253133744][4]:[EngageCmd]:on_ENGAGE_GROUP_TX_UNMUTED AS-01
2025-05-23 08:53:43.249 [94136/0x16de9b000- engineMainQ] W/Group: onAudioCapturePreEncode: maximum allowed transmit time reached
[253136741][2]:[EngageCmd]:on_ENGAGE_GROUP_MAX_TX_TIME_EXCEEDED AS-01, x=
[253136749][4]:[EngageCmd]:on_ENGAGE_GROUP_TX_ENDED AS-01, x={"groupTxDetail":{"id":"AS-01","localPriority":0,"status":2,"txId":0}}
[253136749][4]:[EngageCmd]:on_ENGAGE_GROUP_TIMELINE_EVENT_ENDED AS-01, {"alias":"APL-0000FFDD7EFB","audio":{"ms":2850.0,"samples":22800},"direction":2,"ended":1748015623249,"groupId":"AS-01","groupName":"AirSupport","id":"{70f23be1-0348-4ccc-ba0e-3bd67da63fbf}","nodeId":"{dd6e4daf-74ec-4fe9-a2fb-9da89856afa0}","started":1748015620171,"thisNodeId":"{dd6e4daf-74ec-4fe9-a2fb-9da89856afa0}","type":1,"uri":"file://./data/{dd6e4daf-74ec-4fe9-a2fb-9da89856afa0}/timelines/AS-01/{70f23be1-0348-4ccc-ba0e-3bd67da63fbf}.wav"}
See how the index (0
) is incorporated into the b
as b0
? This is how you tell engage-cmd
to begin transmitting on group 0. It follows that to transmit on group 1, the command would be b1
.
e
command
End transmitting on a group - use the ............running >b0
............running >2025-05-23 08:56:40.772 [94242/0x16ff93000- engineMainQ] I/OSXMicrophone: using specified hardware id 'BuiltInMicrophoneDevice'
[253314347][4]:[EngageCmd]:on_ENGAGE_GROUP_TX_MUTED AS-01
[253314347][4]:[EngageCmd]:on_ENGAGE_GROUP_TIMELINE_EVENT_STARTED AS-01, {"alias":"APL-0000DDFB3FFE","audio":{"ms":0.0,"samples":0},"direction":2,"groupId":"AS-01","groupName":"AirSupport","id":"{39f08907-dde1-4806-a890-69c5364528db}","inProgress":true,"nodeId":"{25bff073-8153-42be-8511-450fc4879b29}","started":1748015800770,"thisNodeId":"{25bff073-8153-42be-8511-450fc4879b29}","type":1,"uri":"file://./data/{25bff073-8153-42be-8511-450fc4879b29}/timelines/AS-01/{39f08907-dde1-4806-a890-69c5364528db}.wav"}
[253314347][4]:[EngageCmd]:on_ENGAGE_GROUP_TX_STARTED AS-01, x={"groupTxDetail":{"id":"AS-01","localPriority":0,"status":1,"txId":0}}
[253314347][4]:[EngageCmd]:on_ENGAGE_GROUP_TX_UNMUTED AS-01
............running >
............running >
............running >e0
............running >[253315132][4]:[EngageCmd]:on_ENGAGE_GROUP_TX_ENDED AS-01, x={"groupTxDetail":{"id":"AS-01","localPriority":0,"status":2,"txId":0}}
[253315132][4]:[EngageCmd]:on_ENGAGE_GROUP_TIMELINE_EVENT_ENDED AS-01, {"alias":"APL-0000DDFB3FFE","audio":{"ms":600.0,"samples":4800},"direction":2,"ended":1748015801581,"groupId":"AS-01","groupName":"AirSupport","id":"{39f08907-dde1-4806-a890-69c5364528db}","nodeId":"{25bff073-8153-42be-8511-450fc4879b29}","started":1748015800770,"thisNodeId":"{25bff073-8153-42be-8511-450fc4879b29}","type":1,"uri":"file://./data/{25bff073-8153-42be-8511-450fc4879b29}/timelines/AS-01/{39f08907-dde1-4806-a890-69c5364528db}.wav"}
............running >
Now, if you have another instance of engage-cmd
on another machine or, frankly, any other VoIP application that can produce and consume RTP audio traffic over IP multicast; you can speak back and forth between engage-cmd
and the other app.
Let's use Rallypoints
A significant challenge to what's been described above is that we're using IP multicast in this mission configuration (those tx
and rx
settings for each group.). And multicast can be particularly challenging! With issues like multicast routing, TTL values on packets, switches pruning traffic, firewalls blocking packets, security issues, and whoatnot; IP multicast is a bit of a bear to deal with.
A much cleaner and simpler solution to these challenges is to use our Rallypoint software. A Rallypoint is technically known as a Selective Forwarding Unit (or SFU) who's primary function to to pass traffic (packets) between entities. The entities in this case are instances of engage-cmd
(or any application that uses our Engage Engine.) It accomplishes this by having each entity to connect to it (the Rallypoint) over a mutually authenticated TLS 1.3 tunnel, and then routes traffic based on stream identifiers. The tunnel is a TCP connection and while control-plane traffic always flows over that link, media can do so too or be transported by point-to-point connections over UDP. For now, though, we'll just use TCP for everything to keep things simple.
Setting up a Rallypoint
Just like the engage-cmd
executable, the Rallypoint executable (rallypointd
) for the various platforms we support is available from https://hq.rallytac.com/builds/. Once you've installed it (it takes just a few seconds), you can connect to it assuming you've got the right X.509 certificates in place. With the configuration we're using, the all-rts-certs.certstore
file contains the default RTS certificates recognized and supported by the Rallypoint so we need not concern ourselves with that right now.
The only major configuration item we need to do on the machine hosting the RP is to open TCP port 7443
on the machine's firewall to allow incoming connections to terminate on the RP.
Also, you don't need a cloud server, a seperate machine, or anything fancy like that for the RP. Pretty much any platform will work just fine - including the "client" machine you're already using for engage-cmd
. For example: on your Windows, Linux, or Mac where you already have engage-cmd
installed, also install an RP. Then - one the same machine - you can run multiple instances of engage-cmd
to talk with each other - obviously in seperate terminal windows.
engage-cmd
Using the RP from Remember that mission file (mi.json
), it looks like this:
{
"id": "a unique id for this mission",
"name": "Sample mission",
"description": "Just a sample mission",
"groups": [
{
"id": "AS-01",
"type": 1,
"name": "AirSupport",
"txAudio": {
"encoder": 25,
"framingMs": 60
},
"tx": {
"address": "237.1.1.1",
"port": 21000
},
"rx": {
"address": "237.1.1.1",
"port": 21000
}
},
{
"id": "C2-MAIN",
"type": 1,
"name": "Primary C2",
"txAudio": {
"encoder": 25,
"framingMs": 60
},
"tx": {
"address": "237.1.1.2",
"port": 21002
},
"rx": {
"address": "237.1.1.2",
"port": 21002
}
}
]
}
We can change it to tell engage-cmd
(actually the Engage Engine inside engage-cmd
) to use an RP instead ... like this:
{
"id": "a unique id for this mission",
"name": "Sample mission",
"description": "Just a sample mission",
"groups": [
{
"id": "AS-01",
"type": 1,
"name": "AirSupport",
"txAudio": {
"encoder": 25,
"framingMs": 60
},
"rallypoints": [
{
"id": "My-RP",
"host": {
"address": "192.168.1.42",
"port": 7443
}
}
]
},
{
"id": "C2-MAIN",
"type": 1,
"name": "Primary C2",
"txAudio": {
"encoder": 25,
"framingMs": 60
},
"rallypoints": [
{
"id": "My-RP",
"host": {
"address": "192.168.1.42",
"port": 7443
}
}
]
}
]
}
...OR...
Rather than messing about with our mission file, we can simply tell engage-cmd
to insert the RP information into the configuration at runtime by specifying an RP file which looks like this:
rp.json
{
"id": "My-RP",
"host": {
"address": "192.168.1.42",
"port": 7443
}
}
You can download this example Rallypoint configuration file:
- rp.json - Example Rallypoint configuration file
... and then passing the -rp
command line parameter as below.
./engage-cmd -cs:all-rts-certs.certstore -ep:ep.json -mi:mi.json -acj -rp:rp.json
The logging output on the terminal will look very similar to before but now instead of connecting the groups to the local multicast, the traffic will instead be routed through and by the RP - in this example at 192.168.1.42
. (Obviously you'd need to change the address to the host name or IP address where your RP is installed.)
For a more detailed discussion about Rallypoints, check out https://github.com/rallytac/pub/wiki/Engage-Rallypoints.
PS: There's no need to change the mission file as specifying the -rp
parameter will cause the software to ignore the tx
and rx
multicast information in the group configurations.
END