Compare commits

..

No commits in common. "dc500c143240492a363328748a707bedcfcf171f" and "7f1abe1261c282863beaeb8652bddbf46698e81c" have entirely different histories.

26 changed files with 267 additions and 808 deletions

29
Cargo.lock generated
View file

@ -571,18 +571,7 @@ version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b6372023ac861f6e6dc89c8344a8f398fb42aaba2b5dbc649ca0c0e9dbcb627"
dependencies = [
"bytecheck_derive 0.6.11",
"ptr_meta",
"simdutf8",
]
[[package]]
name = "bytecheck"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41502630fe304ce54cbb2f8389e017784dee2b0328147779fcbe43b9db06d35d"
dependencies = [
"bytecheck_derive 0.7.0",
"bytecheck_derive",
"ptr_meta",
"simdutf8",
]
@ -598,17 +587,6 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "bytecheck_derive"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eda88c587085bc07dc201ab9df871bd9baa5e07f7754b745e4d7194b43ac1eda"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "bytemuck"
version = "1.13.1"
@ -927,7 +905,6 @@ dependencies = [
name = "cuttle"
version = "0.1.0"
dependencies = [
"bytecheck 0.7.0",
"clap",
"eframe",
"egui_extras",
@ -2536,7 +2513,7 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "581008d2099240d37fb08d77ad713bcaec2c4d89d50b5b21a8bb1996bbab68ab"
dependencies = [
"bytecheck 0.6.11",
"bytecheck",
]
[[package]]
@ -2580,7 +2557,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0200c8230b013893c0b2d6213d6ec64ed2b9be2e0e016682b7224ff82cff5c58"
dependencies = [
"bitvec",
"bytecheck 0.6.11",
"bytecheck",
"hashbrown 0.12.3",
"ptr_meta",
"rend",

View file

@ -8,7 +8,6 @@ default = ["desktop"]
desktop = ["dep:clap", "dep:eframe", "dep:egui_extras", "dep:fast_qr", "dep:png"]
[dependencies]
bytecheck = "0.7"
clap = { version = "4.3", optional = true, features = ["derive", "env"] }
eframe = { version = "0.22", default-features = false, optional = true, features = ["default_fonts", "wgpu", "tts", "accesskit"] }
egui_extras = { version = "0.22", default-features = false, optional = true, features = ["chrono", "image"] }

View file

@ -22,36 +22,15 @@ Raptor codes for that file. Each piece of Raptor code then gets encoded as a QR
displayed for a period of time on the screen. The user then holds up the receiving computer's camera
in order to receive the QR encoded raptor codes, and, *voila*, the file has been transferred!
# Current status (2023-08-20)
# Current status
Currently, the following is done in terms of functionality:
**Desktop**
- [x] transmit short text given on the commandline with a single qr code
- [x] transmit large text given on the commandline with a stream of qr codes
- [x] transmit small files given on the commandline with a single qr code
- [x] transmit large files given on the commandline with a stream of qr codes
- [ ] receive text or files with a single qr code
- [ ] receive text or files with a stream of qr codes
**Android app**
- [ ] transmit short given text with a single qr code
- [ ] transmit large given text with a stream of qr codes
- [ ] transmit small files with a single qr code
- [ ] transmit large files with a stream of qr codes
- [x] receive text or files with a single qr code
- [x] receive text or files with a stream of qr codes
The desktop app (unix-ish only; developed on linux, untested on anything else, but should run on macos)
is closer to its final form in terms of UI/UX, though it currently can only transmit. The Android
app is about halfway done in terms of functionality, but the UI is, shall we say, "bad":
![cuttle mobile app receiving data](./mobile_receiving.png)
I might take a crack at making it less hideous before I start on the mobile transmitting/desktop
receiving side of the functionality, but maybe not! I often want to transfer a file or text from my
phone to my desktop, and I'm willing to live with an ugly UI for a little bit.
The desktop transmitting application is nearly done; already it can convert text input into QR codes
(either a single static one if the data is small enough, otherwise it will stream a never ending
loop of them), and the mobile app has been started. Even without the mobile app, it's still
already useful for transmitting short text strings to the phone, since the Android camera app will
decode any detected QR codes.
The mobile app has been started, and I hope to have it decoding QR streams soon!
# about the name

View file

@ -1,38 +1,3 @@
use rand::{seq::SliceRandom, Rng};
use raptorq::{Decoder, Encoder, EncodingPacket, ObjectTransmissionInformation};
fn main() {
let rng = &mut rand::thread_rng();
let len = 20_000;
let mut v = Vec::with_capacity(len);
for _ in 0..len {
v.push(rng.gen::<u8>());
}
let config = ObjectTransmissionInformation::with_defaults(len as u64, 1200);
let encoder = Encoder::new(&v, config);
let mut packets = encoder
.get_encoded_packets(10)
.iter()
.map(|p| p.serialize())
.collect::<Vec<_>>();
packets.shuffle(rng);
let mut decoder = Decoder::new(config);
let mut v2 = None;
for (i, p) in packets.iter().enumerate() {
v2 = decoder.decode(EncodingPacket::deserialize(p));
if v2.is_some() {
println!(
"recovered after {i} packets received, out of {} total",
packets.len()
);
break;
}
}
assert!(v2.is_some());
assert_eq!(v2.unwrap(), v);
//
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -1,4 +1,3 @@
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
android.enableJetifier=true
org.gradle.daemon=false

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

View file

@ -5,16 +5,9 @@ gen:
flutter_rust_bridge_codegen
lint:
cd native && cargo fmt
dart format .
native: native-debug native-release
native-release:
cd android && ./gradlew :app:cargoBuildRelease
native-debug:
cd android && ./gradlew :app:cargoBuildDebug
clean:
flutter clean
cd native && cargo clean

View file

@ -9,37 +9,22 @@ import 'package:flutter_rust_bridge/flutter_rust_bridge.dart';
import 'package:uuid/uuid.dart';
abstract class Native {
Future<TxConfig?> getTxConfig({required Uint8List bytes, dynamic hint});
Future<Platform> platform({dynamic hint});
FlutterRustBridgeTaskConstMeta get kGetTxConfigConstMeta;
FlutterRustBridgeTaskConstMeta get kPlatformConstMeta;
Future<void> dropTxConfig({required TxConfig txc, dynamic hint});
Future<bool> rustReleaseMode({dynamic hint});
FlutterRustBridgeTaskConstMeta get kDropTxConfigConstMeta;
Future<Uint8List?> decodePackets({required List<RaptorPacket> packets, required TxConfig txconf, dynamic hint});
FlutterRustBridgeTaskConstMeta get kDecodePacketsConstMeta;
FlutterRustBridgeTaskConstMeta get kRustReleaseModeConstMeta;
}
class RaptorPacket {
final Uint8List field0;
const RaptorPacket({
required this.field0,
});
}
class TxConfig {
final int len;
final int mtu;
final String description;
final String? filename;
const TxConfig({
required this.len,
required this.mtu,
required this.description,
this.filename,
});
enum Platform {
Unknown,
AndroidBish,
Ios,
Windows,
Unix,
MacIntel,
MacApple,
Wasm,
}

View file

@ -24,53 +24,34 @@ class NativeImpl implements Native {
/// Only valid on web/WASM platforms.
factory NativeImpl.wasm(FutureOr<WasmModule> module) => NativeImpl(module as ExternalLibrary);
NativeImpl.raw(this._platform);
Future<TxConfig?> getTxConfig({required Uint8List bytes, dynamic hint}) {
var arg0 = _platform.api2wire_uint_8_list(bytes);
Future<Platform> platform({dynamic hint}) {
return _platform.executeNormal(FlutterRustBridgeTask(
callFfi: (port_) => _platform.inner.wire_get_tx_config(port_, arg0),
parseSuccessData: _wire2api_opt_box_autoadd_tx_config,
constMeta: kGetTxConfigConstMeta,
argValues: [bytes],
callFfi: (port_) => _platform.inner.wire_platform(port_),
parseSuccessData: _wire2api_platform,
constMeta: kPlatformConstMeta,
argValues: [],
hint: hint,
));
}
FlutterRustBridgeTaskConstMeta get kGetTxConfigConstMeta => const FlutterRustBridgeTaskConstMeta(
debugName: "get_tx_config",
argNames: ["bytes"],
FlutterRustBridgeTaskConstMeta get kPlatformConstMeta => const FlutterRustBridgeTaskConstMeta(
debugName: "platform",
argNames: [],
);
Future<void> dropTxConfig({required TxConfig txc, dynamic hint}) {
var arg0 = _platform.api2wire_box_autoadd_tx_config(txc);
Future<bool> rustReleaseMode({dynamic hint}) {
return _platform.executeNormal(FlutterRustBridgeTask(
callFfi: (port_) => _platform.inner.wire_drop_tx_config(port_, arg0),
parseSuccessData: _wire2api_unit,
constMeta: kDropTxConfigConstMeta,
argValues: [txc],
callFfi: (port_) => _platform.inner.wire_rust_release_mode(port_),
parseSuccessData: _wire2api_bool,
constMeta: kRustReleaseModeConstMeta,
argValues: [],
hint: hint,
));
}
FlutterRustBridgeTaskConstMeta get kDropTxConfigConstMeta => const FlutterRustBridgeTaskConstMeta(
debugName: "drop_tx_config",
argNames: ["txc"],
);
Future<Uint8List?> decodePackets({required List<RaptorPacket> packets, required TxConfig txconf, dynamic hint}) {
var arg0 = _platform.api2wire_list_raptor_packet(packets);
var arg1 = _platform.api2wire_box_autoadd_tx_config(txconf);
return _platform.executeNormal(FlutterRustBridgeTask(
callFfi: (port_) => _platform.inner.wire_decode_packets(port_, arg0, arg1),
parseSuccessData: _wire2api_opt_uint_8_list,
constMeta: kDecodePacketsConstMeta,
argValues: [packets, txconf],
hint: hint,
));
}
FlutterRustBridgeTaskConstMeta get kDecodePacketsConstMeta => const FlutterRustBridgeTaskConstMeta(
debugName: "decode_packets",
argNames: ["packets", "txconf"],
FlutterRustBridgeTaskConstMeta get kRustReleaseModeConstMeta => const FlutterRustBridgeTaskConstMeta(
debugName: "rust_release_mode",
argNames: [],
);
void dispose() {
@ -78,70 +59,21 @@ class NativeImpl implements Native {
}
// Section: wire2api
String _wire2api_String(dynamic raw) {
return raw as String;
bool _wire2api_bool(dynamic raw) {
return raw as bool;
}
TxConfig _wire2api_box_autoadd_tx_config(dynamic raw) {
return _wire2api_tx_config(raw);
}
String? _wire2api_opt_String(dynamic raw) {
return raw == null ? null : _wire2api_String(raw);
}
TxConfig? _wire2api_opt_box_autoadd_tx_config(dynamic raw) {
return raw == null ? null : _wire2api_box_autoadd_tx_config(raw);
}
Uint8List? _wire2api_opt_uint_8_list(dynamic raw) {
return raw == null ? null : _wire2api_uint_8_list(raw);
}
TxConfig _wire2api_tx_config(dynamic raw) {
final arr = raw as List<dynamic>;
if (arr.length != 4) throw Exception('unexpected arr length: expect 4 but see ${arr.length}');
return TxConfig(
len: _wire2api_u64(arr[0]),
mtu: _wire2api_u16(arr[1]),
description: _wire2api_String(arr[2]),
filename: _wire2api_opt_String(arr[3]),
);
}
int _wire2api_u16(dynamic raw) {
int _wire2api_i32(dynamic raw) {
return raw as int;
}
int _wire2api_u64(dynamic raw) {
return castInt(raw);
}
int _wire2api_u8(dynamic raw) {
return raw as int;
}
Uint8List _wire2api_uint_8_list(dynamic raw) {
return raw as Uint8List;
}
void _wire2api_unit(dynamic raw) {
return;
Platform _wire2api_platform(dynamic raw) {
return Platform.values[raw as int];
}
}
// Section: api2wire
@protected
int api2wire_u16(int raw) {
return raw;
}
@protected
int api2wire_u8(int raw) {
return raw;
}
// Section: finalizer
class NativePlatform extends FlutterRustBridgeBase<NativeWire> {
@ -149,61 +81,9 @@ class NativePlatform extends FlutterRustBridgeBase<NativeWire> {
// Section: api2wire
@protected
ffi.Pointer<wire_uint_8_list> api2wire_String(String raw) {
return api2wire_uint_8_list(utf8.encoder.convert(raw));
}
@protected
ffi.Pointer<wire_TxConfig> api2wire_box_autoadd_tx_config(TxConfig raw) {
final ptr = inner.new_box_autoadd_tx_config_0();
_api_fill_to_wire_tx_config(raw, ptr.ref);
return ptr;
}
@protected
ffi.Pointer<wire_list_raptor_packet> api2wire_list_raptor_packet(List<RaptorPacket> raw) {
final ans = inner.new_list_raptor_packet_0(raw.length);
for (var i = 0; i < raw.length; ++i) {
_api_fill_to_wire_raptor_packet(raw[i], ans.ref.ptr[i]);
}
return ans;
}
@protected
ffi.Pointer<wire_uint_8_list> api2wire_opt_String(String? raw) {
return raw == null ? ffi.nullptr : api2wire_String(raw);
}
@protected
int api2wire_u64(int raw) {
return raw;
}
@protected
ffi.Pointer<wire_uint_8_list> api2wire_uint_8_list(Uint8List raw) {
final ans = inner.new_uint_8_list_0(raw.length);
ans.ref.ptr.asTypedList(raw.length).setAll(0, raw);
return ans;
}
// Section: finalizer
// Section: api_fill_to_wire
void _api_fill_to_wire_box_autoadd_tx_config(TxConfig apiObj, ffi.Pointer<wire_TxConfig> wireObj) {
_api_fill_to_wire_tx_config(apiObj, wireObj.ref);
}
void _api_fill_to_wire_raptor_packet(RaptorPacket apiObj, wire_RaptorPacket wireObj) {
wireObj.field0 = api2wire_uint_8_list(apiObj.field0);
}
void _api_fill_to_wire_tx_config(TxConfig apiObj, wire_TxConfig wireObj) {
wireObj.len = api2wire_u64(apiObj.len);
wireObj.mtu = api2wire_u16(apiObj.mtu);
wireObj.description = api2wire_String(apiObj.description);
wireObj.filename = api2wire_opt_String(apiObj.filename);
}
}
// ignore_for_file: camel_case_types, non_constant_identifier_names, avoid_positional_boolean_parameters, annotate_overrides, constant_identifier_names
@ -284,88 +164,28 @@ class NativeWire implements FlutterRustBridgeWireBase {
_lookup<ffi.NativeFunction<ffi.IntPtr Function(ffi.Pointer<ffi.Void>)>>('init_frb_dart_api_dl');
late final _init_frb_dart_api_dl = _init_frb_dart_api_dlPtr.asFunction<int Function(ffi.Pointer<ffi.Void>)>();
void wire_get_tx_config(
void wire_platform(
int port_,
ffi.Pointer<wire_uint_8_list> bytes,
) {
return _wire_get_tx_config(
return _wire_platform(
port_,
bytes,
);
}
late final _wire_get_tx_configPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Int64, ffi.Pointer<wire_uint_8_list>)>>('wire_get_tx_config');
late final _wire_get_tx_config =
_wire_get_tx_configPtr.asFunction<void Function(int, ffi.Pointer<wire_uint_8_list>)>();
late final _wire_platformPtr = _lookup<ffi.NativeFunction<ffi.Void Function(ffi.Int64)>>('wire_platform');
late final _wire_platform = _wire_platformPtr.asFunction<void Function(int)>();
void wire_drop_tx_config(
void wire_rust_release_mode(
int port_,
ffi.Pointer<wire_TxConfig> _txc,
) {
return _wire_drop_tx_config(
return _wire_rust_release_mode(
port_,
_txc,
);
}
late final _wire_drop_tx_configPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Int64, ffi.Pointer<wire_TxConfig>)>>('wire_drop_tx_config');
late final _wire_drop_tx_config =
_wire_drop_tx_configPtr.asFunction<void Function(int, ffi.Pointer<wire_TxConfig>)>();
void wire_decode_packets(
int port_,
ffi.Pointer<wire_list_raptor_packet> packets,
ffi.Pointer<wire_TxConfig> txconf,
) {
return _wire_decode_packets(
port_,
packets,
txconf,
);
}
late final _wire_decode_packetsPtr = _lookup<
ffi.NativeFunction<
ffi.Void Function(
ffi.Int64, ffi.Pointer<wire_list_raptor_packet>, ffi.Pointer<wire_TxConfig>)>>('wire_decode_packets');
late final _wire_decode_packets = _wire_decode_packetsPtr
.asFunction<void Function(int, ffi.Pointer<wire_list_raptor_packet>, ffi.Pointer<wire_TxConfig>)>();
ffi.Pointer<wire_TxConfig> new_box_autoadd_tx_config_0() {
return _new_box_autoadd_tx_config_0();
}
late final _new_box_autoadd_tx_config_0Ptr =
_lookup<ffi.NativeFunction<ffi.Pointer<wire_TxConfig> Function()>>('new_box_autoadd_tx_config_0');
late final _new_box_autoadd_tx_config_0 =
_new_box_autoadd_tx_config_0Ptr.asFunction<ffi.Pointer<wire_TxConfig> Function()>();
ffi.Pointer<wire_list_raptor_packet> new_list_raptor_packet_0(
int len,
) {
return _new_list_raptor_packet_0(
len,
);
}
late final _new_list_raptor_packet_0Ptr =
_lookup<ffi.NativeFunction<ffi.Pointer<wire_list_raptor_packet> Function(ffi.Int32)>>('new_list_raptor_packet_0');
late final _new_list_raptor_packet_0 =
_new_list_raptor_packet_0Ptr.asFunction<ffi.Pointer<wire_list_raptor_packet> Function(int)>();
ffi.Pointer<wire_uint_8_list> new_uint_8_list_0(
int len,
) {
return _new_uint_8_list_0(
len,
);
}
late final _new_uint_8_list_0Ptr =
_lookup<ffi.NativeFunction<ffi.Pointer<wire_uint_8_list> Function(ffi.Int32)>>('new_uint_8_list_0');
late final _new_uint_8_list_0 = _new_uint_8_list_0Ptr.asFunction<ffi.Pointer<wire_uint_8_list> Function(int)>();
late final _wire_rust_release_modePtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Int64)>>('wire_rust_release_mode');
late final _wire_rust_release_mode = _wire_rust_release_modePtr.asFunction<void Function(int)>();
void free_WireSyncReturn(
WireSyncReturn ptr,
@ -382,36 +202,6 @@ class NativeWire implements FlutterRustBridgeWireBase {
final class _Dart_Handle extends ffi.Opaque {}
final class wire_uint_8_list extends ffi.Struct {
external ffi.Pointer<ffi.Uint8> ptr;
@ffi.Int32()
external int len;
}
final class wire_TxConfig extends ffi.Struct {
@ffi.Uint64()
external int len;
@ffi.Uint16()
external int mtu;
external ffi.Pointer<wire_uint_8_list> description;
external ffi.Pointer<wire_uint_8_list> filename;
}
final class wire_RaptorPacket extends ffi.Struct {
external ffi.Pointer<wire_uint_8_list> field0;
}
final class wire_list_raptor_packet extends ffi.Struct {
external ffi.Pointer<wire_RaptorPacket> ptr;
@ffi.Int32()
external int len;
}
typedef DartPostCObjectFnType
= ffi.Pointer<ffi.NativeFunction<ffi.Bool Function(DartPort port_id, ffi.Pointer<ffi.Void> message)>>;
typedef DartPort = ffi.Int64;

View file

@ -9,6 +9,7 @@ export 'bridge_definitions.dart';
// Re-export the bridge so it is only necessary to import this file.
export 'bridge_generated.dart';
import 'dart:io' as io;
const _base = 'native';

View file

@ -1,33 +1,23 @@
import 'dart:async';
import 'dart:collection';
import 'dart:io';
import 'dart:typed_data';
import 'package:cuttle/utils/mlkit_utils.dart';
import 'package:camerawesome/camerawesome_plugin.dart';
import 'package:flutter/material.dart';
import 'ffi.dart' if (dart.library.html) 'ffi_web.dart';
import 'package:google_mlkit_barcode_scanning/google_mlkit_barcode_scanning.dart';
import 'package:intl/intl.dart';
import 'package:rxdart/rxdart.dart';
import 'ffi.dart';
import 'utils/mlkit_utils.dart';
void main() {
runApp(const MyApp());
}
enum CuttleState {
unitialized,
receiving,
received,
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Cuttle',
title: 'camerAwesome App',
theme: ThemeData(
primarySwatch: Colors.blue,
),
@ -45,22 +35,15 @@ class MyHomePage extends StatefulWidget {
class _MyHomePageState extends State<MyHomePage> {
final _barcodeScanner = BarcodeScanner(formats: [BarcodeFormat.qrCode]);
// late final AnalysisController _scannerController;
final _rxTextController = BehaviorSubject<String>();
late final Stream<String> _rxTextStream = _rxTextController.stream;
TxConfig? _txConfig;
var _cuttleState = CuttleState.unitialized;
final HashSet<RaptorPacket> _rxData = HashSet();
String _rxText = '';
int _rxCount = 1;
int _bSize = 0;
final _formatter = NumberFormat('###,###,###');
final _buffer = <String>[];
final _barcodesController = BehaviorSubject<List<String>>();
late final Stream<List<String>> _barcodesStream = _barcodesController.stream;
final _scrollController = ScrollController();
@override
void dispose() {
_rxTextController.close();
_barcodesController.close();
super.dispose();
}
@ -68,19 +51,19 @@ class _MyHomePageState extends State<MyHomePage> {
Widget build(BuildContext context) {
return Scaffold(
body: CameraAwesomeBuilder.previewOnly(
zoom: 1.5,
onImageForAnalysis: (img) => _processImageBarcode(img),
imageAnalysisConfig: AnalysisConfig(
androidOptions: const AndroidAnalysisOptions.nv21(
width: 600,
width: 1024,
),
maxFramesPerSecond: 20,
autoStart: true,
maxFramesPerSecond: 5,
autoStart: false,
),
builder: (cameraModeState, previewSize, previewRect) {
//_scannerController = cameraModeState.analysisController!;
return _RxTextDisplayWidget(
rxTextStream: _rxTextStream,
return _BarcodeDisplayWidget(
barcodesStream: _barcodesStream,
scrollController: _scrollController,
analysisController: cameraModeState.analysisController!,
);
},
),
@ -93,133 +76,96 @@ class _MyHomePageState extends State<MyHomePage> {
try {
var recognizedBarCodes = await _barcodeScanner.processImage(inputImage);
for (Barcode barcode in recognizedBarCodes) {
var bytes = barcode.rawBytes;
if (bytes == null) {
continue;
}
final Uint8List dbytes = bytes;
switch (_cuttleState) {
case CuttleState.unitialized:
{
var txconf = await api.getTxConfig(bytes: dbytes);
if (txconf != null) {
_txConfig = txconf;
_cuttleState = CuttleState.receiving;
final fname =
_txConfig!.filename ?? "large text on the command line";
final desc = _txConfig!.description;
final text =
'Receiving $fname, ${_formatter.format(_txConfig!.len)} bytes ($desc)';
_rxText = text;
_rxTextController.add(text);
continue;
}
// implicit else here; txconf was null
var text = barcode.rawValue;
if (text != null) {
// it's not a txconfig, and it's not a raptor packet, so it must be a regular qr code
_rxText = text;
_rxTextController.add(text);
_cuttleState = CuttleState.received;
}
}
case CuttleState.receiving:
{
if (_bSize > 0 && dbytes.length < _bSize) {
continue;
}
if (_bSize == 0) {
var txconf = await api.getTxConfig(bytes: dbytes);
if (txconf != null) {
await api.dropTxConfig(txc: txconf);
continue;
}
}
_bSize = dbytes.length;
final packet = RaptorPacket(field0: dbytes);
_rxData.add(packet);
_rxCount += 1;
final bytesTotal = _rxData.length * dbytes.length;
if (_rxCount % 40 == 0) {
_rxCount = 1;
// if we've not received at least as many bytes as txconf.len,
// we cannot have enough bytes to reconstruct, so only try to
// decode if we've gotten at least that many
if (bytesTotal > _txConfig!.len) {
final content = await api.decodePackets(
packets: _rxData.toList(), txconf: _txConfig!);
if (content != null) {
_rxData.clear();
_barcodeScanner.close();
_cuttleState = CuttleState.received;
_rxTextController.add("DONE RECEIVING $_rxText");
final f =
await _saveReceivedFile(_txConfig!.filename, content);
_rxTextController.add("Saved content to $f");
continue;
}
}
}
final pct = (100.0 * bytesTotal / _txConfig!.len).floor();
_rxTextController.add(
"$_rxText -- $pct% received (${_formatter.format(bytesTotal)} bytes)");
}
case CuttleState.received:
continue;
}
debugPrint("Barcode: [${barcode.format}]: ${barcode.rawBytes}");
_addBarcode("[${barcode.format.name}]: ${barcode.rawValue}");
}
} catch (error) {
debugPrint("sending image resulted error $error");
debugPrint("...sending image resulted error $error");
}
}
Future<String> _saveReceivedFile(String? filename, Uint8List bytes) async {
final Directory downloadDir = Directory('/storage/emulated/0/Download');
final String fname =
filename ?? "cuttle_${DateTime.now().millisecondsSinceEpoch}.txt";
final String path = "${downloadDir.path}/$fname";
final file = await File(path).create();
await file.writeAsBytes(bytes, flush: true);
return path;
void _addBarcode(String value) {
try {
if (_buffer.length > 300) {
_buffer.removeRange(_buffer.length - 300, _buffer.length);
}
if (_buffer.isEmpty || value != _buffer[0]) {
_buffer.insert(0, value);
_barcodesController.add(_buffer);
_scrollController.animateTo(
0,
duration: const Duration(milliseconds: 400),
curve: Curves.fastLinearToSlowEaseIn,
);
}
} catch (err) {
debugPrint("...logging error $err");
}
}
}
class _RxTextDisplayWidget extends StatefulWidget {
final Stream<String> rxTextStream;
class _BarcodeDisplayWidget extends StatefulWidget {
final Stream<List<String>> barcodesStream;
final ScrollController scrollController;
const _RxTextDisplayWidget({
final AnalysisController analysisController;
const _BarcodeDisplayWidget({
// ignore: unused_element
super.key,
required this.rxTextStream,
required this.barcodesStream,
required this.scrollController,
required this.analysisController,
});
@override
State<_RxTextDisplayWidget> createState() => _RxTextDisplayWidgetState();
State<_BarcodeDisplayWidget> createState() => _BarcodeDisplayWidgetState();
}
class _RxTextDisplayWidgetState extends State<_RxTextDisplayWidget> {
class _BarcodeDisplayWidgetState extends State<_BarcodeDisplayWidget> {
@override
Widget build(BuildContext context) {
return Align(
alignment: Alignment.bottomCenter,
child: Container(
decoration: const BoxDecoration(
color: Colors.white,
decoration: BoxDecoration(
color: Colors.tealAccent.withOpacity(0.7),
),
child: Column(mainAxisSize: MainAxisSize.min, children: [
Material(
color: Colors.transparent,
child: CheckboxListTile(
value: widget.analysisController.enabled,
onChanged: (newValue) async {
if (widget.analysisController.enabled == true) {
await widget.analysisController.stop();
} else {
await widget.analysisController.start();
}
setState(() {});
},
title: const Text(
"Enable barcode scan",
style: TextStyle(fontWeight: FontWeight.bold),
),
),
),
Container(
height: 120,
padding: const EdgeInsets.symmetric(horizontal: 16),
child: SelectionArea(
child: StreamBuilder<String>(
stream: widget.rxTextStream,
builder: (context, value) =>
!value.hasData ? const SizedBox.expand() : Text(value.data!),
)),
child: StreamBuilder<List<String>>(
stream: widget.barcodesStream,
builder: (context, value) => !value.hasData
? const SizedBox.expand()
: ListView.separated(
padding: const EdgeInsets.only(top: 8),
controller: widget.scrollController,
itemCount: value.data!.length,
separatorBuilder: (context, index) =>
const SizedBox(height: 4),
itemBuilder: (context, index) => Text(value.data![index]),
),
),
),
]),
),

View file

@ -6,11 +6,8 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
crate-type = ["cdylib", "rlib"]
crate-type = ["cdylib", "lib"]
[dependencies]
anyhow = "1"
cuttle = { path = "../../", default-features = false }
flutter_rust_bridge = "1"
raptorq = "1.7.0"
rkyv = { version = "0.7.42", features = ["validation"] }

View file

@ -1,48 +1,59 @@
use raptorq::{Decoder, EncodingPacket, ObjectTransmissionInformation};
// This is the entry point of your Rust library.
// When adding new code to your project, note that only items used
// here will be transformed to their Dart equivalents.
#[derive(Debug, Clone)]
pub struct TxConfig {
pub len: u64,
pub mtu: u16,
pub description: String,
pub filename: Option<String>,
// A plain enum without any fields. This is similar to Dart- or C-style enums.
// flutter_rust_bridge is capable of generating code for enums with fields
// (@freezed classes in Dart and tagged unions in C).
pub enum Platform {
Unknown,
AndroidBish,
Ios,
Windows,
Unix,
MacIntel,
MacApple,
Wasm,
}
#[derive(Debug, Clone)]
pub struct RaptorPacket(pub Vec<u8>);
impl From<cuttle::TxConfig> for TxConfig {
fn from(value: cuttle::TxConfig) -> Self {
TxConfig {
len: value.len,
mtu: value.mtu,
description: value.description,
filename: value.filename,
}
}
}
pub fn get_tx_config(bytes: Vec<u8>) -> Option<TxConfig> {
if let Ok(archive) = rkyv::check_archived_root::<cuttle::TxConfig>(&bytes) {
<cuttle::ArchivedTxConfig as rkyv::Deserialize<cuttle::TxConfig, rkyv::Infallible>>::deserialize(archive, &mut rkyv::Infallible)
.ok().map(Into::into)
// A function definition in Rust. Similar to Dart, the return type must always be named
// and is never inferred.
pub fn platform() -> Platform {
// This is a macro, a special expression that expands into code. In Rust, all macros
// end with an exclamation mark and can be invoked with all kinds of brackets (parentheses,
// brackets and curly braces). However, certain conventions exist, for example the
// vector macro is almost always invoked as vec![..].
//
// The cfg!() macro returns a boolean value based on the current compiler configuration.
// When attached to expressions (#[cfg(..)] form), they show or hide the expression at compile time.
// Here, however, they evaluate to runtime values, which may or may not be optimized out
// by the compiler. A variety of configurations are demonstrated here which cover most of
// the modern oeprating systems. Try running the Flutter application on different machines
// and see if it matches your expected OS.
//
// Furthermore, in Rust, the last expression in a function is the return value and does
// not have the trailing semicolon. This entire if-else chain forms a single expression.
if cfg!(windows) {
Platform::Windows
} else if cfg!(target_os = "android") {
Platform::AndroidBish
} else if cfg!(target_os = "ios") {
Platform::Ios
} else if cfg!(all(target_os = "macos", target_arch = "aarch64")) {
Platform::MacApple
} else if cfg!(target_os = "macos") {
Platform::MacIntel
} else if cfg!(target_family = "wasm") {
Platform::Wasm
} else if cfg!(unix) {
Platform::Unix
} else {
None
Platform::Unknown
}
}
pub fn drop_tx_config(_txc: TxConfig) {}
pub fn decode_packets(packets: Vec<RaptorPacket>, txconf: TxConfig) -> Option<Vec<u8>> {
let conf = ObjectTransmissionInformation::with_defaults(txconf.len, txconf.mtu);
let mut decoder = Decoder::new(conf);
let mut res = None;
for RaptorPacket(p) in &packets {
res = decoder.decode(EncodingPacket::deserialize(p));
if res.is_some() {
break;
}
}
res
// The convention for Rust identifiers is the snake_case,
// and they are automatically converted to camelCase on the Dart side.
pub fn rust_release_mode() -> bool {
cfg!(not(debug_assertions))
}

View file

@ -2,130 +2,23 @@ use super::*;
// Section: wire functions
#[no_mangle]
pub extern "C" fn wire_get_tx_config(port_: i64, bytes: *mut wire_uint_8_list) {
wire_get_tx_config_impl(port_, bytes)
pub extern "C" fn wire_platform(port_: i64) {
wire_platform_impl(port_)
}
#[no_mangle]
pub extern "C" fn wire_drop_tx_config(port_: i64, _txc: *mut wire_TxConfig) {
wire_drop_tx_config_impl(port_, _txc)
}
#[no_mangle]
pub extern "C" fn wire_decode_packets(
port_: i64,
packets: *mut wire_list_raptor_packet,
txconf: *mut wire_TxConfig,
) {
wire_decode_packets_impl(port_, packets, txconf)
pub extern "C" fn wire_rust_release_mode(port_: i64) {
wire_rust_release_mode_impl(port_)
}
// Section: allocate functions
#[no_mangle]
pub extern "C" fn new_box_autoadd_tx_config_0() -> *mut wire_TxConfig {
support::new_leak_box_ptr(wire_TxConfig::new_with_null_ptr())
}
#[no_mangle]
pub extern "C" fn new_list_raptor_packet_0(len: i32) -> *mut wire_list_raptor_packet {
let wrap = wire_list_raptor_packet {
ptr: support::new_leak_vec_ptr(<wire_RaptorPacket>::new_with_null_ptr(), len),
len,
};
support::new_leak_box_ptr(wrap)
}
#[no_mangle]
pub extern "C" fn new_uint_8_list_0(len: i32) -> *mut wire_uint_8_list {
let ans = wire_uint_8_list {
ptr: support::new_leak_vec_ptr(Default::default(), len),
len,
};
support::new_leak_box_ptr(ans)
}
// Section: related functions
// Section: impl Wire2Api
impl Wire2Api<String> for *mut wire_uint_8_list {
fn wire2api(self) -> String {
let vec: Vec<u8> = self.wire2api();
String::from_utf8_lossy(&vec).into_owned()
}
}
impl Wire2Api<TxConfig> for *mut wire_TxConfig {
fn wire2api(self) -> TxConfig {
let wrap = unsafe { support::box_from_leak_ptr(self) };
Wire2Api::<TxConfig>::wire2api(*wrap).into()
}
}
impl Wire2Api<Vec<RaptorPacket>> for *mut wire_list_raptor_packet {
fn wire2api(self) -> Vec<RaptorPacket> {
let vec = unsafe {
let wrap = support::box_from_leak_ptr(self);
support::vec_from_leak_ptr(wrap.ptr, wrap.len)
};
vec.into_iter().map(Wire2Api::wire2api).collect()
}
}
impl Wire2Api<RaptorPacket> for wire_RaptorPacket {
fn wire2api(self) -> RaptorPacket {
RaptorPacket(self.field0.wire2api())
}
}
impl Wire2Api<TxConfig> for wire_TxConfig {
fn wire2api(self) -> TxConfig {
TxConfig {
len: self.len.wire2api(),
mtu: self.mtu.wire2api(),
description: self.description.wire2api(),
filename: self.filename.wire2api(),
}
}
}
impl Wire2Api<Vec<u8>> for *mut wire_uint_8_list {
fn wire2api(self) -> Vec<u8> {
unsafe {
let wrap = support::box_from_leak_ptr(self);
support::vec_from_leak_ptr(wrap.ptr, wrap.len)
}
}
}
// Section: wire structs
#[repr(C)]
#[derive(Clone)]
pub struct wire_list_raptor_packet {
ptr: *mut wire_RaptorPacket,
len: i32,
}
#[repr(C)]
#[derive(Clone)]
pub struct wire_RaptorPacket {
field0: *mut wire_uint_8_list,
}
#[repr(C)]
#[derive(Clone)]
pub struct wire_TxConfig {
len: u64,
mtu: u16,
description: *mut wire_uint_8_list,
filename: *mut wire_uint_8_list,
}
#[repr(C)]
#[derive(Clone)]
pub struct wire_uint_8_list {
ptr: *mut u8,
len: i32,
}
// Section: impl NewWithNullPtr
pub trait NewWithNullPtr {
@ -138,37 +31,6 @@ impl<T> NewWithNullPtr for *mut T {
}
}
impl NewWithNullPtr for wire_RaptorPacket {
fn new_with_null_ptr() -> Self {
Self {
field0: core::ptr::null_mut(),
}
}
}
impl Default for wire_RaptorPacket {
fn default() -> Self {
Self::new_with_null_ptr()
}
}
impl NewWithNullPtr for wire_TxConfig {
fn new_with_null_ptr() -> Self {
Self {
len: Default::default(),
mtu: Default::default(),
description: core::ptr::null_mut(),
filename: core::ptr::null_mut(),
}
}
}
impl Default for wire_TxConfig {
fn default() -> Self {
Self::new_with_null_ptr()
}
}
// Section: sync execution mode utility
#[no_mangle]

View file

@ -22,48 +22,24 @@ use crate::api::*;
// Section: wire functions
fn wire_get_tx_config_impl(port_: MessagePort, bytes: impl Wire2Api<Vec<u8>> + UnwindSafe) {
FLUTTER_RUST_BRIDGE_HANDLER.wrap::<_, _, _, Option<TxConfig>>(
fn wire_platform_impl(port_: MessagePort) {
FLUTTER_RUST_BRIDGE_HANDLER.wrap::<_, _, _, Platform>(
WrapInfo {
debug_name: "get_tx_config",
debug_name: "platform",
port: Some(port_),
mode: FfiCallMode::Normal,
},
move || {
let api_bytes = bytes.wire2api();
move |task_callback| Ok(get_tx_config(api_bytes))
},
move || move |task_callback| Ok(platform()),
)
}
fn wire_drop_tx_config_impl(port_: MessagePort, _txc: impl Wire2Api<TxConfig> + UnwindSafe) {
FLUTTER_RUST_BRIDGE_HANDLER.wrap::<_, _, _, ()>(
fn wire_rust_release_mode_impl(port_: MessagePort) {
FLUTTER_RUST_BRIDGE_HANDLER.wrap::<_, _, _, bool>(
WrapInfo {
debug_name: "drop_tx_config",
debug_name: "rust_release_mode",
port: Some(port_),
mode: FfiCallMode::Normal,
},
move || {
let api__txc = _txc.wire2api();
move |task_callback| Ok(drop_tx_config(api__txc))
},
)
}
fn wire_decode_packets_impl(
port_: MessagePort,
packets: impl Wire2Api<Vec<RaptorPacket>> + UnwindSafe,
txconf: impl Wire2Api<TxConfig> + UnwindSafe,
) {
FLUTTER_RUST_BRIDGE_HANDLER.wrap::<_, _, _, Option<Vec<u8>>>(
WrapInfo {
debug_name: "decode_packets",
port: Some(port_),
mode: FfiCallMode::Normal,
},
move || {
let api_packets = packets.wire2api();
let api_txconf = txconf.wire2api();
move |task_callback| Ok(decode_packets(api_packets, api_txconf))
},
move || move |task_callback| Ok(rust_release_mode()),
)
}
// Section: wrapper structs
@ -88,38 +64,25 @@ where
(!self.is_null()).then(|| self.wire2api())
}
}
impl Wire2Api<u16> for u16 {
fn wire2api(self) -> u16 {
self
}
}
impl Wire2Api<u64> for u64 {
fn wire2api(self) -> u64 {
self
}
}
impl Wire2Api<u8> for u8 {
fn wire2api(self) -> u8 {
self
}
}
// Section: impl IntoDart
impl support::IntoDart for TxConfig {
impl support::IntoDart for Platform {
fn into_dart(self) -> support::DartAbi {
vec![
self.len.into_into_dart().into_dart(),
self.mtu.into_into_dart().into_dart(),
self.description.into_into_dart().into_dart(),
self.filename.into_dart(),
]
match self {
Self::Unknown => 0,
Self::AndroidBish => 1,
Self::Ios => 2,
Self::Windows => 3,
Self::Unix => 4,
Self::MacIntel => 5,
Self::MacApple => 6,
Self::Wasm => 7,
}
.into_dart()
}
}
impl support::IntoDartExceptPrimitive for TxConfig {}
impl rust2dart::IntoIntoDart<TxConfig> for TxConfig {
impl support::IntoDartExceptPrimitive for Platform {}
impl rust2dart::IntoIntoDart<Platform> for Platform {
fn into_into_dart(self) -> Self {
self
}

View file

@ -33,19 +33,19 @@ dependencies:
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
camerawesome: ^1.4.0
cupertino_icons: ^1.0.2
ffi: ^2.0.1
flutter_rust_bridge: ^1.45.0
google_mlkit_barcode_scanning: ^0.5.0
google_mlkit_commons: ^0.2.0
image: ^4.0.17
meta: ^1.8.0
mobile_scanner: ^3.4.1
path: ^1.8.3
path_provider: ^2.1.0
rxdart: ^0.27.7
uuid: ^3.0.7
intl: ^0.18.1
path_provider: ^2.1.0
path: ^1.8.3
mobile_scanner: ^3.4.1
camerawesome: ^1.4.0
google_mlkit_barcode_scanning: ^0.5.0
rxdart: ^0.27.7
image: ^4.0.17
google_mlkit_commons: ^0.2.0
dev_dependencies:
flutter_test:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 816 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 708 KiB

View file

@ -3,8 +3,6 @@ use std::{
time::{Duration, Instant},
};
use rand::seq::SliceRandom;
use raptorq::{Encoder, ObjectTransmissionInformation};
use rkyv::{Archive, Deserialize, Serialize};
#[cfg(feature = "desktop")]
@ -13,11 +11,10 @@ mod desktop;
mod qr_utils;
#[cfg(feature = "desktop")]
pub use qr_utils::{get_content, mk_qr_bytes};
pub use qr_utils::{get_content, mk_qr_bytes, stream_bytes};
pub type CuttleSender = std::sync::mpsc::SyncSender<Vec<u8>>;
pub type CuttleReceiver = std::sync::mpsc::Receiver<Vec<u8>>;
pub const STREAMING_MTU: u16 = 1200;
/// The application state
#[derive(Debug)]
@ -73,48 +70,9 @@ pub enum StreamStatus {
}
#[derive(Debug, Clone, PartialEq, Eq, Archive, Serialize, Deserialize)]
#[archive(check_bytes)]
#[repr(C)]
pub struct TxConfig {
pub len: u64,
pub mtu: u16,
pub description: String,
pub filename: Option<String>,
}
pub fn stream_bytes(
bytes: Vec<u8>,
tx: CuttleSender,
desc: &str,
filename: Option<&str>,
) -> Vec<u8> {
let len = bytes.len() as u64;
let rng = &mut rand::thread_rng();
let config = ObjectTransmissionInformation::with_defaults(len, STREAMING_MTU);
let encoder = Encoder::new(&bytes, config);
let mut packets = encoder
.get_encoded_packets(10)
.iter()
.map(|p| p.serialize())
.collect::<Vec<_>>();
packets.shuffle(rng);
std::thread::spawn(move || {
for packet in packets.iter().cycle() {
tx.send(packet.clone()).unwrap_or_default();
}
});
let txconfig = TxConfig {
len,
mtu: STREAMING_MTU,
description: desc.to_string(),
filename: filename.map(|f| f.to_string()),
};
rkyv::to_bytes::<_, 256>(&txconfig)
.expect("tried to serialize the txconfig")
.to_vec()
}

View file

@ -30,17 +30,16 @@ fn main() -> Result<(), eframe::Error> {
let cli = Cli::parse();
let (description, filename) = if let Some(ref file) = cli.file {
let description = if let Some(ref file) = cli.file {
let text = cli.text().join(" ");
let sep = if text.is_empty() { "" } else { ": " };
let file = std::path::Path::new(&file)
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
(format!("{file}{sep}{text}"), Some(file))
.to_string_lossy();
format!("{file}{sep}{text}")
} else {
("text message".to_string(), None)
"text message".to_string()
};
let bytes = if let Some(ref file) = cli.file {
@ -49,7 +48,7 @@ fn main() -> Result<(), eframe::Error> {
cli.text().join(" ").bytes().collect()
};
let content = get_content(bytes, &description, filename.as_deref());
let content = get_content(bytes, &description);
let flasher = Flasher::new(description, content, cli.fps);

View file

@ -1,11 +1,46 @@
use fast_qr::convert::image::ImageBuilder;
use rand::seq::SliceRandom;
use raptorq::{Encoder, ObjectTransmissionInformation};
use crate::{stream_bytes, Content, StreamedContent};
use crate::{Content, CuttleSender, StreamedContent, TxConfig};
pub const STREAMING_MTU: u16 = 2326;
pub fn stream_bytes(bytes: Vec<u8>, tx: CuttleSender, desc: String) -> Vec<u8> {
let len = bytes.len() as u64;
let txconfig = TxConfig {
len,
mtu: STREAMING_MTU,
description: desc.to_string(),
};
let txconfig = rkyv::to_bytes::<_, 256>(&txconfig)
.expect("tried to serialize the txconfig")
.to_vec();
std::thread::spawn(move || {
let rng = &mut rand::thread_rng();
let config = ObjectTransmissionInformation::with_defaults(len, STREAMING_MTU);
let encoder = Encoder::new(&bytes, config);
let mut packets = encoder
.get_encoded_packets(10)
.iter()
.map(|p| p.serialize())
.collect::<Vec<_>>();
packets.shuffle(rng);
for packet in packets.iter().cycle() {
tx.send(packet.clone()).unwrap_or_default();
}
});
txconfig
}
/// Makes a PNG of a QR code for the given bytes, returns the bytes of the PNG.
pub fn mk_qr_bytes(bytes: &[u8], height: f32) -> Vec<u8> {
let qr = fast_qr::QRBuilder::new(bytes)
.ecl(fast_qr::ECL::L)
.ecl(fast_qr::ECL::M)
.build()
.unwrap();
@ -18,13 +53,13 @@ pub fn mk_qr_bytes(bytes: &[u8], height: f32) -> Vec<u8> {
/// Turns bytes and a description into either a single QR code, or a stream of
/// them, depending on the size of the input.
pub fn get_content(bytes: Vec<u8>, desc: &str, filename: Option<&str>) -> Content {
pub fn get_content(bytes: Vec<u8>, desc: &str) -> Content {
if bytes.len() < 2000 && fast_qr::QRBuilder::new(bytes.clone()).build().is_ok() {
let bytes = bytes.leak();
Content::Static(bytes)
} else {
let (tx, rx) = std::sync::mpsc::sync_channel(2);
let txconfig = stream_bytes(bytes, tx, desc, filename).leak();
let txconfig = stream_bytes(bytes, tx, desc.to_string()).leak();
let stream = StreamedContent::new(txconfig, rx);
Content::Streamed(stream)
}