Skip to content
Open
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 .github/workflows/general.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
node-version: 24

- name: Setup Pana source analysis
run: tool/gh_actions/install_pana.sh
Expand Down
4 changes: 4 additions & 0 deletions devtools_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:
- rohd: true
2 changes: 1 addition & 1 deletion extension/devtools/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ name: rohd
issueTracker: https://github.com/intel/rohd/issues
version: 0.0.1
materialIconCodePoint: '0xe1c5'
requiresConnection: true # optional field - defaults to true
requiresConnection: false
28 changes: 28 additions & 0 deletions rohd_devtools_extension/assets/help/details_help.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# ℹ️ Module Details — Help

<!-- tooltip -->

Signal Details
Click module Select module to view signals
Signal list Shows ports and internal signals

Signal Values
Value column Current signal value (hex/binary)
Width column Bit width of each signal

<!-- details -->

## Signal Details

| Action | Description |
| --- | --- |
| Click module (tree) | Select module and populate signal list |
| Signal list | Shows input ports, output ports, and internal signals |
| Value column | Displays the current value of each signal |
| Width column | Shows the bit width of each signal |

## Export

| Action | Description |
| --- | --- |
| 📷 Camera | Export signal table as PNG image |
34 changes: 34 additions & 0 deletions rohd_devtools_extension/assets/help/devtools_help.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# 🛠 ROHD DevTools — Help

<!-- tooltip -->

Module Tree (left panel)
Click node Select module
Click ▸ / ▾ Expand / collapse
🔃 Refresh Reload hierarchy from VM
Type in search Filter modules by name

Details (right panel)
Signal list Shows ports and internal signals
Search Filter signals by name
Filter Toggle input / output visibility

<!-- details -->

## Module Tree (left panel)

| Key | Description |
| --- | --- |
| Click module | Select module and show signals |
| Click ▸ / ▾ | Expand or collapse sub-modules |
| 🔃 Refresh | Reload hierarchy from the VM |
| Type in search | Filter modules by name |

## Signal Details (right panel)

| Key | Description |
| --- | --- |
| Signal list | Shows input ports, output ports, and internal signals |
| Search | Filter signals by name |
| Filter icon | Toggle input / output signal visibility |
| 📷 Export | Export signal details as PNG |
127 changes: 114 additions & 13 deletions rohd_devtools_extension/lib/rohd_devtools/cubit/rohd_service_cubit.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Intel Corporation
// Copyright (C) 2025-2026 Intel Corporation
// SPDX-License-Identifier: BSD-3-Clause
//
// rohd_service_cubit.dart
Expand All @@ -7,27 +7,76 @@
// 2025 January 28
// Author: Roberto Torres <roberto.torres@intel.com>

import 'dart:async';

import 'package:devtools_app_shared/service.dart';
import 'package:devtools_app_shared/utils.dart';
import 'package:devtools_extensions/devtools_extensions.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:rohd_devtools_extension/rohd_devtools/models/tree_model.dart';
import 'package:rohd_devtools_extension/rohd_devtools/services/tree_service.dart';
import 'package:vm_service/vm_service.dart' as vm;

part 'rohd_service_state.dart';

/// Cubit for managing ROHD service state.
class RohdServiceCubit extends Cubit<RohdServiceState> {
/// The TreeService instance for ROHD.
TreeService? treeService;

/// The discovered ROHD isolate ID.
///
/// Exposed so other consumers (e.g. waveform data source) can target the
/// same isolate that contains the ROHD inspector_service library.
String? get rohdIsolateId => _rohdIsolateId;
String? _rohdIsolateId;

/// Listener for service connection state changes.
void Function()? _connectionListener;

/// Constructor for RohdServiceCubit.
RohdServiceCubit() : super(RohdServiceInitial()) {
evalModuleTree();
// Listen for service connection state changes.
_connectionListener = _onConnectionStateChanged;
serviceManager.connectedState.addListener(_connectionListener!);
// Check if already connected (in case we missed the event).
if (serviceManager.connectedState.value.connected) {
unawaited(Future.microtask(evalModuleTree));
}
}

void _onConnectionStateChanged() {
final connected = serviceManager.connectedState.value.connected;
if (connected) {
// Reset tree service so we use the new connection.
treeService = null;
unawaited(evalModuleTree());
} else {
// VM disconnected — reset stale references.
treeService = null;
_rohdIsolateId = null;
emit(RohdServiceInitial());
}
}

@override
Future<void> close() {
if (_connectionListener != null) {
serviceManager.connectedState.removeListener(_connectionListener!);
_connectionListener = null;
}
return super.close();
}

/// Evaluate the module tree from the ROHD service.
Future<void> evalModuleTree() async {
await _handleModuleTreeOperation(
(treeService) => treeService.evalModuleTree());
}

/// Refresh the module tree from the ROHD service.
Future<void> refreshModuleTree() async {
await _handleModuleTreeOperation(
(treeService) => treeService.refreshModuleTree());
Expand All @@ -37,20 +86,72 @@ class RohdServiceCubit extends Cubit<RohdServiceState> {
Future<TreeModel?> Function(TreeService) operation) async {
try {
emit(RohdServiceLoading());

if (serviceManager.service == null) {
throw Exception('ServiceManager is not initialized');
// When not running in DevTools, emit loaded with null tree.
emit(const RohdServiceLoaded(null));
return;
}

if (treeService == null) {
// Find the isolate that actually has the ROHD library loaded.
// With `dart test`, the DevTools "selected" isolate is often the
// test-runner controller which doesn't import package:rohd. We
// need to scan all isolates to find the one with inspector_service.
final service = serviceManager.service!;
ValueListenable<vm.IsolateRef?>? rohdIsolate;

try {
final vmInfo = await service.getVM();
final isolates = vmInfo.isolates ?? [];

for (final isoRef in isolates) {
final id = isoRef.id;
if (id == null) continue;
try {
final iso = await service.getIsolate(id);
final libs = iso.libraries ?? [];
final hasRohd = libs.any((lib) =>
lib.uri ==
'package:rohd/src/diagnostics/inspector_service.dart');
if (hasRohd) {
debugPrint('[RohdServiceCubit] Found ROHD in '
'${isoRef.name}');
rohdIsolate = ValueNotifier(isoRef);
_rohdIsolateId = id;
break;
}
} on Exception {
// Isolate not loaded yet — skip.
continue;
}
}
} on Exception catch (e) {
debugPrint('[RohdServiceCubit] VM scan failed: $e');
}

if (rohdIsolate == null) {
debugPrint('[RohdServiceCubit] ROHD isolate not found, '
'falling back to selected isolate');
}

treeService = TreeService(
EvalOnDartLibrary(
'package:rohd/src/diagnostics/inspector_service.dart',
service,
serviceManager: serviceManager,
isolate: rohdIsolate,
),
Disposable(),
);
}
treeService ??= TreeService(
EvalOnDartLibrary(
'package:rohd/src/diagnostics/inspector_service.dart',
serviceManager.service!,
serviceManager: serviceManager,
),
Disposable(),
);

final treeModel = await operation(treeService!);
emit(RohdServiceLoaded(treeModel));
} catch (error, trace) {
} on Exception catch (error, trace) {
// Reset treeService so next attempt re-scans for the ROHD isolate.
treeService = null;
_rohdIsolateId = null;
emit(RohdServiceError(error.toString(), trace));
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import 'dart:convert';

import 'package:devtools_app_shared/service.dart';
import 'package:devtools_app_shared/utils.dart';
import 'package:flutter/foundation.dart';
import 'package:rohd_devtools_extension/rohd_devtools/models/tree_model.dart';

class TreeService {
Expand All @@ -28,8 +30,7 @@ class TreeService {
final treeObj = jsonDecode(treeInstance.valueAsString ?? '') as Map;

if (treeObj['status'] == 'fail') {
print('error');

debugPrint('[TreeService] evalModuleTree failed: ${treeObj['message']}');
return null;
} else {
return TreeModel.fromJson(jsonDecode(treeInstance.valueAsString ?? ""));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright (C) 2026 Intel Corporation
// SPDX-License-Identifier: BSD-3-Clause
//
// details_help_button.dart
// Help button widget for the Details tab.
//
// Content is loaded from assets/help/details_help.md.
// Edit that markdown file to update hover tooltip and dialog content.
//
// 2026 April
// Author: Desmond Kirkpatrick <desmond.a.kirkpatrick@intel.com>

import 'package:flutter/material.dart';

import 'package:rohd_devtools_widgets/rohd_devtools_widgets.dart';

/// A help button for the Details tab.
///
/// Content is driven by `assets/help/details_help.md`.
/// Edit that file to update the hover tooltip and click-open dialog.
class DetailsHelpButton extends StatelessWidget {
/// Whether the current theme is dark mode.
final bool isDark;

/// Create a [DetailsHelpButton].
const DetailsHelpButton({required this.isDark, super.key});

@override
Widget build(BuildContext context) => MarkdownHelpButton(
assetPath: 'assets/help/details_help.md',
isDark: isDark,
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
// Author: Yao Jing Quek <yao.jing.quek@intel.com>

import 'package:flutter/material.dart';
import 'package:rohd_devtools_extension/rohd_devtools/ui/devtools_help_button.dart';

class DevtoolAppBar extends StatelessWidget implements PreferredSizeWidget {
const DevtoolAppBar({
Expand All @@ -16,11 +17,17 @@ class DevtoolAppBar extends StatelessWidget implements PreferredSizeWidget {

@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;

return AppBar(
backgroundColor: Theme.of(context).colorScheme.onPrimary,
title: const Text('ROHD DevTool (Beta)'),
leading: const Icon(Icons.build),
actions: <Widget>[
// ── Help ──
DevToolsHelpButton(isDark: isDark),

// ── Licenses ──
Padding(
padding: const EdgeInsets.only(right: 20.0),
child: MouseRegion(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright (C) 2026 Intel Corporation
// SPDX-License-Identifier: BSD-3-Clause
//
// devtools_help_button.dart
// Help button widget for the ROHD DevTools app bar.
//
// Content is loaded from assets/help/devtools_help.md.
// Edit that markdown file to update hover tooltip and dialog content.
//
// 2026 April
// Author: Desmond Kirkpatrick <desmond.a.kirkpatrick@intel.com>

import 'package:flutter/material.dart';

import 'package:rohd_devtools_widgets/rohd_devtools_widgets.dart';

/// A help button for the ROHD DevTools app bar.
///
/// Content is driven by `assets/help/devtools_help.md`.
/// Edit that file to update the hover tooltip and click-open dialog.
class DevToolsHelpButton extends StatelessWidget {
/// Whether the current theme is dark mode.
final bool isDark;

/// Create a [DevToolsHelpButton].
const DevToolsHelpButton({required this.isDark, super.key});

@override
Widget build(BuildContext context) => MarkdownHelpButton(
assetPath: 'assets/help/devtools_help.md',
isDark: isDark,
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,9 @@ class _ModuleTreeCardState extends State<ModuleTreeCard> {
children: [
Container(
decoration: BoxDecoration(
color:
isSelected ? Colors.blue.withOpacity(0.2) : Colors.transparent,
color: isSelected
? Colors.blue.withValues(alpha: 0.2)
: Colors.transparent,
borderRadius: BorderRadius.circular(4.0),
),
padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class ModuleTreeDetailsNavbar extends StatelessWidget {
type: BottomNavigationBarType.fixed,
backgroundColor: const Color(0x1B1B1FEE),
selectedItemColor: Colors.white,
unselectedItemColor: Colors.white.withOpacity(.60),
unselectedItemColor: Colors.white.withValues(alpha: .60),
selectedFontSize: 10,
unselectedFontSize: 10,
onTap: (value) {
Expand Down
Loading
Loading