Skip to content

Commit

Permalink
SerialCommutator now manages its own serial port
Browse files Browse the repository at this point in the history
- This is an attempt at getting around a possible issue with Bonsai's
  serial port libary implementation which is failing on some machines.
- Not sure if it will fix anything
- SerialCommutator is now used in AutoCommuator
  • Loading branch information
jonnew committed Oct 30, 2024
1 parent ea0e393 commit fde25d6
Show file tree
Hide file tree
Showing 3 changed files with 140 additions and 96 deletions.
104 changes: 8 additions & 96 deletions OpenEphys.Commutator/AutoCommutator.bonsai
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:rx="clr-namespace:Bonsai.Reactive;assembly=Bonsai.Core"
xmlns:commutator="clr-namespace:OpenEphys.Commutator;assembly=OpenEphys.Commutator"
xmlns:port="clr-namespace:Bonsai.IO.Ports;assembly=Bonsai.System"
xmlns="https://bonsai-rx.org/2018/workflow">
<Description>Control an Open Ephys commutator by using rotation angle measurements.</Description>
<Workflow>
<Nodes>
<Expression xsi:type="WorkflowInput">
Expand All @@ -16,9 +14,6 @@
<rx:Interval>PT0.1S</rx:Interval>
</Combinator>
</Expression>
<Expression xsi:type="ExternalizedMapping">
<Property Name="RotationAxis" />
</Expression>
<Expression xsi:type="Combinator">
<Combinator xsi:type="commutator:QuaternionToTwist">
<commutator:RotationAxis>
Expand All @@ -28,106 +23,23 @@
</commutator:RotationAxis>
</Combinator>
</Expression>
<Expression xsi:type="rx:Condition">
<Workflow>
<Nodes>
<Expression xsi:type="WorkflowInput">
<Name>Source1</Name>
</Expression>
<Expression xsi:type="NotEqual">
<Operand xsi:type="DoubleProperty">
<Value>0</Value>
</Operand>
</Expression>
<Expression xsi:type="WorkflowOutput" />
</Nodes>
<Edges>
<Edge From="0" To="1" Label="Source1" />
<Edge From="1" To="2" Label="Source1" />
</Edges>
</Workflow>
</Expression>
<Expression xsi:type="Format">
<Format>{{turn : {0}}}</Format>
<Selector>it</Selector>
</Expression>
<Expression xsi:type="ExternalizedMapping">
<Property Name="Value" DisplayName="LedEnable" Description="If true, the commutator's LED will show status information. Otherwise, it will turn off." />
</Expression>
<Expression xsi:type="Combinator">
<Combinator xsi:type="BooleanProperty">
<Value>false</Value>
</Combinator>
</Expression>
<Expression xsi:type="rx:Condition">
<Workflow>
<Nodes>
<Expression xsi:type="WorkflowInput">
<Name>Source1</Name>
</Expression>
<Expression xsi:type="WorkflowOutput" />
</Nodes>
<Edges>
<Edge From="0" To="1" Label="Source1" />
</Edges>
</Workflow>
</Expression>
<Expression xsi:type="Combinator">
<Combinator xsi:type="StringProperty">
<Value>{led: true}</Value>
</Combinator>
</Expression>
<Expression xsi:type="rx:Condition">
<Workflow>
<Nodes>
<Expression xsi:type="WorkflowInput">
<Name>Source1</Name>
</Expression>
<Expression xsi:type="BitwiseNot" />
<Expression xsi:type="WorkflowOutput" />
</Nodes>
<Edges>
<Edge From="0" To="1" Label="Source1" />
<Edge From="1" To="2" Label="Source1" />
</Edges>
</Workflow>
</Expression>
<Expression xsi:type="Combinator">
<Combinator xsi:type="StringProperty">
<Value>{led: false}</Value>
</Combinator>
</Expression>
<Expression xsi:type="Combinator">
<Combinator xsi:type="rx:Merge" />
</Expression>
<Expression xsi:type="ExternalizedMapping">
<Property Name="PortName" />
<Property Name="Enable" />
<Property Name="EnableLed" />
</Expression>
<Expression xsi:type="Combinator">
<Combinator xsi:type="port:SerialWriteLine">
<port:PortName />
<port:NewLine>\r\n</port:NewLine>
<Combinator xsi:type="commutator:SerialCommutator">
<commutator:Enable>true</commutator:Enable>
<commutator:EnableLed>true</commutator:EnableLed>
</Combinator>
</Expression>
<Expression xsi:type="WorkflowOutput" />
</Nodes>
<Edges>
<Edge From="0" To="1" Label="Source1" />
<Edge From="1" To="3" Label="Source1" />
<Edge From="2" To="3" Label="Source2" />
<Edge From="3" To="4" Label="Source1" />
<Edge From="4" To="5" Label="Source1" />
<Edge From="5" To="12" Label="Source1" />
<Edge From="6" To="7" Label="Source1" />
<Edge From="7" To="8" Label="Source1" />
<Edge From="7" To="10" Label="Source1" />
<Edge From="8" To="9" Label="Source1" />
<Edge From="9" To="12" Label="Source2" />
<Edge From="10" To="11" Label="Source1" />
<Edge From="11" To="12" Label="Source3" />
<Edge From="12" To="14" Label="Source1" />
<Edge From="13" To="14" Label="Source2" />
<Edge From="14" To="15" Label="Source1" />
<Edge From="1" To="2" Label="Source1" />
<Edge From="2" To="4" Label="Source1" />
<Edge From="3" To="4" Label="Source2" />
</Edges>
</Workflow>
</WorkflowBuilder>
41 changes: 41 additions & 0 deletions OpenEphys.Commutator/PortTypeConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System.ComponentModel;
using System.IO.Ports;
using System.Linq;
using Bonsai;
using Bonsai.Expressions;
using Bonsai.IO.Ports;

namespace OpenEphys.Commutator
{
class PortNameConverter : StringConverter
{
public override bool GetStandardValuesSupported(ITypeDescriptorContext context)
{
return true;
}

public override StandardValuesCollection GetStandardValues(ITypeDescriptorContext context)
{
if (context != null)
{
var workflowBuilder = (WorkflowBuilder)context.GetService(typeof(WorkflowBuilder));
if (workflowBuilder != null)
{
var portNames = (from builder in workflowBuilder.Workflow.Descendants()
where builder is not DisableBuilder
let createPort = ExpressionBuilder.GetWorkflowElement(builder) as CreateSerialPort
where createPort != null && !string.IsNullOrEmpty(createPort.PortName)
select !string.IsNullOrEmpty(createPort.Name) ? createPort.Name : createPort.PortName)
.Distinct()
.ToList();
if (portNames.Count > 0)
{
return new StandardValuesCollection(portNames);
}
}
}

return new StandardValuesCollection(SerialPort.GetPortNames());
}
}
}
91 changes: 91 additions & 0 deletions OpenEphys.Commutator/SerialCommutator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
using System;
using System.ComponentModel;
using System.IO.Ports;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using Bonsai;

namespace OpenEphys.Commutator
{
/// <summary>
/// Represents an operator controls an Open Ephys commutator by writing a JSON-encoded
/// representations of each element of the input sequence to a serial port and produces the
/// JSON-encoded command string.
/// </summary>
[Description("Controls an Open Ephys commutator using a serial port.")]
public class SerialCommutator : Combinator<double, string>
{

const string ConfigurationCategory = "Configuration";
const string AcquisitionCategory = "Acquisition";

readonly BehaviorSubject<bool> enabled = new(true);
readonly BehaviorSubject<bool> led = new(true);

/// <summary>
/// Gets or sets the name of the serial port.
/// </summary>
[Category(ConfigurationCategory)]
[TypeConverter(typeof(PortNameConverter))]
[Description("The name of the serial port.")]
public string PortName { get; set; }

/// <summary>
/// Gets or sets the commutator enable state.
/// </summary>
/// <remarks>
/// If true, the commutator will activate the motor and respond to turn commands. If false,
/// the motor driver will be deactivated and motion commands will be ignored.
/// </remarks>
[Category(AcquisitionCategory)]
[Description("If true, the commutator will be enabled. If false, it will disable the motor and ignore motion commands.")]
public bool Enable
{
get => enabled.Value;
set => enabled.OnNext(value);
}

/// <summary>
/// Gets or sets the commutator indication LED enable state.
/// </summary>
/// <remarks>
/// If true, the commutator indication LED turn on. If false, the indication LED will turn
/// off.
/// </remarks>
[Category(AcquisitionCategory)]
[Description("If true, the commutator indication LED turn on. If false, the indication LED will turn off.")]
public bool EnableLed
{
get => led.Value;
set => led.OnNext(value);
}

/// <summary>
/// Writes a JSON-encoded representations of each element of the input sequence, as well as
/// configuration property values, to a serial port.
/// </summary>
/// <param name="source">A sequence of motor turn values in units of full rotations.</param>
/// <returns>JSON-encoded command string sent to the commutator.</returns>
public override IObservable<string> Process(IObservable<double> source)
{
return Observable.Using(
() =>
{
var s = new SerialPort(PortName);
s.Open();
return s;
},
s =>
{
var turnCommands = source.Select(x => $"{{turn:{x}}}");
var enabledCommands = enabled.Select(x => x ? "true" : "false").Select(x => $"{{enable:{x}}}");
var ledCommands = led.Select(x => x ? "true" : "false").Select(x => $"{{led:{x}}}");
return turnCommands
.Merge(enabledCommands)
.Merge(ledCommands)
.Do(command => s.Write(command));
}
);
}
}
}

0 comments on commit fde25d6

Please sign in to comment.