diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index fa4dc6f..c9315a5 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup flutter SDK uses: subosito/flutter-action@v2 @@ -26,140 +26,29 @@ jobs: - run: flutter precache - - name: melos bootstrap - run: dart pub global run melos bootstrap + - run: melos bootstrap - - name: melos checkformat - run: dart pub global run melos run checkformat + - run: melos run checkformat - - name: melos analyze - run: dart pub global run melos run analyze + - run: melos run analyze - - name: melos test - run: dart pub global run melos test + - run: melos run test - create-panda-temp-key: - name: Create temporary SSH key - runs-on: panda - concurrency: panda-temp-key - steps: - - uses: actions/checkout@v3 - - - name: Generate keypair - run: | - ssh-keygen -b 4096 -t rsa -f ./sshkey -q -N "" - - - name: Add public key to authorized_keys - run: | - echo expiry-time='"'$(date --date=+1hour +%Y%m%d%H%M)'"' $(cat ./sshkey.pub) > ~/.ssh/authorized_keys - - - name: Encrypt private key for uploading - env: - AES_KEY: ${{ secrets.AES_KEY }} - run: | - gpg \ - --batch --yes \ - --symmetric --cipher-algo AES256 \ - --passphrase="$AES_KEY" \ - --output ./sshkey.gpg \ - sshkey + can-test: + runs-on: doublecan - - name: Upload keypair - uses: actions/upload-artifact@v3 - with: - name: keypair - path: | - sshkey.gpg - sshkey.pub - - integration-test: - runs-on: ubuntu-latest - needs: create-panda-temp-key - concurrency: panda-temp-key steps: - - uses: actions/checkout@v3 - - - name: Download keypair - uses: actions/download-artifact@v3 - with: - name: keypair - path: . - - - name: Decrypt private key - env: - AES_KEY: ${{ secrets.AES_KEY }} - run: | - gpg \ - --batch --yes \ - --decrypt \ - --passphrase="$AES_KEY" \ - --output ./sshkey \ - ./sshkey.gpg - chmod 0600 ./sshkey - - - name: Setup adb forward - run: | - mkdir -p ~/.ssh - echo "[md.ardera.dev]:23 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBC41DBiTm+uvaeZqUp4LcUck99c7+vIv/nS4rH4rt/rmSs5KAlJrz7TwicgDYobAdDL8Nnfyz0F2CG88uCNRea0=" >> ~/.ssh/known_hosts - ssh -vvv -N -n -o ExitOnForwardFailure=yes -o BatchMode=yes -L 127.0.0.1:5555:odroid-c4:5555 -p 23 -i ./sshkey ci@md.ardera.dev & - sleep 5 + - uses: actions/checkout@v4 - - name: Setup Android SDK - uses: android-actions/setup-android@v2 + - name: Setup Dart SDK + uses: dart-lang/setup-dart@v1 - - name: List adb devices - run: | - adb devices -l - adb connect 127.0.0.1:5555 - adb devices -l - - - name: Setup flutter SDK - uses: subosito/flutter-action@v2 - with: - cache: true - - name: Setup melos run: dart pub global activate melos - - - run: flutter doctor -v - - run: flutter precache - - - name: melos bootstrap - run: dart pub global run melos bootstrap - - - name: make ODROID C4 gpio chips accessible - run: | - adb -t 2 shell chmod 0777 /dev/gpiochip0 - adb -t 2 shell chmod 0777 /dev/gpiochip1 - - - name: run flutter_gpiod_test_app on ODROID C4 - working-directory: packages/flutter_gpiod_test_app - run: flutter test -d "127.0.0.1:5555" -t odroidc4 integration_test/gpio_test.dart - - delete-panda-temp-key: - name: Delete temporary SSH key - runs-on: panda - needs: integration-test - concurrency: panda-temp-key - - # run even if integration-test failed - if: ${{ always() }} - steps: - - uses: actions/checkout@v3 + - run: melos bootstrap --no-flutter - - name: Download keypair - uses: actions/download-artifact@v3 - with: - name: keypair - path: . - - - name: Remove key from authorized_keys file - run: | - echo "::group::List authorized keys" - echo "::debug::$(cat ~/.ssh/authorized_keys)" - echo "::endgroup::" - grep -v "$(cat sshkey.pub)" ~/.ssh/authorized_keys | sponge ~/.ssh/authorized_keys - echo "::group::List authorized keys" - echo "::debug::$(cat ~/.ssh/authorized_keys)" - echo "::endgroup::" + - name: Test linux_can + working-directory: packages/linux_can + run: + dart test integration_test --tags "double-can" diff --git a/packages/_ardera_common_libc_bindings/lib/src/epoll_listener.dart b/packages/_ardera_common_libc_bindings/lib/src/epoll_listener.dart index 955c15f..5fdf96f 100644 --- a/packages/_ardera_common_libc_bindings/lib/src/epoll_listener.dart +++ b/packages/_ardera_common_libc_bindings/lib/src/epoll_listener.dart @@ -255,6 +255,7 @@ class EpollIsolate { static const _maxEvents = 128; final _events = ffi.calloc(_maxEvents); + final _eventFdBuffer = ffi.calloc(8); var _shouldRun = true; var _nextCapabilityMapIndex = 1; @@ -456,6 +457,7 @@ class EpollIsolate { void dispose() { _subscription.cancel(); ffi.calloc.free(_events); + ffi.calloc.free(_eventFdBuffer); } int _retry(int Function() syscall, {Set retryErrorCodes = const {EINTR}}) { @@ -505,44 +507,50 @@ class EpollIsolate { )); if (ok < 0) { throw LinuxError('Could not wait for epoll events.', 'epoll_wait', libc.errno); + } else if (ok == 0) { + continue; } - if (ok > 0) { - // process any epoll events - for (var i = 0; i < ok; i++) { - final event = _events + i; + // process any epoll events + for (var i = 0; i < ok; i++) { + final event = _events + i; - final capability = _capabilityMap[event.ref.data.u64]!; + final capability = _capabilityMap[event.ref.data.u64]!; - final flags = {}; - for (final flag in EpollFlag.values) { - if (event.ref.events & flag._value != 0) { - flags.add(flag); - } + final flags = {}; + for (final flag in EpollFlag.values) { + if (event.ref.events & flag._value != 0) { + flags.add(flag); } + } - if (capability == _eventFdCap) { - // do nothing - } else { - assert(capability is Capability); - - // Find and invoke the handler. - final handler = _handlerMap[capability]!; - - try { - final value = handler.callback(this, handler.fd, flags, handler.callbackContext); - - _sendEvent( - handlerId: handler.id, - event: value, - ); - } on Exception catch (err, st) { - _sendError( - handlerId: handler.id, - error: err, - stackTrace: st, - ); - } + if (capability == _eventFdCap) { + // clear the eventfd buffer. + final result = _retry(() => libc.read(_eventFd, _eventFdBuffer.cast(), 8)); + if (result < 0) { + throw LinuxError('Could not clear event fd.', 'read', libc.errno); + } else if (result == 0) { + throw LinuxError('Event fd got EOF.', 'read'); + } + } else { + assert(capability is Capability); + + // Find and invoke the handler. + final handler = _handlerMap[capability]!; + + try { + final value = handler.callback(this, handler.fd, flags, handler.callbackContext); + + _sendEvent( + handlerId: handler.id, + event: value, + ); + } on Exception catch (err, st) { + _sendError( + handlerId: handler.id, + error: err, + stackTrace: st, + ); } } } @@ -709,7 +717,7 @@ class EpollEventLoop { throw RemoteError(_isolateError!, _isolateErrorStackTrace!); } else { - assert(false); + throw StateError('Got unexpected message from isolate: $msg'); } } @@ -818,7 +826,7 @@ class EpollEventLoop { ValueCallback? callback, }) async { assert(_alive); - assert(_handlers.containsKey(listener)); + assert(_handlers.containsKey(listener._id)); final seq = Capability(); diff --git a/packages/_ardera_common_libc_bindings/lib/src/libc_functions.dart b/packages/_ardera_common_libc_bindings/lib/src/libc_functions.dart index 80a1051..06fefaa 100644 --- a/packages/_ardera_common_libc_bindings/lib/src/libc_functions.dart +++ b/packages/_ardera_common_libc_bindings/lib/src/libc_functions.dart @@ -19,9 +19,9 @@ typedef SymbolLookupFn = ffi.Pointer Function(Strin class LibCArm extends LibC { final backend.LibCArm _backend; - LibCArm.fromLookup(SymbolLookupFn _lookup) + LibCArm.fromLookup(super._lookup) : _backend = backend.LibCArm.fromLookup(_lookup), - super._fromLookup(_lookup); + super._fromLookup(); @override int bind(int __fd, ffi.Pointer __addr, int __len) { @@ -222,9 +222,9 @@ class LibCArm extends LibC { class LibCArm64 extends LibC { final backend.LibCArm64 _backend; - LibCArm64.fromLookup(SymbolLookupFn _lookup) + LibCArm64.fromLookup(super._lookup) : _backend = backend.LibCArm64.fromLookup(_lookup), - super._fromLookup(_lookup); + super._fromLookup(); @override int bind(int __fd, ffi.Pointer __addr, int __len) { @@ -425,9 +425,9 @@ class LibCArm64 extends LibC { class LibCI386 extends LibC { final backend.LibCI386 _backend; - LibCI386.fromLookup(SymbolLookupFn _lookup) + LibCI386.fromLookup(super._lookup) : _backend = backend.LibCI386.fromLookup(_lookup), - super._fromLookup(_lookup); + super._fromLookup(); @override int bind(int __fd, ffi.Pointer __addr, int __len) { @@ -628,9 +628,9 @@ class LibCI386 extends LibC { class LibCAmd64 extends LibC { final backend.LibCAmd64 _backend; - LibCAmd64.fromLookup(SymbolLookupFn _lookup) + LibCAmd64.fromLookup(super._lookup) : _backend = backend.LibCAmd64.fromLookup(_lookup), - super._fromLookup(_lookup); + super._fromLookup(); @override int bind(int __fd, ffi.Pointer __addr, int __len) { diff --git a/packages/flutterpi_gstreamer_video_player/example/lib/main.dart b/packages/flutterpi_gstreamer_video_player/example/lib/main.dart index cbb0cca..f0cb82b 100644 --- a/packages/flutterpi_gstreamer_video_player/example/lib/main.dart +++ b/packages/flutterpi_gstreamer_video_player/example/lib/main.dart @@ -39,24 +39,24 @@ class _ExampleVideoPageState extends State { autoInitialize: true, autoPlay: true, looping: true, - additionalOptions: (context) { + additionalOptions: (_) { return [ OptionItem( - onTap: () { + onTap: (_) { _controller.stepForward(); }, iconData: Icons.arrow_right, title: 'Step Forward', ), OptionItem( - onTap: () { + onTap: (_) { _controller.stepBackward(); }, iconData: Icons.arrow_left, title: 'Step Backward', ), OptionItem( - onTap: () {}, + onTap: (_) {}, iconData: Icons.fast_forward_outlined, title: 'Fast Seek', ), @@ -120,7 +120,8 @@ class _CameraViewPageState extends State { } class _VideoApp extends StatefulWidget { - const _VideoApp({Key? key}) : super(key: key); + // ignore: unused_element_parameter + const _VideoApp({super.key}) : super(); @override _VideoAppState createState() => _VideoAppState(); diff --git a/packages/flutterpi_gstreamer_video_player/lib/src/platform.dart b/packages/flutterpi_gstreamer_video_player/lib/src/platform.dart index af2cb47..ff14c87 100644 --- a/packages/flutterpi_gstreamer_video_player/lib/src/platform.dart +++ b/packages/flutterpi_gstreamer_video_player/lib/src/platform.dart @@ -33,8 +33,8 @@ class FlutterpiVideoPlayer extends VideoPlayerPlatform { } @override - Future dispose(int textureId) async { - return await _invoke('dispose', textureId); + Future dispose(int playerId) async { + return await _invoke('dispose', playerId); } static const pipelineUrlScheme = 'gstreamerPipeline'; @@ -89,30 +89,30 @@ class FlutterpiVideoPlayer extends VideoPlayerPlatform { } @override - Future setLooping(int textureId, bool looping) async { - return await _invoke('setLooping', [textureId, looping]); + Future setLooping(int playerId, bool looping) async { + return await _invoke('setLooping', [playerId, looping]); } @override - Future play(int textureId) async { - return await _invoke('play', textureId); + Future play(int playerId) async { + return await _invoke('play', playerId); } @override - Future pause(int textureId) async { - return await _invoke('pause', textureId); + Future pause(int playerId) async { + return await _invoke('pause', playerId); } @override - Future setVolume(int textureId, double volume) async { - return await _invoke('setVolume', [textureId, volume]); + Future setVolume(int playerId, double volume) async { + return await _invoke('setVolume', [playerId, volume]); } @override - Future setPlaybackSpeed(int textureId, double speed) async { + Future setPlaybackSpeed(int playerId, double speed) async { assert(speed > 0); - return await _invoke('setPlaybackSpeed', [textureId, speed]); + return await _invoke('setPlaybackSpeed', [playerId, speed]); } /// Controls the behaviour of [seekTo]. (Dirty workaround because sanely extending VideoPlayerController is hard.) @@ -124,19 +124,19 @@ class FlutterpiVideoPlayer extends VideoPlayerPlatform { var seekMode = SeekMode.normal; @override - Future seekTo(int textureId, Duration position) async { + Future seekTo(int playerId, Duration position) async { late Future future; switch (seekMode) { case SeekMode.normal: future = _invoke( 'seekTo', - [textureId, position.inMilliseconds], + [playerId, position.inMilliseconds], ); break; case SeekMode.fast: try { - future = _invoke('fastSeek', [textureId, position.inMilliseconds]); + future = _invoke('fastSeek', [playerId, position.inMilliseconds]); } finally { seekMode = SeekMode.normal; } @@ -151,14 +151,14 @@ class FlutterpiVideoPlayer extends VideoPlayerPlatform { } @override - Future getPosition(int textureId) async { - final millis = await _invoke('getPosition', textureId); + Future getPosition(int playerId) async { + final millis = await _invoke('getPosition', playerId); return Duration(milliseconds: millis); } @override - Stream videoEventsFor(int textureId) { - return _eventChannelFor(textureId).receiveBroadcastStream().map((dynamic event) { + Stream videoEventsFor(int playerId) { + return _eventChannelFor(playerId).receiveBroadcastStream().map((dynamic event) { final Map map = event as Map; switch (map['event']) { case 'initialized': @@ -190,8 +190,8 @@ class FlutterpiVideoPlayer extends VideoPlayerPlatform { } @override - Widget buildView(int textureId) { - return Texture(textureId: textureId); + Widget buildView(int playerId) { + return Texture(textureId: playerId); } @override diff --git a/packages/linux_can/dart_test.yaml b/packages/linux_can/dart_test.yaml new file mode 100644 index 0000000..41a0a3f --- /dev/null +++ b/packages/linux_can/dart_test.yaml @@ -0,0 +1,43 @@ +tags: + double-can: + # A special CAN test setup, consisting of a Raspberry Pi + # and two MCP2515 CAN controllers connected via SPI. + # + # I have connected one controller to SPI0 and one to SPI1, + # so they don't have to share a SPI bus. SPI bus 1 has to + # be explicitly enabled with a user-chosen CS pin, see + # `/boot/config.txt` snippet below. + # + # The tests assume: + # Interface 3 is can0, Interface 4 is can1. + # Both CAN interfaces are on the same CAN bus, + # with no other device on the connected to that bus. + # + # The dtoverlay setup part in `/boot/config.txt` looks like this: + # ``` + # dtoverlay=mcp2515,spi0-0,interrupt=6 + # + # # spi1 needs to be explicitly enabled + # dtoverlay=spi1-1cs,cs0_pin=18,cs0_spidev=disabled + # dtoverlay=mcp2515,spi1-0,interrupt=5 + # ``` + # + # Both CAN interfaces are up with a bitrate of 125000, + # and can0 should have a transmit queue length of at least 1024: + # ``` + # sudo ip link set can0 type can bitrate 125000 + # sudo ip link set can1 type can bitrate 125000 + # sudo ip link set can0 up + # sudo ip link set can1 up + # sudo ip link set can0 txqueuelen 1024 + # ``` + + vcan: + # A special CAN test setup, requires an up & running vcan (Virtual CAN) + # interface called vcan0. + # + # ``` + # sudo modprobe vcan + # sudo ip link add dev vcan0 type vcan + # sudo ip link set up vcan0 + # ``` diff --git a/packages/linux_can/integration_test/can_test.dart b/packages/linux_can/integration_test/can_test.dart new file mode 100644 index 0000000..f938e3c --- /dev/null +++ b/packages/linux_can/integration_test/can_test.dart @@ -0,0 +1,1186 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:test/test.dart' hide test; +import 'package:test/test.dart' as pkg_test show test; +import 'package:linux_can/linux_can.dart'; + +void main() { + pkg_test.test('LinuxCan.instance.devices returns normally', () { + expect(() => LinuxCan.instance.devices, returnsNormally); + }, tags: 'double-can'); + + group('', () { + late List devices; + + setUp(() { + devices = LinuxCan.instance.devices; + }); + + group('Real CAN Hardware', () { + void test( + String description, + FutureOr Function() callback, { + bool? skip, + Timeout? timeout, + }) { + return pkg_test.test( + description, + callback, + skip: skip, + timeout: timeout, + tags: 'double-can', + ); + } + + test('LinuxCan.instance.devices', () { + expect(devices, hasLength(2)); + + expect(devices[0].networkInterface.index, equals(3)); + expect(devices[0].networkInterface.name, equals('can0')); + expect(devices[1].networkInterface.index, equals(4)); + expect(devices[1].networkInterface.name, equals('can1')); + }); + + group('CanDevice', () { + late CanDevice can0; + late CanDevice can1; + + setUp(() { + can0 = devices.singleWhere( + (device) => device.networkInterface.name == 'can0', + ); + can1 = devices.singleWhere( + (device) => device.networkInterface.name == 'can1', + ); + }); + + test('can0 queryAttributes', () { + final attributes = can0.queryAttributes(); + + expect( + attributes.interfaceFlags, + equals({ + NetInterfaceFlag.up, + NetInterfaceFlag.running, + NetInterfaceFlag.noArp, + NetInterfaceFlag.lowerUp, + NetInterfaceFlag.echo, + }), + ); + expect(attributes.txQueueLength, greaterThanOrEqualTo(1024)); + expect(attributes.operState, equals(NetInterfaceOperState.up)); + + /// TODO: Implement stats + expect(attributes.stats, anything); + expect(attributes.numTxQueues, equals(1)); + expect(attributes.numRxQueues, equals(1)); + + expect(attributes.bitTiming?.bitrate, equals(125000)); + expect(attributes.bitTimingLimits?.hardwareName, equals('mcp251x')); + expect(attributes.bitTimingLimits?.timeSegment1Min, equals(3)); + expect(attributes.bitTimingLimits?.timeSegment1Max, equals(16)); + expect(attributes.bitTimingLimits?.timeSegment2Min, equals(2)); + expect(attributes.bitTimingLimits?.timeSegment2Max, equals(8)); + expect(attributes.bitTimingLimits?.bitRatePrescalerMin, equals(1)); + expect(attributes.bitTimingLimits?.bitRatePrescalerMax, equals(64)); + expect( + attributes.bitTimingLimits?.bitRatePrescalerIncrement, + equals(1), + ); + + expect(attributes.clockFrequency, equals(8000000)); + expect(attributes.state, equals(CanState.active)); + expect(attributes.restartDelay, equals(Duration.zero)); + expect(attributes.busErrorCounters, isNull); + expect(attributes.dataBitTiming, isNull); + expect(attributes.dataBitTimingLimits, isNull); + expect(attributes.termination, isNull); + expect(attributes.supportedTerminations, isNull); + expect(attributes.supportedBitrates, isNull); + expect(attributes.supportedDataBitrates, isNull); + expect(attributes.maxBitrate, equals(0)); + }); + + test('can1 queryAttributes', () { + final attributes = can1.queryAttributes(); + + expect( + attributes.interfaceFlags, + equals({ + NetInterfaceFlag.up, + NetInterfaceFlag.running, + NetInterfaceFlag.noArp, + NetInterfaceFlag.lowerUp, + NetInterfaceFlag.echo, + }), + ); + expect(attributes.txQueueLength, greaterThanOrEqualTo(1024)); + expect(attributes.operState, equals(NetInterfaceOperState.up)); + + /// TODO: Implement stats + expect(attributes.stats, anything); + expect(attributes.numTxQueues, equals(1)); + expect(attributes.numRxQueues, equals(1)); + + expect(attributes.bitTiming?.bitrate, equals(125000)); + expect(attributes.bitTimingLimits?.hardwareName, equals('mcp251x')); + expect(attributes.bitTimingLimits?.timeSegment1Min, equals(3)); + expect(attributes.bitTimingLimits?.timeSegment1Max, equals(16)); + expect(attributes.bitTimingLimits?.timeSegment2Min, equals(2)); + expect(attributes.bitTimingLimits?.timeSegment2Max, equals(8)); + expect(attributes.bitTimingLimits?.bitRatePrescalerMin, equals(1)); + expect(attributes.bitTimingLimits?.bitRatePrescalerMax, equals(64)); + expect( + attributes.bitTimingLimits?.bitRatePrescalerIncrement, + equals(1), + ); + + expect(attributes.clockFrequency, equals(8000000)); + expect(attributes.state, equals(CanState.active)); + expect(attributes.restartDelay, equals(const Duration(seconds: 10))); + expect(attributes.busErrorCounters, isNull); + expect(attributes.dataBitTiming, isNull); + expect(attributes.dataBitTimingLimits, isNull); + expect(attributes.termination, isNull); + expect(attributes.supportedTerminations, isNull); + expect(attributes.supportedBitrates, isNull); + expect(attributes.supportedDataBitrates, isNull); + expect(attributes.maxBitrate, equals(0)); + }); + + test('opening & closing can0 device', () { + late CanSocket socket; + expect(() => socket = can0.open(), returnsNormally); + expect(() => socket.close(), returnsNormally); + }); + + test('opening & closing can1 device', () { + late CanSocket socket; + expect(() => socket = can1.open(), returnsNormally); + expect(() => socket.close(), returnsNormally); + }); + }); + + group('CanSocket', () { + late CanSocket can0; + late CanSocket can1; + + setUp(() { + can0 = devices + .singleWhere( + (device) => device.networkInterface.name == 'can0', + ) + .open(); + can1 = devices + .singleWhere( + (device) => device.networkInterface.name == 'can1', + ) + .open(); + }); + + tearDown(() { + can0.close(); + can1.close(); + }); + + test('can0 send buf size', () { + expect(can0.sendBufSize, equals(26 * 4096)); + }); + + test('writing standard CAN frame to can0', () { + expect( + () => can0.send( + CanFrame.standard(id: 0x123, data: [0x01, 0x02, 0x03, 0x04]), + ), + returnsNormally, + ); + }); + + test( + 'writing empty standard CAN frame to can0 and reading from can1', + () async { + final sentFrame = CanFrame.standard(id: 0x120, data: []); + + final receivedFrame = can1.receiveSingle(); + + await can0.send(sentFrame); + await expectLater(receivedFrame, completion(equals(sentFrame))); + }, + ); + + test( + 'writing full-length CAN frame to can0 and reading from can1', + () async { + final sentFrame = CanFrame.standard( + id: 0x120, + data: [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08], + ); + + final receivedFrame = can1.receiveSingle(); + + await can0.send(sentFrame); + await expectLater(receivedFrame, completion(equals(sentFrame))); + }, + ); + + test( + 'writing standard CAN frame to can0 and reading from can1', + () async { + late CanFrame frame; + + final future = can1.receiveSingle().then( + (received) => frame = received, + ); + + can0.send( + CanFrame.standard(id: 0x123, data: [0x01, 0x02, 0x03, 0x04]), + ); + + await expectLater(future, completes); + + expect(frame, isNotNull); + expect(frame, isA()); + + final dataFrame = frame as CanStandardDataFrame; + expect(dataFrame.id, equals(0x123)); + expect(dataFrame.data, hasLength(4)); + expect(dataFrame.data, equals([0x01, 0x02, 0x03, 0x04])); + }, + ); + + test( + 'writing standard CAN frame to can1 and reading from can0', + () async { + late CanFrame frame; + + final future = can0.receiveSingle().then( + (received) => frame = received, + ); + + can1.send( + CanFrame.standard(id: 0x123, data: [0x01, 0x02, 0x03, 0x04]), + ); + + await expectLater(future, completes); + + expect(frame, isNotNull); + expect(frame, isA()); + + final dataFrame = frame as CanStandardDataFrame; + expect(dataFrame.id, equals(0x123)); + expect(dataFrame.data, hasLength(4)); + expect(dataFrame.data, equals([0x01, 0x02, 0x03, 0x04])); + }, + ); + + test('waiting for data frame on can1', () async { + // FIXME: Maybe empty receive queues here + + final completer = Completer(); + + can1.receiveSingle().then( + (frame) => completer.complete(frame), + onError: (err, st) => completer.completeError(err, st), + ); + + await Future.delayed(const Duration(seconds: 2)); + + expect(completer.isCompleted, isFalse); + + final frame = CanFrame.standard( + id: 0x124, + data: [0x01, 0x02, 0x03, 0x04], + ); + + can0.send(frame); + + await expectLater( + completer.future.timeout(const Duration(seconds: 5)), + completion(frame), + ); + }); + + test('writing lots of frames to can0 and reading from can1', () async { + final sentFrames = List.generate(1024, (i) { + return CanFrame.standard(id: 0x123, data: [i & 0xFF, i >> 8]); + }); + + late List receivedFrames; + final receivedFramesFuture = can1.receive().take(1024).toList().then(( + frames, + ) { + receivedFrames = frames; + }); + + // write all frames to the bus. + // requires a sufficiently big tx queue + sentFrames.forEach(can0.send); + + // wait for all frames to be received + await expectLater( + receivedFramesFuture.timeout(const Duration(seconds: 30)), + completes, + ); + + // check that they match the frames we sent + expect(receivedFrames, containsAll(sentFrames)); + }, skip: true); + + group('kernel filters', () { + test('filtering for single ID emits matching frames', () async { + final frame1 = CanFrame.standard( + id: 0x123, + data: [0x01, 0x02, 0x03, 0x04], + ); + final frame2 = CanFrame.standard( + id: 0x124, + data: [0x01, 0x02, 0x03, 0x04], + ); + + final framesFuture = can1.receiveSingle( + filter: CanFilter.whitelist(const [0x123]), + ); + + can0.send(frame1); + can0.send(frame2); + + await expectLater( + framesFuture.timeout(const Duration(seconds: 10)), + completion(equals(frame1)), + ); + }); + + test( + 'filtering for single ID does not emit non-matching frames', + () async { + final frame1 = CanFrame.standard( + id: 0x123, + data: [0x01, 0x02, 0x03, 0x04], + ); + final frame2 = CanFrame.standard( + id: 0x124, + data: [0x01, 0x02, 0x03, 0x04], + ); + + final frameFuture = can1.receiveSingle( + filter: CanFilter.whitelist(const [0x124]), + ); + + can0.send(frame1); + can0.send(frame2); + + await expectLater( + frameFuture.timeout(const Duration(seconds: 10)), + completion(equals(frame2)), + ); + }, + ); + + test('filtering for multiple IDs emits matching frames', () async { + final frames = [ + CanFrame.standard(id: 0x123, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x124, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x125, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x126, data: [0x01, 0x02, 0x03, 0x04]), + ]; + + final frameFuture = can1.receive(filter: CanFilter.whitelist(const [0x123, 0x124])).take(2).toList(); + + frames.forEach(can0.send); + + await expectLater( + frameFuture.timeout(const Duration(seconds: 10)), + completion(containsAll(frames.take(2))), + ); + }); + + test( + 'filtering for multiple IDs does not emit non-matching frames', + () async { + final frames = [ + CanFrame.standard(id: 0x123, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x124, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x125, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x126, data: [0x01, 0x02, 0x03, 0x04]), + ]; + + final frameFuture = can1 + .receive( + filter: CanFilter.whitelist(const [0x125, 0x126]), + ) + .take(2) + .toList(); + + frames.forEach(can0.send); + + await expectLater( + frameFuture.timeout(const Duration(seconds: 10)), + completion(containsAll(frames.skip(2))), + ); + }, + ); + + test( + 'receiving two streams simulatenously emits same frames on both streams', + () async { + final frames = [ + CanFrame.standard(id: 0x126, data: [0x01, 0x02, 0x03, 0x04]), + ]; + + final frameFuture1 = can1.receiveSingle(); + + final frameFuture2 = can1.receiveSingle(); + + frames.forEach(can0.send); + + await expectLater( + frameFuture1.timeout(const Duration(seconds: 10)), + completion(anyOf(frames)), + ); + await expectLater( + frameFuture2.timeout(const Duration(seconds: 10)), + completion(anyOf(frames)), + ); + }, + ); + + test( + 'receiving two streams simulatenously with different filters', + () async { + final frames = [ + CanFrame.standard(id: 0x11F, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x120, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x121, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x122, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x123, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x124, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x125, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x126, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x127, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x128, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x129, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x12A, data: [0x01, 0x02, 0x03, 0x04]), + ]; + + final frameFuture1 = can1.receive(filter: CanFilter.whitelist([0x123, 0x124])).take(2).toList(); + + final frameFuture2 = can1.receive(filter: CanFilter.whitelist([0x128, 0x129])).take(2).toList(); + + frames.forEach(can0.send); + + await expectLater( + frameFuture1.timeout(const Duration(seconds: 10)), + completion( + containsAll([ + predicate((f) => f is CanFrame && f.id == 0x123), + predicate((f) => f is CanFrame && f.id == 0x124), + ]), + ), + ); + + await expectLater( + frameFuture2.timeout(const Duration(seconds: 10)), + completion( + containsAll([ + predicate((f) => f is CanFrame && f.id == 0x128), + predicate((f) => f is CanFrame && f.id == 0x129), + ]), + ), + ); + }, + ); + + test('blacklisting single ID works', () async { + final frames = [ + CanFrame.standard(id: 0x123, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x124, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x125, data: [0x01, 0x02, 0x03, 0x04]), + ]; + + final framesFuture = can1 + .receive( + emitErrors: true, + filter: CanFilter.blacklist(const [0x124]), + ) + .take(2) + .toList(); + + frames.forEach(can0.send); + + await expectLater( + framesFuture.timeout(const Duration(seconds: 10)), + completion(containsAll([frames[0], frames[2]])), + ); + }); + + test('in-kernel SFF filter matches emulated SFF filter', () async { + final filter = CanFilter.or([ + CanFilter.or([ + const CanFilter.idEquals( + 0x101, + mask: 0x080, + formats: CanFrameFormat.both, + ), + const CanFilter.idEquals( + 0x040, + mask: 0x030, + formats: {CanFrameFormat.base}, + types: {CanFrameType.remote}, + ), + const CanFilter.not( + CanFilter.idEquals(0x123, formats: {CanFrameFormat.base}), + ), + CanFilter.blacklist([0x124]), + ]), + ]); + + final frames = Iterable.generate(2048 * 2, (index) { + if (index < 2048) { + return CanFrame.standard(id: index, data: [0]); + } else { + return CanFrame.standardRemote(id: index - 2048); + } + }); + + final matchingEmulated = frames.where( + (frame) => filter.matches(frame), + ); + + final received = can1.receive(emitErrors: true, filter: filter).take(matchingEmulated.length).toList(); + + frames.forEach(can0.send); + + await expectLater( + received.timeout(const Duration(seconds: 30)), + completion(matchingEmulated), + ); + }, skip: true); + + test('in-kernel EFF filter matches emulated EFF filter', () async { + const filter = CanFilter.or([ + CanFilter.or([ + CanFilter.idEquals( + 0x101, + mask: 0x080, + formats: CanFrameFormat.both, + ), + CanFilter.idEquals( + 0x04024, + mask: 0x03024, + formats: {CanFrameFormat.extended}, + types: {CanFrameType.remote}, + ), + CanFilter.not( + CanFilter.idEquals( + 0x1, + mask: 0x1, + formats: {CanFrameFormat.extended}, + ), + ), + CanFilter.notIdEquals( + 0x2, + mask: 0x2, + formats: {CanFrameFormat.extended}, + ), + CanFilter.notIdEquals( + 0x4, + mask: 0x4, + formats: {CanFrameFormat.extended}, + ), + CanFilter.notIdEquals( + 0x8, + mask: 0x8, + formats: {CanFrameFormat.extended}, + ), + ]), + ]); + + final frames = Iterable.generate(pow(2, 13).toInt(), (index) { + if (index & 1 == 0) { + return CanFrame.extended(id: index >> 1, data: [0]); + } else { + return CanFrame.extendedRemote(id: index >> 1); + } + }); + + final matchingEmulated = frames.where( + (frame) => filter.matches(frame), + ); + + final received = can1.receive(emitErrors: true, filter: filter).take(matchingEmulated.length).toList(); + + frames.forEach(can0.send); + + await expectLater( + received.timeout(const Duration(seconds: 30)), + completion(equals(matchingEmulated)), + ); + }, skip: true); + }); + }); + }); + + group('Virtual CAN', () { + void test( + String description, + FutureOr Function() callback, { + bool? skip, + Timeout? timeout, + }) { + return pkg_test.test( + description, + callback, + skip: skip, + timeout: timeout, + tags: 'vcan', + ); + } + + test('LinuxCan.instance.devices', () { + expect(devices, hasLength(1)); + + // expect(devices[0].networkInterface.index, equals(3)); + expect(devices[0].networkInterface.name, equals('vcan0')); + }); + + group('CanDevice', () { + late CanDevice vcan; + + setUp(() { + vcan = devices.singleWhere( + (device) => device.networkInterface.name == 'vcan0', + ); + }); + + test('queryAttributes', () { + final attributes = vcan.queryAttributes(); + + expect( + attributes.interfaceFlags, + equals({ + NetInterfaceFlag.up, + NetInterfaceFlag.running, + NetInterfaceFlag.noArp, + NetInterfaceFlag.lowerUp, + }), + ); + expect(attributes.txQueueLength, greaterThanOrEqualTo(1000)); + + expect(attributes.operState, equals(NetInterfaceOperState.unknown)); + + /// TODO: Implement stats + expect(attributes.stats, anything); + expect(attributes.numTxQueues, equals(1)); + expect(attributes.numRxQueues, equals(1)); + + expect(attributes.bitTiming?.bitrate, isNull); + expect(attributes.bitTimingLimits?.hardwareName, isNull); + expect(attributes.bitTimingLimits?.timeSegment1Min, isNull); + expect(attributes.bitTimingLimits?.timeSegment1Max, isNull); + expect(attributes.bitTimingLimits?.timeSegment2Min, isNull); + expect(attributes.bitTimingLimits?.timeSegment2Max, isNull); + expect(attributes.bitTimingLimits?.bitRatePrescalerMin, isNull); + expect(attributes.bitTimingLimits?.bitRatePrescalerMax, isNull); + expect(attributes.bitTimingLimits?.bitRatePrescalerIncrement, isNull); + + expect(attributes.clockFrequency, isNull); + expect(attributes.state, isNull); + expect(attributes.restartDelay, isNull); + expect(attributes.busErrorCounters, isNull); + expect(attributes.dataBitTiming, isNull); + expect(attributes.dataBitTimingLimits, isNull); + expect(attributes.termination, isNull); + expect(attributes.supportedTerminations, isNull); + expect(attributes.supportedBitrates, isNull); + expect(attributes.supportedDataBitrates, isNull); + expect(attributes.maxBitrate, isNull); + }); + }); + + group('CanSocket', () { + late CanSocket vcan0; + late CanSocket vcan1; + + setUp(() { + vcan0 = devices + .singleWhere( + (device) => device.networkInterface.name == 'vcan0', + ) + .open(); + vcan1 = devices + .singleWhere( + (device) => device.networkInterface.name == 'vcan0', + ) + .open(); + }); + + tearDown(() { + vcan1.close(); + vcan0.close(); + }); + + test('send buf size', () { + expect(vcan0.sendBufSize, equals(26 * 4096)); + expect(vcan1.sendBufSize, equals(26 * 4096)); + }); + + test('writing standard CAN frame', () async { + await expectLater( + vcan0.send( + CanFrame.standard(id: 0x123, data: [0x01, 0x02, 0x03, 0x04]), + ), + completes, + ); + }); + + group('sending and receiving on different socket', () { + test('empty SFF data frame', () async { + final sentFrame = CanFrame.standard(id: 0x120, data: []); + + final frameFuture = vcan0.receiveSingle(emitErrors: true); + + vcan1.send(sentFrame); + + await expectLater(frameFuture, completion(equals(sentFrame))); + }, timeout: const Timeout(Duration(seconds: 10))); + + test( + 'full-length SFF data frame', + () async { + final sentFrame = CanFrame.standard( + id: 0x120, + data: [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08], + ); + + final frameFuture = vcan1.receiveSingle(emitErrors: true); + + vcan0.send(sentFrame); + + await expectLater(frameFuture, completion(equals(sentFrame))); + }, + timeout: const Timeout(Duration(seconds: 10)), + ); + + test('SFF data frame', () async { + late CanFrame frame; + + final future = vcan1.receiveSingle(emitErrors: true).then((received) => frame = received); + + vcan0.send( + CanFrame.standard(id: 0x123, data: [0x01, 0x02, 0x03, 0x04]), + ); + + await expectLater(future, completes); + + expect(frame, isNotNull); + expect(frame, isA()); + + final dataFrame = frame as CanStandardDataFrame; + expect(dataFrame.id, equals(0x123)); + expect(dataFrame.data, hasLength(4)); + expect(dataFrame.data, equals([0x01, 0x02, 0x03, 0x04])); + }, timeout: const Timeout(Duration(seconds: 10))); + + test('SFF data frame 2', () async { + late CanFrame frame; + + final future = vcan0.receiveSingle(emitErrors: true).then((received) => frame = received); + + vcan1.send( + CanFrame.standard(id: 0x123, data: [0x01, 0x02, 0x03, 0x04]), + ); + + await expectLater(future, completes); + + expect(frame, isNotNull); + expect(frame, isA()); + + final dataFrame = frame as CanStandardDataFrame; + expect(dataFrame.id, equals(0x123)); + expect(dataFrame.data, hasLength(4)); + expect(dataFrame.data, equals([0x01, 0x02, 0x03, 0x04])); + }, timeout: const Timeout(Duration(seconds: 10))); + + test('SFF RTR frame', () async { + late CanFrame frame; + + final future = + vcan0.receiveSingle(emitErrors: true, filter: CanFilter.any).then((received) => frame = received); + + vcan1.send(CanFrame.standardRemote(id: 0x123)); + + await expectLater(future, completes); + + expect(frame, isNotNull); + expect(frame, isA()); + + final dataFrame = frame as CanStandardRemoteFrame; + expect(dataFrame.id, equals(0x123)); + }, timeout: const Timeout(Duration(seconds: 10))); + + test('EFF data frame', () async { + late CanFrame frame; + + final future = vcan0.receiveSingle(emitErrors: true).then((received) => frame = received); + + vcan1.send( + CanFrame.extended(id: 0x123, data: [0x01, 0x02, 0x03, 0x04]), + ); + + await expectLater(future, completes); + + expect(frame, isNotNull); + expect(frame, isA()); + + final dataFrame = frame as CanExtendedDataFrame; + expect(dataFrame.id, equals(0x123)); + expect(dataFrame.data, hasLength(4)); + expect(dataFrame.data, equals([0x01, 0x02, 0x03, 0x04])); + }, timeout: const Timeout(Duration(seconds: 10))); + + test('EFF RTR frame', () async { + late CanFrame frame; + + final future = + vcan0.receiveSingle(emitErrors: true, filter: CanFilter.any).then((received) => frame = received); + + vcan1.send(CanFrame.extendedRemote(id: 0x123)); + + await expectLater(future, completes); + + expect(frame, isNotNull); + expect(frame, isA()); + + final dataFrame = frame as CanExtendedRemoteFrame; + expect(dataFrame.id, equals(0x123)); + }, timeout: const Timeout(Duration(seconds: 10))); + + test('lots of frames', () async { + final sentFrames = List.generate(1024, (i) { + return CanFrame.standard(id: 0x123, data: [i & 0xFF, i >> 8]); + }); + + late List receivedFrames; + final receivedFramesFuture = vcan1.receive().take(1024).toList().then((frames) { + receivedFrames = frames; + }); + + // write all frames to the bus. + // requires a sufficiently big tx queue + sentFrames.forEach(vcan0.send); + + // wait for all frames to be received + await expectLater( + receivedFramesFuture.timeout(const Duration(seconds: 30)), + completes, + ); + + // check that they match the frames we sent + expect(receivedFrames, containsAll(sentFrames)); + }, timeout: const Timeout(Duration(seconds: 40))); + }); + + group('kernel filters', () { + test('filtering for single ID emits matching frames', () async { + final frame1 = CanFrame.standard( + id: 0x123, + data: [0x01, 0x02, 0x03, 0x04], + ); + final frame2 = CanFrame.standard( + id: 0x124, + data: [0x01, 0x02, 0x03, 0x04], + ); + + final framesFuture = vcan1.receiveSingle( + filter: CanFilter.whitelist(const [0x123]), + ); + + vcan0.send(frame1); + vcan0.send(frame2); + + await expectLater( + framesFuture.timeout(const Duration(seconds: 10)), + completion(equals(frame1)), + ); + }); + + test( + 'filtering for single ID does not emit non-matching frames', + () async { + final frame1 = CanFrame.standard( + id: 0x123, + data: [0x01, 0x02, 0x03, 0x04], + ); + final frame2 = CanFrame.standard( + id: 0x124, + data: [0x01, 0x02, 0x03, 0x04], + ); + + final frameFuture = vcan1.receiveSingle( + filter: CanFilter.whitelist(const [0x124]), + ); + + vcan0.send(frame1); + vcan0.send(frame2); + + await expectLater( + frameFuture.timeout(const Duration(seconds: 10)), + completion(equals(frame2)), + ); + }, + ); + + test('filtering for multiple IDs emits matching frames', () async { + final frames = [ + CanFrame.standard(id: 0x123, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x124, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x125, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x126, data: [0x01, 0x02, 0x03, 0x04]), + ]; + + final frameFuture = vcan1.receive(filter: CanFilter.whitelist(const [0x123, 0x124])).take(2).toList(); + + frames.forEach(vcan0.send); + + await expectLater( + frameFuture.timeout(const Duration(seconds: 10)), + completion(containsAll(frames.take(2))), + ); + }); + + test( + 'filtering for multiple IDs does not emit non-matching frames', + () async { + final frames = [ + CanFrame.standard(id: 0x123, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x124, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x125, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x126, data: [0x01, 0x02, 0x03, 0x04]), + ]; + + final frameFuture = vcan1 + .receive( + filter: CanFilter.whitelist(const [0x125, 0x126]), + ) + .take(2) + .toList(); + + frames.forEach(vcan0.send); + + await expectLater( + frameFuture.timeout(const Duration(seconds: 10)), + completion(containsAll(frames.skip(2))), + ); + }, + ); + + test( + 'receiving two streams simulatenously emits same frames on both streams', + () async { + final frames = [ + CanFrame.standard(id: 0x126, data: [0x01, 0x02, 0x03, 0x04]), + ]; + + final frameFuture1 = vcan1.receiveSingle(); + + final frameFuture2 = vcan1.receiveSingle(); + + frames.forEach(vcan0.send); + + await expectLater( + frameFuture1.timeout(const Duration(seconds: 10)), + completion(anyOf(frames)), + ); + await expectLater( + frameFuture2.timeout(const Duration(seconds: 10)), + completion(anyOf(frames)), + ); + }, + ); + + test( + 'receiving two streams simulatenously with different filters', + () async { + final frames = [ + CanFrame.standard(id: 0x11F, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x120, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x121, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x122, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x123, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x124, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x125, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x126, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x127, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x128, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x129, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x12A, data: [0x01, 0x02, 0x03, 0x04]), + ]; + + final frameFuture1 = vcan1.receive(filter: CanFilter.whitelist([0x123, 0x124])).take(2).toList(); + + final frameFuture2 = vcan1.receive(filter: CanFilter.whitelist([0x128, 0x129])).take(2).toList(); + + frames.forEach(vcan0.send); + + await expectLater( + frameFuture1.timeout(const Duration(seconds: 10)), + completion( + containsAll([ + predicate((f) => f is CanFrame && f.id == 0x123), + predicate((f) => f is CanFrame && f.id == 0x124), + ]), + ), + ); + + await expectLater( + frameFuture2.timeout(const Duration(seconds: 10)), + completion( + containsAll([ + predicate((f) => f is CanFrame && f.id == 0x128), + predicate((f) => f is CanFrame && f.id == 0x129), + ]), + ), + ); + }, + ); + + test('blacklisting single ID works', () async { + final frames = [ + CanFrame.standard(id: 0x123, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x124, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x125, data: [0x01, 0x02, 0x03, 0x04]), + ]; + + final framesFuture = vcan1 + .receive( + emitErrors: true, + filter: CanFilter.blacklist(const [0x124]), + ) + .take(2) + .toList(); + + frames.forEach(vcan0.send); + + await expectLater( + framesFuture.timeout(const Duration(seconds: 10)), + completion(containsAll([frames[0], frames[2]])), + ); + }); + + test('in-kernel SFF filter matches emulated SFF filter', () async { + final filter = CanFilter.or([ + CanFilter.or([ + const CanFilter.idEquals( + 0x101, + mask: 0x080, + formats: CanFrameFormat.both, + ), + const CanFilter.idEquals( + 0x040, + mask: 0x030, + formats: {CanFrameFormat.base}, + types: {CanFrameType.remote}, + ), + const CanFilter.not( + CanFilter.idEquals(0x123, formats: {CanFrameFormat.base}), + ), + CanFilter.blacklist([0x124]), + ]), + ]); + + final frames = Iterable.generate(2048 * 2, (index) { + if (index < 2048) { + return CanFrame.standard(id: index, data: [0]); + } else { + return CanFrame.standardRemote(id: index - 2048); + } + }); + + final matchingEmulated = frames.where( + (frame) => filter.matches(frame), + ); + + final received = vcan1.receive(emitErrors: true, filter: filter).take(matchingEmulated.length).toList(); + + frames.forEach(vcan0.send); + + await expectLater( + received.timeout(const Duration(seconds: 30)), + completion(matchingEmulated), + ); + }); + + test('in-kernel EFF filter matches emulated EFF filter', () async { + const filter = CanFilter.or([ + CanFilter.or([ + CanFilter.idEquals( + 0x101, + mask: 0x080, + formats: CanFrameFormat.both, + ), + CanFilter.idEquals( + 0x04024, + mask: 0x03024, + formats: {CanFrameFormat.extended}, + types: {CanFrameType.remote}, + ), + CanFilter.not( + CanFilter.idEquals( + 0x1, + mask: 0x1, + formats: {CanFrameFormat.extended}, + ), + ), + CanFilter.notIdEquals( + 0x2, + mask: 0x2, + formats: {CanFrameFormat.extended}, + ), + CanFilter.notIdEquals( + 0x4, + mask: 0x4, + formats: {CanFrameFormat.extended}, + ), + CanFilter.notIdEquals( + 0x8, + mask: 0x8, + formats: {CanFrameFormat.extended}, + ), + ]), + ]); + + final frames = Iterable.generate(pow(2, 20 + 1).toInt(), (index) { + if (index & 1 == 0) { + return CanFrame.extended(id: index >> 1, data: [0]); + } else { + return CanFrame.extendedRemote(id: index >> 1); + } + }); + + final matchingEmulated = frames.where( + (frame) => filter.matches(frame), + ); + + final received = vcan1.receive(emitErrors: true, filter: filter).take(matchingEmulated.length).toList(); + + frames.forEach(vcan0.send); + + await expectLater( + received.timeout(const Duration(seconds: 30)), + completion(equals(matchingEmulated)), + ); + }); + }); + }); + }); + }); + + pkg_test.test('Event Listener Isolate terminates gracefully', () async { + late Future future; + expect(() { + future = LinuxCan.instance.interface.dispose(); + }, returnsNormally); + await expectLater(future, completes); + }, tags: ['double-can', 'vcan']); +} diff --git a/packages/linux_can/lib/src/can_device.dart b/packages/linux_can/lib/src/can_device.dart index 0f5885d..ae7056a 100644 --- a/packages/linux_can/lib/src/can_device.dart +++ b/packages/linux_can/lib/src/can_device.dart @@ -69,10 +69,11 @@ class CanDevice { /// True if the network interface is up and running. bool get isUp => switch (operationalState) { - NetInterfaceOperState.up => true, - NetInterfaceOperState.unknown => interfaceFlags.containsAll({NetInterfaceFlag.up, NetInterfaceFlag.running}), - _ => false, - }; + NetInterfaceOperState.up => true, + NetInterfaceOperState.unknown => interfaceFlags.containsAll({NetInterfaceFlag.up, NetInterfaceFlag.running}), + _ => false, + }; + /// Some general statistics for this network interface. /// /// Not yet implemented. @@ -458,17 +459,20 @@ class CanSocket implements Sink { onListen: () { // we don't need to drain here since the filter was // set to CanFilter.none directly after opening. - _socketListen( - (frames) { + _socketListen((frames) { + if (_listening) { frames?.forEach((frame) { frame.either( (errors) => errors.forEach(_socketController.addError), _socketController.add, ); }); - }, - _socketController.addError, - ); + } + }, (err, st) { + if (_listening) { + _socketController.addError(err, st); + } + }); }, onCancel: () { if (_listening) { diff --git a/packages/linux_can/lib/src/data_classes.dart b/packages/linux_can/lib/src/data_classes.dart index 7ccc68f..dc2a81f 100644 --- a/packages/linux_can/lib/src/data_classes.dart +++ b/packages/linux_can/lib/src/data_classes.dart @@ -590,7 +590,8 @@ class CanExtendedRemoteFrame extends CanFrame implements CanLegacyFrame, CanExte class CanFdBaseFrame extends CanFrame implements CanBaseFrame, CanDataFrame, CanFdFrame { const CanFdBaseFrame({required this.id, required this.data, required this.flags}) - : assert((id & ~CAN_SFF_MASK == 0) && 0 <= data.length && + : assert((id & ~CAN_SFF_MASK == 0) && + 0 <= data.length && data.length <= 64 && (data.length <= 8 || data.length == 12 || diff --git a/packages/linux_can/lib/src/linux_can.dart b/packages/linux_can/lib/src/linux_can.dart index c98717b..34b7d8f 100644 --- a/packages/linux_can/lib/src/linux_can.dart +++ b/packages/linux_can/lib/src/linux_can.dart @@ -1,11 +1,15 @@ import 'package:linux_can/src/can_device.dart'; import 'package:linux_can/src/platform_interface.dart'; +import 'package:meta/meta.dart'; class LinuxCan { LinuxCan._(); final PlatformInterface _interface = PlatformInterface(); + @visibleForTesting + PlatformInterface get interface => _interface; + static final LinuxCan instance = LinuxCan._(); List get devices { diff --git a/packages/linux_can/lib/src/platform_interface.dart b/packages/linux_can/lib/src/platform_interface.dart index 75e2f7f..2691edc 100644 --- a/packages/linux_can/lib/src/platform_interface.dart +++ b/packages/linux_can/lib/src/platform_interface.dart @@ -11,6 +11,7 @@ import 'package:either_dart/either.dart'; import 'package:ffi/ffi.dart' as ffi; import 'package:linux_can/src/can_device.dart'; import 'package:linux_can/src/data_classes.dart'; +import 'package:meta/meta.dart'; void _writeStringToArrayHelper( String str, @@ -1405,4 +1406,9 @@ class PlatformInterface { } }); } + + @visibleForTesting + Future dispose() async { + await eventListener.dispose(); + } } diff --git a/packages/linux_can/pubspec.yaml b/packages/linux_can/pubspec.yaml index 12771a0..5cd72a5 100644 --- a/packages/linux_can/pubspec.yaml +++ b/packages/linux_can/pubspec.yaml @@ -15,4 +15,5 @@ dependencies: collection: ^1.17.0 either_dart: ^1.0.0 ffi: ^2.0.1 + meta: ^1.16.0 quiver: ^3.2.1 diff --git a/packages/linux_can_test_app/dart_test.yaml b/packages/linux_can_test_app/dart_test.yaml index 1a99013..41a0a3f 100644 --- a/packages/linux_can_test_app/dart_test.yaml +++ b/packages/linux_can_test_app/dart_test.yaml @@ -1,6 +1,6 @@ tags: - pi3-can: - # A special CAN test setup, consisting of a Raspberry Pi 3 + double-can: + # A special CAN test setup, consisting of a Raspberry Pi # and two MCP2515 CAN controllers connected via SPI. # # I have connected one controller to SPI0 and one to SPI1, diff --git a/packages/linux_can_test_app/integration_test/can_test.dart b/packages/linux_can_test_app/integration_test/can_test.dart index 3d8d932..368fbbb 100644 --- a/packages/linux_can_test_app/integration_test/can_test.dart +++ b/packages/linux_can_test_app/integration_test/can_test.dart @@ -7,11 +7,8 @@ import 'package:linux_can/linux_can.dart'; void main() { test.testWidgets('LinuxCan.instance.devices returns normally', (_) async { - expect( - () => LinuxCan.instance.devices, - returnsNormally, - ); - }, tags: 'pi3-can'); + expect(() => LinuxCan.instance.devices, returnsNormally); + }, tags: 'double-can'); group('', () { late List devices; @@ -36,7 +33,7 @@ void main() { timeout: timeout, semanticsEnabled: semanticsEnabled, variant: variant, - tags: 'pi3-can', + tags: 'double-can', ); } @@ -54,8 +51,12 @@ void main() { late CanDevice can1; setUp(() { - can0 = devices.singleWhere((device) => device.networkInterface.name == 'can0'); - can1 = devices.singleWhere((device) => device.networkInterface.name == 'can1'); + can0 = devices.singleWhere( + (device) => device.networkInterface.name == 'can0', + ); + can1 = devices.singleWhere( + (device) => device.networkInterface.name == 'can1', + ); }); testWidgets('can0 queryAttributes', (_) async { @@ -68,10 +69,10 @@ void main() { NetInterfaceFlag.running, NetInterfaceFlag.noArp, NetInterfaceFlag.lowerUp, - NetInterfaceFlag.echo + NetInterfaceFlag.echo, }), ); - expect(attributes.txQueueLength, greaterThanOrEqualTo(1000)); + expect(attributes.txQueueLength, greaterThanOrEqualTo(1024)); expect(attributes.operState, equals(NetInterfaceOperState.up)); /// TODO: Implement stats @@ -87,7 +88,10 @@ void main() { expect(attributes.bitTimingLimits?.timeSegment2Max, equals(8)); expect(attributes.bitTimingLimits?.bitRatePrescalerMin, equals(1)); expect(attributes.bitTimingLimits?.bitRatePrescalerMax, equals(64)); - expect(attributes.bitTimingLimits?.bitRatePrescalerIncrement, equals(1)); + expect( + attributes.bitTimingLimits?.bitRatePrescalerIncrement, + equals(1), + ); expect(attributes.clockFrequency, equals(8000000)); expect(attributes.state, equals(CanState.active)); @@ -112,10 +116,10 @@ void main() { NetInterfaceFlag.running, NetInterfaceFlag.noArp, NetInterfaceFlag.lowerUp, - NetInterfaceFlag.echo + NetInterfaceFlag.echo, }), ); - expect(attributes.txQueueLength, greaterThanOrEqualTo(1000)); + expect(attributes.txQueueLength, greaterThanOrEqualTo(1024)); expect(attributes.operState, equals(NetInterfaceOperState.up)); /// TODO: Implement stats @@ -131,11 +135,14 @@ void main() { expect(attributes.bitTimingLimits?.timeSegment2Max, equals(8)); expect(attributes.bitTimingLimits?.bitRatePrescalerMin, equals(1)); expect(attributes.bitTimingLimits?.bitRatePrescalerMax, equals(64)); - expect(attributes.bitTimingLimits?.bitRatePrescalerIncrement, equals(1)); + expect( + attributes.bitTimingLimits?.bitRatePrescalerIncrement, + equals(1), + ); expect(attributes.clockFrequency, equals(8000000)); expect(attributes.state, equals(CanState.active)); - expect(attributes.restartDelay, equals(Duration.zero)); + expect(attributes.restartDelay, equals(const Duration(seconds: 10))); expect(attributes.busErrorCounters, isNull); expect(attributes.dataBitTiming, isNull); expect(attributes.dataBitTimingLimits, isNull); @@ -148,26 +155,14 @@ void main() { testWidgets('opening & closing can0 device', (_) async { late CanSocket socket; - expect( - () => socket = can0.open(), - returnsNormally, - ); - expect( - () => socket.close(), - returnsNormally, - ); + expect(() => socket = can0.open(), returnsNormally); + expect(() => socket.close(), returnsNormally); }); testWidgets('opening & closing can1 device', (_) async { late CanSocket socket; - expect( - () => socket = can1.open(), - returnsNormally, - ); - expect( - () => socket.close(), - returnsNormally, - ); + expect(() => socket = can1.open(), returnsNormally); + expect(() => socket.close(), returnsNormally); }); }); @@ -176,8 +171,16 @@ void main() { late CanSocket can1; setUp(() { - can0 = devices.singleWhere((device) => device.networkInterface.name == 'can0').open(); - can1 = devices.singleWhere((device) => device.networkInterface.name == 'can1').open(); + can0 = devices + .singleWhere( + (device) => device.networkInterface.name == 'can0', + ) + .open(); + can1 = devices + .singleWhere( + (device) => device.networkInterface.name == 'can1', + ) + .open(); }); tearDown(() { @@ -186,67 +189,92 @@ void main() { }); testWidgets('can0 send buf size', (_) async { - expect(can0.sendBufSize, equals(22 * 4096)); + expect(can0.sendBufSize, equals(26 * 4096)); }); testWidgets('writing standard CAN frame to can0', (_) async { expect( - () => can0.send(CanFrame.standard(id: 0x123, data: [0x01, 0x02, 0x03, 0x04])), + () => can0.send( + CanFrame.standard(id: 0x123, data: [0x01, 0x02, 0x03, 0x04]), + ), returnsNormally, ); }); - testWidgets('writing empty standard CAN frame to can0 and reading from can1', (_) async { - final sentFrame = CanFrame.standard(id: 0x120, data: []); + testWidgets( + 'writing empty standard CAN frame to can0 and reading from can1', + (_) async { + final sentFrame = CanFrame.standard(id: 0x120, data: []); - expectLater(can1.receiveSingle(), equals(sentFrame)); + expectLater(can1.receiveSingle(), completion(equals(sentFrame))); - can0.send(sentFrame); - }); + can0.send(sentFrame); + }, + ); - testWidgets('writing full-length CAN frame to can0 and reading from can1', (_) async { - final sentFrame = CanFrame.standard(id: 0x120, data: [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]); + testWidgets( + 'writing full-length CAN frame to can0 and reading from can1', + (_) async { + final sentFrame = CanFrame.standard( + id: 0x120, + data: [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08], + ); - expectLater(can1.receiveSingle(), equals(sentFrame)); + expectLater(can1.receiveSingle(), completion(equals(sentFrame))); - can0.send(sentFrame); - }); + can0.send(sentFrame); + }, + ); - testWidgets('writing standard CAN frame to can0 and reading from can1', (_) async { - late CanFrame frame; + testWidgets( + 'writing standard CAN frame to can0 and reading from can1', + (_) async { + late CanFrame frame; - final future = can1.receiveSingle().then((received) => frame = received); + final future = can1.receiveSingle().then( + (received) => frame = received, + ); - can0.send(CanFrame.standard(id: 0x123, data: [0x01, 0x02, 0x03, 0x04])); + can0.send( + CanFrame.standard(id: 0x123, data: [0x01, 0x02, 0x03, 0x04]), + ); - await expectLater(future, completes); + await expectLater(future, completes); - expect(frame, isNotNull); - expect(frame, isA()); + expect(frame, isNotNull); + expect(frame, isA()); - final dataFrame = frame as CanStandardDataFrame; - expect(dataFrame.id, equals(0x123)); - expect(dataFrame.data, hasLength(4)); - expect(dataFrame.data, equals([0x01, 0x02, 0x03, 0x04])); - }); + final dataFrame = frame as CanStandardDataFrame; + expect(dataFrame.id, equals(0x123)); + expect(dataFrame.data, hasLength(4)); + expect(dataFrame.data, equals([0x01, 0x02, 0x03, 0x04])); + }, + ); - testWidgets('writing standard CAN frame to can1 and reading from can0', (_) async { - late CanFrame frame; + testWidgets( + 'writing standard CAN frame to can1 and reading from can0', + (_) async { + late CanFrame frame; - final future = can0.receiveSingle().then((received) => frame = received); + final future = can0.receiveSingle().then( + (received) => frame = received, + ); - can1.send(CanFrame.standard(id: 0x123, data: [0x01, 0x02, 0x03, 0x04])); + can1.send( + CanFrame.standard(id: 0x123, data: [0x01, 0x02, 0x03, 0x04]), + ); - await expectLater(future, completes); + await expectLater(future, completes); - expect(frame, isNotNull); - expect(frame, isA()); + expect(frame, isNotNull); + expect(frame, isA()); - final dataFrame = frame as CanStandardDataFrame; - expect(dataFrame.id, equals(0x123)); - expect(dataFrame.data, hasLength(4)); - expect(dataFrame.data, equals([0x01, 0x02, 0x03, 0x04])); - }); + final dataFrame = frame as CanStandardDataFrame; + expect(dataFrame.id, equals(0x123)); + expect(dataFrame.data, hasLength(4)); + expect(dataFrame.data, equals([0x01, 0x02, 0x03, 0x04])); + }, + ); testWidgets('waiting for data frame on can1', (_) async { // FIXME: Maybe empty receive queues here @@ -262,23 +290,30 @@ void main() { expect(completer.isCompleted, isFalse); - final frame = CanFrame.standard(id: 0x124, data: [0x01, 0x02, 0x03, 0x04]); + final frame = CanFrame.standard( + id: 0x124, + data: [0x01, 0x02, 0x03, 0x04], + ); can0.send(frame); - await expectLater(completer.future.timeout(const Duration(seconds: 5)), completion(frame)); + await expectLater( + completer.future.timeout(const Duration(seconds: 5)), + completion(frame), + ); }); - testWidgets('writing lots of frames to can0 and reading from can1', (_) async { - final sentFrames = List.generate( - 1024, - (i) { - return CanFrame.standard(id: 0x123, data: [i & 0xFF, i >> 8]); - }, - ); + testWidgets('writing lots of frames to can0 and reading from can1', ( + _, + ) async { + final sentFrames = List.generate(1024, (i) { + return CanFrame.standard(id: 0x123, data: [i & 0xFF, i >> 8]); + }); late List receivedFrames; - final receivedFramesFuture = can1.receive().take(1024).toList().then((frames) { + final receivedFramesFuture = can1.receive().take(1024).toList().then(( + frames, + ) { receivedFrames = frames; }); @@ -295,6 +330,306 @@ void main() { // check that they match the frames we sent expect(receivedFrames, containsAll(sentFrames)); }); + + group('kernel filters', () { + testWidgets('filtering for single ID emits matching frames', ( + _, + ) async { + final frame1 = CanFrame.standard( + id: 0x123, + data: [0x01, 0x02, 0x03, 0x04], + ); + final frame2 = CanFrame.standard( + id: 0x124, + data: [0x01, 0x02, 0x03, 0x04], + ); + + final framesFuture = can1.receiveSingle( + filter: CanFilter.whitelist(const [0x123]), + ); + + can0.send(frame1); + can0.send(frame2); + + await expectLater( + framesFuture.timeout(const Duration(seconds: 10)), + completion(equals(frame1)), + ); + }); + + testWidgets( + 'filtering for single ID does not emit non-matching frames', + (_) async { + final frame1 = CanFrame.standard( + id: 0x123, + data: [0x01, 0x02, 0x03, 0x04], + ); + final frame2 = CanFrame.standard( + id: 0x124, + data: [0x01, 0x02, 0x03, 0x04], + ); + + final frameFuture = can1.receiveSingle( + filter: CanFilter.whitelist(const [0x124]), + ); + + can0.send(frame1); + can0.send(frame2); + + await expectLater( + frameFuture.timeout(const Duration(seconds: 10)), + completion(equals(frame2)), + ); + }, + ); + + testWidgets('filtering for multiple IDs emits matching frames', ( + _, + ) async { + final frames = [ + CanFrame.standard(id: 0x123, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x124, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x125, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x126, data: [0x01, 0x02, 0x03, 0x04]), + ]; + + final frameFuture = can1.receive(filter: CanFilter.whitelist(const [0x123, 0x124])).take(2).toList(); + + frames.forEach(can0.send); + + await expectLater( + frameFuture.timeout(const Duration(seconds: 10)), + completion(containsAll(frames.take(2))), + ); + }); + + testWidgets( + 'filtering for multiple IDs does not emit non-matching frames', + (_) async { + final frames = [ + CanFrame.standard(id: 0x123, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x124, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x125, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x126, data: [0x01, 0x02, 0x03, 0x04]), + ]; + + final frameFuture = can1 + .receive( + filter: CanFilter.whitelist(const [0x125, 0x126]), + ) + .take(2) + .toList(); + + frames.forEach(can0.send); + + await expectLater( + frameFuture.timeout(const Duration(seconds: 10)), + completion(containsAll(frames.skip(2))), + ); + }, + ); + + testWidgets( + 'receiving two streams simulatenously emits same frames on both streams', + (_) async { + final frames = [ + CanFrame.standard(id: 0x126, data: [0x01, 0x02, 0x03, 0x04]), + ]; + + final frameFuture1 = can1.receiveSingle(); + + final frameFuture2 = can1.receiveSingle(); + + frames.forEach(can0.send); + + await expectLater( + frameFuture1.timeout(const Duration(seconds: 10)), + completion(anyOf(frames)), + ); + await expectLater( + frameFuture2.timeout(const Duration(seconds: 10)), + completion(anyOf(frames)), + ); + }, + ); + + testWidgets( + 'receiving two streams simulatenously with different filters', + (_) async { + final frames = [ + CanFrame.standard(id: 0x11F, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x120, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x121, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x122, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x123, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x124, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x125, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x126, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x127, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x128, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x129, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x12A, data: [0x01, 0x02, 0x03, 0x04]), + ]; + + final frameFuture1 = can1.receive(filter: CanFilter.whitelist([0x123, 0x124])).take(2).toList(); + + final frameFuture2 = can1.receive(filter: CanFilter.whitelist([0x128, 0x129])).take(2).toList(); + + frames.forEach(can0.send); + + await expectLater( + frameFuture1.timeout(const Duration(seconds: 10)), + completion( + containsAll([ + predicate((f) => f is CanFrame && f.id == 0x123), + predicate((f) => f is CanFrame && f.id == 0x124), + ]), + ), + ); + + await expectLater( + frameFuture2.timeout(const Duration(seconds: 10)), + completion( + containsAll([ + predicate((f) => f is CanFrame && f.id == 0x128), + predicate((f) => f is CanFrame && f.id == 0x129), + ]), + ), + ); + }, + ); + + testWidgets('blacklisting single ID works', (_) async { + final frames = [ + CanFrame.standard(id: 0x123, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x124, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x125, data: [0x01, 0x02, 0x03, 0x04]), + ]; + + final framesFuture = can1 + .receive( + emitErrors: true, + filter: CanFilter.blacklist(const [0x124]), + ) + .take(2) + .toList(); + + frames.forEach(can0.send); + + await expectLater( + framesFuture.timeout(const Duration(seconds: 10)), + completion(containsAll([frames[0], frames[2]])), + ); + }); + + testWidgets('in-kernel SFF filter matches emulated SFF filter', ( + _, + ) async { + final filter = CanFilter.or([ + CanFilter.or([ + const CanFilter.idEquals( + 0x101, + mask: 0x080, + formats: CanFrameFormat.both, + ), + const CanFilter.idEquals( + 0x040, + mask: 0x030, + formats: {CanFrameFormat.base}, + types: {CanFrameType.remote}, + ), + const CanFilter.not( + CanFilter.idEquals(0x123, formats: {CanFrameFormat.base}), + ), + CanFilter.blacklist([0x124]), + ]), + ]); + + final frames = Iterable.generate(2048 * 2, (index) { + if (index < 2048) { + return CanFrame.standard(id: index, data: [0]); + } else { + return CanFrame.standardRemote(id: index - 2048); + } + }); + + final matchingEmulated = frames.where( + (frame) => filter.matches(frame), + ); + + final received = can1.receive(emitErrors: true, filter: filter).take(matchingEmulated.length).toList(); + + frames.forEach(can0.send); + + await expectLater( + received.timeout(const Duration(seconds: 30)), + completion(matchingEmulated), + ); + }); + + testWidgets('in-kernel EFF filter matches emulated EFF filter', ( + _, + ) async { + const filter = CanFilter.or([ + CanFilter.or([ + CanFilter.idEquals( + 0x101, + mask: 0x080, + formats: CanFrameFormat.both, + ), + CanFilter.idEquals( + 0x04024, + mask: 0x03024, + formats: {CanFrameFormat.extended}, + types: {CanFrameType.remote}, + ), + CanFilter.not( + CanFilter.idEquals( + 0x1, + mask: 0x1, + formats: {CanFrameFormat.extended}, + ), + ), + CanFilter.notIdEquals( + 0x2, + mask: 0x2, + formats: {CanFrameFormat.extended}, + ), + CanFilter.notIdEquals( + 0x4, + mask: 0x4, + formats: {CanFrameFormat.extended}, + ), + CanFilter.notIdEquals( + 0x8, + mask: 0x8, + formats: {CanFrameFormat.extended}, + ), + ]), + ]); + + final frames = Iterable.generate(pow(2, 13).toInt(), (index) { + if (index & 1 == 0) { + return CanFrame.extended(id: index >> 1, data: [0]); + } else { + return CanFrame.extendedRemote(id: index >> 1); + } + }); + + final matchingEmulated = frames.where( + (frame) => filter.matches(frame), + ); + + final received = can1.receive(emitErrors: true, filter: filter).take(matchingEmulated.length).toList(); + + frames.forEach(can0.send); + + await expectLater( + received.timeout(const Duration(seconds: 30)), + completion(equals(matchingEmulated)), + ); + }); + }); }); }); @@ -329,7 +664,9 @@ void main() { late CanDevice vcan; setUp(() { - vcan = devices.singleWhere((device) => device.networkInterface.name == 'vcan0'); + vcan = devices.singleWhere( + (device) => device.networkInterface.name == 'vcan0', + ); }); testWidgets('queryAttributes', (_) async { @@ -382,8 +719,16 @@ void main() { late CanSocket vcan1; setUp(() { - vcan0 = devices.singleWhere((device) => device.networkInterface.name == 'vcan0').open(); - vcan1 = devices.singleWhere((device) => device.networkInterface.name == 'vcan0').open(); + vcan0 = devices + .singleWhere( + (device) => device.networkInterface.name == 'vcan0', + ) + .open(); + vcan1 = devices + .singleWhere( + (device) => device.networkInterface.name == 'vcan0', + ) + .open(); }); tearDown(() { @@ -398,30 +743,31 @@ void main() { testWidgets('writing standard CAN frame', (_) async { expect( - () => vcan0.send(CanFrame.standard(id: 0x123, data: [0x01, 0x02, 0x03, 0x04])), + () => vcan0.send( + CanFrame.standard(id: 0x123, data: [0x01, 0x02, 0x03, 0x04]), + ), returnsNormally, ); }); group('sending and receiving on different socket', () { - testWidgets( - 'empty SFF data frame', - (_) async { - final sentFrame = CanFrame.standard(id: 0x120, data: []); + testWidgets('empty SFF data frame', (_) async { + final sentFrame = CanFrame.standard(id: 0x120, data: []); - final frameFuture = vcan0.receiveSingle(emitErrors: true); + final frameFuture = vcan0.receiveSingle(emitErrors: true); - vcan1.send(sentFrame); + vcan1.send(sentFrame); - await expectLater(frameFuture, completion(equals(sentFrame))); - }, - timeout: const Timeout(Duration(seconds: 10)), - ); + await expectLater(frameFuture, completion(equals(sentFrame))); + }, timeout: const Timeout(Duration(seconds: 10))); testWidgets( 'full-length SFF data frame', (_) async { - final sentFrame = CanFrame.standard(id: 0x120, data: [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]); + final sentFrame = CanFrame.standard( + id: 0x120, + data: [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08], + ); final frameFuture = vcan1.receiveSingle(emitErrors: true); @@ -432,152 +778,141 @@ void main() { timeout: const Timeout(Duration(seconds: 10)), ); - testWidgets( - 'SFF data frame', - (_) async { - late CanFrame frame; + testWidgets('SFF data frame', (_) async { + late CanFrame frame; - final future = vcan1.receiveSingle(emitErrors: true).then((received) => frame = received); + final future = vcan1.receiveSingle(emitErrors: true).then((received) => frame = received); - vcan0.send(CanFrame.standard(id: 0x123, data: [0x01, 0x02, 0x03, 0x04])); + vcan0.send( + CanFrame.standard(id: 0x123, data: [0x01, 0x02, 0x03, 0x04]), + ); - await expectLater(future, completes); + await expectLater(future, completes); - expect(frame, isNotNull); - expect(frame, isA()); + expect(frame, isNotNull); + expect(frame, isA()); - final dataFrame = frame as CanStandardDataFrame; - expect(dataFrame.id, equals(0x123)); - expect(dataFrame.data, hasLength(4)); - expect(dataFrame.data, equals([0x01, 0x02, 0x03, 0x04])); - }, - timeout: const Timeout(Duration(seconds: 10)), - ); + final dataFrame = frame as CanStandardDataFrame; + expect(dataFrame.id, equals(0x123)); + expect(dataFrame.data, hasLength(4)); + expect(dataFrame.data, equals([0x01, 0x02, 0x03, 0x04])); + }, timeout: const Timeout(Duration(seconds: 10))); - testWidgets( - 'SFF data frame 2', - (_) async { - late CanFrame frame; + testWidgets('SFF data frame 2', (_) async { + late CanFrame frame; - final future = vcan0.receiveSingle(emitErrors: true).then((received) => frame = received); + final future = vcan0.receiveSingle(emitErrors: true).then((received) => frame = received); - vcan1.send(CanFrame.standard(id: 0x123, data: [0x01, 0x02, 0x03, 0x04])); + vcan1.send( + CanFrame.standard(id: 0x123, data: [0x01, 0x02, 0x03, 0x04]), + ); - await expectLater(future, completes); + await expectLater(future, completes); - expect(frame, isNotNull); - expect(frame, isA()); + expect(frame, isNotNull); + expect(frame, isA()); - final dataFrame = frame as CanStandardDataFrame; - expect(dataFrame.id, equals(0x123)); - expect(dataFrame.data, hasLength(4)); - expect(dataFrame.data, equals([0x01, 0x02, 0x03, 0x04])); - }, - timeout: const Timeout(Duration(seconds: 10)), - ); + final dataFrame = frame as CanStandardDataFrame; + expect(dataFrame.id, equals(0x123)); + expect(dataFrame.data, hasLength(4)); + expect(dataFrame.data, equals([0x01, 0x02, 0x03, 0x04])); + }, timeout: const Timeout(Duration(seconds: 10))); - testWidgets( - 'SFF RTR frame', - (_) async { - late CanFrame frame; + testWidgets('SFF RTR frame', (_) async { + late CanFrame frame; - final future = - vcan0.receiveSingle(emitErrors: true, filter: CanFilter.any).then((received) => frame = received); + final future = + vcan0.receiveSingle(emitErrors: true, filter: CanFilter.any).then((received) => frame = received); - vcan1.send(CanFrame.standardRemote(id: 0x123)); + vcan1.send(CanFrame.standardRemote(id: 0x123)); - await expectLater(future, completes); + await expectLater(future, completes); - expect(frame, isNotNull); - expect(frame, isA()); + expect(frame, isNotNull); + expect(frame, isA()); - final dataFrame = frame as CanStandardRemoteFrame; - expect(dataFrame.id, equals(0x123)); - }, - timeout: const Timeout(Duration(seconds: 10)), - ); + final dataFrame = frame as CanStandardRemoteFrame; + expect(dataFrame.id, equals(0x123)); + }, timeout: const Timeout(Duration(seconds: 10))); - testWidgets( - 'EFF data frame', - (_) async { - late CanFrame frame; + testWidgets('EFF data frame', (_) async { + late CanFrame frame; - final future = vcan0.receiveSingle(emitErrors: true).then((received) => frame = received); + final future = vcan0.receiveSingle(emitErrors: true).then((received) => frame = received); - vcan1.send(CanFrame.extended(id: 0x123, data: [0x01, 0x02, 0x03, 0x04])); + vcan1.send( + CanFrame.extended(id: 0x123, data: [0x01, 0x02, 0x03, 0x04]), + ); - await expectLater(future, completes); + await expectLater(future, completes); - expect(frame, isNotNull); - expect(frame, isA()); + expect(frame, isNotNull); + expect(frame, isA()); - final dataFrame = frame as CanExtendedDataFrame; - expect(dataFrame.id, equals(0x123)); - expect(dataFrame.data, hasLength(4)); - expect(dataFrame.data, equals([0x01, 0x02, 0x03, 0x04])); - }, - timeout: const Timeout(Duration(seconds: 10)), - ); + final dataFrame = frame as CanExtendedDataFrame; + expect(dataFrame.id, equals(0x123)); + expect(dataFrame.data, hasLength(4)); + expect(dataFrame.data, equals([0x01, 0x02, 0x03, 0x04])); + }, timeout: const Timeout(Duration(seconds: 10))); - testWidgets( - 'EFF RTR frame', - (_) async { - late CanFrame frame; + testWidgets('EFF RTR frame', (_) async { + late CanFrame frame; - final future = - vcan0.receiveSingle(emitErrors: true, filter: CanFilter.any).then((received) => frame = received); + final future = + vcan0.receiveSingle(emitErrors: true, filter: CanFilter.any).then((received) => frame = received); - vcan1.send(CanFrame.extendedRemote(id: 0x123)); + vcan1.send(CanFrame.extendedRemote(id: 0x123)); - await expectLater(future, completes); + await expectLater(future, completes); - expect(frame, isNotNull); - expect(frame, isA()); + expect(frame, isNotNull); + expect(frame, isA()); - final dataFrame = frame as CanExtendedRemoteFrame; - expect(dataFrame.id, equals(0x123)); - }, - timeout: const Timeout(Duration(seconds: 10)), - ); + final dataFrame = frame as CanExtendedRemoteFrame; + expect(dataFrame.id, equals(0x123)); + }, timeout: const Timeout(Duration(seconds: 10))); - testWidgets( - 'lots of frames', - (_) async { - final sentFrames = List.generate( - 1024, - (i) { - return CanFrame.standard(id: 0x123, data: [i & 0xFF, i >> 8]); - }, - ); + testWidgets('lots of frames', (_) async { + final sentFrames = List.generate(1024, (i) { + return CanFrame.standard(id: 0x123, data: [i & 0xFF, i >> 8]); + }); - late List receivedFrames; - final receivedFramesFuture = vcan1.receive().take(1024).toList().then((frames) { - receivedFrames = frames; - }); + late List receivedFrames; + final receivedFramesFuture = vcan1.receive().take(1024).toList().then((frames) { + receivedFrames = frames; + }); - // write all frames to the bus. - // requires a sufficiently big tx queue - sentFrames.forEach(vcan0.send); + // write all frames to the bus. + // requires a sufficiently big tx queue + sentFrames.forEach(vcan0.send); - // wait for all frames to be received - await expectLater( - receivedFramesFuture.timeout(const Duration(seconds: 30)), - completes, - ); + // wait for all frames to be received + await expectLater( + receivedFramesFuture.timeout(const Duration(seconds: 30)), + completes, + ); - // check that they match the frames we sent - expect(receivedFrames, containsAll(sentFrames)); - }, - timeout: const Timeout(Duration(seconds: 40)), - ); + // check that they match the frames we sent + expect(receivedFrames, containsAll(sentFrames)); + }, timeout: const Timeout(Duration(seconds: 40))); }); group('kernel filters', () { - testWidgets('filtering for single ID emits matching frames', (_) async { - final frame1 = CanFrame.standard(id: 0x123, data: [0x01, 0x02, 0x03, 0x04]); - final frame2 = CanFrame.standard(id: 0x124, data: [0x01, 0x02, 0x03, 0x04]); + testWidgets('filtering for single ID emits matching frames', ( + _, + ) async { + final frame1 = CanFrame.standard( + id: 0x123, + data: [0x01, 0x02, 0x03, 0x04], + ); + final frame2 = CanFrame.standard( + id: 0x124, + data: [0x01, 0x02, 0x03, 0x04], + ); - final framesFuture = vcan1.receiveSingle(filter: CanFilter.whitelist(const [0x123])); + final framesFuture = vcan1.receiveSingle( + filter: CanFilter.whitelist(const [0x123]), + ); vcan0.send(frame1); vcan0.send(frame2); @@ -588,22 +923,35 @@ void main() { ); }); - testWidgets('filtering for single ID does not emit non-matching frames', (_) async { - final frame1 = CanFrame.standard(id: 0x123, data: [0x01, 0x02, 0x03, 0x04]); - final frame2 = CanFrame.standard(id: 0x124, data: [0x01, 0x02, 0x03, 0x04]); + testWidgets( + 'filtering for single ID does not emit non-matching frames', + (_) async { + final frame1 = CanFrame.standard( + id: 0x123, + data: [0x01, 0x02, 0x03, 0x04], + ); + final frame2 = CanFrame.standard( + id: 0x124, + data: [0x01, 0x02, 0x03, 0x04], + ); - final frameFuture = vcan1.receiveSingle(filter: CanFilter.whitelist(const [0x124])); + final frameFuture = vcan1.receiveSingle( + filter: CanFilter.whitelist(const [0x124]), + ); - vcan0.send(frame1); - vcan0.send(frame2); + vcan0.send(frame1); + vcan0.send(frame2); - await expectLater( - frameFuture.timeout(const Duration(seconds: 10)), - completion(equals(frame2)), - ); - }); + await expectLater( + frameFuture.timeout(const Duration(seconds: 10)), + completion(equals(frame2)), + ); + }, + ); - testWidgets('filtering for multiple IDs emits matching frames', (_) async { + testWidgets('filtering for multiple IDs emits matching frames', ( + _, + ) async { final frames = [ CanFrame.standard(id: 0x123, data: [0x01, 0x02, 0x03, 0x04]), CanFrame.standard(id: 0x124, data: [0x01, 0x02, 0x03, 0x04]), @@ -611,12 +959,7 @@ void main() { CanFrame.standard(id: 0x126, data: [0x01, 0x02, 0x03, 0x04]), ]; - final frameFuture = vcan1 - .receive( - filter: CanFilter.whitelist(const [0x123, 0x124]), - ) - .take(2) - .toList(); + final frameFuture = vcan1.receive(filter: CanFilter.whitelist(const [0x123, 0x124])).take(2).toList(); frames.forEach(vcan0.send); @@ -626,97 +969,19 @@ void main() { ); }); - testWidgets('filtering for multiple IDs does not emit non-matching frames', (_) async { - final frames = [ - CanFrame.standard(id: 0x123, data: [0x01, 0x02, 0x03, 0x04]), - CanFrame.standard(id: 0x124, data: [0x01, 0x02, 0x03, 0x04]), - CanFrame.standard(id: 0x125, data: [0x01, 0x02, 0x03, 0x04]), - CanFrame.standard(id: 0x126, data: [0x01, 0x02, 0x03, 0x04]), - ]; - - final frameFuture = vcan1.receive(filter: CanFilter.whitelist(const [0x125, 0x126])).take(2).toList(); - - frames.forEach(vcan0.send); - - await expectLater( - frameFuture.timeout(const Duration(seconds: 10)), - completion(containsAll(frames.skip(2))), - ); - }); - - testWidgets('receiving two streams simulatenously emits same frames on both streams', (_) async { - final frames = [ - CanFrame.standard(id: 0x126, data: [0x01, 0x02, 0x03, 0x04]), - ]; - - final frameFuture1 = vcan1.receiveSingle(); - - final frameFuture2 = vcan1.receiveSingle(); - - frames.forEach(vcan0.send); - - await expectLater( - frameFuture1.timeout(const Duration(seconds: 10)), - completion(anyOf(frames)), - ); - await expectLater( - frameFuture2.timeout(const Duration(seconds: 10)), - completion(anyOf(frames)), - ); - }); - - testWidgets('receiving two streams simulatenously with different filters', (_) async { - final frames = [ - CanFrame.standard(id: 0x11F, data: [0x01, 0x02, 0x03, 0x04]), - CanFrame.standard(id: 0x120, data: [0x01, 0x02, 0x03, 0x04]), - CanFrame.standard(id: 0x121, data: [0x01, 0x02, 0x03, 0x04]), - CanFrame.standard(id: 0x122, data: [0x01, 0x02, 0x03, 0x04]), - CanFrame.standard(id: 0x123, data: [0x01, 0x02, 0x03, 0x04]), - CanFrame.standard(id: 0x124, data: [0x01, 0x02, 0x03, 0x04]), - CanFrame.standard(id: 0x125, data: [0x01, 0x02, 0x03, 0x04]), - CanFrame.standard(id: 0x126, data: [0x01, 0x02, 0x03, 0x04]), - CanFrame.standard(id: 0x127, data: [0x01, 0x02, 0x03, 0x04]), - CanFrame.standard(id: 0x128, data: [0x01, 0x02, 0x03, 0x04]), - CanFrame.standard(id: 0x129, data: [0x01, 0x02, 0x03, 0x04]), - CanFrame.standard(id: 0x12A, data: [0x01, 0x02, 0x03, 0x04]), - ]; - - final frameFuture1 = vcan1.receive(filter: CanFilter.whitelist([0x123, 0x124])).take(2).toList(); - - final frameFuture2 = vcan1.receive(filter: CanFilter.whitelist([0x128, 0x129])).take(2).toList(); - - frames.forEach(vcan0.send); - - await expectLater( - frameFuture1.timeout(const Duration(seconds: 10)), - completion(containsAll([ - predicate((f) => f is CanFrame && f.id == 0x123), - predicate((f) => f is CanFrame && f.id == 0x124), - ])), - ); - - await expectLater( - frameFuture2.timeout(const Duration(seconds: 10)), - completion(containsAll([ - predicate((f) => f is CanFrame && f.id == 0x128), - predicate((f) => f is CanFrame && f.id == 0x129), - ])), - ); - }); - testWidgets( - 'blacklisting single ID works', + 'filtering for multiple IDs does not emit non-matching frames', (_) async { final frames = [ CanFrame.standard(id: 0x123, data: [0x01, 0x02, 0x03, 0x04]), CanFrame.standard(id: 0x124, data: [0x01, 0x02, 0x03, 0x04]), CanFrame.standard(id: 0x125, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x126, data: [0x01, 0x02, 0x03, 0x04]), ]; - final framesFuture = vcan1 + final frameFuture = vcan1 .receive( - emitErrors: true, - filter: CanFilter.blacklist(const [0x124]), + filter: CanFilter.whitelist(const [0x125, 0x126]), ) .take(2) .toList(); @@ -724,120 +989,212 @@ void main() { frames.forEach(vcan0.send); await expectLater( - framesFuture.timeout(const Duration(seconds: 10)), - completion(containsAll([frames[0], frames[2]])), + frameFuture.timeout(const Duration(seconds: 10)), + completion(containsAll(frames.skip(2))), ); }, ); testWidgets( - 'in-kernel SFF filter matches emulated SFF filter', + 'receiving two streams simulatenously emits same frames on both streams', (_) async { - final filter = CanFilter.or([ - CanFilter.or([ - const CanFilter.idEquals( - 0x101, - mask: 0x080, - formats: CanFrameFormat.both, - ), - const CanFilter.idEquals( - 0x040, - mask: 0x030, - formats: {CanFrameFormat.base}, - types: {CanFrameType.remote}, - ), - const CanFilter.not( - CanFilter.idEquals( - 0x123, - formats: {CanFrameFormat.base}, - ), - ), - CanFilter.blacklist([0x124]) - ]), - ]); - - final frames = Iterable.generate(2048 * 2, (index) { - if (index < 2048) { - return CanFrame.standard(id: index, data: [0]); - } else { - return CanFrame.standardRemote(id: index - 2048); - } - }); + final frames = [ + CanFrame.standard(id: 0x126, data: [0x01, 0x02, 0x03, 0x04]), + ]; - final matchingEmulated = frames.where((frame) => filter.matches(frame)); + final frameFuture1 = vcan1.receiveSingle(); - final received = vcan1.receive(emitErrors: true, filter: filter).take(matchingEmulated.length).toList(); + final frameFuture2 = vcan1.receiveSingle(); frames.forEach(vcan0.send); await expectLater( - received.timeout(const Duration(seconds: 30)), - completion(matchingEmulated), + frameFuture1.timeout(const Duration(seconds: 10)), + completion(anyOf(frames)), + ); + await expectLater( + frameFuture2.timeout(const Duration(seconds: 10)), + completion(anyOf(frames)), ); }, ); testWidgets( - 'in-kernel EFF filter matches emulated EFF filter', + 'receiving two streams simulatenously with different filters', (_) async { - const filter = CanFilter.or([ - CanFilter.or([ - CanFilter.idEquals( - 0x101, - mask: 0x080, - formats: CanFrameFormat.both, - ), - CanFilter.idEquals( - 0x04024, - mask: 0x03024, - formats: {CanFrameFormat.extended}, - types: {CanFrameType.remote}, - ), - CanFilter.not( - CanFilter.idEquals( - 0x1, - mask: 0x1, - formats: {CanFrameFormat.extended}, - ), - ), - CanFilter.notIdEquals( - 0x2, - mask: 0x2, - formats: {CanFrameFormat.extended}, - ), - CanFilter.notIdEquals( - 0x4, - mask: 0x4, - formats: {CanFrameFormat.extended}, - ), - CanFilter.notIdEquals( - 0x8, - mask: 0x8, - formats: {CanFrameFormat.extended}, - ) - ]), - ]); - - final frames = Iterable.generate(pow(2, 20 + 1).toInt(), (index) { - if (index & 1 == 0) { - return CanFrame.extended(id: index >> 1, data: [0]); - } else { - return CanFrame.extendedRemote(id: index >> 1); - } - }); + final frames = [ + CanFrame.standard(id: 0x11F, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x120, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x121, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x122, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x123, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x124, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x125, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x126, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x127, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x128, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x129, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x12A, data: [0x01, 0x02, 0x03, 0x04]), + ]; - final matchingEmulated = frames.where((frame) => filter.matches(frame)); + final frameFuture1 = vcan1.receive(filter: CanFilter.whitelist([0x123, 0x124])).take(2).toList(); - final received = vcan1.receive(emitErrors: true, filter: filter).take(matchingEmulated.length).toList(); + final frameFuture2 = vcan1.receive(filter: CanFilter.whitelist([0x128, 0x129])).take(2).toList(); frames.forEach(vcan0.send); await expectLater( - received.timeout(const Duration(seconds: 30)), - completion(equals(matchingEmulated)), + frameFuture1.timeout(const Duration(seconds: 10)), + completion( + containsAll([ + predicate((f) => f is CanFrame && f.id == 0x123), + predicate((f) => f is CanFrame && f.id == 0x124), + ]), + ), + ); + + await expectLater( + frameFuture2.timeout(const Duration(seconds: 10)), + completion( + containsAll([ + predicate((f) => f is CanFrame && f.id == 0x128), + predicate((f) => f is CanFrame && f.id == 0x129), + ]), + ), ); }, ); + + testWidgets('blacklisting single ID works', (_) async { + final frames = [ + CanFrame.standard(id: 0x123, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x124, data: [0x01, 0x02, 0x03, 0x04]), + CanFrame.standard(id: 0x125, data: [0x01, 0x02, 0x03, 0x04]), + ]; + + final framesFuture = vcan1 + .receive( + emitErrors: true, + filter: CanFilter.blacklist(const [0x124]), + ) + .take(2) + .toList(); + + frames.forEach(vcan0.send); + + await expectLater( + framesFuture.timeout(const Duration(seconds: 10)), + completion(containsAll([frames[0], frames[2]])), + ); + }); + + testWidgets('in-kernel SFF filter matches emulated SFF filter', ( + _, + ) async { + final filter = CanFilter.or([ + CanFilter.or([ + const CanFilter.idEquals( + 0x101, + mask: 0x080, + formats: CanFrameFormat.both, + ), + const CanFilter.idEquals( + 0x040, + mask: 0x030, + formats: {CanFrameFormat.base}, + types: {CanFrameType.remote}, + ), + const CanFilter.not( + CanFilter.idEquals(0x123, formats: {CanFrameFormat.base}), + ), + CanFilter.blacklist([0x124]), + ]), + ]); + + final frames = Iterable.generate(2048 * 2, (index) { + if (index < 2048) { + return CanFrame.standard(id: index, data: [0]); + } else { + return CanFrame.standardRemote(id: index - 2048); + } + }); + + final matchingEmulated = frames.where( + (frame) => filter.matches(frame), + ); + + final received = vcan1.receive(emitErrors: true, filter: filter).take(matchingEmulated.length).toList(); + + frames.forEach(vcan0.send); + + await expectLater( + received.timeout(const Duration(seconds: 30)), + completion(matchingEmulated), + ); + }); + + testWidgets('in-kernel EFF filter matches emulated EFF filter', ( + _, + ) async { + const filter = CanFilter.or([ + CanFilter.or([ + CanFilter.idEquals( + 0x101, + mask: 0x080, + formats: CanFrameFormat.both, + ), + CanFilter.idEquals( + 0x04024, + mask: 0x03024, + formats: {CanFrameFormat.extended}, + types: {CanFrameType.remote}, + ), + CanFilter.not( + CanFilter.idEquals( + 0x1, + mask: 0x1, + formats: {CanFrameFormat.extended}, + ), + ), + CanFilter.notIdEquals( + 0x2, + mask: 0x2, + formats: {CanFrameFormat.extended}, + ), + CanFilter.notIdEquals( + 0x4, + mask: 0x4, + formats: {CanFrameFormat.extended}, + ), + CanFilter.notIdEquals( + 0x8, + mask: 0x8, + formats: {CanFrameFormat.extended}, + ), + ]), + ]); + + final frames = Iterable.generate(pow(2, 20 + 1).toInt(), (index) { + if (index & 1 == 0) { + return CanFrame.extended(id: index >> 1, data: [0]); + } else { + return CanFrame.extendedRemote(id: index >> 1); + } + }); + + final matchingEmulated = frames.where( + (frame) => filter.matches(frame), + ); + + final received = vcan1.receive(emitErrors: true, filter: filter).take(matchingEmulated.length).toList(); + + frames.forEach(vcan0.send); + + await expectLater( + received.timeout(const Duration(seconds: 30)), + completion(equals(matchingEmulated)), + ); + }); }); }); });