diff --git a/lib/brilliant_bluetooth.dart b/lib/brilliant_bluetooth.dart index 7b4b8ba..31fbfb0 100644 --- a/lib/brilliant_bluetooth.dart +++ b/lib/brilliant_bluetooth.dart @@ -84,6 +84,7 @@ class BrilliantDevice { } // logs each string message (messages without the 0x01 first byte) and provides a stream of the utf8-decoded strings + // Lua error strings come through here too, so logging at info Stream get stringResponse { // changed to only listen for data coming through the Frame's rx characteristic, not all attached devices as before return _rxChannel!.onValueReceived @@ -101,7 +102,7 @@ class BrilliantDevice { return _rxChannel!.onValueReceived .where((event) => event[0] == 0x01) .map((event) { - _log.fine("Received data: ${event.sublist(1)}"); + _log.finest("Received data: ${event.sublist(1)}"); return event.sublist(1); }); } @@ -162,8 +163,8 @@ class BrilliantDevice { Future sendData(List data) async { try { - _log.info("Sending ${data.length} bytes of plain data"); - _log.fine(data); + _log.finer("Sending ${data.length} bytes of plain data"); + _log.finest(data); if (state != BrilliantConnectionState.connected) { throw ("Device is not connected"); @@ -185,8 +186,8 @@ class BrilliantDevice { /// Same as sendData but user includes the 0x01 header byte to avoid extra memory allocation Future sendDataRaw(List data) async { try { - _log.info("Sending ${data.length-1} bytes of plain data"); - _log.fine(data); + _log.finer("Sending ${data.length-1} bytes of plain data"); + _log.finest(data); if (state != BrilliantConnectionState.connected) { throw ("Device is not connected"); @@ -201,7 +202,7 @@ class BrilliantDevice { } // TODO check throughput difference using withoutResponse: false - await _txChannel!.write(data, withoutResponse: true); + await _txChannel!.write(data, withoutResponse: false); } catch (error) { _log.warning("Couldn't send data. $error"); return Future.error(BrilliantBluetoothException(error.toString())); @@ -228,13 +229,16 @@ class BrilliantDevice { // instead point packetToSend to an UnmodifiableListView range within packetBuffer List packetBuffer = List.filled(maxDataLength! + 1, 0x00); List packetToSend = packetBuffer; + _log.fine('sendMessage: payload size: ${payload.length}'); while (sentBytes < payload.length) { if (firstPacket) { + _log.finer('sendMessage: first packet'); firstPacket = false; if (bytesRemaining < chunksize - 2) { // first and final chunk - small payload + _log.finer('sendMessage: first and final packet'); packetBuffer[0] = 0x01; packetBuffer[1] = messageFlag & 0xFF; packetBuffer[2] = lengthMsb; @@ -245,6 +249,7 @@ class BrilliantDevice { } else if (bytesRemaining == chunksize - 2) { // first and final chunk - small payload, exact packet size match + _log.finer('sendMessage: first and final packet, exact match'); packetBuffer[0] = 0x01; packetBuffer[1] = messageFlag & 0xFF; packetBuffer[2] = lengthMsb; @@ -255,6 +260,7 @@ class BrilliantDevice { } else { // first of many chunks + _log.finer('sendMessage: first of many packets'); packetBuffer[0] = 0x01; packetBuffer[1] = messageFlag & 0xFF; packetBuffer[2] = lengthMsb; @@ -267,6 +273,7 @@ class BrilliantDevice { else { // not the first packet if (bytesRemaining < chunksize) { + _log.finer('sendMessage: not the first packet, final packet'); // final data chunk, smaller than chunksize packetBuffer[0] = 0x01; packetBuffer[1] = messageFlag & 0xFF; @@ -275,6 +282,7 @@ class BrilliantDevice { packetToSend = UnmodifiableListView(packetBuffer.getRange(0, bytesRemaining + 2)); } else { + _log.finer('sendMessage: not the first packet, non-final packet or exact match final packet'); // non-final data chunk or final chunk with exact packet size match packetBuffer[0] = 0x01; packetBuffer[1] = messageFlag & 0xFF; @@ -286,8 +294,11 @@ class BrilliantDevice { // send the chunk await sendDataRaw(packetToSend); + // FIXME just seeing if a flow rate issue is causing Frame to miss packets + await Future.delayed(const Duration(milliseconds: 50)); bytesRemaining = payload.length - sentBytes; + _log.finer('Bytes remaining: $bytesRemaining'); } } @@ -298,6 +309,7 @@ class BrilliantDevice { String file = await rootBundle.loadString(filePath); file = file.replaceAll('\\', '\\\\'); + file = file.replaceAll("\r\n", "\\n"); file = file.replaceAll("\n", "\\n"); file = file.replaceAll("'", "\\'"); file = file.replaceAll('"', '\\"'); @@ -320,7 +332,7 @@ class BrilliantDevice { } // Don't split on an escape character - if (file[index + chunkSize - 1] == '\\') { + while (file[index + chunkSize - 1] == '\\') { chunkSize -= 1; } diff --git a/lib/simple_frame_app.dart b/lib/simple_frame_app.dart index 8a64e83..131af8d 100644 --- a/lib/simple_frame_app.dart +++ b/lib/simple_frame_app.dart @@ -13,8 +13,10 @@ enum ApplicationState { scanning, connecting, connected, + starting, ready, running, + canceling, stopping, disconnecting, } @@ -61,6 +63,8 @@ mixin SimpleFrameAppState on State { case ApplicationState.connected: case ApplicationState.ready: case ApplicationState.running: + case ApplicationState.starting: + case ApplicationState.canceling: // already connected, nothing to do break; default: @@ -96,6 +100,8 @@ mixin SimpleFrameAppState on State { await Future.delayed(const Duration(milliseconds: 500)); await frame!.sendBreakSignal(); + await frame!.sendString('print("Connected to Frame " .. frame.FIRMWARE_VERSION)'); + // Frame is ready to go! currentState = ApplicationState.connected; if (mounted) setState(() {}); @@ -292,7 +298,9 @@ mixin SimpleFrameAppState on State { case ApplicationState.initializing: case ApplicationState.scanning: case ApplicationState.connecting: + case ApplicationState.starting: case ApplicationState.running: + case ApplicationState.canceling: case ApplicationState.stopping: case ApplicationState.disconnecting: pfb.add(const TextButton(onPressed: null, child: Text('Connect'))); @@ -327,21 +335,42 @@ mixin SimpleFrameAppState on State { /// the SimpleFrameApp subclass can override with application-specific code if necessary Future startApplication() async { + currentState = ApplicationState.starting; + if (mounted) setState(() {}); + // try to get the Frame into a known state by making sure there's no main loop running frame!.sendBreakSignal(); await Future.delayed(const Duration(milliseconds: 500)); - // only if there is a frame_app.lua companion app - // TODO could load minified frame_app if one exists? - bool hasFrameApp = (await AssetManifest.loadFromAssetBundle(rootBundle)).listAssets().contains('assets/frame_app.lua'); - if (hasFrameApp) { - // send our frame_app to the Frame - await frame!.uploadScript('frame_app.lua', 'assets/frame_app.lua'); - await Future.delayed(const Duration(milliseconds: 500)); - - // kick off the main application loop - await frame!.sendString('require("frame_app")', awaitResponse: true); - await Future.delayed(const Duration(milliseconds: 500)); + // only if there are lua files to send to Frame (e.g. frame_app.lua companion app, other helper functions, minified versions) + List luaFiles = _filterLuaFiles((await AssetManifest.loadFromAssetBundle(rootBundle)).listAssets()); + + if (luaFiles.isNotEmpty) { + for (var pathFile in luaFiles) { + String fileName = pathFile.split('/').last; + // send the lua script to the Frame + await frame!.uploadScript(fileName, pathFile); + } + + // kick off the main application loop: if there is only one lua file, use it; + // otherwise require a file called "assets/frame_app.min.lua", or "assets/frame_app.lua". + // In that case, the main app file should add require() statements for any dependent modules + if (luaFiles.length == 1) { + String fileName = luaFiles[0].split('/').last; // e.g. "assets/my_file.min.lua" -> "my_file.min.lua" + int lastDotIndex = fileName.lastIndexOf(".lua"); + String bareFileName = fileName.substring(0, lastDotIndex); // e.g. "my_file.min.lua" -> "my_file.min" + + await frame!.sendString('require("$bareFileName")', awaitResponse: true); + } + else if (luaFiles.contains('assets/frame_app.min.lua')) { + await frame!.sendString('require("frame_app.min")', awaitResponse: true); + } + else if (luaFiles.contains('assets/frame_app.lua')) { + await frame!.sendString('require("frame_app")', awaitResponse: true); + } + else { + _log.fine('Multiple Lua files uploaded, but no main file to require()'); + } } currentState = ApplicationState.ready; @@ -350,22 +379,57 @@ mixin SimpleFrameAppState on State { /// the SimpleFrameApp subclass can override with application-specific code if necessary Future stopApplication() async { + currentState = ApplicationState.stopping; + if (mounted) setState(() {}); + // send a break to stop the Lua app loop on Frame await frame!.sendBreakSignal(); await Future.delayed(const Duration(milliseconds: 500)); - // only if there is a frame_app.lua companion app - bool hasFrameApp = (await AssetManifest.loadFromAssetBundle(rootBundle)).listAssets().contains('assets/frame_app.lua'); - if (hasFrameApp) { - // clean up by deregistering any handler and deleting any prior script - await frame!.sendString('frame.bluetooth.receive_callback(nil);frame.file.remove("frame_app.lua");print(0)', awaitResponse: true); - await Future.delayed(const Duration(milliseconds: 500)); + // only if there are lua files uploaded to Frame (e.g. frame_app.lua companion app, other helper functions, minified versions) + List luaFiles = _filterLuaFiles((await AssetManifest.loadFromAssetBundle(rootBundle)).listAssets()); + + if (luaFiles.isNotEmpty) { + // clean up by deregistering any handler + await frame!.sendString('frame.bluetooth.receive_callback(nil);print(0)', awaitResponse: true); + + for (var file in luaFiles) { + // delete any prior scripts + await frame!.sendString('frame.file.remove("${file.split('/').last}");print(0)', awaitResponse: true); + } } currentState = ApplicationState.connected; if (mounted) setState(() {}); } + /// When given the full list of Assets, return only the Lua files (and give .min.lua minified files precedence) + /// Note that file strings will be 'assets/my_file.lua' which we need to find the asset in Flutter, + /// but we need to file.split('/').last if we only want the file name when writing/deleting the file on Frame in the root of its filesystem + List _filterLuaFiles(List files) { + // Create a map to store the base file names without extensions. + Map luaFilesMap = {}; + + for (String file in files) { + // Check if the file ends with .lua or .min.lua + if (file.endsWith('.lua')) { + String baseName; + if (file.endsWith('.min.lua')) { + baseName = file.replaceAll('.min.lua', ''); + } else { + baseName = file.replaceAll('.lua', ''); + } + + // Store the file in the map, giving priority to .min.lua files + if (!luaFilesMap.containsKey(baseName) || file.endsWith('.min.lua')) { + luaFilesMap[baseName] = file; + } + } + } + + // Return the filtered list of Lua files + return luaFilesMap.values.toList(); + } /// the SimpleFrameApp subclass implements application-specific code Future run();