diff --git a/.gitignore b/.gitignore index cdd93e9..af7e6a5 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,6 @@ migrate_working_dir/ /build/ /coverage/ pubspec.lock + +# Runtime output +output.ppm \ No newline at end of file diff --git a/packages/sane/example/main.dart b/packages/sane/example/main.dart index d93c5a7..6805c99 100644 --- a/packages/sane/example/main.dart +++ b/packages/sane/example/main.dart @@ -5,6 +5,8 @@ import 'dart:typed_data'; import 'package:logging/logging.dart'; import 'package:sane/sane.dart'; +import 'package:sane/src/impl/sane_mock.dart'; +import 'package:sane/src/impl/sane_native.dart'; void main(List args) async { Logger.root.level = Level.ALL; @@ -12,10 +14,11 @@ void main(List args) async { print('${record.level.name}: ${record.time}: ${record.message}'); }); - final sane = SaneIsolate(sane: SaneDev()); - await sane.spawn(); + final sane = NativeSane(MockSane()); - await sane.init(); + final version = await sane.initialize(); + + print(version); final devices = await sane.getDevices(localOnly: false); for (final device in devices) { @@ -26,40 +29,36 @@ void main(List args) async { return; } - final handle = await sane.openDevice(devices.first); + final device = devices.first; - final optionDescriptors = await sane.getAllOptionDescriptors(handle); + final optionDescriptors = await device.getAllOptionDescriptors(); for (final optionDescriptor in optionDescriptors) { if (optionDescriptor.name == 'mode') { - await sane.controlStringOption( - handle: handle, - index: optionDescriptor.index, - action: SaneAction.setValue, - value: 'Color', + await device.controlStringOption( + optionDescriptor.index, + SaneAction.setValue, + 'Color', ); break; } } - await sane.start(handle); + await device.start(); - final parameters = await sane.getParameters(handle); + final parameters = await device.getParameters(); print('Parameters: format(${parameters.format}), depth(${parameters.depth})'); final rawPixelDataList = []; Uint8List? bytes; while (true) { - bytes = await sane.read(handle, parameters.bytesPerLine); + bytes = await device.read(bufferSize: parameters.bytesPerLine); if (bytes.isEmpty) break; rawPixelDataList.add(bytes); } - await sane.cancel(handle); - await sane.close(handle); - await sane.exit(); - - sane.kill(); + await device.cancel(); + await device.close(); Uint8List mergeUint8Lists(List lists) { final totalLength = lists.fold(0, (length, list) => length + list.length); @@ -80,4 +79,6 @@ void main(List args) async { ); final rawPixelData = mergeUint8Lists(rawPixelDataList); file.writeAsBytesSync(rawPixelData, mode: FileMode.append); + + await sane.dispose(); } diff --git a/packages/sane/lib/sane.dart b/packages/sane/lib/sane.dart index 7769469..decfc3f 100644 --- a/packages/sane/lib/sane.dart +++ b/packages/sane/lib/sane.dart @@ -2,7 +2,4 @@ library; export 'src/exceptions.dart'; export 'src/sane.dart'; -export 'src/sane_dev.dart'; -export 'src/sane_isolate.dart'; export 'src/structures.dart'; -export 'src/utils.dart'; diff --git a/packages/sane/lib/src/bindings.g.dart b/packages/sane/lib/src/bindings.g.dart index c25efa9..6adf3ff 100644 --- a/packages/sane/lib/src/bindings.g.dart +++ b/packages/sane/lib/src/bindings.g.dart @@ -250,6 +250,17 @@ class LibSane { 'sane_strstatus'); late final _sane_strstatus = _sane_strstatusPtr.asFunction(); + + late final addresses = _SymbolAddresses(this); +} + +class _SymbolAddresses { + final LibSane _library; + + _SymbolAddresses(this._library); + + ffi.Pointer> + get sane_close => _library._sane_closePtr; } enum SANE_Status { diff --git a/packages/sane/lib/src/extensions.dart b/packages/sane/lib/src/extensions.dart index 78d9c8d..6f7846c 100644 --- a/packages/sane/lib/src/extensions.dart +++ b/packages/sane/lib/src/extensions.dart @@ -1,7 +1,9 @@ import 'package:logging/logging.dart'; +import 'package:meta/meta.dart'; import 'package:sane/src/bindings.g.dart'; import 'package:sane/src/exceptions.dart'; +@internal extension SaneStatusExtension on SANE_Status { /// Throws [SaneException] if the status is not [SANE_Status.STATUS_GOOD]. @pragma('vm:prefer-inline') @@ -12,6 +14,7 @@ extension SaneStatusExtension on SANE_Status { } } +@internal extension LoggerExtension on Logger { void redirect(LogRecord record) { log( diff --git a/packages/sane/lib/src/impl/sane_mock.dart b/packages/sane/lib/src/impl/sane_mock.dart new file mode 100644 index 0000000..6e887e1 --- /dev/null +++ b/packages/sane/lib/src/impl/sane_mock.dart @@ -0,0 +1,191 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:logging/logging.dart'; +import 'package:sane/sane.dart'; + +final _logger = Logger('sane.mock'); + +class MockSane implements Sane { + @override + void dispose() => _logger.finest('disposed'); + + @override + Future> getDevices({required bool localOnly}) { + _logger.finest('sane_get_devices()'); + return Future.delayed( + const Duration(seconds: 1), + () => List.generate(3, SaneDevDevice.new), + ); + } + + @override + SaneVersion initialize([AuthCallback? authCallback]) { + _logger.finest('initialized'); + return const SaneVersion.fromCode(13371337); + } +} + +class SaneDevDevice implements SaneDevice { + const SaneDevDevice(this.index); + + final int index; + + @override + Future cancel() { + _logger.finest('sane_cancel()'); + return Future.delayed(const Duration(seconds: 1)); + } + + @override + Future close() { + _logger.finest('sane_close()'); + return Future.delayed(const Duration(seconds: 1)); + } + + @override + String get model => 'Model $index'; + + @override + String get name => 'Name $index'; + + @override + Future read({required int bufferSize}) { + _logger.finest('sane_read()'); + return Future.delayed( + const Duration(seconds: 1), + () => Uint8List.fromList([]), + ); + } + + @override + Future start() { + _logger.finest('sane_start()'); + return Future.delayed(const Duration(seconds: 1)); + } + + @override + String get type => 'Type $index'; + + @override + String? get vendor => 'Vendor $index'; + + @override + Future getOptionDescriptor( + int index, + ) { + _logger.finest('sane_getOptionDescriptor()'); + return Future.delayed( + const Duration(seconds: 1), + () => SaneOptionDescriptor( + index: index, + name: 'name', + title: 'title', + desc: 'desc', + type: SaneOptionValueType.int, + unit: SaneOptionUnit.none, + size: 1, + capabilities: [], + constraint: null, + ), + ); + } + + @override + Future getParameters() { + _logger.finest('sane_getParameters()'); + return Future.delayed( + const Duration(seconds: 1), + () => SaneParameters( + format: SaneFrameFormat.gray, + lastFrame: true, + bytesPerLine: 800, + pixelsPerLine: 100, + lines: 100, + depth: 8, + ), + ); + } + + @override + Future> getAllOptionDescriptors() { + _logger.finest('sane_getAllOptionDescriptors()'); + return Future.delayed( + const Duration(seconds: 1), + () => [ + SaneOptionDescriptor( + index: 0, + name: 'name', + title: 'title', + desc: 'desc', + type: SaneOptionValueType.int, + unit: SaneOptionUnit.none, + size: 1, + capabilities: [], + constraint: null, + ), + ], + ); + } + + @override + Future> controlBoolOption( + int index, + SaneAction action, [ + bool? value, + ]) { + _logger.finest('sane_controlBoolOption()'); + return Future.delayed( + const Duration(seconds: 1), + () => SaneOptionResult(result: value ?? true, infos: []), + ); + } + + @override + Future> controlButtonOption(int index) { + _logger.finest('sane_controlButtonOption()'); + return Future.delayed( + const Duration(seconds: 1), + () => SaneOptionResult(result: null, infos: []), + ); + } + + @override + Future> controlFixedOption( + int index, + SaneAction action, [ + double? value, + ]) { + _logger.finest('sane_controlFixedOption()'); + return Future.delayed( + const Duration(seconds: 1), + () => SaneOptionResult(result: value ?? .1, infos: []), + ); + } + + @override + Future> controlIntOption( + int index, + SaneAction action, [ + int? value, + ]) { + _logger.finest('sane_controlIntOption()'); + return Future.delayed( + const Duration(seconds: 1), + () => SaneOptionResult(result: value ?? 1, infos: []), + ); + } + + @override + Future> controlStringOption( + int index, + SaneAction action, [ + String? value, + ]) { + _logger.finest('sane_controlStringOption()'); + return Future.delayed( + const Duration(seconds: 1), + () => SaneOptionResult(result: value ?? 'value', infos: []), + ); + } +} diff --git a/packages/sane/lib/src/impl/sane_native.dart b/packages/sane/lib/src/impl/sane_native.dart new file mode 100644 index 0000000..c8fa160 --- /dev/null +++ b/packages/sane/lib/src/impl/sane_native.dart @@ -0,0 +1,293 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:sane/src/exceptions.dart'; +import 'package:sane/src/impl/sane_sync.dart'; +import 'package:sane/src/isolate.dart'; +import 'package:sane/src/isolate_messages/cancel.dart'; +import 'package:sane/src/isolate_messages/close.dart'; +import 'package:sane/src/isolate_messages/control_button_option.dart'; +import 'package:sane/src/isolate_messages/control_option.dart'; +import 'package:sane/src/isolate_messages/exit.dart'; +import 'package:sane/src/isolate_messages/get_all_option_descriptors.dart'; +import 'package:sane/src/isolate_messages/get_devices.dart'; +import 'package:sane/src/isolate_messages/get_option_descriptor.dart'; +import 'package:sane/src/isolate_messages/get_parameters.dart'; +import 'package:sane/src/isolate_messages/init.dart'; +import 'package:sane/src/isolate_messages/read.dart'; +import 'package:sane/src/isolate_messages/start.dart'; +import 'package:sane/src/sane.dart'; +import 'package:sane/src/structures.dart'; + +class NativeSane implements Sane { + /// Instantiates or returns a [NativeSane] instance. + /// + /// If no [backingSane] is provided, a [SyncSane] instance will be used. + /// + /// No multiple [NativeSane] instances with different backing SANEs can exist + /// at the same time. Call [dispose] to dispose the current instance. + factory NativeSane([Sane? backingSane]) { + // This would cause `StackOverflowError`s + if (backingSane is NativeSane) { + throw ArgumentError( + 'NativeSane cannot be created from a NativeSane instance', + 'backingSane', + ); + } + + backingSane ??= SyncSane(); + + // Avoid undefined behavior + final instance = _instance; + if (instance != null) { + if (instance.backingSane.runtimeType != backingSane.runtimeType) { + throw StateError( + 'The existing NativeSane instance must be disposed before ' + 'instantiating a new one with a different backing SANE ' + 'implementation.', + ); + } + + // Reuse instance + return instance; + } + + // Create instance + return _instance = NativeSane._(backingSane); + } + + NativeSane._(this.backingSane); + + final Sane backingSane; + + static NativeSane? _instance; + + bool _disposed = false; + SaneIsolate? _isolate; + + Future _getIsolate() async { + if (_disposed) throw SaneDisposedError(); + return _isolate ??= await SaneIsolate.spawn(backingSane); + } + + @override + Future initialize([AuthCallback? authCallback]) async { + final isolate = await _getIsolate(); + final response = await isolate.sendMessage(InitMessage()); + return response.version; + } + + @override + Future dispose({bool force = false}) async { + final isolate = _isolate; + + if (_disposed) return; + + _disposed = true; + _instance = null; + + if (isolate == null) return; + + if (force) { + isolate.kill(); + } else { + await isolate.sendMessage(ExitMessage()); + } + + _isolate = null; + } + + @override + Future> getDevices({ + required bool localOnly, + }) async { + final isolate = await _getIsolate(); + final response = await isolate.sendMessage( + GetDevicesMessage(localOnly: localOnly), + ); + + return response.devices; + } +} + +class NativeSaneDevice implements SaneDevice { + NativeSaneDevice({ + required NativeSane sane, + required this.name, + required this.type, + required this.vendor, + required this.model, + }) : _sane = sane; + + final NativeSane _sane; + + bool _closed = false; + + @override + final String name; + + @override + final String type; + + @override + final String? vendor; + + @override + final String model; + + @override + Future cancel() async { + if (_closed) return; + + final isolate = _sane._isolate; + + if (isolate == null) return; + + final message = CancelMessage(name); + await isolate.sendMessage(message); + } + + @override + Future close() async { + if (_closed) return; + + _closed = true; + + final isolate = _sane._isolate; + + if (isolate == null) return; + + final message = CloseMessage(name); + await isolate.sendMessage(message); + } + + @override + Future read({required int bufferSize}) async { + final isolate = await _sane._getIsolate(); + final message = ReadMessage(bufferSize: bufferSize, deviceName: name); + final response = await isolate.sendMessage(message); + + return response.bytes; + } + + @override + Future start() async { + final isolate = await _sane._getIsolate(); + final message = StartMessage(name); + await isolate.sendMessage(message); + } + + @override + Future getOptionDescriptor(int index) async { + final isolate = await _sane._getIsolate(); + final message = GetOptionDescriptorMessage(deviceName: name, index: index); + + final GetOptionDescriptorResponse(:optionDescriptor) = + await isolate.sendMessage(message); + + return optionDescriptor; + } + + @override + Future> getAllOptionDescriptors() async { + final isolate = await _sane._getIsolate(); + final message = GetAllOptionDescriptorsMessage(name); + + final GetAllOptionDescriptorsResponse(:optionDescriptors) = + await isolate.sendMessage(message); + + return optionDescriptors; + } + + @override + Future> controlBoolOption( + int index, + SaneAction action, + bool? value, + ) async { + final isolate = await _sane._getIsolate(); + final message = ControlValueOptionMessage( + deviceName: name, + index: index, + action: action, + value: value, + ); + final response = await isolate.sendMessage(message); + + return response.result; + } + + @override + Future> controlIntOption( + int index, + SaneAction action, + int? value, + ) async { + final isolate = await _sane._getIsolate(); + final message = ControlValueOptionMessage( + deviceName: name, + index: index, + action: action, + value: value, + ); + final response = await isolate.sendMessage(message); + + return response.result; + } + + @override + Future> controlFixedOption( + int index, + SaneAction action, + double? value, + ) async { + final isolate = await _sane._getIsolate(); + final message = ControlValueOptionMessage( + deviceName: name, + index: index, + action: action, + value: value, + ); + final response = await isolate.sendMessage(message); + + return response.result; + } + + @override + Future> controlStringOption( + int index, + SaneAction action, + String? value, + ) async { + final isolate = await _sane._getIsolate(); + final message = ControlValueOptionMessage( + deviceName: name, + index: index, + action: action, + value: value, + ); + final response = await isolate.sendMessage(message); + + return response.result; + } + + @override + Future> controlButtonOption(int index) async { + final isolate = await _sane._getIsolate(); + final message = ControlButtonOptionMessage( + deviceName: name, + index: index, + ); + final response = await isolate.sendMessage(message); + + return response.result; + } + + @override + Future getParameters() async { + final isolate = await _sane._getIsolate(); + final message = GetParametersMessage(name); + final response = await isolate.sendMessage(message); + return response.parameters; + } +} diff --git a/packages/sane/lib/src/impl/sane_sync.dart b/packages/sane/lib/src/impl/sane_sync.dart new file mode 100644 index 0000000..83998d9 --- /dev/null +++ b/packages/sane/lib/src/impl/sane_sync.dart @@ -0,0 +1,471 @@ +import 'dart:async'; +import 'dart:ffi' as ffi; +import 'dart:ffi'; +import 'dart:typed_data'; + +import 'package:ffi/ffi.dart' as ffi; +import 'package:sane/sane.dart'; +import 'package:sane/src/bindings.g.dart'; +import 'package:sane/src/dylib.dart'; +import 'package:sane/src/extensions.dart'; +import 'package:sane/src/logger.dart'; +import 'package:sane/src/type_conversion.dart'; + +class SyncSane implements Sane { + factory SyncSane() => _instance ??= SyncSane._(); + + SyncSane._(); + + static SyncSane? _instance; + bool _disposed = false; + + @override + SaneVersion initialize([AuthCallback? authCallback]) { + _checkIfDisposed(); + + void authCallbackAdapter( + SANE_String_Const resource, + ffi.Pointer username, + ffi.Pointer password, + ) { + final credentials = authCallback!(resource.toDartString()); + for (var i = 0; + i < credentials.username.length && i < SANE_MAX_USERNAME_LEN; + i++) { + username[i] = credentials.username.codeUnitAt(i); + } + for (var i = 0; + i < credentials.password.length && i < SANE_MAX_PASSWORD_LEN; + i++) { + password[i] = credentials.password.codeUnitAt(i); + } + } + + final versionCodePointer = ffi.calloc(); + final nativeAuthCallback = authCallback != null + ? ffi.NativeCallable.isolateLocal( + authCallbackAdapter, + ).nativeFunction + : ffi.nullptr; + try { + final status = dylib.sane_init(versionCodePointer, nativeAuthCallback); + + logger.finest('sane_init() -> ${status.name}'); + + status.check(); + + final versionCode = versionCodePointer.value; + + final version = SaneVersion.fromCode(versionCode); + + logger.finest('SANE version: $version'); + + return version; + } finally { + ffi.calloc.free(versionCodePointer); + ffi.calloc.free(nativeAuthCallback); + } + } + + @override + Future dispose() { + if (_disposed) return Future.value(); + + final completer = Completer(); + + Future(() { + _disposed = true; + + dylib.sane_exit(); + logger.finest('sane_exit()'); + + completer.complete(); + + _instance = null; + }); + + return completer.future; + } + + @override + List getDevices({required bool localOnly}) { + _checkIfDisposed(); + + final deviceListPointer = + ffi.calloc>>(); + + try { + final status = dylib.sane_get_devices( + deviceListPointer, + localOnly.asSaneBool, + ); + + logger.finest('sane_get_devices() -> ${status.name}'); + + status.check(); + + final devices = []; + + for (var i = 0; deviceListPointer.value[i] != ffi.nullptr; i++) { + final device = deviceListPointer.value[i].ref; + devices.add(SyncSaneDevice(device)); + } + + return List.unmodifiable(devices); + } finally { + ffi.calloc.free(deviceListPointer); + } + } + + @pragma('vm:prefer-inline') + void _checkIfDisposed() { + if (_disposed) throw SaneDisposedError(); + } +} + +class SyncSaneDevice implements SaneDevice, ffi.Finalizable { + factory SyncSaneDevice(SANE_Device device) { + final vendor = device.vendor.toDartString(); + return SyncSaneDevice._( + name: device.name.toDartString(), + vendor: vendor == 'Noname' ? null : vendor, + type: device.type.toDartString(), + model: device.model.toDartString(), + ); + } + + SyncSaneDevice._({ + required this.name, + required this.vendor, + required this.model, + required this.type, + }); + + static final _finalizer = ffi.NativeFinalizer(dylib.addresses.sane_close); + + SANE_Handle? _handle; + + bool _closed = false; + + @override + final String name; + + @override + final String type; + + @override + final String? vendor; + + @override + final String model; + + @override + void cancel() { + _checkIfDisposed(); + + final handle = _handle; + + if (handle == null) return; + + dylib.sane_cancel(handle); + } + + SANE_Handle _open() { + final namePointer = name.toSaneString(); + final handlePointer = ffi.calloc.allocate( + ffi.sizeOf(), + ); + + try { + dylib.sane_open(namePointer, handlePointer).check(); + final handle = handlePointer.value; + _finalizer.attach(this, handle); + return handle; + } finally { + ffi.calloc.free(namePointer); + ffi.calloc.free(handlePointer); + } + } + + @override + void close() { + if (_closed) return; + + _closed = true; + + if (_handle == null) return; + + _finalizer.detach(this); + dylib.sane_close(_handle!); + } + + @override + Uint8List read({required int bufferSize}) { + _checkIfDisposed(); + + final handle = _handle ??= _open(); + + final lengthPointer = ffi.calloc(); + final bufferPointer = ffi.calloc(bufferSize); + + try { + dylib.sane_read(handle, bufferPointer, bufferSize, lengthPointer).check(); + + logger.finest('sane_read()'); + + final length = lengthPointer.value; + final buffer = bufferPointer.cast().asTypedList(length); + + return buffer; + } finally { + ffi.calloc.free(lengthPointer); + ffi.calloc.free(bufferPointer); + } + } + + @override + void start() { + _checkIfDisposed(); + + final handle = _handle ??= _open(); + + dylib.sane_start(handle).check(); + } + + @pragma('vm:prefer-inline') + void _checkIfDisposed() { + if (_closed) throw SaneDisposedError(); + } + + @override + SaneOptionDescriptor getOptionDescriptor(int index) { + _checkIfDisposed(); + + final handle = _handle ??= _open(); + + final optionDescriptorPointer = + dylib.sane_get_option_descriptor(handle, index); + + try { + return saneOptionDescriptorFromNative( + optionDescriptorPointer.ref, + index, + ); + } finally { + ffi.calloc.free(optionDescriptorPointer); + } + } + + @override + List getAllOptionDescriptors() { + _checkIfDisposed(); + + final handle = _handle ??= _open(); + + final optionDescriptors = []; + + for (var i = 0; true; i++) { + final descriptorPointer = dylib.sane_get_option_descriptor(handle, i); + try { + if (descriptorPointer == ffi.nullptr) break; + optionDescriptors.add( + saneOptionDescriptorFromNative(descriptorPointer.ref, i), + ); + } finally { + ffi.calloc.free(descriptorPointer); + } + } + + return optionDescriptors; + } + + SaneOptionResult _controlOption({ + required int index, + required SaneAction action, + T? value, + }) { + _checkIfDisposed(); + + final handle = _handle ??= _open(); + + final optionDescriptor = saneOptionDescriptorFromNative( + dylib.sane_get_option_descriptor(handle, index).ref, + index, + ); + final optionType = optionDescriptor.type; + final optionSize = optionDescriptor.size; + + final infoPointer = ffi.calloc(); + + ffi.Pointer allocateOptionValue() { + return switch (optionType) { + SaneOptionValueType.bool => ffi.calloc(optionSize), + SaneOptionValueType.int => ffi.calloc(optionSize), + SaneOptionValueType.fixed => ffi.calloc(optionSize), + SaneOptionValueType.string => ffi.calloc(optionSize), + SaneOptionValueType.button => ffi.nullptr, + SaneOptionValueType.group => throw const SaneInvalidDataException(), + }; + } + + final valuePointer = allocateOptionValue(); + + if (action == SaneAction.setValue) { + switch (optionType) { + case SaneOptionValueType.bool when value is bool: + (valuePointer as ffi.Pointer).value = value.asSaneBool; + break; + + case SaneOptionValueType.int when value is int: + (valuePointer as ffi.Pointer).value = value; + break; + + case SaneOptionValueType.fixed when value is double: + (valuePointer as ffi.Pointer).value = + doubleToSaneFixed(value); + break; + + case SaneOptionValueType.string when value is String: + (valuePointer as ffi.Pointer).value = + value.toSaneString(); + break; + + case SaneOptionValueType.button: + break; + + case SaneOptionValueType.group: + default: + throw const SaneInvalidDataException(); + } + } + + final status = dylib.sane_control_option( + handle, + index, + nativeSaneActionFromDart(action), + valuePointer.cast(), + infoPointer, + ); + logger.finest( + 'sane_control_option($index, $action, $value) -> ${status.name}', + ); + + status.check(); + + final infos = saneOptionInfoFromNative(infoPointer.value); + late final dynamic result; + switch (optionType) { + case SaneOptionValueType.bool: + result = dartBoolFromSaneBool( + (valuePointer as ffi.Pointer).value, + ); + + case SaneOptionValueType.int: + result = (valuePointer as ffi.Pointer).value; + + case SaneOptionValueType.fixed: + result = + saneFixedToDouble((valuePointer as ffi.Pointer).value); + + case SaneOptionValueType.string: + result = (valuePointer as ffi.Pointer).toDartString(); + + case SaneOptionValueType.button: + result = null; + + default: + throw const SaneInvalidDataException(); + } + + ffi.calloc.free(valuePointer); + ffi.calloc.free(infoPointer); + + return SaneOptionResult( + result: result, + infos: infos, + ); + } + + @override + SaneOptionResult controlBoolOption( + int index, + SaneAction action, + bool? value, + ) { + return _controlOption( + index: index, + action: action, + value: value, + ); + } + + @override + SaneOptionResult controlIntOption( + int index, + SaneAction action, + int? value, + ) { + return _controlOption( + index: index, + action: action, + value: value, + ); + } + + @override + SaneOptionResult controlFixedOption( + int index, + SaneAction action, + double? value, + ) { + return _controlOption( + index: index, + action: action, + value: value, + ); + } + + @override + SaneOptionResult controlStringOption( + int index, + SaneAction action, + String? value, + ) { + return _controlOption( + index: index, + action: action, + value: value, + ); + } + + @override + SaneOptionResult controlButtonOption(int index) { + return _controlOption( + index: index, + action: SaneAction.setValue, + value: null, + ); + } + + @override + SaneParameters getParameters() { + _checkIfDisposed(); + + final handle = _handle ??= _open(); + final nativeParametersPointer = ffi.calloc(); + + try { + final status = dylib.sane_get_parameters( + handle, + nativeParametersPointer, + ); + logger.finest('sane_get_parameters() -> ${status.name}'); + + status.check(); + + return saneParametersFromNative(nativeParametersPointer.ref); + } finally { + ffi.calloc.free(nativeParametersPointer); + } + } +} diff --git a/packages/sane/lib/src/isolate.dart b/packages/sane/lib/src/isolate.dart new file mode 100644 index 0000000..609d9af --- /dev/null +++ b/packages/sane/lib/src/isolate.dart @@ -0,0 +1,134 @@ +import 'dart:async'; +import 'dart:isolate'; + +import 'package:logging/logging.dart'; +import 'package:meta/meta.dart'; +import 'package:sane/src/exceptions.dart'; +import 'package:sane/src/extensions.dart'; +import 'package:sane/src/isolate_messages/exception.dart'; +import 'package:sane/src/isolate_messages/exit.dart'; +import 'package:sane/src/isolate_messages/interface.dart'; +import 'package:sane/src/sane.dart'; + +final _logger = Logger('sane.isolate'); + +@internal +class SaneIsolate { + SaneIsolate._( + this._isolate, + this._sendPort, + ); + + final Isolate _isolate; + final SendPort _sendPort; + + static Future spawn(Sane sane) async { + final receivePort = ReceivePort(); + + final isolate = await Isolate.spawn( + _entryPoint, + (receivePort.sendPort, sane), + onExit: receivePort.sendPort, + ); + + final sendPortCompleter = Completer(); + receivePort.listen((message) { + switch (message) { + case SendPort(): + sendPortCompleter.complete(message); + case LogRecord(): + _logger.redirect(message); + case null: + receivePort.close(); + } + }); + + final sendPort = await sendPortCompleter.future; + return SaneIsolate._(isolate, sendPort); + } + + void kill() => _isolate.kill(priority: Isolate.immediate); + + Future sendMessage( + IsolateMessage message, + ) async { + final replyPort = ReceivePort(); + + _sendPort.send( + _IsolateMessageEnvelope( + replyPort: replyPort.sendPort, + message: message, + ), + ); + + final response = await replyPort.first; + replyPort.close(); + + if (response is ExceptionResponse) { + Error.throwWithStackTrace( + response.exception, + response.stackTrace, + ); + } + + return response as T; + } +} + +typedef _EntryPointArgs = (SendPort sendPort, Sane sane); + +void _entryPoint(_EntryPointArgs args) { + final (sendPort, sane) = args; + + Logger.root.level = Level.ALL; + Logger.root.onRecord.forEach(sendPort.send); + + final receivePort = ReceivePort(); + sendPort.send(receivePort.sendPort); + + late StreamSubscription<_IsolateMessageEnvelope> subscription; + + subscription = + receivePort.cast<_IsolateMessageEnvelope>().listen((envelope) async { + final _IsolateMessageEnvelope(:message, :replyPort) = envelope; + + IsolateResponse response; + + try { + response = await message.handle(sane); + } on SaneException catch (exception, stackTrace) { + response = ExceptionResponse( + exception: exception, + stackTrace: stackTrace, + ); + } + + replyPort.send(response); + + if (message is ExitMessage) { + await subscription.cancel(); + } + }); +} + +class _IsolateMessageEnvelope { + _IsolateMessageEnvelope({ + required this.replyPort, + required this.message, + }); + + final SendPort replyPort; + final IsolateMessage message; +} + +late Map _devices; + +@internal +SaneDevice getDevice(String name) => _devices[name]!; + +@internal +void setDevices(Iterable devices) { + _devices = { + for (final device in devices) device.name: device, + }; +} diff --git a/packages/sane/lib/src/isolate_messages/cancel.dart b/packages/sane/lib/src/isolate_messages/cancel.dart index 4b5d65b..5068008 100644 --- a/packages/sane/lib/src/isolate_messages/cancel.dart +++ b/packages/sane/lib/src/isolate_messages/cancel.dart @@ -1,15 +1,15 @@ +import 'package:sane/src/isolate.dart'; import 'package:sane/src/isolate_messages/interface.dart'; import 'package:sane/src/sane.dart'; -import 'package:sane/src/structures.dart'; class CancelMessage implements IsolateMessage { - CancelMessage({required this.saneHandle}); + CancelMessage(this.deviceName); - final SaneHandle saneHandle; + final String deviceName; @override Future handle(Sane sane) async { - await sane.cancel(saneHandle); + await getDevice(deviceName).cancel(); return CancelResponse(); } } diff --git a/packages/sane/lib/src/isolate_messages/close.dart b/packages/sane/lib/src/isolate_messages/close.dart index 028f854..5109f5c 100644 --- a/packages/sane/lib/src/isolate_messages/close.dart +++ b/packages/sane/lib/src/isolate_messages/close.dart @@ -1,14 +1,15 @@ import 'package:sane/sane.dart'; +import 'package:sane/src/isolate.dart'; import 'package:sane/src/isolate_messages/interface.dart'; class CloseMessage implements IsolateMessage { - CloseMessage({required this.saneHandle}); + const CloseMessage(this.deviceName); - final SaneHandle saneHandle; + final String deviceName; @override Future handle(Sane sane) async { - await sane.close(saneHandle); + await getDevice(deviceName).close(); return CloseResponse(); } } diff --git a/packages/sane/lib/src/isolate_messages/control_button_option.dart b/packages/sane/lib/src/isolate_messages/control_button_option.dart index 8176f48..7cee0cd 100644 --- a/packages/sane/lib/src/isolate_messages/control_button_option.dart +++ b/packages/sane/lib/src/isolate_messages/control_button_option.dart @@ -1,30 +1,28 @@ +import 'package:sane/src/isolate.dart'; import 'package:sane/src/isolate_messages/interface.dart'; import 'package:sane/src/sane.dart'; import 'package:sane/src/structures.dart'; class ControlButtonOptionMessage implements IsolateMessage { - ControlButtonOptionMessage({ - required this.saneHandle, + const ControlButtonOptionMessage({ + required this.deviceName, required this.index, }); - final SaneHandle saneHandle; + final String deviceName; final int index; @override Future handle(Sane sane) async { - return ControlButtonOptionResponse( - result: await sane.controlButtonOption( - handle: saneHandle, - index: index, - ), - ); + final device = getDevice(deviceName); + final result = await device.controlButtonOption(index); + return ControlButtonOptionResponse(result); } } class ControlButtonOptionResponse implements IsolateResponse { - ControlButtonOptionResponse({required this.result}); + ControlButtonOptionResponse(this.result); final SaneOptionResult result; } diff --git a/packages/sane/lib/src/isolate_messages/control_option.dart b/packages/sane/lib/src/isolate_messages/control_option.dart index 99f6942..1804ff7 100644 --- a/packages/sane/lib/src/isolate_messages/control_option.dart +++ b/packages/sane/lib/src/isolate_messages/control_option.dart @@ -1,60 +1,47 @@ +import 'package:sane/src/isolate.dart'; import 'package:sane/src/isolate_messages/interface.dart'; import 'package:sane/src/sane.dart'; import 'package:sane/src/structures.dart'; class ControlValueOptionMessage implements IsolateMessage> { - ControlValueOptionMessage({ - required this.saneHandle, + const ControlValueOptionMessage({ + required this.deviceName, required this.index, required this.action, this.value, }); - final SaneHandle saneHandle; + final String deviceName; final int index; final SaneAction action; final T? value; @override Future> handle(Sane sane) async { + final device = getDevice(deviceName); + switch (value) { case final bool value: - return ControlValueOptionResponse( - result: await sane.controlBoolOption( - handle: saneHandle, - index: index, - action: action, - value: value, - ), - ) as ControlValueOptionResponse; + final result = await device.controlBoolOption(index, action, value); + return ControlValueOptionResponse(result) + as ControlValueOptionResponse; + case final int value: - return ControlValueOptionResponse( - result: await sane.controlIntOption( - handle: saneHandle, - index: index, - action: action, - value: value, - ), - ) as ControlValueOptionResponse; + final result = await device.controlIntOption(index, action, value); + return ControlValueOptionResponse(result) + as ControlValueOptionResponse; + case final double value: - return ControlValueOptionResponse( - result: await sane.controlFixedOption( - handle: saneHandle, - index: index, - action: action, - value: value, - ), - ) as ControlValueOptionResponse; + final result = await device.controlFixedOption(index, action, value); + return ControlValueOptionResponse(result) + as ControlValueOptionResponse; + case final String value: - return ControlValueOptionResponse( - result: await sane.controlStringOption( - handle: saneHandle, - index: index, - action: action, - value: value, - ), - ) as ControlValueOptionResponse; + final result = await device.controlStringOption(index, action, value); + return ControlValueOptionResponse(result) + as ControlValueOptionResponse; + default: throw Exception('Invalid value type.'); } @@ -62,7 +49,7 @@ class ControlValueOptionMessage } class ControlValueOptionResponse implements IsolateResponse { - ControlValueOptionResponse({required this.result}); + ControlValueOptionResponse(this.result); final SaneOptionResult result; } diff --git a/packages/sane/lib/src/isolate_messages/exit.dart b/packages/sane/lib/src/isolate_messages/exit.dart index 0e797e2..ea793c2 100644 --- a/packages/sane/lib/src/isolate_messages/exit.dart +++ b/packages/sane/lib/src/isolate_messages/exit.dart @@ -4,7 +4,7 @@ import 'package:sane/src/sane.dart'; class ExitMessage implements IsolateMessage { @override Future handle(Sane sane) async { - await sane.exit(); + sane.dispose(); return ExitResponse(); } } diff --git a/packages/sane/lib/src/isolate_messages/get_all_option_descriptors.dart b/packages/sane/lib/src/isolate_messages/get_all_option_descriptors.dart index 8412f13..c50d8e8 100644 --- a/packages/sane/lib/src/isolate_messages/get_all_option_descriptors.dart +++ b/packages/sane/lib/src/isolate_messages/get_all_option_descriptors.dart @@ -1,23 +1,24 @@ +import 'package:sane/src/isolate.dart'; import 'package:sane/src/isolate_messages/interface.dart'; import 'package:sane/src/sane.dart'; import 'package:sane/src/structures.dart'; class GetAllOptionDescriptorsMessage implements IsolateMessage { - GetAllOptionDescriptorsMessage({required this.saneHandle}); + GetAllOptionDescriptorsMessage(this.deviceName); - final SaneHandle saneHandle; + final String deviceName; @override Future handle(Sane sane) async { - return GetAllOptionDescriptorsResponse( - optionDescriptors: await sane.getAllOptionDescriptors(saneHandle), - ); + final device = getDevice(deviceName); + final optionDescriptors = await device.getAllOptionDescriptors(); + return GetAllOptionDescriptorsResponse(optionDescriptors); } } class GetAllOptionDescriptorsResponse implements IsolateResponse { - GetAllOptionDescriptorsResponse({required this.optionDescriptors}); + GetAllOptionDescriptorsResponse(this.optionDescriptors); final List optionDescriptors; } diff --git a/packages/sane/lib/src/isolate_messages/get_devices.dart b/packages/sane/lib/src/isolate_messages/get_devices.dart index d54bf17..ed6ef37 100644 --- a/packages/sane/lib/src/isolate_messages/get_devices.dart +++ b/packages/sane/lib/src/isolate_messages/get_devices.dart @@ -1,6 +1,6 @@ +import 'package:sane/src/isolate.dart'; import 'package:sane/src/isolate_messages/interface.dart'; import 'package:sane/src/sane.dart'; -import 'package:sane/src/structures.dart'; class GetDevicesMessage implements IsolateMessage { GetDevicesMessage({required this.localOnly}); @@ -9,14 +9,14 @@ class GetDevicesMessage implements IsolateMessage { @override Future handle(Sane sane) async { - return GetDevicesResponse( - devices: await sane.getDevices(localOnly: localOnly), - ); + final devices = await sane.getDevices(localOnly: localOnly); + setDevices(devices); + return GetDevicesResponse(devices); } } class GetDevicesResponse implements IsolateResponse { - GetDevicesResponse({required this.devices}); + GetDevicesResponse(this.devices); final List devices; } diff --git a/packages/sane/lib/src/isolate_messages/get_option_descriptor.dart b/packages/sane/lib/src/isolate_messages/get_option_descriptor.dart index ea02661..ba84d53 100644 --- a/packages/sane/lib/src/isolate_messages/get_option_descriptor.dart +++ b/packages/sane/lib/src/isolate_messages/get_option_descriptor.dart @@ -1,3 +1,4 @@ +import 'package:sane/src/isolate.dart'; import 'package:sane/src/isolate_messages/interface.dart'; import 'package:sane/src/sane.dart'; import 'package:sane/src/structures.dart'; @@ -5,18 +6,18 @@ import 'package:sane/src/structures.dart'; class GetOptionDescriptorMessage implements IsolateMessage { GetOptionDescriptorMessage({ - required this.saneHandle, + required this.deviceName, required this.index, }); - final SaneHandle saneHandle; + final String deviceName; final int index; @override Future handle(Sane sane) async { - return GetOptionDescriptorResponse( - optionDescriptor: await sane.getOptionDescriptor(saneHandle, index), - ); + final device = getDevice(deviceName); + final optionDescriptor = await device.getOptionDescriptor(index); + return GetOptionDescriptorResponse(optionDescriptor: optionDescriptor); } } diff --git a/packages/sane/lib/src/isolate_messages/get_parameters.dart b/packages/sane/lib/src/isolate_messages/get_parameters.dart index f152386..eedc92e 100644 --- a/packages/sane/lib/src/isolate_messages/get_parameters.dart +++ b/packages/sane/lib/src/isolate_messages/get_parameters.dart @@ -1,22 +1,23 @@ +import 'package:sane/src/isolate.dart'; import 'package:sane/src/isolate_messages/interface.dart'; import 'package:sane/src/sane.dart'; import 'package:sane/src/structures.dart'; class GetParametersMessage implements IsolateMessage { - GetParametersMessage({required this.saneHandle}); + const GetParametersMessage(this.deviceName); - final SaneHandle saneHandle; + final String deviceName; @override Future handle(Sane sane) async { - return GetParametersResponse( - parameters: await sane.getParameters(saneHandle), - ); + final device = getDevice(deviceName); + final parameters = await device.getParameters(); + return GetParametersResponse(parameters); } } class GetParametersResponse implements IsolateResponse { - GetParametersResponse({required this.parameters}); + GetParametersResponse(this.parameters); final SaneParameters parameters; } diff --git a/packages/sane/lib/src/isolate_messages/init.dart b/packages/sane/lib/src/isolate_messages/init.dart index df035db..531d3c0 100644 --- a/packages/sane/lib/src/isolate_messages/init.dart +++ b/packages/sane/lib/src/isolate_messages/init.dart @@ -4,14 +4,13 @@ import 'package:sane/src/sane.dart'; class InitMessage implements IsolateMessage { @override Future handle(Sane sane) async { - return InitResponse( - versionCode: await sane.init(), - ); + final version = await sane.initialize(); + return InitResponse(version); } } class InitResponse implements IsolateResponse { - InitResponse({required this.versionCode}); + InitResponse(this.version); - final int versionCode; + final SaneVersion version; } diff --git a/packages/sane/lib/src/isolate_messages/interface.dart b/packages/sane/lib/src/isolate_messages/interface.dart index 2515622..41fef01 100644 --- a/packages/sane/lib/src/isolate_messages/interface.dart +++ b/packages/sane/lib/src/isolate_messages/interface.dart @@ -1,6 +1,8 @@ -import 'package:sane/src/sane.dart'; +import 'package:sane/sane.dart'; abstract interface class IsolateMessage { + const IsolateMessage(); + Future handle(Sane sane); } diff --git a/packages/sane/lib/src/isolate_messages/open.dart b/packages/sane/lib/src/isolate_messages/open.dart deleted file mode 100644 index 13ca782..0000000 --- a/packages/sane/lib/src/isolate_messages/open.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:sane/src/isolate_messages/interface.dart'; -import 'package:sane/src/sane.dart'; -import 'package:sane/src/structures.dart'; - -class OpenMessage implements IsolateMessage { - OpenMessage({required this.deviceName}); - - final String deviceName; - - @override - Future handle(Sane sane) async { - return OpenResponse( - handle: await sane.open(deviceName), - ); - } -} - -class OpenResponse implements IsolateResponse { - OpenResponse({required this.handle}); - - final SaneHandle handle; -} diff --git a/packages/sane/lib/src/isolate_messages/read.dart b/packages/sane/lib/src/isolate_messages/read.dart index e9ad229..c5a0786 100644 --- a/packages/sane/lib/src/isolate_messages/read.dart +++ b/packages/sane/lib/src/isolate_messages/read.dart @@ -1,30 +1,28 @@ import 'dart:typed_data'; +import 'package:sane/src/isolate.dart'; import 'package:sane/src/isolate_messages/interface.dart'; import 'package:sane/src/sane.dart'; -import 'package:sane/src/structures.dart'; class ReadMessage implements IsolateMessage { - ReadMessage({ - required this.saneHandle, + const ReadMessage({ + required this.deviceName, required this.bufferSize, }); - final SaneHandle saneHandle; + final String deviceName; final int bufferSize; @override Future handle(Sane sane) async { - return ReadResponse( - bytes: await sane.read(saneHandle, bufferSize), - ); + final device = getDevice(deviceName); + final bytes = await device.read(bufferSize: bufferSize); + return ReadResponse(bytes); } } class ReadResponse implements IsolateResponse { - ReadResponse({ - required this.bytes, - }); + ReadResponse(this.bytes); final Uint8List bytes; } diff --git a/packages/sane/lib/src/isolate_messages/set_io_mode.dart b/packages/sane/lib/src/isolate_messages/set_io_mode.dart deleted file mode 100644 index 7478351..0000000 --- a/packages/sane/lib/src/isolate_messages/set_io_mode.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'package:sane/src/isolate_messages/interface.dart'; -import 'package:sane/src/sane.dart'; -import 'package:sane/src/structures.dart'; - -class SetIOModeMessage implements IsolateMessage { - SetIOModeMessage({ - required this.saneHandle, - required this.ioMode, - }); - - final SaneHandle saneHandle; - final SaneIOMode ioMode; - - @override - Future handle(Sane sane) async { - await sane.setIOMode(saneHandle, ioMode); - return SetIOModeResponse(); - } -} - -class SetIOModeResponse implements IsolateResponse {} diff --git a/packages/sane/lib/src/isolate_messages/start.dart b/packages/sane/lib/src/isolate_messages/start.dart index fe961b2..8efbfae 100644 --- a/packages/sane/lib/src/isolate_messages/start.dart +++ b/packages/sane/lib/src/isolate_messages/start.dart @@ -1,15 +1,15 @@ +import 'package:sane/src/isolate.dart'; import 'package:sane/src/isolate_messages/interface.dart'; import 'package:sane/src/sane.dart'; -import 'package:sane/src/structures.dart'; class StartMessage implements IsolateMessage { - StartMessage({required this.saneHandle}); + const StartMessage(this.deviceName); - final SaneHandle saneHandle; + final String deviceName; @override Future handle(Sane sane) async { - await sane.start(saneHandle); + getDevice(deviceName).start(); return StartResponse(); } } diff --git a/packages/sane/lib/src/logger.dart b/packages/sane/lib/src/logger.dart index 7117d01..1a2ca27 100644 --- a/packages/sane/lib/src/logger.dart +++ b/packages/sane/lib/src/logger.dart @@ -1,3 +1,5 @@ import 'package:logging/logging.dart'; +import 'package:meta/meta.dart'; +@internal final logger = Logger('sane'); diff --git a/packages/sane/lib/src/sane.dart b/packages/sane/lib/src/sane.dart index d72532c..05d5452 100644 --- a/packages/sane/lib/src/sane.dart +++ b/packages/sane/lib/src/sane.dart @@ -1,546 +1,210 @@ import 'dart:async'; -import 'dart:ffi' as ffi; import 'dart:typed_data'; -import 'package:ffi/ffi.dart' as ffi; -import 'package:sane/src/bindings.g.dart'; -import 'package:sane/src/dylib.dart'; -import 'package:sane/src/exceptions.dart'; -import 'package:sane/src/extensions.dart'; -import 'package:sane/src/logger.dart'; -import 'package:sane/src/structures.dart'; -import 'package:sane/src/type_conversion.dart'; -import 'package:sane/src/utils.dart'; +import 'package:meta/meta.dart'; +import 'package:sane/sane.dart'; +import 'package:sane/src/impl/sane_mock.dart'; +import 'package:sane/src/impl/sane_native.dart'; +import 'package:sane/src/impl/sane_sync.dart'; typedef AuthCallback = SaneCredentials Function(String resourceName); -class Sane { - factory Sane() => _instance ??= Sane._(); - - Sane._(); - - static Sane? _instance; - bool _exited = false; - final Map _nativeHandles = {}; - - SANE_Handle _getNativeHandle(SaneHandle handle) => _nativeHandles[handle]!; - - Future init({ - AuthCallback? authCallback, - }) { - _checkIfExited(); - - final completer = Completer(); - - void authCallbackAdapter( - SANE_String_Const resource, - ffi.Pointer username, - ffi.Pointer password, - ) { - final credentials = authCallback!(dartStringFromSaneString(resource)!); - for (var i = 0; - i < credentials.username.length && i < SANE_MAX_USERNAME_LEN; - i++) { - username[i] = credentials.username.codeUnitAt(i); - } - for (var i = 0; - i < credentials.password.length && i < SANE_MAX_PASSWORD_LEN; - i++) { - password[i] = credentials.password.codeUnitAt(i); - } - } - - Future(() { - final versionCodePointer = ffi.calloc(); - final nativeAuthCallback = authCallback != null - ? ffi.NativeCallable.isolateLocal( - authCallbackAdapter, - ).nativeFunction - : ffi.nullptr; - final status = dylib.sane_init(versionCodePointer, nativeAuthCallback); - logger.finest('sane_init() -> ${status.name}'); - - status.check(); - - final versionCode = versionCodePointer.value; - logger.finest( - 'SANE version: ${SaneUtils.version(versionCodePointer.value)}', - ); - - ffi.calloc.free(versionCodePointer); - ffi.calloc.free(nativeAuthCallback); - - completer.complete(versionCode); - }); - - return completer.future; - } - - Future exit() { - if (_exited) return Future.value(); - - final completer = Completer(); - - Future(() { - _exited = true; - - dylib.sane_exit(); - logger.finest('sane_exit()'); - - completer.complete(); - - _instance = null; - }); - - return completer.future; - } - - Future> getDevices({ - required bool localOnly, - }) { - _checkIfExited(); - - final completer = Completer>(); - - Future(() { - final deviceListPointer = - ffi.calloc>>(); - final status = dylib.sane_get_devices( - deviceListPointer, - saneBoolFromDartBool(localOnly), - ); - - logger.finest('sane_get_devices() -> ${status.name}'); - - status.check(); - - final devices = []; - for (var i = 0; deviceListPointer.value[i] != ffi.nullptr; i++) { - final nativeDevice = deviceListPointer.value[i].ref; - devices.add(saneDeviceFromNative(nativeDevice)); - } - - ffi.calloc.free(deviceListPointer); - - completer.complete(devices); - }); - - return completer.future; - } - - Future open(String deviceName) { - _checkIfExited(); - - final completer = Completer(); - - Future(() { - final nativeHandlePointer = ffi.calloc(); - final deviceNamePointer = saneStringFromDartString(deviceName); - final status = dylib.sane_open(deviceNamePointer, nativeHandlePointer); - logger.finest('sane_open() -> ${status.name}'); - - status.check(); - - final handle = SaneHandle(deviceName: deviceName); - _nativeHandles.addAll({ - handle: nativeHandlePointer.value, - }); - - ffi.calloc.free(nativeHandlePointer); - ffi.calloc.free(deviceNamePointer); - - completer.complete(handle); - }); +abstract interface class Sane { + /// Instantiates a new asynchronous SANE instance. + /// + /// See also: + /// + /// - [Sane.sync] + factory Sane([Sane? backingSane]) => NativeSane(backingSane); + + /// Instantiates a new synchronous SANE instance. + factory Sane.sync() => SyncSane(); + + /// Instantiates a mock SANE instance for testing. + factory Sane.mock() => MockSane(); + + /// Disposes the SANE instance. + /// + /// Closes all device handles and all future calls are invalid. + /// + /// See also: + /// + /// - [`sane_exit`](https://sane-project.gitlab.io/standard/api.html#sane-exit) + void dispose(); + + /// Initializes the SANE library. + FutureOr initialize([AuthCallback? authCallback]); + + /// Queries the list of devices that are available. + /// + /// This method can be called repeatedly to detect when new devices become + /// available. If argument [localOnly] is true, only local devices are + /// returned (devices directly attached to the machine that SANE is running + /// on). If it is `false`, the device list includes all remote devices that + /// are accessible to the SANE library. + /// + /// See also: + /// + /// - [`sane_get_devices`](https://sane-project.gitlab.io/standard/api.html#sane-get-devices) + FutureOr> getDevices({required bool localOnly}); +} - return completer.future; - } +// TODO(Craftplacer): Turn SaneVersion into an extension type, once available. +@immutable +class SaneVersion { + const SaneVersion.fromCode(this.code); - Future openDevice(SaneDevice device) { - _checkIfExited(); + final int code; - return open(device.name); - } + int get major => (code >> 24) & 0xff; - Future close(SaneHandle handle) { - _checkIfExited(); + int get minor => (code >> 16) & 0xff; - final completer = Completer(); + int get build => (code >> 0) & 0xffff; - Future(() { - dylib.sane_close(_getNativeHandle(handle)); - _nativeHandles.remove(handle); - logger.finest('sane_close()'); + @override + String toString() => '$major.$minor.$build'; - completer.complete(); - }); + @override + bool operator ==(covariant SaneVersion other) => code == other.code; - return completer.future; - } + @override + int get hashCode => code; +} - Future getOptionDescriptor( - SaneHandle handle, +/// Represents a SANE device. +/// +/// Devices can be retrieved using [Sane.getDevices]. +/// +/// See also: +/// +/// - [Device Descriptor Type](https://sane-project.gitlab.io/standard/api.html#device-descriptor-type) +abstract interface class SaneDevice { + /// The name of the device. + String get name; + + /// The type of the device. + /// + /// For a list of predefined types, see [SaneDeviceTypes]. + String get type; + + /// The vendor (manufacturer) of the device. + /// + /// Can be `null` for virtual devices that have no physical vendor associated. + String? get vendor; + + /// The model of the device. + String get model; + + /// Disposes the SANE device. Infers [cancel]. + /// + /// See also: + /// + /// - [`sane_close`](https://sane-project.gitlab.io/standard/api.html#sane-close) + FutureOr close(); + + /// Tries to cancel the currently pending operation of the device immediately + /// or as quickly as possible. + /// + /// See also: + /// + /// - [`sane_cancel`](https://sane-project.gitlab.io/standard/api.html#sane-cancel) + FutureOr cancel(); + + /// Reads image data from the device. + /// + /// The returned [Uint8List] is [bufferSize] bytes long or less. If it is + /// zero, the end of the frame has been reached. + /// + /// Exceptions: + /// + /// - Throws [SaneCancelledException] if the operation was cancelled through + /// a call to [cancel]. + /// - Throws [SaneJammedException] if the document feeder is jammed. + /// - Throws [SaneNoDocumentsException] if the document feeder is out of + /// documents. + /// - Throws [SaneCoverOpenException] if the scanner cover is open. + /// - Throws [SaneIoException] if an error occurred while communicating with + /// the device. + /// - Throws [SaneNoMemoryException] if no memory is available. + /// - Throws [SaneAccessDeniedException] if access to the device has been + /// denied due to insufficient or invalid authentication. + /// + /// See also: + /// + /// - [`sane_read`](https://sane-project.gitlab.io/standard/api.html#sane-read) + FutureOr read({required int bufferSize}); + + /// Initiates acquisition of an image from the device. + /// + /// Exceptions: + /// + /// - Throws [SaneCancelledException] if the operation was cancelled through + /// a call to [cancel]. + /// - Throws [SaneDeviceBusyException] if the device is busy. The operation + /// should be later again. + /// - Throws [SaneJammedException] if the document feeder is jammed. + /// - Throws [SaneNoDocumentsException] if the document feeder is out of + /// documents. + /// - Throws [SaneCoverOpenException] if the scanner cover is open. + /// - Throws [SaneIoException] if an error occurred while communicating with + /// the device. + /// - Throws [SaneNoMemoryException] if no memory is available. + /// - Throws [SaneInvalidDataException] if the sane cannot be started with the + /// current set of options. The frontend should reload the option + /// descriptors. + /// + /// See also: + /// + /// - [`sane_start`](https://sane-project.gitlab.io/standard/api.html#sane-start) + FutureOr start(); + + FutureOr getOptionDescriptor(int index); + + FutureOr> getAllOptionDescriptors(); + + FutureOr> controlBoolOption( int index, - ) { - _checkIfExited(); - - final completer = Completer(); - - Future(() { - final optionDescriptorPointer = - dylib.sane_get_option_descriptor(_getNativeHandle(handle), index); - final optionDescriptor = saneOptionDescriptorFromNative( - optionDescriptorPointer.ref, - index, - ); - - ffi.calloc.free(optionDescriptorPointer); - - completer.complete(optionDescriptor); - }); - - return completer.future; - } - - Future> getAllOptionDescriptors( - SaneHandle handle, - ) { - _checkIfExited(); - - final completer = Completer>(); - - Future(() { - final optionDescriptors = []; - - for (var i = 0; true; i++) { - final descriptorPointer = - dylib.sane_get_option_descriptor(_getNativeHandle(handle), i); - if (descriptorPointer == ffi.nullptr) break; - optionDescriptors.add( - saneOptionDescriptorFromNative(descriptorPointer.ref, i), - ); - } - - completer.complete(optionDescriptors); - }); - - return completer.future; - } - - Future> _controlOption({ - required SaneHandle handle, - required int index, - required SaneAction action, - T? value, - }) { - _checkIfExited(); - - final completer = Completer>(); - - Future(() { - final optionDescriptor = saneOptionDescriptorFromNative( - dylib.sane_get_option_descriptor(_getNativeHandle(handle), index).ref, - index, - ); - final optionType = optionDescriptor.type; - final optionSize = optionDescriptor.size; - - final infoPointer = ffi.calloc(); - - late final ffi.Pointer valuePointer; - switch (optionType) { - case SaneOptionValueType.bool: - valuePointer = ffi.calloc(optionSize); - - case SaneOptionValueType.int: - valuePointer = ffi.calloc(optionSize); - - case SaneOptionValueType.fixed: - valuePointer = ffi.calloc(optionSize); - - case SaneOptionValueType.string: - valuePointer = ffi.calloc(optionSize); - - case SaneOptionValueType.button: - valuePointer = ffi.nullptr; - - case SaneOptionValueType.group: - throw const SaneInvalidDataException(); - } - - if (action == SaneAction.setValue) { - switch (optionType) { - case SaneOptionValueType.bool: - if (value is! bool) continue invalid; - (valuePointer as ffi.Pointer).value = - saneBoolFromDartBool(value); - break; - - case SaneOptionValueType.int: - if (value is! int) continue invalid; - (valuePointer as ffi.Pointer).value = value; - break; - - case SaneOptionValueType.fixed: - if (value is! double) continue invalid; - (valuePointer as ffi.Pointer).value = - doubleToSaneFixed(value); - break; - - case SaneOptionValueType.string: - if (value is! String) continue invalid; - (valuePointer as ffi.Pointer).value = - saneStringFromDartString(value).value; - break; - - case SaneOptionValueType.button: - break; - - case SaneOptionValueType.group: - continue invalid; - - invalid: - default: - throw const SaneInvalidDataException(); - } - } - - final status = dylib.sane_control_option( - _getNativeHandle(handle), - index, - nativeSaneActionFromDart(action), - valuePointer.cast(), - infoPointer, - ); - logger.finest( - 'sane_control_option($index, $action, $value) -> ${status.name}', - ); - - status.check(); - - final infos = saneOptionInfoFromNative(infoPointer.value); - late final dynamic result; - switch (optionType) { - case SaneOptionValueType.bool: - result = dartBoolFromSaneBool( - (valuePointer as ffi.Pointer).value, - ); - - case SaneOptionValueType.int: - result = (valuePointer as ffi.Pointer).value; - - case SaneOptionValueType.fixed: - result = - saneFixedToDouble((valuePointer as ffi.Pointer).value); - - case SaneOptionValueType.string: - result = dartStringFromSaneString( - valuePointer as ffi.Pointer, - ) ?? - ''; - - case SaneOptionValueType.button: - result = null; - - default: - throw const SaneInvalidDataException(); - } - - ffi.calloc.free(valuePointer); - ffi.calloc.free(infoPointer); - - completer.complete( - SaneOptionResult( - result: result, - infos: infos, - ), - ); - }); - - return completer.future; - } - - Future> controlBoolOption({ - required SaneHandle handle, - required int index, - required SaneAction action, + SaneAction action, bool? value, - }) { - return _controlOption( - handle: handle, - index: index, - action: action, - value: value, - ); - } - - Future> controlIntOption({ - required SaneHandle handle, - required int index, - required SaneAction action, - int? value, - }) { - return _controlOption( - handle: handle, - index: index, - action: action, - value: value, - ); - } - - Future> controlFixedOption({ - required SaneHandle handle, - required int index, - required SaneAction action, - double? value, - }) { - return _controlOption( - handle: handle, - index: index, - action: action, - value: value, - ); - } - - Future> controlStringOption({ - required SaneHandle handle, - required int index, - required SaneAction action, - String? value, - }) { - return _controlOption( - handle: handle, - index: index, - action: action, - value: value, - ); - } - - Future> controlButtonOption({ - required SaneHandle handle, - required int index, - }) { - return _controlOption( - handle: handle, - index: index, - action: SaneAction.setValue, - value: null, - ); - } - - Future getParameters(SaneHandle handle) { - _checkIfExited(); - - final completer = Completer(); - - Future(() { - final nativeParametersPointer = ffi.calloc(); - final status = dylib.sane_get_parameters( - _getNativeHandle(handle), - nativeParametersPointer, - ); - logger.finest('sane_get_parameters() -> ${status.name}'); - - status.check(); - - final parameters = saneParametersFromNative(nativeParametersPointer.ref); - - ffi.calloc.free(nativeParametersPointer); - - completer.complete(parameters); - }); - - return completer.future; - } - - Future start(SaneHandle handle) { - _checkIfExited(); - - final completer = Completer(); - - Future(() { - final status = dylib.sane_start(_getNativeHandle(handle)); - logger.finest('sane_start() -> ${status.name}'); - - status.check(); - - completer.complete(); - }); - - return completer.future; - } - - Future read(SaneHandle handle, int bufferSize) { - _checkIfExited(); - - final completer = Completer(); - - Future(() { - final bytesReadPointer = ffi.calloc(); - final bufferPointer = ffi.calloc(bufferSize); + ); - final status = dylib.sane_read( - _getNativeHandle(handle), - bufferPointer, - bufferSize, - bytesReadPointer, - ); - logger.finest('sane_read() -> ${status.name}'); - - status.check(); - - final bytes = Uint8List.fromList( - List.generate( - bytesReadPointer.value, - (i) => (bufferPointer + i).value, - ), - ); - - ffi.calloc.free(bytesReadPointer); - ffi.calloc.free(bufferPointer); - - completer.complete(bytes); - }); - - return completer.future; - } - - Future cancel(SaneHandle handle) { - _checkIfExited(); - - final completer = Completer(); - - Future(() { - dylib.sane_cancel(_getNativeHandle(handle)); - logger.finest('sane_cancel()'); - - completer.complete(); - }); - - return completer.future; - } - - Future setIOMode(SaneHandle handle, SaneIOMode mode) { - _checkIfExited(); - - final completer = Completer(); + FutureOr> controlIntOption( + int index, + SaneAction action, + int? value, + ); - Future(() { - final status = dylib.sane_set_io_mode( - _getNativeHandle(handle), - saneBoolFromIOMode(mode), - ); - logger.finest('sane_set_io_mode() -> ${status.name}'); + FutureOr> controlFixedOption( + int index, + SaneAction action, + double? value, + ); - status.check(); + FutureOr> controlStringOption( + int index, + SaneAction action, + String? value, + ); - completer.complete(); - }); + FutureOr> controlButtonOption(int index); - return completer.future; - } + FutureOr getParameters(); +} - @pragma('vm:prefer-inline') - void _checkIfExited() { - if (_exited) throw SaneDisposedError(); - } +/// Predefined device types for [SaneDevice.type]. +/// +/// See also: +/// +/// - [Predefined Device Information Strings](https://sane-project.gitlab.io/standard/api.html#vendor-names) +abstract final class SaneDeviceTypes { + static const filmScanner = 'film scanner'; + static const flatbedScanner = 'flatbed scanner'; + static const frameGrabber = 'frame grabber'; + static const handheldScanner = 'handheld scanner'; + static const multiFunctionPeripheral = 'multi-function peripheral'; + static const sheetfedScanner = 'sheetfed scanner'; + static const stillCamera = 'still camera'; + static const videoCamera = 'video camera'; + static const virtualDevice = 'virtual device'; } diff --git a/packages/sane/lib/src/sane_dev.dart b/packages/sane/lib/src/sane_dev.dart deleted file mode 100644 index 8e39549..0000000 --- a/packages/sane/lib/src/sane_dev.dart +++ /dev/null @@ -1,216 +0,0 @@ -import 'dart:typed_data'; - -import 'package:logging/logging.dart'; -import 'package:sane/sane.dart'; - -final _logger = Logger('sane.dev'); - -class SaneDev implements Sane { - @override - Future cancel(SaneHandle handle) { - return Future.delayed(const Duration(seconds: 1), () { - _logger.finest('sane_cancel()'); - }); - } - - @override - Future close(SaneHandle handle) { - return Future.delayed(const Duration(seconds: 1), () { - _logger.finest('sane_close()'); - }); - } - - @override - Future> controlBoolOption({ - required SaneHandle handle, - required int index, - required SaneAction action, - bool? value, - }) { - return Future.delayed(const Duration(seconds: 1), () { - _logger.finest('sane_controlBoolOption()'); - return SaneOptionResult(result: value ?? true, infos: []); - }); - } - - @override - Future> controlButtonOption({ - required SaneHandle handle, - required int index, - }) { - return Future.delayed(const Duration(seconds: 1), () { - _logger.finest('sane_controlButtonOption()'); - return SaneOptionResult(result: null, infos: []); - }); - } - - @override - Future> controlFixedOption({ - required SaneHandle handle, - required int index, - required SaneAction action, - double? value, - }) { - return Future.delayed(const Duration(seconds: 1), () { - _logger.finest('sane_controlFixedOption()'); - return SaneOptionResult(result: value ?? .1, infos: []); - }); - } - - @override - Future> controlIntOption({ - required SaneHandle handle, - required int index, - required SaneAction action, - int? value, - }) { - return Future.delayed(const Duration(seconds: 1), () { - _logger.finest('sane_controlIntOption()'); - return SaneOptionResult(result: value ?? 1, infos: []); - }); - } - - @override - Future> controlStringOption({ - required SaneHandle handle, - required int index, - required SaneAction action, - String? value, - }) { - return Future.delayed(const Duration(seconds: 1), () { - _logger.finest('sane_controlStringOption()'); - return SaneOptionResult(result: value ?? 'value', infos: []); - }); - } - - @override - Future exit() { - return Future(() { - _logger.finest('sane_exit()'); - }); - } - - @override - Future> getAllOptionDescriptors( - SaneHandle handle, - ) { - return Future.delayed(const Duration(seconds: 1), () { - _logger.finest('sane_getAllOptionDescriptors()'); - return [ - SaneOptionDescriptor( - index: 0, - name: 'name', - title: 'title', - desc: 'desc', - type: SaneOptionValueType.int, - unit: SaneOptionUnit.none, - size: 1, - capabilities: [], - constraint: null, - ), - ]; - }); - } - - @override - Future> getDevices({ - required bool localOnly, - }) { - return Future.delayed(const Duration(seconds: 1), () { - _logger.finest('sane_getDevices()'); - return [ - for (var i = 0; i < 3; i++) - SaneDevice( - name: 'name $i', - vendor: 'Vendor$i', - model: 'Model$i', - type: 'Type$i', - ), - ]; - }); - } - - @override - Future getOptionDescriptor( - SaneHandle handle, - int index, - ) { - return Future.delayed(const Duration(seconds: 1), () { - _logger.finest('sane_getOptionDescriptor()'); - return SaneOptionDescriptor( - index: index, - name: 'name', - title: 'title', - desc: 'desc', - type: SaneOptionValueType.int, - unit: SaneOptionUnit.none, - size: 1, - capabilities: [], - constraint: null, - ); - }); - } - - @override - Future getParameters(SaneHandle handle) { - return Future.delayed(const Duration(seconds: 1), () { - _logger.finest('sane_getParameters()'); - return SaneParameters( - format: SaneFrameFormat.gray, - lastFrame: true, - bytesPerLine: 800, - pixelsPerLine: 100, - lines: 100, - depth: 8, - ); - }); - } - - @override - Future init({ - AuthCallback? authCallback, - }) { - return Future(() { - _logger.finest('sane_init()'); - return 1; - }); - } - - @override - Future open(String deviceName) { - return Future.delayed(const Duration(seconds: 1), () { - _logger.finest('sane_open()'); - return SaneHandle(deviceName: deviceName); - }); - } - - @override - Future openDevice(SaneDevice device) { - return Future.delayed(const Duration(seconds: 1), () { - _logger.finest('sane_openDevice()'); - return SaneHandle(deviceName: device.name); - }); - } - - @override - Future read(SaneHandle handle, int bufferSize) { - return Future.delayed(const Duration(seconds: 1), () { - _logger.finest('sane_read()'); - return Uint8List.fromList([]); - }); - } - - @override - Future setIOMode(SaneHandle handle, SaneIOMode mode) { - return Future.delayed(const Duration(seconds: 1), () { - _logger.finest('sane_setIOMode()'); - }); - } - - @override - Future start(SaneHandle handle) { - return Future.delayed(const Duration(seconds: 1), () { - _logger.finest('sane_start()'); - }); - } -} diff --git a/packages/sane/lib/src/sane_isolate.dart b/packages/sane/lib/src/sane_isolate.dart deleted file mode 100644 index e27d853..0000000 --- a/packages/sane/lib/src/sane_isolate.dart +++ /dev/null @@ -1,355 +0,0 @@ -import 'dart:async'; -import 'dart:isolate'; -import 'dart:typed_data'; - -import 'package:logging/logging.dart'; -import 'package:sane/sane.dart'; -import 'package:sane/src/extensions.dart'; -import 'package:sane/src/isolate_messages/cancel.dart'; -import 'package:sane/src/isolate_messages/close.dart'; -import 'package:sane/src/isolate_messages/control_button_option.dart'; -import 'package:sane/src/isolate_messages/control_option.dart'; -import 'package:sane/src/isolate_messages/exception.dart'; -import 'package:sane/src/isolate_messages/exit.dart'; -import 'package:sane/src/isolate_messages/get_all_option_descriptors.dart'; -import 'package:sane/src/isolate_messages/get_devices.dart'; -import 'package:sane/src/isolate_messages/get_option_descriptor.dart'; -import 'package:sane/src/isolate_messages/get_parameters.dart'; -import 'package:sane/src/isolate_messages/init.dart'; -import 'package:sane/src/isolate_messages/interface.dart'; -import 'package:sane/src/isolate_messages/open.dart'; -import 'package:sane/src/isolate_messages/read.dart'; -import 'package:sane/src/isolate_messages/set_io_mode.dart'; -import 'package:sane/src/isolate_messages/start.dart'; - -final _logger = Logger('sane.isolate'); - -class SaneIsolate implements Sane { - SaneIsolate({ - required Sane sane, - }) : _sane = sane; - - final Sane _sane; - - late final Isolate _isolate; - late final SendPort _sendPort; - - Future spawn() async { - final receivePort = ReceivePort(); - _isolate = await Isolate.spawn( - _isolateEntryPoint, - _IsolateEntryPointArgs( - mainSendPort: receivePort.sendPort, - sane: _sane, - ), - ); - - final sendPortCompleter = Completer(); - receivePort.listen((message) { - if (message is _SendPortMessage) { - sendPortCompleter.complete(message.sendPort); - } else if (message is _LogRecordMessage) { - _logger.redirect(message.record); - } - }); - _sendPort = await sendPortCompleter.future; - } - - void kill() { - _isolate.kill(priority: Isolate.immediate); - } - - Future _sendMessage( - IsolateMessage message, - ) async { - final replyPort = ReceivePort(); - - _sendPort.send( - _IsolateMessageEnvelope( - replyPort: replyPort.sendPort, - message: message, - ), - ); - - final response = await replyPort.first; - replyPort.close(); - - if (response is ExceptionResponse) { - Error.throwWithStackTrace( - response.exception, - response.stackTrace, - ); - } - - return response as T; - } - - @override - Future init({ - AuthCallback? authCallback, - }) async { - final response = await _sendMessage(InitMessage()); - return response.versionCode; - } - - @override - Future exit() async { - await _sendMessage(ExitMessage()); - } - - @override - Future> getDevices({ - required bool localOnly, - }) async { - final response = await _sendMessage( - GetDevicesMessage(localOnly: localOnly), - ); - - return response.devices; - } - - @override - Future open(String deviceName) async { - final response = await _sendMessage( - OpenMessage(deviceName: deviceName), - ); - - return response.handle; - } - - @override - Future openDevice(SaneDevice device) { - return open(device.name); - } - - @override - Future close(SaneHandle handle) async { - await _sendMessage( - CloseMessage(saneHandle: handle), - ); - } - - @override - Future getOptionDescriptor( - SaneHandle handle, - int index, - ) async { - final response = await _sendMessage( - GetOptionDescriptorMessage( - saneHandle: handle, - index: index, - ), - ); - - return response.optionDescriptor; - } - - @override - Future> getAllOptionDescriptors( - SaneHandle handle, - ) async { - final response = await _sendMessage( - GetAllOptionDescriptorsMessage(saneHandle: handle), - ); - - return response.optionDescriptors; - } - - @override - Future> controlBoolOption({ - required SaneHandle handle, - required int index, - required SaneAction action, - bool? value, - }) async { - final response = await _sendMessage( - ControlValueOptionMessage( - saneHandle: handle, - index: index, - action: action, - value: value, - ), - ); - - return response.result; - } - - @override - Future> controlIntOption({ - required SaneHandle handle, - required int index, - required SaneAction action, - int? value, - }) async { - final response = await _sendMessage( - ControlValueOptionMessage( - saneHandle: handle, - index: index, - action: action, - value: value, - ), - ); - - return response.result; - } - - @override - Future> controlFixedOption({ - required SaneHandle handle, - required int index, - required SaneAction action, - double? value, - }) async { - final response = await _sendMessage( - ControlValueOptionMessage( - saneHandle: handle, - index: index, - action: action, - value: value, - ), - ); - - return response.result; - } - - @override - Future> controlStringOption({ - required SaneHandle handle, - required int index, - required SaneAction action, - String? value, - }) async { - final response = await _sendMessage( - ControlValueOptionMessage( - saneHandle: handle, - index: index, - action: action, - value: value, - ), - ); - - return response.result; - } - - @override - Future> controlButtonOption({ - required SaneHandle handle, - required int index, - }) async { - final response = await _sendMessage( - ControlButtonOptionMessage( - saneHandle: handle, - index: index, - ), - ); - - return response.result; - } - - @override - Future getParameters(SaneHandle handle) async { - final response = await _sendMessage( - GetParametersMessage( - saneHandle: handle, - ), - ); - - return response.parameters; - } - - @override - Future start(SaneHandle handle) async { - await _sendMessage( - StartMessage(saneHandle: handle), - ); - } - - @override - Future read(SaneHandle handle, int bufferSize) async { - final response = await _sendMessage( - ReadMessage( - saneHandle: handle, - bufferSize: bufferSize, - ), - ); - - return response.bytes; - } - - @override - Future cancel(SaneHandle handle) async { - await _sendMessage( - CancelMessage(saneHandle: handle), - ); - } - - @override - Future setIOMode( - SaneHandle handle, - SaneIOMode ioMode, - ) async { - await _sendMessage( - SetIOModeMessage(saneHandle: handle, ioMode: ioMode), - ); - } -} - -class _IsolateEntryPointArgs { - _IsolateEntryPointArgs({ - required this.mainSendPort, - required this.sane, - }); - - final SendPort mainSendPort; - final Sane sane; -} - -void _isolateEntryPoint(_IsolateEntryPointArgs args) { - final isolateReceivePort = ReceivePort(); - args.mainSendPort.send( - _SendPortMessage(isolateReceivePort.sendPort), - ); - - Logger.root.level = Level.ALL; - Logger.root.onRecord.listen((record) { - args.mainSendPort.send( - _LogRecordMessage(record), - ); - }); - - final sane = args.sane; - isolateReceivePort.cast<_IsolateMessageEnvelope>().listen((envelope) async { - late IsolateResponse response; - - try { - response = await envelope.message.handle(sane); - } on SaneException catch (exception, stackTrace) { - response = ExceptionResponse( - exception: exception, - stackTrace: stackTrace, - ); - } - - envelope.replyPort.send(response); - }); -} - -class _IsolateMessageEnvelope { - _IsolateMessageEnvelope({ - required this.replyPort, - required this.message, - }); - - final SendPort replyPort; - final IsolateMessage message; -} - -class _SendPortMessage { - _SendPortMessage(this.sendPort); - final SendPort sendPort; -} - -class _LogRecordMessage { - _LogRecordMessage(this.record); - final LogRecord record; -} diff --git a/packages/sane/lib/src/structures.dart b/packages/sane/lib/src/structures.dart index e199a96..580f4f2 100644 --- a/packages/sane/lib/src/structures.dart +++ b/packages/sane/lib/src/structures.dart @@ -1,5 +1,3 @@ -import 'package:meta/meta.dart'; - class SaneCredentials { SaneCredentials({ required this.username, @@ -10,33 +8,6 @@ class SaneCredentials { final String password; } -class SaneDevice { - SaneDevice({ - required this.name, - required this.vendor, - required this.model, - required this.type, - }); - - final String name; - final String vendor; - final String model; - final String type; -} - -@immutable -class SaneHandle { - const SaneHandle({required this.deviceName}); - final String deviceName; - - @override - bool operator ==(Object other) => - other is SaneHandle && other.deviceName == deviceName; - - @override - int get hashCode => deviceName.hashCode; -} - enum SaneFrameFormat { gray, rgb, @@ -170,8 +141,3 @@ class SaneOptionResult { final T result; final List infos; } - -enum SaneIOMode { - nonBlocking, - blocking; -} diff --git a/packages/sane/lib/src/type_conversion.dart b/packages/sane/lib/src/type_conversion.dart index 0371910..25b912e 100644 --- a/packages/sane/lib/src/type_conversion.dart +++ b/packages/sane/lib/src/type_conversion.dart @@ -1,18 +1,11 @@ import 'dart:ffi' as ffi; import 'package:ffi/ffi.dart' as ffi; +import 'package:meta/meta.dart'; import 'package:sane/src/bindings.g.dart'; import 'package:sane/src/structures.dart'; -SaneDevice saneDeviceFromNative(SANE_Device device) { - return SaneDevice( - name: dartStringFromSaneString(device.name) ?? '', - vendor: dartStringFromSaneString(device.vendor) ?? '', - model: dartStringFromSaneString(device.model) ?? '', - type: dartStringFromSaneString(device.type) ?? '', - ); -} - +@internal SaneFrameFormat saneFrameFormatFromNative(SANE_Frame frame) { return switch (frame) { SANE_Frame.FRAME_GRAY => SaneFrameFormat.gray, @@ -23,6 +16,7 @@ SaneFrameFormat saneFrameFormatFromNative(SANE_Frame frame) { }; } +@internal SaneParameters saneParametersFromNative(SANE_Parameters parameters) { return SaneParameters( format: saneFrameFormatFromNative(parameters.format), @@ -34,6 +28,7 @@ SaneParameters saneParametersFromNative(SANE_Parameters parameters) { ); } +@internal SaneOptionValueType saneOptionValueTypeFromNative(SANE_Value_Type valueType) { return switch (valueType) { SANE_Value_Type.TYPE_BOOL => SaneOptionValueType.bool, @@ -45,6 +40,7 @@ SaneOptionValueType saneOptionValueTypeFromNative(SANE_Value_Type valueType) { }; } +@internal SaneOptionUnit saneOptionUnitFromNative(SANE_Unit unit) { return switch (unit) { SANE_Unit.UNIT_NONE => SaneOptionUnit.none, @@ -57,6 +53,7 @@ SaneOptionUnit saneOptionUnitFromNative(SANE_Unit unit) { }; } +@internal SANE_Action nativeSaneActionFromDart(SaneAction action) { return switch (action) { SaneAction.getValue => SANE_Action.ACTION_GET_VALUE, @@ -65,6 +62,7 @@ SANE_Action nativeSaneActionFromDart(SaneAction action) { }; } +@internal List saneOptionCapabilityFromBitmap(int bitset) { final capabilities = []; @@ -93,6 +91,7 @@ List saneOptionCapabilityFromBitmap(int bitset) { return capabilities; } +@internal SaneOptionConstraint? saneConstraintFromNative( UnnamedUnion1 constraint, SANE_Constraint_Type constraintType, @@ -143,22 +142,23 @@ SaneOptionConstraint? saneConstraintFromNative( case SANE_Constraint_Type.CONSTRAINT_STRING_LIST: final stringList = []; for (var i = 0; constraint.string_list[i] != ffi.nullptr; i++) { - final string = dartStringFromSaneString(constraint.string_list[i])!; + final string = constraint.string_list[i].toDartString(); stringList.add(string); } return SaneOptionConstraintStringList(stringList: stringList); } } +@internal SaneOptionDescriptor saneOptionDescriptorFromNative( SANE_Option_Descriptor optionDescriptor, int index, ) { return SaneOptionDescriptor( index: index, - name: dartStringFromSaneString(optionDescriptor.name) ?? '', - title: dartStringFromSaneString(optionDescriptor.title) ?? '', - desc: dartStringFromSaneString(optionDescriptor.desc) ?? '', + name: optionDescriptor.name.toDartString(), + title: optionDescriptor.title.toDartString(), + desc: optionDescriptor.desc.toDartString(), type: saneOptionValueTypeFromNative(optionDescriptor.type), unit: saneOptionUnitFromNative(optionDescriptor.unit), size: optionDescriptor.size, @@ -171,6 +171,7 @@ SaneOptionDescriptor saneOptionDescriptorFromNative( ); } +@internal List saneOptionInfoFromNative(int bitset) { final infos = []; if (bitset & SANE_INFO_INEXACT != 0) { @@ -185,13 +186,7 @@ List saneOptionInfoFromNative(int bitset) { return infos; } -DartSANE_Word saneBoolFromIOMode(SaneIOMode mode) { - return switch (mode) { - SaneIOMode.blocking => SANE_FALSE, - SaneIOMode.nonBlocking => SANE_TRUE, - }; -} - +@internal bool dartBoolFromSaneBool(int bool) { switch (bool) { case 0: @@ -203,25 +198,29 @@ bool dartBoolFromSaneBool(int bool) { } } -DartSANE_Word saneBoolFromDartBool(bool bool) { - return bool ? SANE_TRUE : SANE_FALSE; +@internal +extension SaneStringExtensions on SANE_String_Const { + String toDartString() => cast().toDartString(); } -String? dartStringFromSaneString(SANE_String_Const stringPointer) { - if (stringPointer == ffi.nullptr) return null; - return stringPointer.cast().toDartString(); +@internal +extension StringExtensions on String { + SANE_String_Const toSaneString() => toNativeUtf8().cast(); } -SANE_String_Const saneStringFromDartString(String string) { - return string.toNativeUtf8().cast(); +@internal +extension BoolExtensions on bool { + DartSANE_Word get asSaneBool => this ? SANE_TRUE : SANE_FALSE; } const int _saneFixedScaleFactor = 1 << SANE_FIXED_SCALE_SHIFT; +@internal double saneFixedToDouble(int saneFixed) { return saneFixed / _saneFixedScaleFactor; } +@internal int doubleToSaneFixed(double double) { return (double * _saneFixedScaleFactor).toInt(); } diff --git a/packages/sane/lib/src/utils.dart b/packages/sane/lib/src/utils.dart deleted file mode 100644 index 0dc7804..0000000 --- a/packages/sane/lib/src/utils.dart +++ /dev/null @@ -1,17 +0,0 @@ -abstract class SaneUtils { - static int versionMajor(int versionCode) { - return (versionCode >> 24) & 0xff; - } - - static int versionMinor(int versionCode) { - return (versionCode >> 16) & 0xff; - } - - static int versionBuild(int versionCode) { - return (versionCode >> 0) & 0xffff; - } - - static String version(int versionCode) { - return '${versionMajor(versionCode)}.${versionMinor(versionCode)}.${versionBuild(versionCode)}'; - } -} diff --git a/packages/sane/pubspec.yaml b/packages/sane/pubspec.yaml index fccbe9b..a836bec 100644 --- a/packages/sane/pubspec.yaml +++ b/packages/sane/pubspec.yaml @@ -25,6 +25,10 @@ ffigen: headers: entry-points: - "/usr/include/sane/sane.h" + functions: + symbol-address: + include: + - sane_close enums: member-rename: ".*": diff --git a/packages/sane/test/sane_singleton_test.dart b/packages/sane/test/sane_singleton_test.dart index 25e81ca..377f34c 100644 --- a/packages/sane/test/sane_singleton_test.dart +++ b/packages/sane/test/sane_singleton_test.dart @@ -1,9 +1,18 @@ +import 'package:logging/logging.dart'; import 'package:sane/sane.dart'; import 'package:test/test.dart'; void main() { late Sane sane; + setUp(() { + Logger.root.level = Level.ALL; + Logger.root.onRecord.listen((record) { + // ignore: avoid_print + print('${record.level.name}: ${record.time}: ${record.message}'); + }); + }); + test('can instantiate', () { sane = Sane(); }); @@ -14,7 +23,7 @@ void main() { }); test('can exit', () { - expect(sane.exit, returnsNormally); + expect(sane.dispose, returnsNormally); }); test('throws upon use', () { @@ -25,7 +34,7 @@ void main() { }); test('can reinstiate with new instance', () { - final newSane = Sane(); + final newSane = Sane(Sane.mock()); expect(sane, isNot(newSane)); sane = newSane; }); diff --git a/packages/sane/test/sane_test.dart b/packages/sane/test/sane_test.dart index 0081b25..d87b485 100644 --- a/packages/sane/test/sane_test.dart +++ b/packages/sane/test/sane_test.dart @@ -3,7 +3,6 @@ import 'package:test/test.dart'; void main() { test('Sane init test', () { - final sane = Sane(); - expect(sane.init, returnsNormally); + expect(Sane.new, returnsNormally); }); }