Skip to content

Commit 5ed638a

Browse files
committed
feat: added usb transfer functionality(Android only support)
1 parent 9e5b0a3 commit 5ed638a

File tree

14 files changed

+1268
-402
lines changed

14 files changed

+1268
-402
lines changed

android/app/src/main/AndroidManifest.xml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
2+
<uses-feature android:name="android.hardware.usb.host" />
23
<application
34
android:label="Badge Magic"
45
android:name="${applicationName}"
@@ -15,6 +16,14 @@
1516
the Android process has started. This theme is visible to the user
1617
while the Flutter UI initializes. After that, this theme continues
1718
to determine the Window background behind the Flutter UI. -->
19+
<intent-filter>
20+
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
21+
</intent-filter>
22+
23+
<meta-data
24+
android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
25+
android:resource="@xml/device_filter" />
26+
1827
<meta-data
1928
android:name="io.flutter.embedding.android.NormalTheme"
2029
android:resource="@style/NormalTheme"
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<resources>
3+
<!-- FOSSASIA BadgeMagic - badgemagic firmware (transfer) -->
4+
<usb-device vendor-id="4348" product-id="55200" /> <!-- 0x10FC, 0x55E0 -->
5+
6+
<!-- FOSSASIA BadgeMagic - rust badgemagic (bootloader)-->
7+
<usb-device vendor-id="1046" product-id="20512" /> <!-- 0x0416, 0x5020 -->
8+
</resources>
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import 'package:badgemagic/bademagic_module/bluetooth/datagenerator.dart';
2+
3+
/// Builds USB CDC payloads from Badge data.
4+
/// Same format as BLE, but split into 64-byte chunks.
5+
class PayloadBuilder {
6+
final DataTransferManager manager;
7+
8+
PayloadBuilder({required this.manager});
9+
10+
Future<List<List<int>>> buildPayloads() async {
11+
// Generate the raw payload using the existing BLE generator
12+
final rawChunks = await manager.generateDataChunk();
13+
14+
// Flatten because BLE uses 16-byte chunks
15+
final flat = rawChunks.expand((c) => c).toList();
16+
17+
// Split into 64-byte chunks for USB CDC
18+
final usbChunks = <List<int>>[];
19+
for (int i = 0; i < flat.length; i += 64) {
20+
usbChunks.add(
21+
flat.sublist(i, i + 64 > flat.length ? flat.length : i + 64),
22+
);
23+
}
24+
25+
return usbChunks;
26+
}
27+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import 'dart:typed_data';
2+
import 'package:logger/Logger.dart';
3+
import 'package:usb_serial/usb_serial.dart';
4+
5+
/// Wrapper around usb_serial for BadgeMagic devices
6+
class UsbCdc {
7+
final logger = Logger();
8+
UsbPort? _port;
9+
10+
// FOSSASIA BadgeMagic Device IDs - BOTH MODES
11+
static const int normalVendorId = 4348; // 0x10FC - Normal mode
12+
static const int normalProductId = 55200; // 0x55E0 - Normal mode
13+
static const int bootloaderVendorId = 1046; // 0x0416 - Bootloader mode
14+
static const int bootloaderProductId = 20512; // 0x5020 - Bootloader mode
15+
16+
/// Open the first available FOSSASIA USB device
17+
Future<bool> openDevice() async {
18+
final devices = await listDevices();
19+
20+
// Filter for FOSSASIA badges - BOTH MODES
21+
final fossasiaDevices = devices
22+
.where((device) =>
23+
(device.vid == normalVendorId && device.pid == normalProductId) ||
24+
(device.vid == bootloaderVendorId &&
25+
device.pid == bootloaderProductId))
26+
.toList();
27+
28+
if (fossasiaDevices.isEmpty) {
29+
logger.e("No FOSSASIA badge found. Available devices: $devices");
30+
return false;
31+
}
32+
33+
final device = fossasiaDevices.first;
34+
logger.d("Found FOSSASIA device: ${device.vid}:${device.pid}");
35+
36+
// BOOTLOADER DETECTION
37+
if (device.vid == bootloaderVendorId && device.pid == bootloaderProductId) {
38+
logger.e("Device is in bootloader mode - cannot transfer data");
39+
throw Exception(
40+
"Device is in bootloader mode. Please disconnect, then connect without holding any buttons.");
41+
}
42+
43+
_port = await device.create();
44+
if (_port == null) {
45+
logger.e("Failed to create USB port");
46+
return false;
47+
}
48+
49+
final opened = await _port!.open();
50+
if (!opened) {
51+
logger.e("Failed to open USB port");
52+
return false;
53+
}
54+
55+
await _port!.setPortParameters(
56+
115200,
57+
UsbPort.DATABITS_8,
58+
UsbPort.STOPBITS_1,
59+
UsbPort.PARITY_NONE,
60+
);
61+
62+
logger.d("USB device opened successfully");
63+
return true;
64+
}
65+
66+
/// Write data to the USB port
67+
Future<void> write(List<int> data) async {
68+
if (_port == null) throw Exception("USB port not open");
69+
70+
try {
71+
await _port!.write(Uint8List.fromList(data));
72+
logger.d("USB chunk written: ${data.length} bytes");
73+
} catch (e) {
74+
logger.e("Failed to write USB chunk: $e");
75+
rethrow;
76+
}
77+
}
78+
79+
/// Close the USB port
80+
Future<void> close() async {
81+
try {
82+
await _port?.close();
83+
logger.d("USB port closed");
84+
} catch (e) {
85+
logger.e("Error closing USB port: $e");
86+
}
87+
}
88+
89+
/// List connected USB devices with better logging
90+
Future<List<UsbDevice>> listDevices() async {
91+
try {
92+
final devices = await UsbSerial.listDevices();
93+
logger.d(
94+
"USB devices found: ${devices.map((d) => 'VID:${d.vid?.toRadixString(16)} PID:${d.pid?.toRadixString(16)}').toList()}");
95+
return devices;
96+
} catch (e) {
97+
logger.e("Error listing USB devices: $e");
98+
return [];
99+
}
100+
}
101+
102+
/// Helper to check if any FOSSASIA device is connected
103+
Future<bool> isFossasiaDeviceConnected() async {
104+
final devices = await listDevices();
105+
return devices.any((device) =>
106+
(device.vid == normalVendorId && device.pid == normalProductId) ||
107+
(device.vid == bootloaderVendorId &&
108+
device.pid == bootloaderProductId));
109+
}
110+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// usb_scan_state.dart
2+
import 'dart:async';
3+
import 'package:badgemagic/bademagic_module/bluetooth/base_ble_state.dart';
4+
import 'package:badgemagic/bademagic_module/usb/payload_builder.dart';
5+
import 'package:badgemagic/bademagic_module/usb/usb_cdc.dart';
6+
import 'package:badgemagic/bademagic_module/bluetooth/completed_state.dart';
7+
import 'package:badgemagic/bademagic_module/usb/usb_write_state.dart';
8+
9+
/// USB scan state (mirrors ScanState for BLE)
10+
class UsbScanState extends NormalBleState {
11+
final PayloadBuilder builder;
12+
13+
UsbScanState({required this.builder});
14+
15+
@override
16+
Future<BleState?> processState() async {
17+
final usb = UsbCdc();
18+
toast.showToast("Searching for USB device...");
19+
20+
try {
21+
final devices =
22+
await usb.listDevices(); // wrapper for UsbSerial.listDevices()
23+
24+
final fossasiaDevices = devices
25+
.where((device) => device.vid == 0x0416 && device.pid == 0x5020)
26+
.toList();
27+
28+
if (fossasiaDevices.isEmpty) {
29+
toast.showErrorToast("No FOSSASIA badge found");
30+
return CompletedState(
31+
isSuccess: false, message: "No FOSSASIA USB device found");
32+
}
33+
34+
toast.showToast("USB device found. Preparing transfer...");
35+
36+
// Directly pass to UsbWriteState
37+
final writeState = UsbWriteState(builder: builder);
38+
return await writeState.process();
39+
} catch (e) {
40+
logger.e("USB scan error: $e");
41+
toast.showErrorToast("USB scan failed: $e");
42+
return CompletedState(isSuccess: false, message: "USB scan failed: $e");
43+
}
44+
}
45+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import 'package:badgemagic/bademagic_module/bluetooth/base_ble_state.dart';
2+
import 'package:badgemagic/bademagic_module/bluetooth/completed_state.dart';
3+
import 'package:badgemagic/bademagic_module/usb/payload_builder.dart';
4+
import 'package:badgemagic/bademagic_module/usb/usb_cdc.dart';
5+
import 'package:badgemagic/bademagic_module/utils/toast_utils.dart';
6+
import 'package:flutter/foundation.dart'; // for kIsWeb, Platform checks
7+
import 'dart:io' show Platform;
8+
import 'package:flutter/services.dart'; // for MissingPluginException
9+
10+
class UsbWriteState extends NormalBleState {
11+
final PayloadBuilder builder;
12+
13+
UsbWriteState({required this.builder});
14+
15+
@override
16+
Future<BleState?> processState() async {
17+
// Unsupported platforms (macOS, Web, etc.)
18+
if (!Platform.isAndroid) {
19+
toast.showErrorToast("USB transfer not supported on this platform");
20+
return CompletedState(isSuccess: false, message: "Unsupported platform");
21+
}
22+
23+
final usb = UsbCdc();
24+
try {
25+
bool opened;
26+
try {
27+
opened = await usb.openDevice();
28+
} on MissingPluginException catch (_) {
29+
toast.showErrorToast(
30+
"USB plugin not available. Please ensure the plugin is installed and platform supports USB.",
31+
);
32+
throw Exception("USB plugin missing or not registered");
33+
}
34+
35+
if (!opened) {
36+
toast.showErrorToast("No BadgeMagic USB device found");
37+
throw Exception("No USB device connected");
38+
}
39+
40+
final dataChunks = await builder.buildPayloads();
41+
logger.d("USB payload chunks: ${dataChunks.length}");
42+
43+
for (final chunk in dataChunks) {
44+
bool success = false;
45+
for (int attempt = 1; attempt <= 3; attempt++) {
46+
try {
47+
await usb.write(chunk);
48+
logger.d("USB chunk written: $chunk");
49+
success = true;
50+
break;
51+
} catch (e) {
52+
logger.e("USB write failed (attempt $attempt/3): $e");
53+
}
54+
}
55+
if (!success) {
56+
toast.showErrorToast("Failed to transfer data over USB");
57+
throw Exception("USB transfer failed");
58+
}
59+
await Future.delayed(const Duration(milliseconds: 20));
60+
}
61+
62+
toast.showToast("USB transfer completed successfully");
63+
return CompletedState(isSuccess: true, message: "USB transfer complete");
64+
} catch (e) {
65+
logger.e("USB transfer error: $e");
66+
if (e.toString().contains("MissingPluginException")) {
67+
// Already handled above; no need to show extra toast
68+
} else if (e.toString().contains("No USB device connected")) {
69+
// Already shown toast above
70+
} else {
71+
toast.showErrorToast("USB transfer failed: ${e.toString()}");
72+
}
73+
throw e;
74+
} finally {
75+
await usb.close();
76+
}
77+
}
78+
}

lib/constants.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ const drawBadgeScreen = "bm_db_screen";
88
const savedClipartScreen = "bm_sc_screen";
99
const savedBadgeScreen = "bm_sb_screen";
1010

11+
enum ConnectionType {
12+
bluetooth,
13+
usb,
14+
}
15+
1116
//Colors used in the app
1217
// Primary Colors
1318
const Color colorPrimary = Color(0xFFD32F2F);

lib/main.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
1616
import 'package:provider/provider.dart';
1717
import 'globals/globals.dart' as globals;
1818
import 'services/localization_service.dart';
19+
import 'package:badgemagic/providers/transfer_provider.dart';
1920

2021
Future<void> main() async {
2122
setupLocator();
@@ -47,7 +48,9 @@ Future<void> main() async {
4748
ChangeNotifierProvider<FontProvider>(
4849
create: (context) => getIt<FontProvider>()),
4950
ChangeNotifierProvider<BadgeScanProvider>(
50-
create: (_) => getIt<BadgeScanProvider>(),
51+
create: (_) => getIt<BadgeScanProvider>()),
52+
ChangeNotifierProvider<TransferProvider>(
53+
create: (context) => TransferProvider(), // Create a new instance
5154
),
5255
],
5356
child: const MyApp(),

0 commit comments

Comments
 (0)