Skip to content

Commit

Permalink
Gave each camera its own Isolate (#6)
Browse files Browse the repository at this point in the history
* 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 5199e9f.

* 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 <[email protected]>
  • Loading branch information
Levi-Lesches and aidanahram authored Nov 8, 2023
1 parent aa0ac56 commit ccbdc2f
Show file tree
Hide file tree
Showing 14 changed files with 357 additions and 210 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# https://dart.dev/guides/libraries/private-files
# Created by `dart pub`
.dart_tool/

pubspec_overrides.yaml
58 changes: 58 additions & 0 deletions bin/example.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// ignore_for_file: avoid_print

import "package:typed_isolate/typed_isolate.dart";

class NumberSender extends IsolateParent<int, int> {
@override
Future<void> run() async {
print("Opening parent...");
print("Sending: 1");
//send(1, "braces");
send(1, "brackets");
await Future<void>.delayed(const Duration(seconds: 1));

print("Sending: 2");
send(2, "brackets");
//send(2, "braces");
await Future<void>.delayed(const Duration(seconds: 1));

print("Sending: 3");
//send(3, "braces");
send(3, "brackets");
await Future<void>.delayed(const Duration(seconds: 1));
}

@override
void onData(int data) => print("Got: $data");
}

class NumberConverter extends IsolateChild<int, int> {
NumberConverter() : super(id: "brackets");

@override
void run() => print("Opening child...");

@override
void onData(int data) => send(data + 5);
}

class NumberConverter2 extends IsolateChild<String, int> {
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<void>.delayed(const Duration(seconds: 1));
await parent.run();
isolate1.kill();
//isolate2.kill();
parent.close();
}
66 changes: 66 additions & 0 deletions bin/test.dart
Original file line number Diff line number Diff line change
@@ -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<ImageData, void> {
// @override
// void onData(void data) { }

// @override
// Future<void> 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<void, ImageData> {
// Child({required super.id});

// @override
// void onData(ImageData data) {
// final pointer = Pointer.fromAddress(data.address).cast<Uint8>();
// 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<void>.delayed(const Duration(seconds: 1));
// print("Running main isolate...");
// await parent.run();
// parent.close();
// isolate.kill();
// }
2 changes: 1 addition & 1 deletion bin/video.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
134 changes: 0 additions & 134 deletions lib/src/camera.dart

This file was deleted.

115 changes: 115 additions & 0 deletions lib/src/camera_isolate.dart
Original file line number Diff line number Diff line change
@@ -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<FrameData, VideoCommand>{
/// 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<void> 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<void> 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));
}
}
}
Loading

0 comments on commit ccbdc2f

Please sign in to comment.