Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/backend/node_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ def __init__(self, tinynav_db_path: str = '/tinynav/tinynav_db'):
self._nav_target_pose: dict | None = None

self.create_subscription(Float32, '/mapping/percent', self._on_mapping_percent, 10)
self.create_subscription(Odometry, '/slam/odometry', self._on_slam_odom, 10)
self.create_subscription(Odometry, '/slam/odometry_visual', self._on_slam_odom, 10)
self.create_subscription(
Odometry, '/mapping/current_pose_in_map', self._on_pose_in_map, 10
)
Expand Down
184 changes: 115 additions & 69 deletions app/frontend/lib/pages/operate_tab.dart
Original file line number Diff line number Diff line change
Expand Up @@ -704,7 +704,7 @@ class _Local3dPlanningViewState extends State<_Local3dPlanningView> {
),
],
),
),
)
);
}
}
Expand Down Expand Up @@ -734,7 +734,7 @@ class _LocalViewModeButton extends StatelessWidget {
fontWeight: FontWeight.w700,
),
),
),
)
);
}
}
Expand Down Expand Up @@ -1049,6 +1049,7 @@ class _PoiSheet extends ConsumerStatefulWidget {
class _PoiSheetState extends ConsumerState<_PoiSheet> {
/// POI ids in the exact order they were checked.
final List<int> _checkedIds = [];
final ScrollController _poiScrollController = ScrollController();

Future<void> _deletePoi(Poi poi) async {
final ok = await showDialog<bool>(
Expand Down Expand Up @@ -1101,84 +1102,129 @@ class _PoiSheetState extends ConsumerState<_PoiSheet> {
}
}

@override
void dispose() {
_poiScrollController.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
final poisAsync = ref.watch(poisProvider);
final status = ref.watch(deviceStatusProvider).valueOrNull;
final localized = ref.watch(planningStreamProvider).valueOrNull?.localized ?? false;
final canGo = status != null && status.online && localized;

return Padding(
padding: EdgeInsets.fromLTRB(
16, 12, 16, 24 + MediaQuery.of(context).viewInsets.bottom),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Container(
width: 36, height: 4,
margin: const EdgeInsets.only(bottom: 14),
decoration: BoxDecoration(
color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(2),
),
),
return SafeArea(
top: false,
child: FractionallySizedBox(
heightFactor: 0.9,
child: Padding(
padding: EdgeInsets.fromLTRB(
16,
12,
16,
24 + MediaQuery.of(context).viewInsets.bottom,
),
// ── Header ──────────────────────────────────────────────────
Row(children: [
const Icon(Icons.place_outlined, size: 20),
const SizedBox(width: 8),
const Text('POIs', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
const Spacer(),
FilledButton.icon(
onPressed: (canGo && _checkedIds.isNotEmpty)
? () => _startNav(poisAsync.valueOrNull ?? [])
: null,
icon: const Icon(Icons.navigation_rounded, size: 16),
label: const Text('Go'),
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
minimumSize: Size.zero,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Container(
width: 36,
height: 4,
margin: const EdgeInsets.only(bottom: 14),
decoration: BoxDecoration(
color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(2),
),
),
),
),
]),
const Divider(height: 20),
// ── POI list ────────────────────────────────────────────────
poisAsync.when(
data: (pois) => pois.isEmpty
? const Padding(
padding: EdgeInsets.symmetric(vertical: 20),
child: Center(
child: Text('No POIs yet', style: TextStyle(color: Colors.grey)),
),
)
: Column(
children: pois
.map((poi) {
final orderIndex = _checkedIds.indexOf(poi.id);
return _PoiTile(
poi: poi,
checked: orderIndex != -1,
orderNumber: orderIndex == -1 ? null : orderIndex + 1,
onChecked: (v) => setState(() {
if (v) {
if (!_checkedIds.contains(poi.id)) {
_checkedIds.add(poi.id);
}
} else {
_checkedIds.remove(poi.id);
}
}),
onDelete: () => _deletePoi(poi),
);
})
.toList(),
Row(children: [
const Icon(Icons.place_outlined, size: 20),
const SizedBox(width: 8),
const Text('POIs', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
const Spacer(),
FilledButton.icon(
onPressed: (canGo && _checkedIds.isNotEmpty)
? () => _startNav(poisAsync.valueOrNull ?? [])
: null,
icon: const Icon(Icons.navigation_rounded, size: 16),
label: const Text('Go'),
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
minimumSize: Size.zero,
),
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Text('$e', style: const TextStyle(color: Colors.red)),
),
]),
const Divider(height: 20),
Expanded(
child: poisAsync.when(
data: (pois) => pois.isEmpty
? const Center(
child: Text('No POIs yet', style: TextStyle(color: Colors.grey)),
)
: ScrollConfiguration(
behavior: ScrollConfiguration.of(context).copyWith(
dragDevices: {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
PointerDeviceKind.trackpad,
PointerDeviceKind.stylus,
PointerDeviceKind.unknown,
},
),
child: Scrollbar(
thumbVisibility: pois.length > 8,
controller: _poiScrollController,
child: ListView.builder(
controller: _poiScrollController,
primary: false,
physics: const AlwaysScrollableScrollPhysics(),
padding: EdgeInsets.zero,
itemCount: pois.length,
itemBuilder: (context, index) {
final poi = pois[index];
final orderIndex = _checkedIds.indexOf(poi.id);
return _PoiTile(
poi: poi,
checked: orderIndex != -1,
orderNumber: orderIndex == -1 ? null : orderIndex + 1,
onChecked: (v) => setState(() {
if (v) {
if (!_checkedIds.contains(poi.id)) {
_checkedIds.add(poi.id);
}
} else {
_checkedIds.remove(poi.id);
}
}),
onDelete: () => _deletePoi(poi),
);
},
),
),
),
loading: () => ListView(
controller: _poiScrollController,
children: const [
SizedBox(
height: 180,
child: Center(child: CircularProgressIndicator()),
),
],
),
error: (e, _) => ListView(
controller: _poiScrollController,
children: [
Text('$e', style: const TextStyle(color: Colors.red)),
],
),
),
),
],
),
],
),
),
);
}
Expand Down
25 changes: 4 additions & 21 deletions app/frontend/test/widget_test.dart
Original file line number Diff line number Diff line change
@@ -1,30 +1,13 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import 'package:tinynav_app/main.dart';

void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const MyApp());

// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);

// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
testWidgets('TinyNav app smoke test', (WidgetTester tester) async {
await tester.pumpWidget(const ProviderScope(child: TinyNavApp()));
await tester.pump();

// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
expect(find.text('TinyNav'), findsOneWidget);
});
}
4 changes: 2 additions & 2 deletions tinynav/core/imu_propagator_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,9 @@ def __init__(self):
Imu, "/camera/camera/imu", self.imu_callback, qos_profile
)
self.odom_sub = self.create_subscription(
Odometry, "/slam/odometry", self.odom_callback, qos_profile
Odometry, "/slam/odometry_visual", self.odom_callback, qos_profile
)
self.odom_pub = self.create_publisher(Odometry, "/slam/odometry_100hz", 50)
self.odom_pub = self.create_publisher(Odometry, "/slam/odometry", 50)

self.imu_buffer = []
self.odom_10hz_buffer = []
Expand Down
2 changes: 1 addition & 1 deletion tinynav/core/perception_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ def __init__(self, verbose_timer: bool = True):
self.input_aligner.registerCallback(1, self._aligned_stereo_callback)
self.input_aligner_seen_imu = False
self.input_aligner_seen_stereo = False
self.odom_pub = self.create_publisher(Odometry, "/slam/odometry", 10)
self.odom_pub = self.create_publisher(Odometry, "/slam/odometry_visual", 10)
self.slam_camera_info_pub = self.create_publisher(CameraInfo, "/slam/camera_info", 10)
self.depth_pub = self.create_publisher(Image, "/slam/depth", 10)
self.disparity_pub_vis = self.create_publisher(Image, '/slam/disparity_vis', 10)
Expand Down
Loading
Loading