Skip to content

Commit

Permalink
updated simple_frame_app and brilliant_bluetooth to the latest versions
Browse files Browse the repository at this point in the history
  • Loading branch information
CitizenOneX committed Sep 5, 2024
1 parent b1597fa commit 6f2a9b2
Show file tree
Hide file tree
Showing 2 changed files with 96 additions and 23 deletions.
23 changes: 17 additions & 6 deletions lib/brilliant_bluetooth.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> get stringResponse {
// changed to only listen for data coming through the Frame's rx characteristic, not all attached devices as before
return _rxChannel!.onValueReceived
Expand All @@ -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);
});
}
Expand Down Expand Up @@ -162,8 +163,8 @@ class BrilliantDevice {

Future<void> sendData(List<int> 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");
Expand All @@ -185,8 +186,8 @@ class BrilliantDevice {
/// Same as sendData but user includes the 0x01 header byte to avoid extra memory allocation
Future<void> sendDataRaw(List<int> 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");
Expand All @@ -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()));
Expand All @@ -228,13 +229,16 @@ class BrilliantDevice {
// instead point packetToSend to an UnmodifiableListView range within packetBuffer
List<int> packetBuffer = List.filled(maxDataLength! + 1, 0x00);
List<int> 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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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');
}
}

Expand Down
96 changes: 79 additions & 17 deletions lib/simple_frame_app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ enum ApplicationState {
scanning,
connecting,
connected,
starting,
ready,
running,
canceling,
stopping,
disconnecting,
}
Expand Down Expand Up @@ -61,6 +63,8 @@ mixin SimpleFrameAppState<T extends StatefulWidget> on State<T> {
case ApplicationState.connected:
case ApplicationState.ready:
case ApplicationState.running:
case ApplicationState.starting:
case ApplicationState.canceling:
// already connected, nothing to do
break;
default:
Expand Down Expand Up @@ -292,7 +296,9 @@ mixin SimpleFrameAppState<T extends StatefulWidget> on State<T> {
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')));
Expand Down Expand Up @@ -327,21 +333,42 @@ mixin SimpleFrameAppState<T extends StatefulWidget> on State<T> {

/// the SimpleFrameApp subclass can override with application-specific code if necessary
Future<void> 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<String> 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;
Expand All @@ -350,22 +377,57 @@ mixin SimpleFrameAppState<T extends StatefulWidget> on State<T> {

/// the SimpleFrameApp subclass can override with application-specific code if necessary
Future<void> 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<String> 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<String> _filterLuaFiles(List<String> files) {
// Create a map to store the base file names without extensions.
Map<String, String> 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<void> run();
Expand Down

0 comments on commit 6f2a9b2

Please sign in to comment.