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.
@@ -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 = 5000;
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 isHealthy = (tsNow - ai.tsLastData) < NO_DATA_THRESHOLD;
483
- if (ai.isHealthy && !isHealthy) {
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 && 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
- const positions = data.positions;
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
- throw new Error(`cannot parse <${this.scheme}>`);
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 => { obj[key] = this.map(key, item[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.64",
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.26",
42
+ "incyclist-devices": "^2.1.28",
43
43
  "uuid": "^9.0.0",
44
44
  "xml2js": "^0.6.2"
45
45
  }