feat(train): the Loop's firmware updates, directly or through the instance (ADR 0030, D10, D40)
Train's server relays Polar's firmware service for the people signed in (`firmware`: offered, check, download), a fetched file kept in memory a day per hardware and version, off with LOOP_FIRMWARE_RELAY=off. The app asks on the day's first connection and on the band's screen, from Polar or the instance as the person chose, never Polar in the instance's place; an update runs only after asking, reads and keeps the band first, and sets it up again with its figures and clock after. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
7c5e539df5
commit
388b3b1ae6
9 changed files with 2318 additions and 45 deletions
|
|
@ -0,0 +1,64 @@
|
|||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:halkyon_train_server/src/firmware/firmware_relay.dart';
|
||||
import 'package:halkyon_train_server/src/generated/protocol.dart';
|
||||
import 'package:serverpod/serverpod.dart';
|
||||
|
||||
/// The relay the endpoint uses: Polar's own service; tests put a fake.
|
||||
FirmwareRelay firmwareRelay = FirmwareRelay(HttpPolarFirmwareService());
|
||||
|
||||
/// Whether the instance relays firmware, as its environment says; tests
|
||||
/// set it.
|
||||
bool Function() firmwareRelayed = () => firmwareRelayedBy(Platform.environment);
|
||||
|
||||
/// Whether [environment] has relaying on (D40): on unless
|
||||
/// `LOOP_FIRMWARE_RELAY` is `off`.
|
||||
bool firmwareRelayedBy(Map<String, String> environment) =>
|
||||
environment['LOOP_FIRMWARE_RELAY']?.trim().toLowerCase() != 'off';
|
||||
|
||||
/// The Polar Loop's firmware through the instance (ADR 0030, D10, D40), for
|
||||
/// the people signed in: the instance asks Polar in their place, so Polar
|
||||
/// learns its address rather than theirs.
|
||||
class FirmwareEndpoint extends Endpoint {
|
||||
@override
|
||||
bool get requireLogin => true;
|
||||
|
||||
/// Whether the instance relays firmware.
|
||||
Future<bool> offered(Session session) async => firmwareRelayed();
|
||||
|
||||
/// The update for the band of [uuid] (Polar's UUID made from its ID) at
|
||||
/// [firmwareVersion] with [hardwareCode]; null where there is none.
|
||||
Future<LoopFirmware?> check(
|
||||
Session session,
|
||||
String uuid,
|
||||
String firmwareVersion,
|
||||
String hardwareCode,
|
||||
) async {
|
||||
_offeredOrRefused();
|
||||
return await firmwareRelay.check(
|
||||
uuid: uuid,
|
||||
firmwareVersion: firmwareVersion,
|
||||
hardwareCode: hardwareCode,
|
||||
);
|
||||
}
|
||||
|
||||
/// The file of [version] for bands of [hardwareCode], as a check named it.
|
||||
Future<ByteData> download(
|
||||
Session session,
|
||||
String hardwareCode,
|
||||
String version,
|
||||
) async {
|
||||
_offeredOrRefused();
|
||||
return await firmwareRelay.download(
|
||||
hardwareCode: hardwareCode,
|
||||
version: version,
|
||||
);
|
||||
}
|
||||
|
||||
void _offeredOrRefused() {
|
||||
if (!firmwareRelayed()) {
|
||||
throw LoopFirmwareRefused(reason: 'off');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:halkyon_train_server/src/generated/protocol.dart';
|
||||
|
||||
/// How long a firmware file is kept once fetched (D40).
|
||||
const _kept = Duration(days: 1);
|
||||
|
||||
/// Polar's firmware service, as the SDK asks it (ADR 0030, D10, D40, read in
|
||||
/// its classes, 8.3.0).
|
||||
abstract interface class PolarFirmwareService() {
|
||||
/// The update for a band, as `POST /api/v1/firmware-update/check` answers
|
||||
/// it: its version and its file's address; null where there is none.
|
||||
Future<({String version, Uri file})?> check({
|
||||
required String uuid,
|
||||
required String firmwareVersion,
|
||||
required String hardwareCode,
|
||||
});
|
||||
|
||||
/// The file at [file].
|
||||
Future<Uint8List> fetch(Uri file);
|
||||
}
|
||||
|
||||
/// [PolarFirmwareService] at `firmware-management.polar.com`, with the
|
||||
/// client name Polar's own SDK sends.
|
||||
class HttpPolarFirmwareService() implements PolarFirmwareService {
|
||||
static final _base = Uri.parse('https://firmware-management.polar.com');
|
||||
|
||||
@override
|
||||
Future<({String version, Uri file})?> check({
|
||||
required String uuid,
|
||||
required String firmwareVersion,
|
||||
required String hardwareCode,
|
||||
}) async {
|
||||
final client = HttpClient();
|
||||
try {
|
||||
final request = await client.postUrl(
|
||||
_base.resolve('/api/v1/firmware-update/check'),
|
||||
);
|
||||
request.headers
|
||||
..contentType = ContentType.json
|
||||
..set(HttpHeaders.acceptHeader, 'application/json');
|
||||
request.write(
|
||||
jsonEncode({
|
||||
'clientId': 'polar-sensor-data-collector-android',
|
||||
'uuid': uuid,
|
||||
'firmwareVersion': firmwareVersion,
|
||||
'hardwareCode': hardwareCode,
|
||||
}),
|
||||
);
|
||||
final response = await request.close();
|
||||
final body = await utf8.decodeStream(response);
|
||||
// No update is answered with no content (or not found).
|
||||
if (response.statusCode == HttpStatus.noContent ||
|
||||
response.statusCode == HttpStatus.notFound) {
|
||||
return null;
|
||||
}
|
||||
if (response.statusCode != HttpStatus.ok) {
|
||||
throw HttpException('firmware check ${response.statusCode}');
|
||||
}
|
||||
return switch (jsonDecode(body)) {
|
||||
{'version': final String version, 'fileUrl': final String file}
|
||||
when Uri.tryParse(file)?.isScheme('https') ?? false =>
|
||||
(version: version, file: Uri.parse(file)),
|
||||
_ => null,
|
||||
};
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Uint8List> fetch(Uri file) async {
|
||||
final client = HttpClient();
|
||||
try {
|
||||
final response = await (await client.getUrl(file)).close();
|
||||
if (response.statusCode != HttpStatus.ok) {
|
||||
throw HttpException('firmware file ${response.statusCode}');
|
||||
}
|
||||
final bytes = BytesBuilder(copy: false);
|
||||
await response.forEach(bytes.add);
|
||||
return bytes.takeBytes();
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The instance relaying Polar's firmware for its people (D40): it asks
|
||||
/// Polar in their place and fetches each file once for every band of its
|
||||
/// hardware, keeping it in memory for a day.
|
||||
class FirmwareRelay(final PolarFirmwareService _polar) {
|
||||
/// The files Polar named, by hardware and version: where each is, and
|
||||
/// the file once fetched.
|
||||
final _files = <String, ({Uri file, Uint8List? bytes, DateTime at})>{};
|
||||
|
||||
/// The update for a band; null where Polar has none. Throws
|
||||
/// [LoopFirmwareRefused] for figures it doesn't take, or where Polar
|
||||
/// didn't answer.
|
||||
Future<LoopFirmware?> check({
|
||||
required String uuid,
|
||||
required String firmwareVersion,
|
||||
required String hardwareCode,
|
||||
}) async {
|
||||
if (!_valid(uuid) || !_valid(firmwareVersion) || !_valid(hardwareCode)) {
|
||||
throw LoopFirmwareRefused(reason: 'invalid');
|
||||
}
|
||||
final ({String version, Uri file})? update;
|
||||
try {
|
||||
update = await _polar.check(
|
||||
uuid: uuid,
|
||||
firmwareVersion: firmwareVersion,
|
||||
hardwareCode: hardwareCode,
|
||||
);
|
||||
} on Object {
|
||||
throw LoopFirmwareRefused(reason: 'unreachable');
|
||||
}
|
||||
if (update == null) {
|
||||
return null;
|
||||
}
|
||||
_forgetOld();
|
||||
final key = _key(hardwareCode, update.version);
|
||||
final known = _files[key];
|
||||
if (known == null || known.file != update.file) {
|
||||
_files[key] = (file: update.file, bytes: null, at: clock.now());
|
||||
}
|
||||
return LoopFirmware(version: update.version);
|
||||
}
|
||||
|
||||
/// The file of [version] for [hardwareCode], as a check named it; fetched
|
||||
/// once and kept for a day. Throws [LoopFirmwareRefused] for one no check
|
||||
/// named, or where Polar didn't answer.
|
||||
Future<ByteData> download({
|
||||
required String hardwareCode,
|
||||
required String version,
|
||||
}) async {
|
||||
_forgetOld();
|
||||
final key = _key(hardwareCode, version);
|
||||
final known = _files[key];
|
||||
if (known == null) {
|
||||
throw LoopFirmwareRefused(reason: 'unknown');
|
||||
}
|
||||
if (known.bytes case final bytes?) {
|
||||
return ByteData.sublistView(bytes);
|
||||
}
|
||||
final Uint8List bytes;
|
||||
try {
|
||||
bytes = await _polar.fetch(known.file);
|
||||
} on Object {
|
||||
throw LoopFirmwareRefused(reason: 'unreachable');
|
||||
}
|
||||
_files[key] = (file: known.file, bytes: bytes, at: clock.now());
|
||||
return ByteData.sublistView(bytes);
|
||||
}
|
||||
|
||||
void _forgetOld() {
|
||||
final before = clock.now().subtract(_kept);
|
||||
_files.removeWhere((_, file) => file.at.isBefore(before));
|
||||
}
|
||||
|
||||
static String _key(String hardwareCode, String version) =>
|
||||
'$hardwareCode/$version';
|
||||
|
||||
/// A band's figure as Polar's are: short, letters, digits, dots and
|
||||
/// dashes.
|
||||
static bool _valid(String value) =>
|
||||
value.isNotEmpty &&
|
||||
value.length <= 64 &&
|
||||
RegExp(r'^[A-Za-z0-9.\-]+$').hasMatch(value);
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
### A firmware update for the person's Polar Loop that the instance relays
|
||||
### from Polar's firmware service (ADR 0030, D10, D40): its version, whose file
|
||||
### the app then downloads (`firmware.download`).
|
||||
class: LoopFirmware
|
||||
fields:
|
||||
### The firmware's version, as Polar names it (`3.1.2`).
|
||||
version: String
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
### Why the instance relays no firmware (ADR 0030, D40): `off` where its
|
||||
### operator turned relaying off, `invalid` for a band's figures it doesn't
|
||||
### take, `unknown` for a file it wasn't told of (or no longer holds),
|
||||
### `unreachable` where Polar's service didn't answer.
|
||||
exception: LoopFirmwareRefused
|
||||
fields:
|
||||
reason: String
|
||||
|
|
@ -21,6 +21,7 @@ import 'package:serverpod_auth_idp_server/serverpod_auth_idp_server.dart'
|
|||
as _iais;
|
||||
import 'package:serverpod_offline_sync_server/serverpod_offline_sync_server.dart'
|
||||
as _izehhkf5;
|
||||
import '../firmware/firmware_endpoint.dart' as _id2y0j3h;
|
||||
import '../map/map_endpoint.dart' as _i35zr6r3;
|
||||
export 'future_calls.dart' show ServerpodFutureCallsGetter;
|
||||
|
||||
|
|
@ -28,6 +29,12 @@ class Endpoints extends _is.EndpointDispatch {
|
|||
@override
|
||||
void initializeEndpoints(_is.Server server) {
|
||||
var endpoints = <String, _is.Endpoint>{
|
||||
'firmware': _id2y0j3h.FirmwareEndpoint()
|
||||
..initialize(
|
||||
server,
|
||||
'firmware',
|
||||
null,
|
||||
),
|
||||
'map': _i35zr6r3.MapEndpoint()
|
||||
..initialize(
|
||||
server,
|
||||
|
|
@ -35,6 +42,78 @@ class Endpoints extends _is.EndpointDispatch {
|
|||
null,
|
||||
),
|
||||
};
|
||||
connectors['firmware'] = _is.EndpointConnector(
|
||||
name: 'firmware',
|
||||
endpoint: endpoints['firmware']!,
|
||||
methodConnectors: {
|
||||
'offered': _is.MethodConnector(
|
||||
name: 'offered',
|
||||
params: {},
|
||||
call:
|
||||
(
|
||||
_is.Session session,
|
||||
Map<String, dynamic> params,
|
||||
) async => (endpoints['firmware'] as _id2y0j3h.FirmwareEndpoint)
|
||||
.offered(session),
|
||||
),
|
||||
'check': _is.MethodConnector(
|
||||
name: 'check',
|
||||
params: {
|
||||
'uuid': _is.ParameterDescription(
|
||||
name: 'uuid',
|
||||
type: _is.getType<String>(),
|
||||
nullable: false,
|
||||
),
|
||||
'firmwareVersion': _is.ParameterDescription(
|
||||
name: 'firmwareVersion',
|
||||
type: _is.getType<String>(),
|
||||
nullable: false,
|
||||
),
|
||||
'hardwareCode': _is.ParameterDescription(
|
||||
name: 'hardwareCode',
|
||||
type: _is.getType<String>(),
|
||||
nullable: false,
|
||||
),
|
||||
},
|
||||
call:
|
||||
(
|
||||
_is.Session session,
|
||||
Map<String, dynamic> params,
|
||||
) async =>
|
||||
(endpoints['firmware'] as _id2y0j3h.FirmwareEndpoint).check(
|
||||
session,
|
||||
params['uuid'],
|
||||
params['firmwareVersion'],
|
||||
params['hardwareCode'],
|
||||
),
|
||||
),
|
||||
'download': _is.MethodConnector(
|
||||
name: 'download',
|
||||
params: {
|
||||
'hardwareCode': _is.ParameterDescription(
|
||||
name: 'hardwareCode',
|
||||
type: _is.getType<String>(),
|
||||
nullable: false,
|
||||
),
|
||||
'version': _is.ParameterDescription(
|
||||
name: 'version',
|
||||
type: _is.getType<String>(),
|
||||
nullable: false,
|
||||
),
|
||||
},
|
||||
call:
|
||||
(
|
||||
_is.Session session,
|
||||
Map<String, dynamic> params,
|
||||
) async => (endpoints['firmware'] as _id2y0j3h.FirmwareEndpoint)
|
||||
.download(
|
||||
session,
|
||||
params['hardwareCode'],
|
||||
params['version'],
|
||||
),
|
||||
),
|
||||
},
|
||||
);
|
||||
connectors['map'] = _is.EndpointConnector(
|
||||
name: 'map',
|
||||
endpoint: endpoints['map']!,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
|
||||
/* To generate run: "serverpod generate" */
|
||||
|
||||
// ignore_for_file: implementation_imports
|
||||
// ignore_for_file: library_private_types_in_public_api
|
||||
// ignore_for_file: non_constant_identifier_names
|
||||
// ignore_for_file: public_member_api_docs
|
||||
// ignore_for_file: type_literal_in_constant_pattern
|
||||
// ignore_for_file: use_super_parameters
|
||||
// ignore_for_file: invalid_use_of_internal_member
|
||||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'package:serverpod/serverpod.dart' as _is;
|
||||
|
||||
/// A firmware update for the person's Polar Loop that the instance relays
|
||||
/// from Polar's firmware service (ADR 0030, D10, D40): its version, whose file
|
||||
/// the app then downloads (`firmware.download`).
|
||||
abstract class LoopFirmware
|
||||
implements _is.SerializableModel, _is.ProtocolSerialization {
|
||||
LoopFirmware._({required this.version});
|
||||
|
||||
factory LoopFirmware({required String version}) = _LoopFirmwareImpl;
|
||||
|
||||
factory LoopFirmware.fromJson(Map<String, dynamic> jsonSerialization) {
|
||||
return LoopFirmware(version: jsonSerialization['version'] as String);
|
||||
}
|
||||
|
||||
/// The firmware's version, as Polar names it (`3.1.2`).
|
||||
String version;
|
||||
|
||||
/// Returns a shallow copy of this [LoopFirmware]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
LoopFirmware copyWith({String? version});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'__className__': 'LoopFirmware',
|
||||
'version': version,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJsonForProtocol() {
|
||||
return {
|
||||
'__className__': 'LoopFirmware',
|
||||
'version': version,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return _is.SerializationManager.encode(this);
|
||||
}
|
||||
}
|
||||
|
||||
class _LoopFirmwareImpl extends LoopFirmware {
|
||||
_LoopFirmwareImpl({required String version}) : super._(version: version);
|
||||
|
||||
/// Returns a shallow copy of this [LoopFirmware]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
@override
|
||||
LoopFirmware copyWith({String? version}) {
|
||||
return LoopFirmware(version: version ?? this.version);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
|
||||
/* To generate run: "serverpod generate" */
|
||||
|
||||
// ignore_for_file: implementation_imports
|
||||
// ignore_for_file: library_private_types_in_public_api
|
||||
// ignore_for_file: non_constant_identifier_names
|
||||
// ignore_for_file: public_member_api_docs
|
||||
// ignore_for_file: type_literal_in_constant_pattern
|
||||
// ignore_for_file: use_super_parameters
|
||||
// ignore_for_file: invalid_use_of_internal_member
|
||||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'package:serverpod/serverpod.dart' as _is;
|
||||
|
||||
/// Why the instance relays no firmware (ADR 0030, D40): `off` where its
|
||||
/// operator turned relaying off, `invalid` for a band's figures it doesn't
|
||||
/// take, `unknown` for a file it wasn't told of (or no longer holds),
|
||||
/// `unreachable` where Polar's service didn't answer.
|
||||
abstract class LoopFirmwareRefused
|
||||
implements
|
||||
_is.SerializableException,
|
||||
_is.SerializableModel,
|
||||
_is.ProtocolSerialization {
|
||||
LoopFirmwareRefused._({required this.reason});
|
||||
|
||||
factory LoopFirmwareRefused({required String reason}) =
|
||||
_LoopFirmwareRefusedImpl;
|
||||
|
||||
factory LoopFirmwareRefused.fromJson(Map<String, dynamic> jsonSerialization) {
|
||||
return LoopFirmwareRefused(reason: jsonSerialization['reason'] as String);
|
||||
}
|
||||
|
||||
String reason;
|
||||
|
||||
/// Returns a shallow copy of this [LoopFirmwareRefused]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
LoopFirmwareRefused copyWith({String? reason});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'__className__': 'LoopFirmwareRefused',
|
||||
'reason': reason,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJsonForProtocol() {
|
||||
return {
|
||||
'__className__': 'LoopFirmwareRefused',
|
||||
'reason': reason,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'LoopFirmwareRefused(reason: $reason)';
|
||||
}
|
||||
}
|
||||
|
||||
class _LoopFirmwareRefusedImpl extends LoopFirmwareRefused {
|
||||
_LoopFirmwareRefusedImpl({required String reason}) : super._(reason: reason);
|
||||
|
||||
/// Returns a shallow copy of this [LoopFirmwareRefused]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
@override
|
||||
LoopFirmwareRefused copyWith({String? reason}) {
|
||||
return LoopFirmwareRefused(reason: reason ?? this.reason);
|
||||
}
|
||||
}
|
||||
|
|
@ -24,6 +24,8 @@ import 'package:serverpod_offline_sync_server/serverpod_offline_sync_server.dart
|
|||
import 'day/day.dart' as _ikmv9f3y;
|
||||
import 'day/day_series.dart' as _ir1ofny6;
|
||||
import 'day/night.dart' as _i17cgxdf;
|
||||
import 'firmware/loop_firmware.dart' as _i4qollgq;
|
||||
import 'firmware/loop_firmware_refused.dart' as _ipcwdy0l;
|
||||
import 'map/map_source.dart' as _ikw0qg8x;
|
||||
import 'privacy/content_key.dart' as _imed3x5d;
|
||||
import 'profile/profile.dart' as _ia5j8fch;
|
||||
|
|
@ -32,6 +34,8 @@ import 'workout/workout_series.dart' as _i92mefto;
|
|||
export 'day/day.dart';
|
||||
export 'day/day_series.dart';
|
||||
export 'day/night.dart';
|
||||
export 'firmware/loop_firmware.dart';
|
||||
export 'firmware/loop_firmware_refused.dart';
|
||||
export 'map/map_source.dart';
|
||||
export 'privacy/content_key.dart';
|
||||
export 'profile/profile.dart';
|
||||
|
|
@ -614,6 +618,12 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
if (t == _i17cgxdf.Night) {
|
||||
return _i17cgxdf.Night.fromJson(data) as T;
|
||||
}
|
||||
if (t == _i4qollgq.LoopFirmware) {
|
||||
return _i4qollgq.LoopFirmware.fromJson(data) as T;
|
||||
}
|
||||
if (t == _ipcwdy0l.LoopFirmwareRefused) {
|
||||
return _ipcwdy0l.LoopFirmwareRefused.fromJson(data) as T;
|
||||
}
|
||||
if (t == _ikw0qg8x.MapSource) {
|
||||
return _ikw0qg8x.MapSource.fromJson(data) as T;
|
||||
}
|
||||
|
|
@ -638,6 +648,15 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
if (t == _is.getType<_i17cgxdf.Night?>()) {
|
||||
return (data != null ? _i17cgxdf.Night.fromJson(data) : null) as T;
|
||||
}
|
||||
if (t == _is.getType<_i4qollgq.LoopFirmware?>()) {
|
||||
return (data != null ? _i4qollgq.LoopFirmware.fromJson(data) : null) as T;
|
||||
}
|
||||
if (t == _is.getType<_ipcwdy0l.LoopFirmwareRefused?>()) {
|
||||
return (data != null
|
||||
? _ipcwdy0l.LoopFirmwareRefused.fromJson(data)
|
||||
: null)
|
||||
as T;
|
||||
}
|
||||
if (t == _is.getType<_ikw0qg8x.MapSource?>()) {
|
||||
return (data != null ? _ikw0qg8x.MapSource.fromJson(data) : null) as T;
|
||||
}
|
||||
|
|
@ -680,6 +699,8 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
_ikmv9f3y.Day => 'Day',
|
||||
_ir1ofny6.DaySeries => 'DaySeries',
|
||||
_i17cgxdf.Night => 'Night',
|
||||
_i4qollgq.LoopFirmware => 'LoopFirmware',
|
||||
_ipcwdy0l.LoopFirmwareRefused => 'LoopFirmwareRefused',
|
||||
_ikw0qg8x.MapSource => 'MapSource',
|
||||
_imed3x5d.ContentKey => 'ContentKey',
|
||||
_ia5j8fch.Profile => 'Profile',
|
||||
|
|
@ -708,6 +729,10 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
return 'DaySeries';
|
||||
case _i17cgxdf.Night():
|
||||
return 'Night';
|
||||
case _i4qollgq.LoopFirmware():
|
||||
return 'LoopFirmware';
|
||||
case _ipcwdy0l.LoopFirmwareRefused():
|
||||
return 'LoopFirmwareRefused';
|
||||
case _ikw0qg8x.MapSource():
|
||||
return 'MapSource';
|
||||
case _imed3x5d.ContentKey():
|
||||
|
|
@ -767,6 +792,12 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
if (dataClassName == 'Night') {
|
||||
return deserialize<_i17cgxdf.Night>(data['data']);
|
||||
}
|
||||
if (dataClassName == 'LoopFirmware') {
|
||||
return deserialize<_i4qollgq.LoopFirmware>(data['data']);
|
||||
}
|
||||
if (dataClassName == 'LoopFirmwareRefused') {
|
||||
return deserialize<_ipcwdy0l.LoopFirmwareRefused>(data['data']);
|
||||
}
|
||||
if (dataClassName == 'MapSource') {
|
||||
return deserialize<_ikw0qg8x.MapSource>(data['data']);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,2 +1,6 @@
|
|||
firmware:
|
||||
- offered:
|
||||
- check:
|
||||
- download:
|
||||
map:
|
||||
- source:
|
||||
|
|
|
|||
Loading…
Reference in a new issue