diff --git a/pyproject.toml b/pyproject.toml
index 8829e426..4afec888 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -28,10 +28,10 @@ Changelog = "https://github.com/AllenNeuralDynamics/Aind.Behavior.VrForaging/rel
[project.optional-dependencies]
-data = ["contraqctor<0.6.0"]
+data = ["contraqctor>=0.5.3, <0.6.0"]
launcher = [
- "aind-clabe[aind-services] >= 0.8.2 ,<0.9.0",
+ "aind-clabe[aind-services]>=0.9",
"aind-data-schema>=2",
"aind_behavior_vr_foraging[data]",
]
diff --git a/src/DataSchemas/aind_behavior_vr_foraging.json b/src/DataSchemas/aind_behavior_vr_foraging.json
index e71d03ef..96aa377b 100644
--- a/src/DataSchemas/aind_behavior_vr_foraging.json
+++ b/src/DataSchemas/aind_behavior_vr_foraging.json
@@ -3,14 +3,14 @@
"AindBehaviorSessionModel": {
"properties": {
"aind_behavior_services_pkg_version": {
- "default": "0.12.3",
+ "default": "0.12.5",
"pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$",
"title": "aind_behavior_services package version",
"type": "string"
},
"version": {
- "const": "0.12.3",
- "default": "0.12.3",
+ "const": "0.12.5",
+ "default": "0.12.5",
"title": "Version",
"type": "string"
},
@@ -353,7 +353,7 @@
"AindVrForagingRig": {
"properties": {
"aind_behavior_services_pkg_version": {
- "default": "0.12.3",
+ "default": "0.12.5",
"pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$",
"title": "aind_behavior_services package version",
"type": "string"
@@ -611,7 +611,7 @@
"title": "Rng Seed"
},
"aind_behavior_services_pkg_version": {
- "default": "0.12.3",
+ "default": "0.12.5",
"pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$",
"title": "aind_behavior_services package version",
"type": "string"
@@ -3013,11 +3013,17 @@
"type": "boolean"
},
"stop_duration": {
- "default": 0,
- "description": "Duration (s) the animal must stop for to lock its choice",
- "minimum": 0,
- "title": "Stop Duration",
- "type": "number"
+ "$ref": "#/$defs/Distribution",
+ "default": {
+ "family": "Scalar",
+ "distribution_parameters": {
+ "family": "Scalar",
+ "value": 0.0
+ },
+ "truncation_parameters": null,
+ "scaling_parameters": null
+ },
+ "description": "Duration (s) the animal must stop for to lock its choice"
},
"time_to_collect_reward": {
"default": 100000,
diff --git a/src/Extensions/AddRewardSite.bonsai b/src/Extensions/AddRewardSite.bonsai
index 31a4345d..68529ff8 100644
--- a/src/Extensions/AddRewardSite.bonsai
+++ b/src/Extensions/AddRewardSite.bonsai
@@ -121,64 +121,11 @@
ActivePatch
-
- RewardSpecification
-
-
-
-
-
-
-
- ActivePatch
-
-
- RewardSpecification
-
-
- OperantLogic
-
-
-
-
-
-
-
-
-
- ActivePatch
-
-
- RewardSpecification.OperantLogic.StopDuration
-
-
- StopDurationOffset
-
-
-
-
-
-
- 1
-
-
-
-
-
-
-
-
-
-
- true
- 0
- 100000
- 10
-
-
-
+
+
+
@@ -198,24 +145,10 @@
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/Extensions/AindBehaviorVrForaging.Generated.cs b/src/Extensions/AindBehaviorVrForaging.Generated.cs
index a6fc59fe..e9c0818c 100644
--- a/src/Extensions/AindBehaviorVrForaging.Generated.cs
+++ b/src/Extensions/AindBehaviorVrForaging.Generated.cs
@@ -43,8 +43,8 @@ public partial class AindBehaviorSessionModel
public AindBehaviorSessionModel()
{
- _aindBehaviorServicesPkgVersion = "0.12.3";
- _version = "0.12.3";
+ _aindBehaviorServicesPkgVersion = "0.12.5";
+ _version = "0.12.5";
_experimenter = new System.Collections.Generic.List();
_experimentVersion = "";
_allowDirtyRepo = false;
@@ -954,7 +954,7 @@ public partial class AindVrForagingRig
public AindVrForagingRig()
{
- _aindBehaviorServicesPkgVersion = "0.12.3";
+ _aindBehaviorServicesPkgVersion = "0.12.5";
_version = "0.6.2-rc3";
_triggeredCameraController = new CameraControllerSpinnakerCamera();
_harpBehavior = new HarpBehavior();
@@ -1521,7 +1521,7 @@ public partial class AindVrForagingTaskParameters
public AindVrForagingTaskParameters()
{
- _aindBehaviorServicesPkgVersion = "0.12.3";
+ _aindBehaviorServicesPkgVersion = "0.12.5";
_environment = new BlockStructure();
_operationControl = new OperationControl();
}
@@ -8336,7 +8336,7 @@ public partial class OperantLogic
private bool _isOperant;
- private double _stopDuration;
+ private Distribution _stopDuration;
private double _timeToCollectReward;
@@ -8345,7 +8345,7 @@ public partial class OperantLogic
public OperantLogic()
{
_isOperant = true;
- _stopDuration = 0D;
+ _stopDuration = new Distribution();
_timeToCollectReward = 100000D;
_graceDistanceThreshold = 10D;
}
@@ -8378,9 +8378,10 @@ public bool IsOperant
///
/// Duration (s) the animal must stop for to lock its choice
///
+ [System.Xml.Serialization.XmlIgnoreAttribute()]
[Newtonsoft.Json.JsonPropertyAttribute("stop_duration")]
[System.ComponentModel.DescriptionAttribute("Duration (s) the animal must stop for to lock its choice")]
- public double StopDuration
+ public Distribution StopDuration
{
get
{
@@ -15277,6 +15278,51 @@ public override string ToString()
}
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.6.1.0 (Newtonsoft.Json v13.0.0.0)")]
+ [Newtonsoft.Json.JsonConverter(typeof(JsonInheritanceConverter), "family")]
+ [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Source)]
+ [Bonsai.CombinatorAttribute(MethodName="Generate")]
+ public partial class Delay
+ {
+
+ public Delay()
+ {
+ }
+
+ protected Delay(Delay other)
+ {
+ }
+
+ public System.IObservable Generate()
+ {
+ return System.Reactive.Linq.Observable.Defer(() => System.Reactive.Linq.Observable.Return(new Delay(this)));
+ }
+
+ public System.IObservable Generate(System.IObservable source)
+ {
+ return System.Reactive.Linq.Observable.Select(source, _ => new Delay(this));
+ }
+
+ protected virtual bool PrintMembers(System.Text.StringBuilder stringBuilder)
+ {
+ return false;
+ }
+
+ public override string ToString()
+ {
+ System.Text.StringBuilder stringBuilder = new System.Text.StringBuilder();
+ stringBuilder.Append(GetType().Name);
+ stringBuilder.Append(" { ");
+ if (PrintMembers(stringBuilder))
+ {
+ stringBuilder.Append(" ");
+ }
+ stringBuilder.Append("}");
+ return stringBuilder.ToString();
+ }
+ }
+
+
[System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.6.1.0 (Newtonsoft.Json v13.0.0.0)")]
[Newtonsoft.Json.JsonConverter(typeof(JsonInheritanceConverter), "family")]
[Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Source)]
@@ -16217,6 +16263,45 @@ private static System.IObservable Process(System.IObservable arguments)
+ {
+ var typeMapping = Type;
+ var returnType = typeMapping != null ? typeMapping.GetType().GetGenericArguments()[0] : typeof(Delay);
+ return System.Linq.Expressions.Expression.Call(
+ typeof(MatchDelay),
+ "Process",
+ new System.Type[] { returnType },
+ System.Linq.Enumerable.Single(arguments));
+ }
+
+
+ private static System.IObservable Process(System.IObservable source)
+ where TResult : Delay
+ {
+ return System.Reactive.Linq.Observable.Create(observer =>
+ {
+ var sourceObserver = System.Reactive.Observer.Create(
+ value =>
+ {
+ var match = value as TResult;
+ if (match != null) observer.OnNext(match);
+ },
+ observer.OnError,
+ observer.OnCompleted);
+ return System.ObservableExtensions.SubscribeSafe(source, sourceObserver);
+ });
+ }
+ }
+
+
[System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.6.1.0 (Newtonsoft.Json v13.0.0.0)")]
[System.ComponentModel.DefaultPropertyAttribute("Type")]
[Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Combinator)]
@@ -17093,6 +17178,11 @@ public System.IObservable Process(System.IObservable(source);
}
+ public System.IObservable Process(System.IObservable source)
+ {
+ return Process(source);
+ }
+
public System.IObservable Process(System.IObservable source)
{
return Process(source);
@@ -17264,6 +17354,7 @@ public System.IObservable Process(System.IObservable
[System.Xml.Serialization.XmlIncludeAttribute(typeof(Bonsai.Expressions.TypeMapping))]
[System.Xml.Serialization.XmlIncludeAttribute(typeof(Bonsai.Expressions.TypeMapping))]
[System.Xml.Serialization.XmlIncludeAttribute(typeof(Bonsai.Expressions.TypeMapping))]
+ [System.Xml.Serialization.XmlIncludeAttribute(typeof(Bonsai.Expressions.TypeMapping))]
[System.Xml.Serialization.XmlIncludeAttribute(typeof(Bonsai.Expressions.TypeMapping))]
[System.Xml.Serialization.XmlIncludeAttribute(typeof(Bonsai.Expressions.TypeMapping))]
[System.Xml.Serialization.XmlIncludeAttribute(typeof(Bonsai.Expressions.TypeMapping))]
diff --git a/src/Extensions/InstantiateSite.bonsai b/src/Extensions/InstantiateSite.bonsai
index 6b82502b..e76d82d1 100644
--- a/src/Extensions/InstantiateSite.bonsai
+++ b/src/Extensions/InstantiateSite.bonsai
@@ -3,11 +3,11 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:rx="clr-namespace:Bonsai.Reactive;assembly=Bonsai.Core"
xmlns:scr="clr-namespace:Bonsai.Scripting.Expressions;assembly=Bonsai.Scripting.Expressions"
- xmlns:gl="clr-namespace:Bonsai.Shaders;assembly=Bonsai.Shaders"
xmlns:p1="clr-namespace:AindVrForagingDataSchema;assembly=Extensions"
+ xmlns:p2="clr-namespace:AllenNeuralDynamics.Core;assembly=AllenNeuralDynamics.Core"
+ xmlns:gl="clr-namespace:Bonsai.Shaders;assembly=Bonsai.Shaders"
xmlns:dsp="clr-namespace:Bonsai.Dsp;assembly=Bonsai.Dsp"
xmlns:harp="clr-namespace:Bonsai.Harp;assembly=Bonsai.Harp"
- xmlns:p2="clr-namespace:AllenNeuralDynamics.Core;assembly=AllenNeuralDynamics.Core"
xmlns:p3="clr-namespace:Bonsai.Numerics.Distributions;assembly=Bonsai.Numerics"
xmlns:bv="clr-namespace:BonVision;assembly=BonVision"
xmlns="https://bonsai-rx.org/2018/workflow">
@@ -151,8 +151,25 @@
OperantLogic.StopDuration
+
+
+
+
+ StopDurationOffset
+
+
+
+
+
+
+
+
- TimeSpan.FromSeconds(it)
+ Math.Max(0, it)
+ Math.Max(0, it)
+
+
+
@@ -161,7 +178,7 @@
- PT0.0000001S
+ PT0S
@@ -178,12 +195,18 @@
-
+
-
-
+
+
+
+
+
+
+
+
diff --git a/src/Platforms/FIP_DAQ_Control/.config/dotnet-tools.json b/src/Platforms/FIP_DAQ_Control/.config/dotnet-tools.json
new file mode 100644
index 00000000..44b1c27d
--- /dev/null
+++ b/src/Platforms/FIP_DAQ_Control/.config/dotnet-tools.json
@@ -0,0 +1,12 @@
+{
+ "version": 1,
+ "isRoot": true,
+ "tools": {
+ "bonsai.sgen": {
+ "version": "0.6.1",
+ "commands": [
+ "bonsai.sgen"
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Platforms/FIP_DAQ_Control/.gitattributes b/src/Platforms/FIP_DAQ_Control/.gitattributes
new file mode 100644
index 00000000..d2d3c413
--- /dev/null
+++ b/src/Platforms/FIP_DAQ_Control/.gitattributes
@@ -0,0 +1,4 @@
+* text=auto
+*.cmd text eol=crlf
+*.bat text eol=crlf
+*.bonsai text
\ No newline at end of file
diff --git a/src/Platforms/FIP_DAQ_Control/.github/workflows/aind-physio-fip.yml b/src/Platforms/FIP_DAQ_Control/.github/workflows/aind-physio-fip.yml
new file mode 100644
index 00000000..9d08e51f
--- /dev/null
+++ b/src/Platforms/FIP_DAQ_Control/.github/workflows/aind-physio-fip.yml
@@ -0,0 +1,327 @@
+name: FIP test suite
+
+on:
+ workflow_dispatch:
+ inputs:
+ bump_type:
+ description: "Version bump type"
+ required: false
+ default: "rc"
+ type: choice
+ options:
+ - rc
+ - patch
+ - minor
+ - major
+ - stable
+ pull_request:
+ push:
+ branches:
+ - main
+ - dev*
+ - release*
+ release:
+ types: [published]
+
+jobs:
+ # ╔──────────────────────────╗
+ # │ _____ _ │
+ # │ |_ _|__ ___| |_ ___ │
+ # │ | |/ _ \/ __| __/ __| │
+ # │ | | __/\__ \ |_\__ \ │
+ # │ |_|\___||___/\__|___/ │
+ # │ │
+ # ╚──────────────────────────╝
+ tests:
+ runs-on: windows-latest
+ name: FIP unit tests
+ steps:
+ - uses: actions/checkout@v5
+
+ - uses: astral-sh/setup-uv@v6
+ with:
+ enable-cache: true
+
+ - name: Install python dependencies
+ run: uv sync
+
+ - name: Run ruff format
+ run: uv run ruff format
+
+ - name: Run ruff check
+ run: uv run ruff check
+
+ - name: Run codespell
+ run: uv run codespell
+
+ - name: Setup .NET Core SDK
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 8.x
+
+ - name: Restore dotnet tools
+ run: dotnet tool restore
+
+ - name: Setup Bonsai environment
+ working-directory: ./bonsai
+ run: ./setup.ps1
+
+ - name: Run python unit tests
+ run: uv run python -m unittest
+
+ - name: Regenerate schemas
+ run: uv run fip regenerate
+
+ - name: Check for uncommitted changes
+ run: |
+ git config --global core.safecrlf false
+ git diff --exit-code || (echo "Untracked changes found" && exit 1)
+
+ # ╔───────────────────────────────────────────────────────────╗
+ # │ ____ ___ ____ ____ ____ _ │
+ # │ / ___|_ _/ ___| _ \ | _ \ ___| | ___ __ _ ___ ___ │
+ # │ | | | | | | | | | | |_) / _ \ |/ _ \/ _` / __|/ _ \ │
+ # │ | |___ | | |___| |_| | | _ < __/ | __/ (_| \__ \ __/ │
+ # │ \____|___\____|____/ |_| \_\___|_|\___|\__,_|___/\___| │
+ # │ │
+ # ╚───────────────────────────────────────────────────────────╝
+ github-rc-release:
+ needs: tests
+ runs-on: windows-latest
+ if: >
+ github.ref == 'refs/heads/main' &&
+ github.event_name == 'push' &&
+ github.event.head_commit.author.email != 'github-actions[bot]@users.noreply.github.com'
+ name: Create GitHub pre-release
+ steps:
+ - uses: actions/checkout@v5
+ with:
+ fetch-depth: 0
+ ref: main
+
+ - uses: astral-sh/setup-uv@v6
+ with:
+ enable-cache: true
+
+ - name: Bump pre-release by default
+ # Note: Bumping the rc will fail if the version is not currently an rc
+ # To solve it, we bump the patch and rc atomically
+ shell: bash # interop with win/linux for if statement
+ run: |
+ if uv version --bump rc --dry-run; then
+ uv version --bump rc
+ else
+ uv version --bump rc --bump patch
+ fi
+
+ - name: Regenerate schemas
+ run: uv run fip regenerate
+
+ - name: Regenerate examples
+ run: uv run ./examples/example.py --path-seed "./examples/{schema}.json"
+
+ - name: Commit version and schema changes
+ run: |
+ git config --global user.name "github-actions[bot]"
+ git config --global user.email "github-actions[bot]@users.noreply.github.com"
+ git add .
+ git commit -m "Bump version and regenerate schemas [skip ci]" || echo "No changes to commit"
+ git push origin main
+
+ - name: Get version
+ id: get_version
+ shell: bash
+ run: |
+ version=$(uv version --short)
+ echo "version=$version" >> $GITHUB_OUTPUT
+
+ - name: Create GitHub Release
+ uses: softprops/action-gh-release@v2
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ tag_name: v${{ steps.get_version.outputs.version }}
+ name: v${{ steps.get_version.outputs.version }}
+ generate_release_notes: true
+ prerelease: true
+ body: |
+ Automated pre-release v${{ steps.get_version.outputs.version }}
+
+ # ╔─────────────────────────────────────────────────────────────────╗
+ # │ ____ _ _ _ ____ _ │
+ # │ | _ \ _ _| |__ | (_) ___ | _ \ ___| | ___ __ _ ___ ___ │
+ # │ | |_) | | | | '_ \| | |/ __| | |_) / _ \ |/ _ \/ _` / __|/ _ \ │
+ # │ | __/| |_| | |_) | | | (__ | _ < __/ | __/ (_| \__ \ __/ │
+ # │ |_| \__,_|_.__/|_|_|\___| |_| \_\___|_|\___|\__,_|___/\___| │
+ # │ │
+ # ╚─────────────────────────────────────────────────────────────────╝
+
+ prepare-public-release:
+ runs-on: windows-latest
+ name: Prepare files for public release
+ needs: tests
+ if: github.event_name == 'workflow_dispatch'
+ outputs:
+ version: ${{ steps.get_version.outputs.version }}
+ is_prerelease: ${{ steps.check_prerelease.outputs.prerelease }}
+ steps:
+ - uses: actions/checkout@v5
+ with:
+ fetch-depth: 0
+ ref: main
+
+ - uses: astral-sh/setup-uv@v6
+ with:
+ enable-cache: true
+
+ - name: Get target version
+ id: get_version
+ shell: bash
+ run: |
+ # Use bump type from input (defaults to rc)
+ bump_type="${{ github.event.inputs.bump_type || 'rc' }}"
+ echo "Bumping version with type: $bump_type"
+
+ # Handle version bumping based on type
+ if [[ "$bump_type" == "rc" ]]; then
+ # Handle rc bumping logic (same as in github-rc-release)
+ if uv version --bump rc --dry-run; then
+ uv version --bump rc
+ else
+ uv version --bump rc --bump patch
+ fi
+ else
+ # Handle patch, minor, major, stable
+ uv version --bump $bump_type
+ fi
+
+ release_version=$(uv run uv version --short)
+ echo "version=$release_version" >> $GITHUB_OUTPUT
+ echo "Release version will be: $release_version"
+
+ - name: Validate version format
+ run: uv version ${{ steps.get_version.outputs.version }} --dry-run
+
+ - name: Update package version
+ run: uv version ${{ steps.get_version.outputs.version }}
+
+ - name: Regenerate schemas
+ run: uv run fip regenerate
+
+ - name: Regenerate examples
+ run: uv run ./examples/example.py --path-seed "./examples/{schema}.json"
+
+ - name: Commit version, schema changes and create tag
+ run: |
+ git config --global user.name "github-actions[bot]"
+ git config --global user.email "github-actions[bot]@users.noreply.github.com"
+ git add .
+ git commit -m "v${{ steps.get_version.outputs.version }} [skip ci]"
+ git tag -a "v${{ steps.get_version.outputs.version }}" -m "Release v${{ steps.get_version.outputs.version }}"
+ git push origin main
+ git push origin "v${{ steps.get_version.outputs.version }}"
+
+ - name: Determine if prerelease
+ id: check_prerelease
+ shell: bash
+ run: |
+ version="${{ steps.get_version.outputs.version }}"
+ if [[ "$version" == *"rc"* ]]; then
+ echo "prerelease=true" >> $GITHUB_OUTPUT
+ else
+ echo "prerelease=false" >> $GITHUB_OUTPUT
+ fi
+
+ - name: Create GitHub Release
+ uses: softprops/action-gh-release@v2
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ tag_name: v${{ steps.get_version.outputs.version }}
+ name: Release v${{ steps.get_version.outputs.version }}
+ generate_release_notes: true
+ prerelease: ${{ steps.check_prerelease.outputs.prerelease }}
+ body: |
+ Release v${{ steps.get_version.outputs.version }}
+
+ This release was manually triggered.
+
+ publish-to-pypi:
+ runs-on: windows-latest
+ name: Publish to PyPI
+ needs: [tests, prepare-public-release]
+ if: |
+ (github.event_name == 'workflow_dispatch') ||
+ (github.event_name == 'release' &&
+ github.event.action == 'published' &&
+ !github.event.release.prerelease &&
+ startsWith(github.ref, 'refs/tags/v'))
+ steps:
+ - uses: actions/checkout@v5
+ with:
+ fetch-depth: 0
+ ref: ${{ github.event_name == 'workflow_dispatch' && 'main' || github.ref_name }}
+
+ - uses: astral-sh/setup-uv@v6
+ with:
+ enable-cache: true
+
+ - name: Verify version consistency (for automatic releases)
+ if: github.event_name == 'release'
+ shell: bash
+ run: |
+ tag_version=$(echo "${{ github.ref_name }}" | sed 's/^v//')
+ package_version=$(uv run uv version --short)
+ if [[ "$tag_version" != "$package_version" ]]; then
+ echo "ERROR: Tag version ($tag_version) doesn't match package version ($package_version)"
+ exit 1
+ fi
+ echo "✅ Version consistency verified: $tag_version"
+
+ - name: Get current version (for manual releases)
+ if: github.event_name == 'workflow_dispatch'
+ shell: bash
+ run: |
+ package_version=$(uv run uv version --short)
+ echo "📦 Publishing version: $package_version"
+
+ - name: Build
+ run: uv build
+
+ - name: Publish to PyPI
+ run: uv publish --token ${{ secrets.AIND_PYPI_TOKEN }}
+
+ # ╔─────────────────────────╗
+ # │ ____ │
+ # │ | _ \ ___ ___ ___ │
+ # │ | | | |/ _ \ / __/ __| │
+ # │ | |_| | (_) | (__\__ \ │
+ # │ |____/ \___/ \___|___/ │
+ # │ │
+ # ╚─────────────────────────╝
+ build-docs:
+ name: Build and deploy documentation to GitHub Pages
+ runs-on: ubuntu-latest
+ needs: publish-to-pypi
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v5
+
+ - name: Install uv
+ uses: astral-sh/setup-uv@v3
+
+ - name: Setup Graphviz
+ uses: ts-graphviz/setup-graphviz@v2
+
+ - name: Install docs group of dependencies
+ run: uv sync --group docs
+
+ - name: Build Sphinx documentation
+ run: uv run sphinx-build -b html docs/ _build/html
+
+ - name: Deploy to GitHub Pages
+ uses: peaceiris/actions-gh-pages@v4
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ publish_dir: _build/html
+ force_orphan: true
diff --git a/src/Platforms/FIP_DAQ_Control/.gitignore b/src/Platforms/FIP_DAQ_Control/.gitignore
new file mode 100644
index 00000000..80053829
--- /dev/null
+++ b/src/Platforms/FIP_DAQ_Control/.gitignore
@@ -0,0 +1,93 @@
+# Bonsai scripting files
+.vs
+.vscode
+bin
+obj
+Packages
+Bonsai.exe.WebView2
+*.bin
+*.avi
+*.dll
+*.exe
+*.exe.settings
+*.bonsai.layout
+local
+*.snl
+bonsai/settings/
+
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Sphinx documentation
+docs/_build/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+# For a library or package, you might want to ignore these files since the code is
+# intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# mypy
+.mypy_cache/
+
+# ruff
+.ruff_cache/
+
+# venv files
+.venv
diff --git a/src/Platforms/FIP_DAQ_Control/.python-version b/src/Platforms/FIP_DAQ_Control/.python-version
new file mode 100644
index 00000000..06c7113d
--- /dev/null
+++ b/src/Platforms/FIP_DAQ_Control/.python-version
@@ -0,0 +1,3 @@
+3.11
+3.12
+3.13
\ No newline at end of file
diff --git a/src/Platforms/FIP_DAQ_Control/LICENSE b/src/Platforms/FIP_DAQ_Control/LICENSE
new file mode 100644
index 00000000..df0d99bd
--- /dev/null
+++ b/src/Platforms/FIP_DAQ_Control/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 Allen Institute for Neural Dynamics
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/src/Platforms/FIP_DAQ_Control/README.md b/src/Platforms/FIP_DAQ_Control/README.md
new file mode 100644
index 00000000..6429384a
--- /dev/null
+++ b/src/Platforms/FIP_DAQ_Control/README.md
@@ -0,0 +1,179 @@
+# Fiber Photometry System Configuration
+
+
+
+[](LICENSE)
+[](https://github.com/astral-sh/ruff)
+[](https://github.com/astral-sh/uv)
+
+For FIP photometry data acquisition and hardware control.
+
+## Overview
+
+The FIP (Frame-projected Independent Photometry) system is a low-cost, scalable photometry setup designed for chronic recording of optical signals from behaving mice during daily training. The system is based on a modified design of Frame-projected Independent Photometry (Kim et al., 2016), using inexpensive, commercially available, off-the-shelf components.
+
+
+
+For more information, see the [AIND Fiber Photometry Platform Page](https://www.allenneuraldynamics.org/platforms/fiber-photometry) and the following protocols:
+
+* Protocol for system assembly:
+* Protocol for system triggering setup:
+
+## Wavelength Information
+
+The table below summarizes the photometry system's optical configuration, showing the relationship between emission channels and their corresponding excitation sources.
+
+| Excitation | | | | Emission | | |
+|------------------|---------------------|--------------|---|------------------|----------------------|-------------------|
+| **Name** | **Wavelength (nm)** | **Led Name** | | **name** | **Wavelength (nm)** | **Detector Name** |
+| Yellow | 565 | 565 nm LED | | Red | ~590 (peak) | Red CMOS |
+| Blue | 470 | 470 nm LED | | Green | ~510 (peak) | Green CMOS |
+| UV | 415 | 415 nm LED | | Isosbestic | 490-540 (passband) | Green CMOS |
+
+## Signal Detection
+
+* **Green Channel**: Primarily used for green GFP based indicators
+* **Red Channel**: Primarily used for RFP-based indicators (e.g., RdLight)
+* **Isosbestic Channel**: Used as a control measurement; shares same emission path as green but with different excitation
+
+The system uses dedicated CMOS cameras for the red and green emissions, with the isosbestic signal being captured by the green camera under different excitation conditions.
+
+## Temporal Multiplexing
+
+The system employs temporal multiplexing to acquire signals from multiple fluorescent indicators through the same optical fiber. This is achieved by rapidly cycling through different excitation wavelengths while synchronizing camera acquisitions:
+
+```
+ --->| |<--- period = 16.67 ms
+Blue LED(470) ████░░░░░░░░░░░████░░░░░░░░░░░████░░░░░░░░░░░████░░░░░░░░░░░████░░░░░░░░░░░
+
+UV LED (415) ░░░░░████░░░░░░░░░░░████░░░░░░░░░░░████░░░░░░░░░░░████░░░░░░░░░░░████░░░░░░
+
+Yellow LED (560)░░░░░░░░░░████░░░░░░░░░░░████░░░░░░░░░░░████░░░░░░░░░░░████░░░░░░░░░░░████░
+
+Green CMOS ████░████░░░░░░████░████░░░░░░████░████░░░░░░████░████░░░░░░████░████░░░░░░ (captures 470/415)
+Red CMOS ░░░░░░░░░░████░░░░░░░░░░░████░░░░░░░░░░░████░░░░░░░░░░░████░░░░░░░░░░░████░ (captures 560)
+ ───────────────────────────────────────────────────────────────────────────►
+ Time
+```
+
+The temporal multiplexing sequence:
+
+1. Blue LED (470nm) excitation -> Green CMOS camera captures signal from GFP-based sensors
+2. UV LED (415nm) excitation -> Green CMOS camera captures isosbestic signal
+3. Yellow LED (560nm) excitation -> Red CMOS camera captures signal from RFP-based sensors
+
+This cycling occurs at 60 Hz, allowing near-simultaneous measurement of multiple signals while preventing crosstalk between channels. Each LED is activated in sequence and cameras are synchronized to capture data only during their respective LED's ON period.
+
+## Using the acquisition system
+
+See [wiki](https://github.com/AllenNeuralDynamics/FIP_DAQ_Control/wiki) for AIND internal installation instructions.
+
+### Pre-requisites (some of these are optional, but recommended for a smoother experience)
+
+* [Visual Studio Code](https://code.visualstudio.com/) (highly recommended for editing code scripts and git commits)
+* [Git for Windows](https://gitforwindows.org/) (highly recommended for cloning and manipulating this repository)
+* [.NET Framework 4.7.2 Developer Pack](https://dotnet.microsoft.com/download/dotnet-framework/thank-you/net472-developer-pack-offline-installer) (required for intellisense when editing code scripts)
+* [Visual C++ Redistributable for Visual Studio 2012](https://www.microsoft.com/en-us/download/details.aspx?id=30679) (native dependency for OpenCV)
+* [Spinnaker SDK 1.29.0.5](https://www.teledynevisionsolutions.com/support/support-center/software-firmware-downloads/iis/spinnaker-sdk-download/spinnaker-sdk--download-files/#anchor6) (device drivers for FLIR cameras)
+ * On FLIR website: `Download > archive > 1.29.0.5 > SpinnakerSDK_FULL_1.29.0.5_x64.exe`
+* [Optional] [UV Python environment manager](https://docs.astral.sh/uv/getting-started/installation/) (highly recommended for managing the Python environment for this project. All examples will assume usage of `uv`. Alternatively, you can use other environment managers such as `venv` or `conda` to create a Python environment and install the required dependencies listed in the package metadata: `pyproject.toml`)
+
+### Installation Steps
+
+1. Clone this repository
+2. Create the environments for Bonsai, run `./bonsai/setup.cmd` (can be by double-clickin it too). This is required to run experiments using the Bonsai script in an experimental PC.
+3. [Optional] Create the environments for Python, run `uv venv` if using uv, or create a virtual environment using your preferred method. This is only used to run the Python script generating configuration files. (rig PCs can inherit those files from somewhere else)
+ * Alternatively, if you are using uv, run `./scripts/deploy.ps1` to bootstrap a Python and Bonsai environment at the same time for the project automatically.
+
+### Generating input configurations [Optional]
+
+The current pipeline relies on two input configuration files. These configure the rig/instruments and session parameters, respectively. These files are formalized as pydantic models as shown in `./examples/examples.py`. Template configuration files are included in the `./examples/` folder, and running the `examples.py` will create configuration files into `./local/`
+
+Briefly:
+
+```python
+from aind_behavior_services.session import AindBehaviorSessionModel
+from aind_physiology_fip.rig import AindPhysioFipRig
+
+this_rig = AindPhysioFipRig(...)
+this_session = AindBehaviorSessionModel(...)
+
+for model in [this_session, this_rig]:
+ with open(model.__class__.__name__ + ".json", "w", encoding="utf-8") as f:
+ f.write(model.model_dump_json(indent=2))
+```
+
+### Running the acquisition
+
+#### Running manually
+
+Acquisition is done through Bonsai via a single entry-point workflow. As any Bonsai workflow, one can run the acquisition workflow via the editor:
+
+* Open Bonsai from the bootstrapped environment in `./bonsai/bonsai.exe`
+* Open the workflow file `./src/main.bonsai`
+* Manually set the two highest level properties `RigPath` and `SessionPath` to the paths of the configuration files generated in the [previous step](#generating-input-configurations).
+* Launch the workflow by clicking the "Run" button in the Bonsai editor.
+* Settings in FipRig.json such as `camera_green_iso` `serial_number` and `cuttlefish_fip` `port_name` needs to be modified per PC for Bonsai to detect those hardware.
+
+> [!Important]
+> `AindBehaviorSessionModel.allow_dirty` property will be checked at the start of the workflow. If set to `False` the workflow will immediately throw an error and stop execution if the repository has uncommitted changes. If the user intends to run the workflow with a dirty repository, they should set this property to `True` in the session configuration file.
+
+#### Running via CLI
+
+The workflow can be launched via the Bonsai Command Line Interface (CLI). Additional documentation on the CLI can be found [here](https://bonsai-rx.org/docs/articles/cli.html).
+To run the acquisition workflow using the CLI, use the following command:
+
+```bash
+"./bonsai/bonsai.exe" "./src/main.bonsai" -p RigPath="../path/to/rig.json" -p SessionPath="../path/to/session.json"
+```
+
+> [!Note]
+> The paths to the configuration file are relative to the workflow working directory (i.e. `./src/`)
+
+Additional flags can be passed to automatically start the workflow (`--start`) or run in headless mode (`--no-editor`) as stated in the Bonsai CLI documentation.
+
+#### Acquiring data
+
+Once the workflow is running, a UI will pop up and users can start acquisition by clicking `Start`. The system will then begin to acquire data from the cameras and store it in the specified session directory. Once the session is ready to stop, users can click `Stop` in the UI. The system will then save the session data and stop/close the workflow.
+
+## Contributors
+
+Contributions to this repository are welcome! However, please ensure that your code adheres to the recommended DevOps practices below:
+
+### Linting
+
+We use [ruff](https://docs.astral.sh/ruff/) as our primary linting tool:.
+
+```bash
+ uv run ruff format .
+ uv run ruff check .
+```
+
+### Testing
+
+Attempt to add tests when new features are added.
+To run the currently available tests, run `uv run python -m unittest` from the root of the repository.
+
+### Lock files
+
+We use [uv](https://docs.astral.sh/uv/) to manage our lock files and therefore encourage everyone to use uv as a package manager as well.
+
+## CLI
+
+The package provides a command line interface (CLI) to facilitate common tasks. The CLI can be accessed by running the following command from the root of the repository:
+
+```bash
+ uv run fip [options]
+```
+
+For a list of available subcommands and options, run:
+
+```bash
+ uv run fip --help
+```
+
+(If you are not using `uv`, activate your python environment and run the `fip` tool directly.)
+
+## Regenerating schemas
+
+Instructions for regenerating schemas can be found [here](https://github.com/AllenNeuralDynamics/Aind.Behavior.Services?tab=readme-ov-file#regenerating-schemas).
diff --git a/src/Platforms/FIP_DAQ_Control/assets/images/fip_light_path.png b/src/Platforms/FIP_DAQ_Control/assets/images/fip_light_path.png
new file mode 100644
index 00000000..9d6e016e
Binary files /dev/null and b/src/Platforms/FIP_DAQ_Control/assets/images/fip_light_path.png differ
diff --git a/src/Platforms/FIP_DAQ_Control/bonsai/Bonsai.config b/src/Platforms/FIP_DAQ_Control/bonsai/Bonsai.config
new file mode 100644
index 00000000..8c443d4a
--- /dev/null
+++ b/src/Platforms/FIP_DAQ_Control/bonsai/Bonsai.config
@@ -0,0 +1,235 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Platforms/FIP_DAQ_Control/bonsai/NuGet.config b/src/Platforms/FIP_DAQ_Control/bonsai/NuGet.config
new file mode 100644
index 00000000..bf53ec14
--- /dev/null
+++ b/src/Platforms/FIP_DAQ_Control/bonsai/NuGet.config
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/src/Platforms/FIP_DAQ_Control/bonsai/setup.cmd b/src/Platforms/FIP_DAQ_Control/bonsai/setup.cmd
new file mode 100644
index 00000000..3c53f3a9
--- /dev/null
+++ b/src/Platforms/FIP_DAQ_Control/bonsai/setup.cmd
@@ -0,0 +1 @@
+powershell -ExecutionPolicy Bypass -File .\setup.ps1
\ No newline at end of file
diff --git a/src/Platforms/FIP_DAQ_Control/bonsai/setup.ps1 b/src/Platforms/FIP_DAQ_Control/bonsai/setup.ps1
new file mode 100644
index 00000000..217c7197
--- /dev/null
+++ b/src/Platforms/FIP_DAQ_Control/bonsai/setup.ps1
@@ -0,0 +1,20 @@
+Push-Location $PSScriptRoot
+if (!(Test-Path "./Bonsai.exe")) {
+ $release = "https://github.com/bonsai-rx/bonsai/releases/latest/download/Bonsai.zip"
+ $configPath = "./Bonsai.config"
+ if (Test-Path $configPath) {
+ [xml]$config = Get-Content $configPath
+ $bootstrapper = $config.PackageConfiguration.Packages.Package.where{$_.id -eq 'Bonsai'}
+ if ($bootstrapper) {
+ $version = $bootstrapper.version
+ $release = "https://github.com/bonsai-rx/bonsai/releases/download/$version/Bonsai.zip"
+ }
+ }
+ Invoke-WebRequest $release -OutFile "temp.zip"
+ Move-Item -Path "NuGet.config" "temp.config" -ErrorAction SilentlyContinue
+ Expand-Archive "temp.zip" -DestinationPath "." -Force
+ Move-Item -Path "temp.config" "NuGet.config" -Force -ErrorAction SilentlyContinue
+ Remove-Item -Path "temp.zip"
+}
+& .\Bonsai.exe --no-editor
+Pop-Location
diff --git a/src/Platforms/FIP_DAQ_Control/docs/Makefile b/src/Platforms/FIP_DAQ_Control/docs/Makefile
new file mode 100644
index 00000000..d4bb2cbb
--- /dev/null
+++ b/src/Platforms/FIP_DAQ_Control/docs/Makefile
@@ -0,0 +1,20 @@
+# Minimal makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line, and also
+# from the environment for the first two.
+SPHINXOPTS ?=
+SPHINXBUILD ?= sphinx-build
+SOURCEDIR = .
+BUILDDIR = _build
+
+# Put it first so that "make" without argument is like "make help".
+help:
+ @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+.PHONY: help Makefile
+
+# Catch-all target: route all unknown targets to Sphinx using the new
+# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
+%: Makefile
+ @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
diff --git a/src/Platforms/FIP_DAQ_Control/docs/_static/dark-logo.svg b/src/Platforms/FIP_DAQ_Control/docs/_static/dark-logo.svg
new file mode 100644
index 00000000..dcc68fb1
--- /dev/null
+++ b/src/Platforms/FIP_DAQ_Control/docs/_static/dark-logo.svg
@@ -0,0 +1,129 @@
+
+
+
diff --git a/src/Platforms/FIP_DAQ_Control/docs/_static/favicon.ico b/src/Platforms/FIP_DAQ_Control/docs/_static/favicon.ico
new file mode 100644
index 00000000..4cec1504
Binary files /dev/null and b/src/Platforms/FIP_DAQ_Control/docs/_static/favicon.ico differ
diff --git a/src/Platforms/FIP_DAQ_Control/docs/_static/light-logo.svg b/src/Platforms/FIP_DAQ_Control/docs/_static/light-logo.svg
new file mode 100644
index 00000000..b20cb67d
--- /dev/null
+++ b/src/Platforms/FIP_DAQ_Control/docs/_static/light-logo.svg
@@ -0,0 +1,128 @@
+
+
+
diff --git a/src/Platforms/FIP_DAQ_Control/docs/api.rig.rst b/src/Platforms/FIP_DAQ_Control/docs/api.rig.rst
new file mode 100644
index 00000000..583f4265
--- /dev/null
+++ b/src/Platforms/FIP_DAQ_Control/docs/api.rig.rst
@@ -0,0 +1,7 @@
+api.rig
+-------------
+
+.. automodule:: aind_physiology_fip.rig
+ :members:
+ :undoc-members:
+ :show-inheritance:
diff --git a/src/Platforms/FIP_DAQ_Control/docs/api.rst b/src/Platforms/FIP_DAQ_Control/docs/api.rst
new file mode 100644
index 00000000..b0b82857
--- /dev/null
+++ b/src/Platforms/FIP_DAQ_Control/docs/api.rst
@@ -0,0 +1,7 @@
+api
+-------------
+.. toctree::
+ :maxdepth: 2
+
+ api.session
+ api.rig
diff --git a/src/Platforms/FIP_DAQ_Control/docs/api.session.rst b/src/Platforms/FIP_DAQ_Control/docs/api.session.rst
new file mode 100644
index 00000000..88b54f51
--- /dev/null
+++ b/src/Platforms/FIP_DAQ_Control/docs/api.session.rst
@@ -0,0 +1,7 @@
+api.session
+-------------
+
+.. autopydantic_model:: aind_behavior_services.session.AindBehaviorSessionModel
+ :members:
+ :undoc-members:
+ :show-inheritance:
diff --git a/src/Platforms/FIP_DAQ_Control/docs/conf.py b/src/Platforms/FIP_DAQ_Control/docs/conf.py
new file mode 100644
index 00000000..ff0cc425
--- /dev/null
+++ b/src/Platforms/FIP_DAQ_Control/docs/conf.py
@@ -0,0 +1,82 @@
+import os
+import sys
+
+import erdantic as erd
+from pydantic import BaseModel
+
+import aind_physiology_fip
+import aind_physiology_fip.rig
+
+sys.path.insert(0, os.path.abspath("../src/DataSchemas"))
+
+SOURCE_ROOT = "https://github.com/AllenNeuralDynamics/Aind.Physiology.Fip/tree/main/src/DataSchemas/"
+
+# -- Project information -----------------------------------------------------
+# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
+
+project = "Aind Physiology Fip project"
+copyright = "2025, Allen Institute for Neural Dynamics"
+author = "Bruno Cruz"
+release = aind_physiology_fip.__semver__
+
+# -- General configuration ---------------------------------------------------
+# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
+
+extensions = [
+ "sphinx-jsonschema",
+ "sphinx_jinja",
+ "sphinx.ext.napoleon",
+ "sphinx.ext.autodoc",
+ "sphinx.ext.autosummary",
+ "sphinx.ext.intersphinx",
+ "sphinx.ext.githubpages",
+ "sphinx.ext.linkcode",
+ "sphinx_mdinclude",
+ "sphinxcontrib.autodoc_pydantic",
+]
+
+templates_path = ["_templates"]
+exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
+
+autosummary_generate = True
+
+autodoc_pydantic_model_show_json = False
+autodoc_pydantic_settings_show_json = False
+
+# -- Options for HTML output -------------------------------------------------
+# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
+
+html_theme = "furo"
+html_static_path = ["_static"]
+html_title = "Aind Physiology Fip"
+html_favicon = "_static/favicon.ico"
+html_theme_options = {
+ "light_logo": "light-logo.svg",
+ "dark_logo": "dark-logo.svg",
+}
+
+# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
+html_show_sphinx = False
+
+# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
+html_show_copyright = False
+
+
+# -- Options for linkcode extension ---------------------------------------
+def linkcode_resolve(domain, info):
+ if domain != "py":
+ return None
+ if not info["module"]:
+ return None
+ filename = info["module"].replace(".", "/")
+ return f"{SOURCE_ROOT}/{filename}.py"
+
+
+# -- Class diagram generation (Optional) ---------------------------------------
+def export_model_diagram(model: BaseModel, root: str = "_static") -> None:
+ diagram = erd.create(model)
+ diagram.draw(f"{root}/{model.__name__}.svg")
+
+
+_diagram_root = "_static"
+# export_model_diagram(MyPydanticBaseModel, _diagram_root)
diff --git a/src/Platforms/FIP_DAQ_Control/docs/index.rst b/src/Platforms/FIP_DAQ_Control/docs/index.rst
new file mode 100644
index 00000000..2ab96a76
--- /dev/null
+++ b/src/Platforms/FIP_DAQ_Control/docs/index.rst
@@ -0,0 +1,21 @@
+Welcome to the Aind Physiology Fip project documentation!
+========================================================================
+
+.. mdinclude:: ../README.md
+
+.. toctree::
+ :maxdepth: 2
+ :caption: Contents:
+ :glob:
+
+ self
+ api
+ json-schemas
+ articles/*
+ GitHub Source Code
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`search`
diff --git a/src/Platforms/FIP_DAQ_Control/docs/json-schemas.rst b/src/Platforms/FIP_DAQ_Control/docs/json-schemas.rst
new file mode 100644
index 00000000..1e5a1ab3
--- /dev/null
+++ b/src/Platforms/FIP_DAQ_Control/docs/json-schemas.rst
@@ -0,0 +1,12 @@
+json-schema
+-------------
+The following json-schemas are used as the format definition of the input for this task. They are the result of the `Pydantic`` models defined in `src/aind_physiology_fip`.
+
+`Download Schema `_
+
+
+Rig Schema
+~~~~~~~~~~~~~~
+.. jsonschema:: https://raw.githubusercontent.com/AllenNeuralDynamics/Aind.Physiology.Fip/main/src/DataSchemas/aind_physio_fip_rig.json#/$defs/AindPhysioFipRig
+ :lift_definitions:
+ :auto_reference:
\ No newline at end of file
diff --git a/src/Platforms/FIP_DAQ_Control/docs/make.bat b/src/Platforms/FIP_DAQ_Control/docs/make.bat
new file mode 100644
index 00000000..954237b9
--- /dev/null
+++ b/src/Platforms/FIP_DAQ_Control/docs/make.bat
@@ -0,0 +1,35 @@
+@ECHO OFF
+
+pushd %~dp0
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+ set SPHINXBUILD=sphinx-build
+)
+set SOURCEDIR=.
+set BUILDDIR=_build
+
+%SPHINXBUILD% >NUL 2>NUL
+if errorlevel 9009 (
+ echo.
+ echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
+ echo.installed, then set the SPHINXBUILD environment variable to point
+ echo.to the full path of the 'sphinx-build' executable. Alternatively you
+ echo.may add the Sphinx directory to PATH.
+ echo.
+ echo.If you don't have Sphinx installed, grab it from
+ echo.https://www.sphinx-doc.org/
+ exit /b 1
+)
+
+if "%1" == "" goto help
+
+%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
+goto end
+
+:help
+%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
+
+:end
+popd
diff --git a/src/Platforms/FIP_DAQ_Control/examples/AindBehaviorSessionModel.json b/src/Platforms/FIP_DAQ_Control/examples/AindBehaviorSessionModel.json
new file mode 100644
index 00000000..d751e339
--- /dev/null
+++ b/src/Platforms/FIP_DAQ_Control/examples/AindBehaviorSessionModel.json
@@ -0,0 +1,18 @@
+{
+ "aind_behavior_services_pkg_version": "0.12.3",
+ "version": "0.12.3",
+ "experiment": "AindPhysioFip",
+ "experimenter": [
+ "Foo",
+ "Bar"
+ ],
+ "date": "2025-10-20T00:46:40.173309Z",
+ "root_path": "c://",
+ "session_name": "test_2025-10-20T004640Z",
+ "subject": "test",
+ "experiment_version": "0.0.0",
+ "notes": "test session",
+ "commit_hash": null,
+ "allow_dirty_repo": true,
+ "skip_hardware_validation": false
+}
\ No newline at end of file
diff --git a/src/Platforms/FIP_DAQ_Control/examples/AindPhysioFipRig.json b/src/Platforms/FIP_DAQ_Control/examples/AindPhysioFipRig.json
new file mode 100644
index 00000000..d11490ee
--- /dev/null
+++ b/src/Platforms/FIP_DAQ_Control/examples/AindPhysioFipRig.json
@@ -0,0 +1,205 @@
+{
+ "aind_behavior_services_pkg_version": "0.12.3",
+ "version": "0.1.2-rc2",
+ "computer_name": "test_computer",
+ "rig_name": "test_rig",
+ "camera_green_iso": {
+ "device_type": "FipCamera",
+ "name": "FipCamera",
+ "additional_settings": null,
+ "calibration": null,
+ "serial_number": "000000",
+ "gain": 0.0,
+ "offset": {
+ "x": 0.0,
+ "y": 0.0
+ }
+ },
+ "camera_red": {
+ "device_type": "FipCamera",
+ "name": "FipCamera",
+ "additional_settings": null,
+ "calibration": null,
+ "serial_number": "000001",
+ "gain": 0.0,
+ "offset": {
+ "x": 0.0,
+ "y": 0.0
+ }
+ },
+ "light_source_uv": {
+ "device_type": "LightSource",
+ "name": "LightSource",
+ "additional_settings": null,
+ "calibration": null,
+ "power": 0.1,
+ "task": {
+ "delta_1": 15650,
+ "delta_2": 666,
+ "delta_3": 300,
+ "delta_4": 50,
+ "light_source_port": 8,
+ "camera_port": 1,
+ "events_enabled": true,
+ "mute_output": false,
+ "pwm_frequency": 10000.0
+ }
+ },
+ "light_source_blue": {
+ "device_type": "LightSource",
+ "name": "mock_device",
+ "additional_settings": null,
+ "calibration": {
+ "device_name": "mock_device",
+ "input": null,
+ "output": {
+ "power_lut": {
+ "0.0": 0.0,
+ "0.1": 10.0,
+ "0.2": 20.0
+ }
+ },
+ "date": null,
+ "description": null,
+ "notes": null
+ },
+ "power": 10.0,
+ "task": {
+ "delta_1": 15650,
+ "delta_2": 666,
+ "delta_3": 300,
+ "delta_4": 50,
+ "light_source_port": 4,
+ "camera_port": 1,
+ "events_enabled": true,
+ "mute_output": false,
+ "pwm_frequency": 10000.0
+ }
+ },
+ "light_source_lime": {
+ "device_type": "LightSource",
+ "name": "mock_device",
+ "additional_settings": null,
+ "calibration": {
+ "device_name": "mock_device",
+ "input": null,
+ "output": {
+ "power_lut": {
+ "0.0": 0.0,
+ "0.1": 10.0,
+ "0.2": 20.0
+ }
+ },
+ "date": null,
+ "description": null,
+ "notes": null
+ },
+ "power": 20.0,
+ "task": {
+ "delta_1": 15650,
+ "delta_2": 666,
+ "delta_3": 300,
+ "delta_4": 50,
+ "light_source_port": 16,
+ "camera_port": 2,
+ "events_enabled": true,
+ "mute_output": false,
+ "pwm_frequency": 10000.0
+ }
+ },
+ "roi_settings": {
+ "camera_green_iso_background": {
+ "center": {
+ "x": 10.0,
+ "y": 10.0
+ },
+ "radius": 10.0
+ },
+ "camera_red_background": {
+ "center": {
+ "x": 10.0,
+ "y": 10.0
+ },
+ "radius": 10.0
+ },
+ "camera_green_iso_roi": [
+ {
+ "center": {
+ "x": 50.0,
+ "y": 50.0
+ },
+ "radius": 20.0
+ },
+ {
+ "center": {
+ "x": 50.0,
+ "y": 150.0
+ },
+ "radius": 20.0
+ },
+ {
+ "center": {
+ "x": 150.0,
+ "y": 50.0
+ },
+ "radius": 20.0
+ },
+ {
+ "center": {
+ "x": 150.0,
+ "y": 150.0
+ },
+ "radius": 20.0
+ }
+ ],
+ "camera_red_roi": [
+ {
+ "center": {
+ "x": 50.0,
+ "y": 50.0
+ },
+ "radius": 20.0
+ },
+ {
+ "center": {
+ "x": 50.0,
+ "y": 150.0
+ },
+ "radius": 20.0
+ },
+ {
+ "center": {
+ "x": 150.0,
+ "y": 50.0
+ },
+ "radius": 20.0
+ },
+ {
+ "center": {
+ "x": 150.0,
+ "y": 150.0
+ },
+ "radius": 20.0
+ }
+ ]
+ },
+ "cuttlefish_fip": {
+ "device_type": "cuTTLefishFip",
+ "name": "cuTTLefishFip",
+ "additional_settings": null,
+ "calibration": null,
+ "who_am_i": 1407,
+ "serial_number": null,
+ "port_name": "COM1"
+ },
+ "networking": {
+ "zmq_publisher": {
+ "connection_string": "@tcp://localhost:5556",
+ "topic": "fip"
+ },
+ "zmq_subscriber": {
+ "connection_string": "@tcp://localhost:5557",
+ "topic": "fip"
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Platforms/FIP_DAQ_Control/examples/example.py b/src/Platforms/FIP_DAQ_Control/examples/example.py
new file mode 100644
index 00000000..3b5aae45
--- /dev/null
+++ b/src/Platforms/FIP_DAQ_Control/examples/example.py
@@ -0,0 +1,98 @@
+import argparse
+import datetime
+import os
+
+from aind_behavior_services.rig.harp import HarpCuttlefishfip
+from aind_behavior_services.session import AindBehaviorSessionModel
+
+from aind_physiology_fip.rig import (
+ AindPhysioFipRig,
+ FipCamera,
+ FipTask,
+ LightSource,
+ LightSourceCalibration,
+ LightSourceCalibrationOutput,
+ Networking,
+ Ports,
+ RoiSettings,
+)
+
+
+def mock_session() -> AindBehaviorSessionModel:
+ """Generates a mock AindBehaviorSessionModel model"""
+ return AindBehaviorSessionModel(
+ date=datetime.datetime.now(tz=datetime.timezone.utc),
+ experiment="AindPhysioFip",
+ root_path="c://",
+ subject="test",
+ notes="test session",
+ experiment_version="0.0.0",
+ allow_dirty_repo=True,
+ skip_hardware_validation=False,
+ experimenter=["Foo", "Bar"],
+ )
+
+
+def mock_rig() -> AindPhysioFipRig:
+ mock_calibration = LightSourceCalibration(
+ device_name="mock_device", output=LightSourceCalibrationOutput(power_lut={0: 0, 0.1: 10, 0.2: 20})
+ )
+
+ return AindPhysioFipRig(
+ rig_name="test_rig",
+ computer_name="test_computer",
+ camera_green_iso=FipCamera(serial_number="000000"),
+ camera_red=FipCamera(serial_number="000001"),
+ light_source_blue=LightSource(
+ power=10,
+ calibration=mock_calibration,
+ task=FipTask(
+ camera_port=Ports.IO0, # GreenCamera + 470nm
+ light_source_port=Ports.IO2,
+ ),
+ ),
+ light_source_lime=LightSource(
+ power=20,
+ calibration=mock_calibration,
+ task=FipTask(
+ camera_port=Ports.IO1, # RedCamera + 560nm
+ light_source_port=Ports.IO4,
+ ),
+ ),
+ light_source_uv=LightSource(
+ power=0.1,
+ calibration=None,
+ task=FipTask(
+ camera_port=Ports.IO0, # GreenCamera + 410nm
+ light_source_port=Ports.IO3,
+ ),
+ ),
+ roi_settings=RoiSettings(),
+ networking=Networking(),
+ cuttlefish_fip=HarpCuttlefishfip(
+ port_name="COM1",
+ ),
+ )
+
+
+def main():
+ parser = argparse.ArgumentParser(description="Generate mock session and rig JSON files")
+ parser.add_argument(
+ "--path-seed",
+ default="./local/{schema}.json",
+ help="Path template for output files (default: ./local/{schema}.json)",
+ )
+ args = parser.parse_args()
+
+ example_session = mock_session()
+ example_rig = mock_rig()
+
+ os.makedirs(os.path.dirname(args.path_seed), exist_ok=True)
+
+ for model in [example_session, example_rig]:
+ with open(args.path_seed.format(schema=model.__class__.__name__), "w", encoding="utf-8") as f:
+ f.write(model.model_dump_json(indent=2))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/src/Platforms/FIP_DAQ_Control/pyproject.toml b/src/Platforms/FIP_DAQ_Control/pyproject.toml
new file mode 100644
index 00000000..9ffbe503
--- /dev/null
+++ b/src/Platforms/FIP_DAQ_Control/pyproject.toml
@@ -0,0 +1,67 @@
+[build-system]
+requires = ["uv_build>=0.8.22"]
+build-backend = "uv_build"
+
+[project]
+name = "aind-physiology-fip"
+description = "A repository for Fiber Photometry data acquisition"
+authors = [
+ { name = "Bruno Cruz", email = "bruno.cruz@alleninstitute.org" },
+ { name = "Kenta Hagihara", email = "kenta.hagihara@alleninstitute.org" },
+]
+license = { text = "MIT" }
+requires-python = ">=3.11"
+version = "0.1.2rc2"
+readme = { file = "README.md", content-type = "text/markdown" }
+classifiers = [
+ "Programming Language :: Python :: 3.11",
+ "License :: OSI Approved :: MIT License",
+ "Operating System :: Microsoft :: Windows",
+]
+
+dependencies = ["aind_behavior_services<0.13"]
+
+[project.urls]
+Documentation = "https://allenneuraldynamics.github.io/Aind.Physiology.Fip/"
+Repository = "https://github.com/AllenNeuralDynamics/Aind.Physiology.Fip/"
+Issues = "https://github.com/AllenNeuralDynamics/Aind.Physiology.Fip/issues"
+Changelog = "https://github.com/AllenNeuralDynamics/Aind.Physiology.Fip/releases"
+
+[project.optional-dependencies]
+
+data = ["contraqctor < 0.6", "pydantic_settings", "opencv-python"]
+
+[dependency-groups]
+
+dev = ["ruff", "codespell", "aind-physiology-fip[data]"]
+
+docs = [
+ 'Sphinx',
+ 'furo',
+ 'sphinx-jinja',
+ 'autodoc_pydantic[erdantic]',
+ 'sphinx-jsonschema',
+ 'sphinx-copybutton',
+ "sphinx_mdinclude",
+]
+
+[project.scripts]
+fip = "aind_physiology_fip.cli:main"
+
+[tool.ruff]
+line-length = 120
+target-version = 'py311'
+
+[tool.ruff.lint]
+extend-select = ['Q', 'RUF100', 'C90', 'I']
+extend-ignore = []
+mccabe = { max-complexity = 14 }
+pydocstyle = { convention = 'google' }
+
+[tool.codespell]
+skip = '.git,*.pdf,*.svg,./bonsai,*.bonsai,./docs/_build,uv.lock'
+ignore-words-list = 'nd'
+
+[tool.uv]
+default-groups = ['dev']
+required-version = '>=0.8.4'
diff --git a/src/Platforms/FIP_DAQ_Control/scripts/deploy.cmd b/src/Platforms/FIP_DAQ_Control/scripts/deploy.cmd
new file mode 100644
index 00000000..98b4bba3
--- /dev/null
+++ b/src/Platforms/FIP_DAQ_Control/scripts/deploy.cmd
@@ -0,0 +1 @@
+powershell -ExecutionPolicy Bypass -File .\scripts\deploy.ps1
\ No newline at end of file
diff --git a/src/Platforms/FIP_DAQ_Control/scripts/deploy.ps1 b/src/Platforms/FIP_DAQ_Control/scripts/deploy.ps1
new file mode 100644
index 00000000..2bd416e6
--- /dev/null
+++ b/src/Platforms/FIP_DAQ_Control/scripts/deploy.ps1
@@ -0,0 +1,27 @@
+$scriptPath = $MyInvocation.MyCommand.Path
+$scriptDirectory = Split-Path -Parent $scriptPath
+Set-Location (Split-Path -Parent $scriptDirectory)
+Write-Output "Creating a Python environment..."
+
+if (-not (Get-Command uv -ErrorAction SilentlyContinue)) {
+ throw "The 'uv' command was not found. See https://docs.astral.sh/uv/getting-started/installation/ for instructions."
+}
+
+if (Test-Path -Path ./.venv) {
+ Remove-Item ./.venv -Recurse -Force
+}
+&uv venv
+.\.venv\Scripts\Activate.ps1
+Write-Output "Synchronizing environment..."
+&uv sync
+Write-Output "Creating a Bonsai environment and installing packages..."
+if (Test-Path -Path "bonsai") {
+ Set-Location "bonsai"
+ .\setup.ps1
+} elseif (Test-Path -Path ".bonsai") {
+ Set-Location ".bonsai"
+ .\setup.ps1
+} else {
+ throw "Neither 'bonsai' nor '.bonsai' directory found."
+}
+Set-Location ..
\ No newline at end of file
diff --git a/src/Platforms/FIP_DAQ_Control/src/DataSchemas/aind_physiology_fip.json b/src/Platforms/FIP_DAQ_Control/src/DataSchemas/aind_physiology_fip.json
new file mode 100644
index 00000000..363a2562
--- /dev/null
+++ b/src/Platforms/FIP_DAQ_Control/src/DataSchemas/aind_physiology_fip.json
@@ -0,0 +1,804 @@
+{
+ "$defs": {
+ "AindBehaviorSessionModel": {
+ "properties": {
+ "aind_behavior_services_pkg_version": {
+ "default": "0.12.3",
+ "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$",
+ "title": "aind_behavior_services package version",
+ "type": "string"
+ },
+ "version": {
+ "const": "0.12.3",
+ "default": "0.12.3",
+ "title": "Version",
+ "type": "string"
+ },
+ "experiment": {
+ "default": null,
+ "description": "Name of the experiment",
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Experiment"
+ },
+ "experimenter": {
+ "default": [],
+ "description": "Name of the experimenter",
+ "items": {
+ "type": "string"
+ },
+ "title": "Experimenter",
+ "type": "array"
+ },
+ "date": {
+ "description": "Date of the experiment",
+ "format": "date-time",
+ "title": "Date",
+ "type": "string"
+ },
+ "root_path": {
+ "description": "Root path where data will be logged",
+ "title": "Root Path",
+ "type": "string"
+ },
+ "session_name": {
+ "default": null,
+ "description": "Name of the session. This will be used to create a folder in the root path. If not provided, it will be generated using subject and date.",
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Session Name"
+ },
+ "subject": {
+ "description": "Name of the subject",
+ "title": "Subject",
+ "type": "string"
+ },
+ "experiment_version": {
+ "default": "",
+ "deprecated": true,
+ "description": "Version of the experiment",
+ "title": "Experiment Version",
+ "type": "string"
+ },
+ "notes": {
+ "default": null,
+ "description": "Notes about the experiment",
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Notes"
+ },
+ "commit_hash": {
+ "default": null,
+ "description": "Commit hash of the repository",
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Commit Hash"
+ },
+ "allow_dirty_repo": {
+ "default": false,
+ "description": "Allow running from a dirty repository",
+ "title": "Allow Dirty Repo",
+ "type": "boolean"
+ },
+ "skip_hardware_validation": {
+ "default": false,
+ "description": "Skip hardware validation",
+ "title": "Skip Hardware Validation",
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "root_path",
+ "subject"
+ ],
+ "title": "AindBehaviorSessionModel",
+ "type": "object"
+ },
+ "AindPhysioFipRig": {
+ "description": "Complete rig configuration model for AIND FIP photometry system.",
+ "properties": {
+ "aind_behavior_services_pkg_version": {
+ "default": "0.12.3",
+ "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$",
+ "title": "aind_behavior_services package version",
+ "type": "string"
+ },
+ "version": {
+ "const": "0.1.2-rc2",
+ "default": "0.1.2-rc2",
+ "title": "Version",
+ "type": "string"
+ },
+ "computer_name": {
+ "description": "Computer name",
+ "title": "Computer Name",
+ "type": "string"
+ },
+ "rig_name": {
+ "description": "Rig name",
+ "title": "Rig Name",
+ "type": "string"
+ },
+ "camera_green_iso": {
+ "$ref": "#/$defs/FipCamera",
+ "description": "Camera for the green and iso channels",
+ "title": "G/Iso Camera"
+ },
+ "camera_red": {
+ "$ref": "#/$defs/FipCamera",
+ "description": "Red camera",
+ "title": "Red Camera"
+ },
+ "light_source_uv": {
+ "$ref": "#/$defs/LightSource",
+ "description": "UV (415nm) light source",
+ "title": "UV light source"
+ },
+ "light_source_blue": {
+ "$ref": "#/$defs/LightSource",
+ "description": "Blue (470nm) light source",
+ "title": "Blue light source"
+ },
+ "light_source_lime": {
+ "$ref": "#/$defs/LightSource",
+ "description": "Lime (560nm) light source",
+ "title": "Lime light source"
+ },
+ "roi_settings": {
+ "default": null,
+ "description": "Region of interest settings. Leave empty to attempt to load from local file or manually define it in the program.",
+ "oneOf": [
+ {
+ "$ref": "#/$defs/RoiSettings"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Region of interest settings"
+ },
+ "cuttlefish_fip": {
+ "$ref": "#/$defs/HarpCuttlefishfip",
+ "description": "CuttlefishFip board for controlling the trigger of cameras and light-sources",
+ "title": "CuttlefishFip"
+ },
+ "networking": {
+ "$ref": "#/$defs/Networking",
+ "default": {
+ "zmq_publisher": {
+ "connection_string": "@tcp://localhost:5556",
+ "topic": "fip"
+ },
+ "zmq_subscriber": {
+ "connection_string": "@tcp://localhost:5557",
+ "topic": "fip"
+ }
+ },
+ "description": "Networking settings"
+ }
+ },
+ "required": [
+ "rig_name",
+ "camera_green_iso",
+ "camera_red",
+ "light_source_uv",
+ "light_source_blue",
+ "light_source_lime",
+ "cuttlefish_fip"
+ ],
+ "title": "AindPhysioFipRig",
+ "type": "object"
+ },
+ "BaseModel": {
+ "properties": {},
+ "title": "BaseModel",
+ "type": "object"
+ },
+ "Circle": {
+ "properties": {
+ "center": {
+ "$ref": "#/$defs/Point2f",
+ "default": {
+ "x": 0.0,
+ "y": 0.0
+ },
+ "description": "Center of the circle (px)"
+ },
+ "radius": {
+ "default": 1,
+ "description": "Radius of the circle (px)",
+ "minimum": 0,
+ "title": "Radius",
+ "type": "number"
+ }
+ },
+ "title": "Circle",
+ "type": "object"
+ },
+ "FipCamera": {
+ "description": "Camera device configuration for FIP photometry system.",
+ "properties": {
+ "device_type": {
+ "const": "FipCamera",
+ "default": "FipCamera",
+ "title": "Device Type",
+ "type": "string"
+ },
+ "device_name": {
+ "default": null,
+ "description": "Device name",
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Device Name"
+ },
+ "additional_settings": {
+ "default": null,
+ "description": "Additional settings",
+ "oneOf": [
+ {
+ "$ref": "#/$defs/BaseModel"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "calibration": {
+ "default": null,
+ "description": "Calibration",
+ "oneOf": [
+ {
+ "$ref": "#/$defs/BaseModel"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "serial_number": {
+ "description": "Camera serial number",
+ "title": "Serial Number",
+ "type": "string"
+ },
+ "gain": {
+ "default": 0,
+ "description": "Gain",
+ "minimum": 0,
+ "title": "Gain",
+ "type": "number"
+ },
+ "offset": {
+ "$ref": "#/$defs/Point2f",
+ "default": {
+ "x": 0.0,
+ "y": 0.0
+ },
+ "description": "Offset (px)"
+ }
+ },
+ "required": [
+ "serial_number"
+ ],
+ "title": "FipCamera",
+ "type": "object"
+ },
+ "FipTask": {
+ "description": "Task configuration for FIP timing and triggering parameters.",
+ "properties": {
+ "delta_1": {
+ "default": 15650,
+ "description": "Delta 1 (us)",
+ "minimum": 0,
+ "title": "Delta 1",
+ "type": "integer"
+ },
+ "delta_2": {
+ "default": 666,
+ "description": "Delta 2 (us)",
+ "minimum": 0,
+ "title": "Delta 2",
+ "type": "integer"
+ },
+ "delta_3": {
+ "default": 300,
+ "description": "Delta 3 (us)",
+ "minimum": 0,
+ "title": "Delta 3",
+ "type": "integer"
+ },
+ "delta_4": {
+ "default": 50,
+ "description": "Delta 4 (us)",
+ "minimum": 0,
+ "title": "Delta 4",
+ "type": "integer"
+ },
+ "light_source_port": {
+ "$ref": "#/$defs/Ports",
+ "description": "Port that triggers the light source."
+ },
+ "camera_port": {
+ "$ref": "#/$defs/Ports",
+ "description": "Port that triggers the camera."
+ },
+ "events_enabled": {
+ "default": true,
+ "description": "Whether to enable events for the task. If False, the task will not trigger any events.",
+ "title": "Events Enabled",
+ "type": "boolean"
+ },
+ "mute_output": {
+ "default": false,
+ "description": "Whether to mute the output of the task. If True, the task will not trigger any outputs but timing will be preserved.",
+ "title": "Mute Output",
+ "type": "boolean"
+ },
+ "pwm_frequency": {
+ "default": 10000,
+ "description": "PWM frequency (Hz) of the light source output.",
+ "minimum": 10000,
+ "title": "Pwm Frequency",
+ "type": "number"
+ }
+ },
+ "required": [
+ "light_source_port",
+ "camera_port"
+ ],
+ "title": "FipTask",
+ "type": "object"
+ },
+ "HarpCuttlefishfip": {
+ "properties": {
+ "device_type": {
+ "const": "cuTTLefishFip",
+ "default": "cuTTLefishFip",
+ "title": "Device Type",
+ "type": "string"
+ },
+ "device_name": {
+ "default": null,
+ "description": "Device name",
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Device Name"
+ },
+ "additional_settings": {
+ "default": null,
+ "description": "Additional settings",
+ "oneOf": [
+ {
+ "$ref": "#/$defs/BaseModel"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "calibration": {
+ "default": null,
+ "description": "Calibration",
+ "oneOf": [
+ {
+ "$ref": "#/$defs/BaseModel"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "who_am_i": {
+ "const": 1407,
+ "default": 1407,
+ "title": "Who Am I",
+ "type": "integer"
+ },
+ "serial_number": {
+ "default": null,
+ "description": "Device serial number",
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Serial Number"
+ },
+ "port_name": {
+ "description": "Device port name",
+ "title": "Port Name",
+ "type": "string"
+ }
+ },
+ "required": [
+ "port_name"
+ ],
+ "title": "HarpCuttlefishfip",
+ "type": "object"
+ },
+ "LightSource": {
+ "description": "Light source device configuration with power control and timing tasks.",
+ "properties": {
+ "device_type": {
+ "const": "LightSource",
+ "default": "LightSource",
+ "title": "Device Type",
+ "type": "string"
+ },
+ "device_name": {
+ "default": null,
+ "description": "Device name",
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Device Name"
+ },
+ "additional_settings": {
+ "default": null,
+ "description": "Additional settings",
+ "oneOf": [
+ {
+ "$ref": "#/$defs/BaseModel"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "calibration": {
+ "default": null,
+ "description": "Calibration for the LightSource. If left empty, 'power' will be used as duty-cycle (0-100).",
+ "oneOf": [
+ {
+ "$ref": "#/$defs/LightSourceCalibration"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Calibration"
+ },
+ "power": {
+ "default": 0,
+ "description": "Power (mW)",
+ "minimum": 0,
+ "title": "Power",
+ "type": "number"
+ },
+ "task": {
+ "$ref": "#/$defs/FipTask",
+ "description": "Task for the light source",
+ "title": "Task"
+ }
+ },
+ "required": [
+ "task"
+ ],
+ "title": "LightSource",
+ "type": "object"
+ },
+ "LightSourceCalibration": {
+ "description": "Calibration model for converting light source duty cycle to power output.",
+ "properties": {
+ "device_name": {
+ "description": "Name of the device being calibrated",
+ "title": "Device name",
+ "type": "string"
+ },
+ "input": {
+ "default": null,
+ "oneOf": [
+ {
+ "$ref": "#/$defs/BaseModel"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Input data"
+ },
+ "output": {
+ "$ref": "#/$defs/LightSourceCalibrationOutput",
+ "title": "Lookup table to convert duty cycle to power (mW)"
+ },
+ "date": {
+ "default": null,
+ "oneOf": [
+ {
+ "format": "date-time",
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Date"
+ },
+ "description": {
+ "default": null,
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Brief description of what is being calibrated"
+ },
+ "notes": {
+ "default": null,
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Notes"
+ }
+ },
+ "required": [
+ "device_name",
+ "output"
+ ],
+ "title": "LightSourceCalibration",
+ "type": "object"
+ },
+ "LightSourceCalibrationOutput": {
+ "description": "Output of the light source calibration process.",
+ "properties": {
+ "power_lut": {
+ "additionalProperties": {
+ "default": 0,
+ "description": "Power (mW)",
+ "minimum": 0,
+ "type": "number"
+ },
+ "description": "Look-up table for LightSource power vs. duty cycle",
+ "title": "Power Lut",
+ "type": "object"
+ }
+ },
+ "required": [
+ "power_lut"
+ ],
+ "title": "LightSourceCalibrationOutput",
+ "type": "object"
+ },
+ "Networking": {
+ "description": "Network configuration settings for ZeroMQ communication.",
+ "properties": {
+ "zmq_publisher": {
+ "$ref": "#/$defs/ZmqConnection",
+ "default": {
+ "connection_string": "@tcp://localhost:5556",
+ "topic": "fip"
+ }
+ },
+ "zmq_subscriber": {
+ "$ref": "#/$defs/ZmqConnection",
+ "default": {
+ "connection_string": "@tcp://localhost:5557",
+ "topic": "fip"
+ }
+ }
+ },
+ "title": "Networking",
+ "type": "object"
+ },
+ "Point2f": {
+ "properties": {
+ "x": {
+ "description": "X coordinate of the point (px)",
+ "title": "X",
+ "type": "number"
+ },
+ "y": {
+ "description": "Y coordinate of the point (px)",
+ "title": "Y",
+ "type": "number"
+ }
+ },
+ "required": [
+ "x",
+ "y"
+ ],
+ "title": "Point2f",
+ "type": "object"
+ },
+ "Ports": {
+ "description": "Available hardware ports in the FIP cuttlefish board.",
+ "enum": [
+ 0,
+ 1,
+ 2,
+ 4,
+ 8,
+ 16,
+ 32,
+ 64,
+ 128
+ ],
+ "title": "Ports",
+ "type": "integer",
+ "x-enumNames": [
+ "None",
+ "Io0",
+ "Io1",
+ "Io2",
+ "Io3",
+ "Io4",
+ "Io5",
+ "Io6",
+ "Io7"
+ ]
+ },
+ "RoiSettings": {
+ "description": "Region of Interest (ROI) settings for camera channels in the FIP system.",
+ "properties": {
+ "camera_green_iso_background": {
+ "$ref": "#/$defs/Circle",
+ "default": {
+ "center": {
+ "x": 10.0,
+ "y": 10.0
+ },
+ "radius": 10.0
+ },
+ "description": "ROI to compute the background for the green/iso camera channel"
+ },
+ "camera_red_background": {
+ "$ref": "#/$defs/Circle",
+ "default": {
+ "center": {
+ "x": 10.0,
+ "y": 10.0
+ },
+ "radius": 10.0
+ },
+ "description": "ROI to compute the background for the red camera channel"
+ },
+ "camera_green_iso_roi": {
+ "default": [
+ {
+ "center": {
+ "x": 50.0,
+ "y": 50.0
+ },
+ "radius": 20.0
+ },
+ {
+ "center": {
+ "x": 50.0,
+ "y": 150.0
+ },
+ "radius": 20.0
+ },
+ {
+ "center": {
+ "x": 150.0,
+ "y": 50.0
+ },
+ "radius": 20.0
+ },
+ {
+ "center": {
+ "x": 150.0,
+ "y": 150.0
+ },
+ "radius": 20.0
+ }
+ ],
+ "description": "ROI for the green/iso camera channel",
+ "items": {
+ "$ref": "#/$defs/Circle"
+ },
+ "title": "Camera Green Iso Roi",
+ "type": "array"
+ },
+ "camera_red_roi": {
+ "default": [
+ {
+ "center": {
+ "x": 50.0,
+ "y": 50.0
+ },
+ "radius": 20.0
+ },
+ {
+ "center": {
+ "x": 50.0,
+ "y": 150.0
+ },
+ "radius": 20.0
+ },
+ {
+ "center": {
+ "x": 150.0,
+ "y": 50.0
+ },
+ "radius": 20.0
+ },
+ {
+ "center": {
+ "x": 150.0,
+ "y": 150.0
+ },
+ "radius": 20.0
+ }
+ ],
+ "description": "ROI for the red camera channel",
+ "items": {
+ "$ref": "#/$defs/Circle"
+ },
+ "title": "Camera Red Roi",
+ "type": "array"
+ }
+ },
+ "title": "RoiSettings",
+ "type": "object"
+ },
+ "ZmqConnection": {
+ "properties": {
+ "connection_string": {
+ "default": "@tcp://localhost:5556",
+ "description": "The connection string for the ZMQ socket.",
+ "title": "Connection String",
+ "type": "string"
+ },
+ "topic": {
+ "default": "",
+ "title": "Topic",
+ "type": "string"
+ }
+ },
+ "title": "ZmqConnection",
+ "type": "object"
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Platforms/FIP_DAQ_Control/src/Extensions.csproj b/src/Platforms/FIP_DAQ_Control/src/Extensions.csproj
new file mode 100644
index 00000000..ee04ad08
--- /dev/null
+++ b/src/Platforms/FIP_DAQ_Control/src/Extensions.csproj
@@ -0,0 +1,22 @@
+
+
+ net472
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Platforms/FIP_DAQ_Control/src/Extensions/AindPhysiologyFip.Generated.cs b/src/Platforms/FIP_DAQ_Control/src/Extensions/AindPhysiologyFip.Generated.cs
new file mode 100644
index 00000000..25a67fd9
--- /dev/null
+++ b/src/Platforms/FIP_DAQ_Control/src/Extensions/AindPhysiologyFip.Generated.cs
@@ -0,0 +1,2380 @@
+//----------------------
+//
+// Generated using the NJsonSchema v10.9.0.0 (Newtonsoft.Json v13.0.0.0) (http://NJsonSchema.org)
+//
+//----------------------
+
+
+namespace AindPhysiologyFip
+{
+ #pragma warning disable // Disable all warnings
+
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.6.1.0 (Newtonsoft.Json v13.0.0.0)")]
+ [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Source)]
+ [Bonsai.CombinatorAttribute(MethodName="Generate")]
+ public partial class AindBehaviorSessionModel
+ {
+
+ private string _aindBehaviorServicesPkgVersion;
+
+ private string _version;
+
+ private string _experiment;
+
+ private System.Collections.Generic.List _experimenter;
+
+ private System.DateTimeOffset _date;
+
+ private string _rootPath;
+
+ private string _sessionName;
+
+ private string _subject;
+
+ private string _experimentVersion;
+
+ private string _notes;
+
+ private string _commitHash;
+
+ private bool _allowDirtyRepo;
+
+ private bool _skipHardwareValidation;
+
+ public AindBehaviorSessionModel()
+ {
+ _aindBehaviorServicesPkgVersion = "0.12.3";
+ _version = "0.12.3";
+ _experimenter = new System.Collections.Generic.List();
+ _experimentVersion = "";
+ _allowDirtyRepo = false;
+ _skipHardwareValidation = false;
+ }
+
+ protected AindBehaviorSessionModel(AindBehaviorSessionModel other)
+ {
+ _aindBehaviorServicesPkgVersion = other._aindBehaviorServicesPkgVersion;
+ _version = other._version;
+ _experiment = other._experiment;
+ _experimenter = other._experimenter;
+ _date = other._date;
+ _rootPath = other._rootPath;
+ _sessionName = other._sessionName;
+ _subject = other._subject;
+ _experimentVersion = other._experimentVersion;
+ _notes = other._notes;
+ _commitHash = other._commitHash;
+ _allowDirtyRepo = other._allowDirtyRepo;
+ _skipHardwareValidation = other._skipHardwareValidation;
+ }
+
+ [Newtonsoft.Json.JsonPropertyAttribute("aind_behavior_services_pkg_version")]
+ public string AindBehaviorServicesPkgVersion
+ {
+ get
+ {
+ return _aindBehaviorServicesPkgVersion;
+ }
+ set
+ {
+ _aindBehaviorServicesPkgVersion = value;
+ }
+ }
+
+ [Newtonsoft.Json.JsonPropertyAttribute("version")]
+ public string Version
+ {
+ get
+ {
+ return _version;
+ }
+ set
+ {
+ _version = value;
+ }
+ }
+
+ ///
+ /// Name of the experiment
+ ///
+ [Newtonsoft.Json.JsonPropertyAttribute("experiment")]
+ [System.ComponentModel.DescriptionAttribute("Name of the experiment")]
+ public string Experiment
+ {
+ get
+ {
+ return _experiment;
+ }
+ set
+ {
+ _experiment = value;
+ }
+ }
+
+ ///
+ /// Name of the experimenter
+ ///
+ [System.Xml.Serialization.XmlIgnoreAttribute()]
+ [Newtonsoft.Json.JsonPropertyAttribute("experimenter")]
+ [System.ComponentModel.DescriptionAttribute("Name of the experimenter")]
+ public System.Collections.Generic.List Experimenter
+ {
+ get
+ {
+ return _experimenter;
+ }
+ set
+ {
+ _experimenter = value;
+ }
+ }
+
+ ///
+ /// Date of the experiment
+ ///
+ [System.Xml.Serialization.XmlIgnoreAttribute()]
+ [Newtonsoft.Json.JsonPropertyAttribute("date")]
+ [System.ComponentModel.DescriptionAttribute("Date of the experiment")]
+ public System.DateTimeOffset Date
+ {
+ get
+ {
+ return _date;
+ }
+ set
+ {
+ _date = value;
+ }
+ }
+
+ ///
+ /// Root path where data will be logged
+ ///
+ [Newtonsoft.Json.JsonPropertyAttribute("root_path", Required=Newtonsoft.Json.Required.Always)]
+ [System.ComponentModel.DescriptionAttribute("Root path where data will be logged")]
+ public string RootPath
+ {
+ get
+ {
+ return _rootPath;
+ }
+ set
+ {
+ _rootPath = value;
+ }
+ }
+
+ ///
+ /// Name of the session. This will be used to create a folder in the root path. If not provided, it will be generated using subject and date.
+ ///
+ [Newtonsoft.Json.JsonPropertyAttribute("session_name")]
+ [System.ComponentModel.DescriptionAttribute("Name of the session. This will be used to create a folder in the root path. If no" +
+ "t provided, it will be generated using subject and date.")]
+ public string SessionName
+ {
+ get
+ {
+ return _sessionName;
+ }
+ set
+ {
+ _sessionName = value;
+ }
+ }
+
+ ///
+ /// Name of the subject
+ ///
+ [Newtonsoft.Json.JsonPropertyAttribute("subject", Required=Newtonsoft.Json.Required.Always)]
+ [System.ComponentModel.DescriptionAttribute("Name of the subject")]
+ public string Subject
+ {
+ get
+ {
+ return _subject;
+ }
+ set
+ {
+ _subject = value;
+ }
+ }
+
+ ///
+ /// Version of the experiment
+ ///
+ [Newtonsoft.Json.JsonPropertyAttribute("experiment_version")]
+ [System.ComponentModel.DescriptionAttribute("Version of the experiment")]
+ public string ExperimentVersion
+ {
+ get
+ {
+ return _experimentVersion;
+ }
+ set
+ {
+ _experimentVersion = value;
+ }
+ }
+
+ ///
+ /// Notes about the experiment
+ ///
+ [Newtonsoft.Json.JsonPropertyAttribute("notes")]
+ [System.ComponentModel.DescriptionAttribute("Notes about the experiment")]
+ public string Notes
+ {
+ get
+ {
+ return _notes;
+ }
+ set
+ {
+ _notes = value;
+ }
+ }
+
+ ///
+ /// Commit hash of the repository
+ ///
+ [Newtonsoft.Json.JsonPropertyAttribute("commit_hash")]
+ [System.ComponentModel.DescriptionAttribute("Commit hash of the repository")]
+ public string CommitHash
+ {
+ get
+ {
+ return _commitHash;
+ }
+ set
+ {
+ _commitHash = value;
+ }
+ }
+
+ ///
+ /// Allow running from a dirty repository
+ ///
+ [Newtonsoft.Json.JsonPropertyAttribute("allow_dirty_repo")]
+ [System.ComponentModel.DescriptionAttribute("Allow running from a dirty repository")]
+ public bool AllowDirtyRepo
+ {
+ get
+ {
+ return _allowDirtyRepo;
+ }
+ set
+ {
+ _allowDirtyRepo = value;
+ }
+ }
+
+ ///
+ /// Skip hardware validation
+ ///
+ [Newtonsoft.Json.JsonPropertyAttribute("skip_hardware_validation")]
+ [System.ComponentModel.DescriptionAttribute("Skip hardware validation")]
+ public bool SkipHardwareValidation
+ {
+ get
+ {
+ return _skipHardwareValidation;
+ }
+ set
+ {
+ _skipHardwareValidation = value;
+ }
+ }
+
+ public System.IObservable Generate()
+ {
+ return System.Reactive.Linq.Observable.Defer(() => System.Reactive.Linq.Observable.Return(new AindBehaviorSessionModel(this)));
+ }
+
+ public System.IObservable Generate(System.IObservable source)
+ {
+ return System.Reactive.Linq.Observable.Select(source, _ => new AindBehaviorSessionModel(this));
+ }
+
+ protected virtual bool PrintMembers(System.Text.StringBuilder stringBuilder)
+ {
+ stringBuilder.Append("AindBehaviorServicesPkgVersion = " + _aindBehaviorServicesPkgVersion + ", ");
+ stringBuilder.Append("Version = " + _version + ", ");
+ stringBuilder.Append("Experiment = " + _experiment + ", ");
+ stringBuilder.Append("Experimenter = " + _experimenter + ", ");
+ stringBuilder.Append("Date = " + _date + ", ");
+ stringBuilder.Append("RootPath = " + _rootPath + ", ");
+ stringBuilder.Append("SessionName = " + _sessionName + ", ");
+ stringBuilder.Append("Subject = " + _subject + ", ");
+ stringBuilder.Append("ExperimentVersion = " + _experimentVersion + ", ");
+ stringBuilder.Append("Notes = " + _notes + ", ");
+ stringBuilder.Append("CommitHash = " + _commitHash + ", ");
+ stringBuilder.Append("AllowDirtyRepo = " + _allowDirtyRepo + ", ");
+ stringBuilder.Append("SkipHardwareValidation = " + _skipHardwareValidation);
+ return true;
+ }
+
+ public override string ToString()
+ {
+ System.Text.StringBuilder stringBuilder = new System.Text.StringBuilder();
+ stringBuilder.Append(GetType().Name);
+ stringBuilder.Append(" { ");
+ if (PrintMembers(stringBuilder))
+ {
+ stringBuilder.Append(" ");
+ }
+ stringBuilder.Append("}");
+ return stringBuilder.ToString();
+ }
+ }
+
+
+ ///
+ /// Complete rig configuration model for AIND FIP photometry system.
+ ///
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.6.1.0 (Newtonsoft.Json v13.0.0.0)")]
+ [System.ComponentModel.DescriptionAttribute("Complete rig configuration model for AIND FIP photometry system.")]
+ [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Source)]
+ [Bonsai.CombinatorAttribute(MethodName="Generate")]
+ public partial class AindPhysioFipRig
+ {
+
+ private string _aindBehaviorServicesPkgVersion;
+
+ private string _version;
+
+ private string _computerName;
+
+ private string _rigName;
+
+ private FipCamera _cameraGreenIso;
+
+ private FipCamera _cameraRed;
+
+ private LightSource _lightSourceUv;
+
+ private LightSource _lightSourceBlue;
+
+ private LightSource _lightSourceLime;
+
+ private RoiSettings _roiSettings;
+
+ private HarpCuttlefishfip _cuttlefishFip;
+
+ private Networking _networking;
+
+ public AindPhysioFipRig()
+ {
+ _aindBehaviorServicesPkgVersion = "0.12.3";
+ _version = "0.1.2-rc2";
+ _cameraGreenIso = new FipCamera();
+ _cameraRed = new FipCamera();
+ _lightSourceUv = new LightSource();
+ _lightSourceBlue = new LightSource();
+ _lightSourceLime = new LightSource();
+ _cuttlefishFip = new HarpCuttlefishfip();
+ _networking = new Networking();
+ }
+
+ protected AindPhysioFipRig(AindPhysioFipRig other)
+ {
+ _aindBehaviorServicesPkgVersion = other._aindBehaviorServicesPkgVersion;
+ _version = other._version;
+ _computerName = other._computerName;
+ _rigName = other._rigName;
+ _cameraGreenIso = other._cameraGreenIso;
+ _cameraRed = other._cameraRed;
+ _lightSourceUv = other._lightSourceUv;
+ _lightSourceBlue = other._lightSourceBlue;
+ _lightSourceLime = other._lightSourceLime;
+ _roiSettings = other._roiSettings;
+ _cuttlefishFip = other._cuttlefishFip;
+ _networking = other._networking;
+ }
+
+ [Newtonsoft.Json.JsonPropertyAttribute("aind_behavior_services_pkg_version")]
+ public string AindBehaviorServicesPkgVersion
+ {
+ get
+ {
+ return _aindBehaviorServicesPkgVersion;
+ }
+ set
+ {
+ _aindBehaviorServicesPkgVersion = value;
+ }
+ }
+
+ [Newtonsoft.Json.JsonPropertyAttribute("version")]
+ public string Version
+ {
+ get
+ {
+ return _version;
+ }
+ set
+ {
+ _version = value;
+ }
+ }
+
+ ///
+ /// Computer name
+ ///
+ [Newtonsoft.Json.JsonPropertyAttribute("computer_name")]
+ [System.ComponentModel.DescriptionAttribute("Computer name")]
+ public string ComputerName
+ {
+ get
+ {
+ return _computerName;
+ }
+ set
+ {
+ _computerName = value;
+ }
+ }
+
+ ///
+ /// Rig name
+ ///
+ [Newtonsoft.Json.JsonPropertyAttribute("rig_name", Required=Newtonsoft.Json.Required.Always)]
+ [System.ComponentModel.DescriptionAttribute("Rig name")]
+ public string RigName
+ {
+ get
+ {
+ return _rigName;
+ }
+ set
+ {
+ _rigName = value;
+ }
+ }
+
+ ///
+ /// Camera for the green and iso channels
+ ///
+ [System.Xml.Serialization.XmlIgnoreAttribute()]
+ [Newtonsoft.Json.JsonPropertyAttribute("camera_green_iso", Required=Newtonsoft.Json.Required.Always)]
+ [System.ComponentModel.DescriptionAttribute("Camera for the green and iso channels")]
+ public FipCamera CameraGreenIso
+ {
+ get
+ {
+ return _cameraGreenIso;
+ }
+ set
+ {
+ _cameraGreenIso = value;
+ }
+ }
+
+ ///
+ /// Red camera
+ ///
+ [System.Xml.Serialization.XmlIgnoreAttribute()]
+ [Newtonsoft.Json.JsonPropertyAttribute("camera_red", Required=Newtonsoft.Json.Required.Always)]
+ [System.ComponentModel.DescriptionAttribute("Red camera")]
+ public FipCamera CameraRed
+ {
+ get
+ {
+ return _cameraRed;
+ }
+ set
+ {
+ _cameraRed = value;
+ }
+ }
+
+ ///
+ /// UV (415nm) light source
+ ///
+ [System.Xml.Serialization.XmlIgnoreAttribute()]
+ [Newtonsoft.Json.JsonPropertyAttribute("light_source_uv", Required=Newtonsoft.Json.Required.Always)]
+ [System.ComponentModel.DescriptionAttribute("UV (415nm) light source")]
+ public LightSource LightSourceUv
+ {
+ get
+ {
+ return _lightSourceUv;
+ }
+ set
+ {
+ _lightSourceUv = value;
+ }
+ }
+
+ ///
+ /// Blue (470nm) light source
+ ///
+ [System.Xml.Serialization.XmlIgnoreAttribute()]
+ [Newtonsoft.Json.JsonPropertyAttribute("light_source_blue", Required=Newtonsoft.Json.Required.Always)]
+ [System.ComponentModel.DescriptionAttribute("Blue (470nm) light source")]
+ public LightSource LightSourceBlue
+ {
+ get
+ {
+ return _lightSourceBlue;
+ }
+ set
+ {
+ _lightSourceBlue = value;
+ }
+ }
+
+ ///
+ /// Lime (560nm) light source
+ ///
+ [System.Xml.Serialization.XmlIgnoreAttribute()]
+ [Newtonsoft.Json.JsonPropertyAttribute("light_source_lime", Required=Newtonsoft.Json.Required.Always)]
+ [System.ComponentModel.DescriptionAttribute("Lime (560nm) light source")]
+ public LightSource LightSourceLime
+ {
+ get
+ {
+ return _lightSourceLime;
+ }
+ set
+ {
+ _lightSourceLime = value;
+ }
+ }
+
+ ///
+ /// Region of interest settings. Leave empty to attempt to load from local file or manually define it in the program.
+ ///
+ [System.Xml.Serialization.XmlIgnoreAttribute()]
+ [Newtonsoft.Json.JsonPropertyAttribute("roi_settings")]
+ [System.ComponentModel.DescriptionAttribute("Region of interest settings. Leave empty to attempt to load from local file or ma" +
+ "nually define it in the program.")]
+ public RoiSettings RoiSettings
+ {
+ get
+ {
+ return _roiSettings;
+ }
+ set
+ {
+ _roiSettings = value;
+ }
+ }
+
+ ///
+ /// CuttlefishFip board for controlling the trigger of cameras and light-sources
+ ///
+ [System.Xml.Serialization.XmlIgnoreAttribute()]
+ [Newtonsoft.Json.JsonPropertyAttribute("cuttlefish_fip", Required=Newtonsoft.Json.Required.Always)]
+ [System.ComponentModel.DescriptionAttribute("CuttlefishFip board for controlling the trigger of cameras and light-sources")]
+ public HarpCuttlefishfip CuttlefishFip
+ {
+ get
+ {
+ return _cuttlefishFip;
+ }
+ set
+ {
+ _cuttlefishFip = value;
+ }
+ }
+
+ ///
+ /// Networking settings
+ ///
+ [System.Xml.Serialization.XmlIgnoreAttribute()]
+ [Newtonsoft.Json.JsonPropertyAttribute("networking")]
+ [System.ComponentModel.DescriptionAttribute("Networking settings")]
+ public Networking Networking
+ {
+ get
+ {
+ return _networking;
+ }
+ set
+ {
+ _networking = value;
+ }
+ }
+
+ public System.IObservable Generate()
+ {
+ return System.Reactive.Linq.Observable.Defer(() => System.Reactive.Linq.Observable.Return(new AindPhysioFipRig(this)));
+ }
+
+ public System.IObservable Generate(System.IObservable source)
+ {
+ return System.Reactive.Linq.Observable.Select(source, _ => new AindPhysioFipRig(this));
+ }
+
+ protected virtual bool PrintMembers(System.Text.StringBuilder stringBuilder)
+ {
+ stringBuilder.Append("AindBehaviorServicesPkgVersion = " + _aindBehaviorServicesPkgVersion + ", ");
+ stringBuilder.Append("Version = " + _version + ", ");
+ stringBuilder.Append("ComputerName = " + _computerName + ", ");
+ stringBuilder.Append("RigName = " + _rigName + ", ");
+ stringBuilder.Append("CameraGreenIso = " + _cameraGreenIso + ", ");
+ stringBuilder.Append("CameraRed = " + _cameraRed + ", ");
+ stringBuilder.Append("LightSourceUv = " + _lightSourceUv + ", ");
+ stringBuilder.Append("LightSourceBlue = " + _lightSourceBlue + ", ");
+ stringBuilder.Append("LightSourceLime = " + _lightSourceLime + ", ");
+ stringBuilder.Append("RoiSettings = " + _roiSettings + ", ");
+ stringBuilder.Append("CuttlefishFip = " + _cuttlefishFip + ", ");
+ stringBuilder.Append("Networking = " + _networking);
+ return true;
+ }
+
+ public override string ToString()
+ {
+ System.Text.StringBuilder stringBuilder = new System.Text.StringBuilder();
+ stringBuilder.Append(GetType().Name);
+ stringBuilder.Append(" { ");
+ if (PrintMembers(stringBuilder))
+ {
+ stringBuilder.Append(" ");
+ }
+ stringBuilder.Append("}");
+ return stringBuilder.ToString();
+ }
+ }
+
+
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.6.1.0 (Newtonsoft.Json v13.0.0.0)")]
+ [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Source)]
+ [Bonsai.CombinatorAttribute(MethodName="Generate")]
+ public partial class BaseModel
+ {
+
+ public BaseModel()
+ {
+ }
+
+ protected BaseModel(BaseModel other)
+ {
+ }
+
+ public System.IObservable Generate()
+ {
+ return System.Reactive.Linq.Observable.Defer(() => System.Reactive.Linq.Observable.Return(new BaseModel(this)));
+ }
+
+ public System.IObservable Generate(System.IObservable source)
+ {
+ return System.Reactive.Linq.Observable.Select(source, _ => new BaseModel(this));
+ }
+
+ protected virtual bool PrintMembers(System.Text.StringBuilder stringBuilder)
+ {
+ return false;
+ }
+
+ public override string ToString()
+ {
+ System.Text.StringBuilder stringBuilder = new System.Text.StringBuilder();
+ stringBuilder.Append(GetType().Name);
+ stringBuilder.Append(" { ");
+ if (PrintMembers(stringBuilder))
+ {
+ stringBuilder.Append(" ");
+ }
+ stringBuilder.Append("}");
+ return stringBuilder.ToString();
+ }
+ }
+
+
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.6.1.0 (Newtonsoft.Json v13.0.0.0)")]
+ [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Source)]
+ [Bonsai.CombinatorAttribute(MethodName="Generate")]
+ public partial class Circle
+ {
+
+ private Point2f _center;
+
+ private double _radius;
+
+ public Circle()
+ {
+ _center = new Point2f();
+ _radius = 1D;
+ }
+
+ protected Circle(Circle other)
+ {
+ _center = other._center;
+ _radius = other._radius;
+ }
+
+ ///
+ /// Center of the circle (px)
+ ///
+ [System.Xml.Serialization.XmlIgnoreAttribute()]
+ [Newtonsoft.Json.JsonPropertyAttribute("center")]
+ [System.ComponentModel.DescriptionAttribute("Center of the circle (px)")]
+ public Point2f Center
+ {
+ get
+ {
+ return _center;
+ }
+ set
+ {
+ _center = value;
+ }
+ }
+
+ ///
+ /// Radius of the circle (px)
+ ///
+ [Newtonsoft.Json.JsonPropertyAttribute("radius")]
+ [System.ComponentModel.DescriptionAttribute("Radius of the circle (px)")]
+ public double Radius
+ {
+ get
+ {
+ return _radius;
+ }
+ set
+ {
+ _radius = value;
+ }
+ }
+
+ public System.IObservable Generate()
+ {
+ return System.Reactive.Linq.Observable.Defer(() => System.Reactive.Linq.Observable.Return(new Circle(this)));
+ }
+
+ public System.IObservable Generate(System.IObservable source)
+ {
+ return System.Reactive.Linq.Observable.Select(source, _ => new Circle(this));
+ }
+
+ protected virtual bool PrintMembers(System.Text.StringBuilder stringBuilder)
+ {
+ stringBuilder.Append("Center = " + _center + ", ");
+ stringBuilder.Append("Radius = " + _radius);
+ return true;
+ }
+
+ public override string ToString()
+ {
+ System.Text.StringBuilder stringBuilder = new System.Text.StringBuilder();
+ stringBuilder.Append(GetType().Name);
+ stringBuilder.Append(" { ");
+ if (PrintMembers(stringBuilder))
+ {
+ stringBuilder.Append(" ");
+ }
+ stringBuilder.Append("}");
+ return stringBuilder.ToString();
+ }
+ }
+
+
+ ///
+ /// Camera device configuration for FIP photometry system.
+ ///
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.6.1.0 (Newtonsoft.Json v13.0.0.0)")]
+ [System.ComponentModel.DescriptionAttribute("Camera device configuration for FIP photometry system.")]
+ [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Source)]
+ [Bonsai.CombinatorAttribute(MethodName="Generate")]
+ public partial class FipCamera
+ {
+
+ private string _deviceType;
+
+ private string _deviceName;
+
+ private BaseModel _additionalSettings;
+
+ private BaseModel _calibration;
+
+ private string _serialNumber;
+
+ private double _gain;
+
+ private Point2f _offset;
+
+ public FipCamera()
+ {
+ _deviceType = "FipCamera";
+ _gain = 0D;
+ _offset = new Point2f();
+ }
+
+ protected FipCamera(FipCamera other)
+ {
+ _deviceType = other._deviceType;
+ _deviceName = other._deviceName;
+ _additionalSettings = other._additionalSettings;
+ _calibration = other._calibration;
+ _serialNumber = other._serialNumber;
+ _gain = other._gain;
+ _offset = other._offset;
+ }
+
+ [Newtonsoft.Json.JsonPropertyAttribute("device_type")]
+ public string DeviceType
+ {
+ get
+ {
+ return _deviceType;
+ }
+ set
+ {
+ _deviceType = value;
+ }
+ }
+
+ ///
+ /// Device name
+ ///
+ [Newtonsoft.Json.JsonPropertyAttribute("device_name")]
+ [System.ComponentModel.DescriptionAttribute("Device name")]
+ public string DeviceName
+ {
+ get
+ {
+ return _deviceName;
+ }
+ set
+ {
+ _deviceName = value;
+ }
+ }
+
+ ///
+ /// Additional settings
+ ///
+ [System.Xml.Serialization.XmlIgnoreAttribute()]
+ [Newtonsoft.Json.JsonPropertyAttribute("additional_settings")]
+ [System.ComponentModel.DescriptionAttribute("Additional settings")]
+ public BaseModel AdditionalSettings
+ {
+ get
+ {
+ return _additionalSettings;
+ }
+ set
+ {
+ _additionalSettings = value;
+ }
+ }
+
+ ///
+ /// Calibration
+ ///
+ [System.Xml.Serialization.XmlIgnoreAttribute()]
+ [Newtonsoft.Json.JsonPropertyAttribute("calibration")]
+ [System.ComponentModel.DescriptionAttribute("Calibration")]
+ public BaseModel Calibration
+ {
+ get
+ {
+ return _calibration;
+ }
+ set
+ {
+ _calibration = value;
+ }
+ }
+
+ ///
+ /// Camera serial number
+ ///
+ [Newtonsoft.Json.JsonPropertyAttribute("serial_number", Required=Newtonsoft.Json.Required.Always)]
+ [System.ComponentModel.DescriptionAttribute("Camera serial number")]
+ public string SerialNumber
+ {
+ get
+ {
+ return _serialNumber;
+ }
+ set
+ {
+ _serialNumber = value;
+ }
+ }
+
+ ///
+ /// Gain
+ ///
+ [Newtonsoft.Json.JsonPropertyAttribute("gain")]
+ [System.ComponentModel.DescriptionAttribute("Gain")]
+ public double Gain
+ {
+ get
+ {
+ return _gain;
+ }
+ set
+ {
+ _gain = value;
+ }
+ }
+
+ ///
+ /// Offset (px)
+ ///
+ [System.Xml.Serialization.XmlIgnoreAttribute()]
+ [Newtonsoft.Json.JsonPropertyAttribute("offset")]
+ [System.ComponentModel.DescriptionAttribute("Offset (px)")]
+ public Point2f Offset
+ {
+ get
+ {
+ return _offset;
+ }
+ set
+ {
+ _offset = value;
+ }
+ }
+
+ public System.IObservable Generate()
+ {
+ return System.Reactive.Linq.Observable.Defer(() => System.Reactive.Linq.Observable.Return(new FipCamera(this)));
+ }
+
+ public System.IObservable Generate(System.IObservable source)
+ {
+ return System.Reactive.Linq.Observable.Select(source, _ => new FipCamera(this));
+ }
+
+ protected virtual bool PrintMembers(System.Text.StringBuilder stringBuilder)
+ {
+ stringBuilder.Append("DeviceType = " + _deviceType + ", ");
+ stringBuilder.Append("DeviceName = " + _deviceName + ", ");
+ stringBuilder.Append("AdditionalSettings = " + _additionalSettings + ", ");
+ stringBuilder.Append("Calibration = " + _calibration + ", ");
+ stringBuilder.Append("SerialNumber = " + _serialNumber + ", ");
+ stringBuilder.Append("Gain = " + _gain + ", ");
+ stringBuilder.Append("Offset = " + _offset);
+ return true;
+ }
+
+ public override string ToString()
+ {
+ System.Text.StringBuilder stringBuilder = new System.Text.StringBuilder();
+ stringBuilder.Append(GetType().Name);
+ stringBuilder.Append(" { ");
+ if (PrintMembers(stringBuilder))
+ {
+ stringBuilder.Append(" ");
+ }
+ stringBuilder.Append("}");
+ return stringBuilder.ToString();
+ }
+ }
+
+
+ ///
+ /// Task configuration for FIP timing and triggering parameters.
+ ///
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.6.1.0 (Newtonsoft.Json v13.0.0.0)")]
+ [System.ComponentModel.DescriptionAttribute("Task configuration for FIP timing and triggering parameters.")]
+ [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Source)]
+ [Bonsai.CombinatorAttribute(MethodName="Generate")]
+ public partial class FipTask
+ {
+
+ private int _delta1;
+
+ private int _delta2;
+
+ private int _delta3;
+
+ private int _delta4;
+
+ private Ports _lightSourcePort;
+
+ private Ports _cameraPort;
+
+ private bool _eventsEnabled;
+
+ private bool _muteOutput;
+
+ private double _pwmFrequency;
+
+ public FipTask()
+ {
+ _delta1 = 15650;
+ _delta2 = 666;
+ _delta3 = 300;
+ _delta4 = 50;
+ _eventsEnabled = true;
+ _muteOutput = false;
+ _pwmFrequency = 10000D;
+ }
+
+ protected FipTask(FipTask other)
+ {
+ _delta1 = other._delta1;
+ _delta2 = other._delta2;
+ _delta3 = other._delta3;
+ _delta4 = other._delta4;
+ _lightSourcePort = other._lightSourcePort;
+ _cameraPort = other._cameraPort;
+ _eventsEnabled = other._eventsEnabled;
+ _muteOutput = other._muteOutput;
+ _pwmFrequency = other._pwmFrequency;
+ }
+
+ ///
+ /// Delta 1 (us)
+ ///
+ [Newtonsoft.Json.JsonPropertyAttribute("delta_1")]
+ [System.ComponentModel.DescriptionAttribute("Delta 1 (us)")]
+ public int Delta1
+ {
+ get
+ {
+ return _delta1;
+ }
+ set
+ {
+ _delta1 = value;
+ }
+ }
+
+ ///
+ /// Delta 2 (us)
+ ///
+ [Newtonsoft.Json.JsonPropertyAttribute("delta_2")]
+ [System.ComponentModel.DescriptionAttribute("Delta 2 (us)")]
+ public int Delta2
+ {
+ get
+ {
+ return _delta2;
+ }
+ set
+ {
+ _delta2 = value;
+ }
+ }
+
+ ///
+ /// Delta 3 (us)
+ ///
+ [Newtonsoft.Json.JsonPropertyAttribute("delta_3")]
+ [System.ComponentModel.DescriptionAttribute("Delta 3 (us)")]
+ public int Delta3
+ {
+ get
+ {
+ return _delta3;
+ }
+ set
+ {
+ _delta3 = value;
+ }
+ }
+
+ ///
+ /// Delta 4 (us)
+ ///
+ [Newtonsoft.Json.JsonPropertyAttribute("delta_4")]
+ [System.ComponentModel.DescriptionAttribute("Delta 4 (us)")]
+ public int Delta4
+ {
+ get
+ {
+ return _delta4;
+ }
+ set
+ {
+ _delta4 = value;
+ }
+ }
+
+ ///
+ /// Port that triggers the light source.
+ ///
+ [System.Xml.Serialization.XmlIgnoreAttribute()]
+ [Newtonsoft.Json.JsonPropertyAttribute("light_source_port", Required=Newtonsoft.Json.Required.Always)]
+ [System.ComponentModel.DescriptionAttribute("Port that triggers the light source.")]
+ public Ports LightSourcePort
+ {
+ get
+ {
+ return _lightSourcePort;
+ }
+ set
+ {
+ _lightSourcePort = value;
+ }
+ }
+
+ ///
+ /// Port that triggers the camera.
+ ///
+ [System.Xml.Serialization.XmlIgnoreAttribute()]
+ [Newtonsoft.Json.JsonPropertyAttribute("camera_port", Required=Newtonsoft.Json.Required.Always)]
+ [System.ComponentModel.DescriptionAttribute("Port that triggers the camera.")]
+ public Ports CameraPort
+ {
+ get
+ {
+ return _cameraPort;
+ }
+ set
+ {
+ _cameraPort = value;
+ }
+ }
+
+ ///
+ /// Whether to enable events for the task. If False, the task will not trigger any events.
+ ///
+ [Newtonsoft.Json.JsonPropertyAttribute("events_enabled")]
+ [System.ComponentModel.DescriptionAttribute("Whether to enable events for the task. If False, the task will not trigger any ev" +
+ "ents.")]
+ public bool EventsEnabled
+ {
+ get
+ {
+ return _eventsEnabled;
+ }
+ set
+ {
+ _eventsEnabled = value;
+ }
+ }
+
+ ///
+ /// Whether to mute the output of the task. If True, the task will not trigger any outputs but timing will be preserved.
+ ///
+ [Newtonsoft.Json.JsonPropertyAttribute("mute_output")]
+ [System.ComponentModel.DescriptionAttribute("Whether to mute the output of the task. If True, the task will not trigger any ou" +
+ "tputs but timing will be preserved.")]
+ public bool MuteOutput
+ {
+ get
+ {
+ return _muteOutput;
+ }
+ set
+ {
+ _muteOutput = value;
+ }
+ }
+
+ ///
+ /// PWM frequency (Hz) of the light source output.
+ ///
+ [Newtonsoft.Json.JsonPropertyAttribute("pwm_frequency")]
+ [System.ComponentModel.DescriptionAttribute("PWM frequency (Hz) of the light source output.")]
+ public double PwmFrequency
+ {
+ get
+ {
+ return _pwmFrequency;
+ }
+ set
+ {
+ _pwmFrequency = value;
+ }
+ }
+
+ public System.IObservable Generate()
+ {
+ return System.Reactive.Linq.Observable.Defer(() => System.Reactive.Linq.Observable.Return(new FipTask(this)));
+ }
+
+ public System.IObservable Generate(System.IObservable source)
+ {
+ return System.Reactive.Linq.Observable.Select(source, _ => new FipTask(this));
+ }
+
+ protected virtual bool PrintMembers(System.Text.StringBuilder stringBuilder)
+ {
+ stringBuilder.Append("Delta1 = " + _delta1 + ", ");
+ stringBuilder.Append("Delta2 = " + _delta2 + ", ");
+ stringBuilder.Append("Delta3 = " + _delta3 + ", ");
+ stringBuilder.Append("Delta4 = " + _delta4 + ", ");
+ stringBuilder.Append("LightSourcePort = " + _lightSourcePort + ", ");
+ stringBuilder.Append("CameraPort = " + _cameraPort + ", ");
+ stringBuilder.Append("EventsEnabled = " + _eventsEnabled + ", ");
+ stringBuilder.Append("MuteOutput = " + _muteOutput + ", ");
+ stringBuilder.Append("PwmFrequency = " + _pwmFrequency);
+ return true;
+ }
+
+ public override string ToString()
+ {
+ System.Text.StringBuilder stringBuilder = new System.Text.StringBuilder();
+ stringBuilder.Append(GetType().Name);
+ stringBuilder.Append(" { ");
+ if (PrintMembers(stringBuilder))
+ {
+ stringBuilder.Append(" ");
+ }
+ stringBuilder.Append("}");
+ return stringBuilder.ToString();
+ }
+ }
+
+
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.6.1.0 (Newtonsoft.Json v13.0.0.0)")]
+ [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Source)]
+ [Bonsai.CombinatorAttribute(MethodName="Generate")]
+ public partial class HarpCuttlefishfip
+ {
+
+ private string _deviceType;
+
+ private string _deviceName;
+
+ private BaseModel _additionalSettings;
+
+ private BaseModel _calibration;
+
+ private int _whoAmI;
+
+ private string _serialNumber;
+
+ private string _portName;
+
+ public HarpCuttlefishfip()
+ {
+ _deviceType = "cuTTLefishFip";
+ _whoAmI = 1407;
+ }
+
+ protected HarpCuttlefishfip(HarpCuttlefishfip other)
+ {
+ _deviceType = other._deviceType;
+ _deviceName = other._deviceName;
+ _additionalSettings = other._additionalSettings;
+ _calibration = other._calibration;
+ _whoAmI = other._whoAmI;
+ _serialNumber = other._serialNumber;
+ _portName = other._portName;
+ }
+
+ [Newtonsoft.Json.JsonPropertyAttribute("device_type")]
+ public string DeviceType
+ {
+ get
+ {
+ return _deviceType;
+ }
+ set
+ {
+ _deviceType = value;
+ }
+ }
+
+ ///
+ /// Device name
+ ///
+ [Newtonsoft.Json.JsonPropertyAttribute("device_name")]
+ [System.ComponentModel.DescriptionAttribute("Device name")]
+ public string DeviceName
+ {
+ get
+ {
+ return _deviceName;
+ }
+ set
+ {
+ _deviceName = value;
+ }
+ }
+
+ ///
+ /// Additional settings
+ ///
+ [System.Xml.Serialization.XmlIgnoreAttribute()]
+ [Newtonsoft.Json.JsonPropertyAttribute("additional_settings")]
+ [System.ComponentModel.DescriptionAttribute("Additional settings")]
+ public BaseModel AdditionalSettings
+ {
+ get
+ {
+ return _additionalSettings;
+ }
+ set
+ {
+ _additionalSettings = value;
+ }
+ }
+
+ ///
+ /// Calibration
+ ///
+ [System.Xml.Serialization.XmlIgnoreAttribute()]
+ [Newtonsoft.Json.JsonPropertyAttribute("calibration")]
+ [System.ComponentModel.DescriptionAttribute("Calibration")]
+ public BaseModel Calibration
+ {
+ get
+ {
+ return _calibration;
+ }
+ set
+ {
+ _calibration = value;
+ }
+ }
+
+ [Newtonsoft.Json.JsonPropertyAttribute("who_am_i")]
+ public int WhoAmI
+ {
+ get
+ {
+ return _whoAmI;
+ }
+ set
+ {
+ _whoAmI = value;
+ }
+ }
+
+ ///
+ /// Device serial number
+ ///
+ [Newtonsoft.Json.JsonPropertyAttribute("serial_number")]
+ [System.ComponentModel.DescriptionAttribute("Device serial number")]
+ public string SerialNumber
+ {
+ get
+ {
+ return _serialNumber;
+ }
+ set
+ {
+ _serialNumber = value;
+ }
+ }
+
+ ///
+ /// Device port name
+ ///
+ [Newtonsoft.Json.JsonPropertyAttribute("port_name", Required=Newtonsoft.Json.Required.Always)]
+ [System.ComponentModel.DescriptionAttribute("Device port name")]
+ public string PortName
+ {
+ get
+ {
+ return _portName;
+ }
+ set
+ {
+ _portName = value;
+ }
+ }
+
+ public System.IObservable Generate()
+ {
+ return System.Reactive.Linq.Observable.Defer(() => System.Reactive.Linq.Observable.Return(new HarpCuttlefishfip(this)));
+ }
+
+ public System.IObservable Generate(System.IObservable source)
+ {
+ return System.Reactive.Linq.Observable.Select(source, _ => new HarpCuttlefishfip(this));
+ }
+
+ protected virtual bool PrintMembers(System.Text.StringBuilder stringBuilder)
+ {
+ stringBuilder.Append("DeviceType = " + _deviceType + ", ");
+ stringBuilder.Append("DeviceName = " + _deviceName + ", ");
+ stringBuilder.Append("AdditionalSettings = " + _additionalSettings + ", ");
+ stringBuilder.Append("Calibration = " + _calibration + ", ");
+ stringBuilder.Append("WhoAmI = " + _whoAmI + ", ");
+ stringBuilder.Append("SerialNumber = " + _serialNumber + ", ");
+ stringBuilder.Append("PortName = " + _portName);
+ return true;
+ }
+
+ public override string ToString()
+ {
+ System.Text.StringBuilder stringBuilder = new System.Text.StringBuilder();
+ stringBuilder.Append(GetType().Name);
+ stringBuilder.Append(" { ");
+ if (PrintMembers(stringBuilder))
+ {
+ stringBuilder.Append(" ");
+ }
+ stringBuilder.Append("}");
+ return stringBuilder.ToString();
+ }
+ }
+
+
+ ///
+ /// Light source device configuration with power control and timing tasks.
+ ///
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.6.1.0 (Newtonsoft.Json v13.0.0.0)")]
+ [System.ComponentModel.DescriptionAttribute("Light source device configuration with power control and timing tasks.")]
+ [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Source)]
+ [Bonsai.CombinatorAttribute(MethodName="Generate")]
+ public partial class LightSource
+ {
+
+ private string _deviceType;
+
+ private string _deviceName;
+
+ private BaseModel _additionalSettings;
+
+ private LightSourceCalibration _calibration;
+
+ private double _power;
+
+ private FipTask _task;
+
+ public LightSource()
+ {
+ _deviceType = "LightSource";
+ _power = 0D;
+ _task = new FipTask();
+ }
+
+ protected LightSource(LightSource other)
+ {
+ _deviceType = other._deviceType;
+ _deviceName = other._deviceName;
+ _additionalSettings = other._additionalSettings;
+ _calibration = other._calibration;
+ _power = other._power;
+ _task = other._task;
+ }
+
+ [Newtonsoft.Json.JsonPropertyAttribute("device_type")]
+ public string DeviceType
+ {
+ get
+ {
+ return _deviceType;
+ }
+ set
+ {
+ _deviceType = value;
+ }
+ }
+
+ ///
+ /// Device name
+ ///
+ [Newtonsoft.Json.JsonPropertyAttribute("device_name")]
+ [System.ComponentModel.DescriptionAttribute("Device name")]
+ public string DeviceName
+ {
+ get
+ {
+ return _deviceName;
+ }
+ set
+ {
+ _deviceName = value;
+ }
+ }
+
+ ///
+ /// Additional settings
+ ///
+ [System.Xml.Serialization.XmlIgnoreAttribute()]
+ [Newtonsoft.Json.JsonPropertyAttribute("additional_settings")]
+ [System.ComponentModel.DescriptionAttribute("Additional settings")]
+ public BaseModel AdditionalSettings
+ {
+ get
+ {
+ return _additionalSettings;
+ }
+ set
+ {
+ _additionalSettings = value;
+ }
+ }
+
+ ///
+ /// Calibration for the LightSource. If left empty, 'power' will be used as duty-cycle (0-100).
+ ///
+ [System.Xml.Serialization.XmlIgnoreAttribute()]
+ [Newtonsoft.Json.JsonPropertyAttribute("calibration")]
+ [System.ComponentModel.DescriptionAttribute("Calibration for the LightSource. If left empty, \'power\' will be used as duty-cycl" +
+ "e (0-100).")]
+ public LightSourceCalibration Calibration
+ {
+ get
+ {
+ return _calibration;
+ }
+ set
+ {
+ _calibration = value;
+ }
+ }
+
+ ///
+ /// Power (mW)
+ ///
+ [Newtonsoft.Json.JsonPropertyAttribute("power")]
+ [System.ComponentModel.DescriptionAttribute("Power (mW)")]
+ public double Power
+ {
+ get
+ {
+ return _power;
+ }
+ set
+ {
+ _power = value;
+ }
+ }
+
+ ///
+ /// Task for the light source
+ ///
+ [System.Xml.Serialization.XmlIgnoreAttribute()]
+ [Newtonsoft.Json.JsonPropertyAttribute("task", Required=Newtonsoft.Json.Required.Always)]
+ [System.ComponentModel.DescriptionAttribute("Task for the light source")]
+ public FipTask Task
+ {
+ get
+ {
+ return _task;
+ }
+ set
+ {
+ _task = value;
+ }
+ }
+
+ public System.IObservable Generate()
+ {
+ return System.Reactive.Linq.Observable.Defer(() => System.Reactive.Linq.Observable.Return(new LightSource(this)));
+ }
+
+ public System.IObservable Generate(System.IObservable source)
+ {
+ return System.Reactive.Linq.Observable.Select(source, _ => new LightSource(this));
+ }
+
+ protected virtual bool PrintMembers(System.Text.StringBuilder stringBuilder)
+ {
+ stringBuilder.Append("DeviceType = " + _deviceType + ", ");
+ stringBuilder.Append("DeviceName = " + _deviceName + ", ");
+ stringBuilder.Append("AdditionalSettings = " + _additionalSettings + ", ");
+ stringBuilder.Append("Calibration = " + _calibration + ", ");
+ stringBuilder.Append("Power = " + _power + ", ");
+ stringBuilder.Append("Task = " + _task);
+ return true;
+ }
+
+ public override string ToString()
+ {
+ System.Text.StringBuilder stringBuilder = new System.Text.StringBuilder();
+ stringBuilder.Append(GetType().Name);
+ stringBuilder.Append(" { ");
+ if (PrintMembers(stringBuilder))
+ {
+ stringBuilder.Append(" ");
+ }
+ stringBuilder.Append("}");
+ return stringBuilder.ToString();
+ }
+ }
+
+
+ ///
+ /// Calibration model for converting light source duty cycle to power output.
+ ///
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.6.1.0 (Newtonsoft.Json v13.0.0.0)")]
+ [System.ComponentModel.DescriptionAttribute("Calibration model for converting light source duty cycle to power output.")]
+ [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Source)]
+ [Bonsai.CombinatorAttribute(MethodName="Generate")]
+ public partial class LightSourceCalibration
+ {
+
+ private string _deviceName;
+
+ private BaseModel _input;
+
+ private LightSourceCalibrationOutput _output;
+
+ private System.DateTimeOffset? _date;
+
+ private string _description;
+
+ private string _notes;
+
+ public LightSourceCalibration()
+ {
+ _output = new LightSourceCalibrationOutput();
+ }
+
+ protected LightSourceCalibration(LightSourceCalibration other)
+ {
+ _deviceName = other._deviceName;
+ _input = other._input;
+ _output = other._output;
+ _date = other._date;
+ _description = other._description;
+ _notes = other._notes;
+ }
+
+ ///
+ /// Name of the device being calibrated
+ ///
+ [Newtonsoft.Json.JsonPropertyAttribute("device_name", Required=Newtonsoft.Json.Required.Always)]
+ [System.ComponentModel.DescriptionAttribute("Name of the device being calibrated")]
+ public string DeviceName
+ {
+ get
+ {
+ return _deviceName;
+ }
+ set
+ {
+ _deviceName = value;
+ }
+ }
+
+ [System.Xml.Serialization.XmlIgnoreAttribute()]
+ [Newtonsoft.Json.JsonPropertyAttribute("input")]
+ public BaseModel Input
+ {
+ get
+ {
+ return _input;
+ }
+ set
+ {
+ _input = value;
+ }
+ }
+
+ [System.Xml.Serialization.XmlIgnoreAttribute()]
+ [Newtonsoft.Json.JsonPropertyAttribute("output", Required=Newtonsoft.Json.Required.Always)]
+ public LightSourceCalibrationOutput Output
+ {
+ get
+ {
+ return _output;
+ }
+ set
+ {
+ _output = value;
+ }
+ }
+
+ [System.Xml.Serialization.XmlIgnoreAttribute()]
+ [Newtonsoft.Json.JsonPropertyAttribute("date")]
+ public System.DateTimeOffset? Date
+ {
+ get
+ {
+ return _date;
+ }
+ set
+ {
+ _date = value;
+ }
+ }
+
+ [Newtonsoft.Json.JsonPropertyAttribute("description")]
+ public string Description
+ {
+ get
+ {
+ return _description;
+ }
+ set
+ {
+ _description = value;
+ }
+ }
+
+ [Newtonsoft.Json.JsonPropertyAttribute("notes")]
+ public string Notes
+ {
+ get
+ {
+ return _notes;
+ }
+ set
+ {
+ _notes = value;
+ }
+ }
+
+ public System.IObservable Generate()
+ {
+ return System.Reactive.Linq.Observable.Defer(() => System.Reactive.Linq.Observable.Return(new LightSourceCalibration(this)));
+ }
+
+ public System.IObservable Generate(System.IObservable source)
+ {
+ return System.Reactive.Linq.Observable.Select(source, _ => new LightSourceCalibration(this));
+ }
+
+ protected virtual bool PrintMembers(System.Text.StringBuilder stringBuilder)
+ {
+ stringBuilder.Append("DeviceName = " + _deviceName + ", ");
+ stringBuilder.Append("Input = " + _input + ", ");
+ stringBuilder.Append("Output = " + _output + ", ");
+ stringBuilder.Append("Date = " + _date + ", ");
+ stringBuilder.Append("Description = " + _description + ", ");
+ stringBuilder.Append("Notes = " + _notes);
+ return true;
+ }
+
+ public override string ToString()
+ {
+ System.Text.StringBuilder stringBuilder = new System.Text.StringBuilder();
+ stringBuilder.Append(GetType().Name);
+ stringBuilder.Append(" { ");
+ if (PrintMembers(stringBuilder))
+ {
+ stringBuilder.Append(" ");
+ }
+ stringBuilder.Append("}");
+ return stringBuilder.ToString();
+ }
+ }
+
+
+ ///
+ /// Output of the light source calibration process.
+ ///
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.6.1.0 (Newtonsoft.Json v13.0.0.0)")]
+ [System.ComponentModel.DescriptionAttribute("Output of the light source calibration process.")]
+ [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Source)]
+ [Bonsai.CombinatorAttribute(MethodName="Generate")]
+ public partial class LightSourceCalibrationOutput
+ {
+
+ private System.Collections.Generic.Dictionary _powerLut;
+
+ public LightSourceCalibrationOutput()
+ {
+ _powerLut = new System.Collections.Generic.Dictionary();
+ }
+
+ protected LightSourceCalibrationOutput(LightSourceCalibrationOutput other)
+ {
+ _powerLut = other._powerLut;
+ }
+
+ ///
+ /// Look-up table for LightSource power vs. duty cycle
+ ///
+ [System.Xml.Serialization.XmlIgnoreAttribute()]
+ [Newtonsoft.Json.JsonPropertyAttribute("power_lut", Required=Newtonsoft.Json.Required.Always)]
+ [System.ComponentModel.DescriptionAttribute("Look-up table for LightSource power vs. duty cycle")]
+ public System.Collections.Generic.Dictionary PowerLut
+ {
+ get
+ {
+ return _powerLut;
+ }
+ set
+ {
+ _powerLut = value;
+ }
+ }
+
+ public System.IObservable Generate()
+ {
+ return System.Reactive.Linq.Observable.Defer(() => System.Reactive.Linq.Observable.Return(new LightSourceCalibrationOutput(this)));
+ }
+
+ public System.IObservable Generate(System.IObservable source)
+ {
+ return System.Reactive.Linq.Observable.Select(source, _ => new LightSourceCalibrationOutput(this));
+ }
+
+ protected virtual bool PrintMembers(System.Text.StringBuilder stringBuilder)
+ {
+ stringBuilder.Append("PowerLut = " + _powerLut);
+ return true;
+ }
+
+ public override string ToString()
+ {
+ System.Text.StringBuilder stringBuilder = new System.Text.StringBuilder();
+ stringBuilder.Append(GetType().Name);
+ stringBuilder.Append(" { ");
+ if (PrintMembers(stringBuilder))
+ {
+ stringBuilder.Append(" ");
+ }
+ stringBuilder.Append("}");
+ return stringBuilder.ToString();
+ }
+ }
+
+
+ ///
+ /// Network configuration settings for ZeroMQ communication.
+ ///
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.6.1.0 (Newtonsoft.Json v13.0.0.0)")]
+ [System.ComponentModel.DescriptionAttribute("Network configuration settings for ZeroMQ communication.")]
+ [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Source)]
+ [Bonsai.CombinatorAttribute(MethodName="Generate")]
+ public partial class Networking
+ {
+
+ private ZmqConnection _zmqPublisher;
+
+ private ZmqConnection _zmqSubscriber;
+
+ public Networking()
+ {
+ _zmqPublisher = new ZmqConnection();
+ _zmqSubscriber = new ZmqConnection();
+ }
+
+ protected Networking(Networking other)
+ {
+ _zmqPublisher = other._zmqPublisher;
+ _zmqSubscriber = other._zmqSubscriber;
+ }
+
+ [System.Xml.Serialization.XmlIgnoreAttribute()]
+ [Newtonsoft.Json.JsonPropertyAttribute("zmq_publisher")]
+ public ZmqConnection ZmqPublisher
+ {
+ get
+ {
+ return _zmqPublisher;
+ }
+ set
+ {
+ _zmqPublisher = value;
+ }
+ }
+
+ [System.Xml.Serialization.XmlIgnoreAttribute()]
+ [Newtonsoft.Json.JsonPropertyAttribute("zmq_subscriber")]
+ public ZmqConnection ZmqSubscriber
+ {
+ get
+ {
+ return _zmqSubscriber;
+ }
+ set
+ {
+ _zmqSubscriber = value;
+ }
+ }
+
+ public System.IObservable Generate()
+ {
+ return System.Reactive.Linq.Observable.Defer(() => System.Reactive.Linq.Observable.Return(new Networking(this)));
+ }
+
+ public System.IObservable Generate(System.IObservable source)
+ {
+ return System.Reactive.Linq.Observable.Select(source, _ => new Networking(this));
+ }
+
+ protected virtual bool PrintMembers(System.Text.StringBuilder stringBuilder)
+ {
+ stringBuilder.Append("ZmqPublisher = " + _zmqPublisher + ", ");
+ stringBuilder.Append("ZmqSubscriber = " + _zmqSubscriber);
+ return true;
+ }
+
+ public override string ToString()
+ {
+ System.Text.StringBuilder stringBuilder = new System.Text.StringBuilder();
+ stringBuilder.Append(GetType().Name);
+ stringBuilder.Append(" { ");
+ if (PrintMembers(stringBuilder))
+ {
+ stringBuilder.Append(" ");
+ }
+ stringBuilder.Append("}");
+ return stringBuilder.ToString();
+ }
+ }
+
+
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.6.1.0 (Newtonsoft.Json v13.0.0.0)")]
+ [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Source)]
+ [Bonsai.CombinatorAttribute(MethodName="Generate")]
+ public partial class Point2f
+ {
+
+ private double _x;
+
+ private double _y;
+
+ public Point2f()
+ {
+ }
+
+ protected Point2f(Point2f other)
+ {
+ _x = other._x;
+ _y = other._y;
+ }
+
+ ///
+ /// X coordinate of the point (px)
+ ///
+ [Newtonsoft.Json.JsonPropertyAttribute("x", Required=Newtonsoft.Json.Required.Always)]
+ [System.ComponentModel.DescriptionAttribute("X coordinate of the point (px)")]
+ public double X
+ {
+ get
+ {
+ return _x;
+ }
+ set
+ {
+ _x = value;
+ }
+ }
+
+ ///
+ /// Y coordinate of the point (px)
+ ///
+ [Newtonsoft.Json.JsonPropertyAttribute("y", Required=Newtonsoft.Json.Required.Always)]
+ [System.ComponentModel.DescriptionAttribute("Y coordinate of the point (px)")]
+ public double Y
+ {
+ get
+ {
+ return _y;
+ }
+ set
+ {
+ _y = value;
+ }
+ }
+
+ public System.IObservable Generate()
+ {
+ return System.Reactive.Linq.Observable.Defer(() => System.Reactive.Linq.Observable.Return(new Point2f(this)));
+ }
+
+ public System.IObservable Generate(System.IObservable source)
+ {
+ return System.Reactive.Linq.Observable.Select(source, _ => new Point2f(this));
+ }
+
+ protected virtual bool PrintMembers(System.Text.StringBuilder stringBuilder)
+ {
+ stringBuilder.Append("X = " + _x + ", ");
+ stringBuilder.Append("Y = " + _y);
+ return true;
+ }
+
+ public override string ToString()
+ {
+ System.Text.StringBuilder stringBuilder = new System.Text.StringBuilder();
+ stringBuilder.Append(GetType().Name);
+ stringBuilder.Append(" { ");
+ if (PrintMembers(stringBuilder))
+ {
+ stringBuilder.Append(" ");
+ }
+ stringBuilder.Append("}");
+ return stringBuilder.ToString();
+ }
+ }
+
+
+ ///
+ /// Available hardware ports in the FIP cuttlefish board.
+ ///
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.6.1.0 (Newtonsoft.Json v13.0.0.0)")]
+ public enum Ports
+ {
+
+ [System.Runtime.Serialization.EnumMemberAttribute(Value="0")]
+ None = 0,
+
+ [System.Runtime.Serialization.EnumMemberAttribute(Value="1")]
+ Io0 = 1,
+
+ [System.Runtime.Serialization.EnumMemberAttribute(Value="2")]
+ Io1 = 2,
+
+ [System.Runtime.Serialization.EnumMemberAttribute(Value="4")]
+ Io2 = 4,
+
+ [System.Runtime.Serialization.EnumMemberAttribute(Value="8")]
+ Io3 = 8,
+
+ [System.Runtime.Serialization.EnumMemberAttribute(Value="16")]
+ Io4 = 16,
+
+ [System.Runtime.Serialization.EnumMemberAttribute(Value="32")]
+ Io5 = 32,
+
+ [System.Runtime.Serialization.EnumMemberAttribute(Value="64")]
+ Io6 = 64,
+
+ [System.Runtime.Serialization.EnumMemberAttribute(Value="128")]
+ Io7 = 128,
+ }
+
+
+ ///
+ /// Region of Interest (ROI) settings for camera channels in the FIP system.
+ ///
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.6.1.0 (Newtonsoft.Json v13.0.0.0)")]
+ [System.ComponentModel.DescriptionAttribute("Region of Interest (ROI) settings for camera channels in the FIP system.")]
+ [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Source)]
+ [Bonsai.CombinatorAttribute(MethodName="Generate")]
+ public partial class RoiSettings
+ {
+
+ private Circle _cameraGreenIsoBackground;
+
+ private Circle _cameraRedBackground;
+
+ private System.Collections.Generic.List _cameraGreenIsoRoi;
+
+ private System.Collections.Generic.List _cameraRedRoi;
+
+ public RoiSettings()
+ {
+ _cameraGreenIsoBackground = new Circle();
+ _cameraRedBackground = new Circle();
+ _cameraGreenIsoRoi = new System.Collections.Generic.List();
+ _cameraRedRoi = new System.Collections.Generic.List();
+ }
+
+ protected RoiSettings(RoiSettings other)
+ {
+ _cameraGreenIsoBackground = other._cameraGreenIsoBackground;
+ _cameraRedBackground = other._cameraRedBackground;
+ _cameraGreenIsoRoi = other._cameraGreenIsoRoi;
+ _cameraRedRoi = other._cameraRedRoi;
+ }
+
+ ///
+ /// ROI to compute the background for the green/iso camera channel
+ ///
+ [System.Xml.Serialization.XmlIgnoreAttribute()]
+ [Newtonsoft.Json.JsonPropertyAttribute("camera_green_iso_background")]
+ [System.ComponentModel.DescriptionAttribute("ROI to compute the background for the green/iso camera channel")]
+ public Circle CameraGreenIsoBackground
+ {
+ get
+ {
+ return _cameraGreenIsoBackground;
+ }
+ set
+ {
+ _cameraGreenIsoBackground = value;
+ }
+ }
+
+ ///
+ /// ROI to compute the background for the red camera channel
+ ///
+ [System.Xml.Serialization.XmlIgnoreAttribute()]
+ [Newtonsoft.Json.JsonPropertyAttribute("camera_red_background")]
+ [System.ComponentModel.DescriptionAttribute("ROI to compute the background for the red camera channel")]
+ public Circle CameraRedBackground
+ {
+ get
+ {
+ return _cameraRedBackground;
+ }
+ set
+ {
+ _cameraRedBackground = value;
+ }
+ }
+
+ ///
+ /// ROI for the green/iso camera channel
+ ///
+ [System.Xml.Serialization.XmlIgnoreAttribute()]
+ [Newtonsoft.Json.JsonPropertyAttribute("camera_green_iso_roi")]
+ [System.ComponentModel.DescriptionAttribute("ROI for the green/iso camera channel")]
+ public System.Collections.Generic.List CameraGreenIsoRoi
+ {
+ get
+ {
+ return _cameraGreenIsoRoi;
+ }
+ set
+ {
+ _cameraGreenIsoRoi = value;
+ }
+ }
+
+ ///
+ /// ROI for the red camera channel
+ ///
+ [System.Xml.Serialization.XmlIgnoreAttribute()]
+ [Newtonsoft.Json.JsonPropertyAttribute("camera_red_roi")]
+ [System.ComponentModel.DescriptionAttribute("ROI for the red camera channel")]
+ public System.Collections.Generic.List CameraRedRoi
+ {
+ get
+ {
+ return _cameraRedRoi;
+ }
+ set
+ {
+ _cameraRedRoi = value;
+ }
+ }
+
+ public System.IObservable Generate()
+ {
+ return System.Reactive.Linq.Observable.Defer(() => System.Reactive.Linq.Observable.Return(new RoiSettings(this)));
+ }
+
+ public System.IObservable Generate(System.IObservable source)
+ {
+ return System.Reactive.Linq.Observable.Select(source, _ => new RoiSettings(this));
+ }
+
+ protected virtual bool PrintMembers(System.Text.StringBuilder stringBuilder)
+ {
+ stringBuilder.Append("CameraGreenIsoBackground = " + _cameraGreenIsoBackground + ", ");
+ stringBuilder.Append("CameraRedBackground = " + _cameraRedBackground + ", ");
+ stringBuilder.Append("CameraGreenIsoRoi = " + _cameraGreenIsoRoi + ", ");
+ stringBuilder.Append("CameraRedRoi = " + _cameraRedRoi);
+ return true;
+ }
+
+ public override string ToString()
+ {
+ System.Text.StringBuilder stringBuilder = new System.Text.StringBuilder();
+ stringBuilder.Append(GetType().Name);
+ stringBuilder.Append(" { ");
+ if (PrintMembers(stringBuilder))
+ {
+ stringBuilder.Append(" ");
+ }
+ stringBuilder.Append("}");
+ return stringBuilder.ToString();
+ }
+ }
+
+
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.6.1.0 (Newtonsoft.Json v13.0.0.0)")]
+ [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Source)]
+ [Bonsai.CombinatorAttribute(MethodName="Generate")]
+ public partial class ZmqConnection
+ {
+
+ private string _connectionString;
+
+ private string _topic;
+
+ public ZmqConnection()
+ {
+ _connectionString = "@tcp://localhost:5556";
+ _topic = "";
+ }
+
+ protected ZmqConnection(ZmqConnection other)
+ {
+ _connectionString = other._connectionString;
+ _topic = other._topic;
+ }
+
+ ///
+ /// The connection string for the ZMQ socket.
+ ///
+ [Newtonsoft.Json.JsonPropertyAttribute("connection_string")]
+ [System.ComponentModel.DescriptionAttribute("The connection string for the ZMQ socket.")]
+ public string ConnectionString
+ {
+ get
+ {
+ return _connectionString;
+ }
+ set
+ {
+ _connectionString = value;
+ }
+ }
+
+ [Newtonsoft.Json.JsonPropertyAttribute("topic")]
+ public string Topic
+ {
+ get
+ {
+ return _topic;
+ }
+ set
+ {
+ _topic = value;
+ }
+ }
+
+ public System.IObservable Generate()
+ {
+ return System.Reactive.Linq.Observable.Defer(() => System.Reactive.Linq.Observable.Return(new ZmqConnection(this)));
+ }
+
+ public System.IObservable Generate(System.IObservable source)
+ {
+ return System.Reactive.Linq.Observable.Select(source, _ => new ZmqConnection(this));
+ }
+
+ protected virtual bool PrintMembers(System.Text.StringBuilder stringBuilder)
+ {
+ stringBuilder.Append("ConnectionString = " + _connectionString + ", ");
+ stringBuilder.Append("Topic = " + _topic);
+ return true;
+ }
+
+ public override string ToString()
+ {
+ System.Text.StringBuilder stringBuilder = new System.Text.StringBuilder();
+ stringBuilder.Append(GetType().Name);
+ stringBuilder.Append(" { ");
+ if (PrintMembers(stringBuilder))
+ {
+ stringBuilder.Append(" ");
+ }
+ stringBuilder.Append("}");
+ return stringBuilder.ToString();
+ }
+ }
+
+
+ ///
+ /// Serializes a sequence of data model objects into JSON strings.
+ ///
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.6.1.0 (Newtonsoft.Json v13.0.0.0)")]
+ [System.ComponentModel.DescriptionAttribute("Serializes a sequence of data model objects into JSON strings.")]
+ [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Transform)]
+ [Bonsai.CombinatorAttribute()]
+ public partial class SerializeToJson
+ {
+
+ public Newtonsoft.Json.Formatting Formatting { get; set; }
+
+ private System.IObservable Process(System.IObservable source)
+ {
+ var formatting = Formatting;
+ return System.Reactive.Linq.Observable.Select(source, value => Newtonsoft.Json.JsonConvert.SerializeObject(value, formatting));
+ }
+
+ public System.IObservable Process(System.IObservable source)
+ {
+ return Process(source);
+ }
+
+ public System.IObservable Process(System.IObservable source)
+ {
+ return Process(source);
+ }
+
+ public System.IObservable Process(System.IObservable source)
+ {
+ return Process(source);
+ }
+
+ public System.IObservable Process(System.IObservable source)
+ {
+ return Process(source);
+ }
+
+ public System.IObservable Process(System.IObservable source)
+ {
+ return Process(source);
+ }
+
+ public System.IObservable Process(System.IObservable source)
+ {
+ return Process(source);
+ }
+
+ public System.IObservable Process(System.IObservable source)
+ {
+ return Process(source);
+ }
+
+ public System.IObservable Process(System.IObservable source)
+ {
+ return Process(source);
+ }
+
+ public System.IObservable Process(System.IObservable source)
+ {
+ return Process(source);
+ }
+
+ public System.IObservable Process(System.IObservable source)
+ {
+ return Process(source);
+ }
+
+ public System.IObservable Process(System.IObservable source)
+ {
+ return Process(source);
+ }
+
+ public System.IObservable Process(System.IObservable source)
+ {
+ return Process(source);
+ }
+
+ public System.IObservable Process(System.IObservable source)
+ {
+ return Process(source);
+ }
+
+ public System.IObservable Process(System.IObservable source)
+ {
+ return Process(source);
+ }
+ }
+
+
+ ///
+ /// Deserializes a sequence of JSON strings into data model objects.
+ ///
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.6.1.0 (Newtonsoft.Json v13.0.0.0)")]
+ [System.ComponentModel.DescriptionAttribute("Deserializes a sequence of JSON strings into data model objects.")]
+ [System.ComponentModel.DefaultPropertyAttribute("Type")]
+ [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Transform)]
+ [System.Xml.Serialization.XmlIncludeAttribute(typeof(Bonsai.Expressions.TypeMapping))]
+ [System.Xml.Serialization.XmlIncludeAttribute(typeof(Bonsai.Expressions.TypeMapping))]
+ [System.Xml.Serialization.XmlIncludeAttribute(typeof(Bonsai.Expressions.TypeMapping))]
+ [System.Xml.Serialization.XmlIncludeAttribute(typeof(Bonsai.Expressions.TypeMapping))]
+ [System.Xml.Serialization.XmlIncludeAttribute(typeof(Bonsai.Expressions.TypeMapping))]
+ [System.Xml.Serialization.XmlIncludeAttribute(typeof(Bonsai.Expressions.TypeMapping))]
+ [System.Xml.Serialization.XmlIncludeAttribute(typeof(Bonsai.Expressions.TypeMapping))]
+ [System.Xml.Serialization.XmlIncludeAttribute(typeof(Bonsai.Expressions.TypeMapping))]
+ [System.Xml.Serialization.XmlIncludeAttribute(typeof(Bonsai.Expressions.TypeMapping))]
+ [System.Xml.Serialization.XmlIncludeAttribute(typeof(Bonsai.Expressions.TypeMapping))]
+ [System.Xml.Serialization.XmlIncludeAttribute(typeof(Bonsai.Expressions.TypeMapping))]
+ [System.Xml.Serialization.XmlIncludeAttribute(typeof(Bonsai.Expressions.TypeMapping))]
+ [System.Xml.Serialization.XmlIncludeAttribute(typeof(Bonsai.Expressions.TypeMapping))]
+ [System.Xml.Serialization.XmlIncludeAttribute(typeof(Bonsai.Expressions.TypeMapping))]
+ public partial class DeserializeFromJson : Bonsai.Expressions.SingleArgumentExpressionBuilder
+ {
+
+ public DeserializeFromJson()
+ {
+ Type = new Bonsai.Expressions.TypeMapping();
+ }
+
+ public Bonsai.Expressions.TypeMapping Type { get; set; }
+
+ public override System.Linq.Expressions.Expression Build(System.Collections.Generic.IEnumerable arguments)
+ {
+ var typeMapping = (Bonsai.Expressions.TypeMapping)Type;
+ var returnType = typeMapping.GetType().GetGenericArguments()[0];
+ return System.Linq.Expressions.Expression.Call(
+ typeof(DeserializeFromJson),
+ "Process",
+ new System.Type[] { returnType },
+ System.Linq.Enumerable.Single(arguments));
+ }
+
+ private static System.IObservable Process(System.IObservable source)
+ {
+ return System.Reactive.Linq.Observable.Select(source, value => Newtonsoft.Json.JsonConvert.DeserializeObject(value));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Platforms/FIP_DAQ_Control/src/Extensions/AindPhysiologyFip.cs b/src/Platforms/FIP_DAQ_Control/src/Extensions/AindPhysiologyFip.cs
new file mode 100644
index 00000000..ee22d6b8
--- /dev/null
+++ b/src/Platforms/FIP_DAQ_Control/src/Extensions/AindPhysiologyFip.cs
@@ -0,0 +1,11 @@
+using System;
+
+namespace AindPhysiologyFip{
+ partial class RoiSettings : ICloneable{
+
+ public object Clone(){
+ var serialized = Newtonsoft.Json.JsonConvert.SerializeObject(this);
+ return Newtonsoft.Json.JsonConvert.DeserializeObject(serialized);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Platforms/FIP_DAQ_Control/src/Extensions/CircleActivity.cs b/src/Platforms/FIP_DAQ_Control/src/Extensions/CircleActivity.cs
new file mode 100644
index 00000000..f2ec41e4
--- /dev/null
+++ b/src/Platforms/FIP_DAQ_Control/src/Extensions/CircleActivity.cs
@@ -0,0 +1,49 @@
+using OpenCV.Net;
+using Bonsai.Vision;
+using System;
+
+namespace FipExtensions
+{
+ public class CircleActivity
+ {
+ public Circle Circle { get; set; }
+ public Scalar Activity { get; set; }
+
+ public CircleActivity() { }
+
+ public override string ToString()
+ {
+ return string.Format("Circle: {0}, Activity: {1}", Circle, Activity);
+ }
+
+
+ public ConnectedComponent AsConnectedComponent()
+ {
+ return ConnectedComponent.FromContour(SeqFromArray(AsPolygon()));
+ }
+
+ public Point[] AsPolygon(int nSegments = 30)
+ {
+ var polygon = new Point[nSegments];
+ for (int i = 0; i < nSegments; i++)
+ {
+ var angle = 2 * Math.PI * i / nSegments;
+ var x = Circle.Center.X + Circle.Radius * Math.Cos(angle);
+ var y = Circle.Center.Y + Circle.Radius * Math.Sin(angle);
+ polygon[i] = new Point((int)x, (int)y);
+ }
+ return polygon;
+ }
+
+ private static Seq SeqFromArray(Point[] input)
+ {
+ var storage = new MemStorage();
+ var output = new Seq(Depth.S32, 2, SequenceKind.Curve, storage);
+ if (input.Length > 0)
+ {
+ output.Push(input);
+ }
+ return output;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Platforms/FIP_DAQ_Control/src/Extensions/CircleActivityCalculator.cs b/src/Platforms/FIP_DAQ_Control/src/Extensions/CircleActivityCalculator.cs
new file mode 100644
index 00000000..3cbce836
--- /dev/null
+++ b/src/Platforms/FIP_DAQ_Control/src/Extensions/CircleActivityCalculator.cs
@@ -0,0 +1,164 @@
+using OpenCV.Net;
+using Bonsai.Vision;
+using System;
+using System.Linq;
+using System.Reactive.Linq;
+using System.ComponentModel;
+using Bonsai;
+using Bonsai.Harp;
+
+namespace FipExtensions
+{
+ [DefaultProperty("Circles")]
+ [Combinator]
+ [WorkflowElementCategory(ElementCategory.Transform)]
+ [Description("Calculates activation intensity inside specified regions of interest for each image in the sequence.")]
+ public class CircleActivityCalculator
+ {
+
+ [Description("The regions of interest for which to calculate activation intensity.")]
+ [Editor("Bonsai.Vision.Design.IplImageCircleEditor, Bonsai.Vision.Design", DesignTypes.UITypeEditor)]
+ public Circle[] Circles { get; set; }
+
+ private ReduceOperation operation = ReduceOperation.Avg;
+ [Description("Specifies the reduction operation used to calculate activation intensity.")]
+ public ReduceOperation Operation
+ {
+ get { return operation; }
+ set { operation = value; }
+ }
+
+ public IObservable> Process(IObservable> source){
+ return Process(source.Select(frame => frame.Value))
+ .Zip(source, (activity, frame) => Timestamped.Create(activity, frame.Seconds));
+ }
+
+ public IObservable Process(IObservable source)
+ {
+ return Observable.Defer(() =>
+ {
+ var roi = default(IplImage);
+ var mask = default(IplImage);
+ var currentCircles = default(Circle[]);
+ var boundingRegions = default(Rect[]);
+ return source.Select(frame =>
+ {
+ var operation = Operation;
+ var output = new CircleActivityCollection(frame);
+ var img = frame.Image;
+ mask = IplImageHelper.EnsureImageFormat(mask, img.Size, IplDepth.U8, 1);
+ if (operation != ReduceOperation.Sum) roi = null;
+ else roi = IplImageHelper.EnsureImageFormat(roi, img.Size, img.Depth, img.Channels);
+ if (Circles != currentCircles)
+ {
+ currentCircles = Circles;
+ if (currentCircles != null)
+ {
+ mask.SetZero();
+ foreach (var circle in currentCircles)
+ {
+ CV.Circle(mask, new Point((int)circle.Center.X, (int)circle.Center.Y), (int)circle.Radius, Scalar.All(255), -1);
+ }
+
+ boundingRegions = currentCircles.Select(circle =>
+ {
+ var left = (int)(circle.Center.X - circle.Radius);
+ var top = (int)(circle.Center.Y - circle.Radius);
+ var width = Math.Min((int)circle.Radius * 2, img.Width - left);
+ var height = Math.Min((int)circle.Radius * 2, img.Height - top);
+ left = Math.Max(left, 0);
+ top = Math.Max(top, 0);
+ return new Rect(left, top, width, height);
+ }).ToArray();
+ }
+ }
+
+ if (currentCircles != null)
+ {
+ var activeMask = mask;
+ if (roi != null)
+ {
+ roi.SetZero();
+ CV.Copy(img, roi, mask);
+ activeMask = roi;
+ }
+
+ var activation = ActivationFunction(operation);
+ for (int i = 0; i < boundingRegions.Length; i++)
+ {
+ var rect = boundingRegions[i];
+ var circle = currentCircles[i];
+ using (var region = img.GetSubRect(rect))
+ using (var regionMask = activeMask.GetSubRect(rect))
+ {
+ output.Add(new CircleActivity
+ {
+ Circle = circle,
+ Activity = activation(region, regionMask)
+ });
+ }
+ }
+ }
+
+ return output;
+ });
+ });
+ }
+
+ static Func ActivationFunction(ReduceOperation operation)
+ {
+ switch (operation)
+ {
+ case ReduceOperation.Avg: return CV.Avg;
+ case ReduceOperation.Max:
+ return (image, mask) =>
+ {
+ Scalar min, max;
+ MinMaxLoc(image, mask, out min, out max);
+ return max;
+ };
+ case ReduceOperation.Min:
+ return (image, mask) =>
+ {
+ Scalar min, max;
+ MinMaxLoc(image, mask, out min, out max);
+ return min;
+ };
+ case ReduceOperation.Sum: return (image, mask) => CV.Sum(mask);
+ default: throw new InvalidOperationException("The specified reduction operation is invalid.");
+ }
+ }
+
+ static void MinMaxLoc(IplImage image, IplImage mask, out Scalar min, out Scalar max)
+ {
+ Point minLoc, maxLoc;
+ if (image.Channels == 1)
+ {
+ CV.MinMaxLoc(image, out min.Val0, out max.Val0, out minLoc, out maxLoc, mask);
+ min.Val1 = max.Val1 = min.Val2 = max.Val2 = min.Val3 = max.Val3 = 0;
+ }
+ else
+ {
+ using (var coi = image.GetSubRect(new Rect(0, 0, image.Width, image.Height)))
+ {
+ coi.ChannelOfInterest = 1;
+ CV.MinMaxLoc(coi, out min.Val0, out max.Val0, out minLoc, out maxLoc, mask);
+ coi.ChannelOfInterest = 2;
+ CV.MinMaxLoc(coi, out min.Val1, out max.Val1, out minLoc, out maxLoc, mask);
+ if (image.Channels > 2)
+ {
+ coi.ChannelOfInterest = 3;
+ CV.MinMaxLoc(coi, out min.Val2, out max.Val2, out minLoc, out maxLoc, mask);
+ if (image.Channels > 3)
+ {
+ coi.ChannelOfInterest = 4;
+ CV.MinMaxLoc(coi, out min.Val3, out max.Val3, out minLoc, out maxLoc, mask);
+ }
+ else min.Val3 = max.Val3 = 0;
+ }
+ else min.Val2 = max.Val2 = min.Val3 = max.Val3 = 0;
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Platforms/FIP_DAQ_Control/src/Extensions/CircleActivityCollection.cs b/src/Platforms/FIP_DAQ_Control/src/Extensions/CircleActivityCollection.cs
new file mode 100644
index 00000000..2d723e8e
--- /dev/null
+++ b/src/Platforms/FIP_DAQ_Control/src/Extensions/CircleActivityCollection.cs
@@ -0,0 +1,21 @@
+using System.Collections.ObjectModel;
+using System.Linq;
+using Bonsai.Vision;
+
+
+namespace FipExtensions
+{
+ public class CircleActivityCollection : Collection
+ {
+ public FipFrame FipFrame { get; set; }
+ public CircleActivityCollection(FipFrame fipFrame) : base()
+ {
+ FipFrame = fipFrame;
+ }
+
+ public Circle[] Circles
+ {
+ get { return this.Select(circleActivity => circleActivity.Circle).ToArray(); }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Platforms/FIP_DAQ_Control/src/Extensions/CircleArrayDistinctUntilChanged.cs b/src/Platforms/FIP_DAQ_Control/src/Extensions/CircleArrayDistinctUntilChanged.cs
new file mode 100644
index 00000000..66293408
--- /dev/null
+++ b/src/Platforms/FIP_DAQ_Control/src/Extensions/CircleArrayDistinctUntilChanged.cs
@@ -0,0 +1,34 @@
+using Bonsai;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Reactive.Linq;
+using Bonsai.Vision;
+
+namespace FipExtensions{
+ [Combinator]
+ [Description("Emits the last value of the source sequence when it changes.")]
+ [WorkflowElementCategory(ElementCategory.Combinator)]
+
+ public class CircleArrayDistinctUntilChanged{
+
+ public IObservable Process(IObservable source){
+ return source.DistinctUntilChanged(new CircleArrayComparer());
+ }
+ }
+
+ internal class CircleArrayComparer: IEqualityComparer
+ {
+ public bool Equals(Circle[] x, Circle[] y)
+ {
+ if (x.Length != y.Length) return false;
+ return x.Zip(y, (a, b) => a.Equals(b)).All(equal => equal);
+ }
+
+ public int GetHashCode(Circle[] obj)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Platforms/FIP_DAQ_Control/src/Extensions/CircleRoiConverter.cs b/src/Platforms/FIP_DAQ_Control/src/Extensions/CircleRoiConverter.cs
new file mode 100644
index 00000000..b61612b4
--- /dev/null
+++ b/src/Platforms/FIP_DAQ_Control/src/Extensions/CircleRoiConverter.cs
@@ -0,0 +1,111 @@
+using Bonsai;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Reactive.Linq;
+
+namespace FipExtensions
+{
+ [Combinator]
+ [Description("Converts the RoiSettings structure to a pair of Circle[] to be used as ROIs and vice-versa. The first element is always the background")]
+ [WorkflowElementCategory(ElementCategory.Transform)]
+ public class RoiCircleConverter
+ {
+ public IObservable Process(IObservable source)
+ {
+ return source.Select(value =>
+ {
+ return new CameraSourceRoiSettings(
+ ToCircleArray(value, FipCameraSource.Green),
+ ToCircleArray(value, FipCameraSource.Red)
+ );
+ });
+ }
+
+ public IObservable Process(IObservable source)
+ {
+ return source.Select(value =>
+ {
+ var settings = new AindPhysiologyFip.RoiSettings();
+ settings.CameraGreenIsoBackground = value.GreenIso.Length == 0 ? null : ConvertToFipCircle(value.GreenIso[0]);
+ settings.CameraGreenIsoRoi = value.GreenIso.Skip(1).Select(ConvertToFipCircle).ToList();
+ settings.CameraRedBackground = value.Red.Length == 0 ? null : ConvertToFipCircle(value.Red[0]);
+ settings.CameraRedRoi = value.Red.Skip(1).Select(ConvertToFipCircle).ToList();
+ return settings;
+ });
+ }
+
+ public IObservable Process(IObservable> source)
+ {
+ return Process(source.Select(value => new CameraSourceRoiSettings(value.Item1, value.Item2)));
+ }
+
+ private static Bonsai.Vision.Circle[] ToCircleArray(AindPhysiologyFip.RoiSettings settings, FipCameraSource cameraSource)
+ {
+ switch (cameraSource)
+ {
+ case FipCameraSource.Iso:
+ case FipCameraSource.Green:
+ return new List() { settings.CameraGreenIsoBackground }
+ .Concat(settings.CameraGreenIsoRoi)
+ .Select(circle => ConvertToBonsaiVisionCircle(circle))
+ .ToArray();
+ case FipCameraSource.Red:
+ return new List() { settings.CameraRedBackground }
+ .Concat(settings.CameraRedRoi)
+ .Select(circle => ConvertToBonsaiVisionCircle(circle))
+ .ToArray();
+ default:
+ throw new InvalidOperationException("Invalid CameraSource specified.");
+ }
+ }
+
+ private static Bonsai.Vision.Circle ConvertToBonsaiVisionCircle(AindPhysiologyFip.Circle circle)
+ {
+ return new Bonsai.Vision.Circle
+ {
+ Center = new OpenCV.Net.Point2f((float)circle.Center.X, (float)circle.Center.Y),
+ Radius = (float)circle.Radius
+ };
+ }
+
+ private static AindPhysiologyFip.Circle ConvertToFipCircle(Bonsai.Vision.Circle circle)
+ {
+ return new AindPhysiologyFip.Circle
+ {
+ Center = new AindPhysiologyFip.Point2f { X = circle.Center.X, Y = circle.Center.Y },
+ Radius = circle.Radius
+ };
+ }
+
+ public class CameraSourceRoiSettings
+ {
+ public CameraSourceRoiSettings(Bonsai.Vision.Circle[] greenIso, Bonsai.Vision.Circle[] red)
+ {
+ GreenIso = greenIso;
+ Red = red;
+ }
+
+ public Bonsai.Vision.Circle[] GreenIso { get; set; }
+ public Bonsai.Vision.Circle[] Red { get; set; }
+
+ public Bonsai.Vision.Circle[] this[FipCameraSource cameraSource]
+ {
+ get
+ {
+ switch (cameraSource)
+ {
+ case FipCameraSource.Iso:
+ case FipCameraSource.Green:
+ return GreenIso;
+ case FipCameraSource.Red:
+ return Red;
+ default:
+ throw new InvalidOperationException("Invalid CameraSource specified.");
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Platforms/FIP_DAQ_Control/src/Extensions/CreateTaskFromLightSource.cs b/src/Platforms/FIP_DAQ_Control/src/Extensions/CreateTaskFromLightSource.cs
new file mode 100644
index 00000000..181adb6b
--- /dev/null
+++ b/src/Platforms/FIP_DAQ_Control/src/Extensions/CreateTaskFromLightSource.cs
@@ -0,0 +1,70 @@
+using Bonsai;
+using System;
+using System.ComponentModel;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reactive.Linq;
+using AindPhysiologyFip;
+using MathNet.Numerics.Interpolation;
+
+[Combinator]
+[Description("Takes a light source and returns a calibrated light source.")]
+[WorkflowElementCategory(ElementCategory.Transform)]
+public class CreateTaskFromLightSource
+{
+ public IObservable Process(IObservable source)
+ {
+ return source.Select(value => {
+ // We take care of the calibration
+ if (value== null) throw new ArgumentNullException("Input is null.");
+ var calibrationData = value.Calibration == null ? null : value.Calibration.Output.PowerLut;
+
+ Tuple interpolator = null;
+ // dutyCycle: power
+ if (calibrationData != null){
+ Dictionary lut = calibrationData.ToDictionary(
+ entry => double.Parse(entry.Key),
+ entry => entry.Value
+ );
+ interpolator = MakeInterpolators(lut);
+ }
+ else{
+ var unityLut = new Dictionary { { 0, 0 }, { 1, 1 } };
+ interpolator = MakeInterpolators(unityLut);
+ }
+
+ // Interpolate the power:
+ var power = value.Power;
+ var calibratedDutyCycle = interpolator.Item2.Interpolate(power);
+ return new CalibratedLightSource(value, interpolator.Item2, interpolator.Item1, calibratedDutyCycle);
+ }
+ );
+ }
+
+
+ private static Tuple MakeInterpolators(Dictionary lut)
+ {
+ var sortedKeys = lut.Keys.OrderBy(k => k).ToArray();
+ var sortedValues = sortedKeys.Select(k => lut[k]).ToArray();
+
+ return Tuple.Create(MathNet.Numerics.Interpolate.Linear(sortedKeys, sortedValues), MathNet.Numerics.Interpolate.Linear(sortedValues, sortedKeys));
+ }
+}
+
+public class CalibratedLightSource{
+
+ public CalibratedLightSource(LightSource lightSource, IInterpolation powerInterpolator, IInterpolation dutyCycleInterpolator, double calibratedDutyCycle)
+ {
+ LightSource = lightSource;
+ DutyCycleToPower = powerInterpolator;
+ PowerToDutyCycle = dutyCycleInterpolator;
+ CalibratedDutyCycle = calibratedDutyCycle;
+ }
+ public readonly LightSource LightSource;
+ public readonly IInterpolation DutyCycleToPower;
+ public readonly IInterpolation PowerToDutyCycle;
+ public readonly double CalibratedDutyCycle;
+
+ public double CalibratedPower {get { return DutyCycleToPower.Interpolate(CalibratedDutyCycle); } }
+
+}
diff --git a/src/Platforms/FIP_DAQ_Control/src/Extensions/FipFrame.cs b/src/Platforms/FIP_DAQ_Control/src/Extensions/FipFrame.cs
new file mode 100644
index 00000000..dd7c2bc9
--- /dev/null
+++ b/src/Platforms/FIP_DAQ_Control/src/Extensions/FipFrame.cs
@@ -0,0 +1,35 @@
+using System.ComponentModel;
+using OpenCV.Net;
+using System;
+
+namespace FipExtensions
+{
+ [Description("Represents a camera frame from a FIP.")]
+ public class FipFrame
+ {
+ public IplImage Image { get; set; }
+ public FipCameraSource Source { get; set; }
+ public long FrameNumber { get; set; }
+ public long FrameTime { get; set; }
+
+ public FipFrame() { }
+
+ public FipFrame(FipFrame other){
+ if (other == null) throw new ArgumentNullException("other");
+ Image = other.Image;
+ Source = other.Source;
+ FrameNumber = other.FrameNumber;
+ FrameTime = other.FrameTime;
+ }
+ }
+
+ [Flags]
+ public enum FipCameraSource
+ {
+ None = 0,
+ Iso = 1 << 0,
+ Green = 1 << 1,
+ Red = 1 << 2,
+ GreenIso = Green | Iso,
+ }
+}
\ No newline at end of file
diff --git a/src/Platforms/FIP_DAQ_Control/src/Extensions/FipLogger.bonsai b/src/Platforms/FIP_DAQ_Control/src/Extensions/FipLogger.bonsai
new file mode 100644
index 00000000..587c9ef3
--- /dev/null
+++ b/src/Platforms/FIP_DAQ_Control/src/Extensions/FipLogger.bonsai
@@ -0,0 +1,93 @@
+
+
+
+
+
+ Source1
+
+
+
+
+
+ LoggingRootPath
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1
+
+
+
+ Rename
+ new(Item1 as LogName, Item2 as DeviceName)
+
+
+
+
+
+ {0}/{1}/{2}
+ Item1,Item2.DeviceName,Item2.LogName
+
+
+
+
+
+
+
+
+ None
+ true
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Platforms/FIP_DAQ_Control/src/Extensions/FipSignalVisualizer.cs b/src/Platforms/FIP_DAQ_Control/src/Extensions/FipSignalVisualizer.cs
new file mode 100644
index 00000000..ee618307
--- /dev/null
+++ b/src/Platforms/FIP_DAQ_Control/src/Extensions/FipSignalVisualizer.cs
@@ -0,0 +1,282 @@
+using System;
+using System.Windows.Forms;
+using System.Collections.Generic;
+using Bonsai;
+using Bonsai.Design;
+using OxyPlot;
+using OxyPlot.Series;
+using OxyPlot.WindowsForms;
+using System.Drawing;
+using OxyPlot.Axes;
+using System.Linq;
+using FipExtensions;
+using Bonsai.Harp;
+using System.Reactive.Linq;
+using Bonsai.Expressions;
+using OpenCV.Net;
+using System.Xml.Serialization;
+
+[assembly: TypeVisualizer(typeof(FipSignalVisualizer),
+ Target = typeof(FipSignalVisualizerSource))]
+
+[Combinator]
+public class FipSignalVisualizerSource
+{
+ private int selectedChannel = 0;
+ private double capacity = 10;
+ private Scalar color = new Scalar(0, 0, 255, 255);
+ private string title = "Photometry Signal";
+
+ private int? maxChannels = null;
+
+ [XmlIgnore]
+ public int SelectedChannel
+ {
+ get { return selectedChannel; }
+ set
+ {
+ if (!maxChannels.HasValue) { selectedChannel = value; }
+ else
+ {
+ if (!(value < 0 || value >= maxChannels))
+ {
+ selectedChannel = value;
+ }
+ }
+ }
+ }
+
+ public double Capacity
+ {
+ get { return capacity; }
+ set { capacity = value; }
+ }
+
+ public Scalar Color
+ {
+ get { return color; }
+ set { color = value; }
+ }
+
+ public string Title
+ {
+ get { return title; }
+ set { title = value; }
+ }
+
+ public IObservable> Process(IObservable> source)
+ {
+ return source.Do(x => this.maxChannels = x.Value.Count);
+ }
+}
+
+
+public class FipSignalVisualizer : BufferedVisualizer
+{
+ internal FipTimeSeries timeSeries;
+ internal FipSignalVisualizerSource source;
+ internal LineSeries lineSeries { get; private set; }
+ private bool resetAxes = true;
+ private double capacity { get; set; }
+
+ private int selectedChannel { get; set; }
+
+ public void SyncWithSource()
+ {
+
+ if (source == null) return;
+ if (capacity != source.Capacity)
+ {
+ capacity = source.Capacity;
+ }
+
+ if (selectedChannel != source.SelectedChannel)
+ {
+ selectedChannel = source.SelectedChannel;
+ if (timeSeries != null && lineSeries != null)
+ {
+ timeSeries.ResetLineSeries(lineSeries);
+ timeSeries.ResetAxes();
+ }
+ }
+ }
+ public override void Load(IServiceProvider provider)
+ {
+ var context = (ITypeVisualizerContext)provider.GetService(typeof(ITypeVisualizerContext));
+ var visualizerElement = ExpressionBuilder.GetVisualizerElement(context.Source);
+ source = (FipSignalVisualizerSource)ExpressionBuilder.GetWorkflowElement(visualizerElement.Builder);
+ SyncWithSource();
+
+ timeSeries = new FipTimeSeries(source.Title)
+ {
+ Capacity = capacity,
+ Dock = DockStyle.Fill,
+ Size = new System.Drawing.Size(800, 600),
+ };
+
+
+
+ var lineSeriesName = string.IsNullOrEmpty(source.Title) ? "TimeSeries" : source.Title;
+ lineSeries = timeSeries.AddNewLineSeries(lineSeriesName, color: OxyColor.FromArgb(
+ (byte)source.Color.Val3,
+ (byte)source.Color.Val2,
+ (byte)source.Color.Val1,
+ (byte)source.Color.Val0));
+ lineSeries.StrokeThickness = 2;
+ lineSeries.Title = lineSeriesName;
+
+ timeSeries.ResetLineSeries(lineSeries);
+ timeSeries.ResetAxes();
+
+ var visualizerService = (IDialogTypeVisualizerService)provider.GetService(typeof(IDialogTypeVisualizerService));
+ if (visualizerService != null)
+ {
+ visualizerService.AddControl(timeSeries);
+ }
+ }
+
+
+ public override void Show(object value)
+ {
+ }
+
+ protected override void Show(DateTime time, object value)
+ {
+ SyncWithSource();
+ Timestamped activity = (Timestamped)value;
+ timeSeries.AddToLineSeries(
+ lineSeries: lineSeries,
+ time: activity.Seconds,
+ value: activity.Value[selectedChannel].Activity.Val0
+ );
+
+ }
+
+ protected override void ShowBuffer(IList> values)
+ {
+ base.ShowBuffer(values);
+ var castedValues = values.Select(x => (Timestamped)x.Value).ToList();
+ if (values.Count > 0)
+ {
+ if (resetAxes)
+ {
+ var time = castedValues.LastOrDefault().Seconds;
+ timeSeries.SetAxes(min: time - capacity, max: time);
+ }
+ timeSeries.UpdatePlot();
+ }
+ }
+ internal void ShowDataBuffer(IList> values, bool resetAxes = true)
+ {
+ this.resetAxes = resetAxes;
+ ShowBuffer(values);
+ }
+
+ public override void Unload()
+ {
+ if (!timeSeries.IsDisposed)
+ {
+ timeSeries.Dispose();
+ }
+ }
+}
+
+public class FipTimeSeries : UserControl
+{
+
+ PlotModel plotModel;
+ PlotView plotView;
+
+ Axis xAxis;
+ Axis yAxis;
+
+ StatusStrip statusStrip;
+
+ public double Capacity { get; set; }
+
+ public FipTimeSeries(string Title)
+ {
+ plotView = new PlotView
+ {
+ Size = Size,
+ Dock = DockStyle.Fill,
+ };
+
+ plotModel = new PlotModel();
+
+ xAxis = new LinearAxis
+ {
+ Position = AxisPosition.Bottom,
+ Title = "Seconds",
+ MajorGridlineStyle = LineStyle.Solid,
+ MinorGridlineStyle = LineStyle.Dot,
+ FontSize = 12
+ };
+
+ yAxis = new LinearAxis
+ {
+ Position = AxisPosition.Left,
+ Title = Title,
+ FontSize = 12
+ };
+
+ plotModel.Axes.Add(xAxis);
+ plotModel.Axes.Add(yAxis);
+
+ plotView.Model = plotModel;
+ Controls.Add(plotView);
+
+ statusStrip = new StatusStrip
+ {
+ Visible = true,
+ };
+
+ Controls.Add(statusStrip);
+ AutoScaleDimensions = new SizeF(6F, 13F);
+ }
+
+
+ public LineSeries AddNewLineSeries(string lineSeriesName, OxyColor color)
+ {
+ LineSeries lineSeries = new LineSeries
+ {
+ Title = lineSeriesName,
+ Color = color
+ };
+ plotModel.Series.Add(lineSeries);
+ return lineSeries;
+ }
+
+ public void AddToLineSeries(LineSeries lineSeries, double time, double value)
+ {
+ lineSeries.Points.RemoveAll(dataPoint => dataPoint.X < time - Capacity);
+ lineSeries.Points.Add(new DataPoint(time, value));
+ }
+
+ public void SetAxes(double min, double max)
+ {
+ xAxis.Minimum = min;
+ xAxis.Maximum = max;
+ }
+
+ public void UpdatePlot()
+ {
+ plotModel.InvalidatePlot(true);
+ }
+
+ public void ResetLineSeries(LineSeries lineSeries)
+ {
+ lineSeries.Points.Clear();
+ }
+
+ public void ResetModelSeries()
+ {
+ plotModel.Series.Clear();
+ }
+
+ public void ResetAxes()
+ {
+ xAxis.Reset();
+ yAxis.Reset();
+ }
+}
\ No newline at end of file
diff --git a/src/Platforms/FIP_DAQ_Control/src/Extensions/FipSpinnakerCapture.cs b/src/Platforms/FIP_DAQ_Control/src/Extensions/FipSpinnakerCapture.cs
new file mode 100644
index 00000000..280e11de
--- /dev/null
+++ b/src/Platforms/FIP_DAQ_Control/src/Extensions/FipSpinnakerCapture.cs
@@ -0,0 +1,91 @@
+using System.ComponentModel;
+using Bonsai.Spinnaker;
+using SpinnakerNET;
+using OpenCV.Net;
+using System;
+
+namespace FipExtensions
+{
+ [Description("Configures and initializes a Spinnaker camera for fiber photometry acquisition.")]
+ public class FipSpinnakerCapture : SpinnakerCapture
+ {
+ public FipSpinnakerCapture()
+ {
+ Gain = 0;
+ Offset = new Point(0, 0);
+ }
+
+ [Description("The gain of the sensor.")]
+ public double Gain { get; set; }
+
+ [Description("The offset of the region of interest.")]
+ public Point Offset { get; set; }
+
+ private const int height = 200;
+ private const int width = 200;
+
+ protected override void Configure(IManagedCamera camera)
+ {
+ try { camera.AcquisitionStop.Execute(); }
+ catch { }
+
+ camera.BinningSelector.Value = BinningSelectorEnums.All.ToString();
+ camera.BinningHorizontalMode.Value = BinningHorizontalModeEnums.Sum.ToString();
+ camera.BinningVerticalMode.Value = BinningVerticalModeEnums.Sum.ToString();
+ camera.BinningHorizontal.Value = 4;
+ camera.BinningVertical.Value = 4;
+ camera.DecimationHorizontalMode.Value = DecimationHorizontalModeEnums.Discard.ToString();
+ camera.DecimationVerticalMode.Value = DecimationVerticalModeEnums.Discard.ToString();
+ camera.DecimationHorizontal.Value = 1;
+ camera.DecimationVertical.Value = 1;
+
+ camera.AcquisitionFrameRateEnable.Value = false;
+ camera.IspEnable.Value = false;
+
+ camera.TriggerDelay.Value = camera.TriggerDelay.Min;
+ camera.TriggerSelector.Value = TriggerSelectorEnums.FrameStart.ToString();
+ camera.TriggerSource.Value = TriggerSourceEnums.Line0.ToString();
+ camera.TriggerActivation.Value = TriggerActivationEnums.RisingEdge.ToString();
+ camera.LineInputFilterSelector.Value = LineInputFilterSelectorEnums.Deglitch.ToString();
+
+ camera.ExposureAuto.Value = ExposureAutoEnums.Off.ToString();
+ camera.ExposureMode.Value = ExposureModeEnums.TriggerWidth.ToString();
+ camera.BlackLevelSelector.Value = BlackLevelSelectorEnums.All.ToString();
+ camera.BlackLevel.Value = 0;
+ camera.DeviceLinkThroughputLimit.Value = camera.DeviceLinkThroughputLimit.Max;
+ camera.GainAuto.Value = GainAutoEnums.Off.ToString();
+ camera.Gain.Value = Gain;
+ camera.GammaEnable.Value = false;
+
+ camera.PixelFormat.Value = PixelFormatEnums.Mono16.ToString();
+ camera.AdcBitDepth.Value = AdcBitDepthEnums.Bit10.ToString();
+
+ SetRegionOfInterest(camera, new Rect(Offset.X, Offset.Y, width, height));
+
+ base.Configure(camera);
+ }
+
+ private static void SetRegionOfInterest(IManagedCamera camera, Rect crop)
+ {
+ if ((crop.Height == 0) || (crop.Width == 0))
+ {
+ if (crop.X != 0 || crop.Y != 0 || crop.Height != 0 || crop.Width != 0)
+ {
+ throw new InvalidOperationException("If Height or Width is 0, all size arguments must be 0.");
+ }
+ camera.OffsetX.Value = 0;
+ camera.OffsetY.Value = 0;
+ camera.Width.Value = camera.WidthMax.Value;
+ camera.Height.Value = camera.HeightMax.Value;
+ }
+ else
+ {
+ camera.Width.Value = crop.Width;
+ camera.Height.Value = crop.Height;
+ camera.OffsetX.Value = crop.X;
+ camera.OffsetY.Value = crop.Y;
+ }
+ }
+ }
+}
+
diff --git a/src/Platforms/FIP_DAQ_Control/src/Extensions/FipWriter.cs b/src/Platforms/FIP_DAQ_Control/src/Extensions/FipWriter.cs
new file mode 100644
index 00000000..d5616bba
--- /dev/null
+++ b/src/Platforms/FIP_DAQ_Control/src/Extensions/FipWriter.cs
@@ -0,0 +1,168 @@
+using Bonsai.IO;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Text;
+using Bonsai;
+using System.ComponentModel;
+using System.Linq;
+using Bonsai.Harp;
+using Bonsai.Dsp;
+using System.Reactive.Linq;
+using OpenCV.Net;
+
+namespace FipExtensions
+{
+ [Combinator]
+ [DefaultProperty("FileName")]
+ [Description("Writes a timestamped stream of Fip data to disk.")]
+ public class FipWriter : FileSink
+ {
+ public int? ExpectedRegionCount = null;
+
+ public IObservable> Process(IObservable> source)
+ {
+ var filePath = Path.Combine(Path.GetDirectoryName(FileName), Path.GetFileNameWithoutExtension(FileName));
+ var fipCsvWriter = new FipCsvWriter(this, ExpectedRegionCount)
+ {
+ FileName = filePath + ".csv",
+ Suffix = Suffix,
+ Buffered = Buffered,
+ Overwrite = Overwrite,
+ };
+ var fipMatrixWriter = new FipMatrixWriter()
+ {
+ Path = filePath + ".bin",
+ Suffix = Suffix,
+ Overwrite = Overwrite,
+ };
+ return source.Publish(ps =>
+ {
+ var fipCsv = fipCsvWriter.Process(ps);
+ var fipMatrix = fipMatrixWriter.Process(ps.Select(v => v.Value.FipFrame.Image.GetMat())).IgnoreElements().Cast>();
+ var fipMetadata = ps.Take(1).Do(f => File.WriteAllText(
+ filePath + "_meta.json",
+ ImageMetadata.FromImage(f.Value.FipFrame.Image).ToJson())).IgnoreElements();
+ return Observable.Merge(fipCsv, fipMatrix, fipMetadata);
+ });
+ }
+
+ private class ImageMetadata
+ {
+ public int Width { get; set; }
+ public int Height { get; set; }
+ public int Channels { get; set; }
+ public MatrixLayout Layout { get; set; }
+ public string Depth { get; set; }
+
+ public string ToJson()
+ {
+ return Newtonsoft.Json.JsonConvert.SerializeObject(this);
+ }
+
+ public static ImageMetadata FromImage(IplImage image, MatrixLayout layout = MatrixLayout.ColumnMajor)
+ {
+ return new ImageMetadata
+ {
+ Width = image.Width,
+ Height = image.Height,
+ Channels = image.Channels,
+ Depth = image.Depth.ToString(),
+ Layout = layout
+ };
+ }
+ }
+
+ class FipMatrixWriter : MatrixWriter
+ {
+ [Description("Specifies the sequential memory layout used to store the sample buffers.")]
+ [Browsable(false)]
+ public new MatrixLayout Layout
+ {
+ get { return base.Layout; }
+ set { base.Layout = value; }
+ }
+
+ public FipMatrixWriter() : base()
+ {
+ Layout = MatrixLayout.ColumnMajor;
+ }
+ }
+
+ class FipCsvWriter : FileSink, StreamWriter>
+ {
+
+ internal int? ExpectedRegionCount = null;
+ const int MetadataOffset = 3;
+
+ internal FipCsvWriter(FipWriter writer, int? expectedRegionCount)
+ {
+ Writer = writer;
+ ExpectedRegionCount = expectedRegionCount;
+ }
+
+ internal FipWriter Writer { get; private set; }
+
+ protected override StreamWriter CreateWriter(string fileName, Timestamped input)
+ {
+ var nRegions = input.Value.Count;
+
+ if (ExpectedRegionCount == null)
+ {
+ ExpectedRegionCount = nRegions;
+ }
+ else if (ExpectedRegionCount != nRegions)
+ {
+ throw new ArgumentException("Number of regions in the input stream does not match the number of regions in the first frame.");
+ }
+
+ if (nRegions == 0) throw new ArgumentException("No regions defined for FipWriter.");
+ var writer = new StreamWriter(fileName, false, Encoding.ASCII);
+ var columns = new List(MetadataOffset + nRegions)
+ {
+ "ReferenceTime",
+ "CameraFrameNumber",
+ "CameraFrameTime",
+ "Background", // We assume the first region is always the background region
+ };
+
+ if (nRegions > 0)
+ {
+ for (int i = 1; i < nRegions; i++)
+ {
+ columns.Add("Fiber_" + (i - 1).ToString(CultureInfo.InvariantCulture));
+ }
+ }
+ var header = string.Join(",", columns);
+ writer.WriteLine(header);
+ return writer;
+ }
+
+ protected override void Write(StreamWriter writer, Timestamped input)
+ {
+ var nRegions = input.Value.Count;
+ if (nRegions != ExpectedRegionCount)
+ {
+ throw new ArgumentException("Number of regions in the input stream does not match the number of regions in the first frame.");
+ }
+ var values = new List(MetadataOffset + nRegions)
+ {
+ input.Seconds.ToString(CultureInfo.InvariantCulture),
+ input.Value.FipFrame.FrameNumber.ToString(CultureInfo.InvariantCulture),
+ input.Value.FipFrame.FrameTime.ToString(CultureInfo.InvariantCulture)
+ };
+
+ var activity = input.Value.Select(x => x.Activity).ToArray();
+
+ for (int i = 0; i < activity.Length; i++)
+ {
+ values.Add(activity[i].Val0.ToString(CultureInfo.InvariantCulture));
+ }
+ var line = string.Join(",", values);
+ writer.WriteLine(line);
+ }
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Platforms/FIP_DAQ_Control/src/Extensions/FlipFipFrame.cs b/src/Platforms/FIP_DAQ_Control/src/Extensions/FlipFipFrame.cs
new file mode 100644
index 00000000..66c6c0bc
--- /dev/null
+++ b/src/Platforms/FIP_DAQ_Control/src/Extensions/FlipFipFrame.cs
@@ -0,0 +1,63 @@
+using Bonsai;
+using System;
+using System.ComponentModel;
+using System.Linq;
+using System.Reactive.Linq;
+using OpenCV.Net;
+using FipExtensions;
+
+[Combinator]
+[Description("Flips the image of the FipFrame in-place.")]
+[WorkflowElementCategory(ElementCategory.Transform)]
+public class FlipInPlace
+{
+
+ private FlipMode? flipMode = null;
+
+ public FlipMode? FlipMode
+ {
+ get { return flipMode; }
+ set { flipMode = value; }
+ }
+
+ public IObservable Process(IObservable source)
+ {
+ return source.Select(img =>
+ {
+ return Flip(img);
+ });
+ }
+
+ public IObservable Process(IObservable source)
+ {
+ return source.Select(frame =>
+ {
+ var img = frame.Image;
+ if (img != null)
+ {
+ img = Flip(img);
+ }
+ return new FipFrame()
+ {
+ Image = img,
+ Source = frame.Source,
+ FrameNumber = frame.FrameNumber,
+ FrameTime = frame.FrameTime
+ };
+ });
+ }
+
+ private IplImage Flip(IplImage img)
+ {
+ if (!flipMode.HasValue)
+ {
+ return img;
+ }
+ if (img.Width != img.Height)
+ {
+ throw new ArgumentException("Image must be square to flip in place.");
+ }
+ CV.Flip(img, img, flipMode.Value);
+ return img;
+ }
+}
\ No newline at end of file
diff --git a/src/Platforms/FIP_DAQ_Control/src/Extensions/ImageEllipsePicker.cs b/src/Platforms/FIP_DAQ_Control/src/Extensions/ImageEllipsePicker.cs
new file mode 100644
index 00000000..e64de65d
--- /dev/null
+++ b/src/Platforms/FIP_DAQ_Control/src/Extensions/ImageEllipsePicker.cs
@@ -0,0 +1,391 @@
+using System;
+using System.Linq;
+using OpenCV.Net;
+using System.Reactive.Linq;
+using System.Windows.Forms;
+using System.Collections.ObjectModel;
+using System.Drawing;
+using OpenTK.Graphics.OpenGL;
+using Bonsai.Design;
+using System.Globalization;
+using System.Drawing.Text;
+using System.Drawing.Drawing2D;
+using System.Collections.Specialized;
+using Point = OpenCV.Net.Point;
+using Font = System.Drawing.Font;
+
+namespace Bonsai.Vision.Design
+{
+ class ImageEllipsePicker : ImageBox
+ {
+ bool disposed;
+ int? selectedRoi;
+ const int FillOpacity = 85;
+ const float LabelFontScale = 0.1f;
+ const double ScaleIncrement = 0.1;
+ readonly ObservableCollection regions = new ObservableCollection();
+ readonly CommandExecutor commandExecutor = new CommandExecutor();
+ IplImage labelImage;
+ IplImageTexture labelTexture;
+ bool refreshLabels;
+ Font labelFont;
+ bool dragging;
+
+ public ImageEllipsePicker()
+ {
+ LabelRegions = true;
+ Canvas.KeyDown += Canvas_KeyDown;
+ commandExecutor.StatusChanged += commandExecutor_StatusChanged;
+ regions.CollectionChanged += regions_CollectionChanged;
+ var mouseDoubleClick = Observable.FromEventPattern(Canvas, "MouseDoubleClick").Select(e => e.EventArgs);
+ var mouseMove = Observable.FromEventPattern(Canvas, "MouseMove").Select(e => e.EventArgs);
+ var mouseDown = Observable.FromEventPattern(Canvas, "MouseDown").Select(e => e.EventArgs);
+ var mouseUp = Observable.FromEventPattern(Canvas, "MouseUp").Select(e => e.EventArgs);
+ var dragStart = mouseDown.Select(x => new Action(() => dragging = true));
+ var dragEnd = mouseUp.Select(x => new Action(() => dragging = false));
+
+ var roiSelected = from downEvt in mouseDown
+ where Image != null
+ let location = NormalizedLocation(downEvt.X, downEvt.Y)
+ let selection = (from region in regions.Select((rect, i) => new { rect, i = (int?)i })
+ let distance = TestIntersection(region.rect, location)
+ where distance < 1
+ orderby distance
+ select region.i)
+ .FirstOrDefault()
+ select new Action(() => SelectedRegion = selection);
+
+ var roiMoveScale = (from downEvt in mouseDown
+ where Image != null && selectedRoi.HasValue
+ let location = NormalizedLocation(downEvt.X, downEvt.Y)
+ let selection = selectedRoi.Value
+ let region = regions[selection]
+ select (from moveEvt in mouseMove.TakeUntil(mouseUp)
+ let target = NormalizedLocation(moveEvt.X, moveEvt.Y)
+ let modifiedRegion = downEvt.Button == MouseButtons.Right
+ ? ScaleRegion(region, target, IsCirclePicker || ModifierKeys.HasFlag(Keys.Control))
+ : MoveRegion(region, target - location)
+ let modifiedRectangle = RegionRectangle(modifiedRegion)
+ where modifiedRectangle.Width > 0 && modifiedRectangle.Height > 0 &&
+ modifiedRectangle.Left >= 0 && modifiedRectangle.Top >= 0 &&
+ modifiedRectangle.Right < Image.Width && modifiedRectangle.Bottom < Image.Height
+ select modifiedRegion)
+ .Publish(ps =>
+ ps.TakeLast(1).Do(modifiedRegion =>
+ commandExecutor.Execute(
+ () => regions[selection] = modifiedRegion,
+ () => regions[selection] = region))
+ .Merge(ps))
+ .Select(displacedRegion => new Action(() => regions[selection] = displacedRegion)))
+ .Switch();
+
+ var regionInsertion = (from downEvt in mouseDown
+ where Image != null && downEvt.Button == MouseButtons.Left && !selectedRoi.HasValue && !(regions.Count >= MaxRegions)
+ let count = regions.Count
+ let origin = NormalizedLocation(downEvt.X, downEvt.Y)
+ select (from moveEvt in mouseMove.TakeUntil(mouseUp)
+ let location = EnsureSizeRatio(
+ origin,
+ NormalizedLocation(moveEvt.X, moveEvt.Y),
+ IsCirclePicker || ModifierKeys.HasFlag(Keys.Control))
+ where location.X - origin.X != 0 && location.Y - origin.Y != 0
+ select CreateEllipseRegion(origin, location))
+ .Publish(ps =>
+ ps.TakeLast(1).Do(region =>
+ commandExecutor.Execute(
+ () => { if (count == regions.Count) AddRegion(region); },
+ () => { regions.Remove(region); SelectedRegion = null; }))
+ .Merge(ps))
+ .Select(region => new Action(() =>
+ {
+ if (selectedRoi.HasValue) regions[selectedRoi.Value] = region;
+ else AddRegion(region);
+ })))
+ .Switch();
+
+ var roiActions = Observable.Merge(dragStart, dragEnd, roiSelected, roiMoveScale, regionInsertion);
+ roiActions.Subscribe(action =>
+ {
+ action();
+ });
+ }
+
+ void regions_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
+ {
+ refreshLabels = true;
+ }
+
+ void commandExecutor_StatusChanged(object sender, EventArgs e)
+ {
+ Canvas.Invalidate();
+ }
+
+ static RotatedRect CreateEllipseRegion(Point origin, Point location)
+ {
+ RotatedRect region;
+ region.Size = new Size2f(Math.Abs(location.X - origin.X), Math.Abs(location.Y - origin.Y));
+ region.Center = new Point2f((location.X + origin.X) / 2f, (location.Y + origin.Y) / 2f);
+ region.Angle = 0;
+ return region;
+ }
+
+ static RotatedRect MoveRegion(RotatedRect region, Point displacement)
+ {
+ region.Center += new Point2f(displacement);
+ return region;
+ }
+
+ static RotatedRect ScaleRegion(RotatedRect region, Point target, bool uniformScaling)
+ {
+ var size = new Size2f(
+ 2 * Math.Abs(target.X - region.Center.X),
+ 2 * Math.Abs(target.Y - region.Center.Y));
+ if (uniformScaling)
+ {
+ var sizeNorm = (float)Math.Sqrt(size.Width * size.Width + size.Height * size.Height);
+ region.Size.Width = sizeNorm;
+ region.Size.Height = sizeNorm;
+ }
+ else region.Size = size;
+ return region;
+ }
+
+ void Canvas_KeyDown(object sender, KeyEventArgs e)
+ {
+ if (dragging) return;
+ if (e.KeyCode == Keys.PageUp) ImageScale += ScaleIncrement;
+ if (e.KeyCode == Keys.PageDown) ImageScale -= ScaleIncrement;
+ if (e.Control && e.KeyCode == Keys.Z) commandExecutor.Undo();
+ if (e.Control && e.KeyCode == Keys.Y) commandExecutor.Redo();
+ if (e.Control && e.KeyCode == Keys.V && !(regions.Count >= MaxRegions))
+ {
+ var roiText = (string)Clipboard.GetData(DataFormats.Text);
+ try
+ {
+ var mousePosition = PointToClient(MousePosition);
+ var offset = NormalizedLocation(mousePosition.X, mousePosition.Y);
+ var roiData = (float[])ArrayConvert.ToArray(roiText, 1, typeof(float));
+ var center = new Point2f(offset.X, offset.Y);
+ var size = new Size2f(roiData[0], roiData[1]);
+ var roi = new RotatedRect(center, size, 0);
+
+ var selection = selectedRoi;
+ commandExecutor.Execute(
+ () => AddRegion(roi),
+ () => { regions.Remove(roi); SelectedRegion = selection; });
+ }
+ catch (ArgumentException) { }
+ catch (InvalidCastException) { }
+ catch (FormatException) { }
+ }
+
+ if (selectedRoi.HasValue)
+ {
+ if (e.Control && e.KeyCode == Keys.C)
+ {
+ var roi = regions[selectedRoi.Value];
+ var roiData = new[] { roi.Size.Width, roi.Size.Height };
+ Clipboard.SetData(DataFormats.Text, ArrayConvert.ToString(roiData));
+ }
+
+ if (e.KeyCode == Keys.Delete)
+ {
+ var selection = selectedRoi.Value;
+ var region = regions[selection];
+ commandExecutor.Execute(
+ () => { regions.RemoveAt(selection); SelectedRegion = null; },
+ () => { regions.Insert(selection, region); SelectedRegion = selection; });
+ }
+ }
+ }
+
+ protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
+ {
+ if (keyData == Keys.Tab && regions.Count > 0)
+ {
+ SelectedRegion = ((selectedRoi ?? 0) + 1) % regions.Count;
+ Canvas.Invalidate();
+ }
+
+ return base.ProcessCmdKey(ref msg, keyData);
+ }
+
+ void AddRegion(RotatedRect region)
+ {
+ regions.Add(region);
+ SelectedRegion = regions.Count - 1;
+ }
+
+ float TestIntersection(RotatedRect region, Point point)
+ {
+ var dx = point.X - region.Center.X;
+ var dy = point.Y - region.Center.Y;
+ var a = region.Size.Width * region.Size.Width / 4;
+ var b = region.Size.Height * region.Size.Height / 4;
+ return dx * dx / a + dy * dy / b;
+ }
+
+ Point NormalizedLocation(int x, int y)
+ {
+ return new Point(
+ Math.Max(0, Math.Min((int)(x * Image.Width / (float)Canvas.Width), Image.Width - 1)),
+ Math.Max(0, Math.Min((int)(y * Image.Height / (float)Canvas.Height), Image.Height - 1)));
+ }
+
+ Point EnsureSizeRatio(Point origin, Point location, bool square)
+ {
+ if (square)
+ {
+ var dx = location.X - origin.X;
+ var dy = location.Y - origin.Y;
+ var width = Math.Abs(dx);
+ var height = Math.Abs(dy);
+ if (width < height) location.Y -= Math.Sign(dy) * (height - width);
+ else location.X -= Math.Sign(dx) * (width - height);
+ }
+ return location;
+ }
+
+ RectangleF RegionRectangle(RotatedRect region)
+ {
+ var x = region.Center.X - region.Size.Width / 2f;
+ var y = region.Center.Y - region.Size.Height / 2f;
+ var width = region.Size.Width;
+ var height = region.Size.Height;
+ return new RectangleF(x, y, width, height);
+ }
+
+ public bool LabelRegions { get; set; }
+
+ public bool IsCirclePicker { get; set; }
+
+ public int? MaxRegions { get; set; }
+
+ public int? SelectedRegion
+ {
+ get { return selectedRoi; }
+ set
+ {
+ selectedRoi = value;
+ refreshLabels = true;
+ }
+ }
+
+ public Collection Regions
+ {
+ get { return regions; }
+ }
+
+ public event EventHandler RegionsChanged
+ {
+ add { regions.CollectionChanged += new NotifyCollectionChangedEventHandler(value); }
+ remove { regions.CollectionChanged -= new NotifyCollectionChangedEventHandler(value); }
+ }
+
+ protected override void OnLoad(EventArgs e)
+ {
+ if (DesignMode) return;
+ GL.Enable(EnableCap.Blend);
+ GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
+ labelTexture = new IplImageTexture();
+ base.OnLoad(e);
+ }
+
+ private void UpdateLabelTexture()
+ {
+ if (labelImage != null)
+ {
+ if (labelFont == null)
+ {
+ var emSize = Font.SizeInPoints * (labelImage.Height * LabelFontScale) / Font.Height;
+ labelFont = new Font(Font.FontFamily, emSize);
+ }
+
+ labelImage.SetZero();
+ using (var labelBitmap = new Bitmap(labelImage.Width, labelImage.Height, labelImage.WidthStep, System.Drawing.Imaging.PixelFormat.Format32bppArgb, labelImage.ImageData))
+ using (var graphics = Graphics.FromImage(labelBitmap))
+ using (var regionBrush = new SolidBrush(Color.FromArgb(FillOpacity, Color.Red)))
+ using (var selectedBrush = new SolidBrush(Color.FromArgb(FillOpacity, Color.LimeGreen)))
+ using (var format = new StringFormat())
+ {
+ graphics.TextRenderingHint = TextRenderingHint.AntiAliasGridFit;
+ graphics.SmoothingMode = SmoothingMode.AntiAlias;
+ format.Alignment = StringAlignment.Center;
+ format.LineAlignment = StringAlignment.Center;
+ for (int i = 0; i < regions.Count; i++)
+ {
+ var rect = RegionRectangle(regions[i]);
+ var brush = i == selectedRoi ? selectedBrush : regionBrush;
+ graphics.FillEllipse(brush, rect);
+ if (LabelRegions)
+ {
+ var label = i > 0 ? (i - 1).ToString(CultureInfo.InvariantCulture) : "B";
+ graphics.DrawString(label, labelFont, Brushes.White, rect, format);
+ }
+ }
+ }
+
+ labelTexture.Update(labelImage);
+ }
+ }
+
+ protected override void SetImage(IplImage image)
+ {
+ if (image == null) labelImage = null;
+ else if (labelImage == null || labelImage.Width != image.Width || labelImage.Height != image.Height)
+ {
+ labelImage = new IplImage(image.Size, IplDepth.U8, 4);
+ refreshLabels = true;
+ }
+ base.SetImage(image);
+ }
+
+ protected override void OnRenderFrame(EventArgs e)
+ {
+ base.OnRenderFrame(e);
+ var image = Image;
+ if (image != null)
+ {
+ GL.Disable(EnableCap.Texture2D);
+ GL.Color3(Color.White);
+ GL.Enable(EnableCap.Texture2D);
+ if (labelImage != null)
+ {
+ if (refreshLabels)
+ {
+ UpdateLabelTexture();
+ refreshLabels = false;
+ }
+ labelTexture.Draw();
+ }
+ }
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (!disposed)
+ {
+ if (disposing)
+ {
+ MakeCurrent();
+ if (labelTexture != null)
+ {
+ labelTexture.Dispose();
+ labelTexture = null;
+ }
+
+ if (labelFont != null)
+ {
+ labelFont.Dispose();
+ labelFont = null;
+ }
+ disposed = true;
+ }
+ }
+
+ base.Dispose(disposing);
+ }
+ }
+}
+
+
diff --git a/src/Platforms/FIP_DAQ_Control/src/Extensions/Licenses.md b/src/Platforms/FIP_DAQ_Control/src/Extensions/Licenses.md
new file mode 100644
index 00000000..99a7fa1e
--- /dev/null
+++ b/src/Platforms/FIP_DAQ_Control/src/Extensions/Licenses.md
@@ -0,0 +1,25 @@
+Several of the extensions used in this project were adapted from other projects. These include:
+
+- [bonsai-rx/bonsai](https://github.com/bonsai-rx/bonsai)
+
+```raw
+Copyright (c) 2011-2024 Bonsai Foundation CIC and Contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+```
\ No newline at end of file
diff --git a/src/Platforms/FIP_DAQ_Control/src/Extensions/LoadRoiDefault.cs b/src/Platforms/FIP_DAQ_Control/src/Extensions/LoadRoiDefault.cs
new file mode 100644
index 00000000..3f93e024
--- /dev/null
+++ b/src/Platforms/FIP_DAQ_Control/src/Extensions/LoadRoiDefault.cs
@@ -0,0 +1,74 @@
+using Bonsai;
+using System;
+using System.ComponentModel;
+using System.Reactive.Linq;
+using AindPhysiologyFip;
+using Newtonsoft.Json;
+using System.IO;
+using System.Linq;
+
+namespace FipExtensions
+{
+ [Combinator]
+ [Description("Loads the ROI default settings from disk. In order of preference: 1. schema file, 2. local file, 3. default settings.")]
+ [WorkflowElementCategory(ElementCategory.Combinator)]
+ public class LoadRoiDefault
+ {
+ private string path = "../local/default.json";
+ [Description("The path to the ROI default settings file.")]
+ [FileNameFilter("JSON|*.json|All Files|*.*")]
+ [Editor("Bonsai.Design.OpenFileNameEditor, Bonsai.Design", DesignTypes.UITypeEditor)]
+ public string Path
+ {
+ get { return path; }
+ set { path = value; }
+ }
+
+
+ public IObservable Process(){
+ return Observable.Return(DefaultRoiSettings());
+ }
+
+ public IObservable Process(IObservable source)
+ {
+ return source.Select(value =>
+ {
+ // 1. We attempt to load the settings from the schema file
+ if (value != null) return value.Clone() as RoiSettings;
+ // 2. If 1. fails, we attempt to load the default settings via the path property
+ value = JsonConvert.DeserializeObject(File.ReadAllText(path));
+ if (value != null){
+
+ return value;}
+
+ // 3. If 2 fails, we create a default RoiSettings object
+ else{return DefaultRoiSettings();}
+ });
+ }
+
+
+ private static Circle makeCircle(double x, double y, double radius)
+ {
+ return new Circle()
+ {
+ Center = new AindPhysiologyFip.Point2f()
+ {
+ X = x,
+ Y = y
+ },
+ Radius = radius
+ };
+ }
+
+ private static RoiSettings DefaultRoiSettings()
+ {
+ return new RoiSettings()
+ {
+ CameraGreenIsoBackground = makeCircle(0, 0, 20),
+ CameraRedBackground = makeCircle(0, 0, 20),
+ CameraGreenIsoRoi = (new double[] { 50, 150 }).SelectMany(x => (new double[] { 50, 150 }).Select(y => makeCircle(x, y, 20))).ToList(),
+ CameraRedRoi = (new double[] { 50, 150 }).SelectMany(x => (new double[] { 50, 150 }).Select(y => makeCircle(x, y, 20))).ToList(),
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Platforms/FIP_DAQ_Control/src/Extensions/Logging.bonsai b/src/Platforms/FIP_DAQ_Control/src/Extensions/Logging.bonsai
new file mode 100644
index 00000000..06beaf24
--- /dev/null
+++ b/src/Platforms/FIP_DAQ_Control/src/Extensions/Logging.bonsai
@@ -0,0 +1,540 @@
+
+
+
+
+
+ Logging
+
+
+
+ LoggingRootPath
+
+
+ DateTime.Now
+ DateTime.Now.ToString("yyyy-MM-ddTHHmmss")
+
+
+
+ 1
+
+
+
+ DateTimeSuffix
+
+
+ LoggingRootPath
+
+
+
+ Fib
+
+
+
+
+ 1
+
+
+
+ Fib
+
+
+
+
+ Source1
+
+
+
+ fip
+
+
+
+ DateTimeSuffix
+
+
+
+
+
+ {0}/{1}_{2}
+ Item1, Item2, Item3
+
+
+
+ 1
+
+
+
+ RootLoggingPath
+
+
+ Logs
+
+
+
+
+ Logs
+
+
+
+
+ 1
+
+
+
+ DeviceName
+
+
+ SessionSchema
+
+
+
+ None
+
+
+
+ DeviceName
+
+
+
+
+
+
+
+ LoggingRootPath
+
+
+ session_output
+
+
+ RigSchema
+
+
+
+ None
+
+
+
+ DeviceName
+
+
+
+
+
+
+
+ LoggingRootPath
+
+
+ rig_output
+
+
+ RawSessionSchema
+
+
+ DeviceName
+
+
+
+
+
+
+
+ LoggingRootPath
+
+
+ session_input
+ json
+
+
+ RawRigSchema
+
+
+ DeviceName
+
+
+
+
+
+
+
+ LoggingRootPath
+
+
+ rig_input
+ json
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ CameraStreams
+
+
+ LogCameraMetadata
+
+
+
+ Source1
+
+
+
+ 1
+
+
+
+ CameraStream
+
+
+ Key
+
+
+ ToString
+ it.ToString()
+
+
+
+
+
+ CameraName
+
+
+ CameraStream
+
+
+
+
+
+ Item1,Item2.Seconds
+
+
+
+
+
+
+
+
+ Value.Seconds,Value.Value.FrameNumber,Value.Value.FrameTime,Timestamp.UtcDateTime
+
+
+ ReLabel
+ new(
+Item1 as ReferenceTime,
+Item2 as CameraFrameNumber,
+Item3 as CameraFrameTime,
+Item4 as CpuTime)
+
+
+ CameraName
+
+
+ camera_{0}_metadata
+ it
+
+
+
+
+
+
+
+ LoggingRootPath
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ParsedFipStreams
+
+
+ LogFipStreams
+
+
+
+ Source1
+
+
+
+ 1
+
+
+
+ fipStream
+
+
+ Key
+
+
+ ToString
+ it.ToString()
+
+
+
+
+
+ fipStreamName
+
+
+ fipStream
+
+
+
+
+
+ fipStreamName
+
+
+
+
+
+
+
+ LoggingRootPath
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ CurrentCombinedRoi
+
+
+
+ 1
+
+
+
+
+ None
+
+
+
+ LoggingRootPath
+
+
+ regions
+ json
+
+
+ SoftwareEvents
+
+
+
+ SoftwareEvent
+
+
+ HarpTimestampSource
+
+
+
+ TaskPoolScheduler
+
+
+
+
+
+
+
+
+
+
+
+
+ SoftwareEvents
+ LoggingRootPath
+
+
+
+ Repository
+
+
+
+ false
+
+
+
+
+ RepositoryStatus
+
+
+
+ SoftwareEvent
+
+
+ CurrentCombinedRoi
+
+
+
+ Regions
+
+
+
+ SoftwareEvent
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Harp
+
+
+
+ CuttlefishFipEvents
+
+
+
+
+
+
+
+
+
+
+
+
+ CuttlefishFip
+
+
+ CuttlefishFipCommands
+
+
+
+
+
+
+
+
+
+
+
+
+ HarpCommands/CuttlefishFip
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Platforms/FIP_DAQ_Control/src/Extensions/ModifyFipCameraSource.cs b/src/Platforms/FIP_DAQ_Control/src/Extensions/ModifyFipCameraSource.cs
new file mode 100644
index 00000000..a914d09a
--- /dev/null
+++ b/src/Platforms/FIP_DAQ_Control/src/Extensions/ModifyFipCameraSource.cs
@@ -0,0 +1,36 @@
+using Bonsai;
+using System;
+using System.ComponentModel;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reactive.Linq;
+using Bonsai.Harp;
+using FipExtensions;
+
+[Combinator]
+[Description("Replaces the camera source of each frame in the sequence.")]
+[WorkflowElementCategory(ElementCategory.Transform)]
+public class ModifyFipCameraSource
+{
+ [Description("The camera source to set for the frames.")]
+ private FipCameraSource source = FipCameraSource.None;
+ public FipCameraSource Source
+ {
+ get { return source; }
+ set { source = value; }
+ }
+
+
+ public IObservable> Process(IObservable> source)
+ {
+ return Process(source.Select(value => value.Value)).Zip(source, (frame, timestamped) =>
+ {
+ return Timestamped.Create(frame, timestamped.Seconds);
+ });
+ }
+
+ public IObservable Process(IObservable source)
+ {
+ return source.Select(frame => {return new FipFrame(frame){Source = Source};});
+ }
+}
diff --git a/src/Platforms/FIP_DAQ_Control/src/Extensions/PascalToSnake.cs b/src/Platforms/FIP_DAQ_Control/src/Extensions/PascalToSnake.cs
new file mode 100644
index 00000000..404c947a
--- /dev/null
+++ b/src/Platforms/FIP_DAQ_Control/src/Extensions/PascalToSnake.cs
@@ -0,0 +1,28 @@
+using Bonsai;
+using System;
+using System.ComponentModel;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reactive.Linq;
+
+namespace FipExtensions
+{
+ [Combinator]
+ [Description("Converts PascalCase strings to snake_case.")]
+ [WorkflowElementCategory(ElementCategory.Transform)]
+ public class PascalToSnake
+ {
+ public IObservable Process(IObservable source)
+ {
+ return source.Select(ConvertPascalToSnake);
+ }
+
+ private string ConvertPascalToSnake(string input)
+ {
+ if (string.IsNullOrEmpty(input)) return input;
+
+ return string.Concat(input.Select((ch, index) =>
+ index > 0 && char.IsUpper(ch) ? "_" + char.ToLower(ch) : char.ToLower(ch).ToString()));
+ }
+ }
+}
diff --git a/src/Platforms/FIP_DAQ_Control/src/Extensions/ReadSchemas.bonsai b/src/Platforms/FIP_DAQ_Control/src/Extensions/ReadSchemas.bonsai
new file mode 100644
index 00000000..76e41be2
--- /dev/null
+++ b/src/Platforms/FIP_DAQ_Control/src/Extensions/ReadSchemas.bonsai
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+ ..\local\AindPhysioFipRig.json
+
+
+
+ RawRigSchema
+
+
+
+
+
+
+ ..\local\AindBehaviorSessionModel.json
+
+
+
+ RawSessionSchema
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Platforms/FIP_DAQ_Control/src/Extensions/SelectCircles.cs b/src/Platforms/FIP_DAQ_Control/src/Extensions/SelectCircles.cs
new file mode 100644
index 00000000..8ca37678
--- /dev/null
+++ b/src/Platforms/FIP_DAQ_Control/src/Extensions/SelectCircles.cs
@@ -0,0 +1,82 @@
+using Bonsai;
+using System;
+using System.ComponentModel;
+using System.Linq;
+using System.Reactive.Linq;
+using OpenCV.Net;
+using Bonsai.Vision;
+
+[Combinator]
+[Description("Allows selecting a set of circles from an image.")]
+[WorkflowElementCategory(ElementCategory.Combinator)]
+[DefaultProperty("Circles")]
+public class SelectCircles
+{
+
+ private string Label = "Channel";
+ public string label
+ {
+ get { return Label; }
+ set
+ {
+ if (!string.IsNullOrEmpty(value))
+ {
+ Label = value;
+ }
+ }
+ }
+
+ private Circle[] circles = new Circle[0];
+ public Circle[] Circles {
+ get { return circles; }
+ set
+ {
+ if (value != null && value.Length > 0)
+ {
+ circles = value;
+ }
+ }
+ }
+
+ public event Action RefreshRequested;
+
+ private bool refresh;
+ [Description("Triggers a refresh of the visualizer regions.")]
+ public bool Refresh
+ {
+ get {return refresh;}
+ set
+ {
+ if (value)
+ {
+ refresh = false; // Reset the property after triggering
+ var handler = RefreshRequested;
+ if (handler != null) handler.Invoke();
+ }
+ }
+ }
+
+ // Consider using a subject to manager the image stream for visualizers
+ // internal IObservable imageStream;
+
+ // public IObservable Process(IObservable source)
+ // {
+ // return source.Publish(ps => {
+ // imageStream = ps;
+ // return ps.Select(value=> Circles);
+ // });
+ // }
+
+ // public IObservable Process(IObservable source)
+ // {
+ // return source.Publish(ps => {
+ // imageStream = ps;
+ // return ps.Select(value=> Circles);
+ // });
+ // }
+
+ public IObservable Process(IObservable source)
+ {
+ return source.Select(value=> Circles);
+ }
+}
diff --git a/src/Platforms/FIP_DAQ_Control/src/Extensions/SelectCirclesVisualizer.cs b/src/Platforms/FIP_DAQ_Control/src/Extensions/SelectCirclesVisualizer.cs
new file mode 100644
index 00000000..757a276d
--- /dev/null
+++ b/src/Platforms/FIP_DAQ_Control/src/Extensions/SelectCirclesVisualizer.cs
@@ -0,0 +1,145 @@
+using System;
+using Bonsai.Vision.Design;
+using Bonsai;
+using Bonsai.Dag;
+using OpenCV.Net;
+using Bonsai.Vision;
+using System.Reactive.Linq;
+using Bonsai.Design;
+using Bonsai.Expressions;
+using System.Linq;
+using System.Windows.Forms;
+using System.Collections.Generic;
+
+[assembly: TypeVisualizer(typeof(SelectCirclesVisualizer), Target = typeof(SelectCircles))]
+
+public class SelectCirclesVisualizer : DialogTypeVisualizer
+{
+ ImageEllipsePicker ellipsePicker;
+ IDisposable inputHandle;
+
+ ///
+ public override void Show(object value)
+ {
+ }
+
+ ///
+ public override void Load(IServiceProvider provider)
+ {
+ var context = (ITypeVisualizerContext)provider.GetService(typeof(ITypeVisualizerContext));
+ var visualizerElement = ExpressionBuilder.GetVisualizerElement(context.Source);
+ var selectRegions = (SelectCircles)ExpressionBuilder.GetWorkflowElement(visualizerElement.Builder);
+
+ ellipsePicker = new ImageEllipsePicker { IsCirclePicker = true, LabelRegions = true, Dock = DockStyle.Fill };
+ UpdateRegions(selectRegions);
+
+ selectRegions.RefreshRequested += () => UpdateRegions(selectRegions);
+
+ ellipsePicker.RegionsChanged += delegate
+ {
+ selectRegions.Circles = ellipsePicker.Regions.ToArray()
+ .Select(region => new Circle(region.Center, region.Size.Width / 2))
+ .ToArray();
+ };
+
+ var imageInput = VisualizerHelper.ImageInput(provider);
+ if (imageInput != null)
+ {
+ inputHandle = imageInput.Subscribe(value => ellipsePicker.Image = (IplImage)value);
+ ellipsePicker.HandleDestroyed += delegate { inputHandle.Dispose(); };
+ }
+
+ var visualizerService = (IDialogTypeVisualizerService)provider.GetService(typeof(IDialogTypeVisualizerService));
+ if (visualizerService != null)
+ {
+ visualizerService.AddControl(ellipsePicker);
+ }
+
+ visualizerService.AddControl(new Label
+ {
+ Text = selectRegions.label,
+ Dock = DockStyle.Top,
+ TextAlign = System.Drawing.ContentAlignment.MiddleCenter,
+ AutoSize = true,
+ });
+ }
+
+ private void UpdateRegions(SelectCircles selectRegions)
+ {
+ if (selectRegions == null || ellipsePicker == null) return;
+ ellipsePicker.Regions.Clear();
+ foreach (var circle in selectRegions.Circles)
+ {
+ var region = new RotatedRect(circle.Center, new Size2f(circle.Radius * 2, circle.Radius * 2), 0);
+ ellipsePicker.Regions.Add(region);
+ }
+ }
+
+ ///
+ public override void Unload()
+ {
+ if (ellipsePicker != null)
+ {
+ ellipsePicker.Dispose();
+ ellipsePicker = null;
+ }
+ }
+}
+
+static class VisualizerHelper
+ {
+
+ internal static IObservable