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

View file

@ -8,7 +8,6 @@ default = ["desktop"]
desktop = ["dep:clap", "dep:eframe", "dep:egui_extras", "dep:fast_qr", "dep:png"] desktop = ["dep:clap", "dep:eframe", "dep:egui_extras", "dep:fast_qr", "dep:png"]
[dependencies] [dependencies]
bytecheck = "0.7"
clap = { version = "4.3", optional = true, features = ["derive", "env"] } clap = { version = "4.3", optional = true, features = ["derive", "env"] }
eframe = { version = "0.22", default-features = false, optional = true, features = ["default_fonts", "wgpu", "tts", "accesskit"] } 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"] } 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 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! 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: 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
**Desktop** loop of them), and the mobile app has been started. Even without the mobile app, it's still
- [x] transmit short text given on the commandline with a single qr code already useful for transmitting short text strings to the phone, since the Android camera app will
- [x] transmit large text given on the commandline with a stream of qr codes decode any detected 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 mobile app has been started, and I hope to have it decoding QR streams soon!
# about the name # about the name

View file

@ -1,38 +1,3 @@
use rand::{seq::SliceRandom, Rng};
use raptorq::{Decoder, Encoder, EncodingPacket, ObjectTransmissionInformation};
fn main() { 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 org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true android.useAndroidX=true
android.enableJetifier=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 flutter_rust_bridge_codegen
lint: lint:
cd native && cargo fmt
dart format . dart format .
native: native-debug native-release
native-release:
cd android && ./gradlew :app:cargoBuildRelease
native-debug:
cd android && ./gradlew :app:cargoBuildDebug
clean: clean:
flutter clean flutter clean
cd native && cargo 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'; import 'package:uuid/uuid.dart';
abstract class Native { 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; FlutterRustBridgeTaskConstMeta get kRustReleaseModeConstMeta;
Future<Uint8List?> decodePackets({required List<RaptorPacket> packets, required TxConfig txconf, dynamic hint});
FlutterRustBridgeTaskConstMeta get kDecodePacketsConstMeta;
} }
class RaptorPacket { enum Platform {
final Uint8List field0; Unknown,
AndroidBish,
const RaptorPacket({ Ios,
required this.field0, Windows,
}); Unix,
} MacIntel,
MacApple,
class TxConfig { Wasm,
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,
});
} }

View file

@ -24,53 +24,34 @@ class NativeImpl implements Native {
/// Only valid on web/WASM platforms. /// Only valid on web/WASM platforms.
factory NativeImpl.wasm(FutureOr<WasmModule> module) => NativeImpl(module as ExternalLibrary); factory NativeImpl.wasm(FutureOr<WasmModule> module) => NativeImpl(module as ExternalLibrary);
NativeImpl.raw(this._platform); NativeImpl.raw(this._platform);
Future<TxConfig?> getTxConfig({required Uint8List bytes, dynamic hint}) { Future<Platform> platform({dynamic hint}) {
var arg0 = _platform.api2wire_uint_8_list(bytes);
return _platform.executeNormal(FlutterRustBridgeTask( return _platform.executeNormal(FlutterRustBridgeTask(
callFfi: (port_) => _platform.inner.wire_get_tx_config(port_, arg0), callFfi: (port_) => _platform.inner.wire_platform(port_),
parseSuccessData: _wire2api_opt_box_autoadd_tx_config, parseSuccessData: _wire2api_platform,
constMeta: kGetTxConfigConstMeta, constMeta: kPlatformConstMeta,
argValues: [bytes], argValues: [],
hint: hint, hint: hint,
)); ));
} }
FlutterRustBridgeTaskConstMeta get kGetTxConfigConstMeta => const FlutterRustBridgeTaskConstMeta( FlutterRustBridgeTaskConstMeta get kPlatformConstMeta => const FlutterRustBridgeTaskConstMeta(
debugName: "get_tx_config", debugName: "platform",
argNames: ["bytes"], argNames: [],
); );
Future<void> dropTxConfig({required TxConfig txc, dynamic hint}) { Future<bool> rustReleaseMode({dynamic hint}) {
var arg0 = _platform.api2wire_box_autoadd_tx_config(txc);
return _platform.executeNormal(FlutterRustBridgeTask( return _platform.executeNormal(FlutterRustBridgeTask(
callFfi: (port_) => _platform.inner.wire_drop_tx_config(port_, arg0), callFfi: (port_) => _platform.inner.wire_rust_release_mode(port_),
parseSuccessData: _wire2api_unit, parseSuccessData: _wire2api_bool,
constMeta: kDropTxConfigConstMeta, constMeta: kRustReleaseModeConstMeta,
argValues: [txc], argValues: [],
hint: hint, hint: hint,
)); ));
} }
FlutterRustBridgeTaskConstMeta get kDropTxConfigConstMeta => const FlutterRustBridgeTaskConstMeta( FlutterRustBridgeTaskConstMeta get kRustReleaseModeConstMeta => const FlutterRustBridgeTaskConstMeta(
debugName: "drop_tx_config", debugName: "rust_release_mode",
argNames: ["txc"], argNames: [],
);
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"],
); );
void dispose() { void dispose() {
@ -78,70 +59,21 @@ class NativeImpl implements Native {
} }
// Section: wire2api // Section: wire2api
String _wire2api_String(dynamic raw) { bool _wire2api_bool(dynamic raw) {
return raw as String; return raw as bool;
} }
TxConfig _wire2api_box_autoadd_tx_config(dynamic raw) { int _wire2api_i32(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) {
return raw as int; return raw as int;
} }
int _wire2api_u64(dynamic raw) { Platform _wire2api_platform(dynamic raw) {
return castInt(raw); return Platform.values[raw as int];
}
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;
} }
} }
// Section: api2wire // Section: api2wire
@protected
int api2wire_u16(int raw) {
return raw;
}
@protected
int api2wire_u8(int raw) {
return raw;
}
// Section: finalizer // Section: finalizer
class NativePlatform extends FlutterRustBridgeBase<NativeWire> { class NativePlatform extends FlutterRustBridgeBase<NativeWire> {
@ -149,61 +81,9 @@ class NativePlatform extends FlutterRustBridgeBase<NativeWire> {
// Section: api2wire // 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: finalizer
// Section: api_fill_to_wire // 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 // 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'); _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>)>(); 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_, int port_,
ffi.Pointer<wire_uint_8_list> bytes,
) { ) {
return _wire_get_tx_config( return _wire_platform(
port_, port_,
bytes,
); );
} }
late final _wire_get_tx_configPtr = late final _wire_platformPtr = _lookup<ffi.NativeFunction<ffi.Void Function(ffi.Int64)>>('wire_platform');
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Int64, ffi.Pointer<wire_uint_8_list>)>>('wire_get_tx_config'); late final _wire_platform = _wire_platformPtr.asFunction<void Function(int)>();
late final _wire_get_tx_config =
_wire_get_tx_configPtr.asFunction<void Function(int, ffi.Pointer<wire_uint_8_list>)>();
void wire_drop_tx_config( void wire_rust_release_mode(
int port_, int port_,
ffi.Pointer<wire_TxConfig> _txc,
) { ) {
return _wire_drop_tx_config( return _wire_rust_release_mode(
port_, port_,
_txc,
); );
} }
late final _wire_drop_tx_configPtr = late final _wire_rust_release_modePtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Int64, ffi.Pointer<wire_TxConfig>)>>('wire_drop_tx_config'); _lookup<ffi.NativeFunction<ffi.Void Function(ffi.Int64)>>('wire_rust_release_mode');
late final _wire_drop_tx_config = late final _wire_rust_release_mode = _wire_rust_release_modePtr.asFunction<void Function(int)>();
_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)>();
void free_WireSyncReturn( void free_WireSyncReturn(
WireSyncReturn ptr, WireSyncReturn ptr,
@ -382,36 +202,6 @@ class NativeWire implements FlutterRustBridgeWireBase {
final class _Dart_Handle extends ffi.Opaque {} 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 typedef DartPostCObjectFnType
= ffi.Pointer<ffi.NativeFunction<ffi.Bool Function(DartPort port_id, ffi.Pointer<ffi.Void> message)>>; = ffi.Pointer<ffi.NativeFunction<ffi.Bool Function(DartPort port_id, ffi.Pointer<ffi.Void> message)>>;
typedef DartPort = ffi.Int64; 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. // Re-export the bridge so it is only necessary to import this file.
export 'bridge_generated.dart'; export 'bridge_generated.dart';
import 'dart:io' as io;
const _base = 'native'; const _base = 'native';

View file

@ -1,33 +1,23 @@
import 'dart:async'; 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:camerawesome/camerawesome_plugin.dart';
import 'package:flutter/material.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:google_mlkit_barcode_scanning/google_mlkit_barcode_scanning.dart';
import 'package:intl/intl.dart';
import 'package:rxdart/rxdart.dart'; import 'package:rxdart/rxdart.dart';
import 'ffi.dart';
import 'utils/mlkit_utils.dart';
void main() { void main() {
runApp(const MyApp()); runApp(const MyApp());
} }
enum CuttleState {
unitialized,
receiving,
received,
}
class MyApp extends StatelessWidget { class MyApp extends StatelessWidget {
const MyApp({super.key}); const MyApp({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( return MaterialApp(
title: 'Cuttle', title: 'camerAwesome App',
theme: ThemeData( theme: ThemeData(
primarySwatch: Colors.blue, primarySwatch: Colors.blue,
), ),
@ -45,22 +35,15 @@ class MyHomePage extends StatefulWidget {
class _MyHomePageState extends State<MyHomePage> { class _MyHomePageState extends State<MyHomePage> {
final _barcodeScanner = BarcodeScanner(formats: [BarcodeFormat.qrCode]); final _barcodeScanner = BarcodeScanner(formats: [BarcodeFormat.qrCode]);
// late final AnalysisController _scannerController;
final _rxTextController = BehaviorSubject<String>();
late final Stream<String> _rxTextStream = _rxTextController.stream;
TxConfig? _txConfig; final _buffer = <String>[];
var _cuttleState = CuttleState.unitialized; final _barcodesController = BehaviorSubject<List<String>>();
final HashSet<RaptorPacket> _rxData = HashSet(); late final Stream<List<String>> _barcodesStream = _barcodesController.stream;
String _rxText = ''; final _scrollController = ScrollController();
int _rxCount = 1;
int _bSize = 0;
final _formatter = NumberFormat('###,###,###');
@override @override
void dispose() { void dispose() {
_rxTextController.close(); _barcodesController.close();
super.dispose(); super.dispose();
} }
@ -68,19 +51,19 @@ class _MyHomePageState extends State<MyHomePage> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
body: CameraAwesomeBuilder.previewOnly( body: CameraAwesomeBuilder.previewOnly(
zoom: 1.5,
onImageForAnalysis: (img) => _processImageBarcode(img), onImageForAnalysis: (img) => _processImageBarcode(img),
imageAnalysisConfig: AnalysisConfig( imageAnalysisConfig: AnalysisConfig(
androidOptions: const AndroidAnalysisOptions.nv21( androidOptions: const AndroidAnalysisOptions.nv21(
width: 600, width: 1024,
), ),
maxFramesPerSecond: 20, maxFramesPerSecond: 5,
autoStart: true, autoStart: false,
), ),
builder: (cameraModeState, previewSize, previewRect) { builder: (cameraModeState, previewSize, previewRect) {
//_scannerController = cameraModeState.analysisController!; return _BarcodeDisplayWidget(
return _RxTextDisplayWidget( barcodesStream: _barcodesStream,
rxTextStream: _rxTextStream, scrollController: _scrollController,
analysisController: cameraModeState.analysisController!,
); );
}, },
), ),
@ -93,133 +76,96 @@ class _MyHomePageState extends State<MyHomePage> {
try { try {
var recognizedBarCodes = await _barcodeScanner.processImage(inputImage); var recognizedBarCodes = await _barcodeScanner.processImage(inputImage);
for (Barcode barcode in recognizedBarCodes) { for (Barcode barcode in recognizedBarCodes) {
var bytes = barcode.rawBytes; debugPrint("Barcode: [${barcode.format}]: ${barcode.rawBytes}");
if (bytes == null) { _addBarcode("[${barcode.format.name}]: ${barcode.rawValue}");
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;
}
} }
} catch (error) { } catch (error) {
debugPrint("sending image resulted error $error"); debugPrint("...sending image resulted error $error");
} }
} }
Future<String> _saveReceivedFile(String? filename, Uint8List bytes) async { void _addBarcode(String value) {
final Directory downloadDir = Directory('/storage/emulated/0/Download'); try {
final String fname = if (_buffer.length > 300) {
filename ?? "cuttle_${DateTime.now().millisecondsSinceEpoch}.txt"; _buffer.removeRange(_buffer.length - 300, _buffer.length);
final String path = "${downloadDir.path}/$fname"; }
final file = await File(path).create(); if (_buffer.isEmpty || value != _buffer[0]) {
await file.writeAsBytes(bytes, flush: true); _buffer.insert(0, value);
return path; _barcodesController.add(_buffer);
_scrollController.animateTo(
0,
duration: const Duration(milliseconds: 400),
curve: Curves.fastLinearToSlowEaseIn,
);
}
} catch (err) {
debugPrint("...logging error $err");
}
} }
} }
class _RxTextDisplayWidget extends StatefulWidget { class _BarcodeDisplayWidget extends StatefulWidget {
final Stream<String> rxTextStream; final Stream<List<String>> barcodesStream;
final ScrollController scrollController;
const _RxTextDisplayWidget({ final AnalysisController analysisController;
const _BarcodeDisplayWidget({
// ignore: unused_element // ignore: unused_element
super.key, super.key,
required this.rxTextStream, required this.barcodesStream,
required this.scrollController,
required this.analysisController,
}); });
@override @override
State<_RxTextDisplayWidget> createState() => _RxTextDisplayWidgetState(); State<_BarcodeDisplayWidget> createState() => _BarcodeDisplayWidgetState();
} }
class _RxTextDisplayWidgetState extends State<_RxTextDisplayWidget> { class _BarcodeDisplayWidgetState extends State<_BarcodeDisplayWidget> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Align( return Align(
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
child: Container( child: Container(
decoration: const BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.tealAccent.withOpacity(0.7),
), ),
child: Column(mainAxisSize: MainAxisSize.min, children: [ 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( Container(
height: 120, height: 120,
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
child: SelectionArea( child: StreamBuilder<List<String>>(
child: StreamBuilder<String>( stream: widget.barcodesStream,
stream: widget.rxTextStream, builder: (context, value) => !value.hasData
builder: (context, value) => ? const SizedBox.expand()
!value.hasData ? const SizedBox.expand() : Text(value.data!), : 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 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib] [lib]
crate-type = ["cdylib", "rlib"] crate-type = ["cdylib", "lib"]
[dependencies] [dependencies]
anyhow = "1" anyhow = "1"
cuttle = { path = "../../", default-features = false }
flutter_rust_bridge = "1" 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)] // A plain enum without any fields. This is similar to Dart- or C-style enums.
pub struct TxConfig { // flutter_rust_bridge is capable of generating code for enums with fields
pub len: u64, // (@freezed classes in Dart and tagged unions in C).
pub mtu: u16, pub enum Platform {
pub description: String, Unknown,
pub filename: Option<String>, AndroidBish,
Ios,
Windows,
Unix,
MacIntel,
MacApple,
Wasm,
} }
#[derive(Debug, Clone)] // A function definition in Rust. Similar to Dart, the return type must always be named
pub struct RaptorPacket(pub Vec<u8>); // and is never inferred.
pub fn platform() -> Platform {
impl From<cuttle::TxConfig> for TxConfig { // This is a macro, a special expression that expands into code. In Rust, all macros
fn from(value: cuttle::TxConfig) -> Self { // end with an exclamation mark and can be invoked with all kinds of brackets (parentheses,
TxConfig { // brackets and curly braces). However, certain conventions exist, for example the
len: value.len, // vector macro is almost always invoked as vec![..].
mtu: value.mtu, //
description: value.description, // The cfg!() macro returns a boolean value based on the current compiler configuration.
filename: value.filename, // 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.
pub fn get_tx_config(bytes: Vec<u8>) -> Option<TxConfig> { //
if let Ok(archive) = rkyv::check_archived_root::<cuttle::TxConfig>(&bytes) { // Furthermore, in Rust, the last expression in a function is the return value and does
<cuttle::ArchivedTxConfig as rkyv::Deserialize<cuttle::TxConfig, rkyv::Infallible>>::deserialize(archive, &mut rkyv::Infallible) // not have the trailing semicolon. This entire if-else chain forms a single expression.
.ok().map(Into::into) 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 { } else {
None Platform::Unknown
} }
} }
pub fn drop_tx_config(_txc: TxConfig) {} // The convention for Rust identifiers is the snake_case,
// and they are automatically converted to camelCase on the Dart side.
pub fn decode_packets(packets: Vec<RaptorPacket>, txconf: TxConfig) -> Option<Vec<u8>> { pub fn rust_release_mode() -> bool {
let conf = ObjectTransmissionInformation::with_defaults(txconf.len, txconf.mtu); cfg!(not(debug_assertions))
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
} }

View file

@ -2,130 +2,23 @@ use super::*;
// Section: wire functions // Section: wire functions
#[no_mangle] #[no_mangle]
pub extern "C" fn wire_get_tx_config(port_: i64, bytes: *mut wire_uint_8_list) { pub extern "C" fn wire_platform(port_: i64) {
wire_get_tx_config_impl(port_, bytes) wire_platform_impl(port_)
} }
#[no_mangle] #[no_mangle]
pub extern "C" fn wire_drop_tx_config(port_: i64, _txc: *mut wire_TxConfig) { pub extern "C" fn wire_rust_release_mode(port_: i64) {
wire_drop_tx_config_impl(port_, _txc) wire_rust_release_mode_impl(port_)
}
#[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)
} }
// Section: allocate functions // 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: related functions
// Section: impl Wire2Api // 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 // 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 // Section: impl NewWithNullPtr
pub trait 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 // Section: sync execution mode utility
#[no_mangle] #[no_mangle]

View file

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

View file

@ -33,19 +33,19 @@ dependencies:
# The following adds the Cupertino Icons font to your application. # The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons. # Use with the CupertinoIcons class for iOS style icons.
camerawesome: ^1.4.0 cupertino_icons: ^1.0.2
ffi: ^2.0.1 ffi: ^2.0.1
flutter_rust_bridge: ^1.45.0 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 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 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: dev_dependencies:
flutter_test: 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}, time::{Duration, Instant},
}; };
use rand::seq::SliceRandom;
use raptorq::{Encoder, ObjectTransmissionInformation};
use rkyv::{Archive, Deserialize, Serialize}; use rkyv::{Archive, Deserialize, Serialize};
#[cfg(feature = "desktop")] #[cfg(feature = "desktop")]
@ -13,11 +11,10 @@ mod desktop;
mod qr_utils; mod qr_utils;
#[cfg(feature = "desktop")] #[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 CuttleSender = std::sync::mpsc::SyncSender<Vec<u8>>;
pub type CuttleReceiver = std::sync::mpsc::Receiver<Vec<u8>>; pub type CuttleReceiver = std::sync::mpsc::Receiver<Vec<u8>>;
pub const STREAMING_MTU: u16 = 1200;
/// The application state /// The application state
#[derive(Debug)] #[derive(Debug)]
@ -73,48 +70,9 @@ pub enum StreamStatus {
} }
#[derive(Debug, Clone, PartialEq, Eq, Archive, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Archive, Serialize, Deserialize)]
#[archive(check_bytes)] #[repr(C)]
pub struct TxConfig { pub struct TxConfig {
pub len: u64, pub len: u64,
pub mtu: u16, pub mtu: u16,
pub description: String, 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 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 text = cli.text().join(" ");
let sep = if text.is_empty() { "" } else { ": " }; let sep = if text.is_empty() { "" } else { ": " };
let file = std::path::Path::new(&file) let file = std::path::Path::new(&file)
.file_name() .file_name()
.unwrap_or_default() .unwrap_or_default()
.to_string_lossy() .to_string_lossy();
.to_string(); format!("{file}{sep}{text}")
(format!("{file}{sep}{text}"), Some(file))
} else { } else {
("text message".to_string(), None) "text message".to_string()
}; };
let bytes = if let Some(ref file) = cli.file { let bytes = if let Some(ref file) = cli.file {
@ -49,7 +48,7 @@ fn main() -> Result<(), eframe::Error> {
cli.text().join(" ").bytes().collect() 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); let flasher = Flasher::new(description, content, cli.fps);

View file

@ -1,11 +1,46 @@
use fast_qr::convert::image::ImageBuilder; 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. /// 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> { pub fn mk_qr_bytes(bytes: &[u8], height: f32) -> Vec<u8> {
let qr = fast_qr::QRBuilder::new(bytes) let qr = fast_qr::QRBuilder::new(bytes)
.ecl(fast_qr::ECL::L) .ecl(fast_qr::ECL::M)
.build() .build()
.unwrap(); .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 /// Turns bytes and a description into either a single QR code, or a stream of
/// them, depending on the size of the input. /// 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() { if bytes.len() < 2000 && fast_qr::QRBuilder::new(bytes.clone()).build().is_ok() {
let bytes = bytes.leak(); let bytes = bytes.leak();
Content::Static(bytes) Content::Static(bytes)
} else { } else {
let (tx, rx) = std::sync::mpsc::sync_channel(2); 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); let stream = StreamedContent::new(txconfig, rx);
Content::Streamed(stream) Content::Streamed(stream)
} }