Skip to content

Commit ebd5679

Browse files
authored
feat: suspension control(closes #100) (#104)
1 parent 7200c8e commit ebd5679

23 files changed

Lines changed: 807 additions & 57 deletions

lib/app/scopes/flows/selected_data_source_scope.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,11 @@ class SelectedDataSourceScope extends AutoRouter {
176176
..subscribeToRightDoor()
177177
..subscribeToWindscreenWipers(),
178178
),
179+
BlocProvider(
180+
create: (context) => SuspensionControlBloc(
181+
dataSource: context.read(),
182+
)..add(const SuspensionControlEvent.getMode()),
183+
),
179184
BlocProvider(
180185
create: (context) {
181186
context.read<OutgoingPackagesCubit>().subscribeTo(

lib/data/services/data_source/demo_data_source.dart

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,7 @@ class DemoDataSource extends DataSource
259259
timer = Timer.periodic(
260260
Duration(milliseconds: updatePeriod),
261261
(timer) async {
262+
if (subscriptionParameters.isEmpty) return;
262263
final innerTimerPeriod =
263264
(updatePeriod / subscriptionParameters.length).floor();
264265
for (var i = 0; i < subscriptionParameters.length; i++) {
@@ -582,6 +583,68 @@ class DemoDataSource extends DataSource
582583
};
583584
await _sendDoorToggleResultCallback(functionId);
584585

586+
return const Result.value(null);
587+
},
588+
),
589+
MainEcuMockResponseWrapper(
590+
ids: {
591+
const DataSourceParameterId.suspensionMode(),
592+
},
593+
unavailableForSubscriptionIds: {},
594+
respondCallback: (id, version, manager, [package]) async {
595+
final data = (package?.data).checkNotNull('Package data');
596+
final requestType = data.first;
597+
assert(
598+
[
599+
FunctionId.requestValue.value,
600+
FunctionId.setValueWithParam.value,
601+
].contains(requestType),
602+
'Supported only "set" and "get" request types',
603+
);
604+
605+
await manager.updateCallback(
606+
id,
607+
SetUint8ResultBody(
608+
success: !generateRandomErrors() || randomBool,
609+
value: (generateRandomErrors() ||
610+
requestType == FunctionId.requestValue.value)
611+
? SuspensionMode.random.id
612+
: package?.data.last ?? 0,
613+
),
614+
version,
615+
);
616+
617+
return const Result.value(null);
618+
},
619+
),
620+
MainEcuMockResponseWrapper(
621+
ids: {
622+
const DataSourceParameterId.suspensionValue(),
623+
},
624+
unavailableForSubscriptionIds: {},
625+
respondCallback: (id, version, manager, [package]) async {
626+
final data = (package?.data).checkNotNull('Package data');
627+
final requestType = data.first;
628+
assert(
629+
[
630+
FunctionId.requestValue.value,
631+
FunctionId.setValueWithParam.value,
632+
].contains(requestType),
633+
'Supported only "set" and "get" request types',
634+
);
635+
636+
await manager.updateCallback(
637+
id,
638+
SetUint8ResultBody(
639+
success: !generateRandomErrors() || randomBool,
640+
value: (generateRandomErrors() ||
641+
requestType == FunctionId.requestValue.value)
642+
? Random().nextInt(SuspensionMode.kMaxManualValue)
643+
: package?.data.last ?? 0,
644+
),
645+
version,
646+
);
647+
585648
return const Result.value(null);
586649
},
587650
),

lib/domain/data_source/blocs/change_gear_bloc.dart

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,17 @@ import 'package:re_seedwork/re_seedwork.dart';
88
part 'change_gear_bloc.freezed.dart';
99

1010
@freezed
11-
class ChangeGearEvent extends EffectEvent with _$ChangeGearEvent {
11+
class ChangeGearEvent with _$ChangeGearEvent {
1212
const factory ChangeGearEvent.change(MotorGear newGear) = _Change;
1313
}
1414

1515
typedef ChangeGearState = AsyncData<MotorGear, Object>;
1616

17-
class ChangeGearBloc extends Bloc<ChangeGearEvent, ChangeGearState>
18-
with BlocEventHandlerMixin {
17+
class ChangeGearBloc extends Bloc<ChangeGearEvent, ChangeGearState> {
1918
ChangeGearBloc({
2019
required this.dataSource,
2120
required this.generalDataCubit,
21+
this.responseTimeout = const Duration(seconds: 2),
2222
}) : super(const ChangeGearState.initial(MotorGear.unknown)) {
2323
on<_Change>(_onChange);
2424
}
@@ -37,6 +37,9 @@ class ChangeGearBloc extends Bloc<ChangeGearEvent, ChangeGearState>
3737
@protected
3838
final GeneralDataCubit generalDataCubit;
3939

40+
@visibleForTesting
41+
final Duration responseTimeout;
42+
4043
Future<void> _onChange(
4144
_Change event,
4245
Emitter<AsyncData<MotorGear, Object>> emit,
@@ -46,7 +49,7 @@ class ChangeGearBloc extends Bloc<ChangeGearEvent, ChangeGearState>
4649
try {
4750
final future = generalDataCubit.stream
4851
.firstWhere((element) => element.mergedGear == event.newGear)
49-
.timeout(const Duration(seconds: 2));
52+
.timeout(responseTimeout);
5053
for (final parameterId in kParameterIds) {
5154
final res = await dataSource.sendPackage(
5255
OutgoingSetValuePackage(
Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
import 'package:flutter_bloc/flutter_bloc.dart';
2+
import 'package:freezed_annotation/freezed_annotation.dart';
3+
import 'package:pixel_app_flutter/domain/data_source/data_source.dart';
4+
import 'package:pixel_app_flutter/domain/data_source/models/package/incoming/incoming_data_source_packages.dart';
5+
import 'package:pixel_app_flutter/domain/data_source/models/package/outgoing/outgoing_data_source_packages.dart';
6+
import 'package:pixel_app_flutter/domain/data_source/models/package_data/package_data.dart';
7+
import 'package:re_seedwork/re_seedwork.dart';
8+
9+
part 'suspension_control_bloc.freezed.dart';
10+
11+
@freezed
12+
class SuspensionControlEvent extends EffectEvent with _$SuspensionControlEvent {
13+
const factory SuspensionControlEvent.switchMode(SuspensionMode mode) =
14+
_SwitchMode;
15+
const factory SuspensionControlEvent.getManual() = _GetManual;
16+
const factory SuspensionControlEvent.setManual(int value) = _SetManual;
17+
const factory SuspensionControlEvent.getMode() = _GetMode;
18+
}
19+
20+
typedef SuspensionControlState
21+
= AsyncData<SuspensionMode, SuspensionControlEvent>;
22+
23+
class SuspensionControlBloc
24+
extends Bloc<SuspensionControlEvent, SuspensionControlState> {
25+
SuspensionControlBloc({
26+
required this.dataSource,
27+
this.responseTimeout = const Duration(seconds: 2),
28+
}) : super(
29+
const SuspensionControlState.initial(
30+
SuspensionMode.manualMiddle(),
31+
),
32+
) {
33+
on<_GetMode>(_onGetMode);
34+
on<_SwitchMode>(_onSwitchMode);
35+
on<_GetManual>(_onGetManualValue);
36+
on<_SetManual>(_onSetManualValue);
37+
}
38+
39+
@protected
40+
final DataSource dataSource;
41+
42+
@visibleForTesting
43+
final Duration responseTimeout;
44+
45+
Future<void> _onGetMode(
46+
_GetMode event,
47+
Emitter<AsyncData<SuspensionMode, SuspensionControlEvent>> emit,
48+
) async {
49+
emit(state.inLoading());
50+
51+
try {
52+
await dataSource.packageStream
53+
.waitFor<SuspensionModeIncomingDataSourcePackage>(
54+
action: () async {
55+
final result = await dataSource.sendPackage(
56+
OutgoingValueRequestPackage(
57+
parameterId: const DataSourceParameterId.suspensionMode(),
58+
),
59+
);
60+
61+
if (result.isError) {
62+
emit(state.inFailure(event));
63+
}
64+
return result.isError;
65+
},
66+
onDone: (package) async {
67+
emit(
68+
package.dataModel.when(
69+
success: (value) {
70+
return AsyncData.success(SuspensionMode.fromId(value));
71+
},
72+
error: () => state.inFailure(event),
73+
),
74+
);
75+
},
76+
timeout: responseTimeout,
77+
);
78+
} catch (e) {
79+
emit(state.inFailure(event));
80+
81+
rethrow;
82+
}
83+
84+
if (state.isSuccess && state.value.isManual) {
85+
add(const SuspensionControlEvent.getManual());
86+
}
87+
}
88+
89+
Future<void> _onGetManualValue(
90+
_GetManual event,
91+
Emitter<AsyncData<SuspensionMode, SuspensionControlEvent>> emit,
92+
) async {
93+
emit(const AsyncData.loading(SuspensionMode.manualMiddle()));
94+
95+
try {
96+
await dataSource.packageStream
97+
.waitFor<SuspensionManualValueIncomingDataSourcePackage>(
98+
action: () async {
99+
final result = await dataSource.sendPackage(
100+
OutgoingValueRequestPackage(
101+
parameterId: const DataSourceParameterId.suspensionValue(),
102+
),
103+
);
104+
105+
if (result.isError) {
106+
emit(state.inFailure(event));
107+
}
108+
return result.isError;
109+
},
110+
onDone: (package) async {
111+
emit(
112+
package.dataModel.when(
113+
success: (value) {
114+
return AsyncData.success(SuspensionMode.manual(value: value));
115+
},
116+
error: () => state.inFailure(event),
117+
),
118+
);
119+
},
120+
timeout: responseTimeout,
121+
);
122+
} catch (e) {
123+
emit(state.inFailure(event));
124+
125+
rethrow;
126+
}
127+
}
128+
129+
Future<void> _onSwitchMode(
130+
_SwitchMode event,
131+
Emitter<AsyncData<SuspensionMode, SuspensionControlEvent>> emit,
132+
) async {
133+
final beforeMode = state.payload;
134+
emit(AsyncData.loading(event.mode));
135+
136+
try {
137+
await dataSource.packageStream
138+
.waitFor<SuspensionModeIncomingDataSourcePackage>(
139+
action: () async {
140+
final result = await dataSource.sendPackage(
141+
OutgoingSetValuePackage(
142+
parameterId: const DataSourceParameterId.suspensionMode(),
143+
setValueBody: SetUint8Body(value: event.mode.id),
144+
),
145+
);
146+
147+
if (result.isError) {
148+
emit(AsyncData.failure(beforeMode, event));
149+
}
150+
return result.isError;
151+
},
152+
onDone: (package) async {
153+
emit(
154+
package.dataModel.when(
155+
success: (value) {
156+
if (value == state.payload.id) {
157+
return state.inSuccess();
158+
}
159+
return AsyncData.failure(
160+
beforeMode,
161+
event,
162+
);
163+
},
164+
error: () => AsyncData.failure(
165+
beforeMode,
166+
event,
167+
),
168+
),
169+
);
170+
},
171+
timeout: responseTimeout,
172+
);
173+
} catch (e) {
174+
emit(AsyncData.failure(beforeMode, event));
175+
176+
rethrow;
177+
}
178+
179+
if (state.isSuccess && state.payload.isManual) {
180+
add(const SuspensionControlEvent.getManual());
181+
}
182+
}
183+
184+
Future<void> _onSetManualValue(
185+
_SetManual event,
186+
Emitter<AsyncData<SuspensionMode, SuspensionControlEvent>> emit,
187+
) async {
188+
final beforeMode = state.payload;
189+
emit(AsyncData.loading(SuspensionMode.manual(value: event.value)));
190+
191+
try {
192+
await dataSource.packageStream
193+
.waitFor<SuspensionManualValueIncomingDataSourcePackage>(
194+
action: () async {
195+
final result = await dataSource.sendPackage(
196+
OutgoingSetValuePackage(
197+
parameterId: const DataSourceParameterId.suspensionValue(),
198+
setValueBody: SetUint8Body(value: event.value),
199+
),
200+
);
201+
202+
if (result.isError) {
203+
emit(
204+
AsyncData.failure(
205+
beforeMode,
206+
event,
207+
),
208+
);
209+
}
210+
return result.isError;
211+
},
212+
onDone: (package) async {
213+
emit(
214+
package.dataModel.when(
215+
success: (value) {
216+
if (value == event.value) {
217+
return state.inSuccess();
218+
}
219+
return AsyncData.failure(
220+
beforeMode,
221+
event,
222+
);
223+
},
224+
error: () => AsyncData.failure(
225+
beforeMode,
226+
event,
227+
),
228+
),
229+
);
230+
},
231+
timeout: responseTimeout,
232+
);
233+
} catch (e) {
234+
emit(
235+
AsyncData.failure(
236+
beforeMode,
237+
event,
238+
),
239+
);
240+
241+
rethrow;
242+
}
243+
}
244+
}
245+
246+
extension on Stream<dynamic> {
247+
Future<void> waitFor<T>({
248+
required Future<bool> Function() action,
249+
required Future<void> Function(T value) onDone,
250+
required Duration timeout,
251+
}) async {
252+
final future = firstWhere((package) => package is T).timeout(timeout);
253+
254+
final stop = await action();
255+
256+
if (stop) return;
257+
258+
await onDone((await future) as T);
259+
}
260+
}

0 commit comments

Comments
 (0)