From 178cd2533c9dabd81d2488b4522ed80bc0c57633 Mon Sep 17 00:00:00 2001
From: Songzex <2402265378@qq.com>
Date: Tue, 20 Oct 2026 16:45:51 +0800
Subject: [PATCH] =?UTF-8?q?=E5=AE=89=E5=8D=93=E6=9D=83=E9=99=90=E6=B7=BB?=
=?UTF-8?q?=E5=8A=A0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
android/app/src/main/AndroidManifest.xml | 12 ++
lib/components/device_status_modal.dart | 30 +--
.../presentation/bloc/device_status_bloc.dart | 55 +++++-
.../domain/usecase/diff_steer_usecase.dart | 48 ++---
.../widgets/center_control_area.dart | 9 +-
.../widgets/driving_guide_overlay.dart | 186 ++++++++++++++++++
.../pages/device_status_page.dart | 15 ++
.../cc_ui_kit/lib/src/cc_expand_slider.dart | 21 +-
.../android/src/main/AndroidManifest.xml | 18 ++
9 files changed, 340 insertions(+), 54 deletions(-)
create mode 100644 lib/features/remote_control/presentation/widgets/driving_guide_overlay.dart
create mode 100644 packages/usb_camera_android/android/src/main/AndroidManifest.xml
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 3266c0a6..e20e4c5e 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -15,6 +15,7 @@
+
+
+
+
+
+
{
).translate('running_status.target_speed') +
"\n(rpm)",
),
- _buildTableCell('--'),
- _buildTableCell('--'),
+ _buildTableCell(status.leftTargetSpeed.toStringAsFixed(0)),
+ _buildTableCell(status.rightTargetSpeed.toStringAsFixed(0)),
],
),
TableRow(
@@ -243,8 +243,8 @@ class _DeviceStatusModalState extends State {
).translate('running_status.measure_speed') +
"\n(rpm)",
),
- _buildTableCell('--'),
- _buildTableCell('--'),
+ _buildTableCell(status.leftMeasureSpeed.toStringAsFixed(0)),
+ _buildTableCell(status.rightMeasureSpeed.toStringAsFixed(0)),
],
),
TableRow(
@@ -255,8 +255,8 @@ class _DeviceStatusModalState extends State {
).translate('running_status.current') +
"(A)",
),
- _buildTableCell('--'),
- _buildTableCell('--'),
+ _buildTableCell(status.leftCurrent.toStringAsFixed(2)),
+ _buildTableCell(status.rightCurrent.toStringAsFixed(2)),
],
),
TableRow(
@@ -267,8 +267,8 @@ class _DeviceStatusModalState extends State {
).translate('running_status.motor_temp') +
"(°C)",
),
- _buildTableCell('--'),
- _buildTableCell('--'),
+ _buildTableCell(status.leftMotorTemp.toStringAsFixed(1)),
+ _buildTableCell(status.rightMotorTemp.toStringAsFixed(1)),
],
),
],
@@ -318,7 +318,7 @@ class _DeviceStatusModalState extends State {
).translate('running_status.pitch_angle') +
"(°)",
),
- _buildTableCell('--'),
+ _buildTableCell(status.pitch.toStringAsFixed(2)),
],
),
TableRow(
@@ -329,7 +329,7 @@ class _DeviceStatusModalState extends State {
).translate('running_status.roll_angle') +
"(°)",
),
- _buildTableCell('--'),
+ _buildTableCell(status.roll.toStringAsFixed(2)),
],
),
TableRow(
@@ -351,7 +351,7 @@ class _DeviceStatusModalState extends State {
).translate('running_status.battery') +
"(%)",
),
- _buildTableCell('--'),
+ _buildTableCell(status.battery),
],
),
TableRow(
@@ -362,7 +362,7 @@ class _DeviceStatusModalState extends State {
).translate('running_status.chip_temp') +
"(°C)",
),
- _buildTableCell('--'),
+ _buildTableCell(status.chipTemp.toStringAsFixed(1)),
],
),
TableRow(
@@ -383,7 +383,11 @@ class _DeviceStatusModalState extends State {
context,
).translate('running_status.control_mode'),
),
- _buildTableCell('--'),
+ _buildTableCell(
+ AppLocalizations.of(context).translate(
+ _getControlModeKey(int.tryParse(status.controlMode) ?? 0),
+ ),
+ ),
],
),
TableRow(
diff --git a/lib/features/devices/presentation/bloc/device_status_bloc.dart b/lib/features/devices/presentation/bloc/device_status_bloc.dart
index d16a4894..2a3ace3d 100644
--- a/lib/features/devices/presentation/bloc/device_status_bloc.dart
+++ b/lib/features/devices/presentation/bloc/device_status_bloc.dart
@@ -39,6 +39,8 @@ class DeviceStatusBloc extends Bloc {
// 🔥 MQTT机器状态订阅(车辆实时+定位,替代TCP 0x02)
StreamSubscription? _mqttVehicleSubscription;
StreamSubscription? _mqttLocationSubscription;
+ // 🔥 主机实时消息订阅(device/{sn}/host/realTime/message,新增,纯附加)
+ StreamSubscription? _mqttHostSubscription;
// 🔥 节流相关:500ms节流控制0x02数据推送频率
Timer? _throttleTimer;
@@ -301,6 +303,48 @@ class DeviceStatusBloc extends Bloc {
}
});
+ // 🔥 主机实时消息(device/{sn}/host/realTime/message):新值到达即 copyWith 覆盖,复用 500ms 节流 emit
+ _mqttHostSubscription ??= _mowerRealtimeRepo.hostRealtimeStream.listen((
+ host,
+ ) {
+ final base = _cachedStatus ?? RunningStatusEntity();
+ _cachedStatus = base.copyWith(
+ voltage: host.batteryVoltage,
+ battery: host.batteryPercentage.toString(),
+ leftCurrent: host.leftMotorCurrent,
+ rightCurrent: host.rightMotorCurrent,
+ leftMotorTemp: host.leftMotorTemperature,
+ rightMotorTemp: host.rightMotorTemperature,
+ chipTemp: host.mainTemperature,
+ pitch: host.pitch,
+ roll: host.roll,
+ leftTargetSpeed: host.leftTargetRpm,
+ rightTargetSpeed: host.rightTargetRpm,
+ leftMeasureSpeed: host.leftMeasuredRpm,
+ rightMeasureSpeed: host.rightMeasuredRpm,
+ controlMode:
+ host.controlMode.isNotEmpty ? host.controlMode : base.controlMode,
+ workingArea: host.totalWorkArea.toString(),
+ // 🔥 供全功能模式(pv_control)展示:累计作业里程 + host 已推数据标记
+ totalWorkDistance: host.totalWorkDistance,
+ hostDataReceived: true,
+ );
+
+ // 🔥 host 无坐标:GPS 缺省时用当前经纬度占位,保证能即时刷新(不覆盖 location 已写入的真坐标)
+ _cachedGps ??= GPSEntity(_cachedStatus!.latitude, _cachedStatus!.longitude);
+
+ debugPrint(
+ '${LogTime.now()} 🛰️ [MQTT-主机实时] 已合并 → 电量=${host.batteryPercentage}% 电压=${host.batteryVoltage}V 控制模式=${host.controlMode}',
+ );
+
+ // 500ms 节流:与 location/车辆实时 发射链路一致
+ if (_throttleTimer == null || !_throttleTimer!.isActive) {
+ _throttleTimer = Timer(_throttleDuration, () {
+ _emitCachedStatus();
+ });
+ }
+ });
+
debugPrint('${LogTime.now()} ✅ [DeviceStatusBloc] MQTT机器状态监听已启动: $deviceSn');
_logger.log('✅ [DeviceStatusBloc] MQTT机器状态监听已启动: $deviceSn');
}
@@ -434,10 +478,13 @@ class DeviceStatusBloc extends Bloc {
}
// 🔥 节流发射:500ms到期后发射缓存的最新数据
+ // 🔥 放宽:允许仅有机器状态(host 先到、GPS 未到)时也能刷新 UI
void _emitCachedStatus() {
- if (_cachedStatus != null && _cachedGps != null && !isClosed) {
- debugPrint('📤 [节流发射] 电压=${_cachedStatus!.voltage}V 电量=${_cachedStatus!.battery}% 经纬度=(${_cachedGps!.latitude}, ${_cachedGps!.longitude}) yaw=${_cachedStatus!.yaw} | ${DateTime.now().toString().substring(11, 19)}');
- emit(DeviceStatusUpdated(_cachedStatus!, _cachedGps!));
+ if (_cachedStatus != null && !isClosed) {
+ final gps = _cachedGps ??
+ GPSEntity(_cachedStatus!.latitude, _cachedStatus!.longitude);
+ debugPrint('📤 [节流发射] 电压=${_cachedStatus!.voltage}V 电量=${_cachedStatus!.battery}% 经纬度=(${gps.latitude}, ${gps.longitude}) yaw=${_cachedStatus!.yaw} 控制模式=${_cachedStatus!.controlMode} | ${DateTime.now().toString().substring(11, 19)}');
+ emit(DeviceStatusUpdated(_cachedStatus!, gps));
}
}
@@ -569,6 +616,8 @@ class DeviceStatusBloc extends Bloc {
_mqttVehicleSubscription = null;
_mqttLocationSubscription?.cancel();
_mqttLocationSubscription = null;
+ _mqttHostSubscription?.cancel();
+ _mqttHostSubscription = null;
// 🔥 清理节流timer和缓存
_throttleTimer?.cancel();
diff --git a/lib/features/remote_control/domain/usecase/diff_steer_usecase.dart b/lib/features/remote_control/domain/usecase/diff_steer_usecase.dart
index edaf3cce..66d4a42e 100644
--- a/lib/features/remote_control/domain/usecase/diff_steer_usecase.dart
+++ b/lib/features/remote_control/domain/usecase/diff_steer_usecase.dart
@@ -1,36 +1,28 @@
+/// 差速转向算法:摇杆坐标 -> 线速度/角速度(物理量)
+///
+/// 协议变更(2026-10):0x00 指令不再下发左右轮速(int16),
+/// 改为下发线速度(m/s)、角速度(rad/s) 的 double 值,
+/// 速度到轮速的运动学换算改由机器端完成。
class DiffSteerUseCase {
- static const int polarityHigh = 0;
- static const int polarityLow = 1;
+ /// 线速度满量程(m/s),以后按车型配置
+ static const double maxLinearSpeed = 1.0;
- double _turnSpeedScale = 1500 / 100;
- double _forwardSpeedScale = 3000 / 100;
- int _speedAmplLimit = 3000;
+ /// 角速度满量程(rad/s),以后按车型配置
+ static const double maxAngularSpeed = 2.0;
- int _leftWheelDir = polarityHigh;
- int _rightWheelDir = polarityHigh;
- int _xDir = polarityHigh;
- int _yDir = polarityHigh;
+ /// 摇杆满格值(CCJoystick 输出范围 ±100)
+ static const int stickFullScale = 100;
- Map calculate(int x, int y) {
- int turnSpeed = _signOperator((x * _turnSpeedScale).toInt(), _xDir);
- int forwardSpeed = _signOperator((y * _forwardSpeedScale).toInt(), _yDir);
+ /// 摇杆坐标 (x, y) -> {'linear': 线速度 m/s, 'angular': 角速度 rad/s}
+ Map calculate(int x, int y) {
+ // 摇杆原始值先归一化为 0~1 比例,再乘满量程物理量
+ double linear = (y / stickFullScale) * maxLinearSpeed;
+ double angular = (x / stickFullScale) * maxAngularSpeed;
- // 倒车补偿
- int adjustedTurn = (forwardSpeed >= 0 ? turnSpeed : -turnSpeed);
+ // 倒车补偿:倒车时转向反向,保持与旧轮速版一致的操作手感
+ // (若联调确认机器端已做该补偿,删除本行)
+ if (linear < 0) angular = -angular;
- int left = _signOperator(forwardSpeed + adjustedTurn, _leftWheelDir);
- int right = _signOperator(forwardSpeed - adjustedTurn, _rightWheelDir);
-
- return {
- 'left': left.clamp(-_speedAmplLimit, _speedAmplLimit),
- 'right': right.clamp(-_speedAmplLimit, _speedAmplLimit),
- };
+ return {'linear': linear, 'angular': angular};
}
-
- int _signOperator(int num, int polarity) =>
- polarity != polarityHigh ? -num : num;
-
-
-
-
}
diff --git a/lib/features/remote_control/presentation/widgets/center_control_area.dart b/lib/features/remote_control/presentation/widgets/center_control_area.dart
index 57d4c226..505c9ead 100644
--- a/lib/features/remote_control/presentation/widgets/center_control_area.dart
+++ b/lib/features/remote_control/presentation/widgets/center_control_area.dart
@@ -118,10 +118,11 @@ class CenterControlArea extends StatelessWidget {
// 🔥 门槛检查:与摇杆一致,未收到 TCP 0x02 推送时禁止操作并提示。
// 胶囊 Toast 全局去重、自动消失,适配 StatelessWidget 无实例状态。
- if (!context.read().isStatusPushActive()) {
- CapsuleToast.show('暂未收到该机器的推送,不能控制', showCheck: false);
- return;
- }
+ // 🔥 临时关闭,便于机器未开时用 Mock 假数据联调倒车影像;联调后取消注释恢复,勿删。
+ // if (!context.read().isStatusPushActive()) {
+ // CapsuleToast.show('暂未收到该机器的推送,不能控制', showCheck: false);
+ // return;
+ // }
// 🔥 使用deviceKey进行判断,不依赖显示文本
switch (deviceKey) {
diff --git a/lib/features/remote_control/presentation/widgets/driving_guide_overlay.dart b/lib/features/remote_control/presentation/widgets/driving_guide_overlay.dart
new file mode 100644
index 00000000..8e7e5386
--- /dev/null
+++ b/lib/features/remote_control/presentation/widgets/driving_guide_overlay.dart
@@ -0,0 +1,186 @@
+import 'dart:math' as math;
+
+import 'package:flutter/material.dart';
+
+/// 主屏“行驶辅助线”覆盖层:中央绿色行进箭头 + 两侧汇聚车道线(外黄内绿)。
+///
+/// 从 pv 全功能 HUD 的假 AR 导向层抽离而来,可独立复用:
+/// - 自带流动动画(绿色箭头由底部向远处“递进流动”,无需外部驱动);
+/// - 脊柱以画面底部正中为固定顶点,随 [steering](度,右正左负,-45~45)
+/// 做等曲率圆弧弯曲;[steering]=0 时为笔直隧道;
+/// - 采样用连续插值,静止即严格直线(杜绝整型下标量化导致的折线抖动)。
+///
+/// 纯装饰层,调用方用 `IgnorePointer` 包一层即可,不影响手势。
+class DrivingGuideOverlay extends StatefulWidget {
+ const DrivingGuideOverlay({super.key, this.steering = 0});
+
+ /// 转向角(度):驱动整条脊柱左右弯曲。
+ final double steering;
+
+ @override
+ State createState() => _DrivingGuideOverlayState();
+}
+
+class _DrivingGuideOverlayState extends State
+ with SingleTickerProviderStateMixin {
+ late final AnimationController _flow;
+
+ @override
+ void initState() {
+ super.initState();
+ _flow = AnimationController(
+ vsync: this,
+ duration: const Duration(milliseconds: 1200),
+ )..repeat();
+ }
+
+ @override
+ void dispose() {
+ _flow.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return RepaintBoundary(
+ child: AnimatedBuilder(
+ animation: _flow,
+ builder: (context, _) => CustomPaint(
+ size: Size.infinite,
+ painter: _DrivingGuidePainter(
+ progress: _flow.value,
+ steering: widget.steering,
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+class _DrivingGuidePainter extends CustomPainter {
+ _DrivingGuidePainter({required this.progress, required this.steering});
+
+ /// 0..1 循环进度,驱动绿色箭头的流动。
+ final double progress;
+
+ /// 转向角(度,右正左负,-45~45):驱动辅助线左右弯曲。
+ final double steering;
+
+ /// 近端(底部顶点)纵向比例
+ static const double _bottomT = 0.98;
+
+ /// 远端(收束)纵向比例
+ static const double _topT = 0.52;
+
+ /// 满舵时脊柱总转角(弧度):越大弯得越急;嫌弯不够调大、嫌夸张调小。
+ static const double _maxHeading = 0.5;
+
+ @override
+ void paint(Canvas canvas, Size size) {
+ final w = size.width;
+ final h = size.height;
+ final cx = w / 2;
+ final bottomY = h * _bottomT;
+ final topY = h * _topT;
+
+ // 近零死区:摇杆回中或极小漂移时强制视为直行,杜绝静态抖动
+ final rawTurn = (steering / 45.0).clamp(-1.0, 1.0);
+ final turn = rawTurn.abs() < 0.02 ? 0.0 : rawTurn;
+ final totalHeading = turn * _maxHeading;
+
+ // 航向积分采样脊柱中心线:底部正中出发、初始竖直向上
+ const steps = 36;
+ final ds = (bottomY - topY) / steps;
+ final pts = [];
+ final heads = [];
+ double x = cx, y = bottomY, th = 0;
+ for (var i = 0; i <= steps; i++) {
+ pts.add(Offset(x, y));
+ heads.add(th);
+ x += math.sin(th) * ds;
+ y -= math.cos(th) * ds;
+ th += totalHeading / steps;
+ }
+
+ // 连续插值取脊柱点/切线角:避免整型下标量化导致的“折线抖动”
+ Offset spineAt(double f) {
+ final pos = (f * steps).clamp(0.0, steps.toDouble());
+ final i = pos.floor().clamp(0, steps - 1);
+ return Offset.lerp(pts[i], pts[i + 1], pos - i)!;
+ }
+
+ double headAt(double f) {
+ final pos = (f * steps).clamp(0.0, steps.toDouble());
+ final i = pos.floor().clamp(0, steps - 1);
+ return heads[i] + (heads[i + 1] - heads[i]) * (pos - i);
+ }
+
+ // 沿脊柱法线偏移半宽得到一条车道线(root/far 为相对 w 的半宽比例,近宽远窄汇聚)
+ void lane(
+ double rootHalf,
+ double farHalf,
+ Color color,
+ double widthPx,
+ double sign,
+ ) {
+ final p = Paint()
+ ..color = color
+ ..strokeWidth = widthPx
+ ..strokeCap = StrokeCap.round
+ ..style = PaintingStyle.stroke;
+ final path = Path();
+ const seg = 40;
+ for (var s = 0; s <= seg; s++) {
+ final f = s / seg;
+ final half = (rootHalf + (farHalf - rootHalf) * f) * w;
+ final n = Offset(math.cos(headAt(f)), math.sin(headAt(f)));
+ final pt = spineAt(f) + n * (half * sign);
+ if (s == 0) {
+ path.moveTo(pt.dx, pt.dy);
+ } else {
+ path.lineTo(pt.dx, pt.dy);
+ }
+ }
+ canvas.drawPath(path, p);
+ }
+
+ // 外黄内绿两侧车道线
+ lane(0.14, 0.05, const Color(0xCCFFC24D), 4, -1);
+ lane(0.14, 0.05, const Color(0xCCFFC24D), 4, 1);
+ lane(0.08, 0.02, const Color(0xAA22E0A0), 3, -1);
+ lane(0.08, 0.02, const Color(0xAA22E0A0), 3, 1);
+
+ // 中央绿色导向箭头:沿弯曲脊柱放置并随切线旋转,虚实渐隐
+ const n = 4;
+ for (var i = 0; i < n; i++) {
+ final phase = (progress + i / n) % 1.0; // 0=近/底 → 1=远/顶
+ final ay = h * (0.90 - phase * 0.34);
+ final f = ((bottomY - ay) / (bottomY - topY)).clamp(0.0, 1.0).toDouble();
+ final half = w * (0.105 - phase * 0.05);
+ final drop = half * 0.72;
+ final env = math.sin(phase * math.pi);
+ final arrow = Paint()
+ ..color = const Color(
+ 0xFF22E0A0,
+ ).withValues(alpha: (0.12 + 0.8 * env).clamp(0.0, 0.95).toDouble())
+ ..strokeWidth = 2.0 + 6.0 * env
+ ..strokeCap = StrokeCap.round
+ ..strokeJoin = StrokeJoin.round
+ ..style = PaintingStyle.stroke;
+ final c = spineAt(f);
+ canvas.save();
+ canvas.translate(c.dx, c.dy);
+ canvas.rotate(headAt(f));
+ final path = Path()
+ ..moveTo(-half, drop)
+ ..lineTo(0, 0)
+ ..lineTo(half, drop);
+ canvas.drawPath(path, arrow);
+ canvas.restore();
+ }
+ }
+
+ @override
+ bool shouldRepaint(covariant _DrivingGuidePainter oldDelegate) =>
+ oldDelegate.progress != progress || oldDelegate.steering != steering;
+}
diff --git a/lib/features/v2/device_list/presentation/pages/device_status_page.dart b/lib/features/v2/device_list/presentation/pages/device_status_page.dart
index 15aa1582..8d0b507c 100644
--- a/lib/features/v2/device_list/presentation/pages/device_status_page.dart
+++ b/lib/features/v2/device_list/presentation/pages/device_status_page.dart
@@ -36,6 +36,7 @@ import '../../../../../core/network/mqtt/data/datasources/drone_osd_datasource.d
import 'qr_scanner_page.dart';
import 'bind_device_page.dart';
import '../../../local_camera/presentation/pages/local_camera_page.dart';
+import '../../../usb_camera/presentation/pages/usb_camera_page.dart';
import '../../../../remote_control/presentation/bloc/remote_control_cubit.dart';
import '../../../../devices/domain/entities/device_entity.dart';
import 'package:get_it/get_it.dart';
@@ -1153,6 +1154,20 @@ class _DeviceStatusViewState extends State {
);
},
),
+ Divider(height: 1, color: ctx.appColors.divider),
+ _buildMenuOption(
+ ctx,
+ icon: Icons.usb,
+ label: 'USB 摄像头',
+ onTap: () {
+ Navigator.pop(ctx);
+ Navigator.of(context).push(
+ MaterialPageRoute(
+ builder: (_) => const UsbCameraPage(),
+ ),
+ );
+ },
+ ),
],
),
),
diff --git a/packages/cc_ui_kit/lib/src/cc_expand_slider.dart b/packages/cc_ui_kit/lib/src/cc_expand_slider.dart
index 356e6278..725d7f31 100644
--- a/packages/cc_ui_kit/lib/src/cc_expand_slider.dart
+++ b/packages/cc_ui_kit/lib/src/cc_expand_slider.dart
@@ -14,6 +14,11 @@ class CCExpandSlider extends StatefulWidget {
final String svgCenter;
final String svgEnd;
final double iconSize;
+
+ /// 图标额外旋转的 90° 段数。有些素材是按「横向滑块」预先旋过 90° 的
+ /// (如 remote_flame*,外层配合 RotatedBox 使用),竖向摆放时需要传 1 转回正。
+ /// 默认 0,不影响既有横 / 竖两种用法。
+ final int iconQuarterTurns;
final double borderRadius;
final VoidCallback? onStart;
final VoidCallback? onCenter;
@@ -28,6 +33,7 @@ class CCExpandSlider extends StatefulWidget {
required this.svgEnd,
required this.label,
this.iconSize = 22.0,
+ this.iconQuarterTurns = 0,
this.borderRadius = 12.0,
this.onStart,
this.onCenter,
@@ -292,12 +298,15 @@ class _CCExpandSliderState extends State {
} else {
color = Colors.white;
}
- return SvgPicture.asset(
- assetPath,
- width: widget.iconSize,
- height: widget.iconSize,
- fit: BoxFit.contain,
- colorFilter: ColorFilter.mode(color, BlendMode.srcIn),
+ return RotatedBox(
+ quarterTurns: widget.iconQuarterTurns,
+ child: SvgPicture.asset(
+ assetPath,
+ width: widget.iconSize,
+ height: widget.iconSize,
+ fit: BoxFit.contain,
+ colorFilter: ColorFilter.mode(color, BlendMode.srcIn),
+ ),
);
}
}
diff --git a/packages/usb_camera_android/android/src/main/AndroidManifest.xml b/packages/usb_camera_android/android/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..2c7d035a
--- /dev/null
+++ b/packages/usb_camera_android/android/src/main/AndroidManifest.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+