@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 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
@@ -0,0 +1,7 @@
1
+ import XML from './src/xml.js';
2
+ import Proto from './src/proto.js';
3
+
4
+ export {
5
+ XML,
6
+ Proto
7
+ };
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])
@@ -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
+ }
@@ -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
+ });