From ccbdc2f4050deddd685a693b81b02ce08cfac476 Mon Sep 17 00:00:00 2001 From: Levi Lesches Date: Wed, 8 Nov 2023 05:04:53 -0500 Subject: [PATCH] Gave each camera its own Isolate (#6) * bug in progress first commit * Isolate test * Fixed .value --> .address * made CameraIsolate Class, need to change VideoServer next * sdaasd sdasd wq Revert "made CameraIsolate Class, need to change VideoServer next" This reverts commit 5199e9f5d38a05e5631ec2b210a6e10f3517e8e8. * working partially * isolate cameras running somewhat buggy * bug fixes * enhancements * Better logging, docs * Moved pubspec overrides to gitignore * Updated pubspec links * Migrated to newer logging version --------- Co-authored-by: aidanahram --- .gitignore | 2 + bin/example.dart | 58 ++++++++++++++++ bin/test.dart | 66 ++++++++++++++++++ bin/video.dart | 2 +- lib/src/camera.dart | 134 ------------------------------------ lib/src/camera_isolate.dart | 115 +++++++++++++++++++++++++++++++ lib/src/collection.dart | 42 +++++------ lib/src/constants.dart | 8 +-- lib/src/frame.dart | 21 ++++++ lib/src/parent_isolate.dart | 52 ++++++++++++++ lib/src/server.dart | 29 +------- lib/video.dart | 2 +- pubspec.lock | 22 ++++-- pubspec.yaml | 14 +--- 14 files changed, 357 insertions(+), 210 deletions(-) create mode 100644 bin/example.dart create mode 100644 bin/test.dart delete mode 100644 lib/src/camera.dart create mode 100644 lib/src/camera_isolate.dart create mode 100644 lib/src/frame.dart create mode 100644 lib/src/parent_isolate.dart diff --git a/.gitignore b/.gitignore index 3a85790..7b1f1cd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ # https://dart.dev/guides/libraries/private-files # Created by `dart pub` .dart_tool/ + +pubspec_overrides.yaml diff --git a/bin/example.dart b/bin/example.dart new file mode 100644 index 0000000..abccda3 --- /dev/null +++ b/bin/example.dart @@ -0,0 +1,58 @@ +// ignore_for_file: avoid_print + +import "package:typed_isolate/typed_isolate.dart"; + +class NumberSender extends IsolateParent { + @override + Future run() async { + print("Opening parent..."); + print("Sending: 1"); + //send(1, "braces"); + send(1, "brackets"); + await Future.delayed(const Duration(seconds: 1)); + + print("Sending: 2"); + send(2, "brackets"); + //send(2, "braces"); + await Future.delayed(const Duration(seconds: 1)); + + print("Sending: 3"); + //send(3, "braces"); + send(3, "brackets"); + await Future.delayed(const Duration(seconds: 1)); + } + + @override + void onData(int data) => print("Got: $data"); +} + +class NumberConverter extends IsolateChild { + NumberConverter() : super(id: "brackets"); + + @override + void run() => print("Opening child..."); + + @override + void onData(int data) => send(data + 5); +} + +class NumberConverter2 extends IsolateChild { + NumberConverter2() : super(id: "braces"); + + @override + void run() => print("Opening child..."); + + @override + void onData(int data) => send("{$data}"); +} + +void main() async { + final parent = NumberSender(); + final isolate1 = await parent.spawn(NumberConverter()); + //final isolate2 = await parent.spawn(NumberConverter2()); + await Future.delayed(const Duration(seconds: 1)); + await parent.run(); + isolate1.kill(); + //isolate2.kill(); + parent.close(); +} diff --git a/bin/test.dart b/bin/test.dart new file mode 100644 index 0000000..95608d1 --- /dev/null +++ b/bin/test.dart @@ -0,0 +1,66 @@ +// // ignore_for_file: avoid_print + +// import "dart:ffi"; +// import "package:typed_isolate/typed_isolate.dart"; +// import "package:opencv_ffi/opencv_ffi.dart"; + +// class ImageData { +// final int address; +// final int length; +// const ImageData({required this.address, required this.length}); +// } + +// class Parent extends IsolateParent { +// @override +// void onData(void data) { } + +// @override +// Future run() async { +// print("Opening camera..."); +// final camera = Camera.fromIndex(0); +// if (!camera.isOpened) { +// print("Could not open camera"); +// return; +// } +// print("Camera opened"); +// for (int i = 0; i < 10; i++) { +// final image = camera.getJpg(); +// if (image == null) { +// print("Could not read frame"); +// continue; +// } +// final address = image.pointer.address; +// final length = image.data.length; +// print("Got an image at ($address) with $length bytes"); +// final data = ImageData(address: image.pointer.address, length: length); +// send(data, 0); +// } +// } +// } + +// class Child extends IsolateChild { +// Child({required super.id}); + +// @override +// void onData(ImageData data) { +// final pointer = Pointer.fromAddress(data.address).cast(); +// final image = OpenCVImage(pointer: pointer, length: data.length); +// print(" Child received pointer ${image.pointer.address} w/ ${image.data.length} bytes"); +// } + +// @override +// void run() { } +// } + +// void main() async { +// final parent = Parent(); +// print("Creating parent isolate... "); +// final child = Child(id: 0); +// final isolate = await parent.spawn(child); +// print("Waiting for child to spawn..."); +// await Future.delayed(const Duration(seconds: 1)); +// print("Running main isolate..."); +// await parent.run(); +// parent.close(); +// isolate.kill(); +// } diff --git a/bin/video.dart b/bin/video.dart index 163db42..447512f 100644 --- a/bin/video.dart +++ b/bin/video.dart @@ -2,6 +2,6 @@ import "package:burt_network/logging.dart"; import "package:video/video.dart"; void main() async { - BurtLogger.level = LogLevel.info; + Logger.level = LogLevel.info; await collection.init(); } diff --git a/lib/src/camera.dart b/lib/src/camera.dart deleted file mode 100644 index ad06b7d..0000000 --- a/lib/src/camera.dart +++ /dev/null @@ -1,134 +0,0 @@ -import "dart:async"; - -import "package:burt_network/burt_network.dart"; -import "package:opencv_ffi/opencv_ffi.dart"; - -import "collection.dart"; -import "periodic_timer.dart"; - -/// Reads from a camera and streams its data and [CameraDetails] to the dashboard. -/// -/// This class uses a few timers to keep track of everything: -/// - `frameTimer` is used to read the camera with an FPS of [CameraDetails.fps] -/// - `fpsTimer` reports the FPS using [LoggerUtils.debug] -/// - `statusTimer` to periodically send the camera's status to the dashboard -/// -/// To use this class, -/// - Call [init] to initialize the camera and [dispose] when you're finished -/// - Use [isRunning] and [details] to see the state of the camera -/// - Use [start] and [stop] to control whether the camera is running -/// - Call [updateDetails] when you want to update the camera's [CameraDetails] -/// -/// This class does not start itself, and can be started and stopped when the dashboard -/// connects or disconnects using [VideoCollection.videoServer]. -class CameraManager { - /// The native camera object from OpenCV. - final Camera camera; - - /// Holds the current details of the camera. - /// - /// Use [updateDetails] to change this. - final CameraDetails details; - - /// A timer to periodically send the camera status to the dashboard. - Timer? statusTimer; - - /// A timer to read from the camera at an FPS given by [details]. - PeriodicTimer? frameTimer; - - /// A timer to log out the [fpsCount] every 5 seconds using [LoggerUtils.debug]. - Timer? fpsTimer; - - /// Records how many FPS this camera is actually running at. - int fpsCount = 0; - - /// Creates a new manager for the given camera and default details. - CameraManager({required this.camera, required this.details}); - - /// Whether the camera is running. - bool get isRunning => frameTimer != null; - - /// The name of this camera (where it is on the rover). - CameraName get name => details.name; - - /// Initializes the camera but does not call [start]. - Future init() async { - logger.verbose("Initializing camera: ${details.name}"); - statusTimer = Timer.periodic( - const Duration(seconds: 5), - (_) => collection.videoServer.sendMessage(VideoData(details: details)), - ); - if (!camera.isOpened) { - logger.verbose("Camera $name is not connected"); - updateDetails(CameraDetails(status: CameraStatus.CAMERA_DISCONNECTED)); - } - } - - /// Disposes of the camera and the timers. - void dispose() { - logger.info("Releasing camera $name"); - camera.dispose(); - frameTimer?.cancel(); - fpsTimer?.cancel(); - statusTimer?.cancel(); - } - - /// Starts the camera and timers. - void start() { - if (isRunning || details.status != CameraStatus.CAMERA_ENABLED) return; - logger.verbose("Starting camera $name"); - final interval = details.fps == 0 - ? Duration.zero - : Duration(milliseconds: 1000 ~/ details.fps); - frameTimer = PeriodicTimer(interval, sendFrame); - fpsTimer = Timer.periodic(const Duration(seconds: 5), (_) { - logger.debug("Camera $name sent ${fpsCount ~/ 5} frames"); - fpsCount = 0; - }); - } - - /// Cancels all timers and stops reading the camera. - void stop() { - logger.verbose("Stopping camera $name"); - frameTimer?.cancel(); - fpsTimer?.cancel(); - frameTimer = null; // easy way to check if you're stopped - } - - /// Updates the camera's [details], which will take effect on the next [sendFrame] call. - /// - /// This function echoes the details to the dashboard as part of the handshake protocol, and - /// resets the timers in case the FPS has changed. Always use this function instead of modifying - /// [details] directly so these steps are not forgotten. - void updateDetails(CameraDetails newDetails) { - details.mergeFromMessage(newDetails); - collection.videoServer.sendMessage(VideoData(details: details)); - stop(); - start(); - } - - /// Reads a frame from the [camera] and sends it to the dashboard. - /// - /// - If the camera could not read the frame, sets the status to [CameraStatus.CAMERA_NOT_RESPONDING] - /// - If the frame was too large to send, calls [updateDetails] with a lower [CameraDetails.quality] - /// - If the quality is already too low, sets the status to [CameraStatus.FRAME_TOO_LARGE] - Future sendFrame() async { - final frame = camera.getJpg(quality: details.quality); - if (frame == null) { - updateDetails(CameraDetails(status: CameraStatus.CAMERA_NOT_RESPONDING)); - } else if (frame.data.length < 60000) { - collection.videoServer - .sendMessage(VideoData(frame: frame.data, details: details)); - fpsCount++; - } else if (details.quality > 25) { - logger.verbose("Lowering quality for $name"); - updateDetails(CameraDetails(quality: details.quality - 1)); - } else { - logger.warning( - "$name recorded a frame that was too large (${frame.data.length} bytes)", - ); - updateDetails(CameraDetails(status: CameraStatus.FRAME_TOO_LARGE)); - } - frame?.dispose(); - } -} diff --git a/lib/src/camera_isolate.dart b/lib/src/camera_isolate.dart new file mode 100644 index 0000000..2d50aa5 --- /dev/null +++ b/lib/src/camera_isolate.dart @@ -0,0 +1,115 @@ +import "dart:async"; + +import "package:opencv_ffi/opencv_ffi.dart"; +import "package:typed_isolate/typed_isolate.dart"; +import "package:burt_network/burt_network.dart"; +import "collection.dart"; +import "frame.dart"; +import "periodic_timer.dart"; + +/// An isolate that is spawned to manage one camera. +/// +/// This class accepts [VideoCommand]s and calls [updateDetails] with the newly-received details. +/// When a frame is read, instead of sending the [VideoData], this class sends only the pointer +/// to the [OpenCVImage] via the [FrameData] class, and the image is read by the parent isolate. +class CameraIsolate extends IsolateChild{ + /// The native camera object from OpenCV. + late final Camera camera; + /// Holds the current details of the camera. + final CameraDetails details; + + /// A timer to periodically send the camera status to the dashboard. + Timer? statusTimer; + /// A timer to read from the camera at an FPS given by [details]. + PeriodicTimer? frameTimer; + /// A timer to log out the [fpsCount] every 5 seconds using [LoggerUtils.debug]. + Timer? fpsTimer; + /// Records how many FPS this camera is actually running at. + int fpsCount = 0; + + /// The log level at which this isolate should be reporting. + LogLevel logLevel; + + /// Creates a new manager for the given camera and default details. + CameraIsolate({required this.details, required this.logLevel}) : super(id: details.name); + + /// The name of this camera (where it is on the rover). + CameraName get name => details.name; + + /// Sends the current status to the dashboard (with an empty frame). + void sendStatus([_]) => send(FrameData(details: details, address: 0, length: 0)); + + @override + Future run() async { + Logger.level = logLevel; + logger.debug("Initializing camera: $name"); + camera = getCamera(name); + statusTimer = Timer.periodic(const Duration(seconds: 5), sendStatus); + if (!camera.isOpened) { + logger.warning("Camera $name is not connected"); + updateDetails(CameraDetails(status: CameraStatus.CAMERA_DISCONNECTED)); + } + start(); + } + + @override + void onData(VideoCommand data) => updateDetails(data.details); + + /// Updates the camera's [details], which will take effect on the next [sendFrame] call. + void updateDetails(CameraDetails newDetails) { + details.mergeFromMessage(newDetails); + stop(); + start(); + } + + /// Disposes of the camera and the timers. + void dispose() { + camera.dispose(); + frameTimer?.cancel(); + fpsTimer?.cancel(); + statusTimer?.cancel(); + logger.info("Disposed camera $name"); + } + + /// Starts the camera and timers. + void start() { + if (details.status != CameraStatus.CAMERA_ENABLED) return; + logger.debug("Starting camera $name. Status=${details.status}"); + final interval = details.fps == 0 ? Duration.zero : Duration(milliseconds: 1000 ~/ details.fps); + frameTimer = PeriodicTimer(interval, sendFrame); + fpsTimer = Timer.periodic(const Duration(seconds: 5), (_) { + logger.trace("Camera $name sent ${fpsCount ~/ 5} frames"); + fpsCount = 0; + }); + } + + /// Cancels all timers and stops reading the camera. + void stop() { + logger.debug("Stopping camera $name"); + frameTimer?.cancel(); + fpsTimer?.cancel(); + } + + /// Reads a frame from the camera and sends it to the dashboard. + /// + /// Checks for multiple errors along the way: + /// - If the camera does not respond, alerts the dashboard + /// - If the frame is too large, reduces the quality (increases JPG compression) + /// - If the quality is already low, alerts the dashboard + Future sendFrame() async { + final frame = camera.getJpg(quality: details.quality); + if (frame == null) { // Error getting the frame + logger.warning("Camera $name didn't respond"); + updateDetails(CameraDetails(status: CameraStatus.CAMERA_NOT_RESPONDING)); + } else if (frame.data.length < 60000) { // Frame can be sent + send(FrameData(address: frame.pointer.address, length: frame.data.length, details: details)); + fpsCount++; + } else if (details.quality > 25) { // Frame too large, try lowering quality + logger.debug("Lowering quality for $name from ${details.quality}"); + updateDetails(CameraDetails(quality: details.quality - 1)); + } else { // Frame too large, cannot lower quality anymore + logger.warning("$name's frames are too large (${frame.data.length} bytes, quality=${details.quality})"); + updateDetails(CameraDetails(status: CameraStatus.FRAME_TOO_LARGE)); + } + } +} diff --git a/lib/src/collection.dart b/lib/src/collection.dart index 08871c7..b6d154e 100644 --- a/lib/src/collection.dart +++ b/lib/src/collection.dart @@ -4,55 +4,47 @@ import "dart:io"; import "package:burt_network/burt_network.dart"; import "package:opencv_ffi/opencv_ffi.dart"; -import "camera.dart"; import "constants.dart"; +import "parent_isolate.dart"; import "server.dart"; /// Default details for a camera /// /// Used when first creating the camera objects CameraDetails getDefaultDetails(CameraName name) => CameraDetails( - name: name, - resolutionWidth: 300, - resolutionHeight: 300, - quality: 50, - fps: 24, - status: CameraStatus.CAMERA_ENABLED, - ); + name: name, + resolutionWidth: 300, + resolutionHeight: 300, + quality: 50, + fps: 24, + status: CameraStatus.CAMERA_ENABLED, +); /// Returns the camera depending on device program is running /// /// Uses [cameraNames] or [cameraIndexes] Camera getCamera(CameraName name) => Platform.isWindows - ? Camera.fromIndex(cameraIndexes[name]!) - : Camera.fromName(cameraNames[name]!); + ? Camera.fromIndex(cameraIndexes[name]!) + : Camera.fromName(cameraNames[name]!); /// Class to contain all video devices -class VideoCollection { - /// Holds a list of available cameras - Map cameras = { - for (final name in CameraName.values) - if (name != CameraName.CAMERA_NAME_UNDEFINED) - name: CameraManager( - camera: getCamera(name), - details: getDefaultDetails(name), - ) - }; - +class Collection { /// [VideoServer] to send messages through /// /// Default port is 8002 for video final videoServer = VideoServer(port: 8002); + /// Main parent isolate + final parent = VideoController(); + /// Function to initialize cameras Future init() async { + logger..trace("Running in trace mode")..debug("Running in debug mode"); await videoServer.init(); - for (final camera in cameras.values) { - await camera.init(); - } + await parent.run(); logger.info("Video program initialized"); } } /// Holds all the devices connected -final collection = VideoCollection(); +final collection = Collection(); diff --git a/lib/src/constants.dart b/lib/src/constants.dart index 2362aef..5127b5e 100644 --- a/lib/src/constants.dart +++ b/lib/src/constants.dart @@ -18,8 +18,8 @@ Map cameraNames = { Map cameraIndexes = { CameraName.ROVER_FRONT: 0, CameraName.ROVER_REAR: 1, - CameraName.AUTONOMY_DEPTH: 1, - CameraName.SUBSYSTEM1: 2, - CameraName.SUBSYSTEM2: 3, - CameraName.SUBSYSTEM3: 4, + CameraName.AUTONOMY_DEPTH: 2, + CameraName.SUBSYSTEM1: 3, + CameraName.SUBSYSTEM2: 4, + CameraName.SUBSYSTEM3: 5, }; diff --git a/lib/src/frame.dart b/lib/src/frame.dart new file mode 100644 index 0000000..a57f0f0 --- /dev/null +++ b/lib/src/frame.dart @@ -0,0 +1,21 @@ +import "package:burt_network/burt_network.dart"; + +/// A container for a pointer to a native buffer that can be sent across isolates. +/// +/// Sending a buffer across isolates would mean that data is copied, which is not ideal for +/// buffers containing an entire JPG image, from multiple isolates, multiple frames per second. +/// Since we cannot yet send FFI pointers across isolates, we have to send its raw address +/// instead. +class FrameData { + /// The [CameraDetails] for this frame. + final CameraDetails details; + /// The address of the FFI pointer containing the image. + final int? address; + /// The amount of bytes to read past [address]. + final int? length; + + /// Creates a [FrameData] containing an actual frame. + FrameData({required this.details, required this.address, required this.length}); + /// Creates a [FrameData] that only has [CameraDetails]. + FrameData.details(this.details) : address = null, length = null; +} diff --git a/lib/src/parent_isolate.dart b/lib/src/parent_isolate.dart new file mode 100644 index 0000000..ffaa940 --- /dev/null +++ b/lib/src/parent_isolate.dart @@ -0,0 +1,52 @@ +import "dart:async"; +import "dart:ffi"; + +import "package:opencv_ffi/opencv_ffi.dart"; +import "package:typed_isolate/typed_isolate.dart"; +import "package:burt_network/burt_network.dart"; + +import "collection.dart"; +import "frame.dart"; +import "camera_isolate.dart"; + +/// A parent isolate that spawns [CameraIsolate]s to manage the cameras. +/// +/// With one isolate per camera, each camera can read in parallel. This class sends [VideoCommand]s +/// from the dashboard to the appropriate [CameraIsolate], and receives [FrameData]s which it uses +/// to read an [OpenCVImage] from native memory and send to the dashboard. By not sending the frame +/// from child isolate to the parent (just the pointer), we save a whole JPG image's worth of bytes +/// from every camera, every frame, every second. That could be up to 5 MB per second of savings. +class VideoController extends IsolateParent{ + @override + Future run() async { + for (final name in CameraName.values) { + if (name == CameraName.CAMERA_NAME_UNDEFINED) continue; + await spawn( + CameraIsolate( + logLevel: Logger.level, + details: getDefaultDetails(name), + ), + ); + } + } + + @override + void onData(FrameData data) { + if (data.address == null) { + collection.videoServer.sendMessage(VideoData(details: data.details)); + } else { + final frame = OpenCVImage(pointer: Pointer.fromAddress(data.address!), length: data.length!); + collection.videoServer.sendMessage(VideoData(frame: frame.data, details: data.details)); + frame.dispose(); + } + } + + /// Stops all the cameras managed by this class. + void stopAll() { + final command = VideoCommand(details: CameraDetails(status: CameraStatus.CAMERA_DISABLED)); + for (final name in CameraName.values) { + if (name == CameraName.CAMERA_NAME_UNDEFINED) continue; + send(command, name); + } + } +} diff --git a/lib/src/server.dart b/lib/src/server.dart index 61f8e34..6b41d64 100644 --- a/lib/src/server.dart +++ b/lib/src/server.dart @@ -7,37 +7,12 @@ class VideoServer extends ServerSocket { /// Requires a port to communicate through VideoServer({required super.port}) : super(device: Device.VIDEO); - @override - void onConnect(SocketInfo source) { - super.onConnect(source); - for (final camera in collection.cameras.values) { - camera.start(); - } - } - - @override - void onDisconnect() { - super.onDisconnect(); - for (final camera in collection.cameras.values) { - camera.stop(); - } - } - @override void onMessage(WrappedMessage wrapper) { // ignore message if not a video message if (wrapper.name != VideoCommand().messageName) return; final command = VideoCommand.fromBuffer(wrapper.data); - // Return the message to tell dashboard the message was received - sendMessage(command); - // Send LOADING before making any changes - sendMessage( - VideoData( - id: command.id, - details: CameraDetails(status: CameraStatus.CAMERA_LOADING), - ), - ); - // Change the settings - collection.cameras[command.details.name]!.updateDetails(command.details); + sendMessage(command); // Echo the request + collection.parent.send(command, command.details.name); } } diff --git a/lib/video.dart b/lib/video.dart index 20e4b91..4d803a6 100644 --- a/lib/video.dart +++ b/lib/video.dart @@ -1,4 +1,4 @@ -export "src/camera.dart"; +export "src/camera_isolate.dart"; export "src/collection.dart"; export "src/constants.dart"; export "src/server.dart"; diff --git a/pubspec.lock b/pubspec.lock index 1c6a532..ef09429 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -92,10 +92,10 @@ packages: dependency: transitive description: name: ffi - sha256: ed5337a5660c506388a9f012be0288fb38b49020ce2b45fe1f8b8323fe429f99 + sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.1.0" file: dependency: transitive description: @@ -164,10 +164,10 @@ packages: dependency: transitive description: name: logger - sha256: "7ad7215c15420a102ec687bb320a7312afd449bac63bfb1c60d9787c27b9767f" + sha256: "6bbb9d6f7056729537a4309bda2e74e18e5d9f14302489cc1e93f33b3fe32cac" url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "2.0.2+1" logging: dependency: transitive description: @@ -214,7 +214,7 @@ packages: path: "../opencv_ffi" relative: true source: path - version: "1.0.0" + version: "1.1.0" package_config: dependency: transitive description: @@ -243,10 +243,10 @@ packages: dependency: transitive description: name: protobuf - sha256: "4034a02b7e231e7e60bff30a8ac13a7347abfdac0798595fae0b90a3f0afe759" + sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d" url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.1.0" pub_semver: dependency: transitive description: @@ -375,6 +375,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" + typed_isolate: + dependency: "direct main" + description: + name: typed_isolate + sha256: e9b8cc039c8ecc3367f7d2ade7feafa2164aa5e2f12df4fb06ae01b2ad47c355 + url: "https://pub.dev" + source: hosted + version: "1.0.1" very_good_analysis: dependency: "direct dev" description: diff --git a/pubspec.yaml b/pubspec.yaml index b736f47..5729303 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -9,19 +9,11 @@ environment: # Add regular dependencies here. dependencies: opencv_ffi: - git: https://github.com/BinghamtonRover/OpenCV-FFI + git: https://github.com/Levi-Lesches/opencv_ffi burt_network: - git: https://github.com/BinghamtonRover/Dart-Networking + git: https://github.com/BinghamtonRover/Networking + typed_isolate: ^1.0.1 dev_dependencies: test: ^1.21.0 very_good_analysis: ^5.0.0+1 - -# This section is for the rover only. On your own system, comment this out. -# (This tells Pub/Dart to find these packages in nearby folders, rather than -# from GitHub, so that we can run without Internet access). -dependency_overrides: - opencv_ffi: - path: ../opencv_ffi - burt_network: - path: ../burt_network