diff --git a/.gitignore b/.gitignore index 24476c5..1336b68 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,6 @@ app.*.map.json /android/app/debug /android/app/profile /android/app/release + +key.properties +/key \ No newline at end of file diff --git a/android/app/build.gradle b/android/app/build.gradle index df42401..b29ba78 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -51,11 +51,26 @@ android { versionName flutterVersionName } + def keystoreProperties = new Properties() + def keystorePropertiesFile = rootProject.file('key.properties') + if (keystorePropertiesFile.exists()) { + keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) + } + + signingConfigs { + release { + keyAlias keystoreProperties['keyAlias'] + keyPassword keystoreProperties['keyPassword'] + storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null + storePassword keystoreProperties['storePassword'] + } + } + buildTypes { release { // TODO: Add your own signing config for the release build. // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig signingConfigs.debug + signingConfig signingConfigs.release } } } diff --git a/lib/boxes/console.dart b/lib/boxes/console.dart new file mode 100644 index 0000000..a21332d --- /dev/null +++ b/lib/boxes/console.dart @@ -0,0 +1,228 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:intl/intl.dart'; +import 'package:ju_rc_app/lib/boxes.dart'; +import 'package:ju_rc_app/lib/serial.dart'; + +class SerialBox extends JuBox { + const SerialBox({super.key}); + + @override + State createState() => _SerialBoxState(); +} + +class _SerialBoxState extends State { + USerial serial = getSerial(); + final ScrollController _controller = ScrollController(); + + @override + void initState() { + super.initState(); + serial.listen(serialListen); + } + + @override + void dispose() { + serial.removeListen(serialListen); + super.dispose(); + } + + void serialListen(String line, SerialCommand? _) { + setState(() { + //change serial lines + _controller.jumpTo(_controller.position.maxScrollExtent); + }); + } + + @override + Widget build(BuildContext context) => GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const SerialDetailPage()), + ); + }, + child: ListView( + controller: _controller, + shrinkWrap: false, + physics: const NeverScrollableScrollPhysics(), + children: serial.lines + .last(20) + .map( + (e) => Row( + children: [ + Padding( + padding: const EdgeInsets.all(0.0), + child: Icon(e.$1 != SerialMessageType.received + ? Icons.arrow_left + : Icons.arrow_right), + ), + Expanded( + child: Text( + e.$3, + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), + ], + ), + ) + .toList(), + )); +} + +class SerialDetailPage extends StatefulWidget { + const SerialDetailPage({super.key}); + + @override + State createState() => _SerialDetailPageState(); +} + +class _SerialDetailPageState extends State { + final ScrollController _controller = ScrollController(); + USerial serial = getSerial(); + bool jump = true; + final List _messages = []; + int messageIndex = 0; + final TextEditingController _textController = TextEditingController(); + FocusNode fc = FocusNode(); + FocusNode _textFocus = FocusNode(); + + @override + void initState() { + super.initState(); + serial.listen(serialListen); + fc.requestFocus(); + } + + @override + void dispose() { + serial.removeListen(serialListen); + _textController.dispose(); + _controller.dispose(); + super.dispose(); + } + + void serialListen(String line, SerialCommand? _) { + if (!jump) return; + setState(() { + //change serial lines + _controller.jumpTo(_controller.position.maxScrollExtent); + }); + } + + void submit() { + if (_textController.text.isNotEmpty) { + setState(() { + serial.sprintln(_textController.text); + _messages.add(_textController.text); + messageIndex = _messages.length; + _textController.clear(); + }); + _textFocus.requestFocus(); + } + } + + void _handleKeyEvent(RawKeyEvent event) { + if (event.runtimeType.toString() == 'RawKeyDownEvent') { + if (event.logicalKey == LogicalKeyboardKey.arrowUp) { + if (messageIndex > 0) { + setState(() { + messageIndex--; + print("up: " + _messages[messageIndex]); + _textController.text = _messages[messageIndex]; + }); + } + } else if (event.logicalKey == LogicalKeyboardKey.arrowDown) { + if (messageIndex >= _messages.length - 1) { + setState(() { + print("clear"); + messageIndex = _messages.length; + _textController.clear(); + _textFocus.requestFocus(); + }); + } else { + setState(() { + messageIndex++; + print("down: " + _messages[messageIndex]); + _textController.text = _messages[messageIndex]; + _textFocus.requestFocus(); + }); + } + } + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + title: const Text( + "Serial Console", + style: TextStyle( + fontSize: 16, + ), + ), + toolbarHeight: 40, + ), + body: Column( + children: [ + Expanded( + child: ListView( + controller: _controller, + children: serial.lines.items + .map( + (e) => Row( + children: [ + Text(DateFormat('HH:mm:ss.S').format(e.$2)), + Padding( + padding: const EdgeInsets.all(0.0), + child: Icon(e.$1 != SerialMessageType.received + ? Icons.arrow_left + : Icons.arrow_right), + ), + Expanded( + child: Text( + e.$3, + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), + ], + ), + ) + .toList(), + )), + RawKeyboardListener( + focusNode: fc, + autofocus: true, + onKey: _handleKeyEvent, + child: Padding( + padding: const EdgeInsets.fromLTRB(8.0, 0, 8.0, 8.0), + child: Row( + children: [ + Expanded( + child: TextField( + focusNode: _textFocus, + controller: _textController, + autofocus: true, + decoration: const InputDecoration( + hintText: 'Enter a Command', + ), + onSubmitted: (_) { + submit(); + }, + ), + ), + IconButton( + icon: const Icon(Icons.send), + onPressed: submit, + ), + ], + ), + )) + ], + )); + } +} diff --git a/lib/boxes/state.dart b/lib/boxes/state.dart new file mode 100644 index 0000000..422fc7f --- /dev/null +++ b/lib/boxes/state.dart @@ -0,0 +1,84 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:ju_rc_app/lib/boxes.dart'; +import 'package:ju_rc_app/lib/serial.dart'; + +class ControllerState extends JuBox { + const ControllerState({super.key}); + + @override + State createState() => _ControllerStateState(); +} + +class _ControllerStateState extends State { + USerial serial = getSerial(); + late Timer timer; + + int connected = 0; + bool mode = false; //true is ble + int arc = 0; + double akku = 0.1; + + @override + void initState() { + super.initState(); + serial.listen(serialListen); + timer = Timer.periodic(const Duration(seconds: 1), (_) async { + serial.sprintln("", system: true); + connected--; + serial.sprintln("", system: true); + serial.sprintln("", system: true); + serial.sprintln("", system: true); + }); + } + + @override + void dispose() { + serial.removeListen(serialListen); + timer.cancel(); + super.dispose(); + } + + void serialListen(String line, SerialCommand? cmdo) { + if (cmdo == null) return; + var cmd = cmdo; + setState(() { + if (cmd.command == "pong") connected = 2; + if (cmd.command == "sendMode") { + if (cmd.arg == "RF") { + mode = false; + } else if (cmd.arg == "BLE") { + mode = true; + } + } + if(cmd.command == "rfArc"){ + arc = int.tryParse(cmd.arg) ?? 15; + } + if(cmd.command == "voltage"){ + akku = double.tryParse(cmd.arg) ?? 0.0; + } + }); + } + + @override + Widget build(BuildContext context) => Column( + children: [ + const Text("Remote controll state"), + Text( + "Serial Connection: ${connected > 0 ? "connected" : "disconnected"}"), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text("RC Mode: "), + if (mode) + const Icon(Icons.bluetooth) + else + const Icon(Icons.settings_input_antenna) + ], + ), + Text("ARC: $arc"), + Text("Akku: ${akku}V") + ], + ); +} diff --git a/lib/connector.dart b/lib/connector.dart index 07ae0b9..9c632af 100644 --- a/lib/connector.dart +++ b/lib/connector.dart @@ -3,33 +3,39 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:ju_rc_app/lib/serial.dart'; +// ignore: must_be_immutable class ConnectionBar extends StatefulWidget { - const ConnectionBar({super.key}); + USerial serial = getSerial(); + ConnectionBar({super.key}); @override - ConnectionBarState createState() => ConnectionBarState(); + // ignore: library_private_types_in_public_api + _ConnectionBarState createState() => _ConnectionBarState(); } -class ConnectionBarState extends State { - late USerial serial; - UPort? connectedTo; - List _ports = []; - StateSetter? sheetsetstate; +class _ConnectionBarState extends State { late Timer timer; String _status = ""; + List _ports = []; + StateSetter? sheetsetstate; + UPort? connectedTo; - ConnectionBarState() { - serial = getSerial(); - serial.listen((line) {}); + _ConnectionBarState() { timer = Timer.periodic(const Duration(seconds: 1), (_) async { if (sheetsetstate == null) return; - var ports = await serial.getPorts(); + var ports = await widget.serial.getPorts(); setState(() { _ports = ports; }); }); } + @override + void dispose() { + timer.cancel(); + super.dispose(); + } + @override void setState(VoidCallback fn) { super.setState(fn); @@ -40,14 +46,15 @@ class ConnectionBarState extends State { void showModal() async { if (connectedTo != null && connectedTo!.connected) { - await serial.disconnect(); + await widget.serial.disconnect(); setState(() { connectedTo = null; }); } else { - var ports = await serial.getPorts(); + var ports = await widget.serial.getPorts(); setState(() { _ports = ports; + _status = ""; }); // ignore: use_build_context_synchronously showModalBottomSheet( @@ -56,6 +63,7 @@ class ConnectionBarState extends State { builder: (BuildContext context, StateSetter setStateM) { sheetsetstate = setStateM; return BottomSheet( + enableDrag: false, builder: (context) => Column(children: [ Padding( padding: const EdgeInsets.all(8), @@ -80,24 +88,24 @@ class ConnectionBarState extends State { Text(port.connected ? "Disconnect" : "Connect"), onPressed: () async { if (port.connected) { - await serial.disconnect(); + await widget.serial.disconnect(); setState(() { connectedTo = null; _status = "Successfully disconnected!"; }); } else { - if (await serial.connect(port)) { + if (await widget.serial.connect(port)) { setState(() { connectedTo = port; _status = "Successfully connected!"; }); - }else{ + } else { setState(() { _status = "Error while connecting!"; }); } } - var ports = await serial.getPorts(); + var ports = await widget.serial.getPorts(); setState(() { _ports = ports; }); diff --git a/lib/lib/boxes.dart b/lib/lib/boxes.dart new file mode 100644 index 0000000..056239c --- /dev/null +++ b/lib/lib/boxes.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; + +class SerialCommand { + String command; + String arg; + SerialCommand(this.command, this.arg); + + static SerialCommand? parse(String line) { + if (!line.startsWith("<")) return null; + int cend = line.indexOf(">"); + if (cend < 0) return null; + return SerialCommand( + line.substring(1, cend), line.substring(cend + 1).replaceAll("\r", "")); + } +} + +abstract class JuBox extends StatefulWidget { + const JuBox({super.key}); +} diff --git a/lib/lib/serial.dart b/lib/lib/serial.dart index 4c9180d..63a1570 100644 --- a/lib/lib/serial.dart +++ b/lib/lib/serial.dart @@ -1,11 +1,33 @@ import 'dart:async'; import 'dart:io'; +import 'dart:math'; import 'dart:typed_data'; import 'package:flutter_libserialport/flutter_libserialport.dart'; +import 'package:ju_rc_app/lib/boxes.dart'; import 'package:usb_serial/transaction.dart'; import 'package:usb_serial/usb_serial.dart'; +enum SerialMessageType { systemSend, userSend, received } + +class LimitedList { + final int maxLength; + final List _items = []; + + LimitedList(this.maxLength); + + void add(T item) { + if (_items.length >= maxLength) { + _items.removeAt(0); + } + _items.add(item); + } + + List last(int len) => _items.sublist(max(0, _items.length - len)); + + List get items => _items; +} + abstract class UPort { String productName; String manufacturerName; @@ -17,33 +39,48 @@ abstract class USerial { Future> getPorts(); Future connect(UPort port); Future disconnect(); - Future sprint(String data); - Future sprintln(String data) { - return sprint("$data\n"); + Future _sprint(String data); + Future sprintln(String data, {bool system = false}) { + lines.add(( + system ? SerialMessageType.systemSend : SerialMessageType.userSend, + DateTime.now(), + data + )); + return _sprint("$data\n"); } - List lines = []; - Function(String)? _callback; + LimitedList<(SerialMessageType, DateTime, String)> lines = + LimitedList(524288); + final List _callback = []; void _receive(String line) { - lines.add(line); - if (_callback != null) { - _callback!(line); + lines.add((SerialMessageType.received, DateTime.now(), line)); + var parsed = SerialCommand.parse(line); + for (var f in _callback) { + f(line, parsed); } } - void listen(Function(String) callback) { - _callback = callback; + void listen(Function(String, SerialCommand?) callback) { + _callback.add(callback); + } + + void removeListen(Function(String, SerialCommand?) callback) { + _callback.remove(callback); } } +USerial? _serial; + USerial getSerial() { + if (_serial != null) return _serial as USerial; if (Platform.isAndroid) { - return USerialAndroid(); + _serial = USerialAndroid(); } else if (Platform.isLinux || Platform.isWindows || Platform.isMacOS) { - return USerialPC(); + _serial = USerialPC(); } else { throw UnimplementedError(); } + return _serial as USerial; } class UPortAndroid extends UPort { @@ -110,7 +147,7 @@ class USerialAndroid extends USerial { 115200, UsbPort.DATABITS_8, UsbPort.STOPBITS_1, UsbPort.PARITY_NONE); _transaction = Transaction.stringTerminated( - _port!.inputStream as Stream, Uint8List.fromList([13, 10])); + _port!.inputStream as Stream, Uint8List.fromList([10])); _subscription = _transaction!.stream.listen((String line) { _receive(line); @@ -140,7 +177,7 @@ class USerialAndroid extends USerial { } @override - Future sprint(String data) async { + Future _sprint(String data) async { if (_port == null) return false; await _port!.write(Uint8List.fromList(data.codeUnits)); return true; @@ -205,7 +242,7 @@ class USerialPC extends USerial { _reader = SerialPortReader(_port!); _transaction = Transaction.stringTerminated( - _reader!.stream, Uint8List.fromList([13, 10])); + _reader!.stream, Uint8List.fromList([10])); _subscription = _transaction!.stream.listen((line) { _receive(line); @@ -245,7 +282,7 @@ class USerialPC extends USerial { } @override - Future sprint(String data) async { + Future _sprint(String data) async { try { if (_port == null) return false; _port!.write(Uint8List.fromList(data.codeUnits)); diff --git a/lib/main.dart b/lib/main.dart index e557d89..5964297 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,10 @@ -import 'package:flutter/material.dart'; -import 'package:ju_rc_app/connector.dart'; +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:ju_rc_app/boxes/console.dart'; +import 'package:ju_rc_app/boxes/state.dart'; +import 'package:ju_rc_app/connector.dart'; +import 'package:ju_rc_app/lib/boxes.dart'; void main() { runApp(const MyApp()); @@ -14,8 +18,15 @@ class MyApp extends StatelessWidget { Widget build(BuildContext context) { return MaterialApp( title: 'JuRcApp', + themeMode: ThemeMode.system, theme: ThemeData( - colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), + //colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), + brightness: Brightness.light, + useMaterial3: true, + ), + darkTheme: ThemeData( + //colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), + brightness: Brightness.dark, useMaterial3: true, ), home: const MyHomePage(title: 'App for juRc'), @@ -32,36 +43,58 @@ class MyHomePage extends StatefulWidget { } class _MyHomePageState extends State { - ConnectionBar conBar = const ConnectionBar(); - + ConnectionBar conBar = ConnectionBar(); + + static List boxes = [const SerialBox(), const ControllerState()]; @override void initState() { super.initState(); - setState(() { - - }); - + setState(() {}); } + @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( backgroundColor: Theme.of(context).colorScheme.inversePrimary, - title: Text(widget.title), - ), - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - conBar - ], + title: Text( + widget.title, + style: const TextStyle( + fontSize: 16, + ), ), + toolbarHeight: 40, ), - floatingActionButton: FloatingActionButton( - onPressed: () {}, - tooltip: 'Reload', - child: const Icon(Icons.replay), + body: CustomScrollView( + slivers: [ + SliverToBoxAdapter( + child: conBar, + ), + SliverPadding( + padding: const EdgeInsets.all(10.0), + sliver: SliverGrid.extent( + maxCrossAxisExtent: + Platform.isAndroid || Platform.isIOS ? 300.0 : 600.0, + crossAxisSpacing: 10.0, + mainAxisSpacing: 10.0, + children: boxes + .map((b) => Container( + decoration: BoxDecoration( + border: Border.all( + width: 0, + color: const Color.fromARGB(0, 0, 0, 0)), + borderRadius: BorderRadius.circular(20.0), + color: Theme.of(context) + .colorScheme + .secondaryContainer, + ), + padding: const EdgeInsets.all(0.3 * 2 * 20), + child: b, + )) + .toList(), + )), + ], ), ); } diff --git a/pubspec.lock b/pubspec.lock index 4d963af..58eff46 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -69,10 +69,10 @@ packages: dependency: transitive description: name: collection - sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a url: "https://pub.dev" source: hosted - version: "1.17.2" + version: "1.18.0" convert: dependency: transitive description: @@ -192,6 +192,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.17" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + url: "https://pub.dev" + source: hosted + version: "0.18.1" js: dependency: transitive description: @@ -260,10 +268,10 @@ packages: dependency: transitive description: name: petitparser - sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750 + sha256: eeb2d1428ee7f4170e2bd498827296a18d4e7fc462b71727d111c0ac7707cfa6 url: "https://pub.dev" source: hosted - version: "5.4.0" + version: "6.0.1" platform: dependency: transitive description: @@ -321,18 +329,18 @@ packages: dependency: transitive description: name: stack_trace - sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.11.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" string_scanner: dependency: transitive description: @@ -353,10 +361,10 @@ packages: dependency: transitive description: name: test_api - sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "0.6.1" typed_data: dependency: transitive description: @@ -381,14 +389,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + web: + dependency: transitive + description: + name: web + sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 + url: "https://pub.dev" + source: hosted + version: "0.1.4-beta" xml: dependency: transitive description: name: xml - sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84" + sha256: af5e77e9b83f2f4adc5d3f0a4ece1c7f45a2467b695c2540381bac793e34e556 url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.4.2" yaml: dependency: transitive description: @@ -398,5 +414,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.1.0-163.1.beta <4.0.0" + dart: ">=3.1.0-185.0.dev <4.0.0" flutter: ">=3.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index bdfa2b3..7a3ccb6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -38,6 +38,7 @@ dependencies: flutter_libserialport: ^0.3.0 usb_serial: ^0.5.1 rive: ^0.11.16 + intl: ^0.18.1 dev_dependencies: flutter_test: