Ares Bridge
Cross-platform USB peer discovery and checksum-verified, bidirectional file transfer for Flutter applications.
Ares Bridge provides one typed Dart API for listener lifecycle, peer readiness, transfer progress, verified completion, received files, and failures. Native transports sit behind the same API, so application code does not need platform branches.
Platform status
| Platform | Role | Current transport |
|---|---|---|
| Android | USB accessory | Android Open Accessory via UsbManager |
| macOS | USB host | AOA for Android; usbmuxd for paired iOS devices |
| iOS | USB accessory | Foreground loopback listener through physical usbmuxd |
| Windows | USB host | API registered; WinUSB/AOA transport not linked yet |
| Linux | USB host | API registered; libusb/AOA transport not linked yet |
| Web | — | Capability reporting; portable USB transfer unsupported |
All six Flutter platforms register the plugin API. Call getCapabilities() at
runtime before displaying transfer controls because transport availability
differs by platform.
Getting started
1. Install
Add the latest published package:
flutter pub add ares_bridge
Or add it manually to pubspec.yaml:
dependencies:
ares_bridge: ^0.0.1
Then import the public API:
import 'package:ares_bridge/ares_bridge.dart';
2. Create the bridge and subscribe to events
Subscribe before starting the listener so the application receives early connection events.
import 'dart:async';
import 'package:ares_bridge/ares_bridge.dart';
import 'package:flutter/services.dart';
final bridge = AresBridge();
final subscriptions = <StreamSubscription<Object?>>[];
Future<void> startBridge() async {
final capabilities = await bridge.getCapabilities();
if (!capabilities.isSupported) {
throw UnsupportedError(
capabilities.reason ??
'Ares Bridge is unavailable on ${capabilities.platform}.',
);
}
subscriptions.add(
bridge.connectionEvents.listen((event) {
print('Connection: ${event.state.name}');
if (event.peerName case final peerName?) {
print('Peer: $peerName');
}
}),
);
subscriptions.add(
bridge.transferProgress.listen((event) {
final percent = (event.fraction * 100).toStringAsFixed(1);
print('${event.fileName}: $percent%');
}),
);
subscriptions.add(
bridge.receivedFiles.listen((event) {
print('Received and verified: ${event.localPath}');
}),
);
subscriptions.add(
bridge.failedTransfers.listen((event) {
print('${event.code}: ${event.message}');
}),
);
try {
await bridge.initialize(
const AresBridgeConfiguration(
role: AresBridgeRole.automatic,
localPeerName: 'Warehouse device',
overwritePolicy: AresOverwritePolicy.rename,
),
);
await bridge.startListening();
} on PlatformException catch (error) {
print('${error.code}: ${error.message}');
rethrow;
}
}
startListening() requests listener startup; it does not wait for a peer.
Enable sending only after a connection event reaches
AresConnectionState.active.
3. Send a file
sourcePath must be an absolute path readable by the native application.
destinationPath, when supplied, is relative to the receiver's configured
incoming directory.
final transferId = await bridge.sendFile(
AresFileTransferRequest(
sourcePath: '/absolute/path/report.pdf',
destinationPath: 'reports/report.pdf',
metadata: const {'origin': 'drag-drop'},
),
);
The returned ID means the native backend accepted the request. It is not a
delivery receipt. Match transferId against completedTransfers or
failedTransfers for the terminal result.
4. Send multiple files
final transferIds = await bridge.sendFiles([
for (final path in droppedPaths)
AresFileTransferRequest(
sourcePath: path,
metadata: const {'origin': 'drag-drop'},
),
]);
Returned IDs correspond to requests by index. Track each transfer separately.
5. Stop and dispose
Cancel stream subscriptions before disposing the bridge:
Future<void> stopBridge() async {
for (final subscription in subscriptions) {
await subscription.cancel();
}
subscriptions.clear();
await bridge.dispose();
}
Use stopListening() instead when you intend to restart the same bridge later.
Connection states
| State | Meaning |
|---|---|
stopped |
Listener and session are closed |
listening |
Local receiver is waiting for a peer |
connecting |
Physical transport or handshake is progressing |
peerReady |
Remote receiver announced readiness |
active |
Handshake complete and heartbeats current |
disconnected |
A known peer is no longer active |
failed |
Connection setup or the active session failed |
A cable attachment alone is not an active connection.
Transfer guarantees
- Incoming destinations are constrained to the configured directory.
- Incoming bytes are written to a hidden partial file.
- Byte count and SHA-256 are verified before finalization.
- Completion is emitted after the destination is flushed and closed.
- Outgoing completion waits for the receiver's verified acknowledgement.
- Event streams are broadcast streams.
Platform setup
Native preparation is required for production integration:
- Android may request USB accessory permission.
- A sandboxed macOS application needs the USB device entitlement.
- iOS transfer requires a trusted physical connection to macOS and the iOS app must remain in the foreground.
- Windows and Linux currently return explicit transport-unavailable errors.
- Web reports unsupported transfer capability rather than failing plugin registration.
Read the complete platform setup guide before shipping.
Documentation
- Complete getting-started guide
- Dart API reference
- Events and errors
- Platform-channel contract
- Protocol and security
- Troubleshooting
Requirements
- Dart
^3.12.2 - Flutter
>=3.3.0 - iOS 13.0 or later
- macOS 10.15.4 or later
License
See LICENSE.
Libraries
- ares_bridge
- Cross-platform USB peer discovery and verified file transfer for Flutter.
- ares_bridge_method_channel
- ares_bridge_platform_interface
- ares_bridge_web