incyclist-services 1.0.64 → 1.0.66
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/lib/devices/ride/model.d.ts +3 -0
- package/lib/devices/ride/service.d.ts +1 -0
- package/lib/devices/ride/service.js +75 -7
- package/lib/routes/base/parsers/gpx.d.ts +14 -0
- package/lib/routes/base/parsers/gpx.js +126 -0
- package/lib/routes/base/parsers/xml.d.ts +2 -1
- package/lib/routes/base/parsers/xml.js +13 -7
- package/lib/routes/base/utils/xml.js +11 -6
- package/package.json +2 -2
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
2
|
import { DeviceProperties } from "incyclist-devices/lib/types/device";
|
|
3
3
|
import { AdapterInfo, ExtendedIncyclistCapability } from "../configuration";
|
|
4
|
+
export type HealthStatus = 'green' | 'amber' | 'red';
|
|
4
5
|
export interface AdapterRideInfo extends AdapterInfo {
|
|
5
6
|
isStarted: boolean;
|
|
6
7
|
tsLastData?: number;
|
|
7
8
|
isHealthy?: boolean;
|
|
9
|
+
isRestarting?: boolean;
|
|
10
|
+
dataStatus?: HealthStatus;
|
|
8
11
|
ivToCheck?: NodeJS.Timeout;
|
|
9
12
|
}
|
|
10
13
|
export interface AdapterStateInfo {
|
|
@@ -39,6 +39,7 @@ export declare class DeviceRideService extends EventEmitter {
|
|
|
39
39
|
startAdapters(adapters: AdapterRideInfo[], startType: 'start' | 'check' | 'pair', props?: RideServiceDeviceProperties): Promise<boolean>;
|
|
40
40
|
startHealthCheck(ai: AdapterRideInfo): void;
|
|
41
41
|
stopHealthCheck(ai: AdapterRideInfo): void;
|
|
42
|
+
prepareReconnect(ai: AdapterRideInfo): Promise<void>;
|
|
42
43
|
start(props: RideServiceDeviceProperties): Promise<boolean>;
|
|
43
44
|
startRetry(props: RideServiceDeviceProperties): Promise<boolean>;
|
|
44
45
|
cancelStart(): Promise<boolean>;
|
|
@@ -23,7 +23,8 @@ const gd_eventlog_1 = require("gd-eventlog");
|
|
|
23
23
|
const logging_1 = require("../../utils/logging");
|
|
24
24
|
const incyclist_devices_1 = require("incyclist-devices");
|
|
25
25
|
const timers_1 = require("timers");
|
|
26
|
-
const NO_DATA_THRESHOLD =
|
|
26
|
+
const NO_DATA_THRESHOLD = 10000;
|
|
27
|
+
const UNHEALTHY_THRESHOLD = 60000;
|
|
27
28
|
class DeviceRideService extends events_1.default {
|
|
28
29
|
static getInstance() {
|
|
29
30
|
if (!DeviceRideService._instance)
|
|
@@ -479,18 +480,28 @@ class DeviceRideService extends events_1.default {
|
|
|
479
480
|
ai.tsLastData = tsNow;
|
|
480
481
|
return;
|
|
481
482
|
}
|
|
482
|
-
const
|
|
483
|
-
|
|
483
|
+
const prevStatus = ai.dataStatus;
|
|
484
|
+
const isAmber = (tsNow - ai.tsLastData) < NO_DATA_THRESHOLD;
|
|
485
|
+
const isRed = (tsNow - ai.tsLastData) < UNHEALTHY_THRESHOLD;
|
|
486
|
+
ai.dataStatus = 'green';
|
|
487
|
+
if (isAmber)
|
|
488
|
+
ai.dataStatus = 'amber';
|
|
489
|
+
if (isRed)
|
|
490
|
+
ai.dataStatus = 'red';
|
|
491
|
+
if (ai.isHealthy && !isAmber) {
|
|
484
492
|
ai.isHealthy = false;
|
|
485
493
|
this.logEvent({ message: 'device unhealthy', device: ai.adapter.getUniqueName(), udid: ai.udid });
|
|
486
494
|
const { enabledCapabilities } = this.getEnabledCapabilities(ai);
|
|
487
|
-
this.emit('unhealthy', ai.udid, enabledCapabilities);
|
|
495
|
+
this.emit('unhealthy', ai.udid, ai.dataStatus, enabledCapabilities);
|
|
488
496
|
}
|
|
489
|
-
else if (!ai.isHealthy &&
|
|
497
|
+
else if (!ai.isHealthy && isAmber) {
|
|
490
498
|
const { enabledCapabilities } = this.getEnabledCapabilities(ai);
|
|
491
499
|
ai.isHealthy = true;
|
|
492
500
|
this.logEvent({ message: 'device healthy', device: ai.adapter.getUniqueName(), udid: ai.udid });
|
|
493
|
-
this.emit('healthy', ai.udid, enabledCapabilities);
|
|
501
|
+
this.emit('healthy', ai.udid, ai.dataStatus, enabledCapabilities);
|
|
502
|
+
}
|
|
503
|
+
if (isAmber && prevStatus === 'green') {
|
|
504
|
+
this.prepareReconnect(ai);
|
|
494
505
|
}
|
|
495
506
|
};
|
|
496
507
|
ai.ivToCheck = (0, timers_1.setInterval)(check, 1000);
|
|
@@ -503,6 +514,61 @@ class DeviceRideService extends events_1.default {
|
|
|
503
514
|
delete ai.isHealthy;
|
|
504
515
|
}
|
|
505
516
|
}
|
|
517
|
+
prepareReconnect(ai) {
|
|
518
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
519
|
+
if (ai.isRestarting)
|
|
520
|
+
return;
|
|
521
|
+
yield (0, sleep_1.sleep)(UNHEALTHY_THRESHOLD - NO_DATA_THRESHOLD - 5000);
|
|
522
|
+
if (ai.isHealthy || ai.isRestarting)
|
|
523
|
+
return;
|
|
524
|
+
const ifName = ai.adapter.getInterface();
|
|
525
|
+
const adapters = this.getAdapterList().filter(ai => ai.adapter.getInterface() === ifName);
|
|
526
|
+
ai.isRestarting = true;
|
|
527
|
+
if (!adapters.find(ai => ai.isHealthy)) {
|
|
528
|
+
this.logger.logEvent({ message: 'restart interface', interface: ifName });
|
|
529
|
+
const i = incyclist_devices_1.InterfaceFactory.create(ifName);
|
|
530
|
+
try {
|
|
531
|
+
const promisesStop = [];
|
|
532
|
+
adapters.map(ai => {
|
|
533
|
+
ai.isRestarting = true;
|
|
534
|
+
ai.adapter.off('data', this.deviceDataHandler);
|
|
535
|
+
promisesStop.push(ai.adapter.stop());
|
|
536
|
+
});
|
|
537
|
+
if (promisesStop.length > 0) {
|
|
538
|
+
yield Promise.allSettled(promisesStop);
|
|
539
|
+
}
|
|
540
|
+
yield i.disconnect();
|
|
541
|
+
yield (0, sleep_1.sleep)(1000);
|
|
542
|
+
yield i.connect;
|
|
543
|
+
const promisesStart = [];
|
|
544
|
+
adapters.map(ai => { promisesStart.push(ai.adapter.start()); });
|
|
545
|
+
if (promisesStart.length > 0) {
|
|
546
|
+
yield Promise.allSettled(promisesStart);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
catch (err) {
|
|
550
|
+
this.logger.logEvent({ message: 'restart interface failed', interface: ifName, reason: err.message });
|
|
551
|
+
}
|
|
552
|
+
adapters.map(ai => {
|
|
553
|
+
ai.adapter.on('data', this.deviceDataHandler);
|
|
554
|
+
ai.isRestarting = false;
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
else {
|
|
558
|
+
this.logger.logEvent({ message: 'restart adapter', device: ai.udid });
|
|
559
|
+
ai.adapter.off('data', this.deviceDataHandler);
|
|
560
|
+
const adapter = ai.adapter;
|
|
561
|
+
try {
|
|
562
|
+
yield adapter.restart();
|
|
563
|
+
}
|
|
564
|
+
catch (err) {
|
|
565
|
+
this.logger.logEvent({ message: 'restart adapter failed', device: ai.udid, reason: err.message });
|
|
566
|
+
}
|
|
567
|
+
ai.adapter.on('data', this.deviceDataHandler);
|
|
568
|
+
}
|
|
569
|
+
ai.isRestarting = false;
|
|
570
|
+
});
|
|
571
|
+
}
|
|
506
572
|
start(props) {
|
|
507
573
|
return __awaiter(this, void 0, void 0, function* () {
|
|
508
574
|
yield this.lazyInit();
|
|
@@ -567,6 +633,7 @@ class DeviceRideService extends events_1.default {
|
|
|
567
633
|
pause() {
|
|
568
634
|
const adapters = this.getAdapterList();
|
|
569
635
|
adapters === null || adapters === void 0 ? void 0 : adapters.forEach(ai => {
|
|
636
|
+
ai.tsLastData = Date.now();
|
|
570
637
|
ai.adapter.pause();
|
|
571
638
|
ai.adapter.off('data', this.deviceDataHandler);
|
|
572
639
|
});
|
|
@@ -574,6 +641,7 @@ class DeviceRideService extends events_1.default {
|
|
|
574
641
|
resume() {
|
|
575
642
|
const adapters = this.getAdapterList();
|
|
576
643
|
adapters === null || adapters === void 0 ? void 0 : adapters.forEach(ai => {
|
|
644
|
+
ai.tsLastData = Date.now();
|
|
577
645
|
ai.adapter.resume();
|
|
578
646
|
ai.adapter.on('data', this.deviceDataHandler);
|
|
579
647
|
});
|
|
@@ -599,7 +667,7 @@ class DeviceRideService extends events_1.default {
|
|
|
599
667
|
const adapterInfo = adapters === null || adapters === void 0 ? void 0 : adapters.find(ai => ai.adapter.isEqual(deviceSettings));
|
|
600
668
|
if (!adapterInfo)
|
|
601
669
|
return;
|
|
602
|
-
adapterInfo.tsLastData = Date.now();
|
|
670
|
+
adapterInfo.tsLastData = data.timestamp || Date.now();
|
|
603
671
|
adapters === null || adapters === void 0 ? void 0 : adapters.forEach(ai => ai.capabilities = ai.adapter.getCapabilities());
|
|
604
672
|
const selectedDevices = this.configurationService.getSelectedDevices();
|
|
605
673
|
this.verifySelected(selectedDevices, incyclist_devices_1.IncyclistCapability.Speed);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { FileInfo, IFileLoader } from "../../../api";
|
|
2
|
+
import { RouteApiDetail } from "../api/types";
|
|
3
|
+
import { ParseResult, RouteInfo, RoutePoint } from "../types";
|
|
4
|
+
import { XmlJSON } from "../utils/xml";
|
|
5
|
+
import { XMLParser, XmlParserContext } from "./xml";
|
|
6
|
+
export declare class GPXParser extends XMLParser {
|
|
7
|
+
protected loader: IFileLoader;
|
|
8
|
+
import(file: FileInfo, xml: XmlJSON, loader?: IFileLoader): Promise<ParseResult<RouteApiDetail>>;
|
|
9
|
+
protected loadDescription(context: XmlParserContext): Promise<void>;
|
|
10
|
+
protected loadPoints(context: XmlParserContext): Promise<void>;
|
|
11
|
+
protected caclulateDistance(point: RoutePoint, prev: RoutePoint): boolean;
|
|
12
|
+
protected parseVideo(context: XmlParserContext): Promise<void>;
|
|
13
|
+
protected buildInfo(context: XmlParserContext): Promise<RouteInfo>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.GPXParser = void 0;
|
|
13
|
+
const utils_1 = require("../../../utils");
|
|
14
|
+
const route_1 = require("../utils/route");
|
|
15
|
+
const xml_1 = require("./xml");
|
|
16
|
+
const MIN_DISTANCE = 1;
|
|
17
|
+
class GPXParser extends xml_1.XMLParser {
|
|
18
|
+
import(file, xml, loader) {
|
|
19
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
20
|
+
xml.expectScheme('gpx');
|
|
21
|
+
this.loader = loader;
|
|
22
|
+
return yield this.parse(file, xml);
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
loadDescription(context) {
|
|
26
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
27
|
+
const data = context.data;
|
|
28
|
+
const metadata = data['metadata'];
|
|
29
|
+
const track = Array.isArray(data['trk']) ? data['trk'][0] : data['trk'];
|
|
30
|
+
if (!track) {
|
|
31
|
+
throw new Error('no track found');
|
|
32
|
+
}
|
|
33
|
+
context.route = {
|
|
34
|
+
title: metadata['name'] || context.fileInfo.name,
|
|
35
|
+
localizedTitle: track['title'] || track['name'],
|
|
36
|
+
country: undefined,
|
|
37
|
+
id: undefined,
|
|
38
|
+
previewUrl: undefined,
|
|
39
|
+
distance: 0,
|
|
40
|
+
elevation: 0,
|
|
41
|
+
points: [],
|
|
42
|
+
description: track['desc']
|
|
43
|
+
};
|
|
44
|
+
if (typeof context.route.localizedTitle === 'string') {
|
|
45
|
+
const lt = context.route.localizedTitle;
|
|
46
|
+
context.route.localizedTitle = { en: lt };
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
loadPoints(context) {
|
|
51
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
52
|
+
const data = context.data;
|
|
53
|
+
const track = Array.isArray(data['trk']) ? data['trk'][0] : data['trk'];
|
|
54
|
+
const segments = track.trkseg;
|
|
55
|
+
let prev;
|
|
56
|
+
segments.forEach(segment => {
|
|
57
|
+
var _a;
|
|
58
|
+
(_a = segment.trkpt) === null || _a === void 0 ? void 0 : _a.forEach(gpxPt => {
|
|
59
|
+
const point = {
|
|
60
|
+
lat: gpxPt.lat,
|
|
61
|
+
lng: gpxPt.lon,
|
|
62
|
+
elevation: 0,
|
|
63
|
+
routeDistance: 0,
|
|
64
|
+
distance: 0
|
|
65
|
+
};
|
|
66
|
+
if (prev) {
|
|
67
|
+
const ignore = this.caclulateDistance(point, prev);
|
|
68
|
+
if (ignore) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
context.route.points.push(point);
|
|
73
|
+
prev = point;
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
caclulateDistance(point, prev) {
|
|
79
|
+
if (prev) {
|
|
80
|
+
const s = Math.abs(utils_1.geo.calculateDistance(prev.lat, prev.lng, point.lat, point.lng));
|
|
81
|
+
point.distance += s;
|
|
82
|
+
point.distance = s;
|
|
83
|
+
point.routeDistance = prev.routeDistance + s;
|
|
84
|
+
if (s < MIN_DISTANCE) {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
point.distance = 0;
|
|
90
|
+
point.routeDistance = 0;
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
parseVideo(context) {
|
|
95
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
buildInfo(context) {
|
|
99
|
+
var _a, _b;
|
|
100
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
101
|
+
const { route } = context;
|
|
102
|
+
const info = {
|
|
103
|
+
id: route.id,
|
|
104
|
+
title: route.title,
|
|
105
|
+
localizedTitle: route.localizedTitle,
|
|
106
|
+
category: route.category,
|
|
107
|
+
country: route.country,
|
|
108
|
+
distance: route.distance,
|
|
109
|
+
elevation: route.elevation,
|
|
110
|
+
points: route.points,
|
|
111
|
+
segments: (_a = route.video) === null || _a === void 0 ? void 0 : _a.selectableSegments,
|
|
112
|
+
requiresDownload: false,
|
|
113
|
+
hasGpx: ((_b = route.points) === null || _b === void 0 ? void 0 : _b.length) > 0,
|
|
114
|
+
hasVideo: true,
|
|
115
|
+
isDemo: false,
|
|
116
|
+
isLocal: true,
|
|
117
|
+
isLoop: (0, route_1.checkIsLoop)(route.points),
|
|
118
|
+
videoFormat: undefined,
|
|
119
|
+
videoUrl: undefined,
|
|
120
|
+
previewUrl: undefined
|
|
121
|
+
};
|
|
122
|
+
return info;
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
exports.GPXParser = GPXParser;
|
|
@@ -15,8 +15,9 @@ export declare class XMLParser implements Parser<XmlJSON, RouteApiDetail> {
|
|
|
15
15
|
supportsContent(xmljson: XmlJSON): boolean;
|
|
16
16
|
protected loadDescription(context: XmlParserContext): Promise<void>;
|
|
17
17
|
protected parse(file: FileInfo, xmljson: XmlJSON): Promise<ParseResult<RouteApiDetail>>;
|
|
18
|
+
protected loadPoints(context: XmlParserContext): Promise<void>;
|
|
18
19
|
protected buildInfo(context: XmlParserContext): Promise<RouteInfo>;
|
|
19
|
-
parseVideo(context: XmlParserContext): Promise<void>;
|
|
20
|
+
protected parseVideo(context: XmlParserContext): Promise<void>;
|
|
20
21
|
loadElevationFromAltitudes(context: XmlParserContext, tagName?: string): Promise<void>;
|
|
21
22
|
loadElevationFromPositions(context: XmlParserContext, tags?: {
|
|
22
23
|
altitudes?: string;
|
|
@@ -60,13 +60,7 @@ class XMLParser {
|
|
|
60
60
|
const data = xmljson.json;
|
|
61
61
|
const context = { fileInfo: file, data };
|
|
62
62
|
yield this.loadDescription(context);
|
|
63
|
-
|
|
64
|
-
if ((positions === null || positions === void 0 ? void 0 : positions.length) > 0) {
|
|
65
|
-
yield this.loadElevationFromPositions(context);
|
|
66
|
-
}
|
|
67
|
-
else {
|
|
68
|
-
yield this.loadElevationFromAltitudes(context);
|
|
69
|
-
}
|
|
63
|
+
yield this.loadPoints(context);
|
|
70
64
|
yield this.parseVideo(context);
|
|
71
65
|
const res = {
|
|
72
66
|
data: yield this.buildInfo(context),
|
|
@@ -75,6 +69,18 @@ class XMLParser {
|
|
|
75
69
|
return res;
|
|
76
70
|
});
|
|
77
71
|
}
|
|
72
|
+
loadPoints(context) {
|
|
73
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
74
|
+
const { data } = context;
|
|
75
|
+
const positions = data['positions'];
|
|
76
|
+
if ((positions === null || positions === void 0 ? void 0 : positions.length) > 0) {
|
|
77
|
+
yield this.loadElevationFromPositions(context);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
yield this.loadElevationFromAltitudes(context);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
78
84
|
buildInfo(context) {
|
|
79
85
|
var _a, _b;
|
|
80
86
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -48,8 +48,11 @@ class XmlJSON {
|
|
|
48
48
|
}
|
|
49
49
|
expectScheme(scheme) {
|
|
50
50
|
this.detectScheme();
|
|
51
|
-
if (scheme !== this.scheme)
|
|
52
|
-
|
|
51
|
+
if (scheme !== this.scheme) {
|
|
52
|
+
if (!this._json[scheme])
|
|
53
|
+
throw new Error(`cannot parse <${this.scheme}>`);
|
|
54
|
+
this.scheme = scheme;
|
|
55
|
+
}
|
|
53
56
|
}
|
|
54
57
|
getSchemeData() {
|
|
55
58
|
if (!this.scheme)
|
|
@@ -71,9 +74,6 @@ class XmlJSON {
|
|
|
71
74
|
return this.map(key, item);
|
|
72
75
|
}
|
|
73
76
|
map(key, item) {
|
|
74
|
-
if (typeof (item) === 'object' && item.$) {
|
|
75
|
-
return this.map(key, item.$);
|
|
76
|
-
}
|
|
77
77
|
if (typeof (item) === 'object') {
|
|
78
78
|
const keys = Object.keys(item);
|
|
79
79
|
if (keys.length === 1 && keys[0] === '0') {
|
|
@@ -84,7 +84,12 @@ class XmlJSON {
|
|
|
84
84
|
}
|
|
85
85
|
else {
|
|
86
86
|
const obj = {};
|
|
87
|
-
keys.forEach(key => {
|
|
87
|
+
keys.forEach(key => {
|
|
88
|
+
if (key === '$')
|
|
89
|
+
Object.assign(obj, this.map(key, item.$));
|
|
90
|
+
else
|
|
91
|
+
obj[key] = this.map(key, item[key]);
|
|
92
|
+
});
|
|
88
93
|
return obj;
|
|
89
94
|
}
|
|
90
95
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "incyclist-services",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.66",
|
|
4
4
|
"peerDependencies": {
|
|
5
5
|
"gd-eventlog": "^0.1.26"
|
|
6
6
|
},
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"axios": "^1.6.1",
|
|
42
|
-
"incyclist-devices": "^2.1.
|
|
42
|
+
"incyclist-devices": "^2.1.28",
|
|
43
43
|
"uuid": "^9.0.0",
|
|
44
44
|
"xml2js": "^0.6.2"
|
|
45
45
|
}
|