@tak-ps/node-cot 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.json +54 -0
- package/.github/workflows/npm-publish.yml +16 -0
- package/LICENSE +21 -0
- package/README.md +30 -0
- package/assets/contact.proto +14 -0
- package/assets/cotevent.proto +45 -0
- package/assets/detail.proto +78 -0
- package/assets/group.proto +13 -0
- package/assets/precisionlocation.proto +13 -0
- package/assets/status.proto +12 -0
- package/assets/takcontrol.proto +24 -0
- package/assets/takmessage.proto +18 -0
- package/assets/takv.proto +15 -0
- package/assets/track.proto +13 -0
- package/index.js +7 -0
- package/package.json +39 -0
- package/scripts/convert.js +29 -0
- package/scripts/parse.js +34 -0
- package/src/proto.js +110 -0
- package/src/xml.js +73 -0
- package/test/cot.test.js +245 -0
package/.eslintrc.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "eslint:recommended",
|
|
3
|
+
"root": true,
|
|
4
|
+
"env": {
|
|
5
|
+
"node": true
|
|
6
|
+
},
|
|
7
|
+
"globals": {
|
|
8
|
+
"Map": true,
|
|
9
|
+
"Promise": true,
|
|
10
|
+
"FormData": true,
|
|
11
|
+
"Blob": true
|
|
12
|
+
},
|
|
13
|
+
"parserOptions": {
|
|
14
|
+
"ecmaVersion": 13,
|
|
15
|
+
"sourceType": "module"
|
|
16
|
+
},
|
|
17
|
+
"plugins": [ "node" ],
|
|
18
|
+
"rules": {
|
|
19
|
+
"no-console": 0,
|
|
20
|
+
"arrow-parens": [ "error", "always" ],
|
|
21
|
+
"no-var": "error",
|
|
22
|
+
"prefer-const": "error",
|
|
23
|
+
"array-bracket-spacing": [ "error", "never" ],
|
|
24
|
+
"comma-dangle": [ "error", "never" ],
|
|
25
|
+
"computed-property-spacing": [ "error", "never" ],
|
|
26
|
+
"eol-last": "error",
|
|
27
|
+
"eqeqeq": [ "error", "smart" ],
|
|
28
|
+
"indent": [ "error", 4, { "SwitchCase": 1 } ],
|
|
29
|
+
"no-confusing-arrow": [ "error", { "allowParens": false } ],
|
|
30
|
+
"no-extend-native": "error",
|
|
31
|
+
"no-mixed-spaces-and-tabs": "error",
|
|
32
|
+
"func-call-spacing": [ "error", "never" ],
|
|
33
|
+
"no-trailing-spaces": "error",
|
|
34
|
+
"no-unused-vars": "error",
|
|
35
|
+
"no-use-before-define": [ "error", "nofunc" ],
|
|
36
|
+
"object-curly-spacing": [ "error", "always" ],
|
|
37
|
+
"prefer-arrow-callback": "error",
|
|
38
|
+
"quotes": [ "error", "single", "avoid-escape" ],
|
|
39
|
+
"semi": [ "error", "always" ],
|
|
40
|
+
"space-infix-ops": "error",
|
|
41
|
+
"spaced-comment": [ "error", "always" ],
|
|
42
|
+
"keyword-spacing": [ "error", { "before": true, "after": true } ],
|
|
43
|
+
"template-curly-spacing": [ "error", "never" ],
|
|
44
|
+
"semi-spacing": "error",
|
|
45
|
+
"strict": "error",
|
|
46
|
+
"node/no-missing-require": "error",
|
|
47
|
+
"valid-jsdoc": ["error", {
|
|
48
|
+
"requireParamDescription": false,
|
|
49
|
+
"requireReturnDescription": false,
|
|
50
|
+
"requireReturnType": false,
|
|
51
|
+
"requireReturn": false
|
|
52
|
+
}]
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
on: push
|
|
2
|
+
|
|
3
|
+
jobs:
|
|
4
|
+
publish:
|
|
5
|
+
runs-on: ubuntu-latest
|
|
6
|
+
steps:
|
|
7
|
+
- uses: actions/checkout@v1
|
|
8
|
+
- uses: actions/setup-node@v1
|
|
9
|
+
with:
|
|
10
|
+
node-version: 14
|
|
11
|
+
- run: npm install
|
|
12
|
+
- run: npm test
|
|
13
|
+
- uses: JS-DevTools/npm-publish@v1
|
|
14
|
+
with:
|
|
15
|
+
token: ${{ secrets.NPM_TOKEN }}
|
|
16
|
+
access: "public"
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2021 Vidterra
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<h1 align=center>node-CoT</h1>
|
|
2
|
+
|
|
3
|
+
<p align=center>Javascript Cursor-On-Target Library</p>
|
|
4
|
+
|
|
5
|
+
Lightweight JavaScript library for parsing and manipulating TAK messages, primarily Cursor-on-Target (COT)
|
|
6
|
+
|
|
7
|
+
## About
|
|
8
|
+
|
|
9
|
+
tak.js converts between TAK message protocols and a Javascript object/JSON format. This makes it easy to read and write TAK messages in a Node.js application.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
### NPM
|
|
14
|
+
|
|
15
|
+
To install `node-cot` with npm run
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @tak-ps/node-cot
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Usage Examples
|
|
22
|
+
|
|
23
|
+
### Basic Usage
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
import { cot } from '@tak-ps/node-cot';
|
|
27
|
+
const message = '<event version="2.0" uid="ANDROID-deadbeef" type="a-f-G-U-C" how="m-g" time="2021-02-27T20:32:24.771Z" start="2021-02-27T20:32:24.771Z" stale="2021-02-27T20:38:39.771Z"><point lat="1.234567" lon="-3.141592" hae="-25.7" ce="9.9" le="9999999.0"/><detail><takv os="29" version="4.0.0.0 (deadbeef).1234567890-CIV" device="Some Android Device" platform="ATAK-CIV"/><contact xmppUsername="xmpp@host.com" endpoint="*:-1:stcp" callsign="JENNY"/><uid Droid="JENNY"/><precisionlocation altsrc="GPS" geopointsrc="GPS"/><__group role="Team Member" name="Cyan"/><status battery="78"/><track course="80.24833892285461" speed="0.0"/></detail></event>'
|
|
28
|
+
const json = cot.xml2js(message)
|
|
29
|
+
console.log(json)
|
|
30
|
+
```
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
syntax = "proto3";
|
|
2
|
+
option optimize_for = LITE_RUNTIME;
|
|
3
|
+
|
|
4
|
+
package atakmap.commoncommo.protobuf.v1;
|
|
5
|
+
|
|
6
|
+
// All items are required unless otherwise noted!
|
|
7
|
+
// "required" means if they are missing on send, the conversion
|
|
8
|
+
// to the message format will be rejected and fall back to opaque
|
|
9
|
+
// XML representation
|
|
10
|
+
message Contact {
|
|
11
|
+
// Endpoint is optional; if missing/empty do not populate.
|
|
12
|
+
string endpoint = 1; // endpoint=
|
|
13
|
+
string callsign = 2; // callsign=
|
|
14
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
|
|
2
|
+
syntax = "proto3";
|
|
3
|
+
|
|
4
|
+
option optimize_for = LITE_RUNTIME;
|
|
5
|
+
|
|
6
|
+
package atakmap.commoncommo.protobuf.v1;
|
|
7
|
+
|
|
8
|
+
import "detail.proto";
|
|
9
|
+
|
|
10
|
+
// A note about timestamps:
|
|
11
|
+
// Uses "timeMs" units, which is number of milliseconds since
|
|
12
|
+
// 1970-01-01 00:00:00 UTC
|
|
13
|
+
//
|
|
14
|
+
// All items are required unless otherwise noted!
|
|
15
|
+
// "required" means if they are missing in the XML during outbound
|
|
16
|
+
// conversion to protobuf, the message will be
|
|
17
|
+
// rejected
|
|
18
|
+
message CotEvent {
|
|
19
|
+
// <event>
|
|
20
|
+
|
|
21
|
+
string type = 1; // <event type="x">
|
|
22
|
+
|
|
23
|
+
string access = 2; // optional
|
|
24
|
+
string qos = 3; // optional
|
|
25
|
+
string opex = 4; // optional
|
|
26
|
+
|
|
27
|
+
string uid = 5; // <event uid="x">
|
|
28
|
+
uint64 sendTime = 6; // <event time="x"> converted to timeMs
|
|
29
|
+
uint64 startTime = 7; // <event start="x"> converted to timeMs
|
|
30
|
+
uint64 staleTime = 8; // <event stale="x"> converted to timeMs
|
|
31
|
+
string how = 9; // <event how="x">
|
|
32
|
+
|
|
33
|
+
// <point>
|
|
34
|
+
double lat = 10; // <point lat="x">
|
|
35
|
+
double lon = 11; // <point lon="x">
|
|
36
|
+
double hae = 12; // <point hae="x"> use 999999 for unknown
|
|
37
|
+
double ce = 13; // <point ce="x"> use 999999 for unknown
|
|
38
|
+
double le = 14; // <point ce="x"> use 999999 for unknown
|
|
39
|
+
|
|
40
|
+
// comprises children of <detail>
|
|
41
|
+
// This is optional - if omitted, then the cot message
|
|
42
|
+
// had no data under <detail>
|
|
43
|
+
Detail detail = 15;
|
|
44
|
+
}
|
|
45
|
+
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
syntax = "proto3";
|
|
2
|
+
option optimize_for = LITE_RUNTIME;
|
|
3
|
+
|
|
4
|
+
package atakmap.commoncommo.protobuf.v1;
|
|
5
|
+
|
|
6
|
+
import "contact.proto";
|
|
7
|
+
import "group.proto";
|
|
8
|
+
import "precisionlocation.proto";
|
|
9
|
+
import "status.proto";
|
|
10
|
+
import "takv.proto";
|
|
11
|
+
import "track.proto";
|
|
12
|
+
|
|
13
|
+
// CotEvent detail
|
|
14
|
+
// The strong typed message fields are optional. If used, they *MUST* adhere
|
|
15
|
+
// to the requirements of the message (see their proto file) and
|
|
16
|
+
// their XML source element used to populate the message MUST
|
|
17
|
+
// be omitted from the xmlDetail.
|
|
18
|
+
// WHOLE ELEMENTS MUST BE CONVERTED TO MESSAGES. Do not try to
|
|
19
|
+
// put part of the data from a given element into one of the messages
|
|
20
|
+
// and put other parts of the data in an element of xmlDetail! This applies
|
|
21
|
+
// especially if you add new things to the XML representation which do not
|
|
22
|
+
// have a place in the equivalent protobuf message. Instead, omit the
|
|
23
|
+
// message and put the entire element in xmlDetail!
|
|
24
|
+
//
|
|
25
|
+
// xmlDetail is optional. If omitted, all Detail data has been
|
|
26
|
+
// converted to the strongly typed message fields.
|
|
27
|
+
// If present, this contains any remaining detail data that has NOT been
|
|
28
|
+
// included in one of the strongly typed message fields. To process the
|
|
29
|
+
// xmlDetail, the following rules MUST be followed:
|
|
30
|
+
// Senders of this message MUST:
|
|
31
|
+
// 1. Remove child elements used to populate the other message
|
|
32
|
+
// fields. If the same child element appears more times than an
|
|
33
|
+
// associated message field(s) is intended to encompass, or if any
|
|
34
|
+
// error occurs mapping to the message equivalent, do not remove
|
|
35
|
+
// the element(s) in question and do not populate the message
|
|
36
|
+
// equivalent.
|
|
37
|
+
// 2. If no data under <detail> remains, STOP - do not populate
|
|
38
|
+
// xmlDetail
|
|
39
|
+
// 3. Serialize the remaining XML tree under <detail>....</detail>
|
|
40
|
+
// as XML in UTF-8 encoding
|
|
41
|
+
// 4. Remove the <detail> and </detail> element tags
|
|
42
|
+
// 5. Remove the XML header
|
|
43
|
+
// 6. Place the result in xmlDetail
|
|
44
|
+
// Receivers of this message MUST do the equivalent of the following:
|
|
45
|
+
// 1. If the field is not present (zero length), stop - do nothing
|
|
46
|
+
// 2. Prepend <detail> and append </detail>
|
|
47
|
+
// 3. Prepend an XML header for UTF-8 encoding, version 1.0
|
|
48
|
+
// (<?xml version="1.0" encoding="UTF-8"?> or similar)
|
|
49
|
+
// 4. Read the result, expecting a valid XML document with a document
|
|
50
|
+
// root of <detail>
|
|
51
|
+
// 5. Merge in XML equivalents of each of the strongly typed
|
|
52
|
+
// messages present in this Detail message.
|
|
53
|
+
// In the event that a sending application does not follow
|
|
54
|
+
// sending rule #1 above properly and data for the same element
|
|
55
|
+
// appears in xmlDetail, the data in xmlDetail should be left alone
|
|
56
|
+
// and the data in the equivalent message should ignored.
|
|
57
|
+
|
|
58
|
+
message Detail {
|
|
59
|
+
string xmlDetail = 1;
|
|
60
|
+
|
|
61
|
+
// <contact>
|
|
62
|
+
Contact contact = 2;
|
|
63
|
+
|
|
64
|
+
// <__group>
|
|
65
|
+
Group group = 3;
|
|
66
|
+
|
|
67
|
+
// <precisionlocation>
|
|
68
|
+
PrecisionLocation precisionLocation = 4;
|
|
69
|
+
|
|
70
|
+
// <status>
|
|
71
|
+
Status status = 5;
|
|
72
|
+
|
|
73
|
+
// <takv>
|
|
74
|
+
Takv takv = 6;
|
|
75
|
+
|
|
76
|
+
// <track>
|
|
77
|
+
Track track = 7;
|
|
78
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
syntax = "proto3";
|
|
2
|
+
option optimize_for = LITE_RUNTIME;
|
|
3
|
+
|
|
4
|
+
package atakmap.commoncommo.protobuf.v1;
|
|
5
|
+
|
|
6
|
+
// All items are required unless otherwise noted!
|
|
7
|
+
// "required" means if they are missing on send, the conversion
|
|
8
|
+
// to the message format will be rejected and fall back to opaque
|
|
9
|
+
// XML representation
|
|
10
|
+
message Group {
|
|
11
|
+
string name = 1; // name=
|
|
12
|
+
string role = 2; // role=
|
|
13
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
syntax = "proto3";
|
|
2
|
+
option optimize_for = LITE_RUNTIME;
|
|
3
|
+
|
|
4
|
+
package atakmap.commoncommo.protobuf.v1;
|
|
5
|
+
|
|
6
|
+
// All items are required unless otherwise noted!
|
|
7
|
+
// "required" means if they are missing on send, the conversion
|
|
8
|
+
// to the message format will be rejected and fall back to opaque
|
|
9
|
+
// XML representation
|
|
10
|
+
message PrecisionLocation {
|
|
11
|
+
string geopointsrc = 1; // geopointsrc=
|
|
12
|
+
string altsrc = 2; // altsrc=
|
|
13
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
syntax = "proto3";
|
|
2
|
+
option optimize_for = LITE_RUNTIME;
|
|
3
|
+
|
|
4
|
+
package atakmap.commoncommo.protobuf.v1;
|
|
5
|
+
|
|
6
|
+
// All items are required unless otherwise noted!
|
|
7
|
+
// "required" means if they are missing on send, the conversion
|
|
8
|
+
// to the message format will be rejected and fall back to opaque
|
|
9
|
+
// XML representation
|
|
10
|
+
message Status {
|
|
11
|
+
uint32 battery = 1; // battery=
|
|
12
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
syntax = "proto3";
|
|
2
|
+
option optimize_for = LITE_RUNTIME;
|
|
3
|
+
|
|
4
|
+
package atakmap.commoncommo.protobuf.v1;
|
|
5
|
+
|
|
6
|
+
// TAK Protocol control message
|
|
7
|
+
// This specifies to a recipient what versions
|
|
8
|
+
// of protocol elements this sender supports during
|
|
9
|
+
// decoding.
|
|
10
|
+
message TakControl {
|
|
11
|
+
// Lowest TAK protocol version supported
|
|
12
|
+
// If not filled in (reads as 0), version 1 is assumed
|
|
13
|
+
uint32 minProtoVersion = 1;
|
|
14
|
+
|
|
15
|
+
// Highest TAK protocol version supported
|
|
16
|
+
// If not filled in (reads as 0), version 1 is assumed
|
|
17
|
+
uint32 maxProtoVersion = 2;
|
|
18
|
+
|
|
19
|
+
// UID of the sending contact. May be omitted if
|
|
20
|
+
// this message is paired in a TakMessage with a CotEvent
|
|
21
|
+
// and the CotEvent contains this information
|
|
22
|
+
string contactUid = 3;
|
|
23
|
+
}
|
|
24
|
+
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
syntax = "proto3";
|
|
2
|
+
option optimize_for = LITE_RUNTIME;
|
|
3
|
+
|
|
4
|
+
import "cotevent.proto";
|
|
5
|
+
import "takcontrol.proto";
|
|
6
|
+
|
|
7
|
+
package atakmap.commoncommo.protobuf.v1;
|
|
8
|
+
|
|
9
|
+
// Top level message sent for TAK Messaging Protocol Version 1.
|
|
10
|
+
message TakMessage {
|
|
11
|
+
// Optional - if omitted, continue using last reported control
|
|
12
|
+
// information
|
|
13
|
+
TakControl takControl = 1;
|
|
14
|
+
|
|
15
|
+
// Optional - if omitted, no event data in this message
|
|
16
|
+
CotEvent cotEvent = 2;
|
|
17
|
+
}
|
|
18
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
syntax = "proto3";
|
|
2
|
+
option optimize_for = LITE_RUNTIME;
|
|
3
|
+
|
|
4
|
+
package atakmap.commoncommo.protobuf.v1;
|
|
5
|
+
|
|
6
|
+
// All items are required unless otherwise noted!
|
|
7
|
+
// "required" means if they are missing on send, the conversion
|
|
8
|
+
// to the message format will be rejected and fall back to opaque
|
|
9
|
+
// XML representation
|
|
10
|
+
message Takv {
|
|
11
|
+
string device = 1; // device=
|
|
12
|
+
string platform = 2; // platform=
|
|
13
|
+
string os = 3; // os=
|
|
14
|
+
string version = 4; // version=
|
|
15
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
syntax = "proto3";
|
|
2
|
+
option optimize_for = LITE_RUNTIME;
|
|
3
|
+
|
|
4
|
+
package atakmap.commoncommo.protobuf.v1;
|
|
5
|
+
|
|
6
|
+
// All items are required unless otherwise noted!
|
|
7
|
+
// "required" means if they are missing on send, the conversion
|
|
8
|
+
// to the message format will be rejected and fall back to opaque
|
|
9
|
+
// XML representation
|
|
10
|
+
message Track {
|
|
11
|
+
double speed = 1; // speed=
|
|
12
|
+
double course = 2; // course=
|
|
13
|
+
}
|
package/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tak-ps/node-cot",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"description": "Lightweight JavaScript library for parsing and manipulating TAK messages",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "tape test/**.test.js",
|
|
9
|
+
"lint": "eslint *.js src/*.js test/*.js"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"protobufjs": "^7.1.2",
|
|
13
|
+
"xml-js": "^1.6.11"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"eslint": "^8.28.0",
|
|
17
|
+
"eslint-plugin-node": "^11.1.0",
|
|
18
|
+
"tape": "^5.6.1"
|
|
19
|
+
},
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/tak-ps/node-cot.git"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"tak",
|
|
26
|
+
"atak",
|
|
27
|
+
"wintak",
|
|
28
|
+
"cot",
|
|
29
|
+
"cusor",
|
|
30
|
+
"target",
|
|
31
|
+
"tactical"
|
|
32
|
+
],
|
|
33
|
+
"author": "Vidterra LLC",
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"bugs": {
|
|
36
|
+
"url": "https://github.com/tak-ps/node-cot/issues"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://github.com/tak-ps/node-cot#readme"
|
|
39
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const path = require('path')
|
|
2
|
+
const {cot, proto} = require(path.join(__dirname, '../index.js'))
|
|
3
|
+
|
|
4
|
+
const run = (message) => {
|
|
5
|
+
if (!message) {
|
|
6
|
+
console.error('Enter a TAK message')
|
|
7
|
+
return
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const bufferMessage = typeof message !== Buffer ? Buffer.from(message, 'hex') : message
|
|
11
|
+
|
|
12
|
+
if (bufferMessage[0] === 191) { // TAK message format 0xbf
|
|
13
|
+
console.log('TAK message received')
|
|
14
|
+
const trimmedBuffer = bufferMessage.slice(3, bufferMessage.length) // remove tak message header from content
|
|
15
|
+
if (bufferMessage[1] === 0) { // is COT XML
|
|
16
|
+
console.error('Enter a TAK proto message')
|
|
17
|
+
} else if (bufferMessage[1] === 1) { // is Protobuf
|
|
18
|
+
console.log('TAK protobuf format')
|
|
19
|
+
const protoMessage = proto.proto2js(trimmedBuffer)
|
|
20
|
+
const cotMessage = proto.protojs2cotjs(protoMessage)
|
|
21
|
+
console.log(cotMessage)
|
|
22
|
+
console.log(cot.js2xml(cotMessage))
|
|
23
|
+
}
|
|
24
|
+
} else { // not TAK message format
|
|
25
|
+
console.error('Enter a TAK proto message')
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
run(process.argv[2])
|
package/scripts/parse.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const path = require('path')
|
|
2
|
+
const {cot, proto} = require(path.join(__dirname, '../index.js'))
|
|
3
|
+
|
|
4
|
+
// https://github.com/deptofdefense/AndroidTacticalAssaultKit-CIV/blob/master/commoncommo/core/impl/protobuf/protocol.txt
|
|
5
|
+
|
|
6
|
+
const run = (message) => {
|
|
7
|
+
if (!message) {
|
|
8
|
+
console.error('Enter a TAK message')
|
|
9
|
+
return
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const bufferMessage = typeof message !== Buffer ? Buffer.from(message, 'hex') : message
|
|
13
|
+
|
|
14
|
+
if (bufferMessage[0] === 191) { // TAK message format 0xbf
|
|
15
|
+
console.log('TAK message received')
|
|
16
|
+
const trimmedBuffer = bufferMessage.slice(3, bufferMessage.length) // remove tak message header from content
|
|
17
|
+
if (bufferMessage[1] === 0) { // is COT XML
|
|
18
|
+
console.log('COT XML format')
|
|
19
|
+
console.log(cot.xml2js(trimmedBuffer)) // try parsing raw XML
|
|
20
|
+
} else if (bufferMessage[1] === 1) { // is Protobuf
|
|
21
|
+
console.log('TAK protobuf format')
|
|
22
|
+
console.log(proto.proto2js(trimmedBuffer))
|
|
23
|
+
}
|
|
24
|
+
} else { // not TAK message format
|
|
25
|
+
try {
|
|
26
|
+
console.log('COT XML received')
|
|
27
|
+
console.log(cot.xml2js(message)) // try parsing raw XML
|
|
28
|
+
} catch (e) {
|
|
29
|
+
console.error('Failed to parse message', e)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
run(process.argv[2])
|
package/src/proto.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import protobuf from 'protobufjs';
|
|
3
|
+
import xmljs from 'xml-js';
|
|
4
|
+
|
|
5
|
+
const root = protobuf.loadSync(path.join(__dirname, '../assets/takmessage.proto'));
|
|
6
|
+
const TakMessage = root.lookupType('atakmap.commoncommo.protobuf.v1.TakMessage');
|
|
7
|
+
|
|
8
|
+
export default class Proto {
|
|
9
|
+
static proto2js(message) {
|
|
10
|
+
if (typeof message === 'undefined' || message === null) {
|
|
11
|
+
throw new Error('Attempted to parse empty TAK proto message');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (typeof message !== Buffer) {
|
|
15
|
+
message = Buffer.from(message, 'hex');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const result = TakMessage.decode(message);
|
|
19
|
+
return TakMessage.toObject(result, {
|
|
20
|
+
longs: String
|
|
21
|
+
}); // or decode
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
static protojs2cotjs(proto) {
|
|
25
|
+
if (!proto.cotEvent) return null;
|
|
26
|
+
|
|
27
|
+
const cot = {
|
|
28
|
+
'event': {
|
|
29
|
+
'_attributes': {
|
|
30
|
+
'version': '2.0',
|
|
31
|
+
'uid': proto.cotEvent.uid,
|
|
32
|
+
'type': proto.cotEvent.type,
|
|
33
|
+
'time': new Date(parseInt(proto.cotEvent.sendTime)).toISOString(),
|
|
34
|
+
'start': new Date(parseInt(proto.cotEvent.startTime)).toISOString(),
|
|
35
|
+
'stale': new Date(parseInt(proto.cotEvent.staleTime)).toISOString(),
|
|
36
|
+
'how': proto.cotEvent.how
|
|
37
|
+
},
|
|
38
|
+
'point': {
|
|
39
|
+
'_attributes': {
|
|
40
|
+
'lat': proto.cotEvent.lat,
|
|
41
|
+
'lon': proto.cotEvent.lon,
|
|
42
|
+
'hae': proto.cotEvent.hae,
|
|
43
|
+
'ce': proto.cotEvent.ce,
|
|
44
|
+
'le': proto.cotEvent.le
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
if (proto.cotEvent.detail) {
|
|
50
|
+
cot.event.detail = {};
|
|
51
|
+
if (proto.cotEvent.detail.takv) {
|
|
52
|
+
cot.event.detail.takv = {
|
|
53
|
+
'_attributes': {
|
|
54
|
+
'os': proto.cotEvent.detail.takv.os,
|
|
55
|
+
'version': proto.cotEvent.detail.takv.version,
|
|
56
|
+
'device': proto.cotEvent.detail.takv.device,
|
|
57
|
+
'platform': proto.cotEvent.detail.takv.platform
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
if (proto.cotEvent.detail.contact) {
|
|
62
|
+
cot.event.detail.contact = {
|
|
63
|
+
'_attributes': {
|
|
64
|
+
'endpoint': proto.cotEvent.detail.contact.endpoint,
|
|
65
|
+
'callsign': proto.cotEvent.detail.contact.callsign
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
// todo add phone
|
|
69
|
+
}
|
|
70
|
+
if (proto.cotEvent.detail.xmlDetail) {
|
|
71
|
+
const result = xmljs.xml2js(proto.cotEvent.detail.xmlDetail, { compact: true });
|
|
72
|
+
cot.event.detail = {
|
|
73
|
+
...cot.event.detail,
|
|
74
|
+
...result
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (proto.cotEvent.detail.group) {
|
|
78
|
+
cot.event.detail.__group = {
|
|
79
|
+
'_attributes': {
|
|
80
|
+
'role': proto.cotEvent.detail.group.role,
|
|
81
|
+
'name': proto.cotEvent.detail.group.name
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
if (proto.cotEvent.detail.status) {
|
|
86
|
+
cot.event.detail.status = {
|
|
87
|
+
'_attributes': {
|
|
88
|
+
'battery': proto.cotEvent.detail.status.battery
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/*
|
|
93
|
+
"precisionlocation": {
|
|
94
|
+
"_attributes": {
|
|
95
|
+
"altsrc": "GPS",
|
|
96
|
+
"geopointsrc": "GPS"
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
"track": {
|
|
101
|
+
"_attributes": {
|
|
102
|
+
"course": "228.71039582198793",
|
|
103
|
+
"speed": "0.0"
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}*/
|
|
107
|
+
}
|
|
108
|
+
return cot;
|
|
109
|
+
}
|
|
110
|
+
}
|
package/src/xml.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import xmljs from 'xml-js';
|
|
2
|
+
|
|
3
|
+
export default class XMLCot {
|
|
4
|
+
static js2xml(js) {
|
|
5
|
+
if (typeof js === 'undefined' || !js) {
|
|
6
|
+
throw new Error('Attempted to parse empty Object');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
return xmljs.js2xml(js, { compact: true });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// accepts an Object decoded with xml2js.decodeType(type)
|
|
13
|
+
static encodeType(type) {
|
|
14
|
+
let result = type.atom;
|
|
15
|
+
if (type.descriptor) {
|
|
16
|
+
result += `-${type.descriptor}`;
|
|
17
|
+
}
|
|
18
|
+
if (type.domain) {
|
|
19
|
+
result += `-${type.domain}`;
|
|
20
|
+
}
|
|
21
|
+
if (type.milstd.length > 0) {
|
|
22
|
+
result += `-${type.milstd.join('-')}`;
|
|
23
|
+
}
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
static jsDate2cot(unix) {
|
|
28
|
+
return new Date(unix).toISOString();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
static xml2js(cot) {
|
|
32
|
+
if (typeof cot === 'undefined' || cot === null) {
|
|
33
|
+
throw new Error('Attempted to parse empty COT message');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (typeof cot === 'object') { // accept a data buffer or string for conversion
|
|
37
|
+
cot = cot.toString();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return xmljs.xml2js(cot, { compact: true });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// accepts a string formatted like 'a-f-G-U-C-I'
|
|
44
|
+
static decodeType(type) {
|
|
45
|
+
const split = type.split('-');
|
|
46
|
+
const atom = split[0];
|
|
47
|
+
const descriptor = split[1] || null;
|
|
48
|
+
const domain = split[2] || null;
|
|
49
|
+
const milstd = split.slice(3);
|
|
50
|
+
return {
|
|
51
|
+
type,
|
|
52
|
+
atom,
|
|
53
|
+
descriptor,
|
|
54
|
+
domain,
|
|
55
|
+
milstd
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
static cotDate2js(iso) {
|
|
60
|
+
return Date.parse(iso);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// convert a decoded point from xml2js(cot) from String to Numbers
|
|
64
|
+
static parsePoint(point) {
|
|
65
|
+
return {
|
|
66
|
+
lat: parseFloat(point.lat),
|
|
67
|
+
lon: parseFloat(point.lon),
|
|
68
|
+
hae: parseFloat(point.hae),
|
|
69
|
+
ce: parseFloat(point.ce),
|
|
70
|
+
le: parseFloat(point.le)
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
package/test/cot.test.js
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import test from 'tape';
|
|
2
|
+
import XML from '../src/xml.js';
|
|
3
|
+
|
|
4
|
+
test('Decode COT message', (t) => {
|
|
5
|
+
const packet = '<event version="2.0" uid="ANDROID-deadbeef" type="a-f-G-U-C" how="m-g" time="2021-02-27T20:32:24.771Z" start="2021-02-27T20:32:24.771Z" stale="2021-02-27T20:38:39.771Z"><point lat="1.234567" lon="-3.141592" hae="-25.7" ce="9.9" le="9999999.0"/><detail><takv os="29" version="4.0.0.0 (deadbeef).1234567890-CIV" device="Some Android Device" platform="ATAK-CIV"/><contact xmppUsername="xmpp@host.com" endpoint="*:-1:stcp" callsign="JENNY"/><uid Droid="JENNY"/><precisionlocation altsrc="GPS" geopointsrc="GPS"/><__group role="Team Member" name="Cyan"/><status battery="78"/><track course="80.24833892285461" speed="0.0"/></detail></event>';
|
|
6
|
+
|
|
7
|
+
t.deepEquals(XML.xml2js(packet), {
|
|
8
|
+
'event': {
|
|
9
|
+
'_attributes': {
|
|
10
|
+
'version': '2.0',
|
|
11
|
+
'uid': 'ANDROID-deadbeef',
|
|
12
|
+
'type': 'a-f-G-U-C',
|
|
13
|
+
'how': 'm-g',
|
|
14
|
+
'time': '2021-02-27T20:32:24.771Z',
|
|
15
|
+
'start': '2021-02-27T20:32:24.771Z',
|
|
16
|
+
'stale': '2021-02-27T20:38:39.771Z'
|
|
17
|
+
},
|
|
18
|
+
'point': {
|
|
19
|
+
'_attributes': {
|
|
20
|
+
'lat': '1.234567',
|
|
21
|
+
'lon': '-3.141592',
|
|
22
|
+
'hae': '-25.7',
|
|
23
|
+
'ce': '9.9',
|
|
24
|
+
'le': '9999999.0'
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
'detail': {
|
|
28
|
+
'takv': {
|
|
29
|
+
'_attributes': {
|
|
30
|
+
'os': '29',
|
|
31
|
+
'version': '4.0.0.0 (deadbeef).1234567890-CIV',
|
|
32
|
+
'device': 'Some Android Device',
|
|
33
|
+
'platform': 'ATAK-CIV'
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
'contact': {
|
|
37
|
+
'_attributes': {
|
|
38
|
+
'xmppUsername': 'xmpp@host.com',
|
|
39
|
+
'endpoint': '*:-1:stcp',
|
|
40
|
+
'callsign': 'JENNY'
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
'uid': { '_attributes': { 'Droid': 'JENNY' } },
|
|
44
|
+
'precisionlocation': { '_attributes': { 'altsrc': 'GPS', 'geopointsrc': 'GPS' } },
|
|
45
|
+
'__group': { '_attributes': { 'role': 'Team Member', 'name': 'Cyan' } },
|
|
46
|
+
'status': { '_attributes': { 'battery': '78' } },
|
|
47
|
+
'track': { '_attributes': { 'course': '80.24833892285461', 'speed': '0.0' } }
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
t.end();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('Decode COT message', (t) => {
|
|
56
|
+
const packet = '<event version="2.0" uid="TEST-deadbeef" type="a" how="m-g" time="2021-03-12T15:49:07.138Z" start="2021-03-12T15:49:07.138Z" stale="2021-03-12T15:49:07.138Z"><point lat="0.000000" lon="0.000000" hae="0.0" ce="9999999.0" le="9999999.0"/><detail><takv os="Android" version="10" device="Some Device" platform="python unittest"/><status battery="83"/><uid Droid="JENNY"/><contact callsign="JENNY" endpoint="*:-1:stcp" phone="800-867-5309"/><__group role="Team Member" name="Cyan"/><track course="90.1" speed="10.3"/></detail></event>';
|
|
57
|
+
|
|
58
|
+
t.deepEquals(XML.xml2js(packet), {
|
|
59
|
+
'event': {
|
|
60
|
+
'_attributes': {
|
|
61
|
+
'version': '2.0',
|
|
62
|
+
'uid': 'TEST-deadbeef',
|
|
63
|
+
'type': 'a',
|
|
64
|
+
'how': 'm-g',
|
|
65
|
+
'time': '2021-03-12T15:49:07.138Z',
|
|
66
|
+
'start': '2021-03-12T15:49:07.138Z',
|
|
67
|
+
'stale': '2021-03-12T15:49:07.138Z'
|
|
68
|
+
},
|
|
69
|
+
'point': {
|
|
70
|
+
'_attributes': {
|
|
71
|
+
'lat': '0.000000',
|
|
72
|
+
'lon': '0.000000',
|
|
73
|
+
'hae': '0.0',
|
|
74
|
+
'ce': '9999999.0',
|
|
75
|
+
'le': '9999999.0'
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
'detail': {
|
|
79
|
+
'takv': {
|
|
80
|
+
'_attributes': {
|
|
81
|
+
'os': 'Android',
|
|
82
|
+
'version': '10',
|
|
83
|
+
'device': 'Some Device',
|
|
84
|
+
'platform': 'python unittest'
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
'status': {
|
|
88
|
+
'_attributes': {
|
|
89
|
+
'battery': '83'
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
'uid': {
|
|
93
|
+
'_attributes': {
|
|
94
|
+
'Droid': 'JENNY'
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
'contact': {
|
|
98
|
+
'_attributes': {
|
|
99
|
+
'callsign': 'JENNY',
|
|
100
|
+
'endpoint': '*:-1:stcp',
|
|
101
|
+
'phone': '800-867-5309'
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
'__group': {
|
|
105
|
+
'_attributes': {
|
|
106
|
+
'role': 'Team Member',
|
|
107
|
+
'name': 'Cyan'
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
'track': {
|
|
111
|
+
'_attributes': {
|
|
112
|
+
'course': '90.1',
|
|
113
|
+
'speed': '10.3'
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
t.end();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('Encode COT message', (t) => {
|
|
124
|
+
const packet = {
|
|
125
|
+
'event': {
|
|
126
|
+
'_attributes': {
|
|
127
|
+
'version': '2.0',
|
|
128
|
+
'uid': 'ANDROID-deadbeef',
|
|
129
|
+
'type': 'a-f-G-U-C',
|
|
130
|
+
'how': 'm-g',
|
|
131
|
+
'time': '2021-02-27T20:32:24.771Z',
|
|
132
|
+
'start': '2021-02-27T20:32:24.771Z',
|
|
133
|
+
'stale': '2021-02-27T20:38:39.771Z'
|
|
134
|
+
},
|
|
135
|
+
'point': {
|
|
136
|
+
'_attributes': {
|
|
137
|
+
'lat': '1.234567',
|
|
138
|
+
'lon': '-3.141592',
|
|
139
|
+
'hae': '-25.7',
|
|
140
|
+
'ce': '9.9',
|
|
141
|
+
'le': '9999999.0'
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
'detail': {
|
|
145
|
+
'takv': {
|
|
146
|
+
'_attributes': {
|
|
147
|
+
'os': '29',
|
|
148
|
+
'version': '4.0.0.0 (deadbeef).1234567890-CIV',
|
|
149
|
+
'device': 'Some Android Device',
|
|
150
|
+
'platform': 'ATAK-CIV'
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
'contact': {
|
|
154
|
+
'_attributes': {
|
|
155
|
+
'xmppUsername': 'xmpp@host.com',
|
|
156
|
+
'endpoint': '*:-1:stcp',
|
|
157
|
+
'callsign': 'JENNY'
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
'uid': { '_attributes': { 'Droid': 'JENNY' } },
|
|
161
|
+
'precisionlocation': { '_attributes': { 'altsrc': 'GPS', 'geopointsrc': 'GPS' } },
|
|
162
|
+
'__group': { '_attributes': { 'role': 'Team Member', 'name': 'Cyan' } },
|
|
163
|
+
'status': { '_attributes': { 'battery': '78' } },
|
|
164
|
+
'track': { '_attributes': { 'course': '80.24833892285461', 'speed': '0.0' } }
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
t.deepEquals(
|
|
170
|
+
XML.js2xml(packet),
|
|
171
|
+
'<event version="2.0" uid="ANDROID-deadbeef" type="a-f-G-U-C" how="m-g" time="2021-02-27T20:32:24.771Z" start="2021-02-27T20:32:24.771Z" stale="2021-02-27T20:38:39.771Z"><point lat="1.234567" lon="-3.141592" hae="-25.7" ce="9.9" le="9999999.0"/><detail><takv os="29" version="4.0.0.0 (deadbeef).1234567890-CIV" device="Some Android Device" platform="ATAK-CIV"/><contact xmppUsername="xmpp@host.com" endpoint="*:-1:stcp" callsign="JENNY"/><uid Droid="JENNY"/><precisionlocation altsrc="GPS" geopointsrc="GPS"/><__group role="Team Member" name="Cyan"/><status battery="78"/><track course="80.24833892285461" speed="0.0"/></detail></event>'
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
t.end();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test('Parse GeoChat message', (t) => {
|
|
178
|
+
const geochat = '<event version="2.0" uid="GeoChat.ANDROID-deadbeef.JOKER MAN.563040b9-2ac9-4af3-9e01-4cb2b05d98ea" type="b-t-f" how="h-g-i-g-o" time="2021-02-23T22:28:22.191Z" start="2021-02-23T22:28:22.191Z" stale="2021-02-24T22:28:22.191Z">\n' +
|
|
179
|
+
' <point lat="1.234567" lon="-3.141592" hae="-25.8" ce="9.9" le="9999999.0"/>\n' +
|
|
180
|
+
' <detail>\n' +
|
|
181
|
+
' <__chat parent="RootContactGroup" groupOwner="false" chatroom="JOKER MAN" id="ANDROID-cafebabe" senderCallsign="JENNY">\n' +
|
|
182
|
+
' <chatgrp uid0="ANDROID-deadbeef" uid1="ANDROID-cafebabe" id="ANDROID-cafebabe"/>\n' +
|
|
183
|
+
' </__chat>\n' +
|
|
184
|
+
' <link uid="ANDROID-deadbeef" type="a-f-G-U-C" relation="p-p"/>\n' +
|
|
185
|
+
' <remarks source="BAO.F.ATAK.ANDROID-deadbeef" to="ANDROID-cafebabe" time="2021-02-23T22:28:22.191Z">test</remarks>\n' +
|
|
186
|
+
' <__serverdestination destinations="123.45.67.89:4242:tcp:ANDROID-deadbeef"/>\n' +
|
|
187
|
+
' <marti>\n' +
|
|
188
|
+
' <dest callsign="JOKER MAN"/>\n' +
|
|
189
|
+
' </marti>\n' +
|
|
190
|
+
' </detail>\n' +
|
|
191
|
+
'</event>';
|
|
192
|
+
|
|
193
|
+
t.deepEquals(XML.xml2js(geochat), {
|
|
194
|
+
'event': {
|
|
195
|
+
'_attributes': {
|
|
196
|
+
'version': '2.0',
|
|
197
|
+
'uid': 'GeoChat.ANDROID-deadbeef.JOKER MAN.563040b9-2ac9-4af3-9e01-4cb2b05d98ea',
|
|
198
|
+
'type': 'b-t-f',
|
|
199
|
+
'how': 'h-g-i-g-o',
|
|
200
|
+
'time': '2021-02-23T22:28:22.191Z',
|
|
201
|
+
'start': '2021-02-23T22:28:22.191Z',
|
|
202
|
+
'stale': '2021-02-24T22:28:22.191Z'
|
|
203
|
+
},
|
|
204
|
+
'point': {
|
|
205
|
+
'_attributes': {
|
|
206
|
+
'lat': '1.234567',
|
|
207
|
+
'lon': '-3.141592',
|
|
208
|
+
'hae': '-25.8',
|
|
209
|
+
'ce': '9.9',
|
|
210
|
+
'le': '9999999.0'
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
'detail': {
|
|
214
|
+
'__chat': {
|
|
215
|
+
'_attributes': {
|
|
216
|
+
'parent': 'RootContactGroup',
|
|
217
|
+
'groupOwner': 'false',
|
|
218
|
+
'chatroom': 'JOKER MAN',
|
|
219
|
+
'id': 'ANDROID-cafebabe',
|
|
220
|
+
'senderCallsign': 'JENNY'
|
|
221
|
+
},
|
|
222
|
+
'chatgrp': {
|
|
223
|
+
'_attributes': {
|
|
224
|
+
'uid0': 'ANDROID-deadbeef',
|
|
225
|
+
'uid1': 'ANDROID-cafebabe',
|
|
226
|
+
'id': 'ANDROID-cafebabe'
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
'link': { '_attributes': { 'uid': 'ANDROID-deadbeef', 'type': 'a-f-G-U-C', 'relation': 'p-p' } },
|
|
231
|
+
'remarks': {
|
|
232
|
+
'_attributes': {
|
|
233
|
+
'source': 'BAO.F.ATAK.ANDROID-deadbeef',
|
|
234
|
+
'to': 'ANDROID-cafebabe',
|
|
235
|
+
'time': '2021-02-23T22:28:22.191Z'
|
|
236
|
+
}, '_text': 'test'
|
|
237
|
+
},
|
|
238
|
+
'__serverdestination': { '_attributes': { 'destinations': '123.45.67.89:4242:tcp:ANDROID-deadbeef' } },
|
|
239
|
+
'marti': { 'dest': { '_attributes': { 'callsign': 'JOKER MAN' } } }
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
t.end();
|
|
245
|
+
});
|