環境
この記事は以下の環境で動いています。
項目 | 値 |
---|---|
CPU | Core i5-8250U |
Ubuntu | 22.04 |
ROS | humble |
インストールについてはROS講座02 インストールを参照してください。
またこの記事のプログラムはgithubにアップロードされています。ROS講座11 gitリポジトリを参照してください。
当flutterアプリのソースコードはsrs_simple_tablet_clientで更新しています。
新しいflutterプロジェクトの作成
flutter create srs_simple_tablet_client
packageの追加
必要なライブライを追加します。以下のコマンドで追加できます。
cd srs_simple_tablet_client
flutter pub add shared_preferences
flutter pub add web_socket_channel
flutter pub add http
ソースコード
今回は
- トップページ: ipアドレスを入力して、メインページに遷移
- メインページ: サーバーとやり取りして情報を表示したり、コマンドを送ったりする
という構成にします。main.dart、TopPage.dart、ExecutePage.dart の3つのファイルを記述します。
TopPage.dart
srs_simple_tablet_client/lib/TopPage.dart
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import './ExecutePage.dart';
class TopPage extends StatefulWidget {
const TopPage({super.key});
@override
State<TopPage> createState() => _TopPage();
}
class _TopPage extends State<TopPage> {
var _hostname_input_controller = TextEditingController();
final Future<SharedPreferences> _prefs = SharedPreferences.getInstance();
@override
void initState() {
super.initState();
Future(() async {
final SharedPreferences prefs = await SharedPreferences.getInstance();
setState(() {
_hostname_input_controller.text = prefs.getString('target_hostname') ?? '';
print(prefs.getString('target_hostname'));
});
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('srs simple tablet client'),
centerTitle: true,
),
body: Container(
padding: EdgeInsets.all(32.0),
child: Center(
child: Column(
children: <Widget>[
Container(
width: 150,
child: TextField(
controller: _hostname_input_controller,
),
),
SizedBox(height: 10),
ElevatedButton(
onPressed: () async{
var hostname = _hostname_input_controller.text;
final SharedPreferences prefs = await _prefs;
prefs.setString('target_hostname', hostname);
Navigator.push(
context,
MaterialPageRoute(builder: (context)=>ExecutePage(hostname: hostname),)
);
},
child: Text('Execute Window'),
),
],
),
),
),
);
}
}
ExecutePage.dart
srs_simple_tablet_client/lib/ExecutePage.dart
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'package:web_socket_channel/web_socket_channel.dart';
class SimpleDialogSample extends StatelessWidget {
final List<Widget> optionList;
const SimpleDialogSample({Key? key, required this.optionList}) : super(key: key);
@override
Widget build(BuildContext context) {
return SimpleDialog(
title: Text('select item'),
children: optionList,
);
}
}
class ExecutePage extends StatefulWidget {
const ExecutePage({Key? key, required this.hostname}) : super(key: key);
final String hostname;
final int port = 8010;
@override
State<ExecutePage> createState() => _ExecutePage();
}
class _ExecutePage extends State<ExecutePage> {
final _scrollController = ScrollController();
WebSocketChannel? statusChannel;
@override
void initState() {
super.initState();
statusChannel = WebSocketChannel.connect(Uri.parse('ws://${widget.hostname}:8010/status'));
}
@override
Widget build(BuildContext context) {
createDrawerListItem(goal_id, key, display_name){
return ListTile(
title: Text(display_name),
onTap: () async {
final response = await http.post(
Uri.http('${widget.hostname}:8010', '/select/drawer'),
headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8',
},
body: jsonEncode(<String, String>{
'goal_id': goal_id,
'key': key
}));
print("response: " + response.body);
Navigator.of(context).pop();
}
);
}
createActionOptionItem(goal_id, key, display_name){
return SimpleDialogOption(
child: Text(display_name),
onPressed: () async {
final response = await http.post(
Uri.http('${widget.hostname}:8010', '/select/action'),
headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8',
},
body: jsonEncode(<String, String>{
'goal_id': goal_id,
'key': key
}));
print("response: " + response.body);
Navigator.pop(context);
},
);
}
return StreamBuilder(
stream: statusChannel?.stream,
builder: (context, snapshot) {
var modeText = "no connection";
var modeColor = Colors.white54;
var boardIconFileName = 'images/326633_error_icon.png';
var boardText = "erorr";
List<Widget> noteWidgets = [];
Widget? actionButtonWidget = null;
List<Widget> modeWidgets = [];
if (snapshot.connectionState == ConnectionState.active && snapshot.hasData) {
var jsonData = jsonDecode(snapshot.data);
// select.drawer
modeWidgets.add(const DrawerHeader(
child: Text("Operation Mode")
));
for (var item in jsonData["select"]["drawer"]["options"]){
modeWidgets.add(createDrawerListItem(item["goal_id"], item["key"], item["display_name"]));
}
// select.action
if (0 < jsonData["select"]["action"]["options"].length) {
List<Widget> optionList = [];
for (var item in jsonData["select"]["action"]["options"]) {
optionList.add(createActionOptionItem(item["goal_id"], item["key"], item["display_name"]));
}
actionButtonWidget = FloatingActionButton(
onPressed: () async {
final String? selectedText = await showDialog<String>(
context: context,
builder: (_) {
return SimpleDialogSample(optionList: optionList,);
}
);
print(selectedText);
},
backgroundColor: Colors.white,
child: const Icon(Icons.navigation),
);
}
// display.header
modeText = jsonData['display']['header']['name'];
final Map<String, Color> modeColorMap = {
'red' : Colors.red,
'green' : Colors.green,
'blue' : Colors.blue,
'yellow' : Colors.yellow,
'gray' : Colors.white54
};
if (modeColorMap.containsKey(jsonData['display']['header']['color'])) {
modeColor = modeColorMap[jsonData['display']['header']['color']]!;
}
// display.board
final Map<String, String> iconImageMap = {
'error' : "images/326633_error_icon.png",
'move' : "images/9057017_play_button_o_icon.png",
'pause' : "images/3671827_outline_pause_icon.png",
'turn' : "images/6428070_arrow_recycle_refresh_reload_return_icon.png",
'manual' : "images/9025635_game_controller_icon.png",
'wait_input' : "images/9165539_tap_touch_icon.png",
};
if (iconImageMap.containsKey(jsonData["display"]["board"]['icon'])) {
boardIconFileName = iconImageMap[jsonData["display"]["board"]['icon']]!;
}
boardText = jsonData["display"]["board"]["message"];
// notes
for (var jsonItem in jsonData['note']['contents']){
var noteColor = Colors.white38;
final Map<String, Color> noteColorMap = {
'info' : Colors.white,
'warning' : Colors.yellow,
'error' : Colors.red,
};
if (noteColorMap.containsKey(jsonItem['level'])) {
noteColor = noteColorMap[jsonItem['level']]!;
}
noteWidgets.add(
Text(
jsonItem["message"],
style: TextStyle(fontSize: 30, color:noteColor),
)
);
}
}
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
backgroundColor: modeColor,
title: Text(
modeText,
style: TextStyle(
fontSize: 80,
),
),
centerTitle: true,
toolbarHeight: 150,
),
endDrawer: Drawer(
child: ListView(
children: modeWidgets
)
),
floatingActionButton: actionButtonWidget,
body: Column(
children: <Widget>[
Expanded(
flex: 4, // 割合.
child: Container(
color: Colors.white12,
child: Scrollbar(
thumbVisibility: true,
trackVisibility: true,
thickness: 12,
controller: _scrollController,
child: ListView(
controller: _scrollController,
children: noteWidgets
),
),
),
),
Expanded(
flex: 6, // 割合.
child: Container(
child: Column(
children: [
Expanded(
child: Padding(
padding: EdgeInsets.all(15),
child: Image.asset(boardIconFileName, color: Colors.white)
),
),
Text(boardText,
style: TextStyle(
fontSize: 80,
color: Colors.white
),
),
],
)
),
)
]
),
);
}
);
}
main.dart
srs_simple_tablet_client/lib/main.dart
import 'package:flutter/material.dart';
import './TopPage.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'srs_simple_tablet_client',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: TopPage(),
);
}
}
画像の配置
srs_simple_tablet_client/image
以下にDartから参照する画像を配置します。
配置後以下のようにファイルを更新する必要があります。
srs_simple_tablet_client/pubspec.yaml
flutter:
assets: # 追加
- images # 追加
実行
サーバー側の実行
source ~/ros2_ws/install/setup.bash
ros2 run srs_simple_tablet_server_nodes api_server
flutterプログラムの実行
cd srs_simple_tablet_client
flutter run -d linux