Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-feature android:name="android.hardware.usb.host" />
<application
android:label="Badge Magic"
android:name="${applicationName}"
Expand All @@ -15,6 +16,14 @@
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<intent-filter>
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
</intent-filter>

<meta-data
android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
android:resource="@xml/device_filter" />

<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
Expand Down
8 changes: 8 additions & 0 deletions android/app/src/main/res/xml/device_filter.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- FOSSASIA BadgeMagic - badgemagic firmware (transfer) -->
<usb-device vendor-id="4348" product-id="55200" /> <!-- 0x10FC, 0x55E0 -->

<!-- FOSSASIA BadgeMagic - rust badgemagic (bootloader)-->
<usb-device vendor-id="1046" product-id="20512" /> <!-- 0x0416, 0x5020 -->
</resources>
27 changes: 27 additions & 0 deletions lib/bademagic_module/usb/payload_builder.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import 'package:badgemagic/bademagic_module/bluetooth/datagenerator.dart';

/// Builds USB CDC payloads from Badge data.
/// Same format as BLE, but split into 64-byte chunks.
class PayloadBuilder {
final DataTransferManager manager;

PayloadBuilder({required this.manager});

Future<List<List<int>>> buildPayloads() async {
// Generate the raw payload using the existing BLE generator
final rawChunks = await manager.generateDataChunk();

// Flatten because BLE uses 16-byte chunks
final flat = rawChunks.expand((c) => c).toList();

// Split into 64-byte chunks for USB CDC
final usbChunks = <List<int>>[];
for (int i = 0; i < flat.length; i += 64) {
usbChunks.add(
flat.sublist(i, i + 64 > flat.length ? flat.length : i + 64),
);
}

return usbChunks;
}
}
110 changes: 110 additions & 0 deletions lib/bademagic_module/usb/usb_cdc.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import 'dart:typed_data';
import 'package:logger/logger.dart';
import 'package:usb_serial/usb_serial.dart';

/// Wrapper around usb_serial for BadgeMagic devices
class UsbCdc {
final logger = Logger();
UsbPort? _port;

// FOSSASIA BadgeMagic Device IDs - BOTH MODES
static const int normalVendorId = 4348; // 0x10FC - Normal mode
static const int normalProductId = 55200; // 0x55E0 - Normal mode
static const int bootloaderVendorId = 1046; // 0x0416 - Bootloader mode
static const int bootloaderProductId = 20512; // 0x5020 - Bootloader mode

/// Open the first available FOSSASIA USB device
Future<bool> openDevice() async {
final devices = await listDevices();

// Filter for FOSSASIA badges - BOTH MODES
final fossasiaDevices = devices
.where((device) =>
(device.vid == normalVendorId && device.pid == normalProductId) ||
(device.vid == bootloaderVendorId &&
device.pid == bootloaderProductId))
.toList();

if (fossasiaDevices.isEmpty) {
logger.e("No FOSSASIA badge found. Available devices: $devices");
return false;
}

final device = fossasiaDevices.first;
logger.d("Found FOSSASIA device: ${device.vid}:${device.pid}");

// BOOTLOADER DETECTION
if (device.vid == bootloaderVendorId && device.pid == bootloaderProductId) {
logger.e("Device is in bootloader mode - cannot transfer data");
throw Exception(
"Device is in bootloader mode. Please disconnect, then connect without holding any buttons.");
}

_port = await device.create();
if (_port == null) {
logger.e("Failed to create USB port");
return false;
}

final opened = await _port!.open();
if (!opened) {
logger.e("Failed to open USB port");
return false;
}

await _port!.setPortParameters(
115200,
UsbPort.DATABITS_8,
UsbPort.STOPBITS_1,
UsbPort.PARITY_NONE,
);

logger.d("USB device opened successfully");
return true;
}

/// Write data to the USB port
Future<void> write(List<int> data) async {
if (_port == null) throw Exception("USB port not open");

try {
await _port!.write(Uint8List.fromList(data));
logger.d("USB chunk written: ${data.length} bytes");
} catch (e) {
logger.e("Failed to write USB chunk: $e");
rethrow;
}
}

/// Close the USB port
Future<void> close() async {
try {
await _port?.close();
logger.d("USB port closed");
} catch (e) {
logger.e("Error closing USB port: $e");
}
}

/// List connected USB devices with better logging
Future<List<UsbDevice>> listDevices() async {
try {
final devices = await UsbSerial.listDevices();
logger.d(
"USB devices found: ${devices.map((d) => 'VID:${d.vid?.toRadixString(16)} PID:${d.pid?.toRadixString(16)}').toList()}");
return devices;
} catch (e) {
logger.e("Error listing USB devices: $e");
return [];
}
}

/// Helper to check if any FOSSASIA device is connected
Future<bool> isFossasiaDeviceConnected() async {
final devices = await listDevices();
return devices.any((device) =>
(device.vid == normalVendorId && device.pid == normalProductId) ||
(device.vid == bootloaderVendorId &&
device.pid == bootloaderProductId));
}
}
45 changes: 45 additions & 0 deletions lib/bademagic_module/usb/usb_scan_state.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// usb_scan_state.dart
import 'dart:async';
import 'package:badgemagic/bademagic_module/bluetooth/base_ble_state.dart';
import 'package:badgemagic/bademagic_module/usb/payload_builder.dart';
import 'package:badgemagic/bademagic_module/usb/usb_cdc.dart';
import 'package:badgemagic/bademagic_module/bluetooth/completed_state.dart';
import 'package:badgemagic/bademagic_module/usb/usb_write_state.dart';

/// USB scan state (mirrors ScanState for BLE)
class UsbScanState extends NormalBleState {
final PayloadBuilder builder;

UsbScanState({required this.builder});

@override
Future<BleState?> processState() async {
final usb = UsbCdc();
toast.showToast("Searching for USB device...");

try {
final devices =
await usb.listDevices(); // wrapper for UsbSerial.listDevices()

final fossasiaDevices = devices
.where((device) => device.vid == 0x0416 && device.pid == 0x5020)
.toList();

if (fossasiaDevices.isEmpty) {
toast.showErrorToast("No FOSSASIA badge found");
return CompletedState(
isSuccess: false, message: "No FOSSASIA USB device found");
}

toast.showToast("USB device found. Preparing transfer...");

// Directly pass to UsbWriteState
final writeState = UsbWriteState(builder: builder);
return await writeState.process();
} catch (e) {
logger.e("USB scan error: $e");
toast.showErrorToast("USB scan failed: $e");
return CompletedState(isSuccess: false, message: "USB scan failed: $e");
}
}
}
76 changes: 76 additions & 0 deletions lib/bademagic_module/usb/usb_write_state.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import 'package:badgemagic/bademagic_module/bluetooth/base_ble_state.dart';
import 'package:badgemagic/bademagic_module/bluetooth/completed_state.dart';
import 'package:badgemagic/bademagic_module/usb/payload_builder.dart';
import 'package:badgemagic/bademagic_module/usb/usb_cdc.dart';
import 'dart:io' show Platform;
import 'package:flutter/services.dart'; // for MissingPluginException

class UsbWriteState extends NormalBleState {
final PayloadBuilder builder;

UsbWriteState({required this.builder});

@override
Future<BleState?> processState() async {
// Unsupported platforms (macOS, Web, etc.)
if (!Platform.isAndroid) {
toast.showErrorToast("USB transfer not supported on this platform");
return CompletedState(isSuccess: false, message: "Unsupported platform");
}

final usb = UsbCdc();
try {
bool opened;
try {
opened = await usb.openDevice();
} on MissingPluginException catch (_) {
toast.showErrorToast(
"USB plugin not available. Please ensure the plugin is installed and platform supports USB.",
);
throw Exception("USB plugin missing or not registered");
}

if (!opened) {
toast.showErrorToast("No BadgeMagic USB device found");
throw Exception("No USB device connected");
}

final dataChunks = await builder.buildPayloads();
logger.d("USB payload chunks: ${dataChunks.length}");

for (final chunk in dataChunks) {
bool success = false;
for (int attempt = 1; attempt <= 3; attempt++) {
try {
await usb.write(chunk);
logger.d("USB chunk written: $chunk");
success = true;
break;
} catch (e) {
logger.e("USB write failed (attempt $attempt/3): $e");
}
}
if (!success) {
toast.showErrorToast("Failed to transfer data over USB");
throw Exception("USB transfer failed");
}
await Future.delayed(const Duration(milliseconds: 20));
}

toast.showToast("USB transfer completed successfully");
return CompletedState(isSuccess: true, message: "USB transfer complete");
} catch (e) {
logger.e("USB transfer error: $e");
if (e.toString().contains("MissingPluginException")) {
// Already handled above; no need to show extra toast
} else if (e.toString().contains("No USB device connected")) {
// Already shown toast above
} else {
toast.showErrorToast("USB transfer failed: ${e.toString()}");
}
throw e;
} finally {
await usb.close();
}
}
}
5 changes: 5 additions & 0 deletions lib/constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ const drawBadgeScreen = "bm_db_screen";
const savedClipartScreen = "bm_sc_screen";
const savedBadgeScreen = "bm_sb_screen";

enum ConnectionType {
bluetooth,
usb,
}

//Colors used in the app
// Primary Colors
const Color colorPrimary = Color(0xFFD32F2F);
Expand Down
5 changes: 4 additions & 1 deletion lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:provider/provider.dart';
import 'globals/globals.dart' as globals;
import 'services/localization_service.dart';
import 'package:badgemagic/providers/transfer_provider.dart';

Future<void> main() async {
setupLocator();
Expand Down Expand Up @@ -47,7 +48,9 @@ Future<void> main() async {
ChangeNotifierProvider<FontProvider>(
create: (context) => getIt<FontProvider>()),
ChangeNotifierProvider<BadgeScanProvider>(
create: (_) => getIt<BadgeScanProvider>(),
create: (_) => getIt<BadgeScanProvider>()),
ChangeNotifierProvider<TransferProvider>(
create: (context) => TransferProvider(), // Create a new instance
),
],
child: const MyApp(),
Expand Down
Loading
Loading